feat(everything): did a massive dnssec refactor to improve robustness
This commit is contained in:
+5
-110
@@ -6,7 +6,6 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/afonsofrancof/sdns-proxy/common/dnssec"
|
||||
"github.com/afonsofrancof/sdns-proxy/common/logger"
|
||||
"github.com/afonsofrancof/sdns-proxy/common/protocols/dnscrypt"
|
||||
"github.com/afonsofrancof/sdns-proxy/common/protocols/doh"
|
||||
@@ -18,14 +17,8 @@ import (
|
||||
)
|
||||
|
||||
type DNSClient interface {
|
||||
Query(msg *dns.Msg) (*dns.Msg, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
type ValidatingDNSClient struct {
|
||||
client DNSClient
|
||||
validator *dnssec.Validator
|
||||
options Options
|
||||
Query(msg *dns.Msg) (sent *dns.Msg, resp *dns.Msg, err error)
|
||||
Close()
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
@@ -39,7 +32,6 @@ type Options struct {
|
||||
func New(upstream string, opts Options) (DNSClient, error) {
|
||||
logger.Debug("Creating DNS client for upstream: %s with options: %+v", upstream, opts)
|
||||
|
||||
// Try to parse as URL
|
||||
parsedURL, err := url.Parse(upstream)
|
||||
if err != nil {
|
||||
logger.Error("Invalid upstream format: %v", err)
|
||||
@@ -48,12 +40,10 @@ func New(upstream string, opts Options) (DNSClient, error) {
|
||||
|
||||
var baseClient DNSClient
|
||||
|
||||
// If it has a scheme, treat it as a full URL
|
||||
if parsedURL.Scheme != "" {
|
||||
logger.Debug("Parsing %s as URL with scheme %s", upstream, parsedURL.Scheme)
|
||||
baseClient, err = createClientFromURL(parsedURL, opts)
|
||||
} else {
|
||||
// No scheme - treat as plain DNS address (defaults to UDP)
|
||||
logger.Debug("Parsing %s as plain DNS address", upstream)
|
||||
baseClient, err = createClientFromPlainAddress(upstream, opts)
|
||||
}
|
||||
@@ -63,105 +53,14 @@ func New(upstream string, opts Options) (DNSClient, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If DNSSEC is not enabled, return the base client
|
||||
// Without DNSSEC, the base protocol client is returned directly.
|
||||
if !opts.DNSSEC {
|
||||
logger.Debug("DNSSEC disabled, returning base client")
|
||||
return baseClient, nil
|
||||
}
|
||||
|
||||
logger.Debug("DNSSEC enabled, wrapping with validator (AuthoritativeDNSSEC: %v)", opts.AuthoritativeDNSSEC)
|
||||
|
||||
var validator *dnssec.Validator
|
||||
if opts.AuthoritativeDNSSEC {
|
||||
validator = dnssec.NewValidatorWithAuthoritativeQueries()
|
||||
} else {
|
||||
validator = dnssec.NewValidator(func(qname string, qtype uint16) (*dns.Msg, error) {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(dns.Fqdn(qname), qtype)
|
||||
msg.Id = dns.Id()
|
||||
msg.RecursionDesired = true
|
||||
msg.SetEdns0(4096, true)
|
||||
return baseClient.Query(msg)
|
||||
})
|
||||
}
|
||||
|
||||
return &ValidatingDNSClient{
|
||||
client: baseClient,
|
||||
validator: validator,
|
||||
options: opts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *ValidatingDNSClient) Query(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)",
|
||||
question.Name, dns.TypeToString[question.Qtype], v.options.DNSSEC, v.options.AuthoritativeDNSSEC, v.options.ValidateOnly, v.options.StrictValidation)
|
||||
}
|
||||
|
||||
// Always query the upstream first
|
||||
response, err := v.client.Query(msg)
|
||||
if err != nil {
|
||||
logger.Debug("Base client query failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If DNSSEC validation is disabled, return response as-is
|
||||
if !v.options.DNSSEC {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Extract question details for validation
|
||||
if len(msg.Question) == 0 {
|
||||
logger.Debug("No questions in message, skipping DNSSEC validation")
|
||||
return response, nil
|
||||
}
|
||||
|
||||
question := msg.Question[0]
|
||||
qname := question.Name
|
||||
qtype := question.Qtype
|
||||
|
||||
logger.Debug("Starting DNSSEC validation for %s %s", qname, dns.TypeToString[qtype])
|
||||
|
||||
// Validate the response
|
||||
validationErr := v.validator.ValidateResponse(response, qname, qtype)
|
||||
|
||||
// Handle validation results based on options
|
||||
if validationErr != nil {
|
||||
// Check if it's a "not signed" error
|
||||
if validationErr == dnssec.ErrResourceNotSigned {
|
||||
logger.Debug("Domain %s is not DNSSEC signed", qname)
|
||||
if v.options.ValidateOnly {
|
||||
logger.Error("Domain %s is not DNSSEC signed (ValidateOnly mode)", qname)
|
||||
return nil, fmt.Errorf("domain %s is not DNSSEC signed", qname)
|
||||
}
|
||||
// Return unsigned response if not in validate-only mode
|
||||
logger.Debug("Returning unsigned response for %s", qname)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// For other validation errors
|
||||
logger.Debug("DNSSEC validation failed for %s: %v", qname, validationErr)
|
||||
if v.options.StrictValidation {
|
||||
logger.Error("DNSSEC validation failed for %s (strict mode): %v", qname, validationErr)
|
||||
return nil, fmt.Errorf("DNSSEC validation failed for %s: %w", qname, validationErr)
|
||||
}
|
||||
|
||||
// In non-strict mode, log the error but return the response
|
||||
logger.Debug("DNSSEC validation failed for %s (non-strict mode), returning response anyway: %v", qname, validationErr)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Validation successful
|
||||
logger.Debug("DNSSEC validation successful for %s %s", qname, dns.TypeToString[qtype])
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (v *ValidatingDNSClient) Close() {
|
||||
logger.Debug("Closing ValidatingDNSClient")
|
||||
if v.client != nil {
|
||||
v.client.Close()
|
||||
}
|
||||
// With DNSSEC, wrap the base client with a validating client.
|
||||
return NewValidating(baseClient, opts), nil
|
||||
}
|
||||
|
||||
func createClientFromURL(parsedURL *url.URL, opts Options) (DNSClient, error) {
|
||||
@@ -202,7 +101,6 @@ func createClientFromPlainAddress(address string, opts Options) (DNSClient, erro
|
||||
}
|
||||
|
||||
logger.Debug("Creating client from plain address: host=%s, port=%s", host, port)
|
||||
// Default to UDP for plain addresses
|
||||
return createClient("udp", host, port, "", opts)
|
||||
}
|
||||
|
||||
@@ -290,9 +188,6 @@ func createClient(scheme, host, port, path string, opts Options) (DNSClient, err
|
||||
|
||||
case "sdns":
|
||||
config := dnscrypt.Config{
|
||||
// Janky solution but whatever
|
||||
// Here we rejoin them as the client wants them together
|
||||
// The host is not really a host but whatever
|
||||
ServerStamp: fmt.Sprintf("%v://%v", scheme, host),
|
||||
DNSSEC: opts.DNSSEC,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/afonsofrancof/sdns-proxy/common/dnssec"
|
||||
"github.com/afonsofrancof/sdns-proxy/common/logger"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type ValidatingDNSClient struct {
|
||||
client DNSClient
|
||||
validator *dnssec.Validator
|
||||
options Options
|
||||
}
|
||||
|
||||
func NewValidating(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()
|
||||
} 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,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *ValidatingDNSClient) 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) {
|
||||
if len(msg.Question) > 0 {
|
||||
question := msg.Question[0]
|
||||
logger.Debug("ValidatingDNSClient 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)
|
||||
}
|
||||
|
||||
// DNSSEC policy lives here
|
||||
msg.SetEdns0(4096, true)
|
||||
// Query the base client
|
||||
sent, response, err := v.client.Query(msg)
|
||||
if err != nil {
|
||||
logger.Debug("Base client query failed: %v", err)
|
||||
return sent, nil, err
|
||||
}
|
||||
|
||||
if len(msg.Question) == 0 {
|
||||
logger.Debug("No questions in message, skipping DNSSEC validation")
|
||||
return sent, response, nil
|
||||
}
|
||||
|
||||
question := sent.Question[0]
|
||||
qname := question.Name
|
||||
qtype := question.Qtype
|
||||
|
||||
logger.Debug("Starting DNSSEC validation for %s %s", qname, dns.TypeToString[qtype])
|
||||
|
||||
validationErr := v.validator.ValidateResponse(response, qname, qtype)
|
||||
|
||||
if validationErr != nil {
|
||||
// Unsigned domain: return the response unless validate-only is set.
|
||||
if validationErr == dnssec.ErrResourceNotSigned {
|
||||
logger.Debug("Domain %s is not DNSSEC signed", qname)
|
||||
if v.options.ValidateOnly {
|
||||
logger.Error("Domain %s is not DNSSEC signed (ValidateOnly mode)", qname)
|
||||
return sent, nil, fmt.Errorf("domain %s is not DNSSEC signed", qname)
|
||||
}
|
||||
return sent, response, nil
|
||||
}
|
||||
|
||||
// Any other validation error.
|
||||
logger.Debug("DNSSEC validation failed for %s: %v", qname, validationErr)
|
||||
if v.options.StrictValidation {
|
||||
logger.Error("DNSSEC validation failed for %s (strict mode): %v", qname, validationErr)
|
||||
return sent, nil, fmt.Errorf("DNSSEC validation failed for %s: %w", qname, validationErr)
|
||||
}
|
||||
logger.Debug("DNSSEC validation failed for %s (non-strict mode), returning response anyway: %v", qname, validationErr)
|
||||
return sent, response, nil
|
||||
}
|
||||
|
||||
logger.Debug("DNSSEC validation successful for %s %s", qname, dns.TypeToString[qtype])
|
||||
return sent, response, nil
|
||||
}
|
||||
|
||||
func (v *ValidatingDNSClient) Close() {
|
||||
logger.Debug("Closing ValidatingDNSClient")
|
||||
if v.client != nil {
|
||||
v.client.Close()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user