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
Another DNS over HTTPS recursive resolver.
@ -20,11 +22,12 @@ 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. |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| 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

View File

@ -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"),
),
};

124
common.js
View File

@ -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, {
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)",
"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) {

View File

@ -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,
);
};

View File

@ -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: "*" };