wg firewall: replace only inet filter (no docker restart), persist DoT 853 via ALLOW_DOT
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cqv4ckcaStUFpUkcJKmmNz
This commit is contained in:
Krzysztof Rudnicki 2026-10-04 21:15:34 +02:00
parent b55b33bc91
commit 038017d9eb
9 changed files with 226 additions and 77 deletions

View File

@ -0,0 +1,16 @@
{
"title": "Firewall applies leave Docker alone; DoT 853 persisted",
"objective": "Any setup_wireguard_ssh.sh allow-* apply must replace only the inet filter table, never restart docker or its containers while docker's own rules are intact, and keep tcp/853 on wg0 (DoT) across every re-apply and reboot via a persisted ALLOW_DOT flag.",
"acceptance_criteria": [
"Rendered ruleset has no 'flush ruleset' and declares-then-deletes inet filter before defining it (test_wg_firewall.sh)",
"restore_docker_rules restarts docker only when 'iptables -t nat -S DOCKER' fails (test_wg_firewall.sh)",
"ALLOW_DOT=true renders iifname wg0 tcp dport 853 accept; open_firewall goes through allow-dot (test_wg_firewall.sh, test_dot_resolver_install.sh)",
"vmbox: re-apply leaves docker, a probe container and a foreign nft table untouched; first-boot load works",
"Host: allow-dot applied with docker and gitea-caddy start times unchanged; verify reports no drift"
],
"out_of_scope": [
"The publicly bound DoT branch's runtime rate-limited rule",
"Changing which ports are open beyond adding 853 on wg0"
],
"verifier": "lib/tests/run_all.sh in shell_coverage_jail.sh, vmbox guest dotfw, setup_wireguard_ssh.sh verify on the host"
}

View File

@ -1,44 +0,0 @@
{
"intent": "Stop the pre-push gate failing with 'tests passed, then Killed' when the machine is under memory pressure, so a push is blocked by the code and not by unrelated load.",
"scope": [
"meta/scripts/ci_mirror.sh - run the two heavy gates serially when memory is tight",
"meta/scripts/ci_mirror_mem.sh - new lib holding the probe, the threshold and the predicate",
"Non-goal: the 2G pytest cap in pytest_changed_packages.py, which measurement showed is correct and was left alone."
],
"changes": [
"Diagnosed the real cause: ci_mirror.sh runs `pre-commit run --all-files` and the pytest gate concurrently with & and wait. Measured tree peaks are ~1.75 GiB for pre-commit (mypy/pylint repo-wide) and ~0.29 GiB for pytest, so the pair wants ~2 GiB at the same instant. The pytest scope runs under MemoryMax=2G with MemorySwapMax=0, so under pressure it is the one the kernel kills - the log reads '1798 passed' then 'Killed', which the gate reports as a test failure.",
"Added available_mem_mib (reads MemAvailable from /proc, no fork) and should_serialise_gates, and made the concurrency conditional on it. Above 4 GiB available the fast parallel path is unchanged.",
"Extracted the probe into ci_mirror_mem.sh following the existing ci_mirror_range.sh / ci_mirror_shell.sh pattern, which also kept ci_mirror.sh under the 250-line cap (245).",
"Exposed the threshold as a predicate rather than a bare constant so SC2034 is resolved by giving the value a real use in its own file, not by a suppression."
],
"verification": [
{
"command": "systemd-run --scope -p MemoryMax=2G -p MemorySwapMax=0 /usr/bin/time -v python -m pytest ... (live tree and clean worktree)",
"result": "pass",
"evidence": "1798 passed, Maximum RSS 293928 KB (~0.29 GiB) both times. Proves the suite is nowhere near its own 2G ceiling, so the ceiling was not the problem and did not need raising."
},
{
"command": "systemd-run --scope -p MemoryMax=8G bash -c 'pre-commit run --all-files; cat .../memory.peak'",
"result": "pass",
"evidence": "1751576576 bytes = ~1.75 GiB across the process tree. /usr/bin/time reported only ~0.29 GiB because it measures the parent, not the forked mypy/pylint children - the cgroup peak is the honest number."
},
{
"command": "Stubbed available_mem_mib at 1000 and 99999 MiB against should_serialise_gates",
"result": "pass",
"evidence": "1000 -> SERIAL, 99999 -> PARALLEL. Boundary table also checked: 4095 SERIAL, 4096 PARALLEL. Confirms the new branch is reachable and not dead code."
},
{
"command": "pre-commit run --files meta/scripts/ci_mirror.sh meta/scripts/ci_mirror_mem.sh",
"result": "pass",
"evidence": "No failures. file-length-cap satisfied at 245 lines; SC2034 resolved without a suppression."
}
],
"risks": [
"Under 4 GiB available the gate is slower, since the stages no longer overlap. That is the intended trade against a false failure.",
"MemAvailable is a snapshot: memory can be consumed after the check by another process, so this reduces the failure window rather than closing it absolutely. The 2G cap with MemorySwapMax=0 still bounds the damage to a clean kill rather than zram thrashing."
],
"rollback": [
"git revert the commit and delete meta/scripts/ci_mirror_mem.sh; the gate returns to unconditional concurrency.",
"After rollback, confirm ci_mirror.sh still sources cleanly (bash -n) and that a push runs the gate."
]
}

