fix(sse): classify LAN embeddings providers as no-auth (#6925) (#6962)

Private/LAN embeddings provider_nodes (10.0.0.0/8, 192.168.0.0/16,
100.64.0.0/10 CGNAT) were excluded by a hand-rolled hostname filter that
only matched localhost/127.0.0.1/172.16-31, forcing them through the
apikey/bearer credential fallback and returning 401 for keyless local
providers like a LAN Ollama instance.

Reuse the shared isPrivateHost()/isCloudMetadataHost() classification from
outboundUrlGuard.ts in both the dynamic-provider filter and the
provider_node fallback branch, so any private host resolves to
authType 'none' while cloud-metadata endpoints stay blocked.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-12 10:23:54 -03:00
committed by GitHub
parent 152d77cf4b
commit c1fc661c90
3 changed files with 152 additions and 7 deletions

View File

@@ -0,0 +1 @@
- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925)

View File

@@ -17,6 +17,7 @@ import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import { handleComboChat } from "@omniroute/open-sse/services/combo.ts";
import { resolveBareModelToConnectionDefault } from "@omniroute/open-sse/services/model.ts";
import { findEmbeddingComboDimensionConflict } from "./familyGuard";
import { isPrivateHost, isCloudMetadataHost } from "@/shared/network/outboundUrlGuard";
import { calculateCost } from "@/lib/usage/costCalculator";
import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta";
import { generateRequestId } from "@/shared/utils/requestId";
@@ -24,6 +25,17 @@ import { generateRequestId } from "@/shared/utils/requestId";
type ValidatedEmbeddingBody = Record<string, unknown> & { model: string };
type ProviderCredentialsResult = Awaited<ReturnType<typeof getProviderCredentials>>;
// #6925: a private/LAN host (RFC1918 10/8, 192.168/16, 172.16/12, CGNAT 100.64/10,
// loopback, .local/.internal, ULA/link-local IPv6) is treated as a trusted no-auth
// local embedding provider — mirrors the outbound-URL guard's private-host
// classification instead of the old hand-rolled localhost/127.0.0.1/172.16-31-only
// regex, which excluded common LAN ranges (10.x, 192.168.x) and forced them through
// the apikey/bearer fallback even when no credentials exist. Cloud-metadata hosts
// (169.254.169.254 etc.) are never treated as no-auth local providers.
function isNoAuthLocalEmbeddingHost(hostname: string): boolean {
return isPrivateHost(hostname) && !isCloudMetadataHost(hostname);
}
export interface EmbeddingHandlerOptions {
clientRawRequest?: {
endpoint: string;
@@ -119,11 +131,7 @@ export async function createEmbeddingResponse(
if (!validTypes.includes(n.apiType || "")) return false;
try {
const hostname = new URL(n.baseUrl).hostname;
return (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(hostname)
);
return isNoAuthLocalEmbeddingHost(hostname);
} catch {
return false;
}
@@ -164,11 +172,22 @@ export async function createEmbeddingResponse(
);
if (matchingNode) {
const baseUrl = String(matchingNode.baseUrl).replace(/\/+$/, "");
// #6925: a private/LAN node reaching this fallback (e.g. a matching
// prefix that skipped the dynamicProviders pass above) must never be
// forced through bearer-auth — only a non-private host falls back to
// apikey/bearer credential resolution.
let nodeHostname = "";
try {
nodeHostname = new URL(matchingNode.baseUrl).hostname;
} catch {
nodeHostname = "";
}
const isNoAuthLocal = nodeHostname !== "" && isNoAuthLocalEmbeddingHost(nodeHostname);
providerConfig = {
id: matchingNode.prefix,
baseUrl: `${baseUrl}/embeddings`,
authType: "apikey",
authHeader: "bearer",
authType: isNoAuthLocal ? "none" : "apikey",
authHeader: isNoAuthLocal ? "none" : "bearer",
models: [],
};
credentialsProviderId = matchingNode.id || provider;

View File

@@ -0,0 +1,125 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Isolate the DB to a temp dir BEFORE importing any module that opens it.
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-embed-lan-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const { createProviderNode } = await import("../../src/lib/db/providers/nodes.ts");
const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts");
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// #6925: a keyless LAN OpenAI-compatible embeddings provider (e.g. Ollama at
// 10.x/192.168.x) must be classified as a no-auth local provider — never
// forced through the apikey/bearer fallback (which returns 401 because no
// credentials exist for a provider that was never meant to need any).
test("#6925: 10.x LAN embeddings provider is treated as no-auth (no Authorization header, no 401)", async () => {
await createProviderNode({
type: "openai-compatible-embeddings",
name: "LAN Ollama",
prefix: "lanollama6925",
apiType: "embeddings",
baseUrl: "http://10.10.0.181:11434/v1",
});
const originalFetch = globalThis.fetch;
let captured: { url: string; headers: Record<string, string> } | null = null;
globalThis.fetch = async (url: RequestInfo | URL, options: RequestInit = {}) => {
captured = {
url: String(url),
headers: (options.headers as Record<string, string>) || {},
};
return new Response(
JSON.stringify({
data: [{ object: "embedding", embedding: [0.1, 0.2], index: 0 }],
usage: { prompt_tokens: 3, total_tokens: 3 },
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
};
try {
const res = await createEmbeddingResponse({
model: "lanollama6925/nomic-embed-text",
input: "hello world",
});
assert.equal(res.status, 200, "LAN embeddings request should succeed, not be blocked by auth");
} finally {
globalThis.fetch = originalFetch;
}
assert.ok(captured, "upstream fetch should have been called");
assert.equal(
captured!.url,
"http://10.10.0.181:11434/v1/embeddings",
"should hit the LAN provider's own embeddings endpoint"
);
assert.equal(
captured!.headers.Authorization,
undefined,
"a keyless LAN provider must not receive a fabricated Authorization header"
);
});
test("#6925: 192.168.x LAN embeddings provider is also treated as no-auth", async () => {
await createProviderNode({
type: "openai-compatible-embeddings",
name: "LAN Ollama 192",
prefix: "lanollama6925b",
apiType: "embeddings",
baseUrl: "http://192.168.1.10:11434/v1",
});
const originalFetch = globalThis.fetch;
let captured: { headers: Record<string, string> } | null = null;
globalThis.fetch = async (_url: RequestInfo | URL, options: RequestInit = {}) => {
captured = { headers: (options.headers as Record<string, string>) || {} };
return new Response(
JSON.stringify({
data: [{ object: "embedding", embedding: [0.1, 0.2], index: 0 }],
usage: { prompt_tokens: 3, total_tokens: 3 },
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
};
try {
const res = await createEmbeddingResponse({
model: "lanollama6925b/nomic-embed-text",
input: "hello world",
});
assert.equal(res.status, 200);
} finally {
globalThis.fetch = originalFetch;
}
assert.ok(captured);
assert.equal(captured!.headers.Authorization, undefined);
});
test("#6925: cloud-metadata endpoint (169.254.169.254) stays blocked, does not become a bare no-auth provider", async () => {
await createProviderNode({
type: "openai-compatible-embeddings",
name: "Metadata Probe",
prefix: "metadataprobe6925",
apiType: "embeddings",
baseUrl: "http://169.254.169.254/v1",
});
const res = await createEmbeddingResponse({
model: "metadataprobe6925/whatever",
input: "hello world",
});
// Must not be silently treated as a trusted no-auth local provider — either
// rejected outright or still routed through the fallback (never authType "none").
assert.notEqual(res.status, 200, "cloud-metadata host must not resolve as a no-auth provider");
});