🌈 Sync 2026-08-18 23:16:22

This commit is contained in:
github-actions[bot]
2026-08-18 23:16:22 +08:00
parent 4ab2511f2c
commit a9e51602b7
14 changed files with 1941 additions and 4 deletions
@@ -0,0 +1,46 @@
# AuthShield Configuration File
# Location: /etc/config/authshield
#
# This file configures AuthShield's intrusion prevention system for OpenWrt.
# AuthShield monitors failed login attempts for LuCI/rpcd and optionally Dropbear SSH,
# temporarily banning offending IPs and implementing circuit breaker protection.
#
# After modifying this file, reload the service:
# /etc/init.d/authshield reload
#
# Or configure via LuCI web interface at: System > AuthShield
config settings
# Main switch - disables all AuthShield functionality when set to '0'
option enabled '0'
# Basic ban settings
option threshold '5' # Failed attempts before IP ban
option window '10' # Time window in seconds for counting failures
option penalty '60' # Ban duration in seconds
option ports '80 443' # Protected ports (space-separated)
option watch_dropbear '0' # Monitor Dropbear SSH (0=no, 1=yes)
option ignore_private_ip '1' # Skip banning private/LAN IPs (0=no, 1=yes)
# Escalation settings (frequent offenders get longer bans)
option escalate_enable '1'
option escalate_threshold '5' # Number of bans within window to trigger escalation
option escalate_window '3600' # Escalation window (1 hour)
option escalate_penalty '86400' # Escalation ban duration (24 hours)
# Global rule settings (long-term tracking across all attempts)
option global_enable '1'
option global_threshold '60' # Failures within window that trigger global ban
option global_window '43200' # Global window (12 hours)
option global_penalty '86400' # Global ban duration (24 hours)
# Circuit breaker settings (blocks WAN access during mass attacks)
option circuit_enable '1'
option circuit_threshold '120' # Total failures across all IPs to trigger lockdown
option circuit_window '43200' # Circuit window (12 hours) - also acts as "memory"
option circuit_penalty '3600' # WAN block duration (1 hour) - unlocks automatically via nftables timeout
# Note: Circuit breaker automatically unlocks after circuit_penalty seconds.
# The circuit_window acts as a "memory" - if attackers resume attempts after unlock,
# and total failures still exceed circuit_threshold, the circuit will immediately re-lock.
# This provides extended protection without requiring manual intervention.
+317
View File
@@ -0,0 +1,317 @@
#!/bin/sh /etc/rc.common
# AuthShield init (procd)
# Enforces bans for LuCI/Dropbear by dropping at top of input_lan/input_wan.
# Generates /var/run/authshield.nft as a *script* include for fw4.
USE_PROCD=1
START=60
STOP=15
NAME=authshield
WATCH_BIN="/usr/sbin/authshield.sh"
FW_INCLUDE="/var/run/authshield.nft"
BAN_TRACK_FILE="/var/run/authshield.bans" # File storing ban history
CIRCUIT_STATUS_FILE="/var/run/authshield.circuit" # Circuit breaker state
SET_V4="authshield_penalty_v4"
SET_V6="authshield_penalty_v6"
SET_CIRCUIT="authshield_circuit_ports"
# ---- helpers ----
get_uciv() {
uci -q get "$1"
}
# Normalize a space/semicolon/comma separated port list into a sorted, deduped CSV (e.g. "80,443")
ports_to_csv() {
printf '%s\n' "$1" \
| tr ' ,;' '\n' \
| awk '
NF {
gsub(/[^0-9]/, "")
if ($0 != "") {
if (!seen[$0]++) {
if (out != "") out = out "," $0
else out = $0
}
}
}
END { print out }
'
}
# Compute effective ports (normalize + add 22 if Dropbear monitoring is enabled)
get_effective_ports() {
local ports watch_dropbear
ports="$(get_uciv authshield.@settings[0].ports)"
[ -n "$ports" ] || ports="80 443"
watch_dropbear="$(get_uciv authshield.@settings[0].watch_dropbear)"
[ -n "$watch_dropbear" ] || watch_dropbear=0
if [ "$watch_dropbear" = "1" ]; then
case " $ports " in
*" 22 "*) : ;;
*) ports="$ports 22" ;;
esac
fi
ports="$(ports_to_csv "$ports")"
echo "$ports"
}
ensure_fw_include() {
# ensure we have a firewall include pointing to $FW_INCLUDE with type 'script'
local found idx
found=0
idx=0
while :; do
local path type
path="$(get_uciv firewall.@include[$idx].path)" || break
type="$(get_uciv firewall.@include[$idx].type)"
if [ "$path" = "$FW_INCLUDE" ]; then
[ "$type" = "script" ] || uci set firewall.@include[$idx].type='script'
found=1
break
fi
idx=$((idx+1))
done
if [ "$found" -eq 0 ]; then
uci add firewall include >/dev/null
uci set firewall.@include[-1].type='script'
uci set firewall.@include[-1].path="$FW_INCLUDE"
fi
uci commit firewall
}
write_fw_include() {
# Read config from UCI (use @settings[0] consistently)
local ports penalty circuit_enable circuit_penalty
ports="$(get_effective_ports)"
penalty="$(get_uciv authshield.@settings[0].penalty)"
circuit_enable="$(get_uciv authshield.@settings[0].circuit_enable)"
circuit_penalty="$(get_uciv authshield.@settings[0].circuit_penalty)"
[ -n "$ports" ] || ports="80 443"
[ -n "$penalty" ] || penalty="60"
[ -n "$circuit_enable" ] || circuit_enable=0
[ -n "$circuit_penalty" ] || circuit_penalty=3600
local ports_csv
ports_csv="$ports"
cat > "$FW_INCLUDE" <<EOF
#!/bin/sh
# Auto-generated by /etc/init.d/authshield DO NOT EDIT.
set -eu
PORTS_CSV="$ports_csv"
PENALTY="$penalty"
SET_V4="$SET_V4"
SET_V6="$SET_V6"
SET_CIRCUIT="$SET_CIRCUIT"
CIRCUIT_ENABLE="$circuit_enable"
CIRCUIT_PENALTY="$circuit_penalty"
# Delete previous rules
del_old_rules() {
local chain="\$1"
nft -a list chain inet fw4 "\$chain" 2>/dev/null | \
awk '/@'\$SET_V4'/ || /@'\$SET_V6'/ && / dport / {print \$NF}' | \
tr -d ';' | while read -r h; do
[ -n "\$h" ] && nft delete rule inet fw4 "\$chain" handle "\$h" 2>/dev/null || true
done
}
# Delete circuit breaker rule from input_wan
del_circuit_rule() {
nft -a list chain inet fw4 input_wan 2>/dev/null | \
awk '/@'\$SET_CIRCUIT'/ && / dport / {print \$NF}' | \
tr -d ';' | while read -r h; do
[ -n "\$h" ] && nft delete rule inet fw4 input_wan handle "\$h" 2>/dev/null || true
done
}
# Ensure table exists (use shell to avoid nft parse errors)
if ! nft list table inet fw4 >/dev/null 2>&1; then
nft add table inet fw4
fi
# Remove any old rules we inserted in 'input' to avoid duplication
del_old_rules input || true
# Add sets with pure nft syntax (no shell redirects inside)
nft -f - <<NFE
add set inet fw4 \$SET_V4 { type ipv4_addr; flags timeout; timeout \${PENALTY}s; }
add set inet fw4 \$SET_V6 { type ipv6_addr; flags timeout; timeout \${PENALTY}s; }
NFE
# Insert the early-drop rules BEFORE conntrack established/related accept
nft insert rule inet fw4 input index 1 tcp dport {\${PORTS_CSV}} ip saddr @\${SET_V4} counter drop 2>/dev/null || true
nft insert rule inet fw4 input index 1 tcp dport {\${PORTS_CSV}} ip6 saddr @\${SET_V6} counter drop 2>/dev/null || true
# Circuit breaker setup (if enabled)
if [ "\$CIRCUIT_ENABLE" = "1" ]; then
# Remove old circuit breaker rule
del_circuit_rule || true
# Create circuit breaker port set with timeout capability
nft add set inet fw4 \$SET_CIRCUIT "{ type inet_service; flags timeout; timeout \${CIRCUIT_PENALTY}s; }" 2>/dev/null || true
# Add rule to input_wan that drops traffic when ports are in the circuit set
# Insert at index 1 to be before any accept rules
if nft list chain inet fw4 input_wan >/dev/null 2>&1; then
nft insert rule inet fw4 input_wan index 1 tcp dport @\${SET_CIRCUIT} counter drop 2>/dev/null || true
fi
fi
exit 0
EOF
chmod +x "$FW_INCLUDE"
}
regen_rules_and_reload_fw() {
write_fw_include
ensure_fw_include
/etc/init.d/firewall reload >/dev/null 2>&1 || true
}
kill_leftovers() {
# best-effort cleanup of stray watcher shells
local pids
pids="$(pgrep -f "$WATCH_BIN" 2>/dev/null || true)"
[ -z "$pids" ] && return 0
kill $pids 2>/dev/null || true
sleep 1
pids="$(pgrep -f "$WATCH_BIN" 2>/dev/null || true)"
[ -z "$pids" ] || kill -9 $pids 2>/dev/null || true
}
# Delete previous rules
del_old_rules() {
local chain="$1"
nft -a list chain inet fw4 "$chain" 2>/dev/null | awk '/@'$SET_V4'/ || /@'$SET_V6'/ && / dport / {print $NF}' | tr -d ';' | while read -r h; do
[ -n "$h" ] && nft delete rule inet fw4 "$chain" handle "$h" 2>/dev/null || true
done
}
# Remove circuit breaker rule
del_circuit_rule() {
nft -a list chain inet fw4 input_wan 2>/dev/null | \
awk '/@'$SET_CIRCUIT'/ && / dport / {print $NF}' | \
tr -d ';' | while read -r handle; do
[ -n "$handle" ] && nft delete rule inet fw4 input_wan handle "$handle" 2>/dev/null || true
done
}
# ---- procd lifecycle ----
start_service() {
local enabled threshold window penalty ports watch_dropbear ignore_private escalate_enable
local circuit_enable circuit_threshold circuit_window circuit_penalty
# Only when enabled do we continue
enabled="$(get_uciv authshield.@settings[0].enabled)"; [ -n "$enabled" ] || enabled=1
[ "$enabled" -eq 1 ] || return 0
# Generate/refresh nftables include & reload firewall
regen_rules_and_reload_fw
# Read watcher env from UCI (@settings[0] consistently)
threshold="$(get_uciv authshield.@settings[0].threshold)"; [ -n "$threshold" ] || threshold=5
window="$(get_uciv authshield.@settings[0].window)"; [ -n "$window" ] || window=10
penalty="$(get_uciv authshield.@settings[0].penalty)"; [ -n "$penalty" ] || penalty=60
global_enable="$(get_uciv authshield.@settings[0].global_enable)"; [ -n "$global_enable" ] || global_enable=1
global_threshold="$(get_uciv authshield.@settings[0].global_threshold)"; [ -n "$global_threshold" ] || global_threshold=60
global_window="$(get_uciv authshield.@settings[0].global_window)"; [ -n "$global_window" ] || global_window=43200
global_penalty="$(get_uciv authshield.@settings[0].global_penalty)"; [ -n "$global_penalty" ] || global_penalty=86400
ports="$(get_effective_ports)"
watch_dropbear="$(get_uciv authshield.@settings[0].watch_dropbear)"; [ -n "$watch_dropbear" ] || watch_dropbear=0
ignore_private="$(get_uciv authshield.@settings[0].ignore_private_ip)"; [ -n "$ignore_private" ] || ignore_private=1
escalate_enable="$(get_uciv authshield.@settings[0].escalate_enable)"; [ -n "$escalate_enable" ] || escalate_enable=1
escalate_threshold="$(get_uciv authshield.@settings[0].escalate_threshold)"; [ -n "$escalate_threshold" ] || escalate_threshold=5
escalate_window="$(get_uciv authshield.@settings[0].escalate_window)"; [ -n "$escalate_window" ] || escalate_window=3600
escalate_penalty="$(get_uciv authshield.@settings[0].escalate_penalty)"; [ -n "$escalate_penalty" ] || escalate_penalty=86400
# Circuit breaker settings
circuit_enable="$(get_uciv authshield.@settings[0].circuit_enable)"; [ -n "$circuit_enable" ] || circuit_enable=1
circuit_threshold="$(get_uciv authshield.@settings[0].circuit_threshold)"; [ -n "$circuit_threshold" ] || circuit_threshold=120
circuit_window="$(get_uciv authshield.@settings[0].circuit_window)"; [ -n "$circuit_window" ] || circuit_window=43200
circuit_penalty="$(get_uciv authshield.@settings[0].circuit_penalty)"; [ -n "$circuit_penalty" ] || circuit_penalty=3600
[ "$enabled" -eq 1 ] || return 0
procd_open_instance
procd_set_param command "$WATCH_BIN"
procd_set_param respawn 5 10 5 # (timeout, retry, max)
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param env THRESHOLD="$threshold" \
WINDOW="$window" \
PENALTY="$penalty" \
PORTS="$ports" \
WATCH_DROPBEAR="$watch_dropbear" \
IGNORE_PRIVATE="$ignore_private" \
ESCALATE_ENABLE="$escalate_enable" \
ESCALATE_THRESHOLD="$escalate_threshold" \
ESCALATE_WINDOW="$escalate_window" \
ESCALATE_PENALTY="$escalate_penalty" \
BAN_TRACK_FILE="$BAN_TRACK_FILE" \
GLOBAL_ENABLE="$global_enable" \
GLOBAL_THRESHOLD="$global_threshold" \
GLOBAL_WINDOW="$global_window" \
GLOBAL_PENALTY="$global_penalty" \
CIRCUIT_ENABLE="$circuit_enable" \
CIRCUIT_THRESHOLD="$circuit_threshold" \
CIRCUIT_WINDOW="$circuit_window" \
CIRCUIT_PENALTY="$circuit_penalty" \
CIRCUIT_STATUS_FILE="$CIRCUIT_STATUS_FILE" \
SET_V4="$SET_V4" \
SET_V6="$SET_V6"
procd_close_instance
}
stop_service() {
# procd will stop our instance; we just clean up stragglers
kill_leftovers || true
# remove the firewall include file only if it exists
[ -f "$FW_INCLUDE" ] && rm -f "$FW_INCLUDE"
# remove the BAN_TRACK_FILE only if it exists
[ -f "$BAN_TRACK_FILE" ] && rm -f "$BAN_TRACK_FILE"
# remove the CIRCUIT_STATUS_FILE only if it exists
[ -f "$CIRCUIT_STATUS_FILE" ] && rm -f "$CIRCUIT_STATUS_FILE"
# Remove any old rules we inserted in 'input'
del_old_rules input || true
# Remove circuit breaker rule from input_wan
del_circuit_rule || true
# delete all the sets we created
nft delete set inet fw4 "$SET_V4" 2>/dev/null || true
nft delete set inet fw4 "$SET_V6" 2>/dev/null || true
nft delete set inet fw4 "$SET_CIRCUIT" 2>/dev/null || true
}
reload_service() {
# Re-read UCI, rebuild procd instance, and relaunch with fresh env
stop
start
}
service_triggers() {
# Reload our service when authshield UCI changes
procd_add_reload_trigger "authshield"
}
# Convenience handler for rc.common `restart`
restart() {
stop
start
}
@@ -0,0 +1,30 @@
#!/bin/ash
# =====================================================================
# /etc/uci-defaults/99-authshield-setup
#
# One-time setup script for AuthShield
# Adds a clean "include" section to firewall4 for /var/run/authshield.nft
# Enables and starts the service, then removes itself.
# =====================================================================
set -e
# Only add include section if not already defined
if ! uci show firewall | grep -q "path='/var/run/authshield.nft'"; then
uci batch <<'EOF'
set firewall.authshield=include
set firewall.authshield.type='script'
set firewall.authshield.path='/var/run/authshield.nft'
set firewall.authshield.reload='1'
commit firewall
EOF
fi
# Reload firewall and enable AuthShield service
/etc/init.d/firewall reload || true
/etc/init.d/authshield enable || true
/etc/init.d/authshield restart || true
# Remove self after successful execution
rm -f /etc/uci-defaults/99-authshield-setup
exit 0
+344
View File
@@ -0,0 +1,344 @@
#!/bin/sh
#
# AuthShield lightweight intrusion prevention for OpenWrt
# Watches syslog for repeated failed logins (LuCI/rpcd, optionally Dropbear)
# and temporarily bans offending IPs using nftables set timeouts.
#
# Notes
# - No LuCI patching needed.
# - IPv4 & IPv6 supported via separate nft sets.
# - Private IPs (RFC1918/loopback/link-local/ULA) can be ignored.
# - Circuit breaker blocks WAN access when total failures exceed threshold.
# - Circuit breaker unlocks automatically via nftables timeout (no early unlock).
#
# ---------- Defaults ----------
WINDOW="${WINDOW:-10}" # Sliding window in seconds for counting failed logins
THRESHOLD="${THRESHOLD:-5}" # Number of failures within WINDOW before a ban
PENALTY="${PENALTY:-60}" # Ban duration in seconds
WATCH_DROPBEAR="${WATCH_DROPBEAR:-0}" # Monitor Dropbear SSH bad passwords (1 = enable)
# Global (long-window) rule defaults
GLOBAL_ENABLE="${GLOBAL_ENABLE:-1}" # Enable long-term global ban tracking
GLOBAL_THRESHOLD="${GLOBAL_THRESHOLD:-60}" # Failures allowed in long-term window
GLOBAL_WINDOW="${GLOBAL_WINDOW:-43200}" # Long-term window in seconds (12h)
GLOBAL_PENALTY="${GLOBAL_PENALTY:-86400}" # 24-hour ban duration for global threshold
IGNORE_PRIVATE="${IGNORE_PRIVATE:-1}" # Ignore local/private IP addresses
# nftables set references (table/chain are prepared by the init script)
SET_V4="${SET_V4:-authshield_penalty_v4}" # IPv4 penalty set name
SET_V6="${SET_V6:-authshield_penalty_v6}" # IPv6 penalty set name
SET_V4_PATH="inet fw4 $SET_V4" # Full path for IPv4 set
SET_V6_PATH="inet fw4 $SET_V6" # Full path for IPv6 set
# Escalation switch and params
ESCALATE_ENABLE="${ESCALATE_ENABLE:-1}" # Enable escalation tracking (1 = on)
ESCALATE_THRESHOLD="${ESCALATE_THRESHOLD:-5}" # Bans within window to trigger escalation
ESCALATE_WINDOW="${ESCALATE_WINDOW:-3600}" # Time window for escalation (1h)
ESCALATE_PENALTY="${ESCALATE_PENALTY:-86400}" # Escalation ban duration (24h)
BAN_TRACK_FILE="${BAN_TRACK_FILE:-/var/run/authshield.bans}" # File storing ban history
# Circuit breaker defaults
CIRCUIT_ENABLE="${CIRCUIT_ENABLE:-1}" # Enable circuit breaker (1 = on)
CIRCUIT_THRESHOLD="${CIRCUIT_THRESHOLD:-120}" # Total failures to trigger lockdown
CIRCUIT_WINDOW="${CIRCUIT_WINDOW:-43200}" # Time window for circuit breaker (12h)
CIRCUIT_PENALTY="${CIRCUIT_PENALTY:-3600}" # WAN block duration (1h)
CIRCUIT_STATUS_FILE="${CIRCUIT_STATUS_FILE:-/var/run/authshield.circuit}" # Circuit state
SET_CIRCUIT="authshield_circuit_ports" # Port set for circuit breaker
PORTS="${PORTS:-80,443}" # Management ports from init
# ---------- Helpers ----------
# Ensure both nft sets exist (exit if firewall isn't ready)
ensure_sets() {
nft list set $SET_V4_PATH >/dev/null 2>&1 || exit 1
nft list set $SET_V6_PATH >/dev/null 2>&1 || exit 1
}
# True if $1 is a private/loopback/link-local/ULA address
is_private_ip() {
case "$1" in
10.* | 192.168.* | 172.1[6-9].* | 172.2[0-9].* | 172.3[0-1].* | 127.* | ::1 | fe80:* | fd* | fc*)
return 0
;;
*)
return 1
;;
esac
}
# Add IP to the nft set with timeout = PENALTY
ban_ip() {
local ip="$1"
local override_dur="$2"
local reason="$3"
# Optionally skip private/local addresses
if [ "$IGNORE_PRIVATE" = "1" ] && is_private_ip "$ip"; then
return 0
fi
# Decide penalty
local dur
if [ -n "$override_dur" ]; then
dur="$override_dur"
elif [ "$ESCALATE_ENABLE" = "1" ]; then
dur="$(record_and_get_penalty "$ip" 2>/dev/null)" || dur="$PENALTY"
else
dur="$PENALTY"
fi
case "$ip" in
*:*) nft add element $SET_V6_PATH "{ $ip timeout ${dur}s }" 2>/dev/null ;; # IPv6
*) nft add element $SET_V4_PATH "{ $ip timeout ${dur}s }" 2>/dev/null ;; # IPv4
esac
case "$reason" in
"global>"*)
logger -t authshield "Global rule ban: $ip for ${dur}s (${reason})"
;;
*)
if [ "$ESCALATE_ENABLE" = "1" ] && [ "$dur" -ge "$ESCALATE_PENALTY" ]; then
logger -t authshield "Escalated ban: $ip for ${dur}s (> ${ESCALATE_THRESHOLD} bans within ${ESCALATE_WINDOW}s)"
else
logger -t authshield "Banned IP $ip for ${dur}s${reason:+ (reason: $reason)}"
fi
;;
esac
}
# Circuit breaker: populate port set with timeout (auto-expires)
circuit_lock() {
local total_count="${1:-0}" # Accept count as parameter
local chain="input_wan"
# Check if chain exists
if ! nft list chain inet fw4 "$chain" >/dev/null 2>&1; then
logger -t authshield "Warning: chain $chain not found, circuit breaker cannot activate"
return 1
fi
# Add all ports to the circuit breaker set with timeout
# Convert comma-separated ports to space-separated for iteration
local port_list
port_list=$(echo "$PORTS" | tr ',' ' ')
for port in $port_list; do
nft add element inet fw4 "$SET_CIRCUIT" "{ $port timeout ${CIRCUIT_PENALTY}s }" 2>/dev/null
done
local expires=$(($(date +%s) + CIRCUIT_PENALTY))
echo "1 $expires $total_count" > "$CIRCUIT_STATUS_FILE"
logger -t authshield "🔒 CIRCUIT BREAKER ACTIVATED: WAN ports {$PORTS} blocked for ${CIRCUIT_PENALTY}s (auto-expires)"
}
# Stream failed login events from syslog and print only the offending IPs (one per line)
stream_failures() {
# Keep logread -f on the left so awk sees a continuous stream.
logread -f | awk -v watchdb="$WATCH_DROPBEAR" '
# Emit the cleaned IP to stdout
function emit_ip(ip) {
gsub(/[,;]$/, "", ip) # strip trailing punctuation
sub(/:[0-9]+$/, "", ip) # strip trailing :port
if (ip != "") { print ip; fflush() }
}
# Scan fields and return the last token that looks like an IP(v4 or v6)
function last_ip_like( i, tok, ip) {
ip = ""
for (i = 1; i <= NF; i++) {
tok = $i
gsub(/^[\[\(]+|[\]\)]+$/, "", tok) # strip [ ( and ) ]
if (tok ~ /^([0-9]{1,3}\.){3}[0-9]{1,3}(:[0-9]+)?[,;]?$/) {
ip = tok
} else if (tok ~ /^[0-9a-fA-F:]+(%[0-9A-Za-z._-]+)?(:[0-9]+)?[,;]?$/) {
ip = tok
}
}
return ip
}
{
line = $0
# LuCI / rpcd / uhttpd failed login lines (case-insensitive on "login")
if (line ~ /(luci|rpcd|uhttpd)/ && line ~ /(fail|failed|bad)/ && line ~ /login/i) {
ip = last_ip_like()
if (ip != "") emit_ip(ip)
next
}
# Dropbear (enabled when watchdb=1)
# Match both "Bad password" and "Login attempt for nonexistent user"
if (watchdb == "1" && line ~ /dropbear/ && (line ~ /(Bad|bad).*password/ || line ~ /[Ll]ogin attempt for nonexistent user/)) {
ip = last_ip_like()
if (ip != "") emit_ip(ip)
next
}
}
'
}
# Sliding-window counter with circuit breaker support:
# - reads IPs (one per line) on stdin
# - bans IP once it has THRESHOLD events within WINDOW seconds
# - tracks total failures for circuit breaker (respects IGNORE_PRIVATE setting)
monitor_and_ban() {
awk -v WIN="$WINDOW" -v TH="$THRESHOLD" \
-v GWIN="$GLOBAL_WINDOW" -v GTH="$GLOBAL_THRESHOLD" -v GEN="$GLOBAL_ENABLE" \
-v CWIN="$CIRCUIT_WINDOW" -v CTH="$CIRCUIT_THRESHOLD" -v CEN="$CIRCUIT_ENABLE" \
-v IGNORE_PRIV="$IGNORE_PRIVATE" '
function now() { return systime() }
# Check if IP is private/loopback/link-local/ULA
function is_private(ip) {
if (ip ~ /^10\./ || ip ~ /^192\.168\./ || ip ~ /^172\.(1[6-9]|2[0-9]|3[0-1])\./ || \
ip ~ /^127\./ || ip == "::1" || ip ~ /^fe80:/ || ip ~ /^fd/ || ip ~ /^fc/) {
return 1
}
return 0
}
# Short-window state (per-IP)
function spush(ts, ip) { SWN[ip]++; SWT[ip "_" SWN[ip]] = ts }
function sprune(ts, ip, n, m, i, t) {
n = SWN[ip]; m = 0
for (i = 1; i <= n; i++) {
t = SWT[ip "_" i]
if (ts - t <= WIN) { m++; SWT[ip "_" m] = t }
}
SWN[ip] = m
}
# Long-window state (per-IP for global rule)
function lpush(ts, ip) { LGN[ip]++; LGT[ip "_" LGN[ip]] = ts }
function lprune(ts, ip, n, m, i, t) {
n = LGN[ip]; m = 0
for (i = 1; i <= n; i++) {
t = LGT[ip "_" i]
if (ts - t <= GWIN) { m++; LGT[ip "_" m] = t }
}
LGN[ip] = m
}
# Circuit breaker: total failures across all IPs (respects IGNORE_PRIV)
function cpush(ts) { CN++; CT[CN] = ts }
function cprune(ts, n, m, i, t) {
n = CN; m = 0
for (i = 1; i <= n; i++) {
t = CT[i]
if (ts - t <= CWIN) { m++; CT[m] = t }
}
CN = m
return CN
}
# Main stream processing
{
ip = $0
t = now()
# Check if IP should be ignored
skip_ip = (IGNORE_PRIV == "1" && is_private(ip)) ? 1 : 0
# Update per-IP counters
sprune(t, ip); spush(t, ip)
lprune(t, ip); lpush(t, ip)
# Update circuit breaker total counter (skip private IPs if IGNORE_PRIV is enabled)
if (CEN == 1) {
if (!skip_ip) {
cpush(t)
}
total = cprune(t)
# Check if circuit threshold exceeded
if (total > CTH) {
print "CIRCUIT_LOCK " total
fflush()
}
}
# Per-IP ban logic
if (SWN[ip] >= TH) {
print "BAN " ip
SWN[ip] = 0 # reset only short window; keep long window for global rule
fflush()
} else if (GEN == 1) {
if (LGN[ip] > GTH) { # strictly greater-than (e.g., >60)
print "BAN24 " ip
fflush()
}
}
}
'
}
# Record the ban for $ip, prune old records, and return the effective penalty (seconds)
record_and_get_penalty() {
local ip="$1"
local now cutoff tmp count
now="$(date +%s)"
cutoff=$(( now - ESCALATE_WINDOW ))
tmp="/var/run/authshield.bans.$$"
mkdir -p /var/run
touch "$BAN_TRACK_FILE"
count="$(awk -v cutoff="$cutoff" -v ip="$ip" -v out="$tmp" '
$1 >= cutoff { print > out; if ($2 == ip) c++ }
END { print (c ? c : 0) }
' "$BAN_TRACK_FILE")"
mv -f "$tmp" "$BAN_TRACK_FILE" 2>/dev/null || true
printf "%s %s\n" "$now" "$ip" >> "$BAN_TRACK_FILE"
if [ $(( count + 1 )) -gt "$ESCALATE_THRESHOLD" ]; then
printf "%s\n" "$ESCALATE_PENALTY"
else
printf "%s\n" "$PENALTY"
fi
}
# ---------- Main ----------
main() {
ensure_sets || { echo "authshield: nft sets missing" >&2; exit 1; }
# Initialize circuit status file if needed
if [ "$CIRCUIT_ENABLE" = "1" ] && [ ! -f "$CIRCUIT_STATUS_FILE" ]; then
echo "0 0 0" > "$CIRCUIT_STATUS_FILE"
fi
# Pipeline:
# [ logread -f → awk (IPs) ] | [ awk sliding window ] | [ shell loop → ban_ip ]
stream_failures | monitor_and_ban | while read -r action value; do
case "$action" in
BAN)
# No override so escalation can apply when enabled
ban_ip "$value" "" "threshold/${THRESHOLD}@${WINDOW}s"
;;
BAN24)
# Explicit override to always apply the global rule duration
ban_ip "$value" "$GLOBAL_PENALTY" "global>${GLOBAL_THRESHOLD}@${GLOBAL_WINDOW}s"
;;
CIRCUIT_LOCK)
if [ "$CIRCUIT_ENABLE" = "1" ]; then
# Check if already locked
local locked=0
if [ -f "$CIRCUIT_STATUS_FILE" ]; then
read locked _ _ < "$CIRCUIT_STATUS_FILE"
fi
if [ "$locked" != "1" ]; then
circuit_lock "$value" # Pass the failure count
fi
fi
;;
esac
done
}
main
@@ -0,0 +1,11 @@
{
"luci-app-authshield": {
"description": "Grant UCI access for luci-app-authshield",
"read": {
"uci": [ "authshield" ]
},
"write": {
"uci": [ "authshield" ]
}
}
}