EN

Host tuning

Under review — wording may still change.

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:

LimitWhat you noticeWhere you change it
Connection tracking of a stateful firewallLookups slow down or fail at busy times, and the upstream looks slow or brokenLinux: net.netfilter.nf_conntrack_max, or your nftables rules
UDP receive bufferPart of a burst of queries gets no answer, and those clients retryLinux: net.core.rmem_max
Device tablePer-device statistics are incomplete, and some counters start again from zeroWarden: [tracking] max_devices
Query log size capThe query log keeps only the end of a busy dayWarden: [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

bash
sudo sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max net.netfilter.nf_conntrack_udp_timeout
sudo dmesg | grep 'table full'
  • If sysctl answers cannot stat /proc/sys/net/netfilter/nf_conntrack_count, connection tracking is not loaded on this host. Nothing in this section applies.
  • If nf_conntrack_count comes close to nf_conntrack_max at busy times, or dmesg prints table full lines, 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.

bash
# 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.conf

The 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.

/etc/nftables.conf (excerpt)
text
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/24 with your client networks.
  • In dns_upstreams, list every server Warden forwards to: the servers of [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 the nameserver lines 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-65535 keeps 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 saddr and ip6 daddr.
  • If your output chain also has policy drop, add two rules to it: meta l4proto { tcp, udp } th sport 53 accept for Warden’s answers, and ip daddr $dns_upstreams meta l4proto { tcp, udp } th dport 53 accept for its queries to the upstream servers. Untracked packets do not match ct state established,related there either, so without these rules your clients get no answers and Warden cannot reach its upstream servers.
  • fib daddr type local limits 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:

bash
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_count

If 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.

Keep the two accept rules narrow
Untracked packets skip the stateful rules. Accept them only from your client networks and your upstream addresses. Never accept 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 lineWhat 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 errorWarden could not set the buffer. It runs with the buffer the socket already had.

Check your host

bash
sudo journalctl -u purge-warden -b | grep 'UDP receive buffer'
sudo ss -ulmn 'sport = :53'
nstat -az UdpRcvbufErrors Udp6RcvbufErrors
  • ss shows each socket on port 53 with a skmem:(...) line: rb is the size of its receive buffer in bytes, and d counts the packets the kernel dropped for that socket.
  • UdpRcvbufErrors (IPv4) and Udp6RcvbufErrors (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

bash
# 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 = 0 turns 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:

bash
curl -s http://127.0.0.1:8053/metrics | grep '^purge_warden_tracked_devices'
warden config show --section tracking

Without 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.

bash
sudo warden config edit      # in [tracking], set: max_devices = 4096
sudo systemctl restart purge-warden

A 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:

Receive buffer and its start line
src/dnsserver.rs → raise_recv_buffer()
src/dnsserver.rs → recv_buffer_note()
Plain upstream
src/upstreamplain.rs → PlainUpstream::new()
Device table cap and eviction
src/trackingengine.rs → evict_stalest_device()
Settings that need a restart
src/cli/commandsstart.rs → RestartOnlyRuntimeFingerprint::changed_sections()
↑↓ to navigate · Enter to open · Esc to close See all results