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 怎么让驱动亲力亲为,以及搬错了之后黑名单、防乒乓如何兜底。