types/netmap, all: use read-only tailcfg.NodeView in NetworkMap

Updates #8948

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/8966/head
Brad Fitzpatrick 11 months ago committed by Brad Fitzpatrick
parent b040094b90
commit 58a4fd43d8

@ -257,21 +257,25 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
}, },
MachineStatus: jsMachineStatus[nm.MachineStatus], MachineStatus: jsMachineStatus[nm.MachineStatus],
}, },
Peers: mapSlice(nm.Peers, func(p *tailcfg.Node) jsNetMapPeerNode { Peers: mapSlice(nm.Peers, func(p tailcfg.NodeView) jsNetMapPeerNode {
name := p.Name name := p.Name()
if name == "" { if name == "" {
// In practice this should only happen for Hello. // 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{ return jsNetMapPeerNode{
jsNetMapNode: jsNetMapNode{ jsNetMapNode: jsNetMapNode{
Name: name, Name: name,
Addresses: mapSlice(p.Addresses, func(a netip.Prefix) string { return a.Addr().String() }), Addresses: addrs,
MachineKey: p.Machine.String(), MachineKey: p.Machine().String(),
NodeKey: p.Key.String(), NodeKey: p.Key().String(),
}, },
Online: p.Online, Online: p.Online(),
TailscaleSSHEnabled: p.Hostinfo.TailscaleSSHEnabled(), TailscaleSSHEnabled: p.Hostinfo().TailscaleSSHEnabled(),
} }
}), }),
LockedOut: nm.TKAEnabled && len(nm.SelfNode.KeySignature) == 0, LockedOut: nm.TKAEnabled && len(nm.SelfNode.KeySignature) == 0,

