diff --git a/go.mod b/go.mod index ebf21a7..85be917 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/miekg/dns v1.1.4 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -55,6 +56,7 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/peterzen/goresolver v1.0.2 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.56.0 // indirect github.com/redis/go-redis/v9 v9.16.0 // indirect diff --git a/go.sum b/go.sum index cd951e8..825604f 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0= +github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -154,6 +156,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/peterzen/goresolver v1.0.2 h1:UxRxk835Onz7Go4oPUsOptSmBlIvN/yJ2kv3Srr3hw4= +github.com/peterzen/goresolver v1.0.2/go.mod h1:LrWRiOeCYApgvR2OhpipNOeaE1yGfI+QQjpF0riJC8M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= @@ -198,6 +202,7 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -207,6 +212,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -216,12 +222,14 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222171317-cd391775e71e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/analyzer/dns_resolver.go b/pkg/analyzer/dns_resolver.go index f60484f..0261f1d 100644 --- a/pkg/analyzer/dns_resolver.go +++ b/pkg/analyzer/dns_resolver.go @@ -23,7 +23,12 @@ package analyzer import ( "context" + "fmt" "net" + "strings" + + "github.com/miekg/dns" + "github.com/peterzen/goresolver" ) // DNSResolver defines the interface for DNS resolution operations. @@ -45,36 +50,147 @@ type DNSResolver interface { LookupHost(ctx context.Context, host string) ([]string, error) } -// StandardDNSResolver is the default DNS resolver implementation that uses net.Resolver. +// StandardDNSResolver is the default DNS resolver implementation that uses goresolver with DNSSEC validation. type StandardDNSResolver struct { - resolver *net.Resolver + resolver *goresolver.Resolver } -// NewStandardDNSResolver creates a new StandardDNSResolver with default settings. +// NewStandardDNSResolver creates a new StandardDNSResolver with DNSSEC validation support. func NewStandardDNSResolver() DNSResolver { + // Pass /etc/resolv.conf to load default DNS configuration + resolver, err := goresolver.NewResolver("/etc/resolv.conf") + if err != nil { + panic(fmt.Sprintf("failed to initialize goresolver: %v", err)) + } + return &StandardDNSResolver{ - resolver: &net.Resolver{ - PreferGo: true, - }, + resolver: resolver, } } -// LookupMX implements DNSResolver.LookupMX using net.Resolver. +// LookupMX implements DNSResolver.LookupMX using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupMX(ctx context.Context, name string) ([]*net.MX, error) { - return r.resolver.LookupMX(ctx, name) + // Ensure the name ends with a dot for DNS queries + queryName := name + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeMX) + if err != nil { + return nil, err + } + + mxRecords := make([]*net.MX, 0, len(rrs)) + for _, rr := range rrs { + if mx, ok := rr.(*dns.MX); ok { + mxRecords = append(mxRecords, &net.MX{ + Host: strings.TrimSuffix(mx.Mx, "."), + Pref: mx.Preference, + }) + } + } + + if len(mxRecords) == 0 { + return nil, fmt.Errorf("no MX records found for %s", name) + } + + return mxRecords, nil } -// LookupTXT implements DNSResolver.LookupTXT using net.Resolver. +// LookupTXT implements DNSResolver.LookupTXT using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupTXT(ctx context.Context, name string) ([]string, error) { - return r.resolver.LookupTXT(ctx, name) + // Ensure the name ends with a dot for DNS queries + queryName := name + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeTXT) + if err != nil { + return nil, err + } + + txtRecords := make([]string, 0, len(rrs)) + for _, rr := range rrs { + if txt, ok := rr.(*dns.TXT); ok { + // Join all TXT strings (a single TXT record can have multiple strings) + txtRecords = append(txtRecords, strings.Join(txt.Txt, "")) + } + } + + if len(txtRecords) == 0 { + return nil, fmt.Errorf("no TXT records found for %s", name) + } + + return txtRecords, nil } -// LookupAddr implements DNSResolver.LookupAddr using net.Resolver. +// LookupAddr implements DNSResolver.LookupAddr using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupAddr(ctx context.Context, addr string) ([]string, error) { - return r.resolver.LookupAddr(ctx, addr) + // Convert IP address to reverse DNS name (e.g., 1.0.0.127.in-addr.arpa.) + arpa, err := dns.ReverseAddr(addr) + if err != nil { + return nil, fmt.Errorf("invalid IP address: %w", err) + } + + rrs, err := r.resolver.StrictNSQuery(arpa, dns.TypePTR) + if err != nil { + return nil, err + } + + ptrRecords := make([]string, 0, len(rrs)) + for _, rr := range rrs { + if ptr, ok := rr.(*dns.PTR); ok { + ptrRecords = append(ptrRecords, strings.TrimSuffix(ptr.Ptr, ".")) + } + } + + if len(ptrRecords) == 0 { + return nil, fmt.Errorf("no PTR records found for %s", addr) + } + + return ptrRecords, nil } -// LookupHost implements DNSResolver.LookupHost using net.Resolver. +// LookupHost implements DNSResolver.LookupHost using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupHost(ctx context.Context, host string) ([]string, error) { - return r.resolver.LookupHost(ctx, host) + // Ensure the host ends with a dot for DNS queries + queryName := host + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + var allAddrs []string + + // Query A records (IPv4) + rrsA, errA := r.resolver.StrictNSQuery(queryName, dns.TypeA) + if errA == nil { + for _, rr := range rrsA { + if a, ok := rr.(*dns.A); ok { + allAddrs = append(allAddrs, a.A.String()) + } + } + } + + // Query AAAA records (IPv6) + rrsAAAA, errAAAA := r.resolver.StrictNSQuery(queryName, dns.TypeAAAA) + if errAAAA == nil { + for _, rr := range rrsAAAA { + if aaaa, ok := rr.(*dns.AAAA); ok { + allAddrs = append(allAddrs, aaaa.AAAA.String()) + } + } + } + + // Return error only if both queries failed + if errA != nil && errAAAA != nil { + return nil, fmt.Errorf("failed to resolve host: IPv4 error: %v, IPv6 error: %v", errA, errAAAA) + } + + if len(allAddrs) == 0 { + return nil, fmt.Errorf("no A or AAAA records found for %s", host) + } + + return allAddrs, nil }