diff --git a/core/network_config_mgr.js b/core/network_config_mgr.js index 3609eabf..a53ff369 100644 --- a/core/network_config_mgr.js +++ b/core/network_config_mgr.js @@ -24,6 +24,7 @@ const { spawn } = require('child_process') const readline = require('readline'); const {Address4, Address6} = require('ip-address'); const _ = require('lodash'); +const uuid = require('uuid'); const pl = require('../platform/PlatformLoader.js'); const platform = pl.getPlatform(); const r = require('../util/firerouter.js'); @@ -671,7 +672,18 @@ class NetworkConfigManager { return errors; } + async validateNcid(networkConfig, inTransaction = false, skipNcid = false) { + const originConfig = await this.getActiveConfig(inTransaction); + if (originConfig && originConfig.ncid && networkConfig.ncid && originConfig.ncid !== networkConfig.ncid) { + if (!skipNcid) return ["ncid not match"]; + } + } + async saveConfig(networkConfig, transaction = false) { + // do not generate ncid in transaction + if (!networkConfig.ncid && !transaction) { + networkConfig.ncid = util.generateUUID(); + } const configString = JSON.stringify(networkConfig); if (configString) { await rclient.setAsync(transaction ? "sysdb:transaction:networkConfig" : "sysdb:networkConfig", configString); diff --git a/sensors/wlan_conf_update_sensor.js b/sensors/wlan_conf_update_sensor.js index e22ee468..cafe13f3 100644 --- a/sensors/wlan_conf_update_sensor.js +++ b/sensors/wlan_conf_update_sensor.js @@ -19,6 +19,7 @@ const fs = require('fs'); const ncm = require('../core/network_config_mgr.js'); const platform = require('../platform/PlatformLoader.js').getPlatform(); const r = require('../util/firerouter.js'); +const util = require('../util/util.js'); class WlanConfUpdateSensor extends Sensor { async run() { @@ -72,6 +73,8 @@ class WlanConfUpdateSensor extends Sensor { this.log.error(`Error occured while applying updated config`, errors); return; } + currentConfig.ncid = util.generateUUID(); + log.info("New ncid generated", currentConfig.ncid); await ncm.saveConfig(currentConfig, false); } } diff --git a/service/routes/config.js b/service/routes/config.js index 3cc056a2..462612b9 100644 --- a/service/routes/config.js +++ b/service/routes/config.js @@ -21,7 +21,9 @@ const bodyParser = require('body-parser'); const log = require('../../util/logger.js')(__filename); const ncm = require('../../core/network_config_mgr.js'); const ns = require('../../core/network_setup.js'); - +const util = require('../../util/util.js'); +const AsyncLock = require('async-lock'); +const lock = new AsyncLock(); const WLAN_FLAG_WEP = 0b1 const WLAN_FLAG_WPA = 0b10 @@ -32,6 +34,7 @@ const WLAN_FLAG_SAE = 0b100000 const WLAN_FLAG_PSK_SHA256 = 0b1000000 const WLAN_FLAG_EAP_SHA256 = 0b10000000 +const LOCK_NETWORK_CONFIG_NCID = "LOCK_NETWORK_CONFIG_NCID"; const _ = require('lodash'); const { exec } = require('child-process-promise'); @@ -254,6 +257,8 @@ router.post('/set', const transID = newConfig.transID; delete newConfig.transactionOp; // do not leave transactionOp in the saved config delete newConfig.transID; + const ignoreNcid = newConfig.ignoreNcid || false; + delete newConfig.ignoreNcid; if (transactionOp && !validTransactionOps.includes(transactionOp)) { const errMsg = `Unrecognized transactionOp in config: ${transactionOp}`; log.error(errMsg); @@ -308,6 +313,15 @@ router.post('/set', } } let errors = await ncm.validateConfig(newConfig); + if (errors && errors.length != 0) { + log.error("Invalid network config", errors); + res.status(400).json({errors: errors}); + return; + } + + await lock.acquire(LOCK_NETWORK_CONFIG_NCID, async () => { + try { + errors = await ncm.validateNcid(newConfig, inTransaction, ignoreNcid); if (errors && errors.length != 0) { log.error("Invalid network config", errors); res.status(400).json({errors: errors}); @@ -336,10 +350,21 @@ router.post('/set', currentTransID = null; }, T_REVERT_TIMEOUT); } + newConfig.ncid = util.generateUUID(); + log.info("New ncid generated", newConfig.ncid); await ncm.saveConfig(newConfig, inTransaction); + res.status(200).json({errors: errors}); } } + } catch (err) { + log.error("Cannot set network config", err.message); + res.status(500).json({errors: [err.message]}); + } + }).catch((err) => { + log.error("Cannot acquire LOCK_NETWORK_CONFIG_NCID", err.message); + res.status(500).json({errors: [err.message]}); + }); }); router.post('/prepare_env', diff --git a/tests/core/test_ncm.js b/tests/core/test_ncm.js new file mode 100644 index 00000000..917abe62 --- /dev/null +++ b/tests/core/test_ncm.js @@ -0,0 +1,62 @@ +/* Copyright 2016-2024 Firewalla Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +'use strict' + +let chai = require('chai'); +let expect = chai.expect; + +const ncm = require('../../core/network_config_mgr.js'); +let log = require('../../util/logger.js')(__filename, 'info'); +const rclient = require('../../util/redis_manager').getRedisClient(); + +describe('Test network config manager', function(){ + this.timeout(30000); + beforeEach((done) => ( + async() => { + this.testkey = "sysdb:transaction:networkConfig"; + this.origin = await rclient.getAsync(this.testkey); + this.nwkey = "sysdb:networkConfig"; + this.nw = await rclient.getAsync(this.nwkey); + done(); + })() + ); + + afterEach((done) => ( + async() => { + await rclient.setAsync(this.testkey, this.origin); + await rclient.setAsync(this.nwkey, this.nw); + done(); + })() + ); + + it('should validate network ncid', async()=> { + const nwConfig = {"version":1,"interface":{"phy":{"eth0":{}}},"ts":1726648571944}; + expect(await ncm.validateNcid(nwConfig, true)).to.be.undefined; + + await rclient.setAsync(this.testkey, `{"version":1,"interface":{"phy":{"eth0":{}}},"ts":1726648571944, "ncid":"test"}`); + expect(await ncm.validateNcid(nwConfig, true)).to.be.undefined; + }); + + it('should fail to validate network ncid', async()=> { + await rclient.setAsync(this.testkey, `{"version":1,"interface":{"phy":{"eth0":{}}},"ts":1726648571944, "ncid":"test"}`); + + const nwConfig = {"version":1,"interface":{"phy":{"eth0":{}}},"ts":1726648571944, ncid: "2df97f9efb0ad09b7201726801377449"}; + expect(await ncm.validateNcid(nwConfig, true)).to.be.eql(["ncid not match"]); + + expect(await ncm.validateNcid(nwConfig, true, true)).to.be.undefined; + }); + +}); diff --git a/tests/util/test_util.js b/tests/util/test_util.js new file mode 100644 index 00000000..dd5416cf --- /dev/null +++ b/tests/util/test_util.js @@ -0,0 +1,47 @@ +/* Copyright 2016-2024 Firewalla Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +'use strict' + +let chai = require('chai'); +let expect = chai.expect; + +let util = require('../../util/util.js'); +let log = require('../../util/logger.js')(__filename, 'info'); + +describe('Test util', function(){ + this.timeout(30000); + + before((done) => ( + async() => { + done(); + })() + ); + + after((done) => ( + async() => { + done(); + })() + ); + + + it('should generate uuid', async()=> { + const u = util.generateUUID(); + log.debug("generate uuid", u); + expect(u.length).to.be.equal(32); + }); + + +}); diff --git a/util/util.js b/util/util.js index 2df347ea..05a72c8f 100644 --- a/util/util.js +++ b/util/util.js @@ -18,6 +18,7 @@ const Promise = require('bluebird'); const { exec } = require('child-process-promise'); const log = require('../util/logger.js')('util'); +const uuid = require('uuid'); const _ = require('lodash') @@ -257,6 +258,11 @@ function parseNumList(str) { return result.filter(n => !isNaN(n)) } +function generateUUID() { + const ts = Date.now() + ''; + return uuid.v4().replace(/-/g,"").substring(ts.length) + ts; +} + module.exports = { extend: extend, getPreferredBName: getPreferredBName, @@ -266,6 +272,7 @@ module.exports = { getHexStrArray: getHexStrArray, generatePSK: generatePSK, generateWpaSupplicantConfig: generateWpaSupplicantConfig, + generateUUID, parseEscapedString, parseHexString, freqToChannel,