// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause //go:build ios package interfaces import ( "log" "tailscale.com/syncs" ) var ( lastKnownDefaultRouteIfName syncs.AtomicValue[string] ) // UpdateLastKnownDefaultRouteInterface is called by ipn-go-bridge in the iOS app when // our NWPathMonitor instance detects a network path transition. func UpdateLastKnownDefaultRouteInterface(ifName string) { if ifName == "" { return } lastKnownDefaultRouteIfName.Store(ifName) log.Printf("defaultroute_ios: update from Swift, ifName = %s", ifName) } func defaultRoute() (d DefaultRouteDetails, err error) { // We cannot rely on the delegated interface data on iOS. The NetworkExtension framework // seems to set the delegate interface only once, upon the *creation* of the VPN tunnel. // If a network transition (e.g. from Wi-Fi to Cellular) happens while the tunnel is // connected, it will be ignored and we will still try to set Wi-Fi as the default route // because the delegated interface is not updated by the NetworkExtension framework. // // We work around this on the Swift side with a NWPathMonitor instance that observes // the interface name of the first currently satisfied network path. Our Swift code will // call into `UpdateLastKnownDefaultRouteInterface`, so we can rely on that when it is set. // // If for any reason the Swift machinery didn't work and we don't get any updates, here // we also have some fallback logic: we try finding a hardcoded Wi-Fi interface called en0. // If en0 is down, we fall back to cellular (pdp_ip0) as a last resort. This doesn't handle // all edge cases like USB-Ethernet adapters or multiple Ethernet interfaces, but is good // enough to ensure connectivity isn't broken. // Start by getting all available interfaces. interfaces, err := netInterfaces() if err != nil { log.Printf("defaultroute_ios: could not get interfaces: %v", err) return d, ErrNoGatewayIndexFound } getInterfaceByName := func(name string) *Interface { for _, ifc := range interfaces { if ifc.Name != name { continue } if !ifc.IsUp() { log.Println("defaultroute_ios: %s is down", name) return nil } addrs, _ := ifc.Addrs() if len(addrs) == 0 { log.Println("defaultroute_ios: %s has no addresses", name) return nil } return &ifc } return nil } // Did Swift set lastKnownDefaultRouteInterface? If so, we should use it and don't bother // with anything else. However, for sanity, do check whether Swift gave us with an interface // that exists, is up, and has an address. if swiftIfName := lastKnownDefaultRouteIfName.Load(); swiftIfName != "" { ifc := getInterfaceByName(swiftIfName) if ifc != nil { log.Printf("defaultroute_ios: using %s (provided by Swift)", ifc.Name) d.InterfaceName = ifc.Name d.InterfaceIndex = ifc.Index return d, nil } } // Start of our fallback logic if Swift didn't give us an interface name, or gave us an invalid // one. // We start by attempting to use the Wi-Fi interface, which on iPhone is always called en0. enZeroIf := getInterfaceByName("en0") if enZeroIf != nil { log.Println("defaultroute_ios: using en0 (fallback)") d.InterfaceName = enZeroIf.Name d.InterfaceIndex = enZeroIf.Index return d, nil } // Did it not work? Let's try with Cellular (pdp_ip0). cellIf := getInterfaceByName("pdp_ip0") if cellIf != nil { log.Println("defaultroute_ios: using pdp_ip0 (fallback)") d.InterfaceName = cellIf.Name d.InterfaceIndex = cellIf.Index return d, nil } log.Println("defaultroute_ios: no running interfaces available") return d, ErrNoGatewayIndexFound }