All Products
Search
Document Center

Alibaba Cloud Service Mesh:How does the server application obtain the client source IP in a service mesh environment

Last Updated:Mar 11, 2026

When a sidecar proxy is injected into a pod, Envoy intercepts all inbound traffic and forwards it to your application over a local connection. As a result, your application sees 127.0.0.6 as the source address for every request instead of the actual client IP. This prevents IP-based access control, session persistence, and accurate access logging. You can configure Alibaba Cloud Service Mesh (ASM) to preserve the real client source IP for both intra-mesh and ingress traffic.

Choose a method

Two traffic patterns require different preservation methods:

Traffic patternDescriptionAvailable methods
East-westService-to-service calls within the meshTPROXY mode, X-Forwarded-For (XFF) header
North-southExternal client traffic through an ingress gatewayexternalTrafficPolicy: Local + XFF

Method comparison for east-west traffic

CriteriaTPROXYXFF header
How it worksUses the Linux kernel transparent proxy to preserve source IP at the socket level. Replaces the default REDIRECT mode, which uses iptables NAT and loses the source IP.Configures the sidecar proxy to inject the real client IP into the X-Forwarded-For HTTP header before forwarding.
OS compatibilityNot compatible with CentOSAll operating systems
Application changesNoneMust read the X-Forwarded-For header
IP visible atTCP connection (socket layer)HTTP header only
Security requirementCAP_NET_ADMINNone

Prerequisites

Deploy sample applications

Deploy the sleep and httpbin sample applications to test source IP behavior before and after configuration.

Deploy the sleep application

  1. Create 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. Apply the manifest:

       kubectl -n default apply -f sleep.yaml

Deploy the httpbin application

  1. Create 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. Apply the manifest:

       kubectl -n default apply -f httpbin.yaml

Preserve the source IP for east-west traffic

By default, when the sleep application sends a request to httpbin within the mesh, httpbin sees 127.0.0.6 as the source IP.

Verify the default behavior

  1. Check that both pods are running: Expected output: Note the pod IP of sleep (172.17.X.XXX).

       kubectl -n default get pods -o wide
       NAME                            READY   STATUS    RESTARTS   AGE     IP             NODE
       httpbin-c85bdb469-4ll2m         2/2     Running   0          3m22s   172.17.X.XXX   cn-hongkong.10.0.0.XX
       sleep-8f764df66-q7dr2           2/2     Running   0          3m9s    172.17.X.XXX   cn-hongkong.10.0.0.XX
  2. Send a request from sleep to httpbin: Expected output: The source address is 127.0.0.6 (the Envoy local address), not the actual IP of the sleep pod.

       kubectl -n default exec -it deploy/sleep -c sleep -- curl http://httpbin:8000/ip
       {
         "origin": "127.0.0.6"
       }
  3. Confirm through socket information: Expected output:

    If netstat is not available in the httpbin container, log on to the container and run apt update && apt install net-tools.
       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   -
  4. Check the Envoy access log for the httpbin pod. A formatted log entry looks like: Key fields:

    FieldValue in this exampleMeaning
    downstream_remote_address172.17.X.XXX:56160The address of the sleep pod (the calling client).
    upstream_local_address127.0.0.6:42169The local address Envoy uses to connect to httpbin. This is why the application sees 127.0.0.6 as the source.
       {
         "downstream_remote_address": "172.17.X.XXX:56160",
         "downstream_local_address": "172.17.X.XXX:80",
         "upstream_local_address": "127.0.0.6:42169",
         "upstream_host": "172.17.X.XXX:80"
       }

Method 1: TPROXY transparent interception mode

TPROXY uses the Linux kernel transparent proxy mechanism to preserve the original source IP at the network layer. Unlike the default REDIRECT mode (which uses iptables NAT and loses the source IP), TPROXY preserves the source IP address at the socket level.

Important

TPROXY is not compatible with CentOS-based node operating systems.

  1. Patch the httpbin deployment to enable TPROXY:

       kubectl patch deployment -n default httpbin -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/interceptionMode":"TPROXY"}}}}}'
  2. Send a request from sleep to httpbin: Expected output: The response now shows the actual IP of the sleep pod.

       kubectl -n default exec -it deploy/sleep -c sleep -- curl http://httpbin:8000/ip
       {
         "origin": "172.17.X.XXX"
       }
  3. Confirm through socket information: Expected output:

    After the pod restarts, reinstall netstat if needed.
       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 -
  4. Verify in the Envoy access log: The upstream_local_address now shows the sleep pod IP instead of 127.0.0.6.

       {
         "downstream_remote_address": "172.17.X.XXX:39058",
         "downstream_local_address": "172.17.X.XXX:80",
         "upstream_local_address": "172.17.X.XXX:46129",
         "upstream_host": "172.17.X.XXX:80"
       }

Method 2: X-Forwarded-For (XFF) request header

