Shadow mode: Housecarl beside the firewall in force, changing nothing
shadow.enabled in housecarl.yaml loads Housecarl's ruleset so that it blocks, rejects and translates nothing: drops and rejects count and accept (an accept only ends Housecarl's chain), policies accept and log what they would drop, NAT only counts, MSS clamping and the flowtable are left out, and raw rules that do more than accept, drop or reject are left out with a warning. Comments mark what would happen ([would drop]). Filter chains run after the other firewall's (+10) or before (-10); NAT chains always first. Every change is checked by nftables in both forms. No probation; takeovers and closing connections are refused. The boot fallback filters nothing. internal/tablewatch puts the table back after a flush ruleset. Mode in status, healthz, whoami, the web UI, the log and the tracer; hc ruleset --enforcing. docs/shadow.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1e6c085ef9
commit
de075edd5d
48 files changed
+1345
-93
No files matched your search
+65
-12
@@ -2,12 +2,12 @@
|
||||
# End-to-end test with real traffic: a Debian 13 router container (nftables
|
||||
# 1.1.3) between an "internet" network and a LAN, IPv4 and IPv6, runs
|
||||
# housecarld. The maintainer's own router ruleset (internal/importer/
|
||||
# testdata/router.nft, with lab addresses) is loaded as it is today, imported,
|
||||
# taken over, and then checked with connections: port forwards, hairpin NAT,
|
||||
# the IPv6 pinhole, masquerading, antispoofing, the drop log, a change on
|
||||
# probation that cuts the client off and rolls itself back, a restart and the
|
||||
# boot ruleset. Needs Docker; no root on the host. Run from the repository
|
||||
# root:
|
||||
# testdata/router.nft, with lab addresses) is loaded as it is today,
|
||||
# imported with housecarld in shadow mode beside it, taken over, and then
|
||||
# checked with connections: port forwards, hairpin NAT, the IPv6 pinhole,
|
||||
# masquerading, antispoofing, the drop log, a change on probation that cuts
|
||||
# the client off and rolls itself back, a restart and the boot ruleset.
|
||||
# Needs Docker; no root on the host. Run from the repository root:
|
||||
#
|
||||
# make e2e or scripts/e2e.sh
|
||||
#
|
||||
@@ -102,7 +102,10 @@ backup: {dir: /w/backups}
|
||||
log: {level: debug}
|
||||
stats: {database: /w/data/stats.db}
|
||||
EOF
|
||||
start() { R sh -c '(HOUSECARL_STATS_INTERVAL=3s /w/housecarld -c /w/housecarl.yaml serve >> /w/srv.log 2>&1 &); for i in $(seq 1 30); do [ -S /run/housecarl/housecarl.sock ] && exit 0; sleep 0.3; done; exit 1'; }
|
||||
{ cat "$WORK/housecarl.yaml"; echo "shadow: {enabled: true}"; } > "$WORK/housecarl-shadow.yaml"
|
||||
CONF=housecarl.yaml
|
||||
start() { R sh -c "(HOUSECARL_STATS_INTERVAL=3s /w/housecarld -c /w/$CONF serve >> /w/srv.log 2>&1 &); for i in \$(seq 1 30); do [ -S /run/housecarl/housecarl.sock ] && exit 0; sleep 0.3; done; exit 1"; }
|
||||
stop() { docker exec ${P}-router sh -c 'kill $(pidof housecarld)'; sleep 1; }
|
||||
|
||||
# ---- today's router: the hand-written ruleset
|
||||
|
||||
@@ -113,11 +116,13 @@ R nft add table ip other
|
||||
run "the port forward works under it" 0 connect net $WAN4 8096
|
||||
contains "jellyfin"
|
||||
|
||||
# ---- import and take over
|
||||
# ---- import in shadow mode: beside today's ruleset, changing nothing
|
||||
|
||||
run "init" 0 R sh -c 'echo secret-pass-123 | /w/housecarld -c /w/housecarl.yaml init --password-stdin'
|
||||
run "start housecarld" 0 start
|
||||
CONF=housecarl-shadow.yaml
|
||||
run "start housecarld in shadow mode" 0 start
|
||||
run "status" 0 H status
|
||||
contains "Mode:.*SHADOW"
|
||||
contains "Other tables:.*inet main"
|
||||
run "two tables: say which" 6 H import --defines /w/router.nft -n
|
||||
contains "several tables"
|
||||
@@ -130,8 +135,56 @@ contains "lan (enp2s0: as given)"
|
||||
contains "vpn (wg0: as given)"
|
||||
run "import" 0 H import --table "inet main" --defines /w/router.nft
|
||||
contains "applied and saved"
|
||||
run "the shadow ruleset is in force" 0 R nft list table inet housecarl
|
||||
contains "would dnat\] jellyfin"
|
||||
contains "priority filter + 10; policy accept;"
|
||||
not_contains "dnat ip to"
|
||||
not_contains "maxseg size set"
|
||||
not_contains "flowtable"
|
||||
run "nothing in it drops, rejects or translates" 1 R sh -c "nft list table inet housecarl | grep -v 'log prefix' | sed 's/comment.*//; s/ct status dnat//' | grep -wE 'drop|reject|dnat|snat|masquerade|redirect'"
|
||||
run "no warning about today's ruleset beside it" 0 H status
|
||||
not_contains "takeover_pending"
|
||||
not_contains "shadow_unprotected"
|
||||
run "what it loads once shadow mode is off" 0 H ruleset --enforcing
|
||||
contains "dnat ip to 10.88.10.57:443"
|
||||
run "the port forward works, by today's ruleset" 0 connect net $WAN4 8096
|
||||
contains "jellyfin"
|
||||
run "and Housecarl's would-be DNAT counted it" 0 R nft list chain inet housecarl dstnat
|
||||
contains 'packets [1-9][0-9]* bytes [0-9]* comment "hc:f[0-9]* \[would dnat\] jellyfin'
|
||||
# A drop Housecarl has and today's ruleset doesn't: logged, let through.
|
||||
SR=$(H -o id rule add forward --from $CLIENT4 --service tcp/9000 -a drop --log --first --comment "e2e shadow")
|
||||
: > "$WORK/net.log"
|
||||
run "a shadow drop lets the connection through" 0 docker exec ${P}-client sh -c "echo hi | nc -N -w 3 $NET4 9000"
|
||||
sleep 1
|
||||
run "it reached the internet" 0 grep -q "$WAN4" "$WORK/net.log"
|
||||
run "the log says it would have been dropped" 0 H log
|
||||
contains "would drop, let through"
|
||||
run "the shadow rule goes" 0 H rule delete -y "$SR"
|
||||
run "no takeover in shadow mode" 5 H takeover "inet main" -y
|
||||
contains "shadow mode"
|
||||
run "healthz" 0 R curl -sf http://127.0.0.1:8070/healthz
|
||||
contains '"mode":"shadow"'
|
||||
run "the boot ruleset is the shadow one" 0 R grep -q "would dnat" /w/data/ruleset.nft
|
||||
run "the boot fallback filters nothing" 1 R grep -wE "drop|masquerade" /w/data/ruleset-fallback.nft
|
||||
# Today's ruleset starts with flush ruleset: reloading it deletes
|
||||
# Housecarl's table, which housecarld puts back.
|
||||
run "today's ruleset reloaded" 0 R nft -f /w/router.nft
|
||||
R nft add table ip other
|
||||
sleep 4
|
||||
run "Housecarl's table is back" 0 R nft list table inet housecarl
|
||||
contains "would dnat"
|
||||
run "and logged" 0 R grep -q "put it back" /w/srv.log
|
||||
|
||||
# ---- enforcing, and take over
|
||||
|
||||
stop
|
||||
CONF=housecarl.yaml
|
||||
run "start housecarld enforcing" 0 start
|
||||
run "status" 0 H status
|
||||
not_contains "SHADOW"
|
||||
run "the ruleset is in force" 0 R nft list table inet housecarl
|
||||
contains "dnat ip to 10.88.10.57:443"
|
||||
not_contains "would"
|
||||
run "zones" 0 H zone list
|
||||
contains "wan.*wan.*yes"
|
||||
contains "dhcp.*enp2s0"
|
||||
@@ -267,7 +320,7 @@ run "used up: the device is cut off" 1 send 1000
|
||||
run "the usage" 0 H quotas
|
||||
contains "e2e quota.*1.0 MB each.*by hand"
|
||||
contains "$CLIENT4 *1\.[0-9] MB"
|
||||
docker exec ${P}-router sh -c 'kill $(pidof housecarld)'; sleep 1; start
|
||||
stop; start
|
||||
run "after a restart, still used up" 1 send 1000
|
||||
run "the usage after a restart" 0 H quotas
|
||||
contains "$CLIENT4 *1\.[0-9] MB"
|
||||
@@ -408,7 +461,7 @@ contains 'iifname "enp2s0" udp dport 67'
|
||||
|
||||
# ---- restart and boot
|
||||
|
||||
docker exec ${P}-router sh -c 'kill $(pidof housecarld)'; sleep 1
|
||||
stop
|
||||
run "the ruleset stays without housecarld" 0 connect net $WAN4 8096
|
||||
contains "jellyfin"
|
||||
run "restart housecarld" 0 start
|
||||
@@ -446,7 +499,7 @@ contains '"firewall":"fallback"'
|
||||
contains " 503"
|
||||
run "status says so" 0 H status
|
||||
contains "NOT in force: the boot fallback"
|
||||
docker exec ${P}-router sh -c 'kill $(pidof housecarld)'; sleep 1
|
||||
stop
|
||||
run "housecarld starts" 0 start
|
||||
run "and puts the real ruleset back" 0 connect net $WAN4 8096
|
||||
contains "jellyfin"
|
||||
|
||||
Reference in new issue
Block a user