mirror of
https://github.com/wweir/sower.git
synced 2026-10-20 00:15:34 +00:00
- degradeUpstream now takes the caller's snapshot length: a concurrent refresh that grows the live upstream list no longer lets the retry index past the stale snapshot and panic the DNS handler. - Internal reverse lookups take a dedicated forward path that never runs the degrade state machine: the query goes only to the selected internal upstream and surfaces SERVFAIL on failure instead of retrying onto a public fallback, closing the TOCTOU leak in the PTR gate. - Local DNS replies set the RecursionAvailable bit per RFC 1035. - A failed TCP retry for a truncated UDP response keeps the truncated response (with TC set) instead of dropping it for SERVFAIL, so networks that block TCP/53 still get usable answers.
770 lines
21 KiB
Go
770 lines
21 KiB
Go
package router
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
var errNoLocalIPForQuestionType = errors.New("no local IP for question type")
|
|
|
|
type retryableDNSError struct {
|
|
rcode int
|
|
}
|
|
|
|
func (e retryableDNSError) Error() string {
|
|
return fmt.Sprintf("retryable dns response code %s", dns.RcodeToString[e.rcode])
|
|
}
|
|
|
|
func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
|
|
// https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071
|
|
if len(req.Question) != 1 {
|
|
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeFormatError))
|
|
return
|
|
}
|
|
|
|
domain := req.Question[0].Name
|
|
qtype := req.Question[0].Qtype
|
|
if req.Question[0].Qclass != dns.ClassINET {
|
|
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeNotImplemented))
|
|
return
|
|
}
|
|
|
|
// 1. rule_based( block > direct > proxy )
|
|
routeDomains := dnsRouteDomains(domain, qtype)
|
|
if dnsRuleMatch(r.BlockRule, routeDomains) {
|
|
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeNameError))
|
|
return
|
|
}
|
|
|
|
// Internal reverse lookups (RFC1918, CGNAT, link-local, loopback, ULA)
|
|
// have no data on public upstream DNS and would leak internal network
|
|
// layout there. They are answered with NXDOMAIN locally unless the
|
|
// upstream that would serve the query is an internal DNS server, and
|
|
// are then forwarded only to that selected upstream: a degraded mixed
|
|
// pool must never retry an internal reverse lookup onto a public
|
|
// fallback.
|
|
if arpaIP, ok := parseReverseName(domain); ok && isInternalIP(arpaIP) {
|
|
if !r.dnsSelectedUpstreamIsInternal() {
|
|
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeNameError))
|
|
return
|
|
}
|
|
addrs, index, _, err := r.currentUpstreamState(time.Now())
|
|
if err != nil || index < 0 || index >= len(addrs) || !isInternalHost(addrs[index]) {
|
|
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeServerFailure))
|
|
return
|
|
}
|
|
resp, err := r.exchangeWithRetry(req, addrs[index])
|
|
if err != nil {
|
|
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeServerFailure))
|
|
return
|
|
}
|
|
_ = w.WriteMsg(resp)
|
|
return
|
|
}
|
|
|
|
if !dnsRuleMatch(r.DirectRule, routeDomains) && dnsRuleMatch(r.ProxyRule, routeDomains) {
|
|
switch {
|
|
case isAddressQuestion(qtype):
|
|
resp, err := r.dnsProxyReply(domain, w.LocalAddr(), req)
|
|
if err != nil {
|
|
slog.Warn("proxy dns", "error", err, "domain", domain)
|
|
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeServerFailure))
|
|
return
|
|
}
|
|
_ = w.WriteMsg(resp)
|
|
return
|
|
default:
|
|
_ = w.WriteMsg(r.dnsNoData(req))
|
|
return
|
|
}
|
|
}
|
|
|
|
// 2. direct query, do not fallback to proxy to avoid side-effect
|
|
resp, err := r.Exchange(req)
|
|
if err != nil {
|
|
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeServerFailure))
|
|
} else {
|
|
_ = w.WriteMsg(resp)
|
|
}
|
|
}
|
|
|
|
func (r *Router) dnsFail(req *dns.Msg, rcode int) *dns.Msg {
|
|
m := new(dns.Msg)
|
|
m.SetRcode(req, rcode)
|
|
m.RecursionAvailable = true
|
|
if opt := req.IsEdns0(); opt != nil {
|
|
// RFC 6891: a response to an EDNS query should carry an OPT record,
|
|
// otherwise strict clients may treat the reply as truncated or
|
|
// downgrade their retry behavior.
|
|
m.SetEdns0(opt.UDPSize(), false)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (r *Router) dnsNoData(req *dns.Msg) *dns.Msg {
|
|
return dnsReply(req)
|
|
}
|
|
|
|
func (r *Router) dnsProxyReply(domain string, localAddr net.Addr, req *dns.Msg) (*dns.Msg, error) {
|
|
qtype := req.Question[0].Qtype
|
|
reply := dnsReply(req)
|
|
|
|
localIP, err := r.proxyReplyIP(localAddr, qtype)
|
|
if err != nil {
|
|
if errors.Is(err, errNoLocalIPForQuestionType) {
|
|
return reply, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
record, err := proxyReplyRecord(domain, qtype, localIP)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reply.Answer = []dns.RR{record}
|
|
return reply, nil
|
|
}
|
|
|
|
func (r *Router) proxyReplyIP(localAddr net.Addr, qtype uint16) (net.IP, error) {
|
|
if !isAddressQuestion(qtype) {
|
|
return nil, fmt.Errorf("unsupported question type %d", qtype)
|
|
}
|
|
|
|
var localAddrErr error
|
|
if localAddr != nil {
|
|
host, _, err := net.SplitHostPort(localAddr.String())
|
|
if err != nil {
|
|
localAddrErr = fmt.Errorf("split local dns address %q: %w", localAddr.String(), err)
|
|
} else {
|
|
ip := net.ParseIP(strings.Trim(host, "[]"))
|
|
switch {
|
|
case ip == nil:
|
|
localAddrErr = fmt.Errorf("parse local dns IP %q", host)
|
|
case !ip.IsUnspecified() && matchesQuestionTypeIP(ip, qtype):
|
|
return ip, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, serveIP := range r.dns.serveIPs {
|
|
if !serveIP.IsUnspecified() && matchesQuestionTypeIP(serveIP, qtype) {
|
|
return serveIP, nil
|
|
}
|
|
}
|
|
|
|
if localAddrErr != nil {
|
|
return nil, localAddrErr
|
|
}
|
|
return nil, fmt.Errorf("%w %d", errNoLocalIPForQuestionType, qtype)
|
|
}
|
|
|
|
func isAddressQuestion(qtype uint16) bool {
|
|
return qtype == dns.TypeA || qtype == dns.TypeAAAA
|
|
}
|
|
|
|
// parseReverseName resolves an in-addr.arpa or ip6.arpa query name back to
|
|
// the IP it represents. It returns ok=false for incomplete zone names (e.g.
|
|
// "168.192.in-addr.arpa.") and malformed names, which keep the default
|
|
// forwarding behavior.
|
|
func parseReverseName(domain string) (net.IP, bool) {
|
|
name := strings.ToLower(strings.TrimSuffix(domain, "."))
|
|
|
|
switch {
|
|
case strings.HasSuffix(name, ".in-addr.arpa"):
|
|
labels := strings.Split(strings.TrimSuffix(name, ".in-addr.arpa"), ".")
|
|
if len(labels) != net.IPv4len {
|
|
return nil, false
|
|
}
|
|
var octets [net.IPv4len]byte
|
|
for i := range labels {
|
|
v, err := strconv.Atoi(labels[net.IPv4len-1-i]) // reversed: name lists least-significant octet first
|
|
if err != nil || v < 0 || v > 255 {
|
|
return nil, false
|
|
}
|
|
octets[i] = byte(v)
|
|
}
|
|
return net.IPv4(octets[0], octets[1], octets[2], octets[3]).To4(), true
|
|
|
|
case strings.HasSuffix(name, ".ip6.arpa"):
|
|
nibbles := strings.Split(strings.TrimSuffix(name, ".ip6.arpa"), ".")
|
|
if len(nibbles) != net.IPv6len*2 {
|
|
return nil, false
|
|
}
|
|
ip := make(net.IP, net.IPv6len)
|
|
for i, h := range nibbles {
|
|
if len(h) != 1 {
|
|
return nil, false // each label is exactly one hex nibble
|
|
}
|
|
v, err := strconv.ParseUint(h, 16, 8)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
// Nibbles are ordered least-significant byte first, low nibble
|
|
// first within each byte (matching dns.ReverseAddr output).
|
|
n := len(nibbles) - 1 - i
|
|
if n%2 == 0 {
|
|
ip[n/2] |= byte(v) << 4
|
|
} else {
|
|
ip[n/2] |= byte(v)
|
|
}
|
|
}
|
|
return ip, true
|
|
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
// isInternalIP reports whether ip belongs to a private or local-only range:
|
|
// RFC1918 / IPv6 ULA (net.IP.IsPrivate), CGNAT shared address space
|
|
// (RFC 6598), link-local unicast, loopback, and the unspecified address.
|
|
func isInternalIP(ip net.IP) bool {
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {
|
|
return true
|
|
}
|
|
if v4 := ip.To4(); v4 != nil {
|
|
// CGNAT 100.64.0.0/10
|
|
return v4[0] == 100 && v4[1]&0xC0 == 0x40
|
|
}
|
|
return false
|
|
}
|
|
|
|
// dnsSelectedUpstreamIsInternal reports whether the upstream that would
|
|
// serve the next query is an internal address. Internal reverse lookups are
|
|
// only forwarded when they can reach an internal DNS server; judging the
|
|
// currently selected upstream rather than the whole pool keeps a degraded
|
|
// mixed pool from forwarding internal PTR queries to a public fallback.
|
|
func (r *Router) dnsSelectedUpstreamIsInternal() bool {
|
|
r.dns.Lock()
|
|
defer r.dns.Unlock()
|
|
|
|
if len(r.dns.upstreamAddrs) > 0 {
|
|
idx := r.dns.upstreamIndex
|
|
if idx >= 0 && idx < len(r.dns.upstreamAddrs) {
|
|
return isInternalHost(r.dns.upstreamAddrs[idx])
|
|
}
|
|
}
|
|
return isInternalHost(r.dns.upstreamDNS) || isInternalHost(r.dns.fallbackDNS)
|
|
}
|
|
|
|
// isInternalHost reports whether host (with optional port) is an internal IP.
|
|
func isInternalHost(hostport string) bool {
|
|
host := hostport
|
|
if h, _, err := net.SplitHostPort(hostport); err == nil {
|
|
host = h
|
|
}
|
|
return isInternalIP(net.ParseIP(strings.Trim(host, "[]")))
|
|
}
|
|
|
|
func dnsRuleMatch(rule interface{ Match(string) bool }, domains []string) bool {
|
|
for _, domain := range domains {
|
|
if rule.Match(domain) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func dnsRouteDomains(domain string, qtype uint16) []string {
|
|
if !isServiceDiscoveryQuestion(qtype) {
|
|
return []string{domain}
|
|
}
|
|
|
|
base, ok := stripDNSServicePrefix(domain)
|
|
if !ok {
|
|
return []string{domain}
|
|
}
|
|
return []string{domain, base}
|
|
}
|
|
|
|
func isServiceDiscoveryQuestion(qtype uint16) bool {
|
|
switch qtype {
|
|
case dns.TypeSRV, dns.TypeSVCB, dns.TypeHTTPS:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func stripDNSServicePrefix(domain string) (string, bool) {
|
|
trimmed := strings.TrimSuffix(domain, ".")
|
|
labels := strings.Split(trimmed, ".")
|
|
if len(labels) <= 2 || !strings.HasPrefix(labels[0], "_") || !strings.HasPrefix(labels[1], "_") {
|
|
return "", false
|
|
}
|
|
return strings.Join(labels[2:], ".") + ".", true
|
|
}
|
|
|
|
func dnsReply(req *dns.Msg) *dns.Msg {
|
|
reply := new(dns.Msg)
|
|
reply.SetReply(req)
|
|
reply.RecursionAvailable = true
|
|
if opt := req.IsEdns0(); opt != nil {
|
|
reply.SetEdns0(opt.UDPSize(), false)
|
|
}
|
|
return reply
|
|
}
|
|
|
|
func proxyReplyRecord(domain string, qtype uint16, localIP net.IP) (dns.RR, error) {
|
|
switch qtype {
|
|
case dns.TypeA:
|
|
return &dns.A{
|
|
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
|
|
A: localIP,
|
|
}, nil
|
|
case dns.TypeAAAA:
|
|
return &dns.AAAA{
|
|
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 20},
|
|
AAAA: localIP,
|
|
}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported question type %d", qtype)
|
|
}
|
|
}
|
|
|
|
func matchesQuestionTypeIP(ip net.IP, qtype uint16) bool {
|
|
switch qtype {
|
|
case dns.TypeA:
|
|
return ip.To4() != nil
|
|
case dns.TypeAAAA:
|
|
return ip.To4() == nil
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
const (
|
|
dnsTimeout = 2 * time.Second
|
|
dnsUDPSize = 1232
|
|
dnsRetryInterval = 30 * time.Second
|
|
dnsRefreshTTL = 5 * time.Minute
|
|
)
|
|
|
|
func (r *Router) Exchange(req *dns.Msg) (_ *dns.Msg, err error) {
|
|
addrs, index, shouldProbe, gen, err := r.currentUpstreamStateWithGeneration(time.Now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if shouldProbe {
|
|
resp, probeErr := r.exchangeWithRetry(req, addrs[0])
|
|
if probeErr == nil {
|
|
r.promoteUpstream(gen)
|
|
return resp, nil
|
|
}
|
|
r.scheduleRetry(time.Now(), gen)
|
|
}
|
|
|
|
resp, err := r.exchangeWithRetry(req, addrs[index])
|
|
if err != nil {
|
|
nextIndex, switched := r.degradeUpstream(index, gen, len(addrs))
|
|
if switched {
|
|
slog.Info("use upstream dns", "ip", addrs[nextIndex])
|
|
resp, err = r.exchangeWithRetry(req, addrs[nextIndex])
|
|
}
|
|
}
|
|
|
|
if resp != nil && isRetryableDNSResponseErr(err) {
|
|
return resp, nil
|
|
}
|
|
return resp, err
|
|
}
|
|
|
|
func isRetryableDNSResponseErr(err error) bool {
|
|
var retryableErr retryableDNSError
|
|
return errors.As(err, &retryableErr)
|
|
}
|
|
|
|
// currentUpstreamState returns the current upstream selection for callers
|
|
// that do not mutate router state after exchanging with it.
|
|
func (r *Router) currentUpstreamState(now time.Time) ([]string, int, bool, error) {
|
|
addrs, index, shouldProbe, _, err := r.currentUpstreamStateWithGeneration(now)
|
|
return addrs, index, shouldProbe, err
|
|
}
|
|
|
|
// currentUpstreamStateWithGeneration returns the generation belonging to the
|
|
// selected addresses. Exchange passes it back to completion paths so a DNS
|
|
// config change cannot let an old request mutate the new selection state.
|
|
func (r *Router) currentUpstreamStateWithGeneration(now time.Time) ([]string, int, bool, uint64, error) {
|
|
for {
|
|
addrs, index, shouldProbe, needRefresh, gen, err := r.prepareUpstreamState(now)
|
|
if !needRefresh || err != nil {
|
|
return addrs, index, shouldProbe, gen, err
|
|
}
|
|
|
|
upstream, fallback := r.dnsServersSnapshot()
|
|
refreshedAddrs, refreshErr := r.buildUpstreamAddrs(upstream, fallback)
|
|
r.finishUpstreamRefresh(now, refreshedAddrs, refreshErr, gen)
|
|
}
|
|
}
|
|
|
|
func (r *Router) degradeUpstream(index int, gen uint64, snapshotLen int) (int, bool) {
|
|
r.dns.Lock()
|
|
defer r.dns.Unlock()
|
|
|
|
// snapshotLen is the length of the caller's snapshot: the retry indexes
|
|
// into that snapshot, so an index past it would panic. A concurrent
|
|
// refresh can grow the live list past the snapshot; in that case the
|
|
// caller's selection is stale and the retry is skipped.
|
|
if gen != r.dns.generation || len(r.dns.upstreamAddrs) == 0 || r.dns.upstreamIndex != index || index >= len(r.dns.upstreamAddrs)-1 || index+1 >= snapshotLen {
|
|
return r.dns.upstreamIndex, false
|
|
}
|
|
|
|
r.dns.upstreamIndex = index + 1
|
|
r.dns.retryAt = time.Now().Add(dnsRetryInterval)
|
|
r.dns.probeInFlight = false
|
|
return r.dns.upstreamIndex, true
|
|
}
|
|
|
|
func (r *Router) promoteUpstream(gen uint64) {
|
|
r.dns.Lock()
|
|
defer r.dns.Unlock()
|
|
if gen != r.dns.generation {
|
|
return
|
|
}
|
|
|
|
r.dns.upstreamIndex = 0
|
|
r.dns.retryAt = time.Time{}
|
|
r.dns.probeInFlight = false
|
|
}
|
|
|
|
func (r *Router) scheduleRetry(now time.Time, gen uint64) {
|
|
r.dns.Lock()
|
|
defer r.dns.Unlock()
|
|
if gen != r.dns.generation {
|
|
return
|
|
}
|
|
|
|
if r.dns.upstreamIndex > 0 {
|
|
r.dns.retryAt = now.Add(dnsRetryInterval)
|
|
r.dns.probeInFlight = false
|
|
}
|
|
}
|
|
|
|
func (r *Router) prepareUpstreamState(now time.Time) ([]string, int, bool, bool, uint64, error) {
|
|
r.dns.Lock()
|
|
defer r.dns.Unlock()
|
|
|
|
needRefresh := false
|
|
switch {
|
|
case len(r.dns.upstreamAddrs) == 0:
|
|
if r.dns.refreshInFlight {
|
|
if fallbackAddrs, ok := r.fallbackOnlyUpstreamsLocked(); ok {
|
|
return fallbackAddrs, 0, false, false, r.dns.generation, nil
|
|
}
|
|
return nil, 0, false, false, r.dns.generation, r.upstreamUnavailableErrLocked()
|
|
}
|
|
if !r.dns.refreshAt.IsZero() && now.Before(r.dns.refreshAt) {
|
|
if fallbackAddrs, ok := r.fallbackOnlyUpstreamsLocked(); ok {
|
|
return fallbackAddrs, 0, false, false, r.dns.generation, nil
|
|
}
|
|
return nil, 0, false, false, r.dns.generation, r.upstreamUnavailableErrLocked()
|
|
}
|
|
r.dns.refreshInFlight = true
|
|
r.dns.refreshGeneration = r.dns.generation
|
|
needRefresh = true
|
|
case r.shouldRefreshUpstreams(now) && !r.dns.refreshInFlight:
|
|
r.dns.refreshInFlight = true
|
|
r.dns.refreshGeneration = r.dns.generation
|
|
needRefresh = true
|
|
}
|
|
|
|
shouldProbe := !needRefresh &&
|
|
len(r.dns.upstreamAddrs) > 0 &&
|
|
r.dns.upstreamIndex > 0 &&
|
|
!r.dns.retryAt.IsZero() &&
|
|
!now.Before(r.dns.retryAt) &&
|
|
!r.dns.probeInFlight
|
|
if shouldProbe {
|
|
r.dns.probeInFlight = true
|
|
r.dns.retryAt = now.Add(dnsRetryInterval)
|
|
}
|
|
|
|
addrs := append([]string(nil), r.dns.upstreamAddrs...)
|
|
return addrs, r.dns.upstreamIndex, shouldProbe, needRefresh, r.dns.generation, nil
|
|
}
|
|
|
|
// dnsServersSnapshot reads the configured upstream servers under lock so
|
|
// buildUpstreamAddrs can run without racing a concurrent SetDNS.
|
|
func (r *Router) dnsServersSnapshot() (upstream, fallback string) {
|
|
r.dns.Lock()
|
|
defer r.dns.Unlock()
|
|
return r.dns.upstreamDNS, r.dns.fallbackDNS
|
|
}
|
|
|
|
// SetDNS swaps the upstream and fallback DNS servers at runtime. Cached
|
|
// upstream addresses are dropped so the next query rebuilds them from the
|
|
// new configuration; an in-flight refresh started under the old config is
|
|
// discarded via the generation check in finishUpstreamRefresh.
|
|
func (r *Router) SetDNS(upstream, fallback string) {
|
|
r.dns.Lock()
|
|
defer r.dns.Unlock()
|
|
r.dns.upstreamDNS = upstream
|
|
r.dns.fallbackDNS = fallback
|
|
r.dns.generation++
|
|
r.dns.refreshInFlight = false
|
|
r.dns.refreshGeneration = 0
|
|
r.dns.upstreamAddrs = nil
|
|
r.dns.upstreamIndex = 0
|
|
r.dns.refreshAt = time.Time{}
|
|
r.dns.retryAt = time.Time{}
|
|
r.dns.lastRefreshErr = nil
|
|
r.dns.probeInFlight = false
|
|
}
|
|
|
|
func (r *Router) buildUpstreamAddrs(upstream, fallback string) ([]string, error) {
|
|
dnsIPs := []string{upstream}
|
|
if upstream == "" {
|
|
var err error
|
|
dnsIPs, err = r.dns.getDNSServer()
|
|
if err != nil {
|
|
slog.Error("get dns server", "error", err, "dns", dnsIPs)
|
|
}
|
|
}
|
|
|
|
addrs := make([]string, 0, len(dnsIPs)+1)
|
|
seen := make(map[string]struct{}, len(dnsIPs)+1)
|
|
appendAddr := func(ip string) {
|
|
if ip == "" || r.isServeIP(ip) {
|
|
return
|
|
}
|
|
addr := net.JoinHostPort(ip, "53")
|
|
if _, ok := seen[addr]; ok {
|
|
return
|
|
}
|
|
seen[addr] = struct{}{}
|
|
addrs = append(addrs, addr)
|
|
}
|
|
|
|
for _, ip := range dnsIPs {
|
|
appendAddr(ip)
|
|
}
|
|
appendAddr(fallback)
|
|
if len(addrs) == 0 {
|
|
return nil, fmt.Errorf("no available upstream dns")
|
|
}
|
|
|
|
return addrs, nil
|
|
}
|
|
|
|
func (r *Router) shouldRefreshUpstreams(now time.Time) bool {
|
|
return r.dns.upstreamDNS == "" && !r.dns.refreshAt.IsZero() && !now.Before(r.dns.refreshAt)
|
|
}
|
|
|
|
func (r *Router) scheduleRefreshLocked(now time.Time) {
|
|
if r.dns.upstreamDNS == "" {
|
|
r.dns.refreshAt = now.Add(dnsRefreshTTL)
|
|
}
|
|
}
|
|
|
|
func (r *Router) applyUpstreamAddrsLocked(addrs []string, now time.Time) {
|
|
currentAddr := ""
|
|
retryAt := r.dns.retryAt
|
|
if len(r.dns.upstreamAddrs) > 0 && r.dns.upstreamIndex >= 0 && r.dns.upstreamIndex < len(r.dns.upstreamAddrs) {
|
|
currentAddr = r.dns.upstreamAddrs[r.dns.upstreamIndex]
|
|
}
|
|
|
|
r.dns.upstreamAddrs = addrs
|
|
r.dns.upstreamIndex = 0
|
|
if currentAddr != "" {
|
|
for i, addr := range addrs {
|
|
if addr == currentAddr {
|
|
r.dns.upstreamIndex = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if r.dns.upstreamIndex == 0 {
|
|
r.dns.retryAt = time.Time{}
|
|
} else {
|
|
r.dns.retryAt = retryAt
|
|
}
|
|
r.dns.lastRefreshErr = nil
|
|
r.dns.refreshInFlight = false
|
|
r.dns.refreshGeneration = 0
|
|
r.dns.probeInFlight = false
|
|
r.scheduleRefreshLocked(now)
|
|
slog.Info("use upstream dns", "ips", addrs)
|
|
}
|
|
|
|
func (r *Router) finishUpstreamRefresh(now time.Time, addrs []string, err error, gen uint64) {
|
|
r.dns.Lock()
|
|
defer r.dns.Unlock()
|
|
|
|
if gen != r.dns.generation || !r.dns.refreshInFlight || r.dns.refreshGeneration != gen {
|
|
// The refresh was invalidated by SetDNS or superseded by a newer one.
|
|
// It no longer owns refreshInFlight and must not alter current state.
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
r.dns.refreshInFlight = false
|
|
r.dns.refreshGeneration = 0
|
|
r.dns.lastRefreshErr = err
|
|
r.scheduleRefreshLocked(now)
|
|
if len(r.dns.upstreamAddrs) > 0 {
|
|
slog.Warn("refresh upstream dns", "error", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
r.applyUpstreamAddrsLocked(addrs, now)
|
|
}
|
|
|
|
func (r *Router) upstreamUnavailableErrLocked() error {
|
|
if r.dns.lastRefreshErr != nil {
|
|
return r.dns.lastRefreshErr
|
|
}
|
|
return fmt.Errorf("upstream dns unavailable")
|
|
}
|
|
|
|
func (r *Router) fallbackOnlyUpstreamsLocked() ([]string, bool) {
|
|
if r.dns.fallbackDNS == "" || r.isServeIP(r.dns.fallbackDNS) {
|
|
return nil, false
|
|
}
|
|
return []string{net.JoinHostPort(r.dns.fallbackDNS, "53")}, true
|
|
}
|
|
|
|
func (r *Router) exchangeWithRetry(req *dns.Msg, addr string) (*dns.Msg, error) {
|
|
resp, err := r.exchangeUDP(req, addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !resp.Truncated {
|
|
return resp, dnsResponseErr(resp)
|
|
}
|
|
|
|
tcpResp, tcpErr := r.exchangeTCP(req, addr)
|
|
if tcpErr != nil {
|
|
// The TCP retry failed (TCP/53 is blocked on some networks). Keep
|
|
// the truncated UDP response instead of SERVFAIL: it still carries
|
|
// the rcode and the TC bit lets the client retry over TCP itself.
|
|
return resp, nil
|
|
}
|
|
return tcpResp, dnsResponseErr(tcpResp)
|
|
}
|
|
|
|
func dnsResponseErr(resp *dns.Msg) error {
|
|
if resp == nil {
|
|
return fmt.Errorf("empty dns response")
|
|
}
|
|
switch resp.Rcode {
|
|
case dns.RcodeServerFailure, dns.RcodeRefused, dns.RcodeNotImplemented:
|
|
return retryableDNSError{rcode: resp.Rcode}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (r *Router) exchangeUDP(req *dns.Msg, addr string) (*dns.Msg, error) {
|
|
msg := req.Copy()
|
|
if msg.IsEdns0() == nil {
|
|
msg.SetEdns0(dnsUDPSize, false)
|
|
}
|
|
stripECS(msg)
|
|
|
|
client := dns.Client{
|
|
Net: "udp",
|
|
Timeout: dnsTimeout,
|
|
UDPSize: dnsUDPSize,
|
|
}
|
|
resp, _, err := client.Exchange(msg, addr)
|
|
return resp, err
|
|
}
|
|
|
|
func (r *Router) exchangeTCP(req *dns.Msg, addr string) (*dns.Msg, error) {
|
|
msg := req.Copy()
|
|
stripECS(msg)
|
|
client := dns.Client{
|
|
Net: "tcp",
|
|
Timeout: dnsTimeout,
|
|
}
|
|
resp, _, err := client.Exchange(msg, addr)
|
|
return resp, err
|
|
}
|
|
|
|
// ClientIPOf returns the client IP to attribute a DNS query to. Queries
|
|
// forwarded by a local resolver (e.g. dnsmasq --add-subnet) carry the real
|
|
// client address in the EDNS Client Subnet option; when present with a
|
|
// full-length source prefix it takes precedence over the transport source
|
|
// address, which would otherwise be the resolver itself. A truncated prefix
|
|
// (e.g. --add-subnet=24) carries only the subnet base, which is not a
|
|
// usable client identity, so it is ignored. ECS is trusted only from the
|
|
// local forwarding resolver: a client that reaches the listener directly
|
|
// can spoof the option, but that only affects console attribution because
|
|
// ECS is stripped before any query is forwarded upstream.
|
|
func ClientIPOf(req *dns.Msg, remote net.Addr) string {
|
|
if opt := req.IsEdns0(); opt != nil {
|
|
for _, o := range opt.Option {
|
|
if sub, ok := o.(*dns.EDNS0_SUBNET); ok && fullLengthECS(sub) {
|
|
if ip := sub.Address.String(); ip != "" {
|
|
return ip
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if remote == nil {
|
|
return ""
|
|
}
|
|
if host, _, err := net.SplitHostPort(remote.String()); err == nil {
|
|
return host
|
|
}
|
|
return remote.String()
|
|
}
|
|
|
|
// fullLengthECS reports whether the subnet option carries the complete
|
|
// client address: source prefix length 32 for IPv4 (family 1) or 128 for
|
|
// IPv6 (family 2). Truncated prefixes only identify a network, not a client.
|
|
func fullLengthECS(sub *dns.EDNS0_SUBNET) bool {
|
|
switch sub.Family {
|
|
case 1:
|
|
return sub.SourceNetmask == 32
|
|
case 2:
|
|
return sub.SourceNetmask == 128
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// stripECS removes the EDNS Client Subnet option before a query is forwarded
|
|
// upstream. The subnet is added by a forwarding resolver (dnsmasq
|
|
// --add-subnet) and is only meaningful between that resolver and this proxy;
|
|
// forwarding it to public resolvers would leak the real client network.
|
|
func stripECS(msg *dns.Msg) {
|
|
opt := msg.IsEdns0()
|
|
if opt == nil {
|
|
return
|
|
}
|
|
kept := opt.Option[:0]
|
|
for _, o := range opt.Option {
|
|
if _, isECS := o.(*dns.EDNS0_SUBNET); !isECS {
|
|
kept = append(kept, o)
|
|
}
|
|
}
|
|
opt.Option = kept
|
|
}
|
|
|
|
func (r *Router) isServeIP(raw string) bool {
|
|
ip := net.ParseIP(strings.Trim(raw, "[]"))
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
|
|
for _, serveIP := range r.dns.serveIPs {
|
|
if serveIP.Equal(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|