This method configures the sidecar proxy to inject the real client IP into the X-Forwarded-For HTTP header before forwarding the request to the application. This approach works on all node operating systems but requires your application to parse the X-Forwarded-For header.

  1. Apply the following EnvoyFilter to the ASM instance. For instructions, see Create an Envoy filter by using an Envoy filter template.

       apiVersion: networking.istio.io/v1alpha3
       kind: EnvoyFilter
       metadata:
         name: enable-xff-for-sidecar-inbound
         namespace: istio-system  # Change this namespace to the namespace where the gateway is located
         labels:
           asm-system: "true"
           provider: "asm"
       spec:
         configPatches:
         - applyTo: NETWORK_FILTER
           match:
             proxy:
               proxyVersion: "^1.*"
             context: SIDECAR_INBOUND
             listener:
               name: "virtualInbound"
               filterChain:
                 filter:
                   name: "envoy.filters.network.http_connection_manager"
           patch:
             operation: MERGE
             value:
               typed_config:
                 "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
                 use_remote_address: true
  2. Send a request from sleep to httpbin: Expected output: The httpbin application reads the client IP from the X-Forwarded-For header and returns it correctly.

       kubectl -n default exec -it deploy/sleep -c sleep -- curl http://httpbin:8000/ip
       {
         "origin": "172.17.X.XXX"
       }
This method requires your application to read the X-Forwarded-For header. The httpbin sample application has this capability built in. If your application does not parse this header, use TPROXY instead.

Preserve the source IP for north-south traffic

For external traffic entering through the ingress gateway, the request path is: Client -> Server Load Balancer (SLB) -> Istio ingress gateway -> backend service. Without proper configuration, the source IP is replaced by a Kubernetes node address during this process.

HTTP protocol

Verify the default behavior

  1. Create http-demo.yaml to expose httpbin through HTTP:

       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. Apply the manifest:

       kubectl -n default apply -f http-demo.yaml
  3. Access httpbin through the ingress gateway: Expected output: The returned IP (10.0.0.93) is a Kubernetes node address, not the actual client IP.

       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"
       }
  4. Verify in the ingress gateway access log: The downstream_remote_address shows the node address instead of the real client IP.

       {
         "downstream_remote_address": "10.0.0.93:5899",
         "downstream_local_address": "172.17.X.XXX:80",
         "upstream_local_address": "172.17.X.XXX:54322",
         "upstream_host": "172.17.X.XXX:80",
         "x_forwarded_for": "10.0.0.93"
       }

Enable source IP preservation

  1. Set the external traffic policy to Local.

    1. Log on to the ASM console. In the left-side navigation pane, choose Service Mesh > Mesh Management.

    2. On the Mesh Management page, click the name of the target ASM instance. In the left-side navigation pane, choose ASM Gateways > Ingress Gateway.

    3. On the Ingress Gateway page, click View YAML on the right side of the target gateway.

    4. In the Edit dialog box, under the spec field, set externalTrafficPolicy to Local, and then click OK.

    Skip this step if your cluster uses the Terway network mode.
    Important

    When externalTrafficPolicy is set to Local, only nodes running an ingress gateway pod can receive external traffic. In production, deploy ingress gateway pods across multiple nodes. If those nodes go down, ingress traffic is completely lost. Make sure you have sufficient replicas distributed across availability zones.

  2. Access httpbin through the ingress gateway: Expected output: The response now shows the actual client source IP address.

       curl http://$GATEWAY_URL:80/ip
       {
         "origin": "120.244.xxx.xxx"
       }
  3. Verify in the ingress gateway access log: Both downstream_remote_address and x_forwarded_for now contain the real client IP.

       {
         "downstream_remote_address": "120.244.XXX.XXX:28504",
         "downstream_local_address": "172.17.X.XXX:80",
         "upstream_local_address": "172.17.X.XXX:57498",
         "upstream_host": "172.17.X.XXX:80",
         "x_forwarded_for": "120.244.XXX.XXX"
       }

HTTPS protocol

After you enable source IP preservation (as described in the HTTP section), apply the same approach for HTTPS.

  1. Create https-demo.yaml to expose httpbin through HTTPS:

       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
  2. Apply the manifest:

       kubectl -n default apply -f https-demo.yaml
  3. Access httpbin through the ingress gateway: Expected output: The response shows the actual client source IP address.

       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"
       }

Envoy access log field reference

When troubleshooting source IP issues, the following Envoy access log fields are relevant:

FieldDescription
downstream_remote_addressThe address of the calling client, or the last proxy in front of Envoy.
downstream_local_addressThe local address where Envoy received the request.
upstream_local_addressThe local address Envoy uses to connect to the upstream application. When this is 127.0.0.6, the application sees the Envoy proxy address instead of the real client IP.
upstream_hostThe address of the upstream application (the backend pod).
x_forwarded_forThe client IP carried in the X-Forwarded-For HTTP header, if present.

Related topics