WGCTL - WireGuard Control
Find a file
Nuno Duque Nunes 7120199004 feat: logs --resolved flag, logs clean, performance improvements
- logs --resolved: show only resolved names, hide raw IPs
- logs clean: remove keepalive handshakes via json::clean_handshakes
- batch_resolve: single Python call for all endpoint resolutions
- fw_row/wg_row: native bash padding replaces ui::pad_mb (5x speedup)
- fw_row/wg_row: correct arrow byte counting (→ = 3 bytes, 1 visible)
- help: updated with new subcommands and flags
- on_load: --resolved, --ascending, --descending registered
2026-05-26 04:34:39 +00:00
commands feat: logs --resolved flag, logs clean, performance improvements 2026-05-26 04:34:39 +00:00
core feat: logs --resolved flag, logs clean, performance improvements 2026-05-26 04:34:39 +00:00
daemon feat: logs descending sort, gap/offline indicator, endpoint resolution 2026-05-26 01:34:48 +00:00
install feat: date format config, batch optimizations, list refactor, fw:: rename, .wgctl data dir 2026-05-11 22:27:33 +00:00
modules feat: logs --resolved flag, logs clean, performance improvements 2026-05-26 04:34:39 +00:00
core.sh init feature 2026-05-19 15:26:31 +00:00
README.md add README 2026-05-16 21:41:38 +00:00
wgctl load module policy 2026-05-23 23:30:39 +00:00

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 (phone10.1.3.0/24, guest10.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/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

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