mwan3: reimplement rpcd plugin using ucode
authorEtienne Champetier <[email protected]>
Fri, 27 Jun 2025 23:18:55 +0000 (19:18 -0400)
committerFlorian Eckert <[email protected]>
Fri, 1 Aug 2025 11:03:01 +0000 (13:03 +0200)
On my "test" router (5 wans, 2 tracking ips per wan), before any rework,
prometheus-node-exporter-lua mwan3 average scraping time was 1230ms
(scraping only the interfaces), after optimizing the shell version,
average time was down to 485ms, with ucode we are now at 41ms.

Signed-off-by: Etienne Champetier <[email protected]>
net/mwan3/Makefile
net/mwan3/files/usr/libexec/rpcd/mwan3 [deleted file]
net/mwan3/files/usr/share/rpcd/ucode/mwan3 [new file with mode: 0644]

index 1631927a6bc24523d90ef67fd850bbebb29e1ca7..5451063302d106d2434325f86a75a7421fdab5fa 100644 (file)
@@ -8,8 +8,8 @@
 include $(TOPDIR)/rules.mk
 
 PKG_NAME:=mwan3
-PKG_VERSION:=2.11.19
-PKG_RELEASE:=5
+PKG_VERSION:=2.12.0
+PKG_RELEASE:=1
 PKG_MAINTAINER:=Florian Eckert <[email protected]>, \
                Aaron Goodman <[email protected]>
 PKG_LICENSE:=GPL-2.0
@@ -28,6 +28,7 @@ define Package/mwan3
      +IPV6:ip6tables \
      +iptables-mod-conntrack-extra \
      +iptables-mod-ipopt \
+     +rpcd-mod-ucode \
      +jshn
    TITLE:=Multiwan hotplug script with connection tracking support
    MAINTAINER:=Florian Eckert <[email protected]>
@@ -47,7 +48,7 @@ endef
 
 define Package/mwan3/postinst
 #!/bin/sh
-if [ -z "$${IPKG_INSTROOT}" ] && [ -x /etc/init.d/rpcd ]; then
+if [ -z "$${IPKG_INSTROOT}" ]; then
        /etc/init.d/rpcd restart
 fi
 exit 0
@@ -55,7 +56,7 @@ endef
 
 define Package/mwan3/postrm
 #!/bin/sh
-if [ -z "$${IPKG_INSTROOT}" ] && [ -x /etc/init.d/rpcd ]; then
+if [ -z "$${IPKG_INSTROOT}" ]; then
        /etc/init.d/rpcd restart
 fi
 exit 0
@@ -91,9 +92,9 @@ define Package/mwan3/install
        $(INSTALL_DATA) ./files/lib/mwan3/mwan3.sh \
                $(1)/lib/mwan3/
 
-       $(INSTALL_DIR) $(1)/usr/libexec/rpcd
-       $(INSTALL_BIN) ./files/usr/libexec/rpcd/mwan3 \
-               $(1)/usr/libexec/rpcd/
+       $(INSTALL_DIR) $(1)/usr/share/rpcd/ucode/
+       $(INSTALL_BIN) ./files/usr/share/rpcd/ucode/mwan3 \
+               $(1)/usr/share/rpcd/ucode/
 
        $(INSTALL_DIR) $(1)/usr/sbin
        $(INSTALL_BIN) ./files/usr/sbin/mwan3 \
