Files
wweir 30e80ab510 fix(router): bound degrade retry to snapshot, gate internal PTR on selected upstream
- 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.
2026-08-12 17:08:32 +08:00

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
}