View File

@ -0,0 +1,41 @@
{
"intent": "A firewall apply (setup_wireguard_ssh.sh allow-*) no longer bounces Docker and its containers, and the DoT rule (tcp/853 on wg0) survives every re-apply instead of vanishing.",
"scope": [
"linux_configuration/features/lib/wg_firewall.sh: ruleset replaces only 'table inet filter'; restore_docker_rules restarts docker only when its nat DOCKER chain is gone; ALLOW_DOT renders 853 on wg0; allow_dot()",
"linux_configuration/features/setup_wireguard_ssh.sh, lib/wg_sshd.sh: ALLOW_DOT default/save_config, allow-dot subcommand and usage",
"linux_configuration/features/lib/dot_resolver_install.sh: wg-bound branch goes through 'setup_wireguard_ssh.sh allow-dot' instead of a runtime 'nft add rule'",
"Tests: new lib/tests/test_wg_firewall.sh; test_dot_resolver_install.sh wg-branch cases rewritten",
"Non-goal: the publicly bound DoT branch still adds its rate-limited rule at runtime"
],
"changes": [
"'flush ruleset' replaced by 'table inet filter' + 'delete table inet filter' before the definition: atomic, leaves Docker's (iptables-nft) and Tailscale's tables alone, and loads on a first boot",
"restore_docker_rules checks 'iptables -t nat -S DOCKER' and only restarts docker when that chain is missing",
"sshd-death rollback deletes only inet filter instead of flushing the whole ruleset",
"ALLOW_DOT persisted in .wireguard_ssh.conf; open_firewall calls allow-dot so the rule is rendered on every apply"
],
"verification": [
{
"command": "vmbox guest dotfw (iptables-nft, docker 29 + busybox probe, an extra 'inet sentinel' table): old HEAD allow-homeassistant vs new allow-dot",
"result": "pass",
"evidence": "old: all non-inet-filter tables deleted, docker + probe restarted; new re-apply from healthy state: docker ActiveEnterTimestamp and probe StartedAt unchanged, sentinel and docker tables kept; verify clean; systemctl restart nftables and first-boot load (no table) rc=0"
},
{
"command": "sudo setup_wireguard_ssh.sh allow-dot (host, iptables legacy)",
"result": "pass",
"evidence": "'docker's own firewall rules are intact; containers untouched'; docker since 2026-09-28 12:41:15 and gitea-caddy StartedAt unchanged; live input chain gains iifname wg0 tcp dport 853 accept; verify: nftables.conf matches; anki/gitea HTTPS 200"
},
{
"command": "lib/tests/test_wg_firewall.sh (root in vmbox) ; shell_coverage_jail.sh --subject lib/tests/run_all.sh <jail_args> --min 0 --fail-on-case-error",
"result": "pass",
"evidence": "12 passed incl. real nft -c; jailed suite rc=0"
}
],
"risks": [
"Tables left over from an older ruleset outside inet filter are no longer flushed by an apply (none exist on the host).",
"If docker's rules live somewhere other than the nat DOCKER chain, a missing-rules state would not trigger the rebuild."
],
"rollback": [
"git revert this commit, then sudo setup_wireguard_ssh.sh allow-web to re-render (old code restarts docker once)",
"After rollback: setup_wireguard_ssh.sh verify, and confirm containers reach the network"
]
}

