fix outbound DNS lookup callback shape (#1101)

This commit is contained in:
ZacharyZcR
2026-07-28 01:47:58 +08:00
committed by GitHub
parent 1139f17319
commit cf827f9a9c
2 changed files with 57 additions and 25 deletions
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import type { LookupAddress, LookupAllOptions } from "dns"; import type { LookupAddress, LookupAllOptions, LookupOptions } from "dns";
import { import {
createDnsLookupHook, createDnsLookupHook,
isBlockedAddress, isBlockedAddress,
@@ -56,6 +56,7 @@ describe("isBlockedAddress", () => {
function runHook( function runHook(
addresses: LookupAddress[], addresses: LookupAddress[],
error: NodeJS.ErrnoException | null = null, error: NodeJS.ErrnoException | null = null,
options: LookupOptions = { all: true },
) { ) {
const fakeLookup = ( const fakeLookup = (
_host: string, _host: string,
@@ -65,14 +66,43 @@ function runHook(
const hook = createDnsLookupHook(fakeLookup); const hook = createDnsLookupHook(fakeLookup);
const callback = vi.fn(); const callback = vi.fn();
hook("example.invalid", { all: true }, callback); hook("example.invalid", options, callback);
return callback; return callback;
} }
describe("createDnsLookupHook", () => { describe("createDnsLookupHook", () => {
it("allows a public IPv4 address through", () => { it("returns every public address when all addresses are requested", () => {
const callback = runHook([{ address: "104.21.52.150", family: 4 }]); const addresses = [
{ address: "104.21.52.150", family: 4 },
{ address: "2606:4700:3034::ac43:c88d", family: 6 },
];
const callback = runHook(addresses);
expect(callback).toHaveBeenCalledWith(null, addresses);
});
it.each<LookupOptions>([{ all: false }, {}])(
"returns one public address for single-address lookup options %j",
(options) => {
const callback = runHook(
[{ address: "104.21.52.150", family: 4 }],
null,
options,
);
expect(callback).toHaveBeenCalledWith(null, "104.21.52.150", 4); expect(callback).toHaveBeenCalledWith(null, "104.21.52.150", 4);
},
);
it("rejects a mixed result containing a mapped private address", () => {
const callback = runHook([
{ address: "104.21.52.150", family: 4 },
{ address: "::ffff:127.0.0.1", family: 6 },
]);
expect(callback).toHaveBeenCalledWith(
expect.objectContaining({
message: "Private destinations are not allowed",
}),
[],
);
}); });
it("rejects a private address with the private-destination error", () => { it("rejects a private address with the private-destination error", () => {
@@ -81,8 +111,7 @@ describe("createDnsLookupHook", () => {
expect.objectContaining({ expect.objectContaining({
message: "Private destinations are not allowed", message: "Private destinations are not allowed",
}), }),
"", [],
0,
); );
}); });
@@ -92,8 +121,7 @@ describe("createDnsLookupHook", () => {
expect.objectContaining({ expect.objectContaining({
message: "DNS resolution returned no addresses", message: "DNS resolution returned no addresses",
}), }),
"", [],
0,
); );
}); });
@@ -102,6 +130,6 @@ describe("createDnsLookupHook", () => {
code: "ENOTFOUND", code: "ENOTFOUND",
}); });
const callback = runHook([], dnsError); const callback = runHook([], dnsError);
expect(callback).toHaveBeenCalledWith(dnsError, "", 0); expect(callback).toHaveBeenCalledWith(dnsError, []);
}); });
}); });
+19 -15
View File
@@ -1,4 +1,9 @@
import { lookup, type LookupAddress, type LookupAllOptions } from "dns"; import {
lookup,
type LookupAddress,
type LookupAllOptions,
type LookupOptions,
} from "dns";
import { BlockList, isIP } from "net"; import { BlockList, isIP } from "net";
import { Agent } from "undici"; import { Agent } from "undici";
@@ -13,8 +18,8 @@ type DnsLookupFn = (
type LookupHookCallback = ( type LookupHookCallback = (
error: NodeJS.ErrnoException | Error | null, error: NodeJS.ErrnoException | Error | null,
address: string, address: string | LookupAddress[],
family: number, family?: number,
) => void; ) => void;
const blockedAddresses = new BlockList(); const blockedAddresses = new BlockList();
@@ -71,28 +76,27 @@ export function isBlockedAddress(address: string): boolean {
export function createDnsLookupHook(dnsLookup: DnsLookupFn = lookup) { export function createDnsLookupHook(dnsLookup: DnsLookupFn = lookup) {
return function lookupHook( return function lookupHook(
host: string, host: string,
lookupOptions: LookupAllOptions, lookupOptions: LookupOptions,
callback: LookupHookCallback, callback: LookupHookCallback,
): void { ): void {
const fail = (error: NodeJS.ErrnoException | Error) => {
if (lookupOptions.all) return callback(error, []);
callback(error, "", 0);
};
dnsLookup( dnsLookup(
host, host,
{ ...lookupOptions, all: true, verbatim: true }, { ...lookupOptions, all: true, verbatim: true },
(error, addresses) => { (error, addresses) => {
if (error) return callback(error, "", 0); if (error) return fail(error);
if (!addresses.length) { if (!addresses.length) {
return callback( return fail(new Error("DNS resolution returned no addresses"));
new Error("DNS resolution returned no addresses"),
"",
0,
);
} }
if (addresses.some(({ address }) => isBlockedAddress(address))) { if (addresses.some(({ address }) => isBlockedAddress(address))) {
return callback( return fail(new Error("Private destinations are not allowed"));
new Error("Private destinations are not allowed"),
"",
0,
);
} }
if (lookupOptions.all) return callback(null, addresses);
const selected = addresses[0]; const selected = addresses[0];
callback(null, selected.address, selected.family); callback(null, selected.address, selected.family);
}, },