#!/usr/bin/env bash # ============================================ # Rule File Parsing # ============================================ function rule::is_base() { local name="${1:-}" [[ -f "$(ctx::rules::base)/${name}.rule" ]] } function rule::exists() { local name="${1:-}" local path path=$(json::find_rule_file "$(ctx::rules)" "$name") [[ -n "$path" ]] } function rule::require_assignable() { local name="${1:-}" if rule::is_base "$name"; then log::error "Cannot assign base rule '${name}' — base rules cannot be assigned directly" return 1 fi } 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::rule_resolve_field "$(ctx::rules)" "$name" "$key" } function rule::get_own() { local name="${1:-}" key="${2:-}" local file file=$(rule::path "$name") || return 0 json::get_raw "$file" "$key" } function rule::get_resolved() { local name="${1:-}" json::rule_resolve "$(ctx::rules)" "$name" } function rule::path() { local name="${1:-}" local path path=$(json::find_rule_file "$(ctx::rules)" "$name") [[ -n "$path" ]] && echo "$path" || return 1 } function rule::get_all() { local name="${1:-}" rule::get_resolved "$name" } function rule::is_applied() { local rule_name="${1:-}" client_ip="${2:-}" 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}" fw::has_block_rule "$client_ip" "$target" "$proto" "$port" return $? fi local first_ip first_ip=$(rule::get "$rule_name" "block_ips" | head -1) if [[ -n "$first_ip" ]]; then fw::has_block_rule "$client_ip" "$first_ip" return $? fi 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}" fw::_forward_exists -s "$client_ip" -d "$target" \ -p "$proto" --dport "$port" -j ACCEPT return $? fi return 1 } # ============================================ # Rule Application # ============================================ function rule::apply() { local rule_name="${1:?rule_name required}" local client_ip="${2:?client_ip required}" local peer_name="${3:-}" rule::require_exists "$rule_name" || return 1 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" if rule::is_applied "$rule_name" "$client_ip"; then log::wg "Rule '${rule_name}' already applied to: ${client_ip}" [[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name" return 0 fi # Process block_ips 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 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 allow_ip; do [[ -z "$allow_ip" ]] && continue fw::allow_ip "$client_ip" "$allow_ip" done < <(rule::get "$rule_name" "allow_ips") # Process allow_ports (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") [[ -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) 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 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:-}" 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 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 allow_ip; do [[ -z "$allow_ip" ]] && continue if [[ "$allow_ip" == *"/"* ]]; then fw::unallow_subnet "$client_ip" "$allow_ip" else fw::unallow_ip "$client_ip" "$allow_ip" fi 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 block_ip; do [[ -z "$block_ip" ]] && continue if [[ "$block_ip" == *"/"* ]]; then fw::unblock_subnet "$client_ip" "$block_ip" else fw::unblock_ip "$client_ip" "$block_ip" fi 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_ip peer_subnet peer_ip=$(peers::get_ip "$peer_name") peer_subnet=$(echo "$peer_ip" | cut -d'.' -f1-3) rule::remove_dns_redirect "${peer_subnet}.0/24" fi [[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "" log::debug "Removed rule '${rule_name}' from: ${client_ip}" } # ============================================ # Bulk Operations # ============================================ function rule::_apply_identity_rule() { local peer_name="${1:-}" client_ip="${2:-}" local identity_name identity_name=$(identity::get_name "$peer_name") [[ -z "$identity_name" ]] && return 0 local identity_rule strict identity_rule=$(identity::rule "$identity_name") [[ -z "$identity_rule" ]] && return 0 strict=$(identity::rule_flags "$identity_name" "strict_rule") if [[ "$strict" == "true" ]]; then fw::flush_peer "$client_ip" rule::apply "$identity_rule" "$client_ip" "$peer_name" else rule::apply "$identity_rule" "$client_ip" "$peer_name" fi } # rule::full_restore_peer # Flush and fully restore all fw rules for a peer — rule rules + block rules. # Use this instead of calling rule::apply + block::restore_rules_for separately # to ensure block rules are never left missing after a flush. function rule::full_restore_peer() { local peer_name="${1:-}" client_ip="${2:-}" [[ -z "$peer_name" || -z "$client_ip" ]] && return 1 fw::flush_peer "$client_ip" local peer_rule peer_rule=$(peers::get_meta "$peer_name" "rule") [[ -n "$peer_rule" ]] && rule::apply "$peer_rule" "$client_ip" "$peer_name" rule::_apply_identity_rule "$peer_name" "$client_ip" block::restore_rules_for "$peer_name" "$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::full_restore_peer "$peer_name" "$client_ip" (( 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 block::is_blocked "$peer_name" && continue 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 # full_restore_peer ensures block rules are restored alongside rule rules rule::full_restore_peer "$peer_name" "$client_ip" done < <(peers::all) log::wg "Rules restored for all peers" } # ============================================ # Rendering # ============================================ function rule::render_flat() { local rule_name="${1:-}" local allow_ports allow_ips block_ips block_ports dns allow_ports=$(rule::get "$rule_name" "allow_ports") allow_ips=$(rule::get "$rule_name" "allow_ips") block_ips=$(rule::get "$rule_name" "block_ips") block_ports=$(rule::get "$rule_name" "block_ports") dns=$(rule::get_own "$rule_name" "dns_redirect") local has_content=false [[ -n "${allow_ports}${allow_ips}${block_ips}${block_ports}" ]] && \ has_content=true if ! $has_content; then printf "\n full access (no restrictions)\n" return 0 fi if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then printf "\n" while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "+" "$e" 2 done <<< "$allow_ports"$'\n'"$allow_ips" fi if [[ -n "$block_ips" || -n "$block_ports" ]]; then printf "\n" while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "-" "$e" 2 done <<< "$block_ips"$'\n'"$block_ports" fi [[ "${dns,,}" == "true" ]] && \ net::print_dns_redirect "$(config::dns)" 6 "DNS" return 0 } function rule::render_entries() { local rule_name="${1:-}" indent="${2:-4}" local allow_ports allow_ips block_ips block_ports dns allow_ports=$(rule::get "$rule_name" "allow_ports" 2>/dev/null || true) allow_ips=$(rule::get "$rule_name" "allow_ips" 2>/dev/null || true) block_ips=$(rule::get "$rule_name" "block_ips" 2>/dev/null || true) block_ports=$(rule::get "$rule_name" "block_ports" 2>/dev/null || true) dns=$(rule::get_own "$rule_name" "dns_redirect") while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "+" "$e" done <<< "$allow_ports"$'\n'"$allow_ips" while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "-" "$e" done <<< "$block_ips"$'\n'"$block_ports" [[ "${dns,,}" == "true" ]] && \ net::print_dns_redirect "$(config::dns)" 6 "DNS" } function rule::render_own_entries() { local rule_name="${1:-}" local rule_file rule_file="$(rule::path "$rule_name")" local allow_ports allow_ips block_ips block_ports dns allow_ports=$(json::get "$rule_file" "allow_ports" 2>/dev/null || true) allow_ips=$(json::get "$rule_file" "allow_ips" 2>/dev/null || true) block_ips=$(json::get "$rule_file" "block_ips" 2>/dev/null || true) block_ports=$(json::get "$rule_file" "block_ports" 2>/dev/null || true) dns=$(json::get "$rule_file" "dns_redirect" 2>/dev/null || true) local combined="${allow_ports}${allow_ips}${block_ips}${block_ports}" [[ -z "${combined//[$'\n']/}" ]] && return 0 while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "+" "$e" done <<< "$allow_ports"$'\n'"$allow_ips" while IFS= read -r e; do [[ -z "$e" ]] && continue net::print_entry "-" "$e" done <<< "$block_ips"$'\n'"$block_ports" [[ "${dns,,}" == "true" ]] && \ net::print_dns_redirect "$(config::dns)" 6 "DNS" return 0 } function rule::render_extends_tree() { local rule_name="${1:-}" local rule_file rule_file="$(rule::path "$rule_name")" local extends_raw=() mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) [[ ${#extends_raw[@]} -eq 0 || -z "${extends_raw[0]:-}" ]] && return 1 for base_name in "${extends_raw[@]}"; do [[ -z "$base_name" ]] && continue printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name" rule::render_entries "$base_name" done local own_output own_output=$(rule::render_own_entries "$rule_name") if [[ -n "$own_output" ]]; then printf "\n \033[0;37mOwn:\033[0m\n" printf "%s\n" "$own_output" fi return 0 } # ============================================ # DNS Redirect # ============================================ function rule::apply_dns_redirect() { local client_subnet="${1:-}" fw::nat_add_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)" } function rule::remove_dns_redirect() { local client_subnet="${1:-}" fw::nat_remove_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)" }