用户空间协议栈设计和netmap综合指南,将网络效率提升到新高度

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
网络型负载均衡 NLB,每月750个小时 15LCU
云解析 DNS,旗舰版 1个月
简介: 这篇综合指南将深入探讨用户空间协议栈设计和netmap技术,以提高网络效率。我们将详细介绍用户空间协议栈的工作原理、优点和挑战,并提供一系列优化策略。同时,我们将重点介绍netmap技术,它是一个高性能数据包I/O框架,可以极大地提升网络吞吐量和响应速度。通过结合用户空间协议栈设计和netmap技术,读者将了解如何最大限度地提高网络连接的速度和效率。本指南适用于网络开发人员、系统管理员和对网络性能优化感兴趣的读者。无论你是初学者还是有经验的专业人士,我们相信这篇文章将为你带来全面的见解和实用的指导,帮助你将网络效率提升到一个新的高度。

一、协议概念

1.1、七层网络模型和五层网络模型

image.png

image.png

应用层: 最接近用户的一层,为用户程序提供网络服务。主要协议有HTTP、FTP、TFTP、SMTP、DNS、POP3、DHCP等。
表示层: 数据的表示、安全、压缩。管理数据的解密和加密。
会话层: 负责在网络中的两个节点之间的建立、维持和终止通信。
传输层: 模型中最重要的一层,负责传输协议的流控和差错校验。数据包离开网卡后进入的就是传输层;主要协议有:TCP、UDP等。
网络层: 将网络地址翻译成对应的物理地址。主要协议有:ICMP、IP等。
数据链路层: 建立逻辑连接、进行硬件地址寻址、差错校验等功能,解决两台相连主机之间的通信问题。主要协议有SLIP、以太网协议/MAC帧协议、ARP和RARP等。
物理层: 模型的最低层,建立、维护、断开物理连接,传输比特流。常见的物理媒介有光纤、电缆、中继器等。主要协议有RS232等。

1.2、以太网

以太网不是一种网络,而是一种局域网技术,它既有数据链路层内容,也有一些物理层内容。局域网技术除了以太网外,还有令牌环网、无线LAN/WAN等。
以太网的网线必须是双绞线,以太网中的所有主机共享一个通信通道; 当局域网中一台主机发送数据后,该局域网的所有设备都会收到该数据。因为共用一个通信通道,因此同一时刻只允许一台主机发送数据;如果同一时刻不只有一个主机发送数据,为避免干扰,该主机会执行碰撞避免算法(等待一段时间后再进行数据重发)。
以太网帧格式如下:
image.png

源地址和目的地址是指网卡MAC地址,长度是48 bit(6字节)。帧协议类型字段有三种,分别对应IP协议、ARP协议和RARP协议。帧末尾是CRC校验码。

定义一个以太网头结构体示例代码:

#define ETHER_ADDR_LEN    6
struct etherhdr {
   
   
    unsigned char dst_mac[ETHER_ADDR_LEN];
    unsigned char src_mac[ETHER_ADDR_LEN];
    unsigned short protocol;
};

输出它的大小:

sizeof(struct etherhdr) = 14

1.3、IP协议

IP协议全称Internet Protocol,即网际互连协议,存在于网络层,负责数据在网络中传输。
IP协议格式如下:

 0              |1              |2              |3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-------+-------+---------------+-------------------------------+
|version|hdr_len| Type Of Server| total length                  |
+-------------------------------+-----+-------------------------+
|                    ID         |flag |    framegament offset    |
+---------------+---------------+-------------------------------+
| TTL           | Protocol      | header CRC                    |
+---------------------------------------------------------------+
|                        Source IP                                |
+---------------------------------------------------------------+
|                        Destination IP                            |
+---------------------------------------------------------------+
|                            Option (if have)                    |
+---------------------------------------------------------------+
|                             Data                                |
|                             ...                                |
+---------------------------------------------------------------+

定义一个IP协议头结构体示例代码:

struct iphdr {
   
   
    unsigned char version : 4,
        hdrlen : 4;

    unsigned char tos;

    unsigned short totlen;

    unsigned short id;
    unsigned short flag : 3,
        offset : 13;
    unsigned char ttl;

    unsigned char protocol;

    unsigned short check;

    unsigned int sip;
    unsigned int dip;
};

1.4、ARP协议

ARP协议全称Address Resolution Protocol,即地址解析协议,是根据IP地址获取MAC地址的一个TCP/IP协议。

