Skip to main content

dnser

A simple tool to configure DNS on Linux, detects whether NetworkManager or systemd-resolved controls your DNS, applies the change through its native mechanism, and undoes it with one command.

$ sudo dnser set quad9 --dot --no-llmnr
OK applied Quad9 (9.9.9.9#dns.quad9.net, ...) to system-wide global override
hardening: DoT=on, LLMNR=off
backup: 20260816T142201Z_baseline.json

Why

Most Linux DNS tools assume a single backend, or only touch the active connection, so every new Wi-Fi network silently resets you.dnser detects the real DNS layer, warns about conflicting configuration elsewhere on the system, and supports a global override that persists across networks.

Design principle: it does one thing. No daemon, no TUI, no auto-apply. Every change is explicit, inspectable with --dry-run, and reversible.

Features

  • Backend-agnostic — NetworkManager (nmcli) and systemd-resolved (resolvectl + drop-ins), auto-detected
  • Persistent global scope — survives network changes, not just the current connection
  • DNS-over-TLS, fail-closed and certificate-verified (systemd-resolved), with a pre-flight check that port 853 is actually reachable before applying
  • Fallback DNS — a secondary resolver consulted only if the primary fails, set explicitly so resolved never silently falls back to its built-in servers
  • Built-in benchmarkingdnser check measures cached and uncached latency for every provider, using only the Python standard library
  • Protocol hardening — disable LLMNR and mDNS, require DNSSEC, disable the local resolver cache
  • Snapshot before every changednser restore reverts cleanly, and dnser unset removes dnser entirely even without a backup
  • --dry-run — see the exact file contents and commands before anything runs
  • Conflict detection — warns about other drop-ins that may override your DNS
  • 15 provider presets (Quad9, Mullvad, AdGuard, Cloudflare, OpenDNS, Google), fully overridable via JSON

Requirements

  • Linux with NetworkManager or systemd-resolved
  • Python 3.10+
  • root for anything that changes system state

Install

From PyPI

pipx install dnser

From source

git clone https://github.com/capitan0n/dnser
cd dnser
pipx install --editable .        # or: pip install -e ".[dev]"

pipx installs into your user PATH, but sudo uses root's PATH. To make sudo dnser work:

sudo ln -s "$(which dnser)" /usr/local/bin/dnser

Usage

dnser status                 # active backend, current DNS, protocol state
dnser list                   # available provider presets
dnser check                  # benchmark all providers (cached + uncached)
dnser backends               # which backends are usable on this system

sudo dnser set quad9         # switch to Quad9
sudo dnser set quad9 --dot   # ...over DNS-over-TLS
sudo dnser set quad9 --fallback cloudflare   # ...with a backup resolver
dnser set quad9 --dry-run    # show what would happen, change nothing

sudo dnser unset             # remove dnser's config, back to system defaults
sudo dnser restore           # undo the last change
dnser restore --list         # show all backups
sudo dnser restore --index 2 # restore a specific backup

Scopes

Flag Changes Persists to new networks?
(default) Current connection only No
--all Every saved connection No
--global System-wide override Yes

systemd-resolved has no NetworkManager-style connection profiles — DNS is configured per-link or globally — so all three scopes behave identically there. dnser says so explicitly when it upgrades your scope rather than reporting something that didn't happen.

Flags for set

Flag Effect Backend
--dot DNS-over-TLS, fail-closed, cert-verified resolved only
--dnssec Require DNSSEC validation resolved only
--no-cache Disable the local resolver cache resolved only
--no-llmnr Disable Link-Local Multicast Name Resolution both
--no-mdns Disable Multicast DNS (.local) both
--no-ipv6 Skip the provider's IPv6 servers both
--fallback Secondary resolver, used only if primary fails both
--iface IF Target a specific interface (default scope) NetworkManager
--dry-run Print the plan and exit both

Flags are individual on purpose — there is no --harden mega-flag. You should know exactly what you changed. Resolved-only flags fail loudly on NetworkManager instead of silently doing less than you asked.

Fallback DNS

--fallback takes a comma-separated list of provider keys and/or raw IPs, used only when every primary server fails:

sudo dnser set quad9 --fallback cloudflare
sudo dnser set quad9 --fallback 9.9.9.10,1.1.1.1
sudo dnser set mullvad --dot --fallback quad9   # fallback is DoT too
  • On systemd-resolved this writes a native FallbackDNS= line. dnser always sets it explicitly, so resolved never silently falls back to its compiled-in servers (Google/Cloudflare) — a surprising leak otherwise.
  • On NetworkManager, which has no separate fallback notion, the servers are appended after the primaries in the same ordered list.
  • Under --dot, a fallback given as a provider key stays certificate-verified; a bare IP is refused, since it can't be verified. Mixing a DoT primary with a plaintext fallback prints a leak warning.
  • A fallback that shares servers with the primary is refused — it would never be consulted, so it's almost certainly a mistake.

How it works

  • systemd-resolved — writes /etc/systemd/resolved.conf.d/00-dnser.conf, then restarts the service
  • NetworkManager — sets ipv4.dns/ipv6.dns on connection profiles (addressed by UUID, since profile names are neither unique nor unambiguous), or writes /etc/NetworkManager/conf.d/00-dnser-global.conf for --global

Files under /etc are written atomically, so an interrupted run can never leave a truncated DNS config behind. Every set and unset snapshots the previous state first, and dnser status scans for other drop-ins that could influence resolution independently of dnser.

Because systemd-resolved's DoT is fail-closed, --dot first opens a quick TCP connection to port 853 as a reachability check: if a local firewall or the ISP blocks it, dnser warns before applying

dnser check sends raw DNS queries over UDP/53: one for a well-known cached name, then randomized subdomains to force cache misses. Sockets are connected before sending so replies from other hosts can't skew the numbers. Providers that only answer over DoT (Mullvad) are skipped rather than reported as broken.

Scope and limitations

dnser manages DNS on desktop and laptop Linux systems that use NetworkManager, systemd-resolved, or both. It does not cover:

  • Server / headless setups without NM or resolved — if your DNS comes from hand-edited /etc/resolv.conf, resolvd, openresolv, Unbound, dnsmasq as a local forwarder, or a container's built-in resolver, dnser has no backend for it and will say so.
  • Immutable-root distros (Fedora Silverblue, NixOS, GNOME OS) — the /etc overlays or read-only rootfs may block dnser's config writes even as root.
  • systemd-networkd without resolved — networkd manages links but not resolution; dnser needs resolved on the other end.
  • Non-Linux — see below.
  • VPN split-tunnel DNS — dnser sets system-wide or per-connection DNS but does not touch VPN-specific routing. If your VPN client pushes its own DNS, that configuration sits in a different layer and may override or be overridden by dnser depending on the order of operations.

Configuration

Provider presets load from the first file found:

  1. ~/.config/dnser/providers.json
  2. /etc/dnser/providers.json
  3. The copy bundled with the package

Backups live in ~/.local/state/dnser/backups/ (last 10 kept), honouring XDG_STATE_HOME. Under sudo they are written to the invoking user's home, not /root, so dnser restore works without root when it doesn't need it.

Contributing

Issues and pull requests are welcome. Please keep changes minimal and in the spirit of the tool: one obvious behaviour per flag, no hidden magic, and no new runtime dependencies without a strong reason.

pip install -e ".[dev]"
ruff check .
pytest

License

Released under the GNU General Public License v3.0 or later. See LICENSE for the full text.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dnser-0.5.0.tar.gz (64.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dnser-0.5.0-py3-none-any.whl (56.0 kB view details)

Uploaded Python 3

File details

Details for the file dnser-0.5.0.tar.gz.

File metadata

  • Download URL: dnser-0.5.0.tar.gz
  • Upload date:
  • Size: 64.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for dnser-0.5.0.tar.gz
Algorithm Hash digest
SHA256 4db0ec2eed5e81c57e326e9e8bf643d4230bef16273fbc02cc9419eef525e440
MD5 f5d394c03b601d6e4317afbab22012a0
BLAKE2b-256 16e9a4a0617e9c30114769f2f99feab5eedf4fcafeeed1b8ff72ca6269dd3039

See more details on using hashes here.

File details

Details for the file dnser-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: dnser-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 56.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for dnser-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e36cc0772e7d0e1ba7a6656ebc1520b6bff1860efb61ae9623beea31d4152ea5
MD5 1f52c0b8a24cb3ac9f273ee86ca38120
BLAKE2b-256 c99c264204a17be898b026ca47932f86e8a0780023c2aa83816215d055f1b820

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page