diff --git a/net/udprelay/server.go b/net/udprelay/server.go index acdbf5ad6..5918863a5 100644 --- a/net/udprelay/server.go +++ b/net/udprelay/server.go @@ -19,6 +19,7 @@ import ( "slices" "strconv" "sync" + "syscall" "time" "go4.org/mem" @@ -29,6 +30,7 @@ import ( "tailscale.com/net/netaddr" "tailscale.com/net/netcheck" "tailscale.com/net/netmon" + "tailscale.com/net/netns" "tailscale.com/net/packet" "tailscale.com/net/sockopts" "tailscale.com/net/stun" @@ -78,6 +80,7 @@ type Server struct { closeCh chan struct{} netChecker *netcheck.Client metrics *metrics + netMon *netmon.Monitor mu sync.Mutex // guards the following fields macSecrets views.Slice[[blake2s.Size]byte] // [0] is most recent, max 2 elements @@ -346,6 +349,7 @@ func NewServer(logf logger.Logf, port uint16, onlyStaticAddrPorts bool, metrics if err != nil { return nil, err } + s.netMon = netMon s.netChecker = &netcheck.Client{ NetMon: netMon, Logf: logger.WithPrefix(logf, "netcheck: "), @@ -542,6 +546,25 @@ func trySetUDPSocketOptions(pconn nettype.PacketConn, logf logger.Logf) { } } +func trySetSOMark(logf logger.Logf, netMon *netmon.Monitor, network, address string, c syscall.RawConn) { + if netns.UseSocketMark() { + // Leverage SO_MARK where available to prevent packets from routing + // *over* Tailscale. Where SO_MARK is unavailable we choose to not set + // SO_BINDTODEVICE as that could prevent handshakes from completing + // where multiple interfaces are in play. + // + // SO_MARK is only used on Linux at the time of writing (2026-01-08), + // and Linux is the popular/common choice for peer relay. If we are + // running on Linux and SO_MARK is unavailable (EPERM), chances are + // there is no TUN device, so routing over Tailscale is impossible + // anyway. Both TUN creation and SO_MARK require CAP_NET_ADMIN. + lis := netns.Listener(logf, netMon) + if lis.Control != nil { + lis.Control(network, address, c) + } + } +} + // bindSockets binds udp4 and udp6 sockets to desiredPort. We consider it // successful if we manage to bind at least one udp4 socket. Multiple sockets // may be bound per address family, e.g. SO_REUSEPORT, depending on platform. @@ -562,7 +585,11 @@ func (s *Server) bindSockets(desiredPort uint16) error { // arbitrary. maxSocketsPerAF := min(16, runtime.NumCPU()) listenConfig := &net.ListenConfig{ - Control: listenControl, + Control: func(network, address string, c syscall.RawConn) error { + trySetReusePort(network, address, c) + trySetSOMark(s.logf, s.netMon, network, address, c) + return nil + }, } for _, network := range []string{"udp4", "udp6"} { SocketsLoop: diff --git a/net/udprelay/server_linux.go b/net/udprelay/server_linux.go index 009ec8cc8..d4cf2a2b1 100644 --- a/net/udprelay/server_linux.go +++ b/net/udprelay/server_linux.go @@ -12,11 +12,10 @@ import ( "golang.org/x/sys/unix" ) -func listenControl(_ string, _ string, c syscall.RawConn) error { +func trySetReusePort(_ string, _ string, c syscall.RawConn) { c.Control(func(fd uintptr) { unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) }) - return nil } func isReusableSocket(uc *net.UDPConn) bool { diff --git a/net/udprelay/server_notlinux.go b/net/udprelay/server_notlinux.go index 042a6dd68..f21020631 100644 --- a/net/udprelay/server_notlinux.go +++ b/net/udprelay/server_notlinux.go @@ -10,9 +10,7 @@ import ( "syscall" ) -func listenControl(_ string, _ string, _ syscall.RawConn) error { - return nil -} +func trySetReusePort(_ string, _ string, _ syscall.RawConn) {} func isReusableSocket(*net.UDPConn) bool { return false