@ -144,11 +144,18 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
ms.lastTKAInfo = resp.TKAInfo 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{ nm := &netmap.NetworkMap{
NodeKey: ms.privateNodeKey.Public(), NodeKey: ms.privateNodeKey.Public(),
PrivateKey: ms.privateNodeKey, PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey, MachineKey: ms.machinePubKey,
Peers: resp.Peers, Peers: peerViews,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile), UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: ms.lastDomain, Domain: ms.lastDomain,
DomainAuditLogID: ms.lastDomainAuditLogID, DomainAuditLogID: ms.lastDomainAuditLogID,

@ -38,6 +38,14 @@ func ips(ss ...string) (ips []netip.Addr) {
return 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) { func TestDNSConfigForNetmap(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -62,7 +70,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
nm: &netmap.NetworkMap{ nm: &netmap.NetworkMap{
Name: "myname.net", Name: "myname.net",
Addresses: ipps("100.101.101.101"), Addresses: ipps("100.101.101.101"),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Name: "peera.net", Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001", "fe75::1002"), 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", Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6 Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
}, },
}, }),
}, },
prefs: &ipn.Prefs{}, prefs: &ipn.Prefs{},
want: &dns.Config{ want: &dns.Config{
@ -96,7 +104,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
nm: &netmap.NetworkMap{ nm: &netmap.NetworkMap{
Name: "myname.net", Name: "myname.net",
Addresses: ipps("fe75::1"), Addresses: ipps("fe75::1"),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Name: "peera.net", Name: "peera.net",
Addresses: ipps("100.102.0.1", "100.102.0.2", "fe75::1001"), 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", Name: "v6-only.net",
Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6 Addresses: ipps("fe75::3"), // no IPv4, so we don't ignore IPv6
}, },
}, }),
}, },
prefs: &ipn.Prefs{}, prefs: &ipn.Prefs{},
want: &dns.Config{ want: &dns.Config{

@ -87,24 +87,26 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow ti
return return
} }
for _, peer := range netmap.Peers { for i, peer := range netmap.Peers {
// Nodes that don't expire have KeyExpiry set to the zero time; // Nodes that don't expire have KeyExpiry set to the zero time;
// skip those and peers that are already marked as expired // skip those and peers that are already marked as expired
// (e.g. from control). // (e.g. from control).
if peer.KeyExpiry.IsZero() || peer.KeyExpiry.After(controlNow) { if peer.KeyExpiry().IsZero() || peer.KeyExpiry().After(controlNow) {
delete(em.previouslyExpired, peer.StableID) delete(em.previouslyExpired, peer.StableID())
continue continue
} else if peer.Expired { } else if peer.Expired() {
continue continue
} }
if !em.previouslyExpired[peer.StableID] { if !em.previouslyExpired[peer.StableID()] {
em.logf("[v1] netmap: flagExpiredPeers: clearing expired peer %v", peer.StableID) em.logf("[v1] netmap: flagExpiredPeers: clearing expired peer %v", peer.StableID())
em.previouslyExpired[peer.StableID] = true em.previouslyExpired[peer.StableID()] = true
} }
mut := peer.AsStruct()
// Actually mark the node as expired // Actually mark the node as expired
peer.Expired = true mut.Expired = true
// Control clears the Endpoints and DERP fields of expired // Control clears the Endpoints and DERP fields of expired
// nodes; do so here as well. The Expired bool is the correct // 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, // NOTE: this is insufficient to actually break connectivity,
// since we discover endpoints via DERP, and due to DERP return // since we discover endpoints via DERP, and due to DERP return
// path optimization. // path optimization.
peer.Endpoints = nil mut.Endpoints = nil
peer.DERP = "" mut.DERP = ""
// Defense-in-depth: break the node's public key as well, in // Defense-in-depth: break the node's public key as well, in
// case something tries to communicate. // 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 var nextExpiry time.Time // zero if none
for _, peer := range nm.Peers { for _, peer := range nm.Peers {
if peer.KeyExpiry.IsZero() { if peer.KeyExpiry().IsZero() {
continue // tagged node continue // tagged node
} else if peer.Expired { } else if peer.Expired() {
// Peer already expired; Expired is set by the // Peer already expired; Expired is set by the
// flagExpiredPeers function, above. // flagExpiredPeers function, above.
continue continue
} else if peer.KeyExpiry.Before(controlNow) { } else if peer.KeyExpiry().Before(controlNow) {
// This peer already expired, and peer.Expired // This peer already expired, and peer.Expired
// isn't set for some reason. Skip this node. // isn't set for some reason. Skip this node.
continue 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 // an expiry; otherwise, only update if this node's expiry is
// sooner than the currently-stored one (since we want the // sooner than the currently-stored one (since we want the
// soonest-occurring expiry time). // soonest-occurring expiry time).
if nextExpiry.IsZero() || peer.KeyExpiry.Before(nextExpiry) { if nextExpiry.IsZero() || peer.KeyExpiry().Before(nextExpiry) {
nextExpiry = peer.KeyExpiry nextExpiry = peer.KeyExpiry()
} }
} }

@ -44,38 +44,38 @@ func TestFlagExpiredPeers(t *testing.T) {
name string name string
controlTime *time.Time controlTime *time.Time
netmap *netmap.NetworkMap netmap *netmap.NetworkMap
want []*tailcfg.Node want []tailcfg.NodeView
}{ }{
{ {
name: "no_expiry", name: "no_expiry",
controlTime: &now, controlTime: &now,
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", timeInFuture), n(1, "foo", timeInFuture),
n(2, "bar", timeInFuture), n(2, "bar", timeInFuture),
}),
}, },
}, want: nodeViews([]*tailcfg.Node{
want: []*tailcfg.Node{
n(1, "foo", timeInFuture), n(1, "foo", timeInFuture),
n(2, "bar", timeInFuture), n(2, "bar", timeInFuture),
}, }),
}, },
{ {
name: "expiry", name: "expiry",
controlTime: &now, controlTime: &now,
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", timeInFuture), n(1, "foo", timeInFuture),
n(2, "bar", timeInPast), n(2, "bar", timeInPast),
}),
}, },
}, want: nodeViews([]*tailcfg.Node{
want: []*tailcfg.Node{
n(1, "foo", timeInFuture), n(1, "foo", timeInFuture),
n(2, "bar", timeInPast, func(n *tailcfg.Node) { n(2, "bar", timeInPast, func(n *tailcfg.Node) {
n.Expired = true n.Expired = true
n.Key = expiredKey n.Key = expiredKey
}), }),
}, }),
}, },
{ {
name: "bad_ControlTime", name: "bad_ControlTime",
@ -83,29 +83,29 @@ func TestFlagExpiredPeers(t *testing.T) {
controlTime: &timeBeforeEpoch, controlTime: &timeBeforeEpoch,
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", timeInFuture), n(1, "foo", timeInFuture),
n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // before ControlTime n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // before ControlTime
}),
}, },
}, want: nodeViews([]*tailcfg.Node{
want: []*tailcfg.Node{
n(1, "foo", timeInFuture), n(1, "foo", timeInFuture),
n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // should have expired, but ControlTime is before epoch n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // should have expired, but ControlTime is before epoch
}, }),
}, },
{ {
name: "tagged_node", name: "tagged_node",
controlTime: &now, controlTime: &now,
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", timeInFuture), n(1, "foo", timeInFuture),
n(2, "bar", time.Time{}), // tagged node; zero expiry n(2, "bar", time.Time{}), // tagged node; zero expiry
}),
}, },
}, want: nodeViews([]*tailcfg.Node{
want: []*tailcfg.Node{
n(1, "foo", timeInFuture), n(1, "foo", timeInFuture),
n(2, "bar", time.Time{}), // not expired n(2, "bar", time.Time{}), // not expired
}, }),
}, },
} }
for _, tt := range tests { for _, tt := range tests {
@ -147,10 +147,10 @@ func TestNextPeerExpiry(t *testing.T) {
{ {
name: "no_expiry", name: "no_expiry",
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", noExpiry), n(1, "foo", noExpiry),
n(2, "bar", noExpiry), n(2, "bar", noExpiry),
}, }),
SelfNode: n(3, "self", noExpiry), SelfNode: n(3, "self", noExpiry),
}, },
want: noExpiry, want: noExpiry,
@ -158,10 +158,10 @@ func TestNextPeerExpiry(t *testing.T) {
{ {
name: "future_expiry_from_peer", name: "future_expiry_from_peer",
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", noExpiry), n(1, "foo", noExpiry),
n(2, "bar", timeInFuture), n(2, "bar", timeInFuture),
}, }),
SelfNode: n(3, "self", noExpiry), SelfNode: n(3, "self", noExpiry),
}, },
want: timeInFuture, want: timeInFuture,
@ -169,10 +169,10 @@ func TestNextPeerExpiry(t *testing.T) {
{ {
name: "future_expiry_from_self", name: "future_expiry_from_self",
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", noExpiry), n(1, "foo", noExpiry),
n(2, "bar", noExpiry), n(2, "bar", noExpiry),
}, }),
SelfNode: n(3, "self", timeInFuture), SelfNode: n(3, "self", timeInFuture),
}, },
want: timeInFuture, want: timeInFuture,
@ -180,10 +180,10 @@ func TestNextPeerExpiry(t *testing.T) {
{ {
name: "future_expiry_from_multiple_peers", name: "future_expiry_from_multiple_peers",
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", timeInFuture), n(1, "foo", timeInFuture),
n(2, "bar", timeInMoreFuture), n(2, "bar", timeInMoreFuture),
}, }),
SelfNode: n(3, "self", noExpiry), SelfNode: n(3, "self", noExpiry),
}, },
want: timeInFuture, want: timeInFuture,
@ -191,9 +191,9 @@ func TestNextPeerExpiry(t *testing.T) {
{ {
name: "future_expiry_from_peer_and_self", name: "future_expiry_from_peer_and_self",
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", timeInMoreFuture), n(1, "foo", timeInMoreFuture),
}, }),
SelfNode: n(2, "self", timeInFuture), SelfNode: n(2, "self", timeInFuture),
}, },
want: timeInFuture, want: timeInFuture,
@ -201,7 +201,7 @@ func TestNextPeerExpiry(t *testing.T) {
{ {
name: "only_self", name: "only_self",
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{}, Peers: nodeViews([]*tailcfg.Node{}),
SelfNode: n(1, "self", timeInFuture), SelfNode: n(1, "self", timeInFuture),
}, },
want: timeInFuture, want: timeInFuture,
@ -209,9 +209,9 @@ func TestNextPeerExpiry(t *testing.T) {
{ {
name: "peer_already_expired", name: "peer_already_expired",
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", timeInPast), n(1, "foo", timeInPast),
}, }),
SelfNode: n(2, "self", timeInFuture), SelfNode: n(2, "self", timeInFuture),
}, },
want: timeInFuture, want: timeInFuture,
@ -219,9 +219,9 @@ func TestNextPeerExpiry(t *testing.T) {
{ {
name: "self_already_expired", name: "self_already_expired",
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", timeInFuture), n(1, "foo", timeInFuture),
}, }),
SelfNode: n(2, "self", timeInPast), SelfNode: n(2, "self", timeInPast),
}, },
want: timeInFuture, want: timeInFuture,
@ -229,9 +229,9 @@ func TestNextPeerExpiry(t *testing.T) {
{ {
name: "all_nodes_already_expired", name: "all_nodes_already_expired",
netmap: &netmap.NetworkMap{ netmap: &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", timeInPast), n(1, "foo", timeInPast),
}, }),
SelfNode: n(2, "self", timeInPast), SelfNode: n(2, "self", timeInPast),
}, },
want: noExpiry, want: noExpiry,
@ -263,9 +263,9 @@ func TestNextPeerExpiry(t *testing.T) {
// If we don't adjust for the local time, this would return a // If we don't adjust for the local time, this would return a
// time in the past. // time in the past.
nm := &netmap.NetworkMap{ nm := &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
n(1, "foo", timeInPast), n(1, "foo", timeInPast),
}, }),
} }
got := em.nextPeerExpiry(nm, now) got := em.nextPeerExpiry(nm, now)
want := now.Add(30 * time.Second) 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 var sb strings.Builder
for i, n := range nodes { for i, n := range nodes {
if i > 0 { if i > 0 {
sb.WriteString(", ") sb.WriteString(", ")
} }
fmt.Fprintf(&sb, "(%d, %q", n.ID, n.Name) fmt.Fprintf(&sb, "(%d, %q", n.ID(), n.Name())
if n.Online != nil { if n.Online() != nil {
fmt.Fprintf(&sb, ", online=%v", *n.Online) fmt.Fprintf(&sb, ", online=%v", *n.Online())
} }
if n.LastSeen != nil { if n.LastSeen() != nil {
fmt.Fprintf(&sb, ", lastSeen=%v", n.LastSeen.Unix()) fmt.Fprintf(&sb, ", lastSeen=%v", n.LastSeen().Unix())
} }
if n.Key != (key.NodePublic{}) { if n.Key() != (key.NodePublic{}) {
fmt.Fprintf(&sb, ", key=%v", n.Key.String()) fmt.Fprintf(&sb, ", key=%v", n.Key().String())
} }
if n.Expired { if n.Expired() {
fmt.Fprintf(&sb, ", expired=true") fmt.Fprintf(&sb, ", expired=true")
} }
sb.WriteString(")") sb.WriteString(")")

@ -204,7 +204,7 @@ type LocalBackend struct {
// netMap is not mutated in-place once set. // netMap is not mutated in-place once set.
netMap *netmap.NetworkMap netMap *netmap.NetworkMap
nmExpiryTimer tstime.TimerController // for updating netMap on node expiry; can be nil 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 activeLogin string // last logged LoginName from netMap
engineStatus ipn.EngineStatus engineStatus ipn.EngineStatus
endpoints []tailcfg.Endpoint endpoints []tailcfg.Endpoint
@ -684,13 +684,13 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
if !prefs.ExitNodeID().IsZero() { if !prefs.ExitNodeID().IsZero() {
if exitPeer, ok := b.netMap.PeerWithStableID(prefs.ExitNodeID()); ok { if exitPeer, ok := b.netMap.PeerWithStableID(prefs.ExitNodeID()); ok {
var online = false var online = false
if exitPeer.Online != nil { if v := exitPeer.Online(); v != nil {
online = *exitPeer.Online online = *v
} }
s.ExitNodeStatus = &ipnstate.ExitNodeStatus{ s.ExitNodeStatus = &ipnstate.ExitNodeStatus{
ID: prefs.ExitNodeID(), ID: prefs.ExitNodeID(),
Online: online, 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.DNSName = b.netMap.Name
ss.UserID = b.netMap.User ss.UserID = b.netMap.User
if sn := b.netMap.SelfNode; sn != nil { if sn := b.netMap.SelfNode; sn != nil {
peerStatusFromNode(ss, sn) peerStatusFromNode(ss, sn.View())
if c := sn.Capabilities; len(c) > 0 { if c := sn.Capabilities; len(c) > 0 {
ss.Capabilities = append([]string(nil), c...) ss.Capabilities = append([]string(nil), c...)
} }
@ -735,28 +735,30 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
exitNodeID := b.pm.CurrentPrefs().ExitNodeID() exitNodeID := b.pm.CurrentPrefs().ExitNodeID()
for _, p := range b.netMap.Peers { for _, p := range b.netMap.Peers {
var lastSeen time.Time var lastSeen time.Time
if p.LastSeen != nil { if p.LastSeen() != nil {
lastSeen = *p.LastSeen lastSeen = *p.LastSeen()
} }
var tailscaleIPs = make([]netip.Addr, 0, len(p.Addresses)) var tailscaleIPs = make([]netip.Addr, 0, p.Addresses().Len())
for _, addr := range p.Addresses { for i := range p.Addresses().LenIter() {
addr := p.Addresses().At(i)
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) { if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
tailscaleIPs = append(tailscaleIPs, addr.Addr()) tailscaleIPs = append(tailscaleIPs, addr.Addr())
} }
} }
online := p.Online()
ps := &ipnstate.PeerStatus{ ps := &ipnstate.PeerStatus{
InNetworkMap: true, InNetworkMap: true,
UserID: p.User, UserID: p.User(),
TailscaleIPs: tailscaleIPs, TailscaleIPs: tailscaleIPs,
HostName: p.Hostinfo.Hostname(), HostName: p.Hostinfo().Hostname(),
DNSName: p.Name, DNSName: p.Name(),
OS: p.Hostinfo.OS(), OS: p.Hostinfo().OS(),
LastSeen: lastSeen, LastSeen: lastSeen,
Online: p.Online != nil && *p.Online, Online: online != nil && *online,
ShareeNode: p.Hostinfo.ShareeNode(), ShareeNode: p.Hostinfo().ShareeNode(),
ExitNode: p.StableID != "" && p.StableID == exitNodeID, ExitNode: p.StableID() != "" && p.StableID() == exitNodeID,
SSH_HostKeys: p.Hostinfo.SSH_HostKeys().AsSlice(), SSH_HostKeys: p.Hostinfo().SSH_HostKeys().AsSlice(),
Location: p.Hostinfo.Location(), Location: p.Hostinfo().Location(),
} }
peerStatusFromNode(ps, p) peerStatusFromNode(ps, p)
@ -767,29 +769,29 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
if u := peerAPIURL(nodeIP(p, netip.Addr.Is6), p6); u != "" { if u := peerAPIURL(nodeIP(p, netip.Addr.Is6), p6); u != "" {
ps.PeerAPIURL = append(ps.PeerAPIURL, 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 // peerStatusFromNode copies fields that exist in the Node struct for
// current node and peers into the provided PeerStatus. // current node and peers into the provided PeerStatus.
func peerStatusFromNode(ps *ipnstate.PeerStatus, n *tailcfg.Node) { func peerStatusFromNode(ps *ipnstate.PeerStatus, n tailcfg.NodeView) {
ps.ID = n.StableID ps.ID = n.StableID()
ps.Created = n.Created ps.Created = n.Created()
ps.ExitNodeOption = tsaddr.ContainsExitRoutes(views.SliceOf(n.AllowedIPs)) ps.ExitNodeOption = tsaddr.ContainsExitRoutes(n.AllowedIPs())
if n.Tags != nil { if n.Tags().Len() != 0 {
v := views.SliceOf(n.Tags) v := n.Tags()
ps.Tags = &v ps.Tags = &v
} }
if n.PrimaryRoutes != nil { if n.PrimaryRoutes().Len() != 0 {
v := views.SliceOf(n.PrimaryRoutes) v := n.PrimaryRoutes()
ps.PrimaryRoutes = &v ps.PrimaryRoutes = &v
} }
if n.Expired { if n.Expired() {
ps.Expired = true ps.Expired = true
} }
if t := n.KeyExpiry; !t.IsZero() { if t := n.KeyExpiry(); !t.IsZero() {
t = t.Round(time.Second) t = t.Round(time.Second)
ps.KeyExpiry = &t 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. // 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 the IP address is a Tailscale IP, the provided port may be 0.
// If ok == true, n and u are valid. // 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() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
n, ok = b.nodeByAddr[ipp.Addr()] 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) ip, ok = b.e.WhoIsIPPort(ipp)
} }
if !ok { if !ok {
return nil, u, false return zero, u, false
} }
n, ok = b.nodeByAddr[ip] n, ok = b.nodeByAddr[ip]
if !ok { 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 { if !ok {
return nil, u, false return zero, u, false
} }
return n, u, true return n, u, true
} }
@ -1114,13 +1117,14 @@ func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool)
} }
for _, peer := range nm.Peers { 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 { if !addr.IsSingleIP() || addr.Addr() != prefs.ExitNodeIP {
continue continue
} }
// Found the node being referenced, upgrade prefs to // Found the node being referenced, upgrade prefs to
// reference it directly for next time. // reference it directly for next time.
prefs.ExitNodeID = peer.StableID prefs.ExitNodeID = peer.StableID()
prefs.ExitNodeIP = netip.Addr{} prefs.ExitNodeIP = netip.Addr{}
return true 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 // If this reports true, the packet filter is invalid (the server is either broken
// or malicious) and should be ignored for safety. // 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 b netipx.IPSetBuilder
var numUnlocked int var numUnlocked int
for _, p := range peers { for _, p := range peers {
if !p.UnsignedPeerAPIOnly { if !p.UnsignedPeerAPIOnly() {
continue continue
} }
numUnlocked++ numUnlocked++
for _, a := range p.AllowedIPs { // not only addresses! for i := range p.AllowedIPs().LenIter() { // not only addresses!
b.AddPrefix(a) b.AddPrefix(p.AllowedIPs().At(i))
} }
} }
if numUnlocked == 0 { if numUnlocked == 0 {
@ -1764,11 +1768,11 @@ func shrinkDefaultRoute(route netip.Prefix, localInterfaceRoutes *netipx.IPSet,
// dnsCIDRsEqual determines whether two CIDR lists are equal // dnsCIDRsEqual determines whether two CIDR lists are equal
// for DNS map construction purposes (that is, only the first entry counts). // for DNS map construction purposes (that is, only the first entry counts).
func dnsCIDRsEqual(newAddr, oldAddr []netip.Prefix) bool { func dnsCIDRsEqual(newAddr, oldAddr views.Slice[netip.Prefix]) bool {
if len(newAddr) != len(oldAddr) { if newAddr.Len() != oldAddr.Len() {
return false return false
} }
if len(newAddr) == 0 || newAddr[0] == oldAddr[0] { if newAddr.Len() == 0 || newAddr.At(0) == oldAddr.At(0) {
return true return true
} }
return false return false
@ -1792,16 +1796,16 @@ func dnsMapsEqual(new, old *netmap.NetworkMap) bool {
if new.Name != old.Name { if new.Name != old.Name {
return false return false
} }
if !dnsCIDRsEqual(new.Addresses, old.Addresses) { if !dnsCIDRsEqual(views.SliceOf(new.Addresses), views.SliceOf(old.Addresses)) {
return false return false
} }
for i, newPeer := range new.Peers { for i, newPeer := range new.Peers {
oldPeer := old.Peers[i] oldPeer := old.Peers[i]
if newPeer.Name != oldPeer.Name { if newPeer.Name() != oldPeer.Name() {
return false return false
} }
if !dnsCIDRsEqual(newPeer.Addresses, oldPeer.Addresses) { if !dnsCIDRsEqual(newPeer.Addresses(), oldPeer.Addresses()) {
return false return false
} }
} }
@ -2418,8 +2422,8 @@ func (b *LocalBackend) Ping(ctx context.Context, ip netip.Addr, pingType tailcfg
if err != nil { if err != nil {
pr.Err = err.Error() pr.Err = err.Error()
} }
if node != nil { if node.Valid() {
pr.NodeName = node.Name pr.NodeName = node.Name()
} }
return pr, nil 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) ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel() defer cancel()
nm := b.NetMap() nm := b.NetMap()
if nm == nil { if nm == nil {
return nil, "", errors.New("no netmap") return zero, "", errors.New("no netmap")
} }
peer, ok := nm.PeerByTailscaleIP(ip) peer, ok := nm.PeerByTailscaleIP(ip)
if !ok { 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 { if peer.Expired() {
return nil, "", errors.New("peer's node key has expired") return zero, "", errors.New("peer's node key has expired")
} }
base := peerAPIBase(nm, peer) base := peerAPIBase(nm, peer)
if base == "" { 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) outReq, err := http.NewRequestWithContext(ctx, "HEAD", base, nil)
if err != nil { if err != nil {
return nil, "", err return zero, "", err
} }
tr := b.Dialer().PeerAPITransport() tr := b.Dialer().PeerAPITransport()
res, err := tr.RoundTrip(outReq) res, err := tr.RoundTrip(outReq)
if err != nil { if err != nil {
return nil, "", err return zero, "", err
} }
defer res.Body.Close() // but unnecessary on HEAD responses defer res.Body.Close() // but unnecessary on HEAD responses
if res.StatusCode != http.StatusOK { 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 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 // isn't configured to make MagicDNS resolution truly
// magic. Details in // magic. Details in
// https://github.com/tailscale/tailscale/issues/1886. // https://github.com/tailscale/tailscale/issues/1886.
set := func(name string, addrs []netip.Prefix) { set := func(name string, addrs views.Slice[netip.Prefix]) {
if len(addrs) == 0 || name == "" { if addrs.Len() == 0 || name == "" {
return return
} }
fqdn, err := dnsname.ToFQDN(name) fqdn, err := dnsname.ToFQDN(name)
if err != nil { if err != nil {
return // TODO: propagate error? 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 var ips []netip.Addr
for _, addr := range addrs { for i := range addrs.LenIter() {
addr := addrs.At(i)
if selfV6Only { if selfV6Only {
if addr.Addr().Is6() { if addr.Addr().Is6() {
ips = append(ips, addr.Addr()) ips = append(ips, addr.Addr())
@ -3130,9 +3142,9 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
} }
dcfg.Hosts[fqdn] = ips dcfg.Hosts[fqdn] = ips
} }
set(nm.Name, nm.Addresses) set(nm.Name, views.SliceOf(nm.Addresses))
for _, peer := range nm.Peers { for _, peer := range nm.Peers {
set(peer.Name, peer.Addresses) set(peer.Name(), peer.Addresses())
} }
for _, rec := range nm.DNS.ExtraRecords { for _, rec := range nm.DNS.ExtraRecords {
switch rec.Type { switch rec.Type {
@ -3995,28 +4007,28 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
// Update the nodeByAddr index. // Update the nodeByAddr index.
if b.nodeByAddr == nil { if b.nodeByAddr == nil {
b.nodeByAddr = map[netip.Addr]*tailcfg.Node{} b.nodeByAddr = map[netip.Addr]tailcfg.NodeView{}
} }
// First pass, mark everything unwanted. // First pass, mark everything unwanted.
for k := range b.nodeByAddr { for k := range b.nodeByAddr {
b.nodeByAddr[k] = nil b.nodeByAddr[k] = tailcfg.NodeView{}
} }
addNode := func(n *tailcfg.Node) { addNode := func(n tailcfg.NodeView) {
for _, ipp := range n.Addresses { for i := range n.Addresses().LenIter() {
if ipp.IsSingleIP() { if ipp := n.Addresses().At(i); ipp.IsSingleIP() {
b.nodeByAddr[ipp.Addr()] = n b.nodeByAddr[ipp.Addr()] = n
} }
} }
} }
if nm.SelfNode != nil { if nm.SelfNode != nil {
addNode(nm.SelfNode) addNode(nm.SelfNode.View())
} }
for _, p := range nm.Peers { for _, p := range nm.Peers {
addNode(p) addNode(p)
} }
// Third pass, actually delete the unwanted items. // Third pass, actually delete the unwanted items.
for k, v := range b.nodeByAddr { for k, v := range b.nodeByAddr {
if v == nil { if !v.Valid() {
delete(b.nodeByAddr, k) delete(b.nodeByAddr, k)
} }
} }
@ -4293,7 +4305,7 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
continue continue
} }
ret = append(ret, &apitype.FileTarget{ ret = append(ret, &apitype.FileTarget{
Node: p, Node: p.AsStruct(),
PeerAPIURL: peerAPI, PeerAPIURL: peerAPI,
}) })
} }
@ -4306,15 +4318,15 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
// the netmap. // the netmap.
// //
// b.mu must be locked. // b.mu must be locked.
func (b *LocalBackend) peerIsTaildropTargetLocked(p *tailcfg.Node) bool { func (b *LocalBackend) peerIsTaildropTargetLocked(p tailcfg.NodeView) bool {
if b.netMap == nil || p == nil { if b.netMap == nil || !p.Valid() {
return false return false
} }
if b.netMap.User == p.User { if b.netMap.User == p.User() {
return true return true
} }
if len(p.Addresses) > 0 && if p.Addresses().Len() > 0 &&
b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.PeerCapabilityFileSharingTarget) { b.peerHasCapLocked(p.Addresses().At(0).Addr(), tailcfg.PeerCapabilityFileSharingTarget) {
// Explicitly noted in the netmap ACL caps as a target. // Explicitly noted in the netmap ACL caps as a target.
return true return true
} }
@ -4374,9 +4386,9 @@ func (b *LocalBackend) registerIncomingFile(inf *incomingFile, active bool) {
} }
} }
func peerAPIPorts(peer *tailcfg.Node) (p4, p6 uint16) { func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) {
svcs := peer.Hostinfo.Services() svcs := peer.Hostinfo().Services()
for i, n := 0, svcs.Len(); i < n; i++ { for i := range svcs.LenIter() {
s := svcs.At(i) s := svcs.At(i)
switch s.Proto { switch s.Proto {
case tailcfg.PeerAPI4: 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. // 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 // 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. // or there's no matching address family based on the netmap's own addresses.
func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string { func peerAPIBase(nm *netmap.NetworkMap, peer tailcfg.NodeView) string {
if nm == nil || peer == nil || !peer.Hostinfo.Valid() { if nm == nil || !peer.Valid() || !peer.Hostinfo().Valid() {
return "" return ""
} }
@ -4429,8 +4441,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
return "" return ""
} }
func nodeIP(n *tailcfg.Node, pred func(netip.Addr) bool) netip.Addr { func nodeIP(n tailcfg.NodeView, pred func(netip.Addr) bool) netip.Addr {
for _, a := range n.Addresses { for i := range n.Addresses().LenIter() {
a := n.Addresses().At(i)
if a.IsSingleIP() && pred(a.Addr()) { if a.IsSingleIP() && pred(a.Addr()) {
return a.Addr() return a.Addr()
} }
@ -4540,15 +4553,15 @@ func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID)
return "", false return "", false
} }
for _, p := range nm.Peers { 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 peerAPIBase(nm, p) + "/dns-query", true
} }
} }
return "", false return "", false
} }
func peerCanProxyDNS(p *tailcfg.Node) bool { func peerCanProxyDNS(p tailcfg.NodeView) bool {
if p.Cap >= 26 { if p.Cap() >= 26 {
// Actually added at 25 // Actually added at 25
// (https://github.com/tailscale/tailscale/blob/3ae6f898cfdb58fd0e30937147dd6ce28c6808dd/tailcfg/tailcfg.go#L51) // (https://github.com/tailscale/tailscale/blob/3ae6f898cfdb58fd0e30937147dd6ce28c6808dd/tailcfg/tailcfg.go#L51)
// so anything >= 26 can do it. // 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 // If p.Cap is not populated (e.g. older control server), then do the old
// thing of searching through services. // thing of searching through services.
services := p.Hostinfo.Services() services := p.Hostinfo().Services()
for i, n := 0, services.Len(); i < n; i++ { for i := range services.LenIter() {
s := services.At(i) if s := services.At(i); s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
return true return true
} }
} }

@ -87,46 +87,46 @@ func TestNetworkMapCompare(t *testing.T) {
}, },
{ {
"Peers identical", "Peers identical",
&netmap.NetworkMap{Peers: []*tailcfg.Node{}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
&netmap.NetworkMap{Peers: []*tailcfg.Node{}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
true, true,
}, },
{ {
"Peer list length", "Peer list length",
// length of Peers list differs // length of Peers list differs
&netmap.NetworkMap{Peers: []*tailcfg.Node{{}}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{}})},
&netmap.NetworkMap{Peers: []*tailcfg.Node{}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{})},
false, false,
}, },
{ {
"Node names identical", "Node names identical",
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
true, true,
}, },
{ {
"Node names differ", "Node names differ",
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "A"}})},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "B"}}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{Name: "B"}})},
false, false,
}, },
{ {
"Node lists identical", "Node lists identical",
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
true, true,
}, },
{ {
"Node lists differ", "Node lists differ",
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node1})},
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node2}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{node1, node2})},
false, false,
}, },
{ {
"Node Users differ", "Node Users differ",
// User field is not checked. // User field is not checked.
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 0}}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{User: 0}})},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 1}}}, &netmap.NetworkMap{Peers: nodeViews([]*tailcfg.Node{{User: 1}})},
true, true,
}, },
} }
@ -483,7 +483,7 @@ func TestPeerAPIBase(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if got != tt.want {
t.Errorf("got %q; want %q", 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) t.Errorf("got %v, want %v", got, tt.want)
} }
}) })

