mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe54c9cd0e | |||
| 554499f9de | |||
| 12c9b52653 | |||
| 03712a6c1d | |||
| 5f03a97fcc | |||
| 6788b05fa0 | |||
| 9494853dee | |||
| 01695c3286 | |||
| bb6e45ed92 | |||
| 63e257f419 | |||
| e145cd95e1 | |||
| 7e790acbfe | |||
| 334aaa1c2b | |||
| 6201671dd0 | |||
| 517d90c3bf |
@@ -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
|
||||
@@ -1,3 +1,4 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "fastlane"
|
||||
gem "fastlane"
|
||||
gem "multi_json"
|
||||
+1
-3
@@ -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,
|
||||
|
||||
+49
-33
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-15
@@ -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()
|
||||
|
||||
|
||||
+7
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-11
@@ -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 } }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 +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 +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 và proxying.
|
||||
Ứng dụng VPN WireGuard & AmneziaWG với auto-tunneling, lockdown, proxy
|
||||
|
||||
+4
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user