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 <mihai@tailscale.com>
pull/6628/head
Mihai Parparita 2 years ago committed by Mihai Parparita
parent cb525a1aad
commit 79f3a5d753

@ -20,6 +20,7 @@ import (
"golang.org/x/net/route" "golang.org/x/net/route"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"tailscale.com/net/netaddr" "tailscale.com/net/netaddr"
"tailscale.com/syncs"
) )
func defaultRoute() (d DefaultRouteDetails, err error) { func defaultRoute() (d DefaultRouteDetails, err error) {
@ -36,7 +37,17 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
return d, nil 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) { 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 // $ netstat -nr
// Routing tables // Routing tables
// Internet: // Internet:
@ -71,6 +82,16 @@ func DefaultRouteInterfaceIndex() (int, error) {
return 0, errors.New("no gateway index found") 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() { func init() {
likelyHomeRouterIP = likelyHomeRouterIPBSDFetchRIB likelyHomeRouterIP = likelyHomeRouterIPBSDFetchRIB
} }

@ -2,12 +2,15 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build darwin && !ts_macext //go:build darwin
package netns package netns
import ( import (
"errors"
"fmt" "fmt"
"log"
"net"
"strings" "strings"
"syscall" "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 // It's intentionally the same signature as net.Dialer.Control
// and net.ListenConfig.Control. // and net.ListenConfig.Control.
func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) error { 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. // Don't bind to an interface for localhost connections.
return nil return nil
} }
@ -36,6 +39,26 @@ func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) e
logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err) logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err)
return nil 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 v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6
proto := unix.IPPROTO_IP proto := unix.IPPROTO_IP
opt := unix.IP_BOUND_IF opt := unix.IP_BOUND_IF
@ -45,14 +68,14 @@ func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) e
} }
var sockErr error var sockErr error
err = c.Control(func(fd uintptr) { err := c.Control(func(fd uintptr) {
sockErr = unix.SetsockoptInt(int(fd), proto, opt, idx) 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 { if err != nil {
return fmt.Errorf("RawConn.Control on %T: %w", c, err) 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 return sockErr
} }

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build (!linux && !windows && !darwin) || (darwin && ts_macext) //go:build !linux && !windows && !darwin
package netns package netns

@ -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)
}
Loading…
Cancel
Save