diff --git a/net/mwan3/files/usr/libexec/rpcd/mwan3 b/net/mwan3/files/usr/libexec/rpcd/mwan3
deleted file mode 100755 (executable)
index 2ced4bb..0000000
+++ /dev/null
@@ -1,235 +0,0 @@
-#!/bin/sh
-
-. /lib/functions.sh
-. /lib/functions/network.sh
-. /usr/share/libubox/jshn.sh
-. /lib/mwan3/common.sh
-
-report_connected_v4() {
-       local address
-
-       if [ -n "$($IPT4 -S mwan3_connected_ipv4 2> /dev/null)" ]; then
-               for address in $($IPS -o save list mwan3_connected_ipv4 | grep add | cut -d " " -f 3); do
-                       json_add_string "" "${address}"
-               done
-       fi
-}
-
-report_connected_v6() {
-       [ $NO_IPV6 -ne 0 ] && return
-       local address
-
-       if [ -n "$($IPT6 -S mwan3_connected_ipv6 2> /dev/null)" ]; then
-               for address in $($IPS -o save list mwan3_connected_ipv6 | grep add | cut -d " " -f 3); do
-                       json_add_string "" "${address}"
-               done
-       fi
-}
-
-report_policies() {
-       local ipt="$1"
-       local policy="$2"
-
-       local percent total_weight weight iface
-
-       total_weight=$($ipt -S $policy | grep -v '.*--comment "out .*" .*$' | cut -s -d'"' -f2 | head -1 | awk '{print $3}')
-
-       for iface in $($ipt -S $policy | grep -v '.*--comment "out .*" .*$' | cut -s -d'"' -f2 | awk '{print $1}'); do
-               weight=$($ipt -S $policy | grep -v '.*--comment "out .*" .*$' | cut -s -d'"' -f2 | awk '$1 == "'$iface'"' | awk '{print $2}')
-               percent=$(($weight*100/$total_weight))
-               json_add_object
-               json_add_string interface "$iface"
-               json_add_int percent "$percent"
-               json_close_object
-       done
-}
-
-report_policies_v4() {
-       local policy
-
-       for policy in $($IPT4 -S | awk '{print $2}' | grep mwan3_policy_ | sort -u); do
-               json_add_array "${policy##*mwan3_policy_}"
-               report_policies "$IPT4" "$policy"
-               json_close_array
-       done
-}
-
-report_policies_v6() {
-       [ $NO_IPV6 -ne 0 ] && return
-       local policy
-
-       for policy in $($IPT6 -S | awk '{print $2}' | grep mwan3_policy_ | sort -u); do
-               json_add_array "${policy##*mwan3_policy_}"
-               report_policies "$IPT6" "$policy"
-               json_close_array
-       done
-}
-
-get_age() {
-       local time_p time_u
-       iface="$2"
-       readfile time_p "$MWAN3TRACK_STATUS_DIR/${iface}/TIME"
-       [ -z "${time_p}" ] || {
-               get_uptime time_n
-               export -n "$1=$((time_n-time_p))"
-       }
-}
-
-get_offline_time() {
-       local time_n time_d iface
-       iface="$2"
-       readfile time_d "$MWAN3TRACK_STATUS_DIR/${iface}/OFFLINE"
-       [ -z "${time_d}" ] || [ "${time_d}" = "0" ] || {
-               get_uptime time_n
-               export -n "$1=$((time_n-time_d))"
-       }
-}
-
-get_mwan3_status() {
-       local iface="${1}"
-       local iface_select="${2}"
-       local running="0"
-       local age=0
-       local online=0
-       local offline=0
-       local enabled time_p time_n time_u time_d status track_status up uptime temp
-
-       if [ "${iface}" != "${iface_select}" ] && [ "${iface_select}" != "" ]; then
-               return
-       fi
-
-       mwan3_get_mwan3track_status track_status "$1"
-       [ "$track_status" = "active" ] && running="1"
-       get_age age "$iface"
-       get_online_time online "$iface"
-       get_offline_time offline "$iface"
-
-       config_get_bool enabled "$iface" enabled 0
-
-       if [ -f "$MWAN3TRACK_STATUS_DIR/${iface}/STATUS" ]; then
-               network_get_uptime uptime "$iface"
-               [ -n "$uptime" ] && up=1 || up=0
-               readfile status "$MWAN3TRACK_STATUS_DIR/${iface}/STATUS"
-       else
-               uptime=0
-               up=0
-               status="unknown"
-       fi
-
-       json_add_object "${iface}"
-       json_add_int age "$age"
-       json_add_int online "${online}"
-       json_add_int offline "${offline}"
-       json_add_int uptime "${uptime}"
-       readfile temp "$MWAN3TRACK_STATUS_DIR/${iface}/SCORE"
-       json_add_int "score" "$temp"
-       readfile temp "$MWAN3TRACK_STATUS_DIR/${iface}/LOST"
-       json_add_int "lost" "$temp"
-       readfile temp "$MWAN3TRACK_STATUS_DIR/${iface}/TURN"
-       json_add_int "turn" "$temp"
-       json_add_string "status" "${status}"
-       json_add_boolean "enabled" "${enabled}"
-       json_add_boolean "running" "${running}"
-       json_add_string "tracking" "${track_status}"
-       json_add_boolean "up" "${up}"
-       json_add_array "track_ip"
-       for file in $MWAN3TRACK_STATUS_DIR/${iface}/TRACK_*; do
-               [ -z "${file#*/TRACK_OUTPUT}" ] && continue
-               [ -z "${file#*/TRACK_\*}" ] && continue
-               track="${file#*/TRACK_}"
-               json_add_object
-               json_add_string ip "${track}"
-               readfile temp "${file}"
-               json_add_string status "$temp"
-               readfile temp "$MWAN3TRACK_STATUS_DIR/${iface}/LATENCY_${track}"
-               json_add_int latency "$temp"
-               readfile temp "$MWAN3TRACK_STATUS_DIR/${iface}/LOSS_${track}"
-               json_add_int packetloss "$temp"
-               json_close_object
-       done
-       json_close_array
-       json_close_object
-}
-
-main () {
-
-       case "$1" in
-               list)
-                       json_init
-                       json_add_object "status"
-                       json_add_string "section" "x"
-                       json_add_string "interface" "x"
-                       json_add_string "policies" "x"
-                       json_close_object
-                       json_dump
-                       ;;
-               call)
-                       case "$2" in
-                       status)
-                               local section iface
-                               read input;
-                               json_load "$input"
-                               json_get_var section section
-                               json_get_var iface interface
-
-                               config_load mwan3
-                               json_init
-                               case "$section" in
-                                       interfaces)
-                                               json_add_object interfaces
-                                               config_foreach get_mwan3_status interface "${iface}"
-                                               json_close_object
-                                               ;;
-                                       connected)
-                                               json_add_object connected
-                                               json_add_array ipv4
-                                               report_connected_v4
-                                               json_close_array
-                                               json_add_array ipv6
-                                               report_connected_v6
-                                               json_close_array
-                                               json_close_object
-                                               ;;
-                                       policies)
-                                               json_add_object policies
-                                               json_add_object ipv4
-                                               report_policies_v4
-                                               json_close_object
-                                               json_add_object ipv6
-                                               report_policies_v6
-                                               json_close_object
-                                               json_close_object
-                                               ;;
-                                       *)
-                                               # interfaces
-                                               json_add_object interfaces
-                                               config_foreach get_mwan3_status interface
-                                               json_close_object
-                                               # connected
-                                               json_add_object connected
-                                               json_add_array ipv4
-                                               report_connected_v4
-                                               json_close_array
-                                               json_add_array ipv6
-                                               report_connected_v6
-                                               json_close_array
-                                               json_close_object
-                                               # policies
-                                               json_add_object policies
-                                               json_add_object ipv4
-                                               report_policies_v4
-                                               json_close_object
-                                               json_add_object ipv6
-                                               report_policies_v6
-                                               json_close_object
-                                               json_close_object
-                                               ;;
-                               esac
-                               json_dump
-                               ;;
-                       esac
-                       ;;
-       esac
-}
-
-main "$@"
diff --git a/net/mwan3/files/usr/share/rpcd/ucode/mwan3 b/net/mwan3/files/usr/share/rpcd/ucode/mwan3
new file mode 100644 (file)
index 0000000..b5c2f4a
--- /dev/null
@@ -0,0 +1,202 @@
+'use strict';
+
+import { popen, readfile } from 'fs';
+import { cursor } from 'uci';
+
+const ubus = require('ubus').connect();
+
+function get_str_raw(iface, property) {
+       return readfile(sprintf('/var/run/mwan3track/%s/%s', iface, property));
+}
+
+function get_str(iface, property) {
+       return rtrim(get_str_raw(iface, property), '\n');
+}
+
+function get_int(iface, property) {
+       return int(get_str(iface, property));
+}
+
+function get_uptime() {
+       return int(split(readfile('/proc/uptime'), '.', 2)[0]);
+}
+
+function get_x_time(uptime, iface, property) {
+       let t = get_int(iface, property);
+       if (t > 0) {
+               t = uptime - t;
+       }
+       return t;
+}
+
+function ucibool(val) {
+       switch (val) {
+               case 'yes':
+               case 'on':
+               case 'true':
+               case 'enabled':
+                       return true;
+               default:
+                       return !!int(val);
+       }
+}
+
+function get_mwan3track_status(iface, uci_track_ips, procd) {
+       if (length(uci_track_ips) == 0) {
+               return 'disabled';
+       }
+       if (procd?.[sprintf('track_%s', iface)]?.running) {
+               const started = get_str(iface, 'STARTED');
+               switch (started) {
+                       case '0':
+                               return 'paused';
+                       case '1':
+                               return 'active';
+                       default:
+                               return 'down';
+               }
+       }
+       return 'down';
+}
+
+const connected_check_cmd = {
+       '4': 'iptables -t mangle -w -S mwan3_connected_ipv4',
+       '6': 'ip6tables -t mangle -w -S mwan3_connected_ipv6',
+};
+const ipset_save_re = regexp('^add mwan3_connected_ipv[46] (.*)\n$');
+
+function get_connected_ips(version) {
+       const check = popen(connected_check_cmd[version], 'r');
+       check.read('all');
+       if (check.close() != 0) {
+               return [];
+       }
+       const ipset = popen(sprintf('ipset -o save list mwan3_connected_ipv%s', version), 'r');
+       const ips = [];
+       for (let line = ipset.read('line'); length(line); line = ipset.read('line')) {
+               const m = match(line, ipset_save_re);
+               if (length(m) == 2) {
+                       push(ips, m[1]);
+               }
+       }
+       ipset.close();
+       return ips;
+}
+
+const policies_cmd = {
+       '4': 'iptables -t mangle -w -S',
+       '6': 'ip6tables -t mangle -w -S'
+};
+const policies_re = regexp('^-A mwan3_policy_([^ ]+) .*?--comment "([^"]+)"');
+
+function get_policies(version) {
+       const ipt = popen(policies_cmd[version], 'r');
+       const policies = {};
+       for (let line = ipt.read('line'); length(line); line = ipt.read('line')) {
+               const m = match(line, policies_re);
+               if (m == null) {
+                       continue;
+               }
+               const policy = m[1];
+               if (!exists(policies, policy)) {
+                       policies[policy] = [];
+               }
+               const intfw = split(m[2], ' ', 3);
+               const weight = int(intfw[1]);
+               const total = int(intfw[2]);
+               if (weight >= 0 && total > 0) {
+                       push(policies[policy], {
+                               'interface': intfw[0],
+                               'percent': weight / total * 100,
+                       })
+               }
+       }
+       ipt.close();
+       return policies;
+}
+
+function interfaces_status(request) {
+       const uci = cursor();
+       const procd = ubus.call('service', 'list', { 'name': 'mwan3' })?.mwan3?.instances;
+       const interfaces = {};
+       uci.foreach('mwan3', 'interface', intf => {
+               const ifname = intf['.name'];
+               if (request.args.interface != null && request.args.interface != ifname) {
+                       return;
+               }
+               const netstatus = ubus.call(sprintf('network.interface.%s', ifname), 'status', {});
+               const uptime = get_uptime();
+               const uci_track_ips = intf['track_ip'];
+               const track_status = get_mwan3track_status(ifname, uci_track_ips, procd);
+               const track_ips = [];
+               for (let ip in uci_track_ips) {
+                       push(track_ips, {
+                               'ip': ip,
+                               'status': get_str(ifname, sprintf('TRACK_%s', ip)) || 'unknown',
+                               'latency': get_int(ifname, sprintf('LATENCY_%s', ip)),
+                               'packetloss': get_int(ifname, sprintf('LOSS_%s', ip)),
+                       });
+               }
+               interfaces[ifname] = {
+                       'age': get_x_time(uptime, ifname, 'TIME'),
+                       'online': get_x_time(uptime, ifname, 'ONLINE'),
+                       'offline': get_x_time(uptime, ifname, 'OFFLINE'),
+                       'uptime': netstatus.uptime || 0,
+                       'score': get_int(ifname, 'SCORE'),
+                       'lost': get_int(ifname, 'LOST'),
+                       'turn': get_int(ifname, 'TURN'),
+                       'status': get_str(ifname, 'STATUS') || 'unknown',
+                       'enabled': ucibool(intf['enabled']),
+                       'running': track_status == 'active',
+                       'tracking': track_status,
+                       'up': netstatus.up,
+                       'track_ip': track_ips,
+               };
+       });
+       return interfaces;
+}
+
+const methods = {
+       status: {
+               args: {
+                       section: 'section',
+                       interface: 'interface'
+               },
+               call: function (request) {
+                       switch (request.args.section) {
+                               case 'connected':
+                                       return {
+                                               'connected': {
+                                                       'ipv4': get_connected_ips('4'),
+                                                       'ipv6': get_connected_ips('6'),
+                                               },
+                                       };
+                               case 'policies':
+                                       return {
+                                               'policies': {
+                                                       'ipv4': get_policies('4'),
+                                                       'ipv6': get_policies('6'),
+                                               },
+                                       };
+                               case 'interfaces':
+                                       return {
+                                               'interfaces': interfaces_status(request),
+                                       };
+                               default:
+                                       return {
+                                               'interfaces': interfaces_status(request),
+                                               'connected': {
+                                                       'ipv4': get_connected_ips('4'),
+                                                       'ipv6': get_connected_ips('6'),
+                                               },
+                                               'policies': {
+                                                       'ipv4': get_policies('4'),
+                                                       'ipv6': get_policies('6'),
+                                               },
+                                       };
+                       }
+               }
+       }
+};
+
+return { 'mwan3': methods };