Accounts & delivery

Protocols

Vortexa provisions SSH / SSH-WS, VMess, VLESS, Trojan and ZIVPN from the same account form. Each one lands differently on the node — this is what actually happens per protocol.
ProtocolTransportsAuthNode service
SSH / SSH-WSRaw TCP, WebSocket, TLS via stunnelUsername + password (Linux user)sshd/Dropbear plus autoscript-node-ssh-ws and stunnel
VMessws, httpupgrade, xhttp, grpcUUIDXray inbound behind Nginx
VLESSws, httpupgrade, xhttp, grpc (+ flow, e.g. xtls-rprx-vision)UUIDXray inbound behind Nginx
Trojanws, httpupgrade, xhttp, grpcPasswordXray inbound behind Nginx
ZIVPNUDP with shared OBFS passwordPassword from a shared listzivpn service with /etc/zivpn/config.json

The xray alias

Creating an account with protocol xray always provisions VLESS. Tick multiple Xray protocols to get sibling rows sharing one UUID.

Every Xray inbound binds to 127.0.0.1. Nginx owns the real, Cloudflare-compatible public ports and proxies inward, which is what makes CDN fronting and shared certificates possible.

local inbound ports on each node
10001 vmess-ws      10011 vmess-xhttp      10021 vmess-httpupgrade
10002 vless-ws      10012 vless-xhttp      10022 vless-httpupgrade
10003 trojan-ws     10013 trojan-xhttp     10023 trojan-httpupgrade
10085 api-in (stats / gRPC handler API)

base offsets: ws=10000, xhttp=10010, httpupgrade=10020, tcp=10030, grpc=10040
protocol index: vmess=1, vless=2, trojan=3

Path convention

  • WebSocket: /vless, /vmess, /trojan.
  • xHTTP: /vless-xh and siblings.
  • HTTPUpgrade: /vless-hu and siblings.
  • gRPC: a bare serviceName such as vless-gr, with no leading slash.

Stream tuning applied to every inbound

FAST_SOCKOPT
{
  "tcpNoDelay": true,
  "tcpFastOpen": true,
  "tcpcongestion": "bbr",
  "tcpKeepAliveIdle": 30,
  "tcpKeepAliveInterval": 15
}

Accounting identity

The Xray “email” used for stats is the account’s stable account id, not the username — usernames may legitimately collide across planes, ids never do.

xHTTP mode narrowing

Even when an account or CDN lists several xHTTP modes, the node picks exactly one and prefers stream-up. packet-up and auto generate many small upload POSTs that badly hurt upload throughput through Nginx and CDN edges.

SSH accounts are real Linux users, created by the provisioning script with a hardened sshd drop-in. Understanding this removes most SSH support tickets.

  • Expiry padding. Linux account expiry is date-granular and disables at start-of-day, so the script sets the expiry to the day after EXPIRES. Exact-hour and quota enforcement is done separately by the agent.
  • Login shell wrapper. /usr/local/bin/grvpn-motd prints the banner (fetched from the local agent, or /etc/grvpn-ssh-banner.html), strips HTML, then execs a real shell. Many tunnel clients (HTTP Custom, Injector, HA Tunnel, NetMod) need an interactive PTY behind sshd or they see immediate EOF.
  • Hardening in sshd_config.d/99-grvpn.conf: password and keyboard-interactive auth on, PermitOpen restricted to the Cloudflare-supported port list so local forwards cannot pivot to arbitrary internal hosts, PermitListen none (remote -R forwards fully disabled), GatewayPorts no, X11Forwarding no.
  • Drop-in repair. Earlier over-restrictive drop-ins (AllowTcpForwarding no, DisableForwarding yes) are stripped, the managed user is appended to any existing AllowUsers (OpenSSH is first-match) and removed from DenyUsers.
  • Password handling. The script hashes with openssl passwd -6 (or Python crypt) and applies it via usermod -p, bypassing PAM cracklib rejecting short or username-like passwords.
  • Unlock logic. configure_login_user() creates or updates the user, sets shell, expiry and hash, force-unlocks (usermod -U / passwd -u) and removes /etc/nologin — the usual cause of “account exists but is locked from a previous revoke”.

Node SSH accounts are tunnel-only

On remote nodes the agent installs a different wrapper that prints the banner and parks forever. It never execs a shell and never runs remote commands, because node SSH accounts exist only to hold a port-forwarding tunnel open.
  1. 1
    Ensure Xray is installed — setup_xray.sh runs if the binary or /usr/local/etc/xray/config.json is missing.
  2. 2
    Add the client to the correct inbound via xray_client.py add <proto>.
  3. 3
    Validate with xray run -test before restarting the service. A failed config test is exactly what produces an account stuck in pending.
  4. 4
    Restart Xray, or for suspensions use the gRPC handler API (xray api rmu -tag=<inbound> <email>) to kick a single user instantly without dropping everyone else’s sessions.

Xray privileges on the node

The agent writes a systemd override granting Xray CAP_NET_ADMIN, CAP_NET_BIND_SERVICE and CAP_NET_RAW and running it as root. This is required: speed caps rely on setsockopt(SO_MARK), and the upstream unit runs as nobody, which silently drops the mark and makes speed limits a no-op. Log paths /var/log/xray/access.log and error.log are created world-writable so Xray starts even when running unprivileged.

  • ZIVPN has no per-user config: /etc/zivpn/config.json holds auth.config, the array of all valid passwords. With no accounts it becomes ["disabled"], which locks out every client.
  • The OBFS password is fleet-wide and identical on every node, so one client profile works everywhere. The service restarts only when the value actually changes.
  • Egress is plane-wide: the outbound chosen for the zivpn protocol key becomes the whole node’s ZIVPN upstream (WARP → local SOCKS5, address+port → that SOCKS5, otherwise direct).
  • Installing the tunnel is a prerequisite — account creation returns 400 until Tunnels → ZIVPN UDP is installed on the target host.

Per-account outbound does nothing for ZIVPN

Setting an individual ZIVPN account’s outboundId has no effect on the node. Only the plane-wide policy for the zivpn key matters.

Each account with a non-zero speed cap gets its own cloned outbound tagged mk-<mark> with a unique sockopt.mark, plus a routing rule sending that user through it. tc then shapes by socket mark instead of client IP — which is what makes caps work behind CDNs, on IPv6 and before the first packet is seen.

  • Speed caps work for WARP-routed users too: the mark travels through the WireGuard outbound's own socket.
  • Blocking rules are inserted ahead of speed-mark routes, so blocking always wins.
SituationRecommendation
Cheap mobile tunnel clients, carrier bundlesSSH-WS or SSH over TLS on a CDN-friendly port
Best throughput on modern clientsVLESS over WebSocket or HTTPUpgrade behind Nginx
Heavily filtered networksVLESS/Trojan on 443 TLS through a CDN with an SNI override
Simple UDP-friendly offeringZIVPN with the fleet-wide OBFS password
Legacy client compatibilityVMess over WebSocket