diff --git a/control/controlclient/map.go b/control/controlclient/map.go index d436064ad..7533e6616 100644 --- a/control/controlclient/map.go +++ b/control/controlclient/map.go @@ -5,6 +5,7 @@ package controlclient import ( + "fmt" "log" "sort" @@ -238,7 +239,7 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) { sortNodes(newFull) } - if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 { + if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 || len(mapRes.PeersChangedPatch) != 0 { peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull)) for _, n := range newFull { peerByID[n.ID] = n @@ -259,6 +260,16 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) { n.Online = &online } } + for _, ec := range mapRes.PeersChangedPatch { + if n, ok := peerByID[ec.NodeID]; ok { + if ec.DERPRegion != 0 { + n.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, ec.DERPRegion) + } + if ec.Endpoints != nil { + n.Endpoints = ec.Endpoints + } + } + } } mapRes.Peers = newFull diff --git a/control/controlclient/map_test.go b/control/controlclient/map_test.go index a2e4ec55c..a2d8566b0 100644 --- a/control/controlclient/map_test.go +++ b/control/controlclient/map_test.go @@ -34,6 +34,16 @@ func TestUndeltaPeers(t *testing.T) { n.LastSeen = &t } } + withDERP := func(d string) func(*tailcfg.Node) { + return func(n *tailcfg.Node) { + n.DERP = d + } + } + withEP := func(ep string) func(*tailcfg.Node) { + return func(n *tailcfg.Node) { + n.Endpoints = []string{ep} + } + } n := func(id tailcfg.NodeID, name string, mod ...func(*tailcfg.Node)) *tailcfg.Node { n := &tailcfg.Node{ID: id, Name: name} for _, f := range mod { @@ -137,7 +147,53 @@ func TestUndeltaPeers(t *testing.T) { n(2, "bar", seenAt(time.Unix(123, 0))), ), }, + { + name: "ep_change_derp", + prev: peers(n(1, "foo", withDERP("127.3.3.40:3"))), + mapRes: &tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{{ + NodeID: 1, + DERPRegion: 4, + }}, + }, + want: peers(n(1, "foo", withDERP("127.3.3.40:4"))), + }, + { + name: "ep_change_udp", + prev: peers(n(1, "foo", withEP("1.2.3.4:111"))), + mapRes: &tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{{ + NodeID: 1, + Endpoints: []string{"1.2.3.4:56"}, + }}, + }, + want: peers(n(1, "foo", withEP("1.2.3.4:56"))), + }, + { + name: "ep_change_udp", + prev: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:111"))), + mapRes: &tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{{ + NodeID: 1, + Endpoints: []string{"1.2.3.4:56"}, + }}, + }, + want: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:56"))), + }, + { + name: "ep_change_both", + prev: peers(n(1, "foo", withDERP("127.3.3.40:3"), withEP("1.2.3.4:111"))), + mapRes: &tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{{ + NodeID: 1, + DERPRegion: 2, + Endpoints: []string{"1.2.3.4:56"}, + }}, + }, + want: peers(n(1, "foo", withDERP("127.3.3.40:2"), withEP("1.2.3.4:56"))), + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if !tt.curTime.IsZero() { diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 18015155e..78cddf423 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -67,7 +67,8 @@ type CapabilityVersion int // 30: 2022-03-22: client can request id tokens. // 31: 2022-04-15: PingRequest & PingResponse TSMP & disco support // 32: 2022-04-17: client knows FilterRule.CapMatch -const CurrentCapabilityVersion CapabilityVersion = 32 +// 33: 2022-07-20: added MapResponse.PeersChangedPatch (DERPRegion + Endpoints) +const CurrentCapabilityVersion CapabilityVersion = 33 type StableID string @@ -1237,6 +1238,15 @@ type MapResponse struct { // PeersRemoved are the NodeIDs that are no longer in the peer list. PeersRemoved []NodeID `json:",omitempty"` + // PeersChangedPatch, if non-nil, means that node(s) have changed. + // This is a lighter version of the older PeersChanged support that + // only supports certain types of updates + // + // These are applied after Peers* above, but in practice the + // control server should only send these on their own, without + // the Peers* fields also set. + PeersChangedPatch []*PeerChange `json:",omitempty"` + // PeerSeenChange contains information on how to update peers' LastSeen // times. If the value is false, the peer is gone. If the value is true, // the LastSeen time is now. Absent means unchanged. @@ -1724,3 +1734,27 @@ type TokenResponse struct { // `uid` | user ID, if not tagged IDToken string `json:"id_token"` } + +// PeerChange is an update to a node. +type PeerChange struct { + // NodeID is the node ID being mutated. If the NodeID is not + // known in the current netmap, this update should be + // ignored. (But the server will try not to send such useless + // updates.) + NodeID NodeID + + // DERPRegion, if non-zero, means that NodeID's home DERP + // region ID is now this number. + DERPRegion int `json:",omitempty"` + + // Endpoints, if non-empty, means that NodeID's UDP Endpoints + // have changed to these. + Endpoints []string `json:",omitempty"` +} + +// DerpMagicIP is a fake WireGuard endpoint IP address that means to +// use DERP. When used (in the Node.DERP field), the port number of +// the WireGuard endpoint is the DERP region ID number to use. +// +// Mnemonic: 3.3.40 are numbers above the keys D, E, R, P. +const DerpMagicIP = "127.3.3.40" diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index b4378e7c0..a1ad3b017 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -442,14 +442,7 @@ func (c *Conn) addDerpPeerRoute(peer key.NodePublic, derpID int, dc *derphttp.Cl mak.Set(&c.derpRoute, peer, derpRoute{derpID, dc}) } -// DerpMagicIP is a fake WireGuard endpoint IP address that means -// to use DERP. When used, the port number of the WireGuard endpoint -// is the DERP server number to use. -// -// Mnemonic: 3.3.40 are numbers above the keys D, E, R, P. -const DerpMagicIP = "127.3.3.40" - -var derpMagicIPAddr = netaddr.MustParseIP(DerpMagicIP) +var derpMagicIPAddr = netaddr.MustParseIP(tailcfg.DerpMagicIP) // activeDerp contains fields for an active DERP connection. type activeDerp struct {