all: add Node.HomeDERP int, phase out "127.3.3.40:$region" hack [capver 111]

This deprecates the old "DERP string" packing a DERP region ID into an
IP:port of 127.3.3.40:$REGION_ID and just uses an integer, like
PeerChange.DERPRegion does.

We still support servers sending the old form; they're converted to
the new form internally right when they're read off the network.

Updates #14636

Change-Id: I9427ec071f02a2c6d75ccb0fcbf0ecff9f19f26f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-01-14 10:19:52 -08:00 committed by Brad Fitzpatrick
parent 66269dc934
commit 2fc4455e6d
19 changed files with 171 additions and 97 deletions

View File

@ -280,7 +280,7 @@ func TestConnMemoryOverhead(t *testing.T) {
growthTotal := int64(ms.HeapAlloc) - int64(ms0.HeapAlloc) growthTotal := int64(ms.HeapAlloc) - int64(ms0.HeapAlloc)
growthEach := float64(growthTotal) / float64(num) growthEach := float64(growthTotal) / float64(num)
t.Logf("Alloced %v bytes, %.2f B/each", growthTotal, growthEach) t.Logf("Alloced %v bytes, %.2f B/each", growthTotal, growthEach)
const max = 2000 const max = 2048
if growthEach > max { if growthEach > max {
t.Errorf("allocated more than expected; want max %v bytes/each", max) t.Errorf("allocated more than expected; want max %v bytes/each", max)
} }

View File

@ -7,7 +7,6 @@ import (
"cmp" "cmp"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"maps" "maps"
"net" "net"
"reflect" "reflect"
@ -166,6 +165,7 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
// For responses that mutate the self node, check for updated nodeAttrs. // For responses that mutate the self node, check for updated nodeAttrs.
if resp.Node != nil { if resp.Node != nil {
upgradeNode(resp.Node)
if DevKnob.StripCaps() { if DevKnob.StripCaps() {
resp.Node.Capabilities = nil resp.Node.Capabilities = nil
resp.Node.CapMap = nil resp.Node.CapMap = nil
@ -181,6 +181,13 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
ms.controlKnobs.UpdateFromNodeAttributes(resp.Node.CapMap) ms.controlKnobs.UpdateFromNodeAttributes(resp.Node.CapMap)
} }
for _, p := range resp.Peers {
upgradeNode(p)
}
for _, p := range resp.PeersChanged {
upgradeNode(p)
}
// Call Node.InitDisplayNames on any changed nodes. // Call Node.InitDisplayNames on any changed nodes.
initDisplayNames(cmp.Or(resp.Node.View(), ms.lastNode), resp) initDisplayNames(cmp.Or(resp.Node.View(), ms.lastNode), resp)
@ -216,6 +223,26 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
return nil return nil
} }
// upgradeNode upgrades Node fields from the server into the modern forms
// not using deprecated fields.
func upgradeNode(n *tailcfg.Node) {
if n == nil {
return
}
if n.LegacyDERPString != "" {
if n.HomeDERP == 0 {
ip, portStr, err := net.SplitHostPort(n.LegacyDERPString)
if ip == tailcfg.DerpMagicIP && err == nil {
port, err := strconv.Atoi(portStr)
if err == nil {
n.HomeDERP = port
}
}
}
n.LegacyDERPString = ""
}
}
func (ms *mapSession) tryHandleIncrementally(res *tailcfg.MapResponse) bool { func (ms *mapSession) tryHandleIncrementally(res *tailcfg.MapResponse) bool {
if ms.controlKnobs != nil && ms.controlKnobs.DisableDeltaUpdates.Load() { if ms.controlKnobs != nil && ms.controlKnobs.DisableDeltaUpdates.Load() {
return false return false
@ -443,7 +470,7 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
stats.changed++ stats.changed++
mut := vp.AsStruct() mut := vp.AsStruct()
if pc.DERPRegion != 0 { if pc.DERPRegion != 0 {
mut.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, pc.DERPRegion) mut.HomeDERP = pc.DERPRegion
patchDERPRegion.Add(1) patchDERPRegion.Add(1)
} }
if pc.Cap != 0 { if pc.Cap != 0 {
@ -631,17 +658,13 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
if !views.SliceEqual(was.Endpoints(), views.SliceOf(n.Endpoints)) { if !views.SliceEqual(was.Endpoints(), views.SliceOf(n.Endpoints)) {
pc().Endpoints = slices.Clone(n.Endpoints) pc().Endpoints = slices.Clone(n.Endpoints)
} }
case "DERP": case "LegacyDERPString":
if was.DERP() != n.DERP { if was.LegacyDERPString() != "" || n.LegacyDERPString != "" {
ip, portStr, err := net.SplitHostPort(n.DERP) panic("unexpected; caller should've already called upgradeNode")
if err != nil || ip != "127.3.3.40" { }
return nil, false case "HomeDERP":
} if was.HomeDERP() != n.HomeDERP {
port, err := strconv.Atoi(portStr) pc().DERPRegion = n.HomeDERP
if err != nil || port < 1 || port > 65535 {
return nil, false
}
pc().DERPRegion = port
} }
case "Hostinfo": case "Hostinfo":
if !was.Hostinfo().Valid() && !n.Hostinfo.Valid() { if !was.Hostinfo().Valid() && !n.Hostinfo.Valid() {

View File

@ -50,9 +50,9 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
n.LastSeen = &t n.LastSeen = &t
} }
} }
withDERP := func(d string) func(*tailcfg.Node) { withDERP := func(regionID int) func(*tailcfg.Node) {
return func(n *tailcfg.Node) { return func(n *tailcfg.Node) {
n.DERP = d n.HomeDERP = regionID
} }
} }
withEP := func(ep string) func(*tailcfg.Node) { withEP := func(ep string) func(*tailcfg.Node) {
@ -189,14 +189,14 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
}, },
{ {
name: "ep_change_derp", name: "ep_change_derp",
prev: peers(n(1, "foo", withDERP("127.3.3.40:3"))), prev: peers(n(1, "foo", withDERP(3))),
mapRes: &tailcfg.MapResponse{ mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{ PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1, NodeID: 1,
DERPRegion: 4, DERPRegion: 4,
}}, }},
}, },
want: peers(n(1, "foo", withDERP("127.3.3.40:4"))), want: peers(n(1, "foo", withDERP(4))),
wantStats: updateStats{changed: 1}, wantStats: updateStats{changed: 1},
}, },
{ {
@ -213,19 +213,19 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
}, },
{ {
name: "ep_change_udp_2", name: "ep_change_udp_2",
prev: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:111"))), prev: peers(n(1, "foo", withDERP(3), withEP("1.2.3.4:111"))),
mapRes: &tailcfg.MapResponse{ mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{ PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1, NodeID: 1,
Endpoints: eps("1.2.3.4:56"), Endpoints: eps("1.2.3.4:56"),
}}, }},
}, },
want: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:56"))), want: peers(n(1, "foo", withDERP(3), withEP("1.2.3.4:56"))),
wantStats: updateStats{changed: 1}, wantStats: updateStats{changed: 1},
}, },
{ {
name: "ep_change_both", name: "ep_change_both",
prev: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:111"))), prev: peers(n(1, "foo", withDERP(3), withEP("1.2.3.4:111"))),
mapRes: &tailcfg.MapResponse{ mapRes: &tailcfg.MapResponse{
PeersChangedPatch: []*tailcfg.PeerChange{{ PeersChangedPatch: []*tailcfg.PeerChange{{
NodeID: 1, NodeID: 1,
@ -233,7 +233,7 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
Endpoints: eps("1.2.3.4:56"), Endpoints: eps("1.2.3.4:56"),
}}, }},
}, },
want: peers(n(1, "foo", withDERP("127.3.3.40:2"), withEP("1.2.3.4:56"))), want: peers(n(1, "foo", withDERP(2), withEP("1.2.3.4:56"))),
wantStats: updateStats{changed: 1}, wantStats: updateStats{changed: 1},
}, },
{ {
@ -744,8 +744,8 @@ func TestPeerChangeDiff(t *testing.T) {
}, },
{ {
name: "patch-derp", name: "patch-derp",
a: &tailcfg.Node{ID: 1, DERP: "127.3.3.40:1"}, a: &tailcfg.Node{ID: 1, HomeDERP: 1},
b: &tailcfg.Node{ID: 1, DERP: "127.3.3.40:2"}, b: &tailcfg.Node{ID: 1, HomeDERP: 2},
want: &tailcfg.PeerChange{NodeID: 1, DERPRegion: 2}, want: &tailcfg.PeerChange{NodeID: 1, DERPRegion: 2},
}, },
{ {
@ -929,23 +929,23 @@ func TestPatchifyPeersChanged(t *testing.T) {
mr0: &tailcfg.MapResponse{ mr0: &tailcfg.MapResponse{
Node: &tailcfg.Node{Name: "foo.bar.ts.net."}, Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
Peers: []*tailcfg.Node{ Peers: []*tailcfg.Node{
{ID: 1, DERP: "127.3.3.40:1", Hostinfo: hi}, {ID: 1, HomeDERP: 1, Hostinfo: hi},
{ID: 2, DERP: "127.3.3.40:2", Hostinfo: hi}, {ID: 2, HomeDERP: 2, Hostinfo: hi},
{ID: 3, DERP: "127.3.3.40:3", Hostinfo: hi}, {ID: 3, HomeDERP: 3, Hostinfo: hi},
}, },
}, },
mr1: &tailcfg.MapResponse{ mr1: &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{ PeersChanged: []*tailcfg.Node{
{ID: 1, DERP: "127.3.3.40:11", Hostinfo: hi}, {ID: 1, HomeDERP: 11, Hostinfo: hi},
{ID: 2, StableID: "other-change", Hostinfo: hi}, {ID: 2, StableID: "other-change", Hostinfo: hi},
{ID: 3, DERP: "127.3.3.40:33", Hostinfo: hi}, {ID: 3, HomeDERP: 33, Hostinfo: hi},
{ID: 4, DERP: "127.3.3.40:4", Hostinfo: hi}, {ID: 4, HomeDERP: 4, Hostinfo: hi},
}, },
}, },
want: &tailcfg.MapResponse{ want: &tailcfg.MapResponse{
PeersChanged: []*tailcfg.Node{ PeersChanged: []*tailcfg.Node{
{ID: 2, StableID: "other-change", Hostinfo: hi}, {ID: 2, StableID: "other-change", Hostinfo: hi},
{ID: 4, DERP: "127.3.3.40:4", Hostinfo: hi}, {ID: 4, HomeDERP: 4, Hostinfo: hi},
}, },
PeersChangedPatch: []*tailcfg.PeerChange{ PeersChangedPatch: []*tailcfg.PeerChange{
{NodeID: 1, DERPRegion: 11}, {NodeID: 1, DERPRegion: 11},
@ -1006,6 +1006,53 @@ func TestPatchifyPeersChanged(t *testing.T) {
} }
} }
func TestUpgradeNode(t *testing.T) {
tests := []struct {
name string
in *tailcfg.Node
want *tailcfg.Node
}{
{
name: "nil",
in: nil,
want: nil,
},
{
name: "empty",
in: new(tailcfg.Node),
want: new(tailcfg.Node),
},
{
name: "derp-both",
in: &tailcfg.Node{HomeDERP: 1, LegacyDERPString: tailcfg.DerpMagicIP + ":2"},
want: &tailcfg.Node{HomeDERP: 1},
},
{
name: "derp-str-only",
in: &tailcfg.Node{LegacyDERPString: tailcfg.DerpMagicIP + ":2"},
want: &tailcfg.Node{HomeDERP: 2},
},
{
name: "derp-int-only",
in: &tailcfg.Node{HomeDERP: 2},
want: &tailcfg.Node{HomeDERP: 2},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got *tailcfg.Node
if tt.in != nil {
got = ptr.To(*tt.in) // shallow clone
}
upgradeNode(got)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("wrong result (-want +got):\n%s", diff)
}
})
}
}
func BenchmarkMapSessionDelta(b *testing.B) { func BenchmarkMapSessionDelta(b *testing.B) {
for _, size := range []int{10, 100, 1_000, 10_000} { for _, size := range []int{10, 100, 1_000, 10_000} {
b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) { b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
@ -1022,7 +1069,7 @@ func BenchmarkMapSessionDelta(b *testing.B) {
res.Peers = append(res.Peers, &tailcfg.Node{ res.Peers = append(res.Peers, &tailcfg.Node{
ID: tailcfg.NodeID(i + 2), ID: tailcfg.NodeID(i + 2),
Name: fmt.Sprintf("peer%d.bar.ts.net.", i), Name: fmt.Sprintf("peer%d.bar.ts.net.", i),
DERP: "127.3.3.40:10", HomeDERP: 10,
Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.2.3/32"), netip.MustParsePrefix("fd7a:115c:a1e0::123/128")}, Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.2.3/32"), netip.MustParsePrefix("fd7a:115c:a1e0::123/128")},
AllowedIPs: []netip.Prefix{netip.MustParsePrefix("100.100.2.3/32"), netip.MustParsePrefix("fd7a:115c:a1e0::123/128")}, AllowedIPs: []netip.Prefix{netip.MustParsePrefix("100.100.2.3/32"), netip.MustParsePrefix("fd7a:115c:a1e0::123/128")},
Endpoints: eps("192.168.1.2:345", "192.168.1.3:678"), Endpoints: eps("192.168.1.2:345", "192.168.1.3:678"),

View File

@ -116,7 +116,7 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow ti
// since we discover endpoints via DERP, and due to DERP return // since we discover endpoints via DERP, and due to DERP return
// path optimization. // path optimization.
mut.Endpoints = nil mut.Endpoints = nil
mut.DERP = "" mut.HomeDERP = 0
// Defense-in-depth: break the node's public key as well, in // Defense-in-depth: break the node's public key as well, in
// case something tries to communicate. // case something tries to communicate.

View File

@ -7381,15 +7381,7 @@ func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, prevSug
} }
distances := make([]nodeDistance, 0, len(candidates)) distances := make([]nodeDistance, 0, len(candidates))
for _, c := range candidates { for _, c := range candidates {
if c.DERP() != "" { if regionID := c.HomeDERP(); regionID != 0 {
ipp, err := netip.ParseAddrPort(c.DERP())
if err != nil {
continue
}
if ipp.Addr() != tailcfg.DerpMagicIPAddr {
continue
}
regionID := int(ipp.Port())
candidatesByRegion[regionID] = append(candidatesByRegion[regionID], c) candidatesByRegion[regionID] = append(candidatesByRegion[regionID], c)
continue continue
} }

View File

@ -1007,8 +1007,8 @@ func TestUpdateNetmapDelta(t *testing.T) {
wants := []*tailcfg.Node{ wants := []*tailcfg.Node{
{ {
ID: 1, ID: 1,
DERP: "127.3.3.40:1", HomeDERP: 1,
}, },
{ {
ID: 2, ID: 2,
@ -2021,7 +2021,7 @@ func TestAutoExitNodeSetNetInfoCallback(t *testing.T) {
netip.MustParsePrefix("100.64.1.1/32"), netip.MustParsePrefix("100.64.1.1/32"),
netip.MustParsePrefix("fe70::1/128"), netip.MustParsePrefix("fe70::1/128"),
}, },
DERP: "127.3.3.40:2", HomeDERP: 2,
} }
defaultDERPMap := &tailcfg.DERPMap{ defaultDERPMap := &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{ Regions: map[int]*tailcfg.DERPRegion{
@ -2985,7 +2985,7 @@ func makePeer(id tailcfg.NodeID, opts ...peerOptFunc) tailcfg.NodeView {
ID: id, ID: id,
StableID: tailcfg.StableNodeID(fmt.Sprintf("stable%d", id)), StableID: tailcfg.StableNodeID(fmt.Sprintf("stable%d", id)),
Name: fmt.Sprintf("peer%d", id), Name: fmt.Sprintf("peer%d", id),
DERP: fmt.Sprintf("127.3.3.40:%d", id), HomeDERP: int(id),
} }
for _, opt := range opts { for _, opt := range opts {
opt(node) opt(node)
@ -3001,13 +3001,13 @@ func withName(name string) peerOptFunc {
func withDERP(region int) peerOptFunc { func withDERP(region int) peerOptFunc {
return func(n *tailcfg.Node) { return func(n *tailcfg.Node) {
n.DERP = fmt.Sprintf("127.3.3.40:%d", region) n.HomeDERP = region
} }
} }
func withoutDERP() peerOptFunc { func withoutDERP() peerOptFunc {
return func(n *tailcfg.Node) { return func(n *tailcfg.Node) {
n.DERP = "" n.HomeDERP = 0
} }
} }

View File

@ -153,7 +153,8 @@ type CapabilityVersion int
// - 108: 2024-11-08: Client sends ServicesHash in Hostinfo, understands c2n GET /vip-services. // - 108: 2024-11-08: Client sends ServicesHash in Hostinfo, understands c2n GET /vip-services.
// - 109: 2024-11-18: Client supports filtertype.Match.SrcCaps (issue #12542) // - 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) // - 110: 2024-12-12: removed never-before-used Tailscale SSH public key support (#14373)
const CurrentCapabilityVersion CapabilityVersion = 110 // - 111: 2025-01-14: Client supports a peer having Node.HomeDERP (issue #14636)
const CurrentCapabilityVersion CapabilityVersion = 111
// ID is an integer ID for a user, node, or login allocated by the // ID is an integer ID for a user, node, or login allocated by the
// control plane. // control plane.
@ -346,15 +347,24 @@ type Node struct {
AllowedIPs []netip.Prefix // range of IP addresses to route to this node 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) Endpoints []netip.AddrPort `json:",omitempty"` // IP+port (public via STUN, and local LANs)
// DERP is this node's home DERP region ID integer, but shoved into an // 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" // IP:port string for legacy reasons. The IP address is always "127.3.3.40"
// (a loopback address (127) followed by the digits over the letters DERP on // (a loopback address (127) followed by the digits over the letters DERP on
// a QWERTY keyboard (3.3.40)). The "port number" is the home DERP region ID // a QWERTY keyboard (3.3.40)). The "port number" is the home LegacyDERPString region ID
// integer. // integer.
// //
// TODO(bradfitz): simplify this legacy mess; add a new HomeDERPRegionID int // Deprecated: HomeDERP has replaced this, but old servers might still send
// field behind a new capver bump. // this field. See tailscale/tailscale#14636. Do not use this field in code
DERP string `json:",omitempty"` // DERP-in-IP:port ("127.3.3.40:N") endpoint // other than in the upgradeNode func, which canonicalizes it to HomeDERP
// if it arrives as a LegacyDERPString string on the wire.
LegacyDERPString string `json:"DERP,omitempty"` // DERP-in-IP:port ("127.3.3.40:N") endpoint
// HomeDERP is the modern version of the DERP string field, with just an
// integer. The client advertises support for this as of capver 111.
//
// HomeDERP may be zero if not (yet) known, but ideally always be non-zero
// for magicsock connectivity to function normally.
HomeDERP int `json:",omitempty"` // DERP region ID of the node's home DERP
Hostinfo HostinfoView Hostinfo HostinfoView
Created time.Time Created time.Time
@ -2162,7 +2172,8 @@ func (n *Node) Equal(n2 *Node) bool {
slicesx.EqualSameNil(n.AllowedIPs, n2.AllowedIPs) && slicesx.EqualSameNil(n.AllowedIPs, n2.AllowedIPs) &&
slicesx.EqualSameNil(n.PrimaryRoutes, n2.PrimaryRoutes) && slicesx.EqualSameNil(n.PrimaryRoutes, n2.PrimaryRoutes) &&
slicesx.EqualSameNil(n.Endpoints, n2.Endpoints) && slicesx.EqualSameNil(n.Endpoints, n2.Endpoints) &&
n.DERP == n2.DERP && n.LegacyDERPString == n2.LegacyDERPString &&
n.HomeDERP == n2.HomeDERP &&
n.Cap == n2.Cap && n.Cap == n2.Cap &&
n.Hostinfo.Equal(n2.Hostinfo) && n.Hostinfo.Equal(n2.Hostinfo) &&
n.Created.Equal(n2.Created) && n.Created.Equal(n2.Created) &&

View File

@ -99,7 +99,8 @@ var _NodeCloneNeedsRegeneration = Node(struct {
Addresses []netip.Prefix Addresses []netip.Prefix
AllowedIPs []netip.Prefix AllowedIPs []netip.Prefix
Endpoints []netip.AddrPort Endpoints []netip.AddrPort
DERP string LegacyDERPString string
HomeDERP int
Hostinfo HostinfoView Hostinfo HostinfoView
Created time.Time Created time.Time
Cap CapabilityVersion Cap CapabilityVersion

View File

@ -367,7 +367,7 @@ func TestNodeEqual(t *testing.T) {
nodeHandles := []string{ nodeHandles := []string{
"ID", "StableID", "Name", "User", "Sharer", "ID", "StableID", "Name", "User", "Sharer",
"Key", "KeyExpiry", "KeySignature", "Machine", "DiscoKey", "Key", "KeyExpiry", "KeySignature", "Machine", "DiscoKey",
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo", "Addresses", "AllowedIPs", "Endpoints", "LegacyDERPString", "HomeDERP", "Hostinfo",
"Created", "Cap", "Tags", "PrimaryRoutes", "Created", "Cap", "Tags", "PrimaryRoutes",
"LastSeen", "Online", "MachineAuthorized", "LastSeen", "Online", "MachineAuthorized",
"Capabilities", "CapMap", "Capabilities", "CapMap",
@ -530,8 +530,13 @@ func TestNodeEqual(t *testing.T) {
true, true,
}, },
{ {
&Node{DERP: "foo"}, &Node{LegacyDERPString: "foo"},
&Node{DERP: "bar"}, &Node{LegacyDERPString: "bar"},
false,
},
{
&Node{HomeDERP: 1},
&Node{HomeDERP: 2},
false, false,
}, },
{ {

View File

@ -139,7 +139,8 @@ func (v NodeView) DiscoKey() key.DiscoPublic { return v.ж.DiscoK
func (v NodeView) Addresses() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.Addresses) } func (v NodeView) Addresses() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.Addresses) }
func (v NodeView) AllowedIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.AllowedIPs) } func (v NodeView) AllowedIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.AllowedIPs) }
func (v NodeView) Endpoints() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Endpoints) } func (v NodeView) Endpoints() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Endpoints) }
func (v NodeView) DERP() string { return v.ж.DERP } func (v NodeView) LegacyDERPString() string { return v.ж.LegacyDERPString }
func (v NodeView) HomeDERP() int { return v.ж.HomeDERP }
func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo } func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo }
func (v NodeView) Created() time.Time { return v.ж.Created } func (v NodeView) Created() time.Time { return v.ж.Created }
func (v NodeView) Cap() CapabilityVersion { return v.ж.Cap } func (v NodeView) Cap() CapabilityVersion { return v.ж.Cap }
@ -192,7 +193,8 @@ var _NodeViewNeedsRegeneration = Node(struct {
Addresses []netip.Prefix Addresses []netip.Prefix
AllowedIPs []netip.Prefix AllowedIPs []netip.Prefix
Endpoints []netip.AddrPort Endpoints []netip.AddrPort
DERP string LegacyDERPString string
HomeDERP int
Hostinfo HostinfoView Hostinfo HostinfoView
Created time.Time Created time.Time
Cap CapabilityVersion Cap CapabilityVersion

View File

@ -805,7 +805,7 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey key.Machi
node.Hostinfo = req.Hostinfo.View() node.Hostinfo = req.Hostinfo.View()
if ni := node.Hostinfo.NetInfo(); ni.Valid() { if ni := node.Hostinfo.NetInfo(); ni.Valid() {
if ni.PreferredDERP() != 0 { if ni.PreferredDERP() != 0 {
node.DERP = fmt.Sprintf("127.3.3.40:%d", ni.PreferredDERP()) node.HomeDERP = ni.PreferredDERP()
} }
} }
} }

View File

@ -287,11 +287,8 @@ func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) {
epStrs[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces)) epStrs[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces))
} }
derp := p.DERP() derp := fmt.Sprintf("D%d", p.HomeDERP())
const derpPrefix = "127.3.3.40:"
if strings.HasPrefix(derp, derpPrefix) {
derp = "D" + derp[len(derpPrefix):]
}
var discoShort string var discoShort string
if !p.DiscoKey().IsZero() { if !p.DiscoKey().IsZero() {
discoShort = p.DiscoKey().ShortString() + " " discoShort = p.DiscoKey().ShortString() + " "
@ -311,7 +308,7 @@ func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) {
// nodeConciseEqual reports whether a and b are equal for the fields accessed by printPeerConcise. // nodeConciseEqual reports whether a and b are equal for the fields accessed by printPeerConcise.
func nodeConciseEqual(a, b tailcfg.NodeView) bool { func nodeConciseEqual(a, b tailcfg.NodeView) bool {
return a.Key() == b.Key() && return a.Key() == b.Key() &&
a.DERP() == b.DERP() && a.HomeDERP() == b.HomeDERP() &&
a.DiscoKey() == b.DiscoKey() && a.DiscoKey() == b.DiscoKey() &&
views.SliceEqual(a.AllowedIPs(), b.AllowedIPs()) && views.SliceEqual(a.AllowedIPs(), b.AllowedIPs()) &&
views.SliceEqual(a.Endpoints(), b.Endpoints()) views.SliceEqual(a.Endpoints(), b.Endpoints())

View File

@ -63,12 +63,12 @@ func TestNetworkMapConcise(t *testing.T) {
Peers: nodeViews([]*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
{ {
Key: testNodeKey(3), Key: testNodeKey(3),
DERP: "127.3.3.40:4", HomeDERP: 4,
Endpoints: eps("10.2.0.100:12", "10.1.0.100:12345"), Endpoints: eps("10.2.0.100:12", "10.1.0.100:12345"),
}, },
}), }),
@ -102,7 +102,7 @@ func TestConciseDiffFrom(t *testing.T) {
Peers: nodeViews([]*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
}), }),
@ -112,7 +112,7 @@ func TestConciseDiffFrom(t *testing.T) {
Peers: nodeViews([]*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
}), }),
@ -126,7 +126,7 @@ func TestConciseDiffFrom(t *testing.T) {
Peers: nodeViews([]*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
}), }),
@ -136,7 +136,7 @@ func TestConciseDiffFrom(t *testing.T) {
Peers: nodeViews([]*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
}), }),
@ -151,7 +151,7 @@ func TestConciseDiffFrom(t *testing.T) {
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
}), }),
@ -162,19 +162,19 @@ func TestConciseDiffFrom(t *testing.T) {
{ {
ID: 1, ID: 1,
Key: testNodeKey(1), Key: testNodeKey(1),
DERP: "127.3.3.40:1", HomeDERP: 1,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
{ {
ID: 3, ID: 3,
Key: testNodeKey(3), Key: testNodeKey(3),
DERP: "127.3.3.40:3", HomeDERP: 3,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
}), }),
@ -189,19 +189,19 @@ func TestConciseDiffFrom(t *testing.T) {
{ {
ID: 1, ID: 1,
Key: testNodeKey(1), Key: testNodeKey(1),
DERP: "127.3.3.40:1", HomeDERP: 1,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
{ {
ID: 3, ID: 3,
Key: testNodeKey(3), Key: testNodeKey(3),
DERP: "127.3.3.40:3", HomeDERP: 3,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
}), }),
@ -212,7 +212,7 @@ func TestConciseDiffFrom(t *testing.T) {
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"), Endpoints: eps("192.168.0.100:12", "192.168.0.100:12354"),
}, },
}), }),
@ -227,7 +227,7 @@ func TestConciseDiffFrom(t *testing.T) {
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "1.1.1.1:1"), Endpoints: eps("192.168.0.100:12", "1.1.1.1:1"),
}, },
}), }),
@ -238,7 +238,7 @@ func TestConciseDiffFrom(t *testing.T) {
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:12", "1.1.1.1:2"), Endpoints: eps("192.168.0.100:12", "1.1.1.1:2"),
}, },
}), }),
@ -253,7 +253,7 @@ func TestConciseDiffFrom(t *testing.T) {
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:41641", "1.1.1.1:41641"), Endpoints: eps("192.168.0.100:41641", "1.1.1.1:41641"),
DiscoKey: testDiscoKey("f00f00f00f"), DiscoKey: testDiscoKey("f00f00f00f"),
AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)}, AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
@ -266,7 +266,7 @@ func TestConciseDiffFrom(t *testing.T) {
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", HomeDERP: 2,
Endpoints: eps("192.168.0.100:41641", "1.1.1.1:41641"), Endpoints: eps("192.168.0.100:41641", "1.1.1.1:41641"),
DiscoKey: testDiscoKey("ba4ba4ba4b"), DiscoKey: testDiscoKey("ba4ba4ba4b"),
AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)}, AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},

