mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
fix(android): Fix auth flow and callback thread safety, and pass fd through FFI (#1930)
* Refactor sharedPreferences to only save the AccountId * Update TeamId -> AccountId to match naming elsewhere * Update JWT -> Token to avoid confusion; this token is **not** a valid JWT and should be treated as an opaque token * Update FFI `connect` to accept an optional file descriptor (int32) as a first argument. This seemed to be the most straightforward way to pass it to the tunnel stack. Retrieving it via callback is another option, but retrieving return vars with the `jni` was more complex. We could have used a similar approach that we did in the Apple client (enumerating all fd's in the `new()` function until we found ours) but this approach is [explicitly documented/recommended](https://developer.android.com/reference/android/net/VpnService.Builder#establish()) by the Android docs so I figured it's not likely to break. Additionally, there was a thread safety bug in the recent JNI callback implementation that consistently crashed the VM with `JNI DETECTED ERROR IN APPLICATION: use of invalid jobject...`. The fix was to use `GlobalRef` which has the explicit purpose of outliving the `JNIEnv` lifetime so that no `static` lifetimes need to be used. --------- Signed-off-by: Jamil <jamilbk@users.noreply.github.com> Co-authored-by: Pratik Velani <pratikvelani@gmail.com> Co-authored-by: Gabi <gabrielalejandro7@gmail.com>
This commit is contained in:
@@ -39,6 +39,7 @@ defmodule Web.Endpoint do
|
||||
|
||||
socket "/live", Phoenix.LiveView.Socket,
|
||||
websocket: [
|
||||
check_origin: :conn,
|
||||
connect_info: [
|
||||
:user_agent,
|
||||
:peer_data,
|
||||
|
||||
@@ -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 #####################
|
||||
###############################
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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\"")
|
||||
|
||||
@@ -3,11 +3,18 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- XXX: Set usesCleartextTraffic to false for added security when APIMock is removed or served over https -->
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".core.FirezoneApp"
|
||||
android:allowBackup="false"
|
||||
@@ -51,7 +58,7 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" android:host="${hostName}" />
|
||||
<data android:scheme="https" android:host="${hostName}" android:path="/handle_client_auth_callback" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@@ -60,7 +67,7 @@
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name="dev.firezone.connlib.VpnService"
|
||||
android:name="dev.firezone.android.tunnel.TunnelService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
<intent-filter>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -8,9 +8,9 @@ internal interface PreferenceRepository {
|
||||
|
||||
fun getConfig(): Flow<Config>
|
||||
|
||||
fun savePortalUrl(value: String): Flow<Unit>
|
||||
fun saveAccountId(value: String): Flow<Unit>
|
||||
|
||||
fun saveJWT(value: String): Flow<Unit>
|
||||
fun saveToken(value: String): Flow<Unit>
|
||||
|
||||
fun saveIsConnectedSync(value: Boolean)
|
||||
|
||||
|
||||
@@ -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<Config> = flow {
|
||||
emit(getConfigSync())
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
override fun savePortalUrl(value: String): Flow<Unit> = flow {
|
||||
override fun saveAccountId(value: String): Flow<Unit> = flow {
|
||||
emit(
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(PORTAL_URL_KEY, value)
|
||||
.putString(ACCOUNT_ID_KEY, value)
|
||||
.apply()
|
||||
)
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
override fun saveJWT(value: String): Flow<Unit> = flow {
|
||||
override fun saveToken(value: String): Flow<Unit> = 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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<Unit> {
|
||||
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 ()
|
||||
}
|
||||
}
|
||||
@@ -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<Unit> = repository.savePortalUrl(portalUrl)
|
||||
operator fun invoke(accountId: String): Flow<Unit> = repository.saveAccountId(accountId)
|
||||
}
|
||||
@@ -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<Unit> = repository.saveJWT(jwt)
|
||||
operator fun invoke(token: String): Flow<Unit> = repository.saveToken(token)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = 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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ViewState>()
|
||||
@@ -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(
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = 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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package dev.firezone.android.tunnel
|
||||
|
||||
data class TunnelConfig (
|
||||
val tunnelAddressIPv4: String,
|
||||
val tunnelAddressIPv6: String,
|
||||
val dnsAddress: String,
|
||||
val dnsFallbackStrategy: String,
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package dev.firezone.android.tunnel
|
||||
|
||||
object TunnelLogger {
|
||||
external fun init()
|
||||
}
|
||||
@@ -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<WeakReference<TunnelListener>> = 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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<String> = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
@@ -45,12 +45,20 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/llContainer" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btDebugUser"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sign_in_debug_user"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btSignIn"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btSignIn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sign_in_fragment_button_text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btSettings"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
<fragment
|
||||
android:id="@+id/splashFragment"
|
||||
android:name="dev.firezone.android.features.splash.ui.SplashFragment"
|
||||
tools:layout="@layout/fragment_onboarding">
|
||||
tools:layout="@layout/fragment_splash">
|
||||
|
||||
<action
|
||||
android:id="@+id/navigateToOnboardingFragment"
|
||||
app:destination="@id/onboardingFragment"
|
||||
android:id="@+id/navigateToSettingsFragment"
|
||||
app:destination="@id/settingsFragment"
|
||||
app:enterAnim="@anim/fade_in"
|
||||
app:exitAnim="@anim/fade_out"
|
||||
app:popEnterAnim="@anim/fade_in"
|
||||
@@ -37,9 +37,9 @@
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/onboardingFragment"
|
||||
android:name="dev.firezone.android.features.onboarding.ui.OnboardingFragment"
|
||||
tools:layout="@layout/fragment_onboarding">
|
||||
android:id="@+id/settingsFragment"
|
||||
android:name="dev.firezone.android.features.settings.ui.SettingsFragment"
|
||||
tools:layout="@layout/fragment_settings">
|
||||
|
||||
<action
|
||||
android:id="@+id/navigateToSignInFragment"
|
||||
@@ -64,8 +64,8 @@
|
||||
app:popExitAnim="@anim/fade_out" />
|
||||
|
||||
<action
|
||||
android:id="@+id/navigateToOnboardingFragment"
|
||||
app:destination="@id/onboardingFragment"
|
||||
android:id="@+id/navigateToSettingsFragment"
|
||||
app:destination="@id/settingsFragment"
|
||||
app:enterAnim="@anim/fade_in"
|
||||
app:exitAnim="@anim/fade_out"
|
||||
app:popEnterAnim="@anim/fade_in"
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
<dimen name="spacing_small">8dp</dimen>
|
||||
<dimen name="spacing_medium">16dp</dimen>
|
||||
|
||||
<!-- Onboarding Fragment -->
|
||||
<!-- Settings Fragment -->
|
||||
<dimen name="iv_logo_size">120dp</dimen>
|
||||
<!-- Onboarding Fragment -->
|
||||
|
||||
<!-- Text Size -->
|
||||
<dimen name="text_large">32sp</dimen>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<resources>
|
||||
<string name="app_short_name">firezone</string>
|
||||
|
||||
<!-- Onboarding Fragment -->
|
||||
<string name="onboarding_fragment_header_title">Login URL</string>
|
||||
<string name="onboarding_fragment_input_hint">team-id</string>
|
||||
<string name="onboarding_fragment_button_text">Save</string>
|
||||
<!-- Settings Fragment -->
|
||||
<string name="settings_fragment_header_title">Login URL</string>
|
||||
<string name="settings_fragment_input_hint">account-id</string>
|
||||
<string name="settings_fragment_button_text">Save</string>
|
||||
|
||||
<!-- Sign In Fragment -->
|
||||
<string name="sign_in_fragment_button_text">Sign In</string>
|
||||
@@ -23,6 +23,7 @@
|
||||
<string name="vpn_permission_description">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.</string>
|
||||
<string name="request_permission">Request Permission</string>
|
||||
<string name="enter_team_id">Enter team id</string>
|
||||
<string name="sign_in_debug_user">Sign In (Debug User)</string>
|
||||
<!-- Error Dialog -->
|
||||
|
||||
</resources>
|
||||
|
||||
40
rust/Cargo.lock
generated
40
rust/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package dev.firezone.connlib
|
||||
|
||||
object Logger {
|
||||
external fun init()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package dev.firezone.connlib
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
class LoggerTest {
|
||||
// TODO
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package dev.firezone.connlib
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
class SessionTest {
|
||||
// TODO
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
package dev.firezone.connlib
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
class VpnServiceTest {
|
||||
// TODO
|
||||
}
|
||||
@@ -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<Session<CallbackHandler>, 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<CallbackHandler> {
|
||||
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<CallbackHandler>,
|
||||
|
||||
@@ -153,6 +153,7 @@ impl WrappedSession {
|
||||
) -> Result<Self, String> {
|
||||
init_logging();
|
||||
Session::connect(
|
||||
None,
|
||||
portal_url.as_str(),
|
||||
token,
|
||||
CallbackHandler(callback_handler.into()),
|
||||
|
||||
@@ -78,7 +78,7 @@ fn main() -> Result<()> {
|
||||
// TODO: allow passing as arg vars
|
||||
let url = parse_env_var::<Url>(URL_ENV_VAR)?;
|
||||
let secret = parse_env_var::<String>(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();
|
||||
|
||||
@@ -64,7 +64,7 @@ fn main() -> Result<()> {
|
||||
// TODO: allow passing as arg vars
|
||||
let url = parse_env_var::<Url>(URL_ENV_VAR)?;
|
||||
let secret = parse_env_var::<String>(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."))
|
||||
|
||||
@@ -234,13 +234,15 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
|
||||
impl<CB: Callbacks + 'static> ControlSession<Messages, CB> for ControlPlane<CB> {
|
||||
#[tracing::instrument(level = "trace", skip(private_key, callbacks))]
|
||||
async fn start(
|
||||
fd: Option<i32>,
|
||||
private_key: StaticSecret,
|
||||
receiver: Receiver<MessageResult<Messages>>,
|
||||
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,
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -32,6 +32,7 @@ struct StopRuntime;
|
||||
pub trait ControlSession<T, CB: Callbacks> {
|
||||
/// Start control-plane with the given private-key in the background.
|
||||
async fn start(
|
||||
fd: Option<i32>,
|
||||
private_key: StaticSecret,
|
||||
receiver: Receiver<MessageResult<T>>,
|
||||
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<Url>, token: String, callbacks: CB) -> Result<Self> {
|
||||
pub fn connect(
|
||||
fd: Option<i32>,
|
||||
portal_url: impl TryInto<Url>,
|
||||
token: String,
|
||||
callbacks: CB,
|
||||
) -> Result<Self> {
|
||||
// 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<StopRuntime>,
|
||||
fd: Option<i32>,
|
||||
portal_url: Url,
|
||||
token: String,
|
||||
callbacks: CallbackErrorFacade<CB>,
|
||||
@@ -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 {
|
||||
|
||||
@@ -144,13 +144,15 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
|
||||
impl<CB: Callbacks + 'static> ControlSession<IngressMessages, CB> for ControlPlane<CB> {
|
||||
#[tracing::instrument(level = "trace", skip(private_key, callbacks))]
|
||||
async fn start(
|
||||
fd: Option<i32>,
|
||||
private_key: StaticSecret,
|
||||
receiver: Receiver<MessageResult<IngressMessages>>,
|
||||
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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<i32>) -> 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);
|
||||
|
||||
@@ -22,6 +22,8 @@ impl DeviceChannel {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create_iface() -> Result<(IfaceConfig, DeviceChannel)> {
|
||||
pub(crate) async fn create_iface(
|
||||
_device_handle: Option<i32>,
|
||||
) -> Result<(IfaceConfig, DeviceChannel)> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -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<i32>,
|
||||
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) => {
|
||||
|
||||
@@ -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<Self> {
|
||||
// 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<i32>) -> Result<Self> {
|
||||
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<Self> {
|
||||
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<usize> {
|
||||
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<impl Callbacks>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ impl IfaceDevice {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new() -> Result<Self> {
|
||||
pub async fn new(_fd: Option<i32>) -> Result<Self> {
|
||||
let mut info = ctl_info {
|
||||
ctl_id: 0,
|
||||
ctl_name: [0; 96],
|
||||
|
||||
@@ -78,7 +78,7 @@ impl IfaceDevice {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new() -> Result<IfaceDevice> {
|
||||
pub async fn new(_fd: Option<i32>) -> Result<IfaceDevice> {
|
||||
debug_assert!(IFACE_NAME.as_bytes().len() < IFNAMSIZ);
|
||||
|
||||
let fd = match unsafe { open(TUN_FILE.as_ptr() as _, O_RDWR) } {
|
||||
|
||||
Reference in New Issue
Block a user