wgengine,ipn/ipnlocal: remove Engine.PeerKeyForIP and the engine's peer route table

The engine kept its own longest-prefix-match table (peerByIPRoute),
rebuilt from the full peer list on every reconfig, to route outbound
packets and answer PeerKeyForIP. That's now the route manager's job:
LocalBackend already installs a PeerByIPPacketFunc backed by the
RouteManager's incrementally-maintained outbound table, so the
engine's copy was redundant state with redundant O(n peers) rebuild
work.

Delete the table, the PeerKeyForIP interface method, and the BART-only
default callback. LocalBackend's peerForIP now queries the
RouteManager's outbound table directly for the subnet-route and
exit-node fallback. Engines running without a LocalBackend (such as
wgengine/bench) must install their own outbound peer lookup, since the
device's standard AllowedIPs trie only covers peers that already
exist and can't lazily create them.

Updates #12542

Change-Id: I25100399e273ed6c2bb1f6136b7cd81bc83e7313
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-07-09 01:13:57 +00:00
parent a89b2dddd1
commit 3aecf0d1ed
6 changed files with 37 additions and 89 deletions

View File

@@ -9489,6 +9489,7 @@ func TestEnginePeerForIPAdjustsForPrefs(t *testing.T) {
nm := buildNetmapWithPeers(selfNode, exitA, exitB, subnetBig, subnetSmall)
var eng wgengine.Engine
var curLB *LocalBackend
var curT *testing.T // active subtest, for test helpers
wantPeer := func(ip string, n tailcfg.NodeView) {
@@ -9515,19 +9516,19 @@ func TestEnginePeerForIPAdjustsForPrefs(t *testing.T) {
wantKey := func(ip string, n tailcfg.NodeView) {
t := curT
t.Helper()
pk, _, ok := eng.PeerKeyForIP(netip.MustParseAddr(ip))
pr, ok := curLB.currentNode().routeMgr.Outbound().Lookup(netip.MustParseAddr(ip))
if !ok {
t.Fatalf("PeerKeyForIP(%s): ok=false, want true", ip)
t.Fatalf("routeMgr.Outbound().Lookup(%s): ok=false, want true", ip)
}
if pk != n.Key() {
t.Fatalf("PeerKeyForIP(%s): key=%v, want %v", ip, pk, n.Key())
if pr.Key != n.Key() {
t.Fatalf("routeMgr.Outbound().Lookup(%s): key=%v, want %v", ip, pr.Key, n.Key())
}
}
wantNotKey := func(ip string) {
t := curT
t.Helper()
if _, _, ok := eng.PeerKeyForIP(netip.MustParseAddr(ip)); ok {
t.Fatalf("PeerKeyForIP(%s): ok=true, want false", ip)
if _, ok := curLB.currentNode().routeMgr.Outbound().Lookup(netip.MustParseAddr(ip)); ok {
t.Fatalf("routeMgr.Outbound().Lookup(%s): ok=true, want false", ip)
}
}
wantSelf := func(ip string) {
@@ -9663,6 +9664,7 @@ func TestEnginePeerForIPAdjustsForPrefs(t *testing.T) {
})
eng = lb.sys.Engine.Get()
curLB = lb
curT = t
tt.check()
})

View File

@@ -143,11 +143,11 @@ func (b *LocalBackend) peerForIP(ip netip.Addr) (_ wgengine.PeerForIP, ok bool)
}
}
pk, route, ok := b.e.PeerKeyForIP(ip)
route, pr, ok := nb.routeMgr.Outbound().LookupPrefixLPM(netip.PrefixFrom(ip, ip.BitLen()))
if !ok {
return wgengine.PeerForIP{}, false
}
nid, ok := nb.NodeByKey(pk)
nid, ok := nb.NodeByKey(pr.Key)
if !ok {
return wgengine.PeerForIP{}, false
}

View File

@@ -2019,9 +2019,6 @@ func (e *mockEngine) SetPeerForIPFunc(func(netip.Addr) (_ wgengine.PeerForIP, ok
func (e *mockEngine) SetPeerConfigFunc(func(key.NodePublic) (allowedIPs []netip.Prefix, ok bool)) {
}
func (e *mockEngine) SyncDevicePeer(key.NodePublic) {}
func (e *mockEngine) PeerKeyForIP(netip.Addr) (_ key.NodePublic, _ netip.Prefix, ok bool) {
return key.NodePublic{}, netip.Prefix{}, false
}
func (e *mockEngine) SetPeerSessionStateFunc(func(key.NodePublic, wgengine.PeerWireGuardState)) {
}
func (e *mockEngine) SetNetLogSource(wgengine.NetLogSource) {}

View File

@@ -82,6 +82,17 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
e1.SetFilter(filter.NewAllowAllForTest(l1))
e2.SetFilter(filter.NewAllowAllForTest(l2))
// There is no LocalBackend in this benchmark, so install trivial
// outbound peer lookups; without one, outbound packets can't
// lazily create their WireGuard peer.
k1pub, k2pub := k1.Public(), k2.Public()
e1.SetPeerByIPPacketFunc(func(dst netip.Addr) (_ key.NodePublic, ok bool) {
return k2pub, a2.Contains(dst)
})
e2.SetPeerByIPPacketFunc(func(dst netip.Addr) (_ key.NodePublic, ok bool) {
return k1pub, a1.Contains(dst)
})
var wait sync.WaitGroup
wait.Add(2)

View File

@@ -18,7 +18,6 @@
"sync/atomic"
"time"
"github.com/gaissmai/bart"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"go4.org/mem"
@@ -104,19 +103,6 @@ type userspaceEngine struct {
wgLock sync.Mutex // serializes all wgdev operations; see lock order comment below
// peerByIPRoute is a longest-prefix-match table built from
// lastCfgFull.Peers AllowedIPs. It's the slow path for
// SetPeerByIPPacketFunc, used when LocalBackend's exact-IP fast path
// (nodeByAddr) misses — i.e. for subnet routes and exit-node default
// routes. Built from lastCfgFull (the wireguard-filtered peer list)
// rather than the netmap so that exit-node selection is honored: the
// netmap has 0.0.0.0/0 in AllowedIPs for every exit-capable peer, but
// lastCfgFull only has it for the currently-selected exit node.
//
// Replaced (not mutated) by maybeReconfigWireguardLocked. Read by
// the per-packet wgdev callback without locking.
peerByIPRoute atomic.Pointer[bart.Table[key.NodePublic]]
// peerForIP, if non-nil, is the callback installed via
// [userspaceEngine.SetPeerForIPFunc]. PeerForIP delegates to it
// for the cold-path control lookups (Ping, TSMP, pendopen, etc).
@@ -525,16 +511,6 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
e.logf("Creating WireGuard device...")
e.wgdev = wgcfg.NewDevice(e.tundev, e.magicConn.Bind(), e.wgLogger.DeviceLogger)
closePool.addFunc(e.wgdev.Close)
// Install a default outbound-packet peer lookup callback. It uses only
// the engine's BART table, which is rebuilt from the wireguard-filtered
// peer list on every Reconfig. Consumers (e.g. LocalBackend) may later
// call SetPeerByIPPacketFunc to additionally install a fast path for
// exact node-address matches; the BART remains the slow-path fallback.
// Without this default, callers that don't run a LocalBackend would
// have no way to route outbound packets to peers, since peers are
// created lazily from inbound packets only via SetPeerLookupFunc.
e.SetPeerByIPPacketFunc(nil)
closePool.addFunc(func() {
if err := e.magicConn.Close(); err != nil {
e.logf("error closing magicconn: %v", err)
@@ -721,16 +697,6 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
// against the current lookup.
e.wgLogger.Invalidate()
// Rebuild the prefix-match peer routing table from the current
// (wireguard-filtered) peer list and publish it atomically.
rt := &bart.Table[key.NodePublic]{}
for _, p := range full.Peers {
for _, pfx := range p.AllowedIPs {
rt.Insert(pfx, p.PublicKey)
}
}
e.peerByIPRoute.Store(rt)
e.logf("wgengine: Reconfig: configuring userspace WireGuard config (with %d peers)", len(full.Peers))
if e.peerConfigFn.Load() != nil {
// The device has a long-lived PeerLookupFunc backed by the
@@ -800,29 +766,23 @@ func (e *userspaceEngine) SyncDevicePeer(k key.NodePublic) {
// SetPeerByIPPacketFunc installs a callback used by wireguard-go to look up
// which peer should handle an outbound packet by destination IP.
//
// If fn is non-nil it is authoritative: LocalBackend's implementation
// consults both the exact node-address fast path and the RouteManager's
// outbound table (covering subnet routes and exit-node default routes),
// and stays correct under incremental netmap deltas. The engine's own
// BART table ([userspaceEngine.peerByIPRoute], rebuilt only on full
// reconfigs) is used only when no fn is installed (e.g. engines running
// without a LocalBackend).
// LocalBackend's implementation consults both the exact node-address fast
// path and the RouteManager's outbound table (covering subnet routes and
// exit-node default routes), and stays correct under incremental netmap
// deltas.
//
// [NewUserspaceEngine] installs a BART-only default at engine creation time,
// so callers that don't call SetPeerByIPPacketFunc (e.g. those not running
// a LocalBackend) still get working outbound packet routing.
// A nil fn uninstalls the callback, reverting the device to its standard
// WireGuard AllowedIPs trie, which only contains peers that already exist
// in the device. Callers without a LocalBackend that need outbound packets
// to lazily create peers must install their own callback.
func (e *userspaceEngine) SetPeerByIPPacketFunc(fn func(netip.Addr) (_ key.NodePublic, ok bool)) {
if fn == nil {
e.wgdev.SetPeerByIPPacketFunc(nil)
return
}
e.wgdev.SetPeerByIPPacketFunc(func(_, dst netip.Addr, _ []byte) (device.NoisePublicKey, bool) {
if fn != nil {
if pk, ok := fn(dst); ok {
return pk.Raw32(), true
}
return device.NoisePublicKey{}, false
}
if rt := e.peerByIPRoute.Load(); rt != nil {
if pk, ok := rt.Lookup(dst); ok {
return pk.Raw32(), true
}
if pk, ok := fn(dst); ok {
return pk.Raw32(), true
}
return device.NoisePublicKey{}, false
})
@@ -1632,20 +1592,6 @@ func (e *userspaceEngine) SetPeerForIPFunc(fn func(netip.Addr) (PeerForIP, bool)
e.peerForIP.Store(&fn)
}
// PeerKeyForIP looks up ip in the engine's AllowedIPs table
// ([userspaceEngine.peerByIPRoute]). See [Engine.PeerKeyForIP].
func (e *userspaceEngine) PeerKeyForIP(ip netip.Addr) (pk key.NodePublic, route netip.Prefix, ok bool) {
if !ip.IsValid() {
return pk, route, false
}
rt := e.peerByIPRoute.Load()
if rt == nil {
return pk, route, false
}
route, pk, ok = rt.LookupPrefixLPM(netip.PrefixFrom(ip, ip.BitLen()))
return pk, route, ok
}
// PeerForIP returns the node responsible for handling the given IP.
// It delegates to the callback installed via [SetPeerForIPFunc]; engines
// without an installed callback return (zero, false).

View File

@@ -126,18 +126,10 @@ type Engine interface {
// If fn is nil, PeerForIP returns (zero, false) for every IP.
//
// LocalBackend installs a func backed by the live nodeBackend for
// exact-match and self addresses, with [Engine.PeerKeyForIP]
// supplying the subnet-route / exit-node fallback.
// exact-match and self addresses, with the RouteManager's outbound
// table supplying the subnet-route / exit-node fallback.
SetPeerForIPFunc(fn func(netip.Addr) (_ PeerForIP, ok bool))
// PeerKeyForIP returns the peer's NodePublic and the matched prefix
// for the longest-prefix match of ip in the engine's AllowedIPs
// table (the wireguard config most recently installed via
// [Engine.Reconfig]). Exit-node selection is honored: an unselected
// exit node's 0.0.0.0/0 is not matched. It is the same table the
// outbound packet hot path consults via [Engine.SetPeerByIPPacketFunc].
PeerKeyForIP(netip.Addr) (_ key.NodePublic, _ netip.Prefix, ok bool)
// GetFilter returns the current packet filter, if any.
GetFilter() *filter.Filter