feat: working finally
parent
df306e18a9
commit
252d11c764
15
TODO.md
15
TODO.md
|
@ -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
|
||||||
|
|
BIN
bin/doggo.bin
BIN
bin/doggo.bin
Binary file not shown.
81
cmd/cli.go
81
cmd/cli.go
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
32
cmd/hub.go
32
cmd/hub.go
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
cmd/parse.go
73
cmd/parse.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
35
cmd/utils.go
35
cmd/utils.go
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
func (r *Resolver) Lookup(questions []dns.Question) error {
|
||||||
var messages = make([]dns.Msg, 0, len(domains))
|
var messages = make([]dns.Msg, 0, len(questions))
|
||||||
|
for _, q := range questions {
|
||||||
for _, d := range domains {
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue