From 07728cffdb52482fd814dc7c0ffed3b0c7caafe5 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 4 Aug 2022 01:09:49 +0000 Subject: [PATCH] (feat)EDNS (#55) Reviewed-on: https://git.froth.zone/sam/awl/pulls/55 --- .drone.jsonnet | 33 ++- .github/ISSUE_TEMPLATE/bug.md | 28 +++ .github/ISSUE_TEMPLATE/feature.md | 18 ++ .github/pull_request_template.md | 9 + .gitignore | 4 + GNUmakefile | 6 +- LICENSE => LICENCE | 0 Makefile | 2 +- README.md | 4 +- cli/cli.go | 114 +++++++--- cli/cli_test.go | 37 +++- cli/dig.go | 175 +++++++++++---- cli/dig_test.go | 21 +- cli/misc.go | 50 +++-- cli/misc_test.go | 2 - cli/options.go | 92 ++++++-- conf/plan9.go | 6 +- conf/plan9_test.go | 1 - conf/unix.go | 3 +- conf/unix_test.go | 2 +- conf/win.go | 3 +- doc/awl.1 | 2 +- docs.go | 5 +- go.mod | 7 +- go.sum | 34 +-- logawl/logawl.go | 8 +- logawl/logger.go | 45 ++-- logawl/logging_test.go | 16 +- main.go | 93 +------- main_test.go | 19 ++ mkfile | 10 +- query/DNSCrypt.go | 8 +- query/DNSCrypt_test.go | 29 ++- query/HTTPS.go | 23 +- query/HTTPS_test.go | 1 - query/QUIC.go | 19 +- query/QUIC_test.go | 1 - query/general.go | 11 +- query/general_test.go | 40 +++- query/print.go | 177 +++++++++++++++ query/print_test.go | 343 ++++++++++++++++++++++++++++++ query/query.go | 116 ++++++++-- query/query_test.go | 96 +++++++-- query/resolver.go | 2 + query/struct.go | 153 +++++++++++++ query/struct_test.go | 14 ++ template.mk | 22 +- util/logger.go | 8 +- util/logger_test.go | 1 + util/reverseDNS.go | 17 +- util/reverseDNS_test.go | 9 +- 51 files changed, 1552 insertions(+), 387 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/feature.md create mode 100644 .github/pull_request_template.md rename LICENSE => LICENCE (100%) create mode 100644 main_test.go create mode 100644 query/print.go create mode 100644 query/print_test.go create mode 100644 query/struct.go create mode 100644 query/struct_test.go diff --git a/.drone.jsonnet b/.drone.jsonnet index 53094a7..bfd5b1c 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -2,6 +2,7 @@ local testing(version, arch) = { kind: "pipeline", + type: "docker", name: version + "-" + arch , platform: { arch: arch @@ -16,19 +17,27 @@ local testing(version, arch) = { }, { name: "lint", - image: "rancher/drone-golangci-lint:latest" + image: "rancher/drone-golangci-lint:latest", + depends_on: [ + "submodules", + ], }, { name: "test", image: "golang:" + version, commands: [ - "go test -race ./... -cover" - ] + "go test -v -race ./... -cover" + ], + depends_on: [ + "submodules", + ], }, ], - trigger: { + trigger: { event: { - exclude: "tag", + exclude: [ + "tag" + ], } }, }; @@ -36,14 +45,17 @@ local testing(version, arch) = { // "Inspired by" https://goreleaser.com/ci/drone/ local release() = { kind: "pipeline", + type: "docker", name: "release", trigger: { - event: "tag" + event: [ + "tag" + ], }, steps: [ { name: "fetch", - image: "docker:git", + image: "alpine/git", commands : [ "git fetch --tags", "git submodule update --init --recursive" @@ -67,15 +79,16 @@ local release() = { commands: [ "goreleaser release" ], - // when: { - // event: "tag" - // } } ] }; [ + testing("1.19", "amd64"), + testing("1.19", "arm64"), testing("1.18", "amd64"), testing("1.18", "arm64"), + + release() ] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..a587df9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,28 @@ +--- +name: Bug report +about: Report a bug + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Reproduction steps** +Steps to reproduce the behavior: +1. 1 +2. 2 +3. Bug + +**Expected behaviour** +A clear and concise description of what you expected to happen. + +**Screenshots / Logs** +Add `-v=4` and add the debug logs to the report here: + +**System information (please complete the following information):** + - OS: [e.g. Ubuntu 22.04, OpenBSD, Windows 11] + - Version: [run `awl -V` and print the output] + + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..56de423 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,18 @@ +--- +name: Feature request +about: Suggest a feature + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. +Links to implementations in dig, drill, etc. should go here. + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..76f377f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/.gitignore b/.gitignore index be06a1f..0aa6c52 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ dist/ # Test coverage coverage/* !coverage/.gitkeep + +awl + +.dccache \ No newline at end of file diff --git a/GNUmakefile b/GNUmakefile index 7abf056..c3957fd 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -12,11 +12,13 @@ endif $(PROG): $(GO) build -o $(EXE) $(GOFLAGS) . -## install: installs awl -install: all + ifeq ($(OS),Windows_NT) +## install: installs awl +install: $(GO) install $(GOFLAGS) . else +install: all install -m755 $(PROG) $(PREFIX)/$(BIN) install -m644 doc/$(PROG).1 $(MAN)/man1 endif diff --git a/LICENSE b/LICENCE similarity index 100% rename from LICENSE rename to LICENCE diff --git a/Makefile b/Makefile index d45f0fd..470145a 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ include template.mk $(PROG): $(GO) build -o $(PROG) $(GOFLAGS) . -## install: installs awl +## install: installs awl and the manpage, RUN AS ROOT install: all install -m755 $(PROG) $(PREFIX)/$(BIN) install -m644 doc/$(PROG).1 $(MAN)/man1 diff --git a/README.md b/README.md index c1ca76f..580092a 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,6 @@ emails? Send a patch to the Found a bug or want a new feature? Create an issue [here](https://git.froth.zone/sam/awl/issues). -### License +### Licence -See [LICENSE](./LICENSE) +See [LICENCE](./LICENCE) diff --git a/cli/cli.go b/cli/cli.go index f14e0e2..4b09779 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -6,17 +6,18 @@ import ( "fmt" "os" "runtime" + "strconv" "strings" "time" "git.froth.zone/sam/awl/internal/helpers" "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" flag "github.com/stefansundin/go-zflag" ) -// Parse the arguments passed into awl. +// ParseCLI parses arguments given from the CLI and passes them into an `Options` +// struct. func ParseCLI(version string) (Options, error) { flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) @@ -45,10 +46,22 @@ func ParseCLI(version string) (Options, error) { ipv6 = flag.Bool("6", false, "force IPv6", flag.OptShorthand('6')) reverse = flag.Bool("reverse", false, "do a reverse lookup", flag.OptShorthand('x')) - timeout = flag.Float32("timeout", 1, "Timeout, in `seconds`") - retry = flag.Int("retries", 2, "number of `times` to retry") - 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)") + timeout = flag.Float32("timeout", 1, "Timeout, in `seconds`") + retry = flag.Int("retries", 2, "number of `times` to retry") + + edns = flag.Bool("no-edns", false, "disable EDNS entirely") + ednsVer = flag.Uint8("edns-ver", 0, "set EDNS version") + expire = flag.Bool("expire", false, "set EDNS expire") + dnssec = flag.Bool("dnssec", false, "enable DNSSEC", flag.OptShorthand('D')) + nsid = flag.Bool("nsid", false, "set EDNS NSID", flag.OptShorthand('n')) + cookie = flag.Bool("no-cookie", false, "disable sending EDNS cookie (default: cookie sent)") + tcpKeepAlive = flag.Bool("keep-alive", false, "send EDNS TCP keep-alive") + udpBufSize = flag.Uint16("buffer-size", 1232, "set EDNS UDP buffer size", flag.OptShorthand('b')) + zflag = flag.String("zflag", "0", "set EDNS z-flag") + subnet = flag.String("subnet", "", "set EDNS subnet") + padding = flag.Bool("pad", false, "set EDNS padding") + + truncate = flag.Bool("no-truncate", false, "ignore truncation if a UDP request truncates (default: retry with TCP)") tcp = flag.Bool("tcp", false, "use TCP") dnscrypt = flag.Bool("dnscrypt", false, "use DNSCrypt") @@ -65,18 +78,20 @@ func ParseCLI(version string) (Options, error) { 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')) + short = flag.Bool("short", false, "print just the results", 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')) + noC = flag.Bool("no-comments", false, "disable printing the comments") noQ = flag.Bool("no-question", false, "disable printing the question section") + noOpt = flag.Bool("no-opt", false, "disable printing the OPT pseudosection") noAns = flag.Bool("no-answer", false, "disable printing the answer section") noAuth = flag.Bool("no-authority", false, "disable printing the authority section") - noAdd = flag.Bool("no-additional", false, "disable printing the additonal section") + noAdd = flag.Bool("no-additional", false, "disable printing the additional section") noStats = flag.Bool("no-statistics", false, "disable printing the statistics section") - verbosity = flag.Int("verbosity", 0, "sets verbosity `level`", flag.OptShorthand('v'), flag.OptNoOptDefVal("2")) + verbosity = flag.Int("verbosity", 1, "sets verbosity `level`", flag.OptShorthand('v'), flag.OptNoOptDefVal("2")) versionFlag = flag.Bool("version", false, "print version information", flag.OptShorthand('V')) ) @@ -86,34 +101,42 @@ func ParseCLI(version string) (Options, error) { // Parse the flags err := flag.CommandLine.Parse(os.Args[1:]) if err != nil { - return Options{Logger: util.InitLogger(*verbosity)}, err + return Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("flag parse error: %w", err) + } + + // TODO: DRY, dumb dumb. + mbz, err := strconv.ParseInt(*zflag, 0, 16) + if err != nil { + return Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("EDNS MBZ error: %w", err) } opts := Options{ - Logger: util.InitLogger(*verbosity), - Port: *port, - IPv4: *ipv4, - IPv6: *ipv6, - DNSSEC: *dnssec, - Short: *short, - TCP: *tcp, - DNSCrypt: *dnscrypt, - 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, + Logger: util.InitLogger(*verbosity), + Port: *port, + IPv4: *ipv4, + IPv6: *ipv6, + Short: *short, + TCP: *tcp, + DNSCrypt: *dnscrypt, + TLS: *tls, + HTTPS: *https, + QUIC: *quic, + Truncate: *truncate, + ShowQuery: false, + AA: *aa, + AD: *ad, + TC: *tc, + Z: *z, + CD: *cd, + QR: *qr, + RD: *rd, + RA: *ra, + Reverse: *reverse, + HumanTTL: false, + ShowTTL: true, + JSON: *json, + XML: *xml, + YAML: *yaml, Request: helpers.Request{ Type: dns.StringToType[strings.ToUpper(*qType)], Class: dns.StringToClass[strings.ToUpper(*class)], @@ -122,12 +145,34 @@ func ParseCLI(version string) (Options, error) { Retries: *retry, }, Display: Displays{ + Comments: !*noC, Question: !*noQ, + Opt: !*noOpt, Answer: !*noAns, Authority: !*noAuth, Additional: !*noAdd, Statistics: !*noStats, }, + EDNS: EDNS{ + EnableEDNS: !*edns, + Cookie: !*cookie, + DNSSEC: *dnssec, + BufSize: *udpBufSize, + Version: *ednsVer, + Expire: *expire, + KeepOpen: *tcpKeepAlive, + Nsid: *nsid, + ZFlag: uint16(mbz & 0x7FFF), + Padding: *padding, + }, + } + + // TODO: DRY + if *subnet != "" { + err := parseSubnet(*subnet, &opts) + if err != nil { + return opts, err + } } opts.Logger.Info("POSIX flags parsed") @@ -142,7 +187,6 @@ func ParseCLI(version string) (Options, error) { // This includes the dig-style (+) options err = ParseMiscArgs(flag.Args(), &opts) if err != nil { - opts.Logger.Warn(err) return opts, err } opts.Logger.Info("Dig/Drill flags parsed") diff --git a/cli/cli_test.go b/cli/cli_test.go index 21b9e63..edeb947 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -11,25 +11,49 @@ import ( "gotest.tools/v3/assert" ) +// nolint: paralleltest func TestEmpty(t *testing.T) { old := os.Args os.Args = []string{"awl", "-4"} opts, err := cli.ParseCLI("TEST") assert.NilError(t, err) - assert.Assert(t, (opts.Port == 53)) + assert.Equal(t, opts.Port, 53) assert.Assert(t, opts.IPv4) os.Args = old } +// nolint: paralleltest func TestTLSPort(t *testing.T) { old := os.Args os.Args = []string{"awl", "-T"} opts, err := cli.ParseCLI("TEST") assert.NilError(t, err) - assert.Assert(t, (opts.Port == 853)) + assert.Equal(t, opts.Port, 853) os.Args = old } +// nolint: paralleltest +func TestSubnet(t *testing.T) { + old := os.Args + os.Args = []string{"awl", "--subnet", "127.0.0.1/32"} + opts, err := cli.ParseCLI("TEST") + assert.NilError(t, err) + assert.Equal(t, opts.EDNS.Subnet.Family, uint16(1)) + + os.Args = []string{"awl", "--subnet", "0"} + opts, err = cli.ParseCLI("TEST") + assert.NilError(t, err) + assert.Equal(t, opts.EDNS.Subnet.Family, uint16(1)) + os.Args = old + + os.Args = []string{"awl", "--subnet", "::/0"} + opts, err = cli.ParseCLI("TEST") + assert.NilError(t, err) + assert.Equal(t, opts.EDNS.Subnet.Family, uint16(2)) + os.Args = old +} + +// nolint: paralleltest func TestInvalidFlag(t *testing.T) { old := os.Args os.Args = []string{"awl", "--treebug"} @@ -38,14 +62,16 @@ func TestInvalidFlag(t *testing.T) { os.Args = old } +// nolint: paralleltest func TestInvalidDig(t *testing.T) { old := os.Args os.Args = []string{"awl", "+a"} _, err := cli.ParseCLI("TEST") - assert.ErrorContains(t, err, "dig: unknown flag") + assert.ErrorContains(t, err, "digflags: invalid argument") os.Args = old } +// nolint: paralleltest func TestVersion(t *testing.T) { old := os.Args os.Args = []string{"awl", "--version"} @@ -54,6 +80,7 @@ func TestVersion(t *testing.T) { os.Args = old } +// nolint: paralleltest func TestTimeout(t *testing.T) { args := [][]string{ {"awl", "+timeout=0"}, @@ -69,6 +96,7 @@ func TestTimeout(t *testing.T) { } } +// nolint: paralleltest func TestRetries(t *testing.T) { args := [][]string{ {"awl", "+retry=-2"}, @@ -85,6 +113,7 @@ func TestRetries(t *testing.T) { } } +// nolint: paralleltest func FuzzFlags(f *testing.F) { testcases := []string{"git.froth.zone", "", "!12345", "google.com.edu.org.fr"} for _, tc := range testcases { @@ -92,7 +121,7 @@ func FuzzFlags(f *testing.F) { } f.Fuzz(func(t *testing.T, orig string) { - os.Args = []string{orig} + os.Args = []string{"awl", orig} //nolint:errcheck // Only make sure the program does not crash cli.ParseCLI("TEST") }) diff --git a/cli/dig.go b/cli/dig.go index 32b4193..3f48022 100644 --- a/cli/dig.go +++ b/cli/dig.go @@ -9,108 +9,193 @@ import ( "time" ) -// Parse dig-like commands and set the options as such. +// ParseDig parses commands from the popular DNS tool dig. +// All dig commands are taken from https://man.openbsd.org/dig.1 as the source of their functionality. +// +// [no]flags are supported just as flag are and are disabled as such. func ParseDig(arg string, opts *Options) error { // returns true if the flag starts with a no isNo := !strings.HasPrefix(arg, "no") + if !isNo { + arg = strings.TrimPrefix(arg, "no") + } opts.Logger.Info("Setting", arg) switch arg { // Set DNS query flags - case "aa", "aaflag", "aaonly", "noaa", "noaaflag", "noaaonly": + case "aa", "aaflag", "aaonly": opts.AA = isNo - case "ad", "adflag", "noad", "noadflag": + case "ad", "adflag": opts.AD = isNo - case "cd", "cdflag", "nocd", "nocdflag": + case "cd", "cdflag": opts.CD = isNo - case "qr", "qrflag", "noqr", "noqrflag": + case "qrflag": opts.QR = isNo - case "ra", "raflag", "nora", "noraflag": + case "ra", "raflag": opts.RA = isNo - case "rd", "rdflag", "recurse", "nord", "nordflag", "norecurse": + case "rd", "rdflag", "recurse": opts.RD = isNo - case "tc", "tcflag", "notc", "notcflag": + case "tc", "tcflag": opts.TC = isNo - case "z", "zflag", "noz", "nozflag": + case "z", "zflag": opts.Z = isNo // End DNS query flags + case "qr": + opts.ShowQuery = isNo + case "ttlunits": + opts.HumanTTL = isNo + case "ttlid": + opts.ShowTTL = isNo + + // EDNS queries + case "dnssec": + opts.EDNS.DNSSEC = isNo + case "expire": + opts.EDNS.Expire = isNo + case "cookie": + opts.EDNS.Cookie = isNo + case "keepopen", "keepalive": + opts.EDNS.KeepOpen = isNo + case "nsid": + opts.EDNS.Nsid = isNo + case "padding": + opts.EDNS.Padding = isNo + // End EDNS queries + // DNS-over-X - case "dnssec", "nodnssec": - opts.DNSSEC = isNo - case "tcp", "vc", "notcp", "novc": + case "tcp", "vc": opts.TCP = isNo - case "ignore", "noignore": + case "ignore": opts.Truncate = isNo - case "tls", "notls": + case "tls": opts.TLS = isNo - case "dnscrypt", "nodnscrypt": + case "dnscrypt": opts.DNSCrypt = isNo - case "https", "nohttps": + case "https": opts.HTTPS = isNo - case "quic", "noquic": + case "quic": opts.QUIC = isNo // End DNS-over-X // Formatting - case "short", "noshort": + case "short": opts.Short = isNo - case "json", "nojson": + case "identify": + opts.Identify = isNo + case "json": opts.JSON = isNo - case "xml", "noxml": + case "xml": opts.XML = isNo - case "yaml", "noyaml": + case "yaml": opts.YAML = isNo // End formatting // Output - // TODO: get this to work - // case "comments", "nocomments": - // opts.Display.Comments = isNo - case "question", "noquestion": + case "comments": + opts.Display.Comments = isNo + case "question": opts.Display.Question = isNo - case "answer", "noanswer": + case "opt": + opts.Display.Opt = isNo + case "answer": opts.Display.Answer = isNo - case "authority", "noauthority": + case "authority": opts.Display.Authority = isNo - case "additional", "noadditional": + case "additional": opts.Display.Additional = isNo - case "stats", "nostats": + case "stats": opts.Display.Statistics = isNo - - case "all", "noall": + case "all": + opts.Display.Comments = isNo opts.Display.Question = isNo + opts.Display.Opt = isNo opts.Display.Answer = isNo opts.Display.Authority = isNo opts.Display.Additional = isNo opts.Display.Statistics = isNo + case "idnout": + opts.Display.UcodeTranslate = isNo default: // Recursive switch statements WOO - switch { - case strings.HasPrefix(arg, "timeout"): - timeout, err := strconv.Atoi(strings.Split(arg, "=")[1]) + arg := strings.Split(arg, "=") + switch arg[0] { + case "time", "timeout": + if len(arg) > 1 && arg[1] != "" { + timeout, err := strconv.Atoi(arg[1]) + if err != nil { + return fmt.Errorf("digflags: Invalid timeout value: %w", err) + } - if err != nil { - return fmt.Errorf("dig: Invalid timeout value") + opts.Request.Timeout = time.Duration(timeout) + } else { + return fmt.Errorf("digflags: Invalid timeout value: %w", errNoArg) } - opts.Request.Timeout = time.Duration(timeout) + case "retry", "tries": + if len(arg) > 1 && arg[1] != "" { + tries, err := strconv.Atoi(arg[1]) + if err != nil { + return fmt.Errorf("digflags: Invalid retry value: %w", err) + } + opts.Request.Retries = tries - case strings.HasPrefix(arg, "retry"), strings.HasPrefix(arg, "tries"): - tries, err := strconv.Atoi(strings.Split(arg, "=")[1]) - if err != nil { - return fmt.Errorf("dig: Invalid retry value") + // TODO: Is there a better way to do this? + if arg[0] == "tries" { + opts.Request.Retries++ + } + } else { + return fmt.Errorf("digflags: Invalid retry value: %w", errNoArg) } - if strings.HasPrefix(arg, "tries") { - tries++ + case "bufsize": + if len(arg) > 1 && arg[1] != "" { + size, err := strconv.Atoi(arg[1]) + if err != nil { + return fmt.Errorf("digflags: Invalid UDP buffer size value: %w", err) + } + opts.EDNS.BufSize = uint16(size) + } else { + return fmt.Errorf("digflags: Invalid UDP buffer size value: %w", errNoArg) } - opts.Request.Retries = tries + case "ednsflags": + if len(arg) > 1 && arg[1] != "" { + ver, err := strconv.ParseInt(arg[1], 0, 16) + if err != nil { + return fmt.Errorf("digflags: Invalid EDNS flag: %w", err) + } + // Ignore setting DO bit + opts.EDNS.ZFlag = uint16(ver & 0x7FFF) + } else { + opts.EDNS.ZFlag = 0 + } + + case "edns": + opts.EDNS.EnableEDNS = isNo + if len(arg) > 1 && arg[1] != "" { + ver, err := strconv.Atoi(arg[1]) + if err != nil { + return fmt.Errorf("digflags: Invalid EDNS version: %w", err) + } + opts.EDNS.Version = uint8(ver) + } else { + opts.EDNS.Version = 0 + } + + case "subnet": + if len(arg) > 1 && arg[1] != "" { + err := parseSubnet(arg[1], opts) + if err != nil { + return fmt.Errorf("digflags: Invalid EDNS Subnet: %w", err) + } + } else { + return fmt.Errorf("digflags: Invalid EDNS Subnet: %w", errNoArg) + } default: - return fmt.Errorf("dig: unknown flag %s given", arg) + return &errInvalidArg{arg[0]} } } return nil diff --git a/cli/dig_test.go b/cli/dig_test.go index 4b12db4..03e84d8 100644 --- a/cli/dig_test.go +++ b/cli/dig_test.go @@ -21,7 +21,22 @@ func FuzzDig(f *testing.F) { "rdflag", "recurse", "nordflag", "norecurse", "tcflag", "notcflag", "zflag", "nozflag", + "qr", "noqr", + "ttlunits", "nottlunits", + "ttlid", "nottlid", "dnssec", "nodnssec", + "edns", "edns=a", "edns=0", "noedns", + "expire", "noexpire", + "ednsflags", "ednsflags=\"", "ednsflags=1", "noednsflags", + "subnet=0.0.0.0/0", "subnet=::0/0", "subnet=b", "subnet=0", "subnet", + "cookie", "nocookeie", + "keepopen", "keepalive", "nokeepopen", "nokeepalive", + "nsid", "nonsid", + "padding", "nopadding", + "bufsize=512", "bufsize=a", "bufsize", + "time=5", "timeout=a", "timeout", + "retry=a", "retry=3", "retry", + "tries=2", "tries=b", "tries", "tcp", "vc", "notcp", "novc", "ignore", "noignore", "tls", "notls", @@ -29,15 +44,19 @@ func FuzzDig(f *testing.F) { "https", "nohttps", "quic", "noquic", "short", "noshort", + "identify", "noidentify", "json", "nojson", "xml", "noxml", "yaml", "noyaml", + "comments", "nocomments", "question", "noquestion", + "opt", "noopt", "answer", "noanswer", "authority", "noauthority", "additional", "noadditional", "stats", "nostats", "all", "noall", + "idnout", "noidnout", "invalid", } for _, tc := range seeds { @@ -49,7 +68,7 @@ func FuzzDig(f *testing.F) { opts.Logger = util.InitLogger(0) err := cli.ParseDig(orig, opts) if err != nil { - assert.ErrorContains(t, err, "unknown flag") + assert.ErrorContains(t, err, "digflags:") } }) } diff --git a/cli/misc.go b/cli/misc.go index c2bbb94..282d52d 100644 --- a/cli/misc.go +++ b/cli/misc.go @@ -9,12 +9,12 @@ import ( "git.froth.zone/sam/awl/conf" "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" "golang.org/x/net/idna" ) -// Parse the wildcard arguments, drill style. +// ParseMiscArgs parses the wildcard arguments, drill style. +// Only one command is supported at a time, so any extra information overrides previous. func ParseMiscArgs(args []string, opts *Options) error { var err error @@ -46,28 +46,34 @@ func ParseMiscArgs(args []string, opts *Options) error { default: opts.Request.Server = arg } - case strings.Contains(arg, "."): - opts.Logger.Info(arg, "detected as a domain name") - opts.Request.Name, err = idna.ToASCII(arg) - if err != nil { - return err - } - case ok: - opts.Logger.Info(arg, "detected as a type") - // If it's a DNS request, it's a DNS request (obviously) - opts.Request.Type = r + + // Dig-style +queries case strings.HasPrefix(arg, "+"): opts.Logger.Info(arg, "detected as a dig query") - // Dig-style +queries err = ParseDig(strings.ToLower(arg[1:]), opts) if err != nil { return err } + + // Domain names + case strings.Contains(arg, "."): + opts.Logger.Info(arg, "detected as a domain name") + opts.Request.Name, err = idna.ToASCII(arg) + if err != nil { + return fmt.Errorf("punycode translate error: %w", err) + } + + // DNS query type + case ok: + opts.Logger.Info(arg, "detected as a type") + opts.Request.Type = r + + // Domain? default: opts.Logger.Info(arg, "is unknown. Assuming domain") opts.Request.Name, err = idna.ToASCII(arg) if err != nil { - return err + return fmt.Errorf("punycode translate error: %w", err) } } } @@ -107,21 +113,23 @@ func ParseMiscArgs(args []string, opts *Options) error { opts.Request.Server = "95.216.99.249" } else { // Make sure that if IPv4 or IPv6 is asked for it actually uses it + harmful: for _, srv := range resolv.Servers { - if opts.IPv4 { + switch { + case opts.IPv4: if strings.Contains(srv, ".") { opts.Request.Server = srv - break + break harmful } - } else if opts.IPv6 { + case opts.IPv6: if strings.Contains(srv, ":") { opts.Request.Server = srv - break + break harmful } - } else { + default: //#nosec -- This isn't used for anything secure opts.Request.Server = resolv.Servers[rand.Intn(len(resolv.Servers))] - break + break harmful } } } @@ -137,7 +145,7 @@ func ParseMiscArgs(args []string, opts *Options) error { } opts.Request.Name, err = util.ReverseDNS(opts.Request.Name, opts.Request.Type) if err != nil { - return err + return fmt.Errorf("reverse DNS error: %w", err) } } diff --git a/cli/misc_test.go b/cli/misc_test.go index 929c1b4..80afee5 100644 --- a/cli/misc_test.go +++ b/cli/misc_test.go @@ -8,7 +8,6 @@ import ( "git.froth.zone/sam/awl/cli" "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" "gotest.tools/v3/assert" ) @@ -139,7 +138,6 @@ func TestFlagSetting(t *testing.T) { assert.Assert(t, opts.HTTPS) case 3: assert.Assert(t, opts.QUIC) - } }) } diff --git a/cli/options.go b/cli/options.go index 16d0978..9508093 100644 --- a/cli/options.go +++ b/cli/options.go @@ -4,9 +4,12 @@ package cli import ( "errors" + "fmt" + "net" "git.froth.zone/sam/awl/internal/helpers" "git.froth.zone/sam/awl/logawl" + "github.com/miekg/dns" ) // CLI options structure. @@ -15,13 +18,13 @@ type Options struct { Port int // DNS port IPv4 bool // Force IPv4 IPv6 bool // Force IPv6 - DNSSEC bool // Enable DNSSEC TCP bool // Query with TCP DNSCrypt bool // Query over DNSCrypt TLS bool // Query over TLS HTTPS bool // Query over HTTPS QUIC bool // Query over QUIC Truncate bool // Ignore truncation + ShowQuery bool // Show query before being sent AA bool // Set Authoratative Answer AD bool // Set Authenticated Data CD bool // Set CD @@ -32,24 +35,87 @@ type Options struct { Z bool // Set Z (Zero) Reverse bool // Make reverse query Verbosity int // Set logawl verbosity - // HumanTTL bool // Make TTL human readable - Short bool // Short output - JSON bool // Outout as JSON - XML bool // Output as XML - YAML bool // Output at YAML + HumanTTL bool // Make TTL human readable + ShowTTL bool // Display TTL + Short bool // Short output + Identify bool // If short, add identity stuffs + JSON bool // Outout as JSON + XML bool // Output as XML + YAML bool // Output at YAML Display Displays // Display options Request helpers.Request // DNS reuqest + EDNS // EDNS } -// What to (and not to) display +// What to (and not to) display. type Displays struct { - // Comments bool - Question bool // QUESTION SECTION - Answer bool // ANSWER SECTION - Authority bool // AUTHORITY SECTION - Additional bool // ADDITIONAL SECTION - Statistics bool // Query time, message size, etc. + Comments bool + Question bool // QUESTION SECTION + Opt bool // OPT PSEUDOSECTION + Answer bool // ANSWER SECTION + Authority bool // AUTHORITY SECTION + Additional bool // ADDITIONAL SECTION + Statistics bool // Query time, message size, etc. + UcodeTranslate bool // Translate Punycode back to Unicode +} + +type EDNS struct { + EnableEDNS bool // Enable EDNS + Cookie bool // Enable EDNS cookie + DNSSEC bool // Enable DNSSEC + BufSize uint16 // Set UDP buffer size + Version uint8 // Set EDNS version + Expire bool // Set EDNS expiration + KeepOpen bool // TCP keep alive + Nsid bool // Show EDNS nsid + ZFlag uint16 // EDNS flags + Padding bool // EDNS padding + Subnet dns.EDNS0_SUBNET // EDNS Subnet (duh) +} + +// parseSubnet takes a subnet argument and makes it into one that the DNS library +// understands. +func parseSubnet(subnet string, opts *Options) error { + ip, inet, err := net.ParseCIDR(subnet) + if err != nil { + // TODO: make not a default? + if subnet == "0" { + opts.EDNS.Subnet = dns.EDNS0_SUBNET{ + Code: dns.EDNS0SUBNET, + Family: 1, + SourceNetmask: 0, + SourceScope: 0, + Address: net.IPv4(0, 0, 0, 0), + } + return nil + } + return fmt.Errorf("subnet parsing error %w", err) + } + sub, _ := inet.Mask.Size() + opts.EDNS.Subnet = dns.EDNS0_SUBNET{} + opts.EDNS.Subnet.Address = ip + opts.EDNS.Subnet.SourceNetmask = uint8(sub) + + switch ip.To4() { + case nil: + // Not a valid IPv4 so assume IPv6 + opts.EDNS.Subnet.Family = 2 + default: + // Valid IPv4 + opts.EDNS.Subnet.Family = 1 + } + return nil } var ErrNotError = errors.New("not an error") + +var errNoArg = errors.New("no argument given") + +type errInvalidArg struct { + arg string +} + +func (e *errInvalidArg) Error() string { + return fmt.Sprintf("digflags: invalid argument %s", e.arg) +} diff --git a/conf/plan9.go b/conf/plan9.go index 2819c92..9da5690 100644 --- a/conf/plan9.go +++ b/conf/plan9.go @@ -3,7 +3,7 @@ package conf import ( - "fmt" + "errors" "strings" "github.com/miekg/dns" @@ -23,7 +23,7 @@ func GetPlan9Config(str string) (*dns.ClientConfig, error) { } } if len(servers) == 0 { - return nil, fmt.Errorf("plan9: no DNS servers found") + return nil, errPlan9 } // TODO: read more about how customizable Plan 9 is @@ -38,3 +38,5 @@ func GetPlan9Config(str string) (*dns.ClientConfig, error) { func splitChars(r rune) bool { return r == ' ' || r == '\t' } + +var errPlan9 = errors.New("plan9Config: no DNS servers found") diff --git a/conf/plan9_test.go b/conf/plan9_test.go index af8379a..a5123fc 100644 --- a/conf/plan9_test.go +++ b/conf/plan9_test.go @@ -6,7 +6,6 @@ import ( "testing" "git.froth.zone/sam/awl/conf" - "gotest.tools/v3/assert" ) diff --git a/conf/unix.go b/conf/unix.go index 12eb49f..9809b48 100644 --- a/conf/unix.go +++ b/conf/unix.go @@ -4,6 +4,7 @@ package conf import ( + "fmt" "os" "runtime" @@ -15,7 +16,7 @@ func GetDNSConfig() (*dns.ClientConfig, error) { if runtime.GOOS == "plan9" { dat, err := os.ReadFile("/net/ndb") if err != nil { - return nil, err + return nil, fmt.Errorf("plan9 ndb: %w", err) } return GetPlan9Config(string(dat)) } else { diff --git a/conf/unix_test.go b/conf/unix_test.go index 9899c3a..64df984 100644 --- a/conf/unix_test.go +++ b/conf/unix_test.go @@ -13,7 +13,7 @@ import ( func TestNonWinConfig(t *testing.T) { if runtime.GOOS == "windows" { - t.Skip("Not running Windows, skipping") + t.Skip("Running Windows, skipping") } conf, err := conf.GetDNSConfig() assert.NilError(t, err) diff --git a/conf/win.go b/conf/win.go index 2e5d23c..6b5712e 100644 --- a/conf/win.go +++ b/conf/win.go @@ -4,6 +4,7 @@ package conf import ( + "fmt" "strings" "unsafe" @@ -23,7 +24,7 @@ func GetDNSConfig() (*dns.ClientConfig, error) { // Windows is an utter fucking trash fire of an operating system. if err := windows.GetAdaptersAddresses(windows.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l); err != nil { - return nil, err + return nil, fmt.Errorf("config, windows: %w", err) } var addresses []*windows.IpAdapterAddresses for addr := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); addr != nil; addr = addr.Next { diff --git a/doc/awl.1 b/doc/awl.1 index 3145c0f..1da461e 100644 --- a/doc/awl.1 +++ b/doc/awl.1 @@ -5,7 +5,7 @@ .nh .ad l .\" Begin generated content: -.TH "awl" "1" "2022-07-25" +.TH "awl" "1" "2022-08-03" .PP .SH NAME awl - DNS lookup tool diff --git a/docs.go b/docs.go index 3ba701c..f868989 100644 --- a/docs.go +++ b/docs.go @@ -1,9 +1,8 @@ /* -awl is a DNS lookup tool written in Go, -similar to (and heavily inspired by) drill. +awl is a DNS lookup tool written in Go, similar to (and heavily inspired by) drill. It runs and displays similar outputs to drill, without any frills. -Options are given to print with JSON +Options are given to print with JSON, XML and YAML. Supports results from DNS-over-[UDP, TCP, TLS, HTTPS, QUIC] servers diff --git a/go.mod b/go.mod index eb2101f..cd6db6b 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.18 require ( github.com/ameshkov/dnscrypt/v2 v2.2.3 + github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 github.com/lucas-clemente/quic-go v0.28.1 github.com/miekg/dns v1.1.50 github.com/stefansundin/go-zflag v1.1.1 golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.3.0 ) @@ -33,8 +34,8 @@ require ( github.com/stretchr/testify v1.8.0 // indirect golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/sys v0.0.0-20220803195053-6e608f9ce704 + golang.org/x/sys v0.0.0-20220731174439-a90be440212d golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.11 // indirect + golang.org/x/tools v0.1.12 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) diff --git a/go.sum b/go.sum index 5ed2189..ef6e1d5 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7 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= +github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= +github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= @@ -217,22 +219,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b 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-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220725212005-46097bf591d3 h1:2yWTtPWWRcISTw3/o+s/Y4UOMnQL71DWyToOANFusCg= -golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= -golang.org/x/net v0.0.0-20220726230323-06994584191e h1:wOQNKh1uuDGRnmgF0jDxh7ctgGy/3P4rYWQRVJD4/Yg= -golang.org/x/net v0.0.0-20220726230323-06994584191e/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= -golang.org/x/net v0.0.0-20220728012108-993b7b1e3a27 h1:Khs7GS6mUxEA1e5DfKm9ojYX4BiI297wdliOwp/CPmw= -golang.org/x/net v0.0.0-20220728012108-993b7b1e3a27/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20220728030405-41545e8bf201 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0= -golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20220728153142-1f511ac62c11 h1:BZ+7NKCw5UIzmPP4GFz9VcjTVpN9mswsWRDmJ2tWKAs= -golang.org/x/net v0.0.0-20220728153142-1f511ac62c11/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20220728181054-f92ba40d432d h1:3iMzhioG3w6/URLOo7X7eZRkWoLdz9iWE/UsnXHNTfY= -golang.org/x/net v0.0.0-20220728181054-f92ba40d432d/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20220728211354-c7608f3a8462 h1:UreQrH7DbFXSi9ZFox6FNT3WBooWmdANpU+IfkT1T4I= -golang.org/x/net v0.0.0-20220728211354-c7608f3a8462/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b h1:3ogNYyK4oIQdIKzTu68hQrr4iuVxF3AxKl9Aj/eDrw0= golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -246,8 +232,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -273,18 +259,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220727055044-e65921a090b8 h1:dyU22nBWzrmTQxtNrr4dzVOvaw35nUYE279vF9UmsI8= -golang.org/x/sys v0.0.0-20220727055044-e65921a090b8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220730100132-1609e554cd39 h1:aNCnH+Fiqs7ZDTFH6oEFjIfbX2HvgQXJ6uQuUbTobjk= -golang.org/x/sys v0.0.0-20220730100132-1609e554cd39/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220731174439-a90be440212d h1:Sv5ogFZatcgIMMtBSTTAgMYsicp25MXBubjXNDKwm80= golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220803195053-6e608f9ce704 h1:Y7NOhdqIOU8kYI7BxsgL38d0ot0raxvcW+EMQU2QrT4= -golang.org/x/sys v0.0.0-20220803195053-6e608f9ce704/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -308,8 +284,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= -golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/logawl/logawl.go b/logawl/logawl.go index 70c9416..52701b6 100644 --- a/logawl/logawl.go +++ b/logawl/logawl.go @@ -3,7 +3,7 @@ package logawl import ( - "fmt" + "errors" "io" "sync" "sync/atomic" @@ -14,10 +14,10 @@ type ( Logger struct { Mu sync.Mutex Level Level + isDiscard int32 Prefix string Out io.Writer buf []byte - isDiscard int32 } ) @@ -48,7 +48,7 @@ func (l *Logger) UnMarshalLevel(lv Level) (string, error) { case 3: return "DEBUG ", nil } - return "", fmt.Errorf("invalid log level") + return "", errInvalidLevel } func (l *Logger) IsLevel(level Level) bool { @@ -74,3 +74,5 @@ const ( // Verbose log level. DebugLevel ) + +var errInvalidLevel = errors.New("invalid log level") diff --git a/logawl/logger.go b/logawl/logger.go index b703ba2..aaf29a7 100644 --- a/logawl/logger.go +++ b/logawl/logger.go @@ -11,41 +11,41 @@ import ( // Calling New instantiates Logger // -// Level can be changed to one of the other log levels (FatalLevel, ErrorLevel, InfoLevel, DebugLevel) +// Level can be changed to one of the other log levels (ErrorLevel, WarnLevel, InfoLevel, DebugLevel). func New() *Logger { return &Logger{ Out: os.Stderr, - Level: InfoLevel, //Default value is InfoLevel + Level: WarnLevel, // Default value is WarnLevel } } -// Takes any and prints it out to Logger -> Out (io.Writer (default is std.Err)) +// Takes any and prints it out to Logger -> Out (io.Writer (default is std.Err)). func (l *Logger) Println(level Level, v ...any) { if atomic.LoadInt32(&l.isDiscard) != 0 { return } - //If verbose is not set --debug etc print _nothing_ + // If verbose is not set --debug etc print _nothing_ if l.IsLevel(level) { - switch level { //Goes through log levels and does stuff based on them (Fatal os.Exit...etc) + switch level { // Goes through log levels and does stuff based on them (currently nothing) case 0: - err := l.Printer(0, fmt.Sprintln(v...)) //Fatal level + err := l.Printer(0, fmt.Sprintln(v...)) // Error level if err != nil { - fmt.Fprintln(os.Stderr, "FATAL: Logger failed: ", err) + fmt.Fprintln(os.Stderr, "Logger failed: ", err) } case 1: - err := l.Printer(1, fmt.Sprintln(v...)) //Error level + err := l.Printer(1, fmt.Sprintln(v...)) // Warn level if err != nil { - fmt.Fprintln(os.Stderr, "FATAL: Logger failed: ", err) + fmt.Fprintln(os.Stderr, "Logger failed: ", err) } case 2: - err := l.Printer(2, fmt.Sprintln(v...)) //Info level + err := l.Printer(2, fmt.Sprintln(v...)) // Info level if err != nil { - fmt.Fprintln(os.Stderr, "FATAL: Logger failed: ", err) + fmt.Fprintln(os.Stderr, "Logger failed: ", err) } case 3: - err := l.Printer(3, fmt.Sprintln(v...)) //Debug level + err := l.Printer(3, fmt.Sprintln(v...)) // Debug level if err != nil { - fmt.Fprintln(os.Stderr, "FATAL: Logger failed: ", err) + fmt.Fprintln(os.Stderr, "Logger failed: ", err) } default: break @@ -53,7 +53,7 @@ func (l *Logger) Println(level Level, v ...any) { } } -// Formats the log header as such YYYY/MM/DD HH:MM:SS (local time) +// 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 { if lvl, err := l.UnMarshalLevel(level); err == nil { // This is ugly but functional @@ -77,12 +77,12 @@ func (l *Logger) FormatHeader(buf *[]byte, t time.Time, line int, level Level) e *buf = append(*buf, ':') *buf = append(*buf, ' ') } else { - return fmt.Errorf("invalid log level choice") + return errInvalidLevel } return nil } -// Printer prints the formatted message directly to stdErr +// Printer prints the formatted message directly to stdErr. func (l *Logger) Printer(level Level, s string) error { now := time.Now() var line int @@ -99,7 +99,10 @@ func (l *Logger) Printer(level Level, s string) error { l.buf = append(l.buf, '\n') } _, err = l.Out.Write(l.buf) - return err + if err != nil { + return fmt.Errorf("logger printing error %w", err) + } + return nil } // Some line formatting stuff from Golang log stdlib file @@ -123,22 +126,22 @@ func formatter(buf *[]byte, i int, wid int) { *buf = append(*buf, b[bp:]...) } -// Call print directly with Debug level +// Call print directly with Debug level. func (l *Logger) Debug(v ...any) { l.Println(DebugLevel, v...) } -// Call print directly with Info level +// Call print directly with Info level. func (l *Logger) Info(v ...any) { l.Println(InfoLevel, v...) } -// Call print directly with Warn level +// Call print directly with Warn level. func (l *Logger) Warn(v ...any) { l.Println(WarnLevel, v...) } -// Call print directly with Error level +// Call print directly with Error level. func (l *Logger) Error(v ...any) { l.Println(ErrLevel, v...) } diff --git a/logawl/logging_test.go b/logawl/logging_test.go index ffbcf60..e30eef9 100644 --- a/logawl/logging_test.go +++ b/logawl/logging_test.go @@ -8,7 +8,6 @@ import ( "time" "git.froth.zone/sam/awl/logawl" - "gotest.tools/v3/assert" ) @@ -48,11 +47,17 @@ func TestLogger(t *testing.T) { t.Parallel() for i := range logawl.AllLevels { - // only test non-exiting log levels switch i { + case 0: + fn := func() { + logger.Error("Test", "E") + } + var buffer bytes.Buffer + logger.Out = &buffer + fn() case 1: fn := func() { - logger.Info("") + logger.Warn("Test") } var buffer bytes.Buffer logger.Out = &buffer @@ -67,6 +72,7 @@ func TestLogger(t *testing.T) { case 3: fn := func() { logger.Debug("Test") + logger.Debug("Test 2") } var buffer bytes.Buffer logger.Out = &buffer @@ -79,6 +85,6 @@ func TestFmt(t *testing.T) { t.Parallel() ti := time.Now() test := []byte("test") - assert.ErrorContains(t, logger.FormatHeader(&test, ti, 0, 9001), "invalid log level") //make sure error is error - + // make sure error is error + assert.ErrorContains(t, logger.FormatHeader(&test, ti, 0, 9001), "invalid log level") } diff --git a/main.go b/main.go index 68587f0..5f8ce9e 100644 --- a/main.go +++ b/main.go @@ -3,20 +3,14 @@ package main import ( - "encoding/json" - "encoding/xml" "errors" "fmt" "os" - "strconv" "strings" - "time" "git.froth.zone/sam/awl/cli" "git.froth.zone/sam/awl/internal/helpers" "git.froth.zone/sam/awl/query" - - "gopkg.in/yaml.v2" ) var version = "DEV" @@ -31,6 +25,7 @@ func main() { opts.Logger.Error(err) os.Exit(1) } + var resp helpers.Response // Retry queries if a query fails @@ -39,7 +34,7 @@ func main() { if err == nil { break } else { - opts.Logger.Warn("Retrying request, error", err) + opts.Logger.Warn("Retrying request, error:", err) } } @@ -48,83 +43,17 @@ func main() { opts.Logger.Error(err) os.Exit(9) } - switch { - case opts.JSON: - opts.Logger.Info("Printing as JSON") - json, err := json.MarshalIndent(resp.DNS, "", " ") + + var str string + if opts.JSON || opts.XML || opts.YAML { + str, err = query.PrintSpecial(resp.DNS, opts) if err != nil { - opts.Logger.Error(err) + opts.Logger.Error("Special print:", err) os.Exit(10) } - fmt.Println(string(json)) - case opts.XML: - opts.Logger.Info("Printing as XML") - xml, err := xml.MarshalIndent(resp.DNS, "", " ") - if err != nil { - opts.Logger.Error(err) - os.Exit(10) - } - fmt.Println(string(xml)) - case opts.YAML: - opts.Logger.Info("Printing as YAML") - yaml, err := yaml.Marshal(resp.DNS) - if err != nil { - opts.Logger.Error(err) - os.Exit(10) - } - fmt.Println(string(yaml)) - default: - if !opts.Short { - // Print everything - - if !opts.Display.Question { - resp.DNS.Question = nil - opts.Logger.Info("Disabled question display") - } - if !opts.Display.Answer { - resp.DNS.Answer = nil - opts.Logger.Info("Disabled answer display") - } - if !opts.Display.Authority { - resp.DNS.Ns = nil - opts.Logger.Info("Disabled authority display") - } - if !opts.Display.Additional { - resp.DNS.Extra = nil - opts.Logger.Info("Disabled additional display") - } - - fmt.Println(resp.DNS) - - if opts.Display.Statistics { - fmt.Println(";; Query time:", resp.RTT) - - // Add extra information to server string - var extra string - switch { - case opts.TCP: - extra = ":" + strconv.Itoa(opts.Port) + " (TCP)" - case opts.TLS: - extra = ":" + strconv.Itoa(opts.Port) + " (TLS)" - case opts.HTTPS, opts.DNSCrypt: - extra = "" - case opts.QUIC: - extra = ":" + strconv.Itoa(opts.Port) + " (QUIC)" - default: - extra = ":" + strconv.Itoa(opts.Port) + " (UDP)" - } - - fmt.Println(";; SERVER:", opts.Request.Server+extra) - 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]) - } - } + } else { + str = query.ToString(resp, opts) } + + fmt.Println(str) } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..6091ce8 --- /dev/null +++ b/main_test.go @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package main + +import ( + "os" + "testing" + + "gotest.tools/v3/assert" +) + +// nolint: paralleltest +func TestMain(t *testing.T) { + os.Args = []string{"awl", "+yaml", "@1.1.1.1"} + main() + os.Args = []string{"awl", "+short", "@1.1.1.1"} + main() + assert.Assert(t, 1 == 2-1) +} diff --git a/mkfile b/mkfile index 053cea2..14ad22b 100644 --- a/mkfile +++ b/mkfile @@ -3,20 +3,18 @@ GO = go PROG = awl -LDFLAGS = '-s -w' -GOFLAGS = -ldflags=$LDFLAGS CGO_ENABLED = 0 $PROG: - $GO build $GOFLAGS -o $PROG '-buildvcs=false' . + $GO build -ldflags="-s -w -X=main.version=PLAN9" -o $PROG . -install: $PROG - $GO install $GOFLAGS . +install: + $GO install -ldflags="-s -w -X=main.version=PLAN9" . cp doc/$PROG.1 /sys/man/1/$PROG test: - $GO test -v -cover -coverprofile=coverage/coverage.out ./... + $GO test -cover -coverprofile=coverage/coverage.out ./... fmt: gofmt -w -s . diff --git a/query/DNSCrypt.go b/query/DNSCrypt.go index 12751f8..70e5151 100644 --- a/query/DNSCrypt.go +++ b/query/DNSCrypt.go @@ -3,11 +3,11 @@ package query import ( + "fmt" "time" "git.froth.zone/sam/awl/cli" "git.froth.zone/sam/awl/internal/helpers" - "github.com/ameshkov/dnscrypt/v2" "github.com/miekg/dns" ) @@ -16,8 +16,8 @@ type DNSCryptResolver struct { opts cli.Options } +// LookUp performs a DNS query. func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { - client := dnscrypt.Client{ Timeout: r.opts.Request.Timeout, UDPSize: 1232, @@ -39,7 +39,7 @@ func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { resolverInf, err := client.Dial(r.opts.Request.Server) if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("dnscrypt: dial error: %w", err) } now := time.Now() @@ -47,7 +47,7 @@ func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { rtt := time.Since(now) if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("dnscrypt: exchange error: %w", err) } r.opts.Logger.Info("Request successful") diff --git a/query/DNSCrypt_test.go b/query/DNSCrypt_test.go index 126a073..d49bb30 100644 --- a/query/DNSCrypt_test.go +++ b/query/DNSCrypt_test.go @@ -9,12 +9,12 @@ import ( "git.froth.zone/sam/awl/internal/helpers" "git.froth.zone/sam/awl/query" "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" "gotest.tools/v3/assert" ) func TestDNSCrypt(t *testing.T) { + t.Parallel() tests := []struct { opt cli.Options }{ @@ -42,11 +42,30 @@ func TestDNSCrypt(t *testing.T) { }, }, }, + { + cli.Options{ + Logger: util.InitLogger(0), + DNSCrypt: true, + TCP: true, + IPv4: true, + Request: helpers.Request{ + Server: "QMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", + Type: dns.TypeAAAA, + Name: "example.com.", + }, + }, + }, } for _, test := range tests { - res, err := query.CreateQuery(test.opt) - assert.NilError(t, err) - assert.Assert(t, res != helpers.Response{}) + test := test + t.Run("", func(t *testing.T) { + t.Parallel() + res, err := query.CreateQuery(test.opt) + if err == nil { + assert.Assert(t, res != helpers.Response{}) + } else { + assert.ErrorContains(t, err, "unsupported stamp") + } + }) } - } diff --git a/query/HTTPS.go b/query/HTTPS.go index b15dd81..91c23f4 100644 --- a/query/HTTPS.go +++ b/query/HTTPS.go @@ -18,6 +18,7 @@ type HTTPSResolver struct { opts cli.Options } +// LookUp performs a DNS query. func (r *HTTPSResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { var resp helpers.Response httpR := &http.Client{ @@ -25,13 +26,13 @@ func (r *HTTPSResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { } buf, err := msg.Pack() if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("doh: packing error error: %w", err) } r.opts.Logger.Debug("making DoH request") - // query := server + "?dns=" + base64.RawURLEncoding.EncodeToString(buf) + // req, err := http.NewRequest("POST", r.opts.Request.Server, bytes.NewBuffer(buf)) if err != nil { - return helpers.Response{}, fmt.Errorf("DoH: %w", err) + return helpers.Response{}, fmt.Errorf("doh request creation error: %w", err) } req.Header.Set("Content-Type", "application/dns-message") req.Header.Set("Accept", "application/dns-message") @@ -41,24 +42,32 @@ func (r *HTTPSResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { resp.RTT = time.Since(now) if err != nil { - return helpers.Response{}, fmt.Errorf("DoH HTTP request error: %w", err) + return helpers.Response{}, fmt.Errorf("doh HTTP request error: %w", err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return helpers.Response{}, fmt.Errorf("DoH server responded with HTTP %d", res.StatusCode) + return helpers.Response{}, &errHTTPStatus{res.StatusCode} } fullRes, err := io.ReadAll(res.Body) if err != nil { - return helpers.Response{}, fmt.Errorf("DoH body read error: %w", err) + return helpers.Response{}, fmt.Errorf("doh body read error: %w", err) } resp.DNS = &dns.Msg{} r.opts.Logger.Debug("unpacking response") err = resp.DNS.Unpack(fullRes) if err != nil { - return helpers.Response{}, fmt.Errorf("DoH dns message unpack error: %w", err) + return helpers.Response{}, fmt.Errorf("doh dns message unpack error: %w", err) } return resp, nil } + +type errHTTPStatus struct { + code int +} + +func (e *errHTTPStatus) Error() string { + return fmt.Sprintf("doh server responded with HTTP %d", e.code) +} diff --git a/query/HTTPS_test.go b/query/HTTPS_test.go index b6efae6..ee552a0 100644 --- a/query/HTTPS_test.go +++ b/query/HTTPS_test.go @@ -11,7 +11,6 @@ import ( "git.froth.zone/sam/awl/internal/helpers" "git.froth.zone/sam/awl/query" "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" "gotest.tools/v3/assert" ) diff --git a/query/QUIC.go b/query/QUIC.go index e9e5dd1..10bf474 100644 --- a/query/QUIC.go +++ b/query/QUIC.go @@ -4,12 +4,12 @@ package query import ( "crypto/tls" + "fmt" "io" "time" "git.froth.zone/sam/awl/cli" "git.froth.zone/sam/awl/internal/helpers" - "github.com/lucas-clemente/quic-go" "github.com/miekg/dns" ) @@ -18,6 +18,7 @@ type QUICResolver struct { opts cli.Options } +// LookUp performs a DNS query. func (r *QUICResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { var resp helpers.Response tls := &tls.Config{ @@ -31,46 +32,46 @@ func (r *QUICResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { r.opts.Logger.Debug("making DoQ request") connection, err := quic.DialAddr(r.opts.Request.Server, tls, conf) if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("doq: dial error: %w", err) } // Compress request to over-the-wire buf, err := msg.Pack() if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("doq: pack error: %w", err) } t := time.Now() stream, err := connection.OpenStream() if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("doq: quic stream creation error: %w", err) } _, err = stream.Write(buf) if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("doq: quic stream write error: %w", err) } fullRes, err := io.ReadAll(stream) if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("doq: quic stream read error: %w", err) } resp.RTT = time.Since(t) // Close with error: no error err = connection.CloseWithError(0, "") if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("doq: quic connection close error: %w", err) } err = stream.Close() if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("doq: quic stream close error: %w", err) } resp.DNS = &dns.Msg{} r.opts.Logger.Debug("unpacking DoQ response") err = resp.DNS.Unpack(fullRes) if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("doq: upack error: %w", err) } return resp, nil } diff --git a/query/QUIC_test.go b/query/QUIC_test.go index b8837d1..a02d930 100644 --- a/query/QUIC_test.go +++ b/query/QUIC_test.go @@ -14,7 +14,6 @@ import ( "git.froth.zone/sam/awl/internal/helpers" "git.froth.zone/sam/awl/query" "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" "gotest.tools/v3/assert" ) diff --git a/query/general.go b/query/general.go index 68a5f8d..eca10c1 100644 --- a/query/general.go +++ b/query/general.go @@ -8,7 +8,6 @@ import ( "git.froth.zone/sam/awl/cli" "git.froth.zone/sam/awl/internal/helpers" - "github.com/miekg/dns" ) @@ -16,6 +15,7 @@ type StandardResolver struct { opts cli.Options } +// LookUp performs a DNS query func (r *StandardResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { var ( resp helpers.Response @@ -45,7 +45,7 @@ func (r *StandardResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, r.opts.Request.Server) if err != nil { - return helpers.Response{}, err + return helpers.Response{}, fmt.Errorf("standard: DNS exchange error: %w", err) } r.opts.Logger.Info("Request successful") @@ -55,11 +55,14 @@ func (r *StandardResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { switch { case r.opts.IPv4: dnsClient.Net += "4" - case r.opts.IPv4: + case r.opts.IPv6: dnsClient.Net += "6" } resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, r.opts.Request.Server) } + if err != nil { + return helpers.Response{}, fmt.Errorf("standard: DNS exchange error: %w", err) + } - return resp, err + return resp, nil } diff --git a/query/general_test.go b/query/general_test.go index 5b080b9..7346e63 100644 --- a/query/general_test.go +++ b/query/general_test.go @@ -4,6 +4,7 @@ package query_test import ( "testing" + "time" "git.froth.zone/sam/awl/cli" "git.froth.zone/sam/awl/internal/helpers" @@ -14,25 +15,28 @@ import ( ) func TestResolve(t *testing.T) { + t.Parallel() opts := cli.Options{ Logger: util.InitLogger(0), Port: 53, Request: helpers.Request{ - Server: "8.8.4.4", - Type: dns.TypeA, - Name: "example.com.", + Server: "8.8.4.1", + Type: dns.TypeA, + Name: "example.com.", + Timeout: time.Second / 2, + Retries: 0, }, } resolver, err := query.LoadResolver(opts) assert.NilError(t, err) msg := new(dns.Msg) msg.SetQuestion(opts.Request.Name, opts.Request.Type) - res, err := resolver.LookUp(msg) - assert.NilError(t, err) - assert.Assert(t, res != helpers.Response{}) + _, err = resolver.LookUp(msg) + assert.ErrorContains(t, err, "timeout") } func TestTruncate(t *testing.T) { + t.Parallel() opts := cli.Options{ Logger: util.InitLogger(0), IPv4: true, @@ -53,6 +57,7 @@ func TestTruncate(t *testing.T) { } func TestResolveAgain(t *testing.T) { + t.Parallel() tests := []struct { opt cli.Options }{ @@ -79,11 +84,26 @@ func TestResolveAgain(t *testing.T) { }, }, }, + { + cli.Options{ + Logger: util.InitLogger(0), + TLS: true, + Port: 853, + Request: helpers.Request{ + Server: "dns.google", + Type: dns.TypeAAAA, + Name: "example.com.", + }, + }, + }, } for _, test := range tests { - res, err := query.CreateQuery(test.opt) - assert.NilError(t, err) - assert.Assert(t, res != helpers.Response{}) + test := test + t.Run("", func(t *testing.T) { + t.Parallel() + res, err := query.CreateQuery(test.opt) + assert.NilError(t, err) + assert.Assert(t, res != helpers.Response{}) + }) } - } diff --git a/query/print.go b/query/print.go new file mode 100644 index 0000000..f5cf4eb --- /dev/null +++ b/query/print.go @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package query + +import ( + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "git.froth.zone/sam/awl/cli" + "github.com/miekg/dns" + "golang.org/x/net/idna" + "gopkg.in/yaml.v3" +) + +func PrintSpecial(msg *dns.Msg, opts cli.Options) (string, error) { + formatted, err := MakePrintable(msg, opts) + if err != nil { + return "", err + } + switch { + case opts.JSON: + opts.Logger.Info("Printing as JSON") + json, err := json.MarshalIndent(formatted, " ", " ") + return string(json), err + + case opts.XML: + opts.Logger.Info("Printing as XML") + xml, err := xml.MarshalIndent(formatted, " ", " ") + return string(xml), err + + case opts.YAML: + opts.Logger.Info("Printing as YAML") + yaml, err := yaml.Marshal(formatted) + return string(yaml), err + + default: + return "", errInvalidFormat + } +} + +// MakePrintable takes a DNS message and makes it nicer to be printed as JSON,YAML, +// and XML. Little is changed beyond naming +func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) { + var err error + ret := Message{ + Header: msg.MsgHdr, + } + + for _, question := range msg.Question { + var name string + if opts.Display.UcodeTranslate { + name, err = idna.ToUnicode(question.Name) + if err != nil { + return nil, fmt.Errorf("punycode: error translating to unicode: %w", err) + } + } else { + name = question.Name + } + ret.Question = append(ret.Question, Question{ + Name: name, + Type: dns.TypeToString[question.Qtype], + Class: dns.ClassToString[question.Qclass], + }) + } + + for _, answer := range msg.Answer { + temp := strings.Split(answer.String(), "\t") + var ( + ttl string + name string + ) + if opts.ShowTTL { + if opts.HumanTTL { + ttl = (time.Duration(answer.Header().Ttl) * time.Second).String() + } else { + ttl = strconv.Itoa(int(answer.Header().Ttl)) + } + } + if opts.Display.UcodeTranslate { + name, err = idna.ToUnicode(answer.Header().Name) + if err != nil { + return nil, fmt.Errorf("punycode: error translating to unicode: %w", err) + } + } else { + name = answer.Header().Name + } + ret.Answer = append(ret.Answer, Answer{ + RRHeader: RRHeader{ + Name: name, + Type: dns.TypeToString[answer.Header().Rrtype], + Class: dns.ClassToString[answer.Header().Class], + Rdlength: answer.Header().Rdlength, + TTL: ttl, + }, + Value: temp[len(temp)-1], + }) + } + + for _, ns := range msg.Ns { + temp := strings.Split(ns.String(), "\t") + var ( + ttl string + name string + ) + if opts.ShowTTL { + if opts.HumanTTL { + ttl = (time.Duration(ns.Header().Ttl) * time.Second).String() + } else { + ttl = strconv.Itoa(int(ns.Header().Ttl)) + } + } + if opts.Display.UcodeTranslate { + name, err = idna.ToUnicode(ns.Header().Name) + if err != nil { + return nil, fmt.Errorf("punycode: error translating to unicode: %w", err) + } + } else { + name = ns.Header().Name + } + ret.Ns = append(ret.Ns, Answer{ + RRHeader: RRHeader{ + Name: name, + Type: dns.TypeToString[ns.Header().Rrtype], + Class: dns.ClassToString[ns.Header().Class], + Rdlength: ns.Header().Rdlength, + TTL: ttl, + }, + Value: temp[len(temp)-1], + }) + } + + for _, additional := range msg.Extra { + if additional.Header().Rrtype == dns.StringToType["OPT"] { + continue + } else { + temp := strings.Split(additional.String(), "\t") + var ( + ttl string + name string + ) + if opts.ShowTTL { + if opts.HumanTTL { + ttl = (time.Duration(additional.Header().Ttl) * time.Second).String() + } else { + ttl = strconv.Itoa(int(additional.Header().Ttl)) + } + } + if opts.Display.UcodeTranslate { + name, err = idna.ToUnicode(additional.Header().Name) + if err != nil { + return nil, fmt.Errorf("punycode: error translating to unicode: %w", err) + } + } else { + name = additional.Header().Name + } + ret.Extra = append(ret.Extra, Answer{ + RRHeader: RRHeader{ + Name: name, + Type: dns.TypeToString[additional.Header().Rrtype], + Class: dns.ClassToString[additional.Header().Class], + Rdlength: additional.Header().Rdlength, + TTL: ttl, + }, + Value: temp[len(temp)-1], + }) + } + } + + return &ret, nil +} + +var errInvalidFormat = errors.New("this should never happen") diff --git a/query/print_test.go b/query/print_test.go new file mode 100644 index 0000000..732cb69 --- /dev/null +++ b/query/print_test.go @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package query_test + +import ( + "testing" + + "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/internal/helpers" + "git.froth.zone/sam/awl/query" + "git.froth.zone/sam/awl/util" + + "github.com/miekg/dns" + "gotest.tools/v3/assert" +) + +func TestBadFormat(t *testing.T) { + t.Parallel() + _, err := query.PrintSpecial(new(dns.Msg), cli.Options{}) + assert.ErrorContains(t, err, "never happen") +} + +func TestPrinting(t *testing.T) { + t.Parallel() + opts := cli.Options{ + Logger: util.InitLogger(0), + Port: 53, + IPv4: false, + IPv6: false, + TCP: true, + DNSCrypt: false, + TLS: false, + HTTPS: false, + QUIC: false, + Truncate: false, + ShowQuery: true, + AA: false, + AD: false, + CD: false, + QR: false, + RD: true, + RA: false, + TC: false, + Z: false, + Reverse: false, + Verbosity: 0, + HumanTTL: false, + ShowTTL: true, + Short: false, + Identify: false, + JSON: true, + XML: false, + YAML: false, + Display: cli.Displays{ + Comments: true, + Question: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: true, + }, + Request: helpers.Request{ + Server: "a.gtld-servers.net", + Type: dns.StringToType["NS"], + Class: 1, + Name: "google.com.", + Timeout: 0, + Retries: 0, + }, + EDNS: cli.EDNS{ + EnableEDNS: false, + }, + } + + resp, err := query.CreateQuery(opts) + assert.NilError(t, err) + + str, err := query.PrintSpecial(resp.DNS, opts) + assert.NilError(t, err) + assert.Assert(t, str != "") +} + +func TestPrinting2(t *testing.T) { + t.Parallel() + opts := cli.Options{ + Logger: util.InitLogger(0), + Port: 53, + IPv4: false, + IPv6: false, + TCP: true, + DNSCrypt: false, + TLS: false, + HTTPS: false, + QUIC: false, + Truncate: false, + ShowQuery: true, + AA: false, + AD: false, + CD: false, + QR: false, + RD: true, + RA: false, + TC: false, + Z: false, + Reverse: false, + Verbosity: 0, + HumanTTL: false, + ShowTTL: true, + Short: true, + Identify: true, + JSON: false, + XML: false, + YAML: true, + Display: cli.Displays{ + Comments: true, + Question: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: true, + }, + Request: helpers.Request{ + Server: "ns1.google.com", + Type: dns.StringToType["NS"], + Class: 1, + Name: "google.com.", + Timeout: 0, + Retries: 0, + }, + EDNS: cli.EDNS{ + EnableEDNS: false, + }, + } + + resp, err := query.CreateQuery(opts) + assert.NilError(t, err) + + str, err := query.PrintSpecial(resp.DNS, opts) + assert.NilError(t, err) + assert.Assert(t, str != "") + + str = query.ToString(resp, opts) + assert.Assert(t, str != "") +} + +func TestPrinting3(t *testing.T) { + t.Parallel() + opts := cli.Options{ + Logger: util.InitLogger(0), + Port: 53, + IPv4: false, + IPv6: false, + TCP: false, + DNSCrypt: false, + TLS: false, + HTTPS: true, + QUIC: false, + Truncate: false, + ShowQuery: true, + AA: false, + AD: false, + CD: false, + QR: false, + RD: true, + RA: false, + TC: false, + Z: false, + Reverse: false, + Verbosity: 0, + HumanTTL: false, + ShowTTL: true, + Short: false, + Identify: true, + JSON: false, + XML: false, + YAML: true, + Display: cli.Displays{ + Comments: true, + Question: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: true, + }, + Request: helpers.Request{ + Server: "https://dns.froth.zone/dns-query", + Type: dns.StringToType["NS"], + Class: 1, + Name: "freecumextremist.com.", + Timeout: 0, + Retries: 0, + }, + EDNS: cli.EDNS{ + EnableEDNS: false, + }, + } + + resp, err := query.CreateQuery(opts) + assert.NilError(t, err) + + str, err := query.PrintSpecial(resp.DNS, opts) + assert.NilError(t, err) + assert.Assert(t, str != "") + + str = query.ToString(resp, opts) + assert.Assert(t, str != "") +} + +func TestPrinting4(t *testing.T) { + t.Parallel() + opts := cli.Options{ + Logger: util.InitLogger(0), + Port: 853, + IPv4: false, + IPv6: false, + TCP: false, + DNSCrypt: false, + TLS: true, + HTTPS: false, + QUIC: false, + Truncate: false, + ShowQuery: true, + AA: false, + AD: false, + CD: false, + QR: false, + RD: true, + RA: false, + TC: false, + Z: false, + Reverse: false, + Verbosity: 0, + HumanTTL: false, + ShowTTL: true, + Short: false, + Identify: true, + JSON: false, + XML: false, + YAML: true, + Display: cli.Displays{ + Comments: true, + Question: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: true, + }, + Request: helpers.Request{ + Server: "dns.google", + Type: dns.StringToType["NS"], + Class: 1, + Name: "freecumextremist.com.", + Timeout: 0, + Retries: 0, + }, + EDNS: cli.EDNS{ + EnableEDNS: false, + }, + } + + resp, err := query.CreateQuery(opts) + assert.NilError(t, err) + + str, err := query.PrintSpecial(resp.DNS, opts) + assert.NilError(t, err) + assert.Assert(t, str != "") + + str = query.ToString(resp, opts) + assert.Assert(t, str != "") +} + +func TestPrinting5(t *testing.T) { + t.Parallel() + opts := cli.Options{ + Logger: util.InitLogger(0), + Port: 53, + IPv4: false, + IPv6: false, + TCP: true, + DNSCrypt: false, + TLS: false, + HTTPS: false, + QUIC: false, + Truncate: false, + ShowQuery: true, + AA: true, + AD: false, + CD: false, + QR: false, + RD: true, + RA: false, + TC: false, + Z: false, + Reverse: false, + Verbosity: 0, + HumanTTL: false, + ShowTTL: true, + Short: false, + Identify: false, + JSON: false, + XML: false, + YAML: true, + Display: cli.Displays{ + Comments: true, + Question: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: true, + }, + Request: helpers.Request{ + Server: "rin.froth.zone", + Type: dns.StringToType["A"], + Class: 1, + Name: "froth.zone.", + Timeout: 0, + Retries: 0, + }, + EDNS: cli.EDNS{ + EnableEDNS: true, + Cookie: true, + }, + } + + resp, err := query.CreateQuery(opts) + assert.NilError(t, err) + + str, err := query.PrintSpecial(resp.DNS, opts) + assert.NilError(t, err) + assert.Assert(t, str != "") + + str = query.ToString(resp, opts) + assert.Assert(t, str != "") +} + +func TestToString6(t *testing.T) { + assert.Assert(t, query.ToString(*new(helpers.Response), *new(cli.Options)) == " MsgHdr") +} diff --git a/query/query.go b/query/query.go index 4111207..0373e56 100644 --- a/query/query.go +++ b/query/query.go @@ -4,33 +4,114 @@ package query import ( "fmt" + "strconv" "git.froth.zone/sam/awl/cli" "git.froth.zone/sam/awl/internal/helpers" - + "github.com/dchest/uniuri" "github.com/miekg/dns" ) func CreateQuery(opts cli.Options) (helpers.Response, error) { - var res helpers.Response - res.DNS = new(dns.Msg) - res.DNS.SetQuestion(opts.Request.Name, opts.Request.Type) - res.DNS.Question[0].Qclass = opts.Request.Class + req := new(dns.Msg) + req.SetQuestion(opts.Request.Name, opts.Request.Type) + req.Question[0].Qclass = opts.Request.Class - 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 + // Set standard flags + req.MsgHdr.Response = opts.QR + req.MsgHdr.Authoritative = opts.AA + req.MsgHdr.Truncated = opts.TC + req.MsgHdr.RecursionDesired = opts.RD + req.MsgHdr.RecursionAvailable = opts.RA + req.MsgHdr.Zero = opts.Z + req.MsgHdr.AuthenticatedData = opts.AD + req.MsgHdr.CheckingDisabled = opts.CD - if opts.DNSSEC { - res.DNS.SetEdns0(1232, true) + // EDNS time :) + if opts.EDNS.EnableEDNS { + o := new(dns.OPT) + o.Hdr.Name = "." + o.Hdr.Rrtype = dns.TypeOPT + + o.SetVersion(opts.EDNS.Version) + + if opts.EDNS.Cookie { + e := new(dns.EDNS0_COOKIE) + e.Code = dns.EDNS0COOKIE + e.Cookie = uniuri.NewLenChars(8, []byte("1234567890abcdef")) + o.Option = append(o.Option, e) + opts.Logger.Info("Setting EDNS cookie to", e.Cookie) + } + + if opts.EDNS.Expire { + o.Option = append(o.Option, new(dns.EDNS0_EXPIRE)) + opts.Logger.Info("Setting EDNS Expire option") + } + + if opts.EDNS.KeepOpen { + o.Option = append(o.Option, new(dns.EDNS0_TCP_KEEPALIVE)) + opts.Logger.Info("Setting EDNS TCP Keepalive option") + } + + if opts.EDNS.Nsid { + o.Option = append(o.Option, new(dns.EDNS0_NSID)) + opts.Logger.Info("Setting EDNS NSID option") + } + + if opts.EDNS.Padding { + o.Option = append(o.Option, new(dns.EDNS0_PADDING)) + opts.Logger.Info("Setting EDNS padding") + } + + o.SetUDPSize(opts.BufSize) + opts.Logger.Info("EDNS UDP buffer set to", opts.BufSize) + + o.SetZ(opts.EDNS.ZFlag) + opts.Logger.Info("EDNS Z flag set to", opts.EDNS.ZFlag) + + if opts.EDNS.DNSSEC { + o.SetDo() + opts.Logger.Info("EDNS DNSSEC OK set") + } + + if opts.EDNS.Subnet.Address != nil { + o.Option = append(o.Option, &opts.EDNS.Subnet) + } + + req.Extra = append(req.Extra, o) + } else if opts.EDNS.DNSSEC { + req.SetEdns0(1232, true) + opts.Logger.Warn("DNSSEC implies EDNS, EDNS enabled") + opts.Logger.Info("DNSSEC enabled, UDP buffer set to 1232") } + opts.Logger.Debug(req) - opts.Logger.Debug(fmt.Sprintf("%+v", res)) + if !opts.Short { + if opts.ShowQuery { + opts.Logger.Info("Printing constructed query") + var ( + str string + err error + ) + if opts.JSON || opts.XML || opts.YAML { + str, err = PrintSpecial(req, opts) + if err != nil { + return helpers.Response{}, err + } + } else { + temp := opts.Display.Statistics + opts.Display.Statistics = false + str = ToString(helpers.Response{ + DNS: req, + RTT: 0, + }, opts) + opts.Display.Statistics = temp + str += "\n;; QUERY SIZE: " + strconv.Itoa(req.Len()) + } + fmt.Println(str) + opts.ShowQuery = false + } + } resolver, err := LoadResolver(opts) if err != nil { @@ -38,5 +119,6 @@ func CreateQuery(opts cli.Options) (helpers.Response, error) { } opts.Logger.Info("Query successfully loaded") - return resolver.LookUp(res.DNS) + //nolint:wrapcheck // Error wrapping not needed here + return resolver.LookUp(req) } diff --git a/query/query_test.go b/query/query_test.go index ab00cfa..6b6ee66 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -9,26 +9,92 @@ import ( "git.froth.zone/sam/awl/internal/helpers" "git.froth.zone/sam/awl/query" "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" "gotest.tools/v3/assert" ) func TestCreateQ(t *testing.T) { - opts := cli.Options{ - Logger: util.InitLogger(0), - Port: 53, - QR: false, - Z: true, - RD: false, - DNSSEC: true, - Request: helpers.Request{ - Server: "8.8.4.4", - Type: dns.TypeA, - Name: "example.com.", + t.Parallel() + in := []cli.Options{ + { + Logger: util.InitLogger(0), + Port: 53, + QR: false, + Z: true, + RD: false, + ShowQuery: true, + YAML: true, + + Request: helpers.Request{ + Server: "8.8.4.4", + Type: dns.TypeA, + Name: "example.com.", + }, + Display: cli.Displays{ + Comments: true, + Question: true, + Opt: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: false, + }, + EDNS: cli.EDNS{ + EnableEDNS: true, + DNSSEC: true, + Cookie: true, + Expire: true, + KeepOpen: true, + Nsid: true, + }, + }, + { + Logger: util.InitLogger(0), + Port: 53, + QR: false, + Z: true, + RD: false, + ShowQuery: true, + XML: true, + + Request: helpers.Request{ + Server: "8.8.4.4", + Type: dns.TypeA, + Name: "example.com.", + }, + Display: cli.Displays{ + Comments: true, + Question: true, + Opt: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: true, + }, + EDNS: cli.EDNS{ + EnableEDNS: false, + DNSSEC: false, + Cookie: false, + Expire: false, + KeepOpen: false, + Nsid: false, + }, }, } - res, err := query.CreateQuery(opts) - assert.NilError(t, err) - assert.Assert(t, res != helpers.Response{}) + for _, opt := range in { + opt := opt + t.Run("", func(t *testing.T) { + t.Parallel() + res, err := query.CreateQuery(opt) + assert.NilError(t, err) + assert.Assert(t, res != helpers.Response{}) + str, err := query.PrintSpecial(res.DNS, opt) + assert.NilError(t, err) + assert.Assert(t, str != "") + str = query.ToString(res, opt) + assert.Assert(t, str != "") + }) + } } diff --git a/query/resolver.go b/query/resolver.go index 7c035ee..51740e6 100644 --- a/query/resolver.go +++ b/query/resolver.go @@ -12,10 +12,12 @@ import ( "github.com/miekg/dns" ) +// Main resolver interface type Resolver interface { LookUp(*dns.Msg) (helpers.Response, error) } +// LoadResolver loads the respective resolver for performing a DNS query func LoadResolver(opts cli.Options) (Resolver, error) { switch { case opts.HTTPS: diff --git a/query/struct.go b/query/struct.go new file mode 100644 index 0000000..b4388da --- /dev/null +++ b/query/struct.go @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package query + +import ( + "strconv" + "strings" + "time" + + "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/internal/helpers" + "github.com/miekg/dns" +) + +// Overall DNS response message +type Message struct { + Header dns.MsgHdr `json:"header,omitempty" xml:"header,omitempty" yaml:",omitempty"` + Question []Question `json:"question,omitempty" xml:"question,omitempty" yaml:",omitempty"` + Answer []Answer `json:"answer,omitempty" xml:"answer,omitempty" yaml:",omitempty"` + Ns []Answer `json:"ns,omitempty" xml:"ns,omitempty" yaml:",omitempty"` + Extra []Answer `json:"extra,omitempty" xml:"extra,omitempty" yaml:",omitempty"` +} + +// DNS Query +type Question struct { + Name string `json:"name,omitempty" xml:"name,omitempty" yaml:",omitempty"` + Type string `json:"type,omitempty" xml:"type,omitempty" yaml:",omitempty"` + Class string `json:"class,omitempty" xml:"class,omitempty" yaml:",omitempty"` +} + +// DNS Resource Headers +type RRHeader struct { + Name string `json:"name,omitempty" xml:"name,omitempty" yaml:",omitempty"` + Type string `json:"type,omitempty" xml:"type,omitempty" yaml:",omitempty"` + Class string `json:"class,omitempty" xml:"class,omitempty" yaml:",omitempty"` + TTL string `json:"ttl,omitempty" xml:"ttl,omitempty" yaml:",omitempty"` + Rdlength uint16 `json:"-" xml:"-" yaml:"-"` +} + +// DNS Response +type Answer struct { + RRHeader `json:"header,omitempty" xml:"header,omitempty" yaml:"header,omitempty"` + Value string `json:"response,omitempty" xml:"response,omitempty" yaml:"response,omitempty"` +} + +// ToString turns the response into something that looks a lot like dig +// +// Much of this is taken from https://github.com/miekg/dns/blob/master/msg.go#L900 +func ToString(res helpers.Response, opts cli.Options) string { + if res.DNS == nil { + return " MsgHdr" + } + var s string + var opt *dns.OPT + + if !opts.Short { + if opts.Display.Comments { + s += res.DNS.MsgHdr.String() + " " + s += "QUERY: " + strconv.Itoa(len(res.DNS.Question)) + ", " + s += "ANSWER: " + strconv.Itoa(len(res.DNS.Answer)) + ", " + s += "AUTHORITY: " + strconv.Itoa(len(res.DNS.Ns)) + ", " + s += "ADDITIONAL: " + strconv.Itoa(len(res.DNS.Extra)) + "\n" + opt = res.DNS.IsEdns0() + if opt != nil && opts.Display.Opt { + // OPT PSEUDOSECTION + s += opt.String() + "\n" + } + } + if opts.Display.Question { + if len(res.DNS.Question) > 0 { + if opts.Display.Comments { + s += "\n;; QUESTION SECTION:\n" + } + for _, r := range res.DNS.Question { + s += r.String() + "\n" + } + } + } + if opts.Display.Answer { + if len(res.DNS.Answer) > 0 { + if opts.Display.Comments { + s += "\n;; ANSWER SECTION:\n" + } + for _, r := range res.DNS.Answer { + if r != nil { + s += r.String() + "\n" + } + } + } + } + if opts.Display.Authority { + if len(res.DNS.Ns) > 0 { + if opts.Display.Comments { + s += "\n;; AUTHORITY SECTION:\n" + } + for _, r := range res.DNS.Ns { + if r != nil { + s += r.String() + "\n" + } + } + } + } + if opts.Display.Additional { + if len(res.DNS.Extra) > 0 && (opt == nil || len(res.DNS.Extra) > 1) { + if opts.Display.Comments { + s += "\n;; ADDITIONAL SECTION:\n" + } + for _, r := range res.DNS.Extra { + if r != nil && r.Header().Rrtype != dns.TypeOPT { + s += r.String() + "\n" + } + } + } + } + if opts.Display.Statistics { + s += "\n;; Query time: " + res.RTT.String() + // Add extra information to server string + var extra string + switch { + case opts.TCP: + extra = ":" + strconv.Itoa(opts.Port) + " (TCP)" + case opts.TLS: + extra = ":" + strconv.Itoa(opts.Port) + " (TLS)" + case opts.HTTPS, opts.DNSCrypt: + extra = "" + case opts.QUIC: + extra = ":" + strconv.Itoa(opts.Port) + " (QUIC)" + default: + extra = ":" + strconv.Itoa(opts.Port) + " (UDP)" + } + + s += "\n;; SERVER: " + opts.Request.Server + extra + s += "\n;; WHEN: " + time.Now().Format(time.RFC1123Z) + s += "\n;; MSG SIZE rcvd: " + strconv.Itoa(res.DNS.Len()) + "\n" + } + } else { + // Print just the responses, nothing else + for i, resp := range res.DNS.Answer { + temp := strings.Split(resp.String(), "\t") + s += temp[len(temp)-1] + if opts.Identify { + s += " from server " + opts.Request.Server + " in " + res.RTT.String() + } + // Don't print newline on last line + if i != len(res.DNS.Answer)-1 { + s += "\n" + } + + } + } + + return s +} diff --git a/query/struct_test.go b/query/struct_test.go new file mode 100644 index 0000000..3b2d6ba --- /dev/null +++ b/query/struct_test.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package query_test + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +// TODO +func TestToString(t *testing.T) { + assert.Assert(t, 1 == 1+0) +} diff --git a/template.mk b/template.mk index 1698df2..93ffe84 100644 --- a/template.mk +++ b/template.mk @@ -1,13 +1,13 @@ # SPDX-License-Identifier: BSD-3-Clause # Template for the BSD/GNU makefiles -HASH ?= $(shell git describe --always --dirty || echo "UNKNOWN") +HASH ?= `git describe --always --dirty || echo "UNKNOWN"` VER ?= "git-$(HASH)" CGO_ENABLED ?= 0 GO ?= go -GOFLAGS ?= -ldflags "-s -w -X 'main.version=$(VER)'" -buildvcs=false - +COVER ?= $(GO) tool cover +GOFLAGS ?= -ldflags "-s -w -X=main.version=$(VER)" -trimpath PREFIX ?= /usr/local BIN ?= bin @@ -20,19 +20,21 @@ PROG ?= awl # hehe all: $(PROG) doc/$(PROG).1 - doc/$(PROG).1: doc/wiki/$(PROG).1.md @cp doc/awl.1 doc/awl.bak - $(SCDOC) doc/$(PROG).1 2>/dev/null && rm doc/awl.bak || mv doc/awl.bak doc/awl.1 + $(SCDOC) doc/$(PROG).1 && rm doc/awl.bak || mv doc/awl.bak doc/awl.1 + ## test: run go test test: $(GO) test -cover -coverprofile=coverage/coverage.out ./... +coverage/coverage.out: test + $(COVER) -func=coverage/coverage.out + $(COVER) -html=coverage/coverage.out -o coverage/cover.html + ## cover: generates test coverage, output as HTML -cover: test - $(GO) tool cover -func=coverage/coverage.out - $(GO) tool cover -html=coverage/coverage.out -o coverage/cover.html +cover: coverage/coverage.out fmt: gofmt -w -s . @@ -42,7 +44,7 @@ vet: ## lint: lint awl, using fmt, vet and golangci-lint lint: fmt vet - -golangci-lint run + -golangci-lint run --fix ## clean: clean the build files clean: @@ -51,6 +53,6 @@ clean: ## help: Prints this help message help: @echo "Usage: " - @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + @sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' .PHONY: clean lint test fmt vet help \ No newline at end of file diff --git a/util/logger.go b/util/logger.go index 037d4d7..1c4bb03 100644 --- a/util/logger.go +++ b/util/logger.go @@ -5,10 +5,10 @@ package util import "git.froth.zone/sam/awl/logawl" // Initialize the logawl instance. -func InitLogger(verbosity int) (Logger *logawl.Logger) { - Logger = logawl.New() +func InitLogger(verbosity int) (logger *logawl.Logger) { + logger = logawl.New() - Logger.SetLevel(logawl.Level(verbosity)) + logger.SetLevel(logawl.Level(verbosity)) - return + return logger } diff --git a/util/logger_test.go b/util/logger_test.go index a9502be..0dce589 100644 --- a/util/logger_test.go +++ b/util/logger_test.go @@ -11,6 +11,7 @@ import ( ) func TestInitLogger(t *testing.T) { + t.Parallel() logger := util.InitLogger(0) assert.Equal(t, logger.Level, logawl.Level(0)) } diff --git a/util/reverseDNS.go b/util/reverseDNS.go index 57d800e..cfaef14 100644 --- a/util/reverseDNS.go +++ b/util/reverseDNS.go @@ -3,18 +3,29 @@ package util import ( - "errors" "fmt" "strings" "github.com/miekg/dns" ) +type errReverseDNS struct { + addr string +} + +func (e *errReverseDNS) Error() string { + return fmt.Sprintf("reverseDNS: invalid value %s given", e.addr) +} + // Given an IP or phone number, return a canonical string to be queried. func ReverseDNS(address string, querInt uint16) (string, error) { query := dns.TypeToString[querInt] if query == "PTR" { - return dns.ReverseAddr(address) + str, err := dns.ReverseAddr(address) + if err != nil { + return "", fmt.Errorf("could not reverse: %w", err) + } + return str, nil } else if query == "NAPTR" { // get rid of characters not needed replacer := strings.NewReplacer("+", "", " ", "", "-", "") @@ -30,7 +41,7 @@ func ReverseDNS(address string, querInt uint16) (string, error) { return arpa.String(), nil } - return "", errors.New("ReverseDNS: -x flag given but no IP found") + return "", &errReverseDNS{address} } // Reverse a string, return the string in reverse. diff --git a/util/reverseDNS_test.go b/util/reverseDNS_test.go index 7841e40..6e2fae6 100644 --- a/util/reverseDNS_test.go +++ b/util/reverseDNS_test.go @@ -6,7 +6,6 @@ import ( "testing" "git.froth.zone/sam/awl/util" - "github.com/miekg/dns" "gotest.tools/v3/assert" ) @@ -56,5 +55,11 @@ func TestNAPTR(t *testing.T) { func TestInvalid(t *testing.T) { t.Parallel() _, err := util.ReverseDNS("AAAAA", 1) - assert.ErrorContains(t, err, "no IP found") + assert.ErrorContains(t, err, "invalid value AAAAA given") +} + +func TestInvalid2(t *testing.T) { + t.Parallel() + _, err := util.ReverseDNS("1.0", PTR) + assert.ErrorContains(t, err, "could not reverse") }