From 7f56d0c0fe6148a5ed2b44a231fa4e6cedd290c6 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Fri, 16 May 2025 09:26:06 -0700 Subject: [PATCH 1/5] android: bump OSS (#651) OSS and Version updated to 1.83.223-t336b3b7df-gd3f34c579 Signed-off-by: kari-ts --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dab17b2..2e80ca7 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 require ( github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab - tailscale.com v1.83.0-pre.0.20250507173847-fd263adc1b5b + tailscale.com v1.83.0-pre.0.20250515212619-336b3b7df0ab ) require ( diff --git a/go.sum b/go.sum index e8e9f52..1bc03ac 100644 --- a/go.sum +++ b/go.sum @@ -243,5 +243,5 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.83.0-pre.0.20250507173847-fd263adc1b5b h1:4CMBtWus+ZVwEB4MVc8lNehVjOpBwkc8M4G0Bsyt9eg= -tailscale.com v1.83.0-pre.0.20250507173847-fd263adc1b5b/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo= +tailscale.com v1.83.0-pre.0.20250515212619-336b3b7df0ab h1:jYAOBs7APf5DopDjTZWTXbUJBp7izN9oBb3fZ4hhC4o= +tailscale.com v1.83.0-pre.0.20250515212619-336b3b7df0ab/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo= From e3c76eb8126179fc709b12f6c0521c9bd3bf1314 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 20 May 2025 10:42:21 -0700 Subject: [PATCH 2/5] android: fix isExitNode check (#646) && takes precedence over ?:, so fix isExitNode to check both IPv4 and IPv6 Updates tailscale/tailscale#15785 Signed-off-by: kari-ts --- android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt index f79b89d..a51579e 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt @@ -105,7 +105,7 @@ class Tailcfg { // isExitNode reproduces the Go logic in local.go peerStatusFromNode val isExitNode: Boolean = - AllowedIPs?.contains("0.0.0.0/0") ?: false && AllowedIPs?.contains("::/0") ?: false + (AllowedIPs?.contains("0.0.0.0/0") ?: false) && (AllowedIPs?.contains("::/0") ?: false) val isMullvadNode: Boolean get() = Name.endsWith(".mullvad.ts.net.") From 81ff8987829f9c4200632403592edb2f27d0db36 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 20 May 2025 10:42:43 -0700 Subject: [PATCH 3/5] android: replace broadcast intent with service intent (#650) We were previously calling startService(intent), which is a direct call consumed by IPNService, but restartVPN was not working as intended because the broadcast receiver was never triggered. Rather than use a broadcast receiver, directly start the service in restartVPN as we do in stopVPN. Also, batch changes to excluded apps so that we don't restart the VPN each time the user toggles an app. Fixes tailscale/corp#28668 Signed-off-by: kari-ts --- .../src/main/java/com/tailscale/ipn/App.kt | 67 +++++-------------- .../main/java/com/tailscale/ipn/IPNService.kt | 11 ++- .../SplitTunnelAppPickerViewModel.kt | 23 +++++-- 3 files changed, 45 insertions(+), 56 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index a91325e..3e834cb 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -7,7 +7,6 @@ import android.app.Application import android.app.Notification import android.app.NotificationChannel import android.app.PendingIntent -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter @@ -37,11 +36,6 @@ import com.tailscale.ipn.ui.viewModel.VpnViewModel import com.tailscale.ipn.ui.viewModel.VpnViewModelFactory import com.tailscale.ipn.util.FeatureFlags import com.tailscale.ipn.util.TSLog -import java.io.File -import java.io.IOException -import java.net.NetworkInterface -import java.security.GeneralSecurityException -import java.util.Locale import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -53,6 +47,11 @@ import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import libtailscale.Libtailscale +import java.io.File +import java.io.IOException +import java.net.NetworkInterface +import java.security.GeneralSecurityException +import java.util.Locale class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -470,25 +469,15 @@ open class UninitializedApp : Application() { } fun restartVPN() { - // Register a receiver to listen for the completion of stopVPN - val stopReceiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - // Ensure stop intent is complete - if (intent?.action == IPNService.ACTION_STOP_VPN) { - // Unregister receiver after receiving the broadcast - context?.unregisterReceiver(this) - // Now start the VPN - startVPN() - } - } - } - - // Register the receiver before stopping VPN - val intentFilter = IntentFilter(IPNService.ACTION_STOP_VPN) - this.registerReceiver(stopReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED) - - stopVPN() + val intent = + Intent(this, IPNService::class.java).apply { action = IPNService.ACTION_RESTART_VPN } + try { + startService(intent) + } catch (illegalStateException: IllegalStateException) { + TSLog.e(TAG, "restartVPN hit IllegalStateException in startService(): $illegalStateException") + } catch (e: Exception) { + TSLog.e(TAG, "restartVPN hit exception in startService(): $e") + } } fun createNotificationChannel(id: String, name: String, description: String, importance: Int) { @@ -569,33 +558,13 @@ open class UninitializedApp : Application() { return builder.build() } - fun addUserDisallowedPackageName(packageName: String) { - if (packageName.isEmpty()) { - TSLog.e(TAG, "addUserDisallowedPackageName called with empty packageName") - return - } - - getUnencryptedPrefs() - .edit() - .putStringSet( - DISALLOWED_APPS_KEY, disallowedPackageNames().toMutableSet().union(setOf(packageName))) - .apply() - - this.restartVPN() - } - - fun removeUserDisallowedPackageName(packageName: String) { - if (packageName.isEmpty()) { - TSLog.e(TAG, "removeUserDisallowedPackageName called with empty packageName") + fun updateUserDisallowedPackageNames(packageNames: List) { + if (packageNames.any { it.isEmpty() }) { + TSLog.e(TAG, "updateUserDisallowedPackageNames called with empty packageName(s)") return } - getUnencryptedPrefs() - .edit() - .putStringSet( - DISALLOWED_APPS_KEY, - disallowedPackageNames().toMutableSet().subtract(setOf(packageName))) - .apply() + getUnencryptedPrefs().edit().putStringSet(DISALLOWED_APPS_KEY, packageNames.toSet()).apply() this.restartVPN() } diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index 917b405..e861d9c 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -12,12 +12,12 @@ import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.util.TSLog -import java.util.UUID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import libtailscale.Libtailscale +import java.util.UUID open class IPNService : VpnService(), libtailscale.IPNService { private val TAG = "IPNService" @@ -46,6 +46,13 @@ open class IPNService : VpnService(), libtailscale.IPNService { close() START_NOT_STICKY } + ACTION_RESTART_VPN -> { + app.setWantRunning(false){ + close() + app.startVPN() + } + START_NOT_STICKY + } ACTION_START_VPN -> { scope.launch { showForegroundNotification() } app.setWantRunning(true) @@ -82,7 +89,6 @@ open class IPNService : VpnService(), libtailscale.IPNService { } override fun close() { - app.setWantRunning(false) {} Notifier.setState(Ipn.State.Stopping) disconnectVPN() Libtailscale.serviceDisconnect(this) @@ -180,5 +186,6 @@ open class IPNService : VpnService(), libtailscale.IPNService { companion object { const val ACTION_START_VPN = "com.tailscale.ipn.START_VPN" const val ACTION_STOP_VPN = "com.tailscale.ipn.STOP_VPN" + const val ACTION_RESTART_VPN = "com.tailscale.ipn.RESTART_VPN" } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt index d00efb6..7611f05 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt @@ -4,14 +4,18 @@ package com.tailscale.ipn.ui.viewModel import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.tailscale.ipn.App import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.mdm.SettingState import com.tailscale.ipn.ui.util.InstalledApp import com.tailscale.ipn.ui.util.InstalledAppsManager import com.tailscale.ipn.ui.util.set +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch class SplitTunnelAppPickerViewModel : ViewModel() { val installedAppsManager = InstalledAppsManager(packageManager = App.get().packageManager) @@ -20,6 +24,8 @@ class SplitTunnelAppPickerViewModel : ViewModel() { val mdmExcludedPackages: StateFlow> = MDMSettings.excludedPackages.flow val mdmIncludedPackages: StateFlow> = MDMSettings.includedPackages.flow + private var saveJob: Job? = null + init { installedApps.set(installedAppsManager.fetchInstalledApps()) excludedPackageNames.set( @@ -30,15 +36,22 @@ class SplitTunnelAppPickerViewModel : ViewModel() { } fun exclude(packageName: String) { - if (excludedPackageNames.value.contains(packageName)) { - return - } + if (excludedPackageNames.value.contains(packageName)) return excludedPackageNames.set(excludedPackageNames.value + packageName) - App.get().addUserDisallowedPackageName(packageName) + debounceSave() } fun unexclude(packageName: String) { excludedPackageNames.set(excludedPackageNames.value - packageName) - App.get().removeUserDisallowedPackageName(packageName) + debounceSave() + } + + private fun debounceSave() { + saveJob?.cancel() + saveJob = + viewModelScope.launch { + delay(500) // Wait to batch multiple rapid updates + App.get().updateUserDisallowedPackageNames(excludedPackageNames.value) + } } } From d5988faf9acbb0902b4fb83bebb5d149efc2dd52 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Tue, 20 May 2025 10:42:53 -0700 Subject: [PATCH 4/5] android: add IME action to trigger custom CustomLogin (#649) Updates tailscale/tailscale#14864 Signed-off-by: kari-ts --- .../main/java/com/tailscale/ipn/ui/view/CustomLogin.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt b/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt index 991ec39..83ae0b8 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button import androidx.compose.material3.ListItem @@ -26,6 +27,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.unit.dp import com.tailscale.ipn.R @@ -140,7 +142,11 @@ fun LoginView( placeholder = { Text(strings.placeholder, style = MaterialTheme.typography.bodySmall) }, - keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.None)) + keyboardOptions = + KeyboardOptions( + capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Go), + keyboardActions = + KeyboardActions(onGo = { onSubmitAction(textVal) })) }) ListItem( From f01fb7062b731e62cc8246f6e4d71f853c602b8d Mon Sep 17 00:00:00 2001 From: Jonathan Nobels Date: Tue, 20 May 2025 14:02:34 -0400 Subject: [PATCH 5/5] android: bump OSS (#652) OSS and Version updated to 1.83.237-tc4fb380f3-g7f56d0c0f Signed-off-by: Jonathan Nobels --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2e80ca7..1c94d33 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 require ( github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 golang.org/x/mobile v0.0.0-20240806205939-81131f6468ab - tailscale.com v1.83.0-pre.0.20250515212619-336b3b7df0ab + tailscale.com v1.83.0-pre.0.20250520103045-c4fb380f3c5f ) require ( diff --git a/go.sum b/go.sum index 1bc03ac..936537d 100644 --- a/go.sum +++ b/go.sum @@ -243,5 +243,5 @@ howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.83.0-pre.0.20250515212619-336b3b7df0ab h1:jYAOBs7APf5DopDjTZWTXbUJBp7izN9oBb3fZ4hhC4o= -tailscale.com v1.83.0-pre.0.20250515212619-336b3b7df0ab/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo= +tailscale.com v1.83.0-pre.0.20250520103045-c4fb380f3c5f h1:C5jwV0h09WVzAVBEAYCR848PqVZJQIqB84ZiS2NmCZQ= +tailscale.com v1.83.0-pre.0.20250520103045-c4fb380f3c5f/go.mod h1:6/S63NMAhmncYT/1zIPDJkvCuZwMw+JnUuOfSPNazpo=