@ -69,15 +69,16 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
var toDelete map[int]bool // peer index => true var toDelete map[int]bool // peer index => true
for i, p := range nm.Peers { for i, p := range nm.Peers {
if p.UnsignedPeerAPIOnly { if p.UnsignedPeerAPIOnly() {
// Not subject to tailnet lock. // Not subject to tailnet lock.
continue 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) b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID, p.StableID)
mak.Set(&toDelete, i, true) mak.Set(&toDelete, i, true)
} else { } 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) 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) 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. // nm.Peers is ordered, so deletion must be order-preserving.
if len(toDelete) > 0 { 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)) filtered := make([]ipnstate.TKAFilteredPeer, 0, len(toDelete))
for i, p := range nm.Peers { for i, p := range nm.Peers {
if !toDelete[i] { if !toDelete[i] {
@ -94,13 +95,14 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
} else { } else {
// Record information about the node we filtered out. // Record information about the node we filtered out.
fp := ipnstate.TKAFilteredPeer{ fp := ipnstate.TKAFilteredPeer{
Name: p.Name, Name: p.Name(),
ID: p.ID, ID: p.ID(),
StableID: p.StableID, StableID: p.StableID(),
TailscaleIPs: make([]netip.Addr, len(p.Addresses)), TailscaleIPs: make([]netip.Addr, p.Addresses().Len()),
NodeKey: p.Key, 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()) { if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
fp.TailscaleIPs[i] = addr.Addr() fp.TailscaleIPs[i] = addr.Addr()
} }

@ -558,26 +558,26 @@ func TestTKAFilterNetmap(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
nm := netmap.NetworkMap{ nm := &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()}, {ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
{ID: 2, Key: n2.Public(), KeySignature: nil}, // missing sig {ID: 2, Key: n2.Public(), KeySignature: nil}, // missing sig
{ID: 3, Key: n3.Public(), KeySignature: n1GoodSig.Serialize()}, // someone elses sig {ID: 3, Key: n3.Public(), KeySignature: n1GoodSig.Serialize()}, // someone elses sig
{ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature {ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()}, {ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
}, }),
} }
b := &LocalBackend{ b := &LocalBackend{
logf: t.Logf, logf: t.Logf,
tka: &tkaState{authority: authority}, 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: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()}, {ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
} })
nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool { nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool {
return x.Raw32() == y.Raw32() return x.Raw32() == y.Raw32()
}) })

