LwIP分析#003#在Linux上运行LwIP的第三种方法

书接上文《LwIP分析之在Linux上运行LwIP的第二种方法》,第二种运行LwIP协议栈的方法,我们借用Linux TAP/TUN虚拟网卡,来联通本机Linux TCP/IP协议栈和LwIP TCP/IP协议栈,然后通过设置路由转发规则,可以达到我们在本机使用curl作为客户端,向运行在LwIP协议栈上的应用层httpd服务端发起请求,而httpd处理完http请求之后,又可以通过LwIP协议栈和TAP/TUN虚拟网卡,将数据返回给我们Linux协议栈上的curl程序。
但它的弊端是只能在本机上进行连通性测试,我们无法在其他主机上,向这个LwIP上的httpd发送请求。我们借助 Linux Bridge 来解决这个问题,也就是在Linux上运行LwIP的第三种方法。

方法三的基本原理

该方法,我们直接使用LwIP官方提供的contrib示例代码,contrib针对Linux的移植文件目录是lwip-STABLE-2_2_0_RELEASE/contrib/ports/unix。

  • 该方法的核心原理是,我们引入Linux Bridge,让Linux协议栈、Linux主机物理网卡、TAP/TUN虚拟网卡、LwIP协议栈、LwIP模拟网卡,都加入到这个Linux Bridge虚拟交换机中。然后将物理网卡的IP转交给Linux Bridge,让其来承担路由以及数据发送的工作。
  • 如果Linux物理网卡的IP地址是 192.168.0.62,我们选择一个没有使用的真实局域网地址,比如192.168.0.200,配置给LwIP模拟网卡。在网桥的作用下,其他主机也可以通过局域网直接访问192.168.0.200。
  • 其他主机的浏览器访问 http://192.168.0.200 时,通过arp协议可以获得192.168.0.200的MAC地址(该过程比较有意思,请读者自行分析),然后将数据发送到局域网中。
  • 这个时候,我们Linux主机的物理网卡捕捉到该数据帧,将该数据包转给Linux Bridge,而Linux Bridge就如同一个交换机一般,它根据MAC转发表,将该数据包转发给 tap0,tap0 又给了 LwIP的模拟网卡。LwIP模拟网卡获取到数据,然后将数据给了LwIP协议栈,然后在LwIP的协议栈中逐层流转和处理。
  • LwIP协议栈的数据链路层、网络层、传输层依次处理之后,到达应用层的httpd功能服务模块,httpd分析http请求处理之后,根据http请求,原路返回一段 html 文件。

其请求数据流转图,可使用下图描述:

Linux Bridge 的配置方法

第一步:Linux主机的物理网卡是ens33,它配置IP地址 192.168.0.62,这个IP可以被同局域网的其他主机访问。请记住你的主机IP,因为后面我们会将这个IP配置给Linux Bridge。

我们看下在配置网桥之前的网卡列表和路由表:

目前有一个实体网卡ens33,IP地址为 192.168.0.62,默认网关是 192.168.0.1。


第二步:将如下内容保存为 settapif.sh 文件,请注意替换你的主机IP地址以及默认网关IP地址

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

sudo ip link add name br0 type bridge
sudo ip link set dev br0 up

sudo ip tuntap add dev tap0 mode tap user `whoami`
sudo ip link set tap0 up
sudo ip link set tap0 master br0

sudo ip address add 192.168.0.62/24 dev br0
sudo ip route append default via 192.168.0.1 dev br0
sudo ip link set ens33 master br0
sudo ip address del 192.168.0.62/24 dev ens33

对上述脚本简单的说明:

  • sudo ip link add name br0 type bridgesudo ip link set dev br0 up

    创建名为 br0 的网桥,并启动它。

  • sudo ip tuntap add dev tap0 mode tap user whoamisudo ip link set tap0 up

    创建名为 tap0 的虚拟网卡,并启动它。

  • sudo ip link set tap0 master br0

    将 tap0 连接到 br0 上,通俗将 就是将 tap0 这个虚拟网卡原来连接Linux协议栈的一端,现在改连接到 br0 这个虚拟交换机的某个端口上,不再对接Linux协议栈。而 tap0 的还有一端,还是连接LwIP协议栈的模拟网卡上。

  • sudo ip address add 192.168.0.62/24 dev br0

    将原来物理网卡ens33的IP地址让给网桥,让网桥发挥网络接入功能。

  • sudo ip route append default via 192.168.0.1 dev br0

    将原来物理网卡ens33的默认网关地址,原模原样的配置给网桥。

  • sudo ip link set ens33 master br0

    将 ens33 连接到 br0 上,通俗将 ens33 这个物理网卡原来连接Linux协议栈的一端,现在改连接到 br0 这个虚拟交换机的某个端口上,不再对接Linux协议栈。而 ens33 的还有一端,还是保持对接原物理网线。

  • sudo ip address del 192.168.0.62/24 dev ens33

    进行完上述操作之后,其实原ens33物理网卡的IP已经失效,有没有已经无所谓,这里我们将其删除,以免引起误解。


第三步:我们打开仿真终端,将其放到服务器上的任意目录,在shell中使用如下命令使用这个配置脚本:

1
$ sudo ./settapif.sh

