1 前言
首先需要回答一个问题,为什么要学习实现用户态协议栈,从技术角度分析,主要是由于用户态的网络协议栈更高效,第二个是用户态协议栈可以实现定制。更高效主要是针对,网卡数据拷贝到协议栈这部分,如果采用用户态协议栈就可以进行一个零拷贝的过程,即利用mmap技术完成。这里实现UDP用户态协议主要是对于协议栈的理解的加深,了解内核协议栈的工作原理。
2 网络协议格式
2.1 以太网协议
以太网协议分为三个部分,协议头,数据和CRC校验。源地址和目的地址是指网卡的硬件地址(也叫MAC地址),类型主要包括IP协议,ARP协议和RARP协议。协议定义来自于RFC894,具体如下图。
头结构定义:
#define ETH_HDR_LEN 6
#define IP_PROTO 0x0800
#define ARP_PROTO 0x0806
#define RARP_PROTO 0x0835
typedef struct _eth_hdr {
unsigned char src_mac[ETH_HDR_LEN];
unsigned char dst_mac[ETH_HDR_LEN];
unsigned short proto;
}eth_hdr;
2.2 IP协议
IP协议比较复杂,包括内容比较多,主要是定义了源ip地址和目的ip地址。默认是20字节的头部长度,当存在选项时头部长度将进行扩展。具体定义见RFC791:
头结构定义:
typedef struct _ip_header
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned char hdr_len:4,
version:4;
#else
unsigned char version:4,
hdr_len:4;
#endif
unsigned char tos_type;
unsigned short pkt_len;
unsigned short mark;
unsigned short flag_offset;
unsigned char ttl;
unsigned char proto;
unsigned short hdr_checksum;
unsigned int src_ip;
unsigned int dst_ip;
unsigned int opt[0];
}ip_header;
2.3 UDP协议
udp协议是定义最为简单的协议,协议基本不提供什么保障,主要定义了源端口和目的端口,也使得UDP在用户层可以进行更为灵活的设计。详细见RFC768:
#define PROTO_UDP 17
typedef struct _udp_header
{
unsigned short src_port;
unsigned short dst_port;
unsigned short length;
unsigned short checksum;
}udp_header;
typedef struct _udp_pkt
{
eth_header eth;
ip_header ip;
udp_header udp;
unsigned char data[0];
}udp_pkt;
2.4 ARP协议
ARP协议的作用是通过IP地址获取MAC地址,主要工作流程是ARP进程在本局域网上广播发送一个ARP请求分组,主要内容是A自己的IP地址、MAC地址,询问的IP地址。然后被询问对象既可以回复自身的ip和mac地址,建立一个arp表,后面可以直接查表。具体见RFC 826:
typedef struct _arp_header
{
unsigned short hw_type;
unsigned short proto;
unsigned char hw_len;
unsigned char addr_len;
unsigned short op;
unsigned short src_mac[ETH_MAC_LEN];
unsigned int src_ip;
unsigned short dst_mac[ETH_MAC_LEN];
unsigned int dst_ip;
}arp_header;
typedef struct _arp_pkt
{
eth_header eth;
arp_header arp;
unsigned char data[0];
}arp_pkt;
注意:
这里存在一个arp攻击问题,就是对于arp欺骗,利用arp广播的原理,然后发送错误的ip和mac个对方造成arp表错乱整个网络无法使用。
2.5 ICMP协议
ICMP协议是一种面向无连接的协议,用于传输出错报告控制信息。具体见RFC792:
结构定义:
typedef struct _icmp_header
{
unsigned char type;
unsigned char code;
unsigned short checksum;
unsigned short identifier;
unsigned short seq;
unsigned char data[32];
}icmp_header;
typedef struct _icmp_pkt
{
eth_header eth;
ip_header ip;
icmp_header icmp;
unsigned char data[0];
}icmp_pkt;
3 UDP用户协议栈实现
利用netmap实现一个简单的UDP协议的解析过程,用到netmap接口:nm_open打开网卡;nm_nextpkt获取网络数据;nm_inject发送网络数据。利用poll进行网络数据接收状态的获取,根据协议一层一层的强转,最终得到用户数据,数据发送也是一层层封装的过程。整体比较简单。网络编程需要考虑网络字节序和用户字节序,后续完善ARP查表和ICMP ping功能的实现。
#include<stdio.h>
#include <poll.h>
#include <arpa/inet.h>
#define NETMAP_WITH_LIBS
#include <net/netmap_user.h>
#pragma pack(1)
int main()
{
eth_header *eth = NULL;
struct pollfd pfd = {0};
struct nm_pkthdr nmh;
unsigned char *buffer = NULL;
//netmap:打开eth0网卡
struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
if (nmr == NULL)
{
return -1;
}
pfd.fd = nmr->fd;
pfd.events = POLLIN;
while(1)
{
int ret = poll(&pfd, 1, -1);
if (ret < 0) continue;
if (pfd.revents & POLLIN)
{
buffer = nm_nextpkt(nmr, &nmh);
eth = (eth_header *)buffer;
if(ntohs(eth->proto) == PROTO_IP)
{
udp_pkt *udp = (udp_pkt *)buffer;
if(udp->ip.proto == PROTO_UDP)
{
struct in_addr addr;
addr.s_addr = udp->ip.src_ip;
int udp_length = ntohs(udp->udp.length);
printf("%s:%d:length:%d, ip_len:%d --> ", inet_ntoa(addr), udp->udp.src_port,
udp_length, ntohs(udp->ip.pkt_len));
udp->data[udp_length-8] = '\0';
printf("udp buffer: %s\n", udp->data);
udp_pkt udp_rt;
echo_udp_pkt(udp, &udp_rt);
nm_inject(nmr, &udp_rt, sizeof(udp_pkt));
}
}
}
}
return 0;
}
注意点:
结构体会进行对齐,造成最后数据包的大小计算错误,这里需要指定#pragma pack(1)