- Detect an existing ipset without timeout support and migrate it in place (temporary set + copy members + ipset swap + destroy) without unblocking. - IPv6: "<set>6" (family inet6) + ip6tables match-set rule; ban/unban pick the set by address family. IPv4 keeps working if IPv6 is unavailable. - Remove unwrap()/panics: MartilloMaldito::new/with_config and every public method return Result<_, MartilloError>; ignore multiport/range rules when parsing secured ports; handle an empty DOCKER-USER chain. - Ipset::save_all / save-ipsets for ipset-persistent (/etc/iptables/ipsets). - structopt -> clap 4, thiserror 1 -> 2. CLI exits 1 on errors. - Kernel tests skip without CAP_NET_ADMIN; run them with `unshare -rn`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
154 lines
5.7 KiB
Bash
Executable File
154 lines
5.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# Integration tests for martillo-maldito CLI using an isolated network namespace.
|
|
# Exercises iptables/ip6tables rule management and ipset (sets are per-netns).
|
|
#
|
|
# Requires: iptables, ip6tables, ipset, jq, and either sudo or an already
|
|
# isolated namespace.
|
|
# Usage: BIN=./target/release/martillo_maldito ./tests/integration_netns.sh
|
|
# or, without sudo (user + network namespace):
|
|
# unshare -rn env SELF_ISOLATED=1 ./tests/integration_netns.sh
|
|
|
|
set -euo pipefail
|
|
|
|
BIN="${BIN:-./target/release/martillo_maldito}"
|
|
NS="martillo-it-$RANDOM"
|
|
|
|
if [[ ! -x "$BIN" ]]; then
|
|
echo "ERROR: binary not found at $BIN"
|
|
echo "Build it first: cargo build --release"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "${SELF_ISOLATED:-}" ]]; then
|
|
if [[ $EUID -ne 0 ]]; then
|
|
echo "ERROR: SELF_ISOLATED needs to run as (namespace) root, e.g. unshare -rn"
|
|
exit 1
|
|
fi
|
|
in_ns() { "$@"; }
|
|
echo "==> Running in the current (already isolated) namespace"
|
|
else
|
|
if ! command -v sudo >/dev/null; then
|
|
echo "ERROR: sudo not installed"
|
|
exit 1
|
|
fi
|
|
cleanup() {
|
|
sudo ip netns del "$NS" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
echo "==> Creating namespace $NS"
|
|
sudo ip netns add "$NS"
|
|
in_ns() { sudo ip netns exec "$NS" "$@"; }
|
|
fi
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
assert_eq() {
|
|
local desc="$1" expected="$2" actual="$3"
|
|
if [[ "$expected" == "$actual" ]]; then
|
|
echo " PASS: $desc"
|
|
PASS=$((PASS+1))
|
|
else
|
|
echo " FAIL: $desc (expected: '$expected', got: '$actual')"
|
|
FAIL=$((FAIL+1))
|
|
fi
|
|
}
|
|
|
|
run_cli() {
|
|
in_ns "$BIN" "$@"
|
|
}
|
|
|
|
ipset_count() {
|
|
in_ns ipset list "$1" | awk '/^Members:/{p=1;next} p && NF{c++} END{print c+0}'
|
|
}
|
|
|
|
echo ""
|
|
echo "==> Test 0: existing set WITHOUT timeout is migrated (members and rule kept)"
|
|
in_ns ipset create banned hash:ip family inet hashsize 16384
|
|
in_ns ipset add banned 198.51.100.1
|
|
in_ns ipset add banned 198.51.100.2
|
|
in_ns iptables -I INPUT 1 -m set --match-set banned src -j DROP
|
|
output=$(run_cli ban-ip -i 192.0.2.30 --timeout 600)
|
|
assert_eq "ban with timeout works on migrated set" "true" "$output"
|
|
assert_eq "header now has timeout" "1" "$(in_ns ipset list -t banned | grep -c 'timeout 0')"
|
|
assert_eq "old members kept" "3" "$(ipset_count banned)"
|
|
assert_eq "match-set rule not duplicated" "1" "$(in_ns iptables -S INPUT | grep -c 'match-set banned src')"
|
|
assert_eq "temporary set removed" "false" "$(in_ns ipset list -n | grep -qx banned_mig && echo true || echo false)"
|
|
|
|
echo ""
|
|
echo "==> Test 1: ban_ip via CLI is idempotent"
|
|
run_cli ban-ip -i 192.0.2.10 >/dev/null
|
|
output=$(run_cli ban-ip -i 192.0.2.10)
|
|
assert_eq "ban-ip idempotent" "true" "$output"
|
|
assert_eq "192.0.2.10 in set" "0" "$(in_ns ipset test banned 192.0.2.10 2>/dev/null; echo $?)"
|
|
run_cli unban-ip -i 192.0.2.10 >/dev/null
|
|
|
|
echo ""
|
|
echo "==> Test 2: get-banned-ips returns JSON array"
|
|
run_cli ban-ip -i 192.0.2.20 >/dev/null
|
|
run_cli ban-ip -i 192.0.2.21 >/dev/null
|
|
output=$(run_cli get-banned-ips)
|
|
assert_eq "output is valid JSON" "true" "$(echo "$output" | jq -e 'type == "array"' >/dev/null 2>&1 && echo true || echo false)"
|
|
|
|
echo ""
|
|
echo "==> Test 3: unban is idempotent"
|
|
output=$(run_cli unban-ip -i 192.0.2.99)
|
|
assert_eq "first unban" "true" "$output"
|
|
output=$(run_cli unban-ip -i 192.0.2.99)
|
|
assert_eq "second unban returns ok" "true" "$output"
|
|
|
|
echo ""
|
|
echo "==> Test 4: secured ports with allowed IPs"
|
|
run_cli secure-port -p 9999 >/dev/null
|
|
assert_eq "port 9999 is secured" "true" "$(run_cli is-port-secured -p 9999)"
|
|
assert_eq "port 8888 is NOT secured" "false" "$(run_cli is-port-secured -p 8888)"
|
|
run_cli allow-ip-for-port -i 10.0.0.5 -p 9999 >/dev/null
|
|
in_ns iptables -A INPUT -p tcp -m multiport --dports 80,443 -j DROP
|
|
allowed=$(run_cli get-secured-ports-with-allowed-ips)
|
|
assert_eq "10.0.0.5 allowed for 9999" "10.0.0.5" "$(echo "$allowed" | jq -r '."9999"[]')"
|
|
assert_eq "multiport rule ignored" "[9999]" "$(run_cli get-secured-ports)"
|
|
run_cli unsecure-port -p 9999 >/dev/null
|
|
run_cli remove-allow-ip-port -i 10.0.0.5 -p 9999 >/dev/null
|
|
assert_eq "port 9999 unsecured" "false" "$(run_cli is-port-secured -p 9999)"
|
|
|
|
echo ""
|
|
echo "==> Test 5: cli list matches ipset list directly"
|
|
cli_count=$(run_cli get-banned-ips | jq 'length')
|
|
direct=$(( $(ipset_count banned) + $(ipset_count banned6) ))
|
|
assert_eq "cli reports same count as ipset" "$direct" "$cli_count"
|
|
|
|
echo ""
|
|
echo "==> Test 6: error on invalid IP"
|
|
output=$(run_cli ban-ip -i "not-an-ip" 2>/dev/null || true)
|
|
assert_eq "invalid IP rejected" "false" "$output"
|
|
assert_eq "invalid IP exits non-zero" "1" "$(run_cli ban-ip -i not-an-ip >/dev/null 2>&1; echo $?)"
|
|
|
|
echo ""
|
|
echo "==> Test 7: IPv6 goes to banned6 with an ip6tables rule"
|
|
output=$(run_cli ban-ip -i 2001:db8::7 --timeout 600)
|
|
assert_eq "ban IPv6" "true" "$output"
|
|
assert_eq "2001:db8::7 in banned6" "0" "$(in_ns ipset test banned6 2001:db8::7 2>/dev/null; echo $?)"
|
|
assert_eq "ip6tables match-set rule" "1" "$(in_ns ip6tables -S INPUT | grep -c 'match-set banned6 src -j DROP')"
|
|
assert_eq "IPv6 listed" "true" "$(run_cli get-banned-ips | jq 'index("2001:db8::7") != null')"
|
|
run_cli unban-ip -i 2001:db8::7 >/dev/null
|
|
assert_eq "IPv6 unbanned" "1" "$(in_ns ipset test banned6 2001:db8::7 2>/dev/null; echo $?)"
|
|
|
|
echo ""
|
|
echo "==> Test 8: save-ipsets writes a restorable file"
|
|
tmpfile=$(mktemp)
|
|
run_cli save-ipsets -f "$tmpfile" >/dev/null
|
|
assert_eq "saved banned" "1" "$(grep -c '^create banned hash:ip family inet ' "$tmpfile")"
|
|
assert_eq "saved banned6" "1" "$(grep -c '^create banned6 hash:ip family inet6 ' "$tmpfile")"
|
|
rm -f "$tmpfile"
|
|
|
|
echo ""
|
|
echo "================================================"
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
echo "================================================"
|
|
|
|
if [[ $FAIL -gt 0 ]]; then
|
|
exit 1
|
|
fi
|
|
|
|
echo "All integration tests passed."
|