1、概述
通常web
應用獲取用戶客戶端的真實ip
一個很常見的需求,例如將用戶真實ip
取到之后對用戶做白名單訪問限制、將用戶ip
記錄到數據庫日志中對用戶的操作做審計等等
在vm
時代是一個比較容易解決的問題,但當一切雲原生化(容器化)之后變得稍微復雜了些
k8s
中運行的應用通過Service
抽象來互相查找、通信和與外部世界溝通,在k8s
中是kube-proxy
組件實現了Service
的通信與負載均衡,流量在傳遞的過程中經過了源地址轉換SNAT
,因此在默認的情況下,常常是拿不到用戶真實的ip
的
這個問題在k8s
官方文檔(https://kubernetes.io/zh/docs/tutorials/services/source-ip/)中基於Cluster IP
、NodePort
、LoadBalancer
三種不同的Service
類型進行了一定的說明,這里不再剖析
2、環境介紹
本篇僅介紹私有雲+外部硬件負載+k8s
集群的真實場景下如何進行配置
相關環境及設備說明如下
組件名 | 型號或版本 |
---|---|
硬件負載設備 | SANGFOR(深信服) AD 6.5R1 |
k8s Ingress 控制器 | NGINX Ingress Controller 0.25.0 |
k8s 集群 | Kubernetes 1.17.0 |
3、相關說明
真實生產場景下,一般提供給用戶的都是七層https
服務
首先域名解析在外部負載設備綁定的公網ip
上,負載周邊可能還會有一些安全設備例如WAF
等,這里不多介紹
流量經過負載后進入到k8s
集群中,其中Ingress Controller
以DaemonSet
方式部署並使用hostNetwork
模式接收並處理到達宿主機的80
、443
端口流量
關於https
證書的配置,一般有以下兩種可選方式:
-
配置在負載設備(負載類型如果只考慮七層負載),由負載負責將數據包封包解包,並轉發到后端,如果用戶通過
https
形式訪問,流量經過的流程是:用戶端——>負載80
端口——>負載443
端口——>服務端(k8s node
)的80
端口 -
配置在后端,例如
Ingress
資源上,如果用戶通過https
形式訪問,流量經過的流程是:用戶端——>負載80
端口——>服務端(k8s node
)的80
端口——>服務端(k8s node
)的443
端口
但是為了獲取用戶的真實ip
,只能選擇方式一,因為如果證書配置在后端服務,流量經過負載時是加密的,負載一般在沒有證書的情況下,是無法對數據包進行解包操作透傳用戶ip
的
以上在公有雲環境下,例如騰訊雲CLB
、阿里雲新的應用型負載ALB
或傳統型負載CLB
均有涉及,可能不盡詳細
4、環境准備
首先需要准備一個后端獲取用戶請求,顯示打印或輸出的應用,可以自己手擼一個簡單應用,當然為了操作簡單也可以選擇nginx
容器在應用日志中查看,更好的方式是選擇whoami
、echoserver
這類鏡像
其中whoami
可以在控制台訪問服務時打印用戶請求等相關信息,echoserver
可以在瀏覽器呈現用戶請求等相關信息
這里為了模擬和真實應用一樣的場景,選擇更為直觀的echoserver
,其源鏡像地址為gcr.io/google-containers/echoserver
如果網絡不佳,可以從我的地址獲取ssgeek/echoserver
首先基於k8s
部署該應用,創建deploy
、svc
、ing
,定義如下
apiVersion: apps/v1
kind: Deployment
metadata:
name: echoserver
labels:
app: echoserver
spec:
selector:
matchLabels:
app: echoserver
template:
metadata:
labels:
app: echoserver
spec:
containers:
- name: echoserver
image: ssgeek/echoserver:latest
ports:
- containerPort: 8080
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
name: echoserver
labels:
app: echoserver
spec:
ports:
- port: 80
targetPort: 8080
name: http
selector:
app: echoserver
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: echoserver
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: echo.ssgeek.com
http:
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
5、負載配置
這里簡單分析及列出關鍵配置
- 插入請求頭以透傳ip
部署好后端服務后,開始配置外部(深信服)負載,除了導入https
證書外,還需要在轉發的請求頭中插入X-Forwarded-For
頭部,確保用戶ip
在經過負載時作為請求頭的一部分傳遞到后端服務器
- 負載設備到后端請求頭部改寫
由於負載設備到后端的80
端口,因此后端只接收http
請求,也就是請求經過負載處理https
及證書相關動作
未添加請求頭部改寫時,對請求抓包的現象對比如下(分別為無https
配置時和有https
配置但未改寫請求頭部時)
6、Ingress Controller 配置
修改Nginx Ingress Controller
配置,添加如下內容
參考:https://kubernetes.github.io/ingress-nginx/user-guide/
data:
use-forwarded-headers: "true"
compute-full-forwarded-for: "true"
forwarded-for-header: "X-Forwarded-For"
-
use-forwarded-headers
如果為
true
,會將傳入的X-Forwarded-*
頭傳遞給upstreams
如果為
false
,會忽略傳入的X-Forwarded-*
頭,用看到的請求信息填充它們。如果直接暴露在互聯網上,或者它在基於L3/packet-based load balancer
后面,並且不改變數據包中的源IP
時使用此選項 -
forwarded-for-header
設置標頭字段以標識客戶端的原始
IP
地址。 默認: X-Forwarded-For -
compute-full-forwarded-for
將遠程地址附加到
X-Forwarded-For
標頭,而不是替換它。 啟用此選項后,upstreams
應用程序將根據其自己的受信任代理列表提取客戶端IP
7、服務端驗證
服務端請求暴露及應用獲取ip
效果如下
正常情況可拿到以下幾類ip
- pod ip
k8s pod
自身的ip
- node ip
k8s pod
所在node
的ip
- 負載 ip
位於請求頭X-Forwarded-For
字段中
- 用戶真實 ip
位於請求頭X-Forwarded-For
字段、x-original-forwarded-for
字段、x-real-ip
字段中
關於x-forwarded-for
、x-original-forwarded-for
、x-real-ip
的說明:
X-Forwarded-For
用於記錄從客戶端地址到最后一個代理服務器的所有地址
X-Real-IP
用於記錄請求的客戶端地址
X-Original-Forwarded-For
字面意思是原始轉發 IP,這是Ingress
的功能,Ingress
將用戶的真實IP
記錄到了這個字段
對應用來說,以java
應用為例,獲取用戶ip
的代碼如下
/**
* 獲取操作用戶ip
* @return
*/
private String getIp() {
HttpServletRequest request;
try {
request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
} catch (NullPointerException e) {
return "127.0.0.1";
}
//取客戶端ip
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0
|| "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0
|| "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0
|| "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根據網卡取本機配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 對於通過多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
return ipAddress;
}
8、小結
本文記錄了私有雲和有外部負載的真實場景下,k8s
集群中的應用獲取用戶ip
的相關實現邏輯及關鍵處理,希望能幫助到大家
See you ~