dnswizard

command module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: AGPL-3.0 Imports: 2 Imported by: 0

README

dnswizard

     _                        _                        _
  __| |_ __  _____      _____(_)______ _ _ __ __ _  __| |
 / _` | '_ \/ __\ \ /\ / /_  / |_  / _` | '__/ _` |/ _` |
| (_| | | | \__ \\ V  V / / /| |/ / (_| | | | (_| | (_| |
 \__,_|_| |_|___/ \_/\_/ /___|_/___\__,_|_|  \__,_|\__,_|

A configurable DNS proxy for lab and development work. Point a name at your laptop without editing /etc/hosts on every machine, and watch what an application actually resolves.

Single static binary, no runtime to install. Runs on Linux, macOS, Windows and BSD.

Install

go install github.com/joda32/dnswizard@latest

Or from a checkout:

make build          # ./dnswizard
make build-all      # binaries for linux/darwin/windows on amd64 and arm64
make install        # into $(go env GOPATH)/bin

Quick start

Run it as a plain forwarding proxy that logs everything — the fastest way to find out what an application is looking up:

sudo dnswizard serve

Point a wildcard domain at your machine, no config file needed:

sudo dnswizard serve -r '*.dev.local=127.0.0.1'
22:52:00 INF  listening proto=udp addr=127.0.0.1:53
22:52:00 INF  listening proto=tcp addr=127.0.0.1:53
22:52:01 INF  cooked  client=127.0.0.1 name=api.dev.local. type=A answer=127.0.0.1
22:52:01 INF  proxied client=127.0.0.1 name=example.com. type=A upstream=1.1.1.1:53 rcode=NOERROR answers=2 rtt=6.1ms

Then tell the machine to use it — add nameserver 127.0.0.1 at the top of /etc/resolv.conf, or set it in your network settings.

No root? Bind an unprivileged port and query it explicitly:

dnswizard serve -l 127.0.0.1:5353 -r '*.dev.local=127.0.0.1'
dnswizard query api.dev.local -s 127.0.0.1:5353

Configuration

dnswizard config init          # writes a commented dnswizard.yaml
dnswizard serve                # picks up ./dnswizard.yaml automatically
listen:
  - 127.0.0.1:53               # bare address binds both UDP and TCP

upstream:                      # tried in order, with failover
  - 1.1.1.1:53
  - 8.8.8.8:53

ttl: 60
fallback: proxy                # proxy | nxdomain | refused | empty

hosts:                         # shorthand: A or AAAA picked automatically
  "*.dev.local": 127.0.0.1
  "api.dev.local":
    - 10.0.0.10
    - 10.0.0.11

records:                       # everything else
  - name: dev.local
    type: MX
    value: mail.dev.local      # preference defaults to 10
  - name: _sip._tcp.dev.local
    type: SRV
    value: "0 5 5060 sip.dev.local"
  - name: dev.local
    type: TXT
    value: "v=spf1 -all"
    ttl: 300

The file is re-read when it changes, so editing records takes effect without a restart. A config that fails to parse is logged and ignored — the running record set stays in place.

Command-line flags override the file. dnswizard config check validates a file and prints the records it would serve.

Record types

A AAAA CNAME MX NS PTR TXT SOA SRV NAPTR CAA DNSKEY RRSIG — and anything else miekg/dns can parse (HTTPS, SVCB, TLSA, …) if you write the value in zone-file syntax.

Values use a friendly shorthand where a zone file would be fussy: bare hostnames for MX, unquoted TXT strings (split into 255-byte chunks automatically), unquoted NAPTR fields, and target names without a trailing dot.

Name matching

Patterns are matched most-specific-first:

Pattern Matches Does not match
api.dev.local api.dev.local v2.api.dev.local
*.dev.local api.dev.local, v2.api.dev.local dev.local
a.*.dev.local a.b.dev.local a.b.c.dev.local
* everything

A leading * spans one or more labels; a * in any other position matches exactly one. An exact name beats a wildcard, and an explicit CNAME beats a broader wildcard address record at the same name.

Choosing what gets faked
# Fake only these names, resolve everything else for real
dnswizard serve -r '*=127.0.0.1' --only dev.local,*.dev.local

# Fake everything except these
dnswizard serve -r '*=127.0.0.1' --except github.com,*.docker.io

Commands

Command Purpose
dnswizard serve run the server
dnswizard query <name> [type] send one query, a small stand-in for dig
dnswizard config init write a starter config
dnswizard config check validate a config and list its records
dnswizard config import <file.ini> convert a legacy INI record file

Run dnswizard serve --help for the full flag list.

Migrating an older setup

An INI record file — a section per record type, one domain=value pair per line — converts directly:

dnswizard config import records.ini -o dnswizard.yaml

Flag equivalents, if you are coming from a Python-era invocation:

Old flag dnswizard
--fakeip 127.0.0.1 -r '*=127.0.0.1'
--fakeip X --fakedomains a.com -r 'a.com=X' -r '*.a.com=X'
--fakeipv6 ::1 -r 'AAAA:*=::1'
--fakemail mail.x.com -r 'MX:*=mail.x.com'
--fakealias www.x.com -r 'CNAME:*=www.x.com'
--fakens ns.x.com -r 'NS:*=ns.x.com'
--file records.ini -c dnswizard.yaml (after config import)
--fakedomains a.com --only a.com
--truedomains a.com --except a.com
--nameservers 4.2.2.1#53#tcp -u tcp://4.2.2.1:53 (old syntax still parses)
-i 0.0.0.0 -p 5353 -l 0.0.0.0:5353
-t / --tcp not needed — UDP and TCP are both served
-6 / --ipv6 not needed — -l '[::1]:53'
--logfile FILE --log-file FILE
-q -q (hides the banner)
Behaviour that changed on purpose
  • Wildcards are stricter. The old matcher compared only as many labels as the shorter of pattern and query, so example.com silently matched every subdomain and *.example.com matched the bare apex. Here those are two distinct patterns. Add both if you want both.
  • UDP and TCP run together, rather than one or the other.
  • Upstreams fail over. Picking one at random with no retry meant a dead server caused intermittent failures. Truncated UDP replies are also retried over TCP automatically.
  • Locally known names do not leak. If a name has records of some type but not the queried one, dnswizard answers NODATA instead of forwarding an internal name to a public resolver. Set nodata_for_known_names: false for the old behaviour.
  • CNAMEs are followed. An A/AAAA query for a name with a local CNAME returns the alias plus the target's addresses, resolving the target upstream if it is not local. Set chase_cname: false to disable.
  • New upstream transports. tcp:// and DNS-over-TLS via tls://1.1.1.1:853#cloudflare-dns.com.
  • fallback. Unmatched queries can answer NXDOMAIN, REFUSED or empty instead of being proxied — useful for an offline lab.

Development

make test        # go test ./...
make lint        # go vet + gofmt check
make build-all   # cross-compile

Planned work is in BACKLOG.md.

Licence

Copyright (C) 2026 Willem Mouton. Released under the GNU Affero General Public License v3.0 — see LICENSE.

In short: use it for anything, including paid client work and internal company use, at no cost and without asking. If you want to build it into a product you sell, or offer it as a hosted service, without releasing that work's source under the AGPL, you need a separate commercial licence — see COMMERCIAL.md. I grant those on reasonable terms, and for free to non-profits, education, and open source projects that cannot use the AGPL.

dnswizard was inspired by the Python-era DNS proxies that came before it, whose authors worked out much of what a tool like this should do. This is an independent implementation and shares no code with them.

Documentation

Overview

Command dnswizard is a configurable DNS proxy for lab and development work.

Directories

Path Synopsis
Package cmd implements the dnswizard command-line interface.
Package cmd implements the dnswizard command-line interface.
internal
config
Package config loads and validates dnswizard's YAML configuration and turns it into the runtime objects the server needs.
Package config loads and validates dnswizard's YAML configuration and turns it into the runtime objects the server needs.
logging
Package logging configures the slog logger used across dnswizard.
Package logging configures the slog logger used across dnswizard.
records
Package records holds the fake-record store: parsing record definitions into wire-format RRs and matching query names against wildcard patterns.
Package records holds the fake-record store: parsing record definitions into wire-format RRs and matching query names against wildcard patterns.
server
Package server implements the DNS listeners and the query handler.
Package server implements the DNS listeners and the query handler.
upstream
Package upstream forwards queries that dnswizard does not answer itself.
Package upstream forwards queries that dnswizard does not answer itself.
watcher
Package watcher reloads the config file when it changes on disk.
Package watcher reloads the config file when it changes on disk.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL