diff --git a/.drone.jsonnet b/.drone.jsonnet index 7ac857c..0bcb4dd 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -1,22 +1,67 @@ -local pipeline(version, arch) = { +local testing(version, arch) = { kind: "pipeline", name: version + "-" + arch , platform: { arch: arch }, steps: [ + { + name: "lint", + image: "rancher/drone-golangci-lint:latest" + }, { name: "test", image: "golang:" + version, commands: [ - "go test ./..." + "go test -race -v ./... -cover" ] + }, + ] +}; + +// "Inspired by" https://goreleaser.com/ci/drone/ +local release() = { + kind: "pipeline", + name: "release", + trigger: { + event: "tag" + }, + steps: [ + { + name: "fetch", + image: "docker:git", + commands : [ + "git fetch --tags" + ] + }, + { + name: "test", + image: "golang", + commands: [ + "go test -race -v ./... -cover" + ] + }, + { + name: "release", + image: "goreleaser/goreleaser", + environment: { + "GITEA_TOKEN": { + from_secret: "GITEA_TOKEN" + } + }, + commands: [ + "goreleaser release" + ], + // when: { + // event: "tag" + // } } ] }; // logawl uses generics so 1.18 is the minimum [ - pipeline("1.18", "amd64"), - pipeline("1.18", "arm64"), + testing("1.18", "amd64"), + testing("1.18", "arm64"), + release() ] \ No newline at end of file diff --git a/.gitignore b/.gitignore index 682e852..356b47d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ # vendor/ # Go workspace file -go.work \ No newline at end of file +go.work +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..88b95e2 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,39 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + # - go generate ./... +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 +universal_binaries: +- replace: true +archives: + - replacements: + darwin: macOS + linux: Linux + windows: Windows + amd64: x86_64 + format_overrides: + - goos: windows + format: zip +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/Makefile b/Makefile index f2ca423..d9bb0e7 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,12 @@ -GO:=go -GOFLAGS:= -ldflags '-s -w' -PREFIX:=/usr/local -BINPATH=$(PREFIX)/bin +# This should only be used for dev environments +GO := go +GOFLAGS := -ldflags '-s -w' +PREFIX := /usr/local +BINPATH = $(PREFIX)/bin +export CGO_ENABLED=1 +export CC=gcc + +.PHONY: clean doc # hehe all: awl @@ -9,19 +14,22 @@ all: awl awl: . $(GO) build -o awl $(GOFLAGS) . +doc: + scdoc < doc/awl.1.md > doc/awl.1 + test: - $(GO) test ./... + $(GO) test -race -v ./... -cover fmt: - $(GO) fmt + gofmt -w -s . vet: - $(GO) vet + $(GO) vet ./... lint: fmt vet install: awl - install awl $(BINPATH) || echo "You probably need to run `sudo make install`" + install awl $(BINPATH) clean: $(GO) clean \ No newline at end of file diff --git a/README.md b/README.md index 7e37b9c..7aedb20 100644 --- a/README.md +++ b/README.md @@ -12,24 +12,14 @@ This was made as my first major experiment with Go, so there are probably things The excellent [dns](https://github.com/miekg/dns) library for Go does most of the heavy lifting. -## What works +## What awl should do -- UDP -- TCP -- TLS -- HTTPS (maybe) -- QUIC (extreme maybe) - -## What doesn't - -- Your sanity after reading my awful code -- A motivation for making this after finding q and doggo - -## What should change - -- Make the CLI less abysmal (migrate to [cobra](https://github.com/spf13/cobra)? - or just use stdlib's flags) - Optimize everything - Make the code less spaghetti (partially completed) - Feature parity with drill - - Making a drop-in replacement for drill? \ No newline at end of file + - Making a drop-in replacement for drill? + - What about dig? + +## What awl won't do +- Print to files or read from files +- Colour outputs (unless?) diff --git a/awl.go b/awl.go deleted file mode 100644 index 6c5af9c..0000000 --- a/awl.go +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -package main - -import ( - "fmt" - "os" -) - -func main() { - app := prepareCLI() - err := app.Run(os.Args) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} diff --git a/cli.go b/cli.go deleted file mode 100644 index e810161..0000000 --- a/cli.go +++ /dev/null @@ -1,216 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -package main - -import ( - "fmt" - "math/rand" - "runtime" - "strings" - - "git.froth.zone/sam/awl/conf" - "git.froth.zone/sam/awl/query" - - "github.com/miekg/dns" - "github.com/urfave/cli/v2" - "golang.org/x/net/idna" -) - -// Do all the magic CLI crap -func prepareCLI() *cli.App { - // Custom version string - cli.VersionPrinter = func(c *cli.Context) { - fmt.Printf("%s version %s, built with %s\n", c.App.Name, c.App.Version, runtime.Version()) - } - - cli.VersionFlag = &cli.BoolFlag{ - Name: "v", - Usage: "show version and exit", - } - - cli.HelpFlag = &cli.BoolFlag{ - Name: "h", - Usage: "show this help and exit", - } - - // Hack to get rid of the annoying default on the CLI - oldFlagStringer := cli.FlagStringer - cli.FlagStringer = func(f cli.Flag) string { - return strings.TrimSuffix(oldFlagStringer(f), " (default: false)") - } - - cli.AppHelpTemplate = `{{.Name}} - {{.Usage}} - - Usage: {{.HelpName}} name [@server] [record] - can be a name or an IP address - defaults to A - - arguments can be in any order - {{if .VisibleFlags}} - Options: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}}` - app := &cli.App{ - Name: "awl", - Usage: "drill, writ small", - Version: "v0.2.1", - Flags: []cli.Flag{ - &cli.IntFlag{ - Name: "port", - Aliases: []string{"p"}, - Usage: "`` to make DNS query", - DefaultText: "53 over plain TCP/UDP, 853 over TLS or QUIC", - }, - &cli.BoolFlag{ - Name: "4", - Usage: "force IPv4", - }, - &cli.BoolFlag{ - Name: "6", - Usage: "force IPv6", - }, - &cli.BoolFlag{ - Name: "dnssec", - Aliases: []string{"D"}, - Usage: "enable DNSSEC", - }, - &cli.BoolFlag{ - Name: "json", - Aliases: []string{"j"}, - Usage: "return the result(s) as JSON", - }, - &cli.BoolFlag{ - Name: "short", - Aliases: []string{"s"}, - Usage: "print just the results, equivalent to dig +short", - }, - &cli.BoolFlag{ - Name: "tcp", - Aliases: []string{"t"}, - Usage: "use TCP (default: use UDP)", - }, - &cli.BoolFlag{ - Name: "tls", - Aliases: []string{"T"}, - Usage: "use DNS-over-TLS", - }, - &cli.BoolFlag{ - Name: "https", - Aliases: []string{"H"}, - Usage: "use DNS-over-HTTPS", - }, - &cli.BoolFlag{ - Name: "quic", - Aliases: []string{"Q"}, - Usage: "use DNS-over-QUIC", - }, - &cli.BoolFlag{ - Name: "no-truncate", - Usage: "ignore truncation if a UDP request truncates (default: retry with TCP)", - }, - &cli.BoolFlag{ - Name: "aa", - Usage: "set AA (Authoratative Answer) flag (default: not set)", - }, - &cli.BoolFlag{ - Name: "tc", - Usage: "set TC (TrunCated) flag (default: not set)", - }, - &cli.BoolFlag{ - Name: "z", - Usage: "set Z (Zero) flag (default: not set)", - }, - &cli.BoolFlag{ - Name: "cd", - Usage: "set CD (Checking Disabled) flag (default: not set)", - }, - &cli.BoolFlag{ - Name: "no-rd", - Usage: "UNset RD (Recursion Desired) flag (default: set)", - }, - &cli.BoolFlag{ - Name: "no-ra", - Usage: "UNset RA (Recursion Available) flag (default: set)", - }, - &cli.BoolFlag{ - Name: "reverse", - Aliases: []string{"x"}, - Usage: "do a reverse lookup", - }, - &cli.BoolFlag{ - Name: "debug", - Usage: "enable verbose logging", - }, - }, - Action: doQuery, - } - return app -} - -// Parse the wildcard arguments, drill style -func parseArgs(args []string, opts query.Options) (query.Answers, error) { - var ( - resp query.Response - err error - ) - for _, arg := range args { - r, ok := dns.StringToType[strings.ToUpper(arg)] - switch { - // If it starts with @, it's a DNS server - case strings.HasPrefix(arg, "@"): - resp.Answers.Server = strings.Split(arg, "@")[1] - case strings.Contains(arg, "."): - resp.Answers.Name, err = idna.ToUnicode(arg) - if err != nil { - return query.Answers{}, err - } - case ok: - // If it's a DNS request, it's a DNS request (obviously) - resp.Answers.Request = r - default: - //else, assume it's a name - resp.Answers.Name, err = idna.ToUnicode(arg) - if err != nil { - return query.Answers{}, err - } - - } - } - - // If nothing was set, set a default - if resp.Answers.Name == "" { - resp.Answers.Name = "." - if resp.Answers.Request == 0 { - resp.Answers.Request = dns.StringToType["NS"] - } - } else { - if resp.Answers.Request == 0 { - resp.Answers.Request = dns.StringToType["A"] - } - } - if resp.Answers.Server == "" { - resolv, err := conf.GetDNSConfig() - if err != nil { // Query Google by default - resp.Answers.Server = "8.8.4.4" - } else { - for _, srv := range resolv.Servers { - if opts.IPv4 { - if strings.Contains(srv, ".") { - resp.Answers.Server = srv - break - } - } else if opts.IPv6 { - if strings.Contains(srv, ":") { - resp.Answers.Server = srv - break - } - } else { - resp.Answers.Server = resolv.Servers[rand.Intn(len(resolv.Servers))] - break - } - } - } - } - - return query.Answers{Server: resp.Answers.Server, Request: resp.Answers.Request, Name: resp.Answers.Name}, nil -} diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000..c6309f6 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package cli + +import ( + "fmt" + "os" + "runtime" + + "git.froth.zone/sam/awl/internal/structs" + "git.froth.zone/sam/awl/util" + + "github.com/miekg/dns" + flag "github.com/stefansundin/go-zflag" +) + +// Parse the arguments passed into awl +func ParseCLI(version string) (Options, error) { + + flag.Usage = func() { + fmt.Println(`awl - drill, writ small + + Usage: awl name [@server] [record] + domain, IP address, phone number + defaults to A + + Arguments may be in any order, including flags. + Dig-like +[no]commands are also supported, see dig(1) or dig -h + + Options:`) + flag.PrintDefaults() + os.Exit(0) + } + + // CLI flag + var ( + port = flag.Int("port", 0, "`port` to make DNS query (default: 53 for UDP/TCP, 853 for TLS/QUIC", flag.OptShorthand('p'), flag.OptDisablePrintDefault(true)) + query = flag.String("query", ".", "domain name to query", flag.OptShorthand('q')) + class = flag.String("class", "IN", "DNS class to query", flag.OptShorthand('c')) + qType = flag.String("qType", "A", "type to query", flag.OptShorthand('t')) + + ipv4 = flag.Bool("4", false, "force IPv4", flag.OptShorthandStr("4")) + ipv6 = flag.Bool("6", false, "force IPv6", flag.OptShorthand('6')) + reverse = flag.Bool("reverse", false, "do a reverse lookup", flag.OptShorthand('x')) + + dnssec = flag.Bool("dnssec", false, "enable DNSSEC", flag.OptShorthand('D')) + truncate = flag.Bool("no-truncate", false, "ignore truncation if a UDP request truncates (default= retry with TCP)") + + tcp = flag.Bool("tcp", false, "use TCP") + tls = flag.Bool("tls", false, "use DNS-over-TLS", flag.OptShorthand('T')) + https = flag.Bool("https", false, "use DNS-over-HTTPS", flag.OptShorthand('H')) + quic = flag.Bool("quic", false, "use DNS-over-QUIC", flag.OptShorthand('Q')) + + aa = flag.Bool("aa", false, "set/unset AA (Authoratative Answer) flag (default: not set)") + ad = flag.Bool("ad", false, "set/unset AD (Authenticated Data) flag (default: not set)") + cd = flag.Bool("cd", false, "set/unset CD (Checking Disabled) flag (default: not set)") + qr = flag.Bool("qr", false, "set/unset QR (QueRy) flag (default: set)", flag.OptDisablePrintDefault(true)) + rd = flag.Bool("rd", true, "set/unset RD (Recursion Desired) flag (default: set)", flag.OptDisablePrintDefault(true)) + ra = flag.Bool("ra", false, "set/unset RA (Recursion Available) flag (default: set)", flag.OptDisablePrintDefault(true)) + tc = flag.Bool("tc", false, "set/unset TC (TrunCated) flag (default: not set)") + z = flag.Bool("z", false, "set/unset Z (Zero) flag (default: not set)", flag.OptShorthand('z')) + + short = flag.Bool("short", false, "print just the results, equivalent to dig +short", flag.OptShorthand('s')) + json = flag.Bool("json", false, "print the result(s) as JSON", flag.OptShorthand('j')) + xml = flag.Bool("xml", false, "print the result(s) as XML", flag.OptShorthand('X')) + yaml = flag.Bool("yaml", false, "print the result(s) as yaml", flag.OptShorthand('y')) + + verbosity = flag.Int("verbosity", 0, "sets verbosity", flag.OptShorthand('v'), flag.OptNoOptDefVal("3")) + versionFlag = flag.Bool("version", false, "print version information", flag.OptShorthand('V')) + ) + + // Don't sort the flags when -h is given + flag.CommandLine.SortFlags = false + + // Parse the flags + flag.Parse() + + opts := Options{ + Logger: util.InitLogger(*verbosity), + Class: dns.StringToClass[*class], + Port: *port, + IPv4: *ipv4, + IPv6: *ipv6, + DNSSEC: *dnssec, + Short: *short, + TCP: *tcp, + TLS: *tls, + HTTPS: *https, + QUIC: *quic, + Truncate: *truncate, + AA: *aa, + AD: *ad, + TC: *tc, + Z: *z, + CD: *cd, + QR: *qr, + RD: *rd, + RA: *ra, + Reverse: *reverse, + JSON: *json, + XML: *xml, + YAML: *yaml, + Request: structs.Request{ + Type: dns.StringToType[*qType], + Name: *query, + }, + } + + if *versionFlag { + fmt.Printf("awl version %s, built with %s\n", version, runtime.Version()) + os.Exit(0) + } + + // Parse all the arguments that don't start with - or -- + // This includes the dig-style (+) options + err := ParseMiscArgs(flag.Args(), &opts) + if err != nil { + opts.Logger.Fatal(err) + } + + if opts.Port == 0 { + if opts.TLS || opts.QUIC { + opts.Port = 853 + } else { + opts.Port = 53 + } + } + + // opts.Request = req + + // Set verbosity to full if just -v is specified + // flag.Lookup("verbosity").NoOptDefVal = "3" + + return opts, nil +} diff --git a/cli/cli_test.go b/cli/cli_test.go new file mode 100644 index 0000000..7ac5666 --- /dev/null +++ b/cli/cli_test.go @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package cli_test + +// TODO: readd these + +// import ( +// "os" +// "testing" + +// "git.froth.zone/sam/awl/internal/structs" +// "git.froth.zone/sam/awl/query" + +// "github.com/miekg/dns" +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/require" +// ) + +// func TestApp(t *testing.T) { +// t.Parallel() +// app := prepareCLI() +// // What more can even be done lmao +// require.NotNil(t, app) +// } + +// func TestArgParse(t *testing.T) { +// t.Parallel() +// tests := []struct { +// in []string +// want structs.Request +// }{ +// { +// []string{"@::1", "localhost", "AAAA"}, +// structs.Request{Server: "::1", Type: dns.TypeAAAA, Name: "localhost"}, +// }, +// { +// []string{"@1.0.0.1", "google.com"}, +// structs.Request{Server: "1.0.0.1", Type: dns.TypeA, Name: "google.com"}, +// }, +// { +// []string{"@8.8.4.4"}, +// structs.Request{Server: "8.8.4.4", Type: dns.TypeNS, Name: "."}, +// }, +// } +// for _, test := range tests { +// act, err := parseArgs(test.in, &query.Options{}) +// assert.Nil(t, err) +// assert.Equal(t, test.want, act) +// } +// } + +// func TestQuery(t *testing.T) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, "--Treebug") +// err := app.Run(args) +// assert.NotNil(t, err) +// } + +// func TestNoArgs(t *testing.T) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, "--no-truncate") +// err := app.Run(args) +// assert.Nil(t, err) +// } + +// func TestFlags(t *testing.T) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, "--debug") +// args = append(args, "--short") +// args = append(args, "-4") +// err := app.Run(args) +// assert.Nil(t, err) +// } + +// func TestHTTPS(t *testing.T) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, "-H") +// // args = append(args, "@https://cloudflare-dns.com/dns-query") +// args = append(args, "git.froth.zone") +// err := app.Run(args) +// assert.Nil(t, err) +// } + +// func TestJSON(t *testing.T) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, "-j") +// args = append(args, "git.froth.zone") +// err := app.Run(args) +// assert.Nil(t, err) +// } + +// func TestTLS(t *testing.T) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, "-T") +// args = append(args, "git.froth.zone") +// err := app.Run(args) +// assert.Nil(t, err) +// } + +// func TestXML(t *testing.T) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, "-X") +// args = append(args, "git.froth.zone") +// err := app.Run(args) +// assert.Nil(t, err) +// } + +// func TestYAML(t *testing.T) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, "-y") +// args = append(args, "git.froth.zone") +// err := app.Run(args) +// assert.Nil(t, err) +// } + +// func TestQUIC(t *testing.T) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, "-Q") +// // args = append(args, "@dns.adguard.com") +// args = append(args, "git.froth.zone") +// err := app.Run(args) +// assert.Nil(t, err) +// } + +// func TestReverse(t *testing.T) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, "-x") +// args = append(args, "8.8.8.8") +// err := app.Run(args) +// assert.Nil(t, err) +// } + +// func FuzzCli(f *testing.F) { +// testcases := []string{"git.froth.zone", "", "!12345", "google.com.edu.org.fr"} +// for _, tc := range testcases { +// f.Add(tc) +// } + +// f.Fuzz(func(t *testing.T, orig string) { +// app := prepareCLI() +// args := os.Args[0:1] +// args = append(args, orig) +// err := app.Run(args) +// if err != nil { +// require.ErrorContains(t, err, "domain must be fully qualified") +// } +// require.Nil(t, err) +// }) +// } diff --git a/cli/dig.go b/cli/dig.go new file mode 100644 index 0000000..938eff5 --- /dev/null +++ b/cli/dig.go @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package cli + +import ( + "fmt" + "strings" +) + +// Parse dig-like commands and set the options as such +func ParseDig(arg string, opts *Options) error { + // returns true if the flag starts with a no + isNo := !strings.HasPrefix(arg, "no") + switch arg { + // Set DNS query flags + case "aaflag", "aaonly", "noaaflag", "noaaonly": + opts.AA = isNo + case "adflag", "noadflag": + opts.AD = isNo + case "cdflag", "nocdflag": + opts.CD = isNo + case "qrflag", "noqrflag": + opts.QR = isNo + case "raflag", "noraflag": + opts.RA = isNo + case "rdflag", "recurse", "nordflag", "norecurse": + opts.RD = isNo + case "tcflag", "notcflag": + opts.TC = isNo + case "zflag", "nozflag": + opts.Z = isNo + // End DNS query flags + + case "dnssec", "nodnssec": + opts.DNSSEC = isNo + case "tcp", "vc", "notcp", "novc": + opts.TCP = isNo + case "ignore", "noignore": + // Invert (ignore truncation when true) + opts.Truncate = !isNo + + // Formatting + case "short", "noshort": + opts.Short = isNo + case "json", "nojson": + opts.JSON = isNo + case "xml", "noxml": + opts.XML = isNo + case "yaml", "noyaml": + opts.YAML = isNo + // End formatting + + default: + return fmt.Errorf("dig: Unknown flag given") + } + return nil +} diff --git a/cli/docs.go b/cli/docs.go new file mode 100644 index 0000000..dbc972b --- /dev/null +++ b/cli/docs.go @@ -0,0 +1,5 @@ +/* +The CLI part of the package, including both POSIX +flag parsing and dig-like flag parsing. +*/ +package cli diff --git a/cli/misc.go b/cli/misc.go new file mode 100644 index 0000000..2e2f6d9 --- /dev/null +++ b/cli/misc.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package cli + +import ( + "fmt" + "math/rand" + "strings" + + "git.froth.zone/sam/awl/conf" + "git.froth.zone/sam/awl/internal/structs" + "git.froth.zone/sam/awl/util" + + "github.com/miekg/dns" + "golang.org/x/net/idna" +) + +// Parse the wildcard arguments, drill style +func ParseMiscArgs(args []string, opts *Options) error { + var err error + + for _, arg := range args { + r, ok := dns.StringToType[strings.ToUpper(arg)] + switch { + // If it starts with @, it's a DNS server + case strings.HasPrefix(arg, "@"): + opts.Request.Server = arg[1:] + case strings.Contains(arg, "."): + opts.Query, err = idna.ToASCII(arg) + if err != nil { + return err + } + case ok: + // If it's a DNS request, it's a DNS request (obviously) + opts.Type = r + case strings.HasPrefix(arg, "+"): + // Dig-style +queries + err = ParseDig(strings.ToLower(arg[1:]), opts) + default: + //else, assume it's a name + opts.Query, err = idna.ToASCII(arg) + if err != nil { + return err + } + + } + } + + // If nothing was set, set a default + if opts.Query == "" { + opts.Query = "." + if opts.Type == 0 { + opts.Type = dns.StringToType["NS"] + } + } else { + if opts.Type == 0 { + opts.Type = dns.StringToType["A"] + } + } + if opts.Request.Server == "" { + switch { + case opts.TLS: + opts.Request.Server = "dns.google" + case opts.HTTPS: + opts.Request.Server = "https://dns.cloudflare.com/dns-query" + case opts.QUIC: + opts.Request.Server = "dns.adguard.com" + default: + resolv, err := conf.GetDNSConfig() + if err != nil { + opts.Request.Server = "9.9.9.9" + } else { + for _, srv := range resolv.Servers { + if opts.IPv4 { + if strings.Contains(srv, ".") { + opts.Request.Server = srv + break + } + } else if opts.IPv6 { + if strings.Contains(srv, ":") { + opts.Request.Server = srv + break + } + } else { + //#nosec -- This isn't used for anything secure + opts.Request.Server = resolv.Servers[rand.Intn(len(resolv.Servers))] + break + } + } + } + } + } + + // Make reverse adresses proper addresses + if opts.Reverse { + if dns.TypeToString[opts.Request.Type] == "A" { + opts.Type = dns.StringToType["PTR"] + } + opts.Query, err = util.ReverseDNS(opts.Query, opts.Request.Type) + if err != nil { + return err + } + } + + if opts.Class == 0 { + opts.Class = dns.StringToClass["IN"] + } + + // if the domain is not canonical, make it canonical + if !strings.HasSuffix(opts.Query, ".") { + opts.Query = fmt.Sprintf("%s.", opts.Query) + } + + opts.Request = structs.Request{ + Server: opts.Request.Server, + Type: opts.Type, + Class: opts.Class, + Name: opts.Query, + } + + return nil +} diff --git a/cli/options.go b/cli/options.go new file mode 100644 index 0000000..c1891c1 --- /dev/null +++ b/cli/options.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package cli + +import ( + "git.froth.zone/sam/awl/internal/structs" + "git.froth.zone/sam/awl/logawl" +) + +// CLI options structure +type Options struct { + Logger *logawl.Logger // Logger + Port int // DNS port + Query string // DNS Query + Class uint16 // DNS Class + Type uint16 // DNS Type + IPv4 bool // Force IPv4 + IPv6 bool // Force IPv6 + DNSSEC bool // Enable DNSSEC + TCP bool // Query with TCP + TLS bool // Query over TLS + HTTPS bool // Query over HTTPS + QUIC bool // Query over QUIC + Truncate bool // Ignore truncation + AA bool // Set Authoratative Answer + AD bool // Set Authenticated Data + CD bool // Set CD + QR bool // Set QueRy + RD bool // Set Recursion Desired + RA bool // Set Recursion Available + TC bool // Set TC (TrunCated) + Z bool // Set Z (Zero) + Reverse bool // Make reverse query + Verbosity int // Set logawl verbosity + Short bool // Short output + JSON bool // Outout as JSON + XML bool // Output as XML + YAML bool // Output at YAML + + Request structs.Request +} diff --git a/cli_test.go b/cli_test.go deleted file mode 100644 index ca1fc89..0000000 --- a/cli_test.go +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -package main - -import ( - "os" - "testing" - - "git.froth.zone/sam/awl/query" - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestApp(t *testing.T) { - app := prepareCLI() - // What more can even be done lmao - require.NotNil(t, app) -} - -func TestArgParse(t *testing.T) { - tests := []struct { - in []string - want query.Answers - }{ - { - []string{"@::1", "localhost", "AAAA"}, - query.Answers{Server: "::1", Request: dns.TypeAAAA, Name: "localhost"}, - }, - { - []string{"@1.0.0.1", "google.com"}, - query.Answers{Server: "1.0.0.1", Request: dns.TypeA, Name: "google.com"}, - }, - { - []string{"@8.8.4.4"}, - query.Answers{Server: "8.8.4.4", Request: dns.TypeNS, Name: "."}, - }, - } - for _, test := range tests { - act, err := parseArgs(test.in, query.Options{}) - assert.Nil(t, err) - assert.Equal(t, test.want, act) - } -} - -func TestQuery(t *testing.T) { - app := prepareCLI() - args := os.Args[0:1] - args = append(args, "--Treebug") - err := app.Run(args) - assert.NotNil(t, err) -} - -func TestNoArgs(t *testing.T) { - app := prepareCLI() - args := os.Args[0:1] - args = append(args, "--no-truncate") - err := app.Run(args) - assert.Nil(t, err) -} - -func TestFlags(t *testing.T) { - app := prepareCLI() - args := os.Args[0:1] - args = append(args, "--debug") - args = append(args, "--short") - args = append(args, "-4") - err := app.Run(args) - assert.Nil(t, err) -} - -func TestHTTPS(t *testing.T) { - app := prepareCLI() - args := os.Args[0:1] - args = append(args, "-H") - args = append(args, "@https://cloudflare-dns.com/dns-query") - args = append(args, "git.froth.zone") - err := app.Run(args) - assert.Nil(t, err) -} - -func TestJSON(t *testing.T) { - app := prepareCLI() - args := os.Args[0:1] - args = append(args, "-j") - args = append(args, "git.froth.zone") - err := app.Run(args) - assert.Nil(t, err) -} - -func TestQUIC(t *testing.T) { - app := prepareCLI() - args := os.Args[0:1] - args = append(args, "-Q") - args = append(args, "@dns.adguard.com") - args = append(args, "git.froth.zone") - err := app.Run(args) - assert.Nil(t, err) -} - -func TestReverse(t *testing.T) { - app := prepareCLI() - args := os.Args[0:1] - args = append(args, "-x") - args = append(args, "8.8.8.8") - err := app.Run(args) - assert.Nil(t, err) -} - -func FuzzCli(f *testing.F) { - testcases := []string{"git.froth.zone", "", "!12345", "google.com.edu.org.fr"} - for _, tc := range testcases { - f.Add(tc) - } - - f.Fuzz(func(t *testing.T, orig string) { - app := prepareCLI() - args := os.Args[0:1] - args = append(args, orig) - err := app.Run(args) - if err != nil { - assert.ErrorContains(t, err, "domain must be fully qualified") - } - assert.Nil(t, err) - }) -} diff --git a/conf/plan9_test.go b/conf/plan9_test.go index fa81fd2..573906d 100644 --- a/conf/plan9_test.go +++ b/conf/plan9_test.go @@ -9,6 +9,7 @@ import ( ) func TestGetPlan9Config(t *testing.T) { + t.Parallel() ndbs := []struct { in string want string diff --git a/doc/awl.1 b/doc/awl.1 new file mode 100644 index 0000000..18cbefa --- /dev/null +++ b/doc/awl.1 @@ -0,0 +1,168 @@ +.\" Generated by scdoc 1.11.2 +.\" Complete documentation for this program is not available as a GNU info page +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.nh +.ad l +.\" Begin generated content: +.TH "AWL" "1" "2022-07-20" +.PP +.SH NAME +awl - drill, writ small +.PP +.SH SYNOPSIS +\fBawl\fR [ \fIOPTIONS\fR ] \fIname\fR [ \fI@server\fR ] [ \fItype\fR ] +.br +where +.PP +\fIname\fR is the query to make (\fBexample: froth.\&zone\fR) +.br +\fI@server\fR is the server to query (\fBexample: dns.\&froth.\&zone\fR) +.br +\fItype\fR is the DNS resource type (\fBexample: AAAA\fR) +.PP +.SH DESCRIPTION +.PP +\fBawl\fR (\fBa\fRwls \fBw\fRant \fBl\fRicorice) is a simple tool designed to make DNS queries, much like the venerable \fIdig\fR(1).\& +An awl is a tool used to make small holes, typically used in tannery (leatherworking).\& +.PP +Written in Go, \fBawl\fR is designed to be a more "modern" version of \fIdrill\fR(1) by including some more RFCs and output options.\& +\fBawl\fR is still heavily Work-In-Progress so some features may get added or removed.\& +.PP +.SH OPTIONS +.RS 4 +\fB-D\fR, \fB--dnssec\fR +.br + Enable DNSSEC.\& This needs to be manually enabled.\& +.PP +\fB--debug\fR +.br + Enable debug logging (currently WIP).\& +.PP +\fB-v\fR +.br + Print the version and exit.\& +.PP +\fB-h\fR +.br + Show a "short" help message.\& +.PP +.RE +.SS Query Options +.RS 4 +\fB-4\fR +.br + Only make query over IPv4 +.PP +\fB-6\fR +.br + Only make query over IPv6 +.PP +\fB-p\fR, \fB--port\fR \fIport\fR +.br + Sets the port to query.\& +.br + +.br +\fIDefault Ports\fR: +.RS 4 +.PD 0 +.IP \(bu 4 +\fI53\fR for \fBUDP\fR and \fBTCP\fR +.IP \(bu 4 +\fI853\fR for \fBTLS\fR and \fBQUIC\fR +.IP \(bu 4 +\fI443\fR for \fBHTTPS\fR +.PD +.PP +.RE +\fB--no-truncate\fR +.br + Ignore UDP truncation (by default, awl \fIretries with TCP\fR) +.PP +\fB-t\fR, \fB--tcp\fR +.br + Use TCP for the query (see \fIRFC 7766\fR) +.PP +\fB-u\fR, \fB--udp\fR +.br + Use UDP for the query (default) +.PP +\fB-T\fR, \fB--tls\fR +.br + Use DNS-over-TLS, implies \fB-t\fR (see \fIRFC 7858\fR) +.PP +\fB-H\fR.\& \fB--https\fR +.br + Use DNS-over-HTTPS (see \fIRFC 8484\fR) +.PP +\fB-Q\fR.\& \fB--quic\fR +.br + Use DNS-over-QUIC (see \fIRFC 9250\fR) +.PP +\fB-x\fR, \fB--reverse\fR +.br + Do a reverse lookup.\& Sets default \fItype\fR to PTR.\& +.br + \fBawl\fR automatically makes an IP or phone number canonical.\& +.PP +.RE +.SS DNS Flags +.PP +.RS 4 +\fB--aa\fR +.br + \fISET\fR Authoritative Answer (default: unset) +.PP +\fB--ad\fR +.br + \fISET\fR Authenticated Data (default: unset) +.PP +\fB--tc\fR +.br + \fISET\fR TC (TrunCated) flag (default: not set) +.PP +\fB-z\fR +.br + \fISET\fR Z (Zero) flag (default: not set) +.PP +\fB--cd\fR +.br + \fISET\fR CD (Checking Disabled) flag (default: not set) +.PP +\fB--no-qr\fR +.br + \fIUNSET\fR QR (QueRy) flag (default: set) +.PP +\fB--no-rd\fR +.br + \fIUNSET\fR RD (Recursion Desired) flag (default: set) +.PP +\fB--no-ra\fR +.br + \fIUNSET\fR RA (Recursion Available) flag (default: set) +.PP +.RE +.SS Output Options +.RS 4 +\fB-j\fR, \fB--json\fR +.br + Print the query results as JSON.\& +.PP +\fB-X\fR, \fB--xml\fR +.br + Print the query results as XML.\& +.PP +\fB-y\fR, \fB--yaml\fR +.br + Print the query results as YAML.\& +.PP +\fB-s\fR, \fB--short\fR +.br + Print just the results.\& +.br + Equivalent to \fBdig +short\fR +.PP +.RE +.SH SEE ALSO +\fIdrill\fR(1), \fIdig\fR(1), the many DNS RFCs \ No newline at end of file diff --git a/doc/awl.1.md b/doc/awl.1.md new file mode 100644 index 0000000..46f3125 --- /dev/null +++ b/doc/awl.1.md @@ -0,0 +1,115 @@ +AWL(1) + +# NAME +awl - drill, writ small + +# SYNOPSIS +*awl* [ _OPTIONS_ ] _name_ [ _@server_ ] [ _type_ ]++ +where + +_name_ is the query to make (*example: froth.zone*)++ +_@server_ is the server to query (*example: dns.froth.zone*)++ +_type_ is the DNS resource type (*example: AAAA*) + +# DESCRIPTION + +*awl* (*a*wls *w*ant *l*icorice) is a simple tool designed to make DNS queries, much like the venerable _dig_(1). +An awl is a tool used to make small holes, typically used in tannery (leatherworking). + +Written in Go, *awl* is designed to be a more "modern" version of _drill_(1) by including some more RFCs and output options. +*awl* is still heavily Work-In-Progress so some features may get added or removed. + +# OPTIONS + _Dig-like queries are supported, see dig(1)_ + + *-D*, *--dnssec*++ + Enable DNSSEC. This needs to be manually enabled. + + *--debug*++ + Enable debug logging (currently WIP). + + *-v*++ + Print the version and exit. + + *-h*++ + Show a "short" help message. + +## Query Options + *-4*++ + Only make query over IPv4 + + *-6*++ + Only make query over IPv6 + + *-p*, *--port* _port_++ + Sets the port to query.++ + ++ +_Default Ports_: + - _53_ for *UDP* and *TCP* + - _853_ for *TLS* and *QUIC* + - _443_ for *HTTPS* + + *--no-truncate*++ + Ignore UDP truncation (by default, awl _retries with TCP_) + + *-t*, *--tcp*++ + Use TCP for the query (see _RFC 7766_) + + *-u*, *--udp*++ + Use UDP for the query (default) + + *-T*, *--tls*++ + Use DNS-over-TLS, implies *-t* (see _RFC 7858_) + + *-H*. *--https*++ + Use DNS-over-HTTPS (see _RFC 8484_) + + *-Q*. *--quic*++ + Use DNS-over-QUIC (see _RFC 9250_) + + *-x*, *--reverse*++ + Do a reverse lookup. Sets default _type_ to PTR.++ + *awl* automatically makes an IP or phone number canonical. + +## DNS Flags + + *--aa*++ + _SET_ Authoritative Answer (default: unset) + + *--ad*++ + _SET_ Authenticated Data (default: unset) + + *--tc*++ + _SET_ TC (TrunCated) flag (default: not set) + + *-z*++ + _SET_ Z (Zero) flag (default: not set) + + *--cd*++ + _SET_ CD (Checking Disabled) flag (default: not set) + + *--no-qr*++ + _UNSET_ QR (QueRy) flag (default: set) + + *--no-rd*++ + _UNSET_ RD (Recursion Desired) flag (default: set) + + *--no-ra*++ + _UNSET_ RA (Recursion Available) flag (default: set) + +## Output Options + *-j*, *--json*++ + Print the query results as JSON. + + *-X*, *--xml*++ + Print the query results as XML. + + *-y*, *--yaml*++ + Print the query results as YAML. + + *-s*, *--short*++ + Print just the results.++ + Equivalent to *dig +short* + +# SEE ALSO +_drill_(1), _dig_(1), the many DNS RFCs \ No newline at end of file diff --git a/go.mod b/go.mod index 1596228..412acf0 100644 --- a/go.mod +++ b/go.mod @@ -5,19 +5,20 @@ go 1.18 require ( github.com/lucas-clemente/quic-go v0.28.0 github.com/miekg/dns v1.1.50 - github.com/urfave/cli/v2 v2.11.0 + github.com/stefansundin/go-zflag v1.1.1 golang.org/x/net v0.0.0-20220708220712-1185a9018129 + gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/cheekybits/genny v1.0.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect @@ -25,9 +26,7 @@ require ( github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/stretchr/testify v1.8.0 - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 diff --git a/go.sum b/go.sum index 0753914..5442a9b 100644 --- a/go.sum +++ b/go.sum @@ -8,7 +8,6 @@ dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1 dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= @@ -17,8 +16,6 @@ github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitf github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -79,8 +76,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lucas-clemente/quic-go v0.27.2 h1:zsMwwniyybb8B/UDNXRSYee7WpQJVOcjQEGgpw2ikXs= -github.com/lucas-clemente/quic-go v0.27.2/go.mod h1:vXgO/11FBSKM+js1NxoaQ/bPtVFYfB7uxhfHXyMhl1A= github.com/lucas-clemente/quic-go v0.28.0 h1:9eXVRgIkMQQyiyorz/dAaOYIx3TFzXsIFkNFz4cxuJM= github.com/lucas-clemente/quic-go v0.28.0/go.mod h1:oGz5DKK41cJt5+773+BSO9BXDsREY4HLf7+0odGAPO0= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= @@ -124,10 +119,7 @@ github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= @@ -153,6 +145,8 @@ github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYED github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stefansundin/go-zflag v1.1.1 h1:XabhzWS588bVvV1z1UctSa6i8zHkXc5W9otqtnDSHw8= +github.com/stefansundin/go-zflag v1.1.1/go.mod h1:HXX5rABl1AoTcZ2jw+CqJ7R8irczaLquGNZlFabZooc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -161,17 +155,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/urfave/cli/v2 v2.10.3 h1:oi571Fxz5aHugfBAJd5nkwSk3fzATXtMlpxdLylSCMo= -github.com/urfave/cli/v2 v2.10.3/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= -github.com/urfave/cli/v2 v2.11.0 h1:c6bD90aLd2iEsokxhxkY5Er0zA2V9fId2aJfwmrF+do= -github.com/urfave/cli/v2 v2.11.0/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= @@ -181,7 +168,6 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -206,18 +192,10 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw= -golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 h1:8NSylCMxLW4JvserAndSgFL7aPli6A68yf0bYFTcWCM= -golang.org/x/net v0.0.0-20220706163947-c90051bbdb60/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220708220712-1185a9018129 h1:vucSRfWwTsoXro7P+3Cjlr6flUMtzCwzlvkxEQtHHB0= golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -253,21 +231,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= -golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220701225701-179beb0bd1a1 h1:+Lm8wRwJpsVpTHuM4tHTwgxjPzv/bjxsHt2cW5EY7XU= -golang.org/x/sys v0.0.0-20220701225701-179beb0bd1a1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220702020025-31831981b65f h1:xdsejrW/0Wf2diT5CPp3XmKUNbr7Xvw8kYilQ+6qjRY= -golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d h1:/m5NbqQelATgoSPVC2Z23sR4kVNokFwDDyWh/3rGY+I= -golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e h1:NHvCuwuS43lGnYhten69ZWqi2QOj/CiDNcKbVqwVoew= -golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/internal/structs/query.go b/internal/structs/query.go new file mode 100644 index 0000000..eb743be --- /dev/null +++ b/internal/structs/query.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package structs + +import ( + "time" + + "github.com/miekg/dns" +) + +// The DNS response +type Response struct { + DNS *dns.Msg // The full DNS response + RTT time.Duration `json:"rtt"` // The time it took to make the DNS query +} + +// A structure for a DNS query +type Request struct { + Server string `json:"server"` // The server to make the DNS request from + Type uint16 `json:"request"` // The type of request + Class uint16 `json:"class"` // DNS Class + Name string `json:"name"` // The domain name to make a DNS request for +} diff --git a/logawl/logger.go b/logawl/logger.go index 607f516..6376a7b 100644 --- a/logawl/logger.go +++ b/logawl/logger.go @@ -28,15 +28,27 @@ func (l *Logger) Println(level Level, v ...any) { if l.IsLevel(level) { switch level { //Goes through log levels and does stuff based on them (Fatal os.Exit...etc) case 0: - l.Printer(0, fmt.Sprintln(v...)) //Fatal level + err := l.Printer(0, fmt.Sprintln(v...)) //Fatal level + if err != nil { + panic(err) + } os.Exit(1) case 1: - l.Printer(1, fmt.Sprintln(v...)) //Error level + err := l.Printer(1, fmt.Sprintln(v...)) //Error level + if err != nil { + panic(err) + } os.Exit(2) case 2: - l.Printer(2, fmt.Sprintln(v...)) //Info level + err := l.Printer(2, fmt.Sprintln(v...)) //Info level + if err != nil { + panic(err) + } case 3: - l.Printer(3, fmt.Sprintln(v...)) //Debug level + err := l.Printer(3, fmt.Sprintln(v...)) //Debug level + if err != nil { + panic(err) + } default: break } @@ -44,7 +56,7 @@ func (l *Logger) Println(level Level, v ...any) { } // Formats the log header as such YYYY/MM/DD HH:MM:SS (local time) -func (l *Logger) formatHeader(buf *[]byte, t time.Time, line int, level Level) error { +func (l *Logger) FormatHeader(buf *[]byte, t time.Time, line int, level Level) error { if lvl, err := l.UnMarshalLevel(level); err == nil { // This is ugly but functional // maybe there can be an append func or something in the future @@ -80,7 +92,7 @@ func (l *Logger) Printer(level Level, s string) error { defer l.Mu.Unlock() l.buf = l.buf[:0] - l.formatHeader(&l.buf, now, line, level) + l.FormatHeader(&l.buf, now, line, level) l.buf = append(l.buf, s...) if len(s) == 0 || s[len(s)-1] != '\n' { l.buf = append(l.buf, '\n') diff --git a/logawl/logging_test.go b/logawl/logging_test.go index 606cd5c..9b187dc 100644 --- a/logawl/logging_test.go +++ b/logawl/logging_test.go @@ -1,52 +1,59 @@ -package logawl +// SPDX-License-Identifier: BSD-3-Clause + +package logawl_test import ( "bytes" "testing" "time" + "git.froth.zone/sam/awl/logawl" + "github.com/stretchr/testify/assert" ) -var logger = New() +var logger = logawl.New() func TestLogawl(t *testing.T) { + t.Parallel() - assert.Equal(t, Level(2), logger.Level) //cast 2 (int) to 2 (level) + assert.Equal(t, logawl.Level(2), logger.Level) //cast 2 (int) to 2 (level) //Validate setting and getting levels from memory works - for i := range AllLevels { - logger.SetLevel(Level(i)) - assert.Equal(t, Level(i), logger.GetLevel()) + for i := range logawl.AllLevels { + logger.SetLevel(logawl.Level(i)) + assert.Equal(t, logawl.Level(i), logger.GetLevel()) } } func TestUnmarshalLevels(t *testing.T) { + t.Parallel() m := make(map[int]string) var err error //Fill map with unmarshalled level info - for i := range AllLevels { - m[i], err = logger.UnMarshalLevel(Level(i)) + for i := range logawl.AllLevels { + m[i], err = logger.UnMarshalLevel(logawl.Level(i)) assert.Nil(t, err) } //iterate over map and assert equal - for i := range AllLevels { - lv, err := logger.UnMarshalLevel(Level(i)) + for i := range logawl.AllLevels { + lv, err := logger.UnMarshalLevel(logawl.Level(i)) assert.Nil(t, err) assert.Equal(t, m[i], lv) } - lv, err := logger.UnMarshalLevel(Level(9001)) + lv, err := logger.UnMarshalLevel(logawl.Level(9001)) assert.NotNil(t, err) assert.Equal(t, "", lv) assert.ErrorContains(t, err, "invalid log level choice") } func TestLogger(t *testing.T) { + t.Parallel() - for i := range AllLevels { + for i := range logawl.AllLevels { // only test non-exiting log levels switch i { case 1: @@ -76,8 +83,9 @@ func TestLogger(t *testing.T) { } func TestFmt(t *testing.T) { + t.Parallel() ti := time.Now() test := []byte("test") - assert.NotNil(t, logger.formatHeader(&test, ti, 0, Level(9001))) //make sure error is error + assert.NotNil(t, logger.FormatHeader(&test, ti, 0, logawl.Level(9001))) //make sure error is error } diff --git a/main.go b/main.go new file mode 100644 index 0000000..991be6b --- /dev/null +++ b/main.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package main + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "os" + "strings" + "time" + + "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/query" + + "gopkg.in/yaml.v2" +) + +var version = "DEV" + +func main() { + opts, err := cli.ParseCLI(version) + if err != nil { + opts.Logger.Fatal(err) + os.Exit(1) + } + resp, err := query.CreateQuery(opts) + if err != nil { + opts.Logger.Fatal(err) + os.Exit(1) + } + switch { + case opts.JSON: + json, err := json.MarshalIndent(resp.DNS, "", " ") + if err != nil { + opts.Logger.Fatal(err) + os.Exit(1) + } + fmt.Println(string(json)) + case opts.XML: + xml, err := xml.MarshalIndent(resp.DNS, "", " ") + if err != nil { + opts.Logger.Fatal(err) + os.Exit(1) + } + fmt.Println(string(xml)) + case opts.YAML: + yaml, err := yaml.Marshal(resp.DNS) + if err != nil { + opts.Logger.Fatal(err) + os.Exit(1) + } + fmt.Println(string(yaml)) + default: + if !opts.Short { + // Print everything + fmt.Println(resp.DNS) + fmt.Println(";; Query time:", resp.RTT) + fmt.Println(";; SERVER:", opts.Request.Server) + fmt.Println(";; WHEN:", time.Now().Format(time.RFC1123Z)) + fmt.Println(";; MSG SIZE rcvd:", resp.DNS.Len()) + } else { + // Print just the responses, nothing else + for _, res := range resp.DNS.Answer { + temp := strings.Split(res.String(), "\t") + fmt.Println(temp[len(temp)-1]) + } + } + } + +} diff --git a/query.go b/query.go deleted file mode 100644 index e90f933..0000000 --- a/query.go +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -package main - -import ( - "encoding/json" - "fmt" - "net" - "strconv" - "strings" - "time" - - "git.froth.zone/sam/awl/query" - "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" - "github.com/urfave/cli/v2" -) - -func doQuery(c *cli.Context) error { - var ( - err error - ) - // load cli flags into options struct - Options := query.Options{ - Logger: util.InitLogger(c.Bool("debug")), - Port: c.Int("port"), - IPv4: c.Bool("4"), - IPv6: c.Bool("6"), - DNSSEC: c.Bool("dnssec"), - Short: c.Bool("short"), - TCP: c.Bool("tcp"), - TLS: c.Bool("tls"), - HTTPS: c.Bool("https"), - QUIC: c.Bool("quic"), - Truncate: c.Bool("no-truncate"), - AA: c.Bool("aa"), - TC: c.Bool("tc"), - Z: c.Bool("z"), - CD: c.Bool("cd"), - NoRD: c.Bool("no-rd"), - NoRA: c.Bool("no-ra"), - Reverse: c.Bool("reverse"), - Debug: c.Bool("debug"), - } - Options.Answers, err = parseArgs(c.Args().Slice(), Options) - if err != nil { - Options.Logger.Error("Unable to parse args") - return err - } - msg := new(dns.Msg) - - if Options.Reverse { - if dns.TypeToString[Options.Answers.Request] == "A" { - Options.Answers.Request = dns.StringToType["PTR"] - } - Options.Answers.Name, err = util.ReverseDNS(Options.Answers.Name, Options.Answers.Request) - if err != nil { - return err - } - } - - // if the domain is not canonical, make it canonical - if !strings.HasSuffix(Options.Answers.Name, ".") { - Options.Answers.Name = fmt.Sprintf("%s.", Options.Answers.Name) - } - msg.SetQuestion(Options.Answers.Name, Options.Answers.Request) - // If port is not set, set it - if Options.Port == 0 { - if Options.TLS || Options.QUIC { - Options.Port = 853 - } else { - Options.Port = 53 - } - } - Options.Logger.Debug("setting any message flags") - // Make this authoritative (does this do anything?) - if Options.AA { - Options.Logger.Debug("making message authorative") - msg.Authoritative = true - } - // Set truncated flag (why) - if Options.TC { - msg.Truncated = true - } - // Set the zero flag if requested (does nothing) - if Options.Z { - Options.Logger.Debug("setting to zero") - msg.Zero = true - } - // Disable DNSSEC validation - if Options.CD { - Options.Logger.Debug("disabling DNSSEC validation") - msg.CheckingDisabled = true - } - // Disable wanting recursion - if Options.NoRD { - Options.Logger.Debug("disabling recursion") - msg.RecursionDesired = false - } - // Disable recursion being available (I don't think this does anything) - if Options.NoRA { - msg.RecursionAvailable = false - } - // Set DNSSEC if requested - if Options.DNSSEC { - Options.Logger.Debug("using DNSSEC") - msg.SetEdns0(1232, true) - } - - resolver, err := query.LoadResolver(Options.Answers.Server, Options) - if err != nil { - return err - } - - if Options.Debug { - Options.Logger.SetLevel(3) - } - - Options.Logger.Debug("Starting awl") - - var in = Options.Answers.DNS - - // Make the DNS request - if Options.HTTPS { - in, Options.Answers.RTT, err = resolver.LookUp(msg) - } else if Options.QUIC { - in, Options.Answers.RTT, err = resolver.LookUp(msg) - } else { - Options.Answers.Server = net.JoinHostPort(Options.Answers.Server, strconv.Itoa(Options.Port)) - d := new(dns.Client) - - // Set TCP/UDP, depending on flags - if Options.TCP || Options.TLS { - d.Net = "tcp" - } else { - Options.Logger.Debug("using udp") - d.Net = "udp" - } - - // Set IPv4 or IPv6, depending on flags - switch { - case Options.IPv4: - d.Net += "4" - case Options.IPv6: - d.Net += "6" - } - - // Add TLS, if requested - if Options.TLS { - d.Net += "-tls" - } - - in, Options.Answers.RTT, err = d.Exchange(msg, Options.Answers.Server) - if err != nil { - return err - } - // If UDP truncates, use TCP instead (unless truncation is to be ignored) - if in.MsgHdr.Truncated && !Options.Truncate { - fmt.Printf(";; Truncated, retrying with TCP\n\n") - d.Net = "tcp" - switch { - case Options.IPv4: - d.Net += "4" - case Options.IPv4: - d.Net += "6" - } - in, Options.Answers.RTT, err = d.Exchange(msg, Options.Answers.Server) - } - } - - if err != nil { - return err - } - - if c.Bool("json") { - json, err := json.MarshalIndent(in, "", " ") - if err != nil { - return err - } - fmt.Println(string(json)) - } else { - if !c.Bool("short") { - // Print everything - fmt.Println(in) - fmt.Println(";; Query time:", Options.Answers.RTT) - fmt.Println(";; SERVER:", Options.Answers.Server) - fmt.Println(";; WHEN:", time.Now().Format(time.RFC1123Z)) - fmt.Println(";; MSG SIZE rcvd:", in.Len()) - } else { - // Print just the responses, nothing else - for _, res := range in.Answer { - temp := strings.Split(res.String(), "\t") - fmt.Println(temp[len(temp)-1]) - } - } - } - - return nil -} diff --git a/query/HTTPS.go b/query/HTTPS.go index f72470c..df0bc6b 100644 --- a/query/HTTPS.go +++ b/query/HTTPS.go @@ -9,53 +9,55 @@ import ( "net/http" "time" + "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/internal/structs" "github.com/miekg/dns" ) type HTTPSResolver struct { server string - opts Options + opts cli.Options } -func (r *HTTPSResolver) LookUp(msg *dns.Msg) (*dns.Msg, time.Duration, error) { - var resp Response +func (r *HTTPSResolver) LookUp(msg *dns.Msg) (structs.Response, error) { + var resp structs.Response httpR := &http.Client{} buf, err := msg.Pack() if err != nil { - return nil, 0, err + return structs.Response{}, err } r.opts.Logger.Debug("making DoH request") // query := server + "?dns=" + base64.RawURLEncoding.EncodeToString(buf) req, err := http.NewRequest("POST", r.server, bytes.NewBuffer(buf)) if err != nil { - return nil, 0, fmt.Errorf("DoH: %s", err.Error()) + return structs.Response{}, fmt.Errorf("DoH: %s", err.Error()) } req.Header.Set("Content-Type", "application/dns-message") req.Header.Set("Accept", "application/dns-message") now := time.Now() res, err := httpR.Do(req) - resp.Answers.RTT = time.Since(now) + resp.RTT = time.Since(now) if err != nil { - return nil, 0, fmt.Errorf("DoH HTTP request error: %s", err.Error()) + return structs.Response{}, fmt.Errorf("DoH HTTP request error: %s", err.Error()) } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, 0, fmt.Errorf("DoH server responded with HTTP %d", res.StatusCode) + return structs.Response{}, fmt.Errorf("DoH server responded with HTTP %d", res.StatusCode) } fullRes, err := io.ReadAll(res.Body) if err != nil { - return nil, 0, fmt.Errorf("DoH body read error: %s", err.Error()) + return structs.Response{}, fmt.Errorf("DoH body read error: %s", err.Error()) } - resp.DNS = dns.Msg{} + resp.DNS = &dns.Msg{} r.opts.Logger.Debug("unpacking response") err = resp.DNS.Unpack(fullRes) if err != nil { - return nil, 0, fmt.Errorf("DoH dns message unpack error: %s", err.Error()) + return structs.Response{}, fmt.Errorf("DoH dns message unpack error: %s", err.Error()) } - return &resp.DNS, resp.Answers.RTT, nil + return resp, nil } diff --git a/query/HTTPS_test.go b/query/HTTPS_test.go new file mode 100644 index 0000000..8309554 --- /dev/null +++ b/query/HTTPS_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package query_test + +import ( + "fmt" + "strings" + "testing" + + "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/internal/structs" + "git.froth.zone/sam/awl/query" + "git.froth.zone/sam/awl/util" + + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" +) + +func TestResolveHTTPS(t *testing.T) { + t.Parallel() + var err error + opts := cli.Options{ + HTTPS: true, + Logger: util.InitLogger(0), + } + testCase := structs.Request{Server: "dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone"} + resolver, err := query.LoadResolver(testCase.Server, opts) + assert.Nil(t, err) + + if !strings.HasPrefix(testCase.Server, "https://") { + testCase.Server = "https://" + testCase.Server + } + // if the domain is not canonical, make it canonical + if !strings.HasSuffix(testCase.Name, ".") { + testCase.Name = fmt.Sprintf("%s.", testCase.Name) + } + + msg := new(dns.Msg) + msg.SetQuestion(testCase.Name, testCase.Type) + msg = msg.SetQuestion(testCase.Name, testCase.Type) + res, err := resolver.LookUp(msg) + assert.Nil(t, err) + assert.NotNil(t, res) + +} + +func Test2ResolveHTTPS(t *testing.T) { + t.Parallel() + opts := cli.Options{ + HTTPS: true, + Logger: util.InitLogger(0), + } + var err error + testCase := structs.Request{Server: "dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone"} + resolver, err := query.LoadResolver(testCase.Server, opts) + assert.Nil(t, err) + msg := new(dns.Msg) + msg.SetQuestion(testCase.Name, testCase.Type) + msg = msg.SetQuestion(testCase.Name, testCase.Type) + res, err := resolver.LookUp(msg) + assert.Error(t, err) + assert.Equal(t, res, structs.Response{}) + +} +func Test3ResolveHTTPS(t *testing.T) { + t.Parallel() + opts := cli.Options{ + HTTPS: true, + Logger: util.InitLogger(0), + } + var err error + testCase := structs.Request{Server: "dns9..quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."} + if !strings.HasPrefix(testCase.Server, "https://") { + testCase.Server = "https://" + testCase.Server + } + // if the domain is not canonical, make it canonical + if !strings.HasSuffix(testCase.Name, ".") { + testCase.Name = fmt.Sprintf("%s.", testCase.Name) + } + resolver, err := query.LoadResolver(testCase.Server, opts) + assert.Nil(t, err) + msg := new(dns.Msg) + msg.SetQuestion(testCase.Name, testCase.Type) + msg = msg.SetQuestion(testCase.Name, testCase.Type) + res, err := resolver.LookUp(msg) + assert.Error(t, err) + assert.Equal(t, res, structs.Response{}) + +} diff --git a/query/QUIC.go b/query/QUIC.go index 98f29f2..3026cee 100644 --- a/query/QUIC.go +++ b/query/QUIC.go @@ -7,63 +7,67 @@ import ( "io" "time" + "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/internal/structs" + "github.com/lucas-clemente/quic-go" "github.com/miekg/dns" ) type QUICResolver struct { server string - opts Options + opts cli.Options } -func (r *QUICResolver) LookUp(msg *dns.Msg) (*dns.Msg, time.Duration, error) { - var resp Response +func (r *QUICResolver) LookUp(msg *dns.Msg) (structs.Response, error) { + var resp structs.Response tls := &tls.Config{ + MinVersion: tls.VersionTLS12, NextProtos: []string{"doq"}, } r.opts.Logger.Debug("making DoQ request") connection, err := quic.DialAddr(r.server, tls, nil) if err != nil { - return nil, 0, err + return structs.Response{}, err } // Compress request to over-the-wire buf, err := msg.Pack() if err != nil { - return nil, 0, err + return structs.Response{}, err } t := time.Now() stream, err := connection.OpenStream() if err != nil { - return nil, 0, err + return structs.Response{}, err } _, err = stream.Write(buf) if err != nil { - return nil, 0, err + return structs.Response{}, err } fullRes, err := io.ReadAll(stream) if err != nil { - return nil, 0, err + return structs.Response{}, err } - resp.Answers.RTT = time.Since(t) + resp.RTT = time.Since(t) // Close with error: no error err = connection.CloseWithError(0, "") if err != nil { - return nil, 0, err + return structs.Response{}, err } err = stream.Close() if err != nil { - return nil, 0, err + return structs.Response{}, err } - resp.DNS = dns.Msg{} + resp.DNS = &dns.Msg{} r.opts.Logger.Debug("unpacking DoQ response") err = resp.DNS.Unpack(fullRes) if err != nil { - return nil, 0, err + return structs.Response{}, err } - return &resp.DNS, resp.Answers.RTT, nil + return resp, nil } diff --git a/query/QUIC_test.go b/query/QUIC_test.go new file mode 100644 index 0000000..bc765f7 --- /dev/null +++ b/query/QUIC_test.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package query_test + +import ( + "fmt" + "net" + "strconv" + "strings" + "testing" + + "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/internal/structs" + "git.froth.zone/sam/awl/query" + "git.froth.zone/sam/awl/util" + + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" +) + +func TestQuic(t *testing.T) { + t.Parallel() + opts := cli.Options{ + QUIC: true, + Logger: util.InitLogger(0), + Port: 853, + Request: structs.Request{Server: "dns.adguard.com"}, + } + testCase := structs.Request{Server: "dns.//./,,adguard.com", Type: dns.TypeA, Name: "git.froth.zone"} + testCase2 := structs.Request{Server: "dns.adguard.com", Type: dns.TypeA, Name: "git.froth.zone"} + var testCases []structs.Request + testCases = append(testCases, testCase) + testCases = append(testCases, testCase2) + for i := range testCases { + switch i { + case 0: + resolver, err := query.LoadResolver(testCases[i].Server, opts) + assert.Nil(t, err) + // if the domain is not canonical, make it canonical + if !strings.HasSuffix(testCase.Name, ".") { + testCases[i].Name = fmt.Sprintf("%s.", testCases[i].Name) + } + msg := new(dns.Msg) + msg.SetQuestion(testCase.Name, testCase.Type) + msg = msg.SetQuestion(testCase.Name, testCase.Type) + res, err := resolver.LookUp(msg) + assert.NotNil(t, err) + assert.Equal(t, res, structs.Response{}) + case 1: + resolver, err := query.LoadResolver(testCase2.Server, opts) + assert.Nil(t, err) + testCase2.Server = net.JoinHostPort(testCase2.Server, strconv.Itoa(opts.Port)) + // if the domain is not canonical, make it canonical + if !strings.HasSuffix(testCase2.Name, ".") { + testCase2.Name = fmt.Sprintf("%s.", testCase2.Name) + } + msg := new(dns.Msg) + msg.SetQuestion(testCase2.Name, testCase2.Type) + msg = msg.SetQuestion(testCase2.Name, testCase2.Type) + res, err := resolver.LookUp(msg) + assert.Nil(t, err) + assert.NotNil(t, res) + } + + } + +} diff --git a/query/general.go b/query/general.go new file mode 100644 index 0000000..b92783a --- /dev/null +++ b/query/general.go @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package query + +import ( + "fmt" + + "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/internal/structs" + + "github.com/miekg/dns" +) + +type StandardResolver struct { + server string + opts cli.Options +} + +func (r *StandardResolver) LookUp(msg *dns.Msg) (structs.Response, error) { + var ( + resp structs.Response + err error + ) + dnsClient := new(dns.Client) + if r.opts.TCP || r.opts.TLS { + dnsClient.Net = "tcp" + } else { + dnsClient.Net = "udp" + } + + switch { + case r.opts.IPv4: + dnsClient.Net += "4" + case r.opts.IPv6: + dnsClient.Net += "6" + } + + if r.opts.TLS { + dnsClient.Net += "-tls" + } + + resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, r.server) + if err != nil { + return structs.Response{}, err + } + + if resp.DNS.MsgHdr.Truncated && !r.opts.Truncate { + fmt.Printf(";; Truncated, retrying with TCP\n\n") + dnsClient.Net = "tcp" + switch { + case r.opts.IPv4: + dnsClient.Net += "4" + case r.opts.IPv4: + dnsClient.Net += "6" + } + resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, r.server) + } + + return resp, err +} diff --git a/query/query.go b/query/query.go new file mode 100644 index 0000000..4421bd9 --- /dev/null +++ b/query/query.go @@ -0,0 +1,34 @@ +package query + +import ( + "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/internal/structs" + + "github.com/miekg/dns" +) + +func CreateQuery(opts cli.Options) (structs.Response, error) { + var res structs.Response + res.DNS = new(dns.Msg) + res.DNS.SetQuestion(opts.Query, opts.Type) + + res.DNS.MsgHdr.Response = opts.QR + res.DNS.MsgHdr.Authoritative = opts.AA + res.DNS.MsgHdr.Truncated = opts.TC + res.DNS.MsgHdr.RecursionDesired = opts.RD + res.DNS.MsgHdr.RecursionAvailable = opts.RA + res.DNS.MsgHdr.Zero = opts.Z + res.DNS.MsgHdr.AuthenticatedData = opts.AD + res.DNS.MsgHdr.CheckingDisabled = opts.CD + + if opts.DNSSEC { + res.DNS.SetEdns0(1232, true) + } + + resolver, err := LoadResolver(opts.Request.Server, opts) + if err != nil { + return structs.Response{}, err + } + + return resolver.LookUp(res.DNS) +} diff --git a/query/query_test.go b/query/query_test.go deleted file mode 100644 index e9591dd..0000000 --- a/query/query_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package query - -import ( - "fmt" - "net" - "strconv" - "strings" - "testing" - - "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" - "github.com/stretchr/testify/assert" -) - -func TestResolveHTTPS(t *testing.T) { - var err error - opts := Options{ - HTTPS: true, - Logger: util.InitLogger(false), - } - testCase := Answers{Server: "dns9.quad9.net/dns-query", Request: dns.TypeA, Name: "git.froth.zone"} - resolver, err := LoadResolver(testCase.Server, opts) - - if !strings.HasPrefix(testCase.Server, "https://") { - testCase.Server = "https://" + testCase.Server - } - // if the domain is not canonical, make it canonical - if !strings.HasSuffix(testCase.Name, ".") { - testCase.Name = fmt.Sprintf("%s.", testCase.Name) - } - - msg := new(dns.Msg) - msg.SetQuestion(testCase.Name, testCase.Request) - msg = msg.SetQuestion(testCase.Name, testCase.Request) - var in *dns.Msg - in, testCase.RTT, err = resolver.LookUp(msg) - assert.Nil(t, err) - assert.NotNil(t, in) - -} - -func Test2ResolveHTTPS(t *testing.T) { - opts := Options{ - HTTPS: true, - Logger: util.InitLogger(false), - } - var err error - testCase := Answers{Server: "dns9.quad9.net/dns-query", Request: dns.TypeA, Name: "git.froth.zone"} - resolver, err := LoadResolver(testCase.Server, opts) - msg := new(dns.Msg) - msg.SetQuestion(testCase.Name, testCase.Request) - msg = msg.SetQuestion(testCase.Name, testCase.Request) - var in *dns.Msg - in, testCase.RTT, err = resolver.LookUp(msg) - assert.NotNil(t, err) - assert.Nil(t, in) - -} -func Test3ResolveHTTPS(t *testing.T) { - opts := Options{ - HTTPS: true, - Logger: util.InitLogger(false), - } - var err error - testCase := Answers{Server: "dns9..quad9.net/dns-query", Request: dns.TypeA, Name: "git.froth.zone."} - if !strings.HasPrefix(testCase.Server, "https://") { - testCase.Server = "https://" + testCase.Server - } - // if the domain is not canonical, make it canonical - if !strings.HasSuffix(testCase.Name, ".") { - testCase.Name = fmt.Sprintf("%s.", testCase.Name) - } - resolver, err := LoadResolver(testCase.Server, opts) - msg := new(dns.Msg) - msg.SetQuestion(testCase.Name, testCase.Request) - msg = msg.SetQuestion(testCase.Name, testCase.Request) - var in *dns.Msg - in, testCase.RTT, err = resolver.LookUp(msg) - assert.NotNil(t, err) - assert.Nil(t, in) - -} - -func TestQuic(t *testing.T) { - opts := Options{ - QUIC: true, - Logger: util.InitLogger(false), - Port: 853, - Answers: Answers{Server: "dns.adguard.com"}, - } - testCase := Answers{Server: "dns.//./,,adguard.com", Request: dns.TypeA, Name: "git.froth.zone"} - testCase2 := Answers{Server: "dns.adguard.com", Request: dns.TypeA, Name: "git.froth.zone"} - var testCases []Answers - testCases = append(testCases, testCase) - testCases = append(testCases, testCase2) - for i := range testCases { - switch i { - case 0: - resolver, err := LoadResolver(testCases[i].Server, opts) - // if the domain is not canonical, make it canonical - if !strings.HasSuffix(testCase.Name, ".") { - testCases[i].Name = fmt.Sprintf("%s.", testCases[i].Name) - } - msg := new(dns.Msg) - msg.SetQuestion(testCase.Name, testCase.Request) - msg = msg.SetQuestion(testCase.Name, testCase.Request) - var in *dns.Msg - in, testCase.RTT, err = resolver.LookUp(msg) - assert.NotNil(t, err) - assert.Nil(t, in) - case 1: - resolver, err := LoadResolver(testCase2.Server, opts) - testCase2.Server = net.JoinHostPort(testCase2.Server, strconv.Itoa(opts.Port)) - // if the domain is not canonical, make it canonical - if !strings.HasSuffix(testCase2.Name, ".") { - testCase2.Name = fmt.Sprintf("%s.", testCase2.Name) - } - msg := new(dns.Msg) - msg.SetQuestion(testCase2.Name, testCase2.Request) - msg = msg.SetQuestion(testCase2.Name, testCase2.Request) - var in *dns.Msg - in, testCase.RTT, err = resolver.LookUp(msg) - assert.Nil(t, err) - assert.NotNil(t, in) - } - - } - -} diff --git a/query/resolver.go b/query/resolver.go index f1ac190..f6df45e 100644 --- a/query/resolver.go +++ b/query/resolver.go @@ -1,60 +1,24 @@ +// SPDX-License-Identifier: BSD-3-Clause + package query import ( "net" "strconv" "strings" - "time" - "git.froth.zone/sam/awl/logawl" + "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/internal/structs" "github.com/miekg/dns" ) -// represent all CLI flags -type Options struct { - Logger *logawl.Logger - - Port int - IPv4 bool - IPv6 bool - DNSSEC bool - Short bool - TCP bool - TLS bool - HTTPS bool - QUIC bool - Truncate bool - AA bool - TC bool - Z bool - CD bool - NoRD bool - NoRA bool - Reverse bool - Debug bool - Answers Answers -} -type Response struct { - Answers Answers `json:"Response"` // These be DNS query answers - DNS dns.Msg -} - -// The Answers struct is the basic structure of a DNS request -// to be returned to the user upon making a request -type Answers struct { - Server string `json:"Server"` // The server to make the DNS request from - DNS *dns.Msg - Request uint16 `json:"Request"` // The type of request - Name string `json:"Name"` // The domain name to make a DNS request for - RTT time.Duration `json:"RTT"` // The time it took to make the DNS query -} - type Resolver interface { - LookUp(*dns.Msg) (*dns.Msg, time.Duration, error) + LookUp(*dns.Msg) (structs.Response, error) } -func LoadResolver(server string, opts Options) (Resolver, error) { - if opts.HTTPS { +func LoadResolver(server string, opts cli.Options) (Resolver, error) { + switch { + case opts.HTTPS: opts.Logger.Debug("loading DoH resolver") if !strings.HasPrefix(server, "https://") { server = "https://" + server @@ -63,14 +27,19 @@ func LoadResolver(server string, opts Options) (Resolver, error) { server: server, opts: opts, }, nil - } else if opts.QUIC { + case opts.QUIC: opts.Logger.Debug("loading DoQ resolver") - server = net.JoinHostPort(opts.Answers.Server, strconv.Itoa(opts.Port)) + server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Port)) return &QUICResolver{ server: server, opts: opts, }, nil + default: + opts.Logger.Debug("loading standard/DoT resolver") + server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Port)) + return &StandardResolver{ + server: server, + opts: opts, + }, nil } - - return nil, nil } diff --git a/util/logger.go b/util/logger.go index 3d39476..4a3b2a7 100644 --- a/util/logger.go +++ b/util/logger.go @@ -1,13 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause + package util import "git.froth.zone/sam/awl/logawl" -func InitLogger(debug bool) (Logger *logawl.Logger) { +// Initialize the logawl instance +func InitLogger(verbosity int) (Logger *logawl.Logger) { Logger = logawl.New() - if debug { - Logger.SetLevel(3) - } + Logger.SetLevel(logawl.Level(verbosity)) return } diff --git a/util/helpers.go b/util/reverseDNS.go similarity index 100% rename from util/helpers.go rename to util/reverseDNS.go diff --git a/util/helpers_test.go b/util/reverseDNS_test.go similarity index 93% rename from util/helpers_test.go rename to util/reverseDNS_test.go index a53a1e4..fe1d16f 100644 --- a/util/helpers_test.go +++ b/util/reverseDNS_test.go @@ -15,18 +15,21 @@ var ( ) func TestIPv4(t *testing.T) { + t.Parallel() act, err := ReverseDNS("8.8.4.4", PTR) assert.Nil(t, err) assert.Equal(t, act, "4.4.8.8.in-addr.arpa.", "IPv4 reverse") } func TestIPv6(t *testing.T) { + t.Parallel() act, err := ReverseDNS("2606:4700:4700::1111", PTR) assert.Nil(t, err) assert.Equal(t, act, "1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa.", "IPv6 reverse") } func TestNAPTR(t *testing.T) { + t.Parallel() tests := []struct { in string want string @@ -44,6 +47,7 @@ func TestNAPTR(t *testing.T) { } func TestInvalid(t *testing.T) { + t.Parallel() _, err := ReverseDNS("AAAAA", 1) - assert.NotNil(t, err) + assert.Error(t, err) }