在服务网格环境下如何保持服务访问时的客户端源IP

本文介绍在服务网格环境下如何保持服务访问时的客户端源IP。

前提条件

背景信息

原始IP在很多情况下被广泛使用,典型的使用场景包括:

  • 应用的访问控制:例如,许多应用程序在检测到用户从不同地域登录时强制执行额外的身份验证,可以通过获取原始IP来完成。

  • 简单的会话保持:可以根据客户端的地址,基于源IP做负载均衡,将来自同一个客户端的请求,转发到同一个服务实例。

  • 访问日志和监控统计:访问日志和监控指标中包含真实的源地址,有助于开发人员进行分析统计。

云上的负载均衡器也支持将客户端源IP传递到后端服务。Istio应提供允许应用程序获取原始源IP的能力。但在使用Istio时,Pod注入了Sidecar代理之后,所有入站流量都是从Envoy重定向。目前,Envoy将流量发送到绑定了本地地址(127.0.0.1)的应用程序,所以应用看不到真正的原始IP。

部署应用示例

  1. 部署sleep应用。

    1. 使用以下内容,创建sleep.yaml

      apiVersion: v1
      kind: ServiceAccount
      metadata:
        name: sleep
      ---
      apiVersion: v1
      kind: Service
      metadata:
        name: sleep
        labels:
          app: sleep
          service: sleep
      spec:
        ports:
        - port: 80
          name: http
        selector:
          app: sleep
      ---
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: sleep
      spec:
        replicas: 1
        selector:
          matchLabels:
            app: sleep
        template:
          metadata:
            labels:
              app: sleep
          spec:
            terminationGracePeriodSeconds: 0
            serviceAccountName: sleep
            containers:
            - name: sleep
              image: curlimages/curl
              command: ["/bin/sleep", "3650d"]
              imagePullPolicy: IfNotPresent
              volumeMounts:
              - mountPath: /etc/sleep/tls
                name: secret-volume
            volumes:
            - name: secret-volume
              secret:
                secretName: sleep-secret
                optional: true
    2. 执行以下命令,部署sleep应用。

      kubectl -n default apply -f  sleep.yaml
  2. 部署httpbin应用。

    1. 使用以下内容,创建httpbin.yaml

      apiVersion: v1
      kind: Service
      metadata:
        name: httpbin
        labels:
          app: httpbin
      spec:
        ports:
        - name: http
          port: 8000
        selector:
          app: httpbin
      ---
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: httpbin
      spec:
        replicas: 1
        selector:
          matchLabels:
            app: httpbin
            version: v1
        template:
          metadata:
            labels:
              app: httpbin
              version: v1
          spec:
            containers:
            - image: docker.io/citizenstig/httpbin
              imagePullPolicy: IfNotPresent
              name: httpbin
              ports:
              - containerPort: 8000
    2. 执行以下命令,部署httpbin应用。

      kubectl -n default apply -f  httpbin.yaml

场景一:东西向流量

未开启TPROXY

在Istio中,东西向服务访问时,由于Sidecar的注入,所有进出服务的流量均被Envoy拦截代理,然后再由Envoy将请求转给应用。因此,应用收到的请求的源地址,是Envoy的访问地址127.0.0.6

  1. 执行以下命令,查看Pod状态。

    kubectl -n default get pods -o wide

    预期输出:

    NAME                            READY   STATUS        RESTARTS   AGE     IP             NODE                     NOMINATED NODE   READINESS GATES
    httpbin-c85bdb469-4ll2m         2/2     Running       0          3m22s   172.17.X.XXX   cn-hongkong.10.0.0.XX    <none>           <none>
    sleep-8f764df66-q7dr2           2/2     Running       0          3m9s    172.17.X.XXX   cn-hongkong.10.0.0.XX    <none>           <none>

    由预期输出得到,sleep应用的地址为172.17.X.XXX

  2. 执行以下命令,从sleep容器发起请求。

    kubectl -n default exec -it deploy/sleep -c sleep -- curl http://httpbin:8000/ip

    预期输出:

    {
      "origin": "127.0.0.6"
    }

    由预期输出得到,httpbin应用收到请求的源地址是Envoy的访问地址127.0.0.6,而不是sleep应用的地址。

  3. 从Socket信息中确认源IP地址是否为127.0.0.6

    1. 登录到httpbin容器,执行以下命令,安装netstat。

      apt update & apt install net-tools
    2. 退出httpbin容器,执行以下命令,根据80端口查看运行相关信息。

      kubectl -n default exec -it deploy/httpbin -c httpbin -- netstat -ntp | grep 80

      预期输出:

      tcp        0      0 172.17.X.XXX:80         127.0.0.6:42691         TIME_WAIT   -

      由预期输出得到,源IP地址为127.0.0.6

  4. 查看httpbin Pod中的代理日志内容。

    格式化处理之后的日志示例如下:

    {
      "trace_id":null,
      "bytes_received":0,
      "upstream_host":"172.17.X.XXX:80",
      "authority":"httpbin:8000",
      "downstream_remote_address":"172.17.X.XXX:56160",
      "upstream_service_time":"1",
      "upstream_transport_failure_reason":null,
      "istio_policy_status":null,
      "path":"/ip",
      "bytes_sent":28,
      "request_id":"4501a50a-dab0-44c9-b52c-2a4f425a****",
      "protocol":"HTTP/1.1",
      "method":"GET",
      "duration":1,
      "start_time":"2022-11-22T16:09:30.394Z",
      "user_agent":"curl/7.86.0-DEV",
      "upstream_local_address":"127.0.0.6:42169",
      "response_flags":"-",
      "route_name":"default",
      "response_code":200,
      "upstream_cluster":"inbound|80||",
      "x_forwarded_for":null,
      "downstream_local_address":"172.17.X.XXX:80",
      "requested_server_name":"outbound_.8000_._.httpbin.default.svc.cluster.local"
    }

    由日志得到:

    • "downstream_remote_address":"172.17.X.XXX:56160":sleep的地址。

    • "downstream_local_address":"172.17.X.XXX:80":sleep访问的目标地址。

    • "upstream_local_address":"127.0.0.6:42169":httpbin Envoy连接httpbin的local address(此时获得的源IP地址是127.0.0.6)。

    • "upstream_host":"172.17.X.XXX:80":httpbin Envoy访问的目标地址。

开启TPROXY保持源IP

修改httpbin应用的deployment,使用TPROXY作为入流量拦截模式。

  1. 执行以下命令,修改httpbin应用的deployment。

    kubectl patch deployment -n default httpbin -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/interceptionMode":"TPROXY"}}}}}'                       
  2. 执行以下命令,从sleep容器发起请求。

    kubectl -n default exec -it deploy/sleep -c sleep -- curl http://httpbin:8000/ip

    预期输出:

    {
      "origin": "172.17.X.XXX"
    }

    由预期输出得到,httpbin可以得到sleep端的真实IP。

  3. 执行以下命令,根据80端口查看运行相关信息。

    说明

    Pod重启之后,需要重新安装netstat。

    kubectl -n default exec -it deploy/httpbin -c httpbin -- netstat -ntp | grep 80

    预期输出:

    tcp        0      0 172.17.X.XXX:80         172.17.X.XXX:36728      ESTABLISHED -

    由预期输出得到,源IP地址为172.17.X.XXX

  4. 查看httpbin Pod中的代理日志内容。

    格式化处理之后的日志示例如下:

    {
      "route_name":"default",
      "bytes_received":0,
      "trace_id":null,
      "request_id":"1ccabe60-63cf-469b-8565-99cac546****",
      "upstream_cluster":"inbound|80||",
      "response_flags":"-",
      "protocol":"HTTP/1.1",
      "upstream_transport_failure_reason":null,
      "requested_server_name":"outbound_.8000_._.httpbin.default.svc.cluster.local",
      "response_code":200,
      "user_agent":"curl/7.86.0-DEV",
      "start_time":"2022-11-22T16:03:32.803Z",
      "path":"/ip",
      "authority":"httpbin:8000",
      "bytes_sent":31,
      "downstream_remote_address":"172.17.X.XXX:39058",
      "upstream_service_time":"1",
      "method":"GET",
      "downstream_local_address":"172.17.X.XXX:80",
      "duration":1,
      "upstream_host":"172.17.X.XXX:80",
      "istio_policy_status":null,
      "upstream_local_address":"172.17.X.XXX:46129",
      "x_forwarded_for":null
    }

    由日志得到:

    • "downstream_remote_address":"172.17.X.XXX:39058":sleep的地址。

    • "downstream_local_address":"172.17.X.XXX:80":sleep访问的目标地址。

    • "upstream_local_address":"172.17.X.XXX:46129":httpbin Envoy连接httpbin的local address(即sleep的IP地址)。

    • "upstream_host":"172.17.X.XXX:80":httpbin Envoy访问的目标地址。

场景二:南北向流量

对于南北向流量,客户端先请求负载均衡,然后将请求转给Istio ingressgateway,再转到后端服务。由于中间多了ingressgateway,使得获取客户端源IP地址变得更加复杂。下文介绍如何设置及验证HTTP和HTTPS协议的请求保留源IP地址。

HTTP协议的请求

未设置保留源IP

  1. 使用以下内容,创建http-demo.yaml,以HTTP协议访问httpbin。

    apiVersion: networking.istio.io/v1alpha3
    kind: Gateway
    metadata:
      name: httpbin-gw-httpprotocol
      namespace: default
    spec:
      selector:
        istio: ingressgateway
      servers:
        - hosts:
            - '*'
          port:
            name: http
            number: 80
            protocol: HTTP
    ---
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: httpbin
      namespace: default
    spec:
      gateways:
        - httpbin-gw-httpprotocol
      hosts:
        - '*'
      http:
        - route:
            - destination:
                host: httpbin
                port:
                  number: 8000
  2. 执行以下命令,部署网关和虚拟服务。

    kubectl -n default apply -f  http-demo.yaml
  3. 执行以下命令,通过ingressgateway访问httpbin。

    export GATEWAY_URL=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    curl http://$GATEWAY_URL:80/ip

    预期输出:

    {
      "origin": "10.0.0.93"
    }

    由预期输出得到,返回的IP地址为Kubernetes集群的节点地址。

  4. 查看ingressgateway的访问日志。

    日志示例如下:

    {
      "upstream_service_time":"1",
      "response_code":200,
      "protocol":"HTTP/1.1",
      "bytes_sent":28,
      "upstream_cluster":"outbound|8000||httpbin.default.svc.cluster.local",
      "start_time":"2022-11-23T03:29:20.017Z",
      "istio_policy_status":null,
      "upstream_transport_failure_reason":null,
      "trace_id":null,
      "route_name":null,
      "request_id":"292903be-a889-4d5d-83a0-ab1f5d1a****",
      "method":"GET",
      "upstream_host":"172.17.X.XXX:80",
      "duration":1,
      "path":"/ip",
      "downstream_local_address":"172.17.X.XXX:80",
      "authority":"47.242.XXX.XX",
      "user_agent":"curl/7.79.1",
      "downstream_remote_address":"10.0.0.93:5899",
      "upstream_local_address":"172.17.X.XXX:54322",
      "requested_server_name":null,
      "x_forwarded_for":"10.0.0.93",
      "response_flags":"-",
      "bytes_received":0
    }

    由日志得到:

    • "downstream_remote_address":"10.0.0.93:5899":不是真实的客户端源地址。

    • "downstream_local_address":"172.17.X.XXX:80":ingressgateway Pod的地址。

    • "upstream_local_address":"172.17.X.XXX:54322":保留了ingressgateway Pod的地址,端口值改变。

    • "upstream_host":"172.17.X.XXX:80":httpbin Pod的地址。

