# 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`

---
