Istio 中实现客户端源 IP 的保持
作者&投稿:威疤 (若有异议请与网页底部的电邮联系)
很多业务场景,我们都希望得到客户端资源 IP。云上负载均衡器,比如,腾讯云 CLB支持将客户端 IP 传递到后端服务。TKE/TCM 也对该能力做了很好的集成。
但在使用 istio 的时候,由于中间链路上,istio ingressgateway 以及 sidecar 的存在,后端服务如果需要获取客户端 IP,特别是四层协议,情况会变得比较复杂。
对于应用服务商来说,它只能看到 Envoy 过来的连接。
先看看一些常见 Loadbalancer/Proxy 的源 IP 保持方法。我们的应用协议一般都是四层、或者七层协议。
七层的客户端源 IP 保持方式比较简单,最具代表性的是 HTTP 头 XFF(X-Forwarded-For) ,XFF 保存原始客户端的源 IP,并透传到后端,应用可以解析 XFF 头,得到客户端的资源 IP。常见的七层代理组件,比如 Nginx、Haproxy,包括 Envoy 都支持该功能。
IPVS/iptables 都支持 DNAT,客户端通过 VIP 访问 LB,请求报文到达 LB 时,LB 根据连接调度算法选择一个后端 Server,将报文的目标地址 VIP 改写成选定 Server 的地址,报文的目标端口改写成选定 Server 的相应端口,最后将修改后的报文发送给选出的 Server。由于 LB 在转发报文时,没有修改报文的来源 IP,所以,后端 Server 可以看到客户端的资源 IP。
Nginx/Haproxy 支持透明代理( Transparent Proxy )。当开启该配置时,LB 与后端服务建立连接时,会将 socket 的源 IP 绑定为客户端的 IP 地址,这里依赖内核TPROXY以及 socket 的 IP_TRANSPARENT 选项。
此外,上面两种方式,后端服务的响应必须经过 LB,再回到 Client,一般还需要策略路由的配合。
TOA( TCP Option Address )是基于四层协议(TCP)获取真实源 IP 的方法,本质是将源 IP 地址插入 TCP 协议的 Options 字段。这需要内核安装对应的TOA内核模块。
Proxy Protocol是 Haproxy 实现的一个四层源地址保留方案。它的原理特别简单,Proxy 在与后端 Server 建立 TCP 连接后,在发送实际应用数据之前,首先发送一个 Proxy Protocol 协议头(包括客户端源 IP/端口、目标IP/端口等信息)。这样,后端 server 通过解析协议头获取真实的客户端源 IP 地址。
Proxy Protocol 需要 Proxy 和 Server 同时支持该协议。但它却可以实现跨多层中间代理保持源 IP。这有点类似七层 XFF 的设计思想。
istio 中,由于 istio ingressgateway 以及 sidecar 的存在,应用要获取客户端源 IP 地址,会变得比较困难。但 Envoy 本身为了支持透明代理,它支持 Proxy Protocol ,再结合 TPROXY,我们可以在 istio 的服务中获取到源 IP。
istio 东西向服务访问时,由于 Sidecar 的注入,所有进出服务的流量均被 Envoy 拦截代理,然后再由 Envoy 将请求转给应用。所以,应用收到的请求的源地址,是 Envoy 访问过来的地址 127.0.0.6 。
可以看到,httpbin 看到的源 IP 是 127.0.0.6 。从 socket 信息,也可以确认这一点。
我们修改 httpbin deployment,使用 TPROXY(注意 httpbin 的 IP 变成了 172.17.0.59 ):
可以看到,httpbin 可以得到 sleep 端的真实 IP。
socket 的状态:
第一行是 httpbin 的接收端 socket,第二行是 envoy 的发送端 socket。
httpbin envoy 日志:
可以看到,
httpbin envoy 连接 httpbin 的 local address 为 sleep 的 IP 地址。
对于南北向流量,客户端先请求 CLB,CLB 将请求转给 ingressgateway,再转到后端服务,由于中间多了 ingressgateway 一跳,想要获取客户端源 IP,变得更加困难。
我们以 TCP 协议访问 httpbin:
通过 ingressgateway 访问 httpbin:
可以看到,httpbin 看到的地址是 ingressgateway 的地址:
虽然我们在 httpbin envoy 开启了透明代理,但 ingressgateway 并不能把 client 的源地址传到 httpbin envoy 。基于 envoy 实现的 Proxy Protocol ,可以解决这个问题。
通过 EnvoyFilter 在 ingressgateway 和 httpbin 同时开启 Proxy Protocol 支持。
再次通过 LB 访问 httpbin:
httpbin 得到了客户端的源 IP。
可以看到,
可以看到,
值得注意的是, httpbin envoy 的 upstream_local_address 保留了客户端的 IP,这样,httpbin 看到的源地址 IP,就是客户端的真实 IP。
TPROXY 的内核实现参考net/netfilter/xt_TPROXY.c。
istio-iptables 会设置下面的 iptables 规则,给数据报文设置标记。
值得一提的是,TPROXY 不用依赖 NAT,本身就可以实现数据包的重定向。另外,结合策略路由,将非本地的数据包通过本地 lo 路由:
TPROXY 的更多详细介绍参考这里。
这里使用了 Version 1(Human-readable header format) ,如下:
可以看到,header 包括 client 和 ingressgateway 的 IP:PORT 信息。更加详细的介绍参考这里。
ingressgateway 作为发送端,使用 ProxyProtocolUpstreamTransport ,构建 Proxy Protocol 头部:
httpbin envoy 作为接收端,配置ListenerFilter( envoy.filters.listener.proxy_protocol )解析 Proxy Protocol 头部:
这里值得注意的, envoy.filters.listener.proxy_protocol 在解析 proxy protocol header 时, local_address 为发送端的 dst_addr(172.17.0.54:8000) , remote_address 为发送端的 src_addr(106.52.131.116) 。顺序刚好反过来了。
经过 proxy_protocol 的处理,连接的 downstream_remote_address 被修改为client的源地址。
对于 sidecar.istio.io/interceptionMode: TPROXY , virtualInbound listener 会增加 envoy.filters.listener.original_src :
envoy.filters.listener.original_src 通过 tcp option 实现修改 upstream_local_address 为 downstream_remote_address ,实现透传client IP。
另外, httbin envoy 作为 ingressgateway 的接收端, virtualInbound listener 还配置了 ListenerFilter( envoy.filters.listener.original_dst ),来看看它的作用。
对于 istio,由 iptable 截持原有 request,并转到15006(in request),或者15001(out request)端口,所以,处理 request 的 socket 的 local address ,并不请求的 original dst address 。 original_dst ListenerFilter 负责将 socket 的 local address 改为 original dst address 。
对于 virtualOutbound listener ,不会直接添加 envoy.filters.listener.original_dst ,而是将 use_original_dst 设置为 true,然后 envoy 会自动添加 envoy.filters.listener.original_dst 。同时, virtualOutbound listener 会将请求,转给请求原目的地址关联的 listener 进行处理。
对于 virtualInbound listener ,会直接添加 envoy.filters.listener.original_dst 。与 virtualOutbound listener 不同的是,它只是将地址改为 original dst address ,而不会将请求转给对应的 listener 处理(对于入请求,并不存在 dst address 的 listener)。实际上,对于入请求是由 FilterChain 完成处理。
参考 istio 生成 virtualInbound listener 的代码:
基于 TPROXY 以及 Proxy Protocol,我们可以在 istio 中,实现四层协议的客户端源 IP 的保持。
文章来自https://www.cnblogs.com/tencent-cloud-native/p/16355019.html
氐强援生: 如果客户端不知道服务端的ip地址,那怎么建立连接? 或者你是在问在服务端怎么获得客户端的ip? socket.getInetAddress() //获得ip地址 socket.getPort()//获得端口号
崇州市13039125114: MFC网络编程,想通过GetSockName函数获取某个客户端的IP地址,不知道参数怎么用? - ?
氐强援生: 可以使用这个实现.BOOL GetSockName( CString& rSocketAddress,UINT& rSocketPort ); 可以使用 inet_addr得到IP地址字符串对应的int.另一个实现中的数据结构:struct sockaddr { ushort sa_family; char sa_data[14]; }; 实际上等同于如...
崇州市13039125114: 如何实现利用js获取客户端的ip地址 - ?
氐强援生: luocongjay 的思路就对了,JavaScript 只负责应用层上的东西,IP地址是属于网络层的,不需要浏览器提供操作接口.你可以写一个 Web API 接口,由服务器端获取 IP 信息并返回,但是这种操作基本上是多余的,因为服务器端打印网页时
崇州市13039125114: 如果要在网站上获取客户的ip并保存在数据库中在asp.net中怎么实现的? - ?
氐强援生: 在ASP中使用 Request.ServerVariables("REMOTE_ADDR") 来取得客户端的IP地址,但如果客户端是使用代理服务器来访问,那取到的就是代理服务器的IP地址,而不是真正的客户端IP地址.<br><br>要想透过代理服务器取得客户端的真实...
崇州市13039125114: java编程 已知两个客户端的IP 如何通过服务器Serversorcket实现它们间的通信? - ?
氐强援生: 用套接字进行通信,并不需要知道客户端IP,只要知道服务器端IP,每个客户端都和服务器端建立连接.然后客户端之间的通信由服务器转发.举个例子,客户端A想对客户端B说 你好.客户端A先把 你好 发送给服务器端 然后服务器端把 你好 发送给客户端B.这样就达到了A和B通信的目的了.
崇州市13039125114: TCP/IP 使用Socket 实现客户端与服务器端通信 - ?
氐强援生: 对于服务器,你先创建一个套接字(socket),然后bind绑定一个套接地址,然后你在用listen,此时你的服务器会一直阻塞在这,处于监听状态,检查是否有客户端来connect,一旦有的话,你的服务器就会accept.所以,是你的服务器是处于监听状态,监听是否有客户端,然后由客户端来连接服务器,服务器接受客户端的连接,而不是服务器去连接客户端.
崇州市13039125114: 在java后台服务器,如何根据HttpSession获得客户端ip? - ?
氐强援生: 只能根据用户请求带过来的sessionID所匹配的ip地址 而且只可以通过request获得ip 反正你能得到session 那得到request是一样简单的不是吗 得到request后命令为:String ip = request.getRemoteAddr(); 这个ip字符串就是了
崇州市13039125114: Python中 服务器端获取webservice客户端IP地址 - ?
氐强援生: 你的什么服务器?应该可以直接取到地址的.REMOTE_ADDR, REMOTE_HOST.环境变量.
崇州市13039125114: LINUX下如何创建TCP客户端和服务器,实现通信 - ?
氐强援生: 1.可能是在获取客户端的ip和端口时,处理出现问题,导致无法正确发送到客户端.2.客户端是否使用固定的端口来接收服务器信息,或服务器是否正确发送到客户端的相应的端口.3.通过上面分析,最大可能是在处理端口出现问题,请重新检查.4.实在不行,最好使用抛出异常方法来捕获错误消息,或是通过一步一步调试分析数据发送过程.
崇州市13039125114: 关于java编程.设计一个基于TCP/IP协议的网络程序,实现如下功能: - ?
氐强援生: 写好了,有什么问题请追问.客户端:import java.net.*; import java.io.*; public class TestSocketClient { public static void main(String[] args) { try { Socket socket = new Socket("127.0.0.1", 5566); System.out.println("请输入计算式:"); ...