维持请求的透明度:如何在 Istio 中保留客户端请求的源 IP

科技   2024-02-05 13:57   重庆  

本博文解析了在 Istio 服务网格中服务端获取客户端源 IP 的挑战,并提供了解决方案。将探讨以下问题:

  • • 数据包传输中源 IP 丢失的原因;

  • • 如何确定客户端源 IP;

  • • 在南北向和东西向请求中传递源 IP 的策略;

  • • 针对 HTTP 和 TCP 协议的处理方法。

源 IP 保留的重要性

保留客户端源 IP 的主要理由包括:

  • • 访问控制策略:基于源 IP 执行身份验证或安全策略;

  • • 负载均衡:实现基于客户端 IP 的请求路由;

  • • 数据分析:包含真实源地址的访问日志和监控指标,助力开发人员进行分析。

保留源 IP 的含义

保留源 IP 指的是在请求从客户端发出、经过负载均衡器或反向代理后,避免真实的客户端源 IP 被替换的情况。

以下是源 IP 地址丢失的流程示例:

上面图只是最常见的一种情况。本文考虑到以下几种情况:
  1. 1. 南北向流量:客户端通过负载均衡器(网关)访问服务端

    1. 1. 只有一层网关

    2. 2. 两层或两层以上网关

  2. 2. 东西向流量:网格内部的服务间访问

  3. 3. 协议:HTTP 和 TCP

如何确认客户端源 IP?

在 Istio 服务网格中,Envoy 代理通常会将客户端 IP 添加到 HTTP 请求的 "X-Forwarded-For" 头部中。以下是确认客户端 IP 的步骤:

  1. 1. 检查 x-forwarded-for 头部:包含请求路径上各代理的 IP 地址。

  2. 2. 选择最后一个 IP:通常,最后一个 IP 是最接近服务器的客户端 IP。

  3. 3. 验证 IP 的可信性:检查代理服务器的信任度。

  4. 4. 使用 x-envoy-external-address:Envoy 可以设置此头部,包含客户端真实 IP。

详情请见 Envoy 文档中对 x-forwarded-for 标头 [1] 的说明。对于 TCP/IP 连接,可以通过协议字段解析客户端 IP。

测试环境

GKE

  • • Client Version: v1.28.4

  • • Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3

  • • Server Version: v1.27.7-gke.1121000

Istio

  • • client version: 1.20.1

  • • control plane version: 1.20.1

  • • data plane version: 1.20.1 (12 proxies)

CNI

我们使用了 Cilium CNI,但是没有开启无 kube-proxy 模式。

  • • cilium-cli: v0.15.18 compiled with go1.21.5 on darwin/amd64

  • • cilium image (default): v1.14.4

  • • cilium image (stable): unknown

  • • cilium image (running): 1.14.5

Node

节点名称内部 IP备注
gke-cluster1-default-pool-5e4152ba-t5h310.128.0.53
gke-cluster1-default-pool-5e4152ba-ubc910.128.0.52
gke-cluster1-default-pool-5e4152ba-yzbg10.128.0.54Ingress Gateway Pod 所在节点

执行测试的本地客户端电脑的公网 IP:123.120.247.15

部署测试示例

下图展示了测试方式:

首先参考 Istio 文档 [2] 部署 Istio,然后为 default 命名空间开启 sidecar 自动注入:

kubectl label namespace default istio-injection=enabled

在 Istio 中部署 echo-server 应用测试。echo-server 是一个基于 Nginx 的服务器,用于回显客户端发送的请求信息,例如请求头、客户端地址、请求方法等。

kubectl create deployment echo-server --image=registry.k8s.io/echoserver:1.4
kubectl expose deployment echo-server --name=clusterip --port=80 --target-port=8080

创建 Ingress Gateway:

cat>config.yaml<<EOF
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: clusterip-gateway
spec:
  selector:
    istio: ingressgateway # 根据你的环境选择适当的 selector
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - "clusterip.jimmysong.io" # 替换成你想要使用的主机名
---
apiVersion:  networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: clusterip-virtualservice
spec:
  hosts:
    - "clusterip.jimmysong.io" # 替换成与 Gateway 中相同的主机名
  gateways:
    - clusterip-gateway # 这里使用 Gateway 的名称
  http:
    - route:
        - destination:
            host: clusterip.default.svc.cluster.local # 替换成你的 Service 的实际主机名
            port:
              number: 80 # Service 的端口
