139 lines
4.0 KiB
Bash
139 lines
4.0 KiB
Bash
#!/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=""
|
|
for(i=1;i<=NF;i++){
|
|
if($i ~ /^AllowedIPs/) {
|
|
split($i,a," = ")
|
|
gsub(/ /,"",a[2])
|
|
split(a[2],b,",")
|
|
|
|
# First IP is the client IP
|
|
split(b[1],c,"/")
|
|
ip=c[1]
|
|
|
|
# The rest of the IPs are access targets
|
|
access_arr=""
|
|
for(j=2;j<=length(b);j++) {
|
|
if(access_arr=="") access_arr = b[j]
|
|
else access_arr = access_arr "," b[j]
|
|
}
|
|
access = access_arr
|
|
}
|
|
}
|
|
if(ip!="" && ip!="0.0.0.0" && ip!="::") {
|
|
printf "%s|%s\n", ip, access
|
|
}
|
|
}
|
|
' "$WG_CONF" | while IFS="|" read -r ip access_string; 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" \
|
|
'.clients[$ip] = {"name": $ip, "access": $access}' \
|
|
"$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 "$@"
|