Compare commits

...

No commits in common. "master" and "pages" have entirely different histories.

84 changed files with 2 additions and 6254 deletions

1
.domains Normal file
View File

@ -0,0 +1 @@
dns.froth.zone

View File

@ -1,10 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -1,35 +0,0 @@
name: Forgejo Release
on:
push:
tags:
- "*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: stable
- name: Install scdoc
run: apt-get update && apt-get install -y scdoc
- name: Release with GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --clean --skip docker,snapcraft
env:
GORELEASER_FORCE_TOKEN: gitea
GITEA_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
UPLOAD_PACKAGES_SECRET: ${{ secrets.PUBLISH_TOKEN }}

View File

@ -1,14 +0,0 @@
name: Mirror Push
on: push
jobs:
mirror:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: git@git.sr.ht:~sammefishe/awl
ssh_private_key: ${{ secrets.SRHT_SSH_KEY }}

View File

@ -1,24 +0,0 @@
name: Test
on: push
jobs:
test:
strategy:
fail-fast: true
matrix:
goVer: ["oldstable", "stable"]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.goVer }}
- name: Test
run: make test-ci

View File

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

View File

@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest a feature
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
Links to implementations in dig, drill, etc. should go here.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,9 +0,0 @@
<!--
Please check the following:
1. Make sure you are targeting the `master` branch.
2. Make sure that you test and format your contributions: `make full_test && make lint`
3. Describe what your pull request does and which issue you're targeting (if any)
-->

View File

@ -1,56 +0,0 @@
name: GitHub Release
on:
push:
tags:
- "*"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
release:
runs-on: ubuntu-latest
env:
SNAPCRAFT_STORE_CREDENTIALS : ${{ secrets.SNAPCRAFT_TOKEN }}
steps:
# Workaround a dumb docker problem where everything has to be lowercase
- id: lowercase
run: |
echo IMAGE_NAME=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]') >> "${GITHUB_ENV}"
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v2
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: Install scdoc
run: sudo apt-get install -y scdoc
- name: Workaround a dumb Snap bug
run: mkdir -p $HOME/.cache/snapcraft/download && mkdir -p $HOME/.cache/snapcraft/stage-packages
- name: Release with GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --clean --skip=aur,homebrew,nix,scoop
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,24 +0,0 @@
name: Test
on: push
jobs:
test:
strategy:
fail-fast: true
matrix:
platform: [macos, windows]
goVer: ["oldstable", "stable"]
runs-on: ${{ matrix.platform }}-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.goVer }}
- name: Test
run: make test-ci

29
.gitignore vendored
View File

@ -1,29 +0,0 @@
# 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
docs/awl.1
docs/awl.1.gz
.dccache

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "docs/wiki"]
path = docs/wiki
url = ../awl.wiki

View File

@ -1,88 +0,0 @@
# 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
- tagliatelle
- whitespace
- wrapcheck
- wsl
disable:
- structcheck
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:
- 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
linters-settings:
tagliatelle:
case:
use-field-name: false
rules:
# Any struct tag type can be used.
# Support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower`
json: goCamel
yaml: goCamel
xml: goCamel
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0

View File

