- conntrack/event.go: TrafficEvent type - conntrack/filter.go: WG subnet filter, IsExternal, ProtoName - conntrack/subscriber.go: netlink conntrack DESTROY subscriber - writer/log.go: JSON line writer with mutex - resolver/peers.go: WG IP → peer name from conf files + endpoint index - resolver/services.go: IP:port → service name from services.json - config/config.go: reads wgctl.json, sensible defaults - cmd/root.go: CLI flags - main.go: wires everything together - DESTROY events only: full byte/packet counts per connection - filters to WireGuard subnet, marks external traffic |
||
|---|---|---|
| commands | ||
| core | ||
| daemon | ||
| install | ||
| modules | ||
| core.sh | ||
| README.md | ||
| wgctl | ||
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
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
# 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
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
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.
# 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:
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)
wgctl block --name phone-nuno # full block
wgctl unblock --name phone-nuno # restore (overrides group blocks)
Specific restrictions (peer stays in WireGuard)
# 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.
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
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
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
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.
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
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
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:
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/unapplyblock.module.sh— block state management, apply/restorefw.module.sh— iptables wrappers (idempotent, mode-aware)net.module.sh— service registry lookups and annotationspeers.module.sh— peer queries, status, formattingmonitor.module.sh— endpoint cache, watchlist
Test Suite
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