设置保留源IP

  1. 设置外部流量策略为Local。(网络模式为Terway的集群可跳过该步骤。)

    1. 登录ASM控制台,在左侧导航栏,选择服务网格 > 网格管理

    2. 网格管理页面,单击目标实例名称,然后在左侧导航栏,选择ASM网关 > 入口网关

    3. 入口网关页面,单击目标网关右侧的查看YAML

    4. 编辑对话框的spec字段下,将externalTrafficPolicy字段配置为Local,然后单击确定设置外部流量策略为Local

  2. 执行以下命令,通过ingressgateway访问httpbin。

    curl http://$GATEWAY_URL:80/ip

    预期输出:

    {
      "origin": "120.244.xxx.xxx"
    }

    由预期输出得到,返回的IP地址为实际的客户端的源IP地址。

  3. 查看ingressgateway的访问日志。

    日志示例如下:

    {
      "istio_policy_status":null,
      "upstream_transport_failure_reason":null,
      "path":"/ip",
      "x_forwarded_for":"120.244.XXX.XXX",
      "route_name":null,
      "method":"GET",
      "duration":2,
      "downstream_remote_address":"120.244.XXX.XXX:28504",
      "bytes_received":0,
      "upstream_cluster":"outbound|8000||httpbin.default.svc.cluster.local",
      "bytes_sent":34,
      "protocol":"HTTP/1.1",
      "response_flags":"-",
      "upstream_local_address":"172.17.X.XXX:57498",
      "upstream_service_time":"2",
      "request_id":"9c0295d4-e77f-4a3a-b292-e5c58d92****",
      "start_time":"2022-11-23T03:24:04.413Z",
      "response_code":200,
      "trace_id":null,
      "authority":"47.242.XXX.XX",
      "user_agent":"curl/7.79.1",
      "downstream_local_address":"172.17.X.XXX:80",
      "upstream_host":"172.17.X.XXX:80",
      "requested_server_name":null
    }

    由日志得到:

    • "downstream_remote_address":"120.244.XXX.XXX:28504":客户端源地址,符合预期。

    • "downstream_local_address":"172.17.X.XXX:80":ingressgateway Pod的地址。

    • "upstream_local_address":"172.17.X.XXX:57498":保留了ingressgateway Pod的地址,端口值改变。

    • "upstream_host":"172.17.X.XXX:80":httpbin Pod的地址。

HTTPS协议的请求

上文已详细说明HTTP协议的请求设置保留源IP地址前后的对比,因此,下文仅介绍如何设置及验证HTTPS协议的请求保留源IP。

  1. 设置保留源IP地址

  2. 使用以下内容,创建https-demo,以HTTPS协议访问httpbin。

    apiVersion: networking.istio.io/v1alpha3
    kind: Gateway
    metadata:
      name: httpbin-gw-https
      namespace: default
    spec:
      selector:
        istio: ingressgateway
      servers:
        - hosts:
            - '*'
          port:
            name: https
            number: 443
            protocol: HTTPS
          tls:
            credentialName: myexample-credential
            mode: SIMPLE
    ---
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: httpbin-https
      namespace: default
    spec:
      gateways:
        - httpbin-gw-https
      hosts:
        - '*'
      http:
        - route:
            - destination:
                host: httpbin
                port:
                  number: 8000
  3. 执行以下命令,部署网关和虚拟服务。

    kubectl -n default apply -f  https-demo.yaml
  4. 执行以下命令,通过ingressgateway访问httpbin。

    export GATEWAY_URL=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    curl -k https://$GATEWAY_URL:443/ip

    预期输出:

    {
      "origin": "120.244.XXX.XXX"
    }

    由预期输出得到,返回的IP地址为实际的客户端的源IP地址。