ARP协议的作用:在同一个局域网中要给对方发消息,就必须得知道对方的MAC地址,而实际大部分情况下只知道对方的IP地址,因此需要通过ARP协议来根据IP地址来获取目标主机的MAC地址。
ARP的数据格式如下:

 0              |1              |2              |3                |4                |5
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7
+-----------------------------------------------------------------------------------------------+
|                                 Ethernet Destination IP                                        |
+-----------------------------------------------------------------------------------------------+
|                                 Ethernet Source IP                                                |
+-------------------------------+-------------------------------+-------------------------------+
|    framegament type            |   harware address tpye        |    Protocol address type        |
+---------------+---------------+-------------------------------+-------------------------------+
| HW_addr_length|Pro_addr_len   | op code                        | Source MAC                     |
+---------------------------------------------------------------+-------------------------------|
|                                 Source MAC                     | Source IP                        |
+-------------------------------+---------------------------------------------------------------|
|        Source IP                |        Destination MAC                                            | 
+-------------------------------+---------------------------------------------------------------+
|        Destination MAC            |         Destination IP                                            |
+-----------------------------------------------------------------------------------------------+
|                                                                                                 |
|                                        PAD                                                         |
|                                                                                                 |
+-----------------------------------------------------------------------------------------------+

可以看出,ARP是MAC帧协议的上层协议,前3个字段和最后一个字段对应的就是以太网头部。由于ARP数据包的长度不足46字节,因此ARP数据包在封装成为MAC帧时还需要补上18字节的填充字段。
定义一个arp协议头结构体示例代码:

struct arphdr{
   
   
    unsigned short h_type;
    unsigned short h_proto;
    unsigned char    h_addrlen;
    unsigned char    protolen;
    unsigned short    oper;
    unsigned char    smac[ETH_ALEN];
    unsigned int    sip;
    unsigned char    dmac[ETH_ALEN];
    unsigned int    dip;
    // pad
};

1.4.1、ARP攻击原理

arp攻击得到主要目的是使网络无法正常通信。 向局域网中的所有主机发送ARP应答,其中包含网关IP地址和虚假的MAC地址。局域网中的主机收到ARP应答跟新ARP表后,再发送数据时,就会发送到虚假的MAC地址导致通信故障,就无法和网关正常通信,导致无法访问互联网。

1.4.2、ARP欺骗原理

ARP欺骗并不会使网络无法正常通信,而是通过冒充网关或其他主机 使得 到达网关或主机的数据流量通过攻击主机进行转发。

比如冒充网关:ARP欺骗发送arp应答给局域网中其他的主机,其中包含网关的IP地址和进行ARP欺骗的主机MAC地址;并且也发送了ARP应答给网关,其中包含局域网中所有主机的IP地址和进行arp欺骗的主机MAC地址。当局域网中主机和网关收到ARP应答跟新ARP表后,主机和网关之间的流量就需要通过攻击主机进行转发。
冒充主机的过程和冒充网关相同。

1.5、ICMP协议

ICMP全称Internet Control Message Protocol,即互联网控制消息协议,位于 IP 报文的数据段。虽然ICMP是网络层协议,但它不直接传递数据到数据链路层,而是封装成IP数据包再传递到数据链路层,IP数据包中的协议类型字段为1就表示ICMP报文。ICMP协议的类型主要有两类:查询报文和差错报文。
ICMP报文格式如下:

 0              |1              |2              |3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+---------------+---------------+-------------------------------+
|     type        | code            |             CRC                    |
+-------------------------------+-------------------------------+
|                    ID            |            Sequence Number        |
+---------------------------------------------------------------+
|                        mask                                    |
+---------------------------------------------------------------+
标识 含义
type 类型,0 代表应答 ICMP 报文、8 代表请求 ICMP 报文。
code 代码,type= 8 && code= 0 表示回显请求(ping 请求);type= 0 && code= 0 表示回显应答(ping 应答);type = 11 && code = 0 表示超时。
CRC 校验和,包括数据在内的整个 ICMP 数据报的检验和
ID 标识符,将发送进程的 ID 号放置在标识符字段,这样即使在主机上运行了多个 ping 程序,ping 程序也可以识别出返回的信息。
Sequence Number 序列号,从 0 开始,每发送一次心得回显请求就加 1 。
mask 子网掩码

定义一个ICMP协议头结构体示例代码:

// ICMP
struct icmphdr {
   
   
    unsigned char type;
    unsigned char code;
    unsigned short check;
    unsigned short identifier;
    unsigned short sep;
    unsigned cahr    data[32];

};

ICMP的应用:
(1)ping命令。向目的服务器发送回显请求,目的服务器发送回显应答;计算发送回显请求数据包的时间与接收到回显应答数据包的时间差,就是数据包一去一回所需要的时间。
(2)traceroute命令。traceroute命令利用 ICMP 差错报文类型,用作追踪路由信息。前提条件是路由器没有禁用 ICMP。

