From c330fa6289bb07024df9949bc05fde968ff841c9 Mon Sep 17 00:00:00 2001 From: afonsofrancof Date: Sun, 12 Jul 2026 00:30:05 +0100 Subject: [PATCH] fix(dnssec): fix DNSSEC by adding TCP fallback to DoUDP --- .gitignore | 4 +-- Makefile | 6 +++- client/client.go | 2 +- client/{validating.go => dnssec_client.go} | 37 ++++++++++++---------- common/dnssec/validator.go | 10 ++++-- common/dnssec/walker_iterative.go | 21 +++++++----- common/protocols/doudp/doudp.go | 34 ++++++++++++++++++++ 7 files changed, 84 insertions(+), 30 deletions(-) rename client/{validating.go => dnssec_client.go} (73%) diff --git a/.gitignore b/.gitignore index 90bf36b..db6809d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,5 @@ **/tls-key-log.txt /results /results.bak -/results_merged -dns_results* +/out +/results-dnssec diff --git a/Makefile b/Makefile index 001c5b3..3e35d10 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,11 @@ LOCAL_IP := 192.168.100.2 .PHONY: all general dnssec clean clean-general clean-dnssec -all: general dnssec +all: get_files general dnssec + +get_files: + rsync -a --progress afonso@afonso-pi:~/sdns-proxy/results/ $(GEN_IN) + rsync -a --progress afonso@afonso-pi:~/sdns-proxy/results/ $(DNS_IN) # ----- general workload: results/ -> out/general/ ----- general: diff --git a/client/client.go b/client/client.go index 985c6cd..4a630e0 100644 --- a/client/client.go +++ b/client/client.go @@ -60,7 +60,7 @@ func New(upstream string, opts Options) (DNSClient, error) { } // With DNSSEC, wrap the base client with a validating client. - return NewValidating(baseClient, opts), nil + return NewDNSSECClient(baseClient, opts), nil } func createClientFromURL(parsedURL *url.URL, opts Options) (DNSClient, error) { diff --git a/client/validating.go b/client/dnssec_client.go similarity index 73% rename from client/validating.go rename to client/dnssec_client.go index 3fc4e1b..33a5d9a 100644 --- a/client/validating.go +++ b/client/dnssec_client.go @@ -2,46 +2,51 @@ package client import ( "fmt" + "net" "github.com/afonsofrancof/sdns-proxy/common/dnssec" "github.com/afonsofrancof/sdns-proxy/common/logger" + "github.com/afonsofrancof/sdns-proxy/common/protocols/doudp" "github.com/miekg/dns" ) -type ValidatingDNSClient struct { +type DNSSECClient struct { client DNSClient - validator *dnssec.Validator options Options + validator *dnssec.Validator } -func NewValidating(base DNSClient, opts Options) DNSClient { +func NewDNSSECClient(base DNSClient, opts Options) DNSClient { logger.Debug("Wrapping base client with DNSSEC validator (AuthoritativeDNSSEC: %v)", opts.AuthoritativeDNSSEC) - var validator *dnssec.Validator if opts.AuthoritativeDNSSEC { - validator = dnssec.NewAuthoritativeValidator() + validator = dnssec.NewAuthoritativeValidator(func(server string) (dnssec.Exchanger, error) { + host, port, err := net.SplitHostPort(server) + if err != nil { + host, port = server, "53" + } + return doudp.New(doudp.Config{ + HostAndPort: net.JoinHostPort(host, port), + DNSSEC: true, + }) + }) } else { validator = dnssec.NewValidator(func(m *dns.Msg) (*dns.Msg, error) { _, r, e := base.Query(m); return r, e }) } - - return &ValidatingDNSClient{ - client: base, - validator: validator, - options: opts, - } + return &DNSSECClient{client: base, validator: validator, options: opts} } -func (v *ValidatingDNSClient) LastValidation() dnssec.ValidationStats { +func (v *DNSSECClient) LastValidation() dnssec.ValidationStats { if v.validator == nil { return dnssec.ValidationStats{} } return v.validator.TakeStats() } -func (v *ValidatingDNSClient) Query(msg *dns.Msg) (*dns.Msg, *dns.Msg, error) { +func (v *DNSSECClient) Query(msg *dns.Msg) (*dns.Msg, *dns.Msg, error) { if len(msg.Question) > 0 { question := msg.Question[0] - logger.Debug("ValidatingDNSClient query: %s %s (DNSSEC: %v, AuthoritativeDNSSEC: %v, ValidateOnly: %v, StrictValidation: %v)", + logger.Debug("DNSSECClient query: %s %s (DNSSEC: %v, AuthoritativeDNSSEC: %v, ValidateOnly: %v, StrictValidation: %v)", question.Name, dns.TypeToString[question.Qtype], v.options.DNSSEC, v.options.AuthoritativeDNSSEC, v.options.ValidateOnly, v.options.StrictValidation) } @@ -90,8 +95,8 @@ func (v *ValidatingDNSClient) Query(msg *dns.Msg) (*dns.Msg, *dns.Msg, error) { return sent, response, nil } -func (v *ValidatingDNSClient) Close() { - logger.Debug("Closing ValidatingDNSClient") +func (v *DNSSECClient) Close() { + logger.Debug("Closing DNSSECClient") if v.client != nil { v.client.Close() } diff --git a/common/dnssec/validator.go b/common/dnssec/validator.go index 2a75185..00f56e9 100644 --- a/common/dnssec/validator.go +++ b/common/dnssec/validator.go @@ -5,6 +5,12 @@ import ( "github.com/miekg/dns" ) +type Exchanger interface { + Query(msg *dns.Msg) (sent, resp *dns.Msg, err error) + Close() +} +type ExchangeFactory func(server string) (Exchanger, error) + type ValidationStats struct { Queries int BytesSent int @@ -28,8 +34,8 @@ func NewValidator(send SendFunc) *Validator { return &Validator{walker: newTrustWalker(send)} } -func NewAuthoritativeValidator() *Validator { - return &Validator{walker: newIterativeWalker()} +func NewAuthoritativeValidator(f ExchangeFactory) *Validator { + return &Validator{walker: newIterativeWalker(f)} } func (v *Validator) ValidateResponse(msg *dns.Msg, qname string, qtype uint16) error { diff --git a/common/dnssec/walker_iterative.go b/common/dnssec/walker_iterative.go index 6fe579d..a944638 100644 --- a/common/dnssec/walker_iterative.go +++ b/common/dnssec/walker_iterative.go @@ -4,7 +4,6 @@ import ( "fmt" "net" "strings" - "time" "github.com/afonsofrancof/sdns-proxy/common/logger" "github.com/miekg/dns" @@ -12,15 +11,15 @@ import ( // iterativeWalker validates top-down. type iterativeWalker struct { - client *dns.Client - st ValidationStats - ipCache map[string]string + newClient ExchangeFactory + st ValidationStats + ipCache map[string]string } -func newIterativeWalker() *iterativeWalker { +func newIterativeWalker(f ExchangeFactory) *iterativeWalker { return &iterativeWalker{ - client: &dns.Client{Timeout: 5 * time.Second}, - ipCache: make(map[string]string), + newClient: f, + ipCache: make(map[string]string), } } @@ -38,7 +37,13 @@ func (w *iterativeWalker) exchange(server, name string, qtype uint16) (*dns.Msg, } w.st.Queries++ - resp, _, err := w.client.Exchange(m, server) + cl, err := w.newClient(server) + if err != nil { + return nil, err + } + defer cl.Close() + + _, resp, err := cl.Query(m) if err == nil && resp != nil { if b, perr := resp.Pack(); perr == nil { w.st.BytesReceived += len(b) diff --git a/common/protocols/doudp/doudp.go b/common/protocols/doudp/doudp.go index f38e626..ea16803 100644 --- a/common/protocols/doudp/doudp.go +++ b/common/protocols/doudp/doudp.go @@ -115,9 +115,43 @@ func (c *Client) Query(msg *dns.Msg) (*dns.Msg, *dns.Msg, error) { return msg, nil, fmt.Errorf("doudp: failed to unpack DNS response: %w", err) } + // RFC 1123 / 7766: a truncated UDP answer must be retried over TCP. + if response.Truncated { + logger.Debug("DoUDP response from %s truncated (TC set), retrying over TCP", c.hostAndPort) + tcpResp, terr := c.queryTCP(msg) + if terr != nil { + return msg, nil, fmt.Errorf("doudp: TCP fallback failed: %w", terr) + } + response = tcpResp + } + if len(response.Answer) > 0 { logger.Debug("DoUDP response from %s: %d answers", c.hostAndPort, len(response.Answer)) } return msg, response, nil } + +func (c *Client) queryTCP(msg *dns.Msg) (*dns.Msg, error) { + conn, err := net.DialTimeout("tcp", c.hostAndPort, c.config.WriteTimeout) + if err != nil { + return nil, fmt.Errorf("dial tcp: %w", err) + } + defer conn.Close() + + co := &dns.Conn{Conn: conn} + if err := co.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout)); err != nil { + return nil, fmt.Errorf("set write deadline: %w", err) + } + if err := co.WriteMsg(msg); err != nil { + return nil, fmt.Errorf("write: %w", err) + } + if err := co.SetReadDeadline(time.Now().Add(c.config.ReadTimeout)); err != nil { + return nil, fmt.Errorf("set read deadline: %w", err) + } + resp, err := co.ReadMsg() + if err != nil { + return nil, fmt.Errorf("read: %w", err) + } + return resp, nil +}