tcpcopy介绍

tcpcopy是一个能将线上环境数据导到测试环境的后台工具,线上的数据对于应用的完整性测试是非常重要, 但是以往将正式环境的数据导入到测试环境测试是非常麻烦的(对于数据量小可以采用tcpreplay),使用tcpcopy以后, 能简化线上数据的测试,只需要将测试服务和tcpcopy部署在不同的机器上进行转发规则,tcpcopy的原理如下图:

安装和启动

(1)下载安装文件和安装依赖库
https://github.com/session-replay-tools/tcpcopy (包括tcpcopy和intercept)
其他依赖库文件:
http://www.tcpdump.org/release/libpcap-1.7.4.tar.gz
http://www.netfilter.org/projects/libnfnetlink/files/libnfnetlink-1.0.1.tar.bz2
http://www.netfilter.org/projects/libmnl/files/libmnl-1.0.3.tar.bz2
http://www.netfilter.org/projects/libnetfilter_queue/files/libnetfilter_queue-1.0.2.tar.bz2
解压以后:
./configure –prefix=xxx
make && make install
(2)配置
如果提示:

checking for LIBNFNETLINK... no
configure: error: Package requirements (libnfnetlink >= 0.0.41) were not met

使用如下配置:

export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/  
echo "/usr/local/lib" >> /etc/ld.so.conf  
ldconfig  
或者 LD_LIBRARY_PATH=$LD_LIBRARY_PATH;/usr/local/lib 将当前安装的so路径加入到配置中

(3)安装intercept

./configure --traditional --nfqueue  
make
make install
启动./intercept -d
如果提示:./intercept: error while loading shared libraries: libnetfilter_queue.so.1: cannot open shared object file: No such file or directory,需要export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib

(4)启动步骤
(说明测试机器xxx.221,线上机器xxx.25,端口都是8030)
a.在测试服务器xxx.221上设置:
启动iptables监听端口8030的出流量转向到NFQUEUE,供intercept调用
iptables -I OUTPUT -p tcp –sport 8030 -j QUEUE
b.在测试服务器xxx.221启动intercept:
./intercept -d 或者类似tcpdump启动:./intercept -F ‘tcp and port 8030’ -d
c.在线上机器xxx.25启动tcpcopy:
./tcpcopy -x 8030-xxx.221:8030 -s xxx.221 -d

tcpcopy使用过程出现的问题

1.rst包出现的请求出现对端端口没有打开,对端会回复rst或者不回复
解决方案:通过tcpdump抓包或者tcpcopy对应的log文件,发现数据包发送到xxx.221不成功,经过一系列排查发现
iptables -I OUTPUT -p tcp –sport 8030 -j QUEUE 流量转向规则没有添加
2.在关闭tcpcopy以后,发现测试服务直接访问不通
解决方案:首先排查对应的intercept是否关闭,然后:

使用 iptables -L -n --line-number 查看原先配置的OUPUT是否删除
使用 iptables -D OUTPUT <上面查看对应的OUTPUT的line number> 删除规则

tcpcopy原理使用–代码分析

tcpcopy拷贝一次流量访问的步骤如下: ①一个访问到达线上前端机;
②socket包在ip层被拷贝了一份传给tcpcopy进程;
③tcpcopy修改包的目的及源地址,发给测试前端机;
④拷贝的包到达测试前端机;
⑤测试前端机处理访问(nginx或者其他服务),并返回结果;
⑥返回结果在ip层被截获、丢弃,由intercept拷贝返回结果的ip header返回;
⑦ip header被发送给线上前端机的tcpcopy进程;

具体源码流程如下:
1)首先,在链路层或者IP层,在把包交到上一层之前,系统会检查有没进程创建了socket(AF_PACKET,SOCK_DGRAM,…)或socket(AF_INET,SOCK_RAW,…)等类型的套接字(即原始套接字sock_raw),如果有这个包就会被复制一份并发送到这个socket的缓冲区;
tcpcopy就是通过这种方式来复制访问流量的;上述的两种抓包方式,前者工作在数据链路层,后者工作在IP层, 在tcpcopy中不同版本所使用的抓包函数不同;

