From a09c59a7c4128eb04aff2e01458cc271cda94387 Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Wed, 13 May 2026 04:14:30 +0000 Subject: [PATCH] fix: iptables rule ordering, idempotent fw functions, rule module cleanup, variable leak fixes --- commands/watch.command.sh | 191 +++++++++++++++++++++++++------------ core/json.sh | 3 +- core/json_helper.py | 16 ++++ daemon/endpoint_cache.json | 6 +- modules/firewall.module.sh | 113 ++++++++++++++++------ modules/rule.module.sh | 134 ++++++++++++-------------- 6 files changed, 296 insertions(+), 167 deletions(-) diff --git a/commands/watch.command.sh b/commands/watch.command.sh index 17a166d..0df1783 100644 --- a/commands/watch.command.sh +++ b/commands/watch.command.sh @@ -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" } # ============================================ diff --git a/core/json.sh b/core/json.sh index b343f93..e01a704 100644 --- a/core/json.sh +++ b/core/json.sh @@ -31,4 +31,5 @@ function json::create_rule() { python3 "$JSON_HELPER" create_rule "$@" /dev/null || true - 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 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:-}" - iptables -A FORWARD -s "$client_ip" -d "$target_subnet" -j DROP + 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" + 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 +} diff --git a/modules/rule.module.sh b/modules/rule.module.sh index 8325f44..909ce45 100644 --- a/modules/rule.module.sh +++ b/modules/rule.module.sh @@ -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 @@ -243,27 +226,27 @@ function rule::restore_all() { local rule_name rule_name=$(peers::get_meta "$peer_name" "rule") [[ -z "$rule_name" ]] && continue - + if ! rule::exists "$rule_name"; then log::wg_warning "Rule '${rule_name}' not found for peer '${peer_name}', skipping" continue fi - + local client_ip client_ip=$(peers::get_ip "$peer_name") [[ -z "$client_ip" ]] && continue - + rule::apply "$rule_name" "$client_ip" done < <(peers::all) log::wg "Rules restored for all peers" } # ============================================ -# 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 \