如何獲取客戶端真實 IP?從 Gin 的一個 "Bug" 說起

  1. 背景

請求 IP 作爲用戶的身份標識屬性之一,是一種非常重要的基礎數據。在很多場景下,我們會基於客戶端請求 IP 去做網絡安全攻擊防範或訪問風險控制。通常我們可以通過 HTTP 協議 Request Headers 中 X-Forwarded-For 頭來獲取真實 IP。然而通過 X-Forwarded-For 頭獲取真實 IP 的方式真的可靠麼?

  1. 概念

X-Forwarded-For 是一個 HTTP 擴展頭。HTTP/1.1(RFC 2616)標準中並沒有對它的定義,它最開始是由 Squid 這個緩存代理軟件引入,用來表示 HTTP 請求端真實 IP,現在已經成爲事實上的標準,被各大 HTTP 代理、負載均衡等轉發服務廣泛使用,並被寫入 RFC 7239(Forwarded HTTP Extension)標準之中。

前段時間石墨文檔某 HTTP 服務升級 Gin 框架到 1.7.2 後突然發現一個 『Bug』,升級後服務端無法獲正確的客戶端 IP,取而代之的是 Kubernetes 集羣中 Nginx Ingress IP。於是我們決定從 Gin 獲取客戶端相應源碼來順藤摸瓜排查一下。

業務方服務之前使用的是 v1.6.3 版本,我們先看看該版本 Context.ClientIP() 方法實現:

// ClientIP 方法可以獲取到請求客戶端的IP
func (c *Context) ClientIP() string {
   // 1. ForwardedByClientIP 默認爲 true,此處會優先取 X-Forwarded-For 值,
   // 如果 X-Forwarded-For 爲空,則會再嘗試取 X-Real-Ip
   if c.engine.ForwardedByClientIP {
      clientIP := c.requestHeader("X-Forwarded-For")
      clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
      if clientIP == "" {
         clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
      }
      if clientIP != "" {
         return clientIP
      }
   }
   // 2. 如果我們手動配置 ForwardedByClientIP 爲 false 且 X-Appengine-Remote-Addr 不爲空,則取 X-Appengine-Remote-Addr 作爲客戶端IP
   if c.engine.AppEngine {
      if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
         return addr
      }
   }
   // 3. 最終才考慮取對端 IP 兜底
   if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
      return ip
}
   return ""
}

再看 v1.7.2 版本, Contexnt.ClientIP() 方法實現:

func (c *Context) RemoteIP() (net.IP, bool) {
   ...
   remoteIP := net.ParseIP(ip) // 獲取客戶端 IP
   ...
   // trustedCIDRs 由 engine 啓動時配置的 TrustedProxies 數組解析而來,表示可以信任的前置代理 CIDR 列表。只有配置了 engine.TrustedProxies 纔有可能解析出正確的可信任 CIDR 列表。
   // 只有 CIDR 列表不爲空,這裏纔會將 remoteIP 和已配置可信 CIDR 列表進行比對。CIDR 列表中任一 CIDR 包含對端 IP,則將第二個返回值置爲 true,表示對端 IP 可信任。
   if c.engine.trustedCIDRs != nil {
      for _, cidr := range c.engine.trustedCIDRs {
         if cidr.Contains(remoteIP) {
            return remoteIP, true
         }
      }
   }
   return remoteIP, false
}
func (c *Context) ClientIP() string {
   // 1. AppEngine 默認爲 false,如果應用通過 Google Cloud App Engine 部署,或用戶手動設置爲 true 且 X-Appengine-Remote-Addr 不爲空,則會取 X-Appengine-Remote-Addr 值作爲客戶端 IP。
   if c.engine.AppEngine {
      if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
         return addr
      }
   }
   // 2. 否則通過 RemoteIP() 方法判斷對端 IP 是否可信,trusted 爲 true 表示可信
   // 詳見上文 Context.RemoteIP() 方法內部註釋。
   remoteIP, trusted := c.RemoteIP()
   if remoteIP == nil {
      return ""
   }
   // 3. 如對端 IP 可信,且 ForwardedByClientIP 爲 true(默認爲 true),且
   // RemoteIPHeaders 不爲空(默認不爲空),則根據 RemoteIPHeaders 中配置的獲取 ClientIP 的 Headers 列表中依次獲取。默認讀取順序:1. X-Forwarded-For;2. X-Real-IP。
   if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
      for _, headerName := range c.engine.RemoteIPHeaders {
         // 對header進行處理,先通過","進行分割,並返回分割後 IP 列表的第一個合法 IP
         ip, valid := validateHeader(c.requestHeader(headerName))
         if valid {
            return ip
         }
      }
   }
   // 3. 最終才考慮取對端 IP 兜底。
   return remoteIP.String()
}
// validateHeader 會對入參header進行校驗,先通過","進行分割成 IP 列表後,對每個 IP 進行合法性檢查,如果任一 IP 不合法,則此Header不合法;否則返回 IP 列表中第一個 IP。
func validateHeader(header string) (clientIP string, valid bool) {
   if header == "" {
      return "", false
   }
   items := strings.Split(header, ",")
   for i, ipStr := range items {
      ipStr = strings.TrimSpace(ipStr)
      ip := net.ParseIP(ipStr)
      ...
      if i == 0 {
         clientIP = ipStr
         valid = true
      }
   }
   return
}
  1. 分析

