mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-10 04:12:47 -04:00
Linux is a weak-host stack, so a LAN-adjacent machine can complete a TCP handshake with a node's peerapi listener by sending a packet to the node's Tailscale IP, with no credentials and no tailnet membership. macOS and iOS already bind the listener to the tunnel interface, and Windows is protected by its strong host model, so Linux tun mode was the only platform that leaked. Bind the Linux listener to the tunnel interface as well, so the kernel only answers connections that arrive from the tunnel or from the local host. A natlab VM test verifies that a same-LAN machine can no longer complete the handshake, while local and peer peerapi keep working. FreeBSD has the same weak-host exposure but no per-socket equivalent, so handling it there with pf is a TODO (#21419). Updates tailscale/corp#48248 Reported-By: Samuel Keeley (@keeleysam) Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I5f8501b0938c9f7aa39c4c12ebddd988c72e89bf
168 lines
4.4 KiB
Go
168 lines
4.4 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux && !android
|
|
|
|
package netns
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"sync"
|
|
"syscall"
|
|
|
|
"golang.org/x/sys/unix"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/net/netmon"
|
|
"tailscale.com/tsconst"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
// socketMarkWorksOnce is the sync.Once & cached value for useSocketMark.
|
|
var socketMarkWorksOnce struct {
|
|
sync.Once
|
|
v bool
|
|
}
|
|
|
|
// socketMarkWorks returns whether SO_MARK works.
|
|
func socketMarkWorks() bool {
|
|
addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:1")
|
|
if err != nil {
|
|
return true // unsure, returning true does the least harm.
|
|
}
|
|
|
|
sConn, err := net.DialUDP("udp", nil, addr)
|
|
if err != nil {
|
|
return true // unsure, return true
|
|
}
|
|
defer sConn.Close()
|
|
|
|
rConn, err := sConn.SyscallConn()
|
|
if err != nil {
|
|
return true // unsure, return true
|
|
}
|
|
|
|
var sockErr error
|
|
err = rConn.Control(func(fd uintptr) {
|
|
sockErr = setBypassMark(fd)
|
|
})
|
|
if err != nil || sockErr != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
var forceBindToDevice = envknob.RegisterBool("TS_FORCE_LINUX_BIND_TO_DEVICE")
|
|
|
|
// UseSocketMark reports whether SO_MARK is in use.
|
|
// If it doesn't, we have to use SO_BINDTODEVICE on our sockets instead.
|
|
func UseSocketMark() bool {
|
|
if forceBindToDevice() {
|
|
return false
|
|
}
|
|
socketMarkWorksOnce.Do(func() {
|
|
socketMarkWorksOnce.v = socketMarkWorks()
|
|
})
|
|
return socketMarkWorksOnce.v
|
|
}
|
|
|
|
// ignoreErrors returns true if we should ignore setsocketopt errors in
|
|
// this instance.
|
|
func ignoreErrors() bool {
|
|
if os.Getuid() != 0 {
|
|
// only root can manipulate these socket flags
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func control(logger.Logf, *netmon.Monitor) func(network, address string, c syscall.RawConn) error {
|
|
return controlC
|
|
}
|
|
|
|
// controlC 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 controlC(network, address string, c syscall.RawConn) error {
|
|
if isLocalhost(address) {
|
|
// Don't bind to an interface for localhost connections.
|
|
return nil
|
|
}
|
|
|
|
var sockErr error
|
|
err := c.Control(func(fd uintptr) {
|
|
if UseSocketMark() {
|
|
sockErr = setBypassMark(fd)
|
|
} else {
|
|
sockErr = bindToDevice(fd)
|
|
}
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("RawConn.Control on %T: %w", c, err)
|
|
}
|
|
if sockErr != nil && ignoreErrors() {
|
|
// TODO(bradfitz): maybe log once? probably too spammy for e.g. CLI tools like tailscale netcheck.
|
|
return nil
|
|
}
|
|
return sockErr
|
|
}
|
|
|
|
func setBypassMark(fd uintptr) error {
|
|
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, tsconst.LinuxBypassMarkNum); err != nil {
|
|
return fmt.Errorf("setting SO_MARK bypass: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func bindToDevice(fd uintptr) error {
|
|
ifc, err := netmon.DefaultRouteInterface()
|
|
if err != nil {
|
|
// Make sure we bind to *some* interface,
|
|
// or we could get a routing loop.
|
|
// "lo" is always wrong, but if we don't have
|
|
// a default route anyway, it doesn't matter.
|
|
ifc = "lo"
|
|
}
|
|
if err := unix.SetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_BINDTODEVICE, ifc); err != nil {
|
|
return fmt.Errorf("setting SO_BINDTODEVICE: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetListenConfigInterfaceName sets lc.Control so that sockets created
|
|
// with it are bound to the named interface using SO_BINDTODEVICE, and
|
|
// thus only receive packets that arrived on that interface. It is the
|
|
// Linux counterpart of darwin's SetListenConfigInterfaceIndex.
|
|
//
|
|
// Failures to set the socket option are logged and otherwise ignored:
|
|
// the socket then simply listens as it would have without this call.
|
|
// Setting SO_BINDTODEVICE requires CAP_NET_RAW, and hardening must not
|
|
// take out the caller's listener on systems where that is unavailable.
|
|
func SetListenConfigInterfaceName(lc *net.ListenConfig, ifName string) 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 = unix.SetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_BINDTODEVICE, ifName)
|
|
})
|
|
if sockErr != nil {
|
|
log.Printf("[unexpected] netns: SO_BINDTODEVICE(%q) on %v %v: %v", ifName, network, address, sockErr)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("RawConn.Control on %T: %w", c, err)
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|