WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
9 changed files with 246 additions and 54 deletions
Showing only changes of commit f831469c27 - Show all commits
+33 -15
View File
@@ -633,6 +633,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
nb.ready()
e.SetPeerByIPPacketFunc(b.lookupPeerByIP)
e.SetPeerConfigFunc(b.peerAllowedIPs)
e.SetPeerForIPFunc(b.peerForIP)
e.SetPeerSessionStateFunc(b.onPeerWireGuardState)
e.SetNetLogSource(netLogNodeSource{b})
@@ -2462,7 +2463,8 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
// the full-netmap behavior of [tkaFilterNetmapLocked].
muts = b.tkaFilterDeltaMutsLocked(muts)
needsAuthReconfig := netmapDeltaNeedsAuthReconfig(cn, muts)
cn.UpdateNetmapDelta(muts)
changedAllowedIPs, _ := cn.UpdateNetmapDelta(muts)
if buildfeatures.HasDrive {
// Drive's lazy remotes-source caches its rebuild keyed by this
// generation, so any delta — peer add/remove, address change,
@@ -2504,20 +2506,27 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
}
ms.UpdateNetmapDelta(muts)
// Sync the WireGuard device for exactly the peers whose allowed
// source prefixes changed, as computed by the route manager when
// the delta was applied above. Removed peers appear with a nil
// value and are removed from the device; SyncDevicePeer reads
// the live per-peer config, so only the keys matter here.
for k := range changedAllowedIPs {
b.e.SyncDevicePeer(k)
}
// Force a full authReconfig + SetSelfNode on any peer add or
// remove. netmapDeltaNeedsAuthReconfig only considered
// NodeMutationUpsert of already-known NodeIDs whose
// peerRouteConfigChanged, so brand-new peers and removes left
// e.lastCfgFull and wgdev's PeerLookupFunc closure stale, and
// outbound wgdev encryption missed those peers. authReconfig
// fixes the wireguard side; SetSelfNode refreshes the engine's
// cached self node. PeerForIP / lookupPeerByIP staleness was
// addressed separately in d4f2917c1b, which routes those lookups
// through nodeBackend's live data, and as part of the broader
// netmap.NetworkMap removal effort the engine no longer caches
// the netmap at all (see tailscale/corp#43394). As of 2026-06-24
// the only remaining staleness this guards against is
// e.lastCfgFull and the wgdev peer set.
// remove. The WireGuard device itself no longer depends on this:
// the SyncDevicePeer calls above keep its peer set delta-correct,
// and its lazy peer creation reads live state via
// [wgengine.Engine.SetPeerConfigFunc]. What still rides
// authReconfig is everything else derived from the full peer set:
// OS routes (router.Config), the quad-100 resolver's MagicDNS
// hosts map (dnsConfigForNetmap), and tstun's per-peer config
// (masquerade addresses and jailed peers, via SetWGConfig). Once
// those become delta-aware too, this can be gated on the route
// manager's OS-routes changes and the tstun-relevant fields
// instead of firing on every peer change.
needsAuthReconfig = needsAuthReconfig || peersUpsertedOrRemoved
if needsAuthReconfig {
if peersUpsertedOrRemoved {
@@ -6126,12 +6135,21 @@ func (b *LocalBackend) authReconfigLocked() {
}
oneCGNATRoute := shouldUseOneCGNATRoute(b.logf, b.sys.NetMon.Get(), b.sys.ControlKnobs(), version.OS())
cn.updateRouteManagerPrefs(routePrefs{
// Sync the WireGuard device for any peers whose allowed source
// prefixes changed with the new prefs, such as the old and new
// exit node when the selection changes. The Reconfig below still
// converges every peer via its full device sync, but this
// incremental sync is what will remain once the full reconfig is
// gated on actual router/DNS changes.
changedAllowedIPs := cn.updateRouteManagerPrefs(routePrefs{
ExitNodeID: prefs.ExitNodeID(),
ExitNodeSelected: prefs.ExitNodeID() != "" || prefs.ExitNodeIP().IsValid(),
RouteAll: flags&netmap.AllowSubnetRoutes != 0,
OneCGNAT: oneCGNATRoute,
})
for k := range changedAllowedIPs {
b.e.SyncDevicePeer(k)
}
rcfg := b.routerConfigLocked(cfg, prefs, nm, oneCGNATRoute)
// Add these extra Allowed IPs after router configuration, because the expected
+2 -2
View File
@@ -2751,7 +2751,7 @@ func TestNotifyForSessionUserProfilesDedupResetsOnSelfChange(t *testing.T) {
// tests LocalBackend.updateNetmapDeltaLocked
func TestUpdateNetmapDelta(t *testing.T) {
b := newTestLocalBackend(t)
if b.currentNode().UpdateNetmapDelta(nil) {
if _, handled := b.currentNode().UpdateNetmapDelta(nil); handled {
t.Errorf("updateNetmapDeltaLocked() = true, want false with nil netmap")
}
@@ -2790,7 +2790,7 @@ func TestUpdateNetmapDelta(t *testing.T) {
t.Fatal("netmap.MutationsFromMapResponse failed")
}
if !b.currentNode().UpdateNetmapDelta(muts) {
if _, handled := b.currentNode().UpdateNetmapDelta(muts); !handled {
t.Fatalf("updateNetmapDeltaLocked() = false, want true with new netmap")
}
+38 -12
View File
@@ -267,6 +267,22 @@ func (nb *nodeBackend) NodeByKey(k key.NodePublic) (_ tailcfg.NodeID, ok bool) {
return nid, ok
}
// PeerAllowedIPs returns the prefixes from which the peer with the
// given public key is currently allowed to originate traffic, or
// ok=false if the key is unknown or the peer currently contributes no
// prefixes.
func (nb *nodeBackend) PeerAllowedIPs(k key.NodePublic) ([]netip.Prefix, bool) {
nb.mu.Lock()
defer nb.mu.Unlock()
id, ok := nb.nodeByKey[k]
if !ok {
return nil, false
}
// Holding nb.mu satisfies routeMgr's serialization requirement:
// all routeMgr mutations also run under nb.mu.
return nb.routeMgr.PeerAllowedIPs(id)
}
// NodeByWireGuardString returns the node ID of the peer whose
// [key.NodePublic.WireGuardGoString] form is s (e.g. "peer(IMTB…r7lM)").
// ok is false if no current peer matches.
@@ -793,11 +809,6 @@ func (nb *nodeBackend) updatePeersLocked() {
rt.Commit()
}
// updateRouteManagerPrefs pushes the routing-relevant prefs and the
// OneCGNAT decision into the route manager. The exit node arrives as
// a stable ID from prefs and is resolved to the current numeric node
// ID here; it resolves to zero (no exit node) if that peer is not in
// the netmap yet, in which case a later netmap update re-runs this.
// routePrefs is the subset of routing-relevant prefs (and derived
// state) that [nodeBackend.updateRouteManagerPrefs] pushes into the
// RouteManager.
@@ -821,7 +832,13 @@ type routePrefs struct {
OneCGNAT bool
}
func (nb *nodeBackend) updateRouteManagerPrefs(p routePrefs) {
// updateRouteManagerPrefs pushes p into the route manager.
//
// It returns the peers whose allowed source prefixes changed as a
// result (for example the old and new exit node when the selection
// changes), as described by [routemanager.Result.AllowedIPs].
// In particular, the value for a key will be nil when that peer was removed.
func (nb *nodeBackend) updateRouteManagerPrefs(p routePrefs) (changedAllowedIPs map[key.NodePublic][]netip.Prefix) {
nb.mu.Lock()
defer nb.mu.Unlock()
var exitID tailcfg.NodeID
@@ -840,7 +857,8 @@ func (nb *nodeBackend) updateRouteManagerPrefs(p routePrefs) {
RouteAll: p.RouteAll,
})
rt.SetTailnetConfig(routemanager.TailnetConfig{OneCGNAT: p.OneCGNAT})
rt.Commit()
res := rt.Commit()
return res.AllowedIPs
}
// setPacketFilter stores the live packet filter rules and parsed
@@ -872,11 +890,16 @@ func (nb *nodeBackend) mergeUserProfiles(profiles map[tailcfg.UserID]tailcfg.Use
}
}
func (nb *nodeBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bool) {
// UpdateNetmapDelta applies the given netmap mutations to the live
// peer state. It returns the peers whose allowed source prefixes
// changed (as described by [routemanager.Result.AllowedIPs]) so the
// caller can sync those peers to the WireGuard device, and reports
// whether it handled all of the mutations.
func (nb *nodeBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (changedAllowedIPs map[key.NodePublic][]netip.Prefix, handled bool) {
nb.mu.Lock()
defer nb.mu.Unlock()
if nb.netMap == nil {
return false
return nil, false
}
// Locally cloned mutable nodes, to avoid calling AsStruct (clone)
@@ -890,7 +913,10 @@ func (nb *nodeBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
// in place as the loop runs. And if we return false, we'll just
// get a full netmap soon and reset all our state anyway.
rt := nb.routeMgr.Begin()
defer rt.Commit()
defer func() {
res := rt.Commit()
changedAllowedIPs = res.AllowedIPs
}()
for _, m := range muts {
switch m := m.(type) {
@@ -932,7 +958,7 @@ func (nb *nodeBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
nv, ok := nb.peers[nid]
if !ok {
// TODO(bradfitz): unexpected metric?
return false
return nil, false
}
n = nv.AsStruct()
mak.Set(&mutableNodes, nv.ID(), n)
@@ -943,7 +969,7 @@ func (nb *nodeBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
nb.peers[nid] = n.View()
}
nb.signalKeyWaitersForTestLocked()
return true
return nil, true
}
// unlockedNodesPermitted reports whether any peer with theUnsignedPeerAPIOnly bool set true has any of its allowed IPs
+19 -6
View File
@@ -317,15 +317,21 @@ func TestNodeBackendRouteManager(t *testing.T) {
wantPeerFor("8.8.8.8", tailcfg.NodeView{}) // exit node not selected
// Selecting peer 2 as the exit node resolves its stable ID and
// installs its /0 routes.
nb.updateRouteManagerPrefs(routePrefs{ExitNodeID: "stable2", ExitNodeSelected: true})
// installs its /0 routes. The commit reports peer 2's allowed
// prefixes as changed.
if changed := nb.updateRouteManagerPrefs(routePrefs{ExitNodeID: "stable2", ExitNodeSelected: true}); len(changed) != 1 || changed[p2.Key()] == nil {
t.Errorf("updateRouteManagerPrefs(exit=stable2) changed = %v; want just %v", changed, p2.Key())
}
wantPeerFor("8.8.8.8", p2)
// A selected exit node that resolves to no current peer must
// blackhole internet traffic, not fall back to "no exit node":
// the default routes stay in the OS route set with no outbound
// peer to carry them.
nb.updateRouteManagerPrefs(routePrefs{ExitNodeID: "no-such-node", ExitNodeSelected: true})
// peer to carry them. Peer 2's allowed prefixes lose the /0s,
// which the commit reports.
if changed := nb.updateRouteManagerPrefs(routePrefs{ExitNodeID: "no-such-node", ExitNodeSelected: true}); len(changed) != 1 || changed[p2.Key()] == nil {
t.Errorf("updateRouteManagerPrefs(exit=unresolved) changed = %v; want just %v", changed, p2.Key())
}
wantPeerFor("8.8.8.8", tailcfg.NodeView{})
if !nb.routeMgr.OSRoutes().Get(netip.MustParsePrefix("0.0.0.0/0")) {
t.Error("unresolved exit node: OSRoutes missing 0.0.0.0/0 blackhole route")
@@ -339,12 +345,19 @@ func TestNodeBackendRouteManager(t *testing.T) {
// Incremental deltas: add peer 3, remove peer 1.
p3 := mkPeer(3, "stable3", "100.64.0.3/32")
if !nb.UpdateNetmapDelta([]netmap.NodeMutation{
changed, handled := nb.UpdateNetmapDelta([]netmap.NodeMutation{
netmap.NodeMutationUpsert{Node: p3},
netmap.MakeNodeMutationRemove(1),
}) {
})
if !handled {
t.Fatal("UpdateNetmapDelta not handled")
}
if len(changed) != 2 || changed[p3.Key()] == nil {
t.Errorf("UpdateNetmapDelta changed = %v; want entries for %v and %v", changed, p3.Key(), p1.Key())
}
if v, ok := changed[p1.Key()]; !ok || v != nil {
t.Errorf("UpdateNetmapDelta changed[%v] = %v, %v; want nil, true for removed peer", p1.Key(), v, ok)
}
wantPeerFor("100.64.0.3", p3)
wantPeerFor("100.64.0.1", tailcfg.NodeView{})
+27 -14
View File
@@ -13,25 +13,38 @@ import (
"tailscale.com/wgengine"
)
// lookupPeerByIP returns the node public key for the peer that owns the
// given IP address. It is the fast path for [Engine.SetPeerByIPPacketFunc],
// handling exact-IP matches against node addresses; subnet routes and exit
// nodes are handled by a BART-based fallback in userspaceEngine that uses
// the wireguard-filtered peer list (see lastCfgFull).
// lookupPeerByIP returns the node public key for the peer that should
// handle traffic to the given IP address. It is installed as the
// [wgengine.Engine.SetPeerByIPPacketFunc] callback: exact node
// addresses hit the nodeByAddr fast path, and subnet routes and
// exit-node default routes fall back to the RouteManager's outbound
// table, so it stays correct under incremental netmap deltas.
//
// It is called by wireguard-go on every outbound packet (not cached), so
// it must be fast.
// It is called by wireguard-go on every outbound packet (not cached),
// so it must be fast.
func (b *LocalBackend) lookupPeerByIP(ip netip.Addr) (key.NodePublic, bool) {
nb := b.currentNode()
nid, ok := nb.NodeByAddr(ip)
if !ok {
return key.NodePublic{}, false
if nid, ok := nb.NodeByAddr(ip); ok {
peer, ok := nb.NodeByID(nid)
if !ok {
return key.NodePublic{}, false
}
return peer.Key(), true
}
peer, ok := nb.NodeByID(nid)
if !ok {
return key.NodePublic{}, false
if pr, ok := nb.routeMgr.Outbound().Lookup(ip); ok {
return pr.Key, true
}
return peer.Key(), true
return key.NodePublic{}, false
}
// peerAllowedIPs returns the prefixes from which the peer with the
// given public key is currently allowed to originate traffic, or
// ok=false if the peer is unknown (or currently routable via no
// prefix at all). It is installed as the
// [wgengine.Engine.SetPeerConfigFunc] callback, backing wireguard-go's
// lazy peer creation and per-delta peer sync.
func (b *LocalBackend) peerAllowedIPs(k key.NodePublic) (_ []netip.Prefix, ok bool) {
return b.currentNode().PeerAllowedIPs(k)
}
// resolveMagicDNS resolves a MagicDNS hostname to the owning node's IP
+3
View File
@@ -2016,6 +2016,9 @@ func (e *mockEngine) InstallCaptureHook(packet.CaptureCallback) {}
func (e *mockEngine) SetPeerByIPPacketFunc(func(netip.Addr) (_ key.NodePublic, ok bool)) {}
func (e *mockEngine) SetPeerForIPFunc(func(netip.Addr) (_ wgengine.PeerForIP, ok bool)) {}
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
}
+76 -5
View File
@@ -122,6 +122,12 @@ type userspaceEngine struct {
// for the cold-path control lookups (Ping, TSMP, pendopen, etc).
peerForIP atomic.Pointer[func(netip.Addr) (_ PeerForIP, ok bool)]
// peerConfigFn, if non-nil, is the live per-peer allowed-IPs
// source installed via [userspaceEngine.SetPeerConfigFunc]. When
// set, wgdev's PeerLookupFunc queries it directly, so reconfigs
// no longer install per-config lookup closures.
peerConfigFn atomic.Pointer[func(key.NodePublic) (allowedIPs []netip.Prefix, ok bool)]
lastCfgFull wgcfg.Config
lastRouter *router.Config
lastDNSConfig dns.ConfigView // or invalid if none
@@ -726,20 +732,84 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
e.peerByIPRoute.Store(rt)
e.logf("wgengine: Reconfig: configuring userspace WireGuard config (with %d peers)", len(full.Peers))
if err := wgcfg.ReconfigDevice(e.wgdev, &full, e.logf); err != nil {
if e.peerConfigFn.Load() != nil {
// The device has a long-lived PeerLookupFunc backed by the
// live config source, so only the peer set needs syncing;
// there is no per-config lookup closure to (re)install, and
// no removed peer can be resurrected with stale state.
//
// TODO(bradfitz): remove this O(n peers) sync. It's redundant
// with the incremental SyncDevicePeer calls that LocalBackend
// makes for exactly the peers whose allowed IPs changed. It
// only remains because peer changes still force a full
// Reconfig; once that's gated on actual router/DNS changes,
// this sync (and full-config peer syncing generally) can go.
peers := make(map[device.NoisePublicKey][]netip.Prefix, len(full.Peers))
for _, p := range full.Peers {
peers[p.PublicKey.Raw32()] = p.AllowedIPs
}
e.wgdev.RemoveMatchingPeers(func(pk device.NoisePublicKey) bool {
_, exists := peers[pk]
return !exists
})
// Update AllowedIPs on any already-active peers whose config
// may have changed. Peers that don't exist yet will get the
// correct AllowedIPs from the device's PeerLookupFunc when
// they are lazily created.
for pk, allowedIPs := range peers {
if peer, ok := e.wgdev.LookupActivePeer(pk); ok {
peer.SetAllowedIPs(allowedIPs)
}
}
} else if err := wgcfg.ReconfigDevice(e.wgdev, &full, e.logf); err != nil {
e.logf("wgdev.Reconfig: %v", err)
return err
}
return nil
}
// SetPeerConfigFunc implements [Engine.SetPeerConfigFunc]. It stores
// fn and installs a single wgdev PeerLookupFunc wrapping it, so
// lazily-created peers always get current allowed IPs and the lookup
// func never needs to be reinstalled as the peer set changes.
func (e *userspaceEngine) SetPeerConfigFunc(fn func(key.NodePublic) (allowedIPs []netip.Prefix, ok bool)) {
if fn == nil {
panic("SetPeerConfigFunc: nil fn")
}
e.peerConfigFn.Store(&fn)
e.wgdev.SetPeerLookupFunc(wgcfg.NewPeerLookupFunc(e.wgdev.Bind(), e.logf, func(pubk device.NoisePublicKey) ([]netip.Prefix, bool) {
return fn(key.NodePublicFromRaw32(mem.B(pubk[:])))
}))
}
// SyncDevicePeer implements [Engine.SyncDevicePeer].
func (e *userspaceEngine) SyncDevicePeer(k key.NodePublic) {
fn := e.peerConfigFn.Load()
if fn == nil {
return
}
e.wgLock.Lock()
defer e.wgLock.Unlock()
allowedIPs, ok := (*fn)(k)
if !ok {
e.wgdev.RemovePeer(k.Raw32())
return
}
if peer, ok := e.wgdev.LookupActivePeer(k.Raw32()); ok {
peer.SetAllowedIPs(allowedIPs)
}
}
// SetPeerByIPPacketFunc installs a callback used by wireguard-go to look up
// which peer should handle an outbound packet by destination IP.
//
// fn is an optional fast path for exact node-address matches (e.g. dst is a
// Tailscale IP). On miss (or if fn is nil), the engine's own BART table
// ([userspaceEngine.peerByIPRoute], built from the wireguard-filtered peer
// list) is consulted to handle subnet routes and exit-node default routes.
// 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).
//
// [NewUserspaceEngine] installs a BART-only default at engine creation time,
// so callers that don't call SetPeerByIPPacketFunc (e.g. those not running
@@ -750,6 +820,7 @@ func (e *userspaceEngine) SetPeerByIPPacketFunc(fn func(netip.Addr) (_ key.NodeP
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 {
+21
View File
@@ -18,6 +18,27 @@ func NewDevice(tunDev tun.Device, bind conn.Bind, logger *device.Logger) *device
return device.NewDevice(tunDev, bind, logger)
}
// NewPeerLookupFunc returns a [device.PeerLookupFunc] that lazily
// creates peers using allowedIPs as the source of each peer's allowed
// IPs. The peer's endpoint is derived from its public key via bind.
func NewPeerLookupFunc(bind conn.Bind, logf logger.Logf, allowedIPs func(device.NoisePublicKey) ([]netip.Prefix, bool)) device.PeerLookupFunc {
return func(pubk device.NoisePublicKey) (_ *device.NewPeerConfig, ok bool) {
ips, ok := allowedIPs(pubk)
if !ok {
return nil, false
}
ep, err := bind.ParseEndpoint(fmt.Sprintf("%x", pubk[:]))
if err != nil {
logf("wgcfg: failed to parse endpoint for peer %x: %v", pubk[:8], err)
return nil, false
}
return &device.NewPeerConfig{
AllowedIPs: ips,
Endpoint: ep,
}, true
}
}
// ReconfigDevice replaces the existing device configuration with cfg.
//
// Instead of using the UAPI text protocol, it uses the wireguard-go direct API
+27
View File
@@ -197,6 +197,33 @@ type Engine interface {
// look up which peer should handle an outbound packet by destination IP.
SetPeerByIPPacketFunc(func(netip.Addr) (_ key.NodePublic, ok bool))
// SetPeerConfigFunc installs the live source of per-peer WireGuard
// configuration: given a peer's public key, fn returns the prefixes
// the peer is currently allowed to originate traffic from, or
// ok=false if the peer is unknown (in which case it must not exist
// in the WireGuard device). The engine installs a single
// [device.PeerLookupFunc] wrapping fn, so lazily-created peers
// always see current state and the lookup func never needs to be
// reinstalled as peers come and go.
//
// It is expected to be called once during LocalBackend construction,
// before the first [Engine.Reconfig]. fn is called rarely (when
// wireguard-go first hears from a peer it doesn't have) and may
// acquire locks.
SetPeerConfigFunc(fn func(key.NodePublic) (allowedIPs []netip.Prefix, ok bool))
// SyncDevicePeer synchronizes the WireGuard device's state for a
// single peer with the config source installed via
// [Engine.SetPeerConfigFunc]: if the source no longer knows the
// peer, it is removed from the device; if the peer is active in the
// device, its allowed IPs are updated. It does O(1) work (plus the
// config source lookup) and is intended to be called for each peer
// added, updated, or removed by an incremental netmap delta,
// avoiding a full [Engine.Reconfig].
//
// It is a no-op if no config source is installed.
SyncDevicePeer(key.NodePublic)
// SetNetLogSource installs the [NetLogSource] consulted by the
// engine's network flow logger for node lookups and the current
// audit logging identity.