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 repository: wgtunnel/fdroid
event-type: fdroid-update 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: publish-play:
if: ${{ github.event_name == 'push' || inputs.track != 'none' }} if: ${{ github.event_name == 'push' || inputs.track != 'none' }}
name: Publish to Google Play name: Publish to Google Play
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build-google-aab
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/
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
with: with:
distribution: 'temurin' ref: ${{ github.event_name == 'push' && github.ref || 'master' }}
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew - name: Download AAB artifact
run: chmod +x gradlew uses: actions/download-artifact@v8
# 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
with: with:
fileName: ${{ env.KEY_STORE_FILE }} name: google-play-aab
fileDir: ${{ env.KEY_STORE_LOCATION }} path: ${{ github.workspace }}/aab
encodedString: ${{ secrets.KEYSTORE }}
# create keystore path for gradle to read - name: Find exact AAB file path
- name: Create keystore path env var id: find-aab
run: | run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }} AAB_PATH=$(find "${{ github.workspace }}/aab" -name "*.aab" -type f | head -1)
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV 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 - name: Create service_account.json
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Deploy with fastlane - name: Set up Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: '3.2' # Not needed with a .ruby-version file ruby-version: '3.4'
bundler-cache: true bundler-cache: true
- name: Distribute app to Prod track 🚀 - name: Upload to Google Play
run: | run: |
track=${{ github.event_name == 'push' && 'production' || inputs.track }} 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
+1
View File
@@ -1,3 +1,4 @@
source "https://rubygems.org" 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.core.notification.TunnelNotificationService
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@@ -20,8 +20,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlin.time.Duration.Companion.milliseconds
class TunnelEventDispatcher( class TunnelEventDispatcher(
private val notificationManager: TunnelNotificationService, private val notificationManager: TunnelNotificationService,
@@ -1,13 +1,18 @@
package com.zaneschepke.wireguardautotunnel.ui.common.textbox package com.zaneschepke.wireguardautotunnel.ui.common.textbox
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth 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.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -29,48 +34,59 @@ fun ConfigurationTextBox(
leading: (@Composable () -> Unit)? = null, leading: (@Composable () -> Unit)? = null,
trailing: (@Composable (Modifier) -> Unit)? = null, trailing: (@Composable (Modifier) -> Unit)? = null,
supportingText: (@Composable () -> Unit)? = null, supportingText: (@Composable () -> Unit)? = null,
interactionSource: MutableInteractionSource = MutableInteractionSource(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
visualTransformation: VisualTransformation = VisualTransformation.None, visualTransformation: VisualTransformation = VisualTransformation.None,
enabled: Boolean = true, enabled: Boolean = true,
readOnly: Boolean = false, readOnly: Boolean = false,
singleLine: Boolean = true, 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( // custom static label notch
isError = isError, if (label.isNotEmpty()) {
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 = {
Text( Text(
label, text = label,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelSmall,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, 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.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -33,7 +35,7 @@ fun CustomTextField(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
textStyle: TextStyle = textStyle: TextStyle =
MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface), MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
label: @Composable () -> Unit, label: @Composable (() -> Unit)? = null,
containerColor: Color, containerColor: Color,
onValueChange: (value: String) -> Unit = {}, onValueChange: (value: String) -> Unit = {},
singleLine: Boolean = true, singleLine: Boolean = true,
@@ -47,10 +49,19 @@ fun CustomTextField(
readOnly: Boolean = false, readOnly: Boolean = false,
enabled: Boolean = true, enabled: Boolean = true,
visualTransformation: VisualTransformation = VisualTransformation.None, visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = MutableInteractionSource(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) { ) {
val space = " "
var isFocused by remember { mutableStateOf(false) } 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 = val cursorBrush =
if (isFocused) SolidColor(MaterialTheme.colorScheme.primary) if (isFocused) SolidColor(MaterialTheme.colorScheme.primary)
else SolidColor(Color.Transparent) else SolidColor(Color.Transparent)
@@ -67,9 +78,14 @@ fun CustomTextField(
} }
BasicTextField( BasicTextField(
value = value, value = textFieldValue,
textStyle = effectiveTextStyle, textStyle = effectiveTextStyle,
onValueChange = { onValueChange(it) }, onValueChange = { newTextFieldValue ->
textFieldValueState = newTextFieldValue
if (value != newTextFieldValue.text) {
onValueChange(newTextFieldValue.text)
}
},
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
readOnly = readOnly, readOnly = readOnly,
@@ -90,15 +106,9 @@ fun CustomTextField(
visualTransformation = visualTransformation, visualTransformation = visualTransformation,
) { ) {
OutlinedTextFieldDefaults.DecorationBox( OutlinedTextFieldDefaults.DecorationBox(
value = space + value, value = value,
innerTextField = { innerTextField = it,
if (value.isEmpty()) { placeholder = placeholder,
if (placeholder != null) {
placeholder()
}
}
it.invoke()
},
contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 14.dp, bottom = 14.dp), contentPadding = OutlinedTextFieldDefaults.contentPadding(top = 14.dp, bottom = 14.dp),
leadingIcon = leading, leadingIcon = leading,
trailingIcon = trailingIcon =
@@ -141,7 +151,6 @@ fun CustomTextField(
label = label, label = label,
visualTransformation = visualTransformation, visualTransformation = visualTransformation,
interactionSource = interactionSource, interactionSource = interactionSource,
placeholder = placeholder,
container = { container = {
OutlinedTextFieldDefaults.Container( OutlinedTextFieldDefaults.Container(
enabled = enabled, enabled = enabled,
@@ -42,7 +42,10 @@ sealed class Route : NavKey {
@Keep @Serializable data object Display : Route() @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() @Keep @Serializable data class TunnelSettings(val id: Int) : Route()
@@ -42,5 +42,12 @@ fun PeerStatisticsSection(peer: ActivePeer) {
style = style, style = style,
color = color, color = color,
) )
peer.endpoint?.let {
StatText(
stringResource(R.string.endpoint_template, it),
style = style,
color = color
)
}
} }
} }
@@ -93,7 +93,6 @@ class ConfigEditViewModel(
tunnel = tunnel, tunnel = tunnel,
tunnels = tunnels, tunnels = tunnels,
isRunning = isRunning, isRunning = isRunning,
globalSettings = globalSettings,
) )
} }
} }
@@ -27,17 +27,21 @@ class ProxySettingsViewModel(
combine(tunnelCoordinator.backendStatus, proxySettingsRepository.flow) { combine(tunnelCoordinator.backendStatus, proxySettingsRepository.flow) {
backendStatus, backendStatus,
settings -> settings ->
ProxySettingsUiState( if (state.isLoading) {
proxySettings = settings, ProxySettingsUiState(
backendStatus = backendStatus, proxySettings = settings,
isLoading = false, backendStatus = backendStatus,
socks5Enabled = settings.socks5ProxyEnabled, isLoading = false,
httpEnabled = settings.httpProxyEnabled, socks5Enabled = settings.socks5ProxyEnabled,
socksBindAddress = settings.socks5ProxyBindAddress ?: "", httpEnabled = settings.httpProxyEnabled,
httpBindAddress = settings.httpProxyBindAddress ?: "", socksBindAddress = settings.socks5ProxyBindAddress ?: "",
proxyUsername = settings.proxyUsername ?: "", httpBindAddress = settings.httpProxyBindAddress ?: "",
proxyPassword = settings.proxyPassword ?: "", proxyUsername = settings.proxyUsername ?: "",
) proxyPassword = settings.proxyPassword ?: "",
)
} else {
state.copy(backendStatus = backendStatus)
}
} }
.collect { reduce { it } } .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="app_shortcuts_desc">Add quick actions to the app icon</string>
<string name="remote_control">Remote control</string> <string name="remote_control">Remote control</string>
<string name="remote_control_desc">Allow other apps (like Tasker) to control tunnels</string> <string name="remote_control_desc">Allow other apps (like Tasker) to control tunnels</string>
<string name="endpoint_template">endpoint: %1$s</string>
</resources> </resources>
+2 -2
View File
@@ -1,6 +1,6 @@
object Constants { object Constants {
const val VERSION_NAME = "5.0.0" const val VERSION_NAME = "5.0.1"
const val VERSION_CODE = 50000 const val VERSION_CODE = 50001
const val TARGET_SDK = 37 const val TARGET_SDK = 37
const val MIN_SDK = 26 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 - Added support got DoT and custom endpoints for peer resolution DNS
- Amnezia tunnel globals - Amnezia tunnel globals
- Improved notifications - Improved notifications
- UI improvements with better feature descriptions
- Various bug fix and app performance improvements - 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 // tracking to prevent races that occur when VPN is first activated and to prevent redundant
// location queries in Legacy mode // 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 { private val privateDnsFlow: Flow<PrivateDnsSettings> = callbackFlow {
val contentResolver = appContext.contentResolver val contentResolver = appContext.contentResolver
@@ -486,7 +487,7 @@ class AndroidNetworkMonitor(
if (defaultCaps == null || defaultNetwork == null) { if (defaultCaps == null || defaultNetwork == null) {
return@combine ConnectivityState( return@combine ConnectivityState(
activeNetwork = ActiveNetwork.Disconnected, activeNetwork = ActiveNetwork.Disconnected(),
locationPermissionsGranted = permissions.locationPermissionGranted, locationPermissionsGranted = permissions.locationPermissionGranted,
locationServicesEnabled = permissions.locationServicesEnabled, locationServicesEnabled = permissions.locationServicesEnabled,
vpnState = VpnState.Inactive, vpnState = VpnState.Inactive,
@@ -561,7 +562,7 @@ class AndroidNetworkMonitor(
ActiveNetwork.Cellular(networkData.cellularEvent.network) ActiveNetwork.Cellular(networkData.cellularEvent.network)
} }
else -> ActiveNetwork.Disconnected else -> ActiveNetwork.Disconnected()
} }
lastKnownActiveNetwork.value = physicalNetwork lastKnownActiveNetwork.value = physicalNetwork
@@ -35,27 +35,29 @@ data class ConnectivityState(
data class Permissions(val locationServicesEnabled: Boolean, val locationPermissionGranted: Boolean) data class Permissions(val locationServicesEnabled: Boolean, val locationPermissionGranted: Boolean)
sealed class ActiveNetwork { sealed class ActiveNetwork {
abstract val network: Network?
fun key(): String { fun key(): String {
return when (this) { return when (this) {
is Wifi -> "wifi:${networkId}" is Wifi -> "wifi:${networkId}"
is Cellular -> "cell:${network?.hashCode() ?: 0}" is Cellular -> "cell:${network?.hashCode() ?: 0}"
is Ethernet -> "eth:${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( data class Wifi(
val ssid: String, val ssid: String,
val securityType: WifiSecurityType?, val securityType: WifiSecurityType?,
val networkId: String, val networkId: String,
val network: Network?, override val network: Network?,
) : ActiveNetwork() ) : 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 { sealed interface VpnState {
@@ -84,7 +84,7 @@ internal class ServiceHolder(val context: Context) {
tunnelServiceDestroyed = CompletableDeferred() tunnelServiceDestroyed = CompletableDeferred()
service.stopSelf() service.shutdown()
tunnelService = CompletableDeferred() tunnelService = CompletableDeferred()
withTimeoutOrNull(1_000L.milliseconds) { tunnelServiceDestroyed.await() } withTimeoutOrNull(1_000L.milliseconds) { tunnelServiceDestroyed.await() }
} }
@@ -98,7 +98,7 @@ internal class ServiceHolder(val context: Context) {
vpnServiceDestroyed = CompletableDeferred() vpnServiceDestroyed = CompletableDeferred()
service.stopSelf() service.shutdown()
vpnService = CompletableDeferred() vpnService = CompletableDeferred()
withTimeoutOrNull(1_000L.milliseconds) { vpnServiceDestroyed.await() } withTimeoutOrNull(1_000L.milliseconds) { vpnServiceDestroyed.await() }
} }
@@ -5,11 +5,12 @@ import com.zaneschepke.networkmonitor.DnsInfo
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.PrivateDnsMode import com.zaneschepke.networkmonitor.PrivateDnsMode
import com.zaneschepke.networkmonitor.StableNetworkEngine import com.zaneschepke.networkmonitor.StableNetworkEngine
import com.zaneschepke.tunnel.DnsConfigManager
import com.zaneschepke.tunnel.NotificationProvider import com.zaneschepke.tunnel.NotificationProvider
import com.zaneschepke.tunnel.StatusCallback import com.zaneschepke.tunnel.StatusCallback
import com.zaneschepke.tunnel.Tunnel import com.zaneschepke.tunnel.Tunnel
import com.zaneschepke.tunnel.VpnBackend 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.event.TunnelEvent
import com.zaneschepke.tunnel.model.BackendMode import com.zaneschepke.tunnel.model.BackendMode
import com.zaneschepke.tunnel.model.DnsBoostrapConfig 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.state.RuntimeDnsConfig
import com.zaneschepke.tunnel.util.RootShellException import com.zaneschepke.tunnel.util.RootShellException
import com.zaneschepke.tunnel.util.buildResolvedPeers import com.zaneschepke.tunnel.util.buildResolvedPeers
import com.zaneschepke.tunnel.util.exponentialBackoffForever
import com.zaneschepke.tunnel.util.isLastTunnelOfServiceType import com.zaneschepke.tunnel.util.isLastTunnelOfServiceType
import com.zaneschepke.tunnel.util.toHostMap import com.zaneschepke.tunnel.util.toHostMap
import com.zaneschepke.wireguardautotunnel.parser.ActiveConfig import com.zaneschepke.wireguardautotunnel.parser.ActiveConfig
@@ -39,6 +39,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -454,68 +455,77 @@ class TunnelBackend(
updateTunnelBootstrapState(tunnel.id, BootstrapState.Complete) updateTunnelBootstrapState(tunnel.id, BootstrapState.Complete)
} }
suspend fun resolvePeers(mode: BackendMode): Map<PublicKey, DnsBootstrapResult> { suspend fun resolvePeers(mode: BackendMode): Map<PublicKey, DnsBootstrapResult> =
val peersToResolve = mode.config.peers.filter { !it.isStaticallyConfigured } coroutineScope {
if (peersToResolve.isEmpty()) return emptyMap() 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?.activeNetwork?.network != null }
stableNetworkEngine.stableState.first { it?.state?.hasInternet() == true }
exponentialBackoffForever { var delayMs = 500L
// If we lose internet while inside the backoff loop, wait again until it comes back while (coroutineContext.isActive) {
if (stableNetworkEngine.stableState.value?.state?.hasInternet() != true) {
Timber.d("No internet — waiting for connectivity before next resolution attempt")
stableNetworkEngine.stableState.first { it?.state?.hasInternet() == true }
Timber.d("Internet restored — resuming peer resolution")
}
Timber.d("Peer resolution attempt (resolved=${results.size}/${peersToResolve.size})") val snapshot = stableNetworkEngine.stableState.value?.state
val network = snapshot?.activeNetwork?.network
for (peer in peersToResolve) { if (network == null) {
if (results.containsKey(peer.publicKey)) continue Timber.d("No network — waiting")
delay(delayMs.milliseconds)
val endpoint = peer.endpoint ?: continue delayMs = (delayMs * 2).coerceAtMost(30_000)
val host = endpoint.substringBeforeLast(":")
val dnsConfig = _status.value.runtimeDnsConfig
val bypassNeeded = mode is BackendMode.Vpn || _status.value.killSwitch.enabled
val dnsResult =
try {
DnsConfigManager.resolveHostBootstrap(
host = host,
protocol = dnsConfig.protocol,
upstream = dnsConfig.upstream,
bypass = bypassNeeded,
)
} catch (e: Exception) {
Timber.w(e, "DNS resolution failed for host=$host")
continue
}
if (dnsResult.ipv4.isEmpty() && dnsResult.ipv6.isEmpty()) {
Timber.w("No IP addresses returned for host=$host")
continue continue
} }
results[peer.publicKey] = delayMs = 500L
dnsResult.copy(ipv4 = dnsResult.ipv4, ipv6 = dnsResult.ipv6.map { "[$it]" })
Timber.d("Successfully resolved $host${results[peer.publicKey]}") 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@coroutineScope results
return@exponentialBackoffForever
}
throw IllegalStateException("Incomplete peer resolution, will retry with backoff")
} }
return results
}
private fun CoroutineScope.startActiveConfigJob( private fun CoroutineScope.startActiveConfigJob(
handle: Int, handle: Int,
tunnelId: 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.Backend
import com.zaneschepke.tunnel.backend.ServiceHolder import com.zaneschepke.tunnel.backend.ServiceHolder
import com.zaneschepke.tunnel.backend.ServiceHolder.Companion.alwaysOnCallback 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 org.koin.java.KoinJavaComponent.inject
import timber.log.Timber import timber.log.Timber
import kotlin.concurrent.atomics.ExperimentalAtomicApi
class TunnelService : LifecycleService() { class TunnelService : LifecycleService() {
private val backend: Backend by inject(Backend::class.java) private val backend: Backend by inject(Backend::class.java)
private val serviceHolder: ServiceHolder by inject(ServiceHolder::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() { override fun onCreate() {
serviceHolder.set(this) serviceHolder.set(this)
@@ -38,9 +46,23 @@ class TunnelService : LifecycleService() {
return START_STICKY return START_STICKY
} }
@OptIn(ExperimentalAtomicApi::class)
fun shutdown() {
userActivatedShutdown = true
stopSelf()
}
@OptIn(ExperimentalAtomicApi::class)
override fun onDestroy() { override fun onDestroy() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
serviceHolder.signalTunnelServiceDestroyed() 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() super.onDestroy()
} }
@@ -20,8 +20,6 @@ import com.zaneschepke.tunnel.model.KillSwitchConfig
import com.zaneschepke.tunnel.util.parseDns import com.zaneschepke.tunnel.util.parseDns
import com.zaneschepke.tunnel.util.parseInetNetwork import com.zaneschepke.tunnel.util.parseInetNetwork
import com.zaneschepke.wireguardautotunnel.parser.Config import com.zaneschepke.wireguardautotunnel.parser.Config
import java.io.IOException
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -31,6 +29,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.java.KoinJavaComponent.inject import org.koin.java.KoinJavaComponent.inject
import timber.log.Timber 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 { 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 serviceHolder: ServiceHolder by inject(ServiceHolder::class.java)
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val shutdownScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@Volatile private var userActivatedShutdown = false
private var hevBridgeJob: Job? = null private var hevBridgeJob: Job? = null
@Volatile private var fd: ParcelFileDescriptor? = null @Volatile private var fd: ParcelFileDescriptor? = null
val builder: Builder
get() = Builder()
override fun onCreate() { override fun onCreate() {
serviceHolder.set(this) serviceHolder.set(this)
ProxyBackend.setSocketProtector(this) ProxyBackend.setSocketProtector(this)
@@ -61,6 +62,7 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
) )
} }
@OptIn(ExperimentalAtomicApi::class)
override fun onDestroy() { override fun onDestroy() {
Timber.d("VpnService destroyed") Timber.d("VpnService destroyed")
try { try {
@@ -70,12 +72,25 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
hevBridgeJob?.cancel() hevBridgeJob?.cancel()
serviceScope.cancel() serviceScope.cancel()
stopHevSocks5Bridge() 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 { } finally {
serviceHolder.signalVpnServiceDestroyed() serviceHolder.signalVpnServiceDestroyed()
super.onDestroy() super.onDestroy()
} }
} }
@OptIn(ExperimentalAtomicApi::class)
fun shutdown() {
userActivatedShutdown = true
stopSelf()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
serviceHolder.set(this) serviceHolder.set(this)
launchForegroundNotification() launchForegroundNotification()
@@ -159,8 +174,9 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
override fun setKillSwitch(config: KillSwitchConfig?) { override fun setKillSwitch(config: KillSwitchConfig?) {
if (config == null) return disableKillSwitch() if (config == null) return disableKillSwitch()
fd?.close()
fd = fd =
builder Builder()
.apply { .apply {
setSession(LOCKDOWN_SESSION_NAME) setSession(LOCKDOWN_SESSION_NAME)
addAddress(IPV4_INTERFACE_ADDRESS, 32) addAddress(IPV4_INTERFACE_ADDRESS, 32)
@@ -180,28 +196,15 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
addRoute(IPV6_DEFAULT_ROUTE, 0) addRoute(IPV6_DEFAULT_ROUTE, 0)
setMtu(DEFAULT_MTU) setMtu(DEFAULT_MTU)
addDnsServer(DEFAULT_DNS_SERVER) 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() .establish()
} }
fun createTunInterface(tunnel: Tunnel, config: Config): ParcelFileDescriptor? { fun createTunInterface(tunnel: Tunnel, config: Config): ParcelFileDescriptor? {
return builder return Builder()
.apply { .apply {
setSession(tunnel.name) 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`.includedApplications?.forEach { addAllowedApplication(it) }
config.`interface`.excludedApplications?.forEach { addDisallowedApplication(it) } config.`interface`.excludedApplications?.forEach { addDisallowedApplication(it) }
@@ -216,16 +219,30 @@ class VpnService : android.net.VpnService(), KillSwitch, SocketProtector {
dnsConfig.searchDomains.forEach { addSearchDomain(it) } dnsConfig.searchDomains.forEach { addSearchDomain(it) }
} }
var sawDefaultRoute = false
config.peers.forEach { peer -> config.peers.forEach { peer ->
peer.allowedIPs?.split(",")?.forEach { entry -> peer.allowedIPs
val (address, prefix) = entry.parseInetNetwork() ?.split(",")
Timber.d("Adding route from config: $address/$prefix") ?.map { it.trim() }
addRoute(address, prefix) ?.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) if (!(sawDefaultRoute && config.peers.size == 1)) {
allowFamily(OsConstants.AF_INET6) allowFamily(OsConstants.AF_INET)
allowFamily(OsConstants.AF_INET6)
}
val mtu = config.`interface`.mtu ?: DEFAULT_MTU val mtu = config.`interface`.mtu ?: DEFAULT_MTU
setMtu(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)
}
}
}