Files
op-packages/luci-app-authshield/root/etc/init.d/authshield
T

318 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
}