EOF

kubectl apply -f config.yaml

查看 Ingress Gateway 中的 Envoy 日志:

kubectl logs -f deployment/istio-ingressgateway -n istio-system

查看 Sleep Pod 中的 Envoy 日志:

kubectl logs -f deployment/sleep -n default -c istio-proxy

查看 Source IP App 中的 Envoy 日志:

kubectl logs -f deployment/echo-server -n default -c istio-proxy

获取网关公网 IP:

export GATEWAY_IP=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

在本地使用 curl 测试:

curl -H "Host: clusterip.jimmysong.io" $GATEWAY_IP

资源 IP

当部署好测试应用后,你需要获取与以下资源的 IP 地址。在接下来的实验环节中将会用到。

Pod

下面是初始状况下的 Pod IP,随着对 Deployment 的补丁,Pod 会重建,名称和 IP 地址都会变。

Pod 名称Pod IP
echo-server-6d9f5d97d7-fznrq10.32.1.205
sleep-9454cc476-2dskx10.32.3.202
istio-ingressgateway-6c96bdcd74-zh46d10.32.1.221

Service

Service 名称Cluster IPExternal IP
clusterip10.36.8.86-
sleep10.36.14.12-
istio-ingressgateway10.36.4.12735.188.212.88

南北向流量

我们首先考虑客户端位于 Kubernetes 集群外,通过负载均衡器来访问集群内部服务的情况。

测试 1:Cluster 流量策略、iptables 流量劫持

这是通过以上步骤部署完测试应用后的默认情况,也是大家遇到的所谓的源 IP 地址丢失的情况。

curl 测试:

curl -H "Host: clusterip.jimmysong.io" $GATEWAY_IP

你只需要关注 client_address 和 x-forwarded-for 这两个结果即可。下文的 curl 测试结果中将省略其他信息。

该结果中字段的含义:

  • • client_address:通过解析 TCP/IP 协议而获取的客户端 IP 地址,在 Envoy 中称为 remote address。

  • • x-forwarded-forx-forwarded-for (XFF) 是一个标准的代理头部,用于指示请求在从客户端到服务器的过程中经过的 IP 地址。一个合规的代理会在代理请求之前将最近客户端的 IP 地址添加到 XFF 列表中。详见 Envoy 文档 [3]

从测试结果中我们可以看出,源 IP 地址变成了 Ingress Gateway Pod 所在节点的 IP 地址(10.128.0.54)。

下图展示是两个 Pod 中的数据包流量路径。

对于这种情况,要想保留源 IP 其实很简单,而且也是 Kubernetes 提供的标准选项。

源 IP 地址是如何丢失的?

下图展示客户端的源 IP 是如何在请求过程中丢失的。

因为负载均衡器将数据包发送到 Kubernetes 集群中的任意节点,在此过程中会进行 SNAT,导致最终发送到 Server Pod 中的客户端源 IP 丢失。

如何保留客户端源 IP

你可以通过设置 service 中的 externalTrafficPolicy 字段为 Local 控制负载均衡器保留源 IP。

externalTrafficPolicy

externalTrafficPolicy 是一个标准 Service 选项 [4],用于定义传入 Kubernetes 节点的流量是否进行负载均衡以及如何进行负载均衡。Cluster 是默认策略,但 Local 通常用于保留传入集群节点的流量的来源 IP。Local 会在集群节点上有效停用负载均衡,以使本地 Pod 接收的流量看到原始来源 IP 地址。

也就是说将 externalTrafficPolicy 设置为 Local 就可以让数据包绕过节点上的 kube-proxy,而直达目标 Pod。但是大多数人在 Kubernetes 中创建 Service 时都没有设置 externalTrafficPolicy,所以使用了默认的 Cluster 策略。

