vpn 0.1.3 → 0.1.4: an https:// paste imports as a subscription
The Custom box validated for a vless:// prefix and rejected anything else, so a provider's subscription URL — the thing most people are handed — got "paste a vless:// URL first" with no hint that the Subscription card two sections down was what it wanted. Now an http(s):// paste in that box is detected and routed to the subscription importer, the placeholder says both are accepted, and the dropdown option reads "Custom — vless:// or subscription URL". Also renames the three bundled entries' status from "coming-soon" to "awaiting-key-issuer". The exits exist and are running xray; what is missing is a way to hand a client credentials without shipping a shared secret. DESIGN.md now records why that list stays empty, since this is the second time the shortcut looked attractive: a vless:// URL is the credential, so writing one into the tarball (immutable, mirrored) or onto a public Sia object (mutable but world-readable) are the same category of mistake. Per-session minting is the fix, because then no shared credential exists to leak. Parser verified against plain-text, standard-base64 and url-safe unpadded-base64 subscription bodies; an HTML error page correctly yields zero entries instead of a JSON parse crash.
This commit is contained in:
parent
c688cf8a20
commit
cee120cb23
4 changed files with 62 additions and 8 deletions
|
|
@ -35,6 +35,39 @@ Anchors:
|
||||||
- Exposes one POST endpoint the extension calls (below).
|
- Exposes one POST endpoint the extension calls (below).
|
||||||
- Talks to Aegis-side wallet signatures when a request claims to be paid.
|
- Talks to Aegis-side wallet signatures when a request claims to be paid.
|
||||||
|
|
||||||
|
### Why the bundled list ships with no credentials
|
||||||
|
|
||||||
|
A `vless://` URL **is** the credential for its exit — UUID plus Reality
|
||||||
|
parameters is all a client needs. That leads to a rule worth stating
|
||||||
|
explicitly, because it has now been violated once and nearly a second
|
||||||
|
time in a different wrapper:
|
||||||
|
|
||||||
|
> A shared exit credential must never be written anywhere immutable or
|
||||||
|
> world-readable. Not the addon tarball (signed, immutable, mirrored
|
||||||
|
> forever). Not a git commit (history is permanent). Not a Sia object
|
||||||
|
> (mutable, but still world-readable, and the addon source that names
|
||||||
|
> the URL is public).
|
||||||
|
|
||||||
|
The first attempt baked the three URLs into `server-list.json` and
|
||||||
|
shipped it as `vpn-0.1.3.tar.gz`; that was rolled back, the tarball
|
||||||
|
deleted from Sia, and all three servers' UUID + Reality keypair +
|
||||||
|
shortId rotated. The second attempt was going to host the same JSON on
|
||||||
|
Sia and overlay it — rotatable and out of git, which is genuinely
|
||||||
|
better, but still publishing a secret. Both are the same category of
|
||||||
|
mistake.
|
||||||
|
|
||||||
|
The distinction that actually matters is not *where* the shared
|
||||||
|
credential lives but *whether a shared credential exists at all*. With
|
||||||
|
per-session minting there is nothing to leak: each connect gets its own
|
||||||
|
UUID with a TTL, revocable on its own, attributable to one requester.
|
||||||
|
That is why the bundled entries sit at `awaiting-key-issuer` rather
|
||||||
|
than being filled in — the honest state, not a placeholder someone
|
||||||
|
should be tempted to complete.
|
||||||
|
|
||||||
|
Working credentials for the three live exits are in the repo-root
|
||||||
|
`.keys/vpn-servers.json` (gitignored via `.gitignore:70 /.keys/`) for
|
||||||
|
operator testing via the panel's Custom box. They stay there.
|
||||||
|
|
||||||
### `POST /api/vpn/new-client`
|
### `POST /api/vpn/new-client`
|
||||||
|
|
||||||
Request body:
|
Request body:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "vpn",
|
"id": "vpn",
|
||||||
"name": "VPN",
|
"name": "VPN",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"description": "Route Theseus's traffic through a Silent Mode VPN endpoint. Runs sing-box locally, exits at one of our servers, and switches on with one click. Paste any vless:// URL to point it at your own endpoint instead.",
|
"description": "Route Theseus's traffic through a Silent Mode VPN endpoint. Runs sing-box locally, exits at one of our servers, and switches on with one click. Paste any vless:// URL to point it at your own endpoint instead.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA1djdjMCA1IDMuNSA5LjIgOCAxMCA0LjUtLjggOC01IDgtMTBWNWwtOC0zeiIgZmlsbD0iIzBhYzE4ZSIvPjxwYXRoIGQ9Ik05IDEybDIgMiA0LTQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=",
|
"icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJMNCA1djdjMCA1IDMuNSA5LjIgOCAxMCA0LjUtLjggOC01IDgtMTBWNWwtOC0zeiIgZmlsbD0iIzBhYzE4ZSIvPjxwYXRoIGQ9Ik05IDEybDIgMiA0LTQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=",
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ button.btn.small { padding: 4px 9px; font-size: 11.5px; }
|
||||||
|
|
||||||
<div id="customBlock" hidden>
|
<div id="customBlock" hidden>
|
||||||
<div class="row" style="margin-top:8px">
|
<div class="row" style="margin-top:8px">
|
||||||
<textarea id="vless" rows="3" placeholder="vless://uuid@host:443?type=tcp&security=reality&pbk=…&sni=…"></textarea>
|
<textarea id="vless" rows="3" placeholder="vless://uuid@host:443?security=reality&pbk=…&sni=… …or paste an https:// subscription URL and it will be imported"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="justify-content:flex-end">
|
<div class="row" style="justify-content:flex-end">
|
||||||
<button class="btn small" id="paste">Paste</button>
|
<button class="btn small" id="paste">Paste</button>
|
||||||
|
|
@ -178,7 +178,28 @@ async function saveSelection() {
|
||||||
try { await SM().storage.set("serverId", selectedServerId); } catch {}
|
try { await SM().storage.set("serverId", selectedServerId); } catch {}
|
||||||
}
|
}
|
||||||
async function saveEndpoint() {
|
async function saveEndpoint() {
|
||||||
currentVless = $("vless").value.trim();
|
const raw = $("vless").value.trim();
|
||||||
|
// A very common mix-up: the provider handed over a subscription URL (plain
|
||||||
|
// https) rather than a single vless:// endpoint, and this box is the first
|
||||||
|
// text field people find. Route it to the subscription importer instead of
|
||||||
|
// rejecting it for not starting with vless://.
|
||||||
|
if (/^https?:\/\//i.test(raw)) {
|
||||||
|
const subHint = document.getElementById("subHint");
|
||||||
|
subHint.className = "hint";
|
||||||
|
subHint.textContent = "that looks like a subscription URL — importing…";
|
||||||
|
try {
|
||||||
|
const r = await SM().invoke("addSubscription", { url: raw });
|
||||||
|
$("vless").value = "";
|
||||||
|
subHint.textContent = `added ${r.label} — ${r.count} servers, pick one above`;
|
||||||
|
render(r.snapshot);
|
||||||
|
renderSubList();
|
||||||
|
} catch (e) {
|
||||||
|
subHint.className = "hint err";
|
||||||
|
subHint.textContent = `not a usable subscription: ${e?.message || e}`;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentVless = raw;
|
||||||
try { await SM().storage.set("vless", currentVless); } catch {}
|
try { await SM().storage.set("vless", currentVless); } catch {}
|
||||||
render(await SM().invoke("status", {}));
|
render(await SM().invoke("status", {}));
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +223,7 @@ function renderServerDropdown(servers) {
|
||||||
}
|
}
|
||||||
const custom = document.createElement("option");
|
const custom = document.createElement("option");
|
||||||
custom.value = CUSTOM_ID;
|
custom.value = CUSTOM_ID;
|
||||||
custom.textContent = "⚙️ Custom vless:// URL";
|
custom.textContent = "⚙️ Custom — vless:// or subscription URL";
|
||||||
sel.appendChild(custom);
|
sel.appendChild(custom);
|
||||||
// Restore the previous selection if it survived the refresh.
|
// Restore the previous selection if it survived the refresh.
|
||||||
if ([...sel.options].some((o) => o.value === prev)) sel.value = prev;
|
if ([...sel.options].some((o) => o.value === prev)) sel.value = prev;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"note": "Baked-in default server list. The addon fetches https://navigate.st/api/vpn/servers on activation and overlays that on top of this — any entry with a matching id replaces the baked-in one, new entries append. Users' saved selection persists by id, so a server can be re-parameterised (Reality key rotated, new host) without the panel losing its state.",
|
"note": "Baked-in default server list. Deliberately carries NO credentials: a vless:// URL is the credential for its exit, and anything shipped in this tarball is public and immutable forever. The three Silent Mode exits exist and run xray, but they stay 'awaiting-key-issuer' here until the gateway can mint a per-session UUID per connect — see DESIGN.md. Until then, Custom accepts your own vless:// URL or a subscription URL.",
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"id": "sm-1",
|
"id": "sm-1",
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
"flag": "🌐",
|
"flag": "🌐",
|
||||||
"country": "",
|
"country": "",
|
||||||
"vless": "",
|
"vless": "",
|
||||||
"status": "coming-soon"
|
"status": "awaiting-key-issuer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sm-2",
|
"id": "sm-2",
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"flag": "🌐",
|
"flag": "🌐",
|
||||||
"country": "",
|
"country": "",
|
||||||
"vless": "",
|
"vless": "",
|
||||||
"status": "coming-soon"
|
"status": "awaiting-key-issuer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sm-3",
|
"id": "sm-3",
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
"flag": "🌐",
|
"flag": "🌐",
|
||||||
"country": "",
|
"country": "",
|
||||||
"vless": "",
|
"vless": "",
|
||||||
"status": "coming-soon"
|
"status": "awaiting-key-issuer"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue