# 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 --type [--subtype ] [--rule ] [--group ] wgctl remove --name [--force] wgctl rename --name --new-name wgctl list [--type phone] [--rule user] [--group family] [--online] [--blocked] [--restricted] wgctl inspect --name [--config] [--qr] wgctl config --name wgctl qr --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` ---