大家好,笔者最近参与了一个问题的处理,虽然这个问题不是很难,但从数据流的角度上来,也是一个容易遇到的经典问题,所以这里沉淀一下,供大家参考:
可以看到 这里业务流程是这样的:服务端解析客户端上报的数据时,会同时解析客户端的IP信息,用于确认客户端的地域、运营商等信息,方便对数据进行分类和二次分析
但最近发现,某个环境的客户端的IP信息比较集中,具体来说,IP基本上集中在10-30个范围内,考虑到这个是真实的客户环境,用户数应该是一个比较大数量级,所以判定这里存在问题
可以看到,这里IP分布比较集中,既然出现了问题,我们开始数据流的倒查
这里以golang代码为例
这里可以看到,在上报数据中获取Header中的X-Forward-For字段,然后取第一个IP地址即可
查询资料可以得知,X-Forward-For基本上是业界的一个标准字段,用来存储HTTP请求过程中IP链路,具体的内容是IP列表,分别用来表示从客户端IP到中间代理IP最后到服务端的IP
每一个代理服务器,都会把与它建联的上一个服务的IP添加到X-Forward-For的里面,并用逗号隔开
这里拓展一个知识点:当我们想要用X-Forward-For获取客户端IP的时候,很可能会听到另一个字段X-Real-IP也能做到同样的事情,这两个字段有什么区别,先看定义:
1.X-Real-IP: 当一个请求通过反向代理服务器时,代理服务器会将客户端的真实 IP 地址添加到 X-Real-IP 头部中 2.X-Forward-For: 当一个请求通过反向代理服务器时,代理服务器会将与它建联的上一个服务的IP添加到X-Forward-For的里面,并用逗号隔开
可以看到,X-Real-IP是有一个信息,即客户端IP,而X-Forwarded-For 有中间链路的所有IP地址,那么他们在这个场景(获取客户端IP)下是否完全等价呢?也不是,主要有以下几个不同:
1.X-Real-IP 是一个非标准的头部字段;而X-Forwarded-For 是一个实际标准的头部字段,被写入 RFC 7239(Forwarded HTTP Extension)标准之中。 2.当请求通过多个代理服务器时,每个代理服务器都会将自己的 IP 地址添加到 X-Forwarded-For 头部中。这意味着,X-Forwarded-For 头部可以提供更多的信息,包括请求经过的所有代理服务器的 IP 地址、便于信息对齐和排障。
可以看到,在复杂场景中(多种非标准协议请求和多个代理服务器场景下),更推荐使用X-Forwarded-For 作为获取客户端IP的字段
回到问题本身,这里我们可以看到,服务端获取IP的方式应该问题不大,那就需要看看在前面路由转发阶段是否存在问题:
一般来说:路由转发有很多种方式,很多云厂商会提供通用LB的服务,这里只需要可视化的配置即可,比如下图:
以腾讯云为例,官方文档中告知,X-Forwarded-For会在LB中默认传递,并支持日志打印:
但考虑到通用性,笔者更喜欢从根本上剖析这里的问题,而不是一个云厂商UI界面的操作员。
一般来说,很多厂商提供到的LB,底层都是Nginx居多,那就先拿Nginx来看:
如果需要获取X-Forwarded-For,则需要在nginx.conf设置以下两个参数
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_add_x_forwarded_for定义如下:
the “X-Forwarded-For” client request header field with the $remote_addr variable appended to it, separated by a comma. If the “X-Forwarded-For” field is not present in the client request header, the $proxy_add_x_forwarded_for variable is equal to the $remote_addr variable.
可以看到proxy_add_x_forwarded_for基本上等同于:"http_x_forwarded_for, remote_addr"这样的格式,如果刨根问底一些的话,不同在于:
看起来,很合理,就两行配置,但是这么配置有没有问题呢?
这里就不得不提,X-Forwarded-For伪造的问题了:
因为X-Forwarded-For只是http的请求的一个头,如果客户端请求时就带上一个伪造的X-Forwarded-For(使用curl -H 'X-Forwarded-For: 8.8.8.8' http://www.dianduidian.com一条命令就能实现),这时Nginx如果使用上边的配置的话由于X-Forwarded-For不为空,所以Nginx只会在现在值的基础上追加,这样后端服务在拿到头后根据约定取最左边ip话就会拿到一个伪造的IP,会有安全风险。
那这里nginx是怎么解决的呢?
在TCP的场景下,TCP必须经过3次握手,客户端的IP是无法伪造的,所以最外层的Nginx代理一定要取$remote_addr的值,对应配置
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
所以解决方案如下:
为了保证后端服务能获取到真实的用户IP,无论中间有多少层代理,必须保证最外层代理能获取到真实的用户IP,这是整个信任连的基础;最外层(直接暴露给用户)的代理一定不能信任请求带过来的X-Forwarded-For,而应取TCP建连时的IP(即$remote_addr),同时必须保证中间层的代理无法被用户直接访问到,否则就不是一个完整的信任链,就有伪造的可能。
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
可以通过下面的命令进行生效和日志查看
service nginx restart cat /path/server/nginx/logs/access.log 或者cat /var/log/nginx/nginx_access.log 或者cat /var/log/nginx/access.log
这里大概介绍了IIS 6、IIS 7、Apache服务的获取方式,大家可以参考下
好了,我们知道了Nginx本身的原理了,现在再把问题更贴近一下现实情况,目前很多的服务都是基于K8S环境进行部署,那么在K8S环境下,客户端的传递有什么不同?
首先,K8S的集群如何外网访问呢?
优点:粗暴直接,跟裸机部署很像,便于新手理解,不用配置/掌握K8S的网络知识就可以运行
缺点:1.Pod 的网络流量与宿主机的网络流量无法区分;2.Pod重启后可能在另一个宿主机,需要保证每台宿主机的网络环境完全一致
总结:适合单集群,单宿主机的情况,基本上适合单机调试和新手上手的情况
优点:粗暴直接,跟裸机部署很像,便于新手理解,不用配置/掌握K8S的网络知识就可以运
缺点:1.Pod重启后可能在另一个宿主机,需要保证每台宿主机的网络环境完全一致
总结:比HostNetwork好在可以区分宿主机还是POD资深的网络流量,但依然会有POD重启的问题
优点:因为会在每个节点自动做相同的设置,对pod重启十分友好,体现出高可用
缺点:学习一下配置方式
总结:选他,选他,选他!
kubectl proxy 在笔者的认知中,不算其中的任何方式,因为它更多的是用来本机调试的,所以没有罗列
这时,很多同学会说,外网访问少了一种,Ingress也是一种解决方案,其实,这是很多网上的一种不严谨的观点,因为,ingress的规则需要Ingress Controller来执行转发,而Ingress Controller并不能直接被外网访问,,一般会通过 Kubernetes 集群内的Service(NodePort或者LoadBalancer)来进行外网访问,所以严格的说,Ingress方案是NodePort或者LoadBalancer的一种实现方式
当然,Ingress是个很好路由转发方案,当外网数据通过Service传递过来了之后,就可以通过Ingress Controller去执行ingress的规则。
所以一般工程场景下,外网访问的组合模式为:外网请求 --- Service(NodePort)--- Ingress Controller(规则为Ingress的yaml)--- Service(ClusterIP)--- POD(后台服务)
我们先理清Ingress和Ingress controller的关系
Ingress是自kubernetes1.1版本后引入的资源类型。必须要部署 Ingress controller 才能创建Ingress资源,Ingress controller是以一种插件的形式提供。Ingress controller 是部署在Kubernetes之上的Docker容器。它的Docker镜像包含一个像nginx或HAProxy的负载均衡器和一个控制器守护进程。控制器守护程序从Kubernetes接收所需的Ingress配置。它会生成一个nginx或HAProxy配置文件,并重新启动负载平衡器进程以使更改生效。换句话说,Ingress controller是由Kubernetes管理的负载均衡器。Ingress controller是由Kubernetes管理的负载均衡器
可以看到,Ingress是用来配置规则的,Ingress controller是用来执行规则的
最简化的 Ingress 配置如下。
注意:如果没有配置 Ingress controller 就将其 POST 到 API server 不会有任何用处。
配置说明
具体参考案例3
这里可以看到与Nginx类似的内容,那怎么把这些配置运行起来呢——需要配置合适 Ingress controller ,作为规则的分发器,业界的主要有以下几种:
看到这里不要觉得头大,事实上,绝大部分都是使用NGINX Ingress Controller作为Ingress Controller,一方面,因为NGINX强大的功能和友好的功能配置,另一方面,NGINX本身也是一个业界流行的路由分发解决方案,最后,NGINX Ingress Controller也是Kubernetes 当前支持并维护,所以无脑选它没问题了
当然,喜欢玩微服务和Golang的小伙伴,也可以用Traefik,可以直接UI配置,虽然功能没有NGINX强大,但友好程度十分突出
NGINX Ingress Controller配置
配置这个的话,有一个很好的文档可以给大家参考:[Ingress-Nginx Controller
配置](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/)
简单来说两句话:
ConfigMap: 使用K8S里面的 Configmap 在 NGINX 中设置全局配置。
Annotations: 如果您想要对特定 Ingress 规则的进行配置,使用Annotations。
举个例子:
如果有两个后端服务:myServiceA, myServiceB,且端口均为80的话
需要将外网的数据转发过来,配置如下:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-myservicea
spec:
rules:
- host: myservicea.foo.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myservicea
port:
number: 80
ingressClassName: nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-myserviceb
spec:
rules:
- host: myserviceb.foo.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myserviceb
port:
number: 80
ingressClassName: nginx
备注:Kubernetes version >= 1.19.x
其中IngressClassName:nginx,表示指定了使用 Nginx Ingress Controller 来处理这个 Ingress 资源,具体内容很像Nginx本身很像,就不解释了
嗯,过了这么久,终于要回到正题,Ingress-Nginx Controller在路由转发的时候,会不会传递客户端IP呢?
答案是,默认不会!
需要配置,那问题来了,怎么配置?还记得我们需要的字段是哪个吗?X-Forwarded-For!那Ingress-Nginx Controller方案里面怎么配置呢?
回到之前的那句话: 使用K8S里面的 Configmap 在 NGINX 中设置全局配置。
所以需要在Configmap里面配置,这里进行了详细的介绍,笔者简单说下,需要配置以下三个参数:
注:左边为开启use-forwarded-headers后ingress nginx主配置文件,右边为开启前
注:左边是未开启compute-full-forwarded-for配置的ingress nginx主配置文件,右边是开启了
当然,这里涉及到一个小技巧,如何查看Ingress-Nginx Controller里面的Nginx配置:
(1)首先进入到Ingress-Nginx Controller的pod中(对,它是一个pod),然后查看线程:
------> kubectl exec -it Ingress-Nginx Controller-XXX /bin/sh ------> ps aux|grep nginx root 352 0.0 0.0 2468624 924 ?? S 10:43上午 0:00.08 nginx: worker processundefinedroot 232 0.0 0.0 2459408 532 ?? S 10:43上午 0:00.02 nginx: master process /usr/local/opt/nginx/bin/ nginx -g daemon off;undefinedroot 2345 0.0 0.0 2432772 640 s000 S+ 1:01下午 0:00.00 grep nginx
上图中,就可以看到nginx的路径为:/usr/local/opt/nginx/bin/nginx
(2)使用nginx的 -t 参数进行配置检查,即可知道实际调用的配置文件路径及是否调用有效,如果有效cat一下就好。
------>/usr/local/opt/nginx/bin/nginx -t nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful ------> cat /usr/local/opt/nginx/bin/nginx
好的,经过这么长时间的探索,我们终于知道了这里怎么设置了,那回到一开始的问题,是不是设置的不对呢?这个问题发生的环境正是使用的K8S的Ingress来配置,且使用的是Ingress-Nginx Controller来做转发,看下这里的值是多少:
破案了,原来真的是这里,
这里再拓展一个小技巧:
需要在configMap 的data字段增加/修改log-format-upstream
当然实际的工程中,可能有多个Ingress服务,需要单独用个文件来配置,避免把全集群都修改了,一般来说,Helm直接下载的组件和其他第三方的组件都会在Nginx原始的yaml文件里面写这样的内容:
这里的value.yaml则是可以使用自定义的yaml限定范围进行覆盖(不要用原始的value.yaml),确保问题可控
具体来说,对于Ingress自定义的configmap,可以绑定对应class,也就是具体使用哪个ingress nginx controller来执行,比如下图就是nginx-1
然后在nginx-1的value.yaml上配置新的日志:
当然了,如果了类似CLB的服务也可以参考这里
其实,也到这里,问题也找到了,基本的概念也清晰,是不是就可以上手了,其实不是,真实的工程环境会比较复杂,具体来说;
回到之前的Nginx方案,里面提到最外层Nginx配置为:
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
实际业务场景中,不一定完全用Nginx来做转发,前面还可能有硬件负载均衡、防火墙,CND等等数据流,CDN厂商有自己的专用获取IP的字段,防火墙也做了一些信息的转换,所以在自己所负责的链路的最前方要跟上游的团队对接好这个字段才行
这里有个比较有意思的问题:负载均衡、防火墙、CDN、服务端真实服务这几个的顺序是什么样的?有知道的同学可以留言一下
一个比较经典的案例如下:案例1
实际上,很多云厂商都有自己的获取IP的标准模式,以腾讯云的TKE为例,这篇文章就提到了如何在 TKE 中获取客户端真实源 IP:
当然,如果部署的是混合云,那问题可能会更加的复杂这篇文章给出了基于腾讯云的混合云的获取IP的方案
上流数据准确,底层数据准确了,那还有一个问题没有解决,客户端IP在发起时就恶意篡改了,这里就可以用Nginx就尝试用real-ip模块来解决这个问题,对应ingress的configmap配置为enable-real-ip
简单来说,可以在Nginx Ingress的ConfigMap中添加以下
proxy-real-ip-cidr: "10.0.0.0/8"
set-real-ip-from: "IP1"
real-ip-header: "X-Forwarded-For"
这个配置实现了以下几个功能:
这样,即使有伪造也是这样的情况:
X-Forwarded-For: 客户端伪造 IP 地址, IP0(client), IP1(proxy), IP2(proxy)
那么,我们只需启用 realip 模块的 real_ip_recursive 递归模式,将从右往左逐步剔除 IP1 信任代理,最后会获取到真实的客户端 IP 地址。
当然这么做的前提是能拿到最开始的代理服务器的IP
最佳实践在这里
###拓展的点:
####拓展点1
前面都做了,数据仍然不准,怎么办,如果接入了CDN服务,大概率不能使用X-Forwarded-For等字段来获取,可以跟CDN服务商约定一个字段来获取
####拓展点2.
Service 的 externalTrafficPolicy 字段为 Local,能不能也可以获取到真实的IP? 可以,通过获取Request.RemoteAddr就行了,但那样就意味着,只能将请求转发到Service自身的所在Node上的pod了,这样就可能导致负载不均衡,工程只有在特定情况才使用,具体参考这篇文章
####拓展点3.
golang的Gin框架里面也有一些具体的获取实现,可以用吗?可以,但要冒风险,Gin的代码兼容性有时也做的不好,参考GIn的问题
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。