266 lines
9.3 KiB
Bash
266 lines
9.3 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# ============================================
|
|
# Lifecycle
|
|
# ============================================
|
|
|
|
function fw::on_load() {
|
|
system::require_command iptables
|
|
}
|
|
|
|
# ============================================
|
|
# Rule Management
|
|
# ============================================
|
|
|
|
function fw::block_ip() {
|
|
local client_ip="${1:-}" target_ip="${2:-}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -j DROP \
|
|
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -j DROP
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \
|
|
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:"
|
|
}
|
|
|
|
function fw::unblock_ip() {
|
|
local client_ip="${1:-}" target_ip="${2:-}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \
|
|
&& iptables -D FORWARD -s "$client_ip" -d "$target_ip" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" 2>/dev/null || true
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -j DROP \
|
|
&& iptables -D FORWARD -s "$client_ip" -d "$target_ip" -j DROP 2>/dev/null || true
|
|
}
|
|
|
|
function fw::block_port() {
|
|
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP \
|
|
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \
|
|
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:"
|
|
|
|
}
|
|
|
|
function fw::unblock_port() {
|
|
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \
|
|
&& iptables -D FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" 2>/dev/null || true
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP \
|
|
&& iptables -D FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP 2>/dev/null || true
|
|
}
|
|
|
|
function fw::block_all() {
|
|
local client_ip="${1:-}" client_name="${2:-}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -j DROP \
|
|
|| iptables -A FORWARD -s "$client_ip" -j DROP
|
|
|
|
log::debug "Blocked all traffic from: ${client_ip}"
|
|
}
|
|
|
|
function fw::unblock_all() {
|
|
local client_ip="${1:-}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -j DROP \
|
|
&& iptables -D FORWARD -s "$client_ip" -j DROP 2>/dev/null || true
|
|
|
|
monitor::unwatch "$client_ip"
|
|
log::debug "Unblocked all traffic from: ${client_ip}"
|
|
}
|
|
|
|
function fw::block_subnet() {
|
|
local client_ip="${1:-}" target_subnet="${2:-}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \
|
|
|| iptables -A FORWARD -s "$client_ip" -d "$target_subnet" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j DROP \
|
|
|| iptables -A FORWARD -s "$client_ip" -d "$target_subnet" -j DROP
|
|
|
|
log::wg_block "Blocked ${client_ip} → subnet ${target_subnet}"
|
|
}
|
|
|
|
function fw::unblock_subnet() {
|
|
local client_ip="${1:-}" target_subnet="${2:-}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \
|
|
&& iptables -D FORWARD -s "$client_ip" -d "$target_subnet" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" 2>/dev/null || true
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j DROP \
|
|
&& iptables -D FORWARD -s "$client_ip" -d "$target_subnet" -j DROP 2>/dev/null || true
|
|
|
|
log::wg_unblock "Unblocked ${client_ip} → subnet ${target_subnet}"
|
|
}
|
|
|
|
function fw::allow_ip() {
|
|
local client_ip="${1:-}" target_ip="${2:-}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -j ACCEPT \
|
|
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -j ACCEPT
|
|
}
|
|
|
|
function fw::unallow_ip() {
|
|
local client_ip="${1:-}" target_ip="${2:-}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -j ACCEPT \
|
|
&& iptables -D FORWARD -s "$client_ip" -d "$target_ip" -j ACCEPT 2>/dev/null || true
|
|
}
|
|
|
|
function fw::allow_port() {
|
|
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT \
|
|
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT
|
|
}
|
|
|
|
function fw::unallow_port() {
|
|
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}"
|
|
|
|
fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT \
|
|
&& iptables -D FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null || true
|
|
}
|
|
|
|
function fw::flush_peer() {
|
|
local client_ip="${1:?client_ip required}"
|
|
log::debug "flush_peer: starting for $client_ip"
|
|
|
|
# Collect line numbers into array
|
|
local linenums=()
|
|
while IFS= read -r linenum; do
|
|
[[ -n "$linenum" ]] && linenums+=("$linenum")
|
|
done < <(iptables -L FORWARD -n --line-numbers | grep -F "$client_ip" | awk '{print $1}')
|
|
|
|
# Delete in reverse order (highest number first)
|
|
local count=0
|
|
local i
|
|
for (( i=${#linenums[@]}-1; i>=0; i-- )); do
|
|
iptables -D FORWARD "${linenums[$i]}" 2>/dev/null || true
|
|
(( count++ )) || true
|
|
done
|
|
|
|
log::debug "flush_peer: removed $count FORWARD rules for: $client_ip"
|
|
}
|
|
|
|
# ============================================
|
|
# Guest Subnet Rules
|
|
# ============================================
|
|
|
|
function fw::apply_dns_redirect() {
|
|
if iptables -t nat -C PREROUTING -s "$(config::subnet_for guest).0/24" -p udp --dport 53 -j DNAT --to-destination "$(config::dns):53" 2>/dev/null; then
|
|
log::wg "Guest DNS redirect already applied"
|
|
return 0
|
|
fi
|
|
|
|
local guest_subnet dns
|
|
guest_subnet="$(config::subnet_for "guest").0/24"
|
|
dns="$(config::dns)"
|
|
|
|
# Log DNS bypass attempts (queries not directed at Pi-hole)
|
|
iptables -t nat -A PREROUTING -s "$guest_subnet" -p udp --dport 53 \
|
|
! -d "$dns" \
|
|
-j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4
|
|
iptables -t nat -A PREROUTING -s "$guest_subnet" -p tcp --dport 53 \
|
|
! -d "$dns" \
|
|
-j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4
|
|
|
|
# Redirect all DNS to Pi-hole
|
|
iptables -t nat -A PREROUTING -s "$guest_subnet" -p udp --dport 53 -j DNAT --to-destination "${dns}:53"
|
|
iptables -t nat -A PREROUTING -s "$guest_subnet" -p tcp --dport 53 -j DNAT --to-destination "${dns}:53"
|
|
|
|
log::wg_block "Guest DNS redirected to Pi-hole (${dns}), bypass attempts will be logged"
|
|
}
|
|
|
|
function fw::remove_dns_redirect() {
|
|
local guest_subnet dns
|
|
guest_subnet="$(config::subnet_for "guest").0/24"
|
|
dns="$(config::dns)"
|
|
|
|
iptables -t nat -D PREROUTING -s "$guest_subnet" -p udp --dport 53 \
|
|
! -d "$dns" \
|
|
-j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4 2>/dev/null || true
|
|
iptables -t nat -D PREROUTING -s "$guest_subnet" -p tcp --dport 53 \
|
|
! -d "$dns" \
|
|
-j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4 2>/dev/null || true
|
|
iptables -t nat -D PREROUTING -s "$guest_subnet" -p udp --dport 53 -j DNAT --to-destination "${dns}:53" 2>/dev/null || true
|
|
iptables -t nat -D PREROUTING -s "$guest_subnet" -p tcp --dport 53 -j DNAT --to-destination "${dns}:53" 2>/dev/null || true
|
|
|
|
log::debug "Removed guest DNS redirect"
|
|
}
|
|
|
|
# ============================================
|
|
# Persistence — block files
|
|
# ============================================
|
|
|
|
function fw::save_block() {
|
|
local name="$1"
|
|
local client_ip="$2"
|
|
local target="${3:-}"
|
|
local port="${4:-}"
|
|
local proto="${5:-}"
|
|
|
|
local block_file
|
|
block_file="$(ctx::block::path "${name}.block")"
|
|
|
|
echo "${client_ip} ${target} ${port} ${proto}" >> "$block_file"
|
|
log::debug "Persisted block rule for: ${name}"
|
|
}
|
|
|
|
function fw::remove_block_file() {
|
|
local name="$1"
|
|
local block_file
|
|
block_file="$(ctx::block::path "${name}.block")"
|
|
|
|
rm -f "$block_file"
|
|
log::debug "Removed block file for: ${name}"
|
|
}
|
|
|
|
function fw::restore_blocks() {
|
|
local blocks_dir
|
|
blocks_dir="$(ctx::blocks)"
|
|
|
|
# Restore rules from meta files (new system)
|
|
rule::restore_all
|
|
|
|
# Restore per-client full-blocks (wgctl block/unblock system)
|
|
for block_file in "${blocks_dir}"/*.block; do
|
|
[[ -f "$block_file" ]] || continue
|
|
local name
|
|
name=$(basename "$block_file" .block)
|
|
while IFS=" " read -r client_ip target port proto; do
|
|
if [[ -z "$target" ]]; then
|
|
fw::block_all "$client_ip" "$name"
|
|
elif [[ -n "$port" ]]; then
|
|
fw::block_port "$client_ip" "$target" "$port" "${proto:-tcp}"
|
|
else
|
|
fw::block_ip "$client_ip" "$target"
|
|
fi
|
|
done < "$block_file"
|
|
log::debug "Restored block rules for: ${name}"
|
|
done
|
|
}
|
|
|
|
# ============================================
|
|
# Helpers
|
|
# ============================================
|
|
|
|
function fw::_nat_exists() {
|
|
fw::_rule_exists nat PREROUTING "$@"
|
|
}
|
|
|
|
# ============================================
|
|
# private
|
|
# ============================================
|
|
|
|
function fw::_rule_exists() {
|
|
local table="${1:-filter}" chain="${2:-FORWARD}"
|
|
shift 2
|
|
iptables -t "$table" -C "$chain" "$@" 2>/dev/null
|
|
}
|
|
|
|
function fw::_forward_exists() {
|
|
iptables -C FORWARD "$@" 2>/dev/null
|
|
}
|