利用AF_PACKET 套接字發送一個任意的以太網幀
以太網是一個鏈路層協議。大多數網絡程序員關注網絡棧的傳輸層及以上,所以不需要直接處理以太網幀,但是某些場景下關注傳輸層以下也是有必要的。如:
1)實現網絡協議棧里面沒有內置的以太網協議類型
2)為測試目的,產生一個畸形或者其它非常規幀
假設你希望發送一個目的IP地址為192.168.0.83的ARP request報文。這個請求報文是以廣播mac地址從eth0口發出
方法概要
1. 選擇需要的以太網類型
2. 創建一個AF_PACKET套接字
3. 決定使用的以太網接口的索引值
4. 構造目的地址
5. 發送以太網幀
以下是使用到的頭文件
Header |
Used by |
errno |
|
memcpy, strerror, strlen |
|
in_addr_t, htons |
|
ETHER_ADDR_LEN, ETH_P_* |
|
struct ifreq |
|
struct ether_arp |
|
struct sockaddr_ll |
|
SIOCGIFINDEX, ioctl |
|
struct sockaddr, struct iovec, struct msghdr, AF_PACKET, SOCK_DGRAM, socket, sendto, sendmsg |
AF_PACKET是針對於Linux的。
利用AF_PACKET套接字發送廣播幀時,設置SO_BROADCAST不是必要的。有一些程序員一定設置,這個沒有啥壞處,並且可以看做是推薦的,因為這樣可以防止以后套接字行為上的任何變化。
1. 選擇需要的以太網類型
以太網幀中的以太網類型指定了它包含的負載的類型。有很多途徑可以獲取以太網類型:
1)頭文件Linux/if_ether.h 定義了大多數常用的以太網類型。包括以太網協議的ETH_P_IP(0x8000)、arp的ETH_P_ARP(0x0806)
和IEEE 802.1Q VLAN tags的ETH_P_8021Q(0x8100)
2)IEEE維護的注冊以太網類型列表
3)半官方的列表由IANA維護
ETH_P_ALL允許任何在沒有使用多個套接字的情況下接受所有以太網類型的報文。
0x88b5和0x88b6是保留以太網類型,供實驗或私人使用。
2. 創建一個AF_PACKET套接字
函數:
socket(domain, type, protocol)
1)domain為AF_PACKET.
2)套接字類型:
SOCK_DGRAM----以太網頭已經構造好了
SOCK_RAW------自己構造以太頭
3)協議類型
這里協議類型等同於前面提到的以太網類型(轉換成網絡字節序),用於過濾接收的報文
在這個列子中socket是發送arp報文,protocol用hton(ETH_P_ARP),type使用SOCK_DGRAM,這樣就不需要自己構造以太頭了。
點擊(此處)折疊或打開
- int fd;
- fd = socket(AF_PACKET,SOCK_DGRAM,hton(ETH_P_ARP))
- if(fd == -1) {
- perror("%s",strerror(errno));
- exit(1);
- }
3. 決定使用的以太網接口的索引值
點擊(此處)折疊或打開
- struct ifreq ifr;
- size_t if_name_len = strlen(if_name);
- if(if_name_len < sizeof(ifr.ifr_name)) {
- memcpy(ifr.ifr_name,if_name,if_name_len);
- }else {
- perror("interface name is too long");
- exit(1);
- }
- if(-1 == ioctl(fd,SIOCGIFINDEX,&ifr)) {
- perror("get if index error :%s",strerror(errno));
- exit(1);
- }
- int ifindx = ifr.ifr_ifindex;
4. 構造目的地址
為了使用AF_PACKET套接字發送一個幀時,其目的地址必須以sockaddr_ll的形式給出。
需要指定的域是sll_family,sll_addr,sll_halen,sll_ifindex和sll_protocol.其它的為0.
點擊(此處)折疊或打開
- struct sockaddr_ll{
- unsigned short sll_family; /* 總是 AF_PACKET */
- unsigned short sll_protocol; /* 物理層的協議 */
- int sll_ifindex; /* 接口號 */
- unsigned short sll_hatype; /* 報頭類型 */
- unsigned char sll_pkttype; /* 分組類型 */
- unsigned char sll_halen; /* 地址長度 */
- unsigned char sll_addr[8]; /* 物理層地址 */
- };
- const unsigned char ether_broadcast_addr[]={0xff,0xff,0xff,0xff,0xff,0xff};
- struct sockaddr_ll addr={0};
- addr.sll_family=AF_PACKET;
- addr.sll_ifindex=ifindex;
- addr.sll_halen=ETHER_ADDR_LEN;
- addr.sll_protocol=htons(ETH_P_ARP);
- memcpy(addr.sll_addr,ether_broadcast_addr,ETHER_ADDR_LEN);
(在寫這個文檔的時候,packet(7)的幫助文檔說只要提供sll_family,sll_addr,sll_halen和sll_ifindex就可以發送了,但是這個是錯誤的,
在打開套接字時候制定的以太網類型是用來過濾接收端報文的,而不是構造發送端報文)
筆者在實驗的時候也證實了這一點,如果沒有指定sll_protocol,報問將無法發送成功。在實際中只要指定如下兩項就可以:
addr.sll_ifindex = ifr.ifr_ifindex;
addr.sll_protocol = htons(ETH_P_ARP);
5. 發送以太網幀
原則上,幀可以通過使用任何具備寫文件描述符的函數發送,然而如果你選擇自動構造鏈路層頭的方式,那么要使用sendto或者sendmsg,以便目的地址可以被指定。這兩種方式中sendmsg更靈活,但是是以更復雜的接口為代價。下面是用每一個函數的具體實現
無論你選擇了哪個函數,每個函數調用將會導致一個單獨報文的發送。因為這個原因,你必須在將所有負載數據包含在一個報文中,或者使用使用sendmsg提供的scatter/gather功能。
在這個特定的場景中,發送的報文是一個ARP請求報文。為了完整性,這里給出了報文構造的一個例子:
點擊(此處)折疊或打開
- struct ether_arp req;
- req.arp_hrd = htons(ARPHRD_ETHER);
- req.arp_pro = htons(ETH_P_IP);
- req.arp_hln = ETHER_ADDR_LEN;
- req.arp_pln = sizeof(in_addr_t);
- req.arp_op = htons(ARPOP_REQUEST);
- memset(&req.arp_tha,0,sizeof(req.arp_tha));
- const char * target_ip_string = "1.1.1.1";
- struct in_addr target_ip_addr={0};
- if(!inet_aton(target_ip_string,&target_ip_addr)) {
- perror("%s is not a valid IP address",target_ip_string);
- exit(1);
- }
- memcpy(&req.arp_tpa,&target_ip_addr.s_addr,sizeof(req.arp_tpa));
5.1)利用ioctl的SIOCGTIFADDR獲取以太網接口的ip地址
點擊(此處)折疊或打開
- if (ioctl(fd,SIOCGIFADDR,&ifr)==-1) {
- perror("%s",strerror(errno));
- exit(1);
- }
- struct sockaddr_in* ipaddr = (struct sockaddr_in*)&ifr.ifr_addr;
- printf("IP address: %s\n",inet_ntoa(ipaddr->sin_addr));
5.2)利用ioctl的SIOCGTIFHWADDR獲取以太網接口的mac地址
網絡接口的硬件地址和格式取決於接口的所屬,所以不能假定它是一個以太網MAC地址。可以通過檢查sockaddr結構中的sa_family來決定。
如果是一個以太網接口,sa_family應該等於ARPHRD_ETHER.
點擊(此處)折疊或打開
- if (ioctl(fd,SIOCGIFHWADDR,&ifr)==-1) {
- perror("%s",strerror(errno));
- exit(1);
- }
- if(ifr.ifr_hwaddr.sa_family != ARPHRD_ETHER) {
- perror("not an Ethernet interface");
- exit(1);
- }
從ifreq結構中提取硬件地址
在檢查完接口類型以后,地址可以安全地從 req.ifr_hwaddr.sa_data中提取出來。sa_data的類型是char,有符號字符型。
所以如果你希望解析它的時候應該先轉換成無符號類型。一個粗魯而直接的方法是通過(unsigned char*)進行強制轉換。
點擊(此處)折疊或打開
- const unsigned char * mac = (unsigned char*)ifr.ifr_hwaddr.sa_data;
- printf("%02x:%02x:%02x:%02x:%02x:%02x\n",mac[0],mac[1],mac[2],mac[3],mac[4],mac[5])
5.3) 使用sendto發送幀
要調用sendto,你必須要提供幀的內容和遠端地址。
點擊(此處)折疊或打開
- If(sendto(fd,&req,sizeof(req),0,(struct sockaddr*)&addr,sizeof(addr)) == -1) {
- perror(“%s”,strerror(errno));
- exit(1);
- }
Sento 函數的第四個參數是改變sendto行為的指定標記,這個例子中不需要使用。
Sendto的返回值是發送的字節數,或者如果出錯返回-1. AF_PACKET幀是自動發送的,所以不像寫TCP套接字時需要循環調用發送函數來發送分開發送的數據。
5.4) 使用sendmsg發送幀
調用sendmsg時,除了數據包內容和遠端地址外,你還必須構造一個iovec數組和一個msghdr結構
點擊(此處)折疊或打開
- Struct iovec iov[1];
- Iov[0].iov_base=req;
- Iov[0].iov_len=sizeof(req);
- Struct msghdr message;
- Message.msg_name =&addr;
- Message.msg_namelen=sizeof(addr);
- Message.msg_iov=iov;
- Message.msg_iovlen=1;
- Message.msg_control=0;
- Message.msg_controllen=0;
- If(sendmsg(fd,&message,0) == -1) {
- Perror(“%s”,strerror(errno));
- }
Iovec數組的目的是提供scatter/gather能力以便報文內容不需要存儲在一個連續的內存區域。在這個例子里整個負載都是存儲在一個buffer中,因此只需要一個數組元素。
Msghdr結構的存在使得recvmsg和sendmsg的參數個數下降到可管理的數目。在進入Sendmsg時,msghdr指定了目的地址,報文內容和輔助數據。