wgctl/modules/firewall.module.sh

328 lines
9.5 KiB
Bash

#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function fw::on_load() {
system::require_command iptables
}
# ============================================
# Rule Management
# ============================================
# ============================================
# Public Interfaces
function fw::has_rule() {
# Generic rule existence check
# fw::has_rule <target> <client_ip> <dest> [port] [proto]
local action="${1:-DROP}" client_ip="${2:-}" target="${3:-}" \
port="${4:-}" proto="${5:-}"
if [[ -n "$port" ]]; then
fw::_forward_exists -s "$client_ip" -d "$target" \
-p "$proto" --dport "$port" -j "$action"
else
fw::_forward_exists -s "$client_ip" -d "$target" -j "$action"
fi
}
function fw::has_block_rule() {
fw::has_rule "DROP" "$@"
}
function fw::has_allow_rule() {
fw::has_rule "ACCEPT" "$@"
}
# ============================================
# Block / Unblock
function fw::block_ip() {
local client_ip="${1:-}" target_ip="${2:-}" mode="${3:-insert}"
fw::_block_pair "$mode" -s "$client_ip" -d "$target_ip"
}
function fw::unblock_ip() {
local client_ip="${1:-}" target_ip="${2:-}"
fw::_unblock_pair -s "$client_ip" -d "$target_ip"
}
function fw::block_port() {
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" \
proto="${4:-tcp}" mode="${5:-insert}"
fw::_block_pair "$mode" \
-s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port"
}
function fw::unblock_port() {
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}"
fw::_unblock_pair \
-s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port"
}
function fw::block_subnet() {
local client_ip="${1:-}" target_subnet="${2:-}" mode="${3:-append}"
fw::_block_pair "$mode" -s "$client_ip" -d "$target_subnet"
log::wg_block "Blocked ${client_ip} → subnet ${target_subnet}"
}
function fw::unblock_subnet() {
local client_ip="${1:-}" target_subnet="${2:-}"
fw::_unblock_pair -s "$client_ip" -d "$target_subnet"
log::wg_unblock "Unblocked ${client_ip} → subnet ${target_subnet}"
}
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}"
}
# ============================================
# Allow / Unallow
function fw::allow_ip() {
local client_ip="${1:-}" target_ip="${2:-}"
fw::_accept_insert -s "$client_ip" -d "$target_ip"
}
function fw::unallow_ip() {
local client_ip="${1:-}" target_ip="${2:-}"
fw::_accept_remove -s "$client_ip" -d "$target_ip"
}
function fw::allow_subnet() {
local client_ip="${1:-}" target_subnet="${2:-}"
fw::_accept_insert -s "$client_ip" -d "$target_subnet"
}
function fw::unallow_subnet() {
local client_ip="${1:-}" target_subnet="${2:-}"
fw::_accept_remove -s "$client_ip" -d "$target_subnet"
}
function fw::allow_port() {
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}"
fw::_accept_insert \
-s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port"
}
function fw::unallow_port() {
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}"
fw::_accept_remove \
-s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port"
}
# ============================================
# Flush
# ============================================
function fw::flush_peer() {
local client_ip="${1:?client_ip required}"
log::debug "flush_peer: starting for $client_ip"
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}')
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"
}
function fw::flush_forward() {
iptables -F FORWARD
log::debug "Flushed FORWARD chain"
}
function fw::flush_nat() {
iptables -t nat -F PREROUTING
log::debug "Flushed NAT PREROUTING chain"
}
function fw::flush_all() {
fw::flush_forward
fw::flush_nat
}
# ============================================
# NAT / DNS Redirect
# ============================================
function fw::nat_add_dns_redirect() {
local subnet="${1:-}" dns="${2:-}" interface="${3:-wg0}"
iptables -t nat -A PREROUTING -i "$interface" -s "$subnet" \
-p udp --dport 53 ! -d "$dns" \
-j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4
iptables -t nat -A PREROUTING -i "$interface" -s "$subnet" \
-p udp --dport 53 -j DNAT --to-destination "${dns}:53"
iptables -t nat -A PREROUTING -i "$interface" -s "$subnet" \
-p tcp --dport 53 -j DNAT --to-destination "${dns}:53"
}
function fw::nat_remove_dns_redirect() {
local subnet="${1:-}" dns="${2:-}" interface="${3:-wg0}"
iptables -t nat -D PREROUTING -i "$interface" -s "$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 "$interface" -s "$subnet" \
-p udp --dport 53 -j DNAT \
--to-destination "${dns}:53" 2>/dev/null || true
iptables -t nat -D PREROUTING -i "$interface" -s "$subnet" \
-p tcp --dport 53 -j DNAT \
--to-destination "${dns}:53" 2>/dev/null || true
}
# ============================================
# Display
# ============================================
function fw::forward_rules_for_ip() {
local ip="${1:-}"
iptables -L FORWARD -n -v </dev/null | grep -F "$ip" || true
}
function fw::proto_name() {
local proto="${1:-0}"
case "$proto" in
6) echo "tcp" ;;
17) echo "udp" ;;
1) echo "icmp" ;;
0) echo "all" ;;
tcp|udp|icmp|all) echo "$proto" ;;
*) echo "$proto" ;;
esac
}
function fw::format_rule() {
local line="${1:-}"
[[ -z "$line" ]] && return 0
local target prot src dst extra
target=$(awk '{print $3}' <<< "$line")
prot=$(awk '{print $4}' <<< "$line")
src=$(awk '{print $8}' <<< "$line")
dst=$(awk '{print $9}' <<< "$line")
extra=$(awk '{for(i=10;i<=NF;i++) printf $i" "}' <<< "$line" | xargs)
local prot_name
prot_name=$(fw::proto_name "$prot")
local dst_fmt="$dst"
if [[ "$extra" =~ dpt:([0-9]+) ]]; then
local port="${BASH_REMATCH[1]}"
dst_fmt="${dst}:${port}:${prot_name}"
fi
local formatted
formatted=$(printf " %-8s %-15s → %s" "$target" "$src" "$dst_fmt")
ui::firewall_rule "$formatted"
}
function fw::list_peer_rules() {
local ip="${1:-}" show_nflog="${2:-false}"
fw::forward_rules_for_ip "$ip" | while IFS= read -r line; do
[[ -z "$line" ]] && continue
! $show_nflog && [[ "$line" =~ NFLOG ]] && continue
fw::format_rule "$line"
done
}
# ============================================
# Counts
# ============================================
function fw::count_peer_rules() {
local ip="${1:-}"
local total=0 accepts=0 drops=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
[[ "$line" =~ NFLOG ]] && continue
(( total++ )) || true
[[ "$line" =~ ACCEPT ]] && (( accepts++ )) || true
[[ "$line" =~ DROP ]] && (( drops++ )) || true
done < <(fw::forward_rules_for_ip "$ip")
echo "${total}|${accepts}|${drops}"
}
# ============================================
# private
# ============================================
function fw::_forward_exists() {
iptables -C FORWARD "$@" 2>/dev/null
}
function fw::_rule_exists() {
local table="${1:-filter}" chain="${2:-FORWARD}"
shift 2
iptables -t "$table" -C "$chain" "$@" 2>/dev/null
}
function fw::_nat_exists() {
fw::_rule_exists nat PREROUTING "$@"
}
# Core NFLOG+DROP block pair — insert or append
function fw::_block_pair() {
local mode="${1:-insert}" # insert | append
shift
# $@ = match args (no -j)
if [[ "$mode" == "insert" ]]; then
# insert: DROP first at pos 1, NFLOG second at pos 1 → NFLOG ends above DROP
fw::_forward_exists "$@" -j DROP \
|| iptables -I FORWARD 1 "$@" -j DROP
fw::_forward_exists "$@" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \
|| iptables -I FORWARD 1 "$@" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:"
else
# append: NFLOG first, DROP second → NFLOG ends above DROP
fw::_forward_exists "$@" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \
|| iptables -A FORWARD "$@" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:"
fw::_forward_exists "$@" -j DROP \
|| iptables -A FORWARD "$@" -j DROP
fi
}
# Core NFLOG+DROP removal
function fw::_unblock_pair() {
shift 0 # no mode needed for deletion
# $@ = match args
fw::_forward_exists "$@" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \
&& iptables -D FORWARD "$@" -j NFLOG \
--nflog-group 1 --nflog-prefix "wgctl-drop:" 2>/dev/null || true
fw::_forward_exists "$@" -j DROP \
&& iptables -D FORWARD "$@" -j DROP 2>/dev/null || true
}
# Core ACCEPT insert
function fw::_accept_insert() {
# $@ = match args
fw::_forward_exists "$@" -j ACCEPT \
|| iptables -I FORWARD 1 "$@" -j ACCEPT
}
# Core ACCEPT removal
function fw::_accept_remove() {
fw::_forward_exists "$@" -j ACCEPT \
&& iptables -D FORWARD "$@" -j ACCEPT 2>/dev/null || true
}