Wi-Fi Direct教程 01:在 Ubuntu 搭建双 mac80211_hwsim + wpa_supplicant 2.12

Wi-Fi Direct教程 01:在 Ubuntu 搭建双 mac80211_hwsim + wpa_supplicant 2.12

摘要:Ubuntu Host 提供双 mac80211_hwsim radio,Docker 固化 wpa_supplicant 2.12 源码、构建依赖和 F12/F5 调试环境,并说明 nl80211/Netlink 边界。

文章目录

  • [Wi-Fi Direct教程 01:在 Ubuntu 搭建双 mac80211_hwsim + wpa_supplicant 2.12](#Wi-Fi Direct教程 01:在 Ubuntu 搭建双 mac80211_hwsim + wpa_supplicant 2.12)
    • [1. 工程结构与固定路径](#1. 工程结构与固定路径)
    • [2. 在 Ubuntu Host 创建两个 hwsim radio](#2. 在 Ubuntu Host 创建两个 hwsim radio)
      • [2.1 用 modinfo 确认 kernel 是否提供 mac80211_hwsim](#2.1 用 modinfo 确认 kernel 是否提供 mac80211_hwsim)
      • [2.2 用 modprobe 加载 module 并创建两个虚拟 radio](#2.2 用 modprobe 加载 module 并创建两个虚拟 radio)
      • [2.3 确认 module 已经实际加载](#2.3 确认 module 已经实际加载)
      • [2.4 后续每次实验统一用 Host 脚本恢复 hwsim](#2.4 后续每次实验统一用 Host 脚本恢复 hwsim)
    • [3. 为什么 wpa_supplicant 需要通过 Linux Netlink 子系统通信](#3. 为什么 wpa_supplicant 需要通过 Linux Netlink 子系统通信)
      • [3.1 wpa_supplicant 在 userspace,Wi-Fi driver 在 kernel](#3.1 wpa_supplicant 在 userspace,Wi-Fi driver 在 kernel)
      • [3.2 Wi-Fi Direct 为什么尤其需要这条控制路径](#3.2 Wi-Fi Direct 为什么尤其需要这条控制路径)
      • [3.3 Netlink、nl80211、libnl 三个名字不要混在一起](#3.3 Netlink、nl80211、libnl 三个名字不要混在一起)
    • [4. Dockerfile 固化 wpa_supplicant 2.12 与 Debug 环境](#4. Dockerfile 固化 wpa_supplicant 2.12 与 Debug 环境)
      • [4.1 apt-get update 和 apt-get install 分别做什么](#4.1 apt-get update 和 apt-get install 分别做什么)
      • [4.2 Image 中安装了哪些工具](#4.2 Image 中安装了哪些工具)
    • [5. Compose 连接 Host hwsim 与 Docker userspace](#5. Compose 连接 Host hwsim 与 Docker userspace)
    • [6. 构建并启动 Container](#6. 构建并启动 Container)
      • [6.1 生成 .env](#6.1 生成 .env)
      • [6.2 先展开 Compose 配置](#6.2 先展开 Compose 配置)
      • [6.3 构建 Image](#6.3 构建 Image)
      • [6.4 检查 Container 状态](#6.4 检查 Container 状态)
    • [7. 从 Container 验证两个 hwsim radio](#7. 从 Container 验证两个 hwsim radio)
    • [8. 每次实验都必须先启动两个独立 wpa_supplicant 2.12 实例](#8. 每次实验都必须先启动两个独立 wpa_supplicant 2.12 实例)
      • [8.1 Terminal A:启动 A 端 wpa_supplicant](#8.1 Terminal A:启动 A 端 wpa_supplicant)
      • [8.2 Terminal B:启动 B 端 wpa_supplicant](#8.2 Terminal B:启动 B 端 wpa_supplicant)
      • [8.3 第三个 Terminal:用 wpa_cli 验证 control interface](#8.3 第三个 Terminal:用 wpa_cli 验证 control interface)
    • [9. 为什么推荐使用 Dev Containers 阅读 2.12 源码](#9. 为什么推荐使用 Dev Containers 阅读 2.12 源码)
      • [9.1 Reopen in Container 实际移动了什么](#9.1 Reopen in Container 实际移动了什么)
      • [9.2 不使用 Dev Containers 能不能在 Host 上 F12](#9.2 不使用 Dev Containers 能不能在 Host 上 F12)
      • [9.3 使用 Dev Containers 与 F12](#9.3 使用 Dev Containers 与 F12)
      • [9.4 Comment Translate 在 Dev Container 中如何使用](#9.4 Comment Translate 在 Dev Container 中如何使用)
    • [10. 使用 F5 + GDB 调试同一份源码](#10. 使用 F5 + GDB 调试同一份源码)
    • [11. 日常启动只需要两层操作](#11. 日常启动只需要两层操作)
    • [12. GitHub Actions:CI、GitHub Pages 与 Docker Image CD](#12. GitHub Actions:CI、GitHub Pages 与 Docker Image CD)
    • [13. 最终环境关系](#13. 最终环境关系)
    • 关键源码索引
    • 资料来源
      • [S1 Linux mac80211_hwsim](#[S1] Linux mac80211_hwsim)
      • [S2 Linux Netlink / cfg80211 / nl80211](#[S2] Linux Netlink / cfg80211 / nl80211)
      • [S3 hostap 2.12 release Git tag](#[S3] hostap 2.12 release Git tag)
      • [S4 Docker Engine Host networking](#[S4] Docker Engine Host networking)
      • [S5 Visual Studio Code Dev Containers / C++ Debugging](#[S5] Visual Studio Code Dev Containers / C++ Debugging)
      • [S6 GitHub Actions 文档](#[S6] GitHub Actions 文档)

实验环境固定分成两层:Ubuntu Host 负责 Linux kernel 与 mac80211_hwsim,Docker 负责 wpa_supplicant 2.12 userspace、源码、编译依赖和调试工具。本文固定使用下面的工程路径:

text 复制代码
Ubuntu Host: /home/wdfk/share/wifi-direct
Container:   /workspace
text 复制代码
Ubuntu x86 VM
│
├── Linux kernel
│   └── mac80211_hwsim radios=2
│       ├── virtual radio A -> WLAN interface A
│       └── virtual radio B -> WLAN interface B
│
└── Docker
    └── wifi-direct-dev
        ├── wpa_supplicant 2.12 source
        ├── wpa_supplicant 2.12 Debug binary
        ├── wpa_cli 2.12
        ├── iw / ip / tcpdump
        ├── GDB / gdbserver
        ├── compile_commands.json
        └── VS Code Dev Containers(推荐)

mac80211_hwsim 是 Linux kernel module。Linux Wireless 官方文档将它定义为用于模拟 IEEE 802.11 radio 的内核模块,并提供 radios 参数控制虚拟 radio 数量。S1Docker Image 不携带一个脱离 Host kernel 独立工作的 mac80211_hwsim.ko;Container 只通过 Host network namespace 使用 Host 已经创建好的虚拟无线接口。

这样既保留了 hwsim 对 Linux 内核的真实依赖,又能把容易产生版本差异的 wpa_supplicant 2.12、依赖、源码、Debug symbols 和 VS Code 配置固定下来。

1. 工程结构与固定路径

工程在 Ubuntu Host 上固定放在:

text 复制代码
/home/wdfk/share/wifi-direct

终端提示符通常类似:

text 复制代码
wdfk@wdfk-ubuntu24:~/share/wifi-direct$

其中 ~ 代表当前用户 home,即 /home/wdfk,因此 ~/share/wifi-direct 与 /home/wdfk/share/wifi-direct 是同一个目录。

Compose 把整个工程 bind mount 到 Container:

text 复制代码
Host:      /home/wdfk/share/wifi-direct
             |
             | bind mount
             v
Container: /workspace

工程结构如下:

text 复制代码
wifi-direct/
├── Dockerfile
├── compose.yaml
├── .env.example
├── README.md
│
├── scripts/
│   └── host-hwsim-up.sh
│
├── .devcontainer/
│   └── devcontainer.json
│
├── .vscode/
│   ├── c_cpp_properties.json
│   ├── launch.json
│   └── settings.json
│
├── config/
│   ├── p2p-a.conf
│   └── p2p-b.conf
│
├── .github/
│   └── workflows/
│       ├── ci.yml
│       ├── deploy-pages.yml
│       └── publish-image.yml
│
├── mkdocs.yml
├── requirements-docs.txt
│
└── docs/
    ├── index.md
    └── Wi-Fi_Direct教程_01_在Ubuntu搭建双mac80211_hwsim_wpa_supplicant_2.12.md

wpa_supplicant 2.12 源码直接构建进 Image,固定位置为。上游官方发布页当前也将 2.12 列为 release 版本。S3

text 复制代码
/opt/wifi-direct/src/wpa_supplicant-2.12

Debug binary 固定为:

text 复制代码
/usr/local/bin/wpa_supplicant-2.12

源码、Debug binary 与 compile_commands.json 来自同一次 Image build,后续 F12 和 F5 因此都针对同一份 2.12 源码。

2. 在 Ubuntu Host 创建两个 hwsim radio

以下命令都在 Ubuntu VM 的 Host Terminal 执行,不在 Container 中执行。

先进入固定工程目录:

bash 复制代码
cd /home/wdfk/share/wifi-direct

cd 是 change directory,用于切换当前工作目录。后续 docker compose 会默认读取当前目录里的 compose.yaml,因此先进入工程根目录。

日常复现实验时,不需要每次重新手敲 modinfo / modprobe。项目现在提供 Host-only 脚本:

bash 复制代码
./scripts/host-hwsim-up.sh

它负责检查当前 kernel 是否提供 mac80211_hwsim、必要时以 radios=2 加载 module,并确认 iw dev 至少能看到两个无线 interface。这个脚本必须在 Ubuntu Host 执行,不能在 Container 中执行。 后面的 2.1~2.3 仍保留手工命令,是为了把脚本背后的 kernel module 生命周期讲清楚。

2.1 用 modinfo 确认 kernel 是否提供 mac80211_hwsim

执行:

bash 复制代码
modinfo mac80211_hwsim

modinfo 是 module information,用于查看一个 Linux kernel module 的元信息,例如:

text 复制代码
模块文件路径
license
作者
依赖
支持的参数

它只是查询,不会把 module 加载进 kernel。

这里执行 modinfo mac80211_hwsim 的目的只有一个:确认当前 Ubuntu kernel 对应的模块目录中确实存在 mac80211_hwsim。

如果输出中能看到类似:

text 复制代码
filename: .../mac80211_hwsim.ko...

即可继续。

2.2 用 modprobe 加载 module 并创建两个虚拟 radio

执行:

bash 复制代码
sudo modprobe mac80211_hwsim radios=2

这条命令拆开看:

部分 含义
sudo 以管理员权限执行,因为加载 kernel module 会修改正在运行的 Linux kernel 状态
modprobe 加载 Linux kernel module,并自动处理它声明的模块依赖
mac80211_hwsim 要加载的无线仿真 module
radios=2 传给 module 的参数,请求创建两个虚拟 IEEE 802.11 radio

这一步真正发生在 Ubuntu Host kernel:

text 复制代码
sudo modprobe mac80211_hwsim radios=2
                    |
                    v
              Linux kernel
                    |
                    v
            mac80211_hwsim
             /            \
        virtual radio A  virtual radio B

Docker 此时还没有参与。

2.3 确认 module 已经实际加载

执行:

bash 复制代码
lsmod | grep '^mac80211_hwsim'

其中:

  • lsmod:列出当前已经加载进 kernel 的 modules;
  • |:shell pipe,把左侧命令输出交给右侧;
  • grep '^mac80211_hwsim':只保留以 mac80211_hwsim 开头的行。

modinfo 回答"系统有没有这个 module",lsmod 回答"这个 module 现在有没有真正加载"。两个命令不要混淆。

还可以直接查看 kernel 注册的无线 PHY:

bash 复制代码
ls -1 /sys/class/ieee80211

正常情况下应出现两个 phy 目录,例如:

text 复制代码
phy0
phy1

如果模块已经以其它参数加载,需要重新创建两个 radio,应先停止占用测试 interface 的进程,然后执行:

bash 复制代码
sudo modprobe -r mac80211_hwsim
sudo modprobe mac80211_hwsim radios=2

modprobe -r 中的 -r 表示 remove,即卸载 module。卸载会删除该 module 创建的虚拟 radio/interface,因此不要在实验进程仍然占用它们时直接执行。

从此处开始固定边界:

text 复制代码
Ubuntu Host:
    modinfo
    modprobe / modprobe -r
    mac80211_hwsim 生命周期

Docker Container:
    wpa_supplicant / wpa_cli / iw / tcpdump / GDB
    不执行 modprobe
    不挂载 /lib/modules
    不拥有 SYS_MODULE

如果 modinfo mac80211_hwsim 提示 module 不存在,应先解决当前 Host kernel 对应模块缺失问题,再继续 Docker 环境。

2.4 后续每次实验统一用 Host 脚本恢复 hwsim

VM 重启、Host 重启或主动执行 modprobe -r mac80211_hwsim 后,hwsim radio 会消失。重新 build/recreate Docker 不会把它们创建回来,因为 module 生命周期属于 Host kernel。

实验统一把下面命令作为前置步骤:

bash 复制代码
cd /home/wdfk/share/wifi-direct
./scripts/host-hwsim-up.sh

正常情况下脚本会输出当前可见的 WLAN interface,并以 ready 结束。如果 module 已经以其它 radios 数量加载,脚本不会贸然卸载正在使用的 module,而是提示先停止相关 wpa_supplicant,再显式执行:

bash 复制代码
./scripts/host-hwsim-up.sh --reload

这样可以把"恢复 Host radio"固化为一条命令,同时保留 Host/Container 权限边界。

Dockerfile 中会安装:

text 复制代码
libnl-3-dev
libnl-genl-3-dev

启动 wpa_supplicant 时还会显式使用:

bash 复制代码
-Dnl80211

这三处配置都来自同一个 Linux 无线控制边界。

3.1 wpa_supplicant 在 userspace,Wi-Fi driver 在 kernel

wpa_supplicant 是 userspace daemon。真正管理无线硬件或虚拟 radio 的 cfg80211、mac80211 和 driver 位于 Linux kernel。

因此 userspace 中的 wpa_supplicant 不能像普通 C 函数那样直接调用 kernel 内部函数:

text 复制代码
错误理解:

wpa_supplicant
    -> 直接调用 cfg80211 某个 C 函数

userspace 和 kernel 之间必须经过 Linux 提供的用户态 ABI/API 边界。

现代 Linux 无线配置使用的核心边界就是:

text 复制代码
Netlink
  + Generic Netlink
      + nl80211 family

Linux kernel 文档把 Netlink 描述为 userspace 与 kernel 双向交换结构化消息的一套 socket 机制;nl80211 则是 Linux 802.11 wireless subsystem 暴露给 userspace 的 Generic Netlink family。cfg80211 在 kernel 内把这个统一接口继续连接到 mac80211 或具体 wireless driver。S2

3.2 Wi-Fi Direct 为什么尤其需要这条控制路径

wpa_supplicant 需要 kernel/driver 执行大量无法靠普通应用 socket 完成的无线管理动作,例如:

text 复制代码
扫描无线环境
切换/选择信道
remain-on-channel
发送/接收 management frame
创建或管理无线 interface
接收异步无线事件

这些动作最终都需要从 userspace 进入 Linux wireless subsystem。

本系列会逐步看到下面的真实分层:
#mermaid-svg-bvyURzfEa453xRbz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bvyURzfEa453xRbz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bvyURzfEa453xRbz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bvyURzfEa453xRbz .error-icon{fill:#552222;}#mermaid-svg-bvyURzfEa453xRbz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bvyURzfEa453xRbz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bvyURzfEa453xRbz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bvyURzfEa453xRbz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bvyURzfEa453xRbz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bvyURzfEa453xRbz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bvyURzfEa453xRbz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bvyURzfEa453xRbz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bvyURzfEa453xRbz .marker.cross{stroke:#333333;}#mermaid-svg-bvyURzfEa453xRbz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bvyURzfEa453xRbz p{margin:0;}#mermaid-svg-bvyURzfEa453xRbz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bvyURzfEa453xRbz .cluster-label text{fill:#333;}#mermaid-svg-bvyURzfEa453xRbz .cluster-label span{color:#333;}#mermaid-svg-bvyURzfEa453xRbz .cluster-label span p{background-color:transparent;}#mermaid-svg-bvyURzfEa453xRbz .label text,#mermaid-svg-bvyURzfEa453xRbz span{fill:#333;color:#333;}#mermaid-svg-bvyURzfEa453xRbz .node rect,#mermaid-svg-bvyURzfEa453xRbz .node circle,#mermaid-svg-bvyURzfEa453xRbz .node ellipse,#mermaid-svg-bvyURzfEa453xRbz .node polygon,#mermaid-svg-bvyURzfEa453xRbz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bvyURzfEa453xRbz .rough-node .label text,#mermaid-svg-bvyURzfEa453xRbz .node .label text,#mermaid-svg-bvyURzfEa453xRbz .image-shape .label,#mermaid-svg-bvyURzfEa453xRbz .icon-shape .label{text-anchor:middle;}#mermaid-svg-bvyURzfEa453xRbz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bvyURzfEa453xRbz .rough-node .label,#mermaid-svg-bvyURzfEa453xRbz .node .label,#mermaid-svg-bvyURzfEa453xRbz .image-shape .label,#mermaid-svg-bvyURzfEa453xRbz .icon-shape .label{text-align:center;}#mermaid-svg-bvyURzfEa453xRbz .node.clickable{cursor:pointer;}#mermaid-svg-bvyURzfEa453xRbz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bvyURzfEa453xRbz .arrowheadPath{fill:#333333;}#mermaid-svg-bvyURzfEa453xRbz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bvyURzfEa453xRbz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bvyURzfEa453xRbz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bvyURzfEa453xRbz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bvyURzfEa453xRbz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bvyURzfEa453xRbz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bvyURzfEa453xRbz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bvyURzfEa453xRbz .cluster text{fill:#333;}#mermaid-svg-bvyURzfEa453xRbz .cluster span{color:#333;}#mermaid-svg-bvyURzfEa453xRbz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bvyURzfEa453xRbz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bvyURzfEa453xRbz rect.text{fill:none;stroke-width:0;}#mermaid-svg-bvyURzfEa453xRbz .icon-shape,#mermaid-svg-bvyURzfEa453xRbz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bvyURzfEa453xRbz .icon-shape p,#mermaid-svg-bvyURzfEa453xRbz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bvyURzfEa453xRbz .icon-shape .label rect,#mermaid-svg-bvyURzfEa453xRbz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bvyURzfEa453xRbz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bvyURzfEa453xRbz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bvyURzfEa453xRbz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} wpa_supplicant 2.12
driver_nl80211
libnl / Generic Netlink
nl80211
cfg80211
mac80211 / mac80211_hwsim

在当前 hwsim 实验中,最下层不是实体 Wi-Fi 芯片,而是:

text 复制代码
mac80211_hwsim

因此这套环境仍然能保留非常关键的一段真实 Linux 无线控制链。

3.3 Netlink、nl80211、libnl 三个名字不要混在一起

可以先这样记:

名称 当前文章中的定位
Netlink Linux userspace 与 kernel 交换结构化消息的 socket 机制
Generic Netlink Netlink 上用于注册和组织不同 kernel family 的通用框架
nl80211 Linux 802.11 无线控制使用的 Generic Netlink family
libnl userspace C library,帮助程序创建、发送、解析 Netlink/Generic Netlink 消息
driver_nl80211 wpa_supplicant 内部对 Linux nl80211 接口的 driver backend

所以 Dockerfile 安装 libnl-3-dev / libnl-genl-3-dev,不是因为"Wi-Fi Direct 协议本身依赖 libnl",而是因为当前 Linux 平台上的 wpa_supplicant 需要通过 driver_nl80211 使用 Linux 的 nl80211/Generic Netlink 控制接口。

当前只需要建立 userspace、nl80211/cfg80211 与 driver 之间的边界。

4. Dockerfile 固化 wpa_supplicant 2.12 与 Debug 环境

4.1 apt-get update 和 apt-get install 分别做什么

下面两件事含义不同:

text 复制代码
apt-get update
    -> 下载/刷新 Ubuntu 软件源的 package index
    -> 不等于升级系统,也不负责安装下面这些工具

apt-get install ...
    -> 根据刚刷新的 index 真正安装 package

-y 表示自动回答安装确认;--no-install-recommends 表示只安装必需依赖,不额外拉入大量推荐软件。

构建最后执行:

bash 复制代码
rm -rf /var/lib/apt/lists/*

删除只在 build 阶段使用的 APT package index,减少 Docker Image layer 体积。

4.2 Image 中安装了哪些工具

Package 作用 本项目为什么需要
build-essential GCC/G++、make 等基础编译工具集合 编译 wpa_supplicant 2.12
pkg-config 查询开发库的 include/lib 编译参数 构建时定位 libnl、OpenSSL 等依赖
libnl-3-dev libnl 核心 Netlink 开发头文件/库 driver_nl80211 构建所需的 userspace Netlink API
libnl-genl-3-dev libnl Generic Netlink 开发库 nl80211 属于 Generic Netlink family
libssl-dev OpenSSL 开发头文件/库 wpa_supplicant 的加密/认证相关实现依赖
libreadline-dev GNU Readline 开发库 改善 wpa_cli 交互式命令编辑
iw Linux nl80211 无线管理工具 查看 phy、interface、无线能力和事件
iproute2 提供 ip 等现代 Linux 网络工具 查看 interface/address/route
iputils-ping 提供 ping 通用 IP 连通性验证
rfkill 查看/控制无线设备 block 状态 排查 radio 被 soft/hard block
tcpdump 抓包工具 观察网络数据包
gdb GNU Debugger VS Code F5 源码调试
gdbserver GDB 远程调试端 为后续远程/分离式调试保留能力
bear 记录真实 compiler invocation 生成 compile_commands.json,供 F12/IntelliSense 使用
git Git 客户端 对照 upstream、commit 和源码历史
less 分页查看长文本 查看日志和源码输出
vim Terminal 编辑器 临时查看/修改配置
procps 提供 ps、pgrep 等 检查 wpa_supplicant 进程
sudo 受控提权 Container 内执行需要管理员权限的无线/GDB 操作
curl HTTP/HTTPS 下载 资料获取和网络调试;wpa_supplicant 源码由 git clone 获取
ca-certificates HTTPS CA 根证书 校验 git clone https://git.w1.fi/hostap.git 等 HTTPS 连接证书

其中最容易误解的是两个 libnl package:它们不是"网络基础知识练习工具",而是 Linux driver_nl80211 userspace 构建链的一部分。原因见上一节。

5. Compose 连接 Host hwsim 与 Docker userspace

当前 compose.yaml:

这里最关键的是 network_mode: host。Docker 官方文档说明 host network mode 让容器共享主机网络命名空间而不获得独立网络栈。S4

mac80211_hwsim 在 Host kernel 中创建 interface;如果 Container 使用默认独立 network namespace,Container 通常只看到自己的 Docker veth/lo,而不是 Host 上那组测试 WLAN interface。

设置:

yaml 复制代码
network_mode: host

后,实验 Container 与 Host 共用 network namespace,所以 Container 内:

bash 复制代码
iw dev

才能看到 Host hwsim 创建的 interface,并由 wpa_supplicant -Dnl80211 对它们执行无线控制操作。

三个 capability 的用途也不同:

text 复制代码
NET_ADMIN
    -> 网络/无线管理操作,例如需要管理权限的 nl80211 command

NET_RAW
    -> raw packet / tcpdump 等实验能力

SYS_PTRACE
    -> GDB ptrace 调试进程

这里没有:

text 复制代码
SYS_MODULE
/lib/modules bind mount

因为 Container 不负责加载 Host kernel module。

6. 构建并启动 Container

所有命令重新回到 Ubuntu Host,并确保当前目录是:

bash 复制代码
cd /home/wdfk/share/wifi-direct

6.1 生成 .env

执行:

bash 复制代码
printf 'LOCAL_UID=%s\nLOCAL_GID=%s\nWIFI_DIRECT_IMAGE=wifi-direct-lab:2.12\nP2P_A=wlan0\nP2P_B=wlan1\n' \
    "$(id -u)" "$(id -g)" > .env

这条命令把两类参数写入 Compose 自动读取的 .env:

  • LOCAL_UID / LOCAL_GID:让 Container 中 dev 用户尽量与 Host 用户身份一致;
  • WIFI_DIRECT_IMAGE:固定本地开发 Image 名;
  • P2P_A / P2P_B:为实验命令提供稳定的逻辑接口变量,默认分别为 wlan0 / wlan1。

查看:

bash 复制代码
cat .env

例如:

text 复制代码
LOCAL_UID=1000
LOCAL_GID=1000
WIFI_DIRECT_IMAGE=wifi-direct-lab:2.12
P2P_A=wlan0
P2P_B=wlan1

这里需要区分"变量名"和"真实 WLAN interface 名"。Compose 只是把 P2P_A、P2P_B 注入 Container,并不会把 Linux kernel 中的网卡强制改名为 wlan0、wlan1。如果后面的 iw dev 实际显示其他名称,修改 .env 中这两项,然后重新创建 Container:

bash 复制代码
docker compose up -d --force-recreate

6.2 先展开 Compose 配置

bash 复制代码
docker compose config

docker compose config 不启动 Container,它会读取:

text 复制代码
compose.yaml
+.env

然后输出变量替换后的最终 Compose 配置。这里可以提前确认 UID/GID、Image 名和 network_mode: host 是否符合预期。

6.3 构建 Image

首次准备环境时执行:

bash 复制代码
docker compose build

docker compose build 根据当前目录中的 Dockerfile 构建 wifi-direct-lab:2.12 Image,但不会启动 Container。后续只有 Dockerfile 或构建参数发生变化时才需要重新构建。

构建成功后启动:

bash 复制代码
docker compose up -d

其中:

  • up:创建并启动 Compose service;
  • -d:detached,后台运行。

以后 Dockerfile 没有变化时,日常只需要 docker compose up -d。

6.4 检查 Container 状态

bash 复制代码
docker compose ps

应能看到 wifi-direct-dev 为 Up/running 状态。

进入 Container:

bash 复制代码
docker compose exec wifi-direct-dev bash

exec 表示在已经运行的 service Container 中再启动一个命令;这里启动的是交互式 Bash。

进入后先检查开发用户,确认 UID :

bash 复制代码
id

应看到当前用户为 dev,UID/GID 与 .env 对应。

然后检查版本和调试产物:

bash 复制代码
wpa_supplicant-2.12 -v
wpa_cli-2.12 -v
gdb --version | head -n 1
iw --version

printf '%s\n' "$WPA_SUPPLICANT_SRC"
test -f "$WPA_SUPPLICANT_SRC/wpa_supplicant/main.c" && echo 'source: OK'
test -f "$WPA_SUPPLICANT_SRC/wpa_supplicant/compile_commands.json" && echo 'compile_commands: OK'
test -x "$WPA_SUPPLICANT_BIN" && echo 'binary: OK'
readelf -S "$WPA_SUPPLICANT_BIN" | grep -E '\.debug_(info|line)'

这里:

  • test -f:检查普通文件是否存在;
  • test -x:检查文件存在且具有 executable 权限;
  • &&:只有左侧检查成功才执行右侧 echo;
  • readelf -S:列 ELF section;
  • grep:筛选 .debug_info、.debug_line 等 DWARF section。

wpa_supplicant 应报告:

text 复制代码
wpa_supplicant v2.12

看到 .debug_info / .debug_line 后,说明当前 binary 保留了源码级 GDB 调试信息。

7. 从 Container 验证两个 hwsim radio

仍在 Container 中执行:

bash 复制代码
iw phy
iw dev

iw 是 Linux 无线配置/查询工具,它本身也是 nl80211 的 userspace client。

两条命令分别关注不同对象:

text 复制代码
iw phy
    -> 查看 wireless PHY / wiphy 能力
    -> 更接近"radio/无线硬件能力"这一层

iw dev
    -> 查看当前创建出的 wireless interface
    -> 后面 wpa_supplicant 的 -i 参数就使用这里看到的 interface 名

正常情况下应看到两个 Wiphy 与对应 interface,例如:

text 复制代码
phy#1
    Interface wlan1

phy#0
    Interface wlan0

实际 interface 名可能不是 wlan0 / wlan1,后续始终以 iw dev 输出为准。

这一步同时验证了两件事:

text 复制代码
Host kernel
    mac80211_hwsim radios=2
        |
        +-- radio/interface A
        +-- radio/interface B

Container userspace
    network_mode: host
        |
        +-- iw 通过 nl80211 能看到同一组 interface

如果 Host 已加载 module,但 Container 中 iw dev 看不到同一组 interface,先回到 Host 执行:

bash 复制代码
docker compose config

确认确实展开为:

yaml 复制代码
network_mode: host

不要为了解决"Container 看不到 interface"而在 Container 中再次 modprobe mac80211_hwsim。

8. 每次实验都必须先启动两个独立 wpa_supplicant 2.12 实例

这里是阶段 0/1 最容易遗漏的运行时步骤:Docker Container 处于 Up 并不代表 A/B 两个 wpa_supplicant 已经运行。 compose.yaml 的 command 只是 sleep infinity,因此每次重启/重建 Container 后,都必须重新打开 Terminal A、B 启动两个 daemon,再在 Terminal C 使用 wpa_cli。

固定顺序如下:
#mermaid-svg-HPILR7Aq0gYkZopA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HPILR7Aq0gYkZopA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HPILR7Aq0gYkZopA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HPILR7Aq0gYkZopA .error-icon{fill:#552222;}#mermaid-svg-HPILR7Aq0gYkZopA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HPILR7Aq0gYkZopA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HPILR7Aq0gYkZopA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HPILR7Aq0gYkZopA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HPILR7Aq0gYkZopA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HPILR7Aq0gYkZopA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HPILR7Aq0gYkZopA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HPILR7Aq0gYkZopA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HPILR7Aq0gYkZopA .marker.cross{stroke:#333333;}#mermaid-svg-HPILR7Aq0gYkZopA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HPILR7Aq0gYkZopA p{margin:0;}#mermaid-svg-HPILR7Aq0gYkZopA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HPILR7Aq0gYkZopA .cluster-label text{fill:#333;}#mermaid-svg-HPILR7Aq0gYkZopA .cluster-label span{color:#333;}#mermaid-svg-HPILR7Aq0gYkZopA .cluster-label span p{background-color:transparent;}#mermaid-svg-HPILR7Aq0gYkZopA .label text,#mermaid-svg-HPILR7Aq0gYkZopA span{fill:#333;color:#333;}#mermaid-svg-HPILR7Aq0gYkZopA .node rect,#mermaid-svg-HPILR7Aq0gYkZopA .node circle,#mermaid-svg-HPILR7Aq0gYkZopA .node ellipse,#mermaid-svg-HPILR7Aq0gYkZopA .node polygon,#mermaid-svg-HPILR7Aq0gYkZopA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HPILR7Aq0gYkZopA .rough-node .label text,#mermaid-svg-HPILR7Aq0gYkZopA .node .label text,#mermaid-svg-HPILR7Aq0gYkZopA .image-shape .label,#mermaid-svg-HPILR7Aq0gYkZopA .icon-shape .label{text-anchor:middle;}#mermaid-svg-HPILR7Aq0gYkZopA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HPILR7Aq0gYkZopA .rough-node .label,#mermaid-svg-HPILR7Aq0gYkZopA .node .label,#mermaid-svg-HPILR7Aq0gYkZopA .image-shape .label,#mermaid-svg-HPILR7Aq0gYkZopA .icon-shape .label{text-align:center;}#mermaid-svg-HPILR7Aq0gYkZopA .node.clickable{cursor:pointer;}#mermaid-svg-HPILR7Aq0gYkZopA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HPILR7Aq0gYkZopA .arrowheadPath{fill:#333333;}#mermaid-svg-HPILR7Aq0gYkZopA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HPILR7Aq0gYkZopA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HPILR7Aq0gYkZopA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HPILR7Aq0gYkZopA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HPILR7Aq0gYkZopA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HPILR7Aq0gYkZopA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HPILR7Aq0gYkZopA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HPILR7Aq0gYkZopA .cluster text{fill:#333;}#mermaid-svg-HPILR7Aq0gYkZopA .cluster span{color:#333;}#mermaid-svg-HPILR7Aq0gYkZopA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HPILR7Aq0gYkZopA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HPILR7Aq0gYkZopA rect.text{fill:none;stroke-width:0;}#mermaid-svg-HPILR7Aq0gYkZopA .icon-shape,#mermaid-svg-HPILR7Aq0gYkZopA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HPILR7Aq0gYkZopA .icon-shape p,#mermaid-svg-HPILR7Aq0gYkZopA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HPILR7Aq0gYkZopA .icon-shape .label rect,#mermaid-svg-HPILR7Aq0gYkZopA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HPILR7Aq0gYkZopA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HPILR7Aq0gYkZopA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HPILR7Aq0gYkZopA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Ubuntu Host: host-hwsim-up.sh
docker compose up -d
Container Terminal A: wpa_supplicant A
Container Terminal B: wpa_supplicant B
Container Terminal C: wpa_cli

Compose 已经把 P2P_A、P2P_B 注入 Container,所以进入任意 Container Terminal 后都可以直接使用,不需要重复执行 export:

bash 复制代码
printf 'P2P_A=%s\nP2P_B=%s\n' "$P2P_A" "$P2P_B"
iw dev

默认应看到:

text 复制代码
P2P_A=wlan0
P2P_B=wlan1

如果 iw dev 中的实际 interface 名不同,回到上一节修改 .env,再重新创建 Container。后续终端命令统一使用 $P2P_A / $P2P_B,避免在教程命令中重复写死 interface 名。

先确认两个变量指向的 interface 确实存在:

bash 复制代码
iw dev "$P2P_A" info
iw dev "$P2P_B" info

8.1 Terminal A:启动 A 端 wpa_supplicant

打开第一个 Container Terminal,执行:

bash 复制代码
sudo wpa_supplicant-2.12 \
    -Dnl80211 \
    -i "$P2P_A" \
    -c /workspace/config/p2p-a.conf \
    -dd \
    -t

这条命令的整体含义是:启动一个前台运行的 wpa_supplicant 2.12 进程,让它只管理 A 端 WLAN interface,并读取 A 端独立配置,同时输出详细带时间戳日志。

参数 当前实验中的作用
sudo 本实验让 wpa_supplicant 以 root 身份执行,确保 nl80211 无线管理操作具备所需权限
wpa_supplicant-2.12 使用 Docker build 固定下来的 2.12 Debug binary,而不是 Ubuntu 系统自带的其他版本
-Dnl80211 选择 driver_nl80211 backend,通过 Generic Netlink / nl80211 与 Linux wireless subsystem 通信
-i "$P2P_A" 让这个实例只绑定 A 端 interface,默认就是 wlan0
-c /workspace/config/p2p-a.conf 读取 A 端配置;其中 ctrl_interface=/run/wpa_supplicant-p2p-a 决定 control socket 目录
-dd 打开较详细的 debug 日志,后续源码跟踪时用于对应函数和状态变化
-t 在 debug 日志前加入时间戳,方便与另一端日志和后续抓包按时间对齐

这里最容易混淆的是 -Dnl80211:它不是一种 Wi-Fi Direct 协议,而是 Linux 平台上 wpa_supplicant 控制无线内核子系统时使用的 driver backend。

命令启动后 Terminal A 会一直被这个前台进程占用,这是预期行为。不要关闭 Terminal A;后续命令在新的 Terminal 中执行。

8.2 Terminal B:启动 B 端 wpa_supplicant

打开第二个 Container Terminal,执行:

bash 复制代码
sudo wpa_supplicant-2.12 \
    -Dnl80211 \
    -i "$P2P_B" \
    -c /workspace/config/p2p-b.conf \
    -dd \
    -t

它与 A 端使用同一个 binary 和同一个 nl80211 backend,但绑定的是 $P2P_B,读取的是 /workspace/config/p2p-b.conf。两个进程因此拥有各自的 interface、配置和 control socket 目录:
#mermaid-svg-OHnLEWNgkHJjcd5F{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OHnLEWNgkHJjcd5F .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OHnLEWNgkHJjcd5F .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OHnLEWNgkHJjcd5F .error-icon{fill:#552222;}#mermaid-svg-OHnLEWNgkHJjcd5F .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OHnLEWNgkHJjcd5F .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OHnLEWNgkHJjcd5F .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OHnLEWNgkHJjcd5F .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OHnLEWNgkHJjcd5F .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OHnLEWNgkHJjcd5F .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OHnLEWNgkHJjcd5F .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OHnLEWNgkHJjcd5F .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OHnLEWNgkHJjcd5F .marker.cross{stroke:#333333;}#mermaid-svg-OHnLEWNgkHJjcd5F svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OHnLEWNgkHJjcd5F p{margin:0;}#mermaid-svg-OHnLEWNgkHJjcd5F .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OHnLEWNgkHJjcd5F .cluster-label text{fill:#333;}#mermaid-svg-OHnLEWNgkHJjcd5F .cluster-label span{color:#333;}#mermaid-svg-OHnLEWNgkHJjcd5F .cluster-label span p{background-color:transparent;}#mermaid-svg-OHnLEWNgkHJjcd5F .label text,#mermaid-svg-OHnLEWNgkHJjcd5F span{fill:#333;color:#333;}#mermaid-svg-OHnLEWNgkHJjcd5F .node rect,#mermaid-svg-OHnLEWNgkHJjcd5F .node circle,#mermaid-svg-OHnLEWNgkHJjcd5F .node ellipse,#mermaid-svg-OHnLEWNgkHJjcd5F .node polygon,#mermaid-svg-OHnLEWNgkHJjcd5F .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OHnLEWNgkHJjcd5F .rough-node .label text,#mermaid-svg-OHnLEWNgkHJjcd5F .node .label text,#mermaid-svg-OHnLEWNgkHJjcd5F .image-shape .label,#mermaid-svg-OHnLEWNgkHJjcd5F .icon-shape .label{text-anchor:middle;}#mermaid-svg-OHnLEWNgkHJjcd5F .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OHnLEWNgkHJjcd5F .rough-node .label,#mermaid-svg-OHnLEWNgkHJjcd5F .node .label,#mermaid-svg-OHnLEWNgkHJjcd5F .image-shape .label,#mermaid-svg-OHnLEWNgkHJjcd5F .icon-shape .label{text-align:center;}#mermaid-svg-OHnLEWNgkHJjcd5F .node.clickable{cursor:pointer;}#mermaid-svg-OHnLEWNgkHJjcd5F .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OHnLEWNgkHJjcd5F .arrowheadPath{fill:#333333;}#mermaid-svg-OHnLEWNgkHJjcd5F .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OHnLEWNgkHJjcd5F .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OHnLEWNgkHJjcd5F .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OHnLEWNgkHJjcd5F .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OHnLEWNgkHJjcd5F .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OHnLEWNgkHJjcd5F .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OHnLEWNgkHJjcd5F .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OHnLEWNgkHJjcd5F .cluster text{fill:#333;}#mermaid-svg-OHnLEWNgkHJjcd5F .cluster span{color:#333;}#mermaid-svg-OHnLEWNgkHJjcd5F div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OHnLEWNgkHJjcd5F .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OHnLEWNgkHJjcd5F rect.text{fill:none;stroke-width:0;}#mermaid-svg-OHnLEWNgkHJjcd5F .icon-shape,#mermaid-svg-OHnLEWNgkHJjcd5F .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OHnLEWNgkHJjcd5F .icon-shape p,#mermaid-svg-OHnLEWNgkHJjcd5F .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OHnLEWNgkHJjcd5F .icon-shape .label rect,#mermaid-svg-OHnLEWNgkHJjcd5F .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OHnLEWNgkHJjcd5F .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OHnLEWNgkHJjcd5F .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OHnLEWNgkHJjcd5F :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} P2P_A / wlan0
wpa_supplicant A
P2P_B / wlan1
wpa_supplicant B
/run/wpa_supplicant-p2p-a/wlan0
/run/wpa_supplicant-p2p-b/wlan1
wpa_cli A
wpa_cli B

这张图只表达 control interface 隔离关系:A 端 wpa_cli 不应连到 B 端 socket,B 端也一样。无线帧如何在两个 hwsim radio 之间交互属于后续 P2P 实验,不在这里展开。

8.3 第三个 Terminal:用 wpa_cli 验证 control interface

打开第三个 Container Terminal,先验证 A:

bash 复制代码
sudo wpa_cli-2.12 \
    -p /run/wpa_supplicant-p2p-a \
    -i "$P2P_A" \
    ping

这条命令不是 ICMP 网络 ping。它是在 wpa_cli control interface 上向 A 端 wpa_supplicant 发送 PING 命令,用于确认 CLI 与 daemon 之间的本地控制通道是否可用。

参数 当前实验中的作用
sudo 让 wpa_cli 可以访问由 root wpa_supplicant 创建的 UNIX control socket;这是本实验的权限处理,不是 P2P 协议要求
wpa_cli-2.12 使用与 wpa_supplicant-2.12 同一源码版本构建的 CLI
-p /run/wpa_supplicant-p2p-a 指定 A 端 control socket 所在目录
-i "$P2P_A" 指定目录下对应的 interface 名;默认组合成 /run/wpa_supplicant-p2p-a/wlan0
ping 发送 control command PING,用于验证 daemon 的 request/reply 通路

成功时返回:

text 复制代码
PONG

PONG 只能证明:

text 复制代码
wpa_cli A
    -> UNIX control socket
    -> wpa_supplicant A
    -> reply PONG

它还不能证明后续 Wi-Fi Direct 协议流程已经工作。

再验证 B:

bash 复制代码
sudo wpa_cli-2.12 \
    -p /run/wpa_supplicant-p2p-b \
    -i "$P2P_B" \
    ping

同样应返回:

text 复制代码
PONG

两个实例都返回 PONG 后,可以确认阶段 0 需要的两条独立 control interface 已经建立。

这里同时存在两类不同的通信边界:

text 复制代码
wpa_cli
    -> UNIX control socket
    -> wpa_supplicant

wpa_supplicant
    -> driver_nl80211
    -> Generic Netlink / nl80211
    -> Linux kernel wireless subsystem

第一条是"用户态工具控制 wpa_supplicant daemon",第二条是"wpa_supplicant 控制 Linux wireless subsystem"。阶段 1 执行 p2p_find 时,首先研究第一条控制路径。

9. 为什么推荐使用 Dev Containers 阅读 2.12 源码

F12 本身并不要求必须运行在 Container 中。只要 VS Code C/C++ 扩展能够同时访问源码和正确的编译信息,在 Ubuntu Host 上直接打开源码同样可以完成跳转。

本工程仍然推荐选择 Reopen in Container ,原因不是 F12 依赖 Docker,而是当前 wpa_supplicant 2.12 的源码、编译数据库、系统头文件、Debug binary 和 GDB 本来就都固定在 Docker Image 中。VS Code Dev Containers 的官方说明也明确了 extension host、terminal 与开发工具可以运行在容器环境中。S5

当前文件位置是:

text 复制代码
Ubuntu Host
└── /home/wdfk/share/wifi-direct
        └── bind mount -> Container /workspace

Docker Container
├── /workspace
├── /opt/wifi-direct/src/wpa_supplicant-2.12
│   └── wpa_supplicant/compile_commands.json
├── /usr/include/...
├── /usr/local/bin/wpa_supplicant-2.12
└── /usr/bin/gdb

Host 上的 /home/wdfk/share/wifi-direct 只对应 Container 的 /workspace。Docker Image 内的:

text 复制代码
/opt/wifi-direct/src/wpa_supplicant-2.12

并不是 Host 文件系统中的普通目录,因此 Host 上直接运行的 VS Code C/C++ 扩展默认看不到这棵源码树。

另外,Bear 在 Image build 时生成的 compile_commands.json 记录的是实际编译环境,其中的源码路径、include path 和 compiler 参数都以 Container 文件系统为基准。F12/IntelliSense 真正需要的不是"有一份 C 文件"这么简单,而是:

text 复制代码
source
+ compile_commands.json
+ include path
+ defines
+ compiler context

因此让 C/C++ 扩展进入同一个 Container,可以直接复用真实构建环境,不需要再维护第二份路径映射。

9.1 Reopen in Container 实际移动了什么

VS Code 图形界面仍然运行在 Ubuntu Desktop。Reopen in Container 主要把 workspace 的开发后端放到 Container 中:

text 复制代码
Ubuntu Host
└── VS Code UI
        |
        | Dev Containers
        v
Docker Container
├── VS Code Server / Extension Host
├── C/C++ extension
├── Terminal
├── wpa_supplicant 2.12 source
├── compile_commands.json
└── GDB / Debug binary

因此这里所说的"在 Container 里阅读源码",不是在 Container 里运行一个新的图形化 VS Code,而是让 VS Code 的 C/C++ 扩展、终端和调试器直接工作在目标开发环境中。

这样 F12 时使用的路径就是实际构建时的路径:

text 复制代码
/opt/wifi-direct/src/wpa_supplicant-2.12/...

不会出现 Host 路径和 Container 路径各维护一份的问题。

9.2 不使用 Dev Containers 能不能在 Host 上 F12

可以,但需要额外准备。

一种做法是把 wpa_supplicant 2.12 源码复制到 Host,再让 Host 上的 C/C++ 扩展读取这份源码。但此时 Docker Image 中已经存在一份实际参与编译的源码,Host 又多出一份源码副本,两者需要人为保证一致。

如果还希望使用 Bear 生成的 compile_commands.json,还需要处理其中的 Container 绝对路径,例如:

text 复制代码
/opt/wifi-direct/src/wpa_supplicant-2.12/...

这些路径在 Host 上并不存在,需要重写路径或改成另一套 includePath / defines 配置。

所以两种方式的区别可以概括为:

方式 能否 F12 额外工作
VS Code Host 直接打开源码 可以 需要把源码放到 Host,并维护编译数据库或 include/define 路径
Reopen in Container 可以 直接使用 Image 中已经构建过的源码和 compile_commands.json

本系列选择第二种方式,目标是让"正在阅读的源码""实际编译的源码""F5 调试的 binary"始终属于同一次 Image build。

9.3 使用 Dev Containers 与 F12

VS Code 安装:

text 复制代码
Dev Containers
C/C++

在 Ubuntu Host 上打开:

text 复制代码
/home/wdfk/share/wifi-direct

从左下角 Remote 指示器选择:

text 复制代码
Reopen in Container

.devcontainer/devcontainer.json 复用工程根目录的 compose.yaml:

字段 作用
dockerComposeFile 告诉 Dev Containers 使用哪个 Compose 文件
service 指定进入 wifi-direct-dev service
workspaceFolder 在 Container 中把 /workspace 作为工程工作目录
shutdownAction: none 关闭 VS Code 窗口时不自动停止实验 Container
remoteUser: dev VS Code Extension Host 使用 dev 用户
C_Cpp.default.compileCommands 让 C/C++ 扩展读取 Bear 生成的真实编译数据库

C/C++ 扩展读取:

text 复制代码
/opt/wifi-direct/src/wpa_supplicant-2.12/wpa_supplicant/compile_commands.json

打开:

text 复制代码
/opt/wifi-direct/src/wpa_supplicant-2.12/wpa_supplicant/main.c

在 main.c 中找到:

c 复制代码
wpa_supplicant_init(...)

把光标放到 wpa_supplicant_init 上按 F12,VS Code 应跳转到同一棵 2.12 source tree 中的定义。

如果跳转没有建立,先在 Dev Container Terminal 检查:

bash 复制代码
test -f /opt/wifi-direct/src/wpa_supplicant-2.12/wpa_supplicant/compile_commands.json

然后执行:

text 复制代码
Ctrl+Shift+P
-> C/C++: Log Diagnostics

确认 compileCommands 指向:

text 复制代码
/opt/wifi-direct/src/wpa_supplicant-2.12/wpa_supplicant/compile_commands.json

9.4 Comment Translate 在 Dev Container 中如何使用

.devcontainer/devcontainer.json 已配置:

text 复制代码
intellsmi.comment-translate

同时默认设置:

text 复制代码
commentTranslate.targetLanguage = zh-CN

这里的 customizations.vscode.extensions 是项目级自动安装声明,但判断扩展是否可用时要看 Container Extension Host 中实际安装结果,不能只看配置文件。

进入 Dev Container 后先执行:

bash 复制代码
code --list-extensions | grep -E 'cpptools|comment-translate'

正常应看到:

text 复制代码
intellsmi.comment-translate
ms-vscode.cpptools

如果只有:

text 复制代码
ms-vscode.cpptools

说明当前 Dev Container 没有真正安装 Comment Translate。当前项目已经实际验证下面的命令可以在 Dev Container 中安装 intellsmi.comment-translate v3.1.0:

bash 复制代码
code --install-extension intellsmi.comment-translate --force

安装成功后重新确认:

bash 复制代码
code --list-extensions | grep -E 'cpptools|comment-translate'

因此本项目采用两层策略:

text 复制代码
正常路径:devcontainer.json 自动声明扩展
    ↓
进入 Container 后检查 code --list-extensions
    ↓
若 Comment Translate 缺失
    ↓
code --install-extension intellsmi.comment-translate --force

手工安装只作为当前 Container 的兜底,不替代 devcontainer.json 中的项目级声明。源码位于 Container 内时,扩展应安装在对应的 Container Extension Host 中,不需要把 Host 的扩展目录手工 mount 进 Docker。

如果 Host VS Code 已经装了 Comment Translate,也可以从 Extensions 视图的 Local - Installed 使用 Install Local Extensions in Dev Container ;如果希望所有 Dev Container 都自动安装,可在 Host VS Code User Settings 中配置 dev.containers.defaultExtensions。

10. 使用 F5 + GDB 调试同一份源码

F5 同样不是"只有 Dev Containers 才能做到"。本工程选择把 C/C++ 扩展、GDB、Debug binary 和源码统一放到 Dev Container 中,避免再维护一套 Host/Container source path 映射。

当前 launch.json 不再使用 pipeTransport,也不需要额外的 gdb-sudo.sh wrapper。调试权限被拆成两层:

text 复制代码
VS Code / C++ Extension
        |
        v
GDB (dev)
        |
        | 127.0.0.1:2345
        v
gdbserver (root)
        |
        v
wpa_supplicant-2.12
        |
        v
nl80211 / Host hwsim interface

普通 dev 用户运行 GDB;只有 /usr/bin/gdbserver 通过受限 sudoers 获得 root 身份。这样不需要让整个 VS Code Extension Host 以 root 运行,也不需要给 dev 配置 NOPASSWD:ALL。

F5 前先停止占用目标 interface 的手工 wpa_supplicant 实例。

在:

text 复制代码
/opt/wifi-direct/src/wpa_supplicant-2.12/wpa_supplicant/main.c

的 main() 设置断点。

打开 Run and Debug,只需要选择一份配置:

text 复制代码
wpa_supplicant 2.12 - P2P

按 F5 后,launch.json 会依次询问:

text 复制代码
1. 本次调试使用的 WLAN interface
2. 对应的 p2p-a.conf / p2p-b.conf

A/B 共用同一份 debug configuration,因此不需要维护两套几乎相同的 JSON。当前 launch.json 的 interface 选项仍以 wlan0 / wlan1 作为阶段 0 默认值;如果 .env 中把 P2P_A / P2P_B 改成了其他真实 interface 名,也需要同步修改 .vscode/launch.json 中两个 p2pInterface 选项。

调试链路为:

text 复制代码
VS Code F5
  -> /usr/bin/gdb                    (dev)
  -> 127.0.0.1:2345
  -> sudo -n /usr/bin/gdbserver      (root)
  -> /usr/local/bin/wpa_supplicant-2.12
  -> -Dnl80211
  -> -i <interface>
  -> /workspace/config/p2p-*.conf
  -> Host hwsim interface

这里的 sudo 只用于启动 gdbserver。Container 仍然没有 SYS_MODULE capability,也不会在 Container 内加载 mac80211_hwsim。

程序能够停在源码断点后,说明下面几层已经对齐:

text 复制代码
F5
 -> GDB
 -> gdbserver
 -> Debug binary
 -> wpa_supplicant 2.12 source

之后可以直接在后续要阅读的 2.12 源码函数中设置断点。

11. 日常启动只需要两层操作

Host 重启后,先确认 hwsim module:

bash 复制代码
lsmod | grep '^mac80211_hwsim' || sudo modprobe mac80211_hwsim radios=2

这里的 || 是 shell 的"前一条失败才执行后一条":如果 grep 没找到已加载的 mac80211_hwsim,才执行 modprobe。因此它适合作为日常启动时的简写;第一次学习时仍建议分别执行 modinfo、modprobe、lsmod,看清每一步结果。

然后进入工程目录启动 Container:

bash 复制代码
cd /home/wdfk/share/wifi-direct
docker compose up -d
docker compose ps
docker compose exec wifi-direct-dev bash

这里 up -d 负责启动,ps 负责查看状态,exec ... bash 负责进入已经运行的开发 Container。

停止 userspace 环境:

bash 复制代码
docker compose down

如果同时需要清理虚拟 radio,再回到 Host 执行:

bash 复制代码
sudo modprobe -r mac80211_hwsim

这两个生命周期彼此独立:

text 复制代码
Docker down
    !=
卸载 mac80211_hwsim

Image 没有变化时,wpa_supplicant 2.12、源码、Debug symbols、GDB 和 compile_commands.json 都不会重新编译。

12. GitHub Actions:CI、GitHub Pages 与 Docker Image CD

工程把"验证"和"正式发布"拆成三条独立 workflow。Pages 自定义工作流和 Docker image 发布均可使用 GitHub 官方 Actions 工作流完成。S6

text 复制代码
.github/workflows/
├── ci.yml
├── deploy-pages.yml
└── publish-image.yml

ci.yml 在 Pull Request、push main 和手工触发时运行。它负责:

text 复制代码
Compose config validation
 -> MkDocs strict build
 -> Docker Image build
 -> wpa_supplicant/wpa_cli version check
 -> compile_commands.json check
 -> GDB/gdbserver check
 -> restricted sudo check

CI 只验证 Docker userspace。GitHub-hosted runner 不被当作本项目的双 hwsim 集成环境,因此 mac80211_hwsim radios=2、双 interface 和真实 P2P 行为仍由 Linux Host 集成实验验证。

deploy-pages.yml 在文档相关内容进入 main 后构建 MkDocs,并通过 GitHub 官方 Pages Actions 上传和部署 site/ artifact。第一次启用时,需要在仓库设置中把 Pages Source 选择为:

text 复制代码
GitHub Actions

publish-image.yml 在 push main、v* tag 或允许的手工执行中发布 GHCR Image。发布前先构建本地 release Image 并执行 userspace 检查;只有检查通过后才登录 GHCR 并 push 已验证的 tags。

push main 时主要得到:

text 复制代码
ghcr.io/<owner>/<repository>:main
ghcr.io/<owner>/<repository>:sha-<commit>

创建例如 v1.2.3 tag 时会得到版本 tag 与 latest:

text 复制代码
v1.2.3
1.2.3
1.2
1
latest
sha-<commit>

GHCR Image 可以包含:

text 复制代码
Ubuntu 24.04 userspace
wpa_supplicant 2.12 source
wpa_supplicant 2.12 Debug binary
wpa_cli 2.12
GDB / gdbserver
compile_commands.json
iw / ip / tcpdump

它不包含 Host kernel 和 mac80211_hwsim.ko。

发布后,把 .env 中的 Image 改为实际地址,例如:

text 复制代码
WIFI_DIRECT_IMAGE=ghcr.io/<owner>/<repository>:main

其他开发者只需要先在自己的 Ubuntu Linux Host 准备 hwsim:

bash 复制代码
sudo modprobe mac80211_hwsim radios=2

然后下载现成 userspace:

bash 复制代码
docker compose pull
docker compose up -d --no-build

docker compose pull 根据 .env / compose.yaml 中的 Image 地址下载已发布镜像;--no-build 明确禁止本地重新执行 Dockerfile,只使用已经拉取的 Image。

发布镜像固定使用 1000:1000。如果 Host 用户 UID/GID 不同,并且需要在 bind mount 的 /workspace 中修改文件,可以继续使用本地 docker compose up -d --build,让 Dockerfile 按 .env 中的 UID/GID 重建开发 Image。

13. 最终环境关系

最终环境固定为:

text 复制代码
Ubuntu Linux Host / VM
│
├── Linux kernel
│   ├── cfg80211
│   ├── mac80211
│   └── mac80211_hwsim radios=2
│       ├── virtual radio A -> interface A
│       └── virtual radio B -> interface B
│
└── Docker Engine
    └── wifi-direct-dev
        ├── wpa_supplicant 2.12 source
        ├── Debug wpa_supplicant 2.12
        ├── wpa_cli 2.12
        ├── GDB / gdbserver
        ├── compile_commands.json
        ├── wpa_supplicant process A
        └── wpa_supplicant process B

当前实验已经确认下面的运行结果:

text 复制代码
Host:
modprobe mac80211_hwsim radios=2

Container:
iw dev
 -> 两个 hwsim WLAN interface

wpa_cli A ping
 -> PONG

wpa_cli B ping
 -> PONG

F12/F5 属于下一步源码阅读与调试能力:F12 用于沿 2.12 源码跳转,F5 用于让 GDB 启动同一份 Debug binary。

最终环境边界固定为:Host 提供 Linux wireless kernel/hwsim,Docker 提供固定版本的 wpa_supplicant 2.12 userspace、源码和调试环境。

关键源码索引

关键对象 / 符号 本文位置 Git 源码
mac80211_hwsim [hwsim radio](#关键对象 / 符号 本文位置 Git 源码 mac80211_hwsim hwsim radio Linux Git nl80211 Netlink / nl80211 Linux Git wpa_supplicant 2.12 2.12 构建基线 hostap 2.12 wpa_supplicant 双实例启动 hostap 2.12) Linux Git
nl80211 [Netlink / nl80211](#关键对象 / 符号 本文位置 Git 源码 mac80211_hwsim hwsim radio Linux Git nl80211 Netlink / nl80211 Linux Git wpa_supplicant 2.12 2.12 构建基线 hostap 2.12 wpa_supplicant 双实例启动 hostap 2.12) Linux Git
wpa_supplicant 2.12 [2.12 构建基线](#关键对象 / 符号 本文位置 Git 源码 mac80211_hwsim hwsim radio Linux Git nl80211 Netlink / nl80211 Linux Git wpa_supplicant 2.12 2.12 构建基线 hostap 2.12 wpa_supplicant 双实例启动 hostap 2.12) hostap 2.12
wpa_supplicant [双实例启动](#关键对象 / 符号 本文位置 Git 源码 mac80211_hwsim hwsim radio Linux Git nl80211 Netlink / nl80211 Linux Git wpa_supplicant 2.12 2.12 构建基线 hostap 2.12 wpa_supplicant 双实例启动 hostap 2.12) hostap 2.12

资料来源

S1 Linux mac80211_hwsim

S3 hostap 2.12 release Git tag

S4 Docker Engine Host networking

S5 Visual Studio Code Dev Containers / C++ Debugging

  • URL/文档:Dev Containers、C++ Debugging
  • 使用位置:第 9~10 节
  • 支撑内容:容器内源码导航、extension host 与 C++/GDB 调试能力。

S6 GitHub Actions 文档

相关推荐
fengkai45451 小时前
十一、MySQL 第 1‑3 章
运维·数据库·mysql
wusam1 小时前
计算机网络第二章习题解答
服务器·网络·计算机网络
EasyGBS2 小时前
国标监控PS流是什么?GB28181视频平台EasyGBS视频封装全链路讲解
服务器·音视频·gb28181
Vcaker2 小时前
Linux学习36-k8s集群升级及kube-vip高可用
linux·运维·学习
土星云SaturnCloud2 小时前
边缘计算 + AI 视频存管一体机:快递末端网点分拣提效与安全管控实战方案
服务器·人工智能·ai·边缘计算
wdfk_prog2 小时前
Wi-Fi Direct教程 02:从 wpa_cli main() 到 P2P_FIND——用源码注释追踪 CLI 控制命令发送
运维·服务器·网络·网络协议·ubuntu·p2p·wifi-direct
DD云验证2 小时前
DD云验证,免费网络验证系统
服务器·python·易语言·网络验证·卡密验证
skywalk81632 小时前
Deepseek harness的4种模式 配置文件内容
linux·人工智能·ubuntu·deepseek·harness
HRTOS3 小时前
HRTOS Shell继续完善:优化printf缓冲区与字符串越界保护,国庆后随新版本更新
运维·单片机·嵌入式硬件·算法·51单片机