通过mmap可以将网卡里的数据映射到内存中去
这里是零拷贝,指的是cpu指令没有参与,但并不是没有拷贝,这是一种DMA的方式
实现协议栈有几种方式,如raw-socket、netmap、dpdk等,这里用netmap实现
一、实现UDP
以下是udp数据帧的格式:以太网头+ip头+udp头+数据
1、数据链路层:封装以太网协议头
#define ETH_ADDR_LENGTH 6 //以太网头 struct ethhdr { //hdr是header的缩写 unsigned char h_dst[ETH_ADDR_LENGTH];//目的地址mac unsigned char h_src[ETH_ADDR_LENGTH];//源地址mac unsigned short h_proto;//协议类型 }; //14字节
写成结构体,对内部的域的操作更为合适。
2、网络层:封装IP协议头
struct iphdr { unsigned char hdrlen:4, //????网络字节序 因为网络字节序是以1个字节为单位的,对于比一个字节小的,要手动将小端改为大端 version:4; // 0x45 大端 unsigned char tos;//type of service unsigned short totlen;//total length unsigned short id;//16位标识 unsigned short flag_offset; //3位标志+13位片偏移 unsigned char ttl; //time to live 生存周期(比如:每经过一个网关ttl-1) // 0x1234// htons unsigned char type;//8位协议 用于指明IP的上层协议. unsigned short check;//16位首部校验和 unsigned int sip;//源ip unsigned int dip;//目的ip }; // 20字节
3、传输层:封装udp头
//udp协议头 struct udphdr { unsigned short sport;//源端口 unsigned short dport;//目的端口 unsigned short length;//udp长度 unsigned short check;//校验值 }; // 8字节
4、封装成udp数据帧
udp数据帧 包含 以太网头、ip头、udp头 以及 用户数据
现在通过一个结构体struct将它们组织起来。
那如何去表现用户数据呢? 如果用一个
unsigned char *data
,占4个字节,但它并没有存具体的用户数据,由于指针空间内存不连续,操作不当可能造成内存泄漏,因此用指针不合适。而由于不知道数据部分多长,一般使用一个较大长度的数组,绝多数情况会浪费空间,因此不适合使用一个固定长度数组。可以选择使用一个0长数组(柔性数组),代表一个用户数据的标签,如果使用sizeof,输出是0,不占任何空间。
使用柔性数组的条件:1.我们并不关心数组的长度,而是可以通过某种方式计算出来,通过udp头中的length来计算出数据长度。 2.内存是提前分配好的,不需要去考虑越界的问题
struct udppkt { struct ethhdr eh; // 以太网头 14字节 struct iphdr ip; // ip头 20字节 struct udphdr udp; // udp头 8字节 unsigned char data[0]; //用户数据 };
这样存在默认字节对齐,此时sizeof(udppkt)为44而不是42
因此要加上1个字节对齐,使得内存上连续.
对于协议解析时,一定要做一个字节对齐
#pragma pack(1) //以1个字节对齐
eth0中有一块地址,指向内存,因此数据是一样的。fd指向得是网卡设备,外界传过来数据,可以通过网卡设备,从fd中检测到数据。然后从内存中读取相应的数据。
对于接受的数据,如何组织呢?
用ringbuffer
内存如何去取数据?
1.轮询 (适合大量数据)
2.事件 (适合少量数据)
stream里面前面三个分别是以太网头、ip头、udp头
后面才是数据
5、ARP
当客户端(windows)向服务端(windows下的linux虚拟机)发送信息的时候,发现发送了一段时间后就没法发送了
在windows下cmd里面输入arp -a
可以看到里面有arp表,里面有我测试的服务端的地址192.168.192.128,因此此时可以发送udp数据成功
但是由于是动态arp,可能此arp项会消失,因此会导致udp数据发送失败。
主要是因为客户端这边的arp表内没有 服务器的arp信息。
当客户端发送数据包的时候,如果arp表内没有相应的信息,就会发送arp请求,因此服务端还要具有相应arp请求的功能。
arp
1.发送请求(广播)
2.接受arp响应,更新arp表
#define ETH_ADDR_LENGTH 6 struct arphdr { unsigned short h_type; unsigned short h_proto; unsigned char h_addrlen; unsigned char h_protolen; unsigned short oper; unsigned char smac[ETH_ADDR_LENGTH]; unsigned int sip; unsigned char dmac[ETH_ADDR_LENGTH]; unsigned int dip; };
6、封装成ARP包
ARP包有两部分组成:以太网头+ARP头(没有数据部分)
struct arppkt { struct ethhdr eh; struct arphdr arp; };
由于使用了netmap,接受的数据直接映射到内存中去了,没有走内核的协议栈
当进程打开的时候,ping不通的,因为该程序没有实现ICMP
因为Ping是发送的ICMP数据
7、完整代码
#include <stdio.h> #include <sys/poll.h> #include <arpa/inet.h> #define NETMAP_WITH_LIBS #include <net/netmap_user.h> #pragma pack(1) //以1个字节对齐 //mac地址是6个字节 也就是以太网协议头要加的内容 #define ETH_ADDR_LENGTH 6 //Ip协议头内容 #define PROTO_IP 0x0800 #define PROTO_ARP 0x0806 #define PROTO_UDP 17 #define PROTO_ICMP 1 //以太网头 struct ethhdr { unsigned char h_dst[ETH_ADDR_LENGTH];//目的地址mac unsigned char h_src[ETH_ADDR_LENGTH];//源地址mac unsigned short h_proto;//协议类型 }; // 14字节 //ip协议头 struct iphdr { unsigned char hdrlen:4, version:4; // 0x45 unsigned char tos;//type of service unsigned short totlen;//total length unsigned short id;//16位标识 unsigned short flag_offset; //3位标志+13位片偏移 unsigned char ttl; //time to live 生存周期(比如:每经过一个网关ttl-1) // 0x1234// htons unsigned char type;//8位协议 用于指明IP的上层协议. unsigned short check;//16位首部校验和 unsigned int sip;//源ip unsigned int dip;//目的ip }; // 20字节 //udp协议头 struct udphdr { unsigned short sport;//源端口 unsigned short dport;//目的端口 unsigned short length;//udp长度 unsigned short check;//校验值 }; // 8字节 struct udppkt { struct ethhdr eh; // 以太网头 14字节 struct iphdr ip; // ip头 20字节 struct udphdr udp; // udp头 8字节 unsigned char data[0]; //用户数据 }; // sizeof(struct udppkt) == struct arphdr { unsigned short h_type; unsigned short h_proto; unsigned char h_addrlen; unsigned char h_protolen; unsigned short oper; unsigned char smac[ETH_ADDR_LENGTH]; unsigned int sip; unsigned char dmac[ETH_ADDR_LENGTH]; unsigned int dip; }; struct arppkt { struct ethhdr eh; struct arphdr arp; }; 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; } void echo_arp_pkt(struct arppkt *arp, struct arppkt *arp_rt, char *mac) { //把源和目的 的ip换一下就行了,然后补个mac地址 memcpy(arp_rt, arp, sizeof(struct arppkt)); memcpy(arp_rt->eh.h_dst, arp->eh.h_src, ETH_ADDR_LENGTH); str2mac(arp_rt->eh.h_src, mac); arp_rt->eh.h_proto = arp->eh.h_proto; arp_rt->arp.h_addrlen = 6; arp_rt->arp.h_protolen = 4; arp_rt->arp.oper = htons(2); str2mac(arp_rt->arp.smac, mac); 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; } // int main() { struct nm_pkthdr h;//ringbuffer的头 struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL); if (nmr == NULL) return -1; //把fd放入pollfd中,如果fd可读,就去操作数据,不可读就不操作 struct pollfd pfd = {0}; pfd.fd = nmr->fd; pfd.events = POLLIN; while (1) { int ret = poll(&pfd, 1, -1);//第一个参数:pollfd,第二个参数:fd个数,第三个参数:-1代表一直阻塞,直到数据过来 if (ret < 0) continue; if (pfd.revents & POLLIN) {//有数据来了 unsigned char *stream = nm_nextpkt(nmr, &h);//取数据(因为已经在内存中了,不能用读,由于是环形ringbuffer,因此取数据叫next package) struct ethhdr *eh = (struct ethhdr *)stream;//把stream中的第一个部分转换为以太网头 if (ntohs(eh->h_proto) == PROTO_IP) { //取出来的上层协议是IP协议 struct udppkt *udp = (struct udppkt *)stream;//转化为udp帧数据格式 if (udp->ip.type == PROTO_UDP) { //udp包 int udplength = ntohs(udp->udp.length); udp->data[udplength-8] = '\0'; //udp总长度-8个字节长度的udp头 就是upd数据部分的长度。 末尾加上字符串结尾'\0' printf("udp --> %s\n", udp->data); } else if (udp->ip.type == PROTO_ICMP) { } } else if (ntohs(eh->h_proto) == PROTO_ARP) {//ARP包 struct arppkt *arp = (struct arppkt *)stream; struct arppkt arp_rt; if (arp->arp.dip == inet_addr("192.168.0.123")) { //如果接受到的广播arp是本机的就回复 (如果不进行判断就是ARP攻击了,不管是什么arp请求,都回复,会导致它们的arp表更新错误的信息) echo_arp_pkt(arp, &arp_rt, "00:50:56:33:1c:ca");//创建一个arp回复的包(源和目的互换,补充上mac地址(ifconfig可以查看)) nm_inject(nmr, &arp_rt, sizeof(arp_rt));//发送arp应答 printf("arp ret\n"); } } } } }
二、实现TCP
tcp数据包为:以太网头+ip头+tcp头+数据
1、封装tcp头
struct tcphdr { unsigned short sport; unsigned short dport; unsigned int seqnum; unsigned int acknum; unsigned char hdrlen_resv; // unsigned char flag; unsigned short window; // 1460 unsigned short checksum; unsigned short urgent_pointer; unsigned int options[0]; };
2、封装tcp数据帧
struct tcppkt { struct ethhdr eh; // 14 struct iphdr ip; // 20 struct tcphdr tcp; // 8 unsigned char data[0]; };
3、tcb
struct ntcb { unsigned int sip;//原ip unsigned int dip;//目的ip unsigned short sport;//原端口 unsigned short dport;//目的端口 unsigned char smac[ETH_ADDR_LENGTH];//原mac(如果arp表没有的话,要传) unsigned char dmac[ETH_ADDR_LENGTH];//目的mac unsigned char status;//状态(包含tcp的11个状态) };
11个状态
typedef enum _tcp_status { TCP_STATUS_CLOSED, TCP_STATUS_LISTEN, TCP_STATUS_SYN_REVD, TCP_STATUS_SYN_SENT, TCP_STATUS_ESTABLISHED, TCP_STATUS_FIN_WAIT_1, TCP_STATUS_FIN_WAIT_2, TCP_STATUS_CLOSING, TCP_STATUS_TIME_WAIT, TCP_STATUS_CLOSE_WAIT, TCP_STATUS_LAST_ACK, };
标志CWR置1就是0x80,标志ECE置1就是0x40,标志URG置1就是0x20,以此类推(对照)
#define TCP_CWR_FLAG 0x80 #define TCP_ECE_FLAG 0x40 #define TCP_URG_FLAG 0x20 #define TCP_ACK_FLAG 0x10 #define TCP_PSH_FLAG 0x08 #define TCP_RST_FLAG 0x04 #define TCP_SYN_FLAG 0x02 #define TCP_FIN_FLAG 0x01
后续拿接受到的flag和上面的与操作,就可以判断出该位是否置为1了
PSH就是告诉接收方赶紧让应用程序处理
RST就代表发送方告诉接受方,你发送的数据不合法,给你重置了
4、伪代码
int main() { struct nm_pkthdr h; struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL); if (nmr == NULL) return -1; struct pollfd pfd = {0}; pfd.fd = nmr->fd; pfd.events = POLLIN; struct ntcb* tcb; while (1) { int ret = poll(&pfd, 1, -1); if (ret < 0) continue; if (pfd.revents & POLLIN) { unsigned char *stream = nm_nextpkt(nmr, &h); struct ethhdr *eh = (struct ethhdr *)stream; if (ntohs(eh->h_proto) == PROTO_IP) { struct udppkt *udp = (struct udppkt *)stream; if (udp->ip.type == PROTO_UDP) { // int udplength = ntohs(udp->udp.length); udp->data[udplength-8] = '\0'; printf("udp --> %s\n", udp->data); } else if (udp->ip.type == PROTO_ICMP) { // //************************************************************// } else if (udp->ip.type == PROTO_TCP) { struct tcppkt *tcp = (struct tcppkt *)stream; /* unsigned int sip = tcp->ip.sip; unsigned int dip = tcp->ip.dip; unsigned short sport = tcp->tcp.sport; unsigned short dport = tcp->tcp.dport; tcb = search_tcb(); */ if (tcb->status == TCP_STATUS_LISTEN) { //listen状态 if (tcp->tcp.flag & TCP_SYN_FLAG) {//并且是syn同步请求,代表着客户端请求建立连接 tcb->status = TCP_STATUS_SYN_REVD;//状态置为 SYN_RECV状态(SYN接受到的状态,等待接受ack) //接受第一次握手,发送第二次握手 // send syn, ack pkt // seqnum, ack } } else if (tcb->status == TCP_STATUS_SYN_REVD) {//SYN_RECD状态 if (tcp->tcp.flag & TCP_ACK_FLAG) {//接收到第三次握手 tcb->status = TCP_STATUS_ESTABLISHED;//状态变为tcp连接建立的状态 } } } //************************************************************// } else if (ntohs(eh->h_proto) == PROTO_ARP) { struct arppkt *arp = (struct arppkt *)stream; struct arppkt arp_rt; if (arp->arp.dip == inet_addr("192.168.0.123")) { // echo_arp_pkt(arp, &arp_rt, "00:50:56:33:1c:ca"); nm_inject(nmr, &arp_rt, sizeof(arp_rt)); printf("arp ret\n"); } } } } }
补充:
对于字节的内存地址是,坐上到右下,依次是增加,坐上角为低位,右下角为低位。
但是对于1个字节(绿色圈的部分)来说, 位的内存地址,左边低,右边高
因此对于结构体还说,字节的内存是依次增大的,而位序正好的是反的。