No description
  • Python 60%
  • C++ 13.3%
  • JavaScript 8.2%
  • QML 8%
  • Java 5.4%
  • Other 5.1%
Find a file
mbulatov 1ec5ad5ef8
All checks were successful
CI / tests (push) Successful in 1m13s
CI / clients (push) Successful in 17m45s
CI / docker (push) Successful in 6s
CI / release (push) Has been skipped
CI / deploy (push) Successful in 26s
add chain host
2026-07-17 08:28:02 +03:00
.forgejo next stage 2026-06-02 14:03:22 +03:00
.vscode add chain host 2026-07-17 08:28:02 +03:00
app-client fix xhttp 2026-07-14 15:12:51 +03:00
src/webshare add chain host 2026-07-17 08:28:02 +03:00
tests add chain host 2026-07-17 08:28:02 +03:00
.dockerignore next stage 2026-05-29 01:07:12 +03:00
.env.example next stage 2026-05-31 11:42:10 +03:00
.gitignore next stage 2026-05-29 01:07:12 +03:00
.gitmodules next stage 2026-05-31 18:20:37 +03:00
docker-compose.prod.yml next stage 2026-05-31 11:42:10 +03:00
docker-compose.yml next stage 2026-05-31 11:42:10 +03:00
Dockerfile next stage 2026-05-31 11:42:10 +03:00
pyproject.toml next stage 2026-05-31 11:42:10 +03:00
README.md add chain host 2026-07-17 08:28:02 +03:00

WebShare

Small dependency-free VPN/proxy access panel inspired by the older webvpn project.

The old project mixed a public UI, NextAuth, FastAPI, PostgreSQL, Redis, Celery, SSH deployment, traffic collection and protocol-specific logic in one large system. This implementation keeps the useful domain model, but makes the first version intentionally boring:

  • one Python process;
  • SQLite storage;
  • no Node, Redis, PostgreSQL or task queue required;
  • built-in static web UI;
  • explicit protocol adapters for client config generation;
  • optional SSH/Docker deployment and monitoring;
  • QR code rendering for client configs;
  • web backup export/import without operation logs;
  • deployable with plain Python or Docker.

It is meant as a clean base that can grow. Remote SSH deployment and live traffic collectors can be added behind the existing deployment/client boundaries without changing the HTTP surface.

Quick Start

cd /home/acnas/projects/my/webshare
python3 -m src.webshare

Open:

http://127.0.0.1:8080

Default local admin:

username: admin
password: admin123

Change the password after the first login.

Configuration

Environment variables:

Variable Default Description
WEBSHARE_BIND 127.0.0.1 HTTP bind address
WEBSHARE_PORT 8080 HTTP port
WEBSHARE_HOST_PORT 8081 Docker host port mapped to container 8080
WEBSHARE_DB ./data/webshare.sqlite3 SQLite database path
WEBSHARE_SECRET generated on start HMAC token secret
WEBSHARE_ADMIN_USERNAME admin Seed admin username
WEBSHARE_ADMIN_EMAIL admin@webshare.local Seed admin email
WEBSHARE_ADMIN_PASSWORD admin123 Seed admin password
WEBSHARE_TOKEN_TTL_SECONDS 86400 Session lifetime
WEBSHARE_MONITOR_INTERVAL_SECONDS 60 Automatic SSH/Docker server check interval; 0 disables it
WEBSHARE_MONITOR_INITIAL_DELAY_SECONDS 10 Delay before the first automatic check
WEBSHARE_APP_REPOSITORY_URL https://forgejo.acnas.net/app/webshare Forgejo repository used by the web panel Apps download links
WEBSHARE_APP_RELEASE_CHECK_TIMEOUT_SECONDS 4 Timeout for checking the latest Forgejo release
WEBSHARE_GEOIP_CITY_DB /app/data/geoip/GeoLite2-City.mmdb in Docker Optional free MMDB City database path for client location lookup
WEBSHARE_GEOIP_ASN_DB /app/data/geoip/GeoLite2-ASN.mmdb in Docker Optional free MMDB ASN database path for ASN/organization lookup
WEBSHARE_GEOIP_AUTO_DOWNLOAD true in Docker Download missing GeoIP databases on startup
WEBSHARE_GEOIP_CITY_URL https://cdn.jsdelivr.net/npm/geolite2-city/GeoLite2-City.mmdb.gz City database download URL
WEBSHARE_GEOIP_ASN_URL empty Optional ASN database download URL
WEBSHARE_GEOIP_DOWNLOAD_TIMEOUT_SECONDS 30 GeoIP download timeout

For persistent sessions, set WEBSHARE_SECRET.

For built-in GeoIP location, Docker downloads a free City database automatically on startup when WEBSHARE_GEOIP_AUTO_DOWNLOAD=true and the file is missing. The default City source is the wp-statistics GeoLite2 package served through jsDelivr, so no paid account or license key is required. You can also provide any free MMDB-compatible City and ASN databases manually and place them at:

data/geoip/GeoLite2-City.mmdb
data/geoip/GeoLite2-ASN.mmdb

