坑系列之阿里SLB上獲取客戶IP
好久沒更新了,正好上週遇到一個獲取不到客戶端IP的BUG,開發環境用nginx做反代都是work的。上到生產環境就獲取不到。思來想去就是生產上多了一個SLB負載均衡。但這是一個老的功能,之前也都是好的,突然就拿的不對了,非常之詭異。
故障重現
為了確認不是程式碼的問題,我們使用tcpdump在服務結點上抓包。
GET /api/foo/bar HTTP/1.1 remoteip: 122.xx.xx.xx x-forwarded-for: 122.xx.xx.xx, 10.130.0.1 accept: application/json, text/plain, */* dnt: 1 user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36 referer: https://app.example.com/login accept-language: zh-CN,zh;q=0.9 x-forwarded-host: app.example.com,app.example.com x-forwarded-port: 80,80 x-forwarded-proto: http,http x-request-id: d74323d45afd4609977eb233d59f9a9e x-trace-id: d74323d45afd4609977eb233d59f9a9e x-real-ip: 10.130.0.1 x-locale: zh_CN host: app.example.com Accept-Encoding: gzip Content-Length: 0 Connection: Keep-Alive
發現報文頭的X-Real-IP是一個VPC的內網地址,說明在我們的nginx中獲取的 $remote_addr
就是 10.130.0.1
,是SLB的地址。
nginx配置如下:
http { server { listen80; server_name192.168.50.88; root/usr/local/var/www/html; location /api { proxy_set_header X-Real-IP $remote_addr;#將remode_addr寫入Http Header proxy_pass http://backend_hosts; } } upstream backend_hosts { server 127.0.0.1:8080; } }
配置很簡單,沒有使用realip模組,直接將 remote_addr
認定為客戶端ip。大家知道 remote_addr
不是http頭,不容易偽造。它是服務端與客戶端建立socket連線時,從客戶端直接獲取的。但是為什麼這裡獲取的ip卻是SLB自身的IP呢?
故障分析
再仔細分析抓包內容,發現其實報文中是包含客戶端原始IP的,分別在 x-forwarded-for
和 remoteip
上。這裡 x-forwarded-for
的值引起了我們的注意,如果是用nginx原始的 $proxy_add_x_forwarded_for
引數,客戶端IP應該會放在最後,但是這裡在第一位,說明SLB對這個頭做過處理。
找到devops詢問是否更改過SLB的配置,發現確實做過調整。為了直接在SLB實現http到https的重定向,將原本的4層負載均衡(tcp)換成了7層負載均衡(http)。試著將SLB恢復原有配置,可以獲取客戶端IP。最終問題定位到SLB的配置上。
再次閱讀 SLB手冊 ,發現以下描述:
負載均衡提供獲取客戶端真實IP地址的功能,該功能預設是開啟的。
四層負載均衡(TCP協議)服務可以直接在後端ECS上獲取客戶端的真實IP地址,無需進行額外的配置。
七層負載均衡(HTTP/HTTPS協議)服務需要對應用伺服器進行配置,然後使用X-Forwarded-For的方式獲取客戶端的真實IP地址。
真實的客戶端IP會被負載均衡放在HTTP頭部的X-Forwarded-For欄位,格式如下:
X-Forwarded-For: 使用者真實IP, 代理伺服器1-IP, 代理伺服器2-IP,…
當使用此方式獲取客戶端真實IP時,獲取的第一個地址就是客戶端真實IP。
檢視SLB配置頁面確實也如文件所說
至於 remote_addr
獲取到SLB的IP也就很容易理解了,當沒有上級代理沒有透傳tcp連線時, remote_addr
獲取的就是上一層代理的ip地址。
故障恢復
既然定位到問題了,那麼需要著手解決,改回4層LB是不現實的。
阿里雲其實提供了兩個方案:
- 按照文件上說的,獲取X-Forwarded-For的第一段IP即為客戶真實IP
- 通過抓包發現SLB會新增一個remoteip的頭,直接使用就行
我們偷個懶,直接用第二種,在nginx將remoteip塞到X-Real-IP上,這樣不用打hotfix即可修復問題。
花絮
其實在故障恢復的過程中,本想在本地復現的。過程就是用nginx搭建一個4層負載代理到7層負載最終到服務。如下圖所示
+------------------++----------------++---------------++----------------+ |||||||| |||||||| |Client+------>TCP LB+----->+HTTP LB+---->SERVER| |||||||| |||||||| +------------------++----------------++---------------++----------------+
tcp負載的配置如下:
stream { upstream tcp_proxy { server 127.0.0.1:80; } server { listen 88; proxy_connect_timeout 1s; proxy_timeout 300s; proxy_pass tcp_proxy; } }
最終發現nginx的tcp代理有個巨大的坑,就是無法透傳 remote_addr
,如果tcp代理跳過http直連服務,獲取到的remote_addr就是127.0.0.1這個本機地址。
翻了翻文件,發現還真有 官方說明
簡而言之,就是要買nginx-plus,裡面有個 proxy_bind $remote_addr transparent;
可以實現透傳功能,滿滿的套路。
總結
DevOps有的時候真的會影響到業務,不同環境不同配置造成難以預料的影響。雖然我們的服務都已經實現了容器化。但是對於這些PaaS元件如何統一配置並且將配置程式碼化,讓多個環境(包括開發,測試,staging)保持一致還是挺值得研究的話題。