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
51 lines
1.8 KiB
Go
51 lines
1.8 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build linux && !android
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"net"
|
|
"net/netip"
|
|
|
|
"tailscale.com/net/netns"
|
|
)
|
|
|
|
func init() {
|
|
initListenConfig = initListenConfigTun
|
|
}
|
|
|
|
// initListenConfigTun binds the peerapi listener to the tunnel interface
|
|
// with SO_BINDTODEVICE.
|
|
//
|
|
// Linux delivers a packet addressed to the node's own Tailscale IP via
|
|
// INPUT from any interface it arrives on (weak host model), so a
|
|
// LAN-adjacent attacker could otherwise complete a TCP handshake with
|
|
// the kernel-level listener, confirming that this machine's MAC
|
|
// address belongs to a given tailnet identity (the peerapi port is
|
|
// derived from the Tailscale IP). Binding the listener to the tunnel
|
|
// interface drops those handshakes at socket lookup. macOS and iOS
|
|
// already bind this listener to the tunnel interface with IP_BOUND_IF,
|
|
// and Windows is protected by its strong host model.
|
|
//
|
|
// No legitimate traffic is lost. Connections to the node's Tailscale
|
|
// IP from the local host are delivered via the tunnel interface, peer
|
|
// connections arrive that way in builds without userspace netstack,
|
|
// and with netstack compiled in (the usual case) peer connections are
|
|
// terminated in userspace and never reach the kernel listener at all.
|
|
// In netstack mode there is no tunnel interface and the listener binds
|
|
// all addresses; the only connections that reach it are loopback dials
|
|
// from netstack and from the local host, so loopback is the right
|
|
// device there.
|
|
func initListenConfigTun(config *net.ListenConfig, addr netip.Addr, tunIfIndex int) error {
|
|
device := "lo"
|
|
if tunIfIndex != 0 {
|
|
iface, err := net.InterfaceByIndex(tunIfIndex)
|
|
if err == nil {
|
|
device = iface.Name
|
|
}
|
|
}
|
|
return netns.SetListenConfigInterfaceName(config, device)
|
|
}
|