网络通信之原始套接字

                                                                                                                                fanxiushu 2017-04-11
我们在做网络编程时候,大都使用的是TCP或者UDP协议的套接字,下边这样的代码就创建了一个TCP或UDP的套接字
int fd = socket(AF_INET, SOCK_STREAM,0);  /// TCP
int fd = socket(AF_NET, SOCK_DGRAM, 0 );  ///  UDP
使用recv或recvfrom接收网络对端数据,使用send或sendto发送数据到网络对端,
这样我们就能在网络的世界里畅游了。
作为更上层的编程者而言,甚至都可以不用知道套接字的存在,
五花八门的编程语言和通讯函数库对套接字接口层层封装,构建成他们自己的一套通讯API函数供给开发者使用。
但无论怎样,进入到通讯协议层都是TCP/IP构成的基础协议世界,
进入到链路层,都是以太网卡和少部分其他类型网卡,以及各种路由器交换机构造的硬件通讯世界。
应用层操作TCP/IP协议的接口就是套接字,以BSD socket作为事实上的标准。
一般情况下我们做通讯,也不大关心TCP/IP等底层的协议交互,因此我们使用TCP,UDP套接字已经足够了,
但是有时,我们需要监控网络,分析各种数据包,或者发送自定义的数据包到网络,
这个时候,就需要另外一种套接字既原始套接字。

原始套接字的创建方法也简单
socket(AF_INET,SOCK_RAE, protocol);
protocol可以是 IPPROTO_IP, IPPROTO_TCP, IPPROTO_ICMP等多种协议,用于抓取或者发送各种协议的数据包。
比如 socket(AF_INET,SOCK_RAW, IPPROTO_TCP); 设置 IPPROTO_TCP协议,
我们就可以利用 recvfrom函数,收到  IP Header + TCP Header + User Data 的报文, 
利用sendto可以发送 TCP + Header + User Data 的报文,
如果调用 setsockopt 设置
IP_HDRINCL 选项, 表示我们发送报文的时候,需要自己构造IP Header,
这样sendto 需要发送  IP Header + TCP Header + User Data 。

一般在TCP建立连接之后,如果我们插一杠子,随意发送一些数据包,除了扰乱TCP处理链,遭到丢弃之外,也不会带来什么效果。
但是我们可以给TCP连接发送一些特殊包,让他达到一些特殊效果。

首先看看TCP头的定义:
struct tcp_t
{
    unsigned short src_port;
    unsigned short dst_port;
    unsigned int   seq_no;
    unsigned int   ack_no;
    unsigned char  hdr_lenoff;
#define TCP_FIN  0x01
#define TCP_SYN  0x02
#define TCP_RST  0x04
#define TCP_PUSH 0x08
#define TCP_ACK  0x10
#define TCP_URG  0x20
    unsigned char  flags;
    unsigned short window_size;
    unsigned short checksum;
    unsigned short urgn_ptr;
};

重点关注tcp_t结构里边的flags标识字段,可以取 TCP_RST,TCP_SYN等标志,
如果我们给已经建立了连接的TCP,发送一个RST数据包,连接将立即遭到断开,
调用sendto发送的时候,需要开启
IP_HDRINCL 选项,因为我们除了需要知道端口之外,还需填写目标IP地址,
 这是一个很有意思的应用。
 
比如在主干网采用旁路方式挂载一个防火墙,这样既不影响主干网的传输,
在采集分析到某些用户不该访问的TCP地址,则直接发送RST数据包,让TCP连接断掉,
这个RST包一般是朝用户端IP发送或者同时朝两端发送。
而在用户端的表现效果,比如用户用浏览器访问了一个不该访问的网站,经常会出现连接被重置的错误,
可能一会儿连接得上网站,可是一会儿又立马断开,这个就是这种类型的防火墙在捣乱。

同样,我们可以利用TCP_SYN发起SYN洪水攻击,也就是常说的DDOS攻击,
TCP连接建立分三个过程,
客户端调用connect,在connect函数里要完成三次握手过程,
客户端首先发送标志是 SYN 的TCP数据包,服务端接收到这个数据包,回复 SYN + ACK 数据包,
客户端接到SYN+ACK回复之后,再回复 ACK数据包,就这样TCP连接建立完成。
如果我们利用原始套接字不停只发送 SYN 数据包,服务端将有大量的资源用于发送 SYN+ACK和等待ACK回复,
如果发送SYN包的客户端足够强大,发起攻击的机器足够多,是会造成服务端假死。
DDOS攻击不单包括大量无效的SYN,还可以是其他一些无效大量的数据包,比如ICMP,UDP数据包等。
表现形式就是大量的客户端机器,成千上万的客户端机器,大量的构造无效数据包,
对某个服务端IP不停的发包
,造成服务端带宽被占光,或者服务器本身来不及处理造成假死。
如果无效数据包遭到防火墙阻断,可以使用成千上万的机器采用正常的connect方式不停的疯狂的连接断开,也能造成同样的效果。

