Metadata-Version: 2.4
Name: wgusr
Version: 1.0.0
Summary: WireGuard user management tool — add, delete, list, view, clear VPN peers
Author: wgusr contributors
License-Expression: MIT
Project-URL: Homepage, https://github.com/olk/wgusr
Project-URL: Repository, https://github.com/olk/wgusr
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Networking
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: wgconfig>=1.2.0
Requires-Dist: wireguard-tools>=0.6.0
Requires-Dist: pynacl>=1.5.0
Requires-Dist: qrcode>=7.4.2
Requires-Dist: pypng>=0.0.21
Requires-Dist: click>=8.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: ruff>=0.7.0; extra == "dev"
Requires-Dist: mypy>=1.11.0; extra == "dev"
Dynamic: license-file

# wgusr

[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

WireGuard peer management — add, delete, list, view, and sync VPN users on a `wg-quick` server.

`wgusr` edits the `[Peer]` section of your `wg0.conf` directly: it generates keys and preshared keys, allocates VPN IPs from the existing pool, writes per-user client configs and QR codes, and hot-applies changes to the running interface — all under a file lock so concurrent invocations are safe.

---

## Why wgusr?

Managing `wg0.conf` by hand means copy-pasting base64 keys, tracking IPs in a spreadsheet, and regenerating QR codes for every new user. `wgusr` automates that plumbing:

- **Atomic, locked edits** — `fcntl.flock` + temp-file-rename so a crash mid-write never corrupts your config.
- **Automatic IP allocation** — derives free IPs from existing `AllowedIPs`; no sidecar pool file.
- **Key generation in pure Python** — no `wg genkey` subprocess needed.
- **Client artifacts** — ready-to-scan WireGuard app configs (ASCII QR + PNG QR) written to `<wg-dir>/clients/`.
- **Hot sync** — apply a single peer or all peers to the live interface without restarting `wg-quick`.

## What wgusr does NOT do

- Does not install or load the `wireguard` kernel module.
- Does not bring the interface up (`wg-quick up wg0` is your responsibility).
- Does not create the `wg-quick@wg0.service` systemd unit (it ships with `wireguard-tools`).
- Does not configure firewall rules beyond the `nft` `PostUp`/`PostDown` hooks it emits during `init`.
- Does not provide a web UI.

`wgusr` edits the config file; `wg-quick` and the kernel apply it.

---

## Prerequisites

- **Python ≥ 3.11**
- **Root** — required to read/write `/etc/wireguard`.
- **`wg` and `wg-quick`** from `wireguard-tools` — needed for `init` and `sync` (key generation itself is pure-Python and does not require them).
- **Loaded `wireguard` kernel module:**
  ```sh
  modprobe wireguard
  ```
- **`net.ipv4.ip_forward=1`** — required for the server to route VPN traffic:
  ```sh
  echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/99-wireguard.conf
  sysctl --system
  ```
- **`nft` table with `WG_IFACES` sets** — `wgusr init` emits `nft add element … WG_IFACES { wg0 }` in `PostUp`. The sets must exist before `wg-quick up` runs, otherwise the interface fails to start. Create them in e.g. `/etc/nftables.d/wireguard.nft`:
  ```
  table ip filter {
      set WG_IFACES { type ifname; }
  }
  table ip nat {
      set WG_IFACES { type ifname; }
  }
  ```
  Apply with: `nft -f /etc/nftables.d/wireguard.nft`.
- **Systemd unit** — `systemctl enable --now wg-quick@wg0` (or bring the interface up manually).

---

## Installation

### Development

```sh
make install-dev    # editable install, includes ruff + mypy
```

### Release

```sh
make build          # builds dist/wgusr-*.whl and *.tar.gz
make install        # pip install dist/wgusr-*-py3-none-any.whl
```

Or directly:

```sh
pip install .
```

Verify:

```sh
wgusr --version
wgusr --help
```

---

## Quick start

### 1. Initialise the interface

```sh
sudo wgusr init
```

`wgusr init` is interactive. It will ask for:

| Prompt | Default | Description |
|---|---|---|
| Listen port | `51820` | UDP port for the server |
| Server public address | *(required)* | Your server's public hostname or IP, e.g. `vpn.example.com` |
| VPN network CIDR | `10.0.73.0/24` | Tunnel network; server takes `.1` |
| Use existing private/public key? | `n` | Generate new keys, paste existing, or read from a file |

`init` creates `/etc/wireguard/wg0.conf` with `Address`, `ListenPort`, `PrivateKey`, `SaveConfig = true`, and `PostUp`/`PostDown` nft hooks. It refuses to overwrite an existing config.

Bring the interface up:

```sh
sudo wg-quick up wg0
sudo systemctl enable --now wg-quick@wg0   # optional: start on boot
```

Print the server's public key (needed by clients):

```sh
sudo wg show wg0 public-key
```

### 2. Add a user

```sh
sudo wgusr add alice vpn.example.com
```

Output:

```
user 'alice' added with IP 10.0.73.2

Client config: /etc/wireguard/clients/alice-wg0.conf

QR code (scan with WireGuard app):
███████ ...
```

Distribute the QR code or the `.conf` file to the client. The client's `[Interface] PrivateKey` and the server's `[Peer] PublicKey` are stored in `/etc/wireguard/clients/`.

### 3. Sync the peer to the live interface

Changes to `wg0.conf` are not applied automatically — `wg-quick` only reads the config at startup. Use `sync` to push a peer live without restarting anything:

```sh
# sync a single user
sudo wgusr sync alice

# sync all peers at once (wg syncconf)
sudo wgusr sync --all
```

---

## Usage workflow

```sh
# Create a new interface (one-time)
sudo wgusr init

# Add a peer
sudo wgusr add alice vpn.example.com               # auto IP
sudo wgusr add bob  vpn.example.com --ip 10.0.73.50 # fixed IP
sudo wgusr add carol vpn.example.com --select-ip   # interactive

# Push to the live interface
sudo wgusr sync alice          # one peer
sudo wgusr sync --all         # all peers at once

# Inspect
sudo wgusr list                # table: USER, IP, PUBKEY
sudo wgusr view alice          # client config + QR

# Remove a peer
sudo wgusr delete alice        # by name
sudo wgusr delete --key <pubkey>  # by public key (escape hatch)
sudo wgusr clear -y            # wipe ALL peers (confirmation required)

# Short-hand
wgusr -v alice                 # equivalent to: wgusr view alice
```

### `wgusr add` flags

| Flag | Description |
|---|---|
| `--ip 10.0.73.x` | Assign a specific VPN IP |
| `--select-ip` | Interactively choose from available IPs |
| `--dns 1.1.1.1` | DNS server for the client (default: server tunnel IP) |
| `--keepalive 25` | `PersistentKeepalive` seconds (0 = off) |
| `--no-psk` | Skip preshared key (not recommended) |
| `--existing-keys` | Use a provided keypair instead of generating |
| `--private-key` | Private key (requires `--existing-keys`) |
| `--public-key` | Public key (requires `--existing-keys`) |

---

## Using wgusr on an existing server

`wgusr init` refuses to overwrite an existing `wg0.conf`. For servers you already have running, `wgusr` works immediately — it only reads the `[Interface]` section to learn the address, port, and private key:

```sh
sudo wgusr list                    # shows existing peers
sudo wgusr add dave vpn.example.com  # appends a new peer
sudo wgusr sync dave               # push dave live
```

Peers added by hand (without `wgusr`) may lack the `# {user}` leading comment. `wgusr` reconciles these automatically from `clients/*.pub` files on every command run.

---

## Example output

### `wgusr list`

```
USER                  IP                 PUBKEY
-------------------------------------------------------------------------------------
alice                 10.0.73.2/32       XElbIX...qFE=                1 user(s)
```

### `wgusr add alice`

```
user 'alice' added with IP 10.0.73.2

Client config: /etc/wireguard/clients/alice-wg0.conf

QR code (scan with WireGuard app):
███████
█ ▄▄▄█
█ █▄█ █
...
```

### `clients/` directory

```
/etc/wireguard/clients/
├── alice-wg0.conf    # WireGuard app config (full tunnel)
├── alice-wg0.png    # QR code PNG
├── alice.pri        # client's private key
├── alice.pub        # client's public key
└── alice.psk        # preshared key
```

---

## Command reference

### Global options

| Flag | Default | Description |
|---|---|---|
| `--wg-dir DIR` | `/etc/wireguard` | WireGuard config directory |
| `--interface`, `-i` | `wg0` | Interface name |
| `--clients-dir DIR` | `<wg-dir>/clients` | Where per-user artifacts are stored |

### `wgusr init`

Interactively create a new `wg0.conf`. One-time setup; refuses to overwrite an existing file.

### `wgusr add <user> <endpoint-host>`

Add a peer to `wg0.conf`, generate keys, allocate an IP, and write client artifacts.

```
wgusr add alice vpn.example.com
wgusr -i wg0 add bob 192.168.1.100 --ip 10.0.73.50
```

### `wgusr delete <user>`

Remove a peer and its client artifacts. Cross-checks the `# {user}` comment against `clients/{user}.pub` before deleting.

```
wgusr delete alice
wgusr delete --key XElbIX...qFE=    # direct public-key delete
```

### `wgusr list`

List all peers with their VPN IPs and public keys.

```
sudo wgusr list
```

### `wgusr view <user>`

Print a user's client config and QR code to stdout.

```
sudo wgusr view alice
```

### `wgusr sync <user>` / `wgusr sync --all`

Apply peer(s) to the live interface via `wg set` (single user) or `wg syncconf` (all).

```
sudo wgusr sync alice          # one peer live immediately
sudo wgusr sync --all         # full wg syncconf
```

### `wgusr clear`

Remove **all** peers and all client artifacts. Requires `--yes` or confirmation prompt.

```
sudo wgusr clear -y
```

---

## Reloading the live interface

`wg-quick` reads `wg0.conf` only at startup. To apply config changes to a running interface:

- **Single peer:** `wgusr sync <user>` — `wg set` updates just that peer, no disruption to others.
- **All peers:** `wgusr sync --all` — `wg syncconf` does a full atomic reconcile.

---

## How it works

`wgusr` reads and writes `wg0.conf` using the `wgconfig` library. Every mutation:

1. Acquires an advisory `fcntl.flock` on `<iface>.lock` (shared for reads, exclusive for writes).
2. Reconciles `# {user}` leading comments from `clients/*.pub` files.
3. Writes to a temp file in the same directory (mode 0o600).
4. Atomically replaces the target via `os.replace` (guaranteed atomic on POSIX).

IP addresses are derived from existing peer `AllowedIPs` — no sidecar pool file is needed. `wgusr sync` shells out to `wg set` or `wg syncconf` to push changes into the kernel; it does not restart `wg-quick`.

---

## Backup & recovery

`/etc/wireguard/clients/` is the source of truth for all private keys, preshared keys, and client configs. To back up or migrate:

```sh
tar -czf wg-backup.tar.gz /etc/wireguard/wg0.conf /etc/wireguard/clients/
```

**Security:** `.pri` and `.psk` files are plaintext (mode 0o600). Store backups encrypted at rest and never sync them to untrusted storage.

To restore: extract the archive and run `wgusr sync --all` to re-apply all peers.

---

## Troubleshooting

| Symptom | Fix |
|---|---|
| `error: 'wg' binary not found` | `apt install wireguard-tools` (or your distro's package) |
| `wg-quick up wg0` fails on `nft add element` | Define the `WG_IFACES` sets before starting the interface (see Prerequisites) |
| VPN clients can't reach the internet | `sysctl net.ipv4.ip_forward=1` and add a masquerade rule to your nft/iptables |
| `wgusr list` shows `(no name)` | Peer lacks a `# {user}` comment; auto-reconciled on the next `wgusr` command |
| Permission denied in `clients/` | Run `wgusr` as root |
| `wg-quick@wg0` is running stale config | `sudo wgusr sync --all` to push current conf, or `systemctl restart wg-quick@wg0` |

---

## Multiple interfaces

Manage multiple WireGuard interfaces by passing `--interface`:

```sh
sudo wgusr -i wg1 add dave vpn.example.com
sudo wgusr -i wg1 sync dave
```

Each interface has its own `wg{n}.conf` and its own `PostUp`/`PostDown` hooks. The `PostDown` hook removes **only** that interface from the shared `WG_IFACES` sets (bug fixed in v0.1.1; older versions flushed the entire set).

---

## Uninstall

```sh
pip uninstall wgusr
```

This removes the CLI. Left behind:

- `/etc/wireguard/wg0.conf` — your server config
- `/etc/wireguard/clients/` — all private keys and client configs
- The running `wg-quick@wg0` service and the nft ruleset

Clean these up manually if no longer needed.

---

## Security notes

- All file writes run under `umask 0o077`; artifacts are mode 0o600.
- Private keys are created with exclusive-create (`O_CREAT | O_EXCL`) — `wgusr` refuses to overwrite an existing key.
- All user inputs are validated before use: username, interface name, IP address, public key, and keepalive are checked against strict regexes and ranges.
- Path-traversal guards prevent `wg-dir` or `interface` from escaping the intended directory.
- After any manual edit to `wg0.conf`, run `wgusr sync --all` to bring the live interface in sync.

---

## License

Licensed under the [MIT License](LICENSE).
