mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 06:12:10 +03:00
Implements end-to-end proxy egress isolation with address-family enforcement and closes four real IP-leak paths surfaced by the proxy subsystem audit. IPv6-only egress: - Detect proxy family (IPv6/IPv4 literal, or per-account auto/ipv4/ipv6 directive) and pin the connect family (family:6/4 + autoSelectFamily:false) on both ProxyAgent (proxyTls) and a custom SOCKS connector that threads socket_options.family into SocksClient (fetch-socks can't), so Happy Eyeballs cannot pick IPv4 for an IPv6-only policy. - De-bracket IPv6-literal proxy hosts (socksOptions.host + proxyHealth tcpCheck) — fixes ENOTFOUND on [::1]-style SOCKS proxies and health checks. - Fail-closed when an IPv6-only hostname proxy has no AAAA (PROXY_FAMILY_UNAVAILABLE) instead of leaking over v4. - DB migration 099 adds the proxy family column; family is preserved through the resolveProxyForConnection cascade and proxies/upstreamProxy modules. Leak fixes: - L1: web TLS clients (grok/claude/chatgpt/perplexity) now fail-closed on proxy-resolution error instead of silently going direct (was the highest-risk asymmetry). - L2: safeResolveProxy fail-closed by default (PROXY_FAIL_OPEN opt-out). - L3: API-key usage/quota fetch routed through the connection proxy context. - L4: NVIDIA validation proxy bypass (#3226) documented. Tests: 46 TDD unit tests + BDD egress-isolation matrix. typecheck:core + eslint clean. Gemini 'critical' SOCKS finding verified as a false positive (socks accepts proxy.host; see PR thread). Integrated into release/v3.8.24.
40 lines
1.3 KiB
TypeScript
40 lines
1.3 KiB
TypeScript
import dns from "node:dns/promises";
|
|
import { detectIpLiteralFamily, stripIpv6Brackets } from "./proxyFamily.ts";
|
|
|
|
export type FamilyLookupFn = (
|
|
hostname: string
|
|
) => Promise<Array<{ address: string; family: number }>>;
|
|
|
|
const defaultLookup: FamilyLookupFn = (hostname) => dns.lookup(hostname, { all: true });
|
|
|
|
/**
|
|
* Fail-closed guarantee for an IPv6-only (or IPv4-only) proxy given as a hostname:
|
|
* refuse early if the hostname has no record in the required family. No-op for IP
|
|
* literals (their family is intrinsic).
|
|
*/
|
|
export async function assertHostnameSupportsFamily(
|
|
host: string,
|
|
family: 4 | 6,
|
|
lookupFn: FamilyLookupFn = defaultLookup
|
|
): Promise<void> {
|
|
if (detectIpLiteralFamily(host) !== null) return;
|
|
let records: Array<{ address: string; family: number }>;
|
|
try {
|
|
records = await lookupFn(stripIpv6Brackets(host));
|
|
} catch (err) {
|
|
throw new Error(
|
|
`[ProxyFamily] DNS resolution failed for ${host}; refusing to egress (fail-closed): ${
|
|
err instanceof Error ? err.message : String(err)
|
|
}`
|
|
);
|
|
}
|
|
const hasFamily = records.some((r) => r.family === family);
|
|
if (!hasFamily) {
|
|
throw new Error(
|
|
`[ProxyFamily] Proxy host ${host} has no ${family === 6 ? "IPv6 (AAAA)" : "IPv4 (A)"} record; refusing ${
|
|
family === 6 ? "IPv6" : "IPv4"
|
|
}-only egress (fail-closed)`
|
|
);
|
|
}
|
|
}
|