Files

310 lines
11 KiB
Bash
Executable File

#!/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
mwan3_set_member_backstops
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 four
# rule-base ranges (iif, fwmark, unreachable, source). The
# fwmark range extends to fwmark_base + MM_UNREACHABLE to cover
# the global blackhole and unreachable rules at fwmark_base +
# MM_BLACKHOLE / MM_UNREACHABLE. The source band is not
# configurable; it is derived from the unreachable base and
# sits immediately above the unreachable band.
# content gate -- the line either references a mwan3-owned routing
# table (lookup <N> with N in 1..MWAN3_INTERFACE_MAX) or carries
# an fwmark masked by mwan3's MMX_MASK. The priority gate alone
# 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 src_min src_max mask
iif_min=$((MWAN3_IIF_RULE_BASE + 1))
iif_max=$((MWAN3_IIF_RULE_BASE + MWAN3_INTERFACE_MAX))
fw_min=$((MWAN3_FWMARK_RULE_BASE + 1))
fw_max=$((MWAN3_FWMARK_RULE_BASE + MM_UNREACHABLE))
ur_min=$((MWAN3_UNREACHABLE_RULE_BASE + 1))
ur_max=$((MWAN3_UNREACHABLE_RULE_BASE + MWAN3_INTERFACE_MAX))
src_min=$((MWAN3_UNREACHABLE_RULE_BASE + MWAN3_INTERFACE_MAX + 2))
src_max=$((MWAN3_UNREACHABLE_RULE_BASE + 2 * MWAN3_INTERFACE_MAX + 1))
# ip rule list prints fwmark masks lowercase; normalise for matching
mask=$(echo "$MMX_MASK" | tr 'A-Z' 'a-z')
$IP rule list | while read -r line; do
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" ]; } || \
{ [ "$pref" -ge "$src_min" ] && [ "$pref" -le "$src_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
# Remove the table entirely. The single atomic delete drops every
# chain, set and map mwan3 created in one kernel transaction, so a
# stopped mwan3 leaves no nft residue.
$NFT delete table inet mwan3 >/dev/null 2>&1
rm -rf $MWAN3_STATUS_DIR $MWAN3TRACK_STATUS_DIR
}
reload_service() {
mwan3_init
# The nft table is created only by the skeleton load in start_service,
# which is skipped when the service starts with every interface
# disabled. In that state the table is absent while the status
# directory (created by mwan3_init) exists, so the service reports
# running and a config change drives a reload, not a start. Rebuilding
# the ruleset here would batch every add against a missing table, the
# atomic commit would fail, and this function would return at the commit
# check before the tracker-mismatch restart below. Recover with a full
# stop and start, which reloads the skeleton and recreates the table.
if ! $NFT list table inet mwan3 >/dev/null 2>&1; then
LOG notice "reload escalated to restart as table inet mwan3 not initialised"
stop
start
return
fi
# Atomically rebuild the entire mwan3 nft ruleset in a single kernel
# transaction. mwan3_nft_reload_start opens the outermost batch level
# (MWAN3_BATCH_DEPTH 0->1) so all subsequent batch_start/commit calls
# 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
mwan3_set_member_backstops
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'
}