diff --git a/elixir/apps/web/lib/web/endpoint.ex b/elixir/apps/web/lib/web/endpoint.ex index 34d592cf9..5313a8b2d 100644 --- a/elixir/apps/web/lib/web/endpoint.ex +++ b/elixir/apps/web/lib/web/endpoint.ex @@ -39,6 +39,7 @@ defmodule Web.Endpoint do socket "/live", Phoenix.LiveView.Socket, websocket: [ + check_origin: :conn, connect_info: [ :user_agent, :peer_data, diff --git a/elixir/config/runtime.exs b/elixir/config/runtime.exs index b15d81c9b..f1ef799d2 100644 --- a/elixir/config/runtime.exs +++ b/elixir/config/runtime.exs @@ -81,12 +81,6 @@ if config_env() == :prod do cookie_signing_salt: compile_config!(:cookie_signing_salt), cookie_encryption_salt: compile_config!(:cookie_encryption_salt) - config :web, Web.Auth, - platform_redirect_urls: %{ - "apple" => "firezone://handle_client_auth_callback", - "android" => "#{external_url_scheme}://#{external_url_host}/handle_client_auth_callback" - } - ############################### ##### API ##################### ############################### diff --git a/kotlin/android/README.md b/kotlin/android/README.md index 3f9cff923..c46720073 100644 --- a/kotlin/android/README.md +++ b/kotlin/android/README.md @@ -15,7 +15,7 @@ ruby server.rb 1. Add the following to a `./local.properties` file: ```gradle -sdk.dir=/path/to/your/ANROID_HOME +sdk.dir=/path/to/your/ANDROID_HOME ``` Replace `/path/to/your/ANDROID_HOME` with the path to your locally installed diff --git a/kotlin/android/app/build.gradle b/kotlin/android/app/build.gradle index 44d540529..8873afa3f 100644 --- a/kotlin/android/app/build.gradle +++ b/kotlin/android/app/build.gradle @@ -30,18 +30,24 @@ android { debug { debuggable true + def localProperties = new Properties() + localProperties.load(new FileInputStream(rootProject.file("local.properties"))) + buildConfigField("String", "TOKEN", "\"${localProperties.getProperty("token")}\"") + // Debug Config manifestPlaceholders.hostName = "app.firez.one" - buildConfigField("String", "AUTH_HOST", "\"localhost\"") - buildConfigField("String", "AUTH_SCHEME", "\"http\"") - buildConfigField("Integer", "AUTH_PORT", "8080") - buildConfigField("String", "CONTROL_PLANE_URL", "\"ws://localhost:8081/\"") + buildConfigField("String", "AUTH_HOST", "\"app.firez.one\"") + buildConfigField("String", "AUTH_SCHEME", "\"https\"") + buildConfigField("Integer", "AUTH_PORT", "443") + buildConfigField("String", "CONTROL_PLANE_URL", "\"wss://api.firez.one/\"") resValue "string", "app_name", "\"Firezone (Dev)\"" } release { proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + buildConfigField("String", "TOKEN", "null") + // Release Config manifestPlaceholders.hostName = "app.firezone.dev" buildConfigField("String", "AUTH_HOST", "\"app.firezone.dev\"") diff --git a/kotlin/android/app/src/main/AndroidManifest.xml b/kotlin/android/app/src/main/AndroidManifest.xml index 6ed3c8f90..9fb314808 100644 --- a/kotlin/android/app/src/main/AndroidManifest.xml +++ b/kotlin/android/app/src/main/AndroidManifest.xml @@ -3,11 +3,18 @@ + + + + + + + - + @@ -60,7 +67,7 @@ android:exported="false" /> diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/BaseUrlInterceptor.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/BaseUrlInterceptor.kt index 78d440ac7..947a48511 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/BaseUrlInterceptor.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/BaseUrlInterceptor.kt @@ -1,11 +1,13 @@ package dev.firezone.android.core import android.content.SharedPreferences +import dev.firezone.android.BuildConfig import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.Response +import java.lang.Exception -private const val PORTAL_URL_KEY = "portalUrl" +private const val ACCOUNT_ID_KEY = "accountId" internal class BaseUrlInterceptor( private val sharedPreferences: SharedPreferences @@ -13,11 +15,12 @@ internal class BaseUrlInterceptor( override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() - val baseUrl = sharedPreferences.getString(PORTAL_URL_KEY, "").orEmpty().toHttpUrl() + val accountId = sharedPreferences.getString(ACCOUNT_ID_KEY, "") ?: "" val newUrl = originalRequest.url.newBuilder() - .scheme(baseUrl.scheme) - .host(baseUrl.host) - .port(baseUrl.port) + .scheme(BuildConfig.AUTH_SCHEME) + .host(BuildConfig.AUTH_HOST) + .port(BuildConfig.AUTH_PORT) + .addPathSegment(accountId) .build() val newRequest = originalRequest.newBuilder() .url(newUrl) diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepository.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepository.kt index 6bc688855..9068b2f75 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepository.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepository.kt @@ -8,9 +8,9 @@ internal interface PreferenceRepository { fun getConfig(): Flow - fun savePortalUrl(value: String): Flow + fun saveAccountId(value: String): Flow - fun saveJWT(value: String): Flow + fun saveToken(value: String): Flow fun saveIsConnectedSync(value: Boolean) diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt index e73e8458b..3dee491c3 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt @@ -14,29 +14,29 @@ internal class PreferenceRepositoryImpl @Inject constructor( ) : PreferenceRepository { override fun getConfigSync(): Config = Config( - portalUrl = sharedPreferences.getString(PORTAL_URL_KEY, null), + accountId = sharedPreferences.getString(ACCOUNT_ID_KEY, null), isConnected = sharedPreferences.getBoolean(IS_CONNECTED_KEY, false), - jwt = sharedPreferences.getString(JWT_KEY, null), + token = sharedPreferences.getString(TOKEN_KEY, null), ) override fun getConfig(): Flow = flow { emit(getConfigSync()) }.flowOn(coroutineDispatcher) - override fun savePortalUrl(value: String): Flow = flow { + override fun saveAccountId(value: String): Flow = flow { emit( sharedPreferences .edit() - .putString(PORTAL_URL_KEY, value) + .putString(ACCOUNT_ID_KEY, value) .apply() ) }.flowOn(coroutineDispatcher) - override fun saveJWT(value: String): Flow = flow { + override fun saveToken(value: String): Flow = flow { emit( sharedPreferences .edit() - .putString(JWT_KEY, value) + .putString(TOKEN_KEY, value) .apply() ) }.flowOn(coroutineDispatcher) @@ -60,9 +60,9 @@ internal class PreferenceRepositoryImpl @Inject constructor( }.flowOn(coroutineDispatcher) companion object { - private const val PORTAL_URL_KEY = "portalUrl" + private const val ACCOUNT_ID_KEY = "accountId" private const val IS_CONNECTED_KEY = "isConnected" - private const val JWT_KEY = "jwt" + private const val TOKEN_KEY = "token" private const val CSRF_KEY = "csrf" } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/model/Config.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/model/Config.kt index 893284e26..41e9a4909 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/model/Config.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/model/Config.kt @@ -1,7 +1,7 @@ package dev.firezone.android.core.data.model internal data class Config( - val portalUrl: String?, + val accountId: String?, val isConnected: Boolean = false, - val jwt: String?, + val token: String?, ) diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/debug/DevSuite.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/debug/DevSuite.kt new file mode 100644 index 000000000..76aa27908 --- /dev/null +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/debug/DevSuite.kt @@ -0,0 +1,23 @@ +package dev.firezone.android.core.debug + +import android.content.Intent +import androidx.fragment.app.FragmentActivity +import dev.firezone.android.BuildConfig +import dev.firezone.android.core.data.PreferenceRepository +import dev.firezone.android.core.presentation.MainActivity +import kotlinx.coroutines.flow.collect +import javax.inject.Inject + +internal class DevSuite @Inject constructor( + private val repository: PreferenceRepository +) { + + suspend fun signInWithDebugUser(activity: FragmentActivity) { + repository.saveAccountId("firezone").collect() + repository.saveToken(BuildConfig.TOKEN).collect() + + val intent = Intent(activity, MainActivity::class.java) + activity.startActivity(intent) + activity.finish() + } +} diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/DebugUserUseCase.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/DebugUserUseCase.kt deleted file mode 100644 index e05f52d35..000000000 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/DebugUserUseCase.kt +++ /dev/null @@ -1,18 +0,0 @@ -package dev.firezone.android.core.domain.preference - -import dev.firezone.android.BuildConfig -import dev.firezone.android.core.data.PreferenceRepository -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flowOf - -internal class DebugUserUseCase @Inject constructor( - private val repository: PreferenceRepository -) { - suspend operator fun invoke(): Flow { - repository.savePortalUrl("${BuildConfig.AUTH_SCHEME}://${BuildConfig.AUTH_HOST}:${BuildConfig.AUTH_PORT}/team-id/").collect() - repository.saveJWT("eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjFMN3k3RUM1T3VSZUNNNnIzX2l0MXNJbjNqeTdiZ2JPSVB3Z0xoejV0SGsifQ.eyJpc3MiOiJodHRwczovL2ZpcmV6b25lLmxvY2FsIiwic3ViIjoidGVzdEBmaXJlem9uZS5kZXYiLCJjbGllbnRfaWQiOiJmaXJlem9uZSIsImV4cCI6MTY3MjgzNzU0NCwiaWF0IjoxNjY4MTMzOTQ0fQ.NvvGWvrMvshKp5MYycDWXa8gQ41Ptrr_nIKzfPWzci8fxwmQYJ5hL1vQpdmECtR5NeGv7qTavi6yq19Kqmwrn27numDXaET2b2xypGbFOm1TJmcbZ4Rxy_-FfAeer-7YNhW_p83a0N7UoPORpxVs8hp76sKe_klfmoM830frrLzeqz0VYxBZXhPiTAlqiG39cY74yk-drxLY4xeRBAXh_TdewrkRkPpTpsrXFz60fF5P8AaRnUKlDSRq89ZIC-zo2ysJsXIZLrJpfcNgkscohZZfXfCLIFaiGvZseW0XHWfq-V5HOXVf09-57GHdmCr-AAJ7sqpnPrSBvg7EDBvylg").collect() - return flowOf () - } -} diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SavePortalUrlUseCase.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveAccountIdUseCase.kt similarity index 60% rename from kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SavePortalUrlUseCase.kt rename to kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveAccountIdUseCase.kt index e915eb3f4..710211425 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SavePortalUrlUseCase.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveAccountIdUseCase.kt @@ -4,8 +4,8 @@ import dev.firezone.android.core.data.PreferenceRepository import javax.inject.Inject import kotlinx.coroutines.flow.Flow -internal class SavePortalUrlUseCase @Inject constructor( +internal class SaveAccountIdUseCase @Inject constructor( private val repository: PreferenceRepository ) { - operator fun invoke(portalUrl: String): Flow = repository.savePortalUrl(portalUrl) + operator fun invoke(accountId: String): Flow = repository.saveAccountId(accountId) } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveJWTUseCase.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveTokenUseCase.kt similarity index 63% rename from kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveJWTUseCase.kt rename to kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveTokenUseCase.kt index 7596b5cc1..80e7cb904 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveJWTUseCase.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveTokenUseCase.kt @@ -4,8 +4,8 @@ import dev.firezone.android.core.data.PreferenceRepository import javax.inject.Inject import kotlinx.coroutines.flow.Flow -internal class SaveJWTUseCase @Inject constructor( +internal class SaveTokenUseCase @Inject constructor( private val repository: PreferenceRepository ) { - operator fun invoke(jwt: String): Flow = repository.saveJWT(jwt) + operator fun invoke(token: String): Flow = repository.saveToken(token) } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/presentation/MainActivity.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/presentation/MainActivity.kt index d58381a5f..36241a648 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/presentation/MainActivity.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/presentation/MainActivity.kt @@ -7,8 +7,6 @@ import androidx.navigation.fragment.NavHostFragment import dev.firezone.android.R import dagger.hilt.android.AndroidEntryPoint -private const val DEEP_LINK_KEY = "deepLink" - @AndroidEntryPoint internal class MainActivity : AppCompatActivity(R.layout.activity_main) { @@ -18,9 +16,6 @@ internal class MainActivity : AppCompatActivity(R.layout.activity_main) { val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainer) as NavHostFragment val navController = navHostFragment.navController - - val deepLink = intent.extras?.getString(DEEP_LINK_KEY).orEmpty() - if (deepLink.isNotEmpty()) navController.navigate(Uri.parse(deepLink)) } @Deprecated("Deprecated in Java") diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkHandlerActivity.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkHandlerActivity.kt index 1c7972752..11de81793 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkHandlerActivity.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkHandlerActivity.kt @@ -1,13 +1,21 @@ package dev.firezone.android.features.applink.ui +import android.content.Intent import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.Log import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import dagger.hilt.android.AndroidEntryPoint +import dev.firezone.android.BuildConfig import dev.firezone.android.R +import dev.firezone.android.core.presentation.MainActivity import dev.firezone.android.databinding.ActivityAppLinkHandlerBinding +import dev.firezone.android.features.session.backend.SessionManager +import dev.firezone.android.features.splash.ui.SplashFragmentDirections +import dev.firezone.android.tunnel.TunnelManager +import dev.firezone.android.tunnel.TunnelSession +import javax.inject.Inject @AndroidEntryPoint class AppLinkHandlerActivity : AppCompatActivity(R.layout.activity_app_link_handler) { @@ -26,12 +34,18 @@ class AppLinkHandlerActivity : AppCompatActivity(R.layout.activity_app_link_hand private fun setupActionObservers() { viewModel.actionLiveData.observe(this) { action -> when (action) { - is AppLinkViewModel.ViewAction.AuthFlowComplete -> { - // Continue with onboarding + AppLinkViewModel.ViewAction.AuthFlowComplete -> { + // TODO: Continue starting the session showing sessionFragment Log.d("AppLinkHandlerActivity", "AuthFlowComplete") + + val intent = Intent(this@AppLinkHandlerActivity, MainActivity::class.java) + this@AppLinkHandlerActivity.startActivity(intent) + this@AppLinkHandlerActivity.finish() + } + AppLinkViewModel.ViewAction.ShowError -> showError() + else -> { + Log.d("AppLinkHandlerActivity", "Unhandled action: $action") } - is AppLinkViewModel.ViewAction.ShowError -> showError() - else -> {} } } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkViewModel.kt index b0d62ed4b..4f0739810 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/applink/ui/AppLinkViewModel.kt @@ -7,8 +7,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dev.firezone.android.core.domain.preference.SaveJWTUseCase +import dev.firezone.android.BuildConfig +import dev.firezone.android.core.domain.preference.SaveTokenUseCase import dev.firezone.android.core.domain.preference.ValidateCsrfTokenUseCase +import dev.firezone.android.features.session.backend.SessionManager +import dev.firezone.android.tunnel.TunnelLogger +import dev.firezone.android.tunnel.TunnelManager +import dev.firezone.android.tunnel.TunnelSession +import kotlinx.coroutines.flow.collect import javax.inject.Inject import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -16,27 +22,41 @@ import kotlinx.coroutines.launch @HiltViewModel internal class AppLinkViewModel @Inject constructor( private val validateCsrfTokenUseCase: ValidateCsrfTokenUseCase, - private val saveJWTUseCase: SaveJWTUseCase, + private val saveTokenUseCase: SaveTokenUseCase, ) : ViewModel() { - + private val callback: TunnelManager = TunnelManager() private val actionMutableLiveData = MutableLiveData() val actionLiveData: LiveData = actionMutableLiveData fun parseAppLink(intent: Intent) { + Log.d("AppLinkViewModel", "Parsing app link...") viewModelScope.launch { + Log.d("AppLinkViewModel", "viewmodelScope.launch") when (intent.data?.lastPathSegment) { PATH_CALLBACK -> { + Log.d("AppLinkViewModel", "PATH_CALLBACK") intent.data?.getQueryParameter(QUERY_CLIENT_CSRF_TOKEN)?.let { csrfToken -> + Log.d("AppLinkViewModel", "csrfToken: $csrfToken") if (validateCsrfTokenUseCase(csrfToken).firstOrNull() == true) { - val jwtToken = intent.data?.getQueryParameter(QUERY_CLIENT_AUTH_TOKEN) ?: "" - saveJWTUseCase(jwtToken) - - actionMutableLiveData.postValue(ViewAction.AuthFlowComplete) + Log.d("AppLinkViewModel", "Valid CSRF token. Continuing to save token...") + } else { + Log.d("AppLinkViewModel", "Invalid CSRF token! Continuing to save token anyway...") } + intent.data?.getQueryParameter(QUERY_CLIENT_AUTH_TOKEN)?.let { token -> + if (token.isNotBlank()) { + // TODO: Don't log auth token + Log.d("AppLinkViewModel", "Found valid auth token in response") + saveTokenUseCase(token).collect() + } else { + Log.d("AppLinkViewModel", "Didn't find auth token in response!") + } + } + + actionMutableLiveData.postValue(ViewAction.AuthFlowComplete) } } else -> { - Log.d("AppLink", "Unknown path segment: ${intent.data?.lastPathSegment}") + Log.d("AppLinkViewModel", "Unknown path segment: ${intent.data?.lastPathSegment}") } } } @@ -46,6 +66,8 @@ internal class AppLinkViewModel @Inject constructor( private const val PATH_CALLBACK = "handle_client_auth_callback" private const val QUERY_CLIENT_CSRF_TOKEN = "client_csrf_token" private const val QUERY_CLIENT_AUTH_TOKEN = "client_auth_token" + private const val QUERY_ACTOR_NAME = "actor_name" + private const val QUERY_IDENTITY_PROVIDER_IDENTIFIER = "identity_provider_identifier" } internal sealed class ViewAction { diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthActivity.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthActivity.kt index d01b5f1dd..5c5729d49 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthActivity.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthActivity.kt @@ -1,5 +1,6 @@ package dev.firezone.android.features.auth.ui +import android.util.Log import android.net.Uri import androidx.appcompat.app.AppCompatActivity import android.os.Bundle @@ -8,6 +9,7 @@ import androidx.appcompat.app.AlertDialog import androidx.browser.customtabs.CustomTabsIntent import dagger.hilt.android.AndroidEntryPoint import dev.firezone.android.R +import dev.firezone.android.util.CustomTabsHelper import dev.firezone.android.databinding.ActivityAuthBinding @AndroidEntryPoint @@ -27,6 +29,7 @@ class AuthActivity : AppCompatActivity(R.layout.activity_auth) { private fun setupActionObservers() { viewModel.actionLiveData.observe(this) { action -> + Log.d("AuthActivity", "setupActionObservers: $action") when (action) { is AuthViewModel.ViewAction.LaunchAuthFlow -> setupWebView(action.url) is AuthViewModel.ViewAction.ShowError -> showError() @@ -37,10 +40,9 @@ class AuthActivity : AppCompatActivity(R.layout.activity_auth) { private fun setupWebView(url: String) { val intent = CustomTabsIntent.Builder().build() - intent.intent.setPackage("com.android.chrome") + intent.intent.setPackage(CustomTabsHelper.getPackageNameToUse(this@AuthActivity)) intent.launchUrl(this@AuthActivity, Uri.parse(url)) } - private fun showError() { AlertDialog.Builder(this) .setTitle(R.string.error_dialog_title) diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt index 637059255..ef511f511 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/auth/ui/AuthViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dev.firezone.android.core.domain.preference.GetConfigUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import dev.firezone.android.BuildConfig import dev.firezone.android.core.domain.auth.GetCsrfTokenUseCase import javax.inject.Inject import kotlinx.coroutines.flow.firstOrNull @@ -24,14 +25,14 @@ internal class AuthViewModel @Inject constructor( fun startAuthFlow() = try { viewModelScope.launch { val config = getConfigUseCase() - .firstOrNull() ?: throw Exception("Config cannot be null") + .firstOrNull() ?: throw Exception("config cannot be null") - val token = getCsrfTokenUseCase() - .firstOrNull() ?: throw Exception("Token cannot be null") + val csrfToken = getCsrfTokenUseCase() + .firstOrNull() ?: throw Exception("csrfToken cannot be null") actionMutableLiveData.postValue( ViewAction.LaunchAuthFlow( - url = "${config.portalUrl}/sign_in?client_csrf_token=$token&client_platform=android" + url = "$AUTH_URL${config.accountId}/sign_in?client_csrf_token=${config.token}&client_platform=android" ) ) } @@ -39,6 +40,10 @@ internal class AuthViewModel @Inject constructor( actionMutableLiveData.postValue(ViewAction.ShowError) } + companion object { + val AUTH_URL = "${BuildConfig.AUTH_SCHEME}://${BuildConfig.AUTH_HOST}:${BuildConfig.AUTH_PORT}/" + } + internal sealed class ViewAction { data class LaunchAuthFlow(val url: String) : ViewAction() diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionCallbackImpl.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionCallbackImpl.kt deleted file mode 100644 index 26c49ea51..000000000 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionCallbackImpl.kt +++ /dev/null @@ -1,48 +0,0 @@ -package dev.firezone.android.features.session.backend - -import android.util.Log -import dev.firezone.connlib.SessionCallback - -class SessionCallbackImpl: SessionCallback { - - override fun onUpdateResources(resources: String) { - // TODO: Call into client app to update resources list and routing table - Log.d(TAG, "onUpdateResources: $resources") - } - - override fun onSetInterfaceConfig( - tunnelAddressIPv4: String, - tunnelAddressIPv6: String, - dnsAddress: String, - dnsFallbackStrategy: String - ) { - Log.d(TAG, "onSetInterfaceConfig: [IPv4:$tunnelAddressIPv4] [IPv6:$tunnelAddressIPv6] [dns:$dnsAddress]") - } - - override fun onTunnelReady(): Boolean { - Log.d(TAG, "onTunnelReady") - return true - } - - override fun onError(error: String): Boolean { - Log.d(TAG, "onError: $error") - return true - } - - override fun onAddRoute(cidrAddress: String) { - Log.d(TAG, "onAddRoute: $cidrAddress") - } - - override fun onRemoveRoute(cidrAddress: String) { - Log.d(TAG, "onRemoveRoute: $cidrAddress") - } - - override fun onDisconnect(error: String?): Boolean { - Log.d(TAG, "onDisconnect $error") - return true - } - - companion object { - private const val TAG: String = "ConnlibCallback" - } -} diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionManager.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionManager.kt index 25f6e4709..ba45ac98c 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionManager.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionManager.kt @@ -1,33 +1,45 @@ package dev.firezone.android.features.session.backend +import android.net.VpnService import android.util.Log import dev.firezone.android.BuildConfig import dev.firezone.android.core.domain.preference.GetConfigUseCase import dev.firezone.android.core.domain.preference.SaveIsConnectedUseCase -import dev.firezone.connlib.Logger -import dev.firezone.connlib.Session +import dev.firezone.android.tunnel.TunnelCallbacks +import dev.firezone.android.tunnel.TunnelLogger +import dev.firezone.android.tunnel.TunnelSession +import dev.firezone.android.tunnel.TunnelManager +import dev.firezone.android.tunnel.TunnelService import javax.inject.Inject internal class SessionManager @Inject constructor( private val getConfigUseCase: GetConfigUseCase, private val saveIsConnectedUseCase: SaveIsConnectedUseCase, ) { - private val callback: SessionCallbackImpl = SessionCallbackImpl() + private val callback: TunnelManager = TunnelManager() fun connect() { try { val config = getConfigUseCase.sync() - if (config.portalUrl != null && config.jwt != null) { - Log.d("Connlib", "portalUrl: ${config.portalUrl}") - Log.d("Connlib", "jwt: ${config.jwt}") + Log.d("Connlib", "accountId: ${config.accountId}") + Log.d("Connlib", "token: ${config.token}") - sessionPtr = Session.connect( - BuildConfig.CONTROL_PLANE_URL, - config.jwt, - callback - ) - setConnectionStatus(true) + if (config.accountId != null && config.token != null) { + Log.d("Connlib", "Attempting to establish VPN connection...") + buildVpnService().establish()?.let { + Log.d("Connlib", "VPN connection established! Attempting to start connlib session...") + sessionPtr = TunnelSession.connect( + it.detachFd(), + BuildConfig.CONTROL_PLANE_URL, + config.token, + TunnelCallbacks() + ) + Log.d("Connlib", "connlib session started! sessionPtr: $sessionPtr") + setConnectionStatus(true) + } ?: let { + Log.d("Connlib", "Failed to build VpnService") + } } } catch (exception: Exception) { Log.e("Connection error:", exception.message.toString()) @@ -36,7 +48,7 @@ internal class SessionManager @Inject constructor( fun disconnect() { try { - Session.disconnect(sessionPtr!!) + TunnelSession.disconnect(sessionPtr!!) setConnectionStatus(false) } catch (exception: Exception) { Log.e("Disconnection error:", exception.message.toString()) @@ -47,12 +59,31 @@ internal class SessionManager @Inject constructor( saveIsConnectedUseCase.sync(value) } + private fun buildVpnService(): VpnService.Builder = + TunnelService().Builder().apply { + // Add a dummy address for now. Needed for the "establish" call to succeed. + // TODO: Remove these in favor of connecting the TunnelSession *without* the fd, and then + // returning the fd in the onSetInterfaceConfig callback. This is being worked on by @conectado + addAddress("100.100.111.1", 32) + addAddress("fd00:2021:1111::100:100:111:1", 128) + + // TODO: These are the staging Resources. Remove these in favor of the onUpdateResources callback. + addRoute("172.31.93.123", 32) + addRoute("172.31.83.10", 32) + addRoute("172.31.82.179", 32) + + setSession("Firezone VPN") + setMtu(1280) + } + internal companion object { var sessionPtr: Long? = null init { + Log.d("Connlib","Attempting to load library from main app...") System.loadLibrary("connlib") - Logger.init() Log.d("Connlib","Library loaded from main app!") + TunnelLogger.init() + Log.d("Connlib","Connlib Logger initialized!") } } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionFragment.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionFragment.kt index 2fc7c41dc..7593d6dce 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionFragment.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionFragment.kt @@ -22,7 +22,7 @@ internal class SessionFragment : Fragment(R.layout.fragment_session) { setupButtonListeners() setupActionObservers() - Log.d("SessionViewModel", "Starting session...") + Log.d("SessionFragment", "Starting session...") viewModel.startSession() } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/onboarding/ui/OnboardingFragment.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt similarity index 67% rename from kotlin/android/app/src/main/java/dev/firezone/android/features/onboarding/ui/OnboardingFragment.kt rename to kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt index 857ee8af5..3df8c2611 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/onboarding/ui/OnboardingFragment.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsFragment.kt @@ -1,4 +1,4 @@ -package dev.firezone.android.features.onboarding.ui +package dev.firezone.android.features.settings.ui import android.content.Intent import android.os.Bundle @@ -7,27 +7,28 @@ import android.view.inputmethod.EditorInfo import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import dev.firezone.android.R -import dev.firezone.android.databinding.FragmentOnboardingBinding +import dev.firezone.android.databinding.FragmentSettingsBinding import dagger.hilt.android.AndroidEntryPoint import dev.firezone.android.BuildConfig import dev.firezone.android.features.auth.ui.AuthActivity @AndroidEntryPoint -internal class OnboardingFragment : Fragment(R.layout.fragment_onboarding) { +internal class SettingsFragment : Fragment(R.layout.fragment_settings) { - private lateinit var binding: FragmentOnboardingBinding - private val viewModel: OnboardingViewModel by viewModels() + private lateinit var binding: FragmentSettingsBinding + private val viewModel: SettingsViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding = FragmentOnboardingBinding.bind(view) + binding = FragmentSettingsBinding.bind(view) setupViews() setupStateObservers() setupActionObservers() setupButtonListener() - viewModel.getPortalUrl() + viewModel.getAccountId() } private fun setupStateObservers() { @@ -41,13 +42,10 @@ internal class OnboardingFragment : Fragment(R.layout.fragment_onboarding) { private fun setupActionObservers() { viewModel.actionLiveData.observe(viewLifecycleOwner) { action -> when (action) { - OnboardingViewModel.ViewAction.NavigateToSignInFragment -> startActivity( - Intent( - requireContext(), - AuthActivity::class.java - ) + is SettingsViewModel.ViewAction.NavigateToSignInFragment -> findNavController().navigate( + R.id.signInFragment ) - is OnboardingViewModel.ViewAction.FillPortalUrl -> { + is SettingsViewModel.ViewAction.FillAccountId -> { binding.etInput.apply { setText(action.value) isCursorVisible = false @@ -59,7 +57,7 @@ internal class OnboardingFragment : Fragment(R.layout.fragment_onboarding) { private fun setupViews() { binding.ilUrlInput.apply { - prefixText = "${BuildConfig.AUTH_SCHEME}://${BuildConfig.AUTH_HOST}:${BuildConfig.AUTH_PORT}/" + prefixText = SettingsViewModel.AUTH_URL } binding.etInput.apply { @@ -72,7 +70,7 @@ internal class OnboardingFragment : Fragment(R.layout.fragment_onboarding) { } binding.btLogin.setOnClickListener { - viewModel.onSaveOnboardingCompleted() + viewModel.onSaveSettingsCompleted() } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/onboarding/ui/OnboardingViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt similarity index 69% rename from kotlin/android/app/src/main/java/dev/firezone/android/features/onboarding/ui/OnboardingViewModel.kt rename to kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt index d46b30e9d..08c855f98 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/onboarding/ui/OnboardingViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/settings/ui/SettingsViewModel.kt @@ -1,4 +1,4 @@ -package dev.firezone.android.features.onboarding.ui +package dev.firezone.android.features.settings.ui import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -6,14 +6,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dev.firezone.android.core.domain.preference.GetConfigUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import dev.firezone.android.core.domain.preference.SavePortalUrlUseCase +import dev.firezone.android.core.domain.preference.SaveAccountIdUseCase +import dev.firezone.android.BuildConfig import javax.inject.Inject import kotlinx.coroutines.launch @HiltViewModel -internal class OnboardingViewModel @Inject constructor( +internal class SettingsViewModel @Inject constructor( private val getConfigUseCase: GetConfigUseCase, - private val savePortalUrlUseCase: SavePortalUrlUseCase, + private val saveAccountIdUseCase: SaveAccountIdUseCase, ) : ViewModel() { private val stateMutableLiveData = MutableLiveData() @@ -24,19 +25,19 @@ internal class OnboardingViewModel @Inject constructor( private var input = "" - fun getPortalUrl() { + fun getAccountId() { viewModelScope.launch { getConfigUseCase().collect { actionMutableLiveData.postValue( - ViewAction.FillPortalUrl(it.portalUrl.orEmpty()) + ViewAction.FillAccountId(it.accountId.orEmpty()) ) } } } - fun onSaveOnboardingCompleted() { + fun onSaveSettingsCompleted() { viewModelScope.launch { - savePortalUrlUseCase(input).collect { + saveAccountIdUseCase(input).collect { actionMutableLiveData.postValue(ViewAction.NavigateToSignInFragment) } } @@ -51,9 +52,13 @@ internal class OnboardingViewModel @Inject constructor( ) } + companion object { + val AUTH_URL = "${BuildConfig.AUTH_SCHEME}://${BuildConfig.AUTH_HOST}:${BuildConfig.AUTH_PORT}/" + } + internal sealed class ViewAction { object NavigateToSignInFragment : ViewAction() - data class FillPortalUrl(val value: String) : ViewAction() + data class FillAccountId(val value: String) : ViewAction() } internal data class ViewState( diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/signin/ui/SignInFragment.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/signin/ui/SignInFragment.kt index 477c1bfdd..5c31984a3 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/signin/ui/SignInFragment.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/signin/ui/SignInFragment.kt @@ -1,48 +1,57 @@ package dev.firezone.android.features.signin.ui +import android.content.Intent +import android.util.Log import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint import dev.firezone.android.R +import dev.firezone.android.core.debug.DevSuite import dev.firezone.android.databinding.FragmentSignInBinding +import dev.firezone.android.features.auth.ui.AuthActivity import dev.firezone.android.features.splash.ui.SplashFragmentDirections +import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint internal class SignInFragment : Fragment(R.layout.fragment_sign_in) { private lateinit var binding: FragmentSignInBinding private val viewModel: SignInViewModel by viewModels() + @Inject + lateinit var devSuite: DevSuite + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding = FragmentSignInBinding.bind(view) - setupActionObservers() + Log.d("SignInFragment", "Showing sign in...") setupButtonListener() } - private fun setupActionObservers() { - viewModel.actionLiveData.observe(viewLifecycleOwner) { action -> - when (action) { - SignInViewModel.SignInViewAction.NavigateToAuthActivity -> findNavController().navigate( - R.id.sessionFragment - ) - } - } - } - private fun setupButtonListener() { with(binding) { + btDebugUser.setOnClickListener { + lifecycleScope.launch { + devSuite.signInWithDebugUser(requireActivity()) + } + } btSignIn.setOnClickListener { - findNavController().navigate( - R.id.sessionFragment + startActivity( + Intent( + requireContext(), + AuthActivity::class.java + ) ) + requireActivity().finish() } btSettings.setOnClickListener { findNavController().navigate( - SplashFragmentDirections.navigateToOnboardingFragment() + SplashFragmentDirections.navigateToSettingsFragment() ) } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashFragment.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashFragment.kt index 95829aa1e..f0a02f5cb 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashFragment.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashFragment.kt @@ -35,8 +35,8 @@ internal class SplashFragment : Fragment(R.layout.fragment_splash) { SplashViewModel.ViewAction.NavigateToSignInFragment -> findNavController().navigate( R.id.signInFragment ) - SplashViewModel.ViewAction.NavigateToOnboardingFragment -> findNavController().navigate( - R.id.onboardingFragment + SplashViewModel.ViewAction.NavigateToSettingsFragment -> findNavController().navigate( + R.id.settingsFragment ) SplashViewModel.ViewAction.NavigateToSessionFragment -> findNavController().navigate( R.id.sessionFragment diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt index 107cf7192..2ad163a77 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/splash/ui/SplashViewModel.kt @@ -8,7 +8,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dev.firezone.android.core.domain.preference.GetConfigUseCase import dagger.hilt.android.lifecycle.HiltViewModel -import dev.firezone.android.core.domain.preference.DebugUserUseCase import javax.inject.Inject import kotlinx.coroutines.delay import kotlinx.coroutines.flow.catch @@ -19,15 +18,12 @@ private const val REQUEST_DELAY = 1000L @HiltViewModel internal class SplashViewModel @Inject constructor( private val useCase: GetConfigUseCase, - private val debugUserUseCase: DebugUserUseCase, ) : ViewModel() { private val actionMutableLiveData = MutableLiveData() val actionLiveData: LiveData = actionMutableLiveData internal fun checkUserState(context: Context) { viewModelScope.launch { - //debugUserUseCase() // sets dummy team-id and token - delay(REQUEST_DELAY) if (!hasVpnPermissions(context)) { actionMutableLiveData.postValue(ViewAction.NavigateToVpnPermission) @@ -37,9 +33,9 @@ internal class SplashViewModel @Inject constructor( Log.e("Error", it.message.toString()) } .collect { user -> - if (user.portalUrl.isNullOrEmpty()) { - actionMutableLiveData.postValue(ViewAction.NavigateToOnboardingFragment) - } else if (user.jwt.isNullOrBlank()) { + if (user.accountId.isNullOrEmpty()) { + actionMutableLiveData.postValue(ViewAction.NavigateToSettingsFragment) + } else if (user.token.isNullOrBlank()) { actionMutableLiveData.postValue(ViewAction.NavigateToSignInFragment) } else { actionMutableLiveData.postValue(ViewAction.NavigateToSessionFragment) @@ -55,7 +51,7 @@ internal class SplashViewModel @Inject constructor( internal sealed class ViewAction { object NavigateToVpnPermission : ViewAction() - object NavigateToOnboardingFragment : ViewAction() + object NavigateToSettingsFragment : ViewAction() object NavigateToSignInFragment : ViewAction() object NavigateToSessionFragment : ViewAction() } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/Tunnel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/Tunnel.kt new file mode 100644 index 000000000..1fb3d137c --- /dev/null +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/Tunnel.kt @@ -0,0 +1,12 @@ +package dev.firezone.android.tunnel + +class Tunnel( + val config: TunnelConfig, + var state: State = State.Down +) { + + sealed interface State { + object Up: State + object Down: State + } +} diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelCallbacks.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelCallbacks.kt new file mode 100644 index 000000000..0cdfb26cc --- /dev/null +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelCallbacks.kt @@ -0,0 +1,49 @@ +package dev.firezone.android.tunnel + +import android.util.Log + +class TunnelCallbacks { + fun onUpdateResources(resourceListJSON: String) { + // TODO: Call into client app to update resources list and routing table + Log.d(TunnelCallbacks.TAG, "onUpdateResources: $resourceListJSON") + } + + fun onSetInterfaceConfig( + tunnelAddressIPv4: String, + tunnelAddressIPv6: String, + dnsAddress: String, + dnsFallbackStrategy: String, + ) { + Log.d(TunnelCallbacks.TAG, "onSetInterfaceConfig: [IPv4:$tunnelAddressIPv4] [IPv6:$tunnelAddressIPv6] [dns:$dnsAddress] [dnsFallbackStrategy:$dnsFallbackStrategy]") + } + + fun onTunnelReady(): Boolean { + Log.d(TunnelCallbacks.TAG, "onTunnelReady") + + return true + } + + fun onError(error: String): Boolean { + Log.d(TunnelCallbacks.TAG, "onError: $error") + + return true + } + + fun onAddRoute(cidrAddress: String) { + Log.d(TunnelCallbacks.TAG, "onAddRoute: $cidrAddress") + } + + fun onRemoveRoute(cidrAddress: String) { + Log.d(TunnelCallbacks.TAG, "onRemoveRoute: $cidrAddress") + } + + fun onDisconnect(error: String?): Boolean { + Log.d(TunnelCallbacks.TAG, "onDisconnect $error") + + return true + } + + companion object { + private const val TAG = "TunnelCallbacks" + } +} diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelConfig.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelConfig.kt new file mode 100644 index 000000000..076e360ab --- /dev/null +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelConfig.kt @@ -0,0 +1,8 @@ +package dev.firezone.android.tunnel + +data class TunnelConfig ( + val tunnelAddressIPv4: String, + val tunnelAddressIPv6: String, + val dnsAddress: String, + val dnsFallbackStrategy: String, +) diff --git a/rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/SessionCallback.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelListener.kt similarity index 86% rename from rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/SessionCallback.kt rename to kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelListener.kt index a995da388..43db6e4e5 100644 --- a/rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/SessionCallback.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelListener.kt @@ -1,6 +1,6 @@ -package dev.firezone.connlib +package dev.firezone.android.tunnel -interface SessionCallback { +interface TunnelListener { fun onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, dnsFallbackStrategy: String) diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelLogger.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelLogger.kt new file mode 100644 index 000000000..d8fd982f6 --- /dev/null +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelLogger.kt @@ -0,0 +1,5 @@ +package dev.firezone.android.tunnel + +object TunnelLogger { + external fun init() +} diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt new file mode 100644 index 000000000..d6946fda7 --- /dev/null +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt @@ -0,0 +1,100 @@ +package dev.firezone.android.tunnel + +import android.util.Log +import java.lang.ref.WeakReference + +class TunnelManager { + + private var activeTunnel: Tunnel? = null + + private val listeners: MutableSet> = mutableSetOf() + + private val callback: TunnelListener = object: TunnelListener { + override fun onUpdateResources(resourceListJSON: String) { + // TODO: Call into client app to update resources list and routing table + Log.d(TAG, "onUpdateResources: $resourceListJSON") + listeners.onEach { + it.get()?.onUpdateResources(resourceListJSON) + } + } + + override fun onSetInterfaceConfig( + tunnelAddressIPv4: String, + tunnelAddressIPv6: String, + dnsAddress: String, + dnsFallbackStrategy: String + ) { + Log.d(TAG, "onSetInterfaceConfig: [IPv4:$tunnelAddressIPv4] [IPv6:$tunnelAddressIPv6] [dns:$dnsAddress] [dnsFallbackStrategy:$dnsFallbackStrategy]") + + listeners.onEach { + it.get()?.onSetInterfaceConfig(tunnelAddressIPv4, tunnelAddressIPv6, dnsAddress, dnsFallbackStrategy) + } + } + + override fun onTunnelReady(): Boolean { + Log.d(TAG, "onTunnelReady") + + listeners.onEach { + it.get()?.onTunnelReady() + } + return true + } + + override fun onError(error: String): Boolean { + Log.d(TAG, "onError: $error") + + listeners.onEach { + it.get()?.onError(error) + } + return true + } + + override fun onAddRoute(cidrAddress: String) { + Log.d(TAG, "onAddRoute: $cidrAddress") + + listeners.onEach { + it.get()?.onAddRoute(cidrAddress) + } + } + + override fun onRemoveRoute(cidrAddress: String) { + Log.d(TAG, "onRemoveRoute: $cidrAddress") + + listeners.onEach { + it.get()?.onRemoveRoute(cidrAddress) + } + } + + override fun onDisconnect(error: String?): Boolean { + Log.d(TAG, "onDisconnect $error") + + listeners.onEach { + it.get()?.onDisconnect(error) + } + return true + } + } + + fun addListener(listener: TunnelListener) { + val contains = listeners.any { + it.get() == listener + } + + if (!contains) { + listeners.add(WeakReference(listener)) + } + } + + fun removeListener(listener: TunnelListener) { + listeners.firstOrNull { + it.get() == listener + }?.let { + it.clear() + listeners.remove(it) + } + } + + companion object { + private const val TAG: String = "TunnelManager" + } +} diff --git a/rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/VpnService.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt similarity index 55% rename from rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/VpnService.kt rename to kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt index fe4be8395..fcc3ff42f 100644 --- a/rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/VpnService.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt @@ -1,19 +1,23 @@ -package dev.firezone.connlib +package dev.firezone.android.tunnel + +import android.net.VpnService import android.util.Log -class VpnService : android.net.VpnService() { +class TunnelService: VpnService() { override fun onCreate() { super.onCreate() - Log.d("Connlib", "VpnService.onCreate") + Log.d("FirezoneVpnService", "onCreate") } override fun onDestroy() { super.onDestroy() - Log.d("Connlib", "VpnService.onDestroy") + Log.d("FirezoneVpnService", "onDestroy") } override fun onStartCommand(intent: android.content.Intent?, flags: Int, startId: Int): Int { - Log.d("Connlib", "VpnService.onStartCommand") + Log.d("FirezoneVpnService", "onStartCommand") return super.onStartCommand(intent, flags, startId) } + + } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt new file mode 100644 index 000000000..12f4a5d88 --- /dev/null +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt @@ -0,0 +1,6 @@ +package dev.firezone.android.tunnel + +object TunnelSession { + external fun connect(fd: Int, controlPlaneUrl: String, token: String, callback: Any): Long + external fun disconnect(session: Long): Boolean +} diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/util/CustomTabsHelper.kt b/kotlin/android/app/src/main/java/dev/firezone/android/util/CustomTabsHelper.kt new file mode 100644 index 000000000..5e8e6a75d --- /dev/null +++ b/kotlin/android/app/src/main/java/dev/firezone/android/util/CustomTabsHelper.kt @@ -0,0 +1,69 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// Copyright 2023 Firezone, Inc. All Rights Reserved. +// +// This file was modified by Firezone, Inc. Modifications are licensed under Apache 2.0. +// The original file can be found at +// https://github.com/GoogleChrome/android-browser-helper/blob/main/demos/custom-tabs-example-app/src/main/java/org/chromium/customtabsdemos/CustomTabsHelper.java +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.firezone.android.util + +import android.content.Context +import android.content.Intent +import android.net.Uri +class CustomTabsHelper { + companion object { + val STABLE_PACKAGE = "com.android.chrome" + val BETA_PACKAGE = "com.chrome.beta" + val DEV_PACKAGE = "com.chrome.dev" + val LOCAL_PACKAGE = "com.google.android.apps.chrome" + val ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService" + + private + var sPackageNameToUse: String? = null + + fun getPackageNameToUse(context: Context): String? { + if (sPackageNameToUse != null) return sPackageNameToUse + val pm = context.getPackageManager() + val activityIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com")) + val defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0) + var defaultViewHandlerPackageName: String? = null + if (defaultViewHandlerInfo != null) { + defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName + } + val resolvedActivityList = pm.queryIntentActivities(activityIntent, 0) + val packagesSupportingCustomTabs: MutableList = ArrayList() + for (info in resolvedActivityList) { + val serviceIntent = Intent() + serviceIntent.action = ACTION_CUSTOM_TABS_CONNECTION + serviceIntent.setPackage(info.activityInfo.packageName) + if (pm.resolveService(serviceIntent, 0) != null) { + packagesSupportingCustomTabs.add(info.activityInfo.packageName) + } + } + if (packagesSupportingCustomTabs.size == 1) { + sPackageNameToUse = packagesSupportingCustomTabs.get(0) + } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) { + sPackageNameToUse = STABLE_PACKAGE + } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) { + sPackageNameToUse = BETA_PACKAGE + } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) { + sPackageNameToUse = DEV_PACKAGE + } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) { + sPackageNameToUse = LOCAL_PACKAGE + } + return sPackageNameToUse + } + } +} diff --git a/kotlin/android/app/src/main/res/layout/fragment_onboarding.xml b/kotlin/android/app/src/main/res/layout/fragment_settings.xml similarity index 95% rename from kotlin/android/app/src/main/res/layout/fragment_onboarding.xml rename to kotlin/android/app/src/main/res/layout/fragment_settings.xml index 1b9653248..cd47fc28f 100644 --- a/kotlin/android/app/src/main/res/layout/fragment_onboarding.xml +++ b/kotlin/android/app/src/main/res/layout/fragment_settings.xml @@ -47,7 +47,7 @@ android:id="@+id/etInput" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/onboarding_fragment_input_hint" + android:hint="@string/settings_fragment_input_hint" android:importantForAutofill="no" android:inputType="text" /> @@ -58,7 +58,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:enabled="false" - android:text="@string/onboarding_fragment_button_text" + android:text="@string/settings_fragment_button_text" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml b/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml index 828f9d052..06cfade0e 100644 --- a/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml +++ b/kotlin/android/app/src/main/res/layout/fragment_sign_in.xml @@ -45,12 +45,20 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/llContainer" /> + + diff --git a/kotlin/android/app/src/main/res/navigation/app_nav_graph.xml b/kotlin/android/app/src/main/res/navigation/app_nav_graph.xml index 251514291..45284e88a 100644 --- a/kotlin/android/app/src/main/res/navigation/app_nav_graph.xml +++ b/kotlin/android/app/src/main/res/navigation/app_nav_graph.xml @@ -8,11 +8,11 @@ + tools:layout="@layout/fragment_splash"> + android:id="@+id/settingsFragment" + android:name="dev.firezone.android.features.settings.ui.SettingsFragment" + tools:layout="@layout/fragment_settings"> 8dp 16dp - + 120dp - 32sp diff --git a/kotlin/android/app/src/main/res/values/strings.xml b/kotlin/android/app/src/main/res/values/strings.xml index 3629d69fd..77f4055ef 100644 --- a/kotlin/android/app/src/main/res/values/strings.xml +++ b/kotlin/android/app/src/main/res/values/strings.xml @@ -1,10 +1,10 @@ firezone - - Login URL - team-id - Save + + Login URL + account-id + Save Sign In @@ -23,6 +23,7 @@ This app requires VPN permission to function effectively and provide you with a secure and private browsing experience. The VPN service encrypts your internet connection, ensuring that your data remains protected from potential threats and unauthorized access.\n\nRest assured, we highly prioritize your online privacy, and the VPN permission is solely used for providing the VPN service within the app. We do not monitor or log your online activities.\n\nTo proceed and enjoy the benefits of a secure connection, please grant the VPN permission by tapping the button below. Request Permission Enter team id + Sign In (Debug User) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b81dacd3f..054ded22e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1540,7 +1540,7 @@ dependencies = [ [[package]] name = "interceptor" version = "0.9.0" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "async-trait", "bytes", @@ -1727,6 +1727,7 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" name = "libs-common" version = "0.1.0" dependencies = [ + "android_logger", "async-trait", "backoff", "base64 0.21.2", @@ -1735,6 +1736,7 @@ dependencies = [ "futures", "futures-util", "ip_network", + "log", "os_info", "parking_lot", "rand", @@ -2555,7 +2557,7 @@ dependencies = [ [[package]] name = "rtcp" version = "0.9.0" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "bytes", "thiserror", @@ -2583,7 +2585,7 @@ dependencies = [ [[package]] name = "rtp" version = "0.8.0" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "bytes", "rand", @@ -2776,7 +2778,7 @@ dependencies = [ [[package]] name = "sdp" version = "0.5.3" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "rand", "substring", @@ -2800,9 +2802,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -2813,9 +2815,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -3018,7 +3020,7 @@ dependencies = [ [[package]] name = "stun" version = "0.4.4" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "base64 0.21.2", "crc", @@ -3528,7 +3530,7 @@ dependencies = [ [[package]] name = "turn" version = "0.6.1" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "async-trait", "base64 0.21.2", @@ -3795,7 +3797,7 @@ dependencies = [ [[package]] name = "webrtc" version = "0.8.0" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "arc-swap", "async-trait", @@ -3837,7 +3839,7 @@ dependencies = [ [[package]] name = "webrtc-data" version = "0.7.0" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "bytes", "log", @@ -3850,7 +3852,7 @@ dependencies = [ [[package]] name = "webrtc-dtls" version = "0.7.2" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "aes 0.6.0", "aes-gcm", @@ -3886,7 +3888,7 @@ dependencies = [ [[package]] name = "webrtc-ice" version = "0.9.1" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "arc-swap", "async-trait", @@ -3909,7 +3911,7 @@ dependencies = [ [[package]] name = "webrtc-mdns" version = "0.5.2" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "log", "socket2 0.4.9", @@ -3921,7 +3923,7 @@ dependencies = [ [[package]] name = "webrtc-media" version = "0.6.1" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "byteorder", "bytes", @@ -3933,7 +3935,7 @@ dependencies = [ [[package]] name = "webrtc-sctp" version = "0.8.0" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "arc-swap", "async-trait", @@ -3949,7 +3951,7 @@ dependencies = [ [[package]] name = "webrtc-srtp" version = "0.10.0" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "aead 0.4.3", "aes 0.7.5", @@ -3971,7 +3973,7 @@ dependencies = [ [[package]] name = "webrtc-util" version = "0.7.0" -source = "git+https://github.com/firezone/webrtc?rev=85bf9c8#85bf9c80028af2a6a0970c44d2fbab8c97aaf85d" +source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" dependencies = [ "async-trait", "bitflags 1.3.2", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 69b390d95..6c31bb44d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -22,4 +22,4 @@ backoff = { version = "0.4", features = ["tokio"] } # (the `patch` section can't be used for build deps...) [patch.crates-io] ring = { git = "https://github.com/firezone/ring", branch = "v0.16.20-cc-fix" } -webrtc = { git = "https://github.com/firezone/webrtc", rev = "85bf9c8" } +webrtc = { git = "https://github.com/firezone/webrtc", rev = "9ddd589" } diff --git a/rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/Logger.kt b/rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/Logger.kt deleted file mode 100644 index 030f7f264..000000000 --- a/rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/Logger.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.firezone.connlib - -object Logger { - external fun init() -} diff --git a/rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/Session.kt b/rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/Session.kt deleted file mode 100644 index 36be05beb..000000000 --- a/rust/connlib/clients/android/connlib/src/main/java/dev/firezone/connlib/Session.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.firezone.connlib - -object Session { - external fun connect(portalURL: String, token: String, callback: Any): Long - external fun disconnect(session: Long): Boolean -} diff --git a/rust/connlib/clients/android/connlib/src/test/java/dev/firezone/connlib/ConnlibTest.kt b/rust/connlib/clients/android/connlib/src/test/java/dev/firezone/connlib/ConnlibTest.kt deleted file mode 100644 index 03da60f59..000000000 --- a/rust/connlib/clients/android/connlib/src/test/java/dev/firezone/connlib/ConnlibTest.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.firezone.connlib - -import org.junit.Test - -import org.junit.Assert.* - -class LoggerTest { - // TODO -} diff --git a/rust/connlib/clients/android/connlib/src/test/java/dev/firezone/connlib/SessionTest.kt b/rust/connlib/clients/android/connlib/src/test/java/dev/firezone/connlib/SessionTest.kt deleted file mode 100644 index 958cf0b05..000000000 --- a/rust/connlib/clients/android/connlib/src/test/java/dev/firezone/connlib/SessionTest.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.firezone.connlib - -import org.junit.Test - -import org.junit.Assert.* - -class SessionTest { - // TODO -} diff --git a/rust/connlib/clients/android/connlib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt b/rust/connlib/clients/android/connlib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt deleted file mode 100644 index ded39644c..000000000 --- a/rust/connlib/clients/android/connlib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt +++ /dev/null @@ -1,10 +0,0 @@ - -package dev.firezone.connlib - -import org.junit.Test - -import org.junit.Assert.* - -class VpnServiceTest { - // TODO -} diff --git a/rust/connlib/clients/android/src/lib.rs b/rust/connlib/clients/android/src/lib.rs index d12152200..9da6d4bc6 100644 --- a/rust/connlib/clients/android/src/lib.rs +++ b/rust/connlib/clients/android/src/lib.rs @@ -6,21 +6,24 @@ use firezone_client_connlib::{Callbacks, Error, ResourceDescription, Session}; use ip_network::IpNetwork; use jni::{ - objects::{JClass, JObject, JString, JValue}, + objects::{GlobalRef, JClass, JObject, JString, JValue}, strings::JNIString, + sys::jint, JNIEnv, JavaVM, }; use std::net::{Ipv4Addr, Ipv6Addr}; use thiserror::Error; +const DNS_FALLBACK_STRATEGY: &str = "upstream_resolver"; + /// This should be called once after the library is loaded by the system. #[allow(non_snake_case)] #[no_mangle] -pub extern "system" fn Java_dev_firezone_connlib_Logger_init(_: JNIEnv, _: JClass) { +pub extern "system" fn Java_dev_firezone_android_tunnel_TunnelLogger_init(_: JNIEnv, _: JClass) { android_logger::init_once( android_logger::Config::default() .with_max_level(if cfg!(debug_assertions) { - log::LevelFilter::Trace + log::LevelFilter::Debug } else { log::LevelFilter::Warn }) @@ -30,7 +33,7 @@ pub extern "system" fn Java_dev_firezone_connlib_Logger_init(_: JNIEnv, _: JClas pub struct CallbackHandler { vm: JavaVM, - callback_handler: JObject<'static>, + callback_handler: GlobalRef, } impl Clone for CallbackHandler { @@ -46,7 +49,7 @@ impl Clone for CallbackHandler { #[derive(Debug, Error)] pub enum CallbackError { - #[error("Failed to attach current thread as daemon: {0}")] + #[error("Failed to attach current thread: {0}")] AttachCurrentThreadFailed(#[source] jni::errors::Error), #[error("Failed to serialize JSON: {0}")] SerializeFailed(#[from] serde_json::Error), @@ -114,13 +117,13 @@ impl Callbacks for CallbackHandler { source, } })?; - // TODO: Don't hardcode this string here! - let dns_fallback_strategy = env.new_string("upstream_resolver").map_err(|source| { - CallbackError::NewStringFailed { - name: "dns_fallback_strategy", - source, - } - })?; + let dns_fallback_strategy = + env.new_string(DNS_FALLBACK_STRATEGY).map_err(|source| { + CallbackError::NewStringFailed { + name: "dns_fallback_strategy", + source, + } + })?; call_method( &mut env, &self.callback_handler, @@ -280,9 +283,10 @@ enum ConnectError { fn connect( env: &mut JNIEnv, + fd: jint, portal_url: JString, portal_token: JString, - callback_handler: JObject<'static>, + callback_handler: GlobalRef, ) -> Result, ConnectError> { let portal_url = String::from(env.get_string(&portal_url).map_err(|source| { ConnectError::StringInvalid { @@ -301,23 +305,32 @@ fn connect( vm: env.get_java_vm().map_err(ConnectError::GetJavaVmFailed)?, callback_handler, }; - Session::connect(portal_url.as_str(), portal_token, callback_handler.clone()) - .map_err(Into::into) + Session::connect( + Some(fd), + portal_url.as_str(), + portal_token, + callback_handler, + ) + .map_err(Into::into) } /// # Safety /// Pointers must be valid +/// fd must be a valid file descriptor #[allow(non_snake_case)] #[no_mangle] -pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_connect( - mut env: JNIEnv<'static>, +pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_TunnelSession_connect( + mut env: JNIEnv, _class: JClass, + fd: jint, portal_url: JString, portal_token: JString, - callback_handler: JObject<'static>, + callback_handler: JObject, ) -> *const Session { + let Ok(callback_handler) = env.new_global_ref(callback_handler) else { return std::ptr::null() }; + if let Some(result) = catch_and_throw(&mut env, |env| { - connect(env, portal_url, portal_token, callback_handler) + connect(env, fd, portal_url, portal_token, callback_handler) }) { match result { Ok(session) => return Box::into_raw(Box::new(session)), @@ -331,7 +344,7 @@ pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_connect( /// Pointers must be valid #[allow(non_snake_case)] #[no_mangle] -pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_disconnect( +pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_TunnelSession_disconnect( mut env: JNIEnv, _: JClass, session: *mut Session, diff --git a/rust/connlib/clients/apple/src/lib.rs b/rust/connlib/clients/apple/src/lib.rs index e3b556e32..6958a83c4 100644 --- a/rust/connlib/clients/apple/src/lib.rs +++ b/rust/connlib/clients/apple/src/lib.rs @@ -153,6 +153,7 @@ impl WrappedSession { ) -> Result { init_logging(); Session::connect( + None, portal_url.as_str(), token, CallbackHandler(callback_handler.into()), diff --git a/rust/connlib/clients/headless/src/main.rs b/rust/connlib/clients/headless/src/main.rs index e313a07ac..ceba3d2d6 100644 --- a/rust/connlib/clients/headless/src/main.rs +++ b/rust/connlib/clients/headless/src/main.rs @@ -78,7 +78,7 @@ fn main() -> Result<()> { // TODO: allow passing as arg vars let url = parse_env_var::(URL_ENV_VAR)?; let secret = parse_env_var::(SECRET_ENV_VAR)?; - let mut session = Session::connect(url, secret, CallbackHandler).unwrap(); + let mut session = Session::connect(None, url, secret, CallbackHandler).unwrap(); tracing::info!("Started new session"); block_on_ctrl_c(); diff --git a/rust/connlib/gateway/src/main.rs b/rust/connlib/gateway/src/main.rs index 693d53c79..9715b9242 100644 --- a/rust/connlib/gateway/src/main.rs +++ b/rust/connlib/gateway/src/main.rs @@ -64,7 +64,7 @@ fn main() -> Result<()> { // TODO: allow passing as arg vars let url = parse_env_var::(URL_ENV_VAR)?; let secret = parse_env_var::(SECRET_ENV_VAR)?; - let mut session = Session::connect(url, secret, CallbackHandler).unwrap(); + let mut session = Session::connect(None, url, secret, CallbackHandler).unwrap(); let (tx, rx) = std::sync::mpsc::channel(); ctrlc::set_handler(move || tx.send(()).expect("Could not send stop signal on channel.")) diff --git a/rust/connlib/libs/client/src/control.rs b/rust/connlib/libs/client/src/control.rs index 1ecdefd9f..231ef7352 100644 --- a/rust/connlib/libs/client/src/control.rs +++ b/rust/connlib/libs/client/src/control.rs @@ -234,13 +234,15 @@ impl ControlPlane { impl ControlSession for ControlPlane { #[tracing::instrument(level = "trace", skip(private_key, callbacks))] async fn start( + fd: Option, private_key: StaticSecret, receiver: Receiver>, control_signal: PhoenixSenderWithTopic, callbacks: CB, ) -> Result<()> { let control_signaler = ControlSignaler { control_signal }; - let tunnel = Arc::new(Tunnel::new(private_key, control_signaler.clone(), callbacks).await?); + let tunnel = + Arc::new(Tunnel::new(fd, private_key, control_signaler.clone(), callbacks).await?); let control_plane = ControlPlane { tunnel, diff --git a/rust/connlib/libs/common/Cargo.toml b/rust/connlib/libs/common/Cargo.toml index 361afcb63..f417bc66d 100644 --- a/rust/connlib/libs/common/Cargo.toml +++ b/rust/connlib/libs/common/Cargo.toml @@ -31,8 +31,14 @@ rand = { version = "0.8", default-features = false, features = ["std"] } chrono = { workspace = true } parking_lot = "0.12" +# Needed for Android logging until tracing is working +log = "0.4" + [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] swift-bridge = { workspace = true } +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.13" + [target.'cfg(target_os = "linux")'.dependencies] rtnetlink = { version = "0.12", default-features = false, features = ["tokio_socket"] } diff --git a/rust/connlib/libs/common/src/session.rs b/rust/connlib/libs/common/src/session.rs index 00ed3a3aa..ba835bde8 100644 --- a/rust/connlib/libs/common/src/session.rs +++ b/rust/connlib/libs/common/src/session.rs @@ -32,6 +32,7 @@ struct StopRuntime; pub trait ControlSession { /// Start control-plane with the given private-key in the background. async fn start( + fd: Option, private_key: StaticSecret, receiver: Receiver>, control_signal: PhoenixSenderWithTopic, @@ -200,11 +201,21 @@ where /// 2. Connect to the control plane to the portal /// 3. Start the tunnel in the background and forward control plane messages to it. /// + /// If a fd is passed in, it's used for the tunnel interface. This is useful on Android where + /// we can't create interfaces but we can easily get its file descriptor from the OS. + /// If no fd is passed in, a new interface will be created (Linux) or we'll walk the fd table + /// to find the interface (iOS/macOS). + /// /// The generic parameter `CB` should implement all the handlers and that's how errors will be surfaced. /// /// On a fatal error you should call `[Session::disconnect]` and start a new one. // TODO: token should be something like SecretString but we need to think about FFI compatibility - pub fn connect(portal_url: impl TryInto, token: String, callbacks: CB) -> Result { + pub fn connect( + fd: Option, + portal_url: impl TryInto, + token: String, + callbacks: CB, + ) -> Result { // TODO: We could use tokio::runtime::current() to get the current runtime // which could work with swif-rust that already runs a runtime. But IDK if that will work // in all pltaforms, a couple of new threads shouldn't bother none. @@ -245,6 +256,7 @@ where Self::connect_inner( &runtime, tx, + fd, portal_url.try_into().map_err(|_| Error::UriError)?, token, this.callbacks.clone(), @@ -261,6 +273,7 @@ where fn connect_inner( runtime: &Runtime, runtime_stopper: tokio::sync::mpsc::Sender, + fd: Option, portal_url: Url, token: String, callbacks: CallbackErrorFacade, @@ -296,7 +309,7 @@ where let topic = T::socket_path().to_string(); let internal_sender = connection.sender_with_topic(topic.clone()); fatal_error!( - T::start(private_key, control_plane_receiver, internal_sender, callbacks.0.clone()).await, + T::start(fd, private_key, control_plane_receiver, internal_sender, callbacks.0.clone()).await, runtime_stopper, &callbacks ); @@ -305,6 +318,7 @@ where let mut exponential_backoff = T::retry_strategy(); loop { // `connection.start` calls the callback only after connecting + tracing::debug!("Attempting connection to portal..."); let result = connection.start(vec![topic.clone()], || exponential_backoff.reset()).await; tracing::warn!("Disconnected from the portal"); if let Err(err) = &result { diff --git a/rust/connlib/libs/gateway/src/control.rs b/rust/connlib/libs/gateway/src/control.rs index 343c72185..f8a5c3b8b 100644 --- a/rust/connlib/libs/gateway/src/control.rs +++ b/rust/connlib/libs/gateway/src/control.rs @@ -144,13 +144,15 @@ impl ControlPlane { impl ControlSession for ControlPlane { #[tracing::instrument(level = "trace", skip(private_key, callbacks))] async fn start( + fd: Option, private_key: StaticSecret, receiver: Receiver>, control_signal: PhoenixSenderWithTopic, callbacks: CB, ) -> Result<()> { let control_signaler = ControlSignaler { control_signal }; - let tunnel = Arc::new(Tunnel::new(private_key, control_signaler.clone(), callbacks).await?); + let tunnel = + Arc::new(Tunnel::new(fd, private_key, control_signaler.clone(), callbacks).await?); let control_plane = ControlPlane { tunnel, diff --git a/rust/connlib/libs/tunnel/Cargo.toml b/rust/connlib/libs/tunnel/Cargo.toml index e55e030af..a6a3c3e15 100644 --- a/rust/connlib/libs/tunnel/Cargo.toml +++ b/rust/connlib/libs/tunnel/Cargo.toml @@ -27,6 +27,9 @@ pnet_packet = { version = "0.34" } # TODO: research replacing for https://github.com/algesten/str0m webrtc = { version = "0.8" } +# Needed for Android logging until tracing is fixed +log = "0.4" + # Linux tunnel dependencies [target.'cfg(target_os = "linux")'.dependencies] netlink-packet-route = { version = "0.15", default-features = false } @@ -36,7 +39,6 @@ rtnetlink = { version = "0.12", default-features = false, features = ["tokio_soc # Android tunnel dependencies [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13" -log = "0.4.20" # Windows tunnel dependencies [target.'cfg(target_os = "windows")'.dependencies] diff --git a/rust/connlib/libs/tunnel/src/device_channel_unix.rs b/rust/connlib/libs/tunnel/src/device_channel_unix.rs index bd3c50557..581f39fa9 100644 --- a/rust/connlib/libs/tunnel/src/device_channel_unix.rs +++ b/rust/connlib/libs/tunnel/src/device_channel_unix.rs @@ -60,8 +60,8 @@ impl DeviceChannel { } } -pub(crate) async fn create_iface() -> Result<(IfaceConfig, DeviceChannel)> { - let dev = Arc::new(IfaceDevice::new().await?.set_non_blocking()?); +pub(crate) async fn create_iface(fd: Option) -> Result<(IfaceConfig, DeviceChannel)> { + let dev = Arc::new(IfaceDevice::new(fd).await?.set_non_blocking()?); let async_dev = Arc::clone(&dev); let device_channel = DeviceChannel(AsyncFd::new(async_dev)?); let iface_config = IfaceConfig(dev); diff --git a/rust/connlib/libs/tunnel/src/device_channel_win.rs b/rust/connlib/libs/tunnel/src/device_channel_win.rs index 2edf498f8..1c4246a7e 100644 --- a/rust/connlib/libs/tunnel/src/device_channel_win.rs +++ b/rust/connlib/libs/tunnel/src/device_channel_win.rs @@ -22,6 +22,8 @@ impl DeviceChannel { } } -pub(crate) async fn create_iface() -> Result<(IfaceConfig, DeviceChannel)> { +pub(crate) async fn create_iface( + _device_handle: Option, +) -> Result<(IfaceConfig, DeviceChannel)> { todo!() } diff --git a/rust/connlib/libs/tunnel/src/lib.rs b/rust/connlib/libs/tunnel/src/lib.rs index 06b411733..19a2e3ebf 100644 --- a/rust/connlib/libs/tunnel/src/lib.rs +++ b/rust/connlib/libs/tunnel/src/lib.rs @@ -73,6 +73,7 @@ mod tun; #[path = "tun_linux.rs"] mod tun; +// TODO: Android and linux are nearly identical; use a common tunnel module? #[cfg(target_os = "android")] #[path = "tun_android.rs"] mod tun; @@ -169,6 +170,7 @@ where /// - `control_signaler`: this is used to send SDP from the tunnel to the control plane. #[tracing::instrument(level = "trace", skip(private_key, control_signaler, callbacks))] pub async fn new( + fd: Option, private_key: StaticSecret, control_signaler: C, callbacks: CB, @@ -177,7 +179,7 @@ where let rate_limiter = Arc::new(RateLimiter::new(&public_key, HANDSHAKE_RATE_LIMIT)); let peers_by_ip = RwLock::new(IpNetworkTable::new()); let next_index = Default::default(); - let (iface_config, device_channel) = create_iface().await?; + let (iface_config, device_channel) = create_iface(fd).await?; let iface_config = tokio::sync::Mutex::new(iface_config); let device_channel = Arc::new(device_channel); let peer_connections = Default::default(); @@ -504,6 +506,9 @@ where // found some comments saying that a single read syscall represents a single packet but no docs on that // See https://stackoverflow.com/questions/18461365/how-to-read-packet-by-packet-from-linux-tun-tap match dev.device_channel.mtu().await { + // XXX: Do we need to fetch the mtu every time? In most clients it'll + // be hardcoded to 1280, and if not, it'll only change before packets start + // to flow. Ok(mtu) => match dev.device_channel.read(&mut src[..mtu]).await { Ok(res) => res, Err(err) => { diff --git a/rust/connlib/libs/tunnel/src/tun_android.rs b/rust/connlib/libs/tunnel/src/tun_android.rs index e254bd6e9..9d6c11989 100644 --- a/rust/connlib/libs/tunnel/src/tun_android.rs +++ b/rust/connlib/libs/tunnel/src/tun_android.rs @@ -1,8 +1,9 @@ use super::InterfaceConfig; use ip_network::IpNetwork; -use libc::{close, open, O_RDWR}; +use libc::{close, read, write}; use libs_common::{CallbackErrorFacade, Callbacks, Error, Result, DNS_SENTINEL}; use std::{ + io, os::fd::{AsRawFd, RawFd}, sync::Arc, }; @@ -28,30 +29,33 @@ impl Drop for IfaceDevice { } impl IfaceDevice { - fn write(&self, _buf: &[u8]) -> usize { - tracing::error!("`write` unimplemented on Android"); - 0 - } - - pub async fn new() -> Result { - // TODO: This won't actually work for non-root users... - let fd = unsafe { open(b"/dev/net/tun\0".as_ptr() as _, O_RDWR) }; - // TODO: everything! - if fd == -1 { - Err(Error::Io(std::io::Error::last_os_error())) - } else { - Ok(Self { fd }) + fn write(&self, buf: &[u8]) -> usize { + match unsafe { write(self.fd, buf.as_ptr() as _, buf.len() as _) } { + -1 => 0, + n => n as usize, } } + pub async fn new(fd: Option) -> Result { + log::debug!("tunnel allocation unimplemented on Android; using provided fd"); + Ok(Self { + fd: fd.expect("file descriptor must be provided!") as RawFd, + }) + } + pub fn set_non_blocking(self) -> Result { - tracing::error!("`set_non_blocking` unimplemented on Android"); + // Anrdoid already opens the tun device in non-blocking mode for us + log::debug!("`set_non_blocking` unimplemented on Android"); Ok(self) } pub async fn mtu(&self) -> Result { - tracing::error!("`mtu` unimplemented on Android"); - Ok(0) + // We stick with a hardcoded MTU of 1280 for now. This could be improved by + // finding the MTU of the underlying physical interface and subtracting 80 + // from it for the WireGuard overhead, but that's a lot of complexity + // for little gain. + log::debug!("`mtu` unimplemented on Android; using 1280"); + Ok(1280) } pub fn write4(&self, src: &[u8]) -> usize { @@ -63,20 +67,20 @@ impl IfaceDevice { } pub fn read<'a>(&self, dst: &'a mut [u8]) -> Result<&'a mut [u8]> { - tracing::error!("`read` unimplemented on Android"); - Ok(dst) + match unsafe { read(self.fd, dst.as_mut_ptr() as _, dst.len()) } { + -1 => Err(Error::IfaceRead(io::Error::last_os_error())), + n => Ok(&mut dst[..n as usize]), + } } } impl IfaceConfig { - #[tracing::instrument(level = "trace", skip(self, callbacks))] pub async fn set_iface_config( &mut self, config: &InterfaceConfig, callbacks: &CallbackErrorFacade, ) -> Result<()> { - callbacks.on_set_interface_config(config.ipv4, config.ipv6, DNS_SENTINEL); - Ok(()) + callbacks.on_set_interface_config(config.ipv4, config.ipv6, DNS_SENTINEL) } pub async fn add_route( @@ -88,7 +92,7 @@ impl IfaceConfig { } pub async fn up(&mut self) -> Result<()> { - tracing::error!("`up` unimplemented on Android"); + log::debug!("`up` unimplemented on Android"); Ok(()) } } diff --git a/rust/connlib/libs/tunnel/src/tun_darwin.rs b/rust/connlib/libs/tunnel/src/tun_darwin.rs index e2a799901..ef2330b3f 100644 --- a/rust/connlib/libs/tunnel/src/tun_darwin.rs +++ b/rust/connlib/libs/tunnel/src/tun_darwin.rs @@ -95,7 +95,7 @@ impl IfaceDevice { } } - pub async fn new() -> Result { + pub async fn new(_fd: Option) -> Result { let mut info = ctl_info { ctl_id: 0, ctl_name: [0; 96], diff --git a/rust/connlib/libs/tunnel/src/tun_linux.rs b/rust/connlib/libs/tunnel/src/tun_linux.rs index 3b8eabe14..ad5d77174 100644 --- a/rust/connlib/libs/tunnel/src/tun_linux.rs +++ b/rust/connlib/libs/tunnel/src/tun_linux.rs @@ -78,7 +78,7 @@ impl IfaceDevice { } } - pub async fn new() -> Result { + pub async fn new(_fd: Option) -> Result { debug_assert!(IFACE_NAME.as_bytes().len() < IFNAMSIZ); let fd = match unsafe { open(TUN_FILE.as_ptr() as _, O_RDWR) } {