X-Forwarded-For: client_ip, proxy1_ip, proxy2_ip

3.1. X-Forwarded-For 是否可以被僞造?

• 不重寫 X-Forwarded-For 的邊緣節點 邊緣節點如果是透傳 HTTP 的 X-Forwarded-For 頭,那麼它就是不安全的,客戶端可以在 HTTP 請求中僞造 X-Forwarded-For 值,且這個值會被向後透傳。

因此不重寫 X-Forwarded-For 的邊緣節點是不安全的邊緣節點,用戶可以僞造 X-Forwarded-For 。

# 不安全
X-Forwareded-For:clientX-Forwarded-For(用戶請求中的 X-Forwarded-For),proxy1,proxy2,proxy3...
# 邊緣節點用 $remote_addr 來覆蓋用戶請求中的 X-Forwarded-For:
proxy_set_header X-Forwarded-For $remote_addr; 
# 安全
X-Forwareded-For:ClientX-Forwarded-For(邊緣節點獲取的 remote_addr),proxy1,proxy2,proxy3...

3.2. 如何才能獲取真實客戶端 IP?

3.2.1. 客戶端 ->WAF->SLB->Ingress->Pod

3.2.1.1. 使用 Nginx real-ip 模塊

使用 Nginx real-ip 模塊獲取,需在 Ingress 上配置 proxy-real-ip-cidr ,把 WAF 和 SLB(7 層) 地址都加上。操作後服務端使用 X-Forwarded-For 可取到真實 IP,通過 X-Original-Forwarded-For 可取到僞造 IP。

這種方案有如下缺點:

• 由於 WAF 是雲廠商維護,WAF 地址池衆多,同時地址會有變化,維護此動態配置難度極大,如更新不及時會導致獲取的客戶端 IP 不準確。• 即使採用此方案,業務方如果要使用新版本的 Gin 的 ctx. ClientIP() 方法,仍然需改動代碼,將所有可信代理配置到 TrustedProxies,這會導致基礎設施和業務服務耦合,這種方案顯然是無法接受的,除非業務方願意將依賴的 Gin 版本鎖死在 v1.6.3。

3.2.1.2. 使用 WAF 自定義 Header

不少雲廠商提供了自定義 Header 來獲取客戶端真實 IP( $remote_addr )能力,我們可以在雲廠商 WAF 終端中提前配置好自定義 Header 頭,比如 X-Appengine-Remote-Addr 或 X-Client-Real-IP 等,用來獲取客戶端真實 IP。

這種方案有如下缺點:

• 如直接複用 X-Appengine-Remote-Addr 這個 Header,則需設置 engine. AppEngine=true,纔可通過 ctx. ClientIP() 方法的前提下獲取客戶端 IP。• 如使用其他 Header,比如 X-Client-Real-IP,則需要自行封裝從 X-Client-Real-IP 中獲取客戶端 IP 方法,同時需要業務配合做改造。

