Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Istio 中的 Sidecar 注入及透明流量劫持过程详解 · Service Mesh|服务网格中文社区 #293

Open
rootsongjc opened this issue Apr 29, 2020 · 13 comments

Comments

@rootsongjc
Copy link
Member

https://www.servicemesher.com/blog/sidecar-injection-iptables-and-traffic-routing/

本文基于 Istio 1.5.1 版本,介绍了 sidecar 模式及其优势 sidecar 注入到数据平面,如何做流量劫持和转发的,以及流量是怎样路由到 upstream 的。

@knight42
Copy link

@rootsongjc 你好,我对 envoy 了解得不多,关于理解 Inbound/Outbound handler 部分有些疑问,盼望解答:

  1. 关于 Inbound Handler,virtualInbound Listener 里其实已经可以看到 envoy.http_connection_manager 包含了 inbound|9080|http|reviews.default.svc.cluster.local 这个 route,为什么 inbound 流量不是直接被 virtualInbound listener 处理而是 "Inbound handler 的流量被 virtualInbound Listener 转移到 172.17.0.15_9080 Listener" 呢?
  2. 关于 Outbound Handler,outbound 在被 virtualOutbound listener 处理后是怎么交给 0.0.0.0_9080 Listener 的?

@rootsongjc
Copy link
Member Author

@knight42
@rootsongjc 你好,我对 envoy 了解得不多,关于理解 Inbound/Outbound handler 部分有些疑问,盼望解答:

  1. 关于 Inbound Handler,virtualInbound Listener 里其实已经可以看到 envoy.http_connection_manager 包含了 inbound|9080|http|reviews.default.svc.cluster.local 这个 route,为什么 inbound 流量不是直接被 virtualInbound listener 处理而是 "Inbound handler 的流量被 virtualInbound Listener 转移到 172.17.0.15_9080 Listener" 呢?
  2. 关于 Outbound Handler,outbound 在被 virtualOutbound listener 处理后是怎么交给 0.0.0.0_9080 Listener 的?

你好,这篇文章主要讲解的是 Istio 中的如何使用 iptables 做流量劫持的,关于流量在 sidecar 里是如何处理的可以查看这篇Sidecar 流量路由机制分析里面有更详细的说明。

@myf5
Copy link

myf5 commented Jun 25, 2020

宋神,我用 demo profile部署的istio 1.6.3,部署bookfinfo,无其它特别配置,productpage的envoy里关于virtualoutbound中,并没有处理到9080的流量转移,请教这是啥可能原因?

[root@k8s-master-v1-16 ~]# istioctl proxy-config listener productpage-v1-7f4cc988c6-qxqjs.istio-bookinfo --port 15001 -o json
[
    {
        "name": "virtualOutbound",
        "address": {
            "socketAddress": {
                "address": "0.0.0.0",
                "portValue": 15001
            }
        },
        "filterChains": [
            {
                "filters": [
                    {
                        "name": "istio.stats",
                        "typedConfig": {
                            "@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
                            "typeUrl": "type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm",
                            "value": {
                                "config": {
                                    "configuration": "{\n  \"debug\": \"false\",\n  \"stat_prefix\": \"istio\"\n}\n",
                                    "root_id": "stats_outbound",
                                    "vm_config": {
                                        "code": {
                                            "local": {
                                                "inline_string": "envoy.wasm.stats"
                                            }
                                        },
                                        "runtime": "envoy.wasm.runtime.null",
                                        "vm_id": "tcp_stats_outbound"
                                    }
                                }
                            }
                        }
                    },
                    {
                        "name": "envoy.tcp_proxy",
                        "typedConfig": {
                            "@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
                            "statPrefix": "PassthroughCluster",
                            "cluster": "PassthroughCluster",
                            "accessLog": [
                                {
                                    "name": "envoy.file_access_log",
                                    "typedConfig": {
                                        "@type": "type.googleapis.com/envoy.config.accesslog.v2.FileAccessLog",
                                        "path": "/dev/stdout",
                                        "format": "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% \"%DYNAMIC_METADATA(istio.mixer:status)%\" \"%UPSTREAM_TRANSPORT_FAILURE_REASON%\" %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\" %UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME%\n"
                                    }
                                }
                            ]
                        }
                    }
                ],
                "name": "virtualOutbound-catchall-tcp"
            }
        ],
        "useOriginalDst": true,
        "trafficDirection": "OUTBOUND"
    }
]

