mirror of https://github.com/tailscale/tailscale/
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.
247 lines
6.6 KiB
Go
247 lines
6.6 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build darwin
|
|
|
|
package netns
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"golang.org/x/net/route"
|
|
"golang.org/x/sys/unix"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/net/interfaces"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
func control(logf logger.Logf) func(network, address string, c syscall.RawConn) error {
|
|
return func(network, address string, c syscall.RawConn) error {
|
|
return controlLogf(logf, network, address, c)
|
|
}
|
|
}
|
|
|
|
var bindToInterfaceByRouteEnv = envknob.RegisterBool("TS_BIND_TO_INTERFACE_BY_ROUTE")
|
|
|
|
var errInterfaceIndexInvalid = errors.New("interface index invalid")
|
|
|
|
// controlLogf marks c as necessary to dial in a separate network namespace.
|
|
//
|
|
// 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 isLocalhost(address) {
|
|
// Don't bind to an interface for localhost connections.
|
|
return nil
|
|
}
|
|
|
|
if disableBindConnToInterface.Load() {
|
|
logf("netns_darwin: binding connection to interfaces disabled")
|
|
return nil
|
|
}
|
|
|
|
idx, err := getInterfaceIndex(logf, address)
|
|
if err != nil {
|
|
// callee logged
|
|
return nil
|
|
}
|
|
|
|
return bindConnToInterface(c, network, address, idx, logf)
|
|
}
|
|
|
|
func getInterfaceIndex(logf logger.Logf, address string) (int, error) {
|
|
// Helper so we can log errors.
|
|
defaultIdx := func() (int, error) {
|
|
idx, err := interfaces.DefaultRouteInterfaceIndex()
|
|
if err != nil {
|
|
logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err)
|
|
return -1, err
|
|
}
|
|
return idx, nil
|
|
}
|
|
|
|
useRoute := bindToInterfaceByRoute.Load() || bindToInterfaceByRouteEnv()
|
|
if !useRoute {
|
|
return defaultIdx()
|
|
}
|
|
|
|
host, _, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
// No port number; use the string directly.
|
|
host = address
|
|
}
|
|
|
|
// If the address doesn't parse, use the default index.
|
|
addr, err := netip.ParseAddr(host)
|
|
if err != nil {
|
|
logf("[unexpected] netns: error parsing address %q: %v", host, err)
|
|
return defaultIdx()
|
|
}
|
|
|
|
idx, err := interfaceIndexFor(addr, true /* canRecurse */)
|
|
if err != nil {
|
|
logf("netns: error in interfaceIndexFor: %v", err)
|
|
return defaultIdx()
|
|
}
|
|
|
|
// Verify that we didn't just choose the Tailscale interface;
|
|
// if so, we fall back to binding from the default.
|
|
_, tsif, err2 := interfaces.Tailscale()
|
|
if err2 == nil && tsif.Index == idx {
|
|
logf("[unexpected] netns: interfaceIndexFor returned Tailscale interface")
|
|
return defaultIdx()
|
|
}
|
|
|
|
return idx, err
|
|
}
|
|
|
|
// interfaceIndexFor returns the interface index that we should bind to in
|
|
// order to send traffic to the provided address.
|
|
func interfaceIndexFor(addr netip.Addr, canRecurse bool) (int, error) {
|
|
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("creating AF_ROUTE socket: %w", err)
|
|
}
|
|
defer unix.Close(fd)
|
|
|
|
var routeAddr route.Addr
|
|
if addr.Is4() {
|
|
routeAddr = &route.Inet4Addr{IP: addr.As4()}
|
|
} else {
|
|
routeAddr = &route.Inet6Addr{IP: addr.As16()}
|
|
}
|
|
|
|
rm := route.RouteMessage{
|
|
// NOTE: This is unix.RTM_VERSION, but we want to pin this to a
|
|
// particular constant so that it doesn't change under us if
|
|
// the x/sys/unix package changes down the road. Currently this
|
|
// is 0x5 on both Darwin x86 and ARM64.
|
|
Version: 0x5,
|
|
Type: unix.RTM_GET,
|
|
Flags: unix.RTF_UP,
|
|
ID: uintptr(os.Getpid()),
|
|
Seq: 1,
|
|
Addrs: []route.Addr{
|
|
unix.RTAX_DST: routeAddr,
|
|
},
|
|
}
|
|
b, err := rm.Marshal()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("marshaling RouteMessage: %w", err)
|
|
}
|
|
_, err = unix.Write(fd, b)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("writing message: %w", err)
|
|
}
|
|
|
|
// On macOS, the RTM_GET call should return exactly one route message.
|
|
// Given the following sizes and constants:
|
|
// - sizeof(struct rt_msghdr) = 92
|
|
// - RTAX_MAX = 8
|
|
// - sizeof(struct sockaddr_in6) = 28
|
|
// - sizeof(struct sockaddr_in) = 16
|
|
// - sizeof(struct sockaddr_dl) = 20
|
|
//
|
|
// The maximum buffer size should be:
|
|
// sizeof(struct rt_msghdr) + RTAX_MAX*sizeof(struct sockaddr_in6)
|
|
// = 92 + 8*28
|
|
// = 316
|
|
//
|
|
// During my testing, responses are typically ~120 bytes.
|
|
//
|
|
// We provide a much larger buffer just in case we're off by a bit, or
|
|
// the kernel decides to return more than one message; 2048 bytes
|
|
// should be plenty here. This also means we can do a single Read.
|
|
var buf [2048]byte
|
|
n, err := unix.Read(fd, buf[:])
|
|
if err != nil {
|
|
return 0, fmt.Errorf("reading message: %w", err)
|
|
}
|
|
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf[:n])
|
|
if err != nil {
|
|
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
|
}
|
|
if len(msgs) == 0 {
|
|
return 0, fmt.Errorf("no messages")
|
|
}
|
|
|
|
for _, msg := range msgs {
|
|
rm, ok := msg.(*route.RouteMessage)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if rm.Version < 3 || rm.Version > 5 || rm.Type != unix.RTM_GET {
|
|
continue
|
|
}
|
|
if len(rm.Addrs) < unix.RTAX_GATEWAY {
|
|
continue
|
|
}
|
|
|
|
switch addr := rm.Addrs[unix.RTAX_GATEWAY].(type) {
|
|
case *route.LinkAddr:
|
|
return addr.Index, nil
|
|
case *route.Inet4Addr:
|
|
// We can get a gateway IP; recursively call ourselves
|
|
// (exactly once) to get the link (and thus index) for
|
|
// the gateway IP.
|
|
if canRecurse {
|
|
return interfaceIndexFor(netip.AddrFrom4(addr.IP), false)
|
|
}
|
|
case *route.Inet6Addr:
|
|
// As above.
|
|
if canRecurse {
|
|
return interfaceIndexFor(netip.AddrFrom16(addr.IP), false)
|
|
}
|
|
default:
|
|
// Unknown type; skip it
|
|
continue
|
|
}
|
|
}
|
|
|
|
return 0, fmt.Errorf("no valid address found")
|
|
}
|
|
|
|
// 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
|
|
if v6 {
|
|
proto = unix.IPPROTO_IPV6
|
|
opt = unix.IPV6_BOUND_IF
|
|
}
|
|
|
|
var sockErr error
|
|
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)
|
|
}
|
|
return sockErr
|
|
}
|