分享一款Linux平台下的tcp协议栈!超级透彻!


差一点

我们就擦肩而过了

有趣

有用

有态度



前言

tcp/ip协议栈相信在大家日常开发里面,或多或少都有接触过,在单片机领域比较常用的有以下几种开源的软件协议栈:

  • LwIP
  • uC/IP
  • uIP
  • TinyTcp

也有些产品直接使用硬件协议栈,如w5500。或者是其他的一些串口转wifi模块,如经典的esp8266等。

作为软件工程师,自然重点是关注软件协议栈。但是,哪怕是上面罗列这几种轻量级的tcp/ip协议栈,源码量都是在万行以上级别的,更别说Linux系统里面动则几十上百万行tcp协议栈了。tcp/ip协议是一个非常庞大的协议族,初学者在研究它的时候,往往容易在细节里面迷失,捉不到重点来学习,更谈不上整体把握。一般学着学着就被劝退了。

笔者在职业生涯前期也从过不少单片机开发工作,对上面的几个开源tcp协议栈也略有研究,只不过这些tcp协议栈主要运行在裸机或者是RTOS下,需要依赖相应的硬件开发板。目前本人主要从事于Linux行业,恰好Linux系統现在较为流行,于是给大家分享一个tcp协议栈--level-ip。level-ip可以直接运行在Linux系统中,不需要其他额外的硬件,希望手中有Linux系统的网络爱好者不要错过它

该tcp协议栈运行在Linux用户空间,与应用程序完全隔离。这个机制模拟了Linux内核的tcp协议栈,它们都可以为其他应用程序提供网络链接服务。比如说,它可以为curl应用程序提供抓取网页的功能。ps:这是借助Linux虚拟网卡实现的。

该tcp协议栈源码已经托管在GitHub:https://github.com/EmbedHacker/level-ip

GitHublevel-ip的README.md已经详细说明实现的具体功能,有兴趣的读者可以去了解一下,再决定要不要阅读本文。

level-ip架构设计

level-ip整体软件架构其实并不复杂,我们从数据的发送流程,可以轻松理解它的全部设计, 如下图所示:

下面详细给大家讲解一下,每层设计的功能

  • 应用程序:主要使用Socket API开发的应用程序,如我们常用的NetUtils工具(curl、wget等)

  • level-ip:运行在Linux用户空间的tcp/ip协议栈

  • 虚拟网卡/tun:中转站,把level-ip协议栈的数据转发给物理网卡,或者是把物理网卡的数据转发给level-ip协议栈

  • 物理网卡:收发以太网链路上的数据

软件运行原理

有了上面对level-ip的架构把握之后,我们从最底层开始讲解软件的运行原理。这样子分析下来,我们将对软件的底层到应用都有一个深入的把握。我们也将会在代码层面上,一窥tcp协议栈的核心理论的全貌。一旦在源码级别上,调试和掌握了tcp/ip协议栈,将带来极大的满足感和成就感,这种记忆是难以磨灭的。

我们先从底层开始,一步步先把这个软件的架构了解清楚,然后尝试把它跑起来,后面我们深入学习的时候,就能边阅读代码边调试了。

一、以太网=》物理网卡

检查Linux系统上的物理网卡,至少要保证能链接外网,否则虚拟网卡也难以借助物理网卡进行外网链接。

检测以太网和物理网卡链接是否正常的方法很简单,我们直接使用ping命令,测试是否能ping同百度网址即可:

ping www.baidu.com

如果能正常ping通,我们可以继续进行下一步,结果如下图:

如果不能ping通,请自行百度/谷歌解决。

二、物理网卡=》虚拟网卡

从物理网卡到虚拟网卡之间的数据转发,我们可以通过iptables工具来设置netfilter防火墙来实现。

(2.1) 首先,我们需要安装iptables工具,使用apt命令安装即可,如下图:

sudo apt install iptables

如果是系统自带iptables工具,则无需再安装,如下图:

(2.2) 接下来我们需要Linux系统充当一个路由器,来帮助我们实现物理网卡和虚拟网卡之间的数据传输。执行以下命令开启Linux路由器功能:

