news 2026/9/23 4:37:37

Mongoose 内置 TCP/IP 协议栈接入 Linux TAP 虚拟网卡:驱动实现与三种组网实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Mongoose 内置 TCP/IP 协议栈接入 Linux TAP 虚拟网卡:驱动实现与三种组网实战
  • 嵌入式
  • 网络
  • 通信
  • 物联网

【免费下载链接】mongoose

Embedded web server, with TCP/IP network stack, MQTT and Websocket

项目地址:https://gitcode.com/gh_mirrors/mon/mongoose
点击查看免费下载

导读

本指南围绕 Mongoose 内置 TCP/IP 协议栈(Mongoose Embedded Network Stack)在 Linux TUN/TAP 设备上的接入实战展开:示例程序通过标准 socket 接口读写一个 TAP 设备,让 Mongoose 的 HTTP/HTTPS、DHCP、SNTP 等网络能力直接跑在一个虚拟网卡之上。文章完整覆盖 TAP 接口的创建、三种典型的宿主机组网方式(纯虚拟接口、桥接到局域网、转发与 NAT 伪装)以及背后的驱动回调与栈事件原理。读完本文,你将掌握如何把 Mongoose 内置协议栈挂到任意"能收发以太网帧"的字符设备上,并能在自己的 Linux 工作站上复现一套完整的嵌入式 Web 服务环境。

示例概览:为什么用 TAP 接口跑 Mongoose

tutorials/tcpip/tap-driver/是一个把 Mongoose 内置 TCP/IP 协议栈 接入 Linux TUN/TAP 设备的完整示例,核心思路非常直接:

  • Mongoose 内置协议栈只关心"能否从底层拿到原始以太网帧"——只要实现了收发帧的驱动回调,它就能独立完成 ARP、IP、TCP/UDP、DHCP 客户端、DNS 与 SNTP 请求等全套网络工作,不依赖操作系统协议栈;
  • TUN/TAP 是 Linux 提供的虚拟网络设备,其中TAP 工作在二层(以太网帧),恰好与 Mongoose 内置栈的 L2 接口天然匹配;
  • 示例使用标准 socket 接口open/read/write/ioctl)打开并读写 TAP 设备,因此既不需要内核模块,也不需要第三方驱动,make即可运行。

示例程序(main.c)在启动后会监听两个端口:

Mongoose version : v<MG_VERSION> Listening on : http://0.0.0.0/ Listening on : https://0.0.0.0/

这里的HTTP_URL/HTTPS_URL由 Makefile 通过编译宏注入,默认值为http://0.0.0.0/https://0.0.0.0/(见 net.h 中的回退定义http://0.0.0.0:8000https://0.0.0.0:8443)。也就是说,一旦 TAP 接口就绪,这台"虚拟设备"就同时提供明文 HTTP 与 TLS HTTPS 服务。

构建配置要点

Makefile 中有一段关键编译选项:

CFLAGS_MONGOOSE += -DMG_ENABLE_LINES=1 -DMG_ENABLE_TCPIP=1 -DMG_ENABLE_SOCKET=0 -DMG_ENABLE_TCPIP_DRIVER_INIT=0

其含义是:

编译宏作用
MG_ENABLE_TCPIP=1启用 Mongoose 内置 TCP/IP 协议栈(即net_builtin.c实现的那套协议)
MG_ENABLE_SOCKET=0关闭对操作系统 socket API 的依赖,网络完全由内置栈接管
MG_ENABLE_TCPIP_DRIVER_INIT=0禁止栈自动调用驱动init,因为本示例自己负责打开 TAP 设备
MG_ENABLE_LINES=1日志输出附带源码文件名与行号,便于调试

启动示例时如果不带任何参数,它默认打开名为tap0的接口、使用 MAC 地址02:00:01:02:03:77。可用的命令行参数(解析逻辑见 main.c):