既然 Service 采用 Local 外部流量策略可以保留客户端的源 IP 地址,那为什么 Kubernetes 不默认采用呢?

通过 Local 模式暴露服务以获取客户端源 IP 是一种对可靠性的妥协,如果大家有更好的方案欢迎推荐给我。

Kubernetes 默认将 Service 的 externalTrafficPolicy 设置为 Cluster 而非 Local,主要是基于以下考虑:

  1. 1. 负载均衡:确保流量在所有节点之间平均分配,避免单个节点过载。

  2. 2. 高可用性:允许流量被集群中任何节点接收,提高服务的可用性。

  3. 3. 简化配置Cluster 模式降低了网络配置的复杂性。

  4. 4. 性能优化:避免由于保留客户端源 IP 而引起的潜在性能问题。

  5. 5. 通用性:兼容多种网络环境和集群配置,适应更广泛的使用场景。

测试 2:Local 流量策略、iptables 流量劫持

将 Ingress Gateway Service 设置为 Local 外部流量策略:

kubectl patch svc istio-ingressgateway -p '{"spec":{"externalTrafficPolicy":"Local"}}' -n istio-system

Curl 测试:

curl -H "Host: clusterip.jimmysong.io" $GATEWAY_IP

通过 Envoy 日志可以得出现在的数据包路径:

客户端源 IP 被正确识别为 123.120.247.15

东西向流量

在 Istio 默认配置的情况下,对于东西向流量,服务端也无法获取正确的客户端源 IP。

测试 3:Local 流量策略、tproxy 流量劫持

将 Source IP App 中的流量拦截方式从 iptables 修改为 tproxy[5]

kubectl patch deployment -n default echo-server -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/interceptionMode":"TPROXY"}}}}}'

注意:此时 Source IP App 的 Pod 将会重建,新的 Pod 名称是 echo-server-686d564647-r7nlq,IP 地址是 10.32.1.140。

Curl 测试:

kubectl exec -it deployment/sleep -it -- curl clusterip

下图展示了数据包路径:

客户端 IP 被正确识别为 10.32.3.202

测试 4:Local 流量策略、iptables 流量劫持

将 Source IP App 中的流量拦截方式恢复为 redirect:

kubectl patch deployment -n default echo-server -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/interceptionMode":"REDIRECT"}}}}}'

注意:此时 Source IP App 的 Pod 将会重建,新的 Pod 名称是 echo-server-6d9f5d97d7-bgpk6,IP 地址是 10.32.1.123。

Curl 测试:

kubectl exec -it deployment/sleep -it -- curl clusterip

下图展示了数据包路径:

客户端源 IP 被识别为 127.0.0.6

单层代理场景总结

在单层代理的情况下,只需要将 Ingress Gateway 的 Service 的 externalTrafficPolicy 设置为 Local 即可保留客户端源 IP。将目标服务的流量拦截模式修改为 TPROXY 即可以保留东西向请求中的源 IP。

多层代理

如果流量在进入 Istio Mesh 前已经经过的多层代理转发,每次流量经过代理时,代理解析 HTTP 流量并将其自身的 IP 地址追加到 x-forwarded-for 标头中。那么可以使用 numTrustedProxies 配置您信任的代理跳数,请参考 Envoy 文档 [6] 了解如何确定 X-Forwarded-For 标头和受信任的客户端地址。

实际上我们很难确定流量在到达 Istio Mesh 时究竟经过了几层代理,但你可以根据 x-forwarded-for 标头了解流量的转发路径。

下图展示了 Envoy 如何根据 x-forwarded-for 标头和 xff_num_trusted_hops(对应 Istio 中的 numTrustedProxies 配置)来确认源 IP 的流程。详见 Envoy 文档 [7]

执行下面的命令为入口网关开启受信代理数量配置:

kubectl patch deployment istio-ingressgateway -n istio-system -p '{"spec":{"template":{"metadata":{"annotations":{"proxy.istio.io/config":"{\"gatewayTopology\":{\"numTrustedProxies\": 2,\"forwardClientCertDetails\":\"SANITIZE_SET\"}}"}}}}}'

