Compare commits

..

No commits in common. "188808ab4d4c1320cc656b0c8d7b1aa5d4517ce9" and "1b6191580d9d4716dc99e8cece1f5537d8fb1033" have entirely different histories.

7 changed files with 105 additions and 211 deletions

View File

@ -21,14 +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://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. | | 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. | | 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. | | CONCURRENT | false | Whether it concurrently queries all servers and returns the fastest result. |
| ENABLE_MOBILECONFIG | false | Whether to enable the [Apple MobileConfig API](#apple-mobileconfig-api). |
## Self-hosted ## Self-hosted
@ -37,15 +36,3 @@ You can use [Netlify CLI](https://cli.netlify.com/commands/serve/) or [`workerd`
Make sure you can connect to upstream servers. Make sure you can connect to upstream servers.
If you find a bug on self-hosted, try to reproduce it at `dohna.ovh` before reporting it. If you find a bug on self-hosted, try to reproduce it at `dohna.ovh` before reporting it.
## Apple MobileConfig API
MobileConfig can configure system-level DNS over HTTPS for Apple devices.
`dohna.ovh` has already enabled this API; you only need to edit a few parameters to point the URL to your domain, so there is no need to enable this API for your self-hosted instance.
| Query Parameter | Default | Description |
| --------------- | --------------------------------- | ------------------------------------------------------ |
| domain | dohna.ovh | Specify the DNS over HTTPS domain. |
| name | Dohna NS | Specify the name of the generated MobileConfig. |
| desc | Yet another DNS over HTTPS relay. | Specify the description of the generated MobileConfig. |

View File

@ -1,4 +1,4 @@
import handler from "./handler/dns"; import handler from "./common";
export default { export default {
fetch: async (request, env) => fetch: async (request, env) =>
@ -10,6 +10,5 @@ export default {
env.IPV6_PREFIX, env.IPV6_PREFIX,
env.CONCURRENT, env.CONCURRENT,
request.headers.get("cf-connecting-ip"), request.headers.get("cf-connecting-ip"),
env.ENABLE_MOBILE_CONFIG,
), ),
}; };

View File

@ -45,111 +45,119 @@ export default async function handler(
} }
const { method, headers, url } = request; const { method, headers, url } = request;
const { search, searchParams } = new URL(url); const { search, searchParams, pathname } = new URL(url);
const ip = const ip =
rawIP || rawIP ||
headers.get("x-forwarded-for").split(",")[0].trim() || headers.get("x-forwarded-for").split(",")[0].trim() ||
headers.get("x-real-ip"); headers.get("x-real-ip");
let res = new Response(null, { status: 400 }); let res = new Response(null, { status: 404 });
// JSON API // JSON API
if (method === "GET" && searchParams.has("name")) { if (pathname === "/resolve") {
if (concurrent) { res = new Response(null, { status: 400 });
res = await Promise.any(
api.map((server) => if (method === "GET" && searchParams.has("name")) {
fetch(server + search, { if (concurrent) {
method: "GET", res = await Promise.any(
headers: { api.map((server) =>
"User-Agent": fetch(server + search, {
"Dohna-NS (https://github.com/LittleChest/Dohna-NS)", method: "GET",
}, headers: {
}).then((res) => { "User-Agent":
if (res.status !== 200) { "Dohna-NS (https://github.com/LittleChest/Dohna-NS)",
throw new Error( },
`Failed to connect to ${server}: ${res.status} ${res.statusText}`, }).then((res) => {
); if (res.status !== 200) {
} throw new Error(
return res; `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); } else {
const server = servers.splice(index, 1)[0]; const servers = [...api];
try { while (servers.length > 0) {
res = await fetch(server + search, { const index = Math.floor(Math.random() * servers.length);
method: "GET", const server = servers.splice(index, 1)[0];
headers: { try {
"User-Agent": res = await fetch(server + search, {
"Dohna-NS (https://github.com/LittleChest/Dohna-NS)", method: "GET",
}, headers: {
}); "User-Agent":
if (res.status === 200) break; "Dohna-NS (https://github.com/LittleChest/Dohna-NS)",
} catch (e) { },
console.warn(`Failed to connect to ${server}: ${e.message}`); });
continue; 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 });
} }
console.error("All upstream JSON API servers failed.");
res = new Response(null, { status: 500 });
} }
} }
// DNS Query // DNS Query
let queryData; if (pathname === "/dns-query") {
res = new Response(null, { status: 400 });
// GET let queryData;
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 // GET
if (method === "POST") { if (method === "GET" && searchParams.has("dns")) {
const requestBody = await request.arrayBuffer(); // Decode the base64-encoded DNS query
try {
// Anti-GFW const decodedQuery = atob(searchParams.get("dns"));
if ( queryData = new Uint8Array(decodedQuery.length);
headers.get("content-length") === "29" && for (let i = 0; i < decodedQuery.length; i++) {
(headers.get("user-agent") === "Go-http-client/1.1" || queryData[i] = decodedQuery.charCodeAt(i);
headers.get("user-agent") === "Go-http-client/2.0") && }
headers.get("accept") === "application/dns-message" && } catch {}
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) { // POST
res = await queryDns( if (method === "POST") {
queryData, const requestBody = await request.arrayBuffer();
ip,
dns, // Anti-GFW
ipv4Prefix, if (
ipv6Prefix, headers.get("content-length") === "29" &&
concurrent, (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; return res;

View File

@ -1,36 +0,0 @@
import dnsHandler from "./dns";
import mobileconfigHandler from "./mobileconfig";
export default async function handler(
request,
dns,
api,
ipv4Prefix = 32,
ipv6Prefix = 128,
concurrent = false,
rawIP,
enableMobileConfig = false,
) {
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,
);
}
// Apple Mobile Config
if (enableMobileConfig && pathname === "/mobileconfig") {
res = mobileconfigHandler(request);
}
return res;
}

View File

@ -1,61 +0,0 @@
export default async function handler(request) {
const { headers, searchParams } = new URL(request.url);
const domain = searchParams.get("domain") || headers.get("domain") || "dohna.ovh";
const name = decodeURIComponent(searchParams.get("name")) || "Dohna NS";
const desc =
decodeURIComponent(searchParams.get("desc")) ||
"Yet another DNS over HTTPS relay.";
return new Response(
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>DNSSettings</key>
<dict>
<key>DNSProtocol</key>
<string>HTTPS</string>
<key>ServerURL</key>
<string>https://${domain}/dns-query</string>
</dict>
<key>PayloadDisplayName</key>
<string>${name}</string>
<key>PayloadDescription</key>
<string>${desc}</string>
<key>PayloadIdentifier</key>
<string>com.apple.dnsSettings.managed.${String(Crypto.randomUUID()).toUpperCase()}</string>
<key>PayloadType</key>
<string>com.apple.dnsSettings.managed</string>
<key>PayloadUUID</key>
<string>${String(Crypto.randomUUID()).toUpperCase()}</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>ProhibitDisablement</key>
<false/>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>${name}</string>
<key>PayloadDescription</key>
<string>${desc}</string>
<key>PayloadIdentifier</key>
<string>${domain}</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>${String(Crypto.randomUUID()).toUpperCase()}</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>`,
{
headers: {
"Content-Type": "application/x-apple-aspen-config",
},
},
);
}

View File

@ -1,4 +1,4 @@
import handler from "./handler/dns"; import handler from "./common";
export default middleware = async (request) => { export default middleware = async (request) => {
return handler( return handler(
@ -8,7 +8,5 @@ export default middleware = async (request) => {
process.env.IPV4_PREFIX, process.env.IPV4_PREFIX,
process.env.IPV6_PREFIX, process.env.IPV6_PREFIX,
process.env.CONCURRENT, process.env.CONCURRENT,
undefined,
process.env.ENABLE_MOBILE_CONFIG,
); );
}; };

View File

@ -1,4 +1,4 @@
import handler from "../../handler/dns.js"; import handler from "../../common.js";
export default async (request) => export default async (request) =>
handler( handler(
request, request,
@ -8,6 +8,5 @@ export default async (request) =>
Netlify.env.get("IPV6_PREFIX"), Netlify.env.get("IPV6_PREFIX"),
Netlify.env.get("CONCURRENT"), Netlify.env.get("CONCURRENT"),
Netlify.context.ip, Netlify.context.ip,
Netlify.env.get("ENABLE_MOBILE_CONFIG"),
); );
export const config = { path: "*" }; export const config = { path: "*" };