fix(gateway): fail closed on unresolved discovery endpoints

This commit is contained in:
Peter Steinberger
2026-03-23 00:25:03 -07:00
parent 0b58829364
commit deecf68b59
9 changed files with 160 additions and 38 deletions

View File

@@ -81,7 +81,8 @@ vi.mock("../daemon/program-args.js", () => ({
}),
}));
vi.mock("../infra/bonjour-discovery.js", () => ({
vi.mock("../infra/bonjour-discovery.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../infra/bonjour-discovery.js")>()),
discoverGatewayBeacons: (opts: unknown) => discoverGatewayBeacons(opts),
}));
@@ -147,6 +148,7 @@ describe("gateway-cli coverage", () => {
displayName: "Studio",
domain: "openclaw.internal.",
host: "studio.openclaw.internal",
port: 18789,
lanHost: "studio.local",
tailnetDns: "studio.tailnet.ts.net",
gatewayPort: 18789,

View File

@@ -1,4 +1,8 @@
import type { GatewayBonjourBeacon } from "../../infra/bonjour-discovery.js";
import {
type GatewayBonjourBeacon,
pickResolvedGatewayHost,
pickResolvedGatewayPort,
} from "../../infra/bonjour-discovery.js";
import { colorize, theme } from "../../terminal/theme.js";
import { parseTimeoutMsWithFallback } from "../parse-timeout.js";
@@ -13,15 +17,14 @@ export function parseDiscoverTimeoutMs(raw: unknown, fallbackMs: number): number
export function pickBeaconHost(beacon: GatewayBonjourBeacon): string | null {
// Security: TXT records are unauthenticated. Prefer the resolved service endpoint (SRV/A/AAAA)
// over TXT-provided routing hints.
const host = beacon.host || beacon.tailnetDns || beacon.lanHost;
return host?.trim() ? host.trim() : null;
// and fail closed when discovery did not resolve a routable host.
return pickResolvedGatewayHost(beacon);
}
export function pickGatewayPort(beacon: GatewayBonjourBeacon): number {
export function pickGatewayPort(beacon: GatewayBonjourBeacon): number | null {
// Security: TXT records are unauthenticated. Prefer the resolved service port over TXT gatewayPort.
const port = beacon.port ?? beacon.gatewayPort ?? 18789;
return port > 0 ? port : 18789;
// Fail closed when discovery did not resolve a routable port.
return pickResolvedGatewayPort(beacon);
}
export function dedupeBeacons(beacons: GatewayBonjourBeacon[]): GatewayBonjourBeacon[] {
@@ -56,7 +59,7 @@ export function renderBeaconLines(beacon: GatewayBonjourBeacon, rich: boolean):
const host = pickBeaconHost(beacon);
const gatewayPort = pickGatewayPort(beacon);
const scheme = beacon.gatewayTls ? "wss" : "ws";
const wsUrl = host ? `${scheme}://${host}:${gatewayPort}` : null;
const wsUrl = host && gatewayPort ? `${scheme}://${host}:${gatewayPort}` : null;
const lines = [`- ${title} ${domain}`];

View File

@@ -444,13 +444,13 @@ describe("gateway discover routing helpers", () => {
expect(pickGatewayPort(beacon)).toBe(18789);
});
it("falls back to TXT host/port when resolve data is missing", () => {
it("fails closed when resolve data is missing", () => {
const beacon: GatewayBonjourBeacon = {
instanceName: "Test",
lanHost: "test-host.local",
gatewayPort: 18789,
};
expect(pickBeaconHost(beacon)).toBe("test-host.local");
expect(pickGatewayPort(beacon)).toBe(18789);
expect(pickBeaconHost(beacon)).toBeNull();
expect(pickGatewayPort(beacon)).toBeNull();
});
});