From 61e691df70f66f46b48c2fb51c746389a5455bc9 Mon Sep 17 00:00:00 2001 From: LittleChest Date: Wed, 11 Feb 2026 15:01:30 +0800 Subject: [PATCH] handler: Add multi-server support --- README.md | 15 +-- _worker.js | 3 +- common.js | 132 ++++++++++++++++++++++++--- middleware.js | 3 +- netlify/edge-functions/middleware.js | 3 +- 5 files changed, 133 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 09c4670..5017755 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ + + # Dohna NS 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 -| Key | Default | Description | -| ----------- | ---------------------------- | -------------------------------------------------- | -| DNS | https://dns.google/dns-query | Specify a DNS over HTTPS server as the upstream. | -| API | https://dns.google/resolve | Specify a JSON API server as the upstream. | -| IPV4_PREFIX | 32 | Specify the EDNS client subnet IPv4 prefix length. | -| IPV6_PREFIX | 128 | Specify the EDNS client subnet IPv6 prefix length. | +| Key | Default | Description | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| 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://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. | +| 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 diff --git a/_worker.js b/_worker.js index 508988c..9ef34d7 100644 --- a/_worker.js +++ b/_worker.js @@ -8,6 +8,7 @@ export default { env.API, env.IPV4_PREFIX, env.IPV6_PREFIX, - request.headers.get("cf-connecting-ip") + env.CONCURRENT, + request.headers.get("cf-connecting-ip"), ), }; diff --git a/common.js b/common.js index c26c1e6..c1a19cd 100644 --- a/common.js +++ b/common.js @@ -1,11 +1,47 @@ export default async function handler( request, - dns = "https://dns.google/dns-query", - api = "https://dns.google/resolve", + dns, + api, ipv4Prefix = 32, ipv6Prefix = 128, + concurrent = false, 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 host = headers.get("Host"); @@ -28,12 +64,40 @@ export default async function handler( res = new Response(null, { status: 400 }); if (method === "GET" && searchParams.has("name")) { - res = fetch(api + search, { - method: "GET", - headers: { - "User-Agent": "Dohna-NS (https://github.com/LittleChest/Dohna-NS)", - }, - }); + if (concurrent) { + res = await Promise.any( + api.map((server) => + 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); } - if (queryData !== undefined) { - res = await queryDns(queryData, ip, dns, ipv4Prefix, ipv6Prefix); + if (queryData) { + res = await queryDns( + queryData, + ip, + dns, + ipv4Prefix, + ipv6Prefix, + concurrent, + ); } } return res; } -async function queryDns(queryData, ip, dns, ipv4Prefix, ipv6Prefix) { +async function queryDns( + queryData, + ip, + dns, + ipv4Prefix, + ipv6Prefix, + concurrent, +) { const hasOptRecord = checkForOptRecord(queryData); let newQueryData = queryData; if (!hasOptRecord && ip) { @@ -107,7 +185,35 @@ async function queryDns(queryData, ip, dns, ipv4Prefix, ipv6Prefix) { 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", headers: { "Content-Type": "application/dns-message", @@ -117,8 +223,6 @@ async function queryDns(queryData, ip, dns, ipv4Prefix, ipv6Prefix) { }, body: newQueryData, }); - - return res; } function checkForOptRecord(data) { diff --git a/middleware.js b/middleware.js index ad4684b..af958ac 100644 --- a/middleware.js +++ b/middleware.js @@ -6,6 +6,7 @@ export default middleware = async (request) => { process.env.DNS, process.env.API, process.env.IPV4_PREFIX, - process.env.IPV6_PREFIX + process.env.IPV6_PREFIX, + process.env.CONCURRENT, ); }; diff --git a/netlify/edge-functions/middleware.js b/netlify/edge-functions/middleware.js index 3062f7f..3fb4ecf 100644 --- a/netlify/edge-functions/middleware.js +++ b/netlify/edge-functions/middleware.js @@ -6,6 +6,7 @@ export default async (request) => Netlify.env.get("API"), Netlify.env.get("IPV4_PREFIX"), Netlify.env.get("IPV6_PREFIX"), - Netlify.context.ip + Netlify.env.get("CONCURRENT"), + Netlify.context.ip, ); export const config = { path: "*" };