handler: Add multi-server support

This commit is contained in:
LittleChest 2026-02-11 15:01:30 +08:00
parent 1a10e16745
commit 61e691df70
5 changed files with 133 additions and 23 deletions

View File

@ -1,3 +1,5 @@
<!-- markdownlint-disable MD034 -->
# Dohna NS # Dohna NS
Another DNS over HTTPS recursive resolver. Another DNS over HTTPS recursive resolver.
@ -19,12 +21,13 @@ Read [Dohna NS Documentation](https://dohna.ovh/) to learn how to install Dohna
## Environment Variables ## Environment Variables
| Key | Default | Description | | Key | Default | Description |
| ----------- | ---------------------------- | -------------------------------------------------- | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| DNS | https://dns.google/dns-query | Specify a DNS over HTTPS server as the upstream. | | DNS | ["https://8.8.8.8/dns-query","https://8.8.4.4/dns-query","https://[2001:4860:4860::8888]/dns-query","https://[2001:4860:4860::8888]/dns-query"] | Specify a DNS over HTTPS server as the upstream. |
| API | https://dns.google/resolve | Specify a JSON API server as the upstream. | | API | ["https://8.8.8.8/resolve","https://8.8.4.4/resolve","https://[2001:4860:4860::8888]/resolve","https://[2001:4860:4860::8888]/resolve"] | Specify a JSON API server as the upstream. |
| IPV4_PREFIX | 32 | Specify the EDNS client subnet IPv4 prefix length. | | IPV4_PREFIX | 32 | Specify the EDNS client subnet IPv4 prefix length. |
| IPV6_PREFIX | 128 | Specify the EDNS client subnet IPv6 prefix length. | | IPV6_PREFIX | 128 | Specify the EDNS client subnet IPv6 prefix length. |
| CONCURRENT | false | Whether it concurrently queries all servers and returns the fastest result. |
## Self-hosted ## Self-hosted

View File

@ -8,6 +8,7 @@ export default {
env.API, env.API,
env.IPV4_PREFIX, env.IPV4_PREFIX,
env.IPV6_PREFIX, env.IPV6_PREFIX,
request.headers.get("cf-connecting-ip") env.CONCURRENT,
request.headers.get("cf-connecting-ip"),
), ),
}; };

132
common.js
View File