sudo sysctl -w net.ipv4.ip_forward=1

执行成功如下图:

(2.3) 继续配置防火墙,允许虚拟网卡的数据包通过,执行命令如下;

sudo iptables -I INPUT --source 10.0.0.0/24 -j ACCEPT

命令执行成功后,默认无信息输出

(2.4) 使用ifconfig -a命令查看物理网卡的接口名称,我这里的物理网卡是enp0s3,如下图:

(2.5) 配置 NAT,修改发送到互联网的数据包的源地址为enp0s3(物理网卡接口),执行以下命令:

sudo iptables -t nat -I POSTROUTING --out-interface enp0s3 -j MASQUERADE

注意:要把命令中的物理物理网卡接口,修改为自己真实机器上的接口。

(2.6) 先创建一个虚拟网卡设备,此时的虚拟网卡是一个新建的设备文件,现在并没有真正的虚拟网卡功能,后面会讲解虚拟网卡设备的生成原理

sudo mknod /dev/net/tap c 10 200sudo chmod 0666 /dev/net/tap

(2.7) 最后设置物理网卡enp0s3和虚拟网卡tap的数据相互转发,执行以下命令:

#物理网卡转发虚拟网卡sudo iptables -I FORWARD --in-interface enp0s3 --out-interface tap0 -j ACCEPT
#虚拟网卡转发物理网卡udo iptables -I FORWARD --in-interface tap0 --out-interface enp0s3 -j ACCEPT

到这里,物理网卡和虚拟网卡的数据转发就完成了。

三、虚拟网卡=》level-ip

Linux虚拟网卡介绍

虚拟网卡tun/tap驱动是一个开源项目,Linux内核在2.4以后的版本已经是默认编译tun/tap驱动到内核中去了。使用以下命令可以查看Linux系统中的虚拟网卡设备:

ls /dev/net/tun

结果如下图:

ps:如果你的Linux开发板中找不到这个虚拟网卡设备,那么需要重新编译你的Linux内核时,编译时检查是否已经配置了CONFIG_TUN宏。

tun/tap驱动原理分析

对驱动不感兴趣的读者,可跳过这一小节,无伤大雅!

顾名思义,tun/tap作为虚拟网卡驱动,肯定是不会直接使用物理网卡来收发数据的。那么它是如何巧妙地进行网络数据收发呢?这就要说起Linux系统下的设备驱动了。

Linux内核中有一个网络设备管理层,处于网络设备驱动和协议栈之间,负责衔接它们之间的数据交互。驱动不需要了解协议栈的细节,协议栈也不需要了解设备驱动的细节。

但是,我们这里不仅是希望教会大家怎么使用,更希望把虚拟网卡驱动的原理尽可能地说清楚,才能做到真正地了解透彻我们这个tcp协议栈的整体机制。

下面的内容,需要大家对Linux设备驱动有一定的基础,最起码要知道最简单的字符设备是怎么生成的,网络设备驱动的几个注册、使用接口等。如果不清楚如何通过open、write、read、close等接口来实现用户态和内核态的数据交互的话,建议先去百度/谷歌一下相关基础知识。

tun/tap驱动程序中有两条主线,我们先感性认识一下它,如下图:

 +----------+| internet |+----|-----++----|-----+|   eth0   |+----|-----++----|-----+|  bridge  |+----|-----++----|---------+|   tap0       ||--------------|| /dev/net/tun |+--|----|---|--+  poll  |   |   |  read  |   |    |  write+--|----|---|--+| my netstack  |+--------------+
  • 字符设备驱动,负责内核与用户态之间的tcp数据传送
  • 网卡设备驱动,负责内核tcp数据在物理链路的收发

上面的图可能不太直观,觉得抽象的话,可以直接看下面的图,总之先感性理解它的两条主线就可以了。

如下图所示:

下面以4.19.71内核源码进行分析:

我们先来看第一个主线--字符设备驱动

字符设备驱动

在内核源码的drivers/net目录下,存在一个tun.c文件,这个c文件就是tun/tap的驱动程序,我们先来看程序入口:

