//go:build darwin package main import ( "context" "fmt" "net" "syscall" ) // BINDING THE SOCKET TO AN INTERFACE, WHICH IS WHY THIS FILE EXISTS. // // macOS INTERFACE SCOPES its cloned subnet routes whenever more than one network service carries a // default route, and every node in this federation does: the wired interface plus Tailscale's tunnel. A // scoped route is usable only by a socket that has been explicitly bound to that interface, which the BSD // tools do through the system path. ping, nc and curl therefore reach the database host on the wire while // Go's dialer, which never sets IP_BOUND_IF, gets EHOSTUNREACH against the same address and port from the // same process at the same moment. That asymmetry is the whole bug, and it is not fixable by editing the // routing table: an unscoped host route helped when the node ran alone and stopped helping the moment the // other node was busy. // // So the fix belongs here rather than in the network. The socket says which interface it is for, the // scoped route matches, and the packet leaves on the wire the way the hardware was cabled to make it. // // THE CONSTANTS ARE DARWIN'S, and the standard library does not export them. IP_BOUND_IF is 25 at the // IPPROTO_IP level and IPV6_BOUND_IF is 125 at IPPROTO_IPV6, both stable in the XNU headers for many // releases. They are written here with their provenance rather than pulled in behind a new dependency for // two integers. const ( darwinIPBoundIf = 25 // netinet/in.h, IP_BOUND_IF darwinIPv6BoundIf = 125 // netinet6/in6.h, IPV6_BOUND_IF ) // bindToInterface returns a dial function whose sockets are bound to the named interface. // // An empty name returns nil, which every caller reads as "use the default dialer". That is deliberate: the // binding is a targeted answer to a scoped route and should be switched on where it is needed rather than // imposed everywhere, because a socket bound to the wrong interface fails in a way that looks exactly like // the problem it was meant to solve. func bindToInterface(name string) (func(ctx context.Context, network, addr string) (net.Conn, error), error) { if name == "" { return nil, nil } ifi, err := net.InterfaceByName(name) if err != nil { return nil, fmt.Errorf("bind interface %q: %w", name, err) } if ifi.Flags&net.FlagUp == 0 { return nil, fmt.Errorf("bind interface %q is down", name) } idx := ifi.Index d := &net.Dialer{ Control: func(network, address string, c syscall.RawConn) error { var setErr error ctrlErr := c.Control(func(fd uintptr) { switch network { case "tcp6", "udp6", "ip6": setErr = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, darwinIPv6BoundIf, idx) default: setErr = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, darwinIPBoundIf, idx) } }) if ctrlErr != nil { return ctrlErr } return setErr }, } return d.DialContext, nil }