fix(platform-infra): expose egress proxy node diagnostics (#820)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-24 12:12:49 +08:00
committed by GitHub
parent 2b3b05c2d6
commit e28fc7ab60
+164 -44
View File
@@ -217,12 +217,35 @@ interface EgressProxySecretMaterial {
fingerprint: string;
subscriptionBytes: number;
selectedOutbound: string;
subscriptionDiagnostics: EgressProxySubscriptionDiagnostics;
configJson: string;
proxyUrl: string;
noProxy: string;
valuesPrinted: false;
}
interface EgressProxySubscriptionCandidateSummary {
sourceLine: number;
kind: SubscriptionNode["kind"];
fingerprint: string;
paramKeys: string[];
selected: boolean;
}
interface EgressProxySubscriptionDiagnostics {
ok: boolean;
preferredOutbound: Sub2ApiEgressProxyConfig["preferredOutbound"];
candidateCount: number;
supportedCount: number;
unsupportedCount: number;
byKind: Record<string, number>;
selectedOutbound: string | null;
selectedFingerprint: string | null;
candidates: EgressProxySubscriptionCandidateSummary[];
error?: string;
valuesPrinted: false;
}
interface ManagedResourceCleanupPlan {
externalDbState: {
enabled: boolean;
@@ -2045,26 +2068,29 @@ function prepareEgressProxySecret(sub2api: Sub2ApiConfig, target: Sub2ApiTargetC
});
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 selected = selectSubscriptionNode(nodes, proxy.preferredOutbound);
const configJson = renderSingBoxConfig(selected, proxy);
const proxyUrl = `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`;
const noProxy = proxy.noProxy.join(",");
return {
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"),
selectedOutbound: selected.kind,
configJson,
proxyUrl,
noProxy,
valuesPrinted: false,
};
fingerprint: fingerprintSecretValues({ subscription: subscription.body, configJson }, ["subscription", "configJson"]),
subscriptionBytes: Buffer.byteLength(subscription.body, "utf8"),
selectedOutbound: selected.kind,
subscriptionDiagnostics: analyzed.diagnostics,
configJson,
proxyUrl,
noProxy,
valuesPrinted: false,
};
}
function fetchSubscription(subscriptionUrl: string): { body: string } {
@@ -2091,17 +2117,87 @@ function decodeSubscription(raw: string): string {
}
}
function safeEgressProxySubscriptionDiagnostics(sub2api: Sub2ApiConfig, proxy: Sub2ApiEgressProxyConfig): EgressProxySubscriptionDiagnostics {
try {
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`,
});
const subscriptionUrl = requiredEnvValue(source.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);
return analyzeEgressProxySubscription(nodes, proxy.preferredOutbound).diagnostics;
} catch (error) {
return {
ok: false,
preferredOutbound: proxy.preferredOutbound,
candidateCount: 0,
supportedCount: 0,
unsupportedCount: 0,
byKind: {},
selectedOutbound: null,
selectedFingerprint: null,
candidates: [],
error: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500),
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 {
const parsed = nodes.map(parseSubscriptionNode).filter((node): node is SubscriptionNode => node !== null);
const preferredNode = parsed.find((node) => node.kind === preferred);
if (preferredNode !== undefined) return preferredNode;
const fallback = parsed.find((node) => node.kind === "vless-reality") ?? parsed.find((node) => node.kind === "hysteria2");
if (fallback !== undefined) return fallback;
throw new Error("master VPN subscription does not contain a supported vless-reality or hysteria2 node");
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 } {
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);
const selectedEntry = parsed.find((entry) => entry.node.kind === preferred) ?? null;
const byKind: Record<string, number> = {};
for (const entry of parsed) byKind[entry.node.kind] = (byKind[entry.node.kind] ?? 0) + 1;
const candidates = parsed.map((entry): EgressProxySubscriptionCandidateSummary => {
const fingerprint = fingerprintSecretValues({ candidate: entry.uri }, ["candidate"]);
return {
sourceLine: entry.sourceLine,
kind: entry.node.kind,
fingerprint,
paramKeys: subscriptionUriParamKeys(entry.uri),
selected: selectedEntry !== null && entry.sourceLine === selectedEntry.sourceLine,
};
});
const selectedFingerprint = selectedEntry === null ? null : fingerprintSecretValues({ candidate: selectedEntry.uri }, ["candidate"]);
const diagnostics: EgressProxySubscriptionDiagnostics = {
ok: selectedEntry !== null,
preferredOutbound: preferred,
candidateCount: nodes.length,
supportedCount: parsed.length,
unsupportedCount: nodes.length - parsed.length,
byKind,
selectedOutbound: selectedEntry?.node.kind ?? null,
selectedFingerprint,
candidates,
valuesPrinted: false,
};
if (selectedEntry === null) {
diagnostics.error = `egressProxy preferredOutbound=${preferred} is absent from subscription; no fallback outbound was selected`;
}
return { diagnostics, selected: selectedEntry?.node ?? null };
}
function subscriptionUriParamKeys(uri: string): string[] {
try {
return Array.from(new URL(uri).searchParams.keys()).sort();
} catch {
return [];
}
}
function parseSubscriptionNode(uri: string): SubscriptionNode | null {
@@ -2834,13 +2930,14 @@ function applyScript(
sourcePath: egressProxySecretMaterial.sourcePath,
secretName: egressProxySecretMaterial.secretName,
secretKey: egressProxySecretMaterial.secretKey,
fingerprint: egressProxySecretMaterial.fingerprint,
subscriptionBytes: egressProxySecretMaterial.subscriptionBytes,
selectedOutbound: egressProxySecretMaterial.selectedOutbound,
proxyUrl: egressProxySecretMaterial.proxyUrl,
noProxy: egressProxySecretMaterial.noProxy,
valuesPrinted: false,
}))})`;
fingerprint: egressProxySecretMaterial.fingerprint,
subscriptionBytes: egressProxySecretMaterial.subscriptionBytes,
selectedOutbound: egressProxySecretMaterial.selectedOutbound,
subscriptionDiagnostics: egressProxySecretMaterial.subscriptionDiagnostics,
proxyUrl: egressProxySecretMaterial.proxyUrl,
noProxy: egressProxySecretMaterial.noProxy,
valuesPrinted: false,
}))})`;
return `
set -u
tmp="$(mktemp -d)"
@@ -3693,6 +3790,9 @@ function validateExternalActiveScript(sub2api: Sub2ApiConfig, target: Sub2ApiTar
const exposure = target.publicExposure?.enabled ? target.publicExposure : null;
const proxy = target.egressProxy?.enabled ? target.egressProxy : null;
const proxyUrl = proxy === null ? "" : `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`;
const egressSubscriptionDiagnostics = proxy === null ? null : safeEgressProxySubscriptionDiagnostics(sub2api, proxy);
const egressSubscriptionJson = egressSubscriptionDiagnostics === null ? "None" : `json.loads(${JSON.stringify(JSON.stringify(egressSubscriptionDiagnostics))})`;
const egressSubscriptionOkExpr = egressSubscriptionDiagnostics === null ? "True" : (egressSubscriptionDiagnostics.ok ? "True" : "False");
const publicProbeBlock = exposure === null ? `
public_dns_rc=0
public_probe_rc=0
@@ -3741,13 +3841,14 @@ fi
"deploymentName": "${proxy.deploymentName}",
"serviceName": "${proxy.serviceName}",
"proxyUrl": "${proxyUrl}",
"healthProbeUrl": "${proxy.healthProbeUrl}",
"applyToSub2Api": ${proxy.applyToSub2Api ? "True" : "False"},
"applyToSentinel": ${proxy.applyToSentinel ? "True" : "False"},
"deployment": {
"exitCode": egress_deploy_rc,
"ready": deployment_ready(load("egress-deploy")),
"stderr": text("egress-deploy.err", 2000),
"healthProbeUrl": "${proxy.healthProbeUrl}",
"applyToSub2Api": ${proxy.applyToSub2Api ? "True" : "False"},
"applyToSentinel": ${proxy.applyToSentinel ? "True" : "False"},
"subscription": ${egressSubscriptionJson},
"deployment": {
"exitCode": egress_deploy_rc,
"ready": deployment_ready(load("egress-deploy")),
"stderr": text("egress-deploy.err", 2000),
},
"service": {
"exitCode": egress_svc_rc,
@@ -3755,14 +3856,15 @@ fi
"clusterIP": ((load("egress-svc") or {}).get("spec") or {}).get("clusterIP"),
"stderr": text("egress-svc.err", 2000),
},
"probe": {
"exitCode": egress_probe_rc,
"stdout": text("egress-probe.out", 2000),
"stderr": text("egress-probe.err", 2000),
},
"valuesPrinted": False,
}`;
const egressProbeOkExpr = proxy === null ? "True" : "(egress_deploy_rc == 0 and egress_svc_rc == 0 and deployment_ready(load('egress-deploy')) and egress_probe_rc == 0)";
"probe": {
"exitCode": egress_probe_rc,
"failureFamily": egress_failure_family(egress_probe_rc, text("egress-probe.err", 2000), text("egress-probe.out", 2000)),
"stdout": text("egress-probe.out", 2000),
"stderr": text("egress-probe.err", 2000),
},
"valuesPrinted": False,
}`;
const egressProbeOkExpr = proxy === null ? "True" : `(egress_deploy_rc == 0 and egress_svc_rc == 0 and deployment_ready(load('egress-deploy')) and egress_probe_rc == 0 and ${egressSubscriptionOkExpr})`;
const publicProbeJson = exposure === null ? "None" : `{
"enabled": True,
"publicBaseUrl": "${exposure.publicBaseUrl}",
@@ -3860,6 +3962,24 @@ def text(name, limit=4000):
except FileNotFoundError:
return ""
def egress_failure_family(exit_code, stderr, stdout):
if exit_code == 0:
return "ok"
combined = f"{stderr}\\n{stdout}".lower()
if "connection refused" in combined or "could not connect" in combined:
return "proxy-unreachable-or-refused"
if "connection reset" in combined or "recv failure" in combined:
return "proxy-upstream-reset"
if "timed out" in combined or "timeout" in combined or "deadline exceeded" in combined:
return "proxy-upstream-timeout"
if "empty reply" in combined or "eof" in combined:
return "proxy-upstream-eof"
if "could not resolve" in combined or "no such host" in combined:
return "dns-failure"
if "status=502" in combined or "http 502" in combined:
return "upstream-http-502"
return "unknown"
def is_allow_all_network_policy(item):
if not isinstance(item, dict):
return False