From 3226a879a8ff9bdc0a9deb16bd55327e096c0849 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Thu, 17 Aug 2023 21:53:21 +0200 Subject: [PATCH] fix: Don't remove devices with linkkey from backup if they are still present in the database (#746) * fix: Don't remove devices with linkkey from backup if they are still present in the database. * updates --- src/adapter/adapter.ts | 2 +- src/adapter/z-stack/adapter/adapter-backup.ts | 27 ++++++++++- src/adapter/z-stack/adapter/zStackAdapter.ts | 4 +- src/controller/controller.ts | 4 +- test/adapter/z-stack/adapter.test.ts | 46 ++++++++++++++----- 5 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index a4d11b7a34..394d3bf9bf 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -157,7 +157,7 @@ abstract class Adapter extends events.EventEmitter { public abstract supportsBackup(): Promise; - public abstract backup(): Promise; + public abstract backup(ieeeAddressesInDatabase: string[]): Promise; public abstract getNetworkParameters(): Promise; diff --git a/src/adapter/z-stack/adapter/adapter-backup.ts b/src/adapter/z-stack/adapter/adapter-backup.ts index 0fe9600a33..e8941971c0 100644 --- a/src/adapter/z-stack/adapter/adapter-backup.ts +++ b/src/adapter/z-stack/adapter/adapter-backup.ts @@ -58,7 +58,7 @@ export class AdapterBackup { /** * Creates a new backup from connected ZNP adapter and returns it in internal backup model format. */ - public async createBackup(): Promise { + public async createBackup(ieeeAddressesInDatabase: string[]): Promise { this.debug("creating backup"); const version: ZnpVersion = await this.getAdapterVersion(); @@ -129,7 +129,7 @@ export class AdapterBackup { /* return backup structure */ /* istanbul ignore next */ - return { + const backup: Models.Backup = { znp: { version: version, trustCenterLinkKeySeed: tclkSeed?.key || undefined @@ -192,6 +192,29 @@ export class AdapterBackup { }; }).filter(e => e) || [] }; + + try { + /** + * Due to a bug in ZStack, some devices go missing from the backed-up device tables which makes them disappear from the backup. + * This causes the devices not to be restored when e.g. re-flashing the adapter. + * If you then try to join a new device via a Zigbee 3.0 router that went missing (those with a linkkey), joning fails as the coordinator + * does not have the linkKey anymore. + * Below we don't remove any devices from the backup which have a linkkey and are still in the database (=ieeeAddressesInDatabase) + */ + const oldBackup = await this.getStoredBackup(); + const missing = oldBackup.devices.filter((d) => + d.linkKey && ieeeAddressesInDatabase.includes(`0x${d.ieeeAddress.toString("hex")}`) && + !backup.devices.find((dd) => d.ieeeAddress === dd.ieeeAddress)); + this.debug( + `Following devices with link key are missing from new backup but present in old backup and database, ` + + `adding them back: ${missing.map((d) => d.ieeeAddress).join(', ')}` + ); + backup.devices = [...backup.devices, ...missing]; + } catch (error) { + this.debug(`Failed to read old backup, not checking for missing routers: ${error}`); + } + + return backup; } /** diff --git a/src/adapter/z-stack/adapter/zStackAdapter.ts b/src/adapter/z-stack/adapter/zStackAdapter.ts index 399acdc44d..e3d8658ef8 100644 --- a/src/adapter/z-stack/adapter/zStackAdapter.ts +++ b/src/adapter/z-stack/adapter/zStackAdapter.ts @@ -852,8 +852,8 @@ class ZStackAdapter extends Adapter { return true; } - public async backup(): Promise { - return this.adapterManager.backup.createBackup(); + public async backup(ieeeAddressesInDatabase: string[]): Promise { + return this.adapterManager.backup.createBackup(ieeeAddressesInDatabase); } public async setChannelInterPAN(channel: number): Promise { diff --git a/src/controller/controller.ts b/src/controller/controller.ts index adf0a67a21..0e8741b37d 100644 --- a/src/controller/controller.ts +++ b/src/controller/controller.ts @@ -325,7 +325,7 @@ class Controller extends events.EventEmitter { this.databaseSave(); if (this.options.backupPath && await this.adapter.supportsBackup()) { debug.log('Creating coordinator backup'); - const backup = await this.adapter.backup(); + const backup = await this.adapter.backup(Device.all().map((d) => d.ieeeAddr)); const unifiedBackup = await BackupUtils.toUnifiedBackup(backup); const tmpBackupPath = this.options.backupPath + '.tmp'; fs.writeFileSync(tmpBackupPath, JSON.stringify(unifiedBackup, null, 2)); @@ -336,7 +336,7 @@ class Controller extends events.EventEmitter { public async coordinatorCheck(): Promise<{missingRouters: Device[]}> { if (await this.adapter.supportsBackup()) { - const backup = await this.adapter.backup(); + const backup = await this.adapter.backup(Device.all().map((d) => d.ieeeAddr)); const devicesInBackup = backup.devices.map((d) => `0x${d.ieeeAddress.toString('hex')}`); const missingRouters = this.getDevices() .filter((d) => d.type === 'Router' && !devicesInBackup.includes(d.ieeeAddr)); diff --git a/test/adapter/z-stack/adapter.test.ts b/test/adapter/z-stack/adapter.test.ts index 85ed6ec299..fd80746fdb 100644 --- a/test/adapter/z-stack/adapter.test.ts +++ b/test/adapter/z-stack/adapter.test.ts @@ -13,7 +13,6 @@ import * as Zcl from '../../../src/zcl'; import * as Constants from '../../../src/adapter/z-stack/constants'; import {ZclDataPayload} from "../../../src/adapter/events"; import {UnifiedBackupStorage} from "../../../src/models"; -import {ZnpAdapterManager} from "../../../src/adapter/z-stack/adapter/manager"; const deepClone = (obj) => JSON.parse(JSON.stringify(obj)); const mockSetTimeout = () => setTimeout = jest.fn().mockImplementation((r) => r()); @@ -1292,7 +1291,7 @@ describe("zstack-adapter", () => { const result = await adapter.start(); expect(result).toBe("restored"); - await adapter.backup(); + await adapter.backup([]); }); it("should restore unified backup with 3.0.x adapter and create backup - no tclk seed", async () => { @@ -1305,7 +1304,7 @@ describe("zstack-adapter", () => { const result = await adapter.start(); expect(result).toBe("restored"); - await adapter.backup(); + await adapter.backup([]); }); it("should restore unified backup with 3.x.0 adapter and create backup - empty", async () => { @@ -1316,7 +1315,7 @@ describe("zstack-adapter", () => { const result = await adapter.start(); expect(result).toBe("restored"); - await adapter.backup(); + await adapter.backup([]); }); it("should (recommission) restore unified backup with 1.2 adapter and create backup - empty", async () => { @@ -1327,7 +1326,7 @@ describe("zstack-adapter", () => { const result = await adapter.start(); expect(result).toBe("restored"); - const backup = await adapter.backup(); + const backup = await adapter.backup([]); expect(backup.networkKeyInfo.frameCounter).toBe(0); }); @@ -1344,7 +1343,7 @@ describe("zstack-adapter", () => { builder.nv(NvItemsIds.LEGACY_NWK_SEC_MATERIAL_TABLE_START + 0, secMaterialTableEntry.serialize("aligned")); mockZnpRequestWith(builder); - const backup = await adapter.backup(); + const backup = await adapter.backup([]); expect(backup.networkKeyInfo.frameCounter).toBe(2800); }); @@ -1356,7 +1355,7 @@ describe("zstack-adapter", () => { for (let i = 0; i < 4; i++) { builder.nv(NvItemsIds.LEGACY_NWK_SEC_MATERIAL_TABLE_START + i, Buffer.from("000000000000000000000000", "hex")); } mockZnpRequestWith(builder); - const backup = await adapter.backup(); + const backup = await adapter.backup([]); expect(backup.networkKeyInfo.frameCounter).toBe(1250); }); @@ -1372,7 +1371,7 @@ describe("zstack-adapter", () => { builder.nv(NvItemsIds.LEGACY_NWK_SEC_MATERIAL_TABLE_START + 3, genericEntry.serialize("aligned")); mockZnpRequestWith(builder); - const backup = await adapter.backup(); + const backup = await adapter.backup([]); expect(backup.networkKeyInfo.frameCounter).toBe(8737); }); @@ -1381,10 +1380,33 @@ describe("zstack-adapter", () => { const result = await adapter.start(); expect(result).toBe("resumed"); - const backup = await adapter.backup(); + const backup = await adapter.backup([]); expect(backup.networkKeyInfo.frameCounter).toBe(0); }); + it("should keep missing devices in backup", async () => { + const backupFile = getTempFile(); + const backupWithMissingDevice = JSON.parse(JSON.stringify(backupMatchingConfig)); + backupWithMissingDevice.devices.push({ + "nwk_address": "20fa", + "ieee_address": "00128d11124fa80b", + "link_key": { + "key": "bff550908aa1529ee90eea3c3bdc26fc", + "rx_counter": 0, + "tx_counter": 2 + } + }); + fs.writeFileSync(backupFile, JSON.stringify(backupMatchingConfig), "utf8"); + adapter = new ZStackAdapter(networkOptions, serialPortOptions, backupFile, {concurrent: 3}); + mockZnpRequestWith(empty3AlignedRequestMock); + await adapter.start(); + fs.writeFileSync(backupFile, JSON.stringify(backupWithMissingDevice), "utf8"); + const backup = await adapter.backup(['0x00128d11124fa80b']); + const missingDevice = backup.devices.find((d) => d.ieeeAddress.toString('hex') == '00128d11124fa80b'); + expect(missingDevice).not.toBeNull(); + expect(missingDevice?.linkKey?.key.toString('hex')).toBe('bff550908aa1529ee90eea3c3bdc26fc'); + }); + it("should fail when backup file is corrupted - Coordinator backup is corrupted", async () => { const backupFile = getTempFile(); fs.writeFileSync(backupFile, "{", "utf8"); @@ -1485,7 +1507,7 @@ describe("zstack-adapter", () => { ); const result = await adapter.start(); expect(result).toBe("resumed"); - await expect(adapter.backup()).rejects.toThrowError("Failed to read adapter IEEE address"); + await expect(adapter.backup([])).rejects.toThrowError("Failed to read adapter IEEE address"); }); it("should fail to create backup with 3.0.x adapter - adapter not commissioned - missing nib", async () => { @@ -1495,7 +1517,7 @@ describe("zstack-adapter", () => { expect(result).toBe("reset"); builder.nv(NvItemsIds.NIB, null); mockZnpRequestWith(builder); - await expect(adapter.backup()).rejects.toThrowError("Cannot backup - adapter not commissioned"); + await expect(adapter.backup([])).rejects.toThrowError("Cannot backup - adapter not commissioned"); }); it("should fail to create backup with 3.0.x adapter - missing active key info", async () => { @@ -1505,7 +1527,7 @@ describe("zstack-adapter", () => { expect(result).toBe("reset"); builder.nv(NvItemsIds.NWK_ACTIVE_KEY_INFO, null); mockZnpRequestWith(builder); - await expect(adapter.backup()).rejects.toThrowError("Cannot backup - missing active key info"); + await expect(adapter.backup([])).rejects.toThrowError("Cannot backup - missing active key info"); }); it("should restore legacy backup with 3.0.x adapter - empty", async () => {