feat: working finally

pull/2/head
Karan Sharma 2020-12-10 21:44:04 +05:30
parent df306e18a9
commit 252d11c764
11 changed files with 186 additions and 200 deletions

15
TODO.md
View File

@ -1,10 +1,11 @@
# doggo - v1.0 Milestone # doggo - v1.0 Milestone
## Resolver ## Resolver
- [ ] Create a DNS Resolver struct - [x] Create a DNS Resolver struct
- [ ] Add methods to initialise the config, set defaults - [ ] Add methods to initialise the config, set defaults
- [ ] Add a resolve method - [x] Add a resolve method
- [ ] Make it separate from Hub - [x] Make it separate from Hub
- [ ] Parse output into separate fields
## CLI Features ## CLI Features
- [ ] `digfile` - [ ] `digfile`
@ -15,10 +16,10 @@
- [ ] Table output - [ ] Table output
## CLI Grunt ## CLI Grunt
- [ ] Query args - [x] Query args
- [ ] Neatly package them to load args in different functions - [x] Neatly package them to load args in different functions
- [ ] Upper case is not mandatory for query type/classes - [x] Upper case is not mandatory for query type/classes
- [ ] Output
## Tests ## Tests
## Documentation ## Documentation

Binary file not shown.

View File

@ -2,9 +2,7 @@ package main
import ( import (
"os" "os"
"strings"
resolver "github.com/mr-karan/doggo/pkg/resolve"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -13,13 +11,15 @@ 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
) )
// initLogger initializes logger // initLogger initializes logger
func initLogger(verbose bool) *logrus.Logger { func initLogger(verbose bool) *logrus.Logger {
logger := logrus.New() logger := logrus.New()
logger.SetFormatter(&logrus.TextFormatter{ logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true, DisableTimestamp: true,
DisableLevelTruncation: true,
}) })
// Set logger level // Set logger level
if verbose { if verbose {
@ -32,66 +32,61 @@ func initLogger(verbose bool) *logrus.Logger {
} }
func main() { func main() {
// Intialize new CLI app
app := cli.NewApp()
app.Name = "doggo"
app.Usage = "Command-line DNS Client"
app.Version = buildVersion
var ( var (
logger = initLogger(true) logger = initLogger(verboseEnabled)
app = cli.NewApp()
) )
// Initialize hub. // Initialize hub.
hub := NewHub(logger, buildVersion) hub := NewHub(logger, buildVersion)
// Register command line args.
// Configure CLI app.
app.Name = "doggo"
app.Usage = "Command-line DNS Client"
app.Version = buildVersion
var qFlags QueryFlags
// Register command line flags.
app.Flags = []cli.Flag{ app.Flags = []cli.Flag{
&cli.StringSliceFlag{ &cli.StringSliceFlag{
Name: "query", Name: "query",
Usage: "Domain name to query", Usage: "Domain name to query",
Destination: hub.Domains, Destination: qFlags.QNames,
},
&cli.StringSliceFlag{
Name: "type",
Usage: "Type of DNS record to be queried (A, AAAA, MX etc)",
Destination: qFlags.QTypes,
},
&cli.StringSliceFlag{
Name: "nameserver",
Usage: "Address of the nameserver to send packets to",
Destination: qFlags.Nameservers,
},
&cli.StringSliceFlag{
Name: "class",
Usage: "Network class of the DNS record to be queried (IN, CH, HS etc)",
Destination: qFlags.QClasses,
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "verbose", Name: "verbose",
Usage: "Enable verbose logging", Usage: "Enable verbose logging",
Destination: &verboseEnabled,
DefaultText: "false",
}, },
} }
app.Action = func(c *cli.Context) error {
// parse arguments app.Before = hub.loadQueryArgs
var domains cli.StringSlice app.Action = func(c *cli.Context) error {
for _, arg := range c.Args().Slice() { if len(hub.QueryFlags.QNames.Value()) == 0 {
if strings.HasPrefix(arg, "@") { cli.ShowAppHelpAndExit(c, 0)
hub.Nameservers = append(hub.Nameservers, arg)
} else if isUpper(arg) {
if parseQueryType(arg) {
hub.QTypes = append(hub.QTypes, arg)
} else if parseQueryClass(arg) {
hub.QClass = append(hub.QClass, arg)
} }
} else { hub.Lookup(c)
domains.Set(arg)
hub.Domains = &domains
}
}
// load defaults
if len(hub.QTypes) == 0 {
hub.QTypes = append(hub.QTypes, "A")
}
if len(hub.Nameservers) == 0 {
ns, err := resolver.GetDefaultNameserver()
if err != nil {
panic(err)
}
hub.Nameservers = append(hub.Nameservers, ns)
}
// resolve query
hub.Resolve()
return nil return nil
} }
// Run the app. // Run the app.
hub.Logger.Info("Starting doggo...") hub.Logger.Debug("Starting doggo...")
err := app.Run(os.Args) err := app.Run(os.Args)
if err != nil { if err != nil {
logger.Errorf("Something terrbily went wrong: %s", err) logger.Errorf("oops! we encountered an issue: %s", err)
} }
} }

View File

@ -1,6 +1,8 @@
package main package main
import ( import (
"github.com/miekg/dns"
"github.com/mr-karan/doggo/pkg/resolve"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -9,25 +11,31 @@ import (
type Hub struct { type Hub struct {
Logger *logrus.Logger Logger *logrus.Logger
Version string Version string
Domains *cli.StringSlice QueryFlags QueryFlags
QTypes []string Questions []dns.Question
QClass []string Resolver *resolve.Resolver
Nameservers []string }
// QueryFlags is used store the value of CLI flags.
type QueryFlags struct {
QNames *cli.StringSlice
QTypes *cli.StringSlice
QClasses *cli.StringSlice
Nameservers *cli.StringSlice
} }
// NewHub initializes an instance of Hub which holds app wide configuration. // NewHub initializes an instance of Hub which holds app wide configuration.
func NewHub(logger *logrus.Logger, buildVersion string) *Hub { func NewHub(logger *logrus.Logger, buildVersion string) *Hub {
// Initialise Resolver
hub := &Hub{ hub := &Hub{
Logger: logger, Logger: logger,
Version: buildVersion, Version: buildVersion,
QueryFlags: QueryFlags{
QNames: cli.NewStringSlice(),
QTypes: cli.NewStringSlice(),
QClasses: cli.NewStringSlice(),
Nameservers: cli.NewStringSlice(),
},
} }
return hub return hub
} }
// initApp acts like a middleware to load app managers with Hub before running any command.
// Use this middleware to perform any action before the command is run.
func (hub *Hub) initApp(fn cli.ActionFunc) cli.ActionFunc {
return func(c *cli.Context) error {
return fn(c)
}
}

37
cmd/lookup.go 100644
View File

@ -0,0 +1,37 @@
package main
import (
"strings"
"github.com/miekg/dns"
"github.com/urfave/cli/v2"
)
func (hub *Hub) Lookup(c *cli.Context) error {
hub.prepareQuestions()
err := hub.Resolver.Lookup(hub.Questions)
if err != nil {
hub.Logger.Error(err)
}
return nil
}
// 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
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)
}
}
}
}