1.6、MTU概念

MTU,全称Maximum Transmission Unit,即最大传输单元。说明一次数据帧可以发送或接收的最大数据量;以字节为单位,一般是是1500,不同网络类型的MTU不同。

(1)如果一次发送要发送的数据超过MTU,需要在IP层对数据进行分片。数据分片和组装在IP层,因为不同网络的MTU不同,不仅源主机可能需要对数据进行分片,数据传输过程中的路由器也可能对数据分片。

(2)以太网规定数据的最小长度为46字节,如果发送数据小于46字节,需要填充,比如ARP数据包就需要填充才能发送。

(3)对于UDP,是定长的8字节报头,如果IP报头没有携带可选项字段,那么UDP一次携带的数据最大为1500-20-8=1472字节,如果超出这个大小就需要在IP层进行分片。分片带来的后果是增加UDP的丢包率。

(4)分片也会增加TCP的丢包率,不过TCP有重传机制;因此需要尽可能避免分片,降低TCP重传次数。

1.7、MSS概念

MSS,全称Maximum Segment Size,即最大报文段大小。表示TCP传往另一端的最大块数据的长度。
当一个连接建立时,连接的双方都要提供各自的MSS。通过协商确定MSS的值(双方MSS的最小值)以避免TCP分片。如果没有分段发生, MSS越大越好。

1.8、TTL概念

TTL,全称Time To Live,即存活时间;指一个数据包可传递的最长距离(跃点数)。
当一个数据包经过一个路由器时,TTL减一;当TTL=0时路由器就会取消数据包的转发。
我们知道网络是有 环 存在的,设计TTL的目的是防止数据包因为不正确的路由表等原因造成无线循环而无法送达导致耗尽网络资源。

二、数据传输框图

网络上所有的数据传输都要经过网卡,网卡将模拟信号转换为数字信号,也就是将物理层信号转换为数据链路层信号。
image.png

注意:
(1)send()返回成功不代表发送成功,send()只是包数据拷贝到写缓冲区,真正发送数据由协议栈完成。如果客户端宕机而服务端一直执行send(),那么在一段时间后send()会返回-1;因为写缓冲区中的数据没有发送出去导致写缓冲区爆满。
(2)协议栈就是数据根据七层网络模型,自顶向下一层一层的协议头包住数据;接收端也是根据七层网络模型,自底向上一层层的解析协议。
(3)驱动如何把数据传递到协议栈?
在Linux kernel有一个sk_buffer结构,sk_buffer将驱动获取的数据通过sk_buffer传递到协议栈中。关于整个过程的执行流程可以参考这篇文章:链接

三、校验和 checksum的计算方法

(1) 先将需要计算checksum数据中的checksum字段设为0;
(2) 将checksum的数据按2 byte(16 bit)划分,如果最后有单个byte的数据,则在其后面补1 byte的0构成2 byte;
(3) 将所有的2 byte(16 bit)z值累加,得到一个4 byte(32 bit)的值;
(4)将得到的4 byte(32 bit)的值的高16bit与低16bit相加得到一个新的4 byte(32 bit)值;若新值大于0xFFFF,再将新值的高16bit与低16bit相加。
(5)将上一步计算所得的值按位取反,即得到checksum值,保存到checksum字段即可。
示例代码:

unsigned short in_cksum(unsigned short *addr,int len)
{
   
   
    register int nleft = len;
    register unsigned short *w = addr;
    register int sum = 0;//32bit
    unsigned short answer = 0;//16bit

    while (nleft > 1)
    {
   
   
        sum += *w++;//16bit为一组累加
        nleft -= 2;
    }

    if (nleft == 1)//存在单个byte情况
    {
   
   
        *(u_char*)(&answer) = *(u_char*)w;
        sum += answer;
    }
    sum = (sum >> 16) + (sum & 0xffff);// 高16bit与低16bit相加
    sum += (sum >> 16);//防止值大于0xffff

    //结果
    answer = ~sum;

    return (answer);
}

四、协议栈设计--netmap

要实现一个协议栈,那么就需要获得原始的协议数据。
image.png

4.1、获取原始协议数据的方法

(1)raw socket,即原始套接字,可以接收本机网卡的数据帧或数据包。有四种方式创建这类socket。

目标 实现
发送接收IP数据包 socket(PF_INET,SOCK_RAW,IPPROTO_TCP \ IPPROTO_UDP \ IPPROTO_ICMP) ;
发送接收以太网数据帧 socket(PF_PACKET,SOCK_RAW,htons(ETH_P_IP \ ETH_P_ARP \ ETH_P_ALL));
发送接收以太网数据帧(不包括以太网头部) socket(PF_PACKET,SOCK_DGRAM,htons(ETH_P_IP \ ETH_P_ARP \ ETH_P_ALL));

