add README
This commit is contained in:
parent
f32ca5c0a1
commit
87f6c770ef
3 changed files with 471 additions and 2 deletions
465
README.md
Normal file
465
README.md
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
# 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`
|
||||
|
||||
---
|
||||
|
|
@ -178,6 +178,7 @@ function cmd::add::run() {
|
|||
log::wg_add "Type: ${type}"
|
||||
log::wg_add "IP: ${ip}"
|
||||
log::wg_add "Tunnel: ${tunnel} (${allowed_ips})"
|
||||
log::wg_add "Endpoint: $(config::endpoint)"
|
||||
log::wg_add "Rule: ${rule:-none}"
|
||||
|
||||
keys::generate_pair "$full_name" || return 1
|
||||
|
|
|
|||
|
|
@ -160,10 +160,11 @@ function cmd::inspect::_blocks_info() {
|
|||
function cmd::inspect::_group_info() {
|
||||
local name="$1"
|
||||
|
||||
ui::section "Groups"
|
||||
|
||||
local groups=()
|
||||
mapfile -t groups < <(json::peer_groups "$(ctx::groups)" "$name")
|
||||
[[ ${#groups[@]} -eq 0 || -z "${groups[0]:-}" ]] && return 0
|
||||
|
||||
ui::section "Groups"
|
||||
|
||||
if [[ ${#groups[@]} -eq 0 ]] || [[ -z "${groups[0]:-}" ]]; then
|
||||
printf " —\n"
|
||||
|
|
@ -195,6 +196,8 @@ function cmd::inspect::_firewall_info() {
|
|||
rules_output+=("$line")
|
||||
done < <(fw::forward_rules_for_ip "$ip" | grep -v NFLOG)
|
||||
|
||||
[[ ${#rules_output[@]} -eq 0 || -z "${rules_output[0]:-}" ]] && return 0
|
||||
|
||||
# printf "\n \033[0;37m── Firewall (\033[0;32m+%d\033[0m \033[0;31m-%d\033[0m) \033[0m%s\n" \
|
||||
# "$accepts" "$drops" "$(printf '─%.0s' {1..28})"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue