fix: resolve ghost iptables rules, set -e crashes, and automate MASQUERADE NAT

This commit is contained in:
datadunia
2026-05-01 13:47:50 +07:00
parent 1268c8a4a2
commit 0b19a9061f
7 changed files with 432 additions and 95 deletions
+111 -42
View File
@@ -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>