#!/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_subnet() { local client_ip="${1:-}" target_subnet="${2:-}" fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j ACCEPT \ || iptables -I FORWARD 1 -s "$client_ip" -d "$target_subnet" -j ACCEPT } function fw::unallow_subnet() { local client_ip="${1:-}" target_subnet="${2:-}" fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j ACCEPT \ && iptables -D FORWARD -s "$client_ip" -d "$target_subnet" -j ACCEPT 2>/dev/null || true } 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" } # ============================================ # Peer related # ============================================ 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 } function fw::format_rule() { local line="${1:-}" [[ -z "$line" ]] && return 0 # Parse verbose iptables format: # pkts bytes target prot opt in out src dst [extra] 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" } # ============================================ # Helpers # ============================================ function fw::_nat_exists() { fw::_rule_exists nat PREROUTING "$@" } function fw::forward_rules_for_ip() { local ip="${1:-}" iptables -L FORWARD -n -v /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 } # ============================================ # 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 }