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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// wpa_supplicant/rrm.c:140(源码有精简)
int wpas_rrm_send_neighbor_rep_request(struct wpa_supplicant *wpa_s,
const struct wpa_ssid_value *ssid,
int lci, int civic,
void (*cb)(void *ctx,
struct wpabuf *neighbor_rep),
void *cb_ctx)
{
struct wpabuf *buf;

// 前置检查:必须在连接状态、对端 AP 支持 RRM、无进行中的请求
if (wpa_s->wpa_state != WPA_COMPLETED || ...)
return -ENOTCONN;
if (!wpa_s->rrm.rrm_used)
return -EOPNOTSUPP;
// 检查 AP 的 RRM Enabled Capabilities IE 中 Neighbor Report 位
if (!(rrm_ie[2] & WLAN_RRM_CAPS_NEIGHBOR_REPORT))
return -EOPNOTSUPP;

// 构建 Action 帧:3 字节头(Category + Action + Dialog Token)
buf = wpabuf_alloc(3 + (ssid ? 2 + ssid->ssid_len : 0) + ...);
wpabuf_put_u8(buf, WLAN_ACTION_RADIO_MEASUREMENT);
wpabuf_put_u8(buf, WLAN_RRM_NEIGHBOR_REPORT_REQUEST);
wpabuf_put_u8(buf, wpa_s->rrm.next_neighbor_rep_token);

// 可选:带上 SSID 子元素,只查特定网络的邻居
if (ssid) {
wpabuf_put_u8(buf, WLAN_EID_SSID);
wpabuf_put_u8(buf, ssid->ssid_len);
wpabuf_put_data(buf, ssid->ssid, ssid->ssid_len);
}

// 下发 Action 帧,注册超时回调
wpa_drv_send_action(wpa_s, ...);
wpa_s->rrm.notify_neighbor_rep = cb;
eloop_register_timeout(RRM_NEIGHBOR_REPORT_TIMEOUT, 0,
wpas_rrm_neighbor_rep_timeout_handler, ...);
return 0;
}

