fix: iptables rule ordering, idempotent fw functions, rule module cleanup, variable leak fixes

This commit is contained in:
Nuno Duque Nunes 2026-05-13 04:14:30 +00:00
parent 8ef8ea91b3
commit a09c59a7c4
6 changed files with 296 additions and 167 deletions

View file

@ -46,33 +46,31 @@ EOF
# ============================================
function cmd::watch::format_event() {
local ts="${1:-}" client="${2:-}" endpoint="${3:-}"
local event="${4:-}" status="${5:-}"
local ts="${1:-}" source="${2:-}" client="${3:-}"
local dest="${4:-}" event="${5:-}" status="${6:-}"
local event_color
case "$event" in
attempt) event_color="\033[1;31m" ;;
handshake) event_color="\033[1;32m" ;;
*) event_color="\033[0;37m" ;;
attempt|drop) event_color="\033[1;31m" ;;
handshake) event_color="\033[1;32m" ;;
*) event_color="\033[0;37m" ;;
esac
local status_str=""
if [[ -n "$status" ]]; then
case "$status" in
blocked) status_str=" \033[1;31mblocked\033[0m" ;;
allowed) status_str=" \033[1;32mallowed\033[0m" ;;
esac
fi
local status_color=""
case "$status" in
blocked) status_color="\033[1;31m" ;;
allowed) status_color="\033[1;32m" ;;
esac
printf " %-20s %-25s %-20s ${event_color}%-12s\033[0m%b\n" \
"$ts" "$client" "${endpoint:-}" "$event" "$status_str"
printf " %-20s %-8s %-22s %-28s ${event_color}%-14s\033[0m ${status_color}%s\033[0m\n" \
"$ts" "$source" "$client" "${dest:-}" "$event" "$status"
}
function cmd::watch::header() {
log::section "wgctl — Live Monitor (Ctrl+C to stop)"
printf "\n %-20s %-25s %-20s %-12s %s\n" \
"TIME" "CLIENT" "ENDPOINT" "EVENT" "STATUS"
printf " %s\n\n" "$(printf '─%.0s' {1..85})"
printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \
"TIME" "SOURCE" "CLIENT" "DESTINATION/ENDPOINT" "EVENT" "STATUS"
printf " %s\n\n" "$(printf '─%.0s' {1..105})"
}
function cmd::watch::_peer_in_filter() {
@ -151,7 +149,7 @@ function cmd::watch::poll_handshakes() {
endpoint=$(monitor::endpoint_for_key "$public_key")
cmd::watch::format_event \
"$formatted_ts" "$client_name" "${endpoint:-}" "handshake" "allowed"
"$formatted_ts" "wg" "$client_name" "${endpoint:-}" "handshake" "allowed"
fi
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
@ -171,55 +169,126 @@ function cmd::watch::tail_events() {
declare -A _WATCH_LAST_ATTEMPT=()
tail -f "$(ctx::events_log)" 2>/dev/null | while IFS= read -r line; do
# Build ip->name map for fw events
declare -A ip_to_name=()
local conf_file
while IFS= read -r conf_file; do
local name
name=$(basename "$conf_file" .conf)
local ip
ip=$(grep "^Address" "$conf_file" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1)
[[ -n "$ip" && -n "$name" ]] && ip_to_name["$ip"]="$name"
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
# Source tracker via temp file (persists across subshell iterations)
local source_file
source_file=$(mktemp)
echo "wg" > "$source_file"
# Cleanup temp file on exit
trap "rm -f '$source_file'" EXIT
tail -f "$(ctx::events_log)" "$(ctx::fw_events_log)" 2>/dev/null \
| while IFS= read -r line; do
[[ -z "$line" ]] && continue
local event_data
event_data=$(json::parse_event "$line")
[[ -z "$event_data" ]] && continue
local ts client endpoint event
IFS="|" read -r ts client endpoint event <<< "$event_data"
# Apply filters
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
cmd::watch::_peer_in_filter "$client" "${peer_set[@]}" || continue
if [[ -n "$filter_type" ]]; then
local conf
conf="$(ctx::clients)/${client}.conf"
[[ -f "$conf" ]] || continue
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
local subnet
subnet=$(config::subnet_for "$filter_type")
string::starts_with "$ip" "$subnet" || continue
# Handle tail -f file headers
if [[ "$line" == "==> "* ]]; then
if [[ "$line" == *"fw_events"* ]]; then
echo "fw" > "$source_file"
else
echo "wg" > "$source_file"
fi
continue
fi
if $restricted_only; then
local conf
conf="$(ctx::clients)/${client}.conf"
[[ -f "$conf" ]] || continue
cmd::list::is_restricted "$client" || continue
local source
source=$(cat "$source_file")
if [[ "$source" == "wg" ]]; then
$allowed_only && continue # wg events are attempts/blocked
local event_data
event_data=$(json::parse_event "$line")
[[ -z "$event_data" ]] && continue
local ts client endpoint event
IFS="|" read -r ts client endpoint event <<< "$event_data"
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
cmd::watch::_peer_in_filter "$client" "${peer_set[@]}" || continue
if [[ -n "$filter_type" ]]; then
local conf
conf="$(ctx::clients)/${client}.conf"
[[ -f "$conf" ]] || continue
local ip
ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1)
local subnet
subnet=$(config::subnet_for "$filter_type")
string::starts_with "$ip" "$subnet" || continue
fi
$restricted_only && { cmd::list::is_restricted "$client" || continue; }
# Dedup
local now
now=$(date +%s)
local safe_client="${client//[-.]/_}"
local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}"
(( now - last < 30 )) && continue
_WATCH_LAST_ATTEMPT[$safe_client]="$now"
local formatted_ts
formatted_ts=$(fmt::datetime_iso "$ts")
cmd::watch::format_event \
"$formatted_ts" "wg" "$client" "${endpoint:-}" "$event" "blocked"
else
# FW event
$allowed_only && continue
$blocked_only && continue # fw drops aren't "blocked peers" per se
local fw_data
fw_data=$(json::parse_fw_event "$line")
[[ -z "$fw_data" ]] && continue
local ts src_ip dst_ip dst_port proto
IFS="|" read -r ts src_ip dst_ip dst_port proto <<< "$fw_data"
[[ -z "$src_ip" ]] && continue
local client="${ip_to_name[$src_ip]:-$src_ip}"
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
cmd::watch::_peer_in_filter "$client" "${peer_set[@]}" || continue
if [[ -n "$filter_type" ]]; then
local peer_client="${ip_to_name[$src_ip]:-}"
[[ -z "$peer_client" ]] && continue
local conf
conf="$(ctx::clients)/${peer_client}.conf"
[[ -f "$conf" ]] || continue
local ip
ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1)
local subnet
subnet=$(config::subnet_for "$filter_type")
string::starts_with "$ip" "$subnet" || continue
fi
local dst_str="${dst_ip:-}"
[[ -n "$dst_port" ]] && dst_str="${dst_ip}:${dst_port}/${proto}"
local formatted_ts
formatted_ts=$(fmt::datetime_iso "$ts")
cmd::watch::format_event \
"$formatted_ts" "fw" "$client" "$dst_str" "drop" "blocked"
fi
$allowed_only && [[ "$event" != "handshake" ]] && continue
local formatted_ts
formatted_ts=$(fmt::datetime_iso "$ts")
# Dedup attempts
local now
now=$(date +%s)
local safe_client="${client//[-.]/_}"
local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}"
local diff=$(( now - last ))
(( diff < 30 )) && continue
_WATCH_LAST_ATTEMPT[$safe_client]="$now"
cmd::watch::format_event \
"$formatted_ts" "$client" "${endpoint:-}" "$event" "blocked"
done
rm -f "$source_file"
}
# ============================================

View file

@ -32,3 +32,4 @@ function json::cleanup_config() { python3 "$JSON_HELPER" cleanup_config "$@"
function json::remove_peer_block() { python3 "$JSON_HELPER" remove_peer_block "$@" </dev/null; }
function json::create_group() { python3 "$JSON_HELPER" create_group "$@" </dev/null; }
function json::parse_event() { python3 "$JSON_HELPER" parse_event "$@" </dev/null; }
function json::parse_fw_event() { python3 "$JSON_HELPER" parse_fw_event "$@" </dev/null; }

View file

@ -696,6 +696,21 @@ def parse_event(line):
except:
pass
def parse_fw_event(line):
"""Parse a single fw_events.log JSON line"""
try:
e = json.loads(line)
ts = e.get('timestamp', '')
src = e.get('src_ip', '')
dst = e.get('dest_ip', '')
port = e.get('dest_port', '')
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
proto_num = e.get('ip.protocol', 0)
proto = proto_map.get(proto_num, str(proto_num))
print(f"{ts}|{src}|{dst}|{port}|{proto}")
except:
pass
commands = {
'get': lambda args: get(args[0], args[1]),
'set': lambda args: set_key(args[0], args[1], args[2]),
@ -727,6 +742,7 @@ commands = {
'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]),
'create_group': lambda args: create_group(args[0], args[1], args[2]),
'parse_event': lambda args: parse_event(args[0]),
'parse_fw_event': lambda args: parse_fw_event(args[0]),
}
if __name__ == '__main__':

View file

@ -1,9 +1,9 @@
{
"phone-fred": "94.63.0.129",
"phone-helena": "148.69.44.173",
"phone-nuno": "94.63.0.129",
"phone-helena": "148.69.46.241",
"phone-nuno": "148.69.51.160",
"tablet-nuno": "148.69.202.5",
"guest-zephyr": "5.13.82.5",
"guest-zephyr-test": "94.63.0.129",
"guest-zephyr-test": "148.69.193.47",
"desktop-roboclean": "46.189.215.231"
}

View file

@ -13,78 +13,115 @@ function fw::on_load() {
# ============================================
function fw::block_ip() {
local client_ip="$1" target_ip="$2"
iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -j DROP
iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:"
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"
local client_ip="${1:-}" 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
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}"
iptables -I FORWARD 1 -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 NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:"
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}"
iptables -D FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP 2>/dev/null || true
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"
local client_ip="${1:-}" client_name="${2:-}"
iptables -A FORWARD -s "$client_ip" -j DROP
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"
local client_ip="${1:-}"
iptables -D FORWARD -s "$client_ip" -j DROP 2>/dev/null || true
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"
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
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"
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
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"
iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -j ACCEPT
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"
iptables -D FORWARD -s "$client_ip" -d "$target_ip" -j ACCEPT 2>/dev/null || true
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}"
iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT
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}"
iptables -D FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null || true
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() {
@ -205,3 +242,25 @@ function fw::restore_blocks() {
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
}

View file

@ -5,12 +5,12 @@
# ============================================
function rule::exists() {
local name="$1"
local name="${1:-}"
[[ -f "$(ctx::rule::path "${name}.rule")" ]]
}
function rule::require_exists() {
local name="$1"
local name="${1:-}"
if ! rule::exists "$name"; then
log::error "Rule not found: ${name}"
return 1
@ -18,52 +18,47 @@ function rule::require_exists() {
}
function rule::get() {
local name="$1" key="$2"
local name="${1:-}" key="${2:-}"
json::get "$(ctx::rule::path "${name}.rule")" "$key"
}
function rule::get_all() {
local name="$1"
local rule_file
rule_file="$(ctx::rule::path "${name}.rule")"
cat "$rule_file"
local name="${1:-}"
cat "$(ctx::rule::path "${name}.rule")"
}
function rule::is_applied() {
local rule_name="$1"
local client_ip="$2"
local rule_name="${1:-}" client_ip="${2:-}"
# Try first block_ports entry
local first_port
first_port=$(rule::get "$rule_name" "block_ports" | head -1)
if [[ -n "$first_port" ]]; then
local target port proto
IFS=":" read -r target port proto <<< "$first_port"
proto="${proto:-tcp}"
iptables -C FORWARD -s "$client_ip" -d "$target" -p "$proto" --dport "$port" -j DROP 2>/dev/null
fw::_forward_exists -s "$client_ip" -d "$target" \
-p "$proto" --dport "$port" -j DROP
return $?
fi
# Fall back to first block_ips entry
local first_ip
first_ip=$(rule::get "$rule_name" "block_ips" | head -1)
if [[ -n "$first_ip" ]]; then
iptables -C FORWARD -s "$client_ip" -d "$first_ip" -j DROP 2>/dev/null
fw::_forward_exists -s "$client_ip" -d "$first_ip" -j DROP
return $?
fi
# No rules to check (admin rule) — check allow_ports
local first_allow
first_allow=$(rule::get "$rule_name" "allow_ports" | head -1)
if [[ -n "$first_allow" ]]; then
local target port proto
IFS=":" read -r target port proto <<< "$first_allow"
proto="${proto:-tcp}"
iptables -C FORWARD -s "$client_ip" -d "$target" -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null
fw::_forward_exists -s "$client_ip" -d "$target" \
-p "$proto" --dport "$port" -j ACCEPT
return $?
fi
# Empty rule (admin) — never "applied" in iptables sense
return 1
}
@ -74,42 +69,27 @@ function rule::is_applied() {
function rule::apply() {
local rule_name="${1:?rule_name required}"
local client_ip="${2:?client_ip required}"
local peer_name="${3:-}" # optional, avoids find_by_ip call
local peer_name="${3:-}"
rule::require_exists "$rule_name" || return 1
# Use provided peer_name or look it up
if [[ -z "$peer_name" ]]; then
peer_name=$(peers::find_by_ip "$client_ip")
fi
log::debug "rule::apply: peer_name=$peer_name ip=$client_ip"
# Check if already applied
if rule::is_applied "$rule_name" "$client_ip"; then
log::wg "Rule '${rule_name}' already applied to: ${client_ip}"
# Still update meta even if rules exist
if [[ -n "$peer_name" ]]; then
peers::set_meta "$peer_name" "rule" "$rule_name"
fi
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
return 0
fi
# Check if already applied
local peer_name
peer_name=$(peers::find_by_ip "$client_ip")
if [[ -n "$peer_name" ]]; then
# Check if already applied via iptables
if rule::is_applied "$rule_name" "$client_ip"; then
log::wg "Rule '${rule_name}' already applied to: ${client_ip}"
return 0
fi
fi
# Process block_ips
while IFS= read -r ip; do
[[ -z "$ip" ]] && continue
fw::block_ip "$client_ip" "$ip"
while IFS= read -r block_ip; do
[[ -z "$block_ip" ]] && continue
fw::block_ip "$client_ip" "$block_ip"
done < <(rule::get "$rule_name" "block_ips")
# Process block_ports
@ -122,12 +102,12 @@ function rule::apply() {
done < <(rule::get "$rule_name" "block_ports")
# Process allow_ips (inserted before blocks)
while IFS= read -r ip; do
[[ -z "$ip" ]] && continue
fw::allow_ip "$client_ip" "$ip"
while IFS= read -r allow_ip; do
[[ -z "$allow_ip" ]] && continue
fw::allow_ip "$client_ip" "$allow_ip"
done < <(rule::get "$rule_name" "allow_ips")
# allow_ports (inserted last = highest priority)
# Process allow_ports (highest priority)
while IFS= read -r entry; do
[[ -z "$entry" ]] && continue
local target port proto
@ -136,20 +116,18 @@ function rule::apply() {
fw::allow_port "$client_ip" "$target" "$port" "$proto"
done < <(rule::get "$rule_name" "allow_ports")
# Persist rule assignment in meta
if [[ -n "$peer_name" ]]; then
peers::set_meta "$peer_name" "rule" "$rule_name"
fi
# Persist rule assignment
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
# DNS redirect
local dns_redirect
dns_redirect=$(rule::get "$rule_name" "dns_redirect")
if [[ "$dns_redirect" == "true" ]]; then
local peer_subnet
peer_subnet=$(peers::get_ip "$peer_name" | cut -d'.' -f1-3)
# Only apply if not already in PREROUTING
if ! iptables -t nat -C PREROUTING -i wg0 -s "${peer_subnet}.0/24" -p udp --dport 53 \
-j DNAT --to-destination "$(config::dns):53" 2>/dev/null; then
if ! fw::_nat_exists -i wg0 -s "${peer_subnet}.0/24" \
-p udp --dport 53 -j DNAT \
--to-destination "$(config::dns):53" 2>/dev/null; then
rule::apply_dns_redirect "${peer_subnet}.0/24"
log::debug "dns_redirect: applied for ${peer_subnet}.0/24"
else
@ -161,11 +139,13 @@ function rule::apply() {
}
function rule::unapply() {
local rule_name="$1"
local client_ip="$2"
local rule_name="${1:-}" client_ip="${2:-}"
rule::require_exists "$rule_name" || return 1
local peer_name
peer_name=$(peers::find_by_ip "$client_ip")
# Remove allow_ports first (reverse order of apply)
while IFS= read -r entry; do
[[ -z "$entry" ]] && continue
@ -176,9 +156,9 @@ function rule::unapply() {
done < <(rule::get "$rule_name" "allow_ports")
# Remove allow_ips
while IFS= read -r ip; do
[[ -z "$ip" ]] && continue
fw::unallow_ip "$client_ip" "$ip"
while IFS= read -r allow_ip; do
[[ -z "$allow_ip" ]] && continue
fw::unallow_ip "$client_ip" "$allow_ip"
done < <(rule::get "$rule_name" "allow_ips")
# Remove block_ports
@ -191,38 +171,41 @@ function rule::unapply() {
done < <(rule::get "$rule_name" "block_ports")
# Remove block_ips
while IFS= read -r ip; do
[[ -z "$ip" ]] && continue
fw::unblock_ip "$client_ip" "$ip"
while IFS= read -r block_ip; do
[[ -z "$block_ip" ]] && continue
fw::unblock_ip "$client_ip" "$block_ip"
done < <(rule::get "$rule_name" "block_ips")
# Remove DNS redirect if applicable
local dns_redirect
dns_redirect=$(rule::get "$rule_name" "dns_redirect")
if [[ "$dns_redirect" == "true" ]]; then
local peer_name subnet
peer_name=$(peers::find_by_ip "$client_ip")
subnet=$(config::subnet_for "$(peers::get_meta "$peer_name" "subtype")")
local subtype
subtype=$(peers::get_meta "$peer_name" "subtype")
local subnet
if [[ -n "$subtype" ]]; then
subnet=$(config::subnet_for "$subtype")
else
local peer_type
peer_type=$(peers::get_type "$peer_name") || true
[[ -z "$peer_type" ]] && peer_type="phone"
subnet=$(config::subnet_for "$peer_type")
fi
rule::remove_dns_redirect "${subnet}.0/24"
fi
# Clear rule from meta
local peer_name
peer_name=$(peers::find_by_ip "$client_ip")
if [[ -n "$peer_name" ]]; then
peers::set_meta "$peer_name" "rule" ""
fi
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" ""
log::debug "Removed rule '${rule_name}' from: ${client_ip}"
}
function rule::reapply_all() {
local rule_name="$1"
local rule_name="${1:-}"
rule::require_exists "$rule_name" || return 1
local peers=()
mapfile -t peers < <(peers::with_rule "$rule_name")
[[ ${#peers[@]} -eq 0 ]] && return 0
local count=0
@ -259,11 +242,11 @@ function rule::restore_all() {
}
# ============================================
# Guest DNS Redirect (rule-level feature)
# DNS Redirect
# ============================================
function rule::apply_dns_redirect() {
local client_subnet="$1"
local client_subnet="${1:-}"
local dns
dns="$(config::dns)"
@ -276,12 +259,13 @@ function rule::apply_dns_redirect() {
}
function rule::remove_dns_redirect() {
local client_subnet="$1"
local client_subnet="${1:-}"
local dns
dns="$(config::dns)"
iptables -t nat -D PREROUTING -i wg0 -s "$client_subnet" -p udp --dport 53 \
! -d "$dns" -j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4 2>/dev/null || true
! -d "$dns" -j LOG --log-prefix "wgctl-dns-redirect: " \
--log-level 4 2>/dev/null || true
iptables -t nat -D PREROUTING -i wg0 -s "$client_subnet" -p udp --dport 53 \
-j DNAT --to-destination "${dns}:53" 2>/dev/null || true
iptables -t nat -D PREROUTING -i wg0 -s "$client_subnet" -p tcp --dport 53 \