絡(luò)協(xié)議實戰(zhàn)(5):DNS 解析原理與排障)
問題背景上一篇結(jié)尾算過冷啟動的賬DNS 是域名變 IP的第一步也是最常被跳過排查的一步。它的事故形態(tài)極有迷惑性用戶說網(wǎng)站打不開運維 ping IP 一切正常最后發(fā)現(xiàn)是 LocalDNS 被運營商劫持插了廣告改完解析記錄明明已經(jīng)生效一部分用戶還是打到舊服務(wù)器另一部分用戶 5 分鐘就好了差異只出在各級緩存的 TTL還有 IPv6 雙棧站點有些人偶發(fā)慢 5 秒才通兇手是只回了一個壞 AAAA 記錄。DNS 的特殊之處在于它不是一個服務(wù)器而是一條四層緩存鏈應(yīng)用→操作系統(tǒng)→LocalDNS→權(quán)威任何一層的緩存策略、超時、污染都會偽裝成網(wǎng)站的問題。本篇把這條鏈拆開先用 struct 手工編解 DNS 報文看清 12 字節(jié)頭、標簽域名和壓縮指針這些線上抓包天天遇到的結(jié)構(gòu)再用確定性模型跑一遍從根服務(wù)器出發(fā)的迭代解析與按 RRset 到期的緩存賬本。學(xué)完你應(yīng)該能回答改了記錄多久全網(wǎng)生效這類問題的正確提問方式是什么。核心原理第一層域名空間是一棵倒掛的樹解析是沿樹的委派鏈問路。www.demo.local的查詢順序根服務(wù)器不知道答案但它知道.local的 NS 是誰回一條委派referralTLD 服務(wù)器同樣把demo.local區(qū)的 NS 指給你最終權(quán)威服務(wù)器域名持有者自管或云廠商托管才給出權(quán)威應(yīng)答AA 標志位。關(guān)鍵設(shè)計是黏合記錄glueTLD 回去問 ns1.demo.local時如果這個 NS 的名字本身在被委派的區(qū)內(nèi)必須同時附上它的 A 記錄——否則你陷入想知道 ns1 的 IP 得先解析 ns1的死循環(huán)。區(qū)zone是授權(quán)的管理單元對應(yīng)一份區(qū)文件里面有 SOA區(qū)的負責(zé)人與序列號、NS、以及你要的所有記錄權(quán)威 DNS 掛了和registrar 的委派指錯了是兩類故障前者你能修后者要改注冊商。第二層存根解析器與遞歸解析器的分工。你的機器是 stub——它不做迭代問路而是把幫我問到答案RD1交給 LocalDNSISP 提供的、114.114.114.114、或公司內(nèi)網(wǎng)的遞歸解析器。LocalDNS 才沿委派鏈問根、問 TLD、問權(quán)威并把沿途答案按 TTL 緩存。所以一次線上解析慢要拆成四段量應(yīng)用到 OS 解析器hosts 文件、NSS/WinDNS 順序、OS→LocalDNS 往返、LocalDNS 到權(quán)威的迭代鏈首次全走約 3 個串行往返、以及緩存命中情況。誰緩存誰負責(zé)公網(wǎng)看到的沒生效九成卡在 OS 緩存和 LocalDNS 緩存這兩層而不是DNS 同步慢——DNS 根本沒有同步機制只有各級按 TTL 過期。第三層報文結(jié)構(gòu)決定了你能看懂一切抓包。RFC 1035 定義的報文12 字節(jié)頭 問題區(qū) 回答/權(quán)威/附加三個 RR 區(qū)頭里三個字段最要緊16 位 ID請求應(yīng)答配對也是早年緩存投毒攻擊可猜的原因——Kaminsky 漏洞后端口隨機化才補上、flagsQR 區(qū)分問/答、RD/RA 遞歸請求與支持、AA 是否權(quán)威、TC 截斷、四個計數(shù)。域名編碼是長度前綴標簽串3www4demo5local0應(yīng)答里為了省空間引入壓縮指針0xC0 0x0C表示名字從報文第 12 字節(jié)處開始抄——解析器必須正確還原它安全史上不少 DNS 解析崩潰漏洞如早期的 LABEL 壓縮遞歸溢出都出在這里。記錄統(tǒng)一是 NAME/TYPE/CLASS/TTL/RDATA 五元組A(v4)、AAAA(v6)、CNAME(別名指向另一個名字)、NS、MX、TXT、SOA、PTR(反向)。TTL 單位是秒最小生效粒度是一條 (名字,類型) 記錄集RRset不是整個域名——這是實驗二的核心。第四層UDP 512 字節(jié)與 TCP、EDNS0。DNS 默認 UDP歷史上響應(yīng)超過 512 字節(jié)設(shè) TC 位、客戶端換 TCP 重試——這個截斷回退在 MTU 異?;蚍阑饓ν檀蟀鼤r表現(xiàn)為只有部分客戶端解析超時。EDNS0RFC 6891把上限提到 4096 并攜帶客戶端子網(wǎng)信息ECS供 CDN 就近調(diào)度。再往后是加密化DoT(853 端口)/DoH(443 端口走 HTTPS) 讓 LocalDNS 可被終端指定Chrome 的安全 DNS可以在繞過操作系統(tǒng) hosts 與 LocalDNS的前提下直接出網(wǎng)——排障時nslookup 正常但瀏覽器解析到別處先查這個。第一次代碼實驗及輸出用 struct 手工完成一次編→解閉環(huán)構(gòu)造www.demo.local的 A 查詢報文并解析回去再拼裝一份帶 CNAMEA 的權(quán)威應(yīng)答其中名字字段全部用0xC0 0x0C壓縮指針指回問題區(qū)——線上抓包你看到的真實報文就是這個字節(jié)形狀。最后用socket.getaddrinfo看本機解析器對一個名字的返回順序IPv6 在 IPv4 前面Happy Eyeballs 的輸入源。importsocketimportstructdefencode_name(name):域名 wire 格式: 每段一個長度字節(jié), 0 結(jié)尾(不用壓縮)returnb.join(bytes([len(p)])p.encode(ascii)forpinname.split(.))b\x00defbuild_query(qid,name):標準查詢: 12 字節(jié)頭 一個問題; flags0x0100 即 RD1(請遞歸)headerstruct.pack(HHHHHH,qid,0x0100,1,0,0,0)returnheaderencode_name(name)struct.pack(HH,1,1)# TYPEA CLASSINdefdecode_name(msg,off):解域名, 支持 0xC0 壓縮指針; 返回 (name, 解析后的新偏移)labels,jumped,end[],False,offwhileTrue:lnmsg[off]ifln0xC00xC0:# 高2位為11: 指針, 指向報文內(nèi)偏移ptrstruct.unpack(H,msg[off:off2])[0]0x3FFFifnotjumped:endoff2jumped,offTrue,ptrcontinueifln0:ifnotjumped:endoff1breaklabels.append(msg[off1:off1ln].decode(ascii))off1lnreturn..join(labels),enddefparse(msg):qid,flags,qd,an,ns,arstruct.unpack(HHHHHH,msg[:12])print(報文 %d 字節(jié): id0x%04x flags0x%04x (QR%d RD%d RA%d) 問題%d 應(yīng)答%d%(len(msg),qid,flags,flags15,flags81,flags71,qd,an))off12for_inrange(qd):qname,offdecode_name(msg,off)qtype,qclassstruct.unpack(HH,msg[off:off4])off4print(問題: %s TYPE%d CLASS%d%(qname,qtype,qclass))for_inrange(an):rname,offdecode_name(msg,off)rtype,rclass,ttl,rdlenstruct.unpack(HHIH,msg[off:off10])off10rdatamsg[off:offrdlen]ifrtype1:valsocket.inet_ntoa(rdata)elifrtype5:val,_decode_name(rdata,0)else:valrepr(rdata)print(應(yīng)答: %-14s TYPE%-2s(%s) TTL%-4d - %s%(rname,rtype,Aifrtype1elseCNAME,ttl,val))offrdlen# 先造一個查詢報文, 原樣解析qbuild_query(0x2F3D,www.demo.local)print( 查詢報文 )parse(q)# 手工拼裝一份權(quán)威應(yīng)答: CNAME A, 兩處 NAME 字段都用壓縮指針 0xC00C(指向偏移12的問題域名)qnameencode_name(www.demo.local)cname_targetencode_name(cdn.demo.local)ans1b\xC0\x0Cstruct.pack(HHIH,5,1,300,len(cname_target))cname_target ipsocket.inet_aton(203.0.113.7)ans2b\xC0\x0Cstruct.pack(HHIH,1,1,60,len(ip))ip flags0x81A0# QR1 AA1 RD1 RA1respstruct.pack(HHHHHH,0x2F3D,flags,1,2,0,0)qnamestruct.pack(HH,1,1)ans1ans2print( 應(yīng)答報文(域名壓縮指針還原) )parse(resp)print( getaddrinfo(localhost) 本機解析器返回順序 )forfamily,socktype,proto,canon,sainsocket.getaddrinfo(localhost,80,typesocket.SOCK_STREAM):print(%s - %s (family%s)%(localhost,sa[0],IPv6iffamilysocket.AF_INET6elseIPv4))運行輸出 查詢報文 報文 32 字節(jié): id0x2f3d flags0x0100 (QR0 RD1 RA0) 問題1 應(yīng)答0 問題: www.demo.local TYPE1 CLASS1 應(yīng)答報文(域名壓縮指針還原) 報文 76 字節(jié): id0x2f3d flags0x81a0 (QR1 RD1 RA1) 問題1 應(yīng)答2 問題: www.demo.local TYPE1 CLASS1 應(yīng)答: www.demo.local TYPE5 (CNAME) TTL300 - cdn.demo.local 應(yīng)答: www.demo.local TYPE1 (A) TTL60 - 203.0.113.7 getaddrinfo(localhost) 本機解析器返回順序 localhost - ::1 (familyIPv6) localhost - 127.0.0.1 (familyIPv4)三個觀察點。查詢報文只有 32 字節(jié)——域名www.demo.local占了 16 字節(jié)的標簽編碼3www4demo5local0這就是DNS 報文又小又密的體感也是壓縮指針存在的理由。應(yīng)答里兩條記錄的 NAME 字段都只有 2 個字節(jié)0xC00C解析器卻還原出完整域名——Wireshark 里看到的Name: www.demo.local (used 1 time)灰字就是它。而getaddrinfo的返回順序里::1排在127.0.0.1前應(yīng)用按這個順序試連接如果站點的 IPv6 配置是壞的AAAA 有記錄但 v6 路徑不通用戶就會經(jīng)歷先等 v6 超時再回落 v4的固定幾秒卡頓——雙棧排障的正確姿勢是分別curl -4與curl -6對比而不是懷疑網(wǎng)站整體慢。工程化改進把解析慢/解析錯變成一條可執(zhí)行的排查流水線。第一步固定四段計時。curl -s -o /dev/null -w namelookup%{time_namelookup} connect%{time_connect} total%{time_total}\n https://域名/先看 namelookup 占多少再nslookup 域名 指定LocalDNS、dig 域名 223.5.5.5 short與dig 域名 運營商LDNS short對比同一時刻不同遞歸器給的答案——答案不一致就是某級緩存存著舊值或被污染最后dig 域名 trace繞開所有 LocalDNS 從根走一遍迭代鏈看權(quán)威本身對不對。三段答案兩兩對比故障層立刻定位trace 對、LDNS 錯 → 遞歸器緩存問題trace 就錯 → 區(qū)配置問題改 SOA serial、NS 委派。第二步變更 IP 前先降 TTL。所有 DNS 生效問題都是TTL 已經(jīng)發(fā)出去的收不回遷移/切流的標準動作是提前 24 小時把記錄 TTL 從 86400 降到 60切完觀察各層無舊緩存后再恢復(fù)。Windows 查 OS 緩存用ipconfig /displaydns、清緩存ipconfig /flushdnsLinux 對應(yīng)systemd-resolve --statistics或 nscd但記住這只清OS 層LocalDNS 的緩存你清不掉——所以驗收要用dig 權(quán)威與dig LDNS雙口徑。第三步認清 hosts 與加密 DNS 的優(yōu)先級鏈。hosts 文件在操作系統(tǒng)解析器里先于 DNS 生效這是本地劫持測試的基礎(chǔ)但 Chrome/Edge 開了安全 DNSDoH時會繞過系統(tǒng)解析器直連指定的 DoH 服務(wù)器——企業(yè)環(huán)境要下發(fā)策略統(tǒng)一關(guān)閉或統(tǒng)一指定內(nèi)網(wǎng) DoH否則 hosts 里的映射有時生效有時不生效的玄學(xué)就是這么來的。同理App 端 SDK如 OkHttp/各類 HTTPDNS可以完全不走系統(tǒng) DNS 用 HTTP 接口查 IP網(wǎng)絡(luò)抓包看不到 DNS 請求不代表沒解析。第四步權(quán)威側(cè)用云解析就要盯它的調(diào)度模型。CDN 的就近調(diào)度依賴誰在替你做遞歸——運營商 LDNS 出口決定你被分到的邊緣節(jié)點開啟了 EDNS-ECS 時精度更高。排用戶被調(diào)度到千里之外的問題先看解析返回的 CNAME 鏈dig noall answer 域名是否按預(yù)期逐級跳轉(zhuǎn)再用不同地區(qū)探測點對比返回 IP 段。CNAME 套 CNAME 超過幾級后部分舊解析器會出兼容問題記錄規(guī)劃時盡量壓平。第二次代碼實驗及輸出下面用邏輯時鐘tick單位秒完全手動推進模擬一個遞歸解析器NS 委派表 每 RRset 獨立 TTL 的緩存??慈齻€現(xiàn)象首次解析如何從根三連問、學(xué)到委派后如何直達權(quán)威、以及同一個域名的 A 記錄過期而 CNAME 還活著為什么是常態(tài)而非 bug。# 確定性模擬: 迭代解析鏈 按 RRset 緩存 TTL(邏輯時鐘, 不依賴真實時間)SERVERS{root:{refers:{local:tld-ns.local}},tld-ns.local:{refers:{demo.local:ns1.demo.local}},ns1.demo.local:{answers:{www.demo.local:{CNAME:(300,[cdn.demo.local]),A:(60,[203.0.113.7,203.0.113.8]),}}},}GLUE{tld-ns.local:198.41.0.4,ns1.demo.local:192.0.2.10}NSC{:root}# 已學(xué)到的區(qū) - NS 服務(wù)器(負緩存同理, 此處從簡)cache{}# (name, type) - (expire_tick, records)tick0# 邏輯秒defask(server,name,rtype):nodeSERVERS[server]ansnode.get(answers,{}).get(name,{})ifrtypeinans:return(answer,)ans[rtype]forlabels,nsinnode.get(refers,{}).items():ifname.endswith(labels):return(referral,labels,ns)return(referral,None,None)defresolve(name,rtype):globaltick key(name,rtype)ifkeyincacheandcache[key][0]tick:print( [%3ds] 緩存命中 %s/%s - %s (剩余TTL %ds)%(tick,name,rtype,,.join(cache[key][1]),cache[key][0]-tick))returnzonemax((zforzinNSCifname.endswith(z)),keylen)serverNSC[zone]whileTrue:rask(server,name,rtype)ifr[0]answer:ttl,recordsr[1],r[2]cache[key](tickttl,records)print( [%3ds] 問 %-14s - 權(quán)威應(yīng)答 %s/%s%s, 緩存 TTL%ds%(tick,server,name,rtype,,.join(records),ttl))return_,labels,nsrprint( [%3ds] 問 %-14s - 委派 %s 區(qū): NS%s (glue A%s)%(tick,server,labels,ns,GLUE[ns]))NSC[labels]ns servernsprint( t0 首次解析 www.demo.local CNAME: 從根迭代三連問 )resolve(www.demo.local,CNAME)print( t0 再解析 A: 已學(xué)到 demo.local 區(qū)的 NS, 直達權(quán)威 )resolve(www.demo.local,A)print( t30 再查 A: 命中緩存 )tick30resolve(www.demo.local,A)print( t61: A 的 60s TTL 已過期, 但 CNAME 的 300s 仍在 )tick61resolve(www.demo.local,CNAME)resolve(www.demo.local,A)print( 緩存賬本: 每個 (名字,類型) 是獨立 RRset, 各自到期 )for(n,t),(exp,val)insorted(cache.items()):print( %-28s 過期于 t%3d 記錄%s%(%s/%s%(n,t),exp,,.join(val)))運行輸出 t0 首次解析 www.demo.local CNAME: 從根迭代三連問 [ 0s] 問 root - 委派 local 區(qū): NStld-ns.local (glue A198.41.0.4) [ 0s] 問 tld-ns.local - 委派 demo.local 區(qū): NSns1.demo.local (glue A192.0.2.10) [ 0s] 問 ns1.demo.local - 權(quán)威應(yīng)答 www.demo.local/CNAMEcdn.demo.local, 緩存 TTL300s t0 再解析 A: 已學(xué)到 demo.local 區(qū)的 NS, 直達權(quán)威 [ 0s] 問 ns1.demo.local - 權(quán)威應(yīng)答 www.demo.local/A203.0.113.7,203.0.113.8, 緩存 TTL60s t30 再查 A: 命中緩存 [ 30s] 緩存命中 www.demo.local/A - 203.0.113.7,203.0.113.8 (剩余TTL 30s) t61: A 的 60s TTL 已過期, 但 CNAME 的 300s 仍在 [ 61s] 緩存命中 www.demo.local/CNAME - cdn.demo.local (剩余TTL 239s) [ 61s] 問 ns1.demo.local - 權(quán)威應(yīng)答 www.demo.local/A203.0.113.7,203.0.113.8, 緩存 TTL60s 緩存賬本: 每個 (名字,類型) 是獨立 RRset, 各自到期 www.demo.local/A 過期于 t121 記錄203.0.113.7,203.0.113.8 www.demo.local/CNAME 過期于 t300 記錄cdn.demo.local這份賬本回答了改了記錄多久生效的機理緩存鍵是(名字, 類型)而不是域名——t61 時同一個www.demo.localCNAME 還在緩存里躺著 239 秒A 記錄卻已經(jīng)過期重查了?,F(xiàn)實里這精確對應(yīng)一個高頻事故你把記錄從 A 改成 CNAME或反向舊記錄按舊 TTL 繼續(xù)供給新舊記錄并存期間行為取決于解析器先撞上哪個過期點——所以變更窗口內(nèi)新舊兩種類型都可能被不同用戶看到應(yīng)用必須容忍。同樣地NS 委派表學(xué)到一次后續(xù)直達模型里第二次解析只問了一站真實遞歸器對委派與負答案NXDOMAIN也各自有 TTL負緩存取 SOA 的 minimum打錯的域名也會被緩存 NXDOMAIN 一陣子——“我明明改好了怎么還說不存在”問的就是它。常見陷阱其一把清 OS 緩存當成全鏈路生效四層緩存里你只能清自己機器那兩層驗證必須用dig LDNS與dig trace雙口徑。其二只留 AAAA 不配 IPv6 出口雙棧優(yōu)先 v6路徑不通就吃掉 Happy Eyeballs 的回落超時癥狀是部分用戶每次都慢 1-5 秒不確定就刪掉 AAAA或上真正的 v6。其三CNAME 指向別的 CDN 后又加 A同一名字同時有 CNAME 與其他記錄違反 RFC 1034權(quán)威可能拒發(fā)或行為未定義想多路就用子域拆分。其四以為公共 DNS更干凈阿里 223.5.5.5/騰訊 DNSPod/1.1.1.1 各用各的遞歸出口對 CDN 調(diào)度返回的邊緣節(jié)點可能不是你用戶所在的區(qū)域企業(yè)內(nèi)網(wǎng)統(tǒng)一 LocalDNS 反而是調(diào)度準確性的保障。其五忽略 UDP 截斷路徑響應(yīng)超包TXT 記錄堆太多、DNSSEC 簽名鏈太長時部分網(wǎng)絡(luò)丟棄大 UDP 包且不回 TC表現(xiàn)為這個域名的 TXT/CAA 查詢偶爾超時dig tcp一試便知。其六改 hosts 測試后忘了 DoH 的存在或 Chrome 升級悄悄開啟使用安全 DNS本地映射失效還懷疑文件沒保存。落地清單排障四板斧curl -w time_namelookup計時 →dig 各家LDNS對比答案 →dig trace驗權(quán)威 →dig tcp排除截斷變更/切流 SOP提前 24h 降 TTL 到 60 → 切換 → 雙口徑驗證 → 恢復(fù) TTL服務(wù)器鏡像里固化ipconfig /flushdns或 systemd-resolved flush-caches進排障手冊瀏覽器 DoH 策略企業(yè)統(tǒng)一下發(fā)權(quán)威區(qū)文件三件套常查SOA serial 是否遞增、NS 委派與區(qū)內(nèi)容是否一致、TTL 梯度是否合理雙棧站點定期curl -4/-6撥測AAAA 記錄與 v6 路由健康度聯(lián)動下線監(jiān)控加一條解析答案變更事件權(quán)威返回的 IP 集合突然變化往往是劫持或誤操作的黃金信號域名能變成 IP、報文結(jié)構(gòu)能看懂了冷啟動的四段賬DNS/TCP/TLS/HTTP就只剩最后一塊拼圖沒拆應(yīng)用層什么時候能開始推數(shù)據(jù)而不是拉下一篇《網(wǎng)絡(luò)協(xié)議實戰(zhàn)6WebSocket 與長連接實戰(zhàn)》我們從 HTTP 的 Upgrade 握手講到幀格式與心跳?;睢槺惆训谝黄乃拇螕]手在長連接上的賬一次算清。參考來源RFC 1035: Domain Names - Implementation and Specification: https://datatracker.ietf.org/doc/html/rfc1035RFC 3596: DNS Extensions to Support IP Version 6: https://datatracker.ietf.org/doc/html/rfc3596RFC 6891: Extension Mechanisms for DNS (EDNS(0)): https://datatracker.ietf.org/doc/html/rfc6891RFC 8484: DNS Queries over HTTPS (DoH): https://datatracker.ietf.org/doc/html/rfc8484WikipediaDomain Name System: https://en.wikipedia.org/wiki/Domain_Name_System 覺得有用就點個贊 收藏方便回頭查閱有疑問直接在評論區(qū)留言我看到都會回。 本文屬于《網(wǎng)絡(luò)協(xié)議實戰(zhàn)》系列持續(xù)更新關(guān)注不迷路。 文章里的代碼都能直接跑。想要可直接 clone 的完整工程 配套部署腳本 / 踩坑清單評論一聲或發(fā)郵件到cj2664qq.com我免費發(fā)你。如果你正好在做類似系統(tǒng)、或有工程化難題想找人做也歡迎郵件聊一句——我按實際情況評估能落地的就接單或出方案。評論和郵件都能直接找到我不用跳別的平臺。