View File

@ -5,7 +5,6 @@ package netmap
import ( import (
"cmp" "cmp"
"fmt"
"net/netip" "net/netip"
"reflect" "reflect"
"slices" "slices"
@ -35,7 +34,7 @@ type NodeMutationDERPHome struct {
} }
func (m NodeMutationDERPHome) Apply(n *tailcfg.Node) { func (m NodeMutationDERPHome) Apply(n *tailcfg.Node) {
n.DERP = fmt.Sprintf("127.3.3.40:%v", m.DERPRegion) n.HomeDERP = m.DERPRegion
} }
// NodeMutation is a NodeMutation that says a node's endpoints have changed. // NodeMutation is a NodeMutation that says a node's endpoints have changed.

View File

@ -1359,7 +1359,7 @@ func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool, p
}) })
de.resetLocked() de.resetLocked()
} }
if n.DERP() == "" { if n.HomeDERP() == 0 {
if de.derpAddr.IsValid() { if de.derpAddr.IsValid() {
de.debugUpdates.Add(EndpointChange{ de.debugUpdates.Add(EndpointChange{
When: time.Now(), When: time.Now(),
@ -1369,7 +1369,7 @@ func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool, p
} }
de.derpAddr = netip.AddrPort{} de.derpAddr = netip.AddrPort{}
} else { } else {
newDerp, _ := netip.ParseAddrPort(n.DERP()) newDerp := netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, uint16(n.HomeDERP()))
if de.derpAddr != newDerp { if de.derpAddr != newDerp {
de.debugUpdates.Add(EndpointChange{ de.debugUpdates.Add(EndpointChange{
When: time.Now(), When: time.Now(),

View File

@ -2337,10 +2337,7 @@ func devPanicf(format string, a ...any) {
func (c *Conn) logEndpointCreated(n tailcfg.NodeView) { func (c *Conn) logEndpointCreated(n tailcfg.NodeView) {
c.logf("magicsock: created endpoint key=%s: disco=%s; %v", n.Key().ShortString(), n.DiscoKey().ShortString(), logger.ArgWriter(func(w *bufio.Writer) { c.logf("magicsock: created endpoint key=%s: disco=%s; %v", n.Key().ShortString(), n.DiscoKey().ShortString(), logger.ArgWriter(func(w *bufio.Writer) {
const derpPrefix = "127.3.3.40:" if regionID := n.HomeDERP(); regionID != 0 {
if strings.HasPrefix(n.DERP(), derpPrefix) {
ipp, _ := netip.ParseAddrPort(n.DERP())
regionID := int(ipp.Port())
code := c.derpRegionCodeLocked(regionID) code := c.derpRegionCodeLocked(regionID)
if code != "" { if code != "" {
code = "(" + code + ")" code = "(" + code + ")"

View File

@ -314,7 +314,7 @@ func meshStacks(logf logger.Logf, mutateNetmap func(idx int, nm *netmap.NetworkM
Addresses: addrs, Addresses: addrs,
AllowedIPs: addrs, AllowedIPs: addrs,
Endpoints: epFromTyped(eps[i]), Endpoints: epFromTyped(eps[i]),
DERP: "127.3.3.40:1", HomeDERP: 1,
} }
nm.Peers = append(nm.Peers, peer.View()) nm.Peers = append(nm.Peers, peer.View())
} }

View File

@ -198,7 +198,7 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
e.logf("open-conn-track: timeout opening %v; peer node %v running pre-0.100", flow, n.Key().ShortString()) e.logf("open-conn-track: timeout opening %v; peer node %v running pre-0.100", flow, n.Key().ShortString())
return return
} }
if n.DERP() == "" { if n.HomeDERP() == 0 {
e.logf("open-conn-track: timeout opening %v; peer node %v not connected to any DERP relay", flow, n.Key().ShortString()) e.logf("open-conn-track: timeout opening %v; peer node %v not connected to any DERP relay", flow, n.Key().ShortString())
return return
} }

View File

@ -85,7 +85,7 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags,
skippedSubnets := new(bytes.Buffer) skippedSubnets := new(bytes.Buffer)
for _, peer := range nm.Peers { for _, peer := range nm.Peers {
if peer.DiscoKey().IsZero() && peer.DERP() == "" && !peer.IsWireGuardOnly() { if peer.DiscoKey().IsZero() && peer.HomeDERP() == 0 && !peer.IsWireGuardOnly() {
// Peer predates both DERP and active discovery, we cannot // Peer predates both DERP and active discovery, we cannot
// communicate with it. // communicate with it.
logf("[v1] wgcfg: skipped peer %s, doesn't offer DERP or disco", peer.Key().ShortString()) logf("[v1] wgcfg: skipped peer %s, doesn't offer DERP or disco", peer.Key().ShortString())