2 Commits

Author SHA1 Message Date
datadunia a799af3d54 debug: dry-run test token and API auth
Debug Release API / debug-api (push) Failing after 0s
2026-05-08 04:49:50 +07:00
datadunia 981506c96c debug: dry-run test token and API auth
Debug Release API / debug-api (push) Failing after 1s
2026-05-08 04:46:13 +07:00
12 changed files with 853 additions and 890 deletions
-15
View File
@@ -1,15 +0,0 @@
name: Beta Release
on:
push:
tags:
- 'v*-beta*'
- 'v*-test*'
jobs:
deploy:
if: "contains(github.ref_name, 'beta') || contains(github.ref_name, 'test')"
uses: ./.gitea/workflows/deploy_call.yaml
with:
prerelease: true
secrets: inherit
-136
View File
@@ -1,136 +0,0 @@
name: Deploy
on:
workflow_call:
inputs:
prerelease:
description: 'Mark as prerelease'
required: false
type: boolean
default: false
permissions:
contents: write
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Clone repository with submodules
run: |
git config --global --remove-section http || true
git config --global --unset-all core.askPass || true
TOKEN="${{ secrets.BUILD_TOKEN }}"
git clone --recurse-submodules \
-c credential.helper="" \
https://token:$TOKEN@git.datadunia.com/devops/wireguard-vpn.git .
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Build Vue frontend
run: |
cd app/frontend
npm install
npm run build
- name: Build Go binaries
run: |
cd app
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../wgrplane-linux-amd64 .
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o ../wgrplane-linux-arm64 .
- name: Generate latest.json
run: |
VERSION="${{ gitea.ref_name }}"
RELEASE_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
REPO="${{ gitea.repository }}"
SERVER="${{ gitea.server_url }}"
cat > latest.json << ENDJSON
{
"version": "$VERSION",
"release_date": "$RELEASE_DATE",
"download_urls": {
"amd64": "${SERVER}/${REPO}/releases/download/${VERSION}/wgrplane-linux-amd64",
"arm64": "${SERVER}/${REPO}/releases/download/${VERSION}/wgrplane-linux-arm64"
}
}
ENDJSON
- name: Create Release and upload assets
env:
TOKEN: ${{ secrets.BUILD_TOKEN }}
run: |
if [ -z "$TOKEN" ]; then
echo "Value: [EMPTY]"
exit 1
else
echo "Length: ${#TOKEN} characters"
fi
REPO="${{ gitea.repository }}"
TAG="${{ gitea.ref_name }}"
API="${{ gitea.server_url }}/api/v1"
# 0. Check & Delete Existing Release
echo "=== 0. Check & Delete Existing Release ==="
EXISTING_RESP=$(curl -s -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG")
EXISTING_ID=$(echo "$EXISTING_RESP" | grep -o '"id":[0-9]*' | head -n 1 | cut -d':' -f2 || true)
if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
echo "⚠️ Found existing release for tag $TAG with ID: $EXISTING_ID. Deleting..."
DELETE_RESP=$(curl -s -w "\n%{http_code}" -X DELETE -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/$EXISTING_ID")
echo "✅ Delete response: $DELETE_RESP"
else
echo "No existing release found for $TAG. Proceeding..."
fi
# 1. Create Release
echo "=== 1. Create New Release ==="
JSON_BODY=$(printf '{"tag_name":"%s","name":"%s","body":"Release %s","draft":false,"prerelease":%s}' "$TAG" "$TAG" "$TAG" "${{ inputs.prerelease }}")
RELEASE_RESP=$(curl -s -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "$JSON_BODY" \
"$API/repos/$REPO/releases")
# Ambil ID dengan lebih teliti
# Tambahkan || true agar grep tidak membuat script crash (karena set -e) jika id tidak ditemukan
RELEASE_ID=$(echo "$RELEASE_RESP" | grep -o '"id":[0-9]*' | head -n 1 | cut -d':' -f2 || true)
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "Gagal membuat release. Response: $RELEASE_RESP"
exit 1
fi
echo "Release ID: $RELEASE_ID"
# 2. Upload Assets
for FILE in wgrplane-linux-amd64 wgrplane-linux-arm64 latest.json; do
if [ -f "$FILE" ]; then
echo "Uploading $FILE..."
curl -s -X POST \
-H "Authorization: token $TOKEN" \
-F "attachment=@$FILE" \
"$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$FILE"
else
echo "Skip $FILE (tidak ditemukan)"
fi
done
- name: Cleanup build artifacts
if: always()
run: |
git config --global --remove-section http || true
git config --global --unset-all core.askPass || true
rm -f wgrplane-linux-amd64 wgrplane-linux-arm64 latest.json
rm -rf app/frontend/node_modules app/frontend/dist
echo "Cleanup done"
+24 -9
View File
@@ -1,14 +1,29 @@
name: Release
name: Debug Release API
on:
push:
tags:
- 'v[0-9]*.[0-9]*.[0-9]'
branches:
- debug-release
jobs:
deploy:
if: "!contains(github.ref_name, 'beta') && !contains(github.ref_name, 'test')"
uses: ./.gitea/workflows/deploy_call.yaml
with:
prerelease: false
secrets: inherit
debug-api:
runs-on: ubuntu-latest
steps:
- name: Debug token and API
run: |
TOKEN="${{ secrets.BUILD_TOKEN }}"
REPO="${{ gitea.repository }}"
API="${{ gitea.server_url }}/api/v1"
echo "=== 1. Token check ==="
echo "Token length: ${#TOKEN}"
echo "Token first 6: ${TOKEN:0:6}"
echo "Token last 4: ${TOKEN: -4}"
echo "=== 2. GET /user (auth test) ==="
curl -sv -H "Authorization: token $TOKEN" "$API/user" 2>&1 | head -30
echo "=== 3. GET releases list ==="
curl -s -w "\nHTTP_CODE:%{http_code}" \
-H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases"
+17 -32
View File
@@ -2,7 +2,7 @@
A complete WireGuard management solution combining two powerful components:
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.
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.
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.
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 nftables directly to the system.
3. **`wg-policy-engine.sh`**: Reads `policy.json` to generate robust rules, applying `iptables` and `ipset` directly to the system.
4. **Watcher Daemon**: Monitors `wg0.conf` for changes via `inotifywait` and triggers the pipeline seamlessly when updates are made.
---
@@ -70,7 +70,9 @@ build.bat
|---------|----------|---------|
| `jq` | **Yes** | `apt install jq` |
| `inotify-tools` | **Yes** (for watcher daemon) | `apt install inotify-tools` |
| `nftables` | **Yes** | `apt install nftables` |
| `ipset` | Optional | `apt install ipset` |
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.
---
@@ -127,7 +129,7 @@ WireGuard uses `AllowedIPs` for Cryptokey Routing (deciding which tunnel interfa
## 🛠 `wg-policy-ctl` CLI Usage
You don't need to manually interact with `nft` or `.json` files. Use the `wg-policy-ctl` wrapper.
You don't need to manually interact with `iptables` or `.json` files. Use the `wg-policy-ctl` wrapper.
```bash
# View the health of the firewall engine and active locks
@@ -136,10 +138,10 @@ wg-policy-ctl status
# View the raw, parsed JSON policy
wg-policy-ctl policy
# Inspect active nftables rules
# Inspect active iptables rules
wg-policy-ctl rules
# Check nftables set contents
# Check memory sets mapping IP targets (ipset)
wg-policy-ctl ipset
# Manually re-sync rules immediately
@@ -276,7 +278,7 @@ Peers without any rules are isolated from other peers and the internet by defaul
| **Scheduling** | robfig/cron v3 |
| **QR Code** | skip2/go-qrcode |
| **Email** | jordan-wright/email (SMTP) |
| **Firewall** | Bash, nftables, inotify-tools, jq |
| **Firewall** | Bash, iptables, ipset, nftables, inotify-tools, jq |
| **Container** | Docker (multi-stage build), docker-compose |
---
@@ -353,36 +355,19 @@ cd ../..
# Server starts on :10087
```
### Option 4: Native CLI / Systemd Service
The WGRplane binary includes a built-in CLI to manage its own systemd service.
### Option 4: Systemd Service
```bash
# Check system dependencies first
./wgrplane doctor
# Install as systemd service (auto-creates unit, enables, and starts)
sudo ./wgrplane install
# Install service file
sudo cp wgrplane.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable wgrplane.service
sudo systemctl start wgrplane.service
# View logs
journalctl -u wgrplane -f
# Other available commands:
sudo ./wgrplane stop
sudo ./wgrplane restart
sudo ./wgrplane uninstall
journalctl -u wgrplane.service -f
```
### Manual Serve & Config
To run the server manually in the foreground with custom ports:
```bash
./wgrplane serve --port 8080 --host 127.0.0.1
```
On first run, it generates a default configuration file at `~/.config/wgrplane/config.json`.
---
## 📁 Project Structure
@@ -401,7 +386,7 @@ On first run, it generates a default configuration file at `~/.config/wgrplane/c
│ ├── frontend/ # Vue 3 SPA (TypeScript, TailwindCSS)
│ └── docs/ # Swagger documentation
├── wg-sync-policy.sh # Parse wg0.conf → policy.json
├── wg-policy-engine.sh # Apply policy.json → nftables
├── wg-policy-engine.sh # Apply policy.json → iptables/ipset
├── wg-sync-watch.sh # inotifywait watcher daemon
├── wg-policy-ctl # CLI wrapper for policy management
├── wg-policy-cleanup.sh # Cleanup script for PostDown
+1 -1
Submodule app updated: b43866d42c...57ffe45ab7
+1 -71
View File
@@ -16,78 +16,12 @@ echo Building %INSTALL_SCRIPT%...
>> "%INSTALL_SCRIPT%" echo exit 1
>> "%INSTALL_SCRIPT%" echo fi
>> "%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 echo "Installing WireGuard Policy Firewall..."
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo echo "Checking dependencies..."
>> "%INSTALL_SCRIPT%" echo apt-get update -y ^|^| true
>> "%INSTALL_SCRIPT%" echo apt-get install -y jq inotify-tools nftables ^|^| true
>> "%INSTALL_SCRIPT%" echo apt-get install -y jq inotify-tools ipset iptables ^|^| true
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo echo "Writing scripts to /usr/local/bin/..."
>> "%INSTALL_SCRIPT%" echo.
@@ -115,8 +49,6 @@ 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-health.timer
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo install_hooks
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo echo "Installation complete!"
>> "%INSTALL_SCRIPT%" echo echo "You can check status with: wg-policy-ctl status"
>> "%INSTALL_SCRIPT%" echo }
@@ -132,8 +64,6 @@ 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 fi
>> "%INSTALL_SCRIPT%" echo.
>> "%INSTALL_SCRIPT%" echo remove_hooks
>> "%INSTALL_SCRIPT%" echo.
>> "%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-health.service
+1 -71
View File
@@ -20,78 +20,12 @@ if [[ $EUID -ne 0 ]]; then
exit 1
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() {
echo "Installing WireGuard Policy Firewall..."
echo "Checking dependencies..."
apt-get update -y || true
apt-get install -y jq inotify-tools nftables || true
apt-get install -y jq inotify-tools ipset iptables || true
echo "Writing scripts to /usr/local/bin/..."
@@ -141,8 +75,6 @@ cat << 'MAIN_EOF_END' >> "$INSTALL_SCRIPT"
systemctl enable --now wg-policy.service
systemctl enable --now wg-policy-health.timer
install_hooks
echo "Installation complete!"
echo "You can check status with: wg-policy-ctl status"
}
@@ -158,8 +90,6 @@ uninstall_policy() {
/usr/local/bin/wg-policy-cleanup.sh || true
fi
remove_hooks
echo "Removing systemd units..."
rm -f /etc/systemd/system/wg-policy.service
rm -f /etc/systemd/system/wg-policy-health.service
+409 -320
View File
File diff suppressed because it is too large Load Diff
+56 -2
View File
@@ -9,9 +9,63 @@ source "${SCRIPT_DIR}/wg-policy-lib.sh"
main() {
log_info "Starting cleanup..."
nft delete table inet wg_policy 2>/dev/null || true
log_info "Removed nftables table inet wg_policy"
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 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
log_info "Cleanup complete"
+28 -15
View File
@@ -13,11 +13,11 @@ Usage: $(basename "$0") <command>
Commands:
status Show full health check report
policy Display current policy.json formatted
rules Show current nftables rules
ipset Show nftables set contents
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 + nftables
backup Manual backup of policy + iptables
stats Show connection and rule statistics
validate Validate policy.json without applying
help Show this help
@@ -43,18 +43,27 @@ cmd_policy() {
}
cmd_rules() {
echo "=== Table: $NFT_TABLE_FULL ==="
if nft list table "$NFT_TABLE_FULL" 2>/dev/null; then
echo "=== IPv4 Chain: $CHAIN ==="
if iptables -L "$CHAIN" -n -v --line-numbers 2>/dev/null; then
echo ""
else
echo "(table not found)"
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 "$NFT_SET_V4" "$NFT_SET_V6"; do
echo "=== nft set: $set_name ==="
if nft list set "$NFT_TABLE_FULL" "$set_name" 2>/dev/null; then
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)"
@@ -85,7 +94,7 @@ cmd_reload() {
cmd_backup() {
backup_policy
backup_nftables
backup_iptables
log_info "Manual backup complete. Files in: $BACKUP_DIR"
}
@@ -102,14 +111,18 @@ cmd_stats() {
jq -r '.clients // {} | to_entries[] | select(.value.access | length == 0) | .key' "$POLICY_FILE" 2>/dev/null || echo "N/A"
echo ""
echo "=== Active nftables rules ==="
nft list chain "$NFT_TABLE_FULL" forward 2>/dev/null | grep -c '#' || echo "N/A"
echo "=== Active iptables rules ==="
iptables -L "$CHAIN" -n 2>/dev/null | tail -n +3 | wc -l || echo "N/A"
echo ""
echo "=== nft set entries ==="
for set_name in "$NFT_SET_V4" "$NFT_SET_V6"; do
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=$(nft list set "$NFT_TABLE_FULL" "$set_name" 2>/dev/null | grep -c -E '^\s+[0-9a-f]' || echo 0)
count=$(ipset list "$set_name" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0)
echo " $set_name: $count entries"
done
}
+232 -177
View File
@@ -1,152 +1,55 @@
#!/bin/bash
# wg-policy-engine.sh — Applies nftables rules from policy.json
# wg-policy-engine.sh — Applies iptables/ipset rules from policy.json
# Fixed: unquoted variables, LOG placement, LAN block targeting,
# atomic chain swap, ipset, IPv6 optional, rollback on failure
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/wg-policy-lib.sh"
NFT_FILE="/tmp/wg-policy.nft"
NFT_BACKUP="/tmp/wg-policy-backup.nft"
# ============================================================
# ROLLBACK
# ============================================================
rollback() {
log_error "ROLLBACK triggered! Restoring previous ruleset..."
trap - ERR
log_error "ROLLBACK triggered! Restoring previous rules..."
nft delete table "$NFT_TABLE_FULL" 2>/dev/null || true
if [[ -f "$NFT_BACKUP" ]]; then
if nft -f "$NFT_BACKUP" 2>/dev/null; then
log_info "Rollback: restored from backup"
# Remove new chain references
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
log_error "Rollback: failed to restore from backup"
break
fi
fi
}
done
# ============================================================
# RULESET GENERATION
# ============================================================
# Flush and remove new chain
iptables -F "$CHAIN" 2>/dev/null || true
iptables -X "$CHAIN" 2>/dev/null || true
generate_ruleset() {
local WG_SUBNET="$1"
local WG_SUBNET_V6="$2"
local LAN_SUBNETS="$3"
local DEF_IF="$4"
# Restore backup chain if it exists
if iptables -L "$CHAIN_BACKUP" -n &>/dev/null; then
# Rename backup chain to active
iptables -N "$CHAIN" 2>/dev/null || iptables -F "$CHAIN"
# Copy rules from backup
iptables-save -c | grep "^-A $CHAIN_BACKUP" | \
sed "s/-A $CHAIN_BACKUP/-A $CHAIN/" | \
iptables-restore -c 2>/dev/null || true
cat > "$NFT_FILE" << 'HEADER'
#!/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
iptables -A FORWARD -i "$WG_IF" -j "$CHAIN"
log_info "Rollback: restored from backup chain"
fi
# Client isolation (IPv6)
if [[ -n "$WG_SUBNET_V6" ]]; then
cat >> "$NFT_FILE" << EOF
# Cleanup backup chain
iptables -F "$CHAIN_BACKUP" 2>/dev/null || true
iptables -X "$CHAIN_BACKUP" 2>/dev/null || true
/* IPv6 isolation */
iifname "wg0" ip6 saddr $WG_SUBNET_V6 ip6 daddr $WG_SUBNET_V6 ct state new drop
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
# Cleanup backup ipsets
destroy_ipset "${IPSET_V4}_bak" 2>/dev/null || true
destroy_ipset "${IPSET_V6}_bak" 2>/dev/null || true
}
# ============================================================
@@ -172,8 +75,8 @@ main() {
exit 1
fi
# Backup current nftables state
backup_nftables
# Backup iptables state
backup_iptables
# Set trap for rollback on failure
trap 'rollback' ERR
@@ -202,69 +105,221 @@ main() {
fi
# === BASE ROUTING & NAT ===
# Enable IP Forwarding
sysctl -w net.ipv4.ip_forward=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"
if command -v ip6tables &>/dev/null; then
sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true
fi
# === DELETE OLD TABLE ===
nft delete table "$NFT_TABLE_FULL" 2>/dev/null || true
# 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
# === GENERATE AND LOAD NEW RULESET ===
generate_ruleset "$WG_SUBNET" "$WG_SUBNET_V6" "$LAN_SUBNETS" "$DEF_IF"
log_info "Generated ruleset: $NFT_FILE"
nft -f "$NFT_FILE"
log_info "Loaded nftables ruleset from $NFT_FILE"
# === POPULATE SETS ===
log_info "Populating whitelist sets..."
local v4_count=0
local v6_count=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
local client_ip target
client_ip=$(echo "$line" | awk '{print $1}')
target=$(echo "$line" | awk '{print $2}')
[[ -z "$client_ip" || -z "$target" ]] && continue
if [[ "$target" == *":"* ]]; then
nft add element "$NFT_TABLE_FULL" "$NFT_SET_V6" { "$client_ip" . "$target" } 2>/dev/null || \
log_warn "Failed to add ${client_ip} . ${target} to set $NFT_SET_V6"
(( v6_count++ )) || true
# === CLEANUP OLD CHAIN (loop until all references removed) ===
log_info "Cleaning up old chain references..."
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
nft add element "$NFT_TABLE_FULL" "$NFT_SET_V4" { "$client_ip" . "$target" } 2>/dev/null || \
log_warn "Failed to add ${client_ip} . ${target} to set $NFT_SET_V4"
(( v4_count++ )) || true
break
fi
done < <(jq -r '
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 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
iptables -X "$CHAIN" 2>/dev/null || true
# === CREATE FRESH CHAIN ===
iptables -N "$CHAIN"
if ! iptables -C FORWARD -i "$WG_IF" -j "$CHAIN" 2>/dev/null; then
iptables -I FORWARD 1 -i "$WG_IF" -j "$CHAIN"
fi
if ! iptables -C FORWARD -o "$WG_IF" -j "$CHAIN" 2>/dev/null; then
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
if [[ "$target" == *":"* ]]; then
if [[ "$use_ipv6" == true ]]; then
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" "${client_ip},${target}" 2>/dev/null || \
log_warn "Failed to add ${client_ip},${target} to ipset $IPSET_V4"
fi
done
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[] |
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
if [[ "$client_ip" == *":"* ]]; then
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 ===
if [[ -n "$WG_SUBNET" ]]; then
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.access != null and (.value.access | length > 0)) |
.key as $ip |
.value.access[] |
"\($ip) \(.)"
' "$POLICY_FILE" 2>/dev/null)
select(.value.internet == "true") |
"\(.key)"
' "$POLICY_FILE" 2>/dev/null | while read -r client_ip; do
[[ -z "$client_ip" ]] && continue
log_info "Set $NFT_SET_V4: $v4_count entries, $NFT_SET_V6: $v6_count entries"
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
# === CLEANUP BACKUP (success path) ===
rm -f "$NFT_BACKUP" 2>/dev/null || true
# === 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)
trap - ERR
# === VERIFY ===
local rule_count
rule_count=$(nft list chain "$NFT_TABLE_FULL" forward 2>/dev/null | grep -c '^\s*' || echo 0)
log_info "Policy applied. Table: $NFT_TABLE, Rules: $rule_count"
rule_count=$(iptables -L "$CHAIN" -n 2>/dev/null | tail -n +3 | wc -l)
log_info "Policy applied. Chain: $CHAIN, Rules: $rule_count"
echo "[OK] nftables policy applied. Table: $NFT_TABLE"
echo "[OK] iptables policy applied. Chain: $CHAIN"
}
main "$@"
+78 -35
View File
@@ -8,16 +8,16 @@ set -euo pipefail
# CONFIGURATION
# ============================================================
readonly WG_IF="${WG_IF:-wg0}"
readonly NFT_TABLE="wg_policy"
readonly NFT_TABLE_FULL="inet wg_policy"
readonly NFT_SET_V4="wg_allowed_v4"
readonly NFT_SET_V6="wg_allowed_v6"
readonly CHAIN="WG_POLICY"
readonly CHAIN_BACKUP="WG_POLICY_BAK"
readonly POLICY_FILE="/etc/wireguard/policy.json"
readonly WG_CONF="/etc/wireguard/wg0.conf"
readonly LOCK_FILE="/var/lock/wg-policy.lock"
readonly BACKUP_DIR="/etc/wireguard/backups"
readonly LOG_PREFIX="WG_DROP"
readonly LOG_RATE="10/minute"
readonly LOG_RATE="10/min"
readonly IPSET_V4="wg_allowed_v4"
readonly IPSET_V6="wg_allowed_v6"
readonly MAX_RETRY=3
readonly RETRY_DELAY=2
readonly DEBOUNCE_SEC=2
@@ -33,14 +33,17 @@ log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2; }
# VALIDATION
# ============================================================
# Validate IPv4 address (strict: 0-255 per octet, no leading zeros)
validate_ipv4() {
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
return 1
fi
local IFS='.'
read -ra octets <<< "$ip"
for octet in "${octets[@]}"; do
# Reject leading zeros (except "0" itself)
if [[ "$octet" =~ ^0[0-9] ]]; then
return 1
fi
@@ -51,6 +54,7 @@ validate_ipv4() {
return 0
}
# Validate IPv4 CIDR (e.g., 192.168.1.0/24)
validate_ipv4_cidr() {
local cidr="$1"
local ip prefix
@@ -59,6 +63,7 @@ validate_ipv4_cidr() {
ip="${cidr%%/*}"
prefix="${cidr##*/}"
else
# Single IP treated as /32
ip="$cidr"
prefix="32"
fi
@@ -73,8 +78,10 @@ validate_ipv4_cidr() {
return 0
}
# Validate IPv6 address (basic check)
validate_ipv6() {
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}$ ]] || \
[[ "$ip" =~ ^::([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$ ]] || \
[[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){1,7}:$ ]] || \
@@ -85,6 +92,7 @@ validate_ipv6() {
return 1
}
# Validate IPv6 CIDR
validate_ipv6_cidr() {
local cidr="$1"
local ip prefix
@@ -107,6 +115,7 @@ validate_ipv6_cidr() {
return 0
}
# Generic CIDR validator — dispatches to v4 or v6
validate_cidr() {
local cidr="$1"
if [[ "$cidr" == *":"* ]]; then
@@ -117,11 +126,36 @@ validate_cidr() {
}
# ============================================================
# NFTABLES HELPERS
# IPSET MANAGEMENT
# ============================================================
has_nft() {
command -v nft &>/dev/null
has_ipset() {
command -v ipset &>/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
}
# ============================================================
@@ -142,7 +176,7 @@ retry() {
log_warn "Attempt $attempt/$max_attempts failed (exit=$exit_code), retrying in ${delay}s..."
sleep "$delay"
(( attempt++ ))
(( delay *= 2 ))
(( delay *= 2 )) # exponential backoff
done
log_error "All $max_attempts attempts failed for: $*"
@@ -164,6 +198,7 @@ acquire_lock() {
}
release_lock() {
# Lock released automatically when fd closes, but we clean up file
rm -f "$LOCK_FILE" 2>/dev/null || true
}
@@ -181,6 +216,7 @@ backup_policy() {
log_info "Backup created: ${BACKUP_DIR}/policy_${timestamp}.json"
fi
# Keep only last 50 backups
local count
count=$(find "$BACKUP_DIR" -name 'policy_*.json' -type f | wc -l)
if (( count > 50 )); then
@@ -193,24 +229,31 @@ backup_policy() {
fi
}
backup_nftables() {
backup_iptables() {
mkdir -p "$BACKUP_DIR"
local timestamp
timestamp="$(date '+%Y%m%d_%H%M%S')"
if nft list ruleset > "${BACKUP_DIR}/nftables_${timestamp}.rules" 2>/dev/null; then
log_info "nftables backup: ${BACKUP_DIR}/nftables_${timestamp}.rules"
if iptables-save > "${BACKUP_DIR}/iptables_${timestamp}.rules" 2>/dev/null; then
log_info "iptables backup: ${BACKUP_DIR}/iptables_${timestamp}.rules"
fi
local count
count=$(find "$BACKUP_DIR" -name 'nftables_*.rules' -type f | wc -l)
if (( count > 20 )); then
find "$BACKUP_DIR" -name 'nftables_*.rules' -type f -printf '%T@ %p\n' \
| sort -n \
| head -n $(( count - 20 )) \
| awk '{print $2}' \
| xargs rm -f
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
count=$(find "$BACKUP_DIR" -name "${prefix}_*.rules" -type f | wc -l)
if (( count > 20 )); then
find "$BACKUP_DIR" -name "${prefix}_*.rules" -type f -printf '%T@ %p\n' \
| sort -n \
| head -n $(( count - 20 )) \
| awk '{print $2}' \
| xargs rm -f
fi
done
}
# ============================================================
@@ -271,32 +314,32 @@ health_check() {
status=1
fi
# 3. Check nftables table exists
if nft list table "$NFT_TABLE_FULL" &>/dev/null; then
# 3. Check chain exists
if iptables -L "$CHAIN" -n &>/dev/null; then
local rule_count
rule_count=$(nft list chain "$NFT_TABLE_FULL" forward 2>/dev/null | grep -c '^\s*' || echo 0)
report+="[OK] Table $NFT_TABLE active ($rule_count rules)\n"
rule_count=$(iptables -L "$CHAIN" -n 2>/dev/null | tail -n +3 | wc -l)
report+="[OK] Chain $CHAIN active ($rule_count rules)\n"
else
report+="[WARN] Table $NFT_TABLE not found\n"
report+="[WARN] Chain $CHAIN not found\n"
status=1
fi
# 4. Check forward chain exists in table
if nft list chain "$NFT_TABLE_FULL" forward &>/dev/null; then
report+="[OK] Forward chain exists in $NFT_TABLE\n"
# 4. Check FORWARD reference
if iptables -L FORWARD -n 2>/dev/null | grep -q "$CHAIN"; then
report+="[OK] FORWARD chain references $CHAIN\n"
else
report+="[WARN] Forward chain not found in $NFT_TABLE\n"
report+="[WARN] FORWARD chain has no reference to $CHAIN\n"
status=1
fi
# 5. Check nftables sets
for set_name in "$NFT_SET_V4" "$NFT_SET_V6"; do
if nft list set "$NFT_TABLE_FULL" "$set_name" &>/dev/null; then
# 5. Check ipset
for set_name in "$IPSET_V4" "$IPSET_V6"; do
if ipset list "$set_name" &>/dev/null; then
local entry_count
entry_count=$(nft list set "$NFT_TABLE_FULL" "$set_name" 2>/dev/null | grep -c '^\s*' || echo 0)
report+="[OK] Set $set_name active ($entry_count entries)\n"
entry_count=$(ipset list "$set_name" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0)
report+="[OK] ipset $set_name active ($entry_count entries)\n"
else
report+="[INFO] Set $set_name not created (may not be needed)\n"
report+="[INFO] ipset $set_name not created (may not be needed)\n"
fi
done