在0.3版本中是:int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP));  
在0.4版本中,用的是:int sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); 

以上两个函数分别工作在链路层和IP层,前者会把进来和出去的包都抓取到,后者只抓取到进来的包。
2)tcpcopy在发送拷贝的数据包的时候,使用了如下socket:

sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);  
并对这个socket设置了IP_HDRINCL:  
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &n, sizeof(n));  
因此网络层不会再增加ip header. 发送之前更改了包的目的ip和端口:  
tcp_header->dest = remote_port;  
ip_header->daddr = remote_ip;  
最后调用sendto函数发送包到测试前端机:  
send_len = sendto(sock,(char *)ip_header,tot_len,0,(struct sockaddr *)&toaddr,sizeof(toaddr));  

3)在测试前端机上加载了ip_queue模块,并设置iptables规则:
iptables -I OUTPUT -p tcp –sport 8030 -j QUEUE
复制的访问流量到达测试前端机上的nginx(这里列举的服务是nginx),nginx处理并返回结果,这个结果包在IP层会被前面所设置的iptables规则匹配发往目标(target)QUEUE,QUEUE是由ip_queue模块实现;下一步这个匹配包就会被内核经过netlink socket发往用户空间的程序(这是测试服务器intercept进程); netlink socket是内核与用户进程之间的一种通信机制,是网络应用程序与内核通信的最常用的接口,可以用来配置网络的各个方面(比如包的过滤); interception用如下方式创建netlink socket:
int sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_FIREWALL);
NETLINK_FIREWALL协议有三种消息类型:IPQM_MODE,IPQM_PACKET,IPQM_VERDICT.内核通过一个IPQM_PACKET消息将刚才截获的返回结果包发送到intercept,intercept给内核发送一个IPQM_VERDICT消息告诉内核对这个包的裁决结果(DROP,ACCEPT,etc.) ;tcpcopy通过这样的办法将测试前端机上nginx返回的结果截获丢弃,并由intercept返回一个ip

拷贝结果包的ip header,发送:
struct receiver_msg_st msg;
...
memset(msg,0,sizeof(struct receiver_msg_st));
memcpy((void *)(msg.ip_header),ip_header,sizeof(struct iphdr));
memcpy((void *)(msg.tcp_header),tcp_header,sizeof(struct tcphdr));
...
send(sock,(const void *)msg,sizeof(struct receiver_msg_st),0);

interception向内核发送IPQM_VERDICT消息报告裁决结果:

struct nlmsghdr* nl_header=(struct nlmsghdr*)buffer;
struct ipq_verdict_msg *ver_data = NULL;
struct sockaddr_nl addr;
nl_header->nlmsg_type=IPQM_VERDICT;
nl_header->nlmsg_len=NLMSG_LENGTH(sizeof(struct ipq_verdict_msg));
nl_header->nlmsg_flags=(NLM_F_REQUEST);
nl_header->nlmsg_pid=getpid();
nl_header->nlmsg_seq=seq++;
ver_data=(struct ipq_verdict_msg *)NLMSG_DATA(nl_header);
ver_data->value=NF_DROP; /*如果要accept这个包,则设为NF_ACCEPT)*/
ver_data->id=packet_id;
memset(addr, 0, sizeof(addr));
addr.nl_family = AF_NETLINK;
addr.nl_pid = 0;
addr.nl_groups = 0;
sendto(firewall_sock,(void *)nl_header,nl_header->nlmsg_len,0,(struct sockaddr *)->addr,sizeof(struct sockaddr_nl));

内核接收到这个包后将packet_id这个包drop或accept,同时从0.4版本开始的tcpcopy利用这个特点保留了一个允许访问的ip列表,因为默认情况下访问测试前端机上nginx服务所得到的结果会在ip层被drop掉,造成在80端口上无法访问nginx;有了这个允许ip列表,即使是刷了iptables规则、起了intercept进程,在某些机器上也是可以正常访问测试前端机上的nginx服务的。