diff --git a/.gitignore b/.gitignore index f10090c..2b9fa53 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ Cargo.lock *.xlines *.metadata *.reputation +bin/ diff --git a/deploy/firewalld-echoircd.sh b/deploy/firewalld-echoircd.sh new file mode 100755 index 0000000..b40a432 --- /dev/null +++ b/deploy/firewalld-echoircd.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# echoIRCd kernel-layer flood mitigation — defense-in-depth on top of the ircd's own +# per-IP `accept_rate` limiter. Drops excess NEW connections to the IRC client ports +# (per source IP) in the kernel, before they ever reach the daemon. +# +# THIS BOX RUNS firewalld, so the rule is installed through firewalld's DIRECT interface +# (`firewall-cmd --direct`). firewalld then owns the rule — it won't be flushed on a +# `firewall-cmd --reload`, and `--permanent` persists it across reboot. (A raw +# `iptables -I INPUT ...` would be silently wiped the next time firewalld rebuilds its +# ruleset, which is why that approach is wrong here.) +# +# SAFE: it only DROPs the rate-limited *excess* to 6767/6770. It changes no zone, +# service, or default policy; ports 6767/6770 stay open as firewalld already has them. +# Loopback is exempt (the liveness probe + local tools). Idempotent and one-command +# removable. +# +# Apply: sudo deploy/firewalld-echoircd.sh add +# Remove: sudo deploy/firewalld-echoircd.sh del +# Show: sudo deploy/firewalld-echoircd.sh show +# +# Uses iptables `hashlimit` (per-source-IP). firewalld rich rules can't express this — +# their `limit` is a single global token bucket, not per-source — so a direct rule is +# the right tool. IPv4 only (the ircd binds 0.0.0.0); add an ipv6 rule if you ever bind ::. +set -u + +PORTS="6767,6770" # IRC client ports (plaintext + TLS); S2S 7700 intentionally left alone +RATE="30/second" # sustained NEW connections/sec per source IP +BURST="60" # instantaneous burst allowed per source IP +ACTION="${1:-}" + +# the raw rule (added into INPUT at priority 0 = top): drop NEW conns to the IRC ports +# from any single source IP that exceeds the rate. `! -i lo` exempts loopback. +RULE=(ipv4 filter INPUT 0 + ! -i lo -p tcp -m multiport --dports "$PORTS" + -m conntrack --ctstate NEW + -m hashlimit --hashlimit-name ircrate --hashlimit-mode srcip + --hashlimit-above "$RATE" --hashlimit-burst "$BURST" + -j DROP) + +add_one() { firewall-cmd "$@" --direct --add-rule "${RULE[@]}"; } +del_one() { firewall-cmd "$@" --direct --remove-rule "${RULE[@]}" >/dev/null 2>&1 || true; } + +case "$ACTION" in + add) + del_one # runtime: clear any prior copy (idempotent) + del_one --permanent + add_one # runtime (takes effect now) + add_one --permanent # persists across reboot / reload + echo "echoircd rate-limit added via firewalld direct rule: ports ${PORTS}, ${RATE} burst ${BURST} per source IP (runtime + permanent)" + ;; + del) + del_one + del_one --permanent + echo "echoircd rate-limit removed (runtime + permanent)" + ;; + show) + echo "== runtime direct rules =="; firewall-cmd --direct --get-all-rules + echo "== permanent direct rules =="; firewall-cmd --permanent --direct --get-all-rules + ;; + *) + echo "usage: $0 {add|del|show}" >&2 + exit 1 + ;; +esac