架構大概如下所示:

3.2.2. 客戶端 ->CDN->WAF->SLB->Ingress->Pod

3.2.2.2. 使用 real-ip

使用 real-ip 模塊獲取,需要在 ingress 上配置 proxy-real-ip-cidr 把 CDN、WAF 和 SLB(7 層)的地址都加上,服務端使用 X-Forwarded-For 可取到真實 IP,通過 X-Original-Forwarded-For 可取到僞造 IP。

此方案優缺點:

• 此場景相比 3.2.1 多了層 CDN,CDN 地址池比 WAF 更大,地址池變化頻率更高,同時廠商也沒有提供 CDN 地址池,維護 Ingress 配置基本不可能。• 即使採用此方案,業務方如果要使用新版本的 Gin 的 ctx. ClientIP() 方法,仍然需改動代碼,將所有可信代理配置到 TrustedProxies,這會導致基礎設施和業務服務耦合,這個肯定無法接受,除非業務方將 Gin 版本鎖死在 1.6.3。

3.2.2.1. 使用 CDN 自定義 Header

此方案優缺點:同 3.1.1。架構大概如下所示:

3.2.3. 客戶端 ->SLB->Ingress->Pod

可通過 Ingress 上設置 use-forwarded-headers 來防止 X-Forwarded-For 僞造。

•use-forwarded-headers=false

適用於 Ingress 前無代理層,例如直接掛在 4 層 SLB 上,ingress 默認重寫 X-Forwarded-For 爲 $remote_addr ,可防止僞造 X-Forwarded-For 。

•use-forwarded-headers=true

適用於 Ingress 前有代理層,例如 7 層 SLB 或 WAF、CDN 等相當於在 nginx.conf 中添加如下配置:

real_ip_header      X-Forwarded-For; 
real_ip_recursive   on; 
set_real_ip_from    0.0.0.0/0; // 默認信任所有 IP,無法避免僞造 X-Forwarded-For

架構大概如下所示:

  1. 總結

從上文中我們不難看出,在雲上覆雜多變的網絡拓撲結構下,我們會頻繁地維護 CDN、WAF、SLB、Ingress 等多種網絡設施配置。如果需完全保證 X-Forwarded-For 不可僞造,對於要升級 Gin 框架的 Go 服務來說,只有如下兩種方案:

• 繼續嘗試通過 X-Forwarded-For 獲取客戶端真實 IP。• 嘗試通過其他 Header 獲取客戶端真實 IP。

4.1. 繼續嘗試通過 X-Forwarded-For 獲取客戶端真實 IP

業務中需配置基礎設施所有前置代理到 TrustedProxies 中,包含 CDN 地址池、WAF 地址池、Kunernetest Nginx Ingress 地址池,這種方案基本無法落地:

• 配置太過複雜,一旦獲取 IP 不準,很難排查。• 導致業務配置和基礎設施耦合,基礎設施如果對 CDN、WAF、Ingress 做變動,業務代碼必須同步變更。• 部分可信代理 IP 根本沒法配置,比如 CDN 地址池。

4.2. 嘗試通過自定義 Header 獲取客戶端真實 IP

基礎設施團隊提供自定義 Header 來獲取客戶端真實 IP,如 X-Client-Real-IP 或 X-Appengine-Remote-Addr 。這種方案需要基礎設施團隊在雲廠商 CDN 或 WAF 終端上做好相應的配置。這種方案:

• 配置簡單可靠,維護成本低,僅需在 CDN、WAF 終端配置自定義 Header 即可。• 如果使用 X-Appengine-Remote-Addr,對於使用 Google Cloud 的 App Engine 的服務不需做任何修改。對於使用的國內雲廠商的服務,則需要顯式的配置 engine. AppEngine = true,然後繼續通過 ctx.ClientIP() 方法即可。• 如果使用其他自定義 Header,如 X-Client-Real-IP 來獲取客戶端真實 IP,建議可以考慮自行封裝 ClientIP(*gin.Context) string 函數,從 X-Client-Real-IP 中獲取客戶端 IP。

資料鏈接:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/C-Xf6haLrOWkmBm2lRTdEQ