1.深入分析

访问 Service 时造成 Client 真实 IP 不可见的原因是 KubeProxy 通过 Iptables 对数据包进行了 SNAT,导致数据包中源IP被替换为了代理节点的物理机IP或者Pod网关IP。

参考【KubeProxy:IPtables 模式】中梳理的 kube-proxy 自定义 Chain 逻辑,我们可以得知 kube-proxy 可以通过 MasqueradeAll 配置对那些数据包进行 MASQUERADE(SNAT) 操作:

  • MasqueradeAll=true:所有访问 Service 的数据包被 SNAT
  • MasqueradeAll=false:源地址不属于 ClusterCIDR(Pod 网段) 的数据包访问 Service 被 SNAT

默认情况下 MasqueradeAll 为 false,即在 Pod 中通过 ClusterIP 访问其他服务时,服务端是能够获取客户端的正确 IP。

但在测试过程中我发现了以下例外:

Pod 通过 ClusterIP 访问自身(访问的后端是自己),此时服务端无法获得 Pod 自己的真实IP,即此时 kube-proxy 会对报文进行 SNAT!!

为了保证通过 NodePort 访问服务时 Client IP 能够保留,Kubernetes 在 Service 引入了 externalTrafficPolicy 参数,该参数支持 Cluster/Local 两种配置。

externalTrafficPolicy 配置为 Local 时,kube-proxy 不会对数据包进行 MASQUERADE,此时服务端能够获取 Client IP,但代价是 Client 只能通过运行着 Pod 的 NodeIP 访问服务,否则数据包会被丢弃。

externalTrafficPolicy = Local 时也存在特殊场景: Node 上通过本地 IP 访问 NodePort 时,所有节点都能正常访问到后端 Pod,数据包会被 SNAT 但是不会被丢弃。

总结上述结论后,我们发现一些数据包只进行 DNAT 就能正常工作,但绝大多数场景中 kube-proxy 需要同时对数据包进行 DNAT/SNAT ,那么这其中的差别是什么呢?

为了回答上述问题,我们需要先明确一些TCP/IP协议栈处理数据包的逻辑:Client 请求数据包的IP头信息是 (srcIP,dstIP)时,应答数据包的IP头信息必须是(dstIP,srcIP),如果请求目的地址和应答源地址不一致,那么数据包会被直接丢弃

当前 iptables 中包括以下两种类型的 NAT 操作:

  • SNAT/MASQUERADE:将数据包中的源地址 IP,替换为指定 IP,而 MASQUERADE 操作是将源地址 IP 替换为发出数据包网卡的IP。

    • 应用场景:网计算机通过公共网出口范围互联网(PS:docker 映射容器 pod 是非常典型的应用)
  • DNAT:将数据包中的目的地址 IP,替换为指定 IP

    • 应用场景:内网计算机向公网暴露自身服务

NAT 底层实现基于 Linux Conntrack(链接追踪机制):内核发送数据包时通过 Conntrack 记录下数据包的 tuple 信息,并进行地址转换,接受返回数据包时查询 Conntrack 表找出之前的记录,并进行反向地址转换

关于 conntrack 可以参考:https://arthurchiao.art/blog/conntrack-design-and-implementation-zh/

大部分介绍 NAT 的文章中总是忽略了,代理服务器接收到返回数据包时对地址进行还原的场景,但这恰恰是理解 kube-proxy 为什么需要对数据包进行 SNAT 的原因。

假设 client 在范围 ClusterIP 时只进行 DNAT:

从上图可以看出,从 Pod 发出应答数据包没有还原源地址,直接从宿主机转发到了 Client。Client 判断包的源 IP 异常,直接将包丢弃。

当 kube-proxy 同时进行 snat/dnat 时,从宿主机发出的应答数据包又回到了代理节点,并成功被还原转发,最后被 Client 接收。

最后我们分析 externalTrafficPolicy = Local 场景。这个场景中,kube-proxy 限制 client 只能访问有 pod 运行的 node,此时“代理 node ”和“宿主 node ”和二为一,因此能够在不SNAT的场景下正常工作。

2. 规避方案

在大多数场景中,仅通过 externalTrafficPolicy 参数难以满足保留真实 IP 的需求,为此本文整理了以下规避方案。

1.通过 Igress

对于 HTTP 服务用户可以通过 Daemonset + HostNet 的方式部署 ingress 控制器,并使用 Ingress 对外暴露服务。http 报文头经过 ingress 控制器时,会被添加 X-Real-Ip 信息以记录 client 真实 IP。

使用traefik/whoami 作为测试服务端,部署文件参考,可以进行验证。

通过 curl 命令请求 whoami 容器的 80 端口,输出如下:

