From 96c29dbd546297103978055f7d049eab34281adf Mon Sep 17 00:00:00 2001 From: kari-ts Date: Thu, 12 Jun 2025 14:16:08 -0700 Subject: [PATCH] android: support allow LAN access for API < 33 Don't add routes from LocalRoutes for devices running API < 33 --- .../src/main/java/com/tailscale/ipn/App.kt | 2 ++ .../tailscale/ipn/ui/view/ExitNodePicker.kt | 4 +--- libtailscale/interfaces.go | 3 +++ libtailscale/net.go | 20 +++++++++++++++++++ libtailscale/tailscale.go | 9 +++++++++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index fdbd295..ebe9528 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -289,6 +289,8 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { override fun getOSVersion(): String = Build.VERSION.RELEASE + override fun getAPILevel(): Int = Build.VERSION.SDK_INT + override fun isChromeOS(): Boolean { return packageManager.hasSystemFeature("android.hardware.type.pc") } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt b/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt index 827925b..7e8b02f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/ExitNodePicker.kt @@ -100,9 +100,7 @@ fun ExitNodePicker( } } - // https://developer.android.com/reference/android/net/VpnService.Builder#excludeRoute(android.net.IpPrefix) - excludeRoute is only supported in API 33+, so don't show the option if allow LAN access is not enabled. - if (!allowLanAccessMDMDisposition.value.hiddenFromUser && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (!allowLanAccessMDMDisposition.value.hiddenFromUser) { item(key = "allowLANAccess") { Lists.SectionDivider() diff --git a/libtailscale/interfaces.go b/libtailscale/interfaces.go index 5663698..1ee4339 100644 --- a/libtailscale/interfaces.go +++ b/libtailscale/interfaces.go @@ -32,6 +32,9 @@ type AppContext interface { // GetOSVersion gets the Android version. GetOSVersion() (string, error) + // GetAPILevel gets the SDK version. + GetAPILevel() (int32, error) + // GetModelName gets the Android device's model name. GetModelName() (string, error) diff --git a/libtailscale/net.go b/libtailscale/net.go index 0405316..2e44323 100644 --- a/libtailscale/net.go +++ b/libtailscale/net.go @@ -165,9 +165,20 @@ func (b *backend) updateTUN(rcfg *router.Config, dcfg *dns.OSConfig) error { b.logger.Logf("updateTUN: set nameservers") } + apiLevel, err := b.appCtx.GetAPILevel() + supportsExcludeRoutes := false + if err == nil && apiLevel >= 33 { + supportsExcludeRoutes = true + } + for _, route := range rcfg.Routes { // Normalize route address; Builder.addRoute does not accept non-zero masked bits. route = route.Masked() + + if !supportsExcludeRoutes && isLocalRoute(rcfg, route) { + continue + } + if err := builder.AddRoute(route.Addr().String(), int32(route.Bits())); err != nil { return err } @@ -237,6 +248,15 @@ func (b *backend) updateTUN(rcfg *router.Config, dcfg *dns.OSConfig) error { return nil } +func isLocalRoute(rcfg *router.Config, r netip.Prefix) bool { + for _, lr := range rcfg.LocalRoutes { + if lr.Masked() == r { + return true + } + } + return false +} + func closeFileDescriptor() error { if vpnService.fd != -1 && vpnService.fdDetached { err := syscall.Close(int(vpnService.fd)) diff --git a/libtailscale/tailscale.go b/libtailscale/tailscale.go index 3a785fa..3fc41e1 100644 --- a/libtailscale/tailscale.go +++ b/libtailscale/tailscale.go @@ -77,6 +77,15 @@ func (a *App) osVersion() string { return version } +// apiLevel returns android.os.Build.VERSION.SDK_INT. +func (a *App) apiLevel() int32 { + level, err := a.appCtx.GetAPILevel() + if err != nil { + panic(err) + } + return level +} + // modelName return the MANUFACTURER + MODEL from // android.os.Build. func (a *App) modelName() string {