istio(二)探索”虚拟服务”概念

接前文《istio(一)探索服务发现行为》,上一次我们探索了istio的默认服务发现能力。

本文我们探索”虚拟服务“概念,利用它可以自定义请求路由规则,结合istio的服务发现功能可以解决更加实际的问题。

准备工作

与上篇博客一样,我们需要创建出包含istio-proxy的客户端与服务端环境。

创建服务端:

创建客户端:

探索

https://nginx.com/是nginx的官网,我们接下来的实验目的就是利用istio来识别访问它的流量,做一些奇奇怪怪的事情,以便理解istio的”虚拟服务“概念。

为什么选择nginx.com呢?因为这个域名是真实存在的,因此能解析到IP,也就能走通后续4层网络通讯的事情,如果胡编一个域名还得去配置coredns解析IP比较麻烦,这个原因简单了解即可。

场景1)识别HTTP协议&转发

我们希望访问http://nginx.com(注意不是HTTPS)的时候,可以利用istio将流量转发给我们自己部署的nginx而不是真的nginx官网,这个很有趣吧?

我们要做的是:识别HTTP流量,匹配HOST是nginx.com,然后转发给my-service这个k8s service对应的POD。

我们创建一个叫做”虚拟服务(virtualservice)“的配置:

当istio-proxy拦截到程序发出的请求时,如果识别出流量是http协议的话就会提取出host,并且看一下是否是nginx.com。

如果host是nginx.com的话,它就会转发给一个目标集群(destination/upstream),这个集群的名字叫做my-nginx,实际就是my-nginx service的域名简写(istio会自动补全),此时istio-proxy的行为就是从my-nginx service的endpoints中找一个POD转发过去。

登录ubuntu容器,发起调用:

root@debian:~/kubernetes/demo# kubectl exec -it my-ubuntu-f464f6885-b9t8l bash

注意我们访问的是HTTP而不是HTTPS。

从返回内容看,我们访问的的确是my-nginx服务,而不是俄罗斯的nginx官网。之所以能实现这个效果,都是因为istio能够识别HTTP流量,并且应用了我们配置的virtualservice虚拟服务规则,才能实现host匹配与转发到service的效果。

我们看一下ubuntu的istio-proxy日志:

[2021-06-09T09:28:48.299Z] “GET / HTTP/1.1” 200 – via_upstream – “-” 0 612 13 12 “-” “curl/7.58.0” “97d36820-b512-9482-8f75-3256f48e429d” “nginx.com” “172.17.0.13:80” outbound|80||my-nginx.default.svc.cluster.local 172.17.0.15:36732 52.58.199.22:80 172.17.0.15:42982 – –

可见istio-proxy识别出了HTTP协议与nginx.com,经过路由规则后送往了outbound|80||my-nginx.default.svc.cluster.local集群。

我们先看envoy listener的配置方法(istioctl proxy-config listener my-ubuntu-f464f6885-b9t8l -o json|less):

前一篇博客也说过,istio-proxy是从4层listener开始的,上面配置说明只要访问80端口的并且能识别出HTTP协议的,咱就走HTTP路由进一步判断,所以这个阶段只能是IP/PORT/Protocol上面做工作。

既然已经识别出是80端口的HTTP流量,那么就可以向上到7层做进一步路由了(istioctl proxy-config route my-ubuntu-f464f6885-b9t8l -o json|less):

可见路由规则已经把virtualservice配置给下发了,匹配nginx.com或者nginx.com:80的HOST,然后转给outbound|80||my-nginx.default.svc.cluster.local集群,这时候就是再下回到4层。

outbound|80||my-nginx.default.svc.cluster.local集群则进一步查看集群信息(istioctl proxy-config cluster my-ubuntu-f464f6885-b9t8l -o json|less):

发现是EDS(endpoint discovery service)来动态服务发现的endpoints列表,其实就是my-service这个k8s service对应的endpoints,这是istiod来负责下发到istio-proxy的。

场景2)识别 TLS&转发

我们现在直接调用https协议的nginx.com域名,这是一个TLS加密的通讯,因此当istio-proxy收到这个流量时无法识别出上层协议是HTTP,自然也无法提取出Host,我们又该如何配置virtualhost呢?

