linux自身有两种从数据链路层接收分组:
一种为fd=socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ALL));
另一种为fd=socket(AF_INET,SOCK_PACKET,htons(ETH_P_ALL));
这其中协议族为PF_PACKET套接字使用较多。
ETH_P_ALL自身定义于 /usr/include/linux/if_ether.h中,
#define ETH_P_ALL 0x0003
ETH_P_ALL占两个字节值为0x0003
其他的:
#define ETH_P_LOOP 0x0060 /* Ethernet Loopback packet */ #define ETH_P_PUP 0x0200 /* Xerox PUP packet */ #define ETH_P_PUPAT 0x0201 /* Xerox PUP Addr Trans packet */ #define ETH_P_IP 0x0800 /* Internet Protocol packet */ #define ETH_P_X25 0x0805 /* CCITT X.25 */ #define ETH_P_ARP 0x0806 /* Address Resolution packet */ #define ETH_P_BPQ 0x08FF /* G8BPQ AX.25 Ethernet Packet [ NOT AN #define ETH_P_IEEEPUP 0x0a00 /* Xerox IEEE802.3 PUP packet */ #define ETH_P_IEEEPUPAT 0x0a01 /* Xerox IEEE802.3 PUP Addr Trans packet*/
以太网封装中,数据链路帧中类型字段为0800指示的数据为ip报文,0806为ARP报文等等。
在socket函数中,若指定第三个参数为ETH_P_ARP则指示只将接收的帧中的哪些类型的帧传递给所创建的套接字。
知道了这些,那么我们可以接收一些if_ether.h里并没有定义的类型的数据链路帧
比如:
socket(PF_PACKET, SOCK_RAW, htons(0x88CC));
这个就是向套接字传递数据链路报文数据类型为88CC的报文,0x88cc报文为LLDP报文,if_ether.h并没有定义。我们使用recvfrom就能够获取到该类型的数据链路帧。
而使用sendto函数可以发送由我们自己定义的数据链路帧报头(这里可以将类型字段设置为0x88cc)。
想想内核将接收的报文传递给原始套接字两个的限制条件:
1)一是TCP/UDP分组绝不会传递给Raw socket
二是大多数ICMP在内核处理完后,传递给Raw socket
三是所有的IGMP在内核处理完后,传递给Raw socket
四是内核不认识其协议字段的IP数据报文传递给Raw socket
五是某个数据以分片的形式到达,则在所有分片到达重组之后,才传递给Raw socket
若以上5个条件满足,那么下面的3个条件也是必需要满足:
2)一是创建原始套接字时,协议字段非0,则接收的数据报文协议字段,必需匹配该值,否则,不传递该报文
二是bind了本地IP地址,数据报的目的地址必需和本地IP地址匹配
三是connect某个外地IP地址,那么则数据报源地址也必须匹配。
也就是说原始套接字实际上是不能够接收tcp/udp分组的,只能使用libpcap库来接收。但是libpcap库的一点限制就是只能接收不能发。
如以下实例发送以太网数据链路帧,指定上层报文类型为0x88cc
/* author:gs<[email protected]> time:2013/9/23 function:send L2 packets */ #include<sys/types.h> #include<sys/socket.h> #include<stdlib.h> #include<string.h> #include<stdint.h>/*for uint16_t*/ #include<stdio.h> #include<net/if.h> #include<linux/if_packet.h> struct lldpdu { char dst[6]; char src[6]; uint16_t type; }; int main(int argc,char* argv[]) { struct lldpdu* lpacket = malloc(sizeof(struct lldpdu)); memset(lpacket,0,sizeof(struct lldpdu)); struct sockaddr_ll sll; memset(&sll,0,sizeof(struct sockaddr_ll)); int sk = socket(PF_PACKET, SOCK_RAW, htons(0x88CC));/*使用pf_packet接口创建套接字*/ sll.sll_family = PF_PACKET; sll.sll_ifindex = if_nametoindex("eth0");/*sll虽然是用来指定目的地址的,但是在这个结构体中sll_ifindex 却指定的是本机发送报文的接口索引*/ sll.sll_protocol = htons(0x88CC); sll.sll_addr[0] = 0x34;/*sll_addr指定目的MAC地址*/ sll.sll_addr[1] = 0xb0; sll.sll_addr[2] = 0x52; sll.sll_addr[3] = 0xda; sll.sll_addr[4] = 0xda; sll.sll_addr[5] = 0x18; lpacket->dst[0] = 0x34;/*自己构造的L2 报文的目的地址*/ lpacket->dst[1] = 0xb0; lpacket->dst[2] = 0x52; lpacket->dst[3] = 0xda; lpacket->dst[4] = 0xda; lpacket->dst[5] = 0x18; lpacket->src[0] = 0x00;/*自己构造的L2 报文的源地址*/ lpacket->src[1] = 0x0c; lpacket->src[2] = 0x29; lpacket->src[3] = 0x8d; lpacket->src[4] = 0xf1; lpacket->src[5] = 0x04; lpacket->type = htons(0x88cc);/*报文类型*/ while(1) { /*只有L2头,数据为空。每隔3秒发送0X88CC报文,即LLDP报文*/ sendto(sk, lpacket, 14, 0, (struct sockaddr*)&sll, sizeof(struct sockaddr_ll)); printf("send one lldp packet\n"); sleep(3); } return 0; }
以上程序发送的LLDP报文,能够在wireshark中捕捉到。
发现一个奇怪的现象,当sll中指定的目的地址和报文中的目的地址不一致时,会出现什么呢?
经实际测试,发现这种情况下,wireshark捕捉的报文以报文中的目的地址为准。而实际上,sll中不指定目的的MAC地址,报文也能被发送到正确的目的地去。它会使用报文的前6个字节,当做目的地址。