paperclip/packages/tailscale-https-broker/src/broker-core.test.ts

594 lines
26 KiB
TypeScript

import { mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { MemoryAuditSink } from "./audit.js";
import { BrokerCore, type CliResult, type ListenerOwnership } from "./broker-core.js";
import { defaultIsAllowedPort } from "./port-policy.js";
import type { BrokerRequest, PeerCredentials } from "./types.js";
const HOST = "paperclip-dev.tail29c1aa.ts.net";
const BIN = "/usr/bin/tailscale";
const RUNTIME_A = "2af79bb1-ecc5-4410-8438-091be135a921";
const RUNTIME_B = "3108ef8e-5ed0-41d9-b561-6b41c41b8545";
const PEER: PeerCredentials = { uid: 999, gid: 987, pid: 4242 };
/** In-memory tailscale serve fake driven by exact argv vectors. */
class FakeTailscale {
ports = new Map<number, string>([[443, "http://127.0.0.1:3100"]]);
failExposePort: number | null = null;
failRemove = false;
sideEffectOnExpose: number | null = null;
retargetPrimaryOnExpose = false;
exposeCalls = 0;
removeCalls = 0;
statusCalls = 0;
run = (argv: string[]): CliResult => {
const [, sub, a2, a3] = argv;
if (sub === "serve" && a2 === "status") {
this.statusCalls += 1;
return { code: 0, stdout: this.statusJson(), stderr: "", timedOut: false };
}
if (sub === "serve" && a2 === "--bg") {
this.exposeCalls += 1;
const port = Number(a3.replace("--https=", ""));
if (this.failExposePort === port) return { code: 1, stdout: "", stderr: "boom", timedOut: false };
this.ports.set(port, `http://127.0.0.1:${port}`);
if (this.sideEffectOnExpose) this.ports.set(this.sideEffectOnExpose, `http://127.0.0.1:${this.sideEffectOnExpose}`);
if (this.retargetPrimaryOnExpose) this.ports.set(443, "http://127.0.0.1:9999");
return { code: 0, stdout: "", stderr: "", timedOut: false };
}
if (sub === "serve" && a2.startsWith("--https=") && a3 === "off") {
this.removeCalls += 1;
if (this.failRemove) return { code: 1, stdout: "", stderr: "no", timedOut: false };
this.ports.delete(Number(a2.replace("--https=", "")));
return { code: 0, stdout: "", stderr: "", timedOut: false };
}
return { code: 2, stdout: "", stderr: "unknown", timedOut: false };
};
private statusJson(): string {
const TCP: Record<string, unknown> = {};
const Web: Record<string, unknown> = {};
for (const [port, proxy] of this.ports) {
TCP[String(port)] = { HTTPS: true };
Web[`${HOST}:${port}`] = { Handlers: { "/": { Proxy: proxy } } };
}
return JSON.stringify({ TCP, Web });
}
}
function makeCore(
fake: FakeTailscale,
registryPath: string,
ownership: (port: number) => ListenerOwnership = () => ({
present: true,
loopbackOnly: true,
ownerUidMatches: true,
inodes: ["5001"],
}),
nowIso: () => string = () => "2026-08-11T00:00:00.000Z",
) {
const audit = new MemoryAuditSink();
const core = new BrokerCore({
tailscaleBinPath: BIN,
registryPath,
auditSink: audit,
peerPolicy: { allowedUids: new Set([999]), allowedGids: new Set([987]) },
nodeIdentity: "node-1",
isAllowedPort: defaultIsAllowedPort,
deps: {
runTailscale: fake.run,
verifyListenerOwnership: ownership,
nowIso,
},
});
return { core, audit };
}
const reserveReq = (runtimeId = RUNTIME_A, port = 42010): BrokerRequest => ({
op: "reserve",
requestId: "req-e",
runtimeId,
listeners: [{ purpose: "app", port }],
});
async function reserveAndExpose(core: BrokerCore, runtimeId = RUNTIME_A, port = 42010, peer = PEER) {
const reserved = await core.handle(reserveReq(runtimeId, port), peer);
if (!reserved.ok || reserved.op !== "reserve") throw new Error("reserve failed");
return await core.handle({
op: "expose",
requestId: "req-x",
runtimeId,
handle: reserved.handle,
}, peer);
}
let dir: string;
let registryPath: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "broker-test-"));
registryPath = join(dir, "registry.json");
});
afterEach(() => {
// tmp dir is left for the OS to reap; tests use unique dirs.
});
describe("expose", () => {
it("reserves before bind, then exposes a same-number loopback listener and persists a lease", async () => {
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath);
const reserved = await core.handle(reserveReq(), PEER);
expect(reserved.ok).toBe(true);
expect(fake.ports.has(42010)).toBe(false);
const beforeList = await core.handle({ op: "list", requestId: "before-list" }, PEER);
expect(beforeList).toMatchObject({ ok: true, listeners: [] });
if (!reserved.ok || reserved.op !== "reserve") throw new Error("expected reserve ok");
const res = await core.handle({ op: "expose", requestId: "req-x", runtimeId: RUNTIME_A, handle: reserved.handle }, PEER);
expect(res.ok).toBe(true);
if (!res.ok || res.op !== "expose") throw new Error("expected expose ok");
expect(res.publicPorts).toEqual([42010]);
expect(res.handle).toMatch(/^[A-Za-z0-9_-]{16,}$/);
expect(fake.ports.get(42010)).toBe("http://127.0.0.1:42010");
const registry = JSON.parse(readFileSync(registryPath, "utf8"));
expect(registry.leases[0].ports).toEqual([42010]);
expect(registry.leases[0].state).toBe("exposed");
// Handle is persisted server-side but never appears in list output.
});
it("is idempotent: re-exposing the same port does not double-apply", async () => {
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath);
const firstReservation = await core.handle(reserveReq(), PEER);
if (!firstReservation.ok || firstReservation.op !== "reserve") throw new Error("reserve failed");
await core.handle({ op: "expose", requestId: "first", runtimeId: RUNTIME_A, handle: firstReservation.handle }, PEER);
const before = fake.exposeCalls;
const repeatedReservation = await core.handle(reserveReq(), PEER);
expect(repeatedReservation).toMatchObject({ ok: true, handle: firstReservation.handle });
const res = await core.handle({ op: "expose", requestId: "second", runtimeId: RUNTIME_A, handle: firstReservation.handle }, PEER);
expect(res.ok).toBe(true);
expect(fake.exposeCalls).toBe(before); // no additional CLI mutation
});
it("rejects and releases an expired reservation before any Serve mutation", async () => {
const fake = new FakeTailscale();
let now = "2026-08-11T00:00:00.000Z";
const { core } = makeCore(fake, registryPath, undefined, () => now);
const reserved = await core.handle(reserveReq(), PEER);
if (!reserved.ok || reserved.op !== "reserve") throw new Error("reserve failed");
now = "2026-08-11T00:06:00.000Z";
const result = await core.handle({
op: "expose",
requestId: "expired",
runtimeId: RUNTIME_A,
handle: reserved.handle,
}, PEER);
expect(result).toMatchObject({ ok: false, code: "reservation_expired" });
expect(fake.exposeCalls).toBe(0);
expect(JSON.parse(readFileSync(registryPath, "utf8")).leases).toEqual([]);
});
it("rejects an unauthorized peer without mutating serve", async () => {
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath);
const res = await core.handle(reserveReq(), { uid: 1000, gid: 987, pid: 1 });
expect(res.ok).toBe(false);
if (res.ok) throw new Error("unreachable");
expect(res.code).toBe("unauthorized_peer");
expect(fake.ports.has(42010)).toBe(false);
});
it("rejects a port outside the dedicated allowlist", async () => {
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath);
const res = await core.handle(reserveReq(RUNTIME_A, 8080), PEER);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe("port_not_allowlisted");
});
it("names which listener predicate failed, and every one of them still denies (SSRF guard)", async () => {
// One code per predicate, so a denial is attributable from the client reply
// alone: `safeMessage()` returns the bare code and the discriminating reason
// reaches only the root-owned audit file, which the calling account cannot
// read. Distinguishing them relaxes nothing — all three deny below.
for (const { ownership, code } of [
{ ownership: { present: false, loopbackOnly: true, ownerUidMatches: true, inodes: [] }, code: "listener_absent" },
{ ownership: { present: true, loopbackOnly: false, ownerUidMatches: true, inodes: ["5001"] }, code: "listener_not_loopback" },
{ ownership: { present: true, loopbackOnly: true, ownerUidMatches: false, inodes: ["5001"] }, code: "listener_ownership_mismatch" },
]) {
const fake = new FakeTailscale();
const { core, audit } = makeCore(fake, registryPath, () => ownership);
const res = await reserveAndExpose(core);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe(code);
// Fail-closed: the denial precedes every Serve read and mutation, so the
// requested port is never published and the primary route is untouched.
expect(fake.exposeCalls).toBe(0);
expect(fake.removeCalls).toBe(0);
expect([...fake.ports]).toEqual([[443, "http://127.0.0.1:3100"]]);
// The discriminating detail must still reach the audit trail.
const denial = audit.events.find((event) => event.decision === "deny");
expect(denial?.reasonCode).toBe(code);
expect(denial?.op).toBe("expose");
}
});
it("denies a listener that is present and correctly owned but cannot be named", async () => {
// Present-but-unattributable is not permission. Without a socket identity
// the broker cannot prove the socket it publishes is the socket it verified,
// so it must refuse rather than fall through to "the predicates passed".
const fake = new FakeTailscale();
const { core, audit } = makeCore(fake, registryPath, () => ({
present: true,
loopbackOnly: true,
ownerUidMatches: true,
inodes: [],
}));
const res = await reserveAndExpose(core);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe("listener_unattributable");
expect(fake.exposeCalls).toBe(0);
expect([...fake.ports]).toEqual([[443, "http://127.0.0.1:3100"]]);
expect(audit.events.find((event) => event.decision === "deny")?.reasonCode).toBe(
"listener_unattributable",
);
});
it("denies before publishing when the listener is substituted during the Serve status read", async () => {
// The window Greptile found: pre-flight verifies the listener, then
// `readServe()` runs a `tailscale` subprocess. A process under the same
// managed-runtime UID can close the verified socket and take the port during
// that subprocess. The three boolean predicates cannot see it — the new
// process satisfies all of them — so only the socket identity changes.
// Publishing then maps tailnet HTTPS at a service that was never authorized.
const fake = new FakeTailscale();
// Armed relative to the start of expose, because reserve already reads Serve
// status once. The swap therefore lands on expose's own status subprocess —
// after its pre-flight verification, before it publishes anything.
let statusReadsBeforeExpose = 0;
const { core, audit } = makeCore(fake, registryPath, () => ({
present: true,
loopbackOnly: true,
ownerUidMatches: true,
inodes: [fake.statusCalls > statusReadsBeforeExpose ? "9999" : "5001"],
}));
const reserved = await core.handle(reserveReq(), PEER);
if (!reserved.ok || reserved.op !== "reserve") throw new Error("reserve failed");
statusReadsBeforeExpose = fake.statusCalls;
const res = await core.handle(
{ op: "expose", requestId: "req-sub", runtimeId: RUNTIME_A, handle: reserved.handle },
PEER,
);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe("listener_substituted");
// Fail closed *before* the mutation: the substituted service is never
// published, so only the pre-existing primary route remains.
expect(fake.exposeCalls).toBe(0);
expect([...fake.ports]).toEqual([[443, "http://127.0.0.1:3100"]]);
expect(audit.events.find((event) => event.decision === "deny")?.reasonCode).toBe(
"listener_substituted",
);
});
it("withdraws the mapping when substitution happens after the port is published", async () => {
// If the swap lands after the mutation instead, the post-publication re-proof
// denies inside the try, so compensation removes what was applied rather
// than leaving a substituted service reachable over HTTPS.
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath, () => ({
present: true,
loopbackOnly: true,
ownerUidMatches: true,
inodes: [fake.exposeCalls >= 1 ? "9999" : "5001"],
}));
const res = await reserveAndExpose(core);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe("listener_substituted");
// The port was published, then withdrawn by compensation.
expect(fake.exposeCalls).toBeGreaterThan(0);
expect(fake.removeCalls).toBeGreaterThan(0);
expect([...fake.ports]).toEqual([[443, "http://127.0.0.1:3100"]]);
});
it("withdraws an idempotently-skipped mapping whose listener was substituted", async () => {
// The gap a skipped mapping opens: on a re-expose the port already carries
// our exact same-number entry, so the apply loop skips it and it never
// enters `appliedPorts`. Denying alone would leave that pre-existing mapping
// active and now pointing at the replacement service, so a rejected request
// would still publish something unauthorized.
const fake = new FakeTailscale();
// Swap armed relative to the start of the *second* expose, so it lands after
// that request's pre-flight verification — the same window as before, but on
// a port the apply loop will skip instead of publish.
let statusReadsBeforeReExpose = Number.POSITIVE_INFINITY;
const { core } = makeCore(fake, registryPath, () => ({
present: true,
loopbackOnly: true,
ownerUidMatches: true,
inodes: [fake.statusCalls > statusReadsBeforeReExpose ? "9999" : "5001"],
}));
// First expose publishes the port for real.
const first = await reserveAndExpose(core);
expect(first.ok).toBe(true);
expect(fake.ports.get(42010)).toBe("http://127.0.0.1:42010");
const exposeCallsAfterFirst = fake.exposeCalls;
// Re-expose the same port. The mapping already matches, so the apply loop
// takes the idempotent path and publishes nothing.
const reserved = await core.handle(reserveReq(), PEER);
if (!reserved.ok || reserved.op !== "reserve") throw new Error("reserve failed");
statusReadsBeforeReExpose = fake.statusCalls;
const second = await core.handle(
{ op: "expose", requestId: "req-again", runtimeId: RUNTIME_A, handle: reserved.handle },
PEER,
);
expect(second.ok).toBe(false);
if (!second.ok) expect(second.code).toBe("listener_substituted");
expect(fake.exposeCalls).toBe(exposeCallsAfterFirst); // nothing re-published
// The substituted mapping must not survive the rejected request.
expect(fake.removeCalls).toBeGreaterThan(0);
expect(fake.ports.has(42010)).toBe(false);
expect([...fake.ports]).toEqual([[443, "http://127.0.0.1:3100"]]);
});
it("still withdraws a substituted idempotent mapping when a reserved lease published nothing", async () => {
// A `reserved` lease has published nothing, so a substitution detected on it
// must deny without withdrawing anything: there is no mapping of ours to
// remove, and removing by port number would delete someone else's.
const fake = new FakeTailscale();
let statusReadsBeforeExpose = Number.POSITIVE_INFINITY;
const { core } = makeCore(fake, registryPath, () => ({
present: true,
loopbackOnly: true,
ownerUidMatches: true,
inodes: [fake.statusCalls > statusReadsBeforeExpose ? "9999" : "5001"],
}));
const reserved = await core.handle(reserveReq(), PEER);
if (!reserved.ok || reserved.op !== "reserve") throw new Error("reserve failed");
statusReadsBeforeExpose = fake.statusCalls;
const res = await core.handle(
{ op: "expose", requestId: "req-res", runtimeId: RUNTIME_A, handle: reserved.handle },
PEER,
);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe("listener_substituted");
// Nothing of ours was published, so nothing is published now.
expect([...fake.ports]).toEqual([[443, "http://127.0.0.1:3100"]]);
});
it("withdraws a substituted idempotent mapping without depending on a readable Serve status", async () => {
// The withdrawal decision must not hinge on a live status read. If it did, a
// status read that fails at exactly the wrong moment would make the broker
// return `listener_substituted` while leaving the replacement service
// exposed. Provenance comes from the `exposed` lease instead, so the remove
// is still issued.
const fake = new FakeTailscale();
let statusReadsBeforeReExpose = Number.POSITIVE_INFINITY;
const { core } = makeCore(fake, registryPath, () => ({
present: true,
loopbackOnly: true,
ownerUidMatches: true,
inodes: [fake.statusCalls > statusReadsBeforeReExpose ? "9999" : "5001"],
}));
const first = await reserveAndExpose(core);
expect(first.ok).toBe(true);
const removeCallsAfterFirst = fake.removeCalls;
const reserved = await core.handle(reserveReq(), PEER);
if (!reserved.ok || reserved.op !== "reserve") throw new Error("reserve failed");
statusReadsBeforeReExpose = fake.statusCalls;
const second = await core.handle(
{ op: "expose", requestId: "req-noread", runtimeId: RUNTIME_A, handle: reserved.handle },
PEER,
);
expect(second.ok).toBe(false);
if (!second.ok) expect(second.code).toBe("listener_substituted");
// The remove was issued for the substituted port even though this request
// published nothing itself.
expect(fake.removeCalls).toBeGreaterThan(removeCallsAfterFirst);
expect(fake.ports.has(42010)).toBe(false);
});
it("withdraws a published mapping when the listener disappears entirely", async () => {
// Absent is not "unchanged". A closed listener leaves the mapping pointing at
// nothing, which a later process on that port would inherit, so it must be
// withdrawn like any other substitution.
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath, () => ({
present: fake.exposeCalls === 0,
loopbackOnly: true,
ownerUidMatches: true,
inodes: fake.exposeCalls === 0 ? ["5001"] : [],
}));
const res = await reserveAndExpose(core);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe("listener_substituted");
expect(fake.removeCalls).toBeGreaterThan(0);
expect([...fake.ports]).toEqual([[443, "http://127.0.0.1:3100"]]);
});
it("denies an absent listener without consuming the reservation or touching Serve", async () => {
// A denial is pure: it neither publishes a mapping nor burns the lease. No
// production caller retries an exposure today (deliberately — a retry needs
// its own reproduced requirement and wiring); this pins the broker-side
// state purity that any such caller, or a plain operator re-run, relies on.
const fake = new FakeTailscale();
let bound = false;
const { core } = makeCore(fake, registryPath, () => ({
present: bound,
loopbackOnly: true,
ownerUidMatches: true,
inodes: bound ? ["5001"] : [],
}));
const reserved = await core.handle(reserveReq(), PEER);
if (!reserved.ok || reserved.op !== "reserve") throw new Error("reserve failed");
const denied = await core.handle(
{ op: "expose", requestId: "req-x1", runtimeId: RUNTIME_A, handle: reserved.handle },
PEER,
);
expect(denied.ok).toBe(false);
if (!denied.ok) expect(denied.code).toBe("listener_absent");
expect(fake.exposeCalls).toBe(0);
expect(fake.ports.has(42010)).toBe(false);
// Once the child is actually bound, the very same reservation still exposes.
bound = true;
const exposed = await core.handle(
{ op: "expose", requestId: "req-x2", runtimeId: RUNTIME_A, handle: reserved.handle },
PEER,
);
expect(exposed.ok).toBe(true);
expect(fake.ports.get(42010)).toBe("http://127.0.0.1:42010");
});
it("never touches a pre-existing manual mapping on the target port", async () => {
const fake = new FakeTailscale();
fake.ports.set(42010, "http://127.0.0.1:5432"); // manual/unrelated service
const { core } = makeCore(fake, registryPath);
const res = await core.handle(reserveReq(), PEER);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe("manual_mapping_present");
expect(fake.ports.get(42010)).toBe("http://127.0.0.1:5432"); // unchanged
});
it("fails closed and preserves :443 if a mutation retargets the primary route", async () => {
const fake = new FakeTailscale();
fake.retargetPrimaryOnExpose = true;
const { core } = makeCore(fake, registryPath);
const res = await reserveAndExpose(core);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe("primary_route_violation");
});
it("compensates and quarantines on an unexpected serve diff", async () => {
const fake = new FakeTailscale();
fake.sideEffectOnExpose = 42011; // an unexpected extra entry appears
fake.failRemove = true; // compensation cannot be proven -> quarantine
const { core } = makeCore(fake, registryPath);
const res = await reserveAndExpose(core);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe("unexpected_serve_diff");
const registry = JSON.parse(readFileSync(registryPath, "utf8"));
expect(registry.quarantinedPorts).toContain(42010);
// A later expose of the quarantined port is refused.
const fake2 = new FakeTailscale();
const { core: core2 } = makeCore(fake2, registryPath);
const res2 = await core2.handle(reserveReq(), PEER);
expect(res2.ok).toBe(false);
if (!res2.ok) expect(res2.code).toBe("quarantined");
});
});
describe("remove", () => {
async function exposeAndGetHandle(core: BrokerCore, port = 42010, runtime = RUNTIME_A) {
const res = await reserveAndExpose(core, runtime, port);
if (!res.ok || res.op !== "expose") throw new Error("expose failed");
return res.handle;
}
it("removes only the owned listener with an exact lease match", async () => {
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath);
const handle = await exposeAndGetHandle(core);
const res = await core.handle(
{ op: "remove", requestId: "req-r", runtimeId: RUNTIME_A, handle },
PEER,
);
expect(res.ok).toBe(true);
if (res.ok && res.op === "remove") expect(res.removedPorts).toEqual([42010]);
expect(fake.ports.has(42010)).toBe(false);
expect(fake.ports.get(443)).toBe("http://127.0.0.1:3100"); // primary intact
});
it("rejects a random handle and a cross-runtime removal", async () => {
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath);
const handle = await exposeAndGetHandle(core);
const bad = await core.handle(
{ op: "remove", requestId: "r", runtimeId: RUNTIME_A, handle: "random-handle-not-real-xxxx" },
PEER,
);
expect(bad.ok).toBe(false);
if (!bad.ok) expect(bad.code).toBe("invalid_handle");
const cross = await core.handle(
{ op: "remove", requestId: "r", runtimeId: RUNTIME_B, handle },
PEER,
);
expect(cross.ok).toBe(false);
if (!cross.ok) expect(cross.code).toBe("listener_ownership_mismatch");
expect(fake.ports.has(42010)).toBe(true); // still there
});
it("releases an unexposed reservation without mutating Serve", async () => {
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath);
const reserved = await core.handle(reserveReq(), PEER);
if (!reserved.ok || reserved.op !== "reserve") throw new Error("reserve failed");
const res = await core.handle(
{ op: "remove", requestId: "req-r", runtimeId: RUNTIME_A, handle: reserved.handle },
PEER,
);
expect(res).toMatchObject({ ok: true, removedPorts: [] });
expect(fake.exposeCalls).toBe(0);
expect(fake.removeCalls).toBe(0);
});
});
describe("list", () => {
it("returns caller-owned ports and never lease handles", async () => {
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath);
await reserveAndExpose(core);
const res = await core.handle({ op: "list", requestId: "req-l" }, PEER);
expect(res.ok).toBe(true);
if (res.ok && res.op === "list") {
expect(res.listeners).toEqual([{ runtimeId: RUNTIME_A, port: 42010, purpose: "app" }]);
expect(JSON.stringify(res)).not.toMatch(/handle/);
}
});
});
describe("node identity", () => {
it("quarantines when the persisted node identity no longer matches", async () => {
const fake = new FakeTailscale();
const { core } = makeCore(fake, registryPath);
await reserveAndExpose(core);
// Rebuild a core with a different node identity against the same registry.
const audit = new MemoryAuditSink();
const core2 = new BrokerCore({
tailscaleBinPath: BIN,
registryPath,
auditSink: audit,
peerPolicy: { allowedUids: new Set([999]), allowedGids: new Set([987]) },
nodeIdentity: "node-CHANGED",
isAllowedPort: defaultIsAllowedPort,
deps: {
runTailscale: fake.run,
verifyListenerOwnership: () => ({ present: true, loopbackOnly: true, ownerUidMatches: true, inodes: ["5001"] }),
nowIso: () => "2026-08-11T00:00:00.000Z",
},
});
const res = await core2.handle(reserveReq(RUNTIME_B, 42011), PEER);
expect(res.ok).toBe(false);
if (!res.ok) expect(res.code).toBe("quarantined");
});
});