Linux环境下基于条件约束的HTTP/TCP透明代理和流量牵引方案

一. 前言

    代理技术,是针对客户端(Client)和服务器(Server)之间的通信交互而言的一种介入技术。依据代理方式的不同,大致可以分为两种:1. 非透明代理;2. 透明代理。

    1. 非透明代理

    即被代理的客户端知晓代理服务器的存在。知晓的方式有很多种,比如客户端浏览器中设置代理服务器的IP地址和端口;又比如说,客户端在访问服务器时,捕获到了服务器返回的HTTP 302重定向报文(通常是代理服务器伪装源服务器返回的)等等。当然,对于不熟悉通信协议的用户来说,HTTP 302重定向这种方式也可以归类为透明代理。

    2. 透明代理

    即被代理的客户端根本不知道有代理服务器的存在。代理服务器在客户端和服务器的通信中途,接管了客户端的通信报文。透明代理的存在方式一般为网关、路由器和透明网桥等之中。

    非透明代理的实现方式一般来说较为简单,搭建一个代理服务器(如Squid和Nginx等),把该代理服务器的IP地址和端口提供给客户端即可,这不是本文讨论的内容。

    透明代理的实现方式相对来说复杂一些,但是也有成熟的解决方案。通常的思路是,在客户端和服务器通信的中途,将客户端的流量透明接管,同时利用4层/7层协议分析技术,过滤出需要代理的流量,并将这些流量牵引至代理服务器。常用的方案有(以Linux为例):

    1. Iptables/Netfilter/Tproxy  ----(特定IP/Port流量)---->  Squid/Nginx代理服务器

    2. Iptables/Netfilter/Tproxy  ----(特定IP/Port流量)---->  Haproxy  ----(7层过滤)---->  Squid/Nginx代理服务器

    第一种方式效率高,但是也存在问题,就是有可能把非HTTP流量送给了HTTP代理服务器(很多非HTTP流量占用的是80/8080/3128端口),可能造成代理服务器出现问题;第二种方式,在前端增加了Haproxy的7层过滤,确保送给代理服务器的流量是HTTP流量。

    这些透明代理方案的问题在于,缺乏精确的流量识别能力,所以需要管控的流量过多(所有HTTP Port的流量),在大背景流量下压力下,代理服务器的资源开销会很高;同时,为了减小代理服务系统出错概率,需要在Squid/Nginx前端对流量进行层级的过滤。试想,流量经过了多个模块的多次过滤,抛开代理服务器资源占用不说,报文的处理时间也被拉长了。

    因此,本文讨论的重点,就是阐述基于条件约束的HTTP/TCP透明代理和流量牵引方案。该方案具备如下特征:

    1. 条件约束

    即待代理的流量,必须在明确知道其内容时,才进行管控。明确的内容,比如GET什么文件,是rar还是exe,Host是什么等等。这样,才能极大的减小管控流量范文。通俗的讲,就是“不见兔子不撒鹰”。

    2. 透明HTTP/TCP代理

    这里说的透明,不是依靠HTTP 302来实现的。HTTP 302能够被用户抓包发现,同时,部分客户端软件是不支持HTTP 302重定向的;另外,针对纯TCP通信(非HTTP通信)而言,是没有类似HTTP 302这种重定向机制的。代理服务器必须能够实现条件约束下的HTTP/TCP流量透明代理。

    3. 资源开销小

    什么叫资源开销小,就是系统只经过一级过滤,即可实现以上1、2的功能要求。

    这样说来,这种透明HTTP/TCP代理的实现是具备一些挑战的。当然,越是有挑战的事情,就越值得去做,哪怕做不出来,这种经历也值得拥有。

    同时,为什么要起名“条件约束”?因为,在条件满足的情况下做出来的事情,那不叫“成功”;只有在有限条件下做出来的事情,才能叫“成功”。呵呵,这说得有点夸大了。

    另外,为什么要在Linux下去实现?因为,本人专业是通信与信息系统,不懂操作系统,但也不是完全不懂,Linux还是懂一些的。而且,做IT的人都知道,“不要重复造轮子 Stop Trying to Reinvent the Wheel”。既然Linux已经提供了Netfilter这么好的轮子,那直接用就可以了。

二. 原理图

    前言中已经对方案基本的原理做了简单描述,这里用一张图来更直观的进行说明。
