vpn: lease a credential from the key-issuer instead of expecting one in the catalogue
Found by driving the add-on in a real Theseus rather than reasoning about it. turnOn() failed with "Silent Mode · 1 is not yet configured (ready)" — resolveEndpoint required the catalogue entry to carry a vless URL, but the gateway catalogue deliberately ships none, because a vless URL is the credential and that endpoint is public. The add-on predates the key-issuer and was never taught to ask for a lease. It now POSTs to /api/vpn/session for any ready exit that has no URL of its own, and caches the lease under its serverId until a minute before expiry. Baked-in and subscription entries still use their own URL and never hit the network.
This commit is contained in:
parent
890e6fae4b
commit
e31c685550
1 changed files with 43 additions and 3 deletions
|
|
@ -24,6 +24,7 @@ const { spawn } = require("node:child_process");
|
|||
const BINARY_MANIFEST_FILE = "binary-manifest.json";
|
||||
const SERVER_LIST_FILE = "server-list.json";
|
||||
const SERVER_LIST_URL = "https://navigate.st/api/vpn/servers";
|
||||
const SESSION_URL = "https://navigate.st/api/vpn/session";
|
||||
const CACHE_SUBDIR = "bin";
|
||||
const CONFIG_SUBDIR = "run";
|
||||
const DOWNLOAD_TIMEOUT_MS = 5 * 60_000;
|
||||
|
|
@ -422,6 +423,40 @@ module.exports = {
|
|||
return file;
|
||||
}
|
||||
|
||||
// Ask the key-issuer for a session credential on this exit. Leases are
|
||||
// cached under their serverId and reused until they expire — the gateway
|
||||
// returns the same lease for a repeat caller anyway, but not re-asking on
|
||||
// every toggle keeps the round trip off the common path and means a brief
|
||||
// gateway outage does not break an already-working exit.
|
||||
async function leaseEndpoint(srv) {
|
||||
const cacheKey = `__lease:${srv.id}`;
|
||||
try {
|
||||
const cached = await api.storage.get(cacheKey, null);
|
||||
if (cached && cached.vless && cached.expiresAt > Date.now() + 60_000) return cached.vless;
|
||||
} catch {}
|
||||
const controller = new AbortController();
|
||||
const t = setTimeout(() => controller.abort(), 20_000);
|
||||
let r, body;
|
||||
try {
|
||||
r = await fetch(SESSION_URL, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ serverId: srv.id, tier: "free" }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
body = await r.text();
|
||||
} catch (e) {
|
||||
throw new Error(`could not reach the key issuer: ${e?.message || e}`);
|
||||
} finally { clearTimeout(t); }
|
||||
let j;
|
||||
try { j = JSON.parse(body); }
|
||||
catch { throw new Error(`key issuer returned non-JSON (${body.slice(0, 80)}…)`); }
|
||||
if (!r.ok || !j.vless) throw new Error(j.error || `key issuer said HTTP ${r.status}`);
|
||||
try { await api.storage.set(cacheKey, { vless: j.vless, expiresAt: j.expiresAt || 0 }); } catch {}
|
||||
api.log(`leased a session on ${srv.id}${j.reused ? " (reused)" : ""}`);
|
||||
return j.vless;
|
||||
}
|
||||
|
||||
// Resolve either a raw vless:// URL or a serverId lookup into the URL to
|
||||
// hand to sing-box. The panel usually sends { serverId } for a preset and
|
||||
// { vless } for a custom paste; either shape is accepted. A serverId of
|
||||
|
|
@ -438,10 +473,15 @@ module.exports = {
|
|||
}
|
||||
const hit = serverList.find((s) => s.id === serverId);
|
||||
if (!hit) throw new Error(`no server with id ${serverId} in the catalog`);
|
||||
if (!hit.vless || hit.status === "coming-soon") {
|
||||
throw new Error(`${hit.label || serverId} is not yet configured (${hit.status || "no vless URL"})`);
|
||||
// A baked-in or subscription entry carries its own URL. A Silent Mode
|
||||
// exit from the gateway catalogue deliberately does not: the catalogue
|
||||
// is public, and a vless URL is the credential. For those we lease one
|
||||
// from the key-issuer, which hands out a per-caller UUID.
|
||||
if (hit.vless) return hit.vless;
|
||||
if (hit.status && hit.status !== "ready") {
|
||||
throw new Error(`${hit.label || serverId} is not available (${hit.status})`);
|
||||
}
|
||||
return hit.vless;
|
||||
return await leaseEndpoint(hit);
|
||||
}
|
||||
throw new Error("no endpoint — pass { vless } or { serverId }");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue