mirror of
https://github.com/LittleChest/Dohna-NS.git
synced 2026-09-20 16:26:50 +08:00
handler: Refactor
This commit is contained in:
+375
@@ -0,0 +1,375 @@
|
||||
export default async function handler(
|
||||
request,
|
||||
dns,
|
||||
api,
|
||||
ipv4Prefix = 32,
|
||||
ipv6Prefix = 128,
|
||||
concurrent = false,
|
||||
rawIP,
|
||||
) {
|
||||
if (!dns || dns.length === 0) {
|
||||
dns = [
|
||||
"https://dns.google/dns-query",
|
||||
"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::8844]/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://dns.google/resolve",
|
||||
"https://8.8.8.8/resolve",
|
||||
"https://8.8.4.4/resolve",
|
||||
"https://[2001:4860:4860::8888]/resolve",
|
||||
"https://[2001:4860:4860::8844]/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 { search, searchParams } = new URL(url);
|
||||
|
||||
const ip =
|
||||
rawIP ||
|
||||
headers.get("x-forwarded-for").split(",")[0].trim() ||
|
||||
headers.get("x-real-ip");
|
||||
|
||||
let res = new Response(null, { status: 400 });
|
||||
|
||||
// JSON API
|
||||
if (method === "GET" && searchParams.has("name")) {
|
||||
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)",
|
||||
},
|
||||
}).then((res) => {
|
||||
if (res.status !== 200) {
|
||||
throw new Error(
|
||||
`Failed to connect to ${server}: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
return res;
|
||||
}),
|
||||
),
|
||||
);
|
||||
} 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)",
|
||||
},
|
||||
});
|
||||
if (res.status === 200) 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 });
|
||||
}
|
||||
}
|
||||
|
||||
// DNS Query
|
||||
let queryData;
|
||||
|
||||
// GET
|
||||
if (method === "GET" && searchParams.has("dns")) {
|
||||
// Decode the base64-encoded DNS query
|
||||
try {
|
||||
const decodedQuery = atob(searchParams.get("dns"));
|
||||
queryData = new Uint8Array(decodedQuery.length);
|
||||
for (let i = 0; i < decodedQuery.length; i++) {
|
||||
queryData[i] = decodedQuery.charCodeAt(i);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// POST
|
||||
if (method === "POST") {
|
||||
const requestBody = await request.arrayBuffer();
|
||||
|
||||
// Anti-GFW
|
||||
if (
|
||||
headers.get("content-length") === "29" &&
|
||||
(headers.get("user-agent") === "Go-http-client/1.1" ||
|
||||
headers.get("user-agent") === "Go-http-client/2.0") &&
|
||||
headers.get("accept") === "application/dns-message" &&
|
||||
headers.get("content-type") === "application/dns-message" &&
|
||||
(headers.get("accept-encoding") === "gzip, br" ||
|
||||
headers.get("accept-encoding") === "gzip")
|
||||
) {
|
||||
const bodyHex = Array.from(new Uint8Array(requestBody))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
if (
|
||||
bodyHex.slice(4) ===
|
||||
"01100001000000000000077477697474657203636f6d0000010001"
|
||||
) {
|
||||
return new Response(null, { status: 403 });
|
||||
}
|
||||
}
|
||||
queryData = new Uint8Array(requestBody);
|
||||
}
|
||||
|
||||
if (queryData) {
|
||||
res = await queryDns(
|
||||
queryData,
|
||||
ip,
|
||||
dns,
|
||||
ipv4Prefix,
|
||||
ipv6Prefix,
|
||||
concurrent,
|
||||
);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async function queryDns(
|
||||
queryData,
|
||||
ip,
|
||||
dns,
|
||||
ipv4Prefix,
|
||||
ipv6Prefix,
|
||||
concurrent,
|
||||
) {
|
||||
const hasOptRecord = checkForOptRecord(queryData);
|
||||
let newQueryData = queryData;
|
||||
if (!hasOptRecord && ip) {
|
||||
// Extract DNS Header and Question Section
|
||||
const [headerAndQuestion] = extractHeaderAndQuestion(queryData);
|
||||
|
||||
// Construct a new OPT record with ECS option
|
||||
const optRecord = createOptRecord(ip, ipv4Prefix, ipv6Prefix);
|
||||
|
||||
// Combine the header, question, and new OPT record to create a new query
|
||||
newQueryData = combineQueryData(headerAndQuestion, optRecord);
|
||||
}
|
||||
|
||||
let res = new Response(null, { status: 500 });
|
||||
if (concurrent) {
|
||||
try {
|
||||
res = await Promise.any(
|
||||
dns.map((server) =>
|
||||
fetchUpstream(server, ip, newQueryData).then((res) => {
|
||||
if (res.status !== 200) {
|
||||
throw new Error(
|
||||
`Failed to connect to ${server}: ${res.status} ${res.statusText}`,
|
||||
);
|
||||
}
|
||||
return res;
|
||||
}),
|
||||
),
|
||||
);
|
||||
} 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);
|
||||
if (res.status === 200) 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",
|
||||
Accept: "application/dns-message",
|
||||
"User-Agent": "Dohna-NS (https://github.com/LittleChest/Dohna-NS)",
|
||||
"X-Forwarded-For": ip,
|
||||
},
|
||||
body: newQueryData,
|
||||
});
|
||||
}
|
||||
|
||||
function checkForOptRecord(data) {
|
||||
// Get the number of additional records (ARCOUNT)
|
||||
const arcount = (data[10] << 8) | data[11];
|
||||
if (arcount === 0) return false;
|
||||
|
||||
let offset = 12; // DNS header is 12 bytes
|
||||
|
||||
// Skip the Question Section
|
||||
const qdcount = (data[4] << 8) | data[5];
|
||||
for (let i = 0; i < qdcount; i++) {
|
||||
while (data[offset] !== 0) offset++; // Skip QNAME
|
||||
offset += 5; // Skip QNAME (0 byte) + QTYPE (2 bytes) + QCLASS (2 bytes)
|
||||
}
|
||||
|
||||
// Skip the Answer Section
|
||||
const ancount = (data[6] << 8) | data[7];
|
||||
for (let i = 0; i < ancount; i++) {
|
||||
// Skip each Answer record
|
||||
while (data[offset] !== 0) offset++;
|
||||
offset += 10; // TYPE(2) + CLASS(2) + TTL(4) + RDLENGTH(2)
|
||||
const rdlength = (data[offset - 2] << 8) | data[offset - 1];
|
||||
offset += rdlength;
|
||||
}
|
||||
|
||||
// Check Additional Section for OPT record
|
||||
for (let i = 0; i < arcount; i++) {
|
||||
if (data[offset] === 0) {
|
||||
// OPT record NAME must be root (0)
|
||||
const type = (data[offset + 1] << 8) | data[offset + 2];
|
||||
if (type === 41) {
|
||||
// 41 is the OPT record type
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Skip this additional record
|
||||
while (data[offset] !== 0) offset++;
|
||||
offset += 10;
|
||||
const rdlength = (data[offset - 2] << 8) | data[offset - 1];
|
||||
offset += rdlength;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractHeaderAndQuestion(data) {
|
||||
let offset = 12; // DNS header is 12 bytes
|
||||
|
||||
// Get the number of questions
|
||||
const qdcount = (data[4] << 8) | data[5];
|
||||
|
||||
// Skip the Question Section
|
||||
for (let i = 0; i < qdcount; i++) {
|
||||
while (data[offset] !== 0) offset++; // Skip QNAME
|
||||
offset += 5; // Skip QNAME (0 byte) + QTYPE (2 bytes) + QCLASS (2 bytes)
|
||||
}
|
||||
|
||||
// Extract Header and Question Section
|
||||
const headerAndQuestion = data.subarray(0, offset);
|
||||
|
||||
return [headerAndQuestion, offset];
|
||||
}
|
||||
|
||||
function createOptRecord(ip, ipv4Prefix, ipv6Prefix) {
|
||||
let ecsData;
|
||||
let family;
|
||||
|
||||
if (isIPv4(ip)) {
|
||||
const prefixLength = ipv4Prefix;
|
||||
// Convert client IP to bytes
|
||||
const ipParts = ip.split(".").map((part) => parseInt(part, 10));
|
||||
family = 1; // IPv4
|
||||
ecsData = [0, 8, 0, 8, 0, family, prefixLength, 0, ...ipParts];
|
||||
} else if (isIPv6(ip)) {
|
||||
const prefixLength = ipv6Prefix;
|
||||
// Convert client IP to bytes
|
||||
const ipParts = ipv6ToBytes(ip);
|
||||
family = 2; // IPv6
|
||||
ecsData = [0, 8, 0, 20, 0, family, prefixLength, 0, ...ipParts];
|
||||
} else {
|
||||
throw new Error("Invalid IP address");
|
||||
}
|
||||
|
||||
// Construct the OPT record
|
||||
return new Uint8Array([
|
||||
0, // Name (root)
|
||||
0,
|
||||
41, // Type: OPT
|
||||
16,
|
||||
0, // UDP payload size (default 4096)
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0, // Extended RCODE and flags
|
||||
0,
|
||||
ecsData.length, // RD Length
|
||||
...ecsData,
|
||||
]);
|
||||
}
|
||||
|
||||
function isIPv4(ip) {
|
||||
return ip.split(".").length === 4;
|
||||
}
|
||||
|
||||
function isIPv6(ip) {
|
||||
return ip.split(":").length > 2; // At least 3 groups separated by colons
|
||||
}
|
||||
|
||||
function ipv6ToBytes(ipv6) {
|
||||
// Split the IPv6 address into segments
|
||||
let segments = ipv6.split(":");
|
||||
|
||||
// Expand shorthand notation (e.g., '::')
|
||||
let expandedSegments = [];
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (segments[i] === "") {
|
||||
// Insert zero segments for "::"
|
||||
let zeroSegments = 8 - (segments.length - 1);
|
||||
expandedSegments.push(...new Array(zeroSegments).fill("0000"));
|
||||
} else {
|
||||
expandedSegments.push(segments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert each segment into a 16-bit number and then into 8-bit numbers
|
||||
let bytes = [];
|
||||
for (let segment of expandedSegments) {
|
||||
let segmentValue = parseInt(segment, 16);
|
||||
bytes.push((segmentValue >> 8) & 0xff); // High byte
|
||||
bytes.push(segmentValue & 0xff); // Low byte
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function combineQueryData(headerAndQuestion, optRecord) {
|
||||
// Combine the header and question section with the new OPT record
|
||||
const newQueryData = new Uint8Array(
|
||||
headerAndQuestion.length + optRecord.length,
|
||||
);
|
||||
newQueryData.set(headerAndQuestion, 0);
|
||||
newQueryData.set(optRecord, headerAndQuestion.length);
|
||||
// https://en.wikipedia.org/wiki/Domain_Name_System#DNS_message_format
|
||||
// Incrementing the QDCOUNT field (offset 3) to 32, signaling an additional record in the question section.
|
||||
// Setting the ARCOUNT field (offset 11) to 1, indicating one additional record in the message.
|
||||
newQueryData.set([32], 3);
|
||||
newQueryData.set([1], 11);
|
||||
return newQueryData;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import dnsHandler from "./dns";
|
||||
|
||||
export default async function handler(
|
||||
request,
|
||||
dns,
|
||||
api,
|
||||
ipv4Prefix = 32,
|
||||
ipv6Prefix = 128,
|
||||
concurrent = false,
|
||||
rawIP,
|
||||
) {
|
||||
const { pathname } = new URL(request.url);
|
||||
let res = new Response(null, { status: 404 });
|
||||
|
||||
// DNS over HTTPS & JSON API
|
||||
if (pathname === "/dns-query" || pathname === "/resolve") {
|
||||
res = dnsHandler(request, dns, api, ipv4Prefix, ipv6Prefix, concurrent, rawIP);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user