The Docker compose files already map these to /app/data/geoip/.... Restart or rebuild the web panel after changing the files so the GeoIP reader cache is refreshed. ASN/organization lookup requires GeoLite2-ASN.mmdb or a custom WEBSHARE_GEOIP_ASN_URL; the default auto-download source only provides City data.

API Shape

All API routes live under /api.

  • POST /api/auth/login
  • GET /api/apps
  • GET /api/me
  • GET|PATCH /api/profile
  • GET|POST /api/users
  • GET|PATCH|DELETE /api/users/{id}
  • GET|POST /api/servers
  • GET|POST /api/servers/{id}/deployments
  • GET|POST /api/clients
  • GET /api/clients/{id}/config
  • GET /api/mobile/deployments
  • GET|POST /api/mobile/clients
  • GET /api/mobile/clients/{id}/sing-box
  • GET /api/mobile/network-status
  • GET|PUT /api/clients/{id}/limit
  • POST /api/clients/{id}/traffic
  • GET /api/reports/summary
  • GET /api/reports/traffic
  • GET /api/reports/near-limit
  • GET /api/settings/oidc
  • PUT /api/settings/oidc
  • GET /api/settings/smtp
  • PUT /api/settings/smtp
  • POST /api/settings/smtp/test
  • GET /api/auth/oidc/config
  • POST /api/auth/oidc/start
  • GET /api/auth/oidc/callback
  • GET /api/export
  • POST /api/import

Tests

python3 -m unittest discover -s tests

No third-party packages are required.

Docker

docker compose up --build

Open the Docker deployment at:

http://127.0.0.1:8081

For production deployments use the image-based compose file:

docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d

By default it pulls:

forgejo.acnas.net/app/webshare:main

Override it with WEBSHARE_IMAGE in .env when you want to pin a specific tag or SHA image.

App Clients

app-client/ is the shared Qt 6.11.1 + QML client. The same QML interface is used for Android, Linux, and Windows, while platform behavior lives behind the C++ controller.

All app clients consume the same optimized sing-box VPN profile shape: IPv4-only TUN, system stack, MTU 1200, automatic routes, DoH DNS and full UDP routing without a blanket UDP/443 block. Platform code only handles how that profile is started and stopped.

The app checks /api/mobile/network-status through webshare.acnas.net to show the public IP, route status, API latency and location data before and after VPN connection. Country, city, ASN and organization are read from reverse-proxy or CDN headers such as CF-IPCountry, X-Country-Code, X-City, X-ASN and X-Organization. If those headers are not configured, WebShare falls back to free MMDB files from WEBSHARE_GEOIP_CITY_DB and WEBSHARE_GEOIP_ASN_DB; if the files are missing, the app still shows the observed public IP and marks location as unknown.

Current implementation status:

  • Linux: Qt/QML desktop app with tray, single-instance behavior, WebShare login, embedded OIDC, profile creation, MTProto Telegram links, update discovery, and bundled webshare-core controlled through the installed polkit helper;
  • Windows: Qt/QML app packaged as a Qt Installer Framework setup executable with bundled webshare-core.exe;
  • Android: the same Qt/QML UI and API flow with the native VpnService/libbox tunnel bridge built from the same submodule branch.
./app-client/build-in-docker.sh all
./app-client/build-in-docker.sh linux
./app-client/build-in-docker.sh linux-installer
./app-client/build-in-docker.sh windows
./app-client/build-in-docker.sh android

Artifacts are exported to:

app-client/dist/webshare-linux-x64.run
app-client/dist/webshare-windows-x64-setup.exe
app-client/dist/webshare-android-universal.apk

webshare-linux-x64.run is the Qt Installer Framework offline installer for Linux. It installs the system menu shortcut, root-owned webshare-core, and a polkit/systemd-managed VPN helper so active local users can connect/disconnect without repeated password prompts.

webshare-windows-x64-setup.exe is the Qt Installer Framework offline installer for Windows.

The Android APK is signed by the Docker build. Add these Forgejo secrets for a release signature, or omit them to get an installable debug-signed APK:

ANDROID_KEYSTORE_BASE64
ANDROID_KEYSTORE_PASSWORD
ANDROID_KEY_ALIAS
ANDROID_KEY_PASSWORD

The default Android artifact is a universal APK for arm64-v8a, armeabi-v7a, x86_64, and x86. Override QT_ANDROID_ABIS in CI if a smaller APK is needed.

The app client always uses https://webshare.acnas.net as its WebShare API endpoint. There is no server field in the app UI and older saved overrides are ignored.

The Account screen can check the latest Forgejo release through /api/apps and open the matching package download.

The client uses a simple screen model:

  • VPN: current connection, connect/disconnect, and available VPN profiles;
  • Telegram: MTProto proxy profiles and opening them in Telegram;
  • New: create a new connection from deployed protocols;
  • Account: refresh, logout, update checks, and runtime information.

OIDC login supports the standard authorization-code flow through a local callback listener and an embedded Qt WebView.

Runtime requirements:

  • webshare-core is bundled with the release archive;
  • WEBSHARE_CORE=/path/to/webshare-core can override the bundled helper;
  • the Linux installer adds a root-owned webshare-core and limited webshare-vpn-helper under /usr/libexec/webshare, plus a polkit rule that allows active local desktop users to manage WebShare VPN without repeated password prompts;
  • Linux installed builds use systemd-run/polkit and do not call sudo, do not call pkexec, do not use setcap, and do not store sudo passwords;
  • Linux needs systemd, polkit, and /dev/net/tun.

Android, Linux and Windows build the core from the same Git submodule: app-client/third_party/sing-box, tracked on the webshare-wrapper branch.

git submodule update --init --recursive app-client/third_party/sing-box
./app-client/build-in-docker.sh all

The core build uses golang:1.24.7-bookworm by default to match the current sing-box go.mod. If the WebShare wrapper branch moves to another supported Go version, set SING_BOX_GO_IMAGE in CI.

CI checks out submodules recursively. To update the helper, commit changes in the submodule branch, push that branch, then commit the new submodule pointer in this repository.

Desktop builds use the release tag list from the cloned repository and append the sing-box-lx client features with_xhttp, with_awg, and with_lx_command. Override SING_BOX_BUILD_TAGS only when replacing the whole tag list is intentional. By default the desktop build skips with_naive_outbound; Windows also skips tfogo_checklinkname0 to keep cross-compilation stable. Android's libbox builder enables the same downstream features directly for both AAR variants.

Linux installer:

chmod +x app-client/dist/webshare-linux-x64.run
./app-client/dist/webshare-linux-x64.run

The installer performs one elevated system-integration step and creates the desktop/app-menu launcher.

Keep polkit installed and running. Installed builds should not ask for the administrator password on every connect/disconnect after the installer has completed its elevated system-integration step.

If /dev/net/tun is missing, enable the TUN module on the host with sudo modprobe tun.

CI and local Docker builds can use a proxy for Qt, Android SDK, Gradle, pip, apt and curl downloads:

WEBSHARE_BUILD_PROXY='http://user:password@proxy.example.net:6669' ./app-client/build-in-docker.sh all

If the proxy value is copied from a WebShare profile and includes a name after #, the build script strips that fragment before passing it to HTTP clients.

Linux and Windows installers use WebShare's internal Qt Installer Framework build by default:

https://forgejo.acnas.net/app/qtif/releases/tag/v6.11.1-ifw4.11.0-2

The Docker build downloads the platform QtIF tools archive, verifies it with SHA256SUMS.txt, and then creates the offline installers. Override QTIFW_RELEASE_TAG, QTIFW_DOWNLOAD_BASE_URL, QTIFW_LINUX_TOOLS_URL or QTIFW_WINDOWS_TOOLS_URL in CI if a newer QtIF release should be used.

Forgejo CI And Releases

The repository includes .forgejo/workflows/ci.yml for Forgejo Actions:

  • pushes and pull requests run Python compilation and unit tests;
  • pushes to main build and push Docker image tags main and sha-<commit>;
  • Git tags matching v* also publish a Docker image with the same tag;
  • pushes to main can deploy the web panel over SSH with Docker Compose;
  • the Linux, Windows, and Android Qt/QML client packages are built in Docker from the Linux Forgejo runner;
  • the Linux and Windows user-facing installers are generated with Qt Installer Framework;
  • release assets are published only for Git tags matching v*.

Docker images are pushed to the Forgejo container registry using the repository path:

forgejo.acnas.net/app/webshare

For repository app/webshare, the pushed tags look like:

forgejo.acnas.net/app/webshare:main
forgejo.acnas.net/app/webshare:sha-abcdef123456
forgejo.acnas.net/app/webshare:v0.1.0

Forgejo Actions provides a temporary FORGEJO_TOKEN / GITHUB_TOKEN for the workflow, but many Forgejo versions/configurations do not allow that automatic token to push to the container registry. If docker push fails with 401 Unauthorized on /v2/.../blobs/uploads/, create a personal access token and add these Forgejo repository secrets:

REGISTRY_USER
REGISTRY_TOKEN
DEPLOY_HOST
DEPLOY_PORT
DEPLOY_USER
DEPLOY_SSH_KEY
DEPLOY_SSH_PASSWORD
DEPLOY_PATH
WEBSHARE_BUILD_PROXY
QT_MIRROR
ANDROID_KEYSTORE_BASE64
ANDROID_KEYSTORE_PASSWORD
ANDROID_KEY_ALIAS
ANDROID_KEY_PASSWORD

REGISTRY_USER is the Forgejo username that owns the token, for example your user or a dedicated deploy bot. REGISTRY_TOKEN is a Forgejo access token from:

User menu -> Settings -> Applications -> Access Tokens

Create a token that can write container/package artifacts for this repository. If your Forgejo token UI has scopes, grant package/container read and write permissions.

For SSH deploy, prefer DEPLOY_SSH_KEY. It must be a private key that can SSH to the production host. If you cannot use a key yet, set DEPLOY_SSH_PASSWORD instead; the workflow will use password auth through sshpass. DEPLOY_PORT is optional and defaults to 22. The remote user must be able to run docker and docker compose.

Prepare the production host once:

sudo mkdir -p /opt/webshare-panel
sudo chown "$USER:$USER" /opt/webshare-panel
cd /opt/webshare-panel
cp /path/to/.env.example .env

Edit /opt/webshare-panel/.env and set at least:

WEBSHARE_SECRET=long-random-secret
WEBSHARE_ADMIN_USERNAME=admin
WEBSHARE_ADMIN_EMAIL=admin@example.com
WEBSHARE_ADMIN_PASSWORD=strong-password
WEBSHARE_HOST_PORT=8081

Then set DEPLOY_PATH=/opt/webshare-panel in Forgejo secrets. On each push to main, CI builds the image, pushes it to Forgejo, copies docker-compose.prod.yml to DEPLOY_PATH, runs:

docker compose -f docker-compose.prod.yml pull webshare
docker compose -f docker-compose.prod.yml up -d --remove-orphans webshare

and checks /api/health from inside the container.

The client CI job uses the same Dockerized build as local development: app-client/build-in-docker.sh. The Forgejo runner must expose a Docker daemon to job containers. With a Docker-in-Docker runner, set the runner config to pass DOCKER_HOST into jobs and point container.docker_host at the DIND daemon.

To publish a release, push a tag:

git tag v0.1.0
git push origin v0.1.0

The tagged workflow creates or updates the Forgejo release and uploads:

webshare-linux-x64.run
webshare-windows-x64-setup.exe
webshare-android-universal.apk

The web panel Apps view reads the latest release from Forgejo and links to the available assets.

Backup And Restore

Admins can use the Backup view in the web panel to download a JSON backup or restore one through the browser.

The backup includes:

  • users and password hashes;
  • OIDC provider settings and linked external identities;
  • SMTP settings, including stored SMTP passwords;
  • servers, SSH settings, SSH passwords, and stored SSH private keys;
  • deployments, generated configs, clients, traffic limits, and traffic records.

The backup does not include operation logs, server check history, or audit logs. Treat the downloaded file as a secret because it contains credentials.

When importing with Replace current configuration, the current users are replaced by the backup users. After restore, sign in with an admin account from the restored backup.

By default, restore also queues a VPN sync for every restored deployment whose server has Auto deploy and sync clients enabled and SSH key or password credentials configured. That sync regenerates the runtime configs from the restored users/clients and runs remote Docker Compose with forced recreate, so the VPN containers receive the restored user list automatically. Deployments on servers without SSH credentials, or with auto-sync disabled, are skipped and can be applied later with Deploy.

OIDC Login

Admins can enable OIDC in Settings -> OIDC. The implementation uses the authorization-code flow with PKCE and verifies RSA-signed id_token values against the provider JWKS. Keycloak works with the standard realm issuer URL:

https://keycloak.example.com/realms/main

Configure the Keycloak client with this redirect URI, shown in the web panel:

https://your-webshare-host/api/auth/oidc/callback

Useful defaults:

  • scopes: openid email profile;
  • username claim: preferred_username;
  • email claim: email;
  • role claim path for Keycloak realm roles: realm_access.roles;
  • admin roles: admin,webshare-admin.

If Allow new OIDC users is enabled, first login creates a local WebShare user and links it to the OIDC issuer + sub. If the email already exists locally, WebShare links that existing account instead. Client secrets are stored in the local SQLite database, so keep data/webshare.sqlite3 private.

SMTP Settings

Admins can configure SMTP in Settings -> SMTP from the web panel.

Supported options:

  • host, port, and security mode: STARTTLS, SSL/TLS, or None;
  • optional username and password;
  • sender email and display name;
  • timeout for SMTP operations;
  • test recipient for sending a test email.

Passwords are stored in the local SQLite database and are included in backups. Use Save and test to verify the current settings without leaving the panel.

When SMTP is enabled, WebShare sends automatic emails for:

  • newly created user accounts;
  • password changes;
  • server/Docker checks that move from healthy to unstable or error;
  • recovery emails when a previously unhealthy server becomes online again.

Server check notifications are transition-based to avoid repeating the same alert on every automatic check.

User Profile

Every signed-in user has a Profile view. Users can change their email, display name, and password. Changing the password requires the current password. Admins can still manage roles and account status from Users.

Remote Deployment

Servers can be managed automatically over SSH.

When adding a server in the UI, fill:

  • Host: SSH host or IP address;
  • Public host: domain/IP that clients will connect to;
  • SSH user and SSH port;
  • Deploy path: remote directory for generated runtime files, default /opt/webshare;
  • SSH private key: private key with Docker access on the remote host;
  • SSH password: optional password auth when a key is not provided;
  • Install Docker if missing: checks Docker Engine and Docker Compose v2 over SSH and installs them when missing;
  • Auto deploy and sync clients: when enabled, deployments and client changes are applied automatically.

The remote host must have:

  • SSH access for the configured user;
  • root or passwordless sudo access when WebShare needs to install Docker;
  • permission for the SSH user to run Docker. WebShare adds the user to the docker group during provisioning when possible.

Key authentication is preferred. Password authentication uses sshpass and is supported by the Docker image. If you run WebShare directly on the host, install sshpass and openssh-client there too. The password is stored in the local SQLite database, so keep data/webshare.sqlite3 private.

Client config QR codes use the system qrencode utility. The Docker image includes it. If you run WebShare directly on the host, install qrencode to see QR codes in the config modal.

What Happens On Deploy

For each protocol deployment WebShare generates a small runtime bundle and uploads it to:

/opt/webshare/<deployment-id>/

VLESS XHTTP uses the same flat path on its Origin server. A chained XHTTP deployment references an existing VLESS Reality deployment; it does not create another runtime directory on the Reality server. That deployment keeps its own /opt/webshare/<reality-deployment-id>/ directory and container.

Then it runs:

docker compose pull
docker compose up -d --remove-orphans --force-recreate

Adding, deleting, or changing clients rebuilds the runtime config and syncs the deployment again when auto_sync is enabled. Containers are force-recreated during sync because most VPN/proxy daemons read their user config only at startup.

Use Version on a deployment to read the currently running Docker container and image information from the remote host. WebShare stores the latest image tag, image id, container status, and timestamps on the deployment.

Use Update on a deployment to pull its configured image reference, recreate the VPN container with the current generated config, and then refresh the stored Docker version information.

Every deployment has a Runtime Docker image tag field in Basic settings (or image_tag in Config JSON). WebShare fixes the repository for each protocol and accepts only a Docker tag such as 26.7.11, 2, or latest; arbitrary image repositories and digests cannot be supplied through the panel. The defaults are Xray 26.7.11, Hysteria and 3proxy latest, and MTG 2. Existing deployment snapshots without image_tag are backfilled with that protocol default during the database migration; the remote container changes only on its next Deploy, Update, or automatic sync. For Hysteria2 this setting controls the Hysteria runtime image only; the short-lived OpenSSL certificate helper remains managed internally by WebShare.

For VLESS Reality and XHTTP, the default 26.7.11 tag resolves to the audited official multi-architecture image pinned by digest. WebShare also retains the audited digest for known older Xray tags, so an explicitly selected version does not lose its pin after a future panel upgrade. Unknown concrete tags resolve to the protocol's fixed repository and remain publisher-mutable. latest is deliberately floating: every Deploy, Update, or automatic client sync runs docker compose pull, so it can download and restart a different image without a panel upgrade. Prefer a concrete version unless that behavior is intentional. A chained XHTTP deployment and its selected Reality deployment keep independent image tags.

A chained XHTTP deployment adds its private service credential to the selected, already existing VLESS Reality deployment; it does not create a second protocol runtime there.

If WebShare restarts during Deploy or Update, the interrupted operation is marked with a warning and its deployment is queued again. A chained deployment ensures its service credential is active on the referenced Reality deployment before applying the Origin configuration that uses it.

Deleting a client removes it from the local database and, when the deployment's server has auto-sync and SSH credentials, queues a sync so the remote VPN config is regenerated without that user. Deleting a deployment or server first marks its records as deleting and queues remote cleanup. WebShare runs Docker Compose down with orphan and volume cleanup in the deployment directory and removes the remote deployment folder. Deleting a chained XHTTP deployment also removes only its internal service credential from the referenced Reality deployment and reapplies that existing runtime; it never deletes the Reality deployment or its ordinary Connections. Only after cleanup succeeds are the local records deleted. Failed or interrupted cleanup keeps the metadata and is retried after restart or by pressing Delete again. The server host, SSH target, and deploy path are frozen while cleanup is pending so a retry cannot silently clean the wrong machine; restore reachability at that same target before retrying. The SSH credentials button remains available while a server is deleting, so a wrong key or password can be repaired before Retry cleanup.

A VLESS Reality deployment referenced by an XHTTP chain, or the server that owns it, cannot be deleted until the dependent chain is deleted. This prevents a working Origin from being left with a missing upstream.

Runtime Files

Supported runtime generation:

Protocol Runtime behavior
socks5 Runs 3proxy/3proxy:latest and generates 3proxy.cfg with all active users
http_proxy Runs 3proxy/3proxy:latest and generates 3proxy.cfg with all active users
hysteria2 Generates hysteria.yaml with user/password auth and a self-signed cert volume
mtproto Runs nineseconds/mtg:2 with an mtg-v2 compatible fake-TLS secret
xray_vless_reality Generates xray.json; auto-generates a Reality keypair on first remote deploy when keys are omitted; explicitly controls the minimum accepted client version
xray_vless_xhttp_tls Deploys an XHTTP Origin as a direct final node or routes it through an existing VLESS Reality deployment; regenerates its per-user VLESS inbound on every client sync