我们再看在配置网桥之后的网卡列表和路由表:

简单来说,有这么几个变化:

  • 增加了网桥 br0,该网桥拥有原物理网卡 ens33 的IP地址等信息。
  • 原 ens33 失去了IP地址,退化为虚拟网线。
  • 增加了虚拟网卡 tap0。
  • 路由表发生了变化,因为 ens33 失去了IP,所以Linux协议栈的路由功能由 br0 来承担。

至此,网桥、虚拟网卡和相应路由表已经准备妥当,我们下面开始编译和运行LwIP协议栈。

注意:如果你是在虚拟机上进行测试,请使用VMWare,在VirtualBox上该运行LwIP的方法行不通!

修改源代码相关配置

首先,先下载源代码并解压,使用如下命令:

1
$ wget https://github.com/lwip-tcpip/lwip/archive/refs/tags/STABLE-2_2_0_RELEASE.tar.gz

第一步:创建配置文件 lwipcfg.h。

我们在 lwip-STABLE-2_2_0_RELEASE/contrib/examples/example_app 目录下创建配置文件 lwipcfg.h ,本文复制同目录的 lwipcfg.h.example 文件。使用下面的命令:

1
2
3
$ cd lwip-STABLE-2_2_0_RELEASE/contrib/examples/example_app
$
$ cp lwipcfg.h.example lwipcfg.h

创建过程如下图所示:

第二步:修改配置文件 lwipcfg.h

第一处源文件内容:

1
2
3
/* If these 2 are not defined, the corresponding config setting is used */
/* #define USE_DHCP 0 */
/* #define USE_AUTOIP 0 */

第一处修改后内容:

1
2
3
/* If these 2 are not defined, the corresponding config setting is used */
#define USE_DHCP 0
#define USE_AUTOIP 0

目的是禁止使用DHCP和AUTOIP,服务使用我们自己指定的IP。


第二处源文件内容:

1
2
#define LWIP_PORT_INIT_IPADDR(addr)   IP4_ADDR((addr), 192,168,1,200)
#define LWIP_PORT_INIT_GW(addr) IP4_ADDR((addr), 192,168,1,1)

第二处修改后内容:

1
2
#define LWIP_PORT_INIT_IPADDR(addr)   IP4_ADDR((addr), 192,168,0,200)
#define LWIP_PORT_INIT_GW(addr) IP4_ADDR((addr), 192,168,0,1)

我们在本地局域网192.168.0.0/24网段上找一个类似 192.168.0.62 并且当前还未被占用的局域网真实IP地址比如 192.168.0.200 ,赋值给LwIP协议栈的模拟网卡。


第三处源文件内容:

1
#define LWIP_HTTPD_APP                0

第三处修改后内容:

1
#define LWIP_HTTPD_APP                1

目的是打开httpd服务,然后我们可以使用curl访问LwIP协议栈上的http服务。如果要测试其他功能,方法类似。


经过创建和修改 lwipcfg.h 配置文件之后,修改前和修改后的对比如下图所示:

大家也可以直接下载查看我配置的 lwipcfg.h

编译LwIP

配置文件修改完成之后,进入 lwip-STABLE-2_2_0_RELEASE/contrib/ports/unix/example_app 目录,开始编译LwIP。

1
2
3
$ cd lwip-STABLE-2_2_0_RELEASE/contrib/ports/unix/example_app
$
$ make

编译过程如下图所示:

编译完成之后,本地生成 example_app 和 makefsdata 两个可执行程序,其中 example_app 就是我们需要的测试程序。

运行LwIP

第一步:使用root权限,使用如下命令启动 example_app 服务端程序:

1
$ export PRECONFIGURED_TAPIF=tap0 ; sudo -E ./example_app

启动过程和结果如下图所示:

export PRECONFIGURED_TAPIF=tap0 做什么用?

  • 前文讲过,测试程序 contrib\ports\unix\port\netif\tapif.c 的 low_level_init 函数会调用 ifconfig 对 tap0 配置IP。
  • 而代码中配置的IP,是网关地址 192.168.0.1!可192.168.0.1是局域网已有的主机,而且是我们的默认路由地址。试想,如果把它给了tap0,必定在局域网上产生冲突,原本正常发给网关的数据,有可能会随机发给tap0,会引起局域网内的其他主机上网异常。
  • 其实在给 tap0 配置IP之前代码中有个判断逻辑,它先读取环境变量 PRECONFIGURED_TAPIF ,如果有这个环境变量说明网卡已经配置过,程序会忽略再次使用 ifconfig 配置网卡。
  • 所以我们启动程序时,先设置 PRECONFIGURED_TAPIF 这个环境变量,并且使用 sudo -E 来启动 example_app,这样 example_app 才能正确读取到这个环境变量,才能不去配置IP,从而避免上述问题。

第二步:在其他主机,比如Windows主机上打开浏览器,然后在浏览器中访问 http://192.168.0.200 然后会看到如下图:


好了,到此我们总结下,基于第二种方法,改进使用 Linux Bridge 解决了其他主机不能访问测试服务的问题。但很明显该方法的弊端就是使用繁琐,在运行 example_app 之前还需要设置网卡,所以我还是喜欢使用第二种方法。