From 79f3a5d753806fff79a8ae6186f4f1f36c0f1027 Mon Sep 17 00:00:00 2001 From: Mihai Parparita Date: Tue, 29 Nov 2022 17:54:45 -0800 Subject: [PATCH] net/netns, net/interfaces: explicitly bind sockets to the default interface on all Darwin variants We were previously only doing this for tailscaled-on-Darwin, but it also appears to help on iOS. Otherwise, when we rebind magicsock UDP connections after a cellular -> WiFi interface change they still keep using cellular one. To do this correctly when using exit nodes, we need to exclude the Tailscale interface when getting the default route, otherwise packets cannot leave the tunnel. There are native macOS/iOS APIs that we can use to do this, so we allow those clients to override the implementation of DefaultRouteInterfaceIndex. Updates #6565, may also help with #5156 Signed-off-by: Mihai Parparita --- net/interfaces/interfaces_bsd.go | 21 ++++++++ ...s_darwin_tailscaled.go => netns_darwin.go} | 37 ++++++++++--- net/netns/netns_default.go | 2 +- net/netns/netns_macios.go | 53 ------------------- 4 files changed, 52 insertions(+), 61 deletions(-) rename net/netns/{netns_darwin_tailscaled.go => netns_darwin.go} (59%) delete mode 100644 net/netns/netns_macios.go diff --git a/net/interfaces/interfaces_bsd.go b/net/interfaces/interfaces_bsd.go index 970f64278..6db9458ea 100644 --- a/net/interfaces/interfaces_bsd.go +++ b/net/interfaces/interfaces_bsd.go @@ -20,6 +20,7 @@ import ( "golang.org/x/net/route" "golang.org/x/sys/unix" "tailscale.com/net/netaddr" + "tailscale.com/syncs" ) func defaultRoute() (d DefaultRouteDetails, err error) { @@ -36,7 +37,17 @@ func defaultRoute() (d DefaultRouteDetails, err error) { return d, nil } +// DefaultRouteInterfaceIndex returns the index of the network interface that +// owns the default route. It returns the first IPv4 or IPv6 default route it +// finds (it does not prefer one or the other). func DefaultRouteInterfaceIndex() (int, error) { + if f := defaultRouteInterfaceIndexFunc.Load(); f != nil { + if ifIndex := f(); ifIndex != 0 { + return ifIndex, nil + } + // Fallthrough if we can't use the alternate implementation. + } + // $ netstat -nr // Routing tables // Internet: @@ -71,6 +82,16 @@ func DefaultRouteInterfaceIndex() (int, error) { return 0, errors.New("no gateway index found") } +var defaultRouteInterfaceIndexFunc syncs.AtomicValue[func() int] + +// SetDefaultRouteInterfaceIndexFunc allows an alternate implementation of +// DefaultRouteInterfaceIndex to be provided. If none is set, or if f() returns a 0 +// (indicating an unknown interface index), then the default implementation (that parses +// the routing table) will be used. +func SetDefaultRouteInterfaceIndexFunc(f func() int) { + defaultRouteInterfaceIndexFunc.Store(f) +} + func init() { likelyHomeRouterIP = likelyHomeRouterIPBSDFetchRIB } diff --git a/net/netns/netns_darwin_tailscaled.go b/net/netns/netns_darwin.go similarity index 59% rename from net/netns/netns_darwin_tailscaled.go rename to net/netns/netns_darwin.go index 4731dbd88..a383a2df2 100644 --- a/net/netns/netns_darwin_tailscaled.go +++ b/net/netns/netns_darwin.go @@ -2,12 +2,15 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build darwin && !ts_macext +//go:build darwin package netns import ( + "errors" "fmt" + "log" + "net" "strings" "syscall" @@ -27,7 +30,7 @@ func control(logf logger.Logf) func(network, address string, c syscall.RawConn) // It's intentionally the same signature as net.Dialer.Control // and net.ListenConfig.Control. func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) error { - if strings.HasPrefix(address, "127.") || address == "::1" { + if isLocalhost(address) { // Don't bind to an interface for localhost connections. return nil } @@ -36,6 +39,26 @@ func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) e logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err) return nil } + + return bindConnToInterface(c, network, address, idx, logf) +} + +// SetListenConfigInterfaceIndex sets lc.Control such that sockets are bound +// to the provided interface index. +func SetListenConfigInterfaceIndex(lc *net.ListenConfig, ifIndex int) error { + if lc == nil { + return errors.New("nil ListenConfig") + } + if lc.Control != nil { + return errors.New("ListenConfig.Control already set") + } + lc.Control = func(network, address string, c syscall.RawConn) error { + return bindConnToInterface(c, network, address, ifIndex, log.Printf) + } + return nil +} + +func bindConnToInterface(c syscall.RawConn, network, address string, ifIndex int, logf logger.Logf) error { v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6 proto := unix.IPPROTO_IP opt := unix.IP_BOUND_IF @@ -45,14 +68,14 @@ func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) e } var sockErr error - err = c.Control(func(fd uintptr) { - sockErr = unix.SetsockoptInt(int(fd), proto, opt, idx) + err := c.Control(func(fd uintptr) { + sockErr = unix.SetsockoptInt(int(fd), proto, opt, ifIndex) }) + if sockErr != nil { + logf("[unexpected] netns: bindConnToInterface(%q, %q), v6=%v, index=%v: %v", network, address, v6, ifIndex, sockErr) + } if err != nil { return fmt.Errorf("RawConn.Control on %T: %w", c, err) } - if sockErr != nil { - logf("[unexpected] netns: control(%q, %q), v6=%v, index=%v: %v", network, address, v6, idx, sockErr) - } return sockErr } diff --git a/net/netns/netns_default.go b/net/netns/netns_default.go index bc841e4ab..17ebfd051 100644 --- a/net/netns/netns_default.go +++ b/net/netns/netns_default.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build (!linux && !windows && !darwin) || (darwin && ts_macext) +//go:build !linux && !windows && !darwin package netns diff --git a/net/netns/netns_macios.go b/net/netns/netns_macios.go deleted file mode 100644 index 6f5983962..000000000 --- a/net/netns/netns_macios.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build darwin || ios - -package netns - -import ( - "errors" - "log" - "net" - "strings" - "syscall" - - "golang.org/x/sys/unix" -) - -// SetListenConfigInterfaceIndex sets lc.Control such that sockets are bound -// to the provided interface index. -func SetListenConfigInterfaceIndex(lc *net.ListenConfig, ifIndex int) error { - if lc == nil { - return errors.New("nil ListenConfig") - } - if lc.Control != nil { - return errors.New("ListenConfig.Control already set") - } - lc.Control = func(network, address string, c syscall.RawConn) error { - var sockErr error - err := c.Control(func(fd uintptr) { - sockErr = bindInterface(fd, network, address, ifIndex) - if sockErr != nil { - log.Printf("netns: bind(%q, %q) on index %v: %v", network, address, ifIndex, sockErr) - } - }) - if err != nil { - return err - } - return sockErr - } - return nil -} - -func bindInterface(fd uintptr, network, address string, ifIndex int) error { - v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6 - proto := unix.IPPROTO_IP - opt := unix.IP_BOUND_IF - if v6 { - proto = unix.IPPROTO_IPV6 - opt = unix.IPV6_BOUND_IF - } - return unix.SetsockoptInt(int(fd), proto, opt, ifIndex) -}