@ -576,7 +576,7 @@ func (pln *peerAPIListener) ServeConn(src netip.AddrPort, c net.Conn) {
} }
h := &peerAPIHandler{ h := &peerAPIHandler{
ps: pln.ps, ps: pln.ps,
isSelf: nm.SelfNode.User == peerNode.User, isSelf: nm.SelfNode.User == peerNode.User(),
remoteAddr: src, remoteAddr: src,
selfNode: nm.SelfNode, selfNode: nm.SelfNode,
peerNode: peerNode, peerNode: peerNode,
@ -597,7 +597,7 @@ type peerAPIHandler struct {
remoteAddr netip.AddrPort remoteAddr netip.AddrPort
isSelf bool // whether peerNode is owned by same user as this node isSelf bool // whether peerNode is owned by same user as this node
selfNode *tailcfg.Node // this node; always non-nil 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 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 // isAddressValid reports whether addr is a valid destination address for this
// node originating from the peer. // node originating from the peer.
func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool { func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool {
if h.peerNode.SelfNodeV4MasqAddrForThisPeer != nil { if v := h.peerNode.SelfNodeV4MasqAddrForThisPeer(); v != nil {
return *h.peerNode.SelfNodeV4MasqAddrForThisPeer == addr return *v == addr
} }
pfx := netip.PrefixFrom(addr, addr.BitLen()) pfx := netip.PrefixFrom(addr, addr.BitLen())
return slices.Contains(h.selfNode.Addresses, pfx) return slices.Contains(h.selfNode.Addresses, pfx)
@ -733,7 +733,7 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
<body> <body>
<h1>Hello, %s (%v)</h1> <h1>Hello, %s (%v)</h1>
This is my Tailscale device. Your device is %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 { if h.isSelf {
fmt.Fprintf(w, "<p>You are the owner of this node.\n") fmt.Fprintf(w, "<p>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. // canPutFile reports whether h can put a file ("Taildrop") to this node.
func (h *peerAPIHandler) canPutFile() bool { func (h *peerAPIHandler) canPutFile() bool {
if h.peerNode.UnsignedPeerAPIOnly { if h.peerNode.UnsignedPeerAPIOnly() {
// Unsigned peers can't send files. // Unsigned peers can't send files.
return false return false
} }
@ -1038,7 +1038,7 @@ func (h *peerAPIHandler) canDebug() bool {
// This node does not expose debug info. // This node does not expose debug info.
return false return false
} }
if h.peerNode.UnsignedPeerAPIOnly { if h.peerNode.UnsignedPeerAPIOnly() {
// Unsigned peers can't debug. // Unsigned peers can't debug.
return false 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. // canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node.
func (h *peerAPIHandler) canWakeOnLAN() bool { func (h *peerAPIHandler) canWakeOnLAN() bool {
if h.peerNode.UnsignedPeerAPIOnly { if h.peerNode.UnsignedPeerAPIOnly() {
return false return false
} }
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityWakeOnLAN) return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityWakeOnLAN)

@ -462,9 +462,9 @@ func TestHandlePeerAPI(t *testing.T) {
e.ph = &peerAPIHandler{ e.ph = &peerAPIHandler{
isSelf: tt.isSelf, isSelf: tt.isSelf,
selfNode: selfNode, selfNode: selfNode,
peerNode: &tailcfg.Node{ peerNode: (&tailcfg.Node{
ComputedName: "some-peer-name", ComputedName: "some-peer-name",
}, }).View(),
ps: &peerAPIServer{ ps: &peerAPIServer{
b: lb, b: lb,
}, },
@ -513,9 +513,9 @@ func TestFileDeleteRace(t *testing.T) {
} }
ph := &peerAPIHandler{ ph := &peerAPIHandler{
isSelf: true, isSelf: true,
peerNode: &tailcfg.Node{ peerNode: (&tailcfg.Node{
ComputedName: "some-peer-name", ComputedName: "some-peer-name",
}, }).View(),
selfNode: &tailcfg.Node{ selfNode: &tailcfg.Node{
Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.100.101/32")}, Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.100.101/32")},
}, },

@ -257,7 +257,7 @@ func (b *LocalBackend) ServeConfig() ipn.ServeConfigView {
return b.serveConfig 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() b.mu.Lock()
sc := b.serveConfig sc := b.serveConfig
b.mu.Unlock() b.mu.Unlock()

@ -201,16 +201,16 @@ func TestServeHTTPProxy(t *testing.T) {
}, },
}, },
} }
b.nodeByAddr = map[netip.Addr]*tailcfg.Node{ b.nodeByAddr = map[netip.Addr]tailcfg.NodeView{
netip.MustParseAddr("100.150.151.152"): { netip.MustParseAddr("100.150.151.152"): (&tailcfg.Node{
ComputedName: "some-peer", ComputedName: "some-peer",
User: tailcfg.UserID(1), User: tailcfg.UserID(1),
}, }).View(),
netip.MustParseAddr("100.150.151.153"): { netip.MustParseAddr("100.150.151.153"): (&tailcfg.Node{
ComputedName: "some-tagged-peer", ComputedName: "some-tagged-peer",
Tags: []string{"tag:server", "tag:test"}, Tags: []string{"tag:server", "tag:test"},
User: tailcfg.UserID(1), User: tailcfg.UserID(1),
}, }).View(),
} }
// Start test serve endpoint. // Start test serve endpoint.

@ -437,7 +437,7 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
return return
} }
res := &apitype.WhoIsResponse{ res := &apitype.WhoIsResponse{
Node: n, // always non-nil per WhoIsResponse contract Node: n.AsStruct(), // always non-nil per WhoIsResponse contract
UserProfile: &u, // always non-nil per WhoIsResponse contract UserProfile: &u, // always non-nil per WhoIsResponse contract
CapMap: b.PeerCaps(ipp.Addr()), CapMap: b.PeerCaps(ipp.Addr()),
} }

@ -48,17 +48,18 @@ func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
} }
} }
for _, p := range nm.Peers { for _, p := range nm.Peers {
if p.Name == "" { if p.Name() == "" {
continue continue
} }
for _, a := range p.Addresses { for i := range p.Addresses().LenIter() {
a := p.Addresses().At(i)
ip := a.Addr() ip := a.Addr()
if ip.Is4() && !have4 { if ip.Is4() && !have4 {
continue continue
} }
ret[canonMapKey(p.Name)] = ip ret[canonMapKey(p.Name())] = ip
if dnsname.HasSuffix(p.Name, suffix) { if dnsname.HasSuffix(p.Name(), suffix) {
ret[canonMapKey(dnsname.TrimSuffix(p.Name, suffix))] = ip ret[canonMapKey(dnsname.TrimSuffix(p.Name(), suffix))] = ip
} }
break break
} }

@ -12,6 +12,14 @@ import (
"tailscale.com/types/netmap" "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) { func TestDNSMapFromNetworkMap(t *testing.T) {
pfx := netip.MustParsePrefix pfx := netip.MustParsePrefix
ip := netip.MustParseAddr ip := netip.MustParseAddr
@ -42,20 +50,20 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
pfx("100.102.103.104/32"), pfx("100.102.103.104/32"),
pfx("100::123/128"), pfx("100::123/128"),
}, },
Peers: []*tailcfg.Node{ Peers: []tailcfg.NodeView{
{ (&tailcfg.Node{
Name: "a.tailnet", Name: "a.tailnet",
Addresses: []netip.Prefix{ Addresses: []netip.Prefix{
pfx("100.0.0.201/32"), pfx("100.0.0.201/32"),
pfx("100::201/128"), pfx("100::201/128"),
}, },
}, }).View(),
{ (&tailcfg.Node{
Name: "b.tailnet", Name: "b.tailnet",
Addresses: []netip.Prefix{ Addresses: []netip.Prefix{
pfx("100::202/128"), pfx("100::202/128"),
}, },
}, }).View(),
}, },
}, },
want: dnsMap{ want: dnsMap{
@ -74,7 +82,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
Addresses: []netip.Prefix{ Addresses: []netip.Prefix{
pfx("100::123/128"), pfx("100::123/128"),
}, },
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Name: "a.tailnet", Name: "a.tailnet",
Addresses: []netip.Prefix{ Addresses: []netip.Prefix{
@ -88,7 +96,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
pfx("100::202/128"), pfx("100::202/128"),
}, },
}, },
}, }),
}, },
want: dnsMap{ want: dnsMap{
"foo": ip("100::123"), "foo": ip("100::123"),

@ -99,7 +99,7 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
gids := strings.Join(ss.conn.userGroupIDs, ",") gids := strings.Join(ss.conn.userGroupIDs, ",")
remoteUser := ci.uprof.LoginName remoteUser := ci.uprof.LoginName
if ci.node.IsTagged() { if ci.node.IsTagged() {
remoteUser = strings.Join(ci.node.Tags, ",") remoteUser = strings.Join(ci.node.Tags().AsSlice(), ",")
} }
incubatorArgs := []string{ incubatorArgs := []string{

@ -64,7 +64,7 @@ type ipnLocalBackend interface {
GetSSH_HostKeys() ([]gossh.Signer, error) GetSSH_HostKeys() ([]gossh.Signer, error)
ShouldRunSSH() bool ShouldRunSSH() bool
NetMap() *netmap.NetworkMap 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) DoNoiseRequest(req *http.Request) (*http.Response, error)
Dialer() *tsdial.Dialer Dialer() *tsdial.Dialer
TailscaleVarRoot() string TailscaleVarRoot() string
@ -791,7 +791,7 @@ func (c *conn) expandDelegateURLLocked(actionURL string) string {
} }
return strings.NewReplacer( return strings.NewReplacer(
"$SRC_NODE_IP", url.QueryEscape(ci.src.Addr().String()), "$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_IP", url.QueryEscape(ci.dst.Addr().String()),
"$DST_NODE_ID", dstNodeID, "$DST_NODE_ID", dstNodeID,
"$SSH_USER", url.QueryEscape(ci.sshUser), "$SSH_USER", url.QueryEscape(ci.sshUser),
@ -1220,7 +1220,7 @@ type sshConnInfo struct {
dst netip.AddrPort dst netip.AddrPort
// node is srcIP's node. // node is srcIP's node.
node *tailcfg.Node node tailcfg.NodeView
// uprof is node's UserProfile. // uprof is node's UserProfile.
uprof tailcfg.UserProfile uprof tailcfg.UserProfile
@ -1334,7 +1334,7 @@ func (c *conn) principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal) bool {
if p.Any { if p.Any {
return true 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 return true
} }
if p.NodeIP != "" { if p.NodeIP != "" {
@ -1702,15 +1702,15 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) {
}, },
SSHUser: ss.conn.info.sshUser, SSHUser: ss.conn.info.sshUser,
LocalUser: ss.conn.localUser.Username, LocalUser: ss.conn.localUser.Username,
SrcNode: strings.TrimSuffix(ss.conn.info.node.Name, "."), SrcNode: strings.TrimSuffix(ss.conn.info.node.Name(), "."),
SrcNodeID: ss.conn.info.node.StableID, SrcNodeID: ss.conn.info.node.StableID(),
ConnectionID: ss.conn.connID, ConnectionID: ss.conn.connID,
} }
if !ss.conn.info.node.IsTagged() { if !ss.conn.info.node.IsTagged() {
ch.SrcNodeUser = ss.conn.info.uprof.LoginName ch.SrcNodeUser = ss.conn.info.uprof.LoginName
ch.SrcNodeUserID = ss.conn.info.node.User ch.SrcNodeUserID = ss.conn.info.node.User()
} else { } else {
ch.SrcNodeTags = ss.conn.info.node.Tags ch.SrcNodeTags = ss.conn.info.node.Tags().AsSlice()
} }
j, err := json.Marshal(ch) j, err := json.Marshal(ch)
if err != nil { if err != nil {
@ -1738,7 +1738,7 @@ func (ss *sshSession) notifyControl(ctx context.Context, nodeKey key.NodePublic,
ConnectionID: ss.conn.connID, ConnectionID: ss.conn.connID,
CapVersion: tailcfg.CurrentCapabilityVersion, CapVersion: tailcfg.CurrentCapabilityVersion,
NodeKey: nodeKey, NodeKey: nodeKey,
SrcNode: ss.conn.info.node.ID, SrcNode: ss.conn.info.node.ID(),
SSHUser: ss.conn.info.sshUser, SSHUser: ss.conn.info.sshUser,
LocalUser: ss.conn.localUser.Username, LocalUser: ss.conn.localUser.Username,
RecordingAttempts: attempts, RecordingAttempts: attempts,

@ -177,7 +177,7 @@ func TestMatchRule(t *testing.T) {
Principals: []*tailcfg.SSHPrincipal{{Node: "some-node-ID"}}, Principals: []*tailcfg.SSHPrincipal{{Node: "some-node-ID"}},
SSHUsers: map[string]string{"*": "ubuntu"}, 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", 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) { func (ts *localState) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
return &tailcfg.Node{ return (&tailcfg.Node{
ID: 2, ID: 2,
StableID: "peer-id", StableID: "peer-id",
}, tailcfg.UserProfile{ }).View(), tailcfg.UserProfile{
LoginName: "peer", LoginName: "peer",
}, true }, true
@ -861,7 +861,7 @@ func TestSSH(t *testing.T) {
sshUser: "test", sshUser: "test",
src: netip.MustParseAddrPort("1.2.3.4:32342"), src: netip.MustParseAddrPort("1.2.3.4:32342"),
dst: netip.MustParseAddrPort("1.2.3.5:22"), dst: netip.MustParseAddrPort("1.2.3.5:22"),
node: &tailcfg.Node{}, node: (&tailcfg.Node{}).View(),
uprof: tailcfg.UserProfile{}, uprof: tailcfg.UserProfile{},
} }
sc.action0 = &tailcfg.SSHAction{Accept: true} sc.action0 = &tailcfg.SSHAction{Accept: true}

@ -388,6 +388,12 @@ func (n *Node) IsTagged() bool {
return len(n.Tags) > 0 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 // InitDisplayNames computes and populates n's display name
// fields: n.ComputedName, n.computedHostIfDifferent, and // fields: n.ComputedName, n.computedHostIfDifferent, and
// n.ComputedNameWithHost. // n.ComputedNameWithHost.

@ -34,7 +34,7 @@ type NetworkMap struct {
Addresses []netip.Prefix // same as tailcfg.Node.Addresses (IP addresses of this Node directly) Addresses []netip.Prefix // same as tailcfg.Node.Addresses (IP addresses of this Node directly)
MachineStatus tailcfg.MachineStatus MachineStatus tailcfg.MachineStatus
MachineKey key.MachinePublic MachineKey key.MachinePublic
Peers []*tailcfg.Node // sorted by Node.ID Peers []tailcfg.NodeView // sorted by Node.ID
DNS tailcfg.DNSConfig DNS tailcfg.DNSConfig
// TODO(maisem) : replace with View. // TODO(maisem) : replace with View.
Hostinfo tailcfg.Hostinfo Hostinfo tailcfg.Hostinfo
@ -84,7 +84,7 @@ type NetworkMap struct {
// AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes. // AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes.
func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool { func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
for _, p := range nm.Peers { for _, p := range nm.Peers {
if len(p.PrimaryRoutes) > 0 { if p.PrimaryRoutes().Len() > 0 {
return true return true
} }
} }
@ -94,19 +94,21 @@ func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
// PeerByTailscaleIP returns a peer's Node based on its Tailscale IP. // PeerByTailscaleIP returns a peer's Node based on its Tailscale IP.
// //
// If nm is nil or no peer is found, ok is false. // 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): // TODO(bradfitz):
if nm == nil { if nm == nil {
return nil, false return tailcfg.NodeView{}, false
} }
for _, n := range nm.Peers { 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 { if a.Addr() == ip {
return n, true return n, true
} }
} }
} }
return nil, false return tailcfg.NodeView{}, false
} }
// MagicDNSSuffix returns the domain's MagicDNS suffix (even if // 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. // 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 { for _, p := range nm.Peers {
if p.StableID == pid { if p.StableID() == pid {
return p, true return p, true
} }
} }
return nil, false return tailcfg.NodeView{}, false
} }
// printConciseHeader prints a concise header line representing nm to buf. // 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 // If this function is changed to access different fields of p, keep
// in nodeConciseEqual in sync. // in nodeConciseEqual in sync.
func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) { func printPeerConcise(buf *strings.Builder, p tailcfg.NodeView) {
aip := make([]string, len(p.AllowedIPs)) aip := make([]string, p.AllowedIPs().Len())
for i, a := range p.AllowedIPs { for i := range aip {
a := p.AllowedIPs().At(i)
s := strings.TrimSuffix(fmt.Sprint(a), "/32") s := strings.TrimSuffix(fmt.Sprint(a), "/32")
aip[i] = s aip[i] = s
} }
ep := make([]string, len(p.Endpoints)) ep := make([]string, p.Endpoints().Len())
for i, e := range p.Endpoints { for i := range ep {
e := p.Endpoints().At(i)
// Align vertically on the ':' between IP and port // Align vertically on the ':' between IP and port
colon := strings.IndexByte(e, ':') colon := strings.IndexByte(e, ':')
spaces := 0 spaces := 0
@ -222,21 +226,21 @@ func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) {
ep[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces)) ep[i] = fmt.Sprintf("%21v", e+strings.Repeat(" ", spaces))
} }
derp := p.DERP derp := p.DERP()
const derpPrefix = "127.3.3.40:" const derpPrefix = "127.3.3.40:"
if strings.HasPrefix(derp, derpPrefix) { if strings.HasPrefix(derp, derpPrefix) {
derp = "D" + derp[len(derpPrefix):] derp = "D" + derp[len(derpPrefix):]
} }
var discoShort string var discoShort string
if !p.DiscoKey.IsZero() { if !p.DiscoKey().IsZero() {
discoShort = p.DiscoKey.ShortString() + " " discoShort = p.DiscoKey().ShortString() + " "
} }
// Most of the time, aip is just one element, so format the // Most of the time, aip is just one element, so format the
// table to look good in that case. This will also make multi- // table to look good in that case. This will also make multi-
// subnet nodes stand out visually. // subnet nodes stand out visually.
fmt.Fprintf(buf, " %v %s%-2v %-15v : %v\n", fmt.Fprintf(buf, " %v %s%-2v %-15v : %v\n",
p.Key.ShortString(), p.Key().ShortString(),
discoShort, discoShort,
derp, derp,
strings.Join(aip, " "), 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. // nodeConciseEqual reports whether a and b are equal for the fields accessed by printPeerConcise.
func nodeConciseEqual(a, b *tailcfg.Node) bool { func nodeConciseEqual(a, b tailcfg.NodeView) bool {
return a.Key == b.Key && return a.Key() == b.Key() &&
a.DERP == b.DERP && a.DERP() == b.DERP() &&
a.DiscoKey == b.DiscoKey && a.DiscoKey() == b.DiscoKey() &&
eqCIDRsIgnoreNil(a.AllowedIPs, b.AllowedIPs) && eqViewsIgnoreNil(a.AllowedIPs(), b.AllowedIPs()) &&
eqStringsIgnoreNil(a.Endpoints, b.Endpoints) eqViewsIgnoreNil(a.Endpoints(), b.Endpoints())
} }
func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string { 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 { for len(aps) > 0 && len(bps) > 0 {
pa, pb := aps[0], bps[0] pa, pb := aps[0], bps[0]
switch { switch {
case pa.ID == pb.ID: case pa.ID() == pb.ID():
if !nodeConciseEqual(pa, pb) { if !nodeConciseEqual(pa, pb) {
diff.WriteByte('-') diff.WriteByte('-')
printPeerConcise(&diff, pa) printPeerConcise(&diff, pa)
@ -276,12 +280,12 @@ func (b *NetworkMap) ConciseDiffFrom(a *NetworkMap) string {
printPeerConcise(&diff, pb) printPeerConcise(&diff, pb)
} }
aps, bps = aps[1:], bps[1:] aps, bps = aps[1:], bps[1:]
case pa.ID > pb.ID: case pa.ID() > pb.ID():
// New peer in b. // New peer in b.
diff.WriteByte('+') diff.WriteByte('+')
printPeerConcise(&diff, pb) printPeerConcise(&diff, pb)
bps = bps[1:] bps = bps[1:]
case pb.ID > pa.ID: case pb.ID() > pa.ID():
// Deleted peer in b. // Deleted peer in b.
diff.WriteByte('-') diff.WriteByte('-')
printPeerConcise(&diff, pa) printPeerConcise(&diff, pa)
@ -316,28 +320,18 @@ const (
AllowSubnetRoutes AllowSubnetRoutes
) )
// eqStringsIgnoreNil reports whether a and b have the same length and // eqViewsIgnoreNil reports whether a and b have the same length and comparably
// contents, but ignore whether a or b are nil. // equal values at each index. It's used for comparing views of slices and not
func eqStringsIgnoreNil(a, b []string) bool { // caring about whether the slices are nil or not.
if len(a) != len(b) { func eqViewsIgnoreNil[T comparable](a, b interface {
Len() int
At(int) T
}) bool {
if a.Len() != b.Len() {
return false return false
} }
for i, v := range a { for i, n := 0, a.Len(); i < n; i++ {
if v != b[i] { if a.At(i) != b.At(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] {
return false return false
} }
} }

@ -34,6 +34,14 @@ func testDiscoKey(hexPrefix string) (ret key.DiscoPublic) {
return key.DiscoPublicFromRaw32(mem.B(bs[:])) 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) { func TestNetworkMapConcise(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
@ -44,7 +52,7 @@ func TestNetworkMapConcise(t *testing.T) {
name: "basic", name: "basic",
nm: &NetworkMap{ nm: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", DERP: "127.3.3.40:2",
@ -55,7 +63,7 @@ func TestNetworkMapConcise(t *testing.T) {
DERP: "127.3.3.40:4", DERP: "127.3.3.40:4",
Endpoints: []string{"10.2.0.100:12", "10.1.0.100:12345"}, 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", 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", name: "no_change",
a: &NetworkMap{ a: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
}, },
}, }),
}, },
b: &NetworkMap{ b: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
}, },
}, }),
}, },
want: "", want: "",
}, },
@ -107,23 +115,23 @@ func TestConciseDiffFrom(t *testing.T) {
name: "header_change", name: "header_change",
a: &NetworkMap{ a: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
}, },
}, }),
}, },
b: &NetworkMap{ b: &NetworkMap{
NodeKey: testNodeKey(2), NodeKey: testNodeKey(2),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, 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", 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", name: "peer_add",
a: &NetworkMap{ a: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
}, },
}, }),
}, },
b: &NetworkMap{ b: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
ID: 1, ID: 1,
Key: testNodeKey(1), Key: testNodeKey(1),
@ -161,7 +169,7 @@ func TestConciseDiffFrom(t *testing.T) {
DERP: "127.3.3.40:3", DERP: "127.3.3.40:3",
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, 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", 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", name: "peer_remove",
a: &NetworkMap{ a: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
ID: 1, ID: 1,
Key: testNodeKey(1), Key: testNodeKey(1),
@ -188,18 +196,18 @@ func TestConciseDiffFrom(t *testing.T) {
DERP: "127.3.3.40:3", DERP: "127.3.3.40:3",
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"},
}, },
}, }),
}, },
b: &NetworkMap{ b: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:12", "192.168.0.100:12354"}, 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", 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", name: "peer_port_change",
a: &NetworkMap{ a: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:12", "1.1.1.1:1"}, Endpoints: []string{"192.168.0.100:12", "1.1.1.1:1"},
}, },
}, }),
}, },
b: &NetworkMap{ b: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
DERP: "127.3.3.40:2", DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:12", "1.1.1.1: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", 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", name: "disco_key_only_change",
a: &NetworkMap{ a: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
@ -242,11 +250,11 @@ func TestConciseDiffFrom(t *testing.T) {
DiscoKey: testDiscoKey("f00f00f00f"), DiscoKey: testDiscoKey("f00f00f00f"),
AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)}, AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
}, },
}, }),
}, },
b: &NetworkMap{ b: &NetworkMap{
NodeKey: testNodeKey(1), NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
ID: 2, ID: 2,
Key: testNodeKey(2), Key: testNodeKey(2),
@ -255,7 +263,7 @@ func TestConciseDiffFrom(t *testing.T) {
DiscoKey: testDiscoKey("ba4ba4ba4b"), DiscoKey: testDiscoKey("ba4ba4ba4b"),
AllowedIPs: []netip.Prefix{netip.PrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)}, 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", 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",
}, },

