Merge commit from fork

This commit is contained in:
ZacharyZcR
2026-07-15 14:55:27 +08:00
committed by GitHub
parent 4b2a60a854
commit e1d1a3e53d
2 changed files with 96 additions and 1 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
import { statsLogger } from "./logger.js"; import { statsLogger } from "./logger.js";
import { safeOutboundFetch } from "./safe-outbound-fetch.js";
export interface AlertPayload { export interface AlertPayload {
hostName: string; hostName: string;
@@ -36,7 +37,7 @@ async function fetchWithRetry(
options: RequestInit, options: RequestInit,
): Promise<void> { ): Promise<void> {
const attempt = async () => { const attempt = async () => {
const res = await fetch(url, options); const res = await safeOutboundFetch(url, options);
if (!res.ok) { if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`); throw new Error(`HTTP ${res.status}: ${res.statusText}`);
} }
+94
View File
@@ -0,0 +1,94 @@
import { lookup } from "dns";
import { BlockList, isIP } from "net";
import { Agent } from "undici";
const blockedAddresses = new BlockList();
for (const [network, prefix] of [
["0.0.0.0", 8],
["10.0.0.0", 8],
["100.64.0.0", 10],
["127.0.0.0", 8],
["169.254.0.0", 16],
["172.16.0.0", 12],
["192.168.0.0", 16],
["198.18.0.0", 15],
["224.0.0.0", 4],
["240.0.0.0", 4],
] as const) {
blockedAddresses.addSubnet(network, prefix, "ipv4");
}
for (const [network, prefix] of [
["::", 128],
["::1", 128],
["::ffff:0:0", 96],
["fc00::", 7],
["fe80::", 10],
["ff00::", 8],
] as const) {
blockedAddresses.addSubnet(network, prefix, "ipv6");
}
function isBlockedAddress(address: string): boolean {
const family = isIP(address);
return (
family === 0 ||
blockedAddresses.check(address, family === 4 ? "ipv4" : "ipv6")
);
}
export async function safeOutboundFetch(
rawUrl: string,
options: RequestInit,
): Promise<Response> {
const url = new URL(rawUrl);
if (
!["http:", "https:"].includes(url.protocol) ||
url.username ||
url.password
) {
throw new Error("Invalid outbound URL");
}
const hostname = url.hostname.replace(/^\[|\]$/g, "");
if (isIP(hostname) && isBlockedAddress(hostname)) {
throw new Error("Private destinations are not allowed");
}
const dispatcher = new Agent({
connect: {
lookup(host, lookupOptions, callback) {
lookup(
host,
{ ...lookupOptions, all: true, verbatim: true },
(error, addresses) => {
if (error) return callback(error, "", 0);
if (
!addresses.length ||
addresses.some(({ address }) => isBlockedAddress(address))
) {
return callback(
new Error("Private destinations are not allowed"),
"",
0,
);
}
const selected = addresses[0];
callback(null, selected.address, selected.family);
},
);
},
},
});
try {
return await fetch(url, {
...options,
redirect: "error",
dispatcher,
} as RequestInit & { dispatcher: Agent });
} finally {
await dispatcher.close();
}
}