This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+5 -91
View File
@@ -1,83 +1,24 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
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 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsdial
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// This file's built on iOS and on two of three macOS build variants:
+193 -26
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tsdial provides a Dialer type that can dial out of tailscaled.
@@ -12,6 +12,7 @@ import (
"net/http"
"net/netip"
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
@@ -19,17 +20,19 @@ import (
"time"
"github.com/gaissmai/bart"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/net/dnscache"
"tailscale.com/net/netknob"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/netutil"
"tailscale.com/net/netx"
"tailscale.com/net/tsaddr"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/nettype"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/mak"
@@ -87,9 +90,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()
@@ -116,6 +126,24 @@ func (c sysConn) Close() error {
return nil
}
// CloseRead implements [nettype.HalfCloser], allowing the underlying Conn
// to be half-closed if possible. Otherwise, this is a no-op.
func (c sysConn) CloseRead() error {
if hc, ok := c.Conn.(nettype.HalfCloser); ok {
return hc.CloseRead()
}
return nil
}
// CloseWrite implements [nettype.HalfCloser], allowing the underlying Conn
// to be half-closed if possible. Otherwise, this is a no-op.
func (c sysConn) CloseWrite() error {
if hc, ok := c.Conn.(nettype.HalfCloser); ok {
return hc.CloseWrite()
}
return nil
}
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
// etc). This is needed on some platforms to set sockopts to bind
// to the same interface index.
@@ -133,6 +161,12 @@ func (d *Dialer) TUNName() string {
return d.tunName
}
// ProbeLocks acquires and releases the dialer's internal mutex.
func (d *Dialer) ProbeLocks() {
d.mu.Lock()
d.mu.Unlock()
}
// SetExitDNSDoH sets (or clears) the exit node DNS DoH server base URL to use.
// The doh URL should contain the scheme, authority, and path, but without
// a '?' and/or query parameters.
@@ -189,7 +223,9 @@ func (d *Dialer) Close() error {
c.Close()
}
d.activeSysConns = nil
d.PeerAPITransport().CloseIdleConnections()
if buildfeatures.HasPeerAPIClient {
d.PeerAPITransport().CloseIdleConnections()
}
return nil
}
@@ -348,28 +384,55 @@ 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)
d.mu.Lock()
defer d.mu.Unlock()
d.dns = m
// 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)
}
// userDialResolve resolves addr as if a user initiating the dial. (e.g. from a
// SOCKS or HTTP outbound proxy)
func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (netip.AddrPort, error) {
// 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.
// (e.g. from a SOCKS or HTTP outbound proxy.)
//
// It returns all candidate addresses so that the caller can apply
// happy eyeballs across address families. The returned slice is
// 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.
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 {
return ipp, err
if err != nil {
return nil, err
}
return []netip.AddrPort{ipp}, nil
}
// Otherwise, hit the network.
@@ -379,7 +442,7 @@ func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (net
host, port, err := splitHostPort(addr)
if err != nil {
// addr is malformed.
return netip.AddrPort{}, err
return nil, err
}
var r net.Resolver
@@ -396,16 +459,53 @@ func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (net
}
ips, err := r.LookupIP(ctx, ipNetOfNetwork(network), host)
if err != nil {
return nil, err
}
out := make([]netip.AddrPort, 0, len(ips))
for _, stdIP := range ips {
ip, ok := netip.AddrFromSlice(stdIP)
if !ok {
continue
}
out = append(out, netip.AddrPortFrom(ip.Unmap(), port))
}
if len(out) == 0 {
return nil, fmt.Errorf("DNS lookup returned no results for %q", host)
}
if debugPreferIPv6() {
slices.SortStableFunc(out, func(a, b netip.AddrPort) int {
a6 := a.Addr().Is6()
b6 := b.Addr().Is6()
if a6 == b6 {
return 0
}
if a6 {
return -1
}
return 1
})
}
return out, nil
}
// userDialResolve resolves addr and returns the first candidate.
// It is for callers that don't perform happy-eyeballs (notably
// [Dialer.UserDialPlan], which only needs to classify one IP).
func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (netip.AddrPort, error) {
ipps, err := d.userDialResolveAll(ctx, network, addr)
if err != nil {
return netip.AddrPort{}, err
}
if len(ips) == 0 {
return netip.AddrPort{}, fmt.Errorf("DNS lookup returned no results for %q", host)
}
ip, _ := netip.AddrFromSlice(ips[0])
return netip.AddrPortFrom(ip.Unmap(), port), nil
return ipps[0], nil
}
// debugPreferIPv6 forces userDialResolveAll to sort AAAA results before
// A results, reproducing the failure mode where a client on an IPv6-capable
// host picks an unreachable AAAA address through an IPv4-only exit node.
// Used by TestExitNodeV4Only to exercise the happy-eyeballs fallback.
var debugPreferIPv6 = envknob.RegisterBool("TS_DEBUG_PREFER_IPV6_USERDIAL")
// ipNetOfNetwork returns "ip", "ip4", or "ip6" corresponding
// to the input value of "tcp", "tcp4", "udp6" etc network
// names.
@@ -479,11 +579,33 @@ func (d *Dialer) SystemDial(ctx context.Context, network, addr string) (net.Conn
// UserDial connects to the provided network address as if a user were
// initiating the dial. (e.g. from a SOCKS or HTTP outbound proxy)
//
// For TCP, if the name resolves to multiple addresses, UserDial races
// connect attempts across address families with a happy-eyeballs delay
// and returns the first one that succeeds. This lets dual-stack names
// work via an exit node whose egress is single-family without the
// caller needing to know which family the exit node can reach.
func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) {
ipp, err := d.userDialResolve(ctx, network, addr)
ipps, err := d.userDialResolveAll(ctx, network, addr)
if err != nil {
return nil, err
}
// Happy eyeballs is a no-op (and undefined) for UDP; there's no
// connect to race.
if len(ipps) == 1 || strings.HasPrefix(network, "udp") {
return d.dialOneUser(ctx, network, ipps[0])
}
// Family filtering for "tcp4"/"tcp6" is already handled by
// userDialResolveAll (via ipNetOfNetwork), so ipps only contains
// addresses of the requested family by this point.
return d.raceDialUser(ctx, ipps)
}
// dialOneUser dials ipp using the appropriate transport for a user
// dial (netstack, peer dialer, system dialer, or std dialer) based
// on what kind of address ipp is.
func (d *Dialer) dialOneUser(ctx context.Context, network string, ipp netip.AddrPort) (net.Conn, error) {
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.Addr()) {
if d.NetstackDialTCP == nil || d.NetstackDialUDP == nil {
return nil, errors.New("Dialer not initialized correctly")
@@ -515,6 +637,51 @@ func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn,
return stdDialer.DialContext(ctx, network, ipp.String())
}
// userDialFallbackDelay is the happy-eyeballs gap between starting
// successive connect attempts. 300ms matches Go's net.Dialer default
// and the value used by net/dnscache.
const userDialFallbackDelay = 300 * time.Millisecond
// raceDialUser races connect attempts across ipps with a happy-eyeballs
// fallback delay, returning the first to succeed. Losers are cancelled
// and any conns they produce are closed. If all fail, the first error
// is returned.
func (d *Dialer) raceDialUser(ctx context.Context, ipps []netip.AddrPort) (net.Conn, error) {
return netx.RaceDial(ctx, ipps,
func(ctx context.Context, network, address string) (net.Conn, error) {
return d.dialOneUser(ctx, network, netip.MustParseAddrPort(address))
},
userDialFallbackDelay,
)
}
// UserDialPlan resolves addr and reports whether the dialer would
// handle it via Tailscale. If viaTailscale is false, the resolved
// address is not a Tailscale route and the caller may dial it directly.
//
// Warning: there is a TOCTOU race if addr contains a DNS name and the
// caller subsequently passes the same DNS name to [Dialer.UserDial], as DNS
// may resolve differently the second time. Callers who want to only
// dial over Tailscale should call [Dialer.UserDial] with the returned
// ipp.String() (an IP:port) rather than the original DNS name.
func (d *Dialer) UserDialPlan(ctx context.Context, network, addr string) (ipp netip.AddrPort, viaTailscale bool, err error) {
ipp, err = d.userDialResolve(ctx, network, addr)
if err != nil {
return netip.AddrPort{}, false, err
}
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.Addr()) {
return ipp, true, nil
}
if routes := d.routes.Load(); routes != nil {
isTailscaleRoute, _ := routes.Lookup(ipp.Addr())
return ipp, isTailscaleRoute, nil
}
if version.IsMacGUIVariant() && tsaddr.IsTailscaleIP(ipp.Addr()) {
return ipp, true, nil
}
return ipp, false, nil
}
// dialPeerAPI connects to a Tailscale peer's peerapi over TCP.
//
// network must a "tcp" type, and addr must be an ip:port. Name resolution
@@ -569,7 +736,7 @@ func (d *Dialer) PeerAPIHTTPClient() *http.Client {
panic("unreachable")
}
d.peerClientOnce.Do(func() {
t := http.DefaultTransport.(*http.Transport).Clone()
t := netutil.NewDefaultTransport()
t.Dial = nil
t.DialContext = d.dialPeerAPI
// Do not use the environment proxy for PeerAPI.