fix(bonjour): suppress ciao crash when networkInterfaces() is denied

Classify ciao interface-enumeration SystemErrors from restricted sandboxes and suppress mDNS advertising instead of letting the Gateway crash.
This commit is contained in:
TinyClaw
2026-04-28 02:30:43 +02:00
committed by GitHub
parent c72f8f357b
commit fb5b46ae48
3 changed files with 37 additions and 1 deletions

View File

@@ -359,6 +359,13 @@ export async function startGatewayBonjourAdvertiser(
if (classification.kind === "cancellation") {
logger.debug(`bonjour: ignoring unhandled ciao rejection: ${classification.formatted}`);
} else if (classification.kind === "interface-enumeration-failure") {
// Restricted sandboxes can refuse os.networkInterfaces(); mDNS cannot
// function without it, so surface a single warning and skip recovery.
// Recovery would just re-enter the same failing syscall.
logger.warn(
`bonjour: disabling mDNS — networkInterfaces() unavailable in this environment: ${classification.formatted}`,
);
} else {
const label =
classification.kind === "netmask-assertion" ? "netmask assertion" : "interface assertion";

View File

@@ -100,6 +100,27 @@ describe("bonjour-ciao", () => {
expect(ignoreCiaoUnhandledRejection(error)).toBe(true);
});
it("classifies networkInterfaces SystemError failures (restricted sandboxes)", () => {
const err = Object.assign(
new Error("A system error occurred: uv_interface_addresses returned Unknown system error 1"),
{ name: "SystemError" },
);
expect(classifyCiaoUnhandledRejection(err)).toEqual({
kind: "interface-enumeration-failure",
formatted:
"SystemError: A system error occurred: uv_interface_addresses returned Unknown system error 1",
});
});
it("suppresses networkInterfaces failures wrapped in cause chains", () => {
const inner = Object.assign(
new Error("A system error occurred: uv_interface_addresses returned Unknown system error 1"),
{ name: "SystemError" },
);
const wrapper = new Error("ciao NetworkManager init failed", { cause: inner });
expect(ignoreCiaoUnhandledRejection(wrapper)).toBe(true);
});
it("keeps unrelated rejections visible", () => {
expect(ignoreCiaoUnhandledRejection(new Error("boom"))).toBe(false);
});

View File

@@ -5,11 +5,16 @@ const CIAO_INTERFACE_ASSERTION_MESSAGE_RE =
/REACHED ILLEGAL STATE!?\s+IPV4 ADDRESS CHANGE FROM (?:DEFINED TO UNDEFINED|UNDEFINED TO DEFINED)!?/u;
const CIAO_NETMASK_ASSERTION_MESSAGE_RE =
/IP ADDRESS VERSION MUST MATCH\.\s+NETMASK CANNOT HAVE A VERSION DIFFERENT FROM THE ADDRESS!?/u;
// Restricted sandboxes (NemoClaw, Docker-in-Docker, k3s with locked-down policy)
// can refuse os.networkInterfaces(), which ciao calls during NetworkManager init.
// Node surfaces this as a SystemError mentioning the libuv syscall by name.
const CIAO_INTERFACE_ENUMERATION_FAILURE_RE = /\bUV_INTERFACE_ADDRESSES\b/u;
export type CiaoProcessErrorClassification =
| { kind: "cancellation"; formatted: string }
| { kind: "interface-assertion"; formatted: string }
| { kind: "netmask-assertion"; formatted: string };
| { kind: "netmask-assertion"; formatted: string }
| { kind: "interface-enumeration-failure"; formatted: string };
function collectCiaoProcessErrorCandidates(reason: unknown): unknown[] {
const queue: unknown[] = [reason];
@@ -64,6 +69,9 @@ export function classifyCiaoProcessError(reason: unknown): CiaoProcessErrorClass
if (CIAO_NETMASK_ASSERTION_MESSAGE_RE.test(message)) {
return { kind: "netmask-assertion", formatted };
}
if (CIAO_INTERFACE_ENUMERATION_FAILURE_RE.test(message)) {
return { kind: "interface-enumeration-failure", formatted };
}
}
return null;
}