diff --git a/core/event.js b/core/event.js index 70b6813b..74e27c0f 100644 --- a/core/event.js +++ b/core/event.js @@ -46,6 +46,7 @@ module.exports = { EVENT_WLAN_DOWN: "wlan_down", EVENT_IP_CHANGE: "ipchange", EVENT_IP6_CHANGE: "ip6change", + EVENT_DNS6_CHANGE: "dns6change", EVENT_PD_CHANGE: "pdchange", EVENT_PPPOE_IPV6_UP: "pppoe_ipv6_up", EVENT_WAN_CONN_CHECK: "wan_conn_check", diff --git a/plugins/dhcp/dhcp6_plugin.js b/plugins/dhcp/dhcp6_plugin.js index 4ca21a0c..5971f126 100644 --- a/plugins/dhcp/dhcp6_plugin.js +++ b/plugins/dhcp/dhcp6_plugin.js @@ -16,6 +16,7 @@ 'use strict'; const pl = require('../plugin_loader.js'); +const event = require('../../core/event.js'); const r = require('../../util/firerouter.js'); const fs = require('fs'); const Promise = require('bluebird'); @@ -40,7 +41,7 @@ class DHCP6Plugin extends DHCPPlugin { async writeDHCPConfFile(iface, tags, type = "stateless", from, to, nameservers, prefixLen, leaseTime = 86400) { tags = tags || []; - nameservers = nameservers || []; + nameservers = nameservers || await this.getDhcp6DnsServers(); let extraTags = ""; type = type || "stateless"; if (tags.length > 0) { @@ -77,6 +78,20 @@ class DHCP6Plugin extends DHCPPlugin { await fs.writeFileAsync(this._getConfFilePath(), content.join("\n")); } + async getDhcp6DnsServers() { + const ifacePlugin = pl.getPluginInstance("interface", this.name); + if (!ifacePlugin || !await ifacePlugin.isInterfacePresent() || !ifacePlugin.isLAN()) { + return []; + } + const parentIface = ifacePlugin.networkConfig && ifacePlugin.networkConfig.ipv6DelegateFrom + if (parentIface) { + const parentIfacePlugin = pl.getPluginInstance("interface", parentIface); + const dns6 = await parentIfacePlugin.getDns6Nameservers(); + return dns6.map( i => i.split('%')[0]); + } + return []; + } + async apply() { let iface = this.name; if (iface.includes(":")) { @@ -97,6 +112,20 @@ class DHCP6Plugin extends DHCPPlugin { this.networkConfig.prefixLen, this.networkConfig.lease); this._restartService(); } + + onEvent(e) { + if (!event.isLoggingSuppressed(e)) + this.log.info(`Received event on ${this.name}`, e); + const eventType = event.getEventType(e); + switch (eventType) { + case event.EVENT_DNS6_CHANGE: { + this._reapplyNeeded = true; + pl.scheduleReapply(); + break; + } + default: + } + } } module.exports = DHCP6Plugin; \ No newline at end of file diff --git a/plugins/interface/intf_base_plugin.js b/plugins/interface/intf_base_plugin.js index 1f74ff71..3413f8bd 100644 --- a/plugins/interface/intf_base_plugin.js +++ b/plugins/interface/intf_base_plugin.js @@ -1553,6 +1553,17 @@ class InterfaceBasePlugin extends Plugin { } break; } + case event.EVENT_DNS6_CHANGE: { + const payload = event.getEventPayload(e); + if (payload.intf === this.name && this.isWAN()) { + // update DNS from DHCP + this.applyDnsSettings().then(() => this.updateRouteForDNS()).catch((err) => { + this.log.error(`Failed to apply DNS settings and update DNS route on ${this.name}`, err.message); + }); + this.propagateConfigChanged(true); + } + break; + } case event.EVENT_PD_CHANGE: { const payload = event.getEventPayload(e); const iface = payload.intf; diff --git a/plugins/routing/routing_plugin.js b/plugins/routing/routing_plugin.js index 9edc8b54..1b52ab08 100644 --- a/plugins/routing/routing_plugin.js +++ b/plugins/routing/routing_plugin.js @@ -80,10 +80,18 @@ class RoutingPlugin extends Plugin { if (!af || af == 4) { // remove DNS specific routes 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 = {}; + for (const dnsRoute of Object.keys(this._dnsRoutes).map(key => this._dnsRoutes[key]).filter(i => i.af == 4)) + await routing.removeRouteFromTable(dnsRoute.dest, dnsRoute.gw, dnsRoute.viaIntf, dnsRoute.tableName ? dnsRoute.tableName :"main", dnsRoute.af).catch((err) => { }); + this._dnsRoutes = Object.entries(this._dnsRoutes).reduce((routes, item) => { if (item[1].af == 6) routes[item[0]] = item[1]; return routes }, {}); + } else { this._dnsRoutes = {} } + } + if (!af || af == 6) { + // remove DNS specific routes + if (_.isObject(this._dnsRoutes)) { + for (const dnsRoute of Object.keys(this._dnsRoutes).map(key => this._dnsRoutes[key]).filter(i => i.af == 6)) + await routing.removeRouteFromTable(dnsRoute.dest, dnsRoute.gw, dnsRoute.viaIntf, dnsRoute.tableName ? dnsRoute.tableName :"main", dnsRoute.af).catch((err) => { }); + this._dnsRoutes = Object.entries(this._dnsRoutes).reduce((routes, item) => { if (item[1].af != 6) routes[item[0]] = item[1]; return routes }, {}); + } else {this._dnsRoutes = {}} } break; } @@ -224,7 +232,7 @@ class RoutingPlugin extends Plugin { } } - _updateDnsRouteCache(dnsIP, gw, viaIntf, metric, tableName="main") { + _updateDnsRouteCache(dnsIP, gw, viaIntf, metric, tableName="main", af=4) { if (!this._dnsRoutes){ this._dnsRoutes = {} } @@ -237,7 +245,7 @@ class RoutingPlugin extends Plugin { return; } } - this._dnsRoutes[viaIntf].push({dest: dnsIP, gw: gw, viaIntf: viaIntf, metric: metric, tableName: tableName}); + this._dnsRoutes[viaIntf].push({dest: dnsIP, gw: gw, viaIntf: viaIntf, metric: metric, tableName: tableName, af:af}); } async refreshGlobalIntfRoutes(intf, af = null) { @@ -302,7 +310,7 @@ class RoutingPlugin extends Plugin { } } - const dns = await intfPlugin.getDNSNameservers(); + const dns = await intfPlugin.getDns4Nameservers(); 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) => { @@ -321,6 +329,18 @@ class RoutingPlugin extends Plugin { 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)}); + + const dns6 = await intfPlugin.getDns6Nameservers(); + if (_.isArray(dns6) && dns6.length > 0) { + for (const dns6IP of dns6) { + await routing.addRouteToTable(dns6IP, gw6, intf, routing.RT_GLOBAL_DEFAULT, metric, 6).catch((err) => { + this.log.warn(`fail to add route -6 ${dns6IP} via ${gw6} dev ${intf} table ${routing.RT_GLOBAL_DEFAULT}, err:`, err.message)}); + await routing.addRouteToTable(dns6IP, gw6, intf, "main", metric, 6).then(() => { + this._updateDnsRouteCache(dns6IP, gw6, intf, metric, "main", 6); + }).catch((err) => { + this.log.warn(`fail to add route -6 ${dns6IP} via ${gw6} dev ${intf} table main, err:`, err.message)}); + } + } } } }); @@ -402,13 +422,24 @@ class RoutingPlugin extends Plugin { if (!af || af == 4) { // remove DNS specific routes if (_.isObject(this._dnsRoutes)) { - for (const dnsRoute of Object.keys(this._dnsRoutes).map(key => this._dnsRoutes[key])) { + for (const dnsRoute of Object.keys(this._dnsRoutes).map(key => this._dnsRoutes[key]).filter(i => i.af == 4)) { 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 = Object.entries(this._dnsRoutes).reduce((routes, item) => { if (item[1].af == 6) routes[item[0]] = item[1]; return routes }, {}); + } else { this._dnsRoutes = {}} + } + if (!af || af == 6) { + // remove DNS specific routes + if (_.isObject(this._dnsRoutes)) { + for (const dnsRoute of Object.keys(this._dnsRoutes).map(key => this._dnsRoutes[key]).filter(i => i.af == 6)) { + await routing.removeRouteFromTable(dnsRoute.dest, dnsRoute.gw, dnsRoute.viaIntf, dnsRoute.tableName ? dnsRoute.tableName : "main", 6).catch((err) => { + this.log.warn('fail to remove dns route from table main, err:', err.message) + }); + } + this._dnsRoutes = Object.entries(this._dnsRoutes).reduce((routes, item) => { if (item[1].af != 6) routes[item[0]] = item[1]; return routes }, {}); + } else { this._dnsRoutes = {}} } } @@ -479,7 +510,7 @@ class RoutingPlugin extends Plugin { 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(); + const dns = await viaIntfPlugin.getDns4Nameservers(); if (_.isArray(dns) && dns.length !== 0) { for (const dnsIP of dns) { await this.upsertRouteToTable(dnsIP, gw, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 4).catch((err) => { @@ -503,6 +534,22 @@ class RoutingPlugin extends Plugin { 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 this.upsertRouteToTable("default", gw6, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 6).catch((err) => { }); await this.upsertRouteToTable("default", gw6, viaIntf, "main", metric, 6).catch((err) => { }); + // add route for ipv6 DNS nameserver IP in global_default table + const dns6 = await viaIntfPlugin.getDns6Nameservers(); + if (_.isArray(dns6) && dns6.length !== 0 ){ + for (const dns6IP of dns6) { + await this.upsertRouteToTable(dns6IP, gw6, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 6).catch((err) => { + this.log.error(`Failed to add route ipv6 to ${routing.RT_GLOBAL_DEFAULT} for dns ${dns6IP} via ${gw6} dev ${viaIntf}`, err.message); + }); + // update all dns routes via the same interface but with new metrics in main table + await this.upsertRouteToTable(dns6IP, gw6, viaIntf, "main", metric, 6).then(() => { + this._updateDnsRouteCache(dns6IP, gw6, viaIntf, metric, "main", 6); + }).catch((err) => { + this.log.error(`Failed to add route to main for dns ${dns6IP} via ${gw6} dev ${viaIntf}`, err.message); + }); + } + } + } else { this.log.info("IPv6 gateway is not defined on global default interface " + viaIntf); } @@ -558,7 +605,7 @@ class RoutingPlugin extends Plugin { await routing.addRouteToTable("default", gw, viaIntf, "main", metric, 4).catch((err) => { }); } // add route for DNS nameserver IP in global_default table - const dns = await viaIntfPlugin.getDNSNameservers(); + const dns = await viaIntfPlugin.getDns4Nameservers(); 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) => { @@ -583,7 +630,7 @@ class RoutingPlugin extends Plugin { if (!this._dnsRoutes[viaIntf]) { this._dnsRoutes[viaIntf] = []; } - this._dnsRoutes[viaIntf].push({dest: dnsIP, gw: gw, viaIntf: viaIntf, metric: metric, tableName: "main"}); + this._dnsRoutes[viaIntf].push({dest: dnsIP, gw: gw, viaIntf: viaIntf, metric: metric, tableName: "main", af: af}); } } } else { @@ -603,6 +650,35 @@ class RoutingPlugin extends Plugin { await routing.addRouteToTable("default", gw6, viaIntf, "main", metric, 6).catch((err) => { }); */ } + // add route for ipv6 DNS nameserver IP in global_default table + const dns6 = await viaIntfPlugin.getDns6Nameservers(); + if (_.isArray(dns6) && dns6.length !== 0) { + for (const dns6IP of dns6) { + await routing.addRouteToTable(dns6IP, gw6, viaIntf, routing.RT_GLOBAL_DEFAULT, metric, 6, true).catch((err) => { + this.log.error(`Failed to add route to ${routing.RT_GLOBAL_DEFAULT} for dns ${dns6IP} via ${gw6} dev ${viaIntf}`, err.message); + }); + let dnsRouteRemoved = false; + // remove all ipv6 dns routes via the same interface but with different metrics in main table + do { + await routing.removeRouteFromTable(dns6IP, gw6, viaIntf, "main", 6).then(() => { + dnsRouteRemoved = true; + }).catch((err) => { + dnsRouteRemoved = false; + }) + } while (dnsRouteRemoved) + await routing.addRouteToTable(dns6IP, gw6, viaIntf, "main", metric, 6, true).catch((err) => { + this.log.error(`Failed to add route to main for dns ${dns6IP} via ${gw6} dev ${viaIntf}`, err.message); + }); + 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: dns6IP, gw: gw6, viaIntf: viaIntf, metric: metric, tableName: "main", af: af}); + } + } } else { this.log.info("Failed to get IPv6 gateway of global default interface " + viaIntf); } @@ -616,6 +692,7 @@ class RoutingPlugin extends Plugin { await routing.addMultiPathRouteToTable("default", routing.RT_GLOBAL_DEFAULT, 6, ...multiPathDesc6).catch((err) => { }); await routing.addMultiPathRouteToTable("default", "main", 6, ...multiPathDesc6).catch((err) => { }); } + break; } default: diff --git a/scripts/firerouter_dhcpcd_resolv_dns6 b/scripts/firerouter_dhcpcd_resolv_dns6 index 3867f5a8..f2ee40de 100644 --- a/scripts/firerouter_dhcpcd_resolv_dns6 +++ b/scripts/firerouter_dhcpcd_resolv_dns6 @@ -10,6 +10,8 @@ resolv_conf_file="/run/resolvconf/interface/$interface.dhcpcd" NL=" " : ${resolvconf:=resolvconf} +dns_changed="" +dns6_md5="" # Extract any ND DNS options from the RA # For now, we ignore the lifetime of the DNS options unless they @@ -93,6 +95,7 @@ add_resolv_conf() [ "$new_domain_name" = "$1" ] && warn=true fi fi + if [ -n "$new_domain_search" ]; then if valid_domainname_list $new_domain_search; then conf="${conf}search $new_domain_search$NL" @@ -106,6 +109,10 @@ add_resolv_conf() done echo "${conf}" > $resolv_conf_file + new_dns6_md5=`md5sum $resolv_conf_file | cut -d" " -f1` + if [ "$dns6_md5" != "$new_dns6_md5" ];then + redis-cli -n 1 publish "dhcpcd6.dns_change" "$interface" + fi if type "$resolvconf" >/dev/null 2>&1; then [ -n "$ifmetric" ] && export IF_METRIC="$ifmetric" @@ -120,8 +127,19 @@ remove_resolv_conf() "$resolvconf" -d "$ifname" -f fi rm $resolv_conf_file + if [ -n "$dns6_md5" ]; then + redis-cli -n 1 publish "dhcpcd6.dns_change" "$interface" + fi +} + +md5_resolv_conf() +{ + if [ -s $resolv_conf_file ]; then + dns6_md5=`md5sum $resolv_conf_file | cut -d" " -f1` + fi } +md5_resolv_conf # For ease of use, map DHCP6 names onto our DHCP4 names case "$reason" in BOUND6|RENEW6|REBIND6|REBOOT6|INFORM6) diff --git a/scripts/firerouter_upgrade_check.sh b/scripts/firerouter_upgrade_check.sh index 50424f00..4cb670a1 100755 --- a/scripts/firerouter_upgrade_check.sh +++ b/scripts/firerouter_upgrade_check.sh @@ -32,7 +32,7 @@ FIREROUTER_CANARY_SCRIPT="${FIREROUTER_HOME}/scripts/firerouter_upgrade_canary.s FRCANARY_FLAG="/home/pi/.router/config/.no_upgrade_canary" if [[ -e "$FIREROUTER_CANARY_SCRIPT" ]];then - $FIREROUTER_CANARY_SCRIPT &> /tmp/firerouter_upgrade_canary.log + bash $FIREROUTER_CANARY_SCRIPT &> /tmp/firerouter_upgrade_canary.log fi if [[ -e $FRCANARY_FLAG ]]; then diff --git a/sensors/ipchange_sensor.js b/sensors/ipchange_sensor.js index d5314681..5782b088 100644 --- a/sensors/ipchange_sensor.js +++ b/sensors/ipchange_sensor.js @@ -39,6 +39,9 @@ class IPChangeSensor extends Sensor { case "dhcpcd6.pd_change": eventType = event.EVENT_PD_CHANGE; break; + case "dhcpcd6.dns_change": + eventType = event.EVENT_DNS6_CHANGE; + break; case "pppoe.ipv6_up": eventType = event.EVENT_PPPOE_IPV6_UP; break; @@ -68,6 +71,7 @@ class IPChangeSensor extends Sensor { sclient.subscribe("pppoe.ip_change"); sclient.subscribe("dhcpcd6.ip_change"); sclient.subscribe("dhcpcd6.pd_change"); + sclient.subscribe("dhcpcd6.dns_change"); sclient.subscribe("pppoe.ipv6_up"); } }