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

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


相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
186 9
|
1月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
32 1
|
24天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
44 5
|
1月前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
1月前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
1月前
|
存储
系统调用处理程序在内核栈中保存了哪些上下文信息?
【10月更文挑战第29天】系统调用处理程序在内核栈中保存的这些上下文信息对于保证系统调用的正确执行和用户程序的正常恢复至关重要。通过准确地保存和恢复这些信息,操作系统能够实现用户模式和内核模式之间的无缝切换,为用户程序提供稳定、可靠的系统服务。
51 4
|
1月前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
48 0
|
2月前
数据结构(栈与列队)
数据结构(栈与列队)
23 1
|
2月前
【数据结构】-- 栈和队列
【数据结构】-- 栈和队列
21 0