Files
small-packages/clashoo/files/etc/init.d/clashoo
T
2026-09-10 23:42:03 +08:00

2404 lines
86 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
}