mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix: harden bonjour DNS label truncation (#73022)
This commit is contained in:
committed by
GitHub
parent
7d2d8af3ab
commit
e60905d754
@@ -34,7 +34,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory-core/dreaming: give narrative generation a 60-second timeout so slower local or remote models can finish instead of timing out at 15 seconds. Fixes #72837. (#72852) Thanks @RayWoo.
|
||||
- Plugins/hooks: inject each plugin's resolved config into internal hook event context without mutating the shared event object. (#72888) Thanks @jalapeno777.
|
||||
- Agents/ACP: pass the resolved ACP agent directory into media understanding so per-agent media caches and config are used for ACP-dispatched image turns. (#72832) Thanks @luyao618.
|
||||
- Gateway/Bonjour: truncate mDNS service names and host labels to the 63-byte DNS label limit without splitting multibyte characters. (#72809) Thanks @luyao618.
|
||||
- Gateway/Bonjour: truncate mDNS service names and host labels to the 63-byte DNS label limit at valid UTF-8 boundaries. (#72809) Thanks @luyao618.
|
||||
- Feishu: treat groups explicitly configured under channels.feishu.groups as admitted even when groupAllowFrom is empty, so per-group mention overrides work with the default allowlist policy. Fixes #67687. (#72789) Thanks @MoerAI.
|
||||
- Gateway/startup: keep hot Gateway boot paths on leaf config imports and add max-RSS reporting to the gateway startup bench so low-memory startup regressions are visible before release. Thanks @vincentkoc.
|
||||
- WebChat: read `chat.history` from active transcript branches, drop stale streamed assistant tails once final history catches up, and coalesce duplicate in-flight Control UI submits, so rewritten prompts, completed replies, and rapid send events no longer render or process twice. Fixes #72975, #72963, and #72974. Thanks @dmagdici, @lhtpluto, and @Benjamin5281999.
|
||||
|
||||
@@ -29,10 +29,19 @@ const {
|
||||
registerUnhandledRejectionHandler,
|
||||
logger,
|
||||
} = mocks;
|
||||
const dnsLabelEncoder = new TextEncoder();
|
||||
|
||||
const asString = (value: unknown, fallback: string) =>
|
||||
typeof value === "string" && value.trim() ? value : fallback;
|
||||
|
||||
function expectDnsLabelByteLength(value: string, expected: number) {
|
||||
expect(dnsLabelEncoder.encode(value).byteLength).toBe(expected);
|
||||
}
|
||||
|
||||
function expectDnsLabelWithinLimit(value: string) {
|
||||
expect(dnsLabelEncoder.encode(value).byteLength).toBeLessThanOrEqual(63);
|
||||
}
|
||||
|
||||
function enableAdvertiserUnitMode(hostname = "test-host") {
|
||||
// Allow advertiser to run in unit tests.
|
||||
delete process.env.VITEST;
|
||||
@@ -735,8 +744,32 @@ describe("gateway bonjour advertiser", () => {
|
||||
await started.stop();
|
||||
});
|
||||
|
||||
it("truncates service name exceeding 63-byte DNS label limit", async () => {
|
||||
const longHostname = "app-41627eae5842473f9e05f139ea307277-7f9477f4d6-lqqzf-abcdefghi";
|
||||
it("truncates reported Kubernetes service name at the DNS label byte limit", async () => {
|
||||
const reportedHostname = "app-41627eae5842473f9e05f139ea307277-7f9477f4d6-lqqzf";
|
||||
enableAdvertiserUnitMode(reportedHostname);
|
||||
|
||||
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;
|
||||
|
||||
expectDnsLabelByteLength(`${reportedHostname} (OpenClaw)`, 64);
|
||||
expect(hostname).toBe(reportedHostname);
|
||||
expectDnsLabelWithinLimit(serviceName);
|
||||
|
||||
await started.stop();
|
||||
});
|
||||
|
||||
it("truncates host labels exceeding the 63-byte DNS label limit", async () => {
|
||||
const longHostname = "app-41627eae5842473f9e05f139ea307277-7f9477f4d6-lqqzf-abcdefghij";
|
||||
enableAdvertiserUnitMode(longHostname);
|
||||
|
||||
const destroy = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -752,10 +785,11 @@ describe("gateway bonjour advertiser", () => {
|
||||
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);
|
||||
expectDnsLabelByteLength(longHostname, 64);
|
||||
expectDnsLabelByteLength(hostname, 63);
|
||||
expect(hostname).toBe(longHostname.slice(0, -1));
|
||||
expect(hostname).not.toMatch(/-$/);
|
||||
expectDnsLabelWithinLimit(serviceName);
|
||||
|
||||
await started.stop();
|
||||
});
|
||||
@@ -777,8 +811,7 @@ describe("gateway bonjour advertiser", () => {
|
||||
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
|
||||
expectDnsLabelWithinLimit(serviceName);
|
||||
expect(serviceName).not.toMatch(/\uFFFD$/);
|
||||
|
||||
await started.stop();
|
||||
|
||||
@@ -184,23 +184,22 @@ function resolveSystemMdnsHostname(): string | null {
|
||||
}
|
||||
|
||||
const MAX_DNS_LABEL_BYTES = 63;
|
||||
const utf8Encoder = new TextEncoder();
|
||||
|
||||
function truncateToDnsLabel(name: string, fallback = "OpenClaw"): string {
|
||||
const encoder = new TextEncoder();
|
||||
const encoded = encoder.encode(name);
|
||||
const encoded = utf8Encoder.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$/, "")
|
||||
.replace(/-+$/, "")
|
||||
.trim() || fallback
|
||||
);
|
||||
for (let end = MAX_DNS_LABEL_BYTES; end > 0; end -= 1) {
|
||||
try {
|
||||
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(encoded.subarray(0, end));
|
||||
return decoded.replace(/-+$/, "").trim() || fallback;
|
||||
} catch {
|
||||
// Try the next shorter prefix until the byte slice ends on a UTF-8 boundary.
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function safeServiceName(name: string) {
|
||||
|
||||
Reference in New Issue
Block a user