Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ There are several options how to install `dnsproxy`.
1. Grab the binary for your device/OS from the [Releases][releases] page.
2. Use the [official Docker image][docker].
3. Build it yourself (see the instruction below).
4. Use the included [systemd service file][service] for running as a system
service on Linux.

[releases]: https://github.com/AdguardTeam/dnsproxy/releases
[docker]: https://hub.docker.com/r/adguard/dnsproxy
[service]: dnsproxy.service

## How to build

Expand Down
133 changes: 133 additions & 0 deletions dnsproxy.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# /etc/systemd/system/dnsproxy.service
#
# Hardened systemd unit for adguardteam/dnsproxy
#
# All upstreams use encrypted protocols (DoH, DoT).
# IP-based upstream URLs avoid bootstrap DNS entirely — no plaintext
# DNS query ever leaves this machine.
#
# Upstream strategy:
# Primary (fastest_addr): Cloudflare, Google via DoH (IP-based)
# HTTP/3: Enabled globally for DoH (benefits CF & Google)
# Fallback: DoT endpoints for all providers
#
# Prerequisites:
# 1. Download the dnsproxy binary to /usr/local/bin/dnsproxy
# 2. chmod 755 /usr/local/bin/dnsproxy
Comment on lines +15 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding installation instructions for package managers (apt, yum, etc.) in addition to the manual binary download method. This would make the service more accessible to users on different distributions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not applicable

# 3. systemctl daemon-reload
# 4. systemctl enable --now dnsproxy.service
#
# Verify no plaintext DNS leaks:
# sudo ss -ulnp sport = 53 # should show only 127.0.0.1
# sudo tcpdump -i any port 53 # should show zero outbound traffic on port 53

[Unit]
Description=dnsproxy — encrypted DNS forwarder (DoH/DoT/DoQ)
Documentation=https://github.com/AdguardTeam/dnsproxy
After=network-online.target nss-lookup.target
Wants=network-online.target
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this service binds to port 53, consider adding Conflicts=systemd-resolved.service dnsmasq.service named.service in the [Unit] section to prevent conflicts with other DNS services that might be running on the system.

Conflicts=systemd-resolved.service dnsmasq.service named.service

[Service]
Type=simple

# Primary upstreams: DoH via IP addresses (zero bootstrap DNS needed)
# Cloudflare — https://1.1.1.1 / 1.0.0.1
# Google — https://8.8.8.8 / 8.8.4.4
# Fallback (DoT): all providers, used only if DoH is down
# --http3 enables HTTP/3 (QUIC transport) for DoH connections
ExecStart=/usr/local/bin/dnsproxy \
--listen=127.0.0.1 \
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service is configured to listen only on localhost (127.0.0.1). If users want to use this as a DNS server for other devices on their network, they would need to modify the --listen parameter. Consider documenting this limitation or providing an alternative configuration example for network-wide usage.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not rocket science to update the listen address :)

--port=53 \
--http3 \
--upstream=https://1.1.1.1/dns-query \
--upstream=https://1.0.0.1/dns-query \
--upstream=https://8.8.8.8/dns-query \
--upstream=https://8.8.4.4/dns-query \
--fallback=tls://1.1.1.1:853 \
--fallback=tls://1.0.0.1:853 \
--fallback=tls://8.8.8.8:853 \
--fallback=tls://8.8.4.4:853 \
--cache \
--cache-optimistic \
--cache-size=4194304 \
--cache-min-ttl=60 \
--cache-max-ttl=86400 \
--upstream-mode=fastest_addr \
--pending-requests-enabled \
--refuse-any \
--timeout=10s \
--max-go-routines=1000

# ── Identity ──────────────────────────────────────────────────────────
# DynamicUser creates a transient uid/gid with no login shell, no
# home directory, and no persistence beyond the service lifetime.
DynamicUser=yes

# ── Capabilities ──────────────────────────────────────────────────────
# Port 53 requires CAP_NET_BIND_SERVICE. Grant only that, nothing else.
# PrivateUsers=yes is intentionally omitted — it creates a user
# namespace that strips real capabilities, breaking port 53 binding.
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes

# ── Filesystem isolation ──────────────────────────────────────────────
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ReadWritePaths=

# StateDirectory creates /var/lib/private/dnsproxy owned by the
# dynamic user. Currently unused but ready for --output if needed.
StateDirectory=dnsproxy
LogsDirectory=dnsproxy
Comment on lines +84 to +85
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service creates StateDirectory and LogsDirectory but doesn't specify directory permissions. Consider adding StateDirectoryMode=0750 and LogsDirectoryMode=0750 to ensure proper permissions are set.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service creates log directories (LogsDirectory=dnsproxy), but there's no documentation on how to access these logs or what logging options are available. Consider adding information about log locations and how to adjust verbosity if needed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a doc comment right above it

StateDirectoryMode=0750
LogsDirectoryMode=0750

# ── Kernel isolation ─────────────────────────────────────────────────
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectClock=yes
ProtectProc=invisible
ProcSubset=pid

# ── Network isolation ────────────────────────────────────────────────
# AF_INET/AF_INET6 — DNS listening + encrypted upstream connections
# AF_UNIX — journald logging socket
# AF_NETLINK — Go runtime enumerates network interfaces at start
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK

# IPAddressDeny/Allow are intentionally omitted. They use BPF cgroup
# filters that would block dnsproxy's own outbound connections to
# upstream resolvers (1.1.1.1, 8.8.8.8, 9.9.9.9, etc).

# ── Syscall filtering ───────────────────────────────────────────────
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources

# ── Misc hardening ──────────────────────────────────────────────────
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
UMask=0077

# ── Resource limits ──────────────────────────────────────────────────
LimitNOFILE=65535
LimitNPROC=512

# ── Restart policy ───────────────────────────────────────────────────
# "No disruption": restart immediately on failure, with burst protection.
Restart=always
RestartSec=2
StartLimitBurst=5
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service file defines StartLimitBurst=5 but is missing the required StartLimitIntervalSec parameter. Without specifying the time interval, the burst protection may not work as expected. Consider adding something like StartLimitIntervalSec=60 to define the time window for the burst limit.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemd in my host complained that StartLimitIntervalSec is unknown and will be ignored. I initially had it, but then got it removed.


[Install]
WantedBy=multi-user.target