This project is for educational purposes only. It is not intended for use in circumventing lawful restrictions or enabling unauthorized access to networks or services.
This repository currently has four deployable roles:
xray_server: the required public VLESS+REALITY server.xray_proxy: the required TCP proxy forxray_server, plus the HTTPS subscription/admin service.xray_xhttpserver: an optional VLESS+XHTTP+TLS server meant to sit behind a CDN.hstd: an optional local Debian router/client that runsxrayvpnd, Wi-Fi AP services, Transmission, and Navidrome.
inventories/all.py deploys all four roles. The minimal public deployment is xray_server plus xray_proxy.
Internet
|
+-----------------+------------------+
| | |
xray_server xray_proxy xray_xhttpserver
VLESS+REALITY nginx stream VLESS+XHTTP+TLS
required required optional
:443/tcp :443/tcp :443/tcp
:80/http subsrv :8080 :80/http
|
subscription/admin
https://<proxy_domain>:8080/
|
hstd
local router/client (optional)
xrayvpnd + tun2socksd + dnsmasq + hostapd + Navidrome
subsrv exposes server configs defined in the servers list of subsrv_config in inventories/xray_proxy.py. The default configuration provides three entries:
- Direct REALITY to
xray_server_addr. - Proxied REALITY to
xray_proxy_addr. - CDN-fronted XHTTP/TLS to
xhttp_cdn_domainif you deploy the optionalxray_xhttpserverpath.
Important: proxy_domain is only for the HTTPS subscription/admin service on port 8080. The proxied REALITY config itself targets the proxy IP address from xray_proxy_addr.
- Linux is recommended.
inventories/all.pybuilds a Debian package locally withmake deb, so you needdpkg-debif you deployhstd.asdfmakepassgpgsshcurl- Python and Go matching
.tool-versions - Python packages:
pyinfraandbcrypt
If you only deploy the required public roles (xray_server and xray_proxy), macOS can work as long as you have the required Python/Go toolchain. For the optional hstd deployment, a Linux workstation is the safest choice because of the .deb build step.
- Debian 12+ on each remote host
- 2 required Internet-facing hosts for
xray_serverandxray_proxy - 1 optional Internet-facing host for
xray_xhttpserver - 1 optional local/LAN Debian host for
hstd - A domain for the subscription/admin service
- A separate origin domain plus a CDN hostname if you deploy
xray_xhttpserver
Install the exact Go and Python versions from the repo root:
asdf plugin add golang
asdf plugin add python
asdf installInstall the Python packages used by deployment:
python -m pip install pyinfra bcryptMake sure pass works before continuing:
pass ls| Purpose | Example | Points to |
|---|---|---|
proxy_domain |
x.example.com |
xray_proxy_addr |
Notes:
proxy_domaingets a Let's Encrypt certificate forsubsrvonxray_proxy.
If you deploy xray_xhttpserver, also configure:
| Purpose | Example | Points to |
|---|---|---|
xhttp_source_domain |
cdn.example.com |
xray_xhttpserver_addr |
xhttp_cdn_domain |
pub.cdn.example.com |
your CDN resource that fronts xhttp_source_domain |
Notes:
xhttp_source_domaingets a Let's Encrypt certificate onxray_xhttpserver.xhttp_cdn_domainis what subscription clients use for the third config.xhttp_source_domainmust resolve directly toxray_xhttpserverduring ACME validation.
Use any Xray binary that supports x25519. One way is:
wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip
unzip Xray-linux-64.zip xray
chmod +x xray
./xray x25519Save:
- the public key for
deploy/xray.py - the private key for
pass
openssl rand -hex 32openssl rand -hex 8The deployment code reads these exact entries:
pass insert hstd/rotate_secret
pass insert hstd/xray_server/reality_private_key
pass insert hstd/xray_proxy/sub_path
pass insert hstd/xray_proxy/admin_user
pass insert hstd/xray_proxy/admin_passwordIf you deploy hstd, also add:
pass insert hstd/wpa_passphraseNotes:
admin_passwordis stored as plain text inpass; deployment hashes it withbcryptautomatically.sub_pathis still required. It acts as the legacy subscription path for link index0, even though the admin UI also exposes encoded per-link URLs.
Replace the sample values with your own:
xray_version = "26.3.27"
xray_xhttpserver_addr = "YOUR_XHTTPSERVER_IP"
xray_server_addr = "YOUR_XRAY_SERVER_IP"
xray_proxy_addr = "YOUR_XRAY_PROXY_IP"
reality_pbk = "YOUR_REALITY_PUBLIC_KEY"
reality_sni = "example.com"
reality_sid = "YOUR_REALITY_SHORT_ID"
proxy_domain = "x.example.com"
xhttp_source_domain = "cdn.example.com"
xhttp_cdn_domain = "pub.cdn.example.com"
xhttp_path = "/images/625385d043bfac1b"
letsencrypt_email = "you@example.com"These values are imported by inventory files and used to populate host variables. What each field is used for:
xray_version: version of xray-core installed onxray_serverandxray_xhttpserverxray_server_addr: direct REALITY server addressxray_proxy_addr: TCP proxy IP used by the proxied REALITY configxray_xhttpserver_addr: XHTTP origin server IP for the optional CDN-fronted pathreality_pbk: public half of the REALITY key pairreality_sni: REALITY SNI and destination hostname basereality_sid: short ID advertised to clientsproxy_domain: hostname for the HTTPS subscription/admin servicexhttp_source_domain: certificate/origin hostname on the optionalxray_xhttpserverxhttp_cdn_domain: CDN hostname given to clients for the optional XHTTP configxhttp_path: XHTTP path used by the optional XHTTP server and client configletsencrypt_email: email for Let's Encrypt certificate registration
Review these files and change the host addresses, SSH users, and domains:
inventories/xray_server.pyinventories/xray_proxy.pyinventories/xray_xhttpserver.pyif you deploy the optional CDN/XHTTP pathinventories/hstd.pyif you deploy the local router/client
Current inventory-specific knobs:
ssh_userreality_destreality_server_namesreality_short_idreality_private_keyrotate_secret
reality_dest, reality_server_names, and reality_short_id are derived from deploy/xray.py by default. reality_private_key and rotate_secret come from pass.
ssh_user_sudoxray_server_addrrotate_secretproxy_domainletsencrypt_emailsub_pathadmin_useradmin_password_hashsubsrv_config
proxy_domain and letsencrypt_email come from deploy/xray.py. The other non-secret values also come from deploy/xray.py, and the secrets come from pass.
subsrv_config is a dict that gets deployed as /etc/subsrv/config.json. It defines the server entries and client routing rules that subsrv serves to subscribers. The structure contains:
servers: list of server entries, each withremark,host, and either REALITY fields (realityPbk,realitySni,realitySid) or XHTTP fields (xhttpPath)routingRules: list of Xray routing rules included in generated client configs
ssh_userrotate_secretxhttp_source_domainxhttp_pathletsencrypt_email
If you use hstd, audit these fields carefully:
- host IP /
ssh_user/_sudo wpa_passphraseapd_ipapd_cidrapd_gateway_cidrlan_gateway_cidrdhcp_range_startdhcp_range_endtransmission_rpc_whitelistxray_out_markxray_traffic_markwan_devapd_devlan_devtun_dev
The hstd role no longer carries REALITY or xray server parameters directly. Instead, it adds a subscription URL during deploy and uses xrayvpn sub sync to fetch connections from subsrv.
Several important values are still hardcoded outside the inventories:
deploy/roles/hstd.py: creates usergleband installs a hardcoded SSH public keydeploy/templates/hstd/hostapd.conf.j2: SSID, channel plan, andcountry_code=RU
If those do not match your environment, change them before deploying.
The deploy tasks build artifacts automatically, but these commands are useful as a preflight check:
cd xrayvpn
make xrayconnectord
make debCurrent outputs:
make xrayconnectordxrayvpn/target/clientrotatexrayvpn/target/subsrv
make buildxrayvpn/target/xrayvpndxrayvpn/target/xrayvpnxrayvpn/target/tun2socksd
make deb(depends onbuild)- all of the above plus
xrayvpn/target/deb/xrayvpn_0.1.0_amd64.deb
- all of the above plus
make deb is only needed for the optional hstd role. The required public roles only need make xrayconnectord.
pyinfra inventories/all.py deploy.pyRemember: inventories/all.py includes hstd. If you do not want that role, do not use all.py.
pyinfra inventories/xray_server.py deploy.py
pyinfra inventories/xray_proxy.py deploy.pypyinfra inventories/xray_xhttpserver.py deploy.py
pyinfra inventories/hstd.py deploy.py- installs
nftables,nginx,curl,ca-certificates - installs
xray-coreversion26.3.27 - uploads
/usr/local/etc/xray/config.json - uploads
clientrotateand its systemd units - enables
nftables,nginx,xray,ssh,clientrotate, andclientrotate.timer - runs
clientrotate.serviceonce after deploy
- installs
nftables,nginx,libnginx-mod-stream,curl,certbot - uploads
subsrv - deploys
/etc/subsrv/config.jsonwith server entries and routing rules fromsubsrv_config - configures nginx stream proxy on
:443 - runs
subsrvdirectly on HTTPS:8080 - obtains a Let's Encrypt certificate for
proxy_domain - enables
nftables,nginx,ssh,subsrv, andcertbot.timer
- installs
nftables,nginx,curl,ca-certificates,certbot - installs
xray-coreversion26.3.27 - uploads
/usr/local/etc/xray/config.json - uploads
clientrotateand its systemd units - obtains a Let's Encrypt certificate for
xhttp_source_domain - enables
nftables,nginx,xray,ssh,clientrotate, andclientrotate.timer - enables
certbot.timer
- builds and uploads
xrayvpn_0.1.0_amd64.deb - installs
nftables,dnsmasq,hostapd,transmission-daemon,ffmpeg,curl,rsync,networkd-dispatcher,systemd-resolved,dns-root-data, andfirmware-misc-nonfree - downloads and installs Navidrome
0.60.3 - configures
systemd-networkd, nftables, dnsmasq, hostapd, Transmission, Navidrome, and thexrayvpnsystemd overrides - adds a subscription URL pointing at
subsrvand runsxrayvpn sub syncto fetch initial connections - runs
clientrotate.serviceto perform an initial client rotation - enables
xrayvpnd,nftables,dnsmasq,hostapd,navidrome,transmission-daemon,ssh,systemd-networkd,networkd-dispatcher,systemd-resolved,clientrotate.timer, and related services
systemctl status xray
systemctl status nginx
systemctl status nftables
systemctl status clientrotate.timersystemctl status nginx
systemctl status subsrv
systemctl status nftables
systemctl status certbot.timerTest the legacy subscription path:
curl https://<proxy_domain>:8080/<sub_path>Expected result: a JSON array containing the direct and proxied REALITY client configs, plus the optional XHTTP config when that path is enabled.
Open the admin UI:
https://<proxy_domain>:8080/admin/
Use the username from hstd/xray_proxy/admin_user and the plain-text password from hstd/xray_proxy/admin_password.
systemctl status xray
systemctl status nginx
systemctl status nftables
systemctl status certbot.timer
systemctl status clientrotate.timersystemctl status xrayvpnd
systemctl status tun2socksd
systemctl status xrayvpnd-refresh.timer
systemctl status clientrotate.timer
systemctl status dnsmasq
systemctl status hostapd
systemctl status transmission-daemon
systemctl status navidrome
systemctl status systemd-networkd
systemctl status networkd-dispatcherUseful local checks on hstd:
xrayvpn conn list
xrayvpn sub list
xrayvpn start
xrayvpn stop
xrayvpn refreshsubsrv listens on :8080 with HTTPS. It is not behind nginx. It reads its server entries and routing rules from /etc/subsrv/config.json (deployed from subsrv_config in the inventory). It serves:
GET /admin/: admin page (cookie-based session auth)GET /admin/ws: websocket feed for admin updatesGET /{link}: JSON subscription response
The admin page currently manages a fixed pool of 100 link slots. It can:
- enable or disable a link
- edit a comment
- show the encoded URL and QR code for each slot
- track devices seen in the last 24 hours
subsrv also periodically refreshes Russian CIDR data (every 2 hours) and injects it into the cidr:ru routing rules in generated client configs.
The project derives client UUIDs deterministically from:
rotate_secret- link index
- the current day in UTC with a
-3hoffset before truncation
The server-side clientrotate binaries write the current UUID set into the Xray configs on xray_server and on xray_xhttpserver when that optional role is deployed. subsrv derives the matching UUIDs on demand, so clients that refresh their subscription get the right values automatically.
Each rotation produces 200 UUIDs: 100 for the current day and 100 for the previous day, ensuring a graceful transition window.
The JSON configs returned by subsrv include routing rules defined in the routingRules field of /etc/subsrv/config.json (configured via subsrv_config in the inventory). This split routing is separate from the Linux-level split routing on hstd.
Each generated client config currently contains:
- a local SOCKS inbound on
127.0.0.1:10808 - a
directoutbound - a
proxyoutbound using one of the advertised server entries - a
blockoutbound
The default routing rules in the inventory currently do this:
- force
domain:yonote.ruanddomain:hstd.spacethrough the proxy - send
geoip:ru,geoip:private, andcidr:rudirectly - send
geosite:category-ruandgeosite:category-gov-rudirectly - send all remaining
tcpandudptraffic through the proxy
These rules can be changed by editing the routingRules list in inventories/xray_proxy.py and redeploying.
The hstd host consumes the HTTPS subscription endpoint. During deploy, a subscription URL is added via xrayvpn sub add, and clientrotate.service runs xrayvpn sub sync to fetch connections from subsrv. The clientrotate.timer periodically re-syncs to pick up rotated UUIDs.
When the tunnel is up, tun2socksd and xrayvpnd install a split-routing setup with these behaviors:
- The main routing table's default route is replaced with the TUN device, so ordinary traffic enters
xray0. - A separate direct route table keeps the original WAN gateway for traffic that must bypass the tunnel.
- Packets emitted by xray-core are marked with
xray_out_markand forced into the direct table, which prevents routing loops. - The service users
debian-transmissionandnavidromebypass the tunnel at the IP-rule layer and use the direct table for Internet traffic. - Those same service users keep access to the local APD subnet through a higher-priority rule that sends APD-destined traffic through the main table instead of the direct table.
tun2socksdadds anxray_vpnnftables table that marks forwarded tunnel traffic withxray_traffic_markand allows the pathslo -> tun,apd -> tun,tun -> wan, andtun -> apd.
Inside xrayvpnd, the active outbound selection is also split by protocol and destination:
- BitTorrent traffic goes direct.
- FTP ports
20-21go direct. - DNS traffic to
8.8.8.8,8.8.4.4, and1.1.1.1goes direct. - Traffic matching the subscription's routing rules (Russian CIDRs, geoip, geosite) goes direct.
- Remaining
tcpandudptraffic goes through the currently selected VLESS outbound.
In practice, that means:
- local service traffic for Transmission and Navidrome bypasses the VPN
- Russian and private destinations bypass the VPN
- everything else is sent into the tunnel and then proxied through the active VLESS link
- Make sure
python -m pip install pyinfra bcryptis done. - Make sure every required
passentry exists underhstd/.... - If
make debfails, installdpkg-deband related packaging tools or skip thehstdrole.
proxy_domainmust resolve toxray_proxy_addr.xhttp_source_domainmust resolve directly toxray_xhttpserver_addrif you deployxray_xhttpserver.- Update
letsencrypt_emailindeploy/xray.pyif needed.
- direct REALITY depends on
xray_server_addr,reality_pbk,reality_sni, andreality_sid - proxied REALITY depends on
xray_proxy_addrforwarding TCP:443toxray_server_addr:443 - XHTTP depends on deploying
xray_xhttpserver, plusxhttp_source_domain,xhttp_cdn_domain,xhttp_path, and working CDN origin configuration
Edit deploy/templates/hstd/hostapd.conf.j2. The SSID, channel settings, and country code are not inventory variables right now.
- Change server IPs, domains, and REALITY parameters in
deploy/xray.py, then redeploy the affected roles. - Change SSH users and role-specific host data in the inventory files, then redeploy.
- Certificates renew automatically via
certbot.timeronxray_proxyand onxray_xhttpserverif deployed. xrayvpn refreshrefreshes geodata onhstd.- If you rotate the REALITY key pair, update both
deploy/xray.pyandhstd/xray_server/reality_private_key, then redeployxray_server,xray_proxy, andhstdif used.