mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-09-18 06:14:15 +08:00
2404 lines
86 KiB
Bash
Executable File
2404 lines
86 KiB
Bash
Executable File
#!/bin/sh /etc/rc.common
|
||
USE_PROCD=1
|
||
EXTRA_COMMANDS="refresh_cron"
|
||
|
||
START=99
|
||
STOP=15
|
||
|
||
CORE_LOG_PATH="/var/log/clashoo/core.log"
|
||
|
||
. /usr/share/clashoo/runtime/dns_helpers.sh 2>/dev/null || true
|
||
|
||
command_exists() {
|
||
command -v "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
shell_quote() {
|
||
printf "'"
|
||
printf '%s' "$1" | sed "s/'/'\\\\''/g"
|
||
printf "'"
|
||
}
|
||
|
||
use_fw4_backend() {
|
||
command_exists fw4 && command_exists nft
|
||
}
|
||
|
||
backend_name() {
|
||
if use_fw4_backend; then
|
||
echo "fw4"
|
||
else
|
||
echo "unsupported"
|
||
fi
|
||
}
|
||
|
||
backend_apply_rules() {
|
||
if use_fw4_backend; then
|
||
/usr/share/clashoo/net/fw4.sh apply
|
||
else
|
||
_log_runtime_error "Current environment requires fw4+nft support"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
backend_remove_rules() {
|
||
if use_fw4_backend; then
|
||
/usr/share/clashoo/net/fw4.sh remove
|
||
else
|
||
return 0
|
||
fi
|
||
}
|
||
|
||
_clashoo_enabled() {
|
||
[ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]
|
||
}
|
||
|
||
# Compute start-wait window: TUN needs longer for utun setup + fake-ip pool init.
|
||
# Override via `uci set clashoo.config.start_wait_secs=N` for slow devices.
|
||
_compute_start_wait() {
|
||
local override
|
||
override="$(uci -q get clashoo.config.start_wait_secs)"
|
||
if [ -n "$override" ] && [ "$override" -gt 0 ] 2>/dev/null; then
|
||
echo "$override"; return
|
||
fi
|
||
if [ "$(uci -q get clashoo.config.tun_mode)" = "1" ]; then
|
||
echo 15
|
||
else
|
||
echo 6
|
||
fi
|
||
}
|
||
|
||
# Wait up to $1 seconds for $2 (mihomo|sing-box) to be alive AND stable.
|
||
# Returns 0 if alive at end of window, 1 if dead/never-started.
|
||
_singbox_instance_pid() {
|
||
ubus call service list "{\"name\":\"sing-box\",\"verbose\":true}" 2>/dev/null \
|
||
| jsonfilter -e '@["sing-box"].instances.*.pid' 2>/dev/null | head -n1
|
||
}
|
||
|
||
_singbox_running() {
|
||
local _pid
|
||
_pid="$(_singbox_instance_pid)"
|
||
[ -n "$_pid" ] && [ -d "/proc/$_pid" ]
|
||
}
|
||
|
||
_wait_core_alive() {
|
||
local max="${1:-6}" want="${2:-mihomo}" i=0
|
||
while [ "$i" -lt "$max" ]; do
|
||
sleep 1
|
||
if [ "$want" = "sing-box" ] || [ "$want" = "singbox" ]; then
|
||
_singbox_running && return 0
|
||
else
|
||
_core_running && return 0
|
||
fi
|
||
i=$((i + 1))
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# Roll back partial state when start failed after backend rules / DNS were applied.
|
||
# Reuses the same cleanup primitives as stop_service so behavior stays consistent.
|
||
_rollback_failed_start() {
|
||
log_msg "Rolling back failed start" "启动失败,正在回退防火墙与 DNS 配置"
|
||
backend_remove_rules >/dev/null 2>&1
|
||
cleanup_stale_core_nft >/dev/null 2>&1
|
||
revert_dns >/dev/null 2>&1
|
||
stop_singbox_service >/dev/null 2>&1
|
||
_kill_core >/dev/null 2>&1
|
||
rm -rf /www/luci-static/yacd 2>/dev/null
|
||
del_cron >/dev/null 2>&1
|
||
}
|
||
|
||
cleanup_stale_core_nft() {
|
||
for _nft_table in "inet mihomo" "inet sing-box"; do
|
||
nft delete table $_nft_table 2>/dev/null || true
|
||
done
|
||
}
|
||
|
||
_core_candidates() {
|
||
echo "/usr/bin/mihomo-stable /usr/bin/mihomo /usr/bin/clash-meta /etc/clashoo/clash"
|
||
}
|
||
|
||
_migrate_legacy_mihomo_stable() {
|
||
local legacy=/usr/bin/clash-meta version
|
||
[ -x /usr/bin/mihomo-stable ] && return 0
|
||
[ -x "$legacy" ] && [ ! -L "$legacy" ] || return 0
|
||
version=$("$legacy" -v 2>/dev/null | head -n 1)
|
||
[ -n "$version" ] || return 0
|
||
echo "$version" | grep -Eiq '(alpha|beta|rc|pre|smart)' && return 0
|
||
cp -f "$legacy" /usr/bin/mihomo-stable 2>/dev/null && chmod 0755 /usr/bin/mihomo-stable
|
||
}
|
||
|
||
_fallback_missing_mihomo_core() {
|
||
local selected fallback
|
||
is_singbox_selected && return 0
|
||
selected="$(uci -q get clashoo.config.core 2>/dev/null)"
|
||
case "$selected" in
|
||
2)
|
||
[ -x /usr/bin/mihomo-stable ] && return 0
|
||
[ -x /usr/bin/mihomo ] || return 0
|
||
fallback=3
|
||
;;
|
||
3)
|
||
[ -x /usr/bin/mihomo ] && return 0
|
||
[ -x /usr/bin/mihomo-stable ] || return 0
|
||
fallback=2
|
||
;;
|
||
*) return 0 ;;
|
||
esac
|
||
uci -q set clashoo.config.core="$fallback"
|
||
uci -q set clashoo.config.dcore="$fallback"
|
||
uci -q commit clashoo >/dev/null 2>&1
|
||
if [ "$fallback" = "3" ]; then
|
||
log_msg "Stable core missing, falling back to Alpha" "稳定版内核缺失,自动回退到 Alpha"
|
||
else
|
||
log_msg "Alpha core missing, falling back to stable" "Alpha 内核缺失,自动回退到稳定版"
|
||
fi
|
||
}
|
||
|
||
_detect_selected_core() {
|
||
local selected
|
||
selected=$(uci get clashoo.config.core 2>/dev/null)
|
||
case "$selected" in
|
||
1)
|
||
for _bin in /usr/bin/smart /etc/clashoo/clash /usr/bin/clash; do
|
||
[ -x "$_bin" ] && echo "$_bin" && return
|
||
done
|
||
return 1
|
||
;;
|
||
2|mihomo)
|
||
[ -x /usr/bin/mihomo-stable ] && echo /usr/bin/mihomo-stable && return
|
||
return 1
|
||
;;
|
||
3|mihomo-alpha)
|
||
[ -x /usr/bin/mihomo ] && echo /usr/bin/mihomo && return
|
||
return 1
|
||
;;
|
||
esac
|
||
_detect_core
|
||
}
|
||
|
||
core_supports_advanced_features() {
|
||
case "$CLASH" in
|
||
*/mihomo|*/mihomo-stable|*/clash-meta|*/smart)
|
||
return 0
|
||
;;
|
||
*)
|
||
return 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# only trust procd instance PIDs (not binary name); openclash/nikki/passwall share
|
||
# the same binary names. Use ubus service list as source of truth.
|
||
_clashoo_instance_pid() {
|
||
ubus call service list "{\"name\":\"clashoo\",\"verbose\":true}" 2>/dev/null \
|
||
| jsonfilter -e '@.clashoo.instances.*.pid' 2>/dev/null | head -n1
|
||
}
|
||
|
||
_core_running() {
|
||
local _pid
|
||
_pid="$(_clashoo_instance_pid)"
|
||
[ -n "$_pid" ] && [ -d "/proc/$_pid" ]
|
||
}
|
||
|
||
# kill only clashoo procd instances, never by binary name — would hit co-installed plugins.
|
||
_kill_core() {
|
||
local _pid _wait
|
||
_pid="$(_clashoo_instance_pid)"
|
||
[ -n "$_pid" ] || return 0
|
||
kill "$_pid" >/dev/null 2>&1
|
||
_wait=0
|
||
while [ "$_wait" -lt 3 ] && [ -d "/proc/$_pid" ]; do
|
||
sleep 1
|
||
_wait=$((_wait + 1))
|
||
done
|
||
[ -d "/proc/$_pid" ] && kill -9 "$_pid" >/dev/null 2>&1
|
||
}
|
||
|
||
_selected_core_running() {
|
||
if is_singbox_selected; then
|
||
_singbox_running
|
||
else
|
||
_core_running
|
||
fi
|
||
}
|
||
|
||
selected_core_family() {
|
||
local core_type
|
||
core_type="$(uci -q get clashoo.config.core_type 2>/dev/null)"
|
||
[ "$core_type" = "singbox" ] && echo "singbox" || echo "mihomo"
|
||
}
|
||
|
||
is_singbox_selected() {
|
||
[ "$(selected_core_family)" = "singbox" ]
|
||
}
|
||
|
||
sync_legacy_core_field() {
|
||
local dcore legacy
|
||
dcore="$(uci -q get clashoo.config.dcore 2>/dev/null)"
|
||
if is_singbox_selected; then
|
||
[ "$dcore" = "5" ] && legacy="5" || legacy="4"
|
||
else
|
||
case "$dcore" in
|
||
1) legacy="1" ;;
|
||
3) legacy="3" ;;
|
||
*) legacy="2" ;;
|
||
esac
|
||
fi
|
||
uci -q set clashoo.config.core="$legacy"
|
||
uci -q commit clashoo >/dev/null 2>&1
|
||
}
|
||
|
||
prepare_singbox_runtime() {
|
||
local active src redir_port tproxy_port mixed_port dns_port
|
||
active="$(uci -q get clashoo.config.singbox_active 2>/dev/null)"
|
||
src="/usr/share/clashoo/config/singbox/${active}"
|
||
|
||
mkdir -p /etc/sing-box /usr/share/sing-box "$(dirname "$CORE_LOG_PATH")" >/dev/null 2>&1
|
||
rm -f /usr/share/sing-box/cache.db >/dev/null 2>&1
|
||
if [ -n "$active" ] && [ -r "$src" ]; then
|
||
cp -f "$src" /etc/sing-box/config.json >/dev/null 2>&1 || return 1
|
||
elif [ ! -r /etc/sing-box/config.json ]; then
|
||
_log_runtime_error "sing-box config.json is missing; please generate or select a sing-box profile first"
|
||
return 1
|
||
fi
|
||
|
||
# Core-only: skip clashoo normalize/takeover. Still version-migrate the
|
||
# imported config so an older-format config (e.g. from momo/homeproxy or a
|
||
# subscription) is accepted by the current sing-box core. The migrate is
|
||
# idempotent — a config already in the current format is left unchanged.
|
||
if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ]; then
|
||
[ -x /usr/bin/ucode ] && ucode /usr/share/clashoo/lib/migrate_singbox.uc /etc/sing-box/config.json >/dev/null 2>&1
|
||
uci -q set sing-box.main.enabled='1'
|
||
uci -q set sing-box.main.user='root'
|
||
uci -q set sing-box.main.conffile='/etc/sing-box/config.json'
|
||
uci -q set sing-box.main.workdir='/usr/share/sing-box'
|
||
uci -q commit sing-box >/dev/null 2>&1
|
||
return 0
|
||
fi
|
||
|
||
if command -v ucode >/dev/null 2>&1; then
|
||
local has_tun_device dash_port dash_pass
|
||
has_tun_device=1
|
||
runtime_has_tun_device || has_tun_device=0
|
||
redir_port="$(uci -q get clashoo.config.redir_port 2>/dev/null)"
|
||
tproxy_port="$(uci -q get clashoo.config.tproxy_port 2>/dev/null)"
|
||
mixed_port="$(uci -q get clashoo.config.mixed_port 2>/dev/null)"
|
||
dns_port="$(uci -q get clashoo.config.listen_port 2>/dev/null)"
|
||
dash_port="$(uci -q get clashoo.config.dash_port 2>/dev/null)"
|
||
dash_pass="$(uci -q get clashoo.config.dash_pass 2>/dev/null)"
|
||
[ -z "$redir_port" ] && redir_port=7891
|
||
[ -z "$tproxy_port" ] && tproxy_port=7982
|
||
[ -z "$mixed_port" ] && mixed_port=7890
|
||
[ -z "$dns_port" ] && dns_port=1053
|
||
[ -z "$dash_port" ] && dash_port=9090
|
||
# routing_mark 6666 must match fw4.sh CORE_ROUTING_MARK (0x1a0a).
|
||
# Never reuse PROXY_FWMARK (0x162) — it would pull egress to lo -> unreachable.
|
||
ucode /usr/share/clashoo/lib/normalize_singbox_config.uc \
|
||
/etc/sing-box/config.json "$redir_port" "$tproxy_port" "$mixed_port" "$has_tun_device" "6666" "$dns_port" "$dash_port" "$dash_pass" >/dev/null 2>&1 || {
|
||
_log_runtime_error "sing-box runtime config normalization failed"
|
||
return 1
|
||
}
|
||
fi
|
||
|
||
uci -q set sing-box.main.enabled='1'
|
||
uci -q set sing-box.main.user='root'
|
||
uci -q set sing-box.main.conffile='/etc/sing-box/config.json'
|
||
uci -q set sing-box.main.workdir='/usr/share/sing-box'
|
||
uci -q commit sing-box >/dev/null 2>&1
|
||
return 0
|
||
}
|
||
|
||
_sync_singbox_binary() {
|
||
# Sync /usr/bin/sing-box to the dcore-selected channel (stable/alpha), coexisting via separate paths.
|
||
local dcore src
|
||
dcore="$(uci -q get clashoo.config.dcore 2>/dev/null)"
|
||
case "$dcore" in
|
||
4) src="/usr/bin/sing-box-stable" ;;
|
||
5) src="/usr/bin/sing-box-alpha" ;;
|
||
*) return 0 ;;
|
||
esac
|
||
[ -x "$src" ] || return 0
|
||
[ "$src" -ef /usr/bin/sing-box ] && return 0
|
||
ln -sf "$src" /usr/bin/sing-box
|
||
}
|
||
|
||
start_singbox_service() {
|
||
_sync_singbox_binary
|
||
if [ ! -x /usr/bin/sing-box ]; then
|
||
_log_runtime_error "sing-box core not found, please install the local sing-box binary first"
|
||
return 1
|
||
fi
|
||
prepare_singbox_runtime || return 1
|
||
# preflight dry-run with watchdog: bail on bad config without touching
|
||
# firewall/dnsmasq, and never let a hung check freeze startup/UI.
|
||
_run_with_timeout 15 /usr/bin/sing-box -c /etc/sing-box/config.json check >> "$CORE_LOG_PATH" 2>&1
|
||
_pf_rc=$?
|
||
if [ "$_pf_rc" -ne 0 ]; then
|
||
_pf_reason="$(_preflight_reason)"
|
||
if [ "$_pf_rc" -eq 124 ]; then
|
||
log_msg "sing-box config preflight timed out, retry shortly. ${_pf_reason}" "sing-box 配置预检超时,稍后自动重试。${_pf_reason}"
|
||
runtime_state_record_health "fail" "preflight:singbox_timeout"
|
||
else
|
||
log_msg "sing-box config dry-run failed; firewall/DNS untouched. ${_pf_reason}" "sing-box 配置预检失败,未应用防火墙与 DNS 改动。${_pf_reason}"
|
||
runtime_state_record_health "fail" "preflight:singbox_dry_run_failed"
|
||
fi
|
||
return 1
|
||
fi
|
||
/etc/init.d/sing-box start >/dev/null 2>&1
|
||
}
|
||
|
||
stop_singbox_service() {
|
||
uci -q set sing-box.main.enabled='0' && uci -q commit sing-box >/dev/null 2>&1 || true
|
||
|
||
# Stop via /etc/init.d/sing-box stop (deregisters procd). Never pidof sing-box — co-installed plugins may share the binary.
|
||
[ -x /etc/init.d/sing-box ] && /etc/init.d/sing-box stop >/dev/null 2>&1 || true
|
||
|
||
local _wait=0 _pid
|
||
while [ "$_wait" -lt 3 ]; do
|
||
_pid="$(_singbox_instance_pid)"
|
||
[ -z "$_pid" ] && return 0
|
||
[ -d "/proc/$_pid" ] || return 0
|
||
sleep 1
|
||
_wait=$((_wait + 1))
|
||
done
|
||
}
|
||
|
||
_log_runtime_error() {
|
||
local message="$1"
|
||
echo "$message" > "$REAL_LOG"
|
||
echo " $(date "+%Y-%m-%d %H:%M:%S") - $message" >> /usr/share/clashoo/clashoo.txt
|
||
}
|
||
|
||
# Run a command with a self-watchdog timeout. busybox has no `timeout` binary,
|
||
# so poll the backgrounded pid and kill it on overrun. Returns the command's
|
||
# exit status, or 124 when the watchdog fired (timed out).
|
||
_run_with_timeout() {
|
||
local _to="$1"; shift
|
||
local _pid _i=0
|
||
"$@" &
|
||
_pid=$!
|
||
while [ "$_i" -lt "$_to" ]; do
|
||
kill -0 "$_pid" 2>/dev/null || { wait "$_pid"; return $?; }
|
||
sleep 1
|
||
_i=$((_i + 1))
|
||
done
|
||
kill -TERM "$_pid" 2>/dev/null
|
||
sleep 1
|
||
kill -KILL "$_pid" 2>/dev/null
|
||
wait "$_pid" 2>/dev/null
|
||
return 124
|
||
}
|
||
|
||
# Pull a concise failure reason out of the core log so users see the real error
|
||
# (e.g. "can't download MMDB") instead of only "preflight failed".
|
||
_preflight_reason() {
|
||
local line
|
||
line="$(grep -E 'level=(error|fatal)' "$CORE_LOG_PATH" 2>/dev/null | tail -1)"
|
||
[ -n "$line" ] || line="$(tail -1 "$CORE_LOG_PATH" 2>/dev/null)"
|
||
case "$line" in
|
||
*'msg="'*) line="$(printf '%s' "$line" | sed -n 's/.*msg="\([^"]*\)".*/\1/p')" ;;
|
||
esac
|
||
printf '%s' "$line"
|
||
}
|
||
|
||
log_msg() {
|
||
local en_msg="$1" zh_msg="$2"
|
||
local out_msg now
|
||
now="$(date "+%Y-%m-%d %H:%M:%S")"
|
||
if [ "${lang}" = "en" ]; then
|
||
out_msg="$en_msg"
|
||
else
|
||
out_msg="$zh_msg"
|
||
fi
|
||
echo "$out_msg" > "$REAL_LOG"
|
||
[ -n "$out_msg" ] && echo " ${now} - ${out_msg}" >> /usr/share/clashoo/clashoo.txt
|
||
}
|
||
|
||
runtime_state_reset() {
|
||
rm -f "$RUNTIME_STATE_FILE" >/dev/null 2>&1 || true
|
||
}
|
||
|
||
runtime_state_set() {
|
||
local key="$1" value="${2:-}" safe_value
|
||
[ -n "$key" ] || return 0
|
||
mkdir -p "$(dirname "$RUNTIME_STATE_FILE")" >/dev/null 2>&1
|
||
touch "$RUNTIME_STATE_FILE"
|
||
value="$(printf '%s' "$value" | tr '\r\n' ' ')"
|
||
safe_value="$(printf '%s' "$value" | sed 's/[\/&|\\]/\\&/g')"
|
||
if grep -q "^${key}=" "$RUNTIME_STATE_FILE" 2>/dev/null; then
|
||
sed -i "s|^${key}=.*|${key}=${safe_value}|g" "$RUNTIME_STATE_FILE"
|
||
else
|
||
echo "${key}=${value}" >> "$RUNTIME_STATE_FILE"
|
||
fi
|
||
}
|
||
|
||
runtime_has_tun_device() {
|
||
ip tuntap add mode tun name cotuntest >/dev/null 2>&1 || return 1
|
||
ip link del cotuntest >/dev/null 2>&1 || true
|
||
return 0
|
||
}
|
||
|
||
resolve_effective_tp_modes() {
|
||
local configured_tcp configured_udp
|
||
configured_tcp="$(uci -q get clashoo.config.tcp_mode 2>/dev/null)"
|
||
configured_udp="$(uci -q get clashoo.config.udp_mode 2>/dev/null)"
|
||
[ -z "$configured_tcp" ] && configured_tcp="redirect"
|
||
[ -z "$configured_udp" ] && configured_udp="$configured_tcp"
|
||
|
||
EFFECTIVE_TCP_MODE="$configured_tcp"
|
||
EFFECTIVE_UDP_MODE="$configured_udp"
|
||
RUNTIME_DEGRADED="0"
|
||
RUNTIME_DEGRADE_REASON=""
|
||
HAS_TUN_DEVICE="1"
|
||
|
||
if ! runtime_has_tun_device; then
|
||
HAS_TUN_DEVICE="0"
|
||
if [ "$EFFECTIVE_TCP_MODE" = "tun" ]; then
|
||
EFFECTIVE_TCP_MODE="redirect"
|
||
RUNTIME_DEGRADED="1"
|
||
fi
|
||
if [ "$EFFECTIVE_UDP_MODE" = "tun" ]; then
|
||
EFFECTIVE_UDP_MODE="tproxy"
|
||
RUNTIME_DEGRADED="1"
|
||
fi
|
||
fi
|
||
|
||
[ "$RUNTIME_DEGRADED" = "1" ] && RUNTIME_DEGRADE_REASON="tun_unavailable_auto_fallback"
|
||
}
|
||
|
||
runtime_state_record_preflight() {
|
||
local configured_tcp configured_udp core_family enhanced_mode
|
||
core_family="$1"
|
||
configured_tcp="$(uci -q get clashoo.config.tcp_mode 2>/dev/null)"
|
||
configured_udp="$(uci -q get clashoo.config.udp_mode 2>/dev/null)"
|
||
enhanced_mode="$(uci -q get clashoo.config.enhanced_mode 2>/dev/null)"
|
||
[ -z "$configured_tcp" ] && configured_tcp="redirect"
|
||
[ -z "$configured_udp" ] && configured_udp="$configured_tcp"
|
||
[ -z "$enhanced_mode" ] && enhanced_mode="fake-ip"
|
||
|
||
runtime_state_set "configured_core" "$core_family"
|
||
runtime_state_set "configured_tcp_mode" "$configured_tcp"
|
||
runtime_state_set "configured_udp_mode" "$configured_udp"
|
||
runtime_state_set "effective_tcp_mode" "$EFFECTIVE_TCP_MODE"
|
||
runtime_state_set "effective_udp_mode" "$EFFECTIVE_UDP_MODE"
|
||
runtime_state_set "has_tun_device" "$HAS_TUN_DEVICE"
|
||
runtime_state_set "degraded" "$RUNTIME_DEGRADED"
|
||
runtime_state_set "degrade_reason" "$RUNTIME_DEGRADE_REASON"
|
||
runtime_state_set "enhanced_mode" "$enhanced_mode"
|
||
runtime_state_set "health_status" "pending"
|
||
runtime_state_set "health_detail" "init"
|
||
runtime_state_set "updated_at" "$(date +%s)"
|
||
}
|
||
|
||
runtime_state_record_health() {
|
||
local status detail
|
||
status="$1"
|
||
detail="${2:-}"
|
||
[ -z "$status" ] && status="unknown"
|
||
runtime_state_set "health_status" "$status"
|
||
runtime_state_set "health_detail" "$detail"
|
||
runtime_state_set "updated_at" "$(date +%s)"
|
||
}
|
||
|
||
runtime_state_accepts_health_update() {
|
||
_selected_core_running || return 1
|
||
case "$(grep '^health_detail=' "$RUNTIME_STATE_FILE" 2>/dev/null | cut -d= -f2-)" in
|
||
service_stopped|service_disabled|boot_disabled)
|
||
return 1
|
||
;;
|
||
esac
|
||
return 0
|
||
}
|
||
|
||
curl_has_final_http_response() {
|
||
awk '
|
||
/^HTTP\// && $0 !~ /Connection established/ { ok=1 }
|
||
END { exit ok ? 0 : 1 }
|
||
'
|
||
}
|
||
|
||
runtime_run_health_check() {
|
||
local dns_state explicit_state transparent_state detail require_fakeip mixed_port _try
|
||
local dns_out dns_addr _url
|
||
|
||
runtime_state_accepts_health_update || return 1
|
||
|
||
dns_state="na"
|
||
explicit_state="na"
|
||
transparent_state="na"
|
||
require_fakeip="0"
|
||
[ "$(uci -q get clashoo.config.enable_dns 2>/dev/null)" != "0" ] && [ "$(uci -q get clashoo.config.enhanced_mode 2>/dev/null)" = "fake-ip" ] && require_fakeip="1"
|
||
|
||
if command_exists nslookup; then
|
||
dns_state="fail"
|
||
for _try in 1 2 3; do
|
||
if command_exists timeout; then
|
||
dns_out="$(timeout 3 nslookup www.google.com 127.0.0.1 2>/dev/null)"
|
||
else
|
||
dns_out="$(nslookup www.google.com 127.0.0.1 2>/dev/null)"
|
||
fi
|
||
dns_addr="$(echo "$dns_out" | awk '/^Address: /{print $2} /^Address [0-9]+: /{print $3}' | grep -E '^[0-9a-fA-F:.]+$' | tail -n1)"
|
||
if [ -n "$dns_addr" ]; then
|
||
if echo "$dns_addr" | grep -Eq '^198\.18\.|^fc00:'; then
|
||
dns_state="ok"
|
||
else
|
||
dns_state="compat"
|
||
fi
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
fi
|
||
|
||
mixed_port="$(uci -q get clashoo.config.mixed_port 2>/dev/null)"
|
||
[ -z "$mixed_port" ] && mixed_port="7890"
|
||
if command_exists curl; then
|
||
explicit_state="fail"
|
||
for _url in https://www.google.com/generate_204 https://www.gstatic.com/generate_204; do
|
||
for _try in 1 2; do
|
||
if curl -4 --proxy "http://127.0.0.1:${mixed_port}" -I --max-time 6 -sS "$_url" 2>/dev/null | curl_has_final_http_response; then
|
||
explicit_state="ok"
|
||
break 2
|
||
fi
|
||
sleep 1
|
||
done
|
||
done
|
||
transparent_state="fail"
|
||
for _url in https://www.google.com https://www.gstatic.com/generate_204; do
|
||
for _try in 1 2; do
|
||
if curl -4 -I --max-time 6 -sS "$_url" 2>/dev/null | curl_has_final_http_response; then
|
||
transparent_state="ok"
|
||
break 2
|
||
fi
|
||
sleep 1
|
||
done
|
||
done
|
||
fi
|
||
|
||
detail="dns:${dns_state};explicit:${explicit_state};transparent:${transparent_state}"
|
||
# Pass on explicit (local mixed-port) reachability alone. The transparent
|
||
# probe tests the router's OWN output-redirect path, which is unreliable on
|
||
# some stacks (PPPoE/GL.iNet, issue #25) yet does not reflect LAN-client
|
||
# proxying — keep it informational in `detail`, not a pass gate.
|
||
if [ "$explicit_state" = "ok" ]; then
|
||
if [ "$require_fakeip" = "0" ] || [ "$dns_state" = "ok" ] || [ "$dns_state" = "compat" ] || [ "$dns_state" = "na" ]; then
|
||
runtime_state_accepts_health_update || return 1
|
||
runtime_state_record_health "pass" "$detail"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
runtime_state_accepts_health_update || return 1
|
||
runtime_state_record_health "fail" "$detail"
|
||
echo " $(date "+%Y-%m-%d %H:%M:%S") - runtime health check failed: ${detail}" >> /usr/share/clashoo/clashoo.txt
|
||
return 1
|
||
}
|
||
|
||
# check now; on fail detach a bounded re-check so cold-start false-fails self-correct
|
||
runtime_run_health_check_settle() {
|
||
runtime_run_health_check && return 0
|
||
local lock="/tmp/clashoo_health_recheck.lock"
|
||
( set -C; echo "$$" >"$lock" ) 2>/dev/null || return 1
|
||
(
|
||
exec 1000>&- 2>/dev/null
|
||
trap 'rm -f "$lock"' EXIT
|
||
_i=0
|
||
while [ "$_i" -lt 8 ]; do
|
||
sleep 15
|
||
_selected_core_running || break
|
||
runtime_run_health_check && break
|
||
_i=$((_i + 1))
|
||
done
|
||
) </dev/null >/dev/null 2>&1 &
|
||
return 1
|
||
}
|
||
|
||
_require_runtime_network_stack() {
|
||
local missing=""
|
||
|
||
for _cmd in fw4 nft ip; do
|
||
command_exists "$_cmd" || missing="$missing $_cmd"
|
||
done
|
||
|
||
if [ -n "$missing" ]; then
|
||
_log_runtime_error "Missing required fw4 runtime tools:$missing"
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
_service_enabled() {
|
||
local svc="$1"
|
||
[ -x "/etc/init.d/${svc}" ] || return 1
|
||
"/etc/init.d/${svc}" enabled >/dev/null 2>&1
|
||
}
|
||
|
||
# auto-detect core binary by priority: mihomo > clash-meta > built-in clash
|
||
_detect_core() {
|
||
for _bin in $(_core_candidates); do
|
||
[ -x "$_bin" ] && echo "$_bin" && return
|
||
done
|
||
echo "/etc/clashoo/clash"
|
||
}
|
||
CLASH="$(_detect_selected_core)"
|
||
CLASH_CONFIG="/etc/clashoo"
|
||
CRON_FILE="/etc/crontabs/root"
|
||
CONFIG_YAML="/etc/clashoo/config.yaml"
|
||
|
||
CUSLIST="/tmp/dnsmasq.d/custom_list.conf"
|
||
CUSLITT="/tmp/dnsmasq.clashoo"
|
||
CUSLISTV="/var/dnsmasq.d/custom_list.conf"
|
||
CUSLITTV="/var/dnsmasq.clashoo"
|
||
REAL_LOG="/usr/share/clashoo/clashoo_real.txt"
|
||
RUNTIME_STATE_FILE="/tmp/clashoo/runtime_state"
|
||
DNS_STATE_DIR="/tmp/clashoo"
|
||
DNS_STATE_SERVER_FILE="${DNS_STATE_DIR}/dnsmasq_server.before"
|
||
DNS_STATE_NORESOLV_FILE="${DNS_STATE_DIR}/dnsmasq_noresolv.before"
|
||
DNS_STATE_CACHE_FILE="${DNS_STATE_DIR}/dnsmasq_cachesize.before"
|
||
DNS_STATE_RESOLV_FILE="${DNS_STATE_DIR}/dnsmasq_resolvfile.before"
|
||
|
||
cleanup_legacy_startup_backups() {
|
||
#===========================================================================================================================
|
||
# Historical backup artifacts can clutter LuCI "System -> Startup".
|
||
# Keep runtime clean by removing known obsolete backup filenames.
|
||
rm -f /etc/init.d/._clash \
|
||
/etc/init.d/._* \
|
||
/etc/init.d/*bak* >/dev/null 2>&1 || true
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
migrate_legacy_clash_dir() {
|
||
local legacy_dir target_dir legacy_path target_path legacy_name
|
||
legacy_dir="/etc/clash"
|
||
target_dir="/etc/clashoo"
|
||
|
||
[ -d "$legacy_dir" ] || return 0
|
||
mkdir -p "$target_dir" >/dev/null 2>&1
|
||
|
||
for legacy_name in config.yaml Country.mmdb geoip.metadb geosite.dat geoip.dat GeoLite2-ASN.mmdb; do
|
||
legacy_path="${legacy_dir}/${legacy_name}"
|
||
target_path="${target_dir}/${legacy_name}"
|
||
[ -e "$legacy_path" ] || continue
|
||
[ -e "$target_path" ] || mv -f "$legacy_path" "$target_path" >/dev/null 2>&1
|
||
done
|
||
|
||
for legacy_name in rules ruleset rule-set profiles; do
|
||
legacy_path="${legacy_dir}/${legacy_name}"
|
||
target_path="${target_dir}/${legacy_name}"
|
||
[ -e "$legacy_path" ] || continue
|
||
[ -e "$target_path" ] || mv -f "$legacy_path" "$target_path" >/dev/null 2>&1
|
||
done
|
||
|
||
rmdir "$legacy_dir" >/dev/null 2>&1 || true
|
||
}
|
||
|
||
backup_dnsmasq_state() {
|
||
#===========================================================================================================================
|
||
[ -f "$DNS_STATE_SERVER_FILE" ] && return 0
|
||
mkdir -p "$DNS_STATE_DIR" >/dev/null 2>&1
|
||
uci -q get dhcp.@dnsmasq[0].server >"$DNS_STATE_SERVER_FILE" 2>/dev/null || : >"$DNS_STATE_SERVER_FILE"
|
||
uci -q get dhcp.@dnsmasq[0].noresolv >"$DNS_STATE_NORESOLV_FILE" 2>/dev/null || : >"$DNS_STATE_NORESOLV_FILE"
|
||
uci -q get dhcp.@dnsmasq[0].cachesize >"$DNS_STATE_CACHE_FILE" 2>/dev/null || : >"$DNS_STATE_CACHE_FILE"
|
||
uci -q get dhcp.@dnsmasq[0].resolvfile >"$DNS_STATE_RESOLV_FILE" 2>/dev/null || : >"$DNS_STATE_RESOLV_FILE"
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
add_clash_dns_server() {
|
||
#===========================================================================================================================
|
||
local dns_port="$1"
|
||
printf '%s' "$dns_port" | grep -Eq '^[0-9]+$' || return 1
|
||
uci del_list dhcp.@dnsmasq[0].server="127.0.0.1#${dns_port}" >/dev/null 2>&1 || true
|
||
uci add_list dhcp.@dnsmasq[0].server="127.0.0.1#${dns_port}" >/dev/null 2>&1
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
restore_dnsmasq_state() {
|
||
#===========================================================================================================================
|
||
local token saved
|
||
[ -d "$DNS_STATE_DIR" ] || return 0
|
||
|
||
if [ -f "$DNS_STATE_SERVER_FILE" ]; then
|
||
saved="$(cat "$DNS_STATE_SERVER_FILE" 2>/dev/null)"
|
||
uci -q delete dhcp.@dnsmasq[0].server >/dev/null 2>&1
|
||
for token in $saved; do
|
||
uci add_list dhcp.@dnsmasq[0].server="$token" >/dev/null 2>&1
|
||
done
|
||
fi
|
||
|
||
if [ -s "$DNS_STATE_NORESOLV_FILE" ]; then
|
||
uci set dhcp.@dnsmasq[0].noresolv="$(cat "$DNS_STATE_NORESOLV_FILE" 2>/dev/null)" >/dev/null 2>&1
|
||
else
|
||
uci -q delete dhcp.@dnsmasq[0].noresolv >/dev/null 2>&1
|
||
fi
|
||
|
||
if [ -s "$DNS_STATE_CACHE_FILE" ]; then
|
||
uci set dhcp.@dnsmasq[0].cachesize="$(cat "$DNS_STATE_CACHE_FILE" 2>/dev/null)" >/dev/null 2>&1
|
||
else
|
||
uci -q delete dhcp.@dnsmasq[0].cachesize >/dev/null 2>&1
|
||
fi
|
||
|
||
if [ -s "$DNS_STATE_RESOLV_FILE" ]; then
|
||
uci set dhcp.@dnsmasq[0].resolvfile="$(cat "$DNS_STATE_RESOLV_FILE" 2>/dev/null)" >/dev/null 2>&1
|
||
else
|
||
uci -q delete dhcp.@dnsmasq[0].resolvfile >/dev/null 2>&1
|
||
fi
|
||
|
||
rm -f "$DNS_STATE_SERVER_FILE" "$DNS_STATE_NORESOLV_FILE" "$DNS_STATE_CACHE_FILE" "$DNS_STATE_RESOLV_FILE" >/dev/null 2>&1
|
||
rmdir "$DNS_STATE_DIR" >/dev/null 2>&1 || true
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
ensure_dns_when_core_stopped() {
|
||
#===========================================================================================================================
|
||
local force="${1:-0}" servers token remote_servers
|
||
[ "$force" = "1" ] || ! _selected_core_running || return 0
|
||
servers="$(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null)"
|
||
remote_servers=""
|
||
for token in $servers; do
|
||
case "$token" in
|
||
127.0.0.1#1053|127.0.0.1#5300|127.0.0.1#7874) ;;
|
||
*) remote_servers="${remote_servers} ${token}" ;;
|
||
esac
|
||
done
|
||
|
||
if [ "$force" = "1" ] || [ -z "$remote_servers" ]; then
|
||
uci -q delete dhcp.@dnsmasq[0].server >/dev/null 2>&1
|
||
for token in $remote_servers; do
|
||
uci add_list dhcp.@dnsmasq[0].server="$token" >/dev/null 2>&1
|
||
done
|
||
fi
|
||
if [ -z "$remote_servers" ]; then
|
||
uci add_list dhcp.@dnsmasq[0].server='119.29.29.29' >/dev/null 2>&1
|
||
uci add_list dhcp.@dnsmasq[0].server='223.5.5.5' >/dev/null 2>&1
|
||
fi
|
||
if [ "$force" = "1" ] || [ -z "$remote_servers" ]; then
|
||
uci set dhcp.@dnsmasq[0].noresolv='0' >/dev/null 2>&1
|
||
fi
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
_dnsmasq_state_digest() {
|
||
local _nr _cs
|
||
_nr="$(uci -q get dhcp.@dnsmasq[0].noresolv 2>/dev/null)"
|
||
_cs="$(uci -q get dhcp.@dnsmasq[0].cachesize 2>/dev/null)"
|
||
case "$_nr" in ''|0|false) _nr=0 ;; *) _nr=1 ;; esac
|
||
[ -n "$_cs" ] || _cs=default
|
||
printf '%s|%s|%s|%s' \
|
||
"$(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null)" \
|
||
"$_nr" "$_cs" \
|
||
"$(uci -q get dhcp.@dnsmasq[0].resolvfile 2>/dev/null)"
|
||
}
|
||
|
||
_reload_dnsmasq() {
|
||
/etc/init.d/dnsmasq reload >/dev/null 2>&1 && return 0
|
||
/etc/init.d/dnsmasq restart >/dev/null 2>&1
|
||
}
|
||
|
||
revert_dns() {
|
||
#===========================================================================================================================
|
||
local _before _after
|
||
_before="$(_dnsmasq_state_digest)"
|
||
restore_dnsmasq_state
|
||
ensure_dns_when_core_stopped "${1:-0}"
|
||
rm -rf "$CUSLIST" "$CUSLITT" "$CUSLISTV" "$CUSLITTV" 2>/dev/null
|
||
_after="$(_dnsmasq_state_digest)"
|
||
if [ "$_before" = "$_after" ]; then
|
||
uci -q revert dhcp
|
||
return 0
|
||
fi
|
||
uci commit dhcp
|
||
_reload_dnsmasq >/dev/null 2>&1
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
add_cron(){
|
||
#===========================================================================================================================
|
||
auto=$(uci get clashoo.config.auto_update 2>/dev/null)
|
||
clear=$(uci get clashoo.config.auto_clear_log 2>/dev/null)
|
||
if [ "${auto:-0}" -eq 1 ] || [ "${clear:-0}" -eq 1 ]; then
|
||
[ -z "$(grep -w "/usr/share/clashoo/update/maintenance_cron.sh" "$CRON_FILE")" ] && echo "23 * * * * sh /usr/share/clashoo/update/maintenance_cron.sh >/dev/null 2>&1" >> "$CRON_FILE"
|
||
fi
|
||
|
||
sub_auto=$(uci get clashoo.config.auto_subscription_update 2>/dev/null)
|
||
if [ "${sub_auto:-0}" -eq 1 ]; then
|
||
[ -z "$(grep -w "/usr/share/clashoo/update/subscription_update_cron.sh" "$CRON_FILE")" ] && echo "7 * * * * sh /usr/share/clashoo/update/subscription_update_cron.sh >/dev/null 2>&1" >> "$CRON_FILE"
|
||
fi
|
||
|
||
lgbm_auto=$(uci get clashoo.config.smart_lgbm_auto_update 2>/dev/null)
|
||
if [ "${lgbm_auto:-0}" -eq 1 ]; then
|
||
lgbm_interval=$(uci get clashoo.config.smart_lgbm_update_interval 2>/dev/null)
|
||
echo "$lgbm_interval" | grep -Eq '^[0-9]+$' || lgbm_interval=72
|
||
[ "$lgbm_interval" -lt 1 ] && lgbm_interval=72
|
||
lgbm_days=$(( lgbm_interval / 24 ))
|
||
[ "$lgbm_days" -lt 1 ] && lgbm_days=1
|
||
[ -z "$(grep -w "update/lgbm_update.sh" "$CRON_FILE")" ] && echo "0 3 */$lgbm_days * * sh /usr/share/clashoo/update/lgbm_update.sh >/dev/null 2>&1" >> "$CRON_FILE"
|
||
fi
|
||
crontab "$CRON_FILE"
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
del_cron(){
|
||
#===========================================================================================================================
|
||
sed -i '/update\/geoip.sh/d' "$CRON_FILE"
|
||
sed -i '/rpc_async.sh update_geoip/d' "$CRON_FILE"
|
||
sed -i '/\/usr\/share\/clash\/geoip.sh/d' "$CRON_FILE"
|
||
sed -i '/update\/update_all.sh/d' "$CRON_FILE"
|
||
sed -i '/\/usr\/share\/clash\/update_all.sh/d' "$CRON_FILE"
|
||
sed -i '/update\/maintenance_cron.sh/d' "$CRON_FILE"
|
||
sed -i '/clashoo.txt/d' "$CRON_FILE"
|
||
sed -i '/update\/lgbm_update.sh/d' "$CRON_FILE"
|
||
sed -i '/\/usr\/share\/clash\/lgbm_update.sh/d' "$CRON_FILE"
|
||
sed -i '/update\/subscription_update_cron.sh/d' "$CRON_FILE"
|
||
/etc/init.d/cron restart
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
refresh_cron() {
|
||
del_cron >/dev/null 2>&1
|
||
[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "1" ] && _selected_core_running && add_cron >/dev/null 2>&1
|
||
return 0
|
||
}
|
||
|
||
|
||
select_config(){
|
||
#===========================================================================================================================
|
||
CONFIG_YAML_PATH=$(uci get clashoo.config.use_config 2>/dev/null)
|
||
log_msg "Checking Config file" "正在检查配置文件"
|
||
|
||
# Runtime dir may be missing (e.g. after a broken uninstall left the package
|
||
# installed but its /etc/clashoo dir removed). Recreate it so the cp below
|
||
# can't silently fail and leave the core without a config.
|
||
mkdir -p "$(dirname "$CONFIG_YAML")" 2>/dev/null
|
||
|
||
if [ -f "$CONFIG_YAML_PATH" ] && [ -s "$CONFIG_YAML_PATH" ];then
|
||
|
||
if ! cp "$CONFIG_YAML_PATH" "$CONFIG_YAML" 2>/dev/null || [ ! -s "$CONFIG_YAML" ];then
|
||
log_msg "Failed to stage config file" "配置写入失败:运行目录缺失或不可写"
|
||
sleep 5
|
||
echo "Clashoo" >$REAL_LOG
|
||
return 1
|
||
fi
|
||
|
||
elif [ ! -f "$CONFIG_YAML_PATH" ] && [ ! -f "$CONFIG_YAML" ];then
|
||
|
||
log_msg "No config found" "找不到配置文章"
|
||
sleep 5
|
||
echo "Clashoo" >$REAL_LOG
|
||
return 1
|
||
|
||
elif [ ! -s "$CONFIG_YAML_PATH" ] && [ ! -s "$CONFIG_YAML" ];then
|
||
|
||
log_msg "Your Config File is Empty" "你的config.yaml有问题还是为了空"
|
||
sleep 5
|
||
echo "Clashoo" >$REAL_LOG
|
||
return 1
|
||
|
||
fi
|
||
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
|
||
|
||
_dns_roles_look_broken() {
|
||
local i=0 t a en broken=1 fb_total=0 fb_domestic=0
|
||
while :; do
|
||
t="$(uci -q get clashoo.@dnsservers[$i].ser_type 2>/dev/null)"
|
||
[ -n "$t" ] || break
|
||
en="$(uci -q get clashoo.@dnsservers[$i].enabled 2>/dev/null)"
|
||
a="$(uci -q get clashoo.@dnsservers[$i].ser_address 2>/dev/null)"
|
||
i=$((i + 1))
|
||
[ "$en" = "0" ] && continue
|
||
case "$t" in
|
||
nameserver)
|
||
printf '%s' "$a" | grep -qE '1\.1\.1\.1|1\.0\.0\.1|one\.one\.one\.one|cloudflare|8\.8\.8\.8|8\.8\.4\.4|dns\.google|9\.9\.9\.9' && broken=0
|
||
;;
|
||
fallback)
|
||
fb_total=$((fb_total + 1))
|
||
printf '%s' "$a" | grep -qE '223\.5\.5\.5|223\.6\.6\.6|119\.29\.29\.29|119\.28\.28\.28|114\.114|180\.184|alidns|doh\.pub|dnspod' && fb_domestic=$((fb_domestic + 1))
|
||
;;
|
||
esac
|
||
done
|
||
[ "$fb_total" -gt 0 ] && [ "$fb_total" = "$fb_domestic" ] && broken=0
|
||
return $broken
|
||
}
|
||
|
||
_dns_drop_legacy_geolocation_policy() {
|
||
local i=0 matcher nameserver
|
||
while uci -q get "clashoo.@dns_policy[$i]" >/dev/null 2>&1; do
|
||
matcher="$(uci -q get "clashoo.@dns_policy[$i].matcher" 2>/dev/null)"
|
||
nameserver="$(uci -q get "clashoo.@dns_policy[$i].nameserver" 2>/dev/null)"
|
||
if [ "$matcher" = "geosite:geolocation-!cn" ] && [ "$nameserver" = "https://cloudflare-dns.com/dns-query" ]; then
|
||
uci -q delete "clashoo.@dns_policy[$i]"
|
||
continue
|
||
fi
|
||
i=$((i + 1))
|
||
done
|
||
}
|
||
|
||
_migrate_dns_roles() {
|
||
local ver
|
||
ver="$(uci -q get clashoo.config.dns_migrated 2>/dev/null)"
|
||
[ "$ver" = "3" ] && return 0
|
||
if [ "$ver" = "2" ]; then
|
||
_dns_drop_legacy_geolocation_policy
|
||
uci -q set clashoo.config.dns_migrated='3'
|
||
uci -q commit clashoo
|
||
return 0
|
||
fi
|
||
if [ "$ver" = "1" ] && ! _dns_roles_look_broken; then
|
||
_dns_drop_legacy_geolocation_policy
|
||
uci -q set clashoo.config.dns_migrated='3'
|
||
uci -q commit clashoo
|
||
return 0
|
||
fi
|
||
while uci -q delete clashoo.@dnsservers[0] >/dev/null 2>&1; do :; done
|
||
while uci -q delete clashoo.@dns_policy[0] >/dev/null 2>&1; do :; done
|
||
_add_dnsserver() {
|
||
local sec
|
||
sec="$(uci -q add clashoo dnsservers)" || return 1
|
||
uci -q set clashoo."$sec".enabled='1'
|
||
uci -q set clashoo."$sec".ser_type="$1"
|
||
uci -q set clashoo."$sec".ser_address="$2"
|
||
uci -q set clashoo."$sec".protocol="${3:-none}"
|
||
[ -n "$4" ] && uci -q set clashoo."$sec".ser_port="$4"
|
||
}
|
||
_add_dnsserver 'nameserver' 'https://dns.alidns.com/dns-query'
|
||
_add_dnsserver 'nameserver' 'https://doh.pub/dns-query'
|
||
_add_dnsserver 'proxy-server-nameserver' '1.1.1.1' 'tls://' '853'
|
||
_add_dnsserver 'fallback' 'https://cloudflare-dns.com/dns-query'
|
||
_add_dnsserver 'fallback' 'https://dns.google/dns-query'
|
||
_add_dnsserver 'direct-nameserver' '223.5.5.5' 'udp://'
|
||
local sec
|
||
sec="$(uci -q add clashoo dns_policy)"
|
||
uci -q set clashoo."$sec".enabled='1'
|
||
uci -q set clashoo."$sec".policy_type='nameserver-policy'
|
||
uci -q set clashoo."$sec".matcher='geosite:cn'
|
||
uci -q add_list clashoo."$sec".nameserver='https://dns.alidns.com/dns-query'
|
||
uci -q add_list clashoo."$sec".nameserver='https://doh.pub/dns-query'
|
||
uci -q set clashoo.config.dns_migrated='3'
|
||
uci -q commit clashoo
|
||
log_msg "Upstream DNS roles migrated" "上游 DNS 角色已恢复为经典防污染模型(国内主解析 + 境外 fallback)"
|
||
}
|
||
|
||
_user_dns_fields() {
|
||
local field out="" force="${1:-}"
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
for field in nameserver proxy-server-nameserver fallback direct-nameserver \
|
||
default-nameserver nameserver-policy respect-rules; do
|
||
case ",$force," in *,"$field",*) continue ;; esac
|
||
[ "$(yq -M e "(.dns // {}) | has(\"${field}\")" "$CONFIG_YAML" 2>/dev/null)" = "true" ] &&
|
||
out="${out}${out:+,}${field}"
|
||
done
|
||
[ "$(yq -M e '.dns."prefer-h3" // false' "$CONFIG_YAML" 2>/dev/null)" = "true" ] &&
|
||
out="${out}${out:+,}prefer-h3"
|
||
printf '%s' "$out"
|
||
}
|
||
|
||
_dns_list_values() {
|
||
local field="$1"
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
yq -M e -r "(.dns.\"${field}\" // [])[]" "$CONFIG_YAML" 2>/dev/null | sed '/^null$/d'
|
||
}
|
||
|
||
_dns_value_is_domestic() {
|
||
case "$1" in
|
||
*223.5.5.5*|*119.29.29.29*|*114.114.114.114*|*180.184.*|*1.2.4.8*|*doh.pub*|*alidns*|*dns.pub*|*dnspod*)
|
||
return 0 ;;
|
||
esac
|
||
return 1
|
||
}
|
||
|
||
_dns_list_all_domestic() {
|
||
local field="$1" value seen=0
|
||
for value in $(_dns_list_values "$field"); do
|
||
seen=1
|
||
_dns_value_is_domestic "$value" || return 1
|
||
done
|
||
[ "$seen" = "1" ]
|
||
}
|
||
|
||
_dns_has_cn_rule_set() {
|
||
_dns_list_values "fake-ip-filter" | grep -qx 'rule-set:cn_domain' || return 1
|
||
yq -M e -r '(.dns."nameserver-policy"."rule-set:cn_domain" // [])[]' "$CONFIG_YAML" 2>/dev/null | grep -q .
|
||
}
|
||
|
||
_clashoo_dns_force_fields() {
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
local managed legacy fields
|
||
fields="nameserver,proxy-server-nameserver,direct-nameserver,default-nameserver,nameserver-policy,respect-rules"
|
||
managed="$(yq -M e '(.dns."x-clashoo-managed-dns" // false) == true' "$CONFIG_YAML" 2>/dev/null)"
|
||
if [ "$managed" = "true" ]; then
|
||
printf '%s' "$fields"
|
||
return 0
|
||
fi
|
||
legacy=0
|
||
if _dns_has_cn_rule_set &&
|
||
_dns_list_all_domestic "nameserver" &&
|
||
_dns_list_all_domestic "proxy-server-nameserver"; then
|
||
legacy=1
|
||
fi
|
||
[ "$legacy" = "1" ] && printf '%s' "$fields"
|
||
return 0
|
||
}
|
||
|
||
_dns_nameserver_inherit_mode() {
|
||
local has_nameserver marker prefer_h3 respect_false has_proxy force_respect
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
|
||
has_nameserver="$(yq -M e '(.dns.nameserver // null) | (tag != "!!null" and length > 0)' "$CONFIG_YAML" 2>/dev/null)"
|
||
[ "$has_nameserver" = "true" ] || return 0
|
||
|
||
marker=0
|
||
if yq -M e -r '(.dns.nameserver // null) | .. | select(tag == "!!str")' "$CONFIG_YAML" 2>/dev/null |
|
||
awk '
|
||
{
|
||
hash = index($0, "#")
|
||
if (!hash) next
|
||
fragment = substr($0, hash + 1)
|
||
split(fragment, parts, "&")
|
||
if (parts[1] !~ /=/) { found = 1; exit }
|
||
}
|
||
END { exit(found ? 0 : 1) }'; then
|
||
marker=1
|
||
fi
|
||
prefer_h3="$(yq -M e '(.dns."prefer-h3" // false) == true' "$CONFIG_YAML" 2>/dev/null)"
|
||
respect_false="$(yq -M e '((.dns // {}) | has("respect-rules")) and ((.dns."respect-rules") == false or (.dns."respect-rules") == "false")' "$CONFIG_YAML" 2>/dev/null)"
|
||
has_proxy="$(yq -M e '(.dns // {}) | has("proxy-server-nameserver")' "$CONFIG_YAML" 2>/dev/null)"
|
||
force_respect=0
|
||
case ",${CLASHOO_DNS_FORCE:-}," in
|
||
*,respect-rules,*) force_respect=1 ;;
|
||
esac
|
||
|
||
# skip suppresses both the generated proxy role and automatic respect-rules.
|
||
if [ "$marker" = "1" ] ||
|
||
{ [ "$prefer_h3" = "true" ] && [ "$force_respect" != "1" ]; } ||
|
||
{ [ "$respect_false" = "true" ] && [ "$force_respect" != "1" ]; } ||
|
||
[ "$(uci -q get clashoo.config.dns_respect_rules 2>/dev/null)" = "0" ]; then
|
||
printf '%s' skip
|
||
return 0
|
||
fi
|
||
case ",${CLASHOO_DNS_FORCE:-}," in
|
||
*,proxy-server-nameserver,*) return 0 ;;
|
||
esac
|
||
[ "$has_proxy" = "true" ] && return 0
|
||
printf '%s' inherit
|
||
}
|
||
|
||
_adapt_dns_loopback_resolver() {
|
||
local filter listen host host_pattern old_port servers replacement tmp
|
||
[ "$(uci -q get clashoo.config.dns_loopback_compat 2>/dev/null)" = "1" ] || return 0
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
|
||
filter="${CLASHOO_DNS_LOOPBACK_FILTER:-/usr/share/clashoo/runtime/dns_loopback_adapter.yq}"
|
||
[ -r "$filter" ] || return 0
|
||
[ "$(yq -M e '(.dns."proxy-server-nameserver" | type)' "$CONFIG_YAML" 2>/dev/null)" = "!!seq" ] || return 0
|
||
|
||
listen="$(yq -M e -r '.dns.listen // ""' "$CONFIG_YAML" 2>/dev/null)"
|
||
old_port="${listen##*:}"
|
||
host="${listen%:*}"
|
||
printf '%s' "$old_port" | grep -Eq '^[0-9]+$' || return 0
|
||
host="$(printf '%s' "$host" | tr 'A-Z' 'a-z')"
|
||
case "$host" in
|
||
""|0.0.0.0|'[::]'|::)
|
||
host_pattern='(localhost|127(\.[0-9]{1,3}){3}|\[?::1\]?)' ;;
|
||
localhost)
|
||
host_pattern='localhost' ;;
|
||
127.*)
|
||
printf '%s' "$host" | grep -Eq '^127(\.[0-9]{1,3}){3}$' || return 0
|
||
host_pattern="$(printf '%s' "$host" | sed 's/\./\\./g')" ;;
|
||
'[::1]'|::1)
|
||
host_pattern='\[?::1\]?' ;;
|
||
*)
|
||
return 0 ;;
|
||
esac
|
||
|
||
servers="$(yq -M e -r '.dns."proxy-server-nameserver"[]' "$CONFIG_YAML" 2>/dev/null)"
|
||
printf '%s\n' "$servers" | grep -Eiq "^([A-Za-z][A-Za-z0-9+.-]*://)?(${host_pattern}):${old_port}($|[/#?])" || return 0
|
||
|
||
replacement="$(uci -q get clashoo.config.dns_loopback_compat_resolver 2>/dev/null)"
|
||
[ -n "$replacement" ] || replacement='udp://223.5.5.5:53 udp://119.29.29.29:53'
|
||
tmp="$(mktemp "${CONFIG_YAML}.dns-loopback.XXXXXX" 2>/dev/null)" || return 0
|
||
export CLASHOO_DNS_OLD_PORT="$old_port"
|
||
export CLASHOO_DNS_HOST_PATTERN="$host_pattern"
|
||
export CLASHOO_DNS_REPLACEMENT="$replacement"
|
||
if yq -M e --from-file "$filter" "$CONFIG_YAML" > "$tmp" 2>/dev/null &&
|
||
mv -f "$tmp" "$CONFIG_YAML"; then
|
||
log_msg "Adapted stale loopback proxy DNS" "已适配失效的回环节点 DNS"
|
||
else
|
||
rm -f "$tmp"
|
||
log_msg "Loopback proxy DNS adaptation failed; preserved source settings" "回环节点 DNS 适配失败,已保留原配置"
|
||
fi
|
||
unset CLASHOO_DNS_OLD_PORT CLASHOO_DNS_HOST_PATTERN CLASHOO_DNS_REPLACEMENT
|
||
return 0
|
||
}
|
||
|
||
backfill_custom_sniffer_overrides() {
|
||
[ "$CLASHOO_KEEP_SNIFFER" = "1" ] || return 0
|
||
[ "$(uci -q get clashoo.config.sniffer_streaming 2>/dev/null)" = "1" ] || return 0
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
[ -r /usr/share/clashoo/runtime/sniffer_overrides.yq ] || return 0
|
||
yq -M e -i --from-file /usr/share/clashoo/runtime/sniffer_overrides.yq "$CONFIG_YAML" 2>/dev/null || true
|
||
}
|
||
|
||
ensure_quic_fallback_guard() {
|
||
[ "$(uci -q get clashoo.config.sniffer_streaming 2>/dev/null)" = "1" ] || return 0
|
||
[ "${EFFECTIVE_UDP_MODE:-off}" != "off" ] || return 0
|
||
[ "$(uci -q get clashoo.config.block_quic 2>/dev/null)" != "1" ] || return 0
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
[ -r /usr/share/clashoo/runtime/quic_fallback_guard.yq ] || return 0
|
||
yq -M e -i --from-file /usr/share/clashoo/runtime/quic_fallback_guard.yq "$CONFIG_YAML" 2>/dev/null || true
|
||
}
|
||
|
||
yml_change() {
|
||
CLASHOO_DNS_FORCE="$(_clashoo_dns_force_fields)"
|
||
CLASHOO_DNS_PRESENT="$(_user_dns_fields "$CLASHOO_DNS_FORCE")"
|
||
CLASHOO_DNS_INHERIT_MODE="$(_dns_nameserver_inherit_mode)"
|
||
CLASHOO_DNS_INHERIT_NAMESERVER=""
|
||
if [ "$CLASHOO_DNS_INHERIT_MODE" = "inherit" ]; then
|
||
CLASHOO_DNS_INHERIT_NAMESERVER="$(yq -M e -o=json '(.dns.nameserver // [])' "$CONFIG_YAML" 2>/dev/null)"
|
||
fi
|
||
export CLASHOO_DNS_FORCE
|
||
export CLASHOO_DNS_PRESENT
|
||
export CLASHOO_DNS_INHERIT_MODE
|
||
export CLASHOO_DNS_INHERIT_NAMESERVER
|
||
CLASHOO_KEEP_FAKEIP_FILTER=""
|
||
CLASHOO_KEEP_SNIFFER=""
|
||
if command -v yq >/dev/null 2>&1 && [ -s "$CONFIG_YAML" ]; then
|
||
[ "$(yq -M e '(.dns // {}) | has("fake-ip-filter")' "$CONFIG_YAML" 2>/dev/null)" = "true" ] &&
|
||
CLASHOO_KEEP_FAKEIP_FILTER=1
|
||
[ "$(yq -M e 'has("sniffer")' "$CONFIG_YAML" 2>/dev/null)" = "true" ] &&
|
||
CLASHOO_KEEP_SNIFFER=1
|
||
fi
|
||
export CLASHOO_KEEP_FAKEIP_FILTER
|
||
export CLASHOO_KEEP_SNIFFER
|
||
ucode /usr/share/clashoo/runtime/mixin.uc 2>/dev/null | yq -M -p json -o yaml > /tmp/_clashoo_mixin.yaml 2>/dev/null &&
|
||
yq -M eval-all '. as $item ireduce ({}; . * $item)' "$CONFIG_YAML" /tmp/_clashoo_mixin.yaml > "${CONFIG_YAML}.mixin" 2>/dev/null &&
|
||
mv -f "${CONFIG_YAML}.mixin" "$CONFIG_YAML" 2>/dev/null &&
|
||
rm -f /tmp/_clashoo_mixin.yaml 2>/dev/null
|
||
backfill_custom_sniffer_overrides
|
||
ensure_quic_fallback_guard
|
||
}
|
||
|
||
_uci_fake_ip_filter_extras() {
|
||
local item list
|
||
list="$(uci -q get clashoo.config.fake_ip_filter 2>/dev/null)"
|
||
set -f
|
||
for item in $list; do
|
||
case "$item" in
|
||
'*.lan'|'localhost.ptlogin2.qq.com')
|
||
continue ;;
|
||
geosite:cn)
|
||
printf '%s\n' 'rule-set:cn_domain' ;;
|
||
*)
|
||
printf '%s\n' "$item" ;;
|
||
esac
|
||
done
|
||
set +f
|
||
}
|
||
|
||
drop_legacy_cn_ip_injection() {
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
|
||
if ! yq -M e -i '
|
||
. as $doc |
|
||
(select(
|
||
(.["rule-providers"].clashoo_cn_ip | length) == 4 and
|
||
.["rule-providers"].clashoo_cn_ip.type == "file" and
|
||
.["rule-providers"].clashoo_cn_ip.path == "./ruleset/cn-ip.txt" and
|
||
.["rule-providers"].clashoo_cn_ip.format == "text" and
|
||
.["rule-providers"].clashoo_cn_ip.behavior == "ipcidr" and
|
||
([$doc | .. | select(tag == "!!str" and contains("clashoo_cn_ip")) |
|
||
select(. != "RULE-SET,clashoo_cn_ip,DIRECT")] | length) == 0
|
||
)) |= (
|
||
del(.["rule-providers"].clashoo_cn_ip) |
|
||
(select(has("rules")) | .rules) |= [.[] | select(. != "RULE-SET,clashoo_cn_ip,DIRECT")]
|
||
)
|
||
' "$CONFIG_YAML" 2>/dev/null; then
|
||
log_msg "Failed to remove legacy CN fallback injection" "清理旧版国内直连注入失败,请检查配置"
|
||
fi
|
||
}
|
||
|
||
_effective_fake_ip_filter_mode() {
|
||
local mode
|
||
mode="$(uci -q get clashoo.config.fake_ip_filter_mode 2>/dev/null)"
|
||
if [ -z "$mode" ] && command -v yq >/dev/null 2>&1 && [ -s "$CONFIG_YAML" ]; then
|
||
mode="$(yq -M e '(.dns."fake-ip-filter-mode" // "") | tostring' "$CONFIG_YAML" 2>/dev/null)"
|
||
[ "$mode" = "null" ] && mode=""
|
||
fi
|
||
[ -n "$mode" ] || mode="blacklist"
|
||
printf '%s\n' "$mode"
|
||
}
|
||
|
||
_shipped_fake_ip_filter_defaults() {
|
||
local f="/usr/share/clashoo/clashoo.default"
|
||
[ -s "$f" ] || return 0
|
||
sed -n "s/^[[:space:]]*list[[:space:]]\{1,\}fake_ip_filter[[:space:]]\{1,\}'\{0,1\}\([^']*\)'\{0,1\}[[:space:]]*$/\1/p" "$f"
|
||
}
|
||
|
||
_fake_ip_filter_as_rule() {
|
||
local f="$1"
|
||
case "$f" in
|
||
*,fake-ip|*,real-ip) printf '%s\n' "$f"; return ;;
|
||
esac
|
||
case "$f" in
|
||
*,*) printf '%s,real-ip\n' "$f" ;;
|
||
geosite:*) printf 'GEOSITE,%s,real-ip\n' "${f#geosite:}" ;;
|
||
rule-set:*) printf 'RULE-SET,%s,real-ip\n' "${f#rule-set:}" ;;
|
||
'+.'*) printf 'DOMAIN-SUFFIX,%s,real-ip\n' "${f#+.}" ;;
|
||
*'*'*) printf 'DOMAIN-WILDCARD,%s,real-ip\n' "$f" ;;
|
||
*) printf 'DOMAIN,%s,real-ip\n' "$f" ;;
|
||
esac
|
||
}
|
||
|
||
_yq_apply_atomic() {
|
||
local expr="$1" cand
|
||
cand="${CONFIG_YAML}.cand.$$"
|
||
cp "$CONFIG_YAML" "$cand" 2>/dev/null || return 1
|
||
if ! yq -M e -i "$expr" "$cand" 2>/dev/null; then
|
||
rm -f "$cand" 2>/dev/null
|
||
return 1
|
||
fi
|
||
if ! yq -M e '.' "$cand" >/dev/null 2>&1; then
|
||
rm -f "$cand" 2>/dev/null
|
||
return 1
|
||
fi
|
||
mv -f "$cand" "$CONFIG_YAML" 2>/dev/null || { rm -f "$cand" 2>/dev/null; return 1; }
|
||
return 0
|
||
}
|
||
|
||
normalize_fake_ip_filter() {
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
|
||
local mode dir work out item skip d
|
||
mode="$(_effective_fake_ip_filter_mode)"
|
||
|
||
if [ "$mode" = "blacklist" ] ||
|
||
[ "$(yq -M e '(.dns // {}) | has("fake-ip-filter")' "$CONFIG_YAML" 2>/dev/null)" != "true" ]; then
|
||
CLASHOO_FIF_MODE="$mode" _yq_apply_atomic '.dns."fake-ip-filter-mode" = strenv(CLASHOO_FIF_MODE)' ||
|
||
log_msg "Failed to write fake-ip-filter-mode, subscription value kept" \
|
||
"写入 fake-ip 过滤模式失败,已保留订阅自带的设置"
|
||
return 0
|
||
fi
|
||
|
||
dir="$(mktemp -d /tmp/.clashoo_fif.XXXXXX 2>/dev/null)" || return 0
|
||
work="$dir/in"
|
||
out="$dir/out"
|
||
yq -M e '.dns."fake-ip-filter"[]' "$CONFIG_YAML" 2>/dev/null > "$work" || { rm -rf "$dir"; return 0; }
|
||
[ -s "$work" ] || { rm -rf "$dir"; return 0; }
|
||
|
||
while IFS= read -r item; do
|
||
[ -n "$item" ] || continue
|
||
if [ "$mode" = "whitelist" ]; then
|
||
skip=0
|
||
set -f
|
||
for d in $(_shipped_fake_ip_filter_defaults); do
|
||
[ "$item" = "$d" ] && skip=1 && break
|
||
done
|
||
set +f
|
||
[ "$skip" = "1" ] && continue
|
||
printf '%s\n' "$item" >> "$out"
|
||
else
|
||
_fake_ip_filter_as_rule "$item" >> "$out"
|
||
fi
|
||
done < "$work"
|
||
|
||
if [ -s "$out" ]; then
|
||
CLASHOO_FIF_MODE="$mode" CLASHOO_FIF="$(cat "$out")" _yq_apply_atomic \
|
||
'.dns."fake-ip-filter-mode" = strenv(CLASHOO_FIF_MODE) | .dns."fake-ip-filter" = (strenv(CLASHOO_FIF) | split("\n"))' ||
|
||
log_msg "Failed to normalize fake-ip filter, subscription settings kept" \
|
||
"规范化 fake-ip 过滤失败,已保留订阅自带的模式与列表"
|
||
else
|
||
CLASHOO_FIF_MODE="$mode" _yq_apply_atomic \
|
||
'.dns."fake-ip-filter-mode" = strenv(CLASHOO_FIF_MODE) | del(.dns."fake-ip-filter")' ||
|
||
log_msg "Failed to normalize fake-ip filter, subscription settings kept" \
|
||
"规范化 fake-ip 过滤失败,已保留订阅自带的模式与列表"
|
||
log_msg "Fake-IP allowlist is empty after dropping shipped defaults, add your own entries" \
|
||
"白名单模式下过滤列表已空(随包默认项在此模式下语义相反已跳过),请自行填写需要 Fake-IP 的域名"
|
||
fi
|
||
rm -rf "$dir" 2>/dev/null
|
||
}
|
||
|
||
_ensure_cn_domain_rule_provider() {
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
yq -M e -i '
|
||
.["rule-providers"].cn_domain = {
|
||
"type": "file",
|
||
"path": "./ruleset/cn.mrs",
|
||
"format": "mrs",
|
||
"behavior": "domain"
|
||
}
|
||
' "$CONFIG_YAML" 2>/dev/null || true
|
||
}
|
||
|
||
append_luci_fake_ip_filter_extras() {
|
||
[ -n "${CLASHOO_KEEP_FAKEIP_FILTER:-}" ] || return 0
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
|
||
local item needs_cn_file
|
||
needs_cn_file="/tmp/.clashoo_need_cn_rs.$$"
|
||
rm -f "$needs_cn_file" 2>/dev/null
|
||
_uci_fake_ip_filter_extras | while IFS= read -r item; do
|
||
[ -n "$item" ] || continue
|
||
[ "$item" = "rule-set:cn_domain" ] && : > "$needs_cn_file"
|
||
CLASHOO_FAKEIP_FILTER_ITEM="$item" yq -M e -i '
|
||
.dns."fake-ip-filter" = (((.dns."fake-ip-filter" // []) + [strenv(CLASHOO_FAKEIP_FILTER_ITEM)]) | unique)
|
||
' "$CONFIG_YAML" 2>/dev/null || true
|
||
done
|
||
[ -f "$needs_cn_file" ] && _ensure_cn_domain_rule_provider
|
||
rm -f "$needs_cn_file" 2>/dev/null
|
||
}
|
||
|
||
ensure_dns_cn_policy_fallback() {
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
[ "$(yq -M e '(.dns // {}) | has("nameserver-policy")' "$CONFIG_YAML" 2>/dev/null)" = "true" ] || return 0
|
||
[ "$(yq -M e '((.dns."nameserver-policy" // {}) | type) == "!!map"' "$CONFIG_YAML" 2>/dev/null)" = "true" ] || return 0
|
||
|
||
if [ "$(yq -M e '(.dns."nameserver-policy" // {}) | has("rule-set:cn_domain")' "$CONFIG_YAML" 2>/dev/null)" = "true" ]; then
|
||
_ensure_cn_domain_rule_provider
|
||
return 0
|
||
fi
|
||
|
||
if [ "$(yq -M e '(.dns."nameserver-policy" // {}) | has("geosite:cn")' "$CONFIG_YAML" 2>/dev/null)" = "true" ]; then
|
||
yq -M e -i '.dns."nameserver-policy"."rule-set:cn_domain" = .dns."nameserver-policy"."geosite:cn"' "$CONFIG_YAML" 2>/dev/null || true
|
||
else
|
||
yq -M e -i '.dns."nameserver-policy"."rule-set:cn_domain" = ["https://223.5.5.5/dns-query", "https://doh.pub/dns-query"]' "$CONFIG_YAML" 2>/dev/null || true
|
||
fi
|
||
_ensure_cn_domain_rule_provider
|
||
}
|
||
|
||
_remap_dns_system_nameserver() {
|
||
command -v yq >/dev/null 2>&1 || return 0
|
||
[ -s "$CONFIG_YAML" ] || return 0
|
||
yq -M e -i '(.dns | .. | select(. == "system")) |= "223.5.5.5"' "$CONFIG_YAML" 2>/dev/null || true
|
||
}
|
||
|
||
ip_rules() {
|
||
#===========================================================================================================================
|
||
sh /usr/share/clashoo/runtime/iprules.sh 2>/dev/null
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
|
||
smart_inject() {
|
||
#===========================================================================================================================
|
||
local smart_auto smart_priority smart_asn smart_lgbm smart_collect smart_collect_size
|
||
local smart_collect_rate smart_lgbm_auto smart_lgbm_interval smart_lgbm_url
|
||
local prefer_asn_val lgbm_val collect_val collect_rate PRIORITY_TMP
|
||
|
||
smart_auto=$(uci get clashoo.config.smart_auto_switch 2>/dev/null)
|
||
|
||
# Remove any fields injected by a previous smart_inject run (idempotency)
|
||
sed -i '/^ uselightgbm:/d; /^ collectdata:/d; /^ prefer-asn:/d; /^ sample-rate:/d' "$CONFIG_YAML" 2>/dev/null
|
||
sed -i '/^ policy-priority:/d' "$CONFIG_YAML" 2>/dev/null
|
||
sed -i '/^lgbm-auto-update:/d; /^lgbm-url:/d; /^lgbm-update-interval:/d' "$CONFIG_YAML" 2>/dev/null
|
||
# revert previous smart injection: type:smart -> url-test
|
||
if command -v yq >/dev/null 2>&1; then
|
||
yq e -i '(.["proxy-groups"] // []) |= map(if .type == "smart" then .type = "url-test" else . end)' "$CONFIG_YAML" 2>/dev/null
|
||
fi
|
||
|
||
[ "${smart_auto:-0}" != "1" ] && return 0
|
||
# only inject smart fields when core=1; type:smart rejected by standard mihomo.
|
||
[ "$(uci -q get clashoo.config.core 2>/dev/null)" = "1" ] || return 0
|
||
|
||
smart_priority=$(uci get clashoo.config.smart_policy_priority 2>/dev/null)
|
||
smart_asn=$(uci get clashoo.config.smart_prefer_asn 2>/dev/null)
|
||
smart_lgbm=$(uci get clashoo.config.smart_uselightgbm 2>/dev/null)
|
||
smart_collect=$(uci get clashoo.config.smart_collectdata 2>/dev/null)
|
||
smart_collect_size=$(uci get clashoo.config.smart_collect_size 2>/dev/null)
|
||
smart_collect_rate=$(uci get clashoo.config.smart_collect_rate 2>/dev/null)
|
||
smart_lgbm_auto=$(uci get clashoo.config.smart_lgbm_auto_update 2>/dev/null)
|
||
smart_lgbm_interval=$(uci get clashoo.config.smart_lgbm_update_interval 2>/dev/null)
|
||
smart_lgbm_url=$(uci get clashoo.config.smart_lgbm_url 2>/dev/null)
|
||
|
||
[ -z "$smart_collect_size" ] && smart_collect_size=100
|
||
[ -z "$smart_collect_rate" ] && smart_collect_rate=1
|
||
[ -z "$smart_lgbm_interval" ] && smart_lgbm_interval=72
|
||
[ -z "$smart_lgbm_url" ] && smart_lgbm_url="https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin"
|
||
|
||
[ "${smart_asn:-0}" = "1" ] && prefer_asn_val="true" || prefer_asn_val="false"
|
||
[ "${smart_lgbm:-0}" = "1" ] && lgbm_val="true" || lgbm_val="false"
|
||
[ "${smart_collect:-0}" = "1" ] && collect_val="true" || collect_val="false"
|
||
collect_rate="${smart_collect_rate:-1}"
|
||
|
||
# Prefer structured YAML mutation (OpenClash-like behavior) when yq exists.
|
||
if command -v yq >/dev/null 2>&1; then
|
||
export SMART_PRIORITY="${smart_priority:-}"
|
||
export SMART_PREFER_ASN="$prefer_asn_val"
|
||
export SMART_USELGBM="$lgbm_val"
|
||
export SMART_COLLECT="$collect_val"
|
||
export SMART_COLLECT_RATE="${collect_rate:-1}"
|
||
export SMART_COLLECT_SIZE="${smart_collect_size:-100}"
|
||
export SMART_LGBM_AUTO="$([ "${smart_lgbm_auto:-0}" = "1" ] && echo "true" || echo "false")"
|
||
export SMART_LGBM_URL="${smart_lgbm_url:-}"
|
||
export SMART_LGBM_INTERVAL="${smart_lgbm_interval:-72}"
|
||
|
||
if yq e '
|
||
.["proxy-groups"] = ((.["proxy-groups"] // []) | map(
|
||
if (.type == "url-test" or .type == "load-balance" or .type == "smart") then
|
||
.type = "smart" |
|
||
.["prefer-asn"] = (env(SMART_PREFER_ASN) == "true") |
|
||
.uselightgbm = (env(SMART_USELGBM) == "true") |
|
||
.collectdata = (env(SMART_COLLECT) == "true") |
|
||
.["sample-rate"] = (env(SMART_COLLECT_RATE) | tonumber) |
|
||
(if env(SMART_PRIORITY) != "" then .["policy-priority"] = env(SMART_PRIORITY) else del(.["policy-priority"]) end)
|
||
else .
|
||
end
|
||
)) |
|
||
(if env(SMART_COLLECT) == "true" then
|
||
.profile = (.profile // {}) |
|
||
.profile["smart-collector-size"] = (env(SMART_COLLECT_SIZE) | tonumber)
|
||
else
|
||
.
|
||
end) |
|
||
(if env(SMART_USELGBM) == "true" then
|
||
.["lgbm-auto-update"] = (env(SMART_LGBM_AUTO) == "true") |
|
||
.["lgbm-url"] = env(SMART_LGBM_URL) |
|
||
.["lgbm-update-interval"] = (env(SMART_LGBM_INTERVAL) | tonumber)
|
||
else
|
||
del(.["lgbm-auto-update"], .["lgbm-url"], .["lgbm-update-interval"])
|
||
end)
|
||
' "$CONFIG_YAML" > /tmp/_smart_inject.yaml 2>/dev/null; then
|
||
mv /tmp/_smart_inject.yaml "$CONFIG_YAML"
|
||
unset SMART_PRIORITY SMART_PREFER_ASN SMART_USELGBM SMART_COLLECT SMART_COLLECT_RATE
|
||
unset SMART_COLLECT_SIZE SMART_LGBM_AUTO SMART_LGBM_URL SMART_LGBM_INTERVAL
|
||
return 0
|
||
fi
|
||
|
||
unset SMART_PRIORITY SMART_PREFER_ASN SMART_USELGBM SMART_COLLECT SMART_COLLECT_RATE
|
||
unset SMART_COLLECT_SIZE SMART_LGBM_AUTO SMART_LGBM_URL SMART_LGBM_INTERVAL
|
||
fi
|
||
|
||
# Fallback path (no yq): line-based mutation with indentation-aware awk.
|
||
# Step 1: Replace any-indentation type: url-test/load-balance to type: smart
|
||
awk '
|
||
/^[ \t]*type:[ \t]*(url-test|load-balance)[ \t]*$/ {
|
||
sub(/type:[ \t]*(url-test|load-balance)[ \t]*$/, "type: smart")
|
||
}
|
||
{ print }
|
||
' "$CONFIG_YAML" > /tmp/_smart_type.yaml && mv /tmp/_smart_type.yaml "$CONFIG_YAML"
|
||
|
||
# Step 2: Build policy-priority value temp file (avoid quoting issues)
|
||
PRIORITY_TMP="/tmp/_smart_priority.txt"
|
||
printf '' > "$PRIORITY_TMP"
|
||
if [ -n "$smart_priority" ]; then
|
||
printf '%s\n' "$smart_priority" > "$PRIORITY_TMP"
|
||
fi
|
||
|
||
# Step 3: Inject smart fields after each "type: smart" with preserved indentation
|
||
awk -v pfile="$PRIORITY_TMP" \
|
||
-v prefer_asn="$prefer_asn_val" \
|
||
-v lgbm="$lgbm_val" \
|
||
-v collect="$collect_val" \
|
||
-v rate="$collect_rate" '
|
||
/^[ \t]*type:[ \t]*smart[ \t]*$/ {
|
||
match($0, /^[ \t]*/)
|
||
indent = substr($0, RSTART, RLENGTH)
|
||
print
|
||
while ((getline line < pfile) > 0) {
|
||
if (line != "")
|
||
print indent "policy-priority: \047" line "\047"
|
||
}
|
||
close(pfile)
|
||
print indent "prefer-asn: " prefer_asn
|
||
print indent "uselightgbm: " lgbm
|
||
print indent "collectdata: " collect
|
||
print indent "sample-rate: " rate
|
||
next
|
||
}
|
||
{ print }
|
||
' "$CONFIG_YAML" > /tmp/_smart_inject.yaml && mv /tmp/_smart_inject.yaml "$CONFIG_YAML"
|
||
rm -f "$PRIORITY_TMP" 2>/dev/null
|
||
|
||
# Step 4: profile.smart-collector-size (preserve profile child indentation from source YAML)
|
||
if grep -q '^profile:[ ]*$' "$CONFIG_YAML"; then
|
||
profile_indent="$(
|
||
awk '
|
||
/^profile:[ \t]*$/ {
|
||
while (getline > 0) {
|
||
if ($0 ~ /^[ \t]*$/) {
|
||
continue
|
||
}
|
||
if ($0 ~ /^[^ \t]/) {
|
||
break
|
||
}
|
||
match($0, /^[ \t]+/)
|
||
print substr($0, RSTART, RLENGTH)
|
||
exit
|
||
}
|
||
}
|
||
' "$CONFIG_YAML"
|
||
)"
|
||
[ -n "$profile_indent" ] || profile_indent=" "
|
||
|
||
if grep -q '^[ ]*smart-collector-size:[ ]*' "$CONFIG_YAML"; then
|
||
awk -v indent="$profile_indent" -v size="$smart_collect_size" '
|
||
/^[ \t]*smart-collector-size:[ \t]*/ {
|
||
print indent "smart-collector-size: " size
|
||
next
|
||
}
|
||
{ print }
|
||
' "$CONFIG_YAML" > /tmp/_smart_profile.yaml && mv /tmp/_smart_profile.yaml "$CONFIG_YAML"
|
||
else
|
||
awk -v indent="$profile_indent" -v size="$smart_collect_size" '
|
||
/^profile:[ \t]*$/ {
|
||
print
|
||
print indent "smart-collector-size: " size
|
||
next
|
||
}
|
||
{ print }
|
||
' "$CONFIG_YAML" > /tmp/_smart_profile.yaml && mv /tmp/_smart_profile.yaml "$CONFIG_YAML"
|
||
fi
|
||
else
|
||
printf '\nprofile:\n store-selected: true\n smart-collector-size: %s\n' "$smart_collect_size" >> "$CONFIG_YAML"
|
||
fi
|
||
|
||
# Step 5: Top-level lgbm fields (only when lgbm enabled)
|
||
if [ "$lgbm_val" = "true" ]; then
|
||
lgbm_auto_val="false"
|
||
[ "${smart_lgbm_auto:-0}" = "1" ] && lgbm_auto_val="true"
|
||
printf 'lgbm-auto-update: %s\nlgbm-url: "%s"\nlgbm-update-interval: %s\n' \
|
||
"$lgbm_auto_val" "$smart_lgbm_url" "$smart_lgbm_interval" >> "$CONFIG_YAML"
|
||
fi
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
|
||
_lan_acl_enabled() {
|
||
local section
|
||
for section in $(uci -q show clashoo 2>/dev/null | sed -n 's/^clashoo\.\([^.=]*\)=lan_acl$/\1/p'); do
|
||
[ "$(uci -q get "clashoo.${section}.enabled" 2>/dev/null)" != "0" ] && return 0
|
||
done
|
||
[ "$(uci -q get clashoo.config.acl_migrated 2>/dev/null)" = "1" ] && return 1
|
||
case "$(uci -q get clashoo.config.access_control 2>/dev/null)" in
|
||
1|2) return 0 ;;
|
||
esac
|
||
return 1
|
||
}
|
||
|
||
_lan_dns_split_enabled() {
|
||
_lan_acl_enabled || return 1
|
||
[ "$(uci -q get clashoo.config.enable_dns 2>/dev/null)" != "0" ] || return 1
|
||
[ "$(uci -q get clashoo.config.ipv4_dns_hijack 2>/dev/null)" = "1" ] ||
|
||
[ "$(uci -q get clashoo.config.ipv6_dns_hijack 2>/dev/null)" = "1" ]
|
||
}
|
||
|
||
yml_dns_change(){
|
||
#===========================================================================================================================
|
||
dns_port=$(sed -n 's/^[[:space:]]*listen:.*:[[:space:]]*['"'"'"]*\([0-9]\{1,\}\)['"'"'"]*[[:space:]]*$/\1/p' /etc/clashoo/config.yaml 2>/dev/null | head -n1)
|
||
listen_port=$(uci get clashoo.config.listen_port 2>/dev/null)
|
||
[ -z "$listen_port" ] && listen_port=5300
|
||
if [ -z "$dns_port" ]; then
|
||
dns_port="$listen_port"
|
||
if grep -q '^dns:' /etc/clashoo/config.yaml && ! grep -q "^ \{0,\}listen:" /etc/clashoo/config.yaml; then
|
||
sed -i "/^dns:/a\ listen: 0.0.0.0:${dns_port}" /etc/clashoo/config.yaml 2>/dev/null
|
||
fi
|
||
fi
|
||
dnsforwader=$(uci get clashoo.config.dnsforwader 2>/dev/null)
|
||
dnscache=$(uci get clashoo.config.dnscache 2>/dev/null)
|
||
if [ "$(uci -q get clashoo.config.enable_dns 2>/dev/null)" = "0" ]; then
|
||
revert_dns >/dev/null 2>&1
|
||
return 0
|
||
fi
|
||
if _lan_dns_split_enabled; then
|
||
revert_dns 1 >/dev/null 2>&1
|
||
return 0
|
||
fi
|
||
backup_dnsmasq_state
|
||
if [ "${dns_port:-0}" -eq 53 ]; then
|
||
sed -i "s@^\([[:space:]]*listen:[[:space:]]*\)0\.0\.0\.0:53@\10.0.0.0:${listen_port}@g" "$CONFIG_YAML"
|
||
dns_port="$listen_port"
|
||
fi
|
||
|
||
if [ "${dnsforwader:-0}" -ne 0 ]; then
|
||
uci -q delete dhcp.@dnsmasq[0].server
|
||
if [ "${dns_port:-0}" -eq 53 ]; then
|
||
|
||
log_msg "Setting dns forwarder" "设置dns转发器"
|
||
|
||
add_clash_dns_server 5300
|
||
uci delete dhcp.@dnsmasq[0].resolvfile
|
||
uci set dhcp.@dnsmasq[0].noresolv=1
|
||
uci commit dhcp
|
||
if [ "${dnscache:-0}" -eq 0 ];then
|
||
log_msg "Disabling dns cache" "禁用dns缓存"
|
||
uci set dhcp.@dnsmasq[0].cachesize=0
|
||
uci commit dhcp
|
||
fi
|
||
else
|
||
echo "Setting dns forwarder" >$REAL_LOG
|
||
add_clash_dns_server "$dns_port"
|
||
uci delete dhcp.@dnsmasq[0].resolvfile
|
||
uci set dhcp.@dnsmasq[0].noresolv=1
|
||
uci commit dhcp
|
||
if [ "${dnscache:-0}" -eq 0 ];then
|
||
log_msg "Disabling dns cache" "禁用dns缓存"
|
||
|
||
uci set dhcp.@dnsmasq[0].cachesize=0
|
||
uci commit dhcp
|
||
fi
|
||
fi
|
||
elif [ "${dnsforwader:-0}" -ne 1 ]; then
|
||
if [ "${dnscache:-0}" -eq 0 ];then
|
||
log_msg "Disabling dns cache" "禁用dns缓存"
|
||
|
||
uci set dhcp.@dnsmasq[0].cachesize=0
|
||
uci commit dhcp
|
||
fi
|
||
fi
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
singbox_dns_change() {
|
||
#===========================================================================================================================
|
||
local dnsforwader dnscache dns_port
|
||
if _lan_dns_split_enabled; then
|
||
revert_dns 1 >/dev/null 2>&1
|
||
return 0
|
||
fi
|
||
dnsforwader="$(uci get clashoo.config.dnsforwader 2>/dev/null)"
|
||
if [ "${dnsforwader:-0}" -eq 0 ]; then
|
||
return 0
|
||
fi
|
||
|
||
dnscache="$(uci get clashoo.config.dnscache 2>/dev/null)"
|
||
dns_port="$(uci get clashoo.config.listen_port 2>/dev/null)"
|
||
[ -z "$dns_port" ] && dns_port=1053
|
||
|
||
backup_dnsmasq_state
|
||
_sb_dns_before="$(_dnsmasq_state_digest)"
|
||
uci -q delete dhcp.@dnsmasq[0].server
|
||
add_clash_dns_server "$dns_port"
|
||
uci -q delete dhcp.@dnsmasq[0].resolvfile
|
||
uci set dhcp.@dnsmasq[0].noresolv='1'
|
||
if [ "${dnscache:-0}" -eq 0 ]; then
|
||
uci set dhcp.@dnsmasq[0].cachesize='0'
|
||
fi
|
||
if [ "$(_dnsmasq_state_digest)" != "$_sb_dns_before" ]; then
|
||
uci commit dhcp >/dev/null 2>&1
|
||
_reload_dnsmasq >/dev/null 2>&1
|
||
else
|
||
uci -q revert dhcp >/dev/null 2>&1
|
||
fi
|
||
unset _sb_dns_before
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
|
||
check(){
|
||
|
||
sed -i 's/^Proxy Group:/proxy-groups:/g' "$CONFIG_YAML"
|
||
sed -i 's/^proxy-provider:/proxy-providers:/g' "$CONFIG_YAML"
|
||
sed -i 's/^Proxy:/proxies:/g' "$CONFIG_YAML"
|
||
sed -i 's/^Rule:/rules:/g' "$CONFIG_YAML"
|
||
sed -i 's/^rule-provider:/rule-providers:/g' "$CONFIG_YAML"
|
||
|
||
}
|
||
|
||
rules(){
|
||
#===========================================================================================================================
|
||
/usr/share/clashoo/net/fw4.sh apply >/dev/null 2>&1
|
||
return $?
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
|
||
restore() {
|
||
sh /usr/share/clashoo/runtime/restore.sh >/dev/null 2>&1
|
||
}
|
||
|
||
_config_needs_mmdb() {
|
||
grep -Eq '^[[:space:]]*-[[:space:]]*["'\'']?GEOIP,|^[[:space:]]*geoip:[[:space:]]|^[[:space:]]*geoip-code:[[:space:]]' "$CONFIG_YAML"
|
||
}
|
||
|
||
_has_valid_local_mmdb() {
|
||
local db_file db_size
|
||
|
||
for db_file in /etc/clashoo/geoip.metadb /etc/clashoo/Country.mmdb; do
|
||
[ -f "$db_file" ] || continue
|
||
db_size=$(wc -c <"$db_file" 2>/dev/null)
|
||
[ -n "$db_size" ] || continue
|
||
if [ "$db_file" = "/etc/clashoo/geoip.metadb" ] && [ "$db_size" -ge 1000000 ]; then
|
||
return 0
|
||
fi
|
||
if [ "$db_file" = "/etc/clashoo/Country.mmdb" ] && [ "$db_size" -ge 2000000 ]; then
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
_config_needs_geosite() {
|
||
# Matches: routing rules (- GEOSITE,...), geosite: child blocks, and
|
||
# list-item refs like `- geosite:cn` / `- 'geosite:cn'` used in
|
||
# fake-ip-filter (the default config ships one, easy to miss).
|
||
grep -Eq "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,|^[[:space:]]*geosite:[[:space:]]*|^[[:space:]]*[\"']geosite:|^[[:space:]]*-[[:space:]]*[\"']?geosite:" "$CONFIG_YAML"
|
||
}
|
||
|
||
_has_valid_local_geosite() {
|
||
local site_file site_size
|
||
|
||
for site_file in /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat; do
|
||
[ -f "$site_file" ] || continue
|
||
site_size=$(wc -c <"$site_file" 2>/dev/null)
|
||
[ -n "$site_size" ] || continue
|
||
if [ "$site_size" -ge 2000000 ]; then
|
||
if [ "$site_file" = "/etc/clashoo/geosite.dat" ]; then
|
||
cp -f "$site_file" /etc/clashoo/GeoSite.dat >/dev/null 2>&1 || true
|
||
chmod 644 /etc/clashoo/GeoSite.dat >/dev/null 2>&1 || true
|
||
fi
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
_strip_geox_url_block() {
|
||
awk '
|
||
BEGIN { skip=0 }
|
||
/^geox-url:[[:space:]]*$/ { skip=1; next }
|
||
skip && /^[^[:space:]]/ { skip=0 }
|
||
!skip { print }
|
||
' "$CONFIG_YAML" > /tmp/_clashoo_geox.yaml && mv /tmp/_clashoo_geox.yaml "$CONFIG_YAML"
|
||
}
|
||
|
||
_strip_yaml_child_block() {
|
||
local regex="$1"
|
||
awk -v target="$regex" '
|
||
function indent_of(line) {
|
||
match(line, /^[[:space:]]*/)
|
||
return RLENGTH
|
||
}
|
||
{
|
||
if (skip) {
|
||
if (length($0) == 0) {
|
||
next
|
||
}
|
||
if (indent_of($0) > base_indent) {
|
||
next
|
||
}
|
||
skip=0
|
||
}
|
||
if ($0 ~ target) {
|
||
skip=1
|
||
base_indent=indent_of($0)
|
||
next
|
||
}
|
||
print
|
||
}
|
||
' "$CONFIG_YAML" > /tmp/_clashoo_geo_block.yaml && mv /tmp/_clashoo_geo_block.yaml "$CONFIG_YAML"
|
||
}
|
||
|
||
_strip_yaml_rule_lines() {
|
||
local regex="$1"
|
||
awk -v target="$regex" '
|
||
$0 ~ target { next }
|
||
{ print }
|
||
' "$CONFIG_YAML" > /tmp/_clashoo_geo_rules.yaml && mv /tmp/_clashoo_geo_rules.yaml "$CONFIG_YAML"
|
||
}
|
||
|
||
|
||
_start_geoip_update_async() {
|
||
if [ -f /var/run/geoip_update ]; then
|
||
return 0
|
||
fi
|
||
touch /var/run/geoip_update >/dev/null 2>&1
|
||
(exec 1000>&-; nohup /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1 </dev/null &)
|
||
}
|
||
|
||
_prepare_geo_bootstrap_mode() {
|
||
local needs_mmdb needs_geosite
|
||
|
||
needs_mmdb=0
|
||
needs_geosite=0
|
||
|
||
_config_needs_mmdb && needs_mmdb=1
|
||
_config_needs_geosite && needs_geosite=1
|
||
|
||
if [ "$needs_mmdb" -eq 1 ] && _has_valid_local_mmdb; then
|
||
needs_mmdb=0
|
||
elif [ "$needs_mmdb" -eq 1 ] && { [ -f /etc/clashoo/Country.mmdb ] || [ -f /etc/clashoo/geoip.metadb ]; }; then
|
||
rm -f /etc/clashoo/Country.mmdb >/dev/null 2>&1
|
||
rm -f /etc/clashoo/geoip.metadb >/dev/null 2>&1
|
||
log_msg "GeoIP database is invalid or incomplete, fallback to bootstrap mode" "GeoIP 数据库无效或下载不完整,已回退到引导模式"
|
||
fi
|
||
[ "$needs_geosite" -eq 1 ] && _has_valid_local_geosite && needs_geosite=0
|
||
|
||
if [ "$needs_mmdb" -eq 0 ] && [ "$needs_geosite" -eq 0 ]; then
|
||
return 0
|
||
fi
|
||
|
||
sed -i '/^geodata-mode:/d; /^geodata-loader:/d' "$CONFIG_YAML" 2>/dev/null
|
||
_strip_geox_url_block
|
||
|
||
if [ "$needs_mmdb" -eq 1 ]; then
|
||
_strip_yaml_rule_lines "^[[:space:]]*-[[:space:]]*[\"']?GEOIP,"
|
||
sed -i '/^[[:space:]]*geoip:[[:space:]]*/d' "$CONFIG_YAML" 2>/dev/null
|
||
sed -i '/^[[:space:]]*geoip-code:[[:space:]]*/d' "$CONFIG_YAML" 2>/dev/null
|
||
fi
|
||
|
||
if [ "$needs_geosite" -eq 1 ]; then
|
||
rm -f /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1
|
||
_strip_yaml_rule_lines "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,"
|
||
_strip_yaml_child_block '^[[:space:]]*geosite:[[:space:]]*$'
|
||
_strip_yaml_child_block "^[[:space:]]*'geosite:[^']*'[[:space:]]*:[[:space:]]*$"
|
||
_strip_yaml_child_block '^[[:space:]]*"geosite:[^"]*"[[:space:]]*:[[:space:]]*$'
|
||
_strip_yaml_child_block '^[[:space:]]*geosite:[^[:space:]]+:[[:space:]]*$'
|
||
_strip_yaml_rule_lines '^[[:space:]]*geosite:[[:space:]]+[^[:space:]].*'
|
||
# list-item refs (e.g. fake-ip-filter: - geosite:cn / - 'geosite:cn')
|
||
sed -i "/^[[:space:]]*-[[:space:]]*[\"']\{0,1\}geosite:/d" "$CONFIG_YAML" 2>/dev/null
|
||
fi
|
||
|
||
# Defer the geo download until the core is up: a fresh box has no working
|
||
# proxy yet, so fetching now would hit the (likely blocked) mirrors and fail.
|
||
# Record what was missing so the post-start watcher can fetch geo through the
|
||
# live proxy and only reload once geo is complete. See _finish_geo_bootstrap.
|
||
printf '%s %s\n' "$needs_mmdb" "$needs_geosite" > /var/run/clashoo_bootstrap_pending 2>/dev/null
|
||
log_msg "Geo data missing, started in bootstrap mode; will fetch geo via proxy then reload" "Geo 数据缺失,已切换为引导模式;待代理可用后自动补全并重载"
|
||
}
|
||
|
||
# After the core is up in bootstrap mode, fetch the missing geo data through the
|
||
# now-working proxy and, once it is complete, reload the full (un-stripped)
|
||
# config so geo rules take effect. Loop-safe: only reloads when the geo that was
|
||
# actually missing is now valid, so the next start won't re-enter bootstrap.
|
||
_finish_geo_bootstrap() {
|
||
local need_mmdb need_geosite still
|
||
[ -f /var/run/clashoo_bootstrap_pending ] || return 0
|
||
# core-only never strips, so a flag there would be stale — clear and skip.
|
||
if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ]; then
|
||
rm -f /var/run/clashoo_bootstrap_pending >/dev/null 2>&1
|
||
return 0
|
||
fi
|
||
read need_mmdb need_geosite < /var/run/clashoo_bootstrap_pending 2>/dev/null
|
||
|
||
log_msg "Bootstrap: fetching geo data via proxy" "引导模式:正在通过代理补全 Geo 数据"
|
||
/usr/share/clashoo/update/geoip.sh >/dev/null 2>&1
|
||
|
||
still=0
|
||
[ "$need_mmdb" = "1" ] && ! _has_valid_local_mmdb && still=1
|
||
[ "$need_geosite" = "1" ] && ! _has_valid_local_geosite && still=1
|
||
if [ "$still" = "1" ]; then
|
||
log_msg "Bootstrap: geo fetch incomplete, will retry on next update" "引导模式:Geo 补全未完成,将在下次更新时重试"
|
||
return 0
|
||
fi
|
||
|
||
rm -f /var/run/clashoo_bootstrap_pending >/dev/null 2>&1
|
||
_clashoo_enabled || return 0
|
||
log_msg "Bootstrap: geo ready, reloading full config" "引导模式:Geo 数据已就绪,正在重载完整配置"
|
||
/etc/init.d/clashoo restart >/dev/null 2>&1
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
_start_validate_core() {
|
||
core="$(uci -q get clashoo.config.core 2>/dev/null)"
|
||
if is_singbox_selected; then
|
||
if [ ! -x /usr/bin/sing-box ]; then
|
||
log_msg "sing-box core not found, please install the local sing-box binary first" "找不到本地 sing-box 内核,请先安装 sing-box 二进制"
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
return 1
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
if [ "${core:-0}" -eq 1 ] && [ ! -x "$CLASH" ]; then
|
||
log_msg "Smart core not found, please install the local Smart binary first" "找不到本地 Smart 内核,请先安装 Smart 二进制"
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
return 1
|
||
elif [ "${core:-0}" -eq 2 ] && [ ! -x "$CLASH" ]; then
|
||
log_msg "mihomo core not found, detected binary is unavailable: $CLASH" "找不到可用的 mihomo 内核: $CLASH"
|
||
sleep 1
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
return 1
|
||
elif [ "${core:-0}" -eq 3 ] && [ ! -x "$CLASH" ]; then
|
||
log_msg "mihomo core not found, detected binary is unavailable: $CLASH" "找不到可用的 mihomo 内核: $CLASH"
|
||
sleep 1
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
return 1
|
||
elif [ "${core:-0}" -ne 1 ] && [ "${core:-0}" -ne 2 ] && [ "${core:-0}" -ne 3 ] && [ ! -x "$CLASH" ]; then
|
||
log_msg "invalid core setting (${core}), detected binary is unavailable: $CLASH" "内核配置异常 (${core}),且找不到可用内核: $CLASH"
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
_start_prepare_config() {
|
||
# Core-only: run the imported config verbatim. select_config already copied
|
||
# it to /etc/clashoo/config.yaml, so skip every mixin/DNS/rule rewrite below.
|
||
[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ] && return 0
|
||
|
||
check >/dev/null 2>&1
|
||
|
||
_migrate_dns_roles >/dev/null 2>&1
|
||
|
||
enable_dns=$(uci get clashoo.config.enable_dns 2>/dev/null)
|
||
|
||
if [ -z "$(grep -E "^ {0,}listen:" /etc/clashoo/config.yaml)" ] || [ -z "$(grep -E "^ {0,}enhanced-mode:" /etc/clashoo/config.yaml)" ] || [ -z "$(grep -E "^ {0,}dns:" /etc/clashoo/config.yaml)" ];then
|
||
|
||
if [ "${enable_dns:-0}" -eq 0 ];then
|
||
uci set clashoo.config.enable_dns="1" && uci commit clashoo
|
||
log_msg "Enabling Custom Dns" "启用自定义DNS"
|
||
fi
|
||
|
||
fi
|
||
|
||
if { egrep -q '^ {0,}rule-providers:' "/etc/clashoo/config.yaml" || egrep -q '^ {0,}script:' "/etc/clashoo/config.yaml"; } && ! core_supports_advanced_features; then
|
||
if [ -x /usr/bin/mihomo ]; then
|
||
uci set clashoo.config.core="3" && uci commit clashoo
|
||
elif [ -x /usr/bin/mihomo-stable ]; then
|
||
uci set clashoo.config.core="2" && uci commit clashoo
|
||
else
|
||
log_msg "Current config requires mihomo/clash-meta core" "当前配置需要 mihomo 或 clash-meta 内核"
|
||
backend_remove_rules >/dev/null 2>&1
|
||
sleep 1
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
exit 0
|
||
fi
|
||
core=$(uci get clashoo.config.core 2>/dev/null)
|
||
CLASH="$(_detect_selected_core)"
|
||
sleep 1
|
||
fi
|
||
|
||
if [ "$(uci get clashoo.config.p_mode 2>/dev/null)" = "script" ]; then
|
||
uci set clashoo.config.p_mode="rule" && uci commit clashoo
|
||
fi
|
||
|
||
# flatten YAML merge anchors first (yq 4.53+ breaks "<<" keys on rewrite)
|
||
if command -v yq >/dev/null 2>&1 && grep -q '<<[[:space:]]*:' "$CONFIG_YAML" 2>/dev/null; then
|
||
yq e -i 'explode(.)' "$CONFIG_YAML" 2>/dev/null || true
|
||
fi
|
||
_adapt_dns_loopback_resolver >/dev/null 2>&1
|
||
# stage bundled cn.mrs into the core dir; mihomo rejects rule-set paths outside /etc/clashoo
|
||
if [ -f /usr/share/clashoo/ruleset/cn.mrs ]; then
|
||
if [ ! -f /etc/clashoo/ruleset/cn.mrs ] || [ /usr/share/clashoo/ruleset/cn.mrs -nt /etc/clashoo/ruleset/cn.mrs ]; then
|
||
mkdir -p /etc/clashoo/ruleset
|
||
cp -f /usr/share/clashoo/ruleset/cn.mrs /etc/clashoo/ruleset/cn.mrs 2>/dev/null
|
||
fi
|
||
fi
|
||
yml_change >/dev/null 2>&1
|
||
append_luci_fake_ip_filter_extras >/dev/null 2>&1
|
||
normalize_fake_ip_filter >/dev/null 2>&1
|
||
drop_legacy_cn_ip_injection >/dev/null 2>&1
|
||
dns_leak_protect=$(uci -q get clashoo.config.dns_leak_protect 2>/dev/null)
|
||
if command -v dns_mihomo_apply_leak_protect >/dev/null 2>&1; then
|
||
case "$(uci -q get clashoo.config.ipv6_proxy 2>/dev/null)" in
|
||
1|true) _ipv6_proxied=1 ;;
|
||
*) _ipv6_proxied=0 ;;
|
||
esac
|
||
if [ "${dns_leak_protect:-0}" = "1" ] && [ "$_ipv6_proxied" = "0" ]; then
|
||
_ipv6_value="false"
|
||
else
|
||
case "$(uci -q get clashoo.config.enable_ipv6 2>/dev/null)" in
|
||
true|1) _ipv6_value="true" ;;
|
||
false|0) _ipv6_value="false" ;;
|
||
*) _ipv6_value="" ;;
|
||
esac
|
||
fi
|
||
unset _ipv6_proxied
|
||
dns_mihomo_apply_leak_protect "$CONFIG_YAML" "${dns_leak_protect:-0}" "$_ipv6_value" >/dev/null 2>&1
|
||
unset _ipv6_value
|
||
fi
|
||
smart_inject >/dev/null 2>&1
|
||
|
||
if ! _has_valid_local_mmdb; then
|
||
log_msg "GeoIP database not found, continue without local GeoIP DB" "未找到本地 GeoIP 数据库,继续启动(可在系统页更新 GeoIP)"
|
||
fi
|
||
|
||
_prepare_geo_bootstrap_mode
|
||
_remap_dns_system_nameserver >/dev/null 2>&1
|
||
ensure_dns_cn_policy_fallback >/dev/null 2>&1
|
||
|
||
|
||
if [ ! -f /etc/clashoo/config.yaml ];then
|
||
log_msg "Cannot find config.yaml" "找不到config.yaml"
|
||
backend_remove_rules >/dev/null 2>&1
|
||
sleep 1
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
exit 0
|
||
fi
|
||
|
||
yml_dns_change >/dev/null 2>&1
|
||
|
||
# Sync processed config (with DNS listen injected) to mihomo working directory
|
||
cp -f "$CONFIG_YAML" "${CLASH_CONFIG}/config.yaml" 2>/dev/null
|
||
|
||
ip_rules >/dev/null 2>&1
|
||
}
|
||
|
||
_core_only_ui_args() {
|
||
[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ] || return 0
|
||
[ -f /etc/clashoo/dashboard/index.html ] || return 0
|
||
grep -Eq '^[[:space:]]*external-ui:' "$CONFIG_YAML" 2>/dev/null && return 0
|
||
printf ' -ext-ui %s' "$(shell_quote /etc/clashoo/dashboard)"
|
||
}
|
||
|
||
_start_launch_core() {
|
||
local _cmd _core_only
|
||
disable_quic_gso=$(uci get clashoo.config.disable_quic_gso 2>/dev/null)
|
||
_core_only=$(uci -q get clashoo.config.core_only 2>/dev/null)
|
||
mkdir -p "$(dirname "$CORE_LOG_PATH")" >/dev/null 2>&1
|
||
touch "$CORE_LOG_PATH" >/dev/null 2>&1
|
||
|
||
# Core-only runs the imported config verbatim, which may reference absolute
|
||
# paths (e.g. OpenClash's external-ui /usr/share/openclash/ui). mihomo's
|
||
# SAFE_PATHS guard rejects paths outside the working dir, so relax it here —
|
||
# in core-only the user is explicitly trusting their own config.
|
||
[ "$_core_only" = "1" ] && export SAFE_PATHS="/"
|
||
|
||
# preflight dry-run with watchdog: bail on bad config without touching
|
||
# firewall/dnsmasq, and never let a geo auto-download (mihomo blocks ~90s on
|
||
# a missing GeoIP/GeoSite when GitHub is unreachable) freeze startup/UI.
|
||
_run_with_timeout 15 "$CLASH" -d "$CLASH_CONFIG" -t >> "$CORE_LOG_PATH" 2>&1
|
||
_pf_rc=$?
|
||
if [ "$_pf_rc" -ne 0 ]; then
|
||
_pf_reason="$(_preflight_reason)"
|
||
if [ "$_pf_rc" -eq 124 ]; then
|
||
log_msg "Config preflight timed out (likely downloading geo data), retry shortly. ${_pf_reason}" "配置预检超时(可能正在下载 Geo 数据),稍后自动重试。${_pf_reason}"
|
||
runtime_state_record_health "fail" "preflight:timeout"
|
||
_start_geoip_update_async
|
||
else
|
||
log_msg "Config dry-run failed; firewall/DNS untouched. ${_pf_reason}" "配置预检失败,未应用防火墙与 DNS 改动。${_pf_reason}"
|
||
runtime_state_record_health "fail" "preflight:config_dry_run_failed"
|
||
fi
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
return 1
|
||
fi
|
||
|
||
procd_open_instance
|
||
_cmd="$(shell_quote "$CLASH") -d $(shell_quote "$CLASH_CONFIG")$(_core_only_ui_args) >> $(shell_quote "$CORE_LOG_PATH") 2>&1"
|
||
procd_set_param command /bin/sh -c "$_cmd"
|
||
[ "$disable_quic_gso" = "1" ] && procd_set_param env QUIC_GO_DISABLE_GSO=1
|
||
[ "$_core_only" = "1" ] && procd_set_param env SAFE_PATHS=/
|
||
procd_set_param respawn 3600 5 5
|
||
procd_close_instance
|
||
[ "$disable_quic_gso" = "1" ] && log_msg "Disable quic-go GSO support" "已启用:禁用 quic-go GSO 支持"
|
||
ln -s /usr/share/clashoo/yacd /www/luci-static/yacd 2>/dev/null
|
||
|
||
# Core-only: just keep the core process; do not take over firewall/DNS/cron.
|
||
if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "1" ]; then
|
||
log_msg "Configuring firewall rules" "配置防火墙规则"
|
||
backend_apply_rules >/dev/null 2>&1
|
||
|
||
if [ "$(_dnsmasq_state_digest)" != "$_dnsmasq_digest_before" ]; then
|
||
log_msg "Reloading DNS resolver" "重启 DNS 解析"
|
||
_reload_dnsmasq >/dev/null 2>&1
|
||
fi
|
||
|
||
restore >/dev/null 2>&1
|
||
add_cron
|
||
else
|
||
log_msg "Core-only mode: skipping firewall/DNS takeover" "仅内核模式:跳过防火墙与 DNS 接管"
|
||
fi
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
# procd starts the core process only after start_service() returns, so we
|
||
# cannot block here waiting for it. Launch a background watcher instead.
|
||
# If the core never appears within the window, roll back the firewall
|
||
# rules and dnsmasq override we already applied — otherwise the box is
|
||
# left with a hijacked DNS pointing at a port nobody is listening on.
|
||
(
|
||
exec 1000>&-
|
||
_bg_max="$(_compute_start_wait)"
|
||
bg_wait=0
|
||
while [ "$bg_wait" -lt "$_bg_max" ]; do
|
||
sleep 1
|
||
if _core_running; then
|
||
runtime_run_health_check_settle >/dev/null 2>&1
|
||
# kick access check immediately so panel shows current state (daemon polls every 5-20s)
|
||
/usr/share/clashoo/net/access_check_cache.sh >/dev/null 2>&1 &
|
||
# bootstrap mode: now that the proxy is up, fetch geo + reload full config
|
||
_finish_geo_bootstrap
|
||
exit 0
|
||
fi
|
||
bg_wait=$((bg_wait + 1))
|
||
done
|
||
_rollback_failed_start
|
||
runtime_state_record_health "fail" "start:core_not_running"
|
||
echo " $(date "+%Y-%m-%d %H:%M:%S") - clashoo core not detected within ${_bg_max}s, rolled back" >> /usr/share/clashoo/clashoo.txt
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
) &
|
||
}
|
||
|
||
boot(){
|
||
# sing-box is managed by Clashoo when selected; keep its own init.d from
|
||
# starting independently before Clashoo has prepared config/firewall/DNS.
|
||
uci -q set sing-box.main.enabled='0' && uci -q commit sing-box >/dev/null 2>&1 || true
|
||
|
||
if [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then
|
||
runtime_state_reset
|
||
runtime_state_set "health_status" "starting"
|
||
runtime_state_set "health_detail" "boot_starting"
|
||
runtime_state_set "updated_at" "$(date +%s)"
|
||
# start_service does mihomo -t dry-run + firewall restart + dnsmasq reload/fallback
|
||
# (~20-30s on LXC). Doing this synchronously in boot() blocks rcS S99 stage
|
||
# and during that window sshd/uhttpd refuse connections (kex reset). Detach
|
||
# to background with a small settle delay so the rcS pipeline finishes and
|
||
# management plane stays responsive while clashoo brings the proxy up.
|
||
(
|
||
exec 1000>&- </dev/null >/dev/null 2>&1
|
||
sleep 5
|
||
_clashoo_enabled || exit 0
|
||
/etc/init.d/clashoo start
|
||
) &
|
||
else
|
||
runtime_state_reset
|
||
runtime_state_set "health_status" "stopped"
|
||
runtime_state_set "health_detail" "boot_disabled"
|
||
runtime_state_set "updated_at" "$(date +%s)"
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
start_service(){
|
||
#===========================================================================================================================
|
||
lang=$(uci get luci.main.lang 2>/dev/null)
|
||
enable=$(uci get clashoo.config.enable 2>/dev/null)
|
||
mode=$(uci get clashoo.config.mode 2>/dev/null)
|
||
tun_mode=$(uci get clashoo.config.tun_mode 2>/dev/null)
|
||
core_only=$(uci -q get clashoo.config.core_only 2>/dev/null)
|
||
|
||
sync_legacy_core_field
|
||
_migrate_legacy_mihomo_stable
|
||
_fallback_missing_mihomo_core
|
||
CLASH="$(_detect_selected_core)"
|
||
|
||
cleanup_legacy_startup_backups
|
||
migrate_legacy_clash_dir
|
||
/usr/share/clashoo/migrate_lan_acl.sh >/dev/null 2>&1 || true
|
||
/usr/share/clashoo/migrate_fake_ip_filter.sh >/dev/null 2>&1 || true
|
||
runtime_state_reset
|
||
|
||
if [ "${enable:-0}" -eq 1 ]; then
|
||
resolve_effective_tp_modes
|
||
runtime_state_record_preflight "$(selected_core_family)"
|
||
log_msg "Starting Clashoo" "启动 Clashoo"
|
||
if is_singbox_selected; then
|
||
if _core_running; then stop_service >/dev/null 2>&1; fi
|
||
resolve_effective_tp_modes
|
||
runtime_state_record_preflight "singbox"
|
||
if ! _require_runtime_network_stack; then
|
||
runtime_state_record_health "fail" "preflight:missing_fw4_stack"
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
return 1
|
||
fi
|
||
if ! _start_validate_core; then
|
||
runtime_state_record_health "fail" "preflight:core_validation_failed"
|
||
return 1
|
||
fi
|
||
if ! start_singbox_service; then
|
||
# preflight failure already recorded; avoid blanket overwrite
|
||
case "$(grep '^health_detail=' "$RUNTIME_STATE_FILE" 2>/dev/null | cut -d= -f2-)" in
|
||
preflight:*) ;;
|
||
*) runtime_state_record_health "fail" "start:singbox_service_failed" ;;
|
||
esac
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
return 1
|
||
fi
|
||
if [ "$core_only" != "1" ]; then
|
||
log_msg "Configuring firewall rules" "配置防火墙规则"
|
||
backend_apply_rules >/dev/null 2>&1
|
||
singbox_dns_change >/dev/null 2>&1
|
||
add_cron >/dev/null 2>&1
|
||
else
|
||
log_msg "Core-only mode: skipping firewall/DNS takeover" "仅内核模式:跳过防火墙与 DNS 接管"
|
||
fi
|
||
# Sync-wait: if sing-box dies within window, roll back applied state.
|
||
# /etc/init.d/sing-box start forks immediately so we can poll here.
|
||
if ! _wait_core_alive "$(_compute_start_wait)" sing-box; then
|
||
_rollback_failed_start
|
||
runtime_state_record_health "fail" "start:singbox_died_after_apply"
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
return 1
|
||
fi
|
||
(exec 1000>&-; runtime_run_health_check_settle >/dev/null 2>&1) &
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
return 0
|
||
fi
|
||
if _core_running; then stop_service >/dev/null 2>&1; fi
|
||
if ! select_config >/dev/null 2>&1; then
|
||
runtime_state_record_health "fail" "preflight:config_missing"
|
||
_log_runtime_error "Config file is missing or empty; please import a subscription/config first"
|
||
return 1
|
||
fi
|
||
|
||
if ! _require_runtime_network_stack; then
|
||
runtime_state_record_health "fail" "preflight:missing_fw4_stack"
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
exit 0
|
||
fi
|
||
|
||
|
||
if ! _start_validate_core; then
|
||
runtime_state_record_health "fail" "preflight:core_validation_failed"
|
||
return 1
|
||
fi
|
||
|
||
if [ -f "$CONFIG_YAML" ] && [ -s "$CONFIG_YAML" ] ; then
|
||
|
||
_dnsmasq_digest_before="$(_dnsmasq_state_digest)"
|
||
_start_prepare_config
|
||
|
||
_start_launch_core
|
||
|
||
else
|
||
if [ "${lang}" = "en" ]; then
|
||
echo " $(date "+%Y-%m-%d %H:%M:%S") - problem with config.yaml, config.yaml is either empty or not found " >> /usr/share/clashoo/clashoo.txt
|
||
elif [ "${lang}" = "zh_cn" ]; then
|
||
echo " $(date "+%Y-%m-%d %H:%M:%S") - 你的config.yaml有问题还是为了空 0kb " >> /usr/share/clashoo/clashoo.txt
|
||
fi
|
||
runtime_state_record_health "fail" "start:config_yaml_invalid"
|
||
log_msg "Config file invalid or empty" "配置文件无效或为空"
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
fi
|
||
|
||
else
|
||
runtime_state_set "health_status" "stopped"
|
||
runtime_state_set "health_detail" "service_disabled"
|
||
runtime_state_set "updated_at" "$(date +%s)"
|
||
if [ "${lang}" = "en" ]; then
|
||
echo " $(date "+%Y-%m-%d %H:%M:%S") - Enable Client and Start Client Again" >> /usr/share/clashoo/clashoo.txt
|
||
elif [ "${lang}" = "zh_cn" ]; then
|
||
echo " $(date "+%Y-%m-%d %H:%M:%S") - 启用客户端并重新启动客户端" >> /usr/share/clashoo/clashoo.txt
|
||
fi
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
fi
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
|
||
remove_mark(){
|
||
/usr/share/clashoo/net/fw4.sh remove >/dev/null 2>&1
|
||
return 0
|
||
}
|
||
|
||
stop_service(){
|
||
#===========================================================================================================================
|
||
# The updater restarts once after an in-place APK upgrade. Skip the
|
||
# package-manager pre-deinstall stop to avoid its ~120s script wait.
|
||
if [ -r /var/run/clashoo_package_upgrade ]; then
|
||
_upgrade_pid="$(cat /var/run/clashoo_package_upgrade 2>/dev/null)"
|
||
case "$_upgrade_pid" in
|
||
''|*[!0-9]*) rm -f /var/run/clashoo_package_upgrade ;;
|
||
*)
|
||
[ -d "/proc/$_upgrade_pid" ] && return 0
|
||
rm -f /var/run/clashoo_package_upgrade
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
lang=$(uci get luci.main.lang 2>/dev/null)
|
||
tun_mode=$(uci get clashoo.config.tun_mode 2>/dev/null)
|
||
|
||
# server.list is read-only; just clean up merged temp files in /tmp
|
||
rm -f /tmp/clashoo/fake_filter.list /tmp/clashoo/fake_filter.yaml 2>/dev/null
|
||
|
||
log_msg "Stopping Clashoo" "正在停止 Clashoo"
|
||
for _ac_sig in TERM KILL; do
|
||
for _ac_pid in $(pgrep -f access_check_daemon 2>/dev/null); do
|
||
tr '\0' ' ' < "/proc/$_ac_pid/cmdline" 2>/dev/null | grep -q access_check_daemon.sh && kill -"$_ac_sig" "$_ac_pid" 2>/dev/null
|
||
done
|
||
done
|
||
rm -rf /tmp/clashoo/access_check_daemon.pid /tmp/clashoo/access_check_daemon.lock 2>/dev/null
|
||
rm -f /var/run/clashoo_bootstrap_pending 2>/dev/null
|
||
log_msg "Removing firewall rules" "清理防火墙规则"
|
||
backend_remove_rules >/dev/null 2>&1
|
||
cleanup_stale_core_nft >/dev/null 2>&1
|
||
revert_dns >/dev/null 2>&1
|
||
|
||
sh /usr/share/clashoo/runtime/backup.sh >/dev/null 2>&1
|
||
|
||
stop_singbox_service
|
||
_kill_core
|
||
|
||
rm -rf /www/luci-static/yacd 2>/dev/null
|
||
|
||
del_cron >/dev/null 2>&1
|
||
|
||
|
||
echo "" >/usr/share/clashoo/clashoo.txt >/dev/null 2>&1
|
||
|
||
echo "0" > /usr/share/clashoo/logstatus_check >/dev/null 2>&1
|
||
|
||
if [ "${lang}" = "en" ]; then
|
||
echo " $(date "+%Y-%m-%d %H:%M:%S") - CLIENT IS DISABLED " >> /usr/share/clashoo/clashoo.txt
|
||
elif [ "${lang}" = "zh_cn" ]; then
|
||
echo " $(date "+%Y-%m-%d %H:%M:%S") - 客户端被禁用 " >> /usr/share/clashoo/clashoo.txt
|
||
fi
|
||
echo "Clashoo" > "$REAL_LOG"
|
||
|
||
if _core_running; then
|
||
_kill_core
|
||
fi
|
||
for _cpid in $(pgrep -f /etc/clashoo 2>/dev/null); do
|
||
_ccl="$(tr '\0' ' ' < "/proc/$_cpid/cmdline" 2>/dev/null)"
|
||
printf '%s' "$_ccl" | grep -q -e '-d /etc/clashoo' -e '-f /etc/clashoo/config.yaml' && kill "$_cpid" 2>/dev/null
|
||
done
|
||
runtime_state_reset
|
||
runtime_state_set "health_status" "stopped"
|
||
runtime_state_set "health_detail" "service_stopped"
|
||
runtime_state_set "updated_at" "$(date +%s)"
|
||
|
||
# kick access check on stop (proxy_listening fast-paths to down; UI reflects stop within 5s)
|
||
(exec 1000>&-; /usr/share/clashoo/net/access_check_cache.sh >/dev/null 2>&1) &
|
||
|
||
#===========================================================================================================================
|
||
}
|
||
|
||
reload_service()
|
||
{
|
||
if is_singbox_selected; then
|
||
if _singbox_running; then
|
||
resolve_effective_tp_modes
|
||
runtime_state_record_preflight "singbox"
|
||
prepare_singbox_runtime >/dev/null 2>&1
|
||
/etc/init.d/sing-box restart >/dev/null 2>&1
|
||
singbox_dns_change >/dev/null 2>&1
|
||
runtime_run_health_check_settle >/dev/null 2>&1
|
||
fi
|
||
elif _core_running; then
|
||
resolve_effective_tp_modes
|
||
runtime_state_record_preflight "mihomo"
|
||
backend_remove_rules 2>/dev/null
|
||
( select_config && _start_prepare_config ) >/dev/null 2>&1
|
||
# hot-reload via mihomo REST API (config + addtype rules), no process restart or tunnel drop
|
||
local _dp _ds _lip
|
||
_dp="$(uci -q get clashoo.config.dash_port)"; [ -n "$_dp" ] || _dp=9090
|
||
_ds="$(uci -q get clashoo.config.dash_pass)"
|
||
_lip="$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F'/' '{print $1}')"
|
||
[ -n "$_lip" ] || _lip=127.0.0.1
|
||
curl -m 5 -H "Authorization: Bearer ${_ds}" -H "Content-Type: application/json" \
|
||
-X PUT -d '{"path":"'"$CONFIG_YAML"'"}' \
|
||
"http://${_lip}:${_dp}/configs?force=true" >/dev/null 2>&1
|
||
backend_apply_rules 2>/dev/null
|
||
if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "1" ]; then
|
||
_reload_dnsmasq >/dev/null 2>&1
|
||
fi
|
||
runtime_run_health_check_settle >/dev/null 2>&1
|
||
fi
|
||
}
|
||
|
||
status_service() {
|
||
if _selected_core_running; then
|
||
echo "running"
|
||
else
|
||
echo "inactive"
|
||
fi
|
||
}
|