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模块即可发挥作用,本文的目的只是反向代理,正向代理具体细节就不在本文介绍了。

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,然后应用:

 

QoS —— 交互式应用最低延迟,HTTP(S)、IMAP等常用服务优先,迅雷、P2P受限

前言

使用迅雷全速下载时,其余网络应用的体验几乎约等于断网状态。

全速下载时,迅雷并没有占用太多上行带宽,于路由器上观察上行速率,同样正常:

可见全速下载时出现的现象,问题在于下行。

迅雷全网搜索资源,这意味着会有许多来自不同地方的计算机,以高速率往我的计算机发来大量数据包。ISP处的队列早已被这些P2P数据包塞满,而SSH、网页等服务的数据包速率不高,根本不够P2P数据包竞争,这些数据包到达ISP处时,只能由于队列已满而被丢弃。

 

其实这个问题,并没有什么好的解决办法,因为ISP处的队列我们没有权限控制。

我们所能做的,只是控制P2P的速率,避免因ISP处队列满而使得数据包被丢弃。

不过,上行队列我们拥有完全控制权,倒可以自由规划。

蓝图

首先,我的主要目的是,当私有网络内有人使用迅雷等进行全速下载时,这个下载不会影响交互式应用、网页流量等,不受影响终端的包括下载者自己,其次,也要保证下载流的下行速率。

这是一项很有挑战性的任务。

下行流量大致可以分为三类:

  • 最低延迟
  • 优先传送
  • 受限

这里没有普通流量这一类,因为普通流量在P2P软件,尤其是迅雷面前尤其不好区分,我尝试过Deep Packet Inspection,迅雷的流量绝大部分被分类到了Other里面。因此,与其花心思、花系统资源去区分这些“普通流量”,还不如直接使用白名单机制,把最低延迟、优先传送的区分出来。

上行流量:

  • 最低延迟
  • 优先传送
  • 普通
  • 受限

最低延迟

最低延迟的数据包,可能会以IP Header中的TOS来标记,但抓包分析发现,基本上没有使用此字段的数据包,甚至是SSH(但内网的SSH有把TOS设为0x10,表示最低延迟,可能是公网有路由清空了该字段)。所以说,只有KCPTUN之类的会“滥用”该字段,使用该字段区分最低延迟数据包意义不大。

TCP三次握手比较关键的是SYN、SYN & ACK,带有SYN和SYN & ACK标记的我们可以归类为最低延迟,此外ACK的小包(小于64字节),我们也可以归类为最低延迟。

SSH,DNS,远程桌面,QQ以及微信等交互式服务,归类为最低延迟。

优先传送

以下协议归类为优先传送:

  • HTTP
  • HTTPS
  • SMTP
  • IMAP
  • GIT

这里有个问题,如果使用迅雷进行HTTP下载,迅雷也会全网搜索HTTP资源,这里正常的HTTP流量与迅雷的HTTP流量不是很好区分。

一个最大的特征是,迅雷可能会与同一个IP建立多个TCP连接,据此可把迅雷下载用的TCP连接归类到受限。

对于单连接的,这里没有好的办法,有以下几个原因:

根据速率区分:前面已提到迅雷会全网搜索资源,意味着迅雷会构建很多TCP连接,单个TCP连接的速率不会很高。阈值低了,影响正常网页流量,高了,无效。

根据传输流量区分:首先HTTP 1.1支持Keep Alive,这意味着对于正常网页的TCP连接来说,流量达到阈值也是有可能的。

上述二者结合:如果我一个网站同时打开多几个页面,岂不是一样被归类为受限?况且现在一个页面数MB,也很正常吧?

根据内网IP构建的TCP连接数区分:我的目的是不要让迅雷影响正常的应用,如果让某个TCP连接数多的IP归类到受限,岂不是不能实现我这个目的?

普通

本分类仅用于上行,未被归类的均归于普通类。

受限

对于下行,未被归类的,均为受限。

P2P的上行发送的都是大于1350字节的UDP数据包,把这些数据包归类为受限即可。

实现

我使用的是中国电信的带宽

下行:100 Mbps,上行:20 Mbps。

互联网接口:eth0,私有网络接口:switch0。

 

首先设置以下数个变量,方便后面的命令使用:

这里我主要的目的在于不要让P2P把ISP处的队列占满,其次对于正常的流量,并不会占用太多带宽,因此正常流量不作速率限制,仅分配不同的优先级,而P2P则限制速率,设置最低的优先级。

区分P2P上行

把大于1350字节的UDP数据包,都标记为100。

区分迅雷TCP流

P2P的不需要区分,因为下行使用的是白名单机制。

主要问题在于迅雷的HTTP下载,实难区分,这里用了一个不太尽人意的方法,把迅雷的TCP流标记为200:

首先,不能在互联网接口上对TCP流进行归类,因为在互联网接口上,所有TCP流的目的地都是我们互联网接口的IP,这导致的问题就是无法独立统计单个内网IP对单个远程IP的链接数。想一想,如果网内有多个人同时访问某个网站,是不是有可能被归类为受限?

因此这里在私有网络的接口上进行归类。

归类用到了两个模块,分别是connlimit,hashlimit。

connlimit模块仅能统计单端的链接数,hashlimit模块能双端统计,但是只能统计速率,因此配合使用,以接近限制端对端链接数的效果。

这里connlimit用到的参数,表示远程一个/24与本地构建的TCP流超过四的情况;hashlimit的参数表示远程一个/24与本地一个IP突发5MB后,数据传输速率>512kb/s的情况。

这里还用到了connmark模块,这个模块用于记录TCP流的标记,下次遇到相同的TCP流时,取上次的标记即可,无需再使用connlimit与hashlimit模块进行判断,因为这两个模块资源消耗不低。

构建上行规则

这里使用了HTB对速率进行限制,并在每个类下面安置了一个SFQ队列,以公平分配一个队列中各流的带宽:

接下来就把数据包归类,我选择使用u32选择器。

u32选择器强大,同时也难用。

u8, u16都会转换为u32,uX跟着一个值与掩码,at后面是偏移量,偏移从IP Header开始。

掩码将会与数据包偏移位置处进行位于,再与给定的值进行比较。

更多u32选择器的细节,见u32选择器的手册页:Universal 32bit classifier in tc(8) LinuxUniversal 32bit classifier in tc(8)

分类规则:

构建下行规则

队列的构建与上面同理,只是少了一个普通流量的类:

归类:

完整的Script

总结

对P2P的归类效果还是非常好的:

1:1是总的下行,1:100是P2P的类别的下行。

P2P上行也很精确地限制在了1 Mbps:

要对迅雷的全网HTTP资源多线程下载完全正确归类可能做不到,但IP对IP流量大,高数据包速率的都能受限,正常的网页流量,交互式应用受到的影响不会太大。

下行速率的控制会不太准确,毕竟我们无法控制远程计算机给我们发包的速率,需要根据实际情况适当减少rate的值。

对于延迟,就比较难保证了,这依赖于ISP的队列规则,即使我们尽量做到不让ISP处丢包,但大量的数据包排队还是会导致其余应用的延迟增加。我这里把P2P的下行速率限制为80 Mbps,P2P全速下载时,延迟也只是增加了5 – 10 ms,可以接受,ISP处的队列比较合理。

使用策略路由实现涉及getsockopt(…, SO_ORIGINAL_DST, …)程序的远程调用(Redsocks, ss-redir之类的远程调用)

Redsocks/ss-redir的实现

DNAT与REDIRECT模块在iptables target extension之列。

这两个模块都能根据参数,修改数据包的header中的目的IP和端口。

既然修改,那就不是附加,毕竟网络层传输层header的内容怎么可能随随便便增删呢,这意味着,数据包的真实目的地会被完完全全地抹掉

Redsocks/ss-redir收到这样的数据包后,不做特殊处理,何以知道这数据包原来想发给谁呢?

这些程序之所以能正常工作,得益于NAT的透明性,被NAT处理的数据包,在内核中均有所记录,NAT后的正常通讯由NAT发起者维持,Redsocks/ss-redir这类程序便是通过内核中的记录得知真实目的地的。

这个原地址的获取可以通过系统调用实现:

optname需要传值SO_ORIGINAL_DST。

Redsocks的调用见base.c的223行

问题所在

假定有以下的网络架构:

正常情况下,NAT与getsockopt(…, SO_ORIGINAL_DST, …)调用程序都运行在Router上,工作正常。

现在把getsockopt(…, SO_ORIGINAL_DST, …)调用程序迁移到Server 1上,很容易想到的,就是把原来的NAT规则改成DNAT的,并指定目的地IP与端口:

NAT的记录由内核管理,这隐含的意思是,只有在发起NAT处理的内核上才能获取到NAT前的信息。

若调用远程的getsockopt(…, SO_ORIGINAL_DST, …)调用程序,即发起NAT的与运行getsockopt(…, SO_ORIGINAL_DST, …)调用程序的不是同一个内核,很明显,这些程序对getsockopt()的调用不可能实现他们的目的。

问题已很明显,这类程序无法通过getsockopt()调用取得真实目的地。

实现getsockopt()的远程调用成本有点高,不太现实。

不过,我们把方式稍作改变,让“远程”变为“本地”。

实现

假定有以下的网络架构:

Server 1上运行着getsockopt(…, SO_ORIGINAL_DST, …)调用程序。

既然处理NAT要与调用getsockopt()的使用同一个内核,那么把需要NAT的那部分也交给Server 1处理好了。

要实现这种需求,可以利用策略路由,正常的数据包按正常处理,原本需要NAT到Server 1的数据包,使用路由规则转发到Server 1。这样就不需要网内其余联网设备作任何改变,只需要对Router与Server 1进行操作。

准备

Router 1上首先把针对getsockopt(…, SO_ORIGINAL_DST, …)调用程序的NAT规则删掉。

路由表

修改/etc/iproute2/rt_tables增加一个路由表,如12行,增加一个ID为250,名为server1的路由表:

往server1路由表中增加一条默认路由规则,把所有数据包都转发到Server 1:

数据包分类

接下来需要把数据包分类,即决定哪些数据包应使用路由表server1。

我这里选择iptables的MARK模块,把所有需要给getsockopt(…, SO_ORIGINAL_DST, …)调用程序处理的数据包,都加一个固定标记,然后让路由让有该特定标记的数据包都根据路由表server1处理。

iptables MARK模块的–set-mark操作仅能在mangle表完成。

假设MARK为150(把…改成你需要的参数):

这里需要注意,Server 1访问国际互联网,也要通过Router,如果getsockopt(…, SO_ORIGINAL_DST, …)调用程序访问互联网会匹配到上面的规则,必须排除,否则就有环路了,最简单的,就是直接排除掉Server 1的MAC地址:

这条排除Server 1的规则需要插入在MARK之前。

最后修改路由策略,让带150 MARK的数据包都使用路由表server1:

Server 1的处理

数据包已在Router处分类,并不经网络层和传输层Header的修改转发到Server 1处。

Server 1要区分这些数据包是要交给getsockopt(…, SO_ORIGINAL_DST, …)调用程序,还是交给自己的程序处理,很简单,看看数据包目的IP是不是自己就行了,要交给getsockopt(…, SO_ORIGINAL_DST, …)调用程序的数据包,目的IP肯定不是Server 1的IP:

总结

这个方法仅适用于Router与Server 1处于同一局域网的情况,因为路由转发仅能在链路层上进行,若需要跨网域调用,可能要考虑使用隧道封装,但这样开销有点大。

Router一般都在芯片上实现了NAT,NAT效率高,Server 1虽然没芯片级的NAT,但我相信,你处理被NAT的数据包的程序,开销比NAT大得多。

解决安装OpenVZ vzctl 4.7后iptables nat表提示”can’t initialize iptables table `nat’: Table does not exist (do you need to insm Perhaps iptables or your kernel needs to be upgraded.”

几天前给一个服务器安装OpenVZ后,使用iptables添加nat规则时,出现错误提示:

执行lsmod看了下:

发现相关的NAT模块都已经加载,应该与内核无关。

更换内核回2.6.32-431.11.2.el6,问题还是如此,无奈之下重装系统。

安装系统后,我手动一个个下载OpenVZ的RPM包,一个个安装,然后重启后nat表是否可以使用,最后发现安装vzctl 4.7并且重启后,iptables的nat表就出问题了。

看了下官网,原来在OpenVZ在2014年04月15号更新了,难怪其他服务器没遇到这问题。

到OpenVZd Bugzilla瞧了瞧,是nf_conntrack模块的问题,官方的技术人员已经给出了解决办法了:

修改/etc/modprobe.d/openvz.conf,把

修改为:

尝试用rmmod卸载nf_conntrack,再用modporbe重新加载nf_conntrack,但是执行rmmod nf_conntrack有提示某些程序在使用nf_conntrack模块,所以只能reboot了。

系统启动后,已可以正常加载出nat表:

OpenVZ此次更新的改动貌似有点大,4.6.1和之前的版本都是用simfs的,4.7开始已默认使用ploop。