STA 漫游(二)802.11k/v/r 三协议 — 找房、通知、无缝切换
连接不是终点。当你拿着手机从一个房间走到另一个房间,信号掉到 -78dBm,STA 面临选择:撑住旧 AP,还是切到新 AP?本文追踪漫游的第二步——搬去哪、怎么搬得无缝。漫游比作搬家:邻居报告是中介推荐房源,BTM 是房东通知你搬,FT 是 VIP 快速通道。本文聚焦 ESS 内漫游,分析 wpa_supplicant、QCOM qcacld-3.0、MTK gen4m 驱动源码(精简处标注
// ...省略...,文件路径标注在代码块首行)。漫游前的扫描机制见《扫描》系列,连接信令见《连接》系列,本文复用不重复。
1 搬去哪?——802.11k 邻居报告
802.11k RRM(Radio Resource Management)的做法很直接:STA 问当前 AP"你附近有哪些邻居?",AP 回复一份邻居报告(Neighbor Report),里面列出附近 AP 的 BSSID、信道号和操作类别——相当于中介递给你的房源清单,上面有地址(BSSID)、所在楼层(信道号)和户型(PHY Type)。拿到这份清单,漫游扫描就能从 "满城跑" 变成 "直奔目标"。
1.1 邻居报告请求的构建
在 supplicant 中,wpas_rrm_send_neighbor_rep_request() 负责构造和发送 Neighbor Report Request:
1 | // wpa_supplicant/rrm.c:140(源码有精简) |
请求帧的结构很精简:Category 指示这是 Radio Measurement 帧,Action Code 指定 Neighbor Report Request,Dialog Token 用来匹配响应。可选的 SSID 子元素让 STA 可以限定 "只告诉我同样是 'Office-WiFi' 这个 SSID 的邻居"。
1.2 邻居报告的内容和使用
AP 回复的 Neighbor Report 中,每个邻居 AP 包含:
- BSSID:邻居的 MAC 地址
- BSSID Information:能力信息(是否支持 WPA/WPA2、是否同一 SSID 等)
- Operating Class + Channel Number:邻居在哪个信道
- PHY Type:物理层类型
- Optional Subelements:可能包含 TSF offset、Beacon Interval 等
以上字段定义见 802.11-2024 §9.4.2.35 Figure 9-416(Neighbor Report 元素格式,BSSID Information 字段细分见 Figure 9-417)。
拿到邻居报告就拿到了 "看房路线图"——不用满城跑,直奔清单上的地址就行。漫游扫描可以缩小到报告中的信道列表,大幅减少扫描时间。一个典型的优化:如果邻居报告中有 3 个候选 AP 分布在 3 个信道,单信道停留时间 ~40ms,总扫描时间只需 ~120ms——而盲扫全频段可能需要 400ms+。
在 QCOM 平台,邻居报告甚至可以进一步卸载到固件。cm_roam_neighbor_report_offload_params 结构体允许 host 配置固件在满足条件时(如 RSSI 低于阈值、Beacon Miss 计数达到触发值、PER 超过阈值)自动发起 802.11k 邻居报告请求,固件拿到结果后直接用于漫游决策——host 全程不参与。
与邻居报告不同,Beacon Report 测量在 QCOM 平台上仅由 host 侧的 SME RRM 模块处理(sme_rrm_process_beacon_report_req_ind(),sme_rrm.c:1072),未实现固件卸载——固件只负责将 AP 发来的 Measurement Request 转发给 host,实际的信道扫描和报告组装由 host 完成。
邻居报告告诉你 "附近有哪些 AP",但没说信号怎么样——清单上有地址,房子好不好得住过才知道。802.11k 的另一项工具 Beacon Report 解决了这个问题:AP 发 Measurement Request(类型 MEASURE_TYPE_BEACON = 5)让 STA 去指定信道实地测量,回报每个 Beacon 帧的物理层指标。
测量有三种模式(enum beacon_report_mode,ieee802_11_defs.h:2229):PASSIVE 只听不发,适合安静的信道;ACTIVE 主动发 Probe Request 再收响应,适合隐藏 SSID 的 AP;TABLE 最省事——直接从扫描缓存里取现成结果上报,不需要额外扫描。每个报告条目包含 RCPI(接收信道功率指示,映射自 RSSI)、RSNI(接收信噪指示)、BSSID、信道号和测量时长等字段(struct rrm_measurement_beacon_report,ieee802_11_defs.h:2276)。
在 supplicant 中,这个请求由 wpas_rm_handle_beacon_req()(rrm.c:1196)处理。PASSIVE/ACTIVE 模式触发定向扫描,扫描完成后 wpas_beacon_rep_scan_process()(rrm.c:1541)遍历结果、构建报告条目,最终由 wpas_rrm_send_msr_report()(rrm.c:448)组装 Radio Measurement Report 帧发回 AP——报告超长时自动分片,最后一个分片带 Last Indication 子元素(rrm.c:402)通知 AP 传完。
TABLE 模式走另一条捷径——wpas_beacon_rep_table()(rrm.c:989)直接读 wpa_s->last_scan_res[] 缓存上报,无需扫描。
邻居报告和 Beacon Report 配合起来,形成完整的 "找房→看房" 链路:邻居报告提供候选的位置(BSSID + 信道),Beacon Report 提供候选在当前位置的实际信号质量。AP 可以先让 STA 知道 "去哪看",再让 STA 报告 "看到什么"——两步结果汇入 BTM 候选列表时,拿到的是经过实地验证的排序,比单纯的配置信息可靠得多。
2 AP 建议你换个家——BTM Request 怎么被处理?
从中介的房源清单,到房东的推荐信——802.11k 解决了 "附近有哪些 AP",接下来看 802.11v 怎么主动建议你搬。BTM(BSS Transition Management)是 802.11v 的核心功能。AP 拿着候选 AP 列表对 STA 说 "我建议你搬去这些地方"——这就像房东给你写了一封推荐信,附上了几套备选房源的地址和推荐理由。STA 可以接受、可以拒绝、也可以不理。本节回答:supplicant 收到 BTM Request 后,怎么解析帧、怎么选候选、怎么发起切换?完整的 BTM 处理链路从 ieee802_11_rx_bss_trans_mgmt_req() 开始。
2.1 BTM Request 的内部结构
1 | // wpa_supplicant/wnm_sta.c:1430(源码有精简) |
解析完成后,处理流程分为两步:先用缓存匹配,不行再定向扫描。这种 "缓存优先" 策略很务实——如果最近一次的扫描结果中就有一个候选 AP 且信号不错,为什么还要浪费时间再扫一次?
2.2 候选匹配:先查缓存再扫描
拿到房东的推荐信后,先翻翻手头已有的房源信息——如果最近刚看过其中一套且条件不错,何必再跑一趟?wnm_scan_process() 承担的就是这个 "先查缓存、再决定要不要实地看" 的候选择优逻辑:
1 | // wpa_supplicant/wnm_sta.c:1155(源码有精简) |
pre_scan_check=true 的返回值语义很有意思:返回 0 不是 "失败",而是 "不要用缓存,去扫描";返回 >0 才是 "缓存命中,已发起连接"。这个设计让同一个函数在不同阶段有不同的行为——第一次调用(预检)和第二次调用(扫描后)复用同一段选优逻辑。
2.3 BTM Response 和重关联
房子看好了,该给房东回信了。候选确定后,wnm_bss_tm_connect() 负责先回 BTM Response(ACCEPT),再发起重关联——相当于先告诉房东 "我搬",然后开始打包行李:
1 | // wpa_supplicant/wnm_sta.c:1105(源码有精简) |
这里有个值得注意的异步设计:BTM Response 的发送和重关联的发起不是原子操作。wnm_send_bss_transition_mgmt_resp() 将 Action 帧交给驱动发送,注册 TX 完成回调——等到 TX 完成后,回调函数再次调用 wnm_bss_tm_connect(),此时 wpa_s->wnm_reply 已被清零,直接进入 wpa_supplicant_connect() 重关联。这种设计确保 AP 先收到 ACCEPT 确认,STA 再发起切换——两件事的顺序对 AP 的状态管理很重要。
但搬家不可能每次都顺利——BTM 流程中有三个可能的失败点。
第一个是候选不足:缓存和定向扫描都找不到合适的候选 AP 时,wnm_scan_process() 发送 WNM_BSS_TM_REJECT_NO_SUITABLE_CANDIDATES(状态码 7,ieee802_11_defs.h:1994)拒绝 BTM——这是 8 种拒绝状态码中 STA 最常用的一种,其余还包括 UNSPECIFIED(1)、INSUFFICIENT_BEACON(2)等。AP 收到 REJECT 后的典型行为取决于厂商实现:企业级控制器通常降低该 STA 的负载均衡权重或等待一段时间后重试,消费级 AP 往往直接放弃这次引导。
第二个失败点在重关联阶段:STA 发出 Reassoc Request 后,目标 AP 可能因为容量满、安全策略不匹配或 PMKID 验证失败而拒绝——此时 supplicant 通过 sme_auth_start_cb()(sme.c:1230)在开始新的连接尝试前清除 bss_trans_mgmt_in_progress 标志,STA 回到正常状态等待下一次漫游触发。
第三个失败点在 QCOM RSO 模式下更隐蔽:固件自行执行 Auth+Reassoc+EAPOL,任何一步失败都通过 WMI_ROAM_EVENTID 上报 roam_fail_reason——这个字段的枚举 wlan_roam_failure_reason_code(wlan_cm_roam_public_struct.h:395)包含 30 多种失败原因,从 ROAM_FAIL_REASON_NO_AP_FOUND(扫描无结果)到 ROAM_FAIL_REASON_EAPOL_M4_NO_ACK(四次握手 M4 未收到 ACK),覆盖了整个漫游链路的每一步。
host 收到失败事件后走 cm_roam_event_handler() 的 ROAM_REASON_INVOKE_ROAM_FAIL 分支,如果连接已断则触发完整的断线重连流程。
这背后是架构取舍:QCOM 把 Auth+Reassoc+EAPOL 全下放固件,host 只在 cm_roam_event_handler() 收到失败事件时才被唤醒,省去逐步骤唤醒 host 的开销、换取低延迟切换;MTK 把漫游状态机留在 host 的 roamingFsmRunEventNewCandidate() 事件链里逐步推进,每一步都能打断点、加日志,可调试、易扩展,代价是每次状态迁移都经过 host 处理路径。
2.4 BTM Response 帧的构建
1 | // wpa_supplicant/wnm_sta.c:1019(源码有精简) |
MBO(Multiband Operation)扩展了 BTM Response 的语义。当 STA 拒绝 BTM 建议时,不仅要告诉 AP"我拒绝",还要说明原因——MBO_TRANSITION_REJECT_REASON_UNSPECIFIED(未指定)、MBO_TRANSITION_REJECT_REASON_FRAME_LOSS(帧丢失)等。AP 收到拒绝原因后可以调整策略,比如降低负载均衡权重或给 STA 更多时间。BTM Response 帧格式由 802.11-2024 §9.6.13.10 Figure 9-1279 定义,包含 Category(WNM)、Action、Dialog Token、Status Code、BSS Termination Delay、Target BSSID 和候选列表七个字段。
BTM 处理的核心是 "缓存优先"——先用最近扫描结果匹配候选,命中就直接切换,没命中才发起定向扫描。异步设计保证 AP 先收到 ACCEPT 再触发重关联,避免状态混乱。
3 如何做到不断线切换?——802.11r FT 快速漫游
标准漫游的问题是认证开销大。完整 802.1X 包含:Auth(2 帧)+ Assoc(2 帧)+ EAP-TLS(6-10 帧)+ 四次握手(4 帧),总耗时 100-500ms,对 VoIP 或实时游戏是灾难性的。802.11r FT(Fast BSS Transition)把密钥材料预推送到目标 AP,漫游时只需 2 帧(FT Auth Req/Resp)+ Reassoc(2 帧),将切换时延压缩到 20-50ms。本节回答:FT 的密钥怎么分层?两种模式有什么区别?驱动怎么把 FT 帧组装出来?
3.1 密钥分层:为什么 FT 可以这么快
FT 的核心洞察是把密钥分成两层:
- PMK-R0:由 PMK(Pairwise Master Key,来自 802.1X 或 PSK)和 MDID(Mobility Domain Identifier)派生。PMK-R0 在整个移动域内有效,同一域内的所有 AP 共享同一个 PMK-R0 持有者(R0KH)。
- PMK-R1:由 PMK-R0 和目标 AP 的 BSSID 派生。每个 AP 有自己的 PMK-R1,由 R1KH(目标 AP 自己或控制器)持有。
为什么分两层而不是直接用 PMK 向每个 AP 派生 PTK?核心价值是密钥隔离——目标 AP 的 PMK-R1 泄露时攻击范围被限制在单个 AP,不会波及其他 AP 和 PMK-R0。R0KH 通常部署在控制器上(企业网络)或首次认证的 AP 上(家庭网络),R1KH 在每个 AP 上独立持有自己的 PMK-R1。
首次关联时(Initial Mobility Domain Association),STA 完成完整的 802.1X 认证并获得 PMK-R0。后续在同一移动域内切换时,不需要重做 802.1X——STA 用 PMK-R0Name、目标 AP 的 R1KH-ID 和自己的 S1KH-ID(即 SPA,STA 的 MAC 地址)派生 PMK-R1Name:PMK-R1Name = Truncate-128 (SHA-256 ("FT-R1N" || PMK-R0Name || R1KH-ID || S1KH-ID))。之后通过 FT Auth Req/Resp 向目标 AP 请求对应的 PMK-R1,然后直接派生 PTK 完成四次握手。
这就像你第一次入住酒店时需要在前台办全套手续(护照、信用卡、签字),但同品牌的连锁酒店之间共享了你的会员信息——换到同品牌另一家酒店时,只需要确认身份就能拿房卡。
FT 密钥层次的完整定义见 802.11-2024 §4.5.4.8(Fast BSS transition 概述)。R0KH 是 PMK-R0 的持有者(通常为 AC 或首次认证的 AP),通过 R0KH-ID 标识——在企业网络中这通常是 AP 控制器的域名或 MAC 地址;R1KH 是 PMK-R1 的持有者(目标 AP 自身或控制器),通过 R1KH-ID 标识——通常就是目标 AP 的 MAC 地址。
在 wpa_supplicant 中,FT 初始关联时 R0KH-ID 和 R1KH-ID 通过 MDIE(Mobility Domain IE)和 FTIE(Fast Transition IE)在 Auth Req/Resp 帧中交换——MTK 驱动的 roamingFsmSendFtActionFrame() 正是从缓存的 FT IE 中取出这些材料填入帧体(见 §3.3 代码)。
3.2 FT 的两种模式:Over-the-Air vs Over-the-DS
如果说标准漫游是 "收拾行李→退房→找新房→办入住" 的完整流程,FT 就是连锁酒店的行李预寄服务——你的行李(密钥材料)已经提前送到新酒店了,人到了只需要确认身份(FT Auth)就能拿房卡。FT 协议定义了两种 "确认身份" 的方式:
- Over-the-Air:你直接去新酒店前台确认身份——STA 直接与新 AP 交换 FT Auth 帧(Category=
CATEGORY_FT_ACTION,Action=ACTION_FT_REQUEST),目标 AP 必须能听到 STA 的信号。这是最常见的模式。 - Over-the-DS:新酒店太远你过不去,让当前酒店前台帮你转交证件——STA 通过当前 AP 向目标 AP 中转 FT 帧,当前 AP 通过有线分布系统(DS)转发给目标 AP,目标 AP 再通过 DS 回复。适合目标 AP 不在 STA 射频范围内的场景。在 supplicant 中,
wpa_ft_start_over_ds()(wpa_ft.c:1307)负责发起这个流程:它先生成新的 SNonce,调用wpa_ft_gen_req_ies()构建 FT IE(包含 PMK-R1Name、ANonce、SNonce),然后设置over_the_ds_in_progress = 1标记进行中的 DS 漫游,最后通过wpa_sm_send_ft_action()发送 FT Action 帧。
MTK 驱动侧,Over-the-DS 有独立的状态管理——FSM 通过 fgIsFtOverDS 标志区分两种模式(roaming_fsm.c:869),FT 帧发送后进入 FT_DS_STATE_ONGOING(roaming_fsm.c:947),收到目标 AP 的 FT Auth Response 后转移到 FT_DS_STATE_AUTHORIZED(roaming_fsm.c:450),超时或失败则标记为 FT_DS_STATE_FAIL(roaming_fsm.c:453)。这组状态(wlan_lib.h:891-894)让驱动在 Over-the-DS 模式下精确跟踪 FT 帧的中转进度——与 Over-the-Air 的 "发送→等待→成功 / 失败" 三步相比,Over-the-DS 多了 "经当前 AP 中转" 这一步,帧的实际路径是 STA → 当前 AP → DS(有线)→ 目标 AP → DS → 当前 AP → STA。每一步都可能丢帧,因此超时和重试逻辑更为关键。
这条六跳路径相比 Over-the-Air 的两跳(STA 直接与目标 AP 交换),RTT 增加约 2-5ms(有线 DS 内部转发延迟 + 当前 AP 的帧中转处理时间),对 VoIP 20ms jitter budget 而言并非可忽略。选择 Over-the-DS 的典型场景不只是 "目标 AP 不在射频范围内"——在高密度企业部署中,STA 可能能听到目标 AP 的 Beacon(信号 -85dBm),但 STA→AP 方向的链路质量不足以让目标 AP 解码 STA 发出的 FT Auth 帧(下行信号好不代表上行也对称),Over-the-DS 通过当前 AP 的有线连接绕过了这个射频瓶颈。
3.3 MTK 驱动中的 FT 帧处理
协议说了两种 "前台确认身份" 的方式,其中 Over-the-Air 是 STA 直接向新 AP 前台报上身份。接下来看 MTK 驱动怎么把这一步的 FT 帧组装出来。在 MTK 的驱动层漫游状态机中,FT 被拆成两个明确的 FSM 状态:ROAMING_STATE_SEND_FT_REQUEST 和 ROAMING_STATE_WAIT_FT_RESPONSE。
当漫游目标确定且需要 FT 时,roamingFsmRunEventNewCandidate() 检测到候选 AP 后进入 ROAMING_STATE_HANDLE_NEW_CANDIDATE 状态,然后判断是否需要 FT。如果候选 AP 在同一个 MDID 内并且 STA 已经有 PMK-R0,状态机转移到 ROAMING_STATE_SEND_FT_REQUEST。
FT Auth Request 是一个 Action 帧,帧头包含四个关键字段:Category(CATEGORY_FT_ACTION,标识这是 FT 帧)、Action(ACTION_FT_REQUEST,标识这是请求而非响应)、STA Address(请求方 MAC 地址)、Target AP Address(目标 AP 的 MAC 地址)。帧体依次携带三个 IE:RSN IE(携带 PMK-R1Name 和 AKMP 套件信息,让目标 AP 知道用哪个密钥层级验证)、MDIE(Mobility Domain Identifier,验证两端在同一个移动域内)、FTIE(Fast Transition IE,携带 ANonce、SNonce 和可选的 R0KH-ID/R1KH-ID,用于密钥派生)。
roamingFsmSendFtActionFrame() 从缓存的 FT IE 中取出这三个 IE 填入帧体,通过 nicTxEnqueueMsdu() 排队发送。MLO 场景下,Target AP Address 不再使用单链路的 BSSID 而是替换为 MLD 地址(prBssDesc->rMlInfo.aucMldAddr),因为 MLO 下多个链路共享同一个 MLD 身份。
1 | // mgmt/roaming_fsm.c:151(MTK 驱动,源码有精简) |
FT Action 帧发送后,状态机转移到 ROAMING_STATE_WAIT_FT_RESPONSE,启动超时定时器等待目标 AP 的 FT Auth Response。roamingFsmRunEventRxFtAction() 处理收到的 FT Action 帧——如果收到 FT Auth Response(状态码 0),推进到 Reassoc 流程;如果超时或状态码非 0,宣告 FT 失败回退到标准 Auth。
FT 的价值在于密钥预推——PMK-R0 在整个移动域共享,PMK-R1 按 AP 隔离,漫游时只需 2 帧交换就能完成认证。MTK 驱动把 FT 拆成 SEND_FT_REQUEST 和 WAIT_FT_RESPONSE 两个 FSM 状态,失败时回退到标准 Auth,不会卡死。
4 总结
本文追完了 ESS 内漫游的第二步:怎么搬、怎么搬得无缝。三条协议各司其职——802.11k 用邻居报告和 Beacon Report 解决 "搬去哪":先拿房源清单(BSSID + 信道)再实地看房(实测信号质量);802.11v 的 BTM 让 AP 主动递上推荐信,STA 缓存优先、没命中再定向扫描,回 ACCEPT 后再重关联;802.11r 的 FT 靠密钥分层(PMK-R0 域内共享、PMK-R1 按 AP 隔离)把认证压到 2 帧,实现 20-50ms 无缝切换。三者串成一条链路:k 给候选、v 下建议、r 保切换不断线。
下一篇预告:《STA 漫游(三):平台执行与调优 — QCOM 代理 vs MTK 亲为》落地到真实平台——QCOM 的 RSO 怎么让固件全权代理,MTK 的 Host Roaming 怎么让驱动亲力亲为,以及搬错了之后黑名单、防乒乓如何兜底。