From 53e7f2e59c6c68d55e997839f4336df2df64aaf2 Mon Sep 17 00:00:00 2001 From: Karan Sharma Date: Wed, 16 Dec 2020 18:38:34 +0530 Subject: [PATCH] feat: Multiple resolvers continued --- TODO.md | 8 +-- cmd/cli.go | 31 +++++++++ cmd/hub.go | 21 ++++-- cmd/lookup.go | 2 +- cmd/output.go | 27 ++++++-- cmd/parse.go | 4 -- cmd/resolver.go | 144 ++++++++++++++++++++++++++++++++--------- pkg/resolvers/doh.go | 81 +++++++++++------------ pkg/resolvers/tcp.go | 59 +++++------------ pkg/resolvers/udp.go | 62 +++++------------- pkg/resolvers/utils.go | 28 -------- 11 files changed, 253 insertions(+), 214 deletions(-) diff --git a/TODO.md b/TODO.md index c56810d..3e8d117 100644 --- a/TODO.md +++ b/TODO.md @@ -8,12 +8,13 @@ - [x] Parse output into separate fields - [ ] Test IPv6/IPv4 only options - [x] Add DOH support -- [x] Add DOT support -- [x] Add DNS protocol on TCP mode support. +- [ ] Add DOT support +- [ ] Add DNS protocol on TCP mode support. + - [ ] Change lookup method. - [x] Major records supported - [x] Support multiple resolvers - [x] Take multiple transport options and initialise resolvers accordingly. -- [ ] Add timeout support +- [x] Add timeout support ## CLI Features - [x] `ndots` support @@ -42,7 +43,6 @@ - [ ] Add meaningful comments where required. - [ ] Meaningful error messages - [ ] Better debug logs -- [ ] ## Tests - [ ] Add tests for Command Line Usage. diff --git a/cmd/cli.go b/cmd/cli.go index 602d7d3..5d10203 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "github.com/knadh/koanf" @@ -78,6 +79,36 @@ func main() { hub.Logger.WithError(err).Error("error parsing flags/arguments") hub.Logger.Exit(2) } + + // Load Nameservers + for _, srv := range hub.QueryFlags.Nameservers { + ns, err := initNameserver(srv) + if err != nil { + hub.Logger.WithError(err).Errorf("error parsing nameserver: %s", ns) + hub.Logger.Exit(2) + } + if ns.Address != "" && ns.Type != "" { + fmt.Println("appending", ns.Address, ns.Type) + hub.Nameservers = append(hub.Nameservers, ns) + } + } + + // fallback to system nameserver + if len(hub.Nameservers) == 0 { + ns, err := getDefaultServers() + if err != nil { + hub.Logger.WithError(err).Errorf("error fetching system default nameserver") + hub.Logger.Exit(2) + } + hub.Nameservers = ns + } + + // Load Resolvers + err = hub.initResolver() + if err != nil { + hub.Logger.WithError(err).Error("error loading resolver") + hub.Logger.Exit(2) + } // Start App if len(hub.QueryFlags.QNames) == 0 { f.Usage() diff --git a/cmd/hub.go b/cmd/hub.go index 61fd8ea..a29de11 100644 --- a/cmd/hub.go +++ b/cmd/hub.go @@ -10,12 +10,13 @@ import ( // Hub represents the structure for all app wide functions and structs. type Hub struct { - Logger *logrus.Logger - Version string - QueryFlags QueryFlags - FreeArgs []string - Questions []dns.Question - Resolver []resolvers.Resolver + Logger *logrus.Logger + Version string + QueryFlags QueryFlags + FreeArgs []string + Questions []dns.Question + Resolver []resolvers.Resolver + Nameservers []Nameserver } // QueryFlags is used store the value of CLI flags. @@ -38,6 +39,13 @@ type QueryFlags struct { Timeout time.Duration `koanf:"timeout"` } +// Nameserver represents the type of Nameserver +// along with it's address. +type Nameserver struct { + Address string + Type string +} + // NewHub initializes an instance of Hub which holds app wide configuration. func NewHub(logger *logrus.Logger, buildVersion string) *Hub { // Initialise Resolver @@ -50,6 +58,7 @@ func NewHub(logger *logrus.Logger, buildVersion string) *Hub { QClasses: []string{}, Nameservers: []string{}, }, + Nameservers: []Nameserver{}, } return hub } diff --git a/cmd/lookup.go b/cmd/lookup.go index c1922b6..d394527 100644 --- a/cmd/lookup.go +++ b/cmd/lookup.go @@ -78,7 +78,7 @@ func (hub *Hub) prepareQuestions() error { } func fetchDomainList(d string, ndots int) ([]string, int, error) { - cfg, err := dns.ClientConfigFromFile(resolvers.DefaultResolvConfPath) + cfg, err := dns.ClientConfigFromFile(DefaultResolvConfPath) if err != nil { return nil, 0, err } diff --git a/cmd/output.go b/cmd/output.go index 9f9acbc..d794b34 100644 --- a/cmd/output.go +++ b/cmd/output.go @@ -124,10 +124,6 @@ func (hub *Hub) outputTerminal(out []Output) { // on the output format specified displays the information. func (hub *Hub) Output(responses [][]resolvers.Response) { out := collectOutput(responses) - if len(out) == 0 { - hub.Logger.Info("No records found") - hub.Logger.Exit(0) - } if hub.QueryFlags.ShowJSON { hub.outputJSON(out) } else { @@ -142,6 +138,29 @@ func collectOutput(responses [][]resolvers.Response) []Output { // get the response for _, r := range rslvr { var addr string + if r.Message.Rcode != dns.RcodeSuccess { + for _, ns := range r.Message.Ns { + blah, ok := ns.(*dns.SOA) + fmt.Println(blah, ok) + blah.String() + 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() + rtt := fmt.Sprintf("%dms", r.RTT.Milliseconds()) + o := Output{ + Name: name, + Type: qtype, + TTL: ttl, + Class: qclass, + Address: addr, + TimeTaken: rtt, + Nameserver: r.Nameserver, + } + out = append(out, o) + } + } for _, a := range r.Message.Answer { switch t := a.(type) { case *dns.A: diff --git a/cmd/parse.go b/cmd/parse.go index 8f147e6..bd1d239 100644 --- a/cmd/parse.go +++ b/cmd/parse.go @@ -15,10 +15,6 @@ func (hub *Hub) loadQueryArgs() error { if err != nil { return err } - err = hub.initResolver() - if err != nil { - return err - } hub.loadFallbacks() return nil } diff --git a/cmd/resolver.go b/cmd/resolver.go index 5c263ee..1094df5 100644 --- a/cmd/resolver.go +++ b/cmd/resolver.go @@ -1,49 +1,129 @@ package main import ( + "errors" + "fmt" + "net" + "net/url" + "runtime" "time" + "github.com/miekg/dns" "github.com/mr-karan/doggo/pkg/resolvers" ) +const ( + //DefaultResolvConfPath specifies path to default resolv config file on UNIX. + DefaultResolvConfPath = "/etc/resolv.conf" + // DefaultTLSPort specifies the default port for a DNS server connecting over TCP over TLS + DefaultTLSPort = "853" + // DefaultUDPPort specifies the default port for a DNS server connecting over UDP + DefaultUDPPort = "53" +) + // initResolver checks for various flags and initialises // the correct resolver based on the config. func (hub *Hub) initResolver() error { - // check if DOH flag is set. - if hub.QueryFlags.IsDOH { - hub.Logger.Debug("initiating DOH resolver") - rslvr, err := resolvers.NewDOHResolver(hub.QueryFlags.Nameservers, resolvers.DOHResolverOpts{ - Timeout: hub.QueryFlags.Timeout * time.Second, - }) - if err != nil { - return err + // for each nameserver, initialise the correct resolver + for _, ns := range hub.Nameservers { + if ns.Type == "doh" { + hub.Logger.Debug("initiating DOH resolver") + rslvr, err := resolvers.NewDOHResolver(ns.Address, resolvers.DOHResolverOpts{ + Timeout: hub.QueryFlags.Timeout * time.Second, + }) + if err != nil { + return err + } + hub.Resolver = append(hub.Resolver, rslvr) } - hub.Resolver = append(hub.Resolver, rslvr) - } - if hub.QueryFlags.IsTCP { - hub.Logger.Debug("initiating TCP resolver") - rslvr, err := resolvers.NewTCPResolver(hub.QueryFlags.Nameservers, resolvers.TCPResolverOpts{ - IPv4Only: hub.QueryFlags.UseIPv4, - IPv6Only: hub.QueryFlags.UseIPv6, - Timeout: hub.QueryFlags.Timeout * time.Second, - }) - if err != nil { - return err + if ns.Type == "tcp" { + hub.Logger.Debug("initiating TCP resolver") + rslvr, err := resolvers.NewTCPResolver(ns.Address, resolvers.TCPResolverOpts{ + IPv4Only: hub.QueryFlags.UseIPv4, + IPv6Only: hub.QueryFlags.UseIPv6, + Timeout: hub.QueryFlags.Timeout * time.Second, + }) + if err != nil { + return err + } + hub.Resolver = append(hub.Resolver, rslvr) } - hub.Resolver = append(hub.Resolver, rslvr) - } - // If so far no resolver has been set, then fallback to UDP. - if hub.QueryFlags.IsUDP || len(hub.Resolver) == 0 { - hub.Logger.Debug("initiating UDP resolver") - rslvr, err := resolvers.NewUDPResolver(hub.QueryFlags.Nameservers, resolvers.UDPResolverOpts{ - IPv4Only: hub.QueryFlags.UseIPv4, - IPv6Only: hub.QueryFlags.UseIPv6, - Timeout: hub.QueryFlags.Timeout * time.Second, - }) - if err != nil { - return err + if ns.Type == "udp" { + hub.Logger.Debug("initiating UDP resolver") + rslvr, err := resolvers.NewUDPResolver(ns.Address, resolvers.UDPResolverOpts{ + IPv4Only: hub.QueryFlags.UseIPv4, + IPv6Only: hub.QueryFlags.UseIPv6, + Timeout: hub.QueryFlags.Timeout * time.Second, + }) + if err != nil { + return err + } + hub.Resolver = append(hub.Resolver, rslvr) } - hub.Resolver = append(hub.Resolver, rslvr) } return nil } + +func getDefaultServers() ([]Nameserver, error) { + if runtime.GOOS == "windows" { + // TODO: Add a method for reading system default nameserver in windows. + return nil, errors.New(`unable to read default nameservers in this machine`) + } + // if no nameserver is provided, take it from `resolv.conf` + cfg, err := dns.ClientConfigFromFile(DefaultResolvConfPath) + if err != nil { + return nil, err + } + servers := make([]Nameserver, 0, len(cfg.Servers)) + for _, s := range cfg.Servers { + ip := net.ParseIP(s) + // handle IPv6 + if ip != nil && ip.To4() != nil { + ns := Nameserver{ + Type: "udp", + Address: fmt.Sprintf("%s:%s", s, cfg.Port), + } + servers = append(servers, ns) + } else { + ns := Nameserver{ + Type: "udp", + Address: fmt.Sprintf("[%s]:%s", s, cfg.Port), + } + servers = append(servers, ns) + } + } + return servers, nil +} + +func initNameserver(n string) (Nameserver, error) { + // Instantiate a dumb UDP resolver as a fallback. + ns := Nameserver{ + Type: "udp", + Address: n, + } + u, err := url.Parse(n) + if err != nil { + return ns, err + } + if u.Scheme == "https" { + ns.Address = u.String() + ns.Type = "doh" + } + if u.Scheme == "tcp" { + if i := net.ParseIP(n); i != nil { + // if no port specified in nameserver, append defaults. + n = net.JoinHostPort(n, DefaultTLSPort) + } + ns.Address = u.String() + ns.Type = "tcp" + } + if u.Scheme == "udp" { + ns.Type = "udp" + if u.Port() == "" { + ns.Address = net.JoinHostPort(u.Hostname(), DefaultUDPPort) + } else { + ns.Address = net.JoinHostPort(u.Hostname(), u.Port()) + } + } + return ns, nil +} diff --git a/pkg/resolvers/doh.go b/pkg/resolvers/doh.go index b999ed4..ed7b5f7 100644 --- a/pkg/resolvers/doh.go +++ b/pkg/resolvers/doh.go @@ -2,7 +2,6 @@ package resolvers import ( "bytes" - "errors" "fmt" "io/ioutil" "net/http" @@ -14,34 +13,30 @@ import ( // DOHResolver represents the config options for setting up a DOH based resolver. type DOHResolver struct { - client *http.Client - servers []string + client *http.Client + server string } type DOHResolverOpts struct { Timeout time.Duration } -// NewDOHResolver accepts a list of nameservers and configures a DOH based resolver. -func NewDOHResolver(servers []string, opts DOHResolverOpts) (Resolver, error) { - if len(servers) == 0 { - return nil, errors.New(`no DOH server specified`) +// NewDOHResolver accepts a nameserver address and configures a DOH based resolver. +func NewDOHResolver(server string, opts DOHResolverOpts) (Resolver, error) { + // do basic validation + u, err := url.ParseRequestURI(server) + if err != nil { + return nil, fmt.Errorf("%s is not a valid HTTPS nameserver", server) } - for _, s := range servers { - u, err := url.ParseRequestURI(s) - if err != nil { - return nil, fmt.Errorf("%s is not a valid HTTPS nameserver", s) - } - if u.Scheme != "https" { - return nil, fmt.Errorf("missing https in %s", s) - } + if u.Scheme != "https" { + return nil, fmt.Errorf("missing https in %s", server) } httpClient := &http.Client{ Timeout: opts.Timeout, } return &DOHResolver{ - client: httpClient, - servers: servers, + client: httpClient, + server: server, }, nil } @@ -57,34 +52,32 @@ func (d *DOHResolver) Lookup(questions []dns.Question) ([]Response, error) { if err != nil { return nil, err } - 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 := d.client.Post(srv, "application/dns-message", bytes.NewBuffer(b)) - if err != nil { - return nil, err - } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("error from nameserver %s", resp.Status) - } - rtt := time.Since(now) - // extract the binary response in DNS Message. - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - err = msg.Unpack(body) - if err != nil { - return nil, err - } - rsp := Response{ - Message: msg, - RTT: rtt, - Nameserver: srv, - } - responses = append(responses, rsp) + 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 := d.client.Post(d.server, "application/dns-message", bytes.NewBuffer(b)) + if err != nil { + return nil, err } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error from nameserver %s", resp.Status) + } + rtt := time.Since(now) + // extract the binary response in DNS Message. + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + err = msg.Unpack(body) + if err != nil { + return nil, err + } + rsp := Response{ + Message: msg, + RTT: rtt, + Nameserver: d.server, + } + responses = append(responses, rsp) } return responses, nil } diff --git a/pkg/resolvers/tcp.go b/pkg/resolvers/tcp.go index fc3fcf9..0a321c1 100644 --- a/pkg/resolvers/tcp.go +++ b/pkg/resolvers/tcp.go @@ -1,21 +1,15 @@ package resolvers import ( - "net" "time" "github.com/miekg/dns" ) -const ( - // DefaultTLSPort specifies the default port for a DNS server connecting over TCP over TLS - DefaultTLSPort = "853" -) - // TCPResolver represents the config options for setting up a Resolver. type TCPResolver struct { - client *dns.Client - servers []string + client *dns.Client + server string } // TCPResolverOpts represents the config options for setting up a TCPResolver. @@ -26,31 +20,10 @@ type TCPResolverOpts struct { } // NewTCPResolver accepts a list of nameservers and configures a DNS resolver. -func NewTCPResolver(servers []string, opts TCPResolverOpts) (Resolver, error) { +func NewTCPResolver(server string, opts TCPResolverOpts) (Resolver, error) { client := &dns.Client{ Timeout: opts.Timeout, } - var nameservers []string - - // load list of nameservers to the config - if len(servers) == 0 { - ns, err := getDefaultServers() - if err != nil { - return nil, err - } - nameservers = ns - } else { - // load the list of servers that user specified. - for _, srv := range servers { - if i := net.ParseIP(srv); i != nil { - // if no port specified in nameserver, append defaults. - nameservers = append(nameservers, net.JoinHostPort(srv, DefaultTLSPort)) - } else { - // use the port user specified. - nameservers = append(nameservers, srv) - } - } - } client.Net = "tcp" if opts.IPv4Only { @@ -60,8 +33,8 @@ func NewTCPResolver(servers []string, opts TCPResolverOpts) (Resolver, error) { client.Net = "tcp6" } return &TCPResolver{ - client: client, - servers: nameservers, + client: client, + server: server, }, nil } @@ -75,19 +48,17 @@ func (r *TCPResolver) Lookup(questions []dns.Question) ([]Response, error) { ) for _, msg := range messages { - for _, srv := range r.servers { - in, rtt, err := r.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) + in, rtt, err := r.client.Exchange(&msg, r.server) + if err != nil { + return nil, err } + msg.Answer = in.Answer + rsp := Response{ + Message: msg, + RTT: rtt, + Nameserver: r.server, + } + responses = append(responses, rsp) } return responses, nil } diff --git a/pkg/resolvers/udp.go b/pkg/resolvers/udp.go index a6c04d2..c1240f8 100644 --- a/pkg/resolvers/udp.go +++ b/pkg/resolvers/udp.go @@ -1,23 +1,15 @@ package resolvers import ( - "net" "time" "github.com/miekg/dns" ) -const ( - // DefaultUDPPort specifies the default port for a DNS server connecting over UDP - DefaultUDPPort = "53" - //DefaultResolvConfPath specifies path to default resolv config file on UNIX. - DefaultResolvConfPath = "/etc/resolv.conf" -) - // UDPResolver represents the config options for setting up a Resolver. type UDPResolver struct { - client *dns.Client - servers []string + client *dns.Client + server string } // UDPResolverOpts holds options for setting up a Classic resolver. @@ -28,31 +20,10 @@ type UDPResolverOpts struct { } // NewUDPResolver accepts a list of nameservers and configures a DNS resolver. -func NewUDPResolver(servers []string, opts UDPResolverOpts) (Resolver, error) { +func NewUDPResolver(server string, opts UDPResolverOpts) (Resolver, error) { client := &dns.Client{ Timeout: opts.Timeout, } - var nameservers []string - - // load list of nameservers to the config - if len(servers) == 0 { - ns, err := getDefaultServers() - if err != nil { - return nil, err - } - nameservers = ns - } else { - // load the list of servers that user specified. - for _, srv := range servers { - if i := net.ParseIP(srv); i != nil { - // if no port specified in nameserver, append defaults. - nameservers = append(nameservers, net.JoinHostPort(srv, DefaultUDPPort)) - } else { - // use the port user specified. - nameservers = append(nameservers, srv) - } - } - } client.Net = "udp" if opts.IPv4Only { @@ -62,34 +33,31 @@ func NewUDPResolver(servers []string, opts UDPResolverOpts) (Resolver, error) { client.Net = "udp6" } return &UDPResolver{ - client: client, - servers: nameservers, + client: client, + server: server, }, 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 (c *UDPResolver) Lookup(questions []dns.Question) ([]Response, error) { +func (r *UDPResolver) Lookup(questions []dns.Question) ([]Response, error) { var ( messages = prepareMessages(questions) responses []Response ) for _, msg := range messages { - for _, srv := range c.servers { - in, rtt, err := c.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) + in, rtt, err := r.client.Exchange(&msg, r.server) + if err != nil { + return nil, err } + rsp := Response{ + Message: *in, + RTT: rtt, + Nameserver: r.server, + } + responses = append(responses, rsp) } return responses, nil } diff --git a/pkg/resolvers/utils.go b/pkg/resolvers/utils.go index b087c65..80d1f2e 100644 --- a/pkg/resolvers/utils.go +++ b/pkg/resolvers/utils.go @@ -1,11 +1,6 @@ package resolvers import ( - "errors" - "fmt" - "net" - "runtime" - "github.com/miekg/dns" ) @@ -23,26 +18,3 @@ func prepareMessages(questions []dns.Question) []dns.Msg { } return messages } - -func getDefaultServers() ([]string, error) { - if runtime.GOOS == "windows" { - // TODO: Add a method for reading system default nameserver in windows. - return nil, errors.New(`unable to read default nameservers in this machine`) - } - // if no nameserver is provided, take it from `resolv.conf` - cfg, err := dns.ClientConfigFromFile(DefaultResolvConfPath) - 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)) - } - } - return servers, nil -}