k8s流量策略与ingress获取真实ip

获取真实ip

方法1 X-Forwarded-For配置

这种是百度中大多数的方法:

适用于7层http转发

业务架构:
Client->WAF->LB->ECS->容器
问题:在容器中获取不到真实的客户端公网IP

分析:

1
2
容器里面抓包验证
tcpdump -i eth0 -s 0 -w /tmp/http.cap port 端口

WAF已经将 真实客户端地址放到了 x-Forwarded-For 的字段中传给了ECS

在容器中抓包,看到一个x-Forwarded-For的字段是错误的,对应的IP为WAF的回源地址

ingress将真实的客户端IP,放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。造成ingress获取ip为WAF回源地址.

处理方法:

1
2
3
4
5
kubectl -n kube-system edit cm nginx-configuration
data:
compute-full-forwarded-for: "true"
forwarded-for-header: "X-Forwarded-For"
use-forwarded-headers: "true"
  1. use-forwarded-headers

    查看NGINX Ingress Controller的ConfigMaps配置文档,可以找到以下配置项use-forwarded-headers如果为true,NGINX会将传入的 X-Forwarded-* 头传递给upstreams。当NGINX位于另一个正在设置这些标头的 L7 proxy / load balancer 之后时,请使用此选项。
    如果为false,NGINX会忽略传入的 X-Forwarded-* 头,用它看到的请求信息填充它们。如果NGINX直接暴露在互联网上,或者它在基于 L3/packet-based load balancer 后面,并且不改变数据包中的源IP,请使用此选项。

    ps: NGINX Ingress Controller直接暴露互联网也就是Edge模式不能开启为true,否则会有伪造ip的安全问题。也就是k8s有公网ip,直接让客户端访问,本配置不要设为true!

  2. forwarded-for-header

    设置标头字段以标识客户端的原始IP地址。 默认: X-Forwarded-For

    ps:如果 NGINX Ingress Controller 在CDN,WAF,LB等后面,设置从头的哪个字段获取IP,默认是X-Forwarded-For

    这个配置应该和use-forwarded-headers配合使用

  3. compute-full-forwarded-for

    将远程地址附加到 X-Forwarded-For 标头,而不是替换它。 启用此选项后,upstreams应用程序将根据其自己的受信任代理列表提取客户端IP

作为Edge需要重写remote_addr,保证了客户端IP不会被伪造
必须:X-Forwarded-For 重写为 $remote_addr
非必须扩展:X-Real-IP 重写为 $remote_addr

ingress默认会重写 proxy_set_header X-Forwarded-For $remote_addr;

方法二 方式二 Proxy Protocol 配置实例

方法三 ingress svc 流量策略设置成Local

如果使用ingress NodePort方式,并以DaemonSet方式安装nginx-ingress-controller,可以实现客户端从任何节点都可以访问,并可获取到客户端的真实IP:

externalTrafficPolicy的好处是,Pod内应用的确可以拿到真实的客户端地址了,但坏处是: 客户端只能使用pod所在的node的IP访问,无法使用其他node的ip访问以及使用vrrp之类的virtual IP来实现ha,对客户端来说会麻烦一点。

设置 service.spec.externalTrafficPolicy 的值为 Local,请求就只会被代理到本地 endpoints 而不会被转发到其它节点。这样就保留了最初的源 IP 地址。

1
2
3
将 Deployment 改为 DaemonSet
删掉 replicas: 1
将ingress svc 改为 externalTrafficPolicy: Local
1
2
3
4
5
6
7
8
9
10
11
12
13
14
                client  
^ / ^ \
/ / \ \
/ v \ v
node 1 service-NodePort node 2 serviceNodePort
^ | ^ |
| | | |
| v | v
node1 ingress-controller node2 ingress-controller
^ \ / ^
\ \ / /
\ \ / /
\ v v /
endpoint

nginx日志$http_x_forwarded_for已经记录了客户端的真实IP

踩坑:

在部署apollo时,一共部署3个环境,线上为内网,测试开发为公网(同一个集群中),在portal组件的检查后端服务中存在无法访问后端服务状况

流量路径

portal组件(pod)–>ingress–>configservice(pod)

因为ingress svc流量策略已经改为Local,但是ingress Deployment并没有改为DaemonSet,所以外端访问时如果ingress pod没有和apollo在一台主机时,处出现pod访问ingress后端被拒绝