1
2
3
4
5
6
7
8
Hostname: whoami-6bd6b584c7-br6tl # 容器主机名称
IP: 127.0.0.1                     # 容器IP
IP: 10.244.34.225                 # 容器IP     
RemoteAddr: 10.244.30.192:13928   # 请求的 REMOTE_ADDR 字段,即客户端IP地址
GET / HTTP/1.1
Host: 172.28.126.36:21769         # Client请求的地址
User-Agent: curl/7.29.0
Accept: */*

当使用 Ingress 访问 whoami 容器时,虽然 RemoteAddr 返回的依然不是 Client 实际IP,但是 HTTP 返回头中多处了以下内容,其中通过 X-Real-Ip 可以获取真实 Client IP 地址:

1
2
3
4
5
6
7
8
略
X-Forwarded-For: 172.28.126.39   # Client地址
X-Forwarded-Host: whoami.rccp.io # Client主机名
X-Forwarded-Port: 80             # Client转发端口
X-Forwarded-Proto: http
X-Real-Ip: 172.28.126.39         # 真实IP地址
X-Request-Id: fa6d6261f72abdae8b3f1403730aca59
X-Scheme: http

该方式的优势是实施非常简单,但是基于TCP连接的服务不起作用,并且可能需要修改业务代码。

2. LoadBalancer

LoadBalancer 是 Kubernetes 原生的 Cluster 类型之一。它的应用场景和 NodePort 类似,但添加了负载均衡功能,能够将流量均匀的分布在不同 Node 上。

但用户配置 LoadBalancer 类型的 Service 时, 可以同时将 externalTrafficPolicy 配置为 Local,并开启 LoadBalancer 的健康检查机制,将没有 Pod 运行的 Node 将从负载均衡列表中剔除。 此时 LB-IP 将只绑定在有 Pod 服务的 Node上,问题就转变成基于 NodePort 范围服务的场景。

LoadBalancer 主要应用在公有云部署场景中,当前部署在物理机房中的 LoadBalancer 工具有以下几个,这些工具能否实现上述效果需要进一步验证:

3. ipvs

正如【KubeProxy:IPVS 模式】 介绍,kube-proxy 的 ipvs 模式是基于 nat 工作的,这种情况下难免需要通过 SNAT 来保证可用性。

实际上 ipvs 还可以工作在 tunnel 和 direct 模式下,那么是否有 kube-proxy 的替代品能够基于 ipvs tunnel 或者 ipvs direct 实现类似功能呢? 答案是有的!!!

kube-router 是 cloudnativelabs 开源的一款 CNI 插件,该插件是一款能在构建 Pod 网络的同时替换 kube-proxy 的工具。

kube-router 的 CNI 功能和其他同类竞品相比并没啥特色,但是它替换 kube-proxy 之后为 Service 提供了基于 ipvs tunnel 的 externalIP,能够实现保留 Client real ip 的需求。

kube-router 的详细工作原理可以参考 kube-router dsr,以下是网络拓扑:

为了让上述拓扑能够正常工作,kube-router 使用了一些小技巧:

  • kube-router 为了能够在 pod 中创建 ipip 设备 kube-tunnel-if ,在所有节点的 kube-router 控制器中挂载了 cri-endpoint,通过直接进入 pod 网络命名空间的方式添加网络设备。
  • kube-router 使用了 ip rule 强制传入的 DSR 数据包使用特定的路由表,来保证数据包能够进入 INPUT 链从而触发 ipvs 工作(???)
  • 通过 TCP MSS 模块减轻 mtu 变化带来的问题

结论,kube-router 基于 ivps tunnel 的方案存在以下问题:

  • 必须基于 externalip 工作
  • pod 和 client 必须三层可达
  • kube-router 需要在 pod 中注入 ipip 设备,虽然该设备是静默的,但是破坏了 cni 规范
  • 个人测试中发现 client 和 node 在同一个网络空间时似乎有问提???
  • 需要完全替换 kube-proxy ,但是 kube-router 的常规功能似乎有一些 bug ???

优势:

  • 能够应用在 TCP 场景
  • 原理相比 ebpf 比较简单

4. ebpf

Calico 和 Cillium 均支持通过 ebpf 机制替换 kube-proxy,这种模式下两者均重新设计了一套自己的 NAT/Conntrack 机制,从而实现访问 Service 时保留 Real IP。

参考:https://projectcalico.docs.tigera.io/about/about-kubernetes-services

5. ttm

百度云内部使用的一种方案:在代理服务端修改TCP报文,并将源 ip 信息写入到 TCP Option 字段中。

Server 加载TTM模块后,TTM模块通过Hook Linux内核TCP协议栈的相关函数, 从TCP报文的tcp option字段中解析出客户端真实源IP和端口号。

参考:https://github.com/baidu/ttm

6. Proxy Protocol

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的格式,那么服务器会直接终止连接

参考:https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt

参考文档