虽说如此,在我们不进行任何配置的情况下直接调用https://nginx.com的话,nginx.com解析为俄罗斯官网IP,然后istio-proxy并不了解这个IP因此会走TCP透传转发,访问也是没任何问题的:

root@my-ubuntu-f464f6885-b9t8l:/# curl https://nginx.com/

[2021-06-09T10:09:28.237Z] “- – -” 0 – – – “-” 743 3341 1524 – “-” “-” “-” “-” “3.125.197.172:443” PassthroughCluster 172.17.0.15:57150 3.125.197.172:443 172.17.0.15:57148 – –

看得出istio-proxy走了PassThroughCluter透传,因为它压根没有目标IP的信息,也无法提取TLS中的协议内容。

但实际上HTTPS请求是要经过TLS握手的,而TLS协议在握手期间HTTPS客户端会在TLS握手包中携带SNI扩展字段,在里面写上要访问的HTTP HOST,这个就给了istio-proxy识别流量目标地址的机会。

下面是TLS握手流程:

在TLS client hello握手包中的SNI字段可以被istio-proxy解析到,客户端在里面写上了HTTP协议层的host,因此istio-proxy就可以基于这个host进行路由和转发目的地控制了。

我们抓包HTTPS调用过程,可以wireshark看到TLS中的SNI字段:

也就说istio-proxy收到TLS握手时就已经知道这个TCP连接要访问的目标地址是nginx.com了,虽然它并不知道应用层协议是HTTP。

我们可以注册一个virtualservice来让istio-proxy尝试做这个识别:

我们在virtualservice中增加了tls加密场景下的路由,我们match的是发往443端口且SNI host是nginx.com的流量,将其转发给nginx.com

这里容易让人迷惑的是destination怎么又是nginx.com,感觉死循环了一样?其实destination通常都是写K8S中存在的service域名,这样istio-proxy会服务发现后面的endpoint来转发,这也是destination的本意。

但是我们现在希望将这个https://nginx.com的流量识别出来,然后再原样转发给俄罗斯的nginx.com就行,因此此时nginx.com就不是一个K8S service的域名了,我们需要主动通过ServiceEntry来注册这个服务到istio,这意味着nginx.com是一个外部服务,被注册到了istio注册中心,可以作为一个destination(也就是envoy的cluster概念)。

因此,当istio-proxy转发到destination nginx.com时就会基于DNS解析nginx.com域名得到俄罗斯服务器 IP,TCP连接它,然后将TLS流量透明转发就好。

请求网址的确返回了俄罗斯网站的HTML:

curl https://nginx.com/
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.19.10</center>
</body>
</html>

可以看到ubuntu中istio-proxy的日志:

[2021-06-09T10:35:29.246Z] “- – -” 0 – – – “-” 743 3341 1536 – “-” “-” “-” “-” “3.125.197.172:443” outbound|443||nginx.com 172.17.0.15:46280 52.58.199.22:443 172.17.0.15:37424 nginx.com –

我们分析istio-proxy规则,先看listener(istioctl proxy-config listener my-ubuntu-f464f6885-b9t8l -o json|less):

该调用命中了443端的listener,然后SNI匹配了nginx.com域名,然后走TCP PROXY透明流量转发给集群outbound|443||nginx.com,我们看一下这个集群:

能看出这个集群是STATIC DNS静态解析出的集群,也就是走域名解析转发啊给soscketAddress中的nginx.com:443地址,流量则是TCP PROXY。

从这里我们也可以看出来,因为istio-proxy无法解密TLS流量内容,因此只能做透明转发,也就只有一次通讯机会,没法实现对upstream的重试等特性,这个很容易理解。

至于istio-proxy作为中间人为什么不能解密TLS流量呢?因为大家可以回看上面TLS握手图的3~4步骤,secret是用服务端公钥加密的,只能由服务端用私钥解密,而最终TLS协商的对称通讯秘钥则需要依赖secret来生成,因此istio-proxy作为中间人不可能拥有服务端的私钥,因此不可能知道secret,因此不可能推导出对称秘钥,也不可能解密TLS流量,也自然不可能解析出7层的HTTP协议细节,无从重试。

总结

本文通过一个奇怪的例子探索了virtualservice虚拟服务,也探索了HTTP流量、HTTPS流量的处理方法,也明白了TLS请求的能力限制底层原因,同时也学会了如何向istio注册一个集群外的服务发现。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~