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,