mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 20:20:43 +00:00
feat(security): support operator-managed network proxy routing (#70044)
* feat: support operator-managed proxy routing * docs: add network proxy changelog entry * fix(proxy): restrict gateway bypass to loopback IPs * fix(cli): harden container proxy URL checks * docs(proxy): clarify gateway bypass scope * docs: remove proxy changelog entry * fix(proxy): clear startup CI guard failures * fix(proxy): harden gateway proxy policy parsing * fix(proxy): honor update shorthand proxy policy * fix(cli): redact proxy URL suffixes * test(proxy): keep gateway help off proxy startup * fix(proxy): keep overlapping lifecycle active * docs: add proxy changelog entry --------- Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveCliCommandPathPolicy } from "./command-path-policy.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CliCommandCatalogEntry } from "./command-catalog.js";
|
||||
import {
|
||||
resolveCliCatalogCommandPath,
|
||||
resolveCliCommandPathPolicy,
|
||||
resolveCliNetworkProxyPolicy,
|
||||
} from "./command-path-policy.js";
|
||||
|
||||
describe("command-path-policy", () => {
|
||||
afterEach(() => {
|
||||
vi.doUnmock("./command-catalog.js");
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("resolves status policy with shared startup semantics", () => {
|
||||
expect(resolveCliCommandPathPolicy(["status"])).toEqual({
|
||||
bypassConfigGuard: false,
|
||||
@@ -9,6 +19,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: false,
|
||||
networkProxy: "bypass",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +30,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "always",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "default",
|
||||
});
|
||||
expect(resolveCliCommandPathPolicy(["channels", "add"])).toEqual({
|
||||
bypassConfigGuard: false,
|
||||
@@ -26,6 +38,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "bypass",
|
||||
});
|
||||
expect(resolveCliCommandPathPolicy(["channels", "status"])).toEqual({
|
||||
bypassConfigGuard: false,
|
||||
@@ -33,6 +46,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: expect.any(Function),
|
||||
});
|
||||
expect(resolveCliCommandPathPolicy(["channels", "list"])).toEqual({
|
||||
bypassConfigGuard: false,
|
||||
@@ -40,6 +54,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "bypass",
|
||||
});
|
||||
expect(resolveCliCommandPathPolicy(["channels", "logs"])).toEqual({
|
||||
bypassConfigGuard: false,
|
||||
@@ -47,6 +62,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "bypass",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,6 +80,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "bypass",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -75,6 +92,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "default",
|
||||
});
|
||||
expect(resolveCliCommandPathPolicy(["config", "validate"])).toEqual({
|
||||
bypassConfigGuard: true,
|
||||
@@ -82,6 +100,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "bypass",
|
||||
});
|
||||
expect(resolveCliCommandPathPolicy(["gateway", "status"])).toEqual({
|
||||
bypassConfigGuard: false,
|
||||
@@ -89,6 +108,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "bypass",
|
||||
});
|
||||
expect(resolveCliCommandPathPolicy(["plugins", "update"])).toEqual({
|
||||
bypassConfigGuard: false,
|
||||
@@ -96,6 +116,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: true,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "default",
|
||||
});
|
||||
for (const commandPath of [
|
||||
["plugins", "install"],
|
||||
@@ -110,6 +131,7 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "default",
|
||||
});
|
||||
}
|
||||
expect(resolveCliCommandPathPolicy(["cron", "list"])).toEqual({
|
||||
@@ -118,6 +140,116 @@ describe("command-path-policy", () => {
|
||||
loadPlugins: "never",
|
||||
hideBanner: false,
|
||||
ensureCliPath: true,
|
||||
networkProxy: "bypass",
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults unknown command paths to network proxy routing", () => {
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "googlemeet", "login"])).toBe(
|
||||
"default",
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves static network proxy bypass policies from the catalog", () => {
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "status"])).toBe("bypass");
|
||||
expect(
|
||||
resolveCliNetworkProxyPolicy(["node", "openclaw", "config", "get", "proxy.enabled"]),
|
||||
).toBe("bypass");
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "proxy", "start"])).toBe("bypass");
|
||||
});
|
||||
|
||||
it("resolves mixed network proxy policies from argv-sensitive catalog entries", () => {
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "gateway"])).toBe("default");
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "gateway", "run"])).toBe("default");
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "gateway", "health"])).toBe("bypass");
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "node", "run"])).toBe("default");
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "node", "status"])).toBe("bypass");
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "agent", "--local"])).toBe("default");
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "agent", "run"])).toBe("bypass");
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "channels", "status"])).toBe("bypass");
|
||||
expect(
|
||||
resolveCliNetworkProxyPolicy(["node", "openclaw", "channels", "status", "--probe"]),
|
||||
).toBe("default");
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "models", "status"])).toBe("bypass");
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "models", "status", "--probe"])).toBe(
|
||||
"default",
|
||||
);
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "skills", "info", "browser"])).toBe(
|
||||
"bypass",
|
||||
);
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "skills", "search", "browser"])).toBe(
|
||||
"default",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the longest catalog command path for deep network proxy overrides", async () => {
|
||||
const catalog: readonly CliCommandCatalogEntry[] = [
|
||||
{ commandPath: ["nodes"], policy: { networkProxy: "bypass" } },
|
||||
{
|
||||
commandPath: ["nodes", "camera", "snap"],
|
||||
exact: true,
|
||||
policy: { networkProxy: "default" },
|
||||
},
|
||||
];
|
||||
|
||||
vi.resetModules();
|
||||
vi.doMock("./command-catalog.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./command-catalog.js")>();
|
||||
return { ...actual, cliCommandCatalog: catalog };
|
||||
});
|
||||
const { resolveCliCatalogCommandPath, resolveCliNetworkProxyPolicy } =
|
||||
await import("./command-path-policy.js");
|
||||
|
||||
expect(resolveCliCatalogCommandPath(["node", "openclaw", "nodes", "camera", "snap"])).toEqual([
|
||||
"nodes",
|
||||
"camera",
|
||||
"snap",
|
||||
]);
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "nodes", "camera", "snap"])).toBe(
|
||||
"default",
|
||||
);
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "nodes", "camera", "list"])).toBe(
|
||||
"bypass",
|
||||
);
|
||||
});
|
||||
|
||||
it("stops catalog command path resolution before positional arguments", () => {
|
||||
expect(
|
||||
resolveCliCatalogCommandPath(["node", "openclaw", "config", "get", "proxy.enabled"]),
|
||||
).toEqual(["config", "get"]);
|
||||
expect(
|
||||
resolveCliCatalogCommandPath(["node", "openclaw", "message", "send", "--to", "demo"]),
|
||||
).toEqual(["message"]);
|
||||
});
|
||||
|
||||
it("treats bare gateway invocations with options as the gateway runtime", () => {
|
||||
const argv = ["node", "openclaw", "gateway", "--port", "1234"];
|
||||
|
||||
expect(resolveCliCatalogCommandPath(argv)).toEqual(["gateway"]);
|
||||
expect(resolveCliNetworkProxyPolicy(argv)).toBe("default");
|
||||
});
|
||||
|
||||
it("does not let gateway run option values spoof bypass subcommands", () => {
|
||||
for (const argv of [
|
||||
["node", "openclaw", "gateway", "--token", "status"],
|
||||
["node", "openclaw", "gateway", "--token=status"],
|
||||
["node", "openclaw", "gateway", "--password", "health"],
|
||||
["node", "openclaw", "gateway", "--password-file", "status"],
|
||||
["node", "openclaw", "gateway", "--ws-log", "compact"],
|
||||
]) {
|
||||
expect(resolveCliCatalogCommandPath(argv), argv.join(" ")).toEqual(["gateway"]);
|
||||
expect(resolveCliNetworkProxyPolicy(argv), argv.join(" ")).toBe("default");
|
||||
}
|
||||
});
|
||||
|
||||
it("still resolves real gateway bypass subcommands after their command token", () => {
|
||||
expect(resolveCliCatalogCommandPath(["node", "openclaw", "gateway", "status"])).toEqual([
|
||||
"gateway",
|
||||
"status",
|
||||
]);
|
||||
expect(
|
||||
resolveCliCatalogCommandPath(["node", "openclaw", "gateway", "status", "--token", "secret"]),
|
||||
).toEqual(["gateway", "status"]);
|
||||
expect(resolveCliNetworkProxyPolicy(["node", "openclaw", "gateway", "status"])).toBe("bypass");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user