Files

48 lines
2.4 KiB
JavaScript

import http from "node:http";
import net from "node:net";
import { lookup } from "node:dns/promises";
import { pathToFileURL } from "node:url";
export function publicIPv4(ip) {
if (net.isIP(ip) !== 4) return false;
const [a,b,c] = ip.split(".").map(Number);
return !(a === 0 || a === 10 || a === 127 || a >= 224 ||
(a === 100 && b >= 64 && b <= 127) || (a === 169 && b === 254) ||
(a === 172 && b >= 16 && b <= 31) || (a === 192 && (b === 168 || b === 0 || (b === 88 && c === 99))) ||
(a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100))) || (a === 203 && b === 0 && c === 113));
}
export async function targetFor(authority, hosts, resolve = lookup) {
const match = /^([a-z0-9.-]+):443$/i.exec(authority ?? "");
if (!match || !hosts.has(match[1].toLowerCase()) || net.isIP(match[1])) throw new Error("HOST_DENIED");
const addresses = await resolve(match[1], { family: 4, all: true });
if (!addresses.length || addresses.some(a => !publicIPv4(a.address))) throw new Error("ADDRESS_DENIED");
return addresses[0].address; // Connect to the checked address, not a second DNS lookup.
}
export function startProxy() {
const hosts = new Set((process.env.EGRESS_HOSTS ?? "").split(",").map(s => s.trim().toLowerCase()).filter(Boolean));
const server = http.createServer((_req,res) => res.writeHead(403).end());
server.maxConnections = 64;
server.headersTimeout = 10000;
server.on("connect", async (req, client, head) => {
client.on("error", () => client.destroy());
client.setTimeout(120000, () => client.destroy());
let upstream;
try {
const ip = await targetFor(req.url, hosts);
if (client.destroyed) return;
upstream = net.connect({ host: ip, port: 443 });
upstream.setTimeout(120000, () => upstream.destroy());
upstream.on("error", () => client.destroy());
client.on("close", () => upstream.destroy());
upstream.on("close", () => client.destroy());
upstream.once("connect", () => {
client.write("HTTP/1.1 200 Connection Established\r\n\r\n");
if (head.length) upstream.write(head);
client.pipe(upstream); upstream.pipe(client);
});
} catch { upstream?.destroy(); client.end("HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n"); }
});
server.listen(3128, "0.0.0.0");
return server;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) startProxy();