本文分享自华为云社区《用户空间协议栈设计和netmap综合指南,将网络效率提升到新高度》,作者:Lion Long 。
协议概念
1.1、七层网络模型和五层网络模型
应用层: 最接近用户的一层,为用户程序提供网络服务。主要协议有HTTP、FTP、TFTP、SMTP、DNS、POP3、DHCP等。
表示层: 数据的表示、安全、压缩。管理数据的解密和加密。
会话层: 负责在网络中的两个节点之间的建立、维持和终止通信。
传输层: 模型中最重要的一层,负责传输协议的流控和差错校验。数据包离开网卡后进入的就是传输层;主要协议有:TCP、UDP等。
网络层: 将网络地址翻译成对应的物理地址。主要协议有:ICMP、IP等。
数据链路层: 建立逻辑连接、进行硬件地址寻址、差错校验等功能,解决两台相连主机之间的通信问题。主要协议有SLIP、以太网协议/MAC帧协议、ARP和RARP等。
物理层: 模型的最低层,建立、维护、断开物理连接,传输比特流。常见的物理媒介有光纤、电缆、中继器等。主要协议有RS232等。
1.2、以太网
以太网不是一种网络,而是一种局域网技术,它既有数据链路层内容,也有一些物理层内容。局域网技术除了以太网外,还有令牌环网、无线LAN/WAN等。
以太网的网线必须是双绞线,以太网中的所有主机共享一个通信通道; 当局域网中一台主机发送数据后,该局域网的所有设备都会收到该数据。因为共用一个通信通道,因此同一时刻只允许一台主机发送数据;如果同一时刻不只有一个主机发送数据,为避免干扰,该主机会执行碰撞避免算法(等待一段时间后再进行数据重发)。
以太网帧格式如下:
源地址和目的地址是指网卡MAC地址,长度是48 bit(6字节)。帧协议类型字段有三种,分别对应IP协议、ARP协议和RARP协议。帧末尾是CRC校验码。
定义一个以太网头结构体示例代码:
arduino
#define ETHER_ADDR_LEN 6
struct etherhdr {
unsigned char dst_mac[ETHER_ADDR_LEN];
unsigned char src_mac[ETHER_ADDR_LEN];
unsigned short protocol;
};
输出它的大小:
c
sizeof(struct etherhdr) = 14
1.3、IP协议
IP协议全称Internet Protocol,即网际互连协议,存在于网络层,负责数据在网络中传输。
IP协议格式如下:
sql
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协议头结构体示例代码:
arduino
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的数据格式如下:
lua
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协议头结构体示例代码:
arduino
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报文格式如下:
lua
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 |
+---------------------------------------------------------------+
定义一个ICMP协议头结构体示例代码:
arduino
// 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的目的是防止数据包因为不正确的路由表等原因造成无线循环而无法送达导致耗尽网络资源。
二、数据传输框图
网络上所有的数据传输都要经过网卡,网卡将模拟信号转换为数字信号,也就是将物理层信号转换为数据链路层信号。
注意:
(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字段即可。
示例代码:
ini
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
要实现一个协议栈,那么就需要获得原始的协议数据。
4.1、获取原始协议数据的方法
(1)raw socket,即原始套接字,可以接收本机网卡的数据帧或数据包。有四种方式创建这类socket。
(2)旁路。netmap、dpdk等
(3)hook。bpf、ebpf等
4.2、零长数组
零长数组,顾名思义,就是长度为零的数组。一般在GUN C中使用,其他编译器使用可能会报错或警告。
零长度数组的一个特点是它不占用内存存储空间。如下示例:
c
#include <stdio.h>
char test[0];
int main()
{
printf("size = %ld\n",sizeof(test));
return 0;
}
// 输出 为 0
在结构体中使用,它同样也不占内存:
c
#include <stdio.h>
struct test{
int len;
int ch[0];
};
int main(void)
{
printf("size of = %ld\n",sizeof(struct test));
return 0;
}
零长数组的使用:内存已经分配,但数据长度不确定,需要计算出数据长度的,就可以使用零长数组。零长数组在内存池中使用比较多。
使用示例:
c
#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
arduino
sudo nano /etc/default/grub
(2)找到GRUB_CMDLINE_LINUX=" "改为GRUB_CMDLINE_LINUX="net.ifnames=0 biosdevname=0"
(3)写入配置
bash
sudo grub-mkconfig -o /boot/grub/grub.cfg
(4)重启系统
reboot
4.4、netmap下载安装
以ubuntu为例。
(1)切换到根目录:
bash
cd /
(2)切换到root权限:
sudo su
(3)在根目录clone netmap:
bash
git clone https://github.com/luigirizzo/netmap.git
正克隆到 'netmap'...
makefile
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)安装编译环境:
arduino
apt-get install build-essential
(5)进入netmap/LINUX 目录:
bash
cd /netmap/LINUX/
(6)执行配置:
bash
./configure
此过程会下载一些东西,然后提示耐心等待一段时间,过程有点久,请耐心等待,如下。
(7)编译和安装:
go
make && make install
此过程也需要耐心等待一段时间,过程有点久。
bash
......
##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成功:
bash
ls /dev/netmap -l
出现如下表示成功:
bash
crw------- 1 root root 10, 54 8月 31 12:53 /dev/netmap
(10)编译运行自己的代码
shell
# 头文件 #include<net/netmap_user.h> 在 /netmap/sys/目录下
# 和/usr/local/include/net/目录下
gcc -o testcode testcode.c -I /netmap/sys/
4.5、协议栈实现代码示例
示例简单实现了arp、icmp、udp的协议栈;其他协议的实现类似。
ini
//需要开启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;
}
总结
要实现一个协议栈,需要清楚七层网络模型,熟悉协议标准;获得协议的原始数据包后需要一层层的拨开解析;发送数据之前需要将协议一层层的往下包装。