diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index b954afffcb047..bd4ab36431fe0 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -894,6 +894,14 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) { ExitNode: p.StableID() != "" && p.StableID() == exitNodeID, SSH_HostKeys: p.Hostinfo().SSH_HostKeys().AsSlice(), Location: p.Hostinfo().Location(), + Capabilities: p.Capabilities().AsSlice(), + } + if cm := p.CapMap(); cm.Len() > 0 { + ps.CapMap = make(tailcfg.NodeCapMap, p.CapMap().Len()) + cm.Range(func(k tailcfg.NodeCapability, v views.Slice[tailcfg.RawMessage]) bool { + ps.CapMap[k] = v.AsSlice() + return true + }) } peerStatusFromNode(ps, p) diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 2713fff7d0867..584c6eef4cfac 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -736,6 +736,89 @@ func TestStatusWithoutPeers(t *testing.T) { } } +func TestStatusPeerCapabilities(t *testing.T) { + tests := []struct { + name string + peers []tailcfg.NodeView + populated bool + }{ + { + name: "peers-with-capabilities", + peers: []tailcfg.NodeView{ + (&tailcfg.Node{ + ID: 1, + StableID: "foo", + IsWireGuardOnly: true, + Hostinfo: (&tailcfg.Hostinfo{}).View(), + Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilitySSH}, + CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ + tailcfg.CapabilitySSH: nil, + }), + }).View(), + (&tailcfg.Node{ + ID: 2, + StableID: "bar", + Hostinfo: (&tailcfg.Hostinfo{}).View(), + Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilityAdmin}, + CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ + tailcfg.CapabilityAdmin: []tailcfg.RawMessage{`{"test": "true}`}, + }), + }).View(), + }, + populated: true, + }, + { + name: "peers-without-capabilities", + peers: []tailcfg.NodeView{ + (&tailcfg.Node{ + ID: 1, + StableID: "foo", + IsWireGuardOnly: true, + Hostinfo: (&tailcfg.Hostinfo{}).View(), + }).View(), + (&tailcfg.Node{ + ID: 2, + StableID: "bar", + Hostinfo: (&tailcfg.Hostinfo{}).View(), + }).View(), + }, + populated: false, + }, + } + b := newTestLocalBackend(t) + + var cc *mockControl + b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) { + cc = newClient(t, opts) + + t.Logf("ccGen: new mockControl.") + cc.called("New") + return cc, nil + }) + b.Start(ipn.Options{}) + b.Login(nil) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b.setNetMapLocked(&netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + MachineAuthorized: true, + Addresses: ipps("100.101.101.101"), + }).View(), + Peers: tt.peers, + }) + got := b.Status() + for _, peer := range got.Peer { + if peer.Capabilities == nil == tt.populated { + t.Errorf("expected populated Capabilities") + } + if peer.CapMap == nil == tt.populated { + t.Errorf("expected populated CapMap") + } + } + }) + } +} + // legacyBackend was the interface between Tailscale frontends // (e.g. cmd/tailscale, iOS/MacOS/Windows GUIs) and the tailscale // backend (e.g. cmd/tailscaled) running on the same machine. diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index c9ad0d0dae943..d86bc1d26387e 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -496,6 +496,12 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) { if t := st.KeyExpiry; t != nil { e.KeyExpiry = ptr.To(*t) } + if v := st.CapMap; v != nil { + e.CapMap = v + } + if v := st.Capabilities; v != nil { + e.Capabilities = v + } e.Location = st.Location }