@ -1,236 +0,0 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
- make clean
- go mod tidy
# Make manpages
- docs/makeman.sh
# Vendor dependencies
- go mod vendor
builds:
- env:
- CGO_ENABLED=0
targets:
- go_first_class
- plan9_amd64
- freebsd_amd64
universal_binaries:
- replace: true
archives:
- files:
- LICENSE
- completions/**
- docs/awl.1.gz
name_template: >-
{{ .ProjectName }}_
{{- if eq .Os "darwin" }}MacOS_
{{- else if eq .Os "freebsd" }}FreeBSD_
{{- else }}{{- title .Os }}_{{ end }}
{{- if eq .Arch "386" }}i386
{{- else if eq .Arch "mips64" }}mips64_hardfloat
{{- else if eq .Arch "mips64le" }}mips64le_hardfloat
{{- else }}{{ .Arch }}{{ end -}}
format_overrides:
- goos: windows
format: zip
- files:
- vendor/**
id: vendor
format: tar.xz
name_template: "{{ .ProjectName }}-{{ .Version }}-deps"
meta: true
wrap_in_directory: "{{ .ProjectName }}"
nfpms:
- id: packages
package_name: awl-dns
vendor: Sam Therapy <sam@samtherapy.net>
maintainer: Sam Therapy <sam@samtherapy.net>
homepage: https://dns.froth.zone/awl
description: |-
Command-line DNS query tool.
Awl supports DNS-over-[UDP,TCP,HTTPS,QUIC] and DNSCrypt.
license: BSD-3-Clause
section: utils
bindir: /usr/bin
formats:
- apk
- archlinux
- deb
- rpm
contents:
- src: completions/bash.bash
dst: /usr/share/bash-completion/completions/awl
- src: docs/awl.1.gz
dst: /usr/share/man/man1/awl.1.gz
- src: LICENSE
dst: /usr/share/docs/awl/copyright
- src: completions/fish.fish
dst: /usr/share/fish/vendor_completions.d/awl.fish
# DEB only
- src: completions/zsh.zsh
dst: /usr/share/zsh/vendor-completions/_awl
packager: deb
# Alpine .apk only
- src: completions/zsh.zsh
dst: /usr/share/zsh/site-functions/_awl
packager: apk
# RPM only
- src: completions/zsh.zsh
dst: /usr/share/zsh/site-functions/_awl
packager: rpm
deb:
lintian_overrides:
- statically-linked-binary
- changelog-file-missing-in-native-package
overrides:
deb:
file_name_template: >-
{{- .PackageName }}_
{{- .Version }}_
{{- if eq .Arch "386" }}i386
{{- else if eq .Arch "arm" }}armel
{{- else }}{{ .Arch }}{{ end -}}
rpm:
file_name_template: >-
{{- .PackageName }}-
{{- .Version }}-
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i686
{{- else if eq .Arch "arm" }}armhfp
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{ .Arch }}{{ end -}}
- id: termux
package_name: awl-dns
vendor: Sam Therapy <sam@samtherapy.net>
maintainer: Sam Therapy <sam@samtherapy.net>
homepage: https://dns.froth.zone/awl
description: |-
Command-line DNS query tool.
Awl supports DNS-over-[UDP,TCP,HTTPS,QUIC] and DNSCrypt.
license: BSD-3-Clause
section: utils
formats:
- termux.deb
file_name_template: >-
{{- .PackageName }}_
{{- .Version }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i686
{{- else if eq .Arch "arm" }}arm
{{- else if eq .Arch "arm64" }}aarch64
{{- else }}{{ .Arch }}{{ end -}}
snapcrafts:
-
name: awl-dns
grade: stable
publish: true
summary: A command-line DNS query tool
description: |-
Awl is a command-line DNS query tool.
Awl supports DNS-over-[UDP,TCP,HTTPS,QUIC] and DNSCrypt.
confinement: strict
license: BSD-3-Clause
base: bare
apps:
awl-dns:
command: awl
plugs:
- network
completer: completions/bash.bash
dockers:
-
image_templates:
- "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:latest"
- "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Tag }}"
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
brews:
- repository:
owner: packaging
name: homebrew
homepage: https://dns.froth.zone/awl
description: A DNS query tool
license: BSD-3-Clause
# custom_block: |
# head "https://git.froth.zone/sam/awl.git"
install: |-
bin.install "awl"
bash_completion.install "completions/bash.bash" => "awl"
zsh_completion.install "completions/zsh.zsh" => "_awl"
fish_completion.install "completions/fish.fish" => "awl.fish"
man1.install "docs/awl.1.gz"
nix:
- repository:
owner: packaging
name: nur
homepage: https://dns.froth.zone/awl
description: A DNS query client
license: bsd3
extra_install: |-
installManPage ./docs/awl.1.gz
installShellCompletion ./completions/*
scoops:
- repository:
owner: packaging
name: scoop
folder: bucket
homepage: https://dns.froth.zone/awl
description: A DNS query client
license: BSD-3-Clause
changelog:
sort: asc
groups:
- title: "Dependency Updates"
regexp: "^.*fix\\(deps\\)*:+.*$"
order: 2
- title: "Features"
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: "Bug fixes"
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: "Other"
order: 999
filters:
exclude:
- "^test:"
- "^docs?:"
- "typo"
- "^ci:"
uploads:
- name: packages
method: PUT
mode: archive
exts:
- deb
- rpm
- apk
- termux.deb
username: sam
target: >-
https://git.froth.zone/api/packages/sam/
{{- if eq .ArtifactExt "deb" }}debian/pool/sid/main/upload
{{- else if eq .ArtifactExt "termux.deb" }}debian/pool/termux/main/upload
{{- else if eq .ArtifactExt "rpm" }}rpm/upload
{{- else if eq .ArtifactExt "apk" }}alpine/edge/main{{ end -}}
custom_artifact_name: true # Truncate the artifact name from the upload URL
gitea_urls:
api: https://git.froth.zone/api/v1
download: https://git.froth.zone

View File

@ -1,3 +0,0 @@
FROM scratch
ENTRYPOINT ["/awl"]
COPY awl /

View File

@ -1,30 +0,0 @@
# SPDX-License-Identifier: BSD-3-Clause
# GNU Makefile allowing for building on Windows (with GNU Make)
include template.mk
ifeq ($(OS),Windows_NT)
EXE := $(PROG).exe
else
EXE := $(PROG)
ifeq ($(shell uname), Darwin)
INSTALLFLAGS :=
else
INSTALLFLAGS := D
endif
endif
## install: installs awl
.PHONY: install
ifeq ($(OS),Windows_NT)
install:
$(GO) install $(GOFLAGS) .
else
install: all
install -$(INSTALLFLAGS)m755 $(PROG) $(DESTDIR)$(PREFIX)/$(BIN)/$(PROG)
install -$(INSTALLFLAGS)m644 docs/$(PROG).1 $(DESTDIR)$(MAN)/man1/$(PROG).1
# completions need to go in one specific place :)
install -$(INSTALLFLAGS)m644 completions/bash.bash $(DESTDIR)$(PREFIX)/$(SHARE)/bash-completion/completions/$(PROG)
install -$(INSTALLFLAGS)m644 completions/fish.fish $(DESTDIR)$(PREFIX)/$(SHARE)/fish/vendor_completions.d/$(PROG).fish
install -$(INSTALLFLAGS)m644 completions/zsh.zsh $(DESTDIR)$(PREFIX)/$(SHARE)/zsh/site-functions/_$(PROG)
endif

11
LICENSE
View File

@ -1,11 +0,0 @@
Copyright 2022 Sam Therapy, Gregward Bulon
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,16 +0,0 @@
# SPDX-License-Identifier: BSD-3-Clause
# BSD/POSIX makefile
include template.mk
EXE := $(PROG)
## install: installs awl
.PHONY: install
install: all
install -Dm755 $(PROG) $(DESTDIR)$(PREFIX)/$(BIN)/$(PROG)
install -Dm644 docs/$(PROG).1 $(DESTDIR)$(MAN)/man1/$(PROG).1
# completions need to go in one specific place :)
install -Dm644 completions/bash.bash $(DESTDIR)$(PREFIX)$(SHARE)/bash-completion/completions/$(PROG)
install -Dm644 completions/fish.fish $(DESTDIR)$(PREFIX)$(SHARE)/fish/vendor_completions.d/$(PROG).fish
install -Dm644 completions/zsh.zsh $(DESTDIR)$(PREFIX)$(SHARE)/zsh/site-functions/_$(PROG)

195
README.md
View File

@ -1,195 +0,0 @@
<!-- markdownlint-disable MD033 -->
# <img src="./docs/img/awl-text.png" width="50%" title="awl logo" alt="awl">
> awl *(noun)*: A pointed tool for making small holes in wood or leather
A command-line DNS lookup tool that supports DNS queries over UDP, TCP, TLS, HTTPS, DNSCrypt, and QUIC.
[![Gitea Release](https://img.shields.io/gitea/v/release/sam/awl?gitea_url=https%3A%2F%2Fgit.froth.zone&display_name=release&style=for-the-badge)](https://git.froth.zone/sam/awl)
[![Last Commit](https://img.shields.io/gitea/last-commit/sam/awl?gitea_url=https%3A%2F%2Fgit.froth.zone&style=for-the-badge)](https://git.froth.zone/sam/awl/commits/branch/master)
[![License](https://img.shields.io/github/license/samtherapy/awl?style=for-the-badge)](https://spdx.org/licenses/BSD-3-Clause.html)
[![Go Report](https://goreportcard.com/badge/dns.froth.zone/awl?style=for-the-badge)](https://goreportcard.com/report/dns.froth.zone/awl)
Awl is designed to be a drop-in replacement for [dig](https://bind9.readthedocs.io/en/v9_18_3/manpages.html#dig-dns-lookup-utility).
## Examples
```shell
# Query a domain over UDP
awl example.com
# Query a domain over HTTPS, print only the results
awl example.com +https --short
# Query a domain over TLS, print as JSON
awl example.com +tls +json
```
For more and the usage, see the [manpage](https://git.froth.zone/sam/awl/wiki/awl.1).
## Installing
On any platform, with [Go](https://go.dev) installed, run the following command to install:
```shell
go install dns.froth.zone/awl@latest
```
### Packaging
Alternatively, many package managers are supported:
<details>
<summary>Linux</summary>
#### Distro-specific
<details>
<summary>Alpine Linux</summary>
Provided by [Gitea packages](https://git.froth.zone/sam/-/packages/alpine/awl-dns) \
***Any distro that uses apk should also work***
```shell
# Add the repository
echo "https://git.froth.zone/api/packages/sam/alpine/edge/main" | tee -a /etc/apk/repositories
# Get the signing key
curl -JO https://git.froth.zone/api/packages/sam/alpine/key --output-dir /etc/apk/keys
# Install
apk add awl-dns
```
</details>
<details>
<summary>Arch</summary>
AUR package available as [awl-dns-git](https://aur.archlinux.org/packages/awl-dns-git/)
```shell
yay -S awl-dns-git ||
paru -S awl-dns-git
```
</details>
<details>
<summary>Debian / Ubuntu</summary>
Provided by [Gitea packages](https://git.froth.zone/sam/-/packages/debian/awl-dns/) \
***Any distro that uses deb/dpkg should also work***
```shell
# Add PGP key
sudo curl https://git.froth.zone/api/packages/sam/debian/repository.key -o /usr/share/keyrings/git-froth-zone-sam.asc
# Add repo
echo "deb [signed-by=/usr/share/keyrings/git-froth-zone-sam.asc] https://git.froth.zone/api/packages/sam/debian sid main" | sudo tee /etc/apt/sources.list.d/git-froth-zone-sam.list
# Update and install
sudo apt update
sudo apt install awl-dns
```
</details>
<details>
<summary>Fedora / RHEL / SUSE</summary>
Provided by [Gitea packages](https://git.froth.zone/sam/-/packages/rpm/awl-dns/) \
***Any distro that uses rpm/dnf might also work, I've never tried it***
```shell
# Add the repository
dnf config-manager --add-repo https://git.froth.zone/api/packages/sam/rpm.repo ||
zypper addrepo https://git.froth.zone/api/packages/sam/rpm.repo
# Install
dnf install awl-dns ||
zypper install awl-dns
```
</details>
<details>
<summary>Gentoo</summary>
```shell
# Add the ebuild repository
eselect repository add froth-zone git https://git.froth.zone/packaging/portage.git
emaint sync -r froth-zone
# Install
emerge -av net-dns/awl
```
</details>
#### Distro-agnostic
<details>
<summary><a href="https://brew.sh" nofollow>Homebrew</a></summary>
```shell
brew install SamTherapy/tap/awl
```
</details>
<details>
<summary>Snap</summary>
Snap package available as [awl-dns](https://snapcraft.io/awl-dns)
```shell
snap install awl-dns ||
sudo snap install awl-dns
```
</details>
</details>
<hr />
<details>
<summary>macOS</summary>
<details open>
<summary><a href="https://brew.sh" nofollow>Homebrew</a></summary>
```shell
brew install SamTherapy/tap/awl
```
</details>
</details>
<hr />
<details>
<summary>Windows</summary>
<details open>
<summary><a href="https://scoop.sh" nofollow>Scoop</a></summary>
```pwsh
scoop bucket add froth https://git.froth.zone/packages/scoop.git
scoop install awl
```
</details>
</details>
## Contributing
Please see the [CONTRIBUTING.md](./docs/CONTRIBUTING.md) file for more information.
TL;DR: If you like the project, spread the word! If you want to contribute, [use the issue tracker](https://git.froth.zone/sam/awl/issues) or [open a pull request](https://git.froth.zone/sam/awl/pulls).
Want to use email instead? Use our [mailing list](https://lists.sr.ht/~sammefishe/awl-devel)!
### Mirrors
The canonical repository is located on [my personal Forgejo instance](https://git.froth.zone/sam/awl). \
Official mirrors are located on [GitHub](https://github.com/SamTherapy/awl) and [SourceHut](https://git.sr.ht/~sammefishe/awl/).
Contributions are accepted on all mirrors, but the Forgejo instance is preferred.
## License
[BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html)
### Credits
- Awl image taken from [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Awl.tif), imaged is licensed CC0.

1
_redirects Normal file
View File

@ -0,0 +1 @@
/* https://dns.froth.zone/awl/:splat 301

View File

@ -1,269 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"errors"
"fmt"
"runtime"
"strconv"
"strings"
"time"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
flag "github.com/stefansundin/go-zflag"
)
// ParseCLI parses arguments given from the CLI and passes them into an `Options`
// struct.
func ParseCLI(args []string, version string) (opts *util.Options, err error) {
// Parse the standard flags
opts, misc, err := parseFlags(args, version)
if err != nil {
return opts, err
}
// Parse all the arguments that don't start with - or --
// This includes the dig-style (+) options
err = ParseMiscArgs(misc, opts)
if err != nil {
return opts, err
}
opts.Logger.Info("Dig/Drill flags parsed")
opts.Logger.Debug(fmt.Sprintf("%+v", opts))
// Special options and exceptions time
if opts.Request.Port == 0 {
if opts.TLS || opts.QUIC {
opts.Request.Port = 853
} else {
opts.Request.Port = 53
}
}
opts.Logger.Info("Port set to", opts.Request.Port)
// Set timeout to 0.5 seconds if set below 0.5
if opts.Request.Timeout < (time.Second / 2) {
opts.Request.Timeout = (time.Second / 2)
}
if opts.Request.Retries < 0 {
opts.Request.Retries = 0
}
if opts.Trace {
if opts.TLS || opts.HTTPS || opts.QUIC {
opts.Logger.Warn("Every query after the root query will only use UDP/TCP")
}
opts.RD = true
}
opts.Logger.Info("Options fully populated")
opts.Logger.Debug(fmt.Sprintf("%+v", opts))
return
}
// Everything that has to do with CLI flags goes here (the posix style, eg. -a and --bbbb).
func parseFlags(args []string, version string) (opts *util.Options, flags []string, err error) {
flagSet := flag.NewFlagSet(args[0], flag.ContinueOnError)
flagSet.Usage = func() {
fmt.Println(`awl - drill, writ small
Usage: awl name [@server] [record]
<name> domain, IP address, phone number
<record> defaults to A
Arguments may be in any order, including flags.
Dig-like +[no]commands are also supported, see dig(1) or dig -h
Options:`)
flagSet.PrintDefaults()
}
// CLI flags
//
// Remember, when adding a flag edit the manpage and the completions :)
var (
port = flagSet.Int("port", 0, "`port` to make DNS query (default: 53 for UDP/TCP, 853 for TLS/QUIC)", flag.OptShorthand('p'), flag.OptDisablePrintDefault(true))
query = flagSet.String("query", "", "domain name to `query` (default: .)", flag.OptShorthand('q'))
class = flagSet.String("class", "IN", "DNS `class` to query", flag.OptShorthand('c'))
qType = flagSet.String("qType", "", "`type` to query (default: A)", flag.OptShorthand('t'))
ipv4 = flagSet.Bool("4", false, "force IPv4", flag.OptShorthand('4'))
ipv6 = flagSet.Bool("6", false, "force IPv6", flag.OptShorthand('6'))
reverse = flagSet.Bool("reverse", false, "do a reverse lookup", flag.OptShorthand('x'))
trace = flagSet.Bool("trace", false, "trace from the root")
timeout = flagSet.Float32("timeout", 5, "Timeout, in `seconds`")
retry = flagSet.Int("retries", 2, "number of `times` to retry")
edns = flagSet.Bool("no-edns", false, "disable EDNS entirely")
ednsVer = flagSet.Uint8("edns-ver", 0, "set EDNS version")
dnssec = flagSet.Bool("dnssec", false, "enable DNSSEC", flag.OptShorthand('D'))
expire = flagSet.Bool("expire", false, "set EDNS expire")
nsid = flagSet.Bool("nsid", false, "set EDNS NSID", flag.OptShorthand('n'))
cookie = flagSet.Bool("no-cookie", false, "disable sending EDNS cookie (default: cookie sent)")
tcpKeepAlive = flagSet.Bool("keep-alive", false, "send EDNS TCP keep-alive")
udpBufSize = flagSet.Uint16("buffer-size", 1232, "set EDNS UDP buffer size", flag.OptShorthand('b'))
mbzflag = flagSet.String("zflag", "0", "set EDNS z-flag `value`")
subnet = flagSet.String("subnet", "", "set EDNS client subnet")
padding = flagSet.Bool("pad", false, "set EDNS padding")
badCookie = flagSet.Bool("no-bad-cookie", false, "ignore BADCOOKIE EDNS responses (default: retry with correct cookie")
truncate = flagSet.Bool("no-truncate", false, "ignore truncation if a UDP request truncates (default: retry with TCP)")
tcp = flagSet.Bool("tcp", false, "use TCP")
dnscrypt = flagSet.Bool("dnscrypt", false, "use DNSCrypt")
tls = flagSet.Bool("tls", false, "use DNS-over-TLS", flag.OptShorthand('T'))
https = flagSet.Bool("https", false, "use DNS-over-HTTPS", flag.OptShorthand('H'))
quic = flagSet.Bool("quic", false, "use DNS-over-QUIC", flag.OptShorthand('Q'))
tlsHost = flagSet.String("tls-host", "", "Server name to use for TLS verification")
noVerify = flagSet.Bool("tls-no-verify", false, "Disable TLS cert verification")
aaflag = flagSet.Bool("aa", false, "set/unset AA (Authoratative Answer) flag (default: not set)")
adflag = flagSet.Bool("ad", false, "set/unset AD (Authenticated Data) flag (default: not set)")
cdflag = flagSet.Bool("cd", false, "set/unset CD (Checking Disabled) flag (default: not set)")
qrflag = flagSet.Bool("qr", false, "set/unset QR (QueRy) flag (default: not set)")
rdflag = flagSet.Bool("rd", true, "set/unset RD (Recursion Desired) flag (default: set)", flag.OptDisablePrintDefault(true))
raflag = flagSet.Bool("ra", false, "set/unset RA (Recursion Available) flag (default: not set)")
tcflag = flagSet.Bool("tc", false, "set/unset TC (TrunCated) flag (default: not set)")
zflag = flagSet.Bool("z", false, "set/unset Z (Zero) flag (default: not set)", flag.OptShorthand('z'))
short = flagSet.Bool("short", false, "print just the results", flag.OptShorthand('s'))
json = flagSet.Bool("json", false, "print the result(s) as JSON", flag.OptShorthand('j'))
xml = flagSet.Bool("xml", false, "print the result(s) as XML", flag.OptShorthand('X'))
yaml = flagSet.Bool("yaml", false, "print the result(s) as yaml", flag.OptShorthand('y'))
noC = flagSet.Bool("no-comments", false, "disable printing the comments")
noQ = flagSet.Bool("no-question", false, "disable printing the question section")
noOpt = flagSet.Bool("no-opt", false, "disable printing the OPT pseudosection")
noAns = flagSet.Bool("no-answer", false, "disable printing the answer section")
noAuth = flagSet.Bool("no-authority", false, "disable printing the authority section")
noAdd = flagSet.Bool("no-additional", false, "disable printing the additional section")
noStats = flagSet.Bool("no-statistics", false, "disable printing the statistics section")
verbosity = flagSet.Int("verbosity", 1, "sets verbosity `level`", flag.OptShorthand('v'), flag.OptNoOptDefVal("2"))
versionFlag = flagSet.Bool("version", false, "print version information", flag.OptShorthand('V'))
)
// Don't sort the flags when -h is given
flagSet.SortFlags = true
// Parse the flags
if err = flagSet.Parse(args[1:]); err != nil {
return &util.Options{Logger: util.InitLogger(*verbosity)}, nil, fmt.Errorf("flag: %w", err)
}
// TODO: DRY, dumb dumb.
mbz, err := strconv.ParseInt(*mbzflag, 0, 16)
if err != nil {
return &util.Options{Logger: util.InitLogger(*verbosity)}, nil, fmt.Errorf("EDNS MBZ: %w", err)
}
opts = &util.Options{
Logger: util.InitLogger(*verbosity),
IPv4: *ipv4,
IPv6: *ipv6,
Trace: *trace,
Short: *short,
TCP: *tcp,
DNSCrypt: *dnscrypt,
TLS: *tls,
TLSHost: *tlsHost,
TLSNoVerify: *noVerify,
HTTPS: *https,
QUIC: *quic,
Truncate: *truncate,
BadCookie: *badCookie,
Reverse: *reverse,
JSON: *json,
XML: *xml,
YAML: *yaml,
HeaderFlags: util.HeaderFlags{
AA: *aaflag,
AD: *adflag,
TC: *tcflag,
Z: *zflag,
CD: *cdflag,
QR: *qrflag,
RD: *rdflag,
RA: *raflag,
},
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,
Port: *port,
},
Display: util.Display{
Comments: !*noC,
Question: !*noQ,
Opt: !*noOpt,
Answer: !*noAns,
Authority: !*noAuth,
Additional: !*noAdd,
Statistics: !*noStats,
TTL: true,
ShowClass: true,
ShowQuery: false,
HumanTTL: false,
UcodeTranslate: true,
},
EDNS: util.EDNS{
EnableEDNS: !*edns,
Cookie: !*cookie,
DNSSEC: *dnssec,
BufSize: *udpBufSize,
Version: *ednsVer,
Expire: *expire,
KeepOpen: *tcpKeepAlive,
Nsid: *nsid,
ZFlag: uint16(mbz & 0x7FFF),
Padding: *padding,
},
HTTPSOptions: util.HTTPSOptions{
Endpoint: "/dns-query",
Get: false,
},
}
// TODO: DRY
if *subnet != "" {
if err = util.ParseSubnet(*subnet, opts); err != nil {
return opts, nil, fmt.Errorf("%w", err)
}
}
opts.Logger.Info("POSIX flags parsed")
opts.Logger.Debug(fmt.Sprintf("%+v", opts))
if *versionFlag {
fmt.Printf("awl version %s, built with %s\n", version, runtime.Version())
return opts, nil, util.ErrNotError
}
flags = flagSet.Args()
return
}
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,189 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package cli_test
import (
"testing"
"time"
cli "dns.froth.zone/awl/cmd"
"dns.froth.zone/awl/pkg/util"
"gotest.tools/v3/assert"
)
func TestEmpty(t *testing.T) {
t.Parallel()
args := []string{"awl", "-4"}
opts, err := cli.ParseCLI(args, "TEST")
assert.NilError(t, err)
assert.Assert(t, opts.IPv4)
assert.Equal(t, opts.Request.Port, 53)
}
func TestTLSPort(t *testing.T) {
t.Parallel()
args := []string{"awl", "-T"}
opts, err := cli.ParseCLI(args, "TEST")
assert.NilError(t, err)
assert.Equal(t, opts.Request.Port, 853)
}
func TestValidSubnet(t *testing.T) {
t.Parallel()
tests := []struct {
args []string
want uint16
}{
{[]string{"awl", "--subnet", "127.0.0.1/32"}, uint16(1)},
{[]string{"awl", "--subnet", "0"}, uint16(1)},
{[]string{"awl", "--subnet", "::/0"}, uint16(2)},
}
for _, test := range tests {
test := test
t.Run(test.args[2], func(t *testing.T) {
t.Parallel()
opts, err := cli.ParseCLI(test.args, "TEST")
assert.NilError(t, err)
assert.Equal(t, opts.EDNS.Subnet.Family, test.want)
})
}
}
func TestInvalidSubnet(t *testing.T) {
t.Parallel()
args := []string{"awl", "--subnet", "/"}
_, err := cli.ParseCLI(args, "TEST")
assert.ErrorContains(t, err, "EDNS subnet")
}
func TestMBZ(t *testing.T) {
t.Parallel()
args := []string{"awl", "--zflag", "G"}
_, err := cli.ParseCLI(args, "TEST")
assert.ErrorContains(t, err, "EDNS MBZ")
}
func TestInvalidFlag(t *testing.T) {
t.Parallel()
args := []string{"awl", "--treebug"}
_, err := cli.ParseCLI(args, "TEST")
assert.ErrorContains(t, err, "unknown flag")
}
func TestInvalidDig(t *testing.T) {
t.Parallel()
args := []string{"awl", "+a"}
_, err := cli.ParseCLI(args, "TEST")
assert.ErrorContains(t, err, "digflags: invalid argument")
}
func TestVersion(t *testing.T) {
t.Parallel()
args := []string{"awl", "--version"}
_, err := cli.ParseCLI(args, "test")
assert.ErrorIs(t, err, util.ErrNotError)
}
func TestTimeout(t *testing.T) {
t.Parallel()
args := [][]string{
{"awl", "+timeout=0"},
{"awl", "--timeout", "0"},
}
for _, test := range args {
test := test
t.Run(test[1], func(t *testing.T) {
t.Parallel()
opt, err := cli.ParseCLI(test, "TEST")
assert.NilError(t, err)
assert.Equal(t, opt.Request.Timeout, time.Second/2)
})
}
}
func TestRetries(t *testing.T) {
t.Parallel()
args := [][]string{
{"awl", "+retry=-2"},
{"awl", "+tries=-2"},
{"awl", "--retries", "-2"},
}
for _, test := range args {
test := test
t.Run(test[1], func(t *testing.T) {
t.Parallel()
opt, err := cli.ParseCLI(test, "TEST")
assert.NilError(t, err)
assert.Equal(t, opt.Request.Retries, 0)
})
}
}
func TestSetHTTPS(t *testing.T) {
t.Parallel()
args := [][]string{
{"awl", "-H", "@dns.froth.zone/dns-query"},
{"awl", "+https", "@dns.froth.zone"},
}
for _, test := range args {
test := test
t.Run(test[1], func(t *testing.T) {
t.Parallel()
opt, err := cli.ParseCLI(test, "TEST")
assert.NilError(t, err)
assert.Equal(t, opt.Request.Server, "dns.froth.zone")
assert.Equal(t, opt.HTTPSOptions.Endpoint, "/dns-query")
})
}
}
func FuzzFlags(f *testing.F) {
testcases := []string{"git.froth.zone", "", "!12345", "google.com.edu.org.fr"}
for _, tc := range testcases {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, orig string) {
// Get rid of outputs
args := []string{"awl", orig}
//nolint:errcheck,gosec // Only make sure the program does not crash
cli.ParseCLI(args, "TEST")
})
}

View File

@ -1,246 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"fmt"
"strconv"
"strings"
"time"
"dns.froth.zone/awl/pkg/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 *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 {
case "trace", "notrace":
opts.Trace = isNo
if isNo {
opts.DNSSEC = true
opts.Display.Comments = false
opts.Display.Question = false
opts.Display.Opt = false
opts.Display.Answer = true
opts.Display.Authority = true
opts.Display.Additional = false
opts.Display.Statistics = false
}
// Set DNS query flags
case "aa", "aaflag", "aaonly":
opts.AA = isNo
case "ad", "adflag":
opts.AD = isNo
case "cd", "cdflag":
opts.CD = isNo
case "qrflag":
opts.QR = isNo
case "ra", "raflag":
opts.RA = isNo
case "rd", "rdflag", "recurse":
opts.RD = isNo
case "tc", "tcflag":
opts.TC = isNo
case "z", "zflag":
opts.Z = isNo
// End DNS query flags
case "qr":
opts.Display.ShowQuery = isNo
case "ttlunits":
opts.Display.HumanTTL = isNo
case "ttl", "ttlid":
opts.Display.TTL = isNo
case "class":
opts.Display.ShowClass = isNo
// EDNS queries
case "do", "dnssec":
opts.EDNS.DNSSEC = isNo
case "expire":
opts.EDNS.Expire = isNo
case "cookie":
opts.EDNS.Cookie = isNo
case "keepopen", "keepalive":
opts.EDNS.KeepOpen = isNo
case "nsid":
opts.EDNS.Nsid = isNo
case "padding":
opts.EDNS.Padding = isNo
// End EDNS queries
// DNS-over-X
case "tcp", "vc":
opts.TCP = isNo
case "ignore":
opts.Truncate = isNo
case "badcookie":
opts.BadCookie = !isNo
case "tls":
opts.TLS = isNo
case "dnscrypt":
opts.DNSCrypt = isNo
case "quic":
opts.QUIC = isNo
// End DNS-over-X
// Formatting
case "short":
opts.Short = isNo
case "identify":
opts.Identify = isNo
case "json":
opts.JSON = isNo
case "xml":
opts.XML = isNo
case "yaml":
opts.YAML = isNo
// End formatting
// Output
case "comments":
opts.Display.Comments = isNo
case "question":
opts.Display.Question = isNo
case "opt":
opts.Display.Opt = isNo
case "answer":
opts.Display.Answer = isNo
case "authority":
opts.Display.Authority = isNo
case "additional":
opts.Display.Additional = isNo
case "stats":
opts.Display.Statistics = isNo
case "all":
opts.Display.Comments = isNo
opts.Display.Question = isNo
opts.Display.Opt = isNo
opts.Display.Answer = isNo
opts.Display.Authority = isNo
opts.Display.Additional = isNo
opts.Display.Statistics = isNo
case "idnin", "idnout":
opts.Display.UcodeTranslate = isNo
default:
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 "https", "https-get", "https-post":
opts.HTTPS = startNo
if isSplit && val != "" {
opts.HTTPSOptions.Endpoint = val
} else {
opts.HTTPSOptions.Endpoint = "/dns-query"
}
if strings.HasSuffix(arg, "get") {
opts.HTTPSOptions.Get = true
}
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

@ -1,82 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package cli_test
import (
"testing"
cli "dns.froth.zone/awl/cmd"
"dns.froth.zone/awl/pkg/util"
"gotest.tools/v3/assert"
)
func FuzzDig(f *testing.F) {
f.Log("ParseDig Fuzzing")
seeds := []string{
"aaflag", "aaonly", "noaaflag", "noaaonly",
"adflag", "noadflag",
"cdflag", "nocdflag",
"qrflag", "noqrflag",
"raflag", "noraflag",
"rdflag", "recurse", "nordflag", "norecurse",
"tcflag", "notcflag",
"zflag", "nozflag",
"qr", "noqr",
"ttlunits", "nottlunits",
"ttlid", "nottlid",
"do", "dnssec", "nodo", "nodnssec",
"edns", "edns=a", "edns=0", "noedns",
"expire", "noexpire",
"ednsflags", "ednsflags=\"", "ednsflags=1", "noednsflags",
"subnet=0.0.0.0/0", "subnet=::0/0", "subnet=b", "subnet=0", "subnet",
"cookie", "nocookie",
"keepopen", "keepalive", "nokeepopen", "nokeepalive",
"nsid", "nonsid",
"padding", "nopadding",
"bufsize=512", "bufsize=a", "bufsize",
"time=5", "timeout=a", "timeout",
"retry=a", "retry=3", "retry",
"tries=2", "tries=b", "tries",
"tcp", "vc", "notcp", "novc",
"ignore", "noignore",
"badcookie", "nobadcookie",
"tls", "notls",
"dnscrypt", "nodnscrypt",
"https", "https=/dns", "https-get", "https-get=/", "nohttps",
"quic", "noquic",
"short", "noshort",
"identify", "noidentify",
"json", "nojson",
"xml", "noxml",
"yaml", "noyaml",
"comments", "nocomments",
"question", "noquestion",
"opt", "noopt",
"answer", "noanswer",
"authority", "noauthority",
"additional", "noadditional",
"stats", "nostats",
"all", "noall",
"idnout", "noidnout",
"class", "noclass",
"trace", "notrace",
"invalid",
}
for _, tc := range seeds {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, orig string) {
// Get rid of outputs
// os.Stdout = os.NewFile(0, os.DevNull)
// os.Stderr = os.NewFile(0, os.DevNull)
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
if err := cli.ParseDig(orig, opts); err != nil {
assert.ErrorContains(t, err, "digflags:")
}
})
}

View File

@ -1,7 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
/*
Package cli is the CLI part of the package, including both POSIX
flag parsing and dig-like flag parsing.
*/
package cli

View File

