#!/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 [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 } 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 }