@myf5
Copy link

myf5 commented Jun 25, 2020

@knight42
@rootsongjc 你好,我对 envoy 了解得不多,关于理解 Inbound/Outbound handler 部分有些疑问,盼望解答:

  1. 关于 Inbound Handler,virtualInbound Listener 里其实已经可以看到 envoy.http_connection_manager 包含了 inbound|9080|http|reviews.default.svc.cluster.local 这个 route,为什么 inbound 流量不是直接被 virtualInbound listener 处理而是 "Inbound handler 的流量被 virtualInbound Listener 转移到 172.17.0.15_9080 Listener" 呢?
  2. 关于 Outbound Handler,outbound 在被 virtualOutbound listener 处理后是怎么交给 0.0.0.0_9080 Listener 的?

当是一个inbound的请求时候,这个时候包的目的地址是pod的IP,目的端口是业务的真实端口(9080,非svc的映射port),由于iptables这个链接目的端口被重改为15006了,会命中virtual inbound listener(0.0.0.0:15006), 这个listener里有一系列的filterchain,它是怎么匹配到真的9080的业务的,比如下面的输出:
addressPrefix是可以匹配的,如果这个pod实际有多个port的话,这里仅仅依靠addressPrefix匹配肯定不行,所以我理解匹配还需要一个协议,但是Match条件里的destinationPort没有被匹配,(根据https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/listener/listener_components.proto, 条件是要全部匹配才可以),所以到底怎么匹配上的呢

            {
                "filterChainMatch": {
                    "destinationPort": 9080,
                    "prefixRanges": [
                        {
                            "addressPrefix": "10.244.2.138",
                            "prefixLen": 32
                        }
                    ],
### 根据https://istio.io/latest/zh/docs/ops/deployment/requirements/ , 
### 同一个业务端口是不能被两个用不同协议的svc来发布的,因此这帮助避免了同端口同协议的match出现在这里
                    "applicationProtocols": [
                        "istio",
                        "istio-http/1.0",
                        "istio-http/1.1",
                        "istio-h2"
                    ]
                },
                "filters": [
                    {
                        "name": "istio.metadata_exchange",
                        "typedConfig": {
                            "@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
                            "typeUrl": "type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchange",
                            "value": {
                                "protocol": "istio-peer-exchange"
                            }
                        }
                    },
                    {
                        "name": "envoy.http_connection_manager",
                        "typedConfig": {
                            "@type": "type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager",
                            "statPrefix": "inbound_10.244.2.138_9080",
                            "routeConfig": {
                                "name": "inbound|9080|http|productpage.istio-bookinfo.svc.cluster.local",
                                "virtualHosts": [
                                    {
                                        "name": "inbound|http|9080",
                                        "domains": [
                                            "*"
                                        ],
                                        "routes": [
                                            {
                                                "name": "default",
                                                "match": {
                                                    "prefix": "/"
                                                },
                                                "route": {
                                                    "cluster": "inbound|9080|http|productpage.istio-bookinfo.svc.cluster.local",
                                                    "timeout": "0s",
                                                    "maxGrpcTimeout": "0s"
                                                },
                                                "decorator": {
                                                    "operation": "productpage.istio-bookinfo.svc.cluster.local:9080/*"
                                                }
                                            }
                                        ]
                                    }
                                ],

@knight42
Copy link

@myf5 virtualOutbound 这个 listener 的 filterChains 里有设置 "useOriginalDst": true,这个属性表示 virtualOutbound 这个 listener 会把请求转给和原目标地址匹配的 listener 进行处理。

接下来如果你执行 istioctl pc listener -n istio-bookinfo productpage-v1-7f4cc988c6-qxqjs --port 9080 应该会看到一个监听在 0.0.0.0:9080 的 listener,它的 filterChains 里有设置 "deprecatedV1": { "bindToPort": false },表示这个 listener 并没有监听实际的端口,只是接受 virtualOutbound listener 转发来的请求。然后你可以再研究下这个 listener 的配置,里面应该有 route config。

@myf5
Copy link

myf5 commented Jun 26, 2020

@knight42
@myf5 virtualOutbound 这个 listener 的 filterChains 里有设置 "useOriginalDst": true,这个属性表示 virtualOutbound 这个 listener 会把请求转给和原目标地址匹配的 listener 进行处理。

接下来如果你执行 istioctl pc listener -n istio-bookinfo productpage-v1-7f4cc988c6-qxqjs --port 9080 应该会看到一个监听在 0.0.0.0:9080 的 listener,它的 filterChains 里有设置 "deprecatedV1": { "bindToPort": false },表示这个 listener 并没有监听实际的端口,只是接受 virtualOutbound listener 转发来的请求。然后你可以再研究下这个 listener 的配置,里面应该有 route config。

谢谢,重新阅读和查了一些资料,这个virtual outbound 和virtual inbound处理机制不一样,inbound是直接通过virtualinbound listener内的filter直接处理具体的业务对应listener,而virtualoutbound借助这里描述的:

use_original_dst
(BoolValue) If a connection is redirected using iptables, the port on which the proxy receives it might be different from the original destination address. When this flag is set to true, the listener hands off redirected connections to the listener associated with the original destination address. If there is no listener associated with the original destination address, the connection is handled by the listener that receives it. Defaults to false.

实现将流量重新在整个配置里重新进行listnener匹配,如果匹配上其它listener则走匹配listener逻辑;如果没有匹配其它listener,则走passthroughcluster直接将包发送到外部。

关于:
iptables redirect port之后,从TCP四层看,这个时候dst port 应该是15001,enovy作为一个反向代理软件在这里怎么知道这个链接原来链接的端口(9080),对iptables了解不是很深入,这里还得研究一下。。

@knight42
Copy link

可以通过给 getsockopt 系统调用传入 SO_ORIGINAL_DST 参数获取原始的地址

@myf5
Copy link

myf5 commented Jun 26, 2020

可以通过给 getsockopt 系统调用传入 SO_ORIGINAL_DST 参数获取原始的地址

理解了,感谢~

@myf5
Copy link

myf5 commented Jun 26, 2020

@knight42 还有一个细节问题想请教,比如当Productpage访问reviews的时候,目标是 reviews的IP:9080. 在iptables,在output链中最终执行目标端口改写为15001,但是目标IP并没有变,那这个连接怎么最终跑到同pod的envoy 容器的15001上了呢?难道不应该继续走到postrouting并直接发送到pod之外吗。我特地查了下productpage容器的路由表,发现并没有将reviews的ip做特别处理。

@knight42
Copy link

在iptables规则里找找-j REDIRECT

@myf5
Copy link

myf5 commented Jun 28, 2020

@knight42
在iptables规则里找找-j REDIRECT

redirect动作指明了跳转的端口,但目的IP未变,接下来感觉应该走postrouting。没理解的地方是为什么匹配到了envoy的监听端口。就好像一台主机本身有个进程监听80端口,但是另一个进程发送一个到互联网其它主机的80请求,怎么会引导会localhost:80呢。

@knight42
Copy link

建议先详细了解一下 iptables

https://ipset.netfilter.org/iptables-extensions.man.html#lbDK

It redirects the packet to the machine itself by changing the destination IP to the primary address of the incoming interface (locally-generated packets are mapped to the localhost address, 127.0.0.1 for IPv4 and ::1 for IPv6).

@myf5
Copy link

myf5 commented Jun 28, 2020

@knight42
建议先详细了解一下 iptables

https://ipset.netfilter.org/iptables-extensions.man.html#lbDK

It redirects the packet to the machine itself by changing the destination IP to the primary address of the incoming interface (locally-generated packets are mapped to the localhost address, 127.0.0.1 for IPv4 and ::1 for IPv6).

感谢明白了。把它和普通的DNAT搞混了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants