Compare commits

..

15 Commits

Author SHA1 Message Date
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
78 changed files with 343 additions and 458 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"
@@ -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,10 @@ 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()
@@ -42,5 +42,12 @@ fun PeerStatisticsSection(peer: ActivePeer) {
style = style,
color = color,
)
peer.endpoint?.let {
StatText(
stringResource(R.string.endpoint_template, it),
style = style,
color = color
)
}
}
}
@@ -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 } }
}
+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.1"
const val VERSION_CODE = 50001
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
@@ -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
@@ -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
@@ -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
@@ -105,7 +105,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
@@ -486,7 +487,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,
@@ -561,7 +562,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 {
@@ -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
@@ -454,68 +455,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,
@@ -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,13 +6,21 @@ 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 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
import kotlin.concurrent.atomics.ExperimentalAtomicApi
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,23 @@ 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()
}
@@ -20,8 +20,6 @@ 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
@@ -31,6 +29,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject
import timber.log.Timber
import java.io.IOException
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.time.Duration.Companion.milliseconds
class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
@@ -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,25 @@ 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 +174,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 +196,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 +219,30 @@ 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)
@@ -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)
}
}
}