Files

169 lines
6.9 KiB
Bash

# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2025 ImmortalWrt.org
#
# Routing/network plumbing for homeproxy-pro.
#
# The tproxy and TUN paths need ip rules, a routing table and (for TUN) the
# device itself before procd starts sing-box, and they need to be torn down
# again on stop. All of that used to live inline in /etc/init.d/homeproxy-pro.
#
# Layering note: this is a device-side module. It calls `log` and, like the
# other runtime modules, requires `config_load` to have run (it reads the
# infra marks and the TUN device name itself).
#
# Sourced by /etc/init.d/homeproxy-pro.
# hp_net_wait_wan [seconds]
# Block until a default route or an "up" wan interface exists. Bounded: after
# the timeout sing-box is started anyway, because a missing default route is
# a common transient state at boot and refusing to start would be worse.
hp_net_wait_wan() {
local limit="${1:-60}"
local wan_waited=0
while ! { ip route show default 2>"/dev/null" | grep -q . \
|| ip -6 route show default 2>"/dev/null" | grep -q . \
|| ifstatus wan 2>"/dev/null" | grep -q '"up": true'; }; do
wan_waited=$((wan_waited + 1))
if [ "$wan_waited" -ge "$limit" ]; then
log "WARNING: No network route after ${limit}s, starting sing-box anyway."
break
fi
sleep 1
done
}
# hp_net_try <描述> <命令...>
# Run a step that may legitimately fail, logging the wrapped message.
hp_net_try() {
local message="$1"; shift
"$@" 2>"/dev/null" || log "Error: ${message}"
}
# hp_net_rule_add <family> <fwmark> <table> <描述>
# Install one ip rule idempotently. `ip rule add` has no replace form and the
# kernel appends a second identical rule instead of reporting a conflict, so a
# repeated start (rc.common's `start` never calls stop_service) used to leave
# duplicates behind. The teardown side already drains them with `while del`;
# this is the matching install side. family is "" for ip, "-6" for ip -6.
hp_net_rule_add() {
local family="$1" mark="$2" table="$3" message="$4"
while ip $family rule del fwmark "$mark" table "$table" 2>"/dev/null"; do :; done
hp_net_try "$message" ip $family rule add fwmark "$mark" table "$table"
}
# hp_net_setup <proxy-mode> <routing-mode>
# Create the tproxy or TUN routing state for the configured proxy mode.
# Requires config_load.
#
# Every step is idempotent on purpose: `start` on an already-running service
# re-runs this function without a preceding teardown. The old code reported
# "failed to add the tproxy local route (table 100)" on every such start
# (EEXIST from `ip route add`) while silently appending a duplicate ip rule.
# Routes use `replace`, rules are drained before they are added, and the TUN
# device is only created when it does not already exist.
hp_net_setup() {
local proxy_mode="$1"
local routing_mode="$2"
local ipv6_support table_mark outbound_udp_node tproxy_mark tun_name tun_mark
config_get_bool ipv6_support "config" "ipv6_support" "0"
config_get table_mark "infra" "table_mark" "100"
case "$proxy_mode" in
"redirect_tproxy")
config_get outbound_udp_node "config" "main_udp_node" "nil"
if [ "$outbound_udp_node" != "nil" ] || [ "$routing_mode" = "custom" ]; then
config_get tproxy_mark "infra" "tproxy_mark" "101"
hp_net_rule_add "" "$tproxy_mark" "$table_mark" \
"failed to add the tproxy ip rule (fwmark ${tproxy_mark}, table ${table_mark})."
hp_net_try "failed to add the tproxy local route (table ${table_mark})." \
ip route replace local 0.0.0.0/0 dev lo table "$table_mark"
if [ "$ipv6_support" -eq "1" ]; then
hp_net_rule_add "-6" "$tproxy_mark" "$table_mark" \
"failed to add the tproxy IPv6 rule (fwmark ${tproxy_mark}, table ${table_mark})."
hp_net_try "failed to add the tproxy IPv6 local route (table ${table_mark})." \
ip -6 route replace local ::/0 dev lo table "$table_mark"
fi
fi
;;
"redirect_tun"|"tun")
config_get tun_name "infra" "tun_name" "singtun0"
config_get tun_mark "infra" "tun_mark" "102"
# Only wait after a real creation: the old unconditional `sleep 1s`
# delayed every start (including reloads) even when the device was
# already present.
if ! ip link show "$tun_name" >"/dev/null" 2>&1; then
hp_net_try "failed to create the TUN device ${tun_name}." \
ip tuntap add mode tun user root name "$tun_name"
sleep 1s
fi
hp_net_try "failed to bring up the TUN device ${tun_name}." \
ip link set "$tun_name" up
hp_net_try "failed to set the TUN default route (table ${table_mark})." \
ip route replace default dev "$tun_name" table "$table_mark"
hp_net_rule_add "" "$tun_mark" "$table_mark" \
"failed to add the TUN ip rule (fwmark ${tun_mark}, table ${table_mark})."
if [ "$ipv6_support" -eq "1" ]; then
hp_net_try "failed to set the TUN IPv6 default route (table ${table_mark})." \
ip -6 route replace default dev "$tun_name" table "$table_mark"
hp_net_rule_add "-6" "$tun_mark" "$table_mark" \
"failed to add the TUN IPv6 rule (fwmark ${tun_mark}, table ${table_mark})."
fi
;;
esac
}
# hp_net_teardown
# Remove every tproxy/TUN rule and route homeproxy-pro added. Requires
# config_load.
#
# ip rule del removes a single matching rule, so a repeated start (or an
# upgrade that starts before the old instance stopped) leaves duplicates
# behind. Delete every match instead of only the first one.
hp_net_teardown() {
local table_mark tproxy_mark tun_mark tun_name
config_get table_mark "infra" "table_mark" "100"
config_get tproxy_mark "infra" "tproxy_mark" "101"
config_get tun_mark "infra" "tun_mark" "102"
config_get tun_name "infra" "tun_name" "singtun0"
while ip rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"; do :; done
ip route del local 0.0.0.0/0 dev lo table "$table_mark" 2>"/dev/null"
while ip -6 rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"; do :; done
ip -6 route del local ::/0 dev lo table "$table_mark" 2>"/dev/null"
ip route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
while ip rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"; do :; done
ip -6 route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
while ip -6 rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"; do :; done
}
# hp_net_remove_tun
# service_stopped() hook on the genuine-stop path only (HP_RELOAD_IN_PROGRESS=0):
# procd calls it after the instances are gone, so the TUN device can be removed.
# Requires config_load.
#
# On the reload path (HP_RELOAD_IN_PROGRESS=1) the caller skips this entirely,
# so the device survives across the stop;start that reload_service drives; the
# table-100 default route is therefore not pulled mid-reload - the
# "tun mode reload exposes the LAN with the real IP" failure that the previous
# TODO documented (review P2, 2026-09-28) is the issue the caller now closes.
hp_net_remove_tun() {
local tun_name
config_get tun_name "infra" "tun_name" "singtun0"
ip link set "$tun_name" down 2>"/dev/null"
ip tuntap del mode tun name "$tun_name" 2>"/dev/null"
}