net/tstun,wgengine,ipn/ipnlocal: make tstun's peerConfigTable use RouteManager table

Previously tstun.Wrapper.SetWGConfig walked wgcfg.Config.Peers on every
netmap to rebuild its own IP-to-peer table for masquerade NAT rewrites
and jailed-peer classification. Now the tun layer instead consumes the
route manager's shared immutable outbound snapshot directly, via a new
Engine.SetPeerRoutes method: LocalBackend pushes the snapshot (plus this
node's native Tailscale addresses) after every route manager commit that
can change it, and per-packet lookups read the interned PeerRoute
attributes from that table.

When no current peer is jailed or masqueraded, LocalBackend installs a
nil table (gated on RouteManager.HasDataPlaneAttrs), preserving the
per-packet nil-check fast path. The exitNodeRequiresMasq machinery is
deleted: its purpose was populating the table with all peers so that
more-specific entries shadow an exit node's /0, and the always-full
route manager table gives that shadowing inherently.

This is the last step before removing the Peers field from wgcfg.Config.

Updates #12542

Change-Id: Ifce09ca929a3f2511303ca1d6efdd583739494ce
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-07-14 12:41:11 -07:00
committed by Brad Fitzpatrick
parent e4144230f4
commit f0ce89b715
15 changed files with 318 additions and 245 deletions
+32 -8
View File
@@ -54,8 +54,10 @@ import (
"tailscale.com/net/netmon"
"tailscale.com/net/packet"
"tailscale.com/net/ping"
"tailscale.com/net/routemanager"
"tailscale.com/net/stun"
"tailscale.com/net/stun/stuntest"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tstun"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
@@ -244,9 +246,31 @@ func newMagicStackWithKey(t testing.TB, logf logger.Logf, ln nettype.PacketListe
}
}
func (s *magicStack) Reconfig(cfg *wgcfg.Config) error {
// Reconfig applies cfg to the stack's device and tun layer. peers,
// if non-nil, are the tailcfg nodes that cfg was derived from; the
// tun layer's per-peer data-plane attributes (masquerade addresses,
// jailed classification) are derived from them with a real
// [routemanager.RouteManager], exactly as LocalBackend does in
// production, so this helper cannot drift from the production
// derivation. Tests whose hand-built configs carry no such
// attributes may pass nil.
func (s *magicStack) Reconfig(cfg *wgcfg.Config, peers []tailcfg.NodeView) error {
s.tsTun.SetWGConfig(cfg)
if peers != nil {
rm := routemanager.New(nil)
mut := rm.Begin()
// Mirror the netmap.AllowSubnetRoutes flag the tests pass to
// nmcfg.WGCfg.
mut.SetPrefs(routemanager.Prefs{RouteAll: true})
for _, n := range peers {
mut.UpsertPeer(n)
}
mut.Commit()
native4, native6 := tsaddr.FirstTailscaleAddrs(slices.All(cfg.Addresses))
s.tsTun.SetPeerRoutes(native4, native6, rm.Outbound())
}
// In production, LocalBackend installs a PeerByIPPacketFunc via
// Engine.SetPeerByIPPacketFunc. Tests that bypass LocalBackend need
// to install one here for outbound packet routing.
@@ -373,7 +397,7 @@ func meshStacks(logf logger.Logf, mutateNetmap func(idx int, nm *netmap.NetworkM
// blow up. Shouldn't happen anyway.
panic(fmt.Sprintf("failed to construct wgcfg from netmap: %v", err))
}
if err := m.Reconfig(wg); err != nil {
if err := m.Reconfig(wg, nm.Peers); err != nil {
if ctx.Err() != nil || errors.Is(err, errConnClosed) {
// shutdown race, don't care.
return
@@ -1199,10 +1223,10 @@ func testTwoDevicePing(t *testing.T, d *devices) {
},
}
if err := m1.Reconfig(m1cfg); err != nil {
if err := m1.Reconfig(m1cfg, nil); err != nil {
t.Fatal(err)
}
if err := m2.Reconfig(m2cfg); err != nil {
if err := m2.Reconfig(m2cfg, nil); err != nil {
t.Fatal(err)
}
@@ -1326,7 +1350,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
t.Run("no-op-dev1-reconfig", func(t *testing.T) {
setT(t)
defer setT(outerT)
if err := m1.Reconfig(m1cfg); err != nil {
if err := m1.Reconfig(m1cfg, nil); err != nil {
t.Fatal(err)
}
ping1(t)
@@ -2525,7 +2549,7 @@ func TestIsWireGuardOnlyPeer(t *testing.T) {
if err != nil {
t.Fatal(err)
}
m.Reconfig(cfg)
m.Reconfig(cfg, nm.Peers)
pbuf := tuntest.Ping(wgaip.Addr(), tsaip.Addr())
m.tun.Outbound <- pbuf
@@ -2586,7 +2610,7 @@ func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) {
if err != nil {
t.Fatal(err)
}
m.Reconfig(cfg)
m.Reconfig(cfg, nm.Peers)
pbuf := tuntest.Ping(wgaip.Addr(), tsaip.Addr())
m.tun.Outbound <- pbuf
@@ -2627,7 +2651,7 @@ func applyNetworkMap(t *testing.T, m *magicStack, nm *netmap.NetworkMap) {
t.Fatal(err)
}
// Apply the wireguard config to the tailscale internal wireguard device.
if err := m.Reconfig(cfg); err != nil {
if err := m.Reconfig(cfg, nm.Peers); err != nil {
t.Fatal(err)
}
}
+6
View File
@@ -18,6 +18,7 @@ import (
"sync/atomic"
"time"
"github.com/gaissmai/bart"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"go4.org/mem"
@@ -33,6 +34,7 @@ import (
"tailscale.com/net/ipset"
"tailscale.com/net/netmon"
"tailscale.com/net/packet"
"tailscale.com/net/routemanager"
"tailscale.com/net/sockstats"
"tailscale.com/net/tsdial"
"tailscale.com/net/tstun"
@@ -1006,6 +1008,10 @@ func (e *userspaceEngine) SetJailedFilter(filt *filter.Filter) {
e.tundev.SetJailedFilter(filt)
}
func (e *userspaceEngine) SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute]) {
e.tundev.SetPeerRoutes(native4, native6, routes)
}
func (e *userspaceEngine) SetStatusCallback(cb StatusCallback) {
e.mu.Lock()
defer e.mu.Unlock()
+13
View File
@@ -9,9 +9,11 @@ import (
"net/netip"
"time"
"github.com/gaissmai/bart"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns"
"tailscale.com/net/packet"
"tailscale.com/net/routemanager"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
@@ -125,6 +127,17 @@ type Engine interface {
// SetJailedFilter updates the packet filter for jailed nodes.
SetJailedFilter(*filter.Filter)
// SetPeerRoutes updates the per-peer route attributes used by the
// tun-layer data plane for per-packet NAT rewrites and
// jailed-filter selection. native4 and native6 are this node's own
// Tailscale addresses, and routes maps each peer's addresses and
// routed prefixes to its attributes; it is a shared immutable
// snapshot from [routemanager.RouteManager.Outbound].
//
// A nil routes table disables all per-packet peer processing;
// callers pass nil when no current peer has any such attributes.
SetPeerRoutes(native4, native6 netip.Addr, routes *bart.Table[*routemanager.PeerRoute])
// SetStatusCallback sets the function to call when the
// WireGuard status changes.
SetStatusCallback(StatusCallback)