net/netcheck: use dnscache.Resolver when resolving DERP IPs

This also adds a bunch of tests for this function to ensure that we're
returning the proper IP(s) in all cases.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I0d9d57170dbab5f2bf07abdf78ecd17e0e635399
This commit is contained in:
Andrew Dunham
2023-04-14 12:13:03 -04:00
parent 64bbf1738e
commit 228d0c6aea
4 changed files with 110 additions and 5 deletions

View File

@@ -25,6 +25,7 @@ import (
"github.com/tcnksm/go-httpstat"
"tailscale.com/derp/derphttp"
"tailscale.com/envknob"
"tailscale.com/net/dnscache"
"tailscale.com/net/interfaces"
"tailscale.com/net/netaddr"
"tailscale.com/net/neterror"
@@ -181,6 +182,15 @@ type Client struct {
// If nil, portmap discovery is not done.
PortMapper *portmapper.Client // lazily initialized on first use
// UseDNSCache controls whether this client should use a
// *dnscache.Resolver to resolve DERP hostnames, when no IP address is
// provided in the DERP map. Note that Tailscale-provided DERP servers
// all specify explicit IPv4 and IPv6 addresses, so this is mostly
// helpful for users with custom DERP servers.
//
// If false, the default net.Resolver will be used, with no caching.
UseDNSCache bool
// For tests
testEnoughRegions int
testCaptivePortalDelay time.Duration
@@ -191,6 +201,7 @@ type Client struct {
last *Report // most recent report
lastFull time.Time // time of last full (non-incremental) report
curState *reportState // non-nil if we're in a call to GetReportn
resolver *dnscache.Resolver // only set if UseDNSCache is true
}
// STUNConn is the interface required by the netcheck Client when
@@ -1514,6 +1525,7 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe
addr := c.nodeAddr(ctx, node, probe.proto)
if !addr.IsValid() {
c.logf("netcheck.runProbe: named node %q has no address", probe.node)
return
}
@@ -1597,12 +1609,46 @@ func (c *Client) nodeAddr(ctx context.Context, n *tailcfg.DERPNode, proto probeP
return
}
// TODO(bradfitz): add singleflight+dnscache here.
addrs, _ := net.DefaultResolver.LookupIPAddr(ctx, n.HostName)
// The default lookup function if we don't set UseDNSCache is to use net.DefaultResolver.
lookupIPAddr := func(ctx context.Context, host string) ([]netip.Addr, error) {
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
var naddrs []netip.Addr
for _, addr := range addrs {
na, ok := netip.AddrFromSlice(addr.IP)
if !ok {
continue
}
naddrs = append(naddrs, na.Unmap())
}
return naddrs, nil
}
c.mu.Lock()
if c.UseDNSCache {
if c.resolver == nil {
c.resolver = &dnscache.Resolver{
Forward: net.DefaultResolver,
UseLastGood: true,
Logf: c.logf,
}
}
resolver := c.resolver
lookupIPAddr = func(ctx context.Context, host string) ([]netip.Addr, error) {
_, _, allIPs, err := resolver.LookupIP(ctx, host)
return allIPs, err
}
}
c.mu.Unlock()
probeIsV4 := proto == probeIPv4
addrs, _ := lookupIPAddr(ctx, n.HostName)
for _, a := range addrs {
if (a.IP.To4() != nil) == (proto == probeIPv4) {
na, _ := netip.AddrFromSlice(a.IP.To4())
return netip.AddrPortFrom(na.Unmap(), uint16(port))
if (a.Is4() && probeIsV4) || (a.Is6() && !probeIsV4) {
return netip.AddrPortFrom(a, uint16(port))
}
}
return