fix: route D601 Sub2API egress through Shadowsocks (#843)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
x-unidesk-log-rotation: &unidesk-log-rotation
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "${UNIDESK_DOCKER_LOG_MAX_SIZE:-20m}"
|
||||
max-file: "${UNIDESK_DOCKER_LOG_MAX_FILE:-3}"
|
||||
|
||||
services:
|
||||
sub2api-master-egress-proxy:
|
||||
image: ghcr.io/shadowsocks/ssserver-rust:latest
|
||||
container_name: unidesk-sub2api-master-egress-proxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "0.0.0.0:18792:18792/tcp"
|
||||
volumes:
|
||||
- /root/unidesk/.state/secrets/platform-infra/sub2api-master-egress-proxy.config.json:/etc/shadowsocks-rust/config.json:ro
|
||||
logging: *unidesk-log-rotation
|
||||
@@ -30,7 +30,7 @@ defaults:
|
||||
|
||||
image:
|
||||
repository: weishaw/sub2api
|
||||
tag: 0.1.136
|
||||
tag: 0.1.138
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
dependencyImages:
|
||||
@@ -80,7 +80,7 @@ targets:
|
||||
redisReplicas: 1
|
||||
image:
|
||||
repository: 127.0.0.1:5000/platform-infra/sub2api
|
||||
tag: 0.1.136
|
||||
tag: 0.1.138
|
||||
pullPolicy: IfNotPresent
|
||||
dependencyImages:
|
||||
postgres: docker.m.daocloud.io/library/postgres:18-alpine
|
||||
@@ -150,10 +150,13 @@ targets:
|
||||
image: 127.0.0.1:5000/platform-infra/sing-box:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
listenPort: 10808
|
||||
sourceRef: platform-infra/master-vpn-subscription.env
|
||||
sourceKey: MASTER_VPN_SUBSCRIPTION_URL
|
||||
sourceType: subscription-url
|
||||
preferredOutbound: hysteria2
|
||||
sourceRef: platform-infra/sub2api-master-egress-proxy.env
|
||||
sourceKey: SUB2API_MASTER_SHADOWSOCKS_PASSWORD
|
||||
sourceType: master-shadowsocks
|
||||
masterShadowsocks:
|
||||
serverHost: 74.48.78.17
|
||||
serverPort: 18792
|
||||
method: chacha20-ietf-poly1305
|
||||
applyToSub2Api: true
|
||||
applyToSentinel: true
|
||||
healthProbeUrl: https://www.gstatic.com/generate_204
|
||||
|
||||
+156
-30
@@ -153,14 +153,21 @@ interface Sub2ApiEgressProxyConfig {
|
||||
listenPort: number;
|
||||
sourceRef: string;
|
||||
sourceKey: string;
|
||||
sourceType: "subscription-url";
|
||||
preferredOutbound: "vless-reality" | "hysteria2";
|
||||
sourceType: "subscription-url" | "master-shadowsocks";
|
||||
preferredOutbound: "vless-reality" | "hysteria2" | null;
|
||||
masterShadowsocks: Sub2ApiMasterShadowsocksConfig | null;
|
||||
noProxy: string[];
|
||||
applyToSub2Api: boolean;
|
||||
applyToSentinel: boolean;
|
||||
healthProbeUrl: string;
|
||||
}
|
||||
|
||||
interface Sub2ApiMasterShadowsocksConfig {
|
||||
serverHost: string;
|
||||
serverPort: number;
|
||||
method: string;
|
||||
}
|
||||
|
||||
interface ExternalDatabaseConfig {
|
||||
mode: "external";
|
||||
sourceRef: string;
|
||||
@@ -215,9 +222,9 @@ interface EgressProxySecretMaterial {
|
||||
secretName: string;
|
||||
secretKey: string;
|
||||
fingerprint: string;
|
||||
subscriptionBytes: number;
|
||||
sourceBytes: number;
|
||||
selectedOutbound: string;
|
||||
subscriptionDiagnostics: EgressProxySubscriptionDiagnostics;
|
||||
sourceDiagnostics: EgressProxySubscriptionDiagnostics;
|
||||
configJson: string;
|
||||
proxyUrl: string;
|
||||
noProxy: string;
|
||||
@@ -226,7 +233,7 @@ interface EgressProxySecretMaterial {
|
||||
|
||||
interface EgressProxySubscriptionCandidateSummary {
|
||||
sourceLine: number;
|
||||
kind: SubscriptionNode["kind"];
|
||||
kind: SubscriptionNode["kind"] | "shadowsocks";
|
||||
fingerprint: string;
|
||||
paramKeys: string[];
|
||||
selected: boolean;
|
||||
@@ -234,6 +241,7 @@ interface EgressProxySubscriptionCandidateSummary {
|
||||
|
||||
interface EgressProxySubscriptionDiagnostics {
|
||||
ok: boolean;
|
||||
sourceType: Sub2ApiEgressProxyConfig["sourceType"];
|
||||
preferredOutbound: Sub2ApiEgressProxyConfig["preferredOutbound"];
|
||||
candidateCount: number;
|
||||
supportedCount: number;
|
||||
@@ -720,8 +728,13 @@ function parseEgressProxyConfig(value: unknown, path: string): Sub2ApiEgressProx
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value !== "object" || Array.isArray(value)) throw new Error(`${configPath}.${path}.egressProxy must be an object`);
|
||||
const record = value as Record<string, unknown>;
|
||||
const sourceType = enumField(record, "sourceType", `${path}.egressProxy`, ["subscription-url"] as const);
|
||||
const preferredOutbound = enumField(record, "preferredOutbound", `${path}.egressProxy`, ["vless-reality", "hysteria2"] as const);
|
||||
const sourceType = enumField(record, "sourceType", `${path}.egressProxy`, ["subscription-url", "master-shadowsocks"] as const);
|
||||
const preferredOutbound = sourceType === "subscription-url" || record.preferredOutbound !== undefined
|
||||
? enumField(record, "preferredOutbound", `${path}.egressProxy`, ["vless-reality", "hysteria2"] as const)
|
||||
: null;
|
||||
const masterShadowsocks = sourceType === "master-shadowsocks"
|
||||
? parseMasterShadowsocksConfig(record.masterShadowsocks, `${path}.egressProxy.masterShadowsocks`)
|
||||
: null;
|
||||
const imagePullPolicy = enumField(record, "imagePullPolicy", `${path}.egressProxy`, ["Always", "IfNotPresent", "Never"] as const);
|
||||
const noProxy = stringArrayField(record, "noProxy", `${path}.egressProxy`);
|
||||
const proxy: Sub2ApiEgressProxyConfig = {
|
||||
@@ -737,6 +750,7 @@ function parseEgressProxyConfig(value: unknown, path: string): Sub2ApiEgressProx
|
||||
sourceKey: stringField(record, "sourceKey", `${path}.egressProxy`),
|
||||
sourceType,
|
||||
preferredOutbound,
|
||||
masterShadowsocks,
|
||||
noProxy,
|
||||
applyToSub2Api: booleanField(record, "applyToSub2Api", `${path}.egressProxy`),
|
||||
applyToSentinel: booleanField(record, "applyToSentinel", `${path}.egressProxy`),
|
||||
@@ -746,6 +760,16 @@ function parseEgressProxyConfig(value: unknown, path: string): Sub2ApiEgressProx
|
||||
return proxy;
|
||||
}
|
||||
|
||||
function parseMasterShadowsocksConfig(value: unknown, path: string): Sub2ApiMasterShadowsocksConfig {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${configPath}.${path} must be an object`);
|
||||
const record = value as Record<string, unknown>;
|
||||
return {
|
||||
serverHost: stringField(record, "serverHost", path),
|
||||
serverPort: integerField(record, "serverPort", path),
|
||||
method: stringField(record, "method", path),
|
||||
};
|
||||
}
|
||||
|
||||
function parseRuntime(root: Record<string, unknown>): Sub2ApiConfig["runtime"] {
|
||||
const value = root.runtime;
|
||||
if (value === undefined) throw new Error(`${configPath}.runtime must be an object`);
|
||||
@@ -907,6 +931,15 @@ function validateEgressProxyConfig(config: Sub2ApiEgressProxyConfig, path: strin
|
||||
validatePort(config.listenPort, `${path}.egressProxy.listenPort`);
|
||||
if (!/^[A-Za-z0-9_./-]+$/u.test(config.sourceRef)) throw new Error(`${configPath}.${path}.egressProxy.sourceRef has an unsupported format`);
|
||||
if (!/^[A-Z0-9_]+$/u.test(config.sourceKey)) throw new Error(`${configPath}.${path}.egressProxy.sourceKey must be an env key`);
|
||||
if (config.sourceType === "subscription-url" && config.preferredOutbound === null) {
|
||||
throw new Error(`${configPath}.${path}.egressProxy.preferredOutbound is required for sourceType=subscription-url`);
|
||||
}
|
||||
if (config.sourceType === "master-shadowsocks") {
|
||||
if (config.masterShadowsocks === null) throw new Error(`${configPath}.${path}.egressProxy.masterShadowsocks is required for sourceType=master-shadowsocks`);
|
||||
validateHostOrIp(config.masterShadowsocks.serverHost, `${path}.egressProxy.masterShadowsocks.serverHost`);
|
||||
validatePort(config.masterShadowsocks.serverPort, `${path}.egressProxy.masterShadowsocks.serverPort`);
|
||||
if (!/^[A-Za-z0-9-]+$/u.test(config.masterShadowsocks.method)) throw new Error(`${configPath}.${path}.egressProxy.masterShadowsocks.method has an unsupported format`);
|
||||
}
|
||||
for (const entry of config.noProxy) {
|
||||
if (!/^[A-Za-z0-9.:_/*-]+$/u.test(entry)) throw new Error(`${configPath}.${path}.egressProxy.noProxy contains an unsupported entry`);
|
||||
}
|
||||
@@ -1482,7 +1515,7 @@ metadata:
|
||||
app.kubernetes.io/part-of: platform-infra
|
||||
app.kubernetes.io/managed-by: unidesk
|
||||
unidesk.ai/runtime-node: ${target.id}
|
||||
unidesk.ai/proxy-source: master-vpn-subscription
|
||||
unidesk.ai/proxy-source: ${proxy.sourceType}
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: ${proxy.deploymentName}
|
||||
@@ -1503,7 +1536,7 @@ metadata:
|
||||
app.kubernetes.io/part-of: platform-infra
|
||||
app.kubernetes.io/managed-by: unidesk
|
||||
unidesk.ai/runtime-node: ${target.id}
|
||||
unidesk.ai/proxy-source: master-vpn-subscription
|
||||
unidesk.ai/proxy-source: ${proxy.sourceType}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
@@ -1518,7 +1551,8 @@ spec:
|
||||
app.kubernetes.io/part-of: platform-infra
|
||||
annotations:
|
||||
unidesk.ai/proxy-source-ref: "${proxy.sourceRef}"
|
||||
unidesk.ai/proxy-preferred-outbound: "${proxy.preferredOutbound}"
|
||||
unidesk.ai/proxy-source-type: "${proxy.sourceType}"
|
||||
unidesk.ai/proxy-selected-outbound: "${proxy.sourceType === "subscription-url" ? proxy.preferredOutbound : "shadowsocks"}"
|
||||
spec:
|
||||
containers:
|
||||
- name: proxy
|
||||
@@ -1590,7 +1624,7 @@ function publicExposureSummary(exposure: Sub2ApiPublicExposureConfig): Record<st
|
||||
function egressProxySummary(proxy: Sub2ApiEgressProxyConfig): Record<string, unknown> {
|
||||
return {
|
||||
enabled: proxy.enabled,
|
||||
mode: "master-vpn-subscription-http-proxy",
|
||||
mode: proxy.sourceType === "master-shadowsocks" ? "master-shadowsocks-http-proxy" : "master-vpn-subscription-http-proxy",
|
||||
deploymentName: proxy.deploymentName,
|
||||
serviceName: proxy.serviceName,
|
||||
secretName: proxy.secretName,
|
||||
@@ -1600,6 +1634,13 @@ function egressProxySummary(proxy: Sub2ApiEgressProxyConfig): Record<string, unk
|
||||
sourceRef: proxy.sourceRef,
|
||||
sourceType: proxy.sourceType,
|
||||
preferredOutbound: proxy.preferredOutbound,
|
||||
masterShadowsocks: proxy.masterShadowsocks === null
|
||||
? null
|
||||
: {
|
||||
serverHost: proxy.masterShadowsocks.serverHost,
|
||||
serverPort: proxy.masterShadowsocks.serverPort,
|
||||
method: proxy.masterShadowsocks.method,
|
||||
},
|
||||
applyToSub2Api: proxy.applyToSub2Api,
|
||||
applyToSentinel: proxy.applyToSentinel,
|
||||
healthProbeUrl: proxy.healthProbeUrl,
|
||||
@@ -1684,9 +1725,10 @@ function plan(options: TargetOptions): Record<string, unknown> {
|
||||
: null,
|
||||
egressProxy: target.egressProxy?.enabled
|
||||
? {
|
||||
mode: `${target.id} in-cluster HTTP proxy client to the master VPN subscription`,
|
||||
mode: `${target.id} in-cluster HTTP proxy client to ${target.egressProxy.sourceType}`,
|
||||
service: `${target.egressProxy.serviceName}.${target.namespace}.svc.cluster.local:${target.egressProxy.listenPort}`,
|
||||
sourceRef: target.egressProxy.sourceRef,
|
||||
sourceType: target.egressProxy.sourceType,
|
||||
applyToSub2Api: target.egressProxy.applyToSub2Api,
|
||||
applyToSentinel: target.egressProxy.applyToSentinel,
|
||||
valuesPrinted: false,
|
||||
@@ -2064,28 +2106,49 @@ function prepareEgressProxySecret(sub2api: Sub2ApiConfig, target: Sub2ApiTargetC
|
||||
const source = readEnvSourceFile({
|
||||
root: secretRoot(sub2api),
|
||||
sourceRef: proxy.sourceRef,
|
||||
missingMessage: (sourcePath) => `egressProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the master VPN subscription source first`,
|
||||
missingMessage: (sourcePath) => `egressProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the declared proxy source first`,
|
||||
});
|
||||
const values = source.values;
|
||||
const subscriptionUrl = requiredEnvValue(values, proxy.sourceKey, proxy.sourceRef);
|
||||
const subscription = fetchSubscription(subscriptionUrl);
|
||||
const decoded = decodeSubscription(subscription.body);
|
||||
const nodes = decoded.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean);
|
||||
const analyzed = analyzeEgressProxySubscription(nodes, proxy.preferredOutbound);
|
||||
if (analyzed.selected === null) throw new Error(analyzed.diagnostics.error ?? `egressProxy preferredOutbound=${proxy.preferredOutbound} is absent from subscription; no fallback outbound was selected`);
|
||||
const selected = analyzed.selected;
|
||||
const configJson = renderSingBoxConfig(selected, proxy);
|
||||
if (proxy.sourceType === "master-shadowsocks") {
|
||||
const password = requiredEnvValue(values, proxy.sourceKey, proxy.sourceRef);
|
||||
const configJson = renderSingBoxMasterShadowsocksConfig(proxy, password);
|
||||
const proxyUrl = `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`;
|
||||
const noProxy = proxy.noProxy.join(",");
|
||||
return {
|
||||
sourceRef: proxy.sourceRef,
|
||||
sourcePath: source.sourcePathRedacted,
|
||||
secretName: proxy.secretName,
|
||||
secretKey: proxy.secretKey,
|
||||
fingerprint: fingerprintSecretValues({ password, configJson }, ["password", "configJson"]),
|
||||
sourceBytes: Buffer.byteLength(password, "utf8"),
|
||||
selectedOutbound: "shadowsocks",
|
||||
sourceDiagnostics: masterShadowsocksDiagnostics(proxy),
|
||||
configJson,
|
||||
proxyUrl,
|
||||
noProxy,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
if (proxy.preferredOutbound === null) throw new Error(`egressProxy preferredOutbound is required for sourceType=${proxy.sourceType}`);
|
||||
const subscriptionUrl = requiredEnvValue(values, proxy.sourceKey, proxy.sourceRef);
|
||||
const subscription = fetchSubscription(subscriptionUrl);
|
||||
const decoded = decodeSubscription(subscription.body);
|
||||
const nodes = decoded.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean);
|
||||
const analyzed = analyzeEgressProxySubscription(nodes, proxy.preferredOutbound);
|
||||
if (analyzed.selected === null) throw new Error(analyzed.diagnostics.error ?? `egressProxy preferredOutbound=${proxy.preferredOutbound} is absent from subscription; no fallback outbound was selected`);
|
||||
const selected = analyzed.selected;
|
||||
const configJson = renderSingBoxConfig(selected, proxy);
|
||||
const proxyUrl = `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`;
|
||||
const noProxy = proxy.noProxy.join(",");
|
||||
return {
|
||||
sourceRef: proxy.sourceRef,
|
||||
sourcePath: source.sourcePathRedacted,
|
||||
secretName: proxy.secretName,
|
||||
secretKey: proxy.secretKey,
|
||||
fingerprint: fingerprintSecretValues({ subscription: subscription.body, configJson }, ["subscription", "configJson"]),
|
||||
subscriptionBytes: Buffer.byteLength(subscription.body, "utf8"),
|
||||
sourceBytes: Buffer.byteLength(subscription.body, "utf8"),
|
||||
selectedOutbound: selected.kind,
|
||||
subscriptionDiagnostics: analyzed.diagnostics,
|
||||
sourceDiagnostics: analyzed.diagnostics,
|
||||
configJson,
|
||||
proxyUrl,
|
||||
noProxy,
|
||||
@@ -2119,11 +2182,13 @@ function decodeSubscription(raw: string): string {
|
||||
|
||||
function safeEgressProxySubscriptionDiagnostics(sub2api: Sub2ApiConfig, proxy: Sub2ApiEgressProxyConfig): EgressProxySubscriptionDiagnostics {
|
||||
try {
|
||||
if (proxy.sourceType === "master-shadowsocks") return masterShadowsocksDiagnostics(proxy);
|
||||
const source = readEnvSourceFile({
|
||||
root: secretRoot(sub2api),
|
||||
sourceRef: proxy.sourceRef,
|
||||
missingMessage: (sourcePath) => `egressProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the master VPN subscription source first`,
|
||||
missingMessage: (sourcePath) => `egressProxy requires ${redactRepoPath(sourcePath)} with ${proxy.sourceKey}; materialize the declared proxy source first`,
|
||||
});
|
||||
if (proxy.preferredOutbound === null) throw new Error(`egressProxy preferredOutbound is required for sourceType=${proxy.sourceType}`);
|
||||
const subscriptionUrl = requiredEnvValue(source.values, proxy.sourceKey, proxy.sourceRef);
|
||||
const subscription = fetchSubscription(subscriptionUrl);
|
||||
const decoded = decodeSubscription(subscription.body);
|
||||
@@ -2132,6 +2197,7 @@ function safeEgressProxySubscriptionDiagnostics(sub2api: Sub2ApiConfig, proxy: S
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
sourceType: proxy.sourceType,
|
||||
preferredOutbound: proxy.preferredOutbound,
|
||||
candidateCount: 0,
|
||||
supportedCount: 0,
|
||||
@@ -2146,17 +2212,59 @@ function safeEgressProxySubscriptionDiagnostics(sub2api: Sub2ApiConfig, proxy: S
|
||||
}
|
||||
}
|
||||
|
||||
function masterShadowsocksDiagnostics(proxy: Sub2ApiEgressProxyConfig): EgressProxySubscriptionDiagnostics {
|
||||
const shadowsocks = proxy.masterShadowsocks;
|
||||
if (shadowsocks === null) {
|
||||
return {
|
||||
ok: false,
|
||||
sourceType: proxy.sourceType,
|
||||
preferredOutbound: proxy.preferredOutbound,
|
||||
candidateCount: 0,
|
||||
supportedCount: 0,
|
||||
unsupportedCount: 0,
|
||||
byKind: {},
|
||||
selectedOutbound: null,
|
||||
selectedFingerprint: null,
|
||||
candidates: [],
|
||||
error: "masterShadowsocks is missing",
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const fingerprint = fingerprintSecretValues({ outbound: `${shadowsocks.serverHost}:${shadowsocks.serverPort}:${shadowsocks.method}` }, ["outbound"]);
|
||||
return {
|
||||
ok: true,
|
||||
sourceType: "master-shadowsocks",
|
||||
preferredOutbound: null,
|
||||
candidateCount: 1,
|
||||
supportedCount: 1,
|
||||
unsupportedCount: 0,
|
||||
byKind: { shadowsocks: 1 },
|
||||
selectedOutbound: "shadowsocks",
|
||||
selectedFingerprint: fingerprint,
|
||||
candidates: [
|
||||
{
|
||||
sourceLine: 1,
|
||||
kind: "shadowsocks",
|
||||
fingerprint,
|
||||
paramKeys: ["serverHost", "serverPort", "method"],
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
type SubscriptionNode =
|
||||
| { kind: "vless-reality"; uri: string; uuid: string; host: string; port: number; sni: string; flow: string | null; fingerprint: string | null; publicKey: string; shortId: string | null }
|
||||
| { kind: "hysteria2"; uri: string; password: string; host: string; port: number; sni: string | null; obfsPassword: string | null; insecure: boolean };
|
||||
|
||||
function selectSubscriptionNode(nodes: string[], preferred: Sub2ApiEgressProxyConfig["preferredOutbound"]): SubscriptionNode {
|
||||
function selectSubscriptionNode(nodes: string[], preferred: Exclude<Sub2ApiEgressProxyConfig["preferredOutbound"], null>): SubscriptionNode {
|
||||
const analyzed = analyzeEgressProxySubscription(nodes, preferred);
|
||||
if (analyzed.selected === null) throw new Error(analyzed.diagnostics.error ?? `egressProxy preferredOutbound=${preferred} is absent from subscription; no fallback outbound was selected`);
|
||||
return analyzed.selected;
|
||||
}
|
||||
|
||||
function analyzeEgressProxySubscription(nodes: string[], preferred: Sub2ApiEgressProxyConfig["preferredOutbound"]): { diagnostics: EgressProxySubscriptionDiagnostics; selected: SubscriptionNode | null } {
|
||||
function analyzeEgressProxySubscription(nodes: string[], preferred: Exclude<Sub2ApiEgressProxyConfig["preferredOutbound"], null>): { diagnostics: EgressProxySubscriptionDiagnostics; selected: SubscriptionNode | null } {
|
||||
const parsed = nodes
|
||||
.map((uri, index) => ({ uri, sourceLine: index, node: parseSubscriptionNode(uri) }))
|
||||
.filter((entry): entry is { uri: string; sourceLine: number; node: SubscriptionNode } => entry.node !== null);
|
||||
@@ -2176,6 +2284,7 @@ function analyzeEgressProxySubscription(nodes: string[], preferred: Sub2ApiEgres
|
||||
const selectedFingerprint = selectedEntry === null ? null : fingerprintSecretValues({ candidate: selectedEntry.uri }, ["candidate"]);
|
||||
const diagnostics: EgressProxySubscriptionDiagnostics = {
|
||||
ok: selectedEntry !== null,
|
||||
sourceType: "subscription-url",
|
||||
preferredOutbound: preferred,
|
||||
candidateCount: nodes.length,
|
||||
supportedCount: parsed.length,
|
||||
@@ -2277,6 +2386,23 @@ function renderSingBoxConfig(node: SubscriptionNode, proxy: Sub2ApiEgressProxyCo
|
||||
obfs: node.obfsPassword === null ? undefined : { type: "salamander", password: node.obfsPassword },
|
||||
tls: { enabled: true, server_name: node.sni ?? node.host, insecure: node.insecure },
|
||||
};
|
||||
return renderSingBoxProxyConfig(outbound, proxy);
|
||||
}
|
||||
|
||||
function renderSingBoxMasterShadowsocksConfig(proxy: Sub2ApiEgressProxyConfig, password: string): string {
|
||||
const shadowsocks = proxy.masterShadowsocks;
|
||||
if (shadowsocks === null) throw new Error("masterShadowsocks config is required");
|
||||
return renderSingBoxProxyConfig({
|
||||
type: "shadowsocks",
|
||||
tag: "master-vpn",
|
||||
server: shadowsocks.serverHost,
|
||||
server_port: shadowsocks.serverPort,
|
||||
method: shadowsocks.method,
|
||||
password,
|
||||
}, proxy);
|
||||
}
|
||||
|
||||
function renderSingBoxProxyConfig(outbound: Record<string, unknown>, proxy: Sub2ApiEgressProxyConfig): string {
|
||||
const config = stripUndefined({
|
||||
log: { level: "info", timestamp: true },
|
||||
inbounds: [
|
||||
@@ -2931,9 +3057,9 @@ function applyScript(
|
||||
secretName: egressProxySecretMaterial.secretName,
|
||||
secretKey: egressProxySecretMaterial.secretKey,
|
||||
fingerprint: egressProxySecretMaterial.fingerprint,
|
||||
subscriptionBytes: egressProxySecretMaterial.subscriptionBytes,
|
||||
sourceBytes: egressProxySecretMaterial.sourceBytes,
|
||||
selectedOutbound: egressProxySecretMaterial.selectedOutbound,
|
||||
subscriptionDiagnostics: egressProxySecretMaterial.subscriptionDiagnostics,
|
||||
sourceDiagnostics: egressProxySecretMaterial.sourceDiagnostics,
|
||||
proxyUrl: egressProxySecretMaterial.proxyUrl,
|
||||
noProxy: egressProxySecretMaterial.noProxy,
|
||||
valuesPrinted: false,
|
||||
@@ -3837,14 +3963,14 @@ fi
|
||||
`;
|
||||
const egressProbeJson = proxy === null ? "None" : `{
|
||||
"enabled": True,
|
||||
"mode": "master-vpn-subscription-http-proxy",
|
||||
"mode": "${proxy.sourceType === "master-shadowsocks" ? "master-shadowsocks-http-proxy" : "master-vpn-subscription-http-proxy"}",
|
||||
"deploymentName": "${proxy.deploymentName}",
|
||||
"serviceName": "${proxy.serviceName}",
|
||||
"proxyUrl": "${proxyUrl}",
|
||||
"healthProbeUrl": "${proxy.healthProbeUrl}",
|
||||
"applyToSub2Api": ${proxy.applyToSub2Api ? "True" : "False"},
|
||||
"applyToSentinel": ${proxy.applyToSentinel ? "True" : "False"},
|
||||
"subscription": ${egressSubscriptionJson},
|
||||
"source": ${egressSubscriptionJson},
|
||||
"deployment": {
|
||||
"exitCode": egress_deploy_rc,
|
||||
"ready": deployment_ready(load("egress-deploy")),
|
||||
|
||||
Reference in New Issue
Block a user