fix(gateway): keep status helpers resilient to netif failures

This commit is contained in:
Codex
2026-03-22 22:36:48 +08:00
committed by Peter Steinberger
parent c0cbc7403b
commit 8c7d603f25
8 changed files with 176 additions and 8 deletions

View File

@@ -215,6 +215,36 @@ describe("gatherDaemonStatus", () => {
expect(status.rpc?.url).toBe("wss://override.example:18790");
});
it("uses fallback network details when interface discovery throws during status inspection", async () => {
daemonLoadedConfig = {
gateway: {
bind: "tailnet",
tls: { enabled: true },
auth: { token: "daemon-token" },
},
};
resolveGatewayBindHost.mockImplementationOnce(async () => {
throw new Error("uv_interface_addresses failed");
});
pickPrimaryTailnetIPv4.mockImplementationOnce(() => {
throw new Error("uv_interface_addresses failed");
});
const status = await gatherDaemonStatus({
rpc: {},
probe: true,
deep: false,
});
expect(status.gateway).toMatchObject({
bindMode: "tailnet",
bindHost: "127.0.0.1",
probeUrl: "wss://127.0.0.1:19001",
});
expect(status.gateway?.probeNote).toContain("interface discovery failed");
expect(status.gateway?.probeNote).toContain("tailnet addresses");
});
it("reuses command environment when reading runtime status", async () => {
serviceReadCommand.mockResolvedValueOnce({
programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"],

View File

@@ -74,6 +74,37 @@ type ResolvedGatewayStatus = {
probeUrlOverride: string | null;
};
function summarizeDisplayNetworkError(error: unknown): string {
if (error instanceof Error) {
const message = error.message.trim();
if (message) {
return message;
}
}
return "network interface discovery failed";
}
function fallbackBindHostForStatus(bindMode: GatewayBindMode, customBindHost?: string): string {
if (bindMode === "lan") {
return "0.0.0.0";
}
if (bindMode === "custom") {
return customBindHost?.trim() || "0.0.0.0";
}
return "127.0.0.1";
}
function appendProbeNote(
existing: string | undefined,
extra: string | undefined,
): string | undefined {
const values = [existing, extra].filter((value): value is string => Boolean(value?.trim()));
if (values.length === 0) {
return undefined;
}
return [...new Set(values)].join(" ");
}
export type DaemonStatus = {
service: {
label: string;
@@ -201,18 +232,34 @@ async function resolveGatewayStatusSummary(params: {
: "env/config";
const bindMode: GatewayBindMode = params.daemonCfg.gateway?.bind ?? "loopback";
const customBindHost = params.daemonCfg.gateway?.customBindHost;
const bindHost = await resolveGatewayBindHost(bindMode, customBindHost);
const tailnetIPv4 = pickPrimaryTailnetIPv4();
let bindHost: string;
let networkWarning: string | undefined;
try {
bindHost = await resolveGatewayBindHost(bindMode, customBindHost);
} catch (error) {
bindHost = fallbackBindHostForStatus(bindMode, customBindHost);
networkWarning = `Status is using fallback network details because interface discovery failed: ${summarizeDisplayNetworkError(error)}.`;
}
let tailnetIPv4: string | undefined;
try {
tailnetIPv4 = pickPrimaryTailnetIPv4();
} catch (error) {
networkWarning = appendProbeNote(
networkWarning,
`Status could not inspect tailnet addresses: ${summarizeDisplayNetworkError(error)}.`,
);
}
const probeHost = pickProbeHostForBind(bindMode, tailnetIPv4, customBindHost);
const probeUrlOverride = trimToUndefined(params.rpcUrlOverride) ?? null;
const scheme = params.daemonCfg.gateway?.tls?.enabled === true ? "wss" : "ws";
const probeUrl = probeUrlOverride ?? `${scheme}://${probeHost}:${daemonPort}`;
const probeNote =
let probeNote =
!probeUrlOverride && bindMode === "lan"
? `bind=lan listens on 0.0.0.0 (all interfaces); probing via ${probeHost}.`
: !probeUrlOverride && bindMode === "loopback"
? "Loopback-only gateway; only local clients can connect."
: undefined;
probeNote = appendProbeNote(probeNote, networkWarning);
return {
gateway: {