For xray_vless_reality, WebShare tries to generate a matching private_key / public_key pair on the remote host by running Xray in Docker during deploy. You can also provide both keys manually in Config JSON.

Example:

{
  "server_name": "www.cloudflare.com",
  "public_key": "REALITY_PUBLIC_KEY",
  "private_key": "REALITY_PRIVATE_KEY",
  "short_id": "abcd1234",
  "min_client_ver": "1.0.0",
  "image_tag": "26.7.11"
}

min_client_ver defaults to 1.0.0 so Xray 26.7.11 does not silently reject existing Reality clients. Raise this value in the deployment only after all clients have been upgraded. After upgrading WebShare, press Deploy once on an existing Reality deployment to write this setting into its remote xray.json.

VLESS XHTTP deployment

xray_vless_xhttp_tls is one logical panel deployment with two routing modes: Direct final node and Route through existing VLESS Reality. WebShare uploads a pinned Xray 26.7.11 runtime to the Origin, publishes its XHTTP inbound on 127.0.0.1:8003 by default, and puts every active WebShare Connection into the generated xray.json with a separate UUID. Chain mode selects a concrete, already deployed VLESS Reality entry as its upstream. It reuses that deployment instead of creating another protocol, container, domain, certificate, or Xray inbound. Chain mode may publish a second host port (by default 8443) to the existing Reality container's unchanged listener.

Creating, disabling, or deleting a connection, disabling/deleting its user, or reaching a hard traffic limit queues a deployment sync when server auto_sync is enabled. Wait for that operation to finish before testing a new profile or assuming a UUID has been revoked. If automatic sync is unavailable, the panel explicitly asks you to press Deploy manually.

The existing HTTP edge remains outside this Docker deployment: configure Yandex Cloud CDN, its DNS/certificate, and Nginx once, then point the Nginx XHTTP location at 127.0.0.1:8003. Do not expose Origin port 8003 through the router. If Nginx runs in a separate container that cannot reach host loopback, use a deliberate Docker network or change bind_address; do not open 8003 to the Internet without a firewall.

Keep the Origin's existing OPTIONS -> POST method map and disable both proxy response buffering and request buffering for the XHTTP location. Its path must match the deployment path (here /api-test).

Keep the three CDN names separate:

  • client_server: address dialed by the client, normally the provider topology hostname rather than the custom domain;
  • server_name: TLS SNI/certificate name accepted by that CDN address;
  • http_host: HTTP Host that selects your CDN resource, for example cdn.acnas.net.

The deployment port is the Origin listener (8003); client_port remains 443. No deployment-wide user UUID is accepted: WebShare generates one UUID per connection.

Direct/final-node Origin example:

{
  "bind_address": "127.0.0.1",
  "client_server": "053a1e6a20302eed.topology.gslb.yccdn.ru",
  "client_port": 443,
  "server_name": "yccdn-mar-16.yccdn.cloud.yandex.net",
  "alpn": ["h2"],
  "fp": "chrome",
  "http_host": "cdn.acnas.net",
  "path": "/api-test",
  "mode": "packet-up",
  "headers": {
    "Content-Type": "application/octet-stream"
  },
  "sc_max_each_post_bytes": "1000000-1000000",
  "sc_min_posts_interval_ms": "30-30",
  "uplink_http_method": "OPTIONS",
  "x_padding_obfs_mode": true,
  "x_padding_placement": "queryInHeader",
  "x_padding_key": "dc",
  "x_padding_header": "X-Cache",
  "x_padding_method": "tokenish",
  "egress": {
    "type": "direct"
  }
}

This produces:

Client -> Yandex CDN -> Nginx -> managed Xray Origin -> Internet

Direct deployment order in the panel:

  1. Add the Origin under Servers, including SSH credentials, and leave auto_sync enabled.
  2. Ensure host port 8003 is free before the first deployment. The generated Origin binds it only on 127.0.0.1, for access from Nginx on the same host.
  3. Add VLESS XHTTP TLS, keep deployment port 8003, enter the three CDN names above, and choose Direct final node.
  4. Wait for the automatic operation or press Deploy; verify the deployment is Running/Active and the webshare-xray-xhttp container is running.
  5. Open Connections and create a connection for each user/device. Every row receives a different UUID and queues another deployment sync. Wait until that operation succeeds before importing or testing the profile.

Run Direct and chained entries simultaneously

The Direct and chained entries can reuse the same Yandex CDN resource, topology address, TLS SNI, custom HTTP Host, source, and certificate. They need different loopback listener ports and XHTTP paths so Nginx can route each request to the correct Origin container:

WebShare field Direct entry Chained entry
Deployment port 8003 8004
Origin bind address 127.0.0.1 127.0.0.1
Client server / CDN address 053a1e6a20302eed.topology.gslb.yccdn.ru 053a1e6a20302eed.topology.gslb.yccdn.ru
Client port 443 443
TLS server name (SNI) yccdn-mar-16.yccdn.cloud.yandex.net yccdn-mar-16.yccdn.cloud.yandex.net
HTTP Host cdn.acnas.net cdn.acnas.net
XHTTP path /api-test /api-chain
Final destination Direct final node Route through existing VLESS Reality
Existing VLESS Reality deployment select the running proxy.acnas.net / VLESS Reality / :6666 deployment
Inner Reality published port 8443
Inner Reality fingerprint hellochrome_120

