最近在 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:
然后在 https://ipleak.net/ 中验证 DoH 是否生效。
启用前
只要有墙内的 DNS 服务器出现,就证明 DNS 查询数据泄露了,难逃审查……
启用后
只有自部署的 DNS 服务器出现在列表中,极大地降低了被审查的风险~