vpn: sing-box on both ends, modern config schema, rebuild URL from catalogue
Three bugs, all found by driving the add-on in a real Theseus and watching the egress IP rather than reasoning about it. 1. The generated config used pre-1.11 schema. `sniff` on an inbound and the `block` outbound type were deprecated in sing-box 1.11 and REMOVED in 1.13, so 1.14.1 refused the whole file and exited 1. Routing is now a bare `final`; rule `action` semantics changed in 1.12 and the explicit inbound→outbound rule was never needed. 2. xray-core 26.3.27's REALITY would not complete a handshake with a sing-box client — and, after ruling out keys (three derivations, a fresh pair used verbatim), shortIds (explicit and empty), clock skew, dest reachability, TLS 1.3/X25519 on the dest, and xtls-rprx-vision, not with a correctly configured xray client either. sing-box against sing-box works first try. The exits now run sing-box, which is what the add-on already ships to every client, so there is no longer a cross-implementation surface at all. Migration script included; it keeps the port, the SNI and the existing uuid pool and only changes the Reality keypair. Worth recording separately: xray's REALITY inbound field is `dest`, not sing-box's `target`. That was wrong too, independently. 3. leaseEndpoint cached the full vless URL. The Reality key and short id live inside that URL, so re-keying an exit left every client failing against a stale copy for the whole 24h lease. It now caches only the uuid and rebuilds the URL from the current catalogue entry, so a re-key takes effect as soon as the catalogue refreshes. Verified in Theseus over CDP: baseline 80.187.100.105, tunnel up 81.31.210.65 (the sm-1 exit), off restores the baseline, and sm-3 is correctly refused to a free-tier caller.
This commit is contained in:
parent
5250672d64
commit
3cb5081bdb
1 changed files with 28 additions and 10 deletions
|
|
@ -88,6 +88,14 @@ function parseVless(url) {
|
||||||
|
|
||||||
// Minimal sing-box outbound config for VLESS+Reality, plus a SOCKS5 inbound
|
// Minimal sing-box outbound config for VLESS+Reality, plus a SOCKS5 inbound
|
||||||
// on 127.0.0.1:<socksPort> that Theseus's session proxy points at.
|
// on 127.0.0.1:<socksPort> that Theseus's session proxy points at.
|
||||||
|
//
|
||||||
|
// Schema notes, learned the hard way against a real binary: `sniff` on an
|
||||||
|
// inbound and the `block` outbound type are legacy fields, deprecated in
|
||||||
|
// sing-box 1.11 and REMOVED in 1.13 — leaving them in makes 1.13+ refuse the
|
||||||
|
// whole config with "legacy inbound fields are deprecated". Routing is a bare
|
||||||
|
// `final` here rather than an explicit inbound→outbound rule, because
|
||||||
|
// everything entering the SOCKS inbound is meant to leave through the tunnel
|
||||||
|
// and rule `action` semantics also changed in 1.12.
|
||||||
function buildSingBoxConfig(vless, socksPort) {
|
function buildSingBoxConfig(vless, socksPort) {
|
||||||
return {
|
return {
|
||||||
log: { level: "warn", timestamp: true },
|
log: { level: "warn", timestamp: true },
|
||||||
|
|
@ -97,7 +105,6 @@ function buildSingBoxConfig(vless, socksPort) {
|
||||||
tag: "in-socks",
|
tag: "in-socks",
|
||||||
listen: "127.0.0.1",
|
listen: "127.0.0.1",
|
||||||
listen_port: socksPort,
|
listen_port: socksPort,
|
||||||
sniff: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
outbounds: [
|
outbounds: [
|
||||||
|
|
@ -123,12 +130,8 @@ function buildSingBoxConfig(vless, socksPort) {
|
||||||
: { enabled: true, server_name: vless.sni || vless.address },
|
: { enabled: true, server_name: vless.sni || vless.address },
|
||||||
},
|
},
|
||||||
{ type: "direct", tag: "out-direct" },
|
{ type: "direct", tag: "out-direct" },
|
||||||
{ type: "block", tag: "out-block" },
|
|
||||||
],
|
],
|
||||||
route: {
|
route: { final: "out-vless" },
|
||||||
final: "out-vless",
|
|
||||||
rules: [{ inbound: ["in-socks"], outbound: "out-vless" }],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -428,11 +431,26 @@ module.exports = {
|
||||||
// returns the same lease for a repeat caller anyway, but not re-asking on
|
// 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
|
// every toggle keeps the round trip off the common path and means a brief
|
||||||
// gateway outage does not break an already-working exit.
|
// gateway outage does not break an already-working exit.
|
||||||
|
// Build a vless:// URL from the CURRENT catalogue entry plus a leased
|
||||||
|
// uuid. Deliberately not from a cached URL: the Reality key and short id
|
||||||
|
// live in that URL, so an exit that re-keys would keep failing against a
|
||||||
|
// stale cached copy until the lease expired. Rebuilding each time means a
|
||||||
|
// re-key is picked up as soon as the catalogue refreshes.
|
||||||
|
function vlessFor(srv, uuid) {
|
||||||
|
const q = new URLSearchParams({
|
||||||
|
type: "tcp", security: "reality", flow: srv.flow || "xtls-rprx-vision",
|
||||||
|
pbk: srv.pbk, sid: srv.sid, sni: srv.sni, fp: srv.fp || "chrome",
|
||||||
|
});
|
||||||
|
return `vless://${uuid}@${srv.host}:${srv.port}?${q}#${encodeURIComponent(srv.label || srv.id)}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function leaseEndpoint(srv) {
|
async function leaseEndpoint(srv) {
|
||||||
const cacheKey = `__lease:${srv.id}`;
|
const cacheKey = `__lease:${srv.id}`;
|
||||||
try {
|
try {
|
||||||
const cached = await api.storage.get(cacheKey, null);
|
const cached = await api.storage.get(cacheKey, null);
|
||||||
if (cached && cached.vless && cached.expiresAt > Date.now() + 60_000) return cached.vless;
|
if (cached && cached.uuid && cached.expiresAt > Date.now() + 60_000) {
|
||||||
|
return vlessFor(srv, cached.uuid);
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const t = setTimeout(() => controller.abort(), 20_000);
|
const t = setTimeout(() => controller.abort(), 20_000);
|
||||||
|
|
@ -451,10 +469,10 @@ module.exports = {
|
||||||
let j;
|
let j;
|
||||||
try { j = JSON.parse(body); }
|
try { j = JSON.parse(body); }
|
||||||
catch { throw new Error(`key issuer returned non-JSON (${body.slice(0, 80)}…)`); }
|
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}`);
|
if (!r.ok || !j.uuid) 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 {}
|
try { await api.storage.set(cacheKey, { uuid: j.uuid, expiresAt: j.expiresAt || 0 }); } catch {}
|
||||||
api.log(`leased a session on ${srv.id}${j.reused ? " (reused)" : ""}`);
|
api.log(`leased a session on ${srv.id}${j.reused ? " (reused)" : ""}`);
|
||||||
return j.vless;
|
return vlessFor(srv, j.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve either a raw vless:// URL or a serverId lookup into the URL to
|
// Resolve either a raw vless:// URL or a serverId lookup into the URL to
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue