Skip to content

Repository files navigation

RosPanel

RosPanel

Self-hosted VPN control panel built on Xray-core — from a single personal server to a network of nodes.

Release Downloads Go Xray-core React Platform Deploy

English · Русский

What it is · Where to get a server · Quick start · Features · CLI · Architecture · Disclaimer · Development · Support


What it is

RosPanel is a self-hosted VPN control panel built on Xray-core. A single process serves several protocols at once, and the panel gives you a web interface for users, subscriptions, plans and payments, routing, statistics, backups and branding — no hand-editing of config files. When one server is no longer enough, the same panel grows into a network of servers: add nodes and every user is served by all of them.

Self-contained: one static binary — no nginx, no certbot, no third-party scripts. The panel issues its own TLS via ACME, and a domain is optional: the certificate will be issued for a bare IP too. The panel is reachable at its own address right after install — no DNS, no reverse proxy, no SSH tunnels. The Xray config is generated from the panel's state (you never touch JSON by hand), users are added and removed on the fly without dropping anyone else's connections, and if the database gets corrupted the panel brings itself back up from the last backup.

Note

This is a control plane, not a VPN client. It configures and operates your own server. The project is intended for educational and research use (see Disclaimer).


🖥️ Where to get a server

The cheapest VPS is enough: 1 vCPU, 1 GB RAM, any Linux — that covers both the panel and Xray for dozens of users.

If you haven't picked a host yet, sign up through the links below. It's a free way to support the project: the price is the same for you, and a share of the rent goes into the panel's development.

VDSina · Aeza · NetGrid · Serv.host · u1Host · Waicore

Thanks to everyone who signs up through them 🙏


🚀 Quick start

Option 1 — install script (recommended)

One command: downloads the release, installs a systemd service, starts it and prints the login.

curl -Ls https://raw.githubusercontent.com/AppsGanin/rospanel/main/install.sh | sudo bash

A domain is optional. The script will ask for one: if you have a domain, enter it; if you don't, just press Enter and the panel comes up on the server's IP. It gets a Let's Encrypt certificate either way — certificates are issued for IP addresses as well, so no browser warnings.

You can set the domain up front and skip the question: curl -Ls … | sudo ROSPANEL_HOST=vpn.example.com bash.

Option 2 — binary + systemd by hand

# download the latest release (replace amd64 with arm64 for ARM servers)
curl -fsSL -o rospanel \
  https://github.com/AppsGanin/rospanel/releases/latest/download/rospanel-linux-amd64
chmod +x rospanel

# install as a service (copies the binary to /usr/local/bin, writes a systemd unit, starts it)
sudo ./rospanel install
#   with a domain right away:  sudo ROSPANEL_HOST=vpn.example.com ./rospanel install

# the login and the secret path are printed ONCE:
journalctl -u rospanel | grep -A6 FIRST-RUN

Option 3 — Docker

docker run -d --name rospanel \
  --network host \
  --cap-add NET_ADMIN \
  -v rospanel-data:/data \
  ghcr.io/appsganin/rospanel:latest

docker logs rospanel | grep -A6 FIRST-RUN

Note

--network host is required so Xray can listen on 443/TCP, 80/TCP and the Hysteria2 UDP ports directly; NET_ADMIN lets the panel manage firewall rules: port hopping and connection limits (nftables), brute-force bans (iptables).

🔑 Default login

Field Value
Username admin
Password admin
Panel path /rospanel/

Right after install the panel is available at https://<domain-or-IP>/rospanel/. The exact link is also in the log (journalctl -u rospanel | grep -A6 FIRST-RUN or docker logs rospanel | grep -A6 FIRST-RUN).

On first login the setup wizard forces a password change and offers to replace the panel path with a random secret — the default admin/admin and /rospanel/ only work up to that step.

Important

After the change the panel is reachable only via the secret path — the root serves a decoy page. Without knowing /<secret>/ there is no login form to find.

🌐 Adding a node

A node is a clean Ubuntu server running the same binary in node mode: the panel generates its config, there is nothing to set up separately. Everything starts in the UI: Servers → Add node, where you enter a name and the node's domain or IP. From there you have two options.

Option 1 — install command. The panel shows a ready one-liner; run it on the node's server as root:

curl -Ls https://raw.githubusercontent.com/AppsGanin/rospanel/main/install.sh \
  | sudo bash -s -- --join 'https://<panel>/<node-api-path>/v1/join#<token>'

Copy the command whole, from the dialog — both the address and the token are filled in automatically. The token is shown once and lives for 24 hours; <node-api-path> is a separate unguessable segment for node sync (neither the panel path nor the REST API path: changing either one won't detach your nodes). If the panel runs on a bare IP (certificate not from a public CA), the panel adds --insecure to the command itself.

Option 1b — Docker. Same image as the panel, run in node mode. node install writes a systemd unit a container has nowhere to put, so the join goes in the environment instead: it is used only when the volume has no node.json yet, so the container can be recreated at will and a spent token in the compose file changes nothing.

services:
  node:
    image: ghcr.io/appsganin/rospanel:latest
    command: node run
    network_mode: host          # Xray binds 443/TCP, 80/TCP and the Hysteria2 UDP ports
    cap_add: [NET_ADMIN]        # nftables: per-IP limits, port hopping
    environment:
      ROSPANEL_JOIN: 'https://<panel>/<node-api-path>/v1/join#<token>'
      # ROSPANEL_JOIN_INSECURE: "1"   # only if the panel is still on a self-signed cert
    volumes: [rospanel-node:/data]
    restart: unless-stopped
volumes:
  rospanel-node:

The panel's Update node button swaps the binary inside the container and exits, so the restart brings back the image's version — update a Docker node with docker pull and a fresh container instead. The volume keeps the identity, so it does not re-join.

Option 2 — install over SSH. The "Install over SSH" tab in the same dialog: the panel logs into the server itself (address, port, user + password or a PEM private key), uploads its own binary and installs the agent — with a live install log. The node version is guaranteed to match the panel's. SSH credentials are never stored.

A few seconds after install the node shows up in the list as online: it reaches out to the panel over outbound HTTPS, so the panel needs no inbound access to it and there is nothing to forward.

systemctl status rospanel-node        # node service
journalctl -u rospanel-node -f        # agent log
rospanel node status                  # local status

Note

One server is either a panel or a node: they share port 443. If the panel service is already on the machine, node install will disable it.


✨ Features

🔐 Protocols, masquerading, TLS

One config, one set of credentials: VLESS-Vision (TCP/443 + uTLS), VLESS-XHTTP-REALITY (masquerading as someone else's TLS), Hysteria2 (UDP + port hopping). On top of that — custom inbounds on every server: VLESS / Trojan / Hysteria2 over any transport (TCP, WebSocket, XHTTP, gRPC, HTTPUpgrade) with their own port, REALITY keys, hop range and fine-grained transport tuning (XHTTP extra, HTTP masquerading, sockopt, extra TLS fields) — as individual fields or raw JSON; the config is validated on the target machine itself (xray -test + port bind) before saving, and combinations a client can't handle are silently kept out of Clash/sing-box subscriptions. The panel hides behind a secret path; any other path serves a decoy site (11 templates). Probe detection notices an IP that scans for the hidden panel — one that requests many distinct paths the decoy doesn't have — and records it for the operator to review; the reply never changes, so a scanner still sees only the decoy and the masquerade holds. Optionally it also drops the IP at the firewall (nftables) and/or sends a daily digest of new scanners — both off by default, recording alone is the safe baseline. TLS out of the box — ACME (Let's Encrypt / ZeroSSL) with auto-renewal; the certificate can also be issued for a bare IP, with no domain and no DNS.

👤 Users

Traffic and time limits with auto-disable and quota auto-reset (day/week/month/year), a device limit (see Device binding below for exactly what it counts) and a per-user speed cap. Traffic accounting via Xray Stats, online status, connection list; expired users can be auto-deleted. Search and filters stay fast with hundreds of users, and bulk operations (enable/disable/reset/extend/delete) go through a single Xray reload. The dashboard shows CPU / RAM / swap / disk and VPN traffic in real time. A connection map breaks down where clients connect from — distinct source IPs per country (from the same geoip database Xray routes with) and per network operator / ASN (from a free iptoasn table the panel fetches itself); no external service.

The device limit — what it counts. A user's device limit (or the one their plan sets) is a single number enforced two ways. On its own it caps concurrent unique source IPs: a user connecting from more distinct addresses than the limit within the online window is dropped from the tunnels until they fall back under it — counted across every server (master and nodes), not per-server. 0 means no IP cap.

The cut is not immediate. The limit has to stay exceeded for two and a half minutes, a little longer than the online window. That is because the two commonest ways to exceed it are not sharing at all: a phone changing network abandons its old address while that address keeps a fresh sighting until the window drops it, and a mobile carrier rotates the public address inside its own pool with no user action whatsoever (one live account shows seven addresses in a single 176.15.0.0/16 over a month). The only way to tell either from a second device is to watch whether the old address keeps being used — which means not cutting the user, because cutting is what stops the traffic being judged. So the limit waits, and an abandoned address leaves the window on its own. Addresses in genuine simultaneous use are all still there when the wait is over, and the cut lands as before.

Settings → Subscriptions → Count devices byHWID only drops the address count altogether — at the price of the only thing that caps how many places one link is used at once, since HWID caps who may fetch the subscription, not who may connect. The default counts both.

Device binding (HWID). Turn this on and the same number also caps distinct installs. Clients that follow the subscription-header convention (Happ, v2RayTun) send a stable install id; the panel binds it to the account on first fetch and counts it against the limit. Once the limit is full a NEW install is refused the subscription while the bound ones keep updating — the check and the insert are one transaction, so two clients cannot both take the last slot. (When a user's own limit is 0, HWID uses a panel-wide fallback limit instead.) The devices are listed in the user card, in the client bot (as separate by IP and by HWID lines when both apply), and on the subscription page, where the owner can release one themselves instead of writing to support; an idle device frees its slot after a configurable TTL, and rotating the subscription link releases them all. Off by default (Settings → Subscriptions); once on, a client that sends no id gets no subscription at all — a cap you can dodge by switching to a quieter app is not a cap — with a switch to serve those clients anyway (counted by address, as before) if some of your users are on them.

Speed limit. A per-user cap in kbit/s, set by hand or by the tariff. Xray has no per-user bandwidth limit, so it is enforced below it — the kernel's own scheduler (HTB), keyed on the addresses that user is currently connected from, in both directions. That means it covers every protocol at once, that everyone behind one NAT address shares a cap, and that for Hysteria2 (whose congestion control ignores loss by design) hitting the cap looks like packet loss rather than a smooth slowdown. Nodes shape their own traffic from their own view of who is connected.

Access groups decide which connections a user gets: built-in lanes and custom inbounds are ticked per server, a user with no group gets everything, a user in several groups gets the union of their connections. The restriction is server-side — the account simply isn't added to the forbidden Xray inbounds (rather than being hidden in the UI), so both the subscription and a hand-crafted link only ever hand out what's allowed. Membership is editable from both sides — in the user's card and in the group itself — and a user's groups are visible in their card and in the list.

📲 Subscriptions

/<path>/<token> — a base64 list plus a page with a QR code, deep links and import into popular clients (auto-routing headers for Happ / INCY / Mihomo), with your own node names. The link can be reset (token rotation) without changing UUIDs and passwords. An announcement inside the client (Happ, v2RayTun) puts a short text right in the app.

The page carries what the account holder needs and nothing they shouldn't hand out: the individual per-lane configs card can be switched off, and with device binding on it lists their own bound devices with a button to release one — so a full device roster is self-service rather than a support ticket.

Response rules override the automatic format detection: an ordered list of operator rules matched against the request (User-Agent or an HWID header) — force a specific format for a given client / OS / version (contains, equals, prefix, regex), or block a client entirely (it is served the decoy). The first matching rule wins; no match falls through to normal detection.

Maintenance mode — one switch puts the public surfaces (subscription page, status page, decoy) on a "temporarily unavailable" page while the panel, API, node sync and the tunnels themselves keep running, so existing connections are untouched and the operator can still sign in.

🧭 Routing and egress

block / direct / WARP / Opera categories with priority, geosite/geoip presets with automatic database downloads, egress through Cloudflare WARP (WireGuard) and the free Opera VPN with region selection. Proxy lanes are independent egresses, each with its own socks5/http upstreams and zone rules, balanced across whatever is alive (Observatory). Config snapshots (a Snapshots tab in the server settings) give an undo history for the whole server config — protocols, ports, REALITY, routing, egress, DNS, decoy and inbounds: save a restore point by hand, and roll back to it if an edit breaks something. A rollback re-validates through xray -test and auto-snapshots the current state first (so it's itself undoable), and it deliberately leaves the certificate and domain untouched, so an undo never risks live access.

🌐 Server network (multi-node)

A single panel manages the master and any number of remote nodes. A server is added from the UI: copy one command for a clean Ubuntu box, or let the panel log in over SSH and install the agent — with a live install log. The node reaches out to the panel itself (outbound HTTPS long-poll), so the panel needs no access to the nodes, and moving the panel doesn't detach them. Users, limits and plans roll out to every node; traffic and devices count against shared limits, while statistics and the user card show how much traffic went through each server. Each node also has a traffic multiplier — a coefficient that scales how fast traffic through it spends a user's quota (2× on an expensive location, 0.5× on a promo one); it bends only the quota, never the per-node byte statistics.

Every server is configured separately (protocols, egress, DNS, REALITY keys, domain and TLS, geo databases, decoy). A node is the same binary in node mode: the panel generates its config, a local xray -test with rollback guards against version mismatches, and updates run from the UI with SHA256 verification.

💳 Plans and payments (optional)

Plans: price, duration, traffic and device limits; price 0 makes a free plan. There's a trial period, a free fallback plan for expired users, renewals and user migration between plans. Payment acceptance — pick a provider: YooKassa, PayPalych, RioPay, RollyPay, SeverPay, Platega, PayPear, AuraPay (cards, SBP, ₽), CryptoBot and Heleket (crypto). The client pays in the bot or on the subscription page, and the plan activates itself. A webhook confirms it (signature verified), polling covers the case where the webhook never arrives; processing is idempotent and the amount is checked against the order. With no provider configured, an admin confirms payments manually.

Warning

Payment providers have not yet been verified against live accounts. If you've connected one of them, please open an issue and say whether it works (which provider, what worked, what broke). That's what lets verified providers be marked as such and the rest get fixed.

👥 Access, roles and audit

Roles: owner (can do everything, exactly one, cannot be deleted), administrator (everything except the admin list), operator (users, statistics, activity log). Permissions are checked server-side on every request; a new admin gets a temporary password that must be changed on first login. Two-factor authentication (TOTP): each admin turns it on for themselves — a code from an authenticator app (Google Authenticator, Aegis, 1Password) on top of the password, the secret encrypted in the database and never handed back out after setup; for a lost phone, rospanel totp reset <login> on the server; a fuller rospanel rescue resets a forgotten password, clears a second factor, or recreates an owner when no admin can sign in at all. The user log records what was done to them and by whom (admin, API key, bot, the user themselves, the system) and survives their deletion. The panel log (visible to the owner) covers logins and failed attempts with IPs, second factors switched on and off, settings changes and backups; only successful actions are written, request bodies never are. The panel log is searchable (free text over action, target, administrator and IP) and filterable by category and date range, and the current view exports to CSV in one click. Both logs are kept for 90 days.

🤖 Integrations

Admin bot in Telegram: user management, plans, subscription QR codes, scheduled backups, event notifications (signups, expirations, Xray failures, payments, certificates).

Client bot: self-signup, a personal menu with the subscription and statistics, plan purchase — plus personal notifications to the user: subscription ending, traffic running out, payment received.

Support bot — a third bot: a person writes to it in a DM and the conversation lands in your forum group, one topic per person; you reply straight from there and the answer goes back to them in the bot.

Broadcasts — nine audience slices (everyone, with and without an account, active, expired, expiring soon, long-inactive, never connected) with an image and a button. Delivery is driven by a recipient table, so an interrupted broadcast resumes instead of restarting, and nobody gets the message twice.

REST API with named keys (Authorization: Bearer), OpenAPI generated from the code and Swagger UI. It covers both halves of the panel: the users, plans, orders and servers you operate, and the configuration behind them — settings, per-server routing and DNS, config save-points with rollback. Administrators, API keys and the panel's secret path are deliberately not exposed. Webhooks send HMAC-SHA256 signed events with retries. Prometheus metrics at /<api-path>/v1/metrics behind the same key — users, traffic, throughput, host stats and one series per node. An MCP server hands the same API to an AI assistant, with the tool list generated from that OpenAPI document: paste …/v1/mcp/<key> into an assistant that takes a URL and there is nothing to install anywhere. Write operations are off unless you ask for them (the /write address). More in docs/api.md.

Connecting an assistant takes one URL and no local install. Create a key in Settings → API, take the base address from the same page, and paste one of:

https://vpn.example.com/<api-path>/v1/mcp/<key>          read-only
https://vpn.example.com/<api-path>/v1/mcp/<key>/write    plus everything that changes state

The address is the credential — as secret as the key inside it, and dead the moment that key is revoked. The two differ only in the toolbox they offer: the short one cannot delete a user even though the key behind it could, which is what makes handing an assistant the read-only URL a real decision rather than a hope.

Status page — an optional public page (Settings → General) showing which servers are up and 90 days of uptime history. Names and availability only: no addresses, no users, no traffic, and no page at all until you switch it on.

🌍 Language (RU / EN)

Surface Language comes from
Panel the admin's own pick (per browser), otherwise the browser's languages
Subscription page Accept-Language
Client and support bots each person's Telegram language
Admin bot a panel-wide setting (Settings → Telegram)
CLI English

🎨 Branding and theme

Your own name and logo instead of "RosPanel" — in the panel and on the subscription page. An accent color repaints the whole interface, and dark mode adapts text, statuses and charts on its own.

🛡️ Abuse detection

The panel checks destination IP addresses from the Xray access log against a list of malicious networks and records matches only — ordinary traffic is never stored. It exists for exactly one purpose: when an abuse complaint arrives about the server's address, you can tell whose traffic it was.

The list is FireHOL level 1: botnet command-and-control servers, attackers and spam networks. A curated level with minimal false positives, without CDNs or shared hosting. Alongside it there's your own list (IP/CIDR), which is checked first. Matches show up in the statistics and in the user's card, attributed to the server the traffic left from; when the daily threshold is exceeded a Telegram notification goes out. Categories, the custom list, the threshold and updates live in Settings → Blocklists.

Checks run against addresses, not domains, and that isn't a simplification. Modern clients resolve DNS outside the tunnel and encrypt SNI (ECH), so all that reaches the server is a bare IP.

Matches are kept for 14 days — enough to handle a complaint.

🧰 Operations and security

Diagnostics in one click: the Xray process, config application, TLS expiry, disk space, geo database freshness, egress health — every check with a hint. A separate connection self-test connects to each protocol as a real client and confirms traffic actually goes out — catching credential, TLS or ALPN drift before a user does. Backup / restore and reset are available from the panel and the CLI.

Updates in one command: the panel verifies SHA256, runs the binary dry, takes a backup and only then replaces itself, keeping the previous version next to it. The Xray core is pinned, and the supervisor restarts it if it crashes. A watchdog covers the harder case a crash handler can't see — a process that stays alive but stops serving: it probes Xray's API and, if it goes unresponsive for several checks in a row, restarts it (with a cooldown against restart storms) and alerts the operator. Runs on the master and every node.

Secrets in the database are encrypted (AES-GCM). Session tokens and API keys are stored as hashes only — even with table access you can't reuse someone's session. Payment confirmation and admin management require re-entering the password. Outbound requests are protected against SSRF, brute force on inbounds is banned via iptables, and the number of connections per IP is limited via nftables.


🛠️ CLI

rospanel                     run the panel (usually via systemd)
rospanel install             install the systemd service and start it (root)
rospanel uninstall [-y]      remove the service (data is kept)
rospanel start|stop|restart  service control
rospanel status              service status
rospanel update [-y]         update to the latest GitHub release
rospanel backup [file]       .tar.gz snapshot (DB + encryption key + certificates + Xray config)
rospanel restore [-y] <file> restore from a snapshot (applied on start)
rospanel host [-y] [domain|IP] show/change the address (reissues TLS)
rospanel path                show the panel URL and check secrets.key / the DB
rospanel totp reset <login>  remove an admin's two-factor auth (lost phone); bare totp lists
rospanel rescue <sub>        regain locked-out access: list | password | unlock | owner
rospanel reset [-y]          factory reset (wipes the DB)
rospanel version             version
rospanel help                full help

Destructive commands (reset, restore, host, uninstall) ask for confirmation; the -y flag skips it.

Node mode

The same binary can run as a panel-managed node. The install command is generated by the "Add node" dialog in the panel — you normally never type it by hand.

rospanel node install --join '<url>'   join the panel and install the service (root)
rospanel node run                       node agent (systemd entry point)
rospanel node set-panel <url>           point the node at a new panel address
rospanel node status                    local node status
rospanel node uninstall [-y]            remove the node service (data is kept)

--join comes from the add-node dialog; the token in it is single-use and lives for 24 hours. For the full walkthrough see 🌐 Adding a node.


🧱 Architecture

The single source of truth is SQLite; the Xray config is always generated from it and applied by the supervisor. The web panel is embedded in the binary.

Stack: Go 1.26 · Xray-core · SQLite (modernc, CGO-free) · React + Vite + Tailwind.


⚠️ Disclaimer

The software is provided "as is", without warranties of any kind.

The project is intended for educational and research use: studying network protocols, TLS and proxy technologies, CTF and network security lab work, authorized penetration testing, and managing your own infrastructure. The project is not intended for circumventing lawful restrictions or for any other unlawful activity; the masquerading mechanisms exist to study the technology and to test service resilience within sanctioned testing.

Responsibility for installing, configuring and operating the software, and for complying with the laws of your jurisdiction, lies with the server operator. The authors and contributors are not responsible for how third parties use the project.


🧑‍💻 Development

# frontend (after changes in web/)
npm --prefix web install
npm --prefix web run build      # → web/dist (embedded into the binary)

# binary
go build -o rospanel ./cmd/rospanel
./rospanel

Localisation. The panel's dictionaries are web/src/i18n/ru.ts and en.ts, typed against each other: a key present in one and missing in the other is a build error, not a silent fallback. What Go renders itself — the bots and the subscription page — lives in internal/i18n/; the backend hands the panel a key plus arguments and never rendered prose. Adding a third language means one more dictionary on each side.

Useful environment variables (all optional): ROSPANEL_DATA (data directory), ROSPANEL_ADMIN_ADDR (the panel's loopback address, 127.0.0.1:8080 by default), XRAY_BIN, ROSPANEL_HOST, ROSPANEL_ACME_EMAIL.

Flood protection (nftables limits on public TCP ports, see "Operations") is configured through the environment too — handy when a whole office or a CGNAT carrier sits behind one IP and clients hit the defaults:

Variable What it does
ROSPANEL_CONNLIMIT=off Disables the limits entirely (nftables rules are removed)
ROSPANEL_CONNLIMIT_MAX Maximum concurrent TCP connections from a single IP
ROSPANEL_CONNLIMIT_RATE Maximum new connections per second from a single IP

The current state is visible in Dashboard → Management → Diagnostics: if nftables isn't installed or the panel isn't running as root, the rules silently won't apply — diagnostics will say so.

PRs and issues are welcome. Commits follow Conventional Commits: release-please uses them to cut releases and publish the binary and the Docker image to GHCR.


💝 Support the project

RosPanel is developed in spare time and distributed for free. If the panel turned out to be useful, the easiest way to support it is DonationAlerts or Boosty (RU cards, one-off or recurring). Thank you! 🙏

Crypto

USDT. Pick a network and copy the address carefully — the sender's and the receiver's network must match. Send USDT only, and only on the network listed: a transfer on the wrong network cannot be recovered. The cheapest fees are on TRON (TRC20) and TON.

Network Token Address
TRC20 (Tron) USDT TJwyrPVEZVZ1YrcmDiZTyFjLo3Q2DmEGzs
ERC20 (Ethereum) USDT 0xf9d663146ce902da91911b214c71cc73a5269d1d
Solana USDT 2qAZRTbaUMTfYuZbD1dCYHjkYgxkw4dUYE9XY3JhC2Cs
TON USDT UQDoat731MLYuIw8ayL3Vhhw7zTBbLvRaQFmDvab--CNNI7e

Bybit. If you're on Bybit too — transfer to UID 136462734: instant and free.

Can't support financially? You can still help — rent servers through the referral links in Where to get a server. It costs you nothing and helps the project.


📄 License

GNU Affero General Public License v3.0 (AGPL-3.0).

The project is free to use, modify and self-host. If you provide network access to a modified version of the panel (including as a service), you must release the source of your changes under the same terms.

Releases

Packages

Used by

Contributors

Languages