Ships the extension small (~50 KB tarball). No binaries in it — the
platform-matched sing-box is downloaded on first "Turn on" from
bns/theseus.x/vpn-binaries/<platform>/, sha256-verified against the
manifest that ships inside this operator-signed tarball, and cached
under extensions-data/vpn/bin/. Every subsequent launch re-verifies
before spawning; a mismatch redownloads rather than trusts what is on
disk.
Config generator produces a sing-box config from a vless:// URL (the
shape a 3x-UI VLESS+Reality inbound produces), plus a SOCKS5 inbound
on 127.0.0.1:<ephemeral>. api.setSessionProxy points every Theseus
request at that port while the tunnel is up; child.on("exit") clears
it if sing-box dies. Off again clears the proxy back to whatever the
browser had.
Panel is a big on/off toggle with a status pill, a paste-and-save
endpoint box, and an Advanced disclosure with "auto-on at browser
start", "re-download binary" and "clear cache". Any user with a
vless:// URL can flip it on today; the free tier and the Silent Mode
exit inbound are the server-side half, documented under DESIGN.md.
Binary manifest ships with PENDING sha256s until the binaries are
uploaded to Sia — ensureBinary refuses to activate on a platform whose
sha256 is PENDING, so a user cannot flip it on against an unverified
download.
159 lines
6.7 KiB
Markdown
159 lines
6.7 KiB
Markdown
# VPN extension — design notes
|
|
|
|
Client is done in v0.1.0. This file covers **what the client expects the
|
|
server side to look like**, so the two halves can be built independently
|
|
without either side inventing a contract the other did not agree to.
|
|
|
|
## Model, in one line
|
|
|
|
3x-UI + xray-core on our VPS provides a VLESS+Reality inbound; the client
|
|
runs sing-box locally and speaks that inbound; a small key-issuer service
|
|
on our gateway mints per-user client entries in the 3x-UI panel on demand.
|
|
This is the SoloBot / YAK VPN pattern with the payment stack, the referral
|
|
program and the Telegram-bot UI stripped out — everything Silent Mode does
|
|
not care about — and with the client wired to Theseus's session proxy
|
|
instead of a system-wide tunnel.
|
|
|
|
Anchors:
|
|
- `D:\Dev\VPN\vpn_bot` — the original bot the code and inbound layout are
|
|
adapted from. `client.py` (its `py3xui` calls) is the reference for our
|
|
key-issuer.
|
|
- `D:\Dev\VPN\VPN module` — the YAK web frontend the payment-less flow
|
|
descends from.
|
|
|
|
## Server side (VPS)
|
|
|
|
1. **Install 3x-UI** as a Docker container or systemd service. One VLESS
|
|
inbound, Reality mode, port 443, `flow=xtls-rprx-vision`, one Reality
|
|
short-id per operator server.
|
|
2. **Provision at least one inbound per exit country** (`nl-1`, `de-1`, …)
|
|
and record its `inbound_id` in the key-issuer's config. Multiserver is
|
|
phase-two; v1 ships one exit.
|
|
3. **Key-issuer** — small FastAPI service (or Node in Argus, matching our
|
|
stack) that:
|
|
- Reads 3x-UI's local API on `127.0.0.1:2053`.
|
|
- Exposes one POST endpoint the extension calls (below).
|
|
- Talks to Aegis-side wallet signatures when a request claims to be paid.
|
|
|
|
### `POST /api/vpn/new-client`
|
|
|
|
Request body:
|
|
```json
|
|
{
|
|
"identity": "bch:<address>" | "anon:<128-bit random>",
|
|
"tier": "free" | "paid",
|
|
"duration_days": 1, // free tier ignores this; paid honours it
|
|
"signature": "…" // wallet signature over the JSON, iff tier=paid
|
|
}
|
|
```
|
|
|
|
Response:
|
|
```json
|
|
{
|
|
"vless": "vless://…",
|
|
"expires_at": 1727000000,
|
|
"quota_bytes": 524288000 // 500 MB for free tier per day
|
|
}
|
|
```
|
|
|
|
Rules:
|
|
- `anon:` requests get one key per source IP per 24h, hard-capped at
|
|
500 MB / 30 minutes of active use (enforced in the xray inbound's
|
|
`total` and `expiry`). One key at a time — reissuing supersedes.
|
|
- `bch:` requests without a signature get the same free-tier treatment.
|
|
- `bch:` requests with a valid signature and a `tier=paid` claim get a
|
|
key sized to what their signed message says. `duration_days` and the
|
|
price paid live in the signed message. Payment verification is a
|
|
separate concern (see next section).
|
|
|
|
### Paid tier
|
|
|
|
BCH-native, no Stripe. Options in decreasing order of Silent Mode-fit:
|
|
|
|
1. **BCH invoice covenant on Aegis** — the extension posts to
|
|
`/api/vpn/invoice` which returns an address + memo. User pays from
|
|
Aegis. Gateway watches the address, unlocks the corresponding key
|
|
when the tx clears. Same on-chain-watcher pattern the marketplace
|
|
already uses in Argus.
|
|
2. **Sirius Studio hosted key sale** — a page under `theseus.x/vpn/buy`
|
|
that runs an offer covenant (like the name-sale covenants shipped
|
|
2026-09-17); paying it produces a signed receipt the extension shows
|
|
to `/api/vpn/new-client`. Consistent with the name-marketplace flow.
|
|
|
|
Either way, no card processor, no email, no account. A signed receipt +
|
|
a fresh `bch:` identity is the whole login.
|
|
|
|
## Binary hosting
|
|
|
|
sing-box binaries live at:
|
|
|
|
```
|
|
bns/theseus.x/vpn-binaries/win32-x64/sing-box-<version>.exe
|
|
bns/theseus.x/vpn-binaries/linux-x64/sing-box-<version>
|
|
bns/theseus.x/vpn-binaries/darwin-arm64/sing-box-<version>
|
|
```
|
|
|
|
Publisher: operator, uploaded with `sia-upload.js`. The addon's
|
|
`binary-manifest.json` pins the sha256 for each platform; that manifest
|
|
ships inside the operator-Ed25519-signed addon tarball, so an attacker
|
|
who compromises Sia cannot swap a binary without matching the pinned
|
|
hash, and cannot change the pinned hash without the operator OTA key.
|
|
|
|
To publish a new sing-box version:
|
|
|
|
1. Download upstream from `github.com/SagerNet/sing-box/releases`,
|
|
record the upstream sha256.
|
|
2. `sia-upload.js sing-box-<version>.exe bns/theseus.x/vpn-binaries/win32-x64`
|
|
(and the two other platforms).
|
|
3. Bump `bundled-addons/vpn/binary-manifest.json` — new sha256s, new URLs,
|
|
new version.
|
|
4. Ship a new addon tarball via the Ed25519 OTA channel — same
|
|
`sign-addon-update.mjs` flow as translate and pdf-editor.
|
|
|
|
Users get the new manifest via OTA, notice their cached sha256 no longer
|
|
matches, and re-download.
|
|
|
|
## System-wide vs Theseus-only
|
|
|
|
v1 = Theseus-only. `api.setSessionProxy` routes only the Chromium session
|
|
Theseus owns. Other apps on the machine are untouched.
|
|
|
|
For system-wide, sing-box supports TUN inbounds. This requires:
|
|
- Admin / sudo on first run (Windows: WinTUN driver install, prompt).
|
|
- Different sing-box config (a `tun` inbound instead of `socks`).
|
|
- A different "placement" mode in the addon panel — currently stubbed
|
|
as "Turn on at browser start" but the same UI slot could take a
|
|
three-way selector: `off | Theseus only | System-wide`.
|
|
|
|
Not in v1. When we add it, the extension activates the TUN inbound only
|
|
after a fresh admin-elevation prompt, and only if the user has ticked
|
|
"system-wide" explicitly. Default forever is browser-scoped — a leaky
|
|
TUN interface on a mistyped click is not a Silent Mode failure mode.
|
|
|
|
## Threat model, first pass
|
|
|
|
- **Operator OTA key compromise** ⇒ attacker can push a malicious
|
|
sing-box binary. Same threat model the Ed25519 OTA channel already
|
|
has; the mitigations (multi-sig, on-BCNR pubkey publication) are
|
|
logged in `docs/ADDON-UPDATES.md` under Roadmap.
|
|
- **Malicious VPS operator** ⇒ can see all VPN traffic that leaves the
|
|
exit. Standard VPN threat model. Users who don't want to trust
|
|
Silent Mode's operator paste their own `vless://` URL in the panel;
|
|
the sing-box config is built locally from whatever URL they give it,
|
|
no server-side involvement.
|
|
- **Sia mirror MITM** ⇒ swaps the sing-box download. Blocked by the
|
|
sha256 check against the OTA-signed manifest.
|
|
- **Reality parameter leak** ⇒ someone who learns the inbound's private
|
|
key can decrypt captured traffic. Same as any Reality deployment; key
|
|
rotation is a 3x-UI operational task, not a Theseus concern.
|
|
|
|
## What is deliberately not in v1
|
|
|
|
- Server selector — one exit for now, no city/country picker.
|
|
- Kill switch — Chromium's session either has the proxy or it does not;
|
|
if sing-box dies mid-session, `child.on("exit")` clears the proxy but
|
|
live requests may drift briefly. A real kill switch needs the TUN
|
|
inbound.
|
|
- Split tunneling by domain — belongs in `api.setSessionProxy`'s
|
|
`bypassList`; UI for it later.
|
|
- Telemetry, quota display, referrals — deliberately out.
|