These are two independent entries that remain active together. Do not edit the Direct deployment and turn it into a chain: submit the deployment form a second time. They share the physical Origin and Nginx 443/tcp, but have different deployment IDs, loopback listeners, XHTTP paths, and user Connections.

  1. Add origin.acnas.net under Servers with working SSH credentials and Docker. This one server owns both Origin containers.
  2. Keep the existing Yandex resource for cdn.acnas.net. Its HTTPS source, SNI, and source Host remain origin.acnas.net. Disable CDN caching and allow the methods used by the working XHTTP entry, including OPTIONS.
  3. On a different server, keep an existing VLESS Reality deployment Active and verified. It must be managed by WebShare over SSH and use direct egress so WebShare can add a private service credential without creating a visible Connection.
  4. Keep ports 8003 and 8004 closed to the Internet. Only Nginx connects to these loopback listeners.

The existing map belongs once in Nginx's http context. Put both locations inside the existing HTTPS server block for origin.acnas.net:

map $request_method $xhttp_proxy_method {
    default  $request_method;
    OPTIONS  POST;
}

# Inside: server { listen 443 ssl; server_name origin.acnas.net; ... }
location ^~ /api-test {
    proxy_pass http://127.0.0.1:8003;
    proxy_method $xhttp_proxy_method;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_pass_request_headers on;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_request_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

location ^~ /api-chain {
    proxy_pass http://127.0.0.1:8004;
    proxy_method $xhttp_proxy_method;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_pass_request_headers on;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_request_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

Create the deployments from the Origin server card in this order:

  1. Submit VLESS XHTTP TLS with port 8003, the Direct column from the table, and Direct final node. Wait until it is Active.
  2. Use the empty form on the same Origin card again. Submit VLESS XHTTP TLS with port 8004, the Chained column, and Route through existing VLESS Reality. Select the existing proxy.acnas.net / VLESS Reality / :6666 deployment. Do not edit the Direct row.
  3. Wait for the chain deployment to become Active. WebShare adds a private, stable service UUID to the selected Reality inbound and applies that deployment before starting the second Origin container. Its Compose runtime publishes 8443:<existing-Reality-container-port>; the existing public port and all old direct Reality profiles remain unchanged. Allow the Origin to reach this TCP port (preferably restrict it to the Origin's IP).
  4. Under Connections, create one connection for the Direct deployment and a second connection for the chained deployment. The same user may own both; each receives its own UUID/profile.
  5. Confirm that the Origin card contains two independent Active rows: Direct and Origin → proxy.acnas.net / VLESS Reality :6666. Deploying or deleting one XHTTP entry does not replace the other.

The three existing runtimes keep independent paths:

# Origin server
/opt/webshare/<direct-deployment-id>/
/opt/webshare/<chain-deployment-id>/

# NL server; this path already belongs to the selected Reality deployment
/opt/webshare/<reality-deployment-id>/

The chain deployment ID is never installed on the NL server. WebShare updates the selected Reality deployment's existing xray.json and container, preserving its normal user Connections alongside the private service credential.

The persisted relationship contains the selected deployment ID plus two non-secret controls for the private hop:

{
  "egress": {
    "type": "reality_deployment",
    "deployment_id": "VLESS-REALITY-DEPLOYMENT-UUID",
    "dial_port": 8443,
    "fingerprint": "hellochrome_120"
  }
}

The address, SNI, Reality public key, and Short ID are derived from the referenced deployment whenever WebShare builds the Origin runtime. Do not copy those values into the chain configuration. dial_port is a host-side publish/dial port: the Reality container still listens on its original deployment port. The inner fingerprint is independent of both the XHTTP client-facing fp and the direct Reality deployment's fp. hellochrome_120 is the default for new panel forms; firefox may be selected for an explicit A/B test without changing either outer profile.

Old stored chains without dial_port continue to dial the Reality deployment's main port and do not add a Docker port mapping. An explicitly stored inner fingerprint is preserved; when it is absent, materialization uses the stable hellochrome_120 inner-hop default. The service UUID remains owned by WebShare and is not duplicated in config_json or exposed as an end-user Connection. The internal chain credential intentionally has no flow; ordinary Reality user credentials keep the deployment's configured flow (normally xtls-rprx-vision).

One Deploy action validates the dependency, applies and verifies the selected Reality deployment with the service UUID, and only then applies the Origin. If the upstream apply fails, WebShare does not switch the Origin to a credential that is not active.

The chain is:

Client -> Yandex CDN -> Nginx -> XHTTP Origin -> existing VLESS Reality -> Internet

Split traffic between Origin and the Reality exit

A chained XHTTP deployment can apply one server-side routing policy to all of its Connections. In the Basic editor, use:

  • Default traffic route: where unmatched traffic exits;
  • Exception domains: one Xray domain matcher per line;
  • Exception IP / CIDR / GeoIP entries: one address or matcher per line.

Exceptions always use the route opposite to the selected default. For example, to send Russian services from the Origin while everything else continues to the Reality exit, select Reality chain. Enter these domain exceptions:

domain:gosuslugi.ru
domain:nalog.gov.ru

Then enter this GeoIP exception:

geoip:ru

The persisted non-secret policy has this shape:

{
  "egress": {
    "type": "reality_deployment",
    "deployment_id": "VLESS-REALITY-DEPLOYMENT-UUID",
    "routing": {
      "default": "chain",
      "domains": ["domain:gosuslugi.ru", "domain:nalog.gov.ru"],
      "ips": ["geoip:ru"]
    }
  }
}

chain exits from the selected Reality server; direct exits from the Origin. An existing chain with no saved policy keeps sending all traffic through Reality; adding its first exception automatically makes Reality the default route. Domain exceptions automatically enable route-only HTTP/TLS/QUIC sniffing. IP exceptions use Origin-side DNS resolution when the client supplied a domain. Traffic whose domain cannot be observed, including some ECH traffic, falls back to an IP exception or the default route. Avoid geoip:private unless Connections must intentionally reach private networks visible from the Origin. Policy edits do not change client URIs or the Reality service UUID, but existing sessions must reconnect before all new rules take effect.

Several chains may reuse one Reality deployment; each receives a different service UUID and may share the same dedicated published port. A dedicated host port cannot be claimed by another deployment or by a chain targeting a different Reality container on the same server; the panel rejects such conflicts before writing runtime files. Deleting one chain removes only its UUID and Origin runtime, then reapplies Reality while preserving the credentials of its ordinary users and other chains. Recreating that shared Reality container can briefly reconnect their active sessions. A referenced Reality deployment and its server cannot be deleted until all chains that use it are deleted. If Reality or its server becomes unavailable, dependent XHTTP chains are excluded from connection choices until the dependency is healthy again.

As soon as the first Deploy attempt begins, the route topology is intentionally fixed. Direct versus chained mode and the selected Reality deployment can only be changed by deleting and recreating the XHTTP deployment. Ordinary CDN/XHTTP settings and user Connections remain editable.

The deployment's Origin/listener port is also frozen after its first Deploy attempt. Delete and recreate the deployment to move that listener to another port; this keeps the old live port reserved until its runtime is cleaned.

XHTTP range values stay JSON strings. Do not add xtls-rprx-vision or Reality to the client XHTTP profile. The Origin uses a separate service credential for the internal Reality hop, without Vision flow. For an HTTP CDN Origin use explicit packet-up.

For mtproto, WebShare generates an mtg v2 compatible secret automatically. You can override the fake-TLS domain with:

{
  "fronting_domain": "telegram.org"
}

Older MTProto deployments that still have a legacy 32-hex secret are repaired on the next Deploy; WebShare regenerates the secret and refreshes the client links before starting the remote container.

Monitoring

The Check button on a server runs remote Docker inspection:

docker ps -a
docker stats --no-stream

WebShare stores the latest check result on the server record and keeps historical server_checks. Monitoring covers Docker/container status, restart count, healthcheck status, exit code, CPU, memory, and container network I/O. Exact per-client traffic still depends on protocol-specific collectors and can be added behind the protocol adapter layer.

Container network I/O is also saved as deployment traffic. The first successful check stores a baseline from Docker counters; the next checks save RX/TX deltas, so the dashboard traffic and Reports graphs move automatically while VPN containers are passing traffic.

Server activity is considered online when the SSH command succeeds and Docker responds successfully. If SSH fails, Docker is unavailable, or the user cannot run Docker, the check is saved as error and the command output is kept in the check payload for debugging.

Deployment Deploy and Version also inspect the resulting Compose containers. If Docker Compose exits successfully but the VPN container immediately exits, restarts, reports unhealthy, or recent logs contain known fatal markers, the deployment is marked error and the Operations details show the container state, exit code, restart count, health status, and recent logs.

Successful one-shot helper containers, such as Hysteria certgen finishing with Exited (0), are treated as healthy. Server-wide checks also ignore stopped containers from old WebShare Compose projects that are no longer present as active deployments in the local database.

WebShare also runs automatic checks in the background. By default it checks every 60 seconds all active servers that have SSH key or password credentials. Set WEBSHARE_MONITOR_INTERVAL_SECONDS=0 to disable automatic monitoring.

The Overview, Servers, Operations, and Reports views refresh themselves while open, so the latest server status appears without pressing refresh.

Operation Logs

Deploy, update, version, and server check actions run as background operations. The Operations view shows:

  • operation type and target;
  • running / success / error status;
  • current message;
  • command log and final JSON result.

The view refreshes automatically every few seconds while it is open. Routine actions no longer force-switch you to Operations; they show a short operation id in the notice area, and you can open the full details when needed.