module_init(tun_init);module_exit(tun_cleanup);MODULE_DESCRIPTION(DRV_DESCRIPTION);MODULE_AUTHOR(DRV_COPYRIGHT);MODULE_LICENSE("GPL");MODULE_ALIAS_MISCDEV(TUN_MINOR);MODULE_ALIAS("devname:net/tun");

很明显,tun_init为驱动程序入口,该函数源码如下:

static int __init tun_init(void){	int ret = 0;
pr_info("%s, %s\n", DRV_DESCRIPTION, DRV_VERSION);
ret = rtnl_link_register(&tun_link_ops); if (ret) { pr_err("Can't register link_ops\n"); goto err_linkops; }
ret = misc_register(&tun_miscdev); if (ret) { pr_err("Can't register misc device %d\n", TUN_MINOR); goto err_misc; }
ret = register_netdevice_notifier(&tun_notifier_block); if (ret) { pr_err("Can't register netdevice notifier\n"); goto err_notifier; }
return 0;
err_notifier: misc_deregister(&tun_miscdev);err_misc: rtnl_link_unregister(&tun_link_ops);err_linkops: return ret;}

重点在第13行,通过misc_register()函数把tun/tap注册为杂类设备,它的参数tun_miscdev是一个杂类设备结构体,定义如下:

static struct miscdevice tun_miscdev = {	.minor = TUN_MINOR,	.name = "tun",	.nodename = "net/tun",	.fops = &tun_fops,};

在这个结构体中,tun_fops为tun/tap设备节点对应的文件访问接口,如下图:

