diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 396bfad..eafc706 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -1,4 +1,3 @@ -# name of the workflow name: Android CI Tag Deployment (Pre-release) on: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e2c2e2c..b08a8b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -162,6 +162,7 @@ dependencies { // get tunnel lib from github packages or mavenLocal implementation(libs.tunnel) + implementation(libs.amneziawg.android) coreLibraryDesugaring(libs.desugar.jdk.libs) // logging diff --git a/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/8.json b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/8.json new file mode 100644 index 0000000..a35f7c3 --- /dev/null +++ b/app/schemas/com.zaneschepke.wireguardautotunnel.data.AppDatabase/8.json @@ -0,0 +1,190 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "b4d4a7c489f6b2f0d3aa4fa6f37b4935", + "entities": [ + { + "tableName": "Settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_tunnel_enabled` INTEGER NOT NULL, `is_tunnel_on_mobile_data_enabled` INTEGER NOT NULL, `trusted_network_ssids` TEXT NOT NULL, `is_always_on_vpn_enabled` INTEGER NOT NULL, `is_tunnel_on_ethernet_enabled` INTEGER NOT NULL, `is_shortcuts_enabled` INTEGER NOT NULL DEFAULT false, `is_tunnel_on_wifi_enabled` INTEGER NOT NULL DEFAULT false, `is_kernel_enabled` INTEGER NOT NULL DEFAULT false, `is_restore_on_boot_enabled` INTEGER NOT NULL DEFAULT false, `is_multi_tunnel_enabled` INTEGER NOT NULL DEFAULT false, `is_auto_tunnel_paused` INTEGER NOT NULL DEFAULT false, `is_ping_enabled` INTEGER NOT NULL DEFAULT false, `is_amnezia_enabled` INTEGER NOT NULL DEFAULT false)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAutoTunnelEnabled", + "columnName": "is_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTunnelOnMobileDataEnabled", + "columnName": "is_tunnel_on_mobile_data_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trustedNetworkSSIDs", + "columnName": "trusted_network_ssids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isAlwaysOnVpnEnabled", + "columnName": "is_always_on_vpn_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isTunnelOnEthernetEnabled", + "columnName": "is_tunnel_on_ethernet_enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShortcutsEnabled", + "columnName": "is_shortcuts_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isTunnelOnWifiEnabled", + "columnName": "is_tunnel_on_wifi_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isKernelEnabled", + "columnName": "is_kernel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isRestoreOnBootEnabled", + "columnName": "is_restore_on_boot_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isMultiTunnelEnabled", + "columnName": "is_multi_tunnel_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isAutoTunnelPaused", + "columnName": "is_auto_tunnel_paused", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isPingEnabled", + "columnName": "is_ping_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isAmneziaEnabled", + "columnName": "is_amnezia_enabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TunnelConfig", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `wg_quick` TEXT NOT NULL, `tunnel_networks` TEXT NOT NULL DEFAULT '', `is_mobile_data_tunnel` INTEGER NOT NULL DEFAULT false, `is_primary_tunnel` INTEGER NOT NULL DEFAULT false, `am_quick` TEXT NOT NULL DEFAULT '')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wgQuick", + "columnName": "wg_quick", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tunnelNetworks", + "columnName": "tunnel_networks", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isMobileDataTunnel", + "columnName": "is_mobile_data_tunnel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "isPrimaryTunnel", + "columnName": "is_primary_tunnel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "amQuick", + "columnName": "am_quick", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_TunnelConfig_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_TunnelConfig_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b4d4a7c489f6b2f0d3aa4fa6f37b4935')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt index 2ccb894..1b1fe5a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/WireGuardAutoTunnel.kt @@ -9,6 +9,8 @@ import com.zaneschepke.wireguardautotunnel.service.tile.AutoTunnelControlTile import com.zaneschepke.wireguardautotunnel.service.tile.TunnelControlTile import com.zaneschepke.wireguardautotunnel.util.ReleaseTree import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel import timber.log.Timber import xyz.teamgravity.pin_lock_compose.PinManager @@ -21,7 +23,16 @@ class WireGuardAutoTunnel : Application() { PinManager.initialize(this) } + override fun onLowMemory() { + super.onLowMemory() + applicationScope.cancel("onLowMemory() called by system") + applicationScope = MainScope() + } + companion object { + + var applicationScope = MainScope() + lateinit var instance: WireGuardAutoTunnel private set diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt index 3cd52c2..b9e73f7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/AppDatabase.kt @@ -6,12 +6,12 @@ import androidx.room.DeleteColumn import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.room.migration.AutoMigrationSpec -import com.zaneschepke.wireguardautotunnel.data.model.Settings -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig @Database( entities = [Settings::class, TunnelConfig::class], - version = 7, + version = 8, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -33,6 +33,7 @@ import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig to = 7, spec = RemoveLegacySettingColumnsMigration::class, ), + AutoMigration(7, 8) ], exportSchema = true, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt index ffd1574..321a6c9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/SettingsDao.kt @@ -5,7 +5,7 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import com.zaneschepke.wireguardautotunnel.data.model.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.Settings import kotlinx.coroutines.flow.Flow @Dao diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt index 96b740d..7eb9000 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/TunnelConfigDao.kt @@ -5,7 +5,7 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/GeneralState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt similarity index 90% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/GeneralState.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt index 17a0893..3de9bb7 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/GeneralState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/GeneralState.kt @@ -1,4 +1,4 @@ -package com.zaneschepke.wireguardautotunnel.data.model +package com.zaneschepke.wireguardautotunnel.data.domain data class GeneralState( val locationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt similarity index 89% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt index 80b0761..450feab 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/Settings.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/Settings.kt @@ -1,4 +1,4 @@ -package com.zaneschepke.wireguardautotunnel.data.model +package com.zaneschepke.wireguardautotunnel.data.domain import androidx.room.ColumnInfo import androidx.room.Entity @@ -50,4 +50,9 @@ data class Settings( defaultValue = "false", ) val isPingEnabled: Boolean = false, + @ColumnInfo( + name = "is_amnezia_enabled", + defaultValue = "false", + ) + val isAmneziaEnabled: Boolean = false, ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt similarity index 60% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt index b16b372..724d842 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/model/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt @@ -1,4 +1,4 @@ -package com.zaneschepke.wireguardautotunnel.data.model +package com.zaneschepke.wireguardautotunnel.data.domain import androidx.room.ColumnInfo import androidx.room.Entity @@ -27,12 +27,24 @@ data class TunnelConfig( defaultValue = "false", ) val isPrimaryTunnel: Boolean = false, + @ColumnInfo( + name = "am_quick", + defaultValue = "", + ) + val amQuick: String = "", ) { companion object { - fun configFromQuick(wgQuick: String): Config { + fun configFromWgQuick(wgQuick: String): Config { val inputStream: InputStream = wgQuick.byteInputStream() - val reader = inputStream.bufferedReader(Charsets.UTF_8) - return Config.parse(reader) + return inputStream.bufferedReader(Charsets.UTF_8).use { + Config.parse(it) + } + } + fun configFromAmQuick(amQuick: String) : org.amnezia.awg.config.Config { + val inputStream: InputStream = amQuick.byteInputStream() + return inputStream.bufferedReader(Charsets.UTF_8).use { + org.amnezia.awg.config.Config.parse(it) + } } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt index 9054036..e10e4f9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRepository.kt @@ -1,6 +1,6 @@ package com.zaneschepke.wireguardautotunnel.data.repository -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig interface AppDataRepository { suspend fun getPrimaryOrFirstTunnel(): TunnelConfig? diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt index 650322a..009b2f0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppDataRoomRepository.kt @@ -1,6 +1,6 @@ package com.zaneschepke.wireguardautotunnel.data.repository -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import javax.inject.Inject class AppDataRoomRepository @Inject constructor( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt index 9b42e1c..0ad678a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/AppStateRepository.kt @@ -1,6 +1,6 @@ package com.zaneschepke.wireguardautotunnel.data.repository -import com.zaneschepke.wireguardautotunnel.data.model.GeneralState +import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState import kotlinx.coroutines.flow.Flow interface AppStateRepository { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt index 4d1bb74..1cd5419 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/DataStoreAppStateRepository.kt @@ -1,7 +1,7 @@ package com.zaneschepke.wireguardautotunnel.data.repository import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager -import com.zaneschepke.wireguardautotunnel.data.model.GeneralState +import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import timber.log.Timber diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt index ff26ac8..d218aa5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomSettingsRepository.kt @@ -1,7 +1,7 @@ package com.zaneschepke.wireguardautotunnel.data.repository import com.zaneschepke.wireguardautotunnel.data.SettingsDao -import com.zaneschepke.wireguardautotunnel.data.model.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.Settings import kotlinx.coroutines.flow.Flow class RoomSettingsRepository(private val settingsDoa: SettingsDao) : SettingsRepository { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt index 199079a..f2f79e4 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/RoomTunnelConfigRepository.kt @@ -1,7 +1,7 @@ package com.zaneschepke.wireguardautotunnel.data.repository import com.zaneschepke.wireguardautotunnel.data.TunnelConfigDao -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt index 2f27f90..e4250f9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/SettingsRepository.kt @@ -1,6 +1,6 @@ package com.zaneschepke.wireguardautotunnel.data.repository -import com.zaneschepke.wireguardautotunnel.data.model.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.Settings import kotlinx.coroutines.flow.Flow interface SettingsRepository { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt index 505815e..999464b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/TunnelConfigRepository.kt @@ -1,6 +1,6 @@ package com.zaneschepke.wireguardautotunnel.data.repository -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt index 1d01659..2b319ac 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/module/TunnelModule.kt @@ -40,14 +40,21 @@ class TunnelModule { return WgQuickBackend(context, rootShell, ToolsInstaller(context, rootShell)) } + @Provides + @Singleton + fun provideAmneziaBackend(@ApplicationContext context: Context) : org.amnezia.awg.backend.Backend { + return org.amnezia.awg.backend.GoBackend(context) + } + @Provides @Singleton fun provideVpnService( + amneziaBackend: org.amnezia.awg.backend.Backend, @Userspace userspaceBackend: Backend, @Kernel kernelBackend: Backend, appDataRepository: AppDataRepository ): VpnService { - return WireGuardTunnel(userspaceBackend, kernelBackend, appDataRepository) + return WireGuardTunnel(amneziaBackend,userspaceBackend, kernelBackend, appDataRepository) } @Provides diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt index 4e22360..8b7f52d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WatcherState.kt @@ -1,20 +1,20 @@ package com.zaneschepke.wireguardautotunnel.service.foreground -import com.wireguard.android.backend.Tunnel -import com.zaneschepke.wireguardautotunnel.data.model.Settings -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig +import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState data class WatcherState( val isWifiConnected: Boolean = false, val config: TunnelConfig? = null, - val vpnStatus: Tunnel.State = Tunnel.State.DOWN, + val vpnStatus: TunnelState = TunnelState.DOWN, val isEthernetConnected: Boolean = false, val isMobileDataConnected: Boolean = false, val currentNetworkSSID: String = "", val settings: Settings = Settings() ) { - private fun isVpnConnected() = vpnStatus == Tunnel.State.UP + private fun isVpnConnected() = vpnStatus == TunnelState.UP fun isEthernetConditionMet(): Boolean { return (isEthernetConnected && settings.isTunnelOnEthernetEnabled && diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt index be31496..36bfb91 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardConnectivityWatcherService.kt @@ -5,9 +5,8 @@ import android.os.Bundle import android.os.PowerManager import androidx.core.app.ServiceCompat import androidx.lifecycle.lifecycleScope -import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.service.network.EthernetService import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService @@ -15,6 +14,7 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService +import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.util.Constants import dagger.hilt.android.AndroidEntryPoint @@ -217,10 +217,10 @@ class WireGuardConnectivityWatcherService : ForegroundService() { private suspend fun watchForPingFailure() { try { do { - if (vpnService.vpnState.value.status == Tunnel.State.UP) { + if (vpnService.vpnState.value.status == TunnelState.UP) { val tunnelConfig = vpnService.vpnState.value.tunnelConfig tunnelConfig?.let { - val config = TunnelConfig.configFromQuick(it.wgQuick) + val config = TunnelConfig.configFromWgQuick(it.wgQuick) val results = config.peers.map { peer -> val host = if (peer.endpoint.isPresent && peer.endpoint.get().resolved.isPresent) @@ -321,14 +321,14 @@ class WireGuardConnectivityWatcherService : ForegroundService() { isWifiConnected = true, ) val ssid = wifiService.getNetworkName(it.networkCapabilities) - ssid?.let { - if(it.contains(Constants.UNREADABLE_SSID)) { + ssid?.let { name -> + if(name.contains(Constants.UNREADABLE_SSID)) { Timber.w("SSID unreadable: missing permissions") } else Timber.i("Detected valid SSID") - appDataRepository.appState.setCurrentSsid(ssid) + appDataRepository.appState.setCurrentSsid(name) networkEventsFlow.value = networkEventsFlow.value.copy( - currentNetworkSSID = ssid, + currentNetworkSSID = name, ) } ?: Timber.w("Failed to read ssid") } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt index b6c2052..61fede6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/WireGuardTunnelService.kt @@ -11,6 +11,8 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.receiver.NotificationActionReceiver import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus +import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState +import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.handshakeStatus @@ -58,7 +60,7 @@ class WireGuardTunnelService : ForegroundService() { lifecycleScope.launch(Dispatchers.IO) { launch { val tunnelId = extras?.getInt(Constants.TUNNEL_EXTRA_KEY) - if (vpnService.getState() == Tunnel.State.UP) { + if (vpnService.getState() == TunnelState.UP) { vpnService.stopTunnel() } vpnService.startTunnel( @@ -77,7 +79,7 @@ class WireGuardTunnelService : ForegroundService() { private suspend fun handshakeNotifications() { var tunnelName: String? = null vpnService.vpnState.collect { state -> - state.statistics + state.statistics ?.mapPeerStats() ?.map { it.value?.handshakeStatus() } .let { statuses -> @@ -102,7 +104,7 @@ class WireGuardTunnelService : ForegroundService() { else -> {} } } - if (state.status == Tunnel.State.UP && state.tunnelConfig?.name != tunnelName) { + if (state.status == TunnelState.UP && state.tunnelConfig?.name != tunnelName) { tunnelName = state.tunnelConfig?.name launchVpnNotification( getString(R.string.tunnel_start_title), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt index afcd91c..7203e6d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt @@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.service.shortcut import android.os.Bundle import androidx.activity.ComponentActivity -import androidx.lifecycle.lifecycleScope +import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.service.foreground.Action import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager @@ -24,7 +24,7 @@ class ShortcutsActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - lifecycleScope.launch(Dispatchers.Main) { + WireGuardAutoTunnel.applicationScope.launch(Dispatchers.IO) { val settings = appDataRepository.settings.getSettings() if (settings.isShortcutsEnabled) { when (intent.getStringExtra(CLASS_NAME_EXTRA_KEY)) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt index 969c0af..e3fb8b9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/AutoTunnelControlTile.kt @@ -4,7 +4,7 @@ import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import dagger.hilt.android.AndroidEntryPoint diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index eb221b6..1ea4f8a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -3,10 +3,10 @@ package com.zaneschepke.wireguardautotunnel.service.tile import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService -import com.wireguard.android.backend.Tunnel -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager +import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope @@ -38,12 +38,12 @@ class TunnelControlTile : TileService() { scope.launch { vpnService.vpnState.collect { it -> when (it.status) { - Tunnel.State.UP -> { + TunnelState.UP -> { setActive() it.tunnelConfig?.name?.let { name -> setTileDescription(name) } } - Tunnel.State.DOWN -> { + TunnelState.DOWN -> { setInactive() val config = appDataRepository.getStartTunnelConfig()?.also { config -> manualStartConfig = config @@ -79,7 +79,7 @@ class TunnelControlTile : TileService() { unlockAndRun { scope.launch { try { - if (vpnService.getState() == Tunnel.State.UP) { + if (vpnService.getState() == TunnelState.UP) { serviceManager.stopVpnServiceForeground( this@TunnelControlTile, isManualStop = true, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelState.kt new file mode 100644 index 0000000..f95cf2a --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelState.kt @@ -0,0 +1,42 @@ +package com.zaneschepke.wireguardautotunnel.service.tunnel + +import com.wireguard.android.backend.Tunnel + +enum class TunnelState { + UP, + DOWN, + TOGGLE; + + fun toWgState() : Tunnel.State { + return when(this) { + UP -> Tunnel.State.UP + DOWN -> Tunnel.State.DOWN + TOGGLE -> Tunnel.State.TOGGLE + } + } + + fun toAmState() : org.amnezia.awg.backend.Tunnel.State { + return when(this) { + UP -> org.amnezia.awg.backend.Tunnel.State.UP + DOWN -> org.amnezia.awg.backend.Tunnel.State.DOWN + TOGGLE -> org.amnezia.awg.backend.Tunnel.State.TOGGLE + } + } + + companion object { + fun from(state: Tunnel.State) : TunnelState { + return when(state) { + Tunnel.State.DOWN -> DOWN + Tunnel.State.TOGGLE -> TOGGLE + Tunnel.State.UP -> UP + } + } + fun from(state: org.amnezia.awg.backend.Tunnel.State) : TunnelState { + return when(state) { + org.amnezia.awg.backend.Tunnel.State.DOWN -> DOWN + org.amnezia.awg.backend.Tunnel.State.TOGGLE -> TOGGLE + org.amnezia.awg.backend.Tunnel.State.UP -> UP + } + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt index 285ba40..67dfe5b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnService.kt @@ -1,15 +1,15 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel import com.wireguard.android.backend.Tunnel -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import kotlinx.coroutines.flow.StateFlow -interface VpnService : Tunnel { - suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): Tunnel.State +interface VpnService : Tunnel, org.amnezia.awg.backend.Tunnel { + suspend fun startTunnel(tunnelConfig: TunnelConfig? = null): TunnelState suspend fun stopTunnel() val vpnState: StateFlow - fun getState(): Tunnel.State + fun getState(): TunnelState } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt index e409980..24b24a6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/VpnState.kt @@ -1,11 +1,10 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel -import com.wireguard.android.backend.Statistics -import com.wireguard.android.backend.Tunnel -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig +import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics data class VpnState( - val status: Tunnel.State = Tunnel.State.DOWN, + val status: TunnelState = TunnelState.DOWN, val tunnelConfig: TunnelConfig? = null, - val statistics: Statistics? = null + val statistics: TunnelStatistics? = null ) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index 4f6b6e4..665aea6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -2,13 +2,15 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel import com.wireguard.android.backend.Backend import com.wireguard.android.backend.BackendException -import com.wireguard.android.backend.Statistics import com.wireguard.android.backend.Tunnel.State import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Userspace +import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics +import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics +import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.util.Constants import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -19,12 +21,15 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.amnezia.awg.backend.Tunnel +import org.amnezia.awg.config.Config import timber.log.Timber import javax.inject.Inject class WireGuardTunnel @Inject constructor( + private val userspaceAmneziaBackend : org.amnezia.awg.backend.Backend, @Userspace private val userspaceBackend: Backend, @Kernel private val kernelBackend: Backend, private val appDataRepository: AppDataRepository, @@ -38,46 +43,70 @@ constructor( private var backend: Backend = userspaceBackend - private var backendIsUserspace = true + private var backendIsWgUserspace = true + + private var backendIsAmneziaUserspace = false init { scope.launch { appDataRepository.settings.getSettingsFlow().collect { - if (it.isKernelEnabled && backendIsUserspace) { + if (it.isKernelEnabled && (backendIsWgUserspace || backendIsAmneziaUserspace)) { Timber.d("Setting kernel backend") backend = kernelBackend - backendIsUserspace = false - } else if (!it.isKernelEnabled && !backendIsUserspace) { - Timber.d("Setting userspace backend") + backendIsWgUserspace = false + backendIsAmneziaUserspace = false + } else if (!it.isKernelEnabled && !it.isAmneziaEnabled && !backendIsWgUserspace) { + Timber.d("Setting WireGuard userspace backend") backend = userspaceBackend - backendIsUserspace = true + backendIsWgUserspace = true + backendIsAmneziaUserspace = false + } else if (it.isAmneziaEnabled && !backendIsAmneziaUserspace) { + Timber.d("Setting Amnezia userspace backend") + backendIsAmneziaUserspace = true + backendIsWgUserspace = false } } } } - override suspend fun startTunnel(tunnelConfig: TunnelConfig?): State { + private fun setState(tunnelConfig: TunnelConfig?, tunnelState: TunnelState) : TunnelState { + return if(backendIsAmneziaUserspace) { + Timber.i("Using Amnezia backend") + val config = tunnelConfig?.let { + if(it.amQuick != "") TunnelConfig.configFromAmQuick(it.amQuick) else { + Timber.w("Using backwards compatible wg config, amnezia specific config not found.") + TunnelConfig.configFromAmQuick(it.wgQuick) + } + } + val state = userspaceAmneziaBackend.setState(this, tunnelState.toAmState(), config) + TunnelState.from(state) + } else { + Timber.i("Using Wg backend") + val wgConfig = tunnelConfig?.let { TunnelConfig.configFromWgQuick(it.wgQuick) } + val state = backend.setState( + this, + tunnelState.toWgState(), + wgConfig, + ) + TunnelState.from(state) + } + } + + override suspend fun startTunnel(tunnelConfig: TunnelConfig?): TunnelState { return try { //TODO we need better error handling here val config = tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel() if (config != null) { emitTunnelConfig(config) - val wgConfig = TunnelConfig.configFromQuick(config.wgQuick) - val state = - backend.setState( - this, - State.UP, - wgConfig, - ) - state + setState(config, TunnelState.UP) } else throw Exception("No tunnels") } catch (e: BackendException) { Timber.e("Failed to start tunnel with error: ${e.message}") - State.DOWN + TunnelState.from(State.DOWN) } } - private fun emitTunnelState(state: State) { + private fun emitTunnelState(state : TunnelState) { _vpnState.tryEmit( _vpnState.value.copy( status = state, @@ -85,7 +114,7 @@ constructor( ) } - private fun emitBackendStatistics(statistics: Statistics) { + private fun emitBackendStatistics(statistics: TunnelStatistics) { _vpnState.tryEmit( _vpnState.value.copy( statistics = statistics, @@ -103,38 +132,49 @@ constructor( override suspend fun stopTunnel() { try { - if (getState() == State.UP) { - val state = backend.setState(this, State.DOWN, null) + if (getState() == TunnelState.UP) { + val state = setState(null, TunnelState.DOWN) emitTunnelState(state) } } catch (e: BackendException) { - Timber.e("Failed to stop tunnel with error: ${e.message}") + Timber.e("Failed to stop wireguard tunnel with error: ${e.message}") + } catch (e: org.amnezia.awg.backend.BackendException) { + Timber.e("Failed to stop amnezia tunnel with error: ${e.message}") } } - override fun getState(): State { - return backend.getState(this) + override fun getState(): TunnelState { + return if(backendIsAmneziaUserspace) TunnelState.from(userspaceAmneziaBackend.getState(this)) + else TunnelState.from(backend.getState(this)) } override fun getName(): String { return _vpnState.value.tunnelConfig?.name ?: "" } - override fun onStateChange(state: State) { + + override fun onStateChange(newState: Tunnel.State) { + handleStateChange(TunnelState.from(newState)) + } + + private fun handleStateChange(state: TunnelState) { val tunnel = this emitTunnelState(state) WireGuardAutoTunnel.requestTunnelTileServiceStateUpdate(WireGuardAutoTunnel.instance) - if (state == State.UP) { + if (state == TunnelState.UP) { statsJob = scope.launch { while (true) { - val statistics = backend.getStatistics(tunnel) - emitBackendStatistics(statistics) + if(backendIsAmneziaUserspace) { + emitBackendStatistics(AmneziaStatistics(userspaceAmneziaBackend.getStatistics(tunnel))) + } else { + emitBackendStatistics(WireGuardStatistics(backend.getStatistics(tunnel))) + } delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) } } } - if (state == State.DOWN) { + if (state == TunnelState.DOWN) { try { statsJob?.cancel() } catch (e : CancellationException) { @@ -142,4 +182,8 @@ constructor( } } } + + override fun onStateChange(state: State) { + handleStateChange(TunnelState.from(state)) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/AmneziaStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/AmneziaStatistics.kt new file mode 100644 index 0000000..b995de6 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/AmneziaStatistics.kt @@ -0,0 +1,34 @@ +package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics + +import org.amnezia.awg.backend.Statistics +import org.amnezia.awg.crypto.Key + +class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics() { + override fun peerStats(peer: Key): PeerStats? { + val key = Key.fromBase64(peer.toBase64()) + val stats = statistics.peer(key) + return stats?.let { + PeerStats( + rxBytes = stats.rxBytes, + txBytes = stats.txBytes, + latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis + ) + } + } + + override fun isTunnelStale(): Boolean { + return statistics.isStale + } + + override fun getPeers(): Array { + return statistics.peers() + } + + override fun rx(): Long { + return statistics.totalRx() + } + + override fun tx(): Long { + return statistics.totalTx() + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/TunnelStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/TunnelStatistics.kt new file mode 100644 index 0000000..aeeb004 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/TunnelStatistics.kt @@ -0,0 +1,18 @@ +package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics + +import org.amnezia.awg.crypto.Key + +abstract class TunnelStatistics { + @JvmRecord + data class PeerStats(val rxBytes: Long, val txBytes: Long, val latestHandshakeEpochMillis: Long) + + abstract fun peerStats(peer: Key): PeerStats? + + abstract fun isTunnelStale() : Boolean + + abstract fun getPeers(): Array + + abstract fun rx() : Long + + abstract fun tx() : Long +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/WireGuardStatistics.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/WireGuardStatistics.kt new file mode 100644 index 0000000..ae2c363 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/statistics/WireGuardStatistics.kt @@ -0,0 +1,36 @@ +package com.zaneschepke.wireguardautotunnel.service.tunnel.statistics + +import com.wireguard.android.backend.Statistics +import org.amnezia.awg.crypto.Key + +class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics() { + override fun peerStats(peer: Key): PeerStats? { + val key = com.wireguard.crypto.Key.fromBase64(peer.toBase64()) + val peerStats = statistics.peer(key) + return peerStats?.let { + PeerStats( + txBytes = peerStats.txBytes, + rxBytes = peerStats.rxBytes, + latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis + ) + } + } + + override fun isTunnelStale(): Boolean { + return statistics.isStale + } + + override fun getPeers(): Array { + return statistics.peers().map { + Key.fromBase64(it.toBase64()) + }.toTypedArray() + } + + override fun rx(): Long { + return statistics.totalRx() + } + + override fun tx(): Long { + return statistics.totalTx() + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt index 8555d8d..d525571 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -2,12 +2,14 @@ package com.zaneschepke.wireguardautotunnel.ui import android.app.Application import android.content.ActivityNotFoundException +import android.content.Context import android.content.Intent import android.net.Uri import android.widget.Toast import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.journeyapps.barcodescanner.BarcodeEncoder import com.wireguard.android.backend.GoBackend import com.zaneschepke.logcatter.Logcatter import com.zaneschepke.logcatter.model.LogMessage @@ -19,6 +21,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber import java.time.Instant @@ -27,9 +30,7 @@ import javax.inject.Inject @HiltViewModel class AppViewModel @Inject -constructor( - private val application: Application, -) : ViewModel() { +constructor() : ViewModel() { val vpnIntent: Intent? = GoBackend.VpnService.prepare(WireGuardAutoTunnel.instance) @@ -49,68 +50,78 @@ constructor( } private fun requestPermissions() { - _appUiState.value = _appUiState.value.copy( - requestPermissions = true, - ) + _appUiState.update { + it.copy( + requestPermissions = true + ) + } } fun permissionsRequested() { - _appUiState.value = _appUiState.value.copy( - requestPermissions = false, - ) + _appUiState.update { + it.copy( + requestPermissions = false + ) + } } - fun openWebPage(url: String) { + fun openWebPage(url: String, context : Context) { try { val webpage: Uri = Uri.parse(url) val intent = Intent(Intent.ACTION_VIEW, webpage).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } - application.startActivity(intent) + context.startActivity(intent) } catch (e: ActivityNotFoundException) { Timber.e(e) - showSnackbarMessage(application.getString(R.string.no_browser_detected)) + showSnackbarMessage(context.getString(R.string.no_browser_detected)) } } fun onVpnPermissionAccepted() { - _appUiState.value = _appUiState.value.copy( - vpnPermissionAccepted = true, - ) + _appUiState.update { + it.copy( + vpnPermissionAccepted = true + ) + } } - fun launchEmail() { + fun launchEmail(context: Context) { try { val intent = Intent(Intent.ACTION_SENDTO).apply { type = Constants.EMAIL_MIME_TYPE - putExtra(Intent.EXTRA_EMAIL, arrayOf(application.getString(R.string.my_email))) - putExtra(Intent.EXTRA_SUBJECT, application.getString(R.string.email_subject)) + putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.my_email))) + putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.email_subject)) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } - application.startActivity( - Intent.createChooser(intent, application.getString(R.string.email_chooser)).apply { + context.startActivity( + Intent.createChooser(intent, context.getString(R.string.email_chooser)).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }, ) } catch (e: ActivityNotFoundException) { Timber.e(e) - showSnackbarMessage(application.getString(R.string.no_email_detected)) + showSnackbarMessage(context.getString(R.string.no_email_detected)) } } fun showSnackbarMessage(message: String) { - _appUiState.value = _appUiState.value.copy( - snackbarMessage = message, - snackbarMessageConsumed = false, - ) + _appUiState.update { + it.copy( + snackbarMessage = message, + snackbarMessageConsumed = false, + ) + } } fun snackbarMessageConsumed() { - _appUiState.value = _appUiState.value.copy( - snackbarMessage = "", - snackbarMessageConsumed = true, - ) + _appUiState.update { + it.copy( + snackbarMessage = "", + snackbarMessageConsumed = true, + ) + } } val logs = mutableStateListOf() @@ -132,17 +143,19 @@ constructor( Logcatter.clear() } - fun saveLogsToFile() { + fun saveLogsToFile(context: Context) { val fileName = "${Constants.BASE_LOG_FILE_NAME}-${Instant.now().epochSecond}.txt" val content = logs.joinToString(separator = "\n") - FileUtils.saveFileToDownloads(application.applicationContext, content, fileName) - Toast.makeText(application, application.getString(R.string.logs_saved), Toast.LENGTH_SHORT) + FileUtils.saveFileToDownloads(context.applicationContext, content, fileName) + Toast.makeText(context, context.getString(R.string.logs_saved), Toast.LENGTH_SHORT) .show() } fun setNotificationPermissionAccepted(accepted: Boolean) { - _appUiState.value = _appUiState.value.copy( - notificationPermissionAccepted = accepted, - ) + _appUiState.update { + it.copy( + notificationPermissionAccepted = accepted, + ) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index 3f00705..b08e579 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -138,7 +138,11 @@ class MainActivity : AppCompatActivity() { return@LaunchedEffect notificationPermissionState.launchPermissionRequest() } if (!appUiState.vpnPermissionAccepted) { - return@LaunchedEffect vpnActivityResultState.launch(appViewModel.vpnIntent) + return@LaunchedEffect appViewModel.vpnIntent?.let { + vpnActivityResultState.launch( + it + ) + }!! } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt index 87964b2..0fa3c57 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/RowListItem.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.wireguard.android.backend.Statistics +import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.util.NumberUtils import com.zaneschepke.wireguardautotunnel.util.toThreeDecimalPlaceString @@ -30,7 +30,7 @@ fun RowListItem( onClick: () -> Unit, rowButton: @Composable () -> Unit, expanded: Boolean, - statistics: Statistics? + statistics: TunnelStatistics? ) { Box( modifier = @@ -59,7 +59,7 @@ fun RowListItem( rowButton() } if (expanded) { - statistics?.peers()?.forEach { + statistics?.getPeers()?.forEach { Row( modifier = Modifier @@ -69,9 +69,9 @@ fun RowListItem( horizontalArrangement = Arrangement.SpaceEvenly, ) { //TODO change these to string resources - val handshakeEpoch = statistics.peer(it)!!.latestHandshakeEpochMillis - val peerTx = statistics.peer(it)!!.txBytes - val peerRx = statistics.peer(it)!!.rxBytes + val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis + val peerTx = statistics.peerStats(it)!!.txBytes + val peerRx = statistics.peerStats(it)!!.rxBytes val peerId = it.toBase64().subSequence(0, 3).toString() + "***" val handshakeSec = NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt deleted file mode 100644 index 76189a5..0000000 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/InterfaceProxy.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.zaneschepke.wireguardautotunnel.ui.models - -import com.wireguard.config.Interface - -data class InterfaceProxy( - var privateKey: String = "", - var publicKey: String = "", - var addresses: String = "", - var dnsServers: String = "", - var listenPort: String = "", - var mtu: String = "" -) { - companion object { - fun from(i: Interface): InterfaceProxy { - return InterfaceProxy( - publicKey = i.keyPair.publicKey.toBase64().trim(), - privateKey = i.keyPair.privateKey.toBase64().trim(), - addresses = i.addresses.joinToString(", ").trim(), - dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(), - listenPort = - if (i.listenPort.isPresent) { - i.listenPort.get().toString().trim() - } else { - "" - }, - mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", - ) - } - } -} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index ead04ad..2973e83 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -486,6 +486,98 @@ fun ConfigScreen( modifier = Modifier.width(IntrinsicSize.Min), ) } + if(uiState.isAmneziaEnabled) { + ConfigurationTextBox( + value = uiState.interfaceProxy.junkPacketCount, + onValueChange = { value -> viewModel.onJunkPacketCountChanged(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.junk_packet_count), + hint = stringResource(R.string.junk_packet_count).lowercase(), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.junkPacketMinSize, + onValueChange = { value -> viewModel.onJunkPacketMinSizeChanged(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.junk_packet_minimum_size), + hint = stringResource(R.string.junk_packet_minimum_size).lowercase(), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.junkPacketMaxSize, + onValueChange = { value -> viewModel.onJunkPacketMaxSizeChanged(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.junk_packet_maximum_size), + hint = stringResource(R.string.junk_packet_maximum_size).lowercase(), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.initPacketJunkSize, + onValueChange = { value -> viewModel.onInitPacketJunkSizeChanged(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.init_packet_junk_size), + hint = stringResource(R.string.init_packet_junk_size).lowercase(), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.responsePacketJunkSize, + onValueChange = { value -> viewModel.onResponsePacketJunkSize(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.response_packet_junk_size), + hint = stringResource(R.string.response_packet_junk_size).lowercase(), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.initPacketMagicHeader, + onValueChange = { value -> viewModel.onInitPacketMagicHeader(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.init_packet_magic_header), + hint = stringResource(R.string.init_packet_magic_header).lowercase(), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.responsePacketMagicHeader, + onValueChange = { value -> viewModel.onResponsePacketMagicHeader(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.response_packet_magic_header), + hint = stringResource(R.string.response_packet_magic_header).lowercase(), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.transportPacketMagicHeader, + onValueChange = { value -> viewModel.onTransportPacketMagicHeader(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.transport_packet_magic_header), + hint = stringResource(R.string.transport_packet_magic_header).lowercase(), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + ConfigurationTextBox( + value = uiState.interfaceProxy.underloadPacketMagicHeader, + onValueChange = { value -> viewModel.onUnderloadPacketMagicHeader(value) }, + keyboardActions = keyboardActions, + label = stringResource(R.string.underload_packet_magic_header), + hint = stringResource(R.string.underload_packet_magic_header).lowercase(), + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + ) + } Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt index 7abc66c..718250c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigUiState.kt @@ -1,8 +1,9 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.config -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig -import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy -import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy +import com.wireguard.config.Config +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig +import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.InterfaceProxy +import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy import com.zaneschepke.wireguardautotunnel.util.Packages data class ConfigUiState( @@ -14,5 +15,58 @@ data class ConfigUiState( val isAllApplicationsEnabled: Boolean = false, val loading: Boolean = true, val tunnel: TunnelConfig? = null, - val tunnelName: String = "" -) + val tunnelName: String = "", + val isAmneziaEnabled: Boolean = false +) { + companion object { + fun from(config : Config) : ConfigUiState { + val proxyPeers = config.peers.map { PeerProxy.from(it) } + val proxyInterface = InterfaceProxy.from(config.`interface`) + var include = true + var isAllApplicationsEnabled = false + val checkedPackages = + if (config.`interface`.includedApplications.isNotEmpty()) { + config.`interface`.includedApplications + } else if (config.`interface`.excludedApplications.isNotEmpty()) { + include = false + config.`interface`.excludedApplications + } else { + isAllApplicationsEnabled = true + emptySet() + } + return ConfigUiState( + proxyPeers, + proxyInterface, + emptyList(), + checkedPackages.toList(), + include, + isAllApplicationsEnabled, + ) + } + fun from(config: org.amnezia.awg.config.Config) : ConfigUiState { + //TODO update with new values + val proxyPeers = config.peers.map { PeerProxy.from(it) } + val proxyInterface = InterfaceProxy.from(config.`interface`) + var include = true + var isAllApplicationsEnabled = false + val checkedPackages = + if (config.`interface`.includedApplications.isNotEmpty()) { + config.`interface`.includedApplications + } else if (config.`interface`.excludedApplications.isNotEmpty()) { + include = false + config.`interface`.excludedApplications + } else { + isAllApplicationsEnabled = true + emptySet() + } + return ConfigUiState( + proxyPeers, + proxyInterface, + emptyList(), + checkedPackages.toList(), + include, + isAllApplicationsEnabled, + ) + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index b7a4fdc..4625813 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -13,10 +13,10 @@ import com.wireguard.config.Peer import com.wireguard.crypto.Key import com.wireguard.crypto.KeyPair import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository -import com.zaneschepke.wireguardautotunnel.ui.models.InterfaceProxy -import com.zaneschepke.wireguardautotunnel.ui.models.PeerProxy +import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository +import com.zaneschepke.wireguardautotunnel.ui.screens.config.model.PeerProxy import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Event import com.zaneschepke.wireguardautotunnel.util.NumberUtils @@ -27,6 +27,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -36,6 +37,7 @@ class ConfigViewModel @Inject constructor( private val application: Application, + private val settingsRepository: SettingsRepository, private val appDataRepository: AppDataRepository ) : ViewModel() { @@ -52,32 +54,17 @@ constructor( val tunnelConfig = appDataRepository.tunnels.getAll() .firstOrNull { it.id.toString() == tunnelId } + val isAmneziaEnabled = settingsRepository.getSettings().isAmneziaEnabled if (tunnelConfig != null) { - val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick) - val proxyPeers = config.peers.map { PeerProxy.from(it) } - val proxyInterface = InterfaceProxy.from(config.`interface`) - var include = true - var isAllApplicationsEnabled = false - val checkedPackages = - if (config.`interface`.includedApplications.isNotEmpty()) { - config.`interface`.includedApplications - } else if (config.`interface`.excludedApplications.isNotEmpty()) { - include = false - config.`interface`.excludedApplications - } else { - isAllApplicationsEnabled = true - emptySet() - } - ConfigUiState( - proxyPeers, - proxyInterface, - packages, - checkedPackages.toList(), - include, - isAllApplicationsEnabled, - false, - tunnelConfig, - tunnelConfig.name, + (if(isAmneziaEnabled) { + val amConfig = if(tunnelConfig.amQuick == "") tunnelConfig.wgQuick else tunnelConfig.amQuick + ConfigUiState.from(TunnelConfig.configFromAmQuick(amConfig)) + } else ConfigUiState.from(TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick))).copy( + packages = packages, + loading = false, + tunnel = tunnelConfig, + tunnelName = tunnelConfig.name, + isAmneziaEnabled = isAmneziaEnabled ) } else { ConfigUiState(loading = false, packages = packages) @@ -168,6 +155,20 @@ constructor( } } + private fun buildAmPeerListFromProxyPeers(): List { + return _uiState.value.proxyPeers.map { + val builder = org.amnezia.awg.config.Peer.Builder() + if (it.allowedIps.isNotEmpty()) builder.parseAllowedIPs(it.allowedIps.trim()) + if (it.publicKey.isNotEmpty()) builder.parsePublicKey(it.publicKey.trim()) + if (it.preSharedKey.isNotEmpty()) builder.parsePreSharedKey(it.preSharedKey.trim()) + if (it.endpoint.isNotEmpty()) builder.parseEndpoint(it.endpoint.trim()) + if (it.persistentKeepalive.isNotEmpty()) { + builder.parsePersistentKeepalive(it.persistentKeepalive.trim()) + } + builder.build() + } + } + private fun emptyCheckedPackagesList() { _uiState.value = _uiState.value.copy(checkedPackageNames = emptyList()) } @@ -190,20 +191,76 @@ constructor( return builder.build() } + private fun buildAmInterfaceListFromProxyInterface(): org.amnezia.awg.config.Interface { + val builder = org.amnezia.awg.config.Interface.Builder() + builder.parsePrivateKey(_uiState.value.interfaceProxy.privateKey.trim()) + builder.parseAddresses(_uiState.value.interfaceProxy.addresses.trim()) + if (_uiState.value.interfaceProxy.dnsServers.isNotEmpty()) { + builder.parseDnsServers(_uiState.value.interfaceProxy.dnsServers.trim()) + } + if (_uiState.value.interfaceProxy.mtu.isNotEmpty()) + builder.parseMtu(_uiState.value.interfaceProxy.mtu.trim()) + if (_uiState.value.interfaceProxy.listenPort.isNotEmpty()) { + builder.parseListenPort(_uiState.value.interfaceProxy.listenPort.trim()) + } + if (isAllApplicationsEnabled()) emptyCheckedPackagesList() + if (_uiState.value.include) builder.includeApplications(_uiState.value.checkedPackageNames) + if (!_uiState.value.include) builder.excludeApplications(_uiState.value.checkedPackageNames) + if(_uiState.value.interfaceProxy.junkPacketCount.isNotEmpty()) { + builder.setJunkPacketCount(_uiState.value.interfaceProxy.junkPacketCount.trim().toInt()) + } + if(_uiState.value.interfaceProxy.junkPacketMinSize.isNotEmpty()) { + builder.setJunkPacketMinSize(_uiState.value.interfaceProxy.junkPacketMinSize.trim().toInt()) + } + if(_uiState.value.interfaceProxy.junkPacketMaxSize.isNotEmpty()) { + builder.setJunkPacketMaxSize(_uiState.value.interfaceProxy.junkPacketMaxSize.trim().toInt()) + } + if(_uiState.value.interfaceProxy.initPacketJunkSize.isNotEmpty()) { + builder.setInitPacketJunkSize(_uiState.value.interfaceProxy.initPacketJunkSize.trim().toInt()) + } + if(_uiState.value.interfaceProxy.responsePacketJunkSize.isNotEmpty()) { + builder.setResponsePacketJunkSize(_uiState.value.interfaceProxy.responsePacketJunkSize.trim().toInt()) + } + if(_uiState.value.interfaceProxy.initPacketMagicHeader.isNotEmpty()) { + builder.setInitPacketMagicHeader(_uiState.value.interfaceProxy.initPacketMagicHeader.trim().toLong()) + } + if(_uiState.value.interfaceProxy.responsePacketMagicHeader.isNotEmpty()) { + builder.setResponsePacketMagicHeader(_uiState.value.interfaceProxy.responsePacketMagicHeader.trim().toLong()) + } + if(_uiState.value.interfaceProxy.transportPacketMagicHeader.isNotEmpty()) { + builder.setTransportPacketMagicHeader(_uiState.value.interfaceProxy.transportPacketMagicHeader.trim().toLong()) + } + if(_uiState.value.interfaceProxy.underloadPacketMagicHeader.isNotEmpty()) { + builder.setUnderloadPacketMagicHeader(_uiState.value.interfaceProxy.underloadPacketMagicHeader.trim().toLong()) + } + return builder.build() + } + + private fun buildConfig() : Config { + val peerList = buildPeerListFromProxyPeers() + val wgInterface = buildInterfaceListFromProxyInterface() + return Config.Builder().addPeers(peerList).setInterface(wgInterface).build() + } + + private fun buildAmConfig() : org.amnezia.awg.config.Config { + val peerList = buildAmPeerListFromProxyPeers() + val amInterface = buildAmInterfaceListFromProxyInterface() + return org.amnezia.awg.config.Config.Builder().addPeers(peerList).setInterface(amInterface).build() + } + fun onSaveAllChanges(): Result { return try { - val peerList = buildPeerListFromProxyPeers() - val wgInterface = buildInterfaceListFromProxyInterface() - val config = Config.Builder().addPeers(peerList).setInterface(wgInterface).build() + val config = buildConfig() val tunnelConfig = when (uiState.value.tunnel) { null -> TunnelConfig( name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString(), ) - else -> uiState.value.tunnel!!.copy( name = _uiState.value.tunnelName, wgQuick = config.toWgQuickString(), + amQuick = if(uiState.value.isAmneziaEnabled) buildAmConfig().toAwgQuickString() + else _uiState.value.tunnel?.amQuick ?: "" ) } updateTunnelConfig(tunnelConfig) @@ -216,121 +273,138 @@ constructor( } fun onPeerPublicKeyChange(index: Int, value: String) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( proxyPeers = _uiState.value.proxyPeers.update( index, _uiState.value.proxyPeers[index].copy(publicKey = value), ), ) + } } fun onPreSharedKeyChange(index: Int, value: String) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( proxyPeers = _uiState.value.proxyPeers.update( index, _uiState.value.proxyPeers[index].copy(preSharedKey = value), ), ) + } } fun onEndpointChange(index: Int, value: String) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( proxyPeers = _uiState.value.proxyPeers.update( index, _uiState.value.proxyPeers[index].copy(endpoint = value), ), ) + } } fun onAllowedIpsChange(index: Int, value: String) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( proxyPeers = _uiState.value.proxyPeers.update( index, _uiState.value.proxyPeers[index].copy(allowedIps = value), ), ) + } } fun onPersistentKeepaliveChanged(index: Int, value: String) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( proxyPeers = _uiState.value.proxyPeers.update( index, _uiState.value.proxyPeers[index].copy(persistentKeepalive = value), ), ) + } } fun onDeletePeer(index: Int) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( proxyPeers = _uiState.value.proxyPeers.removeAt(index), ) + } } fun addEmptyPeer() { - _uiState.value = _uiState.value.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy()) + _uiState.update { + it.copy(proxyPeers = _uiState.value.proxyPeers + PeerProxy()) + } } fun generateKeyPair() { val keyPair = KeyPair() - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( interfaceProxy = _uiState.value.interfaceProxy.copy( privateKey = keyPair.privateKey.toBase64(), publicKey = keyPair.publicKey.toBase64(), ), ) + } } fun onAddressesChanged(value: String) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( interfaceProxy = _uiState.value.interfaceProxy.copy(addresses = value), ) + } + } fun onListenPortChanged(value: String) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( interfaceProxy = _uiState.value.interfaceProxy.copy(listenPort = value), ) + } } fun onDnsServersChanged(value: String) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( interfaceProxy = _uiState.value.interfaceProxy.copy(dnsServers = value), ) + } } fun onMtuChanged(value: String) { - _uiState.value = - _uiState.value.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) + _uiState.update { + it.copy(interfaceProxy = _uiState.value.interfaceProxy.copy(mtu = value)) + } } private fun onInterfacePublicKeyChange(value: String) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( interfaceProxy = _uiState.value.interfaceProxy.copy(publicKey = value), ) + } + } fun onPrivateKeyChange(value: String) { - _uiState.value = - _uiState.value.copy( + _uiState.update { + it.copy( interfaceProxy = _uiState.value.interfaceProxy.copy(privateKey = value), ) + } if (NumberUtils.isValidKey(value)) { val pair = KeyPair(Key.fromBase64(value)) onInterfacePublicKeyChange(pair.publicKey.toBase64()) @@ -344,6 +418,77 @@ constructor( getAllInternetCapablePackages().filter { getPackageLabel(it).lowercase().contains(query.lowercase()) } - _uiState.value = _uiState.value.copy(packages = packages) + _uiState.update { it.copy(packages = packages) } + } + + fun onJunkPacketCountChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketCount = value) + ) + } + } + fun onJunkPacketMinSizeChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMinSize = value) + ) + } + } + + fun onJunkPacketMaxSizeChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(junkPacketMaxSize = value) + ) + } + } + + fun onInitPacketJunkSizeChanged(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketJunkSize = value) + ) + } + } + + fun onResponsePacketJunkSize(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketJunkSize = value) + ) + } + } + + fun onInitPacketMagicHeader(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(initPacketMagicHeader = value) + ) + } + } + + fun onResponsePacketMagicHeader(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(responsePacketMagicHeader = value) + ) + } + } + + fun onTransportPacketMagicHeader(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(transportPacketMagicHeader = value) + ) + } + } + + fun onUnderloadPacketMagicHeader(value: String) { + _uiState.update { + it.copy( + interfaceProxy = _uiState.value.interfaceProxy.copy(underloadPacketMagicHeader = value) + ) + } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt new file mode 100644 index 0000000..8acb61f --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/InterfaceProxy.kt @@ -0,0 +1,63 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.config.model + +import com.wireguard.config.Interface + +data class InterfaceProxy( + val privateKey: String = "", + val publicKey: String = "", + val addresses: String = "", + val dnsServers: String = "", + val listenPort: String = "", + val mtu: String = "", + val junkPacketCount: String = "", + val junkPacketMinSize: String = "", + val junkPacketMaxSize: String = "", + val initPacketJunkSize: String = "", + val responsePacketJunkSize: String = "", + val initPacketMagicHeader: String = "", + val responsePacketMagicHeader: String = "", + val underloadPacketMagicHeader: String = "", + val transportPacketMagicHeader: String = "", +) { + companion object { + fun from(i: Interface): InterfaceProxy { + return InterfaceProxy( + publicKey = i.keyPair.publicKey.toBase64().trim(), + privateKey = i.keyPair.privateKey.toBase64().trim(), + addresses = i.addresses.joinToString(", ").trim(), + dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(), + listenPort = + if (i.listenPort.isPresent) { + i.listenPort.get().toString().trim() + } else { + "" + }, + mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", + ) + } + fun from(i: org.amnezia.awg.config.Interface) : InterfaceProxy { + return InterfaceProxy( + publicKey = i.keyPair.publicKey.toBase64().trim(), + privateKey = i.keyPair.privateKey.toBase64().trim(), + addresses = i.addresses.joinToString(", ").trim(), + dnsServers = i.dnsServers.joinToString(", ").replace("/", "").trim(), + listenPort = + if (i.listenPort.isPresent) { + i.listenPort.get().toString().trim() + } else { + "" + }, + mtu = if (i.mtu.isPresent) i.mtu.get().toString().trim() else "", + junkPacketCount = if(i.junkPacketCount.isPresent) i.junkPacketCount.get().toString() else "", + junkPacketMinSize = if(i.junkPacketMinSize.isPresent) i.junkPacketMinSize.get().toString() else "", + junkPacketMaxSize = if(i.junkPacketMaxSize.isPresent) i.junkPacketMaxSize.get().toString() else "", + initPacketJunkSize = if(i.initPacketJunkSize.isPresent) i.initPacketJunkSize.get().toString() else "", + responsePacketJunkSize = if(i.responsePacketJunkSize.isPresent) i.responsePacketJunkSize.get().toString() else "", + initPacketMagicHeader = if(i.initPacketMagicHeader.isPresent) i.initPacketMagicHeader.get().toString() else "", + responsePacketMagicHeader = if(i.responsePacketMagicHeader.isPresent) i.responsePacketMagicHeader.get().toString() else "", + underloadPacketMagicHeader = if(i.underloadPacketMagicHeader.isPresent) i.underloadPacketMagicHeader.get().toString() else "", + transportPacketMagicHeader = if(i.transportPacketMagicHeader.isPresent) i.transportPacketMagicHeader.get().toString() else "", + ) + } + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/PeerProxy.kt similarity index 63% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/PeerProxy.kt index abf78d3..73b0ae1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/models/PeerProxy.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/model/PeerProxy.kt @@ -1,13 +1,13 @@ -package com.zaneschepke.wireguardautotunnel.ui.models +package com.zaneschepke.wireguardautotunnel.ui.screens.config.model import com.wireguard.config.Peer data class PeerProxy( - var publicKey: String = "", - var preSharedKey: String = "", - var persistentKeepalive: String = "", - var endpoint: String = "", - var allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim() + val publicKey: String = "", + val preSharedKey: String = "", + val persistentKeepalive: String = "", + val endpoint: String = "", + val allowedIps: String = IPV4_WILDCARD.joinToString(", ").trim() ) { companion object { fun from(peer: Peer): PeerProxy { @@ -35,6 +35,31 @@ data class PeerProxy( ) } + fun from(peer: org.amnezia.awg.config.Peer) : PeerProxy { + return PeerProxy( + publicKey = peer.publicKey.toBase64(), + preSharedKey = + if (peer.preSharedKey.isPresent) { + peer.preSharedKey.get().toBase64().trim() + } else { + "" + }, + persistentKeepalive = + if (peer.persistentKeepalive.isPresent) { + peer.persistentKeepalive.get().toString().trim() + } else { + "" + }, + endpoint = + if (peer.endpoint.isPresent) { + peer.endpoint.get().toString().trim() + } else { + "" + }, + allowedIps = peer.allowedIps.joinToString(", ").trim(), + ) + } + val IPV4_PUBLIC_NETWORKS = setOf( "0.0.0.0/5", diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 05202b1..e6a7bf9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.overscroll import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.FileOpen @@ -81,19 +82,22 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions -import com.wireguard.android.backend.Tunnel import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus +import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.CaptureActivityPortrait import com.zaneschepke.wireguardautotunnel.ui.Screen @@ -110,6 +114,8 @@ import com.zaneschepke.wireguardautotunnel.util.truncateWithEllipsis import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.Timer @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @@ -257,11 +263,14 @@ fun MainScreen( Scaffold( modifier = Modifier.pointerInput(Unit) { - detectTapGestures( - onTap = { - selectedTunnel = null - }, - ) + if(uiState.tunnels.isNotEmpty()) { + detectTapGestures( + onTap = { + selectedTunnel = null + }, + ) + } + }, floatingActionButtonPosition = FabPosition.End, floatingActionButton = { @@ -299,16 +308,6 @@ fun MainScreen( } }, ) { - AnimatedVisibility(uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxSize(), - ) { - Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) - } - } if (showBottomSheet) { ModalBottomSheet( onDismissRequest = { showBottomSheet = false }, @@ -401,12 +400,46 @@ fun MainScreen( modifier = Modifier .fillMaxSize() - .overscroll(ScrollableDefaults.overscrollEffect()).nestedScroll(nestedScrollConnection), + .overscroll(ScrollableDefaults.overscrollEffect()) + .nestedScroll(nestedScrollConnection), state = rememberLazyListState(0, uiState.tunnels.count()), userScrollEnabled = true, reverseLayout = false, flingBehavior = ScrollableDefaults.flingBehavior(), ) { + item { + val gettingStarted = buildAnnotatedString { + append(stringResource(id = R.string.see_the)) + append(" ") + pushStringAnnotation(tag = "gettingStarted", annotation = stringResource(id = R.string.getting_started_url)) + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.primary)) { + append(stringResource(id = R.string.getting_started_guide)) + } + pop() + append(" ") + append(stringResource(R.string.unsure_how)) + append(".") + } + AnimatedVisibility( + uiState.tunnels.isEmpty(), exit = fadeOut(), enter = fadeIn()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.padding(top = 100.dp) + ) { + Text(text = stringResource(R.string.no_tunnels), fontStyle = FontStyle.Italic) + ClickableText( + modifier = Modifier.padding(vertical = 10.dp, horizontal = 24.dp), + text = gettingStarted, + style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center), + ) { + gettingStarted.getStringAnnotations(tag = "gettingStarted", it, it).firstOrNull()?.let { annotation -> + appViewModel.openWebPage(annotation.item, context) + } + } + } + } + } item { if (uiState.settings.isAutoTunnelEnabled) { val autoTunnelingLabel = buildAnnotatedString { @@ -462,7 +495,7 @@ fun MainScreen( val leadingIconColor = (if ( uiState.vpnState.tunnelConfig?.name == tunnel.name && - uiState.vpnState.status == Tunnel.State.UP + uiState.vpnState.status == TunnelState.UP ) { uiState.vpnState.statistics ?.mapPeerStats() @@ -508,7 +541,7 @@ fun MainScreen( text = tunnel.name.truncateWithEllipsis(Constants.ALLOWED_DISPLAY_NAME_LENGTH), onHold = { if ( - (uiState.vpnState.status == Tunnel.State.UP) && + (uiState.vpnState.status == TunnelState.UP) && (tunnel.name == uiState.vpnState.tunnelConfig?.name) ) { appViewModel.showSnackbarMessage(Event.Message.TunnelOffAction.message) @@ -520,7 +553,7 @@ fun MainScreen( onClick = { if (!WireGuardAutoTunnel.isRunningOnAndroidTv()) { if ( - uiState.vpnState.status == Tunnel.State.UP && + uiState.vpnState.status == TunnelState.UP && (uiState.vpnState.tunnelConfig?.name == tunnel.name) ) { expanded.value = !expanded.value @@ -578,7 +611,7 @@ fun MainScreen( } else { val checked by remember { derivedStateOf { - (uiState.vpnState.status == Tunnel.State.UP && + (uiState.vpnState.status == TunnelState.UP && tunnel.name == uiState.vpnState.tunnelConfig?.name) } } @@ -620,7 +653,7 @@ fun MainScreen( modifier = Modifier.focusRequester(focusRequester), onClick = { if ( - uiState.vpnState.status == Tunnel.State.UP && + uiState.vpnState.status == TunnelState.UP && (uiState.vpnState.tunnelConfig?.name == tunnel.name) ) { expanded.value = !expanded.value @@ -643,7 +676,7 @@ fun MainScreen( IconButton( onClick = { if ( - uiState.vpnState.status == Tunnel.State.UP && + uiState.vpnState.status == TunnelState.UP && tunnel.name == uiState.vpnState.tunnelConfig?.name ) { appViewModel.showSnackbarMessage( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt index 775bbba..f9de5c8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainUiState.kt @@ -1,6 +1,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.main -import com.zaneschepke.wireguardautotunnel.data.model.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState import com.zaneschepke.wireguardautotunnel.util.TunnelConfigs diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index c18a73f..b4fd09a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -9,8 +9,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wireguard.config.Config import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel -import com.zaneschepke.wireguardautotunnel.data.model.Settings -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService @@ -99,7 +99,7 @@ constructor( } private fun validateConfigString(config: String) { - TunnelConfig.configFromQuick(config) + TunnelConfig.configFromWgQuick(config) } suspend fun onTunnelQrResult(result: String): Result { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt index ded1d5f..828c486 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsUiState.kt @@ -1,6 +1,6 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.options -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig data class OptionsUiState( val id: String? = null, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt index 8d874ed..b189b8d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/options/OptionsViewModel.kt @@ -4,7 +4,7 @@ import androidx.compose.ui.util.fastFirstOrNull import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Event @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -43,9 +44,11 @@ constructor( ) fun init(tunnelId: String) { - _optionState.value = _optionState.value.copy( - id = tunnelId, - ) + _optionState.update { + it.copy( + id = tunnelId + ) + } } fun onDeleteRunSSID(ssid: String) = viewModelScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt index bef66b0..66a7338 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -75,6 +74,7 @@ import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.WgQuickBackend import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel +import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.ui.AppViewModel import com.zaneschepke.wireguardautotunnel.ui.Screen import com.zaneschepke.wireguardautotunnel.ui.common.ClickableIconButton @@ -551,31 +551,43 @@ fun SettingsScreen( } } } - if (WgQuickBackend.hasKernelSupport()) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = Modifier - .fillMaxWidth(fillMaxWidth) - .padding(vertical = 10.dp), + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = Modifier + .fillMaxWidth(fillMaxWidth) + .padding(vertical = 10.dp), + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(15.dp), ) { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.padding(15.dp), - ) { - SectionTitle( - title = stringResource(id = R.string.kernel), - padding = screenPadding, - ) + SectionTitle( + title = stringResource(id = R.string.backend), + padding = screenPadding, + ) + ConfigurationToggle( + stringResource(R.string.use_amnezia), + enabled = + !(uiState.settings.isAutoTunnelEnabled || + uiState.settings.isAlwaysOnVpnEnabled || + (uiState.vpnState.status == TunnelState.UP) || uiState.settings.isKernelEnabled), + checked = uiState.settings.isAmneziaEnabled, + padding = screenPadding, + onCheckChanged = { + viewModel.onToggleAmnezia() + }, + ) + if (WgQuickBackend.hasKernelSupport()) { ConfigurationToggle( stringResource(R.string.use_kernel), enabled = !(uiState.settings.isAutoTunnelEnabled || uiState.settings.isAlwaysOnVpnEnabled || - (uiState.vpnState.status == Tunnel.State.UP)), + (uiState.vpnState.status == TunnelState.UP)), checked = uiState.settings.isKernelEnabled, padding = screenPadding, onCheckChanged = { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt index 301039d..f231c86 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsUiState.kt @@ -1,7 +1,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings -import com.zaneschepke.wireguardautotunnel.data.model.Settings -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState data class SettingsUiState( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt index d71ce51..f40df18 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/SettingsViewModel.kt @@ -8,7 +8,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wireguard.android.util.RootShell import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel -import com.zaneschepke.wireguardautotunnel.data.model.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnService @@ -162,12 +162,29 @@ constructor( ) } + fun onToggleAmnezia() = viewModelScope.launch { + if(uiState.value.settings.isKernelEnabled) { + saveKernelMode(false) + } + saveAmneziaMode(!uiState.value.settings.isAmneziaEnabled) + } + + private fun saveAmneziaMode(on: Boolean) { + saveSettings( + uiState.value.settings.copy( + isAmneziaEnabled = on + ) + ) + } + fun onToggleKernelMode(): Result { if (!uiState.value.settings.isKernelEnabled) { try { rootShell.start() Timber.i("Root shell accepted!") saveKernelMode(on = true) + saveAmneziaMode(false) + } catch (e: RootShell.RootShellException) { Timber.e(e) saveKernelMode(on = false) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt index e9495f4..258aacc 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt @@ -107,7 +107,7 @@ fun SupportScreen( modifier = Modifier.padding(bottom = 20.dp), ) TextButton( - onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url)) }, + onClick = { appViewModel.openWebPage(context.resources.getString(R.string.docs_url), context) }, modifier = Modifier .padding(vertical = 5.dp) .focusRequester(focusRequester), @@ -143,7 +143,7 @@ fun SupportScreen( color = MaterialTheme.colorScheme.onBackground, ) TextButton( - onClick = { appViewModel.openWebPage(context.resources.getString(R.string.discord_url)) }, + onClick = { appViewModel.openWebPage(context.resources.getString(R.string.telegram_url), context) }, modifier = Modifier.padding(vertical = 5.dp), ) { Row( @@ -152,7 +152,7 @@ fun SupportScreen( modifier = Modifier.fillMaxWidth(), ) { Row { - val icon = ImageVector.vectorResource(R.drawable.discord) + val icon = ImageVector.vectorResource(R.drawable.telegram) Icon( icon, icon.name, @@ -175,7 +175,7 @@ fun SupportScreen( color = MaterialTheme.colorScheme.onBackground, ) TextButton( - onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url)) }, + onClick = { appViewModel.openWebPage(context.resources.getString(R.string.github_url), context) }, modifier = Modifier.padding(vertical = 5.dp), ) { Row( @@ -207,7 +207,7 @@ fun SupportScreen( color = MaterialTheme.colorScheme.onBackground, ) TextButton( - onClick = { appViewModel.launchEmail() }, + onClick = { appViewModel.launchEmail(context) }, modifier = Modifier.padding(vertical = 5.dp), ) { Row( @@ -269,7 +269,7 @@ fun SupportScreen( fontSize = 16.sp, modifier = Modifier.clickable { - appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url)) + appViewModel.openWebPage(context.resources.getString(R.string.privacy_policy_url), context) }, ) Row( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt index 99bdf6a..9484409 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportUiState.kt @@ -1,5 +1,5 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support -import com.zaneschepke.wireguardautotunnel.data.model.Settings +import com.zaneschepke.wireguardautotunnel.data.domain.Settings data class SupportUiState(val settings: Settings = Settings()) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt index 0de0ba3..2de1661 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/logs/LogsScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -43,6 +44,8 @@ fun LogsScreen(appViewModel: AppViewModel) { appViewModel.logs } + val context = LocalContext.current + val lazyColumnListState = rememberLazyListState() val clipboardManager: ClipboardManager = LocalClipboardManager.current val scope = rememberCoroutineScope() @@ -57,7 +60,7 @@ fun LogsScreen(appViewModel: AppViewModel) { floatingActionButton = { FloatingActionButton( onClick = { - appViewModel.saveLogsToFile() + appViewModel.saveLogsToFile(context) }, shape = RoundedCornerShape(16.dp), containerColor = MaterialTheme.colorScheme.primary, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt index 2e55229..e0086b0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Constants.kt @@ -2,7 +2,7 @@ package com.zaneschepke.wireguardautotunnel.util object Constants { - const val BASE_LOG_FILE_NAME = "wgtunnel-logs" + const val BASE_LOG_FILE_NAME = "wg_tunnel_logs" const val LOG_BUFFER_SIZE = 3_000L const val MANUAL_TUNNEL_CONFIG_ID = "0" diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt index e82ea9f..7b27335 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/Extensions.kt @@ -2,11 +2,9 @@ package com.zaneschepke.wireguardautotunnel.util import android.content.BroadcastReceiver import android.content.pm.PackageInfo -import com.wireguard.android.backend.Statistics -import com.wireguard.android.backend.Statistics.PeerStats -import com.wireguard.crypto.Key -import com.zaneschepke.wireguardautotunnel.data.model.TunnelConfig +import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus +import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -50,15 +48,15 @@ typealias TunnelConfigs = List typealias Packages = List -fun Statistics.mapPeerStats(): Map { - return this.peers().associateWith { key -> (this.peer(key)) } +fun TunnelStatistics.mapPeerStats(): Map { + return this.getPeers().associateWith { key -> (this.peerStats(key)) } } -fun PeerStats.latestHandshakeSeconds(): Long? { +fun TunnelStatistics.PeerStats.latestHandshakeSeconds(): Long? { return NumberUtils.getSecondsBetweenTimestampAndNow(this.latestHandshakeEpochMillis) } -fun PeerStats.handshakeStatus(): HandshakeStatus { +fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus { // TODO add never connected status after duration return this.latestHandshakeSeconds().let { when { diff --git a/app/src/main/res/drawable/telegram.xml b/app/src/main/res/drawable/telegram.xml new file mode 100644 index 0000000..58f7407 --- /dev/null +++ b/app/src/main/res/drawable/telegram.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 33912eb..08f6d40 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -108,7 +108,6 @@ Join the community Send me an email If you are experiencing issues, have improvement ideas, or just want to engage, the following resources are available: - Kernel Use kernel module SSID already exists Root shell denied @@ -140,7 +139,7 @@ Pin successfully created Enter your pin Create pin - Enabled app lock + Enable app lock Restart on ping fail (beta) Set as mobile data tunnel Set as primary tunnel @@ -158,4 +157,21 @@ Userspace Settings Support + Backend + Kernel + "Use Amnezia userspace " + Junk packet count + Junk packet minimum size + Junk packet maximum size + Init packet junk size + Response packet junk size + Init packet magic header + Response packet magic header + Transport packet magic header + Underload packet magic header + https://t.me/wgtunnel + if you are unsure how to proceed + See the + https://zaneschepke.com/wgtunnel-docs/getting-started.html + Getting started guide \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index c8a68a7..aff5c95 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -1,7 +1,7 @@ object Constants { - const val VERSION_NAME = "3.4.2" + const val VERSION_NAME = "3.4.3-beta" const val JVM_TARGET = "17" - const val VERSION_CODE = 34200 + const val VERSION_CODE = 34202 const val TARGET_SDK = 34 const val MIN_SDK = 26 const val APP_ID = "com.zaneschepke.wireguardautotunnel" diff --git a/fastlane/metadata/android/en-US/changelogs/34202.txt b/fastlane/metadata/android/en-US/changelogs/34202.txt new file mode 100644 index 0000000..fcbdc08 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/34202.txt @@ -0,0 +1,3 @@ +What's new: +- Add Amnezia side-by-side with WireGuard +- Fix app shortcuts bug \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 092ac5d..7575251 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,13 @@ [versions] accompanist = "0.34.0" -activityCompose = "1.8.2" +activityCompose = "1.9.0" +amneziawgAndroid = "1.2.0" androidx-junit = "1.1.5" appcompat = "1.6.1" biometricKtx = "1.2.0-alpha05" coreGoogleShortcuts = "1.1.0" -coreKtx = "1.12.0" -datastorePreferences = "1.0.0" +coreKtx = "1.13.1" +datastorePreferences = "1.1.1" desugar_jdk_libs = "2.0.4" espressoCore = "3.5.1" hiltAndroid = "2.51" @@ -23,8 +24,8 @@ tunnel = "1.0.20230706" androidGradlePlugin = "8.4.0-rc02" kotlin = "1.9.23" ksp = "1.9.23-1.0.19" -composeBom = "2024.03.00" -compose = "1.6.4" +composeBom = "2024.05.00" +compose = "1.6.7" zxingAndroidEmbedded = "4.3.0" zxingCore = "3.5.3" @@ -41,6 +42,7 @@ accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayo accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } #room +amneziawg-android = { module = "com.zaneschepke:amneziawg-android", version.ref = "amneziawgAndroid" } androidx-biometric-ktx = { module = "androidx.biometric:biometric-ktx", version.ref = "biometricKtx" } androidx-core = { module = "androidx.core:core", version.ref = "coreKtx" } androidx-core-google-shortcuts = { module = "androidx.core:core-google-shortcuts", version.ref = "coreGoogleShortcuts" }