@ -96,7 +96,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
eps = append(eps, ep.Addr.String()) eps = append(eps, ep.Addr.String())
} }
n := tailcfg.Node{ n := &tailcfg.Node{
ID: tailcfg.NodeID(0), ID: tailcfg.NodeID(0),
Name: "n1", Name: "n1",
Addresses: []netip.Prefix{a1}, 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{ e2.SetNetworkMap(&netmap.NetworkMap{
NodeKey: k2.Public(), NodeKey: k2.Public(),
PrivateKey: k2, PrivateKey: k2,
Peers: []*tailcfg.Node{&n}, Peers: []tailcfg.NodeView{n.View()},
}) })
p := wgcfg.Peer{ 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()) eps = append(eps, ep.Addr.String())
} }
n := tailcfg.Node{ n := &tailcfg.Node{
ID: tailcfg.NodeID(0), ID: tailcfg.NodeID(0),
Name: "n2", Name: "n2",
Addresses: []netip.Prefix{a2}, 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{ e1.SetNetworkMap(&netmap.NetworkMap{
NodeKey: k1.Public(), NodeKey: k1.Public(),
PrivateKey: k1, PrivateKey: k1,
Peers: []*tailcfg.Node{&n}, Peers: []tailcfg.NodeView{n.View()},
}) })
p := wgcfg.Peer{ p := wgcfg.Peer{

@ -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) }) 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 { if c.netMap != nil {
for _, p := range c.netMap.Peers { 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 { func peerDebugName(p tailcfg.NodeView) string {
if p == nil { if !p.Valid() {
return "" return ""
} }
n := p.Name n := p.Name()
if base, _, ok := strings.Cut(n, "."); ok { if base, _, ok := strings.Cut(n, "."); ok {
return base return base
} }
return p.Hostinfo.Hostname() return p.Hostinfo().Hostname()
} }
func ipPortLess(a, b netip.AddrPort) bool { func ipPortLess(a, b netip.AddrPort) bool {

@ -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 // updateFromNode updates the endpoint based on a tailcfg.Node from a NetMap
// update. // update.
func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) { func (de *endpoint) updateFromNode(n tailcfg.NodeView, heartbeatDisabled bool) {
if n == nil { if !n.Valid() {
panic("nil node when updating endpoint") panic("nil node when updating endpoint")
} }
de.mu.Lock() de.mu.Lock()
defer de.mu.Unlock() defer de.mu.Unlock()
de.heartbeatDisabled = heartbeatDisabled de.heartbeatDisabled = heartbeatDisabled
de.expired = n.Expired de.expired = n.Expired()
epDisco := de.disco.Load() epDisco := de.disco.Load()
var discoKey key.DiscoPublic var discoKey key.DiscoPublic
@ -741,11 +741,11 @@ func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) {
discoKey = epDisco.key discoKey = epDisco.key
} }
if 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.c.logf("[v1] magicsock: disco: node %s changed from %s to %s", de.publicKey.ShortString(), discoKey, n.DiscoKey())
de.disco.Store(&endpointDisco{ de.disco.Store(&endpointDisco{
key: n.DiscoKey, key: n.DiscoKey(),
short: n.DiscoKey.ShortString(), short: n.DiscoKey().ShortString(),
}) })
de.debugUpdates.Add(EndpointChange{ de.debugUpdates.Add(EndpointChange{
When: time.Now(), When: time.Now(),
@ -753,7 +753,7 @@ func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) {
}) })
de.resetLocked() de.resetLocked()
} }
if n.DERP == "" { if n.DERP() == "" {
if de.derpAddr.IsValid() { if de.derpAddr.IsValid() {
de.debugUpdates.Add(EndpointChange{ de.debugUpdates.Add(EndpointChange{
When: time.Now(), When: time.Now(),
@ -763,7 +763,7 @@ func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) {
} }
de.derpAddr = netip.AddrPort{} de.derpAddr = netip.AddrPort{}
} else { } else {
newDerp, _ := netip.ParseAddrPort(n.DERP) newDerp, _ := netip.ParseAddrPort(n.DERP())
if de.derpAddr != newDerp { if de.derpAddr != newDerp {
de.debugUpdates.Add(EndpointChange{ de.debugUpdates.Add(EndpointChange{
When: time.Now(), When: time.Now(),
@ -780,7 +780,8 @@ func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) {
} }
var newIpps []netip.AddrPort 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 { if i > math.MaxInt16 {
// Seems unlikely. // Seems unlikely.
continue continue

@ -711,7 +711,7 @@ func (c *Conn) LastRecvActivityOfNodeKey(nk key.NodePublic) string {
} }
// Ping handles a "tailscale ping" CLI query. // 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() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if c.privateKey.IsZero() { if c.privateKey.IsZero() {
@ -719,17 +719,17 @@ func (c *Conn) Ping(peer *tailcfg.Node, res *ipnstate.PingResult, size int, cb f
cb(res) cb(res)
return return
} }
if len(peer.Addresses) > 0 { if peer.Addresses().Len() > 0 {
res.NodeIP = peer.Addresses[0].Addr().String() 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 == "" { if res.NodeName == "" {
res.NodeName = peer.Hostinfo.Hostname() // else hostname res.NodeName = peer.Hostinfo().Hostname() // else hostname
} else { } else {
res.NodeName, _, _ = strings.Cut(res.NodeName, ".") res.NodeName, _, _ = strings.Cut(res.NodeName, ".")
} }
ep, ok := c.peerMap.endpointForNodeKey(peer.Key) ep, ok := c.peerMap.endpointForNodeKey(peer.Key())
if !ok { if !ok {
res.Err = "unknown peer" res.Err = "unknown peer"
cb(res) cb(res)
@ -753,13 +753,13 @@ func (c *Conn) populateCLIPingResponseLocked(res *ipnstate.PingResult, latency t
// GetEndpointChanges returns the most recent changes for a particular // GetEndpointChanges returns the most recent changes for a particular
// endpoint. The returned EndpointChange structs are for debug use only and // endpoint. The returned EndpointChange structs are for debug use only and
// there are no guarantees about order, size, or content. // 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() c.mu.Lock()
if c.privateKey.IsZero() { if c.privateKey.IsZero() {
c.mu.Unlock() c.mu.Unlock()
return nil, fmt.Errorf("tailscaled stopped") return nil, fmt.Errorf("tailscaled stopped")
} }
ep, ok := c.peerMap.endpointForNodeKey(peer.Key) ep, ok := c.peerMap.endpointForNodeKey(peer.Key())
c.mu.Unlock() c.mu.Unlock()
if !ok { 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) { if len(x) != len(y) {
return false 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 // we'll fall through to the next pass, which allocates but can
// handle full set updates. // handle full set updates.
for _, n := range nm.Peers { for _, n := range nm.Peers {
if ep, ok := c.peerMap.endpointForNodeKey(n.Key); ok { if ep, ok := c.peerMap.endpointForNodeKey(n.Key()); ok {
if n.DiscoKey.IsZero() && !n.IsWireGuardOnly { if n.DiscoKey().IsZero() && !n.IsWireGuardOnly() {
// Discokey transitioned from non-zero to zero? This should not // Discokey transitioned from non-zero to zero? This should not
// happen in the wild, however it could mean: // happen in the wild, however it could mean:
// 1. A node was downgraded from post 0.100 to pre 0.100. // 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 c.peerMap.upsertEndpoint(ep, oldDiscoKey) // maybe update discokey mappings in peerMap
continue 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. // Ancient pre-0.100 node, which does not have a disco key, and will only be reachable via DERP.
continue continue
} }
@ -1828,30 +1828,30 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) {
ep := &endpoint{ ep := &endpoint{
c: c, c: c,
debugUpdates: ringbuffer.New[EndpointChange](entriesPerBuffer), debugUpdates: ringbuffer.New[EndpointChange](entriesPerBuffer),
publicKey: n.Key, publicKey: n.Key(),
publicKeyHex: n.Key.UntypedHexString(), publicKeyHex: n.Key().UntypedHexString(),
sentPing: map[stun.TxID]sentPing{}, sentPing: map[stun.TxID]sentPing{},
endpointState: map[netip.AddrPort]*endpointState{}, endpointState: map[netip.AddrPort]*endpointState{},
heartbeatDisabled: heartbeatDisabled, heartbeatDisabled: heartbeatDisabled,
isWireguardOnly: n.IsWireGuardOnly, isWireguardOnly: n.IsWireGuardOnly(),
} }
if len(n.Addresses) > 0 { if n.Addresses().Len() > 0 {
ep.nodeAddr = n.Addresses[0].Addr() ep.nodeAddr = n.Addresses().At(0).Addr()
} }
ep.initFakeUDPAddr() ep.initFakeUDPAddr()
if n.DiscoKey.IsZero() { if n.DiscoKey().IsZero() {
ep.disco.Store(nil) ep.disco.Store(nil)
} else { } else {
ep.disco.Store(&endpointDisco{ ep.disco.Store(&endpointDisco{
key: n.DiscoKey, key: n.DiscoKey(),
short: n.DiscoKey.ShortString(), short: n.DiscoKey().ShortString(),
}) })
if debugDisco() { // rather than making a new knob 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:" const derpPrefix = "127.3.3.40:"
if strings.HasPrefix(n.DERP, derpPrefix) { if strings.HasPrefix(n.DERP(), derpPrefix) {
ipp, _ := netip.ParseAddrPort(n.DERP) ipp, _ := netip.ParseAddrPort(n.DERP())
regionID := int(ipp.Port()) regionID := int(ipp.Port())
code := c.derpRegionCodeLocked(regionID) code := c.derpRegionCodeLocked(regionID)
if code != "" { if code != "" {
@ -1860,14 +1860,16 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) {
fmt.Fprintf(w, "derp=%v%s ", regionID, code) 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() { if a.IsSingleIP() {
fmt.Fprintf(w, "aip=%v ", a.Addr()) fmt.Fprintf(w, "aip=%v ", a.Addr())
} else { } else {
fmt.Fprintf(w, "aip=%v ", a) 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) fmt.Fprintf(w, "ep=%v ", ep)
} }
})) }))
@ -1885,7 +1887,7 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) {
if c.peerMap.nodeCount() != len(nm.Peers) { if c.peerMap.nodeCount() != len(nm.Peers) {
keep := make(map[key.NodePublic]bool, len(nm.Peers)) keep := make(map[key.NodePublic]bool, len(nm.Peers))
for _, n := range nm.Peers { for _, n := range nm.Peers {
keep[n.Key] = true keep[n.Key()] = true
} }
c.peerMap.forEachEndpoint(func(ep *endpoint) { c.peerMap.forEachEndpoint(func(ep *endpoint) {
if !keep[ep.publicKey] { if !keep[ep.publicKey] {

@ -284,7 +284,7 @@ func meshStacks(logf logger.Logf, mutateNetmap func(idx int, nm *netmap.NetworkM
Endpoints: epStrings(eps[i]), Endpoints: epStrings(eps[i]),
DERP: "127.3.3.40:1", DERP: "127.3.3.40:1",
} }
nm.Peers = append(nm.Peers, peer) nm.Peers = append(nm.Peers, peer.View())
} }
if mutateNetmap != nil { if mutateNetmap != nil {
@ -304,7 +304,7 @@ func meshStacks(logf logger.Logf, mutateNetmap func(idx int, nm *netmap.NetworkM
m.conn.SetNetworkMap(nm) m.conn.SetNetworkMap(nm)
peerSet := make(map[key.NodePublic]struct{}, len(nm.Peers)) peerSet := make(map[key.NodePublic]struct{}, len(nm.Peers))
for _, peer := range nm.Peers { for _, peer := range nm.Peers {
peerSet[peer.Key] = struct{}{} peerSet[peer.Key()] = struct{}{}
} }
m.conn.UpdatePeers(peerSet) m.conn.UpdatePeers(peerSet)
wg, err := nmcfg.WGCfg(nm, logf, netmap.AllowSingleHosts, "") wg, err := nmcfg.WGCfg(nm, logf, netmap.AllowSingleHosts, "")
@ -657,7 +657,9 @@ func TestDiscokeyChange(t *testing.T) {
} }
mu.Lock() mu.Lock()
defer mu.Unlock() 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) 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})) discoKey := key.DiscoPublicFromRaw32(mem.B([]byte{31: 1}))
nodeKey := key.NodePublicFromRaw32(mem.B([]byte{0: 'N', 1: 'K', 31: 0})) nodeKey := key.NodePublicFromRaw32(mem.B([]byte{0: 'N', 1: 'K', 31: 0}))
conn.SetNetworkMap(&netmap.NetworkMap{ conn.SetNetworkMap(&netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: nodeKey, Key: nodeKey,
DiscoKey: discoKey, DiscoKey: discoKey,
Endpoints: []string{sendConn.LocalAddr().String()}, Endpoints: []string{sendConn.LocalAddr().String()},
}, },
}, }),
}) })
conn.SetPrivateKey(key.NodePrivateFromRaw32(mem.B([]byte{0: 1, 31: 0}))) conn.SetPrivateKey(key.NodePrivateFromRaw32(mem.B([]byte{0: 1, 31: 0})))
_, err := conn.ParseEndpoint(nodeKey.UntypedHexString()) _, 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 // 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. // 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})) nodeKey2 := key.NodePublicFromRaw32(mem.B([]byte{0: 'N', 1: 'K', 2: '2', 31: 0}))
conn.SetNetworkMap(&netmap.NetworkMap{ conn.SetNetworkMap(&netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: nodeKey1, Key: nodeKey1,
DiscoKey: discoKey, DiscoKey: discoKey,
Endpoints: []string{"192.168.1.2:345"}, Endpoints: []string{"192.168.1.2:345"},
}, },
}, }),
}) })
_, err := conn.ParseEndpoint(nodeKey1.UntypedHexString()) _, err := conn.ParseEndpoint(nodeKey1.UntypedHexString())
if err != nil { if err != nil {
@ -1459,13 +1469,13 @@ func TestSetNetworkMapChangingNodeKey(t *testing.T) {
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
conn.SetNetworkMap(&netmap.NetworkMap{ conn.SetNetworkMap(&netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: nodeKey2, Key: nodeKey2,
DiscoKey: discoKey, DiscoKey: discoKey,
Endpoints: []string{"192.168.1.2:345"}, Endpoints: []string{"192.168.1.2:345"},
}, },
}, }),
}) })
} }
@ -1792,7 +1802,7 @@ func TestStressSetNetworkMap(t *testing.T) {
} }
// Set the netmap. // Set the netmap.
conn.SetNetworkMap(&netmap.NetworkMap{ conn.SetNetworkMap(&netmap.NetworkMap{
Peers: peers, Peers: nodeViews(peers),
}) })
// Check invariants. // Check invariants.
if err := conn.peerMap.validate(); err != nil { if err := conn.peerMap.validate(); err != nil {
@ -2237,7 +2247,7 @@ func TestIsWireGuardOnlyPeer(t *testing.T) {
PrivateKey: m.privateKey, PrivateKey: m.privateKey,
NodeKey: m.privateKey.Public(), NodeKey: m.privateKey.Public(),
Addresses: []netip.Prefix{tsaip}, Addresses: []netip.Prefix{tsaip},
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: wgkey.Public(), Key: wgkey.Public(),
Endpoints: []string{wgEp.String()}, Endpoints: []string{wgEp.String()},
@ -2245,7 +2255,7 @@ func TestIsWireGuardOnlyPeer(t *testing.T) {
Addresses: []netip.Prefix{wgaip}, Addresses: []netip.Prefix{wgaip},
AllowedIPs: []netip.Prefix{wgaip}, AllowedIPs: []netip.Prefix{wgaip},
}, },
}, }),
} }
m.conn.SetNetworkMap(nm) m.conn.SetNetworkMap(nm)
@ -2295,7 +2305,7 @@ func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) {
PrivateKey: m.privateKey, PrivateKey: m.privateKey,
NodeKey: m.privateKey.Public(), NodeKey: m.privateKey.Public(),
Addresses: []netip.Prefix{tsaip}, Addresses: []netip.Prefix{tsaip},
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: wgkey.Public(), Key: wgkey.Public(),
Endpoints: []string{wgEp.String()}, Endpoints: []string{wgEp.String()},
@ -2304,7 +2314,7 @@ func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) {
AllowedIPs: []netip.Prefix{wgaip}, AllowedIPs: []netip.Prefix{wgaip},
SelfNodeV4MasqAddrForThisPeer: ptr.To(masqip.Addr()), SelfNodeV4MasqAddrForThisPeer: ptr.To(masqip.Addr()),
}, },
}, }),
} }
m.conn.SetNetworkMap(nm) m.conn.SetNetworkMap(nm)
@ -2421,7 +2431,7 @@ func TestIsWireGuardOnlyPickEndpointByPing(t *testing.T) {
PrivateKey: m.privateKey, PrivateKey: m.privateKey,
NodeKey: m.privateKey.Public(), NodeKey: m.privateKey.Public(),
Addresses: []netip.Prefix{tsaip}, Addresses: []netip.Prefix{tsaip},
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: wgkey.Public(), Key: wgkey.Public(),
Endpoints: []string{wgEp.String(), wgEp2.String(), wgEpV6.String()}, Endpoints: []string{wgEp.String(), wgEp2.String(), wgEpV6.String()},
@ -2429,7 +2439,7 @@ func TestIsWireGuardOnlyPickEndpointByPing(t *testing.T) {
Addresses: []netip.Prefix{wgaip}, Addresses: []netip.Prefix{wgaip},
AllowedIPs: []netip.Prefix{wgaip}, AllowedIPs: []netip.Prefix{wgaip},
}, },
}, }),
} }
applyNetworkMap(t, m, nm) applyNetworkMap(t, m, nm)