当 Istio Gateway 收到这个请求时,它将 X-Envoy-External-Address 头设置为您 curl 命令中 X-Forwarded-For 头中的倒数第二个地址(numTrustedProxies: 2)。根据 Istio 的文档,Gateway 在将其转发到服务端负载之前,会将自己的 IP 附加到 X-Forwarded-For 头中。但实际情况是标头中只有客户端源 IP 和 External Gateway Pod IP。

你可以执行下面的命令取消这个补丁:

kubectl patch deployment istio-ingressgateway -n istio-system -p '{"spec":{"template":{"metadata":{"annotations":{"proxy.istio.io/config":"{}"}}}}}'

TCP 流量

上文所说的使用标头获取客户端源 IP 的方式只适用于 L7 网络,对于 L4 网络的 TCP 流量可以使用 Proxy 协议。

Proxy 协议是一种网络协议,它在 TCP 连接的起始处添加了一个协议头部,用于传递连接过程中的一些元数据,如客户端的真实 IP 地址和端口号。这对于在负载均衡器(LB)后部署的应用程序非常有用,因为负载均衡器通常会更改客户端的原始 IP 地址成 LB 的地址,导致服务端无法知晓客户端的真实 IP。很多代理软件都支持 Proxy Protocol,比如 Envoy[8] 和 HAProxy、NGINX 等。

你可以使用下面的命令为 Ingress Gateway 打上补丁,以支持 Proxy 协议:

kubectl patch deployment istio-ingressgateway -n istio-system -p '{"spec":{"template":{"metadata":{"annotations":{"proxy.istio.io/config":"{\\"gatewayTopology\\":{\\"proxyProtocol\\":{}}}"}}}}}'

注意:不是所有的公有云中的 Kubernetes 中 LoadBalancer 类型的 Service 创建的的负载均衡器都支持该配置。比如 GKE 中就不支持。在 AWS NLB 中开启 Proxy 协议请参考该博客 [9]

Envoy 并不建议使用 Proxy 协议,因为它:

  • • 只支持 TCP 协议

  • • 必须上游主机支持

  • • 可能影响性能

关于 Envoy 对 Proxy 协议的支持请参考该文档 [10]

应用场景示例

下面是常见的两个源 IP 地址的应用场景。

基于源 IP 地址的访问控制

在 Istio 的入口网关配置基于源 IP 的访问控制策略。这通过设置入口网关的授权策略,根据源 IP 地址实现访问限制。

下图展示了基于源 IP 地址的访问控制流程图。

场景假设

假设请求经过三个代理,其 IP 地址分别为 1.1.1.12.2.2.2 和 3.3.3.3。在 Ingress Gateway 中,numTrustedProxies 被设置为 2,因此 Istio 信任的源 IP 为 2.2.2.2(即 x-envoy-external-address)。

curl -H "Host: clusterip.jimmysong.io" -H 'X-Forwarded-For: 1.1.1.1,2.2.2.2,3.3.3.3' $GATEWAY_IP

屏蔽特定源 IP

若需屏蔽来自 2.2.2.2 的请求,可以使用以下授权策略:

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: ingress-policy
  namespace: istio-system
spec:
  selector:
    matchLabels:
      app: istio-ingressgateway
  action: DENY
  rules:
    - from:
        - source:
            remoteIpBlocks:
            - "2.2.2.2/24"

使用最终客户端 IP

如果希望识别与 Istio Mesh 直连的客户端 IP(即 x-forwarded-for 中的最后一个 IP,例如 123.120.234.15),则需要用 ipBlocks 配置:

apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: ingress-policy
  namespace: istio-system
spec:
  selector:
    matchLabels:
      app: istio-ingressgateway
  action: DENY
  rules:
    - from:
        - source:
            ipBlocks:
            - "123.120.234.15/24"

这种方法通过配置 Istio 的入口网关授权策略,可以有效地实现基于源 IP 的访问控制。它允许管理员根据不同的需求(如屏蔽特定 IP 或信任最终客户端 IP)灵活设定规则,从而增强了服务的安全性和灵活性。

基于源 IP 地址的负载均衡

