diff --git a/kotlin/android/app/build.gradle b/kotlin/android/app/build.gradle
index d659b746b..25930963e 100644
--- a/kotlin/android/app/build.gradle
+++ b/kotlin/android/app/build.gradle
@@ -114,7 +114,7 @@ dependencies {
// Moshi
implementation "com.squareup.moshi:moshi-kotlin:1.12.0"
- implementation 'com.squareup.moshi:moshi:1.12.0'
+ implementation "com.squareup.moshi:moshi:1.12.0"
// Gson
implementation "com.google.code.gson:gson:2.9.0"
diff --git a/kotlin/android/app/src/main/AndroidManifest.xml b/kotlin/android/app/src/main/AndroidManifest.xml
index 9fb314808..deb73a8ec 100644
--- a/kotlin/android/app/src/main/AndroidManifest.xml
+++ b/kotlin/android/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepository.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepository.kt
index 9068b2f75..21f805dba 100644
--- a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepository.kt
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepository.kt
@@ -12,9 +12,5 @@ internal interface PreferenceRepository {
fun saveToken(value: String): Flow
- fun saveIsConnectedSync(value: Boolean)
-
- fun saveIsConnected(value: Boolean): Flow
-
fun validateCsrfToken(value: String): Flow
}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt
index 3dee491c3..6057d36c4 100644
--- a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/PreferenceRepositoryImpl.kt
@@ -15,7 +15,6 @@ internal class PreferenceRepositoryImpl @Inject constructor(
override fun getConfigSync(): Config = Config(
accountId = sharedPreferences.getString(ACCOUNT_ID_KEY, null),
- isConnected = sharedPreferences.getBoolean(IS_CONNECTED_KEY, false),
token = sharedPreferences.getString(TOKEN_KEY, null),
)
@@ -41,19 +40,6 @@ internal class PreferenceRepositoryImpl @Inject constructor(
)
}.flowOn(coroutineDispatcher)
- override fun saveIsConnectedSync(value: Boolean) {
- sharedPreferences
- .edit()
- .putBoolean(IS_CONNECTED_KEY, value)
- .apply()
- }
-
- override fun saveIsConnected(value: Boolean): Flow = flow {
- emit(
- saveIsConnectedSync(value)
- )
- }.flowOn(coroutineDispatcher)
-
override fun validateCsrfToken(value: String): Flow = flow {
val token = sharedPreferences.getString(CSRF_KEY, "")
emit(token == value)
@@ -61,7 +47,6 @@ internal class PreferenceRepositoryImpl @Inject constructor(
companion object {
private const val ACCOUNT_ID_KEY = "accountId"
- private const val IS_CONNECTED_KEY = "isConnected"
private const val TOKEN_KEY = "token"
private const val CSRF_KEY = "csrf"
}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/di/AppModule.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/di/AppModule.kt
index e7e308425..afaa982a4 100644
--- a/kotlin/android/app/src/main/java/dev/firezone/android/core/di/AppModule.kt
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/di/AppModule.kt
@@ -6,15 +6,10 @@ import android.content.SharedPreferences
import android.content.res.Resources
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
-import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
-import dev.firezone.android.core.domain.preference.GetConfigUseCase
-import dev.firezone.android.core.domain.preference.SaveIsConnectedUseCase
-import dev.firezone.android.tunnel.TunnelManager
internal const val ENCRYPTED_SHARED_PREFERENCES = "encryptedSharedPreferences"
@@ -39,12 +34,4 @@ object AppModule {
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
-
- @Provides
- internal fun provideTunnelManager(
- @ApplicationContext appContext: Context,
- getConfigUseCase: GetConfigUseCase,
- saveIsConnectedUseCase: SaveIsConnectedUseCase,
- moshi: Moshi,
- ): TunnelManager = TunnelManager(appContext, getConfigUseCase, saveIsConnectedUseCase, moshi)
}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveIsConnectedUseCase.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveIsConnectedUseCase.kt
deleted file mode 100644
index 6f1917b1f..000000000
--- a/kotlin/android/app/src/main/java/dev/firezone/android/core/domain/preference/SaveIsConnectedUseCase.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package dev.firezone.android.core.domain.preference
-
-import dev.firezone.android.core.data.PreferenceRepository
-import javax.inject.Inject
-
-internal class SaveIsConnectedUseCase @Inject constructor(
- private val repository: PreferenceRepository
-) {
- operator fun invoke(isConnected: Boolean) {
- repository.saveIsConnectedSync(isConnected)
- }
-
- fun sync(isConnected: Boolean) {
- repository.saveIsConnectedSync(isConnected)
- }
-}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt
index 32621972b..a8d40464b 100644
--- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt
@@ -9,6 +9,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import dev.firezone.android.tunnel.callback.TunnelListener
import dev.firezone.android.tunnel.TunnelManager
import dev.firezone.android.tunnel.model.Resource
+import dev.firezone.android.tunnel.model.Tunnel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@@ -25,41 +26,17 @@ internal class SessionViewModel @Inject constructor(
val actionLiveData: LiveData = actionMutableLiveData
private val tunnelListener = object: TunnelListener {
- override fun onSetInterfaceConfig(
- tunnelAddressIPv4: String,
- tunnelAddressIPv6: String,
- dnsAddress: String,
- dnsFallbackStrategy: String
- ) {
- //TODO("Not yet implemented")
+ override fun onTunnelStateUpdate(state: Tunnel.State) {
+ TODO("Not yet implemented")
}
- override fun onTunnelReady(): Boolean {
- //TODO("Not yet implemented")
- return true
- }
-
- override fun onAddRoute(cidrAddress: String) {
- //TODO("Not yet implemented")
- }
-
- override fun onRemoveRoute(cidrAddress: String) {
- //TODO("Not yet implemented")
- }
-
- override fun onUpdateResources(resources: List) {
- //TODO("Not yet implemented")
+ override fun onResourcesUpdate(resources: List) {
Log.d("TunnelManager", "onUpdateResources: $resources")
_uiState.value = _uiState.value.copy (
resources = resources
)
}
- override fun onDisconnect(error: String?): Boolean {
- //TODO("Not yet implemented")
- return true
- }
-
override fun onError(error: String): Boolean {
//TODO("Not yet implemented")
return true
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt
index c14178841..e95f35574 100644
--- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelManager.kt
@@ -2,113 +2,29 @@ package dev.firezone.android.tunnel
import android.content.Context
import android.content.Intent
-import android.net.VpnService
-import android.provider.Settings
+import android.content.SharedPreferences
import android.util.Log
-import com.google.firebase.installations.FirebaseInstallations
-import com.squareup.moshi.Moshi
-import com.squareup.moshi.adapter
-import dev.firezone.android.BuildConfig
-import dev.firezone.android.core.domain.preference.GetConfigUseCase
-import dev.firezone.android.core.domain.preference.SaveIsConnectedUseCase
-import dev.firezone.android.tunnel.callback.ConnlibCallback
-import dev.firezone.android.tunnel.model.Resource
import dev.firezone.android.tunnel.callback.TunnelListener
+import dev.firezone.android.tunnel.data.TunnelRepository
import dev.firezone.android.tunnel.model.Tunnel
-import dev.firezone.android.tunnel.model.TunnelConfig
import java.lang.ref.WeakReference
-import java.nio.file.Files
-import java.nio.file.Paths
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-@OptIn(ExperimentalStdlibApi::class)
internal class TunnelManager @Inject constructor(
private val appContext: Context,
- private val getConfigUseCase: GetConfigUseCase,
- private val saveIsConnectedUseCase: SaveIsConnectedUseCase,
- private val moshi: Moshi,
+ private val tunnelRepository: TunnelRepository,
) {
- private var activeTunnel: Tunnel? = null
-
private val listeners: MutableSet> = mutableSetOf()
- private val callback: ConnlibCallback = object: ConnlibCallback {
- override fun onUpdateResources(resourceListJSON: String) {
- // TODO: Call into client app to update resources list and routing table
- Log.d(TAG, "onUpdateResources: $resourceListJSON")
- moshi.adapter>().fromJson(resourceListJSON)?.let { resources ->
- listeners.onEach {
- it.get()?.onUpdateResources(resources)
- }
+ private val tunnelRepositoryListener = SharedPreferences.OnSharedPreferenceChangeListener { _, s ->
+ if (s == TunnelRepository.RESOURCES_KEY) {
+ listeners.forEach {
+ it.get()?.onResourcesUpdate(tunnelRepository.getResources())
}
}
-
- override fun onSetInterfaceConfig(
- tunnelAddressIPv4: String,
- tunnelAddressIPv6: String,
- dnsAddress: String,
- dnsFallbackStrategy: String
- ): Int {
- Log.d(TAG, "onSetInterfaceConfig: [IPv4:$tunnelAddressIPv4] [IPv6:$tunnelAddressIPv6] [dns:$dnsAddress] [dnsFallbackStrategy:$dnsFallbackStrategy]")
-
- val tunnel = Tunnel(
- config = TunnelConfig(
- tunnelAddressIPv4, tunnelAddressIPv6, dnsAddress, dnsFallbackStrategy
- )
- )
-
- listeners.onEach {
- it.get()?.onSetInterfaceConfig(tunnelAddressIPv4, tunnelAddressIPv6, dnsAddress, dnsFallbackStrategy)
- }
-
- return buildVpnService(tunnelAddressIPv4, tunnelAddressIPv6).establish()?.detachFd() ?: -1
- }
-
- 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) {
@@ -119,6 +35,9 @@ internal class TunnelManager @Inject constructor(
if (!contains) {
listeners.add(WeakReference(listener))
}
+
+ tunnelRepository.addListener(tunnelRepositoryListener)
+ tunnelRepository.setState(Tunnel.State.Connecting)
}
fun removeListener(listener: TunnelListener) {
@@ -128,99 +47,39 @@ internal class TunnelManager @Inject constructor(
it.clear()
listeners.remove(it)
}
+
+ if (listeners.isEmpty()) {
+ tunnelRepository.removeListener(tunnelRepositoryListener)
+ }
}
- fun startVPN() {
+ fun connect() {
+ startVPNService()
+ }
+
+ fun disconnect() {
+ stopVPNService()
+ }
+
+ private fun startVPNService() {
val intent = Intent(appContext, TunnelService::class.java)
intent.action = TunnelService.ACTION_CONNECT
appContext.startService(intent)
}
- fun stopVPN() {
+ private fun stopVPNService() {
val intent = Intent(appContext, TunnelService::class.java)
intent.action = TunnelService.ACTION_DISCONNECT
appContext.startService(intent)
}
- fun connect() {
- try {
- val config = getConfigUseCase.sync()
-
- Log.d("Connlib", "accountId: ${config.accountId}")
- Log.d("Connlib", "token: ${config.token}")
-
- if (config.accountId != null && config.token != null) {
- Log.d("Connlib", "Attempting to establish TunnelSession...")
- sessionPtr = TunnelSession.connect(
- controlPlaneUrl = BuildConfig.CONTROL_PLANE_URL,
- token = config.token,
- deviceId = deviceId(),
- logDir = appContext.filesDir.absolutePath,
- callback = callback
- )
- Log.d("Connlib", "connlib session started! sessionPtr: ${sessionPtr}")
- setConnectionStatus(true)
- }
- } catch (exception: Exception) {
- Log.e("Connection error:", exception.message.toString())
- }
- }
-
- fun disconnect() {
- try {
- TunnelSession.disconnect(sessionPtr!!)
- setConnectionStatus(false)
- } catch (exception: Exception) {
- Log.e("Disconnection error:", exception.message.toString())
- }
- }
-
- private fun deviceId(): String {
- val deviceId = FirebaseInstallations
- .getInstance()
- .getId()
-
- Log.d("Connlib", "Device ID: ${deviceId}")
-
- return deviceId.toString()
- }
-
- private fun getLogDir(): String {
- // Create log directory if it doesn't exist
- val logDir = appContext.cacheDir.absolutePath + "/log"
- Files.createDirectories(Paths.get(logDir))
- return logDir
- }
-
- private fun setConnectionStatus(value: Boolean) {
- saveIsConnectedUseCase.sync(value)
- }
-
- private fun buildVpnService(ipv4Address: String, ipv6Address: String): VpnService.Builder =
- TunnelService().Builder().apply {
- addAddress(ipv4Address, 32)
- addAddress(ipv6Address, 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")
-
- // TODO: Can we do better?
- setMtu(1280)
- }
-
internal companion object {
- var sessionPtr: Long? = null
-
private const val TAG: String = "TunnelManager"
init {
- Log.d("Connlib","Attempting to load library from main app...")
+ Log.d(TAG,"Attempting to load library from main app...")
System.loadLibrary("connlib")
- Log.d("Connlib","Library loaded from main app!")
+ Log.d(TAG,"Library loaded from main app!")
}
}
}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt
index ed323f841..3920c0629 100644
--- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt
@@ -1,27 +1,264 @@
package dev.firezone.android.tunnel
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Intent
import android.net.VpnService
+import android.system.OsConstants
import android.util.Log
+import androidx.core.app.NotificationCompat
+import com.google.firebase.installations.FirebaseInstallations
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.adapter
+import dagger.hilt.android.AndroidEntryPoint
+import dev.firezone.android.BuildConfig
+import dev.firezone.android.R
+import dev.firezone.android.core.domain.preference.GetConfigUseCase
+import dev.firezone.android.core.presentation.MainActivity
+import dev.firezone.android.tunnel.callback.ConnlibCallback
+import dev.firezone.android.tunnel.data.TunnelRepository
+import dev.firezone.android.tunnel.model.Resource
+import dev.firezone.android.tunnel.model.Tunnel
+import dev.firezone.android.tunnel.model.TunnelConfig
+import java.nio.file.Files
+import java.nio.file.Paths
+import javax.inject.Inject
+@AndroidEntryPoint
+@OptIn(ExperimentalStdlibApi::class)
class TunnelService: VpnService() {
+
+ @Inject
+ internal lateinit var getConfigUseCase: GetConfigUseCase
+
+ @Inject
+ internal lateinit var tunnelRepository: TunnelRepository
+
+ @Inject
+ internal lateinit var moshi: Moshi
+
+ private var sessionPtr: Long? = null
+
+ private val activeTunnel: Tunnel?
+ get() = tunnelRepository.get()
+
+ private val callback: ConnlibCallback = object: ConnlibCallback {
+ override fun onUpdateResources(resourceListJSON: String) {
+ Log.d(TAG, "onUpdateResources: $resourceListJSON")
+ moshi.adapter>().fromJson(resourceListJSON)?.let { resources ->
+ tunnelRepository.setResources(resources)
+ }
+ }
+
+ override fun onSetInterfaceConfig(
+ tunnelAddressIPv4: String,
+ tunnelAddressIPv6: String,
+ dnsAddress: String,
+ dnsFallbackStrategy: String
+ ): Int {
+ Log.d(TAG, "onSetInterfaceConfig: [IPv4:$tunnelAddressIPv4] [IPv6:$tunnelAddressIPv6] [dns:$dnsAddress] [dnsFallbackStrategy:$dnsFallbackStrategy]")
+
+ tunnelRepository.setConfig(
+ TunnelConfig(
+ tunnelAddressIPv4, tunnelAddressIPv6, dnsAddress, dnsFallbackStrategy
+ )
+ )
+
+ // TODO: throw error if failed to establish VpnService
+ val fd = buildVpnService().establish()?.detachFd() ?: -1
+ protect(fd)
+ return fd
+ }
+
+ override fun onTunnelReady(): Boolean {
+ Log.d(TAG, "onTunnelReady")
+
+ tunnelRepository.setState(Tunnel.State.Up)
+ updateStatusNotification("Status: Connected")
+ 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")
+
+ tunnelRepository.addRoute(cidrAddress)
+ }
+
+ override fun onRemoveRoute(cidrAddress: String) {
+ Log.d(TAG, "onRemoveRoute: $cidrAddress")
+
+ tunnelRepository.removeRoute(cidrAddress)
+ }
+
+ override fun onDisconnect(error: String?): Boolean {
+ Log.d(TAG, "onDisconnect $error")
+
+ onTunnelStateUpdate(Tunnel.State.Down)
+ return true
+ }
+ }
+
override fun onCreate() {
super.onCreate()
- Log.d("FirezoneVpnService", "onCreate")
+ Log.d(TAG, "onCreate")
}
override fun onDestroy() {
super.onDestroy()
- Log.d("FirezoneVpnService", "onDestroy")
+ Log.d(TAG, "onDestroy")
}
- override fun onStartCommand(intent: android.content.Intent?, flags: Int, startId: Int): Int {
- Log.d("FirezoneVpnService", "onStartCommand")
- return super.onStartCommand(intent, flags, startId)
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d(TAG, "onStartCommand")
+
+ if (intent != null && ACTION_DISCONNECT == intent.action) {
+ disconnect()
+ return START_NOT_STICKY
+ }
+ connect()
+ return START_STICKY
+ }
+
+ private fun onTunnelStateUpdate(state: Tunnel.State) {
+ tunnelRepository.setState(state)
+ }
+
+ private fun connect() {
+ try {
+ val config = getConfigUseCase.sync()
+
+ Log.d("Connlib", "accountId: ${config.accountId}")
+ Log.d("Connlib", "token: ${config.token}")
+
+ if (config.accountId != null && config.token != null) {
+ Log.d("Connlib", "Attempting to establish TunnelSession...")
+ sessionPtr = TunnelSession.connect(
+ controlPlaneUrl = BuildConfig.CONTROL_PLANE_URL,
+ token = config.token,
+ deviceId = deviceId(),
+ logDir = getLogDir(),
+ callback = callback
+ )
+ Log.d(TAG, "connlib session started! sessionPtr: $sessionPtr")
+
+ onTunnelStateUpdate(Tunnel.State.Connecting)
+
+ updateStatusNotification("Status: Connecting...")
+ }
+ } catch (exception: Exception) {
+ Log.e(TAG, exception.message.toString())
+ }
+ }
+
+ private fun disconnect() {
+ try {
+ sessionPtr?.let {
+ TunnelSession.disconnect(it)
+ }
+ tunnelRepository.setState(Tunnel.State.Down)
+ } catch (exception: Exception) {
+ Log.e(TAG, exception.message.toString())
+ }
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ }
+
+ private fun deviceId(): String {
+ val deviceId = FirebaseInstallations.getInstance().id
+
+ Log.d(TAG, "Device ID: $deviceId")
+
+ return deviceId.toString()
+ }
+
+ private fun getLogDir(): String {
+ // Create log directory if it doesn't exist
+ val logDir = cacheDir.absolutePath + "/log"
+ Files.createDirectories(Paths.get(logDir))
+ return logDir
+ }
+
+ private fun configIntent(): PendingIntent? {
+ return PendingIntent.getActivity(
+ this, 0, Intent(this, MainActivity::class.java),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+
+ private fun buildVpnService(): VpnService.Builder =
+ TunnelService().Builder().apply {
+ activeTunnel?.let { tunnel ->
+ allowFamily(OsConstants.AF_INET)
+ allowFamily(OsConstants.AF_INET6);
+ setMetered(false); // Inherit the metered status from the underlying networks.
+ setUnderlyingNetworks(null); // Use all available networks.
+
+ addAddress(tunnel.config.tunnelAddressIPv4, 32)
+ addAddress(tunnel.config.tunnelAddressIPv6, 128)
+
+ addDnsServer(tunnel.config.dnsAddress)
+
+ /*tunnel.routes.forEach {
+ addRoute(it, 32)
+ }*/
+
+ // TODO: These are the staging Resources. Remove these in favor of the onUpdateResources callback.
+ addRoute("100.100.111.1", 32)
+ addRoute("172.31.82.179", 32)
+ addRoute("172.31.83.10", 32)
+ addRoute("172.31.92.238", 32)
+ addRoute("172.31.93.123", 32)
+
+ setSession(SESSION_NAME)
+
+ // TODO: Can we do better?
+ setMtu(DEFAULT_MTU)
+ }
+ }
+
+ private fun updateStatusNotification(message: String?) {
+ val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+
+ val chan = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ NOTIFICATION_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ chan.description = "firezone vpn status"
+
+ manager.createNotificationChannel(chan)
+
+ val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
+ val notification = notificationBuilder.setOngoing(true)
+ .setSmallIcon(R.drawable.ic_firezone_logo)
+ .setContentTitle(NOTIFICATION_TITLE)
+ .setContentText(message)
+ .setPriority(NotificationManager.IMPORTANCE_MIN)
+ .setCategory(Notification.CATEGORY_SERVICE)
+ .setContentIntent(configIntent())
+ .build()
+
+ startForeground(STATUS_NOTIFICATION_ID, notification)
}
companion object {
- val ACTION_CONNECT = "dev.firezone.android.tunnel.CONNECT"
- val ACTION_DISCONNECT = "dev.firezone.android.tunnel.DISCONNECT"
+ const val ACTION_CONNECT = "dev.firezone.android.tunnel.CONNECT"
+ const val ACTION_DISCONNECT = "dev.firezone.android.tunnel.DISCONNECT"
+
+ private const val NOTIFICATION_CHANNEL_ID = "firezone-vpn-status"
+ private const val NOTIFICATION_CHANNEL_NAME = "firezone-vpn-status"
+ private const val STATUS_NOTIFICATION_ID = 1337
+ private const val NOTIFICATION_TITLE = "Firezone Vpn Status"
+
+ private const val TAG: String = "TunnelService"
+ private const val SESSION_NAME: String = "Firezone VPN"
+ private const val DEFAULT_MTU: Int = 1280
}
}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/callback/TunnelListener.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/callback/TunnelListener.kt
index 54d3e305d..b7017b669 100644
--- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/callback/TunnelListener.kt
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/callback/TunnelListener.kt
@@ -1,20 +1,13 @@
package dev.firezone.android.tunnel.callback
import dev.firezone.android.tunnel.model.Resource
+import dev.firezone.android.tunnel.model.Tunnel
interface TunnelListener {
- fun onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, dnsFallbackStrategy: String)
+ fun onTunnelStateUpdate(state: Tunnel.State)
- fun onTunnelReady(): Boolean
-
- fun onAddRoute(cidrAddress: String)
-
- fun onRemoveRoute(cidrAddress: String)
-
- fun onUpdateResources(resources: List)
-
- fun onDisconnect(error: String?): Boolean
+ fun onResourcesUpdate(resources: List)
fun onError(error: String): Boolean
}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/data/TunnelRepository.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/data/TunnelRepository.kt
new file mode 100644
index 000000000..2db2d9761
--- /dev/null
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/data/TunnelRepository.kt
@@ -0,0 +1,43 @@
+package dev.firezone.android.tunnel.data
+
+import android.content.SharedPreferences
+import dev.firezone.android.tunnel.model.Resource
+import dev.firezone.android.tunnel.model.Tunnel
+import dev.firezone.android.tunnel.model.TunnelConfig
+
+interface TunnelRepository {
+
+ fun get(): Tunnel?
+
+ fun setConfig(config: TunnelConfig)
+
+ fun getConfig(): TunnelConfig?
+
+ fun setState(state: Tunnel.State)
+
+ fun getState(): Tunnel.State
+
+ fun setResources(resources: List)
+
+ fun getResources(): List
+
+ fun addRoute(route: String)
+
+ fun removeRoute(route: String)
+
+ fun getRoutes(): List
+
+ fun clear()
+
+ fun addListener(callback: SharedPreferences.OnSharedPreferenceChangeListener)
+
+ fun removeListener(callback: SharedPreferences.OnSharedPreferenceChangeListener)
+
+ companion object {
+ const val TAG = "TunnelRepository"
+ const val CONFIG_KEY = "tunnelConfigKey"
+ const val STATE_KEY = "tunnelStateKey"
+ const val RESOURCES_KEY = "tunnelResourcesKey"
+ const val ROUTES_KEY = "tunnelRoutesKey"
+ }
+}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/data/TunnelRepositoryImpl.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/data/TunnelRepositoryImpl.kt
new file mode 100644
index 000000000..b199d3dde
--- /dev/null
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/data/TunnelRepositoryImpl.kt
@@ -0,0 +1,112 @@
+package dev.firezone.android.tunnel.data
+
+import android.content.SharedPreferences
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.adapter
+import dev.firezone.android.tunnel.data.TunnelRepository.Companion.CONFIG_KEY
+import dev.firezone.android.tunnel.data.TunnelRepository.Companion.RESOURCES_KEY
+import dev.firezone.android.tunnel.data.TunnelRepository.Companion.ROUTES_KEY
+import dev.firezone.android.tunnel.data.TunnelRepository.Companion.STATE_KEY
+import dev.firezone.android.tunnel.model.Resource
+import dev.firezone.android.tunnel.model.Tunnel
+import dev.firezone.android.tunnel.model.TunnelConfig
+import java.lang.Exception
+import javax.inject.Inject
+
+@OptIn(ExperimentalStdlibApi::class)
+class TunnelRepositoryImpl @Inject constructor(
+ private val sharedPreferences: SharedPreferences,
+ private val moshi: Moshi,
+) : TunnelRepository {
+
+ private val lock = Any()
+
+ override fun addListener(callback: SharedPreferences.OnSharedPreferenceChangeListener) {
+ sharedPreferences.registerOnSharedPreferenceChangeListener(callback)
+ }
+
+ override fun removeListener(callback: SharedPreferences.OnSharedPreferenceChangeListener) {
+ sharedPreferences.unregisterOnSharedPreferenceChangeListener(callback)
+ }
+
+ override fun get(): Tunnel? = synchronized(lock) {
+ return try {
+ Tunnel(
+ config = requireNotNull(getConfig()),
+ state = getState(),
+ routes = getRoutes(),
+ resources = getResources(),
+ )
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ override fun setConfig(config: TunnelConfig) {
+ synchronized(lock) {
+ val json = moshi.adapter().toJson(config)
+ sharedPreferences.edit().putString(CONFIG_KEY, json).apply()
+ }
+ }
+
+ override fun getConfig(): TunnelConfig? = synchronized(lock) {
+ val json = sharedPreferences.getString(CONFIG_KEY, "{}") ?: "{}"
+ return moshi.adapter().fromJson(json)
+ }
+
+ override fun setState(state: Tunnel.State) {
+ synchronized(lock) {
+ sharedPreferences.edit().putString(STATE_KEY, state.name).apply()
+ }
+ }
+
+ override fun getState(): Tunnel.State {
+ val json = sharedPreferences.getString(STATE_KEY, null)
+ return json?.let { Tunnel.State.valueOf(it) } ?: Tunnel.State.Down
+ }
+
+ override fun setResources(resources: List) {
+ synchronized(lock) {
+ val json = moshi.adapter>().toJson(resources)
+ sharedPreferences.edit().putString(RESOURCES_KEY, json).apply()
+ }
+ }
+
+ override fun getResources(): List {
+ synchronized(lock) {
+ val json = sharedPreferences.getString(RESOURCES_KEY, "[]") ?: "[]"
+ return moshi.adapter>().fromJson(json) ?: emptyList()
+ }
+ }
+
+ override fun addRoute(route: String) {
+ synchronized(lock) {
+ getRoutes().toMutableList().run {
+ add(route)
+ val json = moshi.adapter>().toJson(this)
+ sharedPreferences.edit().putString(ROUTES_KEY, json).apply()
+ }
+ }
+ }
+
+ override fun removeRoute(route: String) {
+ synchronized(lock) {
+ getRoutes().toMutableList().run {
+ remove(route)
+ val json = moshi.adapter>().toJson(this)
+ sharedPreferences.edit().putString(ROUTES_KEY, json).apply()
+ }
+ }
+ }
+
+ override fun getRoutes(): List = synchronized(lock) {
+ val json = sharedPreferences.getString(ROUTES_KEY, "[]") ?: "[]"
+ return moshi.adapter>().fromJson(json) ?: emptyList()
+ }
+
+ override fun clear() {
+ synchronized(lock) {
+ sharedPreferences.edit().clear().apply()
+ }
+ }
+}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/di/TunnelModule.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/di/TunnelModule.kt
new file mode 100644
index 000000000..fa8a783e1
--- /dev/null
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/di/TunnelModule.kt
@@ -0,0 +1,51 @@
+package dev.firezone.android.tunnel.di
+
+import android.app.Application
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKey
+import com.squareup.moshi.Moshi
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import dev.firezone.android.tunnel.TunnelManager
+import dev.firezone.android.tunnel.data.TunnelRepository
+import dev.firezone.android.tunnel.data.TunnelRepositoryImpl
+import javax.inject.Named
+import javax.inject.Singleton
+
+internal const val TUNNEL_ENCRYPTED_SHARED_PREFERENCES = "tunnelEncryptedSharedPreferences"
+
+@Module
+@InstallIn(SingletonComponent::class)
+object TunnelModule {
+
+ @Singleton
+ @Provides
+ internal fun provideTunnelRepository(
+ @Named(TunnelRepository.TAG) sharedPreferences: SharedPreferences,
+ moshi: Moshi,
+ ): TunnelRepository = TunnelRepositoryImpl(sharedPreferences, moshi)
+
+ @Provides
+ @Named(TunnelRepository.TAG)
+ internal fun provideTunnelEncryptedSharedPreferences(app: Application): SharedPreferences =
+ EncryptedSharedPreferences.create(
+ app.applicationContext,
+ TUNNEL_ENCRYPTED_SHARED_PREFERENCES,
+ MasterKey.Builder(app.applicationContext)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build(),
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ )
+
+ @Provides
+ internal fun provideTunnelManager(
+ @ApplicationContext appContext: Context,
+ tunnelRepository: TunnelRepository,
+ ): TunnelManager = TunnelManager(appContext, tunnelRepository)
+}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt
index d6aa2a88f..651368b33 100644
--- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt
@@ -1,11 +1,14 @@
package dev.firezone.android.tunnel.model
+import android.os.Parcelable
import com.squareup.moshi.JsonClass
+import kotlinx.parcelize.Parcelize
@JsonClass(generateAdapter = true)
+@Parcelize
data class Resource(
val type: String,
val id: String,
val address: String,
val name: String,
-)
+): Parcelable
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Tunnel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Tunnel.kt
index 1b39f791a..b61bdec90 100644
--- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Tunnel.kt
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Tunnel.kt
@@ -1,15 +1,19 @@
package dev.firezone.android.tunnel.model
-data class Tunnel(
- val config: TunnelConfig,
- var state: State = State.Down,
- val routes: MutableList = mutableListOf(),
- val resources: MutableList = mutableListOf(),
-) {
+import android.os.Parcelable
+import com.squareup.moshi.JsonClass
+import kotlinx.parcelize.Parcelize
- sealed interface State {
- object Up: State
- object CONNECTING: State
- object Down: State
+@JsonClass(generateAdapter = true)
+@Parcelize
+data class Tunnel(
+ val config: TunnelConfig = TunnelConfig(),
+ var state: State = State.Down,
+ val routes: List = emptyList(),
+ val resources: List = emptyList(),
+): Parcelable {
+
+ enum class State {
+ Up, Connecting, Down;
}
}
diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/TunnelConfig.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/TunnelConfig.kt
index 54ed8e1de..d4c852f95 100644
--- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/TunnelConfig.kt
+++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/TunnelConfig.kt
@@ -1,8 +1,12 @@
package dev.firezone.android.tunnel.model
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
data class TunnelConfig (
- val tunnelAddressIPv4: String,
- val tunnelAddressIPv6: String,
- val dnsAddress: String,
- val dnsFallbackStrategy: String,
-)
+ val tunnelAddressIPv4: String = "",
+ val tunnelAddressIPv6: String = "",
+ val dnsAddress: String = "",
+ val dnsFallbackStrategy: String = "",
+): Parcelable