@ -146,21 +146,22 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
return return
} }
n := pip.Node n := pip.Node
if !n.IsWireGuardOnly { if !n.IsWireGuardOnly() {
if n.DiscoKey.IsZero() { if n.DiscoKey().IsZero() {
e.logf("open-conn-track: timeout opening %v; peer node %v running pre-0.100", flow, n.Key.ShortString()) e.logf("open-conn-track: timeout opening %v; peer node %v running pre-0.100", flow, n.Key().ShortString())
return return
} }
if n.DERP == "" { if n.DERP() == "" {
e.logf("open-conn-track: timeout opening %v; peer node %v not connected to any DERP relay", flow, n.Key.ShortString()) e.logf("open-conn-track: timeout opening %v; peer node %v not connected to any DERP relay", flow, n.Key().ShortString())
return return
} }
} }
ps, found := e.getPeerStatusLite(n.Key) ps, found := e.getPeerStatusLite(n.Key())
if !found { if !found {
onlyZeroRoute := true // whether peerForIP returned n only because its /0 route matched 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()) { if r.Bits() != 0 && r.Contains(flow.Dst.Addr()) {
onlyZeroRoute = false onlyZeroRoute = false
break break
@ -178,7 +179,7 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
// node. // node.
return 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 return
} }
@ -189,24 +190,24 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
_ = ps.LastHandshake _ = ps.LastHandshake
online := "?" online := "?"
if n.IsWireGuardOnly { if n.IsWireGuardOnly() {
online = "wg" online = "wg"
} else { } else {
if n.Online != nil { if v := n.Online(); v != nil {
if *n.Online { if *v {
online = "yes" online = "yes"
} else { } else {
online = "no" online = "no"
} }
} }
if n.LastSeen != nil && online != "yes" { if n.LastSeen() != nil && online != "yes" {
online += fmt.Sprintf(", lastseen=%v", durFmt(*n.LastSeen)) online += fmt.Sprintf(", lastseen=%v", durFmt(*n.LastSeen()))
} }
} }
e.logf("open-conn-track: timeout opening %v to node %v; online=%v, lastRecv=%v", e.logf("open-conn-track: timeout opening %v to node %v; online=%v, lastRecv=%v",
flow, n.Key.ShortString(), flow, n.Key().ShortString(),
online, online,
e.magicConn.LastRecvActivityOfNodeKey(n.Key)) e.magicConn.LastRecvActivityOfNodeKey(n.Key()))
} }
func durFmt(t time.Time) string { func durFmt(t time.Time) string {

@ -1226,7 +1226,7 @@ func (e *userspaceEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, size in
} }
peer := pip.Node 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 { switch pingType {
case "disco": case "disco":
e.magicConn.Ping(peer, res, size, cb) 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") 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) srcIP, err := e.mySelfIPMatchingFamily(destIP)
if err != nil { if err != nil {
res.Err = err.Error() res.Err = err.Error()
@ -1295,7 +1295,7 @@ func (e *userspaceEngine) sendICMPEchoRequest(destIP netip.Addr, peer *tailcfg.N
d := time.Since(t0) d := time.Since(t0)
res.LatencySeconds = d.Seconds() res.LatencySeconds = d.Seconds()
res.NodeIP = destIP.String() res.NodeIP = destIP.String()
res.NodeName = peer.ComputedName res.NodeName = peer.ComputedName()
cb(res) cb(res)
}) })
@ -1303,7 +1303,7 @@ func (e *userspaceEngine) sendICMPEchoRequest(destIP netip.Addr, peer *tailcfg.N
e.tundev.InjectOutbound(icmpPing) 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) srcIP, err := e.mySelfIPMatchingFamily(ip)
if err != nil { if err != nil {
res.Err = err.Error() res.Err = err.Error()
@ -1337,7 +1337,7 @@ func (e *userspaceEngine) sendTSMPPing(ip netip.Addr, peer *tailcfg.Node, res *i
d := time.Since(t0) d := time.Since(t0)
res.LatencySeconds = d.Seconds() res.LatencySeconds = d.Seconds()
res.NodeIP = ip.String() res.NodeIP = ip.String()
res.NodeName = peer.ComputedName res.NodeName = peer.ComputedName()
res.PeerAPIPort = pong.PeerAPIPort res.PeerAPIPort = pong.PeerAPIPort
cb(res) 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. // Check for exact matches before looking for subnet matches.
// TODO(bradfitz): add maps for these. on NetworkMap? // TODO(bradfitz): add maps for these. on NetworkMap?
for _, p := range nm.Peers { 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) { if a.Addr() == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) {
return PeerForIP{Node: p, Route: a}, true 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 { for _, a := range nm.Addresses {
if a.Addr() == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) { 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. // call. But TODO(bradfitz): add a lookup map to netmap.NetworkMap.
if !bestKey.IsZero() { if !bestKey.IsZero() {
for _, p := range nm.Peers { for _, p := range nm.Peers {
if p.Key == bestKey { if p.Key() == bestKey {
return PeerForIP{Node: p, Route: best}, true return PeerForIP{Node: p, Route: best}, true
} }
} }

@ -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) { func TestUserspaceEngineReconfig(t *testing.T) {
e, err := NewFakeUserspaceEngine(t.Logf, 0) e, err := NewFakeUserspaceEngine(t.Logf, 0)
if err != nil { if err != nil {
@ -99,11 +107,11 @@ func TestUserspaceEngineReconfig(t *testing.T) {
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
} { } {
nm := &netmap.NetworkMap{ nm := &netmap.NetworkMap{
Peers: []*tailcfg.Node{ Peers: nodeViews([]*tailcfg.Node{
{ {
Key: nkFromHex(nodeHex), Key: nkFromHex(nodeHex),
}, },
}, }),
} }
nk, err := key.ParseNodePublicUntyped(mem.S(nodeHex)) nk, err := key.ParseNodePublicUntyped(mem.S(nodeHex))
if err != nil { if err != nil {

@ -19,30 +19,31 @@ import (
"tailscale.com/wgengine/wgcfg" "tailscale.com/wgengine/wgcfg"
) )
func nodeDebugName(n *tailcfg.Node) string { func nodeDebugName(n tailcfg.NodeView) string {
name := n.Name name := n.Name()
if name == "" { if name == "" {
name = n.Hostinfo.Hostname() name = n.Hostinfo().Hostname()
} }
if i := strings.Index(name, "."); i != -1 { if i := strings.Index(name, "."); i != -1 {
name = name[:i] name = name[:i]
} }
if name == "" && len(n.Addresses) != 0 { if name == "" && n.Addresses().Len() != 0 {
return n.Addresses[0].String() return n.Addresses().At(0).String()
} }
return name return name
} }
// cidrIsSubnet reports whether cidr is a non-default-route subnet // cidrIsSubnet reports whether cidr is a non-default-route subnet
// exported by node that is not one of its own self addresses. // 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 { if cidr.Bits() == 0 {
return false return false
} }
if !cidr.IsSingleIP() { if !cidr.IsSingleIP() {
return true return true
} }
for _, selfCIDR := range node.Addresses { for i := range node.Addresses().LenIter() {
selfCIDR := node.Addresses().At(i)
if cidr == selfCIDR { if cidr == selfCIDR {
return false return false
} }
@ -85,22 +86,23 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags,
skippedSubnets := new(bytes.Buffer) skippedSubnets := new(bytes.Buffer)
for _, peer := range nm.Peers { 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 // Peer predates both DERP and active discovery, we cannot
// communicate with it. // 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 continue
} }
cfg.Peers = append(cfg.Peers, wgcfg.Peer{ cfg.Peers = append(cfg.Peers, wgcfg.Peer{
PublicKey: peer.Key, PublicKey: peer.Key(),
DiscoKey: peer.DiscoKey, DiscoKey: peer.DiscoKey(),
}) })
cpeer := &cfg.Peers[len(cfg.Peers)-1] cpeer := &cfg.Peers[len(cfg.Peers)-1]
didExitNodeWarn := false didExitNodeWarn := false
cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer()
for _, allowedIP := range peer.AllowedIPs { for i := range peer.AllowedIPs().LenIter() {
if allowedIP.Bits() == 0 && peer.StableID != exitNode { allowedIP := peer.AllowedIPs().At(i)
if allowedIP.Bits() == 0 && peer.StableID() != exitNode {
if didExitNodeWarn { if didExitNodeWarn {
// Don't log about both the IPv4 /0 and IPv6 /0. // Don't log about both the IPv4 /0 and IPv6 /0.
continue continue
@ -109,20 +111,20 @@ func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags,
if skippedUnselected.Len() > 0 { if skippedUnselected.Len() > 0 {
skippedUnselected.WriteString(", ") skippedUnselected.WriteString(", ")
} }
fmt.Fprintf(skippedUnselected, "%q (%v)", nodeDebugName(peer), peer.Key.ShortString()) fmt.Fprintf(skippedUnselected, "%q (%v)", nodeDebugName(peer), peer.Key().ShortString())
continue continue
} else if allowedIP.IsSingleIP() && tsaddr.IsTailscaleIP(allowedIP.Addr()) && (flags&netmap.AllowSingleHosts) == 0 { } else if allowedIP.IsSingleIP() && tsaddr.IsTailscaleIP(allowedIP.Addr()) && (flags&netmap.AllowSingleHosts) == 0 {
if skippedIPs.Len() > 0 { if skippedIPs.Len() > 0 {
skippedIPs.WriteString(", ") 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 continue
} else if cidrIsSubnet(peer, allowedIP) { } else if cidrIsSubnet(peer, allowedIP) {
if (flags & netmap.AllowSubnetRoutes) == 0 { if (flags & netmap.AllowSubnetRoutes) == 0 {
if skippedSubnets.Len() > 0 { if skippedSubnets.Len() > 0 {
skippedSubnets.WriteString(", ") 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 continue
} }
} }

@ -52,9 +52,9 @@ var ErrNoChanges = errors.New("no changes made to Engine config")
// PeerForIP is the type returned by Engine.PeerForIP. // PeerForIP is the type returned by Engine.PeerForIP.
type PeerForIP struct { 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. // Engine.PeerForIP returns ok==true.
Node *tailcfg.Node Node tailcfg.NodeView
// IsSelf is whether the Node is the local process. // IsSelf is whether the Node is the local process.
IsSelf bool IsSelf bool

Loading…
Cancel
Save