使用源 IP

在 Kubernetes 集群中运行的应用程序通过 Service 抽象发现并相互通信,并与外部世界通信。本文档解释了发送到不同类型 Service 的数据包的源 IP 地址会发生什么,以及如何根据您的需求切换此行为。

开始之前

术语

本文档使用了以下术语

NAT
网络地址转换
源 NAT
替换数据包上的源 IP 地址;在本页中,通常是指替换为节点的 IP 地址。
目标 NAT
替换数据包上的目标 IP 地址;在本页中,通常是指替换为 Pod 的 IP 地址
VIP
虚拟 IP 地址,例如 Kubernetes 中的每个 Service 分配的虚拟 IP 地址
kube-proxy
网络守护进程,可在每个节点上协调 Service VIP 管理

先决条件

您需要拥有一个 Kubernetes 集群,并且 kubectl 命令行工具必须配置为与您的集群通信。建议在至少有两个节点(不充当控制平面主机)的集群上运行本教程。如果您还没有集群,可以使用 minikube 创建一个,或者可以使用以下 Kubernetes 游乐场

示例使用一个小的 nginx Web 服务器,它通过 HTTP 标头回显接收到的请求的源 IP 地址。您可以按如下方式创建它

kubectl create deployment source-ip-app --image=registry.k8s.io/echoserver:1.10

输出如下:

deployment.apps/source-ip-app created

目标

  • 通过各种类型的 Service 暴露一个简单的应用程序
  • 了解每种 Service 类型如何处理源 IP NAT
  • 了解保留源 IP 地址涉及的权衡

具有 Type=ClusterIP 的 Service 的源 IP

如果您的 kube-proxy 运行在 iptables 模式(默认模式)下,则从集群内部发送到 ClusterIP 的数据包永远不会进行源 NAT。您可以通过获取节点上 kube-proxy 运行的 https://:10249/proxyMode 来查询 kube-proxy 模式。

kubectl get nodes

输出类似于此

NAME                           STATUS     ROLES    AGE     VERSION
kubernetes-node-6jst   Ready      <none>   2h      v1.13.0
kubernetes-node-cx31   Ready      <none>   2h      v1.13.0
kubernetes-node-jj1t   Ready      <none>   2h      v1.13.0

获取其中一个节点上的代理模式 (kube-proxy 监听端口 10249)

# Run this in a shell on the node you want to query.
curl https://:10249/proxyMode

输出如下:

iptables

您可以通过创建 Service 来覆盖源 IP 应用程序,以测试源 IP 的保留

kubectl expose deployment source-ip-app --name=clusterip --port=80 --target-port=8080

输出如下:

service/clusterip exposed
kubectl get svc clusterip

输出类似于

NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
clusterip    ClusterIP   10.0.170.92   <none>        80/TCP    51s

并从同一集群中的 Pod 命中 ClusterIP

kubectl run busybox -it --image=busybox:1.28 --restart=Never --rm

输出类似于此

Waiting for pod default/busybox to be running, status is Pending, pod ready: false
If you don't see a command prompt, try pressing enter.

然后,您可以在该 Pod 内部运行一个命令

# Run this inside the terminal from "kubectl run"
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc noqueue
    link/ether 0a:58:0a:f4:03:08 brd ff:ff:ff:ff:ff:ff
    inet 10.244.3.8/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::188a:84ff:feb0:26a5/64 scope link
       valid_lft forever preferred_lft forever

…然后使用 wget 查询本地 Web 服务器

# Replace "10.0.170.92" with the IPv4 address of the Service named "clusterip"
wget -qO - 10.0.170.92
CLIENT VALUES:
client_address=10.244.3.8
command=GET
...

无论客户端 Pod 和服务器 Pod 位于同一节点还是不同节点,client_address 始终是客户端 Pod 的 IP 地址。

具有 Type=NodePort 的 Service 的源 IP

默认情况下,发送到具有 Type=NodePort 的 Service 的数据包会进行源 NAT。您可以通过创建 NodePort Service 来测试这一点

kubectl expose deployment source-ip-app --name=nodeport --port=80 --target-port=8080 --type=NodePort

输出如下:

service/nodeport exposed
NODEPORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services nodeport)
NODES=$(kubectl get nodes -o jsonpath='{ $.items[*].status.addresses[?(@.type=="InternalIP")].address }')

如果您正在云提供商上运行,您可能需要为上述报告的 nodes:nodeport 打开防火墙规则。现在,您可以尝试通过上述分配的节点端口从集群外部访问该 Service。

for node in $NODES; do curl -s $node:$NODEPORT | grep -i client_address; done

输出类似于

client_address=10.180.1.1
client_address=10.240.0.5
client_address=10.240.0.3

请注意,这些不是正确的客户端 IP,它们是集群内部 IP。这就是发生的情况

  • 客户端将数据包发送到 node2:nodePort
  • node2 将数据包中的源 IP 地址替换为其自身的 IP 地址 (SNAT)
  • node2 将数据包上的目标 IP 替换为 Pod IP
  • 数据包路由到节点 1,然后路由到端点
  • Pod 的回复路由回 node2
  • Pod 的回复发送回客户端

可视化

source IP nodeport figure 01

图。使用 SNAT 的源 IP Type=NodePort

