#!/bin/bash # End-to-end test of Guard with Housecarl and real traffic: a router # container (Debian 13, nftables 1.1.3) runs housecarld between an # "internet" network and a LAN; a server on the LAN runs Guard over a log # file; a host on the internet misbehaves. Checks that Guard bans it at # the router (one port, for the server alone), that never_ban holds, that # bans wait while Housecarl is down, and that Guard on the router bans # scanners from Housecarl's own log of dropped packets. # # Needs Docker and ../housecarl (its image housecarl-nft, made by its # scripts/e2e.sh if missing). Run from the repository root: # # make e2e or scripts/e2e.sh # # KEEP=1 leaves the containers and networks running. set -u ROOT=$(cd "$(dirname "$0")/.." && pwd) HC=${HOUSECARL_SRC:-$ROOT/../housecarl} P=${E2E_PREFIX:-gde2e} IMAGE=${E2E_IMAGE:-housecarl-nft} WORK=${WORK:-$(mktemp -d "${TMPDIR:-/tmp}/guard-e2e.XXXXXX")} docker info >/dev/null 2>&1 || { echo "Docker isn't reachable (set DOCKER_HOST?)"; exit 2; } docker image inspect "$IMAGE" >/dev/null 2>&1 || { echo "no image $IMAGE: run ../housecarl's make e2e once to make it"; exit 2; } (cd "$ROOT" && CGO_ENABLED=0 go build -o "$WORK/guard" ./cmd/guard) || exit 1 (cd "$HC" && CGO_ENABLED=0 go build -o "$WORK/housecarld" ./cmd/housecarld && CGO_ENABLED=0 go build -o "$WORK/hc" ./cmd/hc) || exit 1 cleanup() { if [ "${KEEP:-}" = 1 ]; then echo "left running: containers ${P}-*, networks ${P}-*, files in $WORK"; return; fi docker exec ${P}-router sh -c 'rm -rf /w/data /w/*.log /w/state*' >/dev/null 2>&1 docker rm -f $(docker ps -aq --filter "name=^${P}-") >/dev/null 2>&1 docker network rm ${P}-wan ${P}-lan >/dev/null 2>&1 rm -rf "$WORK" } trap cleanup EXIT docker rm -f $(docker ps -aq --filter "name=^${P}-") >/dev/null 2>&1 docker network rm ${P}-wan ${P}-lan >/dev/null 2>&1 # The internet 198.18.2.0/24, the LAN 10.89.10.0/24; the router has .1 on # the LAN, .2 on the internet; the server is 10.89.10.10. docker network create --subnet 198.18.2.0/24 --gateway 198.18.2.254 ${P}-wan >/dev/null || exit 1 docker network create --subnet 10.89.10.0/24 --gateway 10.89.10.254 ${P}-lan >/dev/null || exit 1 WAN4=198.18.2.2 LAN4=10.89.10.1 SRV=10.89.10.10 NET=198.18.2.10 NET2=198.18.2.11 docker run -d --name ${P}-router --hostname router --cap-add NET_ADMIN --sysctl net.ipv4.ip_forward=1 \ --network ${P}-wan --ip $WAN4 -v "$WORK:/w" "$IMAGE" sleep infinity >/dev/null || exit 1 docker network connect --ip $LAN4 ${P}-lan ${P}-router || exit 1 docker exec ${P}-router sh -c 'ip link set eth0 down && ip link set eth0 name wan && ip link set wan up && ip link set eth1 down && ip link set eth1 name lan && ip link set lan up && ip route replace default via 198.18.2.254' || exit 1 host() { # NAME NETWORK IP GATEWAY docker run -d --name ${P}-$1 --cap-add NET_ADMIN --network ${P}-$2 --ip $3 -v "$WORK:/w" "$IMAGE" sleep infinity >/dev/null || exit 1 docker exec ${P}-$1 ip route replace default via $4 || exit 1 } host srv lan $SRV $LAN4 host net wan $NET 198.18.2.254 docker exec ${P}-net sh -c "ip addr add $NET2/24 dev eth0 && ip route replace 10.89.10.0/24 via $WAN4" # The server: "ssh" on 22 and "web" on 80. docker exec -d ${P}-srv sh -c 'while true; do echo ssh | nc -N -l 0.0.0.0 22; done' docker exec -d ${P}-srv sh -c 'while true; do echo web | nc -N -l 0.0.0.0 80; done' PASS=0; FAIL=0 ok() { echo "PASS: $1"; PASS=$((PASS+1)); } bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); } run() { local desc=$1 want=$2; shift 2 OUT=$("$@" 2>&1); local rc=$? if [ "$rc" = "$want" ]; then ok "$desc (rc=$rc)"; else bad "$desc: rc=$rc want $want"; echo "$OUT" | sed 's/^/ /'; fi } contains() { if echo "$OUT" | grep -q -- "$1"; then ok "output has '$1'"; else bad "output lacks '$1'"; echo "$OUT" | sed 's/^/ /'; fi; } not_contains() { if echo "$OUT" | grep -q -- "$1"; then bad "output has '$1'"; echo "$OUT" | sed 's/^/ /'; else ok "output lacks '$1'"; fi; } # eventually DESC CMD...: retries for 15 seconds until CMD succeeds. eventually() { local desc=$1; shift for i in $(seq 1 30); do OUT=$("$@" 2>&1) && { ok "$desc"; return; }; sleep 0.5; done bad "$desc"; echo "$OUT" | sed 's/^/ /' } R() { docker exec ${P}-router "$@"; } H() { docker exec ${P}-router /w/hc "$@"; } connect() { docker exec ${P}-net nc -w 3 ${2:+-s $2} $SRV $1 > "$WORK/auth.log" } cat > "$WORK/housecarl.yaml" <<'EOF' listen: ["0.0.0.0:8070"] database: /w/data/housecarl.db ruleset: /w/data/ruleset.nft log: {level: debug} stats: {enabled: false} EOF start_hc() { R sh -c "(/w/housecarld -c /w/housecarl.yaml serve >> /w/housecarl.log 2>&1 &); for i in \$(seq 1 30); do [ -S /run/housecarl/housecarl.sock ] && exit 0; sleep 0.3; done; exit 1"; } stop_hc() { R sh -c 'kill $(pidof housecarld)'; sleep 1; } # ---- Housecarl: a router with a ban list for the server run "housecarld starts" 0 start_hc run "zones" 0 sh -c "docker exec ${P}-router /w/hc zone add wan --interface wan --external && docker exec ${P}-router /w/hc zone add lan --interface lan" run "the API from the LAN" 0 H rule add input --in lan --service tcp/8070 run "the internet reaches the server's ssh and web" 0 H rule add forward --in wan --to $SRV --service tcp/22 --service tcp/80 run "the LAN goes out" 0 H rule add forward --in lan --out wan run "a ban list for the server" 0 H banlist add srv --scope hosts --host $SRV run "a user for the server's Guard" 0 H user add guard-srv --no-password --grant bans:write:srv R sh -c "/w/hc token create srv --user guard-srv > /w/token 2>/dev/null" run "its token" 0 test -s "$WORK/token" run "ssh from the internet" 0 connect 22 contains "^ssh$" # ---- Guard on the server, over a log file : > "$WORK/auth.log" cat > "$WORK/guard.yaml" <> /w/guard.log 2>&1' sleep 2 fails 2 $NET sleep 2 run "two failures: no ban yet" 0 H bans srv not_contains "$NET" fails 1 $NET eventually "the third: banned at the router" sh -c "docker exec ${P}-router /w/hc bans srv | grep -q '$NET *tcp/22 .*guard-srv *guard/ssh'" run "ssh from it is cut" 1 connect 22 run "its web isn't (tcp/22 only)" 0 connect 80 contains "^web$" run "guard status" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml status contains "ssh *file /w/auth.log *3 *3 *1" contains "$NET *srv *tcp/ssh *1h *banned" fails 5 $NET2 sleep 3 run "never_ban: not banned" 0 H bans srv not_contains "$NET2" run "ssh from it still works" 0 connect 22 $NET2 contains "^ssh$" run "guard test over the log" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml test ssh /w/auth.log contains "would ban 1" contains "never banned: in never_ban $NET2/32" run "guard unban" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml unban $NET contains "1 bans lifted" run "ssh again" 0 connect 22 contains "^ssh$" # ---- Housecarl away: the ban waits stop_hc fails 3 $NET sleep 3 run "the ban waits" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml status contains "1 bans wait for Housecarl" run "housecarld starts again" 0 start_hc eventually "the ban that waited arrives" sh -c "docker exec ${P}-router /w/hc bans srv | grep -q '$NET *tcp/22'" run "its time grew: a repeat offender" 0 H bans srv contains "$NET *tcp/22 *1h59m\|$NET *tcp/22 *2h" run "ssh from it is cut again" 1 connect 22 run "lift it" 0 H unban srv $NET # ---- Guard on the router: scans in Housecarl's log run "a ban list for the router" 0 H banlist add edge --scope everything run "a logged drop for a scanned port" 0 H rule add input --in wan --service tcp/9999 -a drop --log run "a user for the router's Guard" 0 H user add guard-edge --no-password --grant bans:write:edge --grant log:read R sh -c "/w/hc token create edge --user guard-edge > /w/token-edge 2>/dev/null" cat > "$WORK/guard-edge.yaml" <> /w/guard-edge.log 2>&1' sleep 2 for i in 1 2 3 4; do docker exec ${P}-net nc -w 1 $WAN4 9999 /dev/null 2>&1; done eventually "the scanner is banned from everything" sh -c "docker exec ${P}-router /w/hc bans edge | grep -q '$NET *all .*guard/scans'" run "and can't reach the server at all" 1 connect 80 echo echo "RESULT: $PASS passed, $FAIL failed" [ $FAIL = 0 ]