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,