这篇笔记对 LoRaWAN 常见的 ABP 设备帧计数问题进行了追踪分析,介绍了帧计数禁用的调试办法,以及一个不大常见却又隐蔽的细节问题。希望帮助 LoRaWAN 初学者系统性地了解 LoRaWAN 的帧计数机制。
最近一周接连遇到了两个朋友关于 LoRaWAN 帧计数的问题咨询,特别是一个问题隐藏地比较深,好不容易排查了出来,因此做了笔记记录下。
一个是朋友A发来的,他的问题很典型,很多初学者都会遇到,就是 LoRaWAN 设备莫名其妙就不上报数据了,提示帧计数异常。
我们先从这个问题来说起。
这段协议英文还是比较拗口,中间有一句太长都不好断句,翻出了之前啃过的这段翻译(https://github.com/twowinter/LoRaWAN-Specification_ZH_CN),这段中的 provided 是假如的意思,以前没翻译出来,于是重新整理了下翻译。
终端入网成功后,终端和服务端的上下行帧计数同时置0。 每次发送消息后,发送端与之对应的 FCntUp 或 FCntDown 就会加1。
如果收到的帧计数相比当前计数有增长,同时两者相差小于 MAX_FCNT_GAP(考虑了计数翻转),接收方就按接收的帧计数更新对应值。
如果两者相差大于 MAX_FCNY_GAP 就说明中间丢失了很多数据,这条数据包就被丢掉。
所以我们可以小结下,一个合理的FCnt必须满足两个点: 1.FCnt 必须是增长的; 2.FCnt 的变化值必须小于 MAX_FCNT_GAP;
MAX_FCNT_GAP 是多少呢?在 LoRaWAN 区域参数规范里有介绍,咱们CN470中这个数值是 16384。
回到开头的问题上。
OTAA设备一般很少遇到FCnt的问题,因为每次设备重启可能都会进行Join,这样FCnt直接都置为0了。 而ABP设备没有Join操作,很多设备没做FCnt的保存,一旦重启,设备FCnt归0,而NS还是之前的旧值,那接下来的数据都会被丢弃,一直得等 FCnt 递增超过之前的大小。
同学可能会问,为什么一定要等 FCnt 超过缓存值才能正常通信呢?
因为设备上报的FCnt如果是一个旧的历史值,那说明这是一个收到过的数据,旧的数据就没必要处理了。如果不这样设计的话,那极端情况下,可能会被重放攻击。比如一个水表场景,我可以录下水表过去的一包数据,表明水表的度数,等要交水费重放这一个数据包,这样就不用交水费了。
那一些朋友可能手头就只有一个不完善的ABP设备,一旦重启设备FCnt重置就无法通信了,那该怎么办?
简单的办法是这样,每次重启了就手动在 NS 后台重置下这个 FCnt,让NS的缓存计数也清零。
这样每次重启都要操作NS,还有更简单的办法吗?
开源协议栈 chirpStack 为了方便开发者调试,提供了一个选项 Disable frame-counter validation,可以禁用掉对帧计数的校验。
正常情况下,FCnt 正常,那数据传输也正常,比如下面这两种情况,只要FCnt是增加的,且GAP不超过限值,那都OK
DeviceFCntUp 80 SessionFCntUp 0
DeviceFCntUp 10080 SessionFCntUp 0
禁用了帧计数校验后,就很潇洒了,即使FCnt乱来也都能正常进行数据传输。比如下面的两种情况,一个是设备FCnt重置了,一个是设备FCnt与平台的FCnt差值超过了限值,即使是这样的情况,NS也能正常传输这些设备数据。
DeviceFCntUp 0 SessionFCntUp 249 // FCnt 回滚
DeviceFCntUp 30080 SessionFCntUp 249 // FCnt 超过 MAX_FCNT_GAP
照理来说这个帧计数校验挺好用的,至少小能手使用起来还挺方便。
但最近一个伙伴在进行NS迁移(将一些设备从旧的NS迁到新的NS)时就遇到了一个问题, 明明在新的NS上禁用掉了帧计数校验,设备数据也从网关上报到了NS,但NS却拦截了这个设备的数据,迟迟无法正常传输。
于是找到了 NS 的代码研究起来,伪代码如下,/internal/storage/device_session.go。
func GetDeviceSessionForPHYPayload() {
...
fullFCnt, ok := ValidateAndGetFullFCntUp(s, macPL.FHDR.FCnt)
if !ok {
// If RelaxFCnt is turned on, just trust the uplink FCnt
// this is insecure, but has been requested by many people for
// debugging purposes.
// Note that we do not reset the FCntDown as this would reset the
// downlink frame-counter on a re-transmit, which is not what we
// want.
if s.SkipFCntValidation {
fullFCnt = macPL.FHDR.FCnt
s.FCntUp = macPL.FHDR.FCnt
s.UplinkHistory = []UplinkHistory{}
// validate if the mic is valid given the FCnt reset
// note that we can always set the ConfFCnt as the validation
// function will only use it when the ACK bit is set
micOK, err := phy.ValidateUplinkDataMIC(s.GetMACVersion(), s.ConfFCnt, uint8(txDR), uint8(txCh), s.FNwkSIntKey, s.SNwkSIntKey)
if err != nil {
return DeviceSession{}, errors.Wrap(err, "validate mic error")
}
}
}
...
}
func ValidateAndGetFullFCntUp(s DeviceSession, fCntUp uint32) (uint32, bool) {
// we need to compare the difference of the 16 LSB
gap := uint32(uint16(fCntUp) - uint16(s.FCntUp%65536))
if gap < band.Band().GetDefaults().MaxFCntGap {
return s.FCntUp + gap, true
}
return 0, false
}
从日志来看,DeviceFCntUp 已经达到了 105007,因此超过了 MAX_FCNT_GAP,代码逻辑进入到了 SkipFCntValidation 的条件处理中,在这里返回了 MIC 校验出错的结果。
小能手于是用模拟器做了一些测试,最终定位到 FCnt 上,在 SessionFCntUp 为 0 的情况下,DeviceFCntUp 如果小于 65535 则没问题,超过 65535 则会 MIC 报错。
DeviceFCntUp 57921 SessionFCntUp 0 // mic OK
DeviceFCntUp 123456 SessionFCntUp 0 // mic err
于是推断出来原因应该是这样,FCnt 在空中(LoRaMAC)只能传输16bit,因此 NS 无法知道设备 FCnt 是否大于65535,只能按照低16bit来处理。
所以在这种情况下,FCnt 的同步就成问题了,只能手动告诉 NS 当前 FCnt,这时候 NS 上的 SessionFCntUp 只要低于 DeviceFCntUp,且差值不超 MAX_FCNT_GAP 就可以了。
DeviceFCntUp 是 105007,我在 NS 后台配了个 100007,过一会儿设备便正常上报了。问题解决。