在C中使用raw sorket进行IP包转发
一般来说,ip包的抓取和转发可以用赫赫有名的libpcap来做,发包时不仅仅需要构造ip包,也需要构造以太网帧的头部,并指定链路层的device。但有的同学会觉得libpcap太重了,如果只是用于学习和简单调试,在这里我来简单的介绍另外一种ip包嗅探和转发的方法: 使用raw socket,并提供一个简单对http请求进行ip包转发demo。
在阅读这篇文章及案例前,需要对OSI网络模型有些基本的了解,tcp,ip,http头部信息结构可以看看对应的维基,在此我就不再赘述了。
IpV4:
Tcp: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_segment_structure
Http-request-header: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields
raw socket
那么, 什么是raw-socket呢?他跟普通的socket有什么不同?
详细介绍可以看这里:https://linux.die.net/man/7/raw
简单的说,raw socket和其他socket操作是类似的,只是他支持发送和接受不包含链路层头部的原生ip报文。
就tcp来说,经过系统级三次握手从而建立连接之后,tcp socket接受到的是stream,而如果使用raw socket拿到则是从握手开始的最基本的IP包。
使用步骤
- 创建raw socket(如果不想规定类型是TCP,将IPPROTO_TCP换成IPPROTO_IP)
int s = socket (PF_INET, SOCK_RAW, IPPROTO_TCP);
if(s == -1)
{
perror("Failed to create socket");
}
- 将socket option中的IP_HDRINCL设置为1,这样才能发包(如果只需要抓包不需要这一步)
int one = 1;
const int *val = &one;
if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, val, sizeof (one)) < 0)
{
perror("Error setting IP_HDRINCL");
}
- 将socket的recvfrom置于无限循环中,这样便能无限抓包 (如果只需要发包不需要这一步)
while(1){
struct sockaddr_in serverProxy;
u_char raw_Buffer[1024];
int saddr_size = sizeof serverProxy;
int data_size = recvfrom(s , raw_Buffer , 1024, 0 , (struct sockaddr *)&serverProxy , &saddr_size);
if(data_size < 0)
{
printf("Recvfrom error , failed to get packets\n");
return 1;
}
//Now process the packet
ProcessPacket(raw_Buffer , data_size, s);
}
- 构造ip包后即可发包(IP包篡改可具体看后面案例,现在假设IP包已经构造好了,buffer是报文,iph是ip头部信息,tcph是tcp的头部信息)
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = tcph -> dest;
dest_addr.sin_addr.s_addr = iph -> daddr;
if (sendto (s, buffer, ntohs(iph -> tot_len), 0, (struct sockaddr *) &dest_addr, sizeof (dest_addr)) < 0)
{
perror("sendto failed");
}
else
{
printf ("Packet Send. Length : %d \n" , ntohs(iph -> tot_len));
}
使用案例
demo必须功能简单,实现的场景如下:
A为client, B为代理机器,C为真实http server。
B的raw socket服务启动,监听所有发至端口55555和55556的类型为tcp的IP包
A向B的端口55555发出http请求(curl)
B通过raw socket嗅探到发至端口55555包,提取ip头部、tcp头部内容。
将目标IP修改成C的IP,源IP修改成B的IP,目标端口修改成C的服务端口,储存A的端口号,将源端口修改成55556
若该IP包含http请求,将
Host: B的IP:55555
修改成Host: C的IP:C的端口
重新生成IP头部校验和、TCP头部校验和,生成最终的IP包。
使用raw socket将篡改后IP报文发送给真实的http服务机器C。
B通过raw socket嗅探到发至端口55556包,提取ip头部、tcp头部内容。
将目标IP修改成A的IP,源IP修改成B的IP,目标端口修改成A的端口,源端口修改成55555,重复6操作,将IP包发至A。
重复以上步骤,经过几次IP包的转发后,A可以拿到C的返回内容。
实际代码见:
https://github.com/hongxuanlee/simple_raw_socket
(该案例由于使用了linux系统内置的ip、tcp库,所以不支持mac系统)
如何调试
tcpdump(命令行): https://danielmiessler.com/study/tcpdump/
wireshark(可视化应用): https://www.wireshark.org/
可能的坑
- 修改iptables
如果你的raw socket未绑定端口。linux 内置的TCP端口握手协议可能会优先你的嗅探器返回RST,解决方案是修改iptables,把RST的请求给禁掉。
sudo iptables -A OUTPUT -p tcp -m tcp --tcp-flags RST RST -j DROP
结束后想移除可以使用以下命令
sudo iptables -D OUTPUT -p tcp -m tcp --tcp-flags RST RST -j DROP