netmap: UDP 协议栈的实现

简介: netmap: UDP 协议栈的实现

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
         } 
     }
 }
相关文章
|
3月前
|
存储 网络协议 算法
UDP 协议和 TCP 协议
本文介绍了UDP和TCP协议的基本结构与特性。UDP协议具有简单的报文结构,包括报头和载荷,报头由源端口、目的端口、报文长度和校验和组成。UDP使用CRC校验和来检测传输错误。相比之下,TCP协议提供更可靠的传输服务,其结构复杂,包含序列号、确认序号和标志位等字段。TCP通过确认应答和超时重传来保证数据传输的可靠性,并采用三次握手建立连接,四次挥手断开连接,确保通信的稳定性和完整性。
93 1
UDP 协议和 TCP 协议
|
20天前
|
网络协议 SEO
TCP连接管理与UDP协议IP协议与ethernet协议
TCP、UDP、IP和Ethernet协议是网络通信的基石,各自负责不同的功能和层次。TCP通过三次握手和四次挥手实现可靠的连接管理,适用于需要数据完整性的场景;UDP提供不可靠的传输服务,适用于低延迟要求的实时通信;IP协议负责数据包的寻址和路由,是网络层的重要协议;Ethernet协议定义了局域网的数据帧传输方式,广泛应用于局域网设备之间的通信。理解这些协议的工作原理和应用场景,有助于设计和维护高效可靠的网络系统。
28 4
|
5月前
|
存储 网络协议 Ubuntu
【Linux开发实战指南】基于UDP协议的即时聊天室:快速构建登陆、聊天与退出功能
UDP 是一种无连接的、不可靠的传输层协议,位于IP协议之上。它提供了最基本的数据传输服务,不保证数据包的顺序、可靠到达或无重复。与TCP(传输控制协议)相比,UDP具有较低的传输延迟,因为省去了建立连接和确认接收等过程,适用于对实时性要求较高、但能容忍一定数据丢失的场景,如在线视频、语音通话、DNS查询等。 链表 链表是一种动态数据结构,用于存储一系列元素(节点),每个节点包含数据字段和指向下一个节点的引用(指针)。链表分为单向链表、双向链表和循环链表等类型。与数组相比,链表在插入和删除操作上更为高效,因为它不需要移动元素,只需修改节点间的指针即可。但访问链表中的元素不如数组直接,通常需要从
278 2
|
3月前
|
网络协议
UDP 协议
UDP 协议
130 58
|
2月前
|
网络协议 网络性能优化 C#
C# 一分钟浅谈:UDP 与 TCP 协议区别
【10月更文挑战第8天】在网络编程中,传输层协议的选择对应用程序的性能和可靠性至关重要。本文介绍了 TCP 和 UDP 两种常用协议的基础概念、区别及应用场景,并通过 C# 代码示例详细说明了如何处理常见的问题和易错点。TCP 适用于需要可靠传输和顺序保证的场景,而 UDP 适用于对延迟敏感且可以容忍一定数据丢失的实时应用。
40 1
|
2月前
|
网络协议 算法 数据格式
【TCP/IP】UDP协议数据格式和报文格式
【TCP/IP】UDP协议数据格式和报文格式
137 3
|
2月前
|
存储 网络协议 算法
更深层次理解传输层两协议【UDP | TCP】【UDP 缓冲区 | TCP 8种策略 | 三次握手四次挥手】
UDP和TCP各有所长,UDP以其低延迟、轻量级的特点适用于对实时性要求极高的应用,而TCP凭借其强大的错误检测、流量控制和拥塞控制机制,确保了数据的可靠传输,适用于文件传输、网页浏览等场景。理解它们的工作原理,特别是UDP的缓冲区管理和TCP的8种策略,对于优化网络应用的性能、确保数据的高效和可靠传输至关重要。开发者在选择传输层协议时,应根据实际需求权衡利弊,合理利用这两项关键技术。
74 5
|
2月前
|
JavaScript 安全 Java
谈谈UDP、HTTP、SSL、TLS协议在java中的实际应用
下面我将详细介绍UDP、HTTP、SSL、TLS协议及其工作原理,并提供Java代码示例(由于Deno是一个基于Node.js的运行时,Java代码无法直接在Deno中运行,但可以通过理解Java示例来类比Deno中的实现)。
73 1
|
3月前
|
监控 网络协议 网络性能优化
如何办理支持UDP协议的网络
在当今网络环境中,UDP(用户数据报协议)因传输速度快、延迟低而广泛应用于在线游戏、视频流媒体、VoIP等实时服务。本文详细介绍了办理支持UDP协议网络的方法,包括了解UDP应用场景、选择合适的ISP及网络套餐、购买支持UDP的设备并进行优化设置,以及解决常见问题的策略,帮助用户确保网络稳定性和速度满足实际需求。
|
3月前
|
网络协议
UDP协议在网络通信中的独特应用与优势
UDP(用户数据报协议)作为关键的传输层协议,在网络通信中展现出独特优势。本文探讨UDP的无连接性及低开销特性,使其在实时性要求高的场景如视频流、在线游戏中表现优异;其不保证可靠交付的特性赋予应用程序自定义传输策略的灵活性;面向报文的高效处理能力及短小的包头设计进一步提升了数据传输效率。总之,UDP适用于高速、实时性强且对可靠性要求不高的应用场景,为网络通信提供了多样化的选择。