|
|
|
@@ -43,7 +43,7 @@ interface HostProxyServer {
|
|
|
|
|
|
|
|
|
|
interface HostProxySource {
|
|
|
|
|
id: string;
|
|
|
|
|
sourceType: "benchmark-validated-master-shadowsocks" | "vpn-server-shadowsocks" | "vpn-server-hysteria-existing";
|
|
|
|
|
sourceType: "benchmark-validated-master-shadowsocks" | "vpn-server-shadowsocks" | "vpn-server-hysteria-existing" | "vpn-server-hysteria-managed";
|
|
|
|
|
serverRef: string;
|
|
|
|
|
benchmarkRef: string;
|
|
|
|
|
implementationRef: string;
|
|
|
|
@@ -52,6 +52,15 @@ interface HostProxySource {
|
|
|
|
|
sourceKey: string;
|
|
|
|
|
sourceFingerprint: string;
|
|
|
|
|
masterShadowsocks: MasterShadowsocksSourceSpec | null;
|
|
|
|
|
managedHysteria: {
|
|
|
|
|
server: string;
|
|
|
|
|
serverPort: number;
|
|
|
|
|
auth: string;
|
|
|
|
|
serverName: string;
|
|
|
|
|
insecure: boolean;
|
|
|
|
|
obfsType: string;
|
|
|
|
|
obfsPassword: string;
|
|
|
|
|
} | null;
|
|
|
|
|
client: HostProxyClient;
|
|
|
|
|
proxyUrl: string;
|
|
|
|
|
externalProbeUrl: string;
|
|
|
|
@@ -294,8 +303,9 @@ function parseServer(raw: Record<string, unknown>, label: string): HostProxyServ
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseSource(id: string, raw: Record<string, unknown>): HostProxySource {
|
|
|
|
|
const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks", "vpn-server-shadowsocks", "vpn-server-hysteria-existing"] as const);
|
|
|
|
|
const sourceType = enumField(raw, "sourceType", `${configPath}.sources.${id}`, ["benchmark-validated-master-shadowsocks", "vpn-server-shadowsocks", "vpn-server-hysteria-existing", "vpn-server-hysteria-managed"] as const);
|
|
|
|
|
if (sourceType === "vpn-server-hysteria-existing") return parseExistingHysteriaSource(id, raw, sourceType);
|
|
|
|
|
if (sourceType === "vpn-server-hysteria-managed") return parseManagedHysteriaSource(id, raw, sourceType);
|
|
|
|
|
const sourceConfigRef = stringField(raw, "sourceConfigRef", `${configPath}.sources.${id}`);
|
|
|
|
|
const resolved = resolveEgressProxySourceRef(sourceConfigRef, `${configPath}.sources.${id}.sourceConfigRef`);
|
|
|
|
|
if (resolved.sourceType !== "master-shadowsocks" || resolved.masterShadowsocks === null) {
|
|
|
|
@@ -316,6 +326,7 @@ function parseSource(id: string, raw: Record<string, unknown>): HostProxySource
|
|
|
|
|
sourceKey: resolved.sourceKey,
|
|
|
|
|
sourceFingerprint: resolved.fingerprint,
|
|
|
|
|
masterShadowsocks: resolved.masterShadowsocks,
|
|
|
|
|
managedHysteria: null,
|
|
|
|
|
client,
|
|
|
|
|
proxyUrl,
|
|
|
|
|
externalProbeUrl: urlField(raw, "externalProbeUrl", `${configPath}.sources.${id}`),
|
|
|
|
@@ -337,6 +348,49 @@ function parseSource(id: string, raw: Record<string, unknown>): HostProxySource
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseManagedHysteriaSource(id: string, raw: Record<string, unknown>, sourceType: HostProxySource["sourceType"]): HostProxySource {
|
|
|
|
|
const label = `${configPath}.sources.${id}`;
|
|
|
|
|
const sourceConfigRef = absolutePathField(raw, "sourceConfigRef", label);
|
|
|
|
|
const sourceRaw = record(Bun.YAML.parse(readFileSync(sourceConfigRef, "utf8")), sourceConfigRef);
|
|
|
|
|
const [server, portText] = stringField(sourceRaw, "server", sourceConfigRef).split(":");
|
|
|
|
|
const tls = record(sourceRaw.tls, `${sourceConfigRef}.tls`);
|
|
|
|
|
const obfs = record(sourceRaw.obfs, `${sourceConfigRef}.obfs`);
|
|
|
|
|
const salamander = record(obfs.salamander, `${sourceConfigRef}.obfs.salamander`);
|
|
|
|
|
const client = managedClientSpec(record(raw.client, `${label}.client`), `${label}.client`);
|
|
|
|
|
const proxyUrl = urlField(raw, "proxyUrl", label);
|
|
|
|
|
const expectedProxyUrl = `http://${client.listenHost}:${client.listenPort}`;
|
|
|
|
|
if (proxyUrl !== expectedProxyUrl) throw new Error(`${label}.proxyUrl must be ${expectedProxyUrl}`);
|
|
|
|
|
const managedHysteria = {
|
|
|
|
|
server: server ?? "",
|
|
|
|
|
serverPort: Number(portText),
|
|
|
|
|
auth: stringField(sourceRaw, "auth", sourceConfigRef),
|
|
|
|
|
serverName: stringField(tls, "sni", `${sourceConfigRef}.tls`),
|
|
|
|
|
insecure: booleanField(tls, "insecure", `${sourceConfigRef}.tls`),
|
|
|
|
|
obfsType: stringField(obfs, "type", `${sourceConfigRef}.obfs`),
|
|
|
|
|
obfsPassword: stringField(salamander, "password", `${sourceConfigRef}.obfs.salamander`),
|
|
|
|
|
};
|
|
|
|
|
if (!managedHysteria.server || !Number.isInteger(managedHysteria.serverPort)) throw new Error(`${sourceConfigRef}.server must be host:port`);
|
|
|
|
|
const source = {
|
|
|
|
|
id,
|
|
|
|
|
sourceType,
|
|
|
|
|
serverRef: stringField(raw, "serverRef", label),
|
|
|
|
|
benchmarkRef: stringField(raw, "benchmarkRef", label),
|
|
|
|
|
implementationRef: stringField(raw, "implementationRef", label),
|
|
|
|
|
sourceConfigRef,
|
|
|
|
|
sourceRef: sourceConfigRef,
|
|
|
|
|
sourceKey: "auth,obfs.salamander.password",
|
|
|
|
|
sourceFingerprint: fingerprint({ sourceConfigRef, managedHysteria }),
|
|
|
|
|
masterShadowsocks: null,
|
|
|
|
|
managedHysteria,
|
|
|
|
|
client,
|
|
|
|
|
proxyUrl,
|
|
|
|
|
externalProbeUrl: urlField(raw, "externalProbeUrl", label),
|
|
|
|
|
expectedServer: `${managedHysteria.server}:${managedHysteria.serverPort}`,
|
|
|
|
|
fingerprint: "",
|
|
|
|
|
};
|
|
|
|
|
return { ...source, fingerprint: fingerprint({ sourceType, sourceConfigRef, sourceFingerprint: source.sourceFingerprint, client, proxyUrl }) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseExistingHysteriaSource(id: string, raw: Record<string, unknown>, sourceType: HostProxySource["sourceType"]): HostProxySource {
|
|
|
|
|
const client = existingHysteriaClientSpec(record(raw.client, `${configPath}.sources.${id}.client`), `${configPath}.sources.${id}.client`);
|
|
|
|
|
const proxyUrl = urlField(raw, "proxyUrl", `${configPath}.sources.${id}`);
|
|
|
|
@@ -355,6 +409,7 @@ function parseExistingHysteriaSource(id: string, raw: Record<string, unknown>, s
|
|
|
|
|
sourceKey: "",
|
|
|
|
|
sourceFingerprint: fingerprint({ sourceConfigRef, expectedServer }),
|
|
|
|
|
masterShadowsocks: null,
|
|
|
|
|
managedHysteria: null,
|
|
|
|
|
client,
|
|
|
|
|
proxyUrl,
|
|
|
|
|
externalProbeUrl: urlField(raw, "externalProbeUrl", `${configPath}.sources.${id}`),
|
|
|
|
@@ -625,6 +680,16 @@ function skippedMasterProxyServer(source: HostProxySource, mode: "apply" | "stat
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function proxySecretMaterial(server: HostProxyServer, source: HostProxySource): HostProxySecretMaterial {
|
|
|
|
|
if (source.managedHysteria !== null) {
|
|
|
|
|
const clientConfigJson = renderSingBoxClientConfig(source, "");
|
|
|
|
|
return {
|
|
|
|
|
serverConfigJson: "{}\n",
|
|
|
|
|
clientConfigJson,
|
|
|
|
|
sourcePath: source.sourceConfigRef,
|
|
|
|
|
fingerprint: fingerprintSecretValues({ clientConfigJson }, ["clientConfigJson"]),
|
|
|
|
|
valuesPrinted: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (source.masterShadowsocks === null) throw new Error(`${configPath}.sources.${source.id} does not render managed shadowsocks secret material`);
|
|
|
|
|
const envSource = readEnvSourceFile({
|
|
|
|
|
root: rootPath(".state", "secrets"),
|
|
|
|
@@ -655,7 +720,7 @@ function renderMasterShadowsocksServerConfig(server: HostProxyServer, password:
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSingBoxClientConfig(source: HostProxySource, password: string): string {
|
|
|
|
|
if (source.masterShadowsocks === null || source.client.mode !== "trans-static-binary") {
|
|
|
|
|
if ((source.masterShadowsocks === null && source.managedHysteria === null) || source.client.mode !== "trans-static-binary") {
|
|
|
|
|
throw new Error(`${configPath}.sources.${source.id} cannot render sing-box client config`);
|
|
|
|
|
}
|
|
|
|
|
const client = source.client;
|
|
|
|
@@ -670,13 +735,21 @@ function renderSingBoxClientConfig(source: HostProxySource, password: string): s
|
|
|
|
|
experimental: { clash_api: { external_controller: client.clashApiListen } },
|
|
|
|
|
inbounds,
|
|
|
|
|
outbounds: [
|
|
|
|
|
{
|
|
|
|
|
source.managedHysteria === null ? {
|
|
|
|
|
type: "shadowsocks",
|
|
|
|
|
tag: "master-vpn",
|
|
|
|
|
server: source.masterShadowsocks.serverHost,
|
|
|
|
|
server_port: source.masterShadowsocks.serverPort,
|
|
|
|
|
method: source.masterShadowsocks.method,
|
|
|
|
|
password,
|
|
|
|
|
} : {
|
|
|
|
|
type: "hysteria2",
|
|
|
|
|
tag: "master-vpn",
|
|
|
|
|
server: source.managedHysteria.server,
|
|
|
|
|
server_port: source.managedHysteria.serverPort,
|
|
|
|
|
password: source.managedHysteria.auth,
|
|
|
|
|
obfs: { type: source.managedHysteria.obfsType, password: source.managedHysteria.obfsPassword },
|
|
|
|
|
tls: { enabled: true, server_name: source.managedHysteria.serverName, insecure: source.managedHysteria.insecure },
|
|
|
|
|
},
|
|
|
|
|
{ type: "direct", tag: "direct" },
|
|
|
|
|
{ type: "block", tag: "block" },
|
|
|
|
|