mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
123 lines
3.9 KiB
TypeScript
123 lines
3.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
DEFAULT_PRIVATE_ALLOWLIST,
|
|
evaluateEgress,
|
|
isPrivateDestination,
|
|
parseAllowlist,
|
|
} from "../../src/backend/egress.js";
|
|
|
|
/**
|
|
* The rule that lets a self-hosted Ollama work without turning the backend
|
|
* into an authenticated probe of its own network: private destinations are
|
|
* refused unless an admin named the host.
|
|
*/
|
|
describe("isPrivateDestination", () => {
|
|
it("recognises loopback and private ranges", () => {
|
|
for (const url of [
|
|
"http://localhost:11434",
|
|
"http://127.0.0.1:11434",
|
|
"http://10.0.0.5:11434",
|
|
"http://192.168.1.10:11434",
|
|
"http://172.16.4.4:11434",
|
|
"http://169.254.169.254/latest/meta-data",
|
|
"http://[::1]:11434",
|
|
]) {
|
|
expect(isPrivateDestination(url), url).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("treats public hosts as public", () => {
|
|
for (const url of [
|
|
"https://api.openai.com/v1",
|
|
"https://api.anthropic.com",
|
|
"https://generativelanguage.googleapis.com",
|
|
"http://8.8.8.8",
|
|
]) {
|
|
expect(isPrivateDestination(url), url).toBe(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("evaluateEgress", () => {
|
|
it("allows public destinations without an allowlist entry", () => {
|
|
const decision = evaluateEgress("https://api.openai.com/v1", []);
|
|
expect(decision.allowed).toBe(true);
|
|
expect(decision.isPrivate).toBe(false);
|
|
});
|
|
|
|
it("refuses a private destination that is not allowlisted", () => {
|
|
const decision = evaluateEgress("http://192.168.1.50:11434", ["localhost"]);
|
|
expect(decision.allowed).toBe(false);
|
|
expect(decision.isPrivate).toBe(true);
|
|
expect(decision.reason).toContain("allowlist");
|
|
});
|
|
|
|
it("allows a private destination once its host is allowlisted", () => {
|
|
const decision = evaluateEgress("http://localhost:11434", [
|
|
"localhost",
|
|
"127.0.0.1",
|
|
]);
|
|
expect(decision.allowed).toBe(true);
|
|
expect(decision.isPrivate).toBe(true);
|
|
});
|
|
|
|
it.each(["llm.internal", "host.docker.internal"])(
|
|
"routes allowlisted private DNS name %s through the private path",
|
|
(host) => {
|
|
expect(evaluateEgress(`http://${host}:8000/v1`, [host])).toEqual({
|
|
allowed: true,
|
|
isPrivate: true,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("matches the allowlist case-insensitively", () => {
|
|
expect(
|
|
evaluateEgress("http://LOCALHOST:11434", ["localhost"]).allowed,
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not let one allowlisted host authorise another", () => {
|
|
// A cloud metadata endpoint is the classic target, so an allowlist for
|
|
// localhost must not open 169.254.169.254.
|
|
const decision = evaluateEgress("http://169.254.169.254/latest/meta-data", [
|
|
"localhost",
|
|
"127.0.0.1",
|
|
]);
|
|
expect(decision.allowed).toBe(false);
|
|
});
|
|
|
|
it("refuses non-http protocols and embedded credentials", () => {
|
|
expect(evaluateEgress("file:///etc/passwd", []).allowed).toBe(false);
|
|
expect(evaluateEgress("ftp://example.com", []).allowed).toBe(false);
|
|
expect(evaluateEgress("https://user:pass@api.openai.com", []).allowed).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("refuses a malformed url", () => {
|
|
expect(evaluateEgress("not a url", []).allowed).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("parseAllowlist", () => {
|
|
it("falls back to the defaults when the setting is not text", () => {
|
|
expect(parseAllowlist(null)).toEqual(DEFAULT_PRIVATE_ALLOWLIST);
|
|
expect(parseAllowlist(undefined)).toEqual(DEFAULT_PRIVATE_ALLOWLIST);
|
|
});
|
|
|
|
it("reads one host per line, normalised, skipping anything that is not a host", () => {
|
|
expect(
|
|
parseAllowlist(
|
|
[" LocalHost ", "", "10.0.0.5", "http://x/y", "10.0.0.5"].join("\n"),
|
|
),
|
|
).toEqual(["localhost", "10.0.0.5"]);
|
|
});
|
|
|
|
it("honours a deliberately empty allowlist", () => {
|
|
// An admin clearing the list must actually block everything private,
|
|
// not silently get the defaults back.
|
|
expect(parseAllowlist("")).toEqual([]);
|
|
});
|
|
});
|