Compare commits

..

23 Commits

Author SHA1 Message Date
zaneschepke 6c3c6891eb chore: release v5.0.2 2026-06-14 01:59:14 -04:00
zaneschepke af1848f12d fix: legacy mode triggering location pings more often than necessary due to network security check
#1062
2026-06-13 12:01:20 -04:00
zaneschepke 96cffdfa7d fix: optimize tunnel status callback 2026-06-13 00:51:41 -04:00
zaneschepke afebd975ea fix: remove automatic active tunnel to top, add scrollbars 2026-06-12 02:33:28 -04:00
zaneschepke 588a2a18bd fix: use testnet ip during bootstrap phase 2026-06-11 18:45:28 -04:00
zaneschepke 221b38a119 chore: change proxy mode wording for consistency
#1268
2026-06-10 12:19:11 -04:00
zaneschepke 0008d8b9bb chore: update docs and fastlane metadata for latest features
closes #1268
2026-06-10 12:12:16 -04:00
zaneschepke 9f85638b9a chore: fix whitespace in changelog 2026-06-08 20:47:24 -04:00
zaneschepke fe54c9cd0e chore: release v5.0.1 2026-06-08 20:19:26 -04:00
zaneschepke 554499f9de refactor: add back tunnel endpoint ip to main screen
closes #1265
2026-06-08 20:03:47 -04:00
zaneschepke 12c9b52653 fix: split tunneling bug bypass bug, service shutdown handling
#1266
#1261
2026-06-08 17:21:40 -04:00
zaneschepke 03712a6c1d fix: custom text box input race 2026-06-08 02:29:21 -04:00
zaneschepke 5f03a97fcc fix: proxy settings ui state reset when config active 2026-06-08 02:04:31 -04:00
zaneschepke 6788b05fa0 fix: globals ui state bug while tunnel active 2026-06-08 01:57:38 -04:00
zaneschepke 9494853dee fix: use underlying network for system bootstrap
#1263
2026-06-07 23:22:17 -04:00
zaneschepke 01695c3286 fix: short description too long 2026-06-07 04:56:39 -04:00
zaneschepke bb6e45ed92 fix: localized short descriptions too long 2026-06-07 04:55:52 -04:00
zaneschepke 63e257f419 ci: fix release notes length 2026-06-07 04:32:10 -04:00
zaneschepke e145cd95e1 ci: remove invalid gws locale 2026-06-07 04:18:45 -04:00
zaneschepke 7e790acbfe ci: remove invalid nod locale 2026-06-07 04:05:55 -04:00
zaneschepke 334aaa1c2b ci: fix aab finding 2026-06-07 03:49:23 -04:00
zaneschepke 6201671dd0 fix: google play release pipeline 2026-06-07 03:34:44 -04:00
zaneschepke 517d90c3bf ci: fix fastlane pipeline 2026-06-07 03:23:18 -04:00
94 changed files with 539 additions and 531 deletions
+37 -33
View File
@@ -187,57 +187,61 @@ jobs:
repository: wgtunnel/fdroid
event-type: fdroid-update
build-google-aab:
if: >-
${{
github.event_name == 'push' ||
inputs.track != 'none'
}}
uses: ./.github/workflows/build-aab.yml
secrets: inherit
with:
build_type: release
flavor: google
publish-play:
if: ${{ github.event_name == 'push' || inputs.track != 'none' }}
name: Publish to Google Play
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
needs: build-google-aab
steps:
- uses: actions/checkout@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Here we need to decode keystore.jks from base64 string and place it
# in the folder specified in the release signing configuration
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v2.0
- name: Download AAB artifact
uses: actions/download-artifact@v8
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
name: google-play-aab
path: ${{ github.workspace }}/aab
# create keystore path for gradle to read
- name: Create keystore path env var
- name: Find exact AAB file path
id: find-aab
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
AAB_PATH=$(find "${{ github.workspace }}/aab" -name "*.aab" -type f | head -1)
if [ -z "$AAB_PATH" ]; then
echo "ERROR: No .aab file found after download!"
find "${{ github.workspace }}/aab" -type f
exit 1
fi
echo "Found AAB: $AAB_PATH"
echo "aab_path=$AAB_PATH" >> $GITHUB_OUTPUT
- name: Create service_account.json
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Deploy with fastlane
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2' # Not needed with a .ruby-version file
ruby-version: '3.4'
bundler-cache: true
- name: Distribute app to Prod track 🚀
- name: Upload to Google Play
run: |
track=${{ github.event_name == 'push' && 'production' || inputs.track }}
(cd ${{ github.workspace }} && bundle install && bundle exec fastlane $track --verbose)
bundle exec fastlane run upload_to_play_store \
track:"$track" \
aab:"${{ steps.find-aab.outputs.aab_path }}" \
json_key:"service_account.json" \
package_name:"com.zaneschepke.wireguardautotunnel" \
skip_upload_apk:true
+2 -1
View File
@@ -1,3 +1,4 @@
source "https://rubygems.org"
gem "fastlane"
gem "fastlane"
gem "multi_json"
+14 -17
View File
@@ -49,8 +49,8 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
## About
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling (on-demand VPN activation), while seamlessly supporting both protocols across app modes—including Kernel (for direct WireGuard kernel integration; AmneziaWG not supported), VPN (standard system-level tunneling), Lockdown (a custom kill switch for leak prevention), and Proxy (built-in HTTP/SOCKS5 forwarding)—for enhanced privacy, censorship resistance, and flexibility.
WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired by the official WireGuard Android app. It fills gaps in the official client by adding advanced features like auto-tunneling, AmneziaWG support, different app modes like **Lockdown** (a custom kill switch for leak prevention), and **Local Proxy** (expose a tunnel over a local SOCKS5/HTTP proxy server) for enhanced privacy, censorship resistance, and flexibility.
</div>
<div style="text-align: left;">
@@ -67,21 +67,18 @@ WG Tunnel is an alternative Android client for WireGuard and AmneziaWG, inspired
## Features
- **Tunnel Import Methods**: Easily add tunnels using .conf files, ZIP archives, manual entry, or QR code scanning.
- **Auto-Tunneling**: Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks.
- **Split Tunneling**: Flexible support for routing specific apps or traffic through the VPN.
- **WireGuard Modes**: Full compatibility with WireGuard in both kernel and userspace implementations.
- **AmneziaWG Integration**: Userspace mode for AmneziaWG, providing robust censorship evasion.
- **Always-On VPN**: Ensures continuous protection with Android's Always-On VPN feature.
- **Quick Controls**: Quick Settings tile and home screen shortcuts for easy VPN toggling.
- **Automation Support**: Intent-based automation for controlling tunnels.
- **Auto-Restore**: Seamlessly restores auto-tunneling and active tunnels after device restarts or app updates.
- **Proxying Options**: Built-in HTTP and SOCKS5 proxy support within tunnels.
- **Lockdown Mode**: Custom kill switch for maximum leak prevention and security.
- **Dynamic DNS Handling**: Detects and updates DNS changes without tunnel restarts.
- **Monitoring Tools**: Advanced tunnel monitoring features for tunnel performance monitoring.
- **Android TV Support**: Android TV support for secure streaming and browsing.
- **Advanced DNS**: DNS over HTTPS support for tunnel endpoint resolutions.
- **Auto-Tunneling:** Automatically activate tunnels based on your device's active network details.
- **Deferred Endpoint Bootstrapping:** Safely resolves endpoints and updates peers after the tunnel is up for better reliability and leak protection on startup.
- **Handshake Monitoring:** Real-time handshake monitoring for instant tunnel health feedback.
- **AmneziaWG Support:** Full support for AmneziaWG 2.0, providing robust censorship protection.
- **Split Tunneling:** Flexible support for routing specific apps or traffic through the VPN.
- **Local Proxy Mode:** Expose WireGuard tunnels over a local SOCKS5 or HTTP proxy to browsers or firewall apps (like AdGuard).
- **Lockdown Mode:** Advanced in-app kill switch that blocks all traffic while the tunnel is down.
- **Quick Controls:** Quick Settings tile and home screen shortcuts for easy toggling.
- **Remote Control Support:** Intent-based automation for controlling tunnels and auto-tunneling from automation apps (like Tasker).
- **Dynamic DNS Handling:** Automatically detect and update endpoints on server IP changes without requiring a restart.
- **IPv6 Endpoints:** Automatically upgrade to IPv6 endpoints or fall back to IPv4 based on network conditions without requiring a restart.
- **Android TV Support:** Full support for nearly all features on Android TV.
## Building
@@ -9,9 +9,9 @@ import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationL
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
import kotlin.time.Duration.Companion.milliseconds
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
@@ -20,8 +20,6 @@ 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,
@@ -1,13 +1,18 @@
package com.zaneschepke.wireguardautotunnel.ui.common.textbox
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -29,48 +34,59 @@ fun ConfigurationTextBox(
leading: (@Composable () -> Unit)? = null,
trailing: (@Composable (Modifier) -> Unit)? = null,
supportingText: (@Composable () -> Unit)? = null,
interactionSource: MutableInteractionSource = MutableInteractionSource(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
visualTransformation: VisualTransformation = VisualTransformation.None,
enabled: Boolean = true,
readOnly: Boolean = false,
singleLine: Boolean = true,
) {
Box(modifier = modifier.padding(top = 6.dp)) {
CustomTextField(
isError = isError,
textStyle =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp),
value = value,
visualTransformation = visualTransformation,
singleLine = singleLine,
interactionSource = interactionSource,
onValueChange = onValueChange,
label = null, // Disable built in label
containerColor = MaterialTheme.colorScheme.surface,
placeholder = {
Text(
text = hint,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
trailing = trailing,
supportingText = supportingText,
leading = leading,
readOnly = readOnly,
enabled = enabled,
)
CustomTextField(
isError = isError,
textStyle =
MaterialTheme.typography.bodySmall.copy(color = MaterialTheme.colorScheme.onSurface),
modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp),
value = value,
visualTransformation = visualTransformation,
singleLine = singleLine,
interactionSource = interactionSource,
onValueChange = { onValueChange(it) },
label = {
// custom static label notch
if (label.isNotEmpty()) {
Text(
label,
text = label,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelMedium,
style = MaterialTheme.typography.labelSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier =
Modifier.padding(start = 12.dp)
.offset(y = (-8).dp)
.background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 4.dp),
)
},
containerColor = MaterialTheme.colorScheme.surface,
placeholder = {
Text(
hint,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
trailing = trailing,
supportingText = supportingText,
leading = leading,
readOnly = readOnly,
enabled = enabled,
)
}
}
}
@@ -22,7 +22,9 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
@@ -33,7 +35,7 @@ fun CustomTextField(
modifier: Modifier = Modifier,
textStyle: TextStyle =
MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
label: @Composable () -> Unit,
label: @Composable (() -> Unit)? = null,
containerColor: Color,
onValueChange: (value: String) -> Unit = {},
singleLine: Boolean = true,
@@ -47,10 +49,19 @@ fun CustomTextField(
readOnly: Boolean = false,
enabled: Boolean = true,
visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = MutableInteractionSource(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val space = " "
var isFocused by remember { mutableStateOf(false) }
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) }
val textFieldValue =
remember(value, textFieldValueState) {
if (textFieldValueState.text == value) {
textFieldValueState
} else {
textFieldValueState.copy(text = value, selection = TextRange(value.length))
}
}
val cursorBrush =
if (isFocused) SolidColor(MaterialTheme.colorScheme.primary)
else SolidColor(Color.Transparent)
@@ -67,9 +78,14 @@ fun CustomTextField(
}
BasicTextField(
value = value,
value = textFieldValue,
textStyle = effectiveTextStyle,
onValueChange = { onValueChange(it) },
onValueChange = { newTextFieldValue ->
textFieldValueState = newTextFieldValue
if (value != newTextFieldValue.text) {
onValueChange(newTextFieldValue.text)
}
},
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions,
readOnly = readOnly,
@@ -90,15 +106,9 @@ fun CustomTextField(
visualTransformation = visualTransformation,
) {
OutlinedTextFieldDefaults.DecorationBox(
value = space + value,
innerTextField = {
if (value.isEmpty()) {
if (placeholder != null) {
placeholder()
}
}
it.invoke()
},
value = value,
innerTextField = it,
placeholder = placeholder,
contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 14.dp, bottom = 14.dp),
leadingIcon = leading,
trailingIcon =
@@ -141,7 +151,6 @@ fun CustomTextField(
label = label,
visualTransformation = visualTransformation,
interactionSource = interactionSource,
placeholder = placeholder,
container = {
OutlinedTextFieldDefaults.Container(
enabled = enabled,
@@ -42,7 +42,12 @@ sealed class Route : NavKey {
@Keep @Serializable data object Display : Route()
@Keep @Serializable data object Tunnels : Route()
@Keep
@Serializable
data object Tunnels : Route(), SecureRoute {
override val requiresProtection: Boolean
get() = true
}
@Keep @Serializable data class TunnelSettings(val id: Int) : Route()
@@ -1,10 +1,12 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.components
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.scrollbar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -18,7 +20,13 @@ fun LogList(
) {
LazyColumn(
state = lazyColumnListState,
modifier = modifier.padding(horizontal = 12.dp),
modifier =
modifier
.padding(horizontal = 12.dp)
.scrollbar(
state = lazyColumnListState.scrollIndicatorState,
orientation = Orientation.Vertical,
),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
itemsIndexed(items = logs, key = { index, _ -> index }) { _, log -> LogItem(log = log) }
@@ -1,10 +1,12 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.scrollbar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -13,10 +15,17 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.donate.crypto.comp
@Composable
fun AddressesScreen() {
val scrollState = rememberScrollState()
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
modifier =
Modifier.fillMaxSize()
.verticalScroll(scrollState)
.scrollbar(
state = scrollState.scrollIndicatorState,
orientation = Orientation.Vertical,
),
) {
val clipboard = rememberClipboardHelper()
Address.allAddresses.forEach { AddressItem(it) { address -> clipboard.copy(address) } }
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.support.license.component
import LicenseFileEntry
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -9,11 +10,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Launch
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.scrollbar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -24,8 +27,17 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable
fun LicenseList(licenses: List<LicenseFileEntry>) {
val context = LocalContext.current
val lazyListState = rememberLazyListState()
LazyColumn(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier =
Modifier.fillMaxSize()
.scrollbar(
state = lazyListState.scrollIndicatorState,
orientation = Orientation.Vertical,
),
state = lazyListState,
) {
items(licenses) { entry ->
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -42,5 +42,8 @@ fun PeerStatisticsSection(peer: ActivePeer) {
style = style,
color = color,
)
peer.endpoint?.let {
StatText(stringResource(R.string.endpoint_template, it), style = style, color = color)
}
}
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.size
@@ -12,6 +13,7 @@ import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material3.Icon
import androidx.compose.material3.scrollbar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -78,7 +80,11 @@ fun TunnelList(
viewModel.clearSelectedTunnels()
}
}
.overscroll(rememberOverscrollEffect()),
.overscroll(rememberOverscrollEffect())
.scrollbar(
state = lazyListState.scrollIndicatorState,
orientation = Orientation.Vertical,
),
state = lazyListState,
userScrollEnabled = true,
reverseLayout = false,
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -9,6 +10,7 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.scrollbar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -57,6 +59,8 @@ fun ConfigScreen(
var showQrModal by rememberSaveable { mutableStateOf(false) }
val scrollState = rememberScrollState()
val rawConfig by
remember(liveConfig, uiState.activeConfig, uiState.tunnel?.quickConfig) {
derivedStateOf {
@@ -90,7 +94,13 @@ fun ConfigScreen(
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
modifier =
Modifier.fillMaxSize()
.verticalScroll(scrollState)
.scrollbar(
state = scrollState.scrollIndicatorState,
orientation = Orientation.Vertical,
),
) {
val displayText by
remember(rawConfig, showKeys) { derivedStateOf { maskSensitive(rawConfig, showKeys) } }
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.settings.config.edit
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -12,6 +13,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.HdrAuto
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.scrollbar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -49,11 +51,12 @@ fun ConfigEditScreen(
val uiState by viewModel.collectAsState()
if (uiState.isLoading) return
val locale = Locale.current.platformLocale
var showSelectionDialog by rememberSaveable { mutableStateOf(false) }
val scrollState = rememberScrollState()
sharedViewModel.collectSideEffect { sideEffect ->
when (sideEffect) {
is LocalSideEffect.SaveChanges -> {
@@ -104,7 +107,14 @@ fun ConfigEditScreen(
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().imePadding().verticalScroll(rememberScrollState()),
modifier =
Modifier.fillMaxSize()
.imePadding()
.verticalScroll(scrollState)
.scrollbar(
state = scrollState.scrollIndicatorState,
orientation = Orientation.Vertical,
),
) {
if (uiState.isGlobalConfig) {
Column {
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunnels.sort
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollableDefaults
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@@ -17,6 +18,7 @@ import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.scrollbar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -107,7 +109,11 @@ fun SortScreen(sharedViewModel: SharedAppViewModel = koinActivityViewModel()) {
Modifier.pointerInput(Unit) {
if (tunnelsUiState.tunnels.isEmpty()) return@pointerInput
}
.overscroll(rememberOverscrollEffect()),
.overscroll(rememberOverscrollEffect())
.scrollbar(
state = lazyListState.scrollIndicatorState,
orientation = Orientation.Vertical,
),
state = lazyListState,
userScrollEnabled = true,
reverseLayout = false,
@@ -93,7 +93,6 @@ class ConfigEditViewModel(
tunnel = tunnel,
tunnels = tunnels,
isRunning = isRunning,
globalSettings = globalSettings,
)
}
}
@@ -27,17 +27,21 @@ class ProxySettingsViewModel(
combine(tunnelCoordinator.backendStatus, proxySettingsRepository.flow) {
backendStatus,
settings ->
ProxySettingsUiState(
proxySettings = settings,
backendStatus = backendStatus,
isLoading = false,
socks5Enabled = settings.socks5ProxyEnabled,
httpEnabled = settings.httpProxyEnabled,
socksBindAddress = settings.socks5ProxyBindAddress ?: "",
httpBindAddress = settings.httpProxyBindAddress ?: "",
proxyUsername = settings.proxyUsername ?: "",
proxyPassword = settings.proxyPassword ?: "",
)
if (state.isLoading) {
ProxySettingsUiState(
proxySettings = settings,
backendStatus = backendStatus,
isLoading = false,
socks5Enabled = settings.socks5ProxyEnabled,
httpEnabled = settings.httpProxyEnabled,
socksBindAddress = settings.socks5ProxyBindAddress ?: "",
httpBindAddress = settings.httpProxyBindAddress ?: "",
proxyUsername = settings.proxyUsername ?: "",
proxyPassword = settings.proxyPassword ?: "",
)
} else {
state.copy(backendStatus = backendStatus)
}
}
.collect { reduce { it } }
}
@@ -73,13 +73,7 @@ class SharedAppViewModel(
tunnelCoordinator.backendStatus,
selectedTunnelsRepository.flow,
) { tunnels, backendStatus, selectedTuns ->
val activeTunnelIds = backendStatus.activeTunnels.keys
val sortedTunnels =
tunnels.sortedWith(
compareByDescending<TunnelConfig> { it.id in activeTunnelIds }
.thenBy { it.position }
)
val sortedTunnels = tunnels.sortedBy { it.position }
val displayStates =
backendStatus.activeTunnels.mapValues { (_, activeTunnel) ->
+1
View File
@@ -524,4 +524,5 @@
<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>
<string name="endpoint_template">endpoint: %1$s</string>
</resources>
+2 -2
View File
@@ -1,6 +1,6 @@
object Constants {
const val VERSION_NAME = "5.0.0"
const val VERSION_CODE = 50000
const val VERSION_NAME = "5.0.2"
const val VERSION_CODE = 50002
const val TARGET_SDK = 37
const val MIN_SDK = 26
@@ -1 +1 @@
WireGuard & AmneziaWG VPN klient s automatickým tunelováním, blokováním a proxy.
VPN klient WireGuard & AmneziaWG s automatickým tunelováním
@@ -7,5 +7,4 @@ What's new:
- 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
@@ -0,0 +1,4 @@
What's new:
- Bugfix for some tunnel endpoints failing to resolve in system DNS mode
- Mitigated memory tagging error bug for system DNS mode
- Bugfix for split tunneling regression by reverting Android Auto workaround
@@ -0,0 +1,4 @@
What's new:
- Battery usage optimizations
- Legacy Wi-Fi mode location access optimizations
- Revert active to top of tunnel list
@@ -1,13 +1,17 @@
WG Tunnel is a WireGuard VPN client that strikes the balance between simplicity and robustness, making it the ideal client for casual and power users alike.
Whether you simply want to automate when you're connected to your VPN or you're a power user with advanced privacy use cases, WG Tunnel has you covered.
- **Auto-Tunneling:** Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks.
Features:
- **Auto-Tunneling:** Automatically activate tunnels based on your device's active network details.
- **Deferred Endpoint Bootstrapping:** Safely resolves endpoints and updates peers after the tunnel is up for better reliability and leak protection on startup.
- **Handshake Monitoring:** Real-time handshake monitoring for instant tunnel health feedback.
- **AmneziaWG Support:** Full support for AmneziaWG 2.0, providing robust censorship protection.
- **Split Tunneling:** Flexible support for routing specific apps or traffic through the VPN.
- **App Modes:** Support for multiple tunnel modes, including standard VPN, kernel, lockdown (custom kill switch), and proxy modes.
- **AmneziaWG Integration:** Full support for AmneziaWG, providing robust censorship evasion.
- **Proxying Options:** Built-in HTTP and SOCKS5 proxy support allowing third-party apps to tunnel their traffic.
- **Quick Controls:** Quick Settings tile and home screen shortcuts for easy toggling actions.
- **Automation Support:** Intent-based automation for controlling tunnels and auto-tunneling.
- **Dynamic DNS Handling:** Detects and updates DNS changes without tunnel restarts.
- **Monitoring Tools:** Advanced tunnel monitoring features for tunnel performance monitoring.
- **Android TV Support:** Android TV support for nearly all app features.
- **Local Proxy Mode:** Expose WireGuard tunnels over a local SOCKS5 or HTTP proxy to browsers or firewall apps (like AdGuard).
- **Lockdown Mode:** Advanced in-app kill switch that blocks all traffic while the tunnel is down.
- **Quick Controls:** Quick Settings tile and home screen shortcuts for easy toggling.
- **Remote Control Support:** Intent-based automation for controlling tunnels and auto-tunneling from automation apps (like Tasker).
- **Dynamic DNS Handling:** Automatically detect and update endpoints on server IP changes without requiring a restart.
- **IPv6 Endpoints:** Automatically upgrade to IPv6 endpoints or fall back to IPv4 based on network conditions without requiring a restart.
- **Android TV Support:** Full support for nearly all features on Android TV.
@@ -1 +1 @@
WireGuardi & AmneziaWG VPNi klient automaatse tunnelduse, lukustuse ja proksiga.
WireGuardi ja AmneziaWG VPN-klient automaattunnelduse ja lukustusega
@@ -1 +1 @@
e90dc18c2ed6afd480ad4ef1f284f353ecff5f5eبرنامه ای برای جایگزینی وایرگارد با امکانات بیشتر
برنامه ای برای جایگزینی وایرگارد با امکانات بیشتر
@@ -1,4 +0,0 @@
What's new:
- Fixes deprecate location API bug by adding Wi-Fi info method selection
- Simplified update check dialog UI
- Improve auto-tunnel reliability with delayed recheck
@@ -1,6 +0,0 @@
What's new:
- Tunnel sorting
- Shizuku support for Wi-Fi SSIDs
- Android TV hover visibility improvements
- Auto-tunnel default detection method bug fix
- Other UI changes and improvements
@@ -1,7 +0,0 @@
What's new:
- Fix for tunnel sort bug
- Improved location permissions flow
- Location permission detection and notifications
- Fix for AndroidTV apps detection for split tunneling
- Improved tunnel monitoring and reboot recovery
- Fix tunnel slow reconnect from sleep
@@ -1,8 +0,0 @@
What's new:
- Introduction of app modes
- HTTP/SOCKS5 proxying of tunnels
- Lockdown mode for leakproof kill switch
- Dynamic DNS endpoint updates without tunnel restart
- DoH for peer endpoint resolutions
- AmneziaWG 1.5 with protocol mimic
- Many bug fixes and performance improvements
@@ -1,4 +0,0 @@
What's new:
- Search domain tunnels fail to start bugfix
- DNS fallback to IPv4 on IPv4 only networks bugfix
- Ping target not editable bugfix
@@ -1,5 +0,0 @@
What's new:
- App lock crash bugfix
- Fdroid publishing bugfix
- Exporting logs bugfix
- Auto-tunnel ethernet toggle bugfix
@@ -1,4 +0,0 @@
What's new:
- Monitoring failing to shut down race bugfix
- Notifications stop action bugfix
- Notification relaunch activity when already active bugfix
@@ -1,8 +0,0 @@
What's new:
- UI rework
- Dynamic DNS fixes
- Battery usage bugfix
- Auto-tunnel reliability improvements
- Global split tunneling and config overrides
- Restart on boot and AOVPN bugfixes
- Various other improvements and optimizations
@@ -1,3 +0,0 @@
What's new:
- Auto tunnel start ui bugfix
- Peer stats ui bugfix
@@ -1,8 +0,0 @@
What's new:
- Metered tunnels settings
- Lockdown dual-stack support
- Lockdown multiple profile bugfix
- Split tunneling improved installed packages querying
- Restart active tunnels on configuration changes
- Android TV UI bugfixes
- Various other bugfixes and improvements
@@ -1,5 +0,0 @@
What's new:
- Resource usage bugfix
- Improve network monitoring
- Tab navigation bugfix
- Tunnel metered default bugfix
@@ -1,3 +0,0 @@
What's new:
- Auto tunnel network detection bugfix
- Tunnel notification sometimes don't start bugfix
@@ -1,3 +0,0 @@
What's new:
- Fixes crash on older Android versions where metered tunnel override is unavailable
- Fixes auto-tunnel network monitor incorrectly detecting VPN changes
@@ -1,3 +0,0 @@
What's new:
- Auto-tunnel regression bugfix
- Resource usage bugfix for kill switch mode
@@ -1,6 +0,0 @@
What's new:
- Improved QR scanning and device support
- Display tunnel uptime
- Fixes quick tile crash bug when running app in multiple profiles
- Fixes global overrides regression causing unexpected tunnel start errors
- Fixes network detection race while VPN is active
@@ -1,2 +0,0 @@
What's new:
- Rapid network changes cause invalid network state bugfix
@@ -1,5 +0,0 @@
What's new:
- Amnezia 2.0 support
- Copy split tunnel apps from existing config
- Logger start bugfix
- Quick tile added sync bugfix
@@ -1,3 +0,0 @@
What's new:
- Auto-tunnel screen not loading without connecting to Wi-Fi bugfix
- Import tunnel via URL bugfix
@@ -1,6 +0,0 @@
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 +0,0 @@
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
@@ -1,13 +0,0 @@
WG Tunnel is a WireGuard VPN client that strikes the balance between simplicity and robustness, making it the ideal client for casual and power users alike.
Whether you simply want to automate when you're connected to your VPN or you're a power user with advanced privacy use cases, WG Tunnel has you covered.
- **Auto-Tunneling:** Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks.
- **Split Tunneling:** Flexible support for routing specific apps or traffic through the VPN.
- **App Modes:** Support for multiple tunnel modes, including standard VPN, kernel, lockdown (custom kill switch), and proxy modes.
- **AmneziaWG Integration:** Full support for AmneziaWG, providing robust censorship evasion.
- **Proxying Options:** Built-in HTTP and SOCKS5 proxy support allowing third-party apps to tunnel their traffic.
- **Quick Controls:** Quick Settings tile and home screen shortcuts for easy toggling actions.
- **Automation Support:** Intent-based automation for controlling tunnels and auto-tunneling.
- **Dynamic DNS Handling:** Detects and updates DNS changes without tunnel restarts.
- **Monitoring Tools:** Advanced tunnel monitoring features for tunnel performance monitoring.
- **Android TV Support:** Android TV support for nearly all app features.
@@ -1 +0,0 @@
A WireGuard & AmneziaWG VPN client with auto-tunneling, lockdown & proxying.
-1
View File
@@ -1 +0,0 @@
WG Tunnel
@@ -1 +1 @@
Een WireGuard- en AmneziaWG-VPN-client met automatische tunneling, lockdown en proxying.
WireGuard- en AmneziaWG-VPN-client met autotunneling en lockdown
@@ -1,4 +0,0 @@
What's new:
- Fixes deprecate location API bug by adding Wi-Fi info method selection
- Simplified update check dialog UI
- Improve auto-tunnel reliability with delayed recheck
@@ -1,6 +0,0 @@
What's new:
- Tunnel sorting
- Shizuku support for Wi-Fi SSIDs
- Android TV hover visibility improvements
- Auto-tunnel default detection method bug fix
- Other UI changes and improvements
@@ -1,7 +0,0 @@
What's new:
- Fix for tunnel sort bug
- Improved location permissions flow
- Location permission detection and notifications
- Fix for AndroidTV apps detection for split tunneling
- Improved tunnel monitoring and reboot recovery
- Fix tunnel slow reconnect from sleep
@@ -1,8 +0,0 @@
What's new:
- Introduction of app modes
- HTTP/SOCKS5 proxying of tunnels
- Lockdown mode for leakproof kill switch
- Dynamic DNS endpoint updates without tunnel restart
- DoH for peer endpoint resolutions
- AmneziaWG 1.5 with protocol mimic
- Many bug fixes and performance improvements
@@ -1,4 +0,0 @@
What's new:
- Search domain tunnels fail to start bugfix
- DNS fallback to IPv4 on IPv4 only networks bugfix
- Ping target not editable bugfix
@@ -1,5 +0,0 @@
What's new:
- App lock crash bugfix
- Fdroid publishing bugfix
- Exporting logs bugfix
- Auto-tunnel ethernet toggle bugfix
@@ -1,4 +0,0 @@
What's new:
- Monitoring failing to shut down race bugfix
- Notifications stop action bugfix
- Notification relaunch activity when already active bugfix
@@ -1,8 +0,0 @@
What's new:
- UI rework
- Dynamic DNS fixes
- Battery usage bugfix
- Auto-tunnel reliability improvements
- Global split tunneling and config overrides
- Restart on boot and AOVPN bugfixes
- Various other improvements and optimizations
@@ -1,3 +0,0 @@
What's new:
- Auto tunnel start ui bugfix
- Peer stats ui bugfix
@@ -1,8 +0,0 @@
What's new:
- Metered tunnels settings
- Lockdown dual-stack support
- Lockdown multiple profile bugfix
- Split tunneling improved installed packages querying
- Restart active tunnels on configuration changes
- Android TV UI bugfixes
- Various other bugfixes and improvements
@@ -1,5 +0,0 @@
What's new:
- Resource usage bugfix
- Improve network monitoring
- Tab navigation bugfix
- Tunnel metered default bugfix
@@ -1,3 +0,0 @@
What's new:
- Auto tunnel network detection bugfix
- Tunnel notification sometimes don't start bugfix
@@ -1,3 +0,0 @@
What's new:
- Fixes crash on older Android versions where metered tunnel override is unavailable
- Fixes auto-tunnel network monitor incorrectly detecting VPN changes
@@ -1,3 +0,0 @@
What's new:
- Auto-tunnel regression bugfix
- Resource usage bugfix for kill switch mode
@@ -1,6 +0,0 @@
What's new:
- Improved QR scanning and device support
- Display tunnel uptime
- Fixes quick tile crash bug when running app in multiple profiles
- Fixes global overrides regression causing unexpected tunnel start errors
- Fixes network detection race while VPN is active
@@ -1,2 +0,0 @@
What's new:
- Rapid network changes cause invalid network state bugfix
@@ -1,5 +0,0 @@
What's new:
- Amnezia 2.0 support
- Copy split tunnel apps from existing config
- Logger start bugfix
- Quick tile added sync bugfix
@@ -1,3 +0,0 @@
What's new:
- Auto-tunnel screen not loading without connecting to Wi-Fi bugfix
- Import tunnel via URL bugfix
@@ -1,6 +0,0 @@
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 +0,0 @@
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
@@ -1,13 +0,0 @@
WG Tunnel is a WireGuard VPN client that strikes the balance between simplicity and robustness, making it the ideal client for casual and power users alike.
Whether you simply want to automate when you're connected to your VPN or you're a power user with advanced privacy use cases, WG Tunnel has you covered.
- **Auto-Tunneling:** Automatically activate tunnels based on Wi-Fi SSID, Ethernet connections, or mobile data networks.
- **Split Tunneling:** Flexible support for routing specific apps or traffic through the VPN.
- **App Modes:** Support for multiple tunnel modes, including standard VPN, kernel, lockdown (custom kill switch), and proxy modes.
- **AmneziaWG Integration:** Full support for AmneziaWG, providing robust censorship evasion.
- **Proxying Options:** Built-in HTTP and SOCKS5 proxy support allowing third-party apps to tunnel their traffic.
- **Quick Controls:** Quick Settings tile and home screen shortcuts for easy toggling actions.
- **Automation Support:** Intent-based automation for controlling tunnels and auto-tunneling.
- **Dynamic DNS Handling:** Detects and updates DNS changes without tunnel restarts.
- **Monitoring Tools:** Advanced tunnel monitoring features for tunnel performance monitoring.
- **Android TV Support:** Android TV support for nearly all app features.
@@ -1 +0,0 @@
A WireGuard & AmneziaWG VPN client with auto-tunneling, lockdown & proxying.
-1
View File
@@ -1 +0,0 @@
WG Tunnel
@@ -1 +1 @@
Um cliente VPN WireGuard e AmneziaWG com tunelamento automático, bloqueio e proxy.
Cliente VPN WireGuard e AmneziaWG com túnel automático e bloqueio
@@ -1 +1 @@
Клиент WireGuard & AmneziaWG с автотуннелированием, блокировкой и проксированием
Клиент WireGuard и AmneziaWG с автотуннелированием и блокировкой
@@ -1 +1 @@
Alternatívna aplikácia VPN klienta pre WireGuard a AmneziaWG s ďalšími funkciami
Alternatívny VPN klient pre WireGuard a AmneziaWG s extra funkciami
@@ -1 +1 @@
VPN-клієнт WireGuard та AmneziaWG з автоматичним тунелюванням, блокуванням та проксюванням.
VPN-клієнт WireGuard та AmneziaWG з автотунелюванням і блокуванням
@@ -1 +1 @@
Một ứng dụng client VPN WireGuard & AmneziaWG với tính năng auto-tunneling, lockdown proxying.
ng dụng VPN WireGuard & AmneziaWG với auto-tunneling, lockdown, proxy
@@ -18,7 +18,8 @@ import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.WifiDetectionMethod.
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.WifiDetectionMethod.ROOT
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.WifiDetectionMethod.SHIZUKU
import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
import com.zaneschepke.networkmonitor.util.getCurrentSecurityType
import com.zaneschepke.networkmonitor.util.getLegacySecurityType
import com.zaneschepke.networkmonitor.util.getWifiSecurityType
import com.zaneschepke.networkmonitor.util.getWifiSsid
import com.zaneschepke.networkmonitor.util.hasRequiredLocationPermissions
import com.zaneschepke.networkmonitor.util.isAirplaneModeOn
@@ -105,7 +106,8 @@ class AndroidNetworkMonitor(
// tracking to prevent races that occur when VPN is first activated and to prevent redundant
// location queries in Legacy mode
private val lastKnownActiveNetwork = MutableStateFlow<ActiveNetwork>(ActiveNetwork.Disconnected)
private val lastKnownActiveNetwork =
MutableStateFlow<ActiveNetwork>(ActiveNetwork.Disconnected())
private val privateDnsFlow: Flow<PrivateDnsSettings> = callbackFlow {
val contentResolver = appContext.contentResolver
@@ -414,6 +416,7 @@ class AndroidNetworkMonitor(
return lastActive.ssid
}
}
Timber.d("Triggering new location ping")
wifiManager?.getWifiSsid() ?: ANDROID_UNKNOWN_SSID
}
ROOT ->
@@ -486,7 +489,7 @@ class AndroidNetworkMonitor(
if (defaultCaps == null || defaultNetwork == null) {
return@combine ConnectivityState(
activeNetwork = ActiveNetwork.Disconnected,
activeNetwork = ActiveNetwork.Disconnected(),
locationPermissionsGranted = permissions.locationPermissionGranted,
locationServicesEnabled = permissions.locationServicesEnabled,
vpnState = VpnState.Inactive,
@@ -528,17 +531,47 @@ class AndroidNetworkMonitor(
) &&
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} == true -> {
val wifiEvent = networkData.wifiNetworkEvent
val ssid =
getSsidByDetectionMethod(
detectionMethod,
wifiEvent.networkCapabilities,
wifiEvent.network,
)
val currentNetworkId = wifiEvent.network.toString()
val lastActive = lastKnownActiveNetwork.value
// Use cache in legacy mode
val (ssid, securityType) =
if (
detectionMethod == LEGACY &&
lastActive is ActiveNetwork.Wifi &&
lastActive.networkId == currentNetworkId &&
lastActive.ssid != ANDROID_UNKNOWN_SSID
) {
Timber.d(
"Using cached SSID and Security Type to prevent location ping"
)
lastActive.ssid to lastActive.securityType
} else {
// Fallback
val fetchedSsid =
getSsidByDetectionMethod(
detectionMethod,
wifiEvent.networkCapabilities,
wifiEvent.network,
)
val fetchedSecurity =
if (
detectionMethod == DEFAULT &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
) {
wifiEvent.networkCapabilities.getWifiSecurityType()
} else {
wifiManager?.getLegacySecurityType()
}
fetchedSsid to fetchedSecurity
}
ActiveNetwork.Wifi(
ssid,
wifiManager?.getCurrentSecurityType(),
wifiEvent.network.toString(),
securityType,
currentNetworkId,
wifiEvent.network,
)
}
@@ -561,7 +594,7 @@ class AndroidNetworkMonitor(
ActiveNetwork.Cellular(networkData.cellularEvent.network)
}
else -> ActiveNetwork.Disconnected
else -> ActiveNetwork.Disconnected()
}
lastKnownActiveNetwork.value = physicalNetwork
@@ -35,27 +35,29 @@ data class ConnectivityState(
data class Permissions(val locationServicesEnabled: Boolean, val locationPermissionGranted: Boolean)
sealed class ActiveNetwork {
abstract val network: Network?
fun key(): String {
return when (this) {
is Wifi -> "wifi:${networkId}"
is Cellular -> "cell:${network?.hashCode() ?: 0}"
is Ethernet -> "eth:${network?.hashCode() ?: 0}"
Disconnected -> "none"
is Disconnected -> "none"
}
}
data object Disconnected : ActiveNetwork()
data class Disconnected(override val network: Network? = null) : ActiveNetwork()
data class Wifi(
val ssid: String,
val securityType: WifiSecurityType?,
val networkId: String,
val network: Network?,
override val network: Network?,
) : ActiveNetwork()
data class Cellular(val network: Network?) : ActiveNetwork()
data class Cellular(override val network: Network?) : ActiveNetwork()
data class Ethernet(val network: Network?) : ActiveNetwork()
data class Ethernet(override val network: Network?) : ActiveNetwork()
}
sealed interface VpnState {
@@ -16,7 +16,7 @@ import kotlinx.coroutines.withContext
import timber.log.Timber
@Suppress("DEPRECATION")
fun WifiManager.getCurrentSecurityType(): WifiSecurityType? {
fun WifiManager.getLegacySecurityType(): WifiSecurityType? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
WifiSecurityType.from(connectionInfo.currentSecurityType)
} else {
@@ -24,6 +24,19 @@ fun WifiManager.getCurrentSecurityType(): WifiSecurityType? {
}
}
fun NetworkCapabilities.getWifiSecurityType(): WifiSecurityType? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val transportInfo = this.transportInfo
if (transportInfo is WifiInfo) {
WifiSecurityType.from(transportInfo.currentSecurityType)
} else {
null
}
} else {
null
}
}
@Suppress("DEPRECATION")
suspend fun WifiManager?.getWifiSsid(): String {
return withContext(Dispatchers.IO) {
@@ -84,7 +84,7 @@ internal class ServiceHolder(val context: Context) {
tunnelServiceDestroyed = CompletableDeferred()
service.stopSelf()
service.shutdown()
tunnelService = CompletableDeferred()
withTimeoutOrNull(1_000L.milliseconds) { tunnelServiceDestroyed.await() }
}
@@ -98,7 +98,7 @@ internal class ServiceHolder(val context: Context) {
vpnServiceDestroyed = CompletableDeferred()
service.stopSelf()
service.shutdown()
vpnService = CompletableDeferred()
withTimeoutOrNull(1_000L.milliseconds) { vpnServiceDestroyed.await() }
}
@@ -5,11 +5,12 @@ 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.StatusCallback
import com.zaneschepke.tunnel.Tunnel
import com.zaneschepke.tunnel.VpnBackend
import com.zaneschepke.tunnel.backend.dns.AndroidNetworkResolver
import com.zaneschepke.tunnel.backend.dns.CustomDnsResolver
import com.zaneschepke.tunnel.event.TunnelEvent
import com.zaneschepke.tunnel.model.BackendMode
import com.zaneschepke.tunnel.model.DnsBoostrapConfig
@@ -27,7 +28,6 @@ import com.zaneschepke.tunnel.state.KillSwitchState
import com.zaneschepke.tunnel.state.RuntimeDnsConfig
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
@@ -39,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
@@ -97,10 +98,11 @@ class TunnelBackend(
private var dnsConfigJob: Job? = null
private val statusCallback = StatusCallback { handle, code ->
val state = Tunnel.State.fromNative(code)
state?.let { nativeState ->
val tunnelId = byHandle[handle] ?: return@let
updateTunnelTransportState(tunnelId, nativeState)
val state = Tunnel.State.fromNative(code) ?: return@StatusCallback
val tunnelId = byHandle[handle] ?: return@StatusCallback
val current = _status.value.activeTunnels[tunnelId]?.transportState
if (current != state) {
updateTunnelTransportState(tunnelId, state)
}
}
@@ -139,7 +141,7 @@ class TunnelBackend(
mode.config.`interface`.postUp?.let { runScripts(it, tunnel.id) }
tunnelJobs[result.tunnelId] =
startTunnelJobs(result.handle, tunnel, mode, result.removedPeerEndpoint)
startTunnelJobs(result.handle, tunnel, mode, result.replacedWithNonRoutable)
}
.onFailure { cleanup(tunnel.id) }
}
@@ -397,16 +399,16 @@ class TunnelBackend(
handle: Int,
tunnel: Tunnel,
mode: BackendMode,
removedPeerEndpoint: Boolean,
replacedWithNonRoutable: Boolean,
): Job {
return scope.launch {
supervisorScope {
if (removedPeerEndpoint) {
if (replacedWithNonRoutable) {
updateTunnelBootstrapState(tunnel.id, BootstrapState.ResolvingDns)
startDnsBootstrapJob(handle, tunnel, mode)
}
if (removedPeerEndpoint) {
if (replacedWithNonRoutable) {
when (val strategy = tunnel.ipStrategy) {
Tunnel.IpStrategy.Ipv4Only -> Unit
@@ -454,68 +456,77 @@ class TunnelBackend(
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()
suspend fun resolvePeers(mode: BackendMode): Map<PublicKey, DnsBootstrapResult> =
coroutineScope {
val peersToResolve = mode.config.peers.filter { !it.isStaticallyConfigured }
if (peersToResolve.isEmpty()) return@coroutineScope emptyMap()
val results = mutableMapOf<PublicKey, DnsBootstrapResult>()
val results = mutableMapOf<PublicKey, DnsBootstrapResult>()
// Wait until we have internet before starting any resolution
stableNetworkEngine.stableState.first { it?.state?.hasInternet() == true }
stableNetworkEngine.stableState.first { it?.state?.activeNetwork?.network != null }
exponentialBackoffForever {
var delayMs = 500L
// 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")
}
while (coroutineContext.isActive) {
Timber.d("Peer resolution attempt (resolved=${results.size}/${peersToResolve.size})")
val snapshot = stableNetworkEngine.stableState.value?.state
val network = snapshot?.activeNetwork?.network
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")
if (network == null) {
Timber.d("No network — waiting")
delay(delayMs.milliseconds)
delayMs = (delayMs * 2).coerceAtMost(30_000)
continue
}
results[peer.publicKey] =
dnsResult.copy(ipv4 = dnsResult.ipv4, ipv6 = dnsResult.ipv6.map { "[$it]" })
Timber.d("Successfully resolved $host${results[peer.publicKey]}")
delayMs = 500L
val dnsMode = _status.value.dnsMode
val runtimeDns = _status.value.runtimeDnsConfig
val bypassNeeded = mode is BackendMode.Vpn || _status.value.killSwitch.enabled
val resolver =
when (dnsMode) {
is DnsBoostrapMode.System -> AndroidNetworkResolver(network)
is DnsBoostrapMode.Custom -> CustomDnsResolver(runtimeDns, bypassNeeded)
}
var progressed = false
for (peer in peersToResolve) {
if (results.containsKey(peer.publicKey)) continue
val host = peer.endpoint?.substringBeforeLast(":") ?: continue
try {
val dnsResult = resolver.resolve(host)
if (dnsResult.ipv4.isNotEmpty() || dnsResult.ipv6.isNotEmpty()) {
results[peer.publicKey] =
dnsResult.copy(ipv6 = dnsResult.ipv6.map { "[$it]" })
progressed = true
}
} catch (e: Exception) {
Timber.w(e, "DNS failed for $host")
}
}
if (results.keys.containsAll(peersToResolve.map { it.publicKey })) {
Timber.d("All peers resolved")
return@coroutineScope results
}
if (!progressed) {
Timber.d("No progress — backing off")
delay(delayMs.milliseconds)
delayMs = (delayMs * 2).coerceAtMost(30_000)
}
}
if (results.size == peersToResolve.size) {
return@exponentialBackoffForever
}
throw IllegalStateException("Incomplete peer resolution, will retry with backoff")
return@coroutineScope results
}
return results
}
private fun CoroutineScope.startActiveConfigJob(
handle: Int,
tunnelId: Int,
@@ -27,7 +27,7 @@ internal class WireGuardTunnelEngine(private val serviceHolder: ServiceHolder) :
val ifName = WGT_INTERFACE_PREFIX + tunnel.id
val (config, removedPeerEndpoint) = buildConfig(mode)
val (config, replacedWithNonRoutable) = buildConfig(mode)
// guard against static listenPort issues
val listenPort = config.`interface`.listenPort
@@ -77,7 +77,7 @@ internal class WireGuardTunnelEngine(private val serviceHolder: ServiceHolder) :
handle = handle,
interfaceName = ifName,
mode = mode,
removedPeerEndpoint = removedPeerEndpoint,
replacedWithNonRoutable = replacedWithNonRoutable,
)
}
@@ -91,16 +91,20 @@ internal class WireGuardTunnelEngine(private val serviceHolder: ServiceHolder) :
}
private fun buildConfig(mode: BackendMode): Pair<Config, Boolean> {
var removedPeerEndpoint = false
var replacedWithNonRoutable = false
return mode.config.copy(
peers =
mode.config.peers.map { peer ->
if (!peer.isStaticallyConfigured) {
removedPeerEndpoint = true
rewriteDynamicEndpoint(peer)
// keep support for valid configs with no endpoints
// replace domain configs with nonroutable and let the boostrap job update this
// with the real ip later
if (!peer.isStaticallyConfigured && peer.endpoint != null) {
replacedWithNonRoutable = true
val port = peer.endpoint!!.substringAfterLast(":")
peer.copy(endpoint = "$TEST_NET_IP:$port", persistentKeepalive = 0)
} else peer
}
) to removedPeerEndpoint
) to replacedWithNonRoutable
}
private fun buildBridgeProxyConfig(): ProxyConfig {
@@ -149,11 +153,6 @@ internal class WireGuardTunnelEngine(private val serviceHolder: ServiceHolder) :
}
}
// omit peer endpoint while bootstrapping
private fun rewriteDynamicEndpoint(peer: PeerSection): PeerSection {
return peer.copy(endpoint = null)
}
override suspend fun stop(handle: Int, mode: BackendMode) {
when (mode) {
is BackendMode.Proxy.Standard -> stopProxyTunnel(handle)
@@ -270,6 +269,7 @@ internal class WireGuardTunnelEngine(private val serviceHolder: ServiceHolder) :
}
companion object {
const val TEST_NET_IP = "192.0.2.1"
const val WGT_INTERFACE_PREFIX = "wgtun"
}
}
@@ -0,0 +1,23 @@
package com.zaneschepke.tunnel.backend.dns
import android.net.Network
import com.zaneschepke.tunnel.model.DnsBootstrapResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class AndroidNetworkResolver(private val network: Network) : PeerResolver {
override suspend fun resolve(host: String): DnsBootstrapResult =
withContext(Dispatchers.IO) {
// use underlying network for resolution
val ips = network.getAllByName(host)
Timber.d("Resolution from network bind socket: ${ips.contentToString()}")
val v4 = ips.filter { it.address.size == 4 }.map { it.hostAddress }
val v6 = ips.filter { it.address.size == 16 }.map { it.hostAddress }
DnsBootstrapResult(v4, v6)
}
}
@@ -0,0 +1,18 @@
package com.zaneschepke.tunnel.backend.dns
import com.zaneschepke.tunnel.DnsConfigManager
import com.zaneschepke.tunnel.model.DnsBootstrapResult
import com.zaneschepke.tunnel.state.RuntimeDnsConfig
class CustomDnsResolver(private val dnsConfig: RuntimeDnsConfig, private val bypass: Boolean) :
PeerResolver {
override suspend fun resolve(host: String): DnsBootstrapResult {
return DnsConfigManager.resolveHostBootstrap(
host = host,
protocol = dnsConfig.protocol,
upstream = dnsConfig.upstream,
bypass = bypass,
)
}
}
@@ -0,0 +1,7 @@
package com.zaneschepke.tunnel.backend.dns
import com.zaneschepke.tunnel.model.DnsBootstrapResult
interface PeerResolver {
suspend fun resolve(host: String): DnsBootstrapResult
}
@@ -6,6 +6,11 @@ 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 kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject
import timber.log.Timber
@@ -13,6 +18,9 @@ class TunnelService : LifecycleService() {
private val backend: Backend by inject(Backend::class.java)
private val serviceHolder: ServiceHolder by inject(ServiceHolder::class.java)
private val shutdownScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@Volatile private var userActivatedShutdown = false
override fun onCreate() {
serviceHolder.set(this)
@@ -38,9 +46,24 @@ class TunnelService : LifecycleService() {
return START_STICKY
}
@OptIn(ExperimentalAtomicApi::class)
fun shutdown() {
userActivatedShutdown = true
stopSelf()
}
@OptIn(ExperimentalAtomicApi::class)
override fun onDestroy() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
serviceHolder.signalTunnelServiceDestroyed()
if (!userActivatedShutdown) {
Timber.d("Service being killed by system, clean up tunnels")
shutdownScope.launch {
// TODO eventually, this should only shut down proxy mode tunnels with future multi
// tunnel
backend.stopAllActiveTunnels()
}
}
super.onDestroy()
}
@@ -21,6 +21,7 @@ import com.zaneschepke.tunnel.util.parseDns
import com.zaneschepke.tunnel.util.parseInetNetwork
import com.zaneschepke.wireguardautotunnel.parser.Config
import java.io.IOException
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -38,13 +39,13 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
private val serviceHolder: ServiceHolder by inject(ServiceHolder::class.java)
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val shutdownScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@Volatile private var userActivatedShutdown = false
private var hevBridgeJob: Job? = null
@Volatile private var fd: ParcelFileDescriptor? = null
val builder: Builder
get() = Builder()
override fun onCreate() {
serviceHolder.set(this)
ProxyBackend.setSocketProtector(this)
@@ -61,6 +62,7 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
)
}
@OptIn(ExperimentalAtomicApi::class)
override fun onDestroy() {
Timber.d("VpnService destroyed")
try {
@@ -70,12 +72,26 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
hevBridgeJob?.cancel()
serviceScope.cancel()
stopHevSocks5Bridge()
if (!userActivatedShutdown) {
Timber.d("Service being killed by system, clean up tunnels")
shutdownScope.launch {
// TODO eventually, this should only shut down vpn mode tunnels with future
// multi tunnel
backend.stopAllActiveTunnels()
}
}
} finally {
serviceHolder.signalVpnServiceDestroyed()
super.onDestroy()
}
}
@OptIn(ExperimentalAtomicApi::class)
fun shutdown() {
userActivatedShutdown = true
stopSelf()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
serviceHolder.set(this)
launchForegroundNotification()
@@ -159,8 +175,9 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
override fun setKillSwitch(config: KillSwitchConfig?) {
if (config == null) return disableKillSwitch()
fd?.close()
fd =
builder
Builder()
.apply {
setSession(LOCKDOWN_SESSION_NAME)
addAddress(IPV4_INTERFACE_ADDRESS, 32)
@@ -180,28 +197,15 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
addRoute(IPV6_DEFAULT_ROUTE, 0)
setMtu(DEFAULT_MTU)
addDnsServer(DEFAULT_DNS_SERVER)
// TODO could add an options to kill switch settings for this for ping
// sorts/update checks, etc to bypass killswitch
// addDisallowedApplication(this@VpnService.packageName)
}
.establish()
}
fun createTunInterface(tunnel: Tunnel, config: Config): ParcelFileDescriptor? {
return builder
return Builder()
.apply {
setSession(tunnel.name)
val isSplitTunneling =
!config.`interface`.excludedApplications.isNullOrEmpty() ||
!config.`interface`.includedApplications.isNullOrEmpty()
// important for Android Auto in split tunnel scenarios
// TODO Could make this a standalone feature toggle for strictness as it allows
// secondary network binding from other apps
if (isSplitTunneling) allowBypass()
config.`interface`.includedApplications?.forEach { addAllowedApplication(it) }
config.`interface`.excludedApplications?.forEach { addDisallowedApplication(it) }
@@ -216,16 +220,29 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
dnsConfig.searchDomains.forEach { addSearchDomain(it) }
}
var sawDefaultRoute = false
config.peers.forEach { peer ->
peer.allowedIPs?.split(",")?.forEach { entry ->
val (address, prefix) = entry.parseInetNetwork()
Timber.d("Adding route from config: $address/$prefix")
addRoute(address, prefix)
}
peer.allowedIPs
?.split(",")
?.map { it.trim() }
?.filter { it.isNotEmpty() }
?.forEach { entry ->
val (address, prefix) = entry.parseInetNetwork()
if (prefix == 0) {
sawDefaultRoute = true
}
Timber.d("Adding route from config: $address/$prefix")
addRoute(address, prefix)
}
}
allowFamily(OsConstants.AF_INET)
allowFamily(OsConstants.AF_INET6)
if (!(sawDefaultRoute && config.peers.size == 1)) {
allowFamily(OsConstants.AF_INET)
allowFamily(OsConstants.AF_INET6)
}
val mtu = config.`interface`.mtu ?: DEFAULT_MTU
setMtu(mtu)
@@ -7,5 +7,5 @@ data class EngineStartResult(
val handle: Int,
val interfaceName: String,
val mode: BackendMode,
val removedPeerEndpoint: Boolean,
val replacedWithNonRoutable: Boolean,
)
@@ -1,30 +0,0 @@
package com.zaneschepke.tunnel.util
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import timber.log.Timber
suspend fun exponentialBackoffForever(
initialDelayMs: Long = 500,
factor: Double = 2.0,
maxDelayMs: Long = 30_000,
block: suspend () -> Unit,
) = coroutineScope {
var delayMs = initialDelayMs
while (isActive) {
try {
block()
Timber.d("exponentialBackoffForever: block succeeded, exiting loop")
return@coroutineScope
} catch (e: Exception) {
Timber.w(e, "Backoff operation failed, retrying in ${delayMs}ms...")
delay(delayMs.milliseconds)
delayMs = (delayMs * factor).toLong().coerceAtMost(maxDelayMs)
}
}
}
+9
View File
@@ -24,6 +24,7 @@ var (
cancelFuncs map[int32]context.CancelFunc
tag string
virtualTunnelHandles map[int32]*wireproxyawg.VirtualTun
lastTunnelStatus sync.Map
tunnelMu sync.RWMutex
)
@@ -81,6 +82,13 @@ func awgStartProxy(interfaceName string, config string, uapiPath string, bypass
}
statusCB := func(code device.StatusCode) {
key := handle
if prev, loaded := lastTunnelStatus.LoadOrStore(key, code); loaded {
if prev == code {
return // duplicate, skip
}
lastTunnelStatus.Store(key, code)
}
go C.awgNotifyStatus(C.int32_t(handle), C.int32_t(code))
}
@@ -279,6 +287,7 @@ func awgTurnProxyTunnelOff(virtualTunnelHandle int32) {
virtualTun.Dev.Close()
}
lastTunnelStatus.Delete(virtualTunnelHandle)
shared.ReleaseHandle(virtualTunnelHandle)
C.awgNotifyStatus(
+12 -3
View File
@@ -30,9 +30,10 @@ type TunnelHandle struct {
}
var (
tag string
tunnelHandles = make(map[int32]TunnelHandle)
tunnelMu sync.RWMutex
tag string
tunnelHandles = make(map[int32]TunnelHandle)
lastTunnelStatus sync.Map
tunnelMu sync.RWMutex
)
func init() {
@@ -67,6 +68,13 @@ func awgTurnOn(interfaceName string, tunFd int32, settings string, uapiPath stri
}
statusCB := func(code device.StatusCode) {
key := handle
if prev, loaded := lastTunnelStatus.LoadOrStore(key, code); loaded {
if prev == code {
return // duplicate, skip
}
lastTunnelStatus.Store(key, code)
}
go C.awgNotifyStatus(C.int32_t(handle), C.int32_t(code))
}
@@ -196,6 +204,7 @@ func awgTurnOff(tunnelHandle int32) {
handle.device.Close()
}
lastTunnelStatus.Delete(tunnelHandle)
shared.ReleaseHandle(tunnelHandle)
C.awgNotifyStatus(