参数说明默认值
-i ifcname指定要打开的 TAP 接口名称tap0
-mac XX:XX:XX:XX:XX:XX指定网卡 MAC 地址02:00:01:02:03:77
-v level设置日志级别(mg_log_setMG_LL_*默认值决定

接口的创建由示例之外完成(见下文 Linux 配置),示例只是"打开"一个已存在的接口;接口会随示例退出而消失,这正是文档建议"先建好接口、再做 DHCP/联网准备"的原因。

源码级解读:TAP 驱动与内置栈的对接方式

驱动接口:三个回调函数

Mongoose 内置栈通过struct mg_tcpip_driver与硬件/虚拟设备解耦,其定义在 mongoose.h:

struct mg_tcpip_driver { bool (*init)(struct mg_tcpip_if *); // One-time hardware init; return false on failure size_t (*tx)(const void *, size_t, struct mg_tcpip_if *); // Transmit one raw frame; return bytes sent size_t (*rx)(void *buf, size_t len, struct mg_tcpip_if *); // Read one raw frame; return frame size, 0 if none bool (*poll)(struct mg_tcpip_if *, bool); // Check link status; second arg is true once per second };

TAP 示例在 main.c 中实现了其中三个(initMG_ENABLE_TCPIP_DRIVER_INIT=0被跳过,改由主函数自行完成):

  • tap_tx:调用write()把一帧原始以太网数据写入 TAP 设备文件描述符,写入失败时打印MG_ERROR并返回 0;
  • tap_rx:调用read()从 TAP 设备读取一帧;读取后usleep(1)让出 CPU,避免轮询把单核跑到 100%(注释原文 "This is to avoid 100% CPU");
  • tap_poll:轮询链路状态——return s1 && ifp->driver_data ? true : false;,即只要每秒一次的周期 tick 到来且驱动数据(文件描述符)有效,就认为链路 UP。

随后把这三个回调组装进struct mg_tcpip_driver,连同文件描述符一起塞进struct mg_tcpip_if

struct mg_tcpip_driver driver = {.tx = tap_tx, .poll = tap_poll, .rx = tap_rx}; struct mg_tcpip_if mif = {.driver = &driver, .driver_data = &fd, .enable_req_dns = true, .enable_req_sntp = true, .fn = mif_fn};

driver_data指向 TAP 设备的文件描述符,驱动回调通过ifp->driver_data取回它——这是 Mongoose 驱动体系的标准做法,任何外部硬件(W5500、w5100、PHY 等,见 src/drivers 目录)都遵循同一模式。

打开 TAP 设备的细节

main.c 展示了 Linux 与 OpenBSD 两种打开方式:

#ifndef __OpenBSD__ const char *tuntap_device = "/dev/net/tun"; #else const char *tuntap_device = "/dev/tap0"; #endif int fd = open(tuntap_device, O_RDWR); ... ifr.ifr_flags = IFF_TAP | IFF_NO_PI; // Linux: TAP 模式,去掉额外的 4 字节包头 if (ioctl(fd, TUNSETIFF, (void *) &ifr) < 0) { ... } ... fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); // 非阻塞模式

关键点:

  • Linux 下统一打开字符设备/dev/net/tun,用TUNSETIFFioctl 把 fd 绑定到指定名称的 TAP 接口;
  • IFF_TAP表示二层以太网设备,IFF_NO_PI去掉 Linux 默认附加的 4 字节包信息头,让 Mongoose 收到的就是纯以太网帧;
  • 随后用O_NONBLOCK设为非阻塞,这样tap_rx在无数据时read返回负值,驱动返回 0 即可。

内置栈初始化与事件回调

mg_tcpip_init(&mgr, &mif)完成栈的初始化。从 src/net_builtin.c 的实现可以看到初始化时的几个自动行为:

  • mif.ip == 0(未配置静态 IP)且 L2 类型不是 PPP/PPPoE,则自动开启 DHCP 客户端(ifp->enable_dhcp_client = true)——这就是为什么示例默认依赖 DHCP 分配地址;
  • 未设置dhcp_name时默认使用主机名"mip"参与 DHCP 请求;
  • 未指定recv_queue.size时,轮询式驱动(有rx回调)默认队列大小等于framesize
  • mg_tcpip_init会调用driver->init(本示例已通过编译宏禁用,改由用户代码打开设备)。

示例还注册了网络事件回调mif_fn(main.c),处理两类事件:

if (ev == MG_TCPIP_EV_STATE_CHANGE) { MG_INFO(("State change: %u", *(uint8_t *) ev_data)); } else if (ev == MG_TCPIP_EV_DHCP_DNS) { MG_INFO(("Got DNS from DHCP: %M", mg_print_ip4, (uint32_t *) ev_data)); } else if (ev == MG_TCPIP_EV_DHCP_SNTP) { ... // 构造 udp://<sntp-ip>:123 的 SNTP 服务器地址 }

MG_TCPIP_EV_*事件族在 mongoose.h 中有完整定义,本示例用到的有:

事件ev_data 类型含义
MG_TCPIP_EV_STATE_CHANGEuint8_t *(指向ifp->stateIPv4 链路状态变化
MG_TCPIP_EV_DHCP_DNSuint32_t *DHCP 分配了 DNS 服务器
MG_TCPIP_EV_DHCP_SNTPuint32_t *DHCP 分配了 SNTP 服务器

链路状态机取值(MG_TCPIP_STATE_*,见 mongoose.h):DOWN(0) →LINK_UP(1) →UP(2) →REQ(3,等待 DHCP offer) →IP(4,已获 IP、解析网关 MAC) →READY(5,完全就绪)。测试代码 test/mip_tap_test.c 正是轮询mif.state == MG_TCPIP_STATE_READY来判断协议栈是否完全可用。

DHCP 请求参数:DNS 与 SNTP

示例把enable_req_dnsenable_req_sntp都置为true,这意味着 DHCP 客户端会在请求中携带选项 6(DNS)与选项 42(SNTP)。对应实现见 src/net_builtin.c(DHCP 请求构造时追加*p++ = 6; *p++ = 42;)以及 src/net_builtin.c(解析 DHCP 响应后触发MG_TCPIP_EV_DHCP_DNS/MG_TCPIP_EV_DHCP_SNTP事件)。示例在收到 SNTP 服务器后,还会动态拼出udp://<ip>:123地址供 SNTP 同步时间使用(并在 net.c 中通过定时器周期性调用mg_sntp_connecttime.google.com同步)。

示例自带的 Web 应用

示例的"业务层"在 net.c 中实现,它监听 HTTP 与 HTTPS 两个端口(mg_http_listen),并实现了完整的 REST API:

  • POST /api/login/POST /api/logout:基于用户名口令或 access_token 的登录态管理(authenticateAuthorization: BasicCookie: access_token中取凭据),登录成功通过 HttpOnly Cookie 下发 token,TLS 连接下 Cookie 还会带Secure属性;
  • GET /api/stats/get:返回温度、湿度等模拟遥测数据;
  • GET /api/events/get:分页返回模拟告警事件流(每页 20 条,最多 400 条);
  • GET/POST /api/settings/get|set:读取/修改设备设置(日志开关、日志级别、亮度、设备名);
  • POST /api/firmware/upload:通过mg_ota_begin/mg_ota_write/mg_ota_end演示固件在线升级(OTA);
  • 其余路径由mg_http_serve_dir提供静态文件服务:工作站上读取web_root目录,嵌入式环境则改用mg_fs_packed打包文件系统。

这些 API 与 Mongoose 的 HTTP 路由、JSON 解析(mg_json_get_*)、格式化输出(mg_xprintf%M自定义打印)深度绑定,可作为在 TAP 网络里验证端到端连通性的现成测试目标。

Linux 端准备:创建 TAP 接口

首先创建并启用一个 TAP 接口(示例默认使用tap0):

$ sudo ip tuntap add dev tap0 mode tap $ sudo ip link set tap0 up

注意事项:

  • 某些系统上示例程序需要超级用户权限才能打开该设备;
  • 也可以在创建接口时直接授予用户/组访问权限,例如sudo ip tuntap add dev tap0 mode tap user yourusernamegroup yourgroup
  • 接口生命周期:如果由示例自行创建接口,进程退出后接口会随之消失;而网络准备工作(DHCP 服务器、对外连接)通常需要提前就位,因此文档强烈建议先手动建好接口、再启动示例——这样接口会一直存在,DHCP 服务器等设施可以常驻。

组网方式一:纯虚拟接口(仅宿主机可达)

这种模式下,Mongoose 只能从宿主机访问,但后续可以随时叠加转发与 NAT。步骤:

  1. 给 TAP 接口分配 IP:
    $ sudo ip addr add 192.168.0.1/24 dev tap0
  2. 在该接口上启动一个为192.168.0.0/24子网服务的 DHCP 服务器;或者反过来,给 Mongoose 配置该子网内的固定地址(静态 IP 场景下在struct mg_tcpip_if中设置mif.ipmif.maskmif.gw,参考 test/mip_tap_test.c 的写法);
  3. 启动示例,打开同一个 TAP 接口:
    $ make -C tutorials/tcpip/tap-driver/ clean all

    如果接口名不是tap0,追加ARGS="-i ifcname"(使用实际接口名)。

拓扑示意:

[DHCP server for 192.168.0.x] 192.168.0.1 192.168.0.x ┌─────────┐ │ tap0 ├──────────────────── tap-driver └─────────┘

由于这种模式下宿主机之外的主机都无法访问该接口,DNS 等必要服务需要同样部署在宿主机上,并在 Mongoose 侧配置正确(DHCP 下发的 DNS 由enable_req_dns自动获取)。这是后续所有组网方案的基础形态。

组网方式二:桥接到局域网

如果你已经有虚拟网桥(例如用于连接多个虚拟机的br0/virbr0),可以把 TAP 接口挂到网桥上,Mongoose 将获得整个局域网的访问权,同时也能被工作台和网内其他主机访问。

准备网桥(没有网桥时)

把物理网卡(NIC)纳入网桥时,IP 地址要从网卡迁移到网桥接口上;如果走 DHCP,DHCP 客户端也要运行在网桥接口而不是网卡上。示例(网卡enp9s0,原 IP10.1.0.10/24):

$ sudo ip link add virbr0 type bridge $ sudo ip link set virbr0 up $ sudo ip addr del 10.1.0.10/24 dev enp9s0 $ sudo ip link set enp9s0 master virbr0 $ sudo ip addr add 10.1.0.10/24 dev virbr0

ifconfig检查,并尝试 ping 网内主机验证桥接是否生效。

把 TAP 挂到网桥

$ sudo ip link set tap0 master virbr0

注意:此时tap0的状态在示例尚未打开它之前会显示为 DOWN(NO-CARRIER),这是正常现象:

$ ip link show master virbr0 2: enp9s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master virbr0 state UP mode DEFAULT group default qlen 1000 link/ether 30:5a:3a:08:db:90 brd ff:ff:ff:ff:ff:ff 6: tap0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast master virbr0 state DOWN mode DEFAULT group default qlen 1000 link/ether 66:91:e2:5f:d7:ed brd ff:ff:ff:ff:ff:ff

启动示例

$ make -C tutorials/tcpip/tap-driver/ clean all

接口名不同时同样追加ARGS="-i ifcname"。一切正常的话,示例运行后tap0会变为UPLOWER_UP出现):

$ ip link show master virbr0 2: enp9s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master virbr0 state UP mode DEFAULT group default qlen 1000 link/ether 30:5a:3a:08:db:90 brd ff:ff:ff:ff:ff:ff 6: tap0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master virbr0 state UP mode DEFAULT group default qlen 1000 link/ether 66:91:e2:5f:d7:ed brd ff:ff:ff:ff:ff:ff

拓扑示意:

10.1.0.10 virbr0 │ ┌──────────────────┴──────────────────┐ │ │ 10.1.0.x │ ┌────────┐ │ │ │ tap0 ├─────────────┼─────── tap-driver │ └────────┘ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ vnet1 │ │ enp9s0 │ │ vnet2 │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ └──────┼───────────┼───────────┼──────┘ │ │ │ │ Ethernet │ Local LAN 10.1.0.x │ DHCP server, router ▼ Internet

桥接模式下无需像方式一那样手动加 IP,除非你的局域网里没有 DHCP 服务器——那时才需要给 Mongoose 配一个子网内的固定地址。

注意:如果宿主机运行着 Docker,它引入的防火墙规则可能会破坏桥接(Docker 默认的 FORWARD 链策略会拦截桥接流量)。另外,桥接通常无法用于 Wi-Fi,原因见文末附录。

组网方式三:转发与 NAT(Masquerade)

当虚拟接口已有 IP 时,可以让 Linux 充当 NAT/转发网关,把 Mongoose 的流量转发到另一张接口(例如上网用的enp9s0)。这种模式下 Mongoose 能访问 Internet,工作台也能访问它,但局域网内其他主机访问不到(如需隔离场景非常合适)。

  1. 配置伪装与转发(前提:你已经配好了防火墙):
    $ sudo iptables -t nat -A POSTROUTING -o enp9s0 -j MASQUERADE $ echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward

    注意把enp9s0换成实际的出网接口名。

  2. 启动示例:
    $ make -C tutorials/tcpip/tap-driver/ clean all

    接口名不同时追加ARGS="-i ifcname"

拓扑示意:

[DHCP server for 192.168.0.x] ┌────────────► 192.168.0.1 192.168.0.x │ forwarding ┌─────────┐ │ masquerading │ tap0 ├──────────────────── tap-driver │ └─────────┘ ▼ Your IP (LAN / Public) ┌─────────┐ │ enp9s0 │ └────┬────┘ │ │ Ethernet [WiFi] │ ▼ Internet

变体:NIC 本身已在网桥中

如果你的物理网卡是网桥的一部分(例如你在跑虚拟机),则用网桥接口做伪装而不是物理网卡(此时 IP 属于网桥而不是网卡):

$ sudo iptables -t nat -A POSTROUTING -o virbr0 -j MASQUERADE $ echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward

记得使用正确的网桥接口名。

拓扑示意:

[DHCP server for 192.168.0.x] 10.1.0.10 ◀──────────────► 192.168.0.1 192.168.0.x virbr0 forwarding ┌─────────┐ │ masquerading │ tap0 ├──────────────────── tap-driver │ └─────────┘ ┌──────────────────┴──────────────────┐ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ vnet1 │ │ enp9s0 │ │ vnet2 │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ └──────┼───────────┼───────────┼──────┘ │ │ │ │ Ethernet │ Local LAN 10.1.0.x │ DHCP server, router ▼ Internet

同样地,Wi-Fi 场景下桥接通常不可用,参见附录。

附录:为什么 Wi-Fi 上桥接通常行不通

由于无线标准的工作方式,桥接往往要求能够深入配置接入点/无线路由器(AP),原因在于地址字段的差异:

  • 以太网帧只需要 Sender Address(SA)与 Destination Address(DA)两个 MAC 地址;
  • Wi-Fi 额外要求 Transmitter Address(TA)与 Receiver Address(RA)字段,一共 4 个 MAC 地址;
  • 常见场景(STA 到 AP 的帧 TA = SA,AP 到 STA 的帧 DA = RA)实际上只用了 3 个地址,因此在 AP 上做桥接通常没有问题——AP 只需为 AP→STA 帧改 SA、为 STA→AP 帧改 DA,仍然只用 3 个地址;
  • 在 STA(无线工作站)上做桥接时:STA→AP 帧的 SA 与 TA 不再相等,AP→STA 帧的 DA 与 RA 也不同,必须用满 4 个地址——而大多数 STA 与 AP 默认都不支持这种模式。STA 侧可能需要配置工具开启4addr模式,AP 侧则类似两台 LAN 桥接的场景,需要启用 WDS(Wireless Distribution System)。即便两侧都能配置,由于不同厂商实现方式存在兼容性问题,仍然无法保证一定可用。

因此本文档的三种组网方式中,桥接方案默认面向有线网络;无线环境请优先考虑方式一(纯虚拟接口)或方式三(转发 + NAT)。

延伸阅读

  • 示例入口与 TAP 驱动实现:tutorials/tcpip/tap-driver/main.c、tutorials/tcpip/tap-driver/net.c
  • 驱动与接口结构体、事件定义:mongoose.h
  • 内置协议栈核心实现:src/net_builtin.c(mg_tcpip_init见 src/net_builtin.c)
  • 官方测试中基于同一 TAP 驱动做协议栈测试与 TCP 丢包重传验证:test/mip_tap_test.c
  • 同一思路的抓包版驱动示例(基于 pcap 读原始帧):tutorials/tcpip/pcap-driver
  • 更多内置栈驱动的参考实现(W5500、W5100、PHY 等):src/drivers
  • 嵌入式
  • 网络
  • 通信
  • 物联网

【免费下载链接】mongoose

Embedded web server, with TCP/IP network stack, MQTT and Websocket

项目地址:https://gitcode.com/gh_mirrors/mon/mongoose
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 4:37:35

搞定程卫考试:3步搞定报名避坑指南含完整示例

搞定程卫考试:3步搞定报名避坑指南含完整示例 报错一堆看不懂 StackTrace,是不是觉得脑子都要炸了?别慌,搞水利工程的兄弟们,咱们今天不聊那些虚的,直接上硬菜。 很多刚入行或者想跳槽的朋友,卡在“程卫”这个词上。其实,“程卫”在这里指代的是 注册安全工程师(水利水电方向) 或者行业内俗称的…

作者头像 李华
网站建设 2026/9/23 4:37:32

3个坑让生存方舟进化手机游戏代码跑不通,最佳实践救场

3个坑让生存方舟进化手机游戏代码跑不通,最佳实践救场 复制来的生存方舟进化手机游戏源码,直接运行就报 NullPointerException 或者资源加载失败?别急着骂人,90% 的初学者都卡在“环境不一致”和“异步时序”上。很多博主只贴 Happy…

作者头像 李华
网站建设 2026/9/23 4:36:50

5个坑搞懂艺术签名生成器,附速查手册

5个坑搞懂艺术签名生成器,附速查手册 刚拿到“艺术签名生成器”这道面试题时,你是不是也懵了?看着屏幕上滚动的红字报错,StackTrace 长得像天书,脑子里一片空白。别慌,这种把前端 Canvas 绘图、字体渲染和后端数据持久化揉在一起的题目,专治各种“手生”。我整理了这份 速查手册…

作者头像 李华
网站建设 2026/9/23 4:36:43

3个波纹特效坑让你项目崩盘?这份保姆级教程救急

3个波纹特效坑让你项目崩盘?这份保姆级教程救急 学会 CSS 动画语法,却不知怎么在真实项目里搭起波纹效果?这简直是很多前端新手的噩梦。别慌,这篇保姆级教程专治各种“看着会,一写就废”的疑难杂症。 咱们不整虚的,直接上干货。在掘金技术社区搜“CSS…

作者头像 李华
网站建设 2026/9/23 4:36:35

梦幻西游辅助新手避坑:3个底层原理让你看懂自动化

梦幻西游辅助新手避坑:3个底层原理让你看懂自动化 你刚啃完《Python基础教程》,觉得循环、函数都懂了,结果想写个简单的梦幻西游辅助脚本,连个自动挂机的框架都搭不起来?这种“懂语法却不会搭项目”的断崖式落差,正是无数 新手避坑…

作者头像 李华