initial commit
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
// Package dnsengine contains the UDP and TCP listeners and the query pipeline
|
||||
// that ties the authoritative index, the policy engine, the cache and the
|
||||
// recursive resolver together.
|
||||
package dnsengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/owen/vibedns/internal/cache"
|
||||
"github.com/owen/vibedns/internal/models"
|
||||
"github.com/owen/vibedns/internal/netutil"
|
||||
"github.com/owen/vibedns/internal/policy"
|
||||
"github.com/owen/vibedns/internal/runtimecfg"
|
||||
"github.com/owen/vibedns/internal/version"
|
||||
)
|
||||
|
||||
// outcome records everything the query log and metrics need about one query.
|
||||
type outcome struct {
|
||||
source string
|
||||
rcode int
|
||||
cacheHit bool
|
||||
blocked bool
|
||||
answerCount int
|
||||
decision policy.Decision
|
||||
upstream string
|
||||
}
|
||||
|
||||
// ServeDNS implements dns.Handler. It is the single entry point for every
|
||||
// query, over both UDP and TCP.
|
||||
func (s *Server) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
|
||||
start := time.Now()
|
||||
protocol := "udp"
|
||||
if _, ok := w.RemoteAddr().(*net.TCPAddr); ok {
|
||||
protocol = "tcp"
|
||||
}
|
||||
client := netutil.AddrFromNetAddr(w.RemoteAddr())
|
||||
snap := s.runtime.Current()
|
||||
|
||||
// Rate limiting happens before any work is done. Exceeding clients are
|
||||
// dropped without a response: replying would let an attacker use us as an
|
||||
// amplifier, which is exactly what the limiter exists to prevent.
|
||||
if !s.limiter.Allow(client) {
|
||||
s.metrics.RateLimited.Add(1)
|
||||
s.metrics.ObserveQuery(qtypeName(req), "DROPPED", models.SourceRateLimited, protocol, time.Since(start))
|
||||
s.logQuery(req, client, protocol, outcome{source: models.SourceRateLimited, rcode: -1}, snap, start)
|
||||
return
|
||||
}
|
||||
|
||||
resp, out := s.respond(req, client, snap)
|
||||
if resp == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.finalise(req, resp, protocol, snap)
|
||||
if err := w.WriteMsg(resp); err != nil {
|
||||
s.log.Debug("could not write DNS response", "client", client.String(), "error", err)
|
||||
}
|
||||
|
||||
out.rcode = resp.Rcode
|
||||
out.answerCount = len(resp.Answer)
|
||||
elapsed := time.Since(start)
|
||||
s.metrics.ObserveQuery(qtypeName(req), dns.RcodeToString[resp.Rcode], out.source, protocol, elapsed)
|
||||
s.logQueryOutcome(req, client, protocol, out, snap, elapsed)
|
||||
}
|
||||
|
||||
// respond produces the reply message and describes how it was produced.
|
||||
func (s *Server) respond(req *dns.Msg, client netip.Addr, snap *runtimecfg.Snapshot) (*dns.Msg, outcome) {
|
||||
|
||||
if req.Opcode != dns.OpcodeQuery {
|
||||
return errorReply(req, dns.RcodeNotImplemented), outcome{source: models.SourceError}
|
||||
}
|
||||
if len(req.Question) != 1 {
|
||||
// Multiple questions in one message are not defined by any RFC and no
|
||||
// real client sends them.
|
||||
return errorReply(req, dns.RcodeFormatError), outcome{source: models.SourceError}
|
||||
}
|
||||
|
||||
q := req.Question[0]
|
||||
qname := strings.ToLower(dns.Fqdn(q.Name))
|
||||
do := requestDO(req)
|
||||
|
||||
if q.Qclass == dns.ClassCHAOS {
|
||||
return s.chaosReply(req, snap), outcome{source: models.SourceLocal}
|
||||
}
|
||||
if q.Qclass != dns.ClassINET {
|
||||
return errorReply(req, dns.RcodeRefused), outcome{source: models.SourceRefused}
|
||||
}
|
||||
|
||||
// 1. Client policy. Blocking runs first so a policy applies even to names
|
||||
// that a local zone would otherwise answer.
|
||||
decision := snap.Policy.Evaluate(client, qname)
|
||||
if decision.Blocked {
|
||||
s.metrics.Blocked.Add(1)
|
||||
return s.blockReply(req, decision), outcome{
|
||||
source: models.SourceBlocked, blocked: true, decision: decision,
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Authoritative zones always win over recursion.
|
||||
if resp := snap.Zones.Answer(req, do); resp != nil {
|
||||
s.metrics.Authoritative.Add(1)
|
||||
return resp, outcome{source: models.SourceAuthoritative, decision: decision}
|
||||
}
|
||||
|
||||
// 3. Recursion, subject to the ACL. Authoritative answers above remain
|
||||
// available to clients that are not allowed to recurse.
|
||||
if !snap.Settings.DNS.Recursion {
|
||||
s.metrics.Refused.Add(1)
|
||||
return errorReply(req, dns.RcodeRefused), outcome{source: models.SourceRefused, decision: decision}
|
||||
}
|
||||
if !snap.ACL.Allowed(client) {
|
||||
s.metrics.Refused.Add(1)
|
||||
s.log.Debug("recursion denied by ACL", "client", client.String(), "name", qname)
|
||||
return errorReply(req, dns.RcodeRefused), outcome{source: models.SourceRefused, decision: decision}
|
||||
}
|
||||
|
||||
// 4. Cache.
|
||||
key := cache.KeyFor(dns.Question{Name: qname, Qtype: q.Qtype, Qclass: q.Qclass}, do)
|
||||
if res := s.cache.Get(key, req); res.Hit {
|
||||
s.metrics.CacheHits.Add(1)
|
||||
src := models.SourceCache
|
||||
if res.Stale {
|
||||
s.metrics.StaleServed.Add(1)
|
||||
src = models.SourceStale
|
||||
}
|
||||
res.Msg.RecursionAvailable = true
|
||||
return res.Msg, outcome{source: src, cacheHit: true, decision: decision}
|
||||
}
|
||||
s.metrics.CacheMisses.Add(1)
|
||||
|
||||
// 5. Forward upstream.
|
||||
ctx, cancel := context.WithTimeout(s.ctx, s.forwardBudget(snap))
|
||||
defer cancel()
|
||||
|
||||
fstart := time.Now()
|
||||
result, err := s.resolver.Resolve(ctx, req)
|
||||
s.metrics.ObserveResolver(time.Since(fstart), err != nil)
|
||||
if err != nil {
|
||||
s.log.Debug("recursive resolution failed", "name", qname, "type", qtypeName(req), "error", err)
|
||||
s.metrics.Errors.Add(1)
|
||||
return errorReply(req, dns.RcodeServerFailure), outcome{source: models.SourceError, decision: decision}
|
||||
}
|
||||
|
||||
s.metrics.Recursive.Add(1)
|
||||
// 6. Cache the answer.
|
||||
s.cache.Put(key, result.Msg)
|
||||
|
||||
resp := result.Msg
|
||||
resp.Id = req.Id
|
||||
resp.Question = req.Question
|
||||
resp.RecursionAvailable = true
|
||||
return resp, outcome{source: models.SourceRecursive, decision: decision, upstream: result.Upstream}
|
||||
}
|
||||
|
||||
// forwardBudget bounds the total time spent forwarding one query, leaving the
|
||||
// client's own timeout some headroom.
|
||||
func (s *Server) forwardBudget(snap *runtimecfg.Snapshot) time.Duration {
|
||||
per := time.Duration(snap.Settings.Resolver.TimeoutMS) * time.Millisecond
|
||||
attempts := snap.Settings.Resolver.Retries + 1
|
||||
total := per * time.Duration(attempts)
|
||||
if total > 15*time.Second {
|
||||
total = 15 * time.Second
|
||||
}
|
||||
if total < per {
|
||||
total = per
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// blockReply builds the response for a policy-blocked query.
|
||||
func (s *Server) blockReply(req *dns.Msg, d policy.Decision) *dns.Msg {
|
||||
q := req.Question[0]
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
m.RecursionAvailable = true
|
||||
m.Authoritative = true
|
||||
|
||||
ttl := uint32(60)
|
||||
if d.Policy != nil && d.Policy.TTL > 0 {
|
||||
ttl = d.Policy.TTL
|
||||
}
|
||||
|
||||
switch d.Action() {
|
||||
case models.BlockRefused:
|
||||
m.Rcode = dns.RcodeRefused
|
||||
return m
|
||||
|
||||
case models.BlockSinkhole:
|
||||
switch q.Qtype {
|
||||
case dns.TypeA:
|
||||
if d.Policy != nil && d.Policy.SinkholeV4.IsValid() {
|
||||
m.Answer = append(m.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: ttl},
|
||||
A: d.Policy.SinkholeV4.AsSlice(),
|
||||
})
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
if d.Policy != nil && d.Policy.SinkholeV6.IsValid() {
|
||||
m.Answer = append(m.Answer, &dns.AAAA{
|
||||
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: ttl},
|
||||
AAAA: d.Policy.SinkholeV6.AsSlice(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(m.Answer) == 0 {
|
||||
// Sinkholing only makes sense for address queries; everything else
|
||||
// gets an empty NOERROR so clients do not retry in a loop.
|
||||
m.Ns = append(m.Ns, syntheticSOA(q.Name, ttl))
|
||||
}
|
||||
return m
|
||||
|
||||
default: // NXDOMAIN
|
||||
m.Rcode = dns.RcodeNameError
|
||||
m.Ns = append(m.Ns, syntheticSOA(q.Name, ttl))
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
// syntheticSOA gives a blocked or synthesised negative answer something for the
|
||||
// client to derive a negative cache TTL from.
|
||||
func syntheticSOA(name string, ttl uint32) *dns.SOA {
|
||||
return &dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: dns.Fqdn(name), Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: ttl,
|
||||
},
|
||||
Ns: "localhost.",
|
||||
Mbox: "hostmaster." + dns.Fqdn(name),
|
||||
Serial: 1,
|
||||
Refresh: 3600,
|
||||
Retry: 600,
|
||||
Expire: 86400,
|
||||
Minttl: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
// chaosReply answers version.bind and hostname.bind in the CHAOS class.
|
||||
func (s *Server) chaosReply(req *dns.Msg, snap *runtimecfg.Snapshot) *dns.Msg {
|
||||
q := req.Question[0]
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
m.Authoritative = true
|
||||
|
||||
if q.Qtype != dns.TypeTXT {
|
||||
m.Rcode = dns.RcodeRefused
|
||||
return m
|
||||
}
|
||||
name := strings.ToLower(q.Name)
|
||||
if !snap.Settings.DNS.ExposeVersion {
|
||||
// Revealing the software version by default only helps an attacker.
|
||||
m.Rcode = dns.RcodeRefused
|
||||
return m
|
||||
}
|
||||
var value string
|
||||
switch name {
|
||||
case "version.bind.", "version.server.":
|
||||
value = version.Name + " " + version.Version
|
||||
case "hostname.bind.", "id.server.":
|
||||
value = s.hostname
|
||||
default:
|
||||
m.Rcode = dns.RcodeRefused
|
||||
return m
|
||||
}
|
||||
m.Answer = append(m.Answer, &dns.TXT{
|
||||
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassCHAOS, Ttl: 0},
|
||||
Txt: []string{value},
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
// finalise applies EDNS to the response and truncates it if it will not fit in
|
||||
// the client's UDP buffer.
|
||||
func (s *Server) finalise(req, resp *dns.Msg, protocol string, snap *runtimecfg.Snapshot) {
|
||||
resp.Compress = true
|
||||
|
||||
advertised := uint16(dns.MinMsgSize) // 512, the pre-EDNS limit
|
||||
if opt := req.IsEdns0(); opt != nil && snap.Settings.DNS.EDNSEnabled {
|
||||
clientSize := opt.UDPSize()
|
||||
if clientSize < dns.MinMsgSize {
|
||||
clientSize = dns.MinMsgSize
|
||||
}
|
||||
ourSize := uint16(snap.Settings.DNS.EDNSUDPSize)
|
||||
if clientSize < ourSize {
|
||||
advertised = clientSize
|
||||
} else {
|
||||
advertised = ourSize
|
||||
}
|
||||
// Echo an OPT record so the client knows we speak EDNS, mirroring the
|
||||
// DO bit it asked for.
|
||||
resp.SetEdns0(ourSize, opt.Do())
|
||||
}
|
||||
|
||||
if protocol == "tcp" {
|
||||
return // TCP carries up to 64 KiB; no truncation needed
|
||||
}
|
||||
|
||||
maxUDP := uint16(snap.Settings.DNS.MaxUDPResponse)
|
||||
if advertised < maxUDP {
|
||||
maxUDP = advertised
|
||||
}
|
||||
if resp.Len() > int(maxUDP) {
|
||||
resp.Truncate(int(maxUDP))
|
||||
if resp.Truncated {
|
||||
s.metrics.TruncatedResp.Add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func errorReply(req *dns.Msg, rcode int) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(req, rcode)
|
||||
m.RecursionAvailable = true
|
||||
return m
|
||||
}
|
||||
|
||||
func requestDO(req *dns.Msg) bool {
|
||||
opt := req.IsEdns0()
|
||||
return opt != nil && opt.Do()
|
||||
}
|
||||
|
||||
func qtypeName(req *dns.Msg) string {
|
||||
if len(req.Question) == 0 {
|
||||
return "NONE"
|
||||
}
|
||||
if s, ok := dns.TypeToString[req.Question[0].Qtype]; ok {
|
||||
return s
|
||||
}
|
||||
return "TYPE" + strconv.Itoa(int(req.Question[0].Qtype))
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package dnsengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/owen/vibedns/internal/cache"
|
||||
"github.com/owen/vibedns/internal/metrics"
|
||||
"github.com/owen/vibedns/internal/models"
|
||||
"github.com/owen/vibedns/internal/querylog"
|
||||
"github.com/owen/vibedns/internal/ratelimit"
|
||||
"github.com/owen/vibedns/internal/resolver"
|
||||
"github.com/owen/vibedns/internal/runtimecfg"
|
||||
)
|
||||
|
||||
// Server runs the UDP and TCP DNS listeners.
|
||||
type Server struct {
|
||||
runtime *runtimecfg.Manager
|
||||
cache *cache.Cache
|
||||
resolver *resolver.Resolver
|
||||
limiter *ratelimit.Limiter
|
||||
metrics *metrics.Metrics
|
||||
qlog *querylog.Logger
|
||||
log *slog.Logger
|
||||
hostname string
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
mu sync.Mutex
|
||||
udp *dns.Server
|
||||
tcp *dns.Server
|
||||
running bool
|
||||
udpAddr string
|
||||
tcpAddr string
|
||||
startErr chan error
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// Options bundles the dependencies the DNS server needs.
|
||||
type Options struct {
|
||||
Runtime *runtimecfg.Manager
|
||||
Cache *cache.Cache
|
||||
Resolver *resolver.Resolver
|
||||
Limiter *ratelimit.Limiter
|
||||
Metrics *metrics.Metrics
|
||||
QueryLog *querylog.Logger
|
||||
Log *slog.Logger
|
||||
}
|
||||
|
||||
// New creates a DNS server. Call Start to bind the listeners.
|
||||
func New(opts Options) *Server {
|
||||
host, err := os.Hostname()
|
||||
if err != nil || host == "" {
|
||||
host = "vibedns"
|
||||
}
|
||||
s := &Server{
|
||||
runtime: opts.Runtime,
|
||||
cache: opts.Cache,
|
||||
resolver: opts.Resolver,
|
||||
limiter: opts.Limiter,
|
||||
metrics: opts.Metrics,
|
||||
qlog: opts.QueryLog,
|
||||
log: opts.Log,
|
||||
hostname: host,
|
||||
startErr: make(chan error, 2),
|
||||
}
|
||||
// Refreshing an ageing cache entry keeps popular names warm without the
|
||||
// client ever waiting on the upstream.
|
||||
s.cache.SetPrefetcher(s.prefetch)
|
||||
return s
|
||||
}
|
||||
|
||||
// Start binds both listeners. It returns once they are accepting queries, or
|
||||
// with an error explaining which address could not be bound.
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.running {
|
||||
return errors.New("DNS server is already running")
|
||||
}
|
||||
|
||||
s.ctx, s.cancel = context.WithCancel(ctx)
|
||||
snap := s.runtime.Current()
|
||||
s.udpAddr = snap.Settings.DNS.UDPListen
|
||||
s.tcpAddr = snap.Settings.DNS.TCPListen
|
||||
|
||||
udpSize := snap.Settings.DNS.EDNSUDPSize
|
||||
if udpSize < dns.MinMsgSize {
|
||||
udpSize = dns.MinMsgSize
|
||||
}
|
||||
idle := time.Duration(snap.Settings.DNS.TCPIdleSeconds) * time.Second
|
||||
|
||||
ready := make(chan struct{}, 2)
|
||||
s.udp = &dns.Server{
|
||||
Addr: s.udpAddr,
|
||||
Net: "udp",
|
||||
Handler: s,
|
||||
UDPSize: udpSize,
|
||||
NotifyStartedFunc: func() { ready <- struct{}{} },
|
||||
}
|
||||
s.tcp = &dns.Server{
|
||||
Addr: s.tcpAddr,
|
||||
Net: "tcp",
|
||||
Handler: s,
|
||||
IdleTimeout: func() time.Duration { return idle },
|
||||
ReadTimeout: idle + 2*time.Second,
|
||||
NotifyStartedFunc: func() { ready <- struct{}{} },
|
||||
}
|
||||
|
||||
errCh := make(chan error, 2)
|
||||
s.wg.Add(2)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
if err := s.udp.ListenAndServe(); err != nil {
|
||||
errCh <- fmt.Errorf("DNS UDP listener on %s: %w", s.udpAddr, describeBindError(err, s.udpAddr))
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
if err := s.tcp.ListenAndServe(); err != nil {
|
||||
errCh <- fmt.Errorf("DNS TCP listener on %s: %w", s.tcpAddr, describeBindError(err, s.tcpAddr))
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for both listeners to report ready, or for one to fail.
|
||||
started := 0
|
||||
deadline := time.After(10 * time.Second)
|
||||
for started < 2 {
|
||||
select {
|
||||
case <-ready:
|
||||
started++
|
||||
case err := <-errCh:
|
||||
s.cancel()
|
||||
return err
|
||||
case <-deadline:
|
||||
s.cancel()
|
||||
return fmt.Errorf("DNS listeners did not become ready within 10 seconds")
|
||||
}
|
||||
}
|
||||
|
||||
s.running = true
|
||||
s.log.Info("DNS listeners started", "udp", s.udpAddr, "tcp", s.tcpAddr)
|
||||
|
||||
// Surface a listener that dies later.
|
||||
go func() {
|
||||
select {
|
||||
case err := <-errCh:
|
||||
s.log.Error("DNS listener stopped unexpectedly", "error", err)
|
||||
case <-s.ctx.Done():
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// describeBindError turns a raw bind failure into something actionable.
|
||||
func describeBindError(err error, addr string) error {
|
||||
msg := err.Error()
|
||||
switch {
|
||||
case strings.Contains(msg, "permission denied"):
|
||||
return fmt.Errorf("%w (binding a port below 1024 needs root, or grant the "+
|
||||
"binary CAP_NET_BIND_SERVICE with: setcap 'cap_net_bind_service=+ep' ./vibedns)", err)
|
||||
case strings.Contains(msg, "address already in use"):
|
||||
return fmt.Errorf("%w (another DNS server is already listening on %s; on many "+
|
||||
"systems that is systemd-resolved, which can be disabled with: "+
|
||||
"systemctl disable --now systemd-resolved)", err, addr)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown stops both listeners.
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
udp, tcp, running := s.udp, s.tcp, s.running
|
||||
s.running = false
|
||||
s.mu.Unlock()
|
||||
|
||||
if !running {
|
||||
return nil
|
||||
}
|
||||
if s.cancel != nil {
|
||||
s.cancel()
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
if udp != nil {
|
||||
if err := udp.ShutdownContext(ctx); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
if tcp != nil {
|
||||
if err := tcp.ShutdownContext(ctx); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
s.wg.Wait()
|
||||
s.log.Info("DNS listeners stopped")
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// Running reports whether the listeners are up.
|
||||
func (s *Server) Running() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.running
|
||||
}
|
||||
|
||||
// ListenAddrs returns the bound addresses for the status page.
|
||||
func (s *Server) ListenAddrs() (udp, tcp string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.udpAddr, s.tcpAddr
|
||||
}
|
||||
|
||||
// prefetch refreshes a cache entry in the background.
|
||||
func (s *Server) prefetch(k cache.Key) {
|
||||
snap := s.runtime.Current()
|
||||
if !snap.Settings.DNS.Recursion {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(s.ctx, s.forwardBudget(snap))
|
||||
defer cancel()
|
||||
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion(k.Name, k.Type)
|
||||
req.Question[0].Qclass = k.Class
|
||||
req.RecursionDesired = true
|
||||
if k.DO {
|
||||
req.SetEdns0(uint16(snap.Settings.DNS.EDNSUDPSize), true)
|
||||
}
|
||||
|
||||
res, err := s.resolver.Resolve(ctx, req)
|
||||
if err != nil {
|
||||
s.log.Debug("cache prefetch failed", "name", k.Name, "error", err)
|
||||
return
|
||||
}
|
||||
s.cache.Put(k, res.Msg)
|
||||
}
|
||||
|
||||
// Resolve performs a query through the full pipeline on behalf of the UI's
|
||||
// "test a lookup" tool, without going over the network.
|
||||
func (s *Server) Resolve(ctx context.Context, name string, qtype uint16, client netip.Addr, do bool) (*dns.Msg, string, error) {
|
||||
snap := s.runtime.Current()
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion(dns.Fqdn(name), qtype)
|
||||
req.RecursionDesired = true
|
||||
if do {
|
||||
req.SetEdns0(uint16(snap.Settings.DNS.EDNSUDPSize), true)
|
||||
}
|
||||
resp, out := s.respond(req, client, snap)
|
||||
if resp == nil {
|
||||
return nil, "", errors.New("no response was produced")
|
||||
}
|
||||
return resp, out.source, nil
|
||||
}
|
||||
|
||||
// logQueryOutcome writes one query log entry.
|
||||
func (s *Server) logQueryOutcome(req *dns.Msg, client netip.Addr, protocol string,
|
||||
out outcome, snap *runtimecfg.Snapshot, elapsed time.Duration) {
|
||||
|
||||
if s.qlog == nil || !s.qlog.Enabled() {
|
||||
return
|
||||
}
|
||||
q := req.Question[0]
|
||||
rcode := "DROPPED"
|
||||
if out.rcode >= 0 {
|
||||
rcode = dns.RcodeToString[out.rcode]
|
||||
}
|
||||
|
||||
e := models.QueryLogEntry{
|
||||
Timestamp: time.Now(),
|
||||
ClientIP: client.String(),
|
||||
QName: strings.ToLower(dns.Fqdn(q.Name)),
|
||||
QType: qtypeName(req),
|
||||
Rcode: rcode,
|
||||
Source: out.source,
|
||||
CacheHit: out.cacheHit,
|
||||
Blocked: out.blocked,
|
||||
Protocol: protocol,
|
||||
DurationUS: elapsed.Microseconds(),
|
||||
AnswerCount: out.answerCount,
|
||||
}
|
||||
d := out.decision
|
||||
e.NetworkID = d.NetworkID()
|
||||
e.NetworkName = d.NetworkName()
|
||||
e.PolicyID = d.PolicyID()
|
||||
e.PolicyName = d.PolicyName()
|
||||
if out.blocked {
|
||||
e.BlacklistID = d.ListRef()
|
||||
e.BlacklistName = d.ListName
|
||||
e.MatchedRule = d.MatchedDomain
|
||||
}
|
||||
s.qlog.Log(e)
|
||||
}
|
||||
|
||||
// logQuery records a query that never produced a response, such as one dropped
|
||||
// by the rate limiter.
|
||||
func (s *Server) logQuery(req *dns.Msg, client netip.Addr, protocol string,
|
||||
out outcome, snap *runtimecfg.Snapshot, start time.Time) {
|
||||
|
||||
if len(req.Question) == 0 {
|
||||
return
|
||||
}
|
||||
s.logQueryOutcome(req, client, protocol, out, snap, time.Since(start))
|
||||
}
|
||||
Reference in New Issue
Block a user