mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:31:06 +00:00
fix(bonjour): truncate mDNS service name and hostname to 63-byte DNS label limit
When the system hostname exceeds 63 bytes (common with Kubernetes pod names), the @homebridge/ciao DNS label encoder throws an AssertionError that crashes the gateway on startup. Add truncateToDnsLabel() that safely truncates UTF-8 strings at byte boundaries, applied to both the service instance name and hostname before passing them to ciao. Closes #37705 AI-assisted (built with Hermes orchestration).
This commit is contained in:
committed by
Peter Steinberger
parent
f5b01c1e0e
commit
9ac0b7edbc
@@ -735,6 +735,54 @@ describe("gateway bonjour advertiser", () => {
|
|||||||
await started.stop();
|
await started.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("truncates service name exceeding 63-byte DNS label limit", async () => {
|
||||||
|
const longHostname = "app-41627eae5842473f9e05f139ea307277-7f9477f4d6-lqqzf";
|
||||||
|
enableAdvertiserUnitMode(longHostname);
|
||||||
|
|
||||||
|
const destroy = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const advertise = vi.fn().mockResolvedValue(undefined);
|
||||||
|
mockCiaoService({ advertise, destroy });
|
||||||
|
|
||||||
|
const started = await startAdvertiser({
|
||||||
|
gatewayPort: 18789,
|
||||||
|
sshPort: 2222,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [gatewayCall] = createService.mock.calls as Array<[ServiceCall]>;
|
||||||
|
const serviceName = gatewayCall?.[0]?.name as string;
|
||||||
|
const hostname = gatewayCall?.[0]?.hostname as string;
|
||||||
|
|
||||||
|
// Both name and hostname must be within the 63-byte DNS label limit
|
||||||
|
expect(new TextEncoder().encode(serviceName).byteLength).toBeLessThanOrEqual(63);
|
||||||
|
expect(new TextEncoder().encode(hostname).byteLength).toBeLessThanOrEqual(63);
|
||||||
|
|
||||||
|
await started.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncates multi-byte hostname within DNS label byte limit", async () => {
|
||||||
|
// 21 CJK characters = 63 bytes in UTF-8, adding " (OpenClaw)" pushes over
|
||||||
|
const cjkHostname = "你".repeat(21);
|
||||||
|
enableAdvertiserUnitMode(cjkHostname);
|
||||||
|
|
||||||
|
const destroy = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const advertise = vi.fn().mockResolvedValue(undefined);
|
||||||
|
mockCiaoService({ advertise, destroy });
|
||||||
|
|
||||||
|
const started = await startAdvertiser({
|
||||||
|
gatewayPort: 18789,
|
||||||
|
sshPort: 2222,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [gatewayCall] = createService.mock.calls as Array<[ServiceCall]>;
|
||||||
|
const serviceName = gatewayCall?.[0]?.name as string;
|
||||||
|
|
||||||
|
expect(new TextEncoder().encode(serviceName).byteLength).toBeLessThanOrEqual(63);
|
||||||
|
// Should not end with a replacement character from incomplete multi-byte truncation
|
||||||
|
expect(serviceName).not.toMatch(/\uFFFD$/);
|
||||||
|
|
||||||
|
await started.stop();
|
||||||
|
});
|
||||||
|
|
||||||
it("uses system hostname when OPENCLAW_MDNS_HOSTNAME is unset", async () => {
|
it("uses system hostname when OPENCLAW_MDNS_HOSTNAME is unset", async () => {
|
||||||
// Allow advertiser to run in unit tests.
|
// Allow advertiser to run in unit tests.
|
||||||
delete process.env.VITEST;
|
delete process.env.VITEST;
|
||||||
|
|||||||
@@ -183,9 +183,24 @@ function resolveSystemMdnsHostname(): string | null {
|
|||||||
return firstLabel;
|
return firstLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_DNS_LABEL_BYTES = 63;
|
||||||
|
|
||||||
|
function truncateToDnsLabel(name: string): string {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const encoded = encoder.encode(name);
|
||||||
|
if (encoded.byteLength <= MAX_DNS_LABEL_BYTES) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
// Truncate at byte boundary, then decode back (TextDecoder handles incomplete sequences)
|
||||||
|
const truncated = encoded.slice(0, MAX_DNS_LABEL_BYTES);
|
||||||
|
const decoded = new TextDecoder("utf-8", { fatal: false }).decode(truncated);
|
||||||
|
// Strip any replacement character from incomplete multi-byte sequence at the end
|
||||||
|
return decoded.replace(/\uFFFD$/, "").trim() || "OpenClaw";
|
||||||
|
}
|
||||||
|
|
||||||
function safeServiceName(name: string) {
|
function safeServiceName(name: string) {
|
||||||
const trimmed = name.trim();
|
const trimmed = name.trim();
|
||||||
return trimmed.length > 0 ? trimmed : "OpenClaw";
|
return trimmed.length > 0 ? truncateToDnsLabel(trimmed) : "OpenClaw";
|
||||||
}
|
}
|
||||||
|
|
||||||
function prettifyInstanceName(name: string) {
|
function prettifyInstanceName(name: string) {
|
||||||
@@ -353,11 +368,12 @@ export async function startGatewayBonjourAdvertiser(
|
|||||||
|
|
||||||
const hostnameRaw =
|
const hostnameRaw =
|
||||||
process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || resolveSystemMdnsHostname() || "openclaw";
|
process.env.OPENCLAW_MDNS_HOSTNAME?.trim() || resolveSystemMdnsHostname() || "openclaw";
|
||||||
const hostname =
|
const hostname = truncateToDnsLabel(
|
||||||
hostnameRaw
|
hostnameRaw
|
||||||
.replace(/\.local$/i, "")
|
.replace(/\.local$/i, "")
|
||||||
.split(".")[0]
|
.split(".")[0]
|
||||||
.trim() || "openclaw";
|
.trim() || "openclaw",
|
||||||
|
);
|
||||||
const instanceName =
|
const instanceName =
|
||||||
typeof opts.instanceName === "string" && opts.instanceName.trim()
|
typeof opts.instanceName === "string" && opts.instanceName.trim()
|
||||||
? opts.instanceName.trim()
|
? opts.instanceName.trim()
|
||||||
|
|||||||
Reference in New Issue
Block a user