- Python 60%
- C++ 13.3%
- JavaScript 8.2%
- QML 8%
- Java 5.4%
- Other 5.1%
| .forgejo | ||
| .vscode | ||
| app-client | ||
| src/webshare | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .gitmodules | ||
| docker-compose.prod.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| pyproject.toml | ||
| README.md | ||
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/loginGET /api/appsGET /api/meGET|PATCH /api/profileGET|POST /api/usersGET|PATCH|DELETE /api/users/{id}GET|POST /api/serversGET|POST /api/servers/{id}/deploymentsGET|POST /api/clientsGET /api/clients/{id}/configGET /api/mobile/deploymentsGET|POST /api/mobile/clientsGET /api/mobile/clients/{id}/sing-boxGET /api/mobile/network-statusGET|PUT /api/clients/{id}/limitPOST /api/clients/{id}/trafficGET /api/reports/summaryGET /api/reports/trafficGET /api/reports/near-limitGET /api/settings/oidcPUT /api/settings/oidcGET /api/settings/smtpPUT /api/settings/smtpPOST /api/settings/smtp/testGET /api/auth/oidc/configPOST /api/auth/oidc/startGET /api/auth/oidc/callbackGET /api/exportPOST /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-corecontrolled 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/libboxtunnel 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-coreis bundled with the release archive;WEBSHARE_CORE=/path/to/webshare-corecan override the bundled helper;- the Linux installer adds a root-owned
webshare-coreand limitedwebshare-vpn-helperunder/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 callsudo, do not callpkexec, do not usesetcap, 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
mainbuild and push Docker image tagsmainandsha-<commit>; - Git tags matching
v*also publish a Docker image with the same tag; - pushes to
maincan 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, orNone; - 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
unstableorerror; - recovery emails when a previously unhealthy server becomes
onlineagain.
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 userandSSH 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
sudoaccess when WebShare needs to install Docker; - permission for the SSH user to run Docker. WebShare adds the user to the
dockergroup 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 examplecdn.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:
- Add the Origin under
Servers, including SSH credentials, and leaveauto_syncenabled. - Ensure host port
8003is free before the first deployment. The generated Origin binds it only on127.0.0.1, for access from Nginx on the same host. - Add
VLESS XHTTP TLS, keep deployment port8003, enter the three CDN names above, and chooseDirect final node. - Wait for the automatic operation or press
Deploy; verify the deployment isRunning/Activeand thewebshare-xray-xhttpcontainer is running. - Open
Connectionsand 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.
- Add
origin.acnas.netunderServerswith working SSH credentials and Docker. This one server owns both Origin containers. - Keep the existing Yandex resource for
cdn.acnas.net. Its HTTPS source, SNI, and sourceHostremainorigin.acnas.net. Disable CDN caching and allow the methods used by the working XHTTP entry, includingOPTIONS. - 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.
- Keep ports
8003and8004closed 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:
- Submit
VLESS XHTTP TLSwith port8003, the Direct column from the table, andDirect final node. Wait until it is Active. - Use the empty form on the same Origin card again. Submit
VLESS XHTTP TLSwith port8004, the Chained column, andRoute through existing VLESS Reality. Select the existingproxy.acnas.net / VLESS Reality / :6666deployment. Do not edit the Direct row. - 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). - 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. - Confirm that the Origin card contains two independent Active rows:
DirectandOrigin → 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/errorstatus;- 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.