View File

@ -100,12 +100,12 @@ open_firewall() {
# added explicitly, scoped to the tunnel interface.
if [[ "$BIND_ADDR" != "0.0.0.0" ]]; then
if command -v nft >/dev/null 2>&1 && sudo nft list table inet filter >/dev/null 2>&1; then
if sudo nft list table inet filter | grep -q "iifname \"wg0\" tcp dport $DOT_PORT"; then
log "nftables already permits $DOT_PORT on wg0"
# Through the firewall's owner, which persists ALLOW_DOT and renders
# the rule: an 'nft add rule' here vanished at the next re-apply.
if sudo bash "${WG_SCRIPT:-${SCRIPT_DIR}/setup_wireguard_ssh.sh}" allow-dot; then
log "Permitted TCP $DOT_PORT on wg0 only (persisted)"
else
sudo nft add rule inet filter input iifname wg0 tcp dport "$DOT_PORT" accept ||
log "WARNING: could not add nftables rule for $DOT_PORT on wg0"
log "Permitted TCP $DOT_PORT on wg0 only"
log "WARNING: could not open $DOT_PORT on wg0 (setup_wireguard_ssh.sh allow-dot)"
fi
fi
log "No router port-forward needed - $BIND_ADDR is WireGuard-only."

View File

@ -104,25 +104,31 @@ _t_called 'daemon-reload' "units are followed by a daemon-reload"
out="$(open_firewall 2>&1)"
_t_has "$out" 'WireGuard-only' "wg-bound install needs no router forward"
# nft present and the rule already there: must not add a duplicate.
# nft present: the rule goes through the firewall owner's allow-dot, which
# persists ALLOW_DOT so a later re-apply keeps it (a bare 'nft add rule' was
# silently dropped by the next one).
_t_stub nft
printf '#!/usr/bin/env bash\nprintf "nft %%s\\n" "$*" >>"%s/calls.log"\nif [[ $* == *list* ]]; then printf "iifname \\"wg0\\" tcp dport %s accept\\n" ; fi\nexit 0\n' \
"$TEST_TMPDIR" "$DOT_PORT" >"$TEST_TMPDIR/bin/nft"
chmod +x "$TEST_TMPDIR/bin/nft"
WG_SCRIPT="$TEST_TMPDIR/bin/wg_owner"
printf '#!/usr/bin/env bash\nprintf "wg_owner %%s\\n" "$*" >>"%s/calls.log"\nexit 0\n' \
"$TEST_TMPDIR" >"$WG_SCRIPT"
chmod +x "$WG_SCRIPT"
out="$(open_firewall 2>&1)"
_t_has "$out" 'already permits' "an existing wg0 rule is not duplicated"
_t_has "$out" 'wg0 only (persisted)' "the wg0 rule is opened through the firewall owner"
_t_called 'wg_owner allow-dot' "open_firewall runs setup_wireguard_ssh.sh allow-dot"
if grep -q 'add rule inet filter input iifname wg0' "$TEST_TMPDIR/calls.log"; then
_t_fail "no unpersisted runtime 'nft add rule' for the wg0 branch"
else
_t_pass "no unpersisted runtime 'nft add rule' for the wg0 branch"
fi
# nft present and the rule missing: must add it, scoped to wg0.
printf '#!/usr/bin/env bash\nprintf "nft %%s\\n" "$*" >>"%s/calls.log"\nexit 0\n' \
"$TEST_TMPDIR" >"$TEST_TMPDIR/bin/nft"
chmod +x "$TEST_TMPDIR/bin/nft"
# allow-dot fails (e.g. sshd check rolled it back): warn and carry on.
printf '#!/usr/bin/env bash\nexit 1\n' >"$WG_SCRIPT"
out="$(open_firewall 2>&1)"
_t_has "$out" 'wg0 only' "a missing wg0 rule is added"
_t_called 'add rule inet filter input iifname wg0' "the added rule is scoped to the tunnel"
_t_has "$out" 'WARNING: could not open' "a failed allow-dot warns instead of aborting"
unset WG_SCRIPT
# nft present but the ADD fails (a locked-down or read-only ruleset): the
# install must warn and carry on, never abort. `nft list` still has to succeed
# or the outer guard short-circuits before the add is attempted.
# The public branch still adds its rule at runtime; its add-failure path
# needs an nft whose 'list' succeeds and 'add' fails.
cat >"$TEST_TMPDIR/bin/nft" <<'NFT'
#!/usr/bin/env bash
printf 'nft %s\n' "$*" >>"$TEST_TMPDIR/calls.log"
@ -130,8 +136,6 @@ printf 'nft %s\n' "$*" >>"$TEST_TMPDIR/calls.log"
exit 0
NFT
chmod +x "$TEST_TMPDIR/bin/nft"
out="$(open_firewall 2>&1)"
_t_has "$out" 'WARNING: could not add nftables rule' "a failed wg0 rule add warns instead of aborting"
# --- open_firewall: the publicly-bound branch -------------------------------
# A public bind is an open resolver, so the rule MUST carry a rate limit.

View File

@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Tests for wg_firewall.sh -- the rendered ruleset and the docker restart.
#
# Every apply used to start with 'flush ruleset' and end with a docker
# restart, so opening one port bounced every container on the host. The
# subject now replaces only its own table and restarts docker only when
# docker's own rules are really gone; both are asserted on generated content
# and on the commands actually called.
#
# Rendering writes only where it is told and every command is stubbed, so
# this suite needs no jail, but it runs inside one with its siblings.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./features_harness.sh
. "$SCRIPT_DIR/features_harness.sh"
_t_setup_env
trap _t_teardown EXIT
# The globals setup_wireguard_ssh.sh defines above its source line.
WG_IFACE="wg0"
WG_PORT="51820"
LAN_SUBNET="192.168.1.0/24"
NFT_CONF="$TEST_TMPDIR/nftables.conf"
ALLOW_WEB="true"
ALLOW_DNS="false"
ALLOW_HOMEASSISTANT="true"
ALLOW_DOT="true"
detect_lan_subnet() { :; }
log_ok() { log "$1"; }
log_warn() { log "$1"; }
log_error() { log "$1"; }
die() {
log "$1"
exit 1
}
is_service_active() { [[ ${DOCKER_ACTIVE:-yes} == yes ]]; }
# shellcheck source=../wg_firewall.sh
. "$FEATURES_LIB_DIR/wg_firewall.sh"
printf '\n-- only our own table is replaced --\n'
RULES="$TEST_TMPDIR/rules.nft"
render_nftables_ruleset "$RULES"
if grep -q '^flush ruleset' "$RULES"; then
_t_fail "no 'flush ruleset' (it deletes Docker's and Tailscale's tables)"
else
_t_pass "no 'flush ruleset' (it deletes Docker's and Tailscale's tables)"
fi
_t_eq "table inet filter|delete table inet filter" \
"$(grep -E '^(delete )?table inet filter$' "$RULES" | paste -sd'|')" \
"declare-then-delete precedes the definition, so a first boot loads"
printf '\n-- flags render their rules --\n'
_t_file_has "$RULES" 'iifname "wg0" tcp dport 853 accept' "ALLOW_DOT opens 853 on wg0"
_t_file_has "$RULES" 'iifname "wg0" tcp dport 8123 accept' "ALLOW_HOMEASSISTANT keeps 8123"
_t_file_has "$RULES" 'tcp dport { 80, 443 } accept' "ALLOW_WEB keeps 80/443"
ALLOW_DOT="false"
render_nftables_ruleset "$RULES"
if grep -q 'dport 853' "$RULES"; then
_t_fail "ALLOW_DOT=false renders no 853 rule"
else
_t_pass "ALLOW_DOT=false renders no 853 rule"
fi
if command -v nft >/dev/null 2>&1; then
ALLOW_DOT="true"
render_nftables_ruleset "$RULES"
if nft -c -f "$RULES" >/dev/null 2>&1; then
_t_pass "nft -c accepts the rendered ruleset"
else
# nft -c needs CAP_NET_ADMIN; outside the jail it cannot check at all.
printf ' (skipped: nft -c not permitted here)\n'
fi
fi
printf '\n-- docker is restarted only when its rules are gone --\n'
_t_stub systemctl
printf '#!/usr/bin/env bash\nprintf "iptables %%s\\n" "$*" >>"%s/calls.log"\nexit 0\n' \
"$TEST_TMPDIR" >"$TEST_TMPDIR/bin/iptables"
chmod +x "$TEST_TMPDIR/bin/iptables"
out="$(restore_docker_rules 2>&1)"
_t_has "$out" 'intact; containers untouched' "intact docker rules are left alone"
if grep -q 'systemctl restart docker' "$TEST_TMPDIR/calls.log"; then
_t_fail "no docker restart while its rules are intact"
else
_t_pass "no docker restart while its rules are intact"
fi
printf '#!/usr/bin/env bash\nexit 1\n' >"$TEST_TMPDIR/bin/iptables"
out="$(restore_docker_rules 2>&1)"
_t_has "$out" 'restarted' "missing docker rules trigger a rebuild"
_t_called 'systemctl restart docker' "the rebuild is a docker restart"
: >"$TEST_TMPDIR/calls.log"
DOCKER_ACTIVE=no restore_docker_rules
if grep -q 'iptables\|systemctl' "$TEST_TMPDIR/calls.log"; then
_t_fail "an inactive docker is not touched"
else
_t_pass "an inactive docker is not touched"
fi
_t_report "test_wg_firewall.sh"

View File

@ -12,14 +12,14 @@ render_nftables_ruleset() {
local target="$1"
detect_lan_subnet
local web_rule=""
if [[ $ALLOW_WEB == "true" ]]; then
if [[ ${ALLOW_WEB:-false} == "true" ]]; then
web_rule=$'\n\t\ttcp dport { 80, 443 } accept'
fi
# DNS blocker (setup_dns_blocker.sh): serve DNS (53) -- and DHCP (67) when
# this PC is the LAN DHCP server -- to LAN clients only. Restricted to the
# LAN subnet so nothing is ever exposed to the internet.
local dns_rule="" dhcp_rule=""
if [[ $ALLOW_DNS == "true" ]]; then
if [[ ${ALLOW_DNS:-false} == "true" ]]; then
dns_rule=$'\n\t\tip saddr '"${LAN_SUBNET}"$' udp dport 53 accept'
dns_rule+=$'\n\t\tip saddr '"${LAN_SUBNET}"$' tcp dport 53 accept'
# DHCP clients have no IP yet (saddr 0.0.0.0 -> 255.255.255.255) and the
@ -35,9 +35,20 @@ render_nftables_ruleset() {
ha_rule=$'\n\t\tip saddr '"${LAN_SUBNET}"$' tcp dport 8123 accept'
ha_rule+=$'\n\t\tiifname "'"${WG_IFACE}"$'" tcp dport 8123 accept'
fi
# DNS-over-TLS (setup_dot_resolver.sh, bound to the WireGuard address):
# rendered here so a re-apply keeps it -- a runtime 'nft add rule' did not.
local dot_rule=""
if [[ ${ALLOW_DOT:-false} == "true" ]]; then
dot_rule=$'\n\t\tiifname "'"${WG_IFACE}"$'" tcp dport 853 accept'
fi
cat >"$target" <<EOF
#!/usr/sbin/nft -f
flush ruleset
# Replaces ONLY this table, atomically. 'flush ruleset' also deleted every
# other table (Docker's under iptables-nft, Tailscale's), which is what made
# each apply restart docker. Declaring the table first makes the delete safe
# on a boot where it does not exist yet.
table inet filter
delete table inet filter
table inet filter {
chain input {
@ -53,7 +64,7 @@ table inet filter {
udp dport ${WG_PORT} accept
iifname "${WG_IFACE}" tcp dport 22 accept
ip saddr ${LAN_SUBNET} tcp dport 22 accept${web_rule}${dns_rule}${ha_rule}
ip saddr ${LAN_SUBNET} tcp dport 22 accept${web_rule}${dns_rule}${ha_rule}${dot_rule}
}
chain forward {
type filter hook forward priority 0; policy drop;
@ -120,16 +131,19 @@ verify_nft() {
return "$status"
}
# 'flush ruleset' above deletes Docker's own tables along with everything
# else, and dockerd only rebuilds them on its next network event -- so
# without this, containers keep running with no NAT and no forwarding until
# somebody happens to restart one. Restarting dockerd rebuilds them at once;
# containers with a restart policy come back by themselves.
# Docker keeps its NAT/forward rules in iptables (legacy on this host, so not
# even visible to nft). Replacing only 'inet filter' leaves them alone, so a
# restart -- which bounces every container -- is reserved for when they are
# really gone (e.g. a hand-run 'nft flush ruleset' under iptables-nft).
restore_docker_rules() {
if ! is_service_active docker; then
return 0
fi
log_warn "Restarting docker so it can rebuild the nft rules the flush removed."
if iptables -t nat -S DOCKER >/dev/null 2>&1; then
log_ok "docker's own firewall rules are intact; containers untouched."
return 0
fi
log_warn "docker's nat rules are missing; restarting docker to rebuild them."
systemctl restart docker
log_ok "docker restarted; its firewall rules are back."
}
@ -141,7 +155,7 @@ verify_nftables_then_apply() {
nft -f "$NFT_CONF"
sleep 2
if ! is_service_active sshd; then
nft flush ruleset
nft delete table inet filter
die "sshd died after applying nftables -- rolled back. Investigate before retrying."
fi
log_ok "nftables applied; sshd is still active."
@ -149,3 +163,11 @@ verify_nftables_then_apply() {
log_warn "Before closing this terminal, open a SECOND ssh session now and confirm it connects."
enable_service nftables
}
allow_dot() {
export ALLOW_DOT=true
save_config
write_nftables_ruleset
verify_nftables_then_apply
log_ok "Opened tcp/853 on ${WG_IFACE} (persisted)."
}

View File

@ -235,6 +235,7 @@ Commands:
add-peer <name> Provision a new phone/laptop and print its QR code.
allow-web Open tcp/80 and tcp/443 in the firewall (for a web server on this host).
allow-dns Open udp/tcp 53 from the LAN in the firewall (for the DNS blocker on this host).
allow-dot Open tcp/853 on wg0 in the firewall (for the DoT resolver on this host).
verify Check /etc/nftables.conf matches this script and that
the live forward chain still lets Docker through.
install-verify Install the units that run 'verify' at every boot, so

View File

@ -40,6 +40,7 @@ LAN_SUBNET="${LAN_SUBNET:-}"
export ALLOW_WEB="${ALLOW_WEB:-false}"
export ALLOW_DNS="${ALLOW_DNS:-false}"
export ALLOW_HOMEASSISTANT="${ALLOW_HOMEASSISTANT:-false}"
export ALLOW_DOT="${ALLOW_DOT:-false}"
die() {
log_error "$1"
@ -54,6 +55,7 @@ LAN_SUBNET="${LAN_SUBNET}"
ALLOW_WEB="${ALLOW_WEB}"
ALLOW_DNS="${ALLOW_DNS}"
ALLOW_HOMEASSISTANT="${ALLOW_HOMEASSISTANT}"
ALLOW_DOT="${ALLOW_DOT}"
EOF
chmod 600 "$CONFIG_FILE"
}
@ -93,7 +95,7 @@ main() {
# off -- exec sudo "$0" "$@" inside require_root must re-launch with the
# subcommand still present, or sudo would silently run with no args.
case "$cmd" in
setup | add-peer | revoke | allow-web | allow-dns | allow-homeassistant | verify | install-verify)
setup | add-peer | revoke | allow-web | allow-dns | allow-homeassistant | allow-dot | verify | install-verify)
require_root "$@"
;;
esac
@ -134,6 +136,10 @@ main() {
allow_homeassistant
install_verify_units
;;
allow-dot)
allow_dot
install_verify_units
;;
verify)
verify_nft
;;