:加密DNS查詢與隱私保護(hù)實(shí)踐)
好久沒折騰基礎(chǔ)設(shè)施了前陣子因?yàn)榧依锖娃k公網(wǎng)絡(luò)對(duì)公共DNS的解析結(jié)果一直被各種“改造”加上越來(lái)越在意查詢隱私我決定用一臺(tái)云服務(wù)器ECS自己搭一套DoH服務(wù)。DoH全稱DNS over HTTPS簡(jiǎn)單說(shuō)就是把傳統(tǒng)明文DNS查詢包裹進(jìn)HTTPS請(qǐng)求里讓解析過(guò)程既加密又防篡改。這篇實(shí)踐文章不是純理論科普而是從我選型、配置、踩坑到最終跑通的全過(guò)程記錄內(nèi)容偏動(dòng)手向適合有一定Linux基礎(chǔ)、熟悉Nginx、想自己掌握DNS解析鏈路的人參考。1. 動(dòng)手之前先把DoH這件事想清楚1.1 DoH到底解決了什么痛點(diǎn)傳統(tǒng)DNS查詢走的是UDP 53端口數(shù)據(jù)包是明文。這就意味著從你的電腦到DNS服務(wù)器之間任何一個(gè)中間設(shè)備都能看到你在查詢哪個(gè)域名。公共Wi-Fi下冒充路由器的惡意設(shè)備可以輕松劫持響應(yīng)運(yùn)營(yíng)商層面動(dòng)態(tài)污染和緩存投毒也不是什么稀罕事。更常見的是你打開一個(gè)網(wǎng)頁(yè)卻被強(qiáng)制跳到一個(gè)廣告域名多半就是DNS響應(yīng)被篡改的結(jié)果。DoH的思路很簡(jiǎn)單把DNS查詢和響應(yīng)內(nèi)容封裝成HTTP請(qǐng)求再走TLS加密通道。這樣查詢數(shù)據(jù)被加密中間人看到的是普通HTTPS流量無(wú)法輕易讀取你查詢的域名同時(shí)TLS層能校驗(yàn)服務(wù)器身份響應(yīng)內(nèi)容不容易被篡改。DoH走的是443端口和普通網(wǎng)頁(yè)瀏覽端口一致和DoT那種單獨(dú)的853端口相比在流量特征上更接近日常Web流量。另外它天然能復(fù)用HTTP生態(tài)里的特性比如長(zhǎng)連接、緩存、負(fù)載均衡甚至后續(xù)的HTTP/3。這里多說(shuō)一句DoH并不等于“私有DNS客戶端隨便填個(gè)地址就行”??蛻舳艘嫒軩oH服務(wù)端需要處理兩種標(biāo)準(zhǔn)請(qǐng)求模式GET模式把二進(jìn)制DNS報(bào)文做base64url編碼放在query參數(shù)里POST模式則直接把二進(jìn)制報(bào)文放在請(qǐng)求體里。這兩種模式對(duì)端都有要求配置時(shí)容易出問(wèn)題后面我會(huì)詳細(xì)講。1.2 為什么選云服務(wù)器ECS而不是軟路由或者樹莓派我之前也考慮過(guò)在軟路由上直接跑DoH但很快否決了。第一家里的公網(wǎng)IP是動(dòng)態(tài)的你不可能讓手機(jī)在外網(wǎng)隨時(shí)通過(guò)固定域名連回家里第二家用寬帶的上行帶寬和穩(wěn)定性都不夠理想第三TLS證書申請(qǐng)需要一個(gè)公網(wǎng)可達(dá)的域名和端口家庭寬帶甚至?xí)环獾?0/443端口。用云服務(wù)器ECS來(lái)跑這件事就順理成章。它有固定公網(wǎng)IP443端口可控安全組能精確限制來(lái)源IP系統(tǒng)掛了可以快照回滾性能不夠了可以升配域名解析和證書簽發(fā)都是常規(guī)操作。跑DoH對(duì)計(jì)算要求很低1核1G的實(shí)例就能跑得很穩(wěn)就算加上Nginx和unbound資源占用也遠(yuǎn)達(dá)不到瓶頸。1.3 整體架構(gòu)是怎么設(shè)計(jì)的我最終采用的架構(gòu)是客戶端 - HTTPS 443 - Nginx - DoH轉(zhuǎn)化進(jìn)程 - 127.0.0.1:53 - unbound遞歸DNS這個(gè)鏈路里Nginx只負(fù)責(zé)TLS終結(jié)、路徑分發(fā)、速率限制這些入口層工作DoH轉(zhuǎn)化進(jìn)程負(fù)責(zé)把標(biāo)準(zhǔn)DoH請(qǐng)求里的二進(jìn)制DNS報(bào)文提取出來(lái)轉(zhuǎn)成普通DNS查詢發(fā)給本地unboundunbound則真正負(fù)責(zé)遞歸解析和緩存。這樣做的好處是每一層職責(zé)清晰出了問(wèn)題能快速定位瀏覽器上TLS握手失敗那就是Nginx層的事能握手但解析失敗就要看DoH轉(zhuǎn)化進(jìn)程和unbound層了。之前也考慮過(guò)直接用CoreDNS做DoH服務(wù)端。CoreDNS的DoH插件確實(shí)能直接用但它把TLS終止、HTTP處理、DNS遞歸全揉在一個(gè)進(jìn)程里遇到并發(fā)問(wèn)題或者需要加訪問(wèn)策略時(shí)配置起來(lái)不夠靈活。Nginx加獨(dú)立轉(zhuǎn)化進(jìn)程的方案更符合我對(duì)可運(yùn)維性的要求。2. 環(huán)境準(zhǔn)備與軟件選型2.1 云服務(wù)器與系統(tǒng)基礎(chǔ)環(huán)境我用的是一臺(tái)2核2G的云服務(wù)器ECS系統(tǒng)是Debian最新穩(wěn)定版。選Debian而不是CentOS純粹是因?yàn)榘姹拘隆①Y料多、踩坑容易搜到解決方案。如果選Ubuntu LTS也一樣命令基本通用。安全組規(guī)則我只放行了三個(gè)端口TCP 22SSH、TCP 80證書驗(yàn)證用、TCP 443DoH服務(wù)。其他端口一律拒絕包括UDP 53、TCP 53。這里有一個(gè)細(xì)節(jié)我反復(fù)提醒自己DNS本身需要走UDP 53但那是給內(nèi)部局域網(wǎng)用的云服務(wù)器的安全組不需要對(duì)外暴露53端口。unbound只監(jiān)聽127.0.0.1完全不參與公網(wǎng)訪問(wèn)。2.2 域名與DNS記錄DoH服務(wù)不能只靠IP訪問(wèn)HTTPS證書和客戶端校驗(yàn)都需要域名。我拿了一個(gè)閑置域名在解析服務(wù)商那里給ECS的公網(wǎng)IP添加了一條A記錄子域名類似dns.example.com這種。域名選擇上我建議越短越好因?yàn)镈oH地址是要被寫進(jìn)瀏覽器配置、手機(jī)描述文件里的域名越長(zhǎng)越容易在配置時(shí)敲錯(cuò)。如果你手頭只有純IP技術(shù)上也能通過(guò)自簽證書跑通但瀏覽器里會(huì)出現(xiàn)證書不受信任的警告實(shí)際使用體驗(yàn)很差所以還是建議準(zhǔn)備一個(gè)域名。2.3 軟件選型對(duì)比這是整個(gè)實(shí)踐里比較關(guān)鍵的一步我把自己對(duì)比過(guò)的幾套方案整理了一下方案組件優(yōu)點(diǎn)缺點(diǎn)適用場(chǎng)景方案ANginx DoH轉(zhuǎn)化進(jìn)程 unbound分層清晰、可擴(kuò)展、Nginx生態(tài)成熟需要多維護(hù)一個(gè)轉(zhuǎn)化進(jìn)程生產(chǎn)級(jí)個(gè)人服務(wù)方案BCoreDNS一個(gè)進(jìn)程全搞定、DoH插件原生支持TLS終止和訪問(wèn)控制偏弱、配置偏復(fù)雜想快速驗(yàn)證DoH方案Cdnsmasq Nginx配置簡(jiǎn)單dnsmasq只適合做緩存轉(zhuǎn)發(fā)不支持DNSSEC驗(yàn)證對(duì)解析質(zhì)量要求不高的場(chǎng)景我選了方案A。DoH只是解決“傳輸通道”問(wèn)題真正決定解析質(zhì)量和速度的是上游解析器。unbound作為遞歸解析器支持DNSSEC驗(yàn)證、緩存策略精細(xì)、內(nèi)存占用低比dnsmasq純粹做轉(zhuǎn)發(fā)要強(qiáng)得多。Nginx則負(fù)責(zé)盡可能穩(wěn)定地承接外部HTTPS請(qǐng)求。2.4 關(guān)于DoH轉(zhuǎn)化進(jìn)程的選擇DoH轉(zhuǎn)化進(jìn)程是整個(gè)鏈路里最容易讓人頭疼的部分。現(xiàn)成的開源方案很多找起來(lái)費(fèi)神而且質(zhì)量參差不齊。有些項(xiàng)目只支持POST模式GET模式直接返回400有些項(xiàng)目很久沒人維護(hù)依賴一堆老庫(kù)部署到新系統(tǒng)上編譯都過(guò)不去。我最后選擇了一個(gè)用Go寫的輕量開源程序單文件部署內(nèi)置HTTP服務(wù)支持RFC 8484標(biāo)準(zhǔn)的GET和POST兩種模式可以把請(qǐng)求中的DNS報(bào)文轉(zhuǎn)發(fā)到指定上游。至于具體是哪個(gè)名字我不打算在這里做廣告但你搜“doh proxy”這類關(guān)鍵詞能找到多個(gè)類似實(shí)現(xiàn)選的時(shí)候注意關(guān)注三點(diǎn)是否同時(shí)支持GET和POST、是否能配置上游地址、項(xiàng)目最近是否有活躍更新。如果找不到合適的二進(jìn)制自己寫一個(gè)也不難后面我會(huì)給出一段核心代碼做參考。3. 核心配置與實(shí)操過(guò)程3.1 安裝unbound并配置遞歸解析先裝基礎(chǔ)工具apt update apt install -y unbound nginxunbound的配置文件在 /etc/unbound/unbound.conf下面這段是精簡(jiǎn)后的核心配置server: interface: 127.0.0.1 port: 53 do-ip4: yes do-ip6: no do-daemonize: yes access-control: 127.0.0.0/8 allow access-control: 0.0.0.0/0 refuse cache-min-ttl: 3600 cache-max-ttl: 86400 prefetch: yes prefetch-key: yes num-threads: 2 msg-cache-size: 64m rrset-cache-size: 128m val-clean-additional: yes配置里有兩個(gè)點(diǎn)值得展開說(shuō)。第一個(gè)是access-control我只允許本機(jī)回環(huán)地址訪問(wèn)其他來(lái)源全部refuse。這是防止unbound變成開放式遞歸解析器避免被人利用做DNS放大攻擊。第二個(gè)是prefetch這個(gè)開關(guān)可以讓unbound在緩存快到期的前幾秒自動(dòng)重新查詢避免緩存剛過(guò)期后第一個(gè)查詢要等完整遞歸。另一個(gè)重點(diǎn)是驗(yàn)證unbound是否能正常解析dig 127.0.0.1 example.com如果返回了A記錄說(shuō)明unbound本身工作正常。如果遇到SERVFAIL多半是DNSSEC驗(yàn)證沒過(guò)可以先用dig dnssec 127.0.0.1查看具體的返回狀態(tài)碼。3.2 部署DoH轉(zhuǎn)化進(jìn)程我用的Go開源程序部署方式就是下載二進(jìn)制、給執(zhí)行權(quán)限、寫systemd服務(wù)文件。核心systemd配置如下[Unit] DescriptionDoH Proxy Afternetwork.target unbound.service [Service] ExecStart/usr/local/bin/doh-proxy -listen 127.0.0.1:8080 -upstream 127.0.0.1:53 Restartalways RestartSec3 Userdoh Groupdoh [Install] WantedBymulti-user.target這一步有一個(gè)坑很容易踩想當(dāng)然地把上游地址填成8.8.8.8或公共DNS結(jié)果本地unbound的緩存完全沒用到解析延遲反而變高。正確做法是填127.0.0.1:53讓DoH進(jìn)程只負(fù)責(zé)協(xié)議轉(zhuǎn)換真正的數(shù)據(jù)解析全部交給unbound。如果你不想依賴現(xiàn)成程序自己用Python寫一個(gè)最小實(shí)現(xiàn)也很容易。下面這段代碼用aiohttp做HTTP服務(wù)核心邏輯就是把DoH請(qǐng)求中的DNS報(bào)文原樣轉(zhuǎn)發(fā)到本地unboundimport asyncio import base64 import socket from aiohttp import web async def query_unbound(raw: bytes) - bytes: loop asyncio.get_event_loop() return await loop.run_in_executor(None, _query_sync, raw) def _query_sync(raw: bytes) - bytes: s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(3) s.sendto(raw, (127.0.0.1, 53)) data, _ s.recvfrom(4096) return data async def handle_doh(request): if request.method GET: dns_param request.query.get(dns) raw base64.urlsafe_b64decode(dns_param ) elif request.method POST: raw await request.read() else: return web.Response(status405) resp await query_unbound(raw) return web.Response(bodyresp, content_typeapplication/dns-message) app web.Application() app.router.add_route(*, /dns, handle_doh) web.run_app(app, host127.0.0.1, port8080)這段代碼夠跑通鏈路但production環(huán)境我還是推薦用Go實(shí)現(xiàn)單個(gè)二進(jìn)制文件丟上去就能跑內(nèi)存和并發(fā)表現(xiàn)遠(yuǎn)好于Python解釋器。3.3 申請(qǐng)TLS證書并配置NginxDoH服務(wù)必須跑在HTTPS上我用ACME協(xié)議從免費(fèi)證書機(jī)構(gòu)申請(qǐng)了一張證書。申請(qǐng)方式很多我建議直接用支持自動(dòng)續(xù)期的客戶端工具。先安裝客戶端apt install -y certbot然后執(zhí)行證書簽發(fā)certbot certonly --webroot -w /var/www/html -d dns.example.com這里有個(gè)非常容易踩的坑如果你已經(jīng)在Nginx里配置了location / { deny all; }證書驗(yàn)證請(qǐng)求會(huì)被攔截簽發(fā)過(guò)程直接失敗。所以要么在申請(qǐng)證書之前先把Nginx停掉要么在Nginx里單獨(dú)為ACME驗(yàn)證路徑做放行。我自己就被這個(gè)坑卡了將近半小時(shí)后面在Nginx配置章節(jié)里會(huì)專門給出放行配置。證書簽好之后Nginx監(jiān)聽443端口并把DoH路徑代理給本地8080端口。核心配置如下server { listen 443 ssl; http2 on; server_name dns.example.com; ssl_certificate /etc/letsencrypt/live/dns.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/dns.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; location ^~ /.well-known/acme-challenge/ { root /var/www/html; } location /dns { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; large_client_header_buffers 4 16k; client_body_buffer_size 1k; client_max_body_size 4k; proxy_read_timeout 10s; } location / { deny all; } access_log off; }幾個(gè)配置點(diǎn)我需要強(qiáng)調(diào)ssl_session_cache和ssl_session_timeout能讓同一客戶端復(fù)用TLS會(huì)話避免每次查詢都重新走一遍完整握手對(duì)降低延遲很有幫助。access_log off關(guān)掉了訪問(wèn)日志。DoH請(qǐng)求的URL里本身就帶有base64編碼的DNS查詢報(bào)文如果開著訪問(wèn)日志相當(dāng)于把你所有DNS查詢記錄明文寫在磁盤上那做DoH的隱私意義就大打折扣了。location /dns 用的是精確匹配只暴露這個(gè)路徑其他路徑一律deny。這樣即使有人掃描到你服務(wù)器的443端口也找不到可用端點(diǎn)。改完Nginx配置后一定要先測(cè)試再重載nginx -t systemctl reload nginx3.4 客戶端接入與實(shí)測(cè)驗(yàn)證服務(wù)端跑起來(lái)之后先用命令行工具做一次完整的DoH請(qǐng)求驗(yàn)證。下面這個(gè)命令用的是GET模式dns參數(shù)是base64url編碼的DNS查詢報(bào)文curl -sk -H accept: application/dns-message \ https://dns.example.com/dns?dnsAAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB -o /tmp/doh.bin正常返回的是一個(gè)二進(jìn)制的DNS響應(yīng)報(bào)文可以用xxd /tmp/doh.bin查看內(nèi)容能看到響應(yīng)標(biāo)志位和具體的A記錄信息。如果返回的是JSON那說(shuō)明你訪問(wèn)到的可能是個(gè)JSON接口不是標(biāo)準(zhǔn)的RFC 8484 DoH接口。實(shí)測(cè)通過(guò)后就可以配置終端設(shè)備了。瀏覽器里通常在隱私設(shè)置或安全設(shè)置中找到“安全DNS”選項(xiàng)在里面填入https://dns.example.com/dns即可。手機(jī)系統(tǒng)如果原生支持私有DNS也可以填入這個(gè)DoH地址不支持的可以通過(guò)配置描述文件方式下發(fā)。配置好之后訪問(wèn)一個(gè)域名然后在服務(wù)器上觀察unbound的查詢?nèi)罩救绻芸吹絹?lái)自127.0.0.1的查詢記錄說(shuō)明整條鏈路已經(jīng)通了。4. 性能調(diào)優(yōu)、訪問(wèn)控制與日常運(yùn)維4.1 從緩存到并發(fā)性能還能再壓一壓DoH服務(wù)跑通只是第一步性能調(diào)優(yōu)直接決定實(shí)際使用體驗(yàn)。我的調(diào)優(yōu)方向有三個(gè)層面。第一是unbound的緩存調(diào)整。cache-min-ttl和cache-max-ttl控制緩存的最短和最長(zhǎng)保留時(shí)間prefetch開啟后高頻域名的TTL快到期時(shí)會(huì)自動(dòng)提前查詢讓緩存永遠(yuǎn)“不冷”。我設(shè)置cache-min-ttl為3600秒意味著即使某個(gè)域名原TTL只有60秒在unbound層也會(huì)緩存至少1小時(shí)。這樣做的好處是減少上游遞歸請(qǐng)求但代價(jià)是域名解析結(jié)果更新可能變慢。對(duì)個(gè)人服務(wù)來(lái)說(shuō)這個(gè)取舍完全值得。第二是Nginx的連接復(fù)用。HTTP/2天然支持多路復(fù)用同一個(gè)連接上可以并行發(fā)起多個(gè)DoH請(qǐng)求不需要每次都做TCP握手和TLS握手。如果你的Nginx版本支持HTTP/3還可以把UDP 443的流量也納入進(jìn)來(lái)不過(guò)普通場(chǎng)景下HTTP/2已經(jīng)夠用。打開ssl_session_cache之后TLS握手的開銷進(jìn)一步降低實(shí)測(cè)下來(lái)長(zhǎng)連接場(chǎng)景的延遲表現(xiàn)比獨(dú)立短連接好很多。第三是DoH轉(zhuǎn)化進(jìn)程的并發(fā)處理。Go語(yǔ)言實(shí)現(xiàn)的轉(zhuǎn)化進(jìn)程本身就有很好的并發(fā)能力但如果你的上游unbound成為瓶頸那就需要檢查unbound的num-threads和outgoing-range參數(shù)。num-threads設(shè)置成CPU核數(shù)即可outgoing-range控制同時(shí)向上游發(fā)起的查詢數(shù)量默認(rèn)值太低時(shí)可以通過(guò)調(diào)大來(lái)提升并發(fā)處理能力。4.2 防濫用個(gè)人DoH服務(wù)最大的風(fēng)險(xiǎn)開放DoH服務(wù)最大的風(fēng)險(xiǎn)不是被攻擊而是被掃描工具發(fā)現(xiàn)后當(dāng)作免費(fèi)公共DNS使用。別人的設(shè)備走你的DoH服務(wù)解析會(huì)白白消耗你的帶寬、CPU而且會(huì)增加日志記錄的敏感數(shù)據(jù)量。更麻煩的是如果你的服務(wù)被用于大量異常查詢可能被云廠商標(biāo)記為異常行為。我的防濫用策略有三層建議照抄第一層路徑混淆。不要把DoH端點(diǎn)放在 /dns 這種一眼就能猜到的路徑下改成類似 /dns/3f9a1c2e 這種帶隨機(jī)串的路徑。功能上沒有任何區(qū)別但可以讓掃描器無(wú)法輕易判斷這是一個(gè)DoH端點(diǎn)。第二層Nginx速率限制。針對(duì)單個(gè)來(lái)源IP做請(qǐng)求頻率限制limit_req_zone $binary_remote_addr zonedoh:10m rate5r/s; server { location /dns/3f9a1c2e { limit_req zonedoh burst10 nodelay; proxy_pass http://127.0.0.1:8080; } }rate5r/s表示每個(gè)IP每秒最多5個(gè)請(qǐng)求burst10允許短時(shí)間突發(fā)10個(gè)請(qǐng)求。正常瀏覽器使用DoH的頻率遠(yuǎn)低于這個(gè)值所以不會(huì)影響體驗(yàn)但能有效壓制批量掃描。第三層安全組白名單。在云廠商安全組里只放行自己的家庭出口IP。如果家里是動(dòng)態(tài)IP寫一個(gè)定時(shí)腳本定期檢測(cè)本機(jī)出口IP變了就調(diào)用云廠商API更新安全組規(guī)則。對(duì)于防濫用來(lái)說(shuō)這一層最有效但需要一點(diǎn)腳本開發(fā)能力。如果不想寫腳本也可以退一步只保留路徑混淆和速率限制。4.3 日志、證書自動(dòng)續(xù)期與健康檢查個(gè)人服務(wù)的日常運(yùn)維重點(diǎn)就三件事保證服務(wù)在跑、保證證書沒過(guò)期、保證沒有異常訪問(wèn)。日志方面Nginx access_log直接關(guān)掉。unbound的日志也建議只記錄錯(cuò)誤級(jí)別不要記錄每一條查詢?nèi)罩?。如果你確實(shí)需要分析查詢統(tǒng)計(jì)可以在unbound里單獨(dú)配置統(tǒng)計(jì)模塊而不是依賴訪問(wèn)日志。證書自動(dòng)續(xù)期我用系統(tǒng)的定時(shí)任務(wù)來(lái)實(shí)現(xiàn)certbot renew --quiet --deploy-hook systemctl reload nginx為了避免定時(shí)任務(wù)出問(wèn)題導(dǎo)致證書悄悄過(guò)期我額外寫了一個(gè)簡(jiǎn)單的systemd定時(shí)器每天檢查一次證書剩余天數(shù)如果小于30天就強(qiáng)制續(xù)期。同時(shí)配合云廠商的監(jiān)控告警服務(wù)器能收到系統(tǒng)資源超限提醒。整個(gè)運(yùn)維不需要投入太多精力但必要的檢查不能省。5. 常見問(wèn)題與排查思路5.1 證書驗(yàn)證一直失敗證書簽不下來(lái)優(yōu)先級(jí)最高的排查項(xiàng)是80端口ACME驗(yàn)證路徑是否可達(dá)。一種情況是云廠商安全組沒放行80端口ACME服務(wù)端無(wú)法訪問(wèn)你的驗(yàn)證文件另一種情況是Nginx配置了全局deny把驗(yàn)證請(qǐng)求攔截掉了。排查方法很簡(jiǎn)單curl -I http://dns.example.com/.well-known/acme-challenge/test如果不是返回404而是直接連接失敗那就是安全組或服務(wù)沒起來(lái)的問(wèn)題。如果返回403就是Nginx的localtion匹配順序問(wèn)題需要在server塊里加上前面給出的ACME驗(yàn)證路徑放行配置。5.2 瀏覽器提示無(wú)法連接DoH服務(wù)器這種問(wèn)題一般出在協(xié)議兼容性上。瀏覽器對(duì)DoH服務(wù)端的校驗(yàn)很嚴(yán)格除了TLS證書有效外還要求服務(wù)端正確響應(yīng)GET和POST兩種模式并且返回的Content-Type必須是 application/dns-message。用curl分別測(cè)試兩種模式# GET模式 curl -sk -H accept: application/dns-message \ https://dns.example.com/dns?dnsAAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB -w %{http_code} # POST模式 echo -n AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB | base64 -d /tmp/dns.bin curl -sk -H accept: application/dns-message -H content-type: application/dns-message \ --data-binary /tmp/dns.bin https://dns.example.com/dns -w %{http_code}正常情況下兩個(gè)請(qǐng)求都應(yīng)該返回200響應(yīng)體是二進(jìn)制DNS報(bào)文。如果GET模式返回400或405多半是轉(zhuǎn)化進(jìn)程不支持GET模式如果返回的Content-Type是text/html說(shuō)明請(qǐng)求被某個(gè)反代規(guī)則誤處理了。5.3 解析正常但延遲明顯偏高DoH請(qǐng)求鏈路比普通DNS多出了TLS握手和HTTP解析環(huán)節(jié)延遲理應(yīng)比傳統(tǒng)DNS高一些但如果高到幾百毫秒就需要排查了。第一個(gè)排查點(diǎn)在unbound。直接在本機(jī)執(zhí)行dig 127.0.0.1 example.com看響應(yīng)時(shí)間。如果本身就慢說(shuō)明unbound遞歸解析上游有問(wèn)題可以把prefetch: yes打開或者檢查上游是否訪問(wèn)緩慢。第二個(gè)排查點(diǎn)在Nginx是否成功復(fù)用了TLS會(huì)話。如果每次請(qǐng)求都重新握手延遲會(huì)明顯增加檢查ssl_session_cache是否生效。第三個(gè)排查點(diǎn)是網(wǎng)絡(luò)鏈路本身的RTT。如果你的手機(jī)和云服務(wù)器之間跨運(yùn)營(yíng)商延遲高是正常的可以換一臺(tái)離自己更近的節(jié)點(diǎn)。5.4 證書到期了但自動(dòng)續(xù)期沒觸發(fā)踩過(guò)這個(gè)坑之后我學(xué)乖了。certbot自帶的定時(shí)任務(wù)不是所有系統(tǒng)都會(huì)自動(dòng)啟用你手動(dòng)執(zhí)行過(guò)certbot命令但定時(shí)器可能根本沒有拉起。用systemd timer的化檢查狀態(tài)systemctl status certbot.timer如果定時(shí)器沒啟用手動(dòng)啟用systemctl enable --now certbot.timer如果你用的是cron那就要檢查certbot.log。另一個(gè)容易被忽視的問(wèn)題是續(xù)期時(shí)的deploy-hook證書續(xù)期后Nginx還在用舊證書直到你手動(dòng)reload配置。所以deploy-hook里一定要寫Nginx reload不然就算證書續(xù)期成功服務(wù)端仍然提供舊的過(guò)期證書。5.5 服務(wù)器日志里出現(xiàn)大量陌生IP請(qǐng)求這說(shuō)明你的DoH端點(diǎn)已經(jīng)被掃描器發(fā)現(xiàn)了。不用慌按前面提到的三層防御逐級(jí)處理先把端點(diǎn)路徑改掉然后在Nginx層加速率限制最徹底的手段是安全組白名單。掃描器的攻擊大多來(lái)自海外IP如果你用不上海外訪問(wèn)直接在安全組里只放行自己的國(guó)家和地區(qū)IP段能省掉很多麻煩。如果在安全組層面已經(jīng)限制了白名單但日志里還是有陌生IP那多半不是公網(wǎng)進(jìn)來(lái)的而是你的服務(wù)器上某個(gè)服務(wù)被當(dāng)作了跳板。這時(shí)候要檢查系統(tǒng)進(jìn)程和對(duì)外開放的端口確認(rèn)沒有其他未授權(quán)服務(wù)在運(yùn)行。搭建DoH這套東西技術(shù)難度其實(shí)不算高真正花時(shí)間的是理解每一層為什么要這么配。我在這個(gè)過(guò)程中踩過(guò)最大的坑就是一開始把DoH轉(zhuǎn)化進(jìn)程的上游直接指向了公共DNS結(jié)果unbound的緩存完全沒起作用解析速度反而比直接用公共DoH還慢。后面改成走本地unbound之后整個(gè)鏈路才真正順暢起來(lái)。如果你也想搭一套個(gè)人DoH服務(wù)我的建議是先把架構(gòu)想清楚不要一上來(lái)就去改Nginx配置等鏈路整體跑通之后再逐步加訪問(wèn)控制這樣排查問(wèn)題會(huì)輕松很多。