From 87f6c770ef89c593670ae71305c875fdc6c69ee6 Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Sat, 16 May 2026 21:41:38 +0000 Subject: [PATCH] add README --- README.md | 465 ++++++++++++++++++++++++++++++++++++ commands/add.command.sh | 1 + commands/inspect.command.sh | 7 +- 3 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..237b08f --- /dev/null +++ b/README.md @@ -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 --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` + +--- diff --git a/commands/add.command.sh b/commands/add.command.sh index e279d04..e390ad6 100644 --- a/commands/add.command.sh +++ b/commands/add.command.sh @@ -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 diff --git a/commands/inspect.command.sh b/commands/inspect.command.sh index 9690dca..60bf17e 100644 --- a/commands/inspect.command.sh +++ b/commands/inspect.command.sh @@ -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})"