要在 Istio 中根据源 IP 地址配置负载均衡策略,你需要使用 DestinationRule 资源,并指定 LOAD_BALANCER_POLICY_CONSISTENT_HASH 策略。这种策略允许您根据一致性哈希算法为流量分配目标,可以基于源 IP 地址来实现会话亲和性(session affinity),确保来自同一源 IP 的请求被路由到相同的目标。

源 IP 地址负载均衡示例

下面是一个示例配置,展示了如何使用 DestinationRule 来根据源 IP 地址实现负载均衡:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: example-destination-rule
spec:
  host: example-service
  trafficPolicy:
    loadBalancer:
      consistentHash:
        httpHeaderName: x-forwarded-for # 这通常包含源 IP 地址,适用于经过代理或负载均衡器转发的流量。

注意,如果直接连接到 Istio Ingress Gateway 而不经过其他代理,你可能需要根据实际情况调整 httpHeaderName 或使用其他哈希键,例如 useSourceIp,如下所示:

spec:
  trafficPolicy:
    loadBalancer:
      consistentHash:
        useSourceIp: true

注意:

  • • 使用源 IP 地址作为负载均衡的键时,请确保您理解这可能如何影响流量分布,特别是在源 IP 地址分布不均匀的情况下。

  • • 正如上文所述,在某些环境中,原始的源 IP 可能会被网络设备(如负载均衡器或 NAT 设备)修改,需要确保 x-forwarded-for 头或其他相应机制能准确反映原始的客户端 IP。

总结

  • • 保留源 IP 对于实施访问控制、负载均衡和数据分析至关重要。

  • • Envoy 代理使用 X-Forwarded-For 头部来处理 HTTP 请求中的客户端源 IP。

  • • 通过设置 externalTrafficPolicy 和选择合适的流量劫持方式(REDIRECT 或 TPROXY),可以在南北向和东西向流量中正确获取客户端源 IP。

  • • 处理经过多层代理的流量时,numTrustedProxies 配置是关键。

  • • 对于 TCP 流量,Proxy 协议是一个有效的解决方案。

参考

  • • x-forwarded-for - envoyproxy.io[11]

  • • Proxy protocol on AWS NLB and Istio ingress gateway - istio.io[12]

  • • Configuring Gateway Network Topology - istio.io[13]

  • • IP Transparency - envoyproxy.io[14]

  • • Using Source IP - kubernetes.io[15]

  • • Proxy Protocol - github.com[16]

引用链接

[1] x-forwarded-for 标头: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#config-http-conn-man-headers-x-forwarded-for
[2] Istio 文档: https://istio.io/latest/docs/setup/install/
[3] Envoy 文档: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for
[4] 标准 Service 选项: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip
[5] tproxy: https://jimmysong.io/blog/what-is-tproxy/
[6] Envoy 文档: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for
[7] Envoy 文档: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for
[8] Envoy: https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/listener_filters/proxy_protocol
[9] 该博客: https://istio.io/latest/blog/2020/show-source-ip/
[10] 该文档: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/ip_transparency#proxy-protocol
[11] x-forwarded-for - envoyproxy.io: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for
[12] Proxy protocol on AWS NLB and Istio ingress gateway - istio.io: https://istio.io/latest/blog/2020/show-source-ip/
[13] Configuring Gateway Network Topology - istio.io: https://istio.io/latest/docs/ops/configuration/traffic-management/network-topologies/
[14] IP Transparency - envoyproxy.io: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_features/ip_transparency
[15] Using Source IP - kubernetes.io: https://kubernetes.io/docs/tutorials/services/source-ip/
[16] Proxy Protocol - github.com: https://github.com/haproxy/haproxy/blob/master/doc/proxy-protocol.txt

欢迎加入云原生社区或向社区投稿,点击阅读原文了解更多。

运维开发故事
由一群志同道合的小伙伴共同维护,有运维也有开发,内容不限于Linux运维,devops工具链,k8s容器化技术,监控,日志收集,网络安全,Python或GO开发,团队成员有乔克、wanger、冬哥、素心、华仔、郑哥、夏老师
 最新文章