Another "minor refactor" #61

Merged
sam merged 8 commits from more-stuff into master 2022-08-11 07:25:37 +00:00
66 changed files with 1569 additions and 1223 deletions

View file

@ -26,7 +26,17 @@ local testing(version, arch) = {
name: "test",
image: "golang:" + version,
commands: [
"go test -v -race ./... -cover"
"make test-ci"
],
depends_on: [
"submodules",
],
},
{
name: "fuzz",
image: "golang:" + version,
commands: [
"make fuzz",
],
depends_on: [
"submodules",
@ -65,7 +75,7 @@ local release() = {
name: "test",
image: "golang",
commands: [
"go test -race ./... -cover"
"make test"
]
},
{
@ -89,6 +99,5 @@ local release() = {
testing("1.18", "amd64"),
testing("1.18", "arm64"),
release()
]

View file

@ -1,7 +1,6 @@
---
name: Bug report
about: Report a bug
---
**Describe the bug**
@ -9,6 +8,7 @@ A clear and concise description of what the bug is.
**Reproduction steps**
Steps to reproduce the behavior:
1. 1
2. 2
3. Bug
@ -20,9 +20,9 @@ A clear and concise description of what you expected to happen.
Add `-v=4` and add the debug logs to the report here:
**System information (please complete the following information):**
- OS: [e.g. Ubuntu 22.04, OpenBSD, Windows 11]
- Version: [run `awl -V` and print the output]
- OS: [e.g. Ubuntu 22.04, OpenBSD, Windows 11]
- Version: [run `awl -V` and print the output]
**Additional context**
Add any other context about the problem here.
Add any other context about the problem here.

View file

@ -1,7 +1,6 @@
---
name: Feature request
about: Suggest a feature
---
**Is your feature request related to a problem? Please describe.**
@ -15,4 +14,4 @@ A clear and concise description of any alternative solutions or features you've
Links to implementations in dig, drill, etc. should go here.
**Additional context**
Add any other context or screenshots about the feature request here.
Add any other context or screenshots about the feature request here.

View file

@ -6,4 +6,4 @@ Please check the following:
2. Make sure that you test and format your contributions: `make test && make lint`
3. Describe what your pull request does and which issue you're targeting (if any)
-->
-->

27
.github/workflows/ghrelease.yaml vendored Normal file
View file

@ -0,0 +1,27 @@
name: github-release
on:
push:
tags:
- "*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Checkout submodules
uses: git submodule update --init --recursive
- name: Release with GoReleaser
uses: goreleaser/goreleaser-action@v3
with:
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

24
.github/workflows/test.yaml vendored Normal file
View file

@ -0,0 +1,24 @@
on: push
jobs:
test-gha:
strategy:
fail-fast: true
matrix:
platform: [macos-latest, windows-latest]
goVer: [1.18 1.19]
runs-on: ${{ matrix.platform }}
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.goVer }}
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout submodules
uses: git submodule update --init --recursive
- name: Test
run: go test -race -v ./...

26
.github/workflows/wiki.yaml vendored Normal file
View file

@ -0,0 +1,26 @@
name: Deploy Wiki
on:
push:
paths:
# Trigger only when wiki directory changes
- "doc/wiki/**"
jobs:
deploy-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout submodules
uses: git submodule update --init --recursive
- name: Push Wiki Changes
uses: Andrew-Chen-Wang/github-wiki-action@v3
env:
# Make sure you have that / at the end. We use rsync
# WIKI_DIR's default is wiki/
WIKI_DIR: doc/wiki/
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_MAIL: ${{ secrets.YOUR_EMAIL }}
GH_NAME: ${{ github.repository_owner }}

53
.gitignore vendored
View file

@ -1,27 +1,28 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
dist/
# Test coverage
coverage/*
!coverage/.gitkeep
awl
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
dist/
# Test coverage
coverage/*
!coverage/.gitkeep
awl
doc/awl.1
.dccache

77
.golangci.yaml Normal file
View file

@ -0,0 +1,77 @@
# Refer to golangci-lint's example config file for more options and information:
# https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml
run:
timeout: 5m
modules-download-mode: readonly
skip-dirs:
- "coverage"
- ".github"
linters:
enable:
- errcheck
- errorlint
- gci
- gocritic
- goconst
- godot
- goimports
- govet
- gocritic
- goerr113
- gofmt
- gofumpt
- gosec
- maintidx
- makezero
- misspell
- nlreturn
- nolintlint
- prealloc
- predeclared
- revive
- staticcheck
# - testpackage
- whitespace
- wrapcheck
- wsl
linters-settings:
govet:
check-shadowing: true
enable-all: true
disable-all: false
revive:
ignore-generated-header: false
severity: warning
confidence: 0.8
errorCode: 1
warningCode: 1
rules:
# rules can be uncommented after https://github.com/golangci/golangci-lint/issues/2997 is fixed
- name: blank-imports
- name: context-as-argument
# - name: context-keys-type
- name: dot-imports
- name: duplicated-imports
- name: error-return
- name: error-strings
- name: error-naming
# - name: errorf
- name: exported
- name: if-return
- name: increment-decrement
# - name: modifies-value-receiver
- name: package-comments
- name: range
- name: receiver-naming
# - name: time-naming
# - name: unexported-return
# - name: var-declaration
- name: var-naming
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0

View file

@ -17,7 +17,7 @@ builds:
- amd64
- arm64
universal_binaries:
- replace: true
- replace: true
archives:
- replacements:
darwin: macOS
@ -28,14 +28,14 @@ archives:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- "^docs:"
- "^test:"
gitea_urls:
api: https://git.froth.zone/api/v1/
api: https://git.froth.zone/api/v1/

View file

@ -6,10 +6,10 @@ ifeq ($(OS),Windows_NT)
EXE := $(PROG).exe
else
EXE := $(PROG)
endif
$(PROG):
$(PROG): $(SOURCES)
$(GO) build -o $(EXE) $(GOFLAGS) .
@ -19,8 +19,8 @@ install:
$(GO) install $(GOFLAGS) .
else
install: all
install -m755 $(PROG) $(PREFIX)/$(BIN)
install -m644 doc/$(PROG).1 $(MAN)/man1
install -Dm755 $(PROG) $(DESTDIR)$(PREFIX)/$(BIN)/$(PROG)
install -Dm644 doc/$(PROG).1 $(DESTDIR)$(MAN)/man1/$(PROG).1
endif
.PHONY: install

View file

@ -3,12 +3,12 @@
include template.mk
$(PROG):
$(PROG): $(SOURCES)
$(GO) build -o $(PROG) $(GOFLAGS) .
## install: installs awl and the manpage, RUN AS ROOT
install: all
install -m755 $(PROG) $(PREFIX)/$(BIN)
install -m644 doc/$(PROG).1 $(MAN)/man1
install -Dm755 $(PROG) $(DESTDIR)$(PREFIX)/$(BIN)/$(PROG)
install -Dm644 doc/$(PROG).1 $(DESTDIR)$(MAN)/man1/$(PROG).1
.PHONY: install

View file

@ -17,7 +17,6 @@ newer RFC query types, such as DNS-over-HTTPS and DNS-over-QUIC.
- [Feature wiki](https://git.froth.zone/sam/awl/wiki/Supported)
- [Manpage](https://git.froth.zone/sam/awl/wiki/awl.1)
## Building and installing
### From releases
@ -31,7 +30,6 @@ Dependencies:
- Go >= 1.18
- GNU/BSD make or Plan 9 mk
- [scdoc](https://git.sr.ht/~sircmpwn/scdoc) (optional, for manpage)
Make sure to recursively clone the repo:

View file

@ -3,6 +3,7 @@
package cli
import (
"errors"
"fmt"
"os"
"runtime"
@ -10,7 +11,6 @@ import (
"strings"
"time"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
flag "github.com/stefansundin/go-zflag"
@ -18,7 +18,7 @@ import (
// ParseCLI parses arguments given from the CLI and passes them into an `Options`
// struct.
func ParseCLI(version string) (Options, error) {
func ParseCLI(version string) (util.Options, error) {
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
flag.Usage = func() {
@ -57,7 +57,7 @@ func ParseCLI(version string) (Options, error) {
cookie = flag.Bool("no-cookie", false, "disable sending EDNS cookie (default: cookie sent)")
tcpKeepAlive = flag.Bool("keep-alive", false, "send EDNS TCP keep-alive")
udpBufSize = flag.Uint16("buffer-size", 1232, "set EDNS UDP buffer size", flag.OptShorthand('b'))
zflag = flag.String("zflag", "0", "set EDNS z-flag")
mbzflag = flag.String("zflag", "0", "set EDNS z-flag")
subnet = flag.String("subnet", "", "set EDNS subnet")
padding = flag.Bool("pad", false, "set EDNS padding")
@ -69,14 +69,17 @@ func ParseCLI(version string) (Options, error) {
https = flag.Bool("https", false, "use DNS-over-HTTPS", flag.OptShorthand('H'))
quic = flag.Bool("quic", false, "use DNS-over-QUIC", flag.OptShorthand('Q'))
aa = flag.Bool("aa", false, "set/unset AA (Authoratative Answer) flag (default: not set)")
ad = flag.Bool("ad", false, "set/unset AD (Authenticated Data) flag (default: not set)")
cd = flag.Bool("cd", false, "set/unset CD (Checking Disabled) flag (default: not set)")
qr = flag.Bool("qr", false, "set/unset QR (QueRy) flag (default: not set)")
rd = flag.Bool("rd", true, "set/unset RD (Recursion Desired) flag (default: set)", flag.OptDisablePrintDefault(true))
ra = flag.Bool("ra", false, "set/unset RA (Recursion Available) flag (default: not set)")
tc = flag.Bool("tc", false, "set/unset TC (TrunCated) flag (default: not set)")
z = flag.Bool("z", false, "set/unset Z (Zero) flag (default: not set)", flag.OptShorthand('z'))
tlsHost = flag.String("tls-host", "", "Server name to use for TLS verification")
noVerify = flag.Bool("tls-no-verify", false, "Disable TLS cert verification")
aaflag = flag.Bool("aa", false, "set/unset AA (Authoratative Answer) flag (default: not set)")
adflag = flag.Bool("ad", false, "set/unset AD (Authenticated Data) flag (default: not set)")
cdflag = flag.Bool("cd", false, "set/unset CD (Checking Disabled) flag (default: not set)")
qrflag = flag.Bool("qr", false, "set/unset QR (QueRy) flag (default: not set)")
rdflag = flag.Bool("rd", true, "set/unset RD (Recursion Desired) flag (default: set)", flag.OptDisablePrintDefault(true))
raflag = flag.Bool("ra", false, "set/unset RA (Recursion Available) flag (default: not set)")
tcflag = flag.Bool("tc", false, "set/unset TC (TrunCated) flag (default: not set)")
zflag = flag.Bool("z", false, "set/unset Z (Zero) flag (default: not set)", flag.OptShorthand('z'))
short = flag.Bool("short", false, "print just the results", flag.OptShorthand('s'))
json = flag.Bool("json", false, "print the result(s) as JSON", flag.OptShorthand('j'))
@ -99,52 +102,53 @@ func ParseCLI(version string) (Options, error) {
flag.CommandLine.SortFlags = false
// Parse the flags
err := flag.CommandLine.Parse(os.Args[1:])
if err != nil {
return Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("flag parse error: %w", err)
if err := flag.CommandLine.Parse(os.Args[1:]); err != nil {
return util.Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("flag: %w", err)
}
// TODO: DRY, dumb dumb.
mbz, err := strconv.ParseInt(*zflag, 0, 16)
mbz, err := strconv.ParseInt(*mbzflag, 0, 16)
if err != nil {
return Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("EDNS MBZ error: %w", err)
return util.Options{Logger: util.InitLogger(*verbosity)}, fmt.Errorf("EDNS MBZ: %w", err)
}
opts := Options{
Logger: util.InitLogger(*verbosity),
Port: *port,
IPv4: *ipv4,
IPv6: *ipv6,
Short: *short,
TCP: *tcp,
DNSCrypt: *dnscrypt,
TLS: *tls,
HTTPS: *https,
QUIC: *quic,
Truncate: *truncate,
ShowQuery: false,
AA: *aa,
AD: *ad,
TC: *tc,
Z: *z,
CD: *cd,
QR: *qr,
RD: *rd,
RA: *ra,
Reverse: *reverse,
HumanTTL: false,
ShowTTL: true,
JSON: *json,
XML: *xml,
YAML: *yaml,
Request: helpers.Request{
opts := util.Options{
Logger: util.InitLogger(*verbosity),
Port: *port,
IPv4: *ipv4,
IPv6: *ipv6,
Short: *short,
TCP: *tcp,
DNSCrypt: *dnscrypt,
TLS: *tls,
TLSHost: *tlsHost,
TLSNoVerify: *noVerify,
HTTPS: *https,
QUIC: *quic,
Truncate: *truncate,
ShowQuery: false,
AA: *aaflag,
AD: *adflag,
TC: *tcflag,
Z: *zflag,
CD: *cdflag,
QR: *qrflag,
RD: *rdflag,
RA: *raflag,
Reverse: *reverse,
HumanTTL: false,
ShowTTL: true,
JSON: *json,
XML: *xml,
YAML: *yaml,
Request: util.Request{
Type: dns.StringToType[strings.ToUpper(*qType)],
Class: dns.StringToClass[strings.ToUpper(*class)],
Name: *query,
Timeout: time.Duration(*timeout * float32(time.Second)),
Retries: *retry,
},
Display: Displays{
Display: util.Displays{
Comments: !*noC,
Question: !*noQ,
Opt: !*noOpt,
@ -153,7 +157,7 @@ func ParseCLI(version string) (Options, error) {
Additional: !*noAdd,
Statistics: !*noStats,
},
EDNS: EDNS{
EDNS: util.EDNS{
EnableEDNS: !*edns,
Cookie: !*cookie,
DNSSEC: *dnssec,
@ -169,9 +173,8 @@ func ParseCLI(version string) (Options, error) {
// TODO: DRY
if *subnet != "" {
err := parseSubnet(*subnet, &opts)
if err != nil {
return opts, err
if err = util.ParseSubnet(*subnet, &opts); err != nil {
return opts, fmt.Errorf("%w", err)
}
}
@ -180,6 +183,7 @@ func ParseCLI(version string) (Options, error) {
if *versionFlag {
fmt.Printf("awl version %s, built with %s\n", version, runtime.Version())
return opts, ErrNotError
}
@ -189,6 +193,7 @@ func ParseCLI(version string) (Options, error) {
if err != nil {
return opts, err
}
opts.Logger.Info("Dig/Drill flags parsed")
opts.Logger.Debug(fmt.Sprintf("%+v", opts))
@ -199,6 +204,7 @@ func ParseCLI(version string) (Options, error) {
opts.Port = 53
}
}
opts.Logger.Info("Port set to", opts.Port)
// Set timeout to 0.5 seconds if set below 0.5
@ -212,3 +218,16 @@ func ParseCLI(version string) (Options, error) {
return opts, nil
}
// ErrNotError is for returning not error.
var ErrNotError = errors.New("not an error")
var errNoArg = errors.New("no argument given")
type errInvalidArg struct {
arg string
}
func (e *errInvalidArg) Error() string {
return fmt.Sprintf("digflags: invalid argument %s", e.arg)
}

View file

@ -11,77 +11,111 @@ import (
"gotest.tools/v3/assert"
)
// nolint: paralleltest
func TestEmpty(t *testing.T) {
old := os.Args
os.Args = []string{"awl", "-4"}
opts, err := cli.ParseCLI("TEST")
assert.NilError(t, err)
assert.Equal(t, opts.Port, 53)
assert.Assert(t, opts.IPv4)
os.Args = old
}
// nolint: paralleltest
func TestTLSPort(t *testing.T) {
old := os.Args
os.Args = []string{"awl", "-T"}
opts, err := cli.ParseCLI("TEST")
assert.NilError(t, err)
assert.Equal(t, opts.Port, 853)
os.Args = old
}
// nolint: paralleltest
func TestSubnet(t *testing.T) {
old := os.Args
os.Args = []string{"awl", "--subnet", "127.0.0.1/32"}
opts, err := cli.ParseCLI("TEST")
assert.NilError(t, err)
assert.Equal(t, opts.EDNS.Subnet.Family, uint16(1))
os.Args = old
os.Args = []string{"awl", "--subnet", "0"}
opts, err = cli.ParseCLI("TEST")
assert.NilError(t, err)
assert.Equal(t, opts.EDNS.Subnet.Family, uint16(1))
os.Args = old
os.Args = []string{"awl", "--subnet", "::/0"}
opts, err = cli.ParseCLI("TEST")
assert.NilError(t, err)
assert.Equal(t, opts.EDNS.Subnet.Family, uint16(2))
os.Args = old
os.Args = []string{"awl", "--subnet", "/"}
opts, err = cli.ParseCLI("TEST")
assert.ErrorContains(t, err, "EDNS subnet")
os.Args = old
}
// nolint: paralleltest
func TestInvalidFlag(t *testing.T) {
func TestMBZ(t *testing.T) { //nolint: paralleltest // Race conditions
old := os.Args
os.Args = []string{"awl", "--zflag", "G"}
_, err := cli.ParseCLI("TEST")
assert.ErrorContains(t, err, "EDNS MBZ")
os.Args = old
}
func TestInvalidFlag(t *testing.T) { //nolint: paralleltest // Race conditions
old := os.Args
os.Args = []string{"awl", "--treebug"}
_, err := cli.ParseCLI("TEST")
assert.ErrorContains(t, err, "unknown flag")
os.Args = old
}
// nolint: paralleltest
func TestInvalidDig(t *testing.T) {
func TestInvalidDig(t *testing.T) { //nolint: paralleltest // Race conditions
old := os.Args
os.Args = []string{"awl", "+a"}
_, err := cli.ParseCLI("TEST")
assert.ErrorContains(t, err, "digflags: invalid argument")
os.Args = old
}
// nolint: paralleltest
func TestVersion(t *testing.T) {
func TestVersion(t *testing.T) { //nolint: paralleltest // Race conditions
old := os.Args
os.Args = []string{"awl", "--version"}
_, err := cli.ParseCLI("TEST")
assert.ErrorType(t, err, cli.ErrNotError)
os.Args = old
}
// nolint: paralleltest
func TestTimeout(t *testing.T) {
func TestTimeout(t *testing.T) { //nolint: paralleltest // Race conditions
args := [][]string{
{"awl", "+timeout=0"},
{"awl", "--timeout", "0"},
@ -89,15 +123,17 @@ func TestTimeout(t *testing.T) {
for _, test := range args {
old := os.Args
os.Args = test
opt, err := cli.ParseCLI("TEST")
assert.NilError(t, err)
assert.Equal(t, opt.Request.Timeout, time.Second/2)
os.Args = old
}
}
// nolint: paralleltest
func TestRetries(t *testing.T) {
func TestRetries(t *testing.T) { //nolint: paralleltest // Race conditions
args := [][]string{
{"awl", "+retry=-2"},
{"awl", "+tries=-2"},
@ -106,14 +142,16 @@ func TestRetries(t *testing.T) {
for _, test := range args {
old := os.Args
os.Args = test
opt, err := cli.ParseCLI("TEST")
assert.NilError(t, err)
assert.Equal(t, opt.Request.Retries, 0)
os.Args = old
}
}
// nolint: paralleltest
func FuzzFlags(f *testing.F) {
testcases := []string{"git.froth.zone", "", "!12345", "google.com.edu.org.fr"}
for _, tc := range testcases {
@ -121,8 +159,10 @@ func FuzzFlags(f *testing.F) {
}
f.Fuzz(func(t *testing.T, orig string) {
old := os.Args
os.Args = []string{"awl", orig}
//nolint:errcheck // Only make sure the program does not crash
//nolint:errcheck,gosec // Only make sure the program does not crash
cli.ParseCLI("TEST")
os.Args = old
})
}

View file

@ -7,18 +7,21 @@ import (
"strconv"
"strings"
"time"
"git.froth.zone/sam/awl/util"
)
// ParseDig parses commands from the popular DNS tool dig.
// All dig commands are taken from https://man.openbsd.org/dig.1 as the source of their functionality.
//
// [no]flags are supported just as flag are and are disabled as such.
func ParseDig(arg string, opts *Options) error {
func ParseDig(arg string, opts *util.Options) error {
// returns true if the flag starts with a no
isNo := !strings.HasPrefix(arg, "no")
if !isNo {
arg = strings.TrimPrefix(arg, "no")
}
opts.Logger.Info("Setting", arg)
switch arg {
@ -118,85 +121,100 @@ func ParseDig(arg string, opts *Options) error {
opts.Display.UcodeTranslate = isNo
default:
// Recursive switch statements WOO
arg := strings.Split(arg, "=")
switch arg[0] {
case "time", "timeout":
if len(arg) > 1 && arg[1] != "" {
timeout, err := strconv.Atoi(arg[1])
if err != nil {
return fmt.Errorf("digflags: Invalid timeout value: %w", err)
}
opts.Request.Timeout = time.Duration(timeout)
} else {
return fmt.Errorf("digflags: Invalid timeout value: %w", errNoArg)
}
case "retry", "tries":
if len(arg) > 1 && arg[1] != "" {
tries, err := strconv.Atoi(arg[1])
if err != nil {
return fmt.Errorf("digflags: Invalid retry value: %w", err)
}
opts.Request.Retries = tries
// TODO: Is there a better way to do this?
if arg[0] == "tries" {
opts.Request.Retries++
}
} else {
return fmt.Errorf("digflags: Invalid retry value: %w", errNoArg)
}
case "bufsize":
if len(arg) > 1 && arg[1] != "" {
size, err := strconv.Atoi(arg[1])
if err != nil {
return fmt.Errorf("digflags: Invalid UDP buffer size value: %w", err)
}
opts.EDNS.BufSize = uint16(size)
} else {
return fmt.Errorf("digflags: Invalid UDP buffer size value: %w", errNoArg)
}
case "ednsflags":
if len(arg) > 1 && arg[1] != "" {
ver, err := strconv.ParseInt(arg[1], 0, 16)
if err != nil {
return fmt.Errorf("digflags: Invalid EDNS flag: %w", err)
}
// Ignore setting DO bit
opts.EDNS.ZFlag = uint16(ver & 0x7FFF)
} else {
opts.EDNS.ZFlag = 0
}
case "edns":
opts.EDNS.EnableEDNS = isNo
if len(arg) > 1 && arg[1] != "" {
ver, err := strconv.Atoi(arg[1])
if err != nil {
return fmt.Errorf("digflags: Invalid EDNS version: %w", err)
}
opts.EDNS.Version = uint8(ver)
} else {
opts.EDNS.Version = 0
}
case "subnet":
if len(arg) > 1 && arg[1] != "" {
err := parseSubnet(arg[1], opts)
if err != nil {
return fmt.Errorf("digflags: Invalid EDNS Subnet: %w", err)
}
} else {
return fmt.Errorf("digflags: Invalid EDNS Subnet: %w", errNoArg)
}
default:
return &errInvalidArg{arg[0]}
if err := parseDigEq(isNo, arg, opts); err != nil {
return err
}
}
return nil
}
// For flags that contain "=".
func parseDigEq(startNo bool, arg string, opts *util.Options) error {
// Recursive switch statements WOO
arg, val, isSplit := strings.Cut(arg, "=")
switch arg {
case "time", "timeout":
if isSplit && val != "" {
timeout, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("digflags: timeout : %w", err)
}
opts.Request.Timeout = time.Duration(timeout)
} else {
return fmt.Errorf("digflags: timeout: %w", errNoArg)
}
case "retry", "tries":
if isSplit && val != "" {
tries, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("digflags: retry: %w", err)
}
opts.Request.Retries = tries
// TODO: Is there a better way to do this?
if arg == "tries" {
opts.Request.Retries++
}
} else {
return fmt.Errorf("digflags: retry: %w", errNoArg)
}
case "bufsize":
if isSplit && val != "" {
size, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("digflags: EDNS UDP: %w", err)
}
opts.EDNS.BufSize = uint16(size)
} else {
return fmt.Errorf("digflags: EDNS UDP: %w", errNoArg)
}
case "ednsflags":
if isSplit && val != "" {
ver, err := strconv.ParseInt(val, 0, 16)
if err != nil {
return fmt.Errorf("digflags: EDNS flag: %w", err)
}
// Ignore setting DO bit
opts.EDNS.ZFlag = uint16(ver & 0x7FFF)
} else {
opts.EDNS.ZFlag = 0
}
case "edns":
opts.EDNS.EnableEDNS = startNo
if isSplit && val != "" {
ver, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("digflags: EDNS version: %w", err)
}
opts.EDNS.Version = uint8(ver)
} else {
opts.EDNS.Version = 0
}
case "subnet":
if isSplit && val != "" {
err := util.ParseSubnet(val, opts)
if err != nil {
return fmt.Errorf("digflags: EDNS Subnet: %w", err)
}
} else {
return fmt.Errorf("digflags: EDNS Subnet: %w", errNoArg)
}
default:
return &errInvalidArg{arg}
}
return nil
}

View file

@ -12,6 +12,7 @@ import (
func FuzzDig(f *testing.F) {
f.Log("ParseDig Fuzzing")
seeds := []string{
"aaflag", "aaonly", "noaaflag", "noaaonly",
"adflag", "noadflag",
@ -59,15 +60,15 @@ func FuzzDig(f *testing.F) {
"idnout", "noidnout",
"invalid",
}
for _, tc := range seeds {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, orig string) {
opts := new(cli.Options)
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
err := cli.ParseDig(orig, opts)
if err != nil {
if err := cli.ParseDig(orig, opts); err != nil {
assert.ErrorContains(t, err, "digflags:")
}
})

View file

@ -1,5 +1,5 @@
/*
The CLI part of the package, including both POSIX
Package cli is the CLI part of the package, including both POSIX
flag parsing and dig-like flag parsing.
*/
package cli

View file

@ -15,17 +15,19 @@ import (
// ParseMiscArgs parses the wildcard arguments, drill style.
// Only one command is supported at a time, so any extra information overrides previous.
func ParseMiscArgs(args []string, opts *Options) error {
func ParseMiscArgs(args []string, opts *util.Options) error {
var err error
for _, arg := range args {
r, ok := dns.StringToType[strings.ToUpper(arg)]
switch {
// If it starts with @, it's a DNS server
case strings.HasPrefix(arg, "@"):
arg = arg[1:]
// Automatically set flags based on URI header
opts.Logger.Info(arg, "detected as a server")
switch {
case strings.HasPrefix(arg, "tls://"):
opts.TLS = true
@ -51,6 +53,7 @@ func ParseMiscArgs(args []string, opts *Options) error {
case strings.HasPrefix(arg, "+"):
opts.Logger.Info(arg, "detected as a dig query")
err = ParseDig(strings.ToLower(arg[1:]), opts)
if err != nil {
return err
}
@ -59,8 +62,9 @@ func ParseMiscArgs(args []string, opts *Options) error {
case strings.Contains(arg, "."):
opts.Logger.Info(arg, "detected as a domain name")
opts.Request.Name, err = idna.ToASCII(arg)
if err != nil {
return fmt.Errorf("punycode translate error: %w", err)
return fmt.Errorf("unicode to punycode: %w", err)
}
// DNS query type
@ -72,8 +76,9 @@ func ParseMiscArgs(args []string, opts *Options) error {
default:
opts.Logger.Info(arg, "is unknown. Assuming domain")
opts.Request.Name, err = idna.ToASCII(arg)
if err != nil {
return fmt.Errorf("punycode translate error: %w", err)
return fmt.Errorf("unicode to punycode: %w", err)
}
}
}
@ -82,11 +87,13 @@ func ParseMiscArgs(args []string, opts *Options) error {
if opts.Request.Name == "" {
opts.Logger.Info("Domain not specified, making a default")
opts.Request.Name = "."
if opts.Request.Type == 0 {
opts.Request.Type = dns.StringToType["NS"]
}
} else {
opts.Logger.Info("Query not specified, making an \"A\" query")
if opts.Request.Type == 0 {
opts.Request.Type = dns.StringToType["A"]
}
@ -106,7 +113,9 @@ func ParseMiscArgs(args []string, opts *Options) error {
case opts.QUIC:
opts.Request.Server = "dns.adguard.com"
default:
//nolint:govet // This shadow is intentional
resolv, err := conf.GetDNSConfig()
if err != nil {
// :^)
opts.Logger.Warn("Could not query system for server. Using default")
@ -119,40 +128,48 @@ func ParseMiscArgs(args []string, opts *Options) error {
case opts.IPv4:
if strings.Contains(srv, ".") {
opts.Request.Server = srv
break harmful
}
case opts.IPv6:
if strings.Contains(srv, ":") {
opts.Request.Server = srv
break harmful
}
default:
//#nosec -- This isn't used for anything secure
opts.Request.Server = resolv.Servers[rand.Intn(len(resolv.Servers))]
break harmful
}
}
}
}
}
opts.Logger.Info("DNS server set to", opts.Request.Server)
// Make reverse adresses proper addresses
if opts.Reverse {
opts.Logger.Info("Making reverse DNS query proper *.arpa domain")
if dns.TypeToString[opts.Request.Type] == "A" {
opts.Request.Type = dns.StringToType["PTR"]
}
opts.Request.Name, err = util.ReverseDNS(opts.Request.Name, opts.Request.Type)
if err != nil {
return fmt.Errorf("reverse DNS error: %w", err)
return fmt.Errorf("reverse DNS: %w", err)
}
}
// if the domain is not canonical, make it canonical
if !strings.HasSuffix(opts.Request.Name, ".") {
opts.Request.Name = fmt.Sprintf("%s.", opts.Request.Name)
opts.Logger.Debug("Domain made canonical")
}
return nil
}

View file

@ -14,13 +14,14 @@ import (
func TestParseArgs(t *testing.T) {
t.Parallel()
args := []string{
"go.dev",
"AAAA",
"@1.1.1.1",
"+ignore",
}
opts := new(cli.Options)
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
err := cli.ParseMiscArgs(args, opts)
assert.NilError(t, err)
@ -32,8 +33,9 @@ func TestParseArgs(t *testing.T) {
func TestParseNoInput(t *testing.T) {
t.Parallel()
args := []string{}
opts := new(cli.Options)
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
err := cli.ParseMiscArgs(args, opts)
assert.NilError(t, err)
@ -43,10 +45,11 @@ func TestParseNoInput(t *testing.T) {
func TestParseA(t *testing.T) {
t.Parallel()
args := []string{
"golang.org.",
}
opts := new(cli.Options)
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
err := cli.ParseMiscArgs(args, opts)
assert.NilError(t, err)
@ -56,8 +59,9 @@ func TestParseA(t *testing.T) {
func TestParsePTR(t *testing.T) {
t.Parallel()
args := []string{"8.8.8.8"}
opts := new(cli.Options)
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
opts.Reverse = true
err := cli.ParseMiscArgs(args, opts)
@ -67,8 +71,9 @@ func TestParsePTR(t *testing.T) {
func TestParseInvalidPTR(t *testing.T) {
t.Parallel()
args := []string{"8.88.8"}
opts := new(cli.Options)
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
opts.Reverse = true
err := cli.ParseMiscArgs(args, opts)
@ -77,6 +82,7 @@ func TestParseInvalidPTR(t *testing.T) {
func TestDefaultServer(t *testing.T) {
t.Parallel()
tests := []struct {
in string
want string
@ -86,12 +92,13 @@ func TestDefaultServer(t *testing.T) {
{"HTTPS", "https://dns.cloudflare.com/dns-query"},
{"QUIC", "dns.adguard.com"},
}
for _, test := range tests {
test := test
t.Run(test.in, func(t *testing.T) {
t.Parallel()
args := []string{}
opts := new(cli.Options)
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
switch test.in {
case "DNSCRYPT":
@ -112,6 +119,7 @@ func TestDefaultServer(t *testing.T) {
func TestFlagSetting(t *testing.T) {
t.Parallel()
tests := []struct {
in []string
}{
@ -120,11 +128,12 @@ func TestFlagSetting(t *testing.T) {
{[]string{"@https://dns.cloudflare.com/dns-query"}},
{[]string{"@quic://dns.adguard.com"}},
}
for i, test := range tests {
test := test
i := i
t.Run(strconv.Itoa(i), func(t *testing.T) {
opts := new(cli.Options)
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
t.Parallel()
err := cli.ParseMiscArgs(test.in, opts)
@ -151,14 +160,16 @@ func FuzzParseArgs(f *testing.F) {
"+ignore",
"e",
}
for _, tc := range cases {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, arg string) {
args := []string{arg}
opts := new(cli.Options)
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
//nolint:errcheck // Only make sure the program does not crash
//nolint:errcheck,gosec // Only make sure the program does not crash
cli.ParseMiscArgs(args, opts)
})
}

View file

@ -1,121 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"errors"
"fmt"
"net"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/logawl"
"github.com/miekg/dns"
)
// CLI options structure.
type Options struct {
Logger *logawl.Logger // Logger
Port int // DNS port
IPv4 bool // Force IPv4
IPv6 bool // Force IPv6
TCP bool // Query with TCP
DNSCrypt bool // Query over DNSCrypt
TLS bool // Query over TLS
HTTPS bool // Query over HTTPS
QUIC bool // Query over QUIC
Truncate bool // Ignore truncation
ShowQuery bool // Show query before being sent
AA bool // Set Authoratative Answer
AD bool // Set Authenticated Data
CD bool // Set CD
QR bool // Set QueRy
RD bool // Set Recursion Desired
RA bool // Set Recursion Available
TC bool // Set TC (TrunCated)
Z bool // Set Z (Zero)
Reverse bool // Make reverse query
Verbosity int // Set logawl verbosity
HumanTTL bool // Make TTL human readable
ShowTTL bool // Display TTL
Short bool // Short output
Identify bool // If short, add identity stuffs
JSON bool // Outout as JSON
XML bool // Output as XML
YAML bool // Output at YAML
Display Displays // Display options
Request helpers.Request // DNS reuqest
EDNS // EDNS
}
// What to (and not to) display.
type Displays struct {
Comments bool
Question bool // QUESTION SECTION
Opt bool // OPT PSEUDOSECTION
Answer bool // ANSWER SECTION
Authority bool // AUTHORITY SECTION
Additional bool // ADDITIONAL SECTION
Statistics bool // Query time, message size, etc.
UcodeTranslate bool // Translate Punycode back to Unicode
}
type EDNS struct {
EnableEDNS bool // Enable EDNS
Cookie bool // Enable EDNS cookie
DNSSEC bool // Enable DNSSEC
BufSize uint16 // Set UDP buffer size
Version uint8 // Set EDNS version
Expire bool // Set EDNS expiration
KeepOpen bool // TCP keep alive
Nsid bool // Show EDNS nsid
ZFlag uint16 // EDNS flags
Padding bool // EDNS padding
Subnet dns.EDNS0_SUBNET // EDNS Subnet (duh)
}
// parseSubnet takes a subnet argument and makes it into one that the DNS library
// understands.
func parseSubnet(subnet string, opts *Options) error {
ip, inet, err := net.ParseCIDR(subnet)
if err != nil {
// TODO: make not a default?
if subnet == "0" {
opts.EDNS.Subnet = dns.EDNS0_SUBNET{
Code: dns.EDNS0SUBNET,
Family: 1,
SourceNetmask: 0,
SourceScope: 0,
Address: net.IPv4(0, 0, 0, 0),
}
return nil
}
return fmt.Errorf("subnet parsing error %w", err)
}
sub, _ := inet.Mask.Size()
opts.EDNS.Subnet = dns.EDNS0_SUBNET{}
opts.EDNS.Subnet.Address = ip
opts.EDNS.Subnet.SourceNetmask = uint8(sub)
switch ip.To4() {
case nil:
// Not a valid IPv4 so assume IPv6
opts.EDNS.Subnet.Family = 2
default:
// Valid IPv4
opts.EDNS.Subnet.Family = 1
}
return nil
}
var ErrNotError = errors.New("not an error")
var errNoArg = errors.New("no argument given")
type errInvalidArg struct {
arg string
}
func (e *errInvalidArg) Error() string {
return fmt.Sprintf("digflags: invalid argument %s", e.arg)
}

View file

@ -1,5 +1,5 @@
/*
Helper functions for getting local nameservers
Package conf contains helper functions for getting local nameservers
Currently supported: Unix, Windows, Plan 9 (tested on 9front)
*/

View file

@ -9,6 +9,7 @@ import (
"github.com/miekg/dns"
)
// GetPlan9Config gets DNS information from Plan 9, because it's different from UNIX and Windows.
// Plan 9 stores its network data in /net/ndb, which seems to be formatted a specific way
// Yoink it and use it.
//
@ -16,12 +17,15 @@ import (
func GetPlan9Config(str string) (*dns.ClientConfig, error) {
str = strings.ReplaceAll(str, "\n", "")
spl := strings.FieldsFunc(str, splitChars)
var servers []string
for _, option := range spl {
if strings.HasPrefix(option, "dns=") {
servers = append(servers, strings.TrimPrefix(option, "dns="))
}
}
if len(servers) == 0 {
return nil, errPlan9
}

View file

@ -11,6 +11,7 @@ import (
func TestGetPlan9Config(t *testing.T) {
t.Parallel()
ndbs := []struct {
in string
want string

View file

@ -11,15 +11,21 @@ import (
"github.com/miekg/dns"
)
// Get the DNS configuration, either from /etc/resolv.conf or somewhere else
// GetDNSConfig gets the DNS configuration, either from /etc/resolv.conf or somewhere else.
func GetDNSConfig() (*dns.ClientConfig, error) {
if runtime.GOOS == "plan9" {
dat, err := os.ReadFile("/net/ndb")
if err != nil {
return nil, fmt.Errorf("plan9 ndb: %w", err)
}
return GetPlan9Config(string(dat))
} else {
return dns.ClientConfigFromFile("/etc/resolv.conf")
conf, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil {
return nil, fmt.Errorf("unix config: %w", err)
}
return conf, nil
}
}

View file

@ -15,6 +15,7 @@ func TestNonWinConfig(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Running Windows, skipping")
}
conf, err := conf.GetDNSConfig()
assert.NilError(t, err)
assert.Assert(t, len(conf.Servers) != 0)

View file

@ -17,37 +17,48 @@ import (
https://gist.github.com/moloch--/9fb1c8497b09b45c840fe93dd23b1e98
*/
// WindowsDnsClientConfig - returns all DNS server addresses using windows fuckery.
// GetDNSConfig (Windows version) returns all DNS server addresses using windows fuckery.
//
// Here be dragons.
func GetDNSConfig() (*dns.ClientConfig, error) {
l := uint32(20000)
b := make([]byte, l)
length := uint32(20000)
byt := make([]byte, length)
// Windows is an utter fucking trash fire of an operating system.
if err := windows.GetAdaptersAddresses(windows.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l); err != nil {
//nolint:gosec // This is necessary unless we want to drop 1.18
if err := windows.GetAdaptersAddresses(windows.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&byt[0])), &length); err != nil {
return nil, fmt.Errorf("config, windows: %w", err)
}
var addresses []*windows.IpAdapterAddresses
for addr := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); addr != nil; addr = addr.Next {
//nolint:gosec // This is necessary unless we want to drop 1.18
for addr := (*windows.IpAdapterAddresses)(unsafe.Pointer(&byt[0])); addr != nil; addr = addr.Next {
addresses = append(addresses, addr)
}
resolvers := map[string]bool{}
for _, addr := range addresses {
for next := addr.FirstUnicastAddress; next != nil; next = next.Next {
if addr.OperStatus != windows.IfOperStatusUp {
continue
}
if next.Address.IP() != nil {
for dnsServer := addr.FirstDnsServerAddress; dnsServer != nil; dnsServer = dnsServer.Next {
ip := dnsServer.Address.IP()
if ip.IsMulticast() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {
continue
}
if ip.To16() != nil && strings.HasPrefix(ip.To16().String(), "fec0:") {
continue
}
resolvers[ip.String()] = true
}
break
}
}

View file

@ -15,7 +15,9 @@ func TestWinConfig(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Not running Windows, skipping")
}
conf, err := conf.GetDNSConfig()
assert.NilError(t, err)
assert.Assert(t, len(conf.Servers) != 0)
}

244
doc/awl.1
View file

@ -1,244 +0,0 @@
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "awl" "1" "2022-08-03"
.PP
.SH NAME
awl - DNS lookup tool
.PP
.SH SYNOPSIS
\fBawl\fR [ \fIOPTIONS\fR ] \fIname\fR [ \fI@server\fR ] [ \fItype\fR ]
.br
where
.PP
\fIname\fR is the query to make (\fBexample: froth.\&zone\fR)
.br
\fI@server\fR is the server to query (\fBexample: dns.\&froth.\&zone\fR)
.br
\fItype\fR is the DNS resource type (\fBexample: AAAA\fR)
.PP
.SH DESCRIPTION
.PP
\fBawl\fR (\fBa\fRwls \fBw\fRant \fBl\fRicorice) is a simple tool designed to make DNS queries,
much like the venerable \fIdig\fR(1).\& An awl is a tool used to make small holes,
typically used in leatherworking.\&
.PP
\fBawl\fR is designed to be a more "modern" version of \fIdrill\fR(1) by including
some more recent RFCs and output options.\& \fBawl\fR is still heavily
Work-In-Progress so some features may get added or removed.\&
.PP
.SH OPTIONS
.RS 4
Dig-like +[no]flags are supported, see dig(1)
.PP
\fB-D\fR, \fB--dnssec\fR, \fB+dnssec\fR
.br
Enable DNSSEC.\& This needs to be manually enabled.\&
.PP
\fB-v\fR \fIvalue\fR
.br
Set verbosity (currently WIP)
.PP
\fB-V\fR
.br
Print the version and exit.\&
.PP
\fB-h\fR
.br
Show a "short" help message.\&
.PP
.RE
.SS Query Options
.RS 4
\fB-4\fR
.br
Only make query over IPv4
.PP
\fB-6\fR
.br
Only make query over IPv6
.PP
\fB-p\fR, \fB--port\fR \fIport\fR
.br
Sets the port to query.\&
.br
.br
\fIDefault Ports\fR:
.RS 4
.PD 0
.IP \(bu 4
\fI53\fR for \fBUDP\fR and \fBTCP\fR
.IP \(bu 4
\fI853\fR for \fBTLS\fR and \fBQUIC\fR
.IP \(bu 4
\fI443\fR for \fBHTTPS\fR
.PD
.PP
.RE
\fB-q\fR, \fB--query\fR \fIdomain\fR
.br
Domain to query (eg.\& example.\&com)
.PP
\fB-c\fR, \fB--class\fR \fIclass\fR
.br
DNS class to query (eg.\& IN, CH)
.PP
\fB-t\fR, \fB--qType\fR \fItype\fR
.br
DNS type to query (eg.\& A, NS)
.PP
\fB--no-truncate\fR, \fB+ignore\fR
.br
Ignore UDP truncation (by default, awl \fIretries with TCP\fR)
.PP
\fB--tcp\fR, \fB+tcp\fR, \fB+vc\fR
.br
Use TCP for the query (see \fIRFC 7766\fR)
.PP
\fB--dnscrypt\fR, \fB+dnscrypt\fR
.br
Use DNSCrypt
.PP
\fB-T\fR, \fB--tls\fR, \fB+tls\fR
.br
Use DNS-over-TLS, implies \fB--tcp\fR (see \fIRFC 7858\fR)
.PP
\fB-H\fR.\& \fB--https\fR, \fB+https\fR
.br
Use DNS-over-HTTPS (see \fIRFC 8484\fR)
.PP
\fB-Q\fR.\& \fB--quic\fR, \fB+quic\fR
.br
Use DNS-over-QUIC (see \fIRFC 9250\fR)
.PP
\fB-x\fR, \fB--reverse\fR
.br
Do a reverse lookup.\& Sets default \fItype\fR to PTR.\&
.br
\fBawl\fR automatically makes an IP or phone number canonical.\&
.PP
\fB--timeout\fR \fIseconds\fR, \fB+timeout=\fR\fIseconds\fR
.br
Set the timeout period.\& Floating point numbers are accepted.\&
.br
0.\&5 seconds is the minimum.\&
.PP
\fB--retries\fR \fIint\fR, \fB+tries\fR=\fIint\fR, \fB+ retry\fR=\fIint\fR
.br
Set the number of retries.\&
.br
Retry is one more than tries, dig style
.PP
.RE
.SS DNS Flags
.PP
.RS 4
\fB--aa=[false]\fR, \fB+[no]aaflag\fR
.br
(Set, Unset) AA (Authoritative Answer) flag
.PP
\fB--ad=[false]\fR, \fB+[no]adflag\fR
.br
(Set, Unset) AD (Authenticated Data) flag
.PP
\fB--tc=[false]\fR, \fB+[no]tcflag\fR
.br
(Set, Unset) TC (TrunCated) flag
.PP
\fB-z=[false]\fR, \fB+[no]zflag\fR
.br
(Set, Unset) Z (Zero) flag
.PP
\fB--cd=[false]\fR, \fB+[no]cdflag\fR
.br
(Set, Unset) CD (Checking Disabled) flag
.PP
\fB--qr=[false]\fR, \fB+[no]qrflag\fR
.br
(Set, Unset) QR (QueRy) flag
.PP
\fB--rd=[true]\fR, \fB+[no]rdflag\fR
.br
(Set, Unset) RD (Recursion Desired) flag
.PP
\fB--ra=[false]\fR, \fB+[no]raflag\fR
.br
(Set, Unset) RA (Recursion Available) flag
.PP
.RE
.SS Output Display
.RS 4
\fB--no-question\fR, \fB+noquestion\fR
.br
Do not display the Question section
.PP
\fB--no-answer\fR, \fB+noanswer\fR
.br
Do not display the Answer section
.PP
\fB--no-answer\fR, \fB+noanswer\fR
.br
Do not display the Answer section
.PP
\fB--no-authority\fR, \fB+noauthority\fR
.br
Do not display the Authority section
.PP
\fB--no-additional\fR, \fB+noadditional\fR
.br
Do not display the Additional section
.PP
\fB--no-statistics\fR, \fB+nostats\fR
.br
Do not display the Statistics (additional comments) section
.PP
.RE
.SS Output Formats
.RS 4
\fB-j\fR, \fB--json\fR, \fB+json\fR
.br
Print the query results as JSON.\&
.PP
\fB-X\fR, \fB--xml\fR, \fB+xml\fR
.br
Print the query results as XML.\&
.PP
\fB-y\fR, \fB--yaml\fR, \fB+yaml\fR
.br
Print the query results as YAML.\&
.PP
\fB-s\fR, \fB--short\fR, \fB+short\fR
.br
Print just the address of the answer.\&
.PP
.RE
.SH EXAMPLES
.nf
.RS 4
awl grumbulon\&.xyz -j +cd
.fi
.RE
Run a query of your local resolver for the A records of grumbulon.\&xyz, print
them as JSON and disable DNSSEC verification.\&
.PP
.nf
.RS 4
awl +short example\&.com AAAA @1\&.1\&.1\&.1
.fi
.RE
Query 1.\&1.\&1.\&1 for the AAAA records of example.\&com, print just the answers
.PP
.nf
.RS 4
awl -xT PTR 8\&.8\&.4\&.4 @dns\&.google
.fi
.RE
Query dns.\&google over TLS for the PTR record to the IP address 8.\&8.\&4.\&4
.PP
.SH SEE ALSO
\fIdrill\fR(1), \fIdig\fR(1), the many DNS RFCs

185
doc/awl.1.scd Normal file
View file

@ -0,0 +1,185 @@
awl(1)
# NAME
awl - DNS lookup tool
# SYNOPSIS
_awl_ [ _OPTIONS_ ] _name_ [ _@server_ ] [ _type_ ], where
_name_ is the query to make (_example: froth.zone_)++
_@server_ is the server to query (_example: dns.froth.zone_)++
_type_ is the DNS resource type (_example: AAAA_)
# DESCRIPTION
_awl_ (*a*wls *w*ant *l*icorice) is a simple tool designed to make DNS queries,
much like the venerable _dig_(1). An awl is a tool used to make small holes,
typically used in leatherworking.
_awl_ is designed to be a more "modern" version of _drill_(1) by including
some more recent RFCs and output options. _awl_ is still heavily
Work-In-Progress so some features may get added or removed.
# OPTIONS
Dig-like +[no]flags are supported, see dig(1)
_-D_, _--dnssec_, _+dnssec_
Enable DNSSEC. This needs to be manually enabled.
_-v_ _value_
Set verbosity (currently WIP)
_-V_
Print the version and exit.
_-h_
Show a "short" help message.
## Query Options
_-4_
Only make query over IPv4
_-6_
Only make query over IPv6
_-p_, _--port_ _port_
Sets the port to query. Default ports listed below.
- _53_ for _UDP_ and _TCP_
- _853_ for _TLS_ and _QUIC_
- _443_ for _HTTPS_
_-q_, _--query_ _domain_
Domain to query (eg. example.com)
_-c_, _--class_ _class_
DNS class to query (eg. IN, CH)
_-t_, _--qType_ _type_
DNS type to query (eg. A, NS)
_--no-truncate_, _+ignore_
Ignore UDP truncation (by default, awl _retries with TCP_)
_--tcp_, _+tcp_, _+vc_
Use TCP for the query (see _RFC 7766_)
_--dnscrypt_, _+dnscrypt_
Use DNSCrypt
_-T_, _--tls_, _+tls_
Use DNS-over-TLS, implies _--tcp_ (see _RFC 7858_)
_--tls-host_ _string_
Set hostname to use for TLS certificate validation.
Default is the name of the domain when querying over TLS, and empty for IPs.
_--tls-no-verify_
Ignore TLS validation when performing a DNS query.
_-H_. _--https_, _+https_
Use DNS-over-HTTPS (see _RFC 8484_)
_-Q_. _--quic_, _+quic_
Use DNS-over-QUIC (see _RFC 9250_)
_-x_, _--reverse_
Do a reverse lookup. Sets default _type_ to PTR.++
_awl_ automatically makes an IP or phone number canonical.
_--timeout_ _seconds_, _+timeout=__seconds_
Set the timeout period. Floating point numbers are accepted.++
0.5 seconds is the minimum.
_--retries_ _int_, _+tries_=_int_, _+ retry_=_int_
Set the number of retries.
Retry is one more than tries, dig style
## DNS Flags
_--aa=[false]_, _+[no]aaflag_
(Set, Unset) AA (Authoritative Answer) flag
_--ad=[false]_, _+[no]adflag_
(Set, Unset) AD (Authenticated Data) flag
_--tc=[false]_, _+[no]tcflag_
(Set, Unset) TC (TrunCated) flag
_-z=[false]_, _+[no]zflag_
(Set, Unset) Z (Zero) flag
_--cd=[false]_, _+[no]cdflag_
(Set, Unset) CD (Checking Disabled) flag
_--qr=[false]_, _+[no]qrflag_
(Set, Unset) QR (QueRy) flag
_--rd=[true]_, _+[no]rdflag_
(Set, Unset) RD (Recursion Desired) flag
_--ra=[false]_, _+[no]raflag_
(Set, Unset) RA (Recursion Available) flag
## Output Display
_--no-question_, _+noquestion_
Do not display the Question section
_--no-answer_, _+noanswer_
Do not display the Answer section
_--no-answer_, _+noanswer_
Do not display the Answer section
_--no-authority_, _+noauthority_
Do not display the Authority section
_--no-additional_, _+noadditional_
Do not display the Additional section
_--no-statistics_, _+nostats_
Do not display the Statistics (additional comments) section
## Output Formats
_-j_, _--json_, _+json_
Print the query results as JSON.
_-X_, _--xml_, _+xml_
Print the query results as XML.
_-y_, _--yaml_, _+yaml_
Print the query results as YAML.
_-s_, _--short_, _+short_
Print just the address of the answer.
# EXAMPLES
```
awl grumbulon.xyz -j +cd
```
Run a query of your local resolver for the A records of grumbulon.xyz, print
them as JSON and disable DNSSEC verification.
```
awl +short example.com AAAA @1.1.1.1
```
Query 1.1.1.1 for the AAAA records of example.com, print just the answers
```
awl -xT PTR 8.8.4.4 @dns.google
```
Query dns.google over TLS for the PTR record to the IP address 8.8.4.4
# SEE ALSO
_drill_(1), _dig_(1), the many DNS RFCs

@ -1 +1 @@
Subproject commit 0fba1fbe4b12e8c88514b3f7d98be3e75a5a034d
Subproject commit d25a7f13273737938d5e1e08c7332d39fffadc6b

View file

@ -1,4 +0,0 @@
/*
Useful structs used everywhere I couldn't find a better place to shove
*/
package helpers

View file

@ -1,25 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package helpers
import (
"time"
"github.com/miekg/dns"
)
// The DNS response.
type Response struct {
DNS *dns.Msg // The full DNS response
RTT time.Duration `json:"rtt"` // The time it took to make the DNS query
}
// A structure for a DNS query.
type Request struct {
Server string `json:"server"` // The server to make the DNS request from
Type uint16 `json:"request"` // The type of request
Class uint16 `json:"class"` // DNS Class
Name string `json:"name"` // The domain name to make a DNS request for
Timeout time.Duration // The maximum timeout
Retries int // Number of queries to retry
}

View file

@ -1,13 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package helpers_test
import (
"testing"
"gotest.tools/v3/assert"
)
func TestNothing(t *testing.T) {
assert.Equal(t, 0, 0)
}

View file

@ -1,5 +1,5 @@
/*
LogAwl is a package for custom logging needs
Package logawl is a package for custom logging needs
LogAwl extends the standard log library with support for log levels
This is _different_ from the syslog package in the standard library because you do not define a file
@ -19,7 +19,8 @@ because awl is a cli utility it writes directly to std err.
// You may also set the log level on the fly with
//
// Logger.SetLevel(3)
// This allows you to change the default level (Info) and prevent log messages from being posted at higher verbosity levels
// This allows you to change the default level (Info)
// and prevent log messages from being posted at higher verbosity levels
// for example if
// Logger.SetLevel(3)
// is not called and you call

View file

@ -10,23 +10,26 @@ import (
)
type (
Level int32
// Level is the logging level.
Level int32
// Logger is the overall logger.
Logger struct {
Out io.Writer
Prefix string
buf []byte
Mu sync.Mutex
Level Level
isDiscard int32
Prefix string
Out io.Writer
buf []byte
}
)
// Stores whatever input value is in mem address of l.level.
// SetLevel stores whatever input value is in mem address of l.level.
func (l *Logger) SetLevel(level Level) {
atomic.StoreInt32((*int32)(&l.Level), int32(level))
}
// Mostly nothing.
// GetLevel gets the logger level.
func (l *Logger) GetLevel() Level {
return l.level()
}
@ -36,7 +39,7 @@ func (l *Logger) level() Level {
return Level(atomic.LoadInt32((*int32)(&l.Level)))
}
// Unmarshalls the int value of level for writing the header.
// UnMarshalLevel unmarshalls the int value of level for writing the header.
func (l *Logger) UnMarshalLevel(lv Level) (string, error) {
switch lv {
case 0:
@ -48,13 +51,16 @@ func (l *Logger) UnMarshalLevel(lv Level) (string, error) {
case 3:
return "DEBUG ", nil
}
return "", errInvalidLevel
}
// IsLevel returns true if the logger level is above the level given.
func (l *Logger) IsLevel(level Level) bool {
return l.level() >= level
}
// AllLevels is an array of all valid log levels.
var AllLevels = []Level{
ErrLevel,
WarnLevel,
@ -63,15 +69,21 @@ var AllLevels = []Level{
}
const (
// Fatal logs (will call exit(1)).
// ErrLevel is the fatal (error) log level.
ErrLevel Level = iota
// Error logs.
// WarnLevel is for warning logs.
//
// Example: when one setting implies another, when a request fails but is retried.
WarnLevel
// What is going on level.
// InfoLevel is for saying what is going on when.
// This is essentially the "verbose" option.
//
// When in doubt, use info.
InfoLevel
// Verbose log level.
// DebugLevel is for spewing debug structs/interfaces.
DebugLevel
)

View file

@ -9,7 +9,7 @@ import (
"time"
)
// Calling New instantiates Logger
// New instantiates Logger
//
// Level can be changed to one of the other log levels (ErrorLevel, WarnLevel, InfoLevel, DebugLevel).
func New() *Logger {
@ -19,7 +19,7 @@ func New() *Logger {
}
}
// Takes any and prints it out to Logger -> Out (io.Writer (default is std.Err)).
// Println takes any and prints it out to Logger -> Out (io.Writer (default is std.Err)).
func (l *Logger) Println(level Level, v ...any) {
if atomic.LoadInt32(&l.isDiscard) != 0 {
return
@ -27,24 +27,20 @@ func (l *Logger) Println(level Level, v ...any) {
// If verbose is not set --debug etc print _nothing_
if l.IsLevel(level) {
switch level { // Goes through log levels and does stuff based on them (currently nothing)
case 0:
err := l.Printer(0, fmt.Sprintln(v...)) // Error level
if err != nil {
case ErrLevel:
if err := l.Printer(ErrLevel, fmt.Sprintln(v...)); err != nil {
fmt.Fprintln(os.Stderr, "Logger failed: ", err)
}
case 1:
err := l.Printer(1, fmt.Sprintln(v...)) // Warn level
if err != nil {
case WarnLevel:
if err := l.Printer(WarnLevel, fmt.Sprintln(v...)); err != nil {
fmt.Fprintln(os.Stderr, "Logger failed: ", err)
}
case 2:
err := l.Printer(2, fmt.Sprintln(v...)) // Info level
if err != nil {
case InfoLevel:
if err := l.Printer(InfoLevel, fmt.Sprintln(v...)); err != nil {
fmt.Fprintln(os.Stderr, "Logger failed: ", err)
}
case 3:
err := l.Printer(3, fmt.Sprintln(v...)) // Debug level
if err != nil {
case DebugLevel:
if err := l.Printer(DebugLevel, fmt.Sprintln(v...)); err != nil {
fmt.Fprintln(os.Stderr, "Logger failed: ", err)
}
default:
@ -53,13 +49,14 @@ func (l *Logger) Println(level Level, v ...any) {
}
}
// Formats the log header as such <LogLevel> YYYY/MM/DD HH:MM:SS (local time) <the message to log>.
// FormatHeader formats the log header as such <LogLevel> YYYY/MM/DD HH:MM:SS (local time) <the message to log>.
func (l *Logger) FormatHeader(buf *[]byte, t time.Time, line int, level Level) error {
if lvl, err := l.UnMarshalLevel(level); err == nil {
// This is ugly but functional
// maybe there can be an append func or something in the future
*buf = append(*buf, lvl...)
year, month, day := t.Date()
*buf = append(*buf, '[')
formatter(buf, year, 4)
*buf = append(*buf, '/')
@ -79,46 +76,57 @@ func (l *Logger) FormatHeader(buf *[]byte, t time.Time, line int, level Level) e
} else {
return errInvalidLevel
}
return nil
}
// Printer prints the formatted message directly to stdErr.
func (l *Logger) Printer(level Level, s string) error {
now := time.Now()
var line int
l.Mu.Lock()
defer l.Mu.Unlock()
l.buf = l.buf[:0]
err := l.FormatHeader(&l.buf, now, line, level)
if err != nil {
if err := l.FormatHeader(&l.buf, now, line, level); err != nil {
return err
}
l.buf = append(l.buf, s...)
if len(s) == 0 || s[len(s)-1] != '\n' {
l.buf = append(l.buf, '\n')
}
_, err = l.Out.Write(l.buf)
_, err := l.Out.Write(l.buf)
if err != nil {
return fmt.Errorf("logger printing error %w", err)
return fmt.Errorf("logger printing: %w", err)
}
return nil
}
// Some line formatting stuff from Golang log stdlib file
//
// Please view https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/log/log.go;drc=41e1d9075e428c2fc32d966b3752a3029b620e2c;l=96
// Please view
// https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/log/log.go;drc=41e1d9075e428c2fc32d966b3752a3029b620e2c;l=96
//
// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding.
func formatter(buf *[]byte, i int, wid int) {
// Assemble decimal in reverse order.
var b [20]byte
bp := len(b) - 1
for i >= 10 || wid > 1 {
wid--
q := i / 10
b[bp] = byte('0' + i - q*10)
bp--
i = q
}
// i < 10
@ -126,22 +134,22 @@ func formatter(buf *[]byte, i int, wid int) {
*buf = append(*buf, b[bp:]...)
}
// Call print directly with Debug level.
// Debug calls print directly with Debug level.
func (l *Logger) Debug(v ...any) {
l.Println(DebugLevel, v...)
}
// Call print directly with Info level.
// Info calls print directly with Info level.
func (l *Logger) Info(v ...any) {
l.Println(InfoLevel, v...)
}
// Call print directly with Warn level.
// Warn calls print directly with Warn level.
func (l *Logger) Warn(v ...any) {
l.Println(WarnLevel, v...)
}
// Call print directly with Error level.
// Error calls print directly with Error level.
func (l *Logger) Error(v ...any) {
l.Println(ErrLevel, v...)
}

View file

@ -24,10 +24,11 @@ func TestLogawl(t *testing.T) {
func TestUnmarshalLevels(t *testing.T) {
t.Parallel()
m := make(map[int]string)
var err error
for i := range logawl.AllLevels {
var err error
m[i], err = logger.UnMarshalLevel(logawl.Level(i))
assert.NilError(t, err)
}
@ -52,30 +53,42 @@ func TestLogger(t *testing.T) {
fn := func() {
logger.Error("Test", "E")
}
var buffer bytes.Buffer
logger.Out = &buffer
fn()
case 1:
fn := func() {
logger.Warn("Test")
}
var buffer bytes.Buffer
logger.Out = &buffer
fn()
case 2:
fn := func() {
logger.Info("Test")
}
var buffer bytes.Buffer
logger.Out = &buffer
fn()
case 3:
fn := func() {
logger.Debug("Test")
logger.Debug("Test 2")
}
var buffer bytes.Buffer
logger.Out = &buffer
fn()
}
}
@ -83,6 +96,7 @@ func TestLogger(t *testing.T) {
func TestFmt(t *testing.T) {
t.Parallel()
ti := time.Now()
test := []byte("test")
// make sure error is error

27
main.go
View file

@ -9,24 +9,31 @@ import (
"strings"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/query"
"git.froth.zone/sam/awl/util"
)
var version = "DEV"
func main() {
opts, err := cli.ParseCLI(version)
if err != nil {
if opts, code, err := run(); err != nil {
// TODO: Make not ew
if errors.Is(err, cli.ErrNotError) || strings.Contains(err.Error(), "help requested") {
os.Exit(0)
} else {
opts.Logger.Error(err)
os.Exit(code)
}
opts.Logger.Error(err)
os.Exit(1)
}
}
func run() (opts util.Options, code int, err error) {
opts, err = cli.ParseCLI(version)
if err != nil {
return opts, 1, fmt.Errorf("parse: %w", err)
}
var resp helpers.Response
var resp util.Response
// Retry queries if a query fails
for i := 0; i < opts.Request.Retries; i++ {
@ -40,20 +47,20 @@ func main() {
// Query failed, make it fail
if err != nil {
opts.Logger.Error(err)
os.Exit(9)
return opts, 9, fmt.Errorf("query: %w", err)
}
var str string
if opts.JSON || opts.XML || opts.YAML {
str, err = query.PrintSpecial(resp.DNS, opts)
if err != nil {
opts.Logger.Error("Special print:", err)
os.Exit(10)
return opts, 10, fmt.Errorf("format print: %w", err)
}
} else {
str = query.ToString(resp, opts)
}
fmt.Println(str)
return opts, 0, nil
}

View file

@ -6,14 +6,36 @@ import (
"os"
"testing"
"github.com/stefansundin/go-zflag"
"gotest.tools/v3/assert"
)
// nolint: paralleltest
func TestMain(t *testing.T) {
func TestMain(t *testing.T) { //nolint: paralleltest // Race conditions
old := os.Args
os.Args = []string{"awl", "+yaml", "@1.1.1.1"}
main()
_, code, err := run()
assert.NilError(t, err)
assert.Equal(t, code, 0)
os.Args = []string{"awl", "+short", "@1.1.1.1"}
main()
assert.Assert(t, 1 == 2-1)
_, code, err = run()
assert.NilError(t, err)
assert.Equal(t, code, 0)
os.Args = old
}
func TestHelp(t *testing.T) {
old := os.Args
os.Args = []string{"awl", "-h"}
_, code, err := run()
assert.ErrorIs(t, err, zflag.ErrHelp)
assert.Equal(t, code, 1)
os.Args = old
}

View file

@ -6,27 +6,29 @@ import (
"fmt"
"time"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/util"
"github.com/ameshkov/dnscrypt/v2"
"github.com/miekg/dns"
)
// DNSCryptResolver is for making DNSCrypt queries.
type DNSCryptResolver struct {
opts cli.Options
opts util.Options
}
var _ Resolver = (*DNSCryptResolver)(nil)
// LookUp performs a DNS query.
func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (helpers.Response, error) {
func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (util.Response, error) {
client := dnscrypt.Client{
Timeout: r.opts.Request.Timeout,
UDPSize: 1232,
}
if r.opts.TCP || r.opts.TLS {
client.Net = "tcp"
client.Net = tcp
} else {
client.Net = "udp"
client.Net = udp
}
switch {
@ -35,11 +37,12 @@ func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (helpers.Response, error) {
case r.opts.IPv6:
client.Net += "6"
}
r.opts.Logger.Debug("Using", client.Net, "for making the request")
resolverInf, err := client.Dial(r.opts.Request.Server)
if err != nil {
return helpers.Response{}, fmt.Errorf("dnscrypt: dial error: %w", err)
return util.Response{}, fmt.Errorf("dnscrypt: dial: %w", err)
}
now := time.Now()
@ -47,11 +50,12 @@ func (r *DNSCryptResolver) LookUp(msg *dns.Msg) (helpers.Response, error) {
rtt := time.Since(now)
if err != nil {
return helpers.Response{}, fmt.Errorf("dnscrypt: exchange error: %w", err)
return util.Response{}, fmt.Errorf("dnscrypt: exchange: %w", err)
}
r.opts.Logger.Info("Request successful")
return helpers.Response{
return util.Response{
DNS: res,
RTT: rtt,
}, nil

View file

@ -5,8 +5,6 @@ package query_test
import (
"testing"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/query"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
@ -15,14 +13,15 @@ import (
func TestDNSCrypt(t *testing.T) {
t.Parallel()
tests := []struct {
opt cli.Options
opt util.Options
}{
{
cli.Options{
util.Options{
Logger: util.InitLogger(0),
DNSCrypt: true,
Request: helpers.Request{
Request: util.Request{
Server: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
Type: dns.TypeA,
Name: "example.com.",
@ -30,12 +29,12 @@ func TestDNSCrypt(t *testing.T) {
},
},
{
cli.Options{
util.Options{
Logger: util.InitLogger(0),
DNSCrypt: true,
TCP: true,
IPv4: true,
Request: helpers.Request{
Request: util.Request{
Server: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
Type: dns.TypeAAAA,
Name: "example.com.",
@ -43,12 +42,12 @@ func TestDNSCrypt(t *testing.T) {
},
},
{
cli.Options{
util.Options{
Logger: util.InitLogger(0),
DNSCrypt: true,
TCP: true,
IPv4: true,
Request: helpers.Request{
Request: util.Request{
Server: "QMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
Type: dns.TypeAAAA,
Name: "example.com.",
@ -56,13 +55,16 @@ func TestDNSCrypt(t *testing.T) {
},
},
}
for _, test := range tests {
test := test
t.Run("", func(t *testing.T) {
t.Parallel()
res, err := query.CreateQuery(test.opt)
if err == nil {
assert.Assert(t, res != helpers.Response{})
assert.Assert(t, res != util.Response{})
} else {
assert.ErrorContains(t, err, "unsupported stamp")
}

View file

@ -9,31 +9,37 @@ import (
"net/http"
"time"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
)
// HTTPSResolver is for DNS-over-HTTPS queries.
type HTTPSResolver struct {
opts cli.Options
opts util.Options
}
var _ Resolver = (*HTTPSResolver)(nil)
// LookUp performs a DNS query.
func (r *HTTPSResolver) LookUp(msg *dns.Msg) (helpers.Response, error) {
var resp helpers.Response
func (r *HTTPSResolver) LookUp(msg *dns.Msg) (util.Response, error) {
var resp util.Response
httpR := &http.Client{
Timeout: r.opts.Request.Timeout,
}
buf, err := msg.Pack()
if err != nil {
return helpers.Response{}, fmt.Errorf("doh: packing error error: %w", err)
return util.Response{}, fmt.Errorf("doh: packing: %w", err)
}
r.opts.Logger.Debug("making DoH request")
//
r.opts.Logger.Debug("https: sending HTTPS request")
req, err := http.NewRequest("POST", r.opts.Request.Server, bytes.NewBuffer(buf))
if err != nil {
return helpers.Response{}, fmt.Errorf("doh request creation error: %w", err)
return util.Response{}, fmt.Errorf("doh: request creation: %w", err)
}
req.Header.Set("Content-Type", "application/dns-message")
req.Header.Set("Accept", "application/dns-message")
@ -42,23 +48,32 @@ func (r *HTTPSResolver) LookUp(msg *dns.Msg) (helpers.Response, error) {
resp.RTT = time.Since(now)
if err != nil {
return helpers.Response{}, fmt.Errorf("doh HTTP request error: %w", err)
return util.Response{}, fmt.Errorf("doh: HTTP request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return helpers.Response{}, &errHTTPStatus{res.StatusCode}
return util.Response{}, &errHTTPStatus{res.StatusCode}
}
r.opts.Logger.Debug("https: reading response")
fullRes, err := io.ReadAll(res.Body)
if err != nil {
return helpers.Response{}, fmt.Errorf("doh body read error: %w", err)
return util.Response{}, fmt.Errorf("doh: body read: %w", err)
}
err = res.Body.Close()
if err != nil {
return util.Response{}, fmt.Errorf("doh: body close: %w", err)
}
r.opts.Logger.Debug("https: unpacking response")
resp.DNS = &dns.Msg{}
r.opts.Logger.Debug("unpacking response")
err = resp.DNS.Unpack(fullRes)
if err != nil {
return helpers.Response{}, fmt.Errorf("doh dns message unpack error: %w", err)
return util.Response{}, fmt.Errorf("doh: dns message unpack: %w", err)
}
return resp, nil

View file

@ -3,12 +3,8 @@
package query_test
import (
"fmt"
"strings"
"testing"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/query"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
@ -17,17 +13,19 @@ import (
func TestResolveHTTPS(t *testing.T) {
t.Parallel()
var err error
opts := cli.Options{
opts := util.Options{
HTTPS: true,
Logger: util.InitLogger(0),
Request: helpers.Request{
Request: util.Request{
Server: "https://dns9.quad9.net/dns-query",
Type: dns.TypeA,
Name: "git.froth.zone.",
},
}
// testCase := helpers.Request{Server: "https://dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."}
// testCase := util.Request{Server: "https://dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."}
resolver, err := query.LoadResolver(opts)
assert.NilError(t, err)
@ -36,62 +34,75 @@ func TestResolveHTTPS(t *testing.T) {
// msg = msg.SetQuestion(testCase.Name, testCase.Type)
res, err := resolver.LookUp(msg)
assert.NilError(t, err)
assert.Assert(t, res != helpers.Response{})
assert.Assert(t, res != util.Response{})
}
func Test2ResolveHTTPS(t *testing.T) {
t.Parallel()
opts := cli.Options{
HTTPS: true,
Logger: util.InitLogger(0),
opts := util.Options{
HTTPS: true,
Logger: util.InitLogger(0),
Request: util.Request{Server: "dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."},
}
var err error
testCase := helpers.Request{Server: "dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone"}
testCase := util.Request{Type: dns.TypeA, Name: "git.froth.zone"}
resolver, err := query.LoadResolver(opts)
assert.NilError(t, err)
msg := new(dns.Msg)
msg.SetQuestion(testCase.Name, testCase.Type)
// msg = msg.SetQuestion(testCase.Name, testCase.Type)
res, err := resolver.LookUp(msg)
assert.ErrorContains(t, err, "fully qualified")
assert.Equal(t, res, helpers.Response{})
assert.Equal(t, res, util.Response{})
}
func Test3ResolveHTTPS(t *testing.T) {
t.Parallel()
opts := cli.Options{
HTTPS: true,
Logger: util.InitLogger(0),
opts := util.Options{
HTTPS: true,
Logger: util.InitLogger(0),
Request: util.Request{Server: "dns9..quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."},
}
var err error
testCase := helpers.Request{Server: "dns9..quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."}
// testCase :=
// if the domain is not canonical, make it canonical
if !strings.HasSuffix(testCase.Name, ".") {
testCase.Name = fmt.Sprintf("%s.", testCase.Name)
}
// if !strings.HasSuffix(testCase.Name, ".") {
// testCase.Name = fmt.Sprintf("%s.", testCase.Name)
// }
resolver, err := query.LoadResolver(opts)
assert.NilError(t, err)
msg := new(dns.Msg)
msg.SetQuestion(testCase.Name, testCase.Type)
// msg.SetQuestion(testCase.Name, testCase.Type)
// msg = msg.SetQuestion(testCase.Name, testCase.Type)
res, err := resolver.LookUp(msg)
assert.ErrorContains(t, err, "request error")
assert.Equal(t, res, helpers.Response{})
assert.ErrorContains(t, err, "doh: HTTP request")
assert.Equal(t, res, util.Response{})
}
func Test404ResolveHTTPS(t *testing.T) {
t.Parallel()
var err error
opts := cli.Options{
opts := util.Options{
HTTPS: true,
Logger: util.InitLogger(0),
Request: helpers.Request{
Request: util.Request{
Server: "https://dns9.quad9.net/dns",
Type: dns.TypeA,
Name: "git.froth.zone.",
},
}
// testCase := helpers.Request{Server: "https://dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."}
// testCase := util.Request{Server: "https://dns9.quad9.net/dns-query", Type: dns.TypeA, Name: "git.froth.zone."}
resolver, err := query.LoadResolver(opts)
assert.NilError(t, err)
@ -100,5 +111,5 @@ func Test404ResolveHTTPS(t *testing.T) {
// msg = msg.SetQuestion(testCase.Name, testCase.Type)
res, err := resolver.LookUp(msg)
assert.ErrorContains(t, err, "404")
assert.Equal(t, res, helpers.Response{})
assert.Equal(t, res, util.Response{})
}

View file

@ -8,70 +8,95 @@ import (
"io"
"time"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/util"
"github.com/lucas-clemente/quic-go"
"github.com/miekg/dns"
)
// QUICResolver is for DNS-over-QUIC queries.
type QUICResolver struct {
opts cli.Options
opts util.Options
}
var _ Resolver = (*QUICResolver)(nil)
// LookUp performs a DNS query.
func (r *QUICResolver) LookUp(msg *dns.Msg) (helpers.Response, error) {
var resp helpers.Response
func (r *QUICResolver) LookUp(msg *dns.Msg) (util.Response, error) {
var resp util.Response
tls := &tls.Config{
MinVersion: tls.VersionTLS12,
NextProtos: []string{"doq"},
//nolint:gosec // This is intentional if the user requests it
InsecureSkipVerify: r.opts.TLSNoVerify,
ServerName: r.opts.TLSHost,
MinVersion: tls.VersionTLS12,
NextProtos: []string{"doq"},
}
conf := new(quic.Config)
conf.HandshakeIdleTimeout = r.opts.Request.Timeout
r.opts.Logger.Debug("making DoQ request")
r.opts.Logger.Debug("quic: making query")
connection, err := quic.DialAddr(r.opts.Request.Server, tls, conf)
if err != nil {
return helpers.Response{}, fmt.Errorf("doq: dial error: %w", err)
return util.Response{}, fmt.Errorf("doq: dial: %w", err)
}
r.opts.Logger.Debug("quic: packing query")
// Compress request to over-the-wire
buf, err := msg.Pack()
if err != nil {
return helpers.Response{}, fmt.Errorf("doq: pack error: %w", err)
return util.Response{}, fmt.Errorf("doq: pack: %w", err)
}
t := time.Now()
r.opts.Logger.Debug("quic: creating stream")
stream, err := connection.OpenStream()
if err != nil {
return helpers.Response{}, fmt.Errorf("doq: quic stream creation error: %w", err)
return util.Response{}, fmt.Errorf("doq: quic stream creation: %w", err)
}
r.opts.Logger.Debug("quic: writing to stream")
_, err = stream.Write(buf)
if err != nil {
return helpers.Response{}, fmt.Errorf("doq: quic stream write error: %w", err)
return util.Response{}, fmt.Errorf("doq: quic stream write: %w", err)
}
r.opts.Logger.Debug("quic: reading stream")
fullRes, err := io.ReadAll(stream)
if err != nil {
return helpers.Response{}, fmt.Errorf("doq: quic stream read error: %w", err)
return util.Response{}, fmt.Errorf("doq: quic stream read: %w", err)
}
resp.RTT = time.Since(t)
r.opts.Logger.Debug("quic: closing connection")
// Close with error: no error
err = connection.CloseWithError(0, "")
if err != nil {
return helpers.Response{}, fmt.Errorf("doq: quic connection close error: %w", err)
return util.Response{}, fmt.Errorf("doq: quic connection close: %w", err)
}
r.opts.Logger.Debug("quic: closing stream")
err = stream.Close()
if err != nil {
return helpers.Response{}, fmt.Errorf("doq: quic stream close error: %w", err)
return util.Response{}, fmt.Errorf("doq: quic stream close: %w", err)
}
resp.DNS = &dns.Msg{}
r.opts.Logger.Debug("unpacking DoQ response")
r.opts.Logger.Debug("quic: unpacking response")
err = resp.DNS.Unpack(fullRes)
if err != nil {
return helpers.Response{}, fmt.Errorf("doq: upack error: %w", err)
return util.Response{}, fmt.Errorf("doq: unpack: %w", err)
}
return resp, nil
}

View file

@ -10,8 +10,6 @@ import (
"testing"
"time"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/query"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
@ -20,17 +18,21 @@ import (
func TestQuic(t *testing.T) {
t.Parallel()
opts := cli.Options{
opts := util.Options{
QUIC: true,
Logger: util.InitLogger(0),
Port: 853,
Request: helpers.Request{Server: "dns.adguard.com"},
Request: util.Request{Server: "dns.adguard.com"},
}
testCase := helpers.Request{Server: "dns.//./,,adguard.com", Type: dns.TypeA, Name: "git.froth.zone"}
testCase2 := helpers.Request{Server: "dns.adguard.com", Type: dns.TypeA, Name: "git.froth.zone"}
var testCases []helpers.Request
testCase := util.Request{Server: "dns.//./,,adguard.com", Type: dns.TypeA, Name: "git.froth.zone"}
testCase2 := util.Request{Server: "dns.adguard.com", Type: dns.TypeA, Name: "git.froth.zone"}
var testCases []util.Request
testCases = append(testCases, testCase)
testCases = append(testCases, testCase2)
for i := range testCases {
switch i {
case 0:
@ -40,37 +42,44 @@ func TestQuic(t *testing.T) {
if !strings.HasSuffix(testCase.Name, ".") {
testCases[i].Name = fmt.Sprintf("%s.", testCases[i].Name)
}
msg := new(dns.Msg)
msg.SetQuestion(testCase.Name, testCase.Type)
msg = msg.SetQuestion(testCase.Name, testCase.Type)
// msg = msg.SetQuestion(testCase.Name, testCase.Type)
res, err := resolver.LookUp(msg)
assert.ErrorContains(t, err, "fully qualified")
assert.Equal(t, res, helpers.Response{})
assert.Equal(t, res, util.Response{})
case 1:
resolver, err := query.LoadResolver(opts)
assert.NilError(t, err)
testCase2.Server = net.JoinHostPort(testCase2.Server, strconv.Itoa(opts.Port))
// if the domain is not canonical, make it canonical
if !strings.HasSuffix(testCase2.Name, ".") {
testCase2.Name = fmt.Sprintf("%s.", testCase2.Name)
}
msg := new(dns.Msg)
msg.SetQuestion(testCase2.Name, testCase2.Type)
msg = msg.SetQuestion(testCase2.Name, testCase2.Type)
res, err := resolver.LookUp(msg)
assert.NilError(t, err)
assert.Assert(t, res != helpers.Response{})
assert.Assert(t, res != util.Response{})
}
}
}
func TestInvalidQuic(t *testing.T) {
t.Parallel()
opts := cli.Options{
opts := util.Options{
QUIC: true,
Logger: util.InitLogger(0),
Port: 853,
Request: helpers.Request{Server: "example.com", Type: dns.TypeA, Name: "git.froth.zone", Timeout: 10 * time.Millisecond},
Request: util.Request{Server: "example.com", Type: dns.TypeA, Name: "git.froth.zone", Timeout: 10 * time.Millisecond},
}
resolver, err := query.LoadResolver(opts)
assert.NilError(t, err)
@ -79,5 +88,5 @@ func TestInvalidQuic(t *testing.T) {
msg.SetQuestion(opts.Request.Name, opts.Request.Type)
res, err := resolver.LookUp(msg)
assert.ErrorContains(t, err, "timeout")
assert.Equal(t, res, helpers.Response{})
assert.Equal(t, res, util.Response{})
}

View file

@ -1,2 +1,2 @@
// Package for the special query types
// Package query is for the various query types.
package query

View file

@ -3,32 +3,37 @@
package query
import (
"crypto/tls"
"fmt"
"net"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
)
// StandardResolver is for UDP/TCP resolvers.
type StandardResolver struct {
opts cli.Options
opts util.Options
}
// LookUp performs a DNS query
func (r *StandardResolver) LookUp(msg *dns.Msg) (helpers.Response, error) {
var _ Resolver = (*StandardResolver)(nil)
// LookUp performs a DNS query.
func (r *StandardResolver) LookUp(msg *dns.Msg) (util.Response, error) {
var (
resp helpers.Response
resp util.Response
err error
)
dnsClient := new(dns.Client)
dnsClient.Dialer = &net.Dialer{
Timeout: r.opts.Request.Timeout,
}
if r.opts.TCP || r.opts.TLS {
dnsClient.Net = "tcp"
dnsClient.Net = tcp
} else {
dnsClient.Net = "udp"
dnsClient.Net = udp
}
switch {
@ -40,28 +45,39 @@ func (r *StandardResolver) LookUp(msg *dns.Msg) (helpers.Response, error) {
if r.opts.TLS {
dnsClient.Net += "-tls"
dnsClient.TLSConfig = &tls.Config{
//nolint:gosec // This is intentional if the user requests it
InsecureSkipVerify: r.opts.TLSNoVerify,
ServerName: r.opts.TLSHost,
}
}
r.opts.Logger.Debug("Using", dnsClient.Net, "for making the request")
resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, r.opts.Request.Server)
if err != nil {
return helpers.Response{}, fmt.Errorf("standard: DNS exchange error: %w", err)
return util.Response{}, fmt.Errorf("standard: DNS exchange: %w", err)
}
r.opts.Logger.Info("Request successful")
if resp.DNS.MsgHdr.Truncated && !r.opts.Truncate {
fmt.Printf(";; Truncated, retrying with TCP\n\n")
dnsClient.Net = "tcp"
dnsClient.Net = tcp
switch {
case r.opts.IPv4:
dnsClient.Net += "4"
case r.opts.IPv6:
dnsClient.Net += "6"
}
resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, r.opts.Request.Server)
}
if err != nil {
return helpers.Response{}, fmt.Errorf("standard: DNS exchange error: %w", err)
return util.Response{}, fmt.Errorf("standard: DNS exchange: %w", err)
}
return resp, nil

View file

@ -6,8 +6,6 @@ import (
"testing"
"time"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/query"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
@ -16,10 +14,11 @@ import (
func TestResolve(t *testing.T) {
t.Parallel()
opts := cli.Options{
opts := util.Options{
Logger: util.InitLogger(0),
Port: 53,
Request: helpers.Request{
Request: util.Request{
Server: "8.8.4.1",
Type: dns.TypeA,
Name: "example.com.",
@ -29,19 +28,22 @@ func TestResolve(t *testing.T) {
}
resolver, err := query.LoadResolver(opts)
assert.NilError(t, err)
msg := new(dns.Msg)
msg.SetQuestion(opts.Request.Name, opts.Request.Type)
_, err = resolver.LookUp(msg)
assert.ErrorContains(t, err, "timeout")
}
func TestTruncate(t *testing.T) {
t.Parallel()
opts := cli.Options{
opts := util.Options{
Logger: util.InitLogger(0),
IPv4: true,
Port: 5301,
Request: helpers.Request{
Request: util.Request{
Server: "madns.binarystar.systems",
Type: dns.TypeTXT,
Name: "limit.txt.example.",
@ -49,24 +51,27 @@ func TestTruncate(t *testing.T) {
}
resolver, err := query.LoadResolver(opts)
assert.NilError(t, err)
msg := new(dns.Msg)
msg.SetQuestion(opts.Request.Name, opts.Request.Type)
res, err := resolver.LookUp(msg)
assert.NilError(t, err)
assert.Assert(t, res != helpers.Response{})
assert.Assert(t, res != util.Response{})
}
func TestResolveAgain(t *testing.T) {
t.Parallel()
tests := []struct {
opt cli.Options
opt util.Options
}{
{
cli.Options{
util.Options{
Logger: util.InitLogger(0),
TCP: true,
Port: 53,
Request: helpers.Request{
Request: util.Request{
Server: "8.8.4.4",
Type: dns.TypeA,
Name: "example.com.",
@ -74,10 +79,10 @@ func TestResolveAgain(t *testing.T) {
},
},
{
cli.Options{
util.Options{
Logger: util.InitLogger(0),
Port: 53,
Request: helpers.Request{
Request: util.Request{
Server: "8.8.4.4",
Type: dns.TypeAAAA,
Name: "example.com.",
@ -85,11 +90,11 @@ func TestResolveAgain(t *testing.T) {
},
},
{
cli.Options{
util.Options{
Logger: util.InitLogger(0),
TLS: true,
Port: 853,
Request: helpers.Request{
Request: util.Request{
Server: "dns.google",
Type: dns.TypeAAAA,
Name: "example.com.",
@ -97,13 +102,15 @@ func TestResolveAgain(t *testing.T) {
},
},
}
for _, test := range tests {
test := test
t.Run("", func(t *testing.T) {
t.Parallel()
res, err := query.CreateQuery(test.opt)
assert.NilError(t, err)
assert.Assert(t, res != helpers.Response{})
assert.Assert(t, res != util.Response{})
})
}
}

View file

@ -11,42 +11,49 @@ import (
"strings"
"time"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
"golang.org/x/net/idna"
"gopkg.in/yaml.v3"
)
func PrintSpecial(msg *dns.Msg, opts cli.Options) (string, error) {
// PrintSpecial is for printing as JSON, XML or YAML.
// As of now JSON and XML use the stdlib version.
func PrintSpecial(msg *dns.Msg, opts util.Options) (string, error) {
formatted, err := MakePrintable(msg, opts)
if err != nil {
return "", err
}
switch {
case opts.JSON:
opts.Logger.Info("Printing as JSON")
json, err := json.MarshalIndent(formatted, " ", " ")
return string(json), err
json, err := json.MarshalIndent(formatted, " ", " ")
return string(json), err
case opts.XML:
opts.Logger.Info("Printing as XML")
xml, err := xml.MarshalIndent(formatted, " ", " ")
return string(xml), err
xml, err := xml.MarshalIndent(formatted, " ", " ")
return string(xml), err
case opts.YAML:
opts.Logger.Info("Printing as YAML")
yaml, err := yaml.Marshal(formatted)
return string(yaml), err
yaml, err := yaml.Marshal(formatted)
return string(yaml), err
default:
return "", errInvalidFormat
}
}
// MakePrintable takes a DNS message and makes it nicer to be printed as JSON,YAML,
// and XML. Little is changed beyond naming
func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) {
// and XML. Little is changed beyond naming.
func MakePrintable(msg *dns.Msg, opts util.Options) (*Message, error) {
var err error
ret := Message{
Header: msg.MsgHdr,
}
@ -56,11 +63,12 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) {
if opts.Display.UcodeTranslate {
name, err = idna.ToUnicode(question.Name)
if err != nil {
return nil, fmt.Errorf("punycode: error translating to unicode: %w", err)
return nil, fmt.Errorf("punycode to unicode: %w", err)
}
} else {
name = question.Name
}
ret.Question = append(ret.Question, Question{
Name: name,
Type: dns.TypeToString[question.Qtype],
@ -70,10 +78,12 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) {
for _, answer := range msg.Answer {
temp := strings.Split(answer.String(), "\t")
var (
ttl string
name string
)
if opts.ShowTTL {
if opts.HumanTTL {
ttl = (time.Duration(answer.Header().Ttl) * time.Second).String()
@ -81,14 +91,16 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) {
ttl = strconv.Itoa(int(answer.Header().Ttl))
}
}
if opts.Display.UcodeTranslate {
name, err = idna.ToUnicode(answer.Header().Name)
if err != nil {
return nil, fmt.Errorf("punycode: error translating to unicode: %w", err)
return nil, fmt.Errorf("punycode to unicode: %w", err)
}
} else {
name = answer.Header().Name
}
ret.Answer = append(ret.Answer, Answer{
RRHeader: RRHeader{
Name: name,
@ -103,10 +115,12 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) {
for _, ns := range msg.Ns {
temp := strings.Split(ns.String(), "\t")
var (
ttl string
name string
)
if opts.ShowTTL {
if opts.HumanTTL {
ttl = (time.Duration(ns.Header().Ttl) * time.Second).String()
@ -114,14 +128,16 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) {
ttl = strconv.Itoa(int(ns.Header().Ttl))
}
}
if opts.Display.UcodeTranslate {
name, err = idna.ToUnicode(ns.Header().Name)
if err != nil {
return nil, fmt.Errorf("punycode: error translating to unicode: %w", err)
return nil, fmt.Errorf("punycode to unicode: %w", err)
}
} else {
name = ns.Header().Name
}
ret.Ns = append(ret.Ns, Answer{
RRHeader: RRHeader{
Name: name,
@ -139,10 +155,12 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) {
continue
} else {
temp := strings.Split(additional.String(), "\t")
var (
ttl string
name string
)
if opts.ShowTTL {
if opts.HumanTTL {
ttl = (time.Duration(additional.Header().Ttl) * time.Second).String()
@ -150,14 +168,16 @@ func MakePrintable(msg *dns.Msg, opts cli.Options) (*Message, error) {
ttl = strconv.Itoa(int(additional.Header().Ttl))
}
}
if opts.Display.UcodeTranslate {
name, err = idna.ToUnicode(additional.Header().Name)
if err != nil {
return nil, fmt.Errorf("punycode: error translating to unicode: %w", err)
return nil, fmt.Errorf("punycode to unicode: %w", err)
}
} else {
name = additional.Header().Name
}
ret.Extra = append(ret.Extra, Answer{
RRHeader: RRHeader{
Name: name,

View file

@ -5,339 +5,197 @@ package query_test
import (
"testing"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/query"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
"gotest.tools/v3/assert"
)
func TestRealPrint(t *testing.T) {
t.Parallel()
opts := []util.Options{
{
Logger: util.InitLogger(0),
Port: 53,
TCP: true,
ShowQuery: true,
RD: true,
ShowTTL: true,
HumanTTL: true,
JSON: true,
Display: util.Displays{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
},
Request: util.Request{
Server: "a.gtld-servers.net",
Type: dns.StringToType["NS"],
Class: 1,
Name: "google.com.",
},
EDNS: util.EDNS{
EnableEDNS: false,
},
},
{
Logger: util.InitLogger(0),
Port: 53,
TCP: true,
ShowQuery: true,
RD: true,
Verbosity: 0,
ShowTTL: true,
Short: true,
Identify: true,
YAML: false,
Display: util.Displays{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
},
Request: util.Request{
Server: "ns1.google.com",
Type: dns.StringToType["NS"],
Class: 1,
Name: "google.com.",
Timeout: 0,
Retries: 0,
},
EDNS: util.EDNS{
EnableEDNS: false,
},
},
{
Logger: util.InitLogger(0),
Port: 53,
HTTPS: true,
ShowQuery: true,
RD: true,
ShowTTL: true,
HumanTTL: true,
Identify: true,
XML: true,
Display: util.Displays{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: false,
},
Request: util.Request{
Server: "https://dns.froth.zone/dns-query",
Type: dns.StringToType["NS"],
Class: 1,
Name: "freecumextremist.com.",
Timeout: 0,
Retries: 0,
},
EDNS: util.EDNS{
EnableEDNS: false,
DNSSEC: true,
},
},
{
Logger: util.InitLogger(0),
Port: 853,
TLS: true,
ShowQuery: true,
RD: true,
Verbosity: 0,
ShowTTL: true,
Identify: true,
YAML: false,
Display: util.Displays{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
},
Request: util.Request{
Server: "dns.google",
Type: dns.StringToType["NS"],
Class: 1,
Name: "freecumextremist.com.",
},
},
{
Logger: util.InitLogger(0),
Port: 53,
TCP: true,
ShowQuery: true,
AA: true,
RD: true,
Verbosity: 0,
ShowTTL: true,
YAML: true,
Display: util.Displays{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: false,
},
Request: util.Request{
Server: "rin.froth.zone",
Type: dns.StringToType["A"],
Class: 1,
Name: "froth.zone.",
Timeout: 0,
Retries: 0,
},
EDNS: util.EDNS{
EnableEDNS: true,
Cookie: true,
Padding: true,
},
},
}
for _, test := range opts {
test := test
t.Run("", func(t *testing.T) {
t.Parallel()
resp, err := query.CreateQuery(test)
assert.NilError(t, err)
if test.JSON || test.XML || test.YAML {
str, err := query.PrintSpecial(resp.DNS, test)
assert.NilError(t, err)
assert.Assert(t, str != "")
}
str := query.ToString(resp, test)
assert.Assert(t, str != "")
})
}
}
func TestBadFormat(t *testing.T) {
t.Parallel()
_, err := query.PrintSpecial(new(dns.Msg), cli.Options{})
_, err := query.PrintSpecial(new(dns.Msg), util.Options{})
assert.ErrorContains(t, err, "never happen")
}
func TestPrinting(t *testing.T) {
func TestEmpty(t *testing.T) {
t.Parallel()
opts := cli.Options{
Logger: util.InitLogger(0),
Port: 53,
IPv4: false,
IPv6: false,
TCP: true,
DNSCrypt: false,
TLS: false,
HTTPS: false,
QUIC: false,
Truncate: false,
ShowQuery: true,
AA: false,
AD: false,
CD: false,
QR: false,
RD: true,
RA: false,
TC: false,
Z: false,
Reverse: false,
Verbosity: 0,
HumanTTL: false,
ShowTTL: true,
Short: false,
Identify: false,
JSON: true,
XML: false,
YAML: false,
Display: cli.Displays{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
},
Request: helpers.Request{
Server: "a.gtld-servers.net",
Type: dns.StringToType["NS"],
Class: 1,
Name: "google.com.",
Timeout: 0,
Retries: 0,
},
EDNS: cli.EDNS{
EnableEDNS: false,
},
}
resp, err := query.CreateQuery(opts)
assert.NilError(t, err)
str, err := query.PrintSpecial(resp.DNS, opts)
assert.NilError(t, err)
assert.Assert(t, str != "")
}
func TestPrinting2(t *testing.T) {
t.Parallel()
opts := cli.Options{
Logger: util.InitLogger(0),
Port: 53,
IPv4: false,
IPv6: false,
TCP: true,
DNSCrypt: false,
TLS: false,
HTTPS: false,
QUIC: false,
Truncate: false,
ShowQuery: true,
AA: false,
AD: false,
CD: false,
QR: false,
RD: true,
RA: false,
TC: false,
Z: false,
Reverse: false,
Verbosity: 0,
HumanTTL: false,
ShowTTL: true,
Short: true,
Identify: true,
JSON: false,
XML: false,
YAML: true,
Display: cli.Displays{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
},
Request: helpers.Request{
Server: "ns1.google.com",
Type: dns.StringToType["NS"],
Class: 1,
Name: "google.com.",
Timeout: 0,
Retries: 0,
},
EDNS: cli.EDNS{
EnableEDNS: false,
},
}
resp, err := query.CreateQuery(opts)
assert.NilError(t, err)
str, err := query.PrintSpecial(resp.DNS, opts)
assert.NilError(t, err)
assert.Assert(t, str != "")
str = query.ToString(resp, opts)
assert.Assert(t, str != "")
}
func TestPrinting3(t *testing.T) {
t.Parallel()
opts := cli.Options{
Logger: util.InitLogger(0),
Port: 53,
IPv4: false,
IPv6: false,
TCP: false,
DNSCrypt: false,
TLS: false,
HTTPS: true,
QUIC: false,
Truncate: false,
ShowQuery: true,
AA: false,
AD: false,
CD: false,
QR: false,
RD: true,
RA: false,
TC: false,
Z: false,
Reverse: false,
Verbosity: 0,
HumanTTL: false,
ShowTTL: true,
Short: false,
Identify: true,
JSON: false,
XML: false,
YAML: true,
Display: cli.Displays{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
},
Request: helpers.Request{
Server: "https://dns.froth.zone/dns-query",
Type: dns.StringToType["NS"],
Class: 1,
Name: "freecumextremist.com.",
Timeout: 0,
Retries: 0,
},
EDNS: cli.EDNS{
EnableEDNS: false,
},
}
resp, err := query.CreateQuery(opts)
assert.NilError(t, err)
str, err := query.PrintSpecial(resp.DNS, opts)
assert.NilError(t, err)
assert.Assert(t, str != "")
str = query.ToString(resp, opts)
assert.Assert(t, str != "")
}
func TestPrinting4(t *testing.T) {
t.Parallel()
opts := cli.Options{
Logger: util.InitLogger(0),
Port: 853,
IPv4: false,
IPv6: false,
TCP: false,
DNSCrypt: false,
TLS: true,
HTTPS: false,
QUIC: false,
Truncate: false,
ShowQuery: true,
AA: false,
AD: false,
CD: false,
QR: false,
RD: true,
RA: false,
TC: false,
Z: false,
Reverse: false,
Verbosity: 0,
HumanTTL: false,
ShowTTL: true,
Short: false,
Identify: true,
JSON: false,
XML: false,
YAML: true,
Display: cli.Displays{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
},
Request: helpers.Request{
Server: "dns.google",
Type: dns.StringToType["NS"],
Class: 1,
Name: "freecumextremist.com.",
Timeout: 0,
Retries: 0,
},
EDNS: cli.EDNS{
EnableEDNS: false,
},
}
resp, err := query.CreateQuery(opts)
assert.NilError(t, err)
str, err := query.PrintSpecial(resp.DNS, opts)
assert.NilError(t, err)
assert.Assert(t, str != "")
str = query.ToString(resp, opts)
assert.Assert(t, str != "")
}
func TestPrinting5(t *testing.T) {
t.Parallel()
opts := cli.Options{
Logger: util.InitLogger(0),
Port: 53,
IPv4: false,
IPv6: false,
TCP: true,
DNSCrypt: false,
TLS: false,
HTTPS: false,
QUIC: false,
Truncate: false,
ShowQuery: true,
AA: true,
AD: false,
CD: false,
QR: false,
RD: true,
RA: false,
TC: false,
Z: false,
Reverse: false,
Verbosity: 0,
HumanTTL: false,
ShowTTL: true,
Short: false,
Identify: false,
JSON: false,
XML: false,
YAML: true,
Display: cli.Displays{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
},
Request: helpers.Request{
Server: "rin.froth.zone",
Type: dns.StringToType["A"],
Class: 1,
Name: "froth.zone.",
Timeout: 0,
Retries: 0,
},
EDNS: cli.EDNS{
EnableEDNS: true,
Cookie: true,
},
}
resp, err := query.CreateQuery(opts)
assert.NilError(t, err)
str, err := query.PrintSpecial(resp.DNS, opts)
assert.NilError(t, err)
assert.Assert(t, str != "")
str = query.ToString(resp, opts)
assert.Assert(t, str != "")
}
func TestToString6(t *testing.T) {
assert.Assert(t, query.ToString(*new(helpers.Response), *new(cli.Options)) == "<nil> MsgHdr")
assert.Assert(t, query.ToString(util.Response{}, util.Options{}) == "<nil> MsgHdr")
}

View file

@ -6,13 +6,19 @@ import (
"fmt"
"strconv"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/util"
"github.com/dchest/uniuri"
"github.com/miekg/dns"
)
func CreateQuery(opts cli.Options) (helpers.Response, error) {
const (
tcp = "tcp"
udp = "udp"
)
// CreateQuery creates a DNS query from the options given.
// It sets query flags and EDNS flags from the respective options.
func CreateQuery(opts util.Options) (util.Response, error) {
req := new(dns.Msg)
req.SetQuestion(opts.Request.Name, opts.Request.Type)
req.Question[0].Qclass = opts.Request.Class
@ -40,37 +46,45 @@ func CreateQuery(opts cli.Options) (helpers.Response, error) {
e.Code = dns.EDNS0COOKIE
e.Cookie = uniuri.NewLenChars(8, []byte("1234567890abcdef"))
o.Option = append(o.Option, e)
opts.Logger.Info("Setting EDNS cookie to", e.Cookie)
}
if opts.EDNS.Expire {
o.Option = append(o.Option, new(dns.EDNS0_EXPIRE))
opts.Logger.Info("Setting EDNS Expire option")
}
if opts.EDNS.KeepOpen {
o.Option = append(o.Option, new(dns.EDNS0_TCP_KEEPALIVE))
opts.Logger.Info("Setting EDNS TCP Keepalive option")
}
if opts.EDNS.Nsid {
o.Option = append(o.Option, new(dns.EDNS0_NSID))
opts.Logger.Info("Setting EDNS NSID option")
}
if opts.EDNS.Padding {
o.Option = append(o.Option, new(dns.EDNS0_PADDING))
opts.Logger.Info("Setting EDNS padding")
}
o.SetUDPSize(opts.BufSize)
opts.Logger.Info("EDNS UDP buffer set to", opts.BufSize)
o.SetZ(opts.EDNS.ZFlag)
opts.Logger.Info("EDNS Z flag set to", opts.EDNS.ZFlag)
if opts.EDNS.DNSSEC {
o.SetDo()
opts.Logger.Info("EDNS DNSSEC OK set")
}
@ -84,39 +98,45 @@ func CreateQuery(opts cli.Options) (helpers.Response, error) {
opts.Logger.Warn("DNSSEC implies EDNS, EDNS enabled")
opts.Logger.Info("DNSSEC enabled, UDP buffer set to 1232")
}
opts.Logger.Debug(req)
if !opts.Short {
if opts.ShowQuery {
opts.Logger.Info("Printing constructed query")
var (
str string
err error
)
if opts.JSON || opts.XML || opts.YAML {
str, err = PrintSpecial(req, opts)
if err != nil {
return helpers.Response{}, err
return util.Response{}, err
}
} else {
temp := opts.Display.Statistics
opts.Display.Statistics = false
str = ToString(helpers.Response{
str = ToString(util.Response{
DNS: req,
RTT: 0,
}, opts)
opts.Display.Statistics = temp
str += "\n;; QUERY SIZE: " + strconv.Itoa(req.Len())
str += "\n;; QUERY SIZE: " + strconv.Itoa(req.Len()) + "\n"
}
fmt.Println(str)
opts.ShowQuery = false
}
}
resolver, err := LoadResolver(opts)
if err != nil {
return helpers.Response{}, err
return util.Response{}, err
}
opts.Logger.Info("Query successfully loaded")
//nolint:wrapcheck // Error wrapping not needed here

View file

@ -5,8 +5,6 @@ package query_test
import (
"testing"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/query"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
@ -15,32 +13,30 @@ import (
func TestCreateQ(t *testing.T) {
t.Parallel()
in := []cli.Options{
in := []util.Options{
{
Logger: util.InitLogger(0),
Port: 53,
QR: false,
Z: true,
RD: false,
ShowQuery: true,
YAML: true,
Request: helpers.Request{
Request: util.Request{
Server: "8.8.4.4",
Type: dns.TypeA,
Name: "example.com.",
},
Display: cli.Displays{
Comments: true,
Question: true,
Opt: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: false,
Display: util.Displays{
Comments: true,
Question: true,
Opt: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
},
EDNS: cli.EDNS{
EDNS: util.EDNS{
EnableEDNS: true,
DNSSEC: true,
Cookie: true,
@ -52,18 +48,16 @@ func TestCreateQ(t *testing.T) {
{
Logger: util.InitLogger(0),
Port: 53,
QR: false,
Z: true,
RD: false,
ShowQuery: true,
XML: true,
Request: helpers.Request{
Request: util.Request{
Server: "8.8.4.4",
Type: dns.TypeA,
Name: "example.com.",
},
Display: cli.Displays{
Display: util.Displays{
Comments: true,
Question: true,
Opt: true,
@ -73,26 +67,52 @@ func TestCreateQ(t *testing.T) {
Statistics: true,
UcodeTranslate: true,
},
EDNS: cli.EDNS{
EnableEDNS: false,
DNSSEC: false,
Cookie: false,
Expire: false,
KeepOpen: false,
Nsid: false,
},
{
Logger: util.InitLogger(0),
Port: 853,
// Z: true,
ShowQuery: true,
JSON: true,
QUIC: true,
Request: util.Request{
Server: "dns.adguard.com",
Type: dns.TypeA,
Name: "example.com.",
},
Display: util.Displays{
Comments: true,
Question: true,
Opt: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
},
EDNS: util.EDNS{
EnableEDNS: true,
DNSSEC: true,
Cookie: true,
Expire: true,
Nsid: true,
},
},
}
for _, opt := range in {
opt := opt
t.Run("", func(t *testing.T) {
t.Parallel()
res, err := query.CreateQuery(opt)
assert.NilError(t, err)
assert.Assert(t, res != helpers.Response{})
assert.Assert(t, res != util.Response{})
str, err := query.PrintSpecial(res.DNS, opt)
assert.NilError(t, err)
assert.Assert(t, str != "")
str = query.ToString(res, opt)
assert.Assert(t, str != "")
})

View file

@ -7,44 +7,49 @@ import (
"strconv"
"strings"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
)
// Main resolver interface
// Resolver is the main resolver interface.
type Resolver interface {
LookUp(*dns.Msg) (helpers.Response, error)
LookUp(*dns.Msg) (util.Response, error)
}
// LoadResolver loads the respective resolver for performing a DNS query
func LoadResolver(opts cli.Options) (Resolver, error) {
// LoadResolver loads the respective resolver for performing a DNS query.
func LoadResolver(opts util.Options) (Resolver, error) {
switch {
case opts.HTTPS:
opts.Logger.Info("loading DNS-over-HTTPS resolver")
if !strings.HasPrefix(opts.Request.Server, "https://") {
opts.Request.Server = "https://" + opts.Request.Server
}
return &HTTPSResolver{
opts: opts,
}, nil
case opts.QUIC:
opts.Logger.Info("loading DNS-over-QUIC resolver")
opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Port))
return &QUICResolver{
opts: opts,
}, nil
case opts.DNSCrypt:
opts.Logger.Info("loading DNSCrypt resolver")
if !strings.HasPrefix(opts.Request.Server, "sdns://") {
opts.Request.Server = "sdns://" + opts.Request.Server
}
return &DNSCryptResolver{
opts: opts,
}, nil
default:
opts.Logger.Info("loading standard/DNS-over-TLS resolver")
opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Port))
return &StandardResolver{
opts: opts,
}, nil

View file

@ -7,28 +7,29 @@ import (
"strings"
"time"
"git.froth.zone/sam/awl/cli"
"git.froth.zone/sam/awl/internal/helpers"
"git.froth.zone/sam/awl/util"
"github.com/miekg/dns"
)
// Overall DNS response message
// Message is for overall DNS responses.
//
//nolint:fieldalignment // IMO this looks better when printed like this
type Message struct {
Header dns.MsgHdr `json:"header,omitempty" xml:"header,omitempty" yaml:",omitempty"`
Question []Question `json:"question,omitempty" xml:"question,omitempty" yaml:",omitempty"`
Answer []Answer `json:"answer,omitempty" xml:"answer,omitempty" yaml:",omitempty"`
Ns []Answer `json:"ns,omitempty" xml:"ns,omitempty" yaml:",omitempty"`
Extra []Answer `json:"extra,omitempty" xml:"extra,omitempty" yaml:",omitempty"`
Header dns.MsgHdr `json:"header,omitempty" xml:"header,omitempty" yaml:",omitempty"`
}
// DNS Query
// Question is a DNS Query.
type Question struct {
Name string `json:"name,omitempty" xml:"name,omitempty" yaml:",omitempty"`
Type string `json:"type,omitempty" xml:"type,omitempty" yaml:",omitempty"`
Class string `json:"class,omitempty" xml:"class,omitempty" yaml:",omitempty"`
}
// DNS Resource Headers
// RRHeader is for DNS Resource Headers.
type RRHeader struct {
Name string `json:"name,omitempty" xml:"name,omitempty" yaml:",omitempty"`
Type string `json:"type,omitempty" xml:"type,omitempty" yaml:",omitempty"`
@ -37,21 +38,26 @@ type RRHeader struct {
Rdlength uint16 `json:"-" xml:"-" yaml:"-"`
}
// DNS Response
// Answer is for a DNS Response.
//
//nolint:fieldalignment // IMO this looks better when printed like this
type Answer struct {
RRHeader `json:"header,omitempty" xml:"header,omitempty" yaml:"header,omitempty"`
Value string `json:"response,omitempty" xml:"response,omitempty" yaml:"response,omitempty"`
RRHeader `json:"header,omitempty" xml:"header,omitempty" yaml:"header,omitempty"`
}
// ToString turns the response into something that looks a lot like dig
//
// Much of this is taken from https://github.com/miekg/dns/blob/master/msg.go#L900
func ToString(res helpers.Response, opts cli.Options) string {
func ToString(res util.Response, opts util.Options) string {
if res.DNS == nil {
return "<nil> MsgHdr"
}
var s string
var opt *dns.OPT
var (
s string
opt *dns.OPT
)
if !opts.Short {
if opts.Display.Comments {
@ -61,26 +67,31 @@ func ToString(res helpers.Response, opts cli.Options) string {
s += "AUTHORITY: " + strconv.Itoa(len(res.DNS.Ns)) + ", "
s += "ADDITIONAL: " + strconv.Itoa(len(res.DNS.Extra)) + "\n"
opt = res.DNS.IsEdns0()
if opt != nil && opts.Display.Opt {
// OPT PSEUDOSECTION
s += opt.String() + "\n"
}
}
if opts.Display.Question {
if len(res.DNS.Question) > 0 {
if opts.Display.Comments {
s += "\n;; QUESTION SECTION:\n"
}
for _, r := range res.DNS.Question {
s += r.String() + "\n"
}
}
}
if opts.Display.Answer {
if len(res.DNS.Answer) > 0 {
if opts.Display.Comments {
s += "\n;; ANSWER SECTION:\n"
}
for _, r := range res.DNS.Answer {
if r != nil {
s += r.String() + "\n"
@ -88,11 +99,13 @@ func ToString(res helpers.Response, opts cli.Options) string {
}
}
}
if opts.Display.Authority {
if len(res.DNS.Ns) > 0 {
if opts.Display.Comments {
s += "\n;; AUTHORITY SECTION:\n"
}
for _, r := range res.DNS.Ns {
if r != nil {
s += r.String() + "\n"
@ -100,11 +113,13 @@ func ToString(res helpers.Response, opts cli.Options) string {
}
}
}
if opts.Display.Additional {
if len(res.DNS.Extra) > 0 && (opt == nil || len(res.DNS.Extra) > 1) {
if opts.Display.Comments {
s += "\n;; ADDITIONAL SECTION:\n"
}
for _, r := range res.DNS.Extra {
if r != nil && r.Header().Rrtype != dns.TypeOPT {
s += r.String() + "\n"
@ -112,10 +127,12 @@ func ToString(res helpers.Response, opts cli.Options) string {
}
}
}
if opts.Display.Statistics {
s += "\n;; Query time: " + res.RTT.String()
// Add extra information to server string
var extra string
switch {
case opts.TCP:
extra = ":" + strconv.Itoa(opts.Port) + " (TCP)"
@ -138,14 +155,15 @@ func ToString(res helpers.Response, opts cli.Options) string {
for i, resp := range res.DNS.Answer {
temp := strings.Split(resp.String(), "\t")
s += temp[len(temp)-1]
if opts.Identify {
s += " from server " + opts.Request.Server + " in " + res.RTT.String()
}
// Don't print newline on last line
if i != len(res.DNS.Answer)-1 {
s += "\n"
}
}
}

View file

@ -1,14 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package query_test
import (
"testing"
"gotest.tools/v3/assert"
)
// TODO
func TestToString(t *testing.T) {
assert.Assert(t, 1 == 1+0)
}

View file

@ -1,8 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
":npm",
":gomod"
]
"extends": ["config:base", ":npm", ":gomod"]
}

View file

@ -1,13 +1,18 @@
# SPDX-License-Identifier: BSD-3-Clause
# Template for the BSD/GNU makefiles
HASH ?= `git describe --always --dirty || echo "UNKNOWN"`
VER ?= "git-$(HASH)"
HASH ?= `git describe --always --dirty --broken || echo "UNKNOWN"`
VERSION ?= "git-$(HASH)"
SOURCES ?= $(shell find . -name "*.go" -type f ! -name '*_test*')
TEST_SOURCES ?= $(shell find . -name "*_test.go" -type f)
CGO_ENABLED ?= 0
GO ?= go
TEST ?= $(GO) test -v -race
COVER ?= $(GO) tool cover
GOFLAGS ?= -ldflags "-s -w -X=main.version=$(VER)" -trimpath
GOFLAGS ?= -ldflags "-s -w -X=main.version=$(VERSION)" -trimpath
DESTDIR :=
PREFIX ?= /usr/local
BIN ?= bin
@ -20,15 +25,25 @@ PROG ?= awl
# hehe
all: $(PROG) doc/$(PROG).1
doc/$(PROG).1: doc/wiki/$(PROG).1.md
@cp doc/awl.1 doc/awl.bak
$(SCDOC) <doc/wiki/$(PROG).1.md >doc/$(PROG).1 && rm doc/awl.bak || mv doc/awl.bak doc/awl.1
doc/$(PROG).1: doc/$(PROG).1.scd
$(SCDOC) <doc/$(PROG).1.scd >doc/$(PROG).1
## test: run go test
test:
test: $(TEST_SOURCES)
$(GO) test -cover -coverprofile=coverage/coverage.out ./...
test-ci:
$(TEST)
## fuzz: runs fuzz tests
fuzz:
cd cli
$(TEST) -fuzz=FuzzFlags -fuzztime 10000x
$(TEST) -fuzz=FuzzDig -fuzztime 10000x
$(TEST) -fuzz=FuzzParseArgs -fuzztime 10000x
cd ..
coverage/coverage.out: test
$(COVER) -func=coverage/coverage.out
$(COVER) -html=coverage/coverage.out -o coverage/cover.html
@ -44,11 +59,12 @@ vet:
## lint: lint awl, using fmt, vet and golangci-lint
lint: fmt vet
-golangci-lint run --fix
golangci-lint run --fix
## clean: clean the build files
clean:
$(GO) clean
rm doc/$(PROG).1
## help: Prints this help message
help:

View file

@ -1,2 +1,2 @@
// Helper functions
// Package util contains helper functions that don't belong anywhere else
package util

View file

@ -4,11 +4,11 @@ package util
import "git.froth.zone/sam/awl/logawl"
// Initialize the logawl instance.
func InitLogger(verbosity int) (logger *logawl.Logger) {
logger = logawl.New()
// InitLogger initializes the logawl instance.
func InitLogger(verbosity int) (log *logawl.Logger) {
log = logawl.New()
logger.SetLevel(logawl.Level(verbosity))
log.SetLevel(logawl.Level(verbosity))
return logger
return log
}

View file

@ -12,6 +12,7 @@ import (
func TestInitLogger(t *testing.T) {
t.Parallel()
logger := util.InitLogger(0)
assert.Equal(t, logger.Level, logawl.Level(0))
}

113
util/options.go Normal file
View file

@ -0,0 +1,113 @@
// SPDX-License-Identifier: BSD-3-Clause
package util
import (
"fmt"
"net"
"git.froth.zone/sam/awl/logawl"
"github.com/miekg/dns"
)
// Options is the grand CLI options structure.
type Options struct {
Logger *logawl.Logger
TLSHost string
EDNS
Request Request
Port int
Verbosity int
Display Displays
TC bool
ShowTTL bool
ShowQuery bool
AA bool
AD bool
CD bool
QR bool
RD bool
RA bool
IPv4 bool
Z bool
Reverse bool
HumanTTL bool
Truncate bool
Short bool
Identify bool
JSON bool
XML bool
YAML bool
QUIC bool
HTTPS bool
TLSNoVerify bool
TLS bool
DNSCrypt bool
TCP bool
IPv6 bool
}
// Displays contains toggles for what to (and not to) display.
type Displays struct {
Comments bool
Question bool // QUESTION SECTION
Opt bool // OPT PSEUDOSECTION
Answer bool // ANSWER SECTION
Authority bool // AUTHORITY SECTION
Additional bool // ADDITIONAL SECTION
Statistics bool // Query time, message size, etc.
UcodeTranslate bool // Translate Punycode back to Unicode
}
// EDNS contains toggles for various EDNS options.
type EDNS struct {
Subnet dns.EDNS0_SUBNET
ZFlag uint16
BufSize uint16
EnableEDNS bool
Cookie bool
DNSSEC bool
Expire bool
KeepOpen bool
Nsid bool
Padding bool
Version uint8
}
// ParseSubnet takes a subnet argument and makes it into one that the DNS library
// understands.
func ParseSubnet(subnet string, opts *Options) error {
ip, inet, err := net.ParseCIDR(subnet)
if err != nil {
// TODO: make not a default?
if subnet == "0" {
opts.EDNS.Subnet = dns.EDNS0_SUBNET{
Code: dns.EDNS0SUBNET,
Family: 1,
SourceNetmask: 0,
SourceScope: 0,
Address: net.IPv4(0, 0, 0, 0),
}
return nil
}
return fmt.Errorf("EDNS subnet parsing: %w", err)
}
sub, _ := inet.Mask.Size()
opts.EDNS.Subnet = dns.EDNS0_SUBNET{}
opts.EDNS.Subnet.Address = ip
opts.EDNS.Subnet.SourceNetmask = uint8(sub)
switch ip.To4() {
case nil:
// Not a valid IPv4 so assume IPv6
opts.EDNS.Subnet.Family = 2
default:
// Valid IPv4
opts.EDNS.Subnet.Family = 1
}
return nil
}

37
util/options_test.go Normal file
View file

@ -0,0 +1,37 @@
// SPDX-License-Identifier: BSD-3-Clause
package util_test
import (
"testing"
"git.froth.zone/sam/awl/util"
"gotest.tools/v3/assert"
)
func TestSubnet(t *testing.T) {
t.Parallel()
subnet := []string{
"0.0.0.0/0",
"::0/0",
"0",
"127.0.0.1/32",
}
for _, test := range subnet {
test := test
t.Run(test, func(t *testing.T) {
t.Parallel()
err := util.ParseSubnet(test, new(util.Options))
assert.NilError(t, err)
})
}
}
func TestInvalidSub(t *testing.T) {
t.Parallel()
err := util.ParseSubnet("1", new(util.Options))
assert.ErrorContains(t, err, "invalid CIDR address")
}

25
util/query.go Normal file
View file

@ -0,0 +1,25 @@
// SPDX-License-Identifier: BSD-3-Clause
package util
import (
"time"
"github.com/miekg/dns"
)
// Response is the DNS response.
type Response struct {
DNS *dns.Msg // The full DNS response
RTT time.Duration `json:"rtt"` // The time it took to make the DNS query
}
// Request is a structure for a DNS query.
type Request struct {
Server string `json:"server"`
Name string `json:"name"`
Timeout time.Duration
Retries int
Type uint16 `json:"request"`
Class uint16 `json:"class"`
}

View file

@ -17,14 +17,15 @@ func (e *errReverseDNS) Error() string {
return fmt.Sprintf("reverseDNS: invalid value %s given", e.addr)
}
// Given an IP or phone number, return a canonical string to be queried.
// ReverseDNS is given an IP or phone number and returns a canonical string to be queried.
func ReverseDNS(address string, querInt uint16) (string, error) {
query := dns.TypeToString[querInt]
if query == "PTR" {
str, err := dns.ReverseAddr(address)
if err != nil {
return "", fmt.Errorf("could not reverse: %w", err)
return "", fmt.Errorf("PTR reverse: %w", err)
}
return str, nil
} else if query == "NAPTR" {
// get rid of characters not needed
@ -38,6 +39,7 @@ func ReverseDNS(address string, querInt uint16) (string, error) {
fmt.Fprintf(&arpa, "%c.", c)
}
arpa.WriteString("e164.arpa.")
return arpa.String(), nil
}
@ -50,5 +52,6 @@ func reverse(s string) string {
for i, j := 0, len(rns)-1; i < j; i, j = i+1, j-1 {
rns[i], rns[j] = rns[j], rns[i]
}
return string(rns)
}

View file

@ -17,6 +17,7 @@ var (
func TestIPv4(t *testing.T) {
t.Parallel()
act, err := util.ReverseDNS("8.8.4.4", PTR)
assert.NilError(t, err)
assert.Equal(t, act, "4.4.8.8.in-addr.arpa.", "IPv4 reverse")
@ -24,6 +25,7 @@ func TestIPv4(t *testing.T) {
func TestIPv6(t *testing.T) {
t.Parallel()
act, err := util.ReverseDNS("2606:4700:4700::1111", PTR)
assert.NilError(t, err)
assert.Equal(t, act, "1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa.", "IPv6 reverse")
@ -31,6 +33,7 @@ func TestIPv6(t *testing.T) {
func TestNAPTR(t *testing.T) {
t.Parallel()
tests := []struct {
in string
want string
@ -54,12 +57,14 @@ func TestNAPTR(t *testing.T) {
func TestInvalid(t *testing.T) {
t.Parallel()
_, err := util.ReverseDNS("AAAAA", 1)
assert.ErrorContains(t, err, "invalid value AAAAA given")
}
func TestInvalid2(t *testing.T) {
t.Parallel()
_, err := util.ReverseDNS("1.0", PTR)
assert.ErrorContains(t, err, "could not reverse")
assert.ErrorContains(t, err, "PTR reverse")
}