View File

@ -1,13 +1,76 @@
package main package main
func (hub *Hub) loadFlags() { import (
"runtime"
"strings"
"github.com/miekg/dns"
"github.com/mr-karan/doggo/pkg/resolve"
"github.com/urfave/cli/v2"
)
func (hub *Hub) loadQueryArgs(c *cli.Context) error {
err := hub.parseFreeArgs(c)
if err != nil {
cli.Exit("Error parsing arguments", -1)
}
err = hub.loadResolver(c)
if err != nil {
cli.Exit("Error parsing nameservers", -1)
}
hub.loadFallbacks(c)
return err
} }
func (hub *Hub) loadFreeForm() { // parseFreeArgs tries to parse all the arguments
// given to the CLI. These arguments don't have any specific
// order so we have to deduce based on the pattern of argument.
// For eg, a nameserver must always begin with `@`. In this
// pattern we deduce the arguments and map it to internal query
// options. In case an argument isn't able to fit in any of the existing
// pattern it is considered to be a "query name".
func (hub *Hub) parseFreeArgs(c *cli.Context) error {
for _, arg := range c.Args().Slice() {
if strings.HasPrefix(arg, "@") {
hub.QueryFlags.Nameservers.Set(arg)
} else if _, ok := dns.StringToType[strings.ToUpper(arg)]; ok {
hub.QueryFlags.QTypes.Set(arg)
} else if _, ok := dns.StringToClass[strings.ToUpper(arg)]; ok {
hub.QueryFlags.QClasses.Set(arg)
} else {
// if nothing matches, consider it's a query name.
hub.QueryFlags.QNames.Set(arg)
}
}
return nil
} }
func (hub *Hub) loadFallbacks() { // loadFallbacks sets fallbacks for options
// that are not specified by the user.
func (hub *Hub) loadFallbacks(c *cli.Context) {
if len(hub.QueryFlags.QTypes.Value()) == 0 {
hub.QueryFlags.QTypes.Set("A")
}
if len(hub.QueryFlags.QClasses.Value()) == 0 {
hub.QueryFlags.QClasses.Set("IN")
}
}
// loadResolver checks
func (hub *Hub) loadResolver(c *cli.Context) error {
if len(hub.QueryFlags.Nameservers.Value()) == 0 {
if runtime.GOOS == "windows" {
// TODO: Add a method for reading system default nameserver in windows.
} else {
rslvr, err := resolve.NewResolverFromResolvFile(resolve.DefaultResolvConfPath)
if err != nil {
return err
}
hub.Resolver = rslvr
}
} else {
rslvr := resolve.NewResolver(hub.QueryFlags.Nameservers.Value())
hub.Resolver = rslvr
}
return nil
} }

