出於安全考慮,近期在處理一個記錄使用者真實IP的需求。本來以為很簡單,後來發現沒有本來以為的簡單。這裡主要備忘下,如果伺服器處於埠迴流(hairpin NAT),keepalived,nginx之後,如何取得客戶端的外網IP。
來自客戶端PC的流量路徑如上,在這樣的拓撲中,在應用服務中取得,客戶端PC的外網ip,可能會遇到哪些問題呢?(ip 編的隨意,為便於說明,不考慮合理)。
- 程式設計實現
Java 為例,這個我會。
public static String getClientIP(HttpServletRequest request) { String remoteAddr = request.getRemoteAddr(); return remoteAddr; }
執行一下,輸出呢是 3.3.3.3 。 這是因為這個API所取得的是IP資料包的源地址。Nginx的反向代理時工作在應用層的,當他收到一個http請求時,會對應生成一個新的請求,傳送給應用服務,這個請求的IP包的源地址是Nginx伺服器的IP即3.3.3.3。
- Nginx頭部注入
因為是應用層,那這個請求ip包的源地址肯定就是3.3.3.3了,但是在應用層我們可以附加一點資訊,以便後面的應用服務,可以透過這個附加資訊,瞭解這個請求對應的原始源地址。這個我也會。
在Nginx 中配置。
server { ... proxy_set_header X-Real-IP $remote_addr;
在應用層http協議中,加一個http header。X-Real-IP:$remote_addr. $remote_addr 是一個預設變數,代表所代理轉發請求的原始源ip地址。
在Java 程式中,讀取對應的附加資訊
public String getRealIp(HttpServletRequest request) { String realIp = request.getHeader("X-Real-IP"); if (realIp != null && !realIp.isEmpty()) { return "Client's Real IP: " + realIp; } return ""; }
執行一下,此時輸出2.2.2.2。 顯然我們向前推進了一步。
- Keepalived負載均衡模式
印象裡這裡keepalived的主要作用應該是解決nginx 代理伺服器的單點問題的,似乎也被配置為負載均衡了?翻了下配置檔案,實際的情況如下。
運維大壯說他配置keepalived 時候多考慮了一步,如果機器活著,nginx 掛了怎麼辦,於是又做了一層負載均衡(這種情況虛擬IP不會漂移到右邊的備機)。他說的也確實不是沒有道理。keepalived 的負載均衡貌似是工作在第三層的,那肯定在負載均衡的時候,又對ip包的源地址進行了修改。這是網路層,向nginx 這樣附加資訊肯定是不行了。於是,翻了翻手冊發現,keepalived 的負載均衡支援三種路由模式,NAT,Direct Routing 和 Tunneling。
NAT 模式,會修改源IP,出入流量都會經過負載均衡器。而DR模式,會直接修改MAC地址,那回程流量就不再經過負載均衡器了,也就意味這種模式,源地址不會被修改,回程流量會直接傳送給源ip地址。
DR模式有個要求,就是負載均衡器需要能知道後端服務的MAC地址,這是依賴於ARP實現的,也就是,要求負載均衡器和後端伺服器在同一廣播域。恰好我門可以滿足。於是。
virtual_server 192.168.11.242 80 {
……
lb_kind DR
……
將負載均衡路由模式切換為DR模式。重新看一下這次,取得客戶端地址變成了 1.1.1.1, 這一步一坑。為什麼到達keepalived的ip包的源地址會變成,出口路由器的外網地址呢?
- 路由器埠迴流(Hairpin NAT)
離勝利是不遠了,此時見多識廣的大壯說,這應該是跟埠迴流有關,之前有個系統也是類似問題, 你的web埠配置了埠迴流,如果關掉埠迴流就可以取得外網地址了。什麼是埠迴流?
首先,路由器做了埠對映,1.1.1.1:80->192.168.0.2:80
伺服器A,由於某些原因,不方便使用內網地址192.168.0.2訪問B,而要透過外網IP或者域名訪問伺服器B,即訪問1.1.1.1:80, 按埠轉發規則,路由器會將這個來自於內網介面的流量再次轉發回內網伺服器B,形成了一個180度的急彎——髮卡彎,這也就是Haripin NAT的名字由來,十分形象。
如果不做設定,伺服器A透過訪問1.1.1.1:80 是無法正常訪問伺服器B的。原因是,hairpin會影響Tcp連線建立的握手過程。
1. A傳送握手請求給入口路由器,路由器修改目的ip為192.68.0.2 ,傳送到伺服器B。
2.B收到握手請求後,回覆握手確認應答給這個握手請求的源IP地址,此處是A的地址192.168.0.1
3.因為A,B同一網路,握手確認會直接到達A。
4.A發現這個握手確認回覆的源ip(192.168.0.2)並不是我期望與之建立連線的握手請求目的地址(1.1.1.1),A並不認識B,只認識路由器,導致TCP連線無法建立。
解決以上問題的關鍵,就是讓握手確認應當同樣經過路由器,傳送給A。因此,需要在之前將握手請求轉發給B時,同時修改源ip地址為(1.1.1.1),如此,B伺服器作出確認回覆時,自然也會傳送給1.1.1.1。
但是這個源地址轉化(SNAT)的過程,實際上只對於來自內網的流量是有必要的。對於外網流量,其源IP本身就處於網路外部,必然會經過再次經過路由器返回。
於是聯絡管路由器的小明,請他不要偷懶,規則配置的細緻一點,不要做無差別的源地址轉換。即
1.對內網介面流量進行源地址和目標地址轉換
2.對外網流量只進行目標地址轉化。
重新測試。 終於輸出實際了客戶PC實際ip地址0.0.0.0
OK,一波三折,跌宕起伏,寫到這裡~