为什么要搞这个东西
之前一直在折腾自建 DNS,把家里设备的解析都收敛到自己的服务器上。但有个问题一直没解决:解析出来的 CDN 节点总是离我很远。
原因不难理解。假设我的 DNS 服务器在 A 地,我在 B 地上网。查询发出去的时候,上游 DNS 看到的是「A 地的服务器在问我」,于是返回了一个离 A 地最近的 CDN 节点。可我人在 B 地,这个节点对我来说绕了一大圈。
解决思路叫 ECS(EDNS Client Subnet):查询的时候顺便告诉上游「我实际在哪个网段」,上游就会按这个网段返回就近的结果。
Cloudflare Workers 是个很合适的载体——全球节点、免费额度够用、部署简单。于是就写了这个 DoH 代理,核心功能就一个:自动把客户端的真实 IP 塞进 DNS 查询里。
整体是怎么工作的
客户端 ──DoH──> Cloudflare Worker ──DoH(带ECS)──> 上游 DNS
│
├─ 提取客户端真实 IP
├─ 截断成网段(IPv4 /24,IPv6 /56)
├─ 注入到 DNS 查询报文的 ECS 字段
├─ 查缓存(命中直接返回)
└─ 转发上游,结果写缓存
客户端连过来的时候,Cloudflare 会在请求头里带上真实的客户端 IP。Worker 拿到这个 IP,截断成网段,编码成 ECS 格式,塞进 DNS 查询报文,再转发给上游。
整个过程对客户端是透明的——它只是调了一个普通的 DoH 接口。
参数逐个说
代码开头是配置区,这些参数决定了它怎么跑。
缓存相关
const CACHE_ENABLED = true;
是否启用缓存。用的是 Cloudflare 的 Cache API,按边缘节点缓存,不同地区的节点各存各的。
const CACHE_MIN_TTL = 3600;
const CACHE_MAX_TTL = 21600;
缓存时间的上下限(秒)。上游返回的 TTL 如果小于下限,就按下限存;大于上限,就按上限存;在中间就按上游的值。
下限 1 小时是为了减少回源,上限 6 小时是防止缓存太旧——尤其是 CDN 类域名,TTL 设太大可能返回过时的 IP。
const TTL_JITTER_RATIO = 0.1;
TTL 抖动比例。给缓存时间加一个 ±10% 的随机偏移。
这个参数看着不起眼,但挺有用:如果一批缓存条目同时写入,它们的过期时间也一样,到时候会同时失效,一瞬间全涌向上游,容易打出尖峰。加抖动就是把它们的过期时间错开。
上游服务器
const URL_UPSTREAM_DNS_QUERY = 'https://上游地址';
const URL_UPSTREAM_RESOLVE = 'https://dns.google/resolve';
两个上游地址,对应两种查询格式:
URL_UPSTREAM_DNS_QUERY是标准的 DoH 端点(RFC 8484 二进制格式)URL_UPSTREAM_RESOLVE是 Google 的 JSON API 格式
这俩可以用不同的服务商,我这里是分开的。
const URL_UPSTREAM_DNS_QUERY_BACKUP = 'https://备用地址';
const URL_UPSTREAM_RESOLVE_BACKUP = 'https://备用地址';
备用上游。主上游超时或者挂了会自动切过来。
这里有个坑:备用上游必须支持 ECS,否则切过去以后就近解析就失效了。Cloudflare 官方的 DoH 对 ECS 支持不稳定,所以没用它做备用。
const UPSTREAM_TIMEOUT_MS = 3000;
上游超时时间,3 秒。超了就切备用。
路径和跨域
const REQ_QUERY_PATHNAME = '/dns-query'; // 可自定义
const REQ_RESOLVE_PATHNAME = '/resolve';
查询路径。标准是 /dns-query,也可以改成自定义路径(改后客户端要同步调整)。
改路径主要是为了防嗅探。有些网络环境会对标准的 DNS 路径做识别和干扰,换一个不显眼的路径能绕过一部分。代价是客户端配置要跟着改。
const CORS_HEADERS = { ... };
跨域头,允许浏览器直接调用这个接口。如果你打算在网页里用 JS 发 DNS 查询,这个就派上用场了。
几个实现上的细节
超时和故障切换
async function fetchWithFallback(url, options, backupUrl, timeoutMs) { ... }
这个函数负责「主上游不行就换备用」。判断逻辑是这样的:
- 超时、网络错误、5xx → 切备用
- 4xx → 直接返回,不切
4xx 是请求本身有问题(比如格式错了),换服务器也没用,所以不浪费时间。
客户端 IP 的截断
function getECSData(request) { ... }
从请求头拿到客户端 IP 后,不能把完整 IP 传给上游——那等于把用户的精确地址暴露了。所以要做截断:
- IPv4 截成 /24(前三段,最后一段归零)
- IPv6 截成 /56
这样既能保证地理定位准确,又不会泄露具体地址。
DNS 报文的手工构造
这部分是整个代码里最麻烦的。要往 DNS 查询里塞 ECS,得直接操作二进制:
function modifyDNSQuery(originalArrayBuffer, ecsData) { ... }
流程是:跳过问题区段 → 找到 OPT 记录(EDNS 的容器)→ 看里面有没有 ECS
- 已经有 ECS:不动它
- 有 OPT 但没 ECS:追加到 OPT 的末尾
- 连 OPT 都没有:新建一个 OPT 记录
有个细节值得一提:如果客户端自己带了 ECS,就不覆盖。因为有些客户端(比如某些路由器)会自己算好 ECS,这时候尊重它的选择更合适。
缓存的 TTL 提取
function getMinTTL(response) { ... }
缓存多久不能拍脑袋,得看响应里各条记录的 TTL,取最小的那个。
分两种情况:
- 有回答记录:取 answer 区段里最小的 TTL
- 没有回答记录(比如查不存在的域名):取 SOA 记录的 TTL
解析失败的话就返回 0,表示不缓存——宁可不缓存,也不能缓存一个错误的响应。
缓存键
function getCacheKey(qname, qtype, ecsData) { ... }
缓存键里包含了 ECS 信息,这点很关键。同一个域名,不同的客户端网段可能拿到不同的解析结果,如果共用一个缓存键就乱套了。
支持的查询方式
| 方式 | 路径 | 说明 |
|---|---|---|
| POST | /dns-query |
标准 DoH,二进制格式 |
| GET | /dns-query?dns=... |
标准 DoH,base64url 编码 |
| GET | /resolve?name=... |
Google JSON 格式 |
三种都带 ECS 注入。POST 方式对请求体大小做了限制(4096 字节),防止被滥用。
现在的状态
这个 Worker 已经部署在 Cloudflare 上跑了一段时间,目前:
- 作为自建 DNS 的上游在使用 —— 家里的 mosdns 会把境外域名转发到这里
- 缓存生效 —— 大部分查询命中缓存,回源压力不大
- ECS 工作正常 —— 解析出来的 CDN 节点基本都是就近的
- 故障切换没触发过 —— 主上游一直稳定,备用还没用上
代码本身没再改动,算是个稳定运行的版本。
小结
这个东西解决的核心问题就一个:让 DNS 解析结果离用户更近。
实现上不算复杂,但有几个地方值得注意:
- ECS 的网段要截断 —— 全 IP 传上去既没必要也泄露隐私
- 备用上游必须支持 ECS —— 否则切过去就白搭
- 缓存键要带 ECS —— 不同网段的解析结果可能不一样
- TTL 抖动别省 —— 不加的话容易在过期时打出回源尖峰
代码量不大,Cloudflare 免费额度也够个人用。如果你也在自建 DNS,又遇到 CDN 节点绕远的问题,这套思路可以直接拿来用。
完整代码
代码不长,贴在这里方便对照。不想复制的话也可以直接下载:
展开即可查看:
点击展开完整代码(约 600 行)
//// ===== 配置区 ===== ////
// 是否启用响应缓存(Cloudflare Cache API,按边缘节点缓存)
const CACHE_ENABLED = true;
// 缓存 TTL 上下限(秒)
// 上游返回的 TTL < CACHE_MIN_TTL → 按 CACHE_MIN_TTL 缓存
// 上游返回的 TTL > CACHE_MAX_TTL → 按 CACHE_MAX_TTL 缓存
// 在两者之间 → 按上游返回的 TTL 缓存
// 注意:MAX 设大些可以减少上游查询,但 CDN 类域名可能返回过时 IP
const CACHE_MIN_TTL = 3600;
const CACHE_MAX_TTL = 21600;
// TTL 抖动比例(0~1)。为缓存 TTL 添加 ±ratio 的随机抖动,
// 避免大量缓存条目同时过期导致回源请求尖峰(cache stampede)。
// 设为 0 可关闭抖动。
const TTL_JITTER_RATIO = 0.1;
//// ===== 上游 DoH 服务器 ===== ////
// 这两个地址是转发 DNS 查询的目标服务器
// URL_UPSTREAM_DNS_QUERY:RFC 8484 二进制格式的 DoH 查询端点(POST/GET /dns-query)
// URL_UPSTREAM_RESOLVE :Google JSON API 格式的查询端点(GET /resolve?name=...)
// 注意:/dns-query 和 /resolve 可以使用不同的服务商
// AdGuard 的 /resolve 端点从 Worker 访问时会返回 HTML 页面,因此 /resolve 改用 Google
// 可替换为其他 DoH 服务商:
// Cloudflare: https://cloudflare-dns.com/dns-query / https://cloudflare-dns.com/resolve
// Google: https://dns.google/dns-query / https://dns.google/resolve
// AliDNS: https://dns.alidns.com/dns-query / https://dns.alidns.com/resolve
const URL_UPSTREAM_DNS_QUERY = 'https://dns.nextdns.io/YOUR_ID';
const URL_UPSTREAM_RESOLVE = 'https://dns.google/resolve';
// 备用上游(主上游超时/失败时自动切换)
// 注意:/dns-query 备用必须支持 ECS,否则地理解析失效。
// Google DoH 支持 ECS;Cloudflare 官方 DoH 对 ECS 支持不稳定,不推荐作备用。
const URL_UPSTREAM_DNS_QUERY_BACKUP = 'https://dns.google/dns-query';
// /resolve 备用(Google JSON 兼容格式)
const URL_UPSTREAM_RESOLVE_BACKUP = 'https://dns.alidns.com/resolve';
// 上游请求超时时间(毫秒),超时自动切换备用上游
const UPSTREAM_TIMEOUT_MS = 3000;
//// ===== 常量定义 ===== ////
const APPL_DNS_MSG = 'application/dns-message';
const APPL_DNS_JSON = 'application/dns-json';
// 查询路径名(标准 DoH 端点)
// RFC 8484 二进制格式: /dns-query
// Google JSON API 格式: /resolve
// 如需规避嗅探,可改为自定义路径(如 '/sdns'、'/my-dns' 等),
// 改后客户端配置需同步修改
const REQ_QUERY_PATHNAME = '/dns-query';
const REQ_RESOLVE_PATHNAME = '/resolve';
// CORS 跨域响应头,允许浏览器端直接调用此 DoH 端点
const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Accept',
'Access-Control-Max-Age': '86400',
};
//// ===== 入口点 ===== ////
export default {
async fetch(r, env, ctx) {
return handleRequest(r);
},
};
//// ===== 公共工具函数 ===== ////
// 带超时 + 故障回退的上游请求
// 主上游超时/网络错误/5xx 时自动切换到备用上游
// 4xx 客户端错误属于请求本身问题,直接返回不切换
async function fetchWithFallback(url, options, backupUrl, timeoutMs = UPSTREAM_TIMEOUT_MS) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const resp = await fetch(url, { ...options, signal: controller.signal });
clearTimeout(timer);
if (resp.ok) return resp;
if (resp.status >= 400 && resp.status < 500) return resp;
} catch {
clearTimeout(timer);
}
// 主上游不可用,改用备用上游
const backupController = new AbortController();
const backupTimer = setTimeout(() => backupController.abort(), timeoutMs);
try {
return await fetch(backupUrl, { ...options, signal: backupController.signal });
} finally {
clearTimeout(backupTimer);
}
}
// 将 IPv6 地址截断为 /56 前缀
// /56 = 56位 = 第0-2段完整(48位)+ 第3段高字节(8位)
// 第3段低字节和第4-7段全部置零
function truncateIPv6To56(ipv6) {
let parts = ipv6.split(':');
const hasEmpty = parts.some(p => p === '');
if (hasEmpty) {
const nonEmpty = parts.filter(p => p !== '');
const fillCount = 8 - nonEmpty.length;
const fillArr = new Array(fillCount).fill('0000');
if (parts.length >= 2 && parts[0] === '' && parts[1] === '') {
parts = [...fillArr, ...parts.slice(2).filter(p => p !== '')];
} else if (parts.length >= 2 && parts[parts.length - 1] === '' && parts[parts.length - 2] === '') {
parts = [...parts.slice(0, -2).filter(p => p !== ''), ...fillArr];
} else {
const idx = parts.indexOf('');
parts = [...parts.slice(0, idx), ...fillArr, ...parts.slice(idx + 1)];
}
}
let segments = parts.map(seg => seg.padStart(4, '0'));
segments[3] = segments[3].slice(0, 2) + '00';
return segments.slice(0, 4).join(':') + ':0000:0000:0000:0000';
}
// 从请求头提取客户端 IP,生成 ECS(EDNS Client Subnet)数据
// IPv4 截断为 /24,IPv6 截断为 /56,用于让上游 DNS 返回就近的 CDN 节点
function getECSData(request) {
let ip = request.headers.get("CF-Connecting-IP");
if (!ip) return null;
if (ip.includes(":")) {
return { family: 2, subnet: truncateIPv6To56(ip), prefix: 56 };
} else {
return { family: 1, subnet: ip.split(".").slice(0, 3).join(".") + ".0", prefix: 24 };
}
}
// 将 IP 地址和前缀长度编码为 EDNS Client Subnet (ECS) 二进制缓冲区
function encodeECStoBuffer(family, subnet, prefixLength) {
let addressBytes;
if (family === 2) {
addressBytes = subnet.split(':').flatMap(part => part.match(/../g).map(b => parseInt(b, 16)));
} else {
addressBytes = subnet.split('.').map(n => parseInt(n, 10));
}
if (!addressBytes || addressBytes.length === 0) throw new Error('Invalid IP address');
addressBytes = addressBytes.slice(0, prefixLength / 8);
let ecsLength = 8 + addressBytes.length;
let ecsBuffer = new Uint8Array(ecsLength);
ecsBuffer.set([
0x00, 0x08,
(ecsLength - 4) >> 8, (ecsLength - 4) & 0xff,
(family >> 8) & 0xff, family & 0xff,
prefixLength, 0x00,
...addressBytes
]);
return ecsBuffer;
}
// 编码为 Base64Url 格式(分块处理,避免大缓冲区栈溢出)
function encodeBase64Url(data) {
let binary = '';
const chunkSize = 0x8000;
for (let i = 0; i < data.length; i += chunkSize) {
binary += String.fromCharCode(...data.subarray(i, i + chunkSize));
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// 从 Base64Url 格式解码
function decodeBase64Url(base64Url) {
try {
const base64 = base64Url
.replace(/-/g, '+').replace(/_/g, '/')
.padEnd(base64Url.length + (4 - (base64Url.length % 4)) % 4, '=');
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes;
} catch {
return new Uint8Array(0);
}
}
// 跳过 DNS 名称(标签),返回名称结束后的偏移量
// 支持压缩指针,带边界检查防止畸形报文导致无限循环
function skipName(buf, offset) {
while (offset < buf.length) {
const len = buf[offset];
if (len === 0) return offset + 1;
if ((len & 0xc0) === 0xc0) return offset + 2;
if ((len & 0xc0) !== 0) throw new Error('Invalid DNS label');
offset += 1 + len;
}
throw new Error('Malformed DNS name: no terminator');
}
// 将请求头键名统一转为小写
function normalizeHeaders(requestHeaders) {
const headers = new Headers();
for (const [key, value] of requestHeaders) {
headers.set(key.toLowerCase(), value);
}
return headers;
}
// 检查头值是否包含指定的媒体类型
// 兼容参数(如 "application/dns-message; charset=utf-8")和多值 Accept 头
function headerMatches(headers, headerName, expected) {
const value = headers.get(headerName);
if (!value) return false;
return value.split(',').some(part => part.trim().split(';')[0].trim() === expected);
}
// 为响应添加 CORS 跨域头
function withCORS(response) {
const newHeaders = new Headers(response.headers);
for (const [key, value] of Object.entries(CORS_HEADERS)) {
newHeaders.set(key, value);
}
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
}
//// ===== DNS 报文解析(用于缓存 TTL 提取)===== ////
/**
* 读取一个(可能含压缩指针的)DNS 名称
* @returns {{ name: string, endOffset: number }} 名称字符串和结束偏移量
*/
function readName(buf, offset) {
const b = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
let labels = [];
let pos = offset;
let endPos = 0;
let jumped = false;
let safety = 0;
while (pos < b.length && safety++ < 255) {
const len = b[pos];
if (len === 0) {
if (!jumped) endPos = pos + 1;
break;
}
if ((len & 0xc0) === 0xc0) {
if (pos + 1 >= b.length) throw new Error('Truncated name pointer');
if (!jumped) endPos = pos + 2;
pos = ((len & 0x3f) << 8) | b[pos + 1];
jumped = true;
continue;
}
if ((len & 0xc0) !== 0) throw new Error('Invalid label flags');
if (pos + 1 + len > b.length) throw new Error('Truncated label');
labels.push(new TextDecoder().decode(b.subarray(pos + 1, pos + 1 + len)));
pos += 1 + len;
}
if (safety > 255) throw new Error('Name compression loop');
return { name: labels.join('.'), endOffset: endPos || pos };
}
/**
* 解析 DNS 报文头部和各区段的记录数量(用于提取 TTL)
* @param {ArrayBuffer|Uint8Array} buf - DNS 报文二进制数据
*/
function parseDNSMessage(buf) {
const b = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
if (b.length < 12) throw new Error('DNS message too short');
const ancount = (b[6] << 8) | b[7];
const nscount = (b[8] << 8) | b[9];
const qdcount = (b[4] << 8) | b[5];
let offset = 12;
// 跳过问题区段
for (let i = 0; i < qdcount; i++) { offset = skipName(b, offset); offset += 4; }
// 解析资源记录以提取 TTL
function parseRRs(count) {
const records = [];
for (let i = 0; i < count; i++) {
const { endOffset } = readName(b, offset);
offset = endOffset;
const rtype = (b[offset] << 8) | b[offset + 1];
const ttl = ((b[offset + 4] << 24) | (b[offset + 5] << 16) | (b[offset + 6] << 8) | b[offset + 7]) >>> 0;
const rdlength = (b[offset + 8] << 8) | b[offset + 9];
records.push({ rtype, ttl });
offset += 10 + rdlength;
}
return records;
}
return {
ancount, nscount,
answers: parseRRs(ancount),
authority: parseRRs(nscount),
raw: b
};
}
// 取响应中的最小 TTL(正向取回答区段,负向取权威区段的 SOA)
function getMinTTL(response) {
try {
const parsed = parseDNSMessage(response);
let minTTL;
if (parsed.ancount > 0) {
// 有回答记录:取 answer 中的最小 TTL
minTTL = 3600;
for (const a of parsed.answers) { if (a.ttl < minTTL) minTTL = a.ttl; }
} else {
// 无回答记录:取 SOA TTL,无 SOA 则用 60 秒
minTTL = -1;
for (const a of parsed.authority) {
if (a.rtype === 6 && (minTTL < 0 || a.ttl < minTTL)) minTTL = a.ttl;
}
if (minTTL < 0) minTTL = 60;
}
const clamped = Math.min(Math.max(minTTL, CACHE_MIN_TTL), CACHE_MAX_TTL);
// TTL 抖动:±TTL_JITTER_RATIO 随机偏移,错开各缓存条目的过期时间
if (TTL_JITTER_RATIO > 0 && clamped > CACHE_MIN_TTL) {
const range = Math.max(1, Math.floor(clamped * TTL_JITTER_RATIO));
return clamped + Math.floor(Math.random() * (2 * range + 1)) - range;
}
return clamped;
} catch { return 0; /* 解析失败,不缓存 */ }
}
//// ===== ECS 注入 ===== ////
// 修改原始 DNS 查询报文,注入 ECS(EDNS Client Subnet)选项
// 如果报文已包含 ECS 则不修改
function modifyDNSQuery(originalArrayBuffer, ecsData) {
if (!originalArrayBuffer || originalArrayBuffer.byteLength < 12) {
throw new Error('Invalid DNS query data');
}
const OPT_TYPE = 41, ECS_OPTION_CODE = 0x08;
let b = new Uint8Array(originalArrayBuffer);
let qdcount = (b[4] << 8) | b[5];
let arcount = (b[10] << 8) | b[11];
let offset = 12;
for (let i = 0; i < qdcount; i++) { offset = skipName(b, offset); offset += 4; }
let addOffset = offset, hasOPT = false, hasECS = false;
for (let i = 0; i < arcount; i++) {
let pos = skipName(b, addOffset);
let type = (b[pos] << 8) | b[pos + 1];
if (type === OPT_TYPE) {
hasOPT = true;
let rdlen = (b[pos + 8] << 8) | b[pos + 9];
let rdataEnd = pos + 10 + rdlen, optPos = pos + 10;
while (optPos + 4 <= rdataEnd) {
if (((b[optPos] << 8) | b[optPos + 1]) === ECS_OPTION_CODE) { hasECS = true; break; }
optPos += 4 + ((b[optPos + 2] << 8) | b[optPos + 3]);
}
break;
}
addOffset = pos + 10 + ((b[pos + 8] << 8) | b[pos + 9]);
}
if (hasECS) return b;
let ecsBuf = encodeECStoBuffer(ecsData.family, ecsData.subnet, ecsData.prefix);
let nb;
if (hasOPT) {
// 已有 OPT 记录:追加 ECS 到 RDATA 末尾,更新 RDLENGTH
let origRdLen = (b[addOffset + 9] << 8) | b[addOffset + 10];
let newRdLen = origRdLen + ecsBuf.length;
nb = new Uint8Array(b.length + ecsBuf.length);
nb.set(b.subarray(0, addOffset + 11), 0);
nb[addOffset + 9] = (newRdLen >> 8) & 0xff; nb[addOffset + 10] = newRdLen & 0xff;
nb.set(b.subarray(addOffset + 11), addOffset + 11);
nb.set(ecsBuf, addOffset + 11 + origRdLen);
} else {
// 无 OPT 记录:新建 OPT 记录并追加 ECS
let oh = new Uint8Array([0, 0x00, 0x29, 0x10, 0x00, 0, 0, 0, 0, ecsBuf.length >> 8, ecsBuf.length & 0xff]);
nb = new Uint8Array(b.length + oh.length + ecsBuf.length);
nb.set(b, 0); nb.set(oh, b.length); nb.set(ecsBuf, b.length + oh.length);
nb[10] = (arcount + 1) >> 8; nb[11] = (arcount + 1) & 0xff;
}
return nb;
}
//// ===== 缓存(Cloudflare Cache API)===== ////
// 生成缓存键
function getCacheKey(qname, qtype, ecsData) {
const ecs = ecsData ? `${ecsData.family}/${ecsData.subnet}/${ecsData.prefix}` : 'no-ecs';
return `https://dns-cache.internal/${qname.toLowerCase()}/${qtype}/${ecs}`;
}
// 从缓存中读取 DNS 响应
async function getFromCache(qname, qtype, ecsData) {
const cache = caches.default;
const key = new Request(getCacheKey(qname, qtype, ecsData));
const cached = await cache.match(key);
if (!cached) return null;
return new Uint8Array(await cached.arrayBuffer());
}
// 将 DNS 响应写入缓存,TTL 来自响应中的最小 TTL
async function putToCache(qname, qtype, response, ttl, ecsData) {
const cache = caches.default;
const key = new Request(getCacheKey(qname, qtype, ecsData));
await cache.put(key, new Response(response, {
headers: {
'Content-Type': 'application/octet-stream',
'Cache-Control': `public, max-age=${ttl}`
}
}));
}
// 从原始查询报文中提取问题区段的域名和类型
function getQueryInfo(queryBuffer) {
const b = new Uint8Array(queryBuffer);
if (b.length < 12) return null;
const qdcount = (b[4] << 8) | b[5];
if (qdcount === 0) return null;
const { name, endOffset } = readName(b, 12);
const qtype = (b[endOffset] << 8) | b[endOffset + 1];
return { name, qtype };
}
//// ===== DoH 查询处理函数 ===== ////
// POST 方式的 DoH 查询(RFC 8484 二进制格式,带 ECS 注入和缓存)
async function dns_query_post(request) {
const contentLength = parseInt(request.headers.get('Content-Length') || '0', 10);
if (contentLength > 4096) {
return new Response('Payload Too Large', { status: 413 });
}
let requestBody = await request.arrayBuffer();
const ecsData = getECSData(request);
// 查询缓存
if (CACHE_ENABLED) {
const queryInfo = getQueryInfo(requestBody);
if (queryInfo) {
const cached = await getFromCache(queryInfo.name, queryInfo.qtype, ecsData);
if (cached) {
// 用原始查询的事务 ID 替换缓存响应中的 ID
const originalId = (new Uint8Array(requestBody)[0] << 8) | new Uint8Array(requestBody)[1];
const result = new Uint8Array(cached);
result[0] = (originalId >> 8) & 0xff;
result[1] = originalId & 0xff;
return new Response(result, { headers: { 'Content-Type': APPL_DNS_MSG } });
}
}
}
// 注入 ECS 后转发到上游(带超时 + 故障回退)
if (ecsData) requestBody = modifyDNSQuery(requestBody, ecsData).buffer;
const upstreamResp = await fetchWithFallback(
URL_UPSTREAM_DNS_QUERY,
{ method: "POST", headers: { "content-type": APPL_DNS_MSG }, body: requestBody },
URL_UPSTREAM_DNS_QUERY_BACKUP
);
// 缓存响应
if (CACHE_ENABLED && upstreamResp.ok) {
try {
const queryInfo = getQueryInfo(new Uint8Array(requestBody));
if (queryInfo) {
const respBuffer = await upstreamResp.arrayBuffer();
const ttl = getMinTTL(respBuffer);
if (ttl > 0) {
try { await putToCache(queryInfo.name, queryInfo.qtype, respBuffer, ttl, ecsData); }
catch { /* 缓存写入失败 */ }
}
return new Response(respBuffer, { headers: { 'Content-Type': APPL_DNS_MSG } });
}
} catch { /* 解析失败,透传原始响应 */ }
}
return upstreamResp;
}
// GET 方式的 DoH 查询(RFC 8484 base64url 格式,带 ECS 注入和缓存)
async function dns_query_get(request) {
const params = new URL(request.url).searchParams;
const ecsData = getECSData(request);
// 查询缓存
if (CACHE_ENABLED) {
const dnsParam = params.get("dns");
if (dnsParam) {
const queryBuffer = decodeBase64Url(dnsParam);
const queryInfo = getQueryInfo(queryBuffer);
if (queryInfo) {
const cached = await getFromCache(queryInfo.name, queryInfo.qtype, ecsData);
if (cached) {
const originalId = (queryBuffer[0] << 8) | queryBuffer[1];
const result = new Uint8Array(cached);
result[0] = (originalId >> 8) & 0xff;
result[1] = originalId & 0xff;
return new Response(result, { headers: { 'Content-Type': APPL_DNS_MSG } });
}
}
}
}
// 注入 ECS
if (ecsData) {
let origBuffer = decodeBase64Url(params.get("dns"));
let newBuffer = modifyDNSQuery(origBuffer, ecsData);
params.set("dns", encodeBase64Url(newBuffer));
}
const url = `${URL_UPSTREAM_DNS_QUERY}?${params.toString()}`;
const backupUrl = `${URL_UPSTREAM_DNS_QUERY_BACKUP}?${params.toString()}`;
const upstreamResp = await fetchWithFallback(
url,
{ method: "GET", headers: { "accept": APPL_DNS_MSG } },
backupUrl
);
// 缓存响应
if (CACHE_ENABLED && upstreamResp.ok) {
try {
const dnsParam = params.get("dns");
if (dnsParam) {
const queryBuffer = decodeBase64Url(dnsParam);
const queryInfo = getQueryInfo(queryBuffer);
if (queryInfo) {
const respBuffer = await upstreamResp.arrayBuffer();
const ttl = getMinTTL(respBuffer);
if (ttl > 0) {
try { await putToCache(queryInfo.name, queryInfo.qtype, respBuffer, ttl, ecsData); }
catch { /* 缓存写入失败 */ }
}
return new Response(respBuffer, { headers: { 'Content-Type': APPL_DNS_MSG } });
}
}
} catch { /* 解析失败,透传原始响应 */ }
}
return upstreamResp;
}
// Google JSON API 查询(/resolve?name=...,带 ECS 注入)
async function dns_resolve_googlejson(request) {
const params = new URL(request.url).searchParams;
if (!params.has("edns_client_subnet")) {
let ecsData = getECSData(request);
if (ecsData) params.set("edns_client_subnet", `${ecsData.subnet}/${ecsData.prefix}`);
}
const url = `${URL_UPSTREAM_RESOLVE}?${params.toString()}`;
const backupUrl = `${URL_UPSTREAM_RESOLVE_BACKUP}?${params.toString()}`;
try {
const resp = await fetchWithFallback(
url,
{ method: "GET", headers: { "accept": APPL_DNS_JSON } },
backupUrl
);
if (!resp.ok) {
return new Response(JSON.stringify({ Status: 2 }), {
status: 502,
headers: { 'Content-Type': APPL_DNS_JSON }
});
}
return resp;
} catch {
return new Response(JSON.stringify({ Status: 2 }), {
status: 502,
headers: { 'Content-Type': APPL_DNS_JSON }
});
}
}
//// ===== 路由分发 ===== ////
// 根据请求方法、路径、请求头和查询参数路由到对应的处理函数
function routeRequest(method, pathname, headers, searchParams, request) {
// CORS 预检请求
if (method === 'OPTIONS') {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}
if (method === 'POST' && pathname === REQ_QUERY_PATHNAME && headerMatches(headers, 'content-type', APPL_DNS_MSG)) {
return dns_query_post(request);
} else if (method === 'GET' && pathname === REQ_RESOLVE_PATHNAME && searchParams.get('name')) {
return dns_resolve_googlejson(request);
} else if (method === 'GET' && pathname === REQ_QUERY_PATHNAME && searchParams.has('dns')) {
return dns_query_get(request);
} else {
return new Response('Not Found', { status: 404 });
}
}
// 请求处理入口:解析请求 → 路由 → 添加 CORS → 返回
async function handleRequest(request) {
try {
const headers = normalizeHeaders(request.headers);
const { method, url } = request;
const { searchParams, pathname } = new URL(url);
const response = await routeRequest(method, pathname, headers, searchParams, request);
return withCORS(response);
} catch (err) {
return withCORS(new Response('Bad Gateway', { status: 502 }));
}
}