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
2 changed files with 262 additions and 0 deletions
Showing only changes of commit ae743642d9 - Show all commits
+17
View File
@@ -2452,7 +2452,24 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
}
}
ms.UpdateNetmapDelta(muts)
// Temporary for 1.100.x: force a full authReconfig + SetNetworkMap
// 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 / the engine BART / wgdev's PeerLookupFunc closure
// / e.netMap all stale, and Engine.PeerForIP, lookupPeerByIP, and
// outbound wgdev encryption all missed those peers. authReconfig
// fixes the wireguard side; SetNetworkMap fixes the e.netMap that
// PeerForIP reads. The proper per-peer fix lands in the next dev
// cycle. See tailscale/corp#43394.
needsAuthReconfig = needsAuthReconfig || peersUpsertedOrRemoved
if needsAuthReconfig {
if peersUpsertedOrRemoved {
if nm := cn.netMapWithPeers(); nm != nil {
b.e.SetNetworkMap(nm)
}
}
b.authReconfigLocked()
}
+245
View File
@@ -1695,6 +1695,251 @@ func TestFallbackTCPHandler(t *testing.T) {
}
}
// TestPingPeerLearnedViaDelta verifies that `tailscale ping` works
// for a peer that the local node learned about only via a
// [tailcfg.MapResponse.PeersChanged] delta, never via a full
// [tailcfg.MapResponse.Peers] list.
//
// Before aa5da2e5f22a, every peer change rebuilt a full netmap on
// the engine, so [wgengine.Engine.SetNetworkMap] kept the engine's
// cached netmap fresh and [wgengine.Engine.Reconfig] kept wgdev and
// e.lastCfgFull fresh. After that refactor, peer adds and removes
// ride the delta path and only mutate [nodeBackend]; the engine's
// cached netmap and wireguard config stayed stale, and
// [wgengine.Engine.PeerForIP] / [wgengine.Engine.Ping] / wgdev's
// outbound encryption all missed the new peer.
//
// Two subtests exercise different layers:
//
// - "disco" uses [tailcfg.PingDisco], which goes straight to
// magicsock (which has the peer via UpsertPeer) and bypasses
// wireguard-go entirely. It targets the cold-path PeerForIP
// lookup -- before the fix this missed the new peer with
// "no matching peer".
//
// - "tsmp" uses [tailcfg.PingTSMP], which builds a real
// IP-proto-99 packet and pushes it through the full data path:
// PeerForIP -> lookupPeerByIP -> wgdev encryption ->
// magicsock transport. The receiving side's [tstun.Wrapper]
// intercepts TSMP and replies with a pong. Catches the
// wireguard-go side too: wgdev's PeerLookupFunc closure
// (captured at the last ReconfigDevice) didn't have the new
// peer's noise key, so even after lookupPeerByIP returned the
// right NodePublic wgdev couldn't lazily create the peer for
// outbound encryption.
//
// See tailscale/corp#43394.
func TestPingPeerLearnedViaDelta(t *testing.T) {
for _, pt := range []tailcfg.PingType{tailcfg.PingDisco, tailcfg.PingTSMP} {
t.Run(string(pt), func(t *testing.T) {
testPingPeerLearnedViaDelta(t, pt)
})
}
}
func testPingPeerLearnedViaDelta(t *testing.T, pt tailcfg.PingType) {
if runtime.GOARCH == "386" {
t.Skip("skipping on 386: see https://github.com/tailscale/tailscale/issues/20146")
}
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second)
defer cancel()
controlURL, control := startControl(t)
// Bring up s1 alone so its initial (auto-generated) MapResponse
// has no peers; testcontrol only has s1 registered at this point.
s1, _, s1Key := startServer(t, ctx, controlURL, "s1")
// Switch s1 into manual MapResponse mode. The empty response is a
// no-op heartbeat; the side effect is that suppressAutoMapResponses
// is set for s1, so the s2 join below cannot reach s1 as a full
// auto-generated netmap.
if !control.AddRawMapResponse(s1Key, &tailcfg.MapResponse{}) {
t.Fatal("AddRawMapResponse(s1, empty): node not connected")
}
// Bring up s2. testcontrol would normally push a peer-changed
// update to s1's long-poll, but autos for s1 are suppressed.
// s2 itself is not suppressed, so it gets a full netmap that
// includes s1, which is necessary for disco to complete in both
// directions (and for the TSMP pong to make it back).
_, s2ip, s2Key := startServer(t, ctx, controlURL, "s2")
// Snapshot s2's node-as-seen-by-control and inject it into s1's
// stream as a PeersChanged delta.
s2Node := control.Node(s2Key)
if !control.AddRawMapResponse(s1Key, &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{s2Node},
}) {
t.Fatal("AddRawMapResponse(s1, PeersChanged): node not connected")
}
// Wait for the delta to land in s1's nodeBackend.
if err := waitFor(t, ctx, s1, func(nm *netmap.NetworkMap) bool {
for _, p := range nm.Peers {
if p.Key() == s2Key {
return true
}
}
return false
}); err != nil {
t.Fatalf("waitFor s2 in s1 netmap: %v", err)
}
lc1, err := s1.LocalClient()
if err != nil {
t.Fatal(err)
}
// Per-ping budget within the larger test ctx: enough headroom for
// wireguard-go's RekeyTimeout (5s) plus the actual handshake on
// slow CI (notably GOARCH=386 emulation where Curve25519/ChaCha20
// lack the amd64 assembly fast paths), but tight enough that a
// hung Ping points the finger at this call rather than swallowing
// the whole test deadline.
pingCtx, cancelPing := context.WithTimeout(ctx, 60*time.Second)
defer cancelPing()
pr, err := lc1.Ping(pingCtx, s2ip, pt)
if err != nil {
t.Fatalf("Ping(%s): %v", pt, err)
}
if pr.Err != "" {
t.Fatalf("Ping(%s) s1->s2 failed: %s (want success)", pt, pr.Err)
}
}
// TestPingSubnetRouteOfDeltaPeer verifies that when a peer arrives
// purely via a [tailcfg.MapResponse.PeersChanged] delta and that peer
// advertises a subnet route, the local node can resolve an IP within
// that subnet to the new peer and exchange traffic with it.
//
// Before the fix, [netmapDeltaNeedsAuthReconfig] returned false for a
// brand-new peer Upsert (the NodeID wasn't already known), so
// [LocalBackend.authReconfigLocked] -- the only path that pushes a
// fresh wireguard config into the engine -- never ran. The engine's
// wireguard-filtered peer list and BART stayed stale, so
// [LocalBackend.lookupPeerByIP] and [LocalBackend.peerForIP] both
// missed any IP inside the advertised CIDR, and wgdev's
// PeerLookupFunc closure didn't have the new peer's noise key for
// outbound encryption.
//
// See tailscale/corp#43394.
func TestPingSubnetRouteOfDeltaPeer(t *testing.T) {
if runtime.GOARCH == "386" {
t.Skip("skipping on 386: see https://github.com/tailscale/tailscale/issues/20146")
}
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second)
defer cancel()
controlURL, control := startControl(t)
// Bring up s1 alone so its initial netmap has no peers.
s1, _, s1Key := startServer(t, ctx, controlURL, "s1")
// Accept subnet routes on s1; otherwise nmcfg.WGCfg strips a peer's
// non-self AllowedIPs out of the wireguard config (and thus out of
// the engine BART), and a subnet-router delta wouldn't actually
// install the route locally even with the fix.
lc1 := must.Get(s1.LocalClient())
must.Get(lc1.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{RouteAll: true},
RouteAllSet: true,
}))
// Switch s1 into manual MapResponse mode so the s2 join cannot
// arrive as a full auto-generated netmap.
if !control.AddRawMapResponse(s1Key, &tailcfg.MapResponse{}) {
t.Fatal("AddRawMapResponse(s1, empty): node not connected")
}
// Bring up s2. We'll treat it as a subnet router for 10.0.0.0/24
// by adding that prefix to its AllowedIPs in the delta we inject
// below. (s2 isn't a real subnet router -- we don't try to forward
// traffic through it. The receiving side's tstun.Wrapper handles
// TSMP regardless of dst IP, so the pong comes back as long as
// the encrypt/transport chain works.)
_, _, s2Key := startServer(t, ctx, controlURL, "s2")
const subnet = "10.0.0.0/24"
subnetPrefix := netip.MustParsePrefix(subnet)
probeIP := netip.MustParseAddr("10.0.0.5")
// Inject s2 into s1 as a PeersChanged delta with the subnet route
// in AllowedIPs.
s2Node := control.Node(s2Key)
s2Node.PrimaryRoutes = []netip.Prefix{subnetPrefix}
s2Node.AllowedIPs = append(s2Node.AllowedIPs, subnetPrefix)
if !control.AddRawMapResponse(s1Key, &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{s2Node},
}) {
t.Fatal("AddRawMapResponse(s1, PeersChanged): node not connected")
}
// Wait for the delta to land in s1's nodeBackend.
if err := waitFor(t, ctx, s1, func(nm *netmap.NetworkMap) bool {
for _, p := range nm.Peers {
if p.Key() == s2Key {
return true
}
}
return false
}); err != nil {
t.Fatalf("waitFor s2 in s1 netmap: %v", err)
}
// PingTSMP sends a real IP packet (IP proto 99) over wireguard
// to probeIP, exercising the full data path -- PeerForIP lookup,
// outbound wgdev encryption, magicsock transport. The receiving
// side (s2's tstun.Wrapper) intercepts TSMP regardless of dst
// IP and replies with a pong. So this catches both halves of
// the bug: a stale BART / lastCfgFull (PeerForIP / lookupPeerByIP
// miss) AND a stale wgdev PeerLookupFunc closure (peer's noise
// key not yet registered for outbound encryption).
pingCtx, cancelPing := context.WithTimeout(ctx, 60*time.Second)
defer cancelPing()
pr, err := lc1.Ping(pingCtx, probeIP, tailcfg.PingTSMP)
if err != nil {
t.Fatalf("Ping: %v", err)
}
if pr.Err != "" {
t.Fatalf("ping s1->%v (subnet route via s2) failed: %s (want success)", probeIP, pr.Err)
}
}
// TestPingSelfReturnsIsLocalIP verifies that pinging one's own
// Tailscale IP takes the IsSelf early-out in [wgengine.Engine.Ping]
// instead of trying to ping self via magicsock. Lives here as a
// regression guard against future refactors of the PeerForIP self
// path; the original userspaceEngine.PeerForIP handles self via a
// dedicated nm.GetAddresses() scan, but anything that re-routes
// PeerForIP through a more general index needs to keep
// [wgengine.PeerForIP.IsSelf] set for self addresses.
func TestPingSelfReturnsIsLocalIP(t *testing.T) {
tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second)
defer cancel()
controlURL, _ := startControl(t)
s1, s1ip, _ := startServer(t, ctx, controlURL, "s1")
lc, err := s1.LocalClient()
if err != nil {
t.Fatal(err)
}
pr, err := lc.Ping(ctx, s1ip, tailcfg.PingDisco)
if err != nil {
t.Fatalf("Ping: %v", err)
}
if !pr.IsLocalIP {
t.Errorf("IsLocalIP = false, want true (pr=%+v)", pr)
}
if pr.Err == "" {
t.Errorf("Err = %q, want a 'local Tailscale IP' message", pr.Err)
}
}
func TestCapturePcap(t *testing.T) {
const timeLimit = 120
ctx, cancel := context.WithTimeout(context.Background(), timeLimit*time.Second)