为了避免这种情况,Kubernetes 具有一项 保留客户端源 IP 的功能。如果您将 service.spec.externalTrafficPolicy 的值设置为 Local,kube-proxy 仅代理到本地端点的请求,并且不会将流量转发到其他节点。这种方法保留了原始源 IP 地址。如果没有本地端点,发送到节点的包将被丢弃,因此您可以依赖于在您可能应用的任何数据包处理规则中正确的源 IP。

按如下方式设置 service.spec.externalTrafficPolicy 字段

kubectl patch svc nodeport -p '{"spec":{"externalTrafficPolicy":"Local"}}'

输出如下:

service/nodeport patched

现在,重新运行测试

for node in $NODES; do curl --connect-timeout 1 -s $node:$NODEPORT | grep -i client_address; done

输出类似于

client_address=198.51.100.79

请注意,您只收到一个回复,其中正确的客户端 IP 来自运行端点 Pod 的一个节点。

这就是发生的情况

  • 客户端将数据包发送到 node2:nodePort,该节点没有端点
  • 数据包被丢弃
  • 客户端将数据包发送到 node1:nodePort,该节点确实具有端点
  • node1 将数据包路由到具有正确源 IP 的端点

可视化

source IP nodeport figure 02

图。源 IP Type=NodePort 保留客户端源 IP 地址

具有 Type=LoadBalancer 的 Service 的源 IP

默认情况下,发送到具有 Type=LoadBalancer 的 Service 的数据包会进行源 NAT,因为处于 Ready 状态的所有可调度 Kubernetes 节点都有资格获得负载均衡的流量。因此,如果数据包到达没有端点的节点,系统会将其代理到具有端点的节点,并将数据包上的源 IP 替换为节点的 IP(如前一节所述)。

您可以通过负载均衡器暴露 source-ip-app 来测试这一点

kubectl expose deployment source-ip-app --name=loadbalancer --port=80 --target-port=8080 --type=LoadBalancer

输出如下:

service/loadbalancer exposed

打印 Service 的 IP 地址

kubectl get svc loadbalancer

输出类似于此

NAME           TYPE           CLUSTER-IP    EXTERNAL-IP       PORT(S)   AGE
loadbalancer   LoadBalancer   10.0.65.118   203.0.113.140     80/TCP    5m

接下来,向 Service 的 external-ip 发送请求

curl 203.0.113.140

输出类似于此

CLIENT VALUES:
client_address=10.240.0.5
...

但是,如果您正在 Google Kubernetes Engine/GCE 上运行,将相同的 service.spec.externalTrafficPolicy 字段设置为 Local 会强制没有 Service 端点的节点通过故意失败健康检查从负载均衡流量的节点列表中删除自身。

可视化

Source IP with externalTrafficPolicy

您可以通过设置注释来测试这一点

kubectl patch svc loadbalancer -p '{"spec":{"externalTrafficPolicy":"Local"}}'

您应该立即看到 Kubernetes 分配的 service.spec.healthCheckNodePort 字段

kubectl get svc loadbalancer -o yaml | grep -i healthCheckNodePort

输出类似于此

  healthCheckNodePort: 32122

service.spec.healthCheckNodePort 字段指向每个节点上提供 /healthz 路径健康检查的端口。您可以测试一下

kubectl get pod -o wide -l app=source-ip-app

输出类似于此

NAME                            READY     STATUS    RESTARTS   AGE       IP             NODE
source-ip-app-826191075-qehz4   1/1       Running   0          20h       10.180.1.136   kubernetes-node-6jst

使用 curl 获取各个节点上的 /healthz 端点

# Run this locally on a node you choose
curl localhost:32122/healthz
1 Service Endpoints found

在另一个节点上,您可能会得到不同的结果

# Run this locally on a node you choose
curl localhost:32122/healthz
No Service Endpoints Found

运行在 控制平面 上的控制器负责分配云负载均衡器。相同的控制器还会分配指向 Service 上每个节点的此端口/路径的 HTTP 健康检查。等待大约 10 秒,让没有端点的 2 个节点失败健康检查,然后使用 curl 查询负载均衡器的 IPv4 地址

curl 203.0.113.140

输出类似于此

CLIENT VALUES:
client_address=198.51.100.79
...

跨平台支持

只有一些云提供商才支持通过具有 Type=LoadBalancer 的 Service 进行源 IP 保留。您正在使用的云提供商可能会以几种不同的方式满足负载均衡器的请求

  1. 使用代理终止客户端连接并到您的节点/端点打开新连接。在这种情况下,源 IP 将始终是云 LB 的 IP,而不是客户端的 IP。

  2. 使用数据包转发器,以便从客户端发送到负载均衡器 VIP 的请求最终到达具有客户端 IP(而不是中间代理)的节点。

第一类负载均衡器必须使用负载均衡器和后端之间商定的协议来通信真实的客户端 IP,例如 HTTP ForwardedX-FORWARDED-FOR 标头,或 代理协议。第二类负载均衡器可以利用上述功能,通过在 Service 上创建指向 service.spec.healthCheckNodePort 字段中存储的端口的 HTTP 健康检查。

清理

删除 Service

kubectl delete svc -l app=source-ip-app

删除 Deployment、ReplicaSet 和 Pod

kubectl delete deployment source-ip-app

接下来

上次修改时间为 2024 年 9 月 8 日下午 5:21 PST:更新 source-ip.md (9c58a926d5)