fix: harden local TLS gateway probes (#61935) (thanks @ThanhNguyxn07)

This commit is contained in:
Peter Steinberger
2026-04-06 15:43:15 +01:00
parent b77964f704
commit baf4119ae3
4 changed files with 52 additions and 0 deletions

View File

@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
- TUI/terminal: restore Kitty keyboard protocol and `modifyOtherKeys` state on TUI exit and fatal CLI crashes so parent shells stop inheriting broken keyboard input after `openclaw tui` exits. (#49130) Thanks @biefan.
- Docs/i18n: relocalize final localized-page links after translation so generated locale pages stop keeping stale English-root links when targets appear later in the same run. (#61796) thanks @hxy91819.
- iOS/Watch exec approvals: keep Apple Watch review and approval recovery working while the iPhone is locked or backgrounded, including background-safe reconnects, persisted pending approvals, notification cleanup, and APNs-backed watch refresh recovery. (#61757) Thanks @ngutman.
- Gateway/status: probe local TLS gateways over `wss://`, forward the local cert fingerprint for self-signed loopback probes, and warn when the local TLS runtime cannot load the configured cert. (#61935) Thanks @ThanhNguyxn07.
## 2026.4.5

View File

@@ -664,6 +664,45 @@ describe("gateway-status command", () => {
);
});
it("warns when local TLS is enabled but the certificate fingerprint cannot be loaded", async () => {
const { runtime, runtimeLogs } = createRuntimeCapture();
probeGateway.mockClear();
loadGatewayTlsRuntime.mockResolvedValueOnce({
enabled: false,
required: true,
error: "gateway tls: cert/key missing",
});
readBestEffortConfig.mockResolvedValueOnce({
gateway: {
mode: "local",
tls: { enabled: true },
auth: { mode: "token", token: "ltok" },
},
} as never);
await runGatewayStatus(runtime, { timeout: "15000", json: true });
expect(probeGateway).toHaveBeenCalledWith(
expect.objectContaining({
url: "wss://127.0.0.1:18789",
tlsFingerprint: undefined,
}),
);
const parsed = JSON.parse(runtimeLogs.join("\n")) as {
warnings?: Array<{ code?: string; message?: string; targetIds?: string[] }>;
};
expect(parsed.warnings).toContainEqual(
expect.objectContaining({
code: "local_tls_runtime_unavailable",
targetIds: ["localLoopback"],
}),
);
expect(
parsed.warnings?.find((warning) => warning.code === "local_tls_runtime_unavailable")?.message,
).toContain("gateway tls: cert/key missing");
});
it("passes the full caller timeout through to local loopback probes", async () => {
const { runtime } = createRuntimeCapture();
probeGateway.mockClear();

View File

@@ -122,6 +122,10 @@ export async function gatewayStatusCommand(
sshTarget: probePass.sshTarget,
sshTunnelStarted: probePass.sshTunnelStarted,
sshTunnelError: probePass.sshTunnelError,
localTlsLoadError:
localTlsRuntime && !localTlsRuntime.enabled && localTlsRuntime.required
? (localTlsRuntime.error ?? "gateway tls is enabled but local TLS runtime could not load")
: null,
});
const primary = pickPrimaryProbedTarget(probePass.probed);

View File

@@ -32,6 +32,7 @@ export function buildGatewayStatusWarnings(params: {
sshTarget: string | null;
sshTunnelStarted: boolean;
sshTunnelError: string | null;
localTlsLoadError?: string | null;
}): GatewayStatusWarning[] {
const reachable = params.probed.filter((entry) => isProbeReachable(entry.probe));
const degradedScopeLimited = params.probed.filter((entry) =>
@@ -46,6 +47,13 @@ export function buildGatewayStatusWarnings(params: {
: "SSH tunnel failed to start; falling back to direct probes.",
});
}
if (params.localTlsLoadError) {
warnings.push({
code: "local_tls_runtime_unavailable",
message: `Local gateway TLS is enabled but OpenClaw could not load the local certificate fingerprint: ${params.localTlsLoadError}`,
targetIds: ["localLoopback"],
});
}
if (reachable.length > 1) {
warnings.push({
code: "multiple_gateways",