WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
15 changed files with 522 additions and 359 deletions
Showing only changes of commit aefb1531d1 - Show all commits
+1 -1
View File
@@ -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+
+1 -1
View File
@@ -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+
+1 -1
View File
@@ -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+
+1 -1
View File
@@ -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+
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+146
View File
@@ -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
}
+89
View File
@@ -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
View File
@@ -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
}
-125
View File
@@ -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
View File
@@ -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
View File
@@ -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+
+28
View File
@@ -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)
}
}
}
+93
View File
@@ -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
}