mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:07:04 +01:00
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
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:
parent
b55b33bc91
commit
038017d9eb
@ -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"
|
||||
}
|
||||
@ -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."
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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."
|
||||
|
||||
@ -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.
|
||||
|
||||
103
linux_configuration/features/lib/tests/test_wg_firewall.sh
Executable file
103
linux_configuration/features/lib/tests/test_wg_firewall.sh
Executable 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"
|
||||
@ -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)."
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
;;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user