static const struct file_operations tun_fops = {	.owner	= THIS_MODULE,	.llseek = no_llseek,	.read_iter  = tun_chr_read_iter,	.write_iter = tun_chr_write_iter,	.poll	= tun_chr_poll,	.unlocked_ioctl	= tun_chr_ioctl,#ifdef CONFIG_COMPAT	.compat_ioctl = tun_chr_compat_ioctl,#endif	.open	= tun_chr_open,	.release = tun_chr_close,	.fasync = tun_chr_fasync,#ifdef CONFIG_PROC_FS	.show_fdinfo = tun_chr_show_fdinfo,#endif};

这里我们重点关注tun_chr_open和tun_chr_ioctl,这两个函数是引出第二个主线--网络设备驱动的关键。

网络设备驱动

我们先来看tun_chr_open函数,这个函数做了两件重要的事情,一个是构建了真正的tun驱动文件tun_file,并把它的指针记录在/dev/net/tun文件中。

tfile = (struct tun_file *)sk_alloc(net, AF_UNSPEC, GFP_KERNEL, &tun_proto, 0);

接着初始化网络设备文件的真正操作接口:

tfile->socket.file = file;tfile->socket.ops = &tun_socket_ops;

tun_socket_ops为网络设备文件的收发数据接口:

static const struct proto_ops tun_socket_ops = {	.peek_len = tun_peek_len,	.sendmsg = tun_sendmsg,	.recvmsg = tun_recvmsg,};

再看tun_chr_ioctl函数

在tun_chr_ioctl函数里面,也做了两条重要的事情,分别是就是 调用tun_net_init函数来初始化网络设备的读写函数接口,然后调用regisre_netdev来注册生成相应的网络设备。

tun_chr_ioctl()->文件操作模式是否为TUNGETIFF->tun_get_iff()      ->tun_net_init()      ->register_netdevice()
使用虚拟网卡驱动

从上面的分析,我们已经知道了,我们实际上是通过控制字符设备,来注册使用网络设备的,虚拟网卡下面我们再梳理一下它的整个工作过程:

tun/tap设备驱动通过字符设备文件来实现数据从用户区的获取(open /read/write等)。发送数据时tun/tap设备也不是发送到物理链路,而是通过虚拟网络设备来进行发送,但因为网络设备是虚拟的,所以该网络设备的数据包只能在本机的其他物理网络设备上进行转发。

如下图:

localhost                   outside network   kernel stack                usermode stack [ ./tapip ]       |                         (10.0.0.1)       |                 (write) |        . (read)       |                        \|/      /|\       |                         '        |     tap0 <---- netif_rx() -----/dev/net/tun(10.0.0.2) `-- tun_net_xmit() ------------^

四、level-ip=》应用程序

应用程序中使用的Socket API会被我们自己写的动态库来接管,而不是直接使用内核的tcp服务。而在我们的动态库中,我们可以将通过本地的网络通信(socket unix域),来实现和level-ip的数据通信。

如下图所示:

体验level-ip跑起来

  1. 获取Level-IP源码

    git clone https://github.com/EmbedHacker/level-ip
  2. 安装libcap-dev工具,以后需要用它来修改可执行程序的权限

    sudo apt install libcab-dev
  3. 确保系统安装gcc、make工具后,编译所有的目标文件

    make all
  4. 开启路由功能

    sudo sysctl -w net.ipv4.ip_forward=1
  5. 接受虚拟网卡的输入信息

    sudo iptables -I INPUT --source 10.0.0.0/24 -j ACCEPT
  6. 伪装物理网卡的ip,注意enp0s3为物理网卡,用户应根据ifconfig命令的查询结果修改

    sudo iptables -t nat -I POSTROUTING --out-interface enp0s3 -j MASQUERADE
  7. 设置物理网卡数据转发给虚拟网卡,注意修改物理网卡!!

    sudo iptables -I FORWARD --in-interface enp0s3 --out-interface tap0 -j ACCEPT
  8. 设置虚拟网卡数据转发给物理网卡,注意修改物理网卡!!

    sudo iptables -I FORWARD --in-interface tap0 --out-interface enp0s3 -j ACCEPT
  9. 运行tcp协议栈

    ./lvl-ip
  10. 重新打开另一个端口,运行curl应用程序,抓取百度网址

    cd tools./level-ip ../apps/curl/curl www.baidu.com 80

如果curl程序和tcp协议栈正常运行,则抓取结果如下:

HTTP/1.1 200 OKAccept-Ranges: bytesCache-Control: no-cacheContent-Length: 14615Content-Type: text/htmlDate: Mon, 22 Jun 2020 14:03:20 GMTP3p: CP=" OTI DSP COR IVA OUR IND COM "P3p: CP=" OTI DSP COR IVA OUR IND COM "Pragma: no-cacheServer: BWS/1.1Set-Cookie: BAIDUID=EE1559CC045E03490CEE4C7D88B5DC37:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: BIDUPSID=EE1559CC045E03490CEE4C7D88B5DC37; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: PSTM=1592834600; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.comSet-Cookie: BAIDUID=EE1559CC045E03491F87D3CE34948683:FG=1; max-age=31536000; expires=Tue, 22-Jun-21 14:03:20 GMT; domain=.baidu.com; path=/; version=1; comment=bdTraceid: 1592834600066917530611219588553201043838Vary: Accept-EncodingX-Ua-Compatible: IE=Edge,chrome=1Connection: close
<!DOCTYPE html><!--STATUS OK--><html><head> <meta http-equiv="content-type" content="text/html;charset=utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=Edge"> <link rel="dns-prefetch" href="//s1.bdstatic.com"/> <link rel="dns-prefetch" href="//t1.baidu.com"/> <link rel="dns-prefetch" href="//t2.baidu.com"/> <link rel="dns-prefetch" href="//t3.baidu.com"/> <link rel="dns-prefetch" href="//t10.baidu.com"/> <link rel="dns-prefetch" href="//t11.baidu.com"/> <link rel="dns-prefetch" href="//t12.baidu.com"/> <link rel="dns-prefetch" href="//b1.bdstatic.com"/> <title>百度一下,你就知道</title> <link href="http://s1.bdstatic.com/r/www/cache/static/home/css/index.css" rel="stylesheet" type="text/css" />...

恭喜你把level-ip软件跑起来了,我将为它写更多的源码分析文章,敬请期待!

如果有兴趣了解更多level-ip的源码分析,欢迎关注我的公众号:embed linux share!










你的锅我不背!且看职责链模式

我想把你的x86当单片机玩

从RTOS到Linux0.12进阶之路

嵌入式,真的不需要单元测试?




陪伴是最长情的告白

为你推送最实用的编程知识

识别二维码

关注我们



在看~

捧个人场行~