1 Commits

Author SHA1 Message Date
datadunia b5c9b180dc feat: migrate policy firewall from iptables/ipset to nftables
- Rewrite wg-policy-lib.sh: replace ipset functions with nft helpers, add backup_nftables()
- Rewrite wg-policy-engine.sh: generate nft ruleset file, atomic load via nft -f, rollback support
- Simplify wg-policy-cleanup.sh: single nft delete table inet wg_policy
- Update wg-policy-ctl: nft commands for rules/ipset/stats/backup
- Rebuild install.sh: nftables dependency, WireGuard pre-check, PostUp/PostDown auto-integration
- Update build.sh/build.bat: match install.sh changes
- Update README.md: nftables prerequisites and references
2026-06-21 15:02:48 +07:00
8 changed files with 705 additions and 821 deletions
+8 -10
View File
@@ -2,7 +2,7 @@
A complete WireGuard management solution combining two powerful components: A complete WireGuard management solution combining two powerful components:
1. **WireGuard Dynamic Policy Firewall** - A lightweight, robust iptables/ipset policy engine that restricts and controls WireGuard peer traffic directly from `wg0.conf` using custom `#Access` comments. 1. **WireGuard Dynamic Policy Firewall** - A lightweight, robust nftables policy engine that restricts and controls WireGuard peer traffic directly from `wg0.conf` using custom `#Access` comments.
2. **WGRplane** - A Go-native control plane application with Vue 3 frontend, providing a modern web dashboard for WireGuard management with real-time monitoring, peer CRUD, and hybrid firewall enforcement. 2. **WGRplane** - A Go-native control plane application with Vue 3 frontend, providing a modern web dashboard for WireGuard management with real-time monitoring, peer CRUD, and hybrid firewall enforcement.
--- ---
@@ -31,7 +31,7 @@ A complete WireGuard management solution combining two powerful components:
1. **`wg0.conf`**: The standard WireGuard configuration. Contains standard `[Peer]` configs alongside a custom `#Access` tag. 1. **`wg0.conf`**: The standard WireGuard configuration. Contains standard `[Peer]` configs alongside a custom `#Access` tag.
2. **`wg-sync-policy.sh`**: Safely parses `wg0.conf` and generates a structured `/etc/wireguard/policy.json` atomically. 2. **`wg-sync-policy.sh`**: Safely parses `wg0.conf` and generates a structured `/etc/wireguard/policy.json` atomically.
3. **`wg-policy-engine.sh`**: Reads `policy.json` to generate robust rules, applying `iptables` and `ipset` directly to the system. 3. **`wg-policy-engine.sh`**: Reads `policy.json` to generate robust rules, applying nftables directly to the system.
4. **Watcher Daemon**: Monitors `wg0.conf` for changes via `inotifywait` and triggers the pipeline seamlessly when updates are made. 4. **Watcher Daemon**: Monitors `wg0.conf` for changes via `inotifywait` and triggers the pipeline seamlessly when updates are made.
--- ---
@@ -70,9 +70,7 @@ build.bat
|---------|----------|---------| |---------|----------|---------|
| `jq` | **Yes** | `apt install jq` | | `jq` | **Yes** | `apt install jq` |
| `inotify-tools` | **Yes** (for watcher daemon) | `apt install inotify-tools` | | `inotify-tools` | **Yes** (for watcher daemon) | `apt install inotify-tools` |
| `ipset` | Optional | `apt install ipset` | | `nftables` | **Yes** | `apt install nftables` |
If `ipset` is not installed, the engine will automatically fall back to per-rule `iptables` whitelist entries. This works fine for small deployments. For large numbers of clients/targets, `ipset` is recommended for O(1) lookup performance.
--- ---
@@ -129,7 +127,7 @@ WireGuard uses `AllowedIPs` for Cryptokey Routing (deciding which tunnel interfa
## 🛠 `wg-policy-ctl` CLI Usage ## 🛠 `wg-policy-ctl` CLI Usage
You don't need to manually interact with `iptables` or `.json` files. Use the `wg-policy-ctl` wrapper. You don't need to manually interact with `nft` or `.json` files. Use the `wg-policy-ctl` wrapper.
```bash ```bash
# View the health of the firewall engine and active locks # View the health of the firewall engine and active locks
@@ -138,10 +136,10 @@ wg-policy-ctl status
# View the raw, parsed JSON policy # View the raw, parsed JSON policy
wg-policy-ctl policy wg-policy-ctl policy
# Inspect active iptables rules # Inspect active nftables rules
wg-policy-ctl rules wg-policy-ctl rules
# Check memory sets mapping IP targets (ipset) # Check nftables set contents
wg-policy-ctl ipset wg-policy-ctl ipset
# Manually re-sync rules immediately # Manually re-sync rules immediately
@@ -278,7 +276,7 @@ Peers without any rules are isolated from other peers and the internet by defaul
| **Scheduling** | robfig/cron v3 | | **Scheduling** | robfig/cron v3 |
| **QR Code** | skip2/go-qrcode | | **QR Code** | skip2/go-qrcode |
| **Email** | jordan-wright/email (SMTP) | | **Email** | jordan-wright/email (SMTP) |
| **Firewall** | Bash, iptables, ipset, nftables, inotify-tools, jq | | **Firewall** | Bash, nftables, inotify-tools, jq |
| **Container** | Docker (multi-stage build), docker-compose | | **Container** | Docker (multi-stage build), docker-compose |
--- ---
@@ -403,7 +401,7 @@ On first run, it generates a default configuration file at `~/.config/wgrplane/c
│ ├── frontend/ # Vue 3 SPA (TypeScript, TailwindCSS) │ ├── frontend/ # Vue 3 SPA (TypeScript, TailwindCSS)
│ └── docs/ # Swagger documentation │ └── docs/ # Swagger documentation
├── wg-sync-policy.sh # Parse wg0.conf → policy.json ├── wg-sync-policy.sh # Parse wg0.conf → policy.json
├── wg-policy-engine.sh # Apply policy.json → iptables/ipset ├── wg-policy-engine.sh # Apply policy.json → nftables
├── wg-sync-watch.sh # inotifywait watcher daemon ├── wg-sync-watch.sh # inotifywait watcher daemon
├── wg-policy-ctl # CLI wrapper for policy management ├── wg-policy-ctl # CLI wrapper for policy management
├── wg-policy-cleanup.sh # Cleanup script for PostDown ├── wg-policy-cleanup.sh # Cleanup script for PostDown
+71 -1
View File
@@ -16,12 +16,78 @@ echo Building %INSTALL_SCRIPT%...
>> "%INSTALL_SCRIPT%" echo exit 1 >> "%INSTALL_SCRIPT%" echo exit 1
>> "%INSTALL_SCRIPT%" echo fi >> "%INSTALL_SCRIPT%" echo fi
>> "%INSTALL_SCRIPT%" echo. >> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo if ! command -v wg ^&>/dev/null; then
>> "%INSTALL_SCRIPT%" echo echo "[ERROR] WireGuard is not installed."
>> "%INSTALL_SCRIPT%" echo echo " Debian/Ubuntu: apt install wireguard"
>> "%INSTALL_SCRIPT%" echo echo " RHEL/CentOS: dnf install wireguard-tools"
>> "%INSTALL_SCRIPT%" echo echo " Arch: pacman -S wireguard-tools"
>> "%INSTALL_SCRIPT%" echo exit 1
>> "%INSTALL_SCRIPT%" echo fi
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo if [[ ! -f /etc/wireguard/wg0.conf ]]; then
>> "%INSTALL_SCRIPT%" echo echo "[WARN] WireGuard config not found: /etc/wireguard/wg0.conf"
>> "%INSTALL_SCRIPT%" echo echo " PostUp/PostDown hooks will not be added automatically."
>> "%INSTALL_SCRIPT%" echo echo " Create your wg0.conf first, then re-install."
>> "%INSTALL_SCRIPT%" echo fi
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo WG_CONF="/etc/wireguard/wg0.conf"
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo install_hooks^(^) {
>> "%INSTALL_SCRIPT%" echo if [[ ! -f "$WG_CONF" ]]; then
>> "%INSTALL_SCRIPT%" echo return 0
>> "%INSTALL_SCRIPT%" echo fi
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo if grep -q "wg-sync-policy.sh" "$WG_CONF" 2^>/dev/null; then
>> "%INSTALL_SCRIPT%" echo echo "[OK] PostUp/PostDown hooks already present in $WG_CONF"
>> "%INSTALL_SCRIPT%" echo return 0
>> "%INSTALL_SCRIPT%" echo fi
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo echo "Adding PostUp/PostDown hooks to $WG_CONF..."
>> "%INSTALL_SCRIPT%" echo cp "$WG_CONF" "${WG_CONF}.bak.$(date +%%Y%%m%%d%%H%%M%%S)"
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo local peer_line
>> "%INSTALL_SCRIPT%" echo peer_line=$(grep -n '^\[Peer\]' "$WG_CONF" ^| head -1 ^| cut -d: -f1)
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo if [[ -n "$peer_line" ]]; then
>> "%INSTALL_SCRIPT%" echo sed -i "${peer_line}i\\
>> "%INSTALL_SCRIPT%" echo # WireGuard Policy Firewall hooks\\
>> "%INSTALL_SCRIPT%" echo PostUp = /usr/local/bin/wg-sync-policy.sh; /usr/local/bin/wg-policy-engine.sh\\
>> "%INSTALL_SCRIPT%" echo PostDown = /usr/local/bin/wg-policy-cleanup.sh" "$WG_CONF"
>> "%INSTALL_SCRIPT%" echo else
>> "%INSTALL_SCRIPT%" echo {
>> "%INSTALL_SCRIPT%" echo echo ""
>> "%INSTALL_SCRIPT%" echo echo "# WireGuard Policy Firewall hooks"
>> "%INSTALL_SCRIPT%" echo echo "PostUp = /usr/local/bin/wg-sync-policy.sh; /usr/local/bin/wg-policy-engine.sh"
>> "%INSTALL_SCRIPT%" echo echo "PostDown = /usr/local/bin/wg-policy-cleanup.sh"
>> "%INSTALL_SCRIPT%" echo } ^>^> "$WG_CONF"
>> "%INSTALL_SCRIPT%" echo fi
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo echo "[OK] Hooks added to $WG_CONF"
>> "%INSTALL_SCRIPT%" echo }
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo remove_hooks^(^) {
>> "%INSTALL_SCRIPT%" echo if [[ ! -f "$WG_CONF" ]]; then
>> "%INSTALL_SCRIPT%" echo return 0
>> "%INSTALL_SCRIPT%" echo fi
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo if ! grep -q "wg-sync-policy.sh" "$WG_CONF" 2^>/dev/null; then
>> "%INSTALL_SCRIPT%" echo return 0
>> "%INSTALL_SCRIPT%" echo fi
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo echo "Removing PostUp/PostDown hooks from $WG_CONF..."
>> "%INSTALL_SCRIPT%" echo cp "$WG_CONF" "${WG_CONF}.bak.$(date +%%Y%%m%%d%%H%%M%%S)"
>> "%INSTALL_SCRIPT%" echo sed -i '/# WireGuard Policy Firewall hooks/d' "$WG_CONF"
>> "%INSTALL_SCRIPT%" echo sed -i '/wg-sync-policy\.sh/d' "$WG_CONF"
>> "%INSTALL_SCRIPT%" echo sed -i '/wg-policy-cleanup\.sh/d' "$WG_CONF"
>> "%INSTALL_SCRIPT%" echo echo "[OK] Hooks removed from $WG_CONF"
>> "%INSTALL_SCRIPT%" echo }
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo install_policy^(^) { >> "%INSTALL_SCRIPT%" echo install_policy^(^) {
>> "%INSTALL_SCRIPT%" echo echo "Installing WireGuard Policy Firewall..." >> "%INSTALL_SCRIPT%" echo echo "Installing WireGuard Policy Firewall..."
>> "%INSTALL_SCRIPT%" echo. >> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo echo "Checking dependencies..." >> "%INSTALL_SCRIPT%" echo echo "Checking dependencies..."
>> "%INSTALL_SCRIPT%" echo apt-get update -y ^|^| true >> "%INSTALL_SCRIPT%" echo apt-get update -y ^|^| true
>> "%INSTALL_SCRIPT%" echo apt-get install -y jq inotify-tools ipset iptables ^|^| true >> "%INSTALL_SCRIPT%" echo apt-get install -y jq inotify-tools nftables ^|^| true
>> "%INSTALL_SCRIPT%" echo. >> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo echo "Writing scripts to /usr/local/bin/..." >> "%INSTALL_SCRIPT%" echo echo "Writing scripts to /usr/local/bin/..."
>> "%INSTALL_SCRIPT%" echo. >> "%INSTALL_SCRIPT%" echo.
@@ -49,6 +115,8 @@ call :AppendFile wg-policy-health.timer /etc/systemd/system/wg-policy-health.tim
>> "%INSTALL_SCRIPT%" echo systemctl enable --now wg-policy.service >> "%INSTALL_SCRIPT%" echo systemctl enable --now wg-policy.service
>> "%INSTALL_SCRIPT%" echo systemctl enable --now wg-policy-health.timer >> "%INSTALL_SCRIPT%" echo systemctl enable --now wg-policy-health.timer
>> "%INSTALL_SCRIPT%" echo. >> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo install_hooks
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo echo "Installation complete!" >> "%INSTALL_SCRIPT%" echo echo "Installation complete!"
>> "%INSTALL_SCRIPT%" echo echo "You can check status with: wg-policy-ctl status" >> "%INSTALL_SCRIPT%" echo echo "You can check status with: wg-policy-ctl status"
>> "%INSTALL_SCRIPT%" echo } >> "%INSTALL_SCRIPT%" echo }
@@ -64,6 +132,8 @@ call :AppendFile wg-policy-health.timer /etc/systemd/system/wg-policy-health.tim
>> "%INSTALL_SCRIPT%" echo /usr/local/bin/wg-policy-cleanup.sh ^|^| true >> "%INSTALL_SCRIPT%" echo /usr/local/bin/wg-policy-cleanup.sh ^|^| true
>> "%INSTALL_SCRIPT%" echo fi >> "%INSTALL_SCRIPT%" echo fi
>> "%INSTALL_SCRIPT%" echo. >> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo remove_hooks
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo echo "Removing systemd units..." >> "%INSTALL_SCRIPT%" echo echo "Removing systemd units..."
>> "%INSTALL_SCRIPT%" echo rm -f /etc/systemd/system/wg-policy.service >> "%INSTALL_SCRIPT%" echo rm -f /etc/systemd/system/wg-policy.service
>> "%INSTALL_SCRIPT%" echo rm -f /etc/systemd/system/wg-policy-health.service >> "%INSTALL_SCRIPT%" echo rm -f /etc/systemd/system/wg-policy-health.service
+71 -1
View File
@@ -20,12 +20,78 @@ if [[ $EUID -ne 0 ]]; then
exit 1 exit 1
fi fi
if ! command -v wg &>/dev/null; then
echo "[ERROR] WireGuard is not installed."
echo " Debian/Ubuntu: apt install wireguard"
echo " RHEL/CentOS: dnf install wireguard-tools"
echo " Arch: pacman -S wireguard-tools"
exit 1
fi
if [[ ! -f /etc/wireguard/wg0.conf ]]; then
echo "[WARN] WireGuard config not found: /etc/wireguard/wg0.conf"
echo " PostUp/PostDown hooks will not be added automatically."
echo " Create your wg0.conf first, then re-install."
fi
WG_CONF="/etc/wireguard/wg0.conf"
install_hooks() {
if [[ ! -f "$WG_CONF" ]]; then
return 0
fi
if grep -q "wg-sync-policy.sh" "$WG_CONF" 2>/dev/null; then
echo "[OK] PostUp/PostDown hooks already present in $WG_CONF"
return 0
fi
echo "Adding PostUp/PostDown hooks to $WG_CONF..."
cp "$WG_CONF" "${WG_CONF}.bak.$(date +%Y%m%d%H%M%S)"
local peer_line
peer_line=$(grep -n '^\[Peer\]' "$WG_CONF" | head -1 | cut -d: -f1)
if [[ -n "$peer_line" ]]; then
sed -i "${peer_line}i\\
# WireGuard Policy Firewall hooks\\
PostUp = /usr/local/bin/wg-sync-policy.sh; /usr/local/bin/wg-policy-engine.sh\\
PostDown = /usr/local/bin/wg-policy-cleanup.sh" "$WG_CONF"
else
{
echo ""
echo "# WireGuard Policy Firewall hooks"
echo "PostUp = /usr/local/bin/wg-sync-policy.sh; /usr/local/bin/wg-policy-engine.sh"
echo "PostDown = /usr/local/bin/wg-policy-cleanup.sh"
} >> "$WG_CONF"
fi
echo "[OK] Hooks added to $WG_CONF"
}
remove_hooks() {
if [[ ! -f "$WG_CONF" ]]; then
return 0
fi
if ! grep -q "wg-sync-policy.sh" "$WG_CONF" 2>/dev/null; then
return 0
fi
echo "Removing PostUp/PostDown hooks from $WG_CONF..."
cp "$WG_CONF" "${WG_CONF}.bak.$(date +%Y%m%d%H%M%S)"
sed -i '/# WireGuard Policy Firewall hooks/d' "$WG_CONF"
sed -i '/wg-sync-policy\.sh/d' "$WG_CONF"
sed -i '/wg-policy-cleanup\.sh/d' "$WG_CONF"
echo "[OK] Hooks removed from $WG_CONF"
}
install_policy() { install_policy() {
echo "Installing WireGuard Policy Firewall..." echo "Installing WireGuard Policy Firewall..."
echo "Checking dependencies..." echo "Checking dependencies..."
apt-get update -y || true apt-get update -y || true
apt-get install -y jq inotify-tools ipset iptables || true apt-get install -y jq inotify-tools nftables || true
echo "Writing scripts to /usr/local/bin/..." echo "Writing scripts to /usr/local/bin/..."
@@ -75,6 +141,8 @@ cat << 'MAIN_EOF_END' >> "$INSTALL_SCRIPT"
systemctl enable --now wg-policy.service systemctl enable --now wg-policy.service
systemctl enable --now wg-policy-health.timer systemctl enable --now wg-policy-health.timer
install_hooks
echo "Installation complete!" echo "Installation complete!"
echo "You can check status with: wg-policy-ctl status" echo "You can check status with: wg-policy-ctl status"
} }
@@ -90,6 +158,8 @@ uninstall_policy() {
/usr/local/bin/wg-policy-cleanup.sh || true /usr/local/bin/wg-policy-cleanup.sh || true
fi fi
remove_hooks
echo "Removing systemd units..." echo "Removing systemd units..."
rm -f /etc/systemd/system/wg-policy.service rm -f /etc/systemd/system/wg-policy.service
rm -f /etc/systemd/system/wg-policy-health.service rm -f /etc/systemd/system/wg-policy-health.service
+302 -391
View File
File diff suppressed because it is too large Load Diff
+2 -56
View File
@@ -9,63 +9,9 @@ source "${SCRIPT_DIR}/wg-policy-lib.sh"
main() { main() {
log_info "Starting cleanup..." log_info "Starting cleanup..."
local DEF_IF nft delete table inet wg_policy 2>/dev/null || true
DEF_IF="$(detect_default_if)" log_info "Removed nftables table inet wg_policy"
# === 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 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
log_info "Removed $removed FORWARD references"
fi
iptables -F "$CHAIN" 2>/dev/null || true
iptables -X "$CHAIN" 2>/dev/null || true
# Cleanup backup chain too
iptables -F "$CHAIN_BACKUP" 2>/dev/null || true
iptables -X "$CHAIN_BACKUP" 2>/dev/null || true
# === IPv6 chain cleanup ===
if command -v ip6tables &>/dev/null; then
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
ip6tables -X "$CHAIN_BACKUP" 2>/dev/null || true
fi
# === ipset cleanup ===
destroy_ipset "$IPSET_V4" 2>/dev/null || true
destroy_ipset "$IPSET_V6" 2>/dev/null || true
destroy_ipset "${IPSET_V4}_bak" 2>/dev/null || true
destroy_ipset "${IPSET_V6}_bak" 2>/dev/null || true
# === Lock cleanup ===
rm -f "$LOCK_FILE" 2>/dev/null || true rm -f "$LOCK_FILE" 2>/dev/null || true
log_info "Cleanup complete" log_info "Cleanup complete"
+15 -28
View File
@@ -13,11 +13,11 @@ Usage: $(basename "$0") <command>
Commands: Commands:
status Show full health check report status Show full health check report
policy Display current policy.json formatted policy Display current policy.json formatted
rules Show current iptables rules in WG_POLICY chain rules Show current nftables rules
ipset Show ipset contents ipset Show nftables set contents
log Tail WG_DROP logs (last 50 lines) log Tail WG_DROP logs (last 50 lines)
reload Force re-sync and re-apply policy reload Force re-sync and re-apply policy
backup Manual backup of policy + iptables backup Manual backup of policy + nftables
stats Show connection and rule statistics stats Show connection and rule statistics
validate Validate policy.json without applying validate Validate policy.json without applying
help Show this help help Show this help
@@ -43,27 +43,18 @@ cmd_policy() {
} }
cmd_rules() { cmd_rules() {
echo "=== IPv4 Chain: $CHAIN ===" echo "=== Table: $NFT_TABLE_FULL ==="
if iptables -L "$CHAIN" -n -v --line-numbers 2>/dev/null; then if nft list table "$NFT_TABLE_FULL" 2>/dev/null; then
echo "" echo ""
else else
echo "(chain not found)" echo "(table 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 fi
} }
cmd_ipset() { cmd_ipset() {
for set_name in "$IPSET_V4" "$IPSET_V6"; do for set_name in "$NFT_SET_V4" "$NFT_SET_V6"; do
echo "=== ipset: $set_name ===" echo "=== nft set: $set_name ==="
if ipset list "$set_name" 2>/dev/null; then if nft list set "$NFT_TABLE_FULL" "$set_name" 2>/dev/null; then
echo "" echo ""
else else
echo "(not found)" echo "(not found)"
@@ -94,7 +85,7 @@ cmd_reload() {
cmd_backup() { cmd_backup() {
backup_policy backup_policy
backup_iptables backup_nftables
log_info "Manual backup complete. Files in: $BACKUP_DIR" log_info "Manual backup complete. Files in: $BACKUP_DIR"
} }
@@ -111,18 +102,14 @@ cmd_stats() {
jq -r '.clients // {} | to_entries[] | select(.value.access | length == 0) | .key' "$POLICY_FILE" 2>/dev/null || echo "N/A" jq -r '.clients // {} | to_entries[] | select(.value.access | length == 0) | .key' "$POLICY_FILE" 2>/dev/null || echo "N/A"
echo "" echo ""
echo "=== Active iptables rules ===" echo "=== Active nftables rules ==="
iptables -L "$CHAIN" -n 2>/dev/null | tail -n +3 | wc -l || echo "N/A" nft list chain "$NFT_TABLE_FULL" forward 2>/dev/null | grep -c '#' || echo "N/A"
echo "" echo ""
echo "=== Drop count (since boot) ===" echo "=== nft set entries ==="
iptables -L "$CHAIN" -n -v 2>/dev/null | grep "DROP" | awk '{sum += $1} END {print sum+0, "packets dropped"}' for set_name in "$NFT_SET_V4" "$NFT_SET_V6"; do
echo ""
echo "=== ipset entries ==="
for set_name in "$IPSET_V4" "$IPSET_V6"; do
local count local count
count=$(ipset list "$set_name" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0) count=$(nft list set "$NFT_TABLE_FULL" "$set_name" 2>/dev/null | grep -c -E '^\s+[0-9a-f]' || echo 0)
echo " $set_name: $count entries" echo " $set_name: $count entries"
done done
} }
+165 -220
View File
@@ -1,55 +1,152 @@
#!/bin/bash #!/bin/bash
# wg-policy-engine.sh — Applies iptables/ipset rules from policy.json # wg-policy-engine.sh — Applies nftables rules from policy.json
# Fixed: unquoted variables, LOG placement, LAN block targeting,
# atomic chain swap, ipset, IPv6 optional, rollback on failure
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/wg-policy-lib.sh" source "${SCRIPT_DIR}/wg-policy-lib.sh"
NFT_FILE="/tmp/wg-policy.nft"
NFT_BACKUP="/tmp/wg-policy-backup.nft"
# ============================================================ # ============================================================
# ROLLBACK # ROLLBACK
# ============================================================ # ============================================================
rollback() { rollback() {
log_error "ROLLBACK triggered! Restoring previous rules..." log_error "ROLLBACK triggered! Restoring previous ruleset..."
trap - ERR
# Remove new chain references nft delete table "$NFT_TABLE_FULL" 2>/dev/null || true
while true; do
local rline="" if [[ -f "$NFT_BACKUP" ]]; then
rline=$(iptables -nL FORWARD --line-numbers 2>/dev/null | grep "$CHAIN" | awk '{print $1}' | head -n 1 || true) if nft -f "$NFT_BACKUP" 2>/dev/null; then
if [[ -n "$rline" ]]; then log_info "Rollback: restored from backup"
iptables -D FORWARD "$rline" 2>/dev/null || break
else else
break log_error "Rollback: failed to restore from backup"
fi fi
done fi
}
# Flush and remove new chain # ============================================================
iptables -F "$CHAIN" 2>/dev/null || true # RULESET GENERATION
iptables -X "$CHAIN" 2>/dev/null || true # ============================================================
# Restore backup chain if it exists generate_ruleset() {
if iptables -L "$CHAIN_BACKUP" -n &>/dev/null; then local WG_SUBNET="$1"
# Rename backup chain to active local WG_SUBNET_V6="$2"
iptables -N "$CHAIN" 2>/dev/null || iptables -F "$CHAIN" local LAN_SUBNETS="$3"
# Copy rules from backup local DEF_IF="$4"
iptables-save -c | grep "^-A $CHAIN_BACKUP" | \
sed "s/-A $CHAIN_BACKUP/-A $CHAIN/" | \
iptables-restore -c 2>/dev/null || true
iptables -A FORWARD -i "$WG_IF" -j "$CHAIN" cat > "$NFT_FILE" << 'HEADER'
log_info "Rollback: restored from backup chain" #!/usr/sbin/nft -f
flush ruleset
table inet wg_policy {
set wg_allowed_v4 {
type ipv4_addr . ipv4_addr
flags interval
}
set wg_allowed_v6 {
type ipv6_addr . ipv6_addr
flags interval
}
chain forward {
type filter hook forward priority filter; policy accept;
ct state established,related accept
iifname "wg0" ip saddr . ip daddr @wg_allowed_v4 accept
iifname "wg0" ip6 saddr . ip6 daddr @wg_allowed_v6 accept
HEADER
# Client isolation (IPv4)
if [[ -n "$WG_SUBNET" ]]; then
cat >> "$NFT_FILE" << EOF
/* isolation: WG client to WG client */
iifname "wg0" ip saddr $WG_SUBNET ip daddr $WG_SUBNET ct state new drop
EOF
fi fi
# Cleanup backup chain # Client isolation (IPv6)
iptables -F "$CHAIN_BACKUP" 2>/dev/null || true if [[ -n "$WG_SUBNET_V6" ]]; then
iptables -X "$CHAIN_BACKUP" 2>/dev/null || true cat >> "$NFT_FILE" << EOF
# Cleanup backup ipsets /* IPv6 isolation */
destroy_ipset "${IPSET_V4}_bak" 2>/dev/null || true iifname "wg0" ip6 saddr $WG_SUBNET_V6 ip6 daddr $WG_SUBNET_V6 ct state new drop
destroy_ipset "${IPSET_V6}_bak" 2>/dev/null || true iifname "wg0" ip6 saddr $WG_SUBNET_V6 ip6 daddr fe80::/10 drop
iifname "wg0" ip6 saddr $WG_SUBNET_V6 ip6 daddr fc00::/7 drop
EOF
fi
# LAN block (IPv4)
if [[ -n "$WG_SUBNET" && -n "$LAN_SUBNETS" ]]; then
local lan_list=""
while IFS= read -r subnet; do
[[ -z "$subnet" ]] && continue
[[ "$subnet" == "$WG_SUBNET" ]] && continue
if [[ -n "$lan_list" ]]; then
lan_list+=", $subnet"
else
lan_list="$subnet"
fi
done <<< "$LAN_SUBNETS"
if [[ -n "$lan_list" ]]; then
cat >> "$NFT_FILE" << EOF
/* LAN block */
iifname "wg0" ip saddr $WG_SUBNET ip daddr { $lan_list } drop
EOF
fi
fi
# Internet access per client (inline in chain)
while IFS= read -r client_ip; do
[[ -z "$client_ip" ]] && continue
cat >> "$NFT_FILE" << EOF
/* internet access: $client_ip */
iifname "wg0" ip saddr $client_ip accept
EOF
done < <(jq -r '
.clients // {} | to_entries[] |
select(.value.internet == "true") |
.key
' "$POLICY_FILE" 2>/dev/null)
while IFS= read -r client_ip; do
[[ -z "$client_ip" ]] && continue
cat >> "$NFT_FILE" << EOF
/* internet access v6: $client_ip */
iifname "wg0" ip6 saddr $client_ip accept
EOF
done < <(jq -r '
.clients // {} | to_entries[] |
select(.value.internet == "true") |
select(.key | test(":")) |
.key
' "$POLICY_FILE" 2>/dev/null)
# Log + drop (final rule)
cat >> "$NFT_FILE" << EOF
/* log + drop */
iifname "wg0" limit rate ${LOG_RATE} log prefix "${LOG_PREFIX}: " drop
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "${DEF_IF}" masquerade
}
}
EOF
} }
# ============================================================ # ============================================================
@@ -75,8 +172,8 @@ main() {
exit 1 exit 1
fi fi
# Backup iptables state # Backup current nftables state
backup_iptables backup_nftables
# Set trap for rollback on failure # Set trap for rollback on failure
trap 'rollback' ERR trap 'rollback' ERR
@@ -105,221 +202,69 @@ main() {
fi fi
# === BASE ROUTING & NAT === # === BASE ROUTING & NAT ===
# Enable IP Forwarding
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true 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 sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true
# === BACKUP EXISTING TABLE ===
if nft list table "$NFT_TABLE_FULL" &>/dev/null; then
nft list table "$NFT_TABLE_FULL" > "$NFT_BACKUP" 2>/dev/null || true
log_info "Backed up existing table to $NFT_BACKUP"
fi fi
# Setup MASQUERADE on default interface # === DELETE OLD TABLE ===
if ! iptables -t nat -C POSTROUTING -o "$DEF_IF" -j MASQUERADE 2>/dev/null; then nft delete table "$NFT_TABLE_FULL" 2>/dev/null || true
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) === # === GENERATE AND LOAD NEW RULESET ===
log_info "Cleaning up old chain references..." generate_ruleset "$WG_SUBNET" "$WG_SUBNET_V6" "$LAN_SUBNETS" "$DEF_IF"
while true; do log_info "Generated ruleset: $NFT_FILE"
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 nft -f "$NFT_FILE"
if iptables -L "$CHAIN" -n &>/dev/null; then log_info "Loaded nftables ruleset from $NFT_FILE"
iptables -N "$CHAIN_BACKUP" 2>/dev/null || iptables -F "$CHAIN_BACKUP"
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"
fi
iptables -F "$CHAIN" 2>/dev/null || true # === POPULATE SETS ===
iptables -X "$CHAIN" 2>/dev/null || true log_info "Populating whitelist sets..."
# === CREATE FRESH CHAIN === local v4_count=0
iptables -N "$CHAIN" local v6_count=0
if ! iptables -C FORWARD -i "$WG_IF" -j "$CHAIN" 2>/dev/null; then while IFS= read -r line; do
iptables -I FORWARD 1 -i "$WG_IF" -j "$CHAIN" [[ -z "$line" ]] && continue
fi local client_ip target
client_ip=$(echo "$line" | awk '{print $1}')
if ! iptables -C FORWARD -o "$WG_IF" -j "$CHAIN" 2>/dev/null; then target=$(echo "$line" | awk '{print $2}')
iptables -I FORWARD 2 -o "$WG_IF" -j "$CHAIN"
fi
log_info "Chain $CHAIN created and linked to FORWARD (In/Out)"
# === POPULATE IPSET (hash:net,net for source->target mapping) ===
local use_ipset=false
if has_ipset; then
use_ipset=true
log_info "Populating ipsets..."
ensure_ipset "$IPSET_V4" "inet"
flush_ipset "$IPSET_V4"
# Check if we need IPv6 ipset
local use_ipv6=false
if [[ -n "$WG_SUBNET_V6" ]] && command -v ip6tables &>/dev/null; then
use_ipv6=true
ensure_ipset "$IPSET_V6" "inet6"
flush_ipset "$IPSET_V6"
fi
# Read all access entries and populate ipset (client_ip,target)
jq -r '
.clients // {} | to_entries[] |
select(.value.access != null and (.value.access | length > 0)) |
.key as $ip |
.value.access[] |
"\($ip) \(.)"
' "$POLICY_FILE" 2>/dev/null | while read -r client_ip target; do
[[ -z "$client_ip" || -z "$target" ]] && continue [[ -z "$client_ip" || -z "$target" ]] && continue
if [[ "$target" == *":"* ]]; then if [[ "$target" == *":"* ]]; then
if [[ "$use_ipv6" == true ]]; then nft add element "$NFT_TABLE_FULL" "$NFT_SET_V6" { "$client_ip" . "$target" } 2>/dev/null || \
ipset add "$IPSET_V6" "${client_ip},${target}" 2>/dev/null || \ log_warn "Failed to add ${client_ip} . ${target} to set $NFT_SET_V6"
log_warn "Failed to add ${client_ip},${target} to ipset $IPSET_V6" (( v6_count++ )) || true
fi
else else
ipset add "$IPSET_V4" "${client_ip},${target}" 2>/dev/null || \ nft add element "$NFT_TABLE_FULL" "$NFT_SET_V4" { "$client_ip" . "$target" } 2>/dev/null || \
log_warn "Failed to add ${client_ip},${target} to ipset $IPSET_V4" log_warn "Failed to add ${client_ip} . ${target} to set $NFT_SET_V4"
(( v4_count++ )) || true
fi fi
done done < <(jq -r '
local v4_count v6_count
v4_count=$(ipset list "$IPSET_V4" 2>/dev/null | grep -c '^[0-9]' || echo 0)
v6_count=$(ipset list "$IPSET_V6" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0)
log_info "ipset $IPSET_V4: $v4_count entries, $IPSET_V6: $v6_count entries"
else
log_warn "ipset not installed, falling back to per-rule iptables whitelist"
local use_ipv6=false
if [[ -n "$WG_SUBNET_V6" ]] && command -v ip6tables &>/dev/null; then
use_ipv6=true
fi
fi
# === RULE 1: ESTABLISHED,RELATED — allow return traffic ===
iptables -A "$CHAIN" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# === RULE 2: WHITELIST (per-client source) ===
if [[ "$use_ipset" == true ]]; then
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[] | .clients // {} | to_entries[] |
select(.value.access != null and (.value.access | length > 0)) | select(.value.access != null and (.value.access | length > 0)) |
.key as $ip | .key as $ip |
.value.access[] | .value.access[] |
"\($ip) \(.)" "\($ip) \(.)"
' "$POLICY_FILE" 2>/dev/null | while read -r client_ip target; do ' "$POLICY_FILE" 2>/dev/null)
[[ -z "$client_ip" || -z "$target" ]] && continue
if [[ "$client_ip" == *":"* ]]; then log_info "Set $NFT_SET_V4: $v4_count entries, $NFT_SET_V6: $v6_count entries"
if [[ "$use_ipv6" == true ]]; then
ip6tables -A "$CHAIN" -s "$client_ip" -d "$target" -j ACCEPT 2>/dev/null || true
fi
else
iptables -A "$CHAIN" -s "$client_ip" -d "$target" -j ACCEPT
fi
done
fi
# === RULE 3: ISOLATION — drop NEW connections between WG clients === # === CLEANUP BACKUP (success path) ===
if [[ -n "$WG_SUBNET" ]]; then rm -f "$NFT_BACKUP" 2>/dev/null || true
iptables -A "$CHAIN" \
-s "$WG_SUBNET" \
-d "$WG_SUBNET" \
-m conntrack --ctstate NEW \
-j DROP
log_info "Client isolation enabled for $WG_SUBNET"
fi
if [[ "$use_ipv6" == true && -n "$WG_SUBNET_V6" ]]; then
ip6tables -A "$CHAIN" \
-s "$WG_SUBNET_V6" \
-d "$WG_SUBNET_V6" \
-m conntrack --ctstate NEW \
-j DROP 2>/dev/null || true
log_info "Client isolation enabled for IPv6 $WG_SUBNET_V6"
fi
# === RULE 4: BLOCK LAN — drop from WG subnet to private LAN ===
if [[ -n "$WG_SUBNET" && -n "$LAN_SUBNETS" ]]; then
echo "$LAN_SUBNETS" | while read -r subnet; do
[[ -z "$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"
done
fi
# IPv6 LAN block (link-local and ULA)
if [[ "$use_ipv6" == true && -n "$WG_SUBNET_V6" ]]; then
# Block to link-local (fe80::/10)
ip6tables -A "$CHAIN" -s "$WG_SUBNET_V6" -d "fe80::/10" -j DROP 2>/dev/null || true
# Block to ULA (fc00::/7)
ip6tables -A "$CHAIN" -s "$WG_SUBNET_V6" -d "fc00::/7" -j DROP 2>/dev/null || true
log_info "IPv6 LAN block applied (link-local + ULA)"
fi
# === 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
if [[ "$use_ipv6" == true ]]; then
ip6tables -A "$CHAIN" \
-m limit --limit "$LOG_RATE" \
-j LOG --log-prefix "${LOG_PREFIX}: " --log-level 4 2>/dev/null || true
fi
# === RULE 7: DEFAULT DROP (internet block by default) ===
iptables -A "$CHAIN" -j DROP
if [[ "$use_ipv6" == true ]]; then
ip6tables -A "$CHAIN" -j DROP 2>/dev/null || true
fi
# === CLEANUP BACKUP CHAIN (no rollback needed anymore) ===
iptables -F "$CHAIN_BACKUP" 2>/dev/null || true
iptables -X "$CHAIN_BACKUP" 2>/dev/null || true
# Disable ERR trap (success path) # Disable ERR trap (success path)
trap - ERR trap - ERR
# === VERIFY === # === VERIFY ===
local rule_count local rule_count
rule_count=$(iptables -L "$CHAIN" -n 2>/dev/null | tail -n +3 | wc -l) rule_count=$(nft list chain "$NFT_TABLE_FULL" forward 2>/dev/null | grep -c '^\s*' || echo 0)
log_info "Policy applied. Chain: $CHAIN, Rules: $rule_count" log_info "Policy applied. Table: $NFT_TABLE, Rules: $rule_count"
echo "[OK] iptables policy applied. Chain: $CHAIN" echo "[OK] nftables policy applied. Table: $NFT_TABLE"
} }
main "$@" main "$@"
+29 -72
View File
@@ -8,16 +8,16 @@ set -euo pipefail
# CONFIGURATION # CONFIGURATION
# ============================================================ # ============================================================
readonly WG_IF="${WG_IF:-wg0}" readonly WG_IF="${WG_IF:-wg0}"
readonly CHAIN="WG_POLICY" readonly NFT_TABLE="wg_policy"
readonly CHAIN_BACKUP="WG_POLICY_BAK" readonly NFT_TABLE_FULL="inet wg_policy"
readonly NFT_SET_V4="wg_allowed_v4"
readonly NFT_SET_V6="wg_allowed_v6"
readonly POLICY_FILE="/etc/wireguard/policy.json" readonly POLICY_FILE="/etc/wireguard/policy.json"
readonly WG_CONF="/etc/wireguard/wg0.conf" readonly WG_CONF="/etc/wireguard/wg0.conf"
readonly LOCK_FILE="/var/lock/wg-policy.lock" readonly LOCK_FILE="/var/lock/wg-policy.lock"
readonly BACKUP_DIR="/etc/wireguard/backups" readonly BACKUP_DIR="/etc/wireguard/backups"
readonly LOG_PREFIX="WG_DROP" readonly LOG_PREFIX="WG_DROP"
readonly LOG_RATE="10/min" readonly LOG_RATE="10/minute"
readonly IPSET_V4="wg_allowed_v4"
readonly IPSET_V6="wg_allowed_v6"
readonly MAX_RETRY=3 readonly MAX_RETRY=3
readonly RETRY_DELAY=2 readonly RETRY_DELAY=2
readonly DEBOUNCE_SEC=2 readonly DEBOUNCE_SEC=2
@@ -33,17 +33,14 @@ log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2; }
# VALIDATION # VALIDATION
# ============================================================ # ============================================================
# Validate IPv4 address (strict: 0-255 per octet, no leading zeros)
validate_ipv4() { validate_ipv4() {
local ip="$1" local ip="$1"
# Match basic pattern
if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
return 1 return 1
fi fi
local IFS='.' local IFS='.'
read -ra octets <<< "$ip" read -ra octets <<< "$ip"
for octet in "${octets[@]}"; do for octet in "${octets[@]}"; do
# Reject leading zeros (except "0" itself)
if [[ "$octet" =~ ^0[0-9] ]]; then if [[ "$octet" =~ ^0[0-9] ]]; then
return 1 return 1
fi fi
@@ -54,7 +51,6 @@ validate_ipv4() {
return 0 return 0
} }
# Validate IPv4 CIDR (e.g., 192.168.1.0/24)
validate_ipv4_cidr() { validate_ipv4_cidr() {
local cidr="$1" local cidr="$1"
local ip prefix local ip prefix
@@ -63,7 +59,6 @@ validate_ipv4_cidr() {
ip="${cidr%%/*}" ip="${cidr%%/*}"
prefix="${cidr##*/}" prefix="${cidr##*/}"
else else
# Single IP treated as /32
ip="$cidr" ip="$cidr"
prefix="32" prefix="32"
fi fi
@@ -78,10 +73,8 @@ validate_ipv4_cidr() {
return 0 return 0
} }
# Validate IPv6 address (basic check)
validate_ipv6() { validate_ipv6() {
local ip="$1" local ip="$1"
# Basic IPv6 pattern — covers full, compressed, and mixed notation
if [[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$ ]] || \ if [[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$ ]] || \
[[ "$ip" =~ ^::([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$ ]] || \ [[ "$ip" =~ ^::([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$ ]] || \
[[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){1,7}:$ ]] || \ [[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){1,7}:$ ]] || \
@@ -92,7 +85,6 @@ validate_ipv6() {
return 1 return 1
} }
# Validate IPv6 CIDR
validate_ipv6_cidr() { validate_ipv6_cidr() {
local cidr="$1" local cidr="$1"
local ip prefix local ip prefix
@@ -115,7 +107,6 @@ validate_ipv6_cidr() {
return 0 return 0
} }
# Generic CIDR validator — dispatches to v4 or v6
validate_cidr() { validate_cidr() {
local cidr="$1" local cidr="$1"
if [[ "$cidr" == *":"* ]]; then if [[ "$cidr" == *":"* ]]; then
@@ -126,36 +117,11 @@ validate_cidr() {
} }
# ============================================================ # ============================================================
# IPSET MANAGEMENT # NFTABLES HELPERS
# ============================================================ # ============================================================
has_ipset() { has_nft() {
command -v ipset &>/dev/null command -v nft &>/dev/null
}
ensure_ipset() {
local name="$1" family="$2"
has_ipset || return 0
if ! ipset list "$name" &>/dev/null; then
ipset create "$name" hash:net,net family "$family" hashsize 1024 maxelem 65536 timeout 0
log_info "Created ipset: $name (family=$family)"
fi
}
flush_ipset() {
local name="$1"
has_ipset || return 0
if ipset list "$name" &>/dev/null; then
ipset flush "$name"
fi
}
destroy_ipset() {
local name="$1"
has_ipset || return 0
if ipset list "$name" &>/dev/null; then
ipset destroy "$name"
fi
} }
# ============================================================ # ============================================================
@@ -176,7 +142,7 @@ retry() {
log_warn "Attempt $attempt/$max_attempts failed (exit=$exit_code), retrying in ${delay}s..." log_warn "Attempt $attempt/$max_attempts failed (exit=$exit_code), retrying in ${delay}s..."
sleep "$delay" sleep "$delay"
(( attempt++ )) (( attempt++ ))
(( delay *= 2 )) # exponential backoff (( delay *= 2 ))
done done
log_error "All $max_attempts attempts failed for: $*" log_error "All $max_attempts attempts failed for: $*"
@@ -198,7 +164,6 @@ acquire_lock() {
} }
release_lock() { release_lock() {
# Lock released automatically when fd closes, but we clean up file
rm -f "$LOCK_FILE" 2>/dev/null || true rm -f "$LOCK_FILE" 2>/dev/null || true
} }
@@ -216,7 +181,6 @@ backup_policy() {
log_info "Backup created: ${BACKUP_DIR}/policy_${timestamp}.json" log_info "Backup created: ${BACKUP_DIR}/policy_${timestamp}.json"
fi fi
# Keep only last 50 backups
local count local count
count=$(find "$BACKUP_DIR" -name 'policy_*.json' -type f | wc -l) count=$(find "$BACKUP_DIR" -name 'policy_*.json' -type f | wc -l)
if (( count > 50 )); then if (( count > 50 )); then
@@ -229,31 +193,24 @@ backup_policy() {
fi fi
} }
backup_iptables() { backup_nftables() {
mkdir -p "$BACKUP_DIR" mkdir -p "$BACKUP_DIR"
local timestamp local timestamp
timestamp="$(date '+%Y%m%d_%H%M%S')" timestamp="$(date '+%Y%m%d_%H%M%S')"
if iptables-save > "${BACKUP_DIR}/iptables_${timestamp}.rules" 2>/dev/null; then if nft list ruleset > "${BACKUP_DIR}/nftables_${timestamp}.rules" 2>/dev/null; then
log_info "iptables backup: ${BACKUP_DIR}/iptables_${timestamp}.rules" log_info "nftables backup: ${BACKUP_DIR}/nftables_${timestamp}.rules"
fi fi
if command -v ip6tables-save &>/dev/null; then
ip6tables-save > "${BACKUP_DIR}/ip6tables_${timestamp}.rules" 2>/dev/null || true
fi
# Keep only last 20 iptables backups
for prefix in iptables ip6tables; do
local count local count
count=$(find "$BACKUP_DIR" -name "${prefix}_*.rules" -type f | wc -l) count=$(find "$BACKUP_DIR" -name 'nftables_*.rules' -type f | wc -l)
if (( count > 20 )); then if (( count > 20 )); then
find "$BACKUP_DIR" -name "${prefix}_*.rules" -type f -printf '%T@ %p\n' \ find "$BACKUP_DIR" -name 'nftables_*.rules' -type f -printf '%T@ %p\n' \
| sort -n \ | sort -n \
| head -n $(( count - 20 )) \ | head -n $(( count - 20 )) \
| awk '{print $2}' \ | awk '{print $2}' \
| xargs rm -f | xargs rm -f
fi fi
done
} }
# ============================================================ # ============================================================
@@ -314,32 +271,32 @@ health_check() {
status=1 status=1
fi fi
# 3. Check chain exists # 3. Check nftables table exists
if iptables -L "$CHAIN" -n &>/dev/null; then if nft list table "$NFT_TABLE_FULL" &>/dev/null; then
local rule_count local rule_count
rule_count=$(iptables -L "$CHAIN" -n 2>/dev/null | tail -n +3 | wc -l) rule_count=$(nft list chain "$NFT_TABLE_FULL" forward 2>/dev/null | grep -c '^\s*' || echo 0)
report+="[OK] Chain $CHAIN active ($rule_count rules)\n" report+="[OK] Table $NFT_TABLE active ($rule_count rules)\n"
else else
report+="[WARN] Chain $CHAIN not found\n" report+="[WARN] Table $NFT_TABLE not found\n"
status=1 status=1
fi fi
# 4. Check FORWARD reference # 4. Check forward chain exists in table
if iptables -L FORWARD -n 2>/dev/null | grep -q "$CHAIN"; then if nft list chain "$NFT_TABLE_FULL" forward &>/dev/null; then
report+="[OK] FORWARD chain references $CHAIN\n" report+="[OK] Forward chain exists in $NFT_TABLE\n"
else else
report+="[WARN] FORWARD chain has no reference to $CHAIN\n" report+="[WARN] Forward chain not found in $NFT_TABLE\n"
status=1 status=1
fi fi
# 5. Check ipset # 5. Check nftables sets
for set_name in "$IPSET_V4" "$IPSET_V6"; do for set_name in "$NFT_SET_V4" "$NFT_SET_V6"; do
if ipset list "$set_name" &>/dev/null; then if nft list set "$NFT_TABLE_FULL" "$set_name" &>/dev/null; then
local entry_count local entry_count
entry_count=$(ipset list "$set_name" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0) entry_count=$(nft list set "$NFT_TABLE_FULL" "$set_name" 2>/dev/null | grep -c '^\s*' || echo 0)
report+="[OK] ipset $set_name active ($entry_count entries)\n" report+="[OK] Set $set_name active ($entry_count entries)\n"
else else
report+="[INFO] ipset $set_name not created (may not be needed)\n" report+="[INFO] Set $set_name not created (may not be needed)\n"
fi fi
done done