WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -849,7 +849,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/types/lazy from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/logger from tailscale.com/appc+
|
||||
tailscale.com/types/logid from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/types/netlogfunc from tailscale.com/net/tstun+
|
||||
tailscale.com/types/netlogtype from tailscale.com/wgengine/netlog
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
|
||||
@@ -140,7 +140,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/lazy from tailscale.com/hostinfo+
|
||||
tailscale.com/types/logger from tailscale.com/appc+
|
||||
tailscale.com/types/logid from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/types/netlogfunc from tailscale.com/net/tstun+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/net/batching+
|
||||
|
||||
@@ -159,7 +159,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/lazy from tailscale.com/hostinfo+
|
||||
tailscale.com/types/logger from tailscale.com/appc+
|
||||
tailscale.com/types/logid from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/types/netlogfunc from tailscale.com/net/tstun+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/net/batching+
|
||||
|
||||
@@ -426,7 +426,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/lazy from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/logger from tailscale.com/appc+
|
||||
tailscale.com/types/logid from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/types/netlogfunc from tailscale.com/net/tstun+
|
||||
tailscale.com/types/netlogtype from tailscale.com/wgengine/netlog
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
|
||||
@@ -247,7 +247,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/types/lazy from tailscale.com/cmd/tsidp+
|
||||
tailscale.com/types/logger from tailscale.com/appc+
|
||||
tailscale.com/types/logid from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/types/netlogfunc from tailscale.com/net/tstun+
|
||||
tailscale.com/types/netlogtype from tailscale.com/wgengine/netlog
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
|
||||
+1
-59
@@ -658,6 +658,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
e.SetPeerSessionStateFunc(b.onPeerWireGuardState)
|
||||
e.SetNetLogNodeSource(netLogNodeSource{b})
|
||||
e.SetWGPeerLookup(b.lookupPeerWireGuardString)
|
||||
b.dialer.SetResolveMagicDNS(b.resolveMagicDNS)
|
||||
|
||||
if sys.InitialConfig != nil {
|
||||
if err := b.initPrefsFromConfig(sys.InitialConfig); err != nil {
|
||||
@@ -5817,64 +5818,6 @@ func (b *LocalBackend) NetMapWithPeers() *netmap.NetworkMap {
|
||||
return b.currentNode().netMapWithPeers()
|
||||
}
|
||||
|
||||
// lookupPeerByIP returns the node public key for the peer that owns the
|
||||
// given IP address. It is the fast path for [Engine.SetPeerByIPPacketFunc],
|
||||
// handling exact-IP matches against node addresses; subnet routes and exit
|
||||
// nodes are handled by a BART-based fallback in userspaceEngine that uses
|
||||
// the wireguard-filtered peer list (see lastCfgFull).
|
||||
//
|
||||
// It is called by wireguard-go on every outbound packet (not cached), so
|
||||
// it must be fast.
|
||||
func (b *LocalBackend) lookupPeerByIP(ip netip.Addr) (key.NodePublic, bool) {
|
||||
nb := b.currentNode()
|
||||
nid, ok := nb.NodeByAddr(ip)
|
||||
if !ok {
|
||||
return key.NodePublic{}, false
|
||||
}
|
||||
peer, ok := nb.NodeByID(nid)
|
||||
if !ok {
|
||||
return key.NodePublic{}, false
|
||||
}
|
||||
return peer.Key(), true
|
||||
}
|
||||
|
||||
// peerForIP is the [wgengine.Engine.SetPeerForIPFunc] callback. It returns
|
||||
// which peer is responsible for a given IP address. Despite the name, it
|
||||
// can also return the self node (with IsSelf set). It handles both
|
||||
// Tailscale IPs (returning the owning peer or self) and non-Tailscale
|
||||
// addresses like subnet-routed IPs or exit-node global internet IPs
|
||||
// (returning whichever peer would route that traffic).
|
||||
func (b *LocalBackend) peerForIP(ip netip.Addr) (_ wgengine.PeerForIP, ok bool) {
|
||||
nb := b.currentNode()
|
||||
|
||||
if tsaddr.IsTailscaleIP(ip) {
|
||||
if nid, ok := nb.NodeByAddr(ip); ok {
|
||||
if n, ok := nb.NodeByID(nid); ok {
|
||||
self := nb.Self()
|
||||
return wgengine.PeerForIP{
|
||||
Node: n,
|
||||
IsSelf: self.Valid() && self.ID() == nid,
|
||||
Route: netip.PrefixFrom(ip, ip.BitLen()),
|
||||
}, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pk, route, ok := b.e.PeerKeyForIP(ip)
|
||||
if !ok {
|
||||
return wgengine.PeerForIP{}, false
|
||||
}
|
||||
nid, ok := nb.NodeByKey(pk)
|
||||
if !ok {
|
||||
return wgengine.PeerForIP{}, false
|
||||
}
|
||||
n, ok := nb.NodeByID(nid)
|
||||
if !ok {
|
||||
return wgengine.PeerForIP{}, false
|
||||
}
|
||||
return wgengine.PeerForIP{Node: n, Route: route}, true
|
||||
}
|
||||
|
||||
// onPeerWireGuardState is called by wireguard-go, through wgengine, for
|
||||
// serialized WireGuard session state transitions. wireguard-go is holding locks
|
||||
// while calling this, so this must stay cheap, must not acquire b.mu, and must
|
||||
@@ -7356,7 +7299,6 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
oldNetMap := b.currentNode().NetMap()
|
||||
oldSelf := oldNetMap.SelfNodeOrZero()
|
||||
|
||||
b.dialer.SetNetMap(nm)
|
||||
if ns, ok := b.sys.Netstack.GetOK(); ok {
|
||||
ns.UpdateNetstackIPs(nm)
|
||||
}
|
||||
|
||||
+118
-66
@@ -9,6 +9,7 @@ import (
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/mapx"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
@@ -125,6 +127,17 @@ type nodeBackend struct {
|
||||
// It is mutated in place (with mu held) and must not escape the [nodeBackend].
|
||||
nodeByWGString map[string]tailcfg.NodeID
|
||||
|
||||
// nodeByName maps MagicDNS hostnames (lowercase, no trailing dot) to
|
||||
// node IDs. Both the FQDN and the short name (suffix stripped) are
|
||||
// keys. It is used by the tsdial MagicDNS resolution callback.
|
||||
// It is mutated in place (with mu held) and must not escape the [nodeBackend].
|
||||
nodeByName map[string]tailcfg.NodeID
|
||||
|
||||
// extraDNS stores DNS.ExtraRecords A/AAAA entries from the netmap
|
||||
// (typically service VIPs pushed by control), keyed by canonicalized
|
||||
// hostname (lowercase, no trailing dot).
|
||||
extraDNS map[string]netip.Addr
|
||||
|
||||
// userProfiles is the live set of user profiles, updated incrementally
|
||||
// by mergeUserProfiles as deltas arrive. It parallels the peers map:
|
||||
// netMap.UserProfiles is the frozen snapshot from the last full install,
|
||||
@@ -507,6 +520,7 @@ func (nb *nodeBackend) SetNetMap(nm *netmap.NetworkMap) {
|
||||
nb.netMap = nm
|
||||
nb.updateNodeByAddrLocked()
|
||||
nb.updateNodeByKeyLocked()
|
||||
nb.updateNodeByNameLocked()
|
||||
nb.updatePeersLocked()
|
||||
nb.signalKeyWaitersForTestLocked()
|
||||
if nm != nil {
|
||||
@@ -566,33 +580,21 @@ func (nb *nodeBackend) updateNodeByAddrLocked() {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the nodeByAddr index.
|
||||
if nb.nodeByAddr == nil {
|
||||
nb.nodeByAddr = map[netip.Addr]tailcfg.NodeID{}
|
||||
}
|
||||
// First pass, mark everything unwanted.
|
||||
for k := range nb.nodeByAddr {
|
||||
nb.nodeByAddr[k] = 0
|
||||
}
|
||||
addNode := func(n tailcfg.NodeView) {
|
||||
addNodeAddr := func(n tailcfg.NodeView) {
|
||||
for _, ipp := range n.Addresses().All() {
|
||||
if ipp.IsSingleIP() {
|
||||
nb.nodeByAddr[ipp.Addr()] = n.ID()
|
||||
}
|
||||
}
|
||||
}
|
||||
if nm.SelfNode.Valid() {
|
||||
addNode(nm.SelfNode)
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
addNode(p)
|
||||
}
|
||||
// Third pass, actually delete the unwanted items.
|
||||
for k, v := range nb.nodeByAddr {
|
||||
if v == 0 {
|
||||
delete(nb.nodeByAddr, k)
|
||||
mapx.RepopulateNonzero(&nb.nodeByAddr, func() {
|
||||
if nm.SelfNode.Valid() {
|
||||
addNodeAddr(nm.SelfNode)
|
||||
}
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
addNodeAddr(p)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (nb *nodeBackend) updateNodeByKeyLocked() {
|
||||
@@ -603,40 +605,99 @@ func (nb *nodeBackend) updateNodeByKeyLocked() {
|
||||
return
|
||||
}
|
||||
|
||||
if nb.nodeByKey == nil {
|
||||
nb.nodeByKey = map[key.NodePublic]tailcfg.NodeID{}
|
||||
}
|
||||
if nb.nodeByWGString == nil {
|
||||
nb.nodeByWGString = map[string]tailcfg.NodeID{}
|
||||
}
|
||||
// First pass, mark everything unwanted.
|
||||
for k := range nb.nodeByKey {
|
||||
nb.nodeByKey[k] = 0
|
||||
}
|
||||
for k := range nb.nodeByWGString {
|
||||
nb.nodeByWGString[k] = 0
|
||||
}
|
||||
addNode := func(n tailcfg.NodeView) {
|
||||
nb.nodeByKey[n.Key()] = n.ID()
|
||||
nb.nodeByWGString[n.Key().WireGuardGoString()] = n.ID()
|
||||
}
|
||||
if nm.SelfNode.Valid() {
|
||||
addNode(nm.SelfNode)
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
addNode(p)
|
||||
}
|
||||
// Third pass, actually delete the unwanted items.
|
||||
for k, v := range nb.nodeByKey {
|
||||
if v == 0 {
|
||||
delete(nb.nodeByKey, k)
|
||||
mapx.RepopulateNonzero(&nb.nodeByKey, func() {
|
||||
if nm.SelfNode.Valid() {
|
||||
nb.nodeByKey[nm.SelfNode.Key()] = nm.SelfNode.ID()
|
||||
}
|
||||
}
|
||||
for k, v := range nb.nodeByWGString {
|
||||
if v == 0 {
|
||||
delete(nb.nodeByWGString, k)
|
||||
for _, p := range nm.Peers {
|
||||
nb.nodeByKey[p.Key()] = p.ID()
|
||||
}
|
||||
})
|
||||
mapx.RepopulateNonzero(&nb.nodeByWGString, func() {
|
||||
if nm.SelfNode.Valid() {
|
||||
nb.nodeByWGString[nm.SelfNode.Key().WireGuardGoString()] = nm.SelfNode.ID()
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
nb.nodeByWGString[p.Key().WireGuardGoString()] = p.ID()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// addNodeNameLocked adds both the FQDN and short-name keys for the given
|
||||
// node to nb.nodeByName. nb.mu must be held.
|
||||
func (nb *nodeBackend) addNodeNameLocked(name string, nid tailcfg.NodeID) {
|
||||
if name == "" {
|
||||
// We might support name-less nodes in the future; tailscale/corp#43949
|
||||
return
|
||||
}
|
||||
canon := strings.ToLower(strings.TrimSuffix(name, "."))
|
||||
mak.Set(&nb.nodeByName, canon, nid)
|
||||
if suffix := nb.netMap.MagicDNSSuffix(); dnsname.HasSuffix(canon, suffix) {
|
||||
mak.Set(&nb.nodeByName, dnsname.TrimSuffix(canon, suffix), nid)
|
||||
}
|
||||
}
|
||||
|
||||
// removeNodeNameLocked removes both the FQDN and short-name keys for the
|
||||
// given node from nb.nodeByName. nb.mu must be held.
|
||||
func (nb *nodeBackend) removeNodeNameLocked(name string) {
|
||||
if name == "" {
|
||||
// We might support name-less nodes in the future; tailscale/corp#43949
|
||||
return
|
||||
}
|
||||
canon := strings.ToLower(strings.TrimSuffix(name, "."))
|
||||
delete(nb.nodeByName, canon)
|
||||
if suffix := nb.netMap.MagicDNSSuffix(); dnsname.HasSuffix(canon, suffix) {
|
||||
delete(nb.nodeByName, dnsname.TrimSuffix(canon, suffix))
|
||||
}
|
||||
}
|
||||
|
||||
func (nb *nodeBackend) updateNodeByNameLocked() {
|
||||
nm := nb.netMap
|
||||
if nm == nil {
|
||||
nb.nodeByName = nil
|
||||
nb.extraDNS = nil
|
||||
return
|
||||
}
|
||||
|
||||
mapx.RepopulateNonzero(&nb.nodeByName, func() {
|
||||
if nm.SelfNode.Valid() {
|
||||
nb.addNodeNameLocked(nm.SelfNode.Name(), nm.SelfNode.ID())
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
nb.addNodeNameLocked(p.Name(), p.ID())
|
||||
}
|
||||
})
|
||||
|
||||
// Rebuild extraDNS from DNS.ExtraRecords (service VIPs, etc).
|
||||
nb.extraDNS = nil
|
||||
for _, rec := range nm.DNS.ExtraRecords {
|
||||
if rec.Type != "" {
|
||||
continue
|
||||
}
|
||||
ip, err := netip.ParseAddr(rec.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
mak.Set(&nb.extraDNS, strings.ToLower(strings.TrimSuffix(rec.Name, ".")), ip)
|
||||
}
|
||||
}
|
||||
|
||||
// NodeByName returns the node ID for a MagicDNS hostname. The input
|
||||
// must be lowercase with no trailing dot; both short names ("foo") and
|
||||
// FQDNs ("foo.tail-scale.ts.net") are accepted.
|
||||
func (nb *nodeBackend) NodeByName(hostname string) (_ tailcfg.NodeID, ok bool) {
|
||||
nb.mu.Lock()
|
||||
defer nb.mu.Unlock()
|
||||
nid, ok := nb.nodeByName[hostname]
|
||||
return nid, ok
|
||||
}
|
||||
|
||||
// ExtraDNSByName returns the IP for a DNS.ExtraRecords entry (e.g. service VIPs).
|
||||
func (nb *nodeBackend) ExtraDNSByName(hostname string) (_ netip.Addr, ok bool) {
|
||||
nb.mu.Lock()
|
||||
defer nb.mu.Unlock()
|
||||
ip, ok := nb.extraDNS[hostname]
|
||||
return ip, ok
|
||||
}
|
||||
|
||||
func (nb *nodeBackend) updatePeersLocked() {
|
||||
@@ -646,22 +707,11 @@ func (nb *nodeBackend) updatePeersLocked() {
|
||||
return
|
||||
}
|
||||
|
||||
// First pass, mark everything unwanted.
|
||||
for k := range nb.peers {
|
||||
nb.peers[k] = tailcfg.NodeView{}
|
||||
}
|
||||
|
||||
// Second pass, add everything wanted.
|
||||
for _, p := range nm.Peers {
|
||||
mak.Set(&nb.peers, p.ID(), p)
|
||||
}
|
||||
|
||||
// Third pass, remove deleted things.
|
||||
for k, v := range nb.peers {
|
||||
if !v.Valid() {
|
||||
delete(nb.peers, k)
|
||||
mapx.RepopulateNonzero(&nb.peers, func() {
|
||||
for _, p := range nm.Peers {
|
||||
nb.peers[p.ID()] = p
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// setPacketFilter stores the live packet filter rules and parsed
|
||||
@@ -717,6 +767,7 @@ func (nb *nodeBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
}
|
||||
mak.Set(&nb.nodeByKey, m.Node.Key(), nid)
|
||||
mak.Set(&nb.nodeByWGString, m.Node.Key().WireGuardGoString(), nid)
|
||||
nb.addNodeNameLocked(m.Node.Name(), nid)
|
||||
continue
|
||||
case netmap.NodeMutationRemove:
|
||||
nid := m.NodeIDBeingMutated()
|
||||
@@ -728,6 +779,7 @@ func (nb *nodeBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
}
|
||||
delete(nb.nodeByKey, old.Key())
|
||||
delete(nb.nodeByWGString, old.Key().WireGuardGoString())
|
||||
nb.removeNodeNameLocked(old.Name())
|
||||
delete(nb.peers, nid)
|
||||
}
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
// lookupPeerByIP returns the node public key for the peer that owns the
|
||||
// given IP address. It is the fast path for [Engine.SetPeerByIPPacketFunc],
|
||||
// handling exact-IP matches against node addresses; subnet routes and exit
|
||||
// nodes are handled by a BART-based fallback in userspaceEngine that uses
|
||||
// the wireguard-filtered peer list (see lastCfgFull).
|
||||
//
|
||||
// It is called by wireguard-go on every outbound packet (not cached), so
|
||||
// it must be fast.
|
||||
func (b *LocalBackend) lookupPeerByIP(ip netip.Addr) (key.NodePublic, bool) {
|
||||
nb := b.currentNode()
|
||||
nid, ok := nb.NodeByAddr(ip)
|
||||
if !ok {
|
||||
return key.NodePublic{}, false
|
||||
}
|
||||
peer, ok := nb.NodeByID(nid)
|
||||
if !ok {
|
||||
return key.NodePublic{}, false
|
||||
}
|
||||
return peer.Key(), true
|
||||
}
|
||||
|
||||
// resolveMagicDNS resolves a MagicDNS hostname to the owning node's IP
|
||||
// address, respecting the requested network address family ("tcp4",
|
||||
// "tcp6", "tcp", etc.). It accepts peer FQDNs ("foo.tail-scale.ts.net"),
|
||||
// short names ("foo"), and DNS.ExtraRecords entries (service VIPs).
|
||||
// The hostname must be lowercase with no trailing dot. It is installed
|
||||
// as the [tsdial.Dialer.SetResolveMagicDNS] callback.
|
||||
func (b *LocalBackend) resolveMagicDNS(hostname, network string) (_ netip.Addr, ok bool) {
|
||||
nb := b.currentNode()
|
||||
if nid, ok := nb.NodeByName(hostname); ok {
|
||||
n, ok := nb.NodeByID(nid)
|
||||
if !ok {
|
||||
b.logf("[unexpected] resolveMagicDNS: NodeByName(%q) returned node %v but NodeByID failed", hostname, nid)
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
if ip, ok := nodeAddrForNetwork(n, network); ok {
|
||||
return ip, true
|
||||
}
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
if ip, ok := nb.ExtraDNSByName(hostname); ok && addrFamilyMatch(ip, network) {
|
||||
return ip, true
|
||||
}
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
// nodeAddrForNetwork returns the best address from n for the given
|
||||
// network ("tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"). For
|
||||
// unqualified networks ("tcp", "udp"), it prefers IPv4.
|
||||
func nodeAddrForNetwork(n tailcfg.NodeView, network string) (_ netip.Addr, ok bool) {
|
||||
addrs := n.Addresses()
|
||||
if addrs.Len() == 0 {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
want4 := strings.HasSuffix(network, "4")
|
||||
want6 := strings.HasSuffix(network, "6")
|
||||
var v6 netip.Addr
|
||||
for _, pfx := range addrs.All() {
|
||||
ip := pfx.Addr()
|
||||
if want4 && ip.Is4() {
|
||||
return ip, true
|
||||
}
|
||||
if want6 && ip.Is6() {
|
||||
return ip, true
|
||||
}
|
||||
if !want4 && !want6 {
|
||||
if ip.Is4() {
|
||||
return ip, true
|
||||
}
|
||||
if !v6.IsValid() {
|
||||
v6 = ip
|
||||
}
|
||||
}
|
||||
}
|
||||
if v6.IsValid() {
|
||||
return v6, true
|
||||
}
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
// addrFamilyMatch reports whether ip is compatible with the requested
|
||||
// network address family.
|
||||
func addrFamilyMatch(ip netip.Addr, network string) bool {
|
||||
if strings.HasSuffix(network, "4") {
|
||||
return ip.Is4()
|
||||
}
|
||||
if strings.HasSuffix(network, "6") {
|
||||
return ip.Is6()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// peerForIP returns which peer is responsible for a given IP address.
|
||||
// Despite the name, it can also return the self node (with IsSelf set).
|
||||
// It handles both Tailscale IPs (returning the owning peer or self) and
|
||||
// non-Tailscale addresses like subnet-routed IPs or exit-node global
|
||||
// internet IPs (returning whichever peer would route that traffic).
|
||||
// It is installed as the [wgengine.Engine.SetPeerForIPFunc] callback.
|
||||
func (b *LocalBackend) peerForIP(ip netip.Addr) (_ wgengine.PeerForIP, ok bool) {
|
||||
nb := b.currentNode()
|
||||
|
||||
if tsaddr.IsTailscaleIP(ip) {
|
||||
if nid, ok := nb.NodeByAddr(ip); ok {
|
||||
n, ok := nb.NodeByID(nid)
|
||||
if !ok {
|
||||
b.logf("[unexpected] peerForIP: NodeByAddr(%v) returned node %v but NodeByID failed", ip, nid)
|
||||
return wgengine.PeerForIP{}, false
|
||||
}
|
||||
self := nb.Self()
|
||||
return wgengine.PeerForIP{
|
||||
Node: n,
|
||||
IsSelf: self.Valid() && self.ID() == nid,
|
||||
Route: netip.PrefixFrom(ip, ip.BitLen()),
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
pk, route, ok := b.e.PeerKeyForIP(ip)
|
||||
if !ok {
|
||||
return wgengine.PeerForIP{}, false
|
||||
}
|
||||
nid, ok := nb.NodeByKey(pk)
|
||||
if !ok {
|
||||
return wgengine.PeerForIP{}, false
|
||||
}
|
||||
n, ok := nb.NodeByID(nid)
|
||||
if !ok {
|
||||
return wgengine.PeerForIP{}, false
|
||||
}
|
||||
return wgengine.PeerForIP{Node: n, Route: route}, true
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
func TestResolveMagicDNS(t *testing.T) {
|
||||
b := newTestLocalBackend(t)
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
ID: 1,
|
||||
Name: "self.tail-scale.ts.net.",
|
||||
Key: makeNodeKeyFromID(1),
|
||||
DiscoKey: makeDiscoKeyFromID(1),
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.1/32"),
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
|
||||
},
|
||||
}).View(),
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
Name: "peer1.tail-scale.ts.net.",
|
||||
Key: makeNodeKeyFromID(2),
|
||||
DiscoKey: makeDiscoKeyFromID(2),
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.2/32"),
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
|
||||
},
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 3,
|
||||
Name: "v6only.tail-scale.ts.net.",
|
||||
Key: makeNodeKeyFromID(3),
|
||||
DiscoKey: makeDiscoKeyFromID(3),
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0::3/128"),
|
||||
},
|
||||
}).View(),
|
||||
},
|
||||
DNS: tailcfg.DNSConfig{
|
||||
ExtraRecords: []tailcfg.DNSRecord{
|
||||
{Name: "svc-foo.tail-scale.ts.net.", Value: "100.11.22.33"},
|
||||
},
|
||||
},
|
||||
}
|
||||
nm.Domain = "tail-scale.ts.net"
|
||||
b.currentNode().SetNetMap(nm)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
network string
|
||||
wantIP string
|
||||
wantOK bool
|
||||
}{
|
||||
{name: "fqdn", host: "peer1.tail-scale.ts.net", network: "tcp", wantIP: "100.64.0.2", wantOK: true},
|
||||
{name: "short_name", host: "peer1", network: "tcp", wantIP: "100.64.0.2", wantOK: true},
|
||||
{name: "self_fqdn", host: "self.tail-scale.ts.net", network: "tcp", wantIP: "100.64.0.1", wantOK: true},
|
||||
{name: "self_short", host: "self", network: "tcp", wantIP: "100.64.0.1", wantOK: true},
|
||||
{name: "tcp4", host: "peer1", network: "tcp4", wantIP: "100.64.0.2", wantOK: true},
|
||||
{name: "tcp6", host: "peer1", network: "tcp6", wantIP: "fd7a:115c:a1e0::2", wantOK: true},
|
||||
{name: "v6only_tcp", host: "v6only", network: "tcp", wantIP: "fd7a:115c:a1e0::3", wantOK: true},
|
||||
{name: "v6only_tcp4_miss", host: "v6only", network: "tcp4", wantOK: false},
|
||||
{name: "extra_record", host: "svc-foo.tail-scale.ts.net", network: "tcp", wantIP: "100.11.22.33", wantOK: true},
|
||||
{name: "extra_record_tcp6_miss", host: "svc-foo.tail-scale.ts.net", network: "tcp6", wantOK: false},
|
||||
{name: "unknown", host: "nope", network: "tcp", wantOK: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ip, ok := b.resolveMagicDNS(tt.host, tt.network)
|
||||
if ok != tt.wantOK {
|
||||
t.Fatalf("resolveMagicDNS(%q, %q): ok=%v, want %v", tt.host, tt.network, ok, tt.wantOK)
|
||||
}
|
||||
if ok && ip.String() != tt.wantIP {
|
||||
t.Fatalf("resolveMagicDNS(%q, %q): ip=%v, want %v", tt.host, tt.network, ip, tt.wantIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+4
-90
@@ -4,80 +4,21 @@
|
||||
package tsdial
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// dnsMap maps MagicDNS names (both base + FQDN) to their first IP.
|
||||
// It must not be mutated once created.
|
||||
//
|
||||
// Example keys are "foo.domain.tld.beta.tailscale.net" and "foo",
|
||||
// both without trailing dots, and both always lowercase.
|
||||
type dnsMap map[string]netip.Addr
|
||||
|
||||
// canonMapKey canonicalizes its input s to be a dnsMap map key.
|
||||
// canonMapKey canonicalizes its input s to be a MagicDNS lookup key:
|
||||
// lowercase with no trailing dot.
|
||||
func canonMapKey(s string) string {
|
||||
return strings.ToLower(strings.TrimSuffix(s, "."))
|
||||
}
|
||||
|
||||
func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
|
||||
if nm == nil {
|
||||
return nil
|
||||
}
|
||||
ret := make(dnsMap)
|
||||
suffix := nm.MagicDNSSuffix()
|
||||
have4 := false
|
||||
addrs := nm.GetAddresses()
|
||||
if name := nm.SelfName(); name != "" && addrs.Len() > 0 {
|
||||
ip := addrs.At(0).Addr()
|
||||
ret[canonMapKey(name)] = ip
|
||||
if dnsname.HasSuffix(name, suffix) {
|
||||
ret[canonMapKey(dnsname.TrimSuffix(name, suffix))] = ip
|
||||
}
|
||||
for _, p := range addrs.All() {
|
||||
if p.Addr().Is4() {
|
||||
have4 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.Name() == "" {
|
||||
continue
|
||||
}
|
||||
for _, pfx := range p.Addresses().All() {
|
||||
ip := pfx.Addr()
|
||||
if ip.Is4() && !have4 {
|
||||
continue
|
||||
}
|
||||
ret[canonMapKey(p.Name())] = ip
|
||||
if dnsname.HasSuffix(p.Name(), suffix) {
|
||||
ret[canonMapKey(dnsname.TrimSuffix(p.Name(), suffix))] = ip
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, rec := range nm.DNS.ExtraRecords {
|
||||
if rec.Type != "" {
|
||||
continue
|
||||
}
|
||||
ip, err := netip.ParseAddr(rec.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ret[canonMapKey(rec.Name)] = ip
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// errUnresolved is a sentinel error returned by dnsMap.resolveMemory.
|
||||
// errUnresolved is a sentinel error returned when a hostname is not
|
||||
// resolvable via MagicDNS.
|
||||
var errUnresolved = errors.New("address well formed but not resolved")
|
||||
|
||||
func splitHostPort(addr string) (host string, port uint16, err error) {
|
||||
@@ -91,30 +32,3 @@ func splitHostPort(addr string) (host string, port uint16, err error) {
|
||||
}
|
||||
return host, uint16(port16), nil
|
||||
}
|
||||
|
||||
// Resolve resolves addr into an IP:port using first the MagicDNS contents
|
||||
// of m, else using the system resolver.
|
||||
//
|
||||
// The error is [exactly] errUnresolved if the addr is a name that isn't known
|
||||
// in the map.
|
||||
func (m dnsMap) resolveMemory(ctx context.Context, network, addr string) (_ netip.AddrPort, err error) {
|
||||
host, port, err := splitHostPort(addr)
|
||||
if err != nil {
|
||||
// addr malformed or invalid port.
|
||||
return netip.AddrPort{}, err
|
||||
}
|
||||
if ip, err := netip.ParseAddr(host); err == nil {
|
||||
// addr was literal ip:port.
|
||||
return netip.AddrPortFrom(ip, port), nil
|
||||
}
|
||||
|
||||
// Host is not an IP, so assume it's a DNS name.
|
||||
|
||||
// Try MagicDNS first, otherwise a real DNS lookup.
|
||||
ip := m[canonMapKey(host)]
|
||||
if ip.IsValid() {
|
||||
return netip.AddrPortFrom(ip, port), nil
|
||||
}
|
||||
|
||||
return netip.AddrPort{}, errUnresolved
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tsdial
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView {
|
||||
nv := make([]tailcfg.NodeView, len(v))
|
||||
for i, n := range v {
|
||||
nv[i] = n.View()
|
||||
}
|
||||
return nv
|
||||
}
|
||||
|
||||
func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
pfx := netip.MustParsePrefix
|
||||
ip := netip.MustParseAddr
|
||||
tests := []struct {
|
||||
name string
|
||||
nm *netmap.NetworkMap
|
||||
want dnsMap
|
||||
}{
|
||||
{
|
||||
name: "self",
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Name: "foo.tailnet.",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.102.103.104/32"),
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
}).View(),
|
||||
},
|
||||
want: dnsMap{
|
||||
"foo": ip("100.102.103.104"),
|
||||
"foo.tailnet": ip("100.102.103.104"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "self_and_peers",
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Name: "foo.tailnet.",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.102.103.104/32"),
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
}).View(),
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
Name: "a.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.0.0.201/32"),
|
||||
pfx("100::201/128"),
|
||||
},
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
Name: "b.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::202/128"),
|
||||
},
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
want: dnsMap{
|
||||
"foo": ip("100.102.103.104"),
|
||||
"foo.tailnet": ip("100.102.103.104"),
|
||||
"a": ip("100.0.0.201"),
|
||||
"a.tailnet": ip("100.0.0.201"),
|
||||
"b": ip("100::202"),
|
||||
"b.tailnet": ip("100::202"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "self_has_v6_only",
|
||||
nm: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Name: "foo.tailnet.",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
}).View(),
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{
|
||||
Name: "a.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.0.0.201/32"),
|
||||
pfx("100::201/128"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "b.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::202/128"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
want: dnsMap{
|
||||
"foo": ip("100::123"),
|
||||
"foo.tailnet": ip("100::123"),
|
||||
"a": ip("100::201"),
|
||||
"a.tailnet": ip("100::201"),
|
||||
"b": ip("100::202"),
|
||||
"b.tailnet": ip("100::202"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := dnsMapFromNetworkMap(tt.nm)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("mismatch:\n got %v\nwant %v\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+37
-13
@@ -32,7 +32,6 @@ import (
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -90,9 +89,16 @@ type Dialer struct {
|
||||
|
||||
routes atomic.Pointer[bart.Table[bool]] // or nil if UserDial should not use routes. `true` indicates routes that point into the Tailscale interface
|
||||
|
||||
// resolveMagicDNS, if non-nil, resolves a MagicDNS hostname (short
|
||||
// name or FQDN, without trailing dot, lowercased) to an IP address.
|
||||
// The network parameter ("tcp", "tcp4", "tcp6", "udp", "udp4",
|
||||
// "udp6") constrains the address family of the result. The normal
|
||||
// implementation is [ipnlocal.LocalBackend.resolveMagicDNS],
|
||||
// installed at construction time. It is read without holding mu.
|
||||
resolveMagicDNS atomic.Pointer[func(hostname, network string) (_ netip.Addr, ok bool)]
|
||||
|
||||
mu syncs.Mutex
|
||||
closed bool
|
||||
dns dnsMap
|
||||
tunName string // tun device name
|
||||
netMon *netmon.Monitor
|
||||
netMonUnregister func()
|
||||
@@ -357,14 +363,34 @@ func (d *Dialer) PeerDialControlFunc() func(network, address string, c syscall.R
|
||||
return peerDialControlFunc(d)
|
||||
}
|
||||
|
||||
// SetNetMap sets the current network map and notably, the DNS names
|
||||
// in its DNS configuration.
|
||||
func (d *Dialer) SetNetMap(nm *netmap.NetworkMap) {
|
||||
m := dnsMapFromNetworkMap(nm)
|
||||
// SetResolveMagicDNS installs a callback that resolves MagicDNS hostnames
|
||||
// to IP addresses for UserDial.
|
||||
func (d *Dialer) SetResolveMagicDNS(fn func(hostname, network string) (_ netip.Addr, ok bool)) {
|
||||
if fn == nil {
|
||||
d.resolveMagicDNS.Store(nil)
|
||||
return
|
||||
}
|
||||
d.resolveMagicDNS.Store(&fn)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.dns = m
|
||||
// resolveAddr tries to resolve addr (a "host:port" string) via MagicDNS.
|
||||
// The network parameter ("tcp", "tcp4", "tcp6", etc.) constrains the
|
||||
// address family. It returns errUnresolved if the hostname is not a
|
||||
// known MagicDNS name.
|
||||
func (d *Dialer) resolveAddr(_ context.Context, network, addr string) (netip.AddrPort, error) {
|
||||
host, port, err := splitHostPort(addr)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, err
|
||||
}
|
||||
if ip, err := netip.ParseAddr(host); err == nil {
|
||||
return netip.AddrPortFrom(ip, port), nil
|
||||
}
|
||||
if fn := d.resolveMagicDNS.Load(); fn != nil {
|
||||
if ip, ok := (*fn)(canonMapKey(host), network); ok {
|
||||
return netip.AddrPortFrom(ip, port), nil
|
||||
}
|
||||
}
|
||||
return netip.AddrPort{}, errUnresolved
|
||||
}
|
||||
|
||||
// userDialResolveAll resolves addr as if a user initiating the dial.
|
||||
@@ -375,14 +401,12 @@ func (d *Dialer) SetNetMap(nm *netmap.NetworkMap) {
|
||||
// non-empty on a nil-error return.
|
||||
func (d *Dialer) userDialResolveAll(ctx context.Context, network, addr string) ([]netip.AddrPort, error) {
|
||||
d.mu.Lock()
|
||||
dns := d.dns
|
||||
exitDNSDoH := d.exitDNSDoHBase
|
||||
d.mu.Unlock()
|
||||
|
||||
// MagicDNS or otherwise baked into the NetworkMap? Try that first.
|
||||
// dns.resolveMemory returns a single address; tailnet names have
|
||||
// one IP each, so there's nothing to race.
|
||||
ipp, err := dns.resolveMemory(ctx, network, addr)
|
||||
// Tailnet names have one IP each, so there's nothing to race.
|
||||
ipp, err := d.resolveAddr(ctx, network, addr)
|
||||
if err != errUnresolved {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+1
-1
@@ -242,7 +242,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
tailscale.com/types/lazy from tailscale.com/hostinfo+
|
||||
tailscale.com/types/logger from tailscale.com/appc+
|
||||
tailscale.com/types/logid from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext
|
||||
tailscale.com/types/mapx from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/types/netlogfunc from tailscale.com/net/tstun+
|
||||
tailscale.com/types/netlogtype from tailscale.com/wgengine/netlog
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package mapx
|
||||
|
||||
// RepopulateNonzero re-uses an existing map (preserving its allocated
|
||||
// buckets) by zeroing all values, calling populate to re-fill it, and
|
||||
// then deleting any entries still at their zero value. If *m is nil it
|
||||
// is lazily initialized.
|
||||
//
|
||||
// This avoids the allocation cost of creating a new map on every call,
|
||||
// which matters for maps that are rebuilt frequently (e.g. on every
|
||||
// netmap update).
|
||||
func RepopulateNonzero[K comparable, V comparable](m *map[K]V, populate func()) {
|
||||
if *m == nil {
|
||||
*m = make(map[K]V)
|
||||
}
|
||||
var zero V
|
||||
for k := range *m {
|
||||
(*m)[k] = zero
|
||||
}
|
||||
populate()
|
||||
for k, v := range *m {
|
||||
if v == zero {
|
||||
delete(*m, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package mapx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRepopulateNonzero(t *testing.T) {
|
||||
var m map[string]int
|
||||
|
||||
// First call: nil map gets initialized and populated.
|
||||
RepopulateNonzero(&m, func() {
|
||||
m["a"] = 1
|
||||
m["b"] = 2
|
||||
})
|
||||
if got, want := len(m), 2; got != want {
|
||||
t.Fatalf("len = %d, want %d", got, want)
|
||||
}
|
||||
if m["a"] != 1 || m["b"] != 2 {
|
||||
t.Fatalf("got %v, want map[a:1 b:2]", m)
|
||||
}
|
||||
|
||||
// Second call: "b" is no longer populated, so it should be removed.
|
||||
RepopulateNonzero(&m, func() {
|
||||
m["a"] = 3
|
||||
m["c"] = 4
|
||||
})
|
||||
if got, want := len(m), 2; got != want {
|
||||
t.Fatalf("len = %d, want %d", got, want)
|
||||
}
|
||||
if m["a"] != 3 || m["c"] != 4 {
|
||||
t.Fatalf("got %v, want map[a:3 c:4]", m)
|
||||
}
|
||||
if _, ok := m["b"]; ok {
|
||||
t.Fatal("key 'b' should have been removed")
|
||||
}
|
||||
|
||||
// Populating nothing should clear the map.
|
||||
RepopulateNonzero(&m, func() {})
|
||||
if got := len(m); got != 0 {
|
||||
t.Fatalf("len = %d after empty populate, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRepopulateNonzero(b *testing.B) {
|
||||
for _, size := range []int{5, 100, 100_000} {
|
||||
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
|
||||
b.Run("RepopulateNonzero", func(b *testing.B) {
|
||||
var m map[string]int
|
||||
keys := makeKeys(size)
|
||||
// Seed the map so the first iteration isn't special.
|
||||
RepopulateNonzero(&m, func() {
|
||||
for i, k := range keys {
|
||||
m[k] = i + 1
|
||||
}
|
||||
})
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
RepopulateNonzero(&m, func() {
|
||||
for i, k := range keys {
|
||||
m[k] = i + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
b.Run("clear", func(b *testing.B) {
|
||||
m := make(map[string]int, size)
|
||||
keys := makeKeys(size)
|
||||
for i, k := range keys {
|
||||
m[k] = i + 1
|
||||
}
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
m = make(map[string]int, size)
|
||||
for i, k := range keys {
|
||||
m[k] = i + 1
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func makeKeys(n int) []string {
|
||||
keys := make([]string, n)
|
||||
for i := range keys {
|
||||
keys[i] = fmt.Sprintf("key-%d", i)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
Reference in New Issue
Block a user