
网络层关键字段详解(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]))字段长度是否混淆是否加密空口可见IVI1 bit明文明文✅ 直接看NID7 bit明文明文✅ 直接看CTL1 bit混淆-❌ 需 Privacy KeyTTL7 bit混淆-❌ 需 Privacy KeySEQ24 bit混淆-❌ 需 Privacy KeySRC16 bit混淆-❌ 需 Privacy KeyDST16 bit明文加密部分可见(加密后)Transport PDU变长-AES-CCM❌ 需 EncKeyNetMIC4/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_index0x01));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, 61=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 bitIV 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 NIDFriendship 建立后会派生独立的 Friendship 凭证(含独立 NID):原 NetKey 凭证:NID = 0x12(全网泛洪用) Friendship 凭证:NID = 0x47(LPN↔Friend 私聊用)LPN 和 Friend 通信时用 Friendship NID,其他节点看到 NID=0x47 不匹配自己的任何凭证,直接丢弃,实现私聊隔离。NID vs Network ID(重要区分)NID 和 Network ID 是两个完全不同的东西,容易混淆。维度NIDNetwork ID派生算法k2k3长度7 bit8 字节(64 bit)出现位置Network PDU 头部 Byte 0Secure 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派生算法输出用途出现位置k2NID + EncKey + PrivacyKeyNetwork PDU 收发(最常用)0x2A Network PDUk3Network ID(8B)Secure Network Beacon 标识子网0x2B Beaconk1Identity KeyNode Identity 广播(配网后短期可发现)Proxy 广播k1Beacon KeySecure Network Beacon 的 Auth Value 计算0x2B BeaconFriendship 凭证也由 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