1、获取以太网数据
自定义协议栈,需要获取原始的以太网数据,获取方式有:
- raw socket 原始套接字
- 实现一个网卡驱动 driver
- 旁路:netmap dpdk
- hook 机制:bpf, ebpf
这里以 netmap 为例。
1.1、netmap 原理
netmap 采用 mmap 的方式,将网卡驱动的 ring 内存空间映射到用户空间。这样用户态可以直接操作内存,获取原始的数据,避免了内核和用户态的两次拷贝(网卡 -> 内核协议栈 -> 内存)
1.2、netmap 环境搭建
安装 netmap
# 安装 netmap git clone https://github.com/luigirizzo/netmap.git cd netmap/LINUX ./configure make && make install # 将头文件拷贝到 /usr/include/net cd ./netmap/sys/net/ # netmap 头文件位置 cp * /usr/include/net
启动 netmap
# 开启 netmap insmod netmap.ko ls /dev/netmap -l # 关闭 netmap rmmod netmap.ko
2、udp 协议栈的实现
2.1、以太网帧
// 以太网数据帧头,字节对齐: sizeof = 16 struct ethhdr { unsigned char dmac[ETH_ADDR_LENGTH]; // 目的mac地址 unsigned char smac[ETH_ADDR_LENGTH]; // 源mac地址 unsigned short protocol; // 协议:上层协议的类型,ip:0x0800 };
2.2、ip 协议
// ip 数据包首部 struct iphdr { unsigned char version:4, // ip协议版本,IPv4:0100 hdrlen:4; // 首部长度,* 4 unsigned char tos; // 服务类型 unsigned short totlen; // 总长度,* 1,最大65535字节,超过MTU(1500)分片 unsigned short id; // 标识,相同表示数据包来源于同一报文 unsigned short flag:3, // 标志,MF:more frag、DF:don't frag、未用 flag_offset:13; // 片偏移,标识该数据包在上层数据报文中的偏移量 unsigned char ttl; // 生存时间 time to live,默认是64,避免环路 unsigned char type; // 协议,上层协议的类型,udp, tcp unsigned short check; // 首部校验和 unsigned int sip; // 源ip unsigned int dip; // 目的ip };
2.3、udp 协议
// udp报文首部 struct udphdr { unsigned short sport; // 源端口 unsigned short dport; // 目的端口 unsigned short length; // udp 报文长度 unsigned short check; // udp 校验 };
协议栈中用户数据经过逐层封装,增加各层的首部,得到 udp 数据报
// udp 报文,sizeof(struct udppkt) == 42 struct udppkt { struct ethhdr eh; // 以太网帧首部 struct iphdr ip; // ip 首部 struct udphdr udp; // udp 首部 unsigned char payload[0]; // 应用层数据,柔性数组(零长数组) };
零长数组(柔性数组):柔性数组是定义结构体时创建一个空数组,运行时可以动态进行结构体的扩展。注意零长数组必须声明为结构体的最后一个成员,且不能作为结构体的唯一成员,sizefo返回的结构体的大小不包括柔性数组的内存。
那么,为什么使用柔性数组?下面两种定义方式存在问题:
// 1、指针分配的内存是不连续的,分配内存:结构体->指针,释放内存:指针->结构体 unsigned char* payload; // 2、若数据不够存储,发生越界,内存泄漏。 unsigned char* payload[65535];
使用柔性数组的优势
- 无需初始化,数组名就是偏移
- 不占用空间
- 空间一次分配,分配连续的内存
柔性数组的适用场景
- 可以计算出长度
- 内存初始被分配好
2.4、问题分析
实现 udp 协议后,运行代码,过了一段时间,产生了两个问题:
- 服务端不能继续接收数据:在客户端使用
arp -a
发现没有服务端的 ip 和 mac,这需要我们手动实现ARP协议 - 服务端不能 ping 通:需要手动实现 icmp 协议
3、ARP 协议的实现
// ARP 首部 struct arphdr { unsigned short type; // 硬件类型 unsigned short protocol; // 协议类型 unsigned char addrlen; // 硬件地址长度 unsigned char protolen; // 协议地址长度 unsigned short oper; // 操作类型,ARP请求1,ARP响应2,RARP请求3,RARP响应4 unsigned char smac[ETH_ADDR_LENGTH]; // 源 mac 地址 unsigned int sip; // 源 ip 地址 unsigned char dmac[ETH_ADDR_LENGTH]; // 目的 mac 地址 unsigned int dip; // 目的 ip 地址 }; // ARP 数据包 struct arppkt { struct ethhdr eh; // 以太网帧首部 struct arphdr arp; // ARP 首部 };
在客户端测试 arp -a
,前后的对比:
注:如果不用代码的实现形式,可以采用静态绑定 arp 地址
4、icmp 协议的实现
// icmp 首部,icmp分为差错报文和查询报文,由类型和代码共同决定 struct icmphdr { unsigned char type; // 类型 unsigned char code; // 代码 unsigned short check; // 首部校验和 unsigned short id; // 标识 unsigned short seq; // 序列号 unsigned char data[32]; // 数据 }; // icmp 数据包 struct icmppkt { struct ethhdr eh; // 以太网帧首部 struct iphdr ip; // ip 首部 struct icmphdr icmp; // icmp 首部 };
在客户端 ping
服务端,前后的对比:
5、netmap 代码实现
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/poll.h> #include <arpa/inet.h> #define NETMAP_WITH_LIBS #include <net/netmap_user.h> // netmap 开启 #pragma pack(1) // 以1个字节对齐 #define ETH_ADDR_LENGTH 6 // 以太网 mac 地址长度 #define PROTO_IP 0x0800 // ip协议 #define PROTO_ARP 0x0806 // ARP协议 #define PROTO_UDP 17 #define PROTO_ICMP 1 #define PROTO_IGMP 2 #define UDP 1 #define ICMP 1 #define ARP 1 // 以太网数据帧头,字节对齐: sizeof = 16 struct ethhdr { unsigned char dmac[ETH_ADDR_LENGTH]; // 目的mac地址 unsigned char smac[ETH_ADDR_LENGTH]; // 源mac地址 unsigned short protocol; //协议:上层协议的类型,ip:0x0800 }; // ip 数据包首部 struct iphdr { unsigned char version:4, // ip协议版本,IPv4:0100 hdrlen:4; // 首部长度,* 4 unsigned char tos; // 服务类型 unsigned short tot_len; // 总长度,* 1,最大65535字节,超过MTU(1500)分片 unsigned short id; // 标识,相同表示数据包来源于同一报文 unsigned short flag:3, // 标志,MF:more frag、DF:don't frag、未用 flag_offset:13; // 片偏移,标识该数据包在上层数据报文中的偏移量 unsigned char ttl; // 生存时间 time to live,默认是64,避免环路 unsigned char protocol; // 协议,上层协议的类型,udp, tcp unsigned short check; // 首部校验和 unsigned int sip; // 源ip unsigned int dip; // 目的ip }; // udp报文首部 struct udphdr { unsigned short sport; // 源端口 unsigned short dport; // 目的端口 unsigned short len; // udp报文长度 unsigned short check; // udp 校验 }; // udp 报文,sizeof(struct udppkt) == 42 struct udppkt { struct ethhdr eh; // 以太网帧首部 struct iphdr ip; // ip 首部 struct udphdr udp; // udp 首部 unsigned char payload[0]; //应用层数据,柔性数组(零长数组) }; // ARP 首部 struct arphdr { unsigned short type; // 硬件类型 unsigned short protocol; // 协议类型 unsigned char addrlen; // 硬件地址长度 unsigned char protolen; // 协议地址长度 unsigned short oper; // 操作类型,ARP请求1,ARP响应2,RARP请求3,RARP响应4 unsigned char smac[ETH_ADDR_LENGTH]; // 源 mac 地址 unsigned int sip; // 源 ip 地址 unsigned char dmac[ETH_ADDR_LENGTH]; // 目的 mac 地址 unsigned int dip; // 目的 ip 地址 }; // ARP 数据包 struct arppkt { struct ethhdr eh; // 以太网帧首部 struct arphdr arp; // ARP 首部 }; // icmp 首部,icmp报文分为差错报文和查询报文,由类型和代码共同决定其类型 struct icmphdr { unsigned char type; // 类型 unsigned char code; // 代码,指定类型中的一个功能 unsigned short check; // 首部校验和 unsigned short id; // 标识 unsigned short seq; // 序列号 unsigned char data[32]; }; // icmp 数据包 struct icmppkt { struct ethhdr eh; // 以太网帧首部 struct iphdr ip; // ip 首部 struct icmphdr icmp; // icmp 首部 }; // 字符串"FF:...:FF"转 mac 地址(十六进制数字) int str2mac(char *mac, char *str) { char *p = str; unsigned char value = 0x0; // 十六进制起始 int i = 0; while (*p != '\0') { // : 分割符 if (*p == ':') { mac[i++] = value; value = 0x0; } else { // 处理数字 unsigned char temp = *p; if (temp <= '9' && temp >= '0') { temp -= '0'; } else if (temp <= 'f' && temp >= 'a') { temp -= 'a'; temp += 10; } else if (temp <= 'F' && temp >= 'A') { temp -= 'A'; temp += 10; } else { break; } value <<= 4; value |= temp; } p ++; } mac[i] = value; return 0; } // 回复 arp void echo_arp_pkt(struct arppkt *arp, struct arppkt *arp_rt, char *hmac) { memcpy(arp_rt, arp, sizeof(struct arppkt)); memcpy(arp_rt->eh.dmac, arp->eh.smac, ETH_ADDR_LENGTH); str2mac(arp_rt->eh.smac, hmac); arp_rt->eh.protocol = arp->eh.protocol; arp_rt->arp.addrlen = 6; arp_rt->arp.protolen = 4; arp_rt->arp.oper = htons(2); str2mac(arp_rt->arp.smac, hmac); arp_rt->arp.sip = arp->arp.dip; memcpy(arp_rt->arp.dmac, arp->arp.smac, ETH_ADDR_LENGTH); arp_rt->arp.dip = arp->arp.sip; } // 回复 udp,bad udp,估计是校验的问题 void echo_udp_pkt(struct udppkt *udp, struct udppkt *udp_rt) { memcpy(udp_rt, udp, sizeof(struct udppkt)); memcpy(udp_rt->eh.dmac, udp->eh.smac, ETH_ADDR_LENGTH); memcpy(udp_rt->eh.smac, udp->eh.dmac, ETH_ADDR_LENGTH); udp_rt->ip.sip = udp->ip.dip; udp_rt->ip.dip = udp->ip.sip; udp_rt->udp.sport = udp->udp.dport; udp_rt->udp.dport = udp->udp.sport; } // icmp 校验和 unsigned short in_cksum(unsigned short *addr, int len) { register int nleft = len; register unsigned short *w = addr; register int sum = 0; unsigned short answer = 0; while (nleft > 1) { sum += *w++; nleft -= 2; } if (nleft == 1) { *(u_char *)(&answer) = *(u_char *)w ; sum += answer; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); answer = ~sum; return (answer); } // 回复 icmp void echo_icmp_pkt(struct icmppkt *icmp, struct icmppkt *icmp_rt) { memcpy(icmp_rt, icmp, sizeof(struct icmppkt)); icmp_rt->icmp.type = 0x0; icmp_rt->icmp.code = 0x0; icmp_rt->icmp.check = 0x0; icmp_rt->ip.sip = icmp->ip.dip; icmp_rt->ip.dip = icmp->ip.sip; memcpy(icmp_rt->eh.dmac, icmp->eh.smac, ETH_ADDR_LENGTH); memcpy(icmp_rt->eh.smac, icmp->eh.dmac, ETH_ADDR_LENGTH); icmp_rt->icmp.check = in_cksum((unsigned short*)&icmp_rt->icmp, sizeof(struct icmphdr)); } int main() { struct ethhdr *eh; struct pollfd pfd = {0}; struct nm_pkthdr h; unsigned char *stream = NULL; 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) { // 从内存的 ringbuf 中取出一个包 stream = nm_nextpkt(nmr, &h); eh = (struct ethhdr*)stream; // 若以太网帧携带的是 ip 数据 if (ntohs(eh->protocol) == PROTO_IP) { struct udppkt *udp = (struct udppkt*)stream; // 1、ip数据包携带的是 udp 报文 if (udp->ip.protocol == PROTO_UDP) { struct in_addr addr; addr.s_addr = udp->ip.sip; int udp_length = ntohs(udp->udp.len); printf("%s:%d: udp_length:%d, ip_len:%d -->\n", inet_ntoa(addr), ntohs(udp->udp.sport), udp_length, ntohs(udp->ip.tot_len)); udp->payload[udp_length - 8] = '\0'; // udp报文长度=udp长度-udp首部 printf("udp pkt: %s\n", udp->payload); #if 1 struct udppkt udp_rt; echo_udp_pkt(udp, &udp_rt); nm_inject(nmr, &udp_rt, sizeof(struct udppkt)); #endif } #if ICMP // 2、ip数据包携带的是 icmp 报文 else if (udp->ip.protocol == PROTO_ICMP) { struct icmppkt *icmp = (struct icmppkt*)stream; printf("icmp ---------- --> %d, %x\n", icmp->icmp.type, icmp->icmp.check); if (icmp->icmp.type == 0x08) { struct icmppkt icmp_rt = {0}; echo_icmp_pkt(icmp, &icmp_rt); //printf("icmp check %x\n", icmp_rt.icmp.check); nm_inject(nmr, &icmp_rt, sizeof(struct icmppkt)); } } #endif else if (udp->ip.protocol == PROTO_IGMP) { } else { printf("other ip packet"); } } #if ARP // 若以太网帧携带的是 arp 数据 else if (ntohs(eh->protocol) == PROTO_ARP) { struct arppkt *arp = (struct arppkt *)stream; struct arppkt arp_rt; // 若访问的是本机的ip,回复arp数据包 if (arp->arp.dip == inet_addr("192.168.0.104")) { echo_arp_pkt(arp, &arp_rt, "00:0c:29:18:ef:9d"); // 本地的mac nm_inject(nmr, &arp_rt, sizeof(struct arppkt)); } } #endif } } }