Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5c9b180dc | |||
| eb525879f1 | |||
| b1c7f10fec | |||
| da09fb2e65 | |||
| fefaf49298 | |||
| fc05ac0725 | |||
| 5bcac141d1 | |||
| c7ad30d2df | |||
| 93f6aaea76 | |||
| 7a448ffa3a | |||
| 89443f9481 | |||
| c5d3651a96 | |||
| 548e3949ad | |||
| 4b8d5008d6 | |||
| 25e0cfd15e | |||
| 9c771f098c | |||
| dfc8d9ed57 | |||
| 565d7f1a35 | |||
| 45c7788d1d | |||
| b9fbe1430f | |||
| 5eec7651da | |||
| c8cf771bfa | |||
| ba7b9ac64c | |||
| c42683e968 | |||
| 4a3d099ff9 | |||
| 64e8372fc1 | |||
| d7b1de1885 | |||
| 7d0c0c18d0 | |||
| 8d237b293f | |||
| 43477a3333 | |||
| 42a35302b3 | |||
| 6d9fa80b54 | |||
| a59ebd4813 | |||
| 0a7bf72d6c | |||
| 861d910363 | |||
| d0d529450c | |||
| 0c6bac4c22 | |||
| 7b02527200 | |||
| fa4bf318b6 | |||
| cdd22860b3 | |||
| bbae401002 | |||
| fb198f64bb | |||
| d62936d607 | |||
| 3b0f478d3a | |||
| b39d7a4bd2 | |||
| 81af99dfc4 | |||
| 139463855d | |||
| 309a3fea8f | |||
| 1fdb8135db | |||
| 22b9133431 | |||
| 6e9d389cbf | |||
| 644c6e2dde | |||
| 1716747c09 | |||
| 47abc63658 | |||
| 44c480a89a | |||
| 35dcc2f15f | |||
| f71ef8c3e4 | |||
| 6308017b24 | |||
| 5b4f91e6e0 | |||
| f6af1fb6d8 | |||
| f47b50b857 | |||
| e2c807dc12 | |||
| 144be82e9b | |||
| 0feed113ac | |||
| 20478191a3 | |||
| 32f1e78ee1 | |||
| ac32038ebb | |||
| 417fd99dbf | |||
| 1633fede7a | |||
| 14742b4c27 | |||
| c49964d4d4 | |||
| 4c00af7c44 | |||
| de21d422b6 | |||
| 581350971a | |||
| 8a8927c8e8 | |||
| 282b2a3b69 | |||
| fc1e5513aa | |||
| 481807769e | |||
| 52e629ac02 | |||
| b3676b87cb |
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
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"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v[0-9]*.[0-9]*.[0-9]'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
if: "!contains(github.ref_name, 'beta') && !contains(github.ref_name, 'test')"
|
||||||
|
uses: ./.gitea/workflows/deploy_call.yaml
|
||||||
|
with:
|
||||||
|
prerelease: false
|
||||||
|
secrets: inherit
|
||||||
@@ -1 +1,2 @@
|
|||||||
.test/
|
.test/
|
||||||
|
.sisyphus/
|
||||||
|
|||||||
@@ -1,27 +1,116 @@
|
|||||||
# WireGuard Policy Firewall (`03.wireguard-policy`)
|
# AGENTS.md -- WireGuard Policy Firewall + WGRplane
|
||||||
|
|
||||||
## Architecture & Configuration Flow
|
**Generated:** 2026-05-08
|
||||||
- **Goal:** Dynamic iptables/ipset rules based on WireGuard configuration (`wg0.conf`).
|
**Project:** 03.wireguard-policy
|
||||||
- **Data Flow:** `wg0.conf` -> `wg-sync-policy.sh` -> `policy.json` -> `wg-policy-engine.sh` -> `iptables`/`ipset`
|
|
||||||
- **File Watcher:** `wg-sync-watch.sh` monitors `wg0.conf` via `inotifywait` and debounces changes to re-run the sync and engine.
|
|
||||||
|
|
||||||
## Critical Parsing Rules & Design Constraints
|
## OVERVIEW
|
||||||
- **Target IPs Parsing (`#Access`):** The firewall script uses the custom `#Access` comment in `wg0.conf` to define egress/firewall whitelists for clients.
|
Hybrid project: (1) WireGuard Policy Firewall -- shell script iptables/ipset engine driven by `wg0.conf` `#Access` comments. (2) WGRplane -- Go binary (port 10087) serving REST API + Vue 3 SPA for WireGuard control plane management.
|
||||||
- **Why `#Access` is mandatory:** WireGuard's native `AllowedIPs` on a Server dictates *routing* towards the client. If we put target destinations in the Server's `AllowedIPs`, the Server would wrongly route traffic destined for those IPs *into* the client tunnel. Therefore, a custom `#Access` comment is the only correct way to define firewall whitelist destinations without breaking WireGuard's Cryptokey Routing.
|
|
||||||
- **Do not remove `#Access`:** Future agents MUST NOT attempt to refactor the script to parse targets from `AllowedIPs`. It is architecturally incorrect for this use case.
|
|
||||||
|
|
||||||
## Testing & Verifying
|
## STRUCTURE
|
||||||
- `wg-policy-ctl status`: Check the overall health, including interface status, JSON validity, lock files, and iptables rules counts.
|
```
|
||||||
- `wg-policy-ctl validate`: Validates `policy.json` without applying.
|
03.wireguard-policy/
|
||||||
- `wg-policy-ctl rules`: View the applied iptables rules in the active chain (`WG_POLICY`).
|
├── app/ # WGRplane: Go backend + Vue 3 frontend (see app/AGENTS.md)
|
||||||
- `wg-policy-ctl reload`: Forces a re-sync from `wg0.conf` and re-applies iptables.
|
│ ├── *.go # 13 Go files, flat package (no subdirs)
|
||||||
|
│ ├── frontend/ # Vue 3 SPA (see app/frontend/src/AGENTS.md)
|
||||||
|
│ └── docs/ # Swagger auto-generated (DO NOT EDIT)
|
||||||
|
├── wg-sync-policy.sh # Parses wg0.conf → policy.json (atomic write + flock)
|
||||||
|
├── wg-policy-engine.sh # Reads policy.json → iptables/ipset WG_POLICY chain
|
||||||
|
├── wg-sync-watch.sh # inotifywait daemon, debounces wg0.conf changes
|
||||||
|
├── wg-policy-lib.sh # Shared shell library (source only, never execute directly)
|
||||||
|
├── wg-policy-ctl # CLI wrapper for operator use
|
||||||
|
├── wg-policy-cleanup.sh # PostDown cleanup (run by WireGuard)
|
||||||
|
├── *.service / *.timer # systemd units for daemon + health check
|
||||||
|
├── install.sh # Unified installer (embeds all scripts, built by build.sh)
|
||||||
|
├── build.sh / build.bat # Rebuilds install.sh from source scripts
|
||||||
|
├── Dockerfile # Multi-stage: Go build + frontend build
|
||||||
|
└── README.md # Full user documentation
|
||||||
|
```
|
||||||
|
|
||||||
## Script Constraints & Gotchas
|
## WHERE TO LOOK
|
||||||
- **Atomic Operations:** Always use atomic writes (`mv -f tmp target`) for `policy.json` to prevent the policy engine from reading partial files.
|
| Task | Location |
|
||||||
- **Locking:** `wg-sync-policy.sh` uses file-based locking (`flock`) to prevent race conditions during updates.
|
|------|----------|
|
||||||
- **Rollback:** `wg-policy-engine.sh` creates a backup chain (`WG_POLICY_BAK`) and uses a trap on `ERR` to rollback if applying rules fails halfway.
|
| Policy firewall logic | `wg-sync-policy.sh`, `wg-policy-engine.sh`, `wg-policy-lib.sh` |
|
||||||
- **Dependencies:** Requires `jq` and `inotify-tools`.
|
| Firewall rule chain | `wg-policy-engine.sh` -- WG_POLICY iptables chain |
|
||||||
|
| Policy JSON schema | `wg-sync-policy.sh` output / `wg-policy-engine.sh` input |
|
||||||
|
| WGRplane API handlers | `app/handlers.go` |
|
||||||
|
| WGRplane DB models | `app/models.go` |
|
||||||
|
| Auth middleware | `app/auth.go` (CAUTION: see known issues) |
|
||||||
|
| nftables rules (forward mode) | `app/nftables.go` |
|
||||||
|
| Webhook delivery | `app/webhook.go` |
|
||||||
|
| Vue frontend | `app/frontend/src/` |
|
||||||
|
| i18n translations | `app/frontend/src/i18n/locales/` (en, id, zh) |
|
||||||
|
| Backend i18n | `app/active.{en,id,zh}.json` |
|
||||||
|
| Systemd service config | `wg-policy.service`, `wgrplane.service` |
|
||||||
|
|
||||||
## Development Commands
|
## CRITICAL DESIGN RULES
|
||||||
- Restart the watcher service: `systemctl restart wg-policy.service`
|
|
||||||
- Check service logs: `journalctl -u wg-policy.service -f`
|
### Policy Firewall -- `#Access` MUST NOT be changed
|
||||||
|
- WireGuard `AllowedIPs` on the server side = Cryptokey Routing, not firewall whitelist
|
||||||
|
- Putting destination IPs in server's `AllowedIPs` breaks routing (WG tunnels those packets INTO client)
|
||||||
|
- `#Access` comment is the ONLY correct way to declare firewall destinations per-peer
|
||||||
|
- **NEVER refactor to parse targets from `AllowedIPs`** -- architecturally incorrect
|
||||||
|
|
||||||
|
### Atomic Writes
|
||||||
|
- Always `mv -f tmp target` for `policy.json` -- never write directly
|
||||||
|
- `wg-sync-policy.sh` uses `flock` -- never bypass locking
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
- `wg-policy-engine.sh` creates `WG_POLICY_BAK` chain; traps `ERR` for rollback
|
||||||
|
|
||||||
|
### No SaveConfig
|
||||||
|
- WireGuard `SaveConfig = true` strips ALL comments including `#Access` -- NEVER enable
|
||||||
|
|
||||||
|
## KNOWN ISSUES / GOTCHAS
|
||||||
|
- **AuthMiddleware NOT applied**: `auth.go` defines `AuthMiddleware` but it is NOT wired to any routes in `main.go`. All API endpoints currently unprotected (auth header still checked inside handlers via manual if-check, but middleware chain is absent).
|
||||||
|
- **TOTP secrets in-memory**: `totpSecrets` map in `auth.go` is not persisted; lost on restart.
|
||||||
|
- **WebSocket stats are MOCK**: `stats.go` broadcasts randomly generated numbers, not real WireGuard traffic.
|
||||||
|
- **Plugin system is stub**: `plugins.go` TelegramNotifier/SlackNotifier just print to stdout.
|
||||||
|
- **Binary + DB in app/**: `wgrplane` binary and `wgrplane.db` live in `app/` (non-standard, intentional).
|
||||||
|
- **Duplicate i18n**: `app/active.*.json` (backend i18n) and `app/frontend/src/i18n/locales/` (frontend i18n) are separate systems.
|
||||||
|
|
||||||
|
## ANTI-PATTERNS
|
||||||
|
- Never parse firewall targets from `AllowedIPs` -- use `#Access` only
|
||||||
|
- Never write `policy.json` without atomic mv + flock
|
||||||
|
- Never run `wg-policy-lib.sh` directly (source-only library)
|
||||||
|
- Never enable `SaveConfig = true` in wg0.conf
|
||||||
|
- Do NOT edit `app/docs/docs.go` -- auto-generated by swaggo
|
||||||
|
- Do NOT put business logic in `app/main.go` -- it's bootstrap only
|
||||||
|
|
||||||
|
## COMMANDS
|
||||||
|
|
||||||
|
### Policy Firewall
|
||||||
|
```bash
|
||||||
|
wg-policy-ctl status # Health: interface, JSON validity, rule counts
|
||||||
|
wg-policy-ctl validate # Validate policy.json without applying
|
||||||
|
wg-policy-ctl rules # Show active WG_POLICY iptables rules
|
||||||
|
wg-policy-ctl reload # Force re-sync from wg0.conf + re-apply
|
||||||
|
wg-policy-ctl policy # Show raw policy.json
|
||||||
|
wg-policy-ctl log # Show dropped packet logs (rate-limited)
|
||||||
|
systemctl restart wg-policy.service
|
||||||
|
journalctl -u wg-policy.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### WGRplane (Go App)
|
||||||
|
```bash
|
||||||
|
cd app && go build -o ../wgrplane . # Build Go binary
|
||||||
|
cd app/frontend && npm install && npm run build # Build Vue frontend
|
||||||
|
./wgrplane # Run (serves :10087)
|
||||||
|
systemctl restart wgrplane.service
|
||||||
|
journalctl -u wgrplane.service -f
|
||||||
|
curl -H "wg-rplane-datadunia: test-api-key" http://localhost:10087/api/servers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installer
|
||||||
|
```bash
|
||||||
|
./build.sh # Rebuild install.sh from source scripts (Linux)
|
||||||
|
./build.bat # Rebuild install.sh from source scripts (Windows)
|
||||||
|
sudo ./install.sh install # Full install (deps + scripts + systemd + service)
|
||||||
|
sudo ./install.sh uninstall # Remove all
|
||||||
|
```
|
||||||
|
|
||||||
|
## DEPENDENCIES
|
||||||
|
- Shell: `jq`, `inotify-tools` (required); `ipset` (optional, O(1) lookup)
|
||||||
|
- Go: 1.25.1, SQLite (glebarez/sqlite), Gorilla Mux, GORM, JWT, TOTP, WebSocket
|
||||||
|
- Frontend: Vue 3, TypeScript, Vite, TailwindCSS 4, vue-i18n 9
|
||||||
|
- Auth headers: `wg-rplane-datadunia: <KEY>` (API key) | `Authorization: Bearer <JWT>` | `X-TOTP: <CODE>`
|
||||||
|
- Env vars: `WG_API_KEY` (default: test-api-key), `JWT_SECRET`, `WG_RPLANE_MODE` (forward|standalone), `APP_FRONTEND_DIR`
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
# WireGuard Dynamic Policy Firewall
|
# WireGuard Policy Firewall + WGRplane Control Plane
|
||||||
|
|
||||||
A lightweight, robust, and highly dynamic iptables/ipset policy firewall engine designed to restrict and control WireGuard peer traffic (egress traffic mapping) straight from `wg0.conf`.
|
A complete WireGuard management solution combining two powerful components:
|
||||||
|
|
||||||
Rather than allowing all VPN clients to reach any part of your internal network, this tool isolates clients from each other by default and reads a custom `#Access` comment inside `wg0.conf` to automatically generate strict `iptables` rules and `ipset` whitelists per-client on the fly.
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📑 Table of Contents
|
||||||
|
|
||||||
|
### Policy Firewall (Shell Scripts)
|
||||||
|
- [Architecture & Data Flow](#-architecture--data-flow)
|
||||||
|
- [Installation](#-installation)
|
||||||
|
- [Prerequisites](#-prerequisites)
|
||||||
|
- [wg0.conf Integration](#-integrasi-ke-wg0conf)
|
||||||
|
- [CLI Usage](#-wg-policy-ctl-cli-usage)
|
||||||
|
- [Systemd Integration](#-systemd-integration-watcher-daemon)
|
||||||
|
|
||||||
|
### WGRplane (Go App)
|
||||||
|
- [WGRplane Overview](#-wgrplane-overview)
|
||||||
|
- [WGRplane Architecture](#-wgrplane-architecture)
|
||||||
|
- [WGRplane Features](#-wgrplane-features)
|
||||||
|
- [WGRplane Tech Stack](#-wgrplane-tech-stack)
|
||||||
|
- [WGRplane API Endpoints](#-wgrplane-api-endpoints)
|
||||||
|
- [WGRplane Installation](#-wgrplane-installation)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -10,7 +31,7 @@ Rather than allowing all VPN clients to reach any part of your internal network,
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -49,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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -108,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
|
||||||
@@ -117,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
|
||||||
@@ -171,3 +190,227 @@ Check the watcher logs:
|
|||||||
```bash
|
```bash
|
||||||
journalctl -u wg-policy.service -f
|
journalctl -u wg-policy.service -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 WGRplane Overview
|
||||||
|
|
||||||
|
**WGRplane** is a Go-native WireGuard control plane application with a Vue 3 frontend, providing a modern web dashboard for WireGuard management. It features a single Go binary backend, SPA frontend, dynamic policy firewall integration, and glassmorphism UI design.
|
||||||
|
|
||||||
|
The application lives in the `/app` directory. For full documentation, see [`app/README.md`](app/README.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗 WGRplane Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
wg0.conf (with/without #Access)
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ WGRplane (Go Binary :10087) │
|
||||||
|
│ ┌───────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ Gorilla │ │ GORM │ │ nftables │ │
|
||||||
|
│ │ Mux Router│ │ SQLite │ │ Engine │ │
|
||||||
|
│ └───────────┘ └──────────┘ └──────────┘ │
|
||||||
|
│ ┌───────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ Webhook │ │ Scheduler│ │ WebSocket│ │
|
||||||
|
│ │ Engine │ │ Cron │ │ Hub │ │
|
||||||
|
│ └───────────┘ └──────────┘ └──────────┘ │
|
||||||
|
│ ┌───────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │ Auth │ │ SMTP │ │ Plugins │ │
|
||||||
|
│ │ JWT/TOTP │ │ Email │ │ TG/Slack │ │
|
||||||
|
│ └───────────┘ └──────────┘ └──────────┘ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓ HTTP/WebSocket
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Frontend (Vue 3 + TypeScript + Tailwind) │
|
||||||
|
│ Glassmorphism UI, i18n, Dark/Light mode │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hybrid Mode
|
||||||
|
|
||||||
|
WGRplane supports two server modes:
|
||||||
|
|
||||||
|
- **`forward`** - Directly applies nftables rules on the local machine. Peer policies are enforced via `nft` commands.
|
||||||
|
- **`standalone`** - Acts as a control plane that triggers webhooks to remote WireGuard servers. Policy enforcement happens on the remote side.
|
||||||
|
|
||||||
|
### 2-Column Policy
|
||||||
|
|
||||||
|
Each peer has two independent policy columns:
|
||||||
|
|
||||||
|
| Column | Function |
|
||||||
|
|--------|----------|
|
||||||
|
| **AllowAccess** | List of CIDRs the peer can access (internal targets) |
|
||||||
|
| **AllowInternet** | Boolean flag. If `true`, peer gets unlimited internet access (MASQUERADE) |
|
||||||
|
|
||||||
|
Peers without any rules are isolated from other peers and the internet by default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ WGRplane Features
|
||||||
|
|
||||||
|
- **Go-Native Architecture**: Single Go binary handles all API, database, webhooks, scheduler, and nftables. No Python/Flask needed.
|
||||||
|
- **Complete Peer CRUD**: Add, edit, delete peers. Generate QR codes for mobile client import. Export `.conf` configuration files.
|
||||||
|
- **Hybrid Mode**: `forward` mode (local nftables) or `standalone` mode (webhook to remote servers).
|
||||||
|
- **2-Column Policy UI**: "Allow Access" column (firewall whitelist CIDR) and "Allow Internet" toggle per peer.
|
||||||
|
- **Real-time Monitoring**: WebSocket broadcasts peer statistics and traffic every 5 seconds.
|
||||||
|
- **Automated Scheduling**: Daily cron jobs to delete expired peers, restrict over-limit peers, and reset monthly data usage.
|
||||||
|
- **Security**: API Key authentication (`wg-rplane-datadunia`), JWT Bearer tokens, and TOTP (2FA).
|
||||||
|
- **Webhook Engine**: Integration with remote servers (Mikrotik, etc.). Retry with exponential backoff, custom headers, Go templates.
|
||||||
|
- **Plugin System**: Telegram, Slack, and Traffic Logger notifications.
|
||||||
|
- **i18n & Themes**: Multi-language (English, Indonesian, Chinese). Dark/Light/Auto mode.
|
||||||
|
- **Glassmorphism UI**: Futuristic design with frosted glass cards, buttons, and inputs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 WGRplane Tech Stack
|
||||||
|
|
||||||
|
| Component | Technology |
|
||||||
|
|-----------|------------|
|
||||||
|
| **Backend** | Go, Gorilla Mux, GORM (SQLite via glebarez/sqlite) |
|
||||||
|
| **Frontend** | Vue 3, TypeScript, Vite, TailwindCSS 4, vue-i18n 9 |
|
||||||
|
| **Auth** | JWT (golang-jwt/v5), TOTP (pquerna/otp), API Key |
|
||||||
|
| **WebSockets** | gorilla/websocket |
|
||||||
|
| **Webhooks** | Go net/http with retry + exponential backoff |
|
||||||
|
| **Scheduling** | robfig/cron v3 |
|
||||||
|
| **QR Code** | skip2/go-qrcode |
|
||||||
|
| **Email** | jordan-wright/email (SMTP) |
|
||||||
|
| **Firewall** | Bash, nftables, inotify-tools, jq |
|
||||||
|
| **Container** | Docker (multi-stage build), docker-compose |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 WGRplane API Endpoints Summary
|
||||||
|
|
||||||
|
All endpoints are served on port **10087**. For complete API documentation with request/response details, see [`app/README.md`](app/README.md) or visit `/swagger/` on your running instance.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
| Method | Header | Notes |
|
||||||
|
|--------|--------|-------|
|
||||||
|
| API Key | `wg-rplane-datadunia: <KEY>` | Set via env var `WG_API_KEY`. Default: `test-api-key` |
|
||||||
|
| JWT | `Authorization: Bearer <TOKEN>` | Expires in 15 minutes. Secret via env var `JWT_SECRET` |
|
||||||
|
| TOTP | `X-TOTP: <CODE>` | Required if user enables TOTP |
|
||||||
|
|
||||||
|
### Main Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `/api/servers` | `GET/POST` | List all servers / Create new server |
|
||||||
|
| `/api/servers/{id}` | `GET/PUT/DELETE` | Get/Update/Delete server |
|
||||||
|
| `/api/servers/{id}/peers` | `GET/POST` | List peers / Create new peer |
|
||||||
|
| `/api/peers/{id}` | `PUT/DELETE` | Update/Delete peer |
|
||||||
|
| `/api/peers/{id}/config` | `GET` | Download WireGuard `.conf` file |
|
||||||
|
| `/api/peers/{id}/qrcode` | `GET` | Generate QR code PNG for mobile import |
|
||||||
|
| `/api/servers/{id}/webhooks` | `GET/POST` | List/Create webhooks |
|
||||||
|
| `/api/stats` | `GET` | Global statistics |
|
||||||
|
| `/ws/stats` | WebSocket | Real-time stats broadcast (5s interval) |
|
||||||
|
| `/swagger/` | - | Interactive Swagger UI documentation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 WGRplane Installation
|
||||||
|
|
||||||
|
### Option 1: Docker Compose (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://git.datadunia.com/hainzero/WGRplane.git
|
||||||
|
cd 03.wireguard-policy
|
||||||
|
|
||||||
|
# Start WGRplane and WireGuard
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Access dashboard at http://localhost:10087
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Install Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run automated installer (Ubuntu/Debian/CentOS)
|
||||||
|
sudo ./install.sh install
|
||||||
|
|
||||||
|
# Uninstall
|
||||||
|
sudo ./install.sh uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Manual Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build Go binary
|
||||||
|
cd app
|
||||||
|
go build -o ../wgrplane .
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Build frontend
|
||||||
|
cd app/frontend
|
||||||
|
npm install && npm run build
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
# Run
|
||||||
|
./wgrplane
|
||||||
|
# Server starts on :10087
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 4: Native CLI / Systemd Service
|
||||||
|
|
||||||
|
The WGRplane binary includes a built-in CLI to manage its own systemd service.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check system dependencies first
|
||||||
|
./wgrplane doctor
|
||||||
|
|
||||||
|
# Install as systemd service (auto-creates unit, enables, and starts)
|
||||||
|
sudo ./wgrplane install
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
journalctl -u wgrplane -f
|
||||||
|
|
||||||
|
# Other available commands:
|
||||||
|
sudo ./wgrplane stop
|
||||||
|
sudo ./wgrplane restart
|
||||||
|
sudo ./wgrplane uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
```
|
||||||
|
03.wireguard-policy/
|
||||||
|
├── app/ # Go backend + Vue frontend
|
||||||
|
│ ├── main.go # Bootstrap server, routing, init DB
|
||||||
|
│ ├── handlers.go # REST API route handlers
|
||||||
|
│ ├── models.go # GORM models (Server, Peer, Webhook, SMTP)
|
||||||
|
│ ├── auth.go # JWT, TOTP, API key auth middleware
|
||||||
|
│ ├── nftables.go # nftables rule management (mode forward)
|
||||||
|
│ ├── webhook.go # Webhook engine with retry/backoff
|
||||||
|
│ ├── scheduler.go # Cron jobs (expiry, data limit, reset)
|
||||||
|
│ ├── stats.go # WebSocket Hub for real-time stats
|
||||||
|
│ ├── 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-sync-watch.sh # inotifywait watcher daemon
|
||||||
|
├── wg-policy-ctl # CLI wrapper for policy management
|
||||||
|
├── wg-policy-cleanup.sh # Cleanup script for PostDown
|
||||||
|
├── wg-policy.service # Systemd unit for watcher daemon
|
||||||
|
├── wgrplane.service # Systemd unit for Go backend
|
||||||
|
├── install.sh # Automated installer (Docker + services)
|
||||||
|
├── Dockerfile # Multi-stage Docker build
|
||||||
|
├── docker-compose.yml # Docker Compose stack
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed WGRplane documentation including webhooks, plugins, scheduler, and frontend details, refer to [`app/README.md`](app/README.md).
|
||||||
|
|||||||
+1
-1
Submodule app updated: d4462053a5...b43866d42c
@@ -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
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+2
-56
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user