feat: ndots and search list support
parent
7df12b2229
commit
d9715b1932
18
TODO.md
18
TODO.md
|
@ -5,26 +5,26 @@
|
||||||
- [x]] Add methods to initialise the config, set defaults
|
- [x]] Add methods to initialise the config, set defaults
|
||||||
- [x] Add a resolve method
|
- [x] Add a resolve method
|
||||||
- [x] Make it separate from Hub
|
- [x] Make it separate from Hub
|
||||||
- [ ] Parse output into separate fields
|
- [x] Parse output into separate fields
|
||||||
- [ ] Test UDP6
|
- [ ] Test IPv6
|
||||||
- [x] Add DOH support
|
- [x] Add DOH support
|
||||||
- [x] Add DOT support
|
- [x] Add DOT support
|
||||||
- [x] Add DNS protocol on TCP mode support.
|
- [x] Add DNS protocol on TCP mode support.
|
||||||
|
|
||||||
## CLI Features
|
## CLI Features
|
||||||
- [ ] `digfile`
|
- [ ] `digfile`
|
||||||
- [ ] `ndots` support
|
- [x] `ndots` support
|
||||||
- [ ] `search path` support
|
- [x] `search path` support
|
||||||
- [ ] JSON output
|
- [x] JSON output
|
||||||
- [ ] Colorized output
|
- [x] Colorized output
|
||||||
- [ ] Table output
|
- [x] Table output
|
||||||
|
|
||||||
## CLI Grunt
|
## CLI Grunt
|
||||||
- [x] Query args
|
- [x] Query args
|
||||||
- [x] Neatly package them to load args in different functions
|
- [x] Neatly package them to load args in different functions
|
||||||
- [x] Upper case is not mandatory for query type/classes
|
- [x] Upper case is not mandatory for query type/classes
|
||||||
- [ ] Output
|
- [x] Output
|
||||||
- [ ] Add client transport options
|
- [x] Add client transport options
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|
24
cmd/cli.go
24
cmd/cli.go
|
@ -8,14 +8,13 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Version and date of the build. This is injected at build-time.
|
// Version and date of the build. This is injected at build-time.
|
||||||
buildVersion = "unknown"
|
buildVersion = "unknown"
|
||||||
buildDate = "unknown"
|
buildDate = "unknown"
|
||||||
verboseEnabled = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
logger = initLogger(verboseEnabled)
|
logger = initLogger()
|
||||||
app = cli.NewApp()
|
app = cli.NewApp()
|
||||||
)
|
)
|
||||||
// Initialize hub.
|
// Initialize hub.
|
||||||
|
@ -88,6 +87,17 @@ func main() {
|
||||||
Usage: "Display how long it took for the response to arrive",
|
Usage: "Display how long it took for the response to arrive",
|
||||||
Destination: &hub.QueryFlags.DisplayTimeTaken,
|
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{
|
&cli.BoolFlag{
|
||||||
Name: "json",
|
Name: "json",
|
||||||
Aliases: []string{"J"},
|
Aliases: []string{"J"},
|
||||||
|
@ -95,13 +105,12 @@ func main() {
|
||||||
Destination: &hub.QueryFlags.ShowJSON,
|
Destination: &hub.QueryFlags.ShowJSON,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "verbose",
|
Name: "debug",
|
||||||
Usage: "Enable verbose logging",
|
Usage: "Enable verbose logging",
|
||||||
Destination: &verboseEnabled,
|
Destination: &hub.QueryFlags.Verbose,
|
||||||
DefaultText: "false",
|
DefaultText: "false",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Before = hub.loadQueryArgs
|
app.Before = hub.loadQueryArgs
|
||||||
app.Action = func(c *cli.Context) error {
|
app.Action = func(c *cli.Context) error {
|
||||||
if len(hub.QueryFlags.QNames.Value()) == 0 {
|
if len(hub.QueryFlags.QNames.Value()) == 0 {
|
||||||
|
@ -110,6 +119,7 @@ func main() {
|
||||||
hub.Lookup(c)
|
hub.Lookup(c)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the app.
|
// Run the app.
|
||||||
hub.Logger.Debug("Starting doggo...")
|
hub.Logger.Debug("Starting doggo...")
|
||||||
err := app.Run(os.Args)
|
err := app.Run(os.Args)
|
||||||
|
|
13
cmd/hub.go
13
cmd/hub.go
|
@ -14,6 +14,7 @@ type Hub struct {
|
||||||
QueryFlags QueryFlags
|
QueryFlags QueryFlags
|
||||||
Questions []dns.Question
|
Questions []dns.Question
|
||||||
Resolver resolvers.Resolver
|
Resolver resolvers.Resolver
|
||||||
|
cliContext *cli.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryFlags is used store the value of CLI flags.
|
// QueryFlags is used store the value of CLI flags.
|
||||||
|
@ -30,6 +31,9 @@ type QueryFlags struct {
|
||||||
UseIPv6 bool
|
UseIPv6 bool
|
||||||
DisplayTimeTaken bool
|
DisplayTimeTaken bool
|
||||||
ShowJSON bool
|
ShowJSON bool
|
||||||
|
Verbose bool
|
||||||
|
UseSearchList bool
|
||||||
|
Ndots int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHub initializes an instance of Hub which holds app wide configuration.
|
// 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
|
// initLogger initializes logger
|
||||||
func initLogger(verbose bool) *logrus.Logger {
|
func initLogger() *logrus.Logger {
|
||||||
logger := logrus.New()
|
logger := logrus.New()
|
||||||
logger.SetFormatter(&logrus.TextFormatter{
|
logger.SetFormatter(&logrus.TextFormatter{
|
||||||
DisableTimestamp: true,
|
DisableTimestamp: true,
|
||||||
DisableLevelTruncation: true,
|
DisableLevelTruncation: true,
|
||||||
})
|
})
|
||||||
// Set logger level
|
|
||||||
if verbose {
|
|
||||||
logger.SetLevel(logrus.DebugLevel)
|
|
||||||
logger.Debug("verbose logging enabled")
|
|
||||||
} else {
|
|
||||||
logger.SetLevel(logrus.InfoLevel)
|
|
||||||
}
|
|
||||||
return logger
|
return logger
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,17 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"github.com/mr-karan/doggo/pkg/resolvers"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Lookup sends the DNS queries to the server.
|
// Lookup sends the DNS queries to the server.
|
||||||
func (hub *Hub) Lookup(c *cli.Context) error {
|
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)
|
responses, err := hub.Resolver.Lookup(hub.Questions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -21,19 +26,57 @@ func (hub *Hub) Lookup(c *cli.Context) error {
|
||||||
// prepareQuestions iterates on list of domain names
|
// prepareQuestions iterates on list of domain names
|
||||||
// and prepare a list of questions
|
// and prepare a list of questions
|
||||||
// sent to the server with all possible combinations.
|
// sent to the server with all possible combinations.
|
||||||
func (hub *Hub) prepareQuestions() {
|
func (hub *Hub) prepareQuestions() error {
|
||||||
var question dns.Question
|
var (
|
||||||
|
question dns.Question
|
||||||
|
)
|
||||||
for _, name := range hub.QueryFlags.QNames.Value() {
|
for _, name := range hub.QueryFlags.QNames.Value() {
|
||||||
question.Name = dns.Fqdn(name)
|
var (
|
||||||
// iterate on a list of query types.
|
domains []string
|
||||||
for _, q := range hub.QueryFlags.QTypes.Value() {
|
ndots int
|
||||||
question.Qtype = dns.StringToType[strings.ToUpper(q)]
|
)
|
||||||
// iterate on a list of query classes.
|
|
||||||
for _, c := range hub.QueryFlags.QClasses.Value() {
|
// If `search` flag is specified then fetch the search list
|
||||||
question.Qclass = dns.StringToClass[strings.ToUpper(c)]
|
// from `resolv.conf` and set the
|
||||||
// append a new question for each possible pair.
|
if hub.QueryFlags.UseSearchList {
|
||||||
hub.Questions = append(hub.Questions, question)
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,12 +14,13 @@ import (
|
||||||
|
|
||||||
// Output has a list of fields which are produced for the output
|
// Output has a list of fields which are produced for the output
|
||||||
type Output struct {
|
type Output struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Class string `json:"class"`
|
Class string `json:"class"`
|
||||||
TTL string `json:"ttl"`
|
TTL string `json:"ttl"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
TimeTaken string `json:"rtt"`
|
TimeTaken string `json:"rtt"`
|
||||||
|
Nameserver string `json:"nameserver"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
|
@ -113,12 +114,13 @@ func collectOutput(responses []resolvers.Response) []Output {
|
||||||
qtype := dns.Type(h.Rrtype).String()
|
qtype := dns.Type(h.Rrtype).String()
|
||||||
rtt := fmt.Sprintf("%dms", r.RTT.Milliseconds())
|
rtt := fmt.Sprintf("%dms", r.RTT.Milliseconds())
|
||||||
o := Output{
|
o := Output{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: qtype,
|
Type: qtype,
|
||||||
TTL: ttl,
|
TTL: ttl,
|
||||||
Class: qclass,
|
Class: qclass,
|
||||||
Address: addr,
|
Address: addr,
|
||||||
TimeTaken: rtt,
|
TimeTaken: rtt,
|
||||||
|
Nameserver: r.Nameserver,
|
||||||
}
|
}
|
||||||
out = append(out, o)
|
out = append(out, o)
|
||||||
}
|
}
|
||||||
|
|
10
cmd/parse.go
10
cmd/parse.go
|
@ -4,10 +4,20 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (hub *Hub) loadQueryArgs(c *cli.Context) error {
|
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)
|
err := hub.loadFreeArgs(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cli.Exit("Error parsing arguments", -1)
|
cli.Exit("Error parsing arguments", -1)
|
||||||
|
|
|
@ -23,7 +23,7 @@ func (hub *Hub) initResolver(c *cli.Context) error {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// TODO: Add a method for reading system default nameserver in windows.
|
// TODO: Add a method for reading system default nameserver in windows.
|
||||||
} else {
|
} else {
|
||||||
rslvr, err := resolvers.NewResolverFromResolvFile(resolvers.DefaultResolvConfPath)
|
rslvr, err := resolvers.NewSystemResolver(resolvers.DefaultResolvConfPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package resolvers
|
package resolvers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
@ -47,6 +46,7 @@ func NewClassicResolver(servers []string, opts ClassicResolverOpts) (Resolver, e
|
||||||
nameservers = append(nameservers, srv)
|
nameservers = append(nameservers, srv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.Net = "udp"
|
client.Net = "udp"
|
||||||
if opts.UseIPv4 {
|
if opts.UseIPv4 {
|
||||||
client.Net = "udp4"
|
client.Net = "udp4"
|
||||||
|
@ -66,35 +66,6 @@ func NewClassicResolver(servers []string, opts ClassicResolverOpts) (Resolver, e
|
||||||
}, nil
|
}, 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.
|
// Lookup prepare a list of DNS messages to be sent to the server.
|
||||||
// It's possible to send multiple question in one message
|
// It's possible to send multiple question in one message
|
||||||
// but some nameservers are not able to
|
// but some nameservers are not able to
|
||||||
|
|
|
@ -26,7 +26,7 @@ func NewDOHResolver(servers []string) (Resolver, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DOHResolver) Lookup(questions []dns.Question) ([]Response, error) {
|
func (d *DOHResolver) Lookup(questions []dns.Question) ([]Response, error) {
|
||||||
var (
|
var (
|
||||||
messages = prepareMessages(questions)
|
messages = prepareMessages(questions)
|
||||||
responses []Response
|
responses []Response
|
||||||
|
@ -38,10 +38,10 @@ func (r *DOHResolver) Lookup(questions []dns.Question) ([]Response, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, srv := range r.servers {
|
for _, srv := range d.servers {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
// Make an HTTP POST request to the DNS server with the DNS message as wire format bytes in the body.
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue