308 lines
No EOL
9.5 KiB
Bash
308 lines
No EOL
9.5 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# ============================================
|
|
# Lifecycle
|
|
# ============================================
|
|
|
|
function firewall::on_load() {
|
|
system::require_command iptables
|
|
}
|
|
|
|
# ============================================
|
|
# Rule Management
|
|
# ============================================
|
|
|
|
function firewall::block_ip() {
|
|
local client_ip="$1"
|
|
local target_ip="$2"
|
|
|
|
iptables -A FORWARD -s "$client_ip" -d "$target_ip" -j LOG --log-prefix "wgctl-dropped: " --log-level 4
|
|
iptables -A FORWARD -s "$client_ip" -d "$target_ip" -j DROP
|
|
log::wg_block "Blocked ${client_ip} → ${target_ip}"
|
|
}
|
|
|
|
function firewall::unblock_ip() {
|
|
local client_ip="$1"
|
|
local target_ip="$2"
|
|
|
|
iptables -D FORWARD -s "$client_ip" -d "$target_ip" -j LOG --log-prefix "wgctl-dropped: " --log-level 4 2>/dev/null || true
|
|
iptables -D FORWARD -s "$client_ip" -d "$target_ip" -j DROP 2>/dev/null || true
|
|
log::wg_unblock "Unblocked ${client_ip} → ${target_ip}"
|
|
}
|
|
|
|
function firewall::block_port() {
|
|
local client_ip="$1"
|
|
local target_ip="$2"
|
|
local port="$3"
|
|
local proto="${4:-tcp}"
|
|
|
|
iptables -A FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP
|
|
log::wg_block "Blocked ${client_ip} → ${target_ip}:${port}/${proto}"
|
|
}
|
|
|
|
function firewall::unblock_port() {
|
|
local client_ip="$1"
|
|
local target_ip="$2"
|
|
local port="$3"
|
|
local proto="${4:-tcp}"
|
|
|
|
iptables -D FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP 2>/dev/null || true
|
|
log::wg_unblock "Unblocked ${client_ip} → ${target_ip}:${port}/${proto}"
|
|
}
|
|
|
|
function firewall::block_all() {
|
|
local client_ip="$1"
|
|
local client_name="$2"
|
|
|
|
iptables -A FORWARD -s "$client_ip" -j LOG --log-prefix "wgctl-dropped: " --log-level 4
|
|
iptables -A FORWARD -s "$client_ip" -j DROP
|
|
|
|
log::wg_block "Blocked all traffic from: ${client_ip}"
|
|
}
|
|
|
|
function firewall::unblock_all() {
|
|
local client_ip="$1"
|
|
|
|
iptables -D FORWARD -s "$client_ip" -j LOG --log-prefix "wgctl-dropped: " --log-level 4 2>/dev/null || true
|
|
iptables -D FORWARD -s "$client_ip" -j DROP 2>/dev/null || true
|
|
|
|
monitor::unwatch "$client_ip"
|
|
log::wg_unblock "Unblocked all traffic from: ${client_ip}"
|
|
}
|
|
|
|
function firewall::block_subnet() {
|
|
local client_ip="$1"
|
|
local target_subnet="$2"
|
|
|
|
iptables -A FORWARD -s "$client_ip" -d "$target_subnet" -j DROP
|
|
log::wg_block "Blocked ${client_ip} → subnet ${target_subnet}"
|
|
}
|
|
|
|
function firewall::unblock_subnet() {
|
|
local client_ip="$1"
|
|
local target_subnet="$2"
|
|
|
|
iptables -D FORWARD -s "$client_ip" -d "$target_subnet" -j DROP 2>/dev/null || true
|
|
log::wg_unblock "Unblocked ${client_ip} → subnet ${target_subnet}"
|
|
}
|
|
|
|
# ============================================
|
|
# Guest Subnet Rules
|
|
# ============================================
|
|
|
|
# Sensitive services blocked for all guest peers
|
|
declare -ga GUEST_BLOCKED_SERVICES=(
|
|
"10.0.0.100:8006:tcp" # Proxmox UI
|
|
"10.0.0.100:22:tcp" # Proxmox SSH
|
|
"10.0.0.105:8007:tcp" # PBS UI
|
|
"10.0.0.102:22:tcp" # WireGuard LXC SSH
|
|
"10.0.0.200:80:tcp" # TrueNAS UI HTTP
|
|
"10.0.0.200:443:tcp" # TrueNAS UI HTTPS
|
|
"10.0.0.103:80:tcp" # Pi-hole WebUI
|
|
"10.0.0.101:80:tcp" # NPM WebUI HTTP
|
|
"10.0.0.101:443:tcp" # NPM WebUI HTTPS
|
|
"10.0.0.210:9000:tcp" # Portainer direct port
|
|
)
|
|
|
|
function firewall::guest_rules_applied() {
|
|
local guest_subnet
|
|
guest_subnet="$(config::subnet_for "guest").0/24"
|
|
# Check if at least the first rule exists
|
|
local first_entry="${GUEST_BLOCKED_SERVICES[0]}"
|
|
local target port proto
|
|
IFS=":" read -r target port proto <<< "$first_entry"
|
|
proto="${proto:-tcp}"
|
|
iptables -C FORWARD -s "$guest_subnet" -d "$target" -p "$proto" --dport "$port" -j DROP 2>/dev/null
|
|
}
|
|
|
|
function firewall::apply_guest_rules() {
|
|
local guest_subnet
|
|
guest_subnet="$(config::subnet_for "guest").0/24"
|
|
|
|
# Skip if already applied
|
|
if firewall::guest_rules_applied; then
|
|
log::wg "Guest firewall rules already applied"
|
|
return 0
|
|
fi
|
|
|
|
for entry in "${GUEST_BLOCKED_SERVICES[@]}"; do
|
|
local target port proto
|
|
IFS=":" read -r target port proto <<< "$entry"
|
|
proto="${proto:-tcp}"
|
|
iptables -I FORWARD 1 -s "$guest_subnet" -d "$target" -p "$proto" --dport "$port" -j DROP
|
|
log::wg_block "Guest rule: blocked ${guest_subnet} → ${target}:${port}/${proto}"
|
|
done
|
|
|
|
# Persist guest rules marker
|
|
local marker
|
|
marker="$(ctx::blocks)/_guest_rules.active"
|
|
touch "$marker"
|
|
|
|
log::wg_block "Applied guest firewall rules"
|
|
firewall::apply_guest_dns_redirect
|
|
}
|
|
|
|
function firewall::remove_guest_rules() {
|
|
local guest_subnet
|
|
guest_subnet="$(config::subnet_for "guest").0/24"
|
|
|
|
for entry in "${GUEST_BLOCKED_SERVICES[@]}"; do
|
|
local target port proto
|
|
IFS=":" read -r target port proto <<< "$entry"
|
|
proto="${proto:-tcp}"
|
|
iptables -D FORWARD -s "$guest_subnet" -d "$target" -p "$proto" --dport "$port" -j DROP 2>/dev/null || true
|
|
done
|
|
|
|
# Remove persistence marker
|
|
local marker
|
|
marker="$(ctx::blocks)/_guest_rules.active"
|
|
rm -f "$marker"
|
|
|
|
log::wg_unblock "Removed guest firewall rules"
|
|
firewall::remove_guest_dns_redirect
|
|
}
|
|
|
|
function firewall::apply_guest_dns_redirect() {
|
|
if iptables -t nat -C PREROUTING -i wg0 -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 -i wg0 -s "$guest_subnet" -p udp --dport 53 \
|
|
! -d "$dns" \
|
|
-j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4
|
|
iptables -t nat -A PREROUTING -i wg0 -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 -i wg0 -s "$guest_subnet" -p udp --dport 53 -j DNAT --to-destination "${dns}:53"
|
|
iptables -t nat -A PREROUTING -i wg0 -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 firewall::remove_guest_dns_redirect() {
|
|
local guest_subnet dns
|
|
guest_subnet="$(config::subnet_for "guest").0/24"
|
|
dns="$(config::dns)"
|
|
|
|
iptables -t nat -D PREROUTING -i wg0 -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 -i wg0 -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 -i wg0 -s "$guest_subnet" -p udp --dport 53 -j DNAT --to-destination "${dns}:53" 2>/dev/null || true
|
|
iptables -t nat -D PREROUTING -i wg0 -s "$guest_subnet" -p tcp --dport 53 -j DNAT --to-destination "${dns}:53" 2>/dev/null || true
|
|
|
|
log::wg_unblock "Removed guest DNS redirect"
|
|
}
|
|
|
|
# ============================================
|
|
# Persistence — block files
|
|
# ============================================
|
|
|
|
function firewall::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::wg_block "Persisted block rule for: ${name}"
|
|
}
|
|
|
|
function firewall::remove_block_file() {
|
|
local name="$1"
|
|
local block_file
|
|
block_file="$(ctx::block::path "${name}.block")"
|
|
|
|
rm -f "$block_file"
|
|
log::wg_unblock "Removed block file for: ${name}"
|
|
}
|
|
|
|
function firewall::restore_blocks() {
|
|
local blocks_dir
|
|
blocks_dir="$(ctx::blocks)"
|
|
|
|
# Restore guest rules if marker exists
|
|
local marker="${blocks_dir}/_guest_rules.active"
|
|
if [[ -f "$marker" ]]; then
|
|
firewall::apply_guest_rules
|
|
log::wg "Restored guest firewall rules"
|
|
fi
|
|
|
|
# Restore per-client block rules
|
|
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
|
|
firewall::block_all "$client_ip" "$name"
|
|
elif [[ -n "$port" ]]; then
|
|
firewall::block_port "$client_ip" "$target" "$port" "${proto:-tcp}"
|
|
else
|
|
firewall::block_ip "$client_ip" "$target"
|
|
fi
|
|
done < "$block_file"
|
|
|
|
log::wg "Restored block rules for: ${name}"
|
|
done
|
|
}
|
|
|
|
# ============================================
|
|
# Preset Application
|
|
# ============================================
|
|
|
|
function firewall::apply_preset() {
|
|
local name="$1"
|
|
local client_ip="$2"
|
|
local preset_file
|
|
preset_file="$(ctx::preset::path "${name}.preset")"
|
|
|
|
if [[ ! -f "$preset_file" ]]; then
|
|
log::error "Preset not found: ${name}"
|
|
return 1
|
|
fi
|
|
|
|
source "$preset_file"
|
|
|
|
if [[ -n "${BLOCK_IPS:-}" ]]; then
|
|
for ip in $BLOCK_IPS; do
|
|
firewall::block_ip "$client_ip" "$ip"
|
|
firewall::save_block "$client_ip" "$client_ip" "$ip"
|
|
done
|
|
fi
|
|
|
|
if [[ -n "${BLOCK_SUBNETS:-}" ]]; then
|
|
for subnet in $BLOCK_SUBNETS; do
|
|
firewall::block_subnet "$client_ip" "$subnet"
|
|
firewall::save_block "$client_ip" "$client_ip" "$subnet"
|
|
done
|
|
fi
|
|
|
|
if [[ -n "${BLOCK_PORTS:-}" ]]; then
|
|
for entry in $BLOCK_PORTS; do
|
|
local target port proto
|
|
IFS=":" read -r target port proto <<< "$entry"
|
|
proto="${proto:-tcp}"
|
|
firewall::block_port "$client_ip" "$target" "$port" "$proto"
|
|
firewall::save_block "$name" "$client_ip" "$target" "$port" "$proto"
|
|
done
|
|
fi
|
|
|
|
log::wg_preset "Applied preset '${name}' to: ${client_ip}"
|
|
} |