Linux环境下基于条件约束的HTTP/TCP透明代理和流量牵引方案_第1张图片

    图中假定有一台客户机和一台源服务器。客户机与源服务器连接的中间有一台透明代理服务器。客户机和源服务器的通信报文经过透明代理服务器的转发(透明代理服务器可以是网桥、网关等等)。透明代理服务器在分析客户机流量后,将条件约束的流牵引至目标服务器,这样就实现了HTTP/TCP的代理。

    方案的流程可以描述如下(以HTTP通信为例,纯TCP通信类似):

    1. 客户机与源服务器进行TCP三步握手,并成功;

    2. 客户机发送HTTP GET报文给源服务器,报文到达代理服务器;

    3. 代理服务器分析该HTTP GET报文,提取其中待分析字段,并判断是否需要进行透明代理。若需要,则继续步骤4;否则,跳转步骤7;

    4. 代理服务器与目标服务器进行TCP三步握手,并成功;

    5. 代理服务器向目标服务器发送客户端原HTTP GET报文;

    6. 目标服务器返回HTTP Response报文,转步骤9;

    7. 代理服务器向源服务器转发客户端的HTTP GET报文;

    8. 源服务器返回HTTP Response报文;

    9. 代理服务器向客户端返回HTTP Response报文。

    特别要说明的是,在步骤4和步骤5的时候,代理服务器向目标服务器发送的HTTP GET报文,该报文的源IP地址可以是客户机的IP地址,也可以是代理服务器的IP地址,而目的IP地址则一定为目标服务器的IP地址;在步骤9的时候,代理服务器向客户机返回的HTTP Response报文,源IP地址则一定为源服务器的IP地址,不论该流量是否已经被代理。因为,只有这样,客户机与源服务器的通信才没有中断,在客户机看来,这个通信也是透明的。

    另外要说的一点,目标服务器上其实就是一个Web Server。因此,目标服务器可以和代理服务器是在同一台物理机器上,只需要这台物理机器开放了Web服务即可。因为,下面说到具体方案实现时,会考虑到这一点。


三. 核心技术点

    为了实现上面的功能,同时满足前言中所述的3个特征,需要涉及到以下核心技术点。当然,打上核心技术点的标签,并不表示这些技术点有多么高深,只是为了阐明这些点在实现该方案时起到的重要作用。

3.1. 如何代理和牵引

    前言中说了,不要去重复造轮子。代理和牵引,在Linux系统下就有Netfilter这样的优秀实现,最简单的方式就是使用其中的DNAT功能。

    DNAT可以修改报文的目的IP地址,将修改后的报文路由到新的IP地址上。DNAT可以通过iptables进行配置,使能Netfilter。但是,我们的方案并不是去配置Netfilter,而是要进行更多的“条件约束”。所以,只能直接调用Netfilter的函数了。

    同时,为了实现上面的资源占用率小的要求,这个功能还只能在内核态去实现,不能把报文提到用户态来分析处理。

    引子引来引去,不知道说清楚没有。总之,就一句话,写一个Netfilter的内核模块,使用了DNAT功能。核心的Netfilter模块代码就是调用nf_nat_setup_info这个函数。
/* Set up the info structure to map into this range. */
unsigned int nf_nat_setup_info(struct nf_conn *ct,
                               const struct nf_nat_range *range,
                               enum nf_nat_manip_type maniptype);
if (0 == test_bit(IPS_DST_NAT_DONE_BIT, &(ct->status))){	
	natrange.flags = NF_NAT_RANGE_MAP_IPS | NF_NAT_RANGE_PROTO_SPECIFIED;
	natrange.min_addr.ip = natrange.max_addr.ip = in_aton(PROXY_SERVER);
	natrange.min_proto.tcp.port = natrange.max_proto.tcp.port = htons(PROXY_PORT);

	retVal = nf_nat_setup_info(ct, &natrange, NF_NAT_MANIP_DST);
}
    需要注意的是,不同Linux内核版本,该函数的参数不一样。
    另外,使用时,系统要先加载ip_conntrack, nf_nat模块;调用时,要判断ctinfo中IPS_DST_NAT_DONE_BIT是否已经设置。否则,内核panic就郁闷了。


3.2 解析条件约束

    解析条件约束,就是获取特征报文中的信息,判断这条流是否需要进行代理和牵引。针对HTTP 协议,可以通过解析HTTP报文来获取。例如,HTTP GET报文中包含了URI、Host、Cookies、User-Agent等信息。对于其他消息格式,如HEAD、PUT、POST、DELETE、OPTIONS、CONNECT、TRACE等报文,一样的解析。