@ -1,196 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"fmt"
"math/rand"
"strings"
"dns.froth.zone/awl/conf"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
"golang.org/x/net/idna"
)
// ParseMiscArgs parses the wildcard arguments, dig style.
// Only one command is supported at a time, so any extra information overrides previous.
func ParseMiscArgs(args []string, opts *util.Options) 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
opts.Request.Server = strings.TrimPrefix(arg, "tls://")
opts.Logger.Info("DNS-over-TLS implicitly set")
case strings.HasPrefix(arg, "https://"):
opts.HTTPS = true
opts.Request.Server = arg
opts.Logger.Info("DNS-over-HTTPS implicitly set")
_, endpoint, isSplit := strings.Cut(arg, "/")
if isSplit {
opts.HTTPSOptions.Endpoint = "/" + endpoint
}
case strings.HasPrefix(arg, "quic://"):
opts.QUIC = true
opts.Request.Server = strings.TrimPrefix(arg, "quic://")
opts.Logger.Info("DNS-over-QUIC implicitly set.")
case strings.HasPrefix(arg, "sdns://"):
opts.DNSCrypt = true
opts.Request.Server = arg
opts.Logger.Info("DNSCrypt implicitly set")
case strings.HasPrefix(arg, "tcp://"):
opts.TCP = true
opts.Request.Server = strings.TrimPrefix(arg, "tcp://")
opts.Logger.Info("TCP implicitly set")
case strings.HasPrefix(arg, "udp://"):
opts.Request.Server = strings.TrimPrefix(arg, "udp://")
default:
// Allow HTTPS queries to have a fallback default
if opts.HTTPS {
server, endpoint, isSplit := strings.Cut(arg, "/")
if isSplit {
opts.HTTPSOptions.Endpoint = "/" + endpoint
opts.Request.Server = server
} else {
opts.Request.Server = server
}
} else {
opts.Request.Server = arg
}
}
// Dig-style +queries
case strings.HasPrefix(arg, "+"):
opts.Logger.Info(arg, "detected as a dig query")
if err := ParseDig(strings.ToLower(arg[1:]), opts); err != nil {
return err
}
// Domain names
case strings.Contains(arg, "."):
var err error
opts.Logger.Info(arg, "detected as a domain name")
opts.Request.Name, err = idna.ToASCII(arg)
if err != nil {
return fmt.Errorf("unicode to punycode: %w", err)
}
// DNS query type
case ok:
opts.Logger.Info(arg, "detected as a type")
opts.Request.Type = r
// Domain?
default:
var err error
opts.Logger.Info(arg, "is unknown. Assuming domain")
opts.Request.Name, err = idna.ToASCII(arg)
if err != nil {
return fmt.Errorf("unicode to punycode: %w", err)
}
}
}
// If nothing was set, set a default
if opts.Request.Name == "" {
opts.Logger.Info("Domain not specified, making a default")
opts.Request.Name = "."
if opts.Request.Type == 0 {
opts.Logger.Info("Query not specified, making an \"NS\" query")
opts.Request.Type = dns.StringToType["NS"]
}
} else if opts.Request.Type == 0 {
opts.Logger.Info("Query not specified, making an \"A\" query")
opts.Request.Type = dns.StringToType["A"]
}
if opts.Request.Server == "" {
opts.Logger.Info("Server not specified, selecting a default")
// Set "defaults" for each if there is no input
switch {
case opts.DNSCrypt:
// This is adguard
opts.Request.Server = "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"
case opts.TLS:
opts.Request.Server = "dns.google"
case opts.HTTPS:
opts.Request.Server = "https://dns.cloudflare.com"
case opts.QUIC:
opts.Request.Server = "dns.froth.zone"
default:
var err error
resolv, err := conf.GetDNSConfig()
if err != nil {
// :^)
opts.Logger.Warn("Could not query system for server. Using localhost\n", "Error:", err)
opts.Request.Server = "127.0.0.1"
} else {
// Make sure that if IPv4 or IPv6 is asked for it actually uses it
harmful:
for _, srv := range resolv.Servers {
switch {
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 addresses proper addresses
if opts.Reverse {
var err error
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: %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.Info("Domain made canonical")
}
return nil
}

View File

@ -1,195 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package cli_test
import (
"strings"
"testing"
cli "dns.froth.zone/awl/cmd"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
"gotest.tools/v3/assert"
)
func TestParseArgs(t *testing.T) {
t.Parallel()
args := []string{
"go.dev",
"AAAA",
"@1.1.1.1",
"+ignore",
}
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
err := cli.ParseMiscArgs(args, opts)
assert.NilError(t, err)
assert.Equal(t, opts.Request.Name, "go.dev.")
assert.Equal(t, opts.Request.Type, dns.StringToType["AAAA"])
assert.Equal(t, opts.Request.Server, "1.1.1.1")
assert.Equal(t, opts.Truncate, true)
}
func TestParseNoInput(t *testing.T) {
t.Parallel()
args := []string{}
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
err := cli.ParseMiscArgs(args, opts)
assert.NilError(t, err)
assert.Equal(t, opts.Request.Name, ".")
assert.Equal(t, opts.Request.Type, dns.StringToType["NS"])
}
func TestParseA(t *testing.T) {
t.Parallel()
args := []string{
"golang.org.",
}
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
err := cli.ParseMiscArgs(args, opts)
assert.NilError(t, err)
assert.Equal(t, opts.Request.Name, "golang.org.")
assert.Equal(t, opts.Request.Type, dns.StringToType["A"])
}
func TestParsePTR(t *testing.T) {
t.Parallel()
args := []string{"8.8.8.8"}
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
opts.Reverse = true
err := cli.ParseMiscArgs(args, opts)
assert.NilError(t, err)
assert.Equal(t, opts.Request.Type, dns.StringToType["PTR"])
}
func TestParseInvalidPTR(t *testing.T) {
t.Parallel()
args := []string{"8.88.8"}
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
opts.Reverse = true
err := cli.ParseMiscArgs(args, opts)
assert.ErrorContains(t, err, "unrecognized address")
}
func TestDefaultServer(t *testing.T) {
t.Parallel()
tests := []struct {
in string
want string
}{
{"DNSCrypt", "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20"},
{"TLS", "dns.google"},
{"HTTPS", "https://dns.cloudflare.com"},
{"QUIC", "dns.froth.zone"},
}
for _, test := range tests {
test := test
t.Run(test.in, func(t *testing.T) {
t.Parallel()
args := []string{}
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
switch test.in {
case "DNSCrypt":
opts.DNSCrypt = true
case "TLS":
opts.TLS = true
case "HTTPS":
opts.HTTPS = true
case "QUIC":
opts.QUIC = true
}
err := cli.ParseMiscArgs(args, opts)
assert.NilError(t, err)
assert.Equal(t, opts.Request.Server, test.want)
})
}
}
func TestFlagSetting(t *testing.T) {
t.Parallel()
tests := []struct {
in string
expected string
over string
}{
{"@sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", "DNSCrypt"},
{"@tls://dns.google", "dns.google", "TLS"},
{"@https://dns.cloudflare.com/dns-query", "https://dns.cloudflare.com/dns-query", "HTTPS"},
{"@https://dns.example.net/a", "https://dns.example.net/a", "HTTPS with a set path"},
{"@quic://dns.adguard.com", "dns.adguard.com", "QUIC"},
{"@tcp://dns.froth.zone", "dns.froth.zone", "TCP"},
{"@udp://dns.example.com", "dns.example.com", "UDP"},
}
for _, test := range tests {
test := test
t.Run(test.over, func(t *testing.T) {
t.Parallel()
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
err := cli.ParseMiscArgs([]string{test.in}, opts)
assert.NilError(t, err)
switch {
case strings.HasPrefix(test.over, "DNSCrypt"):
assert.Assert(t, opts.DNSCrypt)
assert.Equal(t, opts.Request.Server, test.expected)
case strings.HasPrefix(test.over, "TLS"):
assert.Assert(t, opts.TLS)
assert.Equal(t, opts.Request.Server, test.expected)
case strings.HasPrefix(test.over, "HTTPS"):
assert.Assert(t, opts.HTTPS)
assert.Equal(t, opts.Request.Server, test.expected)
case strings.HasPrefix(test.over, "QUIC"):
assert.Assert(t, opts.QUIC)
assert.Equal(t, opts.Request.Server, test.expected)
case strings.HasPrefix(test.over, "TCP"):
assert.Assert(t, opts.TCP)
assert.Equal(t, opts.Request.Server, test.expected)
case strings.HasPrefix(test.over, "UDP"):
assert.Assert(t, true)
assert.Equal(t, opts.Request.Server, test.expected)
}
})
}
}
func FuzzParseArgs(f *testing.F) {
cases := []string{
"go.dev",
"AAAA",
"@1.1.1.1",
"+ignore",
"e",
}
for _, tc := range cases {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, arg string) {
// Get rid of outputs
args := []string{arg}
opts := new(util.Options)
opts.Logger = util.InitLogger(0)
//nolint:errcheck,gosec // Only make sure the program does not crash
cli.ParseMiscArgs(args, opts)
})
}

View File

@ -1,6 +0,0 @@
# SPDX-License-Identifier: BSD-3-Clause
# bash completion for awl -*- shell-script -*-
# TODO: MAKE THIS A REAL THING
complete -F _known_hosts awl

View File

@ -1,75 +0,0 @@
# SPDX-License-Identifier: BSD-3-Clause
function __fish_complete_awl
set -l token (commandline -ct)
switch $token
case '+tries=*' '+retry=*' '+time=*' '+bufsize=*' '+edns=*'
printf '%s\n' $token(seq 0 255)
case '-v=*'
printf '%s\n' $token(seq -1 3)
end
end
complete -c awl -x -a "(__fish_print_hostnames) A AAAA AFSDB APL CAA CDNSKEY CDS CERT CNAME DHCID DLV DNAME DNSKEY DS HIP IPSECKEY KEY KX LOC MX NAPTR NS NSEC NSEC3 NSEC3PARAM PTR RRSIG RP SIG SOA SRV SSHFP TA TKEY TLSA TSIG TXT URI"
complete -c awl -x -a "@(__fish_print_hostnames)"
complete -f -c awl -s 4 -d 'Use IPv4 query transport only'
complete -f -c awl -s 6 -d 'Use IPv6 query transport only'
complete -c awl -s c -l class -x -a 'IN CH HS QCLASS' -d 'Specify query class'
complete -c awl -s p -l port -x -d 'Specify port number'
complete -c awl -s q -l query -x -a "(__fish_print_hostnames)" -d 'Query domain'
complete -c awl -s t -l qType -x -a 'A AAAA AFSDB APL CAA CDNSKEY CDS CERT CNAME DHCID DLV DNAME DNSKEY DS HIP IPSECKEY KEY KX LOC MX NAPTR NS NSEC NSEC3 NSEC3PARAM PTR RRSIG RP SIG SOA SRV SSHFP TA TKEY TLSA TSIG TXT URI' -d 'Specify query type'
complete -c awl -l timeout -x -d 'Set timeout'
complete -c awl -l retries -x -d 'Set number of query retries'
complete -c awl -l no-edns -x -d 'Disable EDNS'
complete -f -c awl -l tcp -a '+vc +novc +tcp +notcp' -d 'TCP mode'
complete -f -c awl -l dnscrypt -a '+dnscrypt +nodnscrypt' -d 'Use DNSCrypt'
complete -c awl -s T -l tls -a '+tls +notls' -d 'Use DNS-over-TLS'
complete -c awl -s H -l https -a '+https +nohttps' -d 'Use DNS-over-HTTPS'
complete -c awl -s Q -l quic -a '+quic +noquic' -d 'Use DNS-over-QUIC'
complete -c awl -s j -l json -a '+json +nojson' -d 'Print as JSON'
complete -c awl -s j -l xml -a '+xml +noxml' -d 'Print as XML'
complete -c awl -s j -l yaml -a '+yaml +noyaml' -d 'Print as YAML'
complete -c awl -s x -l reverse -x -d 'Reverse lookup'
complete -f -c awl -s h -l help -d 'Print help and exit'
complete -f -c awl -s V -l version -d 'Print version and exit'
# complete -f -c awl -a '+search +nosearch' -d 'Set whether to use searchlist'
# complete -f -c awl -a '+showsearch +noshowsearch' -d 'Search with intermediate results'
complete -f -c awl -a '+recurse +norecurse' -d 'Recursive mode'
complete -f -c awl -l no-truncate -a '+ignore +noignore' -d 'Dont revert to TCP for TC responses.'
# complete -f -c awl -a '+fail +nofail' -d 'Dont try next server on SERVFAIL'
# complete -f -c awl -a '+besteffort +nobesteffort' -d 'Try to parse even illegal messages'
complete -f -c awl -a '+aaonly +noaaonly' -d 'Set AA flag in query (+[no]aaflag)'
complete -f -c awl -a '+adflag +noadflag' -d 'Set AD flag in query'
complete -f -c awl -a '+cdflag +nocdflag' -d 'Set CD flag in query'
complete -f -c awl -a '+cl +nocl' -d 'Control display of class in records'
# complete -f -c awl -a '+cmd +nocmd' -d 'Control display of command line'
complete -f -c awl -a '+comments +nocomments' -d 'Control display of comment lines'
complete -f -c awl -a '+question +noquestion' -d 'Control display of question'
complete -f -c awl -a '+answer +noanswer' -d 'Control display of answer'
complete -f -c awl -a '+authority +noauthority' -d 'Control display of authority'
complete -f -c awl -a '+additional +noadditional' -d 'Control display of additional'
complete -f -c awl -a '+stats +nostats' -d 'Control display of statistics'
complete -f -c awl -s s -l short -a '+short +noshort' -d 'Disable everything except short form of answer'
complete -f -c awl -a '+ttlid +nottlid' -d 'Control display of ttls in records'
complete -f -c awl -a '+all +noall' -d 'Set or clear all display flags'
complete -f -c awl -a '+qr +noqr' -d 'Print question before sending'
# complete -f -c awl -a '+nssearch +nonssearch' -d 'Search all authoritative nameservers'
complete -f -c awl -a '+identify +noidentify' -d 'ID responders in short answers'
complete -f -c awl -a '+trace +notrace' -d 'Trace delegation down from root'
complete -f -c awl -l dnssec -a '+dnssec +nodnssec +do +nodo' -d 'Request DNSSEC records'
complete -f -c awl -a '+nsid +nonsid' -d 'Request Name Server ID'
# complete -f -c awl -a '+multiline +nomultiline' -d 'Print records in an expanded format'
# complete -f -c awl -a '+onesoa +noonesoa' -d 'AXFR prints only one soa record'
complete -f -c awl -a '+tries=' -d 'Set number of UDP attempts'
complete -f -c awl -a '+retry=' -d 'Set number of UDP retries'
complete -f -c awl -a '+time=' -d 'Set query timeout'
complete -f -c awl -a '+bufsize=' -d 'Set EDNS0 Max UDP packet size'
complete -f -c awl -a '+ndots=' -d 'Set NDOTS value'
complete -f -c awl -a '+edns=' -d 'Set EDNS version'
complete -c awl -a '(__fish_complete_awl)'

View File

@ -1,112 +0,0 @@
#compdef awl
# SPDX-License-Identifier: BSD-3-Clause
local curcontext="$curcontext" state line expl
local -a alts args
[[ -prefix + ]] && args=(
'*+'{no,}'tcp[use TCP instead of UDP for queries]'
'*+'{no,}'ignore[ignore truncation in UDP responses]'
'*+'{no,}'tls[use DNS-over-TLS for queries]'
'*+'{no,}'dnscrypt[use DNSCrypt for queries]'
'*+'{no,}'https=[use DNS-over-HTTPS for queries]:endpoint [/dns-query]'
'*+'{no,}'quic[use DNS-over-QUIC for queries]'
'*+'{no,}'aaonly[set aa flag in the query]'
'*+'{no,}'additional[print additional section of a reply]'
'*+'{no,}'adflag[set the AD (authentic data) bit in the query]'
'*+'{no,}'badcookie[retry BADCOOKIE responses]'
'*+'{no,}'cdflag[set the CD (checking disabled) bit in the query]'
'*+'{no,}'cookie[add a COOKIE option to the request]'
'*+edns=[specify EDNS version for query]:version (0-255)'
'*+noedns[clear EDNS version to be sent]'
'*+ednsflags=[set EDNS flags bits]:flags'
# '*+ednsopt=[specify EDNS option]:code point'
'*+noedns[clear EDNS options to be sent]'
'*+'{no,}'expire[send an EDNS Expire option]'
# '*+'{no,}'idnin[set processing of IDN domain names on input]'
'*+'{no,}'idnout[set conversion of IDN puny code on output]'
'*+'{no,}'keepalive[request EDNS TCP keepalive]'
'*+'{no,}'keepopen[keep TCP socket open between queries]'
'*+'{no,}'recurse[set the RD (recursion desired) bit in the query]'
# '*+'{no,}'nssearch[search all authoritative nameservers]'
'*+'{no,}'trace[trace delegation down from root]'
# '*+'{no,}'cmd[print initial comment in output]'
'*+'{no,}'short[print terse output]'
'*+'{no,}'identify[print IP and port of responder]'
'*+'{no,}'comments[print comment lines in output]'
'*+'{no,}'stats[print statistics]'
'*+padding[set padding block size]'
'*+'{no,}'qr[print query as it was sent]'
'*+'{no,}'question[print question section of a query]'
'*+'{no,}'raflag[set RA flag in the query]'
'*+'{no,}'answer[print answer section of a reply]'
'*+'{no,}'authority[print authority section of a reply]'
'*+'{no,}'all[set all print/display flags]'
'*+'{no,}'subnet=[send EDNS client subnet option]:addr/prefix-length'
'*+'{no,}'tcflag[set TC flag in the query]'
'*+time=[set query timeout]:timeout (seconds) [1]'
'*+timeout=[set query timeout]:timeout (seconds) [1]'
'*+tries=[specify number of UDP query attempts]:tries [3]'
'*+retry=[specify number of UDP query retries]:retries [2]'
# '*+'{no,}'rrcomments[set display of per-record comments]'
# '*+ndots=[specify number of dots to be considered absolute]:dots'
'*+bufsize=[specify UDP buffer size]:size (bytes)'
'*+'{no,}''{dnssec,do}'[enable DNSSEC]'
'*+'{no,}'nsid[include EDNS name server ID request in query]'
'*+'{no,}'class[display the class whening printing the answer]'
'*+'{no,}'ttlid[display the TTL whening printing the record]'
'*+'{no,}'ttlunits[display the TTL in human-readable units]'
# '*+'{no,}'unknownformat[print RDATA in RFC 3597 "unknown" format]'
'*+'{no,}'json[present the results as JSON]'
'*+'{no,}'xml[present the results as XML]'
'*+'{no,}'yaml[present the results as YAML]'
'*+'{no,}'zflag[set Z flag in query]'
)
# TODO: Add the regular (POSIX/GNU) flags
_arguments -s -C $args \
'(- *)-'{h,-help}'[display help information]' \
'(- *)-'{V,-version}'[display version information]' \
'-'{v,-verbosity}'=+[set verbosity to custom level]:verbosity:compadd -M "m\:{\-1-3}={\-1-3}" - \-1 0 1 2 3' \
'-'{v,-verbosity}'+[set verbosity to info]' \
'*-'{p,-port}'+[specify port number]:port:_ports' \
'*-'{q,-query}'+[specify host name to query]:host:_hosts' \
'*-'{c,-class}'+[specify class]:class:compadd -M "m\:{a-z}={A-Z}" - IN CS CH HS' \
'*-'{t,-qType}'+[specify type]:type:_dns_types' \
'*-4+[force IPv4 only]' \
'*-6+[force IPv6 only]' \
'*-'{x,-reverse}'+[reverse lookup]' \
'*--timeout+[timeout in seconds]:number [1]' \
'*--retries+[specify number of query retries]:number [2]' \
'*--no-edns+[disable EDNS]' \
'*--edns-ver+[specify EDNS version for query]:version (0-255) [0]' \
'*-'{D,-dnssec}'+[enable DNSSEC]' \
'*--expire+[send EDNS expire]' \
'*-'{n,-nsid}'+[include EDNS name server ID request in query]' \
'*--no-cookie+[disable sending EDNS cookie]' \
'*--keep-alive+[request EDNS TCP keepalive]' \
'*-'{b,-buffer-size}'+[specify UDP buffer size]:size (bytes) [1232]' \
'*--zflag+[set EDNS z-flag]:decimal, hex or octal [0]' \
'*--subnet+[set EDNS client subnet]:addr/prefix-length' \
'*--no-truncate+[ignore truncation in UDP responses]' \
'*--tcp+[use TCP instead of UDP for queries]' \
'*--dnscrypt+[use DNSCrypt for queries]' \
'*-'{T,-tls}'+[use DNS-over-TLS for queries]' \
'*-'{H,-https}'+[use DNS-over-HTTPS for queries]' \
'*-'{Q,-quic}'+[use DNS-over-QUIC for queries]' \
'*--tls-no-verify+[disable TLS verification]' \
'*--tls-host+[set TLS lookup hostname]:host:_hosts' \
'*-'{s,-short}'+[print terse output]' \
'*-'{j,-json}'+[present the results as JSON]' \
'*-'{X,-xml}'+[present the results as XML]' \
'*-'{y,-yaml}'+[present the results as YAML]' \
'*--trace+[trace from the root]' \
'*: :->args' && ret=0
if [[ -n $state ]]; then
if compset -P @; then
_wanted hosts expl 'DNS server' _hosts && ret=0;
else
_alternative 'hosts:host:_hosts' 'types:query type:_dns_types' && ret=0
fi
fi
return ret

View File

@ -1,8 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
/*
Package conf contains helper functions for getting local nameservers
Currently supported: Unix, Windows, Plan 9 (tested on 9front)
*/
package conf

View File

@ -1,57 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build plan9
package conf
import (
"errors"
"fmt"
"os"
"strings"
"github.com/miekg/dns"
)
// GetDNSConfig 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.
//
// See ndb(7).
func GetDNSConfig() (*dns.ClientConfig, error) {
dat, err := os.ReadFile("/net/ndb")
if err != nil {
return nil, fmt.Errorf("read ndb: %w", err)
}
str := string(dat)
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
}
// TODO: read more about how customizable Plan 9 is
return &dns.ClientConfig{
Servers: servers,
Search: []string{},
Port: "53",
}, nil
}
// Split the string at either space or tabs.
func splitChars(r rune) bool {
return r == ' ' || r == '\t'
}
var errPlan9 = errors.New("plan9Config: no DNS servers found")

View File

@ -1,25 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build plan9
package conf_test
import (
"runtime"
"testing"
"dns.froth.zone/awl/conf"
"gotest.tools/v3/assert"
)
func TestPlan9Config(t *testing.T) {
t.Parallel()
if runtime.GOOS != "plan9" {
t.Skip("Not running Plan 9, skipping")
}
conf, err := conf.GetDNSConfig()
assert.NilError(t, err)
assert.Assert(t, len(conf.Servers) != 0)
}

View File

@ -1,22 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build unix || (!windows && !plan9 && !js && !zos)
// FIXME: Can remove the or on the preprocessor when Go 1.18 becomes obsolete
package conf
import (
"fmt"
"github.com/miekg/dns"
)
// GetDNSConfig gets the DNS configuration, either from /etc/resolv.conf or somewhere else.
func GetDNSConfig() (*dns.ClientConfig, error) {
conf, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil {
return nil, fmt.Errorf("unix config: %w", err)
}
return conf, nil
}

View File

@ -1,26 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build unix || (!windows && !plan9 && !js && !zos)
// FIXME: Can remove the or on the preprocessor when Go 1.18 becomes obsolete
package conf_test
import (
"runtime"
"testing"
"dns.froth.zone/awl/conf"
"gotest.tools/v3/assert"
)
func TestUnixConfig(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" || runtime.GOOS == "plan9" || runtime.GOOS == "js" || runtime.GOOS == "zos" {
t.Skip("Not running Unix-like, skipping")
}
conf, err := conf.GetDNSConfig()
assert.NilError(t, err)
assert.Assert(t, len(conf.Servers) != 0)
}

View File

@ -1,18 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build js
package conf
import (
"errors"
"github.com/miekg/dns"
)
// GetDNSConfig doesn't do anything, because it is impossible (and bad security)
// if it could, as that is the definition of a DNS leak.
func GetDNSConfig() (*dns.ClientConfig, error) {
return nil, errNotImplemented
}
var errNotImplemented = errors.New("not implemented")

View File

@ -1,82 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build windows
package conf
import (
"fmt"
"strings"
"unsafe"
"github.com/miekg/dns"
"golang.org/x/sys/windows"
)
/*
"Stolen" from
https://gist.github.com/moloch--/9fb1c8497b09b45c840fe93dd23b1e98
*/
// GetDNSConfig (Windows version) returns all DNS server addresses using windows fuckery.
//
// Here be dragons.
func GetDNSConfig() (*dns.ClientConfig, error) {
length := uint32(100000)
byt := make([]byte, length)
// Windows is an utter fucking trash fire of an operating system.
//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
//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
}
}
}
// Take unique values only
servers := []string{}
for server := range resolvers {
servers = append(servers, server)
}
// TODO: Make configurable, based on defaults in https://github.com/miekg/dns/blob/master/clientconfig.go
return &dns.ClientConfig{
Servers: servers,
Search: []string{},
Port: "53",
Ndots: 1,
Timeout: 5,
Attempts: 1,
}, nil
}

