net/dnscache: run happy eyeballs with more than one dest IP (#19770)

If the context given to DialContext has a shorter lifetime than the OS
TCP SYN timeout, and TCP SYNs are dropped from the path to the remote,
DialContext would never fall back to try IPv6 after IPv4.

Instead, use the normal happy eyeballs race if there is more than one
address. This does remove the implicit prioritization of IPv4 over IPv6
in cases where there is only a single IPv4 remote address.

Updates #13346

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
This commit is contained in:
Claus Lensbøl
2026-05-19 12:59:11 -04:00
committed by GitHub
parent 5d56cc8512
commit ee0a03b140
2 changed files with 9 additions and 39 deletions

View File

@@ -422,24 +422,21 @@ func (d *dialer) DialContext(ctx context.Context, network, address string) (retC
}
}()
ip, ip6, allIPs, err := d.dnsCache.LookupIP(ctx, host)
ip, _, allIPs, err := d.dnsCache.LookupIP(ctx, host)
if err != nil {
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
}
i4s := v4addrs(allIPs)
if len(i4s) < 2 {
// If we only have one candidate, just dial that, no matter what the
// address family is.
if len(allIPs) == 1 {
d.dnsCache.dlogf("dialing %s, %s for %s", network, ip, address)
c, err := dc.dialOne(ctx, ip.Unmap())
if err == nil || ctx.Err() != nil || !ip6.IsValid() {
return c, err
}
// Fall back to trying IPv6.
return dc.dialOne(ctx, ip6)
return dc.dialOne(ctx, ip.Unmap())
}
// Multiple IPv4 candidates, and 0+ IPv6.
ipsToTry := append(i4s, v6addrs(allIPs)...)
return dc.raceDial(ctx, ipsToTry)
// If we have multiple candidates, across address families, use happy
// eyeballs to find a connection.
return dc.raceDial(ctx, allIPs)
}
func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall) bool {
@@ -645,25 +642,6 @@ type res struct {
}
}
func v4addrs(aa []netip.Addr) (ret []netip.Addr) {
for _, a := range aa {
a = a.Unmap()
if a.Is4() {
ret = append(ret, a)
}
}
return ret
}
func v6addrs(aa []netip.Addr) (ret []netip.Addr) {
for _, a := range aa {
if a.Is6() && !a.Is4In6() {
ret = append(ret, a)
}
}
return ret
}
// TLSDialer is like Dialer but returns a func suitable for using with net/http.Transport.DialTLSContext.
// It returns a *tls.Conn type on success.
// On TLS cert validation failure, it can invoke a backup DNS resolution strategy.

View File

@@ -4,7 +4,6 @@
package vmtest_test
import (
"flag"
"fmt"
"testing"
@@ -13,8 +12,6 @@
"tailscale.com/tstest/natlab/vnet"
)
var knownBroken = flag.Bool("known-broken", false, "run known-broken tests")
func v6cidr(n int) string {
return fmt.Sprintf("2000:%d::1/64", n)
}
@@ -228,12 +225,7 @@ func TestSingleJustIPv6(t *testing.T) {
// TestSingleDualBrokenIPv4 tests a dual-stack node with broken
// (blackholed) IPv4.
//
// See https://github.com/tailscale/tailscale/issues/13346
func TestSingleDualBrokenIPv4(t *testing.T) {
if !*knownBroken {
t.Skip("skipping known-broken test; set --known-broken to run; see https://github.com/tailscale/tailscale/issues/13346")
}
env := vmtest.New(t)
v6AndBlackholedIPv4(env)
env.Start()