Linux下使用原始socket收发数据包
在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);
}