总结

7层转发链路 Client(客户端) > Nginx > K8s Ingress(Nginx ingress)
4层转发链路 Client(客户端) > 公有云LB > K8s Ingress(Nginx ingress) (阿里云默认为此)
ps: 实际业务会串联更多层级的转发。DDOS、WAF、CDN、Api Gateway一般是http 7层转发,LB一般是4层tcp转发

whoami探针

whomai是一个go编写的调试探针工具,回显http头信息
在k8s中部署一个containous/whoami用来作为探针,配置好ingress公网和访问,这样客户端web访问可以看到基本的http头信息,方便调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
namespace: default
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- image: containous/whoami
imagePullPolicy: Always
name: whoami
ports:
- containerPort: 80
name: 80tcp02
protocol: TCP
dnsPolicy: ClusterFirst
restartPolicy: Always
---
kind: Service
apiVersion: v1
metadata:
name: whoami
namespace: default
spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
selector:
app: whoami
---
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
name: whoami
namespace: default
spec:
rules:
- host: k8s-ingress-xxx.xxx.com
http:
paths:
- path: /whoami/(.+)
pathType: ImplementationSpecific
backend:
serviceName: whoami
servicePort: 80

客户端web访问,回显http头示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Hostname: whoami-65b8cc4b-6vwns
IP: 127.0.0.1
IP: 10.42.2.12
RemoteAddr: 10.42.1.0:47850
GET / HTTP/1.1
Host: whoami.iamle.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.162 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6,la;q=0.5
Cookie: _ga=GA1.2.30707523.1570429261;
Dnt: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
X-Forwarded-For: 8.8.8.8, 10.0.0.1
X-Forwarded-Host: whoami.iamle.com
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Original-Forwarded-For: 8.8.8.8
X-Original-Uri: /
X-Real-Ip: 8.8.8.8
X-Request-Id: 3852c9780589ffba4c1f9f2785691d5f
X-Scheme: https

两种方式

  • 7层http头X-Forwarded-For透传

    http工作在网络第7层,http中有个X-Forwarded-For字段

    大部分CDN、WAF、LB用X-Forwarded-For字段来存客户端IP,也有用X-Real-Ip字段,cloudflare、百度云加速还扩展了CF-Connecting-IP字段
    标准数据为

    1
    X-Forwareded-For:Client,proxy1,proxy2,proxy3……

    第一个ip是客户端ip,后面的proxy为路过一层就加一层的ip
    这里的proxy可以是WAF、CDN、LB、Api Gateway等

  • 4层Proxy Protocol透传

    tcp工作在网络第4层,Proxy Protocol就是在tcp中增加一个小的报头,用来存储额外的信息

    代理协议即 Proxy Protocol,是haproxy的作者Willy Tarreau于2010年开发和设计的一个Internet协议,通过为tcp添加一个很小的头信息,来方便的传递客户

    端信息(协议栈、源IP、目的IP、源端口、目的端口等),在网络情况复杂又需要获取客户IP时非常有用。

    其本质是在三次握手结束后由代理在连接中插入了一个携带了原始连接四元组信息的数据包。

    目前 proxy protocol有两个版本,v1仅支持human-readable报头格式(ASCIII码),v2需同时支持human-readable和二进制格式,即需要兼容v1格式
    proxy protocol的接收端必须在接收到完整有效的 proxy protocol 头部后才能开始处理连接数据。因此对于服务器的同一个监听端口,不存在兼容带proxy protocol包的连接和不带proxy protocol包的连接。如果服务器接收到的第一个数据包不符合proxy protocol的格式,那么服务器会直接终止连接。

    Proxy protocol是比较新的协议,但目前已经有很多软件支持,如haproxy、nginx、apache、squid、mysql等等,要使用proxy protocol需要两个角色sender和receiver,sender在与receiver之间建立连接后,会先发送一个带有客户信息的tcp header,因为更改了tcp协议头,需receiver也支持proxy protocol,否则不能识别tcp包头,导致无法成功建立连接。
    nginx是从1.5.12起开始支持的

参考:

https://segmentfault.com/a/1190000022272897

https://kubernetes.io/zh/docs/tutorials/services/source-ip/