You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale-android/pkg/tailscale/tailscale.go

829 lines
23 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"context"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/netip"
"path/filepath"
"reflect"
"strings"
"sync"
"sync/atomic"
"time"
"unsafe"
jnipkg "github.com/tailscale/tailscale-android/pkg/jni"
"github.com/tailscale/tailscale-android/pkg/localapiservice"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/unix"
"inet.af/netaddr"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/logpolicy"
"tailscale.com/logtail"
"tailscale.com/logtail/filch"
"tailscale.com/net/dns"
"tailscale.com/net/interfaces"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/tsdial"
"tailscale.com/paths"
"tailscale.com/smallzstd"
"tailscale.com/tsd"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/netmap"
"tailscale.com/util/clientmetric"
"tailscale.com/util/dnsname"
"tailscale.com/util/must"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
)
import "C"
var dataDirChan = make(chan string, 1)
var (
dataDirOnce sync.Once
dataPath string
)
func dataDir() (string, error) {
dataDirOnce.Do(func() {
dataPath = <-dataDirChan
})
return dataPath, nil
}
var (
// googleClass is a global reference to the com.tailscale.ipn.Google class.
googleClass jnipkg.Class
)
type App struct {
jvm *jnipkg.JVM
// appCtx is a global reference to the com.tailscale.ipn.App instance.
appCtx jnipkg.Object
store *stateStore
logIDPublicAtomic atomic.Pointer[logid.PublicID]
localAPI *localapiservice.LocalAPIService
backend *ipnlocal.LocalBackend
// invalidates receives whenever the window should be refreshed.
invalidates chan struct{}
}
type BackendState struct {
State ipn.State
NetworkMap *netmap.NetworkMap
LostInternet bool
}
type backend struct {
engine wgengine.Engine
backend *ipnlocal.LocalBackend
sys *tsd.System
devices *multiTUN
settings settingsFunc
lastCfg *router.Config
lastDNSCfg *dns.OSConfig
netMon *netmon.Monitor
logIDPublic logid.PublicID
logger *logtail.Logger
// avoidEmptyDNS controls whether to use fallback nameservers
// when no nameservers are provided by Tailscale.
avoidEmptyDNS bool
jvm *jnipkg.JVM
appCtx jnipkg.Object
}
type settingsFunc func(*router.Config, *dns.OSConfig) error
const defaultMTU = 1280 // minimalMTU from wgengine/userspace.go
type ConnectEvent struct {
Enable bool
}
const (
logPrefKey = "privatelogid"
loginMethodPrefKey = "loginmethod"
customLoginServerPrefKey = "customloginserver"
)
func main() {
a := &App{
jvm: (*jnipkg.JVM)(unsafe.Pointer(javaVM())),
appCtx: jnipkg.Object(appContext()),
invalidates: make(chan struct{}, 1),
}
err := a.loadJNIGlobalClassRefs()
if err != nil {
fatalErr(err)
}
a.store = newStateStore(a.jvm, a.appCtx)
interfaces.RegisterInterfaceGetter(a.getInterfaces)
go func() {
ctx := context.Background()
if err := a.runBackend(ctx); err != nil {
fatalErr(err)
}
}()
}
func (a *App) runBackend(ctx context.Context) error {
appDir, err := dataDir()
if err != nil {
fatalErr(err)
}
paths.AppSharedDir.Store(appDir)
hostinfo.SetOSVersion(a.osVersion())
if !googleSignInEnabled() {
hostinfo.SetPackage("nogoogle")
}
deviceModel := a.modelName()
if a.isChromeOS() {
deviceModel = "ChromeOS: " + deviceModel
}
hostinfo.SetDeviceModel(deviceModel)
type configPair struct {
rcfg *router.Config
dcfg *dns.OSConfig
}
configs := make(chan configPair)
configErrs := make(chan error)
b, err := newBackend(appDir, a.jvm, a.appCtx, a.store, func(rcfg *router.Config, dcfg *dns.OSConfig) error {
if rcfg == nil {
return nil
}
configs <- configPair{rcfg, dcfg}
return <-configErrs
})
if err != nil {
return err
}
a.logIDPublicAtomic.Store(&b.logIDPublic)
a.backend = b.backend
defer b.CloseTUNs()
h := localapi.NewHandler(b.backend, log.Printf, b.sys.NetMon.Get(), *a.logIDPublicAtomic.Load())
h.PermitRead = true
h.PermitWrite = true
a.localAPI = localapiservice.New(h)
// Share the localAPI with the JNI shim
//localapiservice.SetLocalAPIService(a.localAPI)
localapiservice.ConfigureShim(a.jvm, a.appCtx, a.localAPI, b.backend)
// Contrary to the documentation for VpnService.Builder.addDnsServer,
// ChromeOS doesn't fall back to the underlying network nameservers if
// we don't provide any.
b.avoidEmptyDNS = a.isChromeOS()
var (
cfg configPair
state BackendState
service jnipkg.Object // of IPNService
)
a.localAPI.Start(ctx, b.backend, ipn.Options{})
for {
select {
case c := <-configs:
cfg = c
if b == nil || service == 0 || cfg.rcfg == nil {
configErrs <- nil
break
}
configErrs <- b.updateTUN(service, cfg.rcfg, cfg.dcfg)
case s := <-onVPNRequested:
jnipkg.Do(a.jvm, func(env *jnipkg.Env) error {
if jnipkg.IsSameObject(env, s, service) {
// We already have a reference.
jnipkg.DeleteGlobalRef(env, s)
return nil
}
if service != 0 {
jnipkg.DeleteGlobalRef(env, service)
}
netns.SetAndroidProtectFunc(func(fd int) error {
return jnipkg.Do(a.jvm, func(env *jnipkg.Env) error {
// Call https://developer.android.com/reference/android/net/VpnService#protect(int)
// to mark fd as a socket that should bypass the VPN and use the underlying network.
cls := jnipkg.GetObjectClass(env, s)
m := jnipkg.GetMethodID(env, cls, "protect", "(I)Z")
ok, err := jnipkg.CallBooleanMethod(env, s, m, jnipkg.Value(fd))
// TODO(bradfitz): return an error back up to netns if this fails, once
// we've had some experience with this and analyzed the logs over a wide
// range of Android phones. For now we're being paranoid and conservative
// and do the JNI call to protect best effort, only logging if it fails.
// The risk of returning an error is that it breaks users on some Android
// versions even when they're not using exit nodes. I'd rather the
// relatively few number of exit node users file bug reports if Tailscale
// doesn't work and then we can look for this log print.
if err != nil || !ok {
log.Printf("[unexpected] VpnService.protect(%d) = %v, %v", fd, ok, err)
}
return nil // even on error. see big TODO above.
})
})
log.Printf("onVPNRequested: rebind required")
// TODO(catzkorn): When we start the android application
// we bind sockets before we have access to the VpnService.protect()
// function which is needed to avoid routing loops. When we activate
// the service we get access to the protect, but do not retrospectively
// protect the sockets already opened, which breaks connectivity.
// As a temporary fix, we rebind and protect the magicsock.Conn on connect
// which restores connectivity.
// See https://github.com/tailscale/corp/issues/13814
b.backend.DebugRebind()
service = s
return nil
})
if m := state.NetworkMap; m != nil {
// TODO
}
if cfg.rcfg != nil && state.State >= ipn.Starting {
if err := b.updateTUN(service, cfg.rcfg, cfg.dcfg); err != nil {
log.Printf("VPN update failed: %v", err)
notifyVPNClosed()
}
}
case s := <-onDisconnect:
b.CloseTUNs()
jnipkg.Do(a.jvm, func(env *jnipkg.Env) error {
defer jnipkg.DeleteGlobalRef(env, s)
if jnipkg.IsSameObject(env, service, s) {
netns.SetAndroidProtectFunc(nil)
jnipkg.DeleteGlobalRef(env, service)
service = 0
}
return nil
})
if state.State >= ipn.Starting {
notifyVPNClosed()
}
case <-onDNSConfigChanged:
if b != nil {
go b.NetworkChanged()
}
}
}
}
// errVPNNotPrepared is used when VPNService.Builder.establish returns
// null, either because the VPNService is not yet prepared or because
// VPN status was revoked.
var errVPNNotPrepared = errors.New("VPN service not prepared or was revoked")
// errMultipleUsers is used when we get a "INTERACT_ACROSS_USERS" error, which
// happens due to a bug in Android. See:
//
// https://github.com/tailscale/tailscale/issues/2180
var errMultipleUsers = errors.New("VPN cannot be created on this device due to an Android bug with multiple users")
func newBackend(dataDir string, jvm *jnipkg.JVM, appCtx jnipkg.Object, store *stateStore,
settings settingsFunc) (*backend, error) {
sys := new(tsd.System)
sys.Set(store)
logf := logger.RusagePrefixLog(log.Printf)
b := &backend{
jvm: jvm,
devices: newTUNDevices(),
settings: settings,
appCtx: appCtx,
}
var logID logid.PrivateID
logID.UnmarshalText([]byte("dead0000dead0000dead0000dead0000dead0000dead0000dead0000dead0000"))
storedLogID, err := store.read(logPrefKey)
// In all failure cases we ignore any errors and continue with the dead value above.
if err != nil || storedLogID == nil {
// Read failed or there was no previous log id.
newLogID, err := logid.NewPrivateID()
if err == nil {
logID = newLogID
enc, err := newLogID.MarshalText()
if err == nil {
store.write(logPrefKey, enc)
}
}
} else {
logID.UnmarshalText([]byte(storedLogID))
}
netMon, err := netmon.New(logf)
if err != nil {
log.Printf("netmon.New: %w", err)
}
b.netMon = netMon
b.setupLogs(dataDir, logID, logf)
dialer := new(tsdial.Dialer)
cb := &router.CallbackRouter{
SetBoth: b.setCfg,
SplitDNS: false,
GetBaseConfigFunc: b.getDNSBaseConfig,
}
engine, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: b.devices,
Router: cb,
DNS: cb,
Dialer: dialer,
SetSubsystem: sys.Set,
NetMon: b.netMon,
})
if err != nil {
return nil, fmt.Errorf("runBackend: NewUserspaceEngine: %v", err)
}
sys.Set(engine)
b.logIDPublic = logID.Public()
ns, err := netstack.Create(logf, sys.Tun.Get(), engine, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper(), nil)
if err != nil {
return nil, fmt.Errorf("netstack.Create: %w", err)
}
sys.Set(ns)
ns.ProcessLocalIPs = false // let Android kernel handle it; VpnBuilder sets this up
ns.ProcessSubnets = true // for Android-being-an-exit-node support
sys.NetstackRouter.Set(true)
if w, ok := sys.Tun.GetOK(); ok {
w.Start()
}
lb, err := ipnlocal.NewLocalBackend(logf, logID.Public(), sys, 0)
if err != nil {
engine.Close()
return nil, fmt.Errorf("runBackend: NewLocalBackend: %v", err)
}
if err := ns.Start(lb); err != nil {
return nil, fmt.Errorf("startNetstack: %w", err)
}
if b.logger != nil {
lb.SetLogFlusher(b.logger.StartFlush)
}
b.engine = engine
b.backend = lb
b.sys = sys
return b, nil
}
func fatalErr(err error) {
// TODO: expose in UI.
log.Printf("fatal error: %v", err)
}
func javaVM() uintptr {
android.mu.Lock()
defer android.mu.Unlock()
return uintptr(unsafe.Pointer(android.jvm))
}
func appContext() uintptr {
android.mu.Lock()
defer android.mu.Unlock()
return uintptr(android.appCtx)
}
// Report interfaces in the device in net.Interface format.
func (a *App) getInterfaces() ([]interfaces.Interface, error) {
var ifaceString string
err := jnipkg.Do(a.jvm, func(env *jnipkg.Env) error {
cls := jnipkg.GetObjectClass(env, a.appCtx)
m := jnipkg.GetMethodID(env, cls, "getInterfacesAsString", "()Ljava/lang/String;")
n, err := jnipkg.CallObjectMethod(env, a.appCtx, m)
ifaceString = jnipkg.GoString(env, jnipkg.String(n))
return err
})
var ifaces []interfaces.Interface
if err != nil {
return ifaces, err
}
for _, iface := range strings.Split(ifaceString, "\n") {
// Example of the strings we're processing:
// wlan0 30 1500 true true false false true | fe80::2f60:2c82:4163:8389%wlan0/64 10.1.10.131/24
// r_rmnet_data0 21 1500 true false false false false | fe80::9318:6093:d1ad:ba7f%r_rmnet_data0/64
// mnet_data2 12 1500 true false false false false | fe80::3c8c:44dc:46a9:9907%rmnet_data2/64
if strings.TrimSpace(iface) == "" {
continue
}
fields := strings.Split(iface, "|")
if len(fields) != 2 {
log.Printf("getInterfaces: unable to split %q", iface)
continue
}
var name string
var index, mtu int
var up, broadcast, loopback, pointToPoint, multicast bool
_, err := fmt.Sscanf(fields[0], "%s %d %d %t %t %t %t %t",
&name, &index, &mtu, &up, &broadcast, &loopback, &pointToPoint, &multicast)
if err != nil {
log.Printf("getInterfaces: unable to parse %q: %v", iface, err)
continue
}
newIf := interfaces.Interface{
Interface: &net.Interface{
Name: name,
Index: index,
MTU: mtu,
},
AltAddrs: []net.Addr{}, // non-nil to avoid Go using netlink
}
if up {
newIf.Flags |= net.FlagUp
}
if broadcast {
newIf.Flags |= net.FlagBroadcast
}
if loopback {
newIf.Flags |= net.FlagLoopback
}
if pointToPoint {
newIf.Flags |= net.FlagPointToPoint
}
if multicast {
newIf.Flags |= net.FlagMulticast
}
addrs := strings.Trim(fields[1], " \n")
for _, addr := range strings.Split(addrs, " ") {
ip, err := netaddr.ParseIPPrefix(addr)
if err == nil {
newIf.AltAddrs = append(newIf.AltAddrs, ip.IPNet())
}
}
ifaces = append(ifaces, newIf)
}
return ifaces, nil
}
// osVersion returns android.os.Build.VERSION.RELEASE. " [nogoogle]" is appended
// if Google Play services are not compiled in.
func (a *App) osVersion() string {
var version string
err := jnipkg.Do(a.jvm, func(env *jnipkg.Env) error {
cls := jnipkg.GetObjectClass(env, a.appCtx)
m := jnipkg.GetMethodID(env, cls, "getOSVersion", "()Ljava/lang/String;")
n, err := jnipkg.CallObjectMethod(env, a.appCtx, m)
version = jnipkg.GoString(env, jnipkg.String(n))
return err
})
if err != nil {
panic(err)
}
return version
}
// modelName return the MANUFACTURER + MODEL from
// android.os.Build.
func (a *App) modelName() string {
var model string
err := jnipkg.Do(a.jvm, func(env *jnipkg.Env) error {
cls := jnipkg.GetObjectClass(env, a.appCtx)
m := jnipkg.GetMethodID(env, cls, "getModelName", "()Ljava/lang/String;")
n, err := jnipkg.CallObjectMethod(env, a.appCtx, m)
model = jnipkg.GoString(env, jnipkg.String(n))
return err
})
if err != nil {
panic(err)
}
return model
}
func (a *App) isChromeOS() bool {
var chromeOS bool
err := jnipkg.Do(a.jvm, func(env *jnipkg.Env) error {
cls := jnipkg.GetObjectClass(env, a.appCtx)
m := jnipkg.GetMethodID(env, cls, "isChromeOS", "()Z")
b, err := jnipkg.CallBooleanMethod(env, a.appCtx, m)
chromeOS = b
return err
})
if err != nil {
panic(err)
}
return chromeOS
}
func googleSignInEnabled() bool {
return googleClass != 0
}
// Loads the global JNI class references. Failures here are fatal if the
// class ref is required for the app to function.
func (a *App) loadJNIGlobalClassRefs() error {
return jnipkg.Do(a.jvm, func(env *jnipkg.Env) error {
loader := jnipkg.ClassLoaderFor(env, a.appCtx)
cl, err := jnipkg.LoadClass(env, loader, "com.tailscale.ipn.Google")
if err != nil {
// Ignore load errors; the Google class is not included in F-Droid builds.
return nil
}
googleClass = jnipkg.Class(jnipkg.NewGlobalRef(env, jnipkg.Object(cl)))
return nil
})
}
// googleDNSServers are used on ChromeOS, where an empty VpnBuilder DNS setting results
// in erasing the platform DNS servers. The developer docs say this is not supposed to happen,
// but nonetheless it does.
var googleDNSServers = []netip.Addr{
netip.MustParseAddr("8.8.8.8"),
netip.MustParseAddr("8.8.4.4"),
netip.MustParseAddr("2001:4860:4860::8888"),
netip.MustParseAddr("2001:4860:4860::8844"),
}
func (b *backend) updateTUN(service jnipkg.Object, rcfg *router.Config, dcfg *dns.OSConfig) error {
if reflect.DeepEqual(rcfg, b.lastCfg) && reflect.DeepEqual(dcfg, b.lastDNSCfg) {
return nil
}
// Close previous tunnel(s).
// This is necessary for ChromeOS, native Android devices
// seem to handle seamless handover between tunnels correctly.
//
// TODO(eliasnaur): If seamless handover becomes a desirable feature, skip
// the closing on ChromeOS.
b.CloseTUNs()
if len(rcfg.LocalAddrs) == 0 {
return nil
}
err := jnipkg.Do(b.jvm, func(env *jnipkg.Env) error {
cls := jnipkg.GetObjectClass(env, service)
// Construct a VPNService.Builder. IPNService.newBuilder calls
// setConfigureIntent, and allowFamily for both IPv4 and IPv6.
m := jnipkg.GetMethodID(env, cls, "newBuilder", "()Landroid/net/VpnService$Builder;")
builder, err := jnipkg.CallObjectMethod(env, service, m)
if err != nil {
return fmt.Errorf("IPNService.newBuilder: %v", err)
}
bcls := jnipkg.GetObjectClass(env, builder)
// builder.setMtu.
setMtu := jnipkg.GetMethodID(env, bcls, "setMtu", "(I)Landroid/net/VpnService$Builder;")
const mtu = defaultMTU
if _, err := jnipkg.CallObjectMethod(env, builder, setMtu, jnipkg.Value(mtu)); err != nil {
return fmt.Errorf("VpnService.Builder.setMtu: %v", err)
}
// builder.addDnsServer
addDnsServer := jnipkg.GetMethodID(env, bcls, "addDnsServer", "(Ljava/lang/String;)Landroid/net/VpnService$Builder;")
// builder.addSearchDomain.
addSearchDomain := jnipkg.GetMethodID(env, bcls, "addSearchDomain", "(Ljava/lang/String;)Landroid/net/VpnService$Builder;")
if dcfg != nil {
nameservers := dcfg.Nameservers
if b.avoidEmptyDNS && len(nameservers) == 0 {
nameservers = googleDNSServers
}
for _, dns := range nameservers {
_, err = jnipkg.CallObjectMethod(env,
builder,
addDnsServer,
jnipkg.Value(jnipkg.JavaString(env, dns.String())),
)
if err != nil {
return fmt.Errorf("VpnService.Builder.addDnsServer(%v): %v", dns, err)
}
}
for _, dom := range dcfg.SearchDomains {
_, err = jnipkg.CallObjectMethod(env,
builder,
addSearchDomain,
jnipkg.Value(jnipkg.JavaString(env, dom.WithoutTrailingDot())),
)
if err != nil {
return fmt.Errorf("VpnService.Builder.addSearchDomain(%v): %v", dom, err)
}
}
}
// builder.addRoute.
addRoute := jnipkg.GetMethodID(env, bcls, "addRoute", "(Ljava/lang/String;I)Landroid/net/VpnService$Builder;")
for _, route := range rcfg.Routes {
// Normalize route address; Builder.addRoute does not accept non-zero masked bits.
route = route.Masked()
_, err = jnipkg.CallObjectMethod(env,
builder,
addRoute,
jnipkg.Value(jnipkg.JavaString(env, route.Addr().String())),
jnipkg.Value(route.Bits()),
)
if err != nil {
return fmt.Errorf("VpnService.Builder.addRoute(%v): %v", route, err)
}
}
// builder.addAddress.
addAddress := jnipkg.GetMethodID(env, bcls, "addAddress", "(Ljava/lang/String;I)Landroid/net/VpnService$Builder;")
for _, addr := range rcfg.LocalAddrs {
_, err = jnipkg.CallObjectMethod(env,
builder,
addAddress,
jnipkg.Value(jnipkg.JavaString(env, addr.Addr().String())),
jnipkg.Value(addr.Bits()),
)
if err != nil {
return fmt.Errorf("VpnService.Builder.addAddress(%v): %v", addr, err)
}
}
// builder.establish.
establish := jnipkg.GetMethodID(env, bcls, "establish", "()Landroid/os/ParcelFileDescriptor;")
parcelFD, err := jnipkg.CallObjectMethod(env, builder, establish)
if err != nil {
if strings.Contains(err.Error(), "INTERACT_ACROSS_USERS") {
return errMultipleUsers
}
return fmt.Errorf("VpnService.Builder.establish: %v", err)
}
if parcelFD == 0 {
return errVPNNotPrepared
}
// detachFd.
parcelCls := jnipkg.GetObjectClass(env, parcelFD)
detachFd := jnipkg.GetMethodID(env, parcelCls, "detachFd", "()I")
tunFD, err := jnipkg.CallIntMethod(env, parcelFD, detachFd)
if err != nil {
return fmt.Errorf("detachFd: %v", err)
}
// Create TUN device.
tunDev, _, err := tun.CreateUnmonitoredTUNFromFD(int(tunFD))
if err != nil {
unix.Close(int(tunFD))
return err
}
b.devices.add(tunDev)
return nil
})
if err != nil {
b.lastCfg = nil
b.CloseTUNs()
return err
}
b.lastCfg = rcfg
b.lastDNSCfg = dcfg
return nil
}
// CloseVPN closes any active TUN devices.
func (b *backend) CloseTUNs() {
b.lastCfg = nil
b.devices.Shutdown()
}
func (b *backend) NetworkChanged() {
if b.sys != nil {
if nm, ok := b.sys.NetMon.GetOK(); ok {
nm.InjectEvent()
}
}
}
func (b *backend) setCfg(rcfg *router.Config, dcfg *dns.OSConfig) error {
return b.settings(rcfg, dcfg)
}
// SetupLogs sets up remote logging.
func (b *backend) setupLogs(logDir string, logID logid.PrivateID, logf logger.Logf) {
if b.netMon == nil {
panic("netMon must be created prior to SetupLogs")
}
transport := logpolicy.NewLogtailTransport(logtail.DefaultHost, b.netMon, log.Printf)
logcfg := logtail.Config{
Collection: logtail.CollectionNode,
PrivateID: logID,
Stderr: log.Writer(),
MetricsDelta: clientmetric.EncodeLogTailMetricsDelta,
IncludeProcID: true,
IncludeProcSequence: true,
NewZstdEncoder: func() logtail.Encoder {
return must.Get(smallzstd.NewEncoder(nil))
},
HTTPC: &http.Client{Transport: transport},
}
logcfg.FlushDelayFn = func() time.Duration { return 2 * time.Minute }
filchOpts := filch.Options{
ReplaceStderr: true,
}
var filchErr error
if logDir != "" {
logPath := filepath.Join(logDir, "ipn.log.")
logcfg.Buffer, filchErr = filch.New(logPath, filchOpts)
}
b.logger = logtail.NewLogger(logcfg, logf)
log.SetFlags(0)
log.SetOutput(b.logger)
log.Printf("goSetupLogs: success")
if logDir == "" {
log.Printf("SetupLogs: no logDir, storing logs in memory")
}
if filchErr != nil {
log.Printf("SetupLogs: filch setup failed: %v", filchErr)
}
}
func (b *backend) getDNSBaseConfig() (ret dns.OSConfig, _ error) {
defer func() {
// If we couldn't find any base nameservers, ultimately fall back to
// Google's. Normally Tailscale doesn't ever pick a default nameserver
// for users but in this case Android's APIs for reading the underlying
// DNS config are lacking, and almost all Android phones use Google
// services anyway, so it's a reasonable default: it's an ecosystem the
// user has selected by having an Android device.
if len(ret.Nameservers) == 0 && googleSignInEnabled() {
log.Printf("getDNSBaseConfig: none found; falling back to Google public DNS")
ret.Nameservers = append(ret.Nameservers, googleDNSServers...)
}
}()
baseConfig := b.getPlatformDNSConfig()
lines := strings.Split(baseConfig, "\n")
if len(lines) == 0 {
return dns.OSConfig{}, nil
}
config := dns.OSConfig{}
addrs := strings.Trim(lines[0], " \n")
for _, addr := range strings.Split(addrs, " ") {
ip, err := netip.ParseAddr(addr)
if err == nil {
config.Nameservers = append(config.Nameservers, ip)
}
}
if len(lines) > 1 {
for _, s := range strings.Split(strings.Trim(lines[1], " \n"), " ") {
domain, err := dnsname.ToFQDN(s)
if err != nil {
log.Printf("getDNSBaseConfig: unable to parse %q: %v", s, err)
continue
}
config.SearchDomains = append(config.SearchDomains, domain)
}
}
return config, nil
}
func (b *backend) getPlatformDNSConfig() string {
var baseConfig string
err := jnipkg.Do(b.jvm, func(env *jnipkg.Env) error {
cls := jnipkg.GetObjectClass(env, b.appCtx)
m := jnipkg.GetMethodID(env, cls, "getDnsConfigObj", "()Lcom/tailscale/ipn/DnsConfig;")
dns, err := jnipkg.CallObjectMethod(env, b.appCtx, m)
if err != nil {
return fmt.Errorf("getDnsConfigObj: %v", err)
}
dnsCls := jnipkg.GetObjectClass(env, dns)
m = jnipkg.GetMethodID(env, dnsCls, "getDnsConfigAsString", "()Ljava/lang/String;")
n, err := jnipkg.CallObjectMethod(env, dns, m)
baseConfig = jnipkg.GoString(env, jnipkg.String(n))
return err
})
if err != nil {
log.Printf("getPlatformDNSConfig JNI: %v", err)
return ""
}
return baseConfig
}