真實IP透傳?PROXY Protocol實戰(zhàn)指南)
1. 私有云K8s里的“IP謎局”為什么Nginx看到的總是內(nèi)網(wǎng)地址先描述一個我常遇到的場景私有云K8s集群部署好之后前面架了一臺HAProxy做統(tǒng)一公網(wǎng)入口業(yè)務(wù)跑在Nginx Ingress和一堆后端Pod里。上線前功能全通上線后業(yè)務(wù)方跑來提了個很微妙的問題——訪問日志里看到的客戶端IP全是內(nèi)網(wǎng)地址要么是某個Node節(jié)點IP要么是Pod網(wǎng)段的地址審計沒法做限流也全亂了。這個問題為什么會存在得從流量路徑說起。通常一個私有云K8s集群的訪問鏈路是客戶端 → 邊界HAProxy → K8s任意Node的NodePort → 調(diào)度到某個Ingress Controller Pod → 再轉(zhuǎn)發(fā)給業(yè)務(wù)Pod。鏈路里有兩個“斷點”。第一個斷點在HAProxy到NodePort這一段。如果HAProxy用的是mode tcp四層轉(zhuǎn)發(fā)它只負(fù)責(zé)把TCP流原封不動地搬到后端端口既不解析HTTP頭也不往里面塞任何客戶端信息。TCP流到了NodePortKube-proxy還要做一次DNAT/SNAT把數(shù)據(jù)包轉(zhuǎn)發(fā)給Ingress Controller Pod。于是Ingress收到的TCP連接源地址已經(jīng)是Node節(jié)點的內(nèi)網(wǎng)IP了。更麻煩的是如果集群用了多網(wǎng)卡、多個Node做NodePort你甚至可能看到源地址是某一臺機器上的VIP或網(wǎng)橋地址。第二個斷點在Ingress Controller到業(yè)務(wù)Pod這一段。Nginx Ingress本身是個反向代理它接收到請求后會用一個新的TCP連接把請求轉(zhuǎn)發(fā)給后端業(yè)務(wù)Pod。這個新連接的源地址是Ingress Pod自己的IP。業(yè)務(wù)應(yīng)用如果不做任何處理直接讀Nginx變量里的$remote_addr拿到的就是Ingress Pod網(wǎng)段里的某個地址和真實客戶端毫無關(guān)系。有不少人第一反應(yīng)是“那我去讀HTTP頭里的X-Forwarded-For不就行了”。這個想法方向?qū)Φ珜嵅倮锟雍芏?。X-Forwarded-For是標(biāo)準(zhǔn)HTTP頭可它有兩個致命問題第一它能不能出現(xiàn)、值是什么完全取決于鏈路上的代理有沒有主動寫如果整個鏈路都是四層轉(zhuǎn)發(fā)這個頭可能壓根不存在。第二它是可以被客戶端偽造的。隨便一個懂HTTP的人都能在請求里預(yù)先塞一個假的X-Forwarded-For: 1.2.3.4如果你沒有任何信任鏈校驗就信了這個值等于把訪問控制和安全審計直接暴露給攻擊者。所以解決這件事的完整思路不是“在應(yīng)用里讀某個頭”而是從鏈路入口開始用協(xié)議機制把真實客戶端IP一級一級傳遞下來每一級都只信任自己上游那個代理最終形成一條完整的信任鏈。有一個專門干這事的協(xié)議叫PROXY Protocol最早是負(fù)載均衡軟件社區(qū)搞出來的現(xiàn)在HAProxy和Nginx都原生支持。它的原理是在TCP連接建立之后、正式業(yè)務(wù)數(shù)據(jù)發(fā)送之前先插一段特殊格式的頭部里面寫明這條連接的真實源IP、源端口、目標(biāo)IP、目標(biāo)端口。四層轉(zhuǎn)發(fā)因此也能攜帶原始客戶端地址了。整個鏈路再加上IP透傳之后大致長這樣客戶端瀏覽器 │ ▼ 公司邊界HAProxy監(jiān)聽80/443 │ L4 TCP流 PROXY Protocol幀 ▼ K8s任意Node的NodePort如30080/30443 │ Kube-proxy調(diào)度轉(zhuǎn)發(fā) ▼ Nginx Ingress Controller Pod解析PROXY幀重寫remote_addr │ HTTP請求 X-Forwarded-For ▼ 業(yè)務(wù)應(yīng)用Pod讀取remote_addr或XFF下面每一層要做什么、怎么配、怎么驗證我按實戰(zhàn)順序拆開講。2. 先從HAProxy動手四層透傳和七層改寫怎么選2.1 方案A四層TCP PROXY Protocol推薦的大頭方案大部分私有云場景我建議把HAProxy作為純粹的TCP負(fù)載均衡器來用業(yè)務(wù)側(cè)的七層能力全交給Nginx Ingress。這樣HAProxy不解析HTTP、不終止TLS、不做任何應(yīng)用層判斷轉(zhuǎn)發(fā)性能最高也最穩(wěn)定。在HAProxy上的配置大概是這樣的。假設(shè)后端兩個Node節(jié)點NodePort選了30080和30443frontend fe_k8s_http bind *:80 mode tcp option tcplog default_backend be_k8s_ingress_http backend be_k8s_ingress_http mode tcp balance roundrobin server k8s-node1 192.168.1.11:30080 send-proxy-v2 check inter 3s rise 2 fall 3 server k8s-node2 192.168.1.12:30080 send-proxy-v2 check inter 3s rise 2 fall 3frontend fe_k8s_https bind *:443 mode tcp option tcplog default_backend be_k8s_ingress_https backend be_k8s_ingress_https mode tcp balance roundrobin server k8s-node1 192.168.1.11:30443 send-proxy-v2 check inter 3s rise 2 fall 3 server k8s-node2 192.168.1.12:30443 send-proxy-v2 check inter 3s rise 2 fall 3關(guān)鍵就在send-proxy-v2這個參數(shù)上。它表示HAProxy向本后端轉(zhuǎn)發(fā)TCP流時要在業(yè)務(wù)數(shù)據(jù)前插入一個PROXY Protocol v2二進制幀里面帶這條連接的真實源IP和端口。2.2 send-proxy和send-proxy-v2到底差在哪這里插一個容易踩坑的細節(jié)。HAProxy里有兩個非常像的參數(shù)send-proxy和send-proxy-v2。前者發(fā)送PROXY v1文本幀后者發(fā)送PROXY v2二進制幀。v1長什么樣是一條ASCII文本開頭是PROXY TCP4 源IP 目標(biāo)IP 源端口 目標(biāo)端口\r\n這樣的可讀格式。v2則是二進制專門設(shè)計成機器可讀開頭有固定的魔數(shù)標(biāo)記解析效率更高也能攜帶更多TLV擴展信息。Nginx 1.9.2以后兩者都支持所以大多數(shù)情況下用哪個都不會出錯。我的習(xí)慣是統(tǒng)一用v2。原因很實際私有云里這套鏈路已經(jīng)夠多了每次抓包排查都靠肉眼去看文本幀太累v2結(jié)構(gòu)規(guī)范、解析明確兩側(cè)版本朝一個方向?qū)R以后升級維護少一事。2.3 方案B七層HTTP X-Forwarded-For的適用場景也有一些情況需要讓HAProxy直接做七層。比如邊界就要按域名分流、要做簡單的ACL放行或者集群里不用Nginx Ingress而是一組獨立Nginx實例直接對外。這種時候四層的PROXY Protocol鏈路就沒法直接用因為HAProxy自己已經(jīng)消費了HTTP流你需要在轉(zhuǎn)發(fā)時主動注入客戶端信息。HAProxy七層配置可以這樣frontend fe_k8s_http bind *:80 mode http option httplog http-request set-header X-Real-IP %[src] http-request set-header X-Forwarded-For %[src] default_backend be_k8s_ingress_http backend be_k8s_ingress_http mode http server k8s-node1 192.168.1.11:30080 check inter 3s rise 2 fall 3 server k8s-node2 192.168.1.12:30080 check inter 3s rise 2 fall 3這里用%[src]直接把HAProxy看到的客戶端源IP寫進X-Real-IP和X-Forwarded-For。但注意X-Forwarded-For的值會被Nginx后面用$proxy_add_x_forwarded_for繼續(xù)追加。如果你不加處理TLS終止時、反復(fù)代理時這個頭里的地址會越來越長、越疊越亂解析時就得多做一層清洗。還有另一個細節(jié)如果你在HAProxy七層改寫里只是簡單set-header會直接覆蓋客戶端原本可能帶的XFF頭。這有時是好事防偽造但如果你還需要原始XFF做鏈路分析就得換寫法。我不能給出一個普適結(jié)論只能說生產(chǎn)環(huán)境一般按“只相信邊界代理注入的那一個值”來設(shè)計最穩(wěn)妥。2.4 兩種方案怎么選給個參考表維度四層TCP PROXY Protocol七層HTTP XFF改寫HAProxy轉(zhuǎn)發(fā)性能高不解析HTTP低一些每個連接都要解析應(yīng)用層TLS終止位置在Nginx Ingress可放在HAProxy也可放Ingress真實IP準(zhǔn)確性很高幀內(nèi)明確攜帶源IP高但依賴鏈路一致且信任邊界清晰配置復(fù)雜度中HAProxy和Ingress都要開中低但XFF清洗麻煩典型適用場景私有云K8s Nginx Ingress大部分生產(chǎn)邊界已有L7需求、非Ingress的Nginx集群我自己的經(jīng)驗是一旦決定未來由Nginx Ingress統(tǒng)一負(fù)責(zé)七層邏輯就直奔方案A。一次把HAProxy改成mode tcp send-proxy-v2后面所有服務(wù)都省心。3. Nginx Ingress層配置listen proxy_protocol與信任邊界3.1 讓Ingress Controller接收PROXY幀HAProxy改完只是發(fā)出去接收側(cè)不認(rèn)賬等于白干。Nginx Ingress Controller默認(rèn)監(jiān)聽80端口時不會解析PROXY幀你不改它會把這幀當(dāng)作普通HTTP請求來解析直接報400或者掛斷連接。這就是為什么不少人改完HAProxy后馬上看到端口全線Down。需要改的是Ingress Controller的啟動ConfigMap。如果通過Helm安裝最方便的是在values里寫入controller: config: proxy-protocol: true set-real-ip-from: 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12如果你已經(jīng)用kubectl裸部署就編輯對應(yīng)的ConfigMap名字一般是ingress-nginx-controller位于ingress-nginx命名空間kubectl -n ingress-nginx edit configmap ingress-nginx-controller在那里的data段加上data: proxy-protocol: true set-real-ip-from: 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12數(shù)據(jù)里的proxy-protocol: true會讓Nginx在listen指令里加上proxy_protocol參數(shù)。set-real-ip-from則聲明只有當(dāng)連接源地址處于這些內(nèi)網(wǎng)網(wǎng)段時才解析PROXY幀并把其中攜帶的客戶端IP視為真實IP。這兩個參數(shù)必須在一起否則你等于無條件信任所有連接上來的客戶端都能隨便自報IP。改完之后驗證一下是否真的生效。執(zhí)行kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- cat /etc/nginx/nginx.conf | grep -A 10 listen 80如果看到類似server { listen 80 proxy_protocol;那就說明listen層已經(jīng)掛上了。3.2 信任鏈的坑use-forwarded-headers到底該不該開這里必須展開講一個很多人搞混的配置項use-forwarded-headers。Nginx Ingress Controller的ConfigMap里經(jīng)常被建議加use-forwarded-headers: true它的作用是讓Nginx把請求頭里的X-Forwarded-For當(dāng)作客戶端真實IP來源并更新$remote_addr。在“前端是一臺七層云負(fù)載均衡且那臺設(shè)備已經(jīng)重寫了XFF”的場景下這是合理的。但在HAProxy四層PROXY Protocol這套鏈路里我非常不建議隨便開這個開關(guān)。原因很直接如果開啟Nginx會優(yōu)先根據(jù)傳入請求里的XFF來重置remote_addr而XFF是可以由客戶端自己預(yù)埋偽造值的。雖然PROXY幀里帶了真實IP但XFF優(yōu)先參與解析之后真實IP反而容易被假值蓋掉。你辛辛苦苦把信任鏈建好最后卻主動把最不可信的一個頭當(dāng)成依據(jù)等于安全防線白做。所以我的生產(chǎn)推薦組合是HAProxy側(cè)四層TCP send-proxy-v2Ingress側(cè)proxy-protocol: trueset-real-ip-from: 內(nèi)網(wǎng)網(wǎng)段use-forwarded-headers不開啟或僅在你能確認(rèn)邊界HAProxy已經(jīng)用七層方式重寫XFF時才開啟這套組合下真實來源信息由PROXY幀承擔(dān)Nginx的$remote_addr會被重置為客戶端公網(wǎng)IP。而在繼續(xù)向后端轉(zhuǎn)發(fā)時Nginx默認(rèn)會為上游連接生成或追加X-Forwarded-For頭里面帶上這個真實IP所以業(yè)務(wù)Pod依然有標(biāo)準(zhǔn)頭可讀。3.3 業(yè)務(wù)Pod側(cè)應(yīng)該讀什么到了業(yè)務(wù)應(yīng)用這一層很多人又會糾結(jié)到底讀$remote_addr還是讀X-Forwarded-For我一般建議業(yè)務(wù)統(tǒng)一讀標(biāo)準(zhǔn)頭X-Forwarded-For并且按“從右往左取第一個非可信內(nèi)網(wǎng)地址”的規(guī)則解析。因為Nginx Ingress在向業(yè)務(wù)轉(zhuǎn)發(fā)時會在已有的XFF基礎(chǔ)上追加它看到的remote_addr而這個remote_addr經(jīng)PROXY幀重置后就是客戶端真實IP。規(guī)整的鏈路里XFF最后一段通常就是客戶端。直接讀$remote_addr在以后鏈路變動、插一層代理時又會拿到內(nèi)網(wǎng)地址編碼層面要盡量避免這種“源地址硬綁定”。4. 端到端驗證讓IP從HAProxy一路亮到業(yè)務(wù)日志配置改完別急著上線。我建議先做一輪端到端驗證確認(rèn)每一級拿到的IP確實對。驗證方法很多我按成本從低到高說。4.1 第一種從業(yè)務(wù)Pod日志驗證先找一個后端業(yè)務(wù)比如典型的Nginx應(yīng)用確認(rèn)它的訪問日志格式里有$remote_addr。然后從公網(wǎng)側(cè)發(fā)起一個請求比如在外部機器上執(zhí)行curl -k https://svc.example.com/ --resolve svc.example.com:443:你的HAProxy公網(wǎng)VIP發(fā)起后立刻去看業(yè)務(wù)Pod日志kubectl logs -n 命名空間 deploy/業(yè)務(wù)Deployment --tail20如果鏈路配置正確你會看到類似192.0.2.100 - - [17/Feb/2025:10:20:33 0800] GET / HTTP/1.1 200 612 - curl/8.0這里的192.0.2.100就是你測試客戶端的公網(wǎng)IP。在透傳之前這個位置大概率是一段10.244.x.x或192.168.x.x的內(nèi)網(wǎng)地址??吹焦W(wǎng)IP出現(xiàn)說明鏈路已經(jīng)通了。4.2 第二種查看Nginx Ingress Controller日志Ingress Controller自己的訪問日志也可以驗證。執(zhí)行kubectl -n ingress-nginx logs -l app.kubernetes.io/nameingress-nginx --tail50里面同樣會把remote_addr打進日志。注意Ingress Controller的日志默認(rèn)包含一堆健康檢查、404探測建議用請求路徑過濾一下或者加一個帶唯一參數(shù)的路徑來定位。4.3 第三種抓包確認(rèn)PROXY幀如果前面兩種發(fā)現(xiàn)IP沒透傳成功抓包是最快的排障方式。在Ingress Controller Pod里抓NodePort對應(yīng)的端口比如kubectl -n ingress-nginx exec deploy/ingress-nginx-controller -- tcpdump -ni eth0 tcp port 80 -c 5如果HAProxy正常發(fā)出PROXY幀你會看到TCP流里第一條數(shù)據(jù)不是GET / HTTP/1.1而是PROXY協(xié)議特有的幀頭。v1文本幀會以PROXY開頭v2二進制幀開頭有\(zhòng)x0D\x0A加QUIT之類的魔數(shù)??吹竭@些說明HAProxy確實發(fā)出、Ingress也確實收到了。如果只看到GET / HTTP/1.1而前面沒有PROXY幀基本可以斷定HAProxy那側(cè)配置沒生效或者后端端口指錯了。4.4 用回顯服務(wù)做一次干凈驗證我習(xí)慣在集群里臨時起一個極簡回顯服務(wù)直接返回HTTP頭里傳入的源信息這樣能快速排除業(yè)務(wù)代碼干擾。思路是部署一個小應(yīng)用在響應(yīng)里輸出Node環(huán)境變量和請求頭。也可以用現(xiàn)成的httpbin鏡像起一個Deployment、Service、Ingress然后從公網(wǎng)訪問并觀察返回體里的頭信息。步驟是部署回顯服務(wù) → 從公網(wǎng)curl訪問 → 在回顯結(jié)果里檢查X-Forwarded-For的最后一個值。如果它不是內(nèi)網(wǎng)地址而是你的公網(wǎng)IP整個鏈路已經(jīng)驗證通過。這種一次性的驗證服務(wù)很值得維護成標(biāo)準(zhǔn)做法以后每次改負(fù)載均衡配置都能先拿它探路。5. 線上踩過的坑與生產(chǎn)配置建議5.1 只改一頭另一頭還在等普通HTTP最常見的事故是HAProxy側(cè)已經(jīng)加上了send-proxy-v2但Ingress Controller的ConfigMap沒改或者改完沒有生效。結(jié)果就是Nginx本來在等一個HTTP請求頭結(jié)果先收到了一段PROXY開頭的幀直接判定為非法請求連HTTP協(xié)議解析都過不去日志里時不時出現(xiàn)端口Down、502/400。排查方法很簡單第一件事永遠是看Ingress Controller的listen配置里有沒有proxy_protocol。有就往HAProxy方向確認(rèn)沒有就先改Ingress側(cè)。5.2 健康檢查通道被PROXY幀沖垮HAProxy后端默認(rèn)會做健康檢查普通的TCP check是建立連接就斷開不會發(fā)PROXY幀。但Nginx Ingress既然開啟了proxy_protocol它會在每次TCP連接建立后等待PROXY幀如果等不到就判定異常健康檢查自然全軍覆沒。表現(xiàn)是HAProxy面板里后端全紅但用瀏覽器訪問有時又能通。我的做法是健康檢查不直接探測NodePort的80/443而是探測Nginx Ingress Controller自帶的管理端口10254。在HAProxy里給后端單獨定義一條健康檢查比如server k8s-node1 192.168.1.11:30080 send-proxy-v2 check port 10254 inter 3s rise 2 fall 3這樣檢查端口與業(yè)務(wù)轉(zhuǎn)發(fā)端口分離業(yè)務(wù)流走80換80健康探針走10254兩不干擾。5.3 多層代理時信任鏈被折疊有些環(huán)境里HAProxy前面還有一臺云平臺自帶的負(fù)載均衡或者是機房硬件負(fù)載均衡。這種多層代理下如果你只在HAProxy上配了PROXY協(xié)議上層設(shè)備不認(rèn)識根本沒把客戶端IP傳下來你在HAProxy里看到的源地址就是上層設(shè)備的內(nèi)網(wǎng)地址后面的一切都建立在錯誤源頭上。要先確認(rèn)最外層設(shè)備支不支持轉(zhuǎn)發(fā)PROXY協(xié)議。支持就逐層配置每層都要聲明信任和啟用協(xié)議不支持就只能退到七層XFF方案在HAProxy里強制set-header注入XFF同時保證最外層設(shè)備也覆蓋XFF。最怕的是每層都在轉(zhuǎn)發(fā)但誰都沒真正把真實客戶端IP寫進去最后一層只是在給自己編故事。5.4 禁止公網(wǎng)直接訪問NodePort這套鏈路成立的前提是“只有你信任的HAProxy能連到NodePort”。如果NodePort直接暴露在公網(wǎng)上任何繞過HAProxy的請求都能直連Ingress而且你還開啟了proxy-protocol那一個聰明的攻擊者完全可以偽造PROXY幀來偽裝任意IP。所以云主機安全組、機房防火墻里一定要限制NodePort只允許HAProxy所在網(wǎng)段訪問絕不暴露公網(wǎng)。這是整條鏈路的底線。5.5 配置變更順序與回滾預(yù)案改這種跨節(jié)點的鏈路配置最忌諱一下子全量上。我踩過一次最疼的坑是在業(yè)務(wù)高峰期同時刷新HAProxy和Ingress配置結(jié)果流量涌入的瞬間后端大量5xx。后來形成習(xí)慣先改Ingress側(cè)讓它可以接收PROXY幀再用HAProxy的disable server逐個摘掉后端驗證一輪沒問題再加回去。每一步控制在分鐘級出問題隨時回滾。說到底IP透傳這件事并不難難的是鏈路里每一層都對“自己是第幾個可信代理、下一層該相信什么”有共識。PROXY Protocol解決的是傳輸層攜帶源IP的問題XFF解決的是應(yīng)用層攜帶源IP的問題兩者配合起來再從HAProxy到Ingress到業(yè)務(wù)Pod逐級驗證一次后面就不用再為“為什么日志全是內(nèi)網(wǎng)IP”頭疼了。