GET / HTTP/1.1
Host: www.sina.com.cn
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
Cookie: UOR=www.google.com.hk,blog.sina.com.cn,; SINAGLOBAL=111.204.219.195_1457679900.711867; U_TRS1=000000c3.4a14b2.56e26e1d.3ae1a792; vjuids=70ec49e0d.153647ebaae.0.cce4d7f6; ULV=1457680151554:2:2:2:111.204.219.195_1457679901.67139:1457679899520; SUBP=0033WrSXqPxfM725Ws9jqgMF55529P9D9WF5ROAhnUO62K6oOWNW_Llm5JpX5Kzt; vjlast=1457685828
switch (ch) {
	case 'C': parser->method = HTTP_CONNECT; break;
        case 'D': parser->method = HTTP_DELETE; break;
        case 'G': parser->method = HTTP_GET; break;
        case 'H': parser->method = HTTP_HEAD; break;
        case 'O': parser->method = HTTP_OPTIONS; break;
        case 'P': parser->method = HTTP_POST; break;
        case 'T': parser->method = HTTP_TRACE; break;
        default:
          SET_ERRNO(HTTP_INVALID_METHOD);
          goto error;
}

// GET URI
decoded_get_pkt.pUri = payload + 4;  // skip 'GET ' in first line
pEnd = search_string(decoded_get_pkt.pUri, " ", payload_len - 4, 1);	//minus 'GET ' len
if (NULL != pEnd){
	decoded_get_pkt.uri_len = (short int)(pEnd - decoded_get_pkt.pUri);
}else{
	return FAILED;
}

// GET FILETYPE
pEnd = decoded_get_pkt.pUri + decoded_get_pkt.uri_len - 1;
while(*pEnd != '/'){
	if (*pEnd == '.'){
		decoded_get_pkt.pFileType = pEnd + 1;
		decoded_get_pkt.file_type_len =decoded_get_pkt.uri_len - (pEnd - decoded_get_pkt.pUri + 1);
		break;
	}

	pEnd--;
}

// GET HOST
pEnd = decoded_get_pkt.pUri + decoded_get_pkt.uri_len;
decoded_get_pkt.pHost = search_string(pEnd + 11, "Host: ", payload_len - (unsigned int)(pEnd + 11 - payload), 6);	// skip first 'GET' line
if (NULL != decoded_get_pkt.pHost){
	decoded_get_pkt.pHost += 6; 	// skip "Host: "

	pEnd = search_string(decoded_get_pkt.pHost, "\r\n", payload_len - (unsigned int)(decoded_get_pkt.pHost - payload), 2);
	if (pEnd != NULL){
		decoded_get_pkt.host_len = (short int)(pEnd - decoded_get_pkt.pHost);
	}else
		return FAILED;
}else{
	return FAILED;
}

    对于非HTTP报文来说,则需要针对不同的报文做特殊处理。例如Bittorrent协议,握手报文的特征字符串则为BitTorrent protocol,需要针对这样的字符串进行匹配来判断。

#define BITTORRENT_HANDSHAKE_STR "BitTorrent protocol"


3.3 处理TCP Sequence/ACK序号

    方案中,由于流量透明代理的地方处于通信过程的TCP三步握手之后,所以代理服务器不能简单依据Netfilter中NAT模块提供TCP序列号纠正,需要自行进行计算。流量透明代理之后,代理服务器工作于客户机和目标服务器之间,那么,代理服务器就需要维护与客户端的TCP连接,以及与目标服务器连接之间的TCP连接,提供序列号变换操作。
    变换关系的处理,其实并不复杂。代理服务器首先需要记住客户机与源服务器通信过程的客户机Initial sequence numbers (ISN-Client)和服务器ISN-Server。在流量代理之后,同时记住目标服务器的ISN-Proxy,通信过程中在这三个ISN之间进行变换计算即可
Seq-Proxy = (ISN-Client+ ISN-Proxy + Client_Send_len) & ((1<<32) - 1);
Seq-Server = (ISN-Server + ISN-Proxy + Proxy_Send_len) & ((1<<32) - 1);
    为什么要按位与上 (1<<32)?因为TCP Sequence/ACK的值是32位无符号整数,超过之后会自动溢出。在内核中,按位与运算的效率要高于取模运算,且内核中是没有%取模操作符的。

3.4 报文校验

    更改报文的序列号之后,需要对报文进行校验。

    报文的校验分为两部分,IP报头校验和TCP报头校验。IP报头校验值只和IP段内容相关,由Netfilter的NAT模块进行DNAT和SNAT操作之后提供,无需自行计算。TCP报文的校验则需要自行计算。需要注意的一点是,客户机发给源服务器的报文,在代理服务器的Pre-Routing处即可计算;而目标服务器返回给代理服务器的报文,由代理服务器进行SNAT之后,在Post-Routing处进行计算,再发给客户机。
tcph->check = 0;
tcph->check = csum_tcpudp_magic(iph->saddr, iph->daddr, 
				ntohs(iph->tot_len) - iph->ihl * 4, iph->protocol, 
				csum_partial((unsigned char *)tcph, ntohs(iph->tot_len) - iph->ihl * 4, 0));

    TCP报文在计算校验前,一定要先把check字段设置为0,否则计算出来的校验码是错误的。


