Wi-Fi Direct 源码分析(13):Persistent Group 如何再次建立——P2P_INVITE、Invitation 与 Reinvocation
摘要:追踪 Persistent Group 凭据保存、P2P_INVITE、Invitation Request/Response 与重新建组,说明 teardown 后如何复用原角色和密钥再次到达 P2P-GROUP-STARTED。
@[toc]
第 12 篇结束在一个很关键的生命周期边界:
1 | active P2P Group |
但第 12 篇同时保留了一个没有继续展开的问题:
当前 Group 被删除以后,为什么同一对设备下一次还能“恢复”原来的 Group,而不必重新完成一次完整的 GO Negotiation?
答案就是 Persistent Group。S3
它并不是一个一直运行、不被删除的 Group,也不是一个后台常驻的虚拟 interface。Persistent Group 真正持久化的是一组能够描述“原 Group 身份与安全关系”的配置数据。当前 active Group 可以退出,Group Interface 可以消失,但这组配置仍然保存在 wpa_supplicant 的 network 配置中。下一次通过 Invitation 重新唤起时,双方可以直接复用已经保存的角色、SSID 和密钥材料,再进入 GO 或 Client 的启动路径。
因此第 13 篇只解决这一条主线:
1 | 第一次形成 persistent Group |
本文只追踪保存的 Persistent Group 如何通过 P2P_INVITE persistent=<id> 被重新建立;相邻但不同语义的命令只在必要的对比处说明。
本文源码基线统一为 hostap 2.12 release tag hostap_2_12(commit 831364bf02710ad09c2f27d3efa92abeeb5634c0)。Persistent network 保存、P2P_INVITE、Invitation Request/Response 与 wpas_p2p_group_add_persistent() 都按同一 Git tag 的 wpa_supplicant/ 与 src/p2p/ 源码核对。S1
1. Persistent Group 持久化的不是“正在运行的 Group”
第 09 篇中,P2P_CONNECT ... persistent 可以要求本次形成 Persistent Group;第 10 篇的 WPS / Group Formation 成功以后,wpa_supplicant 才会把当前 Group 的关键信息复制到一个特殊的 struct wpa_ssid network entry 中。
在 wpas_group_formation_completed() 中可以直接看到这个动作:
1 | if (persistent) |
GO 在不需要 provisioning、直接完成 Group 配置的路径中也会保存:
1 | if (params->persistent_group) { |
所以 Persistent Group 的生命周期不是:
1 | Group 一直保持运行 |
而是:
1 | graph TD |
第 12 篇删除的是 active Group;第 13 篇要使用的是留下来的 persistent network entry。这两个对象必须从一开始就分开理解。
2. wpas_p2p_store_persistent_group() 到底保存了什么
核心函数位于:
1 | wpa_supplicant/p2p_supplicant.c |
它首先尝试寻找已有的 Persistent Group 条目:
1 | for (s = wpa_s->conf->ssid; s; s = s->next) { |
经典 P2P 路径主要使用:
1 | disabled == 2 |
来识别已有条目。如果没有找到,就新建 network,并马上把它标记为 Persistent Group:
1 | s->p2p_group = 1; |
随后填入真正需要长期保留的数据:
1 | s->p2p_group = 1; |
密钥材料也会被复制:
1 | if (ssid->passphrase) { |
SSID 同样被复制到保存条目中:
1 | if (s->ssid == NULL || s->ssid_len < ssid->ssid_len) { |
如果配置允许写回:
1 | if (changed && wpa_s->conf->update_config && |
这就解释了 Persistent Group 为什么能够跨越一次 active Group 的 teardown:真正需要复用的数据已经进入普通配置对象,而不是只保存在已经被释放的 Group Interface 中。
disabled == 2 不是普通的“禁用网络”
hostap 2.12 release tag在 config_file.c 中还有一条非常直接的恢复规则:
1 | if (ssid->disabled == 2) |
wpa_supplicant_i.h 也把 Persistent Group 判定封装成:
1 | static inline int network_is_persistent_group(struct wpa_ssid *ssid) |
因此这里的 disabled == 2 是一个特殊状态:它表示这条 network 不是普通 STA 自动连接项,而是用于 Persistent Group storage。
config_ssid.h 对 bssid 字段也特别注明:当 disabled == 2 时,bssid 保存的是 GO Device Address。
所以一个 persistent entry 至少建立了这样的映射:
| 字段 | 在 Persistent Group 中的含义 |
|---|---|
disabled == 2 |
Persistent Group storage entry |
p2p_persistent_group |
明确标记为持久化 P2P Group |
ssid |
原 Group SSID |
bssid |
GO Device Address |
mode |
本端在该 Group 中保存的角色 |
passphrase / psk |
重新建立安全连接所需的密钥材料 |
| cipher / key management | 原安全配置 |
其中 mode 对下一阶段尤其重要,因为 Reinvocation 不会再通过一次 GO Negotiation 来重新决定角色。
3. 为什么 list_networks 能看到 [P2P-PERSISTENT]
README-P2P 明确说明,list_networks 会同时列出 Persistent Group 的保存信息;S2后续 p2p_group_add 和 p2p_invite 使用这里的 network id 来选择需要恢复的 Group。
源码在生成 LIST_NETWORKS 输出时直接根据 disabled == 2 增加标记:
1 | ret = os_snprintf(pos, end - pos, "\t%s%s%s%s", |
因此可能看到类似:
1 | network id / ssid / bssid / flags |
这里的 4 就是后续命令:
1 | P2P_INVITE persistent=4 |
中的 <network id>。
这个 id 指向保存的 persistent configuration,不指向第 12 篇已经删除的 Group Interface。
4. Reinvocation 的真正用户态入口:P2P_INVITE persistent=<id>
README-P2P 对 Invitation 给出的接口是:
1 | p2p_invite [persistent=<network id>|group=<group ifname>] [peer=address] |
其中两种形式解决的是两个不同问题:
| 命令形式 | 目标 |
|---|---|
persistent=<id> |
重新唤起以前保存过的 Persistent Group |
group=<ifname> |
邀请 peer 加入一个当前已经运行的 Group |
第 13 篇只沿第一条路径继续。
命令进入 ctrl_iface.c 后,p2p_ctrl_invite() 先按前缀分流:
1 | static int p2p_ctrl_invite(struct wpa_supplicant *wpa_s, char *cmd) |
p2p_ctrl_invite_persistent() 对 persistent=<id> 做的第一件关键事情,就是重新取得刚才分析过的 persistent network:
1 | if (os_strncmp(cmd, "persistent=", 11) == 0) { |
然后把这个 struct wpa_ssid *ssid 传入:
1 | return wpas_p2p_invite(wpa_s, _peer, ssid, NULL, freq, freq2, ht40, vht, |
到这里已经出现了和第一次 P2P_CONNECT 完全不同的参数来源:
1 | graph LR |
第 09 篇首次 GO Negotiation 时需要协商“谁做 GO”;第 13 篇 reinvocation 的核心前提则是双方已经保存过这段关系。
5. wpas_p2p_invite() 先从保存配置恢复角色
wpas_p2p_invite() 的源码注释已经直接写明用途:
1 | /* Invite to reinvoke a persistent group */ |
进入函数后,如果没有可用的 persistent entry,会立即失败:
1 | if (!ssid) |
接下来最关键的是角色恢复。
保存条目表明本端原来是 GO
1 | if (ssid->mode == WPAS_MODE_P2P_GO) { |
这里没有 go_intent 比较。保存配置已经说明本端在这个 Persistent Group 中承担 GO 角色。
如果平台需要独立 Group Interface,还会在 Invitation 发出前预留新的 GO interface 地址。
保存条目表明本端原来是 Client
紧接着是另一条分支:
1 | else { |
经典 P2P Client 路径会把保存条目中的:
1 | ssid->bssid |
直接当作需要联系的 GO Device Address。前面已经确认 Persistent Group entry 中的 bssid 字段正是用于保存 GO Device Address。
这里形成了一个非常重要的闭环:
1 | 第一次保存时 |
这也是为什么 README-P2P 说明:如果 peer 本身就是该 Persistent Group 的 GO,某些调用场景中不必再单独提供 peer 参数。
6. Invitation 开始前会停止 Find,并把流程交给 P2P Core
wpas_p2p_invite() 在真正发 Invitation 前先停止当前 Find/Listen:
1 | /* |
经典非 P2P2 路径最后进入:
1 | return p2p_invite(wpa_s->global->p2p, peer_addr, role, bssid, |
这里传给 P2P Core 的几个关键值已经全部来自 Persistent Group 上下文:
1 | peer_addr |
hostap 2.12 同一 Git tag 中的 src/p2p/p2p_invitation.c 可以直接继续追踪:p2p_invite() 进入 Invitation 流程并发送 Invitation Request Public Action frame;收到对端 Invitation Response 后,再通过 struct p2p_config callback 返回 wpa_supplicant glue 层。S1
这条路径的关键不是底层函数名本身,而是它建立了一个新的异步边界:
1 | wpas_p2p_invite() |
因此不能把 wpas_p2p_invite() 和 wpas_p2p_group_add_persistent() 画成一个直接同步调用。
7. 为什么 P2P Core 能回到 wpas_invitation_process() 和 wpas_invitation_result()
这条 callback 关系在 P2P 初始化时已经绑定。
wpas_p2p_init() 中:
1 | p2p.invitation_process = wpas_invitation_process; |
三个 callback 的职责不同:
| callback | 发生在哪一侧 | 主要职责 |
|---|---|---|
wpas_invitation_process() |
收到 Invitation Request 的设备 | 判断是否认识该 Group、是否允许接受、恢复哪个角色 |
wpas_invitation_received() |
Request 接收端发送完 Invitation Response 后 | 把结果通知上层;成功时启动本端 persistent Group |
wpas_invitation_result() |
主动发起 Invitation 的设备收到 Response 后 | 上报 P2P-INVITATION-RESULT;成功时启动本端 persistent Group |
这三个函数正好把 P2P Core 的无线 Action frame 交互桥接回 wpa_supplicant 的配置和 Group 生命周期。
8. 接收端收到 Invitation Request 后,先判断“这个 Group 我认不认识”
接收端真正做策略判断的是:
1 | wpas_invitation_process() |
对于 Persistent Group,它先检查是否已经有同 SSID、同角色的 active Group:
1 | grp = wpas_get_p2p_group(wpa_s, ssid, ssid_len, go); |
如果当前没有运行,则继续检查这次 Invitation 是否已经被本端预先授权,或者配置是否允许自动 persistent reconnect:
1 | if (!is_zero_ether_addr(wpa_s->p2p_auth_invite) && |
然后寻找本机保存的 Persistent Group entry:
1 | for (s = wpa_s->conf->ssid; s; s = s->next) { |
经典 P2P 路径下,接收端要求保存条目能够匹配:
1 | disabled == 2 |
如果不存在,就直接返回:
1 | else if (!s) { |
所以 Persistent Group Reinvocation 不是“对端说有这个 Group,本端就相信”。双方本地都需要有能够匹配的持久化上下文;至少在经典路径中,接收端会用自己的 saved entry 做确认。
9. persistent_reconnect 控制的是“是否自动接受”,不是“有没有保存配置”
README-P2P 对这个开关的定义是:
1 | set persistent_reconnect <0/1> |
启用后,对 Persistent Group reinvocation 的 Invitation 可以不经过单独的上层授权直接接受。
源码刚才已经给出了准确条件:
1 | else if (!wpa_s->conf->persistent_reconnect) |
因此两种行为可以画成:
1 | graph TD |
注意,这个配置不决定 Persistent Group 是否保存,也不等于跳过无线安全认证。它只影响收到 reinvocation Invitation 后,wpa_supplicant 是否允许自动进入恢复流程。
10. 接收端为什么会产生 P2P-INVITATION-RECEIVED 或 P2P-INVITATION-ACCEPTED
P2P Core 处理完 Request、发送 Invitation Response 后,会通过初始化时注册的:
1 | invitation_received |
callback 回到:
1 | wpas_invitation_received() |
这个函数先按 SSID 在本地找 Persistent Group entry:
1 | for (s = wpa_s->conf->ssid; s; s = s->next) { |
已经接受 Invitation
当 status == P2P_SC_SUCCESS 且找到了保存条目时,它会先上报接受事件,再直接进入 persistent Group restart:
1 | if (s) { |
这里出现了第 13 篇最关键的跳转:
1 | Invitation 接受 |
当前不能自动接受
如果前面的策略返回:
1 | P2P_SC_FAIL_INFO_CURRENTLY_UNAVAILABLE |
wpas_invitation_received() 不会立刻重建 Group,而是上报 P2P-INVITATION-RECEIVED,让上层知道有一个 Persistent Group reinvocation 请求等待处理。
有匹配条目时:
1 | if (s->mode == WPAS_MODE_P2P_GO && op_freq) { |
这就是 persistent_reconnect=0 时应用层可能看到“Invitation 到达,但 Group 还没有自动恢复”的原因。
11. 发起端收到 Invitation Response 后怎样继续
主动执行:
1 | P2P_INVITE persistent=<id> |
的一侧,在收到对端 Invitation Response 后,会从 P2P Core 通过:
1 | invitation_result |
callback 进入:
1 | wpas_invitation_result() |
函数一开始就生成:
1 | P2P-INVITATION-RESULT |
事件:
1 | wpas_msg_p2p_invitation_result(wpa_s, status, new_ssid, new_ssid_len, |
如果 pending_invite_ssid_id != -1,说明当前是 Persistent Group reinvocation,而不是“邀请 peer 加入当前 active Group”。
失败时会停在 Invitation 阶段;例如 UNKNOWN_GROUP 还会清理本机保存的对应 peer 信息。
成功后重新取出发起命令时保存的 network id:
1 | ssid = wpa_config_get_network(wpa_s->conf, |
随后根据 Response 中的 channel 信息确定重新建组频率,并最终进入:
1 | wpas_p2p_group_add_persistent(wpa_s, ssid, |
因此 Invitation Request/Response 并不是最终 Group 本身。它解决的是:
- 双方是否同意恢复这条已保存关系;
- Group identity 是否匹配;
- 本次重新启动使用什么 operating channel;
- 成功后双方各自进入哪条本地 Group restart 路径。
真正重新启动 GO 或 Client 的仍然是后面的 wpas_p2p_group_add_persistent()。
12. wpas_p2p_group_add_persistent() 为什么能跳过 GO Negotiation
这是 Persistent Group reinvocation 最核心的一层。
函数首先要求传入的 network 必须真的是 persistent storage entry:
1 | if (ssid->disabled != 2 || ssid->ssid == NULL) |
然后停止 Discovery:
1 | /* Make sure we are not running find during connection establishment */ |
接下来直接根据保存的 ssid->mode 分流。
原角色是 GO
1 | if (ssid->mode == WPAS_MODE_P2P_GO) { |
后面把保存条目中的 PSK、passphrase 与 SSID 重新写入 GO 参数:
1 | params.role_go = 1; |
最后取得 Group Interface 并启动 GO:
1 | wpa_s = wpas_p2p_get_group_iface(wpa_s, addr_allocated, 1); |
原角色是 Client
另一条分支完全不创建 GO:
1 | else if (ssid->mode == WPAS_MODE_INFRA) { |
经典 Client 路径最终直接进入:
1 | return wpas_start_p2p_client(wpa_s, ssid, addr_allocated, freq, |
所以 reinvocation 的角色恢复本质是:
1 | graph TD |
这里没有回到第 09 篇的 GO Negotiation 主线:
1 | p2p_connect() |
因为 GO/Client 角色已经由 saved entry 确定。
但“跳过 GO Negotiation”不等于“什么握手都不需要”。GO 仍然需要真正启动 AP/GO,Client 仍然需要找到 GO、关联并完成安全数据连接;只是角色和持久化凭据不再通过一次全新的 Group Formation 协商重新生成。
13. 为什么最终仍然会回到 P2P-GROUP-STARTED
wpas_p2p_group_add_persistent() 没有创造另一套独立的 Group runtime。
它只是用 saved configuration 选择一条已有的运行路径:
1 | Persistent GO |
或者:
1 | Persistent Client |
因此第 11 篇讲过的 Group Interface、IP Address Allocation/DHCP 与数据面逻辑在 reinvocation 后仍然适用。
Persistent Group 复用的是 Group identity、角色与安全配置;它不会让 IP 层脱离第 11 篇的网络配置流程。
下面这张图把第 13 篇真正需要记住的关系压缩在一起:
图中最重要的不是 Invitation frame 自身,而是上下两端的 saved persistent network 都参与了重新建组:发起端用 network id 取出保存配置,接收端用本地保存条目确认 Group identity 和角色,成功后双方最终都汇入 wpas_p2p_group_add_persistent() 所连接的 GO/Client 运行路径。
14. P2P_INVITE persistent=<id> 与 P2P_GROUP_ADD persistent=<id> 不是同一件事
README-P2P 还提供:
1 | p2p_group_add persistent=<network id> |
这条命令也能使用 Persistent Group entry,所以很容易和 P2P_INVITE 混淆。
ctrl_iface.c 中它最终也是调用:
1 | return wpas_p2p_group_add_persistent(wpa_s, ssid, 0, freq, freq, |
但两条命令的前半段完全不同:
1 | P2P_INVITE persistent=<id> |
而:
1 | P2P_GROUP_ADD persistent=<id> |
所以 P2P_GROUP_ADD persistent=<id> 更接近“使用保存配置在本地重启这个 Group”;P2P_INVITE persistent=<id> 则是“通过 Wi-Fi Direct Invitation procedure 与 peer 协调 reinvocation”。
二者共享后半段 Group restart 实现,不代表前半段协议语义相同。
15. P2P_INVITE group=<ifname> 又是另一条 Invitation 路径
p2p_ctrl_invite() 的另一个分支是:
1 | P2P_INVITE group=<group ifname> peer=<addr> |
它调用:
1 | wpas_p2p_invite_group() |
源码注释把它定义为:
1 | /* Invite to join an active group */ |
也就是说,此时 Group 已经处于运行态,Invitation 的目标是让新 peer 加入这个 active Group,而不是根据 disabled == 2 的保存条目把一个已经结束的 Group 重新唤起。
可以直接按对象生命周期区分:
| 场景 | Group 当前状态 | 主要入口 |
|---|---|---|
| Persistent reinvocation | active Group 已结束,saved entry 仍在 | P2P_INVITE persistent=<id> |
| Active Group invitation | Group 正在运行 | P2P_INVITE group=<ifname> |
| 本地直接恢复 persistent Group | 不先做 peer Invitation | P2P_GROUP_ADD persistent=<id> |
第 13 篇的主线只属于第一行。
16. 三种 Invitation 结果怎样改变后续路径
沿当前代码可以把最重要的结果分成三类。
P2P_SC_SUCCESS
双方接受 reinvocation。
发起端:
1 | P2P-INVITATION-RESULT status=0 |
接收端:
1 | P2P-INVITATION-ACCEPTED |
然后双方各自按保存角色进入 GO/Client 路径。
P2P_SC_FAIL_INFO_CURRENTLY_UNAVAILABLE
本端可能拥有匹配 persistent entry,但当前策略不允许自动接受,例如:
1 | persistent_reconnect = 0 |
且本次 Invitation 没有被预授权。
这时接收端会发出:
1 | P2P-INVITATION-RECEIVED |
把是否继续交给上层,而不是直接启动 Group。
P2P_SC_FAIL_UNKNOWN_GROUP
本端找不到对应的 Persistent Group context。
这表示双方对“这条旧 Group 关系是否还存在”的认知已经不一致。当前源码在部分失败路径中还会清理本地保存的 peer 信息,避免继续反复使用已经失效的关系。
另外 channel intersection 失败也可能导致 Invitation 不能完成;因此“双方都有 persistent entry”只是 reinvocation 的必要条件之一,不代表任何时刻都一定能够重新建立 Group。
17. 为什么 Reinvocation 不是“免认证快速连接”
Persistent Group 最容易被误解成:
1 | 保存了 PSK |
源码并不是这样。
它真正跳过的是第 09 篇中的“重新决定 GO/Client 角色与重新协商 Group 参数”的部分。
重新唤起时仍然要经历:
1 | 找到 peer / 可用 channel |
所以更准确的表述是:
Persistent Group 让双方复用以前已经建立的 Group identity、角色和安全配置,从而避免再次执行完整的 GO Negotiation 与重新生成一套 Group 凭据;它并不绕过无线关联、运行态安全连接和后续 IP 网络建立。
18. 从第 09 篇到第 13 篇,Persistent Group 的完整生命周期已经闭环
把前面几篇真正相关的部分串起来,现在可以看到 Persistent Group 其实跨越了两个 active Group 生命周期。
第一次建立:
1 | 第 09 篇 |
第一次运行和结束:
1 | 第 11 篇 |
第二次恢复:
1 | 第 13 篇 |
这也把三个此前容易混在一起的对象彻底分开:
1 | P2P Device |
第 12 篇 teardown 只结束第三个对象;第 13 篇依靠第二个对象重新创建第三个对象,而第一个对象仍然承担整个 P2P subsystem 的发现和控制基础。
到这里,系列已经形成两层完整闭环:
1 | 04 → 05 → 06 → 07 |
关键源码索引
| 关键对象 / 符号 | 本文位置 | Git 源码 |
|---|---|---|
wpas_p2p_store_persistent_group() |
persistent entry 保存 | hostap 2.12 |
P2P_INVITE persistent=<id> |
用户态入口 | hostap 2.12 |
wpas_p2p_invite() |
supplicant Invitation | hostap 2.12 |
p2p_invite() |
P2P Core Invitation | hostap 2.12 |
p2p_process_invitation_req() |
Invitation Request RX | hostap 2.12 |
wpas_invitation_result() |
Invitation result callback | hostap 2.12 |
wpas_p2p_group_add_persistent() |
persistent group restore | hostap 2.12 |
资料来源
[S1] hostap 2.12 Git:Persistent Group / Invitation implementation
- 版本:
hostap_2_12/831364bf02710ad09c2f27d3efa92abeeb5634c0 - 文件:ctrl_iface.c、p2p_supplicant.c、config_file.c、p2p_invitation.c
- 使用位置:第 1~18 节
- 支撑内容:persistent network entry、
disabled == 2、Invitation Request/Response callback、role restore 与wpas_p2p_group_add_persistent()。
[S2] hostap 2.12 README-P2P
- 来源:wpa_supplicant/README-P2P
- 使用位置:第 3~6、14~15 节
- 支撑内容:
list_networks的[P2P-PERSISTENT]标记、p2p_invite与p2p_group_add persistent=的 command semantics。
[S3] Wi-Fi Direct Specification v1.9
- URL/文档:Wi-Fi Direct Specification v1.9
- 使用位置:第 1、4~13、16~18 节
- 支撑内容:Persistent Group、Invitation 与 reinvocation 的协议语义和 status code 背景。











