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 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
|
||||
|
||||
|
|
20
cmd/cli.go
20
cmd/cli.go
|
@ -10,12 +10,11 @@ var (
|
|||
// Version and date of the build. This is injected at build-time.
|
||||
buildVersion = "unknown"
|
||||
buildDate = "unknown"
|
||||
verboseEnabled = false
|
||||
)
|
||||
|
||||
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)
|
||||
|
|
13
cmd/hub.go
13
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
|
||||
}
|
||||
|
|
|
@ -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,10 +26,34 @@ 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)
|
||||
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)]
|
||||
|
@ -37,3 +66,17 @@ func (hub *Hub) prepareQuestions() {
|
|||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ type Output struct {
|
|||
TTL string `json:"ttl"`
|
||||
Address string `json:"address"`
|
||||
TimeTaken string `json:"rtt"`
|
||||
Nameserver string `json:"nameserver"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
|
@ -119,6 +120,7 @@ func collectOutput(responses []resolvers.Response) []Output {
|
|||
Class: qclass,
|
||||
Address: addr,
|
||||
TimeTaken: rtt,
|
||||
Nameserver: r.Nameserver,
|
||||
}
|
||||
out = append(out, o)
|
||||
}
|
||||
|
|
10
cmd/parse.go
10
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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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