请求帧的结构很精简: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_modeieee802_11_defs.h:2229):PASSIVE 只听不发,适合安静的信道;ACTIVE 主动发 Probe Request 再收响应,适合隐藏 SSID 的 AP;TABLE 最省事——直接从扫描缓存里取现成结果上报,不需要额外扫描。每个报告条目包含 RCPI(接收信道功率指示,映射自 RSSI)、RSNI(接收信噪指示)、BSSID、信道号和测量时长等字段(struct rrm_measurement_beacon_reportieee802_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// wpa_supplicant/wnm_sta.c:1430(源码有精简)
static void ieee802_11_rx_bss_trans_mgmt_req(struct wpa_supplicant *wpa_s,
const u8 *pos, const u8 *end,
int reply)
{
if (end - pos < 5)
return; // 帧太短,至少需要 5 字节头

wnm_btm_reset(wpa_s); // 清空上次的 BTM 上下文

// 解析 5 字节头
wpa_s->wnm_dialog_token = pos[0]; // 1 字节:匹配请求和响应
wpa_s->wnm_mode = pos[1]; // 1 字节:模式标志位
wpa_s->wnm_disassoc_timer = WPA_GET_LE16(pos + 2); // 2 字节:倒计时
valid_int = pos[4]; // 1 字节:候选有效期
wpa_s->wnm_reply = reply;
pos += 5;

// 处理 BSS Termination Duration(如 AP 要关闭)
if (wpa_s->wnm_mode & WNM_BSS_TM_REQ_BSS_TERMINATION_INCLUDED) {
os_memcpy(wpa_s->wnm_bss_termination_duration, pos, 12);
pos += 12;
}

// 处理 Disassociation Imminent:AP 告诉 STA 即将强制断开
if (wpa_s->wnm_mode & WNM_BSS_TM_REQ_ESS_DISASSOC_IMMINENT) {
// 解析可选的 URL,通知上层
wpa_msg(wpa_s, MSG_INFO, ESS_DISASSOC_IMMINENT "%d %u %s", ...);
}

// 如果启用了 BTM offload,只需通知状态,固件自己处理
if (wpa_s->conf->btm_offload) {
wpa_s->bss_tm_status = WNM_BSS_TM_ACCEPT;
wpas_notify_bss_tm_status(wpa_s);
return;
}

// 解析 MBO IE(transition_reason、assoc_retry_delay、cell_preference)
// 解析候选 AP 列表,按 Preference 排序
wnm_parse_candidate_list(wpa_s, pos, end, &num_valid_candidates);
if (wpa_s->wnm_num_neighbor_report) {
wnm_sort_cand_list(wpa_s); // 按 Preference 降序排列
}

// Step 1: 先用缓存扫描结果匹配候选 AP
wpa_supplicant_update_scan_results(wpa_s, NULL);
if (wnm_scan_process(wpa_s, true) > 0)
return; // 缓存命中,直接进入连接流程

// Step 2: 缓存无有效候选,发起定向扫描
wnm_set_scan_freqs(wpa_s); // 限制扫描频率到候选列表
wpa_s->wnm_transition_scan = true;
wpa_supplicant_req_scan(wpa_s, 0, 0); // 触发扫描,结果处理会再次调用 wnm_scan_process
}

解析完成后,处理流程分为两步:先用缓存匹配,不行再定向扫描。这种 "缓存优先" 策略很务实——如果最近一次的扫描结果中就有一个候选 AP 且信号不错,为什么还要浪费时间再扫一次?

2.2 候选匹配:先查缓存再扫描

拿到房东的推荐信后,先翻翻手头已有的房源信息——如果最近刚看过其中一套且条件不错,何必再跑一趟?wnm_scan_process() 承担的就是这个 "先查缓存、再决定要不要实地看" 的候选择优逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// wpa_supplicant/wnm_sta.c:1155(源码有精简)
int wnm_scan_process(struct wpa_supplicant *wpa_s, bool pre_scan_check)
{
// 检查候选列表是否过期
if (!pre_scan_check && os_reltime_initialized(&wpa_s->wnm_cand_valid_until) &&
os_reltime_before(&wpa_s->wnm_cand_valid_until, &wpa_s->scan_trigger_time))
goto send_bss_resp_fail;

// 对比 Neighbor Report 候选列表和扫描结果,选出最佳匹配
bss = compare_scan_neighbor_results(wpa_s, &reason);

if (pre_scan_check) {
// 预扫描检查:缓存命中但数据太老(>10s)或会导致 ping-pong → 返回 0 触发新扫描
if (!bss) return 0;
os_reltime_age(&bss->last_update, &age);
if (age.sec >= 10) return 0;
// 防 ping-pong:候选可能就是当前 AP,或者切过去马上又要切回来
if (wpa_s->current_bss && bss != wpa_s->current_bss &&
wpa_supplicant_need_to_roam_within_ess(wpa_s, wpa_s->current_bss, bss))
return 0;
}

if (!bss) {
status = WNM_BSS_TM_REJECT_NO_SUITABLE_CANDIDATES;
goto send_bss_resp_fail;
}

// 找到有效候选 → 触发连接
wnm_bss_tm_connect(wpa_s, bss, ssid, 1);
return 1;

send_bss_resp_fail:
// 发送 BTM Response(ACCEPT 或 REJECT)
wnm_send_bss_transition_mgmt_resp(wpa_s, status, reason, 0, NULL);
wnm_btm_reset(wpa_s);
return 0;
}

pre_scan_check=true 的返回值语义很有意思:返回 0 不是 "失败",而是 "不要用缓存,去扫描";返回 >0 才是 "缓存命中,已发起连接"。这个设计让同一个函数在不同阶段有不同的行为——第一次调用(预检)和第二次调用(扫描后)复用同一段选优逻辑。

2.3 BTM Response 和重关联

房子看好了,该给房东回信了。候选确定后,wnm_bss_tm_connect() 负责先回 BTM Response(ACCEPT),再发起重关联——相当于先告诉房东 "我搬",然后开始打包行李:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// wpa_supplicant/wnm_sta.c:1105(源码有精简)
static void wnm_bss_tm_connect(struct wpa_supplicant *wpa_s,
struct wpa_bss *bss, struct wpa_ssid *ssid,
int after_new_scan)
{
// 如果 BTM Request 要求回复,先回 BTM Response (ACCEPT + target BSSID)
if (wpa_s->wnm_reply) {
wpa_s->wnm_target_bss = bss;
if (wnm_send_bss_transition_mgmt_resp(
wpa_s, WNM_BSS_TM_ACCEPT,
MBO_TRANSITION_REJECT_REASON_UNSPECIFIED, 0,
bss->bssid) >= 0)
return; // Response 发送完成后,TX 回调会再次调用本函数
}

// 候选就是当前 AP?什么也不做
if (bss == wpa_s->current_bss) {
wnm_btm_reset(wpa_s);
return;
}

// 触发重关联
wpa_s->reassociate = 1;
wpa_supplicant_connect(wpa_s, bss, ssid);
wpa_s->bss_trans_mgmt_in_progress = true;
}

这里有个值得注意的异步设计: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_codewlan_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// wpa_supplicant/wnm_sta.c:1019(源码有精简)
static int wnm_send_bss_transition_mgmt_resp(
struct wpa_supplicant *wpa_s,
enum bss_trans_mgmt_status_code status,
enum mbo_transition_reject_reason reason,
u8 delay, const u8 *target_bssid)
{
wpa_s->wnm_reply = 0;
buf = wpabuf_alloc(BTM_RESP_MIN_SIZE);

wpabuf_put_u8(buf, WLAN_ACTION_WNM); // Category: WNM
wpabuf_put_u8(buf, WNM_BSS_TRANS_MGMT_RESP); // Action: BTM Response
wpabuf_put_u8(buf, wpa_s->wnm_dialog_token); // Dialog Token: 与 Request 匹配
wpabuf_put_u8(buf, status); // Status Code: ACCEPT/REJECT/...
wpabuf_put_u8(buf, delay); // BSS Termination Delay

if (target_bssid) {
wpabuf_put_data(buf, target_bssid, ETH_ALEN); // 目标 BSSID
} else if (status == WNM_BSS_TM_ACCEPT) {
wpabuf_put_data(buf, "\0\0\0\0\0\0", ETH_ALEN); // 802.11 规范要求 ACCEPT 必须带 BSSID
}

if (status == WNM_BSS_TM_ACCEPT && target_bssid)
wnm_add_cand_list(wpa_s, &buf); // ACCEPT 时可以附带候选偏好列表

// 如果是 REJECT,附加 MBO Transition Reject Reason IE
if (status != WNM_BSS_TM_ACCEPT && ...) {
wpas_mbo_ie_bss_trans_reject(wpa_s, mbo, sizeof(mbo), reason);
wpabuf_put_data(buf, mbo, ret);
}

res = wpa_drv_send_action(wpa_s, wpa_s->assoc_freq, 0, wpa_s->bssid,
wpa_s->own_addr, wpa_s->bssid,
wpabuf_head_u8(buf), wpabuf_len(buf), 0);
return res;
}

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 完整交互链路:Measurement Request → Measurement Report (../../../../../../00_Share/WiFi-Source-Analysis-Learning/diagrams/08b-btm-sequence.svg) → BTM Request → 候选扫描 → BTM Response (ACCEPT) → 重关联

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 代码)。

