- Python 87.1%
- HTML 10%
- CSS 2.3%
- JavaScript 0.3%
- Dockerfile 0.3%
Servers with private SSH hosts (NAT setups) couldn't enter their public IP via the dropdown. Replaced it with a plain text input so any IP can be typed. Empty value still means auto-detect on sync. Fixed SocksBindIpForm defaulting to "" so saving without a bind IP no longer returns a 422 Field required error. |
||
|---|---|---|
| .forgejo/workflows | ||
| app | ||
| tests | ||
| vendor/binaries | ||
| web | ||
| .dockerignore | ||
| .env.development.example | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| pytest.ini | ||
| README.md | ||
| requirements.txt | ||
| sonar-project.properties | ||
wg-manager
Small panel to manage WireGuard on multiple Debian VPS nodes.
What it does:
- add servers over SSH
- provision WireGuard remotely
- create multi-server devices
- create one-off single-server profiles
- download config + gluetun snippet
quick start
cd /root/vpn
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
python -m app.main
Then open http://127.0.0.1:8080.
docker
cp .env.example .env # set PANEL_SECRET and ADMIN_PASSWORD
mkdir -p data ssh_keys && chown -R 10001:10001 data ssh_keys
docker compose pull && docker compose up -d
Image: git.stackken.com/kenzim/wg-manager:latest. Put VPS SSH keys in ssh_keys/ before use. In Portainer, leave Command and Entrypoint empty (use image defaults), only mount data and ssh_keys — not /app itself.
development (autoreload)
cd /root/vpn
cp .env.development.example .env.development
set -a
source .env.development
set +a
source .venv/bin/activate
python -m app.main
APP_ENV=development or AUTO_RELOAD=true enables uvicorn autoreload.
basic flow
- put your VPS SSH key in
ssh_keys/(chmod 600) - add server
- click provision
- add device or one-off profile
- sync
- download config
routing & NAT
Clients reach LANs behind a server via route groups (Route Config tab):
- a server can be a full internet exit, a LAN/subnet gateway, or both
- route groups bundle CIDRs and can be imported from the ZeroTier legacy API
- give a route group a NAT interface to auto-apply iptables on the server:
iptables -t nat -A POSTROUTING -s <wg-subnet> -o <iface> -j MASQUERADE - use the
zt+wildcard for ZeroTier (interface names are hashed per-network); ZeroTier imports default tozt+ - the same rules also
ACCEPTclient traffic to the server's own overlay IPs, so you can still reach the server's ZeroTier address under a default-DROP INPUT policy
NAT rules are baked into the wg0 PostUp/PostDown at provision time (persisted across reboots) and applied/removed live when a group is attached to or detached from an already-provisioned server. Re-provision to persist groups attached after the initial provision across reboots.
SOCKS5 proxy
Optional authenticated SOCKS5 on each server (public bind IP):
- open the SOCKS Proxy tab
- create a global SOCKS user (assign to all or selected servers) and/or a server-only user
- per server: enable SOCKS, set bind IP and port, click Sync SOCKS
- connect with
socks5://username:password@bind-ip:port(independent of WireGuard)
Sync installs 3proxy over SSH (from apt when packaged, otherwise uploads a bundled linux-amd64 binary) and opens an iptables INPUT rule for the chosen bind address.
Pytest
pytest
Latest local run: 36.61% total coverage.
Coverage breakdown (auto-updated by pytest):
| File | Coverage |
|---|---|
app/dashboard_state.py |
13.08% |
app/ssh_ops.py |
22.22% |
app/services.py |
26.38% |
app/socks_ops.py |
29.11% |
app/wg_config.py |
29.31% |
app/routes/web.py |
30.72% |
app/wg_keys.py |
38.1% |
app/routes/api.py |
46.61% |
app/security.py |
53.5% |
app/auth.py |
56.38% |
app/logging_config.py |
66.67% |
app/main.py |
68.18% |
app/db.py |
80% |
app/middleware.py |
93.33% |
app/deps.py |
94.44% |
app/config.py |
96.97% |
app/__init__.py |
100% |
app/forms.py |
100% |
app/routes/__init__.py |
100% |
app/wg_defaults.py |
100% |