When tailscaled is running in userspace-networking mode behind an
exit node (e.g. as a SOCKS5 proxy), it resolves a hostname and then
dials a single resolved IP through the tunnel. If the name has both
A and AAAA, Go's net.Resolver merges them and we pick ips[0], which
on an IPv6-native host is usually AAAA. If the exit node has no IPv6
egress (or vice versa), the dial fails silently through the tunnel
and the user sees a hang.
Resolve all candidates and race connect attempts across address
families with a 300ms happy-eyeballs delay, matching Go's net.Dialer
default and the existing pattern in net/dnscache (commit ee0a03b14).
First success wins; losers are cancelled and any conns they produce
are closed. A failBoost channel wakes the launcher when a connect
fails fast (e.g. ICMP "no route" via the tunnel) so we don't sit on
the 300ms timer when the answer is already known.
userDialResolve is refactored into userDialResolveAll (returns the
full candidate list) plus a thin single-IP wrapper for callers like
UserDialPlan that don't race. UserDial's per-IP dispatch (netstack
vs peer dialer vs SystemDial vs std) is extracted to dialOneUser so
each candidate can route correctly on its own merits.
Also fix serveDial in localapi to pass the original hostname to
UserDial rather than a pre-resolved IP, so the race can fire.
This fix is single-ended: it works against any exit node, including
old ones, with no protocol changes. The trade-off versus filtering
on the exit-node side via PeerAPI DoH is that every dial through an
unreachable-family exit node costs one failed connect attempt per
cache window, rather than zero, which is acceptable given the
simplicity.
Fixes #19792
Fixes #13257
Change-Id: I9d7645d0034caf3ee22ecdd8070798353f77e94b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
114 lines
3.4 KiB
Go
114 lines
3.4 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package vnet
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
)
|
|
|
|
var vips = map[string]virtualIP{} // DNS name => details
|
|
|
|
var (
|
|
fakeDNS = newVIP("dns", "4.11.4.11", "2411::411")
|
|
fakeProxyControlplane = newVIP("controlplane.tailscale.com", 1)
|
|
fakeTestAgent = newVIP("test-driver.tailscale", 2)
|
|
fakeControl = newVIP("control.tailscale", 3)
|
|
fakeDERP1 = newVIP("derp1.tailscale", "33.4.0.1") // 3340=DERP; 1=derp 1
|
|
fakeDERP2 = newVIP("derp2.tailscale", "33.4.0.2") // 3340=DERP; 2=derp 2
|
|
fakeLogCatcher = newVIP("log.tailscale.com", 4)
|
|
fakeSyslog = newVIP("syslog.tailscale", 9)
|
|
fakeCloudInit = newVIP("cloud-init.tailscale", 5) // serves cloud-init metadata/userdata per node
|
|
fakeFiles = newVIP("files.tailscale", 6) // serves binary files (tta, tailscale, tailscaled) to VMs
|
|
|
|
// FakeDualStackWeb is a dual-stack webserver VIP used by
|
|
// TestExitNodeV4Only to verify that traffic works through an
|
|
// IPv4-only exit node even when DNS returns both A and AAAA.
|
|
FakeDualStackWeb = newVIP("dualstack-web.example.com", "5.0.0.100", "2052::5:100")
|
|
)
|
|
|
|
type virtualIP struct {
|
|
name string // for DNS
|
|
v4 netip.Addr
|
|
v6 netip.Addr
|
|
}
|
|
|
|
func (v virtualIP) Match(a netip.Addr) bool {
|
|
return v.v4 == a.Unmap() || v.v6 == a
|
|
}
|
|
|
|
// TestDriverIPv4 returns the IPv4 address of the test driver VIP (52.52.0.2).
|
|
// TTA agents dial this IP on port TestDriverPort to connect to the test harness.
|
|
func TestDriverIPv4() netip.Addr { return fakeTestAgent.v4 }
|
|
|
|
// TestDriverPort is the port the test driver listens on.
|
|
const TestDriverPort = 8008
|
|
|
|
// FakeDNSIPv4 returns the fake DNS IPv4 address.
|
|
func FakeDNSIPv4() netip.Addr { return fakeDNS.v4 }
|
|
|
|
// FakeDNSIPv6 returns the fake DNS IPv6 address.
|
|
func FakeDNSIPv6() netip.Addr { return fakeDNS.v6 }
|
|
|
|
// FakeSyslogIPv4 returns the fake syslog IPv4 address.
|
|
func FakeSyslogIPv4() netip.Addr { return fakeSyslog.v4 }
|
|
|
|
// FakeSyslogIPv6 returns the fake syslog IPv6 address.
|
|
func FakeSyslogIPv6() netip.Addr { return fakeSyslog.v6 }
|
|
|
|
// newVIP returns a new virtual IP.
|
|
//
|
|
// opts may be an IPv4 an IPv6 (in string form) or an int (bounded by uint8) to
|
|
// use IPv4 of 52.52.0.x.
|
|
//
|
|
// If the IPv6 is omitted, one is derived from the IPv4.
|
|
//
|
|
// If an opt is invalid or the DNS name is already used, it panics.
|
|
func newVIP(name string, opts ...any) (v virtualIP) {
|
|
if _, ok := vips[name]; ok {
|
|
panic(fmt.Sprintf("duplicate VIP %q", name))
|
|
}
|
|
v.name = name
|
|
for _, o := range opts {
|
|
switch o := o.(type) {
|
|
case string:
|
|
if ip, err := netip.ParseAddr(o); err == nil {
|
|
if ip.Is4() {
|
|
v.v4 = ip
|
|
} else if ip.Is6() {
|
|
v.v6 = ip
|
|
}
|
|
} else {
|
|
panic(fmt.Sprintf("unsupported string option %q", o))
|
|
}
|
|
case int:
|
|
if o <= 0 || o > 255 {
|
|
panic(fmt.Sprintf("bad octet %d", o))
|
|
}
|
|
v.v4 = netip.AddrFrom4([4]byte{52, 52, 0, byte(o)})
|
|
default:
|
|
panic(fmt.Sprintf("unknown option type %T", o))
|
|
}
|
|
}
|
|
if !v.v6.IsValid() && v.v4.IsValid() {
|
|
// Map 1.2.3.4 to 2052::0102:0304
|
|
// But make 52.52.0.x map to 2052::x
|
|
a := [16]byte{0: 0x20, 1: 0x52} // 2052::
|
|
v4 := v.v4.As4()
|
|
if v4[0] == 52 && v4[1] == 52 && v4[2] == 0 {
|
|
a[15] = v4[3]
|
|
} else {
|
|
copy(a[12:], v.v4.AsSlice())
|
|
}
|
|
v.v6 = netip.AddrFrom16(a)
|
|
}
|
|
for _, b := range vips {
|
|
if b.Match(v.v4) || b.Match(v.v6) {
|
|
panic(fmt.Sprintf("VIP %q collides with %q", name, v.name))
|
|
}
|
|
}
|
|
vips[name] = v
|
|
return v
|
|
}
|