Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
// Package netmap contains the netmap.NetworkMap type.package netmap
import ( "cmp" "encoding/json" "fmt" "net/netip" "sort" "strings" "time"
"tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/types/key" "tailscale.com/types/views" "tailscale.com/util/set" "tailscale.com/wgengine/filter/filtertype")
// NetworkMap is the current state of the world.//// The fields should all be considered read-only. They might// alias parts of previous NetworkMap values.type NetworkMap struct { Cached bool // whether this NetworkMap was loaded from disk cache (as opposed to live from network)
SelfNode tailcfg.NodeView AllCaps set.Set[nodecap.Cap] // set version of SelfNode.Capabilities + SelfNode.CapMap NodeKey key.NodePublic
MachineKey key.MachinePublic
Peers []tailcfg.NodeView // sorted by Node.ID DNS tailcfg.DNSConfig
PacketFilter []filtertype.Match PacketFilterRules views.Slice[tailcfg.FilterRule] SSHPolicy *tailcfg.SSHPolicy // or nil, if not enabled/allowed
// CollectServices reports whether this node's Tailnet has // requested that info about services be included in HostInfo. // If set, Hostinfo.ShieldsUp blocks services collection; that // takes precedence over this field. CollectServices bool
// DERPMap is the last DERP server map received. It's reused // between updates and should not be modified. DERPMap *tailcfg.DERPMap
// DisplayMessages are the list of health check problems for this // node from the perspective of the control plane. // If empty, there are no known problems from the control plane's // point of view, but the node might know about its own health // check problems. DisplayMessages map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage
// TKAEnabled indicates whether the tailnet key authority should be // enabled, from the perspective of the control plane. TKAEnabled bool // TKAHead indicates the control plane's understanding of 'head' (the // hash of the latest update message to tick through TKA). TKAHead tka.AUMHash
// Domain is the current Tailnet name. Domain string
// DomainAuditLogID is an audit log ID provided by control and // only populated if the domain opts into data-plane audit logging. // If this is empty, then data-plane audit logging is disabled. DomainAuditLogID string
// UserProfiles contains the profile information of UserIDs referenced // in SelfNode and Peers. UserProfiles map[tailcfg.UserID]tailcfg.UserProfileView}
// User returns nm.SelfNode.User if nm.SelfNode is non-nil, otherwise it returns// 0.func (nm *NetworkMap) User() tailcfg.UserID { if nm.SelfNode.Valid() { return nm.SelfNode.User() } return 0}
// GetAddresses returns the self node's addresses, or the zero value// if SelfNode is invalid.func (nm *NetworkMap) GetAddresses() views.Slice[netip.Prefix] { var zero views.Slice[netip.Prefix] if !nm.SelfNode.Valid() { return zero } return nm.SelfNode.Addresses()}
// GetVIPServiceIPMap returns a map of service names to the slice of// VIP addresses that correspond to the service. The service names are// with the prefix "svc:".//// TODO(tailscale/corp##25997): cache the result of decoding the capmap so that// we don't have to decode it multiple times after each netmap update.func (nm *NetworkMap) GetVIPServiceIPMap() tailcfg.ServiceIPMappings { if nm == nil { return nil } if !nm.SelfNode.Valid() { return nil }
ipMaps, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](nm.SelfNode.CapMap(), nodecap.ServiceHost) if len(ipMaps) != 1 || err != nil { return nil }
return ipMaps[0]}
// GetIPVIPServiceMap returns a map of VIP addresses to the service// names that has the VIP address. The service names are with the// prefix "svc:".func (nm *NetworkMap) GetIPVIPServiceMap() IPServiceMappings { var res IPServiceMappings if nm == nil { return res }
if !nm.SelfNode.Valid() { return res }
serviceIPMap := nm.GetVIPServiceIPMap() if serviceIPMap == nil { return res } res = make(IPServiceMappings) for svc, addrs := range serviceIPMap { for _, addr := range addrs { res[addr] = svc } } return res}
// Services returns the Services visible (accessible) to this node,// decoded from [tailcfg.NodeAttrPrefixServices] entries in the self node's// CapMap. The returned map is keyed by [tailcfg.ServiceDetails.Name],// which is the canonical service name. It returns nil if nm is nil// or SelfNode is invalid.//// TODO(adrianosela): cache the result of decoding the capmap so// we don't have to decode it multiple times after each netmap update.func (nm *NetworkMap) Services() map[tailcfg.ServiceName]tailcfg.ServiceDetails { if nm == nil || !nm.SelfNode.Valid() { return nil } result := make(map[tailcfg.ServiceName]tailcfg.ServiceDetails) for cap := range nm.SelfNode.CapMap().All() { if !strings.HasPrefix(string(cap), string(nodecap.ServicesPrefix)) { continue } svcs, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceDetails](nm.SelfNode.CapMap(), cap) if err != nil || len(svcs) < 1 { continue } // NOTE(adrianosela): the NodeCapMap key suffix is opaque and MUST not // be parsed or relied upon (so we extract name from the inner field). result[svcs[0].Name] = svcs[0] } return result}
// SelfNodeOrZero returns the self node, or a zero value if nm is nil.func (nm *NetworkMap) SelfNodeOrZero() tailcfg.NodeView { if nm == nil { return tailcfg.NodeView{} } return nm.SelfNode}
// AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes.func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool { for _, p := range nm.Peers { // NOTE: (ChaosInTheCRD) if the peer being advertised is a tailscale ip, we ignore it in this check for _, r := range p.PrimaryRoutes().All() { if !tsaddr.IsTailscaleIP(r.Addr()) || !r.IsSingleIP() { return true } } } return false}
// GetMachineStatus returns the MachineStatus of the local node.func (nm *NetworkMap) GetMachineStatus() tailcfg.MachineStatus { if !nm.SelfNode.Valid() { return tailcfg.MachineUnknown } if nm.SelfNode.MachineAuthorized() { return tailcfg.MachineAuthorized } return tailcfg.MachineUnauthorized}
// HasCap reports whether nm is non-nil and nm.AllCaps contains c.func (nm *NetworkMap) HasCap(c nodecap.Cap) bool { return nm != nil && nm.AllCaps.Contains(c)}
// PeerByTailscaleIP returns a peer's Node based on its Tailscale IP.//// If nm is nil or no peer is found, ok is false.func (nm *NetworkMap) PeerByTailscaleIP(ip netip.Addr) (peer tailcfg.NodeView, ok bool) { // TODO(bradfitz): if nm == nil { return tailcfg.NodeView{}, false } for _, n := range nm.Peers { ad := n.Addresses() for i := range ad.Len() { a := ad.At(i) if a.Addr() == ip { return n, true } } } return tailcfg.NodeView{}, false}
// PeerIndexByNodeID returns the index of the peer with the given nodeID// in nm.Peers, or -1 if nm is nil or not found.//// It assumes nm.Peers is sorted by Node.ID.func (nm *NetworkMap) PeerIndexByNodeID(nodeID tailcfg.NodeID) int { if nm == nil { return -1 } idx, ok := sort.Find(len(nm.Peers), func(i int) int { return cmp.Compare(nodeID, nm.Peers[i].ID()) }) if !ok { return -1 } return idx}
// MagicDNSSuffix returns the domain's MagicDNS suffix (even if MagicDNS isn't// necessarily in use) of the provided Node.Name value.//// It will neither start nor end with a period.func MagicDNSSuffixOfNodeName(nodeName string) string { name := strings.Trim(nodeName, ".") if _, rest, ok := strings.Cut(name, "."); ok { return rest } return name}
// MagicDNSSuffix returns the domain's MagicDNS suffix (even if// MagicDNS isn't necessarily in use).//// It will neither start nor end with a period.func (nm *NetworkMap) MagicDNSSuffix() string { return MagicDNSSuffixOfNodeName(nm.SelfName())}
// SelfName returns nm.SelfNode.Name, or the empty string// if nm is nil or nm.SelfNode is invalid.func (nm *NetworkMap) SelfName() string { if nm == nil || !nm.SelfNode.Valid() { return "" } return nm.SelfNode.Name()}
// SelfKeyExpiry returns nm.SelfNode.KeyExpiry, or the zero// value if nil or nm.SelfNode is invalid.func (nm *NetworkMap) SelfKeyExpiry() time.Time { if nm == nil || !nm.SelfNode.Valid() { return time.Time{} } return nm.SelfNode.KeyExpiry()}
// DomainName returns the name of the NetworkMap's// current tailnet. If the map is nil, it returns// an empty string.func (nm *NetworkMap) DomainName() string { if nm == nil { return "" } return nm.Domain}
// StableTailnetID returns the stable ID of the tailnet the current node is a// member of, as sent by control on the self node. It returns the empty string// if nm is nil or nm.SelfNode is invalid.func (nm *NetworkMap) StableTailnetID() tailcfg.StableTailnetID { if nm == nil || !nm.SelfNode.Valid() { return "" } return nm.SelfNode.StableTailnetID()}
// TailnetDisplayName returns the admin-editable name contained in// NodeAttrTailnetDisplayName. If the capability is not present it// returns an empty string.func (nm *NetworkMap) TailnetDisplayName() string { if nm == nil || !nm.SelfNode.Valid() { return "" }
tailnetDisplayNames, err := tailcfg.UnmarshalNodeCapViewJSON[string](nm.SelfNode.CapMap(), nodecap.TailnetDisplayName) if err != nil || len(tailnetDisplayNames) == 0 { return "" }
return tailnetDisplayNames[0]}
func (nm *NetworkMap) String() string { return nm.Concise()}
func (nm *NetworkMap) Concise() string { buf := new(strings.Builder)
nm.printConciseHeader(buf) for _, p := range nm.Peers { printPeerConcise(buf, p) } return buf.String()}
func (nm *NetworkMap) VeryConcise() string { buf := new(strings.Builder) nm.printConciseHeader(buf) return buf.String()}
// PeerWithStableID finds and returns the peer associated to the inputted StableNodeID.func (nm *NetworkMap) PeerWithStableID(pid tailcfg.StableNodeID) (_ tailcfg.NodeView, ok bool) { for _, p := range nm.Peers { if p.StableID() == pid { return p, true } } return tailcfg.NodeView{}, false}
// printConciseHeader prints a concise header line representing nm to buf.//// If this function is changed to access different fields of nm, keep// in equalConciseHeader in sync.func (nm *NetworkMap) printConciseHeader(buf *strings.Builder) { fmt.Fprintf(buf, "netmap: self: %v auth=%v", nm.NodeKey.ShortString(), nm.GetMachineStatus())
var login string up, ok := nm.UserProfiles[nm.User()] if ok { login = up.LoginName() } if login == "" { if nm.User().IsZero() { login = "?" } else { login = fmt.Sprint(nm.User()) } } fmt.Fprintf(buf, " u=%s", login) fmt.Fprintf(buf, " %v", nm.GetAddresses().AsSlice()) buf.WriteByte('\n')}
// equalConciseHeader reports whether a and b are equal for the fields// used by printConciseHeader.func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool { return a.NodeKey == b.NodeKey && a.GetMachineStatus() == b.GetMachineStatus() && a.User() == b.User() && views.SliceEqual(a.GetAddresses(), b.GetAddresses())}
// printPeerConcise appends to buf a line representing the peer p.//// If this function is changed to access different fields of p, keep// in nodeConciseEqual in sync.func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) { aip := make([]string, p.AllowedIPs().Len()) for i, a := range p.AllowedIPs().All() { s := strings.TrimSuffix(a.String(), "/32") aip[i] = s }
epStrs := make([]string, p.Endpoints().Len()) for i, ep := range p.Endpoints().All() { e := ep.String() // Align vertically on the ':' between IP and port colon := strings.IndexByte(e, ':') spaces := 0 for colon > 0 && len(e)+spaces-colon < 6 { spaces++ colon-- } epStrs[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces)) }
derp := fmt.Sprintf("D%d", p.HomeDERP())
var discoShort string if !p.DiscoKey().IsZero() { discoShort = p.DiscoKey().ShortString() + " " }
// Most of the time, aip is just one element, so format the // table to look good in that case. This will also make multi- // subnet nodes stand out visually. fmt.Fprintf(buf, " %v %s%-2v %-15v : %v\n", p.Key().ShortString(), discoShort, derp, strings.Join(aip, " "), strings.Join(epStrs, " "))}
// 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.HomeDERP() == b.HomeDERP() && a.DiscoKey() == b.DiscoKey() && views.SliceEqual(a.AllowedIPs(), b.AllowedIPs()) && views.SliceEqual(a.Endpoints(), b.Endpoints())}
func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string { var diff strings.Builder
// See if header (non-peers, "bare") part of the network map changed. // If so, print its diff lines first. if !a.equalConciseHeader(b) { diff.WriteByte('-') a.printConciseHeader(&diff) diff.WriteByte('+') b.printConciseHeader(&diff) }
aps, bps := a.Peers, b.Peers for len(aps) > 0 && len(bps) > 0 { pa, pb := aps[0], bps[0] switch { case pa.ID() == pb.ID(): if !nodeConciseEqual(pa, pb) { diff.WriteByte('-') printPeerConcise(&diff, pa) diff.WriteByte('+') printPeerConcise(&diff, pb) } aps, bps = aps[1:], bps[1:] case pa.ID() > pb.ID(): // New peer in b. diff.WriteByte('+') printPeerConcise(&diff, pb) bps = bps[1:] case pb.ID() > pa.ID(): // Deleted peer in b. diff.WriteByte('-') printPeerConcise(&diff, pa) aps = aps[1:] } } for _, pa := range aps { diff.WriteByte('-') printPeerConcise(&diff, pa) } for _, pb := range bps { diff.WriteByte('+') printPeerConcise(&diff, pb) } return diff.String()}
func (nm *NetworkMap) JSON() string { b, err := json.MarshalIndent(*nm, "", " ") if err != nil { return fmt.Sprintf("[json error: %v]", err) } return string(b)}
// WGConfigFlags is a bitmask of flags to control the behavior of the// wireguard configuration generation done by NetMap.WGCfg.type WGConfigFlags int
const ( _ WGConfigFlags = 1 << iota AllowSubnetRoutes)
// IPServiceMappings maps IP addresses to service names. This is the inverse of// [tailcfg.ServiceIPMappings], and is used to inform track which service a VIP// is associated with. This is set to b.ipVIPServiceMap every time the netmap is// updated. This is used to reduce the cost for looking up the service name for// the dst IP address in the netStack packet processing workflow.//// This is of the form://// {// "100.65.32.1": "svc:samba",// "fd7a:115c:a1e0::1234": "svc:samba",// "100.102.42.3": "svc:web",// "fd7a:115c:a1e0::abcd": "svc:web",// }type IPServiceMappings map[netip.Addr]tailcfg.ServiceName