Think Deep,Work Lean

关于服务网格获取真实IP问题的复盘

Posted on By zack

背景

使用KubeSphere的微服务,应用中的pod注入sidecar;网关开启微服务治理功能,即给ingress controller pod注入sidecar。结合上一篇,我们知道创建route(即ingress)后,需要加上相应upstream annotation来rewrite request header,才能正常使用分流的功能。

但是现在有个新的问题,无法获取用户的真实IP。

场景: 通过域名访问,通过获取X-Forwarded-For Header,若网关不注入sidecar,则能获取真实IP;网关注入sidecar后,无法正确获取真实IP,每次都是127.0.0.1。

问题复现

我们使用httpbin来复现,httpbin是一个python写的应用,可以获取用户的真实IP,作法就是通过获取request的X-Forwarded-Header。看源码

@app.route("/ip")
def view_origin():
    """Returns the requester's IP Address.
    ---
    tags:
      - Request inspection
    produces:
      - application/json
    responses:
      200:
        description: The Requester's IP Address.
    """

    return jsonify(origin=request.headers.get("X-Forwarded-For", request.remote_addr))

https://github.com/postmanlabs/httpbin/blob/master/httpbin/core.py#L313

问题:程序中通过 X-Forwarded-For header来获取用户的真实IP,如果通过抓包看到进来的请求没有这个header,是不是就无法获取到真实IP?换言之:通过curl或是网页直接请求这个地址,此时并没有主动带这个Header,那是不是就无法获取到真实IP?

答案:当手动传入这个Header的时候,程序会获取到这个Header中的IP地址,即伪造客户端地址。当没有传这个Header的时候,代码也可以获取这个Header的值,是前面流量经过的节点所有的地址。

部署应用

kubectl create deploy httpbin --image zackzhangkai/httpbin --port 80
kubectl expose deploy httpbin --port 8080 --target-port 80

直接访问这个应用:

[root@ssa3 ~]# curl 10.233.28.160:8080/ip
{
  "origin": "10.233.64.1"
}

为什么是 10.233.64.1 ?这个IP是service IP还是Pod IP?

其实这个IP是节点的IP,因为是在节点上直接访问httpbin service的ip及端口的。

# 看下路由,是通过lo转发的
[root@ssa3 ~]# ip r get 10.233.28.160
local 10.233.28.160 dev lo src 10.233.28.160
    cache <local>

# 走回环地址转发,说明这个Ip在该内核网络栈
[root@ssa3 ~]# ip ad | grep 10.233.28.160
    inet 10.233.28.160/32 brd 10.233.28.160 scope global kube-ipvs0

# 10.233.64.1是cni的地址
[root@ssa3 ~]# ip ad show cni0
8: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
    link/ether 8e:d2:ee:ec:57:3c brd ff:ff:ff:ff:ff:ff
    inet 10.233.64.1/24 brd 10.233.64.255 scope global cni0
       valid_lft forever preferred_lft forever
    inet6 fe80::8cd2:eeff:feec:573c/64 scope link
       valid_lft forever preferred_lft forever

[root@ssa3 ~]# ip r
default via 192.168.0.1 dev eth0 proto dhcp metric 100
10.233.64.0/24 dev cni0 proto kernel scope link src 10.233.64.1
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.0.0/24 dev eth0 proto kernel scope link src 192.168.0.13 metric 100

问题:为什么获取到的客户端地址是10.233.64.1?说明是通过cni0转发的,这个路由是怎么走的?留给后续思考。

网关开启sidecar,创建相应ingress

~ k -n kubesphere-controls-system get po | grep test
kubesphere-router-test-5f64fb8fb9-sslg6   2/2     Running   0          4h53m

pod 2/2说明有两个容器,其中一个是sidecar

~ kubectl get ing httpbin -oyaml
  ...
  annotations:
    nginx.ingress.kubernetes.io/upstream-vhost: httpbin.test.svc.cluster.local
spec:
  rules:
  - host: www.httpbin.com
    http:
      paths:
      - backend:
          serviceName: httpbin
          servicePort: 8080
        path: /

通过网关访问无法正确获取客户端IP

➜  ~ curl ssa3:30346/ip -HHost:www.httpbin.com
{
  "origin": "127.0.0.1"
}

网关去掉sidecar,能正常获取客户IP

➜  ~ k -n kubesphere-controls-system get po kubesphere-router-test-7c8f754954-nvsjq
NAME                                      READY   STATUS    RESTARTS   AGE
kubesphere-router-test-7c8f754954-nvsjq   1/1     Running   0          27s

➜  ~ curl ssa3:30346/ip -HHost:www.httpbin.com
{
  "origin": "10.233.64.1"
}

至此已完成复现

分析

尝试手动传入这个Header,发现传入Header的值不生效

➜  ~ curl ssa3:30346/ip -HHost:www.httpbin.com -HX-Forwarded-For:1.2.3.4
{
  "origin": "10.233.64.1"
}

设置nginx 的 use_forwarded_headers: "true",可以解决上述问题

kubectl -n kubesphere-controls-system edit cm kubesphere-router-xxx
...
data:
use-forwarded-headers: "true"

重启ingress controller这个pod,可以看到nginx 的配置文件已经生效

kubectl -n kubesphere-controls-system exec kubesphere-router-test-xxxx -- cat /etc/nginx/nginx.conf
...
                lua_ingress.set_config({
                        use_forwarded_headers = true,

上述的配置是设置nginx,让其把forward-headers的头全部直接转发到upstream的backend上。

➜  ~ curl ssa3:30346/ip -HHost:www.httpbin.com -HX-Forwarded-For:1.2.3.4
{
  "origin": "1.2.3.4"
}

思路:如果注入sidecar后,这个值能生效,那么可以在访问全加个代理,传入这个头部

验证注入sidecar后,可以成功伪造客户端IP。

解决方法

尝试自己搭建nginx,设置proxy_pass set_header,发现当网关注入sidecar后,一直出现http code 426。上网查,说是http proxy 协议不对。istio文档说明需要增加一个envoyfilter来修改代理的protocol。但是没有成功。

再次梳理下问题:只需在请求时增加个代理,给请求加一个头部即可。

检查发现青云的LB已经有了这个功能。

此时借助青云Iaas上的Lb,加上这个Header:

青云LB设置

将网关设置为这个LB的后端,此时访问能够正常获取到客户端IP:

 ~ curl 139.198.120.217:30099/ip -HHost:www.httpbin.com
{
  "origin": "171.113.245.63"
}

此种方法是通过在lb上获取用户真实IP,加在http header里面,并透传至最后面,可以获取到

通过envoyfilter来获取用户的真实IP

Apply下这个envoyfilter

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  annotations:
  name: use-remote-adress
  namespace: istio-system
spec:
  configPatches:
  - applyTo: NETWORK_FILTER
    match:
      context: SIDECAR_OUTBOUND
      listener:
        filterChain:
          filter:
            name: envoy.http_connection_manager
    patch:
      operation: MERGE
      value:
        typedConfig:
          '@type': type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
          skip_xff_append: false
          use_remote_address: true
          xff_num_trusted_hops: 10

envoy官方文档

效果:

 curl ssa3:30346/ip -HHost:www.httpbin.com
{
  "origin": "127.0.0.1,10.233.64.110"
}

此时拿到的是网关的IP

手动传入x-forwarded-for

 curl ssa3:30346/ip -HHost:www.httpbin.com -HX-Forwarded-For:1.2.3.4
{
  "origin": "1.2.3.4,10.233.64.111"
}

可以看到加上这个envoyfilter后可以拿到不仅可以拿到原始头中的外部IP,还可以拿到网关IP。