用 Cloudflare Workers 搭一个带自动 ECS 的 DoH 代理

用 Cloudflare Workers 搭一个带自动 ECS 的 DoH 代理

为什么要搞这个东西

之前一直在折腾自建 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 解析结果离用户更近。

实现上不算复杂,但有几个地方值得注意:

  1. ECS 的网段要截断 —— 全 IP 传上去既没必要也泄露隐私
  2. 备用上游必须支持 ECS —— 否则切过去就白搭
  3. 缓存键要带 ECS —— 不同网段的解析结果可能不一样
  4. 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 }));
    }
}