diff --git a/config/config.json b/config/config.json
index c9edb159..548f2835 100644
--- a/config/config.json
+++ b/config/config.json
@@ -58,6 +58,9 @@
"file_path": "./routing/routing_plugin.js",
"config_path": "routing",
"category": "routing",
+ "config": {
+ "smooth_failover": true
+ },
"init_seq": 5
},
{
diff --git a/platform/goldpro/platform.sh b/platform/goldpro/platform.sh
index cb2c333c..16f9d392 100644
--- a/platform/goldpro/platform.sh
+++ b/platform/goldpro/platform.sh
@@ -1,4 +1,10 @@
NETWORK_SETUP=yes
+NEED_FIRESTATUS=true
+BLUETOOTH_TIMEOUT=3600
+
+function get_pppoe_rps_cpus {
+ echo "e"
+}
function run_horse_light {
flash_interval=${1:-2}
diff --git a/platform/platform.sh b/platform/platform.sh
index a1770731..20f1027c 100644
--- a/platform/platform.sh
+++ b/platform/platform.sh
@@ -4,7 +4,7 @@ FW_PLATFORM_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
UNAME=$(uname -m)
NETWORK_SETUP=yes
-BLUETOOTH_TIMEOUT=0
+BLUETOOTH_TIMEOUT=3600
function run_horse_light {
return
diff --git a/plugins/hostapd/hostapd_plugin.js b/plugins/hostapd/hostapd_plugin.js
index 6b030652..bb0106c4 100644
--- a/plugins/hostapd/hostapd_plugin.js
+++ b/plugins/hostapd/hostapd_plugin.js
@@ -97,6 +97,10 @@ class HostapdPlugin extends Plugin {
}
parameters.bridge = this.networkConfig.bridge;
}
+ if (platform.wifiSD && !await r.verifyPermanentMAC(this.name)) {
+ this.log.error(`Permanent MAC address of ${this.name} is not valid, ignore it`);
+ return;
+ }
if (params.ht_capab && !Array.isArray(params.ht_capab)) delete params.ht_capab
diff --git a/plugins/interface/intf_base_plugin.js b/plugins/interface/intf_base_plugin.js
index ae771061..fd846074 100644
--- a/plugins/interface/intf_base_plugin.js
+++ b/plugins/interface/intf_base_plugin.js
@@ -921,7 +921,7 @@ class InterfaceBasePlugin extends Plugin {
const dnsResult = await this.getDNSResult(u.hostname).catch((err) => false);
if(!dnsResult) {
- this.log.error("failed to resolve dns on domain", u.hostname);
+ this.log.error("failed to resolve dns on domain", u.hostname, 'on', this.name);
delete this.isHttpTesting[defaultTestURL];
return null;
}
@@ -1174,7 +1174,7 @@ class InterfaceBasePlugin extends Plugin {
}
const result = await Promise.any(promises).catch((err) => {
- this.log.error("no valid dns from any nameservers", err.message);
+ this.log.error("no valid dns from any nameservers on", this.name, err.message);
return null;
});
return result;
diff --git a/plugins/interface/wlan_intf_plugin.js b/plugins/interface/wlan_intf_plugin.js
index ece5169e..bcd0027c 100644
--- a/plugins/interface/wlan_intf_plugin.js
+++ b/plugins/interface/wlan_intf_plugin.js
@@ -148,6 +148,14 @@ class WLANInterfacePlugin extends InterfaceBasePlugin {
return true;
}
+ async apply() {
+ if (platform.wifiSD && !await r.verifyPermanentMAC(this.name)) {
+ this.log.error(`Permanent MAC address of ${this.name} is not valid, ignore it`);
+ return;
+ }
+ await super.apply();
+ }
+
async flush() {
await super.flush();
diff --git a/plugins/plugin.js b/plugins/plugin.js
index c316f572..637a7ccb 100644
--- a/plugins/plugin.js
+++ b/plugins/plugin.js
@@ -30,7 +30,7 @@ class Plugin {
init(pluginConfig) {
this.pluginConfig = pluginConfig;
- this.log.info(`Initializing Plugin ${this.constructor.name}...`);
+ this.log.info(`Initializing Plugin ${this.constructor.name} ${JSON.stringify(pluginConfig)}...`);
}
configure(networkConfig) {
diff --git a/plugins/plugin_loader.js b/plugins/plugin_loader.js
index ae45c881..c457f901 100644
--- a/plugins/plugin_loader.js
+++ b/plugins/plugin_loader.js
@@ -69,7 +69,7 @@ async function initPlugins() {
log.info("Plugin initialized", pluginConfs);
}
-function createPluginInstance(category, name, constructor) {
+function createPluginInstance(category, name, constructor, config=null) {
let instance = pluginCategoryMap[category] && pluginCategoryMap[category][name];
if (instance)
return instance;
@@ -83,6 +83,9 @@ function createPluginInstance(category, name, constructor) {
instance = new constructor(name);
instance.name = name;
pluginCategoryMap[category][name] = instance;
+ if (config) {
+ instance.init(config);
+ }
log.info("Instance created", instance.name);
return instance;
}
@@ -179,7 +182,7 @@ async function reapply(config, dryRun = false) {
}
if (value) {
for (let name in value) {
- const instance = createPluginInstance(pluginConf.category, name, pluginConf.c);
+ const instance = createPluginInstance(pluginConf.category, name, pluginConf.c, pluginConf.config);
if (!instance)
continue;
instance._mark = 1;
diff --git a/plugins/routing/routing_plugin.js b/plugins/routing/routing_plugin.js
index f8842e83..d824ccc8 100644
--- a/plugins/routing/routing_plugin.js
+++ b/plugins/routing/routing_plugin.js
@@ -79,11 +79,11 @@ class RoutingPlugin extends Plugin {
}
if (!af || af == 4) {
// remove DNS specific routes
- if (_.isArray(this._dnsRoutes)) {
- for (const dnsRoute of this._dnsRoutes)
- await routing.removeRouteFromTable(dnsRoute.dest, dnsRoute.gw, dnsRoute.viaIntf, "main", 4).catch((err) => { });
+ if (_.isObject(this._dnsRoutes)) {
+ for (const dnsRoute of Object.keys(this._dnsRoutes).map(key => this._dnsRoutes[key]))
+ await routing.removeRouteFromTable(dnsRoute.dest, dnsRoute.gw, dnsRoute.viaIntf, dnsRoute.tableName ? dnsRoute.tableName :"main", 4).catch((err) => { });
}
- this._dnsRoutes = [];
+ this._dnsRoutes = {};
}
break;
}
@@ -165,13 +165,217 @@ class RoutingPlugin extends Plugin {
this.lastApplyTimestamp = now;
}
- async _applyActiveGlobalDefaultRouting(inAsyncContext = false, af = null) {
- this.meterApplyActiveGlobalDefaultRouting();
- // async context and apply/flush context should be mutually exclusive, so they acquire the same LOCK_SHARED
- await lock.acquire(inAsyncContext ? LOCK_SHARED : LOCK_APPLY_ACTIVE_WAN, async () => {
- // flush global default routing table, no need to touch global static routing table here
- await routing.flushRoutingTable(routing.RT_GLOBAL_DEFAULT, af);
- await routing.flushRoutingTable(routing.RT_GLOBAL_LOCAL, af);
+ // should be protected by a lock when invoke
+ async _removeDeviceRouting(intfPlugins, tableName, af = null) {
+ // remove dead wan device from route table
+ if (!intfPlugins) {
+ return;
+ }
+ for (const intf of intfPlugins) {
+ // ignore gateway arg
+ if (!af || af == 4) {
+ await routing.removeDeviceRouteRule(intf.name, tableName).then(() => {
+ }).catch((err) => {
+ this.log.warn(err.message);
+ });
+ }
+ if (!af || af == 6) {
+ await routing.removeDeviceRouteRule(intf.name, tableName, 6).then(() => {
+ }).catch((err) => {
+ this.log.warn(err.message);
+ });
+ }
+ }
+ }
+
+ async _removeDeviceDnsRouting(intfPlugins, af = null) {
+ if (!intfPlugins) {
+ return;
+ }
+ if (!af || af == 4) {
+ for (const intf of intfPlugins) {
+ if (this._dnsRoutes && _.isArray(this._dnsRoutes[intf.name])) {
+ for (const dnsRoute of this._dnsRoutes[intf.name]){
+ await routing.removeRouteFromTable(dnsRoute.dest, dnsRoute.gw, dnsRoute.viaIntf, dnsRoute.tableName ? dnsRoute.tableName: "main", 4).catch((err) => {
+ this.log.warn('fail to remove dns route from table main, err:', err.message)
+ })
+ }
+ delete (this._dnsRoutes, intf.name);
+ }
+ }
+ }
+ }
+
+ async _removeDeviceDefaultRouting(intfPlugins, tableName, af = null) {
+ if (!intfPlugins) {
+ return;
+ }
+ for (const intf of intfPlugins) {
+ if (!af || af == 4) {
+ await routing.removeRouteFromTable("default", null, intf.name, tableName).catch((err) => {
+ this.log.warn(`fail to remove route -4 default dev ${intf.name} table ${tableName}, err:`, err.message);
+ });
+ }
+ if (!af || af == 6) {
+ await routing.removeRouteFromTable("default", null, intf.name, tableName, 6).catch((err) => {
+ this.log.warn(`fail to remove route -6 default dev ${intf.name} table ${tableName}, err:`, err.message);
+ });
+ }
+ }
+ }
+
+ _updateDnsRouteCache(dnsIP, gw, viaIntf, metric, tableName="main") {
+ if (!this._dnsRoutes){
+ this._dnsRoutes = {}
+ }
+ if (!this._dnsRoutes[viaIntf]) {
+ this._dnsRoutes[viaIntf] = [];
+ }
+ for (const dns of this._dnsRoutes[viaIntf]) {
+ if (dns.dest == dnsIP && dns.gw == gw && dns.viaIntf == viaIntf && dns.metric == metric && dns.tableName == tableName) {
+ // ensure no duplicates
+ return;
+ }
+ }
+ this._dnsRoutes[viaIntf].push({dest: dnsIP, gw: gw, viaIntf: viaIntf, metric: metric, tableName: tableName});
+ }
+
+ async refreshGlobalIntfRoutes(intf, af = null) {
+ await lock.acquire(LOCK_SHARED, async () => {
+ // update global routes
+ const intfPlugin = this._wanStatus[intf] && this._wanStatus[intf].plugin;
+ if (!intfPlugin) {
+ return
+ }
+ const state = await intfPlugin.state();
+ const metric = this._wanStatus[intf].seq + 1 + (this._wanStatus[intf].ready ? 0 : 100);
+
+ if ( !af || af == 4 ) {
+ if (state && state.ip4s) {
+ await routing.removeDeviceRouteRule(intf, routing.RT_GLOBAL_LOCAL, 4).catch((err) => {this.log.warn(err.message)});
+ await routing.removeDeviceRouteRule(intf, routing.RT_GLOBAL_DEFAULT, 4).catch((err) => {this.log.warn(err.message)});
+ for (const ip4 of state.ip4s) {
+ const addr = new Address4(ip4);
+ const networkAddr = addr.startAddress();
+ const cidr = `${networkAddr.correctForm()}/${addr.subnetMask}`;
+ await routing.addRouteToTable(cidr, null, intf, routing.RT_GLOBAL_LOCAL, metric, 4).catch((err) => {
+ this.log.warn(`fail to add route ${cidr} dev ${intf} table ${routing.RT_GLOBAL_LOCAL}, err:`, err.message)});
+ await routing.addRouteToTable(cidr, null, intf, routing.RT_GLOBAL_DEFAULT, metric, 4).catch((err) => {
+ this.log.warn(`fail to add route ${cidr} dev ${intf} table ${routing.RT_GLOBAL_DEFAULT}, err:`, err.message)});
+ }
+ }
+ }
+
+ if ( !af || af == 6 ) {
+ if (state && state.ip6) {
+ await routing.removeDeviceRouteRule(intf, routing.RT_GLOBAL_LOCAL, 6).catch((err) => {this.log.warn(err.message)});
+ await routing.removeDeviceRouteRule(intf, routing.RT_GLOBAL_DEFAULT, 6).catch((err) => {this.log.warn(err.message)});
+ for (const ip6 of state.ip6) {
+ const addr = new Address6(ip6);
+ const networkAddr = addr.startAddress();
+ const cidr = `${networkAddr.correctForm()}/${addr.subnetMask}`;
+ await routing.addRouteToTable(cidr, null, intf, routing.RT_GLOBAL_LOCAL, metric, 6).catch((err) => {
+ this.log.warn(`fail to add route -6 ${cidr} dev ${intf} table ${routing.RT_GLOBAL_LOCAL}, err:`, err.message)});
+ await routing.addRouteToTable(cidr, null, intf, routing.RT_GLOBAL_DEFAULT, metric, 6).catch((err) => {
+ this.log.warn(`fail to add route -6 ${cidr} dev ${intf} table ${routing.RT_GLOBAL_DEFAULT}, err:`, err.message)});
+ }
+ }
+ }
+
+ // update default routes
+ if ( !af || af == 4 ) {
+ const gw = await routing.getInterfaceGWIP(intf, 4);
+ if (gw) {
+ await this.upsertRouteToTable("default", gw, intf, routing.RT_GLOBAL_DEFAULT, metric, 4).catch((err) => { this.log.warn('fail to upsert route', err.message)});
+ await this.upsertRouteToTable("default", gw, intf, "main", metric, 4).catch((err) => { this.log.warn('fail to upsert route', err.message)});
+
+ // remove routes in table main except for default
+ const mainRules = await routing.searchRouteRules(null, null, intf, 'main');
+ if (_.isArray(mainRules) && mainRules.length > 0) {
+ for (const rule of mainRules) {
+ if (rule.includes('default') || !rule.includes('via')) { // skip default
+ continue
+ }
+ const cmd = `sudo ip -${af} route del ${rule} dev ${intf} table main`;
+ this.log.debug(`[routing] remove route from table main: ${cmd}`);
+ await exec(cmd).catch((err) => {this.log.warn(`fail to delete route ${cmd}`, err.message)});
+ }
+ }
+
+ const dns = await intfPlugin.getDNSNameservers();
+ if (_.isArray(dns) && dns.length > 0) {
+ for (const dnsIP of dns) {
+ await routing.addRouteToTable(dnsIP, gw, intf, routing.RT_GLOBAL_DEFAULT, metric, 4).catch((err) => {
+ this.log.warn(`fail to add route -4 ${dnsIP} via ${gw} dev ${intf} table ${routing.RT_GLOBAL_DEFAULT}, err:`, err.message)});
+ await routing.addRouteToTable(dnsIP, gw, intf, "main", metric, 4).then(() => {
+ this._updateDnsRouteCache(dnsIP, gw, viaIntf, metric, "main");
+ }).catch((err) => {
+ this.log.warn(`fail to add route -4 ${dnsIP} via ${gw} dev ${intf} table main, err:`, err.message)});
+ }
+ }
+ }
+ }
+
+ if ( !af || af == 6 ) {
+ const gw6 = await routing.getInterfaceGWIP(intf, 6);
+ if (gw6) {
+ await this.upsertRouteToTable("default", gw6, intf, routing.RT_GLOBAL_DEFAULT, metric, 6).catch((err) => { this.log.warn('fail to upsert route', err)});
+ await this.upsertRouteToTable("default", gw6, intf, "main", metric, 6).catch((err) => { this.log.warn('fail to upsert route', err)});
+ }
+ }
+ });
+ }
+
+ async upsertRouteToTable(dest, gateway, intf, tableName, metric, af = 4, type = "unicast") {
+ let replace = false;
+ // check if exists (dest, gateway, int, tableName, metric=null, af)
+ const currentRules = await routing.searchRouteRules(dest, null, intf, tableName, null, af);
+ this.log.debug(`[upsertRoute] search routes found (-${af} ${dest} dev ${intf} table ${tableName}): ${JSON.stringify(currentRules)}`);
+ if (currentRules && currentRules.length > 0) {
+ replace = true;
+ }
+ await routing.addRouteToTable(dest, gateway, intf, tableName, metric, af, replace, type).catch((err) => {
+ this.log.warn(`fail to ${replace?'replace':'add' } route (${dest} via ${gateway} dev ${intf} table ${tableName}), err:`, err.message);
+ });
+
+ // remove but keep the newly added rule
+ if (currentRules && currentRules.length > 0) {
+ for (const rule of currentRules) {
+ const rulemetric = this._getRouteRuleMetric(rule);
+ const rulegateway = this._getRouteRuleGateway(rule);
+ if ( ((!metric && !rulemetric) || rulemetric == metric) && ((!rulegateway && !gateway) || rulegateway == gateway) ){
+ continue;
+ }
+ const cmd = `sudo ip -${af} route del ${rule} dev ${intf} table ${tableName}`;
+ this.log.debug(`[upsertRoute] remove route from table ${tableName}: ${cmd}`);
+ await exec(cmd).catch((err) => {this.log.warn(`fail to delete route`, err.message)});
+ }
+ }
+ }
+
+ _getRouteRuleMetric(rule) {
+ const matches = rule.match(/metric\s(\d+)/);
+ if (matches && matches.length >= 1){
+ return matches[1];
+ }
+ const preferences = rule.match(/preference\s(\d+)/);
+ if (preferences && preferences.length >= 1){
+ return preferences[1];
+ }
+
+ return null;
+ }
+
+ _getRouteRuleGateway(rule) {
+ const matches = rule.match(/via\s([\w:.]+)/);
+ if (matches && matches.length >= 1){
+ return matches[1];
+ }
+
+ return null;
+ }
+
+ async _removeMainRoutes(af=null) {
// remove all default route in main table
let routeRemoved = false;
if (!af || af == 4) {
@@ -180,6 +384,7 @@ class RoutingPlugin extends Plugin {
routeRemoved = true;
}).catch((err) => {
routeRemoved = false;
+ this.log.warn('fail to remove route default from table main, err:', err.message)
});
} while (routeRemoved)
}
@@ -190,17 +395,43 @@ class RoutingPlugin extends Plugin {
routeRemoved = true;
}).catch((err) => {
routeRemoved = false;
+ this.log.warn('fail to remove route default from table main, err:', err.message)
});
} while (routeRemoved)
}
if (!af || af == 4) {
// remove DNS specific routes
- if (_.isArray(this._dnsRoutes)) {
- for (const dnsRoute of this._dnsRoutes)
- await routing.removeRouteFromTable(dnsRoute.dest, dnsRoute.gw, dnsRoute.viaIntf, "main", 4).catch((err) => { });
+ if (_.isObject(this._dnsRoutes)) {
+ for (const dnsRoute of Object.keys(this._dnsRoutes).map(key => this._dnsRoutes[key])) {
+ await routing.removeRouteFromTable(dnsRoute.dest, dnsRoute.gw, dnsRoute.viaIntf, dnsRoute.tableName ? dnsRoute.tableName : "main", 4).catch((err) => {
+ this.log.warn('fail to remove dns route from table main, err:', err.message)
+ });
+ }
}
- this._dnsRoutes = [];
+ this._dnsRoutes = {};
+ }
+ }
+
+ async _applyActiveGlobalDefaultRouting(inAsyncContext = false, af = null) {
+ this.meterApplyActiveGlobalDefaultRouting();
+ // async context and apply/flush context should be mutually exclusive, so they acquire the same LOCK_SHARED
+ await lock.acquire(inAsyncContext ? LOCK_SHARED : LOCK_APPLY_ACTIVE_WAN, async () => {
+ // flush global default routing table, no need to touch global static routing table here
+ if (this.pluginConfig && this.pluginConfig.smooth_failover) {
+ const deadWANIntfs = this.getUnreadyWANPlugins();
+ await this._removeDeviceRouting(deadWANIntfs, routing.RT_GLOBAL_DEFAULT, af);
+ await this._removeDeviceRouting(deadWANIntfs, routing.RT_GLOBAL_LOCAL, af);
+ await this._removeDeviceDnsRouting(deadWANIntfs, af, "main");
+ await this._removeDeviceDefaultRouting(deadWANIntfs, "main", af);
+ } else {
+ await routing.flushRoutingTable(routing.RT_GLOBAL_DEFAULT, af);
+ await routing.flushRoutingTable(routing.RT_GLOBAL_LOCAL, af);
}
+
+ if (!this.pluginConfig || !this.pluginConfig.smooth_failover) {
+ await this._removeMainRoutes(af);
+ }
+
const type = this.networkConfig.default.type || "single";
switch (type) {
case "single":
@@ -221,8 +452,8 @@ class RoutingPlugin extends Plugin {
const addr = new Address4(ip4);
const networkAddr = addr.startAddress();
const cidr = `${networkAddr.correctForm()}/${addr.subnetMask}`;
- await routing.addRouteToTable(cidr, null, viaIntf, routing.RT_GLOBAL_LOCAL, metric).catch((err) => { });
- await routing.addRouteToTable(cidr, null, viaIntf, routing.RT_GLOBAL_DEFAULT, metric).catch((err) => { });
+ await this.upsertRouteToTable(cidr, null, viaIntf, routing.RT_GLOBAL_LOCAL, metric).catch((err) => { });
+ await this.upsertRouteToTable(cidr, null, viaIntf, routing.RT_GLOBAL_DEFAULT, metric).catch((err) => { });
}
} else {
this.log.error("Failed to get ip4 of global default interface " + viaIntf);
@@ -234,8 +465,8 @@ class RoutingPlugin extends Plugin {
const addr = new Address6(ip6Addr);
const networkAddr = addr.startAddress();
const cidr = `${networkAddr.correctForm()}/${addr.subnetMask}`;
- await routing.addRouteToTable(cidr, null, viaIntf, routing.RT_GLOBAL_LOCAL, metric, 6).catch((err) => { });
- await routing.addRouteToTable(cidr, null, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 6).catch((err) => { });
+ await this.upsertRouteToTable(cidr, null, viaIntf, routing.RT_GLOBAL_LOCAL, metric, 6).catch((err) => { });
+ await this.upsertRouteToTable(cidr, null, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 6).catch((err) => { });
}
} else {
this.log.info("No ip6 found on global default interface " + viaIntf);
@@ -245,39 +476,33 @@ class RoutingPlugin extends Plugin {
const gw = await routing.getInterfaceGWIP(viaIntf);
if (!af || af == 4) {
if (gw) { // IPv4 default route for inactive WAN is still required for WAN connectivity check
- await routing.addRouteToTable("default", gw, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 4).catch((err) => { });
- await routing.addRouteToTable("default", gw, viaIntf, "main", metric, 4).catch((err) => { });
+ await this.upsertRouteToTable("default", gw, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 4).catch((err) => { });
+ await this.upsertRouteToTable("default", gw, viaIntf, "main", metric, 4).catch((err) => { });
// add route for DNS nameserver IP in global_default table
const dns = await viaIntfPlugin.getDNSNameservers();
if (_.isArray(dns) && dns.length !== 0) {
for (const dnsIP of dns) {
- await routing.addRouteToTable(dnsIP, gw, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 4, true).catch((err) => {
+ await this.upsertRouteToTable(dnsIP, gw, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 4).catch((err) => {
this.log.error(`Failed to add route to ${routing.RT_GLOBAL_DEFAULT} for dns ${dnsIP} via ${gw} dev ${viaIntf}`, err.message);
});
- let dnsRouteRemoved = false;
- // remove all dns routes via the same interface but with different metrics in main table
- do {
- await routing.removeRouteFromTable(dnsIP, gw, viaIntf, "main").then(() => {
- dnsRouteRemoved = true;
- }).catch((err) => {
- dnsRouteRemoved = false;
- })
- } while (dnsRouteRemoved)
- await routing.addRouteToTable(dnsIP, gw, viaIntf, "main", metric, 4, true).catch((err) => {
+ // update all dns routes via the same interface but with new metrics in main table
+ await this.upsertRouteToTable(dnsIP, gw, viaIntf, "main", metric, 4).then(() => {
+ this._updateDnsRouteCache(dnsIP, gw, viaIntf, metric, "main");
+ }).catch((err) => {
this.log.error(`Failed to add route to main for dns ${dnsIP} via ${gw} dev ${viaIntf}`, err.message);
});
- this._dnsRoutes.push({dest: dnsIP, gw: gw, viaIntf: viaIntf, metric: metric});
}
}
} else {
this.log.error("Failed to get gateway IP of global default interface " + viaIntf);
}
}
+
const gw6 = await routing.getInterfaceGWIP(viaIntf, 6);
if (!af || af == 6) {
if (gw6 && (ready || type === "single")) { // do not add IPv6 default route for inactive WAN under dual WAN setup, WAN connectivity check only uses IPv4
- await routing.addRouteToTable("default", gw6, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 6).catch((err) => { });
- await routing.addRouteToTable("default", gw6, viaIntf, "main", metric, 6).catch((err) => { });
+ await this.upsertRouteToTable("default", gw6, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 6).catch((err) => { });
+ await this.upsertRouteToTable("default", gw6, viaIntf, "main", metric, 6).catch((err) => { });
} else {
this.log.info("IPv6 gateway is not defined on global default interface " + viaIntf);
}
@@ -351,7 +576,14 @@ class RoutingPlugin extends Plugin {
await routing.addRouteToTable(dnsIP, gw, viaIntf, "main", metric, 4, true).catch((err) => {
this.log.error(`Failed to add route to main for dns ${dnsIP} via ${gw} dev ${viaIntf}`, err.message);
});
- this._dnsRoutes.push({dest: dnsIP, gw: gw, viaIntf: viaIntf, metric: metric});
+ if (!this._dnsRoutes){
+ log.warn("should init _dnsRoutes in load_balance mode");
+ this._dnsRoutes = {}
+ }
+ if (!this._dnsRoutes[viaIntf]) {
+ this._dnsRoutes[viaIntf] = [];
+ }
+ this._dnsRoutes[viaIntf].push({dest: dnsIP, gw: gw, viaIntf: viaIntf, metric: metric, tableName: "main"});
}
}
} else {
@@ -398,6 +630,7 @@ class RoutingPlugin extends Plugin {
this.fatal(`Network config for ${this.name} is not set`);
return;
}
+
await lock.acquire(LOCK_SHARED, async () => {
const lastWanStatus = this._wanStatus || {};
if (!this._wanStatus)
@@ -418,7 +651,7 @@ class RoutingPlugin extends Plugin {
const viaIntf2 = settings.viaIntf2;
const viaIntf2Plugin = pl.getPluginInstance("interface", viaIntf2);
if (!viaIntf2Plugin)
- this.fatal(`Cannot find global defautl interface plugin ${viaIntf2}`);
+ this.fatal(`Cannot find global default interface plugin ${viaIntf2}`);
this.subscribeChangeFrom(viaIntf2Plugin);
wanStatus[viaIntf2] = {
active: false,
@@ -674,6 +907,14 @@ class RoutingPlugin extends Plugin {
return null;
}
+ getUnreadyWANPlugins() {
+ if (this._wanStatus){
+ return Object.keys(this._wanStatus).filter(i => this._wanStatus[i].ready == false).map(i => this._wanStatus[i].plugin);
+ }
+ else
+ return null;
+ }
+
getAllWANPlugins() {
if (this._wanStatus)
return Object.keys(this._wanStatus).sort((a, b) => this._wanStatus[a].seq - this._wanStatus[b].seq).map(i => this._wanStatus[i].plugin);
@@ -720,6 +961,18 @@ class RoutingPlugin extends Plugin {
const eventType = event.getEventType(e);
switch (eventType) {
case event.EVENT_IP_CHANGE: {
+ const payload = event.getEventPayload(e);
+ const intf = payload && payload.intf;
+ const intfPlugin = this._wanStatus[intf] && this._wanStatus[intf].plugin
+ const type = (this.networkConfig && this.networkConfig.default && this.networkConfig.default.type) || "single";
+
+ if (this.pluginConfig && this.pluginConfig.smooth_failover) {
+ // update global default routes related to the interface
+ if (intfPlugin && type == 'primary_standby' && this.name == 'global') {
+ this.refreshGlobalIntfRoutes(intf, 4);
+ break;
+ }
+ }
this._reapplyNeeded = true;
pl.scheduleReapply();
break;
diff --git a/scripts/firerouter_dhcpcd_update_rt b/scripts/firerouter_dhcpcd_update_rt
index b308508f..0cac5f9e 100644
--- a/scripts/firerouter_dhcpcd_update_rt
+++ b/scripts/firerouter_dhcpcd_update_rt
@@ -20,8 +20,11 @@ case $reason in
addr_id=1
while [ $addr_id -lt 10 ]; do
var_name="nd1_addr$addr_id"
+ prefix_length_var_name="nd1_prefix_information${addr_id}_length"
+ eval "prefix_length=\$$prefix_length_var_name"
+ echo "haha $prefix_length"
eval "addr=\$$var_name"
- if [ -n "$addr" ]; then
+ if [ -n "$addr" && $prefix_length -ge 64 ]; then
new_addrs="$new_addrs$addr,"
for rt_table in $rt_tables; do
sudo ip -6 r add $addr dev $interface metric $metric mtu $mtu table $rt_table
diff --git a/scripts/firerouter_ppp_ip_up b/scripts/firerouter_ppp_ip_up
index a993c890..13e10c58 100755
--- a/scripts/firerouter_ppp_ip_up
+++ b/scripts/firerouter_ppp_ip_up
@@ -29,5 +29,6 @@ fi
# adjust receive packet steering on pppoe interface
sudo bash -c "echo ${RPS_CPUS} > /sys/class/net/${INTF}/queues/rx-0/rps_cpus" || true
+sudo ethtool -K ${INTF} rx-udp-gro-forwarding on || true
redis-cli -n 1 publish "pppoe.ip_change" "$INTF"
\ No newline at end of file
diff --git a/sensors/wlan_conf_update_sensor.js b/sensors/wlan_conf_update_sensor.js
index e8e94a4b..e22ee468 100644
--- a/sensors/wlan_conf_update_sensor.js
+++ b/sensors/wlan_conf_update_sensor.js
@@ -47,6 +47,10 @@ class WlanConfUpdateSensor extends Sensor {
}
async checkAndUpdateNetworkConfig(iface) {
+ if (!await r.verifyPermanentMAC(iface)) {
+ this.log.error(`Permanent MAC address of ${iface} is not valid, ignore it`);
+ return;
+ }
await ncm.acquireConfigRWLock(async () => {
const currentConfig = await ncm.getActiveConfig();
if (currentConfig && currentConfig.interface) { // assume "interface" exists under root
diff --git a/tests/plugins/test_routing.js b/tests/plugins/test_routing.js
new file mode 100644
index 00000000..551fec43
--- /dev/null
+++ b/tests/plugins/test_routing.js
@@ -0,0 +1,257 @@
+/* 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 exec = require('child-process-promise').exec;
+let log = require('../../util/logger.js')(__filename, 'info');
+let routing = require('../../util/routing.js');
+let RoutingPlugin = require('../../plugins/routing/routing_plugin.js');
+let InterfaceBasePlugin = require('../../plugins/interface/intf_base_plugin.js');
+
+// path=routing {'routing': {...}} {"default":{"viaIntf":"eth0.204","type":"primary_standby","viaIntf2":"eth0","failback":true}}
+const routingConfig = {
+ "default":
+ {
+ "viaIntf": "eth0.204",
+ "type": "primary_standby",
+ "viaIntf2": "eth0",
+ "failback": true
+ }
+}
+
+const routingConfig_fo = {
+ "default":
+ {
+ "viaIntf": "eth0",
+ "type": "primary_standby",
+ "viaIntf2": "eth0.204",
+ "failback": true
+ }
+}
+
+describe('Test Routing WAN', function(){
+ this.timeout(30000);
+
+ before((done) => (
+ async() => {
+ this.plugin = new RoutingPlugin('routing');
+ this.plugin.init({smooth_failover: true});
+ this.needClean = false;
+ let result = await exec("sudo ip link show eth0.288").then( r => r.stdout).catch((err) => {log.debug(err.stderr)});
+ if (result && result !== "") {
+ log.warn("dev eth0.288 conflict, skip prepare");
+ done();
+ return;
+ }
+ result = await exec("sudo ip link add link eth0 name eth0.288 type vlan id 288").then(r => r.stderr).catch((err) => {log.error(err.stderr)});
+ if (result === '') {
+ this.needClean = true;
+ await exec("sudo ip addr add 10.88.8.1/32 dev eth0.288").catch((err) => {log.error("add dev", err.stderr)});
+ await exec("sudo ip link set dev eth0.288 up").catch((err) => {log.error("set dev up", err.stderr)});
+ await exec("ip route show table eth0.288_default").catch((err) => {log.error("show device route table", err.stderr)});
+ await exec("sudo ip route add table eth0.288_default default via 10.88.8.1").catch((err) => {log.error("add route", err.stderr)});
+ await exec("sudo ip route add table eth0.288_default 10.88.8.0/30 dev eth0.288").catch((err) => {log.error("add route", err.stderr)});
+ await exec("sudo ip route add table eth0.288_default 10.88.8.3/32 via 10.88.8.1 metric 223").catch((err) => {log.error("add route", err.stderr)});
+ await exec("sudo ip route add table eth0.288_default 10.88.8.2/32 dev eth0.288 proto kernel scope link src 10.88.8.1").catch((err) => {log.error("add route", err.stderr)});
+ }
+ // fake wan interfaces
+ this.plugin._wanStatus = {};
+ this.plugin._wanStatus["eth0"] = {seq:1, ready: true, active: false, plugin: new InterfaceBasePlugin('eth0')};
+ this.plugin._wanStatus["eth0.204"] = {seq:0, ready: true, active: true, plugin: new InterfaceBasePlugin('eth0.204')};
+ this.plugin._wanStatus["eth0.288"] = {ready: false, active: false, plugin: new InterfaceBasePlugin('eth0.288')};
+
+ await exec("echo 'nameserver 10.8.8.8' >> /home/pi/.router/run/eth0.288.resolv.conf").catch((err) => {log.error("add eth0.288 resolvconf err,", err.stderr)});
+ await exec("echo 'nameserver 8.8.8.8' >> /home/pi/.router/run/eth0.288.resolv.conf").catch((err) => {log.error("add eth0.288 resolvconf err,", err.stderr)});
+
+ done();
+ })()
+ );
+
+ after((done) => (
+ async() => {
+ if (this.needClean) {
+ await exec("sudo ip route flush table eth0.288_default dev eth0.288").catch((err) => {});
+ await exec("sudo ip link set dev eth0.288 down").catch((err) => {});
+ await exec("sudo ip addr del 10.88.8.1/32 dev eth0.288").catch((err) => {});
+ await exec("sudo ip link del eth0.288").catch((err) => {});
+ await exec("rm /home/pi/.router/run/eth0.288.resolv.conf").catch((err) => {log.error("rm eth0.288 resolvconf err,", err.stderr)});
+ }
+ done();
+ })()
+ );
+
+ it('should get unready WAN interfaces', () => {
+ const deadWANs = this.plugin.getUnreadyWANPlugins();
+ expect(deadWANs.length).to.be.equal(1);
+ expect(deadWANs[0].name).to.be.equal('eth0.288');
+ });
+
+ it('should remove dead device route rules', async() => {
+ const deadWANs = this.plugin.getUnreadyWANPlugins();
+
+ let results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(4);
+
+ await this.plugin._removeDeviceRouting(deadWANs, "eth0.288_default");
+
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(0);
+ });
+
+ it('should remove dead target route rules', async() => {
+ await exec("sudo ip route flush table eth0.288_default dev eth0.288").catch((err) => {});
+ await exec("sudo ip route add table eth0.288_default default via 10.88.8.1").catch((err) => {log.error("add route", err.message)});
+ await exec("sudo ip route add table eth0.288_default 8.8.8.8 via 10.88.8.1 dev eth0.288 metric 101").catch((err) => {log.error("add route", err.message)});
+ await exec("sudo ip route add table eth0.288_default 10.8.8.8 via 10.88.8.1 dev eth0.288 metric 101").catch((err) => {log.error("add route", err.message)});
+
+ this.plugin._dnsRoutes = {"eth0.288":[
+ {dest: '8.8.8.8', viaIntf: 'eth0.288', gw: '10.88.8.1', metric: 101, tableName: 'eth0.288_default'},
+ {dest: '10.8.8.8', viaIntf: 'eth0.288', gw: '10.88.8.1', metric: 101, tableName: 'eth0.288_default'},
+ ]};
+ const deadWANs = this.plugin.getUnreadyWANPlugins();
+ expect(deadWANs[0].name).to.be.equal('eth0.288');
+ expect(await deadWANs[0].getDNSNameservers()).to.be.eql(['10.8.8.8', '8.8.8.8']);
+
+ let results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(3);
+
+ // remote default route of dev eth0.288
+ await this.plugin._removeDeviceDefaultRouting(deadWANs, "eth0.288_default");
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(2);
+
+ // remove dns routes
+ await this.plugin._removeDeviceDnsRouting(deadWANs);
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(0);
+ });
+
+ it('should upsert route', async() => {
+ await exec("sudo ip route flush table eth0.288_default dev eth0.288").catch((err) => {});
+ await exec("sudo ip route add table eth0.288_default default via 10.88.8.1").catch((err) => {log.error("add route", err.message)});
+ await exec("sudo ip route add table eth0.288_default 10.88.8.0/30 dev eth0.288").catch((err) => {log.error("add route", err.message)});
+ await exec("sudo ip route add table eth0.288_default 10.88.8.3/32 via 10.88.8.1").catch((err) => {log.error("add route", err.message)});
+ await exec("sudo ip route add table eth0.288_default 10.88.8.3/32 via 10.88.8.1 metric 223").catch((err) => {log.error("add route", err.message)});
+
+ let results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(4);
+
+ await this.plugin.upsertRouteToTable('10.88.8.4/32', '10.88.8.1', 'eth0.288', 'eth0.288_default', 218);
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(5);
+
+ await this.plugin.upsertRouteToTable('10.88.8.3/32', '10.88.8.1', 'eth0.288', 'eth0.288_default', 289);
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(4);
+
+ await this.plugin.upsertRouteToTable('10.88.8.3/32', '10.88.8.1', 'eth0.288', 'eth0.288_default', 1);
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(4);
+ expect(results.includes('10.88.8.3 via 10.88.8.1 metric 1')).to.be.true;
+ expect(results.includes('10.88.8.3 via 10.88.8.1 metric 289')).to.be.false;
+
+ await this.plugin.upsertRouteToTable('10.88.8.3/32', '10.88.8.1', 'eth0.288', 'eth0.288_default', 1);
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(4);
+ expect(results.includes('10.88.8.3 via 10.88.8.1 metric 1')).to.be.true;
+
+ await this.plugin.upsertRouteToTable('10.88.8.3/32', '10.88.8.3', 'eth0.288', 'eth0.288_default', 1);
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(4);
+ expect(results.includes('10.88.8.3 via 10.88.8.3 metric 1')).to.be.true;
+ expect(results.includes('10.88.8.3 via 10.88.8.1 metric 1')).to.be.false;
+
+ await this.plugin.upsertRouteToTable('10.88.8.3/32', '10.88.8.1', 'eth0.288', 'eth0.288_default', 199);
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(4);
+ expect(results.includes('10.88.8.3 via 10.88.8.1 metric 199')).to.be.true;
+ expect(results.includes('10.88.8.3 via 10.88.8.3 metric 1')).to.be.false;
+
+ await this.plugin.upsertRouteToTable('default', '10.88.8.3', 'eth0.288', 'eth0.288_default', 2);
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'eth0.288_default');
+ expect(results.length).to.be.equal(4);
+ expect(results.includes('default via 10.88.8.3 metric 2')).to.be.true;
+ expect(results.includes('default via 10.88.8.1')).to.be.false;
+
+ });
+
+ it('should apply active global default routing with smooth_failover', async() => {
+ await this.plugin.upsertRouteToTable('default', '10.88.8.1', 'eth0.288', 'eth0.288_default', 2);
+ let results;
+
+ this.plugin.networkConfig = routingConfig_fo;
+ this.plugin._wanStatus['eth0.204'].ready = false;
+ await this.plugin._applyActiveGlobalDefaultRouting(false, 4);
+ results = await routing.searchRouteRules('default', null, null, 'main');
+ expect(results.includes('default via 192.168.10.254 dev eth0.204 metric 101')).to.be.true;
+ expect(results.includes('default via 192.168.203.1 dev eth0 metric 2')).to.be.true;
+
+ this.plugin.networkConfig = routingConfig;
+ this.plugin._wanStatus['eth0.204'].ready = true;
+ await this.plugin._applyActiveGlobalDefaultRouting(false, 4);
+ results = await routing.searchRouteRules('default', null, null, 'main');
+ expect(results.includes('default via 192.168.10.254 dev eth0.204 metric 1')).to.be.true;
+ expect(results.includes('default via 192.168.203.1 dev eth0 metric 2')).to.be.true;
+ });
+
+ it('should apply active global default routing without smooth_failover', async() => {
+ let results;
+
+ this.plugin.pluginConfig = {};
+ this.plugin.networkConfig = routingConfig_fo;
+ this.plugin._wanStatus['eth0.204'].ready = false;
+ await this.plugin._applyActiveGlobalDefaultRouting(false, 4);
+ results = await routing.searchRouteRules('default', null, null, 'main');
+ expect(results.includes('default via 192.168.10.254 dev eth0.204 metric 101')).to.be.true;
+ expect(results.includes('default via 192.168.203.1 dev eth0 metric 2')).to.be.true;
+
+ results = await routing.searchRouteRules(null, null, 'eth0.204', 'main');
+ expect(results.includes('192.168.10.0/24 proto kernel scope link src 192.168.10.135')).to.be.true;
+
+ this.plugin.networkConfig = routingConfig;
+ this.plugin._wanStatus['eth0.204'].ready = true;
+ await this.plugin._applyActiveGlobalDefaultRouting(false, 4);
+ results = await routing.searchRouteRules('default', null, null, 'main');
+ expect(results.includes('default via 192.168.10.254 dev eth0.204 metric 1')).to.be.true;
+ expect(results.includes('default via 192.168.203.1 dev eth0 metric 2')).to.be.true;
+ });
+
+ it('should get rule metric', () => {
+ expect(this.plugin._getRouteRuleMetric('table eth0.288_default 10.88.8.0/30 dev eth0.288')).to.be.null;
+ expect(this.plugin._getRouteRuleMetric('table eth0.288_default 10.88.8.0/30 dev eth0.288 metric 1')).to.be.equal('1');
+ expect(this.plugin._getRouteRuleMetric('fe80::/64 dev eth0.204 via fe80::226d:31ff:fe01:2b43 metric 1024 pref medium')).to.be.equal('1024');
+ });
+
+ it('should get rule gateway', () => {
+ expect(this.plugin._getRouteRuleGateway('table eth0.288_default 10.88.8.0/30 dev eth0.288')).to.be.null;
+ expect(this.plugin._getRouteRuleGateway('table eth0.288_default via 10.88.8.1 10.88.8.0/30 dev eth0.288 metric 1')).to.be.equal('10.88.8.1');
+ expect(this.plugin._getRouteRuleGateway('fe80::/64 dev eth0.204 via fe80::226d:31ff:fe01:2b43 metric 1024 pref medium')).to.be.equal('fe80::226d:31ff:fe01:2b43');
+ });
+
+ it('should update global interface routes', async() => {
+ let beforeResults = await routing.searchRouteRules(null, null, 'eth0', 'main');
+
+ await this.plugin.refreshGlobalIntfRoutes('eth0');
+
+ let afterResults = await routing.searchRouteRules(null, null, 'eth0', 'main');
+ expect(afterResults.length).to.be.equal(beforeResults.length);
+ });
+
+});
diff --git a/tests/util/test_routing.js b/tests/util/test_routing.js
new file mode 100644
index 00000000..6512b327
--- /dev/null
+++ b/tests/util/test_routing.js
@@ -0,0 +1,110 @@
+/* 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 exec = require('child-process-promise').exec;
+let log = require('../../util/logger.js')(__filename, 'info');
+let routing = require('../../util/routing.js');
+
+describe('Test routing', function(){
+ this.timeout(30000);
+
+ before((done) => (
+ async() => {
+ this.needClean = false;
+ let result = await exec("sudo ip link show eth0.288").then( r => r.stdout).catch((err) => {log.debug(err.stderr);});
+ if (result && result !== "") {
+ log.warn("dev eth0.288 conflict, skip test");
+ done();
+ return;
+ }
+ result = await exec("sudo ip link add link eth0 name eth0.288 type vlan id 288").then(r => r.stderr).catch((err) => {log.error(err.stderr);});
+ if (result === '') {
+ this.needClean = true;
+ await exec("sudo ip addr add 10.88.8.1/32 dev eth0.288").catch((err) => {log.error("add dev", err.stderr);});
+ await exec("sudo ip link set dev eth0.288 up").catch((err) => {log.error("set dev up", err.stderr);});
+ await exec("sudo ip route add table global_default 10.88.8.0/30 dev eth0.288").catch((err) => {log.error("add route", err.stderr);});
+ await exec("sudo ip route add table global_default 10.88.8.3/32 via 10.88.8.1 metric 223").catch((err) => {log.error("add route", err.stderr);});
+ }
+ done();
+ })()
+ );
+
+ after((done) => (
+ async() => {
+ if (this.needClean) {
+ await exec("sudo ip route flush dev eth0.288 table global_default").catch((err) => {});
+ await exec("sudo ip link set dev eth0.288 down").catch((err) => {});
+ await exec("sudo ip addr del 10.88.8.1/32 dev eth0.288").catch((err) => {});
+ await exec("sudo ip link del eth0.288").catch((err) => {});
+ }
+ done();
+ })()
+ );
+
+
+ it('should get device route rules', async()=> {
+ const input = [
+ {intf: 'eth0.288', tableName: 'global_default'},
+ {gateway: '10.88.8.1', intf: 'eth0.288', tableName: 'global_default', af: 4},
+ {gateway: '10.88.8.1', intf: 'eth0.288', tableName: 'global_default', metric: 223, af: 4},
+ {intf: 'eth0.288', tableName: 'main'},
+ ];
+ const expects = [
+ ['10.88.8.0/30 scope link', '10.88.8.3 via 10.88.8.1 metric 223'],
+ ['10.88.8.3 metric 223'], ['10.88.8.3'], [],
+ ];
+
+ for (let i=0; i {
+ const results = await routing.searchRouteRules(null, null, 'eth0.289', 'global_default');
+ expect(results).to.be.empty;
+ });
+
+ it('should format route rules', () => {
+ const input = [{},{dest: '10.89.18.195'}, {gateway: '10.88.8.1'}, {intf: 'eth0'}, {tableName: 'main'}, {af: 6},
+ {dest:'default', gateway:'10.88.8.1', intf: 'eth0.288', tableName: 'global_default', metric:223, af:4},
+ ];
+ const expects = ['ip -4 route show', 'ip -4 route show 10.89.18.195', 'ip -4 route show via 10.88.8.1',
+ 'ip -4 route show dev eth0', 'ip -4 route show table main', 'ip -6 route show',
+ 'ip -4 route show table global_default default dev eth0.288 via 10.88.8.1 metric 223'];
+
+ for (let i=0; i < input.length; i++) {
+ const output = routing.formatGetRouteCommand(input[i].dest, input[i].gateway, input[i].intf, input[i].tableName, input[i].metric, input[i].af);
+ expect(output).to.be.equal(expects[i]);
+ }
+ });
+
+ it ('should remove device rule', async() => {
+ let results = await routing.searchRouteRules(null, null, 'eth0.288', 'global_default');
+ expect(results.length).to.be.equal(2);
+
+ await routing.removeDeviceRouteRule('eth0.288', 'global_default').catch((err) => {log.debug(err.stderr)});
+
+ results = await routing.searchRouteRules(null, null, 'eth0.288', 'global_default');
+ expect(results.length).to.be.equal(0);
+ });
+
+});
diff --git a/util/firerouter.js b/util/firerouter.js
index 4832cddb..5532a474 100644
--- a/util/firerouter.js
+++ b/util/firerouter.js
@@ -224,6 +224,17 @@ function scheduleRestartFireBoot(delay = 10) {
}, delay * 1000);
}
+async function verifyPermanentMAC(iface) {
+ const pmac = await exec(`sudo ethtool -P ${iface}`).then(result => result.stdout.substring("Permanent address:".length).trim()).catch((err) => {
+ log.error(`Failed to get permanent MAC address of ${iface}`, err.message);
+ return null;
+ });
+ if (pmac && (pmac.toUpperCase().startsWith("20:6D:31:") || pmac.toUpperCase().startsWith("22:6D:31:"))) // Wi-Fi SD may have a private permanent MAC address on wlan1
+ return true;
+ log.error(`Permanent MAC address of ${iface} is invalid: ${pmac}`);
+ return false;
+}
+
module.exports = {
getUserHome: getUserHome,
getHiddenFolder: getHiddenFolder,
@@ -252,5 +263,6 @@ module.exports = {
getInterfaceDelegatedPrefixPath: getInterfaceDelegatedPrefixPath,
getInterfacePDCacheDirectory: getInterfacePDCacheDirectory,
getInterfaceSysFSDirectory: getInterfaceSysFSDirectory,
- switchBranch: switchBranch
+ switchBranch: switchBranch,
+ verifyPermanentMAC: verifyPermanentMAC
};
diff --git a/util/routing.js b/util/routing.js
index 4fd4f904..e74ef4f1 100644
--- a/util/routing.js
+++ b/util/routing.js
@@ -66,7 +66,7 @@ async function createCustomizedRoutingTable(tableName, type = RT_TYPE_REG) {
log.info(`Previous table id of ${tableName} is out of range ${tid}, removing old entry for ${tableName} ...`);
await removeCustomizedRoutingTable(tableName);
} else {
- log.info("Table with same name already exists: " + tid);
+ log.debug("Table with same name already exists: " + tid);
done(null, Number(tid));
return;
}
@@ -190,6 +190,8 @@ async function addRouteToTable(dest, gateway, intf, tableName, preference, af =
cmd = `${cmd} table ${tableName}`;
if (preference)
cmd = `${cmd} preference ${preference}`;
+
+ log.debug('[routing] add route to table:', cmd);
let result = await exec(cmd);
if (result.stderr !== "") {
log.error("Failed to add route to table.", result.stderr);
@@ -197,6 +199,44 @@ async function addRouteToTable(dest, gateway, intf, tableName, preference, af =
}
}
+function formatGetRouteCommand(dest, gateway, intf, tableName, metric, af=4) {
+ let cmd=`ip -${af} route show`;
+ if (tableName) {
+ cmd += ` table ${tableName}`
+ }
+ if (dest) {
+ cmd += ` ${dest}`
+ }
+ if (intf) {
+ cmd += ` dev ${intf}`
+ }
+ if (gateway) {
+ cmd += ` via ${gateway}`
+ }
+ if (metric) {
+ cmd += ` metric ${metric}`
+ }
+ return cmd;
+}
+
+async function searchRouteRules(dest, gateway, intf, tableName, metric=null, af=4) {
+ tableName = tableName || "main";
+ const cmd = formatGetRouteCommand(dest, gateway, intf, tableName, metric, af);
+ const result = await exec(cmd).then(r => r.stdout.trim()).catch((err) => {log.info(`Failed to get route using command '${cmd}'`, err.stderr); return "";});
+
+ return result.split("\n").filter(r => r.length > 0).map(r => r.trim());
+}
+
+async function removeDeviceRouteRule(intf, tableName, af = 4) {
+ const cmd=`sudo ip -${af} route flush table ${tableName} dev ${intf}`;
+ log.debug('[routing] flush device route rule:', cmd);
+ const result = await exec(cmd);
+ if (result.stderr !== "") {
+ log.error(`Failed to exec ${cmd}, err`, result.stderr);
+ throw result.stderr;
+ }
+}
+
async function addMultiPathRouteToTable(dest, tableName, af = 4, ...multipathDesc) {
let cmd = null;
dest = dest || "default";
@@ -221,7 +261,7 @@ async function addMultiPathRouteToTable(dest, tableName, af = 4, ...multipathDes
}
}
-async function removeRouteFromTable(dest, gateway, intf, tableName, af = 4, type = "unicast") {
+async function removeRouteFromTable(dest, gateway, intf, tableName, af = 4, type = "unicast", metric = null) {
dest = dest || "default";
tableName = tableName || "main";
let cmd = `sudo ip -${af} route del ${type} ${dest}`;
@@ -231,7 +271,12 @@ async function removeRouteFromTable(dest, gateway, intf, tableName, af = 4, type
if (intf) {
cmd = `${cmd} dev ${intf}`;
}
+ if (metric) {
+ cmd = `${cmd} metric ${metric}`;
+ }
cmd = `${cmd} table ${tableName}`;
+
+ log.debug(`[routing] remove route from table: ${cmd}`);
let result = await exec(cmd);
if (result.stderr !== "") {
log.error("Failed to remove route from table.", result.stderr);
@@ -246,6 +291,7 @@ async function flushRoutingTable(tableName, af = null) {
if (!af || af == 6)
cmds.push(`sudo ip -6 route flush table ${tableName}`);
for (const cmd of cmds) {
+ log.debug(`[routing] flush route table: ${cmd}`);
await exec(cmd).catch((err) => {
log.error(`Failed to flush routing table using command ${cmd}`, err.message);
});
@@ -340,6 +386,9 @@ module.exports = {
createInterfaceGlobalLocalRoutingRules: createInterfaceGlobalLocalRoutingRules,
removeInterfaceGlobalLocalRoutingRules: removeInterfaceGlobalLocalRoutingRules,
getInterfaceGWIP: getInterfaceGWIP,
+ searchRouteRules: searchRouteRules,
+ formatGetRouteCommand: formatGetRouteCommand, // only for testing
+ removeDeviceRouteRule: removeDeviceRouteRule,
RT_GLOBAL_LOCAL: RT_GLOBAL_LOCAL,
RT_GLOBAL_DEFAULT: RT_GLOBAL_DEFAULT,
RT_WAN_ROUTABLE: RT_WAN_ROUTABLE,