最近看了下udhcp的源代码,其中会根据LISTEN_MODE建立不同的socket进行通信,一个是普通的传输层UDP套接字,另外一个是链路层的套接字,由于本人才疏学浅,所以在网上搜罗了一下有关链路层套接字的东东,在此记录一下。

  链路层套接字也叫原始套接字(raw packet),可以接收网卡上的数据帧,换句话说是直接从网卡上拿数据,可以今夕流量统计和分析,socket的建立有一下几种:

  socket(AF_INET, SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP)

  发送和接收IP数据包,该套接字可以接收协议类型为(tcp udp icmp等)发往本机的ip数据包,不能收到非发往本地ip的数据包(ip软过滤会丢弃这些不是发往本机ip的数据包),不能收到从本机发送出去的数据包,发送的话需要自己组织tcp udp icmp等头部,可以setsockopt来自己包装ip头部,这种套接字用来写个ping程序比较适合

  socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))

  发送和接收以太网数据帧,接收发往本地mac的数据帧,接收从本机发送出去的数据帧(第3个参数需要设置为ETH_P_ALL),接收非发往本地mac的数据帧(网卡需要设置为promisc混杂模式)。


  ETH_P_IP 0x800 只接收发往本机mac的ip类型的数据帧


  ETH_P_ARP 0x806 只接受发往本机mac的arp类型的数据帧


  ETH_P_RARP 0x8035 只接受发往本机mac的rarp类型的数据帧


  ETH_P_ALL 0x3 接收发往本机mac的所有类型ip arp rarp的数据帧, 接收从本机发出的所有类型的数据帧。(混杂模式下,会接收到非发往本地mac的数据帧)


  发送的时候需要自己组装以太网数据帧,所有相关的地址使用struct sockaddr_ll 而不是struct sockaddr_in(因为协议簇是PF_PACKET不是AF_INET了),比如发送给某个机器,对方的地址需要使用struct sockaddr_ll。


  socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))

  发送接收以太网数据帧(不包括以太网头部)。与socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))功能类似,但是不包括以太网头部,与第一种的区别是,他可以收取非IP协议的数据包

  1.原始套接字的一般流程:当收到一个数据包时,(1)网卡首先对其进行硬过滤,(根据网卡的不同模式会有不同的动作,例如网卡如果设置为混杂模式的话,则对数据帧不做任何处理,直接交给下一层。否则对发往非本机的MAC或者广播MAC进行过滤)。如果成功通过则会进入IP层,在进入IP层之前系统会检查系统中是否有通过socket(AF_PACKET, SOCK_RAW, ..)创建的套接字。如果有的话并且协议相符,在这个例子中就是需要ETH_P_IP或者ETH_P_ALL类型,系统就给每个这样的socket接收缓冲区发送一个数据帧拷贝,然后进入再IP层。(2)进入了IP层(ip层会对该数据包进行软过滤,就是检查校验或者丢弃非本机IP或者广播IP的数据包等),如果成功通过的话会进入UDP层。但是在进入UDP层之前,系统会检查系统中是否有通过socket(AF_INET, SOCK_RAW, ..)创建的套接字。如果有的话并且协议相符,如上面的IPPROTO_UDP类型。系统就给每个这样的socket接收缓冲区发送一个数据帧拷贝,然后进入下一步。

===============================================================

链路层原始套接字编程

1、链路层原始套接字创建方法:socket(PF_PACKET, SOCK_RAW, htons(protocol)),其中protocal参数为关心的协议类型。
2、将网卡设置混杂模式,使网卡将收到的所有包(包括组播和广播)都转发给操作系统。代码如下:
    struct ifreq    ifr;
    strcpy(ifr.ifr_name, if_name);
    ioctl(fd, SIOCGIFFLAGS, &ifr);
    ifr.ifr_flags |= IFF_PROMISC;
    ioctl(fd, SIOCSIFFLAGS, &ifr);
3、对于多网卡系统,操作系统在收包时不区分是从哪个网卡收到的,统一转发给用户进程socket,特别的,当用户进程创建了原始套接字socket,那么操作系统在转发消息时,将从网卡收到的buf复制给所有的、关心的原始套接字。
4、可通过bind函数将创建的原始套接字绑定到指定的addr,addr的实际类型为struct sockaddr_ll,绑定时需要设置sll_family,sll_protocol,sll_ifindex这几个参数。其中,sll_ifindex为指定的接口名称的索引,可通过ioctl函数获取ioctl(fd, SIOCGIFINDEX, &ifr);

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
   struct sockaddr_ll{
     unsigned short int sll_family;
     unsigned short int sll_protocol;
     int sll_ifindex;
     unsigned short int sll_hatype;
     unsigned char sll_pkttype;
     unsigned char sll_halen;
     unsigned char sll_addr[8];
   };

包括以太网头和协议内容。其中dest_addr参数必须设置,与应用层sendto函数指定目的地址不同,链路层发送时需要指定将buf从本机的哪个网卡发送出去。同样,实际类型为struct sockaddr_ll,只需要设置sll_ifindex参数即可。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
6、recvfrom函数在接收数据时,如果本机的任意一个网卡设置了混杂模式,那么这个函数都能收到链路层包,除非sockfd采用bind函数绑定了网卡。如果绑定的网卡设置了混杂模式,则只能收到发往本网卡包(包括组播包和广播包等)。其中,src_addr为发送包的源地址。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

7、应用层socket通过(源IP、源端口、目的IP、目的端口)四元组来确定socket对应的进程,操作系统在转发包时能够确定唯一的进程ID;而原始套接字socket没有端口的概念,所以只能通过(源IP、目的IP)或(源MAC、目的MAC)二元组来区分不同的进程。ping程序通过在协议字段里添加进程ID来区分;

-----------------------------------------------------------------------------------------------------------------

SOCK_RAW类型的套接字在发送的时候需要自己加上一个MAC头部(其类型定义在linux/if_ether.h中,ethhdr),另一种是SOCK_DGRAM类型,它是已经进行了MAC层头部处理的,即收上的帧已经去掉了头部,而发送时也无须用户添加头部字段。
Sockaddr_ll结构如下:
接口的sll_ifindex值可以通过ioctl获得,如下面是获得名字为“eth0”的接口的索引号
strcpy(ifr.ifr_name,'eth0');
ioctl(fd_packet,SIOCGIFINDEX,&ifr);
取得的值保存在ifr结构体的ifr_ifindex中,ifr结构类型为“struct ifreq”
要获得接口的物理地址同样使用ioctl可以得到
ioctl(fd_packet,SIOCGIFHWADDR,&ifr);
以数据形式保存在ifr的ifr_hwaddr.sa_data中。