Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Check-phase port allocation repair

CI34011124632 passed the corrected task-input cases and Linux shards, but two
unchanged macOS management-auth tests failed at the public Bun.serve bind with
EADDRINUSE. Both tests used findAvailablePort, whose Node probe closes its socket
before returning a number. The probes bind 127.0.0.1 while remoteConfig makes the public listener bind 0.0.0.0, so a loopback-only availability check also has the wrong address scope. reservedPort prevents the two selected numbers from
being equal; it does not keep either port reserved until Bun binds. The identity
of the intervening occupier is not established by the CI log.

This is a prerequisite repair to the failing verification instrument, not a
change to authentication or production port policy. Modify only
tests/server/server-management-auth.test.ts: replace those two probe-close
setups with a small test helper that wraps Bun.serve synchronously, changes only
port to zero, calls the real Bun.serve and captures the real public/management
listeners while preserving each original hostname and fetch handler. Restore the spy before requests or any awaited cleanup. Derive the
management URL from its actual listener port and assert distinct live listeners.
Keep a valid positive configured ingress port so production config validation
remains unchanged; the fixture explicitly owns ephemeral bind allocation.

The helper joins captured-listener cleanup if startup/fixture validation fails;
the existing finally blocks continue using the real composite server.stop.
Retain every trust, origin, credential, health, consent and pairing assertion.
No retry, sleep, skip, wider auth rule, or production test seam is added.

Verification: independent fixture review followed by fresh exact-head hosted
CI. The same two real HTTP tests must pass, along with the new task-input cases
and full Linux/macOS checks. No local test suite is run.
44 changes: 35 additions & 9 deletions tests/server/server-management-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { getConfigPath, saveConfig } from "../../src/config";
import { startServer } from "../../src/server";
import { findAvailablePort } from "../../src/server/ports";
import type { OcxConfig } from "../../src/types";
import { serveGuiFile, serveSessionBootstrap } from "../../src/server/gui-static";
import { isProxyAdmissionSecret } from "../../src/server/auth-cors";
Expand Down Expand Up @@ -116,6 +115,37 @@ function hubConfig(publicOrigin = "https://hub.example.test"): OcxConfig {
};
}

/** Keep real ingress/handlers while the kernel allocates both ports at the actual bind. */
async function startEphemeralHubServer(deps: Parameters<typeof startServer>[1]) {
const nativeServe = Bun.serve.bind(Bun);
const listeners: Array<ReturnType<typeof Bun.serve>> = [];
const hostnames: unknown[] = [];
const serveSpy = spyOn(Bun, "serve").mockImplementation((options) => {
const listener = nativeServe({ ...options, port: 0 } as Parameters<typeof Bun.serve>[0]);
listeners.push(listener);
hostnames.push("hostname" in options ? options.hostname : undefined);
return listener;
});
try {
let server: ReturnType<typeof startServer>;
try {
server = startServer(0, deps);
} finally {
// startServer is synchronous; restore before requests or any awaited cleanup.
serveSpy.mockRestore();
}
expect(listeners).toHaveLength(2);
expect(listeners[0]).toBe(server);
expect(hostnames).toEqual(["0.0.0.0", "127.0.0.1"]);
const managementPort = listeners[1]?.port;
if (!managementPort || managementPort === server.port) throw new Error("expected distinct live ingress ports");
return { server, managementPort };
} catch (error) {
await Promise.allSettled(listeners.map(async listener => { await listener.stop(true); }));
throw error;
}
}

function websocketHandshakeOpens(url: URL, token: string): Promise<boolean> {
return new Promise(resolve => {
const target = new URL("/v1/responses", url);
Expand Down Expand Up @@ -978,17 +1008,15 @@ describe("management and data-plane credential separation", () => {
});

test("the live listener trusts Tailscale identity only on hub management ingress", async () => {
const managementPort = await findAvailablePort(0, "127.0.0.1");
const publicPort = await findAvailablePort(0, "127.0.0.1", { reservedPort: managementPort });
const config = hubConfig();
config.hub = {
...config.hub,
managementIngress: { enabled: true, port: managementPort },
managementIngress: { enabled: true, port: 10101 },
};
saveConfig(config);
const state = initializeManagementAuthState(config);
if (!state.available) throw new Error("expected management auth state");
const server = startServer(publicPort, { managementAuthState: state });
const { server, managementPort } = await startEphemeralHubServer({ managementAuthState: state });
const headers = { Host: "hub.example.test", "Tailscale-User-Login": "alice@example.test" };
try {
const spoofedPublic = await fetch(new URL("/opencodex-session", server.url), { headers });
Expand Down Expand Up @@ -1164,15 +1192,13 @@ describe("management and data-plane credential separation", () => {
});

test("the management ingress preserves the one-use pairing exchange contract", async () => {
const managementPort = await findAvailablePort(0, "127.0.0.1");
const publicPort = await findAvailablePort(0, "127.0.0.1", { reservedPort: managementPort });
const config = hubConfig();
config.hub = { ...config.hub, managementIngress: { enabled: true, port: managementPort } };
config.hub = { ...config.hub, managementIngress: { enabled: true, port: 10101 } };
saveConfig(config);
const state = initializeManagementAuthState(config);
if (!state.available) throw new Error("expected management auth state");
const created = createGuiPairingGrant("https://dashboard.example.test", config, state);
const server = startServer(publicPort, { managementAuthState: state });
const { server, managementPort } = await startEphemeralHubServer({ managementAuthState: state });
const url = `http://127.0.0.1:${managementPort}/opencodex-session`;
const headers = {
Host: "hub.example.test",
Expand Down
Loading