本文介绍在服务网格环境下如何保持服务访问时的客户端源IP。
前提条件
已创建Kubernetes托管版集群。具体操作,请参见创建Kubernetes托管版集群。
已添加集群到ASM实例。具体操作,请参见添加集群到ASM实例。
已部署入口网关。具体操作,请参见创建入口网关。
通过kubectl连接集群。具体操作,请参见获取集群KubeConfig并通过kubectl工具连接集群。
背景信息
原始IP在很多情况下被广泛使用,典型的使用场景包括:
应用的访问控制:例如,许多应用程序在检测到用户从不同地域登录时强制执行额外的身份验证,可以通过获取原始IP来完成。
简单的会话保持:可以根据客户端的地址,基于源IP做负载均衡,将来自同一个客户端的请求,转发到同一个服务实例。
访问日志和监控统计:访问日志和监控指标中包含真实的源地址,有助于开发人员进行分析统计。
云上的负载均衡器也支持将客户端源IP传递到后端服务。Istio应提供允许应用程序获取原始源IP的能力。但在使用Istio时,Pod注入了Sidecar代理之后,所有入站流量都是从Envoy重定向。目前,Envoy将流量发送到绑定了本地地址(127.0.0.1)的应用程序,所以应用看不到真正的原始IP。
部署应用示例
部署sleep应用。
使用以下内容,创建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
执行以下命令,部署sleep应用。
kubectl -n default apply -f sleep.yaml
部署httpbin应用。
使用以下内容,创建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
执行以下命令,部署httpbin应用。
kubectl -n default apply -f httpbin.yaml
场景一:东西向流量
未开启TPROXY
在Istio中,东西向服务访问时,由于Sidecar的注入,所有进出服务的流量均被Envoy拦截代理,然后再由Envoy将请求转给应用。因此,应用收到的请求的源地址,是Envoy的访问地址127.0.0.6。
执行以下命令,查看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
。执行以下命令,从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应用的地址。从Socket信息中确认源IP地址是否为127.0.0.6。
登录到httpbin容器,执行以下命令,安装netstat。
apt update & apt install net-tools
退出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
。
查看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作为入流量拦截模式。
执行以下命令,修改httpbin应用的deployment。
kubectl patch deployment -n default httpbin -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/interceptionMode":"TPROXY"}}}}}'
执行以下命令,从sleep容器发起请求。
kubectl -n default exec -it deploy/sleep -c sleep -- curl http://httpbin:8000/ip
预期输出:
{ "origin": "172.17.X.XXX" }
由预期输出得到,httpbin可以得到sleep端的真实IP。
执行以下命令,根据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
。查看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
使用以下内容,创建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
执行以下命令,部署网关和虚拟服务。
kubectl -n default apply -f http-demo.yaml
执行以下命令,通过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集群的节点地址。
查看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
设置外部流量策略为Local。(网络模式为Terway的集群可跳过该步骤。)
登录ASM控制台,在左侧导航栏,选择 。
在网格管理页面,单击目标实例名称,然后在左侧导航栏,选择 。
在入口网关页面,单击目标网关右侧的查看YAML。
在编辑对话框的spec字段下,将externalTrafficPolicy字段配置为Local,然后单击确定。
执行以下命令,通过ingressgateway访问httpbin。
curl http://$GATEWAY_URL:80/ip
预期输出:
{ "origin": "120.244.xxx.xxx" }
由预期输出得到,返回的IP地址为实际的客户端的源IP地址。
查看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。
使用以下内容,创建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
执行以下命令,部署网关和虚拟服务。
kubectl -n default apply -f https-demo.yaml
执行以下命令,通过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地址。