From d9715b1932b113b940fa559c6b09750c03c767a9 Mon Sep 17 00:00:00 2001 From: Karan Sharma Date: Sun, 13 Dec 2020 12:45:45 +0530 Subject: [PATCH] feat: ndots and search list support --- TODO.md | 18 ++++----- cmd/cli.go | 24 ++++++++---- cmd/hub.go | 13 +++---- cmd/lookup.go | 67 ++++++++++++++++++++++++++------ cmd/output.go | 26 +++++++------ cmd/parse.go | 10 +++++ cmd/resolver.go | 2 +- pkg/resolvers/classic.go | 31 +-------------- pkg/resolvers/doh.go | 6 +-- pkg/resolvers/system.go | 84 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 199 insertions(+), 82 deletions(-) create mode 100644 pkg/resolvers/system.go diff --git a/TODO.md b/TODO.md index bd46882..cd0d3f5 100644 --- a/TODO.md +++ b/TODO.md @@ -5,26 +5,26 @@ - [x]] Add methods to initialise the config, set defaults - [x] Add a resolve method - [x] Make it separate from Hub -- [ ] Parse output into separate fields -- [ ] Test UDP6 +- [x] Parse output into separate fields +- [ ] Test IPv6 - [x] Add DOH support - [x] Add DOT support - [x] Add DNS protocol on TCP mode support. ## CLI Features - [ ] `digfile` -- [ ] `ndots` support -- [ ] `search path` support -- [ ] JSON output -- [ ] Colorized output -- [ ] Table output +- [x] `ndots` support +- [x] `search path` support +- [x] JSON output +- [x] Colorized output +- [x] Table output ## CLI Grunt - [x] Query args - [x] Neatly package them to load args in different functions - [x] Upper case is not mandatory for query type/classes -- [ ] Output -- [ ] Add client transport options +- [x] Output +- [x] Add client transport options ## Tests diff --git a/cmd/cli.go b/cmd/cli.go index bbe9037..58673bc 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -8,14 +8,13 @@ import ( var ( // Version and date of the build. This is injected at build-time. - buildVersion = "unknown" - buildDate = "unknown" - verboseEnabled = false + buildVersion = "unknown" + buildDate = "unknown" ) func main() { var ( - logger = initLogger(verboseEnabled) + logger = initLogger() app = cli.NewApp() ) // Initialize hub. @@ -88,6 +87,17 @@ func main() { Usage: "Display how long it took for the response to arrive", Destination: &hub.QueryFlags.DisplayTimeTaken, }, + &cli.BoolFlag{ + Name: "search", + Usage: "Use the search list provided in resolv.conf. It sets the `ndots` parameter as well unless overriden by `ndots` flag.", + Destination: &hub.QueryFlags.UseSearchList, + }, + &cli.IntFlag{ + Name: "ndots", + Usage: "Specify the ndots paramter", + DefaultText: "Default value is that set in `/etc/resolv.conf` or 1 if no `ndots` statement is present.", + Destination: &hub.QueryFlags.Ndots, + }, &cli.BoolFlag{ Name: "json", Aliases: []string{"J"}, @@ -95,13 +105,12 @@ func main() { Destination: &hub.QueryFlags.ShowJSON, }, &cli.BoolFlag{ - Name: "verbose", + Name: "debug", Usage: "Enable verbose logging", - Destination: &verboseEnabled, + Destination: &hub.QueryFlags.Verbose, DefaultText: "false", }, } - app.Before = hub.loadQueryArgs app.Action = func(c *cli.Context) error { if len(hub.QueryFlags.QNames.Value()) == 0 { @@ -110,6 +119,7 @@ func main() { hub.Lookup(c) return nil } + // Run the app. hub.Logger.Debug("Starting doggo...") err := app.Run(os.Args) diff --git a/cmd/hub.go b/cmd/hub.go index e584f5b..2afec56 100644 --- a/cmd/hub.go +++ b/cmd/hub.go @@ -14,6 +14,7 @@ type Hub struct { QueryFlags QueryFlags Questions []dns.Question Resolver resolvers.Resolver + cliContext *cli.Context } // QueryFlags is used store the value of CLI flags. @@ -30,6 +31,9 @@ type QueryFlags struct { UseIPv6 bool DisplayTimeTaken bool ShowJSON bool + Verbose bool + UseSearchList bool + Ndots int } // NewHub initializes an instance of Hub which holds app wide configuration. @@ -49,18 +53,11 @@ func NewHub(logger *logrus.Logger, buildVersion string) *Hub { } // initLogger initializes logger -func initLogger(verbose bool) *logrus.Logger { +func initLogger() *logrus.Logger { logger := logrus.New() logger.SetFormatter(&logrus.TextFormatter{ DisableTimestamp: true, DisableLevelTruncation: true, }) - // Set logger level - if verbose { - logger.SetLevel(logrus.DebugLevel) - logger.Debug("verbose logging enabled") - } else { - logger.SetLevel(logrus.InfoLevel) - } return logger } diff --git a/cmd/lookup.go b/cmd/lookup.go index a38a02d..e8ad87a 100644 --- a/cmd/lookup.go +++ b/cmd/lookup.go @@ -4,12 +4,17 @@ import ( "strings" "github.com/miekg/dns" + "github.com/mr-karan/doggo/pkg/resolvers" + "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) // Lookup sends the DNS queries to the server. func (hub *Hub) Lookup(c *cli.Context) error { - hub.prepareQuestions() + err := hub.prepareQuestions() + if err != nil { + return err + } responses, err := hub.Resolver.Lookup(hub.Questions) if err != nil { return err @@ -21,19 +26,57 @@ func (hub *Hub) Lookup(c *cli.Context) error { // prepareQuestions iterates on list of domain names // and prepare a list of questions // sent to the server with all possible combinations. -func (hub *Hub) prepareQuestions() { - var question dns.Question +func (hub *Hub) prepareQuestions() error { + var ( + question dns.Question + ) for _, name := range hub.QueryFlags.QNames.Value() { - question.Name = dns.Fqdn(name) - // iterate on a list of query types. - for _, q := range hub.QueryFlags.QTypes.Value() { - question.Qtype = dns.StringToType[strings.ToUpper(q)] - // iterate on a list of query classes. - for _, c := range hub.QueryFlags.QClasses.Value() { - question.Qclass = dns.StringToClass[strings.ToUpper(c)] - // append a new question for each possible pair. - hub.Questions = append(hub.Questions, question) + var ( + domains []string + ndots int + ) + + // If `search` flag is specified then fetch the search list + // from `resolv.conf` and set the + if hub.QueryFlags.UseSearchList { + list, n, err := fetchDomainList(name, hub.cliContext.IsSet("ndots"), hub.QueryFlags.Ndots) + if err != nil { + return err + } + domains = list + ndots = n + } else { + domains = []string{dns.Fqdn(name)} + } + for _, d := range domains { + hub.Logger.WithFields(logrus.Fields{ + "domain": d, + "ndots": ndots, + }).Debug("Attmepting to resolve") + question.Name = d + // iterate on a list of query types. + for _, q := range hub.QueryFlags.QTypes.Value() { + question.Qtype = dns.StringToType[strings.ToUpper(q)] + // iterate on a list of query classes. + for _, c := range hub.QueryFlags.QClasses.Value() { + question.Qclass = dns.StringToClass[strings.ToUpper(c)] + // append a new question for each possible pair. + hub.Questions = append(hub.Questions, question) + } } } } + return nil +} + +func fetchDomainList(d string, isNdotsSet bool, ndots int) ([]string, int, error) { + cfg, err := dns.ClientConfigFromFile(resolvers.DefaultResolvConfPath) + if err != nil { + return nil, 0, err + } + // if user specified a custom ndots parameter, override it + if isNdotsSet { + cfg.Ndots = ndots + } + return cfg.NameList(d), cfg.Ndots, nil } diff --git a/cmd/output.go b/cmd/output.go index ae33024..295807d 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -14,12 +14,13 @@ import ( // Output has a list of fields which are produced for the output type Output struct { - Name string `json:"name"` - Type string `json:"type"` - Class string `json:"class"` - TTL string `json:"ttl"` - Address string `json:"address"` - TimeTaken string `json:"rtt"` + Name string `json:"name"` + Type string `json:"type"` + Class string `json:"class"` + TTL string `json:"ttl"` + Address string `json:"address"` + TimeTaken string `json:"rtt"` + Nameserver string `json:"nameserver"` } type Query struct { @@ -113,12 +114,13 @@ func collectOutput(responses []resolvers.Response) []Output { qtype := dns.Type(h.Rrtype).String() rtt := fmt.Sprintf("%dms", r.RTT.Milliseconds()) o := Output{ - Name: name, - Type: qtype, - TTL: ttl, - Class: qclass, - Address: addr, - TimeTaken: rtt, + Name: name, + Type: qtype, + TTL: ttl, + Class: qclass, + Address: addr, + TimeTaken: rtt, + Nameserver: r.Nameserver, } out = append(out, o) } diff --git a/cmd/parse.go b/cmd/parse.go index 640195f..66c4469 100644 --- a/cmd/parse.go +++ b/cmd/parse.go @@ -4,10 +4,20 @@ import ( "strings" "github.com/miekg/dns" + "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) func (hub *Hub) loadQueryArgs(c *cli.Context) error { + // set log level + if c.Bool("debug") { + // Set logger level + hub.Logger.SetLevel(logrus.DebugLevel) + } else { + hub.Logger.SetLevel(logrus.InfoLevel) + } + hub.cliContext = c + err := hub.loadFreeArgs(c) if err != nil { cli.Exit("Error parsing arguments", -1) diff --git a/cmd/resolver.go b/cmd/resolver.go index 4f61e92..668719d 100644 --- a/cmd/resolver.go +++ b/cmd/resolver.go @@ -23,7 +23,7 @@ func (hub *Hub) initResolver(c *cli.Context) error { if runtime.GOOS == "windows" { // TODO: Add a method for reading system default nameserver in windows. } else { - rslvr, err := resolvers.NewResolverFromResolvFile(resolvers.DefaultResolvConfPath) + rslvr, err := resolvers.NewSystemResolver(resolvers.DefaultResolvConfPath) if err != nil { return err } diff --git a/pkg/resolvers/classic.go b/pkg/resolvers/classic.go index 2d817fd..7d95ece 100644 --- a/pkg/resolvers/classic.go +++ b/pkg/resolvers/classic.go @@ -1,7 +1,6 @@ package resolvers import ( - "fmt" "net" "github.com/miekg/dns" @@ -47,6 +46,7 @@ func NewClassicResolver(servers []string, opts ClassicResolverOpts) (Resolver, e nameservers = append(nameservers, srv) } } + client.Net = "udp" if opts.UseIPv4 { client.Net = "udp4" @@ -66,35 +66,6 @@ func NewClassicResolver(servers []string, opts ClassicResolverOpts) (Resolver, e }, nil } -// NewResolverFromResolvFile loads the configuration from resolv config file -// and initialises a DNS resolver. -func NewResolverFromResolvFile(resolvFilePath string) (Resolver, error) { - if resolvFilePath == "" { - resolvFilePath = DefaultResolvConfPath - } - cfg, err := dns.ClientConfigFromFile(resolvFilePath) - if err != nil { - return nil, err - } - - servers := make([]string, 0, len(cfg.Servers)) - for _, s := range cfg.Servers { - ip := net.ParseIP(s) - // handle IPv6 - if ip != nil && ip.To4() != nil { - servers = append(servers, fmt.Sprintf("%s:%s", s, cfg.Port)) - } else { - servers = append(servers, fmt.Sprintf("[%s]:%s", s, cfg.Port)) - } - } - - client := &dns.Client{} - return &ClassicResolver{ - client: client, - servers: servers, - }, nil -} - // Lookup prepare a list of DNS messages to be sent to the server. // It's possible to send multiple question in one message // but some nameservers are not able to diff --git a/pkg/resolvers/doh.go b/pkg/resolvers/doh.go index ab3c92d..997f5fd 100644 --- a/pkg/resolvers/doh.go +++ b/pkg/resolvers/doh.go @@ -26,7 +26,7 @@ func NewDOHResolver(servers []string) (Resolver, error) { }, nil } -func (r *DOHResolver) Lookup(questions []dns.Question) ([]Response, error) { +func (d *DOHResolver) Lookup(questions []dns.Question) ([]Response, error) { var ( messages = prepareMessages(questions) responses []Response @@ -38,10 +38,10 @@ func (r *DOHResolver) Lookup(questions []dns.Question) ([]Response, error) { if err != nil { return nil, err } - for _, srv := range r.servers { + for _, srv := range d.servers { now := time.Now() // Make an HTTP POST request to the DNS server with the DNS message as wire format bytes in the body. - resp, err := r.client.Post(srv, "application/dns-message", bytes.NewBuffer(b)) + resp, err := d.client.Post(srv, "application/dns-message", bytes.NewBuffer(b)) if err != nil { return nil, err } diff --git a/pkg/resolvers/system.go b/pkg/resolvers/system.go new file mode 100644 index 0000000..5c9d35e --- /dev/null +++ b/pkg/resolvers/system.go @@ -0,0 +1,84 @@ +package resolvers + +import ( + "fmt" + "net" + + "github.com/miekg/dns" +) + +// SystemResolver represents the config options based on the +// resolvconf file. +type SystemResolver struct { + client *dns.Client + config *dns.ClientConfig + servers []string +} + +// NewSystemResolver loads the configuration from resolv config file +// and initialises a DNS resolver. +func NewSystemResolver(resolvFilePath string) (Resolver, error) { + if resolvFilePath == "" { + resolvFilePath = DefaultResolvConfPath + } + cfg, err := dns.ClientConfigFromFile(resolvFilePath) + if err != nil { + return nil, err + } + + servers := make([]string, 0, len(cfg.Servers)) + for _, s := range cfg.Servers { + ip := net.ParseIP(s) + // handle IPv6 + if ip != nil && ip.To4() != nil { + servers = append(servers, fmt.Sprintf("%s:%s", s, cfg.Port)) + } else { + servers = append(servers, fmt.Sprintf("[%s]:%s", s, cfg.Port)) + } + } + + client := &dns.Client{} + return &SystemResolver{ + client: client, + servers: servers, + config: cfg, + }, nil +} + +// Lookup prepare a list of DNS messages to be sent to the server. +// It's possible to send multiple question in one message +// but some nameservers are not able to +func (s *SystemResolver) Lookup(questions []dns.Question) ([]Response, error) { + for _, q := range questions { + domains := s.config.NameList(q.Name) + for _, d := range domains { + ques := dns.Question{ + Name: d, + Qtype: q.Qtype, + Qclass: q.Qclass, + } + questions = append(questions, ques) + } + } + var ( + messages = prepareMessages(questions) + responses []Response + ) + + for _, msg := range messages { + for _, srv := range s.servers { + in, rtt, err := s.client.Exchange(&msg, srv) + if err != nil { + return nil, err + } + msg.Answer = in.Answer + rsp := Response{ + Message: msg, + RTT: rtt, + Nameserver: srv, + } + responses = append(responses, rsp) + } + } + return responses, nil +}