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

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


相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
相关文章
|
5天前
|
机器学习/深度学习 算法 测试技术
【单调栈】3113. 边界元素是最大值的子数组数目
【单调栈】3113. 边界元素是最大值的子数组数目
|
3天前
栈的基本应用
栈的基本应用
10 3
|
3天前
栈与队列理解
栈与队列理解
9 1
|
3天前
|
存储 算法
数据结构与算法 栈与队列
数据结构与算法 栈与队列
10 0
数据结构与算法 栈与队列
|
3天前
|
C++
数据结构(共享栈
数据结构(共享栈
6 0
|
3天前
|
C++
数据结构(顺序栈
数据结构(顺序栈
11 2
|
4天前
|
容器
【栈与队列】栈与队列的相互转换OJ题
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
10 0
|
4天前
|
存储
【栈】基于顺序表的栈功能实现
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端 称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
12 0
|
4天前
|
存储 程序员
什么是堆,什么是栈
什么是堆,什么是栈
6 0
|
5天前
|
算法 测试技术 C++
【栈 最小公倍数 最大公约数】2197. 替换数组中的非互质数
【栈 最小公倍数 最大公约数】2197. 替换数组中的非互质数
【栈 最小公倍数 最大公约数】2197. 替换数组中的非互质数