#!/usr/bin/env bash # ============================================ # Rule File Parsing # ============================================ function rule::exists() { local name="$1" [[ -f "$(ctx::rule::path "${name}.rule")" ]] } function rule::require_exists() { local name="$1" if ! rule::exists "$name"; then log::error "Rule not found: ${name}" return 1 fi } function rule::get() { 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" } function rule::is_applied() { local rule_name="$1" local 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 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 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 return $? fi # Empty rule (admin) — never "applied" in iptables sense return 1 } # ============================================ # Rule Application # ============================================ function rule::apply() { local rule_name="$1" local client_ip="$2" local peer_name="${3:-}" # optional, avoids find_by_ip call log::debug "rule::apply ENTRY: rule=$rule_name ip=$client_ip peer=$peer_name" rule::require_exists "$rule_name" || return 1 log::debug "rule::apply: exists check passed" # Use provided peer_name or look it up if [[ -z "$peer_name" ]]; then peer_name=$(peers::find_by_ip "$client_ip") fi # 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 return 0 fi # Check if already applied local peer_name peer_name=$(peers::find_by_ip "$client_ip") log::debug "rule::apply: find_by_ip($client_ip) = '$peer_name'" 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" done < <(rule::get "$rule_name" "block_ips") # Process block_ports while IFS= read -r entry; do [[ -z "$entry" ]] && continue local target port proto IFS=":" read -r target port proto <<< "$entry" proto="${proto:-tcp}" fw::block_port "$client_ip" "$target" "$port" "$proto" 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" done < <(rule::get "$rule_name" "allow_ips") # allow_ports (inserted last = highest priority) while IFS= read -r entry; do [[ -z "$entry" ]] && continue local target port proto IFS=":" read -r target port proto <<< "$entry" proto="${proto:-tcp}" fw::allow_port "$client_ip" "$target" "$port" "$proto" done < <(rule::get "$rule_name" "allow_ports") # Persist rule assignment in meta log::debug "rule::apply: peer_name=$peer_name ip=$client_ip" if [[ -n "$peer_name" ]]; then peers::set_meta "$peer_name" "rule" "$rule_name" log::debug "rule::apply: set meta rule=$rule_name for $peer_name" fi 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 rule::apply_dns_redirect "${peer_subnet}.0/24" log::debug "dns_redirect: applied for ${peer_subnet}.0/24" else log::debug "dns_redirect: already applied for ${peer_subnet}.0/24" fi fi log::debug "Applied rule '${rule_name}' to: ${client_ip}" } function rule::unapply() { local rule_name="$1" local client_ip="$2" rule::require_exists "$rule_name" || return 1 # Remove allow_ports first (reverse order of apply) while IFS= read -r entry; do [[ -z "$entry" ]] && continue local target port proto IFS=":" read -r target port proto <<< "$entry" proto="${proto:-tcp}" fw::unallow_port "$client_ip" "$target" "$port" "$proto" 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" done < <(rule::get "$rule_name" "allow_ips") # Remove block_ports while IFS= read -r entry; do [[ -z "$entry" ]] && continue local target port proto IFS=":" read -r target port proto <<< "$entry" proto="${proto:-tcp}" fw::unblock_port "$client_ip" "$target" "$port" "$proto" 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" 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")") 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 log::debug "Removed rule '${rule_name}' from: ${client_ip}" } function rule::reapply_all() { 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 for peer_name in "${peers[@]}"; do local client_ip client_ip=$(peers::get_ip "$peer_name") [[ -z "$client_ip" ]] && continue rule::unapply "$rule_name" "$client_ip" rule::apply "$rule_name" "$client_ip" "$peer_name" (( count++ )) || true done log::wg_success "Rule '${rule_name}' re-applied to ${count} peers" } function rule::restore_all() { while IFS= read -r peer_name; do 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) # ============================================ function rule::apply_dns_redirect() { local client_subnet="$1" local dns dns="$(config::dns)" iptables -t nat -A PREROUTING -i wg0 -s "$client_subnet" -p udp --dport 53 \ ! -d "$dns" -j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4 iptables -t nat -A PREROUTING -i wg0 -s "$client_subnet" -p udp --dport 53 \ -j DNAT --to-destination "${dns}:53" iptables -t nat -A PREROUTING -i wg0 -s "$client_subnet" -p tcp --dport 53 \ -j DNAT --to-destination "${dns}:53" } function rule::remove_dns_redirect() { 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 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 \ -j DNAT --to-destination "${dns}:53" 2>/dev/null || true }