├── coverage └── .gitkeep ├── Dockerfile ├── .gitmodules ├── docs ├── img │ └── awl-text.png ├── makeman.sh ├── prepare-packaging.sh ├── CONTRIBUTING.md └── awl.1.scd ├── pkg ├── resolvers │ ├── docs.go │ ├── DNSCrypt.go │ ├── resolver.go │ ├── DNSCrypt_test.go │ ├── HTTPS_test.go │ ├── general.go │ ├── QUIC_test.go │ ├── HTTPS.go │ ├── general_test.go │ └── QUIC.go ├── query │ ├── docs.go │ ├── query_test.go │ ├── query.go │ ├── print_test.go │ ├── struct.go │ ├── util.go │ └── print.go ├── util │ ├── docs.go │ ├── logger.go │ ├── logger_test.go │ ├── errors.go │ ├── options_test.go │ ├── query.go │ ├── reverseDNS.go │ ├── reverseDNS_test.go │ └── options.go └── logawl │ ├── docs.go │ ├── logawl.go │ ├── logging_test.go │ └── logger.go ├── cmd ├── docs.go ├── dig_test.go ├── cli_test.go ├── misc_test.go ├── misc.go ├── dig.go └── cli.go ├── completions ├── bash.bash ├── fish.fish └── zsh.zsh ├── renovate.json ├── conf ├── docs.go ├── wasm.go ├── plan9_test.go ├── win_test.go ├── unix.go ├── unix_test.go ├── plan9.go └── win.go ├── .editorconfig ├── .github ├── pull_request_template.md ├── workflows │ ├── test.yaml │ └── ghrelease.yaml └── ISSUE_TEMPLATE │ ├── feature.md │ └── bug.md ├── docs.go ├── .gitignore ├── .forgejo └── workflows │ ├── test.yaml │ └── release.yaml ├── Makefile ├── mkfile ├── go.mod ├── GNUmakefile ├── main_test.go ├── LICENSE ├── .golangci.yaml ├── template.mk ├── main.go ├── go.sum ├── README.md └── .goreleaser.yaml /coverage/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ENTRYPOINT ["/awl"] 3 | COPY awl / 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/wiki"] 2 | path = docs/wiki 3 | url = ../awl.wiki 4 | -------------------------------------------------------------------------------- /docs/img/awl-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamTherapy/awl/HEAD/docs/img/awl-text.png -------------------------------------------------------------------------------- /pkg/resolvers/docs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package resolvers contain the various DNS resolvers to use. 3 | */ 4 | package resolvers 5 | -------------------------------------------------------------------------------- /docs/makeman.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | rm -f docs/awl.1.gz 6 | scdoc docs/awl.1 7 | gzip -9 -n docs/awl.1 8 | -------------------------------------------------------------------------------- /pkg/query/docs.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | // Package query is for the various query types. 4 | package query 5 | -------------------------------------------------------------------------------- /pkg/util/docs.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | // Package util contains helper functions that don't belong anywhere else 4 | package util 5 | -------------------------------------------------------------------------------- /cmd/docs.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | /* 4 | Package cli is the CLI part of the package, including both POSIX 5 | flag parsing and dig-like flag parsing. 6 | */ 7 | package cli 8 | -------------------------------------------------------------------------------- /docs/prepare-packaging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | rm -f docs/awl.1.gz 6 | scdoc docs/awl.1 7 | gzip -9kn docs/awl.1 8 | gzip -9kn README.md 9 | gzip -9kn docs/CONTRIBUTING.md 10 | -------------------------------------------------------------------------------- /completions/bash.bash: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | # bash completion for awl -*- shell-script -*- 3 | 4 | 5 | # TODO: MAKE THIS A REAL THING 6 | complete -F _known_hosts awl 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":gomod", 6 | "group:allNonMajor" 7 | ], 8 | "automerge": true 9 | } 10 | -------------------------------------------------------------------------------- /conf/docs.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | /* 4 | Package conf contains helper functions for getting local nameservers 5 | 6 | Currently supported: Unix, Windows, Plan 9 (tested on 9front) 7 | */ 8 | package conf 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /pkg/util/logger.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package util 4 | 5 | import "dns.froth.zone/awl/pkg/logawl" 6 | 7 | // InitLogger initializes the logawl instance. 8 | func InitLogger(verbosity int) (log *logawl.Logger) { 9 | log = logawl.New() 10 | 11 | log.SetLevel(logawl.Level(verbosity)) 12 | 13 | return log 14 | } 15 | -------------------------------------------------------------------------------- /pkg/util/logger_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package util_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "dns.froth.zone/awl/pkg/logawl" 9 | "dns.froth.zone/awl/pkg/util" 10 | "gotest.tools/v3/assert" 11 | ) 12 | 13 | func TestInitLogger(t *testing.T) { 14 | t.Parallel() 15 | 16 | logger := util.InitLogger(0) 17 | assert.Equal(t, logger.Level, logawl.Level(0)) 18 | } 19 | -------------------------------------------------------------------------------- /docs.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | /* 4 | awl is a DNS lookup tool written in Go, similar to (and heavily inspired by) drill. 5 | 6 | It runs and displays similar outputs to drill, without any frills. 7 | Options are given to print with JSON, XML and YAML. 8 | 9 | Supports results from DNS-over-[UDP, TCP, TLS, HTTPS, QUIC] servers 10 | 11 | Why use this over the alternatives? Good question. 12 | */ 13 | package main 14 | -------------------------------------------------------------------------------- /pkg/util/errors.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // ErrHTTPStatus is returned when DoH returns a bad status code. 9 | type ErrHTTPStatus struct { 10 | // Status code 11 | Code int 12 | } 13 | 14 | func (e *ErrHTTPStatus) Error() string { 15 | return fmt.Sprintf("doh server responded with HTTP %d", e.Code) 16 | } 17 | 18 | // ErrNotError is an error that is not actually an error. 19 | var ErrNotError = errors.New("not an error") 20 | -------------------------------------------------------------------------------- /conf/wasm.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | //go:build js 3 | 4 | package conf 5 | 6 | import ( 7 | "errors" 8 | 9 | "github.com/miekg/dns" 10 | ) 11 | 12 | // GetDNSConfig doesn't do anything, because it is impossible (and bad security) 13 | // if it could, as that is the definition of a DNS leak. 14 | func GetDNSConfig() (*dns.ClientConfig, error) { 15 | return nil, errNotImplemented 16 | } 17 | 18 | var errNotImplemented = errors.New("not implemented") 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | dist/ 20 | 21 | # Test coverage 22 | coverage/* 23 | !coverage/.gitkeep 24 | 25 | awl 26 | docs/awl.1 27 | *.gz 28 | 29 | .dccache 30 | -------------------------------------------------------------------------------- /conf/plan9_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | //go:build plan9 3 | 4 | package conf_test 5 | 6 | import ( 7 | "runtime" 8 | "testing" 9 | 10 | "dns.froth.zone/awl/conf" 11 | "gotest.tools/v3/assert" 12 | ) 13 | 14 | func TestPlan9Config(t *testing.T) { 15 | t.Parallel() 16 | 17 | if runtime.GOOS != "plan9" { 18 | t.Skip("Not running Plan 9, skipping") 19 | } 20 | 21 | conf, err := conf.GetDNSConfig() 22 | 23 | assert.NilError(t, err) 24 | assert.Assert(t, len(conf.Servers) != 0) 25 | } 26 | -------------------------------------------------------------------------------- /conf/win_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | //go:build windows 3 | 4 | package conf_test 5 | 6 | import ( 7 | "runtime" 8 | "testing" 9 | 10 | "dns.froth.zone/awl/conf" 11 | "gotest.tools/v3/assert" 12 | ) 13 | 14 | func TestWinConfig(t *testing.T) { 15 | t.Parallel() 16 | 17 | if runtime.GOOS != "windows" { 18 | t.Skip("Not running Windows, skipping") 19 | } 20 | 21 | conf, err := conf.GetDNSConfig() 22 | 23 | assert.NilError(t, err) 24 | assert.Assert(t, len(conf.Servers) != 0) 25 | } 26 | -------------------------------------------------------------------------------- /.forgejo/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: push 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | fail-fast: true 8 | matrix: 9 | goVer: ["oldstable", "stable"] 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v6 15 | with: 16 | submodules: recursive 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v6 20 | with: 21 | go-version: ${{ matrix.goVer }} 22 | 23 | - name: Test 24 | run: make test-ci 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: push 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | fail-fast: true 8 | matrix: 9 | platform: [macos, windows] 10 | goVer: ["oldstable", "stable"] 11 | runs-on: ${{ matrix.platform }}-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v6 15 | with: 16 | submodules: recursive 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v6 20 | with: 21 | go-version: ${{ matrix.goVer }} 22 | 23 | - name: Test 24 | run: make test-ci 25 | -------------------------------------------------------------------------------- /conf/unix.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | //go:build unix || (!windows && !plan9 && !js && !zos) 3 | 4 | // FIXME: Can remove the or on the preprocessor when Go 1.18 becomes obsolete 5 | 6 | package conf 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | // GetDNSConfig gets the DNS configuration, either from /etc/resolv.conf or somewhere else. 15 | func GetDNSConfig() (*dns.ClientConfig, error) { 16 | conf, err := dns.ClientConfigFromFile("/etc/resolv.conf") 17 | if err != nil { 18 | return nil, fmt.Errorf("unix config: %w", err) 19 | } 20 | 21 | return conf, nil 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | Links to implementations in dig, drill, etc. should go here. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | # BSD/POSIX makefile 3 | 4 | include template.mk 5 | 6 | EXE := $(PROG) 7 | 8 | ## install: installs awl 9 | .PHONY: install 10 | install: all 11 | install -Dm755 $(PROG) $(DESTDIR)$(PREFIX)/$(BIN)/$(PROG) 12 | install -Dm644 docs/$(PROG).1 $(DESTDIR)$(MAN)/man1/$(PROG).1 13 | # completions need to go in one specific place :) 14 | install -Dm644 completions/bash.bash $(DESTDIR)$(PREFIX)$(SHARE)/bash-completion/completions/$(PROG) 15 | install -Dm644 completions/fish.fish $(DESTDIR)$(PREFIX)$(SHARE)/fish/vendor_completions.d/$(PROG).fish 16 | install -Dm644 completions/zsh.zsh $(DESTDIR)$(PREFIX)$(SHARE)/zsh/site-functions/_$(PROG) 17 | -------------------------------------------------------------------------------- /mkfile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | # Plan 9 mkfile 3 | 4 | > "${GITHUB_ENV}" 21 | 22 | - name: Login to Container Registry 23 | uses: docker/login-action@v3 24 | with: 25 | registry: ${{ env.REGISTRY }} 26 | username: ${{ github.actor }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Checkout repository 30 | uses: actions/checkout@v6 31 | with: 32 | fetch-depth: 0 33 | submodules: recursive 34 | 35 | - name: Install Snapcraft 36 | uses: samuelmeuli/action-snapcraft@v3 37 | 38 | - name: Set up Go 39 | uses: actions/setup-go@v6 40 | with: 41 | go-version: stable 42 | 43 | - name: Install scdoc 44 | run: sudo apt-get install -y scdoc 45 | 46 | - name: Workaround a dumb Snap bug 47 | run: mkdir -p $HOME/.cache/snapcraft/download && mkdir -p $HOME/.cache/snapcraft/stage-packages 48 | 49 | - name: Release with GoReleaser 50 | uses: goreleaser/goreleaser-action@v6 51 | with: 52 | distribution: goreleaser 53 | version: latest 54 | args: release --clean --skip=aur,homebrew,nix,scoop 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | -------------------------------------------------------------------------------- /pkg/util/reverseDNS_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package util_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "dns.froth.zone/awl/pkg/util" 9 | "github.com/miekg/dns" 10 | "gotest.tools/v3/assert" 11 | ) 12 | 13 | func TestPTR(t *testing.T) { 14 | t.Parallel() 15 | 16 | tests := []struct { 17 | name string 18 | in string 19 | expected string 20 | }{ 21 | { 22 | "IPv4", 23 | "8.8.4.4", "4.4.8.8.in-addr.arpa.", 24 | }, 25 | { 26 | "IPv6", 27 | "2606:4700:4700::1111", "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.", 28 | }, 29 | { 30 | "Inavlid value", 31 | "AAAAA", "", 32 | }, 33 | } 34 | 35 | for _, test := range tests { 36 | test := test 37 | 38 | t.Run(test.name, func(t *testing.T) { 39 | t.Parallel() 40 | 41 | act, err := util.ReverseDNS(test.in, dns.StringToType["PTR"]) 42 | if err == nil { 43 | assert.NilError(t, err) 44 | } else { 45 | assert.ErrorContains(t, err, "unrecognized address") 46 | } 47 | assert.Equal(t, act, test.expected) 48 | }) 49 | } 50 | } 51 | 52 | func TestNAPTR(t *testing.T) { 53 | t.Parallel() 54 | 55 | tests := []struct { 56 | in string 57 | want string 58 | }{ 59 | {"1-800-555-1234", "4.3.2.1.5.5.5.0.0.8.1.e164.arpa."}, 60 | {"+1 800555 1234", "4.3.2.1.5.5.5.0.0.8.1.e164.arpa."}, 61 | {"+46766861004", "4.0.0.1.6.8.6.6.7.6.4.e164.arpa."}, 62 | {"17705551212", "2.1.2.1.5.5.5.0.7.7.1.e164.arpa."}, 63 | } 64 | for _, test := range tests { 65 | // Thanks Goroutines, very cool! 66 | test := test 67 | t.Run(test.in, func(t *testing.T) { 68 | t.Parallel() 69 | act, err := util.ReverseDNS(test.in, dns.StringToType["NAPTR"]) 70 | assert.NilError(t, err) 71 | assert.Equal(t, test.want, act) 72 | }) 73 | } 74 | } 75 | 76 | func TestInvalidAll(t *testing.T) { 77 | _, err := util.ReverseDNS("q", 15236) 78 | assert.ErrorContains(t, err, "invalid value") 79 | } 80 | -------------------------------------------------------------------------------- /pkg/resolvers/resolver.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package resolvers 4 | 5 | import ( 6 | "net" 7 | "strconv" 8 | "strings" 9 | 10 | "dns.froth.zone/awl/pkg/util" 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | const ( 15 | tcp = "tcp" 16 | udp = "udp" 17 | ) 18 | 19 | // Resolver is the main resolver interface. 20 | type Resolver interface { 21 | LookUp(*dns.Msg) (util.Response, error) 22 | } 23 | 24 | // LoadResolver loads the respective resolver for performing a DNS query. 25 | func LoadResolver(opts *util.Options) (resolver Resolver, err error) { 26 | switch { 27 | case opts.HTTPS: 28 | opts.Logger.Info("loading DNS-over-HTTPS resolver") 29 | 30 | if !strings.HasPrefix(opts.Request.Server, "https://") { 31 | opts.Request.Server = "https://" + opts.Request.Server 32 | } 33 | 34 | // Make sure that the endpoint is defaulted to /dns-query 35 | if !strings.HasSuffix(opts.Request.Server, opts.HTTPSOptions.Endpoint) { 36 | opts.Request.Server += opts.HTTPSOptions.Endpoint 37 | } 38 | 39 | resolver = &HTTPSResolver{ 40 | opts: opts, 41 | } 42 | 43 | return 44 | case opts.QUIC: 45 | opts.Logger.Info("loading DNS-over-QUIC resolver") 46 | 47 | if !strings.HasSuffix(opts.Request.Server, ":"+strconv.Itoa(opts.Request.Port)) { 48 | opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Request.Port)) 49 | } 50 | 51 | resolver = &QUICResolver{ 52 | opts: opts, 53 | } 54 | 55 | return 56 | case opts.DNSCrypt: 57 | opts.Logger.Info("loading DNSCrypt resolver") 58 | 59 | if !strings.HasPrefix(opts.Request.Server, "sdns://") { 60 | opts.Request.Server = "sdns://" + opts.Request.Server 61 | } 62 | 63 | resolver = &DNSCryptResolver{ 64 | opts: opts, 65 | } 66 | 67 | return 68 | default: 69 | opts.Logger.Info("loading standard/DNS-over-TLS resolver") 70 | 71 | if !strings.HasSuffix(opts.Request.Server, ":"+strconv.Itoa(opts.Request.Port)) { 72 | opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Request.Port)) 73 | } 74 | 75 | resolver = &StandardResolver{ 76 | opts: opts, 77 | } 78 | 79 | return 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # Refer to golangci-lint's example config file for more options and information: 2 | # https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml 3 | 4 | run: 5 | timeout: 5m 6 | modules-download-mode: readonly 7 | skip-dirs: 8 | - "coverage" 9 | - ".github" 10 | 11 | linters: 12 | enable: 13 | - errcheck 14 | - errorlint 15 | - gci 16 | - gocritic 17 | - goconst 18 | - godot 19 | - goimports 20 | - govet 21 | - gocritic 22 | - goerr113 23 | - gofmt 24 | - gofumpt 25 | - gosec 26 | - maintidx 27 | - makezero 28 | - misspell 29 | - nlreturn 30 | - nolintlint 31 | - prealloc 32 | - predeclared 33 | - revive 34 | - staticcheck 35 | - tagliatelle 36 | - whitespace 37 | - wrapcheck 38 | - wsl 39 | disable: 40 | - structcheck 41 | 42 | linters-settings: 43 | govet: 44 | check-shadowing: true 45 | enable-all: true 46 | disable-all: false 47 | revive: 48 | ignore-generated-header: false 49 | severity: warning 50 | confidence: 0.8 51 | errorCode: 1 52 | warningCode: 1 53 | rules: 54 | - name: blank-imports 55 | - name: context-as-argument 56 | - name: context-keys-type 57 | - name: dot-imports 58 | - name: duplicated-imports 59 | - name: error-return 60 | - name: error-strings 61 | - name: error-naming 62 | - name: errorf 63 | - name: exported 64 | - name: if-return 65 | - name: increment-decrement 66 | - name: modifies-value-receiver 67 | - name: package-comments 68 | - name: range 69 | - name: receiver-naming 70 | - name: time-naming 71 | - name: unexported-return 72 | - name: var-declaration 73 | - name: var-naming 74 | linters-settings: 75 | tagliatelle: 76 | case: 77 | use-field-name: false 78 | rules: 79 | # Any struct tag type can be used. 80 | # Support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower` 81 | json: goCamel 82 | yaml: goCamel 83 | xml: goCamel 84 | 85 | issues: 86 | exclude-use-default: false 87 | max-issues-per-linter: 0 88 | max-same-issues: 0 89 | -------------------------------------------------------------------------------- /pkg/logawl/logawl.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package logawl 4 | 5 | import ( 6 | "errors" 7 | "io" 8 | "sync" 9 | "sync/atomic" 10 | ) 11 | 12 | type ( 13 | // Level is the logging level. 14 | Level int32 15 | 16 | // Logger is the overall logger. 17 | Logger struct { 18 | Out io.Writer 19 | Prefix string 20 | buf []byte 21 | Mu sync.Mutex 22 | Level Level 23 | isDiscard int32 24 | } 25 | ) 26 | 27 | // SetLevel stores whatever input value is in mem address of l.level. 28 | func (logger *Logger) SetLevel(level Level) { 29 | atomic.StoreInt32((*int32)(&logger.Level), int32(level)) 30 | } 31 | 32 | // GetLevel gets the logger level. 33 | func (logger *Logger) GetLevel() Level { 34 | return logger.level() 35 | } 36 | 37 | // Retrieves whatever was stored in mem address of l.level. 38 | func (logger *Logger) level() Level { 39 | return Level(atomic.LoadInt32((*int32)(&logger.Level))) 40 | } 41 | 42 | // UnMarshalLevel unmarshalls the int value of level for writing the header. 43 | func (logger *Logger) UnMarshalLevel(lv Level) (string, error) { 44 | switch lv { 45 | case ErrLevel: 46 | return "ERROR ", nil 47 | case WarnLevel: 48 | return "WARN ", nil 49 | case InfoLevel: 50 | return "INFO ", nil 51 | case DebugLevel: 52 | return "DEBUG ", nil 53 | } 54 | 55 | return "", errInvalidLevel 56 | } 57 | 58 | // IsLevel returns true if the logger level is above the level given. 59 | func (logger *Logger) IsLevel(level Level) bool { 60 | return logger.level() >= level 61 | } 62 | 63 | // AllLevels is an array of all valid log levels. 64 | var AllLevels = []Level{ 65 | ErrLevel, 66 | WarnLevel, 67 | InfoLevel, 68 | DebugLevel, 69 | } 70 | 71 | const ( 72 | // ErrLevel is the fatal (error) log level. 73 | ErrLevel Level = iota 74 | 75 | // WarnLevel is for warning logs. 76 | // 77 | // Example: when one setting implies another, when a request fails but is retried. 78 | WarnLevel 79 | 80 | // InfoLevel is for saying what is going on when. 81 | // This is essentially the "verbose" option. 82 | // 83 | // When in doubt, use info. 84 | InfoLevel 85 | 86 | // DebugLevel is for spewing debug structs/interfaces. 87 | DebugLevel 88 | ) 89 | 90 | var errInvalidLevel = errors.New("invalid log level") 91 | -------------------------------------------------------------------------------- /cmd/dig_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package cli_test 4 | 5 | import ( 6 | "testing" 7 | 8 | cli "dns.froth.zone/awl/cmd" 9 | "dns.froth.zone/awl/pkg/util" 10 | "gotest.tools/v3/assert" 11 | ) 12 | 13 | func FuzzDig(f *testing.F) { 14 | f.Log("ParseDig Fuzzing") 15 | 16 | seeds := []string{ 17 | "aaflag", "aaonly", "noaaflag", "noaaonly", 18 | "adflag", "noadflag", 19 | "cdflag", "nocdflag", 20 | "qrflag", "noqrflag", 21 | "raflag", "noraflag", 22 | "rdflag", "recurse", "nordflag", "norecurse", 23 | "tcflag", "notcflag", 24 | "zflag", "nozflag", 25 | "qr", "noqr", 26 | "ttlunits", "nottlunits", 27 | "ttlid", "nottlid", 28 | "do", "dnssec", "nodo", "nodnssec", 29 | "edns", "edns=a", "edns=0", "noedns", 30 | "expire", "noexpire", 31 | "ednsflags", "ednsflags=\"", "ednsflags=1", "noednsflags", 32 | "subnet=0.0.0.0/0", "subnet=::0/0", "subnet=b", "subnet=0", "subnet", 33 | "cookie", "nocookie", 34 | "keepopen", "keepalive", "nokeepopen", "nokeepalive", 35 | "nsid", "nonsid", 36 | "padding", "nopadding", 37 | "bufsize=512", "bufsize=a", "bufsize", 38 | "time=5", "timeout=a", "timeout", 39 | "retry=a", "retry=3", "retry", 40 | "tries=2", "tries=b", "tries", 41 | "tcp", "vc", "notcp", "novc", 42 | "ignore", "noignore", 43 | "badcookie", "nobadcookie", 44 | "tls", "notls", 45 | "dnscrypt", "nodnscrypt", 46 | "https", "https=/dns", "https-get", "https-get=/", "nohttps", 47 | "quic", "noquic", 48 | "short", "noshort", 49 | "identify", "noidentify", 50 | "json", "nojson", 51 | "xml", "noxml", 52 | "yaml", "noyaml", 53 | "comments", "nocomments", 54 | "question", "noquestion", 55 | "opt", "noopt", 56 | "answer", "noanswer", 57 | "authority", "noauthority", 58 | "additional", "noadditional", 59 | "stats", "nostats", 60 | "all", "noall", 61 | "idnout", "noidnout", 62 | "class", "noclass", 63 | "trace", "notrace", 64 | "invalid", 65 | } 66 | 67 | for _, tc := range seeds { 68 | f.Add(tc) 69 | } 70 | 71 | f.Fuzz(func(t *testing.T, orig string) { 72 | // Get rid of outputs 73 | // os.Stdout = os.NewFile(0, os.DevNull) 74 | // os.Stderr = os.NewFile(0, os.DevNull) 75 | 76 | opts := new(util.Options) 77 | opts.Logger = util.InitLogger(0) 78 | if err := cli.ParseDig(orig, opts); err != nil { 79 | assert.ErrorContains(t, err, "digflags:") 80 | } 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/logawl/logging_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package logawl_test 4 | 5 | import ( 6 | "bytes" 7 | "testing" 8 | "time" 9 | 10 | "dns.froth.zone/awl/pkg/logawl" 11 | "gotest.tools/v3/assert" 12 | ) 13 | 14 | var logger = logawl.New() 15 | 16 | func TestLogawl(t *testing.T) { 17 | t.Parallel() 18 | 19 | for i := range logawl.AllLevels { 20 | logger.SetLevel(logawl.Level(i)) 21 | assert.Equal(t, logawl.Level(i), logger.GetLevel()) 22 | } 23 | } 24 | 25 | func TestUnmarshalLevels(t *testing.T) { 26 | t.Parallel() 27 | 28 | m := make(map[int]string) 29 | 30 | for i := range logawl.AllLevels { 31 | var err error 32 | m[i], err = logger.UnMarshalLevel(logawl.Level(i)) 33 | assert.NilError(t, err) 34 | } 35 | 36 | for i := range logawl.AllLevels { 37 | lv, err := logger.UnMarshalLevel(logawl.Level(i)) 38 | assert.NilError(t, err) 39 | assert.Equal(t, m[i], lv) 40 | } 41 | 42 | lv, err := logger.UnMarshalLevel(logawl.Level(9001)) 43 | assert.Equal(t, "", lv) 44 | assert.ErrorContains(t, err, "invalid log level") 45 | } 46 | 47 | func TestLogger(t *testing.T) { 48 | t.Parallel() 49 | 50 | for i := range logawl.AllLevels { 51 | switch i { 52 | case 0: 53 | fn := func() { 54 | logger.Error("Test", "E") 55 | logger.Errorf("%s", "Test") 56 | } 57 | 58 | var buffer bytes.Buffer 59 | 60 | logger.Out = &buffer 61 | 62 | fn() 63 | case 1: 64 | fn := func() { 65 | logger.Warn("Test") 66 | logger.Warnf("%s", "Test") 67 | } 68 | 69 | var buffer bytes.Buffer 70 | 71 | logger.Out = &buffer 72 | 73 | fn() 74 | case 2: 75 | fn := func() { 76 | logger.Info("Test") 77 | logger.Infof("%s", "Test") 78 | } 79 | 80 | var buffer bytes.Buffer 81 | 82 | logger.Out = &buffer 83 | 84 | fn() 85 | case 3: 86 | fn := func() { 87 | logger.Debug("Test") 88 | logger.Debug("Test 2") 89 | logger.Debugf("%s", "Test") 90 | logger.Debugf("%s %d", "Test", 2) 91 | } 92 | 93 | var buffer bytes.Buffer 94 | 95 | logger.Out = &buffer 96 | 97 | fn() 98 | } 99 | } 100 | } 101 | 102 | func TestFmt(t *testing.T) { 103 | t.Parallel() 104 | 105 | ti := time.Now() 106 | test := []byte("test") 107 | // make sure error is error 108 | assert.ErrorContains(t, logger.FormatHeader(&test, ti, 0, 9001), "invalid log level") 109 | } 110 | -------------------------------------------------------------------------------- /conf/win.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | //go:build windows 3 | 4 | package conf 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "unsafe" 10 | 11 | "github.com/miekg/dns" 12 | "golang.org/x/sys/windows" 13 | ) 14 | 15 | /* 16 | "Stolen" from 17 | https://gist.github.com/moloch--/9fb1c8497b09b45c840fe93dd23b1e98 18 | */ 19 | 20 | // GetDNSConfig (Windows version) returns all DNS server addresses using windows fuckery. 21 | // 22 | // Here be dragons. 23 | func GetDNSConfig() (*dns.ClientConfig, error) { 24 | length := uint32(100000) 25 | byt := make([]byte, length) 26 | 27 | // Windows is an utter fucking trash fire of an operating system. 28 | //nolint:gosec // This is necessary unless we want to drop 1.18 29 | if err := windows.GetAdaptersAddresses(windows.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&byt[0])), &length); err != nil { 30 | return nil, fmt.Errorf("config, windows: %w", err) 31 | } 32 | 33 | var addresses []*windows.IpAdapterAddresses 34 | //nolint:gosec // This is necessary unless we want to drop 1.18 35 | for addr := (*windows.IpAdapterAddresses)(unsafe.Pointer(&byt[0])); addr != nil; addr = addr.Next { 36 | addresses = append(addresses, addr) 37 | } 38 | 39 | resolvers := map[string]bool{} 40 | 41 | for _, addr := range addresses { 42 | for next := addr.FirstUnicastAddress; next != nil; next = next.Next { 43 | if addr.OperStatus != windows.IfOperStatusUp { 44 | continue 45 | } 46 | 47 | if next.Address.IP() != nil { 48 | for dnsServer := addr.FirstDnsServerAddress; dnsServer != nil; dnsServer = dnsServer.Next { 49 | ip := dnsServer.Address.IP() 50 | 51 | if ip.IsMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() { 52 | continue 53 | } 54 | 55 | if ip.To16() != nil && strings.HasPrefix(ip.To16().String(), "fec0:") { 56 | continue 57 | } 58 | 59 | resolvers[ip.String()] = true 60 | } 61 | 62 | break 63 | } 64 | } 65 | } 66 | 67 | // Take unique values only 68 | servers := []string{} 69 | for server := range resolvers { 70 | servers = append(servers, server) 71 | } 72 | 73 | // TODO: Make configurable, based on defaults in https://github.com/miekg/dns/blob/master/clientconfig.go 74 | return &dns.ClientConfig{ 75 | Servers: servers, 76 | Search: []string{}, 77 | Port: "53", 78 | Ndots: 1, 79 | Timeout: 5, 80 | Attempts: 1, 81 | }, nil 82 | } 83 | -------------------------------------------------------------------------------- /pkg/resolvers/DNSCrypt_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package resolvers_test 4 | 5 | import ( 6 | "errors" 7 | "testing" 8 | 9 | "dns.froth.zone/awl/pkg/query" 10 | "dns.froth.zone/awl/pkg/util" 11 | "github.com/ameshkov/dnscrypt/v2" 12 | "github.com/miekg/dns" 13 | "gotest.tools/v3/assert" 14 | ) 15 | 16 | func TestDNSCrypt(t *testing.T) { 17 | t.Parallel() 18 | 19 | //nolint:govet // I could not be assed to refactor this, and it is only for tests 20 | tests := []struct { 21 | name string 22 | opts *util.Options 23 | }{ 24 | { 25 | "Valid", 26 | &util.Options{ 27 | Logger: util.InitLogger(0), 28 | DNSCrypt: true, 29 | Request: util.Request{ 30 | Server: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", 31 | Type: dns.TypeA, 32 | Name: "example.com.", 33 | Retries: 3, 34 | }, 35 | }, 36 | }, 37 | { 38 | "Valid (TCP)", 39 | &util.Options{ 40 | Logger: util.InitLogger(0), 41 | DNSCrypt: true, 42 | TCP: true, 43 | IPv4: true, 44 | Request: util.Request{ 45 | Server: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", 46 | Type: dns.TypeAAAA, 47 | Name: "example.com.", 48 | Retries: 3, 49 | }, 50 | }, 51 | }, 52 | { 53 | "Invalid", 54 | &util.Options{ 55 | Logger: util.InitLogger(0), 56 | DNSCrypt: true, 57 | TCP: true, 58 | IPv4: true, 59 | Request: util.Request{ 60 | Server: "QMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", 61 | Type: dns.TypeAAAA, 62 | Name: "example.com.", 63 | Retries: 0, 64 | }, 65 | }, 66 | }, 67 | } 68 | 69 | for _, test := range tests { 70 | test := test 71 | 72 | t.Run(test.name, func(t *testing.T) { 73 | t.Parallel() 74 | 75 | var ( 76 | res util.Response 77 | err error 78 | ) 79 | for i := 0; i <= test.opts.Request.Retries; i++ { 80 | res, err = query.CreateQuery(test.opts) 81 | if err == nil || errors.Is(err, dnscrypt.ErrInvalidDNSStamp) { 82 | break 83 | } 84 | } 85 | 86 | if err == nil { 87 | assert.Assert(t, res != util.Response{}) 88 | } else { 89 | assert.ErrorContains(t, err, "unsupported stamp") 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkg/resolvers/HTTPS_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package resolvers_test 4 | 5 | import ( 6 | "errors" 7 | "testing" 8 | 9 | "dns.froth.zone/awl/pkg/query" 10 | "dns.froth.zone/awl/pkg/util" 11 | "github.com/miekg/dns" 12 | "gotest.tools/v3/assert" 13 | ) 14 | 15 | func TestHTTPS(t *testing.T) { 16 | t.Parallel() 17 | 18 | //nolint:govet // I could not be assed to refactor this, and it is only for tests 19 | tests := []struct { 20 | name string 21 | opts *util.Options 22 | }{ 23 | { 24 | "Good", 25 | &util.Options{ 26 | HTTPS: true, 27 | Logger: util.InitLogger(0), 28 | Request: util.Request{ 29 | Server: "https://dns9.quad9.net/dns-query", 30 | Type: dns.TypeA, 31 | Name: "git.froth.zone.", 32 | Retries: 3, 33 | }, 34 | }, 35 | }, 36 | { 37 | "404", 38 | &util.Options{ 39 | HTTPS: true, 40 | Logger: util.InitLogger(0), 41 | Request: util.Request{ 42 | Server: "https://dns9.quad9.net/dns", 43 | Type: dns.TypeA, 44 | Name: "git.froth.zone.", 45 | }, 46 | }, 47 | }, 48 | { 49 | "Bad request domain", 50 | &util.Options{ 51 | HTTPS: true, 52 | Logger: util.InitLogger(0), 53 | Request: util.Request{ 54 | Server: "dns9.quad9.net/dns-query", 55 | Type: dns.TypeA, 56 | Name: "git.froth.zone", 57 | }, 58 | }, 59 | }, 60 | { 61 | "Bad server domain", 62 | &util.Options{ 63 | HTTPS: true, 64 | Logger: util.InitLogger(0), 65 | Request: util.Request{ 66 | Server: "dns9..quad9.net/dns-query", 67 | Type: dns.TypeA, 68 | Name: "git.froth.zone.", 69 | }, 70 | }, 71 | }, 72 | } 73 | 74 | for _, test := range tests { 75 | test := test 76 | 77 | t.Run(test.name, func(t *testing.T) { 78 | t.Parallel() 79 | 80 | var ( 81 | res util.Response 82 | err error 83 | ) 84 | for i := 0; i <= test.opts.Request.Retries; i++ { 85 | res, err = query.CreateQuery(test.opts) 86 | if err == nil || errors.Is(err, &util.ErrHTTPStatus{}) { 87 | break 88 | } 89 | } 90 | 91 | if err == nil { 92 | assert.NilError(t, err) 93 | assert.Assert(t, res != util.Response{}) 94 | } else { 95 | if errors.Is(err, &util.ErrHTTPStatus{}) { 96 | assert.ErrorContains(t, err, "404") 97 | } 98 | assert.Equal(t, res, util.Response{}) 99 | } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/resolvers/general.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package resolvers 4 | 5 | import ( 6 | "crypto/tls" 7 | "fmt" 8 | "net" 9 | 10 | "dns.froth.zone/awl/pkg/util" 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | // StandardResolver is for UDP/TCP resolvers. 15 | type StandardResolver struct { 16 | opts *util.Options 17 | } 18 | 19 | var _ Resolver = (*StandardResolver)(nil) 20 | 21 | // LookUp performs a DNS query. 22 | func (resolver *StandardResolver) LookUp(msg *dns.Msg) (resp util.Response, err error) { 23 | dnsClient := new(dns.Client) 24 | dnsClient.Dialer = &net.Dialer{ 25 | Timeout: resolver.opts.Request.Timeout, 26 | } 27 | 28 | if resolver.opts.TCP || resolver.opts.TLS { 29 | dnsClient.Net = tcp 30 | } else { 31 | dnsClient.Net = udp 32 | } 33 | 34 | switch { 35 | case resolver.opts.IPv4: 36 | dnsClient.Net += "4" 37 | case resolver.opts.IPv6: 38 | dnsClient.Net += "6" 39 | } 40 | 41 | if resolver.opts.TLS { 42 | dnsClient.Net += "-tls" 43 | dnsClient.TLSConfig = &tls.Config{ 44 | //nolint:gosec // This is intentional if the user requests it 45 | InsecureSkipVerify: resolver.opts.TLSNoVerify, 46 | ServerName: resolver.opts.TLSHost, 47 | } 48 | } 49 | 50 | resolver.opts.Logger.Info("Using", dnsClient.Net, "for making the request") 51 | 52 | resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, resolver.opts.Request.Server) 53 | if err != nil { 54 | return resp, fmt.Errorf("standard: DNS exchange: %w", err) 55 | } 56 | 57 | switch dns.RcodeToString[resp.DNS.MsgHdr.Rcode] { 58 | case "BADCOOKIE": 59 | if !resolver.opts.BadCookie { 60 | fmt.Printf(";; BADCOOKIE, retrying.\n\n") 61 | 62 | msg.Extra = resp.DNS.Extra 63 | 64 | resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, resolver.opts.Request.Server) 65 | if err != nil { 66 | return resp, fmt.Errorf("badcookie: DNS exchange: %w", err) 67 | } 68 | } 69 | 70 | case "NOERR": 71 | break 72 | } 73 | 74 | resolver.opts.Logger.Info("Request successful") 75 | 76 | if resp.DNS.MsgHdr.Truncated && !resolver.opts.Truncate { 77 | fmt.Printf(";; Truncated, retrying with TCP\n\n") 78 | 79 | dnsClient.Net = tcp 80 | 81 | switch { 82 | case resolver.opts.IPv4: 83 | dnsClient.Net += "4" 84 | case resolver.opts.IPv6: 85 | dnsClient.Net += "6" 86 | } 87 | 88 | resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, resolver.opts.Request.Server) 89 | } 90 | 91 | if err != nil { 92 | return resp, fmt.Errorf("standard: DNS exchange: %w", err) 93 | } 94 | 95 | return 96 | } 97 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to awl 2 | 3 | First off, thank you! We appreciate your interest in wanting to contribute to awl. 4 | 5 | > If you like the project, spread the word! Help us grow by sharing the project with anyone you thing might be interested. Here are some ways you can help: 6 | > 7 | > - Star the project on GitHub 8 | > - Share the project on social media 9 | > - Tell your friends about the project 10 | 11 | ## How to contribute 12 | 13 | If you want to contribute to awl, you can do so by: 14 | 15 | - [Reporting a bug](#reporting-a-bug) 16 | - [Requesting a feature](#requesting-a-feature) 17 | - [Submitting a pull request](#submitting-a-pull-request) 18 | 19 | ### Reporting a bug 20 | 21 | If you find a bug in awl, please [open an issue](https://git.froth.zone/sam/awl/issues) on the project's issue tracker. When reporting a bug, please include as much information as possible, such as: 22 | 23 | - The version of awl you are using 24 | - The operating system you are using 25 | - The steps to reproduce the bug 26 | - Any error messages you received 27 | 28 | ### Requesting a feature 29 | 30 | If you have an idea for a feature you would like to see in awl, please [open an issue](https://git.froth.zone/sam/awl/issues) on the project's issue tracker. When requesting a feature, please include as much information as possible, such as: 31 | 32 | - A description of the feature 33 | - Why you think the feature would be useful 34 | - Any other relevant information 35 | 36 | ### Submitting a pull request 37 | 38 | If you would like to contribute code to awl, you can do so by submitting a pull request. To submit a pull request, follow these steps: 39 | 40 | 1. Fork the project on Git 41 | 2. Create a new branch for your changes 42 | 3. Make your changes 43 | 4. Push your changes to your fork 44 | 5. [Open a pull request](https://git.froth.zone/sam/awl/pulls) on the project's Git repository 45 | 46 | When submitting a pull request, please include as much information as possible, such as: 47 | 48 | - A description of the changes you made 49 | - Why you made the changes 50 | - Any other relevant information 51 | 52 | Alternatively, you can also contribute by sending an email to the project's [mailing list](https://lists.sr.ht/~sammefishe/awl-devel). For more information about using Git over email, refer to [git-send-email.io](https://git-send-email.io/) 53 | 54 | #### Code Style 55 | 56 | Before submitting a pull request, please run `make lint` to ensure your code adheres to the project's code style. 57 | Make sure that you have `golangci-lint` installed, that is our linter of choice. 58 | -------------------------------------------------------------------------------- /pkg/resolvers/QUIC_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | //go:build !gccgo 3 | 4 | package resolvers_test 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "dns.froth.zone/awl/pkg/query" 11 | "dns.froth.zone/awl/pkg/util" 12 | "github.com/miekg/dns" 13 | "gotest.tools/v3/assert" 14 | ) 15 | 16 | func TestQuic(t *testing.T) { 17 | t.Parallel() 18 | 19 | //nolint:govet // I could not be assed to refactor this, and it is only for tests 20 | tests := []struct { 21 | name string 22 | opts *util.Options 23 | }{ 24 | { 25 | "Valid, AdGuard", 26 | &util.Options{ 27 | QUIC: true, 28 | Logger: util.InitLogger(0), 29 | Request: util.Request{ 30 | Server: "dns.adguard.com", 31 | Type: dns.TypeNS, 32 | Port: 853, 33 | Timeout: 750 * time.Millisecond, 34 | Retries: 3, 35 | }, 36 | }, 37 | }, 38 | { 39 | "Bad domain", 40 | &util.Options{ 41 | QUIC: true, 42 | Logger: util.InitLogger(0), 43 | Request: util.Request{ 44 | Server: "dns.//./,,adguard\a.com", 45 | Port: 853, 46 | Type: dns.TypeA, 47 | Name: "git.froth.zone", 48 | Timeout: 100 * time.Millisecond, 49 | Retries: 0, 50 | }, 51 | }, 52 | }, 53 | { 54 | "Not canonical", 55 | &util.Options{ 56 | QUIC: true, 57 | Logger: util.InitLogger(0), 58 | Request: util.Request{ 59 | Server: "dns.adguard.com", 60 | Port: 853, 61 | Type: dns.TypeA, 62 | Name: "git.froth.zone", 63 | Timeout: 100 * time.Millisecond, 64 | Retries: 0, 65 | }, 66 | }, 67 | }, 68 | { 69 | "Invalid query domain", 70 | &util.Options{ 71 | QUIC: true, 72 | Logger: util.InitLogger(0), 73 | Request: util.Request{ 74 | Server: "example.com", 75 | Port: 853, 76 | Type: dns.TypeA, 77 | Name: "git.froth.zone", 78 | Timeout: 10 * time.Millisecond, 79 | Retries: 0, 80 | }, 81 | }, 82 | }, 83 | } 84 | 85 | for _, test := range tests { 86 | test := test 87 | 88 | t.Run(test.name, func(t *testing.T) { 89 | t.Parallel() 90 | 91 | var ( 92 | res util.Response 93 | err error 94 | ) 95 | 96 | for i := 0; i <= test.opts.Request.Retries; i++ { 97 | res, err = query.CreateQuery(test.opts) 98 | if err == nil { 99 | break 100 | } 101 | } 102 | 103 | if err == nil { 104 | assert.NilError(t, err) 105 | assert.Assert(t, res != util.Response{}) 106 | } else { 107 | assert.Assert(t, res == util.Response{}) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /template.mk: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | # Template for the BSD/GNU makefiles 3 | 4 | HASH ?= `git describe --tags --always --dirty --broken | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || echo "UNKNOWN"` 5 | 6 | SOURCES ?= $(shell find . -name "*.go" -type f ! -name '*_test*') 7 | TEST_SOURCES ?= $(shell find . -name "*_test.go" -type f) 8 | 9 | CGO_ENABLED ?= 0 10 | GO ?= go 11 | TEST ?= $(GO) test -race -cover 12 | COVER ?= $(GO) tool cover 13 | GOFLAGS ?= -trimpath -ldflags="-s -w -X=main.version=$(HASH)" 14 | DESTDIR := 15 | 16 | PREFIX ?= /usr/local 17 | BIN ?= bin 18 | SHARE ?= share 19 | 20 | SCDOC ?= scdoc 21 | MAN ?= $(PREFIX)/$(SHARE)/man 22 | 23 | PROG ?= awl 24 | 25 | # hehe 26 | all: $(PROG) docs/$(PROG).1 27 | 28 | $(PROG): $(SOURCES) 29 | $(GO) build -o $(EXE) $(GOFLAGS) . 30 | 31 | docs/$(PROG).1: docs/$(PROG).1.scd 32 | $(SCDOC) <$? >$@ 33 | 34 | docs/wiki/$(PROG).1.md: docs/$(PROG).1 35 | pandoc --from man --to gfm -o $@ $? 36 | 37 | ## update_doc: update documentation (requires pandoc) 38 | update_doc: docs/wiki/$(PROG).1.md 39 | 40 | .PHONY: fmt 41 | fmt: 42 | gofmt -w -s . 43 | 44 | .PHONY: vet 45 | vet: 46 | $(GO) vet ./... 47 | 48 | ## lint: lint awl, using fmt, vet and golangci-lint 49 | .PHONY: lint 50 | lint: fmt vet 51 | golangci-lint run --fix 52 | 53 | coverage/coverage.out: $(TEST_SOURCES) 54 | $(TEST) -coverprofile=$@ ./... 55 | 56 | .PHONY: test 57 | ## test: run go test 58 | test: coverage/coverage.out 59 | 60 | .PHONY: test-ci 61 | test-ci: 62 | $(TEST) ./... 63 | 64 | ## fuzz: runs fuzz tests 65 | fuzz: $(TEST_SOURCES) 66 | $(TEST) -fuzz=FuzzFlags -fuzztime 10000x ./cmd 67 | $(TEST) -fuzz=FuzzDig -fuzztime 10000x ./cmd 68 | $(TEST) -fuzz=FuzzParseArgs -fuzztime 10000x ./cmd 69 | 70 | fuzz-ci: $(TEST_SOURCES) 71 | $(TEST) -fuzz=FuzzFlags -fuzztime 1000x ./cmd 72 | $(TEST) -fuzz=FuzzDig -fuzztime 1000x ./cmd 73 | $(TEST) -fuzz=FuzzParseArgs -fuzztime 1000x ./cmd 74 | 75 | .PHONY: full_test 76 | full_test: test fuzz 77 | 78 | coverage/cover.html: coverage/coverage.out 79 | $(COVER) -func=$? 80 | $(COVER) -html=$? -o $@ 81 | 82 | ## cover: generates test coverage, output as HTML 83 | cover: coverage/cover.html 84 | 85 | ## clean: clean the build files 86 | .PHONY: clean 87 | clean: 88 | $(GO) clean 89 | # Ignore errors if you remove something that doesn't exist 90 | rm -f docs/awl.1{,.gz} 91 | rm -f docs/CONTRIBUTING.md.gz 92 | rm -f README.md.gz 93 | rm -f coverage/cover* 94 | rm -rf dist 95 | rm -rf vendor 96 | 97 | ## help: Prints this help message 98 | .PHONY: help 99 | help: 100 | @echo "Usage: " 101 | @sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /' 102 | -------------------------------------------------------------------------------- /pkg/resolvers/HTTPS.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package resolvers 4 | 5 | import ( 6 | "bytes" 7 | "crypto/tls" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "time" 12 | 13 | "dns.froth.zone/awl/pkg/util" 14 | "github.com/miekg/dns" 15 | ) 16 | 17 | // HTTPSResolver is for DNS-over-HTTPS queries. 18 | type HTTPSResolver struct { 19 | opts *util.Options 20 | client http.Client 21 | } 22 | 23 | var _ Resolver = (*HTTPSResolver)(nil) 24 | 25 | // LookUp performs a DNS query. 26 | func (resolver *HTTPSResolver) LookUp(msg *dns.Msg) (resp util.Response, err error) { 27 | resolver.client = http.Client{ 28 | Timeout: resolver.opts.Request.Timeout, 29 | Transport: &http.Transport{ 30 | MaxConnsPerHost: 1, 31 | MaxIdleConns: 1, 32 | MaxIdleConnsPerHost: 1, 33 | Proxy: http.ProxyFromEnvironment, 34 | TLSClientConfig: &tls.Config{ 35 | //nolint:gosec // This is intentional if the user requests it 36 | InsecureSkipVerify: resolver.opts.TLSNoVerify, 37 | ServerName: resolver.opts.TLSHost, 38 | }, 39 | }, 40 | } 41 | 42 | buf, err := msg.Pack() 43 | if err != nil { 44 | return resp, fmt.Errorf("doh: packing: %w", err) 45 | } 46 | 47 | resolver.opts.Logger.Debug("https: sending HTTPS request") 48 | 49 | var method string 50 | if resolver.opts.HTTPSOptions.Get { 51 | method = "GET" 52 | } else { 53 | method = "POST" 54 | } 55 | 56 | req, err := http.NewRequest(method, resolver.opts.Request.Server, bytes.NewBuffer(buf)) 57 | if err != nil { 58 | return resp, fmt.Errorf("doh: request creation: %w", err) 59 | } 60 | 61 | req.Header.Set("Content-Type", "application/dns-message") 62 | req.Header.Set("Accept", "application/dns-message") 63 | 64 | now := time.Now() 65 | res, err := resolver.client.Do(req) 66 | resp.RTT = time.Since(now) 67 | 68 | if err != nil { 69 | // overwrite RTT or else tests will fail 70 | resp.RTT = 0 71 | 72 | return resp, fmt.Errorf("doh: HTTP request: %w", err) 73 | } 74 | 75 | if res.StatusCode != http.StatusOK { 76 | // overwrite RTT or else tests will fail 77 | resp.RTT = 0 78 | 79 | return resp, &util.ErrHTTPStatus{Code: res.StatusCode} 80 | } 81 | 82 | resolver.opts.Logger.Debug("https: reading response") 83 | 84 | fullRes, err := io.ReadAll(res.Body) 85 | if err != nil { 86 | return resp, fmt.Errorf("doh: body read: %w", err) 87 | } 88 | 89 | err = res.Body.Close() 90 | if err != nil { 91 | return resp, fmt.Errorf("doh: body close: %w", err) 92 | } 93 | 94 | resolver.opts.Logger.Debug("https: unpacking response") 95 | 96 | resp.DNS = &dns.Msg{} 97 | 98 | err = resp.DNS.Unpack(fullRes) 99 | if err != nil { 100 | return resp, fmt.Errorf("doh: dns message unpack: %w", err) 101 | } 102 | 103 | return resp, nil 104 | } 105 | -------------------------------------------------------------------------------- /pkg/resolvers/general_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package resolvers_test 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "dns.froth.zone/awl/pkg/query" 12 | "dns.froth.zone/awl/pkg/util" 13 | "github.com/ameshkov/dnscrypt/v2" 14 | "github.com/miekg/dns" 15 | "gotest.tools/v3/assert" 16 | ) 17 | 18 | func TestResolve(t *testing.T) { 19 | t.Parallel() 20 | 21 | //nolint:govet // I could not be assed to refactor this, and it is only for tests 22 | tests := []struct { 23 | name string 24 | opts *util.Options 25 | }{ 26 | { 27 | "UDP", 28 | &util.Options{ 29 | Logger: util.InitLogger(0), 30 | Request: util.Request{ 31 | Server: "8.8.4.4", 32 | Port: 53, 33 | Type: dns.TypeAAAA, 34 | Name: "example.com.", 35 | Retries: 3, 36 | }, 37 | }, 38 | }, 39 | { 40 | "UDP (Bad Cookie)", 41 | &util.Options{ 42 | Logger: util.InitLogger(0), 43 | BadCookie: false, 44 | Request: util.Request{ 45 | Server: "b.root-servers.net", 46 | Port: 53, 47 | Type: dns.TypeNS, 48 | Name: "example.com.", 49 | Retries: 3, 50 | }, 51 | EDNS: util.EDNS{ 52 | EnableEDNS: true, 53 | Cookie: true, 54 | }, 55 | }, 56 | }, 57 | { 58 | "UDP (Truncated)", 59 | &util.Options{ 60 | Logger: util.InitLogger(0), 61 | IPv4: true, 62 | Request: util.Request{ 63 | Server: "madns.binarystar.systems", 64 | Port: 5301, 65 | Type: dns.TypeTXT, 66 | Name: "limit.txt.example.", 67 | Retries: 3, 68 | }, 69 | }, 70 | }, 71 | { 72 | "TCP", 73 | &util.Options{ 74 | Logger: util.InitLogger(0), 75 | TCP: true, 76 | 77 | Request: util.Request{ 78 | Server: "8.8.4.4", 79 | Port: 53, 80 | Type: dns.TypeA, 81 | Name: "example.com.", 82 | Retries: 3, 83 | }, 84 | }, 85 | }, 86 | { 87 | "TLS", 88 | &util.Options{ 89 | Logger: util.InitLogger(0), 90 | TLS: true, 91 | Request: util.Request{ 92 | Server: "dns.google", 93 | Port: 853, 94 | Type: dns.TypeAAAA, 95 | Name: "example.com.", 96 | Retries: 3, 97 | }, 98 | }, 99 | }, 100 | { 101 | "Timeout", 102 | &util.Options{ 103 | Logger: util.InitLogger(0), 104 | Request: util.Request{ 105 | Server: "8.8.4.1", 106 | Port: 1, 107 | Type: dns.TypeA, 108 | Name: "example.com.", 109 | Timeout: time.Millisecond * 100, 110 | Retries: 0, 111 | }, 112 | }, 113 | }, 114 | } 115 | 116 | for _, test := range tests { 117 | test := test 118 | 119 | t.Run(test.name, func(t *testing.T) { 120 | t.Parallel() 121 | 122 | var ( 123 | res util.Response 124 | err error 125 | ) 126 | for i := 0; i <= test.opts.Request.Retries; i++ { 127 | res, err = query.CreateQuery(test.opts) 128 | if err == nil || errors.Is(err, dnscrypt.ErrInvalidDNSStamp) { 129 | break 130 | } 131 | } 132 | 133 | if err == nil { 134 | assert.NilError(t, err) 135 | assert.Assert(t, res != util.Response{}) 136 | } else { 137 | assert.ErrorIs(t, err, os.ErrDeadlineExceeded) 138 | } 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "math/rand" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | cli "dns.froth.zone/awl/cmd" 14 | "dns.froth.zone/awl/pkg/query" 15 | "dns.froth.zone/awl/pkg/util" 16 | "github.com/miekg/dns" 17 | ) 18 | 19 | var version = "DEV" 20 | 21 | func main() { 22 | if opts, code, err := run(os.Args); err != nil { 23 | // TODO: Make not ew 24 | if errors.Is(err, util.ErrNotError) || strings.Contains(err.Error(), "help requested") { 25 | os.Exit(0) 26 | } else { 27 | opts.Logger.Error(err) 28 | os.Exit(code) 29 | } 30 | } 31 | } 32 | 33 | func run(args []string) (opts *util.Options, code int, err error) { 34 | //nolint:gosec //Secure source not needed 35 | r := rand.New(rand.NewSource(time.Now().Unix())) 36 | 37 | opts, err = cli.ParseCLI(args, version) 38 | if err != nil { 39 | return opts, 1, fmt.Errorf("parse: %w", err) 40 | } 41 | 42 | var ( 43 | resp util.Response 44 | keepTracing bool 45 | tempDomain string 46 | tempQueryType uint16 47 | ) 48 | 49 | for ok := true; ok; ok = keepTracing { 50 | if opts.Trace { 51 | if keepTracing { 52 | opts.Request.Name = tempDomain 53 | opts.Request.Type = tempQueryType 54 | } else { 55 | tempDomain = opts.Request.Name 56 | tempQueryType = opts.Request.Type 57 | 58 | // Override the query because it needs to be done 59 | opts.Request.Name = "." 60 | opts.Request.Type = dns.TypeNS 61 | } 62 | } 63 | // Retry queries if a query fails 64 | for i := 0; i <= opts.Request.Retries; i++ { 65 | resp, err = query.CreateQuery(opts) 66 | if err == nil { 67 | keepTracing = opts.Trace && (!resp.DNS.Authoritative || (opts.Request.Name == "." && tempDomain != ".")) && resp.DNS.MsgHdr.Rcode == 0 68 | 69 | break 70 | } else if i != opts.Request.Retries { 71 | opts.Logger.Warn("Retrying request, error:", err) 72 | } 73 | } 74 | 75 | // Query failed, make it fail 76 | if err != nil { 77 | return opts, 9, fmt.Errorf("query: %w", err) 78 | } 79 | 80 | var str string 81 | if opts.JSON || opts.XML || opts.YAML { 82 | str, err = query.PrintSpecial(resp, opts) 83 | if err != nil { 84 | return opts, 10, fmt.Errorf("format print: %w", err) 85 | } 86 | } else { 87 | str, err = query.ToString(resp, opts) 88 | if err != nil { 89 | return opts, 15, fmt.Errorf("standard print: %w", err) 90 | } 91 | } 92 | 93 | fmt.Println(str) 94 | 95 | if keepTracing { 96 | var records []dns.RR 97 | 98 | if opts.Request.Name == "." { 99 | records = resp.DNS.Answer 100 | } else { 101 | records = resp.DNS.Ns 102 | } 103 | 104 | want := func(rr dns.RR) bool { 105 | temp := strings.Split(rr.String(), "\t") 106 | 107 | return temp[len(temp)-2] == "NS" 108 | } 109 | 110 | i := 0 111 | 112 | for _, x := range records { 113 | if want(x) { 114 | records[i] = x 115 | i++ 116 | } 117 | } 118 | 119 | records = records[:i] 120 | randomRR := records[r.Intn(len(records))] 121 | 122 | v := strings.Split(randomRR.String(), "\t") 123 | opts.Request.Server = strings.TrimSuffix(v[len(v)-1], ".") 124 | 125 | opts.TLS = false 126 | opts.HTTPS = false 127 | opts.QUIC = false 128 | 129 | opts.RD = false 130 | opts.Request.Port = 53 131 | } 132 | } 133 | 134 | return opts, 0, nil 135 | } 136 | -------------------------------------------------------------------------------- /pkg/resolvers/QUIC.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package resolvers 4 | 5 | import ( 6 | "context" 7 | "crypto/tls" 8 | "encoding/binary" 9 | "fmt" 10 | "io" 11 | "strings" 12 | "time" 13 | 14 | "dns.froth.zone/awl/pkg/util" 15 | "github.com/miekg/dns" 16 | "github.com/quic-go/quic-go" 17 | ) 18 | 19 | // QUICResolver is for DNS-over-QUIC queries. 20 | type QUICResolver struct { 21 | opts *util.Options 22 | } 23 | 24 | var _ Resolver = (*QUICResolver)(nil) 25 | 26 | // LookUp performs a DNS query. 27 | func (resolver *QUICResolver) LookUp(msg *dns.Msg) (resp util.Response, err error) { 28 | tls := &tls.Config{ 29 | //nolint:gosec // This is intentional if the user requests it 30 | InsecureSkipVerify: resolver.opts.TLSNoVerify, 31 | ServerName: resolver.opts.TLSHost, 32 | MinVersion: tls.VersionTLS12, 33 | NextProtos: []string{"doq"}, 34 | } 35 | 36 | // Make sure that TLSHost is ALWAYS set 37 | if resolver.opts.TLSHost == "" { 38 | tls.ServerName = strings.Split(resolver.opts.Request.Server, ":")[0] 39 | } 40 | 41 | conf := new(quic.Config) 42 | conf.HandshakeIdleTimeout = resolver.opts.Request.Timeout 43 | 44 | resolver.opts.Logger.Debug("quic: making query") 45 | 46 | ctx, cancel := context.WithTimeout(context.Background(), resolver.opts.Request.Timeout) 47 | defer cancel() 48 | 49 | connection, err := quic.DialAddr(ctx, resolver.opts.Request.Server, tls, conf) 50 | if err != nil { 51 | return resp, fmt.Errorf("doq: dial: %w", err) 52 | } 53 | 54 | resolver.opts.Logger.Debug("quic: packing query") 55 | 56 | msg.Id = 0 57 | // Compress request to over-the-wire 58 | buf, err := msg.Pack() 59 | if err != nil { 60 | return resp, fmt.Errorf("doq: pack: %w", err) 61 | } 62 | 63 | t := time.Now() 64 | 65 | resolver.opts.Logger.Debug("quic: creating stream") 66 | 67 | stream, err := connection.OpenStream() 68 | if err != nil { 69 | return resp, fmt.Errorf("doq: quic stream creation: %w", err) 70 | } 71 | 72 | resolver.opts.Logger.Debug("quic: writing to stream") 73 | 74 | _, err = stream.Write(rfc9250prefix(buf)) 75 | if err != nil { 76 | return resp, fmt.Errorf("doq: quic stream write: %w", err) 77 | } 78 | 79 | err = stream.Close() 80 | if err != nil { 81 | return resp, fmt.Errorf("doq: quic stream close: %w", err) 82 | } 83 | 84 | resolver.opts.Logger.Debug("quic: reading stream") 85 | 86 | fullRes, err := io.ReadAll(stream) 87 | if err != nil { 88 | return resp, fmt.Errorf("doq: quic stream read: %w", err) 89 | } 90 | 91 | resp.RTT = time.Since(t) 92 | 93 | resolver.opts.Logger.Debug("quic: closing connection") 94 | // Close with error: no error 95 | err = connection.CloseWithError(0, "") 96 | if err != nil { 97 | return resp, fmt.Errorf("doq: quic connection close: %w", err) 98 | } 99 | 100 | resolver.opts.Logger.Debug("quic: closing stream") 101 | 102 | resp.DNS = &dns.Msg{} 103 | 104 | resolver.opts.Logger.Debug("quic: unpacking response") 105 | 106 | // Unpack response and lop off the first two bytes (RFC 9250 moment) 107 | err = resp.DNS.Unpack(fullRes[2:]) 108 | if err != nil { 109 | return resp, fmt.Errorf("doq: unpack: %w", err) 110 | } 111 | 112 | return 113 | } 114 | 115 | // rfc9250prefix adds a two-byte prefix to the input data as per RFC 9250. 116 | func rfc9250prefix(in []byte) []byte { 117 | out := make([]byte, 2+len(in)) 118 | binary.BigEndian.PutUint16(out, uint16(len(in))) 119 | copy(out[2:], in) 120 | return out 121 | } 122 | -------------------------------------------------------------------------------- /pkg/query/query_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package query_test 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "dns.froth.zone/awl/pkg/query" 10 | "dns.froth.zone/awl/pkg/util" 11 | "github.com/miekg/dns" 12 | "gotest.tools/v3/assert" 13 | ) 14 | 15 | func TestCreateQ(t *testing.T) { 16 | t.Parallel() 17 | 18 | //nolint:govet // I could not be assed to refactor this, and it is only for tests 19 | tests := []struct { 20 | name string 21 | opts *util.Options 22 | }{ 23 | { 24 | "1", 25 | &util.Options{ 26 | Logger: util.InitLogger(0), 27 | HeaderFlags: util.HeaderFlags{ 28 | Z: true, 29 | }, 30 | YAML: true, 31 | Request: util.Request{ 32 | Server: "8.8.4.4", 33 | Port: 53, 34 | Type: dns.TypeA, 35 | Name: "example.com.", 36 | Retries: 3, 37 | }, 38 | Display: util.Display{ 39 | Comments: true, 40 | Question: true, 41 | Opt: true, 42 | Answer: true, 43 | Authority: true, 44 | Additional: true, 45 | Statistics: true, 46 | ShowQuery: true, 47 | }, 48 | EDNS: util.EDNS{ 49 | ZFlag: 1, 50 | BufSize: 1500, 51 | EnableEDNS: true, 52 | Cookie: true, 53 | DNSSEC: true, 54 | Expire: true, 55 | KeepOpen: true, 56 | Nsid: true, 57 | Padding: true, 58 | Version: 0, 59 | }, 60 | }, 61 | }, 62 | { 63 | "2", 64 | &util.Options{ 65 | Logger: util.InitLogger(0), 66 | HeaderFlags: util.HeaderFlags{ 67 | Z: true, 68 | }, 69 | XML: true, 70 | 71 | Request: util.Request{ 72 | Server: "8.8.4.4", 73 | Port: 53, 74 | Type: dns.TypeA, 75 | Name: "example.com.", 76 | Retries: 3, 77 | }, 78 | Display: util.Display{ 79 | Comments: true, 80 | Question: true, 81 | Opt: true, 82 | Answer: true, 83 | Authority: true, 84 | Additional: true, 85 | Statistics: true, 86 | UcodeTranslate: true, 87 | ShowQuery: true, 88 | }, 89 | }, 90 | }, 91 | { 92 | "3", 93 | &util.Options{ 94 | Logger: util.InitLogger(0), 95 | JSON: true, 96 | QUIC: true, 97 | 98 | Request: util.Request{ 99 | Server: "dns.adguard.com", 100 | Port: 853, 101 | Type: dns.TypeA, 102 | Name: "example.com.", 103 | Retries: 3, 104 | Timeout: time.Second, 105 | }, 106 | Display: util.Display{ 107 | Comments: true, 108 | Question: true, 109 | Opt: true, 110 | Answer: true, 111 | Authority: true, 112 | Additional: true, 113 | Statistics: true, 114 | ShowQuery: true, 115 | }, 116 | EDNS: util.EDNS{ 117 | EnableEDNS: true, 118 | DNSSEC: true, 119 | Cookie: true, 120 | Expire: true, 121 | Nsid: true, 122 | }, 123 | }, 124 | }, 125 | } 126 | 127 | for _, test := range tests { 128 | test := test 129 | 130 | t.Run(test.name, func(t *testing.T) { 131 | t.Parallel() 132 | 133 | var ( 134 | res util.Response 135 | err error 136 | ) 137 | for i := 0; i <= test.opts.Request.Retries; i++ { 138 | res, err = query.CreateQuery(test.opts) 139 | if err == nil { 140 | break 141 | } 142 | } 143 | 144 | assert.NilError(t, err) 145 | assert.Assert(t, res != util.Response{}) 146 | 147 | str, err := query.PrintSpecial(res, test.opts) 148 | 149 | assert.NilError(t, err) 150 | assert.Assert(t, str != "") 151 | 152 | str, err = query.ToString(res, test.opts) 153 | assert.NilError(t, err) 154 | assert.Assert(t, str != "") 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /pkg/query/query.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package query 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | 9 | "dns.froth.zone/awl/pkg/resolvers" 10 | "dns.froth.zone/awl/pkg/util" 11 | "github.com/dchest/uniuri" 12 | "github.com/miekg/dns" 13 | ) 14 | 15 | // CreateQuery creates a DNS query from the options given. 16 | // It sets query flags and EDNS flags from the respective options. 17 | func CreateQuery(opts *util.Options) (util.Response, error) { 18 | req := new(dns.Msg) 19 | req.SetQuestion(opts.Request.Name, opts.Request.Type) 20 | req.Question[0].Qclass = opts.Request.Class 21 | 22 | // Set standard flags 23 | req.MsgHdr.Response = opts.QR 24 | req.MsgHdr.Authoritative = opts.AA 25 | req.MsgHdr.Truncated = opts.TC 26 | req.MsgHdr.RecursionDesired = opts.RD 27 | req.MsgHdr.RecursionAvailable = opts.RA 28 | req.MsgHdr.Zero = opts.Z 29 | req.MsgHdr.AuthenticatedData = opts.AD 30 | req.MsgHdr.CheckingDisabled = opts.CD 31 | 32 | // EDNS time :) 33 | if opts.EDNS.EnableEDNS { 34 | edns := new(dns.OPT) 35 | edns.Hdr.Name = "." 36 | edns.Hdr.Rrtype = dns.TypeOPT 37 | 38 | edns.SetVersion(opts.EDNS.Version) 39 | 40 | if opts.EDNS.Cookie { 41 | cookie := new(dns.EDNS0_COOKIE) 42 | cookie.Code = dns.EDNS0COOKIE 43 | cookie.Cookie = uniuri.NewLenChars(16, []byte("1234567890abcdef")) 44 | edns.Option = append(edns.Option, cookie) 45 | 46 | opts.Logger.Info("Setting EDNS cookie to", cookie.Cookie) 47 | } 48 | 49 | if opts.EDNS.Expire { 50 | edns.Option = append(edns.Option, new(dns.EDNS0_EXPIRE)) 51 | 52 | opts.Logger.Info("Setting EDNS Expire option") 53 | } 54 | 55 | if opts.EDNS.KeepOpen { 56 | edns.Option = append(edns.Option, new(dns.EDNS0_TCP_KEEPALIVE)) 57 | 58 | opts.Logger.Info("Setting EDNS TCP Keepalive option") 59 | } 60 | 61 | if opts.EDNS.Nsid { 62 | edns.Option = append(edns.Option, new(dns.EDNS0_NSID)) 63 | 64 | opts.Logger.Info("Setting EDNS NSID option") 65 | } 66 | 67 | if opts.EDNS.Padding { 68 | edns.Option = append(edns.Option, new(dns.EDNS0_PADDING)) 69 | 70 | opts.Logger.Info("Setting EDNS padding") 71 | } 72 | 73 | edns.SetUDPSize(opts.EDNS.BufSize) 74 | 75 | opts.Logger.Info("EDNS UDP buffer set to", opts.EDNS.BufSize) 76 | 77 | edns.SetZ(opts.EDNS.ZFlag) 78 | 79 | opts.Logger.Info("EDNS Z flag set to", opts.EDNS.ZFlag) 80 | 81 | if opts.EDNS.DNSSEC { 82 | edns.SetDo() 83 | 84 | opts.Logger.Info("EDNS DNSSEC OK set") 85 | } 86 | 87 | if opts.EDNS.Subnet.Address != nil { 88 | edns.Option = append(edns.Option, &opts.EDNS.Subnet) 89 | } 90 | 91 | req.Extra = append(req.Extra, edns) 92 | } else if opts.EDNS.DNSSEC { 93 | req.SetEdns0(1232, true) 94 | opts.Logger.Warn("DNSSEC implies EDNS, EDNS enabled") 95 | opts.Logger.Info("DNSSEC enabled, UDP buffer set to 1232") 96 | } 97 | 98 | opts.Logger.Debug(req) 99 | 100 | if !opts.Short { 101 | if opts.Display.ShowQuery { 102 | opts.Logger.Info("Printing constructed query") 103 | 104 | var ( 105 | str string 106 | err error 107 | ) 108 | 109 | if opts.JSON || opts.XML || opts.YAML { 110 | str, err = PrintSpecial(util.Response{DNS: req}, opts) 111 | if err != nil { 112 | return util.Response{}, err 113 | } 114 | } else { 115 | temp := opts.Display.Statistics 116 | opts.Display.Statistics = false 117 | str, err = ToString( 118 | util.Response{ 119 | DNS: req, 120 | RTT: 0, 121 | }, opts) 122 | if err != nil { 123 | return util.Response{}, err 124 | } 125 | 126 | opts.Display.Statistics = temp 127 | str += "\n;; QUERY SIZE: " + strconv.Itoa(req.Len()) + "\n" 128 | } 129 | 130 | fmt.Println(str) 131 | 132 | opts.Display.ShowQuery = false 133 | } 134 | } 135 | 136 | resolver, err := resolvers.LoadResolver(opts) 137 | if err != nil { 138 | return util.Response{}, fmt.Errorf("unable to load resolvers: %w", err) 139 | } 140 | 141 | opts.Logger.Info("Query successfully loaded") 142 | 143 | //nolint:wrapcheck // Error wrapping not needed here 144 | return resolver.LookUp(req) 145 | } 146 | -------------------------------------------------------------------------------- /cmd/cli_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package cli_test 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | cli "dns.froth.zone/awl/cmd" 10 | "dns.froth.zone/awl/pkg/util" 11 | "gotest.tools/v3/assert" 12 | ) 13 | 14 | func TestEmpty(t *testing.T) { 15 | t.Parallel() 16 | 17 | args := []string{"awl", "-4"} 18 | 19 | opts, err := cli.ParseCLI(args, "TEST") 20 | assert.NilError(t, err) 21 | assert.Assert(t, opts.IPv4) 22 | assert.Equal(t, opts.Request.Port, 53) 23 | } 24 | 25 | func TestTLSPort(t *testing.T) { 26 | t.Parallel() 27 | 28 | args := []string{"awl", "-T"} 29 | 30 | opts, err := cli.ParseCLI(args, "TEST") 31 | assert.NilError(t, err) 32 | assert.Equal(t, opts.Request.Port, 853) 33 | } 34 | 35 | func TestValidSubnet(t *testing.T) { 36 | t.Parallel() 37 | 38 | tests := []struct { 39 | args []string 40 | want uint16 41 | }{ 42 | {[]string{"awl", "--subnet", "127.0.0.1/32"}, uint16(1)}, 43 | {[]string{"awl", "--subnet", "0"}, uint16(1)}, 44 | {[]string{"awl", "--subnet", "::/0"}, uint16(2)}, 45 | } 46 | 47 | for _, test := range tests { 48 | test := test 49 | 50 | t.Run(test.args[2], func(t *testing.T) { 51 | t.Parallel() 52 | 53 | opts, err := cli.ParseCLI(test.args, "TEST") 54 | 55 | assert.NilError(t, err) 56 | assert.Equal(t, opts.EDNS.Subnet.Family, test.want) 57 | }) 58 | } 59 | } 60 | 61 | func TestInvalidSubnet(t *testing.T) { 62 | t.Parallel() 63 | 64 | args := []string{"awl", "--subnet", "/"} 65 | 66 | _, err := cli.ParseCLI(args, "TEST") 67 | assert.ErrorContains(t, err, "EDNS subnet") 68 | } 69 | 70 | func TestMBZ(t *testing.T) { 71 | t.Parallel() 72 | 73 | args := []string{"awl", "--zflag", "G"} 74 | 75 | _, err := cli.ParseCLI(args, "TEST") 76 | 77 | assert.ErrorContains(t, err, "EDNS MBZ") 78 | } 79 | 80 | func TestInvalidFlag(t *testing.T) { 81 | t.Parallel() 82 | 83 | args := []string{"awl", "--treebug"} 84 | 85 | _, err := cli.ParseCLI(args, "TEST") 86 | 87 | assert.ErrorContains(t, err, "unknown flag") 88 | } 89 | 90 | func TestInvalidDig(t *testing.T) { 91 | t.Parallel() 92 | 93 | args := []string{"awl", "+a"} 94 | 95 | _, err := cli.ParseCLI(args, "TEST") 96 | 97 | assert.ErrorContains(t, err, "digflags: invalid argument") 98 | } 99 | 100 | func TestVersion(t *testing.T) { 101 | t.Parallel() 102 | 103 | args := []string{"awl", "--version"} 104 | 105 | _, err := cli.ParseCLI(args, "test") 106 | 107 | assert.ErrorIs(t, err, util.ErrNotError) 108 | } 109 | 110 | func TestTimeout(t *testing.T) { 111 | t.Parallel() 112 | 113 | args := [][]string{ 114 | {"awl", "+timeout=0"}, 115 | {"awl", "--timeout", "0"}, 116 | } 117 | for _, test := range args { 118 | test := test 119 | 120 | t.Run(test[1], func(t *testing.T) { 121 | t.Parallel() 122 | 123 | opt, err := cli.ParseCLI(test, "TEST") 124 | 125 | assert.NilError(t, err) 126 | assert.Equal(t, opt.Request.Timeout, time.Second/2) 127 | }) 128 | } 129 | } 130 | 131 | func TestRetries(t *testing.T) { 132 | t.Parallel() 133 | 134 | args := [][]string{ 135 | {"awl", "+retry=-2"}, 136 | {"awl", "+tries=-2"}, 137 | {"awl", "--retries", "-2"}, 138 | } 139 | for _, test := range args { 140 | test := test 141 | 142 | t.Run(test[1], func(t *testing.T) { 143 | t.Parallel() 144 | 145 | opt, err := cli.ParseCLI(test, "TEST") 146 | 147 | assert.NilError(t, err) 148 | assert.Equal(t, opt.Request.Retries, 0) 149 | }) 150 | } 151 | } 152 | 153 | func TestSetHTTPS(t *testing.T) { 154 | t.Parallel() 155 | 156 | args := [][]string{ 157 | {"awl", "-H", "@dns.froth.zone/dns-query"}, 158 | {"awl", "+https", "@dns.froth.zone"}, 159 | } 160 | for _, test := range args { 161 | test := test 162 | 163 | t.Run(test[1], func(t *testing.T) { 164 | t.Parallel() 165 | 166 | opt, err := cli.ParseCLI(test, "TEST") 167 | 168 | assert.NilError(t, err) 169 | assert.Equal(t, opt.Request.Server, "dns.froth.zone") 170 | assert.Equal(t, opt.HTTPSOptions.Endpoint, "/dns-query") 171 | }) 172 | } 173 | } 174 | 175 | func FuzzFlags(f *testing.F) { 176 | testcases := []string{"git.froth.zone", "", "!12345", "google.com.edu.org.fr"} 177 | 178 | for _, tc := range testcases { 179 | f.Add(tc) 180 | } 181 | 182 | f.Fuzz(func(t *testing.T, orig string) { 183 | // Get rid of outputs 184 | 185 | args := []string{"awl", orig} 186 | //nolint:errcheck,gosec // Only make sure the program does not crash 187 | cli.ParseCLI(args, "TEST") 188 | }) 189 | } 190 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AdguardTeam/golibs v0.32.7 h1:3dmGlAVgmvquCCwHsvEl58KKcRAK3z1UnjMnwSIeDH4= 2 | github.com/AdguardTeam/golibs v0.32.7/go.mod h1:bE8KV1zqTzgZjmjFyBJ9f9O5DEKO717r7e57j1HclJA= 3 | github.com/ameshkov/dnscrypt/v2 v2.4.0 h1:if6ZG2cuQmcP2TwSY+D0+8+xbPfoatufGlOQTMNkI9o= 4 | github.com/ameshkov/dnscrypt/v2 v2.4.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI= 5 | github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= 6 | github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g= 11 | github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY= 12 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 13 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 14 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 15 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 16 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 17 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 18 | github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= 19 | github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= 20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= 23 | github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 24 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 25 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 26 | github.com/stefansundin/go-zflag v1.1.1 h1:XabhzWS588bVvV1z1UctSa6i8zHkXc5W9otqtnDSHw8= 27 | github.com/stefansundin/go-zflag v1.1.1/go.mod h1:HXX5rABl1AoTcZ2jw+CqJ7R8irczaLquGNZlFabZooc= 28 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 29 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 30 | go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= 31 | go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= 32 | golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= 33 | golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 34 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= 35 | golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= 36 | golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= 37 | golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= 38 | golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= 39 | golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 40 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 41 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 42 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 43 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 44 | golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= 45 | golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 46 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 47 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 48 | golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= 49 | golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 51 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 52 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 53 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 54 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= 56 | gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 57 | -------------------------------------------------------------------------------- /completions/fish.fish: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD-3-Clause 2 | function __fish_complete_awl 3 | set -l token (commandline -ct) 4 | switch $token 5 | case '+tries=*' '+retry=*' '+time=*' '+bufsize=*' '+edns=*' 6 | printf '%s\n' $token(seq 0 255) 7 | case '-v=*' 8 | printf '%s\n' $token(seq -1 3) 9 | end 10 | end 11 | 12 | complete -c awl -x -a "(__fish_print_hostnames) A AAAA AFSDB APL CAA CDNSKEY CDS CERT CNAME DHCID DLV DNAME DNSKEY DS HIP IPSECKEY KEY KX LOC MX NAPTR NS NSEC NSEC3 NSEC3PARAM PTR RRSIG RP SIG SOA SRV SSHFP TA TKEY TLSA TSIG TXT URI" 13 | complete -c awl -x -a "@(__fish_print_hostnames)" 14 | 15 | complete -f -c awl -s 4 -d 'Use IPv4 query transport only' 16 | complete -f -c awl -s 6 -d 'Use IPv6 query transport only' 17 | complete -c awl -s c -l class -x -a 'IN CH HS QCLASS' -d 'Specify query class' 18 | complete -c awl -s p -l port -x -d 'Specify port number' 19 | complete -c awl -s q -l query -x -a "(__fish_print_hostnames)" -d 'Query domain' 20 | complete -c awl -s t -l qType -x -a 'A AAAA AFSDB APL CAA CDNSKEY CDS CERT CNAME DHCID DLV DNAME DNSKEY DS HIP IPSECKEY KEY KX LOC MX NAPTR NS NSEC NSEC3 NSEC3PARAM PTR RRSIG RP SIG SOA SRV SSHFP TA TKEY TLSA TSIG TXT URI' -d 'Specify query type' 21 | complete -c awl -l timeout -x -d 'Set timeout' 22 | complete -c awl -l retries -x -d 'Set number of query retries' 23 | complete -c awl -l no-edns -x -d 'Disable EDNS' 24 | complete -f -c awl -l tcp -a '+vc +novc +tcp +notcp' -d 'TCP mode' 25 | complete -f -c awl -l dnscrypt -a '+dnscrypt +nodnscrypt' -d 'Use DNSCrypt' 26 | complete -c awl -s T -l tls -a '+tls +notls' -d 'Use DNS-over-TLS' 27 | complete -c awl -s H -l https -a '+https +nohttps' -d 'Use DNS-over-HTTPS' 28 | complete -c awl -s Q -l quic -a '+quic +noquic' -d 'Use DNS-over-QUIC' 29 | 30 | complete -c awl -s j -l json -a '+json +nojson' -d 'Print as JSON' 31 | complete -c awl -s j -l xml -a '+xml +noxml' -d 'Print as XML' 32 | complete -c awl -s j -l yaml -a '+yaml +noyaml' -d 'Print as YAML' 33 | 34 | complete -c awl -s x -l reverse -x -d 'Reverse lookup' 35 | complete -f -c awl -s h -l help -d 'Print help and exit' 36 | complete -f -c awl -s V -l version -d 'Print version and exit' 37 | 38 | 39 | # complete -f -c awl -a '+search +nosearch' -d 'Set whether to use searchlist' 40 | # complete -f -c awl -a '+showsearch +noshowsearch' -d 'Search with intermediate results' 41 | complete -f -c awl -a '+recurse +norecurse' -d 'Recursive mode' 42 | complete -f -c awl -l no-truncate -a '+ignore +noignore' -d 'Dont revert to TCP for TC responses.' 43 | # complete -f -c awl -a '+fail +nofail' -d 'Dont try next server on SERVFAIL' 44 | # complete -f -c awl -a '+besteffort +nobesteffort' -d 'Try to parse even illegal messages' 45 | complete -f -c awl -a '+aaonly +noaaonly' -d 'Set AA flag in query (+[no]aaflag)' 46 | complete -f -c awl -a '+adflag +noadflag' -d 'Set AD flag in query' 47 | complete -f -c awl -a '+cdflag +nocdflag' -d 'Set CD flag in query' 48 | complete -f -c awl -a '+cl +nocl' -d 'Control display of class in records' 49 | # complete -f -c awl -a '+cmd +nocmd' -d 'Control display of command line' 50 | complete -f -c awl -a '+comments +nocomments' -d 'Control display of comment lines' 51 | complete -f -c awl -a '+question +noquestion' -d 'Control display of question' 52 | complete -f -c awl -a '+answer +noanswer' -d 'Control display of answer' 53 | complete -f -c awl -a '+authority +noauthority' -d 'Control display of authority' 54 | complete -f -c awl -a '+additional +noadditional' -d 'Control display of additional' 55 | complete -f -c awl -a '+stats +nostats' -d 'Control display of statistics' 56 | complete -f -c awl -s s -l short -a '+short +noshort' -d 'Disable everything except short form of answer' 57 | complete -f -c awl -a '+ttlid +nottlid' -d 'Control display of ttls in records' 58 | complete -f -c awl -a '+all +noall' -d 'Set or clear all display flags' 59 | complete -f -c awl -a '+qr +noqr' -d 'Print question before sending' 60 | # complete -f -c awl -a '+nssearch +nonssearch' -d 'Search all authoritative nameservers' 61 | complete -f -c awl -a '+identify +noidentify' -d 'ID responders in short answers' 62 | complete -f -c awl -a '+trace +notrace' -d 'Trace delegation down from root' 63 | complete -f -c awl -l dnssec -a '+dnssec +nodnssec +do +nodo' -d 'Request DNSSEC records' 64 | complete -f -c awl -a '+nsid +nonsid' -d 'Request Name Server ID' 65 | # complete -f -c awl -a '+multiline +nomultiline' -d 'Print records in an expanded format' 66 | # complete -f -c awl -a '+onesoa +noonesoa' -d 'AXFR prints only one soa record' 67 | 68 | complete -f -c awl -a '+tries=' -d 'Set number of UDP attempts' 69 | complete -f -c awl -a '+retry=' -d 'Set number of UDP retries' 70 | complete -f -c awl -a '+time=' -d 'Set query timeout' 71 | complete -f -c awl -a '+bufsize=' -d 'Set EDNS0 Max UDP packet size' 72 | complete -f -c awl -a '+ndots=' -d 'Set NDOTS value' 73 | complete -f -c awl -a '+edns=' -d 'Set EDNS version' 74 | 75 | complete -c awl -a '(__fish_complete_awl)' 76 | -------------------------------------------------------------------------------- /pkg/logawl/logger.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package logawl 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "sync/atomic" 9 | "time" 10 | ) 11 | 12 | // New instantiates Logger 13 | // 14 | // Level can be changed to one of the other log levels (ErrorLevel, WarnLevel, InfoLevel, DebugLevel). 15 | func New() *Logger { 16 | return &Logger{ 17 | Out: os.Stderr, 18 | Level: WarnLevel, // Default value is WarnLevel 19 | } 20 | } 21 | 22 | // Println takes any and prints it out to Logger -> Out (io.Writer (default is std.Err)). 23 | func (logger *Logger) Println(level Level, in ...any) { 24 | if atomic.LoadInt32(&logger.isDiscard) != 0 { 25 | return 26 | } 27 | // If verbose is not set --debug etc print _nothing_ 28 | if logger.IsLevel(level) { 29 | switch level { // Goes through log levels and does stuff based on them (currently nothing) 30 | case ErrLevel: 31 | if err := logger.Printer(ErrLevel, fmt.Sprintln(in...)); err != nil { 32 | fmt.Fprintln(logger.Out, "Logger failed: ", err) 33 | } 34 | case WarnLevel: 35 | if err := logger.Printer(WarnLevel, fmt.Sprintln(in...)); err != nil { 36 | fmt.Fprintln(logger.Out, "Logger failed: ", err) 37 | } 38 | case InfoLevel: 39 | if err := logger.Printer(InfoLevel, fmt.Sprintln(in...)); err != nil { 40 | fmt.Fprintln(logger.Out, "Logger failed: ", err) 41 | } 42 | case DebugLevel: 43 | if err := logger.Printer(DebugLevel, fmt.Sprintln(in...)); err != nil { 44 | fmt.Fprintln(logger.Out, "Logger failed: ", err) 45 | } 46 | default: 47 | break 48 | } 49 | } 50 | } 51 | 52 | // FormatHeader formats the log header as such YYYY/MM/DD HH:MM:SS (local time) . 53 | func (logger *Logger) FormatHeader(buf *[]byte, t time.Time, line int, level Level) error { 54 | if lvl, err := logger.UnMarshalLevel(level); err == nil { 55 | // This is ugly but functional 56 | // maybe there can be an append func or something in the future 57 | *buf = append(*buf, lvl...) 58 | year, month, day := t.Date() 59 | 60 | *buf = append(*buf, '[') 61 | formatter(buf, year, 4) 62 | *buf = append(*buf, '/') 63 | formatter(buf, int(month), 2) 64 | *buf = append(*buf, '/') 65 | formatter(buf, day, 2) 66 | *buf = append(*buf, ' ') 67 | hour, min, sec := t.Clock() 68 | formatter(buf, hour, 2) 69 | *buf = append(*buf, ':') 70 | formatter(buf, min, 2) 71 | *buf = append(*buf, ':') 72 | formatter(buf, sec, 2) 73 | *buf = append(*buf, ']') 74 | *buf = append(*buf, ':') 75 | *buf = append(*buf, ' ') 76 | } else { 77 | return errInvalidLevel 78 | } 79 | 80 | return nil 81 | } 82 | 83 | // Printer prints the formatted message directly to stdErr. 84 | func (logger *Logger) Printer(level Level, s string) error { 85 | now := time.Now() 86 | 87 | var line int 88 | 89 | logger.Mu.Lock() 90 | defer logger.Mu.Unlock() 91 | 92 | logger.buf = logger.buf[:0] 93 | 94 | if err := logger.FormatHeader(&logger.buf, now, line, level); err != nil { 95 | return err 96 | } 97 | 98 | logger.buf = append(logger.buf, s...) 99 | 100 | if len(s) == 0 || s[len(s)-1] != '\n' { 101 | logger.buf = append(logger.buf, '\n') 102 | } 103 | 104 | _, err := logger.Out.Write(logger.buf) 105 | if err != nil { 106 | return fmt.Errorf("logger printing: %w", err) 107 | } 108 | 109 | return nil 110 | } 111 | 112 | // Some line formatting stuff from Golang log stdlib file 113 | // 114 | // Please view 115 | // https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/log/log.go;drc=41e1d9075e428c2fc32d966b3752a3029b620e2c;l=96 116 | // 117 | // Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding. 118 | func formatter(buf *[]byte, i int, wid int) { 119 | // Assemble decimal in reverse order. 120 | var b [20]byte 121 | bp := len(b) - 1 122 | 123 | for i >= 10 || wid > 1 { 124 | wid-- 125 | 126 | q := i / 10 127 | b[bp] = byte('0' + i - q*10) 128 | bp-- 129 | 130 | i = q 131 | } 132 | // i < 10 133 | b[bp] = byte('0' + i) 134 | *buf = append(*buf, b[bp:]...) 135 | } 136 | 137 | // Debug calls print directly with Debug level. 138 | func (logger *Logger) Debug(in ...any) { 139 | logger.Println(DebugLevel, in...) 140 | } 141 | 142 | // Debugf calls print after formatting the string with Debug level. 143 | func (logger *Logger) Debugf(format string, in ...any) { 144 | logger.Println(DebugLevel, fmt.Sprintf(format, in...)) 145 | } 146 | 147 | // Info calls print directly with Info level. 148 | func (logger *Logger) Info(in ...any) { 149 | logger.Println(InfoLevel, in...) 150 | } 151 | 152 | // Infof calls print after formatting the string with Info level. 153 | func (logger *Logger) Infof(format string, in ...any) { 154 | logger.Println(InfoLevel, fmt.Sprintf(format, in...)) 155 | } 156 | 157 | // Warn calls print directly with Warn level. 158 | func (logger *Logger) Warn(in ...any) { 159 | logger.Println(WarnLevel, in...) 160 | } 161 | 162 | // Warnf calls print after formatting the string with Warn level. 163 | func (logger *Logger) Warnf(format string, in ...any) { 164 | logger.Println(WarnLevel, fmt.Sprintf(format, in...)) 165 | } 166 | 167 | // Error calls print directly with Error level. 168 | func (logger *Logger) Error(in ...any) { 169 | logger.Println(ErrLevel, in...) 170 | } 171 | 172 | // Errorf calls print after formatting the string with Error level. 173 | func (logger *Logger) Errorf(format string, in ...any) { 174 | logger.Println(ErrLevel, fmt.Sprintf(format, in...)) 175 | } 176 | -------------------------------------------------------------------------------- /pkg/query/print_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package query_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "dns.froth.zone/awl/pkg/query" 9 | "dns.froth.zone/awl/pkg/util" 10 | "github.com/miekg/dns" 11 | "gotest.tools/v3/assert" 12 | ) 13 | 14 | func TestRealPrint(t *testing.T) { 15 | t.Parallel() 16 | 17 | opts := []*util.Options{ 18 | { 19 | Logger: util.InitLogger(0), 20 | 21 | TCP: true, 22 | 23 | HeaderFlags: util.HeaderFlags{ 24 | RD: true, 25 | }, 26 | 27 | JSON: true, 28 | Display: util.Display{ 29 | Comments: true, 30 | Question: true, 31 | Answer: true, 32 | Authority: true, 33 | Additional: true, 34 | Statistics: true, 35 | UcodeTranslate: true, 36 | TTL: true, 37 | HumanTTL: true, 38 | ShowQuery: true, 39 | }, 40 | Request: util.Request{ 41 | Server: "a.gtld-servers.net", 42 | Port: 53, 43 | Type: dns.StringToType["NS"], 44 | Class: 1, 45 | Name: "google.com.", 46 | Retries: 3, 47 | }, 48 | EDNS: util.EDNS{ 49 | EnableEDNS: false, 50 | }, 51 | }, 52 | { 53 | Logger: util.InitLogger(0), 54 | 55 | TCP: true, 56 | HeaderFlags: util.HeaderFlags{ 57 | RD: true, 58 | }, 59 | Verbosity: 0, 60 | 61 | Short: true, 62 | Identify: true, 63 | YAML: false, 64 | Display: util.Display{ 65 | Comments: true, 66 | Question: true, 67 | Answer: true, 68 | Authority: true, 69 | Additional: true, 70 | Statistics: true, 71 | UcodeTranslate: true, 72 | TTL: true, 73 | ShowQuery: true, 74 | }, 75 | Request: util.Request{ 76 | Server: "ns1.google.com", 77 | Port: 53, 78 | Type: dns.StringToType["NS"], 79 | Class: 1, 80 | Name: "google.com.", 81 | Retries: 3, 82 | }, 83 | EDNS: util.EDNS{ 84 | EnableEDNS: false, 85 | }, 86 | }, 87 | { 88 | Logger: util.InitLogger(0), 89 | HTTPS: true, 90 | HeaderFlags: util.HeaderFlags{ 91 | RD: true, 92 | }, 93 | Identify: true, 94 | XML: true, 95 | Display: util.Display{ 96 | Comments: true, 97 | Question: true, 98 | Answer: true, 99 | Authority: true, 100 | Additional: true, 101 | Statistics: true, 102 | UcodeTranslate: true, 103 | TTL: true, 104 | HumanTTL: true, 105 | ShowQuery: true, 106 | }, 107 | Request: util.Request{ 108 | Server: "https://dns.google/dns-query", 109 | Port: 443, 110 | Type: dns.StringToType["NS"], 111 | Class: 1, 112 | Name: "freecumextremist.com.", 113 | Retries: 3, 114 | }, 115 | EDNS: util.EDNS{ 116 | EnableEDNS: false, 117 | DNSSEC: true, 118 | }, 119 | }, 120 | { 121 | Logger: util.InitLogger(0), 122 | TLS: true, 123 | HeaderFlags: util.HeaderFlags{ 124 | RD: true, 125 | }, 126 | Verbosity: 0, 127 | Display: util.Display{ 128 | Comments: true, 129 | Question: true, 130 | Answer: true, 131 | Authority: true, 132 | Additional: true, 133 | Statistics: true, 134 | UcodeTranslate: true, 135 | TTL: false, 136 | ShowQuery: true, 137 | }, 138 | Request: util.Request{ 139 | Server: "dns.google", 140 | Port: 853, 141 | Type: dns.StringToType["NS"], 142 | Class: 1, 143 | Name: "freecumextremist.com.", 144 | Retries: 3, 145 | }, 146 | }, 147 | { 148 | Logger: util.InitLogger(0), 149 | TCP: true, 150 | 151 | HeaderFlags: util.HeaderFlags{ 152 | AA: true, 153 | RD: true, 154 | }, 155 | Verbosity: 0, 156 | 157 | YAML: true, 158 | Display: util.Display{ 159 | Comments: true, 160 | Question: true, 161 | Answer: true, 162 | Authority: true, 163 | Additional: true, 164 | Statistics: true, 165 | UcodeTranslate: false, 166 | TTL: true, 167 | ShowQuery: true, 168 | }, 169 | Request: util.Request{ 170 | Server: "rin.froth.zone", 171 | Port: 53, 172 | Type: dns.StringToType["A"], 173 | Class: 1, 174 | Name: "froth.zone.", 175 | Retries: 3, 176 | }, 177 | EDNS: util.EDNS{ 178 | EnableEDNS: true, 179 | Cookie: true, 180 | Padding: true, 181 | }, 182 | }, 183 | } 184 | 185 | for _, test := range opts { 186 | test := test 187 | 188 | t.Run("", func(t *testing.T) { 189 | t.Parallel() 190 | 191 | var ( 192 | res util.Response 193 | err error 194 | ) 195 | for i := 0; i <= test.Request.Retries; i++ { 196 | res, err = query.CreateQuery(test) 197 | if err == nil { 198 | break 199 | } 200 | } 201 | assert.NilError(t, err) 202 | 203 | if test.JSON || test.XML || test.YAML { 204 | str := "" 205 | str, err = query.PrintSpecial(res, test) 206 | assert.NilError(t, err) 207 | assert.Assert(t, str != "") 208 | } 209 | str, err := query.ToString(res, test) 210 | assert.NilError(t, err) 211 | assert.Assert(t, str != "") 212 | }) 213 | } 214 | } 215 | 216 | func TestBadFormat(t *testing.T) { 217 | t.Parallel() 218 | 219 | _, err := query.PrintSpecial(util.Response{DNS: new(dns.Msg)}, new(util.Options)) 220 | assert.ErrorContains(t, err, "never happen") 221 | } 222 | 223 | func TestEmpty(t *testing.T) { 224 | t.Parallel() 225 | 226 | str, err := query.ToString(util.Response{}, new(util.Options)) 227 | 228 | assert.Error(t, err, "no message") 229 | assert.Assert(t, str == " MsgHdr") 230 | } 231 | -------------------------------------------------------------------------------- /completions/zsh.zsh: -------------------------------------------------------------------------------- 1 | #compdef awl 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | local curcontext="$curcontext" state line expl 5 | local -a alts args 6 | [[ -prefix + ]] && args=( 7 | '*+'{no,}'tcp[use TCP instead of UDP for queries]' 8 | '*+'{no,}'ignore[ignore truncation in UDP responses]' 9 | '*+'{no,}'tls[use DNS-over-TLS for queries]' 10 | '*+'{no,}'dnscrypt[use DNSCrypt for queries]' 11 | '*+'{no,}'https=[use DNS-over-HTTPS for queries]:endpoint [/dns-query]' 12 | '*+'{no,}'quic[use DNS-over-QUIC for queries]' 13 | '*+'{no,}'aaonly[set aa flag in the query]' 14 | '*+'{no,}'additional[print additional section of a reply]' 15 | '*+'{no,}'adflag[set the AD (authentic data) bit in the query]' 16 | '*+'{no,}'badcookie[retry BADCOOKIE responses]' 17 | '*+'{no,}'cdflag[set the CD (checking disabled) bit in the query]' 18 | '*+'{no,}'cookie[add a COOKIE option to the request]' 19 | '*+edns=[specify EDNS version for query]:version (0-255)' 20 | '*+noedns[clear EDNS version to be sent]' 21 | '*+ednsflags=[set EDNS flags bits]:flags' 22 | # '*+ednsopt=[specify EDNS option]:code point' 23 | '*+noedns[clear EDNS options to be sent]' 24 | '*+'{no,}'expire[send an EDNS Expire option]' 25 | # '*+'{no,}'idnin[set processing of IDN domain names on input]' 26 | '*+'{no,}'idnout[set conversion of IDN puny code on output]' 27 | '*+'{no,}'keepalive[request EDNS TCP keepalive]' 28 | '*+'{no,}'keepopen[keep TCP socket open between queries]' 29 | '*+'{no,}'recurse[set the RD (recursion desired) bit in the query]' 30 | # '*+'{no,}'nssearch[search all authoritative nameservers]' 31 | '*+'{no,}'trace[trace delegation down from root]' 32 | # '*+'{no,}'cmd[print initial comment in output]' 33 | '*+'{no,}'short[print terse output]' 34 | '*+'{no,}'identify[print IP and port of responder]' 35 | '*+'{no,}'comments[print comment lines in output]' 36 | '*+'{no,}'stats[print statistics]' 37 | '*+padding[set padding block size]' 38 | '*+'{no,}'qr[print query as it was sent]' 39 | '*+'{no,}'question[print question section of a query]' 40 | '*+'{no,}'raflag[set RA flag in the query]' 41 | '*+'{no,}'answer[print answer section of a reply]' 42 | '*+'{no,}'authority[print authority section of a reply]' 43 | '*+'{no,}'all[set all print/display flags]' 44 | '*+'{no,}'subnet=[send EDNS client subnet option]:addr/prefix-length' 45 | '*+'{no,}'tcflag[set TC flag in the query]' 46 | '*+time=[set query timeout]:timeout (seconds) [1]' 47 | '*+timeout=[set query timeout]:timeout (seconds) [1]' 48 | '*+tries=[specify number of UDP query attempts]:tries [3]' 49 | '*+retry=[specify number of UDP query retries]:retries [2]' 50 | # '*+'{no,}'rrcomments[set display of per-record comments]' 51 | # '*+ndots=[specify number of dots to be considered absolute]:dots' 52 | '*+bufsize=[specify UDP buffer size]:size (bytes)' 53 | '*+'{no,}''{dnssec,do}'[enable DNSSEC]' 54 | '*+'{no,}'nsid[include EDNS name server ID request in query]' 55 | '*+'{no,}'class[display the class whening printing the answer]' 56 | '*+'{no,}'ttlid[display the TTL whening printing the record]' 57 | '*+'{no,}'ttlunits[display the TTL in human-readable units]' 58 | # '*+'{no,}'unknownformat[print RDATA in RFC 3597 "unknown" format]' 59 | '*+'{no,}'json[present the results as JSON]' 60 | '*+'{no,}'xml[present the results as XML]' 61 | '*+'{no,}'yaml[present the results as YAML]' 62 | '*+'{no,}'zflag[set Z flag in query]' 63 | ) 64 | # TODO: Add the regular (POSIX/GNU) flags 65 | _arguments -s -C $args \ 66 | '(- *)-'{h,-help}'[display help information]' \ 67 | '(- *)-'{V,-version}'[display version information]' \ 68 | '-'{v,-verbosity}'=+[set verbosity to custom level]:verbosity:compadd -M "m\:{\-1-3}={\-1-3}" - \-1 0 1 2 3' \ 69 | '-'{v,-verbosity}'+[set verbosity to info]' \ 70 | '*-'{p,-port}'+[specify port number]:port:_ports' \ 71 | '*-'{q,-query}'+[specify host name to query]:host:_hosts' \ 72 | '*-'{c,-class}'+[specify class]:class:compadd -M "m\:{a-z}={A-Z}" - IN CS CH HS' \ 73 | '*-'{t,-qType}'+[specify type]:type:_dns_types' \ 74 | '*-4+[force IPv4 only]' \ 75 | '*-6+[force IPv6 only]' \ 76 | '*-'{x,-reverse}'+[reverse lookup]' \ 77 | '*--timeout+[timeout in seconds]:number [1]' \ 78 | '*--retries+[specify number of query retries]:number [2]' \ 79 | '*--no-edns+[disable EDNS]' \ 80 | '*--edns-ver+[specify EDNS version for query]:version (0-255) [0]' \ 81 | '*-'{D,-dnssec}'+[enable DNSSEC]' \ 82 | '*--expire+[send EDNS expire]' \ 83 | '*-'{n,-nsid}'+[include EDNS name server ID request in query]' \ 84 | '*--no-cookie+[disable sending EDNS cookie]' \ 85 | '*--keep-alive+[request EDNS TCP keepalive]' \ 86 | '*-'{b,-buffer-size}'+[specify UDP buffer size]:size (bytes) [1232]' \ 87 | '*--zflag+[set EDNS z-flag]:decimal, hex or octal [0]' \ 88 | '*--subnet+[set EDNS client subnet]:addr/prefix-length' \ 89 | '*--no-truncate+[ignore truncation in UDP responses]' \ 90 | '*--tcp+[use TCP instead of UDP for queries]' \ 91 | '*--dnscrypt+[use DNSCrypt for queries]' \ 92 | '*-'{T,-tls}'+[use DNS-over-TLS for queries]' \ 93 | '*-'{H,-https}'+[use DNS-over-HTTPS for queries]' \ 94 | '*-'{Q,-quic}'+[use DNS-over-QUIC for queries]' \ 95 | '*--tls-no-verify+[disable TLS verification]' \ 96 | '*--tls-host+[set TLS lookup hostname]:host:_hosts' \ 97 | '*-'{s,-short}'+[print terse output]' \ 98 | '*-'{j,-json}'+[present the results as JSON]' \ 99 | '*-'{X,-xml}'+[present the results as XML]' \ 100 | '*-'{y,-yaml}'+[present the results as YAML]' \ 101 | '*--trace+[trace from the root]' \ 102 | '*: :->args' && ret=0 103 | 104 | if [[ -n $state ]]; then 105 | if compset -P @; then 106 | _wanted hosts expl 'DNS server' _hosts && ret=0; 107 | else 108 | _alternative 'hosts:host:_hosts' 'types:query type:_dns_types' && ret=0 109 | fi 110 | fi 111 | 112 | return ret 113 | -------------------------------------------------------------------------------- /cmd/misc_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package cli_test 4 | 5 | import ( 6 | "strings" 7 | "testing" 8 | 9 | cli "dns.froth.zone/awl/cmd" 10 | "dns.froth.zone/awl/pkg/util" 11 | "github.com/miekg/dns" 12 | "gotest.tools/v3/assert" 13 | ) 14 | 15 | func TestParseArgs(t *testing.T) { 16 | t.Parallel() 17 | 18 | args := []string{ 19 | "go.dev", 20 | "AAAA", 21 | "@1.1.1.1", 22 | "+ignore", 23 | } 24 | opts := new(util.Options) 25 | opts.Logger = util.InitLogger(0) 26 | err := cli.ParseMiscArgs(args, opts) 27 | assert.NilError(t, err) 28 | assert.Equal(t, opts.Request.Name, "go.dev.") 29 | assert.Equal(t, opts.Request.Type, dns.StringToType["AAAA"]) 30 | assert.Equal(t, opts.Request.Server, "1.1.1.1") 31 | assert.Equal(t, opts.Truncate, true) 32 | } 33 | 34 | func TestParseNoInput(t *testing.T) { 35 | t.Parallel() 36 | 37 | args := []string{} 38 | opts := new(util.Options) 39 | opts.Logger = util.InitLogger(0) 40 | err := cli.ParseMiscArgs(args, opts) 41 | assert.NilError(t, err) 42 | assert.Equal(t, opts.Request.Name, ".") 43 | assert.Equal(t, opts.Request.Type, dns.StringToType["NS"]) 44 | } 45 | 46 | func TestParseA(t *testing.T) { 47 | t.Parallel() 48 | 49 | args := []string{ 50 | "golang.org.", 51 | } 52 | opts := new(util.Options) 53 | opts.Logger = util.InitLogger(0) 54 | err := cli.ParseMiscArgs(args, opts) 55 | assert.NilError(t, err) 56 | assert.Equal(t, opts.Request.Name, "golang.org.") 57 | assert.Equal(t, opts.Request.Type, dns.StringToType["A"]) 58 | } 59 | 60 | func TestParsePTR(t *testing.T) { 61 | t.Parallel() 62 | 63 | args := []string{"8.8.8.8"} 64 | opts := new(util.Options) 65 | opts.Logger = util.InitLogger(0) 66 | opts.Reverse = true 67 | err := cli.ParseMiscArgs(args, opts) 68 | assert.NilError(t, err) 69 | assert.Equal(t, opts.Request.Type, dns.StringToType["PTR"]) 70 | } 71 | 72 | func TestParseInvalidPTR(t *testing.T) { 73 | t.Parallel() 74 | 75 | args := []string{"8.88.8"} 76 | opts := new(util.Options) 77 | opts.Logger = util.InitLogger(0) 78 | opts.Reverse = true 79 | err := cli.ParseMiscArgs(args, opts) 80 | assert.ErrorContains(t, err, "unrecognized address") 81 | } 82 | 83 | func TestDefaultServer(t *testing.T) { 84 | t.Parallel() 85 | 86 | tests := []struct { 87 | in string 88 | want string 89 | }{ 90 | {"DNSCrypt", "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"}, 91 | {"TLS", "dns.google"}, 92 | {"HTTPS", "https://dns.cloudflare.com"}, 93 | {"QUIC", "dns.froth.zone"}, 94 | } 95 | 96 | for _, test := range tests { 97 | test := test 98 | 99 | t.Run(test.in, func(t *testing.T) { 100 | t.Parallel() 101 | args := []string{} 102 | opts := new(util.Options) 103 | opts.Logger = util.InitLogger(0) 104 | switch test.in { 105 | case "DNSCrypt": 106 | opts.DNSCrypt = true 107 | case "TLS": 108 | opts.TLS = true 109 | case "HTTPS": 110 | opts.HTTPS = true 111 | case "QUIC": 112 | opts.QUIC = true 113 | } 114 | err := cli.ParseMiscArgs(args, opts) 115 | assert.NilError(t, err) 116 | assert.Equal(t, opts.Request.Server, test.want) 117 | }) 118 | } 119 | } 120 | 121 | func TestFlagSetting(t *testing.T) { 122 | t.Parallel() 123 | 124 | tests := []struct { 125 | in string 126 | expected string 127 | over string 128 | }{ 129 | {"@sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", "DNSCrypt"}, 130 | {"@tls://dns.google", "dns.google", "TLS"}, 131 | {"@https://dns.cloudflare.com/dns-query", "https://dns.cloudflare.com/dns-query", "HTTPS"}, 132 | {"@https://dns.example.net/a", "https://dns.example.net/a", "HTTPS with a set path"}, 133 | {"@quic://dns.adguard.com", "dns.adguard.com", "QUIC"}, 134 | {"@tcp://dns.froth.zone", "dns.froth.zone", "TCP"}, 135 | {"@udp://dns.example.com", "dns.example.com", "UDP"}, 136 | } 137 | 138 | for _, test := range tests { 139 | test := test 140 | 141 | t.Run(test.over, func(t *testing.T) { 142 | t.Parallel() 143 | 144 | opts := new(util.Options) 145 | opts.Logger = util.InitLogger(0) 146 | 147 | err := cli.ParseMiscArgs([]string{test.in}, opts) 148 | assert.NilError(t, err) 149 | switch { 150 | case strings.HasPrefix(test.over, "DNSCrypt"): 151 | assert.Assert(t, opts.DNSCrypt) 152 | assert.Equal(t, opts.Request.Server, test.expected) 153 | case strings.HasPrefix(test.over, "TLS"): 154 | assert.Assert(t, opts.TLS) 155 | assert.Equal(t, opts.Request.Server, test.expected) 156 | case strings.HasPrefix(test.over, "HTTPS"): 157 | assert.Assert(t, opts.HTTPS) 158 | assert.Equal(t, opts.Request.Server, test.expected) 159 | case strings.HasPrefix(test.over, "QUIC"): 160 | assert.Assert(t, opts.QUIC) 161 | assert.Equal(t, opts.Request.Server, test.expected) 162 | case strings.HasPrefix(test.over, "TCP"): 163 | assert.Assert(t, opts.TCP) 164 | assert.Equal(t, opts.Request.Server, test.expected) 165 | case strings.HasPrefix(test.over, "UDP"): 166 | assert.Assert(t, true) 167 | assert.Equal(t, opts.Request.Server, test.expected) 168 | } 169 | }) 170 | } 171 | } 172 | 173 | func FuzzParseArgs(f *testing.F) { 174 | cases := []string{ 175 | "go.dev", 176 | "AAAA", 177 | "@1.1.1.1", 178 | "+ignore", 179 | "e", 180 | } 181 | 182 | for _, tc := range cases { 183 | f.Add(tc) 184 | } 185 | 186 | f.Fuzz(func(t *testing.T, arg string) { 187 | // Get rid of outputs 188 | 189 | args := []string{arg} 190 | opts := new(util.Options) 191 | opts.Logger = util.InitLogger(0) 192 | //nolint:errcheck,gosec // Only make sure the program does not crash 193 | cli.ParseMiscArgs(args, opts) 194 | }) 195 | } 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # awl 3 | 4 | > awl *(noun)*: A pointed tool for making small holes in wood or leather 5 | 6 | A command-line DNS lookup tool that supports DNS queries over UDP, TCP, TLS, HTTPS, DNSCrypt, and QUIC. 7 | 8 | [![Gitea Release](https://img.shields.io/gitea/v/release/sam/awl?gitea_url=https%3A%2F%2Fgit.froth.zone&display_name=release&style=for-the-badge)](https://git.froth.zone/sam/awl) 9 | [![Last Commit](https://img.shields.io/gitea/last-commit/sam/awl?gitea_url=https%3A%2F%2Fgit.froth.zone&style=for-the-badge)](https://git.froth.zone/sam/awl/commits/branch/master) 10 | [![License](https://img.shields.io/github/license/samtherapy/awl?style=for-the-badge)](https://spdx.org/licenses/BSD-3-Clause.html) 11 | [![Go Report](https://goreportcard.com/badge/dns.froth.zone/awl?style=for-the-badge)](https://goreportcard.com/report/dns.froth.zone/awl) 12 | 13 | Awl is designed to be a drop-in replacement for [dig](https://bind9.readthedocs.io/en/v9_18_3/manpages.html#dig-dns-lookup-utility). 14 | 15 | ## Examples 16 | 17 | ```shell 18 | # Query a domain over UDP 19 | awl example.com 20 | 21 | # Query a domain over HTTPS, print only the results 22 | awl example.com +https --short 23 | 24 | # Query a domain over TLS, print as JSON 25 | awl example.com +tls +json 26 | ``` 27 | 28 | For more and the usage, see the [manpage](https://git.froth.zone/sam/awl/wiki/awl.1). 29 | 30 | ## Installing 31 | 32 | On any platform, with [Go](https://go.dev) installed, run the following command to install: 33 | 34 | ```shell 35 | go install dns.froth.zone/awl@latest 36 | ``` 37 | 38 | ### Packaging 39 | 40 | Alternatively, many package managers are supported: 41 | 42 |
43 | Linux 44 | 45 | #### Distro-specific 46 | 47 |
48 | Alpine Linux 49 | 50 | Provided by [Gitea packages](https://git.froth.zone/sam/-/packages/alpine/awl-dns) \ 51 | ***Any distro that uses apk should also work*** 52 | 53 | ```shell 54 | # Add the repository 55 | echo "https://git.froth.zone/api/packages/sam/alpine/edge/main" | tee -a /etc/apk/repositories 56 | # Get the signing key 57 | curl -JO https://git.froth.zone/api/packages/sam/alpine/key --output-dir /etc/apk/keys 58 | # Install 59 | apk add awl-dns 60 | ``` 61 | 62 |
63 | 64 |
65 | Arch 66 | 67 | AUR package available as [awl-dns-git](https://aur.archlinux.org/packages/awl-dns-git/) 68 | 69 | ```shell 70 | yay -S awl-dns-git || 71 | paru -S awl-dns-git 72 | ``` 73 | 74 |
75 | 76 |
77 | Debian / Ubuntu 78 | 79 | Provided by [Gitea packages](https://git.froth.zone/sam/-/packages/debian/awl-dns/) \ 80 | ***Any distro that uses deb/dpkg should also work*** 81 | 82 | ```shell 83 | # Install the repository and GPG keys 84 | curl -JO https://git.froth.zone/packaging/-/packages/debian/git-froth-zone-debian/2-0/files/6209 85 | sudo dpkg -i git-froth-zone-debian_2-0_all.deb 86 | rm git-froth-zone-debian_2-0_all.deb 87 | # Update and install 88 | sudo apt update 89 | sudo apt install awl-dns 90 | ``` 91 | 92 |
93 | 94 |
95 | Fedora / RHEL / SUSE 96 | 97 | Provided by [Gitea packages](https://git.froth.zone/sam/-/packages/rpm/awl-dns/) \ 98 | ***Any distro that uses rpm/dnf might also work, I've never tried it*** 99 | 100 | ```shell 101 | # Add the repository 102 | dnf config-manager --add-repo https://git.froth.zone/api/packages/sam/rpm.repo || 103 | zypper addrepo https://git.froth.zone/api/packages/sam/rpm.repo 104 | # Install 105 | dnf install awl-dns || 106 | zypper install awl-dns 107 | ``` 108 | 109 |
110 | 111 |
112 | Gentoo 113 | 114 | ```shell 115 | # Add the ebuild repository 116 | eselect repository add froth-zone git https://git.froth.zone/packaging/portage.git 117 | emaint sync -r froth-zone 118 | # Install 119 | emerge -av net-dns/awl 120 | ``` 121 | 122 |
123 | 124 | #### Distro-agnostic 125 | 126 | 127 |
128 | Homebrew 129 | 130 | ```shell 131 | brew install SamTherapy/tap/awl 132 | ``` 133 | 134 |
135 |
136 | Snap 137 | 138 | Snap package available as [awl-dns](https://snapcraft.io/awl-dns) 139 | 140 | ```shell 141 | snap install awl-dns || 142 | sudo snap install awl-dns 143 | ``` 144 | 145 |
146 |
147 |
148 |
149 | macOS 150 | 151 |
152 | Homebrew 153 | 154 | ```shell 155 | brew install SamTherapy/tap/awl 156 | ``` 157 | 158 |
159 |
160 |
161 |
162 | Windows 163 | 164 |
165 | Scoop 166 | 167 | ```pwsh 168 | scoop bucket add froth https://git.froth.zone/packages/scoop.git 169 | scoop install awl 170 | ``` 171 | 172 |
173 |
174 | 175 | ## Contributing 176 | 177 | Please see the [CONTRIBUTING.md](./docs/CONTRIBUTING.md) file for more information. 178 | 179 | TL;DR: If you like the project, spread the word! If you want to contribute, [use the issue tracker](https://git.froth.zone/sam/awl/issues) or [open a pull request](https://git.froth.zone/sam/awl/pulls). 180 | Want to use email instead? Use our [mailing list](https://lists.sr.ht/~sammefishe/awl-devel)! 181 | 182 | ### Mirrors 183 | 184 | The canonical repository is located on [my personal Forgejo instance](https://git.froth.zone/sam/awl). \ 185 | Official mirrors are located on [GitHub](https://github.com/SamTherapy/awl), [GitLab](https://gitlab.com/SamTherapy/awl) and [SourceHut](https://git.sr.ht/~sammefishe/awl). 186 | Contributions are accepted on all mirrors, but the Forgejo instance is preferred. 187 | 188 | ## License 189 | 190 | [BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html) 191 | 192 | ### Credits 193 | 194 | - Awl image taken from [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Awl.tif), imaged is licensed CC0. 195 | -------------------------------------------------------------------------------- /cmd/misc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package cli 4 | 5 | import ( 6 | "fmt" 7 | "math/rand" 8 | "strings" 9 | 10 | "dns.froth.zone/awl/conf" 11 | "dns.froth.zone/awl/pkg/util" 12 | "github.com/miekg/dns" 13 | "golang.org/x/net/idna" 14 | ) 15 | 16 | // ParseMiscArgs parses the wildcard arguments, dig style. 17 | // Only one command is supported at a time, so any extra information overrides previous. 18 | func ParseMiscArgs(args []string, opts *util.Options) error { 19 | for _, arg := range args { 20 | r, ok := dns.StringToType[strings.ToUpper(arg)] 21 | 22 | switch { 23 | // If it starts with @, it's a DNS server 24 | case strings.HasPrefix(arg, "@"): 25 | arg = arg[1:] 26 | // Automatically set flags based on URI header 27 | opts.Logger.Info(arg, "detected as a server") 28 | 29 | switch { 30 | case strings.HasPrefix(arg, "tls://"): 31 | opts.TLS = true 32 | opts.Request.Server = strings.TrimPrefix(arg, "tls://") 33 | opts.Logger.Info("DNS-over-TLS implicitly set") 34 | case strings.HasPrefix(arg, "https://"): 35 | opts.HTTPS = true 36 | opts.Request.Server = arg 37 | opts.Logger.Info("DNS-over-HTTPS implicitly set") 38 | 39 | _, endpoint, isSplit := strings.Cut(arg, "/") 40 | if isSplit { 41 | opts.HTTPSOptions.Endpoint = "/" + endpoint 42 | } 43 | case strings.HasPrefix(arg, "quic://"): 44 | opts.QUIC = true 45 | opts.Request.Server = strings.TrimPrefix(arg, "quic://") 46 | opts.Logger.Info("DNS-over-QUIC implicitly set.") 47 | case strings.HasPrefix(arg, "sdns://"): 48 | opts.DNSCrypt = true 49 | opts.Request.Server = arg 50 | opts.Logger.Info("DNSCrypt implicitly set") 51 | case strings.HasPrefix(arg, "tcp://"): 52 | opts.TCP = true 53 | opts.Request.Server = strings.TrimPrefix(arg, "tcp://") 54 | opts.Logger.Info("TCP implicitly set") 55 | case strings.HasPrefix(arg, "udp://"): 56 | opts.Request.Server = strings.TrimPrefix(arg, "udp://") 57 | default: 58 | // Allow HTTPS queries to have a fallback default 59 | if opts.HTTPS { 60 | server, endpoint, isSplit := strings.Cut(arg, "/") 61 | if isSplit { 62 | opts.HTTPSOptions.Endpoint = "/" + endpoint 63 | opts.Request.Server = server 64 | } else { 65 | opts.Request.Server = server 66 | } 67 | } else { 68 | opts.Request.Server = arg 69 | } 70 | } 71 | 72 | // Dig-style +queries 73 | case strings.HasPrefix(arg, "+"): 74 | opts.Logger.Info(arg, "detected as a dig query") 75 | 76 | if err := ParseDig(strings.ToLower(arg[1:]), opts); err != nil { 77 | return err 78 | } 79 | 80 | // Domain names 81 | case strings.Contains(arg, "."): 82 | var err error 83 | 84 | opts.Logger.Info(arg, "detected as a domain name") 85 | opts.Request.Name, err = idna.ToASCII(arg) 86 | if err != nil { 87 | return fmt.Errorf("unicode to punycode: %w", err) 88 | } 89 | 90 | // DNS query type 91 | case ok: 92 | opts.Logger.Info(arg, "detected as a type") 93 | opts.Request.Type = r 94 | 95 | // Domain? 96 | default: 97 | var err error 98 | 99 | opts.Logger.Info(arg, "is unknown. Assuming domain") 100 | opts.Request.Name, err = idna.ToASCII(arg) 101 | if err != nil { 102 | return fmt.Errorf("unicode to punycode: %w", err) 103 | } 104 | } 105 | } 106 | 107 | // If nothing was set, set a default 108 | if opts.Request.Name == "" { 109 | opts.Logger.Info("Domain not specified, making a default") 110 | opts.Request.Name = "." 111 | 112 | if opts.Request.Type == 0 { 113 | opts.Logger.Info("Query not specified, making an \"NS\" query") 114 | opts.Request.Type = dns.StringToType["NS"] 115 | } 116 | } else if opts.Request.Type == 0 { 117 | opts.Logger.Info("Query not specified, making an \"A\" query") 118 | opts.Request.Type = dns.StringToType["A"] 119 | } 120 | 121 | if opts.Request.Server == "" { 122 | opts.Logger.Info("Server not specified, selecting a default") 123 | // Set "defaults" for each if there is no input 124 | switch { 125 | case opts.DNSCrypt: 126 | // This is adguard 127 | opts.Request.Server = "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20" 128 | case opts.TLS: 129 | opts.Request.Server = "dns.google" 130 | case opts.HTTPS: 131 | opts.Request.Server = "https://dns.cloudflare.com" 132 | case opts.QUIC: 133 | opts.Request.Server = "dns.froth.zone" 134 | default: 135 | var err error 136 | resolv, err := conf.GetDNSConfig() 137 | 138 | if err != nil { 139 | // :^) 140 | opts.Logger.Warn("Could not query system for server. Using localhost\n", "Error:", err) 141 | opts.Request.Server = "127.0.0.1" 142 | } else { 143 | // Make sure that if IPv4 or IPv6 is asked for it actually uses it 144 | harmful: 145 | for _, srv := range resolv.Servers { 146 | switch { 147 | case opts.IPv4: 148 | if strings.Contains(srv, ".") { 149 | opts.Request.Server = srv 150 | 151 | break harmful 152 | } 153 | case opts.IPv6: 154 | if strings.Contains(srv, ":") { 155 | opts.Request.Server = srv 156 | 157 | break harmful 158 | } 159 | default: 160 | //#nosec -- This isn't used for anything secure 161 | opts.Request.Server = resolv.Servers[rand.Intn(len(resolv.Servers))] 162 | 163 | break harmful 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | opts.Logger.Info("DNS server set to", opts.Request.Server) 171 | 172 | // Make reverse addresses proper addresses 173 | if opts.Reverse { 174 | var err error 175 | 176 | opts.Logger.Info("Making reverse DNS query proper *.arpa domain") 177 | 178 | if dns.TypeToString[opts.Request.Type] == "A" { 179 | opts.Request.Type = dns.StringToType["PTR"] 180 | } 181 | 182 | opts.Request.Name, err = util.ReverseDNS(opts.Request.Name, opts.Request.Type) 183 | if err != nil { 184 | return fmt.Errorf("reverse DNS: %w", err) 185 | } 186 | } 187 | 188 | // if the domain is not canonical, make it canonical 189 | if !strings.HasSuffix(opts.Request.Name, ".") { 190 | opts.Request.Name = fmt.Sprintf("%s.", opts.Request.Name) 191 | 192 | opts.Logger.Info("Domain made canonical") 193 | } 194 | 195 | return nil 196 | } 197 | -------------------------------------------------------------------------------- /cmd/dig.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package cli 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "dns.froth.zone/awl/pkg/util" 12 | ) 13 | 14 | // ParseDig parses commands from the popular DNS tool dig. 15 | // All dig commands are taken from https://man.openbsd.org/dig.1 as the source of their functionality. 16 | // 17 | // [no]flags are supported just as flag are and are disabled as such. 18 | func ParseDig(arg string, opts *util.Options) error { 19 | // returns true if the flag starts with a no 20 | isNo := !strings.HasPrefix(arg, "no") 21 | if !isNo { 22 | arg = strings.TrimPrefix(arg, "no") 23 | } 24 | 25 | opts.Logger.Info("Setting", arg) 26 | 27 | switch arg { 28 | case "trace", "notrace": 29 | opts.Trace = isNo 30 | if isNo { 31 | opts.DNSSEC = true 32 | opts.Display.Comments = false 33 | opts.Display.Question = false 34 | opts.Display.Opt = false 35 | opts.Display.Answer = true 36 | opts.Display.Authority = true 37 | opts.Display.Additional = false 38 | opts.Display.Statistics = false 39 | } 40 | // Set DNS query flags 41 | case "aa", "aaflag", "aaonly": 42 | opts.AA = isNo 43 | case "ad", "adflag": 44 | opts.AD = isNo 45 | case "cd", "cdflag": 46 | opts.CD = isNo 47 | case "qrflag": 48 | opts.QR = isNo 49 | case "ra", "raflag": 50 | opts.RA = isNo 51 | case "rd", "rdflag", "recurse": 52 | opts.RD = isNo 53 | case "tc", "tcflag": 54 | opts.TC = isNo 55 | case "z", "zflag": 56 | opts.Z = isNo 57 | // End DNS query flags 58 | 59 | case "qr": 60 | opts.Display.ShowQuery = isNo 61 | case "ttlunits": 62 | opts.Display.HumanTTL = isNo 63 | case "ttl", "ttlid": 64 | opts.Display.TTL = isNo 65 | case "class": 66 | opts.Display.ShowClass = isNo 67 | 68 | // EDNS queries 69 | case "do", "dnssec": 70 | opts.EDNS.DNSSEC = isNo 71 | case "expire": 72 | opts.EDNS.Expire = isNo 73 | case "cookie": 74 | opts.EDNS.Cookie = isNo 75 | case "keepopen", "keepalive": 76 | opts.EDNS.KeepOpen = isNo 77 | case "nsid": 78 | opts.EDNS.Nsid = isNo 79 | case "padding": 80 | opts.EDNS.Padding = isNo 81 | // End EDNS queries 82 | 83 | // DNS-over-X 84 | case "tcp", "vc": 85 | opts.TCP = isNo 86 | case "ignore": 87 | opts.Truncate = isNo 88 | case "badcookie": 89 | opts.BadCookie = !isNo 90 | case "tls": 91 | opts.TLS = isNo 92 | case "dnscrypt": 93 | opts.DNSCrypt = isNo 94 | case "quic": 95 | opts.QUIC = isNo 96 | // End DNS-over-X 97 | 98 | // Formatting 99 | case "short": 100 | opts.Short = isNo 101 | case "identify": 102 | opts.Identify = isNo 103 | case "json": 104 | opts.JSON = isNo 105 | case "xml": 106 | opts.XML = isNo 107 | case "yaml": 108 | opts.YAML = isNo 109 | // End formatting 110 | 111 | // Output 112 | case "comments": 113 | opts.Display.Comments = isNo 114 | case "question": 115 | opts.Display.Question = isNo 116 | case "opt": 117 | opts.Display.Opt = isNo 118 | case "answer": 119 | opts.Display.Answer = isNo 120 | case "authority": 121 | opts.Display.Authority = isNo 122 | case "additional": 123 | opts.Display.Additional = isNo 124 | case "stats": 125 | opts.Display.Statistics = isNo 126 | case "all": 127 | opts.Display.Comments = isNo 128 | opts.Display.Question = isNo 129 | opts.Display.Opt = isNo 130 | opts.Display.Answer = isNo 131 | opts.Display.Authority = isNo 132 | opts.Display.Additional = isNo 133 | opts.Display.Statistics = isNo 134 | case "idnin", "idnout": 135 | opts.Display.UcodeTranslate = isNo 136 | 137 | default: 138 | if err := parseDigEq(isNo, arg, opts); err != nil { 139 | return err 140 | } 141 | } 142 | 143 | return nil 144 | } 145 | 146 | // For flags that contain "=". 147 | func parseDigEq(startNo bool, arg string, opts *util.Options) error { 148 | // Recursive switch statements WOO 149 | arg, val, isSplit := strings.Cut(arg, "=") 150 | switch arg { 151 | case "time", "timeout": 152 | if isSplit && val != "" { 153 | timeout, err := strconv.Atoi(val) 154 | if err != nil { 155 | return fmt.Errorf("digflags: timeout : %w", err) 156 | } 157 | 158 | opts.Request.Timeout = time.Duration(timeout) 159 | } else { 160 | return fmt.Errorf("digflags: timeout: %w", errNoArg) 161 | } 162 | 163 | case "retry", "tries": 164 | if isSplit && val != "" { 165 | tries, err := strconv.Atoi(val) 166 | if err != nil { 167 | return fmt.Errorf("digflags: retry: %w", err) 168 | } 169 | 170 | opts.Request.Retries = tries 171 | 172 | // TODO: Is there a better way to do this? 173 | if arg == "tries" { 174 | opts.Request.Retries-- 175 | } 176 | } else { 177 | return fmt.Errorf("digflags: retry: %w", errNoArg) 178 | } 179 | 180 | case "bufsize": 181 | if isSplit && val != "" { 182 | size, err := strconv.Atoi(val) 183 | if err != nil { 184 | return fmt.Errorf("digflags: EDNS UDP: %w", err) 185 | } 186 | 187 | opts.EDNS.BufSize = uint16(size) 188 | } else { 189 | return fmt.Errorf("digflags: EDNS UDP: %w", errNoArg) 190 | } 191 | 192 | case "ednsflags": 193 | if isSplit && val != "" { 194 | ver, err := strconv.ParseInt(val, 0, 16) 195 | if err != nil { 196 | return fmt.Errorf("digflags: EDNS flag: %w", err) 197 | } 198 | 199 | // Ignore setting DO bit 200 | opts.EDNS.ZFlag = uint16(ver & 0x7FFF) 201 | } else { 202 | opts.EDNS.ZFlag = 0 203 | } 204 | 205 | case "edns": 206 | opts.EDNS.EnableEDNS = startNo 207 | 208 | if isSplit && val != "" { 209 | ver, err := strconv.Atoi(val) 210 | if err != nil { 211 | return fmt.Errorf("digflags: EDNS version: %w", err) 212 | } 213 | 214 | opts.EDNS.Version = uint8(ver) 215 | } else { 216 | opts.EDNS.Version = 0 217 | } 218 | 219 | case "https", "https-get", "https-post": 220 | opts.HTTPS = startNo 221 | if isSplit && val != "" { 222 | opts.HTTPSOptions.Endpoint = val 223 | } else { 224 | opts.HTTPSOptions.Endpoint = "/dns-query" 225 | } 226 | 227 | if strings.HasSuffix(arg, "get") { 228 | opts.HTTPSOptions.Get = true 229 | } 230 | 231 | case "subnet": 232 | if isSplit && val != "" { 233 | err := util.ParseSubnet(val, opts) 234 | if err != nil { 235 | return fmt.Errorf("digflags: EDNS Subnet: %w", err) 236 | } 237 | } else { 238 | return fmt.Errorf("digflags: EDNS Subnet: %w", errNoArg) 239 | } 240 | 241 | default: 242 | return &errInvalidArg{arg} 243 | } 244 | 245 | return nil 246 | } 247 | -------------------------------------------------------------------------------- /pkg/util/options.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package util 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | 9 | "dns.froth.zone/awl/pkg/logawl" 10 | "github.com/miekg/dns" 11 | ) 12 | 13 | // Options is the grand structure for all query options. 14 | type Options struct { 15 | // The logger 16 | Logger *logawl.Logger `json:"-"` 17 | // Host to verify TLS cert with 18 | TLSHost string `json:"tlsHost" example:""` 19 | // EDNS Options 20 | EDNS 21 | 22 | // HTTPS options :) 23 | HTTPSOptions 24 | 25 | // DNS request :) 26 | Request 27 | 28 | // Verbosity levels, see [logawl.AllLevels] 29 | Verbosity int `json:"-" example:"0"` 30 | // Display options 31 | Display Display 32 | // Ignore Truncation 33 | Truncate bool `json:"ignoreTruncate" example:"false"` 34 | // Ignore BADCOOKIE 35 | BadCookie bool `json:"ignoreBadCookie" example:"false"` 36 | // Print only the answer 37 | Short bool `json:"short" example:"false"` 38 | // When Short is true, display where the query came from 39 | Identify bool `json:"identify" example:"false"` 40 | // Perform a reverse DNS query when true 41 | Reverse bool `json:"reverse" example:"false"` 42 | 43 | HeaderFlags 44 | 45 | // Display resposne as JSON 46 | JSON bool `json:"-" xml:"-" yaml:"-"` 47 | // Display response as XML 48 | XML bool `json:"-" xml:"-" yaml:"-"` 49 | // Display response as YAML 50 | YAML bool `json:"-" xml:"-" yaml:"-"` 51 | 52 | // Use TCP instead of UDP to make the query 53 | TCP bool `json:"tcp" example:"false"` 54 | // Use DNS-over-TLS to make the query 55 | TLS bool `json:"dnsOverTLS" example:"false"` 56 | // When using TLS, ignore certificates 57 | TLSNoVerify bool `json:"tlsNoVerify" example:"false"` 58 | // Use DNS-over-HTTPS to make the query 59 | HTTPS bool `json:"dnsOverHTTPS" example:"false"` 60 | // Use DNS-over-QUIC to make the query 61 | //nolint:tagliatelle // QUIC is an acronym 62 | QUIC bool `json:"dnsOverQUIC" example:"false"` 63 | // Use DNSCrypt to make the query 64 | DNSCrypt bool `json:"dnscrypt" example:"false"` 65 | 66 | // Force IPv4 only 67 | IPv4 bool `json:"forceIPv4" example:"false"` 68 | // Force IPv6 only 69 | IPv6 bool `json:"forceIPv6" example:"false"` 70 | 71 | // Trace from the root 72 | Trace bool `json:"trace" example:"false"` 73 | } 74 | 75 | // HTTPSOptions are options exclusively for DNS-over-HTTPS queries. 76 | type HTTPSOptions struct { 77 | // URL endpoint 78 | Endpoint string `json:"endpoint" example:"/dns-query"` 79 | 80 | // True, make GET request. 81 | // False, make POST request. 82 | Get bool `json:"get" example:"false"` 83 | } 84 | 85 | // HeaderFlags are the flags that are in DNS headers. 86 | type HeaderFlags struct { 87 | // Authoritative Answer DNS query flag 88 | AA bool `json:"authoritative" example:"false"` 89 | // Authenticated Data DNS query flag 90 | AD bool `json:"authenticatedData" example:"false"` 91 | // Checking Disabled DNS query flag 92 | CD bool `json:"checkingDisabled" example:"false"` 93 | // QueRy DNS query flag 94 | QR bool `json:"query" example:"false"` 95 | // Recursion Desired DNS query flag 96 | RD bool `json:"recursionDesired" example:"true"` 97 | // Recursion Available DNS query flag 98 | RA bool `json:"recursionAvailable" example:"false"` 99 | // TrunCated DNS query flag 100 | TC bool `json:"truncated" example:"false"` 101 | // Zero DNS query flag 102 | Z bool `json:"zero" example:"false"` 103 | } 104 | 105 | // Display contains toggles for what to (and not to) display. 106 | type Display struct { 107 | /* Section displaying */ 108 | 109 | // Comments? 110 | Comments bool `json:"comments" example:"true"` 111 | // QUESTION SECTION 112 | Question bool `json:"question" example:"true"` 113 | // OPT PSEUDOSECTION 114 | Opt bool `json:"opt" example:"true"` 115 | // ANSWER SECTION 116 | Answer bool `json:"answer" example:"true"` 117 | // AUTHORITY SECTION 118 | Authority bool `json:"authority" example:"true"` 119 | // ADDITIONAL SECTION 120 | Additional bool `json:"additional" example:"true"` 121 | // Query time, message size, etc. 122 | Statistics bool `json:"statistics" example:"true"` 123 | // Display TTL in response 124 | TTL bool `json:"ttl" example:"true"` 125 | 126 | /* Answer formatting */ 127 | 128 | // Display Class in response 129 | ShowClass bool `json:"showClass" example:"true"` 130 | // Display query before it is sent 131 | ShowQuery bool `json:"showQuery" example:"false"` 132 | // Display TTL as human-readable 133 | HumanTTL bool `json:"humanTTL" example:"false"` 134 | // Translate Punycode back to Unicode 135 | UcodeTranslate bool `json:"unicode" example:"true"` 136 | } 137 | 138 | // EDNS contains toggles for various EDNS options. 139 | type EDNS struct { 140 | // Subnet to originate query from. 141 | Subnet dns.EDNS0_SUBNET `json:"subnet"` 142 | // Must Be Zero flag 143 | ZFlag uint16 `json:"zflag" example:"0"` 144 | // UDP buffer size 145 | BufSize uint16 `json:"bufSize" example:"1232"` 146 | // Enable/Disable EDNS entirely 147 | EnableEDNS bool `json:"edns" example:"false"` 148 | // Sending EDNS cookie 149 | Cookie bool `json:"cookie" example:"true"` 150 | // Enabling DNSSEC 151 | DNSSEC bool `json:"dnssec" example:"false"` 152 | // Sending EDNS Expire 153 | Expire bool `json:"expire" example:"false"` 154 | // Sending EDNS TCP keepopen 155 | KeepOpen bool `json:"keepOpen" example:"false"` 156 | // Sending EDNS NSID 157 | Nsid bool `json:"nsid" example:"false"` 158 | // Send EDNS Padding 159 | Padding bool `json:"padding" example:"false"` 160 | // Set EDNS version (default: 0) 161 | Version uint8 `json:"version" example:"0"` 162 | } 163 | 164 | // ParseSubnet takes a subnet argument and makes it into one that the DNS library 165 | // understands. 166 | func ParseSubnet(subnet string, opts *Options) error { 167 | ip, inet, err := net.ParseCIDR(subnet) 168 | if err != nil { 169 | // TODO: make not a default? 170 | if subnet == "0" { 171 | opts.EDNS.Subnet = dns.EDNS0_SUBNET{ 172 | Code: dns.EDNS0SUBNET, 173 | Family: 1, 174 | SourceNetmask: 0, 175 | SourceScope: 0, 176 | Address: net.IPv4(0, 0, 0, 0), 177 | } 178 | 179 | return nil 180 | } 181 | 182 | return fmt.Errorf("EDNS subnet parsing: %w", err) 183 | } 184 | 185 | sub, _ := inet.Mask.Size() 186 | opts.EDNS.Subnet = dns.EDNS0_SUBNET{} 187 | opts.EDNS.Subnet.Address = ip 188 | opts.EDNS.Subnet.SourceNetmask = uint8(sub) 189 | 190 | switch ip.To4() { 191 | case nil: 192 | // Not a valid IPv4 so assume IPv6 193 | opts.EDNS.Subnet.Family = 2 194 | default: 195 | // Valid IPv4 196 | opts.EDNS.Subnet.Family = 1 197 | } 198 | 199 | return nil 200 | } 201 | -------------------------------------------------------------------------------- /pkg/query/struct.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package query 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | // Message is for overall DNS responses. 10 | // 11 | //nolint:govet,tagliatelle // Better looking output is worth a few bytes. 12 | type Message struct { 13 | DateString string `json:"dateString,omitempty" xml:"dateString,omitempty" yaml:"dateString,omitempty"` 14 | DateSeconds int64 `json:"dateSeconds,omitempty" xml:"dateSeconds,omitempty" yaml:"dateSeconds,omitempty"` 15 | MsgSize int `json:"msgLength,omitempty" xml:"msgSize,omitempty" yaml:"msgSize,omitempty"` 16 | ID uint16 `json:"ID" xml:"ID" yaml:"ID" example:"12"` 17 | 18 | Opcode int `json:"opcode" xml:"opcode" yaml:"opcode" example:"QUERY"` 19 | Response bool `json:"QR" xml:"QR" yaml:"QR" example:"true"` 20 | Authoritative bool `json:"AA" xml:"AA" yaml:"AA" example:"false"` 21 | Truncated bool `json:"TC" xml:"TC" yaml:"TC" example:"false"` 22 | RecursionDesired bool `json:"RD" xml:"RD" yaml:"RD" example:"true"` 23 | RecursionAvailable bool `json:"RA" xml:"RA" yaml:"RA" example:"true"` 24 | AuthenticatedData bool `json:"AD" xml:"AD" yaml:"AD" example:"false"` 25 | CheckingDisabled bool `json:"CD" xml:"CD" yaml:"CD" example:"false"` 26 | Zero bool `json:"Z" xml:"Z" yaml:"Z" example:"false"` 27 | 28 | QdCount int `json:"QDCOUNT" xml:"QDCOUNT" yaml:"QDCOUNT" example:"0"` 29 | AnCount int `json:"ANCOUNT" xml:"ANCOUNT" yaml:"ANCOUNT" example:"0"` 30 | NsCount int `json:"NSCOUNT" xml:"NSCOUNT" yaml:"NSCOUNT" example:"0"` 31 | ArCount int `json:"ARCOUNT" xml:"ARCOUNT" yaml:"ARCOUNT" example:"0"` 32 | 33 | Name string `json:"QNAME,omitempty" xml:"QNAME,omitempty" yaml:"QNAME,omitempty" example:"localhost"` 34 | Type uint16 `json:"QTYPE,omitempty" xml:"QTYPE,omitempty" yaml:"QTYPE,omitempty" example:"IN"` 35 | TypeName string `json:"QTYPEname,omitempty" xml:"QTYPEname,omitempty" yaml:"QTYPEname,omitempty" example:"IN"` 36 | Class uint16 `json:"QCLASS,omitempty" xml:"QCLASS,omitempty" yaml:"QCLASS,omitempty" example:"A"` 37 | ClassName string `json:"QCLASSname,omitempty" xml:"QCLASSname,omitempty" yaml:"QCLASSname,omitempty" example:"1"` 38 | 39 | EDNS0 EDNS0 `json:",omitempty" xml:",omitempty" yaml:",omitempty"` 40 | 41 | // Answer Section 42 | AnswerRRs []Answer `json:"answersRRs,omitempty" xml:"answersRRs,omitempty" yaml:"answersRRs,omitempty" example:"false"` 43 | AuthoritativeRRs []Answer `json:"authorityRRs,omitempty" xml:"authorityRRs,omitempty" yaml:"authorityRRs,omitempty" example:"false"` 44 | AdditionalRRs []Answer `json:"additionalRRs,omitempty" xml:"additionalRRs,omitempty" yaml:"additionalRRs,omitempty" example:"false"` 45 | } 46 | 47 | // Answer is for DNS Resource Headers. 48 | // 49 | //nolint:govet,tagliatelle 50 | type Answer struct { 51 | Name string `json:"NAME,omitempty" xml:"NAME,omitempty" yaml:"NAME,omitempty" example:"127.0.0.1"` 52 | Type uint16 `json:"TYPE,omitempty" xml:"TYPE,omitempty" yaml:"TYPE,omitempty" example:"1"` 53 | TypeName string `json:"TYPEname,omitempty" xml:"TYPEname,omitempty" yaml:"TYPEname,omitempty" example:"A"` 54 | Class uint16 `json:"CLASS,omitempty" xml:"CLASS,omitempty" yaml:"CLASS,omitempty" example:"1"` 55 | ClassName string `json:"CLASSname,omitempty" xml:"CLASSname,omitempty" yaml:"CLASSname,omitempty" example:"IN"` 56 | TTL any `json:"TTL,omitempty" xml:"TTL,omitempty" yaml:"TTL,omitempty" example:"0ms"` 57 | Value string `json:"rdata,omitempty" xml:"rdata,omitempty" yaml:"rdata,omitempty"` 58 | Rdlength uint16 `json:"RDLENGTH,omitempty" xml:"RDLENGTH,omitempty" yaml:"RDLENGTH,omitempty"` 59 | Rdhex string `json:"RDATAHEX,omitempty" xml:"RDATAHEX,omitempty" yaml:"RDATAHEX,omitempty"` 60 | } 61 | 62 | // EDNS0 is for all EDNS options. 63 | // 64 | // RFC: https://datatracker.ietf.org/docs/draft-peltan-edns-presentation-format/ 65 | // 66 | //nolint:govet,tagliatelle 67 | type EDNS0 struct { 68 | Flags []string `json:"FLAGS" xml:"FLAGS" yaml:"FLAGS"` 69 | Rcode string `json:"RCODE" xml:"RCODE" yaml:"RCODE"` 70 | PayloadSize uint16 `json:"UDPSIZE" xml:"UDPSIZE" yaml:"UDPSIZE"` 71 | LLQ *EdnsLLQ `json:"LLQ,omitempty" xml:"LLQ,omitempty" yaml:"LLQ,omitempty"` 72 | NsidHex string `json:"NSIDHEX,omitempty" xml:"NSIDHEX,omitempty" yaml:"NSIDHEX,omitempty"` 73 | Nsid string `json:"NSID,omitempty" xml:"NSID,omitempty" yaml:"NSID,omitempty"` 74 | Dau []uint8 `json:"DAU,omitempty" xml:"DAU,omitempty" yaml:"DAU,omitempty"` 75 | Dhu []uint8 `json:"DHU,omitempty" xml:"DHU,omitempty" yaml:"DHU,omitempty"` 76 | N3u []uint8 `json:"N3U,omitempty" xml:"N3U,omitempty" yaml:"N3U,omitempty"` 77 | Subnet *EDNSSubnet `json:"ECS,omitempty" xml:"ECS,omitempty" yaml:"ECS,omitempty"` 78 | Expire uint32 `json:"EXPIRE,omitempty" xml:"EXPIRE,omitempty" yaml:"EXPIRE,omitempty"` 79 | Cookie []string `json:"COOKIE,omitempty" xml:"COOKIE,omitempty" yaml:"COOKIE,omitempty"` 80 | KeepAlive uint16 `json:"KEEPALIVE,omitempty" xml:"KEEPALIVE,omitempty" yaml:"KEEPALIVE,omitempty"` 81 | Padding string `json:"PADDING,omitempty" xml:"PADDING,omitempty" yaml:"PADDING,omitempty"` 82 | Chain string `json:"CHAIN,omitempty" xml:"CHAIN,omitempty" yaml:"CHAIN,omitempty"` 83 | EDE *EDNSErr `json:"EDE,omitempty" xml:"EDE,omitempty" yaml:"EDE,omitempty"` 84 | } 85 | 86 | // EdnsLLQ is for Long-lived queries. 87 | // 88 | //nolint:tagliatelle 89 | type EdnsLLQ struct { 90 | Version uint16 `json:"LLQ-VERSION" xml:"LLQ-VERSION" yaml:"LLQ-VERSION"` 91 | Opcode uint16 `json:"LLQ-OPCODE" xml:"LLQ-OPCODE" yaml:"LLQ-OPCODE"` 92 | Error uint16 `json:"LLQ-ERROR" xml:"LLQ-ERROR" yaml:"LLQ-ERROR"` 93 | ID uint64 `json:"LLQ-ID" xml:"LLQ-ID" yaml:"LLQ-ID"` 94 | Lease uint32 `json:"LLQ-LEASE" xml:"LLQ-LEASE" yaml:"LLQ-LEASE"` 95 | } 96 | 97 | // EDNSSubnet is for EDNS subnet options, 98 | // 99 | //nolint:govet,tagliatelle 100 | type EDNSSubnet struct { 101 | Family uint16 `json:"FAMILY" xml:"FAMILY" yaml:"FAMILY"` 102 | IP string 103 | Source uint8 `json:"SOURCE" xml:"SOURCE" yaml:"SOURCE"` 104 | Scope uint8 `json:"SCOPE,omitempty" xml:"SCOPE,omitempty" yaml:"SCOPE,omitempty"` 105 | } 106 | 107 | // EDNSErr is for EDE codes 108 | // 109 | //nolint:govet,tagliatelle 110 | type EDNSErr struct { 111 | Code uint16 `json:"INFO-CODE" xml:"INFO-CODE" yaml:"INFO-CODE"` 112 | Purpose string 113 | Text string `json:"EXTRA-TEXT,omitempty" xml:"EXTRA-TEXT,omitempty" yaml:"EXTRA-TEXT,omitempty"` 114 | } 115 | 116 | var errNoMessage = errors.New("no message") 117 | -------------------------------------------------------------------------------- /pkg/query/util.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "dns.froth.zone/awl/pkg/util" 10 | "github.com/miekg/dns" 11 | "golang.org/x/net/idna" 12 | ) 13 | 14 | func (message *Message) displayQuestion(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error { 15 | var ( 16 | name string 17 | err error 18 | ) 19 | 20 | for _, question := range msg.Question { 21 | if opts.Display.UcodeTranslate { 22 | name, err = idna.ToUnicode(question.Name) 23 | if err != nil { 24 | return fmt.Errorf("punycode to unicode: %w", err) 25 | } 26 | } else { 27 | name = question.Name 28 | } 29 | 30 | message.Name = name 31 | message.Type = question.Qtype 32 | message.TypeName = dns.TypeToString[question.Qtype] 33 | message.Class = question.Qclass 34 | message.ClassName = dns.ClassToString[question.Qclass] 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (message *Message) displayAnswers(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error { 41 | var ( 42 | ttl any 43 | name string 44 | err error 45 | ) 46 | 47 | for _, answer := range msg.Answer { 48 | temp := strings.Split(answer.String(), "\t") 49 | 50 | if opts.Display.TTL { 51 | if opts.Display.HumanTTL { 52 | ttl = (time.Duration(answer.Header().Ttl) * time.Second).String() 53 | } else { 54 | ttl = answer.Header().Ttl 55 | } 56 | } 57 | 58 | if opts.Display.UcodeTranslate { 59 | name, err = idna.ToUnicode(answer.Header().Name) 60 | if err != nil { 61 | return fmt.Errorf("punycode to unicode: %w", err) 62 | } 63 | } else { 64 | name = answer.Header().Name 65 | } 66 | 67 | message.AnswerRRs = append(message.AnswerRRs, Answer{ 68 | Name: name, 69 | ClassName: dns.ClassToString[answer.Header().Class], 70 | Class: answer.Header().Class, 71 | TypeName: dns.TypeToString[answer.Header().Rrtype], 72 | Type: answer.Header().Rrtype, 73 | Rdlength: answer.Header().Rdlength, 74 | TTL: ttl, 75 | 76 | Value: temp[len(temp)-1], 77 | }) 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (message *Message) displayAuthority(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error { 84 | var ( 85 | ttl any 86 | name string 87 | err error 88 | ) 89 | 90 | for _, ns := range msg.Ns { 91 | temp := strings.Split(ns.String(), "\t") 92 | 93 | if opts.Display.TTL { 94 | if opts.Display.HumanTTL { 95 | ttl = (time.Duration(ns.Header().Ttl) * time.Second).String() 96 | } else { 97 | ttl = ns.Header().Ttl 98 | } 99 | } 100 | 101 | if opts.Display.UcodeTranslate { 102 | name, err = idna.ToUnicode(ns.Header().Name) 103 | if err != nil { 104 | return fmt.Errorf("punycode to unicode: %w", err) 105 | } 106 | } else { 107 | name = ns.Header().Name 108 | } 109 | 110 | message.AuthoritativeRRs = append(message.AuthoritativeRRs, Answer{ 111 | Name: name, 112 | TypeName: dns.TypeToString[ns.Header().Rrtype], 113 | Type: ns.Header().Rrtype, 114 | Class: ns.Header().Class, 115 | ClassName: dns.ClassToString[ns.Header().Class], 116 | Rdlength: ns.Header().Rdlength, 117 | TTL: ttl, 118 | 119 | Value: temp[len(temp)-1], 120 | }) 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func (message *Message) displayAdditional(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error { 127 | var ( 128 | ttl any 129 | name string 130 | err error 131 | ) 132 | 133 | for _, additional := range msg.Extra { 134 | if additional.Header().Rrtype == dns.StringToType["OPT"] { 135 | continue 136 | } else { 137 | temp := strings.Split(additional.String(), "\t") 138 | 139 | if opts.Display.TTL { 140 | if opts.Display.HumanTTL { 141 | ttl = (time.Duration(additional.Header().Ttl) * time.Second).String() 142 | } else { 143 | ttl = additional.Header().Ttl 144 | } 145 | } 146 | 147 | if opts.Display.UcodeTranslate { 148 | name, err = idna.ToUnicode(additional.Header().Name) 149 | if err != nil { 150 | return fmt.Errorf("punycode to unicode: %w", err) 151 | } 152 | } else { 153 | name = additional.Header().Name 154 | } 155 | message.AdditionalRRs = append(message.AdditionalRRs, Answer{ 156 | Name: name, 157 | TypeName: dns.TypeToString[additional.Header().Rrtype], 158 | Type: additional.Header().Rrtype, 159 | Class: additional.Header().Class, 160 | ClassName: dns.ClassToString[additional.Header().Class], 161 | Rdlength: additional.Header().Rdlength, 162 | TTL: ttl, 163 | Value: temp[len(temp)-1], 164 | }) 165 | } 166 | } 167 | 168 | return nil 169 | } 170 | 171 | // ParseOpt parses opts. 172 | func (message *Message) ParseOpt(rcode int, rr dns.OPT) (ret EDNS0, err error) { 173 | ret.Rcode = dns.RcodeToString[rcode] 174 | 175 | // Most of this is taken from https://github.com/miekg/dns/blob/master/edns.go#L76 176 | if rr.Do() { 177 | ret.Flags = append(ret.Flags, "DO") 178 | } 179 | 180 | for i := uint32(1); i <= 0x7FFF; i <<= 1 { 181 | if rr.Hdr.Ttl&i != 0 { 182 | ret.Flags = append(ret.Flags, fmt.Sprintf("BIT%d", i)) 183 | } 184 | } 185 | 186 | ret.PayloadSize = rr.UDPSize() 187 | 188 | for _, opt := range rr.Option { 189 | switch opt := opt.(type) { 190 | case *dns.EDNS0_NSID: 191 | str := opt.String() 192 | 193 | hex, err := hex.DecodeString(str) 194 | if err != nil { 195 | return ret, fmt.Errorf("%w", err) 196 | } 197 | 198 | ret.NsidHex = string(hex) 199 | ret.Nsid = str 200 | 201 | case *dns.EDNS0_SUBNET: 202 | ret.Subnet = &EDNSSubnet{ 203 | Source: opt.SourceNetmask, 204 | Family: opt.Family, 205 | } 206 | 207 | // 1: IPv4 2: IPv6 208 | if ret.Subnet.Family <= 2 { 209 | ret.Subnet.IP = opt.Address.String() 210 | } else { 211 | ret.Subnet.IP = hex.EncodeToString([]byte(opt.Address)) 212 | } 213 | 214 | if opt.SourceScope != 0 { 215 | ret.Subnet.Scope = opt.SourceScope 216 | } 217 | 218 | case *dns.EDNS0_COOKIE: 219 | ret.Cookie = append(ret.Cookie, opt.String()) 220 | 221 | case *dns.EDNS0_EXPIRE: 222 | ret.Expire = opt.Expire 223 | 224 | case *dns.EDNS0_TCP_KEEPALIVE: 225 | ret.KeepAlive = opt.Timeout 226 | 227 | case *dns.EDNS0_LLQ: 228 | ret.LLQ = &EdnsLLQ{ 229 | Version: opt.Version, 230 | Opcode: opt.Opcode, 231 | Error: opt.Error, 232 | ID: opt.Id, 233 | Lease: opt.LeaseLife, 234 | } 235 | 236 | case *dns.EDNS0_DAU: 237 | ret.Dau = opt.AlgCode 238 | 239 | case *dns.EDNS0_DHU: 240 | ret.Dhu = opt.AlgCode 241 | 242 | case *dns.EDNS0_N3U: 243 | ret.N3u = opt.AlgCode 244 | 245 | case *dns.EDNS0_PADDING: 246 | ret.Padding = string(opt.Padding) 247 | 248 | case *dns.EDNS0_EDE: 249 | ret.EDE = &EDNSErr{ 250 | Code: opt.InfoCode, 251 | Purpose: dns.ExtendedErrorCodeToString[opt.InfoCode], 252 | Text: opt.ExtraText, 253 | } 254 | } 255 | } 256 | 257 | return ret, nil 258 | } 259 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - make clean 5 | - go mod tidy 6 | - docs/prepare-packaging.sh 7 | - go mod vendor 8 | 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | targets: 13 | - go_first_class 14 | # - plan9_amd64 15 | - freebsd_amd64 16 | 17 | universal_binaries: 18 | - replace: true 19 | 20 | archives: 21 | - files: 22 | - LICENSE 23 | - completions/** 24 | - docs/awl.1.gz 25 | name_template: >- 26 | {{ .ProjectName }}_ 27 | {{- if eq .Os "darwin" }}MacOS_ 28 | {{- else if eq .Os "freebsd" }}FreeBSD_ 29 | {{- else }}{{- title .Os }}_{{ end }} 30 | {{- if eq .Arch "386" }}i386 31 | {{- else if eq .Arch "mips64" }}mips64_hardfloat 32 | {{- else if eq .Arch "mips64le" }}mips64le_hardfloat 33 | {{- else }}{{ .Arch }}{{ end -}} 34 | format_overrides: 35 | - goos: windows 36 | formats: zip 37 | - files: 38 | - vendor/** 39 | id: vendor 40 | formats: tar.xz 41 | name_template: "{{ .ProjectName }}-{{ .Version }}-deps" 42 | meta: true 43 | wrap_in_directory: "{{ .ProjectName }}" 44 | 45 | nfpms: 46 | - id: packages 47 | package_name: awl-dns 48 | vendor: Sam Therapy 49 | maintainer: Sam Therapy 50 | homepage: https://dns.froth.zone/awl 51 | description: |- 52 | Command-line DNS query tool. 53 | Awl supports DNS-over-[UDP,TCP,HTTPS,QUIC] and DNSCrypt. 54 | license: BSD-3-Clause 55 | section: utils 56 | bindir: /usr/bin 57 | formats: 58 | - apk 59 | - archlinux 60 | - deb 61 | - rpm 62 | contents: 63 | - src: completions/bash.bash 64 | dst: /usr/share/bash-completion/completions/awl 65 | - src: docs/awl.1.gz 66 | dst: /usr/share/man/man1/awl.1.gz 67 | - src: completions/fish.fish 68 | dst: /usr/share/fish/vendor_completions.d/awl.fish 69 | # DEB only 70 | - src: LICENSE 71 | dst: /usr/share/doc/awl/copyright 72 | packager: deb 73 | - src: README.md.gz 74 | dst: /usr/share/doc/awl/README.md.gz 75 | packager: deb 76 | - src: docs/CONTRIBUTING.md.gz 77 | dst: /usr/share/doc/awl/CONTRIBUTING.md.gz 78 | packager: deb 79 | - src: completions/zsh.zsh 80 | dst: /usr/share/zsh/vendor-completions/_awl 81 | packager: deb 82 | # Alpine .apk only 83 | - src: completions/zsh.zsh 84 | dst: /usr/share/zsh/site-functions/_awl 85 | packager: apk 86 | # RPM only 87 | - src: LICENSE 88 | dst: /usr/share/licenses/awl/LICENSE 89 | packager: rpm 90 | - src: README.md 91 | dst: /usr/share/doc/awl/README.md 92 | packager: rpm 93 | - src: docs/CONTRIBUTING.md 94 | dst: /usr/share/doc/awl/CONTRIBUTING.md.gz 95 | packager: rpm 96 | - src: completions/zsh.zsh 97 | dst: /usr/share/zsh/site-functions/_awl 98 | packager: rpm 99 | deb: 100 | lintian_overrides: 101 | - statically-linked-binary 102 | - changelog-file-missing-in-native-package 103 | overrides: 104 | deb: 105 | file_name_template: >- 106 | {{- .PackageName }}_ 107 | {{- .Version }}_ 108 | {{- if eq .Arch "386" }}i386 109 | {{- else if eq .Arch "arm" }}armel 110 | {{- else }}{{ .Arch }}{{ end -}} 111 | rpm: 112 | file_name_template: >- 113 | {{- .PackageName }}- 114 | {{- .Version }}- 115 | {{- if eq .Arch "amd64" }}x86_64 116 | {{- else if eq .Arch "386" }}i686 117 | {{- else if eq .Arch "arm" }}armhfp 118 | {{- else if eq .Arch "arm64" }}aarch64 119 | {{- else }}{{ .Arch }}{{ end -}} 120 | - id: termux 121 | package_name: awl-dns 122 | vendor: Sam Therapy 123 | maintainer: Sam Therapy 124 | homepage: https://dns.froth.zone/awl 125 | description: |- 126 | Command-line DNS query tool. 127 | Awl supports DNS-over-[UDP,TCP,HTTPS,QUIC] and DNSCrypt. 128 | license: BSD-3-Clause 129 | section: utils 130 | formats: 131 | - termux.deb 132 | file_name_template: >- 133 | {{- .PackageName }}_ 134 | {{- .Version }}_ 135 | {{- if eq .Arch "amd64" }}x86_64 136 | {{- else if eq .Arch "386" }}i686 137 | {{- else if eq .Arch "arm" }}arm 138 | {{- else if eq .Arch "arm64" }}aarch64 139 | {{- else }}{{ .Arch }}{{ end -}} 140 | 141 | snapcrafts: 142 | - name: awl-dns 143 | grade: stable 144 | publish: true 145 | summary: A command-line DNS query tool 146 | description: |- 147 | Awl is a command-line DNS query tool. 148 | Awl supports DNS-over-[UDP,TCP,HTTPS,QUIC] and DNSCrypt. 149 | confinement: strict 150 | license: BSD-3-Clause 151 | base: bare 152 | apps: 153 | awl-dns: 154 | command: awl 155 | plugs: 156 | - network 157 | completer: completions/bash.bash 158 | 159 | dockers: 160 | - image_templates: 161 | - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:latest" 162 | - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Tag }}" 163 | 164 | checksum: 165 | name_template: "checksums.txt" 166 | 167 | snapshot: 168 | version_template: "{{ incpatch .Version }}-next" 169 | 170 | homebrew_casks: 171 | - repository: 172 | owner: packaging 173 | name: homebrew 174 | homepage: https://dns.froth.zone/awl 175 | description: A command-line DNS query tool 176 | license: BSD-3-Clause 177 | manpages: 178 | - "docs/awl.1.gz" 179 | completions: 180 | bash: "completions/bash.bash" 181 | zsh: "completions/zsh.zsh" 182 | fish: "completions/fish.fish" 183 | 184 | nix: 185 | - repository: 186 | owner: packaging 187 | name: nur 188 | homepage: https://dns.froth.zone/awl 189 | description: A command-line DNS query tool 190 | license: bsd3 191 | extra_install: |- 192 | installManPage ./docs/awl.1.gz 193 | installShellCompletion ./completions/* 194 | 195 | scoops: 196 | - repository: 197 | owner: packaging 198 | name: scoop 199 | directory: bucket 200 | homepage: https://dns.froth.zone/awl 201 | description: A command-line DNS query tool 202 | license: BSD-3-Clause 203 | 204 | changelog: 205 | sort: asc 206 | groups: 207 | - title: "Dependency Updates" 208 | regexp: "^.*fix\\(deps\\)*:+.*$" 209 | order: 2 210 | - title: "Features" 211 | regexp: "^.*feat[(\\w)]*:+.*$" 212 | order: 0 213 | - title: "Bug fixes" 214 | regexp: "^.*fix[(\\w)]*:+.*$" 215 | order: 1 216 | - title: "Other" 217 | order: 999 218 | filters: 219 | exclude: 220 | - "^test:" 221 | - "^docs?:" 222 | - "typo" 223 | - "^ci:" 224 | 225 | uploads: 226 | - name: packages 227 | method: PUT 228 | mode: archive 229 | exts: 230 | - deb 231 | - rpm 232 | - apk 233 | - termux.deb 234 | username: sam 235 | target: >- 236 | https://git.froth.zone/api/packages/sam/ 237 | {{- if eq .ArtifactExt "deb" }}debian/pool/sid/main/upload 238 | {{- else if eq .ArtifactExt "termux.deb" }}debian/pool/termux/main/upload 239 | {{- else if eq .ArtifactExt "rpm" }}rpm/upload 240 | {{- else if eq .ArtifactExt "apk" }}alpine/edge/main{{ end -}} 241 | custom_artifact_name: true # Truncate the artifact name from the upload URL 242 | 243 | gitea_urls: 244 | api: https://git.froth.zone/api/v1 245 | download: https://git.froth.zone 246 | -------------------------------------------------------------------------------- /docs/awl.1.scd: -------------------------------------------------------------------------------- 1 | awl(1) 2 | ; SPDX-License-Identifier: BSD-3-Clause 3 | 4 | # NAME 5 | 6 | awl - DNS lookup tool 7 | 8 | # SYNOPSIS 9 | 10 | *awl* [ _OPTIONS_ ] _name_ [ _@server_ ] [ _type_ ], where 11 | 12 | _name_ is the query to make (example: froth.zone)++ 13 | _@server_ is the server to query (example: dns.froth.zone)++ 14 | _type_ is the DNS resource type (example: AAAA) 15 | 16 | # DESCRIPTION 17 | 18 | *awl* (*a*wls *w*ant *l*icorice) is a simple tool designed to make DNS queries, 19 | much like the venerable *dig*(1). An awl is a tool used to make small holes, 20 | typically used in leatherworking. 21 | 22 | *awl* is designed to be a more "modern" version of *drill*(1) by including 23 | some more recent RFCs and output options. 24 | 25 | When no arguments are given, *awl* will perform an _NS_ query on the root ('_._'). 26 | 27 | When a nameserver is not given, *awl* will query a random system nameserver. 28 | If one cannot be found, *awl* will query the localhost. 29 | 30 | # OPTIONS 31 | 32 | *-4* 33 | Force only IPv4 34 | 35 | *-6* 36 | Force only IPv6 37 | 38 | *-c*, *--class* _class_ 39 | DNS class to query (eg. IN, CH) 40 | The default is IN. 41 | 42 | *-h* 43 | Show a "short" help message. 44 | 45 | *-p*, *--port* _port_ 46 | Sets the port to query. Default ports listed below. 47 | - _53_ for *UDP* and *TCP* 48 | - _853_ for *TLS* and *QUIC* 49 | - _443_ for *HTTPS* 50 | 51 | *-q*, *--query* _domain_ 52 | Explicitly set a domain to query (eg. example.com) 53 | 54 | *-t*, *--qType* _type_ 55 | Explicitly set a DNS type to query (eg. A, AAAA, NS) 56 | The default is A. 57 | 58 | *-v*[=_int_] 59 | Set verbosity of output 60 | Accepted values are as follows: 61 | - _0_: Only log errors. 62 | - _1_: Log warnings. *This is the default.* 63 | - _2_: Log information *Default when specifying just* _-v_. 64 | - _3_: Log information useful for debugging. 65 | 66 | Setting a value lower than 0 disables logging entirely. 67 | 68 | By default, specifying just *-v* sets the verbosity to 2 (info). 69 | 70 | *-x*, *--reverse* 71 | Do a reverse lookup. Sets default *type* to PTR. 72 | *awl* automatically makes an IP or phone number canonical. 73 | 74 | *-V* 75 | Print the version and exit. 76 | 77 | # QUERY OPTIONS 78 | 79 | Anything in [brackets] is optional. 80 | Many options are inherited from *dig*(1). 81 | 82 | *--aa*[=_bool_], *+*[no]*aaflag*, *+*[no]*aaonly* 83 | Sets the AA (Authoritative Answer) flag. 84 | 85 | *--ad*[=_bool_], *+*[no]*adflag* 86 | Sets the AD (Authenticated Data) flag. 87 | 88 | *--no-additional*, *+*[no]*additional* 89 | Toggle the display of the Additional section. 90 | 91 | *--no-answer*, *+*[no]*answer* 92 | Toggle the display of the Answer section. 93 | 94 | *--no-authority*, *+*[no]*authority* 95 | Toggle the display of the Authority section. 96 | 97 | *--no-bad-cookie*, *+*[no]*badcookie* 98 | \[Do not\] ignore BADCOOKIE responses 99 | 100 | *--buffer-size* _int_, *+bufize*=_int_ 101 | Set the UDP message buffer size, using EDNS. 102 | Max is 65535, minimum is zero. 103 | The default value is 1232. 104 | 105 | *--cd*[=_bool_], *+*[no]*cdflag* 106 | (Set, Unset) CD (Checking Disabled) flag. 107 | 108 | *--no-cookie*, *+*[no]*cookie*[=_string_] 109 | Send an EDNS cookie. 110 | This is enabled by default with a random string. 111 | 112 | *-D*, *--dnssec*, *+dnssec*, *+do* 113 | Request DNSSEC records as well. 114 | This sets the DNSSEC OK bit (DO) 115 | 116 | *--dnscrypt*, *+*[no]*dnscrypt* 117 | Use DNSCrypt. 118 | 119 | *--expire*. *+*[no]*expire* 120 | Send an EDNS Expire. 121 | 122 | 123 | *--edns-ver*, *+edns*[=_int_] 124 | Enable EDNS and set EDNS version. 125 | The maximum value is 255, and the minimum (default) value is 0. 126 | 127 | *--no-edns*, *+noedns* 128 | Disable EDNS. 129 | 130 | *-H*, *--https*, *+*[no]*https*[=_endpoint_], *+*[no]*https-post*[=_endpoint_] 131 | Use DNS-over-HTTPS (see RFC 8484). 132 | The default endpoint is _/dns-query_ 133 | 134 | *+*[no]*https-get*[=_endpoint_] 135 | Use an HTTP GET instead of an HTTP POST when making a DNS-over-HTTPS query. 136 | 137 | *+*[no]*idnout* 138 | Converts [or leaves] punycode on output. 139 | Input is automatically translated to punycode. 140 | 141 | *--no-truncate*, *+ignore* 142 | Ignore UDP truncation (by default, awl *retries with TCP*). 143 | 144 | *-j*, *--json*, *+*[no]*json* 145 | Print the query results as JSON. 146 | The result is *not* in compliance with RFC 8427. 147 | 148 | *--keep-alive*, *+*[no]*keepalive*, *+*[no]*keepopen* 149 | Send an EDNS keep-alive. 150 | This does nothing unless using TCP. 151 | 152 | *--nsid*, *+*[no]*nsid* 153 | Send an EDNS name server ID request. 154 | 155 | *--qr*[=_bool_], *+*[no]*qrflag* 156 | Sets the QR (QueRy) flag. 157 | 158 | *--no-question*, *+*[no]*question* 159 | Toggle the display of the Question section. 160 | 161 | *-Q*. *--quic*, *+*[no]*quic* 162 | Use DNS-over-QUIC (see RFC 9250). 163 | 164 | *-s*, *--short*, *+*[no]*short* 165 | Print just the address of the answer. 166 | 167 | *--no-statistics*, *+*[no]*stats* 168 | Toggle the display of the Statistics (additional comments) section. 169 | 170 | *--subnet* _ip_[_/prefix_], *+*[no]*subnet*[=_ip_[_/prefix_]] 171 | Send an EDNS Client Subnet option with the specified address. 172 | 173 | Like *dig*(1), setting the IP to _0.0.0.0/0_, _::/0_ or _0_ will signal the resolver to not use any client information when returning the query. 174 | 175 | *--tc*[=_bool_], *+*[no]*tcflag* 176 | Sets the TC (TrunCated) flag 177 | 178 | *--tcp*, *+*[no]*tcp*, *+*[no]*vc* 179 | Use TCP for the query (see RFC 7766). 180 | 181 | *--timeout* _seconds_, *+timeout*=_seconds_ 182 | Set the timeout period. Floating point numbers are accepted. 183 | 0.5 seconds is the minimum. 184 | 185 | *-T*, *--tls*, *+*[no]*tls* 186 | Use DNS-over-TLS, implies *--tcp* (see RFC 7858) 187 | 188 | *--tls-host* _string_ 189 | Set hostname to use for TLS certificate validation. 190 | Default is the name of the domain when querying over TLS, and empty for IPs. 191 | 192 | *--tls-no-verify* 193 | Ignore TLS validation when performing a DNS query. 194 | 195 | *--trace*, *+trace* 196 | Trace the path of the query from the root, acting like its own resolver. 197 | This option enables DNSSEC. 198 | When *@server* is specified, this will only affect the initial query. 199 | 200 | *--retries* _int_, *+tries*=_int_, *+retry*=_int_ 201 | Set the number of retries. 202 | Retry is one more than tries, dig style. 203 | 204 | *-X*, *--xml*, *+*[no]*xml* 205 | Print the query results as XML. 206 | 207 | *-y*, *--yaml*, *+*[no]*yaml* 208 | Print the query results as YAML. 209 | 210 | *-z*[=_bool_], *+*[no]*zflag* 211 | Sets the Z (Zero) flag. 212 | 213 | *--zflag* _int_, *+ednsflags*=_int_ 214 | Set the must-be-zero EDNS flags. 215 | Decimal, hexadecimal and octal are supported. 216 | Trying to set DO will be ignored. 217 | 218 | # EXIT STATUS 219 | 220 | The exit code is 0 when a query is successfully made and received. 221 | This includes SERVFAILs, NOTIMPL among others. 222 | 223 | # EXAMPLES 224 | 225 | ``` 226 | awl grumbulon.xyz -j +cd 227 | ``` 228 | 229 | Run a query of your local resolver for the A records of grumbulon.xyz, print 230 | them as JSON and disable DNSSEC verification. 231 | 232 | ``` 233 | awl +short example.com AAAA @1.1.1.1 234 | ``` 235 | 236 | Query 1.1.1.1 for the AAAA records of example.com, print just the answers 237 | 238 | ``` 239 | awl -xT PTR 8.8.4.4 @dns.google 240 | ``` 241 | 242 | Query dns.google over TLS for the PTR record to the IP address 8.8.4.4 243 | 244 | # SEE ALSO 245 | 246 | *drill*(1), *dig*(1) 247 | 248 | # STANDARDS 249 | 250 | RFC 1034,1035 (UDP), 7766 (TCP), 7858 (TLS), 8484 (HTTPS), 9230 (QUIC) 251 | 252 | Probably more, _https://www.statdns.com/rfc_ 253 | 254 | # BUGS 255 | 256 | Full parity with *dig*(1) is not complete. 257 | 258 | This man page is probably not complete. 259 | 260 | Likely numerous more, report them either to the tracker 261 | _https://git.froth.zone/sam/awl/issues_ or via email 262 | _~sammefishe/awl-develop@lists.sr.ht_ 263 | -------------------------------------------------------------------------------- /pkg/query/print.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package query 4 | 5 | import ( 6 | "encoding/json" 7 | "encoding/xml" 8 | "errors" 9 | "fmt" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "dns.froth.zone/awl/pkg/util" 15 | "github.com/miekg/dns" 16 | "golang.org/x/net/idna" 17 | "gopkg.in/yaml.v3" 18 | ) 19 | 20 | // ToString turns the response into something that looks a lot like dig 21 | // 22 | // Much of this is taken from https://github.com/miekg/dns/blob/master/msg.go#L900 23 | func ToString(res util.Response, opts *util.Options) (s string, err error) { 24 | if res.DNS == nil { 25 | return " MsgHdr", errNoMessage 26 | } 27 | 28 | var opt *dns.OPT 29 | 30 | if !opts.Short { 31 | if opts.Display.Comments { 32 | s += res.DNS.MsgHdr.String() + " " 33 | s += "QUERY: " + strconv.Itoa(len(res.DNS.Question)) + ", " 34 | s += "ANSWER: " + strconv.Itoa(len(res.DNS.Answer)) + ", " 35 | s += "AUTHORITY: " + strconv.Itoa(len(res.DNS.Ns)) + ", " 36 | s += "ADDITIONAL: " + strconv.Itoa(len(res.DNS.Extra)) + "\n" 37 | opt = res.DNS.IsEdns0() 38 | 39 | if opt != nil && opts.Display.Opt { 40 | // OPT PSEUDOSECTION 41 | s += opt.String() + "\n" 42 | } 43 | } 44 | 45 | if opts.Display.Question { 46 | if len(res.DNS.Question) > 0 { 47 | if opts.Display.Comments { 48 | s += "\n;; QUESTION SECTION:\n" 49 | } 50 | 51 | for _, r := range res.DNS.Question { 52 | str, err := stringParse(r.String(), false, opts) 53 | if err != nil { 54 | return "", fmt.Errorf("%w", err) 55 | } 56 | 57 | s += str + "\n" 58 | } 59 | } 60 | } 61 | 62 | if opts.Display.Answer { 63 | if len(res.DNS.Answer) > 0 { 64 | if opts.Display.Comments { 65 | s += "\n;; ANSWER SECTION:\n" 66 | } 67 | 68 | for _, r := range res.DNS.Answer { 69 | if r != nil { 70 | str, err := stringParse(r.String(), true, opts) 71 | if err != nil { 72 | return "", fmt.Errorf("%w", err) 73 | } 74 | 75 | s += str + "\n" 76 | } 77 | } 78 | } 79 | } 80 | 81 | if opts.Display.Authority { 82 | if len(res.DNS.Ns) > 0 { 83 | if opts.Display.Comments { 84 | s += "\n;; AUTHORITY SECTION:\n" 85 | } 86 | 87 | for _, r := range res.DNS.Ns { 88 | if r != nil { 89 | str, err := stringParse(r.String(), true, opts) 90 | if err != nil { 91 | return "", fmt.Errorf("%w", err) 92 | } 93 | 94 | s += str + "\n" 95 | } 96 | } 97 | } 98 | } 99 | 100 | if opts.Display.Additional { 101 | if len(res.DNS.Extra) > 0 && (opt == nil || len(res.DNS.Extra) > 1) { 102 | if opts.Display.Comments { 103 | s += "\n;; ADDITIONAL SECTION:\n" 104 | } 105 | 106 | for _, r := range res.DNS.Extra { 107 | if r != nil && r.Header().Rrtype != dns.TypeOPT { 108 | str, err := stringParse(r.String(), true, opts) 109 | if err != nil { 110 | return "", fmt.Errorf("%w", err) 111 | } 112 | 113 | s += str + "\n" 114 | } 115 | } 116 | } 117 | } 118 | 119 | if opts.Display.Statistics { 120 | s += "\n;; Query time: " + res.RTT.String() 121 | s += "\n;; SERVER: " + opts.Request.Server + serverExtra(opts) 122 | s += "\n;; WHEN: " + time.Now().Format(time.RFC1123Z) 123 | s += "\n;; MSG SIZE rcvd: " + strconv.Itoa(res.DNS.Len()) + "\n" 124 | } 125 | } else { 126 | // Print just the responses, nothing else 127 | for i, resp := range res.DNS.Answer { 128 | temp := strings.Split(resp.String(), "\t") 129 | s += temp[len(temp)-1] 130 | 131 | if opts.Identify { 132 | s += " from server " + opts.Request.Server + " in " + res.RTT.String() 133 | } 134 | 135 | // Don't print newline on last line 136 | if i != len(res.DNS.Answer)-1 { 137 | s += "\n" 138 | } 139 | } 140 | } 141 | 142 | return 143 | } 144 | 145 | func serverExtra(opts *util.Options) string { 146 | switch { 147 | case opts.TCP: 148 | return " (TCP)" 149 | case opts.TLS: 150 | return " (TLS)" 151 | case opts.HTTPS, opts.DNSCrypt: 152 | return "" 153 | case opts.QUIC: 154 | return " (QUIC)" 155 | default: 156 | return " (UDP)" 157 | } 158 | } 159 | 160 | // stringParse edits the raw responses to user requests. 161 | func stringParse(str string, isAns bool, opts *util.Options) (string, error) { 162 | split := strings.Split(str, "\t") 163 | 164 | // Make edits if so requested 165 | 166 | // TODO: make less ew? 167 | // This exists because the question section should be left alone EXCEPT for punycode. 168 | 169 | if isAns { 170 | if !opts.Display.TTL { 171 | // Remove from existence 172 | split = append(split[:1], split[2:]...) 173 | } 174 | 175 | if !opts.Display.ShowClass { 176 | // Position depends on if the TTL is there or not. 177 | if opts.Display.TTL { 178 | split = append(split[:2], split[3:]...) 179 | } else { 180 | split = append(split[:1], split[2:]...) 181 | } 182 | } 183 | 184 | if opts.Display.TTL && opts.Display.HumanTTL { 185 | ttl, _ := strconv.Atoi(split[1]) 186 | split[1] = (time.Duration(ttl) * time.Second).String() 187 | } 188 | } 189 | 190 | if opts.Display.UcodeTranslate { 191 | var ( 192 | err error 193 | semi string 194 | ) 195 | 196 | if strings.HasPrefix(split[0], ";") { 197 | split[0] = strings.TrimPrefix(split[0], ";") 198 | semi = ";" 199 | } 200 | 201 | split[0], err = idna.ToUnicode(split[0]) 202 | if err != nil { 203 | return "", fmt.Errorf("punycode: %w", err) 204 | } 205 | 206 | split[0] = semi + split[0] 207 | } 208 | 209 | return strings.Join(split, "\t"), nil 210 | } 211 | 212 | // PrintSpecial is for printing as JSON, XML or YAML. 213 | // As of now JSON and XML use the stdlib version. 214 | func PrintSpecial(res util.Response, opts *util.Options) (string, error) { 215 | formatted, err := MakePrintable(res, opts) 216 | if err != nil { 217 | return "", err 218 | } 219 | 220 | switch { 221 | case opts.JSON: 222 | opts.Logger.Info("Printing as JSON") 223 | 224 | json, err := json.MarshalIndent(formatted, " ", " ") 225 | 226 | return string(json), err 227 | case opts.XML: 228 | opts.Logger.Info("Printing as XML") 229 | 230 | xml, err := xml.MarshalIndent(formatted, " ", " ") 231 | 232 | return string(xml), err 233 | case opts.YAML: 234 | opts.Logger.Info("Printing as YAML") 235 | 236 | yaml, err := yaml.Marshal(formatted) 237 | 238 | return string(yaml), err 239 | default: 240 | return "", errInvalidFormat 241 | } 242 | } 243 | 244 | // MakePrintable takes a DNS message and makes it nicer to be printed as JSON,YAML, 245 | // and XML. Little is changed beyond naming. 246 | func MakePrintable(res util.Response, opts *util.Options) (*Message, error) { 247 | var ( 248 | err error 249 | msg = res.DNS 250 | ) 251 | // The things I do for compatibility 252 | ret := &Message{ 253 | DateString: time.Now().Format(time.RFC3339), 254 | DateSeconds: time.Now().Unix(), 255 | MsgSize: res.DNS.Len(), 256 | ID: msg.Id, 257 | Opcode: msg.Opcode, 258 | Response: msg.Response, 259 | 260 | Authoritative: msg.Authoritative, 261 | Truncated: msg.Truncated, 262 | RecursionDesired: msg.RecursionDesired, 263 | RecursionAvailable: msg.RecursionAvailable, 264 | AuthenticatedData: msg.AuthenticatedData, 265 | CheckingDisabled: msg.CheckingDisabled, 266 | Zero: msg.Zero, 267 | 268 | QdCount: len(msg.Question), 269 | AnCount: len(msg.Answer), 270 | NsCount: len(msg.Ns), 271 | ArCount: len(msg.Extra), 272 | } 273 | 274 | opt := msg.IsEdns0() 275 | if opt != nil && opts.Display.Opt { 276 | ret.EDNS0, err = ret.ParseOpt(msg.Rcode, *opt) 277 | if err != nil { 278 | return nil, fmt.Errorf("edns print: %w", err) 279 | } 280 | } 281 | 282 | if opts.Display.Question { 283 | err = ret.displayQuestion(msg, opts, opt) 284 | if err != nil { 285 | return nil, fmt.Errorf("unable to display questions: %w", err) 286 | } 287 | } 288 | 289 | if opts.Display.Answer { 290 | err = ret.displayAnswers(msg, opts, opt) 291 | if err != nil { 292 | return nil, fmt.Errorf("unable to display answers: %w", err) 293 | } 294 | } 295 | 296 | if opts.Display.Authority { 297 | err = ret.displayAuthority(msg, opts, opt) 298 | if err != nil { 299 | return nil, fmt.Errorf("unable to display authority: %w", err) 300 | } 301 | } 302 | 303 | if opts.Display.Additional { 304 | err = ret.displayAdditional(msg, opts, opt) 305 | if err != nil { 306 | return nil, fmt.Errorf("unable to display additional: %w", err) 307 | } 308 | } 309 | 310 | return ret, nil 311 | } 312 | 313 | var errInvalidFormat = errors.New("this should never happen") 314 | -------------------------------------------------------------------------------- /cmd/cli.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | 3 | package cli 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "runtime" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "dns.froth.zone/awl/pkg/util" 14 | "github.com/miekg/dns" 15 | flag "github.com/stefansundin/go-zflag" 16 | ) 17 | 18 | // ParseCLI parses arguments given from the CLI and passes them into an `Options` 19 | // struct. 20 | func ParseCLI(args []string, version string) (opts *util.Options, err error) { 21 | // Parse the standard flags 22 | opts, misc, err := parseFlags(args, version) 23 | if err != nil { 24 | return opts, err 25 | } 26 | 27 | // Parse all the arguments that don't start with - or -- 28 | // This includes the dig-style (+) options 29 | err = ParseMiscArgs(misc, opts) 30 | if err != nil { 31 | return opts, err 32 | } 33 | 34 | opts.Logger.Info("Dig/Drill flags parsed") 35 | opts.Logger.Debug(fmt.Sprintf("%+v", opts)) 36 | 37 | // Special options and exceptions time 38 | 39 | if opts.Request.Port == 0 { 40 | if opts.TLS || opts.QUIC { 41 | opts.Request.Port = 853 42 | } else { 43 | opts.Request.Port = 53 44 | } 45 | } 46 | 47 | opts.Logger.Info("Port set to", opts.Request.Port) 48 | 49 | // Set timeout to 0.5 seconds if set below 0.5 50 | if opts.Request.Timeout < (time.Second / 2) { 51 | opts.Request.Timeout = (time.Second / 2) 52 | } 53 | 54 | if opts.Request.Retries < 0 { 55 | opts.Request.Retries = 0 56 | } 57 | 58 | if opts.Trace { 59 | if opts.TLS || opts.HTTPS || opts.QUIC { 60 | opts.Logger.Warn("Every query after the root query will only use UDP/TCP") 61 | } 62 | 63 | opts.RD = true 64 | } 65 | 66 | opts.Logger.Info("Options fully populated") 67 | opts.Logger.Debug(fmt.Sprintf("%+v", opts)) 68 | 69 | return 70 | } 71 | 72 | // Everything that has to do with CLI flags goes here (the posix style, eg. -a and --bbbb). 73 | func parseFlags(args []string, version string) (opts *util.Options, flags []string, err error) { 74 | flagSet := flag.NewFlagSet(args[0], flag.ContinueOnError) 75 | 76 | flagSet.Usage = func() { 77 | fmt.Println(`awl - drill, writ small 78 | 79 | Usage: awl name [@server] [record] 80 | domain, IP address, phone number 81 | defaults to A 82 | 83 | Arguments may be in any order, including flags. 84 | Dig-like +[no]commands are also supported, see dig(1) or dig -h 85 | 86 | Options:`) 87 | flagSet.PrintDefaults() 88 | } 89 | 90 | // CLI flags 91 | // 92 | // Remember, when adding a flag edit the manpage and the completions :) 93 | var ( 94 | port = flagSet.Int("port", 0, "`port` to make DNS query (default: 53 for UDP/TCP, 853 for TLS/QUIC)", flag.OptShorthand('p'), flag.OptDisablePrintDefault(true)) 95 | query = flagSet.String("query", "", "domain name to `query` (default: .)", flag.OptShorthand('q')) 96 | class = flagSet.String("class", "IN", "DNS `class` to query", flag.OptShorthand('c')) 97 | qType = flagSet.String("qType", "", "`type` to query (default: A)", flag.OptShorthand('t')) 98 | 99 | ipv4 = flagSet.Bool("4", false, "force IPv4", flag.OptShorthand('4')) 100 | ipv6 = flagSet.Bool("6", false, "force IPv6", flag.OptShorthand('6')) 101 | reverse = flagSet.Bool("reverse", false, "do a reverse lookup", flag.OptShorthand('x')) 102 | trace = flagSet.Bool("trace", false, "trace from the root") 103 | 104 | timeout = flagSet.Float32("timeout", 5, "Timeout, in `seconds`") 105 | retry = flagSet.Int("retries", 2, "number of `times` to retry") 106 | 107 | edns = flagSet.Bool("no-edns", false, "disable EDNS entirely") 108 | ednsVer = flagSet.Uint8("edns-ver", 0, "set EDNS version") 109 | dnssec = flagSet.Bool("dnssec", false, "enable DNSSEC", flag.OptShorthand('D')) 110 | expire = flagSet.Bool("expire", false, "set EDNS expire") 111 | nsid = flagSet.Bool("nsid", false, "set EDNS NSID", flag.OptShorthand('n')) 112 | cookie = flagSet.Bool("no-cookie", false, "disable sending EDNS cookie (default: cookie sent)") 113 | tcpKeepAlive = flagSet.Bool("keep-alive", false, "send EDNS TCP keep-alive") 114 | udpBufSize = flagSet.Uint16("buffer-size", 1232, "set EDNS UDP buffer size", flag.OptShorthand('b')) 115 | mbzflag = flagSet.String("zflag", "0", "set EDNS z-flag `value`") 116 | subnet = flagSet.String("subnet", "", "set EDNS client subnet") 117 | padding = flagSet.Bool("pad", false, "set EDNS padding") 118 | 119 | badCookie = flagSet.Bool("no-bad-cookie", false, "ignore BADCOOKIE EDNS responses (default: retry with correct cookie") 120 | truncate = flagSet.Bool("no-truncate", false, "ignore truncation if a UDP request truncates (default: retry with TCP)") 121 | 122 | tcp = flagSet.Bool("tcp", false, "use TCP") 123 | dnscrypt = flagSet.Bool("dnscrypt", false, "use DNSCrypt") 124 | tls = flagSet.Bool("tls", false, "use DNS-over-TLS", flag.OptShorthand('T')) 125 | https = flagSet.Bool("https", false, "use DNS-over-HTTPS", flag.OptShorthand('H')) 126 | quic = flagSet.Bool("quic", false, "use DNS-over-QUIC", flag.OptShorthand('Q')) 127 | 128 | tlsHost = flagSet.String("tls-host", "", "Server name to use for TLS verification") 129 | noVerify = flagSet.Bool("tls-no-verify", false, "Disable TLS cert verification") 130 | 131 | aaflag = flagSet.Bool("aa", false, "set/unset AA (Authoratative Answer) flag (default: not set)") 132 | adflag = flagSet.Bool("ad", false, "set/unset AD (Authenticated Data) flag (default: not set)") 133 | cdflag = flagSet.Bool("cd", false, "set/unset CD (Checking Disabled) flag (default: not set)") 134 | qrflag = flagSet.Bool("qr", false, "set/unset QR (QueRy) flag (default: not set)") 135 | rdflag = flagSet.Bool("rd", true, "set/unset RD (Recursion Desired) flag (default: set)", flag.OptDisablePrintDefault(true)) 136 | raflag = flagSet.Bool("ra", false, "set/unset RA (Recursion Available) flag (default: not set)") 137 | tcflag = flagSet.Bool("tc", false, "set/unset TC (TrunCated) flag (default: not set)") 138 | zflag = flagSet.Bool("z", false, "set/unset Z (Zero) flag (default: not set)", flag.OptShorthand('z')) 139 | 140 | short = flagSet.Bool("short", false, "print just the results", flag.OptShorthand('s')) 141 | json = flagSet.Bool("json", false, "print the result(s) as JSON", flag.OptShorthand('j')) 142 | xml = flagSet.Bool("xml", false, "print the result(s) as XML", flag.OptShorthand('X')) 143 | yaml = flagSet.Bool("yaml", false, "print the result(s) as yaml", flag.OptShorthand('y')) 144 | 145 | noC = flagSet.Bool("no-comments", false, "disable printing the comments") 146 | noQ = flagSet.Bool("no-question", false, "disable printing the question section") 147 | noOpt = flagSet.Bool("no-opt", false, "disable printing the OPT pseudosection") 148 | noAns = flagSet.Bool("no-answer", false, "disable printing the answer section") 149 | noAuth = flagSet.Bool("no-authority", false, "disable printing the authority section") 150 | noAdd = flagSet.Bool("no-additional", false, "disable printing the additional section") 151 | noStats = flagSet.Bool("no-statistics", false, "disable printing the statistics section") 152 | 153 | verbosity = flagSet.Int("verbosity", 1, "sets verbosity `level`", flag.OptShorthand('v'), flag.OptNoOptDefVal("2")) 154 | versionFlag = flagSet.Bool("version", false, "print version information", flag.OptShorthand('V')) 155 | ) 156 | 157 | // Don't sort the flags when -h is given 158 | flagSet.SortFlags = true 159 | 160 | // Parse the flags 161 | if err = flagSet.Parse(args[1:]); err != nil { 162 | return &util.Options{Logger: util.InitLogger(*verbosity)}, nil, fmt.Errorf("flag: %w", err) 163 | } 164 | 165 | // TODO: DRY, dumb dumb. 166 | mbz, err := strconv.ParseInt(*mbzflag, 0, 16) 167 | if err != nil { 168 | return &util.Options{Logger: util.InitLogger(*verbosity)}, nil, fmt.Errorf("EDNS MBZ: %w", err) 169 | } 170 | 171 | opts = &util.Options{ 172 | Logger: util.InitLogger(*verbosity), 173 | IPv4: *ipv4, 174 | IPv6: *ipv6, 175 | Trace: *trace, 176 | Short: *short, 177 | TCP: *tcp, 178 | DNSCrypt: *dnscrypt, 179 | TLS: *tls, 180 | TLSHost: *tlsHost, 181 | TLSNoVerify: *noVerify, 182 | HTTPS: *https, 183 | QUIC: *quic, 184 | Truncate: *truncate, 185 | BadCookie: *badCookie, 186 | Reverse: *reverse, 187 | JSON: *json, 188 | XML: *xml, 189 | YAML: *yaml, 190 | HeaderFlags: util.HeaderFlags{ 191 | AA: *aaflag, 192 | AD: *adflag, 193 | TC: *tcflag, 194 | Z: *zflag, 195 | CD: *cdflag, 196 | QR: *qrflag, 197 | RD: *rdflag, 198 | RA: *raflag, 199 | }, 200 | Request: util.Request{ 201 | Type: dns.StringToType[strings.ToUpper(*qType)], 202 | Class: dns.StringToClass[strings.ToUpper(*class)], 203 | Name: *query, 204 | Timeout: time.Duration(*timeout * float32(time.Second)), 205 | Retries: *retry, 206 | Port: *port, 207 | }, 208 | Display: util.Display{ 209 | Comments: !*noC, 210 | Question: !*noQ, 211 | Opt: !*noOpt, 212 | Answer: !*noAns, 213 | Authority: !*noAuth, 214 | Additional: !*noAdd, 215 | Statistics: !*noStats, 216 | TTL: true, 217 | ShowClass: true, 218 | ShowQuery: false, 219 | HumanTTL: false, 220 | UcodeTranslate: true, 221 | }, 222 | EDNS: util.EDNS{ 223 | EnableEDNS: !*edns, 224 | Cookie: !*cookie, 225 | DNSSEC: *dnssec, 226 | BufSize: *udpBufSize, 227 | Version: *ednsVer, 228 | Expire: *expire, 229 | KeepOpen: *tcpKeepAlive, 230 | Nsid: *nsid, 231 | ZFlag: uint16(mbz & 0x7FFF), 232 | Padding: *padding, 233 | }, 234 | HTTPSOptions: util.HTTPSOptions{ 235 | Endpoint: "/dns-query", 236 | Get: false, 237 | }, 238 | } 239 | 240 | // TODO: DRY 241 | if *subnet != "" { 242 | if err = util.ParseSubnet(*subnet, opts); err != nil { 243 | return opts, nil, fmt.Errorf("%w", err) 244 | } 245 | } 246 | 247 | opts.Logger.Info("POSIX flags parsed") 248 | opts.Logger.Debug(fmt.Sprintf("%+v", opts)) 249 | 250 | if *versionFlag { 251 | fmt.Printf("awl version %s, built with %s\n", version, runtime.Version()) 252 | 253 | return opts, nil, util.ErrNotError 254 | } 255 | 256 | flags = flagSet.Args() 257 | 258 | return 259 | } 260 | 261 | var errNoArg = errors.New("no argument given") 262 | 263 | type errInvalidArg struct { 264 | arg string 265 | } 266 | 267 | func (e *errInvalidArg) Error() string { 268 | return fmt.Sprintf("digflags: invalid argument %s", e.arg) 269 | } 270 | --------------------------------------------------------------------------------