mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
137 lines
4.1 KiB
JavaScript
137 lines
4.1 KiB
JavaScript
import fs from "node:fs";
|
|
import http from "node:http";
|
|
import net from "node:net";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
import {
|
|
isAllowedGuardHostname,
|
|
parseConnectAuthority,
|
|
parseTlsClientHelloSni,
|
|
sanitizeForwardHeaders,
|
|
} from "./policy.mjs";
|
|
|
|
const MAX_CLIENT_HELLO_BYTES = 64 * 1024;
|
|
const CLIENT_HELLO_TIMEOUT_MS = 3000;
|
|
|
|
export function createGuardProxy({
|
|
policy = "deny-all",
|
|
logPath = "/tmp/egress.jsonl",
|
|
allowHostname = (hostname) => isAllowedGuardHostname(hostname, policy),
|
|
connectSocket = (port, hostname, onConnect) => net.connect(port, hostname, onConnect),
|
|
} = {}) {
|
|
if (!new Set(["deny-all", "devin"]).has(policy)) {
|
|
throw new Error(`Unknown network guard policy: ${policy}`);
|
|
}
|
|
|
|
function audit(hostname, decision, reason) {
|
|
fs.appendFileSync(
|
|
logPath,
|
|
`${JSON.stringify({ at: new Date().toISOString(), hostname, decision, reason })}\n`
|
|
);
|
|
}
|
|
|
|
const server = http.createServer((req, res) => {
|
|
let target;
|
|
try {
|
|
target = new URL(req.url);
|
|
} catch {
|
|
res.writeHead(400).end("invalid proxy target\n");
|
|
return;
|
|
}
|
|
if (target.protocol !== "http:" || target.username || target.password) {
|
|
audit(target.hostname, "deny", "invalid_http_target");
|
|
res.writeHead(403).end("egress denied\n");
|
|
return;
|
|
}
|
|
if (!allowHostname(target.hostname)) {
|
|
audit(target.hostname, "deny", "host_policy");
|
|
res.writeHead(403).end("egress denied\n");
|
|
return;
|
|
}
|
|
audit(target.hostname, "allow", "host_policy");
|
|
const upstream = http.request(
|
|
target,
|
|
{
|
|
method: req.method,
|
|
headers: sanitizeForwardHeaders(req.headers, target),
|
|
},
|
|
(reply) => {
|
|
res.writeHead(reply.statusCode || 502, reply.headers);
|
|
reply.pipe(res);
|
|
}
|
|
);
|
|
req.pipe(upstream);
|
|
upstream.on("error", () => res.writeHead(502).end("upstream error\n"));
|
|
});
|
|
|
|
server.on("connect", (req, client, head) => {
|
|
const authority = parseConnectAuthority(req.url);
|
|
if (!authority) {
|
|
audit(req.url, "deny", "invalid_connect_authority");
|
|
client.end("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
return;
|
|
}
|
|
const { hostname, port } = authority;
|
|
if (!allowHostname(hostname)) {
|
|
audit(hostname, "deny", "host_policy");
|
|
client.end("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
return;
|
|
}
|
|
|
|
let buffer = Buffer.from(head);
|
|
let settled = false;
|
|
const timer = setTimeout(() => fail("client_hello_timeout"), CLIENT_HELLO_TIMEOUT_MS);
|
|
timer.unref?.();
|
|
|
|
const cleanup = () => {
|
|
clearTimeout(timer);
|
|
client.removeListener("data", onData);
|
|
};
|
|
const fail = (reason) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
cleanup();
|
|
audit(hostname, "deny", reason);
|
|
client.destroy();
|
|
};
|
|
const inspect = () => {
|
|
if (buffer.length > MAX_CLIENT_HELLO_BYTES) return fail("client_hello_too_large");
|
|
const parsed = parseTlsClientHelloSni(buffer);
|
|
if (parsed.status === "need-more") return;
|
|
if (parsed.status !== "ok") return fail(parsed.reason || "invalid_client_hello");
|
|
if (parsed.serverName !== hostname) return fail("sni_mismatch");
|
|
settled = true;
|
|
cleanup();
|
|
client.pause();
|
|
const upstream = connectSocket(port, hostname, () => {
|
|
audit(hostname, "allow", "sni_match");
|
|
if (buffer.length) upstream.write(buffer);
|
|
upstream.pipe(client);
|
|
client.pipe(upstream);
|
|
client.resume();
|
|
});
|
|
upstream.on("error", () => client.destroy());
|
|
};
|
|
const onData = (chunk) => {
|
|
buffer = Buffer.concat([buffer, chunk]);
|
|
inspect();
|
|
};
|
|
|
|
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
client.on("data", onData);
|
|
if (buffer.length) inspect();
|
|
client.resume();
|
|
});
|
|
|
|
return server;
|
|
}
|
|
|
|
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
const [host, portText] = (process.env.GUARD_LISTEN || "0.0.0.0:8080").split(":");
|
|
const server = createGuardProxy({
|
|
policy: process.env.GUARD_POLICY || "deny-all",
|
|
logPath: process.env.GUARD_LOG || "/tmp/egress.jsonl",
|
|
});
|
|
server.listen(Number(portText), host);
|
|
}
|