mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 02:11:19 +08:00
⛅ Sync 2026-07-22 03:00:29
This commit is contained in:
parent
5d9f64dd0e
commit
0eb3c6a042
@ -7,7 +7,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=filebrowser
|
||||
PKG_VERSION:=1.4.0-stable
|
||||
PKG_VERSION:=1.5.0-stable
|
||||
PKG_RELEASE=1
|
||||
|
||||
ifeq ($(ARCH),aarch64)
|
||||
|
||||
252
mwan3-nft/Makefile
Normal file
252
mwan3-nft/Makefile
Normal file
@ -0,0 +1,252 @@
|
||||
#
|
||||
# Copyright (C) 2006-2014 OpenWrt.org
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mwan3
|
||||
PKG_VERSION:=3.6.9
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=Florian Eckert <fe@dev.tdt.de>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
PKG_CONFIG_DEPENDS:=CONFIG_IPV6
|
||||
PKG_BUILD_DEPENDS:=libnetfilter_conntrack libmnl
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/mwan3
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Routing and Redirection
|
||||
DEPENDS:= \
|
||||
+ip-full \
|
||||
+libnetfilter-conntrack \
|
||||
+kmod-nft-core \
|
||||
+nftables-json \
|
||||
+rpcd-mod-ucode \
|
||||
+jshn \
|
||||
+ucode \
|
||||
+ucode-mod-rtnl \
|
||||
+ucode-mod-uloop \
|
||||
+ucode-mod-uci \
|
||||
+ucode-mod-ubus \
|
||||
+ucode-mod-fs \
|
||||
+ucode-mod-socket
|
||||
TITLE:=Multiwan hotplug script with connection tracking support (ucode rtmon)
|
||||
MAINTAINER:=Florian Eckert <fe@dev.tdt.de>
|
||||
endef
|
||||
|
||||
define Package/mwan3/description
|
||||
Hotplug script which makes configuration of multiple WAN interfaces simple
|
||||
and manageable. With loadbalancing/failover support for up to 250 wan
|
||||
interfaces, connection tracking and an easy to manage traffic ruleset.
|
||||
mwan3rtmon now uses a ucode implementation leveraging ucode-mod-rtnl
|
||||
for direct netlink access instead of forking ip commands.
|
||||
endef
|
||||
|
||||
define Package/mwan3/conffiles
|
||||
/etc/config/mwan3
|
||||
/etc/mwan3.user
|
||||
endef
|
||||
|
||||
define Package/mwan3/preinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
# Stop mwan3evtd if present (older installs included this daemon). The
|
||||
# pre-upgrade hook runs before APK removes files no longer in the
|
||||
# package, so this must happen here to avoid leaving a running process
|
||||
# with no init script or binary after the file swap.
|
||||
/etc/init.d/mwan3evtd stop 2>/dev/null
|
||||
/etc/init.d/mwan3evtd disable 2>/dev/null
|
||||
# Record whether this is a fresh install so postinst can auto-enable
|
||||
# without overriding an explicit disable by the user on upgrade.
|
||||
[ -f /etc/init.d/mwan3 ] || touch /tmp/mwan3_first_install
|
||||
# Stop mwan3 before APK replaces any files. This is critical for
|
||||
# upgrades: if mwan3 is running when the init script is replaced,
|
||||
# procd's inotify trigger restarts the service while the old ip rules
|
||||
# are still installed, producing RTNETLINK "File exists" errors when
|
||||
# start_service tries to re-add them. Stopping here puts the service
|
||||
# into procd's "stopped" state before the init script changes, so
|
||||
# procd does not auto-restart it.
|
||||
/etc/init.d/mwan3 stop 2>/dev/null
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/mwan3/postinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
# Safety-net stop in case preinst did not run or procd restarted the
|
||||
# service between preinst and postinst.
|
||||
/etc/init.d/mwan3 stop 2>/dev/null
|
||||
# mwan3_prerouting/mwan3_output run at priority mangle + 1, backed by
|
||||
# non-destructive vmap-dispatch save/restore so the placement is
|
||||
# order-independent w.r.t. pbr. Older installs used mangle - 1.
|
||||
# nftables rejects a base chain redeclaration at a different priority,
|
||||
# so flush+delete the old chains before start recreates them.
|
||||
for chain in mwan3_prerouting mwan3_output; do
|
||||
if nft list chain inet fw4 "$$chain" 2>/dev/null | grep -q "priority mangle - 1"; then
|
||||
nft flush chain inet fw4 "$$chain" 2>/dev/null
|
||||
nft delete chain inet fw4 "$$chain" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
# Drop legacy ip->mark sticky maps (replaced by per-(rule,family,member)
|
||||
# ip-only sets). Leftover legacy maps are unreferenced after upgrade
|
||||
# but waste a name and confuse status.
|
||||
for mapname in $$(nft list maps inet 2>/dev/null | \
|
||||
awk '$$1=="map" && $$2 ~ /^mwan3_sticky_v[46]_/ { print $$2 }'); do
|
||||
nft delete map inet fw4 "$$mapname" 2>/dev/null
|
||||
done
|
||||
# mwan3 now operates in table inet mwan3 rather than table inet fw4.
|
||||
# fw4 uses flush-table (not delete-table) on reload, so any mwan3
|
||||
# chains/sets left in fw4 survive fw4 reloads and must be explicitly
|
||||
# removed.
|
||||
for chain in mwan3_prerouting mwan3_output mwan3_postrouting \
|
||||
mwan3_ifaces_in mwan3_rules mwan3_connected mwan3_custom mwan3_dynamic; do
|
||||
nft flush chain inet fw4 "$$chain" 2>/dev/null
|
||||
nft delete chain inet fw4 "$$chain" 2>/dev/null
|
||||
done
|
||||
for setname in mwan3_connected_v4 mwan3_connected_v6 \
|
||||
mwan3_custom_v4 mwan3_custom_v6 \
|
||||
mwan3_dynamic_v4 mwan3_dynamic_v6; do
|
||||
nft delete set inet fw4 "$$setname" 2>/dev/null
|
||||
done
|
||||
for mapname in $$(nft list maps inet fw4 2>/dev/null | \
|
||||
awk '$$1=="map" && $$2 ~ /^mwan3_sticky_/ { print $$2 }'); do
|
||||
nft flush map inet fw4 "$$mapname" 2>/dev/null
|
||||
nft delete map inet fw4 "$$mapname" 2>/dev/null
|
||||
done
|
||||
# Remove stale firewall.mwan3_reload UCI section if present and reload
|
||||
# fw4 only if it was. An unconditional fw4 -q reload would trigger
|
||||
# queued ifup hotplug events that race with mwan3_create_iface_rules
|
||||
# in the init hotplug, producing RTNETLINK "File exists" errors.
|
||||
if uci -q delete firewall.mwan3_reload; then
|
||||
uci commit firewall
|
||||
fw4 -q reload
|
||||
fi
|
||||
# Migrate any fw4-side set references in mwan3 rules to config ipset
|
||||
# declarations in /etc/config/mwan3 (one-shot, idempotent).
|
||||
/lib/mwan3/mwan3-migrate-ipset-v4.sh
|
||||
rm -f /lib/mwan3/mwan3-migrate-ipset-v4.sh
|
||||
/etc/init.d/rpcd restart
|
||||
# Second stop immediately before start: procd may have auto-started mwan3
|
||||
# when the init script was installed (the first stop above runs before that
|
||||
# happens and is therefore a no-op). By this point the auto-start has
|
||||
# completed, so this stop calls stop_service and removes its ip rules.
|
||||
# Without this, mwan3 start below calls procd_close_service (which tells
|
||||
# procd to replace the service instances) but never calls stop_service,
|
||||
# so the ip rules from the auto-start are still present when the init
|
||||
# hotplug events try to add them again.
|
||||
# Auto-enable on fresh install so mwan3 starts at boot without requiring
|
||||
# a manual /etc/init.d/mwan3 enable. On upgrade the flag is absent, so
|
||||
# an explicit disable by the user is preserved.
|
||||
if [ -f /tmp/mwan3_first_install ]; then
|
||||
rm -f /tmp/mwan3_first_install
|
||||
/etc/init.d/mwan3 enable
|
||||
fi
|
||||
/etc/init.d/mwan3 stop 2>/dev/null
|
||||
/etc/init.d/mwan3 start
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/mwan3/postrm
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
/etc/init.d/rpcd restart
|
||||
for f in /tmp/dnsmasq.*.d/mwan3-nftsets.conf /tmp/dnsmasq.d/mwan3-nftsets.conf; do
|
||||
[ -f "$$f" ] && rm -f "$$f"
|
||||
done
|
||||
/etc/init.d/dnsmasq restart 2>/dev/null
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
$(TARGET_CC) $(CFLAGS) $(LDFLAGS) $(FPIC) \
|
||||
-shared \
|
||||
-o $(PKG_BUILD_DIR)/libwrap_mwan3_sockopt.so.1.0 \
|
||||
$(if $(CONFIG_IPV6),-DCONFIG_IPV6) \
|
||||
$(PKG_BUILD_DIR)/sockopt_wrap.c \
|
||||
-ldl
|
||||
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \
|
||||
-o $(PKG_BUILD_DIR)/mwan3ct \
|
||||
$(PKG_BUILD_DIR)/mwan3ct.c \
|
||||
-lnetfilter_conntrack -lmnl -lnfnetlink
|
||||
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \
|
||||
-o $(PKG_BUILD_DIR)/mwan3ipcheck \
|
||||
$(PKG_BUILD_DIR)/mwan3ipcheck.c
|
||||
endef
|
||||
|
||||
define Package/mwan3/install
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/etc/config/mwan3 \
|
||||
$(1)/etc/config/
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/hotplug.d/iface
|
||||
$(INSTALL_DATA) ./files/etc/hotplug.d/iface/25-mwan3 \
|
||||
$(1)/etc/hotplug.d/iface/
|
||||
$(INSTALL_DATA) ./files/etc/hotplug.d/iface/26-mwan3-user \
|
||||
$(1)/etc/hotplug.d/iface/
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/etc/init.d/mwan3 \
|
||||
$(1)/etc/init.d/
|
||||
|
||||
$(INSTALL_DIR) $(1)/lib/mwan3
|
||||
$(INSTALL_DATA) ./files/lib/mwan3/common.sh \
|
||||
$(1)/lib/mwan3/
|
||||
$(INSTALL_DATA) ./files/lib/mwan3/mwan3.sh \
|
||||
$(1)/lib/mwan3/
|
||||
$(INSTALL_DATA) ./files/lib/mwan3/mwan3-skeleton.nft \
|
||||
$(1)/lib/mwan3/
|
||||
$(INSTALL_BIN) ./files/lib/mwan3/mwan3-migrate-ipset-v4.sh \
|
||||
$(1)/lib/mwan3/
|
||||
$(INSTALL_BIN) ./files/lib/mwan3/mwan3-get-addr.uc \
|
||||
$(1)/lib/mwan3/
|
||||
$(INSTALL_BIN) ./files/lib/mwan3/mwan3-manage-rules.uc \
|
||||
$(1)/lib/mwan3/
|
||||
$(INSTALL_BIN) ./files/lib/mwan3/mwan3-list-routes.uc \
|
||||
$(1)/lib/mwan3/
|
||||
$(INSTALL_BIN) ./files/lib/mwan3/mwan3-create-iface-route.uc \
|
||||
$(1)/lib/mwan3/
|
||||
|
||||
$(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 \
|
||||
$(1)/usr/sbin/
|
||||
$(INSTALL_BIN) ./files/usr/sbin/mwan3rtmon \
|
||||
$(1)/usr/sbin/
|
||||
$(INSTALL_BIN) ./files/usr/sbin/mwan3track \
|
||||
$(1)/usr/sbin/
|
||||
$(INSTALL_BIN) ./files/usr/sbin/mwan3-lb-test \
|
||||
$(1)/usr/sbin/
|
||||
$(INSTALL_BIN) ./files/usr/sbin/mwan3-diag \
|
||||
$(1)/usr/sbin/
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc
|
||||
$(INSTALL_BIN) ./files/etc/mwan3.user \
|
||||
$(1)/etc/
|
||||
|
||||
$(CP) $(PKG_BUILD_DIR)/libwrap_mwan3_sockopt.so.1.0 $(1)/lib/mwan3/
|
||||
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/mwan3ct $(1)/usr/sbin/
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/mwan3ipcheck $(1)/usr/bin/
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_DATA) ./files/etc/uci-defaults/mwan3-migrate-flush_conntrack \
|
||||
$(1)/etc/uci-defaults/
|
||||
$(INSTALL_DATA) ./files/etc/uci-defaults/mwan3-remove-firewall-include \
|
||||
$(1)/etc/uci-defaults/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,mwan3))
|
||||
4098
mwan3-nft/README.md
Normal file
4098
mwan3-nft/README.md
Normal file
File diff suppressed because it is too large
Load Diff
139
mwan3-nft/files/etc/config/mwan3
Normal file
139
mwan3-nft/files/etc/config/mwan3
Normal file
@ -0,0 +1,139 @@
|
||||
# For full documentation of mwan3 configuration:
|
||||
# https://openwrt.org/docs/guide-user/network/wan/multiwan/mwan3#mwan3_configuration
|
||||
|
||||
config globals 'globals'
|
||||
option mmx_mask '0x3F00'
|
||||
# option iif_rule_base '1000'
|
||||
# option fwmark_rule_base '2000'
|
||||
# option unreachable_rule_base '3000'
|
||||
|
||||
config interface 'wan'
|
||||
option enabled '0'
|
||||
list track_ip '1.0.0.1'
|
||||
list track_ip '1.1.1.1'
|
||||
list track_ip '208.67.222.222'
|
||||
list track_ip '208.67.220.220'
|
||||
option family 'ipv4'
|
||||
option reliability '2'
|
||||
|
||||
config interface 'wan6'
|
||||
option enabled '0'
|
||||
list track_ip '2606:4700:4700::1001'
|
||||
list track_ip '2606:4700:4700::1111'
|
||||
list track_ip '2620:0:ccd::2'
|
||||
list track_ip '2620:0:ccc::2'
|
||||
option family 'ipv6'
|
||||
option reliability '2'
|
||||
|
||||
config interface 'wanb'
|
||||
option enabled '0'
|
||||
list track_ip '1.0.0.1'
|
||||
list track_ip '1.1.1.1'
|
||||
list track_ip '208.67.222.222'
|
||||
list track_ip '208.67.220.220'
|
||||
option family 'ipv4'
|
||||
option reliability '1'
|
||||
|
||||
config interface 'wanb6'
|
||||
option enabled '0'
|
||||
list track_ip '2606:4700:4700::1001'
|
||||
list track_ip '2606:4700:4700::1111'
|
||||
list track_ip '2620:0:ccd::2'
|
||||
list track_ip '2620:0:ccc::2'
|
||||
option family 'ipv6'
|
||||
option reliability '1'
|
||||
|
||||
config member 'wan_m1_w3'
|
||||
option interface 'wan'
|
||||
option metric '1'
|
||||
option weight '3'
|
||||
|
||||
config member 'wan_m2_w3'
|
||||
option interface 'wan'
|
||||
option metric '2'
|
||||
option weight '3'
|
||||
|
||||
config member 'wanb_m1_w2'
|
||||
option interface 'wanb'
|
||||
option metric '1'
|
||||
option weight '2'
|
||||
|
||||
config member 'wanb_m1_w3'
|
||||
option interface 'wanb'
|
||||
option metric '1'
|
||||
option weight '3'
|
||||
|
||||
config member 'wanb_m2_w2'
|
||||
option interface 'wanb'
|
||||
option metric '2'
|
||||
option weight '2'
|
||||
|
||||
config member 'wan6_m1_w3'
|
||||
option interface 'wan6'
|
||||
option metric '1'
|
||||
option weight '3'
|
||||
|
||||
config member 'wan6_m2_w3'
|
||||
option interface 'wan6'
|
||||
option metric '2'
|
||||
option weight '3'
|
||||
|
||||
config member 'wanb6_m1_w2'
|
||||
option interface 'wanb6'
|
||||
option metric '1'
|
||||
option weight '2'
|
||||
|
||||
config member 'wanb6_m1_w3'
|
||||
option interface 'wanb6'
|
||||
option metric '1'
|
||||
option weight '3'
|
||||
|
||||
config member 'wanb6_m2_w2'
|
||||
option interface 'wanb6'
|
||||
option metric '2'
|
||||
option weight '2'
|
||||
|
||||
config policy 'wan_only'
|
||||
list use_member 'wan_m1_w3'
|
||||
list use_member 'wan6_m1_w3'
|
||||
|
||||
config policy 'wanb_only'
|
||||
list use_member 'wanb_m1_w2'
|
||||
list use_member 'wanb6_m1_w2'
|
||||
|
||||
config policy 'balanced'
|
||||
list use_member 'wan_m1_w3'
|
||||
list use_member 'wanb_m1_w3'
|
||||
list use_member 'wan6_m1_w3'
|
||||
list use_member 'wanb6_m1_w3'
|
||||
|
||||
config policy 'wan_wanb'
|
||||
list use_member 'wan_m1_w3'
|
||||
list use_member 'wanb_m2_w2'
|
||||
list use_member 'wan6_m1_w3'
|
||||
list use_member 'wanb6_m2_w2'
|
||||
|
||||
config policy 'wanb_wan'
|
||||
list use_member 'wan_m2_w3'
|
||||
list use_member 'wanb_m1_w2'
|
||||
list use_member 'wan6_m2_w3'
|
||||
list use_member 'wanb6_m1_w2'
|
||||
|
||||
config rule 'https'
|
||||
option enabled '0'
|
||||
option sticky '1'
|
||||
option dest_port '443'
|
||||
option proto 'tcp'
|
||||
option use_policy 'balanced'
|
||||
|
||||
config rule 'default_rule_v4'
|
||||
option enabled '0'
|
||||
option dest_ip '0.0.0.0/0'
|
||||
option use_policy 'balanced'
|
||||
option family 'ipv4'
|
||||
|
||||
config rule 'default_rule_v6'
|
||||
option enabled '0'
|
||||
option dest_ip '::/0'
|
||||
option use_policy 'balanced'
|
||||
option family 'ipv6'
|
||||
92
mwan3-nft/files/etc/hotplug.d/iface/25-mwan3
Normal file
92
mwan3-nft/files/etc/hotplug.d/iface/25-mwan3
Normal file
@ -0,0 +1,92 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /lib/functions.sh
|
||||
. /lib/functions/network.sh
|
||||
. /lib/mwan3/mwan3.sh
|
||||
|
||||
initscript=/etc/init.d/mwan3
|
||||
. /lib/functions/procd.sh
|
||||
|
||||
|
||||
SCRIPTNAME="mwan3-hotplug"
|
||||
[ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifdown" ] || [ "$ACTION" = "connected" ] || [ "$ACTION" = "disconnected" ] || exit 1
|
||||
[ -n "$INTERFACE" ] || exit 2
|
||||
[ "$FIRSTCONNECT" = "1" ] || [ "$MWAN3_SHUTDOWN" = "1" ] && exit 0
|
||||
|
||||
if { [ "$ACTION" = "ifup" ] || [ "$ACTION" = "connected" ] ; } && [ -z "$DEVICE" ]; then
|
||||
LOG notice "$ACTION called on $INTERFACE with no device set"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
[ "$MWAN3_STARTUP" = "init" ] || procd_lock
|
||||
|
||||
# Exit silently if the nft table is not yet set up. This covers the boot
|
||||
# race where an external ifup hotplug fires before start_service completes,
|
||||
# and the case where mwan3 has been manually stopped. Checked before
|
||||
# mwan3_init to avoid recreating /var/run/mwan3 as a side effect, which
|
||||
# would corrupt the stopped-service indicator. start_service handles all
|
||||
# interface setup itself via its own mwan3_ifup calls.
|
||||
$NFT list chain inet mwan3 mwan3_prerouting &>/dev/null || exit 0
|
||||
|
||||
mwan3_init
|
||||
|
||||
if [ "$MWAN3_STARTUP" != "init" ] && [ "$ACTION" = "ifup" ]; then
|
||||
mwan3_set_user_iface_rules $INTERFACE $DEVICE
|
||||
fi
|
||||
|
||||
config_get_bool enabled $INTERFACE 'enabled' '0'
|
||||
[ "${enabled}" -eq 1 ] || {
|
||||
LOG notice "mwan3 hotplug on $INTERFACE not called because interface disabled"
|
||||
exit 0
|
||||
}
|
||||
|
||||
config_get initial_state $INTERFACE initial_state "online"
|
||||
if [ "$initial_state" = "offline" ]; then
|
||||
readfile status $MWAN3TRACK_STATUS_DIR/$INTERFACE/STATUS 2>/dev/null || status="unknown"
|
||||
[ "$status" = "online" ] || status=offline
|
||||
else
|
||||
status=online
|
||||
fi
|
||||
|
||||
LOG notice "Execute $ACTION event on interface $INTERFACE (${DEVICE:-unknown})"
|
||||
|
||||
case "$ACTION" in
|
||||
connected)
|
||||
mwan3_set_iface_hotplug_state $INTERFACE "online"
|
||||
mwan3_create_iface_nft $INTERFACE $DEVICE
|
||||
mwan3_set_policies_nft
|
||||
mwan3_flush_unreplied_conntrack
|
||||
;;
|
||||
ifup)
|
||||
mwan3_update_peer_track_ip $INTERFACE
|
||||
mwan3_create_iface_nft $INTERFACE $DEVICE
|
||||
mwan3_create_iface_rules $INTERFACE $DEVICE
|
||||
mwan3_create_iface_route $INTERFACE $DEVICE
|
||||
mwan3_set_iface_hotplug_state $INTERFACE "$status"
|
||||
if [ "$MWAN3_STARTUP" != "init" ]; then
|
||||
mwan3_set_general_rules
|
||||
if [ "$status" = "online" ]; then
|
||||
mwan3_set_policies_nft
|
||||
mwan3_flush_unreplied_conntrack
|
||||
fi
|
||||
fi
|
||||
[ "$ACTION" = ifup ] && procd_running mwan3 "track_$INTERFACE" && procd_send_signal mwan3 "track_$INTERFACE" USR2
|
||||
;;
|
||||
disconnected)
|
||||
mwan3_set_iface_hotplug_state $INTERFACE "offline"
|
||||
mwan3_set_policies_nft
|
||||
;;
|
||||
ifdown)
|
||||
mwan3_set_iface_hotplug_state $INTERFACE "offline"
|
||||
mwan3_delete_iface_map_entries $INTERFACE
|
||||
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
|
||||
|
||||
mwan3_flush_conntrack "$INTERFACE" "$ACTION"
|
||||
|
||||
exit 0
|
||||
26
mwan3-nft/files/etc/hotplug.d/iface/26-mwan3-user
Normal file
26
mwan3-nft/files/etc/hotplug.d/iface/26-mwan3-user
Normal file
@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ -f "/etc/mwan3.user" ] && {
|
||||
. /lib/functions.sh
|
||||
. /lib/mwan3/mwan3.sh
|
||||
initscript=/etc/init.d/mwan3
|
||||
. /lib/functions/procd.sh
|
||||
|
||||
[ "$MWAN3_SHUTDOWN" != 1 ] && procd_lock
|
||||
|
||||
[ "$MWAN3_SHUTDOWN" != 1 ] && ! /etc/init.d/mwan3 running && {
|
||||
exit 0
|
||||
}
|
||||
|
||||
config_load mwan3
|
||||
|
||||
config_get_bool enabled "$INTERFACE" enabled 0
|
||||
[ "${enabled}" -eq 1 ] || {
|
||||
exit 0
|
||||
}
|
||||
|
||||
[ -x /etc/mwan3.user ] || chmod 755 /etc/mwan3.user
|
||||
env -i ACTION="$ACTION" INTERFACE="$INTERFACE" DEVICE="$DEVICE" /etc/mwan3.user 1000>&-
|
||||
}
|
||||
|
||||
exit 0
|
||||
316
mwan3-nft/files/etc/init.d/mwan3
Executable file
316
mwan3-nft/files/etc/init.d/mwan3
Executable file
@ -0,0 +1,316 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
. "${IPKG_INSTROOT}/lib/functions/network.sh"
|
||||
. "${IPKG_INSTROOT}/lib/mwan3/mwan3.sh"
|
||||
|
||||
START=20
|
||||
USE_PROCD=1
|
||||
SCRIPTNAME="mwan3-init"
|
||||
|
||||
service_running() {
|
||||
[ -d "$MWAN3_STATUS_DIR" ]
|
||||
}
|
||||
|
||||
start_tracker() {
|
||||
local enabled interface track_gateway
|
||||
interface=$1
|
||||
config_get_bool enabled $interface 'enabled' '0'
|
||||
[ $enabled -eq 0 ] && return
|
||||
|
||||
# Write gateway IP to state file for point-to-point interfaces
|
||||
mwan3_update_peer_track_ip "$interface"
|
||||
|
||||
# Start tracker if there are tracking IPs or track_gateway may provide one
|
||||
if [ -z "$(config_get $interface track_ip)" ]; then
|
||||
config_get_bool track_gateway "$interface" track_gateway 0
|
||||
[ "$track_gateway" -eq 1 ] || return
|
||||
fi
|
||||
|
||||
procd_open_instance "track_${1}"
|
||||
procd_set_param command /usr/sbin/mwan3track $interface
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled hotplug_pids
|
||||
|
||||
mwan3_init
|
||||
|
||||
local _any_enabled=0
|
||||
_check_iface_enabled() {
|
||||
local _e
|
||||
config_get_bool _e "$1" enabled 0
|
||||
[ "$_e" -eq 1 ] && _any_enabled=1
|
||||
}
|
||||
config_foreach _check_iface_enabled interface
|
||||
[ "$_any_enabled" -eq 0 ] && return 0
|
||||
|
||||
# Load the standalone mwan3 nftables table skeleton. This creates
|
||||
# (or atomically re-creates) `table inet mwan3` with its base chains
|
||||
# and sets. Must succeed before any `nft add ... inet mwan3 ...`
|
||||
# call, since those would otherwise silently paper over a missing
|
||||
# table. Abort start if the skeleton cannot load.
|
||||
if ! nft -f /lib/mwan3/mwan3-skeleton.nft; then
|
||||
LOG error "failed to load /lib/mwan3/mwan3-skeleton.nft; aborting start"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mwan3_ensure_nft_framework
|
||||
mwan3_render_config_ipsets
|
||||
mwan3_write_dnsmasq_fragments
|
||||
config_foreach start_tracker interface
|
||||
|
||||
mwan3_update_iface_to_table
|
||||
mwan3_set_dynamic_sets
|
||||
mwan3_set_connected_sets
|
||||
mwan3_set_custom_sets
|
||||
mwan3_set_general_rules
|
||||
config_foreach mwan3_ifup interface "init"
|
||||
wait $hotplug_pids
|
||||
mwan3_set_general_nft
|
||||
mwan3_set_policies_nft
|
||||
mwan3_set_user_rules
|
||||
mwan3_flush_stale_conntrack
|
||||
mwan3_dnsmasq_hup
|
||||
|
||||
# If software flow offloading is active, flush all conntrack entries so
|
||||
# existing flows re-establish under the new policy rules immediately rather
|
||||
# than continuing to use stale flowtable-cached routing decisions.
|
||||
if [ "$(uci -q get firewall.@defaults[0].flow_offloading)" = "1" ] && \
|
||||
[ -e "$CONNTRACK_FILE" ]; then
|
||||
echo f > "$CONNTRACK_FILE"
|
||||
LOG info "Connection tracking flushed on start (software flow offloading active)"
|
||||
fi
|
||||
|
||||
procd_open_instance rtmon_ipv4
|
||||
procd_set_param command /usr/sbin/mwan3rtmon ipv4
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
|
||||
if [ $NO_IPV6 -eq 0 ]; then
|
||||
procd_open_instance rtmon_ipv6
|
||||
procd_set_param command /usr/sbin/mwan3rtmon ipv6
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
fi
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
service_running || exit 0
|
||||
|
||||
# Stop mwan3track instances before tearing down routing
|
||||
# infrastructure. procd sends TERM only after stop_service returns,
|
||||
# so without this the trackers keep pinging while ip rules and nft
|
||||
# chains are deleted underneath them, producing spurious failure logs.
|
||||
# Uses procd_kill (not direct kill) so procd marks the instances as
|
||||
# stopped and does not trigger respawn.
|
||||
local _f _iface _killed=""
|
||||
for _f in "$MWAN3TRACK_STATUS_DIR"/*/PID; do
|
||||
[ -f "$_f" ] || continue
|
||||
_iface="${_f%/PID}"
|
||||
_iface="${_iface##*/}"
|
||||
procd_kill mwan3 "track_$_iface" && _killed=1
|
||||
done
|
||||
[ -n "$_killed" ] && sleep 1
|
||||
|
||||
local chain_name IP family table tid
|
||||
|
||||
mwan3_init
|
||||
config_foreach mwan3_interface_shutdown interface
|
||||
|
||||
for family in ipv4 ipv6; do
|
||||
if [ "$family" = "ipv4" ]; then
|
||||
IP="$IP4"
|
||||
elif [ "$family" = "ipv6" ]; then
|
||||
[ $NO_IPV6 -ne 0 ] && continue
|
||||
IP="$IP6"
|
||||
fi
|
||||
|
||||
for tid in $($IP route list table all | sed -ne 's/.*table \([0-9]\+\).*/\1/p' | sort -u); do
|
||||
[ $tid -gt $MWAN3_INTERFACE_MAX ] && continue
|
||||
$IP route flush table $tid &> /dev/null
|
||||
done
|
||||
|
||||
# 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
|
||||
# fwmark range extends to fwmark_base + MM_UNREACHABLE to cover
|
||||
# the global blackhole and unreachable rules at fwmark_base +
|
||||
# MM_BLACKHOLE / MM_UNREACHABLE.
|
||||
# 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
|
||||
# was the original bug: it deleted any rule in the band, which
|
||||
# is also why an admin override or another package's rule placed
|
||||
# in the same band could be wiped.
|
||||
# Both gates are derived from values mwan3_init reads back from
|
||||
# /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
|
||||
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))
|
||||
# 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
|
||||
pref="${line%%:*}"
|
||||
case "$pref" in
|
||||
''|*[!0-9]*) continue ;;
|
||||
esac
|
||||
# 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
|
||||
continue
|
||||
fi
|
||||
# Content gate (a): carries an fwmark masked by mwan3's MMX_MASK
|
||||
case "$line" in
|
||||
*" fwmark "*"/${mask} "*|*" fwmark "*"/${mask}")
|
||||
$IP rule del pref "$pref" &> /dev/null
|
||||
continue ;;
|
||||
esac
|
||||
# Content gate (b): looks up a mwan3-owned table id
|
||||
case "$line" in
|
||||
*" lookup "*)
|
||||
tbl="${line#* lookup }"
|
||||
tbl="${tbl%% *}"
|
||||
case "$tbl" in
|
||||
''|*[!0-9]*) continue ;;
|
||||
esac
|
||||
[ "$tbl" -ge 1 ] && [ "$tbl" -le "$MWAN3_INTERFACE_MAX" ] && \
|
||||
$IP rule del pref "$pref" &> /dev/null
|
||||
;;
|
||||
esac
|
||||
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
|
||||
$NFT delete table inet mwan3 >/dev/null 2>&1
|
||||
|
||||
rm -rf $MWAN3_STATUS_DIR $MWAN3TRACK_STATUS_DIR
|
||||
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
mwan3_init
|
||||
|
||||
# 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
|
||||
# from build functions nest inside it rather than committing immediately.
|
||||
# The preamble tears down all dynamic chains and internal sets; user-defined
|
||||
# sets and sticky sets are untouched throughout.
|
||||
mwan3_nft_reload_start
|
||||
mwan3_ensure_nft_framework
|
||||
mwan3_render_config_ipsets
|
||||
mwan3_cleanup_orphaned_ipsets
|
||||
mwan3_set_dynamic_sets
|
||||
mwan3_set_connected_sets
|
||||
mwan3_set_custom_sets
|
||||
config_foreach mwan3_rebuild_iface_nft interface
|
||||
mwan3_set_general_nft
|
||||
mwan3_set_policies_nft
|
||||
mwan3_set_user_rules
|
||||
mwan3_nft_reload_commit || return 1
|
||||
|
||||
# ip rules and routing tables are kernel state outside nftables —
|
||||
# update them after the nft commit so the table is already consistent.
|
||||
mwan3_update_iface_to_table
|
||||
mwan3_set_general_rules
|
||||
config_foreach mwan3_rebuild_iface_rules interface
|
||||
|
||||
# Signal mwan3rtmon to reload UCI config and re-sync the custom sets.
|
||||
# This is needed when rt_table_lookup changes: the running mwan3rtmon
|
||||
# holds a stale extra_table_set in memory until it reloads. Sent after
|
||||
# the nft commit so the sets exist when rtmon re-dumps them.
|
||||
procd_send_signal mwan3 rtmon_ipv4 HUP
|
||||
[ $NO_IPV6 -eq 0 ] && procd_send_signal mwan3 rtmon_ipv6 HUP
|
||||
|
||||
# Signal trackers to reload UCI tracking parameters.
|
||||
_signal_tracker() {
|
||||
procd_running mwan3 "track_$1" && \
|
||||
procd_send_signal mwan3 "track_$1" HUP
|
||||
}
|
||||
config_foreach _signal_tracker interface
|
||||
|
||||
# mwan3_write_dnsmasq_fragments restarts dnsmasq internally if fragment
|
||||
# content changed. User-defined nft sets survive the atomic reload intact
|
||||
# so no HUP is needed here when fragments are unchanged.
|
||||
mwan3_write_dnsmasq_fragments
|
||||
|
||||
# If a set was deleted and recreated (flags changed) and had dnsmasq-populated
|
||||
# domain entries, those entries were lost. HUP dnsmasq to repopulate them.
|
||||
[ "$MWAN3_NEED_DNSMASQ_HUP" -eq 1 ] && mwan3_dnsmasq_hup
|
||||
|
||||
# Detect tracker count mismatch. reload_service cannot add or remove
|
||||
# procd service instances -- only start_service can do that. If the
|
||||
# number of running mwan3track processes does not match the number the
|
||||
# current config requires, fall through to a full restart. The restart
|
||||
# starts the correct tracker set; start_service calls mwan3_dnsmasq_hup
|
||||
# so nftset-populated sets are repopulated on name resolution.
|
||||
local _expected_trackers=0 _running_trackers=0
|
||||
_count_tracker_needed() {
|
||||
local enabled track_ip track_gateway
|
||||
config_get_bool enabled "$1" enabled 0
|
||||
[ "$enabled" -eq 1 ] || return
|
||||
if [ -n "$(config_get "$1" track_ip)" ]; then
|
||||
_expected_trackers=$((_expected_trackers + 1))
|
||||
return
|
||||
fi
|
||||
config_get_bool track_gateway "$1" track_gateway 0
|
||||
[ "$track_gateway" -eq 1 ] && _expected_trackers=$((_expected_trackers + 1))
|
||||
}
|
||||
config_foreach _count_tracker_needed interface
|
||||
_count_running_tracker() {
|
||||
procd_running mwan3 "track_$1" && _running_trackers=$((_running_trackers + 1))
|
||||
}
|
||||
config_foreach _count_running_tracker interface
|
||||
if [ "$_running_trackers" -ne "$_expected_trackers" ]; then
|
||||
LOG notice "reload: tracker mismatch (need $_expected_trackers, have $_running_trackers) -- restarting"
|
||||
stop
|
||||
start
|
||||
fi
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger 'mwan3'
|
||||
}
|
||||
20
mwan3-nft/files/etc/mwan3.user
Normal file
20
mwan3-nft/files/etc/mwan3.user
Normal file
@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# This file is interpreted as shell script.
|
||||
# Put your custom mwan3 action here, they will
|
||||
# be executed with each netifd hotplug interface event
|
||||
# on interfaces for which mwan3 is enabled.
|
||||
#
|
||||
# There are three main environment variables that are passed to this script.
|
||||
#
|
||||
# $ACTION
|
||||
# <ifup> Is called by netifd and mwan3track.
|
||||
# <ifdown> Is called by netifd and mwan3track.
|
||||
# <connected> Is only called by mwan3track if tracking was successful.
|
||||
# <disconnected> Is only called by mwan3track if tracking has failed.
|
||||
# $INTERFACE Name of the interface an action relates to (e.g. "wan" or "wwan").
|
||||
# $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
|
||||
@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
mwan3_migrate_flush_conntrack() {
|
||||
local iface="$1"
|
||||
|
||||
config_get value "${iface}" flush_conntrack
|
||||
case $value in
|
||||
always)
|
||||
uci_remove mwan3 "$iface" flush_conntrack
|
||||
uci_add_list mwan3 "$iface" flush_conntrack ifup
|
||||
uci_add_list mwan3 "$iface" flush_conntrack ifdown
|
||||
;;
|
||||
never)
|
||||
uci_remove mwan3 "$iface" flush_conntrack
|
||||
;;
|
||||
esac
|
||||
|
||||
uci_commit mwan3
|
||||
}
|
||||
|
||||
config_load mwan3
|
||||
config_foreach mwan3_migrate_flush_conntrack interface
|
||||
|
||||
exit 0
|
||||
@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
# One-shot upgrade cleanup: remove the stale firewall.mwan3_reload include
|
||||
# section registered by older mwan3 installs that operated in table inet fw4.
|
||||
uci -q delete firewall.mwan3_reload && uci commit firewall
|
||||
exit 0
|
||||
568
mwan3-nft/files/lib/mwan3/common.sh
Normal file
568
mwan3-nft/files/lib/mwan3/common.sh
Normal file
@ -0,0 +1,568 @@
|
||||
#!/bin/sh
|
||||
|
||||
IP4="ip -4"
|
||||
IP6="ip -6"
|
||||
UCODE="ucode"
|
||||
MWAN3_LIB_PATH="/lib/mwan3"
|
||||
MWAN3_CREATE_IFACE_ROUTE="${UCODE} ${MWAN3_LIB_PATH}/mwan3-create-iface-route.uc"
|
||||
MWAN3_GET_ADDR="${UCODE} ${MWAN3_LIB_PATH}/mwan3-get-addr.uc"
|
||||
MWAN3_LIST_ROUTES="${UCODE} ${MWAN3_LIB_PATH}/mwan3-list-routes.uc"
|
||||
MWAN3_MANAGE_RULES="${UCODE} ${MWAN3_LIB_PATH}/mwan3-manage-rules.uc"
|
||||
SCRIPTNAME="$(basename "$0")"
|
||||
|
||||
MWAN3_STATUS_DIR="/var/run/mwan3"
|
||||
MWAN3TRACK_STATUS_DIR="/var/run/mwan3track"
|
||||
|
||||
MWAN3_INTERFACE_MAX=""
|
||||
|
||||
MMX_MASK=""
|
||||
MMX_DEFAULT=""
|
||||
MMX_BLACKHOLE=""
|
||||
MM_BLACKHOLE=""
|
||||
|
||||
MMX_UNREACHABLE=""
|
||||
MM_UNREACHABLE=""
|
||||
MAX_SLEEP=$(((1<<31)-1))
|
||||
|
||||
# nftables inet family handles both IPv4 and IPv6
|
||||
# Check if IPv6 is disabled in the kernel
|
||||
|
||||
[ -d /proc/sys/net/ipv6 ]
|
||||
NO_IPV6=$?
|
||||
|
||||
NFT="nft"
|
||||
MWAN3_NFT_BATCH="$MWAN3_STATUS_DIR/mwan3_nft_batch.$$"
|
||||
MWAN3_BATCH_DEPTH=0
|
||||
MWAN3_NEED_DNSMASQ_HUP=0
|
||||
|
||||
LOG()
|
||||
{
|
||||
local facility=$1; shift
|
||||
|
||||
[ "$facility" = "debug" ] && [ "${MWAN3_VERBOSE_LOGGING:-0}" = "0" ] && return
|
||||
logger -t "${SCRIPTNAME}[$$]" -p $facility "$*"
|
||||
}
|
||||
|
||||
# Execute an nft command. When inside a batch (MWAN3_BATCH_DEPTH > 0), push
|
||||
# the command into the batch file instead of running nft immediately.
|
||||
|
||||
mwan3_nft_exec()
|
||||
{
|
||||
if [ "$MWAN3_BATCH_DEPTH" -gt 0 ]; then
|
||||
mwan3_nft_push "$@"
|
||||
return
|
||||
fi
|
||||
local error
|
||||
error=$($NFT "$@" 2>&1) || {
|
||||
LOG error "nft $*: $error"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Start an nft batch. Truncates the batch file only when opening the outermost
|
||||
# level (depth 0 -> 1). Nested calls increment depth and are otherwise no-ops,
|
||||
# so callers can open their own mini-batches inside a larger reload batch
|
||||
# without inadvertently truncating it.
|
||||
|
||||
mwan3_nft_batch_start()
|
||||
{
|
||||
if [ "$MWAN3_BATCH_DEPTH" -eq 0 ]; then
|
||||
local old_umask
|
||||
old_umask=$(umask)
|
||||
umask 077
|
||||
: > "$MWAN3_NFT_BATCH"
|
||||
umask "$old_umask"
|
||||
fi
|
||||
MWAN3_BATCH_DEPTH=$((MWAN3_BATCH_DEPTH + 1))
|
||||
}
|
||||
|
||||
# Append a line to the nft batch. Called directly with a pre-formatted nft
|
||||
# statement, or indirectly via mwan3_nft_exec when MWAN3_BATCH_DEPTH > 0.
|
||||
|
||||
mwan3_nft_push()
|
||||
{
|
||||
echo "$*" >> "$MWAN3_NFT_BATCH"
|
||||
}
|
||||
|
||||
# Commit the nft batch. Decrements depth; only commits to the kernel when
|
||||
# reaching depth 0 (the outermost level). Inner commits from nested callers
|
||||
# are no-ops: they just decrement the counter and return.
|
||||
|
||||
mwan3_nft_batch_commit()
|
||||
{
|
||||
MWAN3_BATCH_DEPTH=$((MWAN3_BATCH_DEPTH - 1))
|
||||
[ "$MWAN3_BATCH_DEPTH" -gt 0 ] && return 0
|
||||
local error
|
||||
error=$($NFT -f "$MWAN3_NFT_BATCH" 2>&1) || {
|
||||
LOG error "nft batch: $error"
|
||||
rm -f "$MWAN3_NFT_BATCH"
|
||||
return 1
|
||||
}
|
||||
rm -f "$MWAN3_NFT_BATCH"
|
||||
}
|
||||
|
||||
# Begin an atomic reload batch. Opens the outermost batch level (depth 0->1)
|
||||
# so all subsequent mwan3_nft_batch_start/commit calls from build functions
|
||||
# accumulate into the same global batch rather than committing immediately.
|
||||
# The preamble: flushes all skeleton chains, then flushes+deletes all dynamic
|
||||
# chains (iface_in, policy, rule, or-setter, etc.).
|
||||
# Internal mwan3_* sets are deleted so mwan3_ensure_nft_framework can recreate
|
||||
# them with correct flags. User-defined sets are never touched.
|
||||
|
||||
mwan3_nft_reload_start()
|
||||
{
|
||||
local chain setname
|
||||
mwan3_nft_batch_start
|
||||
for chain in mwan3_prerouting mwan3_output mwan3_postrouting \
|
||||
mwan3_ifaces_in mwan3_rules mwan3_connected mwan3_custom mwan3_dynamic; do
|
||||
mwan3_nft_push "flush chain inet mwan3 $chain"
|
||||
done
|
||||
|
||||
# Two-pass: flush all dynamic chains first to remove cross-references
|
||||
# (e.g. mwan3_rule_* chains jump to mwan3_or_meta_* chains), then delete.
|
||||
# A single-pass flush+delete in alphabetical order fails with "Device or
|
||||
# resource busy" because mwan3_or_meta_* sorts before mwan3_rule_*, so
|
||||
# the delete fires while the rule chain still holds a jump reference.
|
||||
|
||||
local _dyn_chains=""
|
||||
for chain in $($NFT list chains inet 2>/dev/null | grep "chain mwan3_" | awk '{gsub(/ \{.*/, ""); print $2}'); do
|
||||
case "$chain" in
|
||||
mwan3_prerouting|mwan3_output|mwan3_postrouting|\
|
||||
mwan3_ifaces_in|mwan3_rules|mwan3_connected|mwan3_custom|mwan3_dynamic)
|
||||
;;
|
||||
*)
|
||||
mwan3_nft_push "flush chain inet mwan3 $chain"
|
||||
_dyn_chains="$_dyn_chains $chain"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
for chain in $_dyn_chains; do
|
||||
mwan3_nft_push "delete chain inet mwan3 $chain"
|
||||
done
|
||||
for setname in mwan3_connected_v4 mwan3_connected_v6 \
|
||||
mwan3_custom_v4 mwan3_custom_v6 \
|
||||
mwan3_dynamic_v4 mwan3_dynamic_v6; do
|
||||
mwan3_nft_push "delete set inet mwan3 $setname"
|
||||
done
|
||||
}
|
||||
|
||||
# Commit the reload batch atomically to the kernel (thin wrapper around
|
||||
# mwan3_nft_batch_commit, which handles the depth decrement and actual commit).
|
||||
|
||||
mwan3_nft_reload_commit()
|
||||
{
|
||||
mwan3_nft_batch_commit
|
||||
}
|
||||
|
||||
# Build an nft mark set expression
|
||||
# iptables: -j MARK --set-xmark VALUE/MASK
|
||||
# means: mark = (mark & ~MASK) | VALUE
|
||||
# nftables: meta mark set (meta mark & ~MASK) | VALUE
|
||||
# Uses & and | symbols (not 'and'/'or' keywords) to avoid parser ambiguity
|
||||
|
||||
mwan3_nft_mark_expr()
|
||||
{
|
||||
local value="$1" mask="$2"
|
||||
local complement
|
||||
complement=$(printf "0x%08x" $(( (~mask) & 0xFFFFFFFF )))
|
||||
echo "meta mark set meta mark & $complement | $value"
|
||||
}
|
||||
|
||||
# Canonicalise a mark value (e.g. 0x100, 0x00000100) to a stable chain-name suffix.
|
||||
# Used to name the per-mark OR-immediate setter chains. Always emits lowercase
|
||||
# 0x%x form so two callers computing the same mark land on the same chain name.
|
||||
|
||||
mwan3_or_chain_suffix()
|
||||
{
|
||||
printf "0x%x" $(($1))
|
||||
}
|
||||
|
||||
# Build the per-mark OR-immediate setter chains used to synthesise non-destructive
|
||||
# cross-register copies via vmap dispatch. For each mark M in the configured set
|
||||
# of valid mwan3 mark values we create:
|
||||
# chain mwan3_or_meta_<M>: meta mark set meta mark | M
|
||||
# chain mwan3_or_ct_<M> : ct mark set ct mark | M
|
||||
# These chains are jumped into from vmap statements that key on the masked value
|
||||
# of the source register; the runtime register value is "lifted" into the
|
||||
# immediate operand of each branch's set statement, sidestepping the kernel
|
||||
# limitation that an nft set-statement expression tree may reference at most one
|
||||
# runtime source register. With these helpers in place, both restore (ct -> meta)
|
||||
# and save (meta -> ct) become non-destructive in the unmasked bits, which
|
||||
# removes mwan3's previous priority dependency on pbr.
|
||||
|
||||
mwan3_build_or_chains_nft()
|
||||
{
|
||||
local id mark suffix want
|
||||
|
||||
# Compute the full set of mark values that may need a setter chain:
|
||||
# every per-iface mark id 1..MWAN3_INTERFACE_MAX, plus the three special
|
||||
# marks (default, blackhole, unreachable).
|
||||
|
||||
want=""
|
||||
for id in $(seq 1 "$MWAN3_INTERFACE_MAX"); do
|
||||
mark=$(mwan3_id2mask id MMX_MASK)
|
||||
suffix=$(mwan3_or_chain_suffix "$mark")
|
||||
want="$want $suffix"
|
||||
done
|
||||
want="$want $(mwan3_or_chain_suffix "$MMX_DEFAULT")"
|
||||
want="$want $(mwan3_or_chain_suffix "$MMX_BLACKHOLE")"
|
||||
want="$want $(mwan3_or_chain_suffix "$MMX_UNREACHABLE")"
|
||||
|
||||
# Always flush+repopulate. A previous idempotency check that only verified
|
||||
# chain *existence* could leave empty chain bodies wedged after a partial
|
||||
# batch failure, with no recovery path. ~126 trivial statements; cheap.
|
||||
|
||||
mwan3_nft_batch_start
|
||||
for suffix in $want; do
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_or_meta_${suffix}"
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_or_ct_${suffix}"
|
||||
mwan3_nft_push "flush chain inet mwan3 mwan3_or_meta_${suffix}"
|
||||
mwan3_nft_push "flush chain inet mwan3 mwan3_or_ct_${suffix}"
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_or_meta_${suffix} meta mark set meta mark | ${suffix}"
|
||||
mwan3_nft_push "add rule inet mwan3 mwan3_or_ct_${suffix} ct mark set ct mark & $MMX_MASK_COMPLEMENT | ${suffix}"
|
||||
done
|
||||
mwan3_nft_batch_commit
|
||||
}
|
||||
|
||||
# Build the verdict-map body of a vmap statement that, given a key set of
|
||||
# mark values, jumps to the matching mwan3_or_<reg>_<mark> setter chain.
|
||||
# Args: $1 = "meta" or "ct" (target register), $2... = mark values
|
||||
# Result echoed as the body for use as: <key-expr> vmap { <body> }
|
||||
|
||||
mwan3_or_vmap_body()
|
||||
{
|
||||
local reg="$1"; shift
|
||||
local mark suffix first=1 body=""
|
||||
for mark in "$@"; do
|
||||
suffix=$(mwan3_or_chain_suffix "$mark")
|
||||
if [ $first -eq 1 ]; then
|
||||
body="${suffix} : jump mwan3_or_${reg}_${suffix}"
|
||||
first=0
|
||||
else
|
||||
body="${body}, ${suffix} : jump mwan3_or_${reg}_${suffix}"
|
||||
fi
|
||||
done
|
||||
echo "$body"
|
||||
}
|
||||
|
||||
# Enumerate every mark value that needs to appear in the restore/save vmaps:
|
||||
# all per-iface marks plus the three specials. Echoes a space-separated list.
|
||||
|
||||
mwan3_all_marks()
|
||||
{
|
||||
local id
|
||||
for id in $(seq 1 "$MWAN3_INTERFACE_MAX"); do
|
||||
mwan3_id2mask id MMX_MASK
|
||||
printf " "
|
||||
done
|
||||
printf "%s %s %s\n" "$MMX_DEFAULT" "$MMX_BLACKHOLE" "$MMX_UNREACHABLE"
|
||||
}
|
||||
|
||||
# Ensure all mwan3 nftables framework objects exist with correct flags.
|
||||
# Always deletes and recreates sets to guarantee auto-merge is present.
|
||||
|
||||
mwan3_ensure_nft_framework()
|
||||
{
|
||||
local setname
|
||||
|
||||
# Always delete existing sets — nft 'add set' is idempotent and won't
|
||||
# update flags (like auto-merge) on existing sets, so we must recreate.
|
||||
# stop_service() flushes chains first, so no rules reference the sets.
|
||||
# Inside a reload batch the deletions are already in the preamble.
|
||||
|
||||
if [ "$MWAN3_BATCH_DEPTH" -eq 0 ]; then
|
||||
for setname in mwan3_connected_v4 mwan3_connected_v6 \
|
||||
mwan3_custom_v4 mwan3_custom_v6 \
|
||||
mwan3_dynamic_v4 mwan3_dynamic_v6; do
|
||||
$NFT delete set inet mwan3 "$setname" >/dev/null 2>&1
|
||||
done
|
||||
fi
|
||||
|
||||
mwan3_nft_batch_start
|
||||
|
||||
# Sets for network classification (interval + auto-merge for CIDR support)
|
||||
|
||||
mwan3_nft_push "add set inet mwan3 mwan3_connected_v4 { type ipv4_addr; flags interval; auto-merge; }"
|
||||
mwan3_nft_push "add set inet mwan3 mwan3_connected_v6 { type ipv6_addr; flags interval; auto-merge; }"
|
||||
mwan3_nft_push "add set inet mwan3 mwan3_custom_v4 { type ipv4_addr; flags interval; auto-merge; }"
|
||||
mwan3_nft_push "add set inet mwan3 mwan3_custom_v6 { type ipv6_addr; flags interval; auto-merge; }"
|
||||
mwan3_nft_push "add set inet mwan3 mwan3_dynamic_v4 { type ipv4_addr; flags interval; auto-merge; }"
|
||||
mwan3_nft_push "add set inet mwan3 mwan3_dynamic_v6 { type ipv6_addr; flags interval; auto-merge; }"
|
||||
|
||||
# Hook chains (base chains with type/hook/priority)
|
||||
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_prerouting { type filter hook prerouting priority mangle + 1; policy accept; }"
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_output { type route hook output priority mangle + 1; policy accept; }"
|
||||
|
||||
# IPv6 SNAT chain (opt-in per interface via 'snat6'). See 10-mwan3.nft
|
||||
# for the rationale; per-iface rules are added by mwan3_create_iface_nft.
|
||||
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_postrouting { type nat hook postrouting priority srcnat - 1; policy accept; }"
|
||||
|
||||
# Internal chains (jumped to from hook chains)
|
||||
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_ifaces_in"
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_rules"
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_connected"
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_custom"
|
||||
mwan3_nft_push "add chain inet mwan3 mwan3_dynamic"
|
||||
|
||||
mwan3_nft_batch_commit
|
||||
}
|
||||
|
||||
mwan3_get_true_iface()
|
||||
{
|
||||
local family V
|
||||
_true_iface=$2
|
||||
config_get family "$2" family ipv4
|
||||
if [ "$family" = "ipv4" ]; then
|
||||
V=4
|
||||
elif [ "$family" = "ipv6" ]; then
|
||||
V=6
|
||||
fi
|
||||
ubus call "network.interface.${2}_${V}" status &>/dev/null && _true_iface="${2}_${V}"
|
||||
export "$1=$_true_iface"
|
||||
}
|
||||
|
||||
mwan3_get_src_ip()
|
||||
{
|
||||
local family _src_ip interface true_iface device addr_cmd default_ip
|
||||
interface=$2
|
||||
mwan3_get_true_iface true_iface $interface
|
||||
|
||||
unset "$1"
|
||||
config_get family "$interface" family ipv4
|
||||
if [ "$family" = "ipv4" ]; then
|
||||
addr_cmd='network_get_ipaddr'
|
||||
default_ip="0.0.0.0"
|
||||
elif [ "$family" = "ipv6" ]; then
|
||||
addr_cmd='network_get_ipaddr6'
|
||||
default_ip="::"
|
||||
fi
|
||||
|
||||
$addr_cmd _src_ip "$true_iface"
|
||||
if [ -z "$_src_ip" ]; then
|
||||
if [ "$family" = "ipv6" ]; then
|
||||
|
||||
# on IPv6-PD interfaces (like PPPoE interfaces) we don't
|
||||
# have a real address, just a prefix, that can be delegated
|
||||
# to interfaces, because using :: (the fallback above) or
|
||||
# the link-local address will not work (reliably, if at
|
||||
# all) try to find an address which we can use instead
|
||||
|
||||
network_get_prefix6 _src_ip "$true_iface"
|
||||
if [ -n "$_src_ip" ]; then
|
||||
|
||||
# got a prefix like 2001:xxxx:yyyy::/48, clean it up to
|
||||
# only contain the prefix -> 2001:xxxx:yyyy
|
||||
|
||||
_src_ip=$(echo "$_src_ip" | sed -e 's;:*/.*$;;')
|
||||
_src_ip=$(${MWAN3_GET_ADDR} 6 "" "$_src_ip")
|
||||
fi
|
||||
fi
|
||||
if [ -z "$_src_ip" ]; then
|
||||
network_get_device device $true_iface
|
||||
local _fam_num=4
|
||||
[ "$family" = "ipv6" ] && _fam_num=6
|
||||
_src_ip=$(${MWAN3_GET_ADDR} "$_fam_num" "$device")
|
||||
fi
|
||||
if [ -n "$_src_ip" ]; then
|
||||
LOG warn "no src $family address found from netifd for interface '$true_iface' dev '$device' guessing $_src_ip"
|
||||
else
|
||||
_src_ip="$default_ip"
|
||||
LOG warn "no src $family address found for interface '$true_iface' dev '$device'"
|
||||
fi
|
||||
fi
|
||||
export "$1=$_src_ip"
|
||||
}
|
||||
|
||||
readfile() {
|
||||
[ -f "$2" ] || return 1
|
||||
|
||||
# read returns 1 on EOF
|
||||
read -d'\0' $1 <"$2" || :
|
||||
}
|
||||
|
||||
mwan3_get_mwan3track_status()
|
||||
{
|
||||
local interface=$2
|
||||
local track_ips track_gateway pid cmdline started
|
||||
mwan3_list_track_ips()
|
||||
{
|
||||
track_ips="$1 $track_ips"
|
||||
}
|
||||
config_list_foreach "$interface" track_ip mwan3_list_track_ips
|
||||
config_get_bool track_gateway "$interface" track_gateway 0
|
||||
|
||||
if [ -z "$track_ips" ] && [ "$track_gateway" -eq 0 ]; then
|
||||
export -n "$1=disabled"
|
||||
return
|
||||
fi
|
||||
readfile pid $MWAN3TRACK_STATUS_DIR/$interface/PID 2>/dev/null
|
||||
if [ -z "$pid" ]; then
|
||||
export -n "$1=down"
|
||||
return
|
||||
fi
|
||||
readfile cmdline /proc/$pid/cmdline 2>/dev/null
|
||||
if [ "$cmdline" != "/bin/sh/usr/sbin/mwan3track${interface}" ]; then
|
||||
export -n "$1=down"
|
||||
return
|
||||
fi
|
||||
readfile started $MWAN3TRACK_STATUS_DIR/$interface/STARTED
|
||||
case "$started" in
|
||||
0)
|
||||
export -n "$1=paused"
|
||||
;;
|
||||
1)
|
||||
export -n "$1=active"
|
||||
;;
|
||||
*)
|
||||
export -n "$1=down"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
mwan3_init()
|
||||
{
|
||||
local bitcnt mmdefault source_routing
|
||||
|
||||
config_load mwan3
|
||||
config_get_bool MWAN3_VERBOSE_LOGGING globals verbose_logging 0
|
||||
|
||||
[ -d $MWAN3_STATUS_DIR ] || { mkdir -m 0700 $MWAN3_STATUS_DIR && mkdir -m 0700 $MWAN3_STATUS_DIR/iface_state; }
|
||||
|
||||
# mwan3's MARKing mask (at least 3 bits should be set)
|
||||
if [ -e "${MWAN3_STATUS_DIR}/mmx_mask" ]; then
|
||||
readfile MMX_MASK "${MWAN3_STATUS_DIR}/mmx_mask"
|
||||
MWAN3_INTERFACE_MAX=$(uci_get_state mwan3 globals iface_max)
|
||||
else
|
||||
config_get MMX_MASK globals mmx_mask '0x3F00'
|
||||
echo "$MMX_MASK"| tr 'A-F' 'a-f' > "${MWAN3_STATUS_DIR}/mmx_mask"
|
||||
LOG debug "Using firewall mask ${MMX_MASK}"
|
||||
|
||||
bitcnt=$(mwan3_count_one_bits MMX_MASK)
|
||||
mmdefault=$(((1<<bitcnt)-1))
|
||||
MWAN3_INTERFACE_MAX=$((mmdefault-3))
|
||||
uci_toggle_state mwan3 globals iface_max "$MWAN3_INTERFACE_MAX"
|
||||
LOG debug "Max interface count is ${MWAN3_INTERFACE_MAX}"
|
||||
fi
|
||||
|
||||
# mark mask constants
|
||||
|
||||
bitcnt=$(mwan3_count_one_bits MMX_MASK)
|
||||
mmdefault=$(((1<<bitcnt)-1))
|
||||
MM_BLACKHOLE=$((mmdefault-2))
|
||||
MM_UNREACHABLE=$((mmdefault-1))
|
||||
|
||||
# MMX_DEFAULT should equal MMX_MASK
|
||||
|
||||
MMX_DEFAULT=$(mwan3_id2mask mmdefault MMX_MASK)
|
||||
MMX_BLACKHOLE=$(mwan3_id2mask MM_BLACKHOLE MMX_MASK)
|
||||
MMX_UNREACHABLE=$(mwan3_id2mask MM_UNREACHABLE MMX_MASK)
|
||||
|
||||
# Precompute mask complement for nft rules
|
||||
|
||||
MMX_MASK_COMPLEMENT=$(printf "0x%08x" $(( (~MMX_MASK) & 0xFFFFFFFF )))
|
||||
|
||||
# Configurable ip rule base priorities. Defaults preserve the historical
|
||||
# layout exactly: per-interface iif lookup rules at id+1000, fwmark lookup
|
||||
# rules at id+2000, fwmark unreachable rules at id+3000. Two ordering
|
||||
# constraints must hold to avoid priority collisions:
|
||||
# iif_rule_base + MWAN3_INTERFACE_MAX < fwmark_rule_base
|
||||
# fwmark_rule_base + MWAN3_INTERFACE_MAX + 1 < unreachable_rule_base
|
||||
# (the +1 reflects that the fwmark tier also contains the global blackhole
|
||||
# and unreachable rules at fwmark_rule_base + MM_BLACKHOLE / MM_UNREACHABLE,
|
||||
# the highest of which is fwmark_rule_base + MWAN3_INTERFACE_MAX + 2.)
|
||||
# If either constraint is violated, all three values are reverted to defaults.
|
||||
#
|
||||
# Like MMX_MASK, the post-validation values are persisted to /var/state/mwan3
|
||||
# at start time and read back from state on subsequent mwan3_init invocations
|
||||
# (including from stop_service). This ensures the values seen by stop_service
|
||||
# identify the rules the running instance actually created, even if
|
||||
# /etc/config/mwan3 has been edited since start. The mmx_mask state file
|
||||
# doubles as the "instance started" indicator.
|
||||
|
||||
if [ -e "${MWAN3_STATUS_DIR}/mmx_mask" ]; then
|
||||
MWAN3_IIF_RULE_BASE=$(uci_get_state mwan3 globals iif_rule_base 1000)
|
||||
MWAN3_FWMARK_RULE_BASE=$(uci_get_state mwan3 globals fwmark_rule_base 2000)
|
||||
MWAN3_UNREACHABLE_RULE_BASE=$(uci_get_state mwan3 globals unreachable_rule_base 3000)
|
||||
else
|
||||
config_get MWAN3_IIF_RULE_BASE globals iif_rule_base 1000
|
||||
config_get MWAN3_FWMARK_RULE_BASE globals fwmark_rule_base 2000
|
||||
config_get MWAN3_UNREACHABLE_RULE_BASE globals unreachable_rule_base 3000
|
||||
|
||||
if [ "$((MWAN3_IIF_RULE_BASE + MWAN3_INTERFACE_MAX))" -ge "$MWAN3_FWMARK_RULE_BASE" ] || \
|
||||
[ "$((MWAN3_FWMARK_RULE_BASE + MWAN3_INTERFACE_MAX + 1))" -ge "$MWAN3_UNREACHABLE_RULE_BASE" ]; then
|
||||
LOG warn "Rule base ordering constraint violated (iif=$MWAN3_IIF_RULE_BASE, fwmark=$MWAN3_FWMARK_RULE_BASE, unreachable=$MWAN3_UNREACHABLE_RULE_BASE, max_interfaces=$MWAN3_INTERFACE_MAX); reverting all to defaults 1000/2000/3000"
|
||||
MWAN3_IIF_RULE_BASE=1000
|
||||
MWAN3_FWMARK_RULE_BASE=2000
|
||||
MWAN3_UNREACHABLE_RULE_BASE=3000
|
||||
fi
|
||||
|
||||
uci_toggle_state mwan3 globals iif_rule_base "$MWAN3_IIF_RULE_BASE"
|
||||
uci_toggle_state mwan3 globals fwmark_rule_base "$MWAN3_FWMARK_RULE_BASE"
|
||||
uci_toggle_state mwan3 globals unreachable_rule_base "$MWAN3_UNREACHABLE_RULE_BASE"
|
||||
fi
|
||||
}
|
||||
|
||||
# maps the 1st parameter so it only uses the bits allowed by the bitmask (2nd parameter)
|
||||
# which means spreading the bits of the 1st parameter to only use the bits that are set to 1 in the 2nd parameter
|
||||
# 0 0 0 0 0 1 0 1 (0x05) 1st parameter
|
||||
# 1 0 1 0 1 0 1 0 (0xAA) 2nd parameter
|
||||
# 1 0 1 result
|
||||
|
||||
mwan3_id2mask()
|
||||
{
|
||||
local bit_msk bit_val result
|
||||
bit_val=0
|
||||
result=0
|
||||
for bit_msk in $(seq 0 31); do
|
||||
if [ $((($2>>bit_msk)&1)) = "1" ]; then
|
||||
if [ $((($1>>bit_val)&1)) = "1" ]; then
|
||||
result=$((result|(1<<bit_msk)))
|
||||
fi
|
||||
bit_val=$((bit_val+1))
|
||||
fi
|
||||
done
|
||||
printf "0x%x" $result
|
||||
}
|
||||
|
||||
# counts how many bits are set to 1
|
||||
# n&(n-1) clears the lowest bit set to 1
|
||||
|
||||
mwan3_count_one_bits()
|
||||
{
|
||||
local count n
|
||||
count=0
|
||||
n=$(($1))
|
||||
while [ "$n" -gt "0" ]; do
|
||||
n=$((n&(n-1)))
|
||||
count=$((count+1))
|
||||
done
|
||||
echo $count
|
||||
}
|
||||
|
||||
get_uptime() {
|
||||
local _tmp
|
||||
readfile _tmp /proc/uptime
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "${_tmp%%.*}"
|
||||
else
|
||||
export -n "$1=${_tmp%%.*}"
|
||||
fi
|
||||
}
|
||||
|
||||
get_online_time() {
|
||||
local time_n time_u iface
|
||||
iface="$2"
|
||||
readfile time_u "$MWAN3TRACK_STATUS_DIR/${iface}/ONLINE" 2>/dev/null
|
||||
[ -z "${time_u}" ] || [ "${time_u}" = "0" ] || {
|
||||
get_uptime time_n
|
||||
export -n "$1=$((time_n-time_u))"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
121
mwan3-nft/files/lib/mwan3/mwan3-create-iface-route.uc
Normal file
121
mwan3-nft/files/lib/mwan3/mwan3-create-iface-route.uc
Normal file
@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env ucode
|
||||
'use strict';
|
||||
|
||||
import * as rtnl from "rtnl";
|
||||
import * as uci from "uci";
|
||||
import * as ubus from "ubus";
|
||||
|
||||
const RTM_GETROUTE = rtnl.const.RTM_GETROUTE;
|
||||
const RTM_NEWROUTE = rtnl.const.RTM_NEWROUTE;
|
||||
const NLM_F_DUMP = rtnl.const.NLM_F_DUMP;
|
||||
const NLM_F_CREATE = rtnl.const.NLM_F_CREATE;
|
||||
const NLM_F_REPLACE = rtnl.const.NLM_F_REPLACE;
|
||||
const RT_TABLE_MAIN = rtnl.const.RT_TABLE_MAIN;
|
||||
const AF_INET = rtnl.const.AF_INET;
|
||||
const AF_INET6 = rtnl.const.AF_INET6;
|
||||
|
||||
const ROUTE_FIELDS = ["dst", "gateway", "oif", "prefsrc", "priority",
|
||||
"scope", "type", "tos", "metrics"];
|
||||
|
||||
function is_default_route(route) {
|
||||
return (route.dst == null ||
|
||||
route.dst == "0.0.0.0/0" ||
|
||||
route.dst == "::/0");
|
||||
}
|
||||
|
||||
function build_route_for_table(route, tid, src_routing) {
|
||||
let r = { family: route.family, table: tid };
|
||||
for (let f in ROUTE_FIELDS)
|
||||
if (route[f] != null)
|
||||
r[f] = route[f];
|
||||
if (r.tos == 0) delete r.tos;
|
||||
if (src_routing && route.src != null)
|
||||
r.src = route.src;
|
||||
return r;
|
||||
}
|
||||
|
||||
let family_num = (ARGV[0] == "6") ? AF_INET6 : AF_INET;
|
||||
let family_name = (ARGV[0] == "6") ? "ipv6" : "ipv4";
|
||||
let table_id = +ARGV[1];
|
||||
let source_routing = +ARGV[2];
|
||||
|
||||
let cur = uci.cursor();
|
||||
cur.load("mwan3");
|
||||
|
||||
let extra_table_set = {};
|
||||
let rt_tables = cur.get("mwan3", "globals", "rt_table_lookup");
|
||||
if (type(rt_tables) == "array") {
|
||||
for (let t in rt_tables)
|
||||
extra_table_set[+t] = true;
|
||||
} else if (rt_tables != null) {
|
||||
extra_table_set[+rt_tables] = true;
|
||||
}
|
||||
|
||||
let name_tid = {};
|
||||
let tid = 0;
|
||||
cur.foreach("mwan3", "interface", function(s) {
|
||||
tid++;
|
||||
let fam = s.family ?? "ipv4";
|
||||
let enabled = +(s.enabled ?? "1");
|
||||
if (enabled && fam == family_name)
|
||||
name_tid[s[".name"]] = tid;
|
||||
});
|
||||
cur.unload("mwan3");
|
||||
|
||||
let dev_table_map = {};
|
||||
let uconn = ubus.connect();
|
||||
if (uconn) {
|
||||
let dump = uconn.call("network.interface", "dump");
|
||||
if (dump && dump.interface) {
|
||||
for (let intf in dump.interface) {
|
||||
let name = intf.interface;
|
||||
let t = name_tid[name];
|
||||
if (t == null) {
|
||||
let m = match(name, /^(.+)_([46])$/);
|
||||
if (m) {
|
||||
let suffix_fam = (m[2] == "4") ? "ipv4" : "ipv6";
|
||||
if (suffix_fam == family_name)
|
||||
t = name_tid[m[1]];
|
||||
}
|
||||
}
|
||||
if (t != null && intf.l3_device)
|
||||
dev_table_map[intf.l3_device] = t;
|
||||
}
|
||||
}
|
||||
uconn.disconnect();
|
||||
}
|
||||
|
||||
let all_routes = rtnl.request(RTM_GETROUTE, NLM_F_DUMP, { family: family_num }) ?? [];
|
||||
|
||||
let source_routes = [];
|
||||
for (let r in all_routes) {
|
||||
if (r.table == RT_TABLE_MAIN || extra_table_set[r.table])
|
||||
push(source_routes, r);
|
||||
}
|
||||
|
||||
let existing_keys = {};
|
||||
for (let r in all_routes) {
|
||||
if (r.table != table_id) continue;
|
||||
let key = (r.dst ?? "") + "|" + (r.oif ?? "") + "|" + (r.gateway ?? "") + "|" + (r.priority ?? "");
|
||||
existing_keys[key] = true;
|
||||
}
|
||||
|
||||
for (let route in source_routes) {
|
||||
let dev = route.oif;
|
||||
let target_tid = (dev != null) ? dev_table_map[dev] : null;
|
||||
|
||||
if (is_default_route(route) || route.dst == "fe80::/64") {
|
||||
if (target_tid != table_id) continue;
|
||||
} else if (target_tid != null && target_tid != table_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = (route.dst ?? "") + "|" + (route.oif ?? "") + "|" + (route.gateway ?? "") + "|" + (route.priority ?? "");
|
||||
if (existing_keys[key]) continue;
|
||||
|
||||
let r = build_route_for_table(route, table_id, source_routing);
|
||||
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));
|
||||
}
|
||||
51
mwan3-nft/files/lib/mwan3/mwan3-get-addr.uc
Normal file
51
mwan3-nft/files/lib/mwan3/mwan3-get-addr.uc
Normal file
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env ucode
|
||||
'use strict';
|
||||
|
||||
import * as rtnl from "rtnl";
|
||||
|
||||
const RTM_GETADDR = rtnl.const.RTM_GETADDR;
|
||||
const NLM_F_DUMP = rtnl.const.NLM_F_DUMP;
|
||||
const AF_INET = rtnl.const.AF_INET;
|
||||
const AF_INET6 = rtnl.const.AF_INET6;
|
||||
|
||||
let family_num = (ARGV[0] == "6") ? AF_INET6 : AF_INET;
|
||||
let device = ARGV[1];
|
||||
let prefix = ARGV[2];
|
||||
|
||||
let addrs = rtnl.request(RTM_GETADDR, NLM_F_DUMP, { family: family_num }) ?? [];
|
||||
|
||||
let result = null;
|
||||
|
||||
if (device != null && device != "" && prefix == null) {
|
||||
for (let a in addrs) {
|
||||
if (a.dev != device) continue;
|
||||
let addr = split(a.local ?? a.address ?? "", "/")[0];
|
||||
if (!addr) continue;
|
||||
if (family_num == AF_INET6) {
|
||||
if (match(addr, /^fe80:/)) continue;
|
||||
if (a.scope != 0) continue;
|
||||
} else {
|
||||
if (a.scope != 0 && a.scope != 253) continue;
|
||||
}
|
||||
result = addr;
|
||||
break;
|
||||
}
|
||||
} else if ((device == null || device == "") && prefix != null) {
|
||||
let pfx = prefix + ":";
|
||||
for (let a in addrs) {
|
||||
let addr = split(a.local ?? a.address ?? "", "/")[0];
|
||||
if (!addr) continue;
|
||||
if (a.scope != 0) continue;
|
||||
if (substr(addr, 0, length(pfx)) == pfx) {
|
||||
result = addr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
printf("%s\n", result);
|
||||
exit(0);
|
||||
} else {
|
||||
exit(1);
|
||||
}
|
||||
52
mwan3-nft/files/lib/mwan3/mwan3-list-routes.uc
Normal file
52
mwan3-nft/files/lib/mwan3/mwan3-list-routes.uc
Normal file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env ucode
|
||||
'use strict';
|
||||
|
||||
import * as rtnl from "rtnl";
|
||||
|
||||
const RTM_GETROUTE = rtnl.const.RTM_GETROUTE;
|
||||
const NLM_F_DUMP = rtnl.const.NLM_F_DUMP;
|
||||
const AF_INET = rtnl.const.AF_INET;
|
||||
const AF_INET6 = rtnl.const.AF_INET6;
|
||||
const RT_TABLE_MAIN = rtnl.const.RT_TABLE_MAIN;
|
||||
|
||||
function is_cidr_route(route, family_num) {
|
||||
let dst = route.dst;
|
||||
if (dst == null) return false;
|
||||
let slash = index(dst, "/");
|
||||
if (slash < 0) return false;
|
||||
let prefix_len = +substr(dst, slash + 1);
|
||||
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 ||
|
||||
match(route.dst, /^169\.254\./) != null));
|
||||
}
|
||||
|
||||
let family_num = (ARGV[0] == "6") ? AF_INET6 : AF_INET;
|
||||
let table_arg = ARGV[1];
|
||||
let table_num = (table_arg == "main") ? RT_TABLE_MAIN : +table_arg;
|
||||
|
||||
let routes = rtnl.request(RTM_GETROUTE, NLM_F_DUMP, { family: family_num }) ?? [];
|
||||
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_linklocal_route(route)) continue;
|
||||
if (family_num == AF_INET) {
|
||||
let first_octet = +split(route.dst, ".")[0];
|
||||
if (first_octet >= 224) continue;
|
||||
}
|
||||
if (seen[route.dst]) continue;
|
||||
seen[route.dst] = true;
|
||||
printf("%s\n", route.dst);
|
||||
}
|
||||
162
mwan3-nft/files/lib/mwan3/mwan3-manage-rules.uc
Normal file
162
mwan3-nft/files/lib/mwan3/mwan3-manage-rules.uc
Normal file
@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env ucode
|
||||
'use strict';
|
||||
|
||||
import * as rtnl from "rtnl";
|
||||
|
||||
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 NLM_F_DUMP = rtnl.const.NLM_F_DUMP;
|
||||
const NLM_F_CREATE = rtnl.const.NLM_F_CREATE;
|
||||
const AF_INET = rtnl.const.AF_INET;
|
||||
const AF_INET6 = rtnl.const.AF_INET6;
|
||||
|
||||
const FR_ACT_TO_TBL = 1;
|
||||
const FR_ACT_BLACKHOLE = 6;
|
||||
const FR_ACT_UNREACHABLE = 7;
|
||||
|
||||
function is_default_route(route) {
|
||||
return (route.dst == null ||
|
||||
route.dst == "0.0.0.0/0" ||
|
||||
route.dst == "::/0");
|
||||
}
|
||||
|
||||
let mode = ARGV[0];
|
||||
|
||||
if (mode == "check") {
|
||||
let family_num = (ARGV[1] == "6") ? AF_INET6 : AF_INET;
|
||||
let priorities = [];
|
||||
for (let i = 2; i < length(ARGV); i++)
|
||||
push(priorities, +ARGV[i]);
|
||||
|
||||
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;
|
||||
|
||||
let result = 0;
|
||||
for (let i = 0; i < length(priorities); i++)
|
||||
if (!existing[priorities[i]])
|
||||
result |= (1 << i);
|
||||
|
||||
printf("%d\n", result);
|
||||
|
||||
} else if (mode == "check-route") {
|
||||
let family_num = (ARGV[1] == "6") ? AF_INET6 : AF_INET;
|
||||
let table_id = +ARGV[2];
|
||||
let device = ARGV[3];
|
||||
|
||||
let routes = rtnl.request(RTM_GETROUTE, NLM_F_DUMP, { family: family_num }) ?? [];
|
||||
for (let r in routes) {
|
||||
if (r.table == table_id && is_default_route(r) && r.oif == device)
|
||||
exit(0);
|
||||
}
|
||||
exit(1);
|
||||
|
||||
} else if (mode == "delete-iface") {
|
||||
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 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.dst != null) continue;
|
||||
|
||||
if (rule.fwmark == null && rule.priority == iif_prio) {
|
||||
rtnl.request(RTM_DELRULE, 0, {
|
||||
family: family,
|
||||
priority: rule.priority,
|
||||
table: rule.table,
|
||||
action: rule.action
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: delete iif rule failed: %s\n", 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,
|
||||
fwmark: rule.fwmark,
|
||||
fwmask: rule.fwmask,
|
||||
table: rule.table,
|
||||
action: rule.action
|
||||
});
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (mode == "add-general") {
|
||||
let family_num = (ARGV[1] == "6") ? AF_INET6 : AF_INET;
|
||||
let bh_prio = +ARGV[2], bh_mark = +ARGV[3];
|
||||
let ur_prio = +ARGV[4], ur_mark = +ARGV[5];
|
||||
let mask = +ARGV[6];
|
||||
|
||||
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[bh_prio]) {
|
||||
rtnl.request(RTM_NEWRULE, NLM_F_CREATE, {
|
||||
family: family_num,
|
||||
priority: bh_prio,
|
||||
fwmark: bh_mark,
|
||||
fwmask: mask,
|
||||
action: FR_ACT_BLACKHOLE
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: add blackhole rule failed: %s\n", err));
|
||||
}
|
||||
|
||||
if (!existing[ur_prio]) {
|
||||
rtnl.request(RTM_NEWRULE, NLM_F_CREATE, {
|
||||
family: family_num,
|
||||
priority: ur_prio,
|
||||
fwmark: ur_mark,
|
||||
fwmask: mask,
|
||||
action: FR_ACT_UNREACHABLE
|
||||
});
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
warn(sprintf("mwan3-manage-rules: add unreachable rule failed: %s\n", err));
|
||||
}
|
||||
}
|
||||
81
mwan3-nft/files/lib/mwan3/mwan3-migrate-ipset-v4.sh
Normal file
81
mwan3-nft/files/lib/mwan3/mwan3-migrate-ipset-v4.sh
Normal file
@ -0,0 +1,81 @@
|
||||
#!/bin/sh
|
||||
# One-shot migration: scan /etc/config/mwan3 rules for ipset references and
|
||||
# emit config ipset sections into /etc/config/mwan3, copying family, loadfile
|
||||
# and inline entry values from /etc/config/firewall where available.
|
||||
# Safe to run multiple times -- skips sets that already have a mwan3 declaration.
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
LOG_TAG="mwan3-migrate-v4"
|
||||
|
||||
config_load mwan3
|
||||
|
||||
# Collect set names referenced by any mwan3 rule.
|
||||
referenced_sets=""
|
||||
_collect_refs()
|
||||
{
|
||||
local ipset_name ipset_src
|
||||
config_get ipset_name "$1" ipset
|
||||
config_get ipset_src "$1" ipset_src
|
||||
[ -n "$ipset_name" ] && referenced_sets="$referenced_sets $ipset_name"
|
||||
[ -n "$ipset_src" ] && referenced_sets="$referenced_sets $ipset_src"
|
||||
}
|
||||
config_foreach _collect_refs rule
|
||||
|
||||
[ -n "$referenced_sets" ] || exit 0
|
||||
|
||||
# Collect existing mwan3-side declarations for idempotency.
|
||||
existing=""
|
||||
_collect_existing()
|
||||
{
|
||||
local name
|
||||
config_get name "$1" name
|
||||
[ -n "$name" ] && existing="$existing $name"
|
||||
}
|
||||
config_foreach _collect_existing ipset
|
||||
|
||||
# For each referenced set not yet declared on the mwan3 side, pick up
|
||||
# family, loadfile and inline entries from /etc/config/firewall and emit
|
||||
# a section in /etc/config/mwan3.
|
||||
for name in $referenced_sets; do
|
||||
case " $existing " in *" $name "*) continue ;; esac
|
||||
|
||||
fw4_family=""
|
||||
fw4_loadfile=""
|
||||
fw4_section=""
|
||||
found=0
|
||||
_fw4_find()
|
||||
{
|
||||
local fname
|
||||
config_get fname "$1" name
|
||||
[ "$fname" = "$name" ] || return
|
||||
config_get fw4_family "$1" family ipv4
|
||||
config_get fw4_loadfile "$1" loadfile
|
||||
fw4_section="$1"
|
||||
found=1
|
||||
}
|
||||
reset_cb
|
||||
config_load firewall
|
||||
config_foreach _fw4_find ipset
|
||||
|
||||
if [ "$found" -eq 1 ]; then
|
||||
sid=$(uci -q add mwan3 ipset)
|
||||
uci -q set mwan3."$sid".name="$name"
|
||||
uci -q set mwan3."$sid".family="$fw4_family"
|
||||
[ -n "$fw4_loadfile" ] && uci -q set mwan3."$sid".loadfile="$fw4_loadfile"
|
||||
_add_entry() { uci -q add_list mwan3."$sid".entry="$1"; }
|
||||
config_list_foreach "$fw4_section" entry _add_entry
|
||||
logger -t "$LOG_TAG" \
|
||||
"added config ipset '$name' (family=$fw4_family) to /etc/config/mwan3"
|
||||
existing="$existing $name"
|
||||
else
|
||||
logger -t "$LOG_TAG" \
|
||||
"WARN: mwan3 rule references set '$name' not found in /etc/config/firewall; declare manually in /etc/config/mwan3 with name, family, and population source"
|
||||
fi
|
||||
|
||||
reset_cb
|
||||
config_load mwan3
|
||||
done
|
||||
|
||||
uci -q commit mwan3
|
||||
exit 0
|
||||
99
mwan3-nft/files/lib/mwan3/mwan3-skeleton.nft
Normal file
99
mwan3-nft/files/lib/mwan3/mwan3-skeleton.nft
Normal file
@ -0,0 +1,99 @@
|
||||
# mwan3 nftables framework (v4)
|
||||
# Standalone ruleset loaded by mwan3's init script via `nft -f` at
|
||||
# start_service, before mwan3_ensure_nft_framework. Defines mwan3's
|
||||
# own table (inet mwan3), its six address sets, its hook base chains
|
||||
# at mangle + 1 priority, and its regular chains.
|
||||
#
|
||||
# All runtime content (per-interface mwan3_iface_in_* chains, policy
|
||||
# chains, sticky-rule chains, setter chains, policy/rule content) is
|
||||
# added dynamically by the mwan3 shell scripts.
|
||||
#
|
||||
# The leading "table inet mwan3" + "delete table inet mwan3" pair is
|
||||
# the canonical idempotent atomic-replace idiom: the first statement
|
||||
# ensures the table exists (no-op if already present) so the delete
|
||||
# can succeed, the second wipes it, and the subsequent definition
|
||||
# block recreates it fresh. All three statements run as one atomic
|
||||
# nft transaction inside `nft -f`.
|
||||
|
||||
table inet mwan3
|
||||
delete table inet mwan3
|
||||
|
||||
table inet mwan3 {
|
||||
# Connected networks (populated by mwan3rtmon and mwan3_set_connected)
|
||||
set mwan3_connected_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
set mwan3_connected_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
|
||||
# Custom routing table networks
|
||||
set mwan3_custom_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
set mwan3_custom_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
|
||||
# Dynamic networks
|
||||
set mwan3_dynamic_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
set mwan3_dynamic_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
|
||||
# Entry point for prerouting
|
||||
# priority mangle + 1: runs AFTER fw4's main mangle and after pbr.
|
||||
# Order-independence with pbr is achieved by using non-destructive
|
||||
# masked save/restore via vmap-dispatch into per-mark OR-immediate
|
||||
# setter chains (mwan3_or_meta_*, mwan3_or_ct_*); see common.sh.
|
||||
chain mwan3_prerouting {
|
||||
type filter hook prerouting priority mangle + 1; policy accept;
|
||||
}
|
||||
|
||||
# Entry point for locally-originated traffic
|
||||
# type route so mark changes trigger re-routing
|
||||
chain mwan3_output {
|
||||
type route hook output priority mangle + 1; policy accept;
|
||||
}
|
||||
|
||||
# Interface input marking (jumped to from mwan3_prerouting/mwan3_output)
|
||||
chain mwan3_ifaces_in {
|
||||
}
|
||||
|
||||
# User-defined classification rules
|
||||
chain mwan3_rules {
|
||||
}
|
||||
|
||||
# Connected/custom/dynamic destination matching
|
||||
chain mwan3_connected {
|
||||
}
|
||||
chain mwan3_custom {
|
||||
}
|
||||
chain mwan3_dynamic {
|
||||
}
|
||||
|
||||
# IPv6 SNAT for router-originated traffic rerouted by mwan3_output. fw4 does
|
||||
# not masquerade IPv6 by default, so packets rerouted from WAN-A onto WAN-B
|
||||
# would egress with WAN-A's source prefix and be dropped upstream. Opt-in
|
||||
# per interface via the 'snat6' UCI option. Per-iface rules are added
|
||||
# dynamically by mwan3.sh and only fire when (oif == this WAN) AND
|
||||
# (mark == this WAN's mark) AND (saddr is local) AND (saddr != this WAN's
|
||||
# IP). Runs at srcnat - 1 so it fires before fw4's srcnat hook.
|
||||
chain mwan3_postrouting {
|
||||
type nat hook postrouting priority srcnat - 1; policy accept;
|
||||
}
|
||||
}
|
||||
2060
mwan3-nft/files/lib/mwan3/mwan3.sh
Normal file
2060
mwan3-nft/files/lib/mwan3/mwan3.sh
Normal file
File diff suppressed because it is too large
Load Diff
248
mwan3-nft/files/usr/sbin/mwan3
Executable file
248
mwan3-nft/files/usr/sbin/mwan3
Executable file
@ -0,0 +1,248 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /lib/functions.sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
. /lib/functions/network.sh
|
||||
. /lib/mwan3/mwan3.sh
|
||||
|
||||
command_help() {
|
||||
local cmd="$1"
|
||||
local help="$2"
|
||||
|
||||
echo "$(printf "%-25s%s" "${cmd}" "${help}")"
|
||||
}
|
||||
|
||||
help()
|
||||
{
|
||||
cat <<EOF
|
||||
Syntax: mwan3 [command]
|
||||
|
||||
Available commands:
|
||||
EOF
|
||||
command_help "start" "Load nftables rules, ip rules and ip routes"
|
||||
command_help "stop" "Unload nftables rules, ip rules and ip routes"
|
||||
command_help "restart" "Reload nftables rules, ip rules and ip routes"
|
||||
command_help "ifup <iface>" "Load rules and routes for specific interface"
|
||||
command_help "ifdown <iface>" "Unload rules and routes for specific interface"
|
||||
command_help "interfaces" "Show interfaces status"
|
||||
command_help "policies" "Show currently active policy"
|
||||
command_help "connected" "Show directly connected networks"
|
||||
command_help "rules" "Show active rules"
|
||||
command_help "status" "Show all status"
|
||||
command_help "internal <ipv4|ipv6>" "Show internal configuration <default: ipv4>"
|
||||
command_help "use <iface> <cmd>" "Run a command bound to <iface> and avoid mwan3 rules"
|
||||
}
|
||||
|
||||
ifdown() {
|
||||
if [ -z "$1" ]; then
|
||||
echo "Error: Expecting interface. Usage: mwan3 ifdown <interface>"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "$2" ]; then
|
||||
echo "Error: Too many arguments. Usage: mwan3 ifdown <interface>"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mwan3_interface_hotplug_shutdown "$1" 1
|
||||
}
|
||||
|
||||
ifup() {
|
||||
. /etc/init.d/mwan3
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Expecting interface. Usage: mwan3 ifup <interface>"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "$2" ]; then
|
||||
echo "Too many arguments. Usage: mwan3 ifup <interface>"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mwan3_ifup "$1" "cmd"
|
||||
}
|
||||
|
||||
interfaces()
|
||||
{
|
||||
config_load mwan3
|
||||
|
||||
echo "Interface status:"
|
||||
config_foreach mwan3_report_iface_status interface
|
||||
echo
|
||||
}
|
||||
|
||||
policies()
|
||||
{
|
||||
echo "Current policies:"
|
||||
mwan3_report_policies_v4
|
||||
echo
|
||||
}
|
||||
|
||||
connected()
|
||||
{
|
||||
echo "Directly connected ipv4 networks:"
|
||||
mwan3_report_connected_v4
|
||||
echo
|
||||
[ $NO_IPV6 -ne 0 ] && return
|
||||
echo "Directly connected ipv6 networks:"
|
||||
mwan3_report_connected_v6
|
||||
echo
|
||||
}
|
||||
|
||||
rules()
|
||||
{
|
||||
echo "Active user rules:"
|
||||
mwan3_report_rules_v4
|
||||
echo
|
||||
}
|
||||
|
||||
status()
|
||||
{
|
||||
interfaces
|
||||
policies
|
||||
connected
|
||||
rules
|
||||
}
|
||||
|
||||
internal()
|
||||
{
|
||||
local family="$1"
|
||||
local dash="-------------------------------------------------"
|
||||
|
||||
if [ -f "/etc/openwrt_release" ]; then
|
||||
. /etc/openwrt_release
|
||||
fi
|
||||
|
||||
local ip output
|
||||
|
||||
if [ "$family" = "ipv6" ]; then
|
||||
ip="$IP6"
|
||||
else
|
||||
ip="$IP4"
|
||||
fi
|
||||
|
||||
echo "Software-Version"
|
||||
echo "$dash"
|
||||
|
||||
local mwan3_ver
|
||||
mwan3_ver=$(apk list -I 2>/dev/null | grep '^mwan3-' | head -1 | sed 's/^mwan3-//; s/ .*//')
|
||||
[ -z "$mwan3_ver" ] && mwan3_ver="unknown"
|
||||
echo "mwan3 - $mwan3_ver"
|
||||
|
||||
echo ""
|
||||
echo "Output of \"$ip a show\""
|
||||
echo "$dash"
|
||||
output="$($ip a show)"
|
||||
if [ "$output" != "" ]; then
|
||||
echo "$output"
|
||||
else
|
||||
echo "No data found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Output of \"$ip route show\""
|
||||
echo "$dash"
|
||||
output="$($ip route show)"
|
||||
if [ "$output" != "" ]; then
|
||||
echo "$output"
|
||||
else
|
||||
echo "No data found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Output of \"$ip rule show\""
|
||||
echo "$dash"
|
||||
output="$($ip rule show)"
|
||||
if [ "$output" != "" ]; then
|
||||
echo "$output"
|
||||
else
|
||||
echo "No data found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Output of \"$ip route list table 1-250\""
|
||||
echo "$dash"
|
||||
local dump=0
|
||||
for i in $(seq 1 250); do
|
||||
output=$($ip route list table $i 2>/dev/null)
|
||||
if [ "$output" != "" ];then
|
||||
dump=1
|
||||
echo "Routing table $i:"
|
||||
echo "$output"
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
if [ "$dump" = "0" ]; then
|
||||
echo "No data found"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Output of \"nft list table inet mwan3\" (mwan3 chains)"
|
||||
echo "$dash"
|
||||
output=$($NFT list table inet mwan3 2>/dev/null | grep -A 100 'chain mwan3_')
|
||||
if [ "$output" != "" ]; then
|
||||
echo "$output"
|
||||
else
|
||||
echo "No data found"
|
||||
fi
|
||||
}
|
||||
|
||||
start() {
|
||||
/etc/init.d/mwan3 enable
|
||||
/etc/init.d/mwan3 start
|
||||
}
|
||||
|
||||
stop() {
|
||||
/etc/init.d/mwan3 disable
|
||||
/etc/init.d/mwan3 stop
|
||||
}
|
||||
|
||||
restart() {
|
||||
/etc/init.d/mwan3 enable
|
||||
/etc/init.d/mwan3 stop
|
||||
/etc/init.d/mwan3 start
|
||||
}
|
||||
|
||||
use() {
|
||||
# Run a command with the device, src_ip and fwmark set to avoid processing by mwan3
|
||||
# firewall rules
|
||||
|
||||
local interface device src_ip family
|
||||
|
||||
interface=$1 ; shift
|
||||
[ -z "$*" ] && echo "no command specified for mwan3 use" >&2 && return
|
||||
network_get_device device $interface
|
||||
[ -z "$device" ] && echo "could not find device for $interface" >&2 && return
|
||||
|
||||
mwan3_get_src_ip src_ip $interface
|
||||
[ -z "$src_ip" ] && echo "could not find src_ip for $interface" >&2 && return
|
||||
|
||||
config_get family $interface family
|
||||
[ -z "$family" ] && echo "could not find family for $interface. Using ipv4." >&2 && family='ipv4'
|
||||
|
||||
echo "Running '$*' with DEVICE=$device SRCIP=$src_ip FWMARK=$MMX_DEFAULT FAMILY=$family" >&2
|
||||
# if a program (not a shell builtin) is run: use "exec" for allowing direct process control
|
||||
if [ -x "$(command -v "$1")" ]; then
|
||||
set -- exec "$@"
|
||||
fi
|
||||
FAMILY=$family DEVICE=$device SRCIP=$src_ip FWMARK=$MMX_DEFAULT LD_PRELOAD=/lib/mwan3/libwrap_mwan3_sockopt.so.1.0 "$@"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
ifup|ifdown|interfaces|policies|connected|rules|status|start|stop|restart|internal)
|
||||
mwan3_init
|
||||
"$@"
|
||||
;;
|
||||
use)
|
||||
mwan3_init
|
||||
"$@"
|
||||
# Propagate mwan3 use command exit code
|
||||
exit "$?"
|
||||
;;
|
||||
*)
|
||||
help
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
426
mwan3-nft/files/usr/sbin/mwan3-diag
Normal file
426
mwan3-nft/files/usr/sbin/mwan3-diag
Normal file
@ -0,0 +1,426 @@
|
||||
#!/usr/bin/ucode
|
||||
// mwan3 comprehensive diagnostic script
|
||||
// Run with: ucode /tmp/mwan3-diag.uc
|
||||
//
|
||||
// Collects network state relevant to mwan3 debugging.
|
||||
// Public routable IPv4 and IPv6 addresses are replaced with PUB4_N / PUB6_N
|
||||
// placeholders throughout the entire output so the report can be posted
|
||||
// publicly. The same physical address always receives the same placeholder,
|
||||
// so cross-references between routing tables, neighbor cache, nft rules,
|
||||
// configuration and log entries remain internally consistent.
|
||||
//
|
||||
// Link-local (fe80::), ULA (fc00::/7), RFC1918, loopback and multicast
|
||||
// addresses are left unchanged -- they are unroutable and diagnostically
|
||||
// important.
|
||||
|
||||
import * as rtnl from 'rtnl';
|
||||
import * as sock from 'socket';
|
||||
import * as ubus from 'ubus';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Address classification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function is_public_v6(addr) {
|
||||
if (!addr || addr == '::' || addr == '::1') return false;
|
||||
let a = lc(addr);
|
||||
if (match(a, /^fe[89ab][0-9a-f]:/)) return false; // link-local fe80::/10
|
||||
if (match(a, /^f[cd]/)) return false; // ULA fc00::/7
|
||||
if (match(a, /^ff/)) return false; // multicast ff00::/8
|
||||
return true;
|
||||
}
|
||||
|
||||
function is_public_v4(addr) {
|
||||
if (!addr) return false;
|
||||
let p = split(addr, '.');
|
||||
if (length(p) != 4) return false;
|
||||
let a = +p[0], b = +p[1];
|
||||
if (a == 0) return false; // "this network" (RFC 1122)
|
||||
if (a == 127) return false; // loopback
|
||||
if (a == 10) return false; // RFC1918
|
||||
if (a == 172 && b >= 16 && b <= 31) return false; // RFC1918
|
||||
if (a == 192 && b == 168) return false; // RFC1918
|
||||
if (a == 169 && b == 254) return false; // link-local
|
||||
if (a >= 224) return false; // multicast + reserved
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Address normalisation via sock.sockaddr()
|
||||
// Returns canonical string form, or null on failure.
|
||||
// Strips any /prefix-length before parsing.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function norm_addr(addr_str) {
|
||||
if (!addr_str) return null;
|
||||
let sa = sock.sockaddr(split(addr_str, '/')[0]);
|
||||
if (!sa || (sa.family != 2 && sa.family != 10)) return null;
|
||||
return sa.address;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 1: collect all structured data via rtnl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let nb_all = rtnl.request(rtnl.const.RTM_GETNEIGH, rtnl.const.NLM_F_DUMP, {});
|
||||
let rt6_all = rtnl.request(rtnl.const.RTM_GETROUTE, rtnl.const.NLM_F_DUMP, { family: 10 });
|
||||
let rt4_all = rtnl.request(rtnl.const.RTM_GETROUTE, rtnl.const.NLM_F_DUMP, { family: 2 });
|
||||
let rul6_all = rtnl.request(rtnl.const.RTM_GETRULE, rtnl.const.NLM_F_DUMP, { family: 10 });
|
||||
let rul4_all = rtnl.request(rtnl.const.RTM_GETRULE, rtnl.const.NLM_F_DUMP, { family: 2 });
|
||||
let addr_all = rtnl.request(rtnl.const.RTM_GETADDR, rtnl.const.NLM_F_DUMP, {});
|
||||
|
||||
// mwan3 interface status via ubus
|
||||
let ub = ubus.connect();
|
||||
let mwan3_st = ub ? ub.call('mwan3', 'status', { section: 'interfaces' }) : null;
|
||||
|
||||
// mwan3 UCI config
|
||||
let mwan3_cfg = null;
|
||||
{
|
||||
let f = fs.open('/etc/config/mwan3', 'r');
|
||||
if (f) { mwan3_cfg = f.read('all'); f.close(); }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 2: extract every address that appears anywhere and classify it
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let pub6_seen = {};
|
||||
let pub4_seen = {};
|
||||
|
||||
function register(addr_str) {
|
||||
if (!addr_str) return;
|
||||
let norm = norm_addr(addr_str);
|
||||
if (!norm) return;
|
||||
let sa = sock.sockaddr(split(addr_str, '/')[0]);
|
||||
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;
|
||||
}
|
||||
|
||||
for (let n in nb_all) { register(n.dst); }
|
||||
for (let r in rt6_all) { register(r.dst); register(r.gateway); register(r.src); register(r.prefsrc); }
|
||||
for (let r in rt4_all) { register(r.dst); register(r.gateway); register(r.src); register(r.prefsrc); }
|
||||
for (let r in rul6_all) { register(r.src); register(r.dst); }
|
||||
for (let r in rul4_all) { register(r.src); register(r.dst); }
|
||||
for (let a in addr_all) { register(a.address); register(a.local); }
|
||||
if (mwan3_st?.interfaces)
|
||||
for (let iface, idata in mwan3_st.interfaces)
|
||||
for (let tip in (idata.track_ip || []))
|
||||
register(tip.ip);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Strip elements from user-defined nft sets, replacing with { ... }.
|
||||
// mwan3 infrastructure sets (names starting with mwan3_) are left intact:
|
||||
// their IPs derive from the routing table and are already in addr_map.
|
||||
// All other sets (user ipsets in the mwan3 table, fw4 sets) may contain
|
||||
// arbitrary IPs not captured in phase 2 and have their elements redacted.
|
||||
function strip_set_elements(text) {
|
||||
let lines = split(text, '\n');
|
||||
let out = [];
|
||||
let skip = false;
|
||||
let user_set = false;
|
||||
for (let line in lines) {
|
||||
if (skip) {
|
||||
if (index(line, '}') != -1) skip = false;
|
||||
continue;
|
||||
}
|
||||
// Detect set declaration: trimmed line is 'set <name> {'
|
||||
let trimmed = trim(line);
|
||||
if (substr(trimmed, 0, 4) == 'set ' && substr(trimmed, length(trimmed) - 2) == ' {') {
|
||||
let setname = substr(trimmed, 4, length(trimmed) - 6);
|
||||
user_set = (substr(setname, 0, 6) != 'mwan3_');
|
||||
}
|
||||
if (user_set) {
|
||||
let ei = index(line, 'elements = {');
|
||||
if (ei != -1) {
|
||||
let after = substr(line, ei + length('elements = {'));
|
||||
push(out, substr(line, 0, ei) + 'elements = { ... }');
|
||||
if (index(after, '}') == -1) skip = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
push(out, line);
|
||||
}
|
||||
return join('\n', out);
|
||||
}
|
||||
|
||||
// Run a shell command and return its anonymised output.
|
||||
function pcmd(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');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Named constants for formatting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let NUD = { '1':'INCOMPLETE', '2':'REACHABLE', '4':'STALE', '8':'DELAY',
|
||||
'16':'PROBE', '32':'FAILED', '64':'NOARP', '128':'PERMANENT' };
|
||||
let PROTO = { '2':'kernel', '3':'boot', '4':'static', '11':'dhcp',
|
||||
'186':'bgp', '187':'isis', '188':'ospf', '189':'rip' };
|
||||
|
||||
function nud_name(s) { return NUD[s] || sprintf('state=0x%x', s); }
|
||||
function proto_name(p) { return PROTO[p] || sprintf('proto=%d', p); }
|
||||
|
||||
let TBL_NAMES = { '253': 'default', '254': 'main' };
|
||||
function tbl_label(n) {
|
||||
let name = TBL_NAMES[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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let mwan3_ver = 'unknown';
|
||||
{
|
||||
let f = fs.popen("apk list --installed 2>/dev/null | grep '^mwan3'");
|
||||
let l = trim(f?.read('line') ?? '');
|
||||
f?.close();
|
||||
let parts = length(l) ? split(l, ' ') : [];
|
||||
if (length(parts)) mwan3_ver = parts[0];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
printf('=== mwan3 Diagnostic Report ===\n');
|
||||
printf('Timestamp : %s', fs.popen('date')?.read('line') ?? '\n');
|
||||
printf('Version : %s\n', mwan3_ver);
|
||||
printf('Table : %s\n', mwan3_tbl);
|
||||
printf('\n');
|
||||
printf('NOTE: Public routable addresses are replaced with PUB4_N / PUB6_N\n');
|
||||
printf(' placeholders. The same address always gets the same placeholder\n');
|
||||
printf(' so cross-references within this report remain consistent.\n');
|
||||
printf(' fe80:: link-local, ULA (fc00::/7), RFC1918 and multicast are shown as-is.\n');
|
||||
printf('\n');
|
||||
|
||||
// --- IPv6 interface addresses ---
|
||||
printf('=== IPv6 Interface Addresses ===\n');
|
||||
for (let a in addr_all) {
|
||||
if (a.family != 10 || a.dev == 'lo') continue;
|
||||
let scope = (a.scope == 253) ? ' scope link' : (a.scope == 254) ? ' scope host' : '';
|
||||
printf(' %-16s %s%s\n', a.dev, anon(a.address), scope);
|
||||
}
|
||||
printf('\n');
|
||||
|
||||
// --- IPv4 interface addresses ---
|
||||
printf('=== IPv4 Interface Addresses ===\n');
|
||||
for (let a in addr_all) {
|
||||
if (a.family != 2 || a.dev == 'lo') continue;
|
||||
printf(' %-16s %s\n', a.dev, anon(a.address));
|
||||
}
|
||||
printf('\n');
|
||||
|
||||
// --- IPv6 routing tables (all tables except local/unspec) ---
|
||||
{
|
||||
let tbls = {};
|
||||
for (let r in rt6_all) if (r.table && r.table != 255) tbls[r.table] = 1;
|
||||
let tlist = sort(keys(tbls), (a, b) => +a - +b);
|
||||
printf('=== IPv6 Routing Tables ===\n');
|
||||
for (let ts in tlist) {
|
||||
let tn = +ts;
|
||||
printf('-- %s --\n', tbl_label(tn));
|
||||
for (let r in rt6_all) {
|
||||
if (r.table != tn) continue;
|
||||
let dst = anon(r.dst) ?? 'default';
|
||||
let src = r.src ? sprintf(' from %s', anon(r.src)) : '';
|
||||
let via = r.gateway ? sprintf(' via %s', anon(r.gateway)) : '';
|
||||
let dev = r.oif ? sprintf(' dev %s', r.oif) : '';
|
||||
let metric = r.priority ? sprintf(' metric %d', r.priority) : '';
|
||||
printf(' %s%s%s%s proto %s%s\n', dst, src, via, dev, proto_name(r.protocol), metric);
|
||||
}
|
||||
printf('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// --- IPv4 routing tables (all tables except local/unspec) ---
|
||||
{
|
||||
let tbls = {};
|
||||
for (let r in rt4_all) if (r.table && r.table != 255) tbls[r.table] = 1;
|
||||
let tlist = sort(keys(tbls), (a, b) => +a - +b);
|
||||
printf('=== IPv4 Routing Tables ===\n');
|
||||
for (let ts in tlist) {
|
||||
let tn = +ts;
|
||||
printf('-- %s --\n', tbl_label(tn));
|
||||
for (let r in rt4_all) {
|
||||
if (r.table != tn) continue;
|
||||
let dst = anon(r.dst) ?? 'default';
|
||||
let via = r.gateway ? sprintf(' via %s', anon(r.gateway)) : '';
|
||||
let dev = r.oif ? sprintf(' dev %s', r.oif) : '';
|
||||
let metric = r.priority ? sprintf(' metric %d', r.priority) : '';
|
||||
printf(' %s%s%s proto %s%s\n', dst, via, dev, proto_name(r.protocol), metric);
|
||||
}
|
||||
printf('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// rtnl does not decode FRA_IIFNAME; parse text output to get iif per priority.
|
||||
function parse_iif(family_flag) {
|
||||
let m = {};
|
||||
let f = fs.popen(sprintf('ip -%s rule list 2>/dev/null', family_flag));
|
||||
if (!f) return m;
|
||||
for (let line = f.read('line'); length(line); line = f.read('line')) {
|
||||
let parts = split(trim(line), /\s+/);
|
||||
let prio = +replace(parts[0], ':', '');
|
||||
for (let i = 1; i + 1 < length(parts); i++) {
|
||||
if (parts[i] == 'iif') { m[prio] = parts[i + 1]; break; }
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
return m;
|
||||
}
|
||||
|
||||
function fmt_rule(r, iif_map) {
|
||||
let src = r.src ? sprintf(' from %s', anon(r.src)) : ' from all';
|
||||
let iif = iif_map[r.priority] ? sprintf(' iif %s', iif_map[r.priority]) : '';
|
||||
let fwmark = r.fwmark ? sprintf(' fwmark 0x%x/0x%x', r.fwmark, r.fwmask) : '';
|
||||
let action;
|
||||
if (r.action == 6) action = 'blackhole';
|
||||
else if (r.action == 7) action = 'unreachable';
|
||||
else if (r.action == 8) action = 'prohibit';
|
||||
else action = sprintf('lookup %s', r.table);
|
||||
printf(' %-12s%s%s%s %s\n', (r.priority ?? 0) + ':', src, iif, fwmark, action);
|
||||
}
|
||||
|
||||
let iif6_map = parse_iif('6');
|
||||
let iif4_map = parse_iif('4');
|
||||
|
||||
// --- IPv6 policy rules ---
|
||||
printf('=== IPv6 Policy Rules ===\n');
|
||||
for (let r in rul6_all) fmt_rule(r, iif6_map);
|
||||
printf('\n');
|
||||
|
||||
// --- IPv4 policy rules ---
|
||||
printf('=== IPv4 Policy Rules ===\n');
|
||||
for (let r in rul4_all) fmt_rule(r, iif4_map);
|
||||
printf('\n');
|
||||
|
||||
// --- IPv6 neighbor cache ---
|
||||
printf('=== IPv6 Neighbor Cache ===\n');
|
||||
for (let n in nb_all) {
|
||||
if (n.family != 10 || n.dev == 'lo') continue;
|
||||
let mac = n.lladdr ? sprintf(' lladdr %s', n.lladdr) : '';
|
||||
printf(' %-40s dev %-16s%s %s\n', anon(n.dst), n.dev, mac, nud_name(n.state));
|
||||
}
|
||||
printf('\n');
|
||||
|
||||
// --- IPv4 neighbor cache ---
|
||||
printf('=== IPv4 Neighbor Cache ===\n');
|
||||
for (let n in nb_all) {
|
||||
if (n.family != 2 || n.dev == 'lo') continue;
|
||||
let mac = n.lladdr ? sprintf(' lladdr %s', n.lladdr) : '';
|
||||
printf(' %-18s dev %-16s%s %s\n', anon(n.dst), n.dev, mac, nud_name(n.state));
|
||||
}
|
||||
printf('\n');
|
||||
|
||||
// --- mwan3 status ---
|
||||
printf('=== mwan3 Interface Status ===\n');
|
||||
if (mwan3_st?.interfaces) {
|
||||
for (let iface, d in mwan3_st.interfaces) {
|
||||
printf(' %-14s status=%-12s score=%d tracking=%s\n',
|
||||
iface, d.status, d.score, d.tracking);
|
||||
for (let tip in (d.track_ip || []))
|
||||
printf(' track %-30s status=%-8s latency=%dms loss=%d%%\n',
|
||||
anon(tip.ip), tip.status, tip.latency, tip.packetloss);
|
||||
}
|
||||
} else {
|
||||
printf(' (mwan3 not running or ubus call failed)\n');
|
||||
}
|
||||
printf('\n');
|
||||
|
||||
// --- mwan3 UCI config ---
|
||||
printf('=== mwan3 Configuration (/etc/config/mwan3) ===\n');
|
||||
if (mwan3_cfg)
|
||||
printf('%s\n', anon_text(mwan3_cfg));
|
||||
else
|
||||
printf(' (not found)\n\n');
|
||||
|
||||
// --- 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))));
|
||||
|
||||
// --- 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('=== fw4 mangle_output chain ===\n');
|
||||
printf('%s\n', pcmd('nft list chain inet fw4 mangle_output 2>/dev/null'));
|
||||
|
||||
printf('=== fw4 output chain ===\n');
|
||||
printf('%s\n', pcmd('nft list chain inet fw4 output 2>/dev/null'));
|
||||
|
||||
// --- 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('=== End of Report ===\n');
|
||||
739
mwan3-nft/files/usr/sbin/mwan3-lb-test
Executable file
739
mwan3-nft/files/usr/sbin/mwan3-lb-test
Executable file
@ -0,0 +1,739 @@
|
||||
#!/bin/sh
|
||||
|
||||
# mwan3-lb-test - Load balancing distribution verifier
|
||||
#
|
||||
# Verifies that a mwan3 load-balancing policy distributes traffic according
|
||||
# to its configured member weights. Inserts temporary nft counter rules,
|
||||
# generates a test command using mwan3 tracking IPs, waits for the user to
|
||||
# run it, then reports per-member actual vs expected hit counts.
|
||||
#
|
||||
# Usage: mwan3-lb-test [-6] -c <client_ip> <policy_name> [ip1 ip2 ...]
|
||||
# mwan3-lb-test cleanup
|
||||
#
|
||||
# -6 Test IPv6 traffic distribution instead of IPv4 (default).
|
||||
#
|
||||
# -c <client_ip>
|
||||
# IP address of the LAN client that will run the test pings
|
||||
# (required). Used to add isolation rules that block pings to
|
||||
# the test destination IPs from all other LAN clients and from
|
||||
# the router itself, preventing counter contamination.
|
||||
#
|
||||
# cleanup Remove any stale mwan3_lb_test_* sets and associated rules
|
||||
# left behind by a run that was killed before it could clean up.
|
||||
#
|
||||
# Optional IP arguments override the default set of well-known public IPs
|
||||
# used as ping destinations. Provide at least two distinct IPs. Use this
|
||||
# when the well-known defaults are not reachable from your WAN links.
|
||||
#
|
||||
# Run on the router as root.
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
TABLE="inet mwan3"
|
||||
FORWARD_TABLE="inet fw4"
|
||||
MWAN3TRACK_STATUS_DIR="/var/run/mwan3track"
|
||||
MMX_MASK=$(uci -q get mwan3.globals.mmx_mask 2>/dev/null || echo "0x3f00")
|
||||
HANDLES=""
|
||||
TEST_RULE_HANDLE=""
|
||||
FORWARD_BLOCK_HANDLE=""
|
||||
OUTPUT_ISOLATION_HANDLE=""
|
||||
TEST_SET="mwan3_lb_test_$$"
|
||||
IPV6=0
|
||||
CLIENT_IP=""
|
||||
POLICY=""
|
||||
MEMBERS=""
|
||||
MEMBER_MARKS=""
|
||||
MEMBER_WEIGHTS=""
|
||||
MEMBER_IFACES=""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Utilities
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
cleanup
|
||||
exit 1
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo "WARNING: $*" >&2
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
# Remove isolation rules
|
||||
if [ -n "$OUTPUT_ISOLATION_HANDLE" ]; then
|
||||
nft delete rule $TABLE mwan3_output handle "$OUTPUT_ISOLATION_HANDLE" 2>/dev/null
|
||||
OUTPUT_ISOLATION_HANDLE=""
|
||||
fi
|
||||
if [ -n "$FORWARD_BLOCK_HANDLE" ]; then
|
||||
nft delete rule $FORWARD_TABLE forward handle "$FORWARD_BLOCK_HANDLE" 2>/dev/null
|
||||
FORWARD_BLOCK_HANDLE=""
|
||||
fi
|
||||
# Remove temporary test rule from mwan3_rules
|
||||
if [ -n "$TEST_RULE_HANDLE" ]; then
|
||||
nft delete rule $TABLE mwan3_rules handle "$TEST_RULE_HANDLE" 2>/dev/null
|
||||
TEST_RULE_HANDLE=""
|
||||
fi
|
||||
# Remove temporary destination sets (one per table)
|
||||
nft delete set $TABLE "$TEST_SET" 2>/dev/null
|
||||
nft delete set $FORWARD_TABLE "$TEST_SET" 2>/dev/null
|
||||
# Remove diagnostic counter rules from policy chain
|
||||
[ -z "$HANDLES" ] && return
|
||||
for h in $HANDLES; do
|
||||
nft delete rule $TABLE "mwan3_policy_${POLICY}" handle "$h" 2>/dev/null
|
||||
done
|
||||
HANDLES=""
|
||||
}
|
||||
|
||||
trap 'echo; echo "Interrupted - cleaning up..."; cleanup; exit 1' INT TERM PIPE
|
||||
|
||||
# Append handle to HANDLES list
|
||||
add_handle() {
|
||||
if [ -z "$HANDLES" ]; then
|
||||
HANDLES="$1"
|
||||
else
|
||||
HANDLES="$HANDLES $1"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cleanup-only invocation: mwan3-lb-test cleanup
|
||||
#
|
||||
# Removes any stale mwan3_lb_test_* sets and associated rules left behind by
|
||||
# an aborted run (e.g. killed with SIGKILL before cleanup() could run).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if [ "$1" = "cleanup" ]; then
|
||||
found=0
|
||||
for stale_set in $(nft list sets inet 2>/dev/null \
|
||||
| awk '/mwan3_lb_test_/{gsub(/.*mwan3_lb_test_/,"mwan3_lb_test_"); gsub(/ \{.*/,""); print}'); do
|
||||
found=1
|
||||
echo "Removing stale set: $stale_set"
|
||||
stale_fwd=$(nft -a list chain $FORWARD_TABLE forward 2>/dev/null \
|
||||
| awk "/@${stale_set}.*drop/{print \$NF; exit}")
|
||||
[ -n "$stale_fwd" ] && {
|
||||
echo " Removing forward isolation rule (handle $stale_fwd)"
|
||||
nft delete rule $FORWARD_TABLE forward handle "$stale_fwd" 2>/dev/null
|
||||
}
|
||||
stale_out=$(nft -a list chain $TABLE mwan3_output 2>/dev/null \
|
||||
| awk "/@${stale_set}.*return/{print \$NF; exit}")
|
||||
[ -n "$stale_out" ] && {
|
||||
echo " Removing output isolation rule (handle $stale_out)"
|
||||
nft delete rule $TABLE mwan3_output handle "$stale_out" 2>/dev/null
|
||||
}
|
||||
stale_handle=$(nft -a list chain $TABLE mwan3_rules 2>/dev/null \
|
||||
| awk "/@${stale_set}/{print \$NF; exit}")
|
||||
[ -n "$stale_handle" ] && {
|
||||
echo " Removing mwan3_rules test rule (handle $stale_handle)"
|
||||
nft delete rule $TABLE mwan3_rules handle "$stale_handle" 2>/dev/null
|
||||
}
|
||||
nft delete set $TABLE "$stale_set" 2>/dev/null
|
||||
nft delete set $FORWARD_TABLE "$stale_set" 2>/dev/null
|
||||
done
|
||||
[ "$found" -eq 0 ] && echo "Nothing to clean up."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-6) IPV6=1; shift ;;
|
||||
-c)
|
||||
[ -z "$2" ] && { echo "ERROR: -c requires an IP address argument" >&2; exit 1; }
|
||||
CLIENT_IP="$2"; shift 2 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 [-6] -c <client_ip> <policy_name> [ip1 ip2 ...]"
|
||||
echo " $0 cleanup"
|
||||
echo
|
||||
echo " -6 Test IPv6 traffic distribution (default: IPv4)"
|
||||
echo " -c <client_ip> IP address of the LAN client running the test pings (required)"
|
||||
echo " cleanup Remove stale rules/sets from an aborted run"
|
||||
echo
|
||||
echo " Optional IP arguments override the default well-known public IPs."
|
||||
echo
|
||||
echo "Available load-balancing policies:"
|
||||
nft list chains inet 2>/dev/null \
|
||||
| awk '/mwan3_policy_/{gsub(/.*mwan3_policy_/,""); gsub(/ \{.*/,""); print}' \
|
||||
| sort -u \
|
||||
| while read -r name; do
|
||||
nft list chain inet mwan3 "mwan3_policy_${name}" 2>/dev/null \
|
||||
| grep -q "numgen" && echo " $name"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ -z "$CLIENT_IP" ] && die "client IP is required - use -c <ip>"
|
||||
|
||||
POLICY="$1"
|
||||
shift
|
||||
CHAIN="mwan3_policy_${POLICY}"
|
||||
CUSTOM_IPS="$*"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-flight: chain exists
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
nft list chain $TABLE "$CHAIN" >/dev/null 2>&1 \
|
||||
|| die "chain $CHAIN not found - is mwan3 running?"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-flight: chain contains a numgen rule (load-balancing policy)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NUMGEN_LINE=$(nft list chain $TABLE "$CHAIN" 2>/dev/null | grep "numgen")
|
||||
[ -n "$NUMGEN_LINE" ] \
|
||||
|| die "policy $POLICY has no numgen rule - it is not a load-balancing policy (single active member or all members offline)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-flight: read members and weights from UCI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
config_load mwan3
|
||||
|
||||
collect_member() {
|
||||
local member="$1"
|
||||
local iface weight
|
||||
config_get iface "$member" interface ""
|
||||
config_get weight "$member" weight 1
|
||||
[ -z "$iface" ] && return
|
||||
MEMBERS="$MEMBERS $member"
|
||||
MEMBER_IFACES="$MEMBER_IFACES $iface"
|
||||
MEMBER_WEIGHTS="$MEMBER_WEIGHTS $weight"
|
||||
}
|
||||
|
||||
config_list_foreach "$POLICY" use_member collect_member
|
||||
|
||||
MEMBERS="${MEMBERS# }"
|
||||
MEMBER_IFACES="${MEMBER_IFACES# }"
|
||||
MEMBER_WEIGHTS="${MEMBER_WEIGHTS# }"
|
||||
|
||||
[ -z "$MEMBERS" ] && die "policy $POLICY has no members in UCI config"
|
||||
|
||||
MEMBER_COUNT=$(echo "$MEMBER_WEIGHTS" | wc -w)
|
||||
[ "$MEMBER_COUNT" -ge 2 ] || die "policy $POLICY has fewer than two members in UCI config"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-flight: parse member marks from live numgen vmap
|
||||
#
|
||||
# The vmap entries are in member order, matching UCI use_member list order.
|
||||
# Each entry jumps to mwan3_or_meta_0xMARK; extract MARK from chain name.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
MEMBER_MARKS=$(echo "$NUMGEN_LINE" \
|
||||
| grep -oE 'mwan3_or_meta_0x[0-9a-f]+' \
|
||||
| sed 's/mwan3_or_meta_//')
|
||||
|
||||
MARK_COUNT=$(echo "$MEMBER_MARKS" | wc -w)
|
||||
|
||||
# For mixed IPv4/IPv6 policies there may be two numgen lines; marks may repeat.
|
||||
# Deduplicate while preserving order.
|
||||
MEMBER_MARKS=$(echo "$MEMBER_MARKS" | tr ' ' '\n' | awk '!seen[$0]++' | tr '\n' ' ')
|
||||
MEMBER_MARKS="${MEMBER_MARKS% }"
|
||||
MARK_COUNT=$(echo "$MEMBER_MARKS" | wc -w)
|
||||
|
||||
[ "$MARK_COUNT" -lt 2 ] \
|
||||
&& die "could not parse at least two member marks from numgen vmap"
|
||||
|
||||
[ "$MARK_COUNT" -ne "$MEMBER_COUNT" ] \
|
||||
&& warn "mark count ($MARK_COUNT) differs from UCI member count ($MEMBER_COUNT) - mixed IPv4/IPv6 policy; per-member results may be approximate"
|
||||
|
||||
# Normalise marks to consistent hex format (no leading zeros beyond 0x prefix)
|
||||
# so our inserted rule syntax and the awk extraction both use the same values.
|
||||
MEMBER_MARKS_NORM=""
|
||||
for mark in $MEMBER_MARKS; do
|
||||
norm=$(printf "0x%x" "$((mark))")
|
||||
MEMBER_MARKS_NORM="$MEMBER_MARKS_NORM $norm"
|
||||
done
|
||||
MEMBER_MARKS="${MEMBER_MARKS_NORM# }"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Startup cleanup: remove stale mwan3_lb_test_* artifacts from aborted runs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
for stale_set in $(nft list sets inet 2>/dev/null \
|
||||
| awk '/mwan3_lb_test_/{gsub(/.*mwan3_lb_test_/,"mwan3_lb_test_"); gsub(/ \{.*/,""); print}'); do
|
||||
stale_fwd=$(nft -a list chain $FORWARD_TABLE forward 2>/dev/null \
|
||||
| awk "/@${stale_set}.*drop/{print \$NF; exit}")
|
||||
[ -n "$stale_fwd" ] && \
|
||||
nft delete rule $FORWARD_TABLE forward handle "$stale_fwd" 2>/dev/null
|
||||
stale_out=$(nft -a list chain $TABLE mwan3_output 2>/dev/null \
|
||||
| awk "/@${stale_set}.*return/{print \$NF; exit}")
|
||||
[ -n "$stale_out" ] && \
|
||||
nft delete rule $TABLE mwan3_output handle "$stale_out" 2>/dev/null
|
||||
stale_handle=$(nft -a list chain $TABLE mwan3_rules 2>/dev/null \
|
||||
| awk "/@${stale_set}/{print \$NF; exit}")
|
||||
[ -n "$stale_handle" ] && \
|
||||
nft delete rule $TABLE mwan3_rules handle "$stale_handle" 2>/dev/null
|
||||
nft delete set $TABLE "$stale_set" 2>/dev/null
|
||||
nft delete set $FORWARD_TABLE "$stale_set" 2>/dev/null
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-flight: warn if other rules use this policy
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
OTHER_RULES=$(nft list chain $TABLE mwan3_rules 2>/dev/null \
|
||||
| grep "jump ${CHAIN}" \
|
||||
| grep -v "^[[:space:]]*#")
|
||||
|
||||
RULE_COUNT=$(echo "$OTHER_RULES" | grep -c "jump ${CHAIN}" 2>/dev/null)
|
||||
if [ "$RULE_COUNT" -gt 1 ]; then
|
||||
warn "$RULE_COUNT rules send traffic to policy $POLICY."
|
||||
warn "flows from rules other than your test rule will also increment the counter."
|
||||
warn "consider temporarily disabling other rules using this policy before testing."
|
||||
echo
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compute N
|
||||
#
|
||||
# NITER = (total_weight / GCD(all_weights)) * multiplier
|
||||
# where multiplier = ceil(30 / base_N), giving NITER >= 30.
|
||||
# This ensures NITER * weight_i / total_weight is an integer for every member.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TOTAL_WEIGHT=0
|
||||
for w in $MEMBER_WEIGHTS; do
|
||||
TOTAL_WEIGHT=$((TOTAL_WEIGHT + w))
|
||||
done
|
||||
|
||||
GCD_WEIGHTS=$(echo "$MEMBER_WEIGHTS" | tr ' ' '\n' | \
|
||||
awk 'BEGIN{g=0} {
|
||||
a=$1; b=g;
|
||||
while(b!=0){t=b; b=a%b; a=t}
|
||||
g=a
|
||||
} END{print g}')
|
||||
|
||||
BASE_N=$((TOTAL_WEIGHT / GCD_WEIGHTS))
|
||||
MULTIPLIER=$(( (30 + BASE_N - 1) / BASE_N ))
|
||||
NITER=$((BASE_N * MULTIPLIER))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Collect test destination IPs
|
||||
#
|
||||
# Use well-known public IPs that should be reachable from any WAN link.
|
||||
# Exclude any IPs that mwan3track is already pinging: those pings go through
|
||||
# mwan3_output -> mwan3_rules and would match the test rule, contaminating
|
||||
# the counter with router-originated traffic.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if [ -n "$CUSTOM_IPS" ]; then
|
||||
TRACK_IPS="$CUSTOM_IPS"
|
||||
CUSTOM_IP_COUNT=$(echo "$TRACK_IPS" | wc -w)
|
||||
[ "$CUSTOM_IP_COUNT" -lt 2 ] && die "at least two destination IPs are required (got $CUSTOM_IP_COUNT)"
|
||||
else
|
||||
# Collect all track_ip values configured across all mwan3 interfaces.
|
||||
MWAN3_TRACKED_IPS=""
|
||||
collect_iface_track_ips() {
|
||||
local iface_track_ips
|
||||
config_get iface_track_ips "$1" track_ip ""
|
||||
MWAN3_TRACKED_IPS="$MWAN3_TRACKED_IPS $iface_track_ips"
|
||||
}
|
||||
config_foreach collect_iface_track_ips interface
|
||||
|
||||
# Default pools - exclude IPs that mwan3track is already pinging
|
||||
# (those go through mwan3_output -> mwan3_rules and contaminate the counter).
|
||||
if [ "$IPV6" -eq 1 ]; then
|
||||
DEFAULT_POOL="2001:4860:4860::8888 2001:4860:4860::8844 2606:4700:4700::1111 2606:4700:4700::1001 2620:fe::fe 2620:fe::9 2620:119:35::35 2620:119:53::53"
|
||||
else
|
||||
DEFAULT_POOL="8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 9.9.9.9 149.112.112.112 208.67.222.222 208.67.220.220 4.2.2.1 4.2.2.2 185.228.168.168 185.228.169.168"
|
||||
fi
|
||||
|
||||
TRACK_IPS=""
|
||||
for ip in $DEFAULT_POOL; do
|
||||
case " $MWAN3_TRACKED_IPS " in
|
||||
*" $ip "*) ;;
|
||||
*) TRACK_IPS="$TRACK_IPS $ip" ;;
|
||||
esac
|
||||
done
|
||||
TRACK_IPS="${TRACK_IPS# }"
|
||||
|
||||
[ -z "$TRACK_IPS" ] && die "all default destination IPs are configured as mwan3 tracking IPs - use the IP override argument to specify test destinations not being tracked"
|
||||
fi
|
||||
TRACK_COUNT=$(echo "$TRACK_IPS" | wc -w)
|
||||
|
||||
# Compute Windows cmd.exe delay between pings.
|
||||
# Windows ping uses a fixed ICMP identifier (id=1) so repeated pings to the
|
||||
# same destination reuse the same conntrack entry. The ct mark from the first
|
||||
# ping is restored on the second, bypassing the test rule (mark != 0 guard).
|
||||
# The full cycle through TRACK_COUNT IPs (each taking up to 2s for the ping
|
||||
# plus WIN_DELAY seconds) must exceed the 30s ICMP conntrack timeout so the
|
||||
# entry expires before we revisit each IP.
|
||||
# Formula: WIN_DELAY such that TRACK_COUNT * (2 + WIN_DELAY) > 30.
|
||||
# Using WIN_DELAY = 30/TRACK_COUNT + 3 gives comfortable margin.
|
||||
WIN_DELAY=$(( 30 / TRACK_COUNT + 3 ))
|
||||
|
||||
# Build the rotated destination array for the generated command.
|
||||
# Each iteration pings TRACK_IPS[i % TRACK_COUNT].
|
||||
# Expand to exactly N destinations, rotating through the pool.
|
||||
DEST_LIST=""
|
||||
i=0
|
||||
for seq in $(seq 1 "$NITER"); do
|
||||
idx=$((i % TRACK_COUNT + 1))
|
||||
dst=$(echo "$TRACK_IPS" | tr ' ' '\n' | sed -n "${idx}p")
|
||||
DEST_LIST="$DEST_LIST $dst"
|
||||
i=$((i + 1))
|
||||
done
|
||||
DEST_LIST="${DEST_LIST# }"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compute expected per-member hits
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
EXPECTED_LIST=""
|
||||
for w in $MEMBER_WEIGHTS; do
|
||||
expected=$((NITER * w / TOTAL_WEIGHT))
|
||||
EXPECTED_LIST="$EXPECTED_LIST $expected"
|
||||
done
|
||||
EXPECTED_LIST="${EXPECTED_LIST# }"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Insert temporary test rule into mwan3_rules
|
||||
#
|
||||
# Creates a temporary nft set containing the test destination IPs and inserts
|
||||
# a rule at the head of mwan3_rules that directs matching traffic to the
|
||||
# policy under test. This ensures the test pings hit the policy chain without
|
||||
# requiring the user to add a permanent rule. Both the rule and the set are
|
||||
# removed by cleanup().
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "Adding temporary test set, isolation rules, and test rule..."
|
||||
echo
|
||||
|
||||
if [ "$IPV6" -eq 1 ]; then
|
||||
SET_TYPE="ipv6_addr"
|
||||
PROTO_MATCH="meta l4proto ipv6-icmp ip6 daddr"
|
||||
PING_CMD="ping6"
|
||||
FORWARD_BLOCK="ip6 saddr != $CLIENT_IP ip6 daddr @$TEST_SET meta l4proto ipv6-icmp icmpv6 type echo-request"
|
||||
OUTPUT_ISOLATION="meta l4proto ipv6-icmp icmpv6 type echo-request ip6 daddr @$TEST_SET"
|
||||
else
|
||||
SET_TYPE="ipv4_addr"
|
||||
PROTO_MATCH="meta l4proto icmp ip daddr"
|
||||
PING_CMD="ping"
|
||||
FORWARD_BLOCK="ip saddr != $CLIENT_IP ip daddr @$TEST_SET meta l4proto icmp icmp type echo-request"
|
||||
OUTPUT_ISOLATION="meta l4proto icmp icmp type echo-request ip daddr @$TEST_SET"
|
||||
fi
|
||||
|
||||
# The rule matches ICMP/ICMPv6 only - not DNS, TCP, or other traffic to these
|
||||
# IPs. This prevents contamination from DNS forwarders and rogue clients that
|
||||
# bypass the local resolver, without needing to avoid popular DNS server IPs.
|
||||
#
|
||||
# nft sets are table-scoped. The mwan3_output and mwan3_rules rules live in
|
||||
# table inet mwan3; the forward isolation rule lives in table inet fw4.
|
||||
# Create the set in both tables and populate both with the same IPs.
|
||||
nft add set $TABLE "$TEST_SET" { type "$SET_TYPE" \; } \
|
||||
|| die "failed to create temporary destination set $TEST_SET in $TABLE"
|
||||
nft add set $FORWARD_TABLE "$TEST_SET" { type "$SET_TYPE" \; } \
|
||||
|| die "failed to create temporary destination set $TEST_SET in $FORWARD_TABLE"
|
||||
|
||||
for ip in $TRACK_IPS; do
|
||||
nft add element $TABLE "$TEST_SET" { "$ip" } 2>/dev/null
|
||||
nft add element $FORWARD_TABLE "$TEST_SET" { "$ip" } 2>/dev/null
|
||||
done
|
||||
|
||||
# Insert forward isolation rule: drop pings to test IPs from all LAN clients
|
||||
# except the nominated test client. Scoped to the test set so other LAN traffic
|
||||
# is unaffected.
|
||||
nft insert rule $FORWARD_TABLE forward $FORWARD_BLOCK drop \
|
||||
|| die "failed to insert forward isolation rule"
|
||||
FORWARD_BLOCK_HANDLE=$(nft -a list chain $FORWARD_TABLE forward 2>/dev/null \
|
||||
| awk "/@${TEST_SET}.*drop/{print \$NF; exit}")
|
||||
[ -n "$FORWARD_BLOCK_HANDLE" ] || die "could not read forward isolation rule handle"
|
||||
|
||||
# Insert output isolation rule: return (skip mwan3 marking) for any router
|
||||
# process pinging test IPs. mwan3track IPs are already excluded from the test
|
||||
# set, so this only affects unrelated processes. 'return' in a base chain is
|
||||
# equivalent to accept - the pings still work, they just don't hit the policy
|
||||
# chain.
|
||||
nft insert rule $TABLE mwan3_output $OUTPUT_ISOLATION return \
|
||||
|| die "failed to insert output isolation rule"
|
||||
OUTPUT_ISOLATION_HANDLE=$(nft -a list chain $TABLE mwan3_output 2>/dev/null \
|
||||
| awk "/@${TEST_SET}.*return/{print \$NF; exit}")
|
||||
[ -n "$OUTPUT_ISOLATION_HANDLE" ] || die "could not read output isolation rule handle"
|
||||
|
||||
nft insert rule $TABLE mwan3_rules \
|
||||
$PROTO_MATCH @"$TEST_SET" meta mark \& "$MMX_MASK" == 0 \
|
||||
jump "mwan3_policy_${POLICY}" \
|
||||
|| die "failed to insert temporary test rule into mwan3_rules"
|
||||
|
||||
TEST_RULE_HANDLE=$(nft -a list chain $TABLE mwan3_rules 2>/dev/null \
|
||||
| awk "/@${TEST_SET}/{print \$NF; exit}")
|
||||
[ -n "$TEST_RULE_HANDLE" ] || die "could not read temporary test rule handle"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Print summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "=== mwan3 load balancing distribution test ==="
|
||||
echo
|
||||
echo "Policy : $POLICY"
|
||||
echo "MMX mask : $MMX_MASK"
|
||||
echo "IP family : $([ "$IPV6" -eq 1 ] && echo IPv6 || echo IPv4)"
|
||||
echo "Test iterations : $NITER (base=$BASE_N x${MULTIPLIER}, total_weight=$TOTAL_WEIGHT, GCD=$GCD_WEIGHTS)"
|
||||
if [ -n "$CUSTOM_IPS" ]; then
|
||||
echo "Destinations : $TRACK_IPS (user override)"
|
||||
elif [ -n "$MWAN3_TRACKED_IPS" ]; then
|
||||
echo "Destinations : $TRACK_IPS (well-known IPs, excluding mwan3 tracking IPs)"
|
||||
else
|
||||
echo "Destinations : $TRACK_IPS (well-known IPs)"
|
||||
fi
|
||||
echo
|
||||
printf "%-20s %-10s %-10s %-16s %s\n" "Member" "Interface" "Weight" "Mark" "Expected hits"
|
||||
printf "%-20s %-10s %-10s %-16s %s\n" "------" "---------" "------" "----" "-------------"
|
||||
|
||||
i=1
|
||||
for member in $MEMBERS; do
|
||||
iface=$(echo "$MEMBER_IFACES" | tr ' ' '\n' | sed -n "${i}p")
|
||||
weight=$(echo "$MEMBER_WEIGHTS" | tr ' ' '\n' | sed -n "${i}p")
|
||||
mark=$(echo "$MEMBER_MARKS" | tr ' ' '\n' | sed -n "${i}p")
|
||||
expected=$(echo "$EXPECTED_LIST" | tr ' ' '\n' | sed -n "${i}p")
|
||||
printf "%-20s %-10s %-10s %-16s %s\n" "$member" "$iface" "$weight" "${mark:--}" "${expected:--}"
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Insert diagnostic rules
|
||||
#
|
||||
# Insertion order (each insert prepends, so insert in reverse of desired order):
|
||||
# desired: [unguarded] [guarded] [numgen] [member counters...] [last_resort]
|
||||
# insert: guarded first (becomes head), then unguarded (pushes guarded to #2)
|
||||
# member counters inserted before last_resort handle using 'add rule position'
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "Adding diagnostic rules..."
|
||||
|
||||
# Guarded counter (mark==0) - inserted at head
|
||||
nft insert rule $TABLE "$CHAIN" meta mark \& "$MMX_MASK" == 0 counter \
|
||||
|| die "failed to add guarded counter"
|
||||
|
||||
GUARDED_HANDLE=$(nft -a list chain $TABLE "$CHAIN" 2>/dev/null \
|
||||
| awk '/meta mark.*== 0x.*counter/{print $NF; exit}')
|
||||
[ -n "$GUARDED_HANDLE" ] || die "could not read guarded counter handle"
|
||||
add_handle "$GUARDED_HANDLE"
|
||||
|
||||
# Unguarded counter - inserted at head (pushes guarded to position 2)
|
||||
nft insert rule $TABLE "$CHAIN" counter \
|
||||
|| die "failed to add unguarded counter"
|
||||
|
||||
UNGUARDED_HANDLE=$(nft -a list chain $TABLE "$CHAIN" 2>/dev/null \
|
||||
| awk '/^\t\tcounter packets/{print $NF; exit}')
|
||||
[ -n "$UNGUARDED_HANDLE" ] || die "could not read unguarded counter handle"
|
||||
add_handle "$UNGUARDED_HANDLE"
|
||||
|
||||
# Per-member counters - inserted before last_resort (last rule in chain)
|
||||
# Use 'nft add rule position <handle>' which inserts AFTER the given handle.
|
||||
# We insert after the last numgen rule handle so counters sit between numgen
|
||||
# and last_resort regardless of how many numgen lines the chain has.
|
||||
NUMGEN_HANDLE=$(nft -a list chain $TABLE "$CHAIN" 2>/dev/null \
|
||||
| awk '/numgen/{last=$NF} END{print last}')
|
||||
[ -n "$NUMGEN_HANDLE" ] || die "could not read numgen rule handle"
|
||||
|
||||
PREV_HANDLE="$NUMGEN_HANDLE"
|
||||
for mark in $MEMBER_MARKS; do
|
||||
nft add rule $TABLE "$CHAIN" position "$PREV_HANDLE" \
|
||||
meta mark \& "$MMX_MASK" == "$mark" counter \
|
||||
|| die "failed to add per-member counter for mark $mark"
|
||||
|
||||
# Read handle of the rule just inserted (it follows PREV_HANDLE)
|
||||
NEW_HANDLE=$(nft -a list chain $TABLE "$CHAIN" 2>/dev/null \
|
||||
| awk -v prev="$PREV_HANDLE" '
|
||||
/handle/{
|
||||
if (found) { print $NF; exit }
|
||||
if ($NF == prev) found=1
|
||||
}')
|
||||
[ -n "$NEW_HANDLE" ] || die "could not read per-member counter handle for mark $mark"
|
||||
add_handle "$NEW_HANDLE"
|
||||
PREV_HANDLE="$NEW_HANDLE"
|
||||
done
|
||||
|
||||
echo "Done. Current chain state:"
|
||||
nft list chain $TABLE "$CHAIN"
|
||||
echo
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Print test command
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "----------------------------------------------------------------------"
|
||||
echo
|
||||
echo "Run the following on a LAN client (or on the router itself to test"
|
||||
echo "the output path):"
|
||||
echo
|
||||
|
||||
# Print DEST_LIST wrapped at 4 IPs per line with \ continuation
|
||||
printf " for dst in"
|
||||
col=0
|
||||
for dst in $DEST_LIST; do
|
||||
if [ "$col" -eq 0 ]; then
|
||||
printf " \\\\\n %s" "$dst"
|
||||
else
|
||||
printf " %s" "$dst"
|
||||
fi
|
||||
col=$(( (col + 1) % 4 ))
|
||||
done
|
||||
echo "; do"
|
||||
echo " echo -n \"\$dst: \""
|
||||
echo " $PING_CMD -c1 -W2 \"\$dst\" >/dev/null 2>&1 && echo ok || echo timeout"
|
||||
echo " sleep 2"
|
||||
echo " done"
|
||||
echo
|
||||
echo "If testing from a Windows client cmd.exe (uses fixed ICMP id - requires"
|
||||
echo "longer inter-ping delay so conntrack entries expire between revisits):"
|
||||
echo
|
||||
if [ "$IPV6" -eq 1 ]; then
|
||||
WIN_PING="ping -6 -n 1 -w 2000"
|
||||
else
|
||||
WIN_PING="ping -n 1 -w 2000"
|
||||
fi
|
||||
# Print Windows FOR loop with ^ continuation at 4 IPs per line
|
||||
printf " for %%i in (^\n"
|
||||
col=0
|
||||
total_dsts=$(echo "$DEST_LIST" | wc -w)
|
||||
cur=0
|
||||
for dst in $DEST_LIST; do
|
||||
cur=$((cur + 1))
|
||||
if [ "$col" -eq 0 ]; then
|
||||
printf " %s" "$dst"
|
||||
else
|
||||
printf " %s" "$dst"
|
||||
fi
|
||||
col=$(( (col + 1) % 4 ))
|
||||
if [ "$col" -eq 0 ] && [ "$cur" -lt "$total_dsts" ]; then
|
||||
printf " ^\n"
|
||||
fi
|
||||
done
|
||||
printf ") do (%s %%i & timeout /t %d /nobreak >nul)\n" "$WIN_PING" "$WIN_DELAY"
|
||||
echo
|
||||
echo "Press Enter here when the test is complete..."
|
||||
echo
|
||||
echo "----------------------------------------------------------------------"
|
||||
read _
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Read results
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo
|
||||
echo "=== Results ==="
|
||||
echo
|
||||
CHAIN_DUMP=$(nft list chain $TABLE "$CHAIN" 2>/dev/null)
|
||||
|
||||
TOTAL=$(echo "$CHAIN_DUMP" \
|
||||
| awk '/^\t\tcounter packets/{for(i=1;i<=NF;i++) if($i=="packets"){print $(i+1); exit}}')
|
||||
NEW=$(echo "$CHAIN_DUMP" \
|
||||
| awk '/meta mark.*== 0x.*counter/{for(i=1;i<=NF;i++) if($i=="packets"){print $(i+1); exit}}')
|
||||
|
||||
# Read per-member counts. The member counter rules match 'meta mark & X == MARK counter'.
|
||||
# They appear after the numgen rule. Extract in mark order.
|
||||
MEMBER_ACTUALS=""
|
||||
for mark in $MEMBER_MARKS; do
|
||||
count=$(echo "$CHAIN_DUMP" \
|
||||
| awk -v mark="$mark" '
|
||||
/meta mark.*== .*counter/{
|
||||
for(i=1;i<=NF;i++){
|
||||
if($i=="==" && strtonum($(i+1))==strtonum(mark) && strtonum(mark)!=0){
|
||||
for(j=i;j<=NF;j++){
|
||||
if($j=="packets"){print $(j+1); exit}
|
||||
}
|
||||
}
|
||||
}
|
||||
}')
|
||||
MEMBER_ACTUALS="$MEMBER_ACTUALS ${count:-0}"
|
||||
done
|
||||
MEMBER_ACTUALS="${MEMBER_ACTUALS# }"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Report
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "--- Summary ---"
|
||||
echo
|
||||
printf "%-40s %s\n" "Completed:" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
printf "%-40s %s\n" "Total packets entering chain:" "${TOTAL:-unknown}"
|
||||
printf "%-40s %s\n" "New-connection packets (mark=0):" "${NEW:-unknown}"
|
||||
printf "%-40s %s\n" "Test iterations:" "$NITER"
|
||||
echo
|
||||
|
||||
FW4_RELOADED=0
|
||||
if [ -z "$TOTAL" ]; then
|
||||
FW4_RELOADED=1
|
||||
warn "Counter rules are absent from $CHAIN."
|
||||
warn "fw4 reload likely occurred during the test, wiping all dynamic nft rules."
|
||||
warn "Re-run the test and return before fw4 triggers (e.g. within a few minutes)."
|
||||
echo
|
||||
fi
|
||||
|
||||
CONTAMINATED=0
|
||||
if [ -n "$TOTAL" ] && [ -n "$NEW" ]; then
|
||||
if [ "$TOTAL" -gt "$NEW" ] 2>/dev/null; then
|
||||
STALE=$((TOTAL - NEW))
|
||||
warn "$STALE packets had mark != 0 when entering the chain."
|
||||
warn "Established-connection packets should not reach this chain."
|
||||
warn "Per-member counts below may be inflated - check for prerouting early-exit bugs."
|
||||
echo
|
||||
CONTAMINATED=1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "--- Per-member distribution ---"
|
||||
echo
|
||||
printf "%-20s %-10s %-10s %-10s %-10s\n" "Member" "Interface" "Expected" "Actual" "Deviation"
|
||||
printf "%-20s %-10s %-10s %-10s %-10s\n" "------" "---------" "--------" "------" "---------"
|
||||
|
||||
PASS=1
|
||||
i=1
|
||||
for member in $MEMBERS; do
|
||||
iface=$(echo "$MEMBER_IFACES" | tr ' ' '\n' | sed -n "${i}p")
|
||||
expected=$(echo "$EXPECTED_LIST" | tr ' ' '\n' | sed -n "${i}p")
|
||||
actual=$(echo "$MEMBER_ACTUALS" | tr ' ' '\n' | sed -n "${i}p")
|
||||
[ -z "$actual" ] && actual=0
|
||||
deviation=$((actual - expected))
|
||||
dev_str="$deviation"
|
||||
[ "$deviation" -gt 0 ] && dev_str="+$deviation"
|
||||
abs_dev=$deviation
|
||||
[ "$abs_dev" -lt 0 ] && abs_dev=$((-abs_dev))
|
||||
[ "$abs_dev" -gt 2 ] && PASS=0
|
||||
printf "%-20s %-10s %-10s %-10s %-10s\n" "$member" "$iface" "$expected" "$actual" "$dev_str"
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo
|
||||
|
||||
if [ -n "$NEW" ] && [ "$NEW" -ne "$NITER" ] 2>/dev/null; then
|
||||
EXTRA=$((NEW - NITER))
|
||||
if [ "$EXTRA" -gt 0 ]; then
|
||||
echo "FAIL: $EXTRA extra new-connection hits beyond $NITER test iterations."
|
||||
echo " Something other than your test flows is reaching this policy chain."
|
||||
echo " Check whether another rule also sends traffic to this policy, or"
|
||||
echo " whether a process on the router is generating connections to the test IPs."
|
||||
PASS=0
|
||||
else
|
||||
echo "NOTE: Fewer hits than expected ($NEW/$NITER)."
|
||||
echo " Some test flows may have timed out or been routed by a higher-priority rule."
|
||||
fi
|
||||
echo
|
||||
fi
|
||||
|
||||
if [ "$FW4_RELOADED" -eq 1 ]; then
|
||||
echo "FAIL: test invalidated - fw4 reload wiped counter rules during the wait."
|
||||
echo " Re-run the test and do not leave it unattended for more than a few minutes."
|
||||
elif [ "$PASS" -eq 1 ] && [ "$CONTAMINATED" -eq 0 ]; then
|
||||
echo "PASS: distribution matches expected weights within tolerance (+/-2 per member)."
|
||||
echo " Load balancing is working according to configured policy."
|
||||
else
|
||||
echo "FAIL: distribution is outside tolerance."
|
||||
echo " Actual counts deviate from expected by more than 2 for at least one member."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Cleaning up diagnostic rules..."
|
||||
cleanup
|
||||
echo "Chain restored."
|
||||
684
mwan3-nft/files/usr/sbin/mwan3rtmon
Executable file
684
mwan3-nft/files/usr/sbin/mwan3rtmon
Executable file
@ -0,0 +1,684 @@
|
||||
#!/usr/bin/env ucode
|
||||
'use strict';
|
||||
|
||||
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";
|
||||
|
||||
const RTM_NEWROUTE = rtnl.const.RTM_NEWROUTE;
|
||||
const RTM_DELROUTE = rtnl.const.RTM_DELROUTE;
|
||||
const RTM_GETROUTE = rtnl.const.RTM_GETROUTE;
|
||||
const NLM_F_DUMP = rtnl.const.NLM_F_DUMP;
|
||||
const NLM_F_CREATE = rtnl.const.NLM_F_CREATE;
|
||||
const NLM_F_REPLACE = rtnl.const.NLM_F_REPLACE;
|
||||
const RT_TABLE_MAIN = rtnl.const.RT_TABLE_MAIN;
|
||||
const AF_INET = rtnl.const.AF_INET;
|
||||
const AF_INET6 = rtnl.const.AF_INET6;
|
||||
const RTNLGRP_IPV4_ROUTE = rtnl.const.RTNLGRP_IPV4_ROUTE;
|
||||
const RTNLGRP_IPV6_ROUTE = rtnl.const.RTNLGRP_IPV6_ROUTE;
|
||||
|
||||
const MWAN3TRACK_STATUS_DIR = "/var/run/mwan3track";
|
||||
const ROUTE_FIELDS = ["dst", "gateway", "oif", "prefsrc", "priority", "scope", "type", "tos", "metrics"];
|
||||
const DEBOUNCE_MS = 100;
|
||||
|
||||
let family_name, family_num, rtnl_group;
|
||||
let mmx_mask;
|
||||
let source_routing;
|
||||
let extra_table_set = {}; // { table_id: true } for O(1) lookup
|
||||
let dev_table_map = {}; // { device_name: table_id }
|
||||
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 ubus_conn;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Logging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function log_msg(level, msg) {
|
||||
|
||||
warn(`mwan3rtmon[${family_name}] ${level}: ${msg}\n`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function load_config() {
|
||||
|
||||
let cur = uci.cursor();
|
||||
cur.load("mwan3");
|
||||
|
||||
// MMX mask
|
||||
|
||||
mmx_mask = cur.get("mwan3", "globals", "mmx_mask") ?? "0x3F00";
|
||||
mmx_mask = +mmx_mask;
|
||||
|
||||
// Source routing option
|
||||
|
||||
source_routing = +(cur.get("mwan3", "globals", "source_routing") ?? "0");
|
||||
|
||||
// Extra lookup tables
|
||||
|
||||
extra_table_set = {};
|
||||
let rt_tables = cur.get("mwan3", "globals", "rt_table_lookup");
|
||||
if (type(rt_tables) == "array") {
|
||||
for (let t in rt_tables)
|
||||
extra_table_set[+t] = true;
|
||||
} else if (rt_tables != null) {
|
||||
extra_table_set[+rt_tables] = true;
|
||||
}
|
||||
|
||||
// Build interface list with table IDs
|
||||
|
||||
iface_list = [];
|
||||
let tid = 0;
|
||||
cur.foreach("mwan3", "interface", function(s) {
|
||||
tid++;
|
||||
let ifname = s[".name"];
|
||||
let fam = s.family ?? "ipv4";
|
||||
let enabled = +(s.enabled ?? "0");
|
||||
let track_ip = s.track_ip;
|
||||
let track_gateway = +(s.track_gateway ?? "0");
|
||||
let has_tracking = track_gateway ||
|
||||
(track_ip != null &&
|
||||
!(type(track_ip) == "array" && length(track_ip) == 0));
|
||||
push(iface_list, {
|
||||
name: ifname,
|
||||
family: fam,
|
||||
tid: tid,
|
||||
enabled: enabled,
|
||||
has_tracking: has_tracking,
|
||||
});
|
||||
});
|
||||
|
||||
cur.unload("mwan3");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Device -> table ID mapping (via ubus)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function build_dev_table_map() {
|
||||
|
||||
dev_table_map = {};
|
||||
|
||||
if (!ubus_conn)
|
||||
return;
|
||||
|
||||
let dump = ubus_conn.call("network.interface", "dump");
|
||||
if (!dump || !dump.interface)
|
||||
return;
|
||||
|
||||
// Build name->tid lookup for enabled interfaces in this family
|
||||
|
||||
let name_tid = {};
|
||||
for (let iface in iface_list) {
|
||||
if (iface.enabled && iface.family == family_name)
|
||||
name_tid[iface.name] = iface.tid;
|
||||
}
|
||||
|
||||
for (let intf in dump.interface) {
|
||||
let tid = name_tid[intf.interface];
|
||||
if (tid != null && intf.l3_device)
|
||||
dev_table_map[intf.l3_device] = tid;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cached nft chain existence check
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function refresh_active_chains() {
|
||||
|
||||
active_chains = {};
|
||||
let p = popen("nft -j list chains inet 2>/dev/null");
|
||||
if (!p)
|
||||
return;
|
||||
let out = p.read("all");
|
||||
p.close();
|
||||
if (!out)
|
||||
return;
|
||||
|
||||
let data;
|
||||
try { data = json(out); } catch(e) { return; }
|
||||
if (!data || !data.nftables)
|
||||
return;
|
||||
|
||||
for (let item in data.nftables) {
|
||||
if (item.chain && item.chain.table == "mwan3")
|
||||
active_chains[item.chain.name] = true;
|
||||
}
|
||||
}
|
||||
|
||||
function is_iface_nft_active(ifname) {
|
||||
|
||||
return !!active_chains["mwan3_iface_in_" + ifname];
|
||||
}
|
||||
|
||||
function get_active_tids() {
|
||||
|
||||
let tids = {};
|
||||
for (let iface in iface_list) {
|
||||
if (iface.family != family_name)
|
||||
continue;
|
||||
if (is_iface_nft_active(iface.name))
|
||||
tids[iface.tid] = iface.name;
|
||||
}
|
||||
return tids;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Check mwan3track status for an interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function get_track_status(ifname) {
|
||||
|
||||
let iface = null;
|
||||
for (let i in iface_list) {
|
||||
if (i.name == ifname) { iface = i; break; }
|
||||
}
|
||||
if (!iface || !iface.has_tracking)
|
||||
return "disabled";
|
||||
|
||||
let pid = trim(readfile(MWAN3TRACK_STATUS_DIR + "/" + ifname + "/PID") ?? "");
|
||||
if (pid == "")
|
||||
return "down";
|
||||
|
||||
let cmdline = readfile("/proc/" + pid + "/cmdline");
|
||||
if (cmdline == null)
|
||||
return "down";
|
||||
|
||||
let started = trim(readfile(MWAN3TRACK_STATUS_DIR + "/" + ifname + "/STARTED") ?? "");
|
||||
if (started == "1")
|
||||
return "active";
|
||||
else if (started == "0")
|
||||
return "paused";
|
||||
|
||||
return "down";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// nft helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function nft_exec(cmd) {
|
||||
|
||||
let p = popen("nft " + cmd + " 2>&1");
|
||||
if (!p)
|
||||
return false;
|
||||
let out = p.read("all");
|
||||
let rc = p.close();
|
||||
if (rc != 0) {
|
||||
log_msg("warn", "nft failed: nft " + cmd + " - " + (out ?? ""));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function nft_batch(cmds) {
|
||||
|
||||
let tmpfile = "/var/run/mwan3/mwan3rtmon_nft_batch";
|
||||
let f = open(tmpfile, "w", 0600);
|
||||
if (!f) {
|
||||
log_msg("warn", "cannot open nft batch file");
|
||||
return false;
|
||||
}
|
||||
for (let cmd in cmds)
|
||||
f.write(cmd + "\n");
|
||||
f.close();
|
||||
|
||||
let p = popen("nft -f " + tmpfile + " 2>&1");
|
||||
if (!p)
|
||||
return false;
|
||||
let out = p.read("all");
|
||||
let rc = p.close();
|
||||
if (rc != 0) {
|
||||
log_msg("warn", "nft batch failed: " + (out ?? ""));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Route helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Build a route object for adding to a per-interface table.
|
||||
// Copies only the relevant fields, sets the target table.
|
||||
// Strips src if source_routing is disabled.
|
||||
|
||||
function build_route_for_table(route, tid) {
|
||||
|
||||
let r = { family: route.family, table: tid };
|
||||
|
||||
for (let f in ROUTE_FIELDS)
|
||||
if (route[f] != null)
|
||||
r[f] = route[f];
|
||||
|
||||
// tos=0 is default, omit it
|
||||
if (r.tos == 0)
|
||||
delete r.tos;
|
||||
|
||||
// Strip src (source-based routing prefix) unless source_routing is enabled
|
||||
if (source_routing && route.src != null)
|
||||
r.src = route.src;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
// Apply a route add or delete to a per-interface table
|
||||
|
||||
function apply_route_to_table(route, tid, is_new) {
|
||||
|
||||
let r = build_route_for_table(route, tid);
|
||||
if (is_new) {
|
||||
rtnl.request(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_REPLACE, r);
|
||||
let err = rtnl.error();
|
||||
if (err)
|
||||
log_msg("warn", "failed to add route to table " +
|
||||
tid + ": " + err);
|
||||
} else {
|
||||
rtnl.request(RTM_DELROUTE, 0, r);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a route is a CIDR (has a prefix length < max for address family)
|
||||
|
||||
function is_cidr_route(route) {
|
||||
|
||||
let dst = route.dst;
|
||||
if (dst == null)
|
||||
return false;
|
||||
|
||||
let slash = index(dst, "/");
|
||||
if (slash < 0)
|
||||
return false;
|
||||
|
||||
let prefix_len = +substr(dst, slash + 1);
|
||||
if (family_num == AF_INET)
|
||||
return (prefix_len < 32);
|
||||
else
|
||||
return (prefix_len < 128);
|
||||
}
|
||||
|
||||
// Check if a route is a default route
|
||||
|
||||
function is_default_route(route) {
|
||||
|
||||
return (route.dst == null ||
|
||||
route.dst == "0.0.0.0/0" ||
|
||||
route.dst == "::/0");
|
||||
}
|
||||
|
||||
// Check if route is link-local (fe80::/10 IPv6 or 169.254.0.0/16 IPv4)
|
||||
|
||||
function is_linklocal_route(route) {
|
||||
|
||||
return (route.dst != null &&
|
||||
(match(route.dst, /^fe80::\//) != null ||
|
||||
match(route.dst, /^169\.254\./) != null));
|
||||
}
|
||||
|
||||
// Get all routes from main table (and extra lookup tables) via netlink
|
||||
|
||||
function get_all_routes() {
|
||||
|
||||
let routes = rtnl.request(RTM_GETROUTE, NLM_F_DUMP,
|
||||
{ family: family_num }) ?? [];
|
||||
|
||||
// Filter to main table + extra lookup tables (O(1) set lookup)
|
||||
|
||||
let result = [];
|
||||
for (let r in routes) {
|
||||
if (r.table == RT_TABLE_MAIN || extra_table_set[r.table])
|
||||
push(result, r);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if a route still exists in the main table (for delete event verification)
|
||||
|
||||
function route_still_exists(route) {
|
||||
|
||||
let routes = rtnl.request(RTM_GETROUTE, NLM_F_DUMP,
|
||||
{ family: family_num }) ?? [];
|
||||
|
||||
for (let r in routes) {
|
||||
if (r.table != RT_TABLE_MAIN)
|
||||
continue;
|
||||
if (r.dst == route.dst && r.oif == route.oif &&
|
||||
r.gateway == route.gateway && r.priority == route.priority)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Populate connected set (nft)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function populate_connected_set() {
|
||||
|
||||
let routes = get_all_routes();
|
||||
let setname = (family_num == AF_INET)
|
||||
? "mwan3_connected_v4" : "mwan3_connected_v6";
|
||||
|
||||
let seen = {};
|
||||
let elements = [];
|
||||
for (let route in routes) {
|
||||
if (is_cidr_route(route) && !is_default_route(route) && !is_linklocal_route(route) && !seen[route.dst]) {
|
||||
push(elements, route.dst);
|
||||
seen[route.dst] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (family_num == AF_INET)
|
||||
push(elements, "224.0.0.0/3");
|
||||
|
||||
sort(elements);
|
||||
let fingerprint = join(",", elements);
|
||||
if (fingerprint == last_connected_fingerprint)
|
||||
return;
|
||||
last_connected_fingerprint = fingerprint;
|
||||
|
||||
let cmds = [ "flush set inet mwan3 " + setname ];
|
||||
|
||||
if (length(elements) > 0)
|
||||
push(cmds, "add element inet mwan3 " + setname +
|
||||
" { " + join(", ", elements) + " }");
|
||||
|
||||
nft_batch(cmds);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Populate per-interface routing tables
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function populate_iface_routes() {
|
||||
|
||||
build_dev_table_map();
|
||||
refresh_active_chains();
|
||||
|
||||
let routes = get_all_routes();
|
||||
let active_tids = get_active_tids();
|
||||
|
||||
if (length(keys(active_tids)) == 0)
|
||||
return;
|
||||
|
||||
for (let route in routes) {
|
||||
let dev = route.oif;
|
||||
let target_tid = (dev != null) ? dev_table_map[dev] : null;
|
||||
|
||||
if (target_tid != null) {
|
||||
|
||||
// Route has a device that maps to a specific interface table
|
||||
|
||||
if (active_tids["" + target_tid] != null) {
|
||||
let r = build_route_for_table(route, target_tid);
|
||||
rtnl.request(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_REPLACE, r);
|
||||
}
|
||||
|
||||
} else if (!is_default_route(route) && !is_linklocal_route(route)) {
|
||||
|
||||
// Broadcast to all active interface tables
|
||||
|
||||
for (let tid_str in keys(active_tids)) {
|
||||
let r = build_route_for_table(route, +tid_str);
|
||||
rtnl.request(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_REPLACE, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repopulate mwan3_custom set from extra_table_set (used on SIGHUP reload)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function repopulate_custom_sets() {
|
||||
|
||||
let setname = (family_num == AF_INET) ? "mwan3_custom_v4" : "mwan3_custom_v6";
|
||||
|
||||
if (length(keys(extra_table_set)) == 0) {
|
||||
nft_exec("flush set inet mwan3 " + setname);
|
||||
return;
|
||||
}
|
||||
|
||||
let routes = rtnl.request(RTM_GETROUTE, NLM_F_DUMP, { family: family_num }) ?? [];
|
||||
|
||||
let cmds = [ "flush set inet mwan3 " + setname ];
|
||||
let seen = {};
|
||||
for (let r in routes) {
|
||||
if (!extra_table_set[r.table])
|
||||
continue;
|
||||
let dst = r.dst;
|
||||
if (dst == null || is_default_route(r) || is_linklocal_route(r))
|
||||
continue;
|
||||
if (!seen[dst]) {
|
||||
push(cmds, "add element inet mwan3 " + setname + " { " + dst + " }");
|
||||
seen[dst] = true;
|
||||
}
|
||||
}
|
||||
nft_batch(cmds);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handle rt_table_lookup route events -> mwan3_custom set
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Called when a route is added to or removed from a table listed in
|
||||
// rt_table_lookup. Mirrors the change into mwan3_custom_v4/v6 so that
|
||||
// destinations reachable via those tables receive MMX_DEFAULT and bypass
|
||||
// mwan3's WAN policy selection. Default and link-local routes are skipped.
|
||||
|
||||
function handle_custom_set_event(route, is_new) {
|
||||
|
||||
let dst = route.dst;
|
||||
if (dst == null || is_default_route(route) || is_linklocal_route(route))
|
||||
return;
|
||||
|
||||
let setname = (family_num == AF_INET) ? "mwan3_custom_v4" : "mwan3_custom_v6";
|
||||
|
||||
if (is_new) {
|
||||
log_msg("debug", "rt_table_lookup: adding " + dst + " to " + setname);
|
||||
nft_exec("add element inet mwan3 " + setname + " { " + dst + " }");
|
||||
} else {
|
||||
log_msg("debug", "rt_table_lookup: removing " + dst + " from " + setname);
|
||||
nft_exec("delete element inet mwan3 " + setname + " { " + dst + " }");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handle route events
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function handle_route_event(event) {
|
||||
|
||||
let cmd = event.cmd;
|
||||
let route = event.msg;
|
||||
|
||||
// Routes in rt_table_lookup tables are mirrored into mwan3_custom_v4/v6.
|
||||
|
||||
if (extra_table_set[route.table]) {
|
||||
handle_custom_set_event(route, cmd == RTM_NEWROUTE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle main table routes
|
||||
|
||||
if (route.table != RT_TABLE_MAIN)
|
||||
return;
|
||||
|
||||
let is_new = (cmd == RTM_NEWROUTE);
|
||||
|
||||
log_msg("debug", (is_new ? "new" : "del") + " route: dst=" +
|
||||
(route.dst ?? "default") + " dev=" + (route.oif ?? "none") +
|
||||
" gw=" + (route.gateway ?? "none"));
|
||||
|
||||
// -- Update connected set --
|
||||
// Both adds and deletes are debounced through populate_connected_set so
|
||||
// that bursts of RTM_NEWROUTE updates for already-present prefixes (common
|
||||
// with IPv6 PD address renewal) do not spawn a rapid stream of nft calls.
|
||||
// The fingerprint in populate_connected_set makes repeated identical calls
|
||||
// cheap when nothing has actually changed.
|
||||
|
||||
if (is_cidr_route(route)) {
|
||||
if (!connected_timer)
|
||||
connected_timer = uloop.timer(DEBOUNCE_MS, () => {
|
||||
populate_connected_set();
|
||||
connected_timer = null;
|
||||
});
|
||||
else
|
||||
connected_timer.set(DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// -- Update per-interface routing tables --
|
||||
|
||||
build_dev_table_map();
|
||||
refresh_active_chains();
|
||||
|
||||
// For delete events, verify the route is actually gone
|
||||
|
||||
if (!is_new && route_still_exists(route)) {
|
||||
log_msg("debug", "deleted but route still exists: " +
|
||||
(route.dst ?? "default"));
|
||||
return;
|
||||
}
|
||||
|
||||
let active_tids = get_active_tids();
|
||||
|
||||
let dev = route.oif;
|
||||
let target_tid = (dev != null) ? dev_table_map[dev] : null;
|
||||
|
||||
if (target_tid != null) {
|
||||
|
||||
// Device-matched route: add to specific interface table
|
||||
// (no track status check, matching shell behavior)
|
||||
|
||||
if (active_tids["" + target_tid] == null)
|
||||
return;
|
||||
|
||||
apply_route_to_table(route, target_tid, is_new);
|
||||
|
||||
} else if (!is_default_route(route) &&
|
||||
!is_linklocal_route(route)) {
|
||||
|
||||
// Broadcast to all active interface tables
|
||||
|
||||
for (let tid_str in keys(active_tids)) {
|
||||
let tid = +tid_str;
|
||||
let ifname = active_tids[tid_str];
|
||||
|
||||
let status = get_track_status(ifname);
|
||||
if (status != "active" && status != "disabled") {
|
||||
log_msg("debug", "interface " + ifname +
|
||||
" is " + status + " - skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
apply_route_to_table(route, tid, is_new);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function main() {
|
||||
|
||||
// Parse args
|
||||
|
||||
family_name = ARGV[0] ?? "ipv4";
|
||||
|
||||
if (family_name == "ipv6") {
|
||||
if (stat("/proc/sys/net/ipv6") == null) {
|
||||
log_msg("warn",
|
||||
"started for ipv6, but ipv6 not enabled on system");
|
||||
exit(1);
|
||||
}
|
||||
family_num = AF_INET6;
|
||||
rtnl_group = RTNLGRP_IPV6_ROUTE;
|
||||
} else {
|
||||
family_name = "ipv4";
|
||||
family_num = AF_INET;
|
||||
rtnl_group = RTNLGRP_IPV4_ROUTE;
|
||||
}
|
||||
|
||||
load_config();
|
||||
|
||||
// Connect to ubus
|
||||
|
||||
ubus_conn = ubus.connect();
|
||||
if (!ubus_conn) {
|
||||
log_msg("warn", "failed to connect to ubus");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
uloop.init();
|
||||
|
||||
// Bind the route listener first so its multicast socket is bound
|
||||
// to the route group before any initial dumps run. Route changes
|
||||
// emitted during the dump phase are then queued on the listener
|
||||
// socket and processed when uloop.run() begins, rather than being
|
||||
// lost to a subscribe-vs-dump race.
|
||||
|
||||
log_msg("debug", "creating route listener for " + family_name);
|
||||
let route_listener = rtnl.listener(
|
||||
handle_route_event,
|
||||
[ RTM_NEWROUTE, RTM_DELROUTE ],
|
||||
[ rtnl_group ]
|
||||
);
|
||||
|
||||
if (!route_listener) {
|
||||
log_msg("warn", "failed to create route listener: " +
|
||||
rtnl.error());
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Initial population, dump-driven. With the listener already
|
||||
// bound, any concurrent route changes will be delivered as deltas
|
||||
// once uloop.run() begins.
|
||||
|
||||
log_msg("debug", "populating initial custom sets");
|
||||
repopulate_custom_sets();
|
||||
|
||||
log_msg("debug", "populating initial connected set");
|
||||
populate_connected_set();
|
||||
|
||||
log_msg("debug", "populating initial per-interface routes");
|
||||
populate_iface_routes();
|
||||
|
||||
// Signal handlers for clean shutdown
|
||||
|
||||
uloop.signal("SIGTERM", () => {
|
||||
log_msg("debug", "received SIGTERM, exiting");
|
||||
uloop.end();
|
||||
});
|
||||
|
||||
uloop.signal("SIGINT", () => {
|
||||
log_msg("debug", "received SIGINT, exiting");
|
||||
uloop.end();
|
||||
});
|
||||
|
||||
uloop.signal("SIGHUP", () => {
|
||||
log_msg("debug", "received SIGHUP, reloading config");
|
||||
load_config();
|
||||
repopulate_custom_sets();
|
||||
});
|
||||
|
||||
log_msg("debug", "entering event loop");
|
||||
uloop.run();
|
||||
|
||||
// Do not call route_listener.close() — ucode-mod-rtnl bug: explicit close
|
||||
// zeroes the resource data pointer while the variable still holds a
|
||||
// reference; the destructor is then called again with NULL on scope exit,
|
||||
// causing a segfault. Let the GC collect the listener naturally instead.
|
||||
log_msg("debug", "exiting cleanly");
|
||||
}
|
||||
|
||||
main();
|
||||
592
mwan3-nft/files/usr/sbin/mwan3track
Executable file
592
mwan3-nft/files/usr/sbin/mwan3track
Executable file
@ -0,0 +1,592 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /lib/functions.sh
|
||||
. /lib/functions/network.sh
|
||||
. /lib/mwan3/common.sh
|
||||
|
||||
INTERFACE=""
|
||||
DEVICE=""
|
||||
|
||||
IFDOWN_EVENT=0
|
||||
IFUP_EVENT=0
|
||||
RELOAD_EVENT=0
|
||||
WRAP_LIB=/lib/mwan3/libwrap_mwan3_sockopt.so.1.0
|
||||
|
||||
stop_subprocs() {
|
||||
local killpids="$(jobs -p)"
|
||||
[ -n "$killpids" ] && kill $killpids
|
||||
}
|
||||
|
||||
WRAP() {
|
||||
# shellcheck disable=SC2048
|
||||
FAMILY=$FAMILY DEVICE=$DEVICE SRCIP=$SRC_IP FWMARK=$MMX_DEFAULT LD_PRELOAD=$WRAP_LIB $*
|
||||
}
|
||||
|
||||
clean_up() {
|
||||
LOG notice "Stopping mwan3track for interface \"${INTERFACE}\". Status was \"${STATUS}\""
|
||||
stop_subprocs
|
||||
exit 0
|
||||
}
|
||||
|
||||
if_down() {
|
||||
LOG info "Detect ifdown event on interface ${INTERFACE} (${DEVICE})"
|
||||
IFDOWN_EVENT=1
|
||||
stop_subprocs
|
||||
}
|
||||
|
||||
if_up() {
|
||||
LOG info "Detect ifup event on interface ${INTERFACE} (${DEVICE})"
|
||||
IFDOWN_EVENT=0
|
||||
IFUP_EVENT=1
|
||||
started
|
||||
stop_subprocs
|
||||
}
|
||||
|
||||
config_reload() {
|
||||
LOG info "Received configuration reload signal for interface ${INTERFACE}"
|
||||
RELOAD_EVENT=1
|
||||
stop_subprocs
|
||||
}
|
||||
|
||||
ping_test_host() {
|
||||
if [ "$FAMILY" = "ipv6" ]; then
|
||||
echo "::1"
|
||||
else
|
||||
echo "127.0.0.1"
|
||||
fi
|
||||
}
|
||||
|
||||
get_ping_command() {
|
||||
if [ -x "/usr/bin/ping" ] && /usr/bin/ping -${FAMILY#ipv} -c1 -q $(ping_test_host) &>/dev/null; then
|
||||
# -4 option added in iputils c3e68ac6 so need to check if we can use it
|
||||
# or if we must use ping and ping6
|
||||
echo "/usr/bin/ping -${FAMILY#ipv}"
|
||||
elif [ "$FAMILY" = "ipv6" ] && [ -x "/usr/bin/ping6" ]; then
|
||||
echo "/usr/bin/ping6"
|
||||
elif [ "$FAMILY" = "ipv4" ] && [ -x "/usr/bin/ping" ]; then
|
||||
echo "/usr/bin/ping"
|
||||
elif [ -x "/bin/ping" ]; then
|
||||
echo "/bin/ping -${FAMILY#ipv}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_track_method() {
|
||||
case "$1" in
|
||||
ping)
|
||||
PING=$(get_ping_command)
|
||||
if [ $? -ne 0 ]; then
|
||||
LOG warn "Missing ping. Please enable BUSYBOX_DEFAULT_PING and recompile busybox or install iputils-ping package."
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
arping)
|
||||
command -v arping 1>/dev/null 2>&1 || {
|
||||
LOG warn "Missing arping. Please install iputils-arping package."
|
||||
return 1
|
||||
}
|
||||
;;
|
||||
httping)
|
||||
command -v httping 1>/dev/null 2>&1 || {
|
||||
LOG warn "Missing httping. Please install httping package."
|
||||
return 1
|
||||
}
|
||||
;;
|
||||
nping-*)
|
||||
command -v nping 1>/dev/null 2>&1 || {
|
||||
LOG warn "Missing nping. Please install nping package."
|
||||
return 1
|
||||
}
|
||||
;;
|
||||
nslookup)
|
||||
command -v nslookup 1>/dev/null 2>&1 || {
|
||||
LOG warn "Missing nslookup. Please install busybox package."
|
||||
return 1
|
||||
}
|
||||
;;
|
||||
*)
|
||||
LOG warn "Unsupported tracking method: $track_method"
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
validate_wrap() {
|
||||
[ -f "$WRAP_LIB" ] && return
|
||||
LOG error "Missing libwrap_mwan3_sockopt. Please reinstall mwan3." && exit 1
|
||||
}
|
||||
|
||||
disconnected() {
|
||||
local status
|
||||
readfile status ${MWAN3TRACK_STATUS_DIR}/${INTERFACE}/STATUS
|
||||
|
||||
STATUS='offline'
|
||||
echo "offline" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/STATUS
|
||||
get_uptime > $MWAN3TRACK_STATUS_DIR/$INTERFACE/OFFLINE
|
||||
echo "0" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/ONLINE
|
||||
score=0
|
||||
[ "$1" = 1 ] && return
|
||||
|
||||
# Only execute disconnectd action if status was online or disconnecting
|
||||
if [ "$status" = "online" ] || [ "$status" = "disconnecting" ]; then
|
||||
LOG notice "Interface $INTERFACE ($DEVICE) is offline"
|
||||
env -i ACTION="disconnected" INTERFACE="$INTERFACE" DEVICE="$DEVICE" /sbin/hotplug-call iface
|
||||
else
|
||||
LOG debug "Skip disconnected event for $INTERFACE ($DEVICE)"
|
||||
fi
|
||||
}
|
||||
|
||||
connected() {
|
||||
STATUS='online'
|
||||
echo "online" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/STATUS
|
||||
echo "0" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/OFFLINE
|
||||
get_uptime > $MWAN3TRACK_STATUS_DIR/$INTERFACE/ONLINE
|
||||
score=$((down+up))
|
||||
host_up_count=0
|
||||
lost=0
|
||||
turn=0
|
||||
loss=0
|
||||
LOG notice "Interface $INTERFACE ($DEVICE) is online"
|
||||
env -i FIRSTCONNECT=$1 ACTION="connected" INTERFACE="$INTERFACE" DEVICE="$DEVICE" /sbin/hotplug-call iface
|
||||
}
|
||||
|
||||
disconnecting() {
|
||||
if [ "$STATUS" != "disconnecting" ] ; then
|
||||
STATUS="disconnecting"
|
||||
echo "disconnecting" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/STATUS
|
||||
LOG notice "Interface $INTERFACE ($DEVICE) is disconnecting"
|
||||
env -i ACTION="disconnecting" INTERFACE="$INTERFACE" DEVICE="$DEVICE" /sbin/hotplug-call iface
|
||||
fi
|
||||
}
|
||||
|
||||
connecting() {
|
||||
if [ "$STATUS" != "connecting" ] ; then
|
||||
STATUS="connecting"
|
||||
echo "connecting" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/STATUS
|
||||
LOG notice "Interface $INTERFACE ($DEVICE) is connecting"
|
||||
env -i ACTION="connecting" INTERFACE="$INTERFACE" DEVICE="$DEVICE" /sbin/hotplug-call iface
|
||||
fi
|
||||
}
|
||||
|
||||
disabled() {
|
||||
STATUS='disabled'
|
||||
echo "disabled" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/STATUS
|
||||
stopped
|
||||
}
|
||||
|
||||
firstconnect() {
|
||||
local true_iface
|
||||
network_flush_cache
|
||||
|
||||
mwan3_get_true_iface true_iface $INTERFACE
|
||||
network_get_device DEVICE $true_iface
|
||||
|
||||
if [ "$STATUS" != "online" ]; then
|
||||
config_get STATUS $INTERFACE initial_state "online"
|
||||
fi
|
||||
|
||||
if ! network_is_up $true_iface || [ -z "$DEVICE" ]; then
|
||||
disabled
|
||||
return
|
||||
fi
|
||||
|
||||
mwan3_get_src_ip SRC_IP $INTERFACE
|
||||
|
||||
LOG debug "firstconnect: called on $INTERFACE/$true_iface ($DEVICE). Status is $STATUS. SRC_IP is $SRC_IP"
|
||||
|
||||
started
|
||||
if [ "$STATUS" = "offline" ]; then
|
||||
disconnected 1
|
||||
else
|
||||
connected 1
|
||||
fi
|
||||
}
|
||||
|
||||
stopped() {
|
||||
STARTED=0
|
||||
echo 0 > $MWAN3TRACK_STATUS_DIR/$INTERFACE/STARTED
|
||||
}
|
||||
|
||||
started() {
|
||||
STARTED=1
|
||||
echo 1 > $MWAN3TRACK_STATUS_DIR/$INTERFACE/STARTED
|
||||
}
|
||||
|
||||
update_status() {
|
||||
local track_ip=$1
|
||||
|
||||
echo "$2" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/TRACK_${track_ip}
|
||||
[ -z "$3" ] && return
|
||||
echo "$3" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/LATENCY_${track_ip}
|
||||
echo "$4" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/LOSS_${track_ip}
|
||||
}
|
||||
|
||||
load_tracking_config() {
|
||||
config_get_bool MWAN3_VERBOSE_LOGGING globals verbose_logging 0
|
||||
config_get FAMILY $INTERFACE family ipv4
|
||||
config_get track_method $INTERFACE track_method ping
|
||||
config_get_bool httping_ssl $INTERFACE httping_ssl 0
|
||||
validate_track_method $track_method || {
|
||||
track_method=ping
|
||||
if validate_track_method $track_method; then
|
||||
LOG warn "Using ping to track interface $INTERFACE avaliability"
|
||||
else
|
||||
LOG err "No track method avaliable"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
config_get reliability $INTERFACE reliability 1
|
||||
config_get count $INTERFACE count 1
|
||||
config_get timeout $INTERFACE timeout 4
|
||||
config_get interval $INTERFACE interval 10
|
||||
config_get down $INTERFACE down 5
|
||||
config_get up $INTERFACE up 5
|
||||
config_get size $INTERFACE size 56
|
||||
config_get max_ttl $INTERFACE max_ttl 60
|
||||
config_get failure_interval $INTERFACE failure_interval $interval
|
||||
config_get_bool keep_failure_interval $INTERFACE keep_failure_interval 0
|
||||
config_get recovery_interval $INTERFACE recovery_interval $interval
|
||||
config_get_bool check_quality $INTERFACE check_quality 0
|
||||
config_get failure_latency $INTERFACE failure_latency 1000
|
||||
config_get recovery_latency $INTERFACE recovery_latency 500
|
||||
config_get failure_loss $INTERFACE failure_loss 40
|
||||
config_get recovery_loss $INTERFACE recovery_loss 10
|
||||
# arping/nslookup/nping don't produce latency/loss samples;
|
||||
# check_quality would trigger arithmetic errors on the empty values.
|
||||
case "$track_method" in
|
||||
ping|httping) ;;
|
||||
*)
|
||||
if [ "$check_quality" -eq 1 ]; then
|
||||
LOG notice "check_quality is not supported for track_method '$track_method' on interface $INTERFACE; treating as disabled"
|
||||
check_quality=0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main() {
|
||||
local reliability count timeout interval failure_interval
|
||||
local recovery_interval down up size
|
||||
local keep_failure_interval check_quality failure_latency
|
||||
local recovery_latency failure_loss recovery_loss
|
||||
|
||||
local max_ttl httping_ssl track_ips do_log
|
||||
|
||||
INTERFACE=$1
|
||||
STATUS=""
|
||||
mkdir -p $MWAN3TRACK_STATUS_DIR/$INTERFACE
|
||||
exec 9>"$MWAN3TRACK_STATUS_DIR/$INTERFACE/LOCK"
|
||||
if ! flock -n 9; then
|
||||
local old_pid
|
||||
old_pid=$(cat $MWAN3TRACK_STATUS_DIR/$INTERFACE/PID 2>/dev/null)
|
||||
if [ -n "$old_pid" ] && [ "$old_pid" != "$$" ] && kill -0 "$old_pid" 2>/dev/null; then
|
||||
LOG notice "Terminating stale mwan3track instance (PID $old_pid) for interface $INTERFACE"
|
||||
kill "$old_pid" 2>/dev/null
|
||||
fi
|
||||
flock 9
|
||||
fi
|
||||
echo $$ > $MWAN3TRACK_STATUS_DIR/$INTERFACE/PID
|
||||
stopped
|
||||
PING_OUTPUT=$MWAN3TRACK_STATUS_DIR/$INTERFACE/PING_OUTPUT
|
||||
|
||||
mwan3_init
|
||||
validate_wrap
|
||||
|
||||
trap clean_up TERM
|
||||
trap if_down USR1
|
||||
trap if_up USR2
|
||||
trap config_reload HUP
|
||||
|
||||
load_tracking_config
|
||||
local sleep_time result ping_status loss latency
|
||||
mwan3_list_track_ips()
|
||||
{
|
||||
track_ips="$track_ips $1"
|
||||
}
|
||||
mwan3_load_track_ips()
|
||||
{
|
||||
local track_gateway gw_ip f ip
|
||||
track_ips=""
|
||||
config_list_foreach "$INTERFACE" track_ip mwan3_list_track_ips
|
||||
config_get_bool track_gateway "$INTERFACE" track_gateway 0
|
||||
if [ "$track_gateway" -eq 1 ]; then
|
||||
gw_ip=$(cat "$MWAN3TRACK_STATUS_DIR/${INTERFACE}/GATEWAY" 2>/dev/null)
|
||||
if [ -n "$gw_ip" ]; then
|
||||
case " $track_ips " in
|
||||
*" $gw_ip "*) ;;
|
||||
*) track_ips="$gw_ip $track_ips" ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
for f in "$MWAN3TRACK_STATUS_DIR/$INTERFACE/TRACK_"*; do
|
||||
[ -f "$f" ] || continue
|
||||
ip="${f##*/TRACK_}"
|
||||
[ "$ip" = "OUTPUT" ] && continue
|
||||
case " $track_ips " in
|
||||
*" $ip "*) ;;
|
||||
*) rm -f "$MWAN3TRACK_STATUS_DIR/$INTERFACE/TRACK_${ip}" \
|
||||
"$MWAN3TRACK_STATUS_DIR/$INTERFACE/LATENCY_${ip}" \
|
||||
"$MWAN3TRACK_STATUS_DIR/$INTERFACE/LOSS_${ip}" ;;
|
||||
esac
|
||||
done
|
||||
local current_check_quality
|
||||
config_get_bool current_check_quality "$INTERFACE" check_quality 0
|
||||
# Mirror the clamp applied at startup so LATENCY_/LOSS_ files
|
||||
# are cleaned up when the effective check_quality is disabled
|
||||
# by virtue of an incompatible track_method.
|
||||
case "$track_method" in
|
||||
ping|httping) ;;
|
||||
*) current_check_quality=0 ;;
|
||||
esac
|
||||
if [ "$current_check_quality" -eq 0 ]; then
|
||||
rm -f $MWAN3TRACK_STATUS_DIR/$INTERFACE/LATENCY_* \
|
||||
$MWAN3TRACK_STATUS_DIR/$INTERFACE/LOSS_* 2>/dev/null
|
||||
fi
|
||||
}
|
||||
mwan3_load_track_ips
|
||||
|
||||
local score=$((down+up))
|
||||
local warn_threshold=$((up + (down + 2) / 3))
|
||||
local host_up_count=0
|
||||
local lost=0
|
||||
local turn=0
|
||||
local failed_hosts=""
|
||||
|
||||
firstconnect
|
||||
while true; do
|
||||
[ $STARTED -eq 0 ] && {
|
||||
sleep $MAX_SLEEP &
|
||||
wait $!
|
||||
}
|
||||
# Process any pending interface events before starting a ping round.
|
||||
# This avoids pinging with stale DEVICE/SRC_IP after wakeup from
|
||||
# disabled state — a USR2 signal sets IFUP_EVENT but DEVICE is not
|
||||
# refreshed until firstconnect() runs below.
|
||||
if [ "${IFDOWN_EVENT}" -eq 1 ]; then
|
||||
LOG debug "Register ifdown event on interface ${INTERFACE} (${DEVICE})"
|
||||
disconnected
|
||||
disabled
|
||||
IFDOWN_EVENT=0
|
||||
continue
|
||||
fi
|
||||
if [ "${IFUP_EVENT}" -eq 1 ]; then
|
||||
LOG debug "Register ifup event on interface ${INTERFACE} (${DEVICE})"
|
||||
config_load mwan3
|
||||
mwan3_load_track_ips
|
||||
firstconnect
|
||||
IFUP_EVENT=0
|
||||
continue
|
||||
fi
|
||||
if [ "${RELOAD_EVENT}" -eq 1 ]; then
|
||||
LOG debug "Processing configuration reload for interface ${INTERFACE}"
|
||||
config_load mwan3
|
||||
load_tracking_config
|
||||
mwan3_load_track_ips
|
||||
if [ $score -gt $((down + up)) ]; then
|
||||
score=$((down + up))
|
||||
fi
|
||||
warn_threshold=$((up + (down + 2) / 3))
|
||||
RELOAD_EVENT=0
|
||||
continue
|
||||
fi
|
||||
sleep_time=$interval
|
||||
for track_ip in $track_ips; do
|
||||
if [ $host_up_count -lt $reliability ]; then
|
||||
case "$track_method" in
|
||||
ping)
|
||||
if [ $check_quality -eq 0 ]; then
|
||||
WRAP $PING -n -c $count -W $timeout -s $size -t $max_ttl -q $track_ip &> /dev/null &
|
||||
wait $!
|
||||
result=$?
|
||||
else
|
||||
WRAP $PING -n -c $count -W $timeout -s $size -t $max_ttl -q $track_ip 2>/dev/null > $PING_OUTPUT &
|
||||
wait $!
|
||||
ping_status=$?
|
||||
loss="$(sed $PING_OUTPUT -ne 's/.* \([0-9]\+\)% packet loss.*/\1/p')"
|
||||
if [ "$ping_status" -ne 0 ] || [ "$loss" -eq 100 ]; then
|
||||
latency=999999
|
||||
loss=100
|
||||
else
|
||||
latency="$(sed $PING_OUTPUT -ne 's%\(rtt\|round-trip\).* = [^/]*/\([0-9]\+\).*%\2%p')"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
arping)
|
||||
WRAP arping -I $DEVICE -c $count -w $timeout -q $track_ip &> /dev/null &
|
||||
wait $!
|
||||
result=$?
|
||||
;;
|
||||
httping)
|
||||
if [ $check_quality -eq 0 ]; then
|
||||
if [ "$httping_ssl" -eq 1 ]; then
|
||||
WRAP httping -c $count -t $timeout -q "https://$track_ip" &> /dev/null &
|
||||
else
|
||||
WRAP httping -c $count -t $timeout -q "http://$track_ip" &> /dev/null &
|
||||
fi
|
||||
wait $!
|
||||
result=$?
|
||||
else
|
||||
if [ "$httping_ssl" -eq 1 ]; then
|
||||
WRAP httping -c $count -t $timeout "https://$track_ip" 2> /dev/null > $PING_OUTPUT &
|
||||
else
|
||||
WRAP httping -c $count -t $timeout "http://$track_ip" 2> /dev/null > $PING_OUTPUT &
|
||||
fi
|
||||
wait $!
|
||||
ping_status=$?
|
||||
loss="$(sed $PING_OUTPUT -ne 's/.* \([0-9]\+\).*% failed.*/\1/p')"
|
||||
if [ "$ping_status" -ne 0 ] || [ "$loss" -eq 100 ]; then
|
||||
latency=999999
|
||||
loss=100
|
||||
else
|
||||
latency="$(sed $PING_OUTPUT -ne 's%\(rtt\|round-trip\).* = [^/]*/\([0-9]\+\).*%\2%p')"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
nping-*)
|
||||
WRAP nping -${FAMILY#ipv} -c $count $track_ip --${track_method#nping-} > $PING_OUTPUT &
|
||||
wait $!
|
||||
result=$(grep Lost $PING_OUTPUT | awk '{print $12}')
|
||||
result=${result:-1}
|
||||
;;
|
||||
nslookup)
|
||||
WRAP nslookup www.google.com $track_ip > $PING_OUTPUT &
|
||||
wait $!
|
||||
result=$?
|
||||
;;
|
||||
esac
|
||||
if [ $check_quality -eq 0 ]; then
|
||||
if [ $result -eq 0 ]; then
|
||||
let host_up_count++
|
||||
update_status "$track_ip" "up"
|
||||
|
||||
[ $score -le $up ] && LOG debug "Check ($track_method) success for target \"$track_ip\" on interface $INTERFACE ($DEVICE). Current score: $score"
|
||||
else
|
||||
let lost++
|
||||
update_status "$track_ip" "down"
|
||||
|
||||
failed_hosts="${failed_hosts:+$failed_hosts }$track_ip"
|
||||
fi
|
||||
else
|
||||
if [ "$loss" -ge "$failure_loss" ] || [ "$latency" -ge "$failure_latency" ]; then
|
||||
let lost++
|
||||
update_status "$track_ip" "down" $latency $loss
|
||||
|
||||
failed_hosts="${failed_hosts:+$failed_hosts }$track_ip(${latency}ms/${loss}%)"
|
||||
elif [ "$loss" -le "$recovery_loss" ] && [ "$latency" -le "$recovery_latency" ]; then
|
||||
let host_up_count++
|
||||
update_status "$track_ip" "up" $latency $loss
|
||||
|
||||
[ $score -le $up ] && LOG debug "Check (${track_method}: latency=${latency}ms loss=${loss}%) success for target \"$track_ip\" on interface $INTERFACE ($DEVICE). Current score: $score"
|
||||
else
|
||||
echo "skipped" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/TRACK_${track_ip}
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "skipped" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/TRACK_${track_ip}
|
||||
fi
|
||||
if [ "${IFDOWN_EVENT}" -eq 1 ] || [ "${IFUP_EVENT}" -eq 1 ] || [ "${RELOAD_EVENT}" -eq 1 ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${IFDOWN_EVENT}" -eq 1 ] || [ "${IFUP_EVENT}" -eq 1 ] || [ "${RELOAD_EVENT}" -eq 1 ]; then
|
||||
host_up_count=0
|
||||
failed_hosts=""
|
||||
continue
|
||||
fi
|
||||
|
||||
# Log failures only when the round actually failed (reliability not met).
|
||||
# Per-host failures are suppressed when a later host met the threshold,
|
||||
# since logging them would falsely imply the interface is degraded.
|
||||
# Also suppress when hotplug has already marked the interface offline
|
||||
# (e.g. manual ifdown) — the USR1 signal may still be in transit.
|
||||
if [ $host_up_count -lt $reliability ] && [ -n "$failed_hosts" ] && [ $score -gt $up ]; then
|
||||
local hp_state
|
||||
readfile hp_state "$MWAN3_STATUS_DIR/iface_state/$INTERFACE" 2>/dev/null
|
||||
if [ "$hp_state" != "offline" ]; then
|
||||
LOG info "Check ($track_method) failed for target(s) \"$failed_hosts\" on interface $INTERFACE ($DEVICE). Current score: $score"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $host_up_count -lt $reliability ]; then
|
||||
let score--
|
||||
|
||||
if [ $score -lt $up ]; then
|
||||
score=0
|
||||
[ ${keep_failure_interval} -eq 1 ] && sleep_time=$failure_interval
|
||||
elif [ $score -eq $up ]; then
|
||||
disconnecting
|
||||
sleep_time=$failure_interval
|
||||
disconnected
|
||||
elif [ $score -gt $up ]; then
|
||||
[ $score -le $warn_threshold ] && disconnecting
|
||||
sleep_time=$failure_interval
|
||||
fi
|
||||
else
|
||||
if [ $score -lt $((down+up)) ] && [ $lost -gt 0 ]; then
|
||||
LOG debug "Lost $lost host(s) on interface $INTERFACE ($DEVICE). Current score: $score"
|
||||
fi
|
||||
|
||||
let score++
|
||||
lost=0
|
||||
|
||||
if [ $score -lt $up ]; then
|
||||
connecting
|
||||
sleep_time=$recovery_interval
|
||||
elif [ $score -eq $up ]; then
|
||||
connecting
|
||||
sleep_time=$recovery_interval
|
||||
connected
|
||||
elif [ $score -gt $up ]; then
|
||||
if [ "$STATUS" = "disconnecting" ]; then
|
||||
STATUS="online"
|
||||
LOG notice "Interface $INTERFACE ($DEVICE) is online"
|
||||
fi
|
||||
echo "online" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/STATUS
|
||||
score=$((down+up))
|
||||
fi
|
||||
fi
|
||||
|
||||
let turn++
|
||||
mkdir -p "$MWAN3TRACK_STATUS_DIR/${1}"
|
||||
echo "${lost}" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/LOST
|
||||
echo "${score}" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/SCORE
|
||||
echo "${turn}" > $MWAN3TRACK_STATUS_DIR/$INTERFACE/TURN
|
||||
get_uptime > $MWAN3TRACK_STATUS_DIR/$INTERFACE/TIME
|
||||
|
||||
host_up_count=0
|
||||
failed_hosts=""
|
||||
if [ "${IFDOWN_EVENT}" -eq 0 ] && [ "${IFUP_EVENT}" -eq 0 ]; then
|
||||
sleep "${sleep_time}" &
|
||||
wait $!
|
||||
fi
|
||||
|
||||
if [ "${IFDOWN_EVENT}" -eq 1 ]; then
|
||||
LOG debug "Register ifdown event on interface ${INTERFACE} (${DEVICE})"
|
||||
disconnected
|
||||
disabled
|
||||
IFDOWN_EVENT=0
|
||||
fi
|
||||
if [ "${IFUP_EVENT}" -eq 1 ]; then
|
||||
LOG debug "Register ifup event on interface ${INTERFACE} (${DEVICE})"
|
||||
config_load mwan3
|
||||
mwan3_load_track_ips
|
||||
firstconnect
|
||||
IFUP_EVENT=0
|
||||
fi
|
||||
if [ "${RELOAD_EVENT}" -eq 1 ]; then
|
||||
LOG debug "Processing configuration reload for interface ${INTERFACE}"
|
||||
config_load mwan3
|
||||
load_tracking_config
|
||||
mwan3_load_track_ips
|
||||
if [ $score -gt $((down + up)) ]; then
|
||||
score=$((down + up))
|
||||
fi
|
||||
warn_threshold=$((up + (down + 2) / 3))
|
||||
RELOAD_EVENT=0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
756
mwan3-nft/files/usr/share/rpcd/ucode/mwan3
Normal file
756
mwan3-nft/files/usr/share/rpcd/ucode/mwan3
Normal file
@ -0,0 +1,756 @@
|
||||
'use strict';
|
||||
|
||||
import { popen, readfile, glob } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import * as rtnl from 'rtnl';
|
||||
|
||||
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, track_gateway, procd) {
|
||||
if (length(uci_track_ips) == 0 && !track_gateway) {
|
||||
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 nft_connected_set = {
|
||||
'4': 'mwan3_connected_v4',
|
||||
'6': 'mwan3_connected_v6',
|
||||
};
|
||||
|
||||
function parse_elem_val(v) {
|
||||
if (type(v) == 'string')
|
||||
return v;
|
||||
if (v?.prefix)
|
||||
return sprintf('%s/%d', v.prefix.addr, v.prefix.len);
|
||||
if (v?.range)
|
||||
return sprintf('%s-%s', v.range[0], v.range[1]);
|
||||
return null;
|
||||
}
|
||||
|
||||
function count_nftset_elements(setname) {
|
||||
if (!match(setname, /^[a-zA-Z0-9_-]+$/))
|
||||
return 0;
|
||||
const nft = popen(sprintf('nft -j list set inet mwan3 %s 2>/dev/null', setname), 'r');
|
||||
if (!nft)
|
||||
return 0;
|
||||
const raw = nft.read('all');
|
||||
nft.close();
|
||||
if (!raw || length(raw) == 0)
|
||||
return 0;
|
||||
let data;
|
||||
try {
|
||||
data = json(raw);
|
||||
} catch(e) {
|
||||
return 0;
|
||||
}
|
||||
if (data?.nftables) {
|
||||
for (let item in data.nftables) {
|
||||
if (item?.set?.elem)
|
||||
return length(item.set.elem);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function get_nftset_members(setname) {
|
||||
if (!match(setname, /^[a-zA-Z0-9_-]+$/))
|
||||
return [];
|
||||
const nft = popen(sprintf('nft -j list set inet mwan3 %s 2>/dev/null', setname), 'r');
|
||||
if (!nft)
|
||||
return [];
|
||||
const raw = nft.read('all');
|
||||
nft.close();
|
||||
if (!raw || length(raw) == 0)
|
||||
return [];
|
||||
let data;
|
||||
try {
|
||||
data = json(raw);
|
||||
} catch(e) {
|
||||
return [];
|
||||
}
|
||||
const members = [];
|
||||
if (data?.nftables) {
|
||||
for (let item in data.nftables) {
|
||||
if (item?.set?.elem) {
|
||||
for (let elem in item.set.elem) {
|
||||
let val;
|
||||
if (type(elem) == 'object' && elem?.elem)
|
||||
val = parse_elem_val(elem.elem?.val);
|
||||
else
|
||||
val = parse_elem_val(elem);
|
||||
if (val != null)
|
||||
push(members, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
function get_nftset_elements(setname, max_count) {
|
||||
if (!match(setname, /^[a-zA-Z0-9_-]+$/))
|
||||
return { elements: [], truncated: false };
|
||||
const nft = popen(sprintf('nft -j list set inet mwan3 %s 2>/dev/null', setname), 'r');
|
||||
if (!nft)
|
||||
return { elements: [], truncated: false };
|
||||
const raw = nft.read('all');
|
||||
nft.close();
|
||||
if (!raw || length(raw) == 0)
|
||||
return { elements: [], truncated: false };
|
||||
let data;
|
||||
try {
|
||||
data = json(raw);
|
||||
} catch(e) {
|
||||
return { elements: [], truncated: false };
|
||||
}
|
||||
const elements = [];
|
||||
let truncated = false;
|
||||
let done = false;
|
||||
if (data?.nftables) {
|
||||
for (let item in data.nftables) {
|
||||
if (done) break;
|
||||
if (item?.set?.elem) {
|
||||
for (let elem in item.set.elem) {
|
||||
if (length(elements) >= max_count) {
|
||||
truncated = true;
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
let val = null;
|
||||
let packets = null;
|
||||
let bytes = null;
|
||||
if (type(elem) == 'object' && elem?.elem) {
|
||||
const inner = elem.elem;
|
||||
if (inner?.counter) {
|
||||
packets = inner.counter.packets;
|
||||
bytes = inner.counter.bytes;
|
||||
}
|
||||
val = parse_elem_val(inner?.val);
|
||||
} else {
|
||||
val = parse_elem_val(elem);
|
||||
}
|
||||
if (val != null) {
|
||||
const entry = { value: val };
|
||||
if (packets != null) {
|
||||
entry.packets = packets;
|
||||
entry.bytes = bytes;
|
||||
}
|
||||
push(elements, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { elements: elements, truncated: truncated };
|
||||
}
|
||||
|
||||
function get_ip_rules() {
|
||||
const rules = [];
|
||||
for (let family in [2, 10]) {
|
||||
const result = rtnl.request(rtnl.const.RTM_GETRULE, rtnl.const.NLM_F_DUMP,
|
||||
{ family: family }) ?? [];
|
||||
for (let r in result)
|
||||
push(rules, r);
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
function get_table_routes(table_id) {
|
||||
const routes = [];
|
||||
for (let family in [2, 10]) {
|
||||
const result = rtnl.request(rtnl.const.RTM_GETROUTE, rtnl.const.NLM_F_DUMP,
|
||||
{ family: family }) ?? [];
|
||||
for (let r in result)
|
||||
if (r.table == table_id)
|
||||
push(routes, r);
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
function nslookup_resolve(host, type) {
|
||||
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');
|
||||
p.close();
|
||||
if (!raw || length(raw) == 0) return [];
|
||||
const found = [];
|
||||
for (let line in split(raw, '\n')) {
|
||||
if (index(line, 'Address:') != 0) continue;
|
||||
const rest = ltrim(substr(line, 8));
|
||||
if (index(rest, '127.0.0.1') >= 0) continue;
|
||||
if (length(rest) > 0) push(found, rest);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
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;
|
||||
const mmdefault = (1 << bitcnt) - 1;
|
||||
const iface_max = mmdefault - 3;
|
||||
|
||||
let IIF_BASE = int(uci.get('mwan3', 'globals', 'iif_rule_base')) || 1000;
|
||||
let FWMARK_BASE = int(uci.get('mwan3', 'globals', 'fwmark_rule_base')) || 2000;
|
||||
let UNREACH_BASE = int(uci.get('mwan3', 'globals', 'unreachable_rule_base')) || 3000;
|
||||
if ((IIF_BASE + iface_max) >= FWMARK_BASE ||
|
||||
(FWMARK_BASE + iface_max + 1) >= UNREACH_BASE) {
|
||||
IIF_BASE = 1000;
|
||||
FWMARK_BASE = 2000;
|
||||
UNREACH_BASE = 3000;
|
||||
}
|
||||
|
||||
// Build ordered interface list with 1-based index (same ordering mwan3 uses)
|
||||
const iface_order = [];
|
||||
const iface_index = {};
|
||||
uci.foreach('mwan3', 'interface', intf => {
|
||||
push(iface_order, intf['.name']);
|
||||
iface_index[intf['.name']] = length(iface_order);
|
||||
});
|
||||
|
||||
// Fetch all ip rules and index by priority for O(1) lookup
|
||||
const all_rules = get_ip_rules();
|
||||
const rule_by_priority = {};
|
||||
for (let rule in all_rules) {
|
||||
if (rule?.priority != null)
|
||||
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[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];
|
||||
const iif_pri = IIF_BASE + id;
|
||||
const fwmark_pri = FWMARK_BASE + id;
|
||||
const unreach_pri = UNREACH_BASE + id;
|
||||
const iif_rule = rule_by_priority[iif_pri];
|
||||
const fwmark_rule = rule_by_priority[fwmark_pri];
|
||||
const unreach_rule = rule_by_priority[unreach_pri];
|
||||
const table_routes = get_table_routes(id);
|
||||
const default_routes = filter(table_routes, r =>
|
||||
r?.dst == null || r?.dst == '0.0.0.0/0' || r?.dst == '::/0'
|
||||
);
|
||||
const status = get_str(ifname, 'STATUS') || 'unknown';
|
||||
|
||||
result[ifname] = {
|
||||
index: id,
|
||||
status: status,
|
||||
iif_rule: {
|
||||
present: iif_rule != null,
|
||||
priority: iif_pri,
|
||||
},
|
||||
fwmark_rule: {
|
||||
present: fwmark_rule != null,
|
||||
priority: fwmark_pri,
|
||||
},
|
||||
unreach_rule: {
|
||||
present: unreach_rule != null,
|
||||
priority: unreach_pri,
|
||||
},
|
||||
table: {
|
||||
id: id,
|
||||
has_default: length(default_routes) > 0,
|
||||
default_routes: map(default_routes, r => ({
|
||||
dst: r.dst ?? 'default',
|
||||
gateway: r.gateway,
|
||||
dev: r.oif,
|
||||
family: (r.family == 10) ? 'ipv6' : 'ipv4',
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 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;
|
||||
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])
|
||||
push(stale, { priority: p });
|
||||
}
|
||||
|
||||
const mwan3_active = length(glob('/var/run/mwan3track/*/STATUS') || []) > 0;
|
||||
|
||||
return {
|
||||
interfaces: result,
|
||||
stale_rules: stale,
|
||||
mwan3_active: mwan3_active,
|
||||
rule_bases: { iif: IIF_BASE, fwmark: FWMARK_BASE, unreach: UNREACH_BASE },
|
||||
};
|
||||
}
|
||||
|
||||
function get_connected_ips(version) {
|
||||
const setname = nft_connected_set[version];
|
||||
const nft = popen(sprintf('nft -j list set inet mwan3 %s 2>/dev/null', setname), 'r');
|
||||
if (!nft) {
|
||||
return [];
|
||||
}
|
||||
const raw = nft.read('all');
|
||||
nft.close();
|
||||
|
||||
if (!raw || length(raw) == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = json(raw);
|
||||
} catch(e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ips = [];
|
||||
// nft JSON output: { nftables: [ { set: { elem: [ ... ] } } ] }
|
||||
if (data?.nftables) {
|
||||
for (let item in data.nftables) {
|
||||
if (item?.set?.elem) {
|
||||
for (let elem in item.set.elem) {
|
||||
if (type(elem) == 'string') {
|
||||
push(ips, elem);
|
||||
} else if (type(elem) == 'object') {
|
||||
// Handle prefix notation: { prefix: { addr: "x.x.x.x", len: N } }
|
||||
if (elem?.prefix) {
|
||||
push(ips, sprintf('%s/%d', elem.prefix.addr, elem.prefix.len));
|
||||
} else if (elem?.range) {
|
||||
push(ips, sprintf('%s-%s', elem.range[0], elem.range[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ips;
|
||||
}
|
||||
|
||||
function get_policies(version) {
|
||||
const uci = cursor();
|
||||
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']] = {
|
||||
interface: m['interface'],
|
||||
metric: int(m['metric'] || 1),
|
||||
weight: int(m['weight'] || 1),
|
||||
};
|
||||
});
|
||||
|
||||
const policies = {};
|
||||
|
||||
uci.foreach('mwan3', 'policy', p => {
|
||||
const pname = p['.name'];
|
||||
let use_members = p['use_member'];
|
||||
if (!use_members) return;
|
||||
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];
|
||||
if (!m) continue;
|
||||
if (iface_family[m.interface] != family_filter) continue;
|
||||
if (!by_metric[m.metric]) by_metric[m.metric] = [];
|
||||
push(by_metric[m.metric], {
|
||||
interface: m.interface,
|
||||
metric: m.metric,
|
||||
weight: m.weight,
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
for (let m in by_metric[metric]) {
|
||||
if (get_str(m.interface, 'STATUS') == 'online') {
|
||||
active_metric = metric;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (active_metric != null) break;
|
||||
}
|
||||
|
||||
// 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]) {
|
||||
if (get_str(m.interface, 'STATUS') == 'online')
|
||||
total_weight += m.weight;
|
||||
}
|
||||
}
|
||||
|
||||
policies[pname] = [];
|
||||
for (let metric in sorted_metrics) {
|
||||
for (let m in by_metric[metric]) {
|
||||
const iface_status = get_str(m.interface, 'STATUS') || 'offline';
|
||||
const is_active = (metric == active_metric) && (iface_status == 'online');
|
||||
const percent = (is_active && total_weight > 0) ? int(m.weight * 100 / total_weight) : 0;
|
||||
push(policies[pname], {
|
||||
'interface': m.interface,
|
||||
'metric': m.metric,
|
||||
'weight': m.weight,
|
||||
'percent': percent,
|
||||
'status': iface_status,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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_gateway = ucibool(intf['track_gateway'] || '0');
|
||||
const track_status = get_mwan3track_status(ifname, uci_track_ips, track_gateway, procd);
|
||||
const track_ips = [];
|
||||
const track_files = glob(sprintf('/var/run/mwan3track/%s/TRACK_*', ifname)) || [];
|
||||
for (let f in track_files) {
|
||||
const ip = substr(f, length(sprintf('/var/run/mwan3track/%s/TRACK_', ifname)));
|
||||
if (ip == 'OUTPUT') continue;
|
||||
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)),
|
||||
});
|
||||
}
|
||||
// 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));
|
||||
if (lat && length(rtrim(lat, '\n')) > 0) {
|
||||
check_quality = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
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 || false,
|
||||
'check_quality': check_quality,
|
||||
'track_ip': track_ips,
|
||||
};
|
||||
});
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
nftset_info: {
|
||||
args: {},
|
||||
call: function(request) {
|
||||
const nft = popen('nft -j list table inet mwan3 2>/dev/null', 'r');
|
||||
if (!nft)
|
||||
return { sets: {} };
|
||||
const raw = nft.read('all');
|
||||
nft.close();
|
||||
if (!raw || length(raw) == 0)
|
||||
return { sets: {} };
|
||||
let data;
|
||||
try {
|
||||
data = json(raw);
|
||||
} catch(e) {
|
||||
return { sets: {} };
|
||||
}
|
||||
const sets = {};
|
||||
if (data?.nftables) {
|
||||
for (let item in data.nftables) {
|
||||
if (item?.set) {
|
||||
const s = item.set;
|
||||
if (s.table == 'mwan3' && !match(s.name, /^mwan3_/)) {
|
||||
let counters = false;
|
||||
if (type(s.stmt) == 'array') {
|
||||
for (let stmt in s.stmt) {
|
||||
if (type(stmt) == 'object' && 'counter' in stmt) {
|
||||
counters = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sets[s.name] = { type: s.type, flags: s.flags || [], counters: counters, count: type(s.elem) == 'array' ? length(s.elem) : 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { sets: sets };
|
||||
}
|
||||
},
|
||||
nftset_members: {
|
||||
args: { set: '' },
|
||||
call: function(request) {
|
||||
const setname = request.args.set;
|
||||
if (!setname)
|
||||
return { error: 'set name required', members: [] };
|
||||
return { members: get_nftset_members(setname) };
|
||||
}
|
||||
},
|
||||
nftset_elements: {
|
||||
args: { set: '', max: 0 },
|
||||
call: function(request) {
|
||||
const setname = request.args.set;
|
||||
if (!setname)
|
||||
return { error: 'set name required', elements: [], truncated: false };
|
||||
const max = (request.args.max > 0) ? int(request.args.max) : 200;
|
||||
const result = get_nftset_elements(setname, max > 5000 ? 5000 : max);
|
||||
result.set = setname;
|
||||
return result;
|
||||
}
|
||||
},
|
||||
resolve_host: {
|
||||
args: { host: '', family: '' },
|
||||
call: function(request) {
|
||||
const host = request.args.host;
|
||||
if (!host || !match(host, /^[a-zA-Z0-9._-]+$/))
|
||||
return { error: 'invalid hostname', v4: [], v6: [] };
|
||||
const family = request.args.family || '';
|
||||
const v4 = (family !== 'ipv6') ? nslookup_resolve(host, 'A') : [];
|
||||
const v6 = (family !== 'ipv4') ? nslookup_resolve(host, 'AAAA') : [];
|
||||
return { v4: v4, v6: v6 };
|
||||
}
|
||||
},
|
||||
routing_health: {
|
||||
args: {},
|
||||
call: function(request) {
|
||||
return routing_health();
|
||||
}
|
||||
},
|
||||
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'),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
nftset_flush: {
|
||||
args: { set: '' },
|
||||
call: function(request) {
|
||||
const setname = request.args.set;
|
||||
if (!setname || !match(setname, /^[a-zA-Z0-9_-]+$/))
|
||||
return { error: 'invalid set name' };
|
||||
const proc = popen(sprintf('nft flush set inet mwan3 %s 2>&1', setname), 'r');
|
||||
if (!proc)
|
||||
return { error: 'failed to run nft' };
|
||||
const out = proc.read('all');
|
||||
const rc = proc.close();
|
||||
if (rc != 0)
|
||||
return { error: rtrim(out, '\n') || 'nft flush failed' };
|
||||
return {};
|
||||
}
|
||||
},
|
||||
nftset_reload: {
|
||||
args: { set: '' },
|
||||
call: function(request) {
|
||||
const setname = request.args.set;
|
||||
if (!setname || !match(setname, /^[a-zA-Z0-9_-]+$/))
|
||||
return { error: 'invalid set name' };
|
||||
|
||||
let proc = popen(sprintf('nft flush set inet mwan3 %s 2>&1', setname), 'r');
|
||||
if (!proc) return { error: 'failed to run nft' };
|
||||
let out = proc.read('all');
|
||||
let rc = proc.close();
|
||||
if (rc != 0) return { error: rtrim(out, '\n') || 'nft flush failed' };
|
||||
|
||||
const uci = cursor();
|
||||
let cfg = null;
|
||||
uci.foreach('mwan3', 'ipset', function(s) {
|
||||
if (s.name == setname) {
|
||||
if (s.enabled !== '0')
|
||||
cfg = s;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!cfg) return {};
|
||||
|
||||
const elements = [];
|
||||
let entries = cfg.entry || [];
|
||||
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 (cfg.loadfile) {
|
||||
const content = readfile(cfg.loadfile);
|
||||
if (content) {
|
||||
for (let line in split(content, '\n')) {
|
||||
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(elements) == 0) return {};
|
||||
|
||||
proc = popen(sprintf('nft add element inet mwan3 %s { %s } 2>&1',
|
||||
setname, join(', ', elements)), 'r');
|
||||
if (!proc) return { error: 'failed to run nft add element' };
|
||||
out = proc.read('all');
|
||||
rc = proc.close();
|
||||
if (rc != 0) return { error: rtrim(out, '\n') || 'nft add element failed' };
|
||||
return {};
|
||||
}
|
||||
},
|
||||
nftset_resolve: {
|
||||
args: { set: '' },
|
||||
call: function(request) {
|
||||
const setname = request.args.set;
|
||||
if (!setname || !match(setname, /^[a-zA-Z0-9_-]+$/))
|
||||
return { error: 'invalid set name' };
|
||||
|
||||
const uci = cursor();
|
||||
let cfg = null;
|
||||
uci.foreach('mwan3', 'ipset', function(s) {
|
||||
if (s.name == setname) {
|
||||
if (s.enabled !== '0')
|
||||
cfg = s;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!cfg) return { error: 'set not found in config' };
|
||||
|
||||
let domains = cfg.domain || [];
|
||||
if (type(domains) != 'array') domains = [domains];
|
||||
if (length(domains) == 0) return {};
|
||||
|
||||
ubus.call('service', 'signal', { name: 'dnsmasq', signal: 1 });
|
||||
|
||||
const qtype = (cfg.family == 'ipv6') ? 'AAAA' : 'A';
|
||||
let resolved = 0;
|
||||
for (let domain in domains) {
|
||||
const addrs = nslookup_resolve(domain, qtype);
|
||||
if (length(addrs) > 0) resolved++;
|
||||
}
|
||||
return { resolved: resolved };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'mwan3': methods };
|
||||
2748
mwan3-nft/mwan3-nftables-internals.html
Normal file
2748
mwan3-nft/mwan3-nftables-internals.html
Normal file
File diff suppressed because it is too large
Load Diff
310
mwan3-nft/src/mwan3ct.c
Normal file
310
mwan3-nft/src/mwan3ct.c
Normal file
@ -0,0 +1,310 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* mwan3ct - targeted conntrack flush helper for mwan3
|
||||
*
|
||||
* Uses libnetfilter_conntrack's NFCT_Q_FLUSH_FILTER to perform
|
||||
* kernel-side filtered conntrack entry deletion by mark and/or
|
||||
* status bits.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
#include <libnetfilter_conntrack/libnetfilter_conntrack.h>
|
||||
|
||||
/*
|
||||
* id2mask - expand a sequential bit index into a sparse mask
|
||||
*
|
||||
* Maps each set bit in 'id' (counting from bit 0) to the
|
||||
* corresponding set bit position in 'mask', producing a value
|
||||
* that occupies only the bit positions defined by 'mask'.
|
||||
*/
|
||||
|
||||
static uint32_t id2mask(uint32_t id, uint32_t mask)
|
||||
{
|
||||
uint32_t result = 0;
|
||||
uint32_t bit = 1;
|
||||
|
||||
for (uint32_t m = mask; m; m &= m - 1) {
|
||||
if (id & bit)
|
||||
result |= m & (-m);
|
||||
bit <<= 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* flush_filtered - flush conntrack entries matching mark and/or status
|
||||
*
|
||||
* Builds a kernel-side filter from the supplied mark and status
|
||||
* value/mask pairs, then issues a single NFCT_Q_FLUSH_FILTER query.
|
||||
* ENOENT (no matching entries) is treated as success.
|
||||
*/
|
||||
|
||||
static int flush_filtered(struct nfct_handle *h,
|
||||
uint32_t mark_val, uint32_t mark_mask,
|
||||
uint32_t status_val, uint32_t status_mask,
|
||||
int have_mark, int have_status)
|
||||
{
|
||||
struct nfct_filter_dump *filter = NULL;
|
||||
int ret;
|
||||
|
||||
filter = nfct_filter_dump_create();
|
||||
if (!filter) {
|
||||
fprintf(stderr, "mwan3ct: nfct_filter_dump_create: %s\n",
|
||||
strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (have_mark) {
|
||||
struct nfct_filter_dump_mark mark = {
|
||||
.val = mark_val,
|
||||
.mask = mark_mask,
|
||||
};
|
||||
nfct_filter_dump_set_attr(filter, NFCT_FILTER_DUMP_MARK,
|
||||
&mark);
|
||||
}
|
||||
if (have_status) {
|
||||
struct nfct_filter_dump_mark status = {
|
||||
.val = status_val,
|
||||
.mask = status_mask,
|
||||
};
|
||||
nfct_filter_dump_set_attr(filter, NFCT_FILTER_DUMP_STATUS,
|
||||
&status);
|
||||
}
|
||||
|
||||
ret = nfct_query(h, NFCT_Q_FLUSH_FILTER, filter);
|
||||
nfct_filter_dump_destroy(filter);
|
||||
|
||||
if (ret == -1 && errno != ENOENT)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* flush_any_marked - flush conntrack entries for all mark values in a mask
|
||||
*
|
||||
* Iterates over every non-zero combination of bits within 'mask'
|
||||
* and issues a filtered flush for each. Used when the caller wants
|
||||
* to remove all mwan3-marked entries regardless of which interface
|
||||
* they belong to.
|
||||
*/
|
||||
|
||||
static int flush_any_marked(struct nfct_handle *h, uint32_t mask,
|
||||
uint32_t status_val, uint32_t status_mask,
|
||||
int have_status)
|
||||
{
|
||||
int bitcount = __builtin_popcount(mask);
|
||||
uint32_t max_id;
|
||||
|
||||
if (bitcount > 16) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
max_id = (1U << bitcount) - 1;
|
||||
|
||||
for (uint32_t id = 1; id <= max_id; id++) {
|
||||
uint32_t mark_val = id2mask(id, mask);
|
||||
int ret = flush_filtered(h, mark_val, mask, status_val,
|
||||
status_mask, 1, have_status);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* parse_u32 - parse a string as a uint32_t with full validation
|
||||
*
|
||||
* Rejects negative values, overflow, and empty input. On success
|
||||
* *endp points past the last consumed character, allowing the caller
|
||||
* to check for trailing garbage. Returns 0 on success, -1 on failure.
|
||||
*/
|
||||
|
||||
static int parse_u32(const char *s, char **endp, uint32_t *out)
|
||||
{
|
||||
unsigned long tmp;
|
||||
|
||||
if (*s == '-')
|
||||
return -1;
|
||||
|
||||
errno = 0;
|
||||
tmp = strtoul(s, endp, 0);
|
||||
if (*endp == s || errno)
|
||||
return -1;
|
||||
if ((uint32_t)tmp != tmp)
|
||||
return -1;
|
||||
*out = (uint32_t)tmp;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* parse_val_mask - parse a "value/mask" string into two uint32 values
|
||||
*
|
||||
* Accepts "val/mask" or just "val", in which case the mask defaults
|
||||
* to the value itself. Returns 0 on success, -1 on parse failure.
|
||||
*/
|
||||
|
||||
static int parse_val_mask(const char *arg, uint32_t *val, uint32_t *mask)
|
||||
{
|
||||
char *end = NULL;
|
||||
|
||||
if (parse_u32(arg, &end, val))
|
||||
return -1;
|
||||
|
||||
if (*end == '/') {
|
||||
if (parse_u32(end + 1, &end, mask))
|
||||
return -1;
|
||||
if (*end != '\0')
|
||||
return -1;
|
||||
} else if (*end == '\0') {
|
||||
*mask = *val;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* usage - print command usage to stderr
|
||||
*/
|
||||
|
||||
static void usage(void)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"Usage: mwan3ct flush [--mark <val>/<mask>] "
|
||||
"[--mark-any <mask>] [--status <val>/<mask>]\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* main - parse arguments and dispatch the requested flush operation
|
||||
*
|
||||
* Supports three mutually exclusive filter modes: --mark (exact
|
||||
* value/mask match), --mark-any (iterate all non-zero mark values
|
||||
* within a mask), and --status (conntrack status bit filter).
|
||||
* --mark and --status, or --mark-any and --status, may be combined.
|
||||
*/
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
uint32_t mark_val = 0, mark_mask = 0;
|
||||
uint32_t status_val = 0, status_mask = 0;
|
||||
int have_mark = 0, have_mark_any = 0, have_status = 0;
|
||||
struct nfct_handle *h = NULL;
|
||||
int ret;
|
||||
|
||||
if (argc >= 2 && (strcmp(argv[1], "--help") == 0 ||
|
||||
strcmp(argv[1], "-h") == 0)) {
|
||||
usage();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (argc < 2 || strcmp(argv[1], "flush") != 0) {
|
||||
usage();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
for (int i = 2; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--mark") == 0 && i + 1 < argc) {
|
||||
if (have_mark) {
|
||||
fprintf(stderr,
|
||||
"mwan3ct: duplicate --mark\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (parse_val_mask(argv[++i], &mark_val, &mark_mask)) {
|
||||
fprintf(stderr, "mwan3ct: bad --mark value\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
have_mark = 1;
|
||||
} else if (strcmp(argv[i], "--mark-any") == 0 && i + 1 < argc) {
|
||||
char *end;
|
||||
|
||||
if (have_mark_any) {
|
||||
fprintf(stderr,
|
||||
"mwan3ct: duplicate --mark-any\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (parse_u32(argv[++i], &end, &mark_mask) ||
|
||||
*end != '\0' || !mark_mask) {
|
||||
fprintf(stderr,
|
||||
"mwan3ct: bad --mark-any mask\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
have_mark_any = 1;
|
||||
} else if (strcmp(argv[i], "--status") == 0 && i + 1 < argc) {
|
||||
if (have_status) {
|
||||
fprintf(stderr,
|
||||
"mwan3ct: duplicate --status\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (parse_val_mask(argv[++i],
|
||||
&status_val, &status_mask)) {
|
||||
fprintf(stderr,
|
||||
"mwan3ct: bad --status value\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
have_status = 1;
|
||||
} else {
|
||||
usage();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
if (have_mark && have_mark_any) {
|
||||
fprintf(stderr,
|
||||
"mwan3ct: --mark and --mark-any "
|
||||
"are mutually exclusive\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (!have_mark && !have_mark_any && !have_status) {
|
||||
fprintf(stderr, "mwan3ct: at least one filter required\n");
|
||||
usage();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (have_mark && !mark_mask) {
|
||||
fprintf(stderr, "mwan3ct: --mark mask must be non-zero\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (have_mark && (mark_val & ~mark_mask)) {
|
||||
fprintf(stderr,
|
||||
"mwan3ct: --mark value has bits outside mask\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (have_status && !status_mask) {
|
||||
fprintf(stderr,
|
||||
"mwan3ct: --status mask must be non-zero\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (have_status && (status_val & ~status_mask)) {
|
||||
fprintf(stderr,
|
||||
"mwan3ct: --status value has bits outside mask\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
h = nfct_open(CONNTRACK, 0);
|
||||
if (!h) {
|
||||
fprintf(stderr, "mwan3ct: nfct_open: %s\n", strerror(errno));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (have_mark_any)
|
||||
ret = flush_any_marked(h, mark_mask, status_val,
|
||||
status_mask, have_status);
|
||||
else
|
||||
ret = flush_filtered(h, mark_val, mark_mask,
|
||||
status_val, status_mask,
|
||||
have_mark, have_status);
|
||||
|
||||
nfct_close(h);
|
||||
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "mwan3ct: flush failed: %s\n", strerror(errno));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
123
mwan3-nft/src/mwan3ipcheck.c
Normal file
123
mwan3-nft/src/mwan3ipcheck.c
Normal file
@ -0,0 +1,123 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* mwan3ipcheck - validate and classify IP addresses and CIDR notation
|
||||
*
|
||||
* Uses inet_pton() to determine whether a string is a valid IPv4 or
|
||||
* IPv6 address, optionally with a CIDR prefix length.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
static int classify(const char *arg)
|
||||
{
|
||||
char buf[64];
|
||||
unsigned char addr[sizeof(struct in6_addr)];
|
||||
const char *pfxstr = NULL;
|
||||
int family;
|
||||
size_t len;
|
||||
|
||||
len = strlen(arg);
|
||||
if (len == 0 || len >= sizeof(buf))
|
||||
return 0;
|
||||
|
||||
memcpy(buf, arg, len + 1);
|
||||
|
||||
char *slash = strchr(buf, '/');
|
||||
if (slash) {
|
||||
*slash = '\0';
|
||||
pfxstr = slash + 1;
|
||||
if (*pfxstr == '\0')
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (inet_pton(AF_INET, buf, addr) == 1)
|
||||
family = AF_INET;
|
||||
else if (inet_pton(AF_INET6, buf, addr) == 1)
|
||||
family = AF_INET6;
|
||||
else
|
||||
return 0;
|
||||
|
||||
if (pfxstr) {
|
||||
char *end;
|
||||
long pfx;
|
||||
int max_pfx = (family == AF_INET) ? 32 : 128;
|
||||
|
||||
if (pfxstr[0] == '0' && pfxstr[1] != '\0')
|
||||
return 0;
|
||||
|
||||
pfx = strtol(pfxstr, &end, 10);
|
||||
if (*end != '\0' || end == pfxstr)
|
||||
return 0;
|
||||
if (pfx < 0 || pfx > max_pfx)
|
||||
return 0;
|
||||
}
|
||||
|
||||
return family;
|
||||
}
|
||||
|
||||
static int classify_list(const char *arg)
|
||||
{
|
||||
char buf[512];
|
||||
char *token = NULL, *saveptr = NULL;
|
||||
int common_family = 0;
|
||||
|
||||
size_t len = strlen(arg);
|
||||
if (len == 0 || len >= sizeof(buf))
|
||||
return 0;
|
||||
|
||||
memcpy(buf, arg, len + 1);
|
||||
|
||||
for (token = strtok_r(buf, ",", &saveptr);
|
||||
token;
|
||||
token = strtok_r(NULL, ",", &saveptr)) {
|
||||
while (*token && isspace((unsigned char)*token))
|
||||
token++;
|
||||
size_t tlen = strlen(token);
|
||||
while (tlen > 0 && isspace((unsigned char)token[tlen - 1]))
|
||||
token[--tlen] = '\0';
|
||||
if (tlen == 0)
|
||||
continue;
|
||||
|
||||
int family = classify(token);
|
||||
if (family == 0)
|
||||
return 0;
|
||||
|
||||
if (common_family == 0)
|
||||
common_family = family;
|
||||
else if (family != common_family)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (common_family == 0)
|
||||
return 0;
|
||||
|
||||
return common_family;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
int result;
|
||||
|
||||
if (argc != 2)
|
||||
result = 0;
|
||||
else
|
||||
result = classify_list(argv[1]);
|
||||
|
||||
if (result == AF_INET)
|
||||
puts("ipv4");
|
||||
else if (result == AF_INET6)
|
||||
puts("ipv6");
|
||||
else if (result == -1) {
|
||||
puts("mixed");
|
||||
return EXIT_FAILURE;
|
||||
} else {
|
||||
puts("invalid");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
260
mwan3-nft/src/sockopt_wrap.c
Normal file
260
mwan3-nft/src/sockopt_wrap.c
Normal file
@ -0,0 +1,260 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Copyright (C) 2020 Aaron Goodman <aaronjg@alumni.stanford.edu>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
/*
|
||||
* sockopt_wrap.c provides a shared library that intercepts syscalls to various
|
||||
* networking functions to bind the sockets a source IP address and network device
|
||||
* and to set the firewall mark on otugoing packets. Parameters are set using the
|
||||
* DEVICE, SRCIP, FWMARK environment variables.
|
||||
*
|
||||
* Additionally the FAMILY environment variable can be set to either 'ipv4' or
|
||||
* 'ipv6' to cause sockets opened with ipv6 or ipv4 to fail, respectively.
|
||||
*
|
||||
* Each environment variable is optional, and if not set, the library will not
|
||||
* enforce the particular parameter.
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <net/ethernet.h>
|
||||
#include <linux/if_packet.h>
|
||||
#include <net/if.h>
|
||||
|
||||
static int (*next_socket)(int domain, int type, int protocol);
|
||||
static int (*next_setsockopt)(int sockfd, int level, int optname,
|
||||
const void *optval, socklen_t optlen);
|
||||
static int (*next_bind)(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
||||
static int (*next_close)(int fd);
|
||||
static ssize_t (*next_send)(int sockfd, const void *buf, size_t len, int flags);
|
||||
static ssize_t (*next_sendto)(int sockfd, const void *buf, size_t len, int flags,
|
||||
const struct sockaddr *dest_addr, socklen_t addrlen);
|
||||
static ssize_t (*next_sendmsg)(int sockfd, const struct msghdr *msg, int flags);
|
||||
static int (*next_connect)(int sockfd, const struct sockaddr *addr,
|
||||
socklen_t addrlen);
|
||||
static int device=0;
|
||||
static struct sockaddr_in source4 = {0};
|
||||
#ifdef CONFIG_IPV6
|
||||
static struct sockaddr_in6 source6 = {0};
|
||||
#endif
|
||||
static struct sockaddr * source = 0;
|
||||
static int sockaddr_size = 0;
|
||||
static int is_bound [1024] = {0};
|
||||
|
||||
#define next_func(x)\
|
||||
void set_next_##x(){\
|
||||
if (next_##x) return;\
|
||||
next_##x = dlsym(RTLD_NEXT, #x);\
|
||||
dlerror_handle();\
|
||||
return;\
|
||||
}
|
||||
|
||||
void dlerror_handle()
|
||||
{
|
||||
char *msg;
|
||||
if ((msg = dlerror()) != NULL) {
|
||||
fprintf(stderr, "socket: dlopen failed : %s\n", msg);
|
||||
fflush(stderr);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
next_func(bind);
|
||||
next_func(close);
|
||||
next_func(setsockopt);
|
||||
next_func(socket);
|
||||
next_func(send);
|
||||
next_func(sendto);
|
||||
next_func(sendmsg);
|
||||
next_func(connect);
|
||||
|
||||
void dobind(int sockfd)
|
||||
{
|
||||
if (source && sockfd < 1024 && !is_bound[sockfd]) {
|
||||
set_next_bind();
|
||||
if (next_bind(sockfd, source, sockaddr_size)) {
|
||||
perror("mwan3 sockopt_wrap: failed to bind to source address");
|
||||
next_close(sockfd);
|
||||
/* Do not exit — let the caller's syscall fail with EBADF so
|
||||
* the tracked process exits with a normal error code. This
|
||||
* can happen when SRC_IP becomes stale (e.g. after a DHCP
|
||||
* address change) and must not terminate the ping abruptly
|
||||
* with no log from mwan3track. */
|
||||
return;
|
||||
}
|
||||
is_bound[sockfd] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
|
||||
{
|
||||
set_next_connect();
|
||||
dobind(sockfd);
|
||||
return next_connect(sockfd, addr, addrlen);
|
||||
}
|
||||
|
||||
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
|
||||
{
|
||||
set_next_send();
|
||||
dobind(sockfd);
|
||||
return next_send(sockfd, buf, len, flags);
|
||||
}
|
||||
|
||||
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
|
||||
const struct sockaddr *dest_addr, socklen_t addrlen)
|
||||
{
|
||||
set_next_sendto();
|
||||
dobind(sockfd);
|
||||
return next_sendto(sockfd, buf, len, flags, dest_addr, addrlen);
|
||||
}
|
||||
|
||||
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)
|
||||
{
|
||||
set_next_sendmsg();
|
||||
dobind(sockfd);
|
||||
return next_sendmsg(sockfd, msg, flags);
|
||||
}
|
||||
|
||||
int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen)
|
||||
{
|
||||
set_next_bind();
|
||||
if (device && addr->sa_family == AF_PACKET) {
|
||||
((struct sockaddr_ll*)addr)->sll_ifindex=device;
|
||||
}
|
||||
else if (source && addr->sa_family == AF_INET) {
|
||||
((struct sockaddr_in*)addr)->sin_addr = source4.sin_addr;
|
||||
}
|
||||
#ifdef CONFIG_IPV6
|
||||
else if (source && addr->sa_family == AF_INET6) {
|
||||
((struct sockaddr_in6*)addr)->sin6_addr = source6.sin6_addr;
|
||||
}
|
||||
#endif
|
||||
if (sockfd < 1024)
|
||||
is_bound[sockfd] = 1;
|
||||
return next_bind(sockfd, addr, addrlen);
|
||||
}
|
||||
|
||||
int close (int sockfd)
|
||||
{
|
||||
set_next_close();
|
||||
if (sockfd < 1024)
|
||||
is_bound[sockfd]=0;
|
||||
return next_close(sockfd);
|
||||
}
|
||||
|
||||
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)
|
||||
{
|
||||
set_next_setsockopt();
|
||||
if (level == SOL_SOCKET && (optname == SO_MARK || optname == SO_BINDTODEVICE))
|
||||
return 0;
|
||||
return next_setsockopt(sockfd, level, optname, optval, optlen);
|
||||
}
|
||||
|
||||
int socket(int domain, int type, int protocol)
|
||||
{
|
||||
int handle;
|
||||
|
||||
const char *socket_str = getenv("DEVICE");
|
||||
const char *srcip_str = getenv("SRCIP");
|
||||
const char *fwmark_str = getenv("FWMARK");
|
||||
const char *family_str = getenv("FAMILY");
|
||||
const int iface_len = socket_str ? strnlen(socket_str, IFNAMSIZ) : 0;
|
||||
int has_family = family_str && *family_str != 0;
|
||||
int has_srcip = srcip_str && *srcip_str != 0;
|
||||
const int fwmark = fwmark_str ? (int)strtol(fwmark_str, NULL, 0) : 0;
|
||||
|
||||
set_next_close();
|
||||
set_next_socket();
|
||||
set_next_send();
|
||||
set_next_setsockopt();
|
||||
set_next_sendmsg();
|
||||
set_next_sendto();
|
||||
set_next_connect();
|
||||
if(has_family) {
|
||||
#ifdef CONFIG_IPV6
|
||||
if(domain == AF_INET && strncmp(family_str,"ipv6",4) == 0)
|
||||
return -1;
|
||||
#endif
|
||||
if(domain == AF_INET6 && strncmp(family_str,"ipv4",4) == 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (domain != AF_INET
|
||||
#ifdef CONFIG_IPV6
|
||||
&& domain != AF_INET6
|
||||
#endif
|
||||
) {
|
||||
return next_socket(domain, type, protocol);
|
||||
}
|
||||
|
||||
|
||||
if (iface_len > 0) {
|
||||
if (iface_len == IFNAMSIZ) {
|
||||
fprintf(stderr, "mwan3 sockopt_wrap: interface name too long\n");
|
||||
fflush(stderr);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_srcip) {
|
||||
int s;
|
||||
void * addr_buf;
|
||||
if (domain == AF_INET) {
|
||||
addr_buf = &source4.sin_addr;
|
||||
sockaddr_size=sizeof source4;
|
||||
memset(&source4, 0, sockaddr_size);
|
||||
source4.sin_family = domain;
|
||||
source = (struct sockaddr*)&source4;
|
||||
}
|
||||
#ifdef CONFIG_IPV6
|
||||
else {
|
||||
addr_buf = &source6.sin6_addr;
|
||||
sockaddr_size=sizeof source6;
|
||||
memset(&source6, 0, sockaddr_size);
|
||||
source6.sin6_family=domain;
|
||||
source = (struct sockaddr*)&source6;
|
||||
}
|
||||
#endif
|
||||
s = inet_pton(domain, srcip_str, addr_buf);
|
||||
if (s == 0) {
|
||||
fprintf(stderr, "socket: ip address invalid format for family %s\n",
|
||||
domain == AF_INET ? "AF_INET" : domain == AF_INET6 ?
|
||||
"AF_INET6" : "unknown");
|
||||
return -1;
|
||||
}
|
||||
if (s < 0) {
|
||||
perror("inet_pton");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
handle = next_socket(domain, type, protocol);
|
||||
if (handle == -1 ) {
|
||||
return handle;
|
||||
}
|
||||
|
||||
if (iface_len > 0) {
|
||||
device=if_nametoindex(socket_str);
|
||||
if (next_setsockopt(handle, SOL_SOCKET, SO_BINDTODEVICE,
|
||||
socket_str, iface_len + 1)) {
|
||||
perror("mwan3 sockopt_wrap: failed to bind to interface");
|
||||
next_close(handle);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (fwmark > 0) {
|
||||
if (next_setsockopt(handle, SOL_SOCKET, SO_MARK,
|
||||
&fwmark, sizeof fwmark)) {
|
||||
perror("mwan3 sockopt_wrap: failed to set firewall mark");
|
||||
next_close(handle);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user