From 4cf19ebf78b8f4ce095c95667f10ef182d7d09c3 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 11 Aug 2022 07:25:36 +0000 Subject: [PATCH] Another "minor refactor" (#61) I need to make fewer of these :) Reviewed-on: https://git.froth.zone/sam/awl/pulls/61 --- .drone.jsonnet | 15 +- .github/ISSUE_TEMPLATE/bug.md | 8 +- .github/ISSUE_TEMPLATE/feature.md | 3 +- .github/pull_request_template.md | 2 +- .github/workflows/ghrelease.yaml | 27 ++ .github/workflows/test.yaml | 24 ++ .github/workflows/wiki.yaml | 26 ++ .gitignore | 53 ++-- .golangci.yaml | 77 +++++ .goreleaser.yaml | 10 +- GNUmakefile | 8 +- Makefile | 6 +- README.md | 2 - cli/cli.go | 117 ++++--- cli/cli_test.go | 70 ++++- cli/dig.go | 176 ++++++----- cli/dig_test.go | 7 +- cli/docs.go | 2 +- cli/misc.go | 25 +- cli/misc_test.go | 29 +- cli/options.go | 121 ------- conf/docs.go | 2 +- conf/plan9.go | 4 + conf/plan9_test.go | 1 + conf/unix.go | 10 +- conf/unix_test.go | 1 + conf/win.go | 21 +- conf/win_test.go | 2 + doc/awl.1 | 244 --------------- doc/awl.1.scd | 185 +++++++++++ doc/wiki | 2 +- internal/helpers/docs.go | 4 - internal/helpers/query.go | 25 -- internal/helpers/query_test.go | 13 - logawl/docs.go | 5 +- logawl/logawl.go | 34 +- logawl/logger.go | 56 ++-- logawl/logging_test.go | 16 +- main.go | 27 +- main_test.go | 32 +- query/DNSCrypt.go | 22 +- query/DNSCrypt_test.go | 22 +- query/HTTPS.go | 45 ++- query/HTTPS_test.go | 65 ++-- query/QUIC.go | 59 +++- query/QUIC_test.go | 37 ++- query/docs.go | 2 +- query/general.go | 38 ++- query/general_test.go | 37 ++- query/print.go | 48 ++- query/print_test.go | 502 +++++++++++------------------- query/query.go | 34 +- query/query_test.go | 76 +++-- query/resolver.go | 17 +- query/struct.go | 42 ++- query/struct_test.go | 14 - renovate.json | 6 +- template.mk | 32 +- util/docs.go | 2 +- util/logger.go | 10 +- util/logger_test.go | 1 + util/options.go | 113 +++++++ util/options_test.go | 37 +++ util/query.go | 25 ++ util/reverseDNS.go | 7 +- util/reverseDNS_test.go | 7 +- 66 files changed, 1569 insertions(+), 1223 deletions(-) create mode 100644 .github/workflows/ghrelease.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .github/workflows/wiki.yaml create mode 100644 .golangci.yaml delete mode 100644 cli/options.go delete mode 100644 doc/awl.1 create mode 100644 doc/awl.1.scd delete mode 100644 internal/helpers/docs.go delete mode 100644 internal/helpers/query.go delete mode 100644 internal/helpers/query_test.go delete mode 100644 query/struct_test.go create mode 100644 util/options.go create mode 100644 util/options_test.go create mode 100644 util/query.go diff --git a/.drone.jsonnet b/.drone.jsonnet index bfd5b1c..99a4d0d 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -26,7 +26,17 @@ local testing(version, arch) = { name: "test", image: "golang:" + version, commands: [ - "go test -v -race ./... -cover" + "make test-ci" + ], + depends_on: [ + "submodules", + ], + }, + { + name: "fuzz", + image: "golang:" + version, + commands: [ + "make fuzz", ], depends_on: [ "submodules", @@ -65,7 +75,7 @@ local release() = { name: "test", image: "golang", commands: [ - "go test -race ./... -cover" + "make test" ] }, { @@ -89,6 +99,5 @@ local release() = { 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 index a587df9..85c4f8e 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -1,7 +1,6 @@ --- name: Bug report about: Report a bug - --- **Describe the bug** @@ -9,6 +8,7 @@ A clear and concise description of what the bug is. **Reproduction steps** Steps to reproduce the behavior: + 1. 1 2. 2 3. Bug @@ -20,9 +20,9 @@ A clear and concise description of what you expected to happen. 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] +- 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 +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md index 56de423..1af52a4 100644 --- a/.github/ISSUE_TEMPLATE/feature.md +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -1,7 +1,6 @@ --- name: Feature request about: Suggest a feature - --- **Is your feature request related to a problem? Please describe.** @@ -15,4 +14,4 @@ A clear and concise description of any alternative solutions or features you've 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 +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 76f377f..78e5195 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,4 +6,4 @@ Please check the following: 2. Make sure that you test and format your contributions: `make test && make lint` 3. Describe what your pull request does and which issue you're targeting (if any) ---> \ No newline at end of file +--> diff --git a/.github/workflows/ghrelease.yaml b/.github/workflows/ghrelease.yaml new file mode 100644 index 0000000..881b41d --- /dev/null +++ b/.github/workflows/ghrelease.yaml @@ -0,0 +1,27 @@ +name: github-release + +on: + push: + tags: + - "*" + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Checkout submodules + uses: git submodule update --init --recursive + + - name: Release with GoReleaser + uses: goreleaser/goreleaser-action@v3 + with: + distribution: goreleaser + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..dd64356 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,24 @@ +on: push + +jobs: + test-gha: + strategy: + fail-fast: true + matrix: + platform: [macos-latest, windows-latest] + goVer: [1.18 1.19] + runs-on: ${{ matrix.platform }} + steps: + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.goVer }} + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Checkout submodules + uses: git submodule update --init --recursive + + - name: Test + run: go test -race -v ./... diff --git a/.github/workflows/wiki.yaml b/.github/workflows/wiki.yaml new file mode 100644 index 0000000..d49f4c6 --- /dev/null +++ b/.github/workflows/wiki.yaml @@ -0,0 +1,26 @@ +name: Deploy Wiki + +on: + push: + paths: + # Trigger only when wiki directory changes + - "doc/wiki/**" + +jobs: + deploy-wiki: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Checkout submodules + uses: git submodule update --init --recursive + + - name: Push Wiki Changes + uses: Andrew-Chen-Wang/github-wiki-action@v3 + env: + # Make sure you have that / at the end. We use rsync + # WIKI_DIR's default is wiki/ + WIKI_DIR: doc/wiki/ + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_MAIL: ${{ secrets.YOUR_EMAIL }} + GH_NAME: ${{ github.repository_owner }} diff --git a/.gitignore b/.gitignore index 0aa6c52..301fd5b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,28 @@ -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work -dist/ - -# Test coverage -coverage/* -!coverage/.gitkeep - -awl - +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +dist/ + +# Test coverage +coverage/* +!coverage/.gitkeep + +awl +doc/awl.1 + .dccache \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..c95c5a7 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,77 @@ +# Refer to golangci-lint's example config file for more options and information: +# https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml + +run: + timeout: 5m + modules-download-mode: readonly + skip-dirs: + - "coverage" + - ".github" + +linters: + enable: + - errcheck + - errorlint + - gci + - gocritic + - goconst + - godot + - goimports + - govet + - gocritic + - goerr113 + - gofmt + - gofumpt + - gosec + - maintidx + - makezero + - misspell + - nlreturn + - nolintlint + - prealloc + - predeclared + - revive + - staticcheck + # - testpackage + - whitespace + - wrapcheck + - wsl + +linters-settings: + govet: + check-shadowing: true + enable-all: true + disable-all: false + revive: + ignore-generated-header: false + severity: warning + confidence: 0.8 + errorCode: 1 + warningCode: 1 + rules: + # rules can be uncommented after https://github.com/golangci/golangci-lint/issues/2997 is fixed + - name: blank-imports + - name: context-as-argument + # - name: context-keys-type + - name: dot-imports + - name: duplicated-imports + - name: error-return + - name: error-strings + - name: error-naming + # - name: errorf + - name: exported + - name: if-return + - name: increment-decrement + # - name: modifies-value-receiver + - name: package-comments + - name: range + - name: receiver-naming + # - name: time-naming + # - name: unexported-return + # - name: var-declaration + - name: var-naming + +issues: + exclude-use-default: false + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 456a50f..70a9ce1 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -17,7 +17,7 @@ builds: - amd64 - arm64 universal_binaries: -- replace: true + - replace: true archives: - replacements: darwin: macOS @@ -28,14 +28,14 @@ archives: - goos: windows format: zip checksum: - name_template: 'checksums.txt' + name_template: "checksums.txt" snapshot: name_template: "{{ incpatch .Version }}-next" changelog: sort: asc filters: exclude: - - '^docs:' - - '^test:' + - "^docs:" + - "^test:" gitea_urls: - api: https://git.froth.zone/api/v1/ \ No newline at end of file + api: https://git.froth.zone/api/v1/ diff --git a/GNUmakefile b/GNUmakefile index c3957fd..3e681d3 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -6,10 +6,10 @@ ifeq ($(OS),Windows_NT) EXE := $(PROG).exe else EXE := $(PROG) + endif - -$(PROG): +$(PROG): $(SOURCES) $(GO) build -o $(EXE) $(GOFLAGS) . @@ -19,8 +19,8 @@ install: $(GO) install $(GOFLAGS) . else install: all - install -m755 $(PROG) $(PREFIX)/$(BIN) - install -m644 doc/$(PROG).1 $(MAN)/man1 + install -Dm755 $(PROG) $(DESTDIR)$(PREFIX)/$(BIN)/$(PROG) + install -Dm644 doc/$(PROG).1 $(DESTDIR)$(MAN)/man1/$(PROG).1 endif .PHONY: install \ No newline at end of file diff --git a/Makefile b/Makefile index 470145a..1ce08b1 100644 --- a/Makefile +++ b/Makefile @@ -3,12 +3,12 @@ include template.mk -$(PROG): +$(PROG): $(SOURCES) $(GO) build -o $(PROG) $(GOFLAGS) . ## install: installs awl and the manpage, RUN AS ROOT install: all - install -m755 $(PROG) $(PREFIX)/$(BIN) - install -m644 doc/$(PROG).1 $(MAN)/man1 + install -Dm755 $(PROG) $(DESTDIR)$(PREFIX)/$(BIN)/$(PROG) + install -Dm644 doc/$(PROG).1 $(DESTDIR)$(MAN)/man1/$(PROG).1 .PHONY: install \ No newline at end of file diff --git a/README.md b/README.md index 580092a..8a69628 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ newer RFC query types, such as DNS-over-HTTPS and DNS-over-QUIC. - [Feature wiki](https://git.froth.zone/sam/awl/wiki/Supported) - [Manpage](https://git.froth.zone/sam/awl/wiki/awl.1) - ## Building and installing ### From releases @@ -31,7 +30,6 @@ Dependencies: - Go >= 1.18 - GNU/BSD make or Plan 9 mk -- [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (optional, for manpage) Make sure to recursively clone the repo: diff --git a/cli/cli.go b/cli/cli.go index 4b09779..cd98ecf 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -3,6 +3,7 @@ package cli import ( + "errors" "fmt" "os" "runtime" @@ -10,7 +11,6 @@ import ( "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" @@ -18,7 +18,7 @@ import ( // ParseCLI parses arguments given from the CLI and passes them into an `Options` // struct. -func ParseCLI(version string) (Options, error) { +func ParseCLI(version string) (util.Options, error) { flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) flag.Usage = func() { @@ -57,7 +57,7 @@ func ParseCLI(version string) (Options, error) { 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") + mbzflag = flag.String("zflag", "0", "set EDNS z-flag") subnet = flag.String("subnet", "", "set EDNS subnet") padding = flag.Bool("pad", false, "set EDNS padding") @@ -69,14 +69,17 @@ func ParseCLI(version string) (Options, error) { 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: not set)") - 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: not set)") - 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')) + tlsHost = flag.String("tls-host", "", "Server name to use for TLS verification") + noVerify = flag.Bool("tls-no-verify", false, "Disable TLS cert verification") + + aaflag = flag.Bool("aa", false, "set/unset AA (Authoratative Answer) flag (default: not set)") + adflag = flag.Bool("ad", false, "set/unset AD (Authenticated Data) flag (default: not set)") + cdflag = flag.Bool("cd", false, "set/unset CD (Checking Disabled) flag (default: not set)") + qrflag = flag.Bool("qr", false, "set/unset QR (QueRy) flag (default: not set)") + rdflag = flag.Bool("rd", true, "set/unset RD (Recursion Desired) flag (default: set)", flag.OptDisablePrintDefault(true)) + raflag = flag.Bool("ra", false, "set/unset RA (Recursion Available) flag (default: not set)") + tcflag = flag.Bool("tc", false, "set/unset TC (TrunCated) flag (default: not set)") + zflag = flag.Bool("z", false, "set/unset Z (Zero) flag (default: not set)", flag.OptShorthand('z')) 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')) @@ -99,52 +102,53 @@ func ParseCLI(version string) (Options, error) { flag.CommandLine.SortFlags = false // Parse the flags - err := flag.CommandLine.Parse(os.Args[1:]) - if err != nil { - return Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("flag parse error: %w", err) + if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { + return util.Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("flag: %w", err) } // TODO: DRY, dumb dumb. - mbz, err := strconv.ParseInt(*zflag, 0, 16) + mbz, err := strconv.ParseInt(*mbzflag, 0, 16) if err != nil { - return Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("EDNS MBZ error: %w", err) + return util.Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("EDNS MBZ: %w", err) } - opts := Options{ - 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{ + opts := util.Options{ + Logger: util.InitLogger(*verbosity), + Port: *port, + IPv4: *ipv4, + IPv6: *ipv6, + Short: *short, + TCP: *tcp, + DNSCrypt: *dnscrypt, + TLS: *tls, + TLSHost: *tlsHost, + TLSNoVerify: *noVerify, + HTTPS: *https, + QUIC: *quic, + Truncate: *truncate, + ShowQuery: false, + AA: *aaflag, + AD: *adflag, + TC: *tcflag, + Z: *zflag, + CD: *cdflag, + QR: *qrflag, + RD: *rdflag, + RA: *raflag, + Reverse: *reverse, + HumanTTL: false, + ShowTTL: true, + JSON: *json, + XML: *xml, + YAML: *yaml, + Request: util.Request{ Type: dns.StringToType[strings.ToUpper(*qType)], Class: dns.StringToClass[strings.ToUpper(*class)], Name: *query, Timeout: time.Duration(*timeout * float32(time.Second)), Retries: *retry, }, - Display: Displays{ + Display: util.Displays{ Comments: !*noC, Question: !*noQ, Opt: !*noOpt, @@ -153,7 +157,7 @@ func ParseCLI(version string) (Options, error) { Additional: !*noAdd, Statistics: !*noStats, }, - EDNS: EDNS{ + EDNS: util.EDNS{ EnableEDNS: !*edns, Cookie: !*cookie, DNSSEC: *dnssec, @@ -169,9 +173,8 @@ func ParseCLI(version string) (Options, error) { // TODO: DRY if *subnet != "" { - err := parseSubnet(*subnet, &opts) - if err != nil { - return opts, err + if err = util.ParseSubnet(*subnet, &opts); err != nil { + return opts, fmt.Errorf("%w", err) } } @@ -180,6 +183,7 @@ func ParseCLI(version string) (Options, error) { if *versionFlag { fmt.Printf("awl version %s, built with %s\n", version, runtime.Version()) + return opts, ErrNotError } @@ -189,6 +193,7 @@ func ParseCLI(version string) (Options, error) { if err != nil { return opts, err } + opts.Logger.Info("Dig/Drill flags parsed") opts.Logger.Debug(fmt.Sprintf("%+v", opts)) @@ -199,6 +204,7 @@ func ParseCLI(version string) (Options, error) { opts.Port = 53 } } + opts.Logger.Info("Port set to", opts.Port) // Set timeout to 0.5 seconds if set below 0.5 @@ -212,3 +218,16 @@ func ParseCLI(version string) (Options, error) { return opts, nil } + +// ErrNotError is for returning not error. +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/cli/cli_test.go b/cli/cli_test.go index edeb947..0250015 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -11,77 +11,111 @@ 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.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.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 = old + 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 + + os.Args = []string{"awl", "--subnet", "/"} + + opts, err = cli.ParseCLI("TEST") + assert.ErrorContains(t, err, "EDNS subnet") + os.Args = old } -// nolint: paralleltest -func TestInvalidFlag(t *testing.T) { +func TestMBZ(t *testing.T) { //nolint: paralleltest // Race conditions + old := os.Args + os.Args = []string{"awl", "--zflag", "G"} + + _, err := cli.ParseCLI("TEST") + + assert.ErrorContains(t, err, "EDNS MBZ") + + os.Args = old +} + +func TestInvalidFlag(t *testing.T) { //nolint: paralleltest // Race conditions old := os.Args os.Args = []string{"awl", "--treebug"} + _, err := cli.ParseCLI("TEST") + assert.ErrorContains(t, err, "unknown flag") + os.Args = old } -// nolint: paralleltest -func TestInvalidDig(t *testing.T) { +func TestInvalidDig(t *testing.T) { //nolint: paralleltest // Race conditions old := os.Args os.Args = []string{"awl", "+a"} + _, err := cli.ParseCLI("TEST") + assert.ErrorContains(t, err, "digflags: invalid argument") + os.Args = old } -// nolint: paralleltest -func TestVersion(t *testing.T) { +func TestVersion(t *testing.T) { //nolint: paralleltest // Race conditions old := os.Args os.Args = []string{"awl", "--version"} + _, err := cli.ParseCLI("TEST") + assert.ErrorType(t, err, cli.ErrNotError) + os.Args = old } -// nolint: paralleltest -func TestTimeout(t *testing.T) { +func TestTimeout(t *testing.T) { //nolint: paralleltest // Race conditions args := [][]string{ {"awl", "+timeout=0"}, {"awl", "--timeout", "0"}, @@ -89,15 +123,17 @@ func TestTimeout(t *testing.T) { for _, test := range args { old := os.Args os.Args = test + opt, err := cli.ParseCLI("TEST") + assert.NilError(t, err) assert.Equal(t, opt.Request.Timeout, time.Second/2) + os.Args = old } } -// nolint: paralleltest -func TestRetries(t *testing.T) { +func TestRetries(t *testing.T) { //nolint: paralleltest // Race conditions args := [][]string{ {"awl", "+retry=-2"}, {"awl", "+tries=-2"}, @@ -106,14 +142,16 @@ func TestRetries(t *testing.T) { for _, test := range args { old := os.Args os.Args = test + opt, err := cli.ParseCLI("TEST") + assert.NilError(t, err) assert.Equal(t, opt.Request.Retries, 0) + os.Args = old } } -// nolint: paralleltest func FuzzFlags(f *testing.F) { testcases := []string{"git.froth.zone", "", "!12345", "google.com.edu.org.fr"} for _, tc := range testcases { @@ -121,8 +159,10 @@ func FuzzFlags(f *testing.F) { } f.Fuzz(func(t *testing.T, orig string) { + old := os.Args os.Args = []string{"awl", orig} - //nolint:errcheck // Only make sure the program does not crash + //nolint:errcheck,gosec // Only make sure the program does not crash cli.ParseCLI("TEST") + os.Args = old }) } diff --git a/cli/dig.go b/cli/dig.go index 3f48022..62b0986 100644 --- a/cli/dig.go +++ b/cli/dig.go @@ -7,18 +7,21 @@ import ( "strconv" "strings" "time" + + "git.froth.zone/sam/awl/util" ) // 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 { +func ParseDig(arg string, opts *util.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 { @@ -118,85 +121,100 @@ func ParseDig(arg string, opts *Options) error { opts.Display.UcodeTranslate = isNo default: - // Recursive switch statements WOO - 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) - } - - opts.Request.Timeout = time.Duration(timeout) - } else { - return fmt.Errorf("digflags: Invalid timeout value: %w", errNoArg) - } - - 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 - - // 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) - } - - 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) - } - - 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 &errInvalidArg{arg[0]} + if err := parseDigEq(isNo, arg, opts); err != nil { + return err } } + + return nil +} + +// For flags that contain "=". +func parseDigEq(startNo bool, arg string, opts *util.Options) error { + // Recursive switch statements WOO + arg, val, isSplit := strings.Cut(arg, "=") + switch arg { + case "time", "timeout": + if isSplit && val != "" { + timeout, err := strconv.Atoi(val) + if err != nil { + return fmt.Errorf("digflags: timeout : %w", err) + } + + opts.Request.Timeout = time.Duration(timeout) + } else { + return fmt.Errorf("digflags: timeout: %w", errNoArg) + } + + case "retry", "tries": + if isSplit && val != "" { + tries, err := strconv.Atoi(val) + if err != nil { + return fmt.Errorf("digflags: retry: %w", err) + } + + opts.Request.Retries = tries + + // TODO: Is there a better way to do this? + if arg == "tries" { + opts.Request.Retries++ + } + } else { + return fmt.Errorf("digflags: retry: %w", errNoArg) + } + + case "bufsize": + if isSplit && val != "" { + size, err := strconv.Atoi(val) + if err != nil { + return fmt.Errorf("digflags: EDNS UDP: %w", err) + } + + opts.EDNS.BufSize = uint16(size) + } else { + return fmt.Errorf("digflags: EDNS UDP: %w", errNoArg) + } + + case "ednsflags": + if isSplit && val != "" { + ver, err := strconv.ParseInt(val, 0, 16) + if err != nil { + return fmt.Errorf("digflags: EDNS flag: %w", err) + } + + // Ignore setting DO bit + opts.EDNS.ZFlag = uint16(ver & 0x7FFF) + } else { + opts.EDNS.ZFlag = 0 + } + + case "edns": + opts.EDNS.EnableEDNS = startNo + + if isSplit && val != "" { + ver, err := strconv.Atoi(val) + if err != nil { + return fmt.Errorf("digflags: EDNS version: %w", err) + } + + opts.EDNS.Version = uint8(ver) + } else { + opts.EDNS.Version = 0 + } + + case "subnet": + if isSplit && val != "" { + err := util.ParseSubnet(val, opts) + if err != nil { + return fmt.Errorf("digflags: EDNS Subnet: %w", err) + } + } else { + return fmt.Errorf("digflags: EDNS Subnet: %w", errNoArg) + } + + default: + return &errInvalidArg{arg} + } + return nil } diff --git a/cli/dig_test.go b/cli/dig_test.go index 03e84d8..c21a19e 100644 --- a/cli/dig_test.go +++ b/cli/dig_test.go @@ -12,6 +12,7 @@ import ( func FuzzDig(f *testing.F) { f.Log("ParseDig Fuzzing") + seeds := []string{ "aaflag", "aaonly", "noaaflag", "noaaonly", "adflag", "noadflag", @@ -59,15 +60,15 @@ func FuzzDig(f *testing.F) { "idnout", "noidnout", "invalid", } + for _, tc := range seeds { f.Add(tc) } f.Fuzz(func(t *testing.T, orig string) { - opts := new(cli.Options) + opts := new(util.Options) opts.Logger = util.InitLogger(0) - err := cli.ParseDig(orig, opts) - if err != nil { + if err := cli.ParseDig(orig, opts); err != nil { assert.ErrorContains(t, err, "digflags:") } }) diff --git a/cli/docs.go b/cli/docs.go index dbc972b..d943551 100644 --- a/cli/docs.go +++ b/cli/docs.go @@ -1,5 +1,5 @@ /* -The CLI part of the package, including both POSIX +Package cli is 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 index 282d52d..cec1fb6 100644 --- a/cli/misc.go +++ b/cli/misc.go @@ -15,17 +15,19 @@ import ( // 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 { +func ParseMiscArgs(args []string, opts *util.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, "@"): arg = arg[1:] // Automatically set flags based on URI header opts.Logger.Info(arg, "detected as a server") + switch { case strings.HasPrefix(arg, "tls://"): opts.TLS = true @@ -51,6 +53,7 @@ func ParseMiscArgs(args []string, opts *Options) error { case strings.HasPrefix(arg, "+"): opts.Logger.Info(arg, "detected as a dig query") err = ParseDig(strings.ToLower(arg[1:]), opts) + if err != nil { return err } @@ -59,8 +62,9 @@ func ParseMiscArgs(args []string, opts *Options) error { 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) + return fmt.Errorf("unicode to punycode: %w", err) } // DNS query type @@ -72,8 +76,9 @@ func ParseMiscArgs(args []string, opts *Options) error { default: opts.Logger.Info(arg, "is unknown. Assuming domain") opts.Request.Name, err = idna.ToASCII(arg) + if err != nil { - return fmt.Errorf("punycode translate error: %w", err) + return fmt.Errorf("unicode to punycode: %w", err) } } } @@ -82,11 +87,13 @@ func ParseMiscArgs(args []string, opts *Options) error { if opts.Request.Name == "" { opts.Logger.Info("Domain not specified, making a default") opts.Request.Name = "." + if opts.Request.Type == 0 { opts.Request.Type = dns.StringToType["NS"] } } else { opts.Logger.Info("Query not specified, making an \"A\" query") + if opts.Request.Type == 0 { opts.Request.Type = dns.StringToType["A"] } @@ -106,7 +113,9 @@ func ParseMiscArgs(args []string, opts *Options) error { case opts.QUIC: opts.Request.Server = "dns.adguard.com" default: + //nolint:govet // This shadow is intentional resolv, err := conf.GetDNSConfig() + if err != nil { // :^) opts.Logger.Warn("Could not query system for server. Using default") @@ -119,40 +128,48 @@ func ParseMiscArgs(args []string, opts *Options) error { case opts.IPv4: if strings.Contains(srv, ".") { opts.Request.Server = srv + break harmful } case opts.IPv6: if strings.Contains(srv, ":") { opts.Request.Server = srv + break harmful } default: //#nosec -- This isn't used for anything secure opts.Request.Server = resolv.Servers[rand.Intn(len(resolv.Servers))] + break harmful } } } } } + opts.Logger.Info("DNS server set to", opts.Request.Server) // Make reverse adresses proper addresses if opts.Reverse { opts.Logger.Info("Making reverse DNS query proper *.arpa domain") + if dns.TypeToString[opts.Request.Type] == "A" { opts.Request.Type = dns.StringToType["PTR"] } + opts.Request.Name, err = util.ReverseDNS(opts.Request.Name, opts.Request.Type) if err != nil { - return fmt.Errorf("reverse DNS error: %w", err) + return fmt.Errorf("reverse DNS: %w", err) } } // if the domain is not canonical, make it canonical if !strings.HasSuffix(opts.Request.Name, ".") { opts.Request.Name = fmt.Sprintf("%s.", opts.Request.Name) + opts.Logger.Debug("Domain made canonical") } + return nil } diff --git a/cli/misc_test.go b/cli/misc_test.go index 80afee5..16d8b50 100644 --- a/cli/misc_test.go +++ b/cli/misc_test.go @@ -14,13 +14,14 @@ import ( func TestParseArgs(t *testing.T) { t.Parallel() + args := []string{ "go.dev", "AAAA", "@1.1.1.1", "+ignore", } - opts := new(cli.Options) + opts := new(util.Options) opts.Logger = util.InitLogger(0) err := cli.ParseMiscArgs(args, opts) assert.NilError(t, err) @@ -32,8 +33,9 @@ func TestParseArgs(t *testing.T) { func TestParseNoInput(t *testing.T) { t.Parallel() + args := []string{} - opts := new(cli.Options) + opts := new(util.Options) opts.Logger = util.InitLogger(0) err := cli.ParseMiscArgs(args, opts) assert.NilError(t, err) @@ -43,10 +45,11 @@ func TestParseNoInput(t *testing.T) { func TestParseA(t *testing.T) { t.Parallel() + args := []string{ "golang.org.", } - opts := new(cli.Options) + opts := new(util.Options) opts.Logger = util.InitLogger(0) err := cli.ParseMiscArgs(args, opts) assert.NilError(t, err) @@ -56,8 +59,9 @@ func TestParseA(t *testing.T) { func TestParsePTR(t *testing.T) { t.Parallel() + args := []string{"8.8.8.8"} - opts := new(cli.Options) + opts := new(util.Options) opts.Logger = util.InitLogger(0) opts.Reverse = true err := cli.ParseMiscArgs(args, opts) @@ -67,8 +71,9 @@ func TestParsePTR(t *testing.T) { func TestParseInvalidPTR(t *testing.T) { t.Parallel() + args := []string{"8.88.8"} - opts := new(cli.Options) + opts := new(util.Options) opts.Logger = util.InitLogger(0) opts.Reverse = true err := cli.ParseMiscArgs(args, opts) @@ -77,6 +82,7 @@ func TestParseInvalidPTR(t *testing.T) { func TestDefaultServer(t *testing.T) { t.Parallel() + tests := []struct { in string want string @@ -86,12 +92,13 @@ func TestDefaultServer(t *testing.T) { {"HTTPS", "https://dns.cloudflare.com/dns-query"}, {"QUIC", "dns.adguard.com"}, } + for _, test := range tests { test := test t.Run(test.in, func(t *testing.T) { t.Parallel() args := []string{} - opts := new(cli.Options) + opts := new(util.Options) opts.Logger = util.InitLogger(0) switch test.in { case "DNSCRYPT": @@ -112,6 +119,7 @@ func TestDefaultServer(t *testing.T) { func TestFlagSetting(t *testing.T) { t.Parallel() + tests := []struct { in []string }{ @@ -120,11 +128,12 @@ func TestFlagSetting(t *testing.T) { {[]string{"@https://dns.cloudflare.com/dns-query"}}, {[]string{"@quic://dns.adguard.com"}}, } + for i, test := range tests { test := test i := i t.Run(strconv.Itoa(i), func(t *testing.T) { - opts := new(cli.Options) + opts := new(util.Options) opts.Logger = util.InitLogger(0) t.Parallel() err := cli.ParseMiscArgs(test.in, opts) @@ -151,14 +160,16 @@ func FuzzParseArgs(f *testing.F) { "+ignore", "e", } + for _, tc := range cases { f.Add(tc) } + f.Fuzz(func(t *testing.T, arg string) { args := []string{arg} - opts := new(cli.Options) + opts := new(util.Options) opts.Logger = util.InitLogger(0) - //nolint:errcheck // Only make sure the program does not crash + //nolint:errcheck,gosec // Only make sure the program does not crash cli.ParseMiscArgs(args, opts) }) } diff --git a/cli/options.go b/cli/options.go deleted file mode 100644 index 9508093..0000000 --- a/cli/options.go +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -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. -type Options struct { - Logger *logawl.Logger // Logger - Port int // DNS port - IPv4 bool // Force IPv4 - IPv6 bool // Force IPv6 - 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 - 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 - 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. -type Displays struct { - 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/docs.go b/conf/docs.go index c57cfb5..cae2ac7 100644 --- a/conf/docs.go +++ b/conf/docs.go @@ -1,5 +1,5 @@ /* -Helper functions for getting local nameservers +Package conf contains helper functions for getting local nameservers Currently supported: Unix, Windows, Plan 9 (tested on 9front) */ diff --git a/conf/plan9.go b/conf/plan9.go index 9da5690..2516cee 100644 --- a/conf/plan9.go +++ b/conf/plan9.go @@ -9,6 +9,7 @@ import ( "github.com/miekg/dns" ) +// GetPlan9Config gets DNS information from Plan 9, because it's different from UNIX and Windows. // Plan 9 stores its network data in /net/ndb, which seems to be formatted a specific way // Yoink it and use it. // @@ -16,12 +17,15 @@ import ( func GetPlan9Config(str string) (*dns.ClientConfig, error) { str = strings.ReplaceAll(str, "\n", "") spl := strings.FieldsFunc(str, splitChars) + var servers []string + for _, option := range spl { if strings.HasPrefix(option, "dns=") { servers = append(servers, strings.TrimPrefix(option, "dns=")) } } + if len(servers) == 0 { return nil, errPlan9 } diff --git a/conf/plan9_test.go b/conf/plan9_test.go index a5123fc..ffd68eb 100644 --- a/conf/plan9_test.go +++ b/conf/plan9_test.go @@ -11,6 +11,7 @@ import ( func TestGetPlan9Config(t *testing.T) { t.Parallel() + ndbs := []struct { in string want string diff --git a/conf/unix.go b/conf/unix.go index 9809b48..c2a24b3 100644 --- a/conf/unix.go +++ b/conf/unix.go @@ -11,15 +11,21 @@ import ( "github.com/miekg/dns" ) -// Get the DNS configuration, either from /etc/resolv.conf or somewhere else +// GetDNSConfig gets the DNS configuration, either from /etc/resolv.conf or somewhere else. func GetDNSConfig() (*dns.ClientConfig, error) { if runtime.GOOS == "plan9" { dat, err := os.ReadFile("/net/ndb") if err != nil { return nil, fmt.Errorf("plan9 ndb: %w", err) } + return GetPlan9Config(string(dat)) } else { - return dns.ClientConfigFromFile("/etc/resolv.conf") + conf, err := dns.ClientConfigFromFile("/etc/resolv.conf") + if err != nil { + return nil, fmt.Errorf("unix config: %w", err) + } + + return conf, nil } } diff --git a/conf/unix_test.go b/conf/unix_test.go index 64df984..54a374c 100644 --- a/conf/unix_test.go +++ b/conf/unix_test.go @@ -15,6 +15,7 @@ func TestNonWinConfig(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Running Windows, skipping") } + conf, err := conf.GetDNSConfig() assert.NilError(t, err) assert.Assert(t, len(conf.Servers) != 0) diff --git a/conf/win.go b/conf/win.go index 6b5712e..71f15a6 100644 --- a/conf/win.go +++ b/conf/win.go @@ -17,37 +17,48 @@ import ( https://gist.github.com/moloch--/9fb1c8497b09b45c840fe93dd23b1e98 */ -// WindowsDnsClientConfig - returns all DNS server addresses using windows fuckery. +// GetDNSConfig (Windows version) returns all DNS server addresses using windows fuckery. +// +// Here be dragons. func GetDNSConfig() (*dns.ClientConfig, error) { - l := uint32(20000) - b := make([]byte, l) + length := uint32(20000) + byt := make([]byte, length) // 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 { + //nolint:gosec // This is necessary unless we want to drop 1.18 + if err := windows.GetAdaptersAddresses(windows.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&byt[0])), &length); err != nil { 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 { + //nolint:gosec // This is necessary unless we want to drop 1.18 + for addr := (*windows.IpAdapterAddresses)(unsafe.Pointer(&byt[0])); addr != nil; addr = addr.Next { addresses = append(addresses, addr) } resolvers := map[string]bool{} + for _, addr := range addresses { for next := addr.FirstUnicastAddress; next != nil; next = next.Next { if addr.OperStatus != windows.IfOperStatusUp { continue } + if next.Address.IP() != nil { for dnsServer := addr.FirstDnsServerAddress; dnsServer != nil; dnsServer = dnsServer.Next { ip := dnsServer.Address.IP() + if ip.IsMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() { continue } + if ip.To16() != nil && strings.HasPrefix(ip.To16().String(), "fec0:") { continue } + resolvers[ip.String()] = true } + break } } diff --git a/conf/win_test.go b/conf/win_test.go index 0b1531e..3da53b3 100644 --- a/conf/win_test.go +++ b/conf/win_test.go @@ -15,7 +15,9 @@ func TestWinConfig(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("Not running Windows, skipping") } + conf, err := conf.GetDNSConfig() + assert.NilError(t, err) assert.Assert(t, len(conf.Servers) != 0) } diff --git a/doc/awl.1 b/doc/awl.1 deleted file mode 100644 index 1da461e..0000000 --- a/doc/awl.1 +++ /dev/null @@ -1,244 +0,0 @@ -.\" 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-08-03" -.PP -.SH NAME -awl - DNS lookup tool -.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 leatherworking.\& -.PP -\fBawl\fR is designed to be a more "modern" version of \fIdrill\fR(1) by including -some more recent 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 -Dig-like +[no]flags are supported, see dig(1) -.PP -\fB-D\fR, \fB--dnssec\fR, \fB+dnssec\fR -.br - Enable DNSSEC.\& This needs to be manually enabled.\& -.PP -\fB-v\fR \fIvalue\fR -.br - Set verbosity (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-q\fR, \fB--query\fR \fIdomain\fR -.br - Domain to query (eg.\& example.\&com) -.PP -\fB-c\fR, \fB--class\fR \fIclass\fR -.br - DNS class to query (eg.\& IN, CH) -.PP -\fB-t\fR, \fB--qType\fR \fItype\fR -.br - DNS type to query (eg.\& A, NS) -.PP -\fB--no-truncate\fR, \fB+ignore\fR -.br - Ignore UDP truncation (by default, awl \fIretries with TCP\fR) -.PP -\fB--tcp\fR, \fB+tcp\fR, \fB+vc\fR -.br - Use TCP for the query (see \fIRFC 7766\fR) -.PP -\fB--dnscrypt\fR, \fB+dnscrypt\fR -.br - Use DNSCrypt -.PP -\fB-T\fR, \fB--tls\fR, \fB+tls\fR -.br - Use DNS-over-TLS, implies \fB--tcp\fR (see \fIRFC 7858\fR) -.PP -\fB-H\fR.\& \fB--https\fR, \fB+https\fR -.br - Use DNS-over-HTTPS (see \fIRFC 8484\fR) -.PP -\fB-Q\fR.\& \fB--quic\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 -\fB--timeout\fR \fIseconds\fR, \fB+timeout=\fR\fIseconds\fR -.br - Set the timeout period.\& Floating point numbers are accepted.\& -.br - 0.\&5 seconds is the minimum.\& -.PP -\fB--retries\fR \fIint\fR, \fB+tries\fR=\fIint\fR, \fB+ retry\fR=\fIint\fR -.br - Set the number of retries.\& -.br - Retry is one more than tries, dig style -.PP -.RE -.SS DNS Flags -.PP -.RS 4 -\fB--aa=[false]\fR, \fB+[no]aaflag\fR -.br - (Set, Unset) AA (Authoritative Answer) flag -.PP -\fB--ad=[false]\fR, \fB+[no]adflag\fR -.br - (Set, Unset) AD (Authenticated Data) flag -.PP -\fB--tc=[false]\fR, \fB+[no]tcflag\fR -.br - (Set, Unset) TC (TrunCated) flag -.PP -\fB-z=[false]\fR, \fB+[no]zflag\fR -.br - (Set, Unset) Z (Zero) flag -.PP -\fB--cd=[false]\fR, \fB+[no]cdflag\fR -.br - (Set, Unset) CD (Checking Disabled) flag -.PP -\fB--qr=[false]\fR, \fB+[no]qrflag\fR -.br - (Set, Unset) QR (QueRy) flag -.PP -\fB--rd=[true]\fR, \fB+[no]rdflag\fR -.br - (Set, Unset) RD (Recursion Desired) flag -.PP -\fB--ra=[false]\fR, \fB+[no]raflag\fR -.br - (Set, Unset) RA (Recursion Available) flag -.PP -.RE -.SS Output Display -.RS 4 -\fB--no-question\fR, \fB+noquestion\fR -.br - Do not display the Question section -.PP -\fB--no-answer\fR, \fB+noanswer\fR -.br - Do not display the Answer section -.PP -\fB--no-answer\fR, \fB+noanswer\fR -.br - Do not display the Answer section -.PP -\fB--no-authority\fR, \fB+noauthority\fR -.br - Do not display the Authority section -.PP -\fB--no-additional\fR, \fB+noadditional\fR -.br - Do not display the Additional section -.PP -\fB--no-statistics\fR, \fB+nostats\fR -.br - Do not display the Statistics (additional comments) section -.PP -.RE -.SS Output Formats -.RS 4 -\fB-j\fR, \fB--json\fR, \fB+json\fR -.br - Print the query results as JSON.\& -.PP -\fB-X\fR, \fB--xml\fR, \fB+xml\fR -.br - Print the query results as XML.\& -.PP -\fB-y\fR, \fB--yaml\fR, \fB+yaml\fR -.br - Print the query results as YAML.\& -.PP -\fB-s\fR, \fB--short\fR, \fB+short\fR -.br - Print just the address of the answer.\& -.PP -.RE -.SH EXAMPLES -.nf -.RS 4 -awl grumbulon\&.xyz -j +cd -.fi -.RE -Run a query of your local resolver for the A records of grumbulon.\&xyz, print -them as JSON and disable DNSSEC verification.\& -.PP -.nf -.RS 4 -awl +short example\&.com AAAA @1\&.1\&.1\&.1 -.fi -.RE -Query 1.\&1.\&1.\&1 for the AAAA records of example.\&com, print just the answers -.PP -.nf -.RS 4 -awl -xT PTR 8\&.8\&.4\&.4 @dns\&.google -.fi -.RE -Query dns.\&google over TLS for the PTR record to the IP address 8.\&8.\&4.\&4 -.PP -.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.scd b/doc/awl.1.scd new file mode 100644 index 0000000..51cb2d3 --- /dev/null +++ b/doc/awl.1.scd @@ -0,0 +1,185 @@ +awl(1) + +# NAME + +awl - DNS lookup tool + +# 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 leatherworking. + +_awl_ is designed to be a more "modern" version of _drill_(1) by including +some more recent RFCs and output options. _awl_ is still heavily +Work-In-Progress so some features may get added or removed. + +# OPTIONS + +Dig-like +[no]flags are supported, see dig(1) + +_-D_, _--dnssec_, _+dnssec_ + Enable DNSSEC. This needs to be manually enabled. + +_-v_ _value_ + Set verbosity (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 listed below. + + - _53_ for _UDP_ and _TCP_ + - _853_ for _TLS_ and _QUIC_ + - _443_ for _HTTPS_ + +_-q_, _--query_ _domain_ + Domain to query (eg. example.com) + +_-c_, _--class_ _class_ + DNS class to query (eg. IN, CH) + +_-t_, _--qType_ _type_ + DNS type to query (eg. A, NS) + +_--no-truncate_, _+ignore_ + Ignore UDP truncation (by default, awl _retries with TCP_) + +_--tcp_, _+tcp_, _+vc_ + Use TCP for the query (see _RFC 7766_) + +_--dnscrypt_, _+dnscrypt_ + Use DNSCrypt + +_-T_, _--tls_, _+tls_ + Use DNS-over-TLS, implies _--tcp_ (see _RFC 7858_) + +_--tls-host_ _string_ + Set hostname to use for TLS certificate validation. + Default is the name of the domain when querying over TLS, and empty for IPs. + +_--tls-no-verify_ + Ignore TLS validation when performing a DNS query. + +_-H_. _--https_, _+https_ + Use DNS-over-HTTPS (see _RFC 8484_) + +_-Q_. _--quic_, _+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. + +_--timeout_ _seconds_, _+timeout=__seconds_ + Set the timeout period. Floating point numbers are accepted.++ +0.5 seconds is the minimum. + +_--retries_ _int_, _+tries_=_int_, _+ retry_=_int_ + Set the number of retries. + Retry is one more than tries, dig style + +## DNS Flags + +_--aa=[false]_, _+[no]aaflag_ + (Set, Unset) AA (Authoritative Answer) flag + +_--ad=[false]_, _+[no]adflag_ + (Set, Unset) AD (Authenticated Data) flag + +_--tc=[false]_, _+[no]tcflag_ + (Set, Unset) TC (TrunCated) flag + +_-z=[false]_, _+[no]zflag_ + (Set, Unset) Z (Zero) flag + +_--cd=[false]_, _+[no]cdflag_ + (Set, Unset) CD (Checking Disabled) flag + +_--qr=[false]_, _+[no]qrflag_ + (Set, Unset) QR (QueRy) flag + +_--rd=[true]_, _+[no]rdflag_ + (Set, Unset) RD (Recursion Desired) flag + +_--ra=[false]_, _+[no]raflag_ + (Set, Unset) RA (Recursion Available) flag + +## Output Display + +_--no-question_, _+noquestion_ + Do not display the Question section + +_--no-answer_, _+noanswer_ + Do not display the Answer section + +_--no-answer_, _+noanswer_ + Do not display the Answer section + +_--no-authority_, _+noauthority_ + Do not display the Authority section + +_--no-additional_, _+noadditional_ + Do not display the Additional section + +_--no-statistics_, _+nostats_ + Do not display the Statistics (additional comments) section + +## Output Formats + +_-j_, _--json_, _+json_ + Print the query results as JSON. + +_-X_, _--xml_, _+xml_ + Print the query results as XML. + +_-y_, _--yaml_, _+yaml_ + Print the query results as YAML. + +_-s_, _--short_, _+short_ + Print just the address of the answer. + +# EXAMPLES + +``` +awl grumbulon.xyz -j +cd +``` + +Run a query of your local resolver for the A records of grumbulon.xyz, print +them as JSON and disable DNSSEC verification. + +``` +awl +short example.com AAAA @1.1.1.1 +``` + +Query 1.1.1.1 for the AAAA records of example.com, print just the answers + +``` +awl -xT PTR 8.8.4.4 @dns.google +``` + +Query dns.google over TLS for the PTR record to the IP address 8.8.4.4 + +# SEE ALSO + +_drill_(1), _dig_(1), the many DNS RFCs diff --git a/doc/wiki b/doc/wiki index 0fba1fb..d25a7f1 160000 --- a/doc/wiki +++ b/doc/wiki @@ -1 +1 @@ -Subproject commit 0fba1fbe4b12e8c88514b3f7d98be3e75a5a034d +Subproject commit d25a7f13273737938d5e1e08c7332d39fffadc6b diff --git a/internal/helpers/docs.go b/internal/helpers/docs.go deleted file mode 100644 index 945e5a6..0000000 --- a/internal/helpers/docs.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Useful structs used everywhere I couldn't find a better place to shove -*/ -package helpers diff --git a/internal/helpers/query.go b/internal/helpers/query.go deleted file mode 100644 index ddc0943..0000000 --- a/internal/helpers/query.go +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -package helpers - -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 - Timeout time.Duration // The maximum timeout - Retries int // Number of queries to retry -} diff --git a/internal/helpers/query_test.go b/internal/helpers/query_test.go deleted file mode 100644 index f3f5da5..0000000 --- a/internal/helpers/query_test.go +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -package helpers_test - -import ( - "testing" - - "gotest.tools/v3/assert" -) - -func TestNothing(t *testing.T) { - assert.Equal(t, 0, 0) -} diff --git a/logawl/docs.go b/logawl/docs.go index ac20e97..f1caa3b 100644 --- a/logawl/docs.go +++ b/logawl/docs.go @@ -1,5 +1,5 @@ /* -LogAwl is a package for custom logging needs +Package logawl is a package for custom logging needs LogAwl extends the standard log library with support for log levels This is _different_ from the syslog package in the standard library because you do not define a file @@ -19,7 +19,8 @@ because awl is a cli utility it writes directly to std err. // You may also set the log level on the fly with // // Logger.SetLevel(3) -// This allows you to change the default level (Info) and prevent log messages from being posted at higher verbosity levels +// This allows you to change the default level (Info) +// and prevent log messages from being posted at higher verbosity levels // for example if // Logger.SetLevel(3) // is not called and you call diff --git a/logawl/logawl.go b/logawl/logawl.go index 52701b6..30ff143 100644 --- a/logawl/logawl.go +++ b/logawl/logawl.go @@ -10,23 +10,26 @@ import ( ) type ( - Level int32 + // Level is the logging level. + Level int32 + + // Logger is the overall logger. Logger struct { + Out io.Writer + Prefix string + buf []byte Mu sync.Mutex Level Level isDiscard int32 - Prefix string - Out io.Writer - buf []byte } ) -// Stores whatever input value is in mem address of l.level. +// SetLevel stores whatever input value is in mem address of l.level. func (l *Logger) SetLevel(level Level) { atomic.StoreInt32((*int32)(&l.Level), int32(level)) } -// Mostly nothing. +// GetLevel gets the logger level. func (l *Logger) GetLevel() Level { return l.level() } @@ -36,7 +39,7 @@ func (l *Logger) level() Level { return Level(atomic.LoadInt32((*int32)(&l.Level))) } -// Unmarshalls the int value of level for writing the header. +// UnMarshalLevel unmarshalls the int value of level for writing the header. func (l *Logger) UnMarshalLevel(lv Level) (string, error) { switch lv { case 0: @@ -48,13 +51,16 @@ func (l *Logger) UnMarshalLevel(lv Level) (string, error) { case 3: return "DEBUG ", nil } + return "", errInvalidLevel } +// IsLevel returns true if the logger level is above the level given. func (l *Logger) IsLevel(level Level) bool { return l.level() >= level } +// AllLevels is an array of all valid log levels. var AllLevels = []Level{ ErrLevel, WarnLevel, @@ -63,15 +69,21 @@ var AllLevels = []Level{ } const ( - // Fatal logs (will call exit(1)). + // ErrLevel is the fatal (error) log level. ErrLevel Level = iota - // Error logs. + // WarnLevel is for warning logs. + // + // Example: when one setting implies another, when a request fails but is retried. WarnLevel - // What is going on level. + // InfoLevel is for saying what is going on when. + // This is essentially the "verbose" option. + // + // When in doubt, use info. InfoLevel - // Verbose log level. + + // DebugLevel is for spewing debug structs/interfaces. DebugLevel ) diff --git a/logawl/logger.go b/logawl/logger.go index aaf29a7..e6aebfd 100644 --- a/logawl/logger.go +++ b/logawl/logger.go @@ -9,7 +9,7 @@ import ( "time" ) -// Calling New instantiates Logger +// New instantiates Logger // // Level can be changed to one of the other log levels (ErrorLevel, WarnLevel, InfoLevel, DebugLevel). func New() *Logger { @@ -19,7 +19,7 @@ func New() *Logger { } } -// Takes any and prints it out to Logger -> Out (io.Writer (default is std.Err)). +// Println 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 @@ -27,24 +27,20 @@ func (l *Logger) Println(level Level, v ...any) { // 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 (currently nothing) - case 0: - err := l.Printer(0, fmt.Sprintln(v...)) // Error level - if err != nil { + case ErrLevel: + if err := l.Printer(ErrLevel, fmt.Sprintln(v...)); err != nil { fmt.Fprintln(os.Stderr, "Logger failed: ", err) } - case 1: - err := l.Printer(1, fmt.Sprintln(v...)) // Warn level - if err != nil { + case WarnLevel: + if err := l.Printer(WarnLevel, fmt.Sprintln(v...)); err != nil { fmt.Fprintln(os.Stderr, "Logger failed: ", err) } - case 2: - err := l.Printer(2, fmt.Sprintln(v...)) // Info level - if err != nil { + case InfoLevel: + if err := l.Printer(InfoLevel, fmt.Sprintln(v...)); err != nil { fmt.Fprintln(os.Stderr, "Logger failed: ", err) } - case 3: - err := l.Printer(3, fmt.Sprintln(v...)) // Debug level - if err != nil { + case DebugLevel: + if err := l.Printer(DebugLevel, fmt.Sprintln(v...)); err != nil { fmt.Fprintln(os.Stderr, "Logger failed: ", err) } default: @@ -53,13 +49,14 @@ func (l *Logger) Println(level Level, v ...any) { } } -// Formats the log header as such YYYY/MM/DD HH:MM:SS (local time) . +// FormatHeader 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 // maybe there can be an append func or something in the future *buf = append(*buf, lvl...) year, month, day := t.Date() + *buf = append(*buf, '[') formatter(buf, year, 4) *buf = append(*buf, '/') @@ -79,46 +76,57 @@ func (l *Logger) FormatHeader(buf *[]byte, t time.Time, line int, level Level) e } else { return errInvalidLevel } + return nil } // Printer prints the formatted message directly to stdErr. func (l *Logger) Printer(level Level, s string) error { now := time.Now() + var line int + l.Mu.Lock() defer l.Mu.Unlock() l.buf = l.buf[:0] - err := l.FormatHeader(&l.buf, now, line, level) - if err != nil { + + if err := l.FormatHeader(&l.buf, now, line, level); err != nil { return err } + l.buf = append(l.buf, s...) + if len(s) == 0 || s[len(s)-1] != '\n' { l.buf = append(l.buf, '\n') } - _, err = l.Out.Write(l.buf) + + _, err := l.Out.Write(l.buf) if err != nil { - return fmt.Errorf("logger printing error %w", err) + return fmt.Errorf("logger printing: %w", err) } + return nil } // Some line formatting stuff from Golang log stdlib file // -// Please view https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/log/log.go;drc=41e1d9075e428c2fc32d966b3752a3029b620e2c;l=96 +// Please view +// https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/log/log.go;drc=41e1d9075e428c2fc32d966b3752a3029b620e2c;l=96 // // Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding. func formatter(buf *[]byte, i int, wid int) { // Assemble decimal in reverse order. var b [20]byte bp := len(b) - 1 + for i >= 10 || wid > 1 { wid-- + q := i / 10 b[bp] = byte('0' + i - q*10) bp-- + i = q } // i < 10 @@ -126,22 +134,22 @@ func formatter(buf *[]byte, i int, wid int) { *buf = append(*buf, b[bp:]...) } -// Call print directly with Debug level. +// Debug calls print directly with Debug level. func (l *Logger) Debug(v ...any) { l.Println(DebugLevel, v...) } -// Call print directly with Info level. +// Info calls print directly with Info level. func (l *Logger) Info(v ...any) { l.Println(InfoLevel, v...) } -// Call print directly with Warn level. +// Warn calls print directly with Warn level. func (l *Logger) Warn(v ...any) { l.Println(WarnLevel, v...) } -// Call print directly with Error level. +// Error calls 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 e30eef9..1b63b01 100644 --- a/logawl/logging_test.go +++ b/logawl/logging_test.go @@ -24,10 +24,11 @@ func TestLogawl(t *testing.T) { func TestUnmarshalLevels(t *testing.T) { t.Parallel() + m := make(map[int]string) - var err error for i := range logawl.AllLevels { + var err error m[i], err = logger.UnMarshalLevel(logawl.Level(i)) assert.NilError(t, err) } @@ -52,30 +53,42 @@ func TestLogger(t *testing.T) { fn := func() { logger.Error("Test", "E") } + var buffer bytes.Buffer + logger.Out = &buffer + fn() case 1: fn := func() { logger.Warn("Test") } + var buffer bytes.Buffer + logger.Out = &buffer + fn() case 2: fn := func() { logger.Info("Test") } + var buffer bytes.Buffer + logger.Out = &buffer + fn() case 3: fn := func() { logger.Debug("Test") logger.Debug("Test 2") } + var buffer bytes.Buffer + logger.Out = &buffer + fn() } } @@ -83,6 +96,7 @@ func TestLogger(t *testing.T) { func TestFmt(t *testing.T) { t.Parallel() + ti := time.Now() test := []byte("test") // make sure error is error diff --git a/main.go b/main.go index 5f8ce9e..1b266f4 100644 --- a/main.go +++ b/main.go @@ -9,24 +9,31 @@ import ( "strings" "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" ) var version = "DEV" func main() { - opts, err := cli.ParseCLI(version) - if err != nil { + if opts, code, err := run(); err != nil { // TODO: Make not ew if errors.Is(err, cli.ErrNotError) || strings.Contains(err.Error(), "help requested") { os.Exit(0) + } else { + opts.Logger.Error(err) + os.Exit(code) } - opts.Logger.Error(err) - os.Exit(1) + } +} + +func run() (opts util.Options, code int, err error) { + opts, err = cli.ParseCLI(version) + if err != nil { + return opts, 1, fmt.Errorf("parse: %w", err) } - var resp helpers.Response + var resp util.Response // Retry queries if a query fails for i := 0; i < opts.Request.Retries; i++ { @@ -40,20 +47,20 @@ func main() { // Query failed, make it fail if err != nil { - opts.Logger.Error(err) - os.Exit(9) + return opts, 9, fmt.Errorf("query: %w", err) } var str string if opts.JSON || opts.XML || opts.YAML { str, err = query.PrintSpecial(resp.DNS, opts) if err != nil { - opts.Logger.Error("Special print:", err) - os.Exit(10) + return opts, 10, fmt.Errorf("format print: %w", err) } } else { str = query.ToString(resp, opts) } fmt.Println(str) + + return opts, 0, nil } diff --git a/main_test.go b/main_test.go index 6091ce8..a1a7c8a 100644 --- a/main_test.go +++ b/main_test.go @@ -6,14 +6,36 @@ import ( "os" "testing" + "github.com/stefansundin/go-zflag" "gotest.tools/v3/assert" ) -// nolint: paralleltest -func TestMain(t *testing.T) { +func TestMain(t *testing.T) { //nolint: paralleltest // Race conditions + old := os.Args + os.Args = []string{"awl", "+yaml", "@1.1.1.1"} - main() + + _, code, err := run() + assert.NilError(t, err) + assert.Equal(t, code, 0) + os.Args = []string{"awl", "+short", "@1.1.1.1"} - main() - assert.Assert(t, 1 == 2-1) + + _, code, err = run() + assert.NilError(t, err) + assert.Equal(t, code, 0) + + os.Args = old +} + +func TestHelp(t *testing.T) { + old := os.Args + + os.Args = []string{"awl", "-h"} + + _, code, err := run() + assert.ErrorIs(t, err, zflag.ErrHelp) + assert.Equal(t, code, 1) + + os.Args = old } diff --git a/query/DNSCrypt.go b/query/DNSCrypt.go index 70e5151..53afc3d 100644 --- a/query/DNSCrypt.go +++ b/query/DNSCrypt.go @@ -6,27 +6,29 @@ import ( "fmt" "time" - "git.froth.zone/sam/awl/cli" - "git.froth.zone/sam/awl/internal/helpers" + "git.froth.zone/sam/awl/util" "github.com/ameshkov/dnscrypt/v2" "github.com/miekg/dns" ) +// DNSCryptResolver is for making DNSCrypt queries. type DNSCryptResolver struct { - opts cli.Options + opts util.Options } +var _ Resolver = (*DNSCryptResolver)(nil) + // LookUp performs a DNS query. -func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { +func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (util.Response, error) { client := dnscrypt.Client{ Timeout: r.opts.Request.Timeout, UDPSize: 1232, } if r.opts.TCP || r.opts.TLS { - client.Net = "tcp" + client.Net = tcp } else { - client.Net = "udp" + client.Net = udp } switch { @@ -35,11 +37,12 @@ func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { case r.opts.IPv6: client.Net += "6" } + r.opts.Logger.Debug("Using", client.Net, "for making the request") resolverInf, err := client.Dial(r.opts.Request.Server) if err != nil { - return helpers.Response{}, fmt.Errorf("dnscrypt: dial error: %w", err) + return util.Response{}, fmt.Errorf("dnscrypt: dial: %w", err) } now := time.Now() @@ -47,11 +50,12 @@ func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { rtt := time.Since(now) if err != nil { - return helpers.Response{}, fmt.Errorf("dnscrypt: exchange error: %w", err) + return util.Response{}, fmt.Errorf("dnscrypt: exchange: %w", err) } + r.opts.Logger.Info("Request successful") - return helpers.Response{ + return util.Response{ DNS: res, RTT: rtt, }, nil diff --git a/query/DNSCrypt_test.go b/query/DNSCrypt_test.go index d49bb30..37df392 100644 --- a/query/DNSCrypt_test.go +++ b/query/DNSCrypt_test.go @@ -5,8 +5,6 @@ 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" @@ -15,14 +13,15 @@ import ( func TestDNSCrypt(t *testing.T) { t.Parallel() + tests := []struct { - opt cli.Options + opt util.Options }{ { - cli.Options{ + util.Options{ Logger: util.InitLogger(0), DNSCrypt: true, - Request: helpers.Request{ + Request: util.Request{ Server: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", Type: dns.TypeA, Name: "example.com.", @@ -30,12 +29,12 @@ func TestDNSCrypt(t *testing.T) { }, }, { - cli.Options{ + util.Options{ Logger: util.InitLogger(0), DNSCrypt: true, TCP: true, IPv4: true, - Request: helpers.Request{ + Request: util.Request{ Server: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", Type: dns.TypeAAAA, Name: "example.com.", @@ -43,12 +42,12 @@ func TestDNSCrypt(t *testing.T) { }, }, { - cli.Options{ + util.Options{ Logger: util.InitLogger(0), DNSCrypt: true, TCP: true, IPv4: true, - Request: helpers.Request{ + Request: util.Request{ Server: "QMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", Type: dns.TypeAAAA, Name: "example.com.", @@ -56,13 +55,16 @@ func TestDNSCrypt(t *testing.T) { }, }, } + for _, test := range tests { 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{}) + assert.Assert(t, res != util.Response{}) } else { assert.ErrorContains(t, err, "unsupported stamp") } diff --git a/query/HTTPS.go b/query/HTTPS.go index 91c23f4..0c56002 100644 --- a/query/HTTPS.go +++ b/query/HTTPS.go @@ -9,31 +9,37 @@ import ( "net/http" "time" - "git.froth.zone/sam/awl/cli" - "git.froth.zone/sam/awl/internal/helpers" + "git.froth.zone/sam/awl/util" "github.com/miekg/dns" ) +// HTTPSResolver is for DNS-over-HTTPS queries. type HTTPSResolver struct { - opts cli.Options + opts util.Options } +var _ Resolver = (*HTTPSResolver)(nil) + // LookUp performs a DNS query. -func (r *HTTPSResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { - var resp helpers.Response +func (r *HTTPSResolver) LookUp(msg *dns.Msg) (util.Response, error) { + var resp util.Response + httpR := &http.Client{ Timeout: r.opts.Request.Timeout, } + buf, err := msg.Pack() if err != nil { - return helpers.Response{}, fmt.Errorf("doh: packing error error: %w", err) + return util.Response{}, fmt.Errorf("doh: packing: %w", err) } - r.opts.Logger.Debug("making DoH request") - // + + r.opts.Logger.Debug("https: sending HTTPS request") + req, err := http.NewRequest("POST", r.opts.Request.Server, bytes.NewBuffer(buf)) if err != nil { - return helpers.Response{}, fmt.Errorf("doh request creation error: %w", err) + return util.Response{}, fmt.Errorf("doh: request creation: %w", err) } + req.Header.Set("Content-Type", "application/dns-message") req.Header.Set("Accept", "application/dns-message") @@ -42,23 +48,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 util.Response{}, fmt.Errorf("doh: HTTP request: %w", err) } - defer res.Body.Close() if res.StatusCode != http.StatusOK { - return helpers.Response{}, &errHTTPStatus{res.StatusCode} + return util.Response{}, &errHTTPStatus{res.StatusCode} } + r.opts.Logger.Debug("https: reading response") + fullRes, err := io.ReadAll(res.Body) if err != nil { - return helpers.Response{}, fmt.Errorf("doh body read error: %w", err) + return util.Response{}, fmt.Errorf("doh: body read: %w", err) } + + err = res.Body.Close() + if err != nil { + return util.Response{}, fmt.Errorf("doh: body close: %w", err) + } + + r.opts.Logger.Debug("https: unpacking response") + 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 util.Response{}, fmt.Errorf("doh: dns message unpack: %w", err) } return resp, nil diff --git a/query/HTTPS_test.go b/query/HTTPS_test.go index ee552a0..b844847 100644 --- a/query/HTTPS_test.go +++ b/query/HTTPS_test.go @@ -3,12 +3,8 @@ package query_test import ( - "fmt" - "strings" "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" @@ -17,17 +13,19 @@ import ( func TestResolveHTTPS(t *testing.T) { t.Parallel() + var err error - opts := cli.Options{ + + opts := util.Options{ HTTPS: true, Logger: util.InitLogger(0), - Request: helpers.Request{ + Request: util.Request{ Server: "https://dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone.", }, } - // testCase := helpers.Request{Server: "https://dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."} + // testCase := util.Request{Server: "https://dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."} resolver, err := query.LoadResolver(opts) assert.NilError(t, err) @@ -36,62 +34,75 @@ func TestResolveHTTPS(t *testing.T) { // msg = msg.SetQuestion(testCase.Name, testCase.Type) res, err := resolver.LookUp(msg) assert.NilError(t, err) - assert.Assert(t, res != helpers.Response{}) + assert.Assert(t, res != util.Response{}) } func Test2ResolveHTTPS(t *testing.T) { t.Parallel() - opts := cli.Options{ - HTTPS: true, - Logger: util.InitLogger(0), + + opts := util.Options{ + HTTPS: true, + Logger: util.InitLogger(0), + Request: util.Request{Server: "dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."}, } + var err error - testCase := helpers.Request{Server: "dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone"} + + testCase := util.Request{Type: dns.TypeA, Name: "git.froth.zone"} resolver, err := query.LoadResolver(opts) assert.NilError(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.ErrorContains(t, err, "fully qualified") - assert.Equal(t, res, helpers.Response{}) + assert.Equal(t, res, util.Response{}) } func Test3ResolveHTTPS(t *testing.T) { t.Parallel() - opts := cli.Options{ - HTTPS: true, - Logger: util.InitLogger(0), + + opts := util.Options{ + HTTPS: true, + Logger: util.InitLogger(0), + Request: util.Request{Server: "dns9..quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."}, } + var err error - testCase := helpers.Request{Server: "dns9..quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."} + + // testCase := // if the domain is not canonical, make it canonical - if !strings.HasSuffix(testCase.Name, ".") { - testCase.Name = fmt.Sprintf("%s.", testCase.Name) - } + // if !strings.HasSuffix(testCase.Name, ".") { + // testCase.Name = fmt.Sprintf("%s.", testCase.Name) + // } + resolver, err := query.LoadResolver(opts) assert.NilError(t, err) + msg := new(dns.Msg) - msg.SetQuestion(testCase.Name, testCase.Type) + // msg.SetQuestion(testCase.Name, testCase.Type) // msg = msg.SetQuestion(testCase.Name, testCase.Type) res, err := resolver.LookUp(msg) - assert.ErrorContains(t, err, "request error") - assert.Equal(t, res, helpers.Response{}) + assert.ErrorContains(t, err, "doh: HTTP request") + assert.Equal(t, res, util.Response{}) } func Test404ResolveHTTPS(t *testing.T) { t.Parallel() + var err error - opts := cli.Options{ + + opts := util.Options{ HTTPS: true, Logger: util.InitLogger(0), - Request: helpers.Request{ + Request: util.Request{ Server: "https://dns9.quad9.net/dns", Type: dns.TypeA, Name: "git.froth.zone.", }, } - // testCase := helpers.Request{Server: "https://dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."} + // testCase := util.Request{Server: "https://dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."} resolver, err := query.LoadResolver(opts) assert.NilError(t, err) @@ -100,5 +111,5 @@ func Test404ResolveHTTPS(t *testing.T) { // msg = msg.SetQuestion(testCase.Name, testCase.Type) res, err := resolver.LookUp(msg) assert.ErrorContains(t, err, "404") - assert.Equal(t, res, helpers.Response{}) + assert.Equal(t, res, util.Response{}) } diff --git a/query/QUIC.go b/query/QUIC.go index 10bf474..9191fdd 100644 --- a/query/QUIC.go +++ b/query/QUIC.go @@ -8,70 +8,95 @@ import ( "io" "time" - "git.froth.zone/sam/awl/cli" - "git.froth.zone/sam/awl/internal/helpers" + "git.froth.zone/sam/awl/util" "github.com/lucas-clemente/quic-go" "github.com/miekg/dns" ) +// QUICResolver is for DNS-over-QUIC queries. type QUICResolver struct { - opts cli.Options + opts util.Options } +var _ Resolver = (*QUICResolver)(nil) + // LookUp performs a DNS query. -func (r *QUICResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { - var resp helpers.Response +func (r *QUICResolver) LookUp(msg *dns.Msg) (util.Response, error) { + var resp util.Response + tls := &tls.Config{ - MinVersion: tls.VersionTLS12, - NextProtos: []string{"doq"}, + //nolint:gosec // This is intentional if the user requests it + InsecureSkipVerify: r.opts.TLSNoVerify, + ServerName: r.opts.TLSHost, + MinVersion: tls.VersionTLS12, + NextProtos: []string{"doq"}, } conf := new(quic.Config) conf.HandshakeIdleTimeout = r.opts.Request.Timeout - r.opts.Logger.Debug("making DoQ request") + r.opts.Logger.Debug("quic: making query") + connection, err := quic.DialAddr(r.opts.Request.Server, tls, conf) if err != nil { - return helpers.Response{}, fmt.Errorf("doq: dial error: %w", err) + return util.Response{}, fmt.Errorf("doq: dial: %w", err) } + r.opts.Logger.Debug("quic: packing query") + // Compress request to over-the-wire buf, err := msg.Pack() if err != nil { - return helpers.Response{}, fmt.Errorf("doq: pack error: %w", err) + return util.Response{}, fmt.Errorf("doq: pack: %w", err) } + t := time.Now() + + r.opts.Logger.Debug("quic: creating stream") + stream, err := connection.OpenStream() if err != nil { - return helpers.Response{}, fmt.Errorf("doq: quic stream creation error: %w", err) + return util.Response{}, fmt.Errorf("doq: quic stream creation: %w", err) } + + r.opts.Logger.Debug("quic: writing to stream") + _, err = stream.Write(buf) if err != nil { - return helpers.Response{}, fmt.Errorf("doq: quic stream write error: %w", err) + return util.Response{}, fmt.Errorf("doq: quic stream write: %w", err) } + r.opts.Logger.Debug("quic: reading stream") + fullRes, err := io.ReadAll(stream) if err != nil { - return helpers.Response{}, fmt.Errorf("doq: quic stream read error: %w", err) + return util.Response{}, fmt.Errorf("doq: quic stream read: %w", err) } + resp.RTT = time.Since(t) + r.opts.Logger.Debug("quic: closing connection") // Close with error: no error err = connection.CloseWithError(0, "") if err != nil { - return helpers.Response{}, fmt.Errorf("doq: quic connection close error: %w", err) + return util.Response{}, fmt.Errorf("doq: quic connection close: %w", err) } + r.opts.Logger.Debug("quic: closing stream") + err = stream.Close() if err != nil { - return helpers.Response{}, fmt.Errorf("doq: quic stream close error: %w", err) + return util.Response{}, fmt.Errorf("doq: quic stream close: %w", err) } resp.DNS = &dns.Msg{} - r.opts.Logger.Debug("unpacking DoQ response") + + r.opts.Logger.Debug("quic: unpacking response") + err = resp.DNS.Unpack(fullRes) if err != nil { - return helpers.Response{}, fmt.Errorf("doq: upack error: %w", err) + return util.Response{}, fmt.Errorf("doq: unpack: %w", err) } + return resp, nil } diff --git a/query/QUIC_test.go b/query/QUIC_test.go index a02d930..6158254 100644 --- a/query/QUIC_test.go +++ b/query/QUIC_test.go @@ -10,8 +10,6 @@ import ( "testing" "time" - "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" @@ -20,17 +18,21 @@ import ( func TestQuic(t *testing.T) { t.Parallel() - opts := cli.Options{ + + opts := util.Options{ QUIC: true, Logger: util.InitLogger(0), Port: 853, - Request: helpers.Request{Server: "dns.adguard.com"}, + Request: util.Request{Server: "dns.adguard.com"}, } - testCase := helpers.Request{Server: "dns.//./,,adguard.com", Type: dns.TypeA, Name: "git.froth.zone"} - testCase2 := helpers.Request{Server: "dns.adguard.com", Type: dns.TypeA, Name: "git.froth.zone"} - var testCases []helpers.Request + testCase := util.Request{Server: "dns.//./,,adguard.com", Type: dns.TypeA, Name: "git.froth.zone"} + testCase2 := util.Request{Server: "dns.adguard.com", Type: dns.TypeA, Name: "git.froth.zone"} + + var testCases []util.Request + testCases = append(testCases, testCase) testCases = append(testCases, testCase2) + for i := range testCases { switch i { case 0: @@ -40,37 +42,44 @@ func TestQuic(t *testing.T) { 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) + // msg = msg.SetQuestion(testCase.Name, testCase.Type) res, err := resolver.LookUp(msg) + assert.ErrorContains(t, err, "fully qualified") - assert.Equal(t, res, helpers.Response{}) + assert.Equal(t, res, util.Response{}) case 1: resolver, err := query.LoadResolver(opts) assert.NilError(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.NilError(t, err) - assert.Assert(t, res != helpers.Response{}) + assert.Assert(t, res != util.Response{}) } } } func TestInvalidQuic(t *testing.T) { t.Parallel() - opts := cli.Options{ + + opts := util.Options{ QUIC: true, Logger: util.InitLogger(0), Port: 853, - Request: helpers.Request{Server: "example.com", Type: dns.TypeA, Name: "git.froth.zone", Timeout: 10 * time.Millisecond}, + Request: util.Request{Server: "example.com", Type: dns.TypeA, Name: "git.froth.zone", Timeout: 10 * time.Millisecond}, } resolver, err := query.LoadResolver(opts) assert.NilError(t, err) @@ -79,5 +88,5 @@ func TestInvalidQuic(t *testing.T) { msg.SetQuestion(opts.Request.Name, opts.Request.Type) res, err := resolver.LookUp(msg) assert.ErrorContains(t, err, "timeout") - assert.Equal(t, res, helpers.Response{}) + assert.Equal(t, res, util.Response{}) } diff --git a/query/docs.go b/query/docs.go index 1a4bfa2..5318e7a 100644 --- a/query/docs.go +++ b/query/docs.go @@ -1,2 +1,2 @@ -// Package for the special query types +// Package query is for the various query types. package query diff --git a/query/general.go b/query/general.go index eca10c1..8e0973b 100644 --- a/query/general.go +++ b/query/general.go @@ -3,32 +3,37 @@ package query import ( + "crypto/tls" "fmt" "net" - "git.froth.zone/sam/awl/cli" - "git.froth.zone/sam/awl/internal/helpers" + "git.froth.zone/sam/awl/util" "github.com/miekg/dns" ) +// StandardResolver is for UDP/TCP resolvers. type StandardResolver struct { - opts cli.Options + opts util.Options } -// LookUp performs a DNS query -func (r *StandardResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { +var _ Resolver = (*StandardResolver)(nil) + +// LookUp performs a DNS query. +func (r *StandardResolver) LookUp(msg *dns.Msg) (util.Response, error) { var ( - resp helpers.Response + resp util.Response err error ) + dnsClient := new(dns.Client) dnsClient.Dialer = &net.Dialer{ Timeout: r.opts.Request.Timeout, } + if r.opts.TCP || r.opts.TLS { - dnsClient.Net = "tcp" + dnsClient.Net = tcp } else { - dnsClient.Net = "udp" + dnsClient.Net = udp } switch { @@ -40,28 +45,39 @@ func (r *StandardResolver) LookUp(msg *dns.Msg) (helpers.Response, error) { if r.opts.TLS { dnsClient.Net += "-tls" + dnsClient.TLSConfig = &tls.Config{ + //nolint:gosec // This is intentional if the user requests it + InsecureSkipVerify: r.opts.TLSNoVerify, + ServerName: r.opts.TLSHost, + } } + r.opts.Logger.Debug("Using", dnsClient.Net, "for making the request") 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 util.Response{}, fmt.Errorf("standard: DNS exchange: %w", err) } + r.opts.Logger.Info("Request successful") if resp.DNS.MsgHdr.Truncated && !r.opts.Truncate { fmt.Printf(";; Truncated, retrying with TCP\n\n") - dnsClient.Net = "tcp" + + dnsClient.Net = tcp + switch { case r.opts.IPv4: dnsClient.Net += "4" 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 util.Response{}, fmt.Errorf("standard: DNS exchange: %w", err) } return resp, nil diff --git a/query/general_test.go b/query/general_test.go index 7346e63..312cf60 100644 --- a/query/general_test.go +++ b/query/general_test.go @@ -6,8 +6,6 @@ import ( "testing" "time" - "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" @@ -16,10 +14,11 @@ import ( func TestResolve(t *testing.T) { t.Parallel() - opts := cli.Options{ + + opts := util.Options{ Logger: util.InitLogger(0), Port: 53, - Request: helpers.Request{ + Request: util.Request{ Server: "8.8.4.1", Type: dns.TypeA, Name: "example.com.", @@ -29,19 +28,22 @@ func TestResolve(t *testing.T) { } resolver, err := query.LoadResolver(opts) assert.NilError(t, err) + msg := new(dns.Msg) msg.SetQuestion(opts.Request.Name, opts.Request.Type) + _, err = resolver.LookUp(msg) assert.ErrorContains(t, err, "timeout") } func TestTruncate(t *testing.T) { t.Parallel() - opts := cli.Options{ + + opts := util.Options{ Logger: util.InitLogger(0), IPv4: true, Port: 5301, - Request: helpers.Request{ + Request: util.Request{ Server: "madns.binarystar.systems", Type: dns.TypeTXT, Name: "limit.txt.example.", @@ -49,24 +51,27 @@ func TestTruncate(t *testing.T) { } 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{}) + assert.Assert(t, res != util.Response{}) } func TestResolveAgain(t *testing.T) { t.Parallel() + tests := []struct { - opt cli.Options + opt util.Options }{ { - cli.Options{ + util.Options{ Logger: util.InitLogger(0), TCP: true, Port: 53, - Request: helpers.Request{ + Request: util.Request{ Server: "8.8.4.4", Type: dns.TypeA, Name: "example.com.", @@ -74,10 +79,10 @@ func TestResolveAgain(t *testing.T) { }, }, { - cli.Options{ + util.Options{ Logger: util.InitLogger(0), Port: 53, - Request: helpers.Request{ + Request: util.Request{ Server: "8.8.4.4", Type: dns.TypeAAAA, Name: "example.com.", @@ -85,11 +90,11 @@ func TestResolveAgain(t *testing.T) { }, }, { - cli.Options{ + util.Options{ Logger: util.InitLogger(0), TLS: true, Port: 853, - Request: helpers.Request{ + Request: util.Request{ Server: "dns.google", Type: dns.TypeAAAA, Name: "example.com.", @@ -97,13 +102,15 @@ func TestResolveAgain(t *testing.T) { }, }, } + for _, test := range tests { 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{}) + assert.Assert(t, res != util.Response{}) }) } } diff --git a/query/print.go b/query/print.go index f5cf4eb..0c81761 100644 --- a/query/print.go +++ b/query/print.go @@ -11,42 +11,49 @@ import ( "strings" "time" - "git.froth.zone/sam/awl/cli" + "git.froth.zone/sam/awl/util" "github.com/miekg/dns" "golang.org/x/net/idna" "gopkg.in/yaml.v3" ) -func PrintSpecial(msg *dns.Msg, opts cli.Options) (string, error) { +// PrintSpecial is for printing as JSON, XML or YAML. +// As of now JSON and XML use the stdlib version. +func PrintSpecial(msg *dns.Msg, opts util.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 + 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 + 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 + 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) { +// and XML. Little is changed beyond naming. +func MakePrintable(msg *dns.Msg, opts util.Options) (*Message, error) { var err error + ret := Message{ Header: msg.MsgHdr, } @@ -56,11 +63,12 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) { if opts.Display.UcodeTranslate { name, err = idna.ToUnicode(question.Name) if err != nil { - return nil, fmt.Errorf("punycode: error translating to unicode: %w", err) + return nil, fmt.Errorf("punycode to unicode: %w", err) } } else { name = question.Name } + ret.Question = append(ret.Question, Question{ Name: name, Type: dns.TypeToString[question.Qtype], @@ -70,10 +78,12 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) { 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() @@ -81,14 +91,16 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) { 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) + return nil, fmt.Errorf("punycode to unicode: %w", err) } } else { name = answer.Header().Name } + ret.Answer = append(ret.Answer, Answer{ RRHeader: RRHeader{ Name: name, @@ -103,10 +115,12 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) { 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() @@ -114,14 +128,16 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) { 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) + return nil, fmt.Errorf("punycode to unicode: %w", err) } } else { name = ns.Header().Name } + ret.Ns = append(ret.Ns, Answer{ RRHeader: RRHeader{ Name: name, @@ -139,10 +155,12 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) { 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() @@ -150,14 +168,16 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) { 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) + return nil, fmt.Errorf("punycode to unicode: %w", err) } } else { name = additional.Header().Name } + ret.Extra = append(ret.Extra, Answer{ RRHeader: RRHeader{ Name: name, diff --git a/query/print_test.go b/query/print_test.go index 732cb69..0e91c2b 100644 --- a/query/print_test.go +++ b/query/print_test.go @@ -5,339 +5,197 @@ 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 TestRealPrint(t *testing.T) { + t.Parallel() + + opts := []util.Options{ + { + Logger: util.InitLogger(0), + Port: 53, + TCP: true, + ShowQuery: true, + RD: true, + ShowTTL: true, + HumanTTL: true, + JSON: true, + Display: util.Displays{ + Comments: true, + Question: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: true, + }, + Request: util.Request{ + Server: "a.gtld-servers.net", + Type: dns.StringToType["NS"], + Class: 1, + Name: "google.com.", + }, + EDNS: util.EDNS{ + EnableEDNS: false, + }, + }, + { + Logger: util.InitLogger(0), + Port: 53, + TCP: true, + ShowQuery: true, + RD: true, + Verbosity: 0, + ShowTTL: true, + Short: true, + Identify: true, + YAML: false, + Display: util.Displays{ + Comments: true, + Question: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: true, + }, + Request: util.Request{ + Server: "ns1.google.com", + Type: dns.StringToType["NS"], + Class: 1, + Name: "google.com.", + Timeout: 0, + Retries: 0, + }, + EDNS: util.EDNS{ + EnableEDNS: false, + }, + }, + { + Logger: util.InitLogger(0), + Port: 53, + HTTPS: true, + ShowQuery: true, + RD: true, + ShowTTL: true, + HumanTTL: true, + Identify: true, + XML: true, + Display: util.Displays{ + Comments: true, + Question: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: false, + }, + Request: util.Request{ + Server: "https://dns.froth.zone/dns-query", + Type: dns.StringToType["NS"], + Class: 1, + Name: "freecumextremist.com.", + Timeout: 0, + Retries: 0, + }, + EDNS: util.EDNS{ + EnableEDNS: false, + DNSSEC: true, + }, + }, + { + Logger: util.InitLogger(0), + Port: 853, + TLS: true, + ShowQuery: true, + RD: true, + Verbosity: 0, + ShowTTL: true, + Identify: true, + YAML: false, + Display: util.Displays{ + Comments: true, + Question: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: true, + }, + Request: util.Request{ + Server: "dns.google", + Type: dns.StringToType["NS"], + Class: 1, + Name: "freecumextremist.com.", + }, + }, + { + Logger: util.InitLogger(0), + Port: 53, + TCP: true, + ShowQuery: true, + AA: true, + RD: true, + Verbosity: 0, + ShowTTL: true, + YAML: true, + Display: util.Displays{ + Comments: true, + Question: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + UcodeTranslate: false, + }, + Request: util.Request{ + Server: "rin.froth.zone", + Type: dns.StringToType["A"], + Class: 1, + Name: "froth.zone.", + Timeout: 0, + Retries: 0, + }, + EDNS: util.EDNS{ + EnableEDNS: true, + Cookie: true, + Padding: true, + }, + }, + } + + for _, test := range opts { + test := test + + t.Run("", func(t *testing.T) { + t.Parallel() + resp, err := query.CreateQuery(test) + assert.NilError(t, err) + + if test.JSON || test.XML || test.YAML { + str, err := query.PrintSpecial(resp.DNS, test) + assert.NilError(t, err) + assert.Assert(t, str != "") + } + str := query.ToString(resp, test) + assert.Assert(t, str != "") + }) + } +} + func TestBadFormat(t *testing.T) { t.Parallel() - _, err := query.PrintSpecial(new(dns.Msg), cli.Options{}) + + _, err := query.PrintSpecial(new(dns.Msg), util.Options{}) assert.ErrorContains(t, err, "never happen") } -func TestPrinting(t *testing.T) { +func TestEmpty(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") + assert.Assert(t, query.ToString(util.Response{}, util.Options{}) == " MsgHdr") } diff --git a/query/query.go b/query/query.go index 0373e56..7e75045 100644 --- a/query/query.go +++ b/query/query.go @@ -6,13 +6,19 @@ import ( "fmt" "strconv" - "git.froth.zone/sam/awl/cli" - "git.froth.zone/sam/awl/internal/helpers" + "git.froth.zone/sam/awl/util" "github.com/dchest/uniuri" "github.com/miekg/dns" ) -func CreateQuery(opts cli.Options) (helpers.Response, error) { +const ( + tcp = "tcp" + udp = "udp" +) + +// CreateQuery creates a DNS query from the options given. +// It sets query flags and EDNS flags from the respective options. +func CreateQuery(opts util.Options) (util.Response, error) { req := new(dns.Msg) req.SetQuestion(opts.Request.Name, opts.Request.Type) req.Question[0].Qclass = opts.Request.Class @@ -40,37 +46,45 @@ func CreateQuery(opts cli.Options) (helpers.Response, error) { 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") } @@ -84,39 +98,45 @@ func CreateQuery(opts cli.Options) (helpers.Response, error) { opts.Logger.Warn("DNSSEC implies EDNS, EDNS enabled") opts.Logger.Info("DNSSEC enabled, UDP buffer set to 1232") } + opts.Logger.Debug(req) 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 + return util.Response{}, err } } else { temp := opts.Display.Statistics opts.Display.Statistics = false - str = ToString(helpers.Response{ + str = ToString(util.Response{ DNS: req, RTT: 0, }, opts) opts.Display.Statistics = temp - str += "\n;; QUERY SIZE: " + strconv.Itoa(req.Len()) + str += "\n;; QUERY SIZE: " + strconv.Itoa(req.Len()) + "\n" } + fmt.Println(str) + opts.ShowQuery = false } } resolver, err := LoadResolver(opts) if err != nil { - return helpers.Response{}, err + return util.Response{}, err } + opts.Logger.Info("Query successfully loaded") //nolint:wrapcheck // Error wrapping not needed here diff --git a/query/query_test.go b/query/query_test.go index 6b6ee66..b4550a1 100644 --- a/query/query_test.go +++ b/query/query_test.go @@ -5,8 +5,6 @@ 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" @@ -15,32 +13,30 @@ import ( func TestCreateQ(t *testing.T) { t.Parallel() - in := []cli.Options{ + + in := []util.Options{ { Logger: util.InitLogger(0), Port: 53, - QR: false, Z: true, - RD: false, ShowQuery: true, YAML: true, - Request: helpers.Request{ + Request: util.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, + Display: util.Displays{ + Comments: true, + Question: true, + Opt: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, }, - EDNS: cli.EDNS{ + EDNS: util.EDNS{ EnableEDNS: true, DNSSEC: true, Cookie: true, @@ -52,18 +48,16 @@ func TestCreateQ(t *testing.T) { { Logger: util.InitLogger(0), Port: 53, - QR: false, Z: true, - RD: false, ShowQuery: true, XML: true, - Request: helpers.Request{ + Request: util.Request{ Server: "8.8.4.4", Type: dns.TypeA, Name: "example.com.", }, - Display: cli.Displays{ + Display: util.Displays{ Comments: true, Question: true, Opt: true, @@ -73,26 +67,52 @@ func TestCreateQ(t *testing.T) { Statistics: true, UcodeTranslate: true, }, - EDNS: cli.EDNS{ - EnableEDNS: false, - DNSSEC: false, - Cookie: false, - Expire: false, - KeepOpen: false, - Nsid: false, + }, + { + Logger: util.InitLogger(0), + Port: 853, + // Z: true, + ShowQuery: true, + JSON: true, + QUIC: true, + + Request: util.Request{ + Server: "dns.adguard.com", + Type: dns.TypeA, + Name: "example.com.", + }, + Display: util.Displays{ + Comments: true, + Question: true, + Opt: true, + Answer: true, + Authority: true, + Additional: true, + Statistics: true, + }, + EDNS: util.EDNS{ + EnableEDNS: true, + DNSSEC: true, + Cookie: true, + Expire: true, + Nsid: true, }, }, } + 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{}) + assert.Assert(t, res != util.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 51740e6..2ede1db 100644 --- a/query/resolver.go +++ b/query/resolver.go @@ -7,44 +7,49 @@ import ( "strconv" "strings" - "git.froth.zone/sam/awl/cli" - "git.froth.zone/sam/awl/internal/helpers" + "git.froth.zone/sam/awl/util" "github.com/miekg/dns" ) -// Main resolver interface +// Resolver is the main resolver interface. type Resolver interface { - LookUp(*dns.Msg) (helpers.Response, error) + LookUp(*dns.Msg) (util.Response, error) } -// LoadResolver loads the respective resolver for performing a DNS query -func LoadResolver(opts cli.Options) (Resolver, error) { +// LoadResolver loads the respective resolver for performing a DNS query. +func LoadResolver(opts util.Options) (Resolver, error) { switch { case opts.HTTPS: opts.Logger.Info("loading DNS-over-HTTPS resolver") + if !strings.HasPrefix(opts.Request.Server, "https://") { opts.Request.Server = "https://" + opts.Request.Server } + return &HTTPSResolver{ opts: opts, }, nil case opts.QUIC: opts.Logger.Info("loading DNS-over-QUIC resolver") opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Port)) + return &QUICResolver{ opts: opts, }, nil case opts.DNSCrypt: opts.Logger.Info("loading DNSCrypt resolver") + if !strings.HasPrefix(opts.Request.Server, "sdns://") { opts.Request.Server = "sdns://" + opts.Request.Server } + return &DNSCryptResolver{ opts: opts, }, nil default: opts.Logger.Info("loading standard/DNS-over-TLS resolver") opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Port)) + return &StandardResolver{ opts: opts, }, nil diff --git a/query/struct.go b/query/struct.go index b4388da..f9a487e 100644 --- a/query/struct.go +++ b/query/struct.go @@ -7,28 +7,29 @@ import ( "strings" "time" - "git.froth.zone/sam/awl/cli" - "git.froth.zone/sam/awl/internal/helpers" + "git.froth.zone/sam/awl/util" "github.com/miekg/dns" ) -// Overall DNS response message +// Message is for overall DNS responses. +// +//nolint:fieldalignment // IMO this looks better when printed like this 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"` + Header dns.MsgHdr `json:"header,omitempty" xml:"header,omitempty" yaml:",omitempty"` } -// DNS Query +// Question is a 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 +// RRHeader is for 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"` @@ -37,21 +38,26 @@ type RRHeader struct { Rdlength uint16 `json:"-" xml:"-" yaml:"-"` } -// DNS Response +// Answer is for a DNS Response. +// +//nolint:fieldalignment // IMO this looks better when printed like this type Answer struct { - RRHeader `json:"header,omitempty" xml:"header,omitempty" yaml:"header,omitempty"` Value string `json:"response,omitempty" xml:"response,omitempty" yaml:"response,omitempty"` + RRHeader `json:"header,omitempty" xml:"header,omitempty" yaml:"header,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 { +func ToString(res util.Response, opts util.Options) string { if res.DNS == nil { return " MsgHdr" } - var s string - var opt *dns.OPT + + var ( + s string + opt *dns.OPT + ) if !opts.Short { if opts.Display.Comments { @@ -61,26 +67,31 @@ func ToString(res helpers.Response, opts cli.Options) string { 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" @@ -88,11 +99,13 @@ func ToString(res helpers.Response, opts cli.Options) string { } } } + 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" @@ -100,11 +113,13 @@ func ToString(res helpers.Response, opts cli.Options) string { } } } + 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" @@ -112,10 +127,12 @@ func ToString(res helpers.Response, opts cli.Options) string { } } } + 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)" @@ -138,14 +155,15 @@ func ToString(res helpers.Response, opts cli.Options) string { 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" } - } } diff --git a/query/struct_test.go b/query/struct_test.go deleted file mode 100644 index 3b2d6ba..0000000 --- a/query/struct_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// 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/renovate.json b/renovate.json index 78c9d9b..d741255 100644 --- a/renovate.json +++ b/renovate.json @@ -1,8 +1,4 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base", - ":npm", - ":gomod" - ] + "extends": ["config:base", ":npm", ":gomod"] } diff --git a/template.mk b/template.mk index 93ffe84..9a42cc5 100644 --- a/template.mk +++ b/template.mk @@ -1,13 +1,18 @@ # SPDX-License-Identifier: BSD-3-Clause # Template for the BSD/GNU makefiles -HASH ?= `git describe --always --dirty || echo "UNKNOWN"` -VER ?= "git-$(HASH)" +HASH ?= `git describe --always --dirty --broken || echo "UNKNOWN"` +VERSION ?= "git-$(HASH)" + +SOURCES ?= $(shell find . -name "*.go" -type f ! -name '*_test*') +TEST_SOURCES ?= $(shell find . -name "*_test.go" -type f) CGO_ENABLED ?= 0 GO ?= go +TEST ?= $(GO) test -v -race COVER ?= $(GO) tool cover -GOFLAGS ?= -ldflags "-s -w -X=main.version=$(VER)" -trimpath +GOFLAGS ?= -ldflags "-s -w -X=main.version=$(VERSION)" -trimpath +DESTDIR := PREFIX ?= /usr/local BIN ?= bin @@ -20,15 +25,25 @@ 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 && rm doc/awl.bak || mv doc/awl.bak doc/awl.1 +doc/$(PROG).1: doc/$(PROG).1.scd + $(SCDOC) doc/$(PROG).1 ## test: run go test -test: +test: $(TEST_SOURCES) $(GO) test -cover -coverprofile=coverage/coverage.out ./... +test-ci: + $(TEST) + +## fuzz: runs fuzz tests +fuzz: + cd cli + $(TEST) -fuzz=FuzzFlags -fuzztime 10000x + $(TEST) -fuzz=FuzzDig -fuzztime 10000x + $(TEST) -fuzz=FuzzParseArgs -fuzztime 10000x + cd .. + coverage/coverage.out: test $(COVER) -func=coverage/coverage.out $(COVER) -html=coverage/coverage.out -o coverage/cover.html @@ -44,11 +59,12 @@ vet: ## lint: lint awl, using fmt, vet and golangci-lint lint: fmt vet - -golangci-lint run --fix + golangci-lint run --fix ## clean: clean the build files clean: $(GO) clean + rm doc/$(PROG).1 ## help: Prints this help message help: diff --git a/util/docs.go b/util/docs.go index 7a114a9..58c7fde 100644 --- a/util/docs.go +++ b/util/docs.go @@ -1,2 +1,2 @@ -// Helper functions +// Package util contains helper functions that don't belong anywhere else package util diff --git a/util/logger.go b/util/logger.go index 1c4bb03..8e5a32c 100644 --- a/util/logger.go +++ b/util/logger.go @@ -4,11 +4,11 @@ package util import "git.froth.zone/sam/awl/logawl" -// Initialize the logawl instance. -func InitLogger(verbosity int) (logger *logawl.Logger) { - logger = logawl.New() +// InitLogger initializes the logawl instance. +func InitLogger(verbosity int) (log *logawl.Logger) { + log = logawl.New() - logger.SetLevel(logawl.Level(verbosity)) + log.SetLevel(logawl.Level(verbosity)) - return logger + return log } diff --git a/util/logger_test.go b/util/logger_test.go index 0dce589..938dc05 100644 --- a/util/logger_test.go +++ b/util/logger_test.go @@ -12,6 +12,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/options.go b/util/options.go new file mode 100644 index 0000000..ff29c65 --- /dev/null +++ b/util/options.go @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package util + +import ( + "fmt" + "net" + + "git.froth.zone/sam/awl/logawl" + "github.com/miekg/dns" +) + +// Options is the grand CLI options structure. +type Options struct { + Logger *logawl.Logger + TLSHost string + EDNS + Request Request + Port int + Verbosity int + Display Displays + TC bool + ShowTTL bool + ShowQuery bool + AA bool + AD bool + CD bool + QR bool + RD bool + RA bool + IPv4 bool + Z bool + Reverse bool + HumanTTL bool + Truncate bool + Short bool + Identify bool + JSON bool + XML bool + YAML bool + QUIC bool + HTTPS bool + TLSNoVerify bool + TLS bool + DNSCrypt bool + TCP bool + IPv6 bool +} + +// Displays contains toggles for what to (and not to) display. +type Displays struct { + 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 +} + +// EDNS contains toggles for various EDNS options. +type EDNS struct { + Subnet dns.EDNS0_SUBNET + ZFlag uint16 + BufSize uint16 + EnableEDNS bool + Cookie bool + DNSSEC bool + Expire bool + KeepOpen bool + Nsid bool + Padding bool + Version uint8 +} + +// 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("EDNS subnet parsing: %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 +} diff --git a/util/options_test.go b/util/options_test.go new file mode 100644 index 0000000..741e7c2 --- /dev/null +++ b/util/options_test.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package util_test + +import ( + "testing" + + "git.froth.zone/sam/awl/util" + "gotest.tools/v3/assert" +) + +func TestSubnet(t *testing.T) { + t.Parallel() + + subnet := []string{ + "0.0.0.0/0", + "::0/0", + "0", + "127.0.0.1/32", + } + + for _, test := range subnet { + test := test + t.Run(test, func(t *testing.T) { + t.Parallel() + err := util.ParseSubnet(test, new(util.Options)) + assert.NilError(t, err) + }) + } +} + +func TestInvalidSub(t *testing.T) { + t.Parallel() + + err := util.ParseSubnet("1", new(util.Options)) + assert.ErrorContains(t, err, "invalid CIDR address") +} diff --git a/util/query.go b/util/query.go new file mode 100644 index 0000000..20851b6 --- /dev/null +++ b/util/query.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package util + +import ( + "time" + + "github.com/miekg/dns" +) + +// Response is 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 +} + +// Request is a structure for a DNS query. +type Request struct { + Server string `json:"server"` + Name string `json:"name"` + Timeout time.Duration + Retries int + Type uint16 `json:"request"` + Class uint16 `json:"class"` +} diff --git a/util/reverseDNS.go b/util/reverseDNS.go index cfaef14..1d6c7fc 100644 --- a/util/reverseDNS.go +++ b/util/reverseDNS.go @@ -17,14 +17,15 @@ 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. +// ReverseDNS is given an IP or phone number and returns a canonical string to be queried. func ReverseDNS(address string, querInt uint16) (string, error) { query := dns.TypeToString[querInt] if query == "PTR" { str, err := dns.ReverseAddr(address) if err != nil { - return "", fmt.Errorf("could not reverse: %w", err) + return "", fmt.Errorf("PTR reverse: %w", err) } + return str, nil } else if query == "NAPTR" { // get rid of characters not needed @@ -38,6 +39,7 @@ func ReverseDNS(address string, querInt uint16) (string, error) { fmt.Fprintf(&arpa, "%c.", c) } arpa.WriteString("e164.arpa.") + return arpa.String(), nil } @@ -50,5 +52,6 @@ func reverse(s string) string { for i, j := 0, len(rns)-1; i < j; i, j = i+1, j-1 { rns[i], rns[j] = rns[j], rns[i] } + return string(rns) } diff --git a/util/reverseDNS_test.go b/util/reverseDNS_test.go index 6e2fae6..5a77748 100644 --- a/util/reverseDNS_test.go +++ b/util/reverseDNS_test.go @@ -17,6 +17,7 @@ var ( func TestIPv4(t *testing.T) { t.Parallel() + act, err := util.ReverseDNS("8.8.4.4", PTR) assert.NilError(t, err) assert.Equal(t, act, "4.4.8.8.in-addr.arpa.", "IPv4 reverse") @@ -24,6 +25,7 @@ func TestIPv4(t *testing.T) { func TestIPv6(t *testing.T) { t.Parallel() + act, err := util.ReverseDNS("2606:4700:4700::1111", PTR) assert.NilError(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") @@ -31,6 +33,7 @@ func TestIPv6(t *testing.T) { func TestNAPTR(t *testing.T) { t.Parallel() + tests := []struct { in string want string @@ -54,12 +57,14 @@ func TestNAPTR(t *testing.T) { func TestInvalid(t *testing.T) { t.Parallel() + _, err := util.ReverseDNS("AAAAA", 1) 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") + assert.ErrorContains(t, err, "PTR reverse") }