代理技术,是针对客户端(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这么好的轮子,那直接用就可以了。图中假定有一台客户机和一台源服务器。客户机与源服务器连接的中间有一台透明代理服务器。客户机和源服务器的通信报文经过透明代理服务器的转发(透明代理服务器可以是网桥、网关等等)。透明代理服务器在分析客户机流量后,将条件约束的流牵引至目标服务器,这样就实现了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个特征,需要涉及到以下核心技术点。当然,打上核心技术点的标签,并不表示这些技术点有多么高深,只是为了阐明这些点在实现该方案时起到的重要作用。
前言中说了,不要去重复造轮子。代理和牵引,在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);
}
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"
Seq-Proxy = (ISN-Client+ ISN-Proxy + Client_Send_len) & ((1<<32) - 1);
Seq-Server = (ISN-Server + ISN-Proxy + Proxy_Send_len) & ((1<<32) - 1);
更改报文的序列号之后,需要对报文进行校验。
报文的校验分为两部分,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,否则计算出来的校验码是错误的。
对于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));
}