mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-13 17:07:40 +00:00
test(gateway): cover loopback and LAN access (#118824)
This commit is contained in:
33
qa/scenarios/runtime/gateway-loopback-lan-access.yaml
Normal file
33
qa/scenarios/runtime/gateway-loopback-lan-access.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
title: Gateway loopback and LAN access
|
||||
|
||||
scenario:
|
||||
id: gateway-loopback-lan-access
|
||||
surface: runtime
|
||||
coverage:
|
||||
primary:
|
||||
- gateway.loopback-and-lan-access
|
||||
objective: Prove that loopback binding stays host-local while LAN binding is reachable through a real non-loopback interface with Gateway authentication enforced.
|
||||
successCriteria:
|
||||
- A loopback-bound Gateway serves HTTP health and authenticated WebSocket health on 127.0.0.1.
|
||||
- The same loopback listener rejects a connection addressed through the host's non-loopback IPv4 interface.
|
||||
- A LAN-bound Gateway serves HTTP health and authenticated WebSocket health through the host's actual non-loopback IPv4 interface.
|
||||
- Invalid shared tokens are rejected and the valid shared token is accepted on both listener modes.
|
||||
docsRefs:
|
||||
- docs/gateway/index.md
|
||||
- docs/gateway/protocol.md
|
||||
- docs/gateway/security/index.md
|
||||
- docs/concepts/qa-e2e-automation.md
|
||||
codeRefs:
|
||||
- src/gateway/net.ts
|
||||
- src/gateway/server.ts
|
||||
- src/gateway/server-runtime-config.ts
|
||||
- test/e2e/qa-lab/runtime/gateway-loopback-lan-access.ts
|
||||
- test/e2e/qa-lab/runtime/gateway-loopback-lan-access.test.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/gateway-loopback-lan-access.ts
|
||||
summary: Starts real loopback and LAN Gateway listeners, then probes their HTTP and WebSocket surfaces from loopback and the host's real LAN interface.
|
||||
args:
|
||||
- --artifact-base
|
||||
- ${outputDir}
|
||||
timeoutMs: 120000
|
||||
56
test/e2e/qa-lab/runtime/gateway-loopback-lan-access.test.ts
Normal file
56
test/e2e/qa-lab/runtime/gateway-loopback-lan-access.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertGatewayLoopbackLanProof,
|
||||
parseGatewayLoopbackLanOptions,
|
||||
runGatewayLoopbackLanProof,
|
||||
type GatewayLoopbackLanProof,
|
||||
} from "./gateway-loopback-lan-access.js";
|
||||
|
||||
describe("Gateway loopback and LAN access producer", () => {
|
||||
it("parses the evidence artifact directory", () => {
|
||||
expect(
|
||||
parseGatewayLoopbackLanOptions(["--artifact-base", ".artifacts/gateway-network"])
|
||||
.artifactBase,
|
||||
).toContain(".artifacts/gateway-network");
|
||||
expect(() => parseGatewayLoopbackLanOptions([])).toThrow("--artifact-base is required");
|
||||
expect(() => parseGatewayLoopbackLanOptions(["--other", "value"])).toThrow("unknown argument");
|
||||
});
|
||||
|
||||
it("rejects incomplete network proof", () => {
|
||||
const incomplete: GatewayLoopbackLanProof = {
|
||||
loopback: {
|
||||
authenticatedHealthRpc: true,
|
||||
healthStatus: 200,
|
||||
invalidTokenRejected: true,
|
||||
isolatedFromLanInterface: false,
|
||||
},
|
||||
lan: {
|
||||
authenticatedHealthRpc: true,
|
||||
healthStatus: 200,
|
||||
invalidTokenRejected: true,
|
||||
nonLoopbackInterface: true,
|
||||
reachableThroughInterface: true,
|
||||
},
|
||||
};
|
||||
expect(() => assertGatewayLoopbackLanProof(incomplete)).toThrow("loopback isolation from LAN");
|
||||
});
|
||||
|
||||
it("proves real loopback isolation, LAN reachability, and shared-token authentication", async () => {
|
||||
const proof = await runGatewayLoopbackLanProof();
|
||||
expect(proof).toEqual({
|
||||
loopback: {
|
||||
authenticatedHealthRpc: true,
|
||||
healthStatus: 200,
|
||||
invalidTokenRejected: true,
|
||||
isolatedFromLanInterface: true,
|
||||
},
|
||||
lan: {
|
||||
authenticatedHealthRpc: true,
|
||||
healthStatus: 200,
|
||||
invalidTokenRejected: true,
|
||||
nonLoopbackInterface: true,
|
||||
reachableThroughInterface: true,
|
||||
},
|
||||
});
|
||||
}, 120_000);
|
||||
});
|
||||
490
test/e2e/qa-lab/runtime/gateway-loopback-lan-access.ts
Normal file
490
test/e2e/qa-lab/runtime/gateway-loopback-lan-access.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
// Produces QA evidence for real Gateway loopback isolation and LAN exposure.
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import net from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { WebSocket, type RawData } from "ws";
|
||||
import { PROTOCOL_VERSION } from "../../../../packages/gateway-protocol/src/index.js";
|
||||
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../../../../src/config/config.js";
|
||||
import { clearSessionStoreCacheForTest } from "../../../../src/config/sessions/store-writer-state.js";
|
||||
import { pickPrimaryLanIPv4 } from "../../../../src/gateway/net.js";
|
||||
import { startGatewayServer, type GatewayServer } from "../../../../src/gateway/server.js";
|
||||
import { getFreeGatewayPort } from "../../../../src/gateway/test-helpers.e2e.js";
|
||||
import { resetAgentEventsForTest } from "../../../../src/infra/agent-events.js";
|
||||
import { rawDataToString } from "../../../../src/infra/ws.js";
|
||||
import { captureEnv, deleteTestEnvValue, setTestEnvValue } from "../../../../src/test-utils/env.js";
|
||||
import {
|
||||
GATEWAY_CLIENT_MODES,
|
||||
GATEWAY_CLIENT_NAMES,
|
||||
} from "../../../../src/utils/message-channel.js";
|
||||
import { createQaScriptEvidenceWriter } from "./script-evidence.js";
|
||||
|
||||
const SOURCE_PATH = "test/e2e/qa-lab/runtime/gateway-loopback-lan-access.ts";
|
||||
const SCENARIO_ID = "gateway-loopback-lan-access";
|
||||
const PROBE_TIMEOUT_MS = 10_000;
|
||||
const ENV_KEYS = [
|
||||
"HOME",
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"OPENCLAW_CONFIG_PATH",
|
||||
"OPENCLAW_GATEWAY_TOKEN",
|
||||
"OPENCLAW_GATEWAY_PASSWORD",
|
||||
"OPENCLAW_SKIP_CHANNELS",
|
||||
"OPENCLAW_SKIP_GMAIL_WATCHER",
|
||||
"OPENCLAW_SKIP_CRON",
|
||||
"OPENCLAW_SKIP_CANVAS_HOST",
|
||||
"OPENCLAW_SKIP_BROWSER_CONTROL_SERVER",
|
||||
"OPENCLAW_SKIP_PROVIDERS",
|
||||
"OPENCLAW_BUNDLED_PLUGINS_DIR",
|
||||
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
|
||||
"OPENCLAW_TEST_MINIMAL_GATEWAY",
|
||||
] as const;
|
||||
|
||||
type ProducerOptions = {
|
||||
artifactBase: string;
|
||||
repoRoot: string;
|
||||
};
|
||||
|
||||
type GatewayResponseFrame = {
|
||||
error?: { message?: string };
|
||||
event?: string;
|
||||
id?: string;
|
||||
ok?: boolean;
|
||||
payload?: unknown;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type ListenerProof = {
|
||||
authenticatedHealthRpc: boolean;
|
||||
healthStatus: number;
|
||||
invalidTokenRejected: boolean;
|
||||
};
|
||||
|
||||
export type GatewayLoopbackLanProof = {
|
||||
lan: ListenerProof & {
|
||||
nonLoopbackInterface: boolean;
|
||||
reachableThroughInterface: boolean;
|
||||
};
|
||||
loopback: ListenerProof & {
|
||||
isolatedFromLanInterface: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export function parseGatewayLoopbackLanOptions(args: string[]): ProducerOptions {
|
||||
let artifactBase: string | undefined;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const option = args[index];
|
||||
const value = args[index + 1];
|
||||
if (option !== "--artifact-base") {
|
||||
throw new Error(`unknown argument: ${option}`);
|
||||
}
|
||||
if (!value || value.startsWith("--")) {
|
||||
throw new Error("--artifact-base requires a value");
|
||||
}
|
||||
artifactBase = value;
|
||||
index += 1;
|
||||
}
|
||||
if (!artifactBase) {
|
||||
throw new Error("--artifact-base is required");
|
||||
}
|
||||
return { artifactBase: path.resolve(artifactBase), repoRoot: process.cwd() };
|
||||
}
|
||||
|
||||
export function assertGatewayLoopbackLanProof(proof: GatewayLoopbackLanProof): void {
|
||||
const checks = [
|
||||
["loopback HTTP health", proof.loopback.healthStatus === 200],
|
||||
["loopback invalid-token rejection", proof.loopback.invalidTokenRejected],
|
||||
["loopback authenticated health RPC", proof.loopback.authenticatedHealthRpc],
|
||||
["loopback isolation from LAN", proof.loopback.isolatedFromLanInterface],
|
||||
["non-loopback interface selection", proof.lan.nonLoopbackInterface],
|
||||
["LAN interface reachability", proof.lan.reachableThroughInterface],
|
||||
["LAN HTTP health", proof.lan.healthStatus === 200],
|
||||
["LAN invalid-token rejection", proof.lan.invalidTokenRejected],
|
||||
["LAN authenticated health RPC", proof.lan.authenticatedHealthRpc],
|
||||
] as const;
|
||||
const failed = checks.filter(([, passed]) => !passed).map(([label]) => label);
|
||||
if (failed.length > 0) {
|
||||
throw new Error(`Gateway network proof failed: ${failed.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
function resetGatewayTestState(): void {
|
||||
resetAgentEventsForTest({ preserveListeners: true });
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
clearSessionStoreCacheForTest();
|
||||
}
|
||||
|
||||
function waitForOpen(ws: WebSocket): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error("timed out opening Gateway WebSocket")),
|
||||
PROBE_TIMEOUT_MS,
|
||||
);
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer);
|
||||
ws.off("open", onOpen);
|
||||
ws.off("error", onError);
|
||||
};
|
||||
const onOpen = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const onError = (error: Error) => {
|
||||
cleanup();
|
||||
reject(error);
|
||||
};
|
||||
ws.once("open", onOpen);
|
||||
ws.once("error", onError);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForFrame(
|
||||
ws: WebSocket,
|
||||
predicate: (frame: GatewayResponseFrame) => boolean,
|
||||
label: string,
|
||||
): Promise<GatewayResponseFrame> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => finish(new Error(`timed out waiting for ${label}`)),
|
||||
PROBE_TIMEOUT_MS,
|
||||
);
|
||||
const onMessage = (data: RawData) => {
|
||||
try {
|
||||
const frame = JSON.parse(rawDataToString(data)) as GatewayResponseFrame;
|
||||
if (predicate(frame)) {
|
||||
finish(undefined, frame);
|
||||
}
|
||||
} catch {
|
||||
// Ignore unrelated non-JSON frames while waiting for the selected response.
|
||||
}
|
||||
};
|
||||
const onClose = (code: number) => finish(new Error(`Gateway WebSocket closed (${code})`));
|
||||
const onError = (error: Error) => finish(error);
|
||||
const finish = (error?: Error, frame?: GatewayResponseFrame) => {
|
||||
clearTimeout(timer);
|
||||
ws.off("message", onMessage);
|
||||
ws.off("close", onClose);
|
||||
ws.off("error", onError);
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(frame as GatewayResponseFrame);
|
||||
}
|
||||
};
|
||||
ws.on("message", onMessage);
|
||||
ws.once("close", onClose);
|
||||
ws.once("error", onError);
|
||||
});
|
||||
}
|
||||
|
||||
async function openGatewayWebSocket(params: {
|
||||
host: string;
|
||||
localAddress?: string;
|
||||
port: number;
|
||||
}): Promise<WebSocket> {
|
||||
const ws = new WebSocket(`ws://${params.host}:${params.port}`, {
|
||||
localAddress: params.localAddress,
|
||||
});
|
||||
const challenge = waitForFrame(
|
||||
ws,
|
||||
(frame) =>
|
||||
frame.type === "event" && (frame as { event?: string }).event === "connect.challenge",
|
||||
"connect challenge",
|
||||
);
|
||||
await waitForOpen(ws);
|
||||
await challenge;
|
||||
return ws;
|
||||
}
|
||||
|
||||
async function sendRequest(
|
||||
ws: WebSocket,
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
): Promise<GatewayResponseFrame> {
|
||||
const id = randomUUID();
|
||||
const response = waitForFrame(
|
||||
ws,
|
||||
(frame) => frame.type === "res" && frame.id === id,
|
||||
`${method} response`,
|
||||
);
|
||||
const request = { type: "req", id, method, params };
|
||||
ws.send(JSON.stringify(request));
|
||||
return await response;
|
||||
}
|
||||
|
||||
async function probeGatewayAuth(params: {
|
||||
host: string;
|
||||
localAddress?: string;
|
||||
port: number;
|
||||
token: string;
|
||||
}): Promise<{ authenticatedHealthRpc: boolean; invalidTokenRejected: boolean }> {
|
||||
const connectParams = (token: string) => ({
|
||||
minProtocol: PROTOCOL_VERSION,
|
||||
maxProtocol: PROTOCOL_VERSION,
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_NAMES.TEST,
|
||||
displayName: "Gateway network QA",
|
||||
version: "1.0.0",
|
||||
platform: process.platform,
|
||||
mode: GATEWAY_CLIENT_MODES.TEST,
|
||||
},
|
||||
caps: [],
|
||||
auth: { token },
|
||||
role: "operator",
|
||||
scopes: [],
|
||||
});
|
||||
|
||||
const invalidWs = await openGatewayWebSocket(params);
|
||||
let invalidTokenRejected = false;
|
||||
try {
|
||||
const invalid = await sendRequest(invalidWs, "connect", connectParams("invalid-token"));
|
||||
invalidTokenRejected =
|
||||
invalid.ok === false && (invalid.error?.message ?? "").toLowerCase().includes("unauthorized");
|
||||
} finally {
|
||||
invalidWs.terminate();
|
||||
}
|
||||
|
||||
const validWs = await openGatewayWebSocket(params);
|
||||
let authenticatedHealthRpc = false;
|
||||
try {
|
||||
const connected = await sendRequest(validWs, "connect", connectParams(params.token));
|
||||
if (connected.ok !== true) {
|
||||
throw new Error(`valid Gateway token was rejected: ${connected.error?.message ?? "unknown"}`);
|
||||
}
|
||||
const health = await sendRequest(validWs, "health", {});
|
||||
authenticatedHealthRpc = health.ok === true;
|
||||
} finally {
|
||||
validWs.terminate();
|
||||
}
|
||||
return { authenticatedHealthRpc, invalidTokenRejected };
|
||||
}
|
||||
|
||||
async function probeHttpHealth(params: {
|
||||
host: string;
|
||||
localAddress?: string;
|
||||
port: number;
|
||||
}): Promise<number> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const request = http.request(
|
||||
{
|
||||
host: params.host,
|
||||
localAddress: params.localAddress,
|
||||
path: "/healthz",
|
||||
port: params.port,
|
||||
},
|
||||
(response) => {
|
||||
response.resume();
|
||||
response.once("end", () => resolve(response.statusCode ?? 0));
|
||||
},
|
||||
);
|
||||
request.once("error", reject);
|
||||
request.setTimeout(PROBE_TIMEOUT_MS, () => {
|
||||
request.destroy(new Error("timed out waiting for Gateway HTTP health"));
|
||||
});
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function probeTcpUnreachable(params: {
|
||||
host: string;
|
||||
localAddress: string;
|
||||
port: number;
|
||||
}): Promise<boolean> {
|
||||
return await new Promise((resolve) => {
|
||||
const socket = net.connect(params);
|
||||
const timer = setTimeout(() => finish(true), 2_000);
|
||||
const finish = (unreachable: boolean) => {
|
||||
clearTimeout(timer);
|
||||
socket.destroy();
|
||||
resolve(unreachable);
|
||||
};
|
||||
socket.once("connect", () => finish(false));
|
||||
socket.once("error", () => finish(true));
|
||||
});
|
||||
}
|
||||
|
||||
async function startGateway(port: number, bind: "lan" | "loopback", token: string) {
|
||||
return await startGatewayServer(port, {
|
||||
auth: { mode: "token", token },
|
||||
bind,
|
||||
controlUiEnabled: false,
|
||||
sidecarStartup: "defer",
|
||||
});
|
||||
}
|
||||
|
||||
async function stopGateway(server: GatewayServer | undefined): Promise<void> {
|
||||
if (server) {
|
||||
await server.close({ reason: "Gateway network QA complete" });
|
||||
}
|
||||
resetGatewayTestState();
|
||||
}
|
||||
|
||||
export async function runGatewayLoopbackLanProof(): Promise<GatewayLoopbackLanProof> {
|
||||
const lanIp = pickPrimaryLanIPv4();
|
||||
if (!lanIp || net.isIP(lanIp) !== 4 || lanIp.startsWith("127.")) {
|
||||
throw new Error("no usable non-loopback IPv4 interface is available");
|
||||
}
|
||||
|
||||
const env = captureEnv([...ENV_KEYS]);
|
||||
// openclaw-temp-dir: standalone producer removes this state root in finally
|
||||
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-network-"));
|
||||
const stateDir = path.join(tempHome, ".openclaw");
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
const emptyPluginsDir = path.join(tempHome, "empty-bundled-plugins");
|
||||
const token = `gateway-network-${randomUUID()}`;
|
||||
let server: GatewayServer | undefined;
|
||||
|
||||
try {
|
||||
for (const key of ENV_KEYS) {
|
||||
deleteTestEnvValue(key);
|
||||
}
|
||||
setTestEnvValue("HOME", tempHome);
|
||||
setTestEnvValue("OPENCLAW_STATE_DIR", stateDir);
|
||||
setTestEnvValue("OPENCLAW_CONFIG_PATH", configPath);
|
||||
setTestEnvValue("OPENCLAW_GATEWAY_TOKEN", token);
|
||||
setTestEnvValue("OPENCLAW_SKIP_CHANNELS", "1");
|
||||
setTestEnvValue("OPENCLAW_SKIP_GMAIL_WATCHER", "1");
|
||||
setTestEnvValue("OPENCLAW_SKIP_CRON", "1");
|
||||
setTestEnvValue("OPENCLAW_SKIP_CANVAS_HOST", "1");
|
||||
setTestEnvValue("OPENCLAW_SKIP_BROWSER_CONTROL_SERVER", "1");
|
||||
setTestEnvValue("OPENCLAW_SKIP_PROVIDERS", "1");
|
||||
setTestEnvValue("OPENCLAW_BUNDLED_PLUGINS_DIR", emptyPluginsDir);
|
||||
setTestEnvValue("OPENCLAW_DISABLE_BUNDLED_PLUGINS", "1");
|
||||
setTestEnvValue("OPENCLAW_TEST_MINIMAL_GATEWAY", "1");
|
||||
await fs.mkdir(emptyPluginsDir, { recursive: true });
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify({ gateway: { mode: "local", auth: { mode: "token", token } } }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
resetGatewayTestState();
|
||||
|
||||
const loopbackPort = await getFreeGatewayPort();
|
||||
server = await startGateway(loopbackPort, "loopback", token);
|
||||
const loopbackHealthStatus = await probeHttpHealth({
|
||||
host: "127.0.0.1",
|
||||
port: loopbackPort,
|
||||
});
|
||||
const loopbackAuth = await probeGatewayAuth({
|
||||
host: "127.0.0.1",
|
||||
port: loopbackPort,
|
||||
token,
|
||||
});
|
||||
const isolatedFromLanInterface = await probeTcpUnreachable({
|
||||
host: lanIp,
|
||||
localAddress: lanIp,
|
||||
port: loopbackPort,
|
||||
});
|
||||
await stopGateway(server);
|
||||
server = undefined;
|
||||
|
||||
const lanPort = await getFreeGatewayPort();
|
||||
server = await startGateway(lanPort, "lan", token);
|
||||
const lanHealthStatus = await probeHttpHealth({
|
||||
host: lanIp,
|
||||
localAddress: lanIp,
|
||||
port: lanPort,
|
||||
});
|
||||
const lanAuth = await probeGatewayAuth({
|
||||
host: lanIp,
|
||||
localAddress: lanIp,
|
||||
port: lanPort,
|
||||
token,
|
||||
});
|
||||
|
||||
const proof: GatewayLoopbackLanProof = {
|
||||
loopback: {
|
||||
...loopbackAuth,
|
||||
healthStatus: loopbackHealthStatus,
|
||||
isolatedFromLanInterface,
|
||||
},
|
||||
lan: {
|
||||
...lanAuth,
|
||||
healthStatus: lanHealthStatus,
|
||||
nonLoopbackInterface: true,
|
||||
reachableThroughInterface: lanHealthStatus === 200 && lanAuth.authenticatedHealthRpc,
|
||||
},
|
||||
};
|
||||
assertGatewayLoopbackLanProof(proof);
|
||||
return proof;
|
||||
} finally {
|
||||
await stopGateway(server);
|
||||
env.restore();
|
||||
await fs.rm(tempHome, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function runProducer(options: ProducerOptions) {
|
||||
const writer = createQaScriptEvidenceWriter({
|
||||
artifactBase: options.artifactBase,
|
||||
logFileName: "gateway-loopback-lan-access.log",
|
||||
primaryModel: "gateway/network-access",
|
||||
providerMode: "mock-openai",
|
||||
repoRoot: options.repoRoot,
|
||||
target: {
|
||||
codeRefs: [
|
||||
SOURCE_PATH,
|
||||
"src/gateway/net.ts",
|
||||
"src/gateway/server.ts",
|
||||
"src/gateway/server-runtime-config.ts",
|
||||
],
|
||||
docsRefs: [
|
||||
"docs/gateway/index.md",
|
||||
"docs/gateway/protocol.md",
|
||||
"docs/gateway/security/index.md",
|
||||
"docs/concepts/qa-e2e-automation.md",
|
||||
],
|
||||
id: SCENARIO_ID,
|
||||
sourcePath: SOURCE_PATH,
|
||||
title: "Gateway loopback and LAN access",
|
||||
},
|
||||
});
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const proof = await runGatewayLoopbackLanProof();
|
||||
await fs.mkdir(options.artifactBase, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(options.artifactBase, "network-summary.json"),
|
||||
`${JSON.stringify(proof, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writer.appendLog("pass: loopback isolation, LAN reachability, and token auth proven\n");
|
||||
return await writer.write({
|
||||
artifacts: [{ kind: "summary", filePath: "network-summary.json" }],
|
||||
details:
|
||||
"loopback HTTP/WS passed; LAN-address isolation passed; LAN-interface HTTP/WS passed; invalid tokens rejected",
|
||||
durationMs: Math.max(1, Date.now() - startedAt),
|
||||
status: "pass",
|
||||
});
|
||||
} catch (error) {
|
||||
const details = error instanceof Error ? error.message : String(error);
|
||||
writer.appendLog(`fail: ${details}\n`);
|
||||
return await writer.write({
|
||||
details,
|
||||
durationMs: Math.max(1, Date.now() - startedAt),
|
||||
status: "fail",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function main(args: string[]) {
|
||||
const evidence = await runProducer(parseGatewayLoopbackLanOptions(args));
|
||||
const status = evidence.entries[0]?.result.status;
|
||||
console.log(`Gateway loopback and LAN access status: ${status}`);
|
||||
return status === "pass" ? 0 : 1;
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
main(process.argv.slice(2))
|
||||
.then((exitCode) => {
|
||||
process.exitCode = exitCode;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user