diff --git a/cmd/tsconnect/wasm/wasm_js.go b/cmd/tsconnect/wasm/wasm_js.go index e8d6b665c..19d7f0c38 100644 --- a/cmd/tsconnect/wasm/wasm_js.go +++ b/cmd/tsconnect/wasm/wasm_js.go @@ -257,21 +257,25 @@ func (i *jsIPN) run(jsCallbacks js.Value) { }, MachineStatus: jsMachineStatus[nm.MachineStatus], }, - Peers: mapSlice(nm.Peers, func(p *tailcfg.Node) jsNetMapPeerNode { - name := p.Name + Peers: mapSlice(nm.Peers, func(p tailcfg.NodeView) jsNetMapPeerNode { + name := p.Name() if name == "" { // In practice this should only happen for Hello. - name = p.Hostinfo.Hostname() + name = p.Hostinfo().Hostname() + } + addrs := make([]string, p.Addresses().Len()) + for i := range p.Addresses().LenIter() { + addrs[i] = p.Addresses().At(i).Addr().String() } return jsNetMapPeerNode{ jsNetMapNode: jsNetMapNode{ Name: name, - Addresses: mapSlice(p.Addresses, func(a netip.Prefix) string { return a.Addr().String() }), - MachineKey: p.Machine.String(), - NodeKey: p.Key.String(), + Addresses: addrs, + MachineKey: p.Machine().String(), + NodeKey: p.Key().String(), }, - Online: p.Online, - TailscaleSSHEnabled: p.Hostinfo.TailscaleSSHEnabled(), + Online: p.Online(), + TailscaleSSHEnabled: p.Hostinfo().TailscaleSSHEnabled(), } }), LockedOut: nm.TKAEnabled && len(nm.SelfNode.KeySignature) == 0, diff --git a/control/controlclient/map.go b/control/controlclient/map.go index eed5694e8..11d87013a 100644 --- a/control/controlclient/map.go +++ b/control/controlclient/map.go @@ -144,11 +144,18 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo ms.lastTKAInfo = resp.TKAInfo } + // TODO(bradfitz): now that this is a view, remove some of the defensive + // cloning elsewhere in mapSession. + peerViews := make([]tailcfg.NodeView, len(resp.Peers)) + for i, n := range resp.Peers { + peerViews[i] = n.View() + } + nm := &netmap.NetworkMap{ NodeKey: ms.privateNodeKey.Public(), PrivateKey: ms.privateNodeKey, MachineKey: ms.machinePubKey, - Peers: resp.Peers, + Peers: peerViews, UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile), Domain: ms.lastDomain, DomainAuditLogID: ms.lastDomainAuditLogID, diff --git a/ipn/ipnlocal/dnsconfig_test.go b/ipn/ipnlocal/dnsconfig_test.go index fd12d4748..7be3725be 100644 --- a/ipn/ipnlocal/dnsconfig_test.go +++ b/ipn/ipnlocal/dnsconfig_test.go @@ -38,6 +38,14 @@ func ips(ss ...string) (ips []netip.Addr) { return } +func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView { + nv := make([]tailcfg.NodeView, len(v)) + for i, n := range v { + nv[i] = n.View() + } + return nv +} + func TestDNSConfigForNetmap(t *testing.T) { tests := []struct { name string @@ -62,7 +70,7 @@ func TestDNSConfigForNetmap(t *testing.T) { nm: &netmap.NetworkMap{ Name: "myname.net", Addresses: ipps("100.101.101.101"), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Name: "peera.net", Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"), @@ -75,7 +83,7 @@ func TestDNSConfigForNetmap(t *testing.T) { Name: "v6-only.net", Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6 }, - }, + }), }, prefs: &ipn.Prefs{}, want: &dns.Config{ @@ -96,7 +104,7 @@ func TestDNSConfigForNetmap(t *testing.T) { nm: &netmap.NetworkMap{ Name: "myname.net", Addresses: ipps("fe75::1"), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Name: "peera.net", Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001"), @@ -109,7 +117,7 @@ func TestDNSConfigForNetmap(t *testing.T) { Name: "v6-only.net", Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6 }, - }, + }), }, prefs: &ipn.Prefs{}, want: &dns.Config{ diff --git a/ipn/ipnlocal/expiry.go b/ipn/ipnlocal/expiry.go index 6939d1133..7c2e472bc 100644 --- a/ipn/ipnlocal/expiry.go +++ b/ipn/ipnlocal/expiry.go @@ -87,24 +87,26 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow ti return } - for _, peer := range netmap.Peers { + for i, peer := range netmap.Peers { // Nodes that don't expire have KeyExpiry set to the zero time; // skip those and peers that are already marked as expired // (e.g. from control). - if peer.KeyExpiry.IsZero() || peer.KeyExpiry.After(controlNow) { - delete(em.previouslyExpired, peer.StableID) + if peer.KeyExpiry().IsZero() || peer.KeyExpiry().After(controlNow) { + delete(em.previouslyExpired, peer.StableID()) continue - } else if peer.Expired { + } else if peer.Expired() { continue } - if !em.previouslyExpired[peer.StableID] { - em.logf("[v1] netmap: flagExpiredPeers: clearing expired peer %v", peer.StableID) - em.previouslyExpired[peer.StableID] = true + if !em.previouslyExpired[peer.StableID()] { + em.logf("[v1] netmap: flagExpiredPeers: clearing expired peer %v", peer.StableID()) + em.previouslyExpired[peer.StableID()] = true } + mut := peer.AsStruct() + // Actually mark the node as expired - peer.Expired = true + mut.Expired = true // Control clears the Endpoints and DERP fields of expired // nodes; do so here as well. The Expired bool is the correct @@ -113,12 +115,14 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow ti // NOTE: this is insufficient to actually break connectivity, // since we discover endpoints via DERP, and due to DERP return // path optimization. - peer.Endpoints = nil - peer.DERP = "" + mut.Endpoints = nil + mut.DERP = "" // Defense-in-depth: break the node's public key as well, in // case something tries to communicate. - peer.Key = key.NodePublicWithBadOldPrefix(peer.Key) + mut.Key = key.NodePublicWithBadOldPrefix(peer.Key()) + + netmap.Peers[i] = mut.View() } } @@ -144,13 +148,13 @@ func (em *expiryManager) nextPeerExpiry(nm *netmap.NetworkMap, localNow time.Tim var nextExpiry time.Time // zero if none for _, peer := range nm.Peers { - if peer.KeyExpiry.IsZero() { + if peer.KeyExpiry().IsZero() { continue // tagged node - } else if peer.Expired { + } else if peer.Expired() { // Peer already expired; Expired is set by the // flagExpiredPeers function, above. continue - } else if peer.KeyExpiry.Before(controlNow) { + } else if peer.KeyExpiry().Before(controlNow) { // This peer already expired, and peer.Expired // isn't set for some reason. Skip this node. continue @@ -160,8 +164,8 @@ func (em *expiryManager) nextPeerExpiry(nm *netmap.NetworkMap, localNow time.Tim // an expiry; otherwise, only update if this node's expiry is // sooner than the currently-stored one (since we want the // soonest-occurring expiry time). - if nextExpiry.IsZero() || peer.KeyExpiry.Before(nextExpiry) { - nextExpiry = peer.KeyExpiry + if nextExpiry.IsZero() || peer.KeyExpiry().Before(nextExpiry) { + nextExpiry = peer.KeyExpiry() } } diff --git a/ipn/ipnlocal/expiry_test.go b/ipn/ipnlocal/expiry_test.go index 399c5877e..29f32d561 100644 --- a/ipn/ipnlocal/expiry_test.go +++ b/ipn/ipnlocal/expiry_test.go @@ -44,38 +44,38 @@ func TestFlagExpiredPeers(t *testing.T) { name string controlTime *time.Time netmap *netmap.NetworkMap - want []*tailcfg.Node + want []tailcfg.NodeView }{ { name: "no_expiry", controlTime: &now, netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInFuture), n(2, "bar", timeInFuture), - }, + }), }, - want: []*tailcfg.Node{ + want: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInFuture), n(2, "bar", timeInFuture), - }, + }), }, { name: "expiry", controlTime: &now, netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInFuture), n(2, "bar", timeInPast), - }, + }), }, - want: []*tailcfg.Node{ + want: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInFuture), n(2, "bar", timeInPast, func(n *tailcfg.Node) { n.Expired = true n.Key = expiredKey }), - }, + }), }, { name: "bad_ControlTime", @@ -83,29 +83,29 @@ func TestFlagExpiredPeers(t *testing.T) { controlTime: &timeBeforeEpoch, netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInFuture), n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // before ControlTime - }, + }), }, - want: []*tailcfg.Node{ + want: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInFuture), n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // should have expired, but ControlTime is before epoch - }, + }), }, { name: "tagged_node", controlTime: &now, netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInFuture), n(2, "bar", time.Time{}), // tagged node; zero expiry - }, + }), }, - want: []*tailcfg.Node{ + want: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInFuture), n(2, "bar", time.Time{}), // not expired - }, + }), }, } for _, tt := range tests { @@ -147,10 +147,10 @@ func TestNextPeerExpiry(t *testing.T) { { name: "no_expiry", netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", noExpiry), n(2, "bar", noExpiry), - }, + }), SelfNode: n(3, "self", noExpiry), }, want: noExpiry, @@ -158,10 +158,10 @@ func TestNextPeerExpiry(t *testing.T) { { name: "future_expiry_from_peer", netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", noExpiry), n(2, "bar", timeInFuture), - }, + }), SelfNode: n(3, "self", noExpiry), }, want: timeInFuture, @@ -169,10 +169,10 @@ func TestNextPeerExpiry(t *testing.T) { { name: "future_expiry_from_self", netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", noExpiry), n(2, "bar", noExpiry), - }, + }), SelfNode: n(3, "self", timeInFuture), }, want: timeInFuture, @@ -180,10 +180,10 @@ func TestNextPeerExpiry(t *testing.T) { { name: "future_expiry_from_multiple_peers", netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInFuture), n(2, "bar", timeInMoreFuture), - }, + }), SelfNode: n(3, "self", noExpiry), }, want: timeInFuture, @@ -191,9 +191,9 @@ func TestNextPeerExpiry(t *testing.T) { { name: "future_expiry_from_peer_and_self", netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInMoreFuture), - }, + }), SelfNode: n(2, "self", timeInFuture), }, want: timeInFuture, @@ -201,7 +201,7 @@ func TestNextPeerExpiry(t *testing.T) { { name: "only_self", netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{}, + Peers: nodeViews([]*tailcfg.Node{}), SelfNode: n(1, "self", timeInFuture), }, want: timeInFuture, @@ -209,9 +209,9 @@ func TestNextPeerExpiry(t *testing.T) { { name: "peer_already_expired", netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInPast), - }, + }), SelfNode: n(2, "self", timeInFuture), }, want: timeInFuture, @@ -219,9 +219,9 @@ func TestNextPeerExpiry(t *testing.T) { { name: "self_already_expired", netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInFuture), - }, + }), SelfNode: n(2, "self", timeInPast), }, want: timeInFuture, @@ -229,9 +229,9 @@ func TestNextPeerExpiry(t *testing.T) { { name: "all_nodes_already_expired", netmap: &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInPast), - }, + }), SelfNode: n(2, "self", timeInPast), }, want: noExpiry, @@ -263,9 +263,9 @@ func TestNextPeerExpiry(t *testing.T) { // If we don't adjust for the local time, this would return a // time in the past. nm := &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ n(1, "foo", timeInPast), - }, + }), } got := em.nextPeerExpiry(nm, now) want := now.Add(30 * time.Second) @@ -275,24 +275,24 @@ func TestNextPeerExpiry(t *testing.T) { }) } -func formatNodes(nodes []*tailcfg.Node) string { +func formatNodes(nodes []tailcfg.NodeView) string { var sb strings.Builder for i, n := range nodes { if i > 0 { sb.WriteString(", ") } - fmt.Fprintf(&sb, "(%d, %q", n.ID, n.Name) + fmt.Fprintf(&sb, "(%d, %q", n.ID(), n.Name()) - if n.Online != nil { - fmt.Fprintf(&sb, ", online=%v", *n.Online) + if n.Online() != nil { + fmt.Fprintf(&sb, ", online=%v", *n.Online()) } - if n.LastSeen != nil { - fmt.Fprintf(&sb, ", lastSeen=%v", n.LastSeen.Unix()) + if n.LastSeen() != nil { + fmt.Fprintf(&sb, ", lastSeen=%v", n.LastSeen().Unix()) } - if n.Key != (key.NodePublic{}) { - fmt.Fprintf(&sb, ", key=%v", n.Key.String()) + if n.Key() != (key.NodePublic{}) { + fmt.Fprintf(&sb, ", key=%v", n.Key().String()) } - if n.Expired { + if n.Expired() { fmt.Fprintf(&sb, ", expired=true") } sb.WriteString(")") diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 9d0cacac0..7eb1f2188 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -204,7 +204,7 @@ type LocalBackend struct { // netMap is not mutated in-place once set. netMap *netmap.NetworkMap nmExpiryTimer tstime.TimerController // for updating netMap on node expiry; can be nil - nodeByAddr map[netip.Addr]*tailcfg.Node + nodeByAddr map[netip.Addr]tailcfg.NodeView activeLogin string // last logged LoginName from netMap engineStatus ipn.EngineStatus endpoints []tailcfg.Endpoint @@ -684,13 +684,13 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func if !prefs.ExitNodeID().IsZero() { if exitPeer, ok := b.netMap.PeerWithStableID(prefs.ExitNodeID()); ok { var online = false - if exitPeer.Online != nil { - online = *exitPeer.Online + if v := exitPeer.Online(); v != nil { + online = *v } s.ExitNodeStatus = &ipnstate.ExitNodeStatus{ ID: prefs.ExitNodeID(), Online: online, - TailscaleIPs: exitPeer.Addresses, + TailscaleIPs: exitPeer.Addresses().AsSlice(), } } } @@ -705,7 +705,7 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func ss.DNSName = b.netMap.Name ss.UserID = b.netMap.User if sn := b.netMap.SelfNode; sn != nil { - peerStatusFromNode(ss, sn) + peerStatusFromNode(ss, sn.View()) if c := sn.Capabilities; len(c) > 0 { ss.Capabilities = append([]string(nil), c...) } @@ -735,28 +735,30 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) { exitNodeID := b.pm.CurrentPrefs().ExitNodeID() for _, p := range b.netMap.Peers { var lastSeen time.Time - if p.LastSeen != nil { - lastSeen = *p.LastSeen + if p.LastSeen() != nil { + lastSeen = *p.LastSeen() } - var tailscaleIPs = make([]netip.Addr, 0, len(p.Addresses)) - for _, addr := range p.Addresses { + var tailscaleIPs = make([]netip.Addr, 0, p.Addresses().Len()) + for i := range p.Addresses().LenIter() { + addr := p.Addresses().At(i) if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) { tailscaleIPs = append(tailscaleIPs, addr.Addr()) } } + online := p.Online() ps := &ipnstate.PeerStatus{ InNetworkMap: true, - UserID: p.User, + UserID: p.User(), TailscaleIPs: tailscaleIPs, - HostName: p.Hostinfo.Hostname(), - DNSName: p.Name, - OS: p.Hostinfo.OS(), + HostName: p.Hostinfo().Hostname(), + DNSName: p.Name(), + OS: p.Hostinfo().OS(), LastSeen: lastSeen, - Online: p.Online != nil && *p.Online, - ShareeNode: p.Hostinfo.ShareeNode(), - ExitNode: p.StableID != "" && p.StableID == exitNodeID, - SSH_HostKeys: p.Hostinfo.SSH_HostKeys().AsSlice(), - Location: p.Hostinfo.Location(), + Online: online != nil && *online, + ShareeNode: p.Hostinfo().ShareeNode(), + ExitNode: p.StableID() != "" && p.StableID() == exitNodeID, + SSH_HostKeys: p.Hostinfo().SSH_HostKeys().AsSlice(), + Location: p.Hostinfo().Location(), } peerStatusFromNode(ps, p) @@ -767,29 +769,29 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) { if u := peerAPIURL(nodeIP(p, netip.Addr.Is6), p6); u != "" { ps.PeerAPIURL = append(ps.PeerAPIURL, u) } - sb.AddPeer(p.Key, ps) + sb.AddPeer(p.Key(), ps) } } // peerStatusFromNode copies fields that exist in the Node struct for // current node and peers into the provided PeerStatus. -func peerStatusFromNode(ps *ipnstate.PeerStatus, n *tailcfg.Node) { - ps.ID = n.StableID - ps.Created = n.Created - ps.ExitNodeOption = tsaddr.ContainsExitRoutes(views.SliceOf(n.AllowedIPs)) - if n.Tags != nil { - v := views.SliceOf(n.Tags) +func peerStatusFromNode(ps *ipnstate.PeerStatus, n tailcfg.NodeView) { + ps.ID = n.StableID() + ps.Created = n.Created() + ps.ExitNodeOption = tsaddr.ContainsExitRoutes(n.AllowedIPs()) + if n.Tags().Len() != 0 { + v := n.Tags() ps.Tags = &v } - if n.PrimaryRoutes != nil { - v := views.SliceOf(n.PrimaryRoutes) + if n.PrimaryRoutes().Len() != 0 { + v := n.PrimaryRoutes() ps.PrimaryRoutes = &v } - if n.Expired { + if n.Expired() { ps.Expired = true } - if t := n.KeyExpiry; !t.IsZero() { + if t := n.KeyExpiry(); !t.IsZero() { t = t.Round(time.Second) ps.KeyExpiry = &t } @@ -798,7 +800,8 @@ func peerStatusFromNode(ps *ipnstate.PeerStatus, n *tailcfg.Node) { // WhoIs reports the node and user who owns the node with the given IP:port. // If the IP address is a Tailscale IP, the provided port may be 0. // If ok == true, n and u are valid. -func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) { +func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) { + var zero tailcfg.NodeView b.mu.Lock() defer b.mu.Unlock() n, ok = b.nodeByAddr[ipp.Addr()] @@ -808,16 +811,16 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.Use ip, ok = b.e.WhoIsIPPort(ipp) } if !ok { - return nil, u, false + return zero, u, false } n, ok = b.nodeByAddr[ip] if !ok { - return nil, u, false + return zero, u, false } } - u, ok = b.netMap.UserProfiles[n.User] + u, ok = b.netMap.UserProfiles[n.User()] if !ok { - return nil, u, false + return zero, u, false } return n, u, true } @@ -1114,13 +1117,14 @@ func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool) } for _, peer := range nm.Peers { - for _, addr := range peer.Addresses { + for i := range peer.Addresses().LenIter() { + addr := peer.Addresses().At(i) if !addr.IsSingleIP() || addr.Addr() != prefs.ExitNodeIP { continue } // Found the node being referenced, upgrade prefs to // reference it directly for next time. - prefs.ExitNodeID = peer.StableID + prefs.ExitNodeID = peer.StableID() prefs.ExitNodeIP = netip.Addr{} return true } @@ -1597,16 +1601,16 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P // // If this reports true, the packet filter is invalid (the server is either broken // or malicious) and should be ignored for safety. -func packetFilterPermitsUnlockedNodes(peers []*tailcfg.Node, packetFilter []filter.Match) bool { +func packetFilterPermitsUnlockedNodes(peers []tailcfg.NodeView, packetFilter []filter.Match) bool { var b netipx.IPSetBuilder var numUnlocked int for _, p := range peers { - if !p.UnsignedPeerAPIOnly { + if !p.UnsignedPeerAPIOnly() { continue } numUnlocked++ - for _, a := range p.AllowedIPs { // not only addresses! - b.AddPrefix(a) + for i := range p.AllowedIPs().LenIter() { // not only addresses! + b.AddPrefix(p.AllowedIPs().At(i)) } } if numUnlocked == 0 { @@ -1764,11 +1768,11 @@ func shrinkDefaultRoute(route netip.Prefix, localInterfaceRoutes *netipx.IPSet, // dnsCIDRsEqual determines whether two CIDR lists are equal // for DNS map construction purposes (that is, only the first entry counts). -func dnsCIDRsEqual(newAddr, oldAddr []netip.Prefix) bool { - if len(newAddr) != len(oldAddr) { +func dnsCIDRsEqual(newAddr, oldAddr views.Slice[netip.Prefix]) bool { + if newAddr.Len() != oldAddr.Len() { return false } - if len(newAddr) == 0 || newAddr[0] == oldAddr[0] { + if newAddr.Len() == 0 || newAddr.At(0) == oldAddr.At(0) { return true } return false @@ -1792,16 +1796,16 @@ func dnsMapsEqual(new, old *netmap.NetworkMap) bool { if new.Name != old.Name { return false } - if !dnsCIDRsEqual(new.Addresses, old.Addresses) { + if !dnsCIDRsEqual(views.SliceOf(new.Addresses), views.SliceOf(old.Addresses)) { return false } for i, newPeer := range new.Peers { oldPeer := old.Peers[i] - if newPeer.Name != oldPeer.Name { + if newPeer.Name() != oldPeer.Name() { return false } - if !dnsCIDRsEqual(newPeer.Addresses, oldPeer.Addresses) { + if !dnsCIDRsEqual(newPeer.Addresses(), oldPeer.Addresses()) { return false } } @@ -2418,8 +2422,8 @@ func (b *LocalBackend) Ping(ctx context.Context, ip netip.Addr, pingType tailcfg if err != nil { pr.Err = err.Error() } - if node != nil { - pr.NodeName = node.Name + if node.Valid() { + pr.NodeName = node.Name() } return pr, nil } @@ -2438,36 +2442,37 @@ func (b *LocalBackend) Ping(ctx context.Context, ip netip.Addr, pingType tailcfg } } -func (b *LocalBackend) pingPeerAPI(ctx context.Context, ip netip.Addr) (peer *tailcfg.Node, peerBase string, err error) { +func (b *LocalBackend) pingPeerAPI(ctx context.Context, ip netip.Addr) (peer tailcfg.NodeView, peerBase string, err error) { + var zero tailcfg.NodeView ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() nm := b.NetMap() if nm == nil { - return nil, "", errors.New("no netmap") + return zero, "", errors.New("no netmap") } peer, ok := nm.PeerByTailscaleIP(ip) if !ok { - return nil, "", fmt.Errorf("no peer found with Tailscale IP %v", ip) + return zero, "", fmt.Errorf("no peer found with Tailscale IP %v", ip) } - if peer.Expired { - return nil, "", errors.New("peer's node key has expired") + if peer.Expired() { + return zero, "", errors.New("peer's node key has expired") } base := peerAPIBase(nm, peer) if base == "" { - return nil, "", fmt.Errorf("no PeerAPI base found for peer %v (%v)", peer.ID, ip) + return zero, "", fmt.Errorf("no PeerAPI base found for peer %v (%v)", peer.ID(), ip) } outReq, err := http.NewRequestWithContext(ctx, "HEAD", base, nil) if err != nil { - return nil, "", err + return zero, "", err } tr := b.Dialer().PeerAPITransport() res, err := tr.RoundTrip(outReq) if err != nil { - return nil, "", err + return zero, "", err } defer res.Body.Close() // but unnecessary on HEAD responses if res.StatusCode != http.StatusOK { - return nil, "", fmt.Errorf("HTTP status %v", res.Status) + return zero, "", fmt.Errorf("HTTP status %v", res.Status) } return peer, base, nil } @@ -3098,17 +3103,24 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger. // isn't configured to make MagicDNS resolution truly // magic. Details in // https://github.com/tailscale/tailscale/issues/1886. - set := func(name string, addrs []netip.Prefix) { - if len(addrs) == 0 || name == "" { + set := func(name string, addrs views.Slice[netip.Prefix]) { + if addrs.Len() == 0 || name == "" { return } fqdn, err := dnsname.ToFQDN(name) if err != nil { return // TODO: propagate error? } - have4 := slices.ContainsFunc(addrs, tsaddr.PrefixIs4) + var have4 bool + for i := range addrs.LenIter() { + if addrs.At(i).Addr().Is4() { + have4 = true + break + } + } var ips []netip.Addr - for _, addr := range addrs { + for i := range addrs.LenIter() { + addr := addrs.At(i) if selfV6Only { if addr.Addr().Is6() { ips = append(ips, addr.Addr()) @@ -3130,9 +3142,9 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger. } dcfg.Hosts[fqdn] = ips } - set(nm.Name, nm.Addresses) + set(nm.Name, views.SliceOf(nm.Addresses)) for _, peer := range nm.Peers { - set(peer.Name, peer.Addresses) + set(peer.Name(), peer.Addresses()) } for _, rec := range nm.DNS.ExtraRecords { switch rec.Type { @@ -3995,28 +4007,28 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { // Update the nodeByAddr index. if b.nodeByAddr == nil { - b.nodeByAddr = map[netip.Addr]*tailcfg.Node{} + b.nodeByAddr = map[netip.Addr]tailcfg.NodeView{} } // First pass, mark everything unwanted. for k := range b.nodeByAddr { - b.nodeByAddr[k] = nil + b.nodeByAddr[k] = tailcfg.NodeView{} } - addNode := func(n *tailcfg.Node) { - for _, ipp := range n.Addresses { - if ipp.IsSingleIP() { + addNode := func(n tailcfg.NodeView) { + for i := range n.Addresses().LenIter() { + if ipp := n.Addresses().At(i); ipp.IsSingleIP() { b.nodeByAddr[ipp.Addr()] = n } } } if nm.SelfNode != nil { - addNode(nm.SelfNode) + addNode(nm.SelfNode.View()) } for _, p := range nm.Peers { addNode(p) } // Third pass, actually delete the unwanted items. for k, v := range b.nodeByAddr { - if v == nil { + if !v.Valid() { delete(b.nodeByAddr, k) } } @@ -4293,7 +4305,7 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) { continue } ret = append(ret, &apitype.FileTarget{ - Node: p, + Node: p.AsStruct(), PeerAPIURL: peerAPI, }) } @@ -4306,15 +4318,15 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) { // the netmap. // // b.mu must be locked. -func (b *LocalBackend) peerIsTaildropTargetLocked(p *tailcfg.Node) bool { - if b.netMap == nil || p == nil { +func (b *LocalBackend) peerIsTaildropTargetLocked(p tailcfg.NodeView) bool { + if b.netMap == nil || !p.Valid() { return false } - if b.netMap.User == p.User { + if b.netMap.User == p.User() { return true } - if len(p.Addresses) > 0 && - b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.PeerCapabilityFileSharingTarget) { + if p.Addresses().Len() > 0 && + b.peerHasCapLocked(p.Addresses().At(0).Addr(), tailcfg.PeerCapabilityFileSharingTarget) { // Explicitly noted in the netmap ACL caps as a target. return true } @@ -4374,9 +4386,9 @@ func (b *LocalBackend) registerIncomingFile(inf *incomingFile, active bool) { } } -func peerAPIPorts(peer *tailcfg.Node) (p4, p6 uint16) { - svcs := peer.Hostinfo.Services() - for i, n := 0, svcs.Len(); i < n; i++ { +func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) { + svcs := peer.Hostinfo().Services() + for i := range svcs.LenIter() { s := svcs.At(i) switch s.Proto { case tailcfg.PeerAPI4: @@ -4402,8 +4414,8 @@ func peerAPIURL(ip netip.Addr, port uint16) string { // peerAPIBase returns the "http://ip:port" URL base to reach peer's peerAPI. // It returns the empty string if the peer doesn't support the peerapi // or there's no matching address family based on the netmap's own addresses. -func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string { - if nm == nil || peer == nil || !peer.Hostinfo.Valid() { +func peerAPIBase(nm *netmap.NetworkMap, peer tailcfg.NodeView) string { + if nm == nil || !peer.Valid() || !peer.Hostinfo().Valid() { return "" } @@ -4429,8 +4441,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string { return "" } -func nodeIP(n *tailcfg.Node, pred func(netip.Addr) bool) netip.Addr { - for _, a := range n.Addresses { +func nodeIP(n tailcfg.NodeView, pred func(netip.Addr) bool) netip.Addr { + for i := range n.Addresses().LenIter() { + a := n.Addresses().At(i) if a.IsSingleIP() && pred(a.Addr()) { return a.Addr() } @@ -4540,15 +4553,15 @@ func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID) return "", false } for _, p := range nm.Peers { - if p.StableID == exitNodeID && peerCanProxyDNS(p) { + if p.StableID() == exitNodeID && peerCanProxyDNS(p) { return peerAPIBase(nm, p) + "/dns-query", true } } return "", false } -func peerCanProxyDNS(p *tailcfg.Node) bool { - if p.Cap >= 26 { +func peerCanProxyDNS(p tailcfg.NodeView) bool { + if p.Cap() >= 26 { // Actually added at 25 // (https://github.com/tailscale/tailscale/blob/3ae6f898cfdb58fd0e30937147dd6ce28c6808dd/tailcfg/tailcfg.go#L51) // so anything >= 26 can do it. @@ -4556,10 +4569,9 @@ func peerCanProxyDNS(p *tailcfg.Node) bool { } // If p.Cap is not populated (e.g. older control server), then do the old // thing of searching through services. - services := p.Hostinfo.Services() - for i, n := 0, services.Len(); i < n; i++ { - s := services.At(i) - if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 { + services := p.Hostinfo().Services() + for i := range services.LenIter() { + if s := services.At(i); s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 { return true } } diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index 9fd44def8..eb9e08d60 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -87,46 +87,46 @@ func TestNetworkMapCompare(t *testing.T) { }, { "Peers identical", - &netmap.NetworkMap{Peers: []*tailcfg.Node{}}, - &netmap.NetworkMap{Peers: []*tailcfg.Node{}}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})}, true, }, { "Peer list length", // length of Peers list differs - &netmap.NetworkMap{Peers: []*tailcfg.Node{{}}}, - &netmap.NetworkMap{Peers: []*tailcfg.Node{}}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{}})}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})}, false, }, { "Node names identical", - &netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}}, - &netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})}, true, }, { "Node names differ", - &netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}}, - &netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "B"}}}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "B"}})}, false, }, { "Node lists identical", - &netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, - &netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})}, true, }, { "Node lists differ", - &netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, - &netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node2}}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node2})}, false, }, { "Node Users differ", // User field is not checked. - &netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 0}}}, - &netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 1}}}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{User: 0}})}, + &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{User: 1}})}, true, }, } @@ -483,7 +483,7 @@ func TestPeerAPIBase(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := peerAPIBase(tt.nm, tt.peer) + got := peerAPIBase(tt.nm, tt.peer.View()) if got != tt.want { t.Errorf("got %q; want %q", got, tt.want) } @@ -758,7 +758,7 @@ func TestPacketFilterPermitsUnlockedNodes(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := packetFilterPermitsUnlockedNodes(tt.peers, tt.filter); got != tt.want { + if got := packetFilterPermitsUnlockedNodes(nodeViews(tt.peers), tt.filter); got != tt.want { t.Errorf("got %v, want %v", got, tt.want) } }) diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index 0057c1bb9..0144f7bdc 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -69,15 +69,16 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) { var toDelete map[int]bool // peer index => true for i, p := range nm.Peers { - if p.UnsignedPeerAPIOnly { + if p.UnsignedPeerAPIOnly() { // Not subject to tailnet lock. continue } - if len(p.KeySignature) == 0 { + keySig := tkatype.MarshaledSignature(p.KeySignature().StringCopy()) // TODO(bradfitz,maisem): this is unfortunate. Change tkatype.MarshaledSignature to a string for viewer? + if len(keySig) == 0 { b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID, p.StableID) mak.Set(&toDelete, i, true) } else { - if err := b.tka.authority.NodeKeyAuthorized(p.Key, p.KeySignature); err != nil { + if err := b.tka.authority.NodeKeyAuthorized(p.Key(), keySig); err != nil { b.logf("Network lock is dropping peer %v(%v) due to failed signature check: %v", p.ID, p.StableID, err) mak.Set(&toDelete, i, true) } @@ -86,7 +87,7 @@ 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)) + peers := make([]tailcfg.NodeView, 0, len(nm.Peers)) filtered := make([]ipnstate.TKAFilteredPeer, 0, len(toDelete)) for i, p := range nm.Peers { if !toDelete[i] { @@ -94,13 +95,14 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) { } 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)), - NodeKey: p.Key, + Name: p.Name(), + ID: p.ID(), + StableID: p.StableID(), + TailscaleIPs: make([]netip.Addr, p.Addresses().Len()), + NodeKey: p.Key(), } - for i, addr := range p.Addresses { + for i := range p.Addresses().LenIter() { + addr := p.Addresses().At(i) if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) { fp.TailscaleIPs[i] = addr.Addr() } diff --git a/ipn/ipnlocal/network-lock_test.go b/ipn/ipnlocal/network-lock_test.go index c6a9f5331..e9325ea13 100644 --- a/ipn/ipnlocal/network-lock_test.go +++ b/ipn/ipnlocal/network-lock_test.go @@ -558,26 +558,26 @@ func TestTKAFilterNetmap(t *testing.T) { t.Fatal(err) } - nm := netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + nm := &netmap.NetworkMap{ + Peers: nodeViews([]*tailcfg.Node{ {ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()}, {ID: 2, Key: n2.Public(), KeySignature: nil}, // missing sig {ID: 3, Key: n3.Public(), KeySignature: n1GoodSig.Serialize()}, // someone elses sig {ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature {ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()}, - }, + }), } b := &LocalBackend{ logf: t.Logf, tka: &tkaState{authority: authority}, } - b.tkaFilterNetmapLocked(&nm) + b.tkaFilterNetmapLocked(nm) - want := []*tailcfg.Node{ + want := nodeViews([]*tailcfg.Node{ {ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()}, {ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()}, - } + }) nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool { return x.Raw32() == y.Raw32() }) diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index f7396da17..d2b894fa9 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -576,7 +576,7 @@ func (pln *peerAPIListener) ServeConn(src netip.AddrPort, c net.Conn) { } h := &peerAPIHandler{ ps: pln.ps, - isSelf: nm.SelfNode.User == peerNode.User, + isSelf: nm.SelfNode.User == peerNode.User(), remoteAddr: src, selfNode: nm.SelfNode, peerNode: peerNode, @@ -597,7 +597,7 @@ type peerAPIHandler struct { remoteAddr netip.AddrPort isSelf bool // whether peerNode is owned by same user as this node selfNode *tailcfg.Node // this node; always non-nil - peerNode *tailcfg.Node // peerNode is who's making the request + peerNode tailcfg.NodeView // peerNode is who's making the request peerUser tailcfg.UserProfile // profile of peerNode } @@ -608,8 +608,8 @@ func (h *peerAPIHandler) logf(format string, a ...any) { // isAddressValid reports whether addr is a valid destination address for this // node originating from the peer. func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool { - if h.peerNode.SelfNodeV4MasqAddrForThisPeer != nil { - return *h.peerNode.SelfNodeV4MasqAddrForThisPeer == addr + if v := h.peerNode.SelfNodeV4MasqAddrForThisPeer(); v != nil { + return *v == addr } pfx := netip.PrefixFrom(addr, addr.BitLen()) return slices.Contains(h.selfNode.Addresses, pfx) @@ -733,7 +733,7 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

Hello, %s (%v)

This is my Tailscale device. Your device is %v. -`, html.EscapeString(who), h.remoteAddr.Addr(), html.EscapeString(h.peerNode.ComputedName)) +`, html.EscapeString(who), h.remoteAddr.Addr(), html.EscapeString(h.peerNode.ComputedName())) if h.isSelf { fmt.Fprintf(w, "

You are the owner of this node.\n") @@ -1024,7 +1024,7 @@ func (f *incomingFile) PartialFile() ipn.PartialFile { // canPutFile reports whether h can put a file ("Taildrop") to this node. func (h *peerAPIHandler) canPutFile() bool { - if h.peerNode.UnsignedPeerAPIOnly { + if h.peerNode.UnsignedPeerAPIOnly() { // Unsigned peers can't send files. return false } @@ -1038,7 +1038,7 @@ func (h *peerAPIHandler) canDebug() bool { // This node does not expose debug info. return false } - if h.peerNode.UnsignedPeerAPIOnly { + if h.peerNode.UnsignedPeerAPIOnly() { // Unsigned peers can't debug. return false } @@ -1047,7 +1047,7 @@ func (h *peerAPIHandler) canDebug() bool { // canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node. func (h *peerAPIHandler) canWakeOnLAN() bool { - if h.peerNode.UnsignedPeerAPIOnly { + if h.peerNode.UnsignedPeerAPIOnly() { return false } return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityWakeOnLAN) diff --git a/ipn/ipnlocal/peerapi_test.go b/ipn/ipnlocal/peerapi_test.go index 8da7726d3..1230b50b7 100644 --- a/ipn/ipnlocal/peerapi_test.go +++ b/ipn/ipnlocal/peerapi_test.go @@ -462,9 +462,9 @@ func TestHandlePeerAPI(t *testing.T) { e.ph = &peerAPIHandler{ isSelf: tt.isSelf, selfNode: selfNode, - peerNode: &tailcfg.Node{ + peerNode: (&tailcfg.Node{ ComputedName: "some-peer-name", - }, + }).View(), ps: &peerAPIServer{ b: lb, }, @@ -513,9 +513,9 @@ func TestFileDeleteRace(t *testing.T) { } ph := &peerAPIHandler{ isSelf: true, - peerNode: &tailcfg.Node{ + peerNode: (&tailcfg.Node{ ComputedName: "some-peer-name", - }, + }).View(), selfNode: &tailcfg.Node{ Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.100.101/32")}, }, diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index 1f9d0e8a0..1b61fdcec 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -257,7 +257,7 @@ func (b *LocalBackend) ServeConfig() ipn.ServeConfigView { return b.serveConfig } -func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) { +func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) { b.mu.Lock() sc := b.serveConfig b.mu.Unlock() diff --git a/ipn/ipnlocal/serve_test.go b/ipn/ipnlocal/serve_test.go index 8565cf28e..d54eb63b8 100644 --- a/ipn/ipnlocal/serve_test.go +++ b/ipn/ipnlocal/serve_test.go @@ -201,16 +201,16 @@ func TestServeHTTPProxy(t *testing.T) { }, }, } - b.nodeByAddr = map[netip.Addr]*tailcfg.Node{ - netip.MustParseAddr("100.150.151.152"): { + b.nodeByAddr = map[netip.Addr]tailcfg.NodeView{ + netip.MustParseAddr("100.150.151.152"): (&tailcfg.Node{ ComputedName: "some-peer", User: tailcfg.UserID(1), - }, - netip.MustParseAddr("100.150.151.153"): { + }).View(), + netip.MustParseAddr("100.150.151.153"): (&tailcfg.Node{ ComputedName: "some-tagged-peer", Tags: []string{"tag:server", "tag:test"}, User: tailcfg.UserID(1), - }, + }).View(), } // Start test serve endpoint. diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index f74786ec0..ec779f79c 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -437,8 +437,8 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) { return } res := &apitype.WhoIsResponse{ - Node: n, // always non-nil per WhoIsResponse contract - UserProfile: &u, // always non-nil per WhoIsResponse contract + Node: n.AsStruct(), // always non-nil per WhoIsResponse contract + UserProfile: &u, // always non-nil per WhoIsResponse contract CapMap: b.PeerCaps(ipp.Addr()), } j, err := json.MarshalIndent(res, "", "\t") diff --git a/net/tsdial/dnsmap.go b/net/tsdial/dnsmap.go index 52b8e6d81..c5ed7e01f 100644 --- a/net/tsdial/dnsmap.go +++ b/net/tsdial/dnsmap.go @@ -48,17 +48,18 @@ func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap { } } for _, p := range nm.Peers { - if p.Name == "" { + if p.Name() == "" { continue } - for _, a := range p.Addresses { + for i := range p.Addresses().LenIter() { + a := p.Addresses().At(i) ip := a.Addr() if ip.Is4() && !have4 { continue } - ret[canonMapKey(p.Name)] = ip - if dnsname.HasSuffix(p.Name, suffix) { - ret[canonMapKey(dnsname.TrimSuffix(p.Name, suffix))] = ip + ret[canonMapKey(p.Name())] = ip + if dnsname.HasSuffix(p.Name(), suffix) { + ret[canonMapKey(dnsname.TrimSuffix(p.Name(), suffix))] = ip } break } diff --git a/net/tsdial/dnsmap_test.go b/net/tsdial/dnsmap_test.go index 0712b6d0d..33eda8b59 100644 --- a/net/tsdial/dnsmap_test.go +++ b/net/tsdial/dnsmap_test.go @@ -12,6 +12,14 @@ import ( "tailscale.com/types/netmap" ) +func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView { + nv := make([]tailcfg.NodeView, len(v)) + for i, n := range v { + nv[i] = n.View() + } + return nv +} + func TestDNSMapFromNetworkMap(t *testing.T) { pfx := netip.MustParsePrefix ip := netip.MustParseAddr @@ -42,20 +50,20 @@ func TestDNSMapFromNetworkMap(t *testing.T) { pfx("100.102.103.104/32"), pfx("100::123/128"), }, - Peers: []*tailcfg.Node{ - { + Peers: []tailcfg.NodeView{ + (&tailcfg.Node{ Name: "a.tailnet", Addresses: []netip.Prefix{ pfx("100.0.0.201/32"), pfx("100::201/128"), }, - }, - { + }).View(), + (&tailcfg.Node{ Name: "b.tailnet", Addresses: []netip.Prefix{ pfx("100::202/128"), }, - }, + }).View(), }, }, want: dnsMap{ @@ -74,7 +82,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) { Addresses: []netip.Prefix{ pfx("100::123/128"), }, - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Name: "a.tailnet", Addresses: []netip.Prefix{ @@ -88,7 +96,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) { pfx("100::202/128"), }, }, - }, + }), }, want: dnsMap{ "foo": ip("100::123"), diff --git a/ssh/tailssh/incubator.go b/ssh/tailssh/incubator.go index dd109dc32..97e701da4 100644 --- a/ssh/tailssh/incubator.go +++ b/ssh/tailssh/incubator.go @@ -99,7 +99,7 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) { gids := strings.Join(ss.conn.userGroupIDs, ",") remoteUser := ci.uprof.LoginName if ci.node.IsTagged() { - remoteUser = strings.Join(ci.node.Tags, ",") + remoteUser = strings.Join(ci.node.Tags().AsSlice(), ",") } incubatorArgs := []string{ diff --git a/ssh/tailssh/tailssh.go b/ssh/tailssh/tailssh.go index 274f8cc70..a063c9354 100644 --- a/ssh/tailssh/tailssh.go +++ b/ssh/tailssh/tailssh.go @@ -64,7 +64,7 @@ type ipnLocalBackend interface { GetSSH_HostKeys() ([]gossh.Signer, error) ShouldRunSSH() bool NetMap() *netmap.NetworkMap - WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) + WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) DoNoiseRequest(req *http.Request) (*http.Response, error) Dialer() *tsdial.Dialer TailscaleVarRoot() string @@ -791,7 +791,7 @@ func (c *conn) expandDelegateURLLocked(actionURL string) string { } return strings.NewReplacer( "$SRC_NODE_IP", url.QueryEscape(ci.src.Addr().String()), - "$SRC_NODE_ID", fmt.Sprint(int64(ci.node.ID)), + "$SRC_NODE_ID", fmt.Sprint(int64(ci.node.ID())), "$DST_NODE_IP", url.QueryEscape(ci.dst.Addr().String()), "$DST_NODE_ID", dstNodeID, "$SSH_USER", url.QueryEscape(ci.sshUser), @@ -1220,7 +1220,7 @@ type sshConnInfo struct { dst netip.AddrPort // node is srcIP's node. - node *tailcfg.Node + node tailcfg.NodeView // uprof is node's UserProfile. uprof tailcfg.UserProfile @@ -1334,7 +1334,7 @@ func (c *conn) principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal) bool { if p.Any { return true } - if !p.Node.IsZero() && ci.node != nil && p.Node == ci.node.StableID { + if !p.Node.IsZero() && ci.node.Valid() && p.Node == ci.node.StableID() { return true } if p.NodeIP != "" { @@ -1702,15 +1702,15 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) { }, SSHUser: ss.conn.info.sshUser, LocalUser: ss.conn.localUser.Username, - SrcNode: strings.TrimSuffix(ss.conn.info.node.Name, "."), - SrcNodeID: ss.conn.info.node.StableID, + SrcNode: strings.TrimSuffix(ss.conn.info.node.Name(), "."), + SrcNodeID: ss.conn.info.node.StableID(), ConnectionID: ss.conn.connID, } if !ss.conn.info.node.IsTagged() { ch.SrcNodeUser = ss.conn.info.uprof.LoginName - ch.SrcNodeUserID = ss.conn.info.node.User + ch.SrcNodeUserID = ss.conn.info.node.User() } else { - ch.SrcNodeTags = ss.conn.info.node.Tags + ch.SrcNodeTags = ss.conn.info.node.Tags().AsSlice() } j, err := json.Marshal(ch) if err != nil { @@ -1738,7 +1738,7 @@ func (ss *sshSession) notifyControl(ctx context.Context, nodeKey key.NodePublic, ConnectionID: ss.conn.connID, CapVersion: tailcfg.CurrentCapabilityVersion, NodeKey: nodeKey, - SrcNode: ss.conn.info.node.ID, + SrcNode: ss.conn.info.node.ID(), SSHUser: ss.conn.info.sshUser, LocalUser: ss.conn.localUser.Username, RecordingAttempts: attempts, diff --git a/ssh/tailssh/tailssh_test.go b/ssh/tailssh/tailssh_test.go index fac2c70e6..17f84e69a 100644 --- a/ssh/tailssh/tailssh_test.go +++ b/ssh/tailssh/tailssh_test.go @@ -177,7 +177,7 @@ func TestMatchRule(t *testing.T) { Principals: []*tailcfg.SSHPrincipal{{Node: "some-node-ID"}}, SSHUsers: map[string]string{"*": "ubuntu"}, }, - ci: &sshConnInfo{node: &tailcfg.Node{StableID: "some-node-ID"}}, + ci: &sshConnInfo{node: (&tailcfg.Node{StableID: "some-node-ID"}).View()}, wantUser: "ubuntu", }, { @@ -283,11 +283,11 @@ func (ts *localState) NetMap() *netmap.NetworkMap { } } -func (ts *localState) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) { - return &tailcfg.Node{ +func (ts *localState) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) { + return (&tailcfg.Node{ ID: 2, StableID: "peer-id", - }, tailcfg.UserProfile{ + }).View(), tailcfg.UserProfile{ LoginName: "peer", }, true @@ -861,7 +861,7 @@ func TestSSH(t *testing.T) { sshUser: "test", src: netip.MustParseAddrPort("1.2.3.4:32342"), dst: netip.MustParseAddrPort("1.2.3.5:22"), - node: &tailcfg.Node{}, + node: (&tailcfg.Node{}).View(), uprof: tailcfg.UserProfile{}, } sc.action0 = &tailcfg.SSHAction{Accept: true} diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 09bd37cea..77b4995e4 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -388,6 +388,12 @@ func (n *Node) IsTagged() bool { return len(n.Tags) > 0 } +// IsTagged reports whether the node has any tags. +func (n NodeView) IsTagged() bool { return n.ж.IsTagged() } + +// DisplayName wraps Node.DisplayName. +func (n NodeView) DisplayName(forOwner bool) string { return n.ж.DisplayName(forOwner) } + // InitDisplayNames computes and populates n's display name // fields: n.ComputedName, n.computedHostIfDifferent, and // n.ComputedNameWithHost. diff --git a/types/netmap/netmap.go b/types/netmap/netmap.go index e521e0064..e0ab02de8 100644 --- a/types/netmap/netmap.go +++ b/types/netmap/netmap.go @@ -34,7 +34,7 @@ type NetworkMap struct { Addresses []netip.Prefix // same as tailcfg.Node.Addresses (IP addresses of this Node directly) MachineStatus tailcfg.MachineStatus MachineKey key.MachinePublic - Peers []*tailcfg.Node // sorted by Node.ID + Peers []tailcfg.NodeView // sorted by Node.ID DNS tailcfg.DNSConfig // TODO(maisem) : replace with View. Hostinfo tailcfg.Hostinfo @@ -84,7 +84,7 @@ type NetworkMap struct { // AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes. func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool { for _, p := range nm.Peers { - if len(p.PrimaryRoutes) > 0 { + if p.PrimaryRoutes().Len() > 0 { return true } } @@ -94,19 +94,21 @@ func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool { // 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.Node, ok bool) { +func (nm *NetworkMap) PeerByTailscaleIP(ip netip.Addr) (peer tailcfg.NodeView, ok bool) { // TODO(bradfitz): if nm == nil { - return nil, false + return tailcfg.NodeView{}, false } for _, n := range nm.Peers { - for _, a := range n.Addresses { + ad := n.Addresses() + for i := 0; i < ad.Len(); i++ { + a := ad.At(i) if a.Addr() == ip { return n, true } } } - return nil, false + return tailcfg.NodeView{}, false } // MagicDNSSuffix returns the domain's MagicDNS suffix (even if @@ -153,13 +155,13 @@ func (nm *NetworkMap) VeryConcise() string { } // PeerWithStableID finds and returns the peer associated to the inputted StableNodeID. -func (nm *NetworkMap) PeerWithStableID(pid tailcfg.StableNodeID) (_ *tailcfg.Node, ok bool) { +func (nm *NetworkMap) PeerWithStableID(pid tailcfg.StableNodeID) (_ tailcfg.NodeView, ok bool) { for _, p := range nm.Peers { - if p.StableID == pid { + if p.StableID() == pid { return p, true } } - return nil, false + return tailcfg.NodeView{}, false } // printConciseHeader prints a concise header line representing nm to buf. @@ -203,15 +205,17 @@ func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool { // // If this function is changed to access different fields of p, keep // in nodeConciseEqual in sync. -func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) { - aip := make([]string, len(p.AllowedIPs)) - for i, a := range p.AllowedIPs { +func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) { + aip := make([]string, p.AllowedIPs().Len()) + for i := range aip { + a := p.AllowedIPs().At(i) s := strings.TrimSuffix(fmt.Sprint(a), "/32") aip[i] = s } - ep := make([]string, len(p.Endpoints)) - for i, e := range p.Endpoints { + ep := make([]string, p.Endpoints().Len()) + for i := range ep { + e := p.Endpoints().At(i) // Align vertically on the ':' between IP and port colon := strings.IndexByte(e, ':') spaces := 0 @@ -222,21 +226,21 @@ func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) { ep[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces)) } - derp := p.DERP + derp := p.DERP() const derpPrefix = "127.3.3.40:" if strings.HasPrefix(derp, derpPrefix) { derp = "D" + derp[len(derpPrefix):] } var discoShort string - if !p.DiscoKey.IsZero() { - discoShort = p.DiscoKey.ShortString() + " " + 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(), + p.Key().ShortString(), discoShort, derp, strings.Join(aip, " "), @@ -244,12 +248,12 @@ func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) { } // nodeConciseEqual reports whether a and b are equal for the fields accessed by printPeerConcise. -func nodeConciseEqual(a, b *tailcfg.Node) bool { - return a.Key == b.Key && - a.DERP == b.DERP && - a.DiscoKey == b.DiscoKey && - eqCIDRsIgnoreNil(a.AllowedIPs, b.AllowedIPs) && - eqStringsIgnoreNil(a.Endpoints, b.Endpoints) +func nodeConciseEqual(a, b tailcfg.NodeView) bool { + return a.Key() == b.Key() && + a.DERP() == b.DERP() && + a.DiscoKey() == b.DiscoKey() && + eqViewsIgnoreNil(a.AllowedIPs(), b.AllowedIPs()) && + eqViewsIgnoreNil(a.Endpoints(), b.Endpoints()) } func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string { @@ -268,7 +272,7 @@ func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string { for len(aps) > 0 && len(bps) > 0 { pa, pb := aps[0], bps[0] switch { - case pa.ID == pb.ID: + case pa.ID() == pb.ID(): if !nodeConciseEqual(pa, pb) { diff.WriteByte('-') printPeerConcise(&diff, pa) @@ -276,12 +280,12 @@ func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string { printPeerConcise(&diff, pb) } aps, bps = aps[1:], bps[1:] - case pa.ID > pb.ID: + case pa.ID() > pb.ID(): // New peer in b. diff.WriteByte('+') printPeerConcise(&diff, pb) bps = bps[1:] - case pb.ID > pa.ID: + case pb.ID() > pa.ID(): // Deleted peer in b. diff.WriteByte('-') printPeerConcise(&diff, pa) @@ -316,28 +320,18 @@ const ( AllowSubnetRoutes ) -// eqStringsIgnoreNil reports whether a and b have the same length and -// contents, but ignore whether a or b are nil. -func eqStringsIgnoreNil(a, b []string) bool { - if len(a) != len(b) { +// eqViewsIgnoreNil reports whether a and b have the same length and comparably +// equal values at each index. It's used for comparing views of slices and not +// caring about whether the slices are nil or not. +func eqViewsIgnoreNil[T comparable](a, b interface { + Len() int + At(int) T +}) bool { + if a.Len() != b.Len() { return false } - for i, v := range a { - if v != b[i] { - return false - } - } - return true -} - -// eqCIDRsIgnoreNil reports whether a and b have the same length and -// contents, but ignore whether a or b are nil. -func eqCIDRsIgnoreNil(a, b []netip.Prefix) bool { - if len(a) != len(b) { - return false - } - for i, v := range a { - if v != b[i] { + for i, n := 0, a.Len(); i < n; i++ { + if a.At(i) != b.At(i) { return false } } diff --git a/types/netmap/netmap_test.go b/types/netmap/netmap_test.go index a8eba7286..cf09eaef1 100644 --- a/types/netmap/netmap_test.go +++ b/types/netmap/netmap_test.go @@ -34,6 +34,14 @@ func testDiscoKey(hexPrefix string) (ret key.DiscoPublic) { return key.DiscoPublicFromRaw32(mem.B(bs[:])) } +func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView { + nv := make([]tailcfg.NodeView, len(v)) + for i, n := range v { + nv[i] = n.View() + } + return nv +} + func TestNetworkMapConcise(t *testing.T) { for _, tt := range []struct { name string @@ -44,7 +52,7 @@ func TestNetworkMapConcise(t *testing.T) { name: "basic", nm: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: testNodeKey(2), DERP: "127.3.3.40:2", @@ -55,7 +63,7 @@ func TestNetworkMapConcise(t *testing.T) { DERP: "127.3.3.40:4", Endpoints: []string{"10.2.0.100:12", "10.1.0.100:12345"}, }, - }, + }), }, want: "netmap: self: [AQEBA] auth=machine-unknown u=? []\n [AgICA] D2 : 192.168.0.100:12 192.168.0.100:12354\n [AwMDA] D4 : 10.2.0.100:12 10.1.0.100:12345\n", }, @@ -83,23 +91,23 @@ func TestConciseDiffFrom(t *testing.T) { name: "no_change", a: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: testNodeKey(2), DERP: "127.3.3.40:2", Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, }, - }, + }), }, b: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: testNodeKey(2), DERP: "127.3.3.40:2", Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, }, - }, + }), }, want: "", }, @@ -107,23 +115,23 @@ func TestConciseDiffFrom(t *testing.T) { name: "header_change", a: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: testNodeKey(2), DERP: "127.3.3.40:2", Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, }, - }, + }), }, b: &NetworkMap{ NodeKey: testNodeKey(2), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: testNodeKey(2), DERP: "127.3.3.40:2", Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, }, - }, + }), }, want: "-netmap: self: [AQEBA] auth=machine-unknown u=? []\n+netmap: self: [AgICA] auth=machine-unknown u=? []\n", }, @@ -131,18 +139,18 @@ func TestConciseDiffFrom(t *testing.T) { name: "peer_add", a: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { ID: 2, Key: testNodeKey(2), DERP: "127.3.3.40:2", Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, }, - }, + }), }, b: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { ID: 1, Key: testNodeKey(1), @@ -161,7 +169,7 @@ func TestConciseDiffFrom(t *testing.T) { DERP: "127.3.3.40:3", Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, }, - }, + }), }, want: "+ [AQEBA] D1 : 192.168.0.100:12 192.168.0.100:12354\n+ [AwMDA] D3 : 192.168.0.100:12 192.168.0.100:12354\n", }, @@ -169,7 +177,7 @@ func TestConciseDiffFrom(t *testing.T) { name: "peer_remove", a: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { ID: 1, Key: testNodeKey(1), @@ -188,18 +196,18 @@ func TestConciseDiffFrom(t *testing.T) { DERP: "127.3.3.40:3", Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, }, - }, + }), }, b: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { ID: 2, Key: testNodeKey(2), DERP: "127.3.3.40:2", Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, }, - }, + }), }, want: "- [AQEBA] D1 : 192.168.0.100:12 192.168.0.100:12354\n- [AwMDA] D3 : 192.168.0.100:12 192.168.0.100:12354\n", }, @@ -207,25 +215,25 @@ func TestConciseDiffFrom(t *testing.T) { name: "peer_port_change", a: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { ID: 2, Key: testNodeKey(2), DERP: "127.3.3.40:2", Endpoints: []string{"192.168.0.100:12", "1.1.1.1:1"}, }, - }, + }), }, b: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { ID: 2, Key: testNodeKey(2), DERP: "127.3.3.40:2", Endpoints: []string{"192.168.0.100:12", "1.1.1.1:2"}, }, - }, + }), }, want: "- [AgICA] D2 : 192.168.0.100:12 1.1.1.1:1 \n+ [AgICA] D2 : 192.168.0.100:12 1.1.1.1:2 \n", }, @@ -233,7 +241,7 @@ func TestConciseDiffFrom(t *testing.T) { name: "disco_key_only_change", a: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { ID: 2, Key: testNodeKey(2), @@ -242,11 +250,11 @@ func TestConciseDiffFrom(t *testing.T) { DiscoKey: testDiscoKey("f00f00f00f"), AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)}, }, - }, + }), }, b: &NetworkMap{ NodeKey: testNodeKey(1), - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { ID: 2, Key: testNodeKey(2), @@ -255,7 +263,7 @@ func TestConciseDiffFrom(t *testing.T) { DiscoKey: testDiscoKey("ba4ba4ba4b"), AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)}, }, - }, + }), }, want: "- [AgICA] d:f00f00f00f000000 D2 100.102.103.104 : 192.168.0.100:41641 1.1.1.1:41641\n+ [AgICA] d:ba4ba4ba4b000000 D2 100.102.103.104 : 192.168.0.100:41641 1.1.1.1:41641\n", }, diff --git a/wgengine/bench/wg.go b/wgengine/bench/wg.go index 5bd722225..7231267d8 100644 --- a/wgengine/bench/wg.go +++ b/wgengine/bench/wg.go @@ -96,7 +96,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip. eps = append(eps, ep.Addr.String()) } - n := tailcfg.Node{ + n := &tailcfg.Node{ ID: tailcfg.NodeID(0), Name: "n1", Addresses: []netip.Prefix{a1}, @@ -106,7 +106,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip. e2.SetNetworkMap(&netmap.NetworkMap{ NodeKey: k2.Public(), PrivateKey: k2, - Peers: []*tailcfg.Node{&n}, + Peers: []tailcfg.NodeView{n.View()}, }) p := wgcfg.Peer{ @@ -133,7 +133,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip. eps = append(eps, ep.Addr.String()) } - n := tailcfg.Node{ + n := &tailcfg.Node{ ID: tailcfg.NodeID(0), Name: "n2", Addresses: []netip.Prefix{a2}, @@ -143,7 +143,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip. e1.SetNetworkMap(&netmap.NetworkMap{ NodeKey: k1.Public(), PrivateKey: k1, - Peers: []*tailcfg.Node{&n}, + Peers: []tailcfg.NodeView{n.View()}, }) p := wgcfg.Peer{ diff --git a/wgengine/magicsock/debughttp.go b/wgengine/magicsock/debughttp.go index f3b61bca4..a1f17b560 100644 --- a/wgengine/magicsock/debughttp.go +++ b/wgengine/magicsock/debughttp.go @@ -101,10 +101,10 @@ func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) { } sort.Slice(ent, func(i, j int) bool { return ent[i].pub.Less(ent[j].pub) }) - peers := map[key.NodePublic]*tailcfg.Node{} + peers := map[key.NodePublic]tailcfg.NodeView{} if c.netMap != nil { for _, p := range c.netMap.Peers { - peers[p.Key] = p + peers[p.Key()] = p } } @@ -187,15 +187,15 @@ func printEndpointHTML(w io.Writer, ep *endpoint) { } -func peerDebugName(p *tailcfg.Node) string { - if p == nil { +func peerDebugName(p tailcfg.NodeView) string { + if !p.Valid() { return "" } - n := p.Name + n := p.Name() if base, _, ok := strings.Cut(n, "."); ok { return base } - return p.Hostinfo.Hostname() + return p.Hostinfo().Hostname() } func ipPortLess(a, b netip.AddrPort) bool { diff --git a/wgengine/magicsock/endpoint.go b/wgengine/magicsock/endpoint.go index b5eada402..5e9a28583 100644 --- a/wgengine/magicsock/endpoint.go +++ b/wgengine/magicsock/endpoint.go @@ -725,15 +725,15 @@ func (de *endpoint) setLastPing(ipp netip.AddrPort, now mono.Time) { // updateFromNode updates the endpoint based on a tailcfg.Node from a NetMap // update. -func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) { - if n == nil { +func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool) { + if !n.Valid() { panic("nil node when updating endpoint") } de.mu.Lock() defer de.mu.Unlock() de.heartbeatDisabled = heartbeatDisabled - de.expired = n.Expired + de.expired = n.Expired() epDisco := de.disco.Load() var discoKey key.DiscoPublic @@ -741,11 +741,11 @@ func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) { discoKey = epDisco.key } - if discoKey != n.DiscoKey { - de.c.logf("[v1] magicsock: disco: node %s changed from %s to %s", de.publicKey.ShortString(), discoKey, n.DiscoKey) + if discoKey != n.DiscoKey() { + de.c.logf("[v1] magicsock: disco: node %s changed from %s to %s", de.publicKey.ShortString(), discoKey, n.DiscoKey()) de.disco.Store(&endpointDisco{ - key: n.DiscoKey, - short: n.DiscoKey.ShortString(), + key: n.DiscoKey(), + short: n.DiscoKey().ShortString(), }) de.debugUpdates.Add(EndpointChange{ When: time.Now(), @@ -753,7 +753,7 @@ func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) { }) de.resetLocked() } - if n.DERP == "" { + if n.DERP() == "" { if de.derpAddr.IsValid() { de.debugUpdates.Add(EndpointChange{ When: time.Now(), @@ -763,7 +763,7 @@ func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) { } de.derpAddr = netip.AddrPort{} } else { - newDerp, _ := netip.ParseAddrPort(n.DERP) + newDerp, _ := netip.ParseAddrPort(n.DERP()) if de.derpAddr != newDerp { de.debugUpdates.Add(EndpointChange{ When: time.Now(), @@ -780,7 +780,8 @@ func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) { } var newIpps []netip.AddrPort - for i, epStr := range n.Endpoints { + for i := range n.Endpoints().LenIter() { + epStr := n.Endpoints().At(i) if i > math.MaxInt16 { // Seems unlikely. continue diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 81975fac8..2e550ed23 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -711,7 +711,7 @@ func (c *Conn) LastRecvActivityOfNodeKey(nk key.NodePublic) string { } // Ping handles a "tailscale ping" CLI query. -func (c *Conn) Ping(peer *tailcfg.Node, res *ipnstate.PingResult, size int, cb func(*ipnstate.PingResult)) { +func (c *Conn) Ping(peer tailcfg.NodeView, res *ipnstate.PingResult, size int, cb func(*ipnstate.PingResult)) { c.mu.Lock() defer c.mu.Unlock() if c.privateKey.IsZero() { @@ -719,17 +719,17 @@ func (c *Conn) Ping(peer *tailcfg.Node, res *ipnstate.PingResult, size int, cb f cb(res) return } - if len(peer.Addresses) > 0 { - res.NodeIP = peer.Addresses[0].Addr().String() + if peer.Addresses().Len() > 0 { + res.NodeIP = peer.Addresses().At(0).Addr().String() } - res.NodeName = peer.Name // prefer DNS name + res.NodeName = peer.Name() // prefer DNS name if res.NodeName == "" { - res.NodeName = peer.Hostinfo.Hostname() // else hostname + res.NodeName = peer.Hostinfo().Hostname() // else hostname } else { res.NodeName, _, _ = strings.Cut(res.NodeName, ".") } - ep, ok := c.peerMap.endpointForNodeKey(peer.Key) + ep, ok := c.peerMap.endpointForNodeKey(peer.Key()) if !ok { res.Err = "unknown peer" cb(res) @@ -753,13 +753,13 @@ func (c *Conn) populateCLIPingResponseLocked(res *ipnstate.PingResult, latency t // GetEndpointChanges returns the most recent changes for a particular // endpoint. The returned EndpointChange structs are for debug use only and // there are no guarantees about order, size, or content. -func (c *Conn) GetEndpointChanges(peer *tailcfg.Node) ([]EndpointChange, error) { +func (c *Conn) GetEndpointChanges(peer tailcfg.NodeView) ([]EndpointChange, error) { c.mu.Lock() if c.privateKey.IsZero() { c.mu.Unlock() return nil, fmt.Errorf("tailscaled stopped") } - ep, ok := c.peerMap.endpointForNodeKey(peer.Key) + ep, ok := c.peerMap.endpointForNodeKey(peer.Key()) c.mu.Unlock() if !ok { @@ -1729,7 +1729,7 @@ func (c *Conn) UpdatePeers(newPeers map[key.NodePublic]struct{}) { } } -func nodesEqual(x, y []*tailcfg.Node) bool { +func nodesEqual(x, y []tailcfg.NodeView) bool { if len(x) != len(y) { return false } @@ -1800,8 +1800,8 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) { // we'll fall through to the next pass, which allocates but can // handle full set updates. for _, n := range nm.Peers { - if ep, ok := c.peerMap.endpointForNodeKey(n.Key); ok { - if n.DiscoKey.IsZero() && !n.IsWireGuardOnly { + if ep, ok := c.peerMap.endpointForNodeKey(n.Key()); ok { + if n.DiscoKey().IsZero() && !n.IsWireGuardOnly() { // Discokey transitioned from non-zero to zero? This should not // happen in the wild, however it could mean: // 1. A node was downgraded from post 0.100 to pre 0.100. @@ -1820,7 +1820,7 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) { c.peerMap.upsertEndpoint(ep, oldDiscoKey) // maybe update discokey mappings in peerMap continue } - if n.DiscoKey.IsZero() && !n.IsWireGuardOnly { + if n.DiscoKey().IsZero() && !n.IsWireGuardOnly() { // Ancient pre-0.100 node, which does not have a disco key, and will only be reachable via DERP. continue } @@ -1828,30 +1828,30 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) { ep := &endpoint{ c: c, debugUpdates: ringbuffer.New[EndpointChange](entriesPerBuffer), - publicKey: n.Key, - publicKeyHex: n.Key.UntypedHexString(), + publicKey: n.Key(), + publicKeyHex: n.Key().UntypedHexString(), sentPing: map[stun.TxID]sentPing{}, endpointState: map[netip.AddrPort]*endpointState{}, heartbeatDisabled: heartbeatDisabled, - isWireguardOnly: n.IsWireGuardOnly, + isWireguardOnly: n.IsWireGuardOnly(), } - if len(n.Addresses) > 0 { - ep.nodeAddr = n.Addresses[0].Addr() + if n.Addresses().Len() > 0 { + ep.nodeAddr = n.Addresses().At(0).Addr() } ep.initFakeUDPAddr() - if n.DiscoKey.IsZero() { + if n.DiscoKey().IsZero() { ep.disco.Store(nil) } else { ep.disco.Store(&endpointDisco{ - key: n.DiscoKey, - short: n.DiscoKey.ShortString(), + key: n.DiscoKey(), + short: n.DiscoKey().ShortString(), }) if debugDisco() { // rather than making a new knob - c.logf("magicsock: created endpoint key=%s: disco=%s; %v", n.Key.ShortString(), n.DiscoKey.ShortString(), logger.ArgWriter(func(w *bufio.Writer) { + c.logf("magicsock: created endpoint key=%s: disco=%s; %v", n.Key().ShortString(), n.DiscoKey().ShortString(), logger.ArgWriter(func(w *bufio.Writer) { const derpPrefix = "127.3.3.40:" - if strings.HasPrefix(n.DERP, derpPrefix) { - ipp, _ := netip.ParseAddrPort(n.DERP) + if strings.HasPrefix(n.DERP(), derpPrefix) { + ipp, _ := netip.ParseAddrPort(n.DERP()) regionID := int(ipp.Port()) code := c.derpRegionCodeLocked(regionID) if code != "" { @@ -1860,14 +1860,16 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) { fmt.Fprintf(w, "derp=%v%s ", regionID, code) } - for _, a := range n.AllowedIPs { + for i := range n.AllowedIPs().LenIter() { + a := n.AllowedIPs().At(i) if a.IsSingleIP() { fmt.Fprintf(w, "aip=%v ", a.Addr()) } else { fmt.Fprintf(w, "aip=%v ", a) } } - for _, ep := range n.Endpoints { + for i := range n.Endpoints().LenIter() { + ep := n.Endpoints().At(i) fmt.Fprintf(w, "ep=%v ", ep) } })) @@ -1885,7 +1887,7 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) { if c.peerMap.nodeCount() != len(nm.Peers) { keep := make(map[key.NodePublic]bool, len(nm.Peers)) for _, n := range nm.Peers { - keep[n.Key] = true + keep[n.Key()] = true } c.peerMap.forEachEndpoint(func(ep *endpoint) { if !keep[ep.publicKey] { diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index c1707a218..5bc68cf37 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -284,7 +284,7 @@ func meshStacks(logf logger.Logf, mutateNetmap func(idx int, nm *netmap.NetworkM Endpoints: epStrings(eps[i]), DERP: "127.3.3.40:1", } - nm.Peers = append(nm.Peers, peer) + nm.Peers = append(nm.Peers, peer.View()) } if mutateNetmap != nil { @@ -304,7 +304,7 @@ func meshStacks(logf logger.Logf, mutateNetmap func(idx int, nm *netmap.NetworkM m.conn.SetNetworkMap(nm) peerSet := make(map[key.NodePublic]struct{}, len(nm.Peers)) for _, peer := range nm.Peers { - peerSet[peer.Key] = struct{}{} + peerSet[peer.Key()] = struct{}{} } m.conn.UpdatePeers(peerSet) wg, err := nmcfg.WGCfg(nm, logf, netmap.AllowSingleHosts, "") @@ -657,7 +657,9 @@ func TestDiscokeyChange(t *testing.T) { } mu.Lock() defer mu.Unlock() - nm.Peers[0].DiscoKey = m1DiscoKey + mut := nm.Peers[0].AsStruct() + mut.DiscoKey = m1DiscoKey + nm.Peers[0] = mut.View() } cleanupMesh := meshStacks(t.Logf, setm1Key, m1, m2) @@ -1248,13 +1250,13 @@ func addTestEndpoint(tb testing.TB, conn *Conn, sendConn net.PacketConn) (key.No discoKey := key.DiscoPublicFromRaw32(mem.B([]byte{31: 1})) nodeKey := key.NodePublicFromRaw32(mem.B([]byte{0: 'N', 1: 'K', 31: 0})) conn.SetNetworkMap(&netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: nodeKey, DiscoKey: discoKey, Endpoints: []string{sendConn.LocalAddr().String()}, }, - }, + }), }) conn.SetPrivateKey(key.NodePrivateFromRaw32(mem.B([]byte{0: 1, 31: 0}))) _, err := conn.ParseEndpoint(nodeKey.UntypedHexString()) @@ -1427,6 +1429,14 @@ func BenchmarkReceiveFrom_Native(b *testing.B) { } } +func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView { + nv := make([]tailcfg.NodeView, len(v)) + for i, n := range v { + nv[i] = n.View() + } + return nv +} + // Test that a netmap update where node changes its node key but // doesn't change its disco key doesn't result in a broken state. // @@ -1444,13 +1454,13 @@ func TestSetNetworkMapChangingNodeKey(t *testing.T) { nodeKey2 := key.NodePublicFromRaw32(mem.B([]byte{0: 'N', 1: 'K', 2: '2', 31: 0})) conn.SetNetworkMap(&netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: nodeKey1, DiscoKey: discoKey, Endpoints: []string{"192.168.1.2:345"}, }, - }, + }), }) _, err := conn.ParseEndpoint(nodeKey1.UntypedHexString()) if err != nil { @@ -1459,13 +1469,13 @@ func TestSetNetworkMapChangingNodeKey(t *testing.T) { for i := 0; i < 3; i++ { conn.SetNetworkMap(&netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: nodeKey2, DiscoKey: discoKey, Endpoints: []string{"192.168.1.2:345"}, }, - }, + }), }) } @@ -1792,7 +1802,7 @@ func TestStressSetNetworkMap(t *testing.T) { } // Set the netmap. conn.SetNetworkMap(&netmap.NetworkMap{ - Peers: peers, + Peers: nodeViews(peers), }) // Check invariants. if err := conn.peerMap.validate(); err != nil { @@ -2237,7 +2247,7 @@ func TestIsWireGuardOnlyPeer(t *testing.T) { PrivateKey: m.privateKey, NodeKey: m.privateKey.Public(), Addresses: []netip.Prefix{tsaip}, - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: wgkey.Public(), Endpoints: []string{wgEp.String()}, @@ -2245,7 +2255,7 @@ func TestIsWireGuardOnlyPeer(t *testing.T) { Addresses: []netip.Prefix{wgaip}, AllowedIPs: []netip.Prefix{wgaip}, }, - }, + }), } m.conn.SetNetworkMap(nm) @@ -2295,7 +2305,7 @@ func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) { PrivateKey: m.privateKey, NodeKey: m.privateKey.Public(), Addresses: []netip.Prefix{tsaip}, - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: wgkey.Public(), Endpoints: []string{wgEp.String()}, @@ -2304,7 +2314,7 @@ func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) { AllowedIPs: []netip.Prefix{wgaip}, SelfNodeV4MasqAddrForThisPeer: ptr.To(masqip.Addr()), }, - }, + }), } m.conn.SetNetworkMap(nm) @@ -2421,7 +2431,7 @@ func TestIsWireGuardOnlyPickEndpointByPing(t *testing.T) { PrivateKey: m.privateKey, NodeKey: m.privateKey.Public(), Addresses: []netip.Prefix{tsaip}, - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: wgkey.Public(), Endpoints: []string{wgEp.String(), wgEp2.String(), wgEpV6.String()}, @@ -2429,7 +2439,7 @@ func TestIsWireGuardOnlyPickEndpointByPing(t *testing.T) { Addresses: []netip.Prefix{wgaip}, AllowedIPs: []netip.Prefix{wgaip}, }, - }, + }), } applyNetworkMap(t, m, nm) diff --git a/wgengine/pendopen.go b/wgengine/pendopen.go index d4ba30a99..af5d3d8a7 100644 --- a/wgengine/pendopen.go +++ b/wgengine/pendopen.go @@ -146,21 +146,22 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) { return } n := pip.Node - if !n.IsWireGuardOnly { - if n.DiscoKey.IsZero() { - e.logf("open-conn-track: timeout opening %v; peer node %v running pre-0.100", flow, n.Key.ShortString()) + if !n.IsWireGuardOnly() { + if n.DiscoKey().IsZero() { + e.logf("open-conn-track: timeout opening %v; peer node %v running pre-0.100", flow, n.Key().ShortString()) return } - if n.DERP == "" { - e.logf("open-conn-track: timeout opening %v; peer node %v not connected to any DERP relay", flow, n.Key.ShortString()) + if n.DERP() == "" { + e.logf("open-conn-track: timeout opening %v; peer node %v not connected to any DERP relay", flow, n.Key().ShortString()) return } } - ps, found := e.getPeerStatusLite(n.Key) + ps, found := e.getPeerStatusLite(n.Key()) if !found { onlyZeroRoute := true // whether peerForIP returned n only because its /0 route matched - for _, r := range n.AllowedIPs { + for i := range n.AllowedIPs().LenIter() { + r := n.AllowedIPs().At(i) if r.Bits() != 0 && r.Contains(flow.Dst.Addr()) { onlyZeroRoute = false break @@ -178,7 +179,7 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) { // node. return } - e.logf("open-conn-track: timeout opening %v; target node %v in netmap but unknown to WireGuard", flow, n.Key.ShortString()) + e.logf("open-conn-track: timeout opening %v; target node %v in netmap but unknown to WireGuard", flow, n.Key().ShortString()) return } @@ -189,24 +190,24 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) { _ = ps.LastHandshake online := "?" - if n.IsWireGuardOnly { + if n.IsWireGuardOnly() { online = "wg" } else { - if n.Online != nil { - if *n.Online { + if v := n.Online(); v != nil { + if *v { online = "yes" } else { online = "no" } } - if n.LastSeen != nil && online != "yes" { - online += fmt.Sprintf(", lastseen=%v", durFmt(*n.LastSeen)) + if n.LastSeen() != nil && online != "yes" { + online += fmt.Sprintf(", lastseen=%v", durFmt(*n.LastSeen())) } } e.logf("open-conn-track: timeout opening %v to node %v; online=%v, lastRecv=%v", - flow, n.Key.ShortString(), + flow, n.Key().ShortString(), online, - e.magicConn.LastRecvActivityOfNodeKey(n.Key)) + e.magicConn.LastRecvActivityOfNodeKey(n.Key())) } func durFmt(t time.Time) string { diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 84a3a5b1f..b57510e96 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -1226,7 +1226,7 @@ func (e *userspaceEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, size in } peer := pip.Node - e.logf("ping(%v): sending %v ping to %v %v ...", ip, pingType, peer.Key.ShortString(), peer.ComputedName) + e.logf("ping(%v): sending %v ping to %v %v ...", ip, pingType, peer.Key().ShortString(), peer.ComputedName()) switch pingType { case "disco": e.magicConn.Ping(peer, res, size, cb) @@ -1254,7 +1254,7 @@ func (e *userspaceEngine) mySelfIPMatchingFamily(dst netip.Addr) (src netip.Addr return netip.Addr{}, errors.New("no self address in netmap matching address family") } -func (e *userspaceEngine) sendICMPEchoRequest(destIP netip.Addr, peer *tailcfg.Node, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) { +func (e *userspaceEngine) sendICMPEchoRequest(destIP netip.Addr, peer tailcfg.NodeView, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) { srcIP, err := e.mySelfIPMatchingFamily(destIP) if err != nil { res.Err = err.Error() @@ -1295,7 +1295,7 @@ func (e *userspaceEngine) sendICMPEchoRequest(destIP netip.Addr, peer *tailcfg.N d := time.Since(t0) res.LatencySeconds = d.Seconds() res.NodeIP = destIP.String() - res.NodeName = peer.ComputedName + res.NodeName = peer.ComputedName() cb(res) }) @@ -1303,7 +1303,7 @@ func (e *userspaceEngine) sendICMPEchoRequest(destIP netip.Addr, peer *tailcfg.N e.tundev.InjectOutbound(icmpPing) } -func (e *userspaceEngine) sendTSMPPing(ip netip.Addr, peer *tailcfg.Node, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) { +func (e *userspaceEngine) sendTSMPPing(ip netip.Addr, peer tailcfg.NodeView, res *ipnstate.PingResult, cb func(*ipnstate.PingResult)) { srcIP, err := e.mySelfIPMatchingFamily(ip) if err != nil { res.Err = err.Error() @@ -1337,7 +1337,7 @@ func (e *userspaceEngine) sendTSMPPing(ip netip.Addr, peer *tailcfg.Node, res *i d := time.Since(t0) res.LatencySeconds = d.Seconds() res.NodeIP = ip.String() - res.NodeName = peer.ComputedName + res.NodeName = peer.ComputedName() res.PeerAPIPort = pong.PeerAPIPort cb(res) }) @@ -1436,7 +1436,8 @@ func (e *userspaceEngine) PeerForIP(ip netip.Addr) (ret PeerForIP, ok bool) { // Check for exact matches before looking for subnet matches. // TODO(bradfitz): add maps for these. on NetworkMap? for _, p := range nm.Peers { - for _, a := range p.Addresses { + for i := range p.Addresses().LenIter() { + a := p.Addresses().At(i) if a.Addr() == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) { return PeerForIP{Node: p, Route: a}, true } @@ -1444,7 +1445,7 @@ func (e *userspaceEngine) PeerForIP(ip netip.Addr) (ret PeerForIP, ok bool) { } for _, a := range nm.Addresses { if a.Addr() == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) { - return PeerForIP{Node: nm.SelfNode, IsSelf: true, Route: a}, true + return PeerForIP{Node: nm.SelfNode.View(), IsSelf: true, Route: a}, true } } @@ -1469,7 +1470,7 @@ func (e *userspaceEngine) PeerForIP(ip netip.Addr) (ret PeerForIP, ok bool) { // call. But TODO(bradfitz): add a lookup map to netmap.NetworkMap. if !bestKey.IsZero() { for _, p := range nm.Peers { - if p.Key == bestKey { + if p.Key() == bestKey { return PeerForIP{Node: p, Route: best}, true } } diff --git a/wgengine/userspace_test.go b/wgengine/userspace_test.go index 07bea455b..34c1bd93b 100644 --- a/wgengine/userspace_test.go +++ b/wgengine/userspace_test.go @@ -84,6 +84,14 @@ func TestNoteReceiveActivity(t *testing.T) { } } +func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView { + nv := make([]tailcfg.NodeView, len(v)) + for i, n := range v { + nv[i] = n.View() + } + return nv +} + func TestUserspaceEngineReconfig(t *testing.T) { e, err := NewFakeUserspaceEngine(t.Logf, 0) if err != nil { @@ -99,11 +107,11 @@ func TestUserspaceEngineReconfig(t *testing.T) { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", } { nm := &netmap.NetworkMap{ - Peers: []*tailcfg.Node{ + Peers: nodeViews([]*tailcfg.Node{ { Key: nkFromHex(nodeHex), }, - }, + }), } nk, err := key.ParseNodePublicUntyped(mem.S(nodeHex)) if err != nil { diff --git a/wgengine/wgcfg/nmcfg/nmcfg.go b/wgengine/wgcfg/nmcfg/nmcfg.go index 66b246ba9..46a68e24d 100644 --- a/wgengine/wgcfg/nmcfg/nmcfg.go +++ b/wgengine/wgcfg/nmcfg/nmcfg.go @@ -19,30 +19,31 @@ import ( "tailscale.com/wgengine/wgcfg" ) -func nodeDebugName(n *tailcfg.Node) string { - name := n.Name +func nodeDebugName(n tailcfg.NodeView) string { + name := n.Name() if name == "" { - name = n.Hostinfo.Hostname() + name = n.Hostinfo().Hostname() } if i := strings.Index(name, "."); i != -1 { name = name[:i] } - if name == "" && len(n.Addresses) != 0 { - return n.Addresses[0].String() + if name == "" && n.Addresses().Len() != 0 { + return n.Addresses().At(0).String() } return name } // cidrIsSubnet reports whether cidr is a non-default-route subnet // exported by node that is not one of its own self addresses. -func cidrIsSubnet(node *tailcfg.Node, cidr netip.Prefix) bool { +func cidrIsSubnet(node tailcfg.NodeView, cidr netip.Prefix) bool { if cidr.Bits() == 0 { return false } if !cidr.IsSingleIP() { return true } - for _, selfCIDR := range node.Addresses { + for i := range node.Addresses().LenIter() { + selfCIDR := node.Addresses().At(i) if cidr == selfCIDR { return false } @@ -85,22 +86,23 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags, skippedSubnets := new(bytes.Buffer) for _, peer := range nm.Peers { - if peer.DiscoKey.IsZero() && peer.DERP == "" && !peer.IsWireGuardOnly { + if peer.DiscoKey().IsZero() && peer.DERP() == "" && !peer.IsWireGuardOnly() { // Peer predates both DERP and active discovery, we cannot // communicate with it. - logf("[v1] wgcfg: skipped peer %s, doesn't offer DERP or disco", peer.Key.ShortString()) + logf("[v1] wgcfg: skipped peer %s, doesn't offer DERP or disco", peer.Key().ShortString()) continue } cfg.Peers = append(cfg.Peers, wgcfg.Peer{ - PublicKey: peer.Key, - DiscoKey: peer.DiscoKey, + PublicKey: peer.Key(), + DiscoKey: peer.DiscoKey(), }) cpeer := &cfg.Peers[len(cfg.Peers)-1] didExitNodeWarn := false - cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer - for _, allowedIP := range peer.AllowedIPs { - if allowedIP.Bits() == 0 && peer.StableID != exitNode { + cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer() + for i := range peer.AllowedIPs().LenIter() { + allowedIP := peer.AllowedIPs().At(i) + if allowedIP.Bits() == 0 && peer.StableID() != exitNode { if didExitNodeWarn { // Don't log about both the IPv4 /0 and IPv6 /0. continue @@ -109,20 +111,20 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags, if skippedUnselected.Len() > 0 { skippedUnselected.WriteString(", ") } - fmt.Fprintf(skippedUnselected, "%q (%v)", nodeDebugName(peer), peer.Key.ShortString()) + fmt.Fprintf(skippedUnselected, "%q (%v)", nodeDebugName(peer), peer.Key().ShortString()) continue } else if allowedIP.IsSingleIP() && tsaddr.IsTailscaleIP(allowedIP.Addr()) && (flags&netmap.AllowSingleHosts) == 0 { if skippedIPs.Len() > 0 { skippedIPs.WriteString(", ") } - fmt.Fprintf(skippedIPs, "%v from %q (%v)", allowedIP.Addr(), nodeDebugName(peer), peer.Key.ShortString()) + fmt.Fprintf(skippedIPs, "%v from %q (%v)", allowedIP.Addr(), nodeDebugName(peer), peer.Key().ShortString()) continue } else if cidrIsSubnet(peer, allowedIP) { if (flags & netmap.AllowSubnetRoutes) == 0 { if skippedSubnets.Len() > 0 { skippedSubnets.WriteString(", ") } - fmt.Fprintf(skippedSubnets, "%v from %q (%v)", allowedIP, nodeDebugName(peer), peer.Key.ShortString()) + fmt.Fprintf(skippedSubnets, "%v from %q (%v)", allowedIP, nodeDebugName(peer), peer.Key().ShortString()) continue } } diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index 3bbb7a895..c5b2969e7 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -52,9 +52,9 @@ var ErrNoChanges = errors.New("no changes made to Engine config") // PeerForIP is the type returned by Engine.PeerForIP. type PeerForIP struct { - // Node is the matched node. It's always non-nil when + // Node is the matched node. It's always a valid value when // Engine.PeerForIP returns ok==true. - Node *tailcfg.Node + Node tailcfg.NodeView // IsSelf is whether the Node is the local process. IsSelf bool