@ -1,11 +1,47 @@
export default async function handler( export default async function handler(
request, request,
dns = "https://dns.google/dns-query", dns,
api = "https://dns.google/resolve", api,
ipv4Prefix = 32, ipv4Prefix = 32,
ipv6Prefix = 128, ipv6Prefix = 128,
concurrent = false,
rawIP, rawIP,
) { ) {
if (!dns || dns.length === 0) {
dns = [
"https://8.8.8.8/dns-query",
"https://8.8.4.4/dns-query",
"https://[2001:4860:4860::8888]/dns-query",
"https://[2001:4860:4860::8888]/dns-query",
];
} else {
try {
dns = JSON.parse(dns.replace(/'/g, '"'));
} catch (e) {
console.warn(
`Unable to parse upstream DNS over HTTPS servers, using ${dns} as the only server.`,
);
dns = [dns];
}
}
if (!api || api.length === 0) {
api = [
"https://8.8.8.8/resolve",
"https://8.8.4.4/resolve",
"https://[2001:4860:4860::8888]/resolve",
"https://[2001:4860:4860::8888]/resolve",
];
} else {
try {
api = JSON.parse(api.replace(/'/g, '"'));
} catch (e) {
console.warn(
`Unable to parse upstream JSON API servers, using ${api} as the only server.`,
);
api = [api];
}
}
const { method, headers, url } = request; const { method, headers, url } = request;
const host = headers.get("Host"); const host = headers.get("Host");
@ -28,12 +64,40 @@ export default async function handler(
res = new Response(null, { status: 400 }); res = new Response(null, { status: 400 });
if (method === "GET" && searchParams.has("name")) { if (method === "GET" && searchParams.has("name")) {
res = fetch(api + search, { if (concurrent) {
method: "GET", res = await Promise.any(
headers: { api.map((server) =>
"User-Agent": "Dohna-NS (https://github.com/LittleChest/Dohna-NS)", fetch(server + search, {
}, method: "GET",
}); headers: {
"User-Agent":
"Dohna-NS (https://github.com/LittleChest/Dohna-NS)",
},
}),
),
);
} else {
const servers = [...api];
while (servers.length > 0) {
const index = Math.floor(Math.random() * servers.length);
const server = servers.splice(index, 1)[0];
try {
res = await fetch(server + search, {
method: "GET",
headers: {
"User-Agent":
"Dohna-NS (https://github.com/LittleChest/Dohna-NS)",
},
});
break;
} catch (e) {
console.warn(`Failed to connect to ${server}: ${e.message}`);
continue;
}
}
console.error("All upstream JSON API servers failed.");
res = new Response(null, { status: 500 });
}
} }
} }
@ -85,15 +149,29 @@ export default async function handler(
queryData = new Uint8Array(requestBody); queryData = new Uint8Array(requestBody);
} }
if (queryData !== undefined) { if (queryData) {
res = await queryDns(queryData, ip, dns, ipv4Prefix, ipv6Prefix); res = await queryDns(
queryData,
ip,
dns,
ipv4Prefix,
ipv6Prefix,
concurrent,
);
} }
} }
return res; return res;
} }
async function queryDns(queryData, ip, dns, ipv4Prefix, ipv6Prefix) { async function queryDns(
queryData,
ip,
dns,
ipv4Prefix,
ipv6Prefix,
concurrent,
) {
const hasOptRecord = checkForOptRecord(queryData); const hasOptRecord = checkForOptRecord(queryData);
let newQueryData = queryData; let newQueryData = queryData;
if (!hasOptRecord && ip) { if (!hasOptRecord && ip) {
@ -107,7 +185,35 @@ async function queryDns(queryData, ip, dns, ipv4Prefix, ipv6Prefix) {
newQueryData = combineQueryData(headerAndQuestion, optRecord); newQueryData = combineQueryData(headerAndQuestion, optRecord);
} }
const res = await fetch(dns, { let res = new Response(null, { status: 500 });
if (concurrent) {
try {
res = await Promise.any(
dns.map((server) => fetchUpstream(server, ip, newQueryData)),
);
} catch (e) {
console.error("All upstream DNS over HTTPS servers failed: " + e.message);
}
} else {
const servers = [...dns];
while (servers.length > 0) {
const index = Math.floor(Math.random() * servers.length);
const server = servers.splice(index, 1)[0];
try {
res = await fetchUpstream(server, ip, newQueryData);
break;
} catch (e) {
console.warn(`Failed to connect to ${server}: ${e.message}`);
continue;
}
}
if (!res) console.error("All upstream DNS over HTTPS servers failed.");
}
return res;
}
function fetchUpstream(dns, ip, newQueryData) {
return fetch(dns, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/dns-message", "Content-Type": "application/dns-message",
@ -117,8 +223,6 @@ async function queryDns(queryData, ip, dns, ipv4Prefix, ipv6Prefix) {
}, },
body: newQueryData, body: newQueryData,
}); });
return res;
} }
function checkForOptRecord(data) { function checkForOptRecord(data) {

View File

@ -6,6 +6,7 @@ export default middleware = async (request) => {
process.env.DNS, process.env.DNS,
process.env.API, process.env.API,
process.env.IPV4_PREFIX, process.env.IPV4_PREFIX,
process.env.IPV6_PREFIX process.env.IPV6_PREFIX,
process.env.CONCURRENT,
); );
}; };

View File

@ -6,6 +6,7 @@ export default async (request) =>
Netlify.env.get("API"), Netlify.env.get("API"),
Netlify.env.get("IPV4_PREFIX"), Netlify.env.get("IPV4_PREFIX"),
Netlify.env.get("IPV6_PREFIX"), Netlify.env.get("IPV6_PREFIX"),
Netlify.context.ip Netlify.env.get("CONCURRENT"),
Netlify.context.ip,
); );
export const config = { path: "*" }; export const config = { path: "*" };