(2)旁路。netmap、dpdk等
(3)hook。bpf、ebpf等

4.2、零长数组

零长数组,顾名思义,就是长度为零的数组。一般在GUN C中使用,其他编译器使用可能会报错或警告。
零长度数组的一个特点是它不占用内存存储空间。如下示例:

#include <stdio.h>

char test[0];

int main()
{
   
   
    printf("size = %ld\n",sizeof(test));
    return 0;
}
// 输出 为 0

在结构体中使用,它同样也不占内存:

#include <stdio.h>

struct test{
   
   
    int len;
    int ch[0];
};

int main(void)
{
   
   
      printf("size of = %ld\n",sizeof(struct test));
      return 0;
}

零长数组的使用:内存已经分配,但数据长度不确定,需要计算出数据长度的,就可以使用零长数组。零长数组在内存池中使用比较多。
使用示例:

#include <stdio.h>

struct test{
   
   
    int len;
    char ch[0];
};
int main(void)
{
   
   
    struct test *buf;
    buf = (struct test *)malloc(sizeof(struct test)+ 16);
     memset(buf,0,sizeof(struct test)+ 16);

    strcpy(buf->ch, "hello world\n");
    puts(buf->ch);

    free(buf);  
    return 0;
}

4.3、修改ens33为eth0

(1)打开/etc/default/grub

sudo nano /etc/default/grub

(2)找到GRUB_CMDLINE_LINUX=" "改为GRUB_CMDLINE_LINUX=“net.ifnames=0 biosdevname=0”
(3)写入配置

sudo grub-mkconfig -o /boot/grub/grub.cfg

(4)重启系统

reboot

4.4、netmap下载安装

以ubuntu为例。
(1)切换到根目录:

cd /

(2)切换到root权限:

sudo su

(3)在根目录clone netmap:

git clone https://github.com/luigirizzo/netmap.git
正克隆到 'netmap'...
remote: Enumerating objects: 28670, done.
remote: Counting objects: 100% (978/978), done.
remote: Compressing objects: 100% (397/397), done.
remote: Total 28670 (delta 603), reused 867 (delta 533), pack-reused 27692
接收对象中: 100% (28670/28670), 10.13 MiB | 2.72 MiB/s, 完成.
处理 delta 中: 100% (18306/18306), 完成.

(4)安装编译环境:

apt-get install build-essential

(5)进入netmap/LINUX 目录

cd /netmap/LINUX/

(6)执行配置:

./configure

此过程会下载一些东西,然后提示耐心等待一段时间,过程有点久,请耐心等待,如下。

image.png

(7)编译和安装:

make && make install

此过程也需要耐心等待一段时间,过程有点久。

......

##install -D -m 644 ice.7.gz //usr/share/man/man7/ice.7.gz
/sbin/depmod -e -F /boot/System.map-4.15.0-142-generic  -a 4.15.0-142-generic
Updating initramfs...
update-initramfs -u
update-initramfs: Generating /boot/initrd.img-4.15.0-142-generic
make[1]: Leaving directory '/netmap/LINUX/ice-1.7.16/src'
make -C ixgbe install INSTALL_MOD_PATH= CFLAGS_EXTRA="-Wno-unused-but-set-variable -Wno-attributes -Wno-maybe-uninitialized -Wno-unused-variable -Wno-unused-label -I/netmap/LINUX -I/netmap/LINUX -I/netmap/LINUX/../sys -I/netmap/LINUX/../sys/dev -DCONFIG_NETMAP -Wno-unused-but-set-variable -g -DCONFIG_NETMAP_NULL -DCONFIG_NETMAP_PTNETMAP -DCONFIG_NETMAP_GENERIC -DCONFIG_NETMAP_MONITOR -DCONFIG_NETMAP_PIPE -DCONFIG_NETMAP_VALE" NETMAP_DRIVER_SUFFIX= KSRC=/lib/modules/4.15.0-142-generic/build KBUILD_EXTRA_SYMBOLS=/netmap/LINUX/Module.symvers
make[1]: Entering directory '/netmap/LINUX/ixgbe-5.3.8/src'
make[2]: Entering directory '/usr/src/linux-headers-4.15.0-142-generic'
  Building modules, stage 2.
  MODPOST 1 modules
make[2]: Leaving directory '/usr/src/linux-headers-4.15.0-142-generic'
Copying manpages...
Installing modules...
make[2]: Entering directory '/usr/src/linux-headers-4.15.0-142-generic'
  INSTALL /netmap/LINUX/ixgbe-5.3.8/src/ixgbe.ko
