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 ~