802.11r FT 密钥层次:PMK → PMK-R0(R0KH 持有,MDID 派生)→ PMK-R1(R1KH 持有,BSSID 派生)→ PTK

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_ONGOINGroaming_fsm.c:947),收到目标 AP 的 FT Auth Response 后转移到 FT_DS_STATE_AUTHORIZEDroaming_fsm.c:450),超时或失败则标记为 FT_DS_STATE_FAILroaming_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_REQUESTROAMING_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// mgmt/roaming_fsm.c:151(MTK 驱动,源码有精简)
uint32_t roamingFsmSendFtActionFrame(struct ADAPTER *prAdapter,
struct STA_RECORD *prStaRec,
struct BSS_DESC_SET *prRoamTarget)
{
struct BSS_DESC *prBssDesc = prRoamTarget->prMainBssDesc;
struct FT_IES *prFtIEs = aisGetFtIe(prAdapter, prStaRec->ucBssIndex,
AIS_FT_R0);
// ... 省略:分配 MSDU_INFO、构造帧头 ...

// 填充 FT Action Request 帧体:RSN IE + MDIE + FTIE
if (prFtIEs->prRsnIE)
kalMemCopy(pos, (uint8_t *)prFtIEs->prRsnIE, u2RsnLen);
if (prFtIEs->prMDIE)
kalMemCopy(pos, (uint8_t *)prFtIEs->prMDIE, u2MDLen);
if (prFtIEs->prFTIE)
kalMemCopy(pos, (uint8_t *)prFtIEs->prFTIE, u2FTLen);

// 排队发送,发送完成后 roamingFtActionTxDone 回调推进状态机
nicTxEnqueueMsdu(prAdapter, prMsduInfo);
}

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