Host tuning
Four limits a busy network can reach before Warden itself runs out of room: how to check each one and how to raise it.
What this is
Warden’s defaults suit a home network. On a larger one, such as an office or a school with hundreds of devices, four limits can start to lose queries or data without an obvious error. Two are settings of the Linux host, two are settings of Warden:
| Limit | What you notice | Where you change it |
|---|---|---|
| Connection tracking of a stateful firewall | Lookups slow down or fail at busy times, and the upstream looks slow or broken | Linux: net.netfilter.nf_conntrack_max, or your nftables rules |
| UDP receive buffer | Part of a burst of queries gets no answer, and those clients retry | Linux: net.core.rmem_max |
| Device table | Per-device statistics are incomplete, and some counters start again from zero | Warden: [tracking] max_devices |
| Query log size cap | The query log keeps only the end of a busy day | Warden: [tracking] query_log_max_size_mb and query_log_max_files |
Run the checks once when you set Warden up for a larger network, and again when the network grows.
Hosts with a stateful firewall
A firewall that tracks connections keeps an entry for every DNS query for about 30 seconds. When its table is full, the kernel drops new packets.
A stateful firewall remembers every connection in the kernel’s connection-tracking table (conntrack). firewalld, which Fedora and RHEL turn on by default, works this way, and so does ufw once you enable it. DNS over UDP has no connections, so the kernel counts every query that comes from a new source port as a new flow. That means:
- every query a client sends to Warden, and
- with the default settings, every query Warden sends to a plain DNS upstream
(
[upstream] mode = "plain", port 53) when the answer is not in its cache.
An answered flow stays in the table for net.netfilter.nf_conntrack_udp_timeout
seconds, 30 on current kernels. So the table can take in roughly this many new
flows per second:
nf_conntrack_max / nf_conntrack_udp_timeout
Every other connection the host tracks shares that budget. The kernel sizes
nf_conntrack_max from the host’s memory, so a small machine gets a small
table. When the table is full, the kernel drops new packets and logs
nf_conntrack: table full, dropping packet. To Warden this looks like a slow
or broken upstream, and clients see lookups that time out.
Check your host
sudo sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max net.netfilter.nf_conntrack_udp_timeout
sudo dmesg | grep 'table full'- If
sysctlanswerscannot stat /proc/sys/net/netfilter/nf_conntrack_count, connection tracking is not loaded on this host. Nothing in this section applies. - If
nf_conntrack_countcomes close tonf_conntrack_maxat busy times, ordmesgprintstable fulllines, the table is too small.
To size the table, count up to two entries per query (one for the client’s query, one for the query to the upstream) and multiply by the timeout. For example, a network with peaks of 1,000 queries per second needs up to 1,000 × 2 × 30 = 60,000 entries for DNS alone, on top of everything else the host tracks.
Fix 1: a larger table
This works on every host, including those that run firewalld or ufw. An entry takes a few hundred bytes of kernel memory, and only while it is in use. 262,144 is an example: use your own figure from the sizing above, with room to spare.
# Apply it now
sudo sysctl -w net.netfilter.nf_conntrack_max=262144
# Keep it after a reboot
echo 'net.netfilter.nf_conntrack_max = 262144' | sudo tee /etc/sysctl.d/90-purge-warden-conntrack.conf
echo 'nf_conntrack' | sudo tee /etc/modules-load.d/nf_conntrack.confThe second file loads the connection-tracking module early at boot, before
systemd applies the files in /etc/sysctl.d/. Without it, the setting can be
applied before the firewall loads the module, and the kernel keeps its own
smaller default.
Fix 2: no tracking for DNS
This fix is for hosts where you write the nftables rules yourself, without
firewalld or ufw. The rules below mark Warden’s DNS packets as untracked, so they
take no entry in the table at all: the queries from your clients and Warden’s
answers, and Warden’s queries to its upstream servers and their answers. The
host’s other DNS traffic, such as its own lookups for package updates, stays
tracked. Untracked packets no longer match ct state established,related, so
the input chain needs two rules that accept them: queries from your clients,
and answers from your upstream servers.
define dns_upstreams = { 192.0.2.53, 192.0.2.54 }
table inet dns_notrack {
chain prerouting {
type filter hook prerouting priority raw; policy accept;
fib daddr type local meta l4proto { tcp, udp } th dport 53 notrack
fib daddr type local ip saddr $dns_upstreams meta l4proto { tcp, udp } th sport 53 notrack
}
chain output {
type filter hook output priority raw; policy accept;
meta l4proto { tcp, udp } th sport 53 notrack
ip daddr $dns_upstreams meta l4proto { tcp, udp } th dport 53 notrack
}
}
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif lo accept
# DNS queries from your clients only
ip saddr 192.168.1.0/24 meta l4proto { tcp, udp } th dport 53 accept
# DNS answers from your upstream servers only
ip saddr $dns_upstreams meta l4proto { tcp, udp } th sport 53 th dport 1024-65535 ct state untracked accept
# ... your other rules
}
}- Replace
192.168.1.0/24with your client networks. - In
dns_upstreams, list every server Warden forwards to: theserversof[upstream]and of[upstream.fallback], and the servers of any[[forwarding]]rule. Do not add the servers that the host itself uses (resolvectl dns, or thenameserverlines of/etc/resolv.conf) unless Warden forwards to them too: their traffic stays tracked and needs no rule. - Warden sends its queries from ports above 1023, and so do the usual system
resolvers, so
th dport 1024-65535keeps the lower ports closed to packets that claim to come from an upstream. - For IPv6 clients or upstreams, define a second set with the IPv6 addresses
and add the same rules with
ip6 saddrandip6 daddr. - If your output chain also has
policy drop, add two rules to it:meta l4proto { tcp, udp } th sport 53 acceptfor Warden’s answers, andip daddr $dns_upstreams meta l4proto { tcp, udp } th dport 53 acceptfor its queries to the upstream servers. Untracked packets do not matchct state established,relatedthere either, so without these rules your clients get no answers and Warden cannot reach its upstream servers. fib daddr type locallimits the prerouting rules to packets addressed to this host, so DNS traffic that the host routes for other machines keeps its tracking. If this host also translates addresses (NAT) for machines that query the same upstream servers directly, use Fix 1: their answers need tracking to find their way back.
Check the file, load it, check that the host itself still resolves names, and watch the counter:
sudo nft -c -f /etc/nftables.conf # check only, changes nothing
sudo nft -f /etc/nftables.conf
getent hosts purge.cc # must print an address
sudo sysctl net.netfilter.nf_conntrack_countIf getent prints nothing, the host can no longer resolve names. Run
sudo nft delete table inet dns_notrack: this removes the notrack rules at
once, and tracking works as before. The two accept rules in the input chain do
no harm without them. Do the same if your clients stop getting answers. Then
compare your file with the excerpt and fix it: the table comes back the next
time the file is loaded, for example at boot.
With the rules in place, nf_conntrack_count no longer climbs with Warden’s
DNS traffic.
th sport 53 from any
address: anyone could then reach every port on this host by sending from port
53. When [server] allow_from is set, Warden also refuses queries from other
sources, but the firewall is the first line.UDP receive buffer
Warden asks the kernel for a 2 MiB buffer on its DNS socket. A host setting can cap it, and Warden’s start line says so.
Queries that arrive faster than Warden reads them wait in the receive buffer of its UDP socket. When the buffer is full, the kernel drops the next ones, and those clients get an answer only after their own timeout and a retry. This happens in bursts: many devices waking up at once, or an app that sends dozens of lookups together.
At start, Warden asks for a receive buffer of 2 MiB (2,097,152 bytes). Linux
doubles every request for its own bookkeeping, so the buffer becomes 4 MiB,
unless net.core.rmem_max is lower: the kernel caps the request at that value.
On many hosts it is 212,992 bytes, the kernel default.
The start line in the journal says which case you are in:
| Start line | What it means |
|---|---|
DNS UDP receive buffer: 4194304 bytes (requested 2097152) | The buffer is as large as Warden asked. Nothing to do. |
DNS UDP receive buffer: 425984 bytes (requested 2097152); net.core.rmem_max caps it, raise it with: sysctl -w net.core.rmem_max=2097152 (persist it in /etc/sysctl.d/) | net.core.rmem_max capped the request (in this example at 212,992 bytes, doubled). DNS works, but a burst can lose queries. |
A warning, cannot raise the DNS UDP receive buffer: followed by the system error | Warden could not set the buffer. It runs with the buffer the socket already had. |
Check your host
sudo journalctl -u purge-warden -b | grep 'UDP receive buffer'
sudo ss -ulmn 'sport = :53'
nstat -az UdpRcvbufErrors Udp6RcvbufErrorsssshows each socket on port 53 with askmem:(...)line:rbis the size of its receive buffer in bytes, anddcounts the packets the kernel dropped for that socket.UdpRcvbufErrors(IPv4) andUdp6RcvbufErrors(IPv6) count, since boot, the UDP packets that every socket on the host dropped because its buffer was full. If either grows at busy times and Warden is the main UDP service on the host, Warden’s buffer is too small.
Raise the cap
# Apply it now
sudo sysctl -w net.core.rmem_max=2097152
# Keep it after a reboot
echo 'net.core.rmem_max = 2097152' | sudo tee /etc/sysctl.d/90-purge-warden-rmem.conf
# Warden sets its buffer only at start
sudo systemctl restart purge-warden
sudo journalctl -u purge-warden --since '5 minutes ago' | grep 'UDP receive buffer'warden reload does not change the buffer, so restart the service.
net.core.rmem_max is only a ceiling: raising it gives no other program a
larger buffer unless that program asks for one.
More than 1,024 devices
Warden keeps statistics for up to 1,024 client addresses. Past that, the least recently seen entries make room, and their counters start again.
Warden keeps a table of the clients it has seen: one entry per client address,
with its query and block counters, the time it was last seen and its last 24
hours of activity. The device views of the dashboard and the per-device
statistics read this table. [tracking] max_devices caps its size, 1,024 by
default.
- One entry per address, not per device. A device with both an IPv4 and an IPv6 address, or one that changes its IPv6 address over time, takes several entries.
- Entries do not expire on their own. When the table is full, each query from a new address removes the least recently seen of 8 sampled entries. If that device comes back, its counters start from zero.
- DNS answers do not change. Only the statistics are affected, and no log line says that an entry was removed.
max_devices = 0turns per-device statistics off.
Check
If the metrics endpoint is on (see Metrics & logs),
purge_warden_tracked_devices is the number of entries in the table. When it
equals max_devices, the table is full:
curl -s http://127.0.0.1:8053/metrics | grep '^purge_warden_tracked_devices'
warden config show --section trackingWithout the metrics endpoint, compare max_devices with the number of client
addresses on your network, counting IPv4 and IPv6 addresses separately.
Raise it
Set max_devices above that number, with room for growth. An entry takes about
1 KiB of memory, so 4,096 entries take about 4 MiB.
sudo warden config edit # in [tracking], set: max_devices = 4096
sudo systemctl restart purge-wardenA change to this setting needs a restart. warden reload does not apply it:
the journal shows reload requires daemon restart for runtime sections: tracking runtime,
and the previous settings keep running.
Query log on a busy network
The query log keeps at most query_log_max_files + 1 files of
query_log_max_size_mb MiB per day: 800 MiB with the defaults. A day with more
traffic keeps only its last 800 MiB. The Query log
page explains how to work out what that means for your network, which settings
keep more, and how much disk the log can use.
Verify it in the code
Every claim on this page is one function away in the source. Where to look:
src/dnsserver.rs → raise_recv_buffer()src/dnsserver.rs → recv_buffer_note()src/upstreamplain.rs → PlainUpstream::new()src/trackingengine.rs → evict_stalest_device()src/cli/commandsstart.rs → RestartOnlyRuntimeFingerprint::changed_sections()