mirror of
https://github.com/LittleChest/Dohna-NS.git
synced 2026-08-05 12:16:15 +08:00
Compare commits
2 Commits
1b6191580d
...
188808ab4d
| Author | SHA1 | Date | |
|---|---|---|---|
| 188808ab4d | |||
| 0d95560738 |
27
README.md
27
README.md
@ -21,13 +21,14 @@ 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
|
||||||
|
|
||||||
@ -36,3 +37,15 @@ 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. |
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import handler from "./common";
|
import handler from "./handler/dns";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fetch: async (request, env) =>
|
fetch: async (request, env) =>
|
||||||
@ -10,5 +10,6 @@ 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,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -45,119 +45,111 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { method, headers, url } = request;
|
const { method, headers, url } = request;
|
||||||
const { search, searchParams, pathname } = new URL(url);
|
const { search, searchParams } = 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: 404 });
|
let res = new Response(null, { status: 400 });
|
||||||
|
|
||||||
// JSON API
|
// JSON API
|
||||||
if (pathname === "/resolve") {
|
if (method === "GET" && searchParams.has("name")) {
|
||||||
res = new Response(null, { status: 400 });
|
if (concurrent) {
|
||||||
|
res = await Promise.any(
|
||||||
if (method === "GET" && searchParams.has("name")) {
|
api.map((server) =>
|
||||||
if (concurrent) {
|
fetch(server + search, {
|
||||||
res = await Promise.any(
|
method: "GET",
|
||||||
api.map((server) =>
|
headers: {
|
||||||
fetch(server + search, {
|
"User-Agent":
|
||||||
method: "GET",
|
"Dohna-NS (https://github.com/LittleChest/Dohna-NS)",
|
||||||
headers: {
|
},
|
||||||
"User-Agent":
|
}).then((res) => {
|
||||||
"Dohna-NS (https://github.com/LittleChest/Dohna-NS)",
|
if (res.status !== 200) {
|
||||||
},
|
throw new Error(
|
||||||
}).then((res) => {
|
`Failed to connect to ${server}: ${res.status} ${res.statusText}`,
|
||||||
if (res.status !== 200) {
|
);
|
||||||
throw new Error(
|
}
|
||||||
`Failed to connect to ${server}: ${res.status} ${res.statusText}`,
|
return res;
|
||||||
);
|
}),
|
||||||
}
|
),
|
||||||
return res;
|
);
|
||||||
}),
|
} else {
|
||||||
),
|
const servers = [...api];
|
||||||
);
|
while (servers.length > 0) {
|
||||||
} else {
|
const index = Math.floor(Math.random() * servers.length);
|
||||||
const servers = [...api];
|
const server = servers.splice(index, 1)[0];
|
||||||
while (servers.length > 0) {
|
try {
|
||||||
const index = Math.floor(Math.random() * servers.length);
|
res = await fetch(server + search, {
|
||||||
const server = servers.splice(index, 1)[0];
|
method: "GET",
|
||||||
try {
|
headers: {
|
||||||
res = await fetch(server + search, {
|
"User-Agent":
|
||||||
method: "GET",
|
"Dohna-NS (https://github.com/LittleChest/Dohna-NS)",
|
||||||
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}`);
|
||||||
if (res.status === 200) break;
|
continue;
|
||||||
} 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
|
||||||
if (pathname === "/dns-query") {
|
let queryData;
|
||||||
res = new Response(null, { status: 400 });
|
|
||||||
|
|
||||||
let queryData;
|
// GET
|
||||||
|
if (method === "GET" && searchParams.has("dns")) {
|
||||||
// GET
|
// Decode the base64-encoded DNS query
|
||||||
if (method === "GET" && searchParams.has("dns")) {
|
try {
|
||||||
// Decode the base64-encoded DNS query
|
const decodedQuery = atob(searchParams.get("dns"));
|
||||||
try {
|
queryData = new Uint8Array(decodedQuery.length);
|
||||||
const decodedQuery = atob(searchParams.get("dns"));
|
for (let i = 0; i < decodedQuery.length; i++) {
|
||||||
queryData = new Uint8Array(decodedQuery.length);
|
queryData[i] = decodedQuery.charCodeAt(i);
|
||||||
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);
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
36
handler/entrypoint.js
Normal file
36
handler/entrypoint.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
61
handler/mobileconfig.js
Normal file
61
handler/mobileconfig.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import handler from "./common";
|
import handler from "./handler/dns";
|
||||||
|
|
||||||
export default middleware = async (request) => {
|
export default middleware = async (request) => {
|
||||||
return handler(
|
return handler(
|
||||||
@ -8,5 +8,7 @@ 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,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import handler from "../../common.js";
|
import handler from "../../handler/dns.js";
|
||||||
export default async (request) =>
|
export default async (request) =>
|
||||||
handler(
|
handler(
|
||||||
request,
|
request,
|
||||||
@ -8,5 +8,6 @@ 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: "*" };
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user