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 pattern | Description | Available methods |
|---|---|---|
| East-west | Service-to-service calls within the mesh | TPROXY mode, X-Forwarded-For (XFF) header |
| North-south | External client traffic through an ingress gateway | externalTrafficPolicy: Local + XFF |
Method comparison for east-west traffic
| Criteria | TPROXY | XFF header |
|---|---|---|
| How it works | Uses 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 compatibility | Not compatible with CentOS | All operating systems |
| Application changes | None | Must read the X-Forwarded-For header |
| IP visible at | TCP connection (socket layer) | HTTP header only |
| Security requirement | CAP_NET_ADMIN | None |
Prerequisites
An ASM Enterprise Edition or Ultimate Edition instance, version 1.15 or later. For more information, see Create an ASM instance and Upgrade an ASM instance.
An ACK managed cluster added to the ASM instance. For more information, see Create an ACK managed cluster and Add a cluster to an ASM instance.
An ingress gateway deployed in the ASM instance. For more information, see Create an ingress gateway.
kubectl connected to the cluster. For more information, see Obtain the kubeconfig file of a cluster and connect to the cluster by using kubectl.
Deploy sample applications
Deploy the sleep and httpbin sample applications to test source IP behavior before and after configuration.
Deploy the sleep application
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: trueApply the manifest:
kubectl -n default apply -f sleep.yaml
Deploy the httpbin application
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: 8000Apply 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
Check that both pods are running: Expected output: Note the pod IP of sleep (
172.17.X.XXX).kubectl -n default get pods -o wideNAME 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.XXSend 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" }Confirm through socket information: Expected output:
If
netstatis not available in the httpbin container, log on to the container and runapt update && apt install net-tools.kubectl -n default exec -it deploy/httpbin -c httpbin -- netstat -ntp | grep 80tcp 0 0 172.17.X.XXX:80 127.0.0.6:42691 TIME_WAIT -Check the Envoy access log for the httpbin pod. A formatted log entry looks like: Key fields:
Field Value in this example Meaning 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.6as 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.
TPROXY is not compatible with CentOS-based node operating systems.
Patch the httpbin deployment to enable TPROXY:
kubectl patch deployment -n default httpbin -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/interceptionMode":"TPROXY"}}}}}'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" }Confirm through socket information: Expected output:
After the pod restarts, reinstall
netstatif needed.kubectl -n default exec -it deploy/httpbin -c httpbin -- netstat -ntp | grep 80tcp 0 0 172.17.X.XXX:80 172.17.X.XXX:36728 ESTABLISHED -Verify in the Envoy access log: The
upstream_local_addressnow shows the sleep pod IP instead of127.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.
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: trueSend a request from sleep to httpbin: Expected output: The httpbin application reads the client IP from the
X-Forwarded-Forheader 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
Create
http-demo.yamlto 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: 8000Apply the manifest:
kubectl -n default apply -f http-demo.yamlAccess 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" }Verify in the ingress gateway access log: The
downstream_remote_addressshows 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
Set the external traffic policy to
Local.Log on to the ASM console. In the left-side navigation pane, choose Service Mesh > Mesh Management.
On the Mesh Management page, click the name of the target ASM instance. In the left-side navigation pane, choose ASM Gateways > Ingress Gateway.
On the Ingress Gateway page, click View YAML on the right side of the target gateway.
In the Edit dialog box, under the
specfield, setexternalTrafficPolicytoLocal, and then click OK.
Skip this step if your cluster uses the Terway network mode.
ImportantWhen
externalTrafficPolicyis set toLocal, 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.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" }Verify in the ingress gateway access log: Both
downstream_remote_addressandx_forwarded_fornow 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.
Create
https-demo.yamlto 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: 8000Apply the manifest:
kubectl -n default apply -f https-demo.yamlAccess 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:
| Field | Description |
|---|---|
downstream_remote_address | The address of the calling client, or the last proxy in front of Envoy. |
downstream_local_address | The local address where Envoy received the request. |
upstream_local_address | The 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_host | The address of the upstream application (the backend pod). |
x_forwarded_for | The client IP carried in the X-Forwarded-For HTTP header, if present. |