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: ...

2025.11.10 11:09:33 · 6 分钟 · yv1ing

Clash 代理家宽 VPS 落地指南

最近发现一直使用的机场 IP 越来越脏了,访问谷歌都会一直弹人机验证,所以打算切换到廉价机场+家宽 VPS 落地的方案,这样成本不会太高,用起来体验也更好一些。 家宽 VPS 配置 为了保证低延时的使用体验,我决定使用香港的机场节点和香港的家宽 VPS。VPS 上只需要开启一个 socks5 代理服务,所以只需要最低的配置即可。这里选了一台 1h2g 的机器(因为 1h0.5g 的售罄了……)。然后使用 gost 作为代理服务: 下载地址: https://github.com/ginuerzh/gost/releases 启动命令:nohup gost -L=<username>:<password>@0.0.0.0:<port> Clash 配置 我一直使用 clash verge 作为代理工具,只需要在 全局扩展脚本 中添加以下代码,即可设置链式代理: // Define main function (script entry) const proxyName = "家宽落地节点"; const chainName = "喻灵的链式代理"; // 链式代理组名称 const dialerProxyName = chainName + "中转节点"; // 链式代理中转节点选择组名称 const socks5Addr = ""; const socks5Port = 8080; const socks5Username = ""; const socks5Password = ""; // 添加后置代理 const appendProxies = [ { "name": proxyName, "dialer-proxy": dialerProxyName, "type": "socks5", "server": socks5Addr, "port": socks5Port, "username": socks5Username, "password": socks5Password } ]; // 添加后置代理组 const appendProxyGroups = [ { "name": dialerProxyName, "type": "select", "include-all": true, "exclude-filter": appendProxies.map(obj => obj.name).join('|') }, { "name": chainName, "type": "select", "proxies": appendProxies.map(obj => obj.name) } ]; function main(config, profileName) { const proxies = config.proxies || []; config.proxies = proxies.concat(appendProxies); const proxyGroups = config["proxy-groups"] || []; let pg; pg = proxyGroups[0]; if (pg) { const pgps = pg.proxies || []; pg.proxies = [chainName].concat(pgps); } config["proxy-groups"] = proxyGroups.concat(appendProxyGroups); return config; } 先在 链式代理中转节点 中选择用于中转本地流量到落地 VPS 的机场节点,选择一个和 VPS 同地域延迟最低的即可: ...

2025.10.25 10:07:59 · 1 分钟 · yv1ing

强网杯 2025-Writeup(部分)

SecretVault 审计源代码,发现对 X-User 请求头的提取有逻辑问题,只需要传空头即可伪造成 admin 身份,获取 flag: Payload: GET /dashboard HTTP/1.1 Host: 47.93.103.116:39040 Connection: close,X-User Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBdXRob3JpemVyIiwic3ViIjoiMSIsImV4cCI6MTc2MDc1ODkwNywibmJmIjoxNzYwNzU1MzA3LCJpYXQiOjE3NjA3NTUzMDcsInVpZCI6IjEifQ.SVosiGo7VXuKuRN-YkQO9EWUt_WAmG7jMAbCRPHfHMs Personal Vault 使用 Lovelymem 工具加载镜像后,直接搜索 flag{ 字符串即可获得 flag: Check-little Claude Code 自行审计生成解密脚本: from Crypto.Util.number import * from Crypto.Util.Padding import unpad from Crypto.Cipher import AES import gmpy2 N = 18795243691459931102679430418438577487182868999316355192329142792373332586982081116157618183340526639820832594356060100434223256500692328397325525717520080923556460823312550686675855168462443732972471029248411895298194999914208659844399140111591879226279321744653193556611846787451047972910648795242491084639500678558330667893360111323258122486680221135246164012614985963764584815966847653119900209852482555918436454431153882157632072409074334094233788430465032930223125694295658614266389920401471772802803071627375280742728932143483927710162457745102593163282789292008750587642545379046283071314559771249725541879213 c = 10533300439600777643268954021939765793377776034841545127500272060105769355397400380934565940944293911825384343828681859639313880125620499839918040578655561456321389174383085564588456624238888480505180939435564595727140532113029361282409382333574306251485795629774577583957179093609859781367901165327940565735323086825447814974110726030148323680609961403138324646232852291416574755593047121480956947869087939071823527722768175903469966103381291413103667682997447846635505884329254225027757330301667560501132286709888787328511645949099996122044170859558132933579900575094757359623257652088436229324185557055090878651740 iv = b'\x91\x16\x04\xb9\xf0RJ\xdd\xf7}\x8cW\xe7n\x81\x8d' ciphertext = bytes.fromhex('bf87027bc63e69d3096365703a6d47b559e0364b1605092b6473ecde6babeff2') p = gmpy2.gcd(c, N) if 1 < p < N: q = N // p e = 3 phi = (p - 1) * (q - 1) d = pow(e, -1, phi) key = pow(c, d, N) aes_key = long_to_bytes(key)[:16] try: cipher = AES.new(key=aes_key, iv=iv, mode=AES.MODE_CBC) plaintext = cipher.decrypt(ciphertext) flag = unpad(plaintext, 16).decode() print(flag) except Exception as e: print(f"[-] AES 解密失败: {e}") The_Interrogation_Room Claude Code 自行审计生成解题脚本: ...

2025.10.19 01:20:32 · 6 分钟 · yv1ing

点亮人生第一机

还在上大三的时候,就一直想装一台 itx 主机来玩,但是当时每个月的生活费有限,实在拿不出闲钱来购买硬件。现在开始工作之后,终于可以实现当时没能实现的愿望了。 为自己的第一台 itx 购置的硬件如下: 处理器: 英特尔至强 E5 2673 v3 内存: 镁光 DDR3 服务器内存 32g x 2 显卡: 蓝宝石 RX 580 8g 主板: 九尾狐 X99 硬盘: 京东京造 m.2 256g + 光威 sata 512g 电源: 鑫动力 1U 全模组 500W 散热: ALLA 五铜管下压式 机箱: 小朱雀白色 当真的组装完成点亮的那一刻,心情真的是无比舒畅的。也许这份装机配置并不是最完美的,但却是对自己最有意义的。

2025.09.13 06:40:16 · 1 分钟 · yv1ing

Obsidian TOS 插件开发

最近打算把笔记还有博客的图床迁移到火山云上,之前写的 Obsidian COS 插件就没办法继续使用了,也不想再安装 PicGo 之类的东西。奈何没有合适的插件,只好在之前的插件基础上,更换到了 TOS 的 SDK 版本,并做了一些改动。 主要功能 粘贴图片直接自动上传到对象存储中,省去了在本地安装 PicGo 等软件的步骤; 删除图片时同步删除对象存储中的文件,避免废弃的图片遗留在云端,造成不必要的开销; 一键删除当前文档中的所有图片,避免删除文档时还要手动逐张删除; 插件预览 插件简介 插件配置 代码开源在 yv1ing/obsidian-tos-picbed,如需使用,直接下载 release 中的文件,移动到 .obsidian/plugins 目录下,并在 Obsidian 中启用即可。

2025.08.31 12:32:32 · 1 分钟 · yv1ing