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
8 changed files with 545 additions and 94 deletions
Showing only changes of commit f3a117e813 - Show all commits
+3 -3
View File
@@ -1291,9 +1291,9 @@ func (h *Handler) serveDial(w http.ResponseWriter, r *http.Request) {
return
}
// Dial via Tailscale using the resolved IP:port to avoid a TOCTOU
// race with DNS re-resolution.
outConn, err := h.b.Dialer().UserDial(r.Context(), network, ipp.String())
// Dial via Tailscale with the original hostname so UserDial can
// resolve all addresses and race across families (happy eyeballs).
outConn, err := h.b.Dialer().UserDial(r.Context(), network, addr)
if err != nil {
http.Error(w, "dial failure: "+err.Error(), http.StatusBadGateway)
return
+13 -80
View File
@@ -14,6 +14,7 @@ import (
"net"
"net/netip"
"runtime"
"strconv"
"sync"
"sync/atomic"
"time"
@@ -24,7 +25,6 @@ import (
"tailscale.com/types/logger"
"tailscale.com/util/cloudenv"
"tailscale.com/util/singleflight"
"tailscale.com/util/slicesx"
"tailscale.com/util/testenv"
)
@@ -552,16 +552,6 @@ const fallbackDelay = 300 * time.Millisecond
// raceDial tries to dial port on each ip in ips, starting a new race
// dial every fallbackDelay apart, returning whichever completes first.
func (dc *dialCall) raceDial(ctx context.Context, ips []netip.Addr) (net.Conn, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
type res struct {
c net.Conn
err error
}
resc := make(chan res) // must be unbuffered
failBoost := make(chan struct{}) // best effort send on dial failure
// Remove IPs that we tried & failed to dial previously
// (such as when we're being called after a dnsfallback lookup and get
// the same results)
@@ -569,77 +559,20 @@ func (dc *dialCall) raceDial(ctx context.Context, ips []netip.Addr) (net.Conn, e
if len(ips) == 0 {
return nil, errors.New("no IPs")
}
// Partition candidate list and then merge such that an IPv6 address is
// in the first spot if present, and then addresses are interleaved.
// This ensures that we're trying an IPv6 address first, then
// alternating between v4 and v6 in case one of the two networks is
// broken.
var iv4, iv6 []netip.Addr
for _, ip := range ips {
if ip.Is6() {
iv6 = append(iv6, ip)
} else {
iv4 = append(iv4, ip)
}
port, err := strconv.ParseUint(dc.port, 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid port %q: %w", dc.port, err)
}
ips = slicesx.Interleave(iv6, iv4)
go func() {
for i, ip := range ips {
if i != 0 {
timer := time.NewTimer(fallbackDelay)
select {
case <-timer.C:
case <-failBoost:
timer.Stop()
case <-ctx.Done():
timer.Stop()
return
}
}
go func(ip netip.Addr) {
c, err := dc.dialOne(ctx, ip)
if err != nil {
// Best effort wake-up a pending dial.
// e.g. IPv4 dials failing quickly on an IPv6-only system.
// In that case we don't want to wait 300ms per IPv4 before
// we get to the IPv6 addresses.
select {
case failBoost <- struct{}{}:
default:
}
}
select {
case resc <- res{c, err}:
case <-ctx.Done():
if c != nil {
c.Close()
}
}
}(ip)
}
}()
var firstErr error
var fails int
for {
select {
case r := <-resc:
if r.c != nil {
return r.c, nil
}
fails++
if firstErr == nil {
firstErr = r.err
}
if fails == len(ips) {
return nil, firstErr
}
case <-ctx.Done():
return nil, ctx.Err()
}
addrs := make([]netip.AddrPort, len(ips))
for i, ip := range ips {
addrs[i] = netip.AddrPortFrom(ip, uint16(port))
}
return netx.RaceDial(ctx, addrs, func(ctx context.Context, network, address string) (net.Conn, error) {
c, err := dc.d.fwd(ctx, network, address)
ipp, _ := netip.ParseAddrPort(address)
dc.noteDialResult(ipp.Addr(), err)
return c, err
}, fallbackDelay)
}
// TLSDialer is like Dialer but returns a func suitable for using with net/http.Transport.DialTLSContext.
+96
View File
@@ -0,0 +1,96 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netx
import (
"context"
"net"
"net/netip"
"time"
"tailscale.com/util/slicesx"
)
// RaceDial races TCP connect attempts across addrs using a
// happy-eyeballs-style staggered approach: a new dial is started every
// fallbackDelay, and the first successful connection wins. Losers are
// cancelled and their connections closed. If all dials fail, the first
// error is returned.
//
// Addresses are interleaved v6-first so that IPv6 is preferred but both
// families are tried promptly. The dial func is always called with
// network "tcp".
func RaceDial(ctx context.Context, addrs []netip.AddrPort, dial DialFunc, fallbackDelay time.Duration) (net.Conn, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var v4, v6 []netip.AddrPort
for _, a := range addrs {
if a.Addr().Is6() {
v6 = append(v6, a)
} else {
v4 = append(v4, a)
}
}
ordered := slicesx.Interleave(v6, v4)
type result struct {
c net.Conn
err error
}
resc := make(chan result) // unbuffered: senders sync with collector
failBoost := make(chan struct{}, 1) // wake the launcher when a dial fails fast
go func() {
for i, addr := range ordered {
if i > 0 {
t := time.NewTimer(fallbackDelay)
select {
case <-t.C:
case <-failBoost:
t.Stop()
case <-ctx.Done():
t.Stop()
return
}
}
go func() {
c, err := dial(ctx, "tcp", addr.String())
if err != nil {
select {
case failBoost <- struct{}{}:
default:
}
}
select {
case resc <- result{c, err}:
case <-ctx.Done():
if c != nil {
c.Close()
}
}
}()
}
}()
var firstErr error
var nFailed int
for {
select {
case r := <-resc:
if r.err == nil {
return r.c, nil
}
if firstErr == nil {
firstErr = r.err
}
nFailed++
if nFailed >= len(ordered) {
return nil, firstErr
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
+133
View File
@@ -0,0 +1,133 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netx
import (
"context"
"errors"
"net"
"net/netip"
"testing"
"testing/synctest"
"time"
)
type fakeConn struct{ net.Conn }
func (fakeConn) Close() error { return nil }
var (
v4Addr1 = netip.MustParseAddrPort("192.0.2.1:443")
v4Addr2 = netip.MustParseAddrPort("192.0.2.2:443")
v6Addr1 = netip.MustParseAddrPort("[2001:db8::1]:443")
v6Addr2 = netip.MustParseAddrPort("[2001:db8::2]:443")
)
func TestRaceDialFirstWins(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
addrs := []netip.AddrPort{v6Addr1, v4Addr1, v6Addr2}
t0 := time.Now()
conn, err := RaceDial(context.Background(), addrs,
func(ctx context.Context, network, address string) (net.Conn, error) {
return fakeConn{}, nil
},
300*time.Millisecond,
)
if err != nil {
t.Fatal(err)
}
if conn == nil {
t.Fatal("expected non-nil conn")
}
conn.Close()
if d := time.Since(t0); d != 0 {
t.Fatalf("took %v; first dial wins immediately so no time should pass", d)
}
})
}
func TestRaceDialAllFail(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
addrs := []netip.AddrPort{v4Addr1, v6Addr1}
want := errors.New("dial failed")
t0 := time.Now()
_, err := RaceDial(context.Background(), addrs,
func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, want
},
300*time.Millisecond,
)
if err == nil {
t.Fatal("expected error")
}
if !errors.Is(err, want) {
t.Fatalf("got %v; want %v", err, want)
}
if d := time.Since(t0); d != 0 {
t.Fatalf("took %v; failBoost should skip all delays", d)
}
})
}
func TestRaceDialCancelledContext(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
t0 := time.Now()
_, err := RaceDial(ctx, []netip.AddrPort{v4Addr1},
func(ctx context.Context, network, address string) (net.Conn, error) {
<-ctx.Done()
return nil, ctx.Err()
},
300*time.Millisecond,
)
if !errors.Is(err, context.Canceled) {
t.Fatalf("got %v; want context.Canceled", err)
}
if d := time.Since(t0); d != 0 {
t.Fatalf("took %v; pre-cancelled context should resolve immediately", d)
}
})
}
func TestRaceDialInterleaving(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var order []string
addrs := []netip.AddrPort{v4Addr1, v4Addr2, v6Addr1, v6Addr2}
t0 := time.Now()
RaceDial(context.Background(), addrs,
func(ctx context.Context, network, address string) (net.Conn, error) {
order = append(order, address)
return nil, errors.New("fail")
},
300*time.Millisecond,
)
if len(order) != 4 {
t.Fatalf("expected 4 dials, got %d", len(order))
}
ipp, _ := netip.ParseAddrPort(order[0])
if !ipp.Addr().Is6() {
t.Errorf("first dial should be v6, got %v", order[0])
}
if d := time.Since(t0); d != 0 {
t.Fatalf("took %v; failBoost should skip all delays", d)
}
})
}
func TestRaceDialFailBoost(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
addrs := []netip.AddrPort{v6Addr1, v4Addr1, v6Addr2}
t0 := time.Now()
RaceDial(context.Background(), addrs,
func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, errors.New("fail")
},
time.Hour, // absurdly long; failBoost bypasses it
)
if d := time.Since(t0); d >= time.Second {
t.Fatalf("took %v; failBoost should have bypassed the hour-long delay", d)
}
})
}
+99 -11
View File
@@ -12,6 +12,7 @@ import (
"net/http"
"net/netip"
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
@@ -19,6 +20,7 @@ import (
"time"
"github.com/gaissmai/bart"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/net/dnscache"
@@ -358,18 +360,27 @@ func (d *Dialer) SetNetMap(nm *netmap.NetworkMap) {
d.dns = m
}
// 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) {
// 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.
// 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)
if err != errUnresolved {
return ipp, err
if err != nil {
return nil, err
}
return []netip.AddrPort{ipp}, nil
}
// Otherwise, hit the network.
@@ -379,7 +390,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 +407,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 +527,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 +585,24 @@ 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.
+137
View File
@@ -5,8 +5,12 @@ package tsdial
import (
"context"
"errors"
"net"
"net/netip"
"sync/atomic"
"testing"
"time"
"github.com/gaissmai/bart"
)
@@ -95,3 +99,136 @@ func TestUserDialPlan(t *testing.T) {
})
}
}
// TestRaceDialUserFallback covers the core happy-eyeballs scenario:
// the first family (e.g. AAAA via an IPv4-only exit node) fails to
// connect, and the second family succeeds. The fallback delay should
// not be required because the failing dial wakes the launcher via
// failBoost.
func TestRaceDialUserFallback(t *testing.T) {
v6 := netip.MustParseAddrPort("[2001:db8::1]:80")
v4 := netip.MustParseAddrPort("192.0.2.1:80")
var v4Calls, v6Calls atomic.Int32
d := &Dialer{
UseNetstackForIP: func(netip.Addr) bool { return true },
NetstackDialTCP: func(ctx context.Context, ipp netip.AddrPort) (net.Conn, error) {
if ipp.Addr().Is6() {
v6Calls.Add(1)
return nil, errors.New("simulated v6 unreachable")
}
v4Calls.Add(1)
c, _ := net.Pipe()
return c, nil
},
NetstackDialUDP: func(context.Context, netip.AddrPort) (net.Conn, error) {
t.Fatal("UDP dialer should not be called for TCP race")
return nil, nil
},
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
t0 := time.Now()
c, err := d.raceDialUser(ctx, []netip.AddrPort{v6, v4})
elapsed := time.Since(t0)
if err != nil {
t.Fatalf("raceDialUser: %v", err)
}
defer c.Close()
if v6Calls.Load() != 1 {
t.Errorf("v6 dial attempts = %d, want 1", v6Calls.Load())
}
if v4Calls.Load() != 1 {
t.Errorf("v4 dial attempts = %d, want 1", v4Calls.Load())
}
// We allow up to the fallback delay; with failBoost the v4 attempt
// should kick off as soon as v6 fails, well under the timer.
if elapsed >= userDialFallbackDelay {
t.Errorf("race took %v; expected failBoost to short-circuit the %v delay",
elapsed, userDialFallbackDelay)
}
}
// TestRaceDialUserAllFail verifies that when every candidate fails,
// raceDialUser returns the first error rather than hanging.
func TestRaceDialUserAllFail(t *testing.T) {
ipps := []netip.AddrPort{
netip.MustParseAddrPort("[2001:db8::1]:80"),
netip.MustParseAddrPort("192.0.2.1:80"),
}
d := &Dialer{
UseNetstackForIP: func(netip.Addr) bool { return true },
NetstackDialTCP: func(_ context.Context, ipp netip.AddrPort) (net.Conn, error) {
return nil, errors.New("nope: " + ipp.String())
},
NetstackDialUDP: func(context.Context, netip.AddrPort) (net.Conn, error) { return nil, nil },
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := d.raceDialUser(ctx, ipps)
if err == nil {
t.Fatal("raceDialUser returned nil error; want error")
}
}
// TestRaceDialUserCancelsLosers verifies that once one dial succeeds,
// any other in-flight dial is cancelled and any conn it eventually
// produces is closed (rather than leaked).
func TestRaceDialUserCancelsLosers(t *testing.T) {
v6 := netip.MustParseAddrPort("[2001:db8::1]:80")
v4 := netip.MustParseAddrPort("192.0.2.1:80")
// v6 blocks until its context is cancelled, then returns a conn we
// must verify is closed.
closed := make(chan struct{})
d := &Dialer{
UseNetstackForIP: func(netip.Addr) bool { return true },
NetstackDialTCP: func(ctx context.Context, ipp netip.AddrPort) (net.Conn, error) {
if ipp.Addr().Is6() {
<-ctx.Done()
a, b := net.Pipe()
go func() {
<-closed
b.Close()
}()
return &closingPipeConn{Conn: a, closed: closed}, nil
}
c, _ := net.Pipe()
return c, nil
},
NetstackDialUDP: func(context.Context, netip.AddrPort) (net.Conn, error) { return nil, nil },
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
c, err := d.raceDialUser(ctx, []netip.AddrPort{v6, v4})
if err != nil {
t.Fatalf("raceDialUser: %v", err)
}
defer c.Close()
select {
case <-closed:
case <-time.After(2 * time.Second):
t.Fatal("loser conn was not closed within 2s")
}
}
type closingPipeConn struct {
net.Conn
closed chan struct{}
}
func (c *closingPipeConn) Close() error {
select {
case <-c.closed:
// already closed
default:
close(c.closed)
}
return c.Conn.Close()
}
+59
View File
@@ -545,6 +545,65 @@ func TestExitNode(t *testing.T) {
}
}
// TestExitNodeV4Only verifies that when an exit node is on an
// IPv4-only network, a client can still connect through it to a
// webserver whose DNS name has both A and AAAA records. This
// exercises the happy-eyeballs race dial in net/tsdial.UserDial:
// the AAAA connect attempt fails (exit node has no IPv6 egress),
// but the A attempt succeeds.
//
// Fixes tailscale/tailscale#13257 and #19792.
func TestExitNodeV4Only(t *testing.T) {
env := vmtest.New(t)
// Exit node network: IPv4 only (no IPv6 prefix → CanV6()=false).
// It advertises both 0.0.0.0/0 and ::/0 (required by tailscale up)
// but the network has no IPv6 WAN, so v6 traffic will be dropped.
exitNet := env.AddNetwork("2.0.0.1", "192.168.2.1/24", vnet.EasyNAT)
// Client network: dual-stack so Go's net.Resolver prefers AAAA.
clientNet := env.AddNetwork("1.0.0.1", "2000:1::1/64", "192.168.1.1/24", vnet.EasyNAT)
// Web server network: use the FakeDualStackWeb VIP's v4 as WAN.
webNet := env.AddNetwork("5.0.0.100", "192.168.5.1/24", vnet.One2OneNAT)
client := env.AddNode("client", clientNet,
vmtest.OS(vmtest.Gokrazy),
// Force AAAA addresses first in userDialResolveAll results so
// the old single-IP code path would pick an unreachable v6 addr.
vnet.TailscaledEnv{Key: "TS_DEBUG_PREFER_IPV6_USERDIAL", Value: "1"})
exit := env.AddNode("exit", exitNet,
vmtest.OS(vmtest.Gokrazy),
vmtest.AdvertiseRoutes("0.0.0.0/0,::/0"))
env.AddNode("webserver", webNet,
vmtest.OS(vmtest.Gokrazy),
vmtest.DontJoinTailnet(),
vmtest.WebServer(8080))
approveStep := env.AddStep("Approve exit-node routes")
fetchStep := env.AddStep("HTTP GET via exit node using dual-stack DNS name")
env.Start()
approveStep.Begin()
env.ApproveRoutes(exit, "0.0.0.0/0", "::/0")
approveStep.End(nil)
fetchStep.Begin()
env.SetExitNode(client, exit)
// Use the VIP hostname so DNS returns both A (5.0.0.100) and AAAA
// (2052::5:100). The exit node's network has no IPv6 WAN, so the
// AAAA connect attempt will fail and the dialer must fall back to
// the A record via happy eyeballs.
body := env.HTTPGet(client, "http://dualstack-web.example.com:8080/")
t.Logf("response: %s", body)
if !strings.Contains(body, "Hello world I am webserver") {
fetchStep.Fatalf("unexpected webserver response: %q", body)
}
if !strings.Contains(body, "from 2.0.0.1") {
fetchStep.Fatalf("expected traffic from exit node WAN (2.0.0.1), got: %q", body)
}
fetchStep.End(nil)
}
// TestDiscoKeyChange verifies that when one node's disco key rotates without
// its WireGuard node key changing, peers detect the change, tear down stale
// WireGuard session state for that peer, and re-establish the tunnel in both
+5
View File
@@ -21,6 +21,11 @@ var (
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 {