View File

@ -1,25 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build windows
package conf_test
import (
"runtime"
"testing"
"dns.froth.zone/awl/conf"
"gotest.tools/v3/assert"
)
func TestWinConfig(t *testing.T) {
t.Parallel()
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)
}

View File

13
docs.go
View File

@ -1,13 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
/*
awl is a DNS lookup tool written in Go, similar to (and heavily inspired by) drill.
It runs and displays similar outputs to drill, without any frills.
Options are given to print with JSON, XML and YAML.
Supports results from DNS-over-[UDP, TCP, TLS, HTTPS, QUIC] servers
Why use this over the alternatives? Good question.
*/
package main

View File

@ -1 +0,0 @@
/dev/null

View File

@ -1,57 +0,0 @@
# Contributing to awl
First off, thank you! We appreciate your interest in wanting to contribute to awl.
> If you like the project, spread the word! Help us grow by sharing the project with anyone you thing might be interested. Here are some ways you can help:
>
> - Star the project on GitHub
> - Share the project on social media
> - Tell your friends about the project
## How to contribute
If you want to contribute to awl, you can do so by:
- [Reporting a bug](#reporting-a-bug)
- [Requesting a feature](#requesting-a-feature)
- [Submitting a pull request](#submitting-a-pull-request)
### Reporting a bug
If you find a bug in awl, please [open an issue](https://git.froth.zone/sam/awl/issues) on the project's issue tracker. When reporting a bug, please include as much information as possible, such as:
- The version of awl you are using
- The operating system you are using
- The steps to reproduce the bug
- Any error messages you received
### Requesting a feature
If you have an idea for a feature you would like to see in awl, please [open an issue](https://git.froth.zone/sam/awl/issues) on the project's issue tracker. When requesting a feature, please include as much information as possible, such as:
- A description of the feature
- Why you think the feature would be useful
- Any other relevant information
### Submitting a pull request
If you would like to contribute code to awl, you can do so by submitting a pull request. To submit a pull request, follow these steps:
1. Fork the project on Git
2. Create a new branch for your changes
3. Make your changes
4. Push your changes to your fork
5. [Open a pull request](https://git.froth.zone/sam/awl/pulls) on the project's Git repository
When submitting a pull request, please include as much information as possible, such as:
- A description of the changes you made
- Why you made the changes
- Any other relevant information
Alternatively, you can also contribute by sending an email to the project's [mailing list](https://lists.sr.ht/~sammefishe/awl-devel). For more information about using Git over email, refer to [git-send-email.io](https://git-send-email.io/)
#### Code Style
Before submitting a pull request, please run `make lint` to ensure your code adheres to the project's code style.
Make sure that you have `golangci-lint` installed, that is our linter of choice.

View File

@ -1,262 +0,0 @@
awl(1)
; SPDX-License-Identifier: BSD-3-Clause
# 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.
When no arguments are given, *awl* will perform an _NS_ query on the root ('_._').
When a nameserver is not given, *awl* will query a random system nameserver.
If one cannot be found, *awl* will query the localhost.
# OPTIONS
*-4*
Force only IPv4
*-6*
Force only IPv6
*-c*, *--class* _class_
DNS class to query (eg. IN, CH)
The default is IN.
*-h*
Show a "short" help message.
*-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_
Explicitly set a domain to query (eg. example.com)
*-t*, *--qType* _type_
Explicitly set a DNS type to query (eg. A, AAAA, NS)
The default is A.
*-v*[=_int_]
Set verbosity of output
Accepted values are as follows:
- _0_: Only log errors.
- _1_: Log warnings. *This is the default.*
- _2_: Log information *Default when specifying just* _-v_.
- _3_: Log information useful for debugging.
Setting a value lower than 0 disables logging entirely.
By default, specifying just *-v* sets the verbosity to 2 (info).
*-x*, *--reverse*
Do a reverse lookup. Sets default *type* to PTR.
*awl* automatically makes an IP or phone number canonical.
*-V*
Print the version and exit.
# QUERY OPTIONS
Anything in [brackets] is optional.
Many options are inherited from *dig*(1).
*--aa*[=_bool_], *+*[no]*aaflag*, *+*[no]*aaonly*
Sets the AA (Authoritative Answer) flag.
*--ad*[=_bool_], *+*[no]*adflag*
Sets the AD (Authenticated Data) flag.
*--no-additional*, *+*[no]*additional*
Toggle the display of the Additional section.
*--no-answer*, *+*[no]*answer*
Toggle the display of the Answer section.
*--no-authority*, *+*[no]*authority*
Toggle the display of the Authority section.
*--no-bad-cookie*, *+*[no]*badcookie*
\[Do not\] ignore BADCOOKIE responses
*--buffer-size* _int_, *+bufize*=_int_
Set the UDP message buffer size, using EDNS.
Max is 65535, minimum is zero.
The default value is 1232.
*--cd*[=_bool_], *+*[no]*cdflag*
(Set, Unset) CD (Checking Disabled) flag.
*--no-cookie*, *+*[no]*cookie*[=_string_]
Send an EDNS cookie.
This is enabled by default with a random string.
*-D*, *--dnssec*, *+dnssec*, *+do*
Request DNSSEC records as well.
This sets the DNSSEC OK bit (DO)
*--dnscrypt*, *+*[no]*dnscrypt*
Use DNSCrypt.
*--expire*. *+*[no]*expire*
Send an EDNS Expire.
*--edns-ver*, *+edns*[=_int_]
Enable EDNS and set EDNS version.
The maximum value is 255, and the minimum (default) value is 0.
*--no-edns*, *+noedns*
Disable EDNS.
*-H*, *--https*, *+*[no]*https*[=_endpoint_], *+*[no]*https-post*[=_endpoint_]
Use DNS-over-HTTPS (see RFC 8484).
The default endpoint is _/dns-query_
*+*[no]*https-get*[=_endpoint_]
Use an HTTP GET instead of an HTTP POST when making a DNS-over-HTTPS query.
*+*[no]*idnout*
Converts [or leaves] punycode on output.
Input is automatically translated to punycode.
*--no-truncate*, *+ignore*
Ignore UDP truncation (by default, awl *retries with TCP*).
*-j*, *--json*, *+*[no]*json*
Print the query results as JSON.
The result is *not* in compliance with RFC 8427.
*--keep-alive*, *+*[no]*keepalive*, *+*[no]*keepopen*
Send an EDNS keep-alive.
This does nothing unless using TCP.
*--nsid*, *+*[no]*nsid*
Send an EDNS name server ID request.
*--qr*[=_bool_], *+*[no]*qrflag*
Sets the QR (QueRy) flag.
*--no-question*, *+*[no]*question*
Toggle the display of the Question section.
*-Q*. *--quic*, *+*[no]*quic*
Use DNS-over-QUIC (see RFC 9250).
*-s*, *--short*, *+*[no]*short*
Print just the address of the answer.
*--no-statistics*, *+*[no]*stats*
Toggle the display of the Statistics (additional comments) section.
*--subnet* _ip_[_/prefix_], *+*[no]*subnet*[=_ip_[_/prefix_]]
Send an EDNS Client Subnet option with the specified address.
Like *dig*(1), setting the IP to _0.0.0.0/0_, _::/0_ or _0_ will signal the resolver to not use any client information when returning the query.
*--tc*[=_bool_], *+*[no]*tcflag*
Sets the TC (TrunCated) flag
*--tcp*, *+*[no]*tcp*, *+*[no]*vc*
Use TCP for the query (see RFC 7766).
*--timeout* _seconds_, *+timeout*=_seconds_
Set the timeout period. Floating point numbers are accepted.
0.5 seconds is the minimum.
*-T*, *--tls*, *+*[no]*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.
*--trace*, *+trace*
Trace the path of the query from the root, acting like its own resolver.
This option enables DNSSEC.
When *@server* is specified, this will only affect the initial query.
*--retries* _int_, *+tries*=_int_, *+retry*=_int_
Set the number of retries.
Retry is one more than tries, dig style.
*-X*, *--xml*, *+*[no]*xml*
Print the query results as XML.
*-y*, *--yaml*, *+*[no]*yaml*
Print the query results as YAML.
*-z*[=_bool_], *+*[no]*zflag*
Sets the Z (Zero) flag.
*--zflag* _int_, *+ednsflags*=_int_
Set the must-be-zero EDNS flags.
Decimal, hexadecimal and octal are supported.
Trying to set DO will be ignored.
# EXIT STATUS
The exit code is 0 when a query is successfully made and received.
This includes SERVFAILs, NOTIMPL among others.
# 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)
# STANDARDS
RFC 1034,1035 (UDP), 7766 (TCP), 7858 (TLS), 8484 (HTTPS), 9230 (QUIC)
Probably more, _https://www.statdns.com/rfc_
# BUGS
Full parity with *dig*(1) is not complete.
This man page is probably not complete.
Likely numerous more, report them either to the tracker
_https://git.froth.zone/sam/awl/issues_ or via email
_~sammefishe/awl-develop@lists.sr.ht_

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 505 KiB

View File

@ -1,7 +0,0 @@
#!/bin/sh
set -e
rm -f docs/awl.1.gz
scdoc <docs/awl.1.scd >docs/awl.1
gzip -9 -n docs/awl.1

@ -1 +0,0 @@
Subproject commit ab0ac7e0bd1b92339cc97ba026f148478df5a860

33
go.mod
View File

@ -1,33 +0,0 @@
module dns.froth.zone/awl
go 1.21.9
toolchain go1.22.2
require (
dns.froth.zone/dnscrypt v0.0.2
github.com/dchest/uniuri v1.2.0
github.com/miekg/dns v1.1.59
github.com/quic-go/quic-go v0.43.0
github.com/stefansundin/go-zflag v1.1.1
golang.org/x/net v0.24.0
golang.org/x/sys v0.19.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1
)
require (
github.com/AdguardTeam/golibs v0.20.3 // indirect
github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/onsi/ginkgo/v2 v2.16.0 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.19.0 // indirect
)

75
go.sum
View File

@ -1,75 +0,0 @@
dns.froth.zone/dnscrypt v0.0.2 h1:ytqjic/Qway4OuLw8ee0ubxdNzy+F3igUGDrEVwyLls=
dns.froth.zone/dnscrypt v0.0.2/go.mod h1:QZ0HAm7mWe8wz1dTqbKmTZhlr06x5qpe6ZCTPJ7uY30=
github.com/AdguardTeam/golibs v0.20.3 h1:5RiDypxBebd4Y2eftwm6JJla18oBqRHwanR7q0rnrxw=
github.com/AdguardTeam/golibs v0.20.3/go.mod h1:/votX6WK1PdcZ3T2kBOPjPCGmfhlKixhI6ljYrFRPvI=
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM=
github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/quic-go v0.43.0 h1:sjtsTKWX0dsHpuMJvLxGqoQdtgJnbAPWY+W+5vjYW/g=
github.com/quic-go/quic-go v0.43.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stefansundin/go-zflag v1.1.1 h1:XabhzWS588bVvV1z1UctSa6i8zHkXc5W9otqtnDSHw8=
github.com/stefansundin/go-zflag v1.1.1/go.mod h1:HXX5rABl1AoTcZ2jw+CqJ7R8irczaLquGNZlFabZooc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

135
main.go
View File

@ -1,135 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"errors"
"fmt"
"math/rand"
"os"
"strings"
"time"
cli "dns.froth.zone/awl/cmd"
"dns.froth.zone/awl/pkg/query"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
)
var version = "DEV"
func main() {
if opts, code, err := run(os.Args); err != nil {
// TODO: Make not ew
if errors.Is(err, util.ErrNotError) || strings.Contains(err.Error(), "help requested") {
os.Exit(0)
} else {
opts.Logger.Error(err)
os.Exit(code)
}
}
}
func run(args []string) (opts *util.Options, code int, err error) {
//nolint:gosec //Secure source not needed
r := rand.New(rand.NewSource(time.Now().Unix()))
opts, err = cli.ParseCLI(args, version)
if err != nil {
return opts, 1, fmt.Errorf("parse: %w", err)
}
var (
resp util.Response
keepTracing bool
tempDomain string
tempQueryType uint16
)
for ok := true; ok; ok = keepTracing {
if opts.Trace {
if keepTracing {
opts.Request.Name = tempDomain
opts.Request.Type = tempQueryType
} else {
tempDomain = opts.Request.Name
tempQueryType = opts.Request.Type
// Override the query because it needs to be done
opts.Request.Name = "."
opts.Request.Type = dns.TypeNS
}
}
// Retry queries if a query fails
for i := 0; i <= opts.Request.Retries; i++ {
resp, err = query.CreateQuery(opts)
if err == nil {
keepTracing = opts.Trace && (!resp.DNS.Authoritative || (opts.Request.Name == "." && tempDomain != ".")) && resp.DNS.MsgHdr.Rcode == 0
break
} else if i != opts.Request.Retries {
opts.Logger.Warn("Retrying request, error:", err)
}
}
// Query failed, make it fail
if err != nil {
return opts, 9, fmt.Errorf("query: %w", err)
}
var str string
if opts.JSON || opts.XML || opts.YAML {
str, err = query.PrintSpecial(resp, opts)
if err != nil {
return opts, 10, fmt.Errorf("format print: %w", err)
}
} else {
str, err = query.ToString(resp, opts)
if err != nil {
return opts, 15, fmt.Errorf("standard print: %w", err)
}
}
fmt.Println(str)
if keepTracing {
var records []dns.RR
if opts.Request.Name == "." {
records = resp.DNS.Answer
} else {
records = resp.DNS.Ns
}
want := func(rr dns.RR) bool {
temp := strings.Split(rr.String(), "\t")
return temp[len(temp)-2] == "NS"
}
i := 0
for _, x := range records {
if want(x) {
records[i] = x
i++
}
}
records = records[:i]
randomRR := records[r.Intn(len(records))]
v := strings.Split(randomRR.String(), "\t")
opts.Request.Server = strings.TrimSuffix(v[len(v)-1], ".")
opts.TLS = false
opts.HTTPS = false
opts.QUIC = false
opts.RD = false
opts.Request.Port = 53
}
}
return opts, 0, nil
}

View File

@ -1,48 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"testing"
"github.com/stefansundin/go-zflag"
"gotest.tools/v3/assert"
)
func TestRun(t *testing.T) {
// t.Parallel()
args := [][]string{
{"awl", "+yaml", "@1.1.1.1"},
{"awl", "+short", "@1.1.1.1"},
}
for _, test := range args {
test := test
t.Run("", func(t *testing.T) {
_, code, err := run(test)
assert.NilError(t, err)
assert.Equal(t, code, 0)
})
}
}
func TestTrace(t *testing.T) {
domains := []string{"git.froth.zone", "google.com", "amazon.com", "freecumextremist.com", "dns.froth.zone", "sleepy.cafe", "pkg.go.dev"}
for i := range domains {
args := []string{"awl", "+trace", domains[i], "@1.1.1.1"}
_, code, err := run(args)
assert.NilError(t, err)
assert.Equal(t, code, 0)
}
}
func TestHelp(t *testing.T) {
// t.Parallel()
args := []string{"awl", "-h"}
_, code, err := run(args)
assert.ErrorIs(t, err, zflag.ErrHelp)
assert.Equal(t, code, 1)
}

36
mkfile
View File

@ -1,36 +0,0 @@
# SPDX-License-Identifier: BSD-3-Clause
# Plan 9 mkfile
</$objtype/mkfile
GO = go
PROG = awl
VERSION = `{awk '{print substr($0,0,8)}' .git/refs/heads/master}
GOFLAGS = -ldflags=-s -ldflags=-w -ldflags=-X=main.version=$VERSION -trimpath
CGO_ENABLED = 0
all:V: $PROG
$PROG:
$GO build $GOFLAGS -o $target .
install:V:
$GO install $GOFLAGS .
# cp docs/$PROG.1 /sys/man/1/$PROG
test:V:
$GO test -v -cover ./...
fmt:V:
gofmt -w -s .
vet:V:
$GO vet ./...
lint:V: fmt vet
clean:V:
$GO clean
nuke:V: clean

View File

@ -1,33 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
/*
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
because awl is a cli utility it writes directly to std err.
*/
// Use the New() function to init logawl
//
// logger := logawl.New()
//
// You can call specific logging levels from your new logger using
//
// logger.Debug("Message to log")
// logger.Warning("Message to log")
// logger.Info("Message to log")
// logger.Error("Message to log")
//
// 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
// for example if
// Logger.SetLevel(3)
// is not called and you call
// Logger.Debug()
// this runs through
// IsLevel(level)
// to verify if the debug log should be sent to std.Err or not based on the current expected log level
package logawl

View File

@ -1,90 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package logawl
import (
"errors"
"io"
"sync"
"sync/atomic"
)
type (
// 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
}
)
// SetLevel stores whatever input value is in mem address of l.level.
func (logger *Logger) SetLevel(level Level) {
atomic.StoreInt32((*int32)(&logger.Level), int32(level))
}
// GetLevel gets the logger level.
func (logger *Logger) GetLevel() Level {
return logger.level()
}
// Retrieves whatever was stored in mem address of l.level.
func (logger *Logger) level() Level {
return Level(atomic.LoadInt32((*int32)(&logger.Level)))
}
// UnMarshalLevel unmarshalls the int value of level for writing the header.
func (logger *Logger) UnMarshalLevel(lv Level) (string, error) {
switch lv {
case ErrLevel:
return "ERROR ", nil
case WarnLevel:
return "WARN ", nil
case InfoLevel:
return "INFO ", nil
case DebugLevel:
return "DEBUG ", nil
}
return "", errInvalidLevel
}
// IsLevel returns true if the logger level is above the level given.
func (logger *Logger) IsLevel(level Level) bool {
return logger.level() >= level
}
// AllLevels is an array of all valid log levels.
var AllLevels = []Level{
ErrLevel,
WarnLevel,
InfoLevel,
DebugLevel,
}
const (
// ErrLevel is the fatal (error) log level.
ErrLevel Level = iota
// WarnLevel is for warning logs.
//
// Example: when one setting implies another, when a request fails but is retried.
WarnLevel
// InfoLevel is for saying what is going on when.
// This is essentially the "verbose" option.
//
// When in doubt, use info.
InfoLevel
// DebugLevel is for spewing debug structs/interfaces.
DebugLevel
)
var errInvalidLevel = errors.New("invalid log level")

View File

@ -1,175 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package logawl
import (
"fmt"
"os"
"sync/atomic"
"time"
)
// New instantiates Logger
//
// Level can be changed to one of the other log levels (ErrorLevel, WarnLevel, InfoLevel, DebugLevel).
func New() *Logger {
return &Logger{
Out: os.Stderr,
Level: WarnLevel, // Default value is WarnLevel
}
}
// Println takes any and prints it out to Logger -> Out (io.Writer (default is std.Err)).
func (logger *Logger) Println(level Level, in ...any) {
if atomic.LoadInt32(&logger.isDiscard) != 0 {
return
}
// If verbose is not set --debug etc print _nothing_
if logger.IsLevel(level) {
switch level { // Goes through log levels and does stuff based on them (currently nothing)
case ErrLevel:
if err := logger.Printer(ErrLevel, fmt.Sprintln(in...)); err != nil {
fmt.Fprintln(logger.Out, "Logger failed: ", err)
}
case WarnLevel:
if err := logger.Printer(WarnLevel, fmt.Sprintln(in...)); err != nil {
fmt.Fprintln(logger.Out, "Logger failed: ", err)
}
case InfoLevel:
if err := logger.Printer(InfoLevel, fmt.Sprintln(in...)); err != nil {
fmt.Fprintln(logger.Out, "Logger failed: ", err)
}
case DebugLevel:
if err := logger.Printer(DebugLevel, fmt.Sprintln(in...)); err != nil {
fmt.Fprintln(logger.Out, "Logger failed: ", err)
}
default:
break
}
}
}
// FormatHeader formats the log header as such <LogLevel> YYYY/MM/DD HH:MM:SS (local time) <the message to log>.
func (logger *Logger) FormatHeader(buf *[]byte, t time.Time, line int, level Level) error {
if lvl, err := logger.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, '/')
formatter(buf, int(month), 2)
*buf = append(*buf, '/')
formatter(buf, day, 2)
*buf = append(*buf, ' ')
hour, min, sec := t.Clock()
formatter(buf, hour, 2)
*buf = append(*buf, ':')
formatter(buf, min, 2)
*buf = append(*buf, ':')
formatter(buf, sec, 2)
*buf = append(*buf, ']')
*buf = append(*buf, ':')
*buf = append(*buf, ' ')
} else {
return errInvalidLevel
}
return nil
}
// Printer prints the formatted message directly to stdErr.
func (logger *Logger) Printer(level Level, s string) error {
now := time.Now()
var line int
logger.Mu.Lock()
defer logger.Mu.Unlock()
logger.buf = logger.buf[:0]
if err := logger.FormatHeader(&logger.buf, now, line, level); err != nil {
return err
}
logger.buf = append(logger.buf, s...)
if len(s) == 0 || s[len(s)-1] != '\n' {
logger.buf = append(logger.buf, '\n')
}
_, err := logger.Out.Write(logger.buf)
if err != nil {
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.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
b[bp] = byte('0' + i)
*buf = append(*buf, b[bp:]...)
}
// Debug calls print directly with Debug level.
func (logger *Logger) Debug(in ...any) {
logger.Println(DebugLevel, in...)
}
// Debugf calls print after formatting the string with Debug level.
func (logger *Logger) Debugf(format string, in ...any) {
logger.Println(DebugLevel, fmt.Sprintf(format, in...))
}
// Info calls print directly with Info level.
func (logger *Logger) Info(in ...any) {
logger.Println(InfoLevel, in...)
}
// Infof calls print after formatting the string with Info level.
func (logger *Logger) Infof(format string, in ...any) {
logger.Println(InfoLevel, fmt.Sprintf(format, in...))
}
// Warn calls print directly with Warn level.
func (logger *Logger) Warn(in ...any) {
logger.Println(WarnLevel, in...)
}
// Warnf calls print after formatting the string with Warn level.
func (logger *Logger) Warnf(format string, in ...any) {
logger.Println(WarnLevel, fmt.Sprintf(format, in...))
}
// Error calls print directly with Error level.
func (logger *Logger) Error(in ...any) {
logger.Println(ErrLevel, in...)
}
// Errorf calls print after formatting the string with Error level.
func (logger *Logger) Errorf(format string, in ...any) {
logger.Println(ErrLevel, fmt.Sprintf(format, in...))
}

View File

@ -1,109 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package logawl_test
import (
"bytes"
"testing"
"time"
"dns.froth.zone/awl/pkg/logawl"
"gotest.tools/v3/assert"
)
var logger = logawl.New()
func TestLogawl(t *testing.T) {
t.Parallel()
for i := range logawl.AllLevels {
logger.SetLevel(logawl.Level(i))
assert.Equal(t, logawl.Level(i), logger.GetLevel())
}
}
func TestUnmarshalLevels(t *testing.T) {
t.Parallel()
m := make(map[int]string)
for i := range logawl.AllLevels {
var err error
m[i], err = logger.UnMarshalLevel(logawl.Level(i))
assert.NilError(t, err)
}
for i := range logawl.AllLevels {
lv, err := logger.UnMarshalLevel(logawl.Level(i))
assert.NilError(t, err)
assert.Equal(t, m[i], lv)
}
lv, err := logger.UnMarshalLevel(logawl.Level(9001))
assert.Equal(t, "", lv)
assert.ErrorContains(t, err, "invalid log level")
}
func TestLogger(t *testing.T) {
t.Parallel()
for i := range logawl.AllLevels {
switch i {
case 0:
fn := func() {
logger.Error("Test", "E")
logger.Errorf("%s", "Test")
}
var buffer bytes.Buffer
logger.Out = &buffer
fn()
case 1:
fn := func() {
logger.Warn("Test")
logger.Warnf("%s", "Test")
}
var buffer bytes.Buffer
logger.Out = &buffer
fn()
case 2:
fn := func() {
logger.Info("Test")
logger.Infof("%s", "Test")
}
var buffer bytes.Buffer
logger.Out = &buffer
fn()
case 3:
fn := func() {
logger.Debug("Test")
logger.Debug("Test 2")
logger.Debugf("%s", "Test")
logger.Debugf("%s %d", "Test", 2)
}
var buffer bytes.Buffer
logger.Out = &buffer
fn()
}
}
}
func TestFmt(t *testing.T) {
t.Parallel()
ti := time.Now()
test := []byte("test")
// make sure error is error
assert.ErrorContains(t, logger.FormatHeader(&test, ti, 0, 9001), "invalid log level")
}

View File

@ -1,4 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Package query is for the various query types.
package query

View File

@ -1,313 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package query
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"strconv"
"strings"
"time"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
"golang.org/x/net/idna"
"gopkg.in/yaml.v3"
)
// 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 util.Response, opts *util.Options) (s string, err error) {
if res.DNS == nil {
return "<nil> MsgHdr", errNoMessage
}
var opt *dns.OPT
if !opts.Short {
if opts.Display.Comments {
s += res.DNS.MsgHdr.String() + " "
s += "QUERY: " + strconv.Itoa(len(res.DNS.Question)) + ", "
s += "ANSWER: " + strconv.Itoa(len(res.DNS.Answer)) + ", "
s += "AUTHORITY: " + strconv.Itoa(len(res.DNS.Ns)) + ", "
s += "ADDITIONAL: " + strconv.Itoa(len(res.DNS.Extra)) + "\n"
opt = res.DNS.IsEdns0()
if opt != nil && opts.Display.Opt {
// OPT PSEUDOSECTION
s += opt.String() + "\n"
}
}
if opts.Display.Question {
if len(res.DNS.Question) > 0 {
if opts.Display.Comments {
s += "\n;; QUESTION SECTION:\n"
}
for _, r := range res.DNS.Question {
str, err := stringParse(r.String(), false, opts)
if err != nil {
return "", fmt.Errorf("%w", err)
}
s += str + "\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 {
str, err := stringParse(r.String(), true, opts)
if err != nil {
return "", fmt.Errorf("%w", err)
}
s += str + "\n"
}
}
}
}
if opts.Display.Authority {
if len(res.DNS.Ns) > 0 {
if opts.Display.Comments {
s += "\n;; AUTHORITY SECTION:\n"
}
for _, r := range res.DNS.Ns {
if r != nil {
str, err := stringParse(r.String(), true, opts)
if err != nil {
return "", fmt.Errorf("%w", err)
}
s += str + "\n"
}
}
}
}
if opts.Display.Additional {
if len(res.DNS.Extra) > 0 && (opt == nil || len(res.DNS.Extra) > 1) {
if opts.Display.Comments {
s += "\n;; ADDITIONAL SECTION:\n"
}
for _, r := range res.DNS.Extra {
if r != nil && r.Header().Rrtype != dns.TypeOPT {
str, err := stringParse(r.String(), true, opts)
if err != nil {
return "", fmt.Errorf("%w", err)
}
s += str + "\n"
}
}
}
}
if opts.Display.Statistics {
s += "\n;; Query time: " + res.RTT.String()
s += "\n;; SERVER: " + opts.Request.Server + serverExtra(opts)
s += "\n;; WHEN: " + time.Now().Format(time.RFC1123Z)
s += "\n;; MSG SIZE rcvd: " + strconv.Itoa(res.DNS.Len()) + "\n"
}
} else {
// Print just the responses, nothing else
for i, resp := range res.DNS.Answer {
temp := strings.Split(resp.String(), "\t")
s += temp[len(temp)-1]
if opts.Identify {
s += " from server " + opts.Request.Server + " in " + res.RTT.String()
}
// Don't print newline on last line
if i != len(res.DNS.Answer)-1 {
s += "\n"
}
}
}
return
}
func serverExtra(opts *util.Options) string {
switch {
case opts.TCP:
return " (TCP)"
case opts.TLS:
return " (TLS)"
case opts.HTTPS, opts.DNSCrypt:
return ""
case opts.QUIC:
return " (QUIC)"
default:
return " (UDP)"
}
}
// stringParse edits the raw responses to user requests.
func stringParse(str string, isAns bool, opts *util.Options) (string, error) {
split := strings.Split(str, "\t")
// Make edits if so requested
// TODO: make less ew?
// This exists because the question section should be left alone EXCEPT for punycode.
if isAns {
if !opts.Display.TTL {
// Remove from existence
split = append(split[:1], split[2:]...)
}
if !opts.Display.ShowClass {
// Position depends on if the TTL is there or not.
if opts.Display.TTL {
split = append(split[:2], split[3:]...)
} else {
split = append(split[:1], split[2:]...)
}
}
if opts.Display.TTL && opts.Display.HumanTTL {
ttl, _ := strconv.Atoi(split[1])
split[1] = (time.Duration(ttl) * time.Second).String()
}
}
if opts.Display.UcodeTranslate {
var (
err error
semi string
)
if strings.HasPrefix(split[0], ";") {
split[0] = strings.TrimPrefix(split[0], ";")
semi = ";"
}
split[0], err = idna.ToUnicode(split[0])
if err != nil {
return "", fmt.Errorf("punycode: %w", err)
}
split[0] = semi + split[0]
}
return strings.Join(split, "\t"), nil
}
// PrintSpecial is for printing as JSON, XML or YAML.
// As of now JSON and XML use the stdlib version.
func PrintSpecial(res util.Response, opts *util.Options) (string, error) {
formatted, err := MakePrintable(res, opts)
if err != nil {
return "", err
}
switch {
case opts.JSON:
opts.Logger.Info("Printing as JSON")
json, err := json.MarshalIndent(formatted, " ", " ")
return string(json), err
case opts.XML:
opts.Logger.Info("Printing as XML")
xml, err := xml.MarshalIndent(formatted, " ", " ")
return string(xml), err
case opts.YAML:
opts.Logger.Info("Printing as YAML")
yaml, err := yaml.Marshal(formatted)
return string(yaml), err
default:
return "", errInvalidFormat
}
}
// MakePrintable takes a DNS message and makes it nicer to be printed as JSON,YAML,
// and XML. Little is changed beyond naming.
func MakePrintable(res util.Response, opts *util.Options) (*Message, error) {
var (
err error
msg = res.DNS
)
// The things I do for compatibility
ret := &Message{
DateString: time.Now().Format(time.RFC3339),
DateSeconds: time.Now().Unix(),
MsgSize: res.DNS.Len(),
ID: msg.Id,
Opcode: msg.Opcode,
Response: msg.Response,
Authoritative: msg.Authoritative,
Truncated: msg.Truncated,
RecursionDesired: msg.RecursionDesired,
RecursionAvailable: msg.RecursionAvailable,
AuthenticatedData: msg.AuthenticatedData,
CheckingDisabled: msg.CheckingDisabled,
Zero: msg.Zero,
QdCount: len(msg.Question),
AnCount: len(msg.Answer),
NsCount: len(msg.Ns),
ArCount: len(msg.Extra),
}
opt := msg.IsEdns0()
if opt != nil && opts.Display.Opt {
ret.EDNS0, err = ret.ParseOpt(msg.Rcode, *opt)
if err != nil {
return nil, fmt.Errorf("edns print: %w", err)
}
}
if opts.Display.Question {
err = ret.displayQuestion(msg, opts, opt)
if err != nil {
return nil, fmt.Errorf("unable to display questions: %w", err)
}
}
if opts.Display.Answer {
err = ret.displayAnswers(msg, opts, opt)
if err != nil {
return nil, fmt.Errorf("unable to display answers: %w", err)
}
}
if opts.Display.Authority {
err = ret.displayAuthority(msg, opts, opt)
if err != nil {
return nil, fmt.Errorf("unable to display authority: %w", err)
}
}
if opts.Display.Additional {
err = ret.displayAdditional(msg, opts, opt)
if err != nil {
return nil, fmt.Errorf("unable to display additional: %w", err)
}
}
return ret, nil
}
var errInvalidFormat = errors.New("this should never happen")

View File

@ -1,230 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package query_test
import (
"testing"
"dns.froth.zone/awl/pkg/query"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
"gotest.tools/v3/assert"
)
func TestRealPrint(t *testing.T) {
t.Parallel()
opts := []*util.Options{
{
Logger: util.InitLogger(0),
TCP: true,
HeaderFlags: util.HeaderFlags{
RD: true,
},
JSON: true,
Display: util.Display{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
TTL: true,
HumanTTL: true,
ShowQuery: true,
},
Request: util.Request{
Server: "a.gtld-servers.net",
Port: 53,
Type: dns.StringToType["NS"],
Class: 1,
Name: "google.com.",
Retries: 3,
},
EDNS: util.EDNS{
EnableEDNS: false,
},
},
{
Logger: util.InitLogger(0),
TCP: true,
HeaderFlags: util.HeaderFlags{
RD: true,
},
Verbosity: 0,
Short: true,
Identify: true,
YAML: false,
Display: util.Display{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
TTL: true,
ShowQuery: true,
},
Request: util.Request{
Server: "ns1.google.com",
Port: 53,
Type: dns.StringToType["NS"],
Class: 1,
Name: "google.com.",
Retries: 3,
},
EDNS: util.EDNS{
EnableEDNS: false,
},
},
{
Logger: util.InitLogger(0),
HTTPS: true,
HeaderFlags: util.HeaderFlags{
RD: true,
},
Identify: true,
XML: true,
Display: util.Display{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
TTL: true,
HumanTTL: true,
ShowQuery: true,
},
Request: util.Request{
Server: "https://dns.froth.zone/dns-query",
Port: 443,
Type: dns.StringToType["NS"],
Class: 1,
Name: "freecumextremist.com.",
Retries: 3,
},
EDNS: util.EDNS{
EnableEDNS: false,
DNSSEC: true,
},
},
{
Logger: util.InitLogger(0),
TLS: true,
HeaderFlags: util.HeaderFlags{
RD: true,
},
Verbosity: 0,
Display: util.Display{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
TTL: false,
ShowQuery: true,
},
Request: util.Request{
Server: "dns.google",
Port: 853,
Type: dns.StringToType["NS"],
Class: 1,
Name: "freecumextremist.com.",
Retries: 3,
},
},
{
Logger: util.InitLogger(0),
TCP: true,
HeaderFlags: util.HeaderFlags{
AA: true,
RD: true,
},
Verbosity: 0,
YAML: true,
Display: util.Display{
Comments: true,
Question: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: false,
TTL: true,
ShowQuery: true,
},
Request: util.Request{
Server: "rin.froth.zone",
Port: 53,
Type: dns.StringToType["A"],
Class: 1,
Name: "froth.zone.",
Retries: 3,
},
EDNS: util.EDNS{
EnableEDNS: true,
Cookie: true,
Padding: true,
},
},
}
for _, test := range opts {
test := test
t.Run("", func(t *testing.T) {
t.Parallel()
var (
res util.Response
err error
)
for i := 0; i <= test.Request.Retries; i++ {
res, err = query.CreateQuery(test)
if err == nil {
break
}
}
assert.NilError(t, err)
if test.JSON || test.XML || test.YAML {
str := ""
str, err = query.PrintSpecial(res, test)
assert.NilError(t, err)
assert.Assert(t, str != "")
}
str, err := query.ToString(res, test)
assert.NilError(t, err)
assert.Assert(t, str != "")
})
}
}
func TestBadFormat(t *testing.T) {
t.Parallel()
_, err := query.PrintSpecial(util.Response{DNS: new(dns.Msg)}, new(util.Options))
assert.ErrorContains(t, err, "never happen")
}
func TestEmpty(t *testing.T) {
t.Parallel()
str, err := query.ToString(util.Response{}, new(util.Options))
assert.Error(t, err, "no message")
assert.Assert(t, str == "<nil> MsgHdr")
}

View File

@ -1,145 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package query
import (
"fmt"
"strconv"
"dns.froth.zone/awl/pkg/resolvers"
"dns.froth.zone/awl/pkg/util"
"github.com/dchest/uniuri"
"github.com/miekg/dns"
)
// 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
// Set standard flags
req.MsgHdr.Response = opts.QR
req.MsgHdr.Authoritative = opts.AA
req.MsgHdr.Truncated = opts.TC
req.MsgHdr.RecursionDesired = opts.RD
req.MsgHdr.RecursionAvailable = opts.RA
req.MsgHdr.Zero = opts.Z
req.MsgHdr.AuthenticatedData = opts.AD
req.MsgHdr.CheckingDisabled = opts.CD
// EDNS time :)
if opts.EDNS.EnableEDNS {
edns := new(dns.OPT)
edns.Hdr.Name = "."
edns.Hdr.Rrtype = dns.TypeOPT
edns.SetVersion(opts.EDNS.Version)
if opts.EDNS.Cookie {
cookie := new(dns.EDNS0_COOKIE)
cookie.Code = dns.EDNS0COOKIE
cookie.Cookie = uniuri.NewLenChars(16, []byte("1234567890abcdef"))
edns.Option = append(edns.Option, cookie)
opts.Logger.Info("Setting EDNS cookie to", cookie.Cookie)
}
if opts.EDNS.Expire {
edns.Option = append(edns.Option, new(dns.EDNS0_EXPIRE))
opts.Logger.Info("Setting EDNS Expire option")
}
if opts.EDNS.KeepOpen {
edns.Option = append(edns.Option, new(dns.EDNS0_TCP_KEEPALIVE))
opts.Logger.Info("Setting EDNS TCP Keepalive option")
}
if opts.EDNS.Nsid {
edns.Option = append(edns.Option, new(dns.EDNS0_NSID))
opts.Logger.Info("Setting EDNS NSID option")
}
if opts.EDNS.Padding {
edns.Option = append(edns.Option, new(dns.EDNS0_PADDING))
opts.Logger.Info("Setting EDNS padding")
}
edns.SetUDPSize(opts.EDNS.BufSize)
opts.Logger.Info("EDNS UDP buffer set to", opts.EDNS.BufSize)
edns.SetZ(opts.EDNS.ZFlag)
opts.Logger.Info("EDNS Z flag set to", opts.EDNS.ZFlag)
if opts.EDNS.DNSSEC {
edns.SetDo()
opts.Logger.Info("EDNS DNSSEC OK set")
}
if opts.EDNS.Subnet.Address != nil {
edns.Option = append(edns.Option, &opts.EDNS.Subnet)
}
req.Extra = append(req.Extra, edns)
} else if opts.EDNS.DNSSEC {
req.SetEdns0(1232, true)
opts.Logger.Warn("DNSSEC implies EDNS, EDNS enabled")
opts.Logger.Info("DNSSEC enabled, UDP buffer set to 1232")
}
opts.Logger.Debug(req)
if !opts.Short {
if opts.Display.ShowQuery {
opts.Logger.Info("Printing constructed query")
var (
str string
err error
)
if opts.JSON || opts.XML || opts.YAML {
str, err = PrintSpecial(util.Response{DNS: req}, opts)
if err != nil {
return util.Response{}, err
}
} else {
temp := opts.Display.Statistics
opts.Display.Statistics = false
str, err = ToString(
util.Response{
DNS: req,
RTT: 0,
}, opts)
if err != nil {
return util.Response{}, err
}
opts.Display.Statistics = temp
str += "\n;; QUERY SIZE: " + strconv.Itoa(req.Len()) + "\n"
}
fmt.Println(str)
opts.Display.ShowQuery = false
}
}
resolver, err := resolvers.LoadResolver(opts)
if err != nil {
return util.Response{}, fmt.Errorf("unable to load resolvers: %w", err)
}
opts.Logger.Info("Query successfully loaded")
//nolint:wrapcheck // Error wrapping not needed here
return resolver.LookUp(req)
}

View File

@ -1,157 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package query_test
import (
"testing"
"time"
"dns.froth.zone/awl/pkg/query"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
"gotest.tools/v3/assert"
)
func TestCreateQ(t *testing.T) {
t.Parallel()
//nolint:govet // I could not be assed to refactor this, and it is only for tests
tests := []struct {
name string
opts *util.Options
}{
{
"1",
&util.Options{
Logger: util.InitLogger(0),
HeaderFlags: util.HeaderFlags{
Z: true,
},
YAML: true,
Request: util.Request{
Server: "8.8.4.4",
Port: 53,
Type: dns.TypeA,
Name: "example.com.",
Retries: 3,
},
Display: util.Display{
Comments: true,
Question: true,
Opt: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
ShowQuery: true,
},
EDNS: util.EDNS{
ZFlag: 1,
BufSize: 1500,
EnableEDNS: true,
Cookie: true,
DNSSEC: true,
Expire: true,
KeepOpen: true,
Nsid: true,
Padding: true,
Version: 0,
},
},
},
{
"2",
&util.Options{
Logger: util.InitLogger(0),
HeaderFlags: util.HeaderFlags{
Z: true,
},
XML: true,
Request: util.Request{
Server: "8.8.4.4",
Port: 53,
Type: dns.TypeA,
Name: "example.com.",
Retries: 3,
},
Display: util.Display{
Comments: true,
Question: true,
Opt: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
UcodeTranslate: true,
ShowQuery: true,
},
},
},
{
"3",
&util.Options{
Logger: util.InitLogger(0),
JSON: true,
QUIC: true,
Request: util.Request{
Server: "dns.froth.zone",
Port: 853,
Type: dns.TypeA,
Name: "example.com.",
Retries: 3,
Timeout: time.Second,
},
Display: util.Display{
Comments: true,
Question: true,
Opt: true,
Answer: true,
Authority: true,
Additional: true,
Statistics: true,
ShowQuery: true,
},
EDNS: util.EDNS{
EnableEDNS: true,
DNSSEC: true,
Cookie: true,
Expire: true,
Nsid: true,
},
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
var (
res util.Response
err error
)
for i := 0; i <= test.opts.Request.Retries; i++ {
res, err = query.CreateQuery(test.opts)
if err == nil {
break
}
}
assert.NilError(t, err)
assert.Assert(t, res != util.Response{})
str, err := query.PrintSpecial(res, test.opts)
assert.NilError(t, err)
assert.Assert(t, str != "")
str, err = query.ToString(res, test.opts)
assert.NilError(t, err)
assert.Assert(t, str != "")
})
}
}

View File

@ -1,116 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package query
import (
"errors"
)
// Message is for overall DNS responses.
//
//nolint:govet,tagliatelle // Better looking output is worth a few bytes.
type Message struct {
DateString string `json:"dateString,omitempty" xml:"dateString,omitempty" yaml:"dateString,omitempty"`
DateSeconds int64 `json:"dateSeconds,omitempty" xml:"dateSeconds,omitempty" yaml:"dateSeconds,omitempty"`
MsgSize int `json:"msgLength,omitempty" xml:"msgSize,omitempty" yaml:"msgSize,omitempty"`
ID uint16 `json:"ID" xml:"ID" yaml:"ID" example:"12"`
Opcode int `json:"opcode" xml:"opcode" yaml:"opcode" example:"QUERY"`
Response bool `json:"QR" xml:"QR" yaml:"QR" example:"true"`
Authoritative bool `json:"AA" xml:"AA" yaml:"AA" example:"false"`
Truncated bool `json:"TC" xml:"TC" yaml:"TC" example:"false"`
RecursionDesired bool `json:"RD" xml:"RD" yaml:"RD" example:"true"`
RecursionAvailable bool `json:"RA" xml:"RA" yaml:"RA" example:"true"`
AuthenticatedData bool `json:"AD" xml:"AD" yaml:"AD" example:"false"`
CheckingDisabled bool `json:"CD" xml:"CD" yaml:"CD" example:"false"`
Zero bool `json:"Z" xml:"Z" yaml:"Z" example:"false"`
QdCount int `json:"QDCOUNT" xml:"QDCOUNT" yaml:"QDCOUNT" example:"0"`
AnCount int `json:"ANCOUNT" xml:"ANCOUNT" yaml:"ANCOUNT" example:"0"`
NsCount int `json:"NSCOUNT" xml:"NSCOUNT" yaml:"NSCOUNT" example:"0"`
ArCount int `json:"ARCOUNT" xml:"ARCOUNT" yaml:"ARCOUNT" example:"0"`
Name string `json:"QNAME,omitempty" xml:"QNAME,omitempty" yaml:"QNAME,omitempty" example:"localhost"`
Type uint16 `json:"QTYPE,omitempty" xml:"QTYPE,omitempty" yaml:"QTYPE,omitempty" example:"IN"`
TypeName string `json:"QTYPEname,omitempty" xml:"QTYPEname,omitempty" yaml:"QTYPEname,omitempty" example:"IN"`
Class uint16 `json:"QCLASS,omitempty" xml:"QCLASS,omitempty" yaml:"QCLASS,omitempty" example:"A"`
ClassName string `json:"QCLASSname,omitempty" xml:"QCLASSname,omitempty" yaml:"QCLASSname,omitempty" example:"1"`
EDNS0 EDNS0 `json:",omitempty" xml:",omitempty" yaml:",omitempty"`
// Answer Section
AnswerRRs []Answer `json:"answersRRs,omitempty" xml:"answersRRs,omitempty" yaml:"answersRRs,omitempty" example:"false"`
AuthoritativeRRs []Answer `json:"authorityRRs,omitempty" xml:"authorityRRs,omitempty" yaml:"authorityRRs,omitempty" example:"false"`
AdditionalRRs []Answer `json:"additionalRRs,omitempty" xml:"additionalRRs,omitempty" yaml:"additionalRRs,omitempty" example:"false"`
}
// Answer is for DNS Resource Headers.
//
//nolint:govet,tagliatelle
type Answer struct {
Name string `json:"NAME,omitempty" xml:"NAME,omitempty" yaml:"NAME,omitempty" example:"127.0.0.1"`
Type uint16 `json:"TYPE,omitempty" xml:"TYPE,omitempty" yaml:"TYPE,omitempty" example:"1"`
TypeName string `json:"TYPEname,omitempty" xml:"TYPEname,omitempty" yaml:"TYPEname,omitempty" example:"A"`
Class uint16 `json:"CLASS,omitempty" xml:"CLASS,omitempty" yaml:"CLASS,omitempty" example:"1"`
ClassName string `json:"CLASSname,omitempty" xml:"CLASSname,omitempty" yaml:"CLASSname,omitempty" example:"IN"`
TTL any `json:"TTL,omitempty" xml:"TTL,omitempty" yaml:"TTL,omitempty" example:"0ms"`
Value string `json:"rdata,omitempty" xml:"rdata,omitempty" yaml:"rdata,omitempty"`
Rdlength uint16 `json:"RDLENGTH,omitempty" xml:"RDLENGTH,omitempty" yaml:"RDLENGTH,omitempty"`
Rdhex string `json:"RDATAHEX,omitempty" xml:"RDATAHEX,omitempty" yaml:"RDATAHEX,omitempty"`
}
// EDNS0 is for all EDNS options.
//
// RFC: https://datatracker.ietf.org/docs/draft-peltan-edns-presentation-format/
//
//nolint:govet,tagliatelle
type EDNS0 struct {
Flags []string `json:"FLAGS" xml:"FLAGS" yaml:"FLAGS"`
Rcode string `json:"RCODE" xml:"RCODE" yaml:"RCODE"`
PayloadSize uint16 `json:"UDPSIZE" xml:"UDPSIZE" yaml:"UDPSIZE"`
LLQ *EdnsLLQ `json:"LLQ,omitempty" xml:"LLQ,omitempty" yaml:"LLQ,omitempty"`
NsidHex string `json:"NSIDHEX,omitempty" xml:"NSIDHEX,omitempty" yaml:"NSIDHEX,omitempty"`
Nsid string `json:"NSID,omitempty" xml:"NSID,omitempty" yaml:"NSID,omitempty"`
Dau []uint8 `json:"DAU,omitempty" xml:"DAU,omitempty" yaml:"DAU,omitempty"`
Dhu []uint8 `json:"DHU,omitempty" xml:"DHU,omitempty" yaml:"DHU,omitempty"`
N3u []uint8 `json:"N3U,omitempty" xml:"N3U,omitempty" yaml:"N3U,omitempty"`
Subnet *EDNSSubnet `json:"ECS,omitempty" xml:"ECS,omitempty" yaml:"ECS,omitempty"`
Expire uint32 `json:"EXPIRE,omitempty" xml:"EXPIRE,omitempty" yaml:"EXPIRE,omitempty"`
Cookie []string `json:"COOKIE,omitempty" xml:"COOKIE,omitempty" yaml:"COOKIE,omitempty"`
KeepAlive uint16 `json:"KEEPALIVE,omitempty" xml:"KEEPALIVE,omitempty" yaml:"KEEPALIVE,omitempty"`
Padding string `json:"PADDING,omitempty" xml:"PADDING,omitempty" yaml:"PADDING,omitempty"`
Chain string `json:"CHAIN,omitempty" xml:"CHAIN,omitempty" yaml:"CHAIN,omitempty"`
EDE *EDNSErr `json:"EDE,omitempty" xml:"EDE,omitempty" yaml:"EDE,omitempty"`
}
// EdnsLLQ is for Long-lived queries.
//
//nolint:tagliatelle
type EdnsLLQ struct {
Version uint16 `json:"LLQ-VERSION" xml:"LLQ-VERSION" yaml:"LLQ-VERSION"`
Opcode uint16 `json:"LLQ-OPCODE" xml:"LLQ-OPCODE" yaml:"LLQ-OPCODE"`
Error uint16 `json:"LLQ-ERROR" xml:"LLQ-ERROR" yaml:"LLQ-ERROR"`
ID uint64 `json:"LLQ-ID" xml:"LLQ-ID" yaml:"LLQ-ID"`
Lease uint32 `json:"LLQ-LEASE" xml:"LLQ-LEASE" yaml:"LLQ-LEASE"`
}
// EDNSSubnet is for EDNS subnet options,
//
//nolint:govet,tagliatelle
type EDNSSubnet struct {
Family uint16 `json:"FAMILY" xml:"FAMILY" yaml:"FAMILY"`
IP string
Source uint8 `json:"SOURCE" xml:"SOURCE" yaml:"SOURCE"`
Scope uint8 `json:"SCOPE,omitempty" xml:"SCOPE,omitempty" yaml:"SCOPE,omitempty"`
}
// EDNSErr is for EDE codes
//
//nolint:govet,tagliatelle
type EDNSErr struct {
Code uint16 `json:"INFO-CODE" xml:"INFO-CODE" yaml:"INFO-CODE"`
Purpose string
Text string `json:"EXTRA-TEXT,omitempty" xml:"EXTRA-TEXT,omitempty" yaml:"EXTRA-TEXT,omitempty"`
}
var errNoMessage = errors.New("no message")

View File

@ -1,258 +0,0 @@
package query
import (
"encoding/hex"
"fmt"
"strings"
"time"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
"golang.org/x/net/idna"
)
func (message *Message) displayQuestion(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error {
var (
name string
err error
)
for _, question := range msg.Question {
if opts.Display.UcodeTranslate {
name, err = idna.ToUnicode(question.Name)
if err != nil {
return fmt.Errorf("punycode to unicode: %w", err)
}
} else {
name = question.Name
}
message.Name = name
message.Type = question.Qtype
message.TypeName = dns.TypeToString[question.Qtype]
message.Class = question.Qclass
message.ClassName = dns.ClassToString[question.Qclass]
}
return nil
}
func (message *Message) displayAnswers(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error {
var (
ttl any
name string
err error
)
for _, answer := range msg.Answer {
temp := strings.Split(answer.String(), "\t")
if opts.Display.TTL {
if opts.Display.HumanTTL {
ttl = (time.Duration(answer.Header().Ttl) * time.Second).String()
} else {
ttl = answer.Header().Ttl
}
}
if opts.Display.UcodeTranslate {
name, err = idna.ToUnicode(answer.Header().Name)
if err != nil {
return fmt.Errorf("punycode to unicode: %w", err)
}
} else {
name = answer.Header().Name
}
message.AnswerRRs = append(message.AnswerRRs, Answer{
Name: name,
ClassName: dns.ClassToString[answer.Header().Class],
Class: answer.Header().Class,
TypeName: dns.TypeToString[answer.Header().Rrtype],
Type: answer.Header().Rrtype,
Rdlength: answer.Header().Rdlength,
TTL: ttl,
Value: temp[len(temp)-1],
})
}
return nil
}
func (message *Message) displayAuthority(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error {
var (
ttl any
name string
err error
)
for _, ns := range msg.Ns {
temp := strings.Split(ns.String(), "\t")
if opts.Display.TTL {
if opts.Display.HumanTTL {
ttl = (time.Duration(ns.Header().Ttl) * time.Second).String()
} else {
ttl = ns.Header().Ttl
}
}
if opts.Display.UcodeTranslate {
name, err = idna.ToUnicode(ns.Header().Name)
if err != nil {
return fmt.Errorf("punycode to unicode: %w", err)
}
} else {
name = ns.Header().Name
}
message.AuthoritativeRRs = append(message.AuthoritativeRRs, Answer{
Name: name,
TypeName: dns.TypeToString[ns.Header().Rrtype],
Type: ns.Header().Rrtype,
Class: ns.Header().Class,
ClassName: dns.ClassToString[ns.Header().Class],
Rdlength: ns.Header().Rdlength,
TTL: ttl,
Value: temp[len(temp)-1],
})
}
return nil
}
func (message *Message) displayAdditional(msg *dns.Msg, opts *util.Options, opt *dns.OPT) error {
var (
ttl any
name string
err error
)
for _, additional := range msg.Extra {
if additional.Header().Rrtype == dns.StringToType["OPT"] {
continue
} else {
temp := strings.Split(additional.String(), "\t")
if opts.Display.TTL {
if opts.Display.HumanTTL {
ttl = (time.Duration(additional.Header().Ttl) * time.Second).String()
} else {
ttl = additional.Header().Ttl
}
}
if opts.Display.UcodeTranslate {
name, err = idna.ToUnicode(additional.Header().Name)
if err != nil {
return fmt.Errorf("punycode to unicode: %w", err)
}
} else {
name = additional.Header().Name
}
message.AdditionalRRs = append(message.AdditionalRRs, Answer{
Name: name,
TypeName: dns.TypeToString[additional.Header().Rrtype],
Type: additional.Header().Rrtype,
Class: additional.Header().Class,
ClassName: dns.ClassToString[additional.Header().Class],
Rdlength: additional.Header().Rdlength,
TTL: ttl,
Value: temp[len(temp)-1],
})
}
}
return nil
}
// ParseOpt parses opts.
func (message *Message) ParseOpt(rcode int, rr dns.OPT) (ret EDNS0, err error) {
ret.Rcode = dns.RcodeToString[rcode]
// Most of this is taken from https://github.com/miekg/dns/blob/master/edns.go#L76
if rr.Do() {
ret.Flags = append(ret.Flags, "DO")
}
for i := uint32(1); i <= 0x7FFF; i <<= 1 {
if rr.Hdr.Ttl&i != 0 {
ret.Flags = append(ret.Flags, fmt.Sprintf("BIT%d", i))
}
}
ret.PayloadSize = rr.UDPSize()
for _, opt := range rr.Option {
switch opt := opt.(type) {
case *dns.EDNS0_NSID:
str := opt.String()
hex, err := hex.DecodeString(str)
if err != nil {
return ret, fmt.Errorf("%w", err)
}
ret.NsidHex = string(hex)
ret.Nsid = str
case *dns.EDNS0_SUBNET:
ret.Subnet = &EDNSSubnet{
Source: opt.SourceNetmask,
Family: opt.Family,
}
// 1: IPv4 2: IPv6
if ret.Subnet.Family <= 2 {
ret.Subnet.IP = opt.Address.String()
} else {
ret.Subnet.IP = hex.EncodeToString([]byte(opt.Address))
}
if opt.SourceScope != 0 {
ret.Subnet.Scope = opt.SourceScope
}
case *dns.EDNS0_COOKIE:
ret.Cookie = append(ret.Cookie, opt.String())
case *dns.EDNS0_EXPIRE:
ret.Expire = opt.Expire
case *dns.EDNS0_TCP_KEEPALIVE:
ret.KeepAlive = opt.Timeout
case *dns.EDNS0_LLQ:
ret.LLQ = &EdnsLLQ{
Version: opt.Version,
Opcode: opt.Opcode,
Error: opt.Error,
ID: opt.Id,
Lease: opt.LeaseLife,
}
case *dns.EDNS0_DAU:
ret.Dau = opt.AlgCode
case *dns.EDNS0_DHU:
ret.Dhu = opt.AlgCode
case *dns.EDNS0_N3U:
ret.N3u = opt.AlgCode
case *dns.EDNS0_PADDING:
ret.Padding = string(opt.Padding)
case *dns.EDNS0_EDE:
ret.EDE = &EDNSErr{
Code: opt.InfoCode,
Purpose: dns.ExtendedErrorCodeToString[opt.InfoCode],
Text: opt.ExtraText,
}
}
}
return ret, nil
}

View File

@ -1,64 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package resolvers
import (
"fmt"
"time"
"dns.froth.zone/awl/pkg/util"
"dns.froth.zone/dnscrypt"
"github.com/miekg/dns"
)
// DNSCryptResolver is for making DNSCrypt queries.
type DNSCryptResolver struct {
opts *util.Options
}
var _ Resolver = (*DNSCryptResolver)(nil)
// LookUp performs a DNS query.
func (resolver *DNSCryptResolver) LookUp(msg *dns.Msg) (resp util.Response, err error) {
client := dnscrypt.Client{
Timeout: resolver.opts.Request.Timeout,
UDPSize: 1232,
}
if resolver.opts.TCP || resolver.opts.TLS {
client.Net = tcp
} else {
client.Net = udp
}
switch {
case resolver.opts.IPv4:
client.Net += "4"
case resolver.opts.IPv6:
client.Net += "6"
}
resolver.opts.Logger.Debug("Using", client.Net, "for making the request")
resolverInf, err := client.Dial(resolver.opts.Request.Server)
if err != nil {
return resp, fmt.Errorf("dnscrypt: dial: %w", err)
}
now := time.Now()
res, err := client.Exchange(msg, resolverInf)
rtt := time.Since(now)
if err != nil {
return resp, fmt.Errorf("dnscrypt: exchange: %w", err)
}
resp = util.Response{
DNS: res,
RTT: rtt,
}
resolver.opts.Logger.Info("Request successful")
return
}

View File

@ -1,93 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package resolvers_test
import (
"errors"
"testing"
"dns.froth.zone/awl/pkg/query"
"dns.froth.zone/awl/pkg/util"
"dns.froth.zone/dnscrypt"
"github.com/miekg/dns"
"gotest.tools/v3/assert"
)
func TestDNSCrypt(t *testing.T) {
t.Parallel()
//nolint:govet // I could not be assed to refactor this, and it is only for tests
tests := []struct {
name string
opts *util.Options
}{
{
"Valid",
&util.Options{
Logger: util.InitLogger(0),
DNSCrypt: true,
Request: util.Request{
Server: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
Type: dns.TypeA,
Name: "example.com.",
Retries: 3,
},
},
},
{
"Valid (TCP)",
&util.Options{
Logger: util.InitLogger(0),
DNSCrypt: true,
TCP: true,
IPv4: true,
Request: util.Request{
Server: "sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
Type: dns.TypeAAAA,
Name: "example.com.",
Retries: 3,
},
},
},
{
"Invalid",
&util.Options{
Logger: util.InitLogger(0),
DNSCrypt: true,
TCP: true,
IPv4: true,
Request: util.Request{
Server: "QMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
Type: dns.TypeAAAA,
Name: "example.com.",
Retries: 0,
},
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
var (
res util.Response
err error
)
for i := 0; i <= test.opts.Request.Retries; i++ {
res, err = query.CreateQuery(test.opts)
if err == nil || errors.Is(err, dnscrypt.ErrInvalidDNSStamp) {
break
}
}
if err == nil {
assert.Assert(t, res != util.Response{})
} else {
assert.ErrorContains(t, err, "unsupported stamp")
}
})
}
}

View File

@ -1,104 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package resolvers
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"net/http"
"time"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
)
// HTTPSResolver is for DNS-over-HTTPS queries.
type HTTPSResolver struct {
opts *util.Options
client http.Client
}
var _ Resolver = (*HTTPSResolver)(nil)
// LookUp performs a DNS query.
func (resolver *HTTPSResolver) LookUp(msg *dns.Msg) (resp util.Response, err error) {
resolver.client = http.Client{
Timeout: resolver.opts.Request.Timeout,
Transport: &http.Transport{
MaxConnsPerHost: 1,
MaxIdleConns: 1,
MaxIdleConnsPerHost: 1,
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
//nolint:gosec // This is intentional if the user requests it
InsecureSkipVerify: resolver.opts.TLSNoVerify,
ServerName: resolver.opts.TLSHost,
},
},
}
buf, err := msg.Pack()
if err != nil {
return resp, fmt.Errorf("doh: packing: %w", err)
}
resolver.opts.Logger.Debug("https: sending HTTPS request")
var method string
if resolver.opts.HTTPSOptions.Get {
method = "GET"
} else {
method = "POST"
}
req, err := http.NewRequest(method, resolver.opts.Request.Server, bytes.NewBuffer(buf))
if err != nil {
return resp, fmt.Errorf("doh: request creation: %w", err)
}
req.Header.Set("Content-Type", "application/dns-message")
req.Header.Set("Accept", "application/dns-message")
now := time.Now()
res, err := resolver.client.Do(req)
resp.RTT = time.Since(now)
if err != nil {
// overwrite RTT or else tests will fail
resp.RTT = 0
return resp, fmt.Errorf("doh: HTTP request: %w", err)
}
if res.StatusCode != http.StatusOK {
// overwrite RTT or else tests will fail
resp.RTT = 0
return resp, &util.ErrHTTPStatus{Code: res.StatusCode}
}
resolver.opts.Logger.Debug("https: reading response")
fullRes, err := io.ReadAll(res.Body)
if err != nil {
return resp, fmt.Errorf("doh: body read: %w", err)
}
err = res.Body.Close()
if err != nil {
return resp, fmt.Errorf("doh: body close: %w", err)
}
resolver.opts.Logger.Debug("https: unpacking response")
resp.DNS = &dns.Msg{}
err = resp.DNS.Unpack(fullRes)
if err != nil {
return resp, fmt.Errorf("doh: dns message unpack: %w", err)
}
return resp, nil
}

View File

@ -1,102 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package resolvers_test
import (
"errors"
"testing"
"dns.froth.zone/awl/pkg/query"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
"gotest.tools/v3/assert"
)
func TestHTTPS(t *testing.T) {
t.Parallel()
//nolint:govet // I could not be assed to refactor this, and it is only for tests
tests := []struct {
name string
opts *util.Options
}{
{
"Good",
&util.Options{
HTTPS: true,
Logger: util.InitLogger(0),
Request: util.Request{
Server: "https://dns9.quad9.net/dns-query",
Type: dns.TypeA,
Name: "git.froth.zone.",
Retries: 3,
},
},
},
{
"404",
&util.Options{
HTTPS: true,
Logger: util.InitLogger(0),
Request: util.Request{
Server: "https://dns9.quad9.net/dns",
Type: dns.TypeA,
Name: "git.froth.zone.",
},
},
},
{
"Bad request domain",
&util.Options{
HTTPS: true,
Logger: util.InitLogger(0),
Request: util.Request{
Server: "dns9.quad9.net/dns-query",
Type: dns.TypeA,
Name: "git.froth.zone",
},
},
},
{
"Bad server domain",
&util.Options{
HTTPS: true,
Logger: util.InitLogger(0),
Request: util.Request{
Server: "dns9..quad9.net/dns-query",
Type: dns.TypeA,
Name: "git.froth.zone.",
},
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
var (
res util.Response
err error
)
for i := 0; i <= test.opts.Request.Retries; i++ {
res, err = query.CreateQuery(test.opts)
if err == nil || errors.Is(err, &util.ErrHTTPStatus{}) {
break
}
}
if err == nil {
assert.NilError(t, err)
assert.Assert(t, res != util.Response{})
} else {
if errors.Is(err, &util.ErrHTTPStatus{}) {
assert.ErrorContains(t, err, "404")
}
assert.Equal(t, res, util.Response{})
}
})
}
}

View File

@ -1,110 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package resolvers
import (
"context"
"crypto/tls"
"fmt"
"io"
"strings"
"time"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
"github.com/quic-go/quic-go"
)
// QUICResolver is for DNS-over-QUIC queries.
type QUICResolver struct {
opts *util.Options
}
var _ Resolver = (*QUICResolver)(nil)
// LookUp performs a DNS query.
func (resolver *QUICResolver) LookUp(msg *dns.Msg) (resp util.Response, err error) {
tls := &tls.Config{
//nolint:gosec // This is intentional if the user requests it
InsecureSkipVerify: resolver.opts.TLSNoVerify,
ServerName: resolver.opts.TLSHost,
MinVersion: tls.VersionTLS12,
NextProtos: []string{"doq"},
}
// Make sure that TLSHost is ALWAYS set
if resolver.opts.TLSHost == "" {
tls.ServerName = strings.Split(resolver.opts.Request.Server, ":")[0]
}
conf := new(quic.Config)
conf.HandshakeIdleTimeout = resolver.opts.Request.Timeout
resolver.opts.Logger.Debug("quic: making query")
ctx, cancel := context.WithTimeout(context.Background(), resolver.opts.Request.Timeout)
defer cancel()
connection, err := quic.DialAddr(ctx, resolver.opts.Request.Server, tls, conf)
if err != nil {
return resp, fmt.Errorf("doq: dial: %w", err)
}
resolver.opts.Logger.Debug("quic: packing query")
// Compress request to over-the-wire
buf, err := msg.Pack()
if err != nil {
return resp, fmt.Errorf("doq: pack: %w", err)
}
t := time.Now()
resolver.opts.Logger.Debug("quic: creating stream")
stream, err := connection.OpenStream()
if err != nil {
return resp, fmt.Errorf("doq: quic stream creation: %w", err)
}
resolver.opts.Logger.Debug("quic: writing to stream")
_, err = stream.Write(buf)
if err != nil {
return resp, fmt.Errorf("doq: quic stream write: %w", err)
}
err = stream.Close()
if err != nil {
return resp, fmt.Errorf("doq: quic stream close: %w", err)
}
resolver.opts.Logger.Debug("quic: reading stream")
fullRes, err := io.ReadAll(stream)
if err != nil {
return resp, fmt.Errorf("doq: quic stream read: %w", err)
}
resp.RTT = time.Since(t)
resolver.opts.Logger.Debug("quic: closing connection")
// Close with error: no error
err = connection.CloseWithError(0, "")
if err != nil {
return resp, fmt.Errorf("doq: quic connection close: %w", err)
}
resolver.opts.Logger.Debug("quic: closing stream")
resp.DNS = &dns.Msg{}
resolver.opts.Logger.Debug("quic: unpacking response")
err = resp.DNS.Unpack(fullRes)
if err != nil {
return resp, fmt.Errorf("doq: unpack: %w", err)
}
return
}

View File

@ -1,125 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build !gccgo
package resolvers_test
import (
"testing"
"time"
"dns.froth.zone/awl/pkg/query"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
"gotest.tools/v3/assert"
)
func TestQuic(t *testing.T) {
t.Parallel()
//nolint:govet // I could not be assed to refactor this, and it is only for tests
tests := []struct {
name string
opts *util.Options
}{
{
"Valid, AdGuard",
&util.Options{
QUIC: true,
Logger: util.InitLogger(0),
Request: util.Request{
Server: "dns.adguard.com",
Type: dns.TypeNS,
Port: 853,
Timeout: 750 * time.Millisecond,
Retries: 3,
},
},
},
{
"Valid, Froth",
&util.Options{
QUIC: true,
Logger: util.InitLogger(0),
Request: util.Request{
Server: "dns.froth.zone",
Type: dns.TypeA,
Name: "git.freecumextremist.com",
Port: 853,
Timeout: 750 * time.Millisecond,
Retries: 3,
},
},
},
{
"Bad domain",
&util.Options{
QUIC: true,
Logger: util.InitLogger(0),
Request: util.Request{
Server: "dns.//./,,adguard\a.com",
Port: 853,
Type: dns.TypeA,
Name: "git.froth.zone",
Timeout: 100 * time.Millisecond,
Retries: 0,
},
},
},
{
"Not canonical",
&util.Options{
QUIC: true,
Logger: util.InitLogger(0),
Request: util.Request{
Server: "dns.adguard.com",
Port: 853,
Type: dns.TypeA,
Name: "git.froth.zone",
Timeout: 100 * time.Millisecond,
Retries: 0,
},
},
},
{
"Invalid query domain",
&util.Options{
QUIC: true,
Logger: util.InitLogger(0),
Request: util.Request{
Server: "example.com",
Port: 853,
Type: dns.TypeA,
Name: "git.froth.zone",
Timeout: 10 * time.Millisecond,
Retries: 0,
},
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
var (
res util.Response
err error
)
for i := 0; i <= test.opts.Request.Retries; i++ {
res, err = query.CreateQuery(test.opts)
if err == nil {
break
}
}
if err == nil {
assert.NilError(t, err)
assert.Assert(t, res != util.Response{})
} else {
assert.Assert(t, res == util.Response{})
}
})
}
}

View File

@ -1,4 +0,0 @@
/*
Package resolvers contain the various DNS resolvers to use.
*/
package resolvers

View File

@ -1,96 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package resolvers
import (
"crypto/tls"
"fmt"
"net"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
)
// StandardResolver is for UDP/TCP resolvers.
type StandardResolver struct {
opts *util.Options
}
var _ Resolver = (*StandardResolver)(nil)
// LookUp performs a DNS query.
func (resolver *StandardResolver) LookUp(msg *dns.Msg) (resp util.Response, err error) {
dnsClient := new(dns.Client)
dnsClient.Dialer = &net.Dialer{
Timeout: resolver.opts.Request.Timeout,
}
if resolver.opts.TCP || resolver.opts.TLS {
dnsClient.Net = tcp
} else {
dnsClient.Net = udp
}
switch {
case resolver.opts.IPv4:
dnsClient.Net += "4"
case resolver.opts.IPv6:
dnsClient.Net += "6"
}
if resolver.opts.TLS {
dnsClient.Net += "-tls"
dnsClient.TLSConfig = &tls.Config{
//nolint:gosec // This is intentional if the user requests it
InsecureSkipVerify: resolver.opts.TLSNoVerify,
ServerName: resolver.opts.TLSHost,
}
}
resolver.opts.Logger.Info("Using", dnsClient.Net, "for making the request")
resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, resolver.opts.Request.Server)
if err != nil {
return resp, fmt.Errorf("standard: DNS exchange: %w", err)
}
switch dns.RcodeToString[resp.DNS.MsgHdr.Rcode] {
case "BADCOOKIE":
if !resolver.opts.BadCookie {
fmt.Printf(";; BADCOOKIE, retrying.\n\n")
msg.Extra = resp.DNS.Extra
resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, resolver.opts.Request.Server)
if err != nil {
return resp, fmt.Errorf("badcookie: DNS exchange: %w", err)
}
}
case "NOERR":
break
}
resolver.opts.Logger.Info("Request successful")
if resp.DNS.MsgHdr.Truncated && !resolver.opts.Truncate {
fmt.Printf(";; Truncated, retrying with TCP\n\n")
dnsClient.Net = tcp
switch {
case resolver.opts.IPv4:
dnsClient.Net += "4"
case resolver.opts.IPv6:
dnsClient.Net += "6"
}
resp.DNS, resp.RTT, err = dnsClient.Exchange(msg, resolver.opts.Request.Server)
}
if err != nil {
return resp, fmt.Errorf("standard: DNS exchange: %w", err)
}
return
}

View File

@ -1,141 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package resolvers_test
import (
"errors"
"os"
"testing"
"time"
"dns.froth.zone/awl/pkg/query"
"dns.froth.zone/awl/pkg/util"
"dns.froth.zone/dnscrypt"
"github.com/miekg/dns"
"gotest.tools/v3/assert"
)
func TestResolve(t *testing.T) {
t.Parallel()
//nolint:govet // I could not be assed to refactor this, and it is only for tests
tests := []struct {
name string
opts *util.Options
}{
{
"UDP",
&util.Options{
Logger: util.InitLogger(0),
Request: util.Request{
Server: "8.8.4.4",
Port: 53,
Type: dns.TypeAAAA,
Name: "example.com.",
Retries: 3,
},
},
},
{
"UDP (Bad Cookie)",
&util.Options{
Logger: util.InitLogger(0),
BadCookie: false,
Request: util.Request{
Server: "b.root-servers.net",
Port: 53,
Type: dns.TypeNS,
Name: "example.com.",
Retries: 3,
},
EDNS: util.EDNS{
EnableEDNS: true,
Cookie: true,
},
},
},
{
"UDP (Truncated)",
&util.Options{
Logger: util.InitLogger(0),
IPv4: true,
Request: util.Request{
Server: "madns.binarystar.systems",
Port: 5301,
Type: dns.TypeTXT,
Name: "limit.txt.example.",
Retries: 3,
},
},
},
{
"TCP",
&util.Options{
Logger: util.InitLogger(0),
TCP: true,
Request: util.Request{
Server: "8.8.4.4",
Port: 53,
Type: dns.TypeA,
Name: "example.com.",
Retries: 3,
},
},
},
{
"TLS",
&util.Options{
Logger: util.InitLogger(0),
TLS: true,
Request: util.Request{
Server: "dns.google",
Port: 853,
Type: dns.TypeAAAA,
Name: "example.com.",
Retries: 3,
},
},
},
{
"Timeout",
&util.Options{
Logger: util.InitLogger(0),
Request: util.Request{
Server: "8.8.4.1",
Port: 1,
Type: dns.TypeA,
Name: "example.com.",
Timeout: time.Millisecond * 100,
Retries: 0,
},
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
var (
res util.Response
err error
)
for i := 0; i <= test.opts.Request.Retries; i++ {
res, err = query.CreateQuery(test.opts)
if err == nil || errors.Is(err, dnscrypt.ErrInvalidDNSStamp) {
break
}
}
if err == nil {
assert.NilError(t, err)
assert.Assert(t, res != util.Response{})
} else {
assert.ErrorIs(t, err, os.ErrDeadlineExceeded)
}
})
}
}

View File

@ -1,81 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package resolvers
import (
"net"
"strconv"
"strings"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
)
const (
tcp = "tcp"
udp = "udp"
)
// Resolver is the main resolver interface.
type Resolver interface {
LookUp(*dns.Msg) (util.Response, error)
}
// LoadResolver loads the respective resolver for performing a DNS query.
func LoadResolver(opts *util.Options) (resolver Resolver, err 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
}
// Make sure that the endpoint is defaulted to /dns-query
if !strings.HasSuffix(opts.Request.Server, opts.HTTPSOptions.Endpoint) {
opts.Request.Server += opts.HTTPSOptions.Endpoint
}
resolver = &HTTPSResolver{
opts: opts,
}
return
case opts.QUIC:
opts.Logger.Info("loading DNS-over-QUIC resolver")
if !strings.HasSuffix(opts.Request.Server, ":"+strconv.Itoa(opts.Request.Port)) {
opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Request.Port))
}
resolver = &QUICResolver{
opts: opts,
}
return
case opts.DNSCrypt:
opts.Logger.Info("loading DNSCrypt resolver")
if !strings.HasPrefix(opts.Request.Server, "sdns://") {
opts.Request.Server = "sdns://" + opts.Request.Server
}
resolver = &DNSCryptResolver{
opts: opts,
}
return
default:
opts.Logger.Info("loading standard/DNS-over-TLS resolver")
if !strings.HasSuffix(opts.Request.Server, ":"+strconv.Itoa(opts.Request.Port)) {
opts.Request.Server = net.JoinHostPort(opts.Request.Server, strconv.Itoa(opts.Request.Port))
}
resolver = &StandardResolver{
opts: opts,
}
return
}
}

View File

@ -1,4 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Package util contains helper functions that don't belong anywhere else
package util

View File

@ -1,19 +0,0 @@
package util
import (
"errors"
"fmt"
)
// ErrHTTPStatus is returned when DoH returns a bad status code.
type ErrHTTPStatus struct {
// Status code
Code int
}
func (e *ErrHTTPStatus) Error() string {
return fmt.Sprintf("doh server responded with HTTP %d", e.Code)
}
// ErrNotError is an error that is not actually an error.
var ErrNotError = errors.New("not an error")

View File

@ -1,14 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package util
import "dns.froth.zone/awl/pkg/logawl"
// InitLogger initializes the logawl instance.
func InitLogger(verbosity int) (log *logawl.Logger) {
log = logawl.New()
log.SetLevel(logawl.Level(verbosity))
return log
}

View File

@ -1,18 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package util_test
import (
"testing"
"dns.froth.zone/awl/pkg/logawl"
"dns.froth.zone/awl/pkg/util"
"gotest.tools/v3/assert"
)
func TestInitLogger(t *testing.T) {
t.Parallel()
logger := util.InitLogger(0)
assert.Equal(t, logger.Level, logawl.Level(0))
}

View File

@ -1,200 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package util
import (
"fmt"
"net"
"dns.froth.zone/awl/pkg/logawl"
"github.com/miekg/dns"
)
// Options is the grand structure for all query options.
type Options struct {
// The logger
Logger *logawl.Logger `json:"-"`
// Host to verify TLS cert with
TLSHost string `json:"tlsHost" example:""`
// EDNS Options
EDNS
// HTTPS options :)
HTTPSOptions
// DNS request :)
Request
// Verbosity levels, see [logawl.AllLevels]
Verbosity int `json:"-" example:"0"`
// Display options
Display Display
// Ignore Truncation
Truncate bool `json:"ignoreTruncate" example:"false"`
// Ignore BADCOOKIE
BadCookie bool `json:"ignoreBadCookie" example:"false"`
// Print only the answer
Short bool `json:"short" example:"false"`
// When Short is true, display where the query came from
Identify bool `json:"identify" example:"false"`
// Perform a reverse DNS query when true
Reverse bool `json:"reverse" example:"false"`
HeaderFlags
// Display resposne as JSON
JSON bool `json:"-" xml:"-" yaml:"-"`
// Display response as XML
XML bool `json:"-" xml:"-" yaml:"-"`
// Display response as YAML
YAML bool `json:"-" xml:"-" yaml:"-"`
// Use TCP instead of UDP to make the query
TCP bool `json:"tcp" example:"false"`
// Use DNS-over-TLS to make the query
TLS bool `json:"dnsOverTLS" example:"false"`
// When using TLS, ignore certificates
TLSNoVerify bool `json:"tlsNoVerify" example:"false"`
// Use DNS-over-HTTPS to make the query
HTTPS bool `json:"dnsOverHTTPS" example:"false"`
// Use DNS-over-QUIC to make the query
//nolint:tagliatelle // QUIC is an acronym
QUIC bool `json:"dnsOverQUIC" example:"false"`
// Use DNSCrypt to make the query
DNSCrypt bool `json:"dnscrypt" example:"false"`
// Force IPv4 only
IPv4 bool `json:"forceIPv4" example:"false"`
// Force IPv6 only
IPv6 bool `json:"forceIPv6" example:"false"`
// Trace from the root
Trace bool `json:"trace" example:"false"`
}
// HTTPSOptions are options exclusively for DNS-over-HTTPS queries.
type HTTPSOptions struct {
// URL endpoint
Endpoint string `json:"endpoint" example:"/dns-query"`
// True, make GET request.
// False, make POST request.
Get bool `json:"get" example:"false"`
}
// HeaderFlags are the flags that are in DNS headers.
type HeaderFlags struct {
// Authoritative Answer DNS query flag
AA bool `json:"authoritative" example:"false"`
// Authenticated Data DNS query flag
AD bool `json:"authenticatedData" example:"false"`
// Checking Disabled DNS query flag
CD bool `json:"checkingDisabled" example:"false"`
// QueRy DNS query flag
QR bool `json:"query" example:"false"`
// Recursion Desired DNS query flag
RD bool `json:"recursionDesired" example:"true"`
// Recursion Available DNS query flag
RA bool `json:"recursionAvailable" example:"false"`
// TrunCated DNS query flag
TC bool `json:"truncated" example:"false"`
// Zero DNS query flag
Z bool `json:"zero" example:"false"`
}
// Display contains toggles for what to (and not to) display.
type Display struct {
/* Section displaying */
// Comments?
Comments bool `json:"comments" example:"true"`
// QUESTION SECTION
Question bool `json:"question" example:"true"`
// OPT PSEUDOSECTION
Opt bool `json:"opt" example:"true"`
// ANSWER SECTION
Answer bool `json:"answer" example:"true"`
// AUTHORITY SECTION
Authority bool `json:"authority" example:"true"`
// ADDITIONAL SECTION
Additional bool `json:"additional" example:"true"`
// Query time, message size, etc.
Statistics bool `json:"statistics" example:"true"`
// Display TTL in response
TTL bool `json:"ttl" example:"true"`
/* Answer formatting */
// Display Class in response
ShowClass bool `json:"showClass" example:"true"`
// Display query before it is sent
ShowQuery bool `json:"showQuery" example:"false"`
// Display TTL as human-readable
HumanTTL bool `json:"humanTTL" example:"false"`
// Translate Punycode back to Unicode
UcodeTranslate bool `json:"unicode" example:"true"`
}
// EDNS contains toggles for various EDNS options.
type EDNS struct {
// Subnet to originate query from.
Subnet dns.EDNS0_SUBNET `json:"subnet"`
// Must Be Zero flag
ZFlag uint16 `json:"zflag" example:"0"`
// UDP buffer size
BufSize uint16 `json:"bufSize" example:"1232"`
// Enable/Disable EDNS entirely
EnableEDNS bool `json:"edns" example:"false"`
// Sending EDNS cookie
Cookie bool `json:"cookie" example:"true"`
// Enabling DNSSEC
DNSSEC bool `json:"dnssec" example:"false"`
// Sending EDNS Expire
Expire bool `json:"expire" example:"false"`
// Sending EDNS TCP keepopen
KeepOpen bool `json:"keepOpen" example:"false"`
// Sending EDNS NSID
Nsid bool `json:"nsid" example:"false"`
// Send EDNS Padding
Padding bool `json:"padding" example:"false"`
// Set EDNS version (default: 0)
Version uint8 `json:"version" example:"0"`
}
// 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
}

View File

@ -1,36 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package util_test
import (
"testing"
"dns.froth.zone/awl/pkg/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",
"Invalid",
}
for _, test := range subnet {
test := test
t.Run(test, func(t *testing.T) {
t.Parallel()
err := util.ParseSubnet(test, new(util.Options))
if err != nil {
assert.ErrorContains(t, err, "invalid CIDR address")
} else {
assert.NilError(t, err)
}
})
}
}

View File

@ -1,35 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package util
import (
"time"
"github.com/miekg/dns"
)
// Response is the DNS response.
type Response struct {
// The full DNS response
DNS *dns.Msg `json:"response"`
// The time it took to make the DNS query
RTT time.Duration `json:"rtt" example:"2000000000"`
}
// Request is a structure for a DNS query.
type Request struct {
// Server to query
Server string `json:"server" example:"1.0.0.1"`
// Domain to query
Name string `json:"name" example:"example.com"`
// Duration to wait until marking request as failed
Timeout time.Duration `json:"timeout" example:"2000000000"`
// Port to make DNS request on
Port int `json:"port" example:"53"`
// Number of failures to make before giving up
Retries int `json:"retries" example:"2"`
// Request type, eg. A, AAAA, NAPTR
Type uint16 `json:"type" example:"1"`
// Request class, eg. IN
Class uint16 `json:"class" example:"1"`
}

View File

@ -1,57 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package util
import (
"fmt"
"strings"
"github.com/miekg/dns"
)
type errReverseDNS struct {
addr string
}
func (errDNS *errReverseDNS) Error() string {
return fmt.Sprintf("reverseDNS: invalid value %s given", errDNS.addr)
}
// 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("PTR reverse: %w", err)
}
return str, nil
} else if query == "NAPTR" {
// get rid of characters not needed
replacer := strings.NewReplacer("+", "", " ", "", "-", "")
address = replacer.Replace(address)
// reverse the order of the string
address = reverse(address)
var arpa strings.Builder
// Make it canonical
for _, c := range address {
fmt.Fprintf(&arpa, "%c.", c)
}
arpa.WriteString("e164.arpa.")
return arpa.String(), nil
}
return "", &errReverseDNS{address}
}
// Reverse a string, return the string in reverse.
func reverse(s string) string {
rns := []rune(s)
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

@ -1,79 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package util_test
import (
"testing"
"dns.froth.zone/awl/pkg/util"
"github.com/miekg/dns"
"gotest.tools/v3/assert"
)
func TestPTR(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in string
expected string
}{
{
"IPv4",
"8.8.4.4", "4.4.8.8.in-addr.arpa.",
},
{
"IPv6",
"2606:4700:4700::1111", "1.1.1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.7.4.0.0.7.4.6.0.6.2.ip6.arpa.",
},
{
"Inavlid value",
"AAAAA", "",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
act, err := util.ReverseDNS(test.in, dns.StringToType["PTR"])
if err == nil {
assert.NilError(t, err)
} else {
assert.ErrorContains(t, err, "unrecognized address")
}
assert.Equal(t, act, test.expected)
})
}
}
func TestNAPTR(t *testing.T) {
t.Parallel()
tests := []struct {
in string
want string
}{
{"1-800-555-1234", "4.3.2.1.5.5.5.0.0.8.1.e164.arpa."},
{"+1 800555 1234", "4.3.2.1.5.5.5.0.0.8.1.e164.arpa."},
{"+46766861004", "4.0.0.1.6.8.6.6.7.6.4.e164.arpa."},
{"17705551212", "2.1.2.1.5.5.5.0.7.7.1.e164.arpa."},
}
for _, test := range tests {
// Thanks Goroutines, very cool!
test := test
t.Run(test.in, func(t *testing.T) {
t.Parallel()
act, err := util.ReverseDNS(test.in, dns.StringToType["NAPTR"])
assert.NilError(t, err)
assert.Equal(t, test.want, act)
})
}
}
func TestInvalidAll(t *testing.T) {
_, err := util.ReverseDNS("q", 15236)
assert.ErrorContains(t, err, "invalid value")
}

View File

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

View File

@ -1,98 +0,0 @@
# SPDX-License-Identifier: BSD-3-Clause
# Template for the BSD/GNU makefiles
HASH ?= `git describe --tags --always --dirty --broken | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || echo "UNKNOWN"`
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 -race -cover
COVER ?= $(GO) tool cover
GOFLAGS ?= -trimpath -ldflags="-s -w -X=main.version=$(HASH)"
DESTDIR :=
PREFIX ?= /usr/local
BIN ?= bin
SHARE ?= share
SCDOC ?= scdoc
MAN ?= $(PREFIX)/$(SHARE)/man
PROG ?= awl
# hehe
all: $(PROG) docs/$(PROG).1
$(PROG): $(SOURCES)
$(GO) build -o $(EXE) $(GOFLAGS) .
docs/$(PROG).1: docs/$(PROG).1.scd
$(SCDOC) <$? >$@
docs/wiki/$(PROG).1.md: docs/$(PROG).1
pandoc --from man --to gfm -o $@ $?
## update_doc: update documentation (requires pandoc)
update_doc: docs/wiki/$(PROG).1.md
.PHONY: fmt
fmt:
gofmt -w -s .
.PHONY: vet
vet:
$(GO) vet ./...
## lint: lint awl, using fmt, vet and golangci-lint
.PHONY: lint
lint: fmt vet
golangci-lint run --fix
coverage/coverage.out: $(TEST_SOURCES)
$(TEST) -coverprofile=$@ ./...
.PHONY: test
## test: run go test
test: coverage/coverage.out
.PHONY: test-ci
test-ci:
$(TEST) ./...
## fuzz: runs fuzz tests
fuzz: $(TEST_SOURCES)
$(TEST) -fuzz=FuzzFlags -fuzztime 10000x ./cmd
$(TEST) -fuzz=FuzzDig -fuzztime 10000x ./cmd
$(TEST) -fuzz=FuzzParseArgs -fuzztime 10000x ./cmd
fuzz-ci: $(TEST_SOURCES)
$(TEST) -fuzz=FuzzFlags -fuzztime 1000x ./cmd
$(TEST) -fuzz=FuzzDig -fuzztime 1000x ./cmd
$(TEST) -fuzz=FuzzParseArgs -fuzztime 1000x ./cmd
.PHONY: full_test
full_test: test fuzz
coverage/cover.html: coverage/coverage.out
$(COVER) -func=$?
$(COVER) -html=$? -o $@
## cover: generates test coverage, output as HTML
cover: coverage/cover.html
## clean: clean the build files
.PHONY: clean
clean:
$(GO) clean
# Ignore errors if you remove something that doesn't exist
rm -f docs/$(PROG).1
rm -f coverage/cover*
rm -rf vendor
## help: Prints this help message
.PHONY: help
help:
@echo "Usage: "
@sed -n 's/^##//p' $(MAKEFILE_LIST) | column -t -s ':' | sed -e 's/^/ /'