#!/bin/bash # wg-sync-policy.sh — Reads wg0.conf, validates, generates policy.json atomically # Fixed: IP validation, atomic write, proper locking, error handling set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "${SCRIPT_DIR}/wg-policy-lib.sh" # ============================================================ # MAIN # ============================================================ main() { log_info "Starting policy sync..." # Validate prerequisites if [[ ! -f "$WG_CONF" ]]; then log_error "WireGuard config not found: $WG_CONF" exit 1 fi if ! command -v jq &>/dev/null; then log_error "jq is required but not installed" exit 1 fi # Acquire lock acquire_lock trap 'release_lock' EXIT # Backup current policy backup_policy # Temporary file for atomic write local tmp_policy tmp_policy="$(mktemp /tmp/wg-policy.XXXXXX)" trap "rm -f \"$tmp_policy\" 2>/dev/null; release_lock" EXIT echo '{"clients":{}}' > "$tmp_policy" # Parse peers from wg0.conf # AWK extracts IP and #Access comment per [Peer] block local parse_errors=0 awk ' BEGIN { RS="\n\\[Peer\\]\n"; FS="\n" } NR>1 { ip=""; access=""; internet="false" for(i=1;i<=NF;i++){ if($i ~ /^AllowedIPs/) { split($i,a," = ") gsub(/ /,"",a[2]) split(a[2],b,",") split(b[1],c,"/") ip=c[1] } if($i ~ /^#Access/) { sub(/^#Access[ \t]*=?[ \t]*/, "", $i) access=$i } if($i ~ /^#Internet/) { if(tolower($i) ~ /true|yes|1|allow/) { internet="true" } } } if(ip!="" && ip!="0.0.0.0" && ip!="::") { printf "%s|%s|%s\n", ip, access, internet } } ' "$WG_CONF" | while IFS="|" read -r ip access_string internet_flag; do # === VALIDATE CLIENT IP === if ! validate_cidr "$ip"; then log_warn "Invalid client IP skipped: '$ip'" (( parse_errors++ )) || true continue fi # === PARSE AND VALIDATE ACCESS TARGETS === local ACCESS_JSON="[]" if [[ -n "$access_string" ]]; then # Split by ; and , then validate each entry local valid_targets=() local IFS_OLD="$IFS" IFS=';,' read -ra targets <<< "$access_string" IFS="$IFS_OLD" for target in "${targets[@]}"; do # Trim whitespace target="$(echo "$target" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" [[ -z "$target" ]] && continue if validate_cidr "$target"; then valid_targets+=("$target") else log_warn "Invalid access target skipped for $ip: '$target'" (( parse_errors++ )) || true fi done if (( ${#valid_targets[@]} > 0 )); then ACCESS_JSON=$(printf '%s\n' "${valid_targets[@]}" | jq -R . | jq -s .) fi fi # Write to temp policy jq --arg ip "$ip" --argjson access "$ACCESS_JSON" --argjson internet "$internet_flag" \ '.clients[$ip] = {"name": $ip, "access": $access, "internet": $internet}' \ "$tmp_policy" > "${tmp_policy}.tmp" && mv "${tmp_policy}.tmp" "$tmp_policy" done # Validate JSON before atomic move if ! jq empty "$tmp_policy" 2>/dev/null; then log_error "Generated JSON is invalid, aborting. Check $tmp_policy" exit 1 fi # Atomic move (same filesystem = atomic rename) mv -f "$tmp_policy" "$POLICY_FILE" log_info "policy.json updated successfully" if (( parse_errors > 0 )); then log_warn "$parse_errors validation errors encountered (see warnings above)" fi local client_count client_count=$(jq '(.clients // {}) | length' "$POLICY_FILE") log_info "Total clients in policy: $client_count" } main "$@"