View File

@ -1,32 +0,0 @@
package main
import (
"fmt"
"github.com/miekg/dns"
)
// Resolve resolves the domain name
func (hub *Hub) Resolve() {
var messages = make([]dns.Msg, 0, len(hub.Domains.Value()))
for _, d := range hub.Domains.Value() {
msg := dns.Msg{}
msg.Id = dns.Id()
msg.RecursionDesired = true
msg.Question = []dns.Question{(dns.Question{dns.Fqdn(d), dns.TypeA, dns.ClassINET})}
messages = append(messages, msg)
}
c := new(dns.Client)
for _, msg := range messages {
in, rtt, err := c.Exchange(&msg, "127.0.0.1:53")
if err != nil {
panic(err)
}
for _, ans := range in.Answer {
if t, ok := ans.(*dns.A); ok {
fmt.Println(t.String())
}
}
fmt.Println("rtt is", rtt, msg.Question[0].Name)
}
}

View File

@ -1,35 +0,0 @@
package main
import "unicode"
var (
QTYPES = []string{"A", "AAAA", "MX"}
QCLASS = []string{"CN", "AAAA", "MX"}
)
func isUpper(s string) bool {
for _, r := range s {
if !unicode.IsUpper(r) && unicode.IsLetter(r) {
return false
}
}
return true
}
func parseQueryType(s string) bool {
for _, b := range QTYPES {
if b == s {
return true
}
}
return false
}
func parseQueryClass(s string) bool {
for _, b := range QCLASS {
if b == s {
return true
}
}
return false
}

View File

@ -1,12 +0,0 @@
package models
// Question represents a given query to the client.
// A question can have multiple domains, multiple nameservers
// but it's the responsibility of the client to send each question
// to the nameserver and collect responses.
type Question struct {
Domain []string
Nameservers []string
QClass []uint16
QType []uint16
}

View File

@ -1,33 +0,0 @@
package resolve
import (
"bufio"
"os"
"strings"
)
// GetDefaultNameserver reads `/etc/resolv.conf` to determine the default
// nameserver configured. Returns an error if unable to parse or
// no nameserver specified. It returns as soon as it finds a line
// with `nameserver` prefix.
// An example format:
// `nameserver 127.0.0.1`
func GetDefaultNameserver() (string, error) {
file, err := os.Open("/etc/resolv.conf")
if err != nil {
return "", err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "nameserver") {
return strings.Fields(scanner.Text())[1], nil
}
}
if err := scanner.Err(); err != nil {
return "", err
}
return "", err
}

View File

@ -54,30 +54,24 @@ func NewResolverFromResolvFile(resolvFilePath string) (*Resolver, error) {
}, nil }, nil
} }
func (r *Resolver) Lookup(domains []string) []error { // Lookup prepare a list of DNS messages to be sent to the server.
// 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
var messages = make([]dns.Msg, 0, len(domains)) func (r *Resolver) Lookup(questions []dns.Question) error {
var messages = make([]dns.Msg, 0, len(questions))
for _, d := range domains { for _, q := range questions {
msg := dns.Msg{} msg := dns.Msg{}
msg.Id = dns.Id() msg.Id = dns.Id()
msg.RecursionDesired = true msg.RecursionDesired = true
// It's recommended to only send 1 question for 1 DNS message. // It's recommended to only send 1 question for 1 DNS message.
msg.Question = []dns.Question{(dns.Question{ msg.Question = []dns.Question{q}
Name: dns.Fqdn(d),
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
})}
messages = append(messages, msg) messages = append(messages, msg)
} }
var errors []error
for _, msg := range messages { for _, msg := range messages {
for _, srv := range r.servers { for _, srv := range r.servers {
in, rtt, err := r.client.Exchange(&msg, srv) in, rtt, err := r.client.Exchange(&msg, srv)
if err != nil { if err != nil {
errors = append(errors, err) return err
} }
for _, ans := range in.Answer { for _, ans := range in.Answer {
if t, ok := ans.(*dns.A); ok { if t, ok := ans.(*dns.A); ok {
@ -87,5 +81,5 @@ func (r *Resolver) Lookup(domains []string) []error {
fmt.Println("rtt is", rtt, msg.Question) fmt.Println("rtt is", rtt, msg.Question)
} }
} }
return errors return nil
} }