diff --git a/cmd/tailscale/cli/network-lock.go b/cmd/tailscale/cli/network-lock.go index 648cc502d..8d2d5800e 100644 --- a/cmd/tailscale/cli/network-lock.go +++ b/cmd/tailscale/cli/network-lock.go @@ -213,6 +213,26 @@ func runNetworkLockStatus(ctx context.Context, args []string) error { } } + if st.Enabled && len(st.FilteredPeers) > 0 { + fmt.Println() + fmt.Println("The following peers are locked out by tailnet lock & do not have connectivity:") + for _, p := range st.FilteredPeers { + var line strings.Builder + line.WriteString("\t") + line.WriteString(p.Name) + line.WriteString("\t") + for i, addr := range p.TailscaleIPs { + line.WriteString(addr.String()) + if i < len(p.TailscaleIPs)-1 { + line.WriteString(", ") + } + } + line.WriteString("\t") + line.WriteString(string(p.StableID)) + fmt.Println(line.String()) + } + } + return nil } diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index e46312a6e..f4c4b8d19 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -14,6 +14,7 @@ import ( "fmt" "io" "net/http" + "net/netip" "os" "path/filepath" "time" @@ -21,6 +22,7 @@ import ( "tailscale.com/envknob" "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/tka" "tailscale.com/types/key" @@ -41,6 +43,7 @@ type tkaState struct { profile ipn.ProfileID authority *tka.Authority storage *tka.FS + filtered []ipnstate.TKAFilteredPeer } // tkaFilterNetmapLocked checks the signatures on each node key, dropping @@ -75,12 +78,30 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) { // nm.Peers is ordered, so deletion must be order-preserving. if len(toDelete) > 0 { peers := make([]*tailcfg.Node, 0, len(nm.Peers)) + filtered := make([]ipnstate.TKAFilteredPeer, 0, len(toDelete)) for i, p := range nm.Peers { if !toDelete[i] { peers = append(peers, p) + } else { + // Record information about the node we filtered out. + fp := ipnstate.TKAFilteredPeer{ + Name: p.Name, + ID: p.ID, + StableID: p.StableID, + TailscaleIPs: make([]netip.Addr, len(p.Addresses)), + } + for i, addr := range p.Addresses { + if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) { + fp.TailscaleIPs[i] = addr.Addr() + } + } + filtered = append(filtered, fp) } } nm.Peers = peers + b.tka.filtered = filtered + } else { + b.tka.filtered = nil } } @@ -399,6 +420,11 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus { } } + filtered := make([]*ipnstate.TKAFilteredPeer, len(b.tka.filtered)) + for i := 0; i < len(filtered); i++ { + filtered[i] = b.tka.filtered[i].Clone() + } + return &ipnstate.NetworkLockStatus{ Enabled: true, Head: &head, @@ -406,6 +432,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus { NodeKey: nodeKey, NodeKeySigned: selfAuthorized, TrustedKeys: outKeys, + FilteredPeers: filtered, } } diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 45399935c..54aafae90 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -24,6 +24,8 @@ import ( "tailscale.com/util/dnsname" ) +//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=TKAFilteredPeer + // Status represents the entire state of the IPN network. type Status struct { // Version is the daemon's long version (see version.Long). @@ -74,6 +76,16 @@ type TKAKey struct { Votes uint } +// TKAFilteredPeer describes a peer which was removed from the netmap +// (i.e. no connectivity) because it failed tailnet lock +// checks. +type TKAFilteredPeer struct { + Name string // DNS + ID tailcfg.NodeID + StableID tailcfg.StableNodeID + TailscaleIPs []netip.Addr // Tailscale IP(s) assigned to this node +} + // NetworkLockStatus represents whether network-lock is enabled, // along with details about the locally-known state of the tailnet // key authority. @@ -99,6 +111,11 @@ type NetworkLockStatus struct { // TrustedKeys describes the keys currently trusted to make changes // to network-lock. TrustedKeys []TKAKey + + // FilteredPeers describes peers which were removed from the netmap + // (i.e. no connectivity) because they failed tailnet lock + // checks. + FilteredPeers []*TKAFilteredPeer } // NetworkLockUpdate describes a change to network-lock state. diff --git a/ipn/ipnstate/ipnstate_clone.go b/ipn/ipnstate/ipnstate_clone.go new file mode 100644 index 000000000..52d8d6c7e --- /dev/null +++ b/ipn/ipnstate/ipnstate_clone.go @@ -0,0 +1,33 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT. + +package ipnstate + +import ( + "net/netip" + + "tailscale.com/tailcfg" +) + +// Clone makes a deep copy of TKAFilteredPeer. +// The result aliases no memory with the original. +func (src *TKAFilteredPeer) Clone() *TKAFilteredPeer { + if src == nil { + return nil + } + dst := new(TKAFilteredPeer) + *dst = *src + dst.TailscaleIPs = append(src.TailscaleIPs[:0:0], src.TailscaleIPs...) + return dst +} + +// A compilation failure here means this code must be regenerated, with the command at the top of this file. +var _TKAFilteredPeerCloneNeedsRegeneration = TKAFilteredPeer(struct { + Name string + ID tailcfg.NodeID + StableID tailcfg.StableNodeID + TailscaleIPs []netip.Addr +}{})