At main.c:160:
- SSL error:02001002:system library:fopen:No such file or directory: bss_file.c:175
- SSL error:2006D080:BIO routines:BIO_new_file:no such file: bss_file.c:178
sign-file: certs/signing_key.pem: No such file or directory
  DEPMOD  4.15.0-142-generic
make[2]: Leaving directory '/usr/src/linux-headers-4.15.0-142-generic'
Running depmod...
make[1]: Leaving directory '/netmap/LINUX/ixgbe-5.3.8/src'
make -C igb install INSTALL_MOD_PATH= CFLAGS_EXTRA="-DDISABLE_PACKET_SPLIT -fno-pie -I/netmap/LINUX -I/netmap/LINUX -I/netmap/LINUX/../sys -I/netmap/LINUX/../sys/dev -DCONFIG_NETMAP -Wno-unused-but-set-variable -g -DCONFIG_NETMAP_NULL -DCONFIG_NETMAP_PTNETMAP -DCONFIG_NETMAP_GENERIC -DCONFIG_NETMAP_MONITOR -DCONFIG_NETMAP_PIPE -DCONFIG_NETMAP_VALE" NETMAP_DRIVER_SUFFIX= KSRC=/lib/modules/4.15.0-142-generic/build KBUILD_EXTRA_SYMBOLS=/netmap/LINUX/Module.symvers
make[1]: Entering directory '/netmap/LINUX/igb-5.3.5.20/src'
make[2]: Entering directory '/usr/src/linux-headers-4.15.0-142-generic'
  Building modules, stage 2.
  MODPOST 1 modules
make[2]: Leaving directory '/usr/src/linux-headers-4.15.0-142-generic'
Copying manpages...
Installing modules...
make[2]: Entering directory '/usr/src/linux-headers-4.15.0-142-generic'
  INSTALL /netmap/LINUX/igb-5.3.5.20/src/igb.ko
At main.c:160:
- SSL error:02001002:system library:fopen:No such file or directory: bss_file.c:175
- SSL error:2006D080:BIO routines:BIO_new_file:no such file: bss_file.c:178
sign-file: certs/signing_key.pem: No such file or directory
  DEPMOD  4.15.0-142-generic
make[2]: Leaving directory '/usr/src/linux-headers-4.15.0-142-generic'
Running depmod...
make[1]: Leaving directory '/netmap/LINUX/igb-5.3.5.20/src'
make -C virtio_net.c install INSTALL_MOD_PATH= EXTRA_CFLAGS="-I/netmap/LINUX -I/netmap/LINUX -I/netmap/LINUX/../sys -I/netmap/LINUX/../sys/dev -DCONFIG_NETMAP -Wno-unused-but-set-variable -g -DCONFIG_NETMAP_NULL -DCONFIG_NETMAP_PTNETMAP -DCONFIG_NETMAP_GENERIC -DCONFIG_NETMAP_MONITOR -DCONFIG_NETMAP_PIPE -DCONFIG_NETMAP_VALE" NETMAP_DRIVER_SUFFIX= KSRC=/lib/modules/4.15.0-142-generic/build
make[1]: Entering directory '/netmap/LINUX/virtio_net.c'
make -C "/lib/modules/4.15.0-142-generic/build" M=/netmap/LINUX/virtio_net.c modules_install
make[2]: Entering directory '/usr/src/linux-headers-4.15.0-142-generic'
  INSTALL /netmap/LINUX/virtio_net.c/virtio_net.ko
At main.c:160:
- SSL error:02001002:system library:fopen:No such file or directory: bss_file.c:175
- SSL error:2006D080:BIO routines:BIO_new_file:no such file: bss_file.c:178
sign-file: certs/signing_key.pem: No such file or directory
  DEPMOD  4.15.0-142-generic
