L4(传输层)IP透明反向代理的实现(传递客户端真实IP)

这种需求,一般来说,会在应用层加个标记标明客户端的IP,例如说HTTP,就是添加个请求头的事情。但并不是所有服务器程序、协议你都能这样下手。所以能不能在不对协议和服务器程序本身做任何改动的情况下,传递客户端的IP呢?

最直接的方法应该就是,把L3的源IP改掉,没对协议进行任何改动,对上层完全透明。先来看看改了L3的source IP会出现什么问题:

上图中的A是个L4负载均衡,Router会把来自Internet的客户端请求转发给A,A再根据策略转发到B、C或D。

假设这时A选择了B,A把请求转发给B时,把L3的源IP改成了客户端的IP,那B收到数据包后,根据B系统上的路由表,回复的包会直接经switch交给router送出Internet,也就是说响应的内容不会经过A送回给客户端。听起来好像没什么问题,还省得A耗费资源转发响应内容,不过TCP需要三次握手,客户端跟A握手成功后,A还需要跟B握手:A这时以客户端的IP向B发出SYN,B收到的SYN后,响应一个目的地IP是客户端的SYN ACK,这个SYN ACK是直接由router送出Internet的,根本不会送到A,所以A跟B是无法成功握手的,那客户端的请求也自然无法送达B。

UDP虽然不用握手,但A转发到B用的UDP源端口不一定跟客户端到A的一样,所以客户端不一定能正常接收到B响应的UDP包。要解决这个问题,就要做到请求从哪转发过来的,响应就得送回哪去,既然是从A来的,就送回给A。

A虽然改了送往B的包的源IP,但即使B把响应送回A了,默认情况下,A收到数据包的目的地IP不是系统已绑定的IP时,也就是非送往本地的数据包,kernel只会选择转发或者丢弃,这种情况下,A跟B也是无法握手成功的,所以,这里还要解决的就是,让A对来自B的非本地数据包,当作本地数据包处理。

到这里,实现这个L4 IP透明代理要解决的问题就很清晰了:

  1. 反向代理服务器修改L3源IP
  2. 被代理服务器把响应转发回给反向代理服务器
  3. 反向代理服务器处理来自被代理服务器的非本地数据包

1. 修改L3源IP

Linux的kernel,有一个IP_TRANSPARENT的选项:

简单来说,这个选项允许应用程序的socket,绑定一个操作系统没有绑定的IP,并且以那个IP对外通讯。

所以,A的反向代理程序,通过这个选项让socket绑定客户端的IP,就能以客户端的IP与B进行通讯:

在A上编译运行上面的源码,抓包可以看到A在以192.168.2.2作为源IP向192.168.1.20:80发送SYN,ss或者netstat也可以看到程序的local IP是192.168.2.2,而A系统本身是没有绑定192.168.2.2的:

目前来说A还是无法与B成功握手的,抓包也只会抓到A一直在向B发送SYN,晚一点还会输出”connect(): Connection timed out”。

2. 让B把响应送回给A

在B上抓包,可以看到B会收到来自2.2的SYN,也会响应SYN ACK,但是会收到2.2的RST:

如何让B辨别来自A的包,并把响应送回A呢?

2.1 选择一:根据B的源IP和端口做策略路由

给B分配多个IP,例如默认用1.19出网,1.20专门给A访问用,可以用这种方法。如果只有一个IP,不建议用这种方法,因为这样B会把所有从被代理的端口发出的数据包都交给A,如果请求不是转发自A的,就无法正常处理。

因为A向B发出的请求的目的IP是1.20,所以B发出响应的源IP一定是1.20。而不是对A的响应,B默认使用1.19,不会发送给A。

2.2 选择二:通过mark做策略路由

例如可以通过mac地址识别来自A的包,并且做个标记:

3. 让A处理非本地数据包

第二步的策略路由做好后,A上抓包,可以看到B的SYN ACK送回来了,但是A没有响应ACK:

因为A没有绑定192.168.2.2这个IP,前面提到过kernel是不会对非本地的数据包做出响应的。

哪些目的地IP作为要本地IP处理,是可以在路由表上指定的,可以把0.0.0.0/0,也就是任何IP都视为本地IP处理,当然不能加在默认路由表上,这样就无法对外通讯了,需要做策略路由。

上面有一个路由规则:“local 0.0.0.0/0”,意思是任何IP都作为本地IP处理,注意规则是加在表60的,符合条件的数据包,才会应用这个表。

第三步的规则做好后,再次运行ip_transparent,就可以看到立刻输出“connection established”了:

4. IP透明反向代理

上面的ip_transparent只是测试A和B之间的握手,并不能作为客户端与B之间通讯的代理,这里用Go实现了一个比较简单的反向代理:

上面核心的部分是函数func createTransparentSocket(client net.Conn, destination string) (net.Conn, error),简单来说,就是获取客户端的IP,然后通过bind()绑定为socket的本地IP,再向B发出构建连接的请求。

