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() { function cmd::watch::format_event() {
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" local ts="${1:-}" source="${2:-}" client="${3:-}"
local event="${4:-}" status="${5:-}" local dest="${4:-}" event="${5:-}" status="${6:-}"
local event_color local event_color
case "$event" in case "$event" in
attempt) event_color="\033[1;31m" ;; attempt|drop) event_color="\033[1;31m" ;;
handshake) event_color="\033[1;32m" ;; handshake) event_color="\033[1;32m" ;;
*) event_color="\033[0;37m" ;; *) event_color="\033[0;37m" ;;
esac esac
local status_str="" local status_color=""
if [[ -n "$status" ]]; then case "$status" in
case "$status" in blocked) status_color="\033[1;31m" ;;
blocked) status_str=" \033[1;31mblocked\033[0m" ;; allowed) status_color="\033[1;32m" ;;
allowed) status_str=" \033[1;32mallowed\033[0m" ;; esac
esac
fi
printf " %-20s %-25s %-20s ${event_color}%-12s\033[0m%b\n" \ printf " %-20s %-8s %-22s %-28s ${event_color}%-14s\033[0m ${status_color}%s\033[0m\n" \
"$ts" "$client" "${endpoint:-}" "$event" "$status_str" "$ts" "$source" "$client" "${dest:-}" "$event" "$status"
} }
function cmd::watch::header() { function cmd::watch::header() {
log::section "wgctl — Live Monitor (Ctrl+C to stop)" log::section "wgctl — Live Monitor (Ctrl+C to stop)"
printf "\n %-20s %-25s %-20s %-12s %s\n" \ printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \
"TIME" "CLIENT" "ENDPOINT" "EVENT" "STATUS" "TIME" "SOURCE" "CLIENT" "DESTINATION/ENDPOINT" "EVENT" "STATUS"
printf " %s\n\n" "$(printf '─%.0s' {1..85})" printf " %s\n\n" "$(printf '─%.0s' {1..105})"
} }
function cmd::watch::_peer_in_filter() { function cmd::watch::_peer_in_filter() {
@ -151,7 +149,7 @@ function cmd::watch::poll_handshakes() {
endpoint=$(monitor::endpoint_for_key "$public_key") endpoint=$(monitor::endpoint_for_key "$public_key")
cmd::watch::format_event \ cmd::watch::format_event \
"$formatted_ts" "$client_name" "${endpoint:-}" "handshake" "allowed" "$formatted_ts" "wg" "$client_name" "${endpoint:-}" "handshake" "allowed"
fi fi
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
@ -171,55 +169,126 @@ function cmd::watch::tail_events() {
declare -A _WATCH_LAST_ATTEMPT=() 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 [[ -z "$line" ]] && continue
local event_data # Handle tail -f file headers
event_data=$(json::parse_event "$line") if [[ "$line" == "==> "* ]]; then
[[ -z "$event_data" ]] && continue if [[ "$line" == *"fw_events"* ]]; then
echo "fw" > "$source_file"
local ts client endpoint event else
IFS="|" read -r ts client endpoint event <<< "$event_data" echo "wg" > "$source_file"
fi
# Apply filters continue
[[ -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
fi fi
if $restricted_only; then local source
local conf source=$(cat "$source_file")
conf="$(ctx::clients)/${client}.conf"
[[ -f "$conf" ]] || continue if [[ "$source" == "wg" ]]; then
cmd::list::is_restricted "$client" || continue $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 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 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::remove_peer_block() { python3 "$JSON_HELPER" remove_peer_block "$@" </dev/null; }
function json::create_group() { python3 "$JSON_HELPER" create_group "$@" </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_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: except:
pass 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 = { commands = {
'get': lambda args: get(args[0], args[1]), 'get': lambda args: get(args[0], args[1]),
'set': lambda args: set_key(args[0], args[1], args[2]), '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]), 'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]),
'create_group': lambda args: create_group(args[0], args[1], args[2]), 'create_group': lambda args: create_group(args[0], args[1], args[2]),
'parse_event': lambda args: parse_event(args[0]), 'parse_event': lambda args: parse_event(args[0]),
'parse_fw_event': lambda args: parse_fw_event(args[0]),
} }
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -1,9 +1,9 @@
{ {
"phone-fred": "94.63.0.129", "phone-fred": "94.63.0.129",
"phone-helena": "148.69.44.173", "phone-helena": "148.69.46.241",
"phone-nuno": "94.63.0.129", "phone-nuno": "148.69.51.160",
"tablet-nuno": "148.69.202.5", "tablet-nuno": "148.69.202.5",
"guest-zephyr": "5.13.82.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" "desktop-roboclean": "46.189.215.231"
} }

View file

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