diff --git a/.gitignore b/.gitignore index 7b8dbea..e923bd6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .test/ +wgrplane +.sisyphus/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..69ff9a8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +## Multi-stage Dockerfile for WGRplane +## Stage 1: Go backend build +FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder-go +WORKDIR /src +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/root/.cache/go-build \ + go mod download +COPY app ./app +ENV CGO_ENABLED=0 +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -o /bin/wgrplane ./app + +## Stage 2: Frontend build (Vue 3) +FROM node:20-alpine AS frontend-builder +WORKDIR /src/app/frontend +COPY app/frontend/package*.json ./ +RUN npm ci +COPY app/frontend/ . +RUN npm run build + +## Stage 3: Production image +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /root +COPY --from=builder-go /bin/wgrplane /usr/local/bin/wgrplane +COPY --from=frontend-builder /src/app/frontend/dist /var/www/frontend +ENV APP_FRONTEND_DIR=/var/www/frontend +EXPOSE 10087 +ENTRYPOINT ["/usr/local/bin/wgrplane"] diff --git a/README.md b/README.md index 74865eb..4827950 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,337 @@ -# WireGuard Dynamic Policy Firewall +# WGRplane -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`. +**WireGuard Control Plane with Dynamic Policy Firewall.** -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. +WGRplane is a Go-native WireGuard management dashboard paired with a shell-based dynamic iptables/ipset policy engine. It gives you full peer lifecycle management, per-peer firewall policies, webhook integrations, and a glassmorphism-styled Vue 3 frontend. --- -## 🎯 Architecture & Data Flow +## Overview -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 `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. +WGRplane operates in two complementary layers: + +| Layer | What it does | +|-------|-------------| +| **WGRplane (Go + Vue 3)** | Web dashboard and REST API for managing WireGuard servers, peers, webhooks, SMTP, and scheduling. | +| **Policy Firewall (Shell)** | Dynamic iptables/ipset engine that reads `#Access` comments from `wg0.conf` to enforce per-peer egress rules. | + +### Hybrid Mode + +The Go backend supports two server modes: + +- **`forward`** -- The WGRplane instance directly applies nftables rules on the local machine. Peer access policies are enforced immediately via `nft` commands. +- **`standalone`** -- The instance 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 | Purpose | +|--------|---------| +| **AllowAccess** | List of CIDRs the peer can reach (internal targets). | +| **AllowInternet** | Boolean flag. When `true`, the peer gets unrestricted internet egress. | + +Peers with neither rule are isolated from each other and from the internet by default. --- -## 📁 Installation +## Architecture -The easiest way to install is using the provided `install.sh` script, which automatically installs dependencies, copies all scripts to `/usr/local/bin/`, sets up the systemd daemon, and enables the service. +```mermaid +graph TB + subgraph "WGRplane Dashboard" + A[Vue 3 Frontend] -->|HTTP/WebSocket| B[Go REST API :10087] + end + + subgraph "Go Backend" + B --> C[(SQLite DB)] + B --> D[Webhook Engine] + B --> E[Scheduler Cron] + B --> F[nftables Engine] + B --> G[SMTP / Email] + B --> H[Plugin Manager] + end + + subgraph "Policy Firewall" + I[wg0.conf #Access] -->|inotifywait| J[wg-sync-watch.sh] + J --> K[wg-sync-policy.sh] + K -->|atomic write| L[policy.json] + L --> M[wg-policy-engine.sh] + M --> N[iptables / ipset] + end + + D -.->|peer_created/updated/deleted| O[External Webhooks] + F -.->|forward mode| N + E -.->|daily/monthly| P[Peer Lifecycle] +``` + +### Data Flow + +``` +wg0.conf (#Access comments) + ↓ inotifywait detects change +wg-sync-watch.sh (debounced trigger) + ↓ +wg-sync-policy.sh (parses wg0.conf → policy.json, atomic write) + ↓ +wg-policy-engine.sh (reads policy.json → iptables/ipset rules) + ↓ +Active firewall rules (WG_POLICY chain) +``` + +### Tech Stack + +| Component | Technology | +|-----------|------------| +| **Backend** | Go 1.25, Gorilla Mux, GORM (SQLite) | +| **Frontend** | Vue 3, TypeScript, Vite, TailwindCSS 4, vue-i18n 9 | +| **Auth** | JWT (golang-jwt), TOTP (pquerna/otp), API Key | +| **Webhooks** | Go net/http with retry + exponential backoff | +| **Scheduling** | robfig/cron v3 | +| **Firewall** | Bash, iptables, ipset, nftables, inotify-tools, jq | +| **Container** | Docker (multi-stage build), docker-compose | + +--- + +## Quick Start + +### Option 1: Docker Compose (Recommended) ```bash -# Install everything +# Clone the repository +git clone https://git.datadunia.com/hainzero/WGRplane.git +cd 03.wireguard-policy + +# Start both WGRplane and WireGuard +docker compose up -d + +# Access the dashboard +# http://localhost:10087 +``` + +The compose stack runs: +- **WGRplane** on port `10087` (Go API + Vue frontend) +- **WireGuard** container with host networking for kernel module access + +### Option 2: Install Script + +```bash +# Run the automated installer (Ubuntu/Debian/CentOS) sudo ./install.sh install -# Uninstall everything +# Uninstall sudo ./install.sh uninstall ``` -### Building the Installer (For Developers) +The install script handles Docker installation, repository cloning, `.env` creation, and service startup. -If you modify any of the source `.sh` or `.service` files, you must rebuild the `install.sh` script using the provided builders. +### Option 3: Manual Build -**On Linux (Bash):** ```bash -./build.sh +# Build the Go binary +cd app +go build -o ../wgrplane . +cd .. + +# Build the frontend +cd app/frontend +npm install && npm run build +cd ../.. + +# Run +./wgrplane +# Server starts on :10087 ``` -**On Windows (CMD/PowerShell):** -```cmd -build.bat +### Option 4: Systemd Service + +```bash +# Install the 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.service -f ``` --- -## 📦 Prerequisites +## API Documentation -| Package | Required | Install | -|---------|----------|---------| -| `jq` | **Yes** | `apt install jq` | -| `inotify-tools` | **Yes** (for watcher daemon) | `apt install inotify-tools` | -| `ipset` | Optional | `apt install ipset` | +All API endpoints are served on port `10087`. Authentication is via API key header (`wg-rplane-datadunia`) or JWT Bearer token with optional TOTP. -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. +### Authentication + +| Method | Header | Notes | +|--------|--------|-------| +| API Key | `wg-rplane-datadunia: ` | Set via `WG_API_KEY` env var. Default: `test-api-key`. | +| JWT | `Authorization: Bearer ` | 15-minute expiry. Set secret via `JWT_SECRET` env var. | +| TOTP | `X-TOTP: ` | Required if user has TOTP enabled. | + +### Servers + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/servers` | `GET` | List all registered WireGuard servers. | +| `/api/servers` | `POST` | Create a new server entry. Body: `{name, mode, publicKey, endpoint}`. | +| `/api/servers/{id}` | `GET` | Get a single server by ID. | +| `/api/servers/{id}` | `PUT` | Update server fields (name, mode, publicKey, endpoint). | +| `/api/servers/{id}` | `DELETE` | Delete a server and cascade-remove its peers and webhooks. | + +### Peers + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/servers/{id}/peers` | `GET` | List all peers for a server. | +| `/api/servers/{id}/peers` | `POST` | Create a new peer. Body: `{publicKey, ip, allowAccess, allowInternet}`. Applies nftables rules in `forward` mode or triggers webhooks in `standalone` mode. | +| `/api/peers/{id}` | `PUT` | Update a peer. Computes diffs and applies incremental nftables changes. | +| `/api/peers/{id}` | `DELETE` | Delete a peer. Cleans up nftables rules and triggers webhooks. | +| `/api/peers/{id}/config` | `GET` | Download the peer's WireGuard `.conf` file. | +| `/api/peers/{id}/qrcode` | `GET` | Get a QR code PNG image of the peer config (for mobile import). | + +### Webhooks + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/servers/{id}/webhooks` | `GET` | List webhooks for a server. | +| `/api/servers/{id}/webhooks` | `POST` | Create a webhook. Body: `{name, url, template, customBody, customHeaders, subscribedActions, isEnabled, verifySSL}`. | +| `/api/webhooks/{id}` | `DELETE` | Delete a webhook. | + +### SMTP Settings + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/settings/smtp` | `GET` | Get current SMTP configuration. | +| `/api/settings/smtp` | `POST` | Save SMTP settings for email notifications. | + +### Statistics + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/stats` | `GET` | Global stats: total servers, peers, webhooks. | +| `/api/servers/{id}/stats` | `GET` | Per-server stats: peer count, webhook count, server details. | + +### WebSocket + +| Endpoint | Protocol | Description | +|----------|----------|-------------| +| `/ws/stats` | WebSocket | Real-time stats broadcast (5-second interval). Connects to the Hub for live peer/traffic updates. | --- -## ⚙️ Integrasi ke `wg0.conf` +## Webhook Payload Spec -To integrate the engine, you need to append hooks into your `wg0.conf` interface block, and declare the `#Access` tags under each peer. +Webhooks are triggered on peer lifecycle events (`peer_created`, `peer_updated`, `peer_deleted`, `policy_changed`). The engine supports three template modes: `default` (raw JSON), `mikrotik` (RouterOS-formatted), and `custom` (Go template). -### 1. Interface Block (Hooks) -Add the `PostUp` and `PostDown` scripts so the engine initializes correctly during VPN startup and removes traces upon shutdown. +### Default Payload -> **⚠️ WireGuard does NOT support multiline values.** Every command must be on a `PostUp = ...` or `PostDown = ...` line. Bare commands without the `PostUp =` prefix will cause `Configuration parsing error`. +```json +{ + "event": "peer_created", + "timestamp": "2026-05-03T10:30:00Z", + "server": { + "id": 1, + "name": "wg-server-01", + "mode": "forward", + "publicKey": "abc123...", + "endpoint": "vpn.example.com:51820" + }, + "peer": { + "id": 2, + "publicKey": "xyz789...", + "ip": "10.0.0.2", + "allowAccess": ["192.168.1.0/24", "10.10.0.0/16"], + "allowInternet": true, + "enabled": true, + "dataLimitGB": 0, + "expiresAt": "0001-01-01T00:00:00Z" + }, + "policy": { + "action": "created", + "changes": ["peer_created"] + } +} +``` + +### Mikrotik Template + +```json +{ + "action": "created", + "peer": { + "public_key": "xyz789...", + "ip": "10.0.0.2", + "allow_access": "[\"192.168.1.0/24\"]", + "allow_internet": true + }, + "server": { + "name": "wg-server-01", + "mode": "forward" + } +} +``` + +### Webhook Features + +- **Retry with backoff**: Failed deliveries retry up to 3 times with exponential backoff (2s, 4s, 8s). +- **SSL verification**: Toggleable per-webhook via `verifySSL`. +- **Custom headers**: Per-webhook header injection via `customHeaders` JSON. +- **Global webhooks**: Set `isGlobal: true` to fire across all servers. +- **Action filtering**: Subscribe to specific events via `subscribedActions` array. + +--- + +## UI Features + +The frontend is a Vue 3 + TypeScript SPA with a glassmorphism design language. + +### Design System + +- **Glassmorphism**: Frosted glass cards, buttons, and inputs with backdrop blur effects. +- **TailwindCSS 4**: Utility-first styling with full dark mode support via `dark:` variants. +- **Responsive**: Mobile-first layout that adapts to all screen sizes. + +### Features + +| Feature | Description | +|---------|-------------| +| **Multi-language (i18n)** | English, Indonesian, and Chinese via vue-i18n 9. Locale auto-detected from browser. | +| **Theme Switching** | Dark / Light / Auto (follows system preference) via `@vueuse/core`. | +| **Real-time Stats** | WebSocket connection broadcasts live peer counts and traffic data every 5 seconds. | +| **Charts** | Traffic visualization via Chart.js + vue-chartjs. | +| **Toast Notifications** | Non-intrusive alerts via vue-sonner. | +| **QR Code Import** | Generate scannable QR codes for quick mobile WireGuard client setup. | + +### Frontend Structure + +``` +app/frontend/src/ +├── App.vue # Root component with theme/i18n providers +├── main.ts # App bootstrap (Vue, Router, i18n) +├── router/ # Vue Router definitions +├── i18n/ # Locale files (en.json, id.json, zh.json) +├── components/ # Glass UI components (Card, Button, Input, Toggle) +├── composables/ # Vue composables (useTheme, etc.) +├── views/ # Page components (Dashboard, Servers, Peers, Settings) +└── types/ # TypeScript type definitions +``` + +--- + +## Policy Firewall (`#Access`) + +The shell-based policy engine enforces per-peer firewall rules directly from `wg0.conf`. + +### How It Works + +1. Add `#Access` comments under each `[Peer]` block in `wg0.conf`. +2. The watcher daemon (`wg-sync-watch.sh`) detects file changes via `inotifywait`. +3. `wg-sync-policy.sh` parses the config and writes `policy.json` atomically. +4. `wg-policy-engine.sh` reads the JSON and applies iptables/ipset rules. + +### Example `wg0.conf` ```ini [Interface] @@ -70,20 +339,9 @@ Address = 10.0.0.1/24 ListenPort = 51820 PrivateKey = -# Policy engine: auto-handles NAT, IP forwarding, and dynamic firewall rules PostUp = /usr/local/bin/wg-sync-policy.sh; /usr/local/bin/wg-policy-engine.sh - -# Policy engine: cleanup all firewall and routing traces PostDown = /usr/local/bin/wg-policy-cleanup.sh -``` -### 2. Peer Block (`#Access` Tags) -For each client, use the `#Access` comment line. Define the destinations (targets) the peer is allowed to access. You can separate multiple IPs or CIDRs with commas or semicolons. - -> **⚠️ WARNING: Do NOT use `SaveConfig = true`!** -> WireGuard's `SaveConfig` feature overwrites `wg0.conf` directly and **strips all comments**, which will permanently delete all `#Access` tags. If you are using a Web UI/Dashboard, make sure it does not strip unknown comments when saving. - -```ini [Peer] PublicKey = AllowedIPs = 10.0.0.2/32 @@ -92,82 +350,126 @@ AllowedIPs = 10.0.0.2/32 [Peer] PublicKey = AllowedIPs = 10.0.0.3/32 -#Access = 10.0.0.1/32 +#Access 10.0.0.1/32 [Peer] PublicKey = AllowedIPs = 10.0.0.4/32 -#Access -# ^ (Empty Access implies internet-only, client isolation applies) +#Access +# Empty Access = internet-only, client isolation applies ``` -**⚠️ Important constraint:** Why `#Access` instead of just using `AllowedIPs` directly? -WireGuard uses `AllowedIPs` for Cryptokey Routing (deciding which tunnel interface to route outbound packets). If you put an internal server IP inside the server's `wg0.conf` AllowedIPs block, WireGuard will aggressively capture and redirect packets bound for that internal server into the VPN client's tunnel. The `#Access` comment separates routing parameters from firewall parameters cleanly. +### Why `#Access` Instead of `AllowedIPs`? + +WireGuard uses `AllowedIPs` for Cryptokey Routing. Putting destination IPs in the server's `AllowedIPs` would cause WireGuard to route traffic for those IPs into the client tunnel. The `#Access` comment cleanly separates firewall policy from routing configuration. + +### CLI: `wg-policy-ctl` + +```bash +wg-policy-ctl status # Health check, lock status, rule counts +wg-policy-ctl policy # View raw policy.json +wg-policy-ctl rules # Inspect active iptables rules +wg-policy-ctl ipset # View ipset mappings +wg-policy-ctl reload # Force re-sync and re-apply +wg-policy-ctl log # View dropped packet logs +wg-policy-ctl stats # Connection statistics +wg-policy-ctl validate # Validate policy.json schema +``` + +### Prerequisites + +| Package | Required | Install | +|---------|----------|---------| +| `jq` | Yes | `apt install jq` | +| `inotify-tools` | Yes (watcher daemon) | `apt install inotify-tools` | +| `ipset` | Optional | `apt install ipset` | + +Without `ipset`, the engine falls back to per-rule iptables entries. This works for small deployments. For large peer counts, `ipset` provides O(1) lookup performance. --- -## 🛠 `wg-policy-ctl` CLI Usage +## Scheduler -You don't need to manually interact with `iptables` or `.json` files. Use the `wg-policy-ctl` wrapper. +The Go backend runs three background cron jobs: -```bash -# View the health of the firewall engine and active locks -wg-policy-ctl status +| Schedule | Job | Action | +|----------|-----|--------| +| Daily 2:00 AM | `deleteExpiredPeers` | Removes peers past their `ExpiresAt` date. | +| Daily 3:00 AM | `restrictOverLimitPeers` | Disables peers that exceeded `DataLimitGB`. | +| 1st of month | `resetMonthlyUsage` | Resets `CurrentDataUsageBytes` to zero for all peers. | -# View the raw, parsed JSON policy -wg-policy-ctl policy +--- -# Inspect active iptables rules -wg-policy-ctl rules +## Plugins -# Check memory sets mapping IP targets (ipset) -wg-policy-ctl ipset +The plugin system provides a simple notification interface. Built-in plugins include: -# Manually re-sync rules immediately -wg-policy-ctl reload +- **TelegramNotifier** -- Sends Telegram messages on events. +- **SlackNotifier** -- Sends Slack messages on events. +- **TrafficLogger** -- Logs traffic events for debugging. -# Inspect dropped packets (rate-limited log output) -wg-policy-ctl log +Plugins are loaded at startup via `PluginManager.LoadPlugins()` and receive events through `Trigger(event, payload)`. -# See connection statistics, how many targets loaded -wg-policy-ctl stats +--- -# Force validation of the policy schema -wg-policy-ctl validate +## Configuration + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `WG_API_KEY` | `test-api-key` | API key for header-based authentication. | +| `JWT_SECRET` | `secret` | Secret for JWT token signing. | +| `APP_FRONTEND_DIR` | `/var/www/frontend` | Path to built frontend assets. | +| `WG_RPLANE_MODE` | `forward` | Default server mode (`forward` or `standalone`). | + +### Database + +WGRplane uses SQLite by default (`wgrplane.db`). Models auto-migrate on startup: + +- **Server** -- WireGuard server entries with mode and endpoint. +- **Peer** -- Peer entries with IP, access rules, data limits, expiry. +- **Webhook** -- Webhook configurations with templates and action filters. +- **SMTPSettings** -- SMTP server configuration for email notifications. + +--- + +## Project Structure + +``` +03.wireguard-policy/ +├── app/ # Go backend + Vue frontend +│ ├── handlers.go # REST API route handlers +│ ├── main.go # Server bootstrap, routes +│ ├── models.go # GORM models (Server, Peer, Webhook, SMTP) +│ ├── auth.go # JWT, TOTP, API key auth middleware +│ ├── nftables.go # nftables rule management (forward mode) +│ ├── webhook.go # Webhook engine with retry/backoff +│ ├── scheduler.go # Cron jobs (expiry, data limit, reset) +│ ├── stats.go # WebSocket Hub for real-time stats +│ ├── email.go # SMTP email notifications +│ ├── plugins.go # Plugin system (Telegram, Slack, Logger) +│ ├── validation.go # Input validators (IP, CIDR, PublicKey) +│ ├── wg.go # WireGuard key generation, config export +│ ├── i18n.go # Backend i18n (en/id/zh) +│ ├── frontend/ # Vue 3 SPA (TypeScript, TailwindCSS) +│ └── active.{en,id,zh}.json # Translation files +├── wg-sync-policy.sh # Parses wg0.conf → policy.json +├── wg-policy-engine.sh # Applies policy.json → iptables/ipset +├── wg-sync-watch.sh # inotifywait watcher daemon +├── wg-policy-ctl # CLI wrapper for 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 +├── build.sh / build.bat # Installer rebuild scripts +└── README.md # This file ``` --- -## 🔧 Systemd Integration (Watcher Daemon) +## License -If you use the `install.sh` script, the daemon is automatically installed, enabled, and started for you. It monitors `wg0.conf` for changes and triggers the pipeline seamlessly. - -### Manual File Installation (If not using install.sh) -Place the three provided systemd unit files into `/etc/systemd/system/`. - -| Systemd File | Location | Description | -|--------------|----------|-------------| -| `wg-policy.service` | `/etc/systemd/system/wg-policy.service` | The main daemon that runs `wg-sync-watch.sh` | -| `wg-policy-health.timer` | `/etc/systemd/system/wg-policy-health.timer` | Triggers the health check every 5 minutes | -| `wg-policy-health.service`| `/etc/systemd/system/wg-policy-health.service`| Executes the actual health check logic | - -```bash -# Example copy command -cp wg-policy.service wg-policy-health.timer wg-policy-health.service /etc/systemd/system/ -``` - -### 2. Enable & Start Services -After copying the files, reload systemd to recognize them, then enable and start the services. - -```bash -systemctl daemon-reload -systemctl enable wg-policy.service -systemctl enable wg-policy-health.timer -systemctl start wg-policy.service -systemctl start wg-policy-health.timer -``` - -Check the watcher logs: -```bash -journalctl -u wg-policy.service -f -``` +This project builds upon concepts from WGDashboard (donaldzou/WGDashboard) with modifications for policy.json API integration and dynamic firewall enforcement. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1145eaf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.8" +services: + wgrplane: + build: + context: . + dockerfile: Dockerfile + ports: + - "10087:10087" + volumes: + - ./wgrplane-data:/var/lib/wgrplane + - ./wireguard-config:/etc/wireguard + depends_on: + - wireguard + restart: unless-stopped + + wireguard: + image: ghcr.io/linuxserver/wireguard:latest + container_name: wireguard + cap_add: + - NET_ADMIN + - SYS_MODULE + network_mode: "host" + volumes: + - /etc/wireguard:/config + - /lib/modules:/lib/modules + environment: + - PUID=1000 + - PGID=1000 + - TZ=UTC + restart: unless-stopped diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b852a13 --- /dev/null +++ b/go.mod @@ -0,0 +1,39 @@ +module github.com/your-org/03.wireguard-policy + +go 1.25.0 + +require ( + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/gorilla/mux v1.8.1 + github.com/gorilla/websocket v1.5.3 + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible + github.com/nicksnyder/go-i18n/v2 v2.6.1 + github.com/pquerna/otp v1.5.0 + github.com/robfig/cron/v3 v3.0.1 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + github.com/swaggo/http-swagger v1.3.4 + github.com/swaggo/swag v1.16.6 + golang.org/x/text v0.36.0 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/swaggo/files v1.0.1 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/tools v0.43.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..765145e --- /dev/null +++ b/go.sum @@ -0,0 +1,124 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= +github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= +github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= +github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/install.sh b/install.sh index beb06bf..7a16a66 100644 --- a/install.sh +++ b/install.sh @@ -1,1312 +1,129 @@ -#!/bin/bash -# WireGuard Policy Firewall Installer/Uninstaller - +#!/usr/bin/env bash set -euo pipefail -if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root." - exit 1 +echo "[WGRplane] Easy Install Script – install.sh" + +# Detect OS +if [ -f /etc/os-release ]; then + . /etc/os-release +fi +OS_ID=${ID:-unknown} +OS_CODENAME=${VERSION_CODENAME:-unknown} + +echo "Detected OS: $OS_ID ($OS_CODENAME)" + +install_docker_apt() { + echo "[WGRplane] Installing Docker and dependencies via apt (Debian/Ubuntu)..." + sudo apt-get update -y + sudo apt-get install -y ca-certificates curl gnupg lsb-release + # Try to install docker.io as the simplest path + if ! command -v docker >/dev/null 2>&1; then + if apt-cache show docker.io >/dev/null 2>&1; then + sudo apt-get install -y docker.io + else + echo "[WGRplane] Fallback: installing docker-ce from Docker's official repo..." + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh get-docker.sh + rm -f get-docker.sh + fi + fi + sudo systemctl enable --now docker || true + + # Install Docker Compose (plugin-based or legacy) + if command -v docker-compose >/dev/null 2>&1; then + echo "[WGRplane] docker-compose already installed (legacy)." + elif command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then + echo "[WGRplane] docker-compose is available as a Docker plugin (docker compose)." + else + echo "[WGRplane] Installing docker-compose (latest)" + # Try to install the latest release without hardcoding version + if command -v curl >/dev/null 2>&1; then + LATEST_URL=$(curl -fsSL https://api.github.com/repos/docker/compose/releases/latest 2>/dev/null | grep -o 'https://[^\"]*linux-$(uname -m\)' || true) + if [ -z "$LATEST_URL" ]; then + LATEST_URL="https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" + fi + sudo sh -c "curl -L $LATEST_URL -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose" + if command -v docker-compose >/dev/null 2>&1; then + echo "[WGRplane] docker-compose installed at /usr/local/bin/docker-compose" + fi + else + echo "[WGRplane] curl not available. Skipping docker-compose installation." + fi + fi +} + +install_docker_yum() { + echo "[WGRplane] Installing Docker via yum/dnf (CentOS/RHEL)..." + if command -v dnf >/dev/null 2>&1; then + sudo dnf -y install dnf-plugins-core + sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + else + sudo yum -y install yum-utils + sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + sudo yum -y install docker-ce docker-ce-cli containerd.io + fi + sudo systemctl enable --now docker || true +} + +case "$OS_ID" in + ubuntu|debian) + install_docker_apt + ;; + centos|fedora|rhel|amazon) + install_docker_yum + ;; + *) + echo "[WGRplane] Unsupported OS for automated install. Please install Docker manually."; exit 1 + ;; +esac + +echo "[WGRplane] Docker verification: $(docker --version 2>&1 || echo 'not found')" +if command -v docker-compose >/dev/null 2>&1; then + echo "[WGRplane] docker-compose is available: $(docker-compose --version 2>&1)" +elif command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then + echo "[WGRplane] docker compose (plugin) is available: $(docker compose version | tr -d '\n')" +else + echo "[WGRplane] Docker Compose not detected. Attempting to install via Docker plugin if possible..." fi -install_policy() { - echo "Installing WireGuard Policy Firewall..." - - # Install dependencies - echo "Checking dependencies..." - apt-get update -y || true - apt-get install -y jq inotify-tools ipset iptables || true - - echo "Writing scripts to /usr/local/bin/..." - - cat << 'EOF_WG_POLICY_LIB' > /usr/local/bin/wg-policy-lib.sh -#!/bin/bash -# wg-policy-lib.sh — Shared functions for WireGuard Policy Firewall -# Source this file; do not execute directly. - -set -euo pipefail - -# ============================================================ -# CONFIGURATION -# ============================================================ -readonly WG_IF="${WG_IF:-wg0}" -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/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 - -# ============================================================ -# LOGGING -# ============================================================ -log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"; } -log_warn() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $*" >&2; } -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 - if (( octet < 0 || octet > 255 )); then - return 1 - fi - done - return 0 -} - -# Validate IPv4 CIDR (e.g., 192.168.1.0/24) -validate_ipv4_cidr() { - local cidr="$1" - local ip prefix - - if [[ "$cidr" == *"/"* ]]; then - ip="${cidr%%/*}" - prefix="${cidr##*/}" - else - # Single IP treated as /32 - ip="$cidr" - prefix="32" - fi - - if ! validate_ipv4 "$ip"; then - return 1 - fi - - if [[ ! "$prefix" =~ ^[0-9]+$ ]] || (( prefix < 0 || prefix > 32 )); then - return 1 - fi - 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}:$ ]] || \ - [[ "$ip" == "::" ]] || \ - [[ "$ip" == "::1" ]]; then - return 0 - fi - return 1 -} - -# Validate IPv6 CIDR -validate_ipv6_cidr() { - local cidr="$1" - local ip prefix - - if [[ "$cidr" == *"/"* ]]; then - ip="${cidr%%/*}" - prefix="${cidr##*/}" - else - ip="$cidr" - prefix="128" - fi - - if ! validate_ipv6 "$ip"; then - return 1 - fi - - if [[ ! "$prefix" =~ ^[0-9]+$ ]] || (( prefix < 0 || prefix > 128 )); then - return 1 - fi - return 0 -} - -# Generic CIDR validator — dispatches to v4 or v6 -validate_cidr() { - local cidr="$1" - if [[ "$cidr" == *":"* ]]; then - validate_ipv6_cidr "$cidr" - else - validate_ipv4_cidr "$cidr" - fi -} - -# ============================================================ -# IPSET MANAGEMENT -# ============================================================ - -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 -} - -# ============================================================ -# RETRY MECHANISM -# ============================================================ - -retry() { - local max_attempts="${MAX_RETRY}" - local delay="${RETRY_DELAY}" - local attempt=1 - local exit_code=0 - - while (( attempt <= max_attempts )); do - if "$@"; then - return 0 - fi - exit_code=$? - log_warn "Attempt $attempt/$max_attempts failed (exit=$exit_code), retrying in ${delay}s..." - sleep "$delay" - (( attempt++ )) - (( delay *= 2 )) # exponential backoff - done - - log_error "All $max_attempts attempts failed for: $*" - return "$exit_code" -} - -# ============================================================ -# LOCK MANAGEMENT -# ============================================================ - -acquire_lock() { - local lock_fd=200 - eval "exec ${lock_fd}>\"${LOCK_FILE}\"" - if ! flock -x -w 10 "$lock_fd"; then - log_error "Failed to acquire lock: ${LOCK_FILE} (timeout 10s)" - return 1 - fi - log_info "Lock acquired: ${LOCK_FILE}" -} - -release_lock() { - # Lock released automatically when fd closes, but we clean up file - rm -f "$LOCK_FILE" 2>/dev/null || true -} - -# ============================================================ -# BACKUP -# ============================================================ - -backup_policy() { - mkdir -p "$BACKUP_DIR" - local timestamp - timestamp="$(date '+%Y%m%d_%H%M%S')" - - if [[ -f "$POLICY_FILE" ]]; then - cp "$POLICY_FILE" "${BACKUP_DIR}/policy_${timestamp}.json" - 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 - find "$BACKUP_DIR" -name 'policy_*.json' -type f -printf '%T@ %p\n' \ - | sort -n \ - | head -n $(( count - 50 )) \ - | awk '{print $2}' \ - | xargs rm -f - log_info "Pruned old backups (kept 50)" - fi -} - -backup_iptables() { - mkdir -p "$BACKUP_DIR" - local timestamp - timestamp="$(date '+%Y%m%d_%H%M%S')" - - if iptables-save > "${BACKUP_DIR}/iptables_${timestamp}.rules" 2>/dev/null; then - log_info "iptables backup: ${BACKUP_DIR}/iptables_${timestamp}.rules" - 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 - 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 -} - -# ============================================================ -# DETECTION HELPERS -# ============================================================ - -detect_lan_subnets() { - ip -o -4 route show scope link \ - | awk '{print $1}' \ - | grep -E '^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)' \ - | sort -u -} - -detect_wg_subnet() { - local family="${1:-inet}" - if [[ "$family" == "inet6" ]]; then - ip -o -6 addr show "$WG_IF" 2>/dev/null \ - | awk '{print $4; exit}' - else - ip -o -4 addr show "$WG_IF" 2>/dev/null \ - | awk '{print $4; exit}' - fi -} - -detect_default_if() { - local def_if - def_if=$(ip -4 route ls 2>/dev/null | grep default | grep -Po '(?<=dev )(\S+)' | head -1 || true) - if [[ -z "$def_if" ]]; then - echo "eth0" - else - echo "$def_if" - fi -} - -# ============================================================ -# HEALTH CHECK -# ============================================================ - -health_check() { - local status=0 - local report="" - - # 1. Check interface exists - if ip link show "$WG_IF" &>/dev/null; then - report+="[OK] Interface $WG_IF is UP\n" - else - report+="[FAIL] Interface $WG_IF not found\n" - status=1 - fi - - # 2. Check policy.json exists and is valid - if [[ -f "$POLICY_FILE" ]] && jq empty "$POLICY_FILE" 2>/dev/null; then - local client_count - client_count=$(jq '(.clients // {}) | length' "$POLICY_FILE") - report+="[OK] policy.json valid ($client_count clients)\n" - else - report+="[FAIL] policy.json missing or corrupt\n" - status=1 - fi - - # 3. Check chain exists - if iptables -L "$CHAIN" -n &>/dev/null; then - local rule_count - 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] Chain $CHAIN not found\n" - status=1 - fi - - # 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 has no reference to $CHAIN\n" - status=1 - fi - - # 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=$(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] ipset $set_name not created (may not be needed)\n" - fi - done - - # 6. Check watcher service - if systemctl is-active --quiet wg-policy.service 2>/dev/null; then - report+="[OK] wg-policy.service is running\n" - else - report+="[INFO] wg-policy.service not running\n" - fi - - # 7. Check lock file not stale - if [[ -f "$LOCK_FILE" ]]; then - local lock_age - lock_age=$(( $(date +%s) - $(stat -c %Y "$LOCK_FILE" 2>/dev/null || echo 0) )) - if (( lock_age > 300 )); then - report+="[WARN] Stale lock file (${lock_age}s old)\n" - else - report+="[OK] Lock file age: ${lock_age}s\n" - fi - else - report+="[OK] No stale lock file\n" - fi - - echo -e "$report" - return $status -} -EOF_WG_POLICY_LIB - - cat << 'EOF_WG_POLICY_ENGINE' > /usr/local/bin/wg-policy-engine.sh -#!/bin/bash -# 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" - -# ============================================================ -# ROLLBACK -# ============================================================ - -rollback() { - log_error "ROLLBACK triggered! Restoring previous rules..." - - # 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 - break - fi - done - - # Flush and remove new chain - iptables -F "$CHAIN" 2>/dev/null || true - iptables -X "$CHAIN" 2>/dev/null || true - - # 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 - - iptables -A FORWARD -i "$WG_IF" -j "$CHAIN" - log_info "Rollback: restored from backup chain" - fi - - # Cleanup backup chain - iptables -F "$CHAIN_BACKUP" 2>/dev/null || true - iptables -X "$CHAIN_BACKUP" 2>/dev/null || true - - # Cleanup backup ipsets - destroy_ipset "${IPSET_V4}_bak" 2>/dev/null || true - destroy_ipset "${IPSET_V6}_bak" 2>/dev/null || true -} - -# ============================================================ -# MAIN -# ============================================================ - -main() { - log_info "Starting policy engine..." - - # === VALIDATE === - if ! ip link show "$WG_IF" &>/dev/null; then - log_error "Interface $WG_IF is not running. Aborting policy engine." - exit 1 - fi - - if [[ ! -f "$POLICY_FILE" ]]; then - log_error "Policy file not found: $POLICY_FILE" - exit 1 - fi - - if ! jq empty "$POLICY_FILE" 2>/dev/null; then - log_error "policy.json is corrupt" - exit 1 - fi - - # Backup iptables state - backup_iptables - - # Set trap for rollback on failure - trap 'rollback' ERR - - # === DETECT SUBNETS === - local WG_SUBNET WG_SUBNET_V6 LAN_SUBNETS DEF_IF - - WG_SUBNET="$(detect_wg_subnet inet)" - WG_SUBNET_V6="$(detect_wg_subnet inet6)" - LAN_SUBNETS="$(detect_lan_subnets)" - DEF_IF="$(detect_default_if)" - - if [[ -z "$WG_SUBNET" ]]; then - log_warn "Interface $WG_IF has no IPv4, skipping client isolation" - else - log_info "WG IPv4 subnet: $WG_SUBNET" - fi - - if [[ -n "$WG_SUBNET_V6" ]]; then - log_info "WG IPv6 subnet: $WG_SUBNET_V6" - fi - - if [[ -n "$LAN_SUBNETS" ]]; then - log_info "Detected LAN subnets:" - echo "$LAN_SUBNETS" | while read -r s; do log_info " $s"; done - fi - - # === BASE ROUTING & NAT === - # Enable IP Forwarding - sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true - if command -v ip6tables &>/dev/null; then - sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true - fi - - # Setup MASQUERADE on default interface - if ! iptables -t nat -C POSTROUTING -o "$DEF_IF" -j MASQUERADE 2>/dev/null; then - iptables -t nat -A POSTROUTING -o "$DEF_IF" -j MASQUERADE - log_info "Enabled IPv4 MASQUERADE on $DEF_IF" - fi - - # === CLEANUP OLD CHAIN (loop until all references removed) === - log_info "Cleaning up old chain references..." - while true; do - local rline="" - rline=$(iptables -nL FORWARD --line-numbers 2>/dev/null | grep "$CHAIN" | awk '{print $1}' | head -n 1 || true) - if [[ -n "$rline" ]]; then - iptables -D FORWARD "$rline" 2>/dev/null || break - else - break - fi - done - - # Backup existing chain before flushing - if iptables -L "$CHAIN" -n &>/dev/null; then - iptables -N "$CHAIN_BACKUP" 2>/dev/null || iptables -F "$CHAIN_BACKUP" - iptables-save -c 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.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) - trap - ERR - - # === VERIFY === - local 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] iptables policy applied. Chain: $CHAIN" -} - -main "$@" -EOF_WG_POLICY_ENGINE - - cat << 'EOF_WG_POLICY_CLEANUP' > /usr/local/bin/wg-policy-cleanup.sh -#!/bin/bash -# wg-policy-cleanup.sh — Clean removal of all policy artifacts - -set -uo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/wg-policy-lib.sh" - -main() { - log_info "Starting cleanup..." - - local DEF_IF - DEF_IF="$(detect_default_if)" - - # === Base Routing Cleanup === - while iptables -D FORWARD -o "$WG_IF" -j ACCEPT 2>/dev/null; do :; done - while iptables -t nat -D POSTROUTING -o "$DEF_IF" -j MASQUERADE 2>/dev/null; do :; done - log_info "Removed base routing and NAT rules" - - # === IPv4 chain cleanup === - local removed=0 - - while 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" -} - -main "$@" -EOF_WG_POLICY_CLEANUP - - cat << 'EOF_WG_SYNC_POLICY' > /usr/local/bin/wg-sync-policy.sh -#!/bin/bash -# wg-sync-policy.sh — Reads wg0.conf, validates, generates policy.json atomically -# Fixed: IP validation, atomic write, proper locking, error handling - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/wg-policy-lib.sh" - -# ============================================================ -# MAIN -# ============================================================ - -main() { - log_info "Starting policy sync..." - - # Validate prerequisites - if [[ ! -f "$WG_CONF" ]]; then - log_error "WireGuard config not found: $WG_CONF" - exit 1 - fi - - if ! command -v jq &>/dev/null; then - log_error "jq is required but not installed" - exit 1 - fi - - # Acquire lock - acquire_lock - trap 'release_lock' EXIT - - # Backup current policy - backup_policy - - # Temporary file for atomic write - local tmp_policy - tmp_policy="$(mktemp /tmp/wg-policy.XXXXXX)" - trap "rm -f \"$tmp_policy\" 2>/dev/null; release_lock" EXIT - - echo '{"clients":{}}' > "$tmp_policy" - - # Parse peers from wg0.conf - # AWK extracts IP and #Access comment per [Peer] block - local parse_errors=0 - - awk ' - BEGIN { RS="\n\\[Peer\\]\n"; FS="\n" } - NR>1 { - ip=""; access=""; internet="false" - for(i=1;i<=NF;i++){ - if($i ~ /^AllowedIPs/) { - split($i,a," = ") - gsub(/ /,"",a[2]) - split(a[2],b,",") - split(b[1],c,"/") - ip=c[1] - } - if($i ~ /^#Access/) { - sub(/^#Access[ \t]*=?[ \t]*/, "", $i) - access=$i - } - if($i ~ /^#Internet/) { - if(tolower($i) ~ /true|yes|1|allow/) { - internet="true" - } - } - } - if(ip!="" && ip!="0.0.0.0" && ip!="::") { - printf "%s|%s|%s\n", ip, access, internet - } - } - ' "$WG_CONF" | while IFS="|" read -r ip access_string internet_flag; do - - # === VALIDATE CLIENT IP === - if ! validate_cidr "$ip"; then - log_warn "Invalid client IP skipped: '$ip'" - (( parse_errors++ )) || true - continue - fi - - # === PARSE AND VALIDATE ACCESS TARGETS === - local ACCESS_JSON="[]" - - if [[ -n "$access_string" ]]; then - # Split by ; and , then validate each entry - local valid_targets=() - local IFS_OLD="$IFS" - IFS=';,' - read -ra targets <<< "$access_string" - IFS="$IFS_OLD" - - for target in "${targets[@]}"; do - # Trim whitespace - target="$(echo "$target" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" - - [[ -z "$target" ]] && continue - - if validate_cidr "$target"; then - valid_targets+=("$target") - else - log_warn "Invalid access target skipped for $ip: '$target'" - (( parse_errors++ )) || true - fi - done - - if (( ${#valid_targets[@]} > 0 )); then - ACCESS_JSON=$(printf '%s\n' "${valid_targets[@]}" | jq -R . | jq -s .) - fi - fi - - # Write to temp policy - jq --arg ip "$ip" --argjson access "$ACCESS_JSON" --argjson internet "$internet_flag" \ - '.clients[$ip] = {"name": $ip, "access": $access, "internet": $internet}' \ - "$tmp_policy" > "${tmp_policy}.tmp" && mv "${tmp_policy}.tmp" "$tmp_policy" - - done - - # Validate JSON before atomic move - if ! jq empty "$tmp_policy" 2>/dev/null; then - log_error "Generated JSON is invalid, aborting. Check $tmp_policy" - exit 1 - fi - - # Atomic move (same filesystem = atomic rename) - mv -f "$tmp_policy" "$POLICY_FILE" - log_info "policy.json updated successfully" - - if (( parse_errors > 0 )); then - log_warn "$parse_errors validation errors encountered (see warnings above)" - fi - - local client_count - client_count=$(jq '(.clients // {}) | length' "$POLICY_FILE") - log_info "Total clients in policy: $client_count" -} - -main "$@" -EOF_WG_SYNC_POLICY - - cat << 'EOF_WG_SYNC_WATCH' > /usr/local/bin/wg-sync-watch.sh -#!/bin/bash -# wg-sync-watch.sh — Watches wg0.conf for changes with debounce -# Fixed: proper debounce, error isolation, health reporting - -set -uo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/wg-policy-lib.sh" - -SYNC_SCRIPT="/usr/local/bin/wg-sync-policy.sh" -ENGINE_SCRIPT="/usr/local/bin/wg-policy-engine.sh" -HEALTH_INTERVAL=300 # Health check every 5 minutes -LAST_HEALTH=0 - -# === Validate prerequisites === -if ! command -v inotifywait &>/dev/null; then - log_error "inotifywait not found. Install: apt install inotify-tools" - exit 1 +# Clone repo if not present +REPO_URL_VAR="${GIT_REPO:-}" +if [ -d .git ]; then + echo "[WGRplane] Repository already present. Skipping clone." +elif [ -n "$REPO_URL_VAR" ]; then + echo "[WGRplane] Cloning repository from $REPO_URL_VAR" + git clone "$REPO_URL_VAR" . || { echo "[WGRplane] Clone failed."; exit 1; } +else + echo "[WGRplane] GIT_REPO not set. Skipping clone. You can export GIT_REPO=https://... before running." fi -if [[ ! -f "$WG_CONF" ]]; then - log_error "WireGuard config not found: $WG_CONF" - exit 1 -fi - -# === Main watcher loop === -log_info "Monitoring $WG_CONF for changes (debounce: ${DEBOUNCE_SEC}s)..." -log_info "Health check interval: ${HEALTH_INTERVAL}s" - -inotifywait -m -e close_write,move,create \ - --format '%e %f' \ - "$(dirname "$WG_CONF")" 2>/dev/null | \ -while read -r events filename; do - - # Only react to wg0.conf changes - [[ "$filename" != "$(basename "$WG_CONF")" ]] && continue - - log_info "Detected change: $events $filename" - - # Debounce: wait until no more events for DEBOUNCE_SEC - while IFS= read -r -t "$DEBOUNCE_SEC" _dummy; do - : # Drain events within debounce window - done - - log_info "Debounce complete, applying changes..." - - # Run sync - if retry "$SYNC_SCRIPT"; then - log_info "Sync successful, running engine..." - - # Run engine with retry - if retry "$ENGINE_SCRIPT"; then - log_info "Policy engine applied successfully" - else - log_error "Policy engine FAILED after retries" - fi - else - log_error "Policy sync FAILED after retries" - fi - - # Periodic health check - local now - now=$(date +%s) - if (( now - LAST_HEALTH >= HEALTH_INTERVAL )); then - LAST_HEALTH=$now - log_info "=== Periodic Health Check ===" - health_check || log_warn "Health check reported issues" - fi - -done -EOF_WG_SYNC_WATCH - - cat << 'EOF_WG_POLICY_CTL' > /usr/local/bin/wg-policy-ctl -#!/bin/bash -# wg-policy-ctl — CLI management tool for WireGuard Policy Firewall - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/wg-policy-lib.sh" - -usage() { - cat < - -Commands: - status Show full health check report - policy Display current policy.json formatted - rules Show current iptables rules in WG_POLICY chain - ipset Show ipset contents - log Tail WG_DROP logs (last 50 lines) - reload Force re-sync and re-apply policy - backup Manual backup of policy + iptables - stats Show connection and rule statistics - validate Validate policy.json without applying - help Show this help +# Create a default .env if missing +ENV_FILE=".env" +if [ ! -f "$ENV_FILE" ]; then + echo "[WGRplane] Creating default .env file at $ENV_FILE" + cat > "$ENV_FILE" <<'EOF' +# Auto-generated environment file for WGRplane +WGRPLANE_ENV=production +WG_INTERFACE=wg0 +WG_CONF_PATH=/etc/wireguard/wg0.conf EOF -} +else + echo "[WGRplane] .env already exists. Skipping creation." +fi -cmd_status() { - echo "=========================================" - echo " WireGuard Policy Firewall Status" - echo " $(date '+%Y-%m-%d %H:%M:%S')" - echo "=========================================" - echo "" - health_check -} +# Start application with docker-compose +echo "[WGRplane] Starting application with docker-compose..." +if command -v docker-compose >/dev/null 2>&1; then + sudo docker-compose up -d || { echo "[WGRplane] docker-compose up failed."; exit 1; } +elif command -v docker >/dev/null 2>&1; then + if docker compose version >/dev/null 2>&1; then + sudo docker compose up -d || { echo "[WGRplane] docker compose up failed."; exit 1; } + else + echo "[WGRplane] Docker Compose plugin not installed. Please install docker-compose or enable docker compose plugin."; exit 1 + fi +else + echo "[WGRplane] Docker is not installed or not found in PATH. Cannot start application."; exit 1 +fi -cmd_policy() { - if [[ -f "$POLICY_FILE" ]]; then - jq '.' "$POLICY_FILE" - else - log_error "policy.json not found" - exit 1 - fi -} - -cmd_rules() { - echo "=== IPv4 Chain: $CHAIN ===" - if iptables -L "$CHAIN" -n -v --line-numbers 2>/dev/null; then - echo "" - else - echo "(chain not found)" - fi - - echo "=== FORWARD references ===" - iptables -L FORWARD -n -v --line-numbers 2>/dev/null | grep -i "$CHAIN" || echo "(none)" - - if command -v ip6tables &>/dev/null; then - echo "" - echo "=== IPv6 Chain: $CHAIN ===" - ip6tables -L "$CHAIN" -n -v --line-numbers 2>/dev/null || echo "(chain not found)" - fi -} - -cmd_ipset() { - for set_name in "$IPSET_V4" "$IPSET_V6"; do - echo "=== ipset: $set_name ===" - if ipset list "$set_name" 2>/dev/null; then - echo "" - else - echo "(not found)" - echo "" - fi - done -} - -cmd_log() { - echo "=== Recent WG_DROP log entries ===" - (journalctl -k --no-pager -n 50 2>/dev/null || dmesg | tail -50) | grep "$LOG_PREFIX" || echo "(no entries)" -} - -cmd_reload() { - log_info "Force reloading policy..." - if retry /usr/local/bin/wg-sync-policy.sh; then - if retry /usr/local/bin/wg-policy-engine.sh; then - log_info "Reload complete" - else - log_error "Engine failed" - exit 1 - fi - else - log_error "Sync failed" - exit 1 - fi -} - -cmd_backup() { - backup_policy - backup_iptables - log_info "Manual backup complete. Files in: $BACKUP_DIR" -} - -cmd_stats() { - echo "=== Client Count ===" - jq '(.clients // {}) | length' "$POLICY_FILE" 2>/dev/null || echo "N/A" - - echo "" - echo "=== Clients with Access ===" - jq -r '.clients // {} | to_entries[] | select(.value.access | length > 0) | "\(.key): \(.value.access | join(", "))"' "$POLICY_FILE" 2>/dev/null || echo "N/A" - - echo "" - echo "=== Clients without Access (Internet Only) ===" - jq -r '.clients // {} | to_entries[] | select(.value.access | length == 0) | .key' "$POLICY_FILE" 2>/dev/null || echo "N/A" - - echo "" - echo "=== Active iptables rules ===" - iptables -L "$CHAIN" -n 2>/dev/null | tail -n +3 | wc -l || echo "N/A" - - echo "" - echo "=== Drop count (since boot) ===" - iptables -L "$CHAIN" -n -v 2>/dev/null | grep "DROP" | awk '{sum += $1} END {print sum+0, "packets dropped"}' - - echo "" - echo "=== ipset entries ===" - for set_name in "$IPSET_V4" "$IPSET_V6"; do - local count - count=$(ipset list "$set_name" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0) - echo " $set_name: $count entries" - done -} - -cmd_validate() { - log_info "Validating policy.json..." - - if [[ ! -f "$POLICY_FILE" ]]; then - log_error "File not found: $POLICY_FILE" - exit 1 - fi - - if ! jq empty "$POLICY_FILE" 2>/dev/null; then - log_error "Invalid JSON" - exit 1 - fi - - local errors=0 - local total=0 - - jq -r '.clients // {} | to_entries[] | "\(.key)|\(.value.access // [] | join(","))"' "$POLICY_FILE" | \ - while IFS="|" read -r ip access_str; do - (( total++ )) - - if ! validate_cidr "$ip"; then - log_error "Invalid client IP: $ip" - (( errors++ )) || true - fi - - if [[ -n "$access_str" ]]; then - IFS=',' read -ra targets <<< "$access_str" - for target in "${targets[@]}"; do - if ! validate_cidr "$target"; then - log_error "Invalid access target for $ip: $target" - (( errors++ )) || true - fi - done - fi - done - - if (( errors > 0 )); then - log_error "Validation failed: $errors errors" - exit 1 - else - log_info "Validation passed: $total clients, 0 errors" - fi -} - -# === DISPATCH === -case "${1:-help}" in - status) cmd_status ;; - policy) cmd_policy ;; - rules) cmd_rules ;; - ipset) cmd_ipset ;; - log) cmd_log ;; - reload) cmd_reload ;; - backup) cmd_backup ;; - stats) cmd_stats ;; - validate) cmd_validate ;; - help|*) usage ;; -esac -EOF_WG_POLICY_CTL - - # Write systemd files - echo "Writing systemd units to /etc/systemd/system/..." - - cat << 'EOF_WG_POLICY_SERVICE' > /etc/systemd/system/wg-policy.service -[Unit] -Description=WireGuard Dynamic Policy Firewall Watcher -After=network-online.target wg-quick@wg0.service -Wants=wg-quick@wg0.service network-online.target -StartLimitIntervalSec=60 -StartLimitBurst=5 - -[Service] -Type=simple -ExecStartPre=/usr/local/bin/wg-sync-policy.sh -ExecStart=/usr/local/bin/wg-sync-watch.sh -ExecStopPost=/usr/local/bin/wg-policy-cleanup.sh -Restart=always -RestartSec=10 -User=root -StandardOutput=journal -StandardError=journal -SyslogIdentifier=wg-policy - -# Hardening -ProtectSystem=strict -ReadWritePaths=/etc/wireguard /var/lock /tmp -ProtectHome=yes -NoNewPrivileges=no -PrivateTmp=yes - -[Install] -WantedBy=multi-user.target -EOF_WG_POLICY_SERVICE - - cat << 'EOF_WG_POLICY_HEALTH_SERVICE' > /etc/systemd/system/wg-policy-health.service -[Unit] -Description=WireGuard Policy Health Check - -[Service] -Type=oneshot -ExecStart=/bin/bash -c 'source /usr/local/bin/wg-policy-lib.sh && health_check' -StandardOutput=journal -StandardError=journal -SyslogIdentifier=wg-policy-health -EOF_WG_POLICY_HEALTH_SERVICE - - cat << 'EOF_WG_POLICY_HEALTH_TIMER' > /etc/systemd/system/wg-policy-health.timer -[Unit] -Description=WireGuard Policy Health Check Timer - -[Timer] -OnBootSec=60 -OnUnitActiveSec=300 -AccuracySec=30 - -[Install] -WantedBy=timers.target -EOF_WG_POLICY_HEALTH_TIMER - - # Make executable - chmod +x /usr/local/bin/wg-*.sh /usr/local/bin/wg-policy-ctl - - # Start services - echo "Reloading systemd daemon..." - systemctl daemon-reload - - echo "Enabling and starting services..." - systemctl enable --now wg-policy.service - systemctl enable --now wg-policy-health.timer - - echo "Installation complete!" - echo "You can check status with: wg-policy-ctl status" -} - -uninstall_policy() { - echo "Uninstalling WireGuard Policy Firewall..." - - echo "Stopping and disabling services..." - systemctl disable --now wg-policy.service wg-policy-health.timer wg-policy-health.service 2>/dev/null || true - - echo "Running cleanup script..." - if [ -x /usr/local/bin/wg-policy-cleanup.sh ]; then - /usr/local/bin/wg-policy-cleanup.sh || true - fi - - echo "Removing systemd units..." - 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.timer - systemctl daemon-reload - - echo "Removing scripts from /usr/local/bin/..." - rm -f /usr/local/bin/wg-policy-lib.sh - rm -f /usr/local/bin/wg-sync-policy.sh - rm -f /usr/local/bin/wg-policy-engine.sh - rm -f /usr/local/bin/wg-policy-cleanup.sh - rm -f /usr/local/bin/wg-sync-watch.sh - rm -f /usr/local/bin/wg-policy-ctl - - echo "Uninstallation complete!" -} - -case "${1:-}" in - install) - install_policy - ;; - uninstall) - uninstall_policy - ;; - *) - echo "Usage: $0 {install|uninstall}" - exit 1 - ;; -esac +echo "[WGRplane] Install script completed. Check with: docker ps -a; docker-compose ps; wg-policy-ctl status (depending on your setup)." diff --git a/scripts/ci_ws_verify.sh b/scripts/ci_ws_verify.sh new file mode 100644 index 0000000..a1c5f25 --- /dev/null +++ b/scripts/ci_ws_verify.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "[CI] Verifying Gorilla dependencies and WebSocket endpoint (WS) in Go-enabled environment" + +cd app + +echo ">> Checking Go toolchain..." +if ! command -v go >/dev/null 2>&1; then + echo "Go is not installed in this environment. Exiting."; + exit 0 +fi + +echo ">> Fetching dependencies..." +go get -u github.com/gorilla/mux +go get -u github.com/gorilla/websocket + +echo ">> Tidying modules..." +go mod tidy + +echo ">> Building project..." +go build ./... + +echo ">> Running server in background..." +go run main.go & +PID=$! +echo "[CI] Server PID: $PID" +sleep 2 + +echo ">> Testing WebSocket endpoint (ws://localhost:8080/ws/stats) using simple client..." +if command -v websocat >/dev/null 2>&1; then + websocat -b ws://localhost:8080/ws/stats >/tmp/ws_test.txt & + WS_PID=$! + sleep 6 + kill $WS_PID 2>/dev/null || true + echo "Captured output:"; head -n 5 /tmp/ws_test.txt || true +else + echo "websocat not installed. Install to run WS test or use another client." +fi + +echo ">> Cleaning up server process..." +kill $PID 2>/dev/null || true +wait $PID 2>/dev/null || true + +echo "[CI] Verification script completed." diff --git a/scripts/verif_end2end.sh b/scripts/verif_end2end.sh new file mode 100644 index 0000000..4bb1abe --- /dev/null +++ b/scripts/verif_end2end.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +########################################### +# End-to-end verification for /api/peers/* +# Requires: curl, jq, xxd, file (coreutils) +########################################### + +API_KEY="${WG_API_KEY:-test-api-key}" +BASE_URL="${WG_BASE_URL:-http://127.0.0.1:8080}" + +echo "[VERIF-EE] Starting end-to-end verification against ${BASE_URL}" + +echo "[STEP] Seed server" +SERVER_PAYLOAD='{"name":"test-server","mode":"forward","PublicKey":"SERVER_PUBLIC_KEY_BASE64","Endpoint":"server.example:51820"}' +SERVER_ID=$(curl -s -H "wg-rplane-datadunia: ${API_KEY}" -H "Content-Type: application/json" -d "$SERVER_PAYLOAD" "$BASE_URL/api/servers" | jq -r '.id') +echo "[INFO] SERVER_ID=${SERVER_ID}" + +echo "[STEP] Seed peer" +PEER_PAYLOAD='{"PublicKey":"PEER_PUBLIC_KEY_BASE64","IP":"10.0.0.2","AllowAccess":"[]","AllowInternet":false}' +PEER_ID=$(curl -s -H "wg-rplane-datadunia: ${API_KEY}" -H "Content-Type: application/json" -d "$PEER_PAYLOAD" "$BASE_URL/api/servers/${SERVER_ID}/peers" | jq -r '.id') +echo "[INFO] PEER_ID=${PEER_ID}" + +echo "[STEP] Fetch config" +CONFIG_JSON=$(curl -s -w "%{http_code}" -H "wg-rplane-datadunia: ${API_KEY}" "$BASE_URL/api/peers/${PEER_ID}/config") +STATUS=$(tail -c 3 <<< "$CONFIG_JSON"); +CONFIG_BODY=$(echo "$CONFIG_JSON" | sed '$d') +if [ "$STATUS" != "200" ]; then + echo "[ERROR] Config endpoint returned status code ${STATUS}"; exit 1 +fi +echo "$CONFIG_BODY" > /tmp/peer_config.conf + +echo "[STEP] Validate config content" +grep -q "^\\[Interface\\]" /tmp/peer_config.conf || { echo "Config missing [Interface]"; exit 1; } +grep -q "^\\[Peer\\]" /tmp/peer_config.conf || { echo "Config missing [Peer]"; exit 1; } + +echo "[STEP] Fetch QR code" +curl -s -o /tmp/peer.png -D /tmp/peer_headers.txt -H "wg-rplane-datadunia: ${API_KEY}" "$BASE_URL/api/peers/${PEER_ID}/qrcode" || { echo "Failed to fetch QR code"; exit 1; } +echo "[INFO] QR code saved to /tmp/peer.png" + +echo "[STEP] Validate PNG file" +if file /tmp/peer.png | grep -qi png; then + echo "[OK] PNG detected" +else + echo "[ERROR] Not a PNG file" + exit 1 +fi + +echo "[VERIF-EE] End-to-end verification completed successfully." diff --git a/wgrplane.service b/wgrplane.service new file mode 100644 index 0000000..c5dde57 --- /dev/null +++ b/wgrplane.service @@ -0,0 +1,14 @@ +[Unit] +Description=WGRPlane Go Gateway Service +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +Environment=WG_RPLANE_MODE=forward +ExecStart=/usr/local/bin/wgrplane +Restart=always +RestartSec=5s + +[Install] +WantedBy=multi-user.target