mapper: send a disable-ipv4 holder its peers over IPv6 only

As SaaS. Peers' 100.x addresses routed into tailscale0 and hid a local
host with the same address, which is what the attr is meant to avoid.
This commit is contained in:
Kristoffer Dalby committed 2026-11-03 17:05:00 +01:00
1 parent 68b0274694
commit b04bbd4317
4 files changed
+46 -3

No files matched your search

+12
View File
@@ -281,6 +281,8 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
// Snapshot these peers' policy caps under one policy-manager lock.
peerCapMaps := b.mapper.state.NodeCapMapsForPeers(peers)
_, ipv4Disabled := b.mapper.state.NodeCapMap(node.ID())[types.NodeAttrDisableIPv4]
// Build tail nodes with per-peer via-aware route function.
tailPeers := make([]*tailcfg.Node, 0, peers.Len())
@@ -327,6 +329,16 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
tn.CapMap = policyv2.PeerCapMap(peer, peerCapMaps[peer.ID()])
tn.Hostinfo = types.PeerHostinfo(tn.Hostinfo)
// A disable-ipv4 holder sees its peers over IPv6 only, as on SaaS, so
// their 100.x addresses stay off its interface. Routes stay.
if ipv4Disabled {
ownIPv4 := func(p netip.Prefix) bool {
return p.Addr().Is4() && slices.Contains(tn.Addresses, p)
}
tn.AllowedIPs = slices.DeleteFunc(tn.AllowedIPs, ownIPv4)
tn.Addresses = slices.DeleteFunc(tn.Addresses, func(p netip.Prefix) bool { return p.Addr().Is4() })
}
// As on Tailscale SaaS, a connected expired peer shows online, but
// without the endpoints and home DERP region that would reach it.
if peer.IsExpired() {
+8
View File
@@ -1812,6 +1812,14 @@ func (b *MapResponseBuilder) buildTailPeersCapMapsReference(peers views.Slice[ty
tn.CapMap = policyv2.PeerCapMap(peer, allCapMaps[peer.ID()])
tn.Hostinfo = types.PeerHostinfo(tn.Hostinfo)
if _, ipv4Disabled := allCapMaps[node.ID()][types.NodeAttrDisableIPv4]; ipv4Disabled {
ownIPv4 := func(p netip.Prefix) bool {
return p.Addr().Is4() && slices.Contains(tn.Addresses, p)
}
tn.AllowedIPs = slices.DeleteFunc(tn.AllowedIPs, ownIPv4)
tn.Addresses = slices.DeleteFunc(tn.Addresses, func(p netip.Prefix) bool { return p.Addr().Is4() })
}
if peer.IsExpired() {
if peer.ActiveSessions() > 0 {
tn.Online, tn.LastSeen = new(true), nil
+23
View File
@@ -353,6 +353,29 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
compareViaMapCompat(t, c, clients)
}
// TestDisableIPv4PeersCompat: a disable-ipv4 holder sees its peers over IPv6
// only, in both recordings of such a viewer.
func TestDisableIPv4PeersCompat(t *testing.T) {
t.Parallel()
for _, id := range []string{"nodeattrs-attr-c15-disable-ipv4", "nodeattrs-attr-c16-disable-ipv4-router"} {
t.Run(id, func(t *testing.T) {
t.Parallel()
c, err := testcapture.Read(filepath.Join("..", "policy", "v2", "testdata", "nodeattrs_results", id+".hujson"))
require.NoError(t, err)
_, clients := startViaMapCompat(t, c, true)
for viewer, want := range c.Captures {
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
comparePeers(collect, clients[viewer].Netmap(), want, clients, id+"/"+viewer, nil)
}, 20*time.Second, 200*time.Millisecond, "viewer %s", viewer)
}
})
}
}
// startViaMapCompat starts a server holding the capture's policy and a
// client per captured node, approving routes as [startCaptureClients] does,
// and waits for every client's first netmap and the peers SaaS gave it.
+3 -3
View File
@@ -44,11 +44,11 @@ var (
// [netip.Prefix] values representing the routes for that node.
type RouteFunc func(id NodeID) []netip.Prefix
// nodeAttrDisableIPv4 is the policy nodeAttr key that suppresses the
// NodeAttrDisableIPv4 is the policy nodeAttr key that suppresses the
// node's own IPv4 CGNAT prefix in [tailcfg.Node.Addresses] and
// [tailcfg.Node.AllowedIPs]. Subnet routes the node advertises remain.
// See https://tailscale.com/docs/reference/troubleshooting/network-configuration/cgnat-conflicts.
const nodeAttrDisableIPv4 nodecap.Cap = "disable-ipv4"
const NodeAttrDisableIPv4 nodecap.Cap = "disable-ipv4"
// filterIPv4 returns ps with every IPv4 prefix dropped. Used by
// [NodeView.TailNode] when the node carries the disable-ipv4 nodeAttr.
@@ -1402,7 +1402,7 @@ func (nv NodeView) TailNode(
// subnet routes -- even IPv4 ones -- survive: routes belong to
// the routing layer, not the node's identity. Mirrors the SaaS
// captures in testdata/nodeattrs_results/nodeattrs-attr-c1{5,6}-disable-ipv4*.
_, ipv4Disabled := selfPolicyCaps[nodeAttrDisableIPv4]
_, ipv4Disabled := selfPolicyCaps[NodeAttrDisableIPv4]
addresses := nv.Prefixes()
if ipv4Disabled {