From c483118df6c8109459e05de6a1ff9105df440ece Mon Sep 17 00:00:00 2001
From: xinnige <xinni.ge1990@gmail.com>
Date: Tue, 5 Nov 2024 10:29:59 +0800
Subject: [PATCH] Fix dns6 option delegate

---
 core/event.js                         |   1 +
 plugins/dhcp/dhcp6_plugin.js          |  17 ++++-
 plugins/interface/intf_base_plugin.js |  13 ++++
 plugins/routing/routing_plugin.js     | 103 ++++++++++++++++++++++----
 scripts/firerouter_dhcpcd_resolv_dns6 |  18 +++++
 scripts/firerouter_upgrade_check.sh   |   2 +-
 sensors/ipchange_sensor.js            |   4 +
 7 files changed, 143 insertions(+), 15 deletions(-)

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..d0e9df45 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');
@@ -51,7 +52,7 @@ class DHCP6Plugin extends DHCPPlugin {
     if (nameservers.length > 0){
       content.push(`dhcp-option=tag:${iface},option6:dns-server,${nameservers.map(a => `[${a}]`).join(",")}`);
     } else { // return router's link-local address as RDNSS option in ra
-      content.push(`dhcp-option=tag:${iface},option6:dns-server,[fe80::]`);
+      content.push(`dhcp-option=tag:${iface},option6:dns-server,[::]`);
     }
 
     switch (type) {
@@ -97,6 +98,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..3a9a82ef 100644
--- a/plugins/interface/intf_base_plugin.js
+++ b/plugins/interface/intf_base_plugin.js
@@ -1553,6 +1553,19 @@ 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
+          pl.acquireApplyLock(async () => {
+            await 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");
   }
 }