Просмотр исходного кода

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 <[email protected]>
Brad Fitzpatrick 1 год назад
Родитель
Сommit
2fc4455e6d

+ 1 - 1
control/controlbase/conn_test.go

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

+ 36 - 13
control/controlclient/map.go

@@ -7,7 +7,6 @@ import (
 	"cmp"
 	"context"
 	"encoding/json"
-	"fmt"
 	"maps"
 	"net"
 	"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.
 	if resp.Node != nil {
+		upgradeNode(resp.Node)
 		if DevKnob.StripCaps() {
 			resp.Node.Capabilities = nil
 			resp.Node.CapMap = nil
@@ -181,6 +181,13 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
 		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.
 	initDisplayNames(cmp.Or(resp.Node.View(), ms.lastNode), resp)
 
@@ -216,6 +223,26 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
 	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 {
 	if ms.controlKnobs != nil && ms.controlKnobs.DisableDeltaUpdates.Load() {
 		return false
@@ -443,7 +470,7 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
 		stats.changed++
 		mut := vp.AsStruct()
 		if pc.DERPRegion != 0 {
-			mut.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, pc.DERPRegion)
+			mut.HomeDERP = pc.DERPRegion
 			patchDERPRegion.Add(1)
 		}
 		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)) {
 				pc().Endpoints = slices.Clone(n.Endpoints)
 			}
-		case "DERP":
-			if was.DERP() != n.DERP {
-				ip, portStr, err := net.SplitHostPort(n.DERP)
-				if err != nil || ip != "127.3.3.40" {
-					return nil, false
-				}
-				port, err := strconv.Atoi(portStr)
-				if err != nil || port < 1 || port > 65535 {
-					return nil, false
-				}
-				pc().DERPRegion = port
+		case "LegacyDERPString":
+			if was.LegacyDERPString() != "" || n.LegacyDERPString != "" {
+				panic("unexpected; caller should've already called upgradeNode")
+			}
+		case "HomeDERP":
+			if was.HomeDERP() != n.HomeDERP {
+				pc().DERPRegion = n.HomeDERP
 			}
 		case "Hostinfo":
 			if !was.Hostinfo().Valid() && !n.Hostinfo.Valid() {

+ 65 - 18
control/controlclient/map_test.go

@@ -50,9 +50,9 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
 			n.LastSeen = &t
 		}
 	}
-	withDERP := func(d string) func(*tailcfg.Node) {
+	withDERP := func(regionID int) func(*tailcfg.Node) {
 		return func(n *tailcfg.Node) {
-			n.DERP = d
+			n.HomeDERP = regionID
 		}
 	}
 	withEP := func(ep string) func(*tailcfg.Node) {
@@ -189,14 +189,14 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
 		},
 		{
 			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{
 				PeersChangedPatch: []*tailcfg.PeerChange{{
 					NodeID:     1,
 					DERPRegion: 4,
 				}},
 			},
-			want:      peers(n(1, "foo", withDERP("127.3.3.40:4"))),
+			want:      peers(n(1, "foo", withDERP(4))),
 			wantStats: updateStats{changed: 1},
 		},
 		{
@@ -213,19 +213,19 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
 		},
 		{
 			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{
 				PeersChangedPatch: []*tailcfg.PeerChange{{
 					NodeID:    1,
 					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},
 		},
 		{
 			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{
 				PeersChangedPatch: []*tailcfg.PeerChange{{
 					NodeID:     1,
@@ -233,7 +233,7 @@ func TestUpdatePeersStateFromResponse(t *testing.T) {
 					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},
 		},
 		{
@@ -744,8 +744,8 @@ func TestPeerChangeDiff(t *testing.T) {
 		},
 		{
 			name: "patch-derp",
-			a:    &tailcfg.Node{ID: 1, DERP: "127.3.3.40:1"},
-			b:    &tailcfg.Node{ID: 1, DERP: "127.3.3.40:2"},
+			a:    &tailcfg.Node{ID: 1, HomeDERP: 1},
+			b:    &tailcfg.Node{ID: 1, HomeDERP: 2},
 			want: &tailcfg.PeerChange{NodeID: 1, DERPRegion: 2},
 		},
 		{
@@ -929,23 +929,23 @@ func TestPatchifyPeersChanged(t *testing.T) {
 			mr0: &tailcfg.MapResponse{
 				Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
 				Peers: []*tailcfg.Node{
-					{ID: 1, DERP: "127.3.3.40:1", Hostinfo: hi},
-					{ID: 2, DERP: "127.3.3.40:2", Hostinfo: hi},
-					{ID: 3, DERP: "127.3.3.40:3", Hostinfo: hi},
+					{ID: 1, HomeDERP: 1, Hostinfo: hi},
+					{ID: 2, HomeDERP: 2, Hostinfo: hi},
+					{ID: 3, HomeDERP: 3, Hostinfo: hi},
 				},
 			},
 			mr1: &tailcfg.MapResponse{
 				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: 3, DERP: "127.3.3.40:33", Hostinfo: hi},
-					{ID: 4, DERP: "127.3.3.40:4", Hostinfo: hi},
+					{ID: 3, HomeDERP: 33, Hostinfo: hi},
+					{ID: 4, HomeDERP: 4, Hostinfo: hi},
 				},
 			},
 			want: &tailcfg.MapResponse{
 				PeersChanged: []*tailcfg.Node{
 					{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{
 					{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) {
 	for _, size := range []int{10, 100, 1_000, 10_000} {
 		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{
 					ID:         tailcfg.NodeID(i + 2),
 					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")},
 					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"),

+ 1 - 1
ipn/ipnlocal/expiry.go

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

+ 1 - 9
ipn/ipnlocal/local.go

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

+ 6 - 6
ipn/ipnlocal/local_test.go

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

+ 18 - 7
tailcfg/tailcfg.go

@@ -153,7 +153,8 @@ type CapabilityVersion int
 //   - 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)
 //   - 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
 // control plane.
@@ -346,15 +347,24 @@ type Node struct {
 	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)
 
-	// 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"
 	// (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.
 	//
-	// TODO(bradfitz): simplify this legacy mess; add a new HomeDERPRegionID int
-	// field behind a new capver bump.
-	DERP string `json:",omitempty"` // DERP-in-IP:port ("127.3.3.40:N") endpoint
+	// Deprecated: HomeDERP has replaced this, but old servers might still send
+	// this field. See tailscale/tailscale#14636. Do not use this field in code
+	// 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
 	Created  time.Time
@@ -2162,7 +2172,8 @@ func (n *Node) Equal(n2 *Node) bool {
 		slicesx.EqualSameNil(n.AllowedIPs, n2.AllowedIPs) &&
 		slicesx.EqualSameNil(n.PrimaryRoutes, n2.PrimaryRoutes) &&
 		slicesx.EqualSameNil(n.Endpoints, n2.Endpoints) &&
-		n.DERP == n2.DERP &&
+		n.LegacyDERPString == n2.LegacyDERPString &&
+		n.HomeDERP == n2.HomeDERP &&
 		n.Cap == n2.Cap &&
 		n.Hostinfo.Equal(n2.Hostinfo) &&
 		n.Created.Equal(n2.Created) &&

+ 2 - 1
tailcfg/tailcfg_clone.go

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

+ 8 - 3
tailcfg/tailcfg_test.go

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

+ 4 - 2
tailcfg/tailcfg_view.go

@@ -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) 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) 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) Created() time.Time                       { return v.ж.Created }
 func (v NodeView) Cap() CapabilityVersion                   { return v.ж.Cap }
@@ -192,7 +193,8 @@ var _NodeViewNeedsRegeneration = Node(struct {
 	Addresses                     []netip.Prefix
 	AllowedIPs                    []netip.Prefix
 	Endpoints                     []netip.AddrPort
-	DERP                          string
+	LegacyDERPString              string
+	HomeDERP                      int
 	Hostinfo                      HostinfoView
 	Created                       time.Time
 	Cap                           CapabilityVersion

+ 1 - 1
tstest/integration/testcontrol/testcontrol.go

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

+ 3 - 6
types/netmap/netmap.go

@@ -287,11 +287,8 @@ func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) {
 		epStrs[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces))
 	}
 
-	derp := p.DERP()
-	const derpPrefix = "127.3.3.40:"
-	if strings.HasPrefix(derp, derpPrefix) {
-		derp = "D" + derp[len(derpPrefix):]
-	}
+	derp := fmt.Sprintf("D%d", p.HomeDERP())
+
 	var discoShort string
 	if !p.DiscoKey().IsZero() {
 		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.
 func nodeConciseEqual(a, b tailcfg.NodeView) bool {
 	return a.Key() == b.Key() &&
-		a.DERP() == b.DERP() &&
+		a.HomeDERP() == b.HomeDERP() &&
 		a.DiscoKey() == b.DiscoKey() &&
 		views.SliceEqual(a.AllowedIPs(), b.AllowedIPs()) &&
 		views.SliceEqual(a.Endpoints(), b.Endpoints())

+ 18 - 18
types/netmap/netmap_test.go

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

+ 1 - 2
types/netmap/nodemut.go

@@ -5,7 +5,6 @@ package netmap
 
 import (
 	"cmp"
-	"fmt"
 	"net/netip"
 	"reflect"
 	"slices"
@@ -35,7 +34,7 @@ type NodeMutationDERPHome struct {
 }
 
 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.

+ 2 - 2
wgengine/magicsock/endpoint.go

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

+ 1 - 4
wgengine/magicsock/magicsock.go

@@ -2337,10 +2337,7 @@ func devPanicf(format string, a ...any) {
 
 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) {
-		const derpPrefix = "127.3.3.40:"
-		if strings.HasPrefix(n.DERP(), derpPrefix) {
-			ipp, _ := netip.ParseAddrPort(n.DERP())
-			regionID := int(ipp.Port())
+		if regionID := n.HomeDERP(); regionID != 0 {
 			code := c.derpRegionCodeLocked(regionID)
 			if code != "" {
 				code = "(" + code + ")"

+ 1 - 1
wgengine/magicsock/magicsock_test.go

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

+ 1 - 1
wgengine/pendopen.go

@@ -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())
 			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())
 			return
 		}

+ 1 - 1
wgengine/wgcfg/nmcfg/nmcfg.go

@@ -85,7 +85,7 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags,
 	skippedSubnets := new(bytes.Buffer)
 
 	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
 			// communicate with it.
 			logf("[v1] wgcfg: skipped peer %s, doesn't offer DERP or disco", peer.Key().ShortString())