Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for dns trace #71

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 15 additions & 23 deletions cmd/doggo/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func main() {
f.StringSliceP("class", "c", []string{}, "Network class of the DNS record to be queried (IN, CH, HS etc)")
f.StringSliceP("nameserver", "n", []string{}, "Address of the nameserver to send packets to")
f.BoolP("reverse", "x", false, "Performs a DNS Lookup for an IPv4 or IPv6 address. Sets the query type and class to PTR and IN respectively.")
f.Bool("trace", false, "Perform a dns trace operation against the given domain name")

// Resolver Options
f.Int("timeout", 5, "Sets the timeout for a query to T seconds. The default timeout is 5 seconds.")
Expand Down Expand Up @@ -108,12 +109,6 @@ func main() {
app.ReverseLookup()
}

// Load fallbacks.
app.LoadFallbacks()

// Load Questions.
app.PrepareQuestions()

// Load Nameservers.
err = app.LoadNameservers()
if err != nil {
Expand Down Expand Up @@ -147,28 +142,25 @@ func main() {
app.Logger.Exit(0)
}

// Resolve Queries.
var (
responses []resolvers.Response
responseErrors []error
)
for _, q := range app.Questions {
for _, rslv := range app.Resolvers {
resp, err := rslv.Lookup(q)
if err != nil {
app.Logger.WithError(err).Error("error looking up DNS records")
responseErrors = append(responseErrors, err)
var responses []resolvers.Response
if k.Bool("trace") {
if responses, err = app.Trace(); err != nil {
app.Logger.WithError(err).Error("failed to trace")

if len(responses) == 0 {
app.Logger.Exit(9) // exit with an error code if there are no responses
}
responses = append(responses, resp)
}
}
} else {
if responses, err = app.Resolve(); err != nil {
app.Logger.WithError(err).Error("error looking up DNS records")

if len(responses) == 0 && len(responseErrors) > 0 {
app.Logger.Exit(9)
if len(responses) == 0 {
app.Logger.Exit(9) // exit with an error code if there are no responses
}
}
}

app.Output(responses)

// Quitting.
app.Logger.Exit(0)
}
1 change: 1 addition & 0 deletions cmd/doggo/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var appHelpTextTemplate = `{{ "NAME" | color "" "heading" }}:
{{"-n, --nameserver=ADDR" | color "yellow" ""}} Address of a specific nameserver to send queries to ({{"9.9.9.9, 8.8.8.8" | color "cyan" ""}} etc).
{{"-c, --class=CLASS" | color "yellow" ""}} Network class of the DNS record ({{"IN, CH, HS" | color "cyan" ""}} etc).
{{"-x, --reverse" | color "yellow" ""}} Performs a DNS Lookup for an IPv4 or IPv6 address. Sets the query type and class to PTR and IN respectively.
{{"--trace" | color "yellow" ""}} Perform a dns trace operation against the given domain name

{{ "Resolver Options" | color "" "heading" }}:
{{"--strategy=STRATEGY" | color "yellow" ""}} Specify strategy to query nameserver listed in etc/resolv.conf. ({{"all, random, first" | color "cyan" ""}}).
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ require (
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/google/pprof v0.0.0-20221219190121-3cb0bae90811 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.4 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
Expand All @@ -34,6 +36,7 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.6.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
Expand All @@ -126,6 +127,8 @@ github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
Expand Down Expand Up @@ -237,6 +240,7 @@ github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
8 changes: 6 additions & 2 deletions internal/app/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,18 @@ func (app *App) outputTerminal(rsp []resolvers.Response) {
table.Append(output)
}
for _, auth := range r.Authorities {
var typOut string
var typOut, addr string
switch typ := auth.Type; typ {
case "SOA":
typOut = red(auth.Type)
addr = auth.MName
case "NS":
typOut = cyan(auth.Type)
addr = auth.Address
default:
typOut = blue(auth.Type)
}
output := []string{green(auth.Name), typOut, auth.Class, auth.TTL, auth.MName, auth.Nameserver}
output := []string{green(auth.Name), typOut, auth.Class, auth.TTL, addr, auth.Nameserver}
// Print how long it took
if app.QueryFlags.DisplayTimeTaken {
output = append(output, auth.RTT)
Expand Down
100 changes: 100 additions & 0 deletions internal/app/resolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package app

import (
"github.com/hashicorp/go-multierror"
"github.com/miekg/dns"
"github.com/mr-karan/doggo/pkg/resolvers"
"github.com/pkg/errors"
"net"
"time"
)

// Resolve resolves the given DNS queries using the configured resolvers
func (app *App) Resolve() (_ []resolvers.Response, err error) {
app.LoadFallbacks()
app.PrepareQuestions()

var responses []resolvers.Response

for _, q := range app.Questions {
for _, rslv := range app.Resolvers {
if resp, lookupError := rslv.Lookup(q); lookupError != nil {
err = multierror.Append(err, lookupError)
} else {
responses = append(responses, resp)
}
}
}

return responses, err
}

// Trace traces the resolution path for a dns query.
//
// It resolves the query from the root nameservers downwards and return the results from each query step.
// It will only use the default or explicitly specified nameserver for the initial discovery of the root nameservers.
// Thereafter, it makes its own queries following the delegation referrals it receives.
//
// adapted from: https://superuser.com/a/715656
func (app *App) Trace() (result []resolvers.Response, err error) {
// pick the first resolver configured
var rslv = app.Resolvers[0]
app.Logger.Debugf("using %v resolver", rslv)

var ques = dns.Question{Name: app.QueryFlags.QNames[0], Qtype: dns.TypeA, Qclass: dns.ClassINET}
var ans resolvers.Response

// first, we ask the configured nameserver for NS record for "." (root)
if ans, err = rslv.Lookup(dns.Question{Name: ".", Qtype: dns.TypeNS, Qclass: dns.ClassINET}); err != nil {
return nil, errors.Wrapf(err, "failed to lookup NS record for root")
}
result = append(result, ans)

var nameservers []string
for _, auth := range ans.Answers {
if auth.Type == dns.TypeToString[dns.TypeNS] {
nameservers = append(nameservers, auth.Address)
}
}

var classicResolverOpts = resolvers.ClassicResolverOpts{UseTLS: false, UseTCP: false}
var resolverOpts = resolvers.Options{
Nameservers: app.Nameservers,
UseIPv4: app.QueryFlags.UseIPv4,
UseIPv6: app.QueryFlags.UseIPv6,
SearchList: app.ResolverOpts.SearchList,
Ndots: app.ResolverOpts.Ndots,
Timeout: app.QueryFlags.Timeout * time.Second,
Logger: app.Logger,
Strategy: app.QueryFlags.Strategy,
InsecureSkipVerify: app.QueryFlags.InsecureSkipVerify,
TLSHostname: app.QueryFlags.TLSHostname,
}

// TODO: randomize picking of nameservers (or query all nameservers?)
var nameserver = net.JoinHostPort(nameservers[0], "53")
for {
rslv, _ = resolvers.NewClassicResolver(nameserver, classicResolverOpts, resolverOpts) // safe to suppress here
if ans, err = rslv.Lookup(ques); err != nil {
return nil, errors.Wrapf(err, "failed to trace %q", ques.Name)
}
result = append(result, ans)

var prev = nameserver
// pick the first authority response of type NS
for _, auth := range ans.Authorities {
if auth.Type == dns.TypeToString[dns.TypeNS] {
nameserver = net.JoinHostPort(auth.Address, "53")
break
}
}

// in case there is no change in the nameserver, does that mean we have reached end of delegation chain?
// currently, we are assuming that and using it as a terminating condition
if len(ans.Answers) > 0 || nameserver == prev {
break
}
}

return result, nil
}
3 changes: 2 additions & 1 deletion pkg/resolvers/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ type Authority struct {
Type string `json:"type"`
Class string `json:"class"`
TTL string `json:"ttl"`
MName string `json:"mname"`
Address string `json:"address,omitempty"`
MName string `json:"mname,omitempty"`
Status string `json:"status"`
RTT string `json:"rtt"`
Nameserver string `json:"nameserver"`
Expand Down
50 changes: 28 additions & 22 deletions pkg/resolvers/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,37 +75,43 @@ func parseMessage(msg *dns.Msg, rtt time.Duration, server string) Response {
timeTaken := fmt.Sprintf("%dms", rtt.Milliseconds())

// Parse Authorities section.
for _, ns := range msg.Ns {
// check for SOA record
soa, ok := ns.(*dns.SOA)
if !ok {
// Currently we only check for SOA in Authority.
// If it's not SOA, skip this message.
continue
}
mname := soa.Ns + " " + soa.Mbox +
" " + strconv.FormatInt(int64(soa.Serial), 10) +
" " + strconv.FormatInt(int64(soa.Refresh), 10) +
" " + strconv.FormatInt(int64(soa.Retry), 10) +
" " + strconv.FormatInt(int64(soa.Expire), 10) +
" " + strconv.FormatInt(int64(soa.Minttl), 10)
h := ns.Header()
name := h.Name
qclass := dns.Class(h.Class).String()
ttl := strconv.FormatInt(int64(h.Ttl), 10) + "s"
qtype := dns.Type(h.Rrtype).String()
auth := Authority{
for _, auth := range msg.Ns {
h := auth.Header()

var (
name = h.Name
qclass = dns.Class(h.Class).String()
ttl = strconv.FormatInt(int64(h.Ttl), 10) + "s"
qtype = dns.Type(h.Rrtype).String()
)

var authority = Authority{
Name: name,
Type: qtype,
TTL: ttl,
Class: qclass,
MName: mname,
Nameserver: server,
RTT: timeTaken,
Status: dns.RcodeToString[msg.Rcode],
}
resp.Authorities = append(resp.Authorities, auth)

// check for SOA record
if soa, ok := auth.(*dns.SOA); ok {
authority.MName = soa.Ns + " " + soa.Mbox +
" " + strconv.FormatInt(int64(soa.Serial), 10) +
" " + strconv.FormatInt(int64(soa.Refresh), 10) +
" " + strconv.FormatInt(int64(soa.Retry), 10) +
" " + strconv.FormatInt(int64(soa.Expire), 10) +
" " + strconv.FormatInt(int64(soa.Minttl), 10)
}

if ns, ok := auth.(*dns.NS); ok {
authority.Address = ns.Ns
}

resp.Authorities = append(resp.Authorities, authority)
}

// Parse Answers section.
for _, a := range msg.Answer {
var (
Expand Down