feat: working finally
parent
df306e18a9
commit
252d11c764
15
TODO.md
15
TODO.md
|
@ -1,10 +1,11 @@
|
|||
# doggo - v1.0 Milestone
|
||||
|
||||
## Resolver
|
||||
- [ ] Create a DNS Resolver struct
|
||||
- [x] Create a DNS Resolver struct
|
||||
- [ ] Add methods to initialise the config, set defaults
|
||||
- [ ] Add a resolve method
|
||||
- [ ] Make it separate from Hub
|
||||
- [x] Add a resolve method
|
||||
- [x] Make it separate from Hub
|
||||
- [ ] Parse output into separate fields
|
||||
|
||||
## CLI Features
|
||||
- [ ] `digfile`
|
||||
|
@ -15,10 +16,10 @@
|
|||
- [ ] Table output
|
||||
|
||||
## CLI Grunt
|
||||
- [ ] Query args
|
||||
- [ ] Neatly package them to load args in different functions
|
||||
- [ ] Upper case is not mandatory for query type/classes
|
||||
|
||||
- [x] Query args
|
||||
- [x] Neatly package them to load args in different functions
|
||||
- [x] Upper case is not mandatory for query type/classes
|
||||
- [ ] Output
|
||||
## Tests
|
||||
|
||||
## 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 (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
resolver "github.com/mr-karan/doggo/pkg/resolve"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
@ -13,13 +11,15 @@ var (
|
|||
// Version and date of the build. This is injected at build-time.
|
||||
buildVersion = "unknown"
|
||||
buildDate = "unknown"
|
||||
verboseEnabled = false
|
||||
)
|
||||
|
||||
// initLogger initializes logger
|
||||
func initLogger(verbose bool) *logrus.Logger {
|
||||
logger := logrus.New()
|
||||
logger.SetFormatter(&logrus.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
DisableTimestamp: true,
|
||||
DisableLevelTruncation: true,
|
||||
})
|
||||
// Set logger level
|
||||
if verbose {
|
||||
|
@ -32,66 +32,61 @@ func initLogger(verbose bool) *logrus.Logger {
|
|||
}
|
||||
|
||||
func main() {
|
||||
// Intialize new CLI app
|
||||
app := cli.NewApp()
|
||||
app.Name = "doggo"
|
||||
app.Usage = "Command-line DNS Client"
|
||||
app.Version = buildVersion
|
||||
var (
|
||||
logger = initLogger(true)
|
||||
logger = initLogger(verboseEnabled)
|
||||
app = cli.NewApp()
|
||||
)
|
||||
// Initialize hub.
|
||||
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{
|
||||
&cli.StringSliceFlag{
|
||||
Name: "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{
|
||||
Name: "verbose",
|
||||
Usage: "Enable verbose logging",
|
||||
Destination: &verboseEnabled,
|
||||
DefaultText: "false",
|
||||
},
|
||||
}
|
||||
app.Action = func(c *cli.Context) error {
|
||||
|
||||
// parse arguments
|
||||
var domains cli.StringSlice
|
||||
for _, arg := range c.Args().Slice() {
|
||||
if strings.HasPrefix(arg, "@") {
|
||||
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)
|
||||
app.Before = hub.loadQueryArgs
|
||||
app.Action = func(c *cli.Context) error {
|
||||
if len(hub.QueryFlags.QNames.Value()) == 0 {
|
||||
cli.ShowAppHelpAndExit(c, 0)
|
||||
}
|
||||
} else {
|
||||
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()
|
||||
hub.Lookup(c)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run the app.
|
||||
hub.Logger.Info("Starting doggo...")
|
||||
hub.Logger.Debug("Starting doggo...")
|
||||
err := app.Run(os.Args)
|
||||
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
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
"github.com/mr-karan/doggo/pkg/resolve"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
@ -9,25 +11,31 @@ import (
|
|||
type Hub struct {
|
||||
Logger *logrus.Logger
|
||||
Version string
|
||||
Domains *cli.StringSlice
|
||||
QTypes []string
|
||||
QClass []string
|
||||
Nameservers []string
|
||||
QueryFlags QueryFlags
|
||||
Questions []dns.Question
|
||||
Resolver *resolve.Resolver
|
||||
}
|
||||
|
||||
// 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.
|
||||
func NewHub(logger *logrus.Logger, buildVersion string) *Hub {
|
||||
// Initialise Resolver
|
||||
hub := &Hub{
|
||||
Logger: logger,
|
||||
Version: buildVersion,
|
||||
QueryFlags: QueryFlags{
|
||||
QNames: cli.NewStringSlice(),
|
||||
QTypes: cli.NewStringSlice(),
|
||||
QClasses: cli.NewStringSlice(),
|
||||
Nameservers: cli.NewStringSlice(),
|
||||
},
|
||||
}
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (r *Resolver) Lookup(domains []string) []error {
|
||||
// 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
|
||||
// but some nameservers are not able to
|
||||
var messages = make([]dns.Msg, 0, len(domains))
|
||||
|
||||
for _, d := range domains {
|
||||
func (r *Resolver) Lookup(questions []dns.Question) error {
|
||||
var messages = make([]dns.Msg, 0, len(questions))
|
||||
for _, q := range questions {
|
||||
msg := dns.Msg{}
|
||||
msg.Id = dns.Id()
|
||||
msg.RecursionDesired = true
|
||||
// It's recommended to only send 1 question for 1 DNS message.
|
||||
msg.Question = []dns.Question{(dns.Question{
|
||||
Name: dns.Fqdn(d),
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
})}
|
||||
msg.Question = []dns.Question{q}
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
var errors []error
|
||||
for _, msg := range messages {
|
||||
for _, srv := range r.servers {
|
||||
in, rtt, err := r.client.Exchange(&msg, srv)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
return err
|
||||
}
|
||||
for _, ans := range in.Answer {
|
||||
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)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue