用户态协议栈设计netmap实现

简介: 用户态协议栈设计netmap实现



通过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.内存是提前分配好的,不需要去考虑越界的问题

C语言0长度数组(可变数组/柔性数组)详解

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) 的意义是什么

#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了

TCP标志位详解(TCP Flag)

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个字节(绿色圈的部分)来说, 位的内存地址,左边低,右边高

因此对于结构体还说,字节的内存是依次增大的,而位序正好的是反的。


相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
4月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
63 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
9月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
344 77
|
8月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
172 11
|
8月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
9月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
248 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
9月前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
143 9
|
9月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
197 7
|
11月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
292 5
|
11月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
246 0