fix: resolve ghost iptables rules, set -e crashes, and automate MASQUERADE NAT
This commit is contained in:
@@ -302,6 +302,16 @@ detect_wg_subnet() {
|
||||
fi
|
||||
}
|
||||
|
||||
detect_default_if() {
|
||||
local def_if
|
||||
def_if=$(ip -4 route ls 2>/dev/null | grep default | grep -Po '(?<=dev )(\S+)' | head -1 || true)
|
||||
if [[ -z "$def_if" ]]; then
|
||||
echo "eth0"
|
||||
else
|
||||
echo "$def_if"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# HEALTH CHECK
|
||||
# ============================================================
|
||||
@@ -439,7 +449,7 @@ main() {
|
||||
awk '
|
||||
BEGIN { RS="\n\\[Peer\\]\n"; FS="\n" }
|
||||
NR>1 {
|
||||
ip=""; access=""
|
||||
ip=""; access=""; internet="false"
|
||||
for(i=1;i<=NF;i++){
|
||||
if($i ~ /^AllowedIPs/) {
|
||||
split($i,a," = ")
|
||||
@@ -452,12 +462,17 @@ main() {
|
||||
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\n", ip, access
|
||||
printf "%s|%s|%s\n", ip, access, internet
|
||||
}
|
||||
}
|
||||
' "$WG_CONF" | while IFS="|" read -r ip access_string; do
|
||||
' "$WG_CONF" | while IFS="|" read -r ip access_string internet_flag; do
|
||||
|
||||
# === VALIDATE CLIENT IP ===
|
||||
if ! validate_cidr "$ip"; then
|
||||
@@ -497,8 +512,8 @@ main() {
|
||||
fi
|
||||
|
||||
# Write to temp policy
|
||||
jq --arg ip "$ip" --argjson access "$ACCESS_JSON" \
|
||||
'.clients[$ip] = {"name": $ip, "access": $access}' \
|
||||
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
|
||||
@@ -622,14 +637,38 @@ main() {
|
||||
echo "$LAN_SUBNETS" | while read -r s; do log_info " $s"; done
|
||||
fi
|
||||
|
||||
# === BASE ROUTING & NAT ===
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true
|
||||
if command -v ip6tables &>/dev/null; then
|
||||
sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if ! iptables -C FORWARD -o "$WG_IF" -j ACCEPT 2>/dev/null; then
|
||||
iptables -I FORWARD 1 -o "$WG_IF" -j ACCEPT
|
||||
log_info "Added FORWARD rule for return traffic to $WG_IF"
|
||||
fi
|
||||
|
||||
if ! iptables -t nat -C POSTROUTING -o "$DEF_IF" -j MASQUERADE 2>/dev/null; then
|
||||
iptables -t nat -A POSTROUTING -o "$DEF_IF" -j MASQUERADE
|
||||
log_info "Enabled IPv4 MASQUERADE on $DEF_IF"
|
||||
fi
|
||||
|
||||
# === CLEANUP OLD CHAIN (loop until all references removed) ===
|
||||
log_info "Cleaning up old chain references..."
|
||||
while iptables -D FORWARD -i "$WG_IF" -j "$CHAIN" 2>/dev/null; do :; done
|
||||
while true; do
|
||||
local rline=""
|
||||
rline=$(iptables -nL FORWARD --line-numbers 2>/dev/null | grep "$CHAIN" | awk '{print $1}' | head -n 1 || true)
|
||||
if [[ -n "$rline" ]]; then
|
||||
iptables -D FORWARD "$rline" 2>/dev/null || break
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Backup existing chain before flushing
|
||||
if iptables -L "$CHAIN" -n &>/dev/null; then
|
||||
iptables -N "$CHAIN_BACKUP" 2>/dev/null || iptables -F "$CHAIN_BACKUP"
|
||||
iptables-save -c | grep "^-A $CHAIN" | \
|
||||
iptables-save -c 2>/dev/null | grep "^-A $CHAIN" | \
|
||||
sed "s/-A $CHAIN/-A $CHAIN_BACKUP/" | \
|
||||
iptables-restore -c 2>/dev/null || true
|
||||
log_info "Backed up existing chain to $CHAIN_BACKUP"
|
||||
@@ -646,7 +685,7 @@ main() {
|
||||
fi
|
||||
log_info "Chain $CHAIN created and linked to FORWARD"
|
||||
|
||||
# === POPULATE IPSET (for large-scale whitelist) ===
|
||||
# === POPULATE IPSET (hash:net,net for source->target mapping) ===
|
||||
local use_ipset=false
|
||||
if has_ipset; then
|
||||
use_ipset=true
|
||||
@@ -663,7 +702,7 @@ main() {
|
||||
flush_ipset "$IPSET_V6"
|
||||
fi
|
||||
|
||||
# Read all access entries and populate ipset
|
||||
# Read all access entries and populate ipset (client_ip,target)
|
||||
jq -r '
|
||||
.clients // {} | to_entries[] |
|
||||
select(.value.access != null and (.value.access | length > 0)) |
|
||||
@@ -675,12 +714,12 @@ main() {
|
||||
|
||||
if [[ "$target" == *":"* ]]; then
|
||||
if [[ "$use_ipv6" == true ]]; then
|
||||
ipset add "$IPSET_V6" "$target" 2>/dev/null || \
|
||||
log_warn "Failed to add $target to ipset $IPSET_V6"
|
||||
ipset add "$IPSET_V6" "${client_ip},${target}" 2>/dev/null || \
|
||||
log_warn "Failed to add ${client_ip},${target} to ipset $IPSET_V6"
|
||||
fi
|
||||
else
|
||||
ipset add "$IPSET_V4" "$target" 2>/dev/null || \
|
||||
log_warn "Failed to add $target to ipset $IPSET_V4"
|
||||
ipset add "$IPSET_V4" "${client_ip},${target}" 2>/dev/null || \
|
||||
log_warn "Failed to add ${client_ip},${target} to ipset $IPSET_V4"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -701,21 +740,10 @@ main() {
|
||||
|
||||
# === RULE 2: WHITELIST (per-client source) ===
|
||||
if [[ "$use_ipset" == true ]]; then
|
||||
jq -r '
|
||||
.clients // {} | to_entries[] |
|
||||
select(.value.access != null and (.value.access | length > 0)) |
|
||||
"\(.key)"
|
||||
' "$POLICY_FILE" 2>/dev/null | while read -r client_ip; do
|
||||
[[ -z "$client_ip" ]] && continue
|
||||
|
||||
if [[ "$client_ip" == *":"* ]]; then
|
||||
if [[ "$use_ipv6" == true ]]; then
|
||||
ip6tables -A "$CHAIN" -s "$client_ip" -m set --match-set "$IPSET_V6" dst -j ACCEPT 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
iptables -A "$CHAIN" -s "$client_ip" -m set --match-set "$IPSET_V4" dst -j ACCEPT
|
||||
fi
|
||||
done
|
||||
iptables -A "$CHAIN" -m set --match-set "$IPSET_V4" src,dst -j ACCEPT
|
||||
if [[ "$use_ipv6" == true ]]; then
|
||||
ip6tables -A "$CHAIN" -m set --match-set "$IPSET_V6" src,dst -j ACCEPT 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
jq -r '
|
||||
.clients // {} | to_entries[] |
|
||||
@@ -777,8 +805,24 @@ main() {
|
||||
log_info "IPv6 LAN block applied (link-local + ULA)"
|
||||
fi
|
||||
|
||||
# === RULE 5: LOGGING (rate-limited) — BEFORE final ACCEPT ===
|
||||
# FIXED: LOG was after ACCEPT in original, now placed before final rule
|
||||
# === RULE 5: INTERNET ACCESS (#Internet = true) ===
|
||||
jq -r '
|
||||
.clients // {} | to_entries[] |
|
||||
select(.value.internet == "true") |
|
||||
"\(.key)"
|
||||
' "$POLICY_FILE" 2>/dev/null | while read -r client_ip; do
|
||||
[[ -z "$client_ip" ]] && continue
|
||||
|
||||
if [[ "$client_ip" == *":"* ]]; then
|
||||
if [[ "$use_ipv6" == true ]]; then
|
||||
ip6tables -A "$CHAIN" -s "$client_ip" -j ACCEPT 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
iptables -A "$CHAIN" -s "$client_ip" -j ACCEPT
|
||||
fi
|
||||
done
|
||||
|
||||
# === RULE 6: LOGGING (rate-limited) — BEFORE final DROP ===
|
||||
iptables -A "$CHAIN" \
|
||||
-m limit --limit "$LOG_RATE" \
|
||||
-j LOG --log-prefix "${LOG_PREFIX}: " --log-level 4
|
||||
@@ -789,11 +833,11 @@ main() {
|
||||
-j LOG --log-prefix "${LOG_PREFIX}: " --log-level 4 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# === RULE 6: DEFAULT ACCEPT (internet access) ===
|
||||
iptables -A "$CHAIN" -j ACCEPT
|
||||
# === RULE 7: DEFAULT DROP (internet block by default) ===
|
||||
iptables -A "$CHAIN" -j DROP
|
||||
|
||||
if [[ "$use_ipv6" == true ]]; then
|
||||
ip6tables -A "$CHAIN" -j ACCEPT 2>/dev/null || true
|
||||
ip6tables -A "$CHAIN" -j DROP 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# === CLEANUP BACKUP CHAIN (no rollback needed anymore) ===
|
||||
@@ -821,9 +865,8 @@ main "$@"
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# wg-policy-cleanup.sh — Clean removal of all policy artifacts
|
||||
# Fixed: proper loop, ipset cleanup, IPv6 cleanup
|
||||
|
||||
set -euo pipefail
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/wg-policy-lib.sh"
|
||||
@@ -831,10 +874,26 @@ source "${SCRIPT_DIR}/wg-policy-lib.sh"
|
||||
main() {
|
||||
log_info "Starting cleanup..."
|
||||
|
||||
local DEF_IF
|
||||
DEF_IF="$(detect_default_if)"
|
||||
|
||||
# === Base Routing Cleanup ===
|
||||
while iptables -D FORWARD -o "$WG_IF" -j ACCEPT 2>/dev/null; do :; done
|
||||
while iptables -t nat -D POSTROUTING -o "$DEF_IF" -j MASQUERADE 2>/dev/null; do :; done
|
||||
log_info "Removed base routing and NAT rules"
|
||||
|
||||
# === IPv4 chain cleanup ===
|
||||
local removed=0
|
||||
while iptables -D FORWARD -i "$WG_IF" -j "$CHAIN" 2>/dev/null; do
|
||||
(( removed++ ))
|
||||
|
||||
while true; do
|
||||
local line=""
|
||||
line=$(iptables -nL FORWARD --line-numbers 2>/dev/null | grep "$CHAIN" | awk '{print $1}' | head -n 1 || true)
|
||||
if [[ -n "$line" ]]; then
|
||||
iptables -D FORWARD "$line" 2>/dev/null || break
|
||||
(( removed++ ))
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if (( removed > 0 )); then
|
||||
@@ -850,7 +909,15 @@ main() {
|
||||
|
||||
# === IPv6 chain cleanup ===
|
||||
if command -v ip6tables &>/dev/null; then
|
||||
while ip6tables -D FORWARD -i "$WG_IF" -j "$CHAIN" 2>/dev/null; do :; done
|
||||
while true; do
|
||||
local line6=""
|
||||
line6=$(ip6tables -nL FORWARD --line-numbers 2>/dev/null | grep "$CHAIN" | awk '{print $1}' | head -n 1 || true)
|
||||
if [[ -n "$line6" ]]; then
|
||||
ip6tables -D FORWARD "$line6" 2>/dev/null || break
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
ip6tables -F "$CHAIN" 2>/dev/null || true
|
||||
ip6tables -X "$CHAIN" 2>/dev/null || true
|
||||
ip6tables -F "$CHAIN_BACKUP" 2>/dev/null || true
|
||||
@@ -1030,16 +1097,18 @@ systemctl start wg-policy-health.timer
|
||||
|
||||
## 4. Integrasi ke `wg0.conf` — Diperbaiki
|
||||
|
||||
> **⚠️ WireGuard does NOT support multiline values.** Every command must be on a `PostUp = ...` or `PostDown = ...` line. Bare commands without the `PostUp =` prefix will cause `Configuration parsing error`.
|
||||
|
||||
```ini
|
||||
[Interface]
|
||||
Address = 10.0.0.1/24
|
||||
ListenPort = 51820
|
||||
PrivateKey = <server-private-key>
|
||||
|
||||
# PostUp: sync policy + apply engine (with retry)
|
||||
PostUp = /usr/local/bin/wg-sync-policy.sh && /usr/local/bin/wg-policy-engine.sh
|
||||
# Policy engine: auto-handles NAT, IP forwarding, and dynamic firewall rules
|
||||
PostUp = /usr/local/bin/wg-sync-policy.sh; /usr/local/bin/wg-policy-engine.sh
|
||||
|
||||
# PostDown: safe cleanup
|
||||
# Policy engine: cleanup all firewall and routing traces
|
||||
PostDown = /usr/local/bin/wg-policy-cleanup.sh
|
||||
|
||||
[Peer]
|
||||
@@ -1050,7 +1119,7 @@ AllowedIPs = 10.0.0.2/32
|
||||
[Peer]
|
||||
PublicKey = <client2-pubkey>
|
||||
AllowedIPs = 10.0.0.3/32
|
||||
#Access 10.0.0.1/32
|
||||
#Access = 10.0.0.1/32
|
||||
|
||||
[Peer]
|
||||
PublicKey = <client3-pubkey>
|
||||
|
||||
@@ -54,28 +54,27 @@ To integrate the engine, you need to append hooks into your `wg0.conf` interface
|
||||
### 1. Interface Block (Hooks)
|
||||
Add the `PostUp` and `PostDown` scripts so the engine initializes correctly during VPN startup and removes traces upon shutdown.
|
||||
|
||||
> **⚠️ WireGuard does NOT support multiline values.** Every command must be on a `PostUp = ...` or `PostDown = ...` line. Bare commands without the `PostUp =` prefix will cause `Configuration parsing error`.
|
||||
|
||||
```ini
|
||||
[Interface]
|
||||
Address = 10.0.0.1/24
|
||||
ListenPort = 51820
|
||||
PrivateKey = <SERVER_PRIVATE_KEY>
|
||||
|
||||
# PostUp: sync policy + apply engine
|
||||
# default PostUp
|
||||
iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;
|
||||
# modification
|
||||
PostUp = /usr/local/bin/wg-sync-policy.sh && /usr/local/bin/wg-policy-engine.sh
|
||||
# Policy engine: auto-handles NAT, IP forwarding, and dynamic firewall rules
|
||||
PostUp = /usr/local/bin/wg-sync-policy.sh; /usr/local/bin/wg-policy-engine.sh
|
||||
|
||||
# PostDown: safe cleanup
|
||||
# default PostUp
|
||||
iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE;
|
||||
# modification
|
||||
# Policy engine: cleanup all firewall and routing traces
|
||||
PostDown = /usr/local/bin/wg-policy-cleanup.sh
|
||||
```
|
||||
|
||||
### 2. Peer Block (`#Access` Tags)
|
||||
For each client, use the `#Access` comment line. Define the destinations (targets) the peer is allowed to access. You can separate multiple IPs or CIDRs with commas or semicolons.
|
||||
|
||||
> **⚠️ WARNING: Do NOT use `SaveConfig = true`!**
|
||||
> WireGuard's `SaveConfig` feature overwrites `wg0.conf` directly and **strips all comments**, which will permanently delete all `#Access` tags. If you are using a Web UI/Dashboard, make sure it does not strip unknown comments when saving.
|
||||
|
||||
```ini
|
||||
[Peer]
|
||||
PublicKey = <CLIENT_1_PUBKEY>
|
||||
|
||||
+28
-5
@@ -1,8 +1,7 @@
|
||||
#!/bin/bash
|
||||
# wg-policy-cleanup.sh — Clean removal of all policy artifacts
|
||||
# Fixed: proper loop, ipset cleanup, IPv6 cleanup
|
||||
|
||||
set -euo pipefail
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/wg-policy-lib.sh"
|
||||
@@ -10,10 +9,26 @@ source "${SCRIPT_DIR}/wg-policy-lib.sh"
|
||||
main() {
|
||||
log_info "Starting cleanup..."
|
||||
|
||||
local DEF_IF
|
||||
DEF_IF="$(detect_default_if)"
|
||||
|
||||
# === Base Routing Cleanup ===
|
||||
while iptables -D FORWARD -o "$WG_IF" -j ACCEPT 2>/dev/null; do :; done
|
||||
while iptables -t nat -D POSTROUTING -o "$DEF_IF" -j MASQUERADE 2>/dev/null; do :; done
|
||||
log_info "Removed base routing and NAT rules"
|
||||
|
||||
# === IPv4 chain cleanup ===
|
||||
local removed=0
|
||||
while iptables -D FORWARD -i "$WG_IF" -j "$CHAIN" 2>/dev/null; do
|
||||
(( removed++ ))
|
||||
|
||||
while true; do
|
||||
local line=""
|
||||
line=$(iptables -nL FORWARD --line-numbers 2>/dev/null | grep "$CHAIN" | awk '{print $1}' | head -n 1 || true)
|
||||
if [[ -n "$line" ]]; then
|
||||
iptables -D FORWARD "$line" 2>/dev/null || break
|
||||
(( removed++ ))
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if (( removed > 0 )); then
|
||||
@@ -29,7 +44,15 @@ main() {
|
||||
|
||||
# === IPv6 chain cleanup ===
|
||||
if command -v ip6tables &>/dev/null; then
|
||||
while ip6tables -D FORWARD -i "$WG_IF" -j "$CHAIN" 2>/dev/null; do :; done
|
||||
while true; do
|
||||
local line6=""
|
||||
line6=$(ip6tables -nL FORWARD --line-numbers 2>/dev/null | grep "$CHAIN" | awk '{print $1}' | head -n 1 || true)
|
||||
if [[ -n "$line6" ]]; then
|
||||
ip6tables -D FORWARD "$line6" 2>/dev/null || break
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
ip6tables -F "$CHAIN" 2>/dev/null || true
|
||||
ip6tables -X "$CHAIN" 2>/dev/null || true
|
||||
ip6tables -F "$CHAIN_BACKUP" 2>/dev/null || true
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
#!/bin/bash
|
||||
# wg-policy-ctl — CLI management tool for WireGuard Policy Firewall
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "${SCRIPT_DIR}/wg-policy-lib.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") <command>
|
||||
|
||||
Commands:
|
||||
status Show full health check report
|
||||
policy Display current policy.json formatted
|
||||
rules Show current iptables rules in WG_POLICY chain
|
||||
ipset Show ipset contents
|
||||
log Tail WG_DROP logs (last 50 lines)
|
||||
reload Force re-sync and re-apply policy
|
||||
backup Manual backup of policy + iptables
|
||||
stats Show connection and rule statistics
|
||||
validate Validate policy.json without applying
|
||||
help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
echo "========================================="
|
||||
echo " WireGuard Policy Firewall Status"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
health_check
|
||||
}
|
||||
|
||||
cmd_policy() {
|
||||
if [[ -f "$POLICY_FILE" ]]; then
|
||||
jq '.' "$POLICY_FILE"
|
||||
else
|
||||
log_error "policy.json not found"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_rules() {
|
||||
echo "=== IPv4 Chain: $CHAIN ==="
|
||||
if iptables -L "$CHAIN" -n -v --line-numbers 2>/dev/null; then
|
||||
echo ""
|
||||
else
|
||||
echo "(chain not found)"
|
||||
fi
|
||||
|
||||
echo "=== FORWARD references ==="
|
||||
iptables -L FORWARD -n -v --line-numbers 2>/dev/null | grep -i "$CHAIN" || echo "(none)"
|
||||
|
||||
if command -v ip6tables &>/dev/null; then
|
||||
echo ""
|
||||
echo "=== IPv6 Chain: $CHAIN ==="
|
||||
ip6tables -L "$CHAIN" -n -v --line-numbers 2>/dev/null || echo "(chain not found)"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_ipset() {
|
||||
for set_name in "$IPSET_V4" "$IPSET_V6"; do
|
||||
echo "=== ipset: $set_name ==="
|
||||
if ipset list "$set_name" 2>/dev/null; then
|
||||
echo ""
|
||||
else
|
||||
echo "(not found)"
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_log() {
|
||||
echo "=== Recent WG_DROP log entries ==="
|
||||
(journalctl -k --no-pager -n 50 2>/dev/null || dmesg | tail -50) | grep "$LOG_PREFIX" || echo "(no entries)"
|
||||
}
|
||||
|
||||
cmd_reload() {
|
||||
log_info "Force reloading policy..."
|
||||
if retry /usr/local/bin/wg-sync-policy.sh; then
|
||||
if retry /usr/local/bin/wg-policy-engine.sh; then
|
||||
log_info "Reload complete"
|
||||
else
|
||||
log_error "Engine failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_error "Sync failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_backup() {
|
||||
backup_policy
|
||||
backup_iptables
|
||||
log_info "Manual backup complete. Files in: $BACKUP_DIR"
|
||||
}
|
||||
|
||||
cmd_stats() {
|
||||
echo "=== Client Count ==="
|
||||
jq '(.clients // {}) | length' "$POLICY_FILE" 2>/dev/null || echo "N/A"
|
||||
|
||||
echo ""
|
||||
echo "=== Clients with Access ==="
|
||||
jq -r '.clients // {} | to_entries[] | select(.value.access | length > 0) | "\(.key): \(.value.access | join(", "))"' "$POLICY_FILE" 2>/dev/null || echo "N/A"
|
||||
|
||||
echo ""
|
||||
echo "=== Clients without Access (Internet Only) ==="
|
||||
jq -r '.clients // {} | to_entries[] | select(.value.access | length == 0) | .key' "$POLICY_FILE" 2>/dev/null || echo "N/A"
|
||||
|
||||
echo ""
|
||||
echo "=== Active iptables rules ==="
|
||||
iptables -L "$CHAIN" -n 2>/dev/null | tail -n +3 | wc -l || echo "N/A"
|
||||
|
||||
echo ""
|
||||
echo "=== Drop count (since boot) ==="
|
||||
iptables -L "$CHAIN" -n -v 2>/dev/null | grep "DROP" | awk '{sum += $1} END {print sum+0, "packets dropped"}'
|
||||
|
||||
echo ""
|
||||
echo "=== ipset entries ==="
|
||||
for set_name in "$IPSET_V4" "$IPSET_V6"; do
|
||||
local count
|
||||
count=$(ipset list "$set_name" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0)
|
||||
echo " $set_name: $count entries"
|
||||
done
|
||||
}
|
||||
|
||||
cmd_validate() {
|
||||
log_info "Validating policy.json..."
|
||||
|
||||
if [[ ! -f "$POLICY_FILE" ]]; then
|
||||
log_error "File not found: $POLICY_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! jq empty "$POLICY_FILE" 2>/dev/null; then
|
||||
log_error "Invalid JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local errors=0
|
||||
local total=0
|
||||
|
||||
jq -r '.clients // {} | to_entries[] | "\(.key)|\(.value.access // [] | join(","))"' "$POLICY_FILE" | \
|
||||
while IFS="|" read -r ip access_str; do
|
||||
(( total++ ))
|
||||
|
||||
if ! validate_cidr "$ip"; then
|
||||
log_error "Invalid client IP: $ip"
|
||||
(( errors++ )) || true
|
||||
fi
|
||||
|
||||
if [[ -n "$access_str" ]]; then
|
||||
IFS=',' read -ra targets <<< "$access_str"
|
||||
for target in "${targets[@]}"; do
|
||||
if ! validate_cidr "$target"; then
|
||||
log_error "Invalid access target for $ip: $target"
|
||||
(( errors++ )) || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
if (( errors > 0 )); then
|
||||
log_error "Validation failed: $errors errors"
|
||||
exit 1
|
||||
else
|
||||
log_info "Validation passed: $total clients, 0 errors"
|
||||
fi
|
||||
}
|
||||
|
||||
# === DISPATCH ===
|
||||
case "${1:-help}" in
|
||||
status) cmd_status ;;
|
||||
policy) cmd_policy ;;
|
||||
rules) cmd_rules ;;
|
||||
ipset) cmd_ipset ;;
|
||||
log) cmd_log ;;
|
||||
reload) cmd_reload ;;
|
||||
backup) cmd_backup ;;
|
||||
stats) cmd_stats ;;
|
||||
validate) cmd_validate ;;
|
||||
help|*) usage ;;
|
||||
esac
|
||||
+78
-33
@@ -16,7 +16,15 @@ rollback() {
|
||||
log_error "ROLLBACK triggered! Restoring previous rules..."
|
||||
|
||||
# Remove new chain references
|
||||
while iptables -D FORWARD -i "$WG_IF" -j "$CHAIN" 2>/dev/null; do :; done
|
||||
while true; do
|
||||
local rline=""
|
||||
rline=$(iptables -nL FORWARD --line-numbers 2>/dev/null | grep "$CHAIN" | awk '{print $1}' | head -n 1 || true)
|
||||
if [[ -n "$rline" ]]; then
|
||||
iptables -D FORWARD "$rline" 2>/dev/null || break
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Flush and remove new chain
|
||||
iptables -F "$CHAIN" 2>/dev/null || true
|
||||
@@ -52,6 +60,11 @@ main() {
|
||||
log_info "Starting policy engine..."
|
||||
|
||||
# === VALIDATE ===
|
||||
if ! ip link show "$WG_IF" &>/dev/null; then
|
||||
log_error "Interface $WG_IF is not running. Aborting policy engine."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$POLICY_FILE" ]]; then
|
||||
log_error "Policy file not found: $POLICY_FILE"
|
||||
exit 1
|
||||
@@ -69,11 +82,12 @@ main() {
|
||||
trap 'rollback' ERR
|
||||
|
||||
# === DETECT SUBNETS ===
|
||||
local WG_SUBNET WG_SUBNET_V6 LAN_SUBNETS
|
||||
local WG_SUBNET WG_SUBNET_V6 LAN_SUBNETS DEF_IF
|
||||
|
||||
WG_SUBNET="$(detect_wg_subnet inet)"
|
||||
WG_SUBNET_V6="$(detect_wg_subnet inet6)"
|
||||
LAN_SUBNETS="$(detect_lan_subnets)"
|
||||
DEF_IF="$(detect_default_if)"
|
||||
|
||||
if [[ -z "$WG_SUBNET" ]]; then
|
||||
log_warn "Interface $WG_IF has no IPv4, skipping client isolation"
|
||||
@@ -90,14 +104,41 @@ main() {
|
||||
echo "$LAN_SUBNETS" | while read -r s; do log_info " $s"; done
|
||||
fi
|
||||
|
||||
# === BASE ROUTING & NAT ===
|
||||
# Enable IP Forwarding
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true
|
||||
if command -v ip6tables &>/dev/null; then
|
||||
sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Allow return traffic to wg interface
|
||||
if ! iptables -C FORWARD -o "$WG_IF" -j ACCEPT 2>/dev/null; then
|
||||
iptables -I FORWARD 1 -o "$WG_IF" -j ACCEPT
|
||||
log_info "Added FORWARD rule for return traffic to $WG_IF"
|
||||
fi
|
||||
|
||||
# Setup MASQUERADE on default interface
|
||||
if ! iptables -t nat -C POSTROUTING -o "$DEF_IF" -j MASQUERADE 2>/dev/null; then
|
||||
iptables -t nat -A POSTROUTING -o "$DEF_IF" -j MASQUERADE
|
||||
log_info "Enabled IPv4 MASQUERADE on $DEF_IF"
|
||||
fi
|
||||
|
||||
# === CLEANUP OLD CHAIN (loop until all references removed) ===
|
||||
log_info "Cleaning up old chain references..."
|
||||
while iptables -D FORWARD -i "$WG_IF" -j "$CHAIN" 2>/dev/null; do :; done
|
||||
while true; do
|
||||
local rline=""
|
||||
rline=$(iptables -nL FORWARD --line-numbers 2>/dev/null | grep "$CHAIN" | awk '{print $1}' | head -n 1 || true)
|
||||
if [[ -n "$rline" ]]; then
|
||||
iptables -D FORWARD "$rline" 2>/dev/null || break
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Backup existing chain before flushing
|
||||
if iptables -L "$CHAIN" -n &>/dev/null; then
|
||||
iptables -N "$CHAIN_BACKUP" 2>/dev/null || iptables -F "$CHAIN_BACKUP"
|
||||
iptables-save -c | grep "^-A $CHAIN" | \
|
||||
iptables-save -c 2>/dev/null | grep "^-A $CHAIN" | \
|
||||
sed "s/-A $CHAIN/-A $CHAIN_BACKUP/" | \
|
||||
iptables-restore -c 2>/dev/null || true
|
||||
log_info "Backed up existing chain to $CHAIN_BACKUP"
|
||||
@@ -114,7 +155,7 @@ main() {
|
||||
fi
|
||||
log_info "Chain $CHAIN created and linked to FORWARD"
|
||||
|
||||
# === POPULATE IPSET (for large-scale whitelist) ===
|
||||
# === POPULATE IPSET (hash:net,net for source->target mapping) ===
|
||||
local use_ipset=false
|
||||
if has_ipset; then
|
||||
use_ipset=true
|
||||
@@ -131,7 +172,7 @@ main() {
|
||||
flush_ipset "$IPSET_V6"
|
||||
fi
|
||||
|
||||
# Read all access entries and populate ipset
|
||||
# Read all access entries and populate ipset (client_ip,target)
|
||||
jq -r '
|
||||
.clients // {} | to_entries[] |
|
||||
select(.value.access != null and (.value.access | length > 0)) |
|
||||
@@ -143,12 +184,12 @@ main() {
|
||||
|
||||
if [[ "$target" == *":"* ]]; then
|
||||
if [[ "$use_ipv6" == true ]]; then
|
||||
ipset add "$IPSET_V6" "$target" 2>/dev/null || \
|
||||
log_warn "Failed to add $target to ipset $IPSET_V6"
|
||||
ipset add "$IPSET_V6" "${client_ip},${target}" 2>/dev/null || \
|
||||
log_warn "Failed to add ${client_ip},${target} to ipset $IPSET_V6"
|
||||
fi
|
||||
else
|
||||
ipset add "$IPSET_V4" "$target" 2>/dev/null || \
|
||||
log_warn "Failed to add $target to ipset $IPSET_V4"
|
||||
ipset add "$IPSET_V4" "${client_ip},${target}" 2>/dev/null || \
|
||||
log_warn "Failed to add ${client_ip},${target} to ipset $IPSET_V4"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -169,21 +210,10 @@ main() {
|
||||
|
||||
# === RULE 2: WHITELIST (per-client source) ===
|
||||
if [[ "$use_ipset" == true ]]; then
|
||||
jq -r '
|
||||
.clients // {} | to_entries[] |
|
||||
select(.value.access != null and (.value.access | length > 0)) |
|
||||
"\(.key)"
|
||||
' "$POLICY_FILE" 2>/dev/null | while read -r client_ip; do
|
||||
[[ -z "$client_ip" ]] && continue
|
||||
|
||||
if [[ "$client_ip" == *":"* ]]; then
|
||||
if [[ "$use_ipv6" == true ]]; then
|
||||
ip6tables -A "$CHAIN" -s "$client_ip" -m set --match-set "$IPSET_V6" dst -j ACCEPT 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
iptables -A "$CHAIN" -s "$client_ip" -m set --match-set "$IPSET_V4" dst -j ACCEPT
|
||||
fi
|
||||
done
|
||||
iptables -A "$CHAIN" -m set --match-set "$IPSET_V4" src,dst -j ACCEPT
|
||||
if [[ "$use_ipv6" == true ]]; then
|
||||
ip6tables -A "$CHAIN" -m set --match-set "$IPSET_V6" src,dst -j ACCEPT 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
jq -r '
|
||||
.clients // {} | to_entries[] |
|
||||
@@ -224,12 +254,11 @@ main() {
|
||||
fi
|
||||
|
||||
# === RULE 4: BLOCK LAN — drop from WG subnet to private LAN ===
|
||||
# FIXED: iterate per LAN subnet, block from WG_SUBNET (not per-client IP)
|
||||
if [[ -n "$WG_SUBNET" && -n "$LAN_SUBNETS" ]]; then
|
||||
echo "$LAN_SUBNETS" | while read -r subnet; do
|
||||
[[ -z "$subnet" ]] && continue
|
||||
# Skip if LAN subnet overlaps with WG subnet
|
||||
[[ -n "$WG_SUBNET" && "$subnet" == "$WG_SUBNET" ]] && continue
|
||||
# Skip if LAN subnet exactly matches WG subnet (handled by Rule 3)
|
||||
[[ "$subnet" == "$WG_SUBNET" ]] && continue
|
||||
|
||||
iptables -A "$CHAIN" -s "$WG_SUBNET" -d "$subnet" -j DROP
|
||||
log_info "Block: $WG_SUBNET → $subnet"
|
||||
@@ -245,8 +274,24 @@ main() {
|
||||
log_info "IPv6 LAN block applied (link-local + ULA)"
|
||||
fi
|
||||
|
||||
# === RULE 5: LOGGING (rate-limited) — BEFORE final ACCEPT ===
|
||||
# FIXED: LOG was after ACCEPT in original, now placed before final rule
|
||||
# === RULE 5: INTERNET ACCESS (#Internet = true) ===
|
||||
jq -r '
|
||||
.clients // {} | to_entries[] |
|
||||
select(.value.internet == "true") |
|
||||
"\(.key)"
|
||||
' "$POLICY_FILE" 2>/dev/null | while read -r client_ip; do
|
||||
[[ -z "$client_ip" ]] && continue
|
||||
|
||||
if [[ "$client_ip" == *":"* ]]; then
|
||||
if [[ "$use_ipv6" == true ]]; then
|
||||
ip6tables -A "$CHAIN" -s "$client_ip" -j ACCEPT 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
iptables -A "$CHAIN" -s "$client_ip" -j ACCEPT
|
||||
fi
|
||||
done
|
||||
|
||||
# === RULE 6: LOGGING (rate-limited) — BEFORE final DROP ===
|
||||
iptables -A "$CHAIN" \
|
||||
-m limit --limit "$LOG_RATE" \
|
||||
-j LOG --log-prefix "${LOG_PREFIX}: " --log-level 4
|
||||
@@ -257,11 +302,11 @@ main() {
|
||||
-j LOG --log-prefix "${LOG_PREFIX}: " --log-level 4 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# === RULE 6: DEFAULT ACCEPT (internet access) ===
|
||||
iptables -A "$CHAIN" -j ACCEPT
|
||||
# === RULE 7: DEFAULT DROP (internet block by default) ===
|
||||
iptables -A "$CHAIN" -j DROP
|
||||
|
||||
if [[ "$use_ipv6" == true ]]; then
|
||||
ip6tables -A "$CHAIN" -j ACCEPT 2>/dev/null || true
|
||||
ip6tables -A "$CHAIN" -j DROP 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# === CLEANUP BACKUP CHAIN (no rollback needed anymore) ===
|
||||
|
||||
+11
-1
@@ -137,7 +137,7 @@ ensure_ipset() {
|
||||
local name="$1" family="$2"
|
||||
has_ipset || return 0
|
||||
if ! ipset list "$name" &>/dev/null; then
|
||||
ipset create "$name" hash:net family "$family" hashsize 1024 maxelem 65536 timeout 0
|
||||
ipset create "$name" hash:net,net family "$family" hashsize 1024 maxelem 65536 timeout 0
|
||||
log_info "Created ipset: $name (family=$family)"
|
||||
fi
|
||||
}
|
||||
@@ -278,6 +278,16 @@ detect_wg_subnet() {
|
||||
fi
|
||||
}
|
||||
|
||||
detect_default_if() {
|
||||
local def_if
|
||||
def_if=$(ip -4 route ls 2>/dev/null | grep default | grep -Po '(?<=dev )(\S+)' | head -1 || true)
|
||||
if [[ -z "$def_if" ]]; then
|
||||
echo "eth0"
|
||||
else
|
||||
echo "$def_if"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# HEALTH CHECK
|
||||
# ============================================================
|
||||
|
||||
+10
-5
@@ -46,7 +46,7 @@ main() {
|
||||
awk '
|
||||
BEGIN { RS="\n\\[Peer\\]\n"; FS="\n" }
|
||||
NR>1 {
|
||||
ip=""; access=""
|
||||
ip=""; access=""; internet="false"
|
||||
for(i=1;i<=NF;i++){
|
||||
if($i ~ /^AllowedIPs/) {
|
||||
split($i,a," = ")
|
||||
@@ -59,12 +59,17 @@ main() {
|
||||
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\n", ip, access
|
||||
printf "%s|%s|%s\n", ip, access, internet
|
||||
}
|
||||
}
|
||||
' "$WG_CONF" | while IFS="|" read -r ip access_string; do
|
||||
' "$WG_CONF" | while IFS="|" read -r ip access_string internet_flag; do
|
||||
|
||||
# === VALIDATE CLIENT IP ===
|
||||
if ! validate_cidr "$ip"; then
|
||||
@@ -104,8 +109,8 @@ main() {
|
||||
fi
|
||||
|
||||
# Write to temp policy
|
||||
jq --arg ip "$ip" --argjson access "$ACCESS_JSON" \
|
||||
'.clients[$ip] = {"name": $ip, "access": $access}' \
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user