diff --git a/control/controlclient/map.go b/control/controlclient/map.go index d5fd84c6d..13b11d6df 100644 --- a/control/controlclient/map.go +++ b/control/controlclient/map.go @@ -241,6 +241,10 @@ func upgradeNode(n *tailcfg.Node) { } n.LegacyDERPString = "" } + + if n.AllowedIPs == nil { + n.AllowedIPs = slices.Clone(n.Addresses) + } } func (ms *mapSession) tryHandleIncrementally(res *tailcfg.MapResponse) bool { diff --git a/control/controlclient/map_test.go b/control/controlclient/map_test.go index 9c8c0c3aa..09441d066 100644 --- a/control/controlclient/map_test.go +++ b/control/controlclient/map_test.go @@ -1007,10 +1007,16 @@ func TestPatchifyPeersChanged(t *testing.T) { } func TestUpgradeNode(t *testing.T) { + a1 := netip.MustParsePrefix("0.0.0.1/32") + a2 := netip.MustParsePrefix("0.0.0.2/32") + a3 := netip.MustParsePrefix("0.0.0.3/32") + a4 := netip.MustParsePrefix("0.0.0.4/32") + tests := []struct { name string in *tailcfg.Node want *tailcfg.Node + also func(t *testing.T, got *tailcfg.Node) // optional }{ { name: "nil", @@ -1037,6 +1043,29 @@ func TestUpgradeNode(t *testing.T) { in: &tailcfg.Node{HomeDERP: 2}, want: &tailcfg.Node{HomeDERP: 2}, }, + { + name: "implicit-allowed-ips-all-set", + in: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a3, a4}}, + want: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a3, a4}}, + }, + { + name: "implicit-allowed-ips-only-address-set", + in: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}}, + want: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{a1, a2}}, + also: func(t *testing.T, got *tailcfg.Node) { + if t.Failed() { + return + } + if &got.Addresses[0] == &got.AllowedIPs[0] { + t.Error("Addresses and AllowIPs alias the same memory") + } + }, + }, + { + name: "implicit-allowed-ips-set-empty-slice", + in: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{}}, + want: &tailcfg.Node{Addresses: []netip.Prefix{a1, a2}, AllowedIPs: []netip.Prefix{}}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1048,6 +1077,9 @@ func TestUpgradeNode(t *testing.T) { if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("wrong result (-want +got):\n%s", diff) } + if tt.also != nil { + tt.also(t, got) + } }) } diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 76945ec10..9b26e8883 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -154,7 +154,8 @@ type CapabilityVersion int // - 109: 2024-11-18: Client supports filtertype.Match.SrcCaps (issue #12542) // - 110: 2024-12-12: removed never-before-used Tailscale SSH public key support (#14373) // - 111: 2025-01-14: Client supports a peer having Node.HomeDERP (issue #14636) -const CurrentCapabilityVersion CapabilityVersion = 111 +// - 112: 2025-01-14: Client interprets AllowedIPs of nil as meaning same as Addresses +const CurrentCapabilityVersion CapabilityVersion = 112 // ID is an integer ID for a user, node, or login allocated by the // control plane. @@ -343,9 +344,18 @@ type Node struct { KeySignature tkatype.MarshaledSignature `json:",omitempty"` Machine key.MachinePublic DiscoKey key.DiscoPublic - Addresses []netip.Prefix // IP addresses of this Node directly - AllowedIPs []netip.Prefix // range of IP addresses to route to this node - Endpoints []netip.AddrPort `json:",omitempty"` // IP+port (public via STUN, and local LANs) + + // Addresses are the IP addresses of this Node directly. + Addresses []netip.Prefix + + // AllowedIPs are the IP ranges to route to this node. + // + // As of CapabilityVersion 112, this may be nil (null or undefined) on the wire + // to mean the same as Addresses. Internally, it is always filled in with + // its possibly-implicit value. + AllowedIPs []netip.Prefix + + Endpoints []netip.AddrPort `json:",omitempty"` // IP+port (public via STUN, and local LANs) // LegacyDERPString is this node's home LegacyDERPString region ID integer, but shoved into an // IP:port string for legacy reasons. The IP address is always "127.3.3.40"