wgctl/README.md
Nuno Duque Nunes 87f6c770ef add README
2026-05-16 21:41:38 +00:00

465 lines
14 KiB
Markdown

# wgctl
> WireGuard management CLI for Linux — peer lifecycle, RBAC-style access control, live monitoring, and firewall enforcement.
wgctl started as a simple peer manager and evolved into a complete WireGuard operations tool. It handles everything from adding a guest device to enforcing fine-grained network access policies across groups of peers, with full persistence across restarts.
---
## Features
- **Peer lifecycle** — add, remove, rename, list, inspect
- **Rule system** — iptables-backed access control with inheritance
- **Network services registry** — name your LAN services, use them everywhere
- **Per-peer and group blocks** — restrict access to specific IPs, ports, subnets, or named services
- **Group management** — organize peers, bulk block/unblock with M:N tracking
- **Live monitoring** — unified watch showing WireGuard handshakes and firewall drops
- **Activity metrics** — per-peer transfer totals and current rates
- **Log management** — structured event logs with dedup, filtering, rotation
- **Audit** — verify iptables state matches expected configuration
- **Interactive shell** — REPL with tab completion and history
- **Full persistence** — all state restored on WireGuard restart
---
## Installation
```bash
git clone https://git.lan.krilio.net/nuno/wgctl
cd wgctl/install
bash install.sh
```
Requires: `bash`, `wireguard-tools`, `python3`, `iptables`, `ulogd2`, `qrencode`
---
## Quick Start
```bash
# Add a peer
wgctl add --name nuno --type phone
# Add a guest with a rule and group
wgctl add --name visitor --type guest --subtype phone --rule guest --group family
# List all peers
wgctl list
# Show detailed info
wgctl inspect --name phone-nuno
# Show QR code for mobile setup
wgctl qr --name phone-nuno
```
---
## Peer Management
```bash
wgctl add --name <name> --type <type> [--subtype <subtype>] [--rule <rule>] [--group <group>]
wgctl remove --name <name> [--force]
wgctl rename --name <name> --new-name <new>
wgctl list [--type phone] [--rule user] [--group family] [--online] [--blocked] [--restricted]
wgctl inspect --name <name> [--config] [--qr]
wgctl config --name <name>
wgctl qr --name <name>
```
**Device types:** `desktop`, `laptop`, `phone`, `tablet`, `guest`, `guest-desktop`, `guest-laptop`, `guest-phone`, `guest-tablet`
Each type maps to a dedicated subnet (`phone``10.1.3.0/24`, `guest``10.1.100.0/24`, etc.), keeping device families organized.
**List status values:**
| Status | Meaning |
|--------|---------|
| `online` | Connected (recent handshake) |
| `offline` | Not connected but in WireGuard server |
| `blocked` | Removed from WireGuard — no tunnel possible |
| `restricted` | In WireGuard but with specific access blocks applied |
---
## Rule System
Rules define what a peer can and cannot access. They are enforced via iptables and restored automatically on restart.
### Rule inheritance
Rules can extend base rules, composing access policies from primitives:
```
Base rules (building blocks):
no-nginx blocks 10.0.0.101
no-proxmox extends no-nginx, blocks 10.0.0.100
no-truenas extends no-nginx, blocks 10.0.0.200
no-admin extends no-proxmox, no-truenas
restricted-dns allows DNS:53 to Pi-hole, blocks Pi-hole otherwise
no-lan blocks 10.0.0.0/24
Assignable rules:
guest extends no-lan, restricted-dns
user extends no-admin, restricted-dns
admin no restrictions
moonlight-02 extends no-lan, allows 10.0.0.244/32
```
Adding a new admin service only requires updating one base rule — all inheriting rules get the restriction automatically.
### Rule commands
```bash
wgctl rule list # list all rules
wgctl rule list --tree # show inheritance tree inline
wgctl rule list --group "VM Rules" # filter by group
wgctl rule list --base # show only base rules
wgctl rule show --name user # show rule with inheritance
wgctl rule show --name moonlight-02 --resolved # show merged/resolved entries
# Create rules
wgctl rule add --name no-npm --base --block-ip 10.0.0.101/32
wgctl rule add --name restricted-dns \
--allow-service pihole:dns \
--block-service pihole
wgctl rule add --name dev-01 \
--desc "Dev VM access" \
--group "VM Rules" \
--extends no-lan \
--allow-ip 10.0.0.50/32
# Update rules
wgctl rule update --name user --add-extends no-nginx
wgctl rule update --name dev-01 --allow-ip 10.0.0.51/32
# Assign to peers
wgctl rule assign --name user --peer phone-nuno
wgctl rule unassign --peer phone-nuno
wgctl rule reapply --name user # re-apply to all assigned peers
wgctl rule reapply --all # re-apply all rules
```
### Rule display (wgctl rule show)
```
Rule: user
────────────────────────────────────────────────
Description: Standard user
Group: Users
DNS: true (inherited)
── Extends ──────────────────────────────────
↳ no-proxmox
- 10.0.0.101/32 → npm
- 10.0.0.100/32 → proxmox
↳ dns-restricted
+ 10.0.0.103:53:udp → pihole:dns-udp
+ 10.0.0.103:53:tcp → pihole:dns-tcp
- 10.0.0.103/32 → pihole
↺ DNS → 10.0.0.103 → pihole
── Peers ──────────────────────────────────
Assigned: 11
phone-nuno 10.1.3.1
...
```
---
## Network Services Registry
Map your LAN services to names for use in rules and blocks. IPs/ports are resolved at creation time — rules remain independent of the registry.
```bash
# Add services
wgctl net add --name proxmox --ip 10.0.0.100 --desc "Proxmox VE" --tag admin
wgctl net add --name proxmox:web-ui --port 8006:tcp
wgctl net add --name proxmox:ssh --port 22:tcp
wgctl net add --name pihole --ip 10.0.0.103 --desc "Pi-hole + Unbound" --tag dns
wgctl net add --name pihole:dns-udp --port 53:udp
wgctl net add --name pihole:dns-tcp --port 53:tcp
# List
wgctl net list
wgctl net list --detailed
wgctl net list --tag admin
wgctl net show --name proxmox
# Remove
wgctl net rm --name proxmox:web-ui # remove specific port
wgctl net rm --name proxmox:ports # remove all ports
wgctl net rm --name proxmox # remove service entirely
```
Services are used with `--service` in `block`/`unblock` and `--block-service`/`--allow-service` in `rule add`:
```bash
wgctl block --name phone-nuno --service proxmox
wgctl block --name phone-nuno --service proxmox:web-ui
wgctl rule add --name no-admin --block-service proxmox --block-service truenas
```
Service names appear as annotations in `inspect` and `rule show`:
```
- 10.0.0.100:8006:tcp → proxmox:web-ui
- 10.0.0.200 → truenas
```
---
## Blocking and Restrictions
### Full block (removes peer from WireGuard)
```bash
wgctl block --name phone-nuno # full block
wgctl unblock --name phone-nuno # restore (overrides group blocks)
```
### Specific restrictions (peer stays in WireGuard)
```bash
# Block by IP, subnet, port, or named service
wgctl block --name phone-nuno --ip 10.0.0.210
wgctl block --name phone-nuno --subnet 10.0.0.0/24
wgctl block --name phone-nuno --port 10.0.0.100:8006:tcp
wgctl block --name phone-nuno --service proxmox
wgctl block --name phone-nuno --service truenas:web-ui --block-name "no truenas ui"
# Unblock specific rules
wgctl unblock --name phone-nuno --ip 10.0.0.210
wgctl unblock --name phone-nuno --service proxmox
wgctl unblock --name phone-nuno --service truenas:web-ui
```
All block rules are persisted in JSON and restored on restart.
### Block state in inspect
```
── Peer Blocks ──────────────────────────────────
🚫 blocked by groups: family
- 10.0.0.210 → docker
- 10.0.0.100:8006:tcp → proxmox:web-ui no truenas ui
```
---
## Groups
Peers can belong to multiple groups (M:N). Group block/unblock tracks which groups have blocked a peer — unblocking one group won't restore a peer still blocked by another.
```bash
wgctl group list
wgctl group add --name family --desc "Family devices"
wgctl group show --name family
# Peer membership
wgctl group peer add --name family --peer phone-nuno
wgctl group peer remove --name family --peer phone-nuno
# Bulk operations
wgctl group block --name family # block all peers
wgctl group unblock --name family # unblock (respects M:N)
wgctl group rule assign --name family --rule user
# Monitoring per group
wgctl group watch --name family
wgctl group logs --name family --limit 20
wgctl group audit --name family
```
---
## Monitoring
### Live monitor
```bash
wgctl watch # all peers — handshakes + fw drops
wgctl watch --name phone-nuno # single peer
wgctl watch --type phone # by device type
wgctl watch --blocked # only blocked peer attempts
wgctl watch --allowed # only handshakes
```
Output shows source (`wg` = WireGuard, `fw` = firewall drop), client, destination/endpoint, event, and status.
### Logs
```bash
wgctl logs # WireGuard events + firewall drops
wgctl logs --name phone-nuno # filter by peer
wgctl logs --fw # firewall drops only
wgctl logs --follow # live tail
wgctl logs remove --name phone-nuno # remove entries for peer
wgctl logs remove --before 7 # remove entries older than 7 days
wgctl logs rotate # rotate logs (default: keep 7 days)
wgctl logs rotate --days 30
```
Duplicate events are collapsed with counts: `attempt (x88)`.
---
## Firewall Inspection
```bash
wgctl fw # show FORWARD chain rules
wgctl fw list --peer phone-nuno # filter by peer
wgctl fw list --rule user # all peers with this rule
wgctl fw list --no-nflog # hide logging rules
wgctl fw nat # show NAT/DNS redirect rules
wgctl fw count # rule counts by type
```
---
## Audit
Verify that iptables state matches expected configuration. Includes inherited rules and peer-specific blocks in expected counts.
```bash
wgctl audit # audit all peers
wgctl audit --peer phone-nuno # single peer
wgctl audit --type guest # by device type
wgctl audit --fix # auto-repair missing rules
```
Output:
```
✅ phone-nuno rule=admin fw: 0/0
✅ phone-helena rule=user fw: 14/14
✅ guest-zephyr rule=moonlight fw: 5/5
✅ phone-test rule=user fw: 16/16 (includes 2 peer-specific blocks)
```
---
## Service Management
```bash
wgctl service start
wgctl service stop
wgctl service restart # flushes and restores all fw rules
wgctl service status
wgctl service enable
wgctl service disable
```
On restart, wgctl restores:
- All rule iptables rules for all peers
- All block state (full blocks, group blocks, peer-specific restrictions)
- DNS redirect NAT rules
---
## Interactive Shell
```bash
wgctl shell
```
Full REPL with tab completion, command history (`~/.wgctl_history`), and all wgctl commands available without the `wgctl` prefix. Bash commands also work.
---
## Configuration
Override defaults in `/etc/wireguard/.wgctl/wgctl.conf`:
```bash
WG_INTERFACE=wg0
WG_ENDPOINT=wg.example.com:51820
WG_DNS=10.0.0.103
WG_LISTEN_PORT=51820
WG_SUBNET=10.1.0.0/16
WG_LAN=10.0.0.0/24
DATE_FORMAT=eu # iso | eu | eu-dash
HANDSHAKE_CHECK_TIME_SEC=300
ACTIVITY_TOTAL_LOW=1000000
ACTIVITY_TOTAL_MED=10000000
ACTIVITY_TOTAL_HIGH=100000000
ACTIVITY_CURRENT_LOW=10000
ACTIVITY_CURRENT_MED=100000
ACTIVITY_CURRENT_HIGH=1000000
```
---
## Data Layout
```
/etc/wireguard/
├── wg0.conf # WireGuard server config
├── clients/ # per-peer client configs + keys
│ ├── phone-nuno.conf
│ ├── phone-nuno_public.key
│ └── phone-nuno_private.key
└── .wgctl/
├── wgctl.conf # config overrides
├── rules/ # assignable rule files
│ ├── user.rule
│ ├── guest.rule
│ └── base/ # base rules (not directly assignable)
│ ├── no-nginx.rule
│ └── no-proxmox.rule
├── groups/ # group definitions
│ └── family.group
├── blocks/ # per-peer block state (JSON)
│ └── phone-test.block
├── meta/ # per-peer metadata (rule, subtype)
│ └── phone-nuno.meta
├── services.json # network services registry
└── daemon/
├── events.log # WireGuard connection events (JSONL)
├── fw_events.log # firewall drop events (JSONL, via ulogd2)
├── watchlist.json # blocked peer IPs for scapy monitor
├── endpoint_cache.json # cached real endpoints
├── transfer_cache.json # activity delta cache
└── wgctl-monitor.py # scapy packet capture daemon
```
---
## Architecture
wgctl follows a layered architecture:
```
commands/ — user-facing commands (add, block, rule, group...)
modules/ — shared business logic (peers, rules, blocks, fw, net...)
core/ — bootstrap, utilities, JSON helper, formatting
install/ — systemd units, logrotate, example config
```
Commands orchestrate. Modules enforce. Python handles all JSON state. Bash handles orchestration and display.
Key modules:
- `rule.module.sh` — rule resolution with inheritance, apply/unapply
- `block.module.sh` — block state management, apply/restore
- `fw.module.sh` — iptables wrappers (idempotent, mode-aware)
- `net.module.sh` — service registry lookups and annotations
- `peers.module.sh` — peer queries, status, formatting
- `monitor.module.sh` — endpoint cache, watchlist
---
## Test Suite
```bash
wgctl test # non-destructive tests (64 tests)
wgctl test --destructive # includes add/remove/block operations
wgctl test --section rules # run specific section
wgctl test --section net
wgctl test --section destructive
```
Sections: `list`, `inspect`, `config`, `rules`, `groups`, `audit`, `logs`, `fw`, `net`, `destructive`
---