make[2]: Leaving directory '/usr/src/linux-headers-4.15.0-142-generic'
make[1]: Leaving directory '/netmap/LINUX/virtio_net.c'
make -C build-apps/dedup install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/dedup'
install -D dedup //usr/local/bin/dedup
install -D -m 644 /netmap/LINUX/../apps/lb/lb.8 //usr/local/share/man/man8/lb.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/dedup'
make -C build-apps/vale-ctl install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/vale-ctl'
install -D vale-ctl //usr/local/bin/vale-ctl
install -D -m 644 /netmap/LINUX/../apps/vale-ctl/vale-ctl.4 //usr/local/share/man/man4/vale-ctl.4
make[1]: Leaving directory '/netmap/LINUX/build-apps/vale-ctl'
make -C build-apps/nmreplay install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/nmreplay'
install -D nmreplay //usr/local/bin/nmreplay
install -D -m 644 /netmap/LINUX/../apps/nmreplay/nmreplay.8 //usr/local/share/man/man8/nmreplay.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/nmreplay'
make -C build-apps/tlem install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/tlem'
install -D tlem //usr/local/bin/tlem
install -D -m 644 /netmap/LINUX/../apps/tlem/tlem.8 //usr/local/share/man/man8/tlem.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/tlem'
make -C build-apps/lb install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/lb'
install -D lb //usr/local/bin/lb
install -D -m 644 /netmap/LINUX/../apps/lb/lb.8 //usr/local/share/man/man8/lb.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/lb'
make -C build-apps/bridge install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/bridge'
install -D bridge //usr/local/bin/bridge
install -D -m 644 /netmap/LINUX/../apps/bridge/bridge.8 //usr/local/share/man/man8/bridge.8
install -D bridge-b //usr/local/bin/bridge-b
install -D -m 644 /netmap/LINUX/../apps/bridge/bridge.8 //usr/local/share/man/man8/bridge.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/bridge'
make -C build-apps/pkt-gen install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-apps/pkt-gen'
install -D pkt-gen //usr/local/bin/pkt-gen
install -D -m 644 /netmap/LINUX/../apps/pkt-gen/pkt-gen.8 //usr/local/share/man/man8/pkt-gen.8
install -D pkt-gen-b //usr/local/bin/pkt-gen-b
install -D -m 644 /netmap/LINUX/../apps/pkt-gen/pkt-gen.8 //usr/local/share/man/man8/pkt-gen.8
make[1]: Leaving directory '/netmap/LINUX/build-apps/pkt-gen'
install -m 0644 -D /netmap/LINUX/../sys/net/netmap.h //usr/local/include/net/netmap.h
install -m 0644 -D /netmap/LINUX/../sys/net/netmap_user.h //usr/local/include/net/netmap_user.h
install -m 0644 -D /netmap/LINUX/../sys/net/netmap_virt.h //usr/local/include/net/netmap_virt.h
install -m 0644 -D /netmap/LINUX/../sys/net/netmap_legacy.h //usr/local/include/net/netmap_legacy.h
install -m 0644 -D /netmap/LINUX/../libnetmap/libnetmap.h //usr/local/include/libnetmap.h
install -D -m 644 /netmap/LINUX/../share/man/man4/netmap.4 //usr/local/share/man/man4/netmap.4
install -D -m 644 /netmap/LINUX/../share/man/man4/vale.4 //usr/local/share/man/man4/vale.4
install -D -m 644 /netmap/LINUX/../share/man/man4/ptnet.4 //usr/local/share/man/man4/ptnet.4
make -C build-libnetmap install SRCDIR=/netmap/LINUX/.. BUILDDIR=/netmap/LINUX DESTDIR="" PREFIX="/usr/local"
make[1]: Entering directory '/netmap/LINUX/build-libnetmap'
install -D libnetmap.a //usr/local/lib/libnetmap.a
make[1]: Leaving directory '/netmap/LINUX/build-libnetmap'

(8)使用netmap:

insmod netmap.ko

每次使用前都要执行insmod netmap.ko,它在/netmap/LINUX/路径下。
(9)检查netmap是否insmod成功:

ls /dev/netmap -l

出现如下表示成功:

crw------- 1 root root 10, 54 8月  31 12:53 /dev/netmap

(10)编译运行自己的代码

# 头文件 #include<net/netmap_user.h> 在 /netmap/sys/目录下
# 和/usr/local/include/net/目录下
gcc -o testcode testcode.c -I /netmap/sys/

4.5、协议栈实现代码示例

示例简单实现了arp、icmp、udp的协议栈;其他协议的实现类似。

//需要开启netmap的宏
#define NETMAP_WITH_LIBS

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#include <net/netmap_user.h>
#include <sys/poll.h>
#include <arpa/inet.h>

#pragma pack(1)//设置一字节对齐方式

#define ETH_ALEN        6
#define PROTO_IP        0x0800 //    IP 协议
#define PROTO_ARP        0x0806

#define PROTOCOL_UDP    17
#define PROTO_ICMP        1
#define PROTO_IGMP        2

#define ICMP_TYPE_ANS    0
#define ICMP_TYPE_REQ    8

#define ETHER_ADDR_LEN    6

#define MY_IP            "192.168.7.146"
#define MY_MAC            "00:0c:29:39:a8:c4"
// ether
struct etherhdr {
   
   
    unsigned char dst_mac[ETHER_ADDR_LEN];
    unsigned char src_mac[ETHER_ADDR_LEN];
    unsigned short protocol;
};

