mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 16:44:23 +08:00
🤞 Sync 2026-10-06 13:52:46
This commit is contained in:
1 parent
4d2d69bed9
commit
947eaea2ee
42 files changed
+2681
-1582
No files matched your search
+8
-3
@@ -8,8 +8,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mwan3-nft
|
||||
PKG_VERSION:=3.6.9
|
||||
PKG_RELEASE:=3
|
||||
PKG_VERSION:=3.6.12
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_MAINTAINER:=Florian Eckert <fe@dev.tdt.de>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
@@ -35,7 +35,8 @@ define Package/$(PKG_NAME)
|
||||
+ucode-mod-uci \
|
||||
+ucode-mod-ubus \
|
||||
+ucode-mod-fs \
|
||||
+ucode-mod-socket
|
||||
+ucode-mod-socket \
|
||||
+ucode-mod-log
|
||||
CONFLICTS:=mwan3
|
||||
TITLE:=Multiwan hotplug script with connection tracking support (ucode rtmon)
|
||||
MAINTAINER:=Florian Eckert <fe@dev.tdt.de>
|
||||
@@ -220,6 +221,10 @@ define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_BIN) ./files/usr/share/rpcd/ucode/mwan3 \
|
||||
$(1)/usr/share/rpcd/ucode/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/ucode/mwan3
|
||||
$(INSTALL_DATA) ./files/usr/share/ucode/mwan3/common.uc \
|
||||
$(1)/usr/share/ucode/mwan3/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) ./files/usr/sbin/mwan3 \
|
||||
$(1)/usr/sbin/
|
||||
|
||||
+721
-179
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
# For full documentation of mwan3 configuration:
|
||||
# https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3#mwan3_configuration
|
||||
# https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3-nft#configuration
|
||||
|
||||
config globals 'globals'
|
||||
option mmx_mask '0x3F00'
|
||||
|
||||
@@ -52,9 +52,24 @@ LOG notice "Execute $ACTION event on interface $INTERFACE (${DEVICE:-unknown})"
|
||||
|
||||
case "$ACTION" in
|
||||
connected)
|
||||
# A tracker recovery must restore the member's routing before the policy
|
||||
# re-selects it, so the member is routable the instant it is marked, then
|
||||
# snap new flows off the worse-metric backups back toward it. The policy
|
||||
# rebuild commits as one nft transaction so no packet classifies against a
|
||||
# half-rebuilt policy chain. The snap-back stays outside that batch on
|
||||
# purpose: a recovery's worse-metric pins point at backups that are still
|
||||
# up, so a source re-pinned in the window before the snap reaches a live
|
||||
# member, not a dead one, unlike a teardown whose pins must be flushed in
|
||||
# the same transaction because they point at the departing member.
|
||||
|
||||
mwan3_set_iface_hotplug_state $INTERFACE "online"
|
||||
mwan3_create_iface_nft $INTERFACE $DEVICE
|
||||
mwan3_create_iface_rules $INTERFACE $DEVICE
|
||||
mwan3_create_iface_route $INTERFACE $DEVICE
|
||||
mwan3_nft_batch_start
|
||||
mwan3_set_policies_nft
|
||||
mwan3_nft_batch_commit
|
||||
mwan3_flush_worse_metric_sticky $INTERFACE
|
||||
mwan3_flush_unreplied_conntrack
|
||||
;;
|
||||
ifup)
|
||||
@@ -66,24 +81,52 @@ case "$ACTION" in
|
||||
if [ "$MWAN3_STARTUP" != "init" ]; then
|
||||
mwan3_set_general_rules
|
||||
if [ "$status" = "online" ]; then
|
||||
|
||||
# Same atomic policy rebuild and out-of-batch snap-back as the
|
||||
# connected arm above; see that comment for why the snap-back is
|
||||
# not folded into the batch. The batch sits inside the online
|
||||
# guard so the init path never opens one.
|
||||
|
||||
mwan3_nft_batch_start
|
||||
mwan3_set_policies_nft
|
||||
mwan3_nft_batch_commit
|
||||
mwan3_flush_worse_metric_sticky $INTERFACE
|
||||
mwan3_flush_unreplied_conntrack
|
||||
fi
|
||||
fi
|
||||
[ "$ACTION" = ifup ] && procd_running mwan3 "track_$INTERFACE" && procd_send_signal mwan3 "track_$INTERFACE" USR2
|
||||
;;
|
||||
disconnected)
|
||||
# A soft tracker failure drops the member from the policy; flush its
|
||||
# sticky pins after the rebuild so a pinned source re-evaluates to the
|
||||
# survivor, the same treatment a hard ifdown gets. The policy rebuild
|
||||
# and the sticky flush commit as one nft transaction so no packet
|
||||
# classifies against a half-rebuilt policy. The member's marked flows
|
||||
# are cleared by the conntrack flush at the handler tail, which is
|
||||
# kernel state outside nftables and stays a separate operation.
|
||||
|
||||
mwan3_set_iface_hotplug_state $INTERFACE "offline"
|
||||
mwan3_nft_batch_start
|
||||
mwan3_set_policies_nft
|
||||
mwan3_delete_iface_map_entries $INTERFACE
|
||||
mwan3_nft_batch_commit
|
||||
;;
|
||||
ifdown)
|
||||
# Drop the member from the policy before tearing down its routing, so the
|
||||
# policy never marks for a member whose rules are already gone; flush its
|
||||
# sticky sets after the rebuild so the rebuild cannot re-seed them. The
|
||||
# policy rebuild and the sticky flush commit as one nft transaction; the
|
||||
# routing teardown and the conntrack flush follow as separate steps.
|
||||
|
||||
mwan3_set_iface_hotplug_state $INTERFACE "offline"
|
||||
mwan3_nft_batch_start
|
||||
mwan3_set_policies_nft
|
||||
mwan3_delete_iface_map_entries $INTERFACE
|
||||
mwan3_nft_batch_commit
|
||||
mwan3_delete_iface_rules $INTERFACE
|
||||
mwan3_delete_iface_route $INTERFACE
|
||||
mwan3_delete_iface_nft $INTERFACE
|
||||
procd_running mwan3 "track_$INTERFACE" && procd_send_signal mwan3 "track_$INTERFACE" USR1
|
||||
mwan3_set_policies_nft
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ start_service() {
|
||||
mwan3_set_connected_sets
|
||||
mwan3_set_custom_sets
|
||||
mwan3_set_general_rules
|
||||
mwan3_set_member_backstops
|
||||
config_foreach mwan3_ifup interface "init"
|
||||
wait $hotplug_pids
|
||||
mwan3_set_general_nft
|
||||
@@ -134,11 +135,13 @@ stop_service() {
|
||||
|
||||
# Delete ip rules created by this mwan3 instance. A rule must pass
|
||||
# both gates before it is deleted:
|
||||
# priority gate -- the priority lies in one of mwan3's three
|
||||
# configured rule-base ranges (iif, fwmark, unreachable). The
|
||||
# priority gate -- the priority lies in one of mwan3's four
|
||||
# rule-base ranges (iif, fwmark, unreachable, source). The
|
||||
# fwmark range extends to fwmark_base + MM_UNREACHABLE to cover
|
||||
# the global blackhole and unreachable rules at fwmark_base +
|
||||
# MM_BLACKHOLE / MM_UNREACHABLE.
|
||||
# MM_BLACKHOLE / MM_UNREACHABLE. The source band is not
|
||||
# configurable; it is derived from the unreachable base and
|
||||
# sits immediately above the unreachable band.
|
||||
# content gate -- the line either references a mwan3-owned routing
|
||||
# table (lookup <N> with N in 1..MWAN3_INTERFACE_MAX) or carries
|
||||
# an fwmark masked by mwan3's MMX_MASK. The priority gate alone
|
||||
@@ -149,13 +152,15 @@ stop_service() {
|
||||
# /var/run/mwan3/mmx_mask and /var/state/mwan3 (set at start_service
|
||||
# time), so they reflect what the running instance created, not
|
||||
# whatever is currently in /etc/config/mwan3.
|
||||
local line pref tbl iif_min iif_max fw_min fw_max ur_min ur_max mask
|
||||
local line pref tbl iif_min iif_max fw_min fw_max ur_min ur_max src_min src_max mask
|
||||
iif_min=$((MWAN3_IIF_RULE_BASE + 1))
|
||||
iif_max=$((MWAN3_IIF_RULE_BASE + MWAN3_INTERFACE_MAX))
|
||||
fw_min=$((MWAN3_FWMARK_RULE_BASE + 1))
|
||||
fw_max=$((MWAN3_FWMARK_RULE_BASE + MM_UNREACHABLE))
|
||||
ur_min=$((MWAN3_UNREACHABLE_RULE_BASE + 1))
|
||||
ur_max=$((MWAN3_UNREACHABLE_RULE_BASE + MWAN3_INTERFACE_MAX))
|
||||
src_min=$((MWAN3_UNREACHABLE_RULE_BASE + MWAN3_INTERFACE_MAX + 2))
|
||||
src_max=$((MWAN3_UNREACHABLE_RULE_BASE + 2 * MWAN3_INTERFACE_MAX + 1))
|
||||
# ip rule list prints fwmark masks lowercase; normalise for matching
|
||||
mask=$(echo "$MMX_MASK" | tr 'A-Z' 'a-z')
|
||||
$IP rule list | while read -r line; do
|
||||
@@ -166,7 +171,8 @@ stop_service() {
|
||||
# Priority gate
|
||||
if ! { { [ "$pref" -ge "$iif_min" ] && [ "$pref" -le "$iif_max" ]; } || \
|
||||
{ [ "$pref" -ge "$fw_min" ] && [ "$pref" -le "$fw_max" ]; } || \
|
||||
{ [ "$pref" -ge "$ur_min" ] && [ "$pref" -le "$ur_max" ]; }; }; then
|
||||
{ [ "$pref" -ge "$ur_min" ] && [ "$pref" -le "$ur_max" ]; } || \
|
||||
{ [ "$pref" -ge "$src_min" ] && [ "$pref" -le "$src_max" ]; }; }; then
|
||||
continue
|
||||
fi
|
||||
# Content gate (a): carries an fwmark masked by mwan3's MMX_MASK
|
||||
@@ -190,40 +196,9 @@ stop_service() {
|
||||
done
|
||||
done
|
||||
|
||||
# Flush all mwan3 chains (rules removed, chains stay as skeletons)
|
||||
for chain_name in $($NFT list chains inet 2>/dev/null | grep "chain mwan3_" | awk '{print $2}'); do
|
||||
$NFT flush chain inet mwan3 "$chain_name" >/dev/null 2>&1
|
||||
done
|
||||
|
||||
# Delete dynamic per-interface and per-policy chains (not the skeleton ones from 10-mwan3.nft)
|
||||
for chain_name in $($NFT list chains inet 2>/dev/null | grep "chain mwan3_" | awk '{print $2}'); do
|
||||
case "$chain_name" in
|
||||
mwan3_prerouting|mwan3_output|mwan3_postrouting|mwan3_ifaces_in|mwan3_rules|mwan3_connected|mwan3_custom|mwan3_dynamic)
|
||||
# These are skeleton chains from the static .nft file; keep them
|
||||
;;
|
||||
*)
|
||||
$NFT delete chain inet mwan3 "$chain_name" >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Final safety flush of skeleton chains (handles any transient failures above)
|
||||
for chain_name in mwan3_prerouting mwan3_output mwan3_postrouting mwan3_ifaces_in mwan3_rules mwan3_connected mwan3_custom mwan3_dynamic; do
|
||||
$NFT flush chain inet mwan3 "$chain_name" >/dev/null 2>&1
|
||||
done
|
||||
|
||||
# Flush all mwan3 sets
|
||||
for setname in $($NFT list sets inet mwan3 2>/dev/null | grep "set mwan3_" | awk '{print $2}'); do
|
||||
$NFT flush set inet mwan3 "$setname" >/dev/null 2>&1
|
||||
done
|
||||
|
||||
# Delete dynamic maps (sticky maps)
|
||||
for mapname in $($NFT list maps inet mwan3 2>/dev/null | grep "map mwan3_" | awk '{print $2}'); do
|
||||
$NFT flush map inet mwan3 "$mapname" >/dev/null 2>&1
|
||||
$NFT delete map inet mwan3 "$mapname" >/dev/null 2>&1
|
||||
done
|
||||
|
||||
# Remove the table entirely so a stopped mwan3 leaves no nft residue
|
||||
# Remove the table entirely. The single atomic delete drops every
|
||||
# chain, set and map mwan3 created in one kernel transaction, so a
|
||||
# stopped mwan3 leaves no nft residue.
|
||||
$NFT delete table inet mwan3 >/dev/null 2>&1
|
||||
|
||||
rm -rf $MWAN3_STATUS_DIR $MWAN3TRACK_STATUS_DIR
|
||||
@@ -233,6 +208,23 @@ stop_service() {
|
||||
reload_service() {
|
||||
mwan3_init
|
||||
|
||||
# The nft table is created only by the skeleton load in start_service,
|
||||
# which is skipped when the service starts with every interface
|
||||
# disabled. In that state the table is absent while the status
|
||||
# directory (created by mwan3_init) exists, so the service reports
|
||||
# running and a config change drives a reload, not a start. Rebuilding
|
||||
# the ruleset here would batch every add against a missing table, the
|
||||
# atomic commit would fail, and this function would return at the commit
|
||||
# check before the tracker-mismatch restart below. Recover with a full
|
||||
# stop and start, which reloads the skeleton and recreates the table.
|
||||
|
||||
if ! $NFT list table inet mwan3 >/dev/null 2>&1; then
|
||||
LOG notice "reload escalated to restart as table inet mwan3 not initialised"
|
||||
stop
|
||||
start
|
||||
return
|
||||
fi
|
||||
|
||||
# Atomically rebuild the entire mwan3 nft ruleset in a single kernel
|
||||
# transaction. mwan3_nft_reload_start opens the outermost batch level
|
||||
# (MWAN3_BATCH_DEPTH 0->1) so all subsequent batch_start/commit calls
|
||||
@@ -256,6 +248,7 @@ reload_service() {
|
||||
# update them after the nft commit so the table is already consistent.
|
||||
mwan3_update_iface_to_table
|
||||
mwan3_set_general_rules
|
||||
mwan3_set_member_backstops
|
||||
config_foreach mwan3_rebuild_iface_rules interface
|
||||
|
||||
# Signal mwan3rtmon to reload UCI config and re-sync the custom sets.
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
# $DEVICE Physical device name of the interface the action relates to (e.g. "eth0" or "wwan0").
|
||||
# Note: On an ifdown event, $DEVICE is not available, use $INTERFACE instead.
|
||||
#
|
||||
# Further documentation can be found here:
|
||||
# https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3#alertsnotifications
|
||||
# Further documentation can be found at:
|
||||
# https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3-nft#hotplug_events_mwan3user
|
||||
@@ -4,6 +4,7 @@
|
||||
import * as rtnl from "rtnl";
|
||||
import * as uci from "uci";
|
||||
import * as ubus from "ubus";
|
||||
import { log_open, log_msg } from 'mwan3.common';
|
||||
|
||||
const RTM_GETROUTE = rtnl.const.RTM_GETROUTE;
|
||||
const RTM_NEWROUTE = rtnl.const.RTM_NEWROUTE;
|
||||
@@ -56,12 +57,14 @@ let tid = 0;
|
||||
cur.foreach("mwan3", "interface", function(s) {
|
||||
tid++;
|
||||
let fam = s.family ?? "ipv4";
|
||||
let enabled = +(s.enabled ?? "1");
|
||||
let enabled = +(s.enabled ?? "0");
|
||||
if (enabled && fam == family_name)
|
||||
name_tid[s[".name"]] = tid;
|
||||
});
|
||||
cur.unload("mwan3");
|
||||
|
||||
log_open("mwan3-create-iface-route");
|
||||
|
||||
let dev_table_map = {};
|
||||
let uconn = ubus.connect();
|
||||
if (uconn) {
|
||||
@@ -117,5 +120,5 @@ for (let route in source_routes) {
|
||||
rtnl.request(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_REPLACE, r);
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-create-iface-route: table %d: %s\n", table_id, err));
|
||||
log_msg("err", sprintf("table %d: %s", table_id, err));
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
'use strict';
|
||||
|
||||
import * as rtnl from "rtnl";
|
||||
import { is_default_equivalent } from 'mwan3.common';
|
||||
|
||||
const RTM_GETROUTE = rtnl.const.RTM_GETROUTE;
|
||||
const NLM_F_DUMP = rtnl.const.NLM_F_DUMP;
|
||||
@@ -18,12 +19,6 @@ function is_cidr_route(route, family_num) {
|
||||
return (family_num == AF_INET) ? (prefix_len < 32) : (prefix_len < 128);
|
||||
}
|
||||
|
||||
function is_default_route(route) {
|
||||
return (route.dst == null ||
|
||||
route.dst == "0.0.0.0/0" ||
|
||||
route.dst == "::/0");
|
||||
}
|
||||
|
||||
function is_linklocal_route(route) {
|
||||
return (route.dst != null &&
|
||||
(match(route.dst, /^fe80::\//) != null ||
|
||||
@@ -40,7 +35,7 @@ let seen = {};
|
||||
for (let route in routes) {
|
||||
if (route.table != table_num) continue;
|
||||
if (!is_cidr_route(route, family_num)) continue;
|
||||
if (is_default_route(route)) continue;
|
||||
if (is_default_equivalent(route)) continue;
|
||||
if (is_linklocal_route(route)) continue;
|
||||
if (family_num == AF_INET) {
|
||||
let first_octet = +split(route.dst, ".")[0];
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
'use strict';
|
||||
|
||||
import * as rtnl from "rtnl";
|
||||
import { log_open, log_msg } from 'mwan3.common';
|
||||
|
||||
const RTM_GETRULE = rtnl.const.RTM_GETRULE;
|
||||
const RTM_DELRULE = rtnl.const.RTM_DELRULE;
|
||||
const RTM_NEWRULE = rtnl.const.RTM_NEWRULE;
|
||||
const RTM_GETROUTE = rtnl.const.RTM_GETROUTE;
|
||||
const RTM_GETADDR = rtnl.const.RTM_GETADDR;
|
||||
const NLM_F_DUMP = rtnl.const.NLM_F_DUMP;
|
||||
const NLM_F_CREATE = rtnl.const.NLM_F_CREATE;
|
||||
const AF_INET = rtnl.const.AF_INET;
|
||||
@@ -22,6 +24,8 @@ function is_default_route(route) {
|
||||
route.dst == "::/0");
|
||||
}
|
||||
|
||||
log_open("mwan3-manage-rules");
|
||||
|
||||
let mode = ARGV[0];
|
||||
|
||||
if (mode == "check") {
|
||||
@@ -59,20 +63,33 @@ if (mode == "check") {
|
||||
let id = +ARGV[1];
|
||||
let iif_base = +ARGV[2];
|
||||
let fwmark_base = +ARGV[3];
|
||||
let unreachable_base = +ARGV[4];
|
||||
let mmx_mask = +ARGV[5];
|
||||
let mmx_mask = +ARGV[4];
|
||||
let src_prio = +ARGV[5];
|
||||
|
||||
let iif_prio = iif_base + id;
|
||||
let fwmark_prio = fwmark_base + id;
|
||||
let unreachable_prio = unreachable_base + id;
|
||||
|
||||
for (let family in [AF_INET, AF_INET6]) {
|
||||
let rules = rtnl.request(RTM_GETRULE, NLM_F_DUMP, { family: family }) ?? [];
|
||||
let fwmark_val = null;
|
||||
|
||||
for (let rule in rules) {
|
||||
if (rule.table != id) continue;
|
||||
if (rule.action != FR_ACT_TO_TBL) continue;
|
||||
|
||||
if (rule.src != null && rule.priority == src_prio) {
|
||||
rtnl.request(RTM_DELRULE, 0, {
|
||||
family: family,
|
||||
priority: rule.priority,
|
||||
src: rule.src,
|
||||
table: rule.table,
|
||||
action: rule.action
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
log_msg("err", sprintf("delete src rule failed: %s", err));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rule.src != null || rule.dst != null) continue;
|
||||
|
||||
if (rule.fwmark == null && rule.priority == iif_prio) {
|
||||
@@ -84,9 +101,8 @@ if (mode == "check") {
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: delete iif rule failed: %s\n", err));
|
||||
log_msg("err", sprintf("delete iif rule failed: %s", err));
|
||||
} else if (rule.fwmark != null && rule.priority == fwmark_prio && rule.fwmask == mmx_mask) {
|
||||
fwmark_val = rule.fwmark;
|
||||
rtnl.request(RTM_DELRULE, 0, {
|
||||
family: family,
|
||||
priority: rule.priority,
|
||||
@@ -97,27 +113,7 @@ if (mode == "check") {
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: delete fwmark rule failed: %s\n", err));
|
||||
}
|
||||
}
|
||||
|
||||
if (fwmark_val != null) {
|
||||
for (let rule in rules) {
|
||||
if (rule.fwmark != fwmark_val) continue;
|
||||
if (rule.action != FR_ACT_UNREACHABLE) continue;
|
||||
if (rule.priority != unreachable_prio) continue;
|
||||
if (rule.fwmask != mmx_mask) continue;
|
||||
if (rule.src != null || rule.dst != null) continue;
|
||||
rtnl.request(RTM_DELRULE, 0, {
|
||||
family: family,
|
||||
priority: rule.priority,
|
||||
fwmark: rule.fwmark,
|
||||
fwmask: rule.fwmask,
|
||||
action: rule.action
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: delete unreachable rule failed: %s\n", err));
|
||||
log_msg("err", sprintf("delete fwmark rule failed: %s", err));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +140,7 @@ if (mode == "check") {
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: add blackhole rule failed: %s\n", err));
|
||||
log_msg("err", sprintf("add blackhole rule failed: %s", err));
|
||||
}
|
||||
|
||||
if (!existing[ur_prio]) {
|
||||
@@ -157,6 +153,55 @@ if (mode == "check") {
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: add unreachable rule failed: %s\n", err));
|
||||
log_msg("err", sprintf("add unreachable rule failed: %s", err));
|
||||
}
|
||||
|
||||
} else if (mode == "add-backstop") {
|
||||
let family_num = (ARGV[1] == "6") ? AF_INET6 : AF_INET;
|
||||
let prio = +ARGV[2], mark = +ARGV[3];
|
||||
let mask = +ARGV[4];
|
||||
|
||||
let rules = rtnl.request(RTM_GETRULE, NLM_F_DUMP, { family: family_num }) ?? [];
|
||||
let existing = {};
|
||||
for (let r in rules)
|
||||
if (r.priority != null)
|
||||
existing[r.priority] = true;
|
||||
|
||||
if (!existing[prio]) {
|
||||
rtnl.request(RTM_NEWRULE, NLM_F_CREATE, {
|
||||
family: family_num,
|
||||
priority: prio,
|
||||
fwmark: mark,
|
||||
fwmask: mask,
|
||||
action: FR_ACT_UNREACHABLE
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
log_msg("err", sprintf("add backstop rule failed: %s", err));
|
||||
}
|
||||
|
||||
} else if (mode == "add-src") {
|
||||
// IPv4 only
|
||||
|
||||
let device = ARGV[1];
|
||||
let table_id = +ARGV[2];
|
||||
let prio = +ARGV[3];
|
||||
|
||||
let addrs = rtnl.request(RTM_GETADDR, NLM_F_DUMP, { family: AF_INET }) ?? [];
|
||||
for (let a in addrs) {
|
||||
if (a.dev != device) continue;
|
||||
if (a.scope != 0) continue;
|
||||
let addr = split(a.local ?? a.address ?? "", "/")[0];
|
||||
if (!addr) continue;
|
||||
rtnl.request(RTM_NEWRULE, NLM_F_CREATE, {
|
||||
family: AF_INET,
|
||||
priority: prio,
|
||||
src: addr,
|
||||
table: table_id,
|
||||
action: FR_ACT_TO_TBL
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
log_msg("err", sprintf("add src rule failed: %s", err));
|
||||
}
|
||||
}
|
||||
+336
-100
@@ -266,7 +266,8 @@ _mwan3_render_one_ipset()
|
||||
|
||||
set_decl="type ${addr_type}; flags interval"
|
||||
[ "$timeout" -gt 0 ] && set_decl="$set_decl, timeout"
|
||||
set_decl="$set_decl; auto-merge;"
|
||||
set_decl="$set_decl;"
|
||||
[ "$timeout" -eq 0 ] && set_decl="$set_decl auto-merge;"
|
||||
[ "$counters" -eq 1 ] && set_decl="$set_decl counter;"
|
||||
[ "$timeout" -gt 0 ] && set_decl="$set_decl timeout ${timeout}s;"
|
||||
[ "$maxelem" -gt 0 ] && set_decl="$set_decl size ${maxelem};"
|
||||
@@ -291,14 +292,35 @@ _mwan3_render_one_ipset()
|
||||
fi
|
||||
|
||||
# Collect all elements (inline list + loadfile) before entering batch.
|
||||
# Every element is validated against the nft set element grammar (IPv4 and
|
||||
# IPv6 addresses, CIDR prefixes and hyphen ranges) so a crafted value
|
||||
# cannot close the element braces early and inject nft statements into the
|
||||
# batch that is later committed with nft -f. Anything carrying a character
|
||||
# outside that class is dropped.
|
||||
|
||||
local elements="" line
|
||||
_add_entry() { elements="${elements:+$elements, }$1"; }
|
||||
_add_entry() {
|
||||
case "$1" in
|
||||
""|*[!0-9A-Fa-f:./-]*)
|
||||
LOG warn "config ipset '$name': dropping invalid entry"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
elements="${elements:+$elements, }$1"
|
||||
}
|
||||
config_list_foreach "$section" entry _add_entry
|
||||
if [ -n "$loadfile" ] && [ -f "$loadfile" ]; then
|
||||
while IFS= read -r line; do
|
||||
line="${line%%#*}"
|
||||
line=$(echo "$line" | xargs 2>/dev/null)
|
||||
[ -n "$line" ] && elements="${elements:+$elements, }$line"
|
||||
[ -n "$line" ] || continue
|
||||
case "$line" in
|
||||
*[!0-9A-Fa-f:./-]*)
|
||||
LOG warn "config ipset '$name': dropping invalid loadfile element"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
elements="${elements:+$elements, }$line"
|
||||
done < "$loadfile"
|
||||
fi
|
||||
|
||||
@@ -466,6 +488,41 @@ mwan3_set_general_rules()
|
||||
"$MMX_MASK"
|
||||
}
|
||||
|
||||
# Install a static unreachable ip rule for every configured interface mark, in
|
||||
# the per-member unreachable priority band. These are created at service start
|
||||
# and reload and removed only at stop, so a packet carrying a member mark whose
|
||||
# fwmark rule is currently absent (the member is down, disabled, or briefly
|
||||
# between a delete and re-add) is rejected instead of leaking to the main
|
||||
# routing table. One rule per member is required because an fwmark rule matches
|
||||
# an exact value under the mask, not "any member mark"; the reserved last_resort
|
||||
# marks are never enumerated here, so last_resort behaviour is unaffected.
|
||||
|
||||
mwan3_set_member_backstops()
|
||||
{
|
||||
mwan3_add_backstop()
|
||||
{
|
||||
local id family fam
|
||||
|
||||
config_get family "$1" family ipv4
|
||||
mwan3_get_iface_id id "$1"
|
||||
[ -n "$id" ] || return 0
|
||||
|
||||
if [ "$family" = "ipv4" ]; then
|
||||
fam=4
|
||||
elif [ "$family" = "ipv6" ] && [ $NO_IPV6 -eq 0 ]; then
|
||||
fam=6
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
|
||||
${MWAN3_MANAGE_RULES} add-backstop "$fam" \
|
||||
"$((id+MWAN3_UNREACHABLE_RULE_BASE))" \
|
||||
"$(mwan3_id2mask id MMX_MASK)" "$MMX_MASK"
|
||||
}
|
||||
|
||||
config_foreach mwan3_add_backstop interface
|
||||
}
|
||||
|
||||
mwan3_set_general_nft()
|
||||
{
|
||||
local chain_exists restore_vmap save_vmap all_marks
|
||||
@@ -843,16 +900,112 @@ mwan3_delete_iface_map_entries()
|
||||
# Sticky scheme: one set per (rule, family, iface_id) holding
|
||||
# only saddrs (no value side). Removing an interface invalidates every
|
||||
# such set whose name ends in "_<id>"; we flush rather than delete since
|
||||
# rule chains may still reference the set name.
|
||||
# rule chains may still reference the set name. The flush lines are pushed
|
||||
# into the caller's open nft batch so they commit in the same transaction
|
||||
# as the policy rebuild; the set enumeration reads committed kernel state
|
||||
# and so stays outside the batch. Call only within an open nft batch.
|
||||
|
||||
for setname in $($NFT list sets inet 2>/dev/null | \
|
||||
awk '$1=="set" && $2 ~ /^mwan3_sticky_v[46]_/ { print $2 }'); do
|
||||
case "$setname" in
|
||||
*_"$id") $NFT flush set inet mwan3 "$setname" 2>/dev/null ;;
|
||||
*_"$id") mwan3_nft_push "flush set inet mwan3 $setname" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
mwan3_flush_worse_metric_sticky()
|
||||
{
|
||||
# On recovery of a better-metric member, snap new flows off the worse-metric
|
||||
# backups it is now preferred over. For each policy the recovered interface
|
||||
# participates in, flush the per-rule sticky sets of that policy's same-family
|
||||
# members whose metric is strictly worse, scoped to the rules that use the
|
||||
# policy, so pinned sources re-evaluate to the recovered member on their next
|
||||
# new flow while established flows ride their conntrack marks to completion
|
||||
# undisturbed. The scope is per policy and per rule by construction: each set
|
||||
# name is built from the rule name and the member id rather than flushing
|
||||
# every set ending in an id, so a member that is also a deliberate-stickiness
|
||||
# member of a separate load-balance policy keeps those pins.
|
||||
|
||||
local recovered="$1" recovered_id rec_family rec_fam_short
|
||||
local _snap_pairs=""
|
||||
|
||||
mwan3_get_iface_id recovered_id "$recovered"
|
||||
[ -n "$recovered_id" ] || return 0
|
||||
|
||||
# mwan3 runs an independent metric contest per address family, so only
|
||||
# same-family members compete with the recovered one; resolve its family once
|
||||
# and snap only that family's backups.
|
||||
|
||||
config_get rec_family "$recovered" family ipv4
|
||||
if [ "$rec_family" = "ipv6" ]; then
|
||||
rec_fam_short="v6"
|
||||
else
|
||||
rec_fam_short="v4"
|
||||
fi
|
||||
|
||||
# Record one "policy=worse-id" pair per strictly-worse-metric same-family
|
||||
# member of each policy the recovered interface is in. The recovered
|
||||
# interface's own id is excluded so its own pins are never cleared.
|
||||
|
||||
_mwan3_snap_collect() {
|
||||
local policy="$1" rec_metric=""
|
||||
|
||||
# The recovered interface's metric in this policy sets the threshold;
|
||||
# take the lowest if it appears more than once.
|
||||
|
||||
_mwan3_snap_rec_metric() {
|
||||
local m_iface m_metric
|
||||
config_get m_iface "$1" interface
|
||||
[ "$m_iface" = "$recovered" ] || return
|
||||
config_get m_metric "$1" metric 1
|
||||
{ [ -z "$rec_metric" ] || [ "$m_metric" -lt "$rec_metric" ]; } && rec_metric="$m_metric"
|
||||
}
|
||||
config_list_foreach "$policy" use_member _mwan3_snap_rec_metric
|
||||
|
||||
# Policy does not include the recovered interface: nothing to snap.
|
||||
|
||||
[ -n "$rec_metric" ] || return
|
||||
|
||||
_mwan3_snap_worse() {
|
||||
local m_iface m_metric m_id m_family
|
||||
config_get m_iface "$1" interface
|
||||
[ -n "$m_iface" ] || return
|
||||
config_get m_family "$m_iface" family ipv4
|
||||
[ "$m_family" = "$rec_family" ] || return
|
||||
config_get m_metric "$1" metric 1
|
||||
[ "$m_metric" -gt "$rec_metric" ] || return
|
||||
mwan3_get_iface_id m_id "$m_iface"
|
||||
[ -n "$m_id" ] || return
|
||||
[ "$m_id" = "$recovered_id" ] && return
|
||||
case " $_snap_pairs " in
|
||||
*" ${policy}=${m_id} "*) ;;
|
||||
*) _snap_pairs="$_snap_pairs ${policy}=${m_id}" ;;
|
||||
esac
|
||||
}
|
||||
config_list_foreach "$policy" use_member _mwan3_snap_worse
|
||||
}
|
||||
config_foreach _mwan3_snap_collect policy
|
||||
|
||||
[ -n "$_snap_pairs" ] || return 0
|
||||
|
||||
# For every rule, flush the worse-metric members' per-rule sticky sets of the
|
||||
# policy that rule uses, in the recovered member's family. A set name that
|
||||
# does not exist is a harmless no-op.
|
||||
|
||||
_mwan3_snap_flush() {
|
||||
local r_policy pair p_name p_id
|
||||
config_get r_policy "$1" use_policy
|
||||
[ -n "$r_policy" ] || return
|
||||
for pair in $_snap_pairs; do
|
||||
p_name="${pair%=*}"
|
||||
p_id="${pair##*=}"
|
||||
[ "$p_name" = "$r_policy" ] || continue
|
||||
$NFT flush set inet mwan3 "mwan3_sticky_${rec_fam_short}_${1}_${p_id}" 2>/dev/null
|
||||
done
|
||||
}
|
||||
config_foreach _mwan3_snap_flush rule
|
||||
}
|
||||
|
||||
mwan3_create_iface_route()
|
||||
{
|
||||
local family id source_routing _fam_num
|
||||
@@ -904,7 +1057,8 @@ mwan3_create_iface_rules()
|
||||
|
||||
$IP rule add pref $((id+MWAN3_IIF_RULE_BASE)) iif "$2" lookup "$id" 2>/dev/null
|
||||
$IP rule add pref $((id+MWAN3_FWMARK_RULE_BASE)) fwmark "$(mwan3_id2mask id MMX_MASK)/$MMX_MASK" lookup "$id" 2>/dev/null
|
||||
$IP rule add pref $((id+MWAN3_UNREACHABLE_RULE_BASE)) fwmark "$(mwan3_id2mask id MMX_MASK)/$MMX_MASK" unreachable 2>/dev/null
|
||||
[ "$family" = "ipv4" ] && ${MWAN3_MANAGE_RULES} add-src "$2" "$id" \
|
||||
"$((id + MWAN3_UNREACHABLE_RULE_BASE + MWAN3_INTERFACE_MAX + 1))"
|
||||
}
|
||||
|
||||
mwan3_delete_iface_rules()
|
||||
@@ -915,13 +1069,13 @@ mwan3_delete_iface_rules()
|
||||
[ -n "$id" ] || return 0
|
||||
|
||||
${MWAN3_MANAGE_RULES} delete-iface "$id" \
|
||||
"$MWAN3_IIF_RULE_BASE" "$MWAN3_FWMARK_RULE_BASE" \
|
||||
"$MWAN3_UNREACHABLE_RULE_BASE" "$MMX_MASK"
|
||||
"$MWAN3_IIF_RULE_BASE" "$MWAN3_FWMARK_RULE_BASE" "$MMX_MASK" \
|
||||
"$((id + MWAN3_UNREACHABLE_RULE_BASE + MWAN3_INTERFACE_MAX + 1))"
|
||||
}
|
||||
|
||||
mwan3_set_policy()
|
||||
{
|
||||
local id iface family metric weight device is_lowest is_offline
|
||||
local id iface family metric weight is_lowest is_offline
|
||||
|
||||
is_lowest=0
|
||||
config_get iface "$1" interface
|
||||
@@ -929,7 +1083,6 @@ mwan3_set_policy()
|
||||
config_get weight "$1" weight 1
|
||||
|
||||
[ -n "$iface" ] || return 0
|
||||
network_get_device device "$iface"
|
||||
[ "$metric" -gt $DEFAULT_LOWEST_METRIC ] && LOG warn "Member interface $iface has >$DEFAULT_LOWEST_METRIC metric. Not appending to policy" && return 0
|
||||
|
||||
mwan3_get_iface_id id "$iface"
|
||||
@@ -976,30 +1129,28 @@ mwan3_set_policy()
|
||||
|
||||
if [ $is_offline -eq 0 ]; then
|
||||
|
||||
# Accumulate members per family: "iface_name:id:weight" tuples
|
||||
# Accumulate members per family: "iface_name:id:weight" tuples.
|
||||
# The branches mirror the metric contest above, so a member only
|
||||
# accumulates through its interface family's contest: an IPv6
|
||||
# member is excluded on a kernel without IPv6, and an
|
||||
# unrecognised family value is never accumulated.
|
||||
|
||||
if [ "$family" = "ipv4" ]; then
|
||||
policy_members_v4="$policy_members_v4 $iface:$id:$weight"
|
||||
else
|
||||
elif [ "$family" = "ipv6" ] && [ $NO_IPV6 -eq 0 ]; then
|
||||
policy_members_v6="$policy_members_v6 $iface:$id:$weight"
|
||||
fi
|
||||
elif [ -n "$device" ]; then
|
||||
|
||||
# Offline interface with device: record for fallback out-device rule
|
||||
|
||||
policy_offline_devices="$policy_offline_devices $iface:$device"
|
||||
fi
|
||||
}
|
||||
|
||||
mwan3_create_policies_nft()
|
||||
{
|
||||
local last_resort lowest_metric_v4 lowest_metric_v6 total_weight_v4 total_weight_v6
|
||||
local policy policy_members_v4 policy_members_v6 policy_offline_devices
|
||||
local policy policy_members_v4 policy_members_v6
|
||||
|
||||
policy="$1"
|
||||
policy_members_v4=""
|
||||
policy_members_v6=""
|
||||
policy_offline_devices=""
|
||||
|
||||
config_get last_resort "$1" last_resort unreachable
|
||||
|
||||
@@ -1097,22 +1248,6 @@ mwan3_create_policies_nft()
|
||||
done
|
||||
fi
|
||||
|
||||
# Add offline device fallback rules
|
||||
|
||||
local dev_entry offline_iface offline_device
|
||||
|
||||
# Only add if no online members
|
||||
|
||||
if [ "$total_weight" -eq 0 ]; then
|
||||
for dev_entry in $policy_offline_devices; do
|
||||
offline_iface="${dev_entry%%:*}"
|
||||
offline_device="${dev_entry#*:}"
|
||||
mwan3_nft_exec add rule inet mwan3 "mwan3_policy_$policy" \
|
||||
oifname "$offline_device" meta mark \& "$MMX_MASK" == 0 \
|
||||
"$(mwan3_nft_mark_expr $MMX_DEFAULT $MMX_MASK)"
|
||||
done
|
||||
fi
|
||||
|
||||
# Add last resort rule
|
||||
|
||||
case "$last_resort" in
|
||||
@@ -1214,7 +1349,7 @@ mwan3_set_user_nft_rule()
|
||||
local ipset_name ipset_src family proto policy src_ip src_port src_iface src_dev
|
||||
local sticky dest_ip dest_port use_policy timeout policy
|
||||
local global_logging rule_logging loglevel rule_policy rule ipv
|
||||
local enabled fwmark fwmask _check_set _set_info
|
||||
local enabled fwmark fwmask _check_set _set_info _fam_anchored
|
||||
|
||||
config_get_bool enabled "$1" enabled 1
|
||||
[ "$enabled" -eq 1 ] || return
|
||||
@@ -1262,18 +1397,30 @@ mwan3_set_user_nft_rule()
|
||||
[ "$family" = "ipv4" ] && [ "$ipv" = "ipv6" ] && return
|
||||
[ "$family" = "ipv6" ] && [ "$ipv" = "ipv4" ] && return
|
||||
|
||||
# family=any rules whose nft expression has no IP-version-specific element
|
||||
# (no src_ip/dest_ip/ipset/ipset_src) generate identical output on both the
|
||||
# ipv4 and ipv6 passes. Skip the ipv6 pass to avoid pushing a duplicate
|
||||
# rule. The ipv4 pass output already matches IPv6 traffic at runtime
|
||||
# because the match operates on family-agnostic fields (meta mark, l4proto,
|
||||
# port, iifname). Exception: proto=icmp requires both passes because ICMP
|
||||
# (protocol 1) and ICMPv6 (protocol 58) are distinct L4 protocols.
|
||||
# A family-anchoring element (src_ip/dest_ip/ipset/ipset_src) ties the
|
||||
# rendered expression to one address family per pass. This list must
|
||||
# cover every matcher whose nft form differs by address family; the
|
||||
# de-duplication guard and the sticky block both key on the flag.
|
||||
|
||||
_fam_anchored=0
|
||||
if [ -n "$src_ip" ] || [ -n "$dest_ip" ] || \
|
||||
[ -n "$ipset_name" ] || [ -n "$ipset_src" ]; then
|
||||
_fam_anchored=1
|
||||
fi
|
||||
|
||||
# family=any rules with no family-anchoring element render once, on
|
||||
# the ipv4 pass: the emitted line matches on family-agnostic fields
|
||||
# (meta mark, l4proto, port, iifname, ether saddr) and carries no
|
||||
# nfproto guard, so it covers both families at its config position,
|
||||
# and the sticky block builds the per-rule chain complete for both
|
||||
# families on that pass. Skip the ipv6 pass to avoid a duplicate
|
||||
# line. Exception: proto=icmp requires both passes because ICMP
|
||||
# (protocol 1) and ICMPv6 (protocol 58) are distinct L4 protocols;
|
||||
# the ipv6 pass then emits its own line but must not rebuild the
|
||||
# sticky chain.
|
||||
|
||||
if [ "$family" = "any" ] && [ "$ipv" = "ipv6" ] && \
|
||||
[ -z "$src_ip" ] && [ -z "$dest_ip" ] && \
|
||||
[ -z "$ipset_name" ] && [ -z "$ipset_src" ] && \
|
||||
[ "$proto" != "icmp" ]; then
|
||||
[ $_fam_anchored -eq 0 ] && [ "$proto" != "icmp" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -1300,7 +1447,13 @@ mwan3_set_user_nft_rule()
|
||||
# the src_ip/dest_ip address validation above: a set of type ipv4_addr
|
||||
# cannot appear in an "ip6 daddr @set" expression, and vice versa.
|
||||
|
||||
local _ipset_name_uci_type="" _ipset_src_uci_type=""
|
||||
local _ipset_name_uci_type="" _ipset_src_uci_type="" _pass_addrtype
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
_pass_addrtype="ipv4_addr"
|
||||
else
|
||||
_pass_addrtype="ipv6_addr"
|
||||
fi
|
||||
|
||||
for _check_set in "$ipset_name" "$ipset_src"; do
|
||||
[ -z "$_check_set" ] && continue
|
||||
local _set_info
|
||||
@@ -1339,6 +1492,25 @@ mwan3_set_user_nft_rule()
|
||||
[ "$ipv" = "ipv6" ] && [ "$_uci_addrtype" = "ipv4_addr" ] && return
|
||||
continue
|
||||
fi
|
||||
|
||||
# An earlier rule in this render may already have pre-created
|
||||
# the set into the open batch, which the kernel query above
|
||||
# cannot see. Consult the render's own record instead: the
|
||||
# first pre-creation establishes the set's type for the whole
|
||||
# render, and every later reference is treated exactly as if
|
||||
# the kernel held a set of that type.
|
||||
|
||||
case " $MWAN3_RENDER_SETS " in
|
||||
*" ${_check_set}=${_pass_addrtype} "*)
|
||||
continue
|
||||
;;
|
||||
*" ${_check_set}="*)
|
||||
[ "$family" = "any" ] && return
|
||||
LOG warn "Rule $rule: set '$_check_set' pre-created as the other address family by an earlier rule, incompatible with family $family"
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
# Not UCI-managed: apply existing guard for external sets.
|
||||
# For family=any, skip the ipv6 pass; the ipv4 pass will pre-create
|
||||
# the set as ipv4_addr until an external creator establishes its type.
|
||||
@@ -1439,15 +1611,20 @@ mwan3_set_user_nft_rule()
|
||||
# set is missing, which would kill ALL user rules.
|
||||
# UCI-managed sets are already added to the batch by mwan3_render_config_ipsets;
|
||||
# skip the kernel check and pre-creation for those.
|
||||
# A set this render has already pre-created is in the record and is
|
||||
# never added twice: two adds of one name in a batch are rejected
|
||||
# when the types differ, and redundant when they match.
|
||||
|
||||
if [ -z "$_ipset_name_uci_type" ] && \
|
||||
! $NFT list set inet mwan3 "$ipset_name" &>/dev/null; then
|
||||
LOG notice "Creating missing nft set '$ipset_name' for rule $rule"
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_name { type ipv4_addr; flags interval; auto-merge; }"
|
||||
else
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_name { type ipv6_addr; flags interval; auto-merge; }"
|
||||
fi
|
||||
case " $MWAN3_RENDER_SETS " in
|
||||
*" ${ipset_name}=${_pass_addrtype} "*) ;;
|
||||
*)
|
||||
LOG notice "Creating missing nft set '$ipset_name' for rule $rule"
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_name { type ${_pass_addrtype}; flags interval; auto-merge; }"
|
||||
MWAN3_RENDER_SETS="$MWAN3_RENDER_SETS ${ipset_name}=${_pass_addrtype}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
nft_match="$nft_match ip daddr @$ipset_name"
|
||||
@@ -1461,12 +1638,14 @@ mwan3_set_user_nft_rule()
|
||||
if [ -n "$ipset_src" ]; then
|
||||
if [ -z "$_ipset_src_uci_type" ] && \
|
||||
! $NFT list set inet mwan3 "$ipset_src" &>/dev/null; then
|
||||
LOG notice "Creating missing nft set '$ipset_src' for rule $rule"
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_src { type ipv4_addr; flags interval; auto-merge; }"
|
||||
else
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_src { type ipv6_addr; flags interval; auto-merge; }"
|
||||
fi
|
||||
case " $MWAN3_RENDER_SETS " in
|
||||
*" ${ipset_src}=${_pass_addrtype} "*) ;;
|
||||
*)
|
||||
LOG notice "Creating missing nft set '$ipset_src' for rule $rule"
|
||||
mwan3_nft_push "add set inet mwan3 $ipset_src { type ${_pass_addrtype}; flags interval; auto-merge; }"
|
||||
MWAN3_RENDER_SETS="$MWAN3_RENDER_SETS ${ipset_src}=${_pass_addrtype}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
nft_match="$nft_match ip saddr @$ipset_src"
|
||||
@@ -1509,7 +1688,7 @@ mwan3_set_user_nft_rule()
|
||||
# like default_rule (family ipv4, no saddr/daddr) generates a bare
|
||||
# "meta mark ... jump policy" that matches IPv6 traffic too.
|
||||
|
||||
if [ -z "$src_ip" ] && [ -z "$dest_ip" ] && [ -z "$ipset_name" ] && [ -z "$ipset_src" ]; then
|
||||
if [ $_fam_anchored -eq 0 ]; then
|
||||
if [ "$family" = "ipv4" ]; then
|
||||
nft_match="${nft_match:+$nft_match }meta nfproto ipv4"
|
||||
elif [ "$family" = "ipv6" ]; then
|
||||
@@ -1549,49 +1728,86 @@ mwan3_set_user_nft_rule()
|
||||
# bit. The save side mirrors this with per-member "update @set" rules
|
||||
# guarded on (meta mark & MMX) == <member_mark>.
|
||||
|
||||
# The per-rule chain is shared by every mwan3_rules line that
|
||||
# jumps to it, so it is built exactly once, complete for every
|
||||
# family whose traffic can reach it. A family=any rule with no
|
||||
# family-anchoring element serves both families from the single
|
||||
# line emitted on the ipv4 pass, so that pass builds both
|
||||
# families' arms; for proto=icmp the ipv6 pass also reaches this
|
||||
# block and only points its own line at the finished chain. The
|
||||
# layout is load-bearing: every lookup precedes the single
|
||||
# fall-through, so a pinned source ORs its member mark into the
|
||||
# packet before the fall-through can hand it to the policy, and
|
||||
# the update rules follow.
|
||||
|
||||
local _policy_member_marks _entry _m_id _m_mark _setname
|
||||
local _fam_short _saddr_kw _addr_type
|
||||
if [ "$ipv" = "ipv4" ]; then
|
||||
_fam_short="v4"; _saddr_kw="ip saddr"; _addr_type="ipv4_addr"
|
||||
else
|
||||
_fam_short="v6"; _saddr_kw="ip6 saddr"; _addr_type="ipv6_addr"
|
||||
local _fam_short _saddr_kw _addr_type _sticky_families _sfam
|
||||
|
||||
_sticky_families="$ipv"
|
||||
if [ "$family" = "any" ] && [ $_fam_anchored -eq 0 ]; then
|
||||
if [ "$ipv" = "ipv6" ]; then
|
||||
_sticky_families=""
|
||||
elif [ $NO_IPV6 -eq 0 ]; then
|
||||
_sticky_families="ipv4 ipv6"
|
||||
fi
|
||||
fi
|
||||
|
||||
mwan3_get_policy_members_for_family "$use_policy" "$ipv"
|
||||
_sticky_fam_vars() {
|
||||
if [ "$1" = "ipv4" ]; then
|
||||
_fam_short="v4"; _saddr_kw="ip saddr"; _addr_type="ipv4_addr"
|
||||
else
|
||||
_fam_short="v6"; _saddr_kw="ip6 saddr"; _addr_type="ipv6_addr"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create sticky rule chain if it doesn't exist yet. The chain was
|
||||
# already flushed in the preamble of mwan3_set_user_rules, so both
|
||||
# ipv4 and ipv6 passes can add their rules without interference.
|
||||
if [ -n "$_sticky_families" ]; then
|
||||
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_rule_$1"
|
||||
# Create sticky rule chain if it doesn't exist yet. The chain
|
||||
# was already flushed in the preamble of mwan3_set_user_rules.
|
||||
|
||||
# Per-member sticky sets and lookup rules.
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_rule_$1"
|
||||
|
||||
for _entry in $_policy_member_marks; do
|
||||
_m_id="${_entry%%:*}"
|
||||
_m_mark="${_entry##*:}"
|
||||
_setname="mwan3_sticky_${_fam_short}_${rule}_${_m_id}"
|
||||
# Per-member sticky sets and lookup rules, every served
|
||||
# family's lookups ahead of the fall-through.
|
||||
|
||||
$NFT list set inet mwan3 "$_setname" &>/dev/null || \
|
||||
mwan3_nft_push "add set inet mwan3 $_setname { type ${_addr_type}; flags timeout; timeout ${timeout}s; }"
|
||||
for _sfam in $_sticky_families; do
|
||||
_sticky_fam_vars "$_sfam"
|
||||
mwan3_get_policy_members_for_family "$use_policy" "$_sfam"
|
||||
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 ${_saddr_kw} @${_setname} jump mwan3_or_meta_$(mwan3_or_chain_suffix "$_m_mark")"
|
||||
done
|
||||
for _entry in $_policy_member_marks; do
|
||||
_m_id="${_entry%%:*}"
|
||||
_m_mark="${_entry##*:}"
|
||||
_setname="mwan3_sticky_${_fam_short}_${rule}_${_m_id}"
|
||||
|
||||
# Fall through to policy for new flows (no sticky entry hit -> mark still 0).
|
||||
$NFT list set inet mwan3 "$_setname" &>/dev/null || \
|
||||
mwan3_nft_push "add set inet mwan3 $_setname { type ${_addr_type}; flags timeout; timeout ${timeout}s; }"
|
||||
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 meta mark & $MMX_MASK == 0 jump mwan3_policy_$use_policy"
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 ${_saddr_kw} @${_setname} jump mwan3_or_meta_$(mwan3_or_chain_suffix "$_m_mark")"
|
||||
done
|
||||
done
|
||||
|
||||
# After the policy assigns a mark, populate the matching per-member
|
||||
# sticky set so subsequent packets from this saddr stay on the same WAN.
|
||||
# Fall through to policy for new flows (no sticky entry hit ->
|
||||
# mark still 0).
|
||||
|
||||
for _entry in $_policy_member_marks; do
|
||||
_m_id="${_entry%%:*}"
|
||||
_m_mark="${_entry##*:}"
|
||||
_setname="mwan3_sticky_${_fam_short}_${rule}_${_m_id}"
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 meta mark & $MMX_MASK == 0 jump mwan3_policy_$use_policy"
|
||||
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 meta mark & $MMX_MASK == $_m_mark update @${_setname} { ${_saddr_kw} timeout ${timeout}s }"
|
||||
done
|
||||
# After the policy assigns a mark, populate the matching
|
||||
# per-member sticky set so subsequent packets from this saddr
|
||||
# stay on the same WAN.
|
||||
|
||||
for _sfam in $_sticky_families; do
|
||||
_sticky_fam_vars "$_sfam"
|
||||
mwan3_get_policy_members_for_family "$use_policy" "$_sfam"
|
||||
|
||||
for _entry in $_policy_member_marks; do
|
||||
_m_id="${_entry%%:*}"
|
||||
_m_mark="${_entry##*:}"
|
||||
_setname="mwan3_sticky_${_fam_short}_${rule}_${_m_id}"
|
||||
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_rule_$1 meta mark & $MMX_MASK == $_m_mark update @${_setname} { ${_saddr_kw} timeout ${timeout}s }"
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
policy_action="jump mwan3_rule_$1"
|
||||
fi
|
||||
@@ -1638,14 +1854,21 @@ mwan3_set_user_iface_rules()
|
||||
|
||||
mwan3_set_user_rules()
|
||||
{
|
||||
local ipv
|
||||
# Record of the external sets this render has pre-created into the
|
||||
# batch, as space-delimited "name=addrtype" tokens, read and appended
|
||||
# to by mwan3_set_user_nft_rule through dynamic scoping. Kernel
|
||||
# queries inside an open batch see pre-batch state, so the renderer
|
||||
# carries its own shadow of what the batch already holds. Reset per
|
||||
# render, so repeated renders emit identical batches.
|
||||
|
||||
local MWAN3_RENDER_SETS=""
|
||||
|
||||
mwan3_nft_batch_start
|
||||
|
||||
mwan3_nft_push "flush chain inet mwan3 mwan3_rules"
|
||||
|
||||
# Pre-create and flush per-rule chains for all enabled UCI rules, before
|
||||
# the per-family loop. Sourcing names from UCI (not the kernel) prevents
|
||||
# the render pass. Sourcing names from UCI (not the kernel) prevents
|
||||
# chains for deleted rules from being recreated after reload. "add chain"
|
||||
# is idempotent: it recreates a chain deleted by mwan3_nft_reload_start or
|
||||
# is a no-op if the chain already exists (hotplug path).
|
||||
@@ -1659,10 +1882,22 @@ mwan3_set_user_rules()
|
||||
}
|
||||
config_foreach _init_rule_chain rule
|
||||
|
||||
for ipv in ipv4 ipv6; do
|
||||
[ "$ipv" = "ipv6" ] && [ $NO_IPV6 -ne 0 ] && continue
|
||||
config_foreach mwan3_set_user_nft_rule rule "$ipv"
|
||||
done
|
||||
# Render the rules in one pass over the config, in config order,
|
||||
# emitting each rule's IPv4 and IPv6 variants at that rule's position.
|
||||
# The chain is evaluated first-match, so a rule's position in the chain
|
||||
# must equal its position in the config; rendering per family instead
|
||||
# would place every IPv6-specific rule after every IPv4-pass rule,
|
||||
# letting a family-agnostic rule shadow IPv6 rules the config places
|
||||
# before it.
|
||||
|
||||
_render_rule() {
|
||||
local _rr_ipv
|
||||
for _rr_ipv in ipv4 ipv6; do
|
||||
[ "$_rr_ipv" = "ipv6" ] && [ $NO_IPV6 -ne 0 ] && continue
|
||||
mwan3_set_user_nft_rule "$1" "$_rr_ipv"
|
||||
done
|
||||
}
|
||||
config_foreach _render_rule rule
|
||||
|
||||
mwan3_nft_batch_commit
|
||||
}
|
||||
@@ -2018,12 +2253,13 @@ mwan3_flush_conntrack()
|
||||
config_list_foreach "$interface" flush_conntrack handle_flush "$action"
|
||||
fi
|
||||
|
||||
# On ifdown, selectively flush conntrack entries for this interface's mark.
|
||||
# This forces flows that were using the failed WAN to immediately re-establish
|
||||
# via the new policy rather than waiting for a TCP retransmit timeout.
|
||||
# More targeted than the UCI flush_conntrack mechanism which flushes everything.
|
||||
# On a teardown transition (a hard ifdown or a soft tracker disconnect),
|
||||
# selectively flush conntrack entries carrying this interface's mark. This
|
||||
# forces flows that were using the failed WAN to immediately re-establish via
|
||||
# the new policy rather than waiting for a TCP retransmit timeout. More
|
||||
# targeted than the UCI flush_conntrack mechanism which flushes everything.
|
||||
|
||||
if [ "$action" = "ifdown" ] && [ -e "$CONNTRACK_FILE" ]; then
|
||||
if { [ "$action" = "ifdown" ] || [ "$action" = "disconnected" ]; } && [ -e "$CONNTRACK_FILE" ]; then
|
||||
local iface_id iface_mark
|
||||
mwan3_get_iface_id iface_id "$interface"
|
||||
if [ -n "$iface_id" ]; then
|
||||
|
||||
@@ -87,14 +87,43 @@ let mwan3_cfg = null;
|
||||
|
||||
let pub6_seen = {};
|
||||
let pub4_seen = {};
|
||||
let lit_alias = {};
|
||||
|
||||
// Register one public address. The placeholder map is keyed and numbered on
|
||||
// normalised forms only; when the literal spelling differs from the
|
||||
// normalised form it is recorded as an alias so free-form text carrying
|
||||
// that spelling is still replaced.
|
||||
|
||||
function register(addr_str) {
|
||||
if (!addr_str) return;
|
||||
let lit = split(addr_str, '/')[0];
|
||||
let norm = norm_addr(addr_str);
|
||||
if (!norm) return;
|
||||
let sa = sock.sockaddr(split(addr_str, '/')[0]);
|
||||
let sa = sock.sockaddr(lit);
|
||||
if (sa.family == 10 && is_public_v6(norm)) pub6_seen[norm] = 1;
|
||||
else if (sa.family == 2 && is_public_v4(norm)) pub4_seen[norm] = 1;
|
||||
else return;
|
||||
if (lit != norm) lit_alias[lit] = norm;
|
||||
}
|
||||
|
||||
// Sweep a block of free-form text for address literals. Splitting on any
|
||||
// run of characters that cannot occur inside an address or prefix keeps
|
||||
// every address-bearing token intact; a token that is not a valid public
|
||||
// address fails validation inside register() and drops out. A colon-less
|
||||
// token must be a full dotted quad: the address parser also accepts
|
||||
// shorthand forms like "1.0", which free-form text produces as ordinary
|
||||
// decimal fragments, and registering one would alias that fragment to a
|
||||
// placeholder everywhere in the report.
|
||||
|
||||
function scan_text(text) {
|
||||
if (!text) return;
|
||||
for (let tok in split(text, /[^0-9a-fA-F:.\/]+/)) {
|
||||
if (!length(tok)) continue;
|
||||
if (index(tok, ':') == -1 &&
|
||||
!match(tok, /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(\/[0-9]+)?$/))
|
||||
continue;
|
||||
register(tok);
|
||||
}
|
||||
}
|
||||
|
||||
for (let n in nb_all) { register(n.dst); }
|
||||
@@ -110,57 +139,8 @@ if (mwan3_st?.interfaces)
|
||||
|
||||
// Register any addresses appearing in the mwan3 UCI config (e.g. track_ip
|
||||
// values not yet reflected in routes/status).
|
||||
if (mwan3_cfg)
|
||||
for (let line in split(mwan3_cfg, '\n'))
|
||||
for (let tok in split(replace(line, "'", ' '), ' '))
|
||||
register(trim(tok));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 3: build deterministic placeholder map
|
||||
// Sort addresses so the same address always gets the same placeholder across
|
||||
// multiple runs (e.g. v3.4 run vs v3.5.1 run).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let pub6_list = sort(keys(pub6_seen));
|
||||
let pub4_list = sort(keys(pub4_seen));
|
||||
let addr_map = {};
|
||||
for (let i = 0; i < length(pub6_list); i++)
|
||||
addr_map[pub6_list[i]] = sprintf('PUB6_%d', i + 1);
|
||||
for (let i = 0; i < length(pub4_list); i++)
|
||||
addr_map[pub4_list[i]] = sprintf('PUB4_%d', i + 1);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Anonymisation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Anonymise a single address string (preserves any /prefix-length suffix).
|
||||
function anon(addr_str) {
|
||||
if (!addr_str) return addr_str;
|
||||
let parts = split(addr_str, '/');
|
||||
let norm = norm_addr(parts[0]);
|
||||
let ph = norm ? (addr_map[norm] ?? null) : null;
|
||||
if (ph) return ph + (length(parts) > 1 ? '/' + parts[1] : '');
|
||||
return addr_str;
|
||||
}
|
||||
|
||||
// Anonymise a block of free-form text (nft output, log lines, etc.).
|
||||
// Sorted longest-first so longer addresses are replaced before any shorter
|
||||
// prefix they might share (prevents partial substitutions).
|
||||
let _sorted_addrs = sort(keys(addr_map), (a, b) => length(b) - length(a));
|
||||
function anon_text(text) {
|
||||
if (!text) return '(no output)';
|
||||
for (let addr in _sorted_addrs) {
|
||||
let ph = addr_map[addr];
|
||||
let pos = 0;
|
||||
while (true) {
|
||||
let found = index(text, addr, pos);
|
||||
if (found == -1) break;
|
||||
text = substr(text, 0, found) + ph + substr(text, found + length(addr));
|
||||
pos = found + length(ph);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
scan_text(mwan3_cfg);
|
||||
|
||||
// Strip elements from user-defined nft sets, replacing with { ... }.
|
||||
// mwan3 infrastructure sets (names starting with mwan3_) are left intact:
|
||||
@@ -197,13 +177,102 @@ function strip_set_elements(text) {
|
||||
return join('\n', out);
|
||||
}
|
||||
|
||||
// Run a shell command and return its anonymised output.
|
||||
function pcmd(cmd) {
|
||||
// Run a shell command and return its raw output.
|
||||
function rcmd(cmd) {
|
||||
let f = fs.popen(cmd);
|
||||
if (!f) return '(failed to run: ' + cmd + ')\n';
|
||||
let out = f.read('all');
|
||||
f.close();
|
||||
return anon_text(out || '(no output)\n');
|
||||
return out || '(no output)\n';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detect which table mwan3 is using (v3.4 = inet fw4, v3.5+ = inet mwan3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let mwan3_tbl = 'inet mwan3';
|
||||
{
|
||||
let f = fs.popen('nft list chain inet mwan3 mwan3_prerouting 2>/dev/null | head -1');
|
||||
let l = f?.read('line');
|
||||
f?.close();
|
||||
if (!l || !length(trim(l))) mwan3_tbl = 'inet fw4';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Capture the free-form text sections the report prints and sweep them for
|
||||
// address literals. Some public addresses exist only in this text, e.g. NAT
|
||||
// map elements derived from no route or interface address, or logged
|
||||
// addresses no longer present in live state, and are registered here so
|
||||
// they receive placeholders. Each section is captured exactly once and the
|
||||
// captured text itself is printed later, so no literal can appear in the
|
||||
// printed copy without having been swept. The mwan3 table is stripped of
|
||||
// user-set elements before the sweep so redacted addresses are not
|
||||
// assigned placeholders.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let nft_mwan3_txt = strip_set_elements(rcmd(sprintf('nft list table %s 2>/dev/null', mwan3_tbl)));
|
||||
let fw4_mangle_pre_txt = rcmd('nft list chain inet fw4 mangle_prerouting 2>/dev/null');
|
||||
let fw4_mangle_out_txt = rcmd('nft list chain inet fw4 mangle_output 2>/dev/null');
|
||||
let fw4_output_txt = rcmd('nft list chain inet fw4 output 2>/dev/null');
|
||||
let log_txt = rcmd('logread 2>/dev/null | grep -E "mwan3track\\[|mwan3-hotplug\\[|mwan3-init\\[|mwan3\\[|mwan3rtmon\\[|mwan3-manage-rules\\[|mwan3-create-iface-route\\[" | tail -200');
|
||||
|
||||
scan_text(nft_mwan3_txt);
|
||||
scan_text(fw4_mangle_pre_txt);
|
||||
scan_text(fw4_mangle_out_txt);
|
||||
scan_text(fw4_output_txt);
|
||||
scan_text(log_txt);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 3: build deterministic placeholder map
|
||||
// Sort addresses so the same address always gets the same placeholder across
|
||||
// multiple runs (e.g. v3.4 run vs v3.5.1 run).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let pub6_list = sort(keys(pub6_seen));
|
||||
let pub4_list = sort(keys(pub4_seen));
|
||||
let addr_map = {};
|
||||
for (let i = 0; i < length(pub6_list); i++)
|
||||
addr_map[pub6_list[i]] = sprintf('PUB6_%d', i + 1);
|
||||
for (let i = 0; i < length(pub4_list); i++)
|
||||
addr_map[pub4_list[i]] = sprintf('PUB4_%d', i + 1);
|
||||
|
||||
// A literal spelling that normalises to a registered address receives the
|
||||
// registered form's placeholder, so both spellings are replaced in text.
|
||||
|
||||
for (let lit, norm in lit_alias)
|
||||
if (addr_map[norm]) addr_map[lit] = addr_map[norm];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Anonymisation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Anonymise a single address string (preserves any /prefix-length suffix).
|
||||
function anon(addr_str) {
|
||||
if (!addr_str) return addr_str;
|
||||
let parts = split(addr_str, '/');
|
||||
let norm = norm_addr(parts[0]);
|
||||
let ph = norm ? (addr_map[norm] ?? null) : null;
|
||||
if (ph) return ph + (length(parts) > 1 ? '/' + parts[1] : '');
|
||||
return addr_str;
|
||||
}
|
||||
|
||||
// Anonymise a block of free-form text (nft output, log lines, etc.).
|
||||
// Sorted longest-first so longer addresses are replaced before any shorter
|
||||
// prefix they might share (prevents partial substitutions).
|
||||
let _sorted_addrs = sort(keys(addr_map), (a, b) => length(b) - length(a));
|
||||
function anon_text(text) {
|
||||
if (!text) return '(no output)';
|
||||
for (let addr in _sorted_addrs) {
|
||||
let ph = addr_map[addr];
|
||||
let pos = 0;
|
||||
while (true) {
|
||||
let found = index(text, addr, pos);
|
||||
if (found == -1) break;
|
||||
text = substr(text, 0, found) + ph + substr(text, found + length(addr));
|
||||
pos = found + length(ph);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -224,18 +293,6 @@ function tbl_label(n) {
|
||||
return name ? sprintf('%s (%d)', name, n) : sprintf('%d', n);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detect which table mwan3 is using (v3.4 = inet fw4, v3.5+ = inet mwan3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let mwan3_tbl = 'inet mwan3';
|
||||
{
|
||||
let f = fs.popen('nft list chain inet mwan3 mwan3_prerouting 2>/dev/null | head -1');
|
||||
let l = f?.read('line');
|
||||
f?.close();
|
||||
if (!l || !length(trim(l))) mwan3_tbl = 'inet fw4';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// mwan3 version
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -407,20 +464,20 @@ else
|
||||
|
||||
// --- full mwan3 nft table ---
|
||||
printf('=== mwan3 nft table (%s) ===\n', mwan3_tbl);
|
||||
printf('%s\n', strip_set_elements(pcmd(sprintf('nft list table %s 2>/dev/null', mwan3_tbl))));
|
||||
printf('%s\n', anon_text(nft_mwan3_txt));
|
||||
|
||||
// --- fw4 mangle chains (relevant to mwan3 mark interactions) ---
|
||||
printf('=== fw4 mangle_prerouting chain ===\n');
|
||||
printf('%s\n', pcmd('nft list chain inet fw4 mangle_prerouting 2>/dev/null'));
|
||||
printf('%s\n', anon_text(fw4_mangle_pre_txt));
|
||||
|
||||
printf('=== fw4 mangle_output chain ===\n');
|
||||
printf('%s\n', pcmd('nft list chain inet fw4 mangle_output 2>/dev/null'));
|
||||
printf('%s\n', anon_text(fw4_mangle_out_txt));
|
||||
|
||||
printf('=== fw4 output chain ===\n');
|
||||
printf('%s\n', pcmd('nft list chain inet fw4 output 2>/dev/null'));
|
||||
printf('%s\n', anon_text(fw4_output_txt));
|
||||
|
||||
// --- recent mwan3 log ---
|
||||
printf('=== Recent mwan3 Log (last 200 lines) ===\n');
|
||||
printf('%s\n', pcmd('logread 2>/dev/null | grep -E "mwan3track\\[|mwan3-hotplug\\[|mwan3-init\\[|mwan3\\[" | tail -200'));
|
||||
printf('%s\n', anon_text(log_txt));
|
||||
|
||||
printf('=== End of Report ===\n');
|
||||
@@ -5,7 +5,8 @@ import * as rtnl from "rtnl";
|
||||
import * as uloop from "uloop";
|
||||
import * as uci from "uci";
|
||||
import * as ubus from "ubus";
|
||||
import { open, readfile, popen, stat } from "fs";
|
||||
import { open, readfile, popen, stat, mkdir, rename, unlink } from "fs";
|
||||
import { log_open, log_verbose, log_msg as mlog, ucibool, is_default_equivalent } from 'mwan3.common';
|
||||
|
||||
const RTM_NEWROUTE = rtnl.const.RTM_NEWROUTE;
|
||||
const RTM_DELROUTE = rtnl.const.RTM_DELROUTE;
|
||||
@@ -32,6 +33,7 @@ let iface_list = []; // [ { name, family, tid, enabled, has_tracking } ]
|
||||
let active_chains = {}; // { chain_name: true } cached from nft
|
||||
let connected_timer = null; // debounce timer for connected set rebuilds
|
||||
let last_connected_fingerprint = ""; // sorted element list from last populate call
|
||||
let route_state_timer = null; // debounce timer for ROUTE_STATUS refresh
|
||||
let ubus_conn;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -40,7 +42,7 @@ let ubus_conn;
|
||||
|
||||
function log_msg(level, msg) {
|
||||
|
||||
warn(`mwan3rtmon[${family_name}] ${level}: ${msg}\n`);
|
||||
mlog(level, `[${family_name}] ${msg}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -52,6 +54,10 @@ function load_config() {
|
||||
let cur = uci.cursor();
|
||||
cur.load("mwan3");
|
||||
|
||||
// Verbose logging gate
|
||||
|
||||
log_verbose(ucibool(cur.get("mwan3", "globals", "verbose_logging") ?? "0"));
|
||||
|
||||
// MMX mask
|
||||
|
||||
mmx_mask = cur.get("mwan3", "globals", "mmx_mask") ?? "0x3F00";
|
||||
@@ -219,9 +225,17 @@ function nft_exec(cmd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run a list of nft commands as a single nft -f transaction. The batch
|
||||
// file is per-family: one mwan3rtmon instance runs per address family
|
||||
// and each instance is single-threaded, so the family suffix alone
|
||||
// keeps the two instances' batch files apart without locking. The file
|
||||
// is removed on every exit path after creation. nft runs through a
|
||||
// read-mode popen with stderr folded into stdout so the failure log
|
||||
// carries nft's own error text.
|
||||
|
||||
function nft_batch(cmds) {
|
||||
|
||||
let tmpfile = "/var/run/mwan3/mwan3rtmon_nft_batch";
|
||||
let tmpfile = "/var/run/mwan3/mwan3rtmon_nft_batch." + family_name;
|
||||
let f = open(tmpfile, "w", 0600);
|
||||
if (!f) {
|
||||
log_msg("warn", "cannot open nft batch file");
|
||||
@@ -232,10 +246,13 @@ function nft_batch(cmds) {
|
||||
f.close();
|
||||
|
||||
let p = popen("nft -f " + tmpfile + " 2>&1");
|
||||
if (!p)
|
||||
if (!p) {
|
||||
unlink(tmpfile);
|
||||
return false;
|
||||
}
|
||||
let out = p.read("all");
|
||||
let rc = p.close();
|
||||
unlink(tmpfile);
|
||||
if (rc != 0) {
|
||||
log_msg("warn", "nft batch failed: " + (out ?? ""));
|
||||
return false;
|
||||
@@ -323,6 +340,126 @@ function is_linklocal_route(route) {
|
||||
match(route.dst, /^169\.254\./) != null));
|
||||
}
|
||||
|
||||
// Does this interface's own routing table hold a usable default route?
|
||||
//
|
||||
// This is the datapath truth: the interface's fwmark ip rule looks up
|
||||
// exactly this table, so an interface whose table N holds no default route
|
||||
// black-holes the traffic mwan3 marks to it, even when the main table still
|
||||
// has a default via the same device. This mirrors the predicate the rpcd
|
||||
// backend uses for has_default and the one mwan3-manage-rules.uc check-route
|
||||
// uses. 'routes' is a single full route dump for this family (all tables).
|
||||
|
||||
function table_has_default(routes, tid, dev) {
|
||||
|
||||
for (let r in routes)
|
||||
if (r.table == tid && is_default_route(r) && r.oif == dev)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// The interfaces this rtmon instance is responsible for: enabled, tracked,
|
||||
// of this family, and currently bound to an l3 device. iface_list is built
|
||||
// by load_config() and already carries name, family, tid, enabled, and
|
||||
// has_tracking. The device comes from the same ubus network.interface dump
|
||||
// build_dev_table_map() uses. An interface with no l3_device yet is skipped;
|
||||
// it gets evaluated later when its device appears and routes change.
|
||||
|
||||
function tracked_interfaces() {
|
||||
|
||||
let ifaces = [];
|
||||
if (!ubus_conn)
|
||||
return ifaces;
|
||||
let dump = ubus_conn.call("network.interface", "dump");
|
||||
if (!dump || !dump.interface)
|
||||
return ifaces;
|
||||
|
||||
let dev_by_name = {};
|
||||
for (let intf in dump.interface)
|
||||
if (intf.l3_device)
|
||||
dev_by_name[intf.interface] = intf.l3_device;
|
||||
|
||||
for (let iface in iface_list) {
|
||||
if (iface.family != family_name || !iface.enabled || !iface.has_tracking)
|
||||
continue;
|
||||
let dev = dev_by_name[iface.name];
|
||||
if (dev == null)
|
||||
continue;
|
||||
push(ifaces, { name: iface.name, tid: iface.tid, dev: dev });
|
||||
}
|
||||
return ifaces;
|
||||
}
|
||||
|
||||
// Write the interface's ROUTE_STATUS file, only when the value changes, and
|
||||
// log the transition. The write is atomic (temp file + rename within the
|
||||
// same tmpfs directory) so the tracker never reads a torn value. The
|
||||
// tracker creates these directories, but rtmon may evaluate an interface
|
||||
// before its tracker has, so mkdir best-effort first; it fails harmlessly
|
||||
// if the directory already exists.
|
||||
|
||||
function write_route_state(ifname, state) {
|
||||
|
||||
let path = MWAN3TRACK_STATUS_DIR + "/" + ifname + "/ROUTE_STATUS";
|
||||
let cur = trim(readfile(path) ?? "");
|
||||
if (cur == state)
|
||||
return;
|
||||
|
||||
mkdir(MWAN3TRACK_STATUS_DIR, 0700);
|
||||
mkdir(MWAN3TRACK_STATUS_DIR + "/" + ifname, 0755);
|
||||
|
||||
let tmp = path + ".tmp";
|
||||
let f = open(tmp, "w", 0644);
|
||||
if (!f) {
|
||||
log_msg("warn", "cannot open ROUTE_STATUS for " + ifname);
|
||||
return;
|
||||
}
|
||||
f.write(state + "\n");
|
||||
f.close();
|
||||
|
||||
if (!rename(tmp, path)) {
|
||||
unlink(tmp);
|
||||
log_msg("warn", "cannot commit ROUTE_STATUS for " + ifname);
|
||||
return;
|
||||
}
|
||||
|
||||
// A missing default route is abnormal, not routine failover, so log the
|
||||
// loss at warn and the return to normal at notice.
|
||||
|
||||
log_msg(state == "down" ? "warn" : "notice", ifname + " default route " + state);
|
||||
}
|
||||
|
||||
// Recompute and persist every responsible interface's route state from one
|
||||
// route dump. The dump spans all tables for this family; table_has_default
|
||||
// filters to each interface's own table. Called at startup, on SIGHUP, and
|
||||
// debounced from the route-event handler.
|
||||
|
||||
function refresh_route_states() {
|
||||
|
||||
let ifaces = tracked_interfaces();
|
||||
if (length(ifaces) == 0)
|
||||
return;
|
||||
|
||||
let routes = rtnl.request(RTM_GETROUTE, NLM_F_DUMP, { family: family_num }) ?? [];
|
||||
|
||||
for (let iface in ifaces)
|
||||
write_route_state(iface.name,
|
||||
table_has_default(routes, iface.tid, iface.dev) ? "up" : "down");
|
||||
}
|
||||
|
||||
// Arm the debounced refresh. Bursts of route events (PD renewal, reconnects,
|
||||
// the boot-time route flood) collapse into a single sweep, exactly as the
|
||||
// connected-set rebuild is debounced.
|
||||
|
||||
function arm_route_state_refresh() {
|
||||
|
||||
if (!route_state_timer)
|
||||
route_state_timer = uloop.timer(DEBOUNCE_MS, () => {
|
||||
refresh_route_states();
|
||||
route_state_timer = null;
|
||||
});
|
||||
else
|
||||
route_state_timer.set(DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// Get all routes from main table (and extra lookup tables) via netlink
|
||||
|
||||
function get_all_routes() {
|
||||
@@ -370,8 +507,18 @@ function populate_connected_set() {
|
||||
|
||||
let seen = {};
|
||||
let elements = [];
|
||||
|
||||
// Main-table routes only. get_all_routes() also returns the extra lookup
|
||||
// tables for the per-interface mirroring, but their routes belong to
|
||||
// mwan3_custom_v4/v6, which grants the identical bypass and is maintained
|
||||
// on those tables' own route events. Sweeping them in here as well would
|
||||
// strand them: extra-table events never arm this rebuild, so a route
|
||||
// deleted from its table would stay in the connected set indefinitely.
|
||||
|
||||
for (let route in routes) {
|
||||
if (is_cidr_route(route) && !is_default_route(route) && !is_linklocal_route(route) && !seen[route.dst]) {
|
||||
if (route.table != RT_TABLE_MAIN)
|
||||
continue;
|
||||
if (is_cidr_route(route) && !is_default_equivalent(route) && !is_linklocal_route(route) && !seen[route.dst]) {
|
||||
push(elements, route.dst);
|
||||
seen[route.dst] = true;
|
||||
}
|
||||
@@ -456,7 +603,7 @@ function repopulate_custom_sets() {
|
||||
if (!extra_table_set[r.table])
|
||||
continue;
|
||||
let dst = r.dst;
|
||||
if (dst == null || is_default_route(r) || is_linklocal_route(r))
|
||||
if (dst == null || is_default_equivalent(r) || is_linklocal_route(r))
|
||||
continue;
|
||||
if (!seen[dst]) {
|
||||
push(cmds, "add element inet mwan3 " + setname + " { " + dst + " }");
|
||||
@@ -478,7 +625,7 @@ function repopulate_custom_sets() {
|
||||
function handle_custom_set_event(route, is_new) {
|
||||
|
||||
let dst = route.dst;
|
||||
if (dst == null || is_default_route(route) || is_linklocal_route(route))
|
||||
if (dst == null || is_default_equivalent(route) || is_linklocal_route(route))
|
||||
return;
|
||||
|
||||
let setname = (family_num == AF_INET) ? "mwan3_custom_v4" : "mwan3_custom_v6";
|
||||
@@ -536,6 +683,10 @@ function handle_route_event(event) {
|
||||
connected_timer.set(DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// Re-evaluate every interface's default-route state on any routing change.
|
||||
|
||||
arm_route_state_refresh();
|
||||
|
||||
// -- Update per-interface routing tables --
|
||||
|
||||
build_dev_table_map();
|
||||
@@ -594,6 +745,7 @@ function main() {
|
||||
// Parse args
|
||||
|
||||
family_name = ARGV[0] ?? "ipv4";
|
||||
log_open("mwan3rtmon");
|
||||
|
||||
if (family_name == "ipv6") {
|
||||
if (stat("/proc/sys/net/ipv6") == null) {
|
||||
@@ -610,6 +762,7 @@ function main() {
|
||||
}
|
||||
|
||||
load_config();
|
||||
log_msg("notice", "started");
|
||||
|
||||
// Connect to ubus
|
||||
|
||||
@@ -653,6 +806,9 @@ function main() {
|
||||
log_msg("debug", "populating initial per-interface routes");
|
||||
populate_iface_routes();
|
||||
|
||||
log_msg("debug", "initial route-state sweep");
|
||||
refresh_route_states();
|
||||
|
||||
// Signal handlers for clean shutdown
|
||||
|
||||
uloop.signal("SIGTERM", () => {
|
||||
@@ -669,8 +825,23 @@ function main() {
|
||||
log_msg("debug", "received SIGHUP, reloading config");
|
||||
load_config();
|
||||
repopulate_custom_sets();
|
||||
refresh_route_states();
|
||||
});
|
||||
|
||||
// Slow periodic re-sweep as defense in depth: bounds recovery if a
|
||||
// route-state change ever arrives without an accompanying main-table
|
||||
// route event (a coalesced or missed netlink multicast). It is cheap
|
||||
// and silent when nothing moves, since write_route_state only writes
|
||||
// on an actual change.
|
||||
|
||||
function schedule_periodic_route_sweep() {
|
||||
uloop.timer(20000, () => {
|
||||
refresh_route_states();
|
||||
schedule_periodic_route_sweep();
|
||||
});
|
||||
}
|
||||
schedule_periodic_route_sweep();
|
||||
|
||||
log_msg("debug", "entering event loop");
|
||||
uloop.run();
|
||||
|
||||
|
||||
@@ -299,7 +299,7 @@ main() {
|
||||
trap config_reload HUP
|
||||
|
||||
load_tracking_config
|
||||
local sleep_time result ping_status loss latency
|
||||
local sleep_time result ping_status loss latency route_state route_state_prev probe_ips
|
||||
mwan3_list_track_ips()
|
||||
{
|
||||
track_ips="$track_ips $1"
|
||||
@@ -352,6 +352,7 @@ main() {
|
||||
local lost=0
|
||||
local turn=0
|
||||
local failed_hosts=""
|
||||
route_state_prev=""
|
||||
|
||||
firstconnect
|
||||
while true; do
|
||||
@@ -391,7 +392,26 @@ main() {
|
||||
continue
|
||||
fi
|
||||
sleep_time=$interval
|
||||
for track_ip in $track_ips; do
|
||||
|
||||
# An interface can be reachable to its tracking target yet hold no
|
||||
# usable default route in its own table, in which case mwan3 marks
|
||||
# traffic to it and the per-interface backstop drops it. mwan3rtmon
|
||||
# writes the interface's default-route state to ROUTE_STATUS; treat
|
||||
# "down" as a failed round and skip probing so the existing score
|
||||
# machinery carries the interface offline, and back online when the
|
||||
# route returns. An absent file means no route monitor has evaluated
|
||||
# this interface, so probe exactly as before.
|
||||
|
||||
route_state=$(cat "$MWAN3TRACK_STATUS_DIR/$INTERFACE/ROUTE_STATUS" 2>/dev/null)
|
||||
probe_ips=$track_ips
|
||||
if [ "$route_state" = "down" ]; then
|
||||
probe_ips=""
|
||||
[ "$route_state_prev" != "down" ] && LOG warn "Interface $INTERFACE ($DEVICE) has no usable default route in its table; treating tracking round as failed"
|
||||
elif [ "$route_state_prev" = "down" ]; then
|
||||
LOG notice "Interface $INTERFACE ($DEVICE) default route restored"
|
||||
fi
|
||||
route_state_prev=$route_state
|
||||
for track_ip in $probe_ips; do
|
||||
if [ $host_up_count -lt $reliability ]; then
|
||||
case "$track_method" in
|
||||
ping)
|
||||
|
||||
@@ -75,6 +75,16 @@ function parse_elem_val(v) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allowlist for nft set elements written back to the kernel: IPv4 and IPv6
|
||||
// addresses, CIDR prefixes and hyphen-written ranges only, which is exactly
|
||||
// what the apply path emits. These values are interpolated into an nft add
|
||||
// element command line built with popen, so anything carrying a character
|
||||
// outside this class is dropped before it can break out of the element list.
|
||||
|
||||
function valid_nft_element(v) {
|
||||
return match(v, /^[0-9A-Fa-f:.\/-]+$/) != null;
|
||||
}
|
||||
|
||||
function count_nftset_elements(setname) {
|
||||
if (!match(setname, /^[a-zA-Z0-9_-]+$/))
|
||||
return 0;
|
||||
@@ -216,6 +226,14 @@ function get_table_routes(table_id) {
|
||||
}
|
||||
|
||||
function nslookup_resolve(host, type) {
|
||||
|
||||
// Reject any host outside the hostname character class before it is
|
||||
// interpolated into the /bin/sh -c command line below. This is the single
|
||||
// chokepoint for both resolve_host and the nftset_resolve domain path,
|
||||
// whose UCI-supplied domain values are otherwise unvalidated.
|
||||
|
||||
if (!match(host, /^[a-zA-Z0-9._-]+$/))
|
||||
return [];
|
||||
const p = popen(sprintf('/bin/busybox nslookup -type=%s %s 127.0.0.1 2>/dev/null', type, host), 'r');
|
||||
if (!p) return [];
|
||||
const raw = p.read('all');
|
||||
@@ -233,7 +251,6 @@ function nslookup_resolve(host, type) {
|
||||
|
||||
function routing_health() {
|
||||
const uci = cursor();
|
||||
|
||||
const mmx_mask = int(uci.get('mwan3', 'globals', 'mmx_mask')) || 0x3F00;
|
||||
let bitcnt = 0;
|
||||
for (let m = mmx_mask; m; m >>= 1) bitcnt += m & 1;
|
||||
@@ -250,34 +267,47 @@ function routing_health() {
|
||||
UNREACH_BASE = 3000;
|
||||
}
|
||||
|
||||
const SRC_BASE = UNREACH_BASE + iface_max + 1;
|
||||
|
||||
// Build ordered interface list with 1-based index (same ordering mwan3 uses)
|
||||
|
||||
const iface_order = [];
|
||||
const iface_index = {};
|
||||
const iface_family = {};
|
||||
uci.foreach('mwan3', 'interface', intf => {
|
||||
push(iface_order, intf['.name']);
|
||||
iface_index[intf['.name']] = length(iface_order);
|
||||
iface_family[intf['.name']] = intf['family'] || 'ipv4';
|
||||
});
|
||||
|
||||
// Fetch all ip rules and index by priority for O(1) lookup
|
||||
|
||||
const all_rules = get_ip_rules();
|
||||
const rule_by_priority = {};
|
||||
const v4_rule_by_priority = {};
|
||||
for (let rule in all_rules) {
|
||||
if (rule?.priority != null)
|
||||
if (rule?.priority != null) {
|
||||
rule_by_priority[rule.priority] = rule;
|
||||
if (rule.family == 2)
|
||||
v4_rule_by_priority[rule.priority] = rule;
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-build the set of priorities that correspond to current UCI interfaces
|
||||
|
||||
const valid_priorities = {};
|
||||
for (let ifname in iface_order) {
|
||||
const id = iface_index[ifname];
|
||||
valid_priorities[IIF_BASE + id] = true;
|
||||
valid_priorities[FWMARK_BASE + id] = true;
|
||||
valid_priorities[UNREACH_BASE + id] = true;
|
||||
valid_priorities[UNREACH_BASE + iface_max + 1 + id] = true;
|
||||
}
|
||||
valid_priorities[FWMARK_BASE + mmdefault - 2] = true;
|
||||
valid_priorities[FWMARK_BASE + mmdefault - 1] = true;
|
||||
|
||||
// Per-interface health report
|
||||
|
||||
const result = {};
|
||||
for (let ifname in iface_order) {
|
||||
const id = iface_index[ifname];
|
||||
@@ -319,17 +349,38 @@ function routing_health() {
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
// The source band is IPv4-only, so test presence against the IPv4
|
||||
// rule index alone. A device holds one source rule per global IPv4
|
||||
// address, all sharing this priority, so a populated slot means the
|
||||
// band exists for this interface, not that every address is covered.
|
||||
|
||||
if (iface_family[ifname] == 'ipv4') {
|
||||
result[ifname].src_rule = {
|
||||
present: v4_rule_by_priority[SRC_BASE + id] != null,
|
||||
priority: SRC_BASE + id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Stale rules: priorities in mwan3 ranges but not matching any current UCI interface
|
||||
|
||||
const stale = [];
|
||||
for (let rule in all_rules) {
|
||||
const p = rule?.priority;
|
||||
if (p == null) continue;
|
||||
|
||||
// The iif and fwmark windows are mmdefault wide (the fwmark band
|
||||
// carries the global blackhole and unreachable rules above
|
||||
// iface_max). The unreachable band holds only ids 1..iface_max;
|
||||
// the slot above it is unused and the source band follows at
|
||||
// iface_max + 1 + id.
|
||||
|
||||
const in_iif_range = (p > IIF_BASE && p <= IIF_BASE + mmdefault);
|
||||
const in_fwmark_range = (p > FWMARK_BASE && p <= FWMARK_BASE + mmdefault);
|
||||
const in_unreach_range = (p > UNREACH_BASE && p <= UNREACH_BASE + mmdefault);
|
||||
if ((in_iif_range || in_fwmark_range || in_unreach_range) && !valid_priorities[p])
|
||||
const in_unreach_range = (p > UNREACH_BASE && p <= UNREACH_BASE + iface_max);
|
||||
const in_src_range = (p > UNREACH_BASE + iface_max + 1 && p <= UNREACH_BASE + 2 * iface_max + 1);
|
||||
if ((in_iif_range || in_fwmark_range || in_unreach_range || in_src_range) && !valid_priorities[p])
|
||||
push(stale, { priority: p });
|
||||
}
|
||||
|
||||
@@ -339,7 +390,7 @@ function routing_health() {
|
||||
interfaces: result,
|
||||
stale_rules: stale,
|
||||
mwan3_active: mwan3_active,
|
||||
rule_bases: { iif: IIF_BASE, fwmark: FWMARK_BASE, unreach: UNREACH_BASE },
|
||||
rule_bases: { iif: IIF_BASE, fwmark: FWMARK_BASE, unreach: UNREACH_BASE, src: SRC_BASE },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -391,12 +442,14 @@ function get_policies(version) {
|
||||
const family_filter = (version == '6') ? 'ipv6' : 'ipv4';
|
||||
|
||||
// Build interface family map: iface_name -> 'ipv4'/'ipv6'
|
||||
|
||||
const iface_family = {};
|
||||
uci.foreach('mwan3', 'interface', intf => {
|
||||
iface_family[intf['.name']] = intf['family'] || 'ipv4';
|
||||
});
|
||||
|
||||
// Build member info map: member_name -> { interface, metric, weight }
|
||||
|
||||
const member_info = {};
|
||||
uci.foreach('mwan3', 'member', m => {
|
||||
member_info[m['.name']] = {
|
||||
@@ -415,6 +468,7 @@ function get_policies(version) {
|
||||
if (type(use_members) != 'array') use_members = [use_members];
|
||||
|
||||
// Collect members matching the requested address family
|
||||
|
||||
const by_metric = {};
|
||||
for (let mname in use_members) {
|
||||
const m = member_info[mname];
|
||||
@@ -431,6 +485,7 @@ function get_policies(version) {
|
||||
if (!length(keys(by_metric))) return;
|
||||
|
||||
// Find the lowest metric with at least one online member
|
||||
|
||||
const sorted_metrics = sort(keys(by_metric), (a, b) => int(a) - int(b));
|
||||
let active_metric = null;
|
||||
for (let metric in sorted_metrics) {
|
||||
@@ -444,6 +499,7 @@ function get_policies(version) {
|
||||
}
|
||||
|
||||
// Sum weights of online members at the active metric for percent calculation
|
||||
|
||||
let total_weight = 0;
|
||||
if (active_metric != null) {
|
||||
for (let m in by_metric[active_metric]) {
|
||||
@@ -498,10 +554,12 @@ function interfaces_status(request) {
|
||||
'packetloss': get_int(ifname, sprintf('LOSS_%s', ip)),
|
||||
});
|
||||
}
|
||||
|
||||
// Derive check_quality from LATENCY file content rather than UCI.
|
||||
// UCI may be stale if check_quality was changed without restarting mwan3track.
|
||||
// mwan3track only writes LATENCY_* files when check_quality=1, so file
|
||||
// presence with content is the authoritative indicator of actual state.
|
||||
|
||||
let check_quality = false;
|
||||
for (let tip in track_ips) {
|
||||
const lat = get_str_raw(ifname, sprintf('LATENCY_%s', tip.ip));
|
||||
@@ -694,7 +752,7 @@ const methods = {
|
||||
if (type(entries) != 'array') entries = [entries];
|
||||
for (let e in entries) {
|
||||
const ev = ltrim(rtrim(e, ' \t\r\n'), ' \t\r\n');
|
||||
if (length(ev) > 0) push(elements, ev);
|
||||
if (length(ev) > 0 && valid_nft_element(ev)) push(elements, ev);
|
||||
}
|
||||
if (cfg.loadfile) {
|
||||
const content = readfile(cfg.loadfile);
|
||||
@@ -703,7 +761,7 @@ const methods = {
|
||||
const ci = index(line, '#');
|
||||
if (ci >= 0) line = substr(line, 0, ci);
|
||||
line = ltrim(rtrim(line, ' \t\r\n'), ' \t\r\n');
|
||||
if (length(line) > 0) push(elements, line);
|
||||
if (length(line) > 0 && valid_nft_element(line)) push(elements, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
'use strict';
|
||||
|
||||
// common.uc: the shared mwan3 ucode library, the ucode analogue of common.sh.
|
||||
// Each shared ucode routine is defined here so it lives, and is tested, in one
|
||||
// place.
|
||||
|
||||
import { openlog, syslog, LOG_PID, LOG_DAEMON } from 'log';
|
||||
|
||||
// ---- Logging ---------------------------------------------------------------
|
||||
//
|
||||
// All mwan3 ucode logs through these functions so the sink, tag format and
|
||||
// verbose gate live in one place. Output goes to syslog, the same destination
|
||||
// logger(1) reaches, so it appears in logread regardless of how procd handles
|
||||
// the process stderr.
|
||||
|
||||
let verbose = false;
|
||||
|
||||
// Map the level names mwan3 uses (the shell's warn/error vocabulary and the
|
||||
// canonical syslog names) onto the exact tokens parse_priority() accepts. It
|
||||
// rejects "warn" and "error", so an unmapped name would be dropped; an unknown
|
||||
// level falls back to notice so a mislabelled message still logs.
|
||||
|
||||
const PRIO = {
|
||||
emerg: "emerg", alert: "alert", crit: "crit",
|
||||
err: "err", error: "err",
|
||||
warn: "warning", warning: "warning",
|
||||
notice: "notice", info: "info", debug: "debug",
|
||||
};
|
||||
|
||||
// Open the syslog connection with the given identity. LOG_PID appends the pid
|
||||
// so the tag reads ident[pid], matching the shell's "${SCRIPTNAME}[$$]"
|
||||
// convention. Facility daemon suits these long-running and service-invoked
|
||||
// components.
|
||||
|
||||
function log_open(ident) {
|
||||
openlog(ident, LOG_PID, LOG_DAEMON);
|
||||
}
|
||||
|
||||
// Set the verbose gate. The caller reads mwan3.globals.verbose_logging and
|
||||
// pushes the parsed bool here; debug is suppressed unless verbose, exactly as
|
||||
// common.sh:LOG() gates the debug facility behind MWAN3_VERBOSE_LOGGING.
|
||||
|
||||
function log_verbose(enable) {
|
||||
verbose = enable;
|
||||
}
|
||||
|
||||
// Emit one message. debug is dropped unless verbose; every other level always
|
||||
// logs. Message content is passed as a %s argument so it is never treated as a
|
||||
// format string.
|
||||
|
||||
function log_msg(level, msg) {
|
||||
if (level == "debug" && !verbose)
|
||||
return;
|
||||
syslog(PRIO[level] ?? "notice", "%s", msg);
|
||||
}
|
||||
|
||||
// ---- Config helpers --------------------------------------------------------
|
||||
|
||||
// Parse a UCI bool the way the shell's config_get_bool does: 1/on/true/yes/
|
||||
// enabled are true, everything else is false. Kept here so every ucode
|
||||
// consumer interprets a UCI bool identically to the shell. It reads no config
|
||||
// itself, so it does not couple the module to uci.
|
||||
|
||||
function ucibool(val) {
|
||||
switch (val) {
|
||||
case 'yes':
|
||||
case 'on':
|
||||
case 'true':
|
||||
case 'enabled':
|
||||
return true;
|
||||
default:
|
||||
return !!int(val);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Route classification --------------------------------------------------
|
||||
|
||||
// The width classifier for the bypass sweeps: a route is default-equivalent
|
||||
// when its destination is absent (a literal default) or its prefix is wide
|
||||
// enough that it can only exist as a component of an in-substance default
|
||||
// route - /2 or wider for IPv4, /3 or wider for IPv6, which covers the
|
||||
// split-half pairs VPN clients install and the 2000::/3 global-unicast
|
||||
// aggregate. Each route is judged alone, so a half-installed pair is still
|
||||
// excluded. The family comes from iptoarr, the inet_pton wrapper, so no
|
||||
// address text is hand-parsed; a destination it cannot parse is not
|
||||
// default-equivalent. The strict literal-default predicate the mirroring and
|
||||
// route-state paths use is deliberately not widened and lives on beside this.
|
||||
|
||||
function is_default_equivalent(route) {
|
||||
let dst = route.dst;
|
||||
if (dst == null)
|
||||
return true;
|
||||
let slash = index(dst, "/");
|
||||
if (slash < 0)
|
||||
return false;
|
||||
let a = iptoarr(substr(dst, 0, slash));
|
||||
if (a == null)
|
||||
return false;
|
||||
return +substr(dst, slash + 1) <= (length(a) == 16 ? 3 : 2);
|
||||
}
|
||||
|
||||
// ---- Exports ---------------------------------------------------------------
|
||||
|
||||
// The module's public interface. Declared as an export list rather than inline
|
||||
// on each function, which this ucode build does not accept.
|
||||
|
||||
export { log_open, log_verbose, log_msg, ucibool, is_default_equivalent };
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user