通过第一个运行参数指定本地监听的IP和端口,第二个运行参数指定被代理服务器的IP和端口:

从client向A发出请求:

B上查看Nginx的访问日志,可以看到记录的IP是client的:

A上的反向代理程序的输出:

这个程序只实现了IPv4 TCP的IP透明反向代理,UDP以及IPv6的实现也同理。

除了反向代理,其实正向代理也是可以通过IP_TRANSPARENT实现,配合iptables的TPROXY模块即可发挥作用,本文的目的只是反向代理,正向代理具体细节就不在本文介绍了。

Linux IPv4 forward自动丢弃源IP为系统已绑定的IP的问题

A是路由,其上做了策略,一部分符合特定条件的流量,是要转发到B进行封装处理,A和B之间做了数个VLAN,B根据VLAN应用不同的策略。

不过这里的问题是,B的默认网关也是A(环路问题是不存在的,已经有对应规则处理),所以B自身发出的流量也会由A应用策略,转发回给B自己。而B收到src IP是local IP(系统已绑定的IP)的包时,kernel会直接丢弃数据包,所以除了B自己的数据包,B都能正常封装。

解决这个问题的一个方法,是让A在把数据包转发给B前,做一次SNAT,这样B收到的数据包,src IP就是A的IP了。其实SNAT并没有什么影响,A对NAT也有hardware offload,但这样操作实在觉得多此一举。

翻了下kernel ip sysctl部分的文档,发现个参数accept_local:

accept_local – BOOLEAN

Accept packets with local source addresses. In combination with suitable routing, this can be used to direct packets between two local interfaces over the wire and have them accepted properly. default FALSE

所以把这个参数置1就能不依赖SNAT解决问题了:

TCP Sendbuffer Size与RTT的问题

最近从A服务器下载数据时,发现如果走B服务器的VPN下载,速率只能跑到300KB左右,但是B从A直接下载却能跑到1MB+/s,B到本地的速度也远超过A到B的速度,其实一开始我以为是VPN实现的问题,有段时间在思考如何优化VPN,但是尝试了其它VPN实现,问题并没有解决。后来发现,走B的代理而不是VPN,速度正常。

本地到B的RTT大约160ms,B到A大约50ms。

Wireshark抓包,对tcp sequence number统计了下:

放大点看:

大概每传输60KB,就要等大约200ms才能继续。200ms刚好差不多等于本地走VPN到A的RTT。传这60KB,只花了大概10ms,忽略这10ms不算的话,每秒大概只能接收五次60KB,5*60KB=300KB,刚好接近走VPN的下载速度。

TCP要发送的数据包存放在send buffer中,send buffer已使用的空间,在收到ACK前是不能释放的,所以,如果这时send buffer满了而未收到ACK,那kernel就不会继续对外发送数据。

所以,走VPN慢的原因,是因为A设定的的send buffer太小,在本地发出的ACK送达A前,A的send buffer就满了。走B代理快,是因为B到A的RTT比较小,ACK是由B上的代理程序在接收到数据后发送给A的,所以A很快就能收到ACK而继续向B发送数据,B到本地没有send buffer过小的问题,所以B上的代理往本地发回数据的速率几乎与A到B的速率一致。

L2TPv3 MTU

L2TPv3支持自动拆包,封装后的数据包如果超出裸线路的MTU也能正常运作。

拆包合包毕竟需要耗资源的,这种多余的操作,能避免的话当然最好。

要避免,首先确定好L2TPv3隧道的MTU。

图:

UDP封装模式:

L2TPv3隧道MTU = 裸线路MTU – IP头 – UDP头 – L2TPv3头 – 以太网头

经测试L2TPv3头在这种模式下是12字节,如果裸线路MTU是1500,那么隧道MTU = 1500 – 20 – 8 – 12 – 14 = 1446,TCP MSS = 1446 – 20 – 20 = 1406

IP封装模式:

L2TPv3隧道MTU = 裸线路MTU – IP头 –  L2TPv3头 – 以太网头

经测试L2TPv3头在这种模式下是8字节,如果裸线路MTU是1500,那么隧道MTU = 1500 – 20 – 8 – 14 = 1458,TCP MSS = 1458- 20 – 20 = 1418

 

以太网头不一定是14字节,根据实际使用情况自行调整。

Fix Ubuntu policy-based route with fwmark not work

近期发现Ubuntu基于ip rule add fwmark … lookup …所做的策略路由无法正常使用,而Debian正常。抓包的特征就是,出网数据包能应用到策略路由,也能抓到回应的数据包,但回应的数据包被内核丢弃,不会送达user space。

而ip rule add from … lookup …则正常。

查找资料得知是rp_filter的影响(见此:https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt),继而发现Ubuntu的/etc/sysctl.d是有以下这个文件的:

编辑/etc/sysctl.d/10-network-security.conf,把net.ipv4.conf.default.rp_filter以及net.ipv4.conf.all.rp_filter改成2,然后应用: