mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d78443e7fa | |||
| 40d0466c14 | |||
| 5220c1a10c | |||
| 0e4e421628 | |||
| abdbf74755 | |||
| 5bc49eec50 | |||
| c7040b8081 | |||
| 26ecfec3fc | |||
| 5408cf3954 | |||
| 22c4a303fc |
+12
-2
@@ -10,13 +10,18 @@ import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationS
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class TunnelEventDispatcher(
|
||||
private val notificationManager: TunnelNotificationService,
|
||||
@@ -24,6 +29,7 @@ class TunnelEventDispatcher(
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
fun bind(
|
||||
scope: CoroutineScope,
|
||||
providerEvents: Flow<TunnelEvent>,
|
||||
@@ -102,7 +108,8 @@ class TunnelEventDispatcher(
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
|
||||
val displayState =
|
||||
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
displayStates[id]
|
||||
?: DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
|
||||
TunnelNotificationLine(
|
||||
id = id,
|
||||
@@ -113,6 +120,7 @@ class TunnelEventDispatcher(
|
||||
.associateBy { it.id }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.debounce(500.milliseconds) // give the service notification time to display
|
||||
.onEach { vpnLines -> notificationManager.updateVpnPersistentNotification(vpnLines) }
|
||||
.launchIn(scope)
|
||||
|
||||
@@ -129,7 +137,8 @@ class TunnelEventDispatcher(
|
||||
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState =
|
||||
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
displayStates[id]
|
||||
?: DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
|
||||
TunnelNotificationLine(
|
||||
id = id,
|
||||
@@ -140,6 +149,7 @@ class TunnelEventDispatcher(
|
||||
.associateBy { it.id }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.debounce(500.milliseconds) // give the service notification time to display
|
||||
.onEach { proxyLines ->
|
||||
notificationManager.updateProxyPersistentNotification(proxyLines)
|
||||
}
|
||||
|
||||
+17
-4
@@ -47,12 +47,15 @@ class TunnelCoordinator(
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
|
||||
private val _userOverrideFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||
val userOverrideFlow = _userOverrideFlow.asSharedFlow()
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>> =
|
||||
tunnelProvider.backendStatus
|
||||
.map { status ->
|
||||
status.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||
DisplayTunnelState.from(activeTunnel)
|
||||
DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
.debounce(400L.milliseconds)
|
||||
@@ -107,11 +110,24 @@ class TunnelCoordinator(
|
||||
) = tunnelMutex.withLock {
|
||||
// wait for app to be bootstrapped
|
||||
bootstrapCoordinator.isReady.first { it }
|
||||
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
|
||||
// enforce single tunnel, for now
|
||||
if (backendStatus.value.activeTunnels.isNotEmpty()) {
|
||||
stopActiveTunnelsInternal()
|
||||
}
|
||||
|
||||
startTunnelInternal(config, source)
|
||||
}
|
||||
|
||||
suspend fun stopTunnel(id: Int, source: TunnelActionSource = TunnelActionSource.USER) =
|
||||
tunnelMutex.withLock {
|
||||
if (source == TunnelActionSource.USER) {
|
||||
_userOverrideFlow.tryEmit(Unit)
|
||||
}
|
||||
stopTunnelInternal(id, source)
|
||||
}
|
||||
|
||||
@@ -167,9 +183,6 @@ class TunnelCoordinator(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO for now, enforce single tunnel until multi-tunneling is implement
|
||||
stopActiveTunnelsInternal()
|
||||
|
||||
tunnelProvider
|
||||
.startTunnel(
|
||||
tunnel =
|
||||
|
||||
+12
-5
@@ -17,6 +17,7 @@ class AutoTunnelEngine {
|
||||
}
|
||||
}
|
||||
Decision.None -> AutoTunnelEvent.DoNothing
|
||||
is Decision.StopDueToNoInternet -> AutoTunnelEvent.StopAllDueToNoInternet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +28,17 @@ class AutoTunnelEngine {
|
||||
|
||||
val activeTunnelIds = backend.activeTunnels.keys.toSet()
|
||||
|
||||
val desiredTunnels = resolveDesiredTunnels(state).map { it.id }.toSet()
|
||||
|
||||
// stop condition overrides everything
|
||||
if (!network.hasInternet() && settings.isStopOnNoInternetEnabled) {
|
||||
return Decision.Sync(start = emptySet(), stop = activeTunnelIds)
|
||||
if (!network.hasInternet()) {
|
||||
return if (settings.isStopOnNoInternetEnabled) {
|
||||
Decision.StopDueToNoInternet
|
||||
} else {
|
||||
// keep tunnel state neutral on no internet otherwise
|
||||
Decision.None
|
||||
}
|
||||
}
|
||||
|
||||
val desiredTunnels = resolveDesiredTunnels(state).map { it.id }.toSet()
|
||||
|
||||
val toStart = desiredTunnels - activeTunnelIds
|
||||
val toStop = activeTunnelIds - desiredTunnels
|
||||
|
||||
@@ -96,5 +101,7 @@ class AutoTunnelEngine {
|
||||
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : Decision
|
||||
|
||||
data object None : Decision
|
||||
|
||||
data object StopDueToNoInternet : Decision
|
||||
}
|
||||
}
|
||||
|
||||
+79
-69
@@ -16,7 +16,6 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.TunnelActionEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.AutoTunnelSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.model.TunnelConfig
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
|
||||
@@ -26,13 +25,16 @@ import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.to
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -63,8 +65,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
private var autoTunnelJob: Job? = null
|
||||
private var permissionsJob: Job? = null
|
||||
private var overridesJob: Job? = null
|
||||
|
||||
@Volatile private var manualOverrideState = ManualOverrideState()
|
||||
private var noInternetStopJob: Job? = null
|
||||
|
||||
private data class PermissionWarningState(
|
||||
val detectionMethod: AndroidNetworkMonitor.WifiDetectionMethod,
|
||||
@@ -73,11 +74,8 @@ class AutoTunnelService : LifecycleService() {
|
||||
val ssidReadRequired: Boolean,
|
||||
)
|
||||
|
||||
private data class ManualOverrideState(
|
||||
val fingerprint: AutoTunnelState.NetworkFingerprint? = null,
|
||||
val stoppedTunnelIds: Set<Int> = emptySet(),
|
||||
val startedTunnelIds: Set<Int> = emptySet(),
|
||||
)
|
||||
@Volatile private var hasUserOverride = false
|
||||
private var lastNetworkFingerprint: AutoTunnelState.NetworkFingerprint? = null
|
||||
|
||||
private val autoTunnelStateFlow: Flow<AutoTunnelState> by lazy {
|
||||
val networkFlow = networkEngine.stableState.mapNotNull { it?.state?.toDomain() }
|
||||
@@ -121,7 +119,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
permissionsJob?.cancel()
|
||||
permissionsJob = startLocationPermissionsNotificationJob()
|
||||
overridesJob?.cancel()
|
||||
overridesJob = startOverridesJob()
|
||||
overridesJob = startUserOverrideJob()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
@@ -130,48 +128,23 @@ class AutoTunnelService : LifecycleService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cancelNoInternetStopJob()
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stateHolder.setActive(false)
|
||||
AutoTunnelTileRefresher.refresh(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun startOverridesJob(): Job =
|
||||
private fun startUserOverrideJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
tunnelCoordinator.actions.collect { action ->
|
||||
tunnelCoordinator.userOverrideFlow.collect {
|
||||
reconciliationMutex.withLock {
|
||||
manualOverrideState =
|
||||
when (action) {
|
||||
is TunnelActionEvent.Started -> {
|
||||
|
||||
if (action.source != TunnelActionSource.USER) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
manualOverrideState.copy(
|
||||
startedTunnelIds =
|
||||
manualOverrideState.startedTunnelIds + action.tunnelId,
|
||||
stoppedTunnelIds =
|
||||
manualOverrideState.stoppedTunnelIds - action.tunnelId,
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelActionEvent.Stopped -> {
|
||||
|
||||
if (action.source != TunnelActionSource.USER) {
|
||||
return@withLock
|
||||
}
|
||||
|
||||
manualOverrideState.copy(
|
||||
stoppedTunnelIds =
|
||||
manualOverrideState.stoppedTunnelIds + action.tunnelId,
|
||||
startedTunnelIds =
|
||||
manualOverrideState.startedTunnelIds - action.tunnelId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("Updated manual overrides: $manualOverrideState")
|
||||
if (!hasUserOverride) {
|
||||
Timber.d(
|
||||
"User manually overrode Auto Tunnel on current network. Pausing auto decisions."
|
||||
)
|
||||
}
|
||||
hasUserOverride = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,50 +175,83 @@ class AutoTunnelService : LifecycleService() {
|
||||
)
|
||||
}
|
||||
|
||||
// Instead of stopping tunnel right away on no internet, we kick off this job to add short delay
|
||||
// and re-evaluation to prevent unwanted stops
|
||||
// on flaky networks and network transitions
|
||||
private fun scheduleNoInternetStop() {
|
||||
noInternetStopJob?.cancel()
|
||||
|
||||
noInternetStopJob =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
delay(NO_INTERNET_GRACE_PERIOD_MS.milliseconds)
|
||||
|
||||
reconciliationMutex.withLock {
|
||||
val currentNetworkState = networkEngine.stableState.value?.state?.toDomain()
|
||||
|
||||
val stillNoInternet = currentNetworkState?.hasInternet() == false
|
||||
val stopOnNoInternetEnabled =
|
||||
autoTunnelRepository.flow.firstOrNull()?.isStopOnNoInternetEnabled == true
|
||||
|
||||
if (stillNoInternet && stopOnNoInternetEnabled) {
|
||||
val currentActiveIds =
|
||||
tunnelCoordinator.backendStatus.value.activeTunnels.keys
|
||||
|
||||
if (currentActiveIds.isNotEmpty()) {
|
||||
Timber.w(
|
||||
"No internet grace period expired and still no internet. Stopping tunnels: $currentActiveIds"
|
||||
)
|
||||
currentActiveIds.forEach { tunnelId ->
|
||||
tunnelCoordinator.stopTunnel(
|
||||
tunnelId,
|
||||
TunnelActionSource.AUTO_TUNNEL,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.d(
|
||||
"No internet grace period expired, but internet is back or setting disabled. Doing nothing."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelNoInternetStopJob() {
|
||||
noInternetStopJob?.cancel()
|
||||
noInternetStopJob = null
|
||||
}
|
||||
|
||||
private fun startAutoTunnelStateJob(): Job =
|
||||
lifecycleScope.launch(ioDispatcher) {
|
||||
autoTunnelStateFlow.collectLatest { state ->
|
||||
reconciliationMutex.withLock {
|
||||
updateFingerprintIfNeeded(state)
|
||||
|
||||
val rawEvent = engine.evaluate(state)
|
||||
|
||||
val event = applyOverrides(rawEvent)
|
||||
|
||||
Timber.d("AutoTunnel reconciliation event: $event")
|
||||
|
||||
handleAutoTunnelEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFingerprintIfNeeded(state: AutoTunnelState) {
|
||||
val fingerprint = state.networkFingerPrint
|
||||
val currentFingerprint = state.networkFingerPrint
|
||||
|
||||
if (manualOverrideState.fingerprint != fingerprint) {
|
||||
Timber.d("Network changed, clearing overrides")
|
||||
|
||||
manualOverrideState = ManualOverrideState(fingerprint = fingerprint)
|
||||
if (lastNetworkFingerprint != currentFingerprint) {
|
||||
if (hasUserOverride) {
|
||||
Timber.d("Network fingerprint changed, clearing user override")
|
||||
}
|
||||
hasUserOverride = false
|
||||
lastNetworkFingerprint = currentFingerprint
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyOverrides(event: AutoTunnelEvent): AutoTunnelEvent {
|
||||
|
||||
if (event !is AutoTunnelEvent.Sync) {
|
||||
return event
|
||||
return if (hasUserOverride) {
|
||||
AutoTunnelEvent.DoNothing
|
||||
} else {
|
||||
event
|
||||
}
|
||||
|
||||
val filteredStart =
|
||||
event.start.filterNot { it.id in manualOverrideState.stoppedTunnelIds }.toSet()
|
||||
|
||||
val filteredStop =
|
||||
event.stop.filterNot { it in manualOverrideState.startedTunnelIds }.toSet()
|
||||
|
||||
if (filteredStart.isEmpty() && filteredStop.isEmpty()) {
|
||||
return AutoTunnelEvent.DoNothing
|
||||
}
|
||||
|
||||
return event.copy(start = filteredStart, stop = filteredStop)
|
||||
}
|
||||
|
||||
private fun combineSettings():
|
||||
@@ -334,7 +340,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
private suspend fun handleAutoTunnelEvent(event: AutoTunnelEvent) {
|
||||
when (event) {
|
||||
is AutoTunnelEvent.Sync -> {
|
||||
|
||||
cancelNoInternetStopJob()
|
||||
event.stop.forEach { tunnelId ->
|
||||
Timber.d("Stopping tunnel: $tunnelId")
|
||||
tunnelCoordinator.stopTunnel(tunnelId, TunnelActionSource.AUTO_TUNNEL)
|
||||
@@ -345,8 +351,12 @@ class AutoTunnelService : LifecycleService() {
|
||||
tunnelCoordinator.startTunnel(config, TunnelActionSource.AUTO_TUNNEL)
|
||||
}
|
||||
}
|
||||
|
||||
AutoTunnelEvent.StopAllDueToNoInternet -> scheduleNoInternetStop()
|
||||
AutoTunnelEvent.DoNothing -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NO_INTERNET_GRACE_PERIOD_MS = 10_000L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,6 @@ sealed interface AutoTunnelEvent {
|
||||
data class Sync(val start: Set<TunnelConfig>, val stop: Set<Int>) : AutoTunnelEvent
|
||||
|
||||
data object DoNothing : AutoTunnelEvent
|
||||
|
||||
data object StopAllDueToNoInternet : AutoTunnelEvent
|
||||
}
|
||||
|
||||
+7
-2
@@ -4,7 +4,12 @@ import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
|
||||
|
||||
sealed interface TunnelActionEvent {
|
||||
|
||||
data class Started(val tunnelId: Int, val source: TunnelActionSource) : TunnelActionEvent
|
||||
val source: TunnelActionSource
|
||||
val tunnelId: Int
|
||||
|
||||
data class Stopped(val tunnelId: Int, val source: TunnelActionSource) : TunnelActionEvent
|
||||
data class Started(override val tunnelId: Int, override val source: TunnelActionSource) :
|
||||
TunnelActionEvent
|
||||
|
||||
data class Stopped(override val tunnelId: Int, override val source: TunnelActionSource) :
|
||||
TunnelActionEvent
|
||||
}
|
||||
|
||||
+2
-2
@@ -250,7 +250,7 @@ fun currentRouteAsNavbarState(
|
||||
val global = route !is ConfigEdit
|
||||
val tunnelName =
|
||||
if (!global) globalState.tunnelNames[route.id]
|
||||
else context.getString(R.string.configuration_globals)
|
||||
else context.getString(R.string.tunnel_configuration)
|
||||
NavbarState(
|
||||
topLeading = { TvBackButton { navController.pop() } },
|
||||
showBottomItems = true,
|
||||
@@ -297,7 +297,7 @@ fun currentRouteAsNavbarState(
|
||||
is SplitTunnelGlobal -> {
|
||||
val tunnelName =
|
||||
if (route is SplitTunnel) globalState.tunnelNames[route.id]
|
||||
else context.getString(R.string.global_split_tunneling)
|
||||
else context.getString(R.string.splt_tunneling)
|
||||
NavbarState(
|
||||
topLeading = { TvBackButton { navController.pop() } },
|
||||
topTitle = tunnelName ?: "",
|
||||
|
||||
+5
-2
@@ -302,7 +302,9 @@ fun AutoTunnelScreen(
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.PublicOff, contentDescription = null) },
|
||||
title = stringResource(R.string.stop_on_no_internet),
|
||||
description = { DescriptionText(stringResource(R.string.stop_on_internet_loss)) },
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.stop_on_no_internet_desc))
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = uiState.autoTunnelSettings.isStopOnNoInternetEnabled,
|
||||
@@ -318,7 +320,7 @@ fun AutoTunnelScreen(
|
||||
}
|
||||
Column {
|
||||
GroupLabel(
|
||||
stringResource(R.string.other),
|
||||
stringResource(R.string.automation),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
SurfaceRow(
|
||||
@@ -330,6 +332,7 @@ fun AutoTunnelScreen(
|
||||
onClick = { viewModel.setStartAtBoot(it) },
|
||||
)
|
||||
},
|
||||
description = { DescriptionText(stringResource(R.string.start_on_boot_desc)) },
|
||||
onClick = { viewModel.setStartAtBoot(!uiState.autoTunnelSettings.startOnBoot) },
|
||||
)
|
||||
}
|
||||
|
||||
+2
@@ -176,6 +176,7 @@ fun SettingsScreen(
|
||||
leading = { Icon(Icons.Outlined.Public, contentDescription = null) },
|
||||
title = stringResource(R.string.tunnel_globals),
|
||||
onClick = { navController.push(Route.TunnelGlobals) },
|
||||
description = { DescriptionText(stringResource(R.string.tunnel_globals_desc)) },
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Terminal, contentDescription = null) },
|
||||
@@ -232,6 +233,7 @@ fun SettingsScreen(
|
||||
modifier = modifier,
|
||||
)
|
||||
},
|
||||
description = { DescriptionText(stringResource(R.string.local_logging_desc)) },
|
||||
onClick = { navController.push(Route.Logs) },
|
||||
)
|
||||
SurfaceRow(
|
||||
|
||||
+2
-2
@@ -56,7 +56,7 @@ fun TunnelGlobalsScreen(
|
||||
)
|
||||
},
|
||||
enabled = sharedUiState.tunnelMode != TunnelMode.PROXY,
|
||||
title = stringResource(R.string.global_split_tunneling),
|
||||
title = stringResource(R.string.splt_tunneling),
|
||||
trailing = { modifier ->
|
||||
SwitchWithDivider(
|
||||
checked = uiState.settings.isGlobalSplitTunnelEnabled,
|
||||
@@ -82,7 +82,7 @@ fun TunnelGlobalsScreen(
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Description, contentDescription = null) },
|
||||
title = stringResource(R.string.configuration_globals),
|
||||
title = stringResource(R.string.tunnel_configuration),
|
||||
onClick = {
|
||||
uiState.globalTunnelConfig?.let {
|
||||
navController.push(Route.ConfigGlobal(id = it.id))
|
||||
|
||||
+6
-4
@@ -11,7 +11,7 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||
import androidx.compose.material.icons.filled.AppShortcut
|
||||
import androidx.compose.material.icons.filled.SmartToy
|
||||
import androidx.compose.material.icons.filled.SettingsRemote
|
||||
import androidx.compose.material.icons.outlined.AdminPanelSettings
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
@@ -133,20 +133,22 @@ fun AndroidIntegrationsScreen(viewModel: SettingsViewModel = koinViewModel()) {
|
||||
onClick = { viewModel.setShortcutsEnabled(it) },
|
||||
)
|
||||
},
|
||||
title = stringResource(R.string.enabled_app_shortcuts),
|
||||
title = stringResource(R.string.app_shortcuts),
|
||||
description = { DescriptionText(stringResource(R.string.app_shortcuts_desc)) },
|
||||
onClick = {
|
||||
viewModel.setShortcutsEnabled(!settingsState.settings.isShortcutsEnabled)
|
||||
},
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Filled.SmartToy, contentDescription = null) },
|
||||
leading = { Icon(Icons.Filled.SettingsRemote, contentDescription = null) },
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = settingsState.isRemoteEnabled,
|
||||
onClick = { viewModel.setRemoteEnabled(it) },
|
||||
)
|
||||
},
|
||||
title = stringResource(R.string.enable_remote_app_control),
|
||||
title = stringResource(R.string.remote_control),
|
||||
description = { DescriptionText(stringResource(R.string.remote_control_desc)) },
|
||||
onClick = { viewModel.setRemoteEnabled(!settingsState.isRemoteEnabled) },
|
||||
)
|
||||
AnimatedVisibility(settingsState.isRemoteEnabled) {
|
||||
|
||||
+8
@@ -2,6 +2,8 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.settings.proxy.compoents
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelMode
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.CustomBottomSheet
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.sheet.SheetOption
|
||||
@@ -27,6 +29,12 @@ fun AppModeBottomSheet(
|
||||
onDismiss()
|
||||
onAppModeChange(it)
|
||||
},
|
||||
description =
|
||||
when (it) {
|
||||
TunnelMode.VPN -> stringResource(R.string.vpn_desc)
|
||||
TunnelMode.PROXY -> stringResource(R.string.local_proxy_desc)
|
||||
TunnelMode.LOCK_DOWN -> stringResource(R.string.lockdown_desc)
|
||||
},
|
||||
selected = tunnelMode == it,
|
||||
)
|
||||
}
|
||||
|
||||
+6
-1
@@ -19,6 +19,7 @@ import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ThemedSwitch
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.text.DescriptionText
|
||||
import com.zaneschepke.wireguardautotunnel.ui.navigation.Route
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import org.koin.compose.viewmodel.koinActivityViewModel
|
||||
@@ -45,13 +46,16 @@ fun SecurityScreen(viewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
onClick = { viewModel.setScreenRecordingSecurity(it) },
|
||||
)
|
||||
},
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.screen_recording_protection_desc))
|
||||
},
|
||||
onClick = {
|
||||
viewModel.setScreenRecordingSecurity(!uiState.isScreenRecordingProtectionEnabled)
|
||||
},
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Pin, contentDescription = null) },
|
||||
title = stringResource(R.string.enable_app_lock),
|
||||
title = stringResource(R.string.app_lock),
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = uiState.pinLockEnabled,
|
||||
@@ -64,6 +68,7 @@ fun SecurityScreen(viewModel: SharedAppViewModel = koinActivityViewModel()) {
|
||||
},
|
||||
)
|
||||
},
|
||||
description = { DescriptionText(stringResource(R.string.app_lock_desc)) },
|
||||
onClick = {
|
||||
if (!uiState.pinLockEnabled) {
|
||||
navController.push(Route.Lock)
|
||||
|
||||
+16
-2
@@ -14,6 +14,8 @@ import androidx.compose.material.icons.rounded.Circle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -34,6 +36,8 @@ import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.SharedAppViewModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -46,6 +50,14 @@ fun TunnelList(
|
||||
val context = LocalContext.current
|
||||
val isTv = LocalIsAndroidTV.current
|
||||
|
||||
val now by
|
||||
produceState(System.currentTimeMillis()) {
|
||||
while (true) {
|
||||
delay(1_000L.milliseconds)
|
||||
value = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -88,7 +100,9 @@ fun TunnelList(
|
||||
}
|
||||
|
||||
val displayState =
|
||||
uiState.displayStates[tunnel.id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
remember(activeTunnel, now, uiState.displayStates[tunnel.id]) {
|
||||
uiState.displayStates[tunnel.id] ?: DisplayTunnelState.from(activeTunnel, now)
|
||||
}
|
||||
|
||||
val isRunning = uiState.backendStatus.activeTunnels.containsKey(tunnel.id)
|
||||
|
||||
@@ -107,7 +121,7 @@ fun TunnelList(
|
||||
Icon(
|
||||
Icons.Rounded.Circle,
|
||||
contentDescription = stringResource(R.string.tunnel_monitoring),
|
||||
tint = displayState.asColor(),
|
||||
tint = remember(displayState) { displayState.asColor() },
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
},
|
||||
|
||||
+27
-29
@@ -141,6 +141,10 @@ fun TunnelSettingsScreen(
|
||||
onClick = { navController.push(Route.IPv6(tunnel.id)) },
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val meteredDisabled = sharedUiState.tunnelMode == TunnelMode.PROXY
|
||||
val meteredTunnelDesc =
|
||||
if (meteredDisabled) stringResource(R.string.unavailable_in_mode)
|
||||
else stringResource(R.string.metered_tunnel_desc)
|
||||
SurfaceRow(
|
||||
leading = {
|
||||
Icon(
|
||||
@@ -153,15 +157,9 @@ fun TunnelSettingsScreen(
|
||||
},
|
||||
title = stringResource(R.string.metered_tunnel),
|
||||
enabled = sharedUiState.tunnelMode != TunnelMode.PROXY,
|
||||
description =
|
||||
if (sharedUiState.tunnelMode == TunnelMode.PROXY) {
|
||||
{
|
||||
DescriptionText(
|
||||
stringResource(R.string.unavailable_in_mode),
|
||||
disabled = true,
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
description = {
|
||||
DescriptionText(meteredTunnelDesc, disabled = meteredDisabled)
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = tunnel.isMetered,
|
||||
@@ -172,26 +170,26 @@ fun TunnelSettingsScreen(
|
||||
onClick = { viewModel.onMetered(!tunnel.isMetered) },
|
||||
)
|
||||
}
|
||||
Column {
|
||||
GroupLabel(
|
||||
stringResource(R.string.automation),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
|
||||
title = stringResource(R.string.ddns_auto_update),
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.ddns_auto_update_description))
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = tunnel.dynamicDnsEnabled,
|
||||
onClick = { viewModel.onDynamicDns(it) },
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.onDynamicDns(!tunnel.dynamicDnsEnabled) },
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
GroupLabel(
|
||||
stringResource(R.string.automation),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.Dns, contentDescription = null) },
|
||||
title = stringResource(R.string.ddns_auto_update),
|
||||
description = {
|
||||
DescriptionText(stringResource(R.string.ddns_auto_update_description))
|
||||
},
|
||||
trailing = {
|
||||
ThemedSwitch(
|
||||
checked = tunnel.dynamicDnsEnabled,
|
||||
onClick = { viewModel.onDynamicDns(it) },
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.onDynamicDns(!tunnel.dynamicDnsEnabled) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -112,7 +112,7 @@ fun ConfigEditScreen(
|
||||
leading = {
|
||||
Icon(ImageVector.vectorResource(R.drawable.host), contentDescription = null)
|
||||
},
|
||||
title = stringResource(R.string.global_dns_servers),
|
||||
title = stringResource(R.string.dns_servers),
|
||||
trailing = { modifier ->
|
||||
ThemedSwitch(
|
||||
checked = uiState.globalSettings.dnsEnabled,
|
||||
@@ -126,7 +126,7 @@ fun ConfigEditScreen(
|
||||
)
|
||||
SurfaceRow(
|
||||
leading = { Icon(Icons.Outlined.HdrAuto, contentDescription = null) },
|
||||
title = stringResource(R.string.global_amnezia_configuration),
|
||||
title = stringResource(R.string.amnezia_configuration),
|
||||
trailing = { modifier ->
|
||||
ThemedSwitch(
|
||||
checked = uiState.globalSettings.amneziaEnabled,
|
||||
|
||||
+18
-11
@@ -61,36 +61,47 @@ sealed class DisplayTunnelState {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(activeTunnel: ActiveTunnel): DisplayTunnelState {
|
||||
private const val HANDSHAKE_FAILURE_DEGRADED_THRESHOLD_MS = 6_000L
|
||||
|
||||
// During this window we avoid showing Degraded even if we see HandshakeFailure
|
||||
private const val POST_RESOLUTION_GRACE_PERIOD_MS = 3_500L
|
||||
|
||||
fun from(activeTunnel: ActiveTunnel, now: Long): DisplayTunnelState {
|
||||
val transport = activeTunnel.transportState
|
||||
val bootstrap = activeTunnel.bootstrapState
|
||||
val mode = activeTunnel.mode
|
||||
val isVpnStyle = mode is BackendMode.Vpn || mode is BackendMode.Proxy.KillSwitchPrimary
|
||||
|
||||
// Static peers bootstrap never goes to complete, treat none the same
|
||||
val bootstrapPhaseDone =
|
||||
bootstrap is BootstrapState.Complete || bootstrap is BootstrapState.None
|
||||
|
||||
// Check if we recently completed peer resolution
|
||||
val recentlyResolvedPeers =
|
||||
activeTunnel.lastPeerUpdateMs > 0 &&
|
||||
(now - activeTunnel.lastPeerUpdateMs) < POST_RESOLUTION_GRACE_PERIOD_MS
|
||||
|
||||
return when {
|
||||
transport is Tunnel.State.Down -> Disconnected
|
||||
|
||||
bootstrap is BootstrapState.Failed -> Degraded
|
||||
|
||||
// DNS resolution still in progress
|
||||
bootstrap is BootstrapState.ResolvingDns ||
|
||||
bootstrap is BootstrapState.UpdatingPeers -> ResolvingDns
|
||||
|
||||
transport is Tunnel.State.Up.Healthy -> Connected
|
||||
|
||||
transport is Tunnel.State.Up.HandshakeFailure -> {
|
||||
val age = System.currentTimeMillis() - activeTunnel.lastStateChangeMs
|
||||
val age = now - activeTunnel.lastStateChangeMs
|
||||
|
||||
if (age > 15_000L && bootstrapPhaseDone) {
|
||||
if (recentlyResolvedPeers && bootstrapPhaseDone) {
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
} else if (
|
||||
age > HANDSHAKE_FAILURE_DEGRADED_THRESHOLD_MS && bootstrapPhaseDone
|
||||
) {
|
||||
Degraded
|
||||
} else if (isVpnStyle && bootstrapPhaseDone) {
|
||||
EstablishingConnection
|
||||
} else if (bootstrapPhaseDone) {
|
||||
// For regular proxy mode, we go to ready once past bootstrap phase
|
||||
Ready
|
||||
} else {
|
||||
Connecting
|
||||
@@ -99,16 +110,12 @@ sealed class DisplayTunnelState {
|
||||
|
||||
transport is Tunnel.State.Starting -> {
|
||||
when {
|
||||
bootstrapPhaseDone -> {
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
}
|
||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
else -> Connecting
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback after bootstrap phase is done
|
||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
|
||||
else -> Connecting
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -61,7 +61,7 @@ fun TunnelMode.asTitleString(context: Context): String {
|
||||
fun TunnelMode.asString(context: Context): String {
|
||||
return when (this) {
|
||||
TunnelMode.VPN -> context.getString(R.string.vpn)
|
||||
TunnelMode.PROXY -> context.getString(R.string.proxy)
|
||||
TunnelMode.PROXY -> context.getString(R.string.local_proxy)
|
||||
TunnelMode.LOCK_DOWN -> context.getString(R.string.lockdown)
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ fun DnsError.labelRes(): Int {
|
||||
fun ActiveTunnel.statusText(context: Context): String {
|
||||
return context.getString(
|
||||
R.string.status_template,
|
||||
DisplayTunnelState.from(this).asLocalizedString(context),
|
||||
DisplayTunnelState.from(this, System.currentTimeMillis()).asLocalizedString(context),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -42,6 +42,7 @@ class ProxySettingsViewModel(
|
||||
.collect { reduce { it } }
|
||||
}
|
||||
|
||||
// TODO add a dialog requesting restart if any tunnels active
|
||||
fun save() = intent {
|
||||
reduce { state.copy(showSaveModal = false) }
|
||||
|
||||
@@ -98,9 +99,6 @@ class ProxySettingsViewModel(
|
||||
|
||||
proxySettingsRepository.upsert(updated)
|
||||
|
||||
tunnelCoordinator.toggleTunnels()
|
||||
tunnelCoordinator.toggleTunnels()
|
||||
|
||||
postSideEffect(
|
||||
GlobalSideEffect.Snackbar(StringValue.StringResource(R.string.config_changes_saved))
|
||||
)
|
||||
|
||||
+16
-3
@@ -18,6 +18,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.sideeffect.GlobalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ConfigParseException
|
||||
import com.zaneschepke.wireguardautotunnel.ui.sideeffect.LocalSideEffect
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.GlobalAppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.TunnelsUiState
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Theme
|
||||
@@ -71,10 +72,22 @@ class SharedAppViewModel(
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
tunnelCoordinator.backendStatus,
|
||||
selectedTunnelsRepository.flow,
|
||||
tunnelCoordinator.tunnelDisplayStates,
|
||||
) { tunnels, backendStatus, selectedTuns, displayStates ->
|
||||
) { tunnels, backendStatus, selectedTuns ->
|
||||
val activeTunnelIds = backendStatus.activeTunnels.keys
|
||||
|
||||
val sortedTunnels =
|
||||
tunnels.sortedWith(
|
||||
compareByDescending<TunnelConfig> { it.id in activeTunnelIds }
|
||||
.thenBy { it.position }
|
||||
)
|
||||
|
||||
val displayStates =
|
||||
backendStatus.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||
DisplayTunnelState.from(activeTunnel, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
TunnelsUiState(
|
||||
tunnels = tunnels,
|
||||
tunnels = sortedTunnels,
|
||||
backendStatus = backendStatus,
|
||||
displayStates = displayStates,
|
||||
selectedTunnels = selectedTuns,
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -27,7 +26,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -133,7 +131,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -156,7 +153,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -257,7 +253,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -272,7 +267,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
<string name="preshared_key">Předsdílený klíč</string>
|
||||
<string name="seconds">s</string>
|
||||
<string name="cancel">Zrušit</string>
|
||||
<string name="enabled_app_shortcuts">Zapnout zkratky aplikací</string>
|
||||
<string name="unknown_error">Došlo k neznámé chybě</string>
|
||||
<string name="tunnel_on_wifi">Tunelovat na Wi-Fi</string>
|
||||
<string name="email_subject">WG Tunnel podpora</string>
|
||||
@@ -65,7 +64,6 @@
|
||||
<string name="no_browser_detected">Žádný prohlížeč nebyl nalezen</string>
|
||||
<string name="pin_created">PIN úspěšně vytvořen</string>
|
||||
<string name="enter_pin">Zadejte PIN</string>
|
||||
<string name="enable_app_lock">Zapnout zámek aplikace</string>
|
||||
<string name="settings">Nastavení</string>
|
||||
<string name="support">Podpora</string>
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
@@ -90,7 +88,6 @@
|
||||
<string name="primary_tunnel">Výchozí tunel</string>
|
||||
<string name="skip">Přeskočit</string>
|
||||
<string name="donate">Přispět na projekt</string>
|
||||
<string name="stop_on_internet_loss">Zastavit tunel při ztrátě internetu</string>
|
||||
<string name="stop_on_no_internet">Zastavit při ztrátě internetu</string>
|
||||
<string name="native_kill_switch">Nativní kill switch</string>
|
||||
<string name="vpn_channel_description">Kanál pro oznámení o stavu VPN</string>
|
||||
@@ -144,7 +141,6 @@
|
||||
<string name="vpn_denied_dialog_title">Oprávnění zamítnuto</string>
|
||||
<string name="app_permission_title">Ovládání tunelů a funkcí automatického tunelování</string>
|
||||
<string name="app_permission_description">https://hosted.weblate.org/translate/wg-tunnel/strings/en/?checksum=e52d7eb2e28a9a12 Ovládání funkcí tunelu a automatického tunelování.</string>
|
||||
<string name="enable_remote_app_control">Povolit vzdálené ovládání aplikace</string>
|
||||
<string name="export_success">Export byl úspěšně dokončen</string>
|
||||
<string name="download">Stáhnout</string>
|
||||
<string name="check_for_update">Zkontrolovat aktualizaci</string>
|
||||
@@ -276,7 +272,6 @@
|
||||
<string name="back">Zpět</string>
|
||||
<string name="already_donated">Již darováno</string>
|
||||
<string name="selected">Vybrané</string>
|
||||
<string name="global_split_tunneling">Globální dělené tunelování</string>
|
||||
<string name="active_network">Aktivní síť:</string>
|
||||
<string name="delete_active_message">Aktivní tunel nelze odstranit.</string>
|
||||
<string name="help_translate">Pomozte s překladem aplikace</string>
|
||||
@@ -284,7 +279,6 @@
|
||||
<string name="other">Ostatní</string>
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Konfigurace</string>
|
||||
<string name="global_dns_servers">Globální DNS servery</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="backup_and_restore">Zálohování a obnovení</string>
|
||||
<string name="about">O aplikaci</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -29,7 +28,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -135,7 +133,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -158,7 +155,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -258,7 +254,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -273,7 +268,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<string name="base64_key">base64-Schlüssel</string>
|
||||
<string name="delete_tunnel">Tunnel löschen</string>
|
||||
<string name="persistent_keepalive">Dauerhaftes Keepalive</string>
|
||||
<string name="enable_app_lock">App-Sperre aktivieren</string>
|
||||
<string name="interface_">Schnittstelle</string>
|
||||
<string name="listen_port">Eingehender Port</string>
|
||||
<string name="random">(zufällig)</string>
|
||||
@@ -45,7 +44,6 @@
|
||||
<string name="seconds">Sekunden</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="preshared_key">Geteilter Schlüssel</string>
|
||||
<string name="enabled_app_shortcuts">App-Verknüpfungen aktivieren</string>
|
||||
<string name="tunnel_on_wifi">Tunnel bei WLAN</string>
|
||||
<string name="email_subject">WG Tunnel Unterstützung</string>
|
||||
<string name="docs_description">Dokumentation lesen</string>
|
||||
@@ -125,7 +123,6 @@
|
||||
<string name="pre_down">Vor Deaktivierung</string>
|
||||
<string name="post_down">Nach Deaktivierung</string>
|
||||
<string name="quick_actions">Schnellaktionen</string>
|
||||
<string name="stop_on_internet_loss">Stoppen, wenn die Internetverbindung getrennt wird</string>
|
||||
<string name="bypass_lan_for_kill_switch">LAN umgehen für Notschalter</string>
|
||||
<string name="hide_scripts">Skripte verbergen</string>
|
||||
<string name="enable_amnezia_compatibility">Amnezia Kompatibilität aktivieren</string>
|
||||
@@ -153,7 +150,6 @@
|
||||
<string name="copy">Kopieren</string>
|
||||
<string name="service_running_error">Dienst läuft nicht</string>
|
||||
<string name="auth_error">Nicht autorisiert</string>
|
||||
<string name="enable_remote_app_control">App-Fernsteuerung aktivieren</string>
|
||||
<string name="export_failed">Export fehlgeschlagen</string>
|
||||
<string name="app_permission_description">https://hosted.weblate.org/translate/wg-tunnel/strings/en/?checksum=e52d7eb2e28a9a12Steuere Tunnel und Auto-Tunnel Funktionen.</string>
|
||||
<string name="dns_resolve_error">DNS-Auflösung fehlgeschlagen</string>
|
||||
@@ -281,14 +277,12 @@
|
||||
<string name="resources">Resourcen</string>
|
||||
<string name="back">Zurück</string>
|
||||
<string name="already_donated">Bereits gespendet</string>
|
||||
<string name="global_split_tunneling">Globales Split-Tunneling</string>
|
||||
<string name="active_network">Aktives Netzwerk:</string>
|
||||
<string name="help_translate">Hilf mit, die App zu übersetzen</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Sonstiges</string>
|
||||
<string name="kill_switch">Notschalter</string>
|
||||
<string name="configuration">Konfiguration</string>
|
||||
<string name="global_dns_servers">Globale DNS Server</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="backup_and_restore">Sichern und Wiederherstellen</string>
|
||||
<string name="about">Über</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
<string name="optional">(opcional)</string>
|
||||
<string name="seconds">Segundos</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="enabled_app_shortcuts">Habilitar acesos directos de app</string>
|
||||
<string name="unknown_error">Error desconocido</string>
|
||||
<string name="email_subject">Ayuda de WG Tunnel</string>
|
||||
<string name="interface_">Interfaz</string>
|
||||
@@ -65,7 +64,6 @@
|
||||
<string name="pin_created">Pin creado con éxito</string>
|
||||
<string name="enter_pin">Introduce tu pin</string>
|
||||
<string name="create_pin">Crear pin</string>
|
||||
<string name="enable_app_lock">Activar el bloqueo de aplicaciones</string>
|
||||
<string name="set_primary_tunnel">Establecer como túnel Principal</string>
|
||||
<string name="edit_tunnel">Editar túnel</string>
|
||||
<string name="settings">Ajustes</string>
|
||||
@@ -121,7 +119,6 @@
|
||||
<string name="learn_more">Saber más</string>
|
||||
<string name="wildcards_active">Comodines activos</string>
|
||||
<string name="stop_on_no_internet">Detener cuando no hay internet</string>
|
||||
<string name="stop_on_internet_loss">Detener túnel cuando se pierda el internet</string>
|
||||
<string name="native_kill_switch">Interruptor de apagado nativo</string>
|
||||
<string name="allow_lan_traffic">Permitir tráfico LAN</string>
|
||||
<string name="bypass_lan_for_kill_switch">Excluir LAN del interruptor de apagado</string>
|
||||
@@ -166,7 +163,6 @@
|
||||
<string name="check_for_update">Comprobar actualización</string>
|
||||
<string name="update_check_failed">Fallo al comprobar la actualización.</string>
|
||||
<string name="flavor_template">Variante: %1$s</string>
|
||||
<string name="enable_remote_app_control">Activar control remoto de la app</string>
|
||||
<string name="export_success">Exportación completada</string>
|
||||
<string name="download">Descargar</string>
|
||||
<string name="latest_installed">Ya se está ejecutando la última versión.</string>
|
||||
@@ -280,14 +276,12 @@
|
||||
<string name="resources">Recursos</string>
|
||||
<string name="back">Atrás</string>
|
||||
<string name="already_donated">Ya he donado</string>
|
||||
<string name="global_split_tunneling">Túnel dividido global</string>
|
||||
<string name="active_network">Red activa:</string>
|
||||
<string name="help_translate">Ayuda a traducir la app</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Otros</string>
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuración</string>
|
||||
<string name="global_dns_servers">Servidores DNS globales</string>
|
||||
<string name="contact">Contacto</string>
|
||||
<string name="backup_and_restore">Copia de seguridad/Restaurar</string>
|
||||
<string name="about">Acerca de</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<string name="allow">Luba</string>
|
||||
<string name="select_all">Vali kõik</string>
|
||||
<string name="export_success">Eksportimine õnnestus</string>
|
||||
<string name="enable_remote_app_control">Luba rakenduse kaugjuhtimine</string>
|
||||
<string name="add_from_url">Lisa võrguaadressilt</string>
|
||||
<string name="enter_config_url">Sisesta seadistuse võrguaadress</string>
|
||||
<string name="error_download_failed">Seadistuse allalaadimine ei õnnestunud</string>
|
||||
@@ -151,8 +150,6 @@
|
||||
<string name="splt_tunneling">Jagatud tunneldus</string>
|
||||
<string name="stop">Peata</string>
|
||||
<string name="stop_on_no_internet">Peata internetiühenduse puudumisel</string>
|
||||
<string name="stop_on_internet_loss">Peata tunnel internetiühenduse kadumisel</string>
|
||||
<string name="enable_app_lock">Luba rakenduse lukustamine</string>
|
||||
<string name="launch_app_settings">Käivita rakenduse seadistused</string>
|
||||
<string name="use_wildcards">Kasuta nimedes metamärke</string>
|
||||
<string name="wildcards_active">Metamärgid on kasutusel</string>
|
||||
@@ -181,7 +178,6 @@
|
||||
<string name="_default">Vaikimisi meetod</string>
|
||||
<string name="wifi_detection_method">WiFi tuvastamise meetod</string>
|
||||
<string name="current_template">Praegune: %1$s</string>
|
||||
<string name="enabled_app_shortcuts">Luba rakenduse otseteed</string>
|
||||
<string name="auto_tunnel_title">Tunneli automaatne loomine</string>
|
||||
<string name="auto_tunnel_not_running">Tunneli automaatne käivitamine pole kasutusel</string>
|
||||
<string name="auto_tunnel_running">Tunneli automaatne käivitamine on kasutusel</string>
|
||||
@@ -280,14 +276,12 @@
|
||||
<string name="resources">Ressursid</string>
|
||||
<string name="back">Tagasi</string>
|
||||
<string name="already_donated">Juba annetasin</string>
|
||||
<string name="global_split_tunneling">Üldine jagatud tunneldus</string>
|
||||
<string name="active_network">Aktiivne võrk:</string>
|
||||
<string name="help_translate">Aita seda rakendust tõlkida</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Muu</string>
|
||||
<string name="kill_switch">kiirpeatamine</string>
|
||||
<string name="configuration">Seadistused</string>
|
||||
<string name="global_dns_servers">Üldised nimeserverid</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="backup_and_restore">Varundus ja taastamine</string>
|
||||
<string name="about">Rakenduse teave</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<string name="save">Save</string>
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -39,7 +38,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -143,7 +141,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -165,7 +162,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -259,7 +255,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -274,7 +269,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<string name="pin_created">Pin luotu</string>
|
||||
<string name="enter_pin">Syötä pin-koodi</string>
|
||||
<string name="create_pin">Luo pin-koodi</string>
|
||||
<string name="enable_app_lock">Ota käyttöön sovelluksen lukitus</string>
|
||||
<string name="set_primary_tunnel">Aseta ensisijaiseksi tunneliksi</string>
|
||||
<string name="edit_tunnel">Muokkaa tunnelia</string>
|
||||
<string name="settings">Asetukset</string>
|
||||
@@ -87,7 +86,6 @@
|
||||
<string name="add_wifi_name">Lisää WIFI:n nimi</string>
|
||||
<string name="language">Kieli</string>
|
||||
<string name="include">Sisällytä</string>
|
||||
<string name="enabled_app_shortcuts">Salli sovelluksen pikakuvakkeet</string>
|
||||
<string name="automatic">Automaattinen</string>
|
||||
<string name="tunnel_name">Tunnelin nimi</string>
|
||||
<string name="restart_at_boot">Käynnistä laitteen käynnistyksen yhteydessä</string>
|
||||
@@ -114,7 +112,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="google_donation_message">Unfortunately, due to Google\'s policies, donation links are not allowed in the Play Store version of this app. Please browse the project\'s webpages to find where to donate.</string>
|
||||
@@ -187,7 +184,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -203,7 +199,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
<string name="join_matrix">Join Matrix community</string>
|
||||
@@ -282,7 +277,6 @@
|
||||
<string name="fix">Fix</string>
|
||||
<string name="tunnel_running_name_message">Name unchangeable while tunnel is active.</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
<string name="show_amnezia_properties">Voir les propriétés d\'Amnezia</string>
|
||||
<string name="never">Jamais</string>
|
||||
<string name="logs">Journaux</string>
|
||||
<string name="enabled_app_shortcuts">Activer les raccourcis</string>
|
||||
<string name="unknown_error">Une erreur inconnue s\'est produite</string>
|
||||
<string name="email_subject">Assistance WG Tunnel</string>
|
||||
<string name="yes">Oui</string>
|
||||
@@ -79,7 +78,6 @@
|
||||
<string name="set_primary_tunnel">Tunnel utilisé quand aucun tunnel favori n\'est défini</string>
|
||||
<string name="auto">(Auto)</string>
|
||||
<string name="pin_created">Code PIN bien créé</string>
|
||||
<string name="enable_app_lock">Activer le verrouillage de l\'appli</string>
|
||||
<string name="edit_tunnel">Éditer le tunnel</string>
|
||||
<string name="settings">Réglages</string>
|
||||
<string name="junk_packet_count">Nombre de paquets indésirables</string>
|
||||
@@ -118,7 +116,6 @@
|
||||
<string name="remove_amnezia_compatibility">Retirer la prise en charge d\'Amnezia</string>
|
||||
<string name="hide_amnezia_properties">Cacher les propriétés d\'Amnezia</string>
|
||||
<string name="stop_on_no_internet">Arrêt en l\'absence d\'internet</string>
|
||||
<string name="stop_on_internet_loss">Couper les tunnels en l\'absence d\'internet</string>
|
||||
<string name="native_kill_switch">Arrêt d\'urgence natif</string>
|
||||
<string name="allow_lan_traffic">Autoriser le trafic LAN</string>
|
||||
<string name="bypass_lan_for_kill_switch">Contourner le LAN en cas d\'arrêt d\'urgence</string>
|
||||
@@ -213,7 +210,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Avec toute ma gratitude,</string>
|
||||
<string name="selected">Sélectionné</string>
|
||||
<string name="global_split_tunneling">Tunneling sélectif global</string>
|
||||
<string name="active_network">Réseau actif:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Impossible de supprimer un tunnel actif.</string>
|
||||
@@ -227,7 +223,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Poignée de glissement</string>
|
||||
<string name="global_dns_servers">Serveurs DNS global</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Échec. Les serveurs proxy doivent utiliser des ports différents.</string>
|
||||
<string name="backup_and_restore">Sauvegarde et restauration</string>
|
||||
@@ -287,7 +282,6 @@
|
||||
<string name="fix">Corriger</string>
|
||||
<string name="tunnel_running_name_message">Le nom ne peut pas être modifié tant que le tunnel est actif.</string>
|
||||
<string name="export_failed">L\'exportation a échoué</string>
|
||||
<string name="enable_remote_app_control">Activer le contrôle à distance des applications</string>
|
||||
<string name="donation_closing">Mon rêve serait de travailler à plein temps pour vous sur ce projet.</string>
|
||||
<string name="update_download_failed">La mise à jour n\'a pas pu être téléchargée.</string>
|
||||
<string name="network_name">Réseau:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -27,7 +26,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -134,7 +132,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -157,7 +154,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -259,7 +255,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -274,7 +269,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -390,7 +384,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -432,7 +425,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -466,7 +458,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="delete_tunnel">Alagút törlése</string>
|
||||
<string name="tunnel_mobile_data">Alagút mobiladat-forgalmon</string>
|
||||
<string name="logs">Naplók</string>
|
||||
<string name="enable_app_lock">Alkalmazászár engedélyezése</string>
|
||||
<string name="config_changes_saved">Konfigurációs módosítások mentve.</string>
|
||||
<string name="join_telegram">Csatlakozás a Telegram közösséghez</string>
|
||||
<string name="pin_created">PIN-kód sikeresen létrehozva</string>
|
||||
@@ -29,7 +28,6 @@
|
||||
<string name="remote_key">Távoli kulcs</string>
|
||||
<string name="mobile_data">Mobiladat</string>
|
||||
<string name="use_shell_via_shizuku">Shell használata Shizuku-n keresztül a Wi-Fi információkhoz, így nincs szükség helymeghatározási engedélyre nem rootolt eszközökön</string>
|
||||
<string name="stop_on_internet_loss">Alagút leállítása az internet megszűnésekor</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Alapértelmezett alagút indítása rendszerindításkor</string>
|
||||
<string name="allow_lan_traffic">LAN forgalom engedélyezése</string>
|
||||
@@ -135,7 +133,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Hálával,</string>
|
||||
<string name="selected">Kiválasztva</string>
|
||||
<string name="global_split_tunneling">Globális split tunneling</string>
|
||||
<string name="active_network">Aktív hálózat:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Aktív alagút nem törölhető.</string>
|
||||
@@ -158,7 +155,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Konfiguráció</string>
|
||||
<string name="drag_handle">Húzóka</string>
|
||||
<string name="global_dns_servers">Globális DNS szerverek</string>
|
||||
<string name="display_theme">Megjelenítési téma</string>
|
||||
<string name="contact">Kapcsolat</string>
|
||||
<string name="ports_must_differ">Sikertelen. A proxyknak különböző portokat kell használniuk.</string>
|
||||
@@ -258,7 +254,6 @@
|
||||
<string name="unavailable_in_mode">Nem érhető el a jelenlegi módban</string>
|
||||
<string name="server_port">Szerver:Port</string>
|
||||
<string name="camera_permission_required">Kamera engedély szükséges</string>
|
||||
<string name="enabled_app_shortcuts">App gyorsindítók engedélyezése</string>
|
||||
<string name="preferred_tunnel">Preferált alagút</string>
|
||||
<string name="allow">Engedélyezés</string>
|
||||
<string name="underload_packet_magic_header">Underload csomag magic header</string>
|
||||
@@ -273,7 +268,6 @@
|
||||
<string name="settings">Beállítások</string>
|
||||
<string name="incorrect_pin">A PIN-kód helytelen</string>
|
||||
<string name="export_failed">Exportálás sikertelen</string>
|
||||
<string name="enable_remote_app_control">Távoli vezérlés engedélyezése</string>
|
||||
<string name="donation_closing">Minden álmom, hogy teljes munkaidőben ezen a projekten dolgozhassak.</string>
|
||||
<string name="update_download_failed">Frissítés letöltése sikertelen.</string>
|
||||
<string name="network_name">Hálózat:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<string name="seconds">Detik</string>
|
||||
<string name="persistent_keepalive">Keepalive persisten</string>
|
||||
<string name="cancel">Batal</string>
|
||||
<string name="enabled_app_shortcuts">Aktifkan pintasan aplikasi</string>
|
||||
<string name="unknown_error">Terjadi kesalahan tidak dikenal</string>
|
||||
<string name="tunnel_on_wifi">Terowongan pada Wi-Fi</string>
|
||||
<string name="email_subject">Dukungan WG Tunnel</string>
|
||||
@@ -64,7 +63,6 @@
|
||||
<string name="prominent_background_location_message">Fitur ini memerlukan izin lokasi latar belakang untuk mengaktifkan pemantauan SSID Wi-Fi bahkan saat aplikasi ditutup. Untuk detail selengkapnya, silakan lihat Kebijakan Privasi yang tertaut di layar Dukungan.</string>
|
||||
<string name="copy_public_key">Salin kunci publik</string>
|
||||
<string name="base64_key">Kunci Base64</string>
|
||||
<string name="enable_app_lock">Aktifkan kunci aplikasi</string>
|
||||
<string name="delete_tunnel">Hapus terowongan</string>
|
||||
<string name="delete_tunnel_message">Apakah Anda yakin ingin menghapus terowongan yang dipilih?</string>
|
||||
<string name="no_email_detected">Tidak ada aplikasi email yang terdeteksi</string>
|
||||
@@ -93,7 +91,6 @@
|
||||
<string name="remote_key">Kunci jarak jauh</string>
|
||||
<string name="mobile_data">Data seluler</string>
|
||||
<string name="use_shell_via_shizuku">Gunakan shell melalui Shizuku untuk mendapatkan informasi Wi-Fi, sehingga tidak memerlukan izin lokasi pada perangkat yang tidak di-root</string>
|
||||
<string name="stop_on_internet_loss">Hentikan terowongan saat koneksi internet terputus</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Mulai terowongan bawaan saat booting</string>
|
||||
<string name="allow_lan_traffic">Izinkan lalu lintas LAN</string>
|
||||
@@ -174,7 +171,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Dengan penuh rasa syukur,</string>
|
||||
<string name="selected">Dipilih</string>
|
||||
<string name="global_split_tunneling">Terowongan terpisah global</string>
|
||||
<string name="active_network">Jaringan Aktif:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Tidak dapat menghapus terowongan yang sedang aktif.</string>
|
||||
@@ -192,7 +188,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Konfigurasi</string>
|
||||
<string name="drag_handle">Pegangan Seret</string>
|
||||
<string name="global_dns_servers">Server DNS global</string>
|
||||
<string name="display_theme">Tema tampilan</string>
|
||||
<string name="contact">Kontak</string>
|
||||
<string name="ports_must_differ">Gagal. Proksi harus memiliki port yang berbeda.</string>
|
||||
@@ -279,7 +274,6 @@
|
||||
<string name="fix">Perbaiki</string>
|
||||
<string name="tunnel_running_name_message">Nama tidak dapat diubah saat terowongan aktif.</string>
|
||||
<string name="export_failed">Ekspor gagal</string>
|
||||
<string name="enable_remote_app_control">Aktifkan kontrol aplikasi jarak jauh</string>
|
||||
<string name="donation_closing">Impian saya adalah bekerja penuh waktu untuk Anda pada proyek ini.</string>
|
||||
<string name="update_download_failed">Unduhan pembaruan gagal.</string>
|
||||
<string name="network_name">Jaringan:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<string name="public_key">Chiave pubblica</string>
|
||||
<string name="addresses">Indirizzi</string>
|
||||
<string name="dns_servers">Server DNS</string>
|
||||
<string name="enabled_app_shortcuts">Abilita le scorciatoie da app</string>
|
||||
<string name="email_subject">Supporto di Tunnel WG</string>
|
||||
<string name="email_chooser">Invia una email…</string>
|
||||
<string name="docs_description">Leggi la documentazione</string>
|
||||
@@ -64,7 +63,6 @@
|
||||
<string name="no_browser_detected">Nessun browser rilevato</string>
|
||||
<string name="incorrect_pin">Il PIN non è corretto</string>
|
||||
<string name="pin_created">PIN correttamente creato</string>
|
||||
<string name="enable_app_lock">Abilita blocco app</string>
|
||||
<string name="set_primary_tunnel">Tunnel utilizzato quando non è configurato alcun tunnel preferito</string>
|
||||
<string name="edit_tunnel">Modifica tunnel</string>
|
||||
<string name="settings">Impostazioni</string>
|
||||
@@ -88,7 +86,6 @@
|
||||
<string name="prominent_background_location_message">Questa caratteristica richiede il permesso di localizzazione in background per abilitare il monitoraggio dell\'SSID delle reti Wi-fi anche quando l\'applicazione è chiusa. Per maggiori dettagli, consultare il link alla Privacy Policy presente nella schermata \"Supporto\".</string>
|
||||
<string name="auto_tunnel_title">Servizio tunneling automatico</string>
|
||||
<string name="use_root_shell_for_wifi">Utilizzare una shell root per ottenere le informazioni Wi-Fi, evitando la necessità dell\'autorizzazioni della posizione</string>
|
||||
<string name="stop_on_internet_loss">Arresta il tunnel in caso di assenza di connessione internet</string>
|
||||
<string name="allow_lan_traffic">Abilita traffico LAN</string>
|
||||
<string name="tunnel_control">Controllo tunnel</string>
|
||||
<string name="include_lan">Includi la LAN</string>
|
||||
@@ -158,7 +155,6 @@
|
||||
<string name="service_running_error">Il servizio non è in esecuzione</string>
|
||||
<string name="config_error">Configurazione non valida</string>
|
||||
<string name="dns_resolve_error">Risoluzione DNS fallita</string>
|
||||
<string name="enable_remote_app_control">Abilita il controllo dell\'app da remoto</string>
|
||||
<string name="version_template">Versione: %1$s</string>
|
||||
<string name="flavor_template">Caratteristica: %1$s</string>
|
||||
<string name="update_available">Aggiornamento disponibile!</string>
|
||||
@@ -272,7 +268,6 @@
|
||||
<string name="back">Indietro</string>
|
||||
<string name="active_tunnel_update_failed">Aggiornamento tunnel attivo non riuscito</string>
|
||||
<string name="already_donated">Già donato</string>
|
||||
<string name="global_split_tunneling">Tunneling diviso globale</string>
|
||||
<string name="active_network">Rete attiva:</string>
|
||||
<string name="help_translate">Aiuta a tradurre l\'app</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
@@ -280,7 +275,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configurazione</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Server DNS globali</string>
|
||||
<string name="contact">Contatti</string>
|
||||
<string name="backup_and_restore">Backup e ripristino</string>
|
||||
<string name="about">Informazioni</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
<string name="cancel">キャンセル</string>
|
||||
<string name="unknown_error">不明なエラーが発生しました</string>
|
||||
<string name="error_no_file_explorer">ファイルエクスプローラーはインストールされていません</string>
|
||||
<string name="enabled_app_shortcuts">アプリのショートカットを有効にする</string>
|
||||
<string name="no_email_detected">メールアプリは検出されません</string>
|
||||
<string name="email_description">メールを送る</string>
|
||||
<string name="no_browser_detected">ブラウザは検出されません</string>
|
||||
@@ -55,7 +54,6 @@
|
||||
<string name="auto_tunnel_title">自動トンネルサービス</string>
|
||||
<string name="edit_tunnel">トンネルの編集</string>
|
||||
<string name="create_pin">新規PINを作成</string>
|
||||
<string name="enable_app_lock">アプリロックを有効にする</string>
|
||||
<string name="always_on_message">VPN接続の許可が拒否されました。</string>
|
||||
<string name="always_on_message2">他のすべてのアプリで常時接続VPNがオフになっていることを確認して、再度お試しください</string>
|
||||
<string name="notifications">通知</string>
|
||||
@@ -83,7 +81,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -167,7 +164,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -184,7 +180,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -277,7 +272,6 @@
|
||||
<string name="fix">Fix</string>
|
||||
<string name="tunnel_running_name_message">Name unchangeable while tunnel is active.</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
<string name="seconds">წამები</string>
|
||||
<string name="persistent_keepalive">მუდმივი keepalive</string>
|
||||
<string name="cancel">უარყოფა</string>
|
||||
<string name="enabled_app_shortcuts">გააქტიურე აპლიკაციის მალსახმობები</string>
|
||||
<string name="unknown_error">დაფიქსირდა უცნობი შეცდომა</string>
|
||||
<string name="tunnel_on_wifi">ტუნელის გამოტენება Wi-Fi-ს მეშვეობით</string>
|
||||
<string name="email_subject">ვგ ტუნელის დახმარება</string>
|
||||
@@ -61,7 +60,6 @@
|
||||
<string name="pin_created">პინი წარმატებით შეიქმნა</string>
|
||||
<string name="enter_pin">შეიყვანეთ პინი</string>
|
||||
<string name="create_pin">შექმენით პინი</string>
|
||||
<string name="enable_app_lock">გააქტიურე აპლიკაციის დაბლოკვა</string>
|
||||
<string name="restart_at_boot">ჩართვისას დაწყება</string>
|
||||
<string name="vpn_denied_dialog_title">უფლება უარყოფილია</string>
|
||||
<string name="auto_tunnel_channel_id" translatable="false">Auto-tunnel Channel</string>
|
||||
@@ -141,7 +139,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -218,7 +215,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -236,7 +232,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -319,7 +314,6 @@
|
||||
<string name="tunnel_running_name_message">Name unchangeable while tunnel is active.</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<string name="delete_tunnel">터널 삭제</string>
|
||||
<string name="tunnel_mobile_data">모바일 데이터에서 터널 사용</string>
|
||||
<string name="logs">로그</string>
|
||||
<string name="enable_app_lock">앱 잠금 켜기</string>
|
||||
<string name="config_changes_saved">변경한 설정이 저장되었습니다.</string>
|
||||
<string name="join_telegram">Telegram 커뮤니티 참여</string>
|
||||
<string name="pin_created">Pin 생성 성공</string>
|
||||
@@ -27,7 +26,6 @@
|
||||
<string name="remote_key">원격 키</string>
|
||||
<string name="mobile_data">모바일 데이터</string>
|
||||
<string name="use_shell_via_shizuku">Shizuku 셸을 이용하여, 루팅하지 않은 기기에서 위치 권한을 요구하지 않고 Wi-Fi 정보를 얻습니다</string>
|
||||
<string name="stop_on_internet_loss">인터넷이 끊겼을 때 터널 중지</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">부팅 시 기본 터널 시작</string>
|
||||
<string name="allow_lan_traffic">LAN 트래픽 허용</string>
|
||||
@@ -134,7 +132,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">감사를 담아,</string>
|
||||
<string name="selected">선택함</string>
|
||||
<string name="global_split_tunneling">글로벌 분할 터널링</string>
|
||||
<string name="active_network">활성 네트워크:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">활성화된 터널은 삭제할 수 없습니다.</string>
|
||||
@@ -157,7 +154,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">설정</string>
|
||||
<string name="drag_handle">핸들 드래그</string>
|
||||
<string name="global_dns_servers">글로벌 DNS 서버</string>
|
||||
<string name="display_theme">표시 테마</string>
|
||||
<string name="contact">연락</string>
|
||||
<string name="ports_must_differ">실패. 프록시는 다른 포트를 이용해야 합니다.</string>
|
||||
@@ -259,7 +255,6 @@
|
||||
<string name="unavailable_in_mode">현재 모드에서는 이용 불가</string>
|
||||
<string name="server_port">서버:포트</string>
|
||||
<string name="camera_permission_required">카메라 권한 필요</string>
|
||||
<string name="enabled_app_shortcuts">앱 바로가기 켜기</string>
|
||||
<string name="preferred_tunnel">선호 터널</string>
|
||||
<string name="allow">허용</string>
|
||||
<string name="underload_packet_magic_header">언더로드 패킷 매직 헤더</string>
|
||||
@@ -274,7 +269,6 @@
|
||||
<string name="settings">설정</string>
|
||||
<string name="incorrect_pin">Pin이 잘못되었습니다</string>
|
||||
<string name="export_failed">내보내기 실패</string>
|
||||
<string name="enable_remote_app_control">원격 앱 제어 켜기</string>
|
||||
<string name="donation_closing">프로젝트에 모든 시간을 할애하는 것이 제 꿈입니다.</string>
|
||||
<string name="update_download_failed">업데이트 다운로드에 실패했습니다.</string>
|
||||
<string name="network_name">네트워크:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -29,7 +28,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -135,7 +133,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -158,7 +155,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -258,7 +254,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -273,7 +268,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="name">Naam</string>
|
||||
<string name="seconds">Seconden</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="enabled_app_shortcuts">App snelkoppelingen inschakelen</string>
|
||||
<string name="random">(willekeurig)</string>
|
||||
<string name="thank_you">Bedankt voor het gebruiken van WG Tunnel!</string>
|
||||
<string name="trusted_ssid_value_description">Verstuur SSID</string>
|
||||
@@ -44,7 +43,6 @@
|
||||
<string name="auto_tunnel_title">Auto-tunnel service</string>
|
||||
<string name="open_issue">Open een melding</string>
|
||||
<string name="create_pin">Maak PIN</string>
|
||||
<string name="enable_app_lock">Schakel app-lock in</string>
|
||||
<string name="init_packet_junk_size">Initiële junk packet grootte</string>
|
||||
<string name="junk_packet_maximum_size">Junk packet maximum grootte</string>
|
||||
<string name="response_packet_junk_size">Response junk packet grootte</string>
|
||||
@@ -110,7 +108,6 @@
|
||||
<string name="enable_amnezia_compatibility">Amnezia compatibiliteit inschakelen</string>
|
||||
<string name="add_from_url">Toevoegen met link</string>
|
||||
<string name="stop_on_no_internet">Stoppen wanneer geen internet</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel bij verlies van internet</string>
|
||||
<string name="wildcards_active">Wildcards actief</string>
|
||||
<string name="use_root_shell_for_wifi">Gebruik root shell om wifi naam te bepalen, zodat locatie toestemmingen niet nodig zijn</string>
|
||||
<string name="start_auto">Start auto-tunnel</string>
|
||||
@@ -154,7 +151,6 @@
|
||||
<string name="auto_tunnel_not_running">Auto-tunnel is niet actief</string>
|
||||
<string name="auth_error">Niet toegelaten</string>
|
||||
<string name="service_running_error">Service niet actief</string>
|
||||
<string name="enable_remote_app_control">Activeer applicatie controle vanop afstand</string>
|
||||
<string name="select_all">Selecteer alles</string>
|
||||
<string name="export_success">Export gelukt</string>
|
||||
<string name="download">Download</string>
|
||||
@@ -251,7 +247,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Hartelijk bedankt,</string>
|
||||
<string name="selected">Geselecteerd</string>
|
||||
<string name="global_split_tunneling">Globale split tunneling</string>
|
||||
<string name="active_network">Actief Netwerk:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Actieve tunnel kan niet worden verwijderd.</string>
|
||||
@@ -262,7 +257,6 @@
|
||||
<string name="new_tunnel">Nieuwe tunnel</string>
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuratie</string>
|
||||
<string name="global_dns_servers">Globale DNS servers</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Mislukt. Proxyservers moeten verschillende poorten hebben.</string>
|
||||
<string name="backup_and_restore">Backup en herstellen</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -27,7 +26,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -134,7 +132,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -157,7 +154,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -259,7 +255,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -274,7 +269,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -390,7 +384,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -432,7 +425,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -466,7 +458,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -11,13 +11,11 @@
|
||||
<string name="vpn_on">Włącz VPN</string>
|
||||
<string name="vpn_off">Wyłącz VPN</string>
|
||||
<string name="interface_">Interfejs</string>
|
||||
<string name="enabled_app_shortcuts">Włącz skróty aplikacji</string>
|
||||
<string name="privacy_policy">Polityka prywatności</string>
|
||||
<string name="tunnel_mobile_data">Tunel przez mobilną transmisję danych</string>
|
||||
<string name="random">(losowy)</string>
|
||||
<string name="pin_created">Kod PIN został pomyślnie utworzony</string>
|
||||
<string name="enter_pin">Podaj kod PIN</string>
|
||||
<string name="enable_app_lock">Włącz blokadę aplikacji</string>
|
||||
<string name="response_packet_junk_size">Rozmiar śmieciowego pakietu odpowiedzi</string>
|
||||
<string name="response_packet_magic_header">Nagłówek magicznego pakietu odpowiedzi</string>
|
||||
<string name="transport_packet_magic_header">Nagłówek magicznego pakietu transportowego</string>
|
||||
@@ -57,7 +55,7 @@
|
||||
<string name="wildcards_active">Symbole wieloznaczne aktywne</string>
|
||||
<string name="create_pin">Utwórz kod PIN</string>
|
||||
<string name="junk_packet_maximum_size">Maksymalny rozmiar pakietu śmieciowego</string>
|
||||
<string name="local_logging">Lokalne rejestrowanie</string>
|
||||
<string name="local_logging">Monitor lokalnych dzienników</string>
|
||||
<string name="monitoring_state_changes">Monitorowanie zmian stanu</string>
|
||||
<string name="add_tunnels_text">Dodaj z pliku lub archiwum ZIP</string>
|
||||
<string name="unknown_error">Wystąpił nieznany błąd</string>
|
||||
@@ -114,11 +112,10 @@
|
||||
<string name="set_primary_tunnel">Tunel używany, gdy nie skonfigurowano preferowanego tunelu</string>
|
||||
<string name="vpn_channel_id" translatable="false">VPN Channel</string>
|
||||
<string name="stop_on_no_internet">Zatrzymaj, gdy nie ma Internetu</string>
|
||||
<string name="stop_on_internet_loss">Zatrzymaj tunel przy utracie Internetu</string>
|
||||
<string name="allow_lan_traffic">Zezwól na ruch LAN</string>
|
||||
<string name="bypass_lan_for_kill_switch">Omiń LAN dla wyłącznika awaryjnego</string>
|
||||
<string name="vpn_channel_description">Powiadomienia dotyczące tuneli VPN.</string>
|
||||
<string name="auto_tunnel_channel_description">Powiadomienia o zdarzeniach autotunelowania.</string>
|
||||
<string name="vpn_channel_description">Kanał powiadomień o stanie VPN</string>
|
||||
<string name="auto_tunnel_channel_description">Kanał powiadomień o stanie autotunelowania</string>
|
||||
<string name="stop">Zatrzymaj</string>
|
||||
<string name="splt_tunneling">Tunelowanie dzielone</string>
|
||||
<string name="pre_up">Przed aktywacją</string>
|
||||
@@ -143,7 +140,7 @@
|
||||
<string name="join_telegram">Dołącz do społeczności Telegramu</string>
|
||||
<string name="error_download_failed">Nie udało się pobrać konfiguracji</string>
|
||||
<string name="service_running_error">Usługa nie działa</string>
|
||||
<string name="app_permission_title">Sterowanie tunelem i funkcje autotunelowania.</string>
|
||||
<string name="app_permission_title">Mostek sterujący WG Tunnel</string>
|
||||
<string name="enter_config_url">Wpisz adres URL konfiguracji</string>
|
||||
<string name="save">Zapisz</string>
|
||||
<string name="search">Szukaj</string>
|
||||
@@ -156,7 +153,6 @@
|
||||
<string name="config_error">Nieprawidłowa konfiguracja</string>
|
||||
<string name="dropdown">Rozwijane</string>
|
||||
<string name="auth_error">Brak autoryzacji</string>
|
||||
<string name="enable_remote_app_control">Włącz zdalne sterowanie aplikacją</string>
|
||||
<string name="add_tunnel">Dodaj tunel</string>
|
||||
<string name="select">Wybierz</string>
|
||||
<string name="dns_resolve_error">Rozwiązywanie DNS się nie powiodło</string>
|
||||
@@ -242,14 +238,14 @@
|
||||
<string name="read_failed">Nie udało się odczytać danych.</string>
|
||||
<string name="config_error_template">Błędna konfiguracja. %1$s w lokalizacji: %2$s.</string>
|
||||
<string name="ports_must_differ">Nie udało się. Serwery proxy muszą mieć różne porty.</string>
|
||||
<string name="password_no_spaces">Hasło nie może zawierać spacji</string>
|
||||
<string name="tunnel_name_taken">Nazwa tunelu jest już używana</string>
|
||||
<string name="password_no_spaces">Hasło nie może zawierać spacji.</string>
|
||||
<string name="tunnel_name_taken">Nazwa tunelu jest już używana.</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="mimic_quic">Imituj QUIC</string>
|
||||
<string name="mimic_dns">Imituj DNS</string>
|
||||
<string name="mimic_sip">Imituj SIP</string>
|
||||
<string name="ddns_auto_update">Dynamiczny DNS (DDNS)</string>
|
||||
<string name="ddns_auto_update_description">Ponownie rozwiązuje nazwę hosta serwera i aktualizuje punkty końcowe równorzędne po zmianach DDNS</string>
|
||||
<string name="ddns_auto_update">Automatyczna aktualizacja dynamicznego DNS</string>
|
||||
<string name="ddns_auto_update_description">Automatyczna aktualizacja adresu IP po zmianach DDNS</string>
|
||||
<string name="mode_disabled_template">Funkcja niedostępna w trybie %1$s.</string>
|
||||
<string name="lockdown">Zablokowane</string>
|
||||
<string name="active_tunnel_update_failed">Aktualizacja aktywnego tunelu się nie powiodła</string>
|
||||
@@ -302,8 +298,6 @@
|
||||
<string name="metered_tunnel">Tunel taryfowy</string>
|
||||
<string name="lockdown_settings">Ustawienia blokady</string>
|
||||
<string name="unavailable_in_mode">Niedostępne w obecnym trybie</string>
|
||||
<string name="global_split_tunneling">Globalne tunelowanie dzielone</string>
|
||||
<string name="global_dns_servers">Globalne serwery DNS</string>
|
||||
<string name="dual_stack">Sieć dual-stack</string>
|
||||
<string name="dual_stack_description">Tunele muszą obsługiwać protokoły IPv4 i IPv6</string>
|
||||
<string name="save_changes">Zapisz zmiany</string>
|
||||
@@ -358,93 +352,91 @@
|
||||
<string name="github_sponsors_url" translatable="false">https://github.com/sponsors/zaneschepke</string>
|
||||
<string name="transport_packet_junk_size">Rozmiar śmieciowego pakietu transportu</string>
|
||||
<string name="cookie_packet_junk_size">Rozmiar śmieciowego pakietu ciasteczka</string>
|
||||
<string name="fallback_to_ipv4">Awaryjny powrót do IPv4</string>
|
||||
<string name="excluded_apps">Wykluczone aplikacje: %1$s</string>
|
||||
<string name="resolution_method">Metoda rozwiązania</string>
|
||||
<string name="notification_ipv6_recovery_message">%1$s odzyskał łączność IPv6</string>
|
||||
<string name="tunnel_state_resolving_dns">Rozwiązywanie DNS</string>
|
||||
<string name="handshake_template">wymiana potwierdzeń: %1$s</string>
|
||||
<string name="fallback_to_ipv4">Fallback to IPv4</string>
|
||||
<string name="excluded_apps">%1$s apps excluded</string>
|
||||
<string name="resolution_method">Resolution method</string>
|
||||
<string name="notification_ipv6_recovery_message">%1$s recovered IPv6 connectivity</string>
|
||||
<string name="tunnel_state_resolving_dns">Resolving DNS</string>
|
||||
<string name="handshake_template">handshake: %1$s</string>
|
||||
<string name="peer_template">peer: %1$s</string>
|
||||
<string name="errors">Błędy</string>
|
||||
<string name="no_system_dns_information">Brak informacji o systemie DNS</string>
|
||||
<string name="export_unsupported">Eksport nie jest obsługiwany na tym urządzeniu</string>
|
||||
<string name="tunnel_globals">Globalne zmienne tunelu</string>
|
||||
<string name="sort_by_latency">Sortuj według opóźnienia</string>
|
||||
<string name="ready">Gotowy</string>
|
||||
<string name="no_system_dns_detected">Nie wykryto systemu DNS</string>
|
||||
<string name="name_error_empty">Nazwa tunelu nie może być pusta</string>
|
||||
<string name="notification_ipv4_fallback_message">%1$s przełączył się na łączność IPv4</string>
|
||||
<string name="errors">Errors</string>
|
||||
<string name="no_system_dns_information">No system DNS information</string>
|
||||
<string name="export_unsupported">Export is not supported on this device</string>
|
||||
<string name="tunnel_globals">Tunnel globals</string>
|
||||
<string name="sort_by_latency">Sort by latency</string>
|
||||
<string name="ready">Ready</string>
|
||||
<string name="no_system_dns_detected">No system DNS detected</string>
|
||||
<string name="name_error_empty">Tunnel name cannot be empty</string>
|
||||
<string name="notification_ipv4_fallback_message">%1$s switched to IPv4 connectivity</string>
|
||||
<string name="notification_tunnel_status_format">%1$s • %2$s</string>
|
||||
<string name="dns_error_invalid_ip_or_host">Nieprawidłowy adres IP lub nazwa hosta</string>
|
||||
<string name="proxy_channel_description">Powiadomienia dotyczące tuneli proxy.</string>
|
||||
<string name="system_dns_servers">Serwery: %1$s</string>
|
||||
<string name="balanced">Zrównoważona (3 s)</string>
|
||||
<string name="import_url_description">Adres URL musi być bezpieczny i obsługiwać plik .conf.</string>
|
||||
<string name="dns_error_invalid_port">Port musi być pomiędzy 1 a 65535</string>
|
||||
<string name="view_configuration">Wyświetl konfigurację</string>
|
||||
<string name="dot">DNS poprzez TLS (DoT)</string>
|
||||
<string name="ipv6_recovery">Odzyskiwanie IPv6</string>
|
||||
<string name="screen_recording_protection">Ochrona przed nagrywaniem ekranu</string>
|
||||
<string name="dns_error_invalid_host">Host nie może być pusty</string>
|
||||
<string name="toggle_sensitive_data_visibility">Przełącz widoczność poufnych danych</string>
|
||||
<string name="ipv4_fallback">Zapasowy IPv4</string>
|
||||
<string name="dns_error_empty">Punkt końcowy nie może być pusty</string>
|
||||
<string name="tunnel_state_connected">Połączono</string>
|
||||
<string name="dns_error_invalid_scheme">DoH musi używać HTTPS</string>
|
||||
<string name="configuration_globals">Globalne zmienne konfiguracji</string>
|
||||
<string name="private_dns_hostname">Prywatny DNS: nazwa hosta (%1$s)</string>
|
||||
<string name="app">Aplikacja</string>
|
||||
<string name="refresh_rate">Częstotliwość odświeżania statystyki</string>
|
||||
<string name="app_selection">Wybór aplikacji</string>
|
||||
<string name="tunnel_state_handshake_failure">Niepowodzenie wymiany potwierdzeń</string>
|
||||
<string name="notification_dynamic_dns_message">%1$s zaktualizowany po zmianie dynamicznego DNS</string>
|
||||
<string name="tunnel_state_establishing_connection">Nawiązywanie połączenia</string>
|
||||
<string name="prefer_ipv6">Preferuj IPv6</string>
|
||||
<string name="peer_endpoints">Punkty końcowe peerów</string>
|
||||
<string name="dns_endpoint_hint">Adres IP, nazwa hosta lub adres URL DoH</string>
|
||||
<string name="restore_ipv6">Przywróć IPv6</string>
|
||||
<string name="network">Sieć</string>
|
||||
<string name="fallback_to_ipv4_desc">W przypadku awarii protokołu IPv6 przełącz się na protokół IPv4 bez ponownego uruchamiania tunelu.</string>
|
||||
<string name="tunnel_state_disconnected">Rozłączono</string>
|
||||
<string name="live">Czasu rzeczywistego (1 s)</string>
|
||||
<string name="uptime_template">czas pracy: %1$s</string>
|
||||
<string name="events">Zdarzenia</string>
|
||||
<string name="peer_resolution">Rozwiązanie peerów</string>
|
||||
<string name="restore_ipv6_desc">Po wykryciu sieci IPv6 przełącz się z powrotem na IPv6.</string>
|
||||
<string name="errors_channel_description">Kanał dla błędów aplikacji i tuneli</string>
|
||||
<string name="vpn_permission_required">Wymagane zezwolenie VPN</string>
|
||||
<string name="app_channel_description">Kanał powiadomień ogólnych aplikacji, takich jak aktualizacje wersji</string>
|
||||
<string name="automation">Automatyzacja</string>
|
||||
<string name="balance_saver">Oszczędzanie baterii (10 s)</string>
|
||||
<string name="pinging_servers">Pingowanie serwerów…</string>
|
||||
<string name="dynamic_dns_update">Aktualizacja dynamicznego DNS</string>
|
||||
<string name="statistics">Statystyka</string>
|
||||
<string name="events_channel_description">Kanał dla zdarzeń aplikacji, takich jak zdarzenia automatyzacji</string>
|
||||
<string name="private_dns_automatic">Prywatny DNS: automatyczny</string>
|
||||
<string name="tunnel_statistics">Statystyka aktywnego tunelu</string>
|
||||
<string name="ipv6_settings">Ustawienia IPv6</string>
|
||||
<string name="error">Błąd</string>
|
||||
<string name="dns_error_invalid_url">Nieprawidłowy format adresu URL</string>
|
||||
<string name="view_live_tunnel">Wyświetl aktywny tunel</string>
|
||||
<string name="mode">Tryb</string>
|
||||
<string name="tunnel_state_starting">Uruchamianie</string>
|
||||
<string name="dns_endpoint_label">Punkt końcowy serwera DNS</string>
|
||||
<string name="plain_dns">Zwykły DNS (port 53)</string>
|
||||
<string name="included_apps">Uwzględnione aplikacje: %1$s</string>
|
||||
<string name="error_http_port_unavailable">Port nasłuchiwania HTTP %1$d jest już używany.\nWybierz inny port.</string>
|
||||
<string name="current_system_dns">Bieżący system DNS</string>
|
||||
<string name="global_amnezia_configuration">Globalna konfiguracja Amnezia</string>
|
||||
<string name="security">Bezpieczeństwo</string>
|
||||
<string name="more_options">Więcej opcji</string>
|
||||
<string name="status_template">stan: %1$s</string>
|
||||
<string name="export_canceled">Eksport anulowano</string>
|
||||
<string name="prefer_ipv6_desc">Używaj punktów końcowych IPv6, jeśli sieć to obsługuje.</string>
|
||||
<string name="stop_all">Zatrzymaj wszystko</string>
|
||||
<string name="copy_from">Skopiuj z</string>
|
||||
<string name="special_junk_packet">Specjalny pakiet śmieciowy</string>
|
||||
<string name="error_socks5_port_unavailable">Port SOCKS5 %1$d jest już zajęty.\nWybierz inny port.</string>
|
||||
<string name="tunnel_scripting">Obsługa skryptu przed/po</string>
|
||||
<string name="initializing">Inicjalizacja…</string>
|
||||
<string name="dns_error_invalid_ip_or_host">Invalid IP address or hostname</string>
|
||||
<string name="proxy_channel_description">Notifications for proxy tunnels.</string>
|
||||
<string name="system_dns_servers">Servers: %1$s</string>
|
||||
<string name="balanced">Balanced (3s)</string>
|
||||
<string name="import_url_description">The URL must be secure and serve a .conf file.</string>
|
||||
<string name="dns_error_invalid_port">Port must be between 1 and 65535</string>
|
||||
<string name="view_configuration">View configuration</string>
|
||||
<string name="dot">DNS over TLS (DoT)</string>
|
||||
<string name="ipv6_recovery">IPv6 recovery</string>
|
||||
<string name="screen_recording_protection">Screen recording protection</string>
|
||||
<string name="dns_error_invalid_host">Host cannot be empty</string>
|
||||
<string name="toggle_sensitive_data_visibility">Toggle sensitive data visibility</string>
|
||||
<string name="ipv4_fallback">IPv4 fallback</string>
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
<string name="app_selection">App selection</string>
|
||||
<string name="tunnel_state_handshake_failure">Handshake failure</string>
|
||||
<string name="notification_dynamic_dns_message">%1$s updated after Dynamic DNS change</string>
|
||||
<string name="tunnel_state_establishing_connection">Establishing connection</string>
|
||||
<string name="prefer_ipv6">Prefer IPv6</string>
|
||||
<string name="peer_endpoints">Peer endpoints</string>
|
||||
<string name="dns_endpoint_hint">IP, hostname, or DoH URL</string>
|
||||
<string name="restore_ipv6">Restore IPv6</string>
|
||||
<string name="network">Network</string>
|
||||
<string name="fallback_to_ipv4_desc">Switch to IPv4 if IPv6 fails, without restarting the tunnel.</string>
|
||||
<string name="tunnel_state_disconnected">Disconnected</string>
|
||||
<string name="live">Real-time (1s)</string>
|
||||
<string name="uptime_template">uptime: %1$s</string>
|
||||
<string name="events">Events</string>
|
||||
<string name="peer_resolution">Peer Resolution</string>
|
||||
<string name="restore_ipv6_desc">Switch back to IPv6 when an IPv6 network is detected.</string>
|
||||
<string name="errors_channel_description">A channel for application and tunnel errors</string>
|
||||
<string name="vpn_permission_required">VPN permission required</string>
|
||||
<string name="app_channel_description">A channel for general application notifications, like version updates</string>
|
||||
<string name="automation">Automation</string>
|
||||
<string name="balance_saver">Battery Saver (10s)</string>
|
||||
<string name="pinging_servers">Pinging servers…</string>
|
||||
<string name="dynamic_dns_update">Dynamic DNS update</string>
|
||||
<string name="statistics">Statistics</string>
|
||||
<string name="events_channel_description">A channel for app events, like automation event</string>
|
||||
<string name="private_dns_automatic">Private DNS: automatic</string>
|
||||
<string name="tunnel_statistics">Live tunnel statistics</string>
|
||||
<string name="ipv6_settings">IPv6 settings</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="dns_error_invalid_url">Invalid URL format</string>
|
||||
<string name="view_live_tunnel">View live tunnel</string>
|
||||
<string name="mode">Mode</string>
|
||||
<string name="tunnel_state_starting">Starting</string>
|
||||
<string name="dns_endpoint_label">DNS server endpoint</string>
|
||||
<string name="plain_dns">Plain DNS (port 53)</string>
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
<string name="export_canceled">Export canceled</string>
|
||||
<string name="prefer_ipv6_desc">Use IPv6 endpoints when the network supports it.</string>
|
||||
<string name="stop_all">Stop all</string>
|
||||
<string name="copy_from">Copy from</string>
|
||||
<string name="special_junk_packet">Special junk packet</string>
|
||||
<string name="error_socks5_port_unavailable">SOCKS5 port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="tunnel_scripting">Pre/Post script support</string>
|
||||
<string name="initializing">Initializing…</string>
|
||||
<string name="errors_channel_id" translatable="false">Errors Channel</string>
|
||||
<string name="s2" translatable="false">S2</string>
|
||||
<string name="jc" translatable="false">Jc</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
<string name="app_name">WG Tunnel</string>
|
||||
<string name="error_file_extension">O ficheiro não é .conf ou .zip</string>
|
||||
<string name="prominent_background_location_message">Este recurso precisa de permissões de localização em segundo plano para ativar o monitoramento do SSID da rede Wi-Fi mesmo quando a aplicação está fechado. Para mais pormenores, por favor veja a Política de Privacidade no ecrã de Suporte.</string>
|
||||
<string name="enabled_app_shortcuts">Ativar atalhos de aplicações</string>
|
||||
<string name="tunnels">Túneis</string>
|
||||
<string name="privacy_policy">Política de Privacidade</string>
|
||||
<string name="okay">OK</string>
|
||||
@@ -71,7 +70,6 @@
|
||||
<string name="auto">(automático)</string>
|
||||
<string name="pin_created">Pin criado com sucesso</string>
|
||||
<string name="create_pin">Criar um pin</string>
|
||||
<string name="enable_app_lock">Ligar bloqueio de aplicação</string>
|
||||
<string name="edit_tunnel">Editar túnel</string>
|
||||
<string name="set_primary_tunnel">Selecionar como túnel principal</string>
|
||||
<string name="support">Suporte</string>
|
||||
@@ -110,7 +108,6 @@
|
||||
<string name="donate">Contribua com esse projeto</string>
|
||||
<string name="local_logging">Registro local</string>
|
||||
<string name="stop_on_no_internet">Interromper quando não há internet</string>
|
||||
<string name="stop_on_internet_loss">Interrompa o túnel quando a internet não estiver disponível</string>
|
||||
<string name="native_kill_switch">Interruptor de desligamento padrão</string>
|
||||
<string name="allow_lan_traffic">Permitir tráfego LAN</string>
|
||||
<string name="bypass_lan_for_kill_switch">Ignorar LAN no interruptor de desligamento</string>
|
||||
@@ -201,7 +198,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -216,7 +212,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
<string name="join_matrix">Join Matrix community</string>
|
||||
@@ -286,7 +281,6 @@
|
||||
<string name="fix">Fix</string>
|
||||
<string name="tunnel_running_name_message">Name unchangeable while tunnel is active.</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
<string name="okay">OK</string>
|
||||
<string name="tunnel_on_ethernet">Túnel na ethernet</string>
|
||||
<string name="create_import">Criar do zero</string>
|
||||
<string name="enabled_app_shortcuts">Ativar atalhos de aplicações</string>
|
||||
<string name="tunnel_on_wifi">Túnel em Wi-Fi não confiável</string>
|
||||
<string name="email_subject">Apoio para o WG Tunnel</string>
|
||||
<string name="email_chooser">Enviar um email…</string>
|
||||
@@ -34,7 +33,6 @@
|
||||
<string name="auto">(automático)</string>
|
||||
<string name="pin_created">Pin criado com sucesso</string>
|
||||
<string name="enter_pin">Digite o seu pin</string>
|
||||
<string name="enable_app_lock">Ligar bloqueio de aplicação</string>
|
||||
<string name="edit_tunnel">Editar túnel</string>
|
||||
<string name="junk_packet_count">Quantidade de pacotes-lixo</string>
|
||||
<string name="junk_packet_minimum_size">Tamanho mínimo de pacote-lixo</string>
|
||||
@@ -111,7 +109,6 @@
|
||||
<string name="monitoring_state_changes">Monitorar estado de alterações</string>
|
||||
<string name="donate">Contribua com projeto</string>
|
||||
<string name="add_from_clipboard">Adicionar da área de transferência</string>
|
||||
<string name="stop_on_internet_loss">Interrompa o túnel quando a internet não estiver disponível</string>
|
||||
<string name="stop">pausar</string>
|
||||
<string name="splt_tunneling">Tunelamento dividido</string>
|
||||
<string name="show_scripts">Mostrar scripts</string>
|
||||
@@ -202,7 +199,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -216,7 +212,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
<string name="join_matrix">Join Matrix community</string>
|
||||
@@ -286,7 +281,6 @@
|
||||
<string name="fix">Fix</string>
|
||||
<string name="tunnel_running_name_message">Name unchangeable while tunnel is active.</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
<string name="all">Все</string>
|
||||
<string name="no_email_detected">Приложение для отправки почты не найдено</string>
|
||||
<string name="enter_pin">Введите PIN-код</string>
|
||||
<string name="enable_app_lock">Защита приложения</string>
|
||||
<string name="settings">Настройки</string>
|
||||
<string name="support">Поддержка</string>
|
||||
<string name="init_packet_junk_size">S1</string>
|
||||
@@ -63,7 +62,6 @@
|
||||
<string name="yes">Да</string>
|
||||
<string name="prominent_background_location_message">Эта функция требует фоновый доступ к местоположению для отслеживания имён сетей Wi-Fi, даже когда приложение закрыто. Для получения дополнительной информации, прочтите политику конфиденциальности на экране поддержки.</string>
|
||||
<string name="copy_public_key">Копировать открытый ключ</string>
|
||||
<string name="enabled_app_shortcuts">Ярлыки приложения</string>
|
||||
<string name="open_issue">Сообщить о проблеме</string>
|
||||
<string name="incorrect_pin">Некорректный PIN-код</string>
|
||||
<string name="pin_created">PIN-код создан успешно</string>
|
||||
@@ -115,7 +113,6 @@
|
||||
<string name="show_scripts">Показать сценарии</string>
|
||||
<string name="quick_actions">Быстрые действия</string>
|
||||
<string name="stop_on_no_internet">Остановить без интернета</string>
|
||||
<string name="stop_on_internet_loss">Остановить туннель при потере интернета</string>
|
||||
<string name="native_kill_switch">Штатное экстренное отключение</string>
|
||||
<string name="allow_lan_traffic">Обход LAN</string>
|
||||
<string name="bypass_lan_for_kill_switch">Разрешать трафик LAN при экстренном отключении</string>
|
||||
@@ -157,7 +154,6 @@
|
||||
<string name="copy">Копировать</string>
|
||||
<string name="delete">Удалить</string>
|
||||
<string name="export_failed">Экспорт не выполнен</string>
|
||||
<string name="enable_remote_app_control">Удалённое управление приложением</string>
|
||||
<string name="error_download_failed">Невозможно скачать конфигурацию</string>
|
||||
<string name="select_all">Выбрать все</string>
|
||||
<string name="export_success">Экспорт успешно выполнен</string>
|
||||
@@ -281,14 +277,12 @@
|
||||
<string name="resources">Ресурсы</string>
|
||||
<string name="back">Назад</string>
|
||||
<string name="already_donated">Пожертвование уже сделано</string>
|
||||
<string name="global_split_tunneling">Общее раздельное туннелирование</string>
|
||||
<string name="active_network">Активная сеть:</string>
|
||||
<string name="help_translate">Помочь перевести приложение</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Другое</string>
|
||||
<string name="kill_switch">экстренное отключение</string>
|
||||
<string name="configuration">Конфигурация</string>
|
||||
<string name="global_dns_servers">Общие серверы DNS</string>
|
||||
<string name="contact">Контакты</string>
|
||||
<string name="backup_and_restore">Резервное копирование</string>
|
||||
<string name="about">О приложении</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
<string name="seconds">Sekundy</string>
|
||||
<string name="persistent_keepalive">Trvalé udržanie spojenia</string>
|
||||
<string name="cancel">Zrušiť</string>
|
||||
<string name="enabled_app_shortcuts">Povoliť skratky aplikácií</string>
|
||||
<string name="unknown_error">Došlo k neznámej chybe</string>
|
||||
<string name="tunnel_on_wifi">Tunel na nedôveryhodnej wifi</string>
|
||||
<string name="email_subject">Podpora tunela WG</string>
|
||||
@@ -71,7 +70,6 @@
|
||||
<string name="save">Save</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Zapnúť App Lock</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin úspešne vytvorený</string>
|
||||
<string name="post_up">Post up</string>
|
||||
@@ -82,7 +80,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -166,7 +163,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -184,7 +180,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Motív zobrazenia</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -276,7 +271,6 @@
|
||||
<string name="settings">Nastavenia</string>
|
||||
<string name="incorrect_pin">Pin je nesprávny</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -27,7 +26,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -133,7 +131,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -156,7 +153,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -257,7 +253,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -272,7 +267,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
<string name="listen_port">துறைமுகம் கேளுங்கள்</string>
|
||||
<string name="persistent_keepalive">தொடர்ச்சியான கீப்அலிவ்</string>
|
||||
<string name="cancel">ரத்துசெய்</string>
|
||||
<string name="enabled_app_shortcuts">பயன்பாட்டு குறுக்குவழிகளை இயக்கவும்</string>
|
||||
<string name="unknown_error">தெரியாத பிழை ஏற்பட்டது</string>
|
||||
<string name="tunnel_on_wifi">நம்பத்தகாத வைஃபை மீது சுரங்கப்பாதை</string>
|
||||
<string name="email_subject">WG சுரங்கப்பாதை உதவி</string>
|
||||
@@ -48,7 +47,6 @@
|
||||
<string name="pin_created">முள் வெற்றிகரமாக உருவாக்கப்பட்டது</string>
|
||||
<string name="enter_pin">உங்கள் முள் உள்ளிடவும்</string>
|
||||
<string name="create_pin">முள் உருவாக்கவும்</string>
|
||||
<string name="enable_app_lock">பயன்பாட்டு பூட்டை இயக்கவும்</string>
|
||||
<string name="settings">அமைப்புகள்</string>
|
||||
<string name="support">உதவி</string>
|
||||
<string name="junk_packet_count">குப்பை பாக்கெட் எண்ணிக்கை</string>
|
||||
@@ -92,7 +90,6 @@
|
||||
<string name="local_logging">உள்ளூர்வாசிகள்</string>
|
||||
<string name="add_from_clipboard">கிளிப்போர்டில் இருந்து சேர்க்கவும்</string>
|
||||
<string name="stop_on_no_internet">இணையத்தில் நிறுத்துங்கள்</string>
|
||||
<string name="stop_on_internet_loss">இணைய இழப்பில் சுரங்கப்பாதையை நிறுத்துங்கள்</string>
|
||||
<string name="native_kill_switch">சொந்த கொலை சுவிட்ச்</string>
|
||||
<string name="allow_lan_traffic">லேன் போக்குவரத்தை அனுமதிக்கவும்</string>
|
||||
<string name="bypass_lan_for_kill_switch">கொலை சுவிட்சுக்கு பைபாச் லேன்</string>
|
||||
@@ -157,7 +154,6 @@
|
||||
<string name="dns_resolve_error">டிஎன்எச் தீர்மானம் பிழை</string>
|
||||
<string name="auth_error">அங்கீகரிக்கப்பட்ட பிழை இல்லை</string>
|
||||
<string name="service_running_error">பணி இயங்கும் பிழை</string>
|
||||
<string name="enable_remote_app_control">தொலைநிலை பயன்பாட்டுக் கட்டுப்பாட்டை இயக்கவும்</string>
|
||||
<string name="flavor_template">சுவை: %1$s</string>
|
||||
<string name="add_from_url">முகவரி இலிருந்து சேர்க்கவும்</string>
|
||||
<string name="delete_logs">பதிவுகளை நீக்கவும் அழிக்கவும்</string>
|
||||
@@ -232,7 +228,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -245,7 +240,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
<string name="backup_and_restore">Backup and restore</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel on mobile data</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Configuration changes saved.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -27,7 +26,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -134,7 +132,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -157,7 +154,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -258,7 +254,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -273,7 +268,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
<string name="seconds">Saniye</string>
|
||||
<string name="persistent_keepalive">Kalıcı canlı tutma</string>
|
||||
<string name="cancel">İptal</string>
|
||||
<string name="enabled_app_shortcuts">Uygulama kısayollarını etkinleştir</string>
|
||||
<string name="unknown_error">Bilinmeyen bir hata oluştu</string>
|
||||
<string name="tunnel_on_wifi">Güvenilmeyen wifi’da tünel</string>
|
||||
<string name="my_email" translatable="false">support@zaneschepke.com</string>
|
||||
@@ -73,7 +72,6 @@
|
||||
<string name="pin_created">Pin başarıyla oluşturuldu</string>
|
||||
<string name="enter_pin">Pin’inizi girin</string>
|
||||
<string name="create_pin">Pin oluştur</string>
|
||||
<string name="enable_app_lock">Uygulama kilidini etkinleştir</string>
|
||||
<string name="set_primary_tunnel">Birincil tünel olarak ayarla</string>
|
||||
<string name="edit_tunnel">Tüneli düzenle</string>
|
||||
<string name="settings">Ayarlar</string>
|
||||
@@ -121,7 +119,6 @@
|
||||
<string name="local_logging">Yerel günlüğe kaydetme</string>
|
||||
<string name="add_from_clipboard">Panodan ekle</string>
|
||||
<string name="stop_on_no_internet">İnternet olmadığında durdur</string>
|
||||
<string name="stop_on_internet_loss">İnternet kaybında tüneli durdur</string>
|
||||
<string name="native_kill_switch">Yerel kill switch</string>
|
||||
<string name="allow_lan_traffic">LAN trafiğine izin ver</string>
|
||||
<string name="bypass_lan_for_kill_switch">Kill switch için LAN’ı atla</string>
|
||||
@@ -219,7 +216,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -233,7 +229,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
<string name="join_matrix">Join Matrix community</string>
|
||||
@@ -297,7 +292,6 @@
|
||||
<string name="fix">Fix</string>
|
||||
<string name="tunnel_running_name_message">Name unchangeable while tunnel is active.</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<string name="preshared_key">Загальний ключ</string>
|
||||
<string name="seconds">Секунд</string>
|
||||
<string name="persistent_keepalive">Підтримка роботи тунелю (keepalive)</string>
|
||||
<string name="enabled_app_shortcuts">Дозволити ярлики</string>
|
||||
<string name="unknown_error">Невідома помилка</string>
|
||||
<string name="tunnel_on_wifi">Тунелювати недовірені мережі Wi-Fi</string>
|
||||
<string name="email_subject">Підтримка WG-Tunnel</string>
|
||||
@@ -67,7 +66,6 @@
|
||||
<string name="pin_created">PIN-код створено успішно</string>
|
||||
<string name="enter_pin">Введіть PIN-код</string>
|
||||
<string name="create_pin">Створити PIN-код</string>
|
||||
<string name="enable_app_lock">Увімкнути блокування додатку</string>
|
||||
<string name="edit_tunnel">Редагувати тунель</string>
|
||||
<string name="set_primary_tunnel">Встановити як основний тунель</string>
|
||||
<string name="support">Підтримка</string>
|
||||
@@ -132,7 +130,6 @@
|
||||
<string name="local_logging">Локальне ведення журналу</string>
|
||||
<string name="start_auto">Запустити автотунель</string>
|
||||
<string name="stop_auto">Зупинити автотунель</string>
|
||||
<string name="stop_on_internet_loss">Зупинити тунель під час втрати інтернету</string>
|
||||
<string name="vpn_channel_description">Канал сповіщень про стан VPN</string>
|
||||
<string name="enable_amnezia_compatibility">Включити сумісність із Amnezia</string>
|
||||
<string name="enter_config_url">Введіть URL-адресу конфігурації</string>
|
||||
@@ -208,7 +205,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">З вдячністю,</string>
|
||||
<string name="selected">Вибрані</string>
|
||||
<string name="global_split_tunneling">Глобальне роздільне тунелювання</string>
|
||||
<string name="active_network">Активна мережа:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Неможливо видалити активний тунель.</string>
|
||||
@@ -222,7 +218,6 @@
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Конфігурація</string>
|
||||
<string name="drag_handle">Елемент перетягування</string>
|
||||
<string name="global_dns_servers">Глобальні DNS сервери</string>
|
||||
<string name="contact">Зв\'язок</string>
|
||||
<string name="ports_must_differ">Помилка. Проксі мають мати різні порти.</string>
|
||||
<string name="join_matrix">Приєднуйтесь до спільноти Matrix</string>
|
||||
@@ -288,7 +283,6 @@
|
||||
<string name="fix">Виправлення</string>
|
||||
<string name="tunnel_running_name_message">Неможливо змінити назву, поки тунель активний.</string>
|
||||
<string name="export_failed">Експорт не вдався</string>
|
||||
<string name="enable_remote_app_control">Увімкнути дистанційне керування застосунком</string>
|
||||
<string name="donation_closing">Присвятити весь свій робочий час цьому проєкту — моя мрія.</string>
|
||||
<string name="update_download_failed">Не вдалося завантажити оновлення.</string>
|
||||
<string name="network_name">Мережа:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<string name="random">(بے ترتیب)</string>
|
||||
<string name="preshared_key">پہلے سے مشترکہ کلید</string>
|
||||
<string name="cancel">منسوخ</string>
|
||||
<string name="enabled_app_shortcuts">ایپ شارٹ کٹس کو فعال کریں</string>
|
||||
<string name="tunnel_on_wifi">ناقابل اعتماد وائی فائی پر ٹنل</string>
|
||||
<string name="email_subject">ڈبلیو جی ٹنل سپورٹ</string>
|
||||
<string name="docs_description">دستاویزات پڑھیں</string>
|
||||
@@ -82,7 +81,6 @@
|
||||
<string name="donate">پروجیکٹ کے لیے عطیہ کریں</string>
|
||||
<string name="local_logging">مقامی لاگنگ</string>
|
||||
<string name="stop_on_no_internet">بغیر انٹرنیٹ پر روکیں</string>
|
||||
<string name="stop_on_internet_loss">انٹرنیٹ لاس پر ٹنل روکیں</string>
|
||||
<string name="allow_lan_traffic">لین ٹریفک کی اجازت دیں</string>
|
||||
<string name="splt_tunneling">سپلٹ ٹنلنگ</string>
|
||||
<string name="show_scripts">اسکرپٹس دکھائیں</string>
|
||||
@@ -113,7 +111,6 @@
|
||||
<string name="response_packet_junk_size">رسپانس پیکٹ جنک سائز</string>
|
||||
<string name="junk_packet_minimum_size">جنک پیکٹ کا کم از کم سائز</string>
|
||||
<string name="edit_tunnel">ٹنل میں ترمیم کریں</string>
|
||||
<string name="enable_app_lock">ایپ لاک فعال کریں</string>
|
||||
<string name="logs">لاگز</string>
|
||||
<string name="restart_at_boot">بوٹ پر دوبارہ شروع کریں</string>
|
||||
<string name="support">سپورٹ</string>
|
||||
@@ -135,7 +132,6 @@
|
||||
<string name="vpn_channel_description">وی پی این ریاستی اطلاعات کے لیے ایک چینل</string>
|
||||
<string name="hide_scripts">اسکرپٹس چھپائیں</string>
|
||||
<string name="add_from_clipboard">کلپ بورڈ سے شامل کریں</string>
|
||||
<string name="enable_remote_app_control">ریموٹ ایپ کنٹرول فعال کریں</string>
|
||||
<string name="app_permission_title">ڈبلیو جی ٹنل کنٹرول برج</string>
|
||||
<string name="app_permission_description">ٹنل اور خودکار ٹنل کی خصوصیات کو کنٹرول کریں۔</string>
|
||||
<string name="add_from_url">یو آر ایل سے شامل کریں</string>
|
||||
@@ -281,14 +277,12 @@
|
||||
<string name="resources">Resources</string>
|
||||
<string name="back">Back</string>
|
||||
<string name="already_donated">Already donated</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="help_translate">Help translate the app</string>
|
||||
<string name="ethernet">Ethernet</string>
|
||||
<string name="other">Other</string>
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="backup_and_restore">Backup and restore</string>
|
||||
<string name="about">About</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<string name="delete_tunnel">Delete tunnel</string>
|
||||
<string name="tunnel_mobile_data">Tunnel trên dữ liệu di động</string>
|
||||
<string name="logs">Logs</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="config_changes_saved">Các thay đổi cấu hình đã được lưu.</string>
|
||||
<string name="join_telegram">Join Telegram community</string>
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
@@ -29,7 +28,6 @@
|
||||
<string name="remote_key">Remote key</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="use_shell_via_shizuku">Use shell via Shizuku to get Wi-Fi information, preventing the need for location permission on non-rooted devices</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="vpn">VPN</string>
|
||||
<string name="tunnel_boot_description">Start the default tunnel on boot</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
@@ -134,7 +132,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">Gratefully,</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="active_network">Active Network:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">Cannot delete active tunnel.</string>
|
||||
@@ -157,7 +154,6 @@
|
||||
<string name="mtu">MTU</string>
|
||||
<string name="configuration">Configuration</string>
|
||||
<string name="drag_handle">Drag Handle</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="display_theme">Display theme</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="ports_must_differ">Failed. Proxies must have different ports.</string>
|
||||
@@ -257,7 +253,6 @@
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="server_port">Server:Port</string>
|
||||
<string name="camera_permission_required">Camera permission required</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="preferred_tunnel">Preferred tunnel</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="underload_packet_magic_header">Underload packet magic header</string>
|
||||
@@ -272,7 +267,6 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="incorrect_pin">Pin is incorrect</string>
|
||||
<string name="export_failed">Export failed</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="donation_closing">It\'s my dream to work for you on this project full-time.</string>
|
||||
<string name="update_download_failed">Update download failed.</string>
|
||||
<string name="network_name">Network:</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
<string name="private_key">私钥</string>
|
||||
<string name="listen_port">监听端口</string>
|
||||
<string name="optional">(可选)</string>
|
||||
<string name="enabled_app_shortcuts">创建桌面快捷方式</string>
|
||||
<string name="docs_description">阅读文档</string>
|
||||
<string name="email_description">给作者发邮件</string>
|
||||
<string name="error_root_denied">root 权限未开启</string>
|
||||
@@ -57,7 +56,6 @@
|
||||
<string name="yes">是</string>
|
||||
<string name="all">全部</string>
|
||||
<string name="no_email_detected">未安装邮件应用</string>
|
||||
<string name="enable_app_lock">锁定应用</string>
|
||||
<string name="settings">设置</string>
|
||||
<string name="support">支持</string>
|
||||
<string name="junk_packet_minimum_size">无效包最小值</string>
|
||||
@@ -113,7 +111,6 @@
|
||||
<string name="donate">捐赠</string>
|
||||
<string name="local_logging">本地日志监控器</string>
|
||||
<string name="stop_on_no_internet">无网络时停用</string>
|
||||
<string name="stop_on_internet_loss">网络丢失时停止隧道</string>
|
||||
<string name="bypass_lan_for_kill_switch">绕过局域网流量</string>
|
||||
<string name="auto_tunnel_channel_description">自动隧道状态通知频道</string>
|
||||
<string name="stop">停止</string>
|
||||
@@ -154,7 +151,6 @@
|
||||
<string name="copy">复制</string>
|
||||
<string name="config_error">无效配置</string>
|
||||
<string name="service_running_error">服务未运行</string>
|
||||
<string name="enable_remote_app_control">开启远程应用控制</string>
|
||||
<string name="join_matrix">加入 Matrix 社区</string>
|
||||
<string name="app_permission_description">控制隧道和自动隧道功能.</string>
|
||||
<string name="app_permission_title">控制隧道和自动隧道功能。</string>
|
||||
@@ -300,8 +296,6 @@
|
||||
<string name="metered_tunnel">流量计费的隧道</string>
|
||||
<string name="lockdown_settings">锁定设置</string>
|
||||
<string name="unavailable_in_mode">当前模式下不可用</string>
|
||||
<string name="global_split_tunneling">全局分流隧道</string>
|
||||
<string name="global_dns_servers">全局 DNS 服务器</string>
|
||||
<string name="dual_stack">双栈</string>
|
||||
<string name="dual_stack_description">隧道必须支持 IPv4 和 IPv6</string>
|
||||
<string name="save_changes">保存更改</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
<string name="enter_pin">輸入 PIN</string>
|
||||
<string name="pin_created">成功建立 PIN</string>
|
||||
<string name="incorrect_pin">PIN 不正確</string>
|
||||
<string name="enable_app_lock">啟用應用程式鎖定</string>
|
||||
<string name="set_primary_tunnel">沒有配置首選隧道時使用的隧道</string>
|
||||
<string name="edit_tunnel">編輯隧道</string>
|
||||
<string name="vpn_settings">系統 VPN 設定</string>
|
||||
@@ -64,7 +63,6 @@
|
||||
<string name="add_wifi_name">新增 WiFi 名稱</string>
|
||||
<string name="allow_lan_traffic">允許 LAN 流量</string>
|
||||
<string name="stop_on_no_internet">沒有連上網路時停止</string>
|
||||
<string name="stop_on_internet_loss">網路斷線時停止隧道</string>
|
||||
<string name="add_from_clipboard">從剪貼簿新增</string>
|
||||
<string name="stop">停止</string>
|
||||
<string name="exclude_lan">排除 LAN</string>
|
||||
@@ -83,7 +81,6 @@
|
||||
<string name="vpn_off">VPN 已關閉</string>
|
||||
<string name="error_root_denied">無法取得 root 權限</string>
|
||||
<string name="root_accepted">已取得 root 權限</string>
|
||||
<string name="enable_remote_app_control">啟用遠端應用程式控制</string>
|
||||
<string name="join_telegram">加入 Telegram 社群</string>
|
||||
<string name="add_from_url">從 URL 新增</string>
|
||||
<string name="join_matrix">加入 Matrix 社群</string>
|
||||
@@ -248,7 +245,6 @@
|
||||
<string name="kofi">Ko-fi</string>
|
||||
<string name="donation_signoff">感激地,</string>
|
||||
<string name="selected">已選擇的</string>
|
||||
<string name="global_split_tunneling">全域隧道拆分</string>
|
||||
<string name="active_network">正在使用的網路:</string>
|
||||
<string name="range_hint">(%1$d–%2$d)</string>
|
||||
<string name="delete_active_message">無法刪除正在使用的隧道。</string>
|
||||
@@ -260,7 +256,6 @@
|
||||
<string name="new_tunnel">新隧道</string>
|
||||
<string name="kill_switch">kill switch</string>
|
||||
<string name="configuration">組態</string>
|
||||
<string name="global_dns_servers">全域 DNS 伺服器</string>
|
||||
<string name="contact">聯絡</string>
|
||||
<string name="ports_must_differ">失敗。代理必須具有不同連接埠。</string>
|
||||
<string name="backup_and_restore">備份和還原</string>
|
||||
@@ -295,7 +290,6 @@
|
||||
<string name="mimic_sip">模仿 SIP</string>
|
||||
<string name="unavailable_in_mode">當前模式下不可用</string>
|
||||
<string name="server_port">伺服器:連接埠</string>
|
||||
<string name="enabled_app_shortcuts">啟用應用程式捷徑</string>
|
||||
<string name="preferred_tunnel">首選隧道</string>
|
||||
<string name="tunnel_running_name_message">隧道正在使用時無法變更名稱。</string>
|
||||
<string name="donation_closing">為您全職開發本專案是我的夢想。</string>
|
||||
@@ -391,7 +385,6 @@
|
||||
<string name="dns_error_empty">Endpoint cannot be empty</string>
|
||||
<string name="tunnel_state_connected">Connected</string>
|
||||
<string name="dns_error_invalid_scheme">DoH must use HTTPS</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="private_dns_hostname">Private DNS: hostname (%1$s)</string>
|
||||
<string name="app">App</string>
|
||||
<string name="refresh_rate">Statistics refresh rate</string>
|
||||
@@ -433,7 +426,6 @@
|
||||
<string name="included_apps">%1$s apps included</string>
|
||||
<string name="error_http_port_unavailable">HTTP listener port %1$d is already in use.\nPlease choose a different port.</string>
|
||||
<string name="current_system_dns">Current system DNS</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="more_options">More options</string>
|
||||
<string name="status_template">status: %1$s</string>
|
||||
@@ -467,7 +459,4 @@
|
||||
<string name="i4" translatable="false">I4</string>
|
||||
<string name="s1" translatable="false">S1</string>
|
||||
<string name="h4" translatable="false">H4</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
<string name="seconds">Seconds</string>
|
||||
<string name="persistent_keepalive">Persistent keepalive</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="enabled_app_shortcuts">Enable app shortcuts</string>
|
||||
<string name="unknown_error">Unknown error occurred</string>
|
||||
<string name="tunnel_on_wifi">Tunnel on Wi-Fi</string>
|
||||
<string name="my_email" translatable="false">support@zaneschepke.com</string>
|
||||
@@ -83,7 +82,6 @@
|
||||
<string name="pin_created">Pin successfully created</string>
|
||||
<string name="enter_pin">Enter PIN</string>
|
||||
<string name="create_pin">Create PIN</string>
|
||||
<string name="enable_app_lock">Enable app lock</string>
|
||||
<string name="set_primary_tunnel">Tunnel used when no preferred tunnel is configured</string>
|
||||
<string name="edit_tunnel">Edit tunnel</string>
|
||||
<string name="settings">Settings</string>
|
||||
@@ -129,8 +127,7 @@
|
||||
<string name="donate">Donate to project</string>
|
||||
<string name="local_logging">Local logging</string>
|
||||
<string name="add_from_clipboard">Add from clipboard</string>
|
||||
<string name="stop_on_no_internet">Stop on no internet</string>
|
||||
<string name="stop_on_internet_loss">Stop tunnel on internet loss</string>
|
||||
<string name="stop_on_no_internet">Stop on no network</string>
|
||||
<string name="native_kill_switch">Native kill switch</string>
|
||||
<string name="allow_lan_traffic">Allow LAN traffic</string>
|
||||
<string name="bypass_lan_for_kill_switch">Bypass LAN for kill switch</string>
|
||||
@@ -138,7 +135,7 @@
|
||||
<string name="auto_tunnel_channel_id" translatable="false">Auto-tunnel Channel</string>
|
||||
<string name="auto_tunnel_channel_description">Notifications for auto-tunnel events.</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="splt_tunneling">Split tunneling</string>
|
||||
<string name="splt_tunneling">App split tunneling</string>
|
||||
<string name="show_scripts">Show scripts</string>
|
||||
<string name="pre_up">Pre up</string>
|
||||
<string name="post_up">Post up</string>
|
||||
@@ -179,7 +176,6 @@
|
||||
<string name="dns_resolve_error">DNS resolution failed</string>
|
||||
<string name="auth_error">Unauthorized</string>
|
||||
<string name="service_running_error">Service not running</string>
|
||||
<string name="enable_remote_app_control">Enable remote app control</string>
|
||||
<string name="select_all">Select all</string>
|
||||
<string name="export_success">Export success</string>
|
||||
<string name="download">Download</string>
|
||||
@@ -276,7 +272,7 @@
|
||||
<string name="active_tunnel_update_failed">Active tunnel update failed</string>
|
||||
<string name="ddns_auto_update">Dynamic DNS (DDNS)</string>
|
||||
<string name="ddns_auto_update_description">
|
||||
Re-resolves the server hostname and updates peer endpoints on DDNS changes
|
||||
Automatically update the server endpoint when its IP changes
|
||||
</string>
|
||||
<string name="mode_disabled_template">Feature unavailable in %1$s mode.</string>
|
||||
<string name="lockdown">Lockdown</string>
|
||||
@@ -358,9 +354,6 @@
|
||||
<string name="metered_tunnel">Metered tunnel</string>
|
||||
<string name="lockdown_settings">Lockdown settings</string>
|
||||
<string name="unavailable_in_mode">Unavailable in current mode</string>
|
||||
<string name="global_split_tunneling">Global split tunneling</string>
|
||||
<string name="global_dns_servers">Global DNS servers</string>
|
||||
<string name="global_amnezia_configuration">Global Amnezia configuration</string>
|
||||
<string name="dual_stack">Dual-stack</string>
|
||||
<string name="dual_stack_description">Tunnels must support IPv4 and IPv6</string>
|
||||
<string name="save_changes">Save changes</string>
|
||||
@@ -475,7 +468,6 @@
|
||||
|
||||
<string name="error">Error</string>
|
||||
<string name="tunnel_globals">Tunnel globals</string>
|
||||
<string name="configuration_globals">Configuration globals</string>
|
||||
<string name="screen_recording_protection">Screen recording protection</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="ready">Ready</string>
|
||||
@@ -504,14 +496,32 @@
|
||||
<string name="tunnel_state_establishing_connection">Establishing connection</string>
|
||||
<string name="peer_endpoints">Peer endpoints</string>
|
||||
<string name="prefer_ipv6">Prefer IPv6</string>
|
||||
<string name="prefer_ipv6_desc">Use IPv6 endpoints when the network supports it.</string>
|
||||
<string name="prefer_ipv6_desc">Use IPv6 endpoints when the network supports it</string>
|
||||
|
||||
<string name="fallback_to_ipv4">Fallback to IPv4</string>
|
||||
<string name="fallback_to_ipv4_desc">Switch to IPv4 if IPv6 fails, without restarting the tunnel.</string>
|
||||
<string name="fallback_to_ipv4_desc">Switch to IPv4 if IPv6 fails, without restarting the tunnel</string>
|
||||
|
||||
<string name="restore_ipv6">Restore IPv6</string>
|
||||
<string name="restore_ipv6_desc">Switch back to IPv6 when an IPv6 network is detected.</string>
|
||||
<string name="no_tunnels_yet">Nothing here… yet.</string>
|
||||
<string name="getting_started_guidance">Use the + button to import a tunnel, or view the getting started guide.</string>
|
||||
<string name="getting_started_guide_link">view the getting started guide</string>
|
||||
<string name="metered_tunnel_desc">Mark this tunnel as metered so apps treat it like mobile data</string>
|
||||
<string name="local_proxy">Local proxy</string>
|
||||
<string name="local_proxy_desc">Expose the tunnel as a local SOCKS5/HTTP proxy</string>
|
||||
<string name="lockdown_desc">Permanent kill switch that blocks all non-tunnel traffic</string>
|
||||
<string name="vpn_desc">Standard system-wide VPN mode</string>
|
||||
<string name="stop_on_no_internet_desc">Stop the tunnel when the device has no network connection</string>
|
||||
<string name="start_on_boot_desc">Start auto-tunnel automatically on boot</string>
|
||||
<string name="local_logging_desc">Local log storage with live viewer</string>
|
||||
<string name="tunnel_globals_desc">Global configuration overrides for all tunnels</string>
|
||||
<string name="tunnel_configuration">Tunnel configuration</string>
|
||||
<string name="amnezia_configuration">Amnezia configuration</string>
|
||||
<string name="screen_recording_protection_desc">Block screen recording on screens with sensitive data</string>
|
||||
<string name="app_lock">App lock</string>
|
||||
<string name="app_lock_desc">Require a PIN to access the app</string>
|
||||
<string name="app_shortcuts">App shortcuts</string>
|
||||
<string name="app_shortcuts_desc">Add quick actions to the app icon</string>
|
||||
<string name="remote_control">Remote control</string>
|
||||
<string name="remote_control_desc">Allow other apps (like Tasker) to control tunnels</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
What's new:
|
||||
- Improved tunnel reliability and leak protection with new deferred endpoint bootstrapping
|
||||
- Improved tunnel status monitoring with real time handshake status monitoring
|
||||
- Kernel mode and ping monitoring removed
|
||||
- Full config and live tunnel view in quick format
|
||||
- Automatic ipv4/ipv6 endpoint fallback and recovery
|
||||
- Added support got DoT and custom endpoints for peer resolution DNS
|
||||
- Amnezia tunnel globals
|
||||
- Improved notifications
|
||||
- UI improvements with better feature descriptions
|
||||
- Various bug fix and app performance improvements
|
||||
@@ -1,5 +1,5 @@
|
||||
Co nowego:
|
||||
- Obsługa Amnezia 2.0.
|
||||
- Kopiowanie aplikacji rozdzielonego tunelu z istniejącej konfiguracji.
|
||||
- Naprawiono błąd uruchamiania rejestratora.
|
||||
- Naprawiono błąd synchronizacji dodanego szybkiego kafelka.
|
||||
What's new:
|
||||
- Amnezia 2.0 support
|
||||
- Copy split tunnel apps from existing config
|
||||
- Logger start bugfix
|
||||
- Quick tile added sync bugfix
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Co nowego:
|
||||
- Naprawiono błąd powodujący, że ekran autotunelowania nie wczytywał się bez połączenia z Wi-Fi.
|
||||
- Naprawiono błąd związany z importowaniem tunelu przez adres URL.
|
||||
What's new:
|
||||
- Auto-tunnel screen not loading without connecting to Wi-Fi bugfix
|
||||
- Import tunnel via URL bugfix
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Co nowego:
|
||||
- Naprawiono błąd trybu blokady profilu prywatnego.
|
||||
- Optymalizacja wydajności interfejsu użytkownika.
|
||||
- Naprawiono błąd powodujący awarię nawigacji wstecznej w niektórych scenariuszach.
|
||||
- Naprawiono błąd automatycznego wyścigu tunelowania po zmianach w Amnezia 2.0.
|
||||
- Lokalizacje.
|
||||
What's new:
|
||||
- Private profile lockdown mode bugfix
|
||||
- UI performance optimizations
|
||||
- Back navigation crash in certain scenarios bugfix
|
||||
- Auto tunneling race after Amnezia 2.0 changes bugfix
|
||||
- Localizations
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Co nowego:
|
||||
- Naprawiono uzgadnianie trybu drzemki.
|
||||
- Opcjonalnie naprawiono błąd I2-5.
|
||||
- Naprawiono błąd powodujący awarię podczas tworzenia od podstaw.
|
||||
- Wyświetlanie statystyk tunelu w powiadomieniach.
|
||||
- Filtrowanie tunelu według opóźnienia.
|
||||
- Tłumaczenia.
|
||||
What's new:
|
||||
- Doze mode handshake fix
|
||||
- Optional I2-5 bugfix
|
||||
- Create from scratch crash bugfix
|
||||
- Show tunnel statistics in notification
|
||||
- Filter tunnel by latency
|
||||
- Translations
|
||||
|
||||
@@ -21,6 +21,19 @@ interface Tunnel {
|
||||
data object Down : State
|
||||
|
||||
data object Starting : State
|
||||
|
||||
data object Stopping : State
|
||||
|
||||
companion object {
|
||||
fun fromNative(code: Int): State? {
|
||||
return when (code) {
|
||||
0 -> Up.Healthy
|
||||
1 -> Up.HandshakeFailure
|
||||
99 -> Down
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface IpStrategy {
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.zaneschepke.tunnel.model.DnsBoostrapMode
|
||||
import com.zaneschepke.tunnel.model.KillSwitchConfig
|
||||
import com.zaneschepke.tunnel.service.VpnService
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import kotlin.reflect.KClass
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface Backend {
|
||||
@@ -27,9 +26,6 @@ interface Backend {
|
||||
|
||||
suspend fun setBootstrapDnsMode(mode: DnsBoostrapMode)
|
||||
|
||||
// Emergency synchronous teardown to be called only from Service.onDestroy()
|
||||
fun emergencyStopAllOfTypeSync(modeClass: KClass<out BackendMode>)
|
||||
|
||||
suspend fun stopAllActiveTunnels(): Result<Unit>
|
||||
|
||||
val status: Flow<BackendStatus>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.tunnel.backend
|
||||
|
||||
import com.zaneschepke.tunnel.state.EngineState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class DefaultEngineStateProvider : EngineStateProvider {
|
||||
|
||||
private val _state = MutableStateFlow(EngineState())
|
||||
|
||||
override val state: Flow<EngineState> = _state
|
||||
|
||||
fun update(transform: (EngineState) -> EngineState) {
|
||||
_state.value = transform(_state.value)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.zaneschepke.tunnel.backend
|
||||
|
||||
import com.zaneschepke.tunnel.state.EngineState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal interface EngineStateProvider {
|
||||
val state: Flow<EngineState>
|
||||
}
|
||||
@@ -12,9 +12,10 @@ object RootShell {
|
||||
|
||||
fun requestRootPermission(): Boolean {
|
||||
return try {
|
||||
val result = Shell.cmd("true").exec()
|
||||
result.isSuccess
|
||||
val shell = Shell.cmd("su").exec()
|
||||
shell.isSuccess
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Root permission request failed or timed out")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,46 +2,26 @@ package com.zaneschepke.tunnel.backend
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.tunnel.ProxyBackend
|
||||
import com.zaneschepke.tunnel.StatusCallback
|
||||
import com.zaneschepke.tunnel.VpnBackend
|
||||
import com.zaneschepke.tunnel.service.TunnelService
|
||||
import com.zaneschepke.tunnel.service.VpnService
|
||||
import com.zaneschepke.tunnel.state.NativeTunnelStatus
|
||||
import com.zaneschepke.tunnel.util.BackendException
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
|
||||
internal class ServiceHolder(val context: Context) {
|
||||
|
||||
internal val uapiPath = context.dataDir.absolutePath
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
private val nativeCallbacksRegistered = AtomicBoolean(false)
|
||||
|
||||
private val _nativeStatuses = MutableSharedFlow<NativeTunnelStatus>(extraBufferCapacity = 64)
|
||||
|
||||
val nativeStatuses = _nativeStatuses.asSharedFlow()
|
||||
|
||||
private val statusCallback = StatusCallback { handle, code ->
|
||||
val status = NativeTunnelStatus.NativeTunnelStatusCode.from(code)
|
||||
|
||||
if (status == null) {
|
||||
Timber.d("Unknown native status code: $code")
|
||||
return@StatusCallback
|
||||
}
|
||||
|
||||
Timber.d("Native Callback - Handle: $handle, Code: $status")
|
||||
|
||||
_nativeStatuses.tryEmit(NativeTunnelStatus(handle = handle, code = status))
|
||||
}
|
||||
@Volatile private var vpnService = CompletableDeferred<VpnService>()
|
||||
@Volatile private var tunnelService = CompletableDeferred<TunnelService>()
|
||||
@Volatile private var vpnServiceDestroyed = CompletableDeferred<Unit>()
|
||||
@Volatile private var tunnelServiceDestroyed = CompletableDeferred<Unit>()
|
||||
|
||||
fun set(service: VpnService) {
|
||||
vpnService.complete(service)
|
||||
@@ -51,117 +31,81 @@ internal class ServiceHolder(val context: Context) {
|
||||
tunnelService.complete(service)
|
||||
}
|
||||
|
||||
fun getVpnService(): VpnService {
|
||||
fun signalVpnServiceDestroyed() {
|
||||
vpnServiceDestroyed.complete(Unit)
|
||||
}
|
||||
|
||||
vpnService.getNow(null)?.let {
|
||||
return it
|
||||
fun signalTunnelServiceDestroyed() {
|
||||
tunnelServiceDestroyed.complete(Unit)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun getVpnService(): VpnService {
|
||||
if (vpnService.isCompleted && !vpnService.isCancelled) {
|
||||
return vpnService.getCompleted()
|
||||
}
|
||||
|
||||
try {
|
||||
if (android.net.VpnService.prepare(context) != null) {
|
||||
throw BackendException.Unauthorized("Permission unavailable to use VpnService")
|
||||
}
|
||||
|
||||
context.startForegroundService(Intent(context, VpnService::class.java))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error starting VPN service")
|
||||
if (android.net.VpnService.prepare(context) != null) {
|
||||
throw BackendException.Unauthorized("Permission unavailable to use VpnService")
|
||||
}
|
||||
|
||||
context.startForegroundService(Intent(context, VpnService::class.java))
|
||||
|
||||
return try {
|
||||
vpnService.get(2, TimeUnit.SECONDS)
|
||||
} catch (e: TimeoutException) {
|
||||
withTimeout(3_000L.milliseconds) { vpnService.await() }
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
Timber.e(e, "Timed out getting VpnService")
|
||||
throw BackendException.InternalError("Failed to get VpnService")
|
||||
}
|
||||
}
|
||||
|
||||
fun getTunnelService(): TunnelService {
|
||||
|
||||
tunnelService.getNow(null)?.let {
|
||||
return it
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun getTunnelService(): TunnelService {
|
||||
if (tunnelService.isCompleted && !tunnelService.isCancelled) {
|
||||
return tunnelService.getCompleted()
|
||||
}
|
||||
|
||||
try {
|
||||
context.startForegroundService(Intent(context, TunnelService::class.java))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error starting TunnelService")
|
||||
}
|
||||
context.startForegroundService(Intent(context, TunnelService::class.java))
|
||||
|
||||
return try {
|
||||
tunnelService.get(2, TimeUnit.SECONDS)
|
||||
} catch (e: TimeoutException) {
|
||||
withTimeout(3_000L.milliseconds) { tunnelService.await() }
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
Timber.e(e, "Timed out getting TunnelService")
|
||||
throw BackendException.InternalError("Failed to get TunnelService")
|
||||
}
|
||||
}
|
||||
|
||||
fun stopVpnService() {
|
||||
val service = vpnService.getNow(null) ?: return
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun stopTunnelService() {
|
||||
val service =
|
||||
if (tunnelService.isCompleted && !tunnelService.isCancelled) {
|
||||
tunnelService.getCompleted()
|
||||
} else return
|
||||
|
||||
Timber.d("Stopping VpnService")
|
||||
|
||||
ProxyBackend.setSocketProtector(null)
|
||||
tunnelServiceDestroyed = CompletableDeferred()
|
||||
|
||||
service.stopSelf()
|
||||
tunnelService = CompletableDeferred()
|
||||
withTimeoutOrNull(1_000L.milliseconds) { tunnelServiceDestroyed.await() }
|
||||
}
|
||||
|
||||
fun stopTunnelService() {
|
||||
val service = tunnelService.getNow(null) ?: return
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun stopVpnService() {
|
||||
val service =
|
||||
if (vpnService.isCompleted && !vpnService.isCancelled) {
|
||||
vpnService.getCompleted()
|
||||
} else return
|
||||
|
||||
Timber.d("Stopping TunnelService")
|
||||
vpnServiceDestroyed = CompletableDeferred()
|
||||
|
||||
service.stopSelf()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
fun ensureNativeCallbacksRegistered() {
|
||||
if (!nativeCallbacksRegistered.compareAndSet(expectedValue = false, newValue = true)) {
|
||||
return
|
||||
}
|
||||
|
||||
VpnBackend.setStatusCallback(statusCallback)
|
||||
|
||||
Timber.d("Registered native status callback")
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
fun maybeUnregisterNativeCallbacks() {
|
||||
val vpnAlive = vpnService.getNow(null) != null
|
||||
val tunnelAlive = tunnelService.getNow(null) != null
|
||||
|
||||
if (vpnAlive || tunnelAlive) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!nativeCallbacksRegistered.compareAndSet(expectedValue = true, newValue = false)) {
|
||||
return
|
||||
}
|
||||
|
||||
VpnBackend.setStatusCallback(null)
|
||||
|
||||
Timber.d("Unregistered native status callback")
|
||||
}
|
||||
|
||||
fun clear(service: VpnService) {
|
||||
if (vpnService.getNow(null) === service) {
|
||||
vpnService = CompletableFuture()
|
||||
}
|
||||
|
||||
maybeUnregisterNativeCallbacks()
|
||||
}
|
||||
|
||||
fun clear(service: TunnelService) {
|
||||
if (tunnelService.getNow(null) === service) {
|
||||
tunnelService = CompletableFuture()
|
||||
}
|
||||
maybeUnregisterNativeCallbacks()
|
||||
vpnService = CompletableDeferred()
|
||||
withTimeoutOrNull(1_000L.milliseconds) { vpnServiceDestroyed.await() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_MTU = 1280
|
||||
// for consumer to set AOVPN callback
|
||||
var alwaysOnCallback: WeakReference<VpnService.AlwaysOnCallback>? = null
|
||||
@Volatile var vpnService = CompletableFuture<VpnService>()
|
||||
@Volatile var tunnelService = CompletableFuture<TunnelService>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,784 +0,0 @@
|
||||
package com.zaneschepke.tunnel.backend
|
||||
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.DnsConfigManager
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.event.ActorEvent
|
||||
import com.zaneschepke.tunnel.event.ActorEvent.ActiveConfigUpdated
|
||||
import com.zaneschepke.tunnel.event.ActorEvent.BootstrapConfigUpdated
|
||||
import com.zaneschepke.tunnel.event.ActorEvent.BootstrapStateChanged
|
||||
import com.zaneschepke.tunnel.event.ActorEvent.EngineStatus
|
||||
import com.zaneschepke.tunnel.event.ActorEvent.KillSwitchStateChanged
|
||||
import com.zaneschepke.tunnel.event.ActorEvent.PeersUpdated
|
||||
import com.zaneschepke.tunnel.event.ActorEvent.ResolvedPeersApplied
|
||||
import com.zaneschepke.tunnel.event.ActorEvent.TunnelStarted
|
||||
import com.zaneschepke.tunnel.event.ActorEvent.TunnelStopped
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent.NoRootShellAccess
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.model.DnsBootstrapResult
|
||||
import com.zaneschepke.tunnel.model.PublicKey
|
||||
import com.zaneschepke.tunnel.model.RunningTunnel
|
||||
import com.zaneschepke.tunnel.model.TunnelCommand
|
||||
import com.zaneschepke.tunnel.state.ActiveTunnel
|
||||
import com.zaneschepke.tunnel.state.ActorState
|
||||
import com.zaneschepke.tunnel.state.BootstrapState
|
||||
import com.zaneschepke.tunnel.state.NativeTunnelStatus
|
||||
import com.zaneschepke.tunnel.state.TunnelRuntimeState
|
||||
import com.zaneschepke.tunnel.util.RootShellException
|
||||
import com.zaneschepke.tunnel.util.buildResolvedPeers
|
||||
import com.zaneschepke.tunnel.util.exponentialBackoffForever
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ActiveConfig
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import timber.log.Timber
|
||||
|
||||
internal class TunnelActor(
|
||||
private val scope: CoroutineScope,
|
||||
private val engine: TunnelEngine,
|
||||
private val stableNetworkEngine: StableNetworkEngine,
|
||||
) {
|
||||
|
||||
private val inbox = Channel<TunnelCommand>(Channel.UNLIMITED)
|
||||
|
||||
// track running hooks to prevent service shutdown until post down hooks complete
|
||||
private val _runningPostDownHooks = MutableStateFlow(0)
|
||||
val runningHooks = _runningPostDownHooks.asStateFlow()
|
||||
|
||||
private val _state =
|
||||
MutableStateFlow(ActorState(byTunnelId = emptyMap(), byHandle = emptyMap()))
|
||||
|
||||
val state: StateFlow<ActorState> = _state.asStateFlow()
|
||||
|
||||
private val _events = MutableSharedFlow<TunnelEvent>(extraBufferCapacity = 32)
|
||||
|
||||
val events = _events.asSharedFlow()
|
||||
|
||||
private val tunnelJobs = mutableMapOf<Int, Job>()
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
engine.status.distinctUntilChanged().collect { status ->
|
||||
when (status.code) {
|
||||
NativeTunnelStatus.NativeTunnelStatusCode.STOPPED -> {
|
||||
val tunnelId = _state.value.byHandle[status.handle] ?: return@collect
|
||||
stopTunnel(tunnelId, status.handle)
|
||||
}
|
||||
|
||||
NativeTunnelStatus.NativeTunnelStatusCode.HEALTHY,
|
||||
NativeTunnelStatus.NativeTunnelStatusCode.HANDSHAKE_FAILURE -> {
|
||||
apply(EngineStatus(status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
for (cmd in inbox) {
|
||||
try {
|
||||
when (cmd) {
|
||||
is TunnelCommand.Start -> {
|
||||
val result = engine.start(cmd.tunnel, cmd.mode)
|
||||
apply(TunnelStarted(result, cmd))
|
||||
|
||||
val runtime = _state.value.byTunnelId[result.tunnelId] ?: continue
|
||||
|
||||
val job =
|
||||
startTunnelJobs(
|
||||
tunnelId = result.tunnelId,
|
||||
runtime = runtime,
|
||||
removedPeerEndpoint = result.removedPeerEndpoint,
|
||||
)
|
||||
|
||||
tunnelJobs[result.tunnelId] = job
|
||||
|
||||
job.invokeOnCompletion { tunnelJobs.remove(result.tunnelId, job) }
|
||||
}
|
||||
|
||||
is TunnelCommand.Stop -> {
|
||||
val runtime = _state.value.byTunnelId[cmd.tunnelId] ?: continue
|
||||
|
||||
engine.stop(runtime.running.handle, runtime.running.mode)
|
||||
}
|
||||
|
||||
is TunnelCommand.SetBootstrapConfig -> {
|
||||
apply(BootstrapConfigUpdated(cmd.config))
|
||||
}
|
||||
|
||||
is TunnelCommand.UpdatePeers -> {
|
||||
val runtime = _state.value.byTunnelId[cmd.tunnelId] ?: continue
|
||||
val running = runtime.running
|
||||
|
||||
val peers = running.buildResolvedPeers(preferIpv6 = cmd.preferIpv6)
|
||||
|
||||
engine.updatePeers(
|
||||
handle = running.handle,
|
||||
mode = running.mode,
|
||||
peers = peers,
|
||||
)
|
||||
|
||||
apply(
|
||||
PeersUpdated(
|
||||
tunnelId = cmd.tunnelId,
|
||||
peers = peers,
|
||||
preferIpv6 = cmd.preferIpv6,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelCommand.ApplyResolvedPeers -> {
|
||||
val runtime = _state.value.byTunnelId[cmd.tunnelId] ?: continue
|
||||
val running = runtime.running
|
||||
|
||||
engine.updatePeers(
|
||||
handle = running.handle,
|
||||
mode = running.mode,
|
||||
peers = cmd.peers,
|
||||
)
|
||||
|
||||
apply(
|
||||
ResolvedPeersApplied(
|
||||
tunnelId = cmd.tunnelId,
|
||||
cache = cmd.cache,
|
||||
peers = cmd.peers,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelCommand.UpdateActiveConfig -> {
|
||||
val runtime = _state.value.byTunnelId[cmd.tunnelId] ?: continue
|
||||
val running = runtime.running
|
||||
|
||||
val activeConfig = engine.getActiveConfig(running.handle, running.mode)
|
||||
|
||||
if (runtime.active.activeConfig == activeConfig) {
|
||||
continue
|
||||
}
|
||||
|
||||
apply(
|
||||
ActiveConfigUpdated(
|
||||
tunnelId = cmd.tunnelId,
|
||||
activeConfig = activeConfig,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelCommand.SetBootstrapState -> {
|
||||
apply(
|
||||
BootstrapStateChanged(
|
||||
tunnelId = cmd.tunnelId,
|
||||
bootstrapState = cmd.state,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelCommand.UpdateKillSwitch -> {
|
||||
apply(KillSwitchStateChanged(cmd.enabled))
|
||||
}
|
||||
|
||||
is TunnelCommand.RunHook -> {
|
||||
val isPostDown = cmd.phase == TunnelCommand.RunHook.Phase.PostDown
|
||||
|
||||
if (isPostDown) {
|
||||
_runningPostDownHooks.update { it + 1 }
|
||||
}
|
||||
|
||||
try {
|
||||
cmd.cmds?.forEach { cmd ->
|
||||
withTimeout(3_000.milliseconds) {
|
||||
withContext(Dispatchers.IO) { RootShell.run(cmd) }
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Timber.w(t, "Root shell commands failed")
|
||||
if (t is RootShellException.NoRootAccess) {
|
||||
_events.emit(NoRootShellAccess(tunnelId = cmd.tunnelId))
|
||||
}
|
||||
} finally {
|
||||
if (isPostDown) {
|
||||
_runningPostDownHooks.update { (it - 1).coerceAtLeast(0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "Tunnel command failed: $cmd")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun send(cmd: TunnelCommand) {
|
||||
inbox.send(cmd)
|
||||
}
|
||||
|
||||
private fun apply(event: ActorEvent) {
|
||||
_state.value = reduce(_state.value, event)
|
||||
}
|
||||
|
||||
private fun startTunnelJobs(
|
||||
tunnelId: Int,
|
||||
runtime: TunnelRuntimeState,
|
||||
removedPeerEndpoint: Boolean,
|
||||
): Job {
|
||||
return scope.launch {
|
||||
supervisorScope {
|
||||
val running = runtime.running
|
||||
|
||||
if (!running.mode.config.peers.all { it.isStaticallyConfigured }) {
|
||||
startDnsBootstrapJob(tunnelId)
|
||||
}
|
||||
|
||||
if (removedPeerEndpoint) {
|
||||
when (val strategy = running.tunnel.ipStrategy) {
|
||||
Tunnel.IpStrategy.Ipv4Only -> Unit
|
||||
|
||||
is Tunnel.IpStrategy.PreferIpv6 -> {
|
||||
if (strategy.recoveryEnabled || strategy.fallbackToIpv4Enabled) {
|
||||
startIpv6Job(tunnelId, strategy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
running.tunnel.features.forEach { feature ->
|
||||
when (feature) {
|
||||
is Tunnel.Feature.ActiveConfigMonitor -> {
|
||||
startActiveConfigJob(tunnelId, feature.intervalSeconds)
|
||||
}
|
||||
|
||||
Tunnel.Feature.DynamicDNS -> {
|
||||
startDynamicDnsJob(tunnelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
awaitCancellation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTunnel(tunnelId: Int, handle: Int) {
|
||||
tunnelJobs.remove(tunnelId)?.cancel()
|
||||
apply(TunnelStopped(tunnelId, handle))
|
||||
}
|
||||
|
||||
private fun CoroutineScope.startDnsBootstrapJob(tunnelId: Int) = launch {
|
||||
send(TunnelCommand.SetBootstrapState(tunnelId, BootstrapState.ResolvingDns))
|
||||
|
||||
val runtime = state.value.byTunnelId[tunnelId] ?: return@launch
|
||||
|
||||
val running = runtime.running
|
||||
|
||||
val cache = resolvePeers(running)
|
||||
ensureActive()
|
||||
|
||||
val updatedRunning = running.copy(peerBootstrapCache = cache)
|
||||
|
||||
val networkHasIpv6 = stableNetworkEngine.stableState.value?.state?.hasIpv6 ?: false
|
||||
|
||||
val peers =
|
||||
updatedRunning.buildResolvedPeers(
|
||||
preferIpv6 = running.currentPreferIpv6 && networkHasIpv6
|
||||
)
|
||||
send(TunnelCommand.SetBootstrapState(tunnelId, BootstrapState.UpdatingPeers))
|
||||
send(TunnelCommand.ApplyResolvedPeers(tunnelId = tunnelId, cache = cache, peers = peers))
|
||||
send(TunnelCommand.SetBootstrapState(tunnelId, BootstrapState.Complete))
|
||||
}
|
||||
|
||||
private fun CoroutineScope.startDynamicDnsJob(tunnelId: Int) = launch {
|
||||
val controller =
|
||||
DynamicDnsController(
|
||||
stabilityWindowMs = DDNS_STABILITY_WINDOW,
|
||||
failureWindowMs = DDNS_FAILURE_WINDOW,
|
||||
minCheckIntervalMs = DDNS_MIN_CHECK_INTERVAL,
|
||||
)
|
||||
|
||||
combine(
|
||||
stableNetworkEngine.stableState.filterNotNull(),
|
||||
state.mapNotNull { it.byTunnelId[tunnelId]?.active },
|
||||
) { stable, activeTunnel ->
|
||||
stable to activeTunnel
|
||||
}
|
||||
.collect { (stable, activeTunnel) ->
|
||||
val runtime = state.value.byTunnelId[tunnelId] ?: return@collect
|
||||
if (!stable.state.hasInternet()) return@collect
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val isHealthy = activeTunnel.transportState is Tunnel.State.Up.Healthy
|
||||
val isHandshakeFailure =
|
||||
activeTunnel.transportState is Tunnel.State.Up.HandshakeFailure
|
||||
|
||||
if (!controller.shouldCheck(now, isHealthy, isHandshakeFailure)) return@collect
|
||||
|
||||
// Fresh DNS resolve
|
||||
val freshDns = resolvePeers(runtime.running)
|
||||
if (freshDns.isEmpty()) {
|
||||
controller.markChecked(now)
|
||||
return@collect
|
||||
}
|
||||
|
||||
ensureActive()
|
||||
|
||||
// Query live state from WireGuard UAPI
|
||||
val activeConfig =
|
||||
try {
|
||||
engine.getActiveConfig(runtime.running.handle, runtime.running.mode)
|
||||
} catch (t: Throwable) {
|
||||
Timber.w(t, "UAPI query failed during DDNS check")
|
||||
controller.markChecked(now)
|
||||
return@collect
|
||||
}
|
||||
|
||||
val mismatches = findEndpointMismatches(freshDns, activeConfig, runtime.running)
|
||||
if (mismatches.isEmpty()) {
|
||||
controller.markChecked(now)
|
||||
return@collect
|
||||
}
|
||||
|
||||
controller.markChecked(now)
|
||||
|
||||
Timber.i("Dynamic DNS drift detected for peers: $mismatches — updating")
|
||||
|
||||
_events.emit(
|
||||
TunnelEvent.DynamicDnsUpdate(tunnelId = tunnelId, changedPeers = mismatches)
|
||||
)
|
||||
|
||||
val latestRuntime = state.value.byTunnelId[tunnelId] ?: return@collect
|
||||
val updatedCache = latestRuntime.running.peerBootstrapCache + freshDns
|
||||
|
||||
send(
|
||||
TunnelCommand.ApplyResolvedPeers(
|
||||
tunnelId = tunnelId,
|
||||
cache = updatedCache,
|
||||
peers =
|
||||
latestRuntime.running
|
||||
.copy(peerBootstrapCache = updatedCache)
|
||||
.buildResolvedPeers(
|
||||
preferIpv6 = latestRuntime.running.currentPreferIpv6
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findEndpointMismatches(
|
||||
freshDns: Map<PublicKey, DnsBootstrapResult>,
|
||||
activeConfig: ActiveConfig?,
|
||||
running: RunningTunnel,
|
||||
): List<PublicKey> {
|
||||
if (activeConfig == null) return emptyList()
|
||||
|
||||
val currentEndpoints = activeConfig.peers.associateBy { it.publicKey }
|
||||
|
||||
return freshDns.mapNotNull { (pubKey, dnsResult) ->
|
||||
val current = currentEndpoints[pubKey] ?: return@mapNotNull null
|
||||
val currentEndpoint = current.endpoint ?: return@mapNotNull null
|
||||
|
||||
val normalizedCurrent = normalizeEndpointForComparison(currentEndpoint)
|
||||
|
||||
// Choose which fresh address to compare against
|
||||
val freshAddress =
|
||||
if (running.currentPreferIpv6 && dnsResult.ipv6.isNotEmpty()) {
|
||||
dnsResult.ipv6.first()
|
||||
} else {
|
||||
dnsResult.ipv4.firstOrNull() ?: dnsResult.ipv6.firstOrNull()
|
||||
} ?: return@mapNotNull null
|
||||
|
||||
if (freshAddress != normalizedCurrent) pubKey else null
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalizes an endpoint string so IPv6 addresses have brackets. */
|
||||
private fun normalizeEndpointForComparison(endpoint: String): String {
|
||||
val host = endpoint.substringBeforeLast(":")
|
||||
val port = endpoint.substringAfterLast(":")
|
||||
|
||||
return if (host.contains(":")) {
|
||||
// Looks like IPv6
|
||||
if (host.startsWith("[")) endpoint else "[$host]:$port"
|
||||
} else {
|
||||
endpoint
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.startIpv6Job(tunnelId: Int, strategy: Tunnel.IpStrategy.PreferIpv6) =
|
||||
launch {
|
||||
var currentNetworkKey: String? = null
|
||||
var hasRecoveredOnThisNetwork = false
|
||||
var hasFallenBackOnThisNetwork = false
|
||||
var healthySinceMs: Long? = null
|
||||
var failureCount = 0
|
||||
var firstFailureTime = 0L
|
||||
var ipv6Bad = false
|
||||
|
||||
combine(
|
||||
stableNetworkEngine.stableState.filterNotNull(),
|
||||
state.mapNotNull { it.byTunnelId[tunnelId]?.active },
|
||||
) { stable, activeTunnel ->
|
||||
val newKey = stable.key
|
||||
|
||||
if (newKey != currentNetworkKey) {
|
||||
currentNetworkKey = newKey
|
||||
hasRecoveredOnThisNetwork = false
|
||||
hasFallenBackOnThisNetwork = false
|
||||
healthySinceMs = null
|
||||
failureCount = 0
|
||||
firstFailureTime = 0L
|
||||
ipv6Bad = false
|
||||
|
||||
Timber.d("Stable network changed resetting IPv6 state ($newKey)")
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val isUsingIpv6 =
|
||||
activeTunnel.activeConfig?.peers?.any {
|
||||
it.endpoint?.startsWith("[") == true
|
||||
} ?: false
|
||||
|
||||
val isHealthy = activeTunnel.transportState is Tunnel.State.Up.Healthy
|
||||
val isHandshakeFailure =
|
||||
activeTunnel.transportState is Tunnel.State.Up.HandshakeFailure
|
||||
|
||||
healthySinceMs = if (isHealthy) healthySinceMs ?: now else null
|
||||
val healthyDuration = healthySinceMs?.let { now - it } ?: 0L
|
||||
|
||||
Timber.d(
|
||||
"IPv6 strategy | net=$newKey | usingIPv6=$isUsingIpv6 | healthy=$isHealthy | healthyDuration=${healthyDuration}ms | hasRecovered=$hasRecoveredOnThisNetwork | hasFallback=$hasFallenBackOnThisNetwork | hasIPv6=${stable.state.hasIpv6} | ipv6Bad=$ipv6Bad | state=${activeTunnel.transportState}"
|
||||
)
|
||||
|
||||
if (!isHandshakeFailure) {
|
||||
failureCount = 0
|
||||
firstFailureTime = 0L
|
||||
}
|
||||
|
||||
// Fallback IPv6 to IPv4
|
||||
if (
|
||||
strategy.fallbackToIpv4Enabled &&
|
||||
isHandshakeFailure &&
|
||||
isUsingIpv6 &&
|
||||
!hasFallenBackOnThisNetwork
|
||||
) {
|
||||
|
||||
if (failureCount == 0) firstFailureTime = now
|
||||
failureCount++
|
||||
|
||||
val failureDuration = now - firstFailureTime
|
||||
|
||||
Timber.d(
|
||||
"IPv6 strategy | Fallback check: failureCount=$failureCount duration=${failureDuration}ms"
|
||||
)
|
||||
|
||||
if (
|
||||
failureCount >= IPV4_FALLBACK_FAILURE_COUNT &&
|
||||
failureDuration >= IPV4_FALLBACK_FAILURE_DURATION
|
||||
) {
|
||||
|
||||
hasFallenBackOnThisNetwork = true
|
||||
ipv6Bad = true
|
||||
|
||||
Timber.d("Fallback to IPv4 triggered on $newKey (marking IPv6 bad)")
|
||||
|
||||
_events.emit(TunnelEvent.FallbackToIpv4(tunnelId))
|
||||
|
||||
send(TunnelCommand.UpdatePeers(tunnelId, preferIpv6 = false))
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery IPv4 to IPv6
|
||||
if (
|
||||
strategy.recoveryEnabled &&
|
||||
!isUsingIpv6 &&
|
||||
!hasRecoveredOnThisNetwork &&
|
||||
healthySinceMs != null &&
|
||||
stable.state.hasIpv6 &&
|
||||
!ipv6Bad
|
||||
) {
|
||||
|
||||
Timber.d(
|
||||
"IPv6 strategy | Recovery check: healthy for ${healthyDuration}ms (need >= ${RECOVERY_STABILITY_WINDOW}ms)"
|
||||
)
|
||||
|
||||
if (healthyDuration >= RECOVERY_STABILITY_WINDOW) {
|
||||
hasRecoveredOnThisNetwork = true
|
||||
|
||||
Timber.d(
|
||||
"Recovered to IPv6 on $newKey (healthy for ${healthyDuration}ms)"
|
||||
)
|
||||
|
||||
_events.emit(TunnelEvent.RecoveredToIpv6(tunnelId))
|
||||
|
||||
send(TunnelCommand.UpdatePeers(tunnelId, preferIpv6 = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
.collect {}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.startActiveConfigJob(tunnelId: Int, interval: Int) = launch {
|
||||
while (isActive) {
|
||||
send(TunnelCommand.UpdateActiveConfig(tunnelId))
|
||||
delay(interval.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reduce(state: ActorState, event: ActorEvent): ActorState {
|
||||
return when (event) {
|
||||
is EngineStatus -> {
|
||||
val tunnelId = state.byHandle[event.status.handle] ?: return state
|
||||
val runtime = state.byTunnelId[tunnelId] ?: return state
|
||||
|
||||
val newTransportState =
|
||||
when (event.status.code) {
|
||||
NativeTunnelStatus.NativeTunnelStatusCode.HEALTHY -> Tunnel.State.Up.Healthy
|
||||
|
||||
NativeTunnelStatus.NativeTunnelStatusCode.HANDSHAKE_FAILURE ->
|
||||
Tunnel.State.Up.HandshakeFailure
|
||||
|
||||
NativeTunnelStatus.NativeTunnelStatusCode.STOPPED ->
|
||||
return state // should never happen
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val updatedActive =
|
||||
runtime.active.copy(
|
||||
transportState = newTransportState,
|
||||
lastStateChangeMs = now,
|
||||
lastHealthChangeMs =
|
||||
if (newTransportState is Tunnel.State.Up.Healthy) {
|
||||
now
|
||||
} else {
|
||||
runtime.active.lastHealthChangeMs
|
||||
},
|
||||
)
|
||||
|
||||
state.copy(
|
||||
byTunnelId =
|
||||
state.byTunnelId + (tunnelId to runtime.copy(active = updatedActive))
|
||||
)
|
||||
}
|
||||
is TunnelStarted -> {
|
||||
val result = event.result
|
||||
val cmd = event.cmd
|
||||
|
||||
val running =
|
||||
RunningTunnel(
|
||||
handle = result.handle,
|
||||
interfaceName = result.interfaceName,
|
||||
mode = result.mode,
|
||||
tunnel = cmd.tunnel,
|
||||
currentPreferIpv6 = cmd.tunnel.ipStrategy is Tunnel.IpStrategy.PreferIpv6,
|
||||
)
|
||||
|
||||
val runtime =
|
||||
TunnelRuntimeState(
|
||||
running = running,
|
||||
active =
|
||||
ActiveTunnel(
|
||||
transportState = Tunnel.State.Starting,
|
||||
interfaceName = result.interfaceName,
|
||||
mode = result.mode,
|
||||
uptime = System.currentTimeMillis(),
|
||||
activeConfig = null,
|
||||
),
|
||||
)
|
||||
|
||||
state.copy(
|
||||
byTunnelId = state.byTunnelId + (result.tunnelId to runtime),
|
||||
byHandle = state.byHandle + (result.handle to result.tunnelId),
|
||||
)
|
||||
}
|
||||
|
||||
is TunnelStopped -> {
|
||||
state.copy(
|
||||
byTunnelId = state.byTunnelId - event.tunnelId,
|
||||
byHandle = state.byHandle - event.handle,
|
||||
)
|
||||
}
|
||||
is PeersUpdated -> {
|
||||
val runtime = state.byTunnelId[event.tunnelId] ?: return state
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val updatedActive = runtime.active.copy(lastPeerUpdateMs = now)
|
||||
|
||||
val updatedRunning =
|
||||
runtime.running.copy(
|
||||
currentPreferIpv6 = event.preferIpv6,
|
||||
resolvedPeers = event.peers,
|
||||
)
|
||||
|
||||
state.copy(
|
||||
byTunnelId =
|
||||
state.byTunnelId +
|
||||
(event.tunnelId to
|
||||
runtime.copy(running = updatedRunning, active = updatedActive))
|
||||
)
|
||||
}
|
||||
is ResolvedPeersApplied -> {
|
||||
val runtime = state.byTunnelId[event.tunnelId] ?: return state
|
||||
val running = runtime.running
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val updatedActive = runtime.active.copy(lastPeerUpdateMs = now)
|
||||
|
||||
val updatedRunning =
|
||||
running.copy(resolvedPeers = event.peers, peerBootstrapCache = event.cache)
|
||||
|
||||
state.copy(
|
||||
byTunnelId =
|
||||
state.byTunnelId +
|
||||
(event.tunnelId to
|
||||
runtime.copy(running = updatedRunning, active = updatedActive))
|
||||
)
|
||||
}
|
||||
|
||||
is ActiveConfigUpdated -> {
|
||||
val runtime = state.byTunnelId[event.tunnelId] ?: return state
|
||||
|
||||
val updated =
|
||||
runtime.copy(active = runtime.active.copy(activeConfig = event.activeConfig))
|
||||
|
||||
state.copy(byTunnelId = state.byTunnelId + (event.tunnelId to updated))
|
||||
}
|
||||
is BootstrapStateChanged -> {
|
||||
val runtime = state.byTunnelId[event.tunnelId] ?: return state
|
||||
|
||||
val updated =
|
||||
runtime.copy(
|
||||
active = runtime.active.copy(bootstrapState = event.bootstrapState)
|
||||
)
|
||||
|
||||
state.copy(byTunnelId = state.byTunnelId + (event.tunnelId to updated))
|
||||
}
|
||||
|
||||
is KillSwitchStateChanged -> {
|
||||
state.copy(killSwitchEnabled = event.enabled)
|
||||
}
|
||||
|
||||
is BootstrapConfigUpdated -> {
|
||||
state.copy(dnsConfig = event.config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun emergencyStop(tunnelId: Int) {
|
||||
val runtime = _state.value.byTunnelId[tunnelId] ?: return
|
||||
val handle = runtime.running.handle
|
||||
val mode = runtime.running.mode
|
||||
|
||||
Timber.d("Emergency stop tunnel $tunnelId (handle=$handle, mode=$mode)")
|
||||
|
||||
engine.stop(handle, mode)
|
||||
|
||||
// Immediately clean up actor state
|
||||
stopTunnel(tunnelId, handle)
|
||||
}
|
||||
|
||||
// Convenience method for services
|
||||
fun emergencyStopAllOfType(modeClass: KClass<out BackendMode>) {
|
||||
_state.value.byTunnelId
|
||||
.filter { (_, runtime) -> modeClass.isInstance(runtime.running.mode) }
|
||||
.keys
|
||||
.forEach { emergencyStop(it) }
|
||||
}
|
||||
|
||||
suspend fun resolvePeers(runningTunnel: RunningTunnel): Map<PublicKey, DnsBootstrapResult> {
|
||||
|
||||
val peersToResolve = runningTunnel.mode.config.peers.filter { !it.isStaticallyConfigured }
|
||||
|
||||
if (peersToResolve.isEmpty()) return emptyMap()
|
||||
|
||||
val results = mutableMapOf<PublicKey, DnsBootstrapResult>()
|
||||
|
||||
exponentialBackoffForever {
|
||||
val bypassNeeded =
|
||||
runningTunnel.mode is BackendMode.Vpn || state.value.killSwitchEnabled
|
||||
|
||||
Timber.d("Peer resolution attempt (resolved=${results.size}/${peersToResolve.size})")
|
||||
|
||||
for (peer in peersToResolve) {
|
||||
|
||||
// already resolved
|
||||
if (results.containsKey(peer.publicKey)) continue
|
||||
|
||||
val endpoint = peer.endpoint ?: continue
|
||||
val host = endpoint.substringBeforeLast(":")
|
||||
|
||||
val dnsConfig = state.value.dnsConfig
|
||||
|
||||
val dnsResult =
|
||||
try {
|
||||
DnsConfigManager.resolveHostBootstrap(
|
||||
host = host,
|
||||
dnsConfig.protocol,
|
||||
dnsConfig.upstream,
|
||||
bypass = bypassNeeded,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "DNS failed for $host")
|
||||
continue
|
||||
}
|
||||
|
||||
if (dnsResult.ipv4.isEmpty() && dnsResult.ipv6.isEmpty()) {
|
||||
Timber.w("No IPs for $host")
|
||||
continue
|
||||
}
|
||||
|
||||
results[peer.publicKey] =
|
||||
dnsResult.copy(
|
||||
ipv4 = dnsResult.ipv4,
|
||||
// normalize
|
||||
ipv6 = dnsResult.ipv6.map { "[$it]" },
|
||||
)
|
||||
|
||||
Timber.d("Resolved $host to ${results[peer.publicKey]}")
|
||||
}
|
||||
|
||||
// exit
|
||||
if (results.size == peersToResolve.size) {
|
||||
return@exponentialBackoffForever
|
||||
}
|
||||
|
||||
// force retry
|
||||
throw IllegalStateException("Incomplete resolution, retrying...")
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DDNS_MIN_CHECK_INTERVAL = 30_000L
|
||||
private const val DDNS_FAILURE_WINDOW = 15_000L
|
||||
private const val DDNS_STABILITY_WINDOW = 15_000L
|
||||
private const val IPV4_FALLBACK_FAILURE_COUNT = 4
|
||||
private const val IPV4_FALLBACK_FAILURE_DURATION = 10_000L
|
||||
private const val RECOVERY_STABILITY_WINDOW = 5_000L
|
||||
}
|
||||
}
|
||||
@@ -4,36 +4,62 @@ import com.zaneschepke.networkmonitor.ActiveNetwork
|
||||
import com.zaneschepke.networkmonitor.DnsInfo
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.PrivateDnsMode
|
||||
import com.zaneschepke.networkmonitor.StableNetworkEngine
|
||||
import com.zaneschepke.tunnel.DnsConfigManager
|
||||
import com.zaneschepke.tunnel.NotificationProvider
|
||||
import com.zaneschepke.tunnel.ProxyBackend
|
||||
import com.zaneschepke.tunnel.StatusCallback
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.VpnBackend
|
||||
import com.zaneschepke.tunnel.event.TunnelEvent
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.model.DnsBoostrapConfig
|
||||
import com.zaneschepke.tunnel.model.DnsBoostrapMode
|
||||
import com.zaneschepke.tunnel.model.DnsBootstrapResult
|
||||
import com.zaneschepke.tunnel.model.Host
|
||||
import com.zaneschepke.tunnel.model.KillSwitchConfig
|
||||
import com.zaneschepke.tunnel.model.TunnelCommand
|
||||
import com.zaneschepke.tunnel.model.PublicKey
|
||||
import com.zaneschepke.tunnel.service.VpnService
|
||||
import com.zaneschepke.tunnel.state.ActiveTunnel
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.tunnel.state.BootstrapState
|
||||
import com.zaneschepke.tunnel.state.EngineStartResult
|
||||
import com.zaneschepke.tunnel.state.KillSwitchState
|
||||
import com.zaneschepke.tunnel.state.RuntimeDnsConfig
|
||||
import com.zaneschepke.tunnel.util.BackendException
|
||||
import com.zaneschepke.tunnel.util.RootShellException
|
||||
import com.zaneschepke.tunnel.util.buildResolvedPeers
|
||||
import com.zaneschepke.tunnel.util.exponentialBackoffForever
|
||||
import com.zaneschepke.tunnel.util.isLastTunnelOfServiceType
|
||||
import com.zaneschepke.tunnel.util.toHostMap
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ActiveConfig
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.reflect.KClass
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
import timber.log.Timber
|
||||
@@ -42,10 +68,11 @@ class TunnelBackend(
|
||||
private val scope: CoroutineScope,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
override val notificationProvider: NotificationProvider,
|
||||
private val stableNetworkEngine: StableNetworkEngine,
|
||||
) : Backend {
|
||||
|
||||
private val serviceHolder: ServiceHolder by inject(ServiceHolder::class.java)
|
||||
private val actor: TunnelActor by inject(TunnelActor::class.java)
|
||||
private val engine: TunnelEngine by inject(TunnelEngine::class.java)
|
||||
|
||||
private val _status = MutableStateFlow(BackendStatus())
|
||||
override val status: Flow<BackendStatus> = _status.asStateFlow()
|
||||
@@ -53,100 +80,98 @@ class TunnelBackend(
|
||||
private val _events = MutableSharedFlow<TunnelEvent>(extraBufferCapacity = 32)
|
||||
override val events = _events.asSharedFlow()
|
||||
|
||||
private val tunnelMutex = Mutex()
|
||||
|
||||
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
|
||||
private val byHandle = ConcurrentHashMap<Int, Int>()
|
||||
private val byTunnelId = ConcurrentHashMap<Int, Int>()
|
||||
private val peerUpdateMutexes = ConcurrentHashMap<Int, Mutex>()
|
||||
|
||||
enum class PeerUpdateReason {
|
||||
DDNS_CHECK,
|
||||
IPV4_FALLBACK,
|
||||
IPV6_RECOVERY,
|
||||
NETWORK_CHANGE_RESET,
|
||||
}
|
||||
|
||||
private var dnsConfigJob: Job? = null
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
var hadVpnTunnels = false
|
||||
var hadProxyTunnels = false
|
||||
private val statusCallback = StatusCallback { handle, code ->
|
||||
val state = Tunnel.State.fromNative(code)
|
||||
state?.let { nativeState ->
|
||||
val tunnelId = byHandle[handle] ?: return@let
|
||||
updateTunnelTransportState(tunnelId, nativeState)
|
||||
}
|
||||
}
|
||||
|
||||
var lastActiveTunnelIds: Set<Int> = emptySet()
|
||||
override suspend fun start(tunnel: Tunnel, mode: BackendMode): Result<Unit> =
|
||||
tunnelMutex.withLock {
|
||||
runCatching {
|
||||
if (_status.value.activeTunnels.containsKey(tunnel.id)) {
|
||||
Timber.d("Tunnel ${tunnel.id} already running — ignoring start")
|
||||
return@runCatching
|
||||
}
|
||||
|
||||
actor.state.collect { actorState ->
|
||||
val hasVpnNow =
|
||||
actorState.byTunnelId.values.any { it.running.mode is BackendMode.Vpn }
|
||||
val isFirst = _status.value.activeTunnels.isEmpty()
|
||||
|
||||
val hasProxyNow =
|
||||
actorState.byTunnelId.values.any { it.running.mode is BackendMode.Proxy }
|
||||
|
||||
val activeTunnels = actorState.byTunnelId.mapValues { it.value.active }
|
||||
|
||||
_status.update { current -> current.copy(activeTunnels = activeTunnels) }
|
||||
|
||||
val currentTunnelIds = activeTunnels.keys
|
||||
|
||||
// update tile
|
||||
if (currentTunnelIds != lastActiveTunnelIds) {
|
||||
addOrReplaceActiveTunnel(
|
||||
tunnel.id,
|
||||
ActiveTunnel(
|
||||
tunnel = tunnel,
|
||||
transportState = Tunnel.State.Starting,
|
||||
mode = mode,
|
||||
),
|
||||
)
|
||||
notificationProvider.refreshTile(serviceHolder.context)
|
||||
lastActiveTunnelIds = currentTunnelIds
|
||||
|
||||
val scriptsEnabled = tunnel.scriptsEnabled
|
||||
|
||||
if (isFirst) VpnBackend.setStatusCallback(statusCallback)
|
||||
|
||||
if (scriptsEnabled)
|
||||
mode.config.`interface`.preUp?.let { runScripts(it, tunnel.id) }
|
||||
|
||||
val result = engine.start(tunnel, mode)
|
||||
|
||||
onEngineStartResult(tunnel.id, result)
|
||||
|
||||
if (scriptsEnabled)
|
||||
mode.config.`interface`.postUp?.let { runScripts(it, tunnel.id) }
|
||||
|
||||
tunnelJobs[result.tunnelId] =
|
||||
startTunnelJobs(result.handle, tunnel, mode, result.removedPeerEndpoint)
|
||||
}
|
||||
.onFailure { cleanup(tunnel.id) }
|
||||
}
|
||||
|
||||
// VPN cleanup
|
||||
if (hadVpnTunnels && !hasVpnNow) {
|
||||
private fun onEngineStartResult(tunnelId: Int, result: EngineStartResult) {
|
||||
updateActiveTunnel(tunnelId) {
|
||||
it.copy(interfaceName = result.interfaceName, uptime = System.currentTimeMillis())
|
||||
}
|
||||
byHandle[result.handle] = tunnelId
|
||||
byTunnelId[tunnelId] = result.handle
|
||||
}
|
||||
|
||||
ensureActive()
|
||||
actor.runningHooks.first { it == 0 }
|
||||
private fun cleanup(tunnelId: Int) {
|
||||
tunnelJobs.remove(tunnelId)?.cancel()
|
||||
removeActiveTunnel(tunnelId)
|
||||
byTunnelId[tunnelId]?.let { byHandle.remove(it) }
|
||||
byTunnelId.remove(tunnelId)
|
||||
peerUpdateMutexes.remove(tunnelId)
|
||||
}
|
||||
|
||||
val latestState = actor.state.value
|
||||
val stillHasVpn =
|
||||
latestState.byTunnelId.values.any { it.running.mode is BackendMode.Vpn }
|
||||
|
||||
if (!shouldKeepVpnServiceAlive(stillHasVpn)) {
|
||||
Timber.d("Stopping VPN service after hooks completed")
|
||||
ProxyBackend.setSocketProtector(null)
|
||||
serviceHolder.stopVpnService()
|
||||
} else {
|
||||
Timber.d("VPN shutdown aborted — state changed during hook wait")
|
||||
}
|
||||
private suspend fun runScripts(commands: List<String>, tunnelId: Int) {
|
||||
try {
|
||||
commands.forEach { cmd ->
|
||||
withTimeout(3_000.milliseconds) {
|
||||
withContext(Dispatchers.IO) { RootShell.run(cmd) }
|
||||
}
|
||||
|
||||
// Proxy cleanup
|
||||
if (hadProxyTunnels && !hasProxyNow) {
|
||||
|
||||
ensureActive()
|
||||
actor.runningHooks.first { it == 0 }
|
||||
|
||||
val latestState = actor.state.value
|
||||
val stillHasProxy =
|
||||
latestState.byTunnelId.values.any { it.running.mode is BackendMode.Proxy }
|
||||
|
||||
if (!stillHasProxy) {
|
||||
Timber.d("Stopping tunnel service after hooks completed")
|
||||
serviceHolder.stopTunnelService()
|
||||
} else {
|
||||
Timber.d("Proxy shutdown aborted — state changed during hook wait")
|
||||
}
|
||||
}
|
||||
|
||||
hadVpnTunnels = hasVpnNow
|
||||
hadProxyTunnels = hasProxyNow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldKeepVpnServiceAlive(hasVpnTunnels: Boolean): Boolean {
|
||||
return hasVpnTunnels || _status.value.killSwitch.enabled
|
||||
}
|
||||
|
||||
override suspend fun start(tunnel: Tunnel, mode: BackendMode): Result<Unit> = runCatching {
|
||||
val existing = actor.state.value.byTunnelId[tunnel.id]
|
||||
|
||||
if (existing != null) {
|
||||
Timber.d("Tunnel ${tunnel.id} already running — ignoring start")
|
||||
return@runCatching
|
||||
}
|
||||
|
||||
val scriptsEnabled = tunnel.scriptsEnabled
|
||||
|
||||
val preUp = mode.config.`interface`.preUp
|
||||
if (!preUp.isNullOrEmpty() && scriptsEnabled) {
|
||||
actor.send(TunnelCommand.RunHook(tunnel.id, TunnelCommand.RunHook.Phase.PreUp, preUp))
|
||||
}
|
||||
actor.send(TunnelCommand.Start(tunnel, mode))
|
||||
|
||||
val postUp = mode.config.`interface`.postUp
|
||||
if (!postUp.isNullOrEmpty() && scriptsEnabled) {
|
||||
actor.send(TunnelCommand.RunHook(tunnel.id, TunnelCommand.RunHook.Phase.PostUp, postUp))
|
||||
} catch (t: Throwable) {
|
||||
Timber.w(t, "Root shell commands failed")
|
||||
if (t is RootShellException.NoRootAccess) {
|
||||
_events.emit(TunnelEvent.NoRootShellAccess(tunnelId = tunnelId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,36 +179,48 @@ class TunnelBackend(
|
||||
ServiceHolder.alwaysOnCallback = WeakReference(alwaysOnCallback)
|
||||
}
|
||||
|
||||
override suspend fun stop(id: Int): Result<Unit> = runCatching {
|
||||
// TODO need a clean localized message for this passed by provider, but this error should
|
||||
// never happen
|
||||
val runtime =
|
||||
actor.state.value.byTunnelId[id]
|
||||
?: throw BackendException.InternalError(
|
||||
"Tunnel $id is not active or no longer exists"
|
||||
)
|
||||
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 scriptsEnabled = runtime.running.tunnel.scriptsEnabled
|
||||
val mode = runtime.running.mode
|
||||
val isLast = _status.value.activeTunnels.size == 1
|
||||
val isLastOfServiceType = _status.value.isLastTunnelOfServiceType(id)
|
||||
|
||||
val preDown = mode.config.`interface`.preDown
|
||||
if (!preDown.isNullOrEmpty() && scriptsEnabled) {
|
||||
actor.send(TunnelCommand.RunHook(id, TunnelCommand.RunHook.Phase.PreDown, preDown))
|
||||
try {
|
||||
stopTunnelInternal(id, activeTun)
|
||||
} finally {
|
||||
notificationProvider.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
actor.send(TunnelCommand.Stop(id))
|
||||
}
|
||||
|
||||
val postDown = mode.config.`interface`.postDown
|
||||
if (!postDown.isNullOrEmpty() && scriptsEnabled) {
|
||||
actor.send(TunnelCommand.RunHook(id, TunnelCommand.RunHook.Phase.PostDown, postDown))
|
||||
private suspend fun stopTunnelInternal(tunnelId: Int, activeTunnel: ActiveTunnel) {
|
||||
updateTunnelTransportState(tunnelId, Tunnel.State.Stopping)
|
||||
val handle = byTunnelId[tunnelId] ?: return
|
||||
val scriptsEnabled = activeTunnel.tunnel?.scriptsEnabled == true
|
||||
val mode = activeTunnel.mode ?: return
|
||||
try {
|
||||
if (scriptsEnabled) mode.config.`interface`.preDown?.let { runScripts(it, tunnelId) }
|
||||
engine.stop(handle, activeTunnel.mode)
|
||||
if (scriptsEnabled) mode.config.`interface`.postDown?.let { runScripts(it, tunnelId) }
|
||||
} finally {
|
||||
cleanup(tunnelId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setKillSwitch(config: KillSwitchConfig) = runCatching {
|
||||
val service = serviceHolder.getVpnService()
|
||||
service.setKillSwitch(config)
|
||||
|
||||
actor.send(TunnelCommand.UpdateKillSwitch(true))
|
||||
|
||||
_status.update { current ->
|
||||
current.copy(killSwitch = current.killSwitch.copy(enabled = true, config = config))
|
||||
}
|
||||
@@ -192,9 +229,6 @@ class TunnelBackend(
|
||||
override suspend fun disableKillSwitch() = runCatching {
|
||||
val service = serviceHolder.getVpnService()
|
||||
service.setKillSwitch(null)
|
||||
|
||||
actor.send(TunnelCommand.UpdateKillSwitch(false))
|
||||
|
||||
_status.update { current ->
|
||||
current.copy(
|
||||
killSwitch =
|
||||
@@ -209,17 +243,14 @@ class TunnelBackend(
|
||||
|
||||
override suspend fun setBootstrapDnsMode(mode: DnsBoostrapMode) {
|
||||
_status.update { it.copy(dnsMode = mode) }
|
||||
|
||||
when (mode) {
|
||||
is DnsBoostrapMode.Custom -> {
|
||||
Timber.d(
|
||||
"DNS Bootstrap mode set to custom: ${mode.config.protocol} -> ${mode.config.upstream}"
|
||||
)
|
||||
|
||||
dnsConfigJob?.cancel()
|
||||
dnsConfigJob = null
|
||||
|
||||
actor.send(TunnelCommand.SetBootstrapConfig(RuntimeDnsConfig.from(mode.config)))
|
||||
updateRuntimeDnsConfig(mode.config)
|
||||
}
|
||||
DnsBoostrapMode.System -> {
|
||||
Timber.d("DNS Bootstrap mode set to System")
|
||||
@@ -229,6 +260,15 @@ class TunnelBackend(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun stopAllActiveTunnels() = tunnelMutex.withLock {
|
||||
_status.value.activeTunnels.forEach { (id, tunnel) -> stopTunnelInternal(id, tunnel) }
|
||||
notificationProvider.refreshTile(serviceHolder.context)
|
||||
VpnBackend.setStatusCallback(null)
|
||||
serviceHolder.stopTunnelService()
|
||||
serviceHolder.stopVpnService()
|
||||
Result.success(Unit)
|
||||
}
|
||||
|
||||
private suspend fun emitInitialSystemDnsConfig() {
|
||||
val state =
|
||||
withTimeoutOrNull(2_500L.milliseconds) {
|
||||
@@ -244,20 +284,7 @@ class TunnelBackend(
|
||||
val config = determineSystemDnsBoostrapConfig(dns)
|
||||
|
||||
Timber.d("DNS initial emission: protocol=${config.protocol} upstream=${config.upstream}")
|
||||
|
||||
actor.send(TunnelCommand.SetBootstrapConfig(RuntimeDnsConfig.from(config)))
|
||||
}
|
||||
|
||||
override fun emergencyStopAllOfTypeSync(modeClass: KClass<out BackendMode>) {
|
||||
actor.emergencyStopAllOfType(modeClass)
|
||||
_status.update { it.copy(activeTunnels = emptyMap()) }
|
||||
notificationProvider.refreshTile(serviceHolder.context)
|
||||
}
|
||||
|
||||
override suspend fun stopAllActiveTunnels(): Result<Unit> = runCatching {
|
||||
_status.value.activeTunnels.forEach { (id, _) -> stop(id) }
|
||||
_status.update { it.copy(activeTunnels = emptyMap()) }
|
||||
notificationProvider.refreshTile(serviceHolder.context)
|
||||
updateRuntimeDnsConfig(config)
|
||||
}
|
||||
|
||||
private fun determineSystemDnsBoostrapConfig(dnsInfo: DnsInfo): DnsBoostrapConfig {
|
||||
@@ -287,8 +314,7 @@ class TunnelBackend(
|
||||
}
|
||||
|
||||
private fun startSystemDnsMonitoring() {
|
||||
if (dnsConfigJob?.isActive == true) return
|
||||
|
||||
dnsConfigJob?.cancel()
|
||||
dnsConfigJob = scope.launch {
|
||||
networkMonitor.connectivityStateFlow
|
||||
.distinctUntilChangedBy { it.underlyingDnsInfo }
|
||||
@@ -302,9 +328,499 @@ class TunnelBackend(
|
||||
)
|
||||
|
||||
val config = determineSystemDnsBoostrapConfig(dns)
|
||||
|
||||
actor.send(TunnelCommand.SetBootstrapConfig(RuntimeDnsConfig.from(config)))
|
||||
updateRuntimeDnsConfig(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStatus(transform: (BackendStatus) -> BackendStatus) {
|
||||
_status.update(transform)
|
||||
}
|
||||
|
||||
private fun updateRuntimeDnsConfig(boostrapConfig: DnsBoostrapConfig) {
|
||||
_status.update { current ->
|
||||
current.copy(runtimeDnsConfig = RuntimeDnsConfig.from(boostrapConfig))
|
||||
}
|
||||
}
|
||||
|
||||
fun addOrReplaceActiveTunnel(id: Int, tunnel: ActiveTunnel) {
|
||||
updateStatus { current ->
|
||||
current.copy(activeTunnels = current.activeTunnels + (id to tunnel))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateActiveTunnel(id: Int, transform: (ActiveTunnel) -> ActiveTunnel) {
|
||||
updateStatus { current ->
|
||||
val existing = current.activeTunnels[id] ?: return@updateStatus current
|
||||
current.copy(activeTunnels = current.activeTunnels + (id to transform(existing)))
|
||||
}
|
||||
}
|
||||
|
||||
fun removeActiveTunnel(id: Int) {
|
||||
updateStatus { current -> current.copy(activeTunnels = current.activeTunnels - id) }
|
||||
}
|
||||
|
||||
fun updateTunnelTransportState(id: Int, newState: Tunnel.State) {
|
||||
updateActiveTunnel(id) { tunnel ->
|
||||
val stateChanged = tunnel.transportState != newState
|
||||
tunnel.copy(
|
||||
transportState = newState,
|
||||
lastStateChangeMs =
|
||||
if (stateChanged) {
|
||||
System.currentTimeMillis()
|
||||
} else {
|
||||
tunnel.lastStateChangeMs
|
||||
},
|
||||
lastHealthChangeMs =
|
||||
if (newState is Tunnel.State.Up.Healthy) {
|
||||
if (stateChanged || tunnel.lastHealthChangeMs == 0L)
|
||||
System.currentTimeMillis()
|
||||
else tunnel.lastHealthChangeMs
|
||||
} else {
|
||||
tunnel.lastHealthChangeMs
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTunnelBootstrapState(id: Int, newState: BootstrapState) {
|
||||
updateActiveTunnel(id) { tunnel -> tunnel.copy(bootstrapState = newState) }
|
||||
}
|
||||
|
||||
fun markPeerUpdate(id: Int) {
|
||||
updateActiveTunnel(id) { tunnel ->
|
||||
tunnel.copy(lastPeerUpdateMs = System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTunnelJobs(
|
||||
handle: Int,
|
||||
tunnel: Tunnel,
|
||||
mode: BackendMode,
|
||||
removedPeerEndpoint: Boolean,
|
||||
): Job {
|
||||
return scope.launch {
|
||||
supervisorScope {
|
||||
if (removedPeerEndpoint) {
|
||||
updateTunnelBootstrapState(tunnel.id, BootstrapState.ResolvingDns)
|
||||
startDnsBootstrapJob(handle, tunnel, mode)
|
||||
}
|
||||
|
||||
if (removedPeerEndpoint) {
|
||||
when (val strategy = tunnel.ipStrategy) {
|
||||
Tunnel.IpStrategy.Ipv4Only -> Unit
|
||||
|
||||
is Tunnel.IpStrategy.PreferIpv6 -> {
|
||||
if (strategy.recoveryEnabled || strategy.fallbackToIpv4Enabled) {
|
||||
startIpv6Job(handle, tunnel.id, strategy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tunnel.features.forEach { feature ->
|
||||
when (feature) {
|
||||
is Tunnel.Feature.ActiveConfigMonitor -> {
|
||||
startActiveConfigJob(handle, tunnel.id, mode, feature.intervalSeconds)
|
||||
}
|
||||
Tunnel.Feature.DynamicDNS -> {
|
||||
startDynamicDnsJob(handle, tunnel.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
awaitCancellation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.startDnsBootstrapJob(
|
||||
handle: Int,
|
||||
tunnel: Tunnel,
|
||||
mode: BackendMode,
|
||||
) = launch {
|
||||
updateTunnelBootstrapState(tunnel.id, BootstrapState.ResolvingDns)
|
||||
val resultMap = 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)
|
||||
updateTunnelBootstrapState(tunnel.id, BootstrapState.UpdatingPeers)
|
||||
engine.updatePeers(handle, mode, resolvedPeers)
|
||||
markPeerUpdate(tunnel.id)
|
||||
updateTunnelBootstrapState(tunnel.id, BootstrapState.Complete)
|
||||
}
|
||||
|
||||
suspend fun resolvePeers(mode: BackendMode): Map<PublicKey, DnsBootstrapResult> {
|
||||
val peersToResolve = mode.config.peers.filter { !it.isStaticallyConfigured }
|
||||
if (peersToResolve.isEmpty()) return emptyMap()
|
||||
|
||||
val results = mutableMapOf<PublicKey, DnsBootstrapResult>()
|
||||
|
||||
// Wait until we have internet before starting any resolution
|
||||
stableNetworkEngine.stableState.first { it?.state?.hasInternet() == true }
|
||||
|
||||
exponentialBackoffForever {
|
||||
|
||||
// If we lose internet while inside the backoff loop, wait again until it comes back
|
||||
if (stableNetworkEngine.stableState.value?.state?.hasInternet() != true) {
|
||||
Timber.d("No internet — waiting for connectivity before next resolution attempt")
|
||||
stableNetworkEngine.stableState.first { it?.state?.hasInternet() == true }
|
||||
Timber.d("Internet restored — resuming peer resolution")
|
||||
}
|
||||
|
||||
Timber.d("Peer resolution attempt (resolved=${results.size}/${peersToResolve.size})")
|
||||
|
||||
for (peer in peersToResolve) {
|
||||
if (results.containsKey(peer.publicKey)) continue
|
||||
|
||||
val endpoint = peer.endpoint ?: continue
|
||||
val host = endpoint.substringBeforeLast(":")
|
||||
|
||||
val dnsConfig = _status.value.runtimeDnsConfig
|
||||
val bypassNeeded = mode is BackendMode.Vpn || _status.value.killSwitch.enabled
|
||||
|
||||
val dnsResult =
|
||||
try {
|
||||
DnsConfigManager.resolveHostBootstrap(
|
||||
host = host,
|
||||
protocol = dnsConfig.protocol,
|
||||
upstream = dnsConfig.upstream,
|
||||
bypass = bypassNeeded,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "DNS resolution failed for host=$host")
|
||||
continue
|
||||
}
|
||||
|
||||
if (dnsResult.ipv4.isEmpty() && dnsResult.ipv6.isEmpty()) {
|
||||
Timber.w("No IP addresses returned for host=$host")
|
||||
continue
|
||||
}
|
||||
|
||||
results[peer.publicKey] =
|
||||
dnsResult.copy(ipv4 = dnsResult.ipv4, ipv6 = dnsResult.ipv6.map { "[$it]" })
|
||||
Timber.d("Successfully resolved $host → ${results[peer.publicKey]}")
|
||||
}
|
||||
|
||||
if (results.size == peersToResolve.size) {
|
||||
return@exponentialBackoffForever
|
||||
}
|
||||
|
||||
throw IllegalStateException("Incomplete peer resolution, will retry with backoff")
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
private fun CoroutineScope.startActiveConfigJob(
|
||||
handle: Int,
|
||||
tunnelId: Int,
|
||||
mode: BackendMode,
|
||||
interval: Int,
|
||||
) = launch {
|
||||
while (isActive) {
|
||||
val activeConfig = engine.getActiveConfig(handle, mode)
|
||||
updateActiveTunnel(tunnelId) { it.copy(activeConfig = activeConfig) }
|
||||
delay(interval.seconds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.startDynamicDnsJob(handle: Int, tunnelId: Int) = launch {
|
||||
val controller =
|
||||
DynamicDnsController(
|
||||
stabilityWindowMs = DDNS_STABILITY_WINDOW,
|
||||
failureWindowMs = DDNS_FAILURE_WINDOW,
|
||||
minCheckIntervalMs = DDNS_MIN_CHECK_INTERVAL,
|
||||
)
|
||||
|
||||
combine(
|
||||
stableNetworkEngine.stableState.filterNotNull(),
|
||||
status.mapNotNull { it.activeTunnels[tunnelId] },
|
||||
) { stable, activeTunnel ->
|
||||
stable to activeTunnel
|
||||
}
|
||||
.collect { (stable, activeTunnel) ->
|
||||
if (!stable.state.hasInternet()) return@collect
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val isHealthy = activeTunnel.transportState is Tunnel.State.Up.Healthy
|
||||
val isHandshakeFailure =
|
||||
activeTunnel.transportState is Tunnel.State.Up.HandshakeFailure
|
||||
|
||||
if (!controller.shouldCheck(now, isHealthy, isHandshakeFailure)) return@collect
|
||||
|
||||
controller.markChecked(now)
|
||||
|
||||
val mode = activeTunnel.mode ?: return@collect
|
||||
|
||||
reconcilePeers(
|
||||
tunnelId = tunnelId,
|
||||
handle = handle,
|
||||
mode = mode,
|
||||
reason = PeerUpdateReason.DDNS_CHECK,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findEndpointMismatches(
|
||||
freshDns: Map<PublicKey, DnsBootstrapResult>,
|
||||
activeConfig: ActiveConfig,
|
||||
preferIpv6: Boolean,
|
||||
): Map<PublicKey, Host> {
|
||||
val currentEndpoints = activeConfig.peers.associateBy { it.publicKey }
|
||||
|
||||
return freshDns
|
||||
.mapNotNull { (pubKey, dnsResult) ->
|
||||
val current = currentEndpoints[pubKey] ?: return@mapNotNull null
|
||||
val currentEndpoint = current.endpoint ?: return@mapNotNull null
|
||||
|
||||
val normalizedCurrent = normalizeEndpointForComparison(currentEndpoint)
|
||||
|
||||
val freshAddress =
|
||||
if (preferIpv6 && dnsResult.ipv6.isNotEmpty()) {
|
||||
dnsResult.ipv6.first()
|
||||
} else {
|
||||
dnsResult.ipv4.firstOrNull() ?: dnsResult.ipv6.firstOrNull()
|
||||
} ?: return@mapNotNull null
|
||||
|
||||
if (freshAddress != normalizedCurrent) {
|
||||
pubKey to freshAddress
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
}
|
||||
|
||||
private fun normalizeEndpointForComparison(endpoint: String): String {
|
||||
val host = endpoint.substringBeforeLast(":")
|
||||
val port = endpoint.substringAfterLast(":")
|
||||
|
||||
return if (host.contains(":")) {
|
||||
// Looks like IPv6
|
||||
if (host.startsWith("[")) endpoint else "[$host]:$port"
|
||||
} else {
|
||||
endpoint
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.startIpv6Job(
|
||||
handle: Int,
|
||||
tunnelId: Int,
|
||||
strategy: Tunnel.IpStrategy.PreferIpv6,
|
||||
) = launch {
|
||||
var currentNetworkKey: String? = null
|
||||
var hasRecoveredOnThisNetwork = false
|
||||
var hasFallenBackOnThisNetwork = false
|
||||
var healthySinceMs: Long? = null
|
||||
var failureCount = 0
|
||||
var firstFailureTime = 0L
|
||||
var ipv6Bad = false
|
||||
|
||||
combine(
|
||||
stableNetworkEngine.stableState.filterNotNull(),
|
||||
status.mapNotNull { it.activeTunnels[tunnelId] },
|
||||
) { stable, activeTunnel ->
|
||||
stable to activeTunnel
|
||||
}
|
||||
.collect { (stable, activeTunnel) ->
|
||||
val newKey = stable.key
|
||||
val mode = activeTunnel.mode ?: return@collect
|
||||
|
||||
// Reset state upon network transition
|
||||
if (newKey != currentNetworkKey) {
|
||||
// Capture the old state before we reset local variables
|
||||
val wasFallenBack = activeTunnel.isFallenBackToIpv4ForNetwork
|
||||
|
||||
currentNetworkKey = newKey
|
||||
hasRecoveredOnThisNetwork = false
|
||||
hasFallenBackOnThisNetwork = false
|
||||
healthySinceMs = null
|
||||
failureCount = 0
|
||||
firstFailureTime = 0L
|
||||
ipv6Bad = false
|
||||
|
||||
Timber.d("Stable network changed resetting IPv6 state ($newKey)")
|
||||
|
||||
// Optimistically revert to IPv6 on a new network
|
||||
if (wasFallenBack) {
|
||||
Timber.d(
|
||||
"Network changed while in IPv4 fallback. Attempting optimistic reset."
|
||||
)
|
||||
reconcilePeers(
|
||||
tunnelId = tunnelId,
|
||||
handle = handle,
|
||||
mode = mode,
|
||||
reason = PeerUpdateReason.NETWORK_CHANGE_RESET,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
// Determine if currently using IPv6
|
||||
val isUsingIpv6 =
|
||||
activeTunnel.activeConfig?.peers?.any { it.endpoint?.startsWith("[") == true }
|
||||
?: mode.config.peers.any { it.endpoint?.startsWith("[") == true }
|
||||
|
||||
val isHealthy = activeTunnel.transportState is Tunnel.State.Up.Healthy
|
||||
val isHandshakeFailure =
|
||||
activeTunnel.transportState is Tunnel.State.Up.HandshakeFailure
|
||||
|
||||
healthySinceMs = if (isHealthy) healthySinceMs ?: now else null
|
||||
val healthyDuration = healthySinceMs?.let { now - it } ?: 0L
|
||||
|
||||
if (!isHandshakeFailure) {
|
||||
failureCount = 0
|
||||
firstFailureTime = 0L
|
||||
}
|
||||
|
||||
// Fallback
|
||||
if (
|
||||
strategy.fallbackToIpv4Enabled &&
|
||||
isHandshakeFailure &&
|
||||
isUsingIpv6 &&
|
||||
!hasFallenBackOnThisNetwork
|
||||
) {
|
||||
if (failureCount == 0) firstFailureTime = now
|
||||
failureCount++
|
||||
|
||||
val failureDuration = now - firstFailureTime
|
||||
|
||||
Timber.d(
|
||||
"IPv6 strategy | Fallback check: failureCount=$failureCount duration=${failureDuration}ms"
|
||||
)
|
||||
|
||||
if (
|
||||
failureCount >= IPV4_FALLBACK_FAILURE_COUNT &&
|
||||
failureDuration >= IPV4_FALLBACK_FAILURE_DURATION
|
||||
) {
|
||||
hasFallenBackOnThisNetwork = true
|
||||
ipv6Bad = true
|
||||
|
||||
Timber.d("Fallback to IPv4 triggered on $newKey (marking IPv6 bad)")
|
||||
|
||||
reconcilePeers(
|
||||
tunnelId = tunnelId,
|
||||
handle = handle,
|
||||
mode = mode,
|
||||
reason = PeerUpdateReason.IPV4_FALLBACK,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery
|
||||
if (
|
||||
strategy.recoveryEnabled &&
|
||||
!isUsingIpv6 &&
|
||||
!hasRecoveredOnThisNetwork &&
|
||||
healthySinceMs != null &&
|
||||
stable.state.hasIpv6 &&
|
||||
!ipv6Bad
|
||||
) {
|
||||
Timber.d(
|
||||
"IPv6 strategy | Recovery check: healthy for ${healthyDuration}ms (need >= ${RECOVERY_STABILITY_WINDOW}ms)"
|
||||
)
|
||||
|
||||
if (healthyDuration >= RECOVERY_STABILITY_WINDOW) {
|
||||
hasRecoveredOnThisNetwork = true
|
||||
|
||||
Timber.d("Recovered to IPv6 on $newKey (healthy for ${healthyDuration}ms)")
|
||||
|
||||
reconcilePeers(
|
||||
tunnelId = tunnelId,
|
||||
handle = handle,
|
||||
mode = mode,
|
||||
reason = PeerUpdateReason.IPV6_RECOVERY,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun reconcilePeers(
|
||||
tunnelId: Int,
|
||||
handle: Int,
|
||||
mode: BackendMode,
|
||||
reason: PeerUpdateReason,
|
||||
) {
|
||||
val mutex = peerUpdateMutexes.getOrPut(tunnelId) { Mutex() }
|
||||
mutex.withLock {
|
||||
when (reason) {
|
||||
PeerUpdateReason.IPV4_FALLBACK -> {
|
||||
updateActiveTunnel(tunnelId) { it.copy(isFallenBackToIpv4ForNetwork = true) }
|
||||
}
|
||||
PeerUpdateReason.IPV6_RECOVERY,
|
||||
PeerUpdateReason.NETWORK_CHANGE_RESET -> {
|
||||
updateActiveTunnel(tunnelId) { it.copy(isFallenBackToIpv4ForNetwork = false) }
|
||||
if (reason == PeerUpdateReason.NETWORK_CHANGE_RESET) return
|
||||
}
|
||||
PeerUpdateReason.DDNS_CHECK -> Unit
|
||||
}
|
||||
|
||||
val updatedActiveTunnel = _status.value.activeTunnels[tunnelId] ?: return
|
||||
val tunnel = updatedActiveTunnel.tunnel ?: return
|
||||
|
||||
val results = resolvePeers(mode)
|
||||
if (results.isEmpty()) return
|
||||
|
||||
val networkHasIpv6 = stableNetworkEngine.stableState.value?.state?.hasIpv6 == true
|
||||
val preferIpv6 =
|
||||
tunnel.ipStrategy is Tunnel.IpStrategy.PreferIpv6 &&
|
||||
networkHasIpv6 &&
|
||||
!updatedActiveTunnel.isFallenBackToIpv4ForNetwork
|
||||
|
||||
val activeConfig =
|
||||
try {
|
||||
engine.getActiveConfig(handle, mode)
|
||||
} catch (t: Throwable) {
|
||||
Timber.w(t, "UAPI query failed during peer reconciliation")
|
||||
return
|
||||
} ?: return
|
||||
|
||||
val mismatches = findEndpointMismatches(results, activeConfig, preferIpv6)
|
||||
|
||||
Timber.d("Reconciliation complete for $reason. Mismatches found: ${mismatches.size}")
|
||||
|
||||
if (mismatches.isNotEmpty()) {
|
||||
Timber.i("Updating peers due to $reason: $mismatches")
|
||||
|
||||
val resolvedPeers = mode.config.buildResolvedPeers(mismatches)
|
||||
|
||||
try {
|
||||
engine.updatePeers(handle, mode, resolvedPeers)
|
||||
markPeerUpdate(tunnelId)
|
||||
|
||||
when (reason) {
|
||||
PeerUpdateReason.IPV4_FALLBACK ->
|
||||
_events.emit(TunnelEvent.FallbackToIpv4(tunnelId))
|
||||
PeerUpdateReason.IPV6_RECOVERY ->
|
||||
_events.emit(TunnelEvent.RecoveredToIpv6(tunnelId))
|
||||
PeerUpdateReason.DDNS_CHECK ->
|
||||
_events.emit(
|
||||
TunnelEvent.DynamicDnsUpdate(tunnelId, mismatches.keys.toList())
|
||||
)
|
||||
PeerUpdateReason.NETWORK_CHANGE_RESET -> Unit
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "Failed to apply peer updates to WireGuard engine")
|
||||
}
|
||||
} else {
|
||||
Timber.d("No mismatches found, skipping event emission.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DDNS_MIN_CHECK_INTERVAL = 30_000L
|
||||
private const val DDNS_FAILURE_WINDOW = 15_000L
|
||||
private const val DDNS_STABILITY_WINDOW = 15_000L
|
||||
private const val IPV4_FALLBACK_FAILURE_COUNT = 4
|
||||
private const val IPV4_FALLBACK_FAILURE_DURATION = 10_000L
|
||||
private const val RECOVERY_STABILITY_WINDOW = 5_000L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,14 @@ package com.zaneschepke.tunnel.backend
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.EngineStartResult
|
||||
import com.zaneschepke.tunnel.state.EngineState
|
||||
import com.zaneschepke.tunnel.state.NativeTunnelStatus
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ActiveConfig
|
||||
import com.zaneschepke.wireguardautotunnel.parser.PeerSection
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal interface TunnelEngine {
|
||||
|
||||
val status: Flow<NativeTunnelStatus>
|
||||
val state: Flow<EngineState>
|
||||
suspend fun start(tunnel: Tunnel, mode: BackendMode): EngineStartResult
|
||||
|
||||
fun start(tunnel: Tunnel, mode: BackendMode): EngineStartResult
|
||||
|
||||
fun stop(handle: Int, mode: BackendMode)
|
||||
suspend fun stop(handle: Int, mode: BackendMode)
|
||||
|
||||
suspend fun updatePeers(handle: Int, mode: BackendMode, peers: List<PeerSection>)
|
||||
|
||||
|
||||
@@ -9,32 +9,32 @@ import com.zaneschepke.tunnel.model.ProxyConfig
|
||||
import com.zaneschepke.tunnel.service.VpnService
|
||||
import com.zaneschepke.tunnel.service.VpnService.Companion.HEV_BRIDGE_TRAFFIC_TAG
|
||||
import com.zaneschepke.tunnel.state.EngineStartResult
|
||||
import com.zaneschepke.tunnel.state.EngineState
|
||||
import com.zaneschepke.tunnel.state.NativeTunnelStatus
|
||||
import com.zaneschepke.tunnel.util.BackendException
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ActiveConfig
|
||||
import com.zaneschepke.wireguardautotunnel.parser.Config
|
||||
import com.zaneschepke.wireguardautotunnel.parser.PeerSection
|
||||
import java.io.IOException
|
||||
import java.net.DatagramSocket
|
||||
import java.net.ServerSocket
|
||||
import java.net.SocketException
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
internal class WireGuardTunnelEngine(
|
||||
private val serviceHolder: ServiceHolder,
|
||||
stateProvider: EngineStateProvider,
|
||||
) : TunnelEngine {
|
||||
internal class WireGuardTunnelEngine(private val serviceHolder: ServiceHolder) : TunnelEngine {
|
||||
|
||||
override val status: Flow<NativeTunnelStatus> = serviceHolder.nativeStatuses
|
||||
|
||||
override val state: Flow<EngineState> = stateProvider.state
|
||||
|
||||
override fun start(tunnel: Tunnel, mode: BackendMode): EngineStartResult {
|
||||
override suspend fun start(tunnel: Tunnel, mode: BackendMode): EngineStartResult {
|
||||
|
||||
val ifName = WGT_INTERFACE_PREFIX + tunnel.id
|
||||
|
||||
val (config, removedPeerEndpoint) = buildConfig(mode)
|
||||
|
||||
// guard against static listenPort issues
|
||||
val listenPort = config.`interface`.listenPort
|
||||
if (listenPort != null) {
|
||||
waitForUdpPortAvailable(listenPort)
|
||||
}
|
||||
|
||||
val handle =
|
||||
when (mode) {
|
||||
is BackendMode.Proxy.KillSwitchPrimary -> {
|
||||
@@ -154,7 +154,7 @@ internal class WireGuardTunnelEngine(
|
||||
return peer.copy(endpoint = null)
|
||||
}
|
||||
|
||||
override fun stop(handle: Int, mode: BackendMode) {
|
||||
override suspend fun stop(handle: Int, mode: BackendMode) {
|
||||
when (mode) {
|
||||
is BackendMode.Proxy.Standard -> stopProxyTunnel(handle)
|
||||
is BackendMode.Vpn -> stopVpnTunnel(handle)
|
||||
@@ -162,7 +162,7 @@ internal class WireGuardTunnelEngine(
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopKillSwitchPrimaryTunnel(handle: Int) {
|
||||
private suspend fun stopKillSwitchPrimaryTunnel(handle: Int) {
|
||||
ProxyBackend.awgTurnProxyTunnelOff(handle)
|
||||
val service = serviceHolder.getVpnService()
|
||||
service.stopHevSocks5Bridge()
|
||||
@@ -176,7 +176,7 @@ internal class WireGuardTunnelEngine(
|
||||
VpnBackend.awgTurnOff(handle)
|
||||
}
|
||||
|
||||
private fun startVpnTunnel(tunnel: Tunnel, ifName: String, config: Config): Int {
|
||||
private suspend fun startVpnTunnel(tunnel: Tunnel, ifName: String, config: Config): Int {
|
||||
|
||||
val service = serviceHolder.getVpnService()
|
||||
|
||||
@@ -197,7 +197,29 @@ internal class WireGuardTunnelEngine(
|
||||
return handle
|
||||
}
|
||||
|
||||
private fun startProxyTunnel(
|
||||
private fun isUdpPortAvailable(port: Int): Boolean {
|
||||
if (port !in 1..65_535) return false
|
||||
return try {
|
||||
DatagramSocket(port).use { true }
|
||||
} catch (_: SocketException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(BackendException::class)
|
||||
private suspend fun waitForUdpPortAvailable(port: Int, timeoutMs: Long = 3000L) {
|
||||
val deadline = System.currentTimeMillis() + timeoutMs
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
if (isUdpPortAvailable(port)) return
|
||||
delay(50.milliseconds)
|
||||
}
|
||||
throw BackendException.ListenPortUnavailable(
|
||||
"UDP ListenPort $port is still in use after waiting $timeoutMs ms",
|
||||
port,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun startProxyTunnel(
|
||||
ifName: String,
|
||||
config: Config,
|
||||
proxyConfig: ProxyConfig,
|
||||
|
||||
@@ -2,10 +2,7 @@ package com.zaneschepke.tunnel.di
|
||||
|
||||
import com.zaneschepke.tunnel.TunnelLibraryInitializer
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.backend.DefaultEngineStateProvider
|
||||
import com.zaneschepke.tunnel.backend.EngineStateProvider
|
||||
import com.zaneschepke.tunnel.backend.ServiceHolder
|
||||
import com.zaneschepke.tunnel.backend.TunnelActor
|
||||
import com.zaneschepke.tunnel.backend.TunnelBackend
|
||||
import com.zaneschepke.tunnel.backend.TunnelEngine
|
||||
import com.zaneschepke.tunnel.backend.WireGuardTunnelEngine
|
||||
@@ -23,10 +20,8 @@ val tunnelModule = module {
|
||||
|
||||
single { ServiceHolder(androidContext()) }
|
||||
// expect networkMonitor and NotificationProvider to be available to koin from app
|
||||
single<Backend> { TunnelBackend(get(named(CoroutineScopes.IO_SCOPE)), get(), get()) }
|
||||
single<TunnelActor> { TunnelActor(get(named(CoroutineScopes.IO_SCOPE)), get(), get()) }
|
||||
single<EngineStateProvider> { DefaultEngineStateProvider() }
|
||||
single<TunnelEngine> { WireGuardTunnelEngine(get(), get()) }
|
||||
single<Backend> { TunnelBackend(get(named(CoroutineScopes.IO_SCOPE)), get(), get(), get()) }
|
||||
single<TunnelEngine> { WireGuardTunnelEngine(get()) }
|
||||
}
|
||||
|
||||
enum class CoroutineScopes {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.zaneschepke.tunnel.event
|
||||
|
||||
import com.zaneschepke.tunnel.model.DnsBootstrapResult
|
||||
import com.zaneschepke.tunnel.model.PublicKey
|
||||
import com.zaneschepke.tunnel.model.TunnelCommand
|
||||
import com.zaneschepke.tunnel.state.BootstrapState
|
||||
import com.zaneschepke.tunnel.state.EngineStartResult
|
||||
import com.zaneschepke.tunnel.state.NativeTunnelStatus
|
||||
import com.zaneschepke.tunnel.state.RuntimeDnsConfig
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ActiveConfig
|
||||
import com.zaneschepke.wireguardautotunnel.parser.PeerSection
|
||||
|
||||
sealed class ActorEvent {
|
||||
data class EngineStatus(val status: NativeTunnelStatus) : ActorEvent()
|
||||
|
||||
data class TunnelStarted(val result: EngineStartResult, val cmd: TunnelCommand.Start) :
|
||||
ActorEvent()
|
||||
|
||||
data class TunnelStopped(val tunnelId: Int, val handle: Int) : ActorEvent()
|
||||
|
||||
data class PeersUpdated(
|
||||
val tunnelId: Int,
|
||||
val peers: List<PeerSection>,
|
||||
val preferIpv6: Boolean,
|
||||
) : ActorEvent()
|
||||
|
||||
data class BootstrapConfigUpdated(val config: RuntimeDnsConfig) : ActorEvent()
|
||||
|
||||
data class ResolvedPeersApplied(
|
||||
val tunnelId: Int,
|
||||
val cache: Map<PublicKey, DnsBootstrapResult>,
|
||||
val peers: List<PeerSection>,
|
||||
) : ActorEvent()
|
||||
|
||||
data class ActiveConfigUpdated(val tunnelId: Int, val activeConfig: ActiveConfig?) :
|
||||
ActorEvent()
|
||||
|
||||
data class BootstrapStateChanged(val tunnelId: Int, val bootstrapState: BootstrapState) :
|
||||
ActorEvent()
|
||||
|
||||
data class KillSwitchStateChanged(val enabled: Boolean) : ActorEvent()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.zaneschepke.tunnel.model
|
||||
|
||||
typealias PublicKey = String
|
||||
|
||||
typealias Host = String
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.tunnel.model
|
||||
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.parser.PeerSection
|
||||
|
||||
typealias PublicKey = String
|
||||
|
||||
data class RunningTunnel(
|
||||
val handle: Int,
|
||||
val interfaceName: String,
|
||||
val tunnel: Tunnel,
|
||||
val mode: BackendMode,
|
||||
val currentPreferIpv6: Boolean = false,
|
||||
val resolvedPeers: List<PeerSection>? = null,
|
||||
val peerBootstrapCache: Map<PublicKey, DnsBootstrapResult> = emptyMap(),
|
||||
)
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.zaneschepke.tunnel.model
|
||||
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.state.BootstrapState
|
||||
import com.zaneschepke.tunnel.state.RuntimeDnsConfig
|
||||
import com.zaneschepke.wireguardautotunnel.parser.PeerSection
|
||||
|
||||
sealed class TunnelCommand {
|
||||
|
||||
data class Start(val tunnel: Tunnel, val mode: BackendMode) : TunnelCommand()
|
||||
|
||||
data class Stop(val tunnelId: Int) : TunnelCommand()
|
||||
|
||||
data class UpdateActiveConfig(val tunnelId: Int) : TunnelCommand()
|
||||
|
||||
data class UpdateKillSwitch(val enabled: Boolean) : TunnelCommand()
|
||||
|
||||
data class ApplyResolvedPeers(
|
||||
val tunnelId: Int,
|
||||
val cache: Map<PublicKey, DnsBootstrapResult>,
|
||||
val peers: List<PeerSection>,
|
||||
) : TunnelCommand()
|
||||
|
||||
data class UpdatePeers(val tunnelId: Int, val preferIpv6: Boolean) : TunnelCommand()
|
||||
|
||||
data class SetBootstrapState(val tunnelId: Int, val state: BootstrapState) : TunnelCommand()
|
||||
|
||||
data class SetBootstrapConfig(val config: RuntimeDnsConfig) : TunnelCommand()
|
||||
|
||||
data class RunHook(val tunnelId: Int, val phase: Phase, val cmds: List<String>?) :
|
||||
TunnelCommand() {
|
||||
enum class Phase {
|
||||
PreUp,
|
||||
PostUp,
|
||||
PreDown,
|
||||
PostDown,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import androidx.lifecycle.LifecycleService
|
||||
import com.zaneschepke.tunnel.backend.Backend
|
||||
import com.zaneschepke.tunnel.backend.ServiceHolder
|
||||
import com.zaneschepke.tunnel.backend.ServiceHolder.Companion.alwaysOnCallback
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -16,15 +15,14 @@ class TunnelService : LifecycleService() {
|
||||
private val serviceHolder: ServiceHolder by inject(ServiceHolder::class.java)
|
||||
|
||||
override fun onCreate() {
|
||||
ServiceHolder.tunnelService.complete(this)
|
||||
serviceHolder.ensureNativeCallbacksRegistered()
|
||||
serviceHolder.set(this)
|
||||
launchForegroundNotification()
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
ServiceHolder.tunnelService.complete(this)
|
||||
serviceHolder.set(this)
|
||||
launchForegroundNotification()
|
||||
|
||||
// Service restarted by system, reuse always-on VPN callback
|
||||
@@ -41,10 +39,8 @@ class TunnelService : LifecycleService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
backend.emergencyStopAllOfTypeSync(BackendMode.Proxy.Standard::class)
|
||||
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
serviceHolder.clear(this)
|
||||
serviceHolder.signalTunnelServiceDestroyed()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -15,14 +15,13 @@ import com.zaneschepke.tunnel.backend.KillSwitch
|
||||
import com.zaneschepke.tunnel.backend.ServiceHolder
|
||||
import com.zaneschepke.tunnel.backend.ServiceHolder.Companion.DEFAULT_MTU
|
||||
import com.zaneschepke.tunnel.backend.ServiceHolder.Companion.alwaysOnCallback
|
||||
import com.zaneschepke.tunnel.backend.ServiceHolder.Companion.vpnService
|
||||
import com.zaneschepke.tunnel.backend.SocketProtector
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.model.KillSwitchConfig
|
||||
import com.zaneschepke.tunnel.util.parseDns
|
||||
import com.zaneschepke.tunnel.util.parseInetNetwork
|
||||
import com.zaneschepke.wireguardautotunnel.parser.Config
|
||||
import java.io.IOException
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -47,10 +46,8 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
|
||||
get() = Builder()
|
||||
|
||||
override fun onCreate() {
|
||||
vpnService.complete(this)
|
||||
// We call this for all backend modes as it is shared for bootstrapping bypass
|
||||
serviceHolder.set(this)
|
||||
ProxyBackend.setSocketProtector(this)
|
||||
serviceHolder.ensureNativeCallbacksRegistered()
|
||||
launchForegroundNotification()
|
||||
super.onCreate()
|
||||
}
|
||||
@@ -66,30 +63,21 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
|
||||
|
||||
override fun onDestroy() {
|
||||
Timber.d("VpnService destroyed")
|
||||
|
||||
try {
|
||||
ProxyBackend.setSocketProtector(null)
|
||||
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
|
||||
disableKillSwitch()
|
||||
hevBridgeJob?.cancel()
|
||||
|
||||
serviceScope.cancel()
|
||||
|
||||
backend.emergencyStopAllOfTypeSync(BackendMode.Vpn::class)
|
||||
backend.emergencyStopAllOfTypeSync(BackendMode.Proxy.KillSwitchPrimary::class)
|
||||
|
||||
stopHevSocks5Bridge()
|
||||
|
||||
serviceHolder.clear(this)
|
||||
} finally {
|
||||
serviceHolder.signalVpnServiceDestroyed()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
vpnService.complete(this)
|
||||
serviceHolder.set(this)
|
||||
launchForegroundNotification()
|
||||
|
||||
// Service restarted by system or Always-on VPN started
|
||||
@@ -141,7 +129,7 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
|
||||
if (attempt % 5 == 0) {
|
||||
Timber.d("SOCKS5 not ready yet, retrying...")
|
||||
}
|
||||
delay(300)
|
||||
delay(300.milliseconds)
|
||||
}
|
||||
}
|
||||
Timber.e("Timed out waiting for SOCKS5 proxy to be ready")
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package com.zaneschepke.tunnel.state
|
||||
|
||||
import com.zaneschepke.pinger.model.PingStats
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.wireguardautotunnel.parser.ActiveConfig
|
||||
|
||||
data class ActiveTunnel(
|
||||
val tunnel: Tunnel? = null,
|
||||
val mode: BackendMode? = null,
|
||||
val transportState: Tunnel.State = Tunnel.State.Down,
|
||||
val bootstrapState: BootstrapState = BootstrapState.None,
|
||||
val lastStateChangeMs: Long = System.currentTimeMillis(),
|
||||
val lastHealthChangeMs: Long = 0L,
|
||||
val interfaceName: String? = null,
|
||||
val activeConfig: ActiveConfig? = null,
|
||||
val pingStats: PingStats? = null,
|
||||
val mode: BackendMode? = null,
|
||||
val uptime: Long? = null,
|
||||
val lastPeerUpdateMs: Long = 0L,
|
||||
val isFallenBackToIpv4ForNetwork: Boolean = false,
|
||||
) {
|
||||
val isPeerUpdating: Boolean
|
||||
get() = System.currentTimeMillis() - lastPeerUpdateMs < PEER_UPDATE_GRACE_MS
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.zaneschepke.tunnel.state
|
||||
|
||||
data class ActorState(
|
||||
val byTunnelId: Map<Int, TunnelRuntimeState>,
|
||||
val byHandle: Map<Int, Int>,
|
||||
val killSwitchEnabled: Boolean = false,
|
||||
val dnsConfig: RuntimeDnsConfig = RuntimeDnsConfig(),
|
||||
)
|
||||
@@ -6,4 +6,5 @@ data class BackendStatus(
|
||||
val killSwitch: KillSwitchState = KillSwitchState(),
|
||||
val activeTunnels: Map<Int, ActiveTunnel> = emptyMap(),
|
||||
val dnsMode: DnsBoostrapMode = DnsBoostrapMode.System,
|
||||
val runtimeDnsConfig: RuntimeDnsConfig = RuntimeDnsConfig(),
|
||||
)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.zaneschepke.tunnel.state
|
||||
|
||||
internal data class EngineState(
|
||||
val tunnels: Map<Int, TunnelRuntimeState> = emptyMap(),
|
||||
val killSwitch: KillSwitchState = KillSwitchState(),
|
||||
val dns: DnsState = DnsState(),
|
||||
)
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.zaneschepke.tunnel.state
|
||||
|
||||
data class NativeTunnelStatus(val handle: Int, val code: NativeTunnelStatusCode) {
|
||||
enum class NativeTunnelStatusCode(val code: Int) {
|
||||
HEALTHY(0),
|
||||
HANDSHAKE_FAILURE(1),
|
||||
STOPPED(99);
|
||||
|
||||
companion object {
|
||||
fun from(code: Int): NativeTunnelStatusCode? {
|
||||
return entries.firstOrNull { it.code == code }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.zaneschepke.tunnel.state
|
||||
|
||||
import com.zaneschepke.tunnel.model.RunningTunnel
|
||||
|
||||
data class TunnelRuntimeState(val running: RunningTunnel, val active: ActiveTunnel)
|
||||
@@ -9,4 +9,6 @@ sealed class BackendException : Exception() {
|
||||
class Socks5PortUnavailable(override val message: String, val port: Int) : BackendException()
|
||||
|
||||
class HttpPortUnavailable(override val message: String, val port: Int) : BackendException()
|
||||
|
||||
class ListenPortUnavailable(override val message: String, val port: Int) : BackendException()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.zaneschepke.tunnel.util
|
||||
|
||||
import android.os.Build
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.model.DnsBootstrapResult
|
||||
import com.zaneschepke.tunnel.model.DnsConfig
|
||||
import com.zaneschepke.tunnel.model.RunningTunnel
|
||||
import com.zaneschepke.tunnel.model.Host
|
||||
import com.zaneschepke.tunnel.model.PublicKey
|
||||
import com.zaneschepke.tunnel.state.BackendStatus
|
||||
import com.zaneschepke.wireguardautotunnel.parser.Config
|
||||
import com.zaneschepke.wireguardautotunnel.parser.PeerSection
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
@@ -63,27 +67,37 @@ internal fun String.parseDns(): DnsConfig {
|
||||
return DnsConfig(servers, domains)
|
||||
}
|
||||
|
||||
internal fun RunningTunnel.buildResolvedPeers(preferIpv6: Boolean): List<PeerSection> {
|
||||
|
||||
fun selectIp(cache: DnsBootstrapResult, preferIpv6: Boolean): String? {
|
||||
|
||||
val ipv4 = cache.ipv4.firstOrNull()
|
||||
val ipv6 = cache.ipv6.firstOrNull()
|
||||
|
||||
return when {
|
||||
preferIpv6 -> ipv6 ?: ipv4
|
||||
else -> ipv4 ?: ipv6
|
||||
}
|
||||
}
|
||||
|
||||
return mode.config.peers.map { peer ->
|
||||
val endpoint = peer.endpoint ?: return@map peer
|
||||
val port = endpoint.substringAfterLast(":")
|
||||
|
||||
val dnsCache = peerBootstrapCache[peer.publicKey] ?: return@map peer
|
||||
|
||||
val selectedIp = selectIp(cache = dnsCache, preferIpv6 = preferIpv6) ?: return@map peer
|
||||
|
||||
peer.copy(endpoint = "$selectedIp:$port")
|
||||
internal fun Config.buildResolvedPeers(hostMap: Map<PublicKey, Host>): List<PeerSection> {
|
||||
return this.peers.map { peer ->
|
||||
val updatedHost = hostMap[peer.publicKey] ?: return@map peer
|
||||
val port = peer.endpoint?.substringAfterLast(":") ?: return@map peer
|
||||
peer.copy(endpoint = "$updatedHost:$port")
|
||||
}
|
||||
}
|
||||
|
||||
fun Map<PublicKey, DnsBootstrapResult>.toHostMap(preferIpv6: Boolean): Map<PublicKey, Host> =
|
||||
mapNotNull { (pubKey, result) ->
|
||||
val host =
|
||||
if (preferIpv6) {
|
||||
result.ipv6.firstOrNull() ?: result.ipv4.firstOrNull()
|
||||
} else {
|
||||
result.ipv4.firstOrNull() ?: result.ipv6.firstOrNull()
|
||||
}
|
||||
host?.let { pubKey to it }
|
||||
}
|
||||
.toMap()
|
||||
|
||||
fun BackendStatus.isLastTunnelOfServiceType(tunnelId: Int): Boolean {
|
||||
val mode = activeTunnels[tunnelId]?.mode ?: return false
|
||||
return when (mode) {
|
||||
is BackendMode.Vpn,
|
||||
is BackendMode.Proxy.KillSwitchPrimary -> {
|
||||
activeTunnels.values.count {
|
||||
it.mode is BackendMode.Vpn || it.mode is BackendMode.Proxy.KillSwitchPrimary
|
||||
} == 1
|
||||
}
|
||||
is BackendMode.Proxy.Standard -> {
|
||||
activeTunnels.values.count { it.mode is BackendMode.Proxy.Standard } == 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"net"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/amnezia-vpn/amneziawg-go/conn"
|
||||
"github.com/amnezia-vpn/amneziawg-go/device"
|
||||
@@ -25,6 +24,7 @@ var (
|
||||
cancelFuncs map[int32]context.CancelFunc
|
||||
tag string
|
||||
virtualTunnelHandles map[int32]*wireproxyawg.VirtualTun
|
||||
tunnelMu sync.RWMutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -35,7 +35,6 @@ func init() {
|
||||
|
||||
//export awgStartProxy
|
||||
func awgStartProxy(interfaceName string, config string, uapiPath string, bypass int32) int32 {
|
||||
|
||||
conf, err := wireproxyawg.ParseConfigString(config)
|
||||
if err != nil {
|
||||
shared.LogError(tag, "Invalid config file", err)
|
||||
@@ -49,22 +48,32 @@ func awgStartProxy(interfaceName string, config string, uapiPath string, bypass
|
||||
}
|
||||
|
||||
setting, err := wireproxyawg.CreateIPCRequest(conf.Device, false)
|
||||
|
||||
if err != nil {
|
||||
shared.LogError(tag, "Create IPC request failed", err)
|
||||
shared.LogError(tag, "Create IPC request failed")
|
||||
shared.ReleaseHandle(handle)
|
||||
return -1
|
||||
}
|
||||
|
||||
tun, tnet, err := netstack.CreateNetTUN(setting.DeviceAddr, setting.DNS, setting.MTU)
|
||||
tun, tnet, err := netstack.CreateNetTUN(
|
||||
setting.DeviceAddr,
|
||||
setting.DNS,
|
||||
setting.MTU,
|
||||
)
|
||||
if err != nil {
|
||||
shared.LogError(tag, "Create TUN failed", err)
|
||||
shared.LogError(tag, "Create TUN failed")
|
||||
shared.ReleaseHandle(handle)
|
||||
return -1
|
||||
}
|
||||
|
||||
name, err := tun.Name()
|
||||
if err != nil {
|
||||
shared.LogError(tag, "Failed to get TUN name: %v", err)
|
||||
shared.ReleaseHandle(handle)
|
||||
tun.Close()
|
||||
return -1
|
||||
}
|
||||
|
||||
var bind conn.Bind
|
||||
|
||||
if bypass == 1 {
|
||||
bind = conn.NewStdNetBindWithControl(protectControlFunc)
|
||||
} else {
|
||||
@@ -75,28 +84,34 @@ func awgStartProxy(interfaceName string, config string, uapiPath string, bypass
|
||||
go C.awgNotifyStatus(C.int32_t(handle), C.int32_t(code))
|
||||
}
|
||||
|
||||
dev := device.NewDevice(tun, bind, shared.NewLogger("Tun/"+interfaceName), statusCB)
|
||||
dev := device.NewDevice(
|
||||
tun,
|
||||
bind,
|
||||
shared.NewLogger("Tun/"+interfaceName),
|
||||
statusCB,
|
||||
)
|
||||
|
||||
dev.DisableSomeRoamingForBrokenMobileSemantics()
|
||||
|
||||
err = dev.IpcSet(setting.IpcRequest)
|
||||
|
||||
if err != nil {
|
||||
shared.LogError(tag, "Ipc setting failed", err)
|
||||
if err = dev.IpcSet(setting.IpcRequest); err != nil {
|
||||
shared.LogError(tag, "Ipc setting failed")
|
||||
shared.ReleaseHandle(handle)
|
||||
dev.Close()
|
||||
return -1
|
||||
}
|
||||
|
||||
uapiFile, err := ipc.UAPIOpen(uapiPath, name)
|
||||
|
||||
var uapi net.Listener
|
||||
uapiFile, uapiErr := ipc.UAPIOpen(uapiPath, name)
|
||||
|
||||
if err != nil {
|
||||
shared.LogError(tag, "UAPIOpen: %v", err)
|
||||
if uapiErr != nil {
|
||||
shared.LogError(tag, "UAPIOpen: %v", uapiErr)
|
||||
} else {
|
||||
uapi, err = ipc.UAPIListen(uapiPath, name, uapiFile)
|
||||
|
||||
if err != nil {
|
||||
uapiFile.Close()
|
||||
shared.LogError(tag, "UAPIListen: %v", err)
|
||||
uapiFile.Close()
|
||||
uapi = nil
|
||||
} else {
|
||||
go func() {
|
||||
for {
|
||||
@@ -110,10 +125,18 @@ func awgStartProxy(interfaceName string, config string, uapiPath string, bypass
|
||||
}
|
||||
}
|
||||
|
||||
err = dev.Up()
|
||||
if err != nil {
|
||||
shared.LogError(tag, "Failed to bring up device", err)
|
||||
uapiFile.Close()
|
||||
if err = dev.Up(); err != nil {
|
||||
shared.LogError(tag, "Failed to bring up device")
|
||||
|
||||
if uapiFile != nil {
|
||||
uapiFile.Close()
|
||||
}
|
||||
|
||||
if uapi != nil {
|
||||
uapi.Close()
|
||||
}
|
||||
|
||||
shared.ReleaseHandle(handle)
|
||||
dev.Close()
|
||||
return -1
|
||||
}
|
||||
@@ -128,15 +151,14 @@ func awgStartProxy(interfaceName string, config string, uapiPath string, bypass
|
||||
PingRecordLock: new(sync.Mutex),
|
||||
}
|
||||
|
||||
virtualTunnelHandles[handle] = virtualTun
|
||||
|
||||
// Create cancellable context
|
||||
tunnelCtx, tunnelCancel := context.WithCancel(context.Background())
|
||||
cancelFuncs[handle] = tunnelCancel
|
||||
|
||||
// Spawn all routines with context
|
||||
tunnelMu.Lock()
|
||||
virtualTunnelHandles[handle] = virtualTun
|
||||
cancelFuncs[handle] = tunnelCancel
|
||||
tunnelMu.Unlock()
|
||||
|
||||
for _, spawner := range conf.Routines {
|
||||
shared.LogDebug(tag, "Spawning routine..")
|
||||
go func(s wireproxyawg.RoutineSpawner) {
|
||||
if err := s.SpawnRoutine(tunnelCtx, virtualTun); err != nil {
|
||||
shared.LogError(tag, "Routine failed: %v", err)
|
||||
@@ -144,13 +166,16 @@ func awgStartProxy(interfaceName string, config string, uapiPath string, bypass
|
||||
}(spawner)
|
||||
}
|
||||
|
||||
shared.LogDebug(tag, "Done starting proxy and tunnel")
|
||||
shared.LogDebug(tag, "Done starting proxy and tunnel for handle %d", handle)
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
//export awgUpdateProxyTunnelPeers
|
||||
func awgUpdateProxyTunnelPeers(tunnelHandle int32, settings string) int32 {
|
||||
tunnelMu.RLock()
|
||||
handle, ok := virtualTunnelHandles[tunnelHandle]
|
||||
tunnelMu.RUnlock()
|
||||
if !ok {
|
||||
shared.LogError(tag, "Tunnel is not up")
|
||||
return -1
|
||||
@@ -180,7 +205,9 @@ func awgUpdateProxyTunnelPeers(tunnelHandle int32, settings string) int32 {
|
||||
|
||||
//export awgGetProxyConfig
|
||||
func awgGetProxyConfig(tunnelHandle int32) *C.char {
|
||||
tunnelMu.RLock()
|
||||
handle, ok := virtualTunnelHandles[tunnelHandle]
|
||||
tunnelMu.RUnlock()
|
||||
if !ok {
|
||||
shared.LogError(tag, "Tunnel is not up")
|
||||
return nil
|
||||
@@ -212,20 +239,38 @@ func protectControlFunc(network, address string, c syscall.RawConn) error {
|
||||
|
||||
//export awgTurnProxyTunnelOff
|
||||
func awgTurnProxyTunnelOff(virtualTunnelHandle int32) {
|
||||
|
||||
tunnelMu.Lock()
|
||||
|
||||
virtualTun, ok := virtualTunnelHandles[virtualTunnelHandle]
|
||||
if !ok {
|
||||
shared.LogError(tag, "Tunnel handle %d not found", virtualTunnelHandle)
|
||||
tunnelMu.Unlock()
|
||||
|
||||
shared.LogError(
|
||||
tag,
|
||||
"Tunnel handle %d not found",
|
||||
virtualTunnelHandle,
|
||||
)
|
||||
return
|
||||
}
|
||||
shared.LogDebug(tag, "Tearing down tunnel %d", virtualTunnelHandle)
|
||||
|
||||
if cancel, exists := cancelFuncs[virtualTunnelHandle]; exists {
|
||||
cancel := cancelFuncs[virtualTunnelHandle]
|
||||
|
||||
delete(virtualTunnelHandles, virtualTunnelHandle)
|
||||
delete(cancelFuncs, virtualTunnelHandle)
|
||||
|
||||
tunnelMu.Unlock()
|
||||
|
||||
shared.LogDebug(
|
||||
tag,
|
||||
"Tearing down tunnel %d",
|
||||
virtualTunnelHandle,
|
||||
)
|
||||
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
delete(cancelFuncs, virtualTunnelHandle)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Close UAPI listener and underlying file
|
||||
if virtualTun.Uapi != nil {
|
||||
virtualTun.Uapi.Close()
|
||||
}
|
||||
@@ -234,12 +279,16 @@ func awgTurnProxyTunnelOff(virtualTunnelHandle int32) {
|
||||
virtualTun.Dev.Close()
|
||||
}
|
||||
|
||||
go C.awgNotifyStatus(
|
||||
shared.ReleaseHandle(virtualTunnelHandle)
|
||||
|
||||
C.awgNotifyStatus(
|
||||
C.int32_t(virtualTunnelHandle),
|
||||
C.int32_t(shared.StatusStop),
|
||||
)
|
||||
|
||||
delete(virtualTunnelHandles, virtualTunnelHandle)
|
||||
shared.ReleaseHandle(virtualTunnelHandle)
|
||||
shared.LogDebug(tag, "Tunnel %d fully closed (UAPI/Dev/Bind purged)", virtualTunnelHandle)
|
||||
shared.LogDebug(
|
||||
tag,
|
||||
"Tunnel handle %d fully closed",
|
||||
virtualTunnelHandle,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/amnezia-vpn/amneziawg-go/conn"
|
||||
"github.com/amnezia-vpn/amneziawg-go/device"
|
||||
@@ -31,6 +32,7 @@ type TunnelHandle struct {
|
||||
var (
|
||||
tag string
|
||||
tunnelHandles = make(map[int32]TunnelHandle)
|
||||
tunnelMu sync.RWMutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -40,7 +42,6 @@ func init() {
|
||||
//export awgTurnOn
|
||||
func awgTurnOn(interfaceName string, tunFd int32, settings string, uapiPath string) int32 {
|
||||
tunnel, name, err := tun.CreateUnmonitoredTUNFromFD(int(tunFd))
|
||||
|
||||
if err != nil {
|
||||
unix.Close(int(tunFd))
|
||||
shared.LogError(tag, "CreateUnmonitoredTUNFromFD: %v", err)
|
||||
@@ -50,52 +51,56 @@ func awgTurnOn(interfaceName string, tunFd int32, settings string, uapiPath stri
|
||||
conf, err := wireproxyawg.ParseConfigString(settings)
|
||||
if err != nil {
|
||||
shared.LogError(tag, "Invalid config file", err)
|
||||
unix.Close(int(tunFd))
|
||||
if tunnel != nil {
|
||||
tunnel.Close()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
shared.LogDebug(tag, "Creating device with domain blocking enabled: %v", conf.Device.DomainBlockingEnabled)
|
||||
|
||||
handle, err2 := shared.GenerateUniqueHandle()
|
||||
handle, err := shared.GenerateUniqueHandle()
|
||||
if err != nil {
|
||||
shared.LogError(tag, "Unable to generate handle: %v", err)
|
||||
if tunnel != nil {
|
||||
tunnel.Close()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
statusCB := func(code device.StatusCode) {
|
||||
go C.awgNotifyStatus(C.int32_t(handle), C.int32_t(code))
|
||||
}
|
||||
|
||||
tunDevice := device.NewDevice(tunnel, conn.NewStdNetBind(), shared.NewLogger("Tun/"+interfaceName), statusCB)
|
||||
|
||||
tunDevice.DisableSomeRoamingForBrokenMobileSemantics()
|
||||
|
||||
ipcRequest, err := wireproxyawg.CreateIPCRequest(conf.Device, false)
|
||||
if err != nil {
|
||||
shared.LogError(tag, "CreateIPCRequest: %v", err)
|
||||
unix.Close(int(tunFd))
|
||||
shared.ReleaseHandle(handle)
|
||||
tunDevice.Close()
|
||||
return -1
|
||||
}
|
||||
|
||||
err = tunDevice.IpcSet(ipcRequest.IpcRequest)
|
||||
if err != nil {
|
||||
unix.Close(int(tunFd))
|
||||
shared.ReleaseHandle(handle)
|
||||
shared.LogError(tag, "IpcSet: %v", err)
|
||||
shared.ReleaseHandle(handle)
|
||||
tunDevice.Close()
|
||||
return -1
|
||||
}
|
||||
|
||||
var uapi net.Listener
|
||||
|
||||
uapiFile, err := ipc.UAPIOpen(uapiPath, name)
|
||||
|
||||
if err != nil {
|
||||
shared.LogError(tag, "UAPIOpen: %v", err)
|
||||
uapiFile, uapiErr := ipc.UAPIOpen(uapiPath, name)
|
||||
if uapiErr != nil {
|
||||
shared.LogError(tag, "UAPIOpen: %v", uapiErr)
|
||||
uapiFile = nil
|
||||
} else {
|
||||
uapi, err = ipc.UAPIListen(uapiPath, name, uapiFile) // uapiPath as rootdir, name as interface
|
||||
uapi, err = ipc.UAPIListen(uapiPath, name, uapiFile)
|
||||
if err != nil {
|
||||
uapiFile.Close()
|
||||
shared.LogError(tag, "UAPIListen: %v", err)
|
||||
uapiFile.Close()
|
||||
uapiFile = nil
|
||||
uapi = nil
|
||||
} else {
|
||||
go func() {
|
||||
for {
|
||||
@@ -112,29 +117,33 @@ func awgTurnOn(interfaceName string, tunFd int32, settings string, uapiPath stri
|
||||
err = tunDevice.Up()
|
||||
if err != nil {
|
||||
shared.LogError(tag, "Unable to bring up device: %v", err)
|
||||
uapiFile.Close()
|
||||
shared.ReleaseHandle(handle)
|
||||
tunDevice.Close()
|
||||
return -1
|
||||
}
|
||||
shared.LogDebug(tag, "Device started")
|
||||
|
||||
if err2 != nil {
|
||||
shared.LogError(tag, "Unable to find empty handle", err2)
|
||||
uapiFile.Close()
|
||||
if uapiFile != nil {
|
||||
uapiFile.Close()
|
||||
}
|
||||
if uapi != nil {
|
||||
uapi.Close()
|
||||
}
|
||||
shared.ReleaseHandle(handle)
|
||||
tunDevice.Close()
|
||||
return -1
|
||||
}
|
||||
|
||||
tunnelHandles[handle] = TunnelHandle{device: tunDevice, uapi: uapi}
|
||||
shared.LogDebug(tag, "Tunnel started successfully for handle %d", handle)
|
||||
|
||||
tunnelMu.Lock()
|
||||
tunnelHandles[handle] = TunnelHandle{
|
||||
device: tunDevice,
|
||||
uapi: uapi,
|
||||
}
|
||||
tunnelMu.Unlock()
|
||||
return handle
|
||||
}
|
||||
|
||||
//export awgUpdateTunnelPeers
|
||||
func awgUpdateTunnelPeers(tunnelHandle int32, settings string) int32 {
|
||||
tunnelMu.RLock()
|
||||
handle, ok := tunnelHandles[tunnelHandle]
|
||||
tunnelMu.RUnlock()
|
||||
if !ok {
|
||||
shared.LogError(tag, "Tunnel is not up")
|
||||
return -1
|
||||
@@ -158,75 +167,107 @@ func awgUpdateTunnelPeers(tunnelHandle int32, settings string) int32 {
|
||||
return -1
|
||||
}
|
||||
|
||||
shared.LogDebug(tag, "Configuration updated successfully")
|
||||
shared.LogDebug(tag, "Configuration updated successfully with handle %d", handle)
|
||||
return 0
|
||||
}
|
||||
|
||||
//export awgTurnOff
|
||||
func awgTurnOff(tunnelHandle int32) {
|
||||
|
||||
tunnelMu.Lock()
|
||||
|
||||
handle, ok := tunnelHandles[tunnelHandle]
|
||||
if !ok {
|
||||
tunnelMu.Unlock()
|
||||
|
||||
shared.LogError(tag, "Tunnel is not up")
|
||||
return
|
||||
}
|
||||
|
||||
go C.awgNotifyStatus(
|
||||
C.int32_t(tunnelHandle),
|
||||
C.int32_t(shared.StatusStop),
|
||||
)
|
||||
|
||||
delete(tunnelHandles, tunnelHandle)
|
||||
|
||||
tunnelMu.Unlock()
|
||||
|
||||
if handle.uapi != nil {
|
||||
handle.uapi.Close()
|
||||
}
|
||||
handle.device.Close()
|
||||
|
||||
if handle.device != nil {
|
||||
handle.device.Close()
|
||||
}
|
||||
|
||||
shared.ReleaseHandle(tunnelHandle)
|
||||
|
||||
C.awgNotifyStatus(
|
||||
C.int32_t(tunnelHandle),
|
||||
C.int32_t(shared.StatusStop),
|
||||
)
|
||||
}
|
||||
|
||||
//export awgGetSocketV4
|
||||
func awgGetSocketV4(tunnelHandle int32) int32 {
|
||||
|
||||
tunnelMu.RLock()
|
||||
handle, ok := tunnelHandles[tunnelHandle]
|
||||
tunnelMu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
|
||||
bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
|
||||
if bind == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
fd, err := bind.PeekLookAtSocketFd4()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return int32(fd)
|
||||
}
|
||||
|
||||
//export awgGetSocketV6
|
||||
func awgGetSocketV6(tunnelHandle int32) int32 {
|
||||
|
||||
tunnelMu.RLock()
|
||||
handle, ok := tunnelHandles[tunnelHandle]
|
||||
tunnelMu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
|
||||
bind, _ := handle.device.Bind().(conn.PeekLookAtSocketFd)
|
||||
if bind == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
fd, err := bind.PeekLookAtSocketFd6()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return int32(fd)
|
||||
}
|
||||
|
||||
//export awgGetConfig
|
||||
func awgGetConfig(tunnelHandle int32) *C.char {
|
||||
|
||||
tunnelMu.RLock()
|
||||
handle, ok := tunnelHandles[tunnelHandle]
|
||||
tunnelMu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
settings, err := handle.device.IpcGet()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return C.CString(settings)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user