Skip to content

Repository files navigation

VPN Manager

A Telegram bot and Django admin panel that manage a fleet of self-hosted VPN servers — issuing, revoking and renewing client configs for VLESS (Xray) and AmneziaWG without anyone touching the servers by hand.

The bot is the user-facing half: request access, get a config, see your configs. The Django admin is the operator half: approve users, manage servers and protocols, set quotas, deactivate configs. Both act on the same models, and every model change is pushed to the actual server over SSH.

What it does

Access is gated. A new user can only press "request access". That creates an AccessRequest and sends the admin an inline ✅/❌ keyboard in Telegram. Until approval the user is inactive and gets nothing. Each user has a config quota; admins are exempt.

Configs are placed automatically. On creation, VPNServer.get_least_loaded() picks the active server with the most free slots, honouring each server's max_configs limit. If every server is full, creation fails loudly rather than silently overloading one.

Model state is the source of truth. Saving a config generates it on the chosen server; flipping is_active enables or disables the client remotely; deleting the row removes the peer from the server. Deactivating a user cascades to all of their configs. Configs expire after a year by default and can be renewed.

Two protocols, one interface. XRayManager and WGManager both extend BaseConfigManager, which handles the transport: pull the server's config file, mutate it, push it back, restart the container — all as a context manager, so the connection and temp files are always cleaned up.

How it talks to servers

Every server operation goes over SSH with Paramiko, and the config file is reached one of two ways depending on how that server is set up:

  • Mounted volumes — SFTP straight to config_path on the host.
  • No volumesdocker cp in and out of the running container.

For VLESS, adding a client does not go through the config file at all. It calls Xray's gRPC HandlerService.AlterInbound with an AddUserOperation (flow: xtls-rprx-vision), so a new client is live immediately with no container restart. gRPC failures are translated into typed exceptions — EmailExistsError, InboundTagNotFoundError, XrayError — instead of raw RpcError.

For AmneziaWG, WGManager generates the keypair, allocates the next free IP, appends the peer, and builds the client .conf file to hand back to the user.

Stack

  • Django 5.2 — models, admin panel, migrations
  • aiogram 3 — async Telegram bot, routers, middleware
  • Paramiko — SSH and SFTP to the VPN servers
  • grpcio — Xray Handler API client (generated stubs in xray_api/grpc_generated_files)
  • PostgreSQL (SQLite optionally, via SQLITE_DB=true)
  • Sentry — error reporting
  • wgconfig / wireguard-tools — WireGuard config handling
  • Docker · uv · Python 3.12

The bot bridges async aiogram handlers to sync Django ORM calls with asgiref.sync_to_async.

Quick start

uv sync

Create a .env file:

TELEGRAM_BOT_TOKEN=your-bot-token
MAIN_ADMIN_ID=your-telegram-id
SSH_KEY_PATH=/path/to/private/key
WG_PRESHARED_KEY=your-wireguard-preshared-key
SENTRY_DSN_DJANGO=your-sentry-dsn

POSTGRES_DB=vpn_manager
POSTGRES_USER=vpn_manager_user
POSTGRES_PASSWORD=
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
# SQLITE_DB=true   # use SQLite instead of PostgreSQL

Then:

cd vpn_manager
uv run python manage.py migrate
uv run python manage.py createsuperuser
uv run python manage.py runserver        # admin panel
uv run python telegram_bot/main.py       # bot

Or with Docker:

docker compose up -d

Add a VPNServer and its ServerProtocol in the admin panel before issuing the first config.

Layout

Path Role
django_vpn/models/ VPNUser, AccessRequest, VPNServer, ServerProtocol, VLESSConfig, AmneziaWGConfig
django_vpn/managers/ BaseConfigManager and the VLESS / AmneziaWG implementations
django_vpn/xray_api/ Xray gRPC client, generated protobuf stubs, typed exceptions
django_vpn/utils/ SSH transport, WireGuard and VLESS config helpers
django_vpn/admin.py the operator console — users, configs, servers, inlines
telegram_bot/ aiogram routers, authorization middleware, config generation
django_vpn/management/commands/ fix_config, migrate_to_3xui (bulk-migrate VLESS users into a 3x-ui panel, supports --dry-run)

Status

Working and in use, but this is a personal-scale tool, not a product:

  • No automated tests. The riskiest gap, given that a bad save mutates a live server's config.
  • Config expiry is not enforced. expires_at is stored and is_expired() exists, but nothing sweeps expired configs yet.
  • Config generation is synchronous. A slow SSH round-trip blocks the bot handler; moving it to a task queue is the main planned change.
  • celery and qrcode are declared as dependencies but not yet used.

Planned, roughly in order of value: expiry enforcement and a periodic reconciliation check between the database and what is actually on each server; async config generation; per-config user comments; payments through the bot.

Related

Superseded for my own use by the Remnawave ecosystem, which I now contribute to instead — this project is where I worked out the problem first.

License

MIT

About

Telegram bot and Django admin running a fleet of self-hosted VPN servers — VLESS and AmneziaWG configs issued over SSH

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages