在Linux系統中,使用非原始的socket,可以收發TCP或者UDP等網絡層數據包。如果要處理網絡層以下的數據包,比如ICMP、ARP等,或者更底層,比如鏈路層數據包,就得使用原始socket了。
創建socket
創建socket要使用socket函數,socket函數的原型為:
#include <sys/types.h>
#include <sys/socket.h> int socket(int domain, int type, int protocol);
其中,domain比較常用的值有:
AF_UNIX
Unix本地通訊AF_INET
IPv4網絡協議AF_INET6
IPv6網絡協議AF_NETLINK
內核用戶界面設備AF_PACKET
底層包設備
我們要處理網絡層以下的數據包,就得使用AF_PACKET
。
type比較常用的值有:
SOCK_STREAM
用于面向字節流的協議,如TCPSOCK_DGRAM
用于面向數據報的協議,如UDPSOCK_RAW
用于非處理的原始數據包
我們這里要使用SOCK_RAW
。
protocol是要使用的網絡協議。
如下代碼,創建一個原始socket:
#include <sys/types.h>
#include <sys/socket.h>int raw_socket_new () {int fd;fd = socket (PF_PACKET, SOCK_RAW, IPPROTO_RAW);if (fd < 0){fprintf (stderr, "socket error: %s\n", strerror (errno));}return fd;
綁定網口
如果我們處理非原始的數據包,可以使用網絡層綁定IP,或者連接IP(其實socket也會自動綁定IP),協議棧會自動根據IP把數據包發往特定的網絡接口,不需要綁定網口。
我們通過原始socket發送底層數據包,就需要綁定網口了。
綁定網口的方法很簡單,先使用if_nametoindex
函數,找到對應網口名的編號,之后設置到struct sockaddr_ll
結構中,調用bind函數就行了。
`if_nametoindex`的原型為:
#include <net/if.h>unsigned int if_nametoindex(const char *ifname);
struct sockaddr_ll
的定義為:
#include <linux/if_packet.h>struct sockaddr_ll { unsigned short sll_family; __be16 sll_protocol; int sll_ifindex; unsigned short sll_hatype; unsigned char sll_pkttype; unsigned char sll_halen; unsigned char sll_addr[8];
};
以下函數,就把給定的網絡接口,綁定到了給定的socket上:
#include <net/if.h>
#include <linux/if_packet.h>int raw_socket_bind(int fd, const char *eth) {struct sockaddr_ll sa;memset (&sa, 0, sizeof (struct sockaddr_ll));// 原始數據包sa.sll_family = AF_PACKET;// 自定義二層協議sa.sll_protocol = htons (CUSTOM_TYPE);sa.sll_ifindex = if_nametoindex (eth);if (bind (jdpdk->fd, (struct sockaddr *)&sa, sizeof (struct sockaddr_ll))!= 0){fprintf (stderr, "bind error: %s\n", strerror (errno));return -1;}return 0;
}
二層包結構
socket創建成功,綁定了網口,就可以發送自定義的二層數據包了。
根據TCP/IP標準,二層的數據包包頭為14個字節,即6字節的目標MAC地址、6字節的源MAC地址再加2字節的包類型。
在net/ethernet.h
中,結構定義為:
struct ether_header
{uint8_t ether_dhost[ETH_ALEN]; uint8_t ether_shost[ETH_ALEN];uint16_t ether_type;
} __attribute__ ((__packed__));
在我們使用網口發送的時候,源MAC地址可以設成全0,但是目標MAC地址,必須設置。
我們可以實現一個組包函數,把給定的數據,設上預訂的目標地址:
int raw_encode (const char *dst_mac, unsigned short type, const char *data, size_t data_len, char *packet, size_t packet_len) {assert (14 + data_len <= packet_len);memcpy (packet, dst_mac, 6);memset (packet + 6, 0, 6);* (unsigned short *) (packet + 12) = htons (type);memcpy (packet + 14, data, data_len);return 14 + data_len;
}
發送
發送函數就比較簡單,跟網絡層的發送沒有區別。
int raw_send (int fd, const char *data, int data_len) {int ret;ret = send (fd, data, data_len, 0); if (ret != data_len) { fprintf (stderr, "send error: %s\n", strerror (errno)); }return ret;
}
接收
接收函數也同樣,只是記得接收成功以后,偏移14字節(即二層包頭以后)才是我們的應用層數據:
int raw_recv (int fd, char *packet, size_t packet_len) {int ret;ret = recv (fd, packet, packet_len, 0);return ret;
}
原始socket示例:arp組包
根據TCP/IP協議,當我們網絡層使用TCP、UDP或者ICMP等使用IP地址作為目標的數據包時,協議棧要查詢本機的arp表,找到IP地址對應的MAC地址。
如果查詢失敗,就會發送arp請求,通過arp響應來得到目標MAC地址。
所以,arp協議有兩種數據包:一種是arp請求,一種是arp響應。對應到arp結構中,就是操作碼的值為1或者2。
在linux/if_arp.h
中,有arp包的包頭結構:
struct arphdr { __be16 ar_hrd;__be16 ar_pro;unsigned char ar_hln;unsigned char ar_pln;__be16 ar_op;
};
在arp的包體中,分別是發送MAC、發送IP、目標MAC與目標IP。
- 當arp請求的時候,設置arp包頭中的操作碼為1,另外設置發送MAC(即本機MAC)與目標IP,發送IP與目標MAC置空。
當arp響應的時候,設置arp包頭中的操作碼為2,另外設置發送MAC(即本機MAC)、發送IP(即本機IP)、目標MAC(即請求時的發送MAC),目標IP置空。
我們寫一個組包函數:
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <netinet/if_ether.h>
#include <memory.h>void
buf_encode_arp (uint8_t *buf, uint16_t opcode, const uint8_t *source_mac, const uint8_t *dst_mac, unsigned long sip, unsigned long dip)
{struct ether_header *hdr; hdr = (struct ether_header *)buf; if (source_mac) { memcpy (hdr->ether_shost, source_mac, sizeof hdr->ether_shost);}// arp請求的時候,二層目標MAC地址填全0xFF,否則填全0memset (hdr->ether_dhost, opcode == 1 ? 0xFF : 0, sizeof hdr->ether_dhost);if (dst_mac) { memcpy (hdr->ether_dhost, dst_mac, sizeof hdr->ether_dhost);}hdr->ether_type = htons (ETHERTYPE_ARP);struct ether_arp *arp = (struct ether_arp *)(hdr + 1);// 設置arp包頭arp->ea_hdr.ar_hrd = htons (1);arp->ea_hdr.ar_pro = htons (0x800);arp->ea_hdr.ar_hln = 6;arp->ea_hdr.ar_pln = sizeof (uint32_t);arp->ea_hdr.ar_op = htons (opcode);// 根據輸入參數,設置arp包體memset (arp->arp_sha, 0, sizeof arp->arp_sha);if (source_mac) { memcpy (arp->arp_sha, source_mac, sizeof arp->arp_sha);} memset (arp->arp_tha, 0, sizeof arp->arp_tha);if (dst_mac) { memcpy (arp->arp_tha, dst_mac, sizeof arp->arp_tha);}memcpy (&arp->arp_spa, &sip, 4);memcpy (&arp->arp_tpa, &dip, 4);
}