mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix: reject invalid discovery ports
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { discoverGatewayBeacons } from "./bonjour-discovery.js";
|
||||
import { discoverGatewayBeacons, resolveGatewayDiscoveryEndpoint } from "./bonjour-discovery.js";
|
||||
|
||||
const WIDE_AREA_DOMAIN = "openclaw.internal.";
|
||||
|
||||
@@ -187,6 +187,52 @@ describe("bonjour-discovery", () => {
|
||||
expect(beacon.txt?.displayName).toBe("Peter’s Mac Studio");
|
||||
});
|
||||
|
||||
it("rejects malformed and out-of-range advertised ports", async () => {
|
||||
const run = vi.fn(async (argv: string[]) => {
|
||||
const domain = argv[3] ?? "";
|
||||
if (argv[0] === "dns-sd" && argv[1] === "-B" && domain === "local.") {
|
||||
return {
|
||||
stdout: ["Add 2 3 local. _openclaw-gw._tcp. Broken Gateway", ""].join("\n"),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (argv[0] === "dns-sd" && argv[1] === "-L") {
|
||||
return {
|
||||
stdout: [
|
||||
"Broken Gateway._openclaw-gw._tcp. can be reached at broken.local:18789abc",
|
||||
"txtvers=1 displayName=Broken gatewayPort=70000 sshPort=22x",
|
||||
"",
|
||||
].join("\n"),
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`unexpected argv: ${argv.join(" ")}`);
|
||||
});
|
||||
|
||||
const beacons = await discoverGatewayBeacons({
|
||||
platform: "darwin",
|
||||
timeoutMs: 800,
|
||||
domains: ["local."],
|
||||
run: run as unknown as typeof runCommandWithTimeout,
|
||||
});
|
||||
|
||||
expect(beacons).toHaveLength(1);
|
||||
const beacon = beacons[0] as BeaconRecord;
|
||||
expect(beacon.host).toBe("broken.local");
|
||||
expect(beacon.port).toBeUndefined();
|
||||
expect(beacon.gatewayPort).toBeUndefined();
|
||||
expect(beacon.sshPort).toBeUndefined();
|
||||
expect(resolveGatewayDiscoveryEndpoint(beacon)).toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to tailnet DNS probing for wide-area when split DNS is not configured", async () => {
|
||||
const calls: Array<{ argv: string[]; timeoutMs: number }> = [];
|
||||
const zone = WIDE_AREA_DOMAIN.replace(/\.$/, "");
|
||||
|
||||
@@ -37,7 +37,13 @@ export function resolveGatewayDiscoveryEndpoint(
|
||||
): GatewayDiscoveryResolvedEndpoint | null {
|
||||
const host = beacon.host?.trim();
|
||||
const port = beacon.port;
|
||||
if (!host || typeof port !== "number" || !Number.isFinite(port) || port <= 0) {
|
||||
if (
|
||||
!host ||
|
||||
typeof port !== "number" ||
|
||||
!Number.isSafeInteger(port) ||
|
||||
port <= 0 ||
|
||||
port > MAX_TCP_PORT
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const gatewayTls = beacon.gatewayTls === true;
|
||||
@@ -70,6 +76,7 @@ export type GatewayBonjourDiscoverOpts = {
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 2000;
|
||||
const GATEWAY_SERVICE_TYPE = "_openclaw-gw._tcp";
|
||||
const MAX_TCP_PORT = 65_535;
|
||||
|
||||
function decodeDnsSdEscapes(value: string): string {
|
||||
let decoded = false;
|
||||
@@ -146,9 +153,9 @@ function parseDigSrv(stdout: string): { host: string; port: number } | null {
|
||||
if (parts.length < 4) {
|
||||
return null;
|
||||
}
|
||||
const port = Number.parseInt(parts[2] ?? "", 10);
|
||||
const port = parsePortOrUndefined(parts[2]);
|
||||
const hostRaw = parts[3] ?? "";
|
||||
if (!Number.isFinite(port) || port <= 0) {
|
||||
if (port === undefined) {
|
||||
return null;
|
||||
}
|
||||
const host = hostRaw.replace(/\.$/, "");
|
||||
@@ -193,11 +200,12 @@ function parseTailscaleStatusIPv4s(stdout: string): string[] {
|
||||
return uniqueStrings(out);
|
||||
}
|
||||
|
||||
function parseIntOrNull(value: string | undefined): number | undefined {
|
||||
function parsePortOrUndefined(value: string | undefined): number | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return parseStrictInteger(value);
|
||||
const parsed = parseStrictInteger(value);
|
||||
return parsed !== undefined && parsed > 0 && parsed <= MAX_TCP_PORT ? parsed : undefined;
|
||||
}
|
||||
|
||||
function parseTxtTokens(tokens: string[]): Record<string, string> {
|
||||
@@ -246,12 +254,12 @@ function parseDnsSdResolve(stdout: string, instanceName: string): GatewayBonjour
|
||||
}
|
||||
|
||||
if (line.includes("can be reached at")) {
|
||||
const match = line.match(/can be reached at\s+([^\s:]+):(\d+)/i);
|
||||
const match = line.match(/can be reached at\s+([^\s:]+):([^\s]+)/i);
|
||||
if (match?.[1]) {
|
||||
beacon.host = match[1].replace(/\.$/, "");
|
||||
}
|
||||
if (match?.[2]) {
|
||||
beacon.port = parseIntOrNull(match[2]);
|
||||
beacon.port = parsePortOrUndefined(match[2]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -275,8 +283,8 @@ function parseDnsSdResolve(stdout: string, instanceName: string): GatewayBonjour
|
||||
if (txt.cliPath) {
|
||||
beacon.cliPath = txt.cliPath;
|
||||
}
|
||||
beacon.gatewayPort = parseIntOrNull(txt.gatewayPort);
|
||||
beacon.sshPort = parseIntOrNull(txt.sshPort);
|
||||
beacon.gatewayPort = parsePortOrUndefined(txt.gatewayPort);
|
||||
beacon.sshPort = parsePortOrUndefined(txt.sshPort);
|
||||
if (txt.gatewayTls) {
|
||||
const raw = normalizeOptionalLowercaseString(txt.gatewayTls);
|
||||
beacon.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
|
||||
@@ -450,8 +458,8 @@ async function discoverWideAreaViaTailnetDns(
|
||||
host: srvParsed.host,
|
||||
port: srvParsed.port,
|
||||
txt: Object.keys(txtMap).length ? txtMap : undefined,
|
||||
gatewayPort: parseIntOrNull(txtMap.gatewayPort),
|
||||
sshPort: parseIntOrNull(txtMap.sshPort),
|
||||
gatewayPort: parsePortOrUndefined(txtMap.gatewayPort),
|
||||
sshPort: parsePortOrUndefined(txtMap.sshPort),
|
||||
tailnetDns: txtMap.tailnetDns || undefined,
|
||||
cliPath: txtMap.cliPath || undefined,
|
||||
};
|
||||
@@ -516,7 +524,7 @@ function parseAvahiBrowse(stdout: string): GatewayBonjourBeacon[] {
|
||||
if (trimmed.startsWith("port =")) {
|
||||
const match = trimmed.match(/port\s*=\s*\[(\d+)\]/);
|
||||
if (match?.[1]) {
|
||||
current.port = parseIntOrNull(match[1]);
|
||||
current.port = parsePortOrUndefined(match[1]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -537,8 +545,8 @@ function parseAvahiBrowse(stdout: string): GatewayBonjourBeacon[] {
|
||||
if (txt.cliPath) {
|
||||
current.cliPath = txt.cliPath;
|
||||
}
|
||||
current.gatewayPort = parseIntOrNull(txt.gatewayPort);
|
||||
current.sshPort = parseIntOrNull(txt.sshPort);
|
||||
current.gatewayPort = parsePortOrUndefined(txt.gatewayPort);
|
||||
current.sshPort = parsePortOrUndefined(txt.sshPort);
|
||||
if (txt.gatewayTls) {
|
||||
const raw = normalizeOptionalLowercaseString(txt.gatewayTls);
|
||||
current.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
|
||||
|
||||
Reference in New Issue
Block a user