mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a9773d202 | |||
| 3cb4480a65 | |||
| a7f3255a76 | |||
| 7d7b99f448 | |||
| 74e9e462bb | |||
| 619e3c1cde | |||
| 77f8a8215b | |||
| 8772036dd7 | |||
| 63625ccbd7 | |||
| 9ac7ae77b3 | |||
| e062fbb34d | |||
| 16d5586433 |
@@ -53,7 +53,6 @@
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.App.Start"
|
||||
tools:targetApi="tiramisu">
|
||||
@@ -74,6 +73,13 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="wg" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.SHOW_APP_INFO" />
|
||||
@@ -171,7 +177,7 @@
|
||||
<service
|
||||
android:name=".service.tile.TunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:icon="@drawable/ic_qs_logo"
|
||||
android:label="@string/tunnel_control"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
@@ -188,7 +194,7 @@
|
||||
<service
|
||||
android:name=".service.tile.AutoTunnelControlTile"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:icon="@drawable/ic_qs_logo"
|
||||
android:label="@string/auto_tunnel"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<meta-data
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.zaneschepke.wireguardautotunnel
|
||||
|
||||
import ProxySettingsScreen
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
@@ -31,15 +33,10 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CheckCircleOutline
|
||||
import androidx.compose.material.icons.outlined.Error
|
||||
import androidx.compose.material.icons.outlined.ErrorOutline
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
import androidx.compose.material.icons.outlined.WarningAmber
|
||||
import androidx.compose.material.icons.rounded.Error
|
||||
import androidx.compose.material.icons.rounded.Info
|
||||
import androidx.compose.material.icons.rounded.Warning
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -55,6 +52,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -66,6 +64,7 @@ import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
@@ -87,6 +86,8 @@ import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalIsAndroidTV
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.banner.AppAlertBanner
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.LocalNetworkPermissionDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.SecureRoute
|
||||
@@ -136,6 +137,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.installApk
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.restartApp
|
||||
import com.zaneschepke.wireguardautotunnel.util.permission.LocalNetworkPermissionHelper
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.ConfigEditViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SplitTunnelViewModel
|
||||
@@ -181,7 +183,8 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
roomBackup = RoomBackup(this).database(appDatabase).enableLogDebug(true).maxFileCount(5)
|
||||
|
||||
handleIncomingIntent(intent)
|
||||
handleConfigFileIntent(intent)
|
||||
handleWgDeepLinkIntent(intent)
|
||||
|
||||
installSplashScreen().apply {
|
||||
setKeepOnScreenCondition { !viewModel.container.stateFlow.value.isAppLoaded }
|
||||
@@ -205,6 +208,68 @@ class MainActivity : AppCompatActivity() {
|
||||
var requestingTunnelMode by remember {
|
||||
mutableStateOf<Pair<TunnelMode?, TunnelConfig?>>(Pair(null, null))
|
||||
}
|
||||
var showLocalNetworkRationale by remember { mutableStateOf(false) }
|
||||
var hasPromptedLocalNetwork by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val localNetworkPermissionLauncher =
|
||||
rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (!isGranted) {
|
||||
val canAskAgain =
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
this,
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK,
|
||||
)
|
||||
|
||||
if (!canAskAgain) {
|
||||
val intent =
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", packageName, null)
|
||||
}
|
||||
startActivity(intent)
|
||||
} else {
|
||||
toaster.show(
|
||||
message =
|
||||
context.getString(R.string.local_network_permission_denied),
|
||||
type = ToastType.Warning,
|
||||
duration = 6000.milliseconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.isAppLoaded) {
|
||||
if (
|
||||
uiState.isAppLoaded &&
|
||||
!hasPromptedLocalNetwork &&
|
||||
LocalNetworkPermissionHelper.shouldRequestPermission() &&
|
||||
!LocalNetworkPermissionHelper.isPermissionGranted(context)
|
||||
) {
|
||||
hasPromptedLocalNetwork = true
|
||||
showLocalNetworkRationale = true
|
||||
}
|
||||
}
|
||||
|
||||
if (showLocalNetworkRationale) {
|
||||
LocalNetworkPermissionDialog(
|
||||
onDismiss = {
|
||||
showLocalNetworkRationale = false
|
||||
toaster.show(
|
||||
message = context.getString(R.string.local_network_permission_denied),
|
||||
type = ToastType.Warning,
|
||||
duration = 6000.milliseconds,
|
||||
)
|
||||
},
|
||||
onContinue = {
|
||||
showLocalNetworkRationale = false
|
||||
|
||||
localNetworkPermissionLauncher.launch(
|
||||
Manifest.permission.ACCESS_LOCAL_NETWORK
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val startingStack = buildList {
|
||||
add(Route.Tunnels)
|
||||
@@ -295,6 +360,17 @@ class MainActivity : AppCompatActivity() {
|
||||
},
|
||||
)
|
||||
|
||||
uiState.pendingWgImportUrl?.let { url ->
|
||||
val host = Uri.parse(url).host ?: url
|
||||
InfoDialog(
|
||||
onDismiss = { viewModel.dismissWgImport() },
|
||||
onAttest = { viewModel.importFromUrl(url) },
|
||||
title = stringResource(R.string.add_from_url),
|
||||
body = { Text(stringResource(R.string.wg_url_confirm_message, host)) },
|
||||
confirmText = stringResource(R.string.okay),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (uiState.shouldShowDonationSnackbar && !uiState.alreadyDonated) {
|
||||
viewModel.setShouldShowDonationSnackbar(false)
|
||||
@@ -597,6 +673,21 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleWgDeepLinkIntent(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val uri = intent.data ?: return
|
||||
if (uri.scheme == "wg") {
|
||||
val httpsUrl = uri.toString().replaceFirst("wg://", "https://")
|
||||
viewModel.promptWgImport(httpsUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
networkMonitor.checkPermissionsAndUpdateState()
|
||||
}
|
||||
|
||||
fun performBackup(encrypt: Boolean = false, password: String? = null) {
|
||||
roomBackup
|
||||
.backupLocation(RoomBackup.BACKUP_FILE_LOCATION_CUSTOM_DIALOG)
|
||||
@@ -673,18 +764,14 @@ class MainActivity : AppCompatActivity() {
|
||||
.restore()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
networkMonitor.checkPermissionsAndUpdateState()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIncomingIntent(intent)
|
||||
handleConfigFileIntent(intent)
|
||||
handleWgDeepLinkIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIncomingIntent(intent: Intent?) {
|
||||
private fun handleConfigFileIntent(intent: Intent?) {
|
||||
intent ?: return
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW,
|
||||
|
||||
@@ -51,6 +51,13 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
|
||||
private val backend: Backend by inject()
|
||||
|
||||
private val alwaysOnCallback =
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(KoinViewModelScopeApi::class)
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -86,13 +93,7 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
Timber.plant(ReleaseTree())
|
||||
}
|
||||
|
||||
backend.setAlwaysOnCallback(
|
||||
object : VpnService.AlwaysOnCallback {
|
||||
override fun alwaysOnTriggered() {
|
||||
applicationScope.launch { tunnelCoordinator.startDefault() }
|
||||
}
|
||||
}
|
||||
)
|
||||
backend.setAlwaysOnCallback(alwaysOnCallback)
|
||||
|
||||
val dispatcher = get<TunnelEventDispatcher>()
|
||||
val coordinator = get<TunnelCoordinator>()
|
||||
|
||||
+18
-4
@@ -117,7 +117,7 @@ class TunnelCoordinator(
|
||||
|
||||
// enforce single tunnel, for now
|
||||
if (backendStatus.value.activeTunnels.isNotEmpty()) {
|
||||
stopActiveTunnelsInternal()
|
||||
stopActiveTunnelsInternal(source)
|
||||
}
|
||||
|
||||
startTunnelInternal(config, source)
|
||||
@@ -131,7 +131,13 @@ class TunnelCoordinator(
|
||||
stopTunnelInternal(id, source)
|
||||
}
|
||||
|
||||
suspend fun stopActiveTunnels() = tunnelMutex.withLock { stopActiveTunnelsInternal() }
|
||||
suspend fun stopActiveTunnels(source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
stopActiveTunnelsInternal(source)
|
||||
}
|
||||
|
||||
private suspend fun startTunnelInternal(
|
||||
tunnelConfig: TunnelConfig,
|
||||
@@ -218,7 +224,7 @@ class TunnelCoordinator(
|
||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||
}
|
||||
|
||||
stopActiveTunnelsInternal()
|
||||
stopActiveTunnelsInternal(source)
|
||||
return@withLock
|
||||
}
|
||||
|
||||
@@ -243,7 +249,15 @@ class TunnelCoordinator(
|
||||
.onFailure { _errors.emit(TunnelErrorEvent.from(it, id)) }
|
||||
}
|
||||
|
||||
private suspend fun stopActiveTunnelsInternal() {
|
||||
private suspend fun stopActiveTunnelsInternal(
|
||||
source: TunnelActionSource = TunnelActionSource.USER
|
||||
) {
|
||||
val active = tunnelProvider.backendStatus.value.activeTunnels
|
||||
|
||||
active.keys.forEach { id ->
|
||||
_actions.emit(TunnelActionEvent.Stopped(tunnelId = id, source = source))
|
||||
}
|
||||
|
||||
tunnelProvider.stopActiveTunnels()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -19,9 +19,8 @@ class ShortcutsActivity : ComponentActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
applicationScope.launch {
|
||||
shortcutCoordinator.handle(intent)
|
||||
finish()
|
||||
}
|
||||
finish()
|
||||
|
||||
applicationScope.launch { shortcutCoordinator.handle(intent) }
|
||||
}
|
||||
}
|
||||
|
||||
-9
@@ -4,14 +4,11 @@ import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendCoreException
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.BackendMessage
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.LockdownSettings
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -52,10 +49,4 @@ class TunnelBackendProvider(
|
||||
override suspend fun disableLockDown(): Result<Unit> {
|
||||
return backend.disableKillSwitch()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localErrorEvents = MutableSharedFlow<Pair<String?, BackendCoreException>>()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val localMessageEvents = MutableSharedFlow<Pair<String?, BackendMessage>>()
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.zaneschepke.wireguardautotunnel.ui.common.dialog
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
|
||||
@Composable
|
||||
fun LocalNetworkPermissionDialog(onDismiss: () -> Unit, onContinue: () -> Unit) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.local_network_permission_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_intro),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_issues_intro),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_tunnels),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_autotunnel),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_feature_proxy),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_nearby_devices),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.local_network_permission_recommendation),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onContinue) { Text(text = stringResource(R.string._continue)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(text = stringResource(R.string.not_now)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -18,5 +18,6 @@ data class GlobalAppUiState(
|
||||
val selectedTunnelCount: Int = 0,
|
||||
val alreadyDonated: Boolean = false,
|
||||
val isPinVerified: Boolean = false,
|
||||
val pendingWgImportUrl: String? = null,
|
||||
val isScreenRecordingProtectionEnabled: Boolean = false,
|
||||
)
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
|
||||
suspend fun HttpResponse.isHtmlResponse(): Boolean {
|
||||
val contentType = headers["Content-Type"] ?: ""
|
||||
if (contentType.contains("text/html", ignoreCase = true)) return true
|
||||
|
||||
val bodyStart = bodyAsText().trimStart()
|
||||
return bodyStart.startsWith("<!DOCTYPE", ignoreCase = true) ||
|
||||
bodyStart.startsWith("<html", ignoreCase = true)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.zaneschepke.wireguardautotunnel.util.permission
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
object LocalNetworkPermissionHelper {
|
||||
|
||||
fun shouldRequestPermission(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN
|
||||
}
|
||||
|
||||
fun isPermissionGranted(context: Context): Boolean {
|
||||
return if (shouldRequestPermission()) {
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-7
@@ -29,6 +29,7 @@ import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.QuickConfig
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelName
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asStringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isHtmlResponse
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.saveTunnelsUniquely
|
||||
import com.zaneschepke.wireguardautotunnel.util.network.NetworkUtils
|
||||
import io.ktor.client.HttpClient
|
||||
@@ -274,17 +275,30 @@ class SharedAppViewModel(
|
||||
|
||||
fun importFromQr(conf: String) = intent { importFromClipboard(conf) }
|
||||
|
||||
fun promptWgImport(url: String) = intent { reduce { state.copy(pendingWgImportUrl = url) } }
|
||||
|
||||
fun dismissWgImport() = intent { reduce { state.copy(pendingWgImportUrl = null) } }
|
||||
|
||||
fun importFromUrl(url: String) = intent {
|
||||
reduce { state.copy(pendingWgImportUrl = null) }
|
||||
|
||||
try {
|
||||
httpClient.prepareGet(url).execute { response ->
|
||||
if (response.status.value in 200..299) {
|
||||
val body = response.bodyAsText()
|
||||
importFromClipboard(body)
|
||||
} else {
|
||||
throw IOException(
|
||||
"Failed to download file with error status: ${response.status.value}"
|
||||
)
|
||||
if (response.status.value !in 200..299) {
|
||||
throw IOException("Server returned error: ${response.status.value}")
|
||||
}
|
||||
|
||||
if (response.isHtmlResponse()) {
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(
|
||||
StringValue.StringResource(R.string.error_invalid_config_url),
|
||||
ToastType.Error,
|
||||
)
|
||||
)
|
||||
return@execute
|
||||
}
|
||||
val body = response.bodyAsText()
|
||||
importFromClipboard(body)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<group
|
||||
android:scaleX="1.18"
|
||||
android:scaleY="1.18"
|
||||
android:pivotX="512"
|
||||
android:pivotY="512"
|
||||
android:translateX="-45"
|
||||
android:translateY="-45">
|
||||
<path
|
||||
android:pathData="M779.7,207.8C782.5,207.8 785.4,207.8 788.2,207.8C851.8,207.6 851.8,207.6 871.1,225.5C882.4,237.3 887.6,250.9 887.5,267.1C886.9,284.9 879,300.9 872,317C871,319.2 870.1,321.4 869.1,323.7C867.7,327.1 866.2,330.6 864.7,334.1C861,342.7 857.3,351.4 853.7,360.1C847.1,375.7 840.5,391.3 833.8,406.9C831.1,413.1 828.5,419.2 825.8,425.4C822.9,432.3 819.9,439.2 817,446C804.9,474 804.9,474 793.3,502.2C791,507.8 788.5,513.3 785.9,518.7C782,527 778.5,535.5 775,544C772,551 769,557.9 766.1,564.9C765.6,566 765.1,567.2 764.6,568.4C763.5,570.7 762.5,573.1 761.5,575.5C760,579 758.5,582.6 757,586.1C751.8,598.2 746.6,610.4 741.2,622.4C737.3,631.3 733.4,640.3 729.5,649.2C725.8,657.9 722,666.6 718,675.2C715.7,680.4 713.5,685.6 711.4,690.8C708.5,697.8 705.4,704.7 702.1,711.6C700.8,714.5 699.4,717.4 698,720.4C685.7,746.7 672.9,772.5 643.9,783.1C639.6,784.4 635.5,784.7 631,785C630.2,785.1 629.5,785.1 628.7,785.2C610.7,785.8 596.7,779.5 583.5,767.8C569.4,754.5 562,735.7 554.3,718.3C552.1,713.5 549.9,708.7 547.5,704.1C543.9,696.9 540.8,689.6 537.6,682.2C534.6,675 531.4,667.8 528.3,660.6C522.3,646.9 516.3,633.3 510.4,619.6C508.5,615.1 506.5,610.6 504.6,606.1C494.1,582.1 494.1,582.1 489.6,571.2C488,567.4 486.4,563.6 484.6,559.8C481.8,553.6 479.2,547.4 476.6,541.1C472.4,531.2 472.4,531.2 468,521.5C461.3,507.3 455.5,492.6 449.5,478.1C444.9,466.9 440.2,455.8 435.3,444.7C431.5,436.1 427.7,427.4 424.1,418.8C423.8,418.1 423.5,417.4 423.2,416.6C420,409.1 416.8,401.6 413.7,394C413.4,393.3 413.2,392.7 412.9,392C411.6,389 410.4,386 409.2,383C406.8,377.1 404.3,371.4 401.6,365.7C397.9,357.9 394.6,349.9 391.3,341.9C390.4,339.6 389.5,337.4 388.6,335.2C388,333.7 387.3,332.2 386.7,330.7C384.7,325.7 382.6,320.7 380.6,315.8C380,314.5 379.5,313.2 378.9,311.9C377.9,309.5 376.9,307.1 375.9,304.7C368.4,286.7 361.9,264.8 369.7,245.7C373.8,237.4 377.9,230 385,224C385.6,223.5 386.2,223 386.8,222.4C397,213.9 412.5,209.7 425.8,210.1C442.3,212.1 455.2,220.4 466,233C471.9,241.6 476.5,250.7 480.9,260.1C481.5,261.3 482.2,262.6 482.8,263.9C487.3,273.5 491.7,283.1 496,292.8C497,294.9 497.9,297.1 498.9,299.2C503.6,309.6 508.1,320.1 512.5,330.6C515.1,336.6 517.7,342.5 520.5,348.4C524.1,356.1 527.4,363.9 530.6,371.8C531.7,374.4 532.8,377 533.9,379.7C534.3,380.6 534.3,380.6 534.7,381.6C536.3,385.3 537.8,388.9 539.5,392.5C541.5,396.7 543.3,401 545.1,405.3C545.4,406 545.6,406.6 545.9,407.3C547,409.9 548.1,412.6 549.2,415.2C552.5,423.2 555.9,431.1 559.6,438.9C561.7,443.4 563.7,447.9 565.5,452.5C567.6,457.7 569.8,462.9 572.3,467.9C575.9,475.7 579.2,483.5 582.5,491.4C585.8,499.2 589.1,507 592.5,514.7C617.2,571.5 617.2,571.5 625.1,591.7C625.8,593.9 625.8,593.9 627,595C627.3,594.3 627.6,593.5 627.9,592.7C632.8,580 637.7,567.3 643.4,554.9C646.9,547.2 650.2,539.5 653.4,531.8C653.7,531.1 654,530.4 654.3,529.7C657.2,522.9 660,516 662.9,509.1C663.3,508 663.8,506.9 664.2,505.8C665,503.9 665.9,501.9 666.7,499.9C668.9,494.5 671.3,489.1 673.8,483.7C675.7,479.4 677.6,475 679.5,470.7C679.9,469.7 680.3,468.7 680.8,467.7C682.2,464.5 683.6,461.2 685,458C686,455.6 687.1,453.3 688.1,450.9C689.7,447.1 691.3,443.4 693,439.6C695.8,433.2 698.5,426.8 701.3,420.3C714.6,389.7 714.6,389.7 727.2,358.9C729.7,352.7 732.4,346.8 735.3,340.8C736.9,337.4 738.2,333.8 739.6,330.3C740.1,329.1 740.5,327.9 741,326.7C741.3,325.8 741.6,324.9 742,324C741.2,324 741.2,324 740.3,324C726.5,324.1 712.7,324.1 698.9,324.1C692.2,324.1 685.5,324.1 678.9,324.2C672.4,324.2 665.9,324.2 659.5,324.2C657,324.2 654.6,324.2 652.1,324.2C640.5,324.3 629,324.2 617.4,323.1C616.6,323.1 615.8,323 614.9,322.9C597,321 581.4,314.3 569,301C566.5,297.8 564.7,294.6 563,291C562.5,290 562,289 561.5,288C556.5,276.1 555.8,261.8 559.6,249.4C562,243.5 565.3,238.2 569,233C569.7,232 569.7,232 570.4,231C581.2,217.3 599.1,212.3 615.8,210.2C637.1,208 659,208.7 680.4,208.5C683.5,208.4 686.5,208.4 689.5,208.4C719.6,208.1 749.6,207.9 779.7,207.8Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:pathData="M263.3,223.5C276.3,234.5 282.6,248.9 289.6,264C290.9,266.8 292.2,269.5 293.5,272.2C303,291.5 311.4,311.3 320,331C320.3,331.7 320.6,332.5 321,333.2C323.4,338.9 325.9,344.5 328.3,350.2C332.2,359.1 336.1,368.1 340,377C346.2,391.1 352.3,405.2 358.4,419.3C360.8,424.9 363.3,430.5 365.7,436.1C366.5,438 367.3,439.9 368.1,441.7C370.6,447.4 373,453 375.5,458.7C382.5,474.8 389.4,491 396.4,507.2C398.1,511.3 399.9,515.5 401.7,519.6C408.7,536 415.8,552.5 422.7,568.9C424,572 425.3,575.2 426.7,578.3C433,593.2 439.2,608.1 445.5,623C447.5,627.7 449.4,632.4 451.4,637C453.8,642.9 456.3,648.7 458.7,654.6C459.9,657.5 461.2,660.4 462.4,663.3C467.1,674.6 471.8,685.9 476.4,697.3C477.1,698.8 477.1,698.8 477.7,700.4C484.3,716.8 488.7,735.5 482,752.6C479.3,758.3 475.9,763.2 472,768C471.6,768.6 471.1,769.2 470.7,769.8C464.5,777.8 455.6,782.4 446,785C445,785.3 444,785.7 443,786C431.6,787 420.6,786.5 410,782C408.7,781.5 408.7,781.5 407.4,781C380.7,769.4 369.2,736.1 358.2,711.6C356.7,708.3 355.1,705 353.6,701.7C340.8,674.4 329,646.7 317.5,618.9C316,615.2 314.3,611.5 312.7,607.9C309.7,601.4 306.9,594.9 304.1,588.4C303.9,587.8 303.6,587.2 303.3,586.5C299.7,578 296.1,569.4 292.6,560.8C291,556.9 289.2,553 287.4,549.1C284.7,543 282,536.9 279.3,530.8C278.7,529.3 278.1,527.9 277.4,526.5C276.2,523.6 274.9,520.8 273.7,517.9C272.3,514.6 270.8,511.3 269.4,508C264.2,496.1 259.1,484.2 254.1,472.3C250.4,463.6 246.7,455 242.9,446.3C242.5,445.4 242.2,444.5 241.8,443.6C240,439.6 238.3,435.6 236.5,431.6C234.9,427.8 233.2,424.1 231.6,420.4C231.4,419.7 231.1,419.1 230.8,418.5C226.1,407.6 221.5,396.7 217,385.8C214.5,379.6 211.9,373.5 209.2,367.5C207.4,363.7 205.8,359.9 204.1,356.1C203.8,355.2 203.4,354.4 203,353.5C199.1,344.2 195.2,334.9 191.3,325.6C191,324.9 190.7,324.1 190.3,323.3C187.1,315.5 183.8,307.6 180.7,299.8C180.2,298.6 179.7,297.4 179.2,296.2C177.6,291.9 176.2,287.5 175,283C174.8,282.4 174.7,281.8 174.5,281.2C171.2,266.5 173.7,250.9 181.4,238.2C191.3,223.5 203.1,215.7 220.6,212.3C236.3,210.9 250.7,213.5 263.3,223.5Z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 KiB |
@@ -138,6 +138,7 @@
|
||||
<string name="config_error">Ungültige Konfiguration</string>
|
||||
<string name="join_matrix">Matrix-Community beitreten</string>
|
||||
<string name="error_download_failed">Download der Konfiguration fehlgeschlagen</string>
|
||||
<string name="wg_url_confirm_message">Möchtest du wirklich Tunnel von %1$s hinzufügen? Verbinde dich niemals mit einem nicht vertrauenswürdigen VPN!</string>
|
||||
<string name="add_from_url">Von URL hinzufügen</string>
|
||||
<string name="export_logs">Gespeicherte Logs exportieren</string>
|
||||
<string name="app_permission_title">Steuere Tunnel und Auto-Tunnel Funktionen.</string>
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
<string name="delete">Удалить</string>
|
||||
<string name="export_failed">Экспорт не выполнен</string>
|
||||
<string name="error_download_failed">Невозможно скачать конфигурацию</string>
|
||||
<string name="wg_url_confirm_message">Добавить туннели от %1$s? Никогда не подключайтесь к неизвестному VPN!</string>
|
||||
<string name="select_all">Выбрать все</string>
|
||||
<string name="export_success">Экспорт успешно выполнен</string>
|
||||
<string name="check_for_update">Проверить обновление</string>
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
<string name="add_from_url">Add from URL</string>
|
||||
<string name="enter_config_url">Enter config URL</string>
|
||||
<string name="error_download_failed">Failed to download config</string>
|
||||
<string name="wg_url_confirm_message">Are you sure you want to add tunnels from %1$s? Never connect to an untrusted VPN!</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="select">Select</string>
|
||||
@@ -535,4 +536,22 @@
|
||||
<string name="hide_password">Hide password</string>
|
||||
<string name="restore_failed_wrong_password">Restore failed. Wrong password</string>
|
||||
<string name="restore_failed_invalid_file">Restore failed. Select a valid backup file (.sqlite3 or .sqlite3.aes)</string>
|
||||
<string name="error_invalid_config_url">This link returned an invalid config file. Make sure you are using a direct download link</string>
|
||||
|
||||
<string name="local_network_permission_title">Local Network Access Needed</string>
|
||||
|
||||
<string name="local_network_permission_intro">WG Tunnel needs access to your local network for several features to work properly.</string>
|
||||
|
||||
<string name="local_network_permission_issues_intro">Without this permission, you may experience issues with:</string>
|
||||
|
||||
<string name="local_network_permission_feature_tunnels">- Connecting to certain tunnels</string>
|
||||
<string name="local_network_permission_feature_autotunnel">- Auto-tunneling and split tunneling features</string>
|
||||
<string name="local_network_permission_feature_proxy">- Local proxy and bypass functionality</string>
|
||||
|
||||
<string name="local_network_permission_recommendation">Granting this permission is strongly recommended.</string>
|
||||
<string name="local_network_permission_nearby_devices">Note: Android labels this permission as “nearby devices”.</string>
|
||||
|
||||
<string name="not_now">Not now</string>
|
||||
|
||||
<string name="local_network_permission_denied">Local network access denied. Some features may not work properly</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
object Constants {
|
||||
const val VERSION_NAME = "5.0.4"
|
||||
const val VERSION_CODE = 50004
|
||||
const val VERSION_NAME = "5.0.6"
|
||||
const val VERSION_CODE = 50006
|
||||
const val TARGET_SDK = 37
|
||||
const val MIN_SDK = 26
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
What's new:
|
||||
- Bugfix for certain scenarios that were not cleaning up the vpn service fully
|
||||
- Allows tunnel imports via wg:// url deep links
|
||||
- Improvements to Always-On VPN reliability
|
||||
- Improved cellular connectivity detection for dual sims
|
||||
@@ -0,0 +1,3 @@
|
||||
What's new:
|
||||
- Bugfix for Android 17 local network permission requirement
|
||||
- Bugfix for app shortcuts causing crash
|
||||
+37
-21
@@ -102,6 +102,8 @@ class AndroidNetworkMonitor(
|
||||
private var ethernetCallback: ConnectivityManager.NetworkCallback? = null
|
||||
|
||||
private val airplaneModeState = MutableStateFlow(appContext.isAirplaneModeOn())
|
||||
private val activeCellularNetworks =
|
||||
MutableStateFlow<Map<Network, NetworkCapabilities>>(emptyMap())
|
||||
private val airplaneModeFlow: Flow<Boolean> = airplaneModeState.asStateFlow()
|
||||
|
||||
// tracking to prevent races that occur when VPN is first activated and to prevent redundant
|
||||
@@ -317,14 +319,19 @@ class AndroidNetworkMonitor(
|
||||
private val cellularFlow: Flow<TransportEvent> = callbackFlow {
|
||||
val onAvailable: (Network) -> Unit = { network ->
|
||||
Timber.d("Cellular onAvailable: $network")
|
||||
// Defensive cleanup
|
||||
activeCellularNetworks.update { it - network }
|
||||
}
|
||||
val onLost: (Network) -> Unit = { network ->
|
||||
Timber.d("Cellular onLost: $network")
|
||||
activeCellularNetworks.update { it - network }
|
||||
trySend(TransportEvent.Lost(network))
|
||||
}
|
||||
val onCapabilitiesChanged: (Network, NetworkCapabilities) -> Unit = { network, caps ->
|
||||
Timber.d("Cellular onCapabilitiesChanged: $network")
|
||||
trySend(TransportEvent.CapabilitiesChanged(network, caps))
|
||||
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||
activeCellularNetworks.update { it + (network to caps) }
|
||||
trySend(TransportEvent.CapabilitiesChanged(network, caps))
|
||||
}
|
||||
}
|
||||
|
||||
cellularCallback =
|
||||
@@ -339,13 +346,10 @@ class AndroidNetworkMonitor(
|
||||
|
||||
val request =
|
||||
NetworkRequest.Builder()
|
||||
.apply { addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) }
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.build()
|
||||
|
||||
connectivityManager?.registerNetworkCallback(request, cellularCallback!!)
|
||||
|
||||
trySend(TransportEvent.Unknown)
|
||||
|
||||
awaitClose {
|
||||
runCatching { connectivityManager?.unregisterNetworkCallback(cellularCallback!!) }
|
||||
.onFailure { Timber.e(it, "Error unregistering cellular network callback") }
|
||||
@@ -438,6 +442,26 @@ class AndroidNetworkMonitor(
|
||||
.also { Timber.d("Current SSID via ${method.name}: $it") }
|
||||
}
|
||||
|
||||
private fun hasGoodCellularNetwork(): Boolean =
|
||||
activeCellularNetworks.value.values.any { caps ->
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) &&
|
||||
(Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED))
|
||||
}
|
||||
|
||||
private fun getGoodCellularNetwork(): Network? =
|
||||
activeCellularNetworks.value.entries
|
||||
.firstOrNull { (_, caps) ->
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) &&
|
||||
(Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED))
|
||||
}
|
||||
?.key
|
||||
|
||||
// default network events don't contain detailed capability information of underlying networks,
|
||||
// so we need to track separately
|
||||
private data class NetworkData(
|
||||
@@ -577,21 +601,13 @@ class AndroidNetworkMonitor(
|
||||
}
|
||||
|
||||
// only count cellular as connected if validated AND not in airplane mode
|
||||
!isAirplaneOn &&
|
||||
networkData.cellularEvent is TransportEvent.CapabilitiesChanged &&
|
||||
networkData.cellularEvent.networkCapabilities?.let { caps ->
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
||||
caps.hasCapability(
|
||||
NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||
) &&
|
||||
caps.hasCapability(
|
||||
NetworkCapabilities.NET_CAPABILITY_VALIDATED
|
||||
) &&
|
||||
caps.hasCapability(
|
||||
NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
|
||||
)
|
||||
} == true -> {
|
||||
ActiveNetwork.Cellular(networkData.cellularEvent.network)
|
||||
!isAirplaneOn && hasGoodCellularNetwork() -> {
|
||||
val goodNetwork = getGoodCellularNetwork()
|
||||
if (goodNetwork != null) {
|
||||
ActiveNetwork.Cellular(goodNetwork)
|
||||
} else {
|
||||
ActiveNetwork.Disconnected()
|
||||
}
|
||||
}
|
||||
|
||||
else -> ActiveNetwork.Disconnected()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
|
||||
<!--foreground service special use for non VPN service tunnels, android 14-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<!--foreground service special use for VPN service tunnels, android 14-->
|
||||
|
||||
@@ -23,10 +23,8 @@ import com.zaneschepke.tunnel.state.KillSwitchState
|
||||
import com.zaneschepke.tunnel.util.RootShell
|
||||
import com.zaneschepke.tunnel.util.RootShellException
|
||||
import com.zaneschepke.tunnel.util.buildResolvedPeers
|
||||
import com.zaneschepke.tunnel.util.isLastTunnelOfServiceType
|
||||
import com.zaneschepke.tunnel.util.toHostMap
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ActiveConfig
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -92,8 +90,6 @@ class TunnelBackend(
|
||||
NETWORK_CHANGE_RESET,
|
||||
}
|
||||
|
||||
private var dnsConfigJob: Job? = null
|
||||
|
||||
private val statusCallback = StatusCallback { handle, code ->
|
||||
val state = Tunnel.State.fromNative(code) ?: return@StatusCallback
|
||||
val tunnelId = byHandle[handle] ?: return@StatusCallback
|
||||
@@ -107,7 +103,7 @@ class TunnelBackend(
|
||||
tunnelMutex.withLock {
|
||||
runCatching {
|
||||
if (_status.value.activeTunnels.containsKey(tunnel.id)) {
|
||||
Timber.d("Tunnel ${tunnel.id} already running — ignoring start")
|
||||
Timber.w("Tunnel ${tunnel.id} already running")
|
||||
return@runCatching
|
||||
}
|
||||
|
||||
@@ -130,74 +126,68 @@ class TunnelBackend(
|
||||
if (scriptsEnabled)
|
||||
mode.config.`interface`.preUp?.let { runScripts(it, tunnel.id) }
|
||||
|
||||
val fd = setupServiceForMode(tunnel, mode)
|
||||
setupServiceForMode(tunnel, mode)
|
||||
|
||||
if (hasDynamicEndpoints(mode)) {
|
||||
pendingResolutionJobs[tunnel.id] = startTunnelBootstrapJob(tunnel, mode, fd)
|
||||
pendingResolutionJobs[tunnel.id] = startTunnelBootstrapJob(tunnel, mode)
|
||||
} else {
|
||||
val result = engine.start(tunnel, mode, fd)
|
||||
val result = engine.start(tunnel, mode)
|
||||
onEngineStartResult(tunnel.id, result)
|
||||
|
||||
if (scriptsEnabled) {
|
||||
mode.config.`interface`.postUp?.let { runScripts(it, tunnel.id) }
|
||||
}
|
||||
|
||||
tunnelJobs[tunnel.id] = startTunnelJobs(result.handle, tunnel, mode)
|
||||
}
|
||||
}
|
||||
.onFailure { cleanup(tunnel.id) }
|
||||
}
|
||||
|
||||
private fun startTunnelBootstrapJob(tunnel: Tunnel, mode: BackendMode, fd: Int?) =
|
||||
scope.launch {
|
||||
try {
|
||||
updateTunnelBootstrapState(tunnel.id, BootstrapState.ResolvingDns)
|
||||
private fun startTunnelBootstrapJob(tunnel: Tunnel, mode: BackendMode) = scope.launch {
|
||||
try {
|
||||
updateTunnelBootstrapState(tunnel.id, BootstrapState.ResolvingDns)
|
||||
|
||||
val resultMap = endpointResolver.resolvePeers(mode)
|
||||
ensureActive()
|
||||
val resultMap = endpointResolver.resolvePeers(mode)
|
||||
ensureActive()
|
||||
|
||||
val networkHasIpv6 = stableNetworkEngine.stableState.value?.state?.hasIpv6 ?: false
|
||||
val hostMap =
|
||||
resultMap.toHostMap(
|
||||
preferIpv6 =
|
||||
tunnel.ipStrategy is Tunnel.IpStrategy.PreferIpv6 && networkHasIpv6
|
||||
)
|
||||
val resolvedPeers = mode.config.buildResolvedPeers(hostMap)
|
||||
val networkHasIpv6 = stableNetworkEngine.stableState.value?.state?.hasIpv6 ?: false
|
||||
val hostMap =
|
||||
resultMap.toHostMap(
|
||||
preferIpv6 = tunnel.ipStrategy is Tunnel.IpStrategy.PreferIpv6 && networkHasIpv6
|
||||
)
|
||||
val resolvedPeers = mode.config.buildResolvedPeers(hostMap)
|
||||
|
||||
updateTunnelBootstrapState(tunnel.id, BootstrapState.Complete)
|
||||
updateTunnelBootstrapState(tunnel.id, BootstrapState.Complete)
|
||||
|
||||
val resolvedConfig = mode.config.copy(peers = resolvedPeers)
|
||||
val updatedMode =
|
||||
when (mode) {
|
||||
is BackendMode.Vpn -> mode.copy(config = resolvedConfig)
|
||||
is BackendMode.Proxy.Standard -> mode.copy(config = resolvedConfig)
|
||||
is BackendMode.Proxy.KillSwitchPrimary -> mode.copy(config = resolvedConfig)
|
||||
}
|
||||
|
||||
val result = engine.start(tunnel, updatedMode, fd)
|
||||
onEngineStartResult(tunnel.id, result)
|
||||
|
||||
val scriptsEnabled = tunnel.scriptsEnabled
|
||||
if (scriptsEnabled) {
|
||||
mode.config.`interface`.postUp?.let { runScripts(it, tunnel.id) }
|
||||
val resolvedConfig = mode.config.copy(peers = resolvedPeers)
|
||||
val updatedMode =
|
||||
when (mode) {
|
||||
is BackendMode.Vpn -> mode.copy(config = resolvedConfig)
|
||||
is BackendMode.Proxy.Standard -> mode.copy(config = resolvedConfig)
|
||||
is BackendMode.Proxy.KillSwitchPrimary -> mode.copy(config = resolvedConfig)
|
||||
}
|
||||
|
||||
tunnelJobs[tunnel.id] = startTunnelJobs(result.handle, tunnel, updatedMode)
|
||||
} catch (t: Throwable) {
|
||||
if (t is kotlinx.coroutines.CancellationException) {
|
||||
Timber.d("Bootstrap job cancelled for tunnel ${tunnel.id}")
|
||||
throw t
|
||||
} else {
|
||||
Timber.e(t, "Tunnel bootstrap failed for ${tunnel.id}")
|
||||
}
|
||||
cleanup(tunnel.id)
|
||||
} finally {
|
||||
pendingResolutionJobs.remove(tunnel.id)
|
||||
val result = engine.start(tunnel, updatedMode)
|
||||
onEngineStartResult(tunnel.id, result)
|
||||
|
||||
val scriptsEnabled = tunnel.scriptsEnabled
|
||||
if (scriptsEnabled) {
|
||||
mode.config.`interface`.postUp?.let { runScripts(it, tunnel.id) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setupServiceForMode(tunnel: Tunnel, mode: BackendMode): Int? {
|
||||
var fd: Int? = null
|
||||
tunnelJobs[tunnel.id] = startTunnelJobs(result.handle, tunnel, updatedMode)
|
||||
} catch (t: Throwable) {
|
||||
if (t is kotlinx.coroutines.CancellationException) {
|
||||
Timber.d("Bootstrap job cancelled for tunnel ${tunnel.id}")
|
||||
} else {
|
||||
Timber.e(t, "Tunnel bootstrap failed for ${tunnel.id}")
|
||||
cleanup(tunnel.id)
|
||||
}
|
||||
if (t is kotlinx.coroutines.CancellationException) throw t
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setupServiceForMode(tunnel: Tunnel, mode: BackendMode) {
|
||||
when (mode) {
|
||||
is BackendMode.Proxy.KillSwitchPrimary -> {
|
||||
serviceHolder.ensureVpnProtectorRegistered()
|
||||
@@ -207,10 +197,9 @@ class TunnelBackend(
|
||||
}
|
||||
is BackendMode.Vpn -> {
|
||||
val service = serviceHolder.ensureVpnProtectorRegistered()
|
||||
fd = service.createTunInterface(tunnel, mode.config)?.detachFd()
|
||||
service.createTunInterface(tunnel, mode.config)
|
||||
}
|
||||
}
|
||||
return fd
|
||||
}
|
||||
|
||||
private fun onEngineStartResult(tunnelId: Int, result: EngineStartResult) {
|
||||
@@ -221,13 +210,27 @@ class TunnelBackend(
|
||||
byTunnelId[tunnelId] = result.handle
|
||||
}
|
||||
|
||||
private fun cleanup(tunnelId: Int) {
|
||||
private suspend fun cleanup(tunnelId: Int) {
|
||||
pendingResolutionJobs.remove(tunnelId)?.cancel()
|
||||
tunnelJobs.remove(tunnelId)?.cancel()
|
||||
|
||||
val activeTunnels = _status.value.activeTunnels
|
||||
|
||||
val vpnTypeCount = activeTunnels.values.count { it.mode is BackendMode.Vpn }
|
||||
|
||||
val proxyTypeCount = activeTunnels.values.count { it.mode is BackendMode.Proxy.Standard }
|
||||
|
||||
removeActiveTunnel(tunnelId)
|
||||
byTunnelId[tunnelId]?.let { byHandle.remove(it) }
|
||||
byTunnelId.remove(tunnelId)
|
||||
peerUpdateMutexes.remove(tunnelId)
|
||||
|
||||
if (vpnTypeCount == 1) {
|
||||
serviceHolder.stopVpnService()
|
||||
}
|
||||
if (proxyTypeCount == 1) {
|
||||
serviceHolder.stopTunnelService()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runScripts(commands: List<String>, tunnelId: Int) {
|
||||
@@ -246,29 +249,20 @@ class TunnelBackend(
|
||||
}
|
||||
|
||||
override fun setAlwaysOnCallback(alwaysOnCallback: VpnService.AlwaysOnCallback) {
|
||||
ServiceHolder.alwaysOnCallback = WeakReference(alwaysOnCallback)
|
||||
ServiceHolder.alwaysOnCallback = alwaysOnCallback
|
||||
}
|
||||
|
||||
override suspend fun stop(id: Int): Result<Unit> = tunnelMutex.withLock {
|
||||
runCatching {
|
||||
val activeTun = _status.value.activeTunnels[id] ?: return@runCatching
|
||||
val mode = activeTun.mode ?: return@runCatching
|
||||
updateTunnelTransportState(id, Tunnel.State.Stopping)
|
||||
|
||||
val isLast = _status.value.activeTunnels.size == 1
|
||||
val isLastOfServiceType = _status.value.isLastTunnelOfServiceType(id)
|
||||
|
||||
try {
|
||||
stopTunnelInternal(id, activeTun)
|
||||
} finally {
|
||||
applicationProvider.refreshTile(serviceHolder.context)
|
||||
if (isLast) VpnBackend.setStatusCallback(null)
|
||||
if (isLastOfServiceType) {
|
||||
when (mode) {
|
||||
is BackendMode.Proxy.KillSwitchPrimary,
|
||||
is BackendMode.Vpn -> serviceHolder.stopVpnService()
|
||||
is BackendMode.Proxy.Standard -> serviceHolder.stopTunnelService()
|
||||
}
|
||||
if (_status.value.activeTunnels.isEmpty()) {
|
||||
VpnBackend.setStatusCallback(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,8 +271,6 @@ class TunnelBackend(
|
||||
private suspend fun stopTunnelInternal(tunnelId: Int, activeTunnel: ActiveTunnel) {
|
||||
updateTunnelTransportState(tunnelId, Tunnel.State.Stopping)
|
||||
|
||||
pendingResolutionJobs.remove(tunnelId)?.cancel()
|
||||
|
||||
val handle = byTunnelId[tunnelId]
|
||||
|
||||
if (handle == null) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.zaneschepke.wireguardautotunnel.parser.PeerSection
|
||||
|
||||
internal interface TunnelEngine {
|
||||
|
||||
suspend fun start(tunnel: Tunnel, mode: BackendMode, fd: Int?): EngineStartResult
|
||||
suspend fun start(tunnel: Tunnel, mode: BackendMode): EngineStartResult
|
||||
|
||||
suspend fun stop(handle: Int, mode: BackendMode)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.UUID
|
||||
|
||||
internal class WireGuardTunnelEngine(private val serviceHolder: ServiceHolder) : TunnelEngine {
|
||||
|
||||
override suspend fun start(tunnel: Tunnel, mode: BackendMode, fd: Int?): EngineStartResult {
|
||||
override suspend fun start(tunnel: Tunnel, mode: BackendMode): EngineStartResult {
|
||||
|
||||
val ifName = WGT_INTERFACE_PREFIX + tunnel.id
|
||||
|
||||
@@ -56,7 +56,8 @@ internal class WireGuardTunnelEngine(private val serviceHolder: ServiceHolder) :
|
||||
startProxyTunnel(ifName, mode.config, proxyConfig, false)
|
||||
}
|
||||
is BackendMode.Vpn -> {
|
||||
startVpnTunnel(ifName, mode.config, fd)
|
||||
val service = serviceHolder.getVpnService()
|
||||
startVpnTunnel(ifName, mode.config, service.detachVpnTunnelFd())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.tunnel.ProxyBackend
|
||||
import com.zaneschepke.tunnel.util.BackendException
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -50,7 +49,7 @@ internal class ServiceHolder(val context: Context) {
|
||||
}
|
||||
|
||||
if (_vpnService.value == null) {
|
||||
context.startService(Intent(context, VpnService::class.java))
|
||||
VpnService.start(context, VpnService::class.java)
|
||||
}
|
||||
|
||||
return try {
|
||||
@@ -76,16 +75,22 @@ internal class ServiceHolder(val context: Context) {
|
||||
|
||||
suspend fun stopVpnService() {
|
||||
val service = _vpnService.value ?: return
|
||||
clearVpnService()
|
||||
service.shutdown()
|
||||
withTimeoutOrNull(1_000L.milliseconds) { vpnServiceFlow.first { it == null } }
|
||||
try {
|
||||
service.shutdown()
|
||||
withTimeoutOrNull(1_500L.milliseconds) { vpnServiceFlow.first { it == null } }
|
||||
} finally {
|
||||
clearVpnService()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopTunnelService() {
|
||||
val service = _tunnelService.value ?: return
|
||||
clearTunnelService()
|
||||
service.shutdown()
|
||||
withTimeoutOrNull(1_000L.milliseconds) { tunnelServiceFlow.first { it == null } }
|
||||
try {
|
||||
service.shutdown()
|
||||
withTimeoutOrNull(1_500L.milliseconds) { tunnelServiceFlow.first { it == null } }
|
||||
} finally {
|
||||
clearTunnelService()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,6 +109,6 @@ internal class ServiceHolder(val context: Context) {
|
||||
const val SPECIAL_USE_SERVICE_TYPE_ID = 1 shl 30
|
||||
const val DEFAULT_MTU = 1280
|
||||
// for consumer to set AOVPN callback
|
||||
var alwaysOnCallback: WeakReference<VpnService.AlwaysOnCallback>? = null
|
||||
var alwaysOnCallback: VpnService.AlwaysOnCallback? = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class TunnelService : LifecycleService() {
|
||||
(intent.component!!.packageName != packageName)
|
||||
) {
|
||||
Timber.d("TunnelService started by system")
|
||||
alwaysOnCallback?.get()?.alwaysOnTriggered()
|
||||
alwaysOnCallback?.alwaysOnTriggered()
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.zaneschepke.tunnel.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.TrafficStats
|
||||
import android.os.Build
|
||||
@@ -39,10 +40,9 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
|
||||
|
||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val shutdownScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@Volatile private var userActivatedShutdown = false
|
||||
private var hevBridgeJob: Job? = null
|
||||
@Volatile private var fd: ParcelFileDescriptor? = null
|
||||
@Volatile private var hevBridgeFd: ParcelFileDescriptor? = null
|
||||
@Volatile private var vpnTunFd: ParcelFileDescriptor? = null
|
||||
|
||||
override fun onCreate() {
|
||||
serviceHolder.set(this)
|
||||
@@ -58,31 +58,21 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
|
||||
// Stop the companion foreground service alongside the VPN teardown
|
||||
stopService(Intent(this, VpnCompanionService::class.java))
|
||||
|
||||
closeVpnTunnelFd()
|
||||
disableKillSwitch()
|
||||
hevBridgeJob?.cancel()
|
||||
serviceScope.cancel()
|
||||
stopHevSocks5Bridge()
|
||||
if (!userActivatedShutdown) {
|
||||
Timber.d("Service being killed by system, clean up tunnels")
|
||||
shutdownScope.launch { backend.stopAllActiveTunnels() }
|
||||
}
|
||||
} finally {
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
fun shutdown() {
|
||||
userActivatedShutdown = true
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onRevoke() {
|
||||
Timber.w("VPN privilege revoked by system")
|
||||
userActivatedShutdown = false
|
||||
Timber.w("VPN revoked by user via system settings")
|
||||
disableKillSwitch()
|
||||
stopHevSocks5Bridge()
|
||||
serviceScope.launch { backend.stopAllActiveTunnels() }
|
||||
shutdownScope.launch { backend.stopAllActiveTunnels() }
|
||||
stopSelf()
|
||||
super.onRevoke()
|
||||
}
|
||||
@@ -90,21 +80,40 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
serviceHolder.set(this)
|
||||
|
||||
// Ensure the companion service is up immediately to provide foreground process
|
||||
bootKeepaliveService()
|
||||
|
||||
// Service restarted by system or Always-on VPN started
|
||||
if (
|
||||
intent == null ||
|
||||
intent.component == null ||
|
||||
(intent.component!!.packageName != packageName)
|
||||
) {
|
||||
Timber.d("VpnService started by system (Always-On trigger)")
|
||||
alwaysOnCallback?.get()?.alwaysOnTriggered()
|
||||
// system recovery restart
|
||||
if (intent == null) {
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
val isUserLaunch = intent.getBooleanExtra(getUserLaunchExtraKey(this), false)
|
||||
|
||||
val platformSaysAlwaysOn =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
isAlwaysOn
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
val isAlwaysOnTrigger =
|
||||
!isUserLaunch && (intent.action == SERVICE_INTERFACE || platformSaysAlwaysOn)
|
||||
|
||||
if (isAlwaysOnTrigger) {
|
||||
Timber.d("VpnService started by system (Always-On trigger)")
|
||||
alwaysOnCallback?.alwaysOnTriggered()
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
// have to close fds before we can trigger service shutdown
|
||||
closeVpnTunnelFd()
|
||||
disableKillSwitch()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun bootKeepaliveService() {
|
||||
try {
|
||||
val intent = Intent(this, VpnCompanionService::class.java)
|
||||
@@ -119,7 +128,7 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
|
||||
val job = serviceScope.launch {
|
||||
TrafficStats.setThreadStatsTag(HEV_BRIDGE_TRAFFIC_TAG)
|
||||
try {
|
||||
val vpnFd = fd ?: throw IOException("No VPN interface fd available")
|
||||
val vpnFd = hevBridgeFd ?: throw IOException("No VPN interface fd available")
|
||||
|
||||
repeat(60) { attempt ->
|
||||
try {
|
||||
@@ -176,15 +185,15 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
|
||||
}
|
||||
|
||||
private fun disableKillSwitch() {
|
||||
fd?.close()
|
||||
fd = null
|
||||
hevBridgeFd?.close()
|
||||
hevBridgeFd = null
|
||||
}
|
||||
|
||||
override fun setKillSwitch(config: KillSwitchConfig?) {
|
||||
if (config == null) return disableKillSwitch()
|
||||
fd?.close()
|
||||
hevBridgeFd?.close()
|
||||
val intent = backend.applicationProvider.createVpnConfigurePendingIntent(this@VpnService)
|
||||
fd =
|
||||
hevBridgeFd =
|
||||
Builder()
|
||||
.apply {
|
||||
setSession(LOCKDOWN_SESSION_NAME)
|
||||
@@ -211,76 +220,94 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
|
||||
.establish()
|
||||
}
|
||||
|
||||
fun createTunInterface(tunnel: Tunnel, config: Config): ParcelFileDescriptor? {
|
||||
fun createTunInterface(tunnel: Tunnel, config: Config) {
|
||||
val intent = backend.applicationProvider.createVpnConfigurePendingIntent(this@VpnService)
|
||||
return Builder()
|
||||
.apply {
|
||||
setSession(tunnel.name)
|
||||
setConfigureIntent(intent)
|
||||
setMtu(config.`interface`.mtu ?: DEFAULT_MTU)
|
||||
setBlocking(true)
|
||||
setUnderlyingNetworks(null)
|
||||
vpnTunFd?.close()
|
||||
vpnTunFd = null
|
||||
vpnTunFd =
|
||||
Builder()
|
||||
.apply {
|
||||
setSession(tunnel.name)
|
||||
setConfigureIntent(intent)
|
||||
setMtu(config.`interface`.mtu ?: DEFAULT_MTU)
|
||||
setBlocking(true)
|
||||
setUnderlyingNetworks(null)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
setMetered(tunnel.isMetered)
|
||||
}
|
||||
|
||||
config.`interface`.includedApplications?.forEach { addAllowedApplication(it) }
|
||||
config.`interface`.excludedApplications?.forEach { addDisallowedApplication(it) }
|
||||
|
||||
var hasIpv4 = false
|
||||
var hasIpv6 = false
|
||||
var sawDefaultRoute = false
|
||||
|
||||
// Parse interface addresses
|
||||
config.`interface`.address?.split(",")?.forEach { rawAddress ->
|
||||
val (address, prefixLength) = rawAddress.parseInetNetwork()
|
||||
addAddress(address, prefixLength)
|
||||
if (address is Inet4Address) hasIpv4 = true else hasIpv6 = true
|
||||
}
|
||||
|
||||
// Parse peer routes
|
||||
config.peers.forEach { peer ->
|
||||
peer.allowedIPs
|
||||
?.split(",")
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.forEach { entry ->
|
||||
val (address, prefix) = entry.parseInetNetwork()
|
||||
addRoute(address, prefix)
|
||||
|
||||
if (prefix == 0) {
|
||||
sawDefaultRoute = true
|
||||
}
|
||||
if (address is Inet4Address) hasIpv4 = true else hasIpv6 = true
|
||||
}
|
||||
}
|
||||
|
||||
// "Kill-switch" semantics (mirrors wireguard-android)
|
||||
val isKillSwitchRouting = sawDefaultRoute && config.peers.size == 1
|
||||
|
||||
if (!isKillSwitchRouting) {
|
||||
allowFamily(OsConstants.AF_INET)
|
||||
allowFamily(OsConstants.AF_INET6)
|
||||
}
|
||||
|
||||
// Only add DNS servers whose family is supported
|
||||
config.`interface`.dns?.let { rawDns ->
|
||||
val dnsConfig = rawDns.parseDns()
|
||||
dnsConfig.dnsServers.forEach { dnsServer ->
|
||||
val isIpv6 = dnsServer is Inet6Address
|
||||
if ((isIpv6 && hasIpv6) || (!isIpv6 && hasIpv4)) {
|
||||
addDnsServer(dnsServer)
|
||||
} else {
|
||||
Timber.w(
|
||||
"Dropped DNS server $dnsServer: IP family not allowed by interface/routes"
|
||||
)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
setMetered(tunnel.isMetered)
|
||||
}
|
||||
|
||||
config.`interface`.includedApplications?.forEach { addAllowedApplication(it) }
|
||||
config.`interface`.excludedApplications?.forEach {
|
||||
addDisallowedApplication(it)
|
||||
}
|
||||
|
||||
var hasIpv4 = false
|
||||
var hasIpv6 = false
|
||||
var sawDefaultRoute = false
|
||||
|
||||
// Parse interface addresses
|
||||
config.`interface`.address?.split(",")?.forEach { rawAddress ->
|
||||
val (address, prefixLength) = rawAddress.parseInetNetwork()
|
||||
addAddress(address, prefixLength)
|
||||
if (address is Inet4Address) hasIpv4 = true else hasIpv6 = true
|
||||
}
|
||||
|
||||
// Parse peer routes
|
||||
config.peers.forEach { peer ->
|
||||
peer.allowedIPs
|
||||
?.split(",")
|
||||
?.map { it.trim() }
|
||||
?.filter { it.isNotEmpty() }
|
||||
?.forEach { entry ->
|
||||
val (address, prefix) = entry.parseInetNetwork()
|
||||
addRoute(address, prefix)
|
||||
|
||||
if (prefix == 0) {
|
||||
sawDefaultRoute = true
|
||||
}
|
||||
if (address is Inet4Address) hasIpv4 = true else hasIpv6 = true
|
||||
}
|
||||
}
|
||||
|
||||
// "Kill-switch" semantics (mirrors wireguard-android)
|
||||
val isKillSwitchRouting = sawDefaultRoute && config.peers.size == 1
|
||||
|
||||
if (!isKillSwitchRouting) {
|
||||
allowFamily(OsConstants.AF_INET)
|
||||
allowFamily(OsConstants.AF_INET6)
|
||||
}
|
||||
|
||||
// Only add DNS servers whose family is supported
|
||||
config.`interface`.dns?.let { rawDns ->
|
||||
val dnsConfig = rawDns.parseDns()
|
||||
dnsConfig.dnsServers.forEach { dnsServer ->
|
||||
val isIpv6 = dnsServer is Inet6Address
|
||||
if ((isIpv6 && hasIpv6) || (!isIpv6 && hasIpv4)) {
|
||||
addDnsServer(dnsServer)
|
||||
} else {
|
||||
Timber.w(
|
||||
"Dropped DNS server $dnsServer: IP family not allowed by interface/routes"
|
||||
)
|
||||
}
|
||||
}
|
||||
dnsConfig.searchDomains.forEach { addSearchDomain(it) }
|
||||
}
|
||||
dnsConfig.searchDomains.forEach { addSearchDomain(it) }
|
||||
}
|
||||
}
|
||||
.establish()
|
||||
.establish()
|
||||
}
|
||||
|
||||
fun detachVpnTunnelFd(): Int? {
|
||||
val tunFd = vpnTunFd
|
||||
vpnTunFd = null
|
||||
return tunFd?.detachFd()
|
||||
}
|
||||
|
||||
fun closeVpnTunnelFd() {
|
||||
try {
|
||||
vpnTunFd?.close()
|
||||
} catch (_: Exception) {}
|
||||
vpnTunFd = null
|
||||
}
|
||||
|
||||
override fun startHevSocks5Bridge(port: Int, pass: String) {
|
||||
@@ -317,6 +344,21 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private fun getUserLaunchExtraKey(context: Context): String {
|
||||
return "${context.packageName}.EXTRA_IS_USER_LAUNCH"
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun start(context: Context, serviceClass: Class<out VpnService>) {
|
||||
val intent =
|
||||
Intent(context, serviceClass).apply {
|
||||
action = SERVICE_INTERFACE
|
||||
putExtra(getUserLaunchExtraKey(context), true)
|
||||
}
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
private const val LOCKDOWN_SESSION_NAME = "Lockdown"
|
||||
private const val LOCALHOST = "127.0.0.1"
|
||||
private const val IPV4_INTERFACE_ADDRESS = "10.0.0.1"
|
||||
|
||||
Reference in New Issue
Block a user