diff --git a/control/controlclient/map.go b/control/controlclient/map.go index a96876407..8f154728d 100644 --- a/control/controlclient/map.go +++ b/control/controlclient/map.go @@ -268,6 +268,24 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) { if ec.Endpoints != nil { n.Endpoints = ec.Endpoints } + if ec.Key != nil { + n.Key = *ec.Key + } + if ec.DiscoKey != nil { + n.DiscoKey = *ec.DiscoKey + } + if v := ec.Online; v != nil { + n.Online = ptrCopy(v) + } + if v := ec.LastSeen; v != nil { + n.LastSeen = ptrCopy(v) + } + if v := ec.KeyExpiry; v != nil { + n.KeyExpiry = *v + } + if v := ec.Capabilities; v != nil { + n.Capabilities = *v + } } } } @@ -277,6 +295,16 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) { mapRes.PeersRemoved = nil } +// ptrCopy returns a pointer to a newly allocated shallow copy of *v. +func ptrCopy[T any](v *T) *T { + if v == nil { + return nil + } + ret := new(T) + *ret = *v + return ret +} + func nodesSorted(v []*tailcfg.Node) bool { for i, n := range v { if i > 0 && n.ID <= v[i-1].ID { diff --git a/control/controlclient/map_test.go b/control/controlclient/map_test.go index a2d8566b0..3f94dd132 100644 --- a/control/controlclient/map_test.go +++ b/control/controlclient/map_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "go4.org/mem" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/netmap" @@ -192,7 +193,90 @@ func TestUndeltaPeers(t *testing.T) { }, want: peers(n(1, "foo", withDERP("127.3.3.40:2"), withEP("1.2.3.4:56"))), }, - } + { + name: "change_key", + prev: peers(n(1, "foo")), + mapRes: &tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{{ + NodeID: 1, + Key: ptrTo(key.NodePublicFromRaw32(mem.B(append(make([]byte, 31), 'A')))), + }}, + }, want: peers(&tailcfg.Node{ + ID: 1, + Name: "foo", + Key: key.NodePublicFromRaw32(mem.B(append(make([]byte, 31), 'A'))), + }), + }, + { + name: "change_disco_key", + prev: peers(n(1, "foo")), + mapRes: &tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{{ + NodeID: 1, + DiscoKey: ptrTo(key.DiscoPublicFromRaw32(mem.B(append(make([]byte, 31), 'A')))), + }}, + }, want: peers(&tailcfg.Node{ + ID: 1, + Name: "foo", + DiscoKey: key.DiscoPublicFromRaw32(mem.B(append(make([]byte, 31), 'A'))), + }), + }, + { + name: "change_online", + prev: peers(n(1, "foo")), + mapRes: &tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{{ + NodeID: 1, + Online: ptrTo(true), + }}, + }, want: peers(&tailcfg.Node{ + ID: 1, + Name: "foo", + Online: ptrTo(true), + }), + }, + { + name: "change_last_seen", + prev: peers(n(1, "foo")), + mapRes: &tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{{ + NodeID: 1, + LastSeen: ptrTo(time.Unix(123, 0).UTC()), + }}, + }, want: peers(&tailcfg.Node{ + ID: 1, + Name: "foo", + LastSeen: ptrTo(time.Unix(123, 0).UTC()), + }), + }, + { + name: "change_key_expiry", + prev: peers(n(1, "foo")), + mapRes: &tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{{ + NodeID: 1, + KeyExpiry: ptrTo(time.Unix(123, 0).UTC()), + }}, + }, want: peers(&tailcfg.Node{ + ID: 1, + Name: "foo", + KeyExpiry: time.Unix(123, 0).UTC(), + }), + }, + { + name: "change_capabilities", + prev: peers(n(1, "foo")), + mapRes: &tailcfg.MapResponse{ + PeersChangedPatch: []*tailcfg.PeerChange{{ + NodeID: 1, + Capabilities: ptrTo([]string{"foo"}), + }}, + }, want: peers(&tailcfg.Node{ + ID: 1, + Name: "foo", + Capabilities: []string{"foo"}, + }), + }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -207,6 +291,10 @@ func TestUndeltaPeers(t *testing.T) { } } +func ptrTo[T any](v T) *T { + return &v +} + func formatNodes(nodes []*tailcfg.Node) string { var sb strings.Builder for i, n := range nodes { diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 033edb980..04b1117a9 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -70,7 +70,8 @@ type CapabilityVersion int // 32: 2022-04-17: client knows FilterRule.CapMatch // 33: 2022-07-20: added MapResponse.PeersChangedPatch (DERPRegion + Endpoints) // 34: 2022-08-02: client understands CapabilityFileSharingTarget -const CurrentCapabilityVersion CapabilityVersion = 34 +// 36: 2022-08-02: added PeersChangedPatch.{Key,DiscoKey,Online,LastSeen,KeyExpiry,Capabilities} +const CurrentCapabilityVersion CapabilityVersion = 36 type StableID string @@ -1763,6 +1764,26 @@ type PeerChange struct { // Endpoints, if non-empty, means that NodeID's UDP Endpoints // have changed to these. Endpoints []string `json:",omitempty"` + + // Key, if non-nil, means that the NodeID's wireguard public key changed. + Key *key.NodePublic `json:",omitempty"` + + // DiscoKey, if non-nil, means that the NodeID's discokey changed. + DiscoKey *key.DiscoPublic `json:",omitempty"` + + // Online, if non-nil, means that the NodeID's online status changed. + Online *bool `json:",omitempty"` + + // LastSeen, if non-nil, means that the NodeID's online status changed. + LastSeen *time.Time `json:",omitempty"` + + // KeyExpiry, if non-nil, changes the NodeID's key expiry. + KeyExpiry *time.Time `json:",omitempty"` + + // Capabilities, if non-nil, means that the NodeID's capabilities changed. + // It's a pointer to a slice for "omitempty", to allow differentiating + // a change to empty from no change. + Capabilities *[]string `json:",omitempty"` } // DerpMagicIP is a fake WireGuard endpoint IP address that means to