// IP
struct iphdr {
   
   
    unsigned char version : 4,
        hdrlen : 4;

    unsigned char tos;

    unsigned short totlen;

    unsigned short id;
    unsigned short flag : 3,
        offset : 13;
    unsigned char ttl;

    unsigned char protocol;

    unsigned short check;

    unsigned int sip;
    unsigned int dip;
};

// UDP
struct udphdr {
   
   
    unsigned short sport;
    unsigned short dport;
    unsigned short length;
    unsigned short check;
};

struct udppkt {
   
   
    struct etherhdr eth;
    struct iphdr ip;
    struct udphdr udp;

    unsigned char payload[0];// 零长数组

};

// ARP
struct arphdr{
   
   
    unsigned short h_type;
    unsigned short h_proto;
    unsigned char    h_addrlen;
    unsigned char    protolen;
    unsigned short    oper;
    unsigned char    smac[ETH_ALEN];
    unsigned int    sip;
    unsigned char    dmac[ETH_ALEN];
    unsigned int    dip;

};

struct arppkt {
   
   
    struct etherhdr eth;
    struct arphdr arp;
};

// ICMP
struct icmphdr {
   
   
    unsigned char type;
    unsigned char code;
    unsigned short check;
    unsigned short identifier;
    unsigned short sep;
    unsigned char    data[32];

};

struct icmppkt{
   
   
    struct etherhdr eth;
    struct iphdr ip;
    struct icmphdr icmp;

};


void echo_udp_pkt(struct udppkt *udp,struct udppkt *udp_rt)
{
   
   
    memcpy(udp_rt, udp, sizeof(struct udppkt));
    memcpy(udp_rt->eth.dst_mac, udp->eth.src_mac, ETH_ALEN);
    memcpy(udp_rt->eth.src_mac, udp->eth.dst_mac, ETH_ALEN);

    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;
}

unsigned short in_cksum(unsigned short *addr,int len)
{
   
   
    register int nleft = len;
    register unsigned short *w = addr;
    register int sum = 0;//32bit
    unsigned short answer = 0;//16bit

    while (nleft > 1)
    {
   
   
        sum += *w++;//16bit为一组累加
        nleft -= 2;
    }

    if (nleft == 1)//存在单个byte情况
    {
   
   
        *(u_char*)(&answer) = *(u_char*)w;
        sum += answer;
    }
    sum = (sum >> 16) + (sum & 0xffff);// 高16bit与低16bit相加
    sum += (sum >> 16);//防止值大于0xffff

    //结果
    answer = ~sum;

    return (answer);
}

void echo_icmp_pkt(struct icmppkt *icmp, struct icmppkt *icmp_rt)
{
   
   
    memcpy(icmp_rt, icmp, sizeof(struct icmppkt));

    memcpy(icmp_rt->eth.dst_mac, icmp->eth.src_mac, ETH_ALEN);
    memcpy(icmp_rt->eth.src_mac, icmp->eth.dst_mac, ETH_ALEN);

    icmp_rt->icmp.type = ICMP_TYPE_ANS;
    icmp_rt->icmp.code = 0;
    icmp_rt->icmp.check = 0;

    icmp_rt->ip.sip = icmp->ip.dip;
    icmp_rt->ip.dip = icmp->ip.sip;

    icmp_rt->icmp.check = in_cksum((unsigned short*)&icmp_rt->icmp, sizeof(struct icmphdr));


}


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 *hmac) {
   
   

    memcpy(arp_rt, arp, sizeof(struct arppkt));

    memcpy(arp_rt->eth.dst_mac, arp->eth.src_mac, ETH_ALEN);
    str2mac(arp_rt->eth.src_mac, hmac);
    arp_rt->eth.protocol = arp->eth.protocol;

    arp_rt->arp.h_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_ALEN);
    arp_rt->arp.dip = arp->arp.sip;

}

// netmap

