Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package resolver
|
||||
|
||||
+497
-116
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package resolver
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -41,8 +42,10 @@ import (
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/race"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
@@ -63,6 +66,17 @@ func truncatedFlagSet(pkt []byte) bool {
|
||||
return (binary.BigEndian.Uint16(pkt[2:4]) & dnsFlagTruncated) != 0
|
||||
}
|
||||
|
||||
// setTCFlag sets the TC (truncated) flag in the DNS packet header.
|
||||
// The packet must be at least headerBytes in length.
|
||||
func setTCFlag(packet []byte) {
|
||||
if len(packet) < headerBytes {
|
||||
return
|
||||
}
|
||||
flags := binary.BigEndian.Uint16(packet[2:4])
|
||||
flags |= dnsFlagTruncated
|
||||
binary.BigEndian.PutUint16(packet[2:4], flags)
|
||||
}
|
||||
|
||||
const (
|
||||
// dohIdleConnTimeout is how long to keep idle HTTP connections
|
||||
// open to DNS-over-HTTPS servers. 10 seconds is a sensible
|
||||
@@ -131,47 +145,59 @@ func getRCode(packet []byte) dns.RCode {
|
||||
return dns.RCode(packet[3] & 0x0F)
|
||||
}
|
||||
|
||||
// findOPTRecord finds and validates the EDNS OPT record at the end of a DNS packet.
|
||||
// Returns the requested buffer size and a pointer to the OPT record bytes if valid,
|
||||
// or (0, nil) if no valid OPT record is found.
|
||||
// The OPT record must be at the very end of the packet with no option codes.
|
||||
func findOPTRecord(packet []byte) (requestedSize uint16, opt []byte) {
|
||||
const optFixedBytes = 11 // size of an OPT record with no option codes
|
||||
const edns0Version = 0 // EDNS version number (currently only version 0 is defined)
|
||||
|
||||
if len(packet) < headerBytes+optFixedBytes {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
arCount := binary.BigEndian.Uint16(packet[10:12])
|
||||
if arCount == 0 {
|
||||
// OPT shows up in an AR, so there must be no OPT
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.2
|
||||
opt = packet[len(packet)-optFixedBytes:]
|
||||
|
||||
if opt[0] != 0 {
|
||||
// OPT NAME must be 0 (root domain)
|
||||
return 0, nil
|
||||
}
|
||||
if dns.Type(binary.BigEndian.Uint16(opt[1:3])) != dns.TypeOPT {
|
||||
// Not an OPT record
|
||||
return 0, nil
|
||||
}
|
||||
requestedSize = binary.BigEndian.Uint16(opt[3:5])
|
||||
// Ignore extended RCODE in opt[5]
|
||||
if opt[6] != edns0Version {
|
||||
// Be conservative and don't touch unknown versions.
|
||||
return 0, nil
|
||||
}
|
||||
// Ignore flags in opt[6:9]
|
||||
if binary.BigEndian.Uint16(opt[9:11]) != 0 {
|
||||
// RDLEN must be 0 (no variable length data). We're at the end of the
|
||||
// packet so this should be 0 anyway.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return requestedSize, opt
|
||||
}
|
||||
|
||||
// clampEDNSSize attempts to limit the maximum EDNS response size. This is not
|
||||
// an exhaustive solution, instead only easy cases are currently handled in the
|
||||
// interest of speed and reduced complexity. Only OPT records at the very end of
|
||||
// the message with no option codes are addressed.
|
||||
// TODO: handle more situations if we discover that they happen often
|
||||
func clampEDNSSize(packet []byte, maxSize uint16) {
|
||||
// optFixedBytes is the size of an OPT record with no option codes.
|
||||
const optFixedBytes = 11
|
||||
const edns0Version = 0
|
||||
|
||||
if len(packet) < headerBytes+optFixedBytes {
|
||||
return
|
||||
}
|
||||
|
||||
arCount := binary.BigEndian.Uint16(packet[10:12])
|
||||
if arCount == 0 {
|
||||
// OPT shows up in an AR, so there must be no OPT
|
||||
return
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.2
|
||||
opt := packet[len(packet)-optFixedBytes:]
|
||||
|
||||
if opt[0] != 0 {
|
||||
// OPT NAME must be 0 (root domain)
|
||||
return
|
||||
}
|
||||
if dns.Type(binary.BigEndian.Uint16(opt[1:3])) != dns.TypeOPT {
|
||||
// Not an OPT record
|
||||
return
|
||||
}
|
||||
requestedSize := binary.BigEndian.Uint16(opt[3:5])
|
||||
// Ignore extended RCODE in opt[5]
|
||||
if opt[6] != edns0Version {
|
||||
// Be conservative and don't touch unknown versions.
|
||||
return
|
||||
}
|
||||
// Ignore flags in opt[6:9]
|
||||
if binary.BigEndian.Uint16(opt[9:11]) != 0 {
|
||||
// RDLEN must be 0 (no variable length data). We're at the end of the
|
||||
// packet so this should be 0 anyway)..
|
||||
requestedSize, opt := findOPTRecord(packet)
|
||||
if opt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -183,6 +209,57 @@ func clampEDNSSize(packet []byte, maxSize uint16) {
|
||||
binary.BigEndian.PutUint16(opt[3:5], maxSize)
|
||||
}
|
||||
|
||||
// getEDNSBufferSize extracts the EDNS buffer size from a DNS request packet.
|
||||
// Returns (bufferSize, true) if a valid EDNS OPT record is found,
|
||||
// or (0, false) if no EDNS OPT record is found or if there's an error.
|
||||
func getEDNSBufferSize(packet []byte) (uint16, bool) {
|
||||
requestedSize, opt := findOPTRecord(packet)
|
||||
return requestedSize, opt != nil
|
||||
}
|
||||
|
||||
// checkResponseSizeAndSetTC sets the TC (truncated) flag in the DNS header when
|
||||
// the response exceeds the maximum UDP size. If no EDNS OPT record is present
|
||||
// in the request, it sets the TC flag when the response is bigger than 512 bytes
|
||||
// per RFC 1035. If an EDNS OPT record is present, it sets the TC flag when the
|
||||
// response is bigger than the EDNS buffer size. The response buffer is not
|
||||
// truncated; only the TC flag is set. Returns the response unchanged except for
|
||||
// the TC flag being set if needed.
|
||||
func checkResponseSizeAndSetTC(response []byte, request []byte, family string, logf logger.Logf) []byte {
|
||||
const defaultUDPSize = 512 // default maximum UDP DNS packet size per RFC 1035
|
||||
|
||||
// Only check for UDP queries; TCP can handle larger responses
|
||||
if family != "udp" {
|
||||
return response
|
||||
}
|
||||
|
||||
// Check if TC flag is already set
|
||||
if len(response) < headerBytes {
|
||||
return response
|
||||
}
|
||||
if truncatedFlagSet(response) {
|
||||
// TC flag already set, nothing to do
|
||||
return response
|
||||
}
|
||||
|
||||
ednsSize, hasEDNS := getEDNSBufferSize(request)
|
||||
|
||||
// Determine maximum allowed size
|
||||
var maxSize int
|
||||
if hasEDNS {
|
||||
maxSize = int(ednsSize)
|
||||
} else {
|
||||
// No EDNS: enforce default UDP size limit per RFC 1035
|
||||
maxSize = defaultUDPSize
|
||||
}
|
||||
|
||||
// Check if response exceeds maximum size
|
||||
if len(response) > maxSize {
|
||||
setTCFlag(response)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// dnsForwarderFailing should be raised when the forwarder is unable to reach the
|
||||
// upstream resolvers. This is a high severity warning as it results in "no internet".
|
||||
// This warning must be cleared when the forwarder is working again.
|
||||
@@ -249,6 +326,30 @@ type forwarder struct {
|
||||
// /etc/resolv.conf is missing/corrupt, and the peerapi ExitDNS stub
|
||||
// resolver lookup.
|
||||
cloudHostFallback []resolverAndDelay
|
||||
|
||||
// schemes are the collection of registered URI scheme names that
|
||||
// dynamically decide which resolver to use at the time of each query. The
|
||||
// key is the scheme (the portion before the first `:`) and the value is a
|
||||
// handler that determines where the current query should be sent.
|
||||
// Use schemeCacheLocked() to get the current contents that can continue to
|
||||
// be accessed once mu is released. This allows the (much more common)
|
||||
// resolver code path to avoid repeated locking and unlocking.
|
||||
// When modified, call invalidateSchemeCacheLocked() before unlocking mu.
|
||||
schemes map[string]CustomSchemeHandler
|
||||
// schemeCache is an immutable copy of schemes. Do not read directly,
|
||||
// use schemeCacheLocked() which will regenerate its contents as needed.
|
||||
schemeCache views.Map[string, CustomSchemeHandler]
|
||||
|
||||
// acceptDNS tracks the CorpDNS pref (--accept-dns)
|
||||
// This lets us skip health warnings if the forwarder receives inbound
|
||||
// queries directly - but we didn't configure it with any upstream resolvers.
|
||||
// That's an error, but not a health error if the user has disabled CorpDNS.
|
||||
acceptDNS bool
|
||||
}
|
||||
|
||||
func (f *forwarder) probeLocks() {
|
||||
f.mu.Lock()
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
func newForwarder(logf logger.Logf, netMon *netmon.Monitor, linkSel ForwardLinkSelector, dialer *tsdial.Dialer, health *health.Tracker, knobs *controlknobs.Knobs) *forwarder {
|
||||
@@ -360,7 +461,7 @@ func cloudResolvers() []resolverAndDelay {
|
||||
// Resolver.SetConfig on reconfig.
|
||||
//
|
||||
// The memory referenced by routesBySuffix should not be modified.
|
||||
func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]*dnstype.Resolver) {
|
||||
func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]*dnstype.Resolver, acceptDNS bool) {
|
||||
routes := make([]route, 0, len(routesBySuffix))
|
||||
|
||||
cloudHostFallback := cloudResolvers()
|
||||
@@ -394,6 +495,7 @@ func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]*dnstype.Resolve
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.acceptDNS = acceptDNS
|
||||
f.routes = routes
|
||||
f.cloudHostFallback = cloudHostFallback
|
||||
}
|
||||
@@ -430,6 +532,62 @@ func (f *forwarder) getKnownDoHClientForProvider(urlBase string) (c *http.Client
|
||||
if len(allIPs) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return f.newDoHClientLocked(urlBase, urlBase, allIPs)
|
||||
}
|
||||
|
||||
// getDoHClientForResolver returns an HTTP client for the DoH server described
|
||||
// by r. Known public providers ([publicdns.DoHIPsOfBase]) work as before. For
|
||||
// arbitrary providers — typically enterprise resolvers recovered from the
|
||||
// OS's base configuration — the resolver must either carry a
|
||||
// BootstrapResolution, or its URL host must be an IP literal (which needs no
|
||||
// bootstrap): we cannot resolve the DoH server's own name through DNS without
|
||||
// recursing through ourselves.
|
||||
func (f *forwarder) getDoHClientForResolver(r *dnstype.Resolver) (c *http.Client, ok bool) {
|
||||
urlBase := r.Addr
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
key := dohClientCacheKey(urlBase, r.BootstrapResolution)
|
||||
if c, ok := f.dohClient[key]; ok {
|
||||
return c, true
|
||||
}
|
||||
allIPs := publicdns.DoHIPsOfBase(urlBase)
|
||||
if len(allIPs) == 0 {
|
||||
if len(r.BootstrapResolution) > 0 {
|
||||
allIPs = r.BootstrapResolution
|
||||
} else {
|
||||
// A URL whose host is an IP literal dials itself; no bootstrap
|
||||
// resolution is needed.
|
||||
u, err := url.Parse(urlBase)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
ip, err := netip.ParseAddr(u.Hostname())
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
allIPs = []netip.Addr{ip}
|
||||
}
|
||||
}
|
||||
return f.newDoHClientLocked(key, urlBase, allIPs)
|
||||
}
|
||||
|
||||
// dohClientCacheKey distinguishes cached clients: bootstrap results can change
|
||||
// with the underlying network, unlike known public provider IPs.
|
||||
func dohClientCacheKey(urlBase string, bootstrap []netip.Addr) string {
|
||||
if len(bootstrap) == 0 || len(publicdns.DoHIPsOfBase(urlBase)) > 0 {
|
||||
return urlBase
|
||||
}
|
||||
ips := make([]string, len(bootstrap))
|
||||
for i, ip := range bootstrap {
|
||||
ips[i] = ip.String()
|
||||
}
|
||||
return urlBase + "\x00" + strings.Join(ips, ",")
|
||||
}
|
||||
|
||||
// newDoHClientLocked builds a DoH HTTP client that dials urlBase's host at
|
||||
// the given IPs, caching the client under cacheKey (which differs from
|
||||
// urlBase when bootstrap results vary). f.mu must be held.
|
||||
func (f *forwarder) newDoHClientLocked(cacheKey, urlBase string, allIPs []netip.Addr) (c *http.Client, ok bool) {
|
||||
dohURL, err := url.Parse(urlBase)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
@@ -465,7 +623,7 @@ func (f *forwarder) getKnownDoHClientForProvider(urlBase string) (c *http.Client
|
||||
if f.dohClient == nil {
|
||||
f.dohClient = map[string]*http.Client{}
|
||||
}
|
||||
f.dohClient[urlBase] = c
|
||||
f.dohClient[cacheKey] = c
|
||||
return c, true
|
||||
}
|
||||
|
||||
@@ -535,21 +693,32 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
|
||||
if !buildfeatures.HasPeerAPIClient {
|
||||
return nil, feature.ErrUnavailable
|
||||
}
|
||||
return f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
|
||||
res, err := f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check response size and set TC flag if needed (only for UDP queries)
|
||||
res = checkResponseSizeAndSetTC(res, fq.packet, fq.family, f.logf)
|
||||
return res, nil
|
||||
}
|
||||
if strings.HasPrefix(rr.name.Addr, "https://") {
|
||||
// Only known DoH providers are supported currently. Specifically, we
|
||||
// only support DoH providers where we can TCP connect to them on port
|
||||
// 443 at the same IP address they serve normal UDP DNS from (1.1.1.1,
|
||||
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custom DoH providers
|
||||
// aren't currently supported. There's no backup DNS resolution path for
|
||||
// them.
|
||||
urlBase := rr.name.Addr
|
||||
if hc, ok := f.getKnownDoHClientForProvider(urlBase); ok {
|
||||
return f.sendDoH(ctx, urlBase, hc, fq.packet)
|
||||
// Known DoH providers (see tailscale.com/net/dns/publicdns) are dialed
|
||||
// at their well-known IPs. Arbitrary providers — typically enterprise
|
||||
// resolvers recovered from the OS's base DNS configuration — are only
|
||||
// usable when the resolver carries a bootstrap resolution, or when the
|
||||
// URL's host is an IP literal, since we cannot resolve the DoH
|
||||
// server's own name through DNS without recursing through ourselves.
|
||||
if hc, ok := f.getDoHClientForResolver(rr.name); ok {
|
||||
res, err := f.sendDoH(ctx, rr.name.Addr, hc, fq.packet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check response size and set TC flag if needed (only for UDP queries)
|
||||
res = checkResponseSizeAndSetTC(res, fq.packet, fq.family, f.logf)
|
||||
return res, nil
|
||||
}
|
||||
metricDNSFwdErrorType.Add(1)
|
||||
return nil, fmt.Errorf("arbitrary https:// resolvers not supported yet")
|
||||
return nil, fmt.Errorf("cannot dial https:// resolver %q: no known IPs or bootstrap resolution", rr.name.Addr)
|
||||
}
|
||||
if strings.HasPrefix(rr.name.Addr, "tls://") {
|
||||
metricDNSFwdErrorType.Add(1)
|
||||
@@ -634,8 +803,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
|
||||
}
|
||||
|
||||
// If we got a truncated UDP response, return that instead of an error.
|
||||
var trErr truncatedResponseError
|
||||
if errors.As(err, &trErr) {
|
||||
if trErr, ok := errors.AsType[truncatedResponseError](err); ok {
|
||||
return trErr.res, nil
|
||||
}
|
||||
return nil, err
|
||||
@@ -647,6 +815,27 @@ type truncatedResponseError struct {
|
||||
|
||||
func (tr truncatedResponseError) Error() string { return "response truncated" }
|
||||
|
||||
// rcodeResponseError is returned when an upstream DNS server responds with an
|
||||
// rcode that is treated as a soft error (currently REFUSED and SERVFAIL). The
|
||||
// response bytes are preserved so they can be returned to the client rather
|
||||
// than synthesizing a new response.
|
||||
type rcodeResponseError struct {
|
||||
rcode dns.RCode
|
||||
res []byte
|
||||
}
|
||||
|
||||
func (r rcodeResponseError) Error() string { return r.Unwrap().Error() }
|
||||
func (r rcodeResponseError) Unwrap() error {
|
||||
switch r.rcode {
|
||||
case dns.RCodeRefused:
|
||||
return errRefused
|
||||
case dns.RCodeServerFailure:
|
||||
return errServerFailure
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var errRefused = errors.New("response code indicates refusal")
|
||||
var errServerFailure = errors.New("response code indicates server issue")
|
||||
var errTxIDMismatch = errors.New("txid doesn't match")
|
||||
|
||||
@@ -659,21 +848,10 @@ func (f *forwarder) sendUDP(ctx context.Context, fq *forwardQuery, rr resolverAn
|
||||
metricDNSFwdUDP.Add(1)
|
||||
ctx = sockstats.WithSockStats(ctx, sockstats.LabelDNSForwarderUDP, f.logf)
|
||||
|
||||
ln, err := f.packetListener(ipp.Addr())
|
||||
conn, err := f.dialUDP(ctx, ipp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Specify the exact UDP family to work around https://github.com/golang/go/issues/52264
|
||||
udpFam := "udp4"
|
||||
if ipp.Addr().Is6() {
|
||||
udpFam = "udp6"
|
||||
}
|
||||
conn, err := ln.ListenPacket(ctx, udpFam, ":0")
|
||||
if err != nil {
|
||||
f.logf("ListenPacket failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
fq.closeOnCtxDone.Add(conn)
|
||||
@@ -690,44 +868,72 @@ func (f *forwarder) sendUDP(ctx context.Context, fq *forwardQuery, rr resolverAn
|
||||
|
||||
// The 1 extra byte is to detect packet truncation.
|
||||
out := make([]byte, maxResponseBytes+1)
|
||||
n, _, err := conn.ReadFromUDPAddrPort(out)
|
||||
if err != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
|
||||
// The conn is unconnected (see dialUDP), so datagrams can arrive from
|
||||
// any address, not just the resolver we queried. A reply must come
|
||||
// from the resolver's address and carry the transaction ID we sent.
|
||||
// Datagrams that are neither are dropped rather than acted on, so
|
||||
// neither a spoofed reply nor a single stray datagram can decide the
|
||||
// query. The loop ends when the conn is closed, which the query's
|
||||
// context cancellation does via fq.closeOnCtxDone.
|
||||
var n int
|
||||
for {
|
||||
var src netip.AddrPort
|
||||
var err error
|
||||
n, src, err = conn.ReadFromUDPAddrPort(out)
|
||||
if err != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !neterror.PacketWasTruncated(err) {
|
||||
metricDNSFwdUDPErrorRead.Add(1)
|
||||
return nil, err
|
||||
}
|
||||
// Windows reports a datagram larger than out as a
|
||||
// truncation error, returning the bytes that fit in
|
||||
// out but no source address. Fall through and let the
|
||||
// txid check decide, since the source can't be checked.
|
||||
} else if src != ipp {
|
||||
// Not from the resolver we asked, so not a reply to
|
||||
// this query.
|
||||
metricDNSFwdUDPDropSrc.Add(1)
|
||||
continue
|
||||
}
|
||||
if neterror.PacketWasTruncated(err) {
|
||||
err = nil
|
||||
} else {
|
||||
metricDNSFwdUDPErrorRead.Add(1)
|
||||
return nil, err
|
||||
if n < headerBytes {
|
||||
f.logf("recv: packet too small (%d bytes)", n)
|
||||
continue
|
||||
}
|
||||
if getTxID(out[:n]) != fq.txid {
|
||||
metricDNSFwdUDPErrorTxID.Add(1)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
truncated := n > maxResponseBytes
|
||||
if truncated {
|
||||
n = maxResponseBytes
|
||||
}
|
||||
if n < headerBytes {
|
||||
f.logf("recv: packet too small (%d bytes)", n)
|
||||
}
|
||||
out = out[:n]
|
||||
txid := getTxID(out)
|
||||
if txid != fq.txid {
|
||||
metricDNSFwdUDPErrorTxID.Add(1)
|
||||
return nil, errTxIDMismatch
|
||||
}
|
||||
tcFlagAlreadySet := truncatedFlagSet(out)
|
||||
|
||||
rcode := getRCode(out)
|
||||
|
||||
// don't forward transient errors back to the client when the server fails
|
||||
if rcode == dns.RCodeServerFailure {
|
||||
f.logf("recv: response code indicating server failure: %d", rcode)
|
||||
switch rcode {
|
||||
case dns.RCodeServerFailure:
|
||||
f.logf("sendUDP: response code indicating server failure: %d", rcode)
|
||||
metricDNSFwdUDPErrorServer.Add(1)
|
||||
return nil, errServerFailure
|
||||
return nil, rcodeResponseError{dns.RCodeServerFailure, out}
|
||||
case dns.RCodeRefused:
|
||||
// treat REFUSED as a soft error so other resolvers in the race can respond
|
||||
f.logf("sendUDP: response code indicating refusal: %d", rcode)
|
||||
metricDNSFwdUDPErrorRefused.Add(1)
|
||||
return nil, rcodeResponseError{dns.RCodeRefused, out}
|
||||
}
|
||||
|
||||
if truncated {
|
||||
// Set the truncated bit if it wasn't already.
|
||||
flags := binary.BigEndian.Uint16(out[2:4])
|
||||
flags |= dnsFlagTruncated
|
||||
binary.BigEndian.PutUint16(out[2:4], flags)
|
||||
// Set the truncated bit if buffer was truncated during read and the flag isn't already set
|
||||
if truncated && !tcFlagAlreadySet {
|
||||
setTCFlag(out)
|
||||
|
||||
// TODO(#2067): Remove any incomplete records? RFC 1035 section 6.2
|
||||
// states that truncation should head drop so that the authority
|
||||
@@ -736,6 +942,8 @@ func (f *forwarder) sendUDP(ctx context.Context, fq *forwardQuery, rr resolverAn
|
||||
// best we can do.
|
||||
}
|
||||
|
||||
out = checkResponseSizeAndSetTC(out, fq.packet, fq.family, f.logf)
|
||||
|
||||
if truncatedFlagSet(out) {
|
||||
metricDNSFwdTruncated.Add(1)
|
||||
}
|
||||
@@ -745,6 +953,58 @@ func (f *forwarder) sendUDP(ctx context.Context, fq *forwardQuery, rr resolverAn
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// dialUDP returns a UDP conn to ipp, over netstack if that's the only way to
|
||||
// reach it. Same dispatch as [tsdial.Dialer.dialOneUser].
|
||||
func (f *forwarder) dialUDP(ctx context.Context, ipp netip.AddrPort) (nettype.PacketConn, error) {
|
||||
if f.dialer.UseNetstackForIP != nil && f.dialer.UseNetstackForIP(ipp.Addr()) {
|
||||
if f.dialer.NetstackDialUDP == nil {
|
||||
return nil, errors.New("dialer not initialized correctly: no NetstackDialUDP")
|
||||
}
|
||||
conn, err := f.dialer.NetstackDialUDP(ctx, ipp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &netstackPacketConn{Conn: conn, peer: ipp}, nil
|
||||
}
|
||||
|
||||
ln, err := f.packetListener(ipp.Addr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Name the family explicitly: netns looks for a "6" in this string to
|
||||
// choose between IP_BOUND_IF and IPV6_BOUND_IF on macOS, and "udp" would
|
||||
// give a v6 socket bound with the v4 option.
|
||||
udpFam := "udp4"
|
||||
if ipp.Addr().Is6() {
|
||||
udpFam = "udp6"
|
||||
}
|
||||
conn, err := ln.ListenPacket(ctx, udpFam, ":0")
|
||||
if err != nil {
|
||||
f.logf("ListenPacket failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// netstackPacketConn presents a conn already connected to peer as a
|
||||
// [nettype.PacketConn].
|
||||
type netstackPacketConn struct {
|
||||
net.Conn
|
||||
peer netip.AddrPort
|
||||
}
|
||||
|
||||
func (c *netstackPacketConn) WriteToUDPAddrPort(b []byte, _ netip.AddrPort) (int, error) {
|
||||
return c.Write(b)
|
||||
}
|
||||
|
||||
// ReadFromUDPAddrPort returns how much of the datagram fit in b; gVisor drops
|
||||
// the rest without erroring, as a kernel socket does.
|
||||
func (c *netstackPacketConn) ReadFromUDPAddrPort(b []byte) (int, netip.AddrPort, error) {
|
||||
n, err := c.Read(b)
|
||||
return n, c.peer, err
|
||||
}
|
||||
|
||||
var optDNSForwardUseRoutes = envknob.RegisterOptBool("TS_DEBUG_DNS_FORWARD_USE_ROUTES")
|
||||
|
||||
// ShouldUseRoutes reports whether the DNS resolver should consider routes when dialing
|
||||
@@ -765,7 +1025,7 @@ func ShouldUseRoutes(knobs *controlknobs.Knobs) bool {
|
||||
switch runtime.GOOS {
|
||||
case "android", "ios":
|
||||
// On mobile platforms with lower memory limits (e.g., 50MB on iOS),
|
||||
// this behavior is still gated by the "user-dial-routes" nodeAttr.
|
||||
// this behavior is still gated by the "user-dial-routes" nodecap.
|
||||
return knobs != nil && knobs.UserDialUseRoutes.Load()
|
||||
default:
|
||||
// On all other platforms, it is the default behavior,
|
||||
@@ -856,10 +1116,16 @@ func (f *forwarder) sendTCP(ctx context.Context, fq *forwardQuery, rr resolverAn
|
||||
rcode := getRCode(out)
|
||||
|
||||
// don't forward transient errors back to the client when the server fails
|
||||
if rcode == dns.RCodeServerFailure {
|
||||
switch rcode {
|
||||
case dns.RCodeServerFailure:
|
||||
f.logf("sendTCP: response code indicating server failure: %d", rcode)
|
||||
metricDNSFwdTCPErrorServer.Add(1)
|
||||
return nil, errServerFailure
|
||||
return nil, rcodeResponseError{dns.RCodeServerFailure, out}
|
||||
case dns.RCodeRefused:
|
||||
// treat REFUSED as a soft error so other resolvers in the race can respond
|
||||
f.logf("sendTCP: response code indicating refusal: %d", rcode)
|
||||
metricDNSFwdTCPErrorRefused.Add(1)
|
||||
return nil, rcodeResponseError{dns.RCodeRefused, out}
|
||||
}
|
||||
|
||||
// TODO(andrew): do we need to do this?
|
||||
@@ -868,15 +1134,66 @@ func (f *forwarder) sendTCP(ctx context.Context, fq *forwardQuery, rr resolverAn
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// applySchemes resolves any custom-scheme entries in rrs using the provided
|
||||
// scheme handlers, returning the resulting slice. Entries whose handler returns
|
||||
// an error or empty string are dropped. Entries with no registered scheme pass
|
||||
// through unchanged. If schemes is nil, rrs is returned as-is.
|
||||
func applySchemes(logf logger.Logf, rrs []resolverAndDelay, schemes views.Map[string, CustomSchemeHandler]) []resolverAndDelay {
|
||||
if schemes.IsNil() {
|
||||
return rrs
|
||||
}
|
||||
var result []resolverAndDelay
|
||||
for i, rr := range rrs {
|
||||
scheme, _, hasColon := strings.Cut(rr.name.Addr, ":")
|
||||
handler, isCustom := schemes.GetOk(scheme)
|
||||
if !hasColon || !isCustom {
|
||||
if result != nil {
|
||||
result = append(result, rr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Avoid making a results slice in the common case where there
|
||||
// are no custom scheme resolvers.
|
||||
if result == nil {
|
||||
result = make([]resolverAndDelay, i, len(rrs))
|
||||
copy(result, rrs)
|
||||
}
|
||||
newAddr, err := handler(rr.name.Addr)
|
||||
if err != nil {
|
||||
logf("error from custom scheme handler, skipping resolver : %v", err)
|
||||
}
|
||||
if err != nil || newAddr == "" {
|
||||
continue
|
||||
}
|
||||
newResolver := *rr.name
|
||||
newResolver.Addr = newAddr
|
||||
result = append(result, resolverAndDelay{name: &newResolver, startDelay: rr.startDelay})
|
||||
}
|
||||
// If we didn't have any custom schemes, return the original rrs.
|
||||
if result == nil {
|
||||
return rrs
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// resolvers returns the resolvers to use for domain.
|
||||
func (f *forwarder) resolvers(domain dnsname.FQDN) []resolverAndDelay {
|
||||
f.mu.Lock()
|
||||
routes := f.routes
|
||||
cloudHostFallback := f.cloudHostFallback
|
||||
schemes := f.schemeCacheLocked()
|
||||
f.mu.Unlock()
|
||||
|
||||
for _, route := range routes {
|
||||
if route.Suffix == "." || route.Suffix.Contains(domain) {
|
||||
return route.Resolvers
|
||||
if route.Suffix != "." && !route.Suffix.Contains(domain) {
|
||||
continue
|
||||
}
|
||||
resolved := applySchemes(f.logf, route.Resolvers, schemes)
|
||||
// If scheme resolution filtered out all resolvers from a non-empty
|
||||
// route, fall through to the next matching route. If the resolvers
|
||||
// were configured to be empty allow resolved to be empty.
|
||||
if len(resolved) > 0 || len(route.Resolvers) == 0 {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
return cloudHostFallback // or nil if no fallback
|
||||
@@ -893,6 +1210,39 @@ func (f *forwarder) GetUpstreamResolvers(name dnsname.FQDN) []*dnstype.Resolver
|
||||
return upstreamResolvers
|
||||
}
|
||||
|
||||
// RegisterCustomScheme adds a [CustomSchemeHandler] that is called to provide
|
||||
// an updated address when a [dnstype.Resolver.Addr] uses that scheme.
|
||||
func (f *forwarder) RegisterCustomScheme(scheme string, h CustomSchemeHandler) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if _, ok := f.schemes[scheme]; ok {
|
||||
return fmt.Errorf("scheme %q already registered", scheme)
|
||||
}
|
||||
f.invalidateSchemeCacheLocked()
|
||||
mak.Set(&f.schemes, scheme, h)
|
||||
return nil
|
||||
}
|
||||
|
||||
// invalidateSchemeCacheLocked clears f.schemeCache so that it will be rebuilt
|
||||
// on the next call to f.schemeCacheLocked().
|
||||
func (f *forwarder) invalidateSchemeCacheLocked() {
|
||||
f.schemeCache = views.Map[string, CustomSchemeHandler]{}
|
||||
}
|
||||
|
||||
// schemeCacheLocked returns an immutable copy of f.schemes that can be used
|
||||
// after mu is unlocked.
|
||||
func (f *forwarder) schemeCacheLocked() views.Map[string, CustomSchemeHandler] {
|
||||
if !f.schemeCache.IsNil() {
|
||||
return f.schemeCache
|
||||
}
|
||||
if f.schemes == nil {
|
||||
return f.schemeCache // returns a nil view
|
||||
}
|
||||
// Regenerate the cache
|
||||
f.schemeCache = views.MapOf(maps.Clone(f.schemes))
|
||||
return f.schemeCache
|
||||
}
|
||||
|
||||
// forwardQuery is information and state about a forwarded DNS query that's
|
||||
// being sent to 1 or more upstreams.
|
||||
//
|
||||
@@ -966,8 +1316,11 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
if len(resolvers) == 0 {
|
||||
resolvers = f.resolvers(domain)
|
||||
if len(resolvers) == 0 {
|
||||
// No upstream resolver for this name isn't a forwarder failure:
|
||||
// it's split DNS / a name we weren't asked to handle. Count it
|
||||
// rather than raising dnsForwarderFailing, which is reserved for
|
||||
// resolvers we found but couldn't reach. See tailscale/tailscale#19931.
|
||||
metricDNSFwdErrorNoUpstream.Add(1)
|
||||
f.health.SetUnhealthy(dnsForwarderFailing, health.Args{health.ArgDNSServers: ""})
|
||||
f.logf("no upstream resolvers set, returning SERVFAIL")
|
||||
|
||||
res, err := servfailResponse(query)
|
||||
@@ -1031,6 +1384,7 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
|
||||
var firstErr error
|
||||
var numErr int
|
||||
var sawNonRefused bool
|
||||
for {
|
||||
select {
|
||||
case v := <-resc:
|
||||
@@ -1050,30 +1404,56 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
if !errors.Is(err, errRefused) {
|
||||
sawNonRefused = true
|
||||
}
|
||||
numErr++
|
||||
if numErr == len(resolvers) {
|
||||
if errors.Is(firstErr, errServerFailure) {
|
||||
res, err := servfailResponse(query)
|
||||
if err != nil {
|
||||
f.logf("building servfail response: %v", err)
|
||||
var res packet
|
||||
if sawNonRefused {
|
||||
// At least one server failed with SERVFAIL or a transport error
|
||||
// (e.g. network failure, TxID mismatch, unsupported resolver type).
|
||||
// All such errors map to SERVFAIL at the client level.
|
||||
// Prefer returning the upstream SERVFAIL bytes from firstErr if
|
||||
// available; otherwise synthesize a SERVFAIL response. Note the
|
||||
// rcode guard: firstErr may be a REFUSED rcodeResponseError if it
|
||||
// arrived before the SERVFAIL that set sawNonRefused.
|
||||
if rcodeErr, ok := errors.AsType[rcodeResponseError](firstErr); ok && rcodeErr.rcode == dns.RCodeServerFailure {
|
||||
res = packet{rcodeErr.res, query.family, query.addr}
|
||||
} else {
|
||||
r, err := servfailResponse(query)
|
||||
if err != nil {
|
||||
f.logf("building servfail response: %v", err)
|
||||
return firstErr
|
||||
}
|
||||
res = r
|
||||
}
|
||||
} else {
|
||||
// !sawNonRefused means every error was an rcodeResponseError with rcode REFUSED,
|
||||
// so firstErr is guaranteed to wrap one.
|
||||
rcodeErr, ok := errors.AsType[rcodeResponseError](firstErr)
|
||||
if !ok {
|
||||
f.logf("unexpected: all errors were REFUSED but firstErr is not rcodeResponseError: %v", firstErr)
|
||||
return firstErr
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
metricDNSFwdErrorContext.Add(1)
|
||||
metricDNSFwdErrorContextGotError.Add(1)
|
||||
var resolverAddrs []string
|
||||
for _, rr := range resolvers {
|
||||
resolverAddrs = append(resolverAddrs, rr.name.Addr)
|
||||
}
|
||||
f.health.SetUnhealthy(dnsForwarderFailing, health.Args{health.ArgDNSServers: strings.Join(resolverAddrs, ",")})
|
||||
case responseChan <- res:
|
||||
if f.verboseFwd {
|
||||
f.logf("forwarder response(%d, %v, %d) = %d, %v", fq.txid, typ, len(domain), len(res.bs), firstErr)
|
||||
}
|
||||
return nil
|
||||
res = packet{rcodeErr.res, query.family, query.addr}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
metricDNSFwdErrorContext.Add(1)
|
||||
metricDNSFwdErrorContextGotError.Add(1)
|
||||
var resolverAddrs []string
|
||||
for _, rr := range resolvers {
|
||||
resolverAddrs = append(resolverAddrs, rr.name.Addr)
|
||||
}
|
||||
if f.acceptDNS {
|
||||
f.health.SetUnhealthy(dnsForwarderFailing, health.Args{health.ArgDNSServers: strings.Join(resolverAddrs, ",")})
|
||||
}
|
||||
case responseChan <- res:
|
||||
if f.verboseFwd {
|
||||
f.logf("forwarder response(%d, %v, %d) = %d, %v", fq.txid, typ, len(domain), len(res.bs), firstErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
@@ -1086,13 +1466,14 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
|
||||
// If we haven't got an error or a successful response,
|
||||
// include all resolvers in the error message so we can
|
||||
// at least see what what servers we're trying to
|
||||
// query.
|
||||
// at least see what servers we're trying to query.
|
||||
var resolverAddrs []string
|
||||
for _, rr := range resolvers {
|
||||
resolverAddrs = append(resolverAddrs, rr.name.Addr)
|
||||
}
|
||||
f.health.SetUnhealthy(dnsForwarderFailing, health.Args{health.ArgDNSServers: strings.Join(resolverAddrs, ",")})
|
||||
if f.acceptDNS {
|
||||
f.health.SetUnhealthy(dnsForwarderFailing, health.Args{health.ArgDNSServers: strings.Join(resolverAddrs, ",")})
|
||||
}
|
||||
return fmt.Errorf("waiting for response or error from %v: %w", resolverAddrs, ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_macext && (darwin || ios)
|
||||
|
||||
+253
-71
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package resolver implements a stub DNS resolver that can also serve
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
const dnsSymbolicFQDN = "magicdns.localhost-tailscale-daemon."
|
||||
@@ -48,8 +49,26 @@ const dnsSymbolicFQDN = "magicdns.localhost-tailscale-daemon."
|
||||
// truncation in a platform-agnostic way.
|
||||
const maxResponseBytes = 4095
|
||||
|
||||
// defaultTTL is the TTL of all responses from Resolver.
|
||||
const defaultTTL = 600 * time.Second
|
||||
// defaultTTL is the TTL of positive responses from Resolver.
|
||||
//
|
||||
// It's short because the source of truth (the netmap-fed host maps)
|
||||
// is local and in-memory, so re-queries are nearly free, while
|
||||
// anything cached downstream (e.g. mDNSResponder on macOS) delays
|
||||
// clients noticing node renames for the full TTL (tailscale/corp#45631).
|
||||
const defaultTTL = 5 * time.Second
|
||||
|
||||
// negativeTTL is how long resolvers may cache the nonexistence of a
|
||||
// name (or of records of the queried type) for domains we're
|
||||
// authoritative for. It's advertised via the SOA record attached to
|
||||
// the authority section of NXDOMAIN and no-data responses, per RFC
|
||||
// 2308. Without it, some resolvers (notably mDNSResponder) seem to
|
||||
// cache negative entries for a really long time, so a name queried
|
||||
// shortly before a node rename doesn't start resolving for a while
|
||||
// (tailscale/corp#45631).
|
||||
const negativeTTL = 10 * time.Second
|
||||
|
||||
// timeNow is time.Now, except in tests.
|
||||
var timeNow = time.Now
|
||||
|
||||
var (
|
||||
errNotQuery = errors.New("not a DNS query")
|
||||
@@ -69,6 +88,9 @@ type packet struct {
|
||||
// Else forward the query to the most specific matching entry in Routes.
|
||||
// Else return SERVFAIL.
|
||||
type Config struct {
|
||||
// True if [Prefs.CorpDNS] is true or --accept-dns=true was specified.
|
||||
// This should only be used for error handling and health reporting.
|
||||
AcceptDNS bool
|
||||
// Routes is a map of DNS name suffix to the resolvers to use for
|
||||
// queries within that suffix.
|
||||
// Queries only match the most specific suffix.
|
||||
@@ -79,6 +101,12 @@ type Config struct {
|
||||
// LocalDomains is a list of DNS name suffixes that should not be
|
||||
// routed to upstream resolvers.
|
||||
LocalDomains []dnsname.FQDN
|
||||
// SubdomainHosts is a set of FQDNs from Hosts that should also
|
||||
// resolve subdomain queries to the same IPs. If a query like
|
||||
// "sub.node.tailnet.ts.net" doesn't match Hosts directly, and
|
||||
// "node.tailnet.ts.net" is in SubdomainHosts, the query resolves
|
||||
// to the IPs for "node.tailnet.ts.net".
|
||||
SubdomainHosts set.Set[dnsname.FQDN]
|
||||
}
|
||||
|
||||
// WriteToBufioWriter write a debug version of c for logs to w, omitting
|
||||
@@ -162,7 +190,7 @@ func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]*dnstype.Resolver) {
|
||||
}
|
||||
kk = append(kk, k)
|
||||
}
|
||||
sort.Slice(kk, func(i, j int) bool { return kk[i] < kk[j] })
|
||||
slices.Sort(kk)
|
||||
w.WriteByte('{')
|
||||
for i, k := range kk {
|
||||
if i > 0 {
|
||||
@@ -214,10 +242,61 @@ type Resolver struct {
|
||||
closed chan struct{}
|
||||
|
||||
// mu guards the following fields from being updated while used.
|
||||
mu syncs.Mutex
|
||||
localDomains []dnsname.FQDN
|
||||
hostToIP map[dnsname.FQDN][]netip.Addr
|
||||
ipToHost map[netip.Addr]dnsname.FQDN
|
||||
mu syncs.Mutex
|
||||
localDomains []dnsname.FQDN
|
||||
hostToIP map[dnsname.FQDN][]netip.Addr
|
||||
ipToHost map[netip.Addr]dnsname.FQDN
|
||||
subdomainHosts set.Set[dnsname.FQDN]
|
||||
magicHosts MagicDNSHosts // or nil if none installed
|
||||
}
|
||||
|
||||
// MagicDNSHosts is a live source of MagicDNS host records, installed
|
||||
// via [Resolver.SetMagicDNSHosts].
|
||||
//
|
||||
// It replaces the per-node entries of [Config.Hosts]: instead of the
|
||||
// caller pushing a full snapshot of every node's name and addresses
|
||||
// into the resolver on every (possibly incremental) netmap change,
|
||||
// the resolver pulls the answer for one name on demand from the
|
||||
// caller's live indexes. [Config.Hosts] remains for control's
|
||||
// DNS.ExtraRecords entries, which are few, and is consulted first.
|
||||
//
|
||||
// Implementations must be safe for concurrent use and cheap: the
|
||||
// methods are called on the DNS query serving path. Name lookups are
|
||||
// case-insensitive: the resolver passes lowercase names, but
|
||||
// implementations must not rely on that.
|
||||
//
|
||||
// Short names ("foo") are only resolved if MagicDNS is enabled
|
||||
// for the current tailnet.
|
||||
type MagicDNSHosts interface {
|
||||
// LookupHost returns the IPs to answer for the node with the
|
||||
// given MagicDNS FQDN, and whether the name is known. It returns
|
||||
// all answerable IPs regardless of record type; the resolver
|
||||
// filters them by the query's type, and a known name with no IPs
|
||||
// of the query's family is "name exists, no records", not
|
||||
// NXDOMAIN.
|
||||
LookupHost(dnsname.FQDN) (ips []netip.Addr, ok bool)
|
||||
|
||||
// LookupPTR returns the MagicDNS FQDN of the node that owns
|
||||
// the given Tailscale IP, and whether the IP is known.
|
||||
LookupPTR(netip.Addr) (_ dnsname.FQDN, ok bool)
|
||||
|
||||
// SubdomainHost reports whether fqdn names a node with the
|
||||
// [tailcfg.NodeAttrDNSSubdomainResolve] attribute, whose
|
||||
// subdomains all resolve to the node's own addresses.
|
||||
SubdomainHost(dnsname.FQDN) bool
|
||||
}
|
||||
|
||||
// SetMagicDNSHosts installs the live MagicDNS host source consulted
|
||||
// by forward and reverse MagicDNS lookups that miss [Config.Hosts].
|
||||
// It is expected to be called once, before the resolver serves
|
||||
// queries.
|
||||
func (r *Resolver) SetMagicDNSHosts(h MagicDNSHosts) {
|
||||
if !buildfeatures.HasDNS {
|
||||
return
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.magicHosts = h
|
||||
}
|
||||
|
||||
type ForwardLinkSelector interface {
|
||||
@@ -255,6 +334,16 @@ func New(logf logger.Logf, linkSel ForwardLinkSelector, dialer *tsdial.Dialer, h
|
||||
|
||||
func (r *Resolver) TestOnlySetHook(hook func(Config)) { r.saveConfigForTests = hook }
|
||||
|
||||
// ProbeLocks acquires and releases the resolver's internal mutexes.
|
||||
func (r *Resolver) ProbeLocks() {
|
||||
r.mu.Lock()
|
||||
r.mu.Unlock()
|
||||
|
||||
if r.forwarder != nil {
|
||||
r.forwarder.probeLocks()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) SetConfig(cfg Config) error {
|
||||
if !buildfeatures.HasDNS {
|
||||
return nil
|
||||
@@ -271,16 +360,29 @@ func (r *Resolver) SetConfig(cfg Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
r.forwarder.setRoutes(cfg.Routes)
|
||||
r.forwarder.setRoutes(cfg.Routes, cfg.AcceptDNS)
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.localDomains = cfg.LocalDomains
|
||||
r.hostToIP = cfg.Hosts
|
||||
r.ipToHost = reverse
|
||||
r.subdomainHosts = cfg.SubdomainHosts
|
||||
return nil
|
||||
}
|
||||
|
||||
// CustomSchemeHandler takes a URI (retrieved from [dnstype.Resolver.Addr]) and
|
||||
// returns an updated URI to use for the current query. The result is only valid
|
||||
// for right now and may change over time.
|
||||
type CustomSchemeHandler func(addr string) (newAddr string, err error)
|
||||
|
||||
// RegisterCustomScheme adds a [CustomSchemaHandler] that is called to provide
|
||||
// an updated address to the forwarder when a [dnstype.Resolver.Addr] uses that
|
||||
// scheme.
|
||||
func (r *Resolver) RegisterCustomScheme(scheme string, h CustomSchemeHandler) error {
|
||||
return r.forwarder.RegisterCustomScheme(scheme, h)
|
||||
}
|
||||
|
||||
// Close shuts down the resolver and ensures poll goroutines have exited.
|
||||
// The Resolver cannot be used again after Close is called.
|
||||
func (r *Resolver) Close() {
|
||||
@@ -328,7 +430,12 @@ func (r *Resolver) Query(ctx context.Context, bs []byte, family string, from net
|
||||
return (<-responses).bs, nil
|
||||
}
|
||||
|
||||
return out, err
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
out = checkResponseSizeAndSetTC(out, bs, family, r.logf)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetUpstreamResolvers returns the resolvers that would be used to resolve
|
||||
@@ -642,9 +749,26 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
|
||||
r.mu.Lock()
|
||||
hosts := r.hostToIP
|
||||
localDomains := r.localDomains
|
||||
subdomainHosts := r.subdomainHosts
|
||||
magicHosts := r.magicHosts
|
||||
r.mu.Unlock()
|
||||
|
||||
addrs, found := hosts[domain]
|
||||
if !found && magicHosts != nil {
|
||||
addrs, found = magicHosts.LookupHost(domain)
|
||||
}
|
||||
if !found {
|
||||
for parent := domain.Parent(); parent != ""; parent = parent.Parent() {
|
||||
if subdomainHosts.Contains(parent) {
|
||||
addrs, found = hosts[parent]
|
||||
break
|
||||
}
|
||||
if magicHosts != nil && magicHosts.SubdomainHost(parent) {
|
||||
addrs, found = magicHosts.LookupHost(parent)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
for _, suffix := range localDomains {
|
||||
if suffix.Contains(domain) {
|
||||
@@ -715,23 +839,16 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
|
||||
}
|
||||
|
||||
// resolveViaDomain synthesizes an IP address for quad-A DNS requests of the form
|
||||
// `<IPv4-address-with-hypens-instead-of-dots>-via-<siteid>[.*]`. Two prior formats that
|
||||
// didn't pan out (due to a Chrome issue and DNS search ndots issues) were
|
||||
// `<IPv4-address>.via-<X>` and the older `via-<X>.<IPv4-address>`,
|
||||
// where X is a decimal, or hex-encoded number with a '0x' prefix.
|
||||
// `<IPv4-address-with-hypens-instead-of-dots>-via-<siteid>[.*]`.
|
||||
// For example: "192-168-1-2-via-7" or "192-168-1-2-via-7.foo.ts.net."
|
||||
//
|
||||
// This exists as a convenient mapping into Tailscales 'Via Range'.
|
||||
//
|
||||
// It returns a zero netip.Addr and true to indicate a successful response with
|
||||
// an empty answers section if the specified domain is a valid Tailscale 4via6
|
||||
// domain, but the request type is neither quad-A nor ALL.
|
||||
//
|
||||
// TODO(maisem/bradfitz/tom): `<IPv4-address>.via-<X>` was introduced
|
||||
// (2022-06-02) to work around an issue in Chrome where it would treat
|
||||
// "http://via-1.1.2.3.4" as a search string instead of a URL. We should rip out
|
||||
// the old format in early 2023.
|
||||
func (r *Resolver) resolveViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Addr, bool) {
|
||||
fqdn := string(domain.WithoutTrailingDot())
|
||||
func (r *Resolver) resolveViaDomain(dnsName dnsname.FQDN, typ dns.Type) (netip.Addr, bool) {
|
||||
fqdn := string(dnsName.WithoutTrailingDot())
|
||||
switch typ {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
|
||||
// For Type A requests, we should return a successful response
|
||||
@@ -745,45 +862,23 @@ func (r *Resolver) resolveViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Ad
|
||||
default:
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
if len(fqdn) < len("via-X.0.0.0.0") {
|
||||
if len(fqdn) < len("0-0-0-0-via-0") {
|
||||
return netip.Addr{}, false // too short to be valid
|
||||
}
|
||||
|
||||
var siteID string
|
||||
var ip4Str string
|
||||
switch {
|
||||
case strings.Contains(fqdn, "-via-"):
|
||||
// Format number 3: "192-168-1-2-via-7" or "192-168-1-2-via-7.foo.ts.net."
|
||||
// Third time's a charm. The earlier two formats follow after this block.
|
||||
firstLabel, domain, _ := strings.Cut(fqdn, ".") // "192-168-1-2-via-7"
|
||||
if !(domain == "" || dnsname.HasSuffix(domain, "ts.net") || dnsname.HasSuffix(domain, "tailscale.net")) {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
v4hyphens, suffix, ok := strings.Cut(firstLabel, "-via-")
|
||||
if !ok {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
siteID = suffix
|
||||
ip4Str = strings.ReplaceAll(v4hyphens, "-", ".")
|
||||
case strings.HasPrefix(fqdn, "via-"):
|
||||
firstDot := strings.Index(fqdn, ".")
|
||||
if firstDot < 0 {
|
||||
return netip.Addr{}, false // missing dot delimiters
|
||||
}
|
||||
siteID = fqdn[len("via-"):firstDot]
|
||||
ip4Str = fqdn[firstDot+1:]
|
||||
default:
|
||||
lastDot := strings.LastIndex(fqdn, ".")
|
||||
if lastDot < 0 {
|
||||
return netip.Addr{}, false // missing dot delimiters
|
||||
}
|
||||
suffix := fqdn[lastDot+1:]
|
||||
if !strings.HasPrefix(suffix, "via-") {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
siteID = suffix[len("via-"):]
|
||||
ip4Str = fqdn[:lastDot]
|
||||
if !strings.Contains(fqdn, "-via-") {
|
||||
return netip.Addr{}, false // not a 4via6 domain
|
||||
}
|
||||
firstLabel, domain, _ := strings.Cut(fqdn, ".") // "192-168-1-2-via-7"
|
||||
if !(domain == "" || dnsname.HasSuffix(domain, "ts.net") || dnsname.HasSuffix(domain, "tailscale.net")) {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
v4hyphens, suffix, ok := strings.Cut(firstLabel, "-via-")
|
||||
if !ok {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
siteID := suffix
|
||||
ip4Str := strings.ReplaceAll(v4hyphens, "-", ".")
|
||||
|
||||
ip4, err := netip.ParseAddr(ip4Str)
|
||||
if err != nil {
|
||||
@@ -846,6 +941,9 @@ func (r *Resolver) fqdnForIPLocked(ip netip.Addr, name dnsname.FQDN) (dnsname.FQ
|
||||
}
|
||||
|
||||
ret, ok := r.ipToHost[ip]
|
||||
if !ok && r.magicHosts != nil {
|
||||
ret, ok = r.magicHosts.LookupPTR(ip)
|
||||
}
|
||||
if !ok {
|
||||
for _, suffix := range r.localDomains {
|
||||
if suffix.Contains(name) {
|
||||
@@ -883,6 +981,14 @@ type response struct {
|
||||
|
||||
// NSs are the responses to an NS query.
|
||||
NSs []*net.NS
|
||||
|
||||
// SOAZone, if non-empty, is the zone we're authoritative for
|
||||
// that contains Question's name. marshalResponse attaches its
|
||||
// SOA record to the authority section so that resolvers bound
|
||||
// their negative caching to negativeTTL, per RFC 2308. It must
|
||||
// only be set on negative responses: NXDOMAIN, or success with
|
||||
// no records of the queried type.
|
||||
SOAZone dnsname.FQDN
|
||||
}
|
||||
|
||||
var dnsParserPool = &sync.Pool{
|
||||
@@ -1064,6 +1170,35 @@ func marshalSRV(queryName dns.Name, srvs []*net.SRV, builder *dns.Builder) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalSOA serializes zone's SOA record into the authority section
|
||||
// of an active builder, which must have had StartAuthorities called.
|
||||
// Its only purpose is communicating negativeTTL to caching resolvers
|
||||
// (RFC 2308), so all fields other than the TTLs are placeholders.
|
||||
func marshalSOA(zone dnsname.FQDN, builder *dns.Builder) error {
|
||||
name, err := dns.NewName(zone.WithTrailingDot())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return builder.SOAResource(dns.ResourceHeader{
|
||||
Name: name,
|
||||
Type: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
TTL: uint32(negativeTTL / time.Second),
|
||||
}, dns.SOAResource{
|
||||
NS: name,
|
||||
MBox: name,
|
||||
// A serial should only change when the zone data does,
|
||||
// but nothing consumes ours (no secondaries, no zone
|
||||
// transfers), and the current time is at least monotonic
|
||||
// and cheap. Unix seconds fit in the uint32 until 2106.
|
||||
Serial: uint32(timeNow().Unix()),
|
||||
Refresh: uint32(negativeTTL / time.Second),
|
||||
Retry: uint32(negativeTTL / time.Second),
|
||||
Expire: uint32(negativeTTL / time.Second),
|
||||
MinTTL: uint32(negativeTTL / time.Second),
|
||||
})
|
||||
}
|
||||
|
||||
// marshalResponse serializes the DNS response into a new buffer.
|
||||
func marshalResponse(resp *response) ([]byte, error) {
|
||||
resp.Header.Response = true
|
||||
@@ -1098,6 +1233,14 @@ func marshalResponse(resp *response) ([]byte, error) {
|
||||
|
||||
// Only successful responses contain answers.
|
||||
if !isSuccess {
|
||||
if resp.SOAZone != "" {
|
||||
if err := builder.StartAuthorities(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := marshalSOA(resp.SOAZone, &builder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return builder.Finish()
|
||||
}
|
||||
|
||||
@@ -1131,6 +1274,15 @@ func marshalResponse(resp *response) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.SOAZone != "" {
|
||||
if err := builder.StartAuthorities(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := marshalSOA(resp.SOAZone, &builder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return builder.Finish()
|
||||
}
|
||||
|
||||
@@ -1248,6 +1400,19 @@ func rdnsNameToIPv6(name dnsname.FQDN) (ip netip.Addr, ok bool) {
|
||||
|
||||
// respondReverse returns a DNS response to a PTR query.
|
||||
// It is assumed that resp.Question is populated by respond before this is called.
|
||||
// authoritativeZoneFor returns the zone from the configured local
|
||||
// domains that contains name, or "" if none does.
|
||||
func (r *Resolver) authoritativeZoneFor(name dnsname.FQDN) dnsname.FQDN {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for _, suffix := range r.localDomains {
|
||||
if suffix.Contains(name) {
|
||||
return suffix
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *response) ([]byte, error) {
|
||||
if hasRDNSBonjourPrefix(name) {
|
||||
metricDNSReverseMissBonjour.Add(1)
|
||||
@@ -1259,6 +1424,9 @@ func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *respons
|
||||
metricDNSReverseMissOther.Add(1)
|
||||
return nil, errNotOurName
|
||||
}
|
||||
if resp.Header.RCode == dns.RCodeNameError {
|
||||
resp.SOAZone = r.authoritativeZoneFor(name)
|
||||
}
|
||||
|
||||
metricDNSMagicDNSSuccessReverse.Add(1)
|
||||
return marshalResponse(resp)
|
||||
@@ -1315,6 +1483,17 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
|
||||
resp := parser.response()
|
||||
resp.Header.RCode = rcode
|
||||
resp.IP = ip
|
||||
switch {
|
||||
case rcode == dns.RCodeNameError:
|
||||
resp.SOAZone = r.authoritativeZoneFor(name)
|
||||
case rcode == dns.RCodeSuccess && !ip.IsValid():
|
||||
switch parser.Question.Type {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
|
||||
// The name exists but has no records of the queried
|
||||
// type (e.g. an AAAA query for an IPv4-only node).
|
||||
resp.SOAZone = r.authoritativeZoneFor(name)
|
||||
}
|
||||
}
|
||||
metricDNSMagicDNSSuccessName.Add(1)
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
@@ -1376,21 +1555,24 @@ var (
|
||||
metricDNSFwdErrorType = clientmetric.NewCounter("dns_query_fwd_error_type")
|
||||
metricDNSFwdTruncated = clientmetric.NewCounter("dns_query_fwd_truncated")
|
||||
|
||||
metricDNSFwdUDP = clientmetric.NewCounter("dns_query_fwd_udp") // on entry
|
||||
metricDNSFwdUDPWrote = clientmetric.NewCounter("dns_query_fwd_udp_wrote") // sent UDP packet
|
||||
metricDNSFwdUDPErrorWrite = clientmetric.NewCounter("dns_query_fwd_udp_error_write")
|
||||
metricDNSFwdUDPErrorServer = clientmetric.NewCounter("dns_query_fwd_udp_error_server")
|
||||
metricDNSFwdUDPErrorTxID = clientmetric.NewCounter("dns_query_fwd_udp_error_txid")
|
||||
metricDNSFwdUDPErrorRead = clientmetric.NewCounter("dns_query_fwd_udp_error_read")
|
||||
metricDNSFwdUDPSuccess = clientmetric.NewCounter("dns_query_fwd_udp_success")
|
||||
metricDNSFwdUDP = clientmetric.NewCounter("dns_query_fwd_udp") // on entry
|
||||
metricDNSFwdUDPWrote = clientmetric.NewCounter("dns_query_fwd_udp_wrote") // sent UDP packet
|
||||
metricDNSFwdUDPDropSrc = clientmetric.NewCounter("dns_query_fwd_udp_drop_src")
|
||||
metricDNSFwdUDPErrorWrite = clientmetric.NewCounter("dns_query_fwd_udp_error_write")
|
||||
metricDNSFwdUDPErrorServer = clientmetric.NewCounter("dns_query_fwd_udp_error_server")
|
||||
metricDNSFwdUDPErrorRefused = clientmetric.NewCounter("dns_query_fwd_udp_error_refused")
|
||||
metricDNSFwdUDPErrorTxID = clientmetric.NewCounter("dns_query_fwd_udp_error_txid")
|
||||
metricDNSFwdUDPErrorRead = clientmetric.NewCounter("dns_query_fwd_udp_error_read")
|
||||
metricDNSFwdUDPSuccess = clientmetric.NewCounter("dns_query_fwd_udp_success")
|
||||
|
||||
metricDNSFwdTCP = clientmetric.NewCounter("dns_query_fwd_tcp") // on entry
|
||||
metricDNSFwdTCPWrote = clientmetric.NewCounter("dns_query_fwd_tcp_wrote") // sent TCP packet
|
||||
metricDNSFwdTCPErrorWrite = clientmetric.NewCounter("dns_query_fwd_tcp_error_write")
|
||||
metricDNSFwdTCPErrorServer = clientmetric.NewCounter("dns_query_fwd_tcp_error_server")
|
||||
metricDNSFwdTCPErrorTxID = clientmetric.NewCounter("dns_query_fwd_tcp_error_txid")
|
||||
metricDNSFwdTCPErrorRead = clientmetric.NewCounter("dns_query_fwd_tcp_error_read")
|
||||
metricDNSFwdTCPSuccess = clientmetric.NewCounter("dns_query_fwd_tcp_success")
|
||||
metricDNSFwdTCP = clientmetric.NewCounter("dns_query_fwd_tcp") // on entry
|
||||
metricDNSFwdTCPWrote = clientmetric.NewCounter("dns_query_fwd_tcp_wrote") // sent TCP packet
|
||||
metricDNSFwdTCPErrorWrite = clientmetric.NewCounter("dns_query_fwd_tcp_error_write")
|
||||
metricDNSFwdTCPErrorServer = clientmetric.NewCounter("dns_query_fwd_tcp_error_server")
|
||||
metricDNSFwdTCPErrorRefused = clientmetric.NewCounter("dns_query_fwd_tcp_error_refused")
|
||||
metricDNSFwdTCPErrorTxID = clientmetric.NewCounter("dns_query_fwd_tcp_error_txid")
|
||||
metricDNSFwdTCPErrorRead = clientmetric.NewCounter("dns_query_fwd_tcp_error_read")
|
||||
metricDNSFwdTCPSuccess = clientmetric.NewCounter("dns_query_fwd_tcp_success")
|
||||
|
||||
metricDNSFwdDoH = clientmetric.NewCounter("dns_query_fwd_doh")
|
||||
metricDNSFwdDoHErrorStatus = clientmetric.NewCounter("dns_query_fwd_doh_error_status")
|
||||
|
||||
Reference in new issue
Block a user