Cloudflare 自建 DoH 服务器
最近在 L 站上看到好几个佬友因为翻墙被叔叔请去喝茶了,感觉还是比较慌,毕竟年底了,大家都在刷 KPI。为了尽可能地降低风险,简单学习了一下可能泄露个人信息的环节,打算从 DNS 泄露这里入手。 因为人在墙内,所有流量肯定都会经过运营商,遭到审查是在所难免的。在进行 DNS 查询的时候,如果被发现查询了 google 等被墙的站点,那毫无疑问就会判定存在翻墙行为。为了避免 DNS 查询泄露,一种简单方式就是启用 DoH (DNS over Https),即将 DNS 查询过程封装在 Https 流量中,避免中间人对 DNS 数据进行窃听。 DoH 服务器有很多,但最稳妥的方式还是自建一个,利用 Cloudflare 的 worker 即可几乎零成本部署。 在 Cloudflare 中新建一个 worker,复制以下代码到 worker 中: let dohServer = "cloudflare-dns.com"; let dohPath = 'dns-query'; export default { async fetch(request, env) { const jsonDoH = `https://${dohServer}/resolve`; const dnsDoH = `https://${dohServer}/dns-query`; const url = new URL(request.url); const path = url.pathname; // 处理OPTIONS预检请求 if (request.method === 'OPTIONS') { return new Response(null, { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': '*', 'Access-Control-Max-Age': '86400' } }); } // DoH服务器处理 if (path === `/${dohPath}`) { return await handleDoHRequest(request, dnsDoH, jsonDoH); } // DNS查询参数处理 if (url.searchParams.has("doh")) { return await handleDnsQuery(url, dnsDoH); } // 默认返回nginx欢迎页 return new Response(getNginxPage(), { headers: { 'Content-Type': 'text/html; charset=UTF-8', }, }); } } // 处理DoH请求 async function handleDoHRequest(request, dnsDoH, jsonDoH) { const { method, headers, body } = request; const userAgent = headers.get('User-Agent') || 'DoH Client'; const url = new URL(request.url); const { searchParams } = url; try { if (method === 'GET' && !url.search) { return new Response('Bad Request', { status: 400, headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Access-Control-Allow-Origin': '*' } }); } let response; if (method === 'GET' && searchParams.has('name')) { const searchQuery = searchParams.has('type') ? url.search : url.search + '&type=A'; response = await fetch(dnsDoH + searchQuery, { headers: { 'Accept': 'application/dns-json', 'User-Agent': userAgent } }); if (!response.ok) { response = await fetch(jsonDoH + searchQuery, { headers: { 'Accept': 'application/dns-json', 'User-Agent': userAgent } }); } } else if (method === 'GET') { response = await fetch(dnsDoH + url.search, { headers: { 'Accept': 'application/dns-message', 'User-Agent': userAgent } }); } else if (method === 'POST') { response = await fetch(dnsDoH, { method: 'POST', headers: { 'Accept': 'application/dns-message', 'Content-Type': 'application/dns-message', 'User-Agent': userAgent }, body: body }); } else { return new Response('Unsupported request format', { status: 400, headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Access-Control-Allow-Origin': '*' } }); } if (!response.ok) { const errorText = await response.text(); throw new Error(`DoH returned error (${response.status}): ${errorText.substring(0, 200)}`); } const responseHeaders = new Headers(response.headers); responseHeaders.set('Access-Control-Allow-Origin', '*'); responseHeaders.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); responseHeaders.set('Access-Control-Allow-Headers', '*'); if (method === 'GET' && searchParams.has('name')) { responseHeaders.set('Content-Type', 'application/json'); } return new Response(response.body, { status: response.status, statusText: response.statusText, headers: responseHeaders }); } catch (error) { console.error("DoH request error:", error); return new Response(JSON.stringify({ error: `DoH request error: ${error.message}`, stack: error.stack }, null, 4), { status: 500, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } }); } } // 处理DNS查询请求 async function handleDnsQuery(url, dnsDoH) { const domain = url.searchParams.get("domain") || url.searchParams.get("name") || "www.google.com"; const dohUrl = url.searchParams.get("doh") || dnsDoH; const type = url.searchParams.get("type") || "all"; if (dohUrl.includes(url.host)) { return await handleLocalDohRequest(domain, type, dnsDoH); } try { if (type === "all") { const ipv4Result = await queryDns(dohUrl, domain, "A"); const ipv6Result = await queryDns(dohUrl, domain, "AAAA"); const nsResult = await queryDns(dohUrl, domain, "NS"); const combinedResult = { Status: ipv4Result.Status || ipv6Result.Status || nsResult.Status, TC: ipv4Result.TC || ipv6Result.TC || nsResult.TC, RD: ipv4Result.RD || ipv6Result.RD || nsResult.RD, RA: ipv4Result.RA || ipv6Result.RA || nsResult.RA, AD: ipv4Result.AD || ipv6Result.AD || nsResult.AD, CD: ipv4Result.CD || ipv6Result.CD || nsResult.CD, Question: [], Answer: [...(ipv4Result.Answer || []), ...(ipv6Result.Answer || [])], ipv4: { records: ipv4Result.Answer || [] }, ipv6: { records: ipv6Result.Answer || [] }, ns: { records: [] } }; [ipv4Result, ipv6Result, nsResult].forEach(result => { if (result.Question) { if (Array.isArray(result.Question)) { combinedResult.Question.push(...result.Question); } else { combinedResult.Question.push(result.Question); } } }); const nsRecords = []; if (nsResult.Answer && nsResult.Answer.length > 0) { nsResult.Answer.forEach(record => { if (record.type === 2) { nsRecords.push(record); } }); } if (nsResult.Authority && nsResult.Authority.length > 0) { nsResult.Authority.forEach(record => { if (record.type === 2 || record.type === 6) { nsRecords.push(record); combinedResult.Answer.push(record); } }); } combinedResult.ns.records = nsRecords; return new Response(JSON.stringify(combinedResult, null, 2), { headers: { "content-type": "application/json; charset=UTF-8" } }); } else { const result = await queryDns(dohUrl, domain, type); return new Response(JSON.stringify(result, null, 2), { headers: { "content-type": "application/json; charset=UTF-8" } }); } } catch (err) { console.error("DNS query failed:", err); return new Response(JSON.stringify({ error: `DNS query failed: ${err.message}`, doh: dohUrl, domain: domain, stack: err.stack }, null, 2), { headers: { "content-type": "application/json; charset=UTF-8" }, status: 500 }); } } // DNS查询通用函数 async function queryDns(dohServer, domain, type) { const dohUrl = new URL(dohServer); dohUrl.searchParams.set("name", domain); dohUrl.searchParams.set("type", type); const fetchOptions = [ { headers: { 'Accept': 'application/dns-json' } }, { headers: {} }, { headers: { 'Accept': 'application/json' } }, { headers: { 'Accept': 'application/dns-json', 'User-Agent': 'Mozilla/5.0 DNS Client' } } ]; let lastError = null; for (const options of fetchOptions) { try { const response = await fetch(dohUrl.toString(), options); if (response.ok) { const contentType = response.headers.get('content-type') || ''; if (contentType.includes('json') || contentType.includes('dns-json')) { return await response.json(); } else { const textResponse = await response.text(); try { return JSON.parse(textResponse); } catch (jsonError) { throw new Error(`Cannot parse response as JSON: ${jsonError.message}`); } } } const errorText = await response.text(); lastError = new Error(`DoH server error (${response.status}): ${errorText.substring(0, 200)}`); } catch (err) { lastError = err; } } throw lastError || new Error("DNS query failed"); } // 处理本地DoH请求 async function handleLocalDohRequest(domain, type, dnsDoH) { try { if (type === "all") { const [ipv4Result, ipv6Result, nsResult] = await Promise.all([ queryDns(dnsDoH, domain, "A"), queryDns(dnsDoH, domain, "AAAA"), queryDns(dnsDoH, domain, "NS") ]); const nsRecords = []; if (nsResult.Answer && nsResult.Answer.length > 0) { nsRecords.push(...nsResult.Answer.filter(record => record.type === 2)); } if (nsResult.Authority && nsResult.Authority.length > 0) { nsRecords.push(...nsResult.Authority.filter(record => record.type === 2 || record.type === 6)); } const combinedResult = { Status: ipv4Result.Status || ipv6Result.Status || nsResult.Status, TC: ipv4Result.TC || ipv6Result.TC || nsResult.TC, RD: ipv4Result.RD || ipv6Result.RD || nsResult.RD, RA: ipv4Result.RA || ipv6Result.RA || nsResult.RA, AD: ipv4Result.AD || ipv6Result.AD || nsResult.AD, CD: ipv4Result.CD || ipv6Result.CD || nsResult.CD, Question: [...(ipv4Result.Question || []), ...(ipv6Result.Question || []), ...(nsResult.Question || [])], Answer: [ ...(ipv4Result.Answer || []), ...(ipv6Result.Answer || []), ...nsRecords ], ipv4: { records: ipv4Result.Answer || [] }, ipv6: { records: ipv6Result.Answer || [] }, ns: { records: nsRecords } }; return new Response(JSON.stringify(combinedResult, null, 2), { headers: { "content-type": "application/json; charset=UTF-8", 'Access-Control-Allow-Origin': '*' } }); } else { const result = await queryDns(dnsDoH, domain, type); return new Response(JSON.stringify(result, null, 2), { headers: { "content-type": "application/json; charset=UTF-8", 'Access-Control-Allow-Origin': '*' } }); } } catch (err) { console.error("DoH query failed:", err); return new Response(JSON.stringify({ error: `DoH query failed: ${err.message}`, stack: err.stack }, null, 2), { headers: { "content-type": "application/json; charset=UTF-8", 'Access-Control-Allow-Origin': '*' }, status: 500 }); } } // 返回nginx默认欢迎页 function getNginxPage() { return `<!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>`; } 部署之后,将 worker 的地址填写到 Chrome 的自定义 DNS 服务器中,并启用安全 DNS: ...