int main()
{
   
   
    printf("length = %ld\n", sizeof(struct etherhdr));

    struct pollfd pfd = {
   
    0 };// poll
    struct nm_pkthdr h;
    struct etherhdr *eh;
    // 打开/dev/netmap,映射网卡数据到内存空间
    struct nm_desc *nmr = nm_open("netmap:eth0", NULL,0,NULL);

    if (nmr == NULL)
    {
   
   
        printf("netmap open fail!\n");
        return -1;
    }


    pfd.fd = nmr->fd;// 指向/dev/netmap
    pfd.events = POLLIN;//监听读事件

    while (1)
    {
   
   
        int ret = poll(&pfd, 1, -1);
        if (ret < 0)
            continue;

        if (pfd.events & POLLIN)//操作内存
        {
   
   
            unsigned char *stream = nm_nextpkt(nmr, &h);//从环形队列中取出一个数据包
            eh = (struct etherhdr *)stream;
            //将网络数据转换为本地字节序
            if (ntohs(eh->protocol) == PROTO_IP)
            {
   
   
                struct udppkt *pkt = (struct udppkt *)stream;
                if (pkt->ip.protocol == PROTOCOL_UDP)
                {
   
   
                    struct in_addr addr;
                    addr.s_addr = pkt->ip.sip;
                    // udp包length字段表示的是整个UDP包的总长度(包含udp的头长度)
                    int length = ntohs(pkt->udp.length);
                    printf("%s:%d:length:%d,ip length:%d\n", 
                        inet_ntoa(addr),
                        pkt->udp.sport,
                        length,
                        ntohs(pkt->ip.totlen));
                    pkt->payload[length - 8] = '\0';
                    printf("pkt: %s\n", pkt->payload);

                    struct udppkt udp_rt;
                    echo_udp_pkt(pkt, &udp_rt);
                    nm_inject(nmr, &udp_rt, sizeof(struct udppkt));

                }
                else if (pkt->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 == ICMP_TYPE_REQ)//0 代表应答 ICMP 报文、8 代表请求 ICMP 报文。
                    {
   
   
                        struct icmppkt icmp_rt = {
   
    0 };
                        echo_icmp_pkt(icmp, &icmp_rt);

                        nm_inject(nmr, &icmp_rt, sizeof(struct icmppkt));

                    }
                }
                else if (pkt->ip.protocol == PROTO_IGMP)
                {
   
   
                    printf("PROTO_IGMP packet\n");
                }
                else
                {
   
   
                    printf("other ip packet\n");
                }
            }
            else if (ntohs(eh->protocol) == PROTO_ARP)
            {
   
   
                struct arppkt *arp = (struct arppkt*)stream;
                struct arppkt arp_rt;
                if (arp->arp.dip == inet_addr(MY_IP))
                {
   
   
                    echo_arp_pkt(arp, &arp_rt, MY_MAC);
                    nm_inject(nmr, &arp_rt, sizeof(struct arppkt));
                }
            }
        }
    }


    return 0;
}

总结

要实现一个协议栈,需要清楚七层网络模型,熟悉协议标准;获得协议的原始数据包后需要一层层的拨开解析;发送数据之前需要将协议一层层的往下包装。
image.png
image.png

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
目录
相关文章
|
1月前
|
负载均衡 网络协议 算法
|
23天前
|
网络协议 网络安全 网络虚拟化
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算
本文介绍了十个重要的网络技术术语,包括IP地址、子网掩码、域名系统(DNS)、防火墙、虚拟专用网络(VPN)、路由器、交换机、超文本传输协议(HTTP)、传输控制协议/网际协议(TCP/IP)和云计算。通过这些术语的详细解释,帮助读者更好地理解和应用网络技术,应对数字化时代的挑战和机遇。
66 3
|
23天前
|
存储 网络协议 安全
30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场
本文精选了 30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场。
66 2
|
26天前
|
网络虚拟化
生成树协议(STP)及其演进版本RSTP和MSTP,旨在解决网络中的环路问题,提高网络的可靠性和稳定性
生成树协议(STP)及其演进版本RSTP和MSTP,旨在解决网络中的环路问题,提高网络的可靠性和稳定性。本文介绍了这三种协议的原理、特点及区别,并提供了思科和华为设备的命令示例,帮助读者更好地理解和应用这些协议。
40 4
|
1月前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
49 13
|
1月前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密
|
1月前
|
网络协议 算法 网络性能优化
计算机网络常见面试题(一):TCP/IP五层模型、TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议
计算机网络常见面试题(一):TCP/IP五层模型、应用层常见的协议、TCP与UDP的区别,TCP三次握手、四次挥手,TCP传输可靠性保障、ARQ协议、ARP协议
|
1月前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
287 2
|
1月前
|
编解码 安全 Linux
网络空间安全之一个WH的超前沿全栈技术深入学习之路(10-2):保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali——Liinux-Debian:就怕你学成黑客啦!)作者——LJS
保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali以及常见的报错及对应解决方案、常用Kali功能简便化以及详解如何具体实现
|
1月前
|
安全 网络协议 算法
网络空间安全之一个WH的超前沿全栈技术深入学习之路(8-1):主动信息收集之ping、Nmap 就怕你学成黑客啦!
网络空间安全之一个WH的超前沿全栈技术深入学习之路(8-1):主动信息收集之ping、Nmap 就怕你学成黑客啦!