也没有什么有效措施防止这种攻击,除了不断增加出口带宽或增加服务器硬件的处理能力。

以上说的原始套接字,都只能抓取到IP层的数据,而不能抓取链路层的数据包,一个完整的以太网的链路层的数据包形式如下
MAC Header  +  IP Header  +  TCP(UDP or Other) Header  +  User Data 。
到SOCK_RAW原始套接字层, MAC Header 已经被系统去除了,因此也无法获取 ARP、RARP这些链路层的数据包。

我们抓取完整链路层数据包做什么用呢?
可以分析所以从网卡接收或发送的数据包,从而查看是否存在流量异常,或者分析各种协议。
比如winpcap库,就是能抓取链路层数据包的库,以及由她衍生出的一些产品,如ethereal,wireshark等抓包分析软件。
我们也可以利用链路层,构造一些自己的数据包让网卡帮我们传输自己定义的数据包。

当然还有一个用途,
结合上一篇介绍的 “windows虚拟网卡驱动开发”
链接地址: http://blog.csdn.net/fanxiushu/article/details/69415282
可以让真实的局域网络和虚拟网卡组成一个虚实不分的混合网络,

很早的一篇文章
http://blog.csdn.net/fanxiushu/article/details/8526719 (桥接虚拟局域网和真实局域网到同一网段原理)
介绍过类似的原理。

简单的说,如果我们能在某个局域网的某台机器上抓取到链路层的数据包,把这数据包通过服务器转发等方式提交给虚拟网卡,
虚拟网卡产生的链路层数据包也通过服务器转发等方式提交给这台机器,这台机器再把这个数据包发送给局域网。
这样就把虚拟网卡和真实的局域网混合到一块了。

很庆幸的是,linux平台提供了 PF_PACKET协议族来实现链路层的数据包的抓取和写入。
而windows平台则没有这么幸运了,需要开发NDIS协议层驱动程序,才能完成如此任务。

这里只简单介绍linux平台下链路层数据包的抓取过程,其实也是挺简单的,比在windows平台实现NDIS协议驱动简单多了。

PF_PACKET协议族也是采用套接字形式
socket(PF_PACKET, type, protocol);
接收和发送分别使用 recvfrom 和 sendto 函数,
地址也不再是 sockaddr_in 而是采用 sockaddr_ll 地址。
其中type取值 SOCK_RAW 或则 SOCK_DGRAM
SOCK_RAW, 就是接收到原汁原味的链路层数据包,包括 MAC Header + IP Header + TCP(UDP) Header + User Data 。
                        发送的时候也必须发送原汁原味的链路层数据包。
SOCK_DGRAM,会把MAC Header去掉,这个就跟 使用原始套接字很类似了,发送的时候也只需发送到IP头即可,
                           MAC头会根据发送时候填写的 sockaddr_ll地址由系统自动填写MAC头。

protocol 包括  ETH_P_IP, 值     0x0800 ,抓取 IP数据包
                       ETH_P_ARP,   0x0806,抓取 ARP数据包
                       ETH_P_RARP, 0x0835, 抓取RARP数据包
以上的值刚好是以太网的MAC头部里边的协议类型值,这不是巧合。
因此我们可以利用以太网发送我们自己的数据包,比如在MAC Header定义一个 0x4F4F 的数据包,
这个时候 protocol = 0X4F4F, 就能使用recvfrom和sendto 接收和发送我们自己定义的数据包了。
还有一个 ETH_P_ALL, 这个代表可以抓取链路层的所有数据包,同时包括从主机发到网卡的数据包。

之后这样调用
socket(PF_PACKeT, SOCK_RAW, htons(ETH_P_ALL);
然后开一个线程循环执行 recvfrom 就能接收到所有数据包,对这些数据进行分析处理,
然后决定是否可以通过服务器转发给虚拟网卡。
等从虚拟网卡接收到链路层的数据包,再通过 sendto函数写入。

这样就把虚拟网卡和局域网混合到一起,让他们在一个锅里搅合了。



你可能感兴趣的:(TCP,C,C++)