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:
Jamil
2023-08-23 14:13:55 -07:00
committed by GitHub
parent bf95d0480b
commit 3316d9098a
62 changed files with 645 additions and 325 deletions

View File

@@ -39,6 +39,7 @@ defmodule Web.Endpoint do
socket "/live", Phoenix.LiveView.Socket,
websocket: [
check_origin: :conn,
connect_info: [
:user_agent,
:peer_data,

View File

@@ -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 #####################
###############################

View File

@@ -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

View File

@@ -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\"")

View File

@@ -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>

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"
}
}

View File

@@ -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?,
)

View File

@@ -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()
}
}

View File

@@ -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 ()
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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")

View File

@@ -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 -> {}
}
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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()

View File

@@ -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"
}
}

View File

@@ -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!")
}
}
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -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(

View File

@@ -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()
)
}
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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
}
}

View File

@@ -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"
}
}

View File

@@ -0,0 +1,8 @@
package dev.firezone.android.tunnel
data class TunnelConfig (
val tunnelAddressIPv4: String,
val tunnelAddressIPv6: String,
val dnsAddress: String,
val dnsFallbackStrategy: String,
)

View File

@@ -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)

View File

@@ -0,0 +1,5 @@
package dev.firezone.android.tunnel
object TunnelLogger {
external fun init()
}

View File

@@ -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"
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}
}
}

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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"

View File

@@ -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>

View File

@@ -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
View File

@@ -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",

View File

@@ -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" }

View File

@@ -1,5 +0,0 @@
package dev.firezone.connlib
object Logger {
external fun init()
}

View File

@@ -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
}

View File

@@ -1,9 +0,0 @@
package dev.firezone.connlib
import org.junit.Test
import org.junit.Assert.*
class LoggerTest {
// TODO
}

View File

@@ -1,9 +0,0 @@
package dev.firezone.connlib
import org.junit.Test
import org.junit.Assert.*
class SessionTest {
// TODO
}

View File

@@ -1,10 +0,0 @@
package dev.firezone.connlib
import org.junit.Test
import org.junit.Assert.*
class VpnServiceTest {
// TODO
}

View File

@@ -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>,

View File

@@ -153,6 +153,7 @@ impl WrappedSession {
) -> Result<Self, String> {
init_logging();
Session::connect(
None,
portal_url.as_str(),
token,
CallbackHandler(callback_handler.into()),

View File

@@ -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();

View File

@@ -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."))

View File

@@ -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,

View File

@@ -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"] }

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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]

View File

@@ -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);

View File

@@ -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!()
}

View File

@@ -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) => {

View File

@@ -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(())
}
}

View File

@@ -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],

View File

@@ -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) } {