3.5 代理后如何获取源服务器真实IP地址

    对于HTTP的代理而言,目标服务器是不需要获取源服务器真实IP地址。因为,从HTTP GET报文中,目标服务器可以通过Host字段获取源服务器域名,再进行DNS解析即可。

    对于TCP的代理而言,目标服务器则需要获取源服务器真实IP地址。如果目标服务器程序与代理服务器不在同一物理服务器上,只能通过额外连接进行交互获取源服务器真实IP地址;若处在同一台物理服务器上,Netfilter子系统中提供了获取的方法,即在目标服务程序中,对接入的连接使用getsockopt函数提取SO_ORIGINAL_DST参数。
struct sockaddr_in ori_serv_addr;
socklen_t ori_serv_addr_len = sizeof(ori_serv_addr);
if (0 == getsockopt(client_fd, SOL_IP, SO_ORIGINAL_DST, (struct sockaddr*)&ori_serv_addr, &ori_serv_addr_len)) {
    printf ("The original server address => %s:%u\n", inet_ntoa(ori_serv_addr.sin_addr), ntohs(ori_serv_addr.sin_port));
}

四. 验证

    内核模块写完了,当然要迫不及待的验证一下。验证的目的,就是看功能是否符合标题所言,即“条件约束”下的透明代理。
    为了简便,采用HTTP协议进行验证。实验环境的拓扑图和文章(二)段中介绍的原理图一样,源服务器为新浪,目标服务器为QQ(两大门户网站,被我小戏弄一番,应该没事)。

    各个服务器的参数如下:
    (1)客户机
        Windows 7,Chrome浏览器,Wireshark抓包工具
    (2)代理服务器
        Ubuntu 12.04.5 LTS,网桥模式;
        代理程序内核模块基于Linux Kernel 3.13.0-32-generic
    (3)源服务器及请求URL:
        a) http://i1.sinaimg.cn/dy/deco/2013/0329/logo/LOGO_1x.png
        b) http://i1.sinaimg.cn/lx/2011/0426/U5475P622DT20110426110242.jpg
        c) http://n.sinaimg.cn/tech/transform/20160311/31iY-fxqhmvp6148849.jpg
    (4)目标服务器
        www.qq.com
        ip地址101.226.103.106

    验证场景:
        a) 当客户机请求域名  i1.sinaimg.cn 的png文件时,将请求代理到目标服务器www.qq.com,目标服务器IP地址101.226.103.106;
        b) 当客户机请求域名  i1.sinaimg.cn 的非png文件时,流量一律放行;
        c) 当客户机请求非域名i1.sinaimg.cn 内容时,流量一律放行;

    操作步骤:
        a) 代理服务器安装双网卡,开启网桥模式;一边接互联网,一边接客户机;客户机通过该网桥连入互联网
        b) 客户机开启Wireshark抓包
        c) 客户机使用Chrome浏览器访问上面3个URL,获取数据。

4.1 验证场景a

Linux环境下基于条件约束的HTTP/TCP透明代理和流量牵引方案_第2张图片
    从客户机的Chrome浏览器截图可以看到,浏览器没有获取任何数据。
Linux环境下基于条件约束的HTTP/TCP透明代理和流量牵引方案_第3张图片
    从客户机Wireshark抓包可以看到,服务器返回的HTTP 508响应,这是QQ服务器返回的响应,没有数据;符合预期,流量被正确识别,并代理和牵引到了QQ服务器。

4.2 验证场景b

Linux环境下基于条件约束的HTTP/TCP透明代理和流量牵引方案_第4张图片
    从客户机的Chrome浏览器截图可以看到,浏览器正确获取了图片文件,显示正常。
Linux环境下基于条件约束的HTTP/TCP透明代理和流量牵引方案_第5张图片
    从客户机Wireshark抓包可以看到,服务器返回的HTTP 200 OK响应,且为正确的文件响应;符合预期,该流量没有被代理和牵引。

4.3 验证场景c

Linux环境下基于条件约束的HTTP/TCP透明代理和流量牵引方案_第6张图片
    从客户机的Chrome浏览器截图可以看到,浏览器正确获取了图片文件,显示正常。
Linux环境下基于条件约束的HTTP/TCP透明代理和流量牵引方案_第7张图片
    从客户机Wireshark抓包可以看到,服务器返回的HTTP 200 OK响应,且为正确的文件响应;符合预期,该流量没有被代理和牵引。

五. 后续

    网桥模式的方案验证完了,下一步打算移植到Openwrt环境下。Openwrt环境天生的路由器模式,正好验证一下路由模式下方案是否可行。

你可能感兴趣的:(Linux环境下基于条件约束的HTTP/TCP透明代理和流量牵引方案)