【蓝牙精品系列文档】【03_blemesh架构】07_网络层_2
网络层关键字段详解(IVI / NID / SEQ / IV Index)
本文档对 Mesh Network PDU 头部的关键字段做深入解析,包括空口识别、包交互、用途,以及全网更新流程(IV Update / Key Refresh)。
一、Network PDU 头部回顾
┌──────────┬──────────┬────────┬─────────┬─────────┬───────────┬──────┐ │ Byte 0 │ Byte 1 │ Byte2~4│ Byte5~6 │ Byte7~8 │ Byte9+ │ 尾部 │ │ IVI+NID │ CTL+TTL │ SEQ │ SRC │ DST │ Transport │NetMIC│ │ 1 B │ 1 B │ 3 B │ 2 B │ 2 B │ PDU │4/8B │ └──────────┴──────────┴────────┴─────────┴─────────┴───────────┴──────┘字段宏定义(源码net.c):
#defineIVI(pdu)((pdu)[0]>>7)/* Byte 0 bit7 */#defineNID(pdu)((pdu)[0]&0x7f)/* Byte 0 bit0~6 */#defineCTL(pdu)((pdu)[1]>>7)/* Byte 1 bit7 */#defineTTL(pdu)((pdu)[1]&0x7f)/* Byte 1 bit0~6 */#defineSEQ(pdu)(sys_get_be24(&pdu[2]))/* Byte 2~4 大端 */#defineSRC(pdu)(sys_get_be16(&(pdu)[5]))#defineDST(pdu)(sys_get_be16(&(pdu)[7]))| 字段 | 长度 | 是否混淆 | 是否加密 | 空口可见 |
|---|---|---|---|---|
| IVI | 1 bit | 明文 | 明文 | ✅ 直接看 |
| NID | 7 bit | 明文 | 明文 | ✅ 直接看 |
| CTL | 1 bit | 混淆 | - | ❌ 需 Privacy Key |
| TTL | 7 bit | 混淆 | - | ❌ 需 Privacy Key |
| SEQ | 24 bit | 混淆 | - | ❌ 需 Privacy Key |
| SRC | 16 bit | 混淆 | - | ❌ 需 Privacy Key |
| DST | 16 bit | 明文 | 加密 | 部分可见(加密后) |
| Transport PDU | 变长 | - | AES-CCM | ❌ 需 EncKey |
| NetMIC | 4/8B | - | 加密生成 | ❌ 需 EncKey |
重点:IVI 和 NID 是空口上唯一能直接看到的头部字段,这是为了让接收方快速选 NetKey 而设计的。
二、IVI(IV Index 最低位)
字段定义
| 项 | 值 |
|---|---|
| 长度 | 1 bit |
| 位置 | Byte 0 bit7 |
| 是否混淆 | 明文 |
| 取值 | 0 或 1,即 IV Index 的最低位 |
空口识别
Byte 0: ┌───┬─────────────────────────────────────────┐ │IVI│ NID (7 bit) │ │1b │ │ └───┴─────────────────────────────────────────┘ ↑ IV Index 最低位 例:IV Index = 0x00000005 → IVI = 1 IV Index = 0x00000006 → IVI = 0用途:快速选 IV Index 版本
问题背景:IV Update 期间,节点同时持有两个 IV Index(旧的和新的),需要判断收到的包用哪个 IV Index 解密。
IVI 的作用:接收方先看 IVI,快速判断是「新 IV」还是「旧 IV」:
/* net.c - net_decrypt() */rx->old_iv=(IVI(in->om_data)!=(bt_mesh.iv_index&0x01));IVI == 本地 iv_index 最低位→新 IV(old_iv = false)IVI != 本地 iv_index 最低位→旧 IV(old_iv = true)
然后选用对应的 IV Index 构造 Nonce 解密:
/* net.c */#defineBT_MESH_NET_IVI_RX(rx)((rx)->old_iv?\(bt_mesh.iv_index-1):bt_mesh.iv_index)包交互示例
关键前提:节点进入 IV Update in Progress 后,bt_mesh.iv_index已经是 +1 后的新值(源码bt_mesh.iv_index = iv_index)。所以「持有 5 和 6」的节点 B,其bt_mesh.iv_index = 6,不是 5。
节点 A (Normal, iv_index=5) 节点 B (IV Update, iv_index=6, 持有 5 和 6) │ │ │ Network PDU (A 用 IV=5 发) │ │ [IVI=1][NID=...] ← 5 & 1 = 1 │ ───────────────────────────> │ B: iv_index=6, 6 & 1 = 0 │ │ old_iv = (IVI=1 != 0) = true │ │ → 用 IV=6-1=5 解密 ✅(B 持有旧 IV=5) │ │ │ (B 用 IV=6 发包) │ │ [IVI=0][NID=...] ← 6 & 1 = 0 │ <─────────────────────────── │ A 视角:iv_index=5, 5 & 1 = 1 │ │ old_iv = (IVI=0 != 1) = true │ │ → 按「旧 IV」规则用 IV=5-1=4 试解 ❌ │ │ → A 只持有 IV=5,没有 4,解密失败,丢弃 │ │ │ │ 实际情况:这个包是 B 用新 IV=6 发的 │ │ A 的 iv_index 还停在 5,不认识 IV=6 │ │ A 的 old_iv 逻辑只能处理「比本地旧 1」的包 │ │ 无法处理「比本地新 1」的包 → 当成旧 IV=4 试解 │ │ │ │ 解决:A 必须先收到 Secure Network Beacon │ │ 同步到 iv_index=6 后,才能解 B 的包: │ │ 同步后 A: iv_index=6, 6&1=0 │ │ → IVI=0 匹配 → old_iv=false → 用 IV=6 解 ✅关键规则:
old_iv = true表示「这是旧 IV 的包」,接收方用iv_index - 1解密old_iv = false表示「这是新 IV 的包」,接收方用iv_index解密- IV Update 期间节点能解新旧两个 IV(持有
iv_index和iv_index-1) - Normal 状态节点只能解当前 IV,收不到新 IV 的包(需通过 Beacon 同步后才能)
为什么只用 1 bit
- IV Index 是 32 bit,但空口只暴露最低位
- 隐私:不暴露完整 IV Index,避免攻击者追踪网络年龄
- 效率:1 bit 足够区分「新旧 IV」两种状态
- 完整 IV Index 通过 Secure Network Beacon 同步(带 32-bit IV Index 字段)
三、NID(Network ID)
字段定义
| 项 | 值 |
|---|---|
| 长度 | 7 bit |
| 位置 | Byte 0 bit0~6 |
| 是否混淆 | 明文 |
| 取值范围 | 0x00 ~ 0x7F(128 种) |
| 派生 | k2(NetKey) 派生 |
派生过程
NetKey (16B) │ └── k2 派生 ──┬── NID (7 bit) ← 网络标识 ├── Encryption Key ← AES-CCM 加密 └── Privacy Key ← XOR 混淆源码subnet.c:
staticintmsg_cred_create(structbt_mesh_net_cred*cred,constuint8_t*p,size_tp_len){returnbt_mesh_k2(key,p,p_len,&cred->nid,cred->enc,cred->privacy);}NID、EncKey、PrivacyKey 三者一起派生,绑定为同一个bt_mesh_net_cred结构体。
空口识别
Byte 0: ┌───┬─────────────────────────────────────────┐ │IVI│ NID (7 bit) │ │ │ 0x00 ~ 0x7F │ └───┴─────────────────────────────────────────┘用途:快速选 NetKey 凭证
问题背景:一个节点可能持有多个 NetKey(多子网),收到包时要判断用哪个 NetKey 解密。如果不用 NID,得对每个 NetKey 都试一遍 AES-CCM 解密,开销巨大。
NID 的作用:粗筛——只有 NID 匹配的凭证才尝试解密:
/* net.c - net_decrypt() */if(NID(in->om_data)!=cred->nid){returnfalse;/* NID 不匹配,直接跳过,不试解密 */}包交互示例
节点持有 3 个 NetKey: NetKey A → NID = 0x12, EncKey A, PrivKey A NetKey B → NID = 0x34, EncKey B, PrivKey B NetKey C → NID = 0x56, EncKey C, PrivKey C 收到空口包 [IVI=0][NID=0x34]... 遍历 3 个凭证: ├─ NID 0x12 != 0x34 → 跳过(不试解密 A) ├─ NID 0x34 == 0x34 → 匹配!用 EncKey B 解密 └─ NID 0x56 != 0x34 → 跳过(不试解密 C) 省了 2 次 AES-CCM 解密运算NID 碰撞问题
NID 只有 7 bit = 128 种,两个不同 NetKey 可能派生出相同 NID(概率 1/128)。
碰撞时怎么办:
- NID 匹配的凭证可能多个 → 都试解密
- 真正区分靠NetMIC 校验:错误 NetKey 解密后 MIC 不对,包被丢弃
- NID 只是粗筛,不能单靠 NID 认定「这是我的网」
Friendship NID
Friendship 建立后会派生独立的 Friendship 凭证(含独立 NID):
原 NetKey 凭证:NID = 0x12(全网泛洪用) Friendship 凭证:NID = 0x47(LPN↔Friend 私聊用)LPN 和 Friend 通信时用 Friendship NID,其他节点看到 NID=0x47 不匹配自己的任何凭证,直接丢弃,实现私聊隔离。
NID vs Network ID(重要区分)
NID 和 Network ID 是两个完全不同的东西,容易混淆。
| 维度 | NID | Network ID |
|---|---|---|
| 派生算法 | k2 | k3 |
| 长度 | 7 bit | 8 字节(64 bit) |
| 出现位置 | Network PDU 头部 Byte 0 | Secure Network Beacon / Proxy 广播 |
| 用途 | 快速识别用哪个 NetKey 凭证解密 | 标识子网(Beacon 里) |
| 空口可见 | ✅ 明文(每个 0x2A 包都有) | ✅ 明文(在 0x2B Beacon 里) |
| 取值空间 | 128 种(易碰撞) | 2^64 种(几乎不碰撞) |
源码证据
NID 来自 k2 派生(7 bit,存在bt_mesh_net_cred):
structbt_mesh_net_cred{uint8_tnid;/* NID: 7 bit */uint8_tenc[16];/* Encryption Key */uint8_tprivacy[16];/* Privacy Key */};Network ID 来自 k3 派生(8 字节,存在 subnet keys):
/* subnet.h */structbt_mesh_subnet_keys{...uint8_tnet_id[8];/* Network ID: 8 字节 */};/* subnet.c */err=bt_mesh_k3(key,keys->net_id);/* k3 派生 Network ID */出现在空口的不同位置
┌─────────────────────────────────────────────────────────────┐ │ 0x2A Network PDU │ │ ┌──────────┬─────┬──────┬──────┬────────┬──────┬─────┐ │ │ │ IVI+NID │ CTL │ TTL │ SEQ │ SRC │ DST │ ... │ │ │ │ 1 B │ 1B │ 7b │ 24b │ 16b │ 16b │ │ │ │ └──────────┴─────┴──────┴──────┴────────┴──────┴─────┘ │ │ ↑ │ │ NID (7 bit) ← k2 派生,每个 Network PDU 都有 │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ 0x2B Secure Network Beacon │ │ ┌──────────┬───────┬────────────┬────────────┬──────────┐ │ │ │BeaconType│ Flags │ Network ID │ IV Index │ Auth Val │ │ │ │ 0x01 │ 1B │ 8B │ 4B │ 8B │ │ │ └──────────┴───────┴────────────┴────────────┴──────────┘ │ │ ↑ │ │ Network ID (8B) ← k3 派生,只在 Beacon 里 │ └─────────────────────────────────────────────────────────────┘为什么需要两个不同的标识
| 标识 | 解决的问题 |
|---|---|
| NID(7 bit) | Network PDU 每秒可能几十上百个,需要极快粗筛用哪个 NetKey 解密。7 bit 够了,碰撞靠 MIC 兜底 |
| Network ID(8B) | Beacon 频率低(10 秒一次),需要精确识别子网,8 字节几乎不碰撞,用于 Secure Network Beacon 验证 + Proxy 节点发现 |
设计哲学:
- NID 是「快但不精确」——为每个 Network PDU 节省解密开销
- Network ID 是「慢但精确」——为低频 Beacon 提供可靠子网识别
Proxy 广告也用 Network ID
源码proxy_srv.c:
memcpy(proxy_svc_data+3,sub->keys[SUBNET_KEY_TX_IDX(sub)].net_id,8);Proxy 节点广播时把 Network ID 放在 Service Data 里,手机扫描到后用 Network ID 判断「这个 Proxy 能接入我要的子网吗」。
NetKey 完整派生家族
NetKey 是网络层所有密钥的根,通过 k1/k2/k3 不同算法派生出不同用途的密钥:
Network Key (16 B) │ ├── k2 派生 ──┬── NID (7 bit) ← 网络标识(Network PDU 粗筛) │ ├── Encryption Key (16B) ← Network PDU 加密(AES-CCM) │ └── Privacy Key (16B) ← Network PDU 混淆(XOR) │ ├── k3 派生 ──── Network ID (8 B) ← Secure Network Beacon 标识子网 │ ├── k1 派生 ──── Identity Key (16B) ← Node Identity 广播(Proxy 发现具体节点) │ 用途:Proxy 节点配网后短期广播,让手机发现自己 │ 算法:AES-ECB(IdentityKey, Random+Addr) → Hash │ 位置:Proxy 广播 Service Data │ 防护:只有有 NetKey 的手机能验证,知道是哪个节点 │ └── k1 派生 ──── Beacon Key (16B) ← Secure Network Beacon 认证(防伪造) 用途:计算 Beacon 的 Auth Value 算法:AES-CMAC(BeaconKey, Flags+NetID+IVIdx) → 8B 位置:0x2B Beacon 尾部 Auth Value 字段 防护:没有 Beacon Key 无法伪造合法 Beacon| 派生算法 | 输出 | 用途 | 出现位置 |
|---|---|---|---|
| k2 | NID + EncKey + PrivacyKey | Network PDU 收发(最常用) | 0x2A Network PDU |
| k3 | Network ID(8B) | Secure Network Beacon 标识子网 | 0x2B Beacon |
| k1 | Identity Key | Node Identity 广播(配网后短期可发现) | Proxy 广播 |
| k1 | Beacon Key | Secure Network Beacon 的 Auth Value 计算 | 0x2B Beacon |
Friendship 凭证也由 NetKey 派生
LPN 和 Friend 建立 Friendship 时,派生独立的 Friendship 凭证,输入还是 NetKey,但加了额外参数:
intbt_mesh_friend_cred_create(structbt_mesh_net_cred*cred,uint16_tlpn_addr,uint16_tfrnd_addr,uint16_tlpn_counter,uint16_tfrnd_counter,constuint8_tkey[16]){uint8_tp[9];p[0]=0x01;sys_put_be16(lpn_addr,p+1);sys_put_be16(frnd_addr,p+3);sys_put_be16(lpn_counter,p+5);sys_put_be16(frnd_counter,p+7);returnmsg_cred_create(cred,p,sizeof(p),key);/* key 就是 NetKey */}Friendship Key = k2(NetKey, [0x01|LPNAddr|FriendAddr|LPNCounter|FriendCounter]) → Friendship NID + Friendship EncKey + Friendship PrivacyKey根都是 NetKey,只是派生参数不同,产生不同的凭证组合。
Identity Key 详解(Node Identity 广播)
用途:Proxy 节点配网后短期广播,让手机发现自己。
派生(源码crypto.h)
staticinlineintbt_mesh_identity_key(constuint8_tnet_key[16],uint8_tidentity_key[16]){returnbt_mesh_id128(net_k