diff --git a/clashoo/Makefile b/clashoo/Makefile new file mode 100644 index 000000000..8b8087c8a --- /dev/null +++ b/clashoo/Makefile @@ -0,0 +1,111 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=clashoo +PKG_SHORT_SHA:=63bd52e +PKG_COMMIT_DATE:=2026.09.27 + +PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA) +PKG_RELEASE:=1 + +PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz +PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA) +PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA) +PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src +PKG_SOURCE_VERSION:=16a867c31778167b5297025cafd444ea8463b108 +PKG_HASH:=skip +PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA) + +PKG_LICENSE:=GPL3.0+ +PKG_MAINTAINER:=kenzok8 + +PKG_BUILD_DEPENDS:=golang/host +PKG_BUILD_PARALLEL:=1 +PKG_BUILD_FLAGS:=no-mips16 + +PKG_BUILD_TIME:=$(shell date -u -Iseconds) + +GO_PKG:=github.com/metacubex/mihomo +GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME) +GO_PKG_TAGS:=with_gvisor + +include $(INCLUDE_DIR)/package.mk +include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk + +define Package/clashoo + SECTION:=net + CATEGORY:=Network + TITLE:=Clashoo (Mihomo) + DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle +yq +firewall4 +ip-full +coreutils-nohup +coreutils-timeout +kmod-inet-diag +kmod-nft-socket +kmod-nft-tproxy +kmod-tun +kmod-dummy +unzip + PROVIDES:=mihomo clash-meta +endef + +define Package/clashoo/description + Mihomo proxy core with runtime environment for Clashoo. +endef + +define Package/clashoo/conffiles +/etc/config/clashoo +endef + +define Package/clashoo/install + $(call GoPackage/Package/Install/Bin,$(1)) + + $(INSTALL_DIR) $(1)/usr/bin + ln -sf /usr/bin/mihomo $(1)/usr/bin/clash-meta + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_DIR) $(1)/etc/clashoo + $(INSTALL_DIR) $(1)/etc/clashoo/provider + $(INSTALL_DIR) $(1)/etc/clashoo/proxyprovider + $(INSTALL_DIR) $(1)/etc/clashoo/ruleprovider + $(INSTALL_DIR) $(1)/usr/share/clashoo + $(INSTALL_DIR) $(1)/usr/share/clashbackup + $(INSTALL_DIR) $(1)/usr/share/clashoo/config + $(INSTALL_DIR) $(1)/usr/share/clashoo/config/sub + $(INSTALL_DIR) $(1)/usr/share/clashoo/config/upload + $(INSTALL_DIR) $(1)/usr/share/clashoo/config/custom + + $(INSTALL_BIN) $(CURDIR)/files/etc/init.d/clashoo $(1)/etc/init.d/clashoo + $(INSTALL_CONF) $(CURDIR)/files/etc/config/clashoo $(1)/etc/config/clashoo + $(INSTALL_DATA) $(CURDIR)/files/etc/config/clashoo $(1)/usr/share/clashoo/clashoo.default + $(CP) $(CURDIR)/files/usr/share/clashoo/* $(1)/usr/share/clashoo/ + $(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/migrate_lan_acl.sh $(1)/usr/share/clashoo/migrate_lan_acl.sh + $(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/migrate_fake_ip_filter.sh $(1)/usr/share/clashoo/migrate_fake_ip_filter.sh + $(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/detect_core_arch.sh $(1)/usr/share/clashoo/detect_core_arch.sh + $(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/diagnose_bypass.sh $(1)/usr/share/clashoo/diagnose_bypass.sh + + $(RM) $(1)/usr/share/clashoo/dashboard + $(RM) $(1)/usr/share/clashoo/yacd + $(RM) $(1)/usr/share/clashoo/update.log + $(RM) $(1)/usr/share/clashoo/geoip.log + $(RM) $(1)/usr/share/clashoo/core_down_complete +endef + +define Package/clashoo/postinst +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + [ -x /usr/bin/mihomo ] && [ ! -x /usr/bin/clash-meta ] && ln -sf /usr/bin/mihomo /usr/bin/clash-meta + rm -f /usr/share/clashoo/check_luci_version.sh /usr/share/clashoo/check_clash_meta_version.sh /usr/share/clashoo/check_mihomo_core_version.sh + rm -f /usr/share/clashoo/cuslist.sh /usr/share/clashoo/groups.sh /usr/share/clashoo/load_groups.sh /usr/share/clashoo/panel_diag.sh + rm -f /usr/share/clashoo/rmlist.sh /usr/share/clashoo/uplist.sh /usr/share/clashoo/rpc_restart_async.sh /usr/share/clashoo/rpc_update_china_ip_async.sh + rm -rf /usr/share/clashoo/create + /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 + /usr/share/clashoo/detect_core_arch.sh >/dev/null 2>&1 || true + if [ "$$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then + /etc/init.d/clashoo enable 2>/dev/null || true + else + /etc/init.d/clashoo disable 2>/dev/null || true + fi +fi +exit 0 +endef + +define Build/Prepare + $(Build/Prepare/Default) + $(RM) -r $(PKG_BUILD_DIR)/rules/logic_test +endef + +$(eval $(call GoBinPackage,clashoo)) +$(eval $(call BuildPackage,clashoo)) diff --git a/clashoo/files/etc/config/clashoo b/clashoo/files/etc/config/clashoo new file mode 100644 index 000000000..1b0e23920 --- /dev/null +++ b/clashoo/files/etc/config/clashoo @@ -0,0 +1,156 @@ + +config clashoo 'config' + option redir_port '7891' + option http_port '8080' + option socks_port '1080' + option mixed_port '7890' + option enable_ipv6 'true' + option enable '0' + option dash_port '9090' + option dashboard_panel 'zashboard' + option level 'info' + option core '3' + option allow_lan '1' + option dnsforwader '1' + option subcri 'meta' + option cusrule '0' + option dnscache '1' + option p_mode 'rule' + option append_rules '0' + option enhanced_mode 'fake-ip' + option fake_ip_range '198.18.0.1/16' + option fake_ip_range6 'fc00::/18' + list fake_ip_filter '+.lan' + list fake_ip_filter '+.local' + list fake_ip_filter 'localhost.ptlogin2.qq.com' + list fake_ip_filter 'rule-set:cn_domain' + option fake_ip_filter_migrated '1' + option listen_port '1053' + option interf '0' + option download_core 'amd64-compatible' + option dcore '3' + option same_tag '1' + option geoip_source '3' + option geoip_format 'mmdb' + option geodata_loader 'standard' + option tproxy_port '7982' + option access_control '0' + option acl_migrated '1' + option tcp_mode 'redirect' + option tun_mode '0' + option core_mirror_prefix 'https://gh-proxy.com/' + option core_download_advanced '0' + list default_nameserver '223.5.5.5' + list default_nameserver '119.29.29.29' + option dns_ecs '' + option dns_ecs_override '0' + option fallback_filter_geoip '0' + list fallback_filter_ipcidr '240.0.0.0/4' + option dns_respect_rules '1' + option dns_loopback_compat '0' + option dns_migrated '1' + option block_quic '0' + option singbox_independent_cache '0' + option udp_mode 'tproxy' + option ipv4_dns_hijack '1' + option ipv6_dns_hijack '1' + option ipv4_proxy '1' + option ipv6_proxy '1' + option fake_ip_ping_hijack '1' + option dns_leak_protect '0' + option geoip_mmdb_url 'https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb' + option geosite_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat' + option geoip_dat_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat' + option enable_udp '1' + option stack 'gvisor' + option bypass_china '1' + option bypass_china_ipv6 '1' + option bypass_port_mode 'common' + option sniffer_streaming '1' + option selection_cache '1' + option fake_ip_cache '1' + option enable_dns '1' + option disable_quic_gso '1' + option smart_policy_priority 'Premium:0.9;SG:1.3' + option smart_prefer_asn '1' + option smart_uselightgbm '1' + option smart_collectdata '1' + option smart_collect_size '100' + option smart_collect_rate '1' + option smart_lgbm_auto_update '1' + option smart_lgbm_update_interval '72' + option smart_lgbm_url 'https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin' + list defaul_nameserver '114.114.114.114' + list defaul_nameserver '223.5.5.5' + option auto_update '1' + option auto_update_time '72' + option auto_subscription_update '0' + option subscription_update_interval '72' + option auto_clear_log '1' + option clear_time '72' + option core_type 'mihomo' + option smart_auto_switch '0' + option core_only '0' + +config hosts + option enabled '0' + option adress '*.clash.dev' + option ip '127.0.0.1' + +config hosts + option enabled '0' + option adress 'alpha.clash.dev' + option ip '::1' + +config dnsservers + option enabled '1' + option ser_type 'nameserver' + option ser_address 'https://dns.alidns.com/dns-query' + option protocol 'none' + +config dnsservers + option enabled '1' + option ser_type 'nameserver' + option ser_address 'https://doh.pub/dns-query' + option protocol 'none' + +config dnsservers + option enabled '1' + option ser_type 'proxy-server-nameserver' + option ser_address '1.1.1.1' + option protocol 'tls://' + option ser_port '853' + +config dnsservers + option enabled '1' + option ser_type 'fallback' + option ser_address 'https://cloudflare-dns.com/dns-query' + option protocol 'none' + +config dnsservers + option enabled '1' + option ser_type 'fallback' + option ser_address 'https://dns.google/dns-query' + option protocol 'none' + +config dnsservers + option enabled '1' + option ser_type 'direct-nameserver' + option ser_address '223.5.5.5' + option protocol 'udp://' + +config dns_policy + option enabled '1' + option policy_type 'nameserver-policy' + option matcher 'geosite:cn' + list nameserver 'https://dns.alidns.com/dns-query' + list nameserver 'https://doh.pub/dns-query' + +config authentication + +# default empty custom-rule row so the section shows the input form, not just an add +# button; injectors skip rules with an empty ipaaddr, so it generates nothing until filled +config addtype + option type 'DOMAIN-SUFFIX' + option pgroup 'DIRECT' + option ipaaddr '' diff --git a/clashoo/files/etc/init.d/clashoo b/clashoo/files/etc/init.d/clashoo new file mode 100755 index 000000000..b7e25ab9a --- /dev/null +++ b/clashoo/files/etc/init.d/clashoo @@ -0,0 +1,2507 @@ +#!/bin/sh /etc/rc.common +USE_PROCD=1 +EXTRA_COMMANDS="refresh_cron" + +START=99 +STOP=15 + +CORE_LOG_PATH="/var/log/clashoo/core.log" +SINGBOX_BIN_DIR="/usr/share/clashoo/bin" +SINGBOX_RUNTIME_DIR="/var/run/clashoo/sing-box" +SINGBOX_RUNTIME_CONFIG="$SINGBOX_RUNTIME_DIR/config.json" +SINGBOX_WORKDIR="$SINGBOX_RUNTIME_DIR/work" + +. /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 +} + +selected_singbox_binary() { + local dcore + dcore="$(uci -q get clashoo.config.dcore 2>/dev/null)" + case "$dcore" in + 4) echo "$SINGBOX_BIN_DIR/sing-box-stable" ;; + 5) echo "$SINGBOX_BIN_DIR/sing-box-alpha" ;; + *) return 1 ;; + esac +} + +_migrate_legacy_singbox_binary() { + local dcore target legacy staged + dcore="$(uci -q get clashoo.config.dcore 2>/dev/null)" + target="$(selected_singbox_binary)" || return 1 + [ -e "$target" ] && return 0 + case "$dcore" in + 4) legacy="/usr/bin/sing-box-stable" ;; + 5) legacy="/usr/bin/sing-box-alpha" ;; + *) return 1 ;; + esac + [ -x "$legacy" ] || return 1 + mkdir -p "$SINGBOX_BIN_DIR" >/dev/null 2>&1 || return 1 + staged="${target}.$$" + rm -f "$staged" >/dev/null 2>&1 + cp -f "$legacy" "$staged" >/dev/null 2>&1 || { + rm -f "$staged" >/dev/null 2>&1 + return 1 + } + chmod 0755 "$staged" >/dev/null 2>&1 || { + rm -f "$staged" >/dev/null 2>&1 + return 1 + } + mv -f "$staged" "$target" >/dev/null 2>&1 || { + rm -f "$staged" >/dev/null 2>&1 + return 1 + } + return 0 +} + +_ensure_singbox_binary() { + local target + target="$(selected_singbox_binary)" || return 1 + [ -e "$target" ] || _migrate_legacy_singbox_binary || return 1 + [ -x "$target" ] +} + +_singbox_pid_owns_private_binary() { + local pid stable alpha exe cmdline + pid="$1" + stable="$SINGBOX_BIN_DIR/sing-box-stable" + alpha="$SINGBOX_BIN_DIR/sing-box-alpha" + [ -d "/proc/$pid" ] || return 1 + if command_exists readlink; then + exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null)" + if [ "$exe" = "$stable" ] || [ "$exe" = "$alpha" ]; then + return 0 + fi + fi + cmdline="$(tr '\000' ' ' < "/proc/$pid/cmdline" 2>/dev/null)" + case "$cmdline" in + "$stable"|"$stable "*|"$alpha"|"$alpha "*) return 0 ;; + esac + return 1 +} + +_singbox_instance_pid() { + local _pid + _pid="$(ubus call service list '{"name":"clashoo","verbose":true}' 2>/dev/null \ + | jsonfilter -e '@.clashoo.instances.singbox.pid' 2>/dev/null | head -n1)" + [ -n "$_pid" ] && _singbox_pid_owns_private_binary "$_pid" && echo "$_pid" +} + +_singbox_running() { + local _pid + _pid="$(_singbox_instance_pid)" + [ -n "$_pid" ] && [ -d "/proc/$_pid" ] +} + +# 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 tmp_config + active="$(uci -q get clashoo.config.singbox_active 2>/dev/null)" + src="/usr/share/clashoo/config/singbox/${active}" + tmp_config="${SINGBOX_RUNTIME_CONFIG}.tmp.$$" + + mkdir -p "$SINGBOX_RUNTIME_DIR" "$SINGBOX_WORKDIR" "$(dirname "$CORE_LOG_PATH")" >/dev/null 2>&1 || return 1 + rm -f "$tmp_config" >/dev/null 2>&1 + if [ -n "$active" ] && [ -r "$src" ]; then + cp -f "$src" "$tmp_config" >/dev/null 2>&1 || { + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } + elif [ -r "$SINGBOX_RUNTIME_CONFIG" ]; then + cp -f "$SINGBOX_RUNTIME_CONFIG" "$tmp_config" >/dev/null 2>&1 || { + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } + else + _log_runtime_error "sing-box config.json is missing; please generate or select a sing-box profile first" + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + fi + + command -v ucode >/dev/null 2>&1 || { + _log_runtime_error "ucode is required to prepare sing-box runtime config" + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } + # Migrate every imported config, including core-only mode, before any + # Clashoo-specific normalization. The temp file is in the runtime dir so + # a failed migration/normalization never replaces the last good config. + ucode /usr/share/clashoo/lib/migrate_singbox.uc "$tmp_config" >/dev/null 2>&1 || { + _log_runtime_error "sing-box runtime config migration failed" + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } + + if [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" != "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 \ + "$tmp_config" "$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" + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } + fi + + mv -f "$tmp_config" "$SINGBOX_RUNTIME_CONFIG" >/dev/null 2>&1 || { + rm -f "$tmp_config" >/dev/null 2>&1 + return 1 + } + return 0 +} + +start_singbox_service() { + local bin + bin="$(selected_singbox_binary)" || return 1 + if ! _ensure_singbox_binary; 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 "$bin" -c "$SINGBOX_RUNTIME_CONFIG" 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 + procd_open_instance "singbox" + procd_set_param command "$bin" run -c "$SINGBOX_RUNTIME_CONFIG" -D "$SINGBOX_WORKDIR" + procd_set_param user root + procd_set_param file "$SINGBOX_RUNTIME_CONFIG" + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param limits core="unlimited" + procd_set_param limits nofile="1000000 1000000" + procd_set_param respawn 3600 5 5 + procd_close_instance +} + +stop_singbox_service() { + local _wait=0 _pid + # procd_kill removes the named instance and disables its respawn; signaling + # the child through ubus would let procd recreate it behind our back. + procd_kill clashoo singbox >/dev/null 2>&1 || true + _pid="$(_singbox_instance_pid)" + [ -n "$_pid" ] || return 0 + while [ "$_wait" -lt 3 ]; do + _pid="$(_singbox_instance_pid)" + [ -z "$_pid" ] && return 0 + _singbox_running || 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 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 +} + +_dns_proxy_nameservers() { + local i=0 t en a p port s out="" prefix + 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)" + p="$(uci -q get clashoo.@dnsservers[$i].protocol 2>/dev/null)" + port="$(uci -q get clashoo.@dnsservers[$i].ser_port 2>/dev/null)" + i=$((i + 1)) + [ "$t" = "proxy-server-nameserver" ] || continue + [ "$en" = "0" ] && continue + if printf '%s' "$a" | grep -Eq '^[A-Za-z][A-Za-z0-9+.-]*://'; then + s="$a" + else + case "$p" in + ''|none) prefix='' ;; + udp|udp://) prefix='udp://' ;; + tcp|tcp://) prefix='tcp://' ;; + dot|tls|tls://) prefix='tls://' ;; + doh|https|https://) prefix='https://' ;; + doq|quic|quic://) prefix='quic://' ;; + *) prefix="$p" ;; + esac + s="${prefix}${a}" + [ -n "$port" ] && s="${s}:${port}" + fi + [ -n "$s" ] && out="${out}${out:+ }${s}" + done + printf '%s' "$out" +} + +_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="$(_dns_proxy_nameservers)" + [ -n "$replacement" ] || replacement='tls://1.1.1.1:853' + 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 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() { + local core bin + core="$(uci -q get clashoo.config.core 2>/dev/null)" + if is_singbox_selected; then + bin="$(selected_singbox_binary)" + if [ -z "$bin" ] || ! _ensure_singbox_binary; 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(){ + 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 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 + # procd does not launch the named instance until start_service returns + # and rc.common submits the completed service definition. Watch it in + # the background, just like the mihomo path, then roll back only if the + # registered private instance never becomes healthy. + ( + exec 1000>&- + _bg_max="$(_compute_start_wait)" + _bg_wait=0 + while [ "$_bg_wait" -lt "$_bg_max" ]; do + sleep 1 + if _singbox_running; then + runtime_run_health_check_settle >/dev/null 2>&1 + exit 0 + fi + _bg_wait=$((_bg_wait + 1)) + done + _rollback_failed_start + runtime_state_record_health "fail" "start:singbox_not_running" + echo " $(date "+%Y-%m-%d %H:%M:%S") - clashoo sing-box not detected within ${_bg_max}s, rolled back" >> /usr/share/clashoo/clashoo.txt + echo "Clashoo" > "$REAL_LOG" + ) & + 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 + # Use rc.common's transaction wrapper so the complete Clashoo service + # definition is rebuilt before the named instance is registered again. + # This intentionally reapplies Clashoo's own firewall/DNS state; calling + # start_service directly would skip that wrapper and procd setup. + stop_singbox_service >/dev/null 2>&1 + start + 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 +} diff --git a/clashoo/files/usr/share/clashoo/detect_core_arch.sh b/clashoo/files/usr/share/clashoo/detect_core_arch.sh new file mode 100755 index 000000000..fbb3defda --- /dev/null +++ b/clashoo/files/usr/share/clashoo/detect_core_arch.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +[ -n "$(uci -q get clashoo.config.download_core 2>/dev/null)" ] && exit 0 + +raw="$(uname -m 2>/dev/null)" +[ -n "$raw" ] || raw="$(apk --print-arch 2>/dev/null)" +[ -n "$raw" ] || raw="$(opkg status libc 2>/dev/null | awk -F': ' '/^Architecture/{print $2; exit}')" +[ -n "$raw" ] || exit 0 + +case "$raw" in + x86_64|amd64) arch="amd64-compatible" ;; + aarch64*|arm64) arch="arm64" ;; + armv7*|arm_cortex-a[7-9]*|arm_cortex-a1[0-9]*) arch="armv7" ;; + armv6*|arm_cortex-a[56]*) arch="armv6" ;; + arm*) arch="armv5" ;; + i[3-6]86) arch="386" ;; + mips64el*) arch="mips64le" ;; + mips64*) arch="mips64" ;; + mipsel*) arch="mipsle" ;; + mips*) arch="mips" ;; + *) exit 0 ;; +esac + +uci -q set clashoo.config.download_core="$arch" +uci -q commit clashoo +exit 0 diff --git a/clashoo/files/usr/share/clashoo/diagnose_bypass.sh b/clashoo/files/usr/share/clashoo/diagnose_bypass.sh new file mode 100644 index 000000000..0e01a30d2 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/diagnose_bypass.sh @@ -0,0 +1,181 @@ +#!/bin/sh + +umask 077 + +CONFIG_YAML="${CONFIG_YAML:-/etc/clashoo/config.yaml}" +NFT_V4="${NFT_V4:-/usr/share/clashoo/nftables/geoip_cn.nft}" +NFT_V6="${NFT_V6:-/usr/share/clashoo/nftables/geoip6_cn.nft}" +MAX_CONN=200 + +TMPDIR_D="$(mktemp -d /tmp/.clashoo_diag.XXXXXX 2>/dev/null)" || exit 1 +trap 'rm -rf "$TMPDIR_D" 2>/dev/null' EXIT INT TERM + +uci_get() { + uci -q get "clashoo.config.$1" 2>/dev/null +} + +section() { + printf '\n===== %s =====\n' "$1" +} + +yesno() { + [ -n "$1" ] && [ "$1" != "0" ] && [ "$1" != "false" ] && echo "on" || echo "off" +} + +bool_on() { + case "$1" in + 1|true|TRUE|yes|on) return 0 ;; + *) return 1 ;; + esac +} + +bypass4="$(uci_get bypass_china)" +bypass6="$(uci_get bypass_china_ipv6)" +[ -n "$bypass6" ] || bypass6="$bypass4" + +section "配置摘要" +printf '服务开关 : %s\n' "$(yesno "$(uci_get enable)")" +printf '运行模式 : %s\n' "$(uci_get enhanced_mode)" +printf 'TCP / UDP : %s / %s\n' "$(uci_get tcp_mode)" "$(uci_get udp_mode)" +printf '大陆绕过 v4 : %s\n' "$(yesno "$bypass4")" +printf '大陆绕过 v6 : %s%s\n' "$(yesno "$bypass6")" \ + "$([ -z "$(uci_get bypass_china_ipv6)" ] && echo '(未设置,跟随 v4)')" +printf '接管端口 : %s' "$(uci_get bypass_port_mode)" +[ "$(uci_get bypass_port_mode)" = "custom" ] && printf ' (%s)' "$(uci_get bypass_port_custom)" +printf '\n' +printf 'IPv6 代理 : %s\n' "$(yesno "$(uci_get ipv6_proxy)")" +printf '阻断 QUIC : %s\n' "$(yesno "$(uci_get block_quic)")" + +if [ "$(uci_get bypass_port_mode)" = "common" ]; then + printf '\n注意: 接管端口为 common,只代理 22/53/80/443/8080/8443,\n' + printf ' 游戏与 P2P 常用的高位端口不会走代理。\n' +fi + +section "fake-ip 过滤" +if [ -s "$CONFIG_YAML" ]; then + if grep -qE '^[[:space:]]*fake-ip-filter:' "$CONFIG_YAML" 2>/dev/null; then + filter_mode="$(uci_get fake_ip_filter_mode)" + [ -n "$filter_mode" ] || filter_mode="$(yq -M e '(.dns."fake-ip-filter-mode" // "blacklist") | tostring' "$CONFIG_YAML" 2>/dev/null)" + if [ "$filter_mode" = "whitelist" ]; then + echo "白名单模式:列表中的域名使用 fake-IP" + elif grep -qiE 'rule-set:cn_domain|RULE-SET,[[:space:]]*cn_domain,[[:space:]]*real-ip' "$CONFIG_YAML" 2>/dev/null; then + echo "含 rule-set:cn_domain (国内域名返回真实 IP)" + else + echo "缺少 rule-set:cn_domain" + echo " 国内域名会拿到 fake-ip 走代理,与硬编码国内 IP 分成两条路。" + fi + else + echo "配置中无 fake-ip-filter" + fi +else + echo "找不到 $CONFIG_YAML" +fi + +section "内核分流规则" +if [ -s "$CONFIG_YAML" ]; then + echo "-- rules 末尾 5 条 --" + grep -E '^[[:space:]]*-[[:space:]]*["'"'"']?(GEOIP|RULE-SET|MATCH|IP-CIDR|DOMAIN)' "$CONFIG_YAML" 2>/dev/null | tail -5 +fi + +section "IP 库" +for f in "$NFT_V4" "$NFT_V6"; do + if [ -s "$f" ]; then + printf '%-46s %6s 条 %s\n' "$f" \ + "$(grep -cE '^[[:space:]]*[0-9a-fA-F].*/' "$f" 2>/dev/null)" \ + "$(date -r "$f" '+%Y-%m-%d %H:%M' 2>/dev/null)" + else + printf '%-46s 缺失\n' "$f" + fi +done +for f in /etc/clashoo/Country.mmdb /etc/clashoo/geoip.metadb; do + [ -f "$f" ] && printf '%-46s %s\n' "$f" "$(ls -lh "$f" 2>/dev/null | awk '{print $5}')" +done + +set_count() { + nft list set inet fw4 "$1" 2>/dev/null | tr ',' '\n' | grep -cE '[0-9a-fA-F]+[.:].*/' +} + +section "防火墙绕过规则" +if nft list sets inet fw4 2>/dev/null | grep -q clashoo_china; then + for ch in dstnat mangle_prerouting; do + nft -a list chain inet fw4 "$ch" 2>/dev/null | + grep -E '@clashoo_china6? ' | sed "s|^[[:space:]]*| ${ch}: |" + done + echo + echo "已加载元素数(nft auto-merge 会合并相邻网段,少于文件行数属正常):" + bool_on "$bypass4" && printf ' clashoo_china : %s\n' "$(set_count clashoo_china)" + bool_on "$bypass6" && printf ' clashoo_china6 : %s\n' "$(set_count clashoo_china6)" +else + echo "当前 nftables 中没有 clashoo_china 集合(绕过未启用或服务未运行)" +fi + +section "绕过命中检查" +if ! bool_on "$bypass4" && ! bool_on "$bypass6"; then + echo "两个绕过均未启用,所有被接管的流量都交给内核判定,无需检查" +else + SECRET="$(uci_get dash_pass)" + PORT="$(uci_get dash_port)" + LAN_IP="$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F/ '{print $1}')" + [ -n "$LAN_IP" ] || LAN_IP="127.0.0.1" + CONN_JSON="$TMPDIR_D/connections.json" + + if [ -z "$PORT" ]; then + echo "未配置控制端口,跳过" + elif ! curl -m 5 -s -H "Authorization: Bearer ${SECRET}" \ + "http://${LAN_IP}:${PORT}/connections" -o "$CONN_JSON" 2>/dev/null; then + echo "无法访问内核 API (http://${LAN_IP}:${PORT}),跳过" + elif [ ! -s "$CONN_JSON" ]; then + echo "内核 API 返回为空,跳过" + else + jsonfilter -i "$CONN_JSON" -e '@["connections"][*]["metadata"]["destinationIP"]' \ + 2>/dev/null > "${CONN_JSON}.ip" + jsonfilter -i "$CONN_JSON" -e '@["connections"][*]["chains"][0]' \ + 2>/dev/null > "${CONN_JSON}.ch" + + total="$(wc -l < "${CONN_JSON}.ip" 2>/dev/null | tr -d ' ')" + [ -n "$total" ] || total=0 + + if [ "$total" -eq 0 ]; then + echo "当前无活动连接" + elif [ "$total" != "$(wc -l < "${CONN_JSON}.ch" 2>/dev/null | tr -d ' ')" ]; then + echo "内核 API 字段数不一致,跳过" + else + awk 'NR==FNR { a[FNR] = $0; next } { print a[FNR] "\t" $0 }' \ + "${CONN_JSON}.ip" "${CONN_JSON}.ch" 2>/dev/null \ + | awk -F'\t' '$2 != "DIRECT" && $1 != "" && $1 !~ /^(198\.18\.|::|fc00:|fd)/ { print $1 "\t" $2 }' \ + | sort -u | head -n "$MAX_CONN" > "${CONN_JSON}.chk" + + hit=0 + : > "${CONN_JSON}.bad" + while IFS="$(printf '\t')" read -r dip chain; do + [ -n "$dip" ] || continue + case "$dip" in + *:*) bool_on "$bypass6" || continue + set_name=clashoo_china6 ;; + *) bool_on "$bypass4" || continue + set_name=clashoo_china ;; + esac + if nft get element inet fw4 "$set_name" "{ $dip }" >/dev/null 2>&1; then + hit=$((hit + 1)) + printf ' %-40s -> %s\n' "$dip" "$chain" >> "${CONN_JSON}.bad" + fi + done < "${CONN_JSON}.chk" + + printf '活动连接 %s 条,其中走代理的去重目的地 %s 个(上限 %s)\n' \ + "$total" "$(wc -l < "${CONN_JSON}.chk" 2>/dev/null | tr -d ' ')" "$MAX_CONN" + if [ "$hit" -gt 0 ]; then + printf '命中绕过白名单却仍走代理: %s 个\n\n' "$hit" + head -20 "${CONN_JSON}.bad" + echo + echo "这些目的地在防火墙白名单内,本应直连,却出现在代理链路上。" + echo "可能是连接建立于规则变更之前,或该地址族的绕过规则未生效。" + else + echo "未发现白名单地址走代理的连接" + fi + rm -f "${CONN_JSON}.chk" "${CONN_JSON}.bad" 2>/dev/null + fi + rm -f "${CONN_JSON}.ip" "${CONN_JSON}.ch" 2>/dev/null + fi +fi + +printf '\n' diff --git a/clashoo/files/usr/share/clashoo/lib/migrate_singbox.uc b/clashoo/files/usr/share/clashoo/lib/migrate_singbox.uc new file mode 100644 index 000000000..a200b982e --- /dev/null +++ b/clashoo/files/usr/share/clashoo/lib/migrate_singbox.uc @@ -0,0 +1,385 @@ +#!/usr/bin/ucode + +'use strict'; + +import { readfile, writefile } from 'fs'; + +// Standalone sing-box config version-migration, used by the init.d core-only +// path to upgrade an imported config (momo/homeproxy/etc.) to the format the +// current sing-box core accepts, WITHOUT clashoo normalize/takeover. +// +// IMPORTANT: migrate_singbox() below is kept byte-for-byte in sync with the +// copy in luci-app-clashoo/.../luci.clashoo (the UI "migrate" button). If you +// change one, change the other. + +function migrate_singbox(cfg) { + // --- Fix DNS servers: address-based → type-based, remove detour --- + if (cfg.dns && cfg.dns.servers) { + cfg.dns.servers = map(cfg.dns.servers || [], function(srv) { + let addr = srv.address || ''; + let new_srv = {}; + let skip = { address: 1, detour: 1, address_resolver: 1, address_strategy: 1 }; + for (let k in srv) if (!skip[k]) new_srv[k] = srv[k]; + + if (addr === 'fakeip') { + new_srv.type = 'fakeip'; + } else if (addr === 'local' || addr === '') { + new_srv.type = new_srv.type || 'local'; + } else if (match(addr, /^rcode:\/\//) || addr === 'rcode') { + new_srv._rcode = true; // mark for removal, handled via dns rule action + } else if (match(addr, /^h3:\/\//)) { + let host = replace(replace(addr, /^h3:\/\//, ''), /\/.*$/, ''); + new_srv.type = 'h3'; new_srv.server = host; + } else if (match(addr, /^https:\/\//)) { + let host = replace(replace(addr, /^https:\/\//, ''), /\/.*$/, ''); + new_srv.type = 'https'; new_srv.server = host; + } else if (match(addr, /^tls:\/\//)) { + new_srv.type = 'tls'; new_srv.server = replace(addr, /^tls:\/\//, ''); + } else if (match(addr, /^tcp:\/\//)) { + new_srv.type = 'tcp'; new_srv.server = replace(addr, /^tcp:\/\//, ''); + } else if (addr && !new_srv.type) { + new_srv.type = 'udp'; new_srv.server = addr; // plain IP → UDP + } else if (addr) { + new_srv.address = addr; // unknown format, keep as-is + } + return new_srv; + }); + } + + // --- Remove rcode servers (unsupported in 1.12+ new format), convert referencing rules to reject --- + let rcode_tags = {}; + if (cfg.dns && cfg.dns.servers) { + let good = []; + for (let srv in cfg.dns.servers) { + if (srv._rcode) rcode_tags[srv.tag] = true; + else push(good, srv); + } + cfg.dns.servers = good; + } + if (cfg.dns && cfg.dns.rules && length(keys(rcode_tags)) > 0) { + cfg.dns.rules = map(cfg.dns.rules || [], function(rule) { + if (rule.server && rcode_tags[rule.server]) { + let r = {}; + for (let k in rule) if (k !== 'server' && k !== 'action') r[k] = rule[k]; + r.action = 'reject'; + return r; + } + return rule; + }); + } + + // --- Remove legacy dns.fakeip.enabled (1.12+ fakeip is configured via type:"fakeip" server) --- + if (cfg.dns && cfg.dns.fakeip) { + let fp = cfg.dns.fakeip; + let new_fp = {}; + for (let k in fp) if (k !== 'enabled') new_fp[k] = fp[k]; + if (length(keys(new_fp)) > 0) cfg.dns.fakeip = new_fp; + else delete cfg.dns['fakeip']; + } + + // --- Fix DNS rules: add action:"route" when server is set --- + if (cfg.dns && cfg.dns.rules) { + cfg.dns.rules = map(cfg.dns.rules || [], function(rule) { + if (rule.server && !rule.action) { + let r = {}; + for (let k in rule) r[k] = rule[k]; + r.action = 'route'; + return r; + } + return rule; + }); + } + + // --- Fix legacy special outbounds (block / dns types) --- + let special = {}; + if (cfg.outbounds) { + let kept = []; + for (let ob in cfg.outbounds) { + if (ob.type === 'block' || ob.type === 'dns') { + special[ob.tag] = ob.type; + } else { + push(kept, ob); + } + } + cfg.outbounds = kept; + } + + // Patch route rules that pointed to removed special outbounds + if (cfg.route && cfg.route.rules && length(keys(special)) > 0) { + cfg.route.rules = map(cfg.route.rules || [], function(rule) { + let sp = rule.outbound && special[rule.outbound]; + if (!sp) return rule; + let r = {}; + for (let k in rule) if (k !== 'outbound') r[k] = rule[k]; + r.action = (sp === 'dns') ? 'hijack-dns' : 'reject'; + return r; + }); + } + + // --- Migrate geoip/geosite database refs to rule_set (removed in sing-box 1.12) --- + let needed_rule_sets = {}; + + let migrate_geo_rule = function(rule) { + let has_geo = rule.geoip || rule.geosite; + if (!has_geo) return rule; + let r = {}; + let new_rs = []; + for (let k in rule) { + if (k === 'geoip') { + for (let n in rule[k]) { + let tag = 'geoip-' + n; + needed_rule_sets[tag] = 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-' + n + '.srs'; + push(new_rs, tag); + } + } else if (k === 'geosite') { + for (let n in rule[k]) { + let tag = 'geosite-' + n; + needed_rule_sets[tag] = 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-' + n + '.srs'; + push(new_rs, tag); + } + } else { + r[k] = rule[k]; + } + } + // Merge new rule_set tags with any existing ones + let existing_rs = type(r.rule_set) === 'array' ? r.rule_set : + (r.rule_set ? [r.rule_set] : []); + for (let t in new_rs) push(existing_rs, t); + r.rule_set = existing_rs; + return r; + }; + + if (cfg.route && cfg.route.rules) + cfg.route.rules = map(cfg.route.rules, migrate_geo_rule); + if (cfg.dns && cfg.dns.rules) + cfg.dns.rules = map(cfg.dns.rules, migrate_geo_rule); + + // download_detour must be DIRECT: at first start rule-sets aren't loaded, so + // routing it via a proxy deadlocks (DNS recursion -> sing-box FATAL). + let pick_dl_detour = function() { + if (cfg.outbounds) { + for (let ob in cfg.outbounds) { + if (ob && ob.type === 'direct' && ob.tag) return ob.tag; + } + } + return 'DIRECT'; + }; + + // Add rule_set download entries for each referenced geo tag + if (length(keys(needed_rule_sets)) > 0) { + if (!cfg.route) cfg.route = {}; + if (!cfg.route.rule_set) cfg.route.rule_set = []; + let existing_tags = {}; + for (let rs in cfg.route.rule_set) existing_tags[rs.tag] = true; + let dl_detour = pick_dl_detour(); + for (let tag in keys(needed_rule_sets)) { + if (!existing_tags[tag]) { + push(cfg.route.rule_set, { + tag: tag, + type: 'remote', + format: 'binary', + url: needed_rule_sets[tag], + download_detour: dl_detour + }); + } + } + } + + // --- Remove deprecated 'outbound' DNS rule items (fatal in sing-box 1.13) --- + // Replacement: set route.default_domain_resolver to the plain IP-based resolver + let dns_resolver_tag = ''; + if (cfg.dns && cfg.dns.servers) { + for (let srv in cfg.dns.servers) { + if ((srv.type === 'udp' || srv.type === 'local') && srv.server && + match(srv.server, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { + dns_resolver_tag = srv.tag; + break; + } + } + } + if (cfg.dns && cfg.dns.rules) { + let filtered = []; + for (let rule in cfg.dns.rules) { + if (!rule.outbound) push(filtered, rule); + } + cfg.dns.rules = filtered; + } + // Set route.default_domain_resolver (replaces outbound:any DNS routing) + if (dns_resolver_tag) { + if (!cfg.route) cfg.route = {}; + if (!cfg.route.default_domain_resolver) + cfg.route.default_domain_resolver = dns_resolver_tag; + } + + // --- Fix DNS servers with domain-name server field: add domain_resolver --- + // sing-box 1.12+ requires domain_resolver when server= is a hostname (not IP) + if (cfg.dns && cfg.dns.servers && dns_resolver_tag) { + let domain_types = { https: 1, h3: 1, tls: 1, tcp: 1 }; + cfg.dns.servers = map(cfg.dns.servers, function(srv) { + if (!domain_types[srv.type]) return srv; + if (!srv.server || match(srv.server, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return srv; + if (srv.domain_resolver) return srv; + let r = {}; + for (let k in srv) r[k] = srv[k]; + r.domain_resolver = dns_resolver_tag; + return r; + }); + } + + // --- Fix DNS rules referencing non-existent server tags → action: reject --- + if (cfg.dns && cfg.dns.servers && cfg.dns.rules) { + let valid_servers = {}; + for (let srv in cfg.dns.servers) valid_servers[srv.tag] = true; + cfg.dns.rules = map(cfg.dns.rules, function(rule) { + if (!rule.server || valid_servers[rule.server]) return rule; + let r = {}; + for (let k in rule) if (k !== 'server' && k !== 'action') r[k] = rule[k]; + r.action = 'reject'; + return r; + }); + } + + // --- Fix reject method: sing-box 1.14 rejects "dropped", only "default"/"drop" --- + if (cfg.dns && cfg.dns.rules) { + cfg.dns.rules = map(cfg.dns.rules, function(rule) { + if (rule.action !== 'reject' || !rule.method) return rule; + let m = rule.method; + if (m === 'dropped') m = 'drop'; + if (m !== 'default' && m !== 'drop') { + // unknown value: drop it, sing-box defaults to NXDOMAIN + let r = {}; + for (let k in rule) if (k !== 'method') r[k] = rule[k]; + return r; + } + if (m === rule.method) return rule; + let r = {}; + for (let k in rule) r[k] = rule[k]; + r.method = m; + return r; + }); + } + + // --- Migrate TUN inet4_address/inet6_address -> address array (1.12+), and + // strip legacy inbound fields removed in 1.13 (sniff / sniff_override_destination / + // sniff_timeout / domain_strategy). If any inbound had sniff on, prepend a + // { "action": "sniff" } route rule as the equivalent. --- + let had_sniff = false; + if (cfg.inbounds) { + cfg.inbounds = map(cfg.inbounds, function(ib) { + let r = {}; + let addrs = []; + for (let k in ib) { + if (k === 'inet4_address') { + if (type(ib[k]) === 'array') { for (let a in ib[k]) push(addrs, a); } + else push(addrs, ib[k]); + } else if (k === 'inet6_address') { + if (type(ib[k]) === 'array') { for (let a in ib[k]) push(addrs, a); } + else push(addrs, ib[k]); + } else if (k === 'sniff') { + if (ib[k]) had_sniff = true; + } else if (k === 'sniff_override_destination' || k === 'sniff_timeout' || + k === 'domain_strategy') { + // drop — moved to route.rules actions in 1.13 + } else { + r[k] = ib[k]; + } + } + if (length(addrs) > 0) r.address = addrs; + return r; + }); + } + if (had_sniff) { + if (!cfg.route) cfg.route = {}; + if (!cfg.route.rules) cfg.route.rules = []; + let has_sniff_rule = false; + for (let rl in cfg.route.rules) + if (rl.action === 'sniff') { has_sniff_rule = true; break; } + if (!has_sniff_rule) { + let new_rules = [ { action: 'sniff' } ]; + for (let rl in cfg.route.rules) push(new_rules, rl); + cfg.route.rules = new_rules; + } + } + + // --- sing-box 1.14 requires route.default_domain_resolver; add one if missing, + // preferring dns.final, else the first dns.servers entry with a tag. --- + if (cfg.route && !cfg.route.default_domain_resolver) { + let resolver_tag = ''; + if (cfg.dns && cfg.dns.final) resolver_tag = cfg.dns.final; + else if (cfg.dns && cfg.dns.servers) { + for (let srv in cfg.dns.servers) + if (srv.tag) { resolver_tag = srv.tag; break; } + } + if (resolver_tag) + cfg.route.default_domain_resolver = { server: resolver_tag }; + } + + // --- Remove dangling outbound refs from selectors/urltest, and drop airline + // pseudo-nodes. Two cases: (1) subconverter may reference a missing tag (e.g. + // REJECT); (2) "Traffic:.../Expire:.../quota" pseudo-nodes are real SS/Vmess + // outbounds (kept so the UI can read traffic/expiry) but don't forward — in a + // selector/urltest they win (0ms) and swallow all foreign traffic. --- + let is_pseudo_tag = function(t) { + if (!t) return false; + return match(t, /^Traffic[::]/) || + match(t, /^Expire[::]/) || + match(t, /剩余流量|剩余[::]/) || + match(t, /距离下次重置/) || + match(t, /到期(时间|日期)?[::]/) || + match(t, /官网[::]|网站[::]|套餐[::]?|客服[::]/) || + match(t, /QQ[群]?[::]/) || + match(t, /Telegram|TG群|官方群/) || + match(t, /续费|订阅地址|流量重置/); + }; + if (cfg.outbounds) { + let defined_tags = {}; + for (let ob in cfg.outbounds) defined_tags[ob.tag] = true; + cfg.outbounds = map(cfg.outbounds, function(ob) { + if ((ob.type !== 'selector' && ob.type !== 'urltest' && ob.type !== 'loadbalance') || + !ob.outbounds) return ob; + let valid = []; + for (let t in ob.outbounds) { + if (!defined_tags[t]) continue; + if (is_pseudo_tag(t)) continue; + push(valid, t); + } + if (length(valid) === length(ob.outbounds)) return ob; + let r = {}; + for (let k in ob) r[k] = ob[k]; + r.outbounds = valid; + return r; + }); + } + + // --- Force download_detour=DIRECT on every remote rule_set --- + // subconverter often emits download_detour="auto", but the proxy isn't ready at + // first start -> urltest deadlock ("fetch rule-set: deadline exceeded") -> FATAL. + // The srs URLs already use a CN-reachable mirror, so DIRECT is always correct. + if (cfg.route && cfg.route.rule_set) { + let dl_detour = pick_dl_detour(); + cfg.route.rule_set = map(cfg.route.rule_set, function(rs) { + if (!rs || rs.type !== 'remote') return rs; + let r = {}; + for (let k in rs) r[k] = rs[k]; + r.download_detour = dl_detour; + return r; + }); + } + + return cfg; +} + + +// ---- CLI entry: ucode migrate_singbox.uc ---- +let _path = ARGV[0] || ''; +if (!_path) { print("missing path\n"); exit(1); } +let _raw = readfile(_path); +if (!_raw) { print("read failed\n"); exit(1); } +let _cfg = json(_raw); +if (!_cfg) { print("json parse failed\n"); exit(1); } +let _before = sprintf('%J', _cfg); +_cfg = migrate_singbox(_cfg); +let _after = sprintf('%J', _cfg); +if (_before === _after) { print("nochange\n"); exit(0); } +if (writefile(_path, _after) === null) { print("write failed\n"); exit(1); } +print("migrated\n"); diff --git a/clashoo/files/usr/share/clashoo/lib/normalize_singbox_config.uc b/clashoo/files/usr/share/clashoo/lib/normalize_singbox_config.uc new file mode 100644 index 000000000..cd6a1d3c4 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/lib/normalize_singbox_config.uc @@ -0,0 +1,939 @@ +#!/usr/bin/ucode + +'use strict'; + +import { readfile, writefile, access, popen } from 'fs'; + +let path = ARGV[0] || ''; +let redir_port = +(ARGV[1] || '7891'); +let tproxy_port = +(ARGV[2] || '7982'); +let mixed_port = +(ARGV[3] || '7890'); +let has_tun_device = (ARGV[4] || '1') == '1'; +if (has_tun_device && system('(ip tuntap add mode tun name cotuntest >/dev/null 2>&1 && ip link del cotuntest >/dev/null 2>&1)') != 0) + has_tun_device = false; + +// default 6666 must match fw4.sh CORE_ROUTING_MARK (0x1a0a) +let routing_mark = +(ARGV[5] || '6666'); +let dns_port = +(ARGV[6] || '1053'); +let dash_port = +(ARGV[7] || '9090'); +let dash_secret = ARGV[8] != null ? (ARGV[8] + '') : ''; + +if (!path) { + print("missing path\n"); + exit(1); +} + +let raw = readfile(path); +if (!raw) { + print("read failed\n"); + exit(1); +} + +let cfg = json(raw); +if (!cfg) { + print("json parse failed\n"); + exit(1); +} + +function s_len(s) { + return length(s || ''); +} + +function s_sub(s, start, count) { + if (count == null) + return substr(s || '', start); + return substr(s || '', start, count); +} + +function is_space(ch) { + return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'; +} + +function trim_s(s) { + s = (s == null) ? '' : (s + ''); + while (s_len(s) > 0 && is_space(s_sub(s, 0, 1))) + s = s_sub(s, 1); + while (s_len(s) > 0 && is_space(s_sub(s, s_len(s) - 1, 1))) + s = s_sub(s, 0, s_len(s) - 1); + return s; +} + +let default_interface = ''; +let _default_if_pipe = popen("ip -4 route show default 2>/dev/null | awk '{print $5; exit}'"); +if (_default_if_pipe) { + default_interface = trim_s(_default_if_pipe.read('all')); + _default_if_pipe.close(); +} + +function starts_with(s, prefix) { + return s_sub(s || '', 0, s_len(prefix)) == prefix; +} + +function find_char(s, ch) { + for (let i = 0; i < s_len(s); i++) + if (s_sub(s, i, 1) == ch) + return i; + return -1; +} + +function find_last_char(s, ch) { + for (let i = s_len(s) - 1; i >= 0; i--) + if (s_sub(s, i, 1) == ch) + return i; + return -1; +} + +function split_uci_words(line) { + let out = [], cur = '', quote = '', esc = false; + for (let i = 0; i < s_len(line); i++) { + let ch = s_sub(line, i, 1); + if (esc) { + cur += ch; + esc = false; + continue; + } + if (ch == '\\') { + esc = true; + continue; + } + if (quote) { + if (ch == quote) + quote = ''; + else + cur += ch; + continue; + } + if (ch == '"' || ch == "'") { + quote = ch; + continue; + } + if (is_space(ch)) { + if (s_len(cur) > 0) { + push(out, cur); + cur = ''; + } + continue; + } + cur += ch; + } + if (s_len(cur) > 0) + push(out, cur); + return out; +} + +function load_clashoo_uci() { + let uci_path = ARGV[9] || '/etc/config/clashoo'; + let txt = readfile(uci_path) || ''; + let sections = [], cur = null; + for (let line in split(txt, '\n')) { + line = trim_s(line); + if (!s_len(line) || starts_with(line, '#')) + continue; + let words = split_uci_words(line); + if (!length(words)) + continue; + if (words[0] == 'config') { + cur = { type: words[1] || '', name: words[2] || '', options: {}, lists: {} }; + push(sections, cur); + continue; + } + if (!cur || length(words) < 3) + continue; + if (words[0] == 'option') + cur.options[words[1]] = words[2]; + else if (words[0] == 'list') { + if (cur.lists[words[1]] == null) + cur.lists[words[1]] = []; + push(cur.lists[words[1]], words[2]); + } + } + return sections; +} + +let clashoo_uci = load_clashoo_uci(); + +function uci_config_section() { + for (let s in clashoo_uci) + if (s.type == 'clashoo' && (s.name == 'config' || s.name == '')) + return s; + return {}; +} + +let uci_cfg = uci_config_section(); + +function uci_opt(key, def) { + if (uci_cfg.options && uci_cfg.options[key] != null) + return uci_cfg.options[key]; + return def; +} + +function uci_list(key) { + if (uci_cfg.lists && uci_cfg.lists[key] != null) + return uci_cfg.lists[key]; + if (uci_cfg.options && uci_cfg.options[key] != null) + return [ uci_cfg.options[key] ]; + return []; +} + +function uci_sections(type_name) { + let out = []; + for (let s in clashoo_uci) + if (s.type == type_name) + push(out, s); + return out; +} + +function opt_bool(v, def) { + if (v == null || v == '') + return def; + return v === true || v == '1' || v == 'true' || v == 'yes' || v == 'on'; +} + +function normalize_dns_uri(address, protocol, port) { + address = trim_s(address || ''); + protocol = trim_s(protocol || ''); + port = trim_s(port || ''); + if (!s_len(address)) + return ''; + if (find_char(address, ':') >= 0 && find_char(address, '/') >= 0) + return address; + if (protocol == 'none') + protocol = ''; + if (protocol == 'dot') + protocol = 'tls://'; + else if (protocol == 'doh') + protocol = 'https://'; + else if (protocol == 'doq') + protocol = 'quic://'; + if (s_len(protocol) && !starts_with(protocol, 'udp://') && !starts_with(protocol, 'tcp://') && + !starts_with(protocol, 'tls://') && !starts_with(protocol, 'https://') && !starts_with(protocol, 'quic://')) + protocol += '://'; + return protocol + address + (s_len(port) ? ':' + port : ''); +} + +function direct_outbound_tag() { + for (let ob in (cfg.outbounds || [])) + if (ob && ob.type == 'direct' && s_len(ob.tag || '')) + return ob.tag; + return 'DIRECT'; +} + +function dns_server_obj(uri, tag, fallback_type) { + uri = normalize_dns_uri(uri || '', '', ''); + if (!s_len(uri)) + return null; + let scheme = fallback_type || 'udp'; + let rest = uri; + let p = -1; + for (let i = 0; i < s_len(uri) - 2; i++) { + if (s_sub(uri, i, 3) == '://') { + p = i; + break; + } + } + if (p >= 0) { + scheme = s_sub(uri, 0, p); + rest = s_sub(uri, p + 3); + } + if (scheme == 'dot') + scheme = 'tls'; + else if (scheme == 'doh') + scheme = 'https'; + else if (scheme == 'doq') + scheme = 'quic'; + + let path = ''; + let slash = find_char(rest, '/'); + if (slash >= 0) { + path = s_sub(rest, slash); + rest = s_sub(rest, 0, slash); + } + + let server = rest, server_port = null; + let colon = find_last_char(rest, ':'); + if (colon > 0 && find_char(rest, ']') < 0) { + server = s_sub(rest, 0, colon); + let n = +(s_sub(rest, colon + 1)); + if (n === n) + server_port = n; + } + + let obj = { type: scheme, tag: tag, server: server }; + if (server_port != null) + obj.server_port = server_port; + if (path && (scheme == 'https' || scheme == 'h3')) + obj.path = path; + if ((scheme == 'https' || scheme == 'tls' || scheme == 'quic') && tag != 'dns_resolver') + obj.domain_resolver = 'dns_resolver'; + /* sing-box 1.12+ DNS servers detour to the proxy by default, so a server + * resolving its own domain via dns_resolver loops -> "deadline exceeded". + * Force direct/resolver servers to DIRECT; only dns_proxy may use the proxy. */ + if (tag == 'dns_resolver' || tag == 'dns_direct' || tag == 'dns_foreign') + obj.detour = direct_outbound_tag(); + return obj; +} + +function dns_servers_by_role(role) { + let out = []; + for (let s in uci_sections('dnsservers')) { + if (!opt_bool(s.options.enabled, true)) + continue; + if ((s.options.ser_type || 'nameserver') != role) + continue; + let uri = normalize_dns_uri(s.options.ser_address || '', s.options.protocol || '', s.options.ser_port || ''); + if (s_len(uri)) + push(out, uri); + } + return out; +} + +function first_or(arr, fallback) { + return length(arr) ? arr[0] : fallback; +} + +function local_rule_set_path(tag) { + if (!s_len(tag)) + return ''; + let path = '/usr/share/clashoo/ruleset/' + tag + '.srs'; + if (access(path, 'r')) + return path; + if (tag == 'geolocation-cn' || tag == 'cn') { + path = '/usr/share/clashoo/ruleset/geosite-cn.srs'; + if (access(path, 'r')) + return path; + } + return ''; +} + +function keep_remote_rule_set(rs) { + return false; +} + +function normalize_rule_set_url(url) { + url = url || ''; + url = replace(url, /^https:\/\/gh-proxy\.com\//, ''); + let m = match(url, /^https:\/\/raw\.githubusercontent\.com\/([^\/]+)\/([^\/]+)\/([^\/]+)\/(.+)$/); + if (m) + return 'https://cdn.jsdelivr.net/gh/' + m[1] + '/' + m[2] + '@' + m[3] + '/' + m[4]; + m = match(url, /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)$/); + if (m) + return 'https://cdn.jsdelivr.net/gh/' + m[1] + '/' + m[2] + '@' + m[3] + '/' + m[4]; + return url; +} + +function has_route_rule_set(tag) { + for (let rs in (cfg.route || {}).rule_set || []) + if (rs && rs.tag == tag) + return true; + return false; +} + +function add_remote_rule_set(tag, url) { + cfg.route = cfg.route || {}; + cfg.route.rule_set = cfg.route.rule_set || []; + if (has_route_rule_set(tag)) + return; + push(cfg.route.rule_set, { + tag: tag, + type: 'remote', + format: 'binary', + url: url + }); +} + +function array_has_value(arr, val) { + if (type(arr) != 'array') + return false; + for (let item in arr) + if (item == val) + return true; + return false; +} + +let dnsmasq_uid = null; +let _dnsmasq_uid_pipe = popen("id -u dnsmasq 2>/dev/null"); +if (_dnsmasq_uid_pipe) { + let value = trim_s(_dnsmasq_uid_pipe.read('all')); + _dnsmasq_uid_pipe.close(); + if (match(value, /^[0-9]+$/)) + dnsmasq_uid = +value; +} + +function ensure_tun_dnsmasq_exclude(ib) { + if (dnsmasq_uid == null) + return; + let excludes = ib.exclude_uid; + if (type(excludes) != 'array') + excludes = excludes != null ? [ excludes ] : []; + if (!array_has_value(excludes, dnsmasq_uid)) + push(excludes, dnsmasq_uid); + ib.exclude_uid = excludes; +} + +function ensure_tun_cn_exclude(ib) { + let excludes = ib.route_exclude_address_set; + if (type(excludes) != 'array') + excludes = excludes ? [ excludes ] : []; + if (!array_has_value(excludes, 'cn-ip')) + push(excludes, 'cn-ip'); + ib.route_exclude_address_set = excludes; +} + +function prune_tun_cn_exclude_if_missing() { + if (has_route_rule_set('cn-ip')) + return; + for (let ib in cfg.inbounds || []) { + if (!ib || ib.type != 'tun') + continue; + let excludes = []; + for (let item in ib.route_exclude_address_set || []) + if (item != 'cn-ip') + push(excludes, item); + if (length(excludes)) + ib.route_exclude_address_set = excludes; + else + delete ib.route_exclude_address_set; + } +} + +function matcher_rule(matcher, server_tag) { + let r = { server: server_tag }; + if (starts_with(matcher, 'geosite:')) + r.rule_set = s_sub(matcher, 8); + else if (starts_with(matcher, 'rule_set:')) + r.rule_set = s_sub(matcher, 9); + else if (starts_with(matcher, 'domain:')) + r.domain = s_sub(matcher, 7); + else if (starts_with(matcher, 'domain-suffix:')) + r.domain_suffix = s_sub(matcher, 14); + else + r.domain_suffix = matcher; + return r; +} + +function apply_dns_from_uci() { + cfg.dns = cfg.dns || {}; + /* strip mihomo-style DNS fields (enable/ipv6/listen/fake-ip-filter/nameserver...) + * that leak in from YAML conversion; sing-box rejects them (Fatal). */ + let _mihomo_dns_fields = ['enable', 'ipv6', 'listen', 'fake-ip-filter', 'fake-ip-range', + 'enhanced-mode', 'nameserver', 'fallback', 'fallback-filter', + 'use-hosts', 'default-nameserver', 'proxy-server-nameserver', + 'direct-nameserver', 'nameserver-policy']; + for (let _i = 0; _i < length(_mihomo_dns_fields); _i++) + delete cfg.dns[_mihomo_dns_fields[_i]]; + /* strip mihomo-style experimental fields */ + let _mihomo_exp = ['sniff-tls-sni', 'sniff', 'sniffer']; + for (let _me = 0; _me < length(_mihomo_exp); _me++) + if (cfg.experimental && cfg.experimental[_mihomo_exp[_me]] != null) + delete cfg.experimental[_mihomo_exp[_me]]; + /* strip non-sing-box root-level fields */ + let _mihomo_root = ['clash-for-android', 'cfw-bypass', 'sniffer', 'profile', + 'geodata-mode', 'geodata-loader', 'geox-url', 'geo-auto-update', + 'geo-update-interval', 'tun', 'ipv6', 'interface-name', + 'port', 'socks-port', 'mixed-port', 'redir-port', 'tproxy-port', 'mode', 'allow-lan', 'log-level', 'external-controller', 'secret', 'bind-address', 'routing-mark', 'find-process-mode', 'tcp-concurrent', 'unified-delay', + 'keep-alive-interval', 'keep-alive-idle', 'disable-keep-alive']; + /* strip subconverter note fields and non-standard keys */ + let _subconv_fields = ['_note', '_sub_url', 'hosts', 'script', 'enable', 'fake-ip-filter', 'fake-ip-range']; + for (let _sf = 0; _sf < length(_subconv_fields); _sf++) + if (cfg[_subconv_fields[_sf]] != null) + delete cfg[_subconv_fields[_sf]]; + for (let _mr = 0; _mr < length(_mihomo_root); _mr++) + if (cfg[_mihomo_root[_mr]] != null) + delete cfg[_mihomo_root[_mr]]; + let bootstrap = uci_list('default_nameserver'); + if (!length(bootstrap)) + bootstrap = uci_list('defaul_nameserver'); + let resolver_uri = first_or(bootstrap, '223.5.5.5'); + let direct_uri = first_or(dns_servers_by_role('direct-nameserver'), first_or(dns_servers_by_role('proxy-server-nameserver'), 'https://doh.pub/dns-query')); + let proxy_uri = first_or(dns_servers_by_role('nameserver'), first_or(dns_servers_by_role('fallback'), 'https://1.1.1.1/dns-query')); + + let servers = []; + push(servers, dns_server_obj(resolver_uri, 'dns_resolver', 'udp')); + push(servers, dns_server_obj(direct_uri, 'dns_direct', 'udp')); + push(servers, dns_server_obj(proxy_uri, 'dns_proxy', 'tls')); + + let enhanced = uci_opt('enhanced_mode', 'fake-ip'); + if (enhanced == 'fake-ip') { + let fake_range = uci_opt('fake_ip_range', '198.18.0.1/16'); + push(servers, { + type: 'fakeip', + tag: 'dns_fakeip', + inet4_range: fake_range || '198.18.0.1/16', + inet6_range: 'fc00::/18' + }); + } + + let rules = []; + push(servers, { type: 'udp', tag: 'dns_foreign', server: '1.1.1.1' }); + if (enhanced == 'fake-ip') { + push(rules, { + rule_set: 'geolocation-!cn', + server: 'dns_fakeip' + }); + } + + let clean_servers = []; + for (let s in servers) + if (s) + push(clean_servers, s); + cfg.dns.servers = clean_servers; + cfg.dns.rules = rules; + + /* Keep bootstrap direct; proxy unmatched queries in strict mode. */ + if (opt_bool(uci_opt('dns_leak_protect', '0'), false)) { + cfg.dns.final = 'dns_proxy'; + if (!opt_bool(uci_opt('ipv6_proxy', '0'), false)) + unshift(cfg.dns.rules, { query_type: ['AAAA'], action: 'reject', method: 'drop' }); + } else + cfg.dns.final = 'dns_direct'; + + let ecs = trim_s(uci_opt('dns_ecs', '')); + if (s_len(ecs)) + cfg.dns.client_subnet = ecs; + else + delete cfg.dns.client_subnet; + + if (opt_bool(uci_opt('singbox_independent_cache', '0'), false)) + cfg.dns.independent_cache = true; + else + delete cfg.dns.independent_cache; + + /* sing-box 1.14 requires this (else Fatal); dns_resolver is DIRECT so first-start node lookups use direct DNS */ + cfg.route.default_domain_resolver = 'dns_resolver'; + } + +let inbounds = cfg.inbounds || []; +let normalized = []; +let has_redirect = false; +let has_tproxy = false; +let has_mixed = false; +let has_tun = false; +let has_dns_in = false; +let wants_tun = has_tun_device && (uci_opt('tcp_mode', '') == 'tun' || uci_opt('udp_mode', '') == 'tun'); +let tun_stack = uci_opt('stack', 'mixed') || 'mixed'; + +if (wants_tun) { + add_remote_rule_set('cn-ip', + 'https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/cn.srs'); +} + +for (let ib in inbounds) { + if (!ib) + continue; + + if (ib.type == 'tun' || ib.tag == 'tun-in') { + /* Keep tun inbound only when the selected proxy mode actually uses TUN. */ + if (wants_tun && !has_tun) { + ib.type = 'tun'; + ib.tag = ib.tag || 'tun-in'; + if (!ib.address) + ib.address = [ '172.19.0.1/30', 'fdfe:dcba:9876::1/126' ]; + ib.auto_route = true; + ib.auto_redirect = true; + ib.auto_redirect_input_mark = '0x2023'; + ib.auto_redirect_output_mark = '0x2024'; + ib.strict_route = true; + ib.stack = tun_stack; + ensure_tun_dnsmasq_exclude(ib); + ensure_tun_cn_exclude(ib); + push(normalized, ib); + has_tun = true; + } + continue; + } + + if (ib.tag == 'redirect-in' || ib.type == 'redirect') { + if (has_redirect) + continue; + ib.type = 'redirect'; + ib.tag = 'redirect-in'; + ib.listen = '0.0.0.0'; + ib.listen_port = redir_port; + has_redirect = true; + push(normalized, ib); + continue; + } + + if (ib.tag == 'tproxy-in' || ib.type == 'tproxy') { + if (has_tproxy) + continue; + ib.type = 'tproxy'; + ib.tag = 'tproxy-in'; + ib.listen = '0.0.0.0'; + ib.listen_port = tproxy_port; + ib.network = 'udp'; + has_tproxy = true; + push(normalized, ib); + continue; + } + + if (ib.tag == 'mixed-in' || ib.type == 'mixed') { + if (has_mixed) + continue; + ib.type = 'mixed'; + ib.tag = 'mixed-in'; + ib.listen = '0.0.0.0'; + ib.listen_port = mixed_port; + has_mixed = true; + push(normalized, ib); + continue; + } + + if (ib.tag == 'dns-in') { + if (has_dns_in) + continue; + ib.type = 'direct'; + ib.tag = 'dns-in'; + ib.listen = '0.0.0.0'; + ib.listen_port = dns_port; + has_dns_in = true; + push(normalized, ib); + continue; + } + + push(normalized, ib); +} + +if (!has_redirect) { + push(normalized, { + type: 'redirect', + tag: 'redirect-in', + listen: '0.0.0.0', + listen_port: redir_port + }); +} + +if (!has_mixed) { + push(normalized, { + type: 'mixed', + tag: 'mixed-in', + listen: '0.0.0.0', + listen_port: mixed_port + }); +} + +if (wants_tun && !has_tun) { + let tun_ib = { + type: 'tun', + tag: 'tun-in', + address: [ '172.19.0.1/30', 'fdfe:dcba:9876::1/126' ], + auto_route: true, + auto_redirect: true, + auto_redirect_input_mark: '0x2023', + auto_redirect_output_mark: '0x2024', + strict_route: true, + stack: tun_stack + }; + ensure_tun_dnsmasq_exclude(tun_ib); + ensure_tun_cn_exclude(tun_ib); + push(normalized, tun_ib); +} + +if (!has_tproxy) { + push(normalized, { + type: 'tproxy', + tag: 'tproxy-in', + listen: '0.0.0.0', + listen_port: tproxy_port, + network: 'udp' + }); +} + +if (!has_dns_in) { + push(normalized, { + type: 'direct', + tag: 'dns-in', + listen: '0.0.0.0', + listen_port: dns_port + }); +} + +cfg.inbounds = normalized; + +for (let ob in (cfg.outbounds || [])) { + if (!ob || type(ob) != 'object') + continue; + + let t = ob.type || ''; + if (t == 'selector' || t == 'urltest' || t == 'fallback' || t == 'load_balance' || t == 'dns' || t == 'block') + continue; + + /* Clash SS obfs is plugin=obfs + plugin-opts object; sing-box wants + * plugin=obfs-local + "obfs=http;obfs-host=..." string. */ + if (ob.plugin == 'obfs') + ob.plugin = 'obfs-local'; + if (ob.plugin_opts != null && type(ob.plugin_opts) == 'object') { + let _parts = []; + let _map = { mode: 'obfs', host: 'obfs-host', uri: 'obfs-uri' }; + for (let _k in ob.plugin_opts) { + let _v = ob.plugin_opts[_k]; + if (type(_v) == 'object' || type(_v) == 'array') continue; + push(_parts, (_map[_k] || _k) + '=' + _v); + } + ob.plugin_opts = join(';', _parts); + } + if (ob.plugin != null && ob.plugin != 'obfs-local' && ob.plugin != 'v2ray-plugin' && ob.plugin != 'shadow-tls') { + delete ob.plugin; + delete ob.plugin_opts; + } + + /* Force ipv4_only on DIRECT outbound: most devices are IPv4-only egress, + * a CN domain resolving AAAA stalls on "network is unreachable". */ + if (ob.type == 'direct' && ob.tag == 'DIRECT') + ob.domain_resolver = { server: 'dns_resolver', strategy: 'ipv4_only' }; + + if (wants_tun) { + delete ob.routing_mark; + if (ob.type == 'direct' && ob.tag == 'DIRECT' && default_interface) + ob.bind_interface = ob.bind_interface || default_interface; + } else if (ob.routing_mark == null) + ob.routing_mark = routing_mark; + } + +/* Drop airline pseudo-nodes (Traffic:/Expire:/quota/官网/QQ/etc.) from selector/urltest lists. + * They're real SS/Vmess (kept so the UI reads traffic/expiry) but don't forward; in a + * selector/urltest they win and swallow foreign traffic ("CN ok, foreign out"). */ +let _is_pseudo_tag = function(t) { + if (!t) return false; + return match(t, /^Traffic[::]/) || match(t, /^Expire[::]/) || + match(t, /剩余流量|剩余[::]/) || match(t, /距离下次重置/) || + match(t, /到期(时间|日期)?[::]/) || + match(t, /官网[::]|网站[::]|套餐[::]?|客服[::]/) || + match(t, /QQ[群]?[::]/) || match(t, /Telegram|TG群|官方群/) || + match(t, /续费|订阅地址|流量重置/); +}; +for (let ob in (cfg.outbounds || [])) { + if (!ob || type(ob) != 'object') continue; + let t = ob.type || ''; + if (t != 'selector' && t != 'urltest' && t != 'fallback' && t != 'load_balance') continue; + if (type(ob.outbounds) != 'array') continue; + let cleaned = []; + for (let tag in ob.outbounds) if (!_is_pseudo_tag(tag)) push(cleaned, tag); + ob.outbounds = cleaned; +} + +cfg.route = cfg.route || {}; +cfg.route.rules = cfg.route.rules || []; +let has_dns_hijack = false; +for (let rule in cfg.route.rules) { + if (!rule || type(rule) != 'object') + continue; + if (rule.inbound == 'dns-in' && rule.action == 'hijack-dns') { + has_dns_hijack = true; + break; + } +} +if (!has_dns_hijack) { + unshift(cfg.route.rules, { + inbound: 'dns-in', + action: 'hijack-dns' + }); +} +cfg.route.auto_detect_interface = true; +apply_dns_from_uci(); + +/* DNS leak protection: reject DoT/DoQ (853) to force DNS through the core; after hijack-dns so the dns inbound survives */ +if (opt_bool(uci_opt('dns_leak_protect', '0'), false)) { + let _has_853 = false; + for (let _r in cfg.route.rules) { + if (_r && type(_r) == 'object' && _r.action == 'reject' && _r.port) { + if (type(_r.port) == 'array') { + for (let _p in _r.port) if (_p == 853) { _has_853 = true; break; } + } else if (_r.port == 853) { + _has_853 = true; + } + } + if (_has_853) break; + } + if (!_has_853) { + let _new_rules = []; + let _inserted = false; + for (let _ri = 0; _ri < length(cfg.route.rules); _ri++) { + push(_new_rules, cfg.route.rules[_ri]); + if (!_inserted && cfg.route.rules[_ri] && cfg.route.rules[_ri].action == 'hijack-dns') { + push(_new_rules, { port: [853], action: 'reject' }); + _inserted = true; + } + } + if (!_inserted) unshift(_new_rules, { port: [853], action: 'reject' }); + cfg.route.rules = _new_rules; + } +} + +/* ===== Custom routing rules (UCI: config addtype) -> route.rules, mirrors mihomo iprules.sh ===== + * User "domain/IP -> direct/proxy" rules from LuCI, inserted after the DNS funnel + * (hijack-dns / 853-reject) and before other rules, so overrides win without bypassing it. */ +let _proxy_outbound_tag = function() { + for (let ob in (cfg.outbounds || [])) + if (ob && ob.tag == '🚀 节点选择' && (ob.type == 'selector' || ob.type == 'urltest')) + return ob.tag; + for (let ob in (cfg.outbounds || [])) + if (ob && ob.type == 'selector' && s_len(ob.tag || '') && !_is_pseudo_tag(ob.tag)) + return ob.tag; + return direct_outbound_tag(); +}; +let _load_addtype_rules = function() { + let out = []; + let dtag = direct_outbound_tag(); + let ptag = null; + for (let s in uci_sections('addtype')) { + let o = s.options || {}; + let addr = trim_s(o.ipaaddr || ''); + let rtype = trim_s(o.type || ''); + let pg = trim_s(o.pgroup || ''); + if (!s_len(addr) || !s_len(rtype) || !s_len(pg)) + continue; + let r = {}; + if (rtype == 'DOMAIN') + r.domain = addr; + else if (rtype == 'DOMAIN-SUFFIX') + r.domain_suffix = addr; + else if (rtype == 'DOMAIN-KEYWORD') + r.domain_keyword = addr; + else if (rtype == 'IP-CIDR' || rtype == 'IP-CIDR6') + r.ip_cidr = addr; + else + continue; + if (pg == 'DIRECT') + r.outbound = dtag; + else { + if (ptag == null) + ptag = _proxy_outbound_tag(); + r.outbound = ptag; + } + push(out, r); + } + return out; +}; +let _addtype_rules = _load_addtype_rules(); +if (length(_addtype_rules)) { + let _nr = []; + let _ins = false; + for (let _i = 0; _i < length(cfg.route.rules); _i++) { + let _ru = cfg.route.rules[_i]; + let _dns_front = _ru && type(_ru) == 'object' && + (_ru.action == 'hijack-dns' || (_ru.action == 'reject' && _ru.port)); + if (!_ins && !_dns_front) { + for (let _cr in _addtype_rules) + push(_nr, _cr); + _ins = true; + } + push(_nr, _ru); + } + if (!_ins) + for (let _cr in _addtype_rules) + push(_nr, _cr); + cfg.route.rules = _nr; +} + +if (uci_opt('enhanced_mode', 'fake-ip') == 'fake-ip') { + add_remote_rule_set('geolocation-!cn', + 'https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-!cn.srs'); +} + +/* Local .srs -> local; remaining remote rule_sets must keep/get a download_detour. + * CN first-start deadlock: no srs -> fetch rules -> via auto-select -> urltest dials + * the proxy -> resolves its domain -> pulled into an unloaded rule_set by DNS rules -> deadline. + * The srs URLs use a CN-reachable mirror, so download_detour + * must be DIRECT (real direct-outbound tag if any, else 'DIRECT'). */ +let _pick_dl_detour = function() { + if (cfg.outbounds) { + for (let ob in cfg.outbounds) { + if (ob && ob.type == 'direct' && ob.tag) return ob.tag; + } + } + return 'DIRECT'; +}; +let _dl_detour = _pick_dl_detour(); +for (let rs in (cfg.route || {}).rule_set || []) { + if (!rs) continue; + if (rs.type != 'remote') { delete rs.download_detour; continue; } + let path = local_rule_set_path(rs.tag || ''); + if (rs.tag && access(path, 'r')) { + delete rs.url; delete rs.download_detour; rs.type = 'local'; rs.path = path; + continue; + } + if (keep_remote_rule_set(rs) && rs.url) { + rs.url = normalize_rule_set_url(rs.url); + rs.download_detour = _dl_detour; + continue; + } + /* No local cache -> skip this remote rule_set so a failed download doesn't block start. + * Rule-sets are fetched to /usr/share/clashoo/ruleset/ once proxied. */ + continue; + +} +/* filter out remote rule_sets with no local cache */ +let _clean_rs = []; +for (let _rsi = 0; _rsi < length(cfg.route.rule_set); _rsi++) { + let _rs = cfg.route.rule_set[_rsi]; + if (!_rs) continue; + if (_rs.type == 'remote' && _rs.url != null) { + let _local_path = local_rule_set_path(_rs.tag || ''); + if (!_rs.tag || !access(_local_path, 'r')) + if (keep_remote_rule_set(_rs) && _rs.url) { + _rs.url = normalize_rule_set_url(_rs.url); + _rs.download_detour = _dl_detour; + push(_clean_rs, _rs); + continue; + } else { + continue; /* no local cache -> drop from final list */ + } + /* has local cache -> convert to local */ + delete _rs.url; delete _rs.download_detour; _rs.type = 'local'; _rs.path = _local_path; + } + push(_clean_rs, _rs); +} +cfg.route.rule_set = _clean_rs; +prune_tun_cn_exclude_if_missing(); + + /* drop DNS/route rules referencing a removed rule_set, else sing-box FATAL: rule-set not found */ + let _existing_tags = {}; + for (let _rs in cfg.route.rule_set || []) if (_rs && _rs.tag) _existing_tags[_rs.tag] = true; + let _rule_has_ref = function(_r) { + if (!_r || type(_r) != 'object') return false; + if (_r.rule_set) { + if (type(_r.rule_set) == 'array') { for (let _t in _r.rule_set) if (!_existing_tags[_t]) return false; } + else if (!_existing_tags[_r.rule_set]) return false; + } + return true; + }; + /* clean dns.rules */ + if (cfg.dns && type(cfg.dns.rules) == 'array') { + let _clean_dns_rules = []; + for (let _dr in cfg.dns.rules) if (_rule_has_ref(_dr)) push(_clean_dns_rules, _dr); + cfg.dns.rules = _clean_dns_rules; + } + /* clean route.rules */ + if (cfg.route && type(cfg.route.rules) == 'array') { + let _clean_rt_rules = []; + for (let _rr in cfg.route.rules) if (_rule_has_ref(_rr)) push(_clean_rt_rules, _rr); + cfg.route.rules = _clean_rt_rules; + } + cfg.experimental = cfg.experimental || {}; +cfg.experimental.clash_api = cfg.experimental.clash_api || {}; +cfg.experimental.clash_api.external_controller = '0.0.0.0:' + dash_port; +cfg.experimental.clash_api.external_ui = '/etc/clashoo/dashboard'; +cfg.experimental.clash_api.secret = dash_secret; + +cfg.experimental.cache_file = { + enabled: true, + store_fakeip: true +}; + +/* OpenWrt already owns system time sync. Keep sing-box NTP disabled by + * default to avoid noisy IPv6 UDP/123 failures on IPv4-only routers. */ +cfg.ntp = cfg.ntp || {}; +cfg.ntp.enabled = false; + +cfg.log = cfg.log || {}; +cfg.log.output = '/var/log/clashoo/core.log'; +if (!cfg.log.level) + cfg.log.level = 'info'; + +if (writefile(path, sprintf('%J', cfg)) === null) { + print("write failed\n"); + exit(1); +} + +print("normalized\n"); diff --git a/clashoo/files/usr/share/clashoo/lib/templates/default.json b/clashoo/files/usr/share/clashoo/lib/templates/default.json new file mode 100644 index 000000000..84f1ee22e --- /dev/null +++ b/clashoo/files/usr/share/clashoo/lib/templates/default.json @@ -0,0 +1,138 @@ +{ + "log": { + "level": "info", + "timestamp": true + }, + "dns": { + "servers": [ + { "type": "tcp", "tag": "dns_proxy", "server": "1.1.1.1", "domain_resolver": "dns_resolver" }, + { "type": "https", "tag": "dns_direct", "server": "dns.alidns.com", "domain_resolver": "dns_resolver" }, + { "type": "udp", "tag": "dns_resolver", "server": "223.5.5.5" }, + { "type": "fakeip", "tag": "dns_fakeip", "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" } + ], + "rules": [ + { "rule_set": "geolocation-!cn", "query_type": ["A", "AAAA", "CNAME"], "server": "dns_fakeip" }, + { "query_type": ["A", "AAAA", "CNAME"], "invert": true, "action": "reject" } + ], + "final": "dns_direct" + }, + "ntp": { + "enabled": false, + "server": "time.apple.com", + "server_port": 123, + "interval": "30m" + }, + "inbounds": [ + { "type": "mixed", "tag": "mixed-in", "listen": "0.0.0.0", "listen_port": 2080 }, + { "type": "tun", "tag": "tun-in", "address": ["172.19.0.1/30"], "auto_route": true, "auto_redirect": true, "strict_route": true, "stack": "mixed", "route_exclude_address_set": ["cn-ip"] } + ], + "outbounds": [ + { + "type": "selector", "tag": "🚀 节点选择", + "outbounds": ["🌍 全部节点","♻️ 自动选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","DIRECT"], + "default": "🌍 全部节点" + }, + { "type": "selector", "tag": "▶️ YouTube", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "🔍 Google", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "🔮 OpenAI", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "🧠 Claude", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "🤖 Gemini", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "🐦 Twitter", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "📦 GitHub", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "Ⓜ️ Microsoft", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "🎵 TikTok", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "📺 Netflix", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "🎬 流媒体", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "💳 PayPal", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "📲 电报消息", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "🍏 苹果服务", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] }, + { "type": "selector", "tag": "🎯 全球直连", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] }, + { "type": "selector", "tag": "🏠 私有网络", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] }, + { "type": "selector", "tag": "🌐 非中国", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + { "type": "selector", "tag": "🐟 漏网之鱼", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] }, + + { "type": "selector", "tag": "🇭🇰 香港节点", "outbounds": ["__NODES_HK__"] }, + { "type": "selector", "tag": "🇯🇵 日本节点", "outbounds": ["__NODES_JP__"] }, + { "type": "selector", "tag": "🇺🇲 美国节点", "outbounds": ["__NODES_US__"] }, + { "type": "selector", "tag": "🇸🇬 新加坡节点","outbounds": ["__NODES_SG__"] }, + { "type": "selector", "tag": "🇺🇳 集合节点", "outbounds": ["__NODES_OTHER__"] }, + { "type": "selector", "tag": "🌍 全部节点", "outbounds": ["__NODES__"] }, + + { "type": "urltest", "tag": "♻️ 香港自动", "outbounds": ["__NODES_HK__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true }, + { "type": "urltest", "tag": "♻️ 日本自动", "outbounds": ["__NODES_JP__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true }, + { "type": "urltest", "tag": "♻️ 美国自动", "outbounds": ["__NODES_US__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true }, + { "type": "urltest", "tag": "♻️ 新加坡自动", "outbounds": ["__NODES_SG__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true }, + { "type": "urltest", "tag": "♻️ 集合自动", "outbounds": ["__NODES_OTHER__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true }, + { "type": "urltest", "tag": "♻️ 自动选择", "outbounds": ["__NODES__"], "url": "https://www.gstatic.com/generate_204", "interval": "10m", "tolerance": 50, "interrupt_exist_connections": true }, + + { "type": "direct", "tag": "DIRECT" } + ], + "route": { + "default_domain_resolver": "dns_resolver", + "rule_set": [ + { "tag": "openai", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/openai.srs", "download_detour": "DIRECT" }, + { "tag": "anthropic", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/anthropic.srs", "download_detour": "DIRECT" }, + { "tag": "google-gemini", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/google-gemini.srs", "download_detour": "DIRECT" }, + { "tag": "youtube", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/youtube.srs", "download_detour": "DIRECT" }, + { "tag": "google", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/google.srs", "download_detour": "DIRECT" }, + { "tag": "twitter", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/twitter.srs", "download_detour": "DIRECT" }, + { "tag": "github", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/github.srs", "download_detour": "DIRECT" }, + { "tag": "gitlab", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/gitlab.srs", "download_detour": "DIRECT" }, + { "tag": "microsoft", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/microsoft.srs", "download_detour": "DIRECT" }, + { "tag": "tiktok", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/tiktok.srs", "download_detour": "DIRECT" }, + { "tag": "netflix", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/netflix.srs", "download_detour": "DIRECT" }, + { "tag": "hulu", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/hulu.srs", "download_detour": "DIRECT" }, + { "tag": "disney", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/disney.srs", "download_detour": "DIRECT" }, + { "tag": "hbo", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/hbo.srs", "download_detour": "DIRECT" }, + { "tag": "amazon", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/amazon.srs", "download_detour": "DIRECT" }, + { "tag": "bahamut", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/bahamut.srs", "download_detour": "DIRECT" }, + { "tag": "paypal", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/paypal.srs", "download_detour": "DIRECT" }, + { "tag": "apple", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/apple.srs", "download_detour": "DIRECT" }, + { "tag": "geolocation-cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-cn.srs", "download_detour": "DIRECT" }, + { "tag": "cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/cn.srs", "download_detour": "DIRECT" }, + { "tag": "geolocation-!cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-!cn.srs", "download_detour": "DIRECT" }, + { "tag": "google-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/google.srs", "download_detour": "DIRECT" }, + { "tag": "private-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/private.srs", "download_detour": "DIRECT" }, + { "tag": "cn-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/cn.srs", "download_detour": "DIRECT" }, + { "tag": "telegram-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/telegram.srs", "download_detour": "DIRECT" } + ], + "rules": [ + { "action": "sniff" }, + { "clash_mode": "direct", "outbound": "DIRECT" }, + { "clash_mode": "global", "outbound": "♻️ 自动选择" }, + { "protocol": "dns", "action": "hijack-dns" }, + { "rule_set": "openai", "outbound": "🔮 OpenAI" }, + { "rule_set": "anthropic", "outbound": "🧠 Claude" }, + { "rule_set": "google-gemini", "outbound": "🤖 Gemini" }, + { "rule_set": "youtube", "outbound": "▶️ YouTube" }, + { "rule_set": "google", "outbound": "🔍 Google" }, + { "rule_set": "twitter", "outbound": "🐦 Twitter" }, + { "rule_set": "tiktok", "outbound": "🎵 TikTok" }, + { "rule_set": "netflix", "outbound": "📺 Netflix" }, + { "rule_set": ["hulu", "disney", "hbo", "amazon", "bahamut"], "outbound": "🎬 流媒体" }, + { "rule_set": "paypal", "outbound": "💳 PayPal" }, + { "rule_set": "microsoft", "outbound": "Ⓜ️ Microsoft" }, + { "rule_set": "apple", "outbound": "🍏 苹果服务" }, + { "rule_set": ["github", "gitlab"], "outbound": "📦 GitHub" }, + { "rule_set": "telegram-ip", "outbound": "📲 电报消息" }, + { "rule_set": "private-ip", "outbound": "🏠 私有网络" }, + { "rule_set": ["geolocation-cn", "cn"], "outbound": "🎯 全球直连" }, + { "rule_set": "cn-ip", "outbound": "🎯 全球直连" }, + { "domain_suffix": ["cn"], "outbound": "🎯 全球直连" }, + { "rule_set": "geolocation-!cn", "outbound": "🌐 非中国" }, + { "rule_set": "google-ip", "outbound": "🔍 Google" } + ], + "auto_detect_interface": true, + "final": "🐟 漏网之鱼" + }, + "experimental": { + "cache_file": { "enabled": true, "store_fakeip": true }, + "clash_api": { + "external_controller": "0.0.0.0:9090", + "external_ui": "dashboard", + "secret": "clashoo", + "external_ui_download_url": "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip", + "external_ui_download_detour": "DIRECT" + } + } +} diff --git a/clashoo/files/usr/share/clashoo/lib/yaml2singbox.uc b/clashoo/files/usr/share/clashoo/lib/yaml2singbox.uc new file mode 100644 index 000000000..37e40c766 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/lib/yaml2singbox.uc @@ -0,0 +1,468 @@ +#!/usr/bin/ucode +/* yaml2singbox.uc — convert mihomo/clash YAML proxies to sing-box outbounds + * Usage: ucode yaml2singbox.uc [template-json] [json-out] + * Supports: ss, vmess, vless, trojan, hysteria2, tuic. Others are skipped. + * "__NODES__" in the template selector outbounds is expanded to all node tags. */ + +'use strict'; + +import { readfile, writefile, popen } from 'fs'; + +const TPL_DEFAULT = '/usr/share/clashoo/lib/templates/default.json'; +const SUPPORTED = { + 'ss': 'shadowsocks', + 'shadowsocks':'shadowsocks', + 'vmess': 'vmess', + 'vless': 'vless', + 'trojan': 'trojan', + 'hysteria2': 'hysteria2', + 'hy2': 'hysteria2', + 'tuic': 'tuic' +}; + +function logerr(msg) { + warn(sprintf("[yaml2singbox] %s\n", msg)); +} + +function die(msg, code) { + logerr(msg); + exit(code || 1); +} + +/* ---------- YAML read (via yq) ---------- */ +function quote_sh(s) { + /* single-quote for shell; escape embedded quotes */ + return "'" + replace(s, "'", "'\\''") + "'"; +} + +function read_yaml_as_json(path) { + const cmd = sprintf("yq -o=json eval . %s 2>/dev/null", quote_sh(path)); + const h = popen(cmd, "r"); + if (!h) + die(sprintf("popen failed: %s", cmd)); + let buf = "", chunk; + while ((chunk = h.read(65536))) + buf += chunk; + h.close(); + if (!length(buf)) + die(sprintf("yq returned empty for %s (yaml loadable?)", path)); + const data = json(buf); + if (data === null) + die("json parse of yq output failed"); + return data; +} + +/* ---------- helpers ---------- */ +function pick(obj, ...keys) { + for (let k in keys) + if (obj[k] != null) + return obj[k]; + return null; +} + +function tobool(v) { + if (v === true || v === 'true' || v === 1 || v === '1') return true; + if (v === false || v === 'false' || v === 0 || v === '0') return false; + return null; +} + +function toint(v) { + if (v == null) return null; + let n = +v; + return (n === n) ? n : null; /* NaN check */ +} + +function strip_null(obj) { + if (type(obj) !== 'object') return obj; + const out = {}; + for (let k in obj) { + const v = obj[k]; + if (v == null) continue; + if (type(v) === 'object') { + const sv = strip_null(v); + if (length(sv) > 0) out[k] = sv; + } else if (type(v) === 'array') { + const arr = map(v, (x) => (type(x) === 'object') ? strip_null(x) : x); + if (length(arr) > 0) out[k] = arr; + } else { + out[k] = v; + } + } + return out; +} + +/* ---------- TLS block assembly ---------- */ +function build_tls(p) { + const enabled = tobool(p.tls) || (p.sni && length(p.sni) > 0) || (p.servername && length(p.servername) > 0); + if (!enabled) return null; + const tls = { + enabled: true, + server_name: pick(p, 'sni', 'servername'), + insecure: tobool(p['skip-cert-verify']) === true ? true : null, + alpn: p.alpn + }; + const reality = p['reality-opts']; + if (reality && type(reality) === 'object') { + tls.reality = { + enabled: true, + public_key: reality['public-key'], + short_id: reality['short-id'] + }; + } + const client_fp = p['client-fingerprint']; + if (client_fp) + tls.utls = { enabled: true, fingerprint: client_fp }; + return strip_null(tls); +} + +/* ---------- transport (ws / grpc / http) ---------- */ +function build_transport(p) { + const net = p.network; + if (!net || net === 'tcp') return null; + if (net === 'ws') { + const opts = p['ws-opts'] || {}; + return strip_null({ + type: 'ws', + path: opts.path, + headers: opts.headers, + max_early_data: toint(opts['max-early-data']), + early_data_header_name: opts['early-data-header-name'] + }); + } + if (net === 'grpc') { + const opts = p['grpc-opts'] || {}; + return { type: 'grpc', service_name: opts['grpc-service-name'] }; + } + if (net === 'http' || net === 'h2') { + const opts = p['h2-opts'] || p['http-opts'] || {}; + return strip_null({ + type: 'http', + host: opts.host || opts.Host, + path: opts.path + }); + } + if (net === 'httpupgrade') { + const opts = p['http-upgrade-opts'] || {}; + return strip_null({ type: 'httpupgrade', path: opts.path, host: opts.host }); + } + logerr(sprintf("unknown transport network '%s' for node '%s', ignored", net, p.name)); + return null; +} + +/* ---------- per-protocol conversion ---------- */ +function convert_ss(p) { + const plugin = p.plugin, popts = p['plugin-opts'] || {}; + let plugin_opts = null; + if (plugin && type(popts) === 'object') { + /* Field mapping: Clash simple-obfs -> sing-box obfs-local + * Clash: mode=http;host=xxx → sing-box: obfs=http;obfs-host=xxx */ + const FIELD_MAP = { + 'mode': 'obfs', + 'host': 'obfs-host', + 'uri': 'obfs-uri' + }; + const parts = []; + for (let k in popts) { + const v = popts[k]; + if (type(v) === 'object') continue; + push(parts, (FIELD_MAP[k] || k) + '=' + v); + } + plugin_opts = join(';', parts); + } + /* Clash "obfs" maps to sing-box plugin "obfs-local" */ + let sb_plugin = null; + if (plugin === 'obfs') sb_plugin = 'obfs-local'; + else if (plugin === 'v2ray-plugin') sb_plugin = 'v2ray-plugin'; + else if (plugin === 'shadow-tls') sb_plugin = 'shadow-tls'; + else if (plugin) sb_plugin = plugin; + + return strip_null({ + type: 'shadowsocks', + tag: p.name, + server: p.server, + server_port: toint(p.port), + method: p.cipher, + password: p.password, + plugin: sb_plugin, + plugin_opts: plugin_opts + }); +} + +function convert_vmess(p) { + return strip_null({ + type: 'vmess', + tag: p.name, + server: p.server, + server_port: toint(p.port), + uuid: p.uuid, + alter_id: toint(p.alterId) || 0, + security: p.cipher || 'auto', + tls: build_tls(p), + transport: build_transport(p) + }); +} + +function convert_vless(p) { + return strip_null({ + type: 'vless', + tag: p.name, + server: p.server, + server_port: toint(p.port), + uuid: p.uuid, + flow: p.flow, + tls: build_tls(p), + transport: build_transport(p) + }); +} + +function convert_trojan(p) { + return strip_null({ + type: 'trojan', + tag: p.name, + server: p.server, + server_port: toint(p.port), + password: p.password, + tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server }, + transport: build_transport(p) + }); +} + +function convert_hysteria2(p) { + const obfs_pass = p['obfs-password']; + return strip_null({ + type: 'hysteria2', + tag: p.name, + server: p.server, + server_port: toint(p.port), + password: pick(p, 'password', 'auth'), + up_mbps: toint(p.up), + down_mbps: toint(p.down), + obfs: (p.obfs && obfs_pass) ? { type: p.obfs, password: obfs_pass } : null, + tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server } + }); +} + +function convert_tuic(p) { + return strip_null({ + type: 'tuic', + tag: p.name, + server: p.server, + server_port: toint(p.port), + uuid: p.uuid, + password: p.password, + congestion_control: p['congestion-controller'] || p.congestion, + udp_relay_mode: p['udp-relay-mode'], + tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server, alpn: p.alpn } + }); +} + +function convert_proxy(p) { + const sb_type = SUPPORTED[p.type]; + if (!sb_type) { + if (p.type && p.type !== 'select') + logerr(sprintf("skip unsupported type '%s' for '%s'", p.type, p.name)); + return null; + } + if (!p.server || !p.port || !p.name) { + logerr(sprintf("skip proxy missing server/port/name (type=%s)", p.type)); + return null; + } + switch (p.type) { + case 'ss': + case 'shadowsocks': return convert_ss(p); + case 'vmess': return convert_vmess(p); + case 'vless': return convert_vless(p); + case 'trojan': return convert_trojan(p); + case 'hysteria2': + case 'hy2': return convert_hysteria2(p); + case 'tuic': return convert_tuic(p); + } + return null; +} + +/* ---------- dedup (tag must be unique) ---------- */ +function dedupe_tags(nodes) { + const seen = {}; + for (let n in nodes) { + let base = n.tag, t = base, i = 2; + while (seen[t]) { + t = base + '_' + i; + i++; + } + n.tag = t; + seen[t] = true; + } + return nodes; +} + +/* Drop airline pseudo-nodes (Traffic:/Expire:/quota/官网/QQ etc.) from selectors + * and urltests. They're real outbounds (so the UI can read traffic/expiry) but + * don't forward — in a selector/urltest they win (0ms) and swallow all traffic. */ +function is_pseudo_node_tag(tag) { + if (!tag) return false; + const t = '' + tag; + const patterns = [ + /^Traffic[::]/i, + /^Expire[::]/i, + /剩余流量|剩余[::]/, + /距离下次重置/, + /到期(时间|日期)?[::]/, + /官网[::]/, + /网站[::]/, + /套餐[::]?/, + /客服[::]/, + /QQ[群]?[::]/i, + /Telegram|TG群|官方群/i, + /续费|订阅地址|流量重置/, + ]; + for (let re in patterns) if (match(t, re)) return true; + return false; +} + +/* ---------- detect region from tag -> 'HK'/'JP'/'US'/'SG'/'OTHER'/'' ---------- */ +function region_of(tag) { + if (!tag) return ''; + const t = '' + tag; + if (match(t, /港|🇭🇰|HK[^A-Za-z]|[^A-Za-z]HK|^HK$|[Hh]ong[Kk]/)) return 'HK'; + if (match(t, /日|🇯🇵|JP[^A-Za-z]|[^A-Za-z]JP|^JP$|[Jj]apan/)) return 'JP'; + if (match(t, /美|🇺🇸|US[^A-Za-z]|[^A-Za-z]US|^US$|[Uu]nited.?[Ss]tates|[Aa]merica/)) return 'US'; + if (match(t, /新加坡|🇸🇬|SG[^A-Za-z]|[^A-Za-z]SG|^SG$|[Ss]ingapore/)) return 'SG'; + if (match(t, /台湾|台|🇹🇼|TW[^A-Za-z]|[^A-Za-z]TW|^TW$|[Tt]aiwan|韩国|韩|🇰🇷|KR[^A-Za-z]|[^A-Za-z]KR|^KR$|[Kk]orea/)) return 'OTHER'; + return ''; +} + +/* ---------- expand __NODES__/__NODES_XX__ placeholders to real tags (dropping pseudo-nodes) ---------- */ +function expand_node_placeholder(outbounds, node_tags) { + const real_tags = []; + for (let t in node_tags) if (!is_pseudo_node_tag(t)) push(real_tags, t); + + /* bucket by region */ + let tags_hk = [], tags_jp = [], tags_us = [], tags_sg = [], tags_other = []; + for (let t in real_tags) { + const r = region_of(t); + if (r === 'HK') push(tags_hk, t); + else if (r === 'JP') push(tags_jp, t); + else if (r === 'US') push(tags_us, t); + else if (r === 'SG') push(tags_sg, t); + else if (r === 'OTHER') push(tags_other, t); + } + /* when a region has no nodes, fall back to all nodes (avoid empty outbounds) */ + if (!length(tags_hk)) tags_hk = real_tags; + if (!length(tags_jp)) tags_jp = real_tags; + if (!length(tags_us)) tags_us = real_tags; + if (!length(tags_sg)) tags_sg = real_tags; + if (!length(tags_other)) tags_other = real_tags; + + for (let ob in outbounds) { + if (ob.type !== 'selector' && ob.type !== 'urltest') continue; + if (!ob.outbounds || type(ob.outbounds) !== 'array') continue; + const expanded = []; + for (let item in ob.outbounds) { + let list = null; + if (item === '__NODES__') list = real_tags; + else if (item === '__NODES_HK__') list = tags_hk; + else if (item === '__NODES_JP__') list = tags_jp; + else if (item === '__NODES_US__') list = tags_us; + else if (item === '__NODES_SG__') list = tags_sg; + else if (item === '__NODES_OTHER__') list = tags_other; + if (list !== null) { for (let t in list) push(expanded, t); } + else { push(expanded, item); } + } + ob.outbounds = expanded; + } + return outbounds; +} + +/* ---------- proxy-providers ---------- */ +function resolve_providers(yaml) { + if (type(yaml['proxy-providers']) !== 'object') + return []; + const all = []; + for (let name in yaml['proxy-providers']) { + const p = yaml['proxy-providers'][name]; + if (p.type !== 'http' || !p.url) { + logerr(sprintf("provider '%s': skip (not http or no url)", name)); + continue; + } + const cmd = sprintf("wget -q -O- --timeout=20 %s 2>/dev/null | yq -o=json eval . 2>/dev/null", quote_sh(p.url)); + const h = popen(cmd, "r"); + if (!h) { logerr(sprintf("provider '%s': download failed", name)); continue; } + let buf = "", chunk; + while ((chunk = h.read(65536))) buf += chunk; + h.close(); + if (!length(buf)) { logerr(sprintf("provider '%s': empty response", name)); continue; } + const data = json(buf); + if (!data || type(data.proxies) !== 'array') { + logerr(sprintf("provider '%s': no proxies array in response", name)); + continue; + } + logerr(sprintf("provider '%s': %d proxies", name, length(data.proxies))); + for (let px in data.proxies) push(all, px); + } + return all; +} + +/* ---------- main ---------- */ +const yaml_path = ARGV[0]; +const tpl_path = ARGV[1] || TPL_DEFAULT; +const out_path = ARGV[2]; + +if (!yaml_path) + die("usage: ucode yaml2singbox.uc [template-json] [json-out]"); + +const yaml = read_yaml_as_json(yaml_path); + +let proxies = []; +if (type(yaml.proxies) === 'array') + proxies = yaml.proxies; +else + logerr("no inline proxies array, checking proxy-providers..."); + +if (type(yaml['proxy-providers']) === 'object') { + const pproxies = resolve_providers(yaml); + for (let p in pproxies) push(proxies, p); +} + +if (!length(proxies)) + die(sprintf("no proxies found in %s (inline or via providers)", yaml_path)); + +const tpl_raw = readfile(tpl_path); +if (!tpl_raw) + die(sprintf("cannot read template %s", tpl_path)); +const tpl = json(tpl_raw); +if (!tpl || type(tpl.outbounds) !== 'array') + die(sprintf("template %s has no outbounds[]", tpl_path)); + +/* convert proxies */ +const nodes = []; +let skipped = 0; +for (let p in proxies) { + const o = convert_proxy(p); + if (o) push(nodes, o); else skipped++; +} +dedupe_tags(nodes); + +if (!length(nodes)) + die(sprintf("no usable nodes converted from %d proxies (skipped=%d)", length(proxies), skipped)); + +logerr(sprintf("converted=%d skipped=%d", length(nodes), skipped)); + +/* prepend converted nodes to template outbounds */ +const final_outbounds = []; +for (let n in nodes) push(final_outbounds, n); +for (let ob in tpl.outbounds) push(final_outbounds, ob); + +/* expand __NODES__ placeholders */ +const node_tags = map(nodes, (n) => n.tag); +expand_node_placeholder(final_outbounds, node_tags); + +tpl.outbounds = final_outbounds; + +const out = sprintf("%.J\n", tpl); +if (out_path) { + if (!writefile(out_path, out)) + die(sprintf("writefile failed: %s", out_path)); + logerr(sprintf("wrote %s (%d bytes, %d nodes)", out_path, length(out), length(nodes))); +} else { + print(out); +} diff --git a/clashoo/files/usr/share/clashoo/migrate_fake_ip_filter.sh b/clashoo/files/usr/share/clashoo/migrate_fake_ip_filter.sh new file mode 100755 index 000000000..0cc4f362d --- /dev/null +++ b/clashoo/files/usr/share/clashoo/migrate_fake_ip_filter.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +set -f + +[ "$(uci -q get clashoo.config.fake_ip_filter_migrated 2>/dev/null)" = "1" ] && exit 0 + +filters="$(uci -q get clashoo.config.fake_ip_filter 2>/dev/null)" +has_legacy=0 +for filter in $filters; do + [ "$filter" = "*.lan" ] && has_legacy=1 +done + +if [ "$has_legacy" = "1" ]; then + uci -q delete clashoo.config.fake_ip_filter + added_lan=0 + added_local=0 + for filter in $filters; do + case "$filter" in + '*.lan'|'+.lan') + if [ "$added_lan" = "0" ]; then + uci -q add_list clashoo.config.fake_ip_filter='+.lan' + added_lan=1 + fi + ;; + '+.local') + if [ "$added_local" = "0" ]; then + uci -q add_list clashoo.config.fake_ip_filter='+.local' + added_local=1 + fi + ;; + *) + uci -q add_list clashoo.config.fake_ip_filter="$filter" + ;; + esac + done + if [ "$added_local" = "0" ]; then + uci -q add_list clashoo.config.fake_ip_filter='+.local' + fi +fi + +uci -q set clashoo.config.fake_ip_filter_migrated='1' +uci -q commit clashoo diff --git a/clashoo/files/usr/share/clashoo/migrate_lan_acl.sh b/clashoo/files/usr/share/clashoo/migrate_lan_acl.sh new file mode 100644 index 000000000..750e39940 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/migrate_lan_acl.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +[ "$(uci -q get clashoo.config.acl_migrated 2>/dev/null)" = "1" ] && exit 0 +if uci -q show clashoo 2>/dev/null | grep -q '=lan_acl$'; then + uci -q set clashoo.config.acl_migrated='1' + uci -q commit clashoo + exit 0 +fi + +mode="$(uci -q get clashoo.config.access_control 2>/dev/null)" + +add_group() { + local dns="$1" proxy="$2" list_key="${3:-}" section value + + section="$(uci -q add clashoo lan_acl)" || exit 1 + uci -q set clashoo."$section".enabled='1' + uci -q set clashoo."$section".dns="$dns" + uci -q set clashoo."$section".proxy="$proxy" + if [ -n "$list_key" ]; then + for value in $(uci -q get clashoo.config."$list_key" 2>/dev/null); do + uci -q add_list clashoo."$section".ip="$value" + done + fi +} + +case "$mode" in + 1) + add_group 1 1 proxy_lan_ips + add_group 0 0 + ;; + 2) + add_group 0 0 reject_lan_ips + add_group 1 1 + ;; +esac + +uci -q set clashoo.config.acl_migrated='1' +uci -q commit clashoo diff --git a/clashoo/files/usr/share/clashoo/net/access_check.sh b/clashoo/files/usr/share/clashoo/net/access_check.sh new file mode 100755 index 000000000..6356d8419 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/net/access_check.sh @@ -0,0 +1,89 @@ +#!/bin/sh +# clashoo access check +# 用法: access_check.sh [mode] +# mode = direct | proxy(默认 proxy) +# direct: 经 openwrt 主干直连,不走任何代理 +# proxy: 走 clashoo 本地代理端口,验证核心出站 + +url="$1" +mode="${2:-proxy}" +[ -n "$url" ] || exit 1 + +attempts=1 +ok=0 +sum_ms=0 +last_code=000 + +url_host() { + printf '%s' "$1" | sed -n 's#^[a-zA-Z][a-zA-Z0-9+.-]*://\([^/:?#]*\).*$#\1#p' +} + +url_port() { + _u="$1" + _p="$(printf '%s' "$_u" | sed -n 's#^[a-zA-Z][a-zA-Z0-9+.-]*://[^/:?#]*:\([0-9][0-9]*\).*$#\1#p')" + [ -n "$_p" ] && { printf '%s' "$_p"; return; } + case "$_u" in + https://*) printf '443' ;; + http://*) printf '80' ;; + *) printf '443' ;; + esac +} + +run_with_timeout() { + _secs="$1" + shift + if command -v timeout >/dev/null 2>&1; then + timeout "$_secs" "$@" 2>/dev/null + else + "$@" 2>/dev/null + fi +} + +resolve_real_ip() { + _host="$1" + [ -n "$_host" ] || return 0 + for _dns in 223.5.5.5 119.29.29.29 1.1.1.1; do + _ip="$(run_with_timeout 1 nslookup "$_host" "$_dns" | awk '/^Address: /{print $2} /^Address [0-9]+: /{print $3}' | grep -E '^[0-9]+(\.[0-9]+){3}$' | tail -n1)" + [ -n "$_ip" ] && { printf '%s' "$_ip"; return; } + done +} + +set -- -4 -L --max-time 5 --connect-timeout 3 -s -o /dev/null -w '%{http_code} %{time_total}' +if [ "$mode" = "direct" ]; then + # 强制不走任何代理,避免误读 http_proxy 环境变量 + set -- "$@" --noproxy '*' + host="$(url_host "$url")" + port="$(url_port "$url")" + real_ip="$(resolve_real_ip "$host")" + [ -n "$host" ] && [ -n "$port" ] && [ -n "$real_ip" ] && set -- "$@" --resolve "${host}:${port}:${real_ip}" +else + proxy_port="$(uci get clashoo.config.mixed_port 2>/dev/null)" + [ -z "$proxy_port" ] && proxy_port="$(uci get clashoo.config.http_port 2>/dev/null)" + [ -z "$proxy_port" ] && proxy_port=7890 + set -- "$@" -x "http://127.0.0.1:${proxy_port}" +fi +set -- "$@" "$url" + +i=1 +while [ "$i" -le "$attempts" ]; do + out="$(curl "$@" 2>/dev/null || true)" + code="$(printf '%s' "$out" | awk '{print $1}')" + time_s="$(printf '%s' "$out" | awk '{print $2}')" + [ -n "$code" ] || code=000 + [ -n "$time_s" ] || time_s=0 + ms="$(awk -v t="$time_s" 'BEGIN{printf "%d", t*1000}')" + last_code="$code" + if [ "$code" = "200" ] || [ "$code" = "204" ] || [ "$code" = "301" ] || [ "$code" = "302" ]; then + ok=$((ok + 1)) + sum_ms=$((sum_ms + ms)) + fi + i=$((i + 1)) +done + +loss=$((attempts - ok)) +avg_ms=0 +if [ "$ok" -gt 0 ]; then + avg_ms=$((sum_ms / ok)) +fi + +echo "ok=$ok attempts=$attempts loss=$loss avg_ms=$avg_ms code=$last_code" diff --git a/clashoo/files/usr/share/clashoo/net/access_check_cache.sh b/clashoo/files/usr/share/clashoo/net/access_check_cache.sh new file mode 100755 index 000000000..ff930eb04 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/net/access_check_cache.sh @@ -0,0 +1,195 @@ +#!/bin/sh + +set -eu + +CACHE_DIR="/tmp/clashoo" +CACHE_FILE="/tmp/clashoo_check_cache" +LOCK_DIR="${CACHE_DIR}/access_check.lock" +LOCK_PID_FILE="${LOCK_DIR}/pid" +UPDATING_FLAG="${CACHE_DIR}/access_check_updating" +TMP_FILE="${CACHE_FILE}.tmp.$$" +DIAG_LOG="/tmp/clashoo_access_check.log" + +take_lock() { + mkdir "$LOCK_DIR" 2>/dev/null || return 1 + printf '%s\n' "$$" > "$LOCK_PID_FILE" 2>/dev/null + return 0 +} + +pid_is_cache_worker() { + [ -r "/proc/$1/cmdline" ] || return 1 + tr '\0' ' ' <"/proc/$1/cmdline" 2>/dev/null | grep -q 'access_check_cache\.sh' +} + +lock_is_dead() { + _pid="$(cat "$LOCK_PID_FILE" 2>/dev/null)" + case "$_pid" in + ''|*[!0-9]*) + [ -n "$(find "$LOCK_DIR" -maxdepth 0 -mmin +3 2>/dev/null)" ] + return $? + ;; + esac + [ ! -d "/proc/$_pid" ] && return 0 + ! pid_is_cache_worker "$_pid" +} + +mkdir -p "$CACHE_DIR" +if ! take_lock; then + lock_is_dead || exit 0 + rm -rf "$LOCK_DIR" >/dev/null 2>&1 + take_lock || exit 0 +fi +trap 'rm -rf "$LOCK_DIR" "$UPDATING_FLAG" "$TMP_FILE"' EXIT INT TERM +: > "$UPDATING_FLAG" + +safe_int() { + case "${1:-}" in + ''|*[!0-9]*) + printf '0' + ;; + *) + printf '%s' "$1" + ;; + esac +} + +safe_code() { + case "${1:-}" in + ''|*[!0-9A-Za-z]*) + printf '000' + ;; + *) + printf '%s' "$1" + ;; + esac +} + +parse_field() { + _line="$1" + _key="$2" + printf '%s\n' "$_line" | sed -n "s/.*${_key}=\\([^ ]*\\).*/\\1/p" +} + +probe_json() { + _line="$1" + _ok="$(safe_int "$(parse_field "$_line" "ok")")" + _attempts="$(safe_int "$(parse_field "$_line" "attempts")")" + _loss="$(safe_int "$(parse_field "$_line" "loss")")" + _avg_ms="$(safe_int "$(parse_field "$_line" "avg_ms")")" + _code="$(safe_code "$(parse_field "$_line" "code")")" + + _state="down" + if [ "$_ok" -ge "$_attempts" ] && [ "$_attempts" -gt 0 ]; then + if [ "$_avg_ms" -ge 2500 ]; then + _state="high_latency" + else + _state="ok" + fi + elif [ "$_ok" -gt 0 ]; then + _state="loss" + fi + + _ok_bool=false + [ "$_ok" -gt 0 ] && _ok_bool=true + + printf '{"ok":%s,"state":"%s","code":"%s","ok_count":%s,"attempts":%s,"loss":%s,"avg_ms":%s}' \ + "$_ok_bool" "$_state" "$_code" "$_ok" "$_attempts" "$_loss" "$_avg_ms" +} + +log_diag() { + _line="$(date '+%Y-%m-%d %H:%M:%S') $1" + printf '%s\n' "$_line" >>"$DIAG_LOG" 2>/dev/null + _n="$(wc -l <"$DIAG_LOG" 2>/dev/null || echo 0)" + if [ "${_n:-0}" -gt 200 ]; then + tail -n 100 "$DIAG_LOG" >"${DIAG_LOG}.tmp" 2>/dev/null && mv "${DIAG_LOG}.tmp" "$DIAG_LOG" 2>/dev/null + fi +} + +probe_run() { + _url="$1" + _mode="$2" + # nice + ionice:探测 IO/CPU 都低优先级,避免抢占 LuCI dispatcher + nice -n 19 /usr/share/clashoo/net/access_check.sh "$_url" "$_mode" 2>/dev/null || true +} + +proxy_port="$(uci -q get clashoo.config.mixed_port)" +[ -z "$proxy_port" ] && proxy_port="$(uci -q get clashoo.config.http_port)" +[ -z "$proxy_port" ] && proxy_port="7890" +tcp_mode="$(uci -q get clashoo.config.tcp_mode)" +[ -z "$tcp_mode" ] && tcp_mode="redirect" +udp_mode="$(uci -q get clashoo.config.udp_mode)" +[ -z "$udp_mode" ] && udp_mode="$tcp_mode" + +proxy_listening() { + # 检测 mixed-port 是否在 LISTEN(clashoo 未运行时立即标 proxy down,避免显示假绿) + if command -v ss >/dev/null 2>&1; then + ss -tln 2>/dev/null | awk -v p=":${proxy_port}" '$0 ~ "LISTEN" && index($4, p) {found=1; exit} END{exit !found}' + elif command -v netstat >/dev/null 2>&1; then + netstat -tln 2>/dev/null | awk -v p=":${proxy_port}" '$0 ~ "LISTEN" && index($4, p) {found=1; exit} END{exit !found}' + else + return 0 + fi +} + +# 并行探测,把 CPU 抢占窗口从串行 2s+ 压缩到最慢一路的耗时 +f_db="${TMP_FILE}.db" +f_dy="${TMP_FILE}.dy" +f_pb="${TMP_FILE}.pb" +f_py="${TMP_FILE}.py" +probe_run "https://www.douyin.com/generate_204" "direct" >"$f_db" & +probe_run "https://www.youtube.com/generate_204" "direct" >"$f_dy" & +if proxy_listening; then + proxy_skipped=0 + probe_run "https://www.douyin.com/generate_204" "proxy" >"$f_pb" & + probe_run "https://www.youtube.com/generate_204" "proxy" >"$f_py" & +else + # 代理端口未监听(clashoo 已停止),跳过探测,直接标 down + proxy_skipped=1 + echo "ok=0 attempts=1 loss=1 avg_ms=0 code=skip" >"$f_pb" + echo "ok=0 attempts=1 loss=1 avg_ms=0 code=skip" >"$f_py" +fi +wait +direct_bytedance="$(cat "$f_db" 2>/dev/null)" +direct_youtube="$(cat "$f_dy" 2>/dev/null)" +proxy_bytedance="$(cat "$f_pb" 2>/dev/null)" +proxy_youtube="$(cat "$f_py" 2>/dev/null)" +rm -f "$f_db" "$f_dy" "$f_pb" "$f_py" + +updated_at="$(date +%s)" + +probe_all_down() { + for _p in "$direct_bytedance" "$direct_youtube" "$proxy_bytedance" "$proxy_youtube"; do + case "$_p" in + ''|*"ok=0 "*) ;; + *) return 1 ;; + esac + done + return 0 +} + +if [ "$(uci -q get clashoo.config.access_check_debug)" = "1" ] || probe_all_down; then + log_diag "port=${proxy_port} proxy_skipped=${proxy_skipped} direct_bd=[${direct_bytedance}] direct_yt=[${direct_youtube}] proxy_bd=[${proxy_bytedance}] proxy_yt=[${proxy_youtube}]" +fi + +cat >"$TMP_FILE" </dev/null; then + old_pid="$(cat "$PID_FILE" 2>/dev/null)" + if [ -n "$old_pid" ] && [ -d "/proc/$old_pid" ] && \ + tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null | grep -q 'access_check_daemon\.sh'; then + exit 0 + fi + rm -rf "$LOCK_DIR" "$PID_FILE" >/dev/null 2>&1 + mkdir "$LOCK_DIR" 2>/dev/null || exit 0 +fi + +cleanup() { + rm -rf "$LOCK_DIR" "$PID_FILE" +} +trap cleanup EXIT +trap 'cleanup; exit 0' INT TERM + +echo "$$" >"$PID_FILE" + +while :; do + [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ] || break + /usr/share/clashoo/net/access_check_cache.sh >/dev/null 2>&1 || true + if [ -n "$INTERVAL_FIXED" ]; then + sleep "$INTERVAL_FIXED" + else + sleep 30 + fi +done diff --git a/clashoo/files/usr/share/clashoo/net/dns_auto_setup.sh b/clashoo/files/usr/share/clashoo/net/dns_auto_setup.sh new file mode 100644 index 000000000..b1e22c8de --- /dev/null +++ b/clashoo/files/usr/share/clashoo/net/dns_auto_setup.sh @@ -0,0 +1,240 @@ +#!/bin/sh + +APPLY=0 +TIMEOUT="${CLASHOO_DNS_TEST_TIMEOUT:-2}" +START_TS="$(date +%s 2>/dev/null || echo 0)" + +while [ $# -gt 0 ]; do + case "$1" in + --apply) APPLY=1 ;; + esac + shift +done + +json_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +json_pair() { + printf '"%s":"%s"' "$1" "$(json_escape "$2")" +} + +is_ipv4() { + echo "$1" | grep -Eq '^[0-9]+(\.[0-9]+){3}$' +} + +append_unique() { + list="$1" + item="$2" + [ -n "$item" ] || { printf '%s' "$list"; return; } + case " $list " in + *" $item "*) printf '%s' "$list" ;; + *) printf '%s %s' "$list" "$item" ;; + esac +} + +current_default_nameservers() { + uci -q get clashoo.config.default_nameserver 2>/dev/null || true +} + +current_dns_servers() { + uci -q show clashoo 2>/dev/null | awk -F= ' + function clean(v) { + gsub(/^'\''|'\''$/, "", v); + return v; + } + /^clashoo\.@dnsservers\[[0-9]+\]\./ { + line=$1; + val=clean($2); + sub(/^clashoo\.@dnsservers\[/, "", line); + idx=line; + sub(/\].*$/, "", idx); + opt=line; + sub(/^[0-9]+\]\./, "", opt); + data[idx, opt]=val; + seen[idx]=1; + } + END { + for (idx in seen) { + if (data[idx, "enabled"] == "0") continue; + role=data[idx, "ser_type"]; + addr=data[idx, "ser_address"]; + proto=data[idx, "protocol"]; + port=data[idx, "ser_port"]; + if (addr == "") continue; + if (proto != "" && proto != "none" && addr !~ /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//) { + addr=proto addr; + if (port != "" && addr !~ /:[0-9]+$/) addr=addr ":" port; + } + print role "|" addr; + } + }' +} + +servers_for_role() { + role="$1" + current_dns_servers | awk -F'|' -v role="$role" '$1 == role { print $2 }' +} + +candidate_host_port() { + uri="$1" + default_port="$2" + hostport="${uri#*://}" + hostport="${hostport%%/*}" + host="${hostport%%:*}" + port="${hostport##*:}" + [ "$port" != "$hostport" ] || port="$default_port" + printf '%s %s\n' "$host" "$port" +} + +probe_tls() { + uri="$1" + set -- $(candidate_host_port "$uri" 853) + host="$1" + port="$2" + command -v nc >/dev/null 2>&1 || return 1 + nc -w "$TIMEOUT" "$host" "$port" >/dev/null 2>&1 +} + +probe_ip_dns() { + ip="$1" + domain="$2" + command -v nslookup >/dev/null 2>&1 || return 1 + nslookup "$domain" "$ip" >/dev/null 2>&1 +} + +probe_latency_ms() { + candidate="$1" + domain="$2" + if [ "${candidate#https://}" != "$candidate" ]; then + if printf '%s' "$candidate" | grep -q '?'; then + q="$candidate&name=$domain&type=A" + else + q="$candidate?name=$domain&type=A" + fi + command -v curl >/dev/null 2>&1 || return 1 + t="$(curl -fsS --connect-timeout "$TIMEOUT" --max-time "$TIMEOUT" \ + -H 'accept: application/dns-json' -o /dev/null -w '%{time_total}' "$q" 2>/dev/null)" || return 1 + printf '%s' "$t" | awk '{ms=$1*1000; if (ms<1) ms=1; printf "%d", ms}' + elif [ "${candidate#tls://}" != "$candidate" ]; then + probe_tls "$candidate" || return 1 + printf '900' + elif [ "${candidate#udp://}" != "$candidate" ]; then + probe_ip_dns "${candidate#udp://}" "$domain" || return 1 + printf '950' + elif [ "${candidate#tcp://}" != "$candidate" ]; then + probe_ip_dns "${candidate#tcp://}" "$domain" || return 1 + printf '950' + else + is_ipv4 "$candidate" && probe_ip_dns "$candidate" "$domain" || return 1 + printf '950' + fi +} + +select_best() { + list="$1" + domain="$2" + fallback="$3" + best="" + best_score=999999 + order=0 + for candidate in $list; do + order=$((order + 1)) + ms="$(probe_latency_ms "$candidate" "$domain")" || { FAILED_COUNT=$((FAILED_COUNT + 1)); continue; } + score=$((ms * 100 + order)) + if [ "$score" -lt "$best_score" ]; then + best="$candidate" + best_score="$score" + fi + done + [ -n "$best" ] || best="$fallback" + printf '%s' "$best" +} + +add_dnsserver() { + role="$1" + addr="$2" + proto="$3" + port="$4" + sec="$(uci -q add clashoo dnsservers)" || return 1 + [ -n "$sec" ] || return 1 + uci -q set clashoo."$sec".enabled='1' || return 1 + uci -q set clashoo."$sec".ser_type="$role" || return 1 + uci -q set clashoo."$sec".ser_address="$addr" || return 1 + uci -q set clashoo."$sec".protocol="$proto" || return 1 + if [ -n "$port" ]; then + uci -q set clashoo."$sec".ser_port="$port" || return 1 + fi + return 0 +} + +set_default_nameserver() { + uci -q delete clashoo.config.default_nameserver >/dev/null 2>&1 || true + uci -q add_list clashoo.config.default_nameserver="$1" || return 1 + return 0 +} + +apply_result() { + uci -q set clashoo.config.enable_dns='1' || { uci -q revert clashoo; return 1; } + set_default_nameserver "$BOOTSTRAP" || { uci -q revert clashoo; return 1; } + while uci -q delete clashoo.@dnsservers[0] >/dev/null 2>&1; do :; done + add_dnsserver 'nameserver' "$NAMESERVER" 'none' '' || { uci -q revert clashoo; return 1; } + add_dnsserver 'direct-nameserver' "$BOOTSTRAP" 'udp://' '' || { uci -q revert clashoo; return 1; } + add_dnsserver 'proxy-server-nameserver' "$PROXY_NS" 'none' '' || { uci -q revert clashoo; return 1; } + add_dnsserver 'fallback' "$FALLBACK_NS" 'none' '' || { uci -q revert clashoo; return 1; } + uci -q commit clashoo || { uci -q revert clashoo; return 1; } + return 0 +} + +is_domestic_dns() { + printf '%s' "$1" | grep -qE 'alidns|doh\.pub|dnspod|223\.5\.5\.5|223\.6\.6\.6|119\.29\.29\.29|119\.28\.28\.28|114\.114|180\.184|onedns' +} +is_overseas_dns() { + printf '%s' "$1" | grep -qE 'cloudflare|1\.1\.1\.1|1\.0\.0\.1|one\.one\.one\.one|dns\.google|8\.8\.8\.8|8\.8\.4\.4|9\.9\.9\.9|quad9|opendns|208\.67\.' +} + +FAILED_COUNT=0 +BOOTSTRAP_CANDIDATES="223.5.5.5 119.29.29.29 180.184.1.1" +DOMESTIC_CANDIDATES="https://dns.alidns.com/dns-query https://doh.pub/dns-query udp://223.5.5.5 udp://119.29.29.29" +FALLBACK_CANDIDATES="https://cloudflare-dns.com/dns-query https://dns.google/dns-query tls://1.1.1.1:853" + +for ns in $(current_default_nameservers); do + is_ipv4 "$ns" && BOOTSTRAP_CANDIDATES="$(append_unique "$BOOTSTRAP_CANDIDATES" "$ns")" +done +for ns in $(servers_for_role 'nameserver') $(servers_for_role 'direct-nameserver'); do + is_domestic_dns "$ns" && DOMESTIC_CANDIDATES="$(append_unique "$DOMESTIC_CANDIDATES" "$ns")" +done +for ns in $(servers_for_role 'fallback'); do + is_overseas_dns "$ns" && FALLBACK_CANDIDATES="$(append_unique "$FALLBACK_CANDIDATES" "$ns")" +done + +BOOTSTRAP="$(select_best "$BOOTSTRAP_CANDIDATES" www.baidu.com 223.5.5.5)" +NAMESERVER="$(select_best "$DOMESTIC_CANDIDATES" www.baidu.com https://dns.alidns.com/dns-query)" +PROXY_NS="tls://1.1.1.1:853" +FALLBACK_NS="$(select_best "$FALLBACK_CANDIDATES" www.google.com https://cloudflare-dns.com/dns-query)" + +APPLIED=false +if [ "$APPLY" = "1" ]; then + if apply_result; then + APPLIED=true + else + printf '{"success":false,"error":"apply_failed","message":"DNS 自动配置写入失败"}\n' + exit 1 + fi +fi + +END_TS="$(date +%s 2>/dev/null || echo "$START_TS")" +ELAPSED_MS=$(( (END_TS - START_TS) * 1000 )) + +printf '{' +printf '"success":true,' +printf '"applied":%s,' "$APPLIED" +printf '"restarted":false,' +json_pair bootstrap "$BOOTSTRAP"; printf ',' +json_pair nameserver "$NAMESERVER"; printf ',' +json_pair direct_nameserver "$BOOTSTRAP"; printf ',' +json_pair proxy_nameserver "$PROXY_NS"; printf ',' +json_pair fallback "$FALLBACK_NS"; printf ',' +printf '"failed_count":%s,' "$FAILED_COUNT" +printf '"elapsed_ms":%s' "$ELAPSED_MS" +printf '}\n' diff --git a/clashoo/files/usr/share/clashoo/net/fw4.sh b/clashoo/files/usr/share/clashoo/net/fw4.sh new file mode 100755 index 000000000..540c6b051 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/net/fw4.sh @@ -0,0 +1,898 @@ +#!/bin/sh + +set -eu + +NFT_DIR="/var/run/clash" +SETS_RULES="${NFT_DIR}/fw4_sets.nft" +DSTNAT_RULES="${NFT_DIR}/fw4_dstnat.nft" +MANGLE_RULES="${NFT_DIR}/fw4_mangle.nft" +OUTPUT_RULES="${NFT_DIR}/fw4_output.nft" +BUILTIN_NFT_DIR="/usr/share/clashoo/nftables" +GEOIP_CN_NFT="${BUILTIN_NFT_DIR}/geoip_cn.nft" +GEOIP6_CN_NFT="${BUILTIN_NFT_DIR}/geoip6_cn.nft" +LOCAL_OUTPUT_TABLE="clashoo_local" +QUIC_BLOCK_TABLE="clashoo_quic" +# PROXY_FWMARK: inbound TPROXY mark, ip rule -> table PROXY_ROUTE_TABLE -> lo. +# CORE_ROUTING_MARK: mihomo outbound SO_MARK (= routing-mark in config). +# Must differ: otherwise mihomo egress is pulled to lo -> unreachable. +# When changing CORE_ROUTING_MARK, also update yum_change.sh routing_mark_dec. +PROXY_FWMARK="0x162" +PROXY_ROUTE_TABLE="0x162" +CORE_ROUTING_MARK="0x1a0a" # = 6666 +ACL_BYPASS_FWMARK="0x163" +SINGBOX_BYPASS_FWMARK="0x2024" +ACL_BYPASS_PREF="8998" +DNSMASQ_BYPASS_PREF="8999" + +uci_get() { + uci -q get "$1" 2>/dev/null || true +} + +bool_enabled() { + case "$1" in + 1|true|TRUE|yes|on) return 0 ;; + *) return 1 ;; + esac +} + +acl_bool() { + [ -n "$1" ] || return 0 + bool_enabled "$1" +} + +tun_available() { + ip tuntap add mode tun name cotuntest >/dev/null 2>&1 || return 1 + ip link del cotuntest >/dev/null 2>&1 || true + return 0 +} + +config_redir_port() { + uci_get clashoo.config.redir_port +} + +config_tproxy_port() { + local port + port="$(uci_get clashoo.config.tproxy_port)" + if [ -n "$port" ]; then + printf '%s\n' "$port" + else + config_redir_port + fi +} + +config_tcp_mode() { + uci_get clashoo.config.tcp_mode +} + +config_udp_mode() { + uci_get clashoo.config.udp_mode +} + +config_access_control() { + [ "$(uci_get clashoo.config.acl_migrated)" = "1" ] && { + printf '0\n' + return + } + uci_get clashoo.config.access_control +} + +acl_sections() { + uci -q show clashoo 2>/dev/null | sed -n 's/^clashoo\.\([^.=]*\)=lan_acl$/\1/p' +} + +acl_list() { + uci_get "$1" +} + +acl_sections_catchall_last() { + local section + for section in $(acl_sections); do + [ -z "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] || printf '%s\n' "$section" + done + for section in $(acl_sections); do + [ -n "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] || printf '%s\n' "$section" + done +} + +grouped_acl_enabled() { + local section + [ "$(uci_get clashoo.config.acl_migrated)" = "1" ] || return 1 + for section in $(acl_sections); do + [ "$(uci_get "clashoo.${section}.enabled")" != "0" ] && return 0 + done + return 1 +} + +acl_has_catchall() { + local section + for section in $(acl_sections); do + [ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue + [ -z "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] && return 0 + done + return 1 +} + +lan_acl_enabled() { + grouped_acl_enabled && return 0 + case "$(config_access_control)" in + 1|2) return 0 ;; + esac + return 1 +} + +config_enable_dns() { + uci_get clashoo.config.enable_dns +} + +config_dns_port() { + local port="" + if [ "$(uci_get clashoo.config.core_type)" != "singbox" ]; then + port="$(sed -n 's/^[[:space:]]*listen:.*:[[:space:]]*['"'"'"]*\([0-9]\{1,\}\)['"'"'"]*[[:space:]]*$/\1/p' /etc/clashoo/config.yaml 2>/dev/null | head -n1)" + fi + [ -n "$port" ] || port="$(uci_get clashoo.config.listen_port)" + printf '%s' "$port" | grep -Eq '^[0-9]+$' || port=1053 + printf '%s\n' "$port" +} + +lan_dns_split_enabled() { + lan_acl_enabled || return 1 + bool_enabled "$(config_enable_dns)" || return 1 + bool_enabled "$(config_ipv4_dns_hijack)" || bool_enabled "$(config_ipv6_dns_hijack)" +} + +tun_acl_enabled() { + lan_acl_enabled || return 1 + [ "$(config_tcp_mode)" = "tun" ] || [ "$(config_udp_mode)" = "tun" ] +} + +config_ipv4_dns_hijack() { + uci_get clashoo.config.ipv4_dns_hijack +} + +config_ipv6_dns_hijack() { + uci_get clashoo.config.ipv6_dns_hijack +} + +config_ipv4_proxy() { + uci_get clashoo.config.ipv4_proxy +} + +config_ipv6_proxy() { + uci_get clashoo.config.ipv6_proxy +} + +singbox_tun_active() { + [ "$(uci_get clashoo.config.core_type)" = "singbox" ] || return 1 + [ "$(config_tcp_mode)" = "tun" ] || [ "$(config_udp_mode)" = "tun" ] +} + +config_bypass_china() { + uci_get clashoo.config.bypass_china +} + +config_bypass_china_ipv6() { + local value + value="$(uci_get clashoo.config.bypass_china_ipv6)" + if [ -n "$value" ]; then + printf '%s\n' "$value" + else + config_bypass_china + fi +} + +config_block_quic() { + uci_get clashoo.config.block_quic +} + +config_bypass_port_mode() { + uci_get clashoo.config.bypass_port_mode +} + +config_bypass_port_custom() { + uci_get clashoo.config.bypass_port_custom +} + +config_legacy_bypass_port() { + uci_list clashoo.config.bypass_port +} + +config_proxy_tcp_dport() { + local mode custom legacy value + mode="$(config_bypass_port_mode)" + custom="$(config_bypass_port_custom)" + legacy="$(uci_get clashoo.config.proxy_tcp_dport)" + [ -z "$legacy" ] && legacy="$(config_legacy_bypass_port)" + + case "$mode" in + all) + # 空值在 render_port_match 中表示该协议的全部端口 + printf '%s\n' '' + ;; + common) + printf '%s\n' '22,53,80,443,8080,8443' + ;; + custom) + value="$custom" + [ -z "$value" ] && value="$legacy" + printf '%s\n' "$value" + ;; + *) + printf '%s\n' "$legacy" + ;; + esac +} + +config_proxy_udp_dport() { + local mode custom legacy value + mode="$(config_bypass_port_mode)" + custom="$(config_bypass_port_custom)" + legacy="$(uci_get clashoo.config.proxy_udp_dport)" + [ -z "$legacy" ] && legacy="$(config_legacy_bypass_port)" + + case "$mode" in + all) + printf '%s\n' '' + ;; + common) + printf '%s\n' '22,53,80,443,8080,8443' + ;; + custom) + value="$custom" + [ -z "$value" ] && value="$legacy" + printf '%s\n' "$value" + ;; + *) + printf '%s\n' "$legacy" + ;; + esac +} + +config_bypass_dscp() { + uci_list clashoo.config.bypass_dscp +} + +config_bypass_fwmark() { + uci_list clashoo.config.bypass_fwmark +} + +config_fake_ip_range6() { + local value + value="$(uci_get clashoo.config.fake_ip_range6)" + [ -n "$value" ] || value="fc00::/18" + [ "$(uci_get clashoo.config.enhanced_mode)" = "redir-host" ] && return 0 + bool_enabled "$(uci_get clashoo.config.enable_ipv6)" || return 0 + printf '%s\n' "$value" +} + +config_fake_ip_range() { + local value + value="$(uci_get clashoo.config.fake_ip_range)" + [ -n "$value" ] && { + printf '%s\n' "$value" + return + } + printf '198.18.0.1/16\n' +} + +uci_list() { + local key="$1" + uci -q show "$key" 2>/dev/null | sed -n "s/^${key}=//p" | sed "s/'//g" +} + +ensure_firewall_include() { + local name="$1" + local path="$2" + local chain="${3:-}" + local position="${4:-chain-pre}" + + uci -q batch <<-EOF >/dev/null + set firewall.${name}=include + set firewall.${name}.type='nftables' + set firewall.${name}.path='${path}' + set firewall.${name}.position='${position}' + $( [ -n "$chain" ] && printf "set firewall.%s.chain='%s'\n" "$name" "$chain" ) + commit firewall +EOF +} + +remove_firewall_include() { + local name="$1" + uci -q delete firewall."${name}" >/dev/null 2>&1 || true +} + +render_common_returns() { + local fake6 + fake6="$(config_fake_ip_range6)" + printf '%s\n' 'meta nfproto ipv4 ip daddr { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 } return' + if [ -n "$fake6" ]; then + printf 'meta nfproto ipv6 ip6 daddr { ::1/128, fc00::/7, fe80::/10, ff00::/8 } ip6 daddr != %s return\n' "$fake6" + else + printf '%s\n' 'meta nfproto ipv6 ip6 daddr { ::1/128, fc00::/7, fe80::/10, ff00::/8 } return' + fi +} + +render_ip_elements() { + local list="$1" + local first=1 entry + for entry in $list; do + if [ "$first" -eq 0 ]; then + printf ', ' + fi + printf '%s' "$entry" + first=0 + done +} + +render_token_elements() { + printf '%s\n' "$1" | tr ',\t' ' ' | awk ' + BEGIN { first = 1 } + { + for (i = 1; i <= NF; i++) { + if ($i == "") + continue + if (!first) + printf ", " + printf "%s", $i + first = 0 + } + }' +} + +detect_coexist_fwmarks() { + # Merge co-installed proxy plugin fwmarks into bypass to avoid intercepting + # their traffic. Only when init.d exists (passwall:0x1 passwall2:0xff + # nikki:tproxy 0x80/tun 0x81 mask 0xff). + local marks="" + [ -x /etc/init.d/passwall ] && marks="$marks 0x1" + [ -x /etc/init.d/passwall2 ] && marks="$marks 0xff" + if [ -x /etc/init.d/nikki ]; then + local nm + nm="$(uci -q get nikki.routing.tproxy_fw_mark) $(uci -q get nikki.routing.tun_fw_mark)" + [ -z "$(echo "$nm" | tr -d ' ')" ] && nm="0x80 0x81" + marks="$marks $nm" + fi + printf '%s\n' "$marks" +} + +merge_fwmark_tokens() { + # Always bypass PROXY_FWMARK (inbound TPROXY) and CORE_ROUTING_MARK (core egress) + # so nft never traps them. Also merge co-installed plugin fwmarks for coexistence. + local coexist + coexist="$(detect_coexist_fwmarks)" + printf '%s %s %s %s\n' "$1" "$PROXY_FWMARK" "$CORE_ROUTING_MARK" "$coexist" | tr ',\t' ' ' | awk ' + BEGIN { first = 1 } + { + for (i = 1; i <= NF; i++) { + if ($i == "" || seen[$i]++) + continue + if (!first) + printf ", " + printf "%s", $i + first = 0 + } + }' +} + +render_port_match() { + local proto="$1" + local ports="$2" + local port_elements + + port_elements="$(render_token_elements "$ports")" + if [ -n "$port_elements" ]; then + printf 'meta l4proto %s %s dport { %s }' "$proto" "$proto" "$port_elements" + else + printf 'meta l4proto %s' "$proto" + fi +} + +render_acl_dns_rules() { + local redirect_port="$1" section ipv4 ipv6 mac ipv4_elements ipv6_elements mac_elements action + for section in $(acl_sections_catchall_last); do + [ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue + if acl_bool "$(uci_get "clashoo.${section}.dns")"; then + action="counter redirect to :${redirect_port}" + elif singbox_tun_active; then + action="meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return" + else + action="counter return" + fi + ipv4="$(acl_list "clashoo.${section}.ip")" + ipv6="$(acl_list "clashoo.${section}.ip6")" + mac="$(acl_list "clashoo.${section}.mac")" + ipv4_elements="$(render_ip_elements "$ipv4")" + ipv6_elements="$(render_ip_elements "$ipv6")" + mac_elements="$(render_ip_elements "$mac")" + + if [ -z "$ipv4_elements$ipv6_elements$mac_elements" ]; then + bool_enabled "$(config_ipv4_dns_hijack)" && printf 'meta nfproto ipv4 meta l4proto { tcp, udp } th dport 53 %s\n' "$action" + bool_enabled "$(config_ipv6_dns_hijack)" && printf 'meta nfproto ipv6 meta l4proto { tcp, udp } th dport 53 %s\n' "$action" + continue + fi + if [ -n "$ipv4_elements" ] && bool_enabled "$(config_ipv4_dns_hijack)"; then + printf 'meta nfproto ipv4 ip saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$ipv4_elements" "$action" + fi + if [ -n "$ipv6_elements" ] && bool_enabled "$(config_ipv6_dns_hijack)"; then + printf 'meta nfproto ipv6 ip6 saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$ipv6_elements" "$action" + fi + if [ -n "$mac_elements" ]; then + bool_enabled "$(config_ipv4_dns_hijack)" && printf 'meta nfproto ipv4 ether saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$mac_elements" "$action" + bool_enabled "$(config_ipv6_dns_hijack)" && printf 'meta nfproto ipv6 ether saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$mac_elements" "$action" + fi + done +} + +render_acl_proxy_rules() { + local mode="$1" proto="$2" port_match="$3" target_port="$4" + local section ipv4 ipv6 mac ipv4_elements ipv6_elements mac_elements enabled action4 action6 + + for section in $(acl_sections_catchall_last); do + [ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue + enabled="$(uci_get "clashoo.${section}.proxy")" + case "$mode" in + redirect) + if acl_bool "$enabled"; then + if singbox_tun_active; then + action4="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} counter redirect to :${target_port}" + else + action4="${port_match} counter redirect to :${target_port}" + fi + action6="$action4" + elif singbox_tun_active; then + action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return" + action6="$action4" + else + action4="meta l4proto ${proto} counter return" + action6="$action4" + fi + ;; + tproxy) + if acl_bool "$enabled"; then + if singbox_tun_active; then + action4="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} tproxy ip to :${target_port} meta mark set ${PROXY_FWMARK} counter accept" + action6="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} tproxy ip6 to :${target_port} meta mark set ${PROXY_FWMARK} counter accept" + else + action4="${port_match} tproxy ip to :${target_port} meta mark set ${PROXY_FWMARK} counter accept" + action6="${port_match} tproxy ip6 to :${target_port} meta mark set ${PROXY_FWMARK} counter accept" + fi + elif singbox_tun_active; then + action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return" + action6="$action4" + else + action4="meta l4proto ${proto} counter return" + action6="$action4" + fi + ;; + tun) + if acl_bool "$enabled"; then + action4="meta l4proto ${proto} counter return" + elif singbox_tun_active; then + action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter accept" + else + action4="meta l4proto ${proto} meta mark set ${ACL_BYPASS_FWMARK} counter accept" + fi + action6="$action4" + ;; + esac + + ipv4="$(acl_list "clashoo.${section}.ip")" + ipv6="$(acl_list "clashoo.${section}.ip6")" + mac="$(acl_list "clashoo.${section}.mac")" + ipv4_elements="$(render_ip_elements "$ipv4")" + ipv6_elements="$(render_ip_elements "$ipv6")" + mac_elements="$(render_ip_elements "$mac")" + + if [ -z "$ipv4_elements$ipv6_elements$mac_elements" ]; then + bool_enabled "$(config_ipv4_proxy)" && printf 'meta nfproto ipv4 %s\n' "$action4" + bool_enabled "$(config_ipv6_proxy)" && printf 'meta nfproto ipv6 %s\n' "$action6" + continue + fi + if [ -n "$ipv4_elements" ] && bool_enabled "$(config_ipv4_proxy)"; then + printf 'meta nfproto ipv4 ip saddr { %s } %s\n' "$ipv4_elements" "$action4" + fi + if [ -n "$ipv6_elements" ] && bool_enabled "$(config_ipv6_proxy)"; then + printf 'meta nfproto ipv6 ip6 saddr { %s } %s\n' "$ipv6_elements" "$action6" + fi + if [ -n "$mac_elements" ]; then + bool_enabled "$(config_ipv4_proxy)" && printf 'meta nfproto ipv4 ether saddr { %s } %s\n' "$mac_elements" "$action4" + bool_enabled "$(config_ipv6_proxy)" && printf 'meta nfproto ipv6 ether saddr { %s } %s\n' "$mac_elements" "$action6" + fi + done +} + +# Split proxied clients to the core DNS while ACL-bypassed clients use dnsmasq. +# Gated on UCI IPv4/IPv6 toggles. +# return 0: no output when both off, avoid set -e false-positive. +render_dns_hijack() { + local access_control="$1" redirect_port=53 + if lan_dns_split_enabled; then + redirect_port="$(config_dns_port)" + fi + if grouped_acl_enabled; then + render_acl_dns_rules "$redirect_port" + acl_has_catchall && return 0 + else + [ "$access_control" = "1" ] && printf '%s\n' 'ip saddr != @clash_proxy_lan return' + [ "$access_control" = "2" ] && printf '%s\n' 'ip saddr @clash_reject_lan return' + fi + bool_enabled "$(config_ipv4_dns_hijack)" && \ + printf '%s\n' "meta nfproto ipv4 meta l4proto { tcp, udp } th dport 53 counter redirect to :${redirect_port}" + bool_enabled "$(config_ipv6_dns_hijack)" && \ + printf '%s\n' "meta nfproto ipv6 meta l4proto { tcp, udp } th dport 53 counter redirect to :${redirect_port}" + return 0 +} + +apply_local_output_rule() { + local redir_port fake_ip_range tcp_mode bypass_fwmark bypass_china + local fwmark_elements fwmark_rule china_set china_rule dnsmasq_rule dnsmasq_uid + redir_port="$(config_redir_port)" + fake_ip_range="$(config_fake_ip_range)" + tcp_mode="$(config_tcp_mode)" + bypass_fwmark="$(config_bypass_fwmark)" + bypass_china="$(config_bypass_china)" + + # Keep local-output redirect usable when tun mode is selected but tun + # device is unavailable on the system. + if [ "$tcp_mode" = "tun" ] && ! tun_available; then + tcp_mode="redirect" + fi + + nft delete table ip ${LOCAL_OUTPUT_TABLE} >/dev/null 2>&1 || true + + # Only apply local output redirect when tcp_mode is redirect + [ "$tcp_mode" != "redirect" ] && return 0 + + # bypass mihomo's own marks (prevent self-hijack -> dead loop) + fwmark_rule="" + fwmark_elements="$(merge_fwmark_tokens "$bypass_fwmark")" + [ -n "$fwmark_elements" ] && fwmark_rule="meta mark { ${fwmark_elements} } return" + + # CN IP bypass (uses clashoo_china set from geoip_cn.nft) + china_set="" + china_rule="" + if bool_enabled "$bypass_china" && [ -s "$GEOIP_CN_NFT" ]; then + china_set="$(cat "$GEOIP_CN_NFT")" + china_rule="ip daddr @clashoo_china return" + fi + + dnsmasq_rule="" + if lan_dns_split_enabled; then + dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)" + printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$' && dnsmasq_rule="meta skuid ${dnsmasq_uid} return" + fi + + nft -f - </dev/null 2>&1 || true +} + +remove_acl_bypass_rules() { + local dnsmasq_uid + ip rule del pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true + ip -6 rule del pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true + dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)" + if printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$'; then + ip rule del pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true + ip -6 rule del pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true + fi +} + +apply_acl_bypass_rules() { + local dnsmasq_uid + remove_acl_bypass_rules + tun_acl_enabled || return 0 + ip rule add pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true + ip -6 rule add pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true + dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)" + if printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$'; then + ip rule add pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true + ip -6 rule add pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true + fi +} + +apply_block_quic_rule() { + local fake_ip_range china_set china_rule scope + nft delete table inet ${QUIC_BLOCK_TABLE} >/dev/null 2>&1 || true + bool_enabled "$(config_block_quic)" || return 0 + + fake_ip_range="$(config_fake_ip_range)" + china_set="" + scope="ip daddr ${fake_ip_range}" + if [ "$(uci_get clashoo.config.enhanced_mode)" != "fake-ip" ]; then + if bool_enabled "$(config_bypass_china)" && [ -s "$GEOIP_CN_NFT" ]; then + china_set="$(cat "$GEOIP_CN_NFT")" + china_rule="ip daddr @clashoo_china return" + fi + scope="" + fi + + nft -f - </dev/null 2>&1 || true +} + +write_empty_set() { + local set_name="$1" + local set_type="$2" + + printf 'set %s {\n\ttype %s;\n\tflags interval;\n\tauto-merge;\n}\n\n' "$set_name" "$set_type" +} + +append_set_from_file_or_empty() { + local file_path="$1" + local set_name="$2" + local set_type="$3" + + if [ -s "$file_path" ]; then + cat "$file_path" + printf '\n' + else + write_empty_set "$set_name" "$set_type" + fi +} + +generate_rules() { + local redir_port tproxy_port tcp_mode udp_mode access_control fake_ip_range proxy_lan_ips reject_lan_ips + local proxy_tcp_dport proxy_udp_dport bypass_dscp bypass_fwmark bypass_china bypass_china_ipv6 + local acl_catchall=0 + redir_port="$(config_redir_port)" + tproxy_port="$(config_tproxy_port)" + tcp_mode="$(config_tcp_mode)" + udp_mode="$(config_udp_mode)" + access_control="$(config_access_control)" + grouped_acl_enabled && acl_has_catchall && acl_catchall=1 + bypass_china="$(config_bypass_china)" + bypass_china_ipv6="$(config_bypass_china_ipv6)" + proxy_tcp_dport="$(config_proxy_tcp_dport)" + proxy_udp_dport="$(config_proxy_udp_dport)" + bypass_dscp="$(config_bypass_dscp)" + bypass_fwmark="$(config_bypass_fwmark)" + fake_ip_range="$(config_fake_ip_range)" + proxy_lan_ips="$(uci_list clashoo.config.proxy_lan_ips)" + reject_lan_ips="$(uci_list clashoo.config.reject_lan_ips)" + + # When tun device is unavailable, fall back to non-tun transparent modes + # so routing rules still take effect for sing-box redirect/tproxy inbounds. + if [ "$tcp_mode" = "tun" ] || [ "$udp_mode" = "tun" ]; then + if ! tun_available; then + [ "$tcp_mode" = "tun" ] && tcp_mode="redirect" + [ "$udp_mode" = "tun" ] && udp_mode="tproxy" + fi + fi + + mkdir -p "$NFT_DIR" + + # Build optional elements lines (nftables rejects empty elements = {}) + local proxy_elements reject_elements dscp_elements fwmark_elements + proxy_elements="$(render_ip_elements "$proxy_lan_ips")" + reject_elements="$(render_ip_elements "$reject_lan_ips")" + dscp_elements="$(render_token_elements "$bypass_dscp")" + fwmark_elements="$(merge_fwmark_tokens "$bypass_fwmark")" + tcp_match="$(render_port_match tcp "$proxy_tcp_dport")" + udp_match="$(render_port_match udp "$proxy_udp_dport")" + + { + printf 'set clashoo_localnetwork {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n' + printf '\telements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 }\n}\n\n' + + append_set_from_file_or_empty "$GEOIP_CN_NFT" clashoo_china ipv4_addr + append_set_from_file_or_empty "$GEOIP6_CN_NFT" clashoo_china6 ipv6_addr + + printf 'set clash_proxy_lan {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n' + [ -n "$proxy_elements" ] && printf '\telements = { %s }\n' "$proxy_elements" + printf '}\n\n' + + printf 'set clash_reject_lan {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n' + [ -n "$reject_elements" ] && printf '\telements = { %s }\n' "$reject_elements" + printf '}\n\n' + + if grouped_acl_enabled && [ "$tcp_mode" = "tun" ]; then + printf 'chain clashoo_acl_tun_tcp {\n' + render_acl_proxy_rules tun tcp "$tcp_match" "$tproxy_port" + printf '}\n\n' + fi + if grouped_acl_enabled && [ "$udp_mode" = "tun" ]; then + printf 'chain clashoo_acl_tun_udp {\n' + render_acl_proxy_rules tun udp "$udp_match" "$tproxy_port" + printf '}\n' + fi + } > "$SETS_RULES" + + : > "$OUTPUT_RULES" + + +# DNS hijack rules go atop DSTNAT_RULES: must run for all TCP modes + # (redirect/tproxy/tun) and BEFORE proxy redirect rules so port 53 is not stolen. + render_dns_hijack "$access_control" > "$DSTNAT_RULES" + + +# TCP rules: redirect mode appends proxy redirect; tproxy/tun skip this chain + case "$tcp_mode" in + redirect) + tcp_match="$(render_port_match tcp "$proxy_tcp_dport")" + cat >> "$DSTNAT_RULES" < "$MANGLE_RULES" + else + : > "$MANGLE_RULES" + fi +} + +apply_rules() { + generate_rules + ensure_firewall_include clash_fw4_sets "$SETS_RULES" '' table-pre + ensure_firewall_include clash_fw4_dstnat "$DSTNAT_RULES" dstnat + remove_firewall_include clash_fw4_output + if [ -s "$MANGLE_RULES" ]; then + _route_table_dec="$((PROXY_ROUTE_TABLE))" + ensure_firewall_include clash_fw4_mangle "$MANGLE_RULES" mangle_prerouting + ip rule show 2>/dev/null | grep -q "fwmark ${PROXY_FWMARK}.*lookup ${_route_table_dec}" || + ip rule add fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + ip route show table "$PROXY_ROUTE_TABLE" 2>/dev/null | grep -q 'local 0.0.0.0/0 dev lo' || + ip route add local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + if bool_enabled "$(config_ipv6_proxy)"; then + ip -6 rule show 2>/dev/null | grep -q "fwmark ${PROXY_FWMARK}.*lookup ${_route_table_dec}" || + ip -6 rule add fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + ip -6 route show table "$PROXY_ROUTE_TABLE" 2>/dev/null | grep -q 'local default dev lo' || + ip -6 route add local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + else + ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + fi + else + remove_firewall_include clash_fw4_mangle + ip rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + ip route del local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + fi + /etc/init.d/firewall restart >/dev/null 2>&1 || /etc/init.d/firewall reload >/dev/null 2>&1 || true + apply_acl_bypass_rules + apply_local_output_rule + apply_block_quic_rule +} + +remove_rules() { + remove_acl_bypass_rules + remove_local_output_rule + remove_block_quic_rule + remove_firewall_include clash_fw4_sets + remove_firewall_include clash_fw4_dstnat + remove_firewall_include clash_fw4_output + remove_firewall_include clash_fw4_mangle + remove_firewall_include clash_fw4_forward + uci commit firewall >/dev/null 2>&1 || true + rm -f "$SETS_RULES" "$DSTNAT_RULES" "$OUTPUT_RULES" "$MANGLE_RULES" + ip rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + ip route del local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true + /etc/init.d/firewall restart >/dev/null 2>&1 || /etc/init.d/firewall reload >/dev/null 2>&1 || true +} + +case "${1:-}" in + apply) + apply_rules + ;; + remove) + remove_rules + ;; + *) + echo "Usage: $0 {apply|remove}" >&2 + exit 1 + ;; +esac diff --git a/clashoo/files/usr/share/clashoo/net/log_format.awk b/clashoo/files/usr/share/clashoo/net/log_format.awk new file mode 100644 index 000000000..0813c3b78 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/net/log_format.awk @@ -0,0 +1,43 @@ +# clashoo 运行日志格式化 +# 输出: MM-DD HH:MM:SS msg + +# mihomo 原生行: time="YYYY-MM-DDTHH:MM:SS..." level=... msg="..." +/^time="[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/ { + # extract YYYY-MM-DD and HH:MM:SS + ts_date = substr($0, 12, 5) # MM-DD + ts_time = substr($0, 18, 8) # HH:MM:SS + utc_h = substr(ts_time, 1, 2) + 0 + cst_h = (utc_h + 8) % 24 + ts = sprintf("%s %02d:%s", ts_date, cst_h, substr(ts_time, 4)) + + prefix = "" + if (match($0, /level=warning /)) prefix = " [warn]" + else if (match($0, /level=error /)) prefix = " [err]" + else if (match($0, /level=fatal /)) prefix = " [fatal]" + + i = index($0, "msg=\"") + if (i > 0) { + rest = substr($0, i + 5) + sub(/"[[:space:]]*$/, "", rest) + print ts prefix " " rest + next + } + print ts " " $0 + next +} + +# log_msg 行: " YYYY-MM-DD HH:MM:SS - msg" → "MM-DD HH:MM:SS msg" +/^[[:space:]]+[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][[:space:]]+[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/ { + gsub(/^[[:space:]]+/, "") # trim leading space + ts_date = substr($0, 6, 5) # MM-DD + ts_time = substr($0, 12, 8) # HH:MM:SS + # remove "YYYY-MM-DD HH:MM:SS - " prefix + sub(/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][[:space:]]+[0-9][0-9]:[0-9][0-9]:[0-9][0-9][[:space:]]*-[[:space:]]*/, "") + print ts_date " " ts_time " " $0 + next +} + +# 空行丢弃 +NF == 0 { next } + +{ print } diff --git a/clashoo/files/usr/share/clashoo/nftables/geoip6_cn.nft b/clashoo/files/usr/share/clashoo/nftables/geoip6_cn.nft new file mode 100644 index 000000000..ad0f24d8d --- /dev/null +++ b/clashoo/files/usr/share/clashoo/nftables/geoip6_cn.nft @@ -0,0 +1,3414 @@ +set clashoo_china6 { + type ipv6_addr; + flags interval; + auto-merge; + elements = { + 2001:250::/30, + 2001:254::/33, + 2001:255::/32, + 2001:7fa:5::/48, + 2001:c68::/32, + 2001:cc0::/32, + 2001:da8::/32, + 2001:daa:1::/48, + 2001:daa:2::/47, + 2001:daa:4::/47, + 2001:daa:6::/48, + 2001:daa:9::/48, + 2001:dc7::/45, + 2001:dc7:9::/48, + 2001:dc7:a::/47, + 2001:dc7:c::/46, + 2001:dc7:10::/44, + 2001:dc7:20::/43, + 2001:dc7:40::/42, + 2001:dc7:80::/41, + 2001:dc7:100::/40, + 2001:dc7:200::/39, + 2001:dc7:400::/38, + 2001:dc7:800::/37, + 2001:dc7:1000::/36, + 2001:dc7:2000::/35, + 2001:dc7:4000::/34, + 2001:dc7:8000::/33, + 2001:dd8:1::/48, + 2001:dd9::/48, + 2001:df5:7800::/48, + 2001:4510:400::/40, + 2001:4510:1480::/41, + 2001:4511:1480::/41, + 2400:1040::/32, + 2400:1160::/32, + 2400:12c0::/32, + 2400:1340::/32, + 2400:1380::/32, + 2400:15c0::/32, + 2400:1640::/32, + 2400:16c0::/32, + 2400:1740::/32, + 2400:17c0::/32, + 2400:1840::/32, + 2400:18c0::/32, + 2400:1940::/32, + 2400:19a0::/32, + 2400:19c0::/32, + 2400:1a40::/32, + 2400:1ac0::/32, + 2400:1b40::/32, + 2400:1cc0::/32, + 2400:1d40::/32, + 2400:1dc0::/32, + 2400:1e40::/32, + 2400:1ec0::/32, + 2400:1f40::/32, + 2400:1fc0::/32, + 2400:3040::/32, + 2400:3140::/32, + 2400:3160::/32, + 2400:31c0::/32, + 2400:3200::/32, + 2400:32c0::/32, + 2400:3340::/32, + 2400:33c0::/32, + 2400:3440::/32, + 2400:34c0::/32, + 2400:3540::/32, + 2400:35c0::/32, + 2400:3600::/32, + 2400:3640::/32, + 2400:3660::/32, + 2400:36c0::/32, + 2400:38c0::/32, + 2400:39c0::/32, + 2400:3a00::/32, + 2400:3a40::/32, + 2400:3b40::/32, + 2400:3c40::/32, + 2400:3cc0::/32, + 2400:3e00::/32, + 2400:3f40::/32, + 2400:3f60::/46, + 2400:3f60:5::/48, + 2400:3f60:6::/47, + 2400:3f60:8::/45, + 2400:3f60:13::/48, + 2400:3f60:14::/46, + 2400:3f60:18::/45, + 2400:3f60:20::/43, + 2400:3f60:40::/42, + 2400:3f60:80::/41, + 2400:3f60:100::/40, + 2400:3f60:200::/39, + 2400:3f60:400::/38, + 2400:3f60:800::/37, + 2400:3f60:1000::/36, + 2400:3f60:2000::/35, + 2400:3f60:4000::/34, + 2400:3f60:8000::/33, + 2400:3fc0::/32, + 2400:4440::/32, + 2400:44c0::/32, + 2400:4540::/32, + 2400:4600::/32, + 2400:4640::/32, + 2400:46c0::/32, + 2400:4920::/32, + 2400:4bc0::/32, + 2400:4e00::/32, + 2400:4e40::/32, + 2400:5080::/32, + 2400:5280:1400::/38, + 2400:5280:1c00::/38, + 2400:5280:3400::/38, + 2400:5280:4c00::/38, + 2400:5280:5000::/36, + 2400:5280:6000::/35, + 2400:5280:8000::/34, + 2400:5280:c000::/35, + 2400:5280:e000::/36, + 2400:5280:f000::/37, + 2400:5280:f800::/38, + 2400:5400::/32, + 2400:5580::/32, + 2400:55c0::/32, + 2400:55e0::/32, + 2400:5600::/32, + 2400:5640::/32, + 2400:56c0::/32, + 2400:57c0::/32, + 2400:5840::/32, + 2400:5a00::/32, + 2400:5a40::/32, + 2400:5a60::/41, + 2400:5a60:80::/42, + 2400:5a60:c0::/43, + 2400:5a60:e0::/44, + 2400:5a60:f0::/45, + 2400:5a60:f8::/46, + 2400:5a60:fc::/47, + 2400:5a60:fe::/48, + 2400:5a60:100::/40, + 2400:5a60:200::/39, + 2400:5a60:400::/38, + 2400:5a60:800::/37, + 2400:5a60:1000::/36, + 2400:5a60:2000::/35, + 2400:5a60:4000::/34, + 2400:5a60:8001::/48, + 2400:5a60:8002::/47, + 2400:5a60:8004::/46, + 2400:5a60:8008::/45, + 2400:5a60:8010::/44, + 2400:5a60:8020::/43, + 2400:5a60:8040::/42, + 2400:5a60:8080::/41, + 2400:5a60:8100::/40, + 2400:5a60:8200::/39, + 2400:5a60:8400::/38, + 2400:5a60:8800::/37, + 2400:5a60:9000::/36, + 2400:5a60:a000::/35, + 2400:5a60:c000::/34, + 2400:5ac0::/32, + 2400:5b40::/32, + 2400:5bc0::/32, + 2400:5c40::/32, + 2400:5c80::/32, + 2400:5cc0::/32, + 2400:5e20::/32, + 2400:5e80::/32, + 2400:5ee0::/32, + 2400:5fc0::/32, + 2400:6000::/32, + 2400:6040::/32, + 2400:60c0::/32, + 2400:61c0::/32, + 2400:6200::/32, + 2400:6600::/32, + 2400:6640::/32, + 2400:66a0::/32, + 2400:66c0::/32, + 2400:66e0::/32, + 2400:6740::/32, + 2400:67a0::/32, + 2400:67c0::/32, + 2400:6840::/32, + 2400:68c0::/32, + 2400:6940::/32, + 2400:69c0::/32, + 2400:6a00::/32, + 2400:6a40::/32, + 2400:6ac0::/32, + 2400:6b40::/32, + 2400:6bc0::/32, + 2400:6c40::/32, + 2400:6cc0::/32, + 2400:6d40::/32, + 2400:6da0::/32, + 2400:6dc0::/32, + 2400:6e00::/32, + 2400:6e40::/32, + 2400:6e60::/32, + 2400:6ec0::/32, + 2400:6f40::/32, + 2400:6f80::/32, + 2400:6fc0::/32, + 2400:7040::/32, + 2400:70a0::/32, + 2400:7100::/32, + 2400:7140::/32, + 2400:71c0::/32, + 2400:7200::/32, + 2400:7240::/32, + 2400:72c0::/32, + 2400:7340::/32, + 2400:73c0::/32, + 2400:73e0::/32, + 2400:7440::/32, + 2400:74c0::/32, + 2400:7540::/32, + 2400:75a0::/28, + 2400:75c0::/32, + 2400:7640::/32, + 2400:7680::/32, + 2400:76c0::/32, + 2400:7740::/32, + 2400:77c0::/32, + 2400:79c0::/32, + 2400:7ae0::/32, + 2400:7bc0::/32, + 2400:7f80::/32, + 2400:7fc0::/32, + 2400:8080::/32, + 2400:8200::/31, + 2400:82c0::/32, + 2400:8580::/32, + 2400:8600::/32, + 2400:86a0::/32, + 2400:86e0::/32, + 2400:8780::/32, + 2400:87c0::/32, + 2400:8840::/32, + 2400:8920::/32, + 2400:8980::/32, + 2400:89c0::/32, + 2400:8be0::/32, + 2400:8ce0::/32, + 2400:8e00::/32, + 2400:8e60::/32, + 2400:8f00::/32, + 2400:8f60::/32, + 2400:8fc0::/32, + 2400:9020::/32, + 2400:9040::/32, + 2400:9380:8001::/48, + 2400:9380:8003::/48, + 2400:9380:8021::/48, + 2400:9380:8040::/48, + 2400:9380:8140::/48, + 2400:9380:8201::/48, + 2400:9380:8301::/48, + 2400:9380:9001::/48, + 2400:9380:9009::/48, + 2400:9380:9020::/47, + 2400:9380:9050::/47, + 2400:9380:9060::/48, + 2400:9380:9071::/48, + 2400:9380:9080::/47, + 2400:9380:90b1::/48, + 2400:9380:90b2::/47, + 2400:9380:90b4::/46, + 2400:9380:9109::/48, + 2400:9380:9121::/48, + 2400:9380:9220::/47, + 2400:9380:9250::/47, + 2400:9380:9260::/48, + 2400:9380:9271::/48, + 2400:9380:9280::/47, + 2400:9380:9282::/48, + 2400:9380:92b1::/48, + 2400:9380:92b2::/47, + 2400:9380:92b4::/46, + 2400:93e0::/32, + 2400:9520::/32, + 2400:9580::/32, + 2400:95c0::/32, + 2400:95e0::/32, + 2400:9600::/32, + 2400:9620::/32, + 2400:98c0::/32, + 2400:9960::/32, + 2400:99e0::/32, + 2400:9a00::/32, + 2400:9ca0::/32, + 2400:9e00::/32, + 2400:a320::/32, + 2400:a380::/32, + 2400:a420::/32, + 2400:a480::/32, + 2400:a5a0::/32, + 2400:a6a0::/39, + 2400:a6a0:300::/40, + 2400:a6a0:400::/38, + 2400:a6a0:800::/37, + 2400:a6a0:1000::/36, + 2400:a6a0:2000::/35, + 2400:a6a0:4000::/34, + 2400:a6a0:8000::/33, + 2400:a780::/32, + 2400:a860::/32, + 2400:a8a0::/32, + 2400:a8c0::/32, + 2400:a900::/32, + 2400:a980::/29, + 2400:a9a0::/32, + 2400:abc0::/32, + 2400:ae00::/32, + 2400:b200::/32, + 2400:b500::/32, + 2400:b600::/32, + 2400:b620::/32, + 2400:b6c0::/32, + 2400:b700::/32, + 2400:b9a0::/32, + 2400:b9c0::/32, + 2400:ba00::/32, + 2400:be00::/32, + 2400:bf00::/32, + 2400:c200::/32, + 2400:c380::/32, + 2400:c840::/32, + 2400:c8c0::/32, + 2400:c940::/32, + 2400:c9c0::/32, + 2400:ca40::/32, + 2400:cac0::/32, + 2400:cb40::/32, + 2400:cb80::/33, + 2400:cb80:8000::/40, + 2400:cb80:8100::/43, + 2400:cb80:8120::/44, + 2400:cb80:8130::/45, + 2400:cb80:8138::/47, + 2400:cb80:813a::/48, + 2400:cb80:813c::/47, + 2400:cb80:813e::/48, + 2400:cb80:8140::/42, + 2400:cb80:8180::/41, + 2400:cb80:8200::/39, + 2400:cb80:8400::/38, + 2400:cb80:8800::/37, + 2400:cb80:9000::/36, + 2400:cb80:a000::/35, + 2400:cb80:c000::/34, + 2400:cbc0::/32, + 2400:cc40::/32, + 2400:cc80::/32, + 2400:ccc0::/32, + 2400:cd40::/32, + 2400:cda0::/32, + 2400:cdc0::/32, + 2400:ce00::/32, + 2400:ce40::/32, + 2400:cf40::/32, + 2400:cfc0::/32, + 2400:d0a0::/32, + 2400:d0c0::/32, + 2400:d100::/32, + 2400:d160::/32, + 2400:d1c0::/32, + 2400:d200::/32, + 2400:d300::/32, + 2400:d440::/32, + 2400:d600::/32, + 2400:d6a0::/32, + 2400:d6c0::/32, + 2400:d720::/32, + 2400:d780::/32, + 2400:d7a0::/32, + 2400:da00::/32, + 2400:da60::/32, + 2400:dd00::/28, + 2400:dd40::/32, + 2400:dda0::/32, + 2400:de00::/32, + 2400:de80::/32, + 2400:dee0::/32, + 2400:e0c0::/32, + 2400:e680::/32, + 2400:e7e0::/32, + 2400:e880::/32, + 2400:ebc0::/32, + 2400:ed60::/32, + 2400:eda0::/32, + 2400:edc0::/32, + 2400:ee00::/32, + 2400:eec0::/32, + 2400:ef40::/32, + 2400:f480::/32, + 2400:f5c0::/32, + 2400:f6e0::/32, + 2400:f720::/32, + 2400:f7c0::/32, + 2400:f840::/32, + 2400:f860::/32, + 2400:f980::/32, + 2400:fac0::/32, + 2400:fb40::/32, + 2400:fb60::/32, + 2400:fbc0::/32, + 2400:fc40::/32, + 2400:fcc0::/32, + 2400:fe00::/32, + 2401:20:100::/40, + 2401:20:200::/39, + 2401:20:400::/38, + 2401:20:800::/37, + 2401:20:1000::/39, + 2401:20:1200::/40, + 2401:20:1300::/43, + 2401:20:1320::/44, + 2401:20:1334::/46, + 2401:20:1338::/45, + 2401:20:1340::/42, + 2401:20:1380::/41, + 2401:20:1400::/38, + 2401:20:1800::/37, + 2401:20:2000::/40, + 2401:20:2100::/46, + 2401:20:2108::/45, + 2401:20:2110::/44, + 2401:20:2120::/43, + 2401:20:2140::/42, + 2401:20:2180::/41, + 2401:20:2200::/39, + 2401:20:2400::/38, + 2401:20:2800::/37, + 2401:20:3000::/36, + 2401:20:4000::/34, + 2401:20:8000::/33, + 2401:60::/32, + 2401:80::/32, + 2401:140::/32, + 2401:1c0::/32, + 2401:540::/32, + 2401:620::/32, + 2401:7c0::/32, + 2401:800::/32, + 2401:9c0::/32, + 2401:a00::/32, + 2401:a40::/32, + 2401:ac0::/32, + 2401:b40::/32, + 2401:ba0::/32, + 2401:bc0::/32, + 2401:c40::/32, + 2401:cc0::/32, + 2401:d40::/32, + 2401:e00::/32, + 2401:1000::/32, + 2401:1160::/32, + 2401:11c0::/32, + 2401:1200::/32, + 2401:12c0::/32, + 2401:1320::/32, + 2401:15c0::/32, + 2401:18c0::/32, + 2401:18e0::/28, + 2401:1940::/32, + 2401:19c0::/32, + 2401:1a40::/32, + 2401:1ac0::/32, + 2401:1c60::/32, + 2401:1ce0::/32, + 2401:1d40::/32, + 2401:1da0::/43, + 2401:1da0:30::/44, + 2401:1da0:40::/42, + 2401:1da0:80::/41, + 2401:1da0:100::/40, + 2401:1da0:200::/39, + 2401:1da0:400::/38, + 2401:1da0:800::/37, + 2401:1da0:1000::/36, + 2401:1da0:2000::/35, + 2401:1da0:4000::/34, + 2401:1da0:8000::/33, + 2401:1dc0::/32, + 2401:1de0::/32, + 2401:1e00::/32, + 2401:1ec0::/32, + 2401:1f40::/32, + 2401:2040::/32, + 2401:2080::/32, + 2401:23c0::/32, + 2401:2600::/32, + 2401:2780::/32, + 2401:2980::/32, + 2401:2a00:f000::/43, + 2401:2b40::/32, + 2401:2e00::/32, + 2401:2e20::/32, + 2401:3100::/32, + 2401:3380::/32, + 2401:33c0::/32, + 2401:3440::/32, + 2401:3480::/32, + 2401:34a0::/31, + 2401:34c0::/32, + 2401:3640::/32, + 2401:3780::/32, + 2401:3800::/32, + 2401:3880::/32, + 2401:3980::/32, + 2401:3a00::/32, + 2401:3a80::/32, + 2401:3b80::/32, + 2401:3c20::/32, + 2401:3c80::/32, + 2401:3d80::/32, + 2401:3e80::/32, + 2401:3f80::/32, + 2401:4080::/32, + 2401:4180::/32, + 2401:4280::/32, + 2401:4380::/32, + 2401:4480::/32, + 2401:4580::/32, + 2401:4680::/32, + 2401:4780::/32, + 2401:4880::/32, + 2401:4a80::/32, + 2401:4b00::/32, + 2401:4f80::/32, + 2401:5680::/32, + 2401:58a0::/32, + 2401:59c0::/32, + 2401:5b40::/32, + 2401:5c20::/32, + 2401:5c60::/32, + 2401:5c80::/32, + 2401:5fa0::/32, + 2401:70e0::/32, + 2401:7180::/32, + 2401:71c0::/32, + 2401:7240::/32, + 2401:7320::/32, + 2401:7360::/32, + 2401:73a0::/32, + 2401:7580::/32, + 2401:7660::/32, + 2401:7680::/32, + 2401:7700::/32, + 2401:7780::/32, + 2401:77e0::/32, + 2401:7820::/32, + 2401:7880::/32, + 2401:78e0::/32, + 2401:7980::/32, + 2401:7a00::/32, + 2401:7a80::/32, + 2401:7b80::/32, + 2401:7bc0::/32, + 2401:7c80::/32, + 2401:7cc0::/32, + 2401:7ce0::/32, + 2401:7d40::/32, + 2401:7d80::/32, + 2401:7e00::/32, + 2401:7f80::/32, + 2401:8200::/32, + 2401:82c0::/32, + 2401:8380::/32, + 2401:8540::/32, + 2401:8600::/32, + 2401:8680::/34, + 2401:8680:4000::/46, + 2401:8680:4008::/45, + 2401:8680:4010::/44, + 2401:8680:4020::/43, + 2401:8680:4040::/42, + 2401:8680:4080::/41, + 2401:8680:4103::/48, + 2401:8680:4104::/46, + 2401:8680:4108::/45, + 2401:8680:4110::/44, + 2401:8680:4120::/43, + 2401:8680:4140::/42, + 2401:8680:4180::/41, + 2401:8680:4200::/39, + 2401:8680:4400::/38, + 2401:8680:4800::/37, + 2401:8680:5000::/36, + 2401:8680:6000::/35, + 2401:8680:8000::/33, + 2401:8720::/32, + 2401:87e0::/32, + 2401:8820::/31, + 2401:8840::/32, + 2401:8be0::/32, + 2401:8d00::/47, + 2401:8d00:3::/48, + 2401:8d00:4::/46, + 2401:8d00:8::/45, + 2401:8d00:10::/46, + 2401:8d00:17::/48, + 2401:8d00:18::/45, + 2401:8d00:20::/43, + 2401:8d00:40::/42, + 2401:8d00:80::/41, + 2401:8d00:100::/40, + 2401:8d00:200::/39, + 2401:8d00:400::/38, + 2401:8d00:800::/37, + 2401:8d00:1000::/36, + 2401:8d00:2000::/35, + 2401:8d00:4000::/34, + 2401:8d00:8000::/33, + 2401:8da0::/32, + 2401:8f40::/32, + 2401:8fc0::/32, + 2401:90a0::/32, + 2401:9260::/32, + 2401:92a0::/32, + 2401:92e0::/32, + 2401:9340::/32, + 2401:95e0::/32, + 2401:9600::/32, + 2401:96c0::/32, + 2401:96e0::/32, + 2401:9720::/32, + 2401:9740::/32, + 2401:97a0::/32, + 2401:98c0::/32, + 2401:9a00::/32, + 2401:9ac0::/32, + 2401:9b20::/31, + 2401:9b40::/32, + 2401:9b60::/32, + 2401:9bc0::/32, + 2401:9ca0::/32, + 2401:9d20::/32, + 2401:9dc0::/32, + 2401:9e20::/32, + 2401:9e40::/32, + 2401:9f80::/32, + 2401:9fa0::/32, + 2401:a140::/32, + 2401:a2e0::/32, + 2401:a340::/32, + 2401:a3a0::/32, + 2401:a3c0::/32, + 2401:a4c0::/32, + 2401:a4e0::/32, + 2401:a540::/32, + 2401:a5c0::/32, + 2401:a620::/32, + 2401:a640::/32, + 2401:a6e0::/32, + 2401:a720::/32, + 2401:a940::/32, + 2401:a980::/32, + 2401:a9a0::/32, + 2401:aa00::/32, + 2401:aa20::/32, + 2401:aa40::/32, + 2401:ab60::/32, + 2401:aba0::/32, + 2401:acc0::/32, + 2401:ad40::/32, + 2401:adc0::/32, + 2401:afa0::/32, + 2401:b040::/32, + 2401:b180::/32, + 2401:b220::/32, + 2401:b340::/32, + 2401:b360::/32, + 2401:b400::/32, + 2401:b4c0::/32, + 2401:b4e0::/32, + 2401:b540::/32, + 2401:b580::/32, + 2401:b5a0::/32, + 2401:b600::/32, + 2401:b680::/32, + 2401:b6c0::/32, + 2401:b6e0::/32, + 2401:b7c0::/32, + 2401:b940::/32, + 2401:ba00::/32, + 2401:ba40::/32, + 2401:bb20::/32, + 2401:bc60::/31, + 2401:bda0::/32, + 2401:be00::/32, + 2401:bf20::/32, + 2401:c020::/32, + 2401:c200::/32, + 2401:c540::/32, + 2401:c600::/32, + 2401:c640::/32, + 2401:c6c0::/32, + 2401:c840::/32, + 2401:c8c0::/32, + 2401:ca00::/32, + 2401:ca20::/32, + 2401:cb80::/32, + 2401:cbe0::/32, + 2401:cc00::/32, + 2401:cc60::/32, + 2401:ce00::/32, + 2401:cf40::/32, + 2401:cfc0::/32, + 2401:cfe0::/32, + 2401:d060::/32, + 2401:d0c0::/32, + 2401:d0e0::/32, + 2401:d140::/32, + 2401:d180::/32, + 2401:d2c0::/32, + 2401:d340::/32, + 2401:d420::/32, + 2401:d780::/32, + 2401:d7e0::/32, + 2401:d8e0::/32, + 2401:d920::/28, + 2401:da00::/32, + 2401:dbe0::/32, + 2401:dd20::/32, + 2401:dd60::/32, + 2401:de00::/32, + 2401:dfe0::/32, + 2401:e020::/32, + 2401:e080::/32, + 2401:e0c0::/32, + 2401:e140::/32, + 2401:e240::/32, + 2401:e2c0::/32, + 2401:e340::/32, + 2401:e360::/32, + 2401:e620::/32, + 2401:e840::/32, + 2401:e8c0::/32, + 2401:e940::/32, + 2401:e9c0::/32, + 2401:ec00::/32, + 2401:ec40::/32, + 2401:f0a0::/32, + 2401:f0e0::/32, + 2401:f220::/32, + 2401:f300::/32, + 2401:f320::/32, + 2401:f3e0::/32, + 2401:f7c0::/32, + 2401:f860::/46, + 2401:f860:4::/47, + 2401:f860:6::/48, + 2401:f860:8::/47, + 2401:f860:a::/48, + 2401:f860:c::/46, + 2401:f860:10::/44, + 2401:f860:20::/43, + 2401:f860:40::/42, + 2401:f860:80::/45, + 2401:f860:89::/48, + 2401:f860:8a::/47, + 2401:f860:8c::/46, + 2401:f860:91::/48, + 2401:f860:95::/48, + 2401:f860:96::/48, + 2401:f860:98::/45, + 2401:f860:a0::/43, + 2401:f860:c0::/43, + 2401:f860:e0::/44, + 2401:f860:f0::/46, + 2401:f860:f4::/47, + 2401:f860:f7::/48, + 2401:f860:f8::/45, + 2401:f860:200::/39, + 2401:f860:400::/38, + 2401:f860:800::/37, + 2401:f860:1000::/36, + 2401:f860:2000::/35, + 2401:f860:4000::/34, + 2401:f860:8000::/33, + 2401:fa80::/32, + 2401:fb80::/32, + 2401:fc60::/32, + 2401:fc80::/32, + 2401:ffc0::/32, + 2402:440::/32, + 2402:5c0::/32, + 2402:840::/32, + 2402:a60::/32, + 2402:c20::/32, + 2402:c60::/32, + 2402:e00::/32, + 2402:fc0::/32, + 2402:1000::/32, + 2402:1160::/32, + 2402:1440::/32, + 2402:14c0::/32, + 2402:1520::/32, + 2402:1600::/32, + 2402:16e0::/32, + 2402:1740::/32, + 2402:18a0::/32, + 2402:19c0::/32, + 2402:1be0::/32, + 2402:1c20::/32, + 2402:1f80::/32, + 2402:2000::/32, + 2402:20e0::/33, + 2402:20e0:8000::/34, + 2402:20e0:c000::/35, + 2402:20e0:e000::/36, + 2402:20e0:f001::/48, + 2402:20e0:f002::/47, + 2402:20e0:f004::/46, + 2402:20e0:f008::/45, + 2402:20e0:f010::/44, + 2402:20e0:f020::/43, + 2402:20e0:f040::/42, + 2402:20e0:f080::/41, + 2402:20e0:f100::/40, + 2402:20e0:f200::/39, + 2402:20e0:f400::/38, + 2402:20e0:f800::/37, + 2402:2280::/32, + 2402:2440::/32, + 2402:24c0::/32, + 2402:2540::/32, + 2402:2620::/32, + 2402:2640::/32, + 2402:2760::/32, + 2402:2a00::/32, + 2402:2b80::/32, + 2402:2bc0::/32, + 2402:2ca0::/32, + 2402:2d00::/32, + 2402:2d80::/32, + 2402:2e60::/32, + 2402:2e80::/32, + 2402:2f40::/32, + 2402:3040::/32, + 2402:3140::/32, + 2402:3180:4::/46, + 2402:3180:8::/45, + 2402:3180:10::/44, + 2402:3180:20::/43, + 2402:3180:40::/42, + 2402:3180:80::/41, + 2402:3180:100::/40, + 2402:3180:200::/39, + 2402:3180:400::/38, + 2402:3180:800::/37, + 2402:3180:1000::/36, + 2402:3180:2000::/35, + 2402:3180:4000::/34, + 2402:31c0::/32, + 2402:3240::/32, + 2402:32e0::/32, + 2402:3320::/32, + 2402:33a0::/32, + 2402:33c0::/32, + 2402:34e0::/32, + 2402:36e0::/32, + 2402:39c0::/32, + 2402:3a40::/32, + 2402:3ac0::/32, + 2402:3ba0::/32, + 2402:3c00::/32, + 2402:3d20:2000::/35, + 2402:3d20:4000::/34, + 2402:3d20:8000::/33, + 2402:3de0::/32, + 2402:3e00::/32, + 2402:3ec0::/32, + 2402:3f80::/32, + 2402:4060::/32, + 2402:4140::/32, + 2402:42c0::/32, + 2402:4340::/32, + 2402:43c0::/32, + 2402:4440::/32, + 2402:4500::/32, + 2402:4540::/32, + 2402:4820::/32, + 2402:4a00::/32, + 2402:4a40::/32, + 2402:4a80::/32, + 2402:4ac0::/32, + 2402:4b80::/32, + 2402:4bc0::/32, + 2402:4be0::/32, + 2402:4c40::/32, + 2402:4d60::/32, + 2402:4d80::/32, + 2402:4e00::/32, + 2402:4ec0::/32, + 2402:4f80::/32, + 2402:50a0::/32, + 2402:5180::/32, + 2402:51a0::/32, + 2402:5340::/32, + 2402:5820::/32, + 2402:5880::/32, + 2402:5920::/32, + 2402:5940::/32, + 2402:59c0::/32, + 2402:5a40::/32, + 2402:5b40::/32, + 2402:5bc0::/32, + 2402:5d00::/32, + 2402:5e00::/32, + 2402:5e40::/32, + 2402:5ec0::/32, + 2402:5f20::/32, + 2402:5f40::/32, + 2402:6060::/32, + 2402:6280::/32, + 2402:6320::/32, + 2402:64c0::/32, + 2402:66c0::/32, + 2402:6740::/32, + 2402:67c0::/32, + 2402:6960::/32, + 2402:6a00::/32, + 2402:6b40::/32, + 2402:6bc0::/32, + 2402:6e00::/32, + 2402:6e80::/32, + 2402:6f40::/32, + 2402:6fc0::/32, + 2402:7040::/32, + 2402:7060::/32, + 2402:7080::/32, + 2402:70a0::/29, + 2402:70c0::/32, + 2402:7140::/32, + 2402:71c0::/32, + 2402:7240::/32, + 2402:72a0::/32, + 2402:72c0::/32, + 2402:73e0::/32, + 2402:7540::/32, + 2402:75c0::/32, + 2402:7740::/32, + 2402:7d00::/32, + 2402:7d80::/32, + 2402:8180::/32, + 2402:8300::/32, + 2402:8380::/32, + 2402:85c0::/32, + 2402:8800::/32, + 2402:8840::/32, + 2402:8900::/32, + 2402:8940::/32, + 2402:89c0::/32, + 2402:8b40::/32, + 2402:8bc0::/32, + 2402:8cc0::/32, + 2402:8d40::/32, + 2402:8f40::/32, + 2402:8f80::/32, + 2402:9240::/32, + 2402:92c0::/32, + 2402:93c0::/32, + 2402:9440::/32, + 2402:9480::/32, + 2402:94c0::/32, + 2402:9580::/32, + 2402:95c0::/32, + 2402:9680::/32, + 2402:96c0::/32, + 2402:9840::/32, + 2402:98c0::/32, + 2402:9940::/32, + 2402:9a80::/32, + 2402:9b80::/32, + 2402:9f80::/32, + 2402:9fc0::/32, + 2402:a080::/32, + 2402:a180::/32, + 2402:a200::/32, + 2402:a240::/32, + 2402:a280::/32, + 2402:a380::/32, + 2402:a640::/32, + 2402:a680::/32, + 2402:a6c0::/32, + 2402:a840::/32, + 2402:a880::/32, + 2402:aa80::/32, + 2402:ab80::/32, + 2402:ae00::/32, + 2402:ae40::/32, + 2402:aec0::/32, + 2402:af80::/32, + 2402:afc0::/32, + 2402:b080::/32, + 2402:b200::/32, + 2402:b440::/32, + 2402:b6c0::/32, + 2402:b880::/32, + 2402:b8c0::/32, + 2402:b940:100::/40, + 2402:b940:400::/38, + 2402:b940:800::/37, + 2402:b940:1000::/36, + 2402:b940:2000::/35, + 2402:b940:4000::/34, + 2402:b940:8000::/33, + 2402:b980::/32, + 2402:ba80::/32, + 2402:bac0::/32, + 2402:bbc0::/32, + 2402:bf80::/32, + 2402:c280::/32, + 2402:c3c0::/32, + 2402:c5c0::/32, + 2402:c9c0::/32, + 2402:cc40::/32, + 2402:cf00::/32, + 2402:cf40::/32, + 2402:d040::/32, + 2402:d140::/32, + 2402:d2c0:3::/48, + 2402:d2c0:4::/46, + 2402:d2c0:8::/45, + 2402:d2c0:10::/44, + 2402:d2c0:20::/43, + 2402:d2c0:40::/42, + 2402:d2c0:80::/41, + 2402:d2c0:100::/40, + 2402:d2c0:200::/39, + 2402:d2c0:400::/38, + 2402:d2c0:800::/37, + 2402:d2c0:1000::/36, + 2402:d2c0:2000::/35, + 2402:d2c0:4000::/34, + 2402:d2c0:8000::/33, + 2402:d300::/32, + 2402:d340::/32, + 2402:d380::/32, + 2402:d5c0::/32, + 2402:d6c0::/32, + 2402:d740::/32, + 2402:d780::/32, + 2402:d880::/32, + 2402:d980::/32, + 2402:da40::/32, + 2402:db40::/34, + 2402:db40:4000::/36, + 2402:db40:5000::/37, + 2402:db40:5800::/40, + 2402:db40:5901::/48, + 2402:db40:5902::/47, + 2402:db40:5904::/46, + 2402:db40:5908::/45, + 2402:db40:5910::/44, + 2402:db40:5920::/43, + 2402:db40:5940::/42, + 2402:db40:5980::/41, + 2402:db40:5a00::/39, + 2402:db40:5c00::/38, + 2402:db40:6000::/35, + 2402:db40:8000::/33, + 2402:dcc0::/32, + 2402:de40::/32, + 2402:dec0::/32, + 2402:df40::/32, + 2402:dfc0::/32, + 2402:e040::/32, + 2402:e0c0::/32, + 2402:e140::/32, + 2402:e2c0::/32, + 2402:e3c0::/32, + 2402:e480::/32, + 2402:e540::/32, + 2402:e680::/32, + 2402:e740::/32, + 2402:e780::/32, + 2402:e7c0::/37, + 2402:e7c0:800::/38, + 2402:e7c0:1000::/36, + 2402:e7c0:2000::/35, + 2402:e7c0:4000::/34, + 2402:e7c0:8000::/33, + 2402:e880::/32, + 2402:e980::/32, + 2402:eb80::/32, + 2402:ec80::/32, + 2402:ed80::/32, + 2402:ef40::/32, + 2402:ef80::/32, + 2402:f000::/32, + 2402:f140::/32, + 2402:f480::/32, + 2402:f540::/32, + 2402:f580::/32, + 2402:f780::/32, + 2402:f8c0::/32, + 2402:f980::/32, + 2402:f9c0::/32, + 2402:fac0::/32, + 2402:fcc0::/32, + 2402:ff40::/32, + 2402:ffc0::/32, + 2403:600::/32, + 2403:700::/32, + 2403:7c0::/32, + 2403:800::/31, + 2403:980::/32, + 2403:a80::/32, + 2403:b80::/32, + 2403:c80::/32, + 2403:d80::/32, + 2403:e80::/32, + 2403:f00::/32, + 2403:f80::/32, + 2403:fc0::/32, + 2403:1180::/32, + 2403:1340::/32, + 2403:1440::/32, + 2403:1580::/32, + 2403:16c0::/32, + 2403:17c0::/32, + 2403:1980::/32, + 2403:1b80::/32, + 2403:1c80::/32, + 2403:1d80::/32, + 2403:1dc0::/32, + 2403:1e80::/32, + 2403:1ec0::/36, + 2403:1ec0:1000::/37, + 2403:1ec0:1800::/40, + 2403:1ec0:1901::/48, + 2403:1ec0:1902::/47, + 2403:1ec0:1904::/46, + 2403:1ec0:1908::/45, + 2403:1ec0:1950::/44, + 2403:1ec0:1960::/43, + 2403:1ec0:1980::/41, + 2403:1ec0:1a00::/39, + 2403:1ec0:1c00::/38, + 2403:1ec0:2000::/35, + 2403:1ec0:4000::/34, + 2403:1ec0:8000::/33, + 2403:1f80::/32, + 2403:2040::/32, + 2403:2080::/32, + 2403:2180::/32, + 2403:2240::/32, + 2403:2280::/32, + 2403:2380::/32, + 2403:2440::/32, + 2403:24c0::/32, + 2403:2580::/32, + 2403:25c0::/32, + 2403:2680::/32, + 2403:2740::/32, + 2403:2780::/32, + 2403:28c0::/32, + 2403:2940::/32, + 2403:2a00::/32, + 2403:2a40::/32, + 2403:2ac0::/32, + 2403:2b40::/32, + 2403:2bc0::/32, + 2403:2cc0::/32, + 2403:2f40::/32, + 2403:2fc0::/32, + 2403:3040::/32, + 2403:30c0::/32, + 2403:3140::/32, + 2403:3280::/32, + 2403:32c0::/32, + 2403:3380::/32, + 2403:3480::/32, + 2403:3580::/32, + 2403:3640::/32, + 2403:3680::/32, + 2403:36c0::/32, + 2403:3740::/32, + 2403:3780::/32, + 2403:37c0::/32, + 2403:3840::/32, + 2403:3880::/32, + 2403:38c0::/32, + 2403:3940::/32, + 2403:3980::/32, + 2403:39c0::/32, + 2403:3b80::/32, + 2403:3c40::/32, + 2403:3c80::/32, + 2403:3cc0::/32, + 2403:3d40::/32, + 2403:3d80::/32, + 2403:3dc0::/32, + 2403:3e80::/32, + 2403:3ec0::/32, + 2403:3f80::/32, + 2403:4080::/32, + 2403:4180::/32, + 2403:4280::/32, + 2403:4300::/32, + 2403:4380::/32, + 2403:4580::/32, + 2403:4680::/32, + 2403:4780::/32, + 2403:4840::/32, + 2403:4880::/32, + 2403:4980::/32, + 2403:4a40::/32, + 2403:4a80::/32, + 2403:4b40::/32, + 2403:4b80::/32, + 2403:4c80::/32, + 2403:4cc0::/32, + 2403:4d80::/32, + 2403:4ec0::/32, + 2403:5080::/32, + 2403:5280::/32, + 2403:5380::/32, + 2403:54c0::/32, + 2403:5540::/32, + 2403:5580::/32, + 2403:5640::/32, + 2403:5780::/32, + 2403:5980::/32, + 2403:5a80::/32, + 2403:5b40::/32, + 2403:5b80::/32, + 2403:5c80::/32, + 2403:5d80::/32, + 2403:5e40::/32, + 2403:5e80::/32, + 2403:5ec0::/32, + 2403:5f80::/32, + 2403:5fc0::/32, + 2403:6080::/32, + 2403:6180::/32, + 2403:6280::/32, + 2403:62c0::/32, + 2403:6380::/42, + 2403:6380:41::/48, + 2403:6380:43::/48, + 2403:6380:44::/46, + 2403:6380:48::/45, + 2403:6380:50::/44, + 2403:6380:60::/43, + 2403:6380:80::/41, + 2403:6380:100::/40, + 2403:6380:200::/39, + 2403:6380:400::/38, + 2403:6380:800::/37, + 2403:6380:1000::/36, + 2403:6380:2000::/35, + 2403:6380:4000::/34, + 2403:6380:8000::/33, + 2403:6580::/32, + 2403:6680::/32, + 2403:6740::/32, + 2403:6780::/32, + 2403:6880::/32, + 2403:6980::/32, + 2403:6a00::/32, + 2403:6c80::/32, + 2403:6d40::/32, + 2403:6d80::/32, + 2403:6e80::/32, + 2403:6f40::/32, + 2403:6fc0::/32, + 2403:7040::/32, + 2403:7080::/32, + 2403:7180::/32, + 2403:7280::/32, + 2403:7380::/32, + 2403:7480::/32, + 2403:7540::/32, + 2403:7580::/32, + 2403:76c0::/32, + 2403:7700::/32, + 2403:78c0::/32, + 2403:7a80::/32, + 2403:7b00::/32, + 2403:7d80::/32, + 2403:7e80::/32, + 2403:7f80::/32, + 2403:8080::/32, + 2403:8180::/32, + 2403:8280::/32, + 2403:8380::/32, + 2403:83c0::/32, + 2403:8480::/32, + 2403:8580::/32, + 2403:8880::/32, + 2403:8900::/32, + 2403:8980::/32, + 2403:8a40::/32, + 2403:8a80::/32, + 2403:8b00::/32, + 2403:8b80::/32, + 2403:8c00::/32, + 2403:8c80::/32, + 2403:8d00::/32, + 2403:8d80::/32, + 2403:9080::/32, + 2403:9180::/32, + 2403:9280::/32, + 2403:9380::/32, + 2403:9480::/32, + 2403:9580::/32, + 2403:9680::/32, + 2403:9780::/32, + 2403:9880::/32, + 2403:9a80::/32, + 2403:9ac0::/32, + 2403:9b00::/32, + 2403:9b40::/32, + 2403:9b80::/32, + 2403:9c80::/32, + 2403:9d00::/32, + 2403:9d80::/32, + 2403:9e40::/32, + 2403:9e80::/32, + 2403:9ec0::/32, + 2403:9f80::/32, + 2403:a100::/32, + 2403:a140::/32, + 2403:a200::/32, + 2403:a300::/32, + 2403:a480::/32, + 2403:a580::/32, + 2403:a680::/32, + 2403:a6c0::/32, + 2403:a780::/32, + 2403:a880::/32, + 2403:a940::/32, + 2403:a980::/32, + 2403:a9c0::/32, + 2403:aa40::/32, + 2403:aa80::/32, + 2403:ab80::/32, + 2403:ac00::/32, + 2403:af80::/32, + 2403:b080::/32, + 2403:b180::/32, + 2403:b280::/32, + 2403:b380::/32, + 2403:b400::/32, + 2403:b480::/32, + 2403:b580::/32, + 2403:b680::/32, + 2403:b780::/32, + 2403:b880::/32, + 2403:b980::/32, + 2403:ba40::/32, + 2403:c040::/32, + 2403:c080::/32, + 2403:c100::/32, + 2403:c140::/32, + 2403:c180::/32, + 2403:c3c0::/32, + 2403:c440::/32, + 2403:c480::/32, + 2403:c4c0::/32, + 2403:c980::/32, + 2403:cdc0::/32, + 2403:cf80::/32, + 2403:d080::/32, + 2403:d180::/32, + 2403:d280::/32, + 2403:d2c0::/32, + 2403:d380::/32, + 2403:d400::/32, + 2403:d440::/32, + 2403:d480::/32, + 2403:d580::/32, + 2403:d680::/32, + 2403:d780::/32, + 2403:d7c0::/32, + 2403:d880::/32, + 2403:d980::/32, + 2403:d9c0::/32, + 2403:da80::/32, + 2403:dac0::/32, + 2403:db00::/32, + 2403:db80::/32, + 2403:dc80::/32, + 2403:dd80::/32, + 2403:de80::/32, + 2403:df80::/32, + 2403:e080::/32, + 2403:e180::/32, + 2403:e280::/32, + 2403:e300::/32, + 2403:e480::/32, + 2403:e500::/32, + 2403:e580::/32, + 2403:e640::/32, + 2403:e680::/32, + 2403:e700::/32, + 2403:e780::/32, + 2403:e7c0::/32, + 2403:e880::/32, + 2403:e980::/32, + 2403:ea80::/32, + 2403:eac0::/32, + 2403:eb80::/32, + 2403:ec80::/32, + 2403:ed00::/32, + 2403:ed40::/32, + 2403:ed80::/32, + 2403:ee80::/32, + 2403:ef80::/32, + 2403:f080::/32, + 2403:f100::/32, + 2403:f180::/32, + 2403:f240::/32, + 2403:f280::/32, + 2403:f300::/32, + 2403:f380::/32, + 2403:f4c0::/32, + 2403:f580::/32, + 2403:f740::/32, + 2403:f8c0::/32, + 2403:f980::/32, + 2403:fb00::/32, + 2403:fb80::/32, + 2403:fc40::/32, + 2403:fe40::/32, + 2403:fe80::/32, + 2403:fec0::/32, + 2403:ff80::/32, + 2403:ffc0::/31, + 2404:100::/32, + 2404:158::/32, + 2404:240::/32, + 2404:280::/32, + 2404:440::/32, + 2404:480::/32, + 2404:680::/32, + 2404:a80::/32, + 2404:b80::/32, + 2404:bc0::/32, + 2404:c40::/32, + 2404:d80::/32, + 2404:f00::/32, + 2404:f80::/32, + 2404:1080::/32, + 2404:10c0::/32, + 2404:1180::/32, + 2404:14c0::/32, + 2404:1880::/32, + 2404:1c80::/32, + 2404:1cc0::/32, + 2404:1d80::/39, + 2404:1d80:200::/44, + 2404:1d80:210::/47, + 2404:1d80:212::/48, + 2404:1d80:214::/46, + 2404:1d80:218::/45, + 2404:1d80:220::/43, + 2404:1d80:240::/42, + 2404:1d80:280::/41, + 2404:1d80:300::/40, + 2404:1d80:400::/38, + 2404:1d80:800::/37, + 2404:1d80:1000::/36, + 2404:1d80:2000::/35, + 2404:1d80:4000::/34, + 2404:1d80:8000::/33, + 2404:1e80::/32, + 2404:1f40::/32, + 2404:21c0::/32, + 2404:30c0::/32, + 2404:3140::/32, + 2404:31c0::/32, + 2404:3240::/32, + 2404:32c0::/32, + 2404:3300::/32, + 2404:3340::/32, + 2404:3480::/32, + 2404:35c0::/32, + 2404:3640::/32, + 2404:36c0::/32, + 2404:3700::/32, + 2404:3740::/32, + 2404:37c0::/32, + 2404:3840::/32, + 2404:3940::/32, + 2404:3f40::/32, + 2404:41c0::/32, + 2404:4540::/32, + 2404:4740::/32, + 2404:4d00::/32, + 2404:4dc0::/32, + 2404:51c0::/32, + 2404:5640::/32, + 2404:5a80::/32, + 2404:5b00::/32, + 2404:5d00::/32, + 2404:6000::/32, + 2404:6100::/32, + 2404:6380::/32, + 2404:6500::/32, + 2404:65c0::/32, + 2404:6a40::/32, + 2404:6f80::/32, + 2404:7100::/32, + 2404:7180::/32, + 2404:71c0::/32, + 2404:7240::/32, + 2404:74c0::/32, + 2404:7600::/32, + 2404:7740::/32, + 2404:7940::/32, + 2404:7d00::/32, + 2404:8040::/32, + 2404:80c0::/32, + 2404:8140::/32, + 2404:81c0::/32, + 2404:8480::/32, + 2404:8580::/32, + 2404:8700::/32, + 2404:8880::/32, + 2404:8a80::/32, + 2404:8dc0::/32, + 2404:9340::/32, + 2404:9b80::/32, + 2404:9c80::/32, + 2404:a000::/32, + 2404:a080::/32, + 2404:a0c0::/32, + 2404:a180::/32, + 2404:a240::/32, + 2404:a740::/32, + 2404:b100::/32, + 2404:b340::/32, + 2404:b3c0::/32, + 2404:b440::/32, + 2404:b4c0::/32, + 2404:b900::/32, + 2404:bbc0::/32, + 2404:bc40::/32, + 2404:c1c0::/32, + 2404:c240::/32, + 2404:c2c0::/32, + 2404:c300::/32, + 2404:c3c0::/32, + 2404:c440::/32, + 2404:c4c0::/32, + 2404:c540::/32, + 2404:c5c0::/32, + 2404:c940::/32, + 2404:c9c0::/32, + 2404:cd00::/32, + 2404:d040::/32, + 2404:d080::/32, + 2404:d140::/32, + 2404:d280::/32, + 2404:d3c0::/32, + 2404:d640::/32, + 2404:d6c0::/32, + 2404:d7c0::/32, + 2404:d840::/32, + 2404:dd80::/32, + 2404:df00::/32, + 2404:e280::/32, + 2404:e540::/32, + 2404:e780::/32, + 2404:e880::/32, + 2404:e8c0::/32, + 2404:eb80::/32, + 2404:ec40::/32, + 2404:ecc0::/32, + 2404:edc0::/32, + 2404:f040::/32, + 2404:f4c0:f000::/44, + 2404:f7c0::/32, + 2405:80::/32, + 2405:480::/32, + 2405:580::/32, + 2405:680::/32, + 2405:6c0:2::/47, + 2405:6c0:4::/46, + 2405:6c0:8::/45, + 2405:6c0:10::/44, + 2405:6c0:20::/43, + 2405:6c0:40::/42, + 2405:6c0:80::/41, + 2405:6c0:100::/40, + 2405:6c0:200::/39, + 2405:6c0:400::/38, + 2405:6c0:800::/37, + 2405:6c0:1000::/36, + 2405:6c0:2000::/35, + 2405:6c0:4000::/34, + 2405:6c0:8000::/33, + 2405:780::/32, + 2405:880::/32, + 2405:940::/32, + 2405:980::/32, + 2405:9c0::/32, + 2405:a80::/32, + 2405:b80::/32, + 2405:c80::/32, + 2405:d80::/32, + 2405:e80::/32, + 2405:f80::/32, + 2405:1080::/32, + 2405:1180::/32, + 2405:1280::/32, + 2405:1380::/32, + 2405:1480::/32, + 2405:1580::/32, + 2405:1680::/32, + 2405:18c0::/32, + 2405:1c80::/32, + 2405:1d80::/32, + 2405:1e80::/32, + 2405:1f80::/32, + 2405:1fc0::/32, + 2405:2080::/32, + 2405:2180::/32, + 2405:2280::/32, + 2405:2340::/32, + 2405:2380::/32, + 2405:2480::/32, + 2405:24c0::/32, + 2405:2580::/32, + 2405:2680::/32, + 2405:2780::/32, + 2405:2880::/32, + 2405:2980::/32, + 2405:2a80::/32, + 2405:2b80::/32, + 2405:2bc0::/32, + 2405:2c80::/32, + 2405:2d80::/32, + 2405:2e80::/32, + 2405:2ec0::/32, + 2405:2f40::/32, + 2405:2f80::/32, + 2405:3140::/32, + 2405:31c0::/32, + 2405:37c0::/32, + 2405:3880::/32, + 2405:3980::/32, + 2405:39c0::/32, + 2405:3a80::/32, + 2405:3ac0::/32, + 2405:3b00::/32, + 2405:3b80::/32, + 2405:3bc0::/32, + 2405:3c40::/32, + 2405:3c80::/32, + 2405:3d80::/32, + 2405:3e80::/32, + 2405:3f40::/32, + 2405:3f80::/32, + 2405:4080::/32, + 2405:4140::/32, + 2405:4180::/32, + 2405:41c0::/32, + 2405:4280::/32, + 2405:4380::/32, + 2405:4480::/32, + 2405:44c0::/32, + 2405:4540::/32, + 2405:4580::/32, + 2405:4680::/32, + 2405:4780::/32, + 2405:4880::/32, + 2405:4980::/32, + 2405:4a80::/32, + 2405:4b80::/32, + 2405:4d40::/32, + 2405:4e80::/32, + 2405:4f80::/32, + 2405:5080::/32, + 2405:5180::/32, + 2405:5240:400::/38, + 2405:5240:c00::/38, + 2405:5240:1000::/36, + 2405:5240:2000::/35, + 2405:5240:4000::/34, + 2405:5240:8000::/33, + 2405:5280::/32, + 2405:52c0::/32, + 2405:5380::/32, + 2405:5480::/32, + 2405:5580::/32, + 2405:5680::/32, + 2405:5780::/32, + 2405:57c0::/32, + 2405:5880::/32, + 2405:5980::/32, + 2405:5a80::/32, + 2405:5b80::/32, + 2405:5c80::/32, + 2405:5cc0::/32, + 2405:5d40::/32, + 2405:5d80::/32, + 2405:5dc0::/32, + 2405:5e80::/32, + 2405:5f80::/32, + 2405:6080::/32, + 2405:6180::/32, + 2405:6200::/32, + 2405:6880::/32, + 2405:68c0::/32, + 2405:6940::/32, + 2405:69c0::/32, + 2405:6a80::/32, + 2405:6b80::/32, + 2405:6c80::/32, + 2405:6d80::/32, + 2405:6e80::/32, + 2405:6f00::/32, + 2405:6f80::/32, + 2405:7040::/32, + 2405:7080::/32, + 2405:7180::/32, + 2405:7280::/32, + 2405:7380::/32, + 2405:7480::/32, + 2405:7580::/32, + 2405:7680::/32, + 2405:7780::/32, + 2405:7880::/32, + 2405:78c0::/32, + 2405:7980::/32, + 2405:79c0::/32, + 2405:7a80::/32, + 2405:7b80::/32, + 2405:7c80::/32, + 2405:7d40::/32, + 2405:7f40::/32, + 2405:7fc0::/32, + 2405:8280::/32, + 2405:8480::/32, + 2405:84c0::/34, + 2405:84c0:4000::/35, + 2405:84c0:6000::/37, + 2405:84c0:6800::/38, + 2405:84c0:6c00::/39, + 2405:84c0:6e00::/40, + 2405:84c0:7000::/36, + 2405:84c0:8000::/48, + 2405:84c0:8002::/48, + 2405:84c0:8006::/48, + 2405:84c0:8008::/45, + 2405:84c0:8010::/48, + 2405:84c0:8019::/48, + 2405:84c0:801c::/46, + 2405:84c0:8020::/47, + 2405:84c0:8036::/47, + 2405:84c0:8038::/46, + 2405:84c0:803c::/47, + 2405:84c0:803e::/48, + 2405:84c0:8041::/48, + 2405:84c0:8042::/47, + 2405:84c0:8044::/47, + 2405:84c0:8048::/46, + 2405:84c0:804c::/47, + 2405:84c0:804e::/48, + 2405:84c0:8050::/48, + 2405:84c0:8053::/48, + 2405:84c0:8058::/45, + 2405:84c0:8060::/43, + 2405:84c0:8080::/41, + 2405:84c0:8100::/40, + 2405:84c0:8200::/39, + 2405:84c0:8400::/38, + 2405:84c0:8800::/37, + 2405:84c0:9000::/36, + 2405:84c0:a000::/40, + 2405:84c0:a101::/48, + 2405:84c0:a102::/47, + 2405:84c0:a104::/46, + 2405:84c0:a108::/45, + 2405:84c0:a110::/44, + 2405:84c0:a120::/43, + 2405:84c0:a140::/42, + 2405:84c0:a180::/41, + 2405:84c0:a200::/39, + 2405:84c0:a400::/38, + 2405:84c0:a800::/37, + 2405:84c0:b000::/36, + 2405:84c0:c000::/34, + 2405:8580::/32, + 2405:8680::/32, + 2405:8780::/32, + 2405:8880::/32, + 2405:8980::/32, + 2405:8a40::/32, + 2405:8a80::/32, + 2405:8ac0::/32, + 2405:8b80::/32, + 2405:8c80::/32, + 2405:8d80::/32, + 2405:8e80::/32, + 2405:8f80::/32, + 2405:9080::/32, + 2405:9180::/32, + 2405:9280::/32, + 2405:9300::/32, + 2405:9340::/32, + 2405:9380::/32, + 2405:93c0::/32, + 2405:9480::/32, + 2405:94c0::/32, + 2405:9580::/32, + 2405:9680::/32, + 2405:9700::/32, + 2405:9780::/32, + 2405:97c0::/32, + 2405:9880::/32, + 2405:9900::/32, + 2405:9980::/32, + 2405:99c0::/32, + 2405:9a80::/32, + 2405:9b00::/32, + 2405:9b80::/32, + 2405:9bc0::/32, + 2405:9e00::/32, + 2405:a240::/32, + 2405:a500::/32, + 2405:a680::/32, + 2405:a900::/32, + 2405:a980::/32, + 2405:aa80::/32, + 2405:ab00::/32, + 2405:ad00::/32, + 2405:af00::/32, + 2405:b100::/32, + 2405:b300::/32, + 2405:b7c0::/32, + 2405:b880::/32, + 2405:b980::/32, + 2405:bb00::/32, + 2405:bd00::/32, + 2405:bd80::/32, + 2405:bdc0::/32, + 2405:be80::/32, + 2405:bf00::/32, + 2405:c040::/32, + 2405:c280::/32, + 2405:c380::/32, + 2405:c480::/32, + 2405:c500::/32, + 2405:c580::/32, + 2405:c680::/32, + 2405:c780::/32, + 2405:c880::/32, + 2405:c980::/32, + 2405:ca80::/32, + 2405:cb80::/32, + 2405:cc80::/32, + 2405:cd80::/32, + 2405:ce80::/32, + 2405:d280::/32, + 2405:d4c0::/32, + 2405:d700::/32, + 2405:d900::/32, + 2405:df40::/32, + 2405:e000::/32, + 2405:e040::/32, + 2405:e1c0::/32, + 2405:e600::/32, + 2405:ef40::/30, + 2405:f340::/32, + 2405:f6c0::/32, + 2405:f940::/32, + 2405:fdc0::/32, + 2406:40::/32, + 2406:80::/32, + 2406:c0::/32, + 2406:140:400::/38, + 2406:140:c00::/38, + 2406:140:1000::/36, + 2406:140:2000::/35, + 2406:140:4000::/34, + 2406:140:8000::/33, + 2406:280::/32, + 2406:440::/32, + 2406:4c0::/32, + 2406:840::/48, + 2406:840:3::/48, + 2406:840:4::/46, + 2406:840:8::/45, + 2406:840:18::/45, + 2406:840:21::/48, + 2406:840:22::/47, + 2406:840:24::/46, + 2406:840:28::/45, + 2406:840:30::/44, + 2406:840:40::/46, + 2406:840:45::/48, + 2406:840:47::/48, + 2406:840:48::/45, + 2406:840:50::/44, + 2406:840:60::/44, + 2406:840:70::/45, + 2406:840:7a::/47, + 2406:840:7c::/46, + 2406:840:82::/47, + 2406:840:84::/46, + 2406:840:88::/45, + 2406:840:91::/48, + 2406:840:92::/47, + 2406:840:94::/46, + 2406:840:98::/45, + 2406:840:a0::/43, + 2406:840:c0::/42, + 2406:840:102::/48, + 2406:840:104::/46, + 2406:840:108::/45, + 2406:840:111::/48, + 2406:840:112::/47, + 2406:840:114::/46, + 2406:840:118::/45, + 2406:840:120::/43, + 2406:840:140::/42, + 2406:840:180::/41, + 2406:840:201::/48, + 2406:840:202::/47, + 2406:840:204::/46, + 2406:840:208::/45, + 2406:840:210::/44, + 2406:840:220::/43, + 2406:840:240::/42, + 2406:840:280::/42, + 2406:840:2c0::/43, + 2406:840:2e1::/48, + 2406:840:2e2::/47, + 2406:840:2e4::/46, + 2406:840:2e8::/45, + 2406:840:2f0::/44, + 2406:840:300::/41, + 2406:840:382::/47, + 2406:840:384::/46, + 2406:840:388::/45, + 2406:840:390::/44, + 2406:840:3a0::/43, + 2406:840:3c0::/47, + 2406:840:3c3::/48, + 2406:840:3c4::/46, + 2406:840:3c8::/45, + 2406:840:3d0::/44, + 2406:840:3e1::/48, + 2406:840:3e2::/47, + 2406:840:3e4::/46, + 2406:840:3e8::/45, + 2406:840:3f0::/44, + 2406:840:400::/44, + 2406:840:412::/47, + 2406:840:414::/46, + 2406:840:418::/45, + 2406:840:421::/48, + 2406:840:422::/47, + 2406:840:424::/46, + 2406:840:428::/45, + 2406:840:430::/44, + 2406:840:440::/42, + 2406:840:481::/48, + 2406:840:482::/47, + 2406:840:484::/46, + 2406:840:488::/45, + 2406:840:490::/44, + 2406:840:4a0::/43, + 2406:840:4c0::/42, + 2406:840:500::/42, + 2406:840:541::/48, + 2406:840:542::/47, + 2406:840:544::/46, + 2406:840:548::/45, + 2406:840:550::/44, + 2406:840:560::/43, + 2406:840:580::/41, + 2406:840:600::/41, + 2406:840:681::/48, + 2406:840:682::/47, + 2406:840:684::/46, + 2406:840:688::/45, + 2406:840:690::/44, + 2406:840:6a0::/43, + 2406:840:6c0::/48, + 2406:840:6c2::/47, + 2406:840:6c4::/46, + 2406:840:6c8::/45, + 2406:840:6d0::/44, + 2406:840:6e0::/43, + 2406:840:700::/44, + 2406:840:711::/48, + 2406:840:712::/47, + 2406:840:714::/46, + 2406:840:718::/45, + 2406:840:720::/43, + 2406:840:740::/42, + 2406:840:780::/41, + 2406:840:804::/47, + 2406:840:807::/48, + 2406:840:808::/45, + 2406:840:810::/44, + 2406:840:820::/43, + 2406:840:840::/48, + 2406:840:843::/48, + 2406:840:844::/46, + 2406:840:848::/45, + 2406:840:850::/44, + 2406:840:861::/48, + 2406:840:862::/47, + 2406:840:864::/46, + 2406:840:868::/45, + 2406:840:870::/44, + 2406:840:884::/46, + 2406:840:888::/45, + 2406:840:890::/44, + 2406:840:8a0::/43, + 2406:840:8c0::/42, + 2406:840:900::/40, + 2406:840:a00::/44, + 2406:840:a12::/47, + 2406:840:a14::/46, + 2406:840:a18::/45, + 2406:840:a20::/44, + 2406:840:a30::/48, + 2406:840:a33::/48, + 2406:840:a34::/46, + 2406:840:a38::/45, + 2406:840:a40::/42, + 2406:840:a80::/41, + 2406:840:b00::/40, + 2406:840:c00::/47, + 2406:840:c03::/48, + 2406:840:c04::/46, + 2406:840:c08::/45, + 2406:840:c10::/44, + 2406:840:c20::/43, + 2406:840:c40::/43, + 2406:840:c61::/48, + 2406:840:c62::/47, + 2406:840:c64::/46, + 2406:840:c68::/45, + 2406:840:c70::/44, + 2406:840:c80::/43, + 2406:840:ca0::/44, + 2406:840:cb0::/45, + 2406:840:cb9::/48, + 2406:840:cba::/47, + 2406:840:cbc::/46, + 2406:840:cc0::/47, + 2406:840:cc3::/48, + 2406:840:cc4::/46, + 2406:840:cc8::/45, + 2406:840:cd0::/44, + 2406:840:ce0::/43, + 2406:840:d00::/40, + 2406:840:e00::/39, + 2406:840:1000::/48, + 2406:840:1002::/47, + 2406:840:1004::/46, + 2406:840:1008::/45, + 2406:840:1010::/44, + 2406:840:1020::/43, + 2406:840:1040::/42, + 2406:840:1080::/41, + 2406:840:1100::/40, + 2406:840:1200::/39, + 2406:840:1400::/38, + 2406:840:1801::/48, + 2406:840:1802::/47, + 2406:840:1804::/46, + 2406:840:1808::/45, + 2406:840:1810::/44, + 2406:840:1820::/43, + 2406:840:1840::/42, + 2406:840:1880::/41, + 2406:840:1900::/40, + 2406:840:1a00::/39, + 2406:840:1c00::/38, + 2406:840:2000::/39, + 2406:840:2200::/40, + 2406:840:2300::/41, + 2406:840:2381::/48, + 2406:840:2382::/47, + 2406:840:2384::/46, + 2406:840:2388::/45, + 2406:840:2390::/44, + 2406:840:23a0::/43, + 2406:840:23c0::/42, + 2406:840:2400::/38, + 2406:840:2801::/48, + 2406:840:2802::/47, + 2406:840:2804::/46, + 2406:840:2808::/45, + 2406:840:2810::/44, + 2406:840:2820::/43, + 2406:840:2840::/42, + 2406:840:2880::/48, + 2406:840:2882::/47, + 2406:840:2884::/46, + 2406:840:2888::/45, + 2406:840:2890::/44, + 2406:840:28a0::/43, + 2406:840:28c0::/42, + 2406:840:2900::/40, + 2406:840:2a00::/39, + 2406:840:2c00::/38, + 2406:840:3000::/37, + 2406:840:3801::/48, + 2406:840:3802::/47, + 2406:840:3804::/46, + 2406:840:3808::/45, + 2406:840:3810::/44, + 2406:840:3820::/43, + 2406:840:3840::/42, + 2406:840:3880::/41, + 2406:840:3900::/40, + 2406:840:3a00::/39, + 2406:840:3c00::/38, + 2406:840:4000::/40, + 2406:840:4102::/47, + 2406:840:4104::/46, + 2406:840:4108::/45, + 2406:840:4110::/44, + 2406:840:4120::/43, + 2406:840:4140::/42, + 2406:840:4180::/41, + 2406:840:4200::/40, + 2406:840:4300::/41, + 2406:840:4380::/42, + 2406:840:43c0::/43, + 2406:840:43e2::/47, + 2406:840:43e4::/46, + 2406:840:43e8::/45, + 2406:840:43f0::/44, + 2406:840:4400::/38, + 2406:840:4802::/47, + 2406:840:4804::/46, + 2406:840:4808::/45, + 2406:840:4810::/44, + 2406:840:4820::/43, + 2406:840:4840::/42, + 2406:840:4882::/47, + 2406:840:4884::/46, + 2406:840:4888::/45, + 2406:840:4890::/44, + 2406:840:48a0::/43, + 2406:840:48c0::/42, + 2406:840:4900::/40, + 2406:840:4a00::/39, + 2406:840:4c00::/38, + 2406:840:5000::/40, + 2406:840:5102::/47, + 2406:840:5104::/46, + 2406:840:5108::/45, + 2406:840:5110::/44, + 2406:840:5120::/43, + 2406:840:5140::/42, + 2406:840:5180::/41, + 2406:840:5200::/39, + 2406:840:5400::/38, + 2406:840:5802::/47, + 2406:840:5804::/46, + 2406:840:5808::/45, + 2406:840:5810::/44, + 2406:840:5820::/43, + 2406:840:5840::/43, + 2406:840:5862::/47, + 2406:840:5864::/46, + 2406:840:5868::/45, + 2406:840:5870::/44, + 2406:840:5882::/47, + 2406:840:5884::/46, + 2406:840:5888::/45, + 2406:840:5890::/44, + 2406:840:58a0::/43, + 2406:840:58c0::/42, + 2406:840:5900::/40, + 2406:840:5a00::/39, + 2406:840:5c02::/47, + 2406:840:5c04::/46, + 2406:840:5c08::/45, + 2406:840:5c10::/44, + 2406:840:5c20::/43, + 2406:840:5c40::/42, + 2406:840:5c80::/41, + 2406:840:5d00::/40, + 2406:840:5e00::/39, + 2406:840:6000::/35, + 2406:840:8000::/40, + 2406:840:8200::/39, + 2406:840:8400::/38, + 2406:840:8800::/37, + 2406:840:9010::/44, + 2406:840:9020::/43, + 2406:840:9040::/42, + 2406:840:9080::/41, + 2406:840:9101::/48, + 2406:840:9103::/48, + 2406:840:9104::/46, + 2406:840:9108::/45, + 2406:840:9112::/47, + 2406:840:9114::/46, + 2406:840:9118::/45, + 2406:840:9120::/43, + 2406:840:9140::/43, + 2406:840:9160::/44, + 2406:840:9170::/45, + 2406:840:9179::/48, + 2406:840:917a::/47, + 2406:840:917c::/46, + 2406:840:9180::/41, + 2406:840:9300::/40, + 2406:840:9400::/39, + 2406:840:9610::/44, + 2406:840:9630::/44, + 2406:840:9640::/43, + 2406:840:9660::/46, + 2406:840:9664::/47, + 2406:840:9667::/48, + 2406:840:9668::/45, + 2406:840:9670::/44, + 2406:840:9680::/41, + 2406:840:9800::/46, + 2406:840:9808::/45, + 2406:840:9810::/45, + 2406:840:9818::/47, + 2406:840:981b::/48, + 2406:840:9820::/43, + 2406:840:9850::/44, + 2406:840:9860::/47, + 2406:840:9865::/48, + 2406:840:9866::/47, + 2406:840:9868::/45, + 2406:840:9870::/44, + 2406:840:9880::/41, + 2406:840:9900::/42, + 2406:840:9940::/43, + 2406:840:9960::/47, + 2406:840:9965::/48, + 2406:840:9968::/46, + 2406:840:996d::/48, + 2406:840:996e::/47, + 2406:840:9981::/48, + 2406:840:9982::/47, + 2406:840:9984::/46, + 2406:840:9988::/45, + 2406:840:99a0::/43, + 2406:840:99c0::/42, + 2406:840:9a00::/39, + 2406:840:9c00::/39, + 2406:840:9e00::/41, + 2406:840:9e80::/42, + 2406:840:9ec0::/43, + 2406:840:9ee0::/44, + 2406:840:9f00::/41, + 2406:840:9f80::/42, + 2406:840:9fc0::/43, + 2406:840:9fe0::/44, + 2406:840:a000::/35, + 2406:840:c000::/37, + 2406:840:c800::/38, + 2406:840:cd00::/40, + 2406:840:ce00::/39, + 2406:840:e000::/41, + 2406:840:e090::/44, + 2406:840:e0a0::/43, + 2406:840:e0c0::/45, + 2406:840:e0c8::/46, + 2406:840:e0cc::/47, + 2406:840:e0ce::/48, + 2406:840:e0d0::/44, + 2406:840:e0e4::/46, + 2406:840:e0e9::/48, + 2406:840:e0ea::/47, + 2406:840:e0ec::/46, + 2406:840:e0f0::/44, + 2406:840:e100::/45, + 2406:840:e108::/46, + 2406:840:e10c::/47, + 2406:840:e10e::/48, + 2406:840:e110::/44, + 2406:840:e120::/43, + 2406:840:e140::/45, + 2406:840:e148::/46, + 2406:840:e14c::/47, + 2406:840:e14e::/48, + 2406:840:e150::/44, + 2406:840:e160::/43, + 2406:840:e190::/44, + 2406:840:e1a0::/43, + 2406:840:e1c0::/42, + 2406:840:e210::/44, + 2406:840:e220::/43, + 2406:840:e240::/43, + 2406:840:e260::/44, + 2406:840:e271::/48, + 2406:840:e272::/47, + 2406:840:e274::/46, + 2406:840:e278::/46, + 2406:840:e27c::/47, + 2406:840:e27e::/48, + 2406:840:e290::/44, + 2406:840:e2a0::/43, + 2406:840:e2c0::/42, + 2406:840:e300::/48, + 2406:840:e305::/48, + 2406:840:e307::/48, + 2406:840:e308::/45, + 2406:840:e310::/44, + 2406:840:e320::/44, + 2406:840:e340::/44, + 2406:840:e350::/48, + 2406:840:e352::/47, + 2406:840:e355::/48, + 2406:840:e356::/47, + 2406:840:e358::/46, + 2406:840:e35c::/48, + 2406:840:e35e::/47, + 2406:840:e360::/43, + 2406:840:e380::/41, + 2406:840:e400::/40, + 2406:840:e500::/42, + 2406:840:e540::/43, + 2406:840:e560::/44, + 2406:840:e570::/45, + 2406:840:e578::/46, + 2406:840:e57c::/47, + 2406:840:e57f::/48, + 2406:840:e580::/41, + 2406:840:e610::/44, + 2406:840:e622::/47, + 2406:840:e624::/46, + 2406:840:e628::/45, + 2406:840:e630::/44, + 2406:840:e640::/43, + 2406:840:e660::/46, + 2406:840:e664::/47, + 2406:840:e668::/45, + 2406:840:e670::/44, + 2406:840:e680::/41, + 2406:840:e700::/43, + 2406:840:e730::/44, + 2406:840:e740::/42, + 2406:840:e780::/41, + 2406:840:e800::/45, + 2406:840:e808::/46, + 2406:840:e80c::/47, + 2406:840:e80e::/48, + 2406:840:e810::/44, + 2406:840:e820::/43, + 2406:840:e840::/45, + 2406:840:e848::/46, + 2406:840:e84c::/47, + 2406:840:e84e::/48, + 2406:840:e850::/44, + 2406:840:e860::/43, + 2406:840:e880::/41, + 2406:840:e900::/40, + 2406:840:ea00::/40, + 2406:840:eb06::/48, + 2406:840:eb09::/48, + 2406:840:eb0a::/48, + 2406:840:eb0c::/47, + 2406:840:eb0e::/48, + 2406:840:eb10::/44, + 2406:840:eb20::/43, + 2406:840:eb40::/42, + 2406:840:ebc0::/42, + 2406:840:ec00::/41, + 2406:840:ec80::/42, + 2406:840:ecc0::/43, + 2406:840:ecf0::/44, + 2406:840:ed00::/40, + 2406:840:ee00::/42, + 2406:840:ee42::/47, + 2406:840:ee45::/48, + 2406:840:ee46::/47, + 2406:840:ee48::/47, + 2406:840:ee4a::/48, + 2406:840:ee4c::/48, + 2406:840:ee4e::/47, + 2406:840:ee50::/44, + 2406:840:ee60::/43, + 2406:840:ee80::/43, + 2406:840:eea0::/44, + 2406:840:eeb0::/48, + 2406:840:eeb3::/48, + 2406:840:eeb4::/46, + 2406:840:eeb8::/45, + 2406:840:eec0::/43, + 2406:840:eee0::/46, + 2406:840:eee4::/48, + 2406:840:eee6::/47, + 2406:840:eee8::/45, + 2406:840:eef0::/44, + 2406:840:ef00::/41, + 2406:840:ef80::/42, + 2406:840:efc0::/43, + 2406:840:efe0::/44, + 2406:840:f000::/41, + 2406:840:f080::/43, + 2406:840:f0a0::/48, + 2406:840:f0a2::/47, + 2406:840:f0a4::/46, + 2406:840:f0a8::/47, + 2406:840:f0ab::/48, + 2406:840:f0ac::/46, + 2406:840:f0b0::/44, + 2406:840:f0c0::/42, + 2406:840:f101::/48, + 2406:840:f102::/47, + 2406:840:f104::/46, + 2406:840:f108::/45, + 2406:840:f110::/44, + 2406:840:f120::/43, + 2406:840:f140::/42, + 2406:840:f180::/41, + 2406:840:f200::/40, + 2406:840:f300::/43, + 2406:840:f320::/44, + 2406:840:f330::/46, + 2406:840:f334::/47, + 2406:840:f336::/48, + 2406:840:f338::/45, + 2406:840:f340::/42, + 2406:840:f390::/44, + 2406:840:f3a0::/43, + 2406:840:f3c0::/42, + 2406:840:f400::/48, + 2406:840:f403::/48, + 2406:840:f404::/46, + 2406:840:f408::/45, + 2406:840:f410::/44, + 2406:840:f420::/43, + 2406:840:f440::/45, + 2406:840:f448::/46, + 2406:840:f44c::/47, + 2406:840:f44e::/48, + 2406:840:f450::/44, + 2406:840:f460::/43, + 2406:840:f480::/41, + 2406:840:f500::/40, + 2406:840:f600::/42, + 2406:840:f640::/43, + 2406:840:f670::/44, + 2406:840:f680::/47, + 2406:840:f682::/48, + 2406:840:f684::/46, + 2406:840:f688::/45, + 2406:840:f690::/44, + 2406:840:f6a0::/43, + 2406:840:f6c0::/42, + 2406:840:f700::/40, + 2406:840:f800::/42, + 2406:840:f840::/43, + 2406:840:f864::/46, + 2406:840:f86a::/47, + 2406:840:f86d::/48, + 2406:840:f86e::/47, + 2406:840:f870::/44, + 2406:840:f880::/45, + 2406:840:f888::/48, + 2406:840:f88a::/47, + 2406:840:f88c::/46, + 2406:840:f890::/44, + 2406:840:f8a0::/43, + 2406:840:f8c0::/42, + 2406:840:f900::/41, + 2406:840:f980::/44, + 2406:840:f9a0::/43, + 2406:840:f9c0::/42, + 2406:840:fa00::/48, + 2406:840:fa02::/47, + 2406:840:fa04::/46, + 2406:840:fa08::/45, + 2406:840:fa10::/44, + 2406:840:fa20::/43, + 2406:840:fa41::/48, + 2406:840:fa42::/47, + 2406:840:fa44::/46, + 2406:840:fa48::/45, + 2406:840:fa50::/44, + 2406:840:fa70::/44, + 2406:840:fa81::/48, + 2406:840:fa82::/47, + 2406:840:fa84::/46, + 2406:840:fa88::/45, + 2406:840:fa90::/44, + 2406:840:faa0::/43, + 2406:840:fac0::/42, + 2406:840:fb00::/40, + 2406:840:fc14::/46, + 2406:840:fc18::/45, + 2406:840:fc24::/46, + 2406:840:fc28::/45, + 2406:840:fc34::/46, + 2406:840:fc38::/45, + 2406:840:fc44::/46, + 2406:840:fc48::/45, + 2406:840:fc54::/46, + 2406:840:fc58::/45, + 2406:840:fc64::/46, + 2406:840:fc68::/45, + 2406:840:fc74::/46, + 2406:840:fc78::/45, + 2406:840:fc80::/44, + 2406:840:fc94::/46, + 2406:840:fc98::/45, + 2406:840:fca4::/46, + 2406:840:fca8::/45, + 2406:840:fcb4::/46, + 2406:840:fcb8::/45, + 2406:840:fcc0::/44, + 2406:840:fcd1::/48, + 2406:840:fcd2::/47, + 2406:840:fcd4::/46, + 2406:840:fcd8::/45, + 2406:840:fce0::/44, + 2406:840:fcf6::/47, + 2406:840:fcf8::/45, + 2406:840:fd00::/43, + 2406:840:fd20::/44, + 2406:840:fd31::/48, + 2406:840:fd32::/47, + 2406:840:fd34::/46, + 2406:840:fd38::/45, + 2406:840:fd41::/48, + 2406:840:fd42::/47, + 2406:840:fd44::/48, + 2406:840:fd47::/48, + 2406:840:fd4a::/47, + 2406:840:fd4c::/46, + 2406:840:fd51::/48, + 2406:840:fd52::/47, + 2406:840:fd54::/48, + 2406:840:fd57::/48, + 2406:840:fd5a::/47, + 2406:840:fd5c::/46, + 2406:840:fd61::/48, + 2406:840:fd62::/47, + 2406:840:fd64::/48, + 2406:840:fd67::/48, + 2406:840:fd6a::/47, + 2406:840:fd6c::/46, + 2406:840:fd71::/48, + 2406:840:fd72::/47, + 2406:840:fd74::/48, + 2406:840:fd77::/48, + 2406:840:fd7a::/47, + 2406:840:fd7c::/46, + 2406:840:fd81::/48, + 2406:840:fd82::/47, + 2406:840:fd84::/48, + 2406:840:fd87::/48, + 2406:840:fd8a::/47, + 2406:840:fd8c::/46, + 2406:840:fd94::/46, + 2406:840:fd98::/46, + 2406:840:fd9c::/47, + 2406:840:fd9e::/48, + 2406:840:fda1::/48, + 2406:840:fda2::/47, + 2406:840:fda4::/46, + 2406:840:fda8::/45, + 2406:840:fdb1::/48, + 2406:840:fdb2::/47, + 2406:840:fdb4::/46, + 2406:840:fdb8::/45, + 2406:840:fdc1::/48, + 2406:840:fdc2::/47, + 2406:840:fdc4::/46, + 2406:840:fdc8::/45, + 2406:840:fdd0::/44, + 2406:840:fde0::/43, + 2406:840:fe00::/43, + 2406:840:fe20::/46, + 2406:840:fe24::/47, + 2406:840:fe26::/48, + 2406:840:fe28::/45, + 2406:840:fe30::/44, + 2406:840:fe40::/44, + 2406:840:fe52::/47, + 2406:840:fe54::/46, + 2406:840:fe58::/45, + 2406:840:fe60::/43, + 2406:840:fe80::/44, + 2406:840:fe95::/48, + 2406:840:fe9c::/48, + 2406:840:fe9f::/48, + 2406:840:fea0::/48, + 2406:840:fea9::/48, + 2406:840:feb0::/47, + 2406:840:feb2::/48, + 2406:840:feb4::/46, + 2406:840:feb8::/45, + 2406:840:fec1::/48, + 2406:840:fec6::/47, + 2406:840:fec8::/47, + 2406:840:fed0::/48, + 2406:840:fed2::/48, + 2406:840:fed5::/48, + 2406:840:fed6::/48, + 2406:840:fed9::/48, + 2406:840:feda::/48, + 2406:840:fede::/48, + 2406:840:fee0::/45, + 2406:840:fee8::/46, + 2406:840:feec::/48, + 2406:840:feee::/47, + 2406:840:fef1::/48, + 2406:840:ff00::/40, + 2406:880::/32, + 2406:8c0::/32, + 2406:d80::/32, + 2406:e80::/32, + 2406:f80::/32, + 2406:1080::/32, + 2406:1100::/32, + 2406:1180::/32, + 2406:1280::/32, + 2406:1380::/32, + 2406:1480::/32, + 2406:1580::/32, + 2406:15c0::/32, + 2406:1680::/32, + 2406:1780::/32, + 2406:1880::/32, + 2406:1980::/32, + 2406:1a80::/32, + 2406:1b80::/32, + 2406:1c80::/32, + 2406:1d80::/32, + 2406:1e40::/32, + 2406:1e80::/32, + 2406:1f80::/32, + 2406:2080::/32, + 2406:2640::/32, + 2406:2700::/32, + 2406:2780::/32, + 2406:2880::/32, + 2406:2980::/32, + 2406:2a80::/32, + 2406:2b80::/32, + 2406:2c40::/32, + 2406:2c80::/32, + 2406:2d80::/32, + 2406:2e80::/32, + 2406:2f80::/32, + 2406:3080::/32, + 2406:3180::/32, + 2406:31c0::/32, + 2406:3280::/32, + 2406:3300::/32, + 2406:3340::/32, + 2406:3380::/32, + 2406:3480::/32, + 2406:34c0::/32, + 2406:3580::/32, + 2406:3640::/32, + 2406:3680::/32, + 2406:3700::/32, + 2406:3780::/32, + 2406:3880::/32, + 2406:3980::/32, + 2406:39c0::/32, + 2406:3ac0::/32, + 2406:3e80::/32, + 2406:3f80::/32, + 2406:4080::/32, + 2406:40c0::/32, + 2406:4180::/32, + 2406:4280::/32, + 2406:42c0::/32, + 2406:4340::/32, + 2406:4380::/32, + 2406:43c0::/32, + 2406:4480::/32, + 2406:4500::/32, + 2406:4680::/32, + 2406:4b80::/32, + 2406:4c80::/32, + 2406:4d00::/32, + 2406:4d80::/32, + 2406:4e80::/32, + 2406:4f00::/32, + 2406:4f80::/32, + 2406:5080::/32, + 2406:50c0::/32, + 2406:5180::/32, + 2406:5280::/32, + 2406:52c0::/32, + 2406:5340::/32, + 2406:5380::/32, + 2406:5480::/32, + 2406:5580::/32, + 2406:5680::/32, + 2406:5780::/32, + 2406:5840::/31, + 2406:5940::/32, + 2406:5980::/32, + 2406:5a40::/32, + 2406:5ac0::/32, + 2406:5b40::/32, + 2406:5d80::/32, + 2406:5e80::/32, + 2406:5f80::/32, + 2406:6080::/32, + 2406:6100::/32, + 2406:6180::/32, + 2406:61c0::/29, + 2406:6280::/32, + 2406:6300::/32, + 2406:6340::/32, + 2406:6380::/32, + 2406:6480::/32, + 2406:6500::/32, + 2406:6580::/32, + 2406:65c0::/32, + 2406:6640::/32, + 2406:6680::/32, + 2406:6780::/32, + 2406:6880::/32, + 2406:6980::/32, + 2406:6a80::/32, + 2406:6b80::/32, + 2406:6bc0::/32, + 2406:6c80::/32, + 2406:6d80::/32, + 2406:6e80::/32, + 2406:6f80::/32, + 2406:7080::/32, + 2406:7280::/32, + 2406:7380::/32, + 2406:7480::/32, + 2406:7580::/32, + 2406:7680::/32, + 2406:7780::/32, + 2406:7880::/32, + 2406:7980::/32, + 2406:7a80::/32, + 2406:7b80::/32, + 2406:7c80::/32, + 2406:7d00::/32, + 2406:7d80::/32, + 2406:7e80::/32, + 2406:7f80::/32, + 2406:7fc0::/32, + 2406:8080::/32, + 2406:8180::/32, + 2406:8280::/32, + 2406:8380::/32, + 2406:8480::/32, + 2406:8500::/32, + 2406:8580::/32, + 2406:8780::/32, + 2406:8880::/32, + 2406:8980::/32, + 2406:8a80::/32, + 2406:8b80::/32, + 2406:8c80::/32, + 2406:8d80::/32, + 2406:8e80::/32, + 2406:8f40::/32, + 2406:8f80::/32, + 2406:9200::/32, + 2406:9380::/32, + 2406:9480::/32, + 2406:94c0::/32, + 2406:9780::/32, + 2406:9d80::/32, + 2406:9e80::/32, + 2406:9f80::/32, + 2406:a080::/32, + 2406:a180::/32, + 2406:a280::/32, + 2406:a380::/32, + 2406:a480::/32, + 2406:a580::/32, + 2406:a680::/32, + 2406:a780::/32, + 2406:a7c0::/32, + 2406:a880::/32, + 2406:a8c0::/32, + 2406:a980::/32, + 2406:aa80::/32, + 2406:aac0::/32, + 2406:ab80::/32, + 2406:ac80::/32, + 2406:acc0::/32, + 2406:ad40::/32, + 2406:ad80::/32, + 2406:ae80::/32, + 2406:af80::/32, + 2406:b080::/32, + 2406:b640::/32, + 2406:b880::/32, + 2406:b980::/32, + 2406:ba80::/32, + 2406:bb80::/32, + 2406:bc80::/32, + 2406:bd40::/32, + 2406:bd80::/32, + 2406:bdc0::/32, + 2406:be80::/32, + 2406:bf80::/32, + 2406:c080::/32, + 2406:c180::/32, + 2406:c280::/32, + 2406:c340::/32, + 2406:c480::/32, + 2406:c580::/32, + 2406:c680::/32, + 2406:c780::/32, + 2406:c880::/32, + 2406:c900::/32, + 2406:c980::/32, + 2406:ca80::/32, + 2406:cac0::/32, + 2406:cb80::/32, + 2406:cc80::/32, + 2406:cd80::/32, + 2406:ce80::/32, + 2406:cf00::/30, + 2406:cf80::/32, + 2406:d080::/32, + 2406:d140::/33, + 2406:d140:c000::/34, + 2406:d180::/32, + 2406:d280::/32, + 2406:d2c0::/32, + 2406:d380::/32, + 2406:d440::/32, + 2406:d480::/32, + 2406:d580::/32, + 2406:d680::/32, + 2406:d780::/32, + 2406:d880::/32, + 2406:d980::/32, + 2406:db80::/32, + 2406:dc80::/32, + 2406:dd00::/32, + 2406:dd80:400::/38, + 2406:dd80:c00::/38, + 2406:dd80:1000::/36, + 2406:dd80:2000::/35, + 2406:dd80:4000::/34, + 2406:dd80:8000::/33, + 2406:de80::/32, + 2406:df80::/32, + 2406:e080::/32, + 2406:e180::/32, + 2406:e2c0::/32, + 2406:e380::/32, + 2406:e3c0::/32, + 2406:e500::/32, + 2406:e580::/32, + 2406:e680::/32, + 2406:e780::/32, + 2406:e8c0::/32, + 2406:ea40::/28, + 2406:f280::/32, + 2406:f300::/32, + 2406:f4c0::/32, + 2406:f7c0::/32, + 2406:f980::/32, + 2406:fc80::/32, + 2406:fd80::/32, + 2406:fe80::/32, + 2406:ff00::/32, + 2407:480::/32, + 2407:580::/32, + 2407:cc0::/32, + 2407:f40::/32, + 2407:17c0::/32, + 2407:1900::/32, + 2407:1d00::/32, + 2407:2280::/32, + 2407:2380::/32, + 2407:23c0::/32, + 2407:2780::/32, + 2407:2840::/32, + 2407:2ac0::/32, + 2407:31c0::/32, + 2407:3340::/32, + 2407:3540::/32, + 2407:3700::/32, + 2407:3740::/32, + 2407:37c0::/32, + 2407:3900::/32, + 2407:3f40::/32, + 2407:43c0::/32, + 2407:4440::/32, + 2407:4580::/32, + 2407:4680::/32, + 2407:4740::/32, + 2407:4880::/32, + 2407:4980::/32, + 2407:4a80::/32, + 2407:4c80::/32, + 2407:4d80::/32, + 2407:4e80::/32, + 2407:4f00::/32, + 2407:5380::/32, + 2407:53c0::/32, + 2407:5500::/32, + 2407:5780::/32, + 2407:5840::/32, + 2407:6040::/32, + 2407:6580::/32, + 2407:6c40::/32, + 2407:7680::/32, + 2407:7780::/32, + 2407:7880::/32, + 2407:7980::/32, + 2407:7c80::/32, + 2407:7d00::/32, + 2407:7d80::/32, + 2407:7e80::/32, + 2407:8880::/32, + 2407:8b80::/32, + 2407:8f40::/32, + 2407:9080::/32, + 2407:9180::/32, + 2407:94c0::/32, + 2407:9680::/32, + 2407:9980::/32, + 2407:9b40::/32, + 2407:9bc0::/32, + 2407:9f00::/32, + 2407:9f80::/32, + 2407:a040::/32, + 2407:a640::/32, + 2407:a7c0::/32, + 2407:a880::/32, + 2407:a940::/32, + 2407:ad80::/32, + 2407:ae80::/32, + 2407:af80::/32, + 2407:b080::/32, + 2407:b180::/32, + 2407:b280::/32, + 2407:b380::/32, + 2407:b580::/32, + 2407:b680::/32, + 2407:b780::/32, + 2407:b880::/32, + 2407:b980::/32, + 2407:ba00::/32, + 2407:ba80::/32, + 2407:bb80::/32, + 2407:bc00::/32, + 2407:bc80::/32, + 2407:bd80::/32, + 2407:bdc0::/32, + 2407:be80::/32, + 2407:bf80::/32, + 2407:c080::/32, + 2407:c380::/32, + 2407:c400::/32, + 2407:c480::/32, + 2407:c580::/32, + 2407:c680::/32, + 2407:c780::/32, + 2407:c880::/32, + 2407:c900::/32, + 2407:c980::/32, + 2407:cb80::/32, + 2407:cc80::/32, + 2407:cd80::/32, + 2407:ce80::/32, + 2407:cf80::/32, + 2407:d480::/32, + 2407:d580::/32, + 2407:d680::/32, + 2407:d780::/32, + 2407:d7c0::/32, + 2407:d880::/32, + 2407:d8c0::/32, + 2407:d980::/32, + 2407:d9c0::/32, + 2407:da80::/32, + 2407:db80::/32, + 2407:dc80::/32, + 2407:dd80::/32, + 2407:de80::/32, + 2407:df80::/32, + 2407:dfc0::/32, + 2407:e080::/32, + 2407:e180::/32, + 2407:e280::/32, + 2407:e380::/32, + 2407:e480::/32, + 2407:e580::/32, + 2407:e680::/32, + 2407:e780::/32, + 2407:e800::/32, + 2407:ea80::/32, + 2407:eb80::/32, + 2407:ec40::/32, + 2407:ec80::/32, + 2407:ecc0::/32, + 2407:ed80::/32, + 2407:ee80::/32, + 2407:ef80::/32, + 2407:f080::/32, + 2407:f180::/32, + 2407:f280::/32, + 2407:f380::/32, + 2407:f480::/32, + 2407:f580::/32, + 2407:f680::/32, + 2407:f780::/32, + 2407:f880::/32, + 2407:f980::/32, + 2407:fa80::/32, + 2407:fb80::/32, + 2407:fc80::/32, + 2407:fd80::/32, + 2408:4000::/40, + 2408:4000:100::/47, + 2408:4000:103::/48, + 2408:4000:104::/46, + 2408:4000:108::/45, + 2408:4000:110::/44, + 2408:4000:120::/43, + 2408:4000:140::/42, + 2408:4000:180::/41, + 2408:4000:200::/39, + 2408:4000:400::/38, + 2408:4000:800::/37, + 2408:4000:1000::/36, + 2408:4000:2000::/35, + 2408:4000:4000::/34, + 2408:4000:8000::/33, + 2408:4001::/32, + 2408:4002::/31, + 2408:4004::/30, + 2408:4008::/29, + 2408:4010::/30, + 2408:4014::/31, + 2408:4016:1::/48, + 2408:4016:2::/47, + 2408:4016:4::/46, + 2408:4016:8::/45, + 2408:4016:10::/44, + 2408:4016:20::/43, + 2408:4016:40::/42, + 2408:4016:80::/41, + 2408:4016:100::/40, + 2408:4016:200::/39, + 2408:4016:400::/38, + 2408:4016:800::/37, + 2408:4016:1000::/36, + 2408:4016:2000::/35, + 2408:4016:4000::/34, + 2408:4016:8000::/33, + 2408:4017::/32, + 2408:4018::/29, + 2408:4020::/27, + 2408:4040::/26, + 2408:4080::/25, + 2408:4100::/24, + 2408:4200::/23, + 2408:6000::/24, + 2408:8000::/20, + 2409:1000::/20, + 2409:2000::/21, + 2409:6000::/20, + 2409:8000::/21, + 2409:8800::/22, + 2409:8c00::/25, + 2409:8c80::/30, + 2409:8c84::/32, + 2409:8c85::/34, + 2409:8c85:4000::/36, + 2409:8c85:5000::/38, + 2409:8c85:5400::/43, + 2409:8c85:5420::/44, + 2409:8c85:5430::/47, + 2409:8c85:5433::/48, + 2409:8c85:5434::/46, + 2409:8c85:5438::/45, + 2409:8c85:5440::/42, + 2409:8c85:5480::/41, + 2409:8c85:5500::/40, + 2409:8c85:5600::/39, + 2409:8c85:5800::/37, + 2409:8c85:6000::/35, + 2409:8c85:8000::/33, + 2409:8c86::/31, + 2409:8c88::/29, + 2409:8c90::/28, + 2409:8ca0::/27, + 2409:8cc0::/26, + 2409:8d00::/24, + 2409:8e00::/23, + 240a:2000::/24, + 240a:4000::/21, + 240a:6000::/24, + 240a:8000::/21, + 240a:a000::/20, + 240a:c000::/20, + 240b:2000::/22, + 240b:6000::/20, + 240b:8000::/21, + 240b:a000::/25, + 240b:e000::/26, + 240c:0:1::/48, + 240c:0:2::/47, + 240c:0:4::/46, + 240c:0:8::/45, + 240c:0:10::/44, + 240c:0:20::/43, + 240c:0:40::/42, + 240c:0:80::/41, + 240c:0:100::/40, + 240c:0:200::/39, + 240c:0:400::/38, + 240c:0:800::/37, + 240c:0:1000::/36, + 240c:0:2000::/35, + 240c:0:4000::/34, + 240c:0:8000::/33, + 240c:6:2::/48, + 240c:6:4::/46, + 240c:6:8::/45, + 240c:6:10::/44, + 240c:6:20::/43, + 240c:6:40::/42, + 240c:6:80::/41, + 240c:6:100::/40, + 240c:6:200::/39, + 240c:6:400::/38, + 240c:6:800::/37, + 240c:6:1000::/36, + 240c:6:2000::/35, + 240c:6:4000::/34, + 240c:6:8000::/33, + 240c:f::/48, + 240c:f:2::/47, + 240c:f:4::/46, + 240c:f:8::/45, + 240c:f:10::/44, + 240c:f:20::/43, + 240c:f:40::/42, + 240c:f:80::/41, + 240c:f:100::/40, + 240c:f:200::/39, + 240c:f:400::/38, + 240c:f:800::/37, + 240c:f:1000::/36, + 240c:f:2000::/35, + 240c:f:4000::/34, + 240c:f:8000::/33, + 240c:4000::/22, + 240c:8000::/21, + 240c:c000::/20, + 240d:4000::/21, + 240d:8000::/24, + 240e::/21, + 240e:800::/24, + 240e:900::/26, + 240e:940::/27, + 240e:960::/28, + 240e:970::/29, + 240e:978::/30, + 240e:97c::/32, + 240e:97d::/33, + 240e:97d:8001::/48, + 240e:97d:8002::/47, + 240e:97d:8004::/46, + 240e:97d:8008::/45, + 240e:97d:8010::/44, + 240e:97d:8020::/43, + 240e:97d:8040::/42, + 240e:97d:8080::/41, + 240e:97d:8100::/40, + 240e:97d:8200::/39, + 240e:97d:8400::/38, + 240e:97d:8800::/37, + 240e:97d:9000::/36, + 240e:97d:a000::/35, + 240e:97d:c000::/34, + 240e:97e::/31, + 240e:980::/25, + 240e:a00::/23, + 240e:c00::/22, + 240e:1000::/20, + 240e:2000::/19, + 240f:4000::/24, + 240f:8000::/24, + 240f:c000::/24, + 2602:f70d::/36, + 2605:9d80:8001::/48, + 2605:9d80:8011::/48, + 2605:9d80:8021::/48, + 2605:9d80:8031::/48, + 2605:9d80:8041::/48, + 2605:9d80:8081::/48, + 2605:9d80:9003::/48, + 2605:9d80:9013::/48, + 2605:9d80:9023::/48, + 2605:9d80:9033::/48, + 2605:9d80:9042::/48, + 2605:9d80:9071::/48, + 2605:9d80:9092::/48, + 2620:57:4004::/47, + 2804:1e48:9002::/48, + 2a03:f900::/29, + 2a04:3e00:1002::/48, + 2a04:f580:8010::/47, + 2a04:f580:8090::/48, + 2a04:f580:8210::/47, + 2a04:f580:8290::/48, + 2a04:f580:9010::/48, + 2a04:f580:9012::/47, + 2a04:f580:9020::/48, + 2a04:f580:9030::/48, + 2a04:f580:9040::/48, + 2a04:f580:9050::/48, + 2a04:f580:9060::/48, + 2a04:f580:9070::/48, + 2a04:f580:9080::/48, + 2a04:f580:9210::/48, + 2a04:f580:9212::/47, + 2a04:f580:9220::/48, + 2a04:f580:9230::/48, + 2a04:f580:9240::/48, + 2a04:f580:9250::/48, + 2a04:f580:9260::/48, + 2a04:f580:9270::/48, + 2a04:f580:9280::/48, + 2a04:f580:9290::/48, + 2a05:1085::/32, + 2a0a:2840::/29, + 2a0a:d680::/37, + 2a0a:d680:800::/39, + 2a0a:d680:a00::/43, + 2a0a:d680:a20::/44, + 2a0a:d680:a30::/48, + 2a0a:d680:a32::/47, + 2a0a:d680:a34::/46, + 2a0a:d680:a38::/45, + 2a0a:d680:a40::/42, + 2a0a:d680:a80::/41, + 2a0a:d680:b00::/40, + 2a0a:d680:c00::/38, + 2a0a:d680:1000::/36, + 2a0a:d680:2000::/35, + 2a0a:d680:4000::/34, + 2a0a:d680:8000::/40, + 2a0a:d680:8102::/47, + 2a0a:d680:8104::/46, + 2a0a:d680:8108::/45, + 2a0a:d680:8110::/44, + 2a0a:d680:8120::/43, + 2a0a:d680:8140::/42, + 2a0a:d680:8180::/41, + 2a0a:d680:8200::/39, + 2a0a:d680:8400::/38, + 2a0a:d680:8800::/37, + 2a0a:d680:9000::/36, + 2a0a:d680:a000::/35, + 2a0a:d680:c000::/34, + 2a0a:d681::/33, + 2a0a:d681:8000::/34, + 2a0a:d681:c000::/35, + 2a0a:d681:e100::/40, + 2a0a:d681:e200::/39, + 2a0a:d681:e400::/38, + 2a0a:d681:e800::/37, + 2a0a:d681:f000::/36, + 2a0a:d682::/33, + 2a0a:d682:8000::/34, + 2a0a:d682:c000::/36, + 2a0a:d683::/32, + 2a0a:d684::/32, + 2a0a:d685::/40, + 2a0a:d685:100::/41, + 2a0a:d685:180::/42, + 2a0a:d685:1c0::/43, + 2a0a:d685:1e2::/47, + 2a0a:d685:1e4::/46, + 2a0a:d685:1e8::/45, + 2a0a:d685:1f0::/45, + 2a0a:d685:1f8::/47, + 2a0a:d685:1fa::/48, + 2a0a:d685:202::/47, + 2a0a:d685:204::/46, + 2a0a:d685:208::/45, + 2a0a:d685:210::/44, + 2a0a:d685:220::/43, + 2a0a:d685:240::/42, + 2a0a:d685:280::/41, + 2a0a:d685:400::/38, + 2a0a:d685:800::/37, + 2a0a:d685:1000::/36, + 2a0a:d685:2000::/35, + 2a0a:d685:4000::/34, + 2a0a:d685:8000::/33, + 2a0a:d686::/32, + 2a0a:d687::/33, + 2a0a:d687:8000::/34, + 2a0a:d687:c000::/35, + 2a0a:d687:e000::/36, + 2a0a:d687:f000::/48, + 2a0a:d687:f002::/47, + 2a0a:d687:f006::/48, + 2a0a:d687:f009::/48, + 2a0a:d687:f00a::/47, + 2a0a:d687:f00c::/46, + 2a0a:d687:f010::/44, + 2a0a:d687:f020::/43, + 2a0a:d687:f040::/42, + 2a0a:d687:f080::/41, + 2a0a:d687:f100::/40, + 2a0a:d687:f200::/39, + 2a0a:d687:f400::/38, + 2a0a:d687:f800::/37, + 2a0c:b641:573::/48, + 2a0e:7580::/32, + 2a0e:7582::/31, + 2a0e:7584::/30, + 2a0e:aa06:49f::/48, + 2a0e:b107:740::/44, + 2a13:1800:1::/48, + 2a13:1800:2::/47, + 2a13:1800:4::/46, + 2a13:1800:8::/45, + 2a13:1800:11::/48, + 2a13:1800:12::/47, + 2a13:1800:14::/46, + 2a13:1800:18::/45, + 2a13:1800:20::/43, + 2a13:1800:40::/42, + 2a13:1800:90::/44, + 2a13:1800:a0::/43, + 2a13:1800:c0::/42, + 2a13:1800:100::/40, + 2a13:1800:200::/40, + 2a13:1800:310::/44, + 2a13:1800:320::/43, + 2a13:1800:340::/42, + 2a13:1800:380::/41, + 2a13:1800:400::/38, + 2a13:1800:800::/37, + 2a13:1800:1000::/36, + 2a13:1800:2000::/35, + 2a13:1800:4000::/34, + 2a13:1800:8000::/33, + 2a13:1801::/40, + 2a13:1801:100::/41, + 2a13:1801:1a0::/43, + 2a13:1801:1c0::/42, + 2a13:1801:210::/44, + 2a13:1801:220::/43, + 2a13:1801:240::/42, + 2a13:1801:280::/41, + 2a13:1801:300::/40, + 2a13:1801:400::/38, + 2a13:1801:800::/37, + 2a13:1801:1000::/36, + 2a13:1801:2000::/35, + 2a13:1801:4000::/34, + 2a13:1801:8000::/33, + 2a13:1802:20::/43, + 2a13:1802:40::/42, + 2a13:1802:80::/41, + 2a13:1802:100::/40, + 2a13:1802:200::/39, + 2a13:1802:400::/38, + 2a13:1802:800::/37, + 2a13:1802:1000::/36, + 2a13:1802:2000::/35, + 2a13:1802:4000::/34, + 2a13:1802:8000::/34, + 2a13:1802:c000::/44, + 2a13:1802:c020::/43, + 2a13:1802:c040::/42, + 2a13:1802:c080::/41, + 2a13:1802:c100::/40, + 2a13:1802:c200::/39, + 2a13:1802:c400::/38, + 2a13:1802:c800::/37, + 2a13:1802:d000::/36, + 2a13:1802:e000::/35, + 2a13:1803::/32, + 2a13:1804::/30, + 2a13:8b40::/29, + 2c0f:f7a8:8011::/48, + 2c0f:f7a8:8050::/48, + 2c0f:f7a8:805f::/48, + 2c0f:f7a8:8150::/48, + 2c0f:f7a8:815f::/48, + 2c0f:f7a8:8211::/48, + 2c0f:f7a8:9041::/48 + } +} diff --git a/clashoo/files/usr/share/clashoo/nftables/geoip_cn.nft b/clashoo/files/usr/share/clashoo/nftables/geoip_cn.nft new file mode 100644 index 000000000..0a0ed205f --- /dev/null +++ b/clashoo/files/usr/share/clashoo/nftables/geoip_cn.nft @@ -0,0 +1,6212 @@ +set clashoo_china { + type ipv4_addr; + flags interval; + auto-merge; + elements = { + 1.0.1.0/24, + 1.0.2.0/23, + 1.0.8.0/21, + 1.0.32.0/19, + 1.1.0.0/24, + 1.1.2.0/23, + 1.1.4.0/22, + 1.1.8.0/21, + 1.1.16.0/20, + 1.1.32.0/19, + 1.2.0.0/23, + 1.2.2.0/24, + 1.2.4.0/22, + 1.2.8.0/21, + 1.2.16.0/20, + 1.2.32.0/19, + 1.2.64.0/18, + 1.3.0.0/16, + 1.4.1.0/24, + 1.4.2.0/23, + 1.4.4.0/22, + 1.4.8.0/21, + 1.4.16.0/20, + 1.4.32.0/19, + 1.4.64.0/18, + 1.8.0.0/16, + 1.10.0.0/21, + 1.10.8.0/23, + 1.10.11.0/24, + 1.10.12.0/22, + 1.10.16.0/20, + 1.10.32.0/19, + 1.10.64.0/18, + 1.12.0.0/14, + 1.24.0.0/13, + 1.45.0.0/16, + 1.48.0.0/14, + 1.56.0.0/13, + 1.68.0.0/14, + 1.80.0.0/12, + 1.116.0.0/15, + 1.118.0.0/23, + 1.118.4.0/22, + 1.118.8.0/21, + 1.118.16.0/20, + 1.118.37.0/24, + 1.118.38.0/23, + 1.118.40.0/21, + 1.118.56.0/21, + 1.118.96.0/19, + 1.118.128.0/17, + 1.119.0.0/16, + 1.180.0.0/14, + 1.184.0.0/15, + 1.188.0.0/14, + 1.192.0.0/13, + 1.202.0.0/15, + 1.204.0.0/14, + 5.10.138.0/23, + 5.10.140.0/24, + 5.10.143.0/24, + 8.128.4.0/22, + 8.128.32.0/19, + 8.128.64.0/19, + 8.128.96.0/20, + 8.129.0.0/16, + 8.130.0.0/15, + 8.132.0.0/14, + 8.136.0.0/13, + 8.144.0.0/14, + 8.148.0.0/19, + 8.148.36.0/22, + 8.148.41.0/24, + 8.148.43.0/24, + 8.148.64.0/18, + 8.148.128.0/17, + 8.149.0.0/16, + 8.150.0.0/19, + 8.150.32.0/21, + 8.150.64.0/23, + 8.152.0.0/13, + 8.160.0.0/15, + 8.162.0.0/17, + 8.163.0.0/16, + 8.164.0.0/14, + 8.168.0.0/15, + 8.176.0.0/17, + 8.176.128.0/19, + 14.0.0.0/21, + 14.0.12.0/22, + 14.1.0.0/22, + 14.1.24.0/22, + 14.1.108.0/22, + 14.16.0.0/12, + 14.102.128.0/22, + 14.102.180.0/22, + 14.103.0.0/16, + 14.104.0.0/13, + 14.112.0.0/12, + 14.130.0.0/15, + 14.134.0.0/15, + 14.144.0.0/12, + 14.192.60.0/22, + 14.192.76.0/22, + 14.196.0.0/15, + 14.204.0.0/15, + 14.208.0.0/12, + 16.2.142.0/23, + 27.0.128.0/21, + 27.0.160.0/21, + 27.0.188.0/22, + 27.0.204.0/22, + 27.0.208.0/21, + 27.8.0.0/13, + 27.16.0.0/12, + 27.34.232.0/21, + 27.36.0.0/14, + 27.40.0.0/13, + 27.50.40.0/21, + 27.50.128.0/17, + 27.54.72.0/21, + 27.54.152.0/21, + 27.54.192.0/18, + 27.98.208.0/20, + 27.98.224.0/19, + 27.99.128.0/17, + 27.103.0.0/16, + 27.106.128.0/18, + 27.106.204.0/22, + 27.109.32.0/19, + 27.109.124.0/22, + 27.112.0.0/18, + 27.112.80.0/20, + 27.112.112.0/21, + 27.113.128.0/18, + 27.115.0.0/17, + 27.116.44.0/22, + 27.121.72.0/21, + 27.121.120.0/21, + 27.128.0.0/15, + 27.131.220.0/22, + 27.144.0.0/16, + 27.148.0.0/14, + 27.152.0.0/13, + 27.184.0.0/13, + 27.192.0.0/11, + 27.224.0.0/14, + 36.0.0.0/22, + 36.0.8.0/21, + 36.0.16.0/20, + 36.0.32.0/19, + 36.0.64.0/18, + 36.0.128.0/17, + 36.1.0.0/16, + 36.4.0.0/14, + 36.16.0.0/12, + 36.32.0.0/14, + 36.36.0.0/16, + 36.37.0.0/19, + 36.37.36.0/23, + 36.37.39.0/24, + 36.37.40.0/21, + 36.37.48.0/20, + 36.40.0.0/13, + 36.48.0.0/15, + 36.50.226.0/23, + 36.50.254.0/23, + 36.51.0.0/16, + 36.56.0.0/13, + 36.96.0.0/11, + 36.128.0.0/13, + 36.136.0.0/14, + 36.140.0.0/17, + 36.140.128.0/18, + 36.140.192.0/20, + 36.140.208.0/21, + 36.140.216.0/23, + 36.140.218.0/24, + 36.140.220.0/22, + 36.140.224.0/19, + 36.141.0.0/16, + 36.142.0.0/15, + 36.144.0.0/12, + 36.160.0.0/11, + 36.192.0.0/11, + 36.248.0.0/14, + 36.254.0.0/16, + 36.255.116.0/22, + 36.255.128.0/22, + 36.255.164.0/22, + 36.255.172.0/22, + 36.255.176.0/22, + 39.0.0.0/24, + 39.0.2.0/23, + 39.0.4.0/22, + 39.0.8.0/21, + 39.0.16.0/20, + 39.0.32.0/19, + 39.0.64.0/18, + 39.0.128.0/17, + 39.64.0.0/11, + 39.96.0.0/16, + 39.97.0.0/17, + 39.97.128.0/18, + 39.97.192.0/21, + 39.97.200.0/22, + 39.97.208.0/20, + 39.97.224.0/19, + 39.98.0.0/15, + 39.100.0.0/14, + 39.104.0.0/14, + 39.108.0.0/16, + 39.128.0.0/10, + 40.72.0.0/15, + 40.125.128.0/17, + 40.126.64.0/18, + 40.162.0.0/16, + 42.0.0.0/22, + 42.0.8.0/21, + 42.0.16.0/21, + 42.0.24.0/22, + 42.0.32.0/19, + 42.0.128.0/18, + 42.0.192.0/21, + 42.0.201.0/24, + 42.0.202.0/23, + 42.0.204.0/22, + 42.0.208.0/20, + 42.0.224.0/19, + 42.1.0.0/19, + 42.1.32.0/20, + 42.1.48.0/21, + 42.1.56.0/22, + 42.1.128.0/17, + 42.4.0.0/14, + 42.48.0.0/13, + 42.56.0.0/14, + 42.62.0.0/17, + 42.62.128.0/19, + 42.62.160.0/20, + 42.62.180.0/22, + 42.62.184.0/21, + 42.63.0.0/16, + 42.80.0.0/15, + 42.83.64.0/20, + 42.83.80.0/22, + 42.83.88.0/21, + 42.83.96.0/19, + 42.83.128.0/17, + 42.84.0.0/14, + 42.88.0.0/13, + 42.96.64.0/19, + 42.96.96.0/21, + 42.96.108.0/22, + 42.96.112.0/20, + 42.96.128.0/17, + 42.97.0.0/16, + 42.99.0.0/18, + 42.99.64.0/19, + 42.99.96.0/20, + 42.99.112.0/22, + 42.99.120.0/21, + 42.100.0.0/14, + 42.120.0.0/15, + 42.122.0.0/16, + 42.123.0.0/19, + 42.123.36.0/22, + 42.123.40.0/21, + 42.123.48.0/20, + 42.123.64.0/18, + 42.123.128.0/17, + 42.128.0.0/12, + 42.156.0.0/19, + 42.156.36.0/22, + 42.156.40.0/21, + 42.156.48.0/20, + 42.156.64.0/18, + 42.156.128.0/17, + 42.157.0.0/16, + 42.158.0.0/15, + 42.160.0.0/12, + 42.176.0.0/13, + 42.184.0.0/15, + 42.186.0.0/16, + 42.187.0.0/18, + 42.187.64.0/19, + 42.187.96.0/20, + 42.187.112.0/21, + 42.187.120.0/22, + 42.187.128.0/17, + 42.192.0.0/13, + 42.201.0.0/17, + 42.202.0.0/15, + 42.204.0.0/14, + 42.208.0.0/12, + 42.224.0.0/12, + 42.240.0.0/17, + 42.240.128.0/18, + 42.240.192.0/19, + 42.240.224.0/20, + 42.240.240.0/21, + 42.240.248.0/23, + 42.242.0.0/15, + 42.244.0.0/14, + 42.248.0.0/13, + 43.136.0.0/13, + 43.144.0.0/15, + 43.148.0.0/14, + 43.176.0.0/12, + 43.192.0.0/14, + 43.196.0.0/15, + 43.224.12.0/22, + 43.224.24.0/22, + 43.224.44.0/22, + 43.224.52.0/22, + 43.224.56.0/22, + 43.224.68.0/22, + 43.224.72.0/22, + 43.224.80.0/22, + 43.224.100.0/22, + 43.224.160.0/22, + 43.224.176.0/22, + 43.224.184.0/22, + 43.224.200.0/21, + 43.224.208.0/21, + 43.224.216.0/22, + 43.224.240.0/22, + 43.225.76.0/22, + 43.225.84.0/22, + 43.225.120.0/22, + 43.225.175.0/24, + 43.225.180.0/22, + 43.225.208.0/22, + 43.225.216.0/21, + 43.225.224.0/20, + 43.225.240.0/21, + 43.225.252.0/22, + 43.226.32.0/19, + 43.226.64.0/19, + 43.226.96.0/20, + 43.226.112.0/21, + 43.226.120.0/22, + 43.226.128.0/18, + 43.226.192.0/20, + 43.226.208.0/21, + 43.226.236.0/22, + 43.226.240.0/20, + 43.227.0.0/21, + 43.227.8.0/22, + 43.227.32.0/19, + 43.227.64.0/19, + 43.227.96.0/21, + 43.227.104.0/22, + 43.227.136.0/21, + 43.227.144.0/22, + 43.227.152.0/21, + 43.227.160.0/20, + 43.227.176.0/21, + 43.227.188.0/22, + 43.227.192.0/19, + 43.227.232.0/22, + 43.227.248.0/21, + 43.228.0.0/18, + 43.228.64.0/21, + 43.228.76.0/22, + 43.228.100.0/22, + 43.228.116.0/22, + 43.228.120.0/22, + 43.228.132.0/22, + 43.228.136.0/22, + 43.228.148.0/22, + 43.228.152.0/22, + 43.228.188.0/22, + 43.228.204.0/22, + 43.228.240.0/22, + 43.229.40.0/22, + 43.229.48.0/22, + 43.229.56.0/22, + 43.229.96.0/22, + 43.229.136.0/21, + 43.229.144.0/22, + 43.229.168.0/21, + 43.229.176.0/20, + 43.229.192.0/21, + 43.229.216.0/21, + 43.229.232.0/21, + 43.230.20.0/22, + 43.230.32.0/22, + 43.230.68.0/22, + 43.230.72.0/22, + 43.230.124.0/22, + 43.230.220.0/22, + 43.230.224.0/19, + 43.231.32.0/20, + 43.231.80.0/20, + 43.231.96.0/20, + 43.231.136.0/21, + 43.231.144.0/20, + 43.231.160.0/20, + 43.231.176.0/21, + 43.236.0.0/16, + 43.237.0.0/17, + 43.237.128.0/18, + 43.237.192.0/22, + 43.237.200.0/21, + 43.237.208.0/20, + 43.237.224.0/19, + 43.238.0.0/16, + 43.239.0.0/19, + 43.239.32.0/20, + 43.239.48.0/22, + 43.239.116.0/22, + 43.239.120.0/22, + 43.239.172.0/22, + 43.239.176.0/23, + 43.239.179.0/24, + 43.240.0.0/22, + 43.240.56.0/21, + 43.240.68.0/22, + 43.240.72.0/21, + 43.240.84.0/22, + 43.240.124.0/22, + 43.240.128.0/21, + 43.240.136.0/22, + 43.240.156.0/22, + 43.240.192.0/19, + 43.240.240.0/20, + 43.241.0.0/20, + 43.241.16.0/21, + 43.241.48.0/22, + 43.241.76.0/22, + 43.241.80.0/20, + 43.241.112.0/22, + 43.241.168.0/21, + 43.241.176.0/21, + 43.241.184.0/22, + 43.241.208.0/20, + 43.241.224.0/20, + 43.241.240.0/22, + 43.241.248.0/22, + 43.242.8.0/21, + 43.242.16.0/20, + 43.242.48.0/22, + 43.242.53.0/24, + 43.242.54.0/24, + 43.242.56.0/21, + 43.242.64.0/22, + 43.242.72.0/21, + 43.242.80.0/20, + 43.242.96.0/22, + 43.242.144.0/20, + 43.242.160.0/21, + 43.242.168.0/22, + 43.242.180.0/22, + 43.242.188.0/22, + 43.242.192.0/21, + 43.242.204.0/22, + 43.242.216.0/21, + 43.242.252.0/22, + 43.243.4.0/22, + 43.243.8.0/21, + 43.243.16.0/22, + 43.243.88.0/22, + 43.243.128.0/22, + 43.243.136.0/22, + 43.243.144.0/21, + 43.243.156.0/22, + 43.243.180.0/22, + 43.243.228.0/22, + 43.243.232.0/22, + 43.243.244.0/22, + 43.246.0.0/18, + 43.246.64.0/19, + 43.246.96.0/22, + 43.246.112.0/22, + 43.246.228.0/22, + 43.247.4.0/22, + 43.247.8.0/22, + 43.247.44.0/22, + 43.247.48.0/22, + 43.247.68.0/22, + 43.247.76.0/22, + 43.247.84.0/22, + 43.247.88.0/21, + 43.247.96.0/21, + 43.247.108.0/22, + 43.247.112.0/22, + 43.247.148.0/22, + 43.247.152.0/22, + 43.247.176.0/20, + 43.247.196.0/22, + 43.247.200.0/21, + 43.247.208.0/20, + 43.247.224.0/22, + 43.247.232.0/21, + 43.247.240.0/20, + 43.248.0.0/21, + 43.248.20.0/22, + 43.248.28.0/22, + 43.248.48.0/22, + 43.248.76.0/22, + 43.248.80.0/20, + 43.248.96.0/19, + 43.248.128.0/20, + 43.248.144.0/21, + 43.248.176.0/20, + 43.248.192.0/20, + 43.248.208.0/22, + 43.248.228.0/22, + 43.248.232.0/22, + 43.248.244.0/22, + 43.249.4.0/22, + 43.249.120.0/22, + 43.249.132.0/22, + 43.249.136.0/22, + 43.249.144.0/20, + 43.249.160.0/21, + 43.249.168.0/22, + 43.249.192.0/22, + 43.249.236.0/22, + 43.250.4.0/22, + 43.250.12.0/22, + 43.250.16.0/21, + 43.250.28.0/22, + 43.250.32.0/21, + 43.250.96.0/21, + 43.250.108.0/22, + 43.250.112.0/22, + 43.250.128.0/22, + 43.250.144.0/21, + 43.250.160.0/22, + 43.250.168.0/22, + 43.250.176.0/22, + 43.250.180.0/23, + 43.250.200.0/22, + 43.250.212.0/22, + 43.250.216.0/21, + 43.250.236.0/22, + 43.250.244.0/22, + 43.251.4.0/22, + 43.251.36.0/22, + 43.251.118.0/23, + 43.251.192.0/22, + 43.251.232.0/22, + 43.251.244.0/22, + 43.252.48.0/22, + 43.252.56.0/22, + 43.254.0.0/21, + 43.254.8.0/22, + 43.254.24.0/22, + 43.254.36.0/22, + 43.254.44.0/22, + 43.254.52.0/22, + 43.254.72.0/22, + 43.254.84.0/22, + 43.254.88.0/21, + 43.254.100.0/22, + 43.254.104.0/22, + 43.254.112.0/21, + 43.254.128.0/22, + 43.254.136.0/21, + 43.254.144.0/20, + 43.254.168.0/21, + 43.254.180.0/22, + 43.254.184.0/21, + 43.254.192.0/21, + 43.254.200.0/22, + 43.254.208.0/22, + 43.254.220.0/22, + 43.254.224.0/20, + 43.254.240.0/22, + 43.254.248.0/21, + 43.255.0.0/21, + 43.255.8.0/22, + 43.255.48.0/22, + 43.255.64.0/20, + 43.255.84.0/22, + 43.255.96.0/22, + 43.255.144.0/22, + 43.255.176.0/22, + 43.255.184.0/22, + 43.255.192.0/22, + 43.255.200.0/21, + 43.255.208.0/21, + 43.255.224.0/21, + 43.255.232.0/22, + 43.255.244.0/22, + 44.30.120.0/24, + 45.40.192.0/18, + 45.65.16.0/20, + 45.112.132.0/22, + 45.112.188.0/22, + 45.112.208.0/20, + 45.112.228.0/22, + 45.112.232.0/21, + 45.113.12.0/22, + 45.113.16.0/20, + 45.113.40.0/22, + 45.113.52.0/22, + 45.113.56.0/22, + 45.113.72.0/22, + 45.113.144.0/21, + 45.113.168.0/22, + 45.113.176.0/22, + 45.113.184.0/22, + 45.113.200.0/21, + 45.113.208.0/20, + 45.113.240.0/22, + 45.113.252.0/22, + 45.114.0.0/22, + 45.114.32.0/22, + 45.114.40.0/22, + 45.114.52.0/22, + 45.114.96.0/22, + 45.114.136.0/22, + 45.114.196.0/22, + 45.114.200.0/22, + 45.114.228.0/22, + 45.114.252.0/22, + 45.115.18.0/23, + 45.115.44.0/22, + 45.115.100.0/22, + 45.115.120.0/22, + 45.115.132.0/22, + 45.115.144.0/22, + 45.115.156.0/22, + 45.115.164.0/22, + 45.115.200.0/22, + 45.115.212.0/22, + 45.115.244.0/22, + 45.115.248.0/22, + 45.116.16.0/22, + 45.116.24.0/22, + 45.116.32.0/21, + 45.116.52.0/22, + 45.116.96.0/21, + 45.116.140.0/22, + 45.116.152.0/22, + 45.116.208.0/22, + 45.117.8.0/22, + 45.117.20.0/22, + 45.117.68.0/22, + 45.117.124.0/22, + 45.117.252.0/22, + 45.119.60.0/22, + 45.119.64.0/21, + 45.119.72.0/22, + 45.119.104.0/22, + 45.119.116.0/22, + 45.119.232.0/22, + 45.120.100.0/22, + 45.120.140.0/22, + 45.120.164.0/22, + 45.120.240.0/22, + 45.121.52.0/22, + 45.121.64.0/21, + 45.121.72.0/22, + 45.121.92.0/22, + 45.121.172.0/22, + 45.121.176.0/22, + 45.121.215.0/24, + 45.121.240.0/20, + 45.122.0.0/19, + 45.122.32.0/21, + 45.122.40.0/22, + 45.122.60.0/22, + 45.122.64.0/19, + 45.122.96.0/20, + 45.122.112.0/21, + 45.122.160.0/19, + 45.122.192.0/20, + 45.122.208.0/21, + 45.122.216.0/22, + 45.123.28.0/22, + 45.123.32.0/21, + 45.123.44.0/22, + 45.123.48.0/20, + 45.123.64.0/20, + 45.123.80.0/21, + 45.123.88.0/22, + 45.123.120.0/22, + 45.123.128.0/21, + 45.123.136.0/22, + 45.123.148.0/22, + 45.123.152.0/21, + 45.123.164.0/22, + 45.123.168.0/21, + 45.123.176.0/21, + 45.123.184.0/22, + 45.123.204.0/22, + 45.123.212.0/22, + 45.123.224.0/19, + 45.124.0.0/22, + 45.124.20.0/22, + 45.124.28.0/22, + 45.124.32.0/21, + 45.124.68.0/22, + 45.124.76.0/22, + 45.124.80.0/22, + 45.124.100.0/22, + 45.124.124.0/22, + 45.124.172.0/22, + 45.124.176.0/22, + 45.124.208.0/22, + 45.124.248.0/22, + 45.125.24.0/22, + 45.125.44.0/22, + 45.125.52.0/22, + 45.125.56.0/22, + 45.125.76.0/22, + 45.125.80.0/20, + 45.125.96.0/21, + 45.125.136.0/22, + 45.126.48.0/21, + 45.126.100.0/22, + 45.126.108.0/22, + 45.126.112.0/21, + 45.126.120.0/22, + 45.126.212.0/22, + 45.126.220.0/22, + 45.127.8.0/21, + 45.127.128.0/22, + 45.127.144.0/21, + 45.127.156.0/22, + 45.248.8.0/22, + 45.248.80.0/21, + 45.248.96.0/20, + 45.248.128.0/21, + 45.248.204.0/22, + 45.248.208.0/20, + 45.248.224.0/19, + 45.249.0.0/21, + 45.249.12.0/22, + 45.249.16.0/20, + 45.249.32.0/21, + 45.249.112.0/22, + 45.249.188.0/22, + 45.249.192.0/20, + 45.249.208.0/21, + 45.250.12.0/22, + 45.250.16.0/22, + 45.250.28.0/22, + 45.250.32.0/21, + 45.250.40.0/22, + 45.250.76.0/22, + 45.250.80.0/20, + 45.250.96.0/22, + 45.250.104.0/21, + 45.250.112.0/20, + 45.250.128.0/20, + 45.250.144.0/21, + 45.250.152.0/22, + 45.250.164.0/22, + 45.250.180.0/22, + 45.250.184.0/21, + 45.250.192.0/22, + 45.251.0.0/22, + 45.251.8.0/22, + 45.251.16.0/21, + 45.251.52.0/22, + 45.251.84.0/22, + 45.251.88.0/21, + 45.251.96.0/21, + 45.251.120.0/21, + 45.251.140.0/22, + 45.251.144.0/20, + 45.251.160.0/19, + 45.251.192.0/19, + 45.251.224.0/22, + 45.252.0.0/21, + 45.252.9.0/24, + 45.252.10.0/23, + 45.252.12.0/22, + 45.252.16.0/20, + 45.252.32.0/20, + 45.252.48.0/22, + 45.252.88.0/21, + 45.252.96.0/22, + 45.252.104.0/21, + 45.252.112.0/20, + 45.252.128.0/20, + 45.252.148.0/22, + 45.252.152.0/21, + 45.252.160.0/20, + 45.252.176.0/22, + 45.252.192.0/19, + 45.252.224.0/21, + 45.252.232.0/22, + 45.253.0.0/18, + 45.253.64.0/20, + 45.253.80.0/21, + 45.253.92.0/22, + 45.253.96.0/20, + 45.253.112.0/21, + 45.253.120.0/22, + 45.253.132.0/22, + 45.253.136.0/21, + 45.253.144.0/20, + 45.253.160.0/19, + 45.253.192.0/19, + 45.253.224.0/20, + 45.253.240.0/22, + 45.254.0.0/20, + 45.254.16.0/21, + 45.254.28.0/22, + 45.254.40.0/22, + 45.254.48.0/20, + 45.254.64.0/18, + 45.254.128.0/18, + 45.254.192.0/19, + 45.254.224.0/21, + 45.254.236.0/22, + 45.254.240.0/22, + 45.254.248.0/22, + 45.255.0.0/18, + 45.255.64.0/19, + 45.255.96.0/20, + 45.255.112.0/21, + 45.255.120.0/22, + 45.255.136.0/21, + 45.255.144.0/20, + 45.255.160.0/19, + 45.255.192.0/19, + 45.255.224.0/20, + 45.255.240.0/21, + 45.255.248.0/22, + 47.92.0.0/14, + 47.96.0.0/11, + 49.4.0.0/17, + 49.4.128.0/22, + 49.4.160.0/20, + 49.4.178.0/23, + 49.4.180.0/22, + 49.4.184.0/21, + 49.4.192.0/18, + 49.5.0.0/16, + 49.6.0.0/15, + 49.51.0.0/19, + 49.51.54.0/23, + 49.51.56.0/21, + 49.51.88.0/21, + 49.51.96.0/19, + 49.51.226.0/23, + 49.52.0.0/14, + 49.64.0.0/11, + 49.112.0.0/13, + 49.120.0.0/14, + 49.128.0.0/24, + 49.128.2.0/23, + 49.128.4.0/22, + 49.140.0.0/15, + 49.152.0.0/14, + 49.208.0.0/14, + 49.213.60.0/23, + 49.220.0.0/14, + 49.232.0.0/14, + 49.239.0.0/18, + 49.239.192.0/18, + 49.246.224.0/23, + 49.246.228.0/22, + 49.246.232.0/23, + 49.246.236.0/22, + 49.246.240.0/20, + 52.80.0.0/14, + 52.130.0.0/15, + 54.222.0.0/15, + 57.176.0.0/15, + 58.14.0.0/21, + 58.14.16.0/20, + 58.14.32.0/19, + 58.14.64.0/18, + 58.14.128.0/17, + 58.15.0.0/16, + 58.16.0.0/13, + 58.24.0.0/15, + 58.30.0.0/15, + 58.32.0.0/11, + 58.65.232.0/21, + 58.67.128.0/17, + 58.68.224.0/19, + 58.82.0.0/17, + 58.83.0.0/16, + 58.87.64.0/18, + 58.99.128.0/17, + 58.100.0.0/15, + 58.116.0.0/14, + 58.128.0.0/13, + 58.144.0.0/16, + 58.154.0.0/15, + 58.192.0.0/11, + 58.240.0.0/12, + 59.32.0.0/11, + 59.64.0.0/12, + 59.80.0.0/15, + 59.82.0.0/16, + 59.83.0.0/17, + 59.83.144.0/20, + 59.83.160.0/20, + 59.83.192.0/19, + 59.83.224.0/21, + 59.83.232.0/22, + 59.107.0.0/17, + 59.107.226.0/24, + 59.107.255.0/24, + 59.108.0.0/14, + 59.151.0.0/17, + 59.152.16.0/20, + 59.152.32.0/21, + 59.152.64.0/20, + 59.152.112.0/21, + 59.153.4.0/22, + 59.153.32.0/22, + 59.153.60.0/22, + 59.153.64.0/21, + 59.153.72.0/22, + 59.153.92.0/22, + 59.153.136.0/22, + 59.153.152.0/22, + 59.153.164.0/22, + 59.153.168.0/22, + 59.153.173.0/24, + 59.153.174.0/23, + 59.153.176.0/20, + 59.153.192.0/22, + 59.155.0.0/16, + 59.172.0.0/14, + 59.191.0.0/17, + 59.192.0.0/10, + 60.0.0.0/11, + 60.55.0.0/16, + 60.63.0.0/16, + 60.160.0.0/11, + 60.194.0.0/15, + 60.200.0.0/13, + 60.208.0.0/12, + 60.232.0.0/15, + 60.235.0.0/16, + 60.245.128.0/17, + 60.247.0.0/16, + 60.252.0.0/16, + 60.253.128.0/17, + 60.255.0.0/16, + 61.4.80.0/20, + 61.4.176.0/20, + 61.8.160.0/20, + 61.14.212.0/22, + 61.14.216.0/21, + 61.14.240.0/21, + 61.28.0.0/17, + 61.29.128.0/18, + 61.29.192.0/19, + 61.29.224.0/20, + 61.45.128.0/18, + 61.45.224.0/20, + 61.47.128.0/18, + 61.48.0.0/13, + 61.87.192.0/18, + 61.128.0.0/11, + 61.160.0.0/12, + 61.176.0.0/13, + 61.184.0.0/15, + 61.186.0.0/23, + 61.186.3.0/24, + 61.186.4.0/22, + 61.186.8.0/21, + 61.186.16.0/20, + 61.186.32.0/19, + 61.186.64.0/18, + 61.186.128.0/17, + 61.187.0.0/16, + 61.188.0.0/14, + 61.232.0.0/14, + 61.236.0.0/15, + 61.240.0.0/14, + 62.234.0.0/16, + 63.140.0.0/24, + 63.140.3.0/24, + 66.102.240.0/21, + 66.102.248.0/23, + 66.102.251.0/24, + 66.102.252.0/24, + 66.102.254.0/23, + 68.79.0.0/18, + 69.163.104.0/24, + 69.163.106.0/24, + 69.163.123.0/24, + 69.230.192.0/18, + 69.231.128.0/18, + 69.234.192.0/18, + 69.235.128.0/18, + 71.131.192.0/18, + 71.132.0.0/18, + 71.136.64.0/18, + 71.137.0.0/18, + 81.68.0.0/14, + 81.173.18.0/23, + 81.173.20.0/22, + 81.173.28.0/24, + 82.156.0.0/15, + 94.191.0.0/17, + 101.0.0.0/22, + 101.1.0.0/22, + 101.2.172.0/22, + 101.4.0.0/14, + 101.16.0.0/12, + 101.33.128.0/17, + 101.34.0.0/15, + 101.36.0.0/18, + 101.36.64.0/20, + 101.36.82.0/23, + 101.36.84.0/22, + 101.36.88.0/21, + 101.36.128.0/17, + 101.37.0.0/16, + 101.38.0.0/15, + 101.40.0.0/14, + 101.48.0.0/15, + 101.50.8.0/21, + 101.50.56.0/22, + 101.52.0.0/16, + 101.53.100.0/22, + 101.54.0.0/16, + 101.55.224.0/21, + 101.64.0.0/13, + 101.72.0.0/14, + 101.76.0.0/15, + 101.78.0.0/22, + 101.78.32.0/19, + 101.80.0.0/12, + 101.96.0.0/21, + 101.96.8.0/22, + 101.96.16.0/20, + 101.96.128.0/17, + 101.99.96.0/19, + 101.101.64.0/19, + 101.101.100.0/24, + 101.101.102.0/23, + 101.101.104.0/21, + 101.101.112.0/20, + 101.102.64.0/19, + 101.102.100.0/23, + 101.102.102.0/24, + 101.102.104.0/21, + 101.102.112.0/20, + 101.104.0.0/14, + 101.110.64.0/19, + 101.110.96.0/20, + 101.110.116.0/22, + 101.110.120.0/21, + 101.120.0.0/14, + 101.124.0.0/15, + 101.126.0.0/16, + 101.128.0.0/22, + 101.128.8.0/21, + 101.128.16.0/20, + 101.128.32.0/19, + 101.129.0.0/16, + 101.130.0.0/15, + 101.132.0.0/14, + 101.144.0.0/12, + 101.192.0.0/18, + 101.192.68.0/22, + 101.192.84.0/22, + 101.192.88.0/21, + 101.192.132.0/22, + 101.192.164.0/22, + 101.192.168.0/21, + 101.192.176.0/20, + 101.192.192.0/19, + 101.193.0.0/18, + 101.193.100.0/22, + 101.193.108.0/22, + 101.193.112.0/22, + 101.193.120.0/21, + 101.193.132.0/22, + 101.193.164.0/22, + 101.193.168.0/21, + 101.193.176.0/20, + 101.193.192.0/19, + 101.194.0.0/15, + 101.196.0.0/14, + 101.200.0.0/15, + 101.203.128.0/19, + 101.203.160.0/21, + 101.203.172.0/22, + 101.203.176.0/20, + 101.204.0.0/14, + 101.224.0.0/13, + 101.232.0.0/17, + 101.232.128.0/18, + 101.232.200.0/21, + 101.232.212.0/22, + 101.232.220.0/22, + 101.232.224.0/22, + 101.232.232.0/21, + 101.232.240.0/22, + 101.233.0.0/16, + 101.234.64.0/21, + 101.234.76.0/22, + 101.234.80.0/20, + 101.234.96.0/19, + 101.236.0.0/16, + 101.237.0.0/17, + 101.237.128.0/18, + 101.237.192.0/19, + 101.237.224.0/22, + 101.237.230.0/23, + 101.237.243.0/24, + 101.237.244.0/23, + 101.237.246.0/24, + 101.237.250.0/24, + 101.237.253.0/24, + 101.238.0.0/15, + 101.240.0.0/13, + 101.248.0.0/15, + 101.251.0.0/22, + 101.251.8.0/21, + 101.251.16.0/20, + 101.251.32.0/19, + 101.251.64.0/18, + 101.251.128.0/17, + 101.252.0.0/15, + 101.254.0.0/16, + 103.1.8.0/22, + 103.1.20.0/22, + 103.1.24.0/22, + 103.1.72.0/22, + 103.1.88.0/22, + 103.1.168.0/22, + 103.2.108.0/22, + 103.2.156.0/22, + 103.2.164.0/22, + 103.2.188.0/23, + 103.2.200.0/21, + 103.2.208.0/21, + 103.3.84.0/22, + 103.3.88.0/21, + 103.3.96.0/19, + 103.3.128.0/20, + 103.3.148.0/22, + 103.3.152.0/21, + 103.4.56.0/22, + 103.4.168.0/22, + 103.4.184.0/22, + 103.4.224.0/22, + 103.5.36.0/22, + 103.5.52.0/22, + 103.5.56.0/22, + 103.5.152.0/22, + 103.5.168.0/22, + 103.5.192.0/23, + 103.5.194.0/24, + 103.5.252.0/22, + 103.6.76.0/22, + 103.6.220.0/22, + 103.7.140.0/22, + 103.7.212.0/22, + 103.7.216.0/21, + 103.8.0.0/21, + 103.8.8.0/22, + 103.8.32.0/22, + 103.8.52.0/22, + 103.8.68.0/22, + 103.8.108.0/22, + 103.8.156.0/22, + 103.8.200.0/21, + 103.8.220.0/22, + 103.9.8.0/22, + 103.9.24.0/22, + 103.9.108.0/22, + 103.9.152.0/22, + 103.9.248.0/21, + 103.10.0.0/22, + 103.10.16.0/22, + 103.10.84.0/22, + 103.10.140.0/22, + 103.11.16.0/22, + 103.11.168.0/22, + 103.11.180.0/22, + 103.12.32.0/22, + 103.12.92.0/22, + 103.12.98.0/23, + 103.12.136.0/22, + 103.12.184.0/22, + 103.12.232.0/22, + 103.13.12.0/22, + 103.13.124.0/22, + 103.13.196.0/22, + 103.13.244.0/22, + 103.14.84.0/22, + 103.14.132.0/22, + 103.14.136.0/22, + 103.14.156.0/22, + 103.14.240.0/22, + 103.15.4.0/22, + 103.15.8.0/22, + 103.15.16.0/22, + 103.15.96.0/22, + 103.15.200.0/22, + 103.16.52.0/22, + 103.16.80.0/21, + 103.16.88.0/22, + 103.16.108.0/22, + 103.16.124.0/22, + 103.17.40.0/22, + 103.17.64.0/22, + 103.17.120.0/22, + 103.17.136.0/22, + 103.17.160.0/22, + 103.17.204.0/22, + 103.17.228.0/22, + 103.18.186.0/23, + 103.18.192.0/22, + 103.18.208.0/21, + 103.18.224.0/22, + 103.19.12.0/22, + 103.19.40.0/21, + 103.19.64.0/21, + 103.19.72.0/22, + 103.19.232.0/22, + 103.20.12.0/22, + 103.20.32.0/24, + 103.20.34.0/23, + 103.20.46.0/24, + 103.20.68.0/22, + 103.20.112.0/22, + 103.20.128.0/22, + 103.20.160.0/22, + 103.20.248.0/22, + 103.21.98.0/23, + 103.21.102.0/23, + 103.21.112.0/21, + 103.21.140.0/22, + 103.21.176.0/22, + 103.22.0.0/18, + 103.22.64.0/19, + 103.22.100.0/22, + 103.22.104.0/21, + 103.22.112.0/20, + 103.22.188.0/22, + 103.22.228.0/22, + 103.22.252.0/22, + 103.23.8.0/22, + 103.23.56.0/22, + 103.23.160.0/21, + 103.23.176.0/22, + 103.23.228.0/22, + 103.24.24.0/22, + 103.24.116.0/22, + 103.24.128.0/22, + 103.24.144.0/22, + 103.24.176.0/22, + 103.24.184.0/22, + 103.24.220.0/22, + 103.24.228.0/22, + 103.24.252.0/22, + 103.25.9.0/24, + 103.25.20.0/22, + 103.25.24.0/21, + 103.25.32.0/21, + 103.25.40.0/22, + 103.25.48.0/22, + 103.25.64.0/21, + 103.25.148.0/22, + 103.25.156.0/22, + 103.25.216.0/22, + 103.26.0.0/22, + 103.26.64.0/22, + 103.26.76.0/22, + 103.26.132.0/22, + 103.26.156.0/22, + 103.26.160.0/22, + 103.26.228.0/22, + 103.26.240.0/22, + 103.27.4.0/22, + 103.27.12.0/22, + 103.27.24.0/22, + 103.27.56.0/22, + 103.27.96.0/22, + 103.27.212.0/22, + 103.27.240.0/22, + 103.28.4.0/22, + 103.28.8.0/22, + 103.28.184.0/22, + 103.28.204.0/22, + 103.28.212.0/22, + 103.29.16.0/22, + 103.29.24.0/23, + 103.29.128.0/21, + 103.29.136.0/22, + 103.29.236.0/23, + 103.30.20.0/22, + 103.30.96.0/22, + 103.30.104.0/22, + 103.30.148.0/22, + 103.30.228.0/22, + 103.30.236.0/22, + 103.31.0.0/22, + 103.31.48.0/20, + 103.31.64.0/21, + 103.31.148.0/22, + 103.31.160.0/22, + 103.31.168.0/22, + 103.31.200.0/22, + 103.31.242.0/23, + 103.32.0.0/15, + 103.34.0.0/16, + 103.35.0.0/19, + 103.35.32.0/20, + 103.35.48.0/22, + 103.35.104.0/22, + 103.35.117.0/24, + 103.35.119.0/24, + 103.35.180.0/22, + 103.35.220.0/22, + 103.36.28.0/22, + 103.36.36.0/22, + 103.36.56.0/21, + 103.36.64.0/22, + 103.36.72.0/22, + 103.36.96.0/22, + 103.36.132.0/22, + 103.36.136.0/22, + 103.36.160.0/20, + 103.36.192.0/19, + 103.36.224.0/20, + 103.36.240.0/21, + 103.37.12.0/22, + 103.37.16.0/22, + 103.37.24.0/22, + 103.37.44.0/22, + 103.37.52.0/22, + 103.37.56.0/22, + 103.37.72.0/22, + 103.37.100.0/22, + 103.37.104.0/22, + 103.37.136.0/21, + 103.37.144.0/20, + 103.37.160.0/21, + 103.37.172.0/22, + 103.37.176.0/22, + 103.37.188.0/22, + 103.37.208.0/20, + 103.37.252.0/22, + 103.38.0.0/22, + 103.38.32.0/22, + 103.38.40.0/21, + 103.38.56.0/22, + 103.38.76.0/22, + 103.38.84.0/22, + 103.38.92.0/22, + 103.38.96.0/22, + 103.38.116.0/22, + 103.38.132.0/22, + 103.38.140.0/22, + 103.38.224.0/21, + 103.38.232.0/22, + 103.38.252.0/23, + 103.39.64.0/22, + 103.39.88.0/22, + 103.39.100.0/22, + 103.39.104.0/22, + 103.39.160.0/19, + 103.39.200.0/21, + 103.39.208.0/20, + 103.39.224.0/21, + 103.39.232.0/22, + 103.40.12.0/22, + 103.40.16.0/20, + 103.40.32.0/20, + 103.40.88.0/22, + 103.40.158.0/23, + 103.40.173.0/24, + 103.40.174.0/23, + 103.40.192.0/22, + 103.40.212.0/22, + 103.40.220.0/22, + 103.40.228.0/22, + 103.40.232.0/21, + 103.40.240.0/20, + 103.41.0.0/22, + 103.41.52.0/22, + 103.41.116.0/22, + 103.41.140.0/22, + 103.41.148.0/22, + 103.41.152.0/22, + 103.41.160.0/21, + 103.41.220.0/22, + 103.41.224.0/21, + 103.41.232.0/22, + 103.42.8.0/22, + 103.42.24.0/22, + 103.42.32.0/22, + 103.42.64.0/21, + 103.42.76.0/22, + 103.42.232.0/22, + 103.43.96.0/21, + 103.43.104.0/22, + 103.43.124.0/22, + 103.43.132.0/22, + 103.43.184.0/22, + 103.43.192.0/21, + 103.43.208.0/22, + 103.43.220.0/22, + 103.43.224.0/22, + 103.43.240.0/22, + 103.44.56.0/22, + 103.44.80.0/22, + 103.44.120.0/21, + 103.44.144.0/22, + 103.44.168.0/22, + 103.44.176.0/20, + 103.44.192.0/22, + 103.44.196.0/23, + 103.44.199.0/24, + 103.44.200.0/21, + 103.44.224.0/22, + 103.44.236.0/22, + 103.44.240.0/20, + 103.45.0.0/18, + 103.45.72.0/21, + 103.45.80.0/20, + 103.45.96.0/19, + 103.45.128.0/18, + 103.45.192.0/19, + 103.45.224.0/22, + 103.45.248.0/22, + 103.46.0.0/22, + 103.46.12.0/22, + 103.46.16.0/20, + 103.46.32.0/19, + 103.46.64.0/18, + 103.46.128.0/21, + 103.46.136.0/22, + 103.46.152.0/21, + 103.46.160.0/20, + 103.46.176.0/21, + 103.46.244.0/22, + 103.46.248.0/22, + 103.47.4.0/22, + 103.47.20.0/22, + 103.47.36.0/22, + 103.47.40.0/22, + 103.47.48.0/22, + 103.47.80.0/22, + 103.47.96.0/22, + 103.47.108.0/22, + 103.47.116.0/22, + 103.47.120.0/22, + 103.47.136.0/21, + 103.47.212.0/22, + 103.48.52.0/22, + 103.48.92.0/22, + 103.48.148.0/22, + 103.48.152.0/21, + 103.48.202.0/23, + 103.48.216.0/21, + 103.48.224.0/20, + 103.48.240.0/21, + 103.49.12.0/22, + 103.49.20.0/22, + 103.49.72.0/21, + 103.49.96.0/22, + 103.49.108.0/22, + 103.49.176.0/21, + 103.49.197.0/24, + 103.50.36.0/22, + 103.50.44.0/22, + 103.50.48.0/20, + 103.50.64.0/21, + 103.50.72.0/22, + 103.50.108.0/22, + 103.50.112.0/20, + 103.50.132.0/22, + 103.50.136.0/21, + 103.50.172.0/22, + 103.50.176.0/20, + 103.50.192.0/21, + 103.50.200.0/22, + 103.50.220.0/22, + 103.50.224.0/20, + 103.50.240.0/21, + 103.50.248.0/22, + 103.52.40.0/22, + 103.52.72.0/21, + 103.52.80.0/21, + 103.52.96.0/21, + 103.52.104.0/22, + 103.52.160.0/21, + 103.52.172.0/22, + 103.52.176.0/22, + 103.52.184.0/22, + 103.52.196.0/22, + 103.53.64.0/21, + 103.53.92.0/22, + 103.53.100.0/22, + 103.53.124.0/22, + 103.53.128.0/20, + 103.53.144.0/22, + 103.53.180.0/22, + 103.53.204.0/22, + 103.53.208.0/21, + 103.53.236.0/22, + 103.53.248.0/22, + 103.54.8.0/22, + 103.54.48.0/22, + 103.54.160.0/21, + 103.54.212.0/22, + 103.54.240.0/22, + 103.55.80.0/22, + 103.55.120.0/22, + 103.55.152.0/22, + 103.55.172.0/22, + 103.55.204.0/22, + 103.55.208.0/22, + 103.55.228.0/22, + 103.55.236.0/22, + 103.56.20.0/22, + 103.56.32.0/22, + 103.56.56.0/21, + 103.56.72.0/21, + 103.56.94.0/23, + 103.56.100.0/22, + 103.56.104.0/22, + 103.56.140.0/22, + 103.56.152.0/22, + 103.56.184.0/22, + 103.56.200.0/22, + 103.57.12.0/22, + 103.57.52.0/22, + 103.57.56.0/22, + 103.57.76.0/22, + 103.57.136.0/22, + 103.57.196.0/22, + 103.58.24.0/22, + 103.59.76.0/22, + 103.59.112.0/21, + 103.59.120.0/23, + 103.59.124.0/22, + 103.59.128.0/22, + 103.59.148.0/22, + 103.59.164.0/22, + 103.59.168.0/23, + 103.60.44.0/22, + 103.60.164.0/22, + 103.60.228.0/22, + 103.60.236.0/22, + 103.61.60.0/22, + 103.61.104.0/22, + 103.61.140.0/22, + 103.61.152.0/21, + 103.61.160.0/22, + 103.61.172.0/22, + 103.61.176.0/22, + 103.61.188.0/22, + 103.62.24.0/22, + 103.62.53.0/24, + 103.62.55.0/24, + 103.62.72.0/21, + 103.62.80.0/21, + 103.62.88.0/22, + 103.62.96.0/19, + 103.62.128.0/21, + 103.62.156.0/22, + 103.62.160.0/19, + 103.62.192.0/22, + 103.62.204.0/22, + 103.62.208.0/20, + 103.62.224.0/22, + 103.63.32.0/19, + 103.63.64.0/20, + 103.63.80.0/21, + 103.63.88.0/22, + 103.63.140.0/22, + 103.63.144.0/22, + 103.63.152.0/22, + 103.63.160.0/20, + 103.63.176.0/21, + 103.63.184.0/22, + 103.63.192.0/20, + 103.63.208.0/22, + 103.63.240.0/20, + 103.64.0.0/21, + 103.64.24.0/21, + 103.64.32.0/19, + 103.64.64.0/18, + 103.64.140.0/22, + 103.64.144.0/22, + 103.64.152.0/21, + 103.64.160.0/19, + 103.64.192.0/18, + 103.65.0.0/20, + 103.65.16.0/22, + 103.65.48.0/20, + 103.65.64.0/19, + 103.65.100.0/22, + 103.65.104.0/21, + 103.65.112.0/22, + 103.65.144.0/20, + 103.65.160.0/20, + 103.65.204.0/22, + 103.65.224.0/23, + 103.66.32.0/22, + 103.66.40.0/22, + 103.66.108.0/22, + 103.66.200.0/22, + 103.66.240.0/20, + 103.67.0.0/21, + 103.67.8.0/22, + 103.67.100.0/22, + 103.67.104.0/21, + 103.67.112.0/20, + 103.67.128.0/20, + 103.67.144.0/21, + 103.67.172.0/22, + 103.67.192.0/22, + 103.67.212.0/22, + 103.67.252.0/22, + 103.68.64.0/22, + 103.68.88.0/22, + 103.68.100.0/22, + 103.68.128.0/22, + 103.69.16.0/22, + 103.69.62.0/23, + 103.70.8.0/22, + 103.70.14.0/23, + 103.70.148.0/22, + 103.70.224.0/23, + 103.70.227.0/24, + 103.70.236.0/22, + 103.70.252.0/22, + 103.71.0.0/22, + 103.71.68.0/22, + 103.71.72.0/22, + 103.71.80.0/21, + 103.71.88.0/22, + 103.71.120.0/21, + 103.71.128.0/22, + 103.71.144.0/22, + 103.71.196.0/22, + 103.71.200.0/22, + 103.71.232.0/22, + 103.72.12.0/22, + 103.72.16.0/20, + 103.72.32.0/20, + 103.72.48.0/21, + 103.72.112.0/21, + 103.72.124.0/22, + 103.72.128.0/21, + 103.72.172.0/22, + 103.72.180.0/22, + 103.72.224.0/21, + 103.72.233.0/24, + 103.72.234.0/23, + 103.72.236.0/22, + 103.72.240.0/20, + 103.73.0.0/19, + 103.73.48.0/22, + 103.73.116.0/22, + 103.73.120.0/22, + 103.73.128.0/20, + 103.73.144.0/22, + 103.73.168.0/22, + 103.73.176.0/22, + 103.73.204.0/22, + 103.73.208.0/22, + 103.73.244.0/22, + 103.73.248.0/22, + 103.74.24.0/21, + 103.74.32.0/20, + 103.74.48.0/22, + 103.74.56.0/21, + 103.74.80.0/22, + 103.74.148.0/22, + 103.74.152.0/21, + 103.74.204.0/22, + 103.74.232.0/22, + 103.75.82.0/23, + 103.75.88.0/21, + 103.75.104.0/21, + 103.75.112.0/22, + 103.75.120.0/22, + 103.75.128.0/22, + 103.75.144.0/22, + 103.75.152.0/22, + 103.76.60.0/22, + 103.76.64.0/21, + 103.76.72.0/22, + 103.76.92.0/22, + 103.76.216.0/21, + 103.76.224.0/22, + 103.77.28.0/22, + 103.77.52.0/22, + 103.77.56.0/22, + 103.77.88.0/22, + 103.77.132.0/22, + 103.77.148.0/22, + 103.77.220.0/22, + 103.78.56.0/21, + 103.78.64.0/21, + 103.78.124.0/22, + 103.78.172.0/22, + 103.78.176.0/22, + 103.78.196.0/22, + 103.78.228.0/22, + 103.79.24.0/21, + 103.79.36.0/22, + 103.79.40.0/21, + 103.79.56.0/21, + 103.79.64.0/21, + 103.79.80.0/21, + 103.79.136.0/22, + 103.79.188.0/22, + 103.79.192.0/20, + 103.79.208.0/21, + 103.79.228.0/23, + 103.80.44.0/22, + 103.80.72.0/22, + 103.80.176.0/21, + 103.80.184.0/22, + 103.80.192.0/22, + 103.80.200.0/22, + 103.80.232.0/22, + 103.81.4.0/22, + 103.81.8.0/22, + 103.81.16.0/21, + 103.81.44.0/22, + 103.81.48.0/22, + 103.81.96.0/22, + 103.81.120.0/22, + 103.81.148.0/22, + 103.81.164.0/22, + 103.81.184.0/23, + 103.81.200.0/22, + 103.81.232.0/22, + 103.82.60.0/22, + 103.82.68.0/22, + 103.82.84.0/22, + 103.82.104.0/22, + 103.82.224.0/22, + 103.82.236.0/22, + 103.83.44.0/22, + 103.83.52.0/22, + 103.83.60.0/22, + 103.83.64.0/22, + 103.83.72.0/22, + 103.83.112.0/22, + 103.84.0.0/22, + 103.84.12.0/22, + 103.84.16.0/20, + 103.84.48.0/22, + 103.84.64.0/22, + 103.84.72.0/22, + 103.84.170.0/23, + 103.85.44.0/22, + 103.85.48.0/22, + 103.85.84.0/22, + 103.85.136.0/22, + 103.85.144.0/22, + 103.85.164.0/22, + 103.85.168.0/21, + 103.85.176.0/22, + 103.85.186.0/23, + 103.86.28.0/22, + 103.86.32.0/22, + 103.86.60.0/22, + 103.86.204.0/22, + 103.86.208.0/20, + 103.86.224.0/19, + 103.87.0.0/21, + 103.87.20.0/22, + 103.87.32.0/22, + 103.87.96.0/22, + 103.87.132.0/22, + 103.87.180.0/22, + 103.87.224.0/22, + 103.88.4.0/22, + 103.88.8.0/21, + 103.88.16.0/21, + 103.88.32.0/21, + 103.88.60.0/22, + 103.88.64.0/22, + 103.88.72.0/22, + 103.88.96.0/22, + 103.88.164.0/22, + 103.88.212.0/22, + 103.89.28.0/22, + 103.89.96.0/20, + 103.89.112.0/21, + 103.89.148.0/22, + 103.89.172.0/22, + 103.89.184.0/21, + 103.89.192.0/19, + 103.89.224.0/21, + 103.90.52.0/22, + 103.90.56.0/23, + 103.90.92.0/22, + 103.90.100.0/22, + 103.90.104.0/21, + 103.90.112.0/20, + 103.90.128.0/21, + 103.90.152.0/22, + 103.90.168.0/22, + 103.90.176.0/22, + 103.90.188.0/22, + 103.90.192.0/22, + 103.91.36.0/22, + 103.91.40.0/22, + 103.91.108.0/22, + 103.91.112.0/23, + 103.91.138.0/23, + 103.91.152.0/22, + 103.91.176.0/22, + 103.91.200.0/22, + 103.91.208.0/21, + 103.91.236.0/22, + 103.91.252.0/22, + 103.92.0.0/20, + 103.92.48.0/20, + 103.92.64.0/20, + 103.92.80.0/22, + 103.92.86.0/24, + 103.92.88.0/22, + 103.92.108.0/22, + 103.92.124.0/22, + 103.92.132.0/22, + 103.92.156.0/22, + 103.92.164.0/22, + 103.92.168.0/21, + 103.92.176.0/20, + 103.92.192.0/22, + 103.92.236.0/22, + 103.92.240.0/20, + 103.93.0.0/21, + 103.93.28.0/22, + 103.93.84.0/22, + 103.93.142.0/23, + 103.93.152.0/22, + 103.93.180.0/22, + 103.93.204.0/22, + 103.94.12.0/22, + 103.94.20.0/22, + 103.94.32.0/20, + 103.94.72.0/22, + 103.94.88.0/22, + 103.94.116.0/22, + 103.94.160.0/22, + 103.94.200.0/22, + 103.95.52.0/22, + 103.95.68.0/22, + 103.95.88.0/21, + 103.95.128.0/22, + 103.95.136.0/21, + 103.95.144.0/22, + 103.95.152.0/22, + 103.95.216.0/21, + 103.95.236.0/22, + 103.95.240.0/20, + 103.96.8.0/22, + 103.96.124.0/22, + 103.96.136.0/22, + 103.96.152.0/21, + 103.96.160.0/19, + 103.96.192.0/20, + 103.96.208.0/21, + 103.96.216.0/22, + 103.96.224.0/23, + 103.97.8.0/21, + 103.97.16.0/20, + 103.97.36.0/22, + 103.97.40.0/22, + 103.97.56.0/22, + 103.97.60.0/23, + 103.97.64.0/21, + 103.97.72.0/22, + 103.97.112.0/21, + 103.97.144.0/21, + 103.97.188.0/22, + 103.97.192.0/22, + 103.97.229.0/24, + 103.98.0.0/23, + 103.98.40.0/21, + 103.98.48.0/22, + 103.98.56.0/22, + 103.98.80.0/22, + 103.98.88.0/21, + 103.98.96.0/21, + 103.98.124.0/22, + 103.98.136.0/21, + 103.98.144.0/22, + 103.98.164.0/22, + 103.98.168.0/22, + 103.98.180.0/22, + 103.98.196.0/22, + 103.98.216.0/21, + 103.98.224.0/21, + 103.98.232.0/22, + 103.98.240.0/20, + 103.99.53.0/24, + 103.99.54.0/23, + 103.99.56.0/22, + 103.99.62.0/24, + 103.99.104.0/22, + 103.99.116.0/22, + 103.99.120.0/22, + 103.99.152.0/22, + 103.99.220.0/22, + 103.99.232.0/21, + 103.100.0.0/22, + 103.100.32.0/22, + 103.100.40.0/22, + 103.100.48.0/21, + 103.100.56.0/22, + 103.100.62.0/24, + 103.100.64.0/22, + 103.100.88.0/22, + 103.100.116.0/22, + 103.100.144.0/22, + 103.100.236.0/22, + 103.100.240.0/22, + 103.100.248.0/21, + 103.101.8.0/21, + 103.101.60.0/22, + 103.101.120.0/21, + 103.101.144.0/21, + 103.101.180.0/22, + 103.101.184.0/22, + 103.102.76.0/22, + 103.102.80.0/22, + 103.102.168.0/21, + 103.102.180.0/22, + 103.102.184.0/21, + 103.102.192.0/21, + 103.102.200.0/22, + 103.102.208.0/21, + 103.103.12.0/22, + 103.103.16.0/22, + 103.103.36.0/22, + 103.103.72.0/22, + 103.103.188.0/22, + 103.103.200.0/21, + 103.103.220.0/22, + 103.103.224.0/21, + 103.103.232.0/22, + 103.103.248.0/21, + 103.104.0.0/21, + 103.104.36.0/22, + 103.104.40.0/22, + 103.104.64.0/22, + 103.104.104.0/22, + 103.104.152.0/22, + 103.104.188.0/22, + 103.104.198.0/23, + 103.104.252.0/22, + 103.105.0.0/21, + 103.105.12.0/22, + 103.105.16.0/22, + 103.105.60.0/22, + 103.105.116.0/22, + 103.105.133.0/24, + 103.105.134.0/23, + 103.105.180.0/22, + 103.105.184.0/22, + 103.105.200.0/21, + 103.105.220.0/22, + 103.106.36.0/22, + 103.106.40.0/21, + 103.106.60.0/22, + 103.106.68.0/22, + 103.106.96.0/22, + 103.106.120.0/22, + 103.106.128.0/21, + 103.106.160.0/22, + 103.106.196.0/22, + 103.106.202.0/24, + 103.106.212.0/22, + 103.106.252.0/22, + 103.107.0.0/22, + 103.107.28.0/22, + 103.107.32.0/22, + 103.107.44.0/22, + 103.107.72.0/22, + 103.107.108.0/22, + 103.107.164.0/22, + 103.107.168.0/22, + 103.107.188.0/22, + 103.107.192.0/22, + 103.107.208.0/20, + 103.108.52.0/22, + 103.108.160.0/21, + 103.108.196.0/22, + 103.108.208.0/21, + 103.108.224.0/22, + 103.108.244.0/22, + 103.109.20.0/22, + 103.109.48.0/22, + 103.109.88.0/22, + 103.109.248.0/22, + 103.110.92.0/22, + 103.110.116.0/22, + 103.110.132.0/22, + 103.110.136.0/22, + 103.110.152.0/21, + 103.110.188.0/22, + 103.110.204.0/22, + 103.111.64.0/22, + 103.111.172.0/22, + 103.111.252.0/22, + 103.112.72.0/22, + 103.112.88.0/21, + 103.112.108.0/22, + 103.112.112.0/21, + 103.112.140.0/22, + 103.113.4.0/22, + 103.113.144.0/22, + 103.113.220.0/22, + 103.113.232.0/21, + 103.114.4.0/22, + 103.114.28.0/22, + 103.114.68.0/22, + 103.114.100.0/22, + 103.114.148.0/22, + 103.114.156.0/23, + 103.114.176.0/22, + 103.114.212.0/22, + 103.114.236.0/22, + 103.114.240.0/22, + 103.115.52.0/22, + 103.115.68.0/22, + 103.115.92.0/22, + 103.115.120.0/22, + 103.115.148.0/22, + 103.115.248.0/22, + 103.116.40.0/22, + 103.116.76.0/22, + 103.116.92.0/22, + 103.116.120.0/22, + 103.116.128.0/22, + 103.116.138.0/23, + 103.116.150.0/24, + 103.116.184.0/22, + 103.116.220.0/22, + 103.116.224.0/21, + 103.117.16.0/22, + 103.117.73.0/24, + 103.117.74.0/23, + 103.117.88.0/22, + 103.117.188.0/22, + 103.117.220.0/22, + 103.118.52.0/22, + 103.118.56.0/21, + 103.118.64.0/21, + 103.118.72.0/22, + 103.118.88.0/22, + 103.118.210.0/23, + 103.118.212.0/22, + 103.118.216.0/22, + 103.118.248.0/22, + 103.119.28.0/22, + 103.119.115.0/24, + 103.119.156.0/22, + 103.119.180.0/22, + 103.119.200.0/22, + 103.120.52.0/22, + 103.120.72.0/22, + 103.120.88.0/22, + 103.120.96.0/21, + 103.120.140.0/22, + 103.120.196.0/22, + 103.120.224.0/22, + 103.121.52.0/22, + 103.121.160.0/21, + 103.121.250.0/24, + 103.121.252.0/22, + 103.122.48.0/22, + 103.122.192.0/22, + 103.122.240.0/22, + 103.123.4.0/22, + 103.123.56.0/22, + 103.123.88.0/21, + 103.123.116.0/22, + 103.123.176.0/22, + 103.123.200.0/21, + 103.123.208.0/21, + 103.124.24.0/22, + 103.124.48.0/22, + 103.124.64.0/22, + 103.124.212.0/22, + 103.124.216.0/22, + 103.125.20.0/22, + 103.125.44.0/22, + 103.125.132.0/22, + 103.125.164.0/22, + 103.125.196.0/22, + 103.125.236.0/22, + 103.125.249.0/24, + 103.125.250.0/23, + 103.126.0.0/22, + 103.126.16.0/22, + 103.126.44.0/22, + 103.126.124.0/22, + 103.126.128.0/21, + 103.130.132.0/22, + 103.130.160.0/22, + 103.130.228.0/22, + 103.131.20.0/22, + 103.131.36.0/22, + 103.131.138.0/23, + 103.131.152.0/22, + 103.131.168.0/22, + 103.131.224.0/21, + 103.131.240.0/22, + 103.132.22.0/23, + 103.132.48.0/22, + 103.132.60.0/22, + 103.132.64.0/20, + 103.132.80.0/22, + 103.132.104.0/21, + 103.132.112.0/21, + 103.132.120.0/22, + 103.132.161.0/24, + 103.132.188.0/22, + 103.132.208.0/21, + 103.133.12.0/22, + 103.133.40.0/22, + 103.133.128.0/22, + 103.133.232.0/22, + 103.134.196.0/22, + 103.134.232.0/23, + 103.135.80.0/22, + 103.135.124.0/22, + 103.135.148.0/22, + 103.135.156.0/22, + 103.135.160.0/21, + 103.135.176.0/22, + 103.135.184.0/22, + 103.135.192.0/21, + 103.135.236.0/22, + 103.136.128.0/22, + 103.136.232.0/22, + 103.137.58.0/23, + 103.137.60.0/24, + 103.137.76.0/22, + 103.137.136.0/23, + 103.137.180.0/22, + 103.137.236.0/22, + 103.138.2.0/23, + 103.138.134.0/23, + 103.138.208.0/23, + 103.138.220.0/23, + 103.138.248.0/23, + 103.139.22.0/23, + 103.139.92.0/23, + 103.139.134.0/23, + 103.139.172.0/23, + 103.139.204.0/23, + 103.139.212.0/23, + 103.140.14.0/23, + 103.140.140.0/23, + 103.140.144.0/23, + 103.140.192.0/23, + 103.141.10.0/23, + 103.141.58.0/23, + 103.141.128.0/23, + 103.141.186.0/23, + 103.141.242.0/23, + 103.142.28.0/23, + 103.142.58.0/23, + 103.142.82.0/23, + 103.142.96.0/23, + 103.142.122.0/23, + 103.142.128.0/23, + 103.142.154.0/23, + 103.142.156.0/23, + 103.142.172.0/23, + 103.142.180.0/23, + 103.142.186.0/23, + 103.142.234.0/23, + 103.142.238.0/23, + 103.142.248.0/23, + 103.143.16.0/22, + 103.143.74.0/23, + 103.143.120.0/23, + 103.143.124.0/23, + 103.143.132.0/22, + 103.143.174.0/23, + 103.143.228.0/23, + 103.144.41.0/24, + 103.144.66.0/23, + 103.144.70.0/23, + 103.144.72.0/23, + 103.144.136.0/23, + 103.144.158.0/23, + 103.145.42.0/23, + 103.145.93.0/24, + 103.145.98.0/23, + 103.145.188.0/23, + 103.146.72.0/23, + 103.146.91.0/24, + 103.146.126.0/23, + 103.146.138.0/23, + 103.146.236.0/23, + 103.147.124.0/23, + 103.147.206.0/23, + 103.148.174.0/23, + 103.149.6.0/23, + 103.149.17.0/24, + 103.149.44.0/23, + 103.149.210.0/23, + 103.149.214.0/23, + 103.149.220.0/23, + 103.149.242.0/23, + 103.149.244.0/22, + 103.150.10.0/23, + 103.150.24.0/23, + 103.150.66.0/23, + 103.150.72.0/23, + 103.150.122.0/23, + 103.150.126.0/23, + 103.150.128.0/22, + 103.150.146.0/23, + 103.150.164.0/23, + 103.150.172.0/23, + 103.150.200.0/23, + 103.150.216.0/23, + 103.150.244.0/23, + 103.151.142.0/23, + 103.151.148.0/23, + 103.151.158.0/23, + 103.152.28.0/22, + 103.152.56.0/23, + 103.152.76.0/23, + 103.152.120.0/22, + 103.152.152.0/23, + 103.152.168.0/23, + 103.152.186.0/23, + 103.152.192.0/23, + 103.152.200.0/23, + 103.152.208.0/23, + 103.152.224.0/23, + 103.152.250.0/23, + 103.153.114.0/23, + 103.153.122.0/23, + 103.153.132.0/23, + 103.153.146.0/23, + 103.153.160.0/23, + 103.154.18.0/23, + 103.154.30.0/23, + 103.154.32.0/23, + 103.154.40.0/23, + 103.154.66.0/23, + 103.154.162.0/23, + 103.154.164.0/23, + 103.154.168.0/23, + 103.155.14.0/23, + 103.155.34.0/23, + 103.155.48.0/23, + 103.155.76.0/23, + 103.155.101.0/24, + 103.155.214.0/23, + 103.156.28.0/23, + 103.156.68.0/23, + 103.156.104.0/23, + 103.156.158.0/23, + 103.156.174.0/23, + 103.156.186.0/23, + 103.156.228.0/23, + 103.157.30.0/23, + 103.157.174.0/23, + 103.157.212.0/23, + 103.157.234.0/23, + 103.157.254.0/23, + 103.158.0.0/23, + 103.158.8.0/23, + 103.158.16.0/23, + 103.158.200.0/23, + 103.158.224.0/23, + 103.159.124.0/23, + 103.159.134.0/23, + 103.159.142.0/23, + 103.160.32.0/22, + 103.160.112.0/22, + 103.160.244.0/23, + 103.160.254.0/23, + 103.161.14.0/23, + 103.161.102.0/23, + 103.161.208.0/23, + 103.161.220.0/23, + 103.161.254.0/23, + 103.162.10.0/23, + 103.162.32.0/23, + 103.162.116.0/23, + 103.163.28.0/23, + 103.163.32.0/23, + 103.163.46.0/23, + 103.163.74.0/23, + 103.163.180.0/23, + 103.164.4.0/23, + 103.164.32.0/23, + 103.164.40.0/22, + 103.164.64.0/23, + 103.164.76.0/23, + 103.164.178.0/23, + 103.165.44.0/23, + 103.165.52.0/23, + 103.165.82.0/23, + 103.165.110.0/23, + 103.166.20.0/23, + 103.166.50.0/23, + 103.166.52.0/22, + 103.166.84.0/23, + 103.166.138.0/23, + 103.167.0.0/23, + 103.167.36.0/23, + 103.167.100.0/23, + 103.168.98.0/23, + 103.168.170.0/23, + 103.169.50.0/23, + 103.169.62.0/23, + 103.169.108.0/23, + 103.169.162.0/23, + 103.169.202.0/23, + 103.169.216.0/24, + 103.170.4.0/23, + 103.170.134.0/23, + 103.170.212.0/23, + 103.171.32.0/23, + 103.171.166.0/24, + 103.171.214.0/23, + 103.172.32.0/23, + 103.172.160.0/23, + 103.173.102.0/23, + 103.173.182.0/23, + 103.173.184.0/23, + 103.174.94.0/23, + 103.175.114.0/23, + 103.175.118.0/23, + 103.175.197.0/24, + 103.176.52.0/23, + 103.176.222.0/23, + 103.176.244.0/23, + 103.177.28.0/23, + 103.177.44.0/23, + 103.177.71.0/24, + 103.177.162.0/23, + 103.178.240.0/23, + 103.179.76.0/22, + 103.180.108.0/24, + 103.180.226.0/23, + 103.181.234.0/23, + 103.183.26.0/23, + 103.183.66.0/23, + 103.183.122.0/23, + 103.183.124.0/23, + 103.184.46.0/23, + 103.184.60.0/23, + 103.185.78.0/23, + 103.185.80.0/23, + 103.186.4.0/23, + 103.186.108.0/23, + 103.186.162.0/24, + 103.186.228.0/23, + 103.189.92.0/23, + 103.189.152.0/22, + 103.190.20.0/23, + 103.190.71.0/24, + 103.190.104.0/23, + 103.190.116.0/22, + 103.190.122.0/23, + 103.191.102.0/23, + 103.191.242.0/23, + 103.192.0.0/19, + 103.192.48.0/21, + 103.192.56.0/22, + 103.192.84.0/22, + 103.192.88.0/21, + 103.192.96.0/20, + 103.192.112.0/22, + 103.192.128.0/20, + 103.192.144.0/22, + 103.192.164.0/22, + 103.192.188.0/22, + 103.192.208.0/21, + 103.192.216.0/22, + 103.192.252.0/22, + 103.193.40.0/21, + 103.193.120.0/22, + 103.193.140.0/22, + 103.193.160.0/22, + 103.193.188.0/22, + 103.193.192.0/22, + 103.193.212.0/22, + 103.193.216.0/21, + 103.193.224.0/20, + 103.194.16.0/22, + 103.195.104.0/22, + 103.195.112.0/22, + 103.195.152.0/22, + 103.195.160.0/22, + 103.196.64.0/22, + 103.196.72.0/22, + 103.196.88.0/21, + 103.196.96.0/22, + 103.196.168.0/22, + 103.197.0.0/22, + 103.197.180.0/22, + 103.197.228.0/22, + 103.198.20.0/22, + 103.198.60.0/22, + 103.198.64.0/22, + 103.198.72.0/22, + 103.198.124.0/22, + 103.198.156.0/22, + 103.198.180.0/22, + 103.198.196.0/22, + 103.198.216.0/21, + 103.198.224.0/20, + 103.198.240.0/21, + 103.199.164.0/22, + 103.199.196.0/22, + 103.199.228.0/22, + 103.199.248.0/21, + 103.200.52.0/22, + 103.200.64.0/21, + 103.200.136.0/21, + 103.200.144.0/20, + 103.200.160.0/19, + 103.200.192.0/22, + 103.200.220.0/22, + 103.200.224.0/19, + 103.201.0.0/20, + 103.201.16.0/21, + 103.201.28.0/22, + 103.201.32.0/19, + 103.201.64.0/22, + 103.201.76.0/22, + 103.201.80.0/20, + 103.201.96.0/20, + 103.201.112.0/21, + 103.201.120.0/22, + 103.201.152.0/21, + 103.201.160.0/19, + 103.201.192.0/18, + 103.202.0.0/19, + 103.202.32.0/20, + 103.202.56.0/22, + 103.202.60.0/24, + 103.202.62.0/23, + 103.202.64.0/22, + 103.202.68.0/23, + 103.202.72.0/21, + 103.202.80.0/20, + 103.202.96.0/19, + 103.202.128.0/20, + 103.202.144.0/22, + 103.202.152.0/24, + 103.202.154.0/23, + 103.202.156.0/22, + 103.202.160.0/19, + 103.202.192.0/20, + 103.202.212.0/22, + 103.202.228.0/22, + 103.202.236.0/22, + 103.202.240.0/20, + 103.203.0.0/19, + 103.203.32.0/22, + 103.203.96.0/19, + 103.203.128.0/22, + 103.203.140.0/22, + 103.203.164.0/22, + 103.203.168.0/22, + 103.203.192.0/22, + 103.203.200.0/22, + 103.203.212.0/22, + 103.203.216.0/22, + 103.204.24.0/22, + 103.204.72.0/22, + 103.204.88.0/22, + 103.204.112.0/22, + 103.204.136.0/21, + 103.204.144.0/21, + 103.204.152.0/22, + 103.204.196.0/22, + 103.204.216.0/23, + 103.204.232.0/21, + 103.205.4.0/22, + 103.205.40.0/21, + 103.205.52.0/22, + 103.205.108.0/22, + 103.205.116.0/22, + 103.205.136.0/22, + 103.205.162.0/24, + 103.205.188.0/22, + 103.205.192.0/21, + 103.205.200.0/22, + 103.205.236.0/22, + 103.205.248.0/21, + 103.206.0.0/22, + 103.206.44.0/22, + 103.206.148.0/22, + 103.207.104.0/22, + 103.207.184.0/21, + 103.207.192.0/20, + 103.207.208.0/21, + 103.207.220.0/22, + 103.207.228.0/22, + 103.207.232.0/22, + 103.208.12.0/22, + 103.208.16.0/22, + 103.208.28.0/22, + 103.208.40.0/21, + 103.208.48.0/22, + 103.209.112.0/22, + 103.209.136.0/22, + 103.209.200.0/22, + 103.209.208.0/22, + 103.209.216.0/22, + 103.210.96.0/22, + 103.210.156.0/22, + 103.210.160.0/19, + 103.210.216.0/22, + 103.211.44.0/22, + 103.211.96.0/22, + 103.211.156.0/22, + 103.211.164.0/22, + 103.211.220.0/22, + 103.211.224.0/22, + 103.211.248.0/22, + 103.212.0.0/20, + 103.212.44.0/22, + 103.212.48.0/22, + 103.212.84.0/22, + 103.212.100.0/22, + 103.212.108.0/22, + 103.212.148.0/22, + 103.212.164.0/22, + 103.212.196.0/22, + 103.212.200.0/22, + 103.212.252.0/22, + 103.213.40.0/21, + 103.213.48.0/20, + 103.213.64.0/19, + 103.213.96.0/22, + 103.213.132.0/22, + 103.213.136.0/21, + 103.213.144.0/20, + 103.213.160.0/19, + 103.213.196.0/22, + 103.213.226.0/23, + 103.213.232.0/23, + 103.214.48.0/22, + 103.214.84.0/22, + 103.214.212.0/22, + 103.214.240.0/21, + 103.215.28.0/22, + 103.215.32.0/21, + 103.215.44.0/22, + 103.215.100.0/22, + 103.215.108.0/22, + 103.215.116.0/22, + 103.215.120.0/22, + 103.215.140.0/22, + 103.216.4.0/22, + 103.216.8.0/21, + 103.216.16.0/20, + 103.216.32.0/20, + 103.216.64.0/22, + 103.216.108.0/22, + 103.216.136.0/22, + 103.216.152.0/22, + 103.216.156.0/23, + 103.216.224.0/22, + 103.216.240.0/20, + 103.217.0.0/18, + 103.217.168.0/22, + 103.217.180.0/22, + 103.217.184.0/21, + 103.217.192.0/20, + 103.218.8.0/21, + 103.218.16.0/21, + 103.218.28.0/22, + 103.218.32.0/19, + 103.218.64.0/20, + 103.218.80.0/21, + 103.218.88.0/22, + 103.218.178.0/23, + 103.218.192.0/20, + 103.218.208.0/21, + 103.218.216.0/22, + 103.219.24.0/21, + 103.219.32.0/21, + 103.219.64.0/22, + 103.219.84.0/22, + 103.219.88.0/21, + 103.219.96.0/21, + 103.219.176.0/22, + 103.219.184.0/22, + 103.220.48.0/20, + 103.220.64.0/22, + 103.220.92.0/22, + 103.220.96.0/20, + 103.220.116.0/22, + 103.220.120.0/21, + 103.220.128.0/20, + 103.220.144.0/21, + 103.220.152.0/22, + 103.220.160.0/19, + 103.220.192.0/21, + 103.220.200.0/22, + 103.220.240.0/21, + 103.221.88.0/22, + 103.221.96.0/19, + 103.221.128.0/18, + 103.221.192.0/20, + 103.222.0.0/20, + 103.222.16.0/22, + 103.222.24.0/21, + 103.222.32.0/19, + 103.222.64.0/18, + 103.222.128.0/18, + 103.222.192.0/19, + 103.222.224.0/21, + 103.222.232.0/22, + 103.222.240.0/21, + 103.223.16.0/20, + 103.223.32.0/19, + 103.223.64.0/19, + 103.223.96.0/20, + 103.223.112.0/21, + 103.223.124.0/22, + 103.223.128.0/21, + 103.223.140.0/22, + 103.223.144.0/20, + 103.223.160.0/20, + 103.223.176.0/21, + 103.223.188.0/22, + 103.223.192.0/18, + 103.224.0.0/22, + 103.224.40.0/21, + 103.224.60.0/22, + 103.224.220.0/22, + 103.224.224.0/21, + 103.224.232.0/22, + 103.225.18.0/24, + 103.226.16.0/22, + 103.226.40.0/22, + 103.226.57.0/24, + 103.226.58.0/23, + 103.226.60.0/22, + 103.226.80.0/22, + 103.226.116.0/23, + 103.226.132.0/22, + 103.226.156.0/22, + 103.226.180.0/22, + 103.226.196.0/22, + 103.227.48.0/22, + 103.227.72.0/21, + 103.227.80.0/22, + 103.227.100.0/22, + 103.227.120.0/22, + 103.227.132.0/22, + 103.227.136.0/22, + 103.227.196.0/22, + 103.227.204.0/22, + 103.227.212.0/22, + 103.227.228.0/22, + 103.228.12.0/22, + 103.228.88.0/22, + 103.228.136.0/22, + 103.228.160.0/22, + 103.228.176.0/22, + 103.228.204.0/22, + 103.228.208.0/22, + 103.228.228.0/22, + 103.228.232.0/22, + 103.229.20.0/22, + 103.229.136.0/22, + 103.229.148.0/22, + 103.229.172.0/22, + 103.229.212.0/22, + 103.229.216.0/21, + 103.229.228.0/22, + 103.229.236.0/22, + 103.229.240.0/22, + 103.230.0.0/22, + 103.230.28.0/22, + 103.230.44.0/22, + 103.230.96.0/22, + 103.230.110.0/23, + 103.230.196.0/22, + 103.230.200.0/21, + 103.230.212.0/22, + 103.230.236.0/22, + 103.231.16.0/21, + 103.231.64.0/21, + 103.231.180.0/22, + 103.231.185.0/24, + 103.231.186.0/23, + 103.231.244.0/22, + 103.232.4.0/22, + 103.232.144.0/22, + 103.232.166.0/23, + 103.232.188.0/22, + 103.233.4.0/22, + 103.233.44.0/22, + 103.233.52.0/22, + 103.233.104.0/22, + 103.233.128.0/22, + 103.233.136.0/22, + 103.233.162.0/23, + 103.233.178.0/24, + 103.233.228.0/22, + 103.234.0.0/22, + 103.234.20.0/22, + 103.234.56.0/22, + 103.234.128.0/22, + 103.234.172.0/22, + 103.234.180.0/22, + 103.234.244.0/22, + 103.235.48.0/22, + 103.235.56.0/21, + 103.235.80.0/22, + 103.235.85.0/24, + 103.235.87.0/24, + 103.235.100.0/22, + 103.235.132.0/22, + 103.235.136.0/21, + 103.235.144.0/21, + 103.235.184.0/22, + 103.235.192.0/22, + 103.235.200.0/22, + 103.235.220.0/22, + 103.235.224.0/19, + 103.236.0.0/18, + 103.236.64.0/19, + 103.236.96.0/22, + 103.236.116.0/23, + 103.236.120.0/22, + 103.236.184.0/22, + 103.236.220.0/22, + 103.236.232.0/22, + 103.236.240.0/21, + 103.236.248.0/23, + 103.236.250.0/24, + 103.236.252.0/22, + 103.237.0.0/20, + 103.237.24.0/21, + 103.237.68.0/22, + 103.237.88.0/22, + 103.237.152.0/22, + 103.237.176.0/20, + 103.237.192.0/18, + 103.238.0.0/21, + 103.238.18.0/23, + 103.238.20.0/22, + 103.238.24.0/21, + 103.238.32.0/21, + 103.238.40.0/22, + 103.238.46.0/23, + 103.238.48.0/21, + 103.238.56.0/22, + 103.238.88.0/21, + 103.238.96.0/22, + 103.238.132.0/22, + 103.238.140.0/22, + 103.238.144.0/22, + 103.238.152.0/23, + 103.238.160.0/19, + 103.238.196.0/22, + 103.238.204.0/22, + 103.238.252.0/22, + 103.239.44.0/22, + 103.239.68.0/22, + 103.239.152.0/21, + 103.239.180.0/22, + 103.239.184.0/22, + 103.239.192.0/21, + 103.239.204.0/22, + 103.239.208.0/22, + 103.239.224.0/22, + 103.239.244.0/22, + 103.240.16.0/22, + 103.240.36.0/22, + 103.240.42.0/23, + 103.240.72.0/22, + 103.240.84.0/22, + 103.240.124.0/22, + 103.240.172.0/22, + 103.240.188.0/22, + 103.240.201.0/24, + 103.240.202.0/23, + 103.240.244.0/22, + 103.241.12.0/22, + 103.241.92.0/22, + 103.241.96.0/22, + 103.241.160.0/22, + 103.241.172.0/23, + 103.241.184.0/21, + 103.241.220.0/22, + 103.242.64.0/22, + 103.242.128.0/22, + 103.242.160.0/22, + 103.242.168.0/21, + 103.242.176.0/22, + 103.242.200.0/22, + 103.242.212.0/22, + 103.242.220.0/22, + 103.242.240.0/22, + 103.243.136.0/22, + 103.243.252.0/22, + 103.244.16.0/22, + 103.244.26.0/23, + 103.244.58.0/23, + 103.244.60.0/22, + 103.244.64.0/20, + 103.244.80.0/21, + 103.244.119.0/24, + 103.244.164.0/22, + 103.244.232.0/22, + 103.244.252.0/22, + 103.245.23.0/24, + 103.245.52.0/22, + 103.245.60.0/22, + 103.245.80.0/22, + 103.245.124.0/22, + 103.245.128.0/22, + 103.246.8.0/21, + 103.246.120.0/21, + 103.246.132.0/22, + 103.246.152.0/22, + 103.247.168.0/21, + 103.247.176.0/22, + 103.247.191.0/24, + 103.247.200.0/22, + 103.247.212.0/22, + 103.248.0.0/23, + 103.248.100.0/22, + 103.248.124.0/22, + 103.248.152.0/22, + 103.248.168.0/22, + 103.248.192.0/22, + 103.248.212.0/22, + 103.248.224.0/22, + 103.249.8.0/21, + 103.249.52.0/22, + 103.249.128.0/22, + 103.249.136.0/22, + 103.249.144.0/22, + 103.249.164.0/22, + 103.249.168.0/21, + 103.249.176.0/22, + 103.249.188.0/22, + 103.249.192.0/22, + 103.249.244.0/22, + 103.249.252.0/22, + 103.250.32.0/22, + 103.250.104.0/22, + 103.250.124.0/22, + 103.250.180.0/22, + 103.250.192.0/22, + 103.250.216.0/22, + 103.250.224.0/22, + 103.250.236.0/22, + 103.250.248.0/21, + 103.251.32.0/22, + 103.251.84.0/22, + 103.251.96.0/22, + 103.251.124.0/22, + 103.251.129.0/24, + 103.251.131.0/24, + 103.251.160.0/22, + 103.251.192.0/22, + 103.251.204.0/22, + 103.251.240.0/22, + 103.252.28.0/22, + 103.252.36.0/22, + 103.252.64.0/22, + 103.252.96.0/22, + 103.252.104.0/22, + 103.252.172.0/22, + 103.252.204.0/22, + 103.252.208.0/22, + 103.252.232.0/22, + 103.252.248.0/22, + 103.253.4.0/22, + 103.253.60.0/22, + 103.253.204.0/22, + 103.253.220.0/22, + 103.253.224.0/22, + 103.253.232.0/22, + 103.254.8.0/22, + 103.254.20.0/22, + 103.254.64.0/21, + 103.254.76.0/22, + 103.254.112.0/22, + 103.254.176.0/22, + 103.254.188.0/22, + 103.255.68.0/22, + 103.255.88.0/21, + 103.255.136.0/21, + 103.255.184.0/22, + 103.255.200.0/22, + 103.255.212.0/22, + 103.255.228.0/22, + 104.192.108.0/23, + 104.192.110.0/24, + 106.0.0.0/24, + 106.0.2.0/23, + 106.0.4.0/22, + 106.0.8.0/21, + 106.0.16.0/20, + 106.0.44.0/22, + 106.0.64.0/18, + 106.2.0.0/17, + 106.2.128.0/20, + 106.2.144.0/21, + 106.2.152.0/22, + 106.2.156.0/23, + 106.2.160.0/19, + 106.2.192.0/18, + 106.3.0.0/18, + 106.3.64.0/20, + 106.3.80.0/22, + 106.3.88.0/21, + 106.3.96.0/19, + 106.3.128.0/17, + 106.4.0.0/14, + 106.8.0.0/15, + 106.11.0.0/16, + 106.12.0.0/14, + 106.16.0.0/12, + 106.32.0.0/12, + 106.48.64.0/18, + 106.50.0.0/16, + 106.52.0.0/14, + 106.56.0.0/13, + 106.74.0.0/16, + 106.75.0.0/17, + 106.75.128.0/18, + 106.75.200.0/21, + 106.75.208.0/20, + 106.75.224.0/19, + 106.80.0.0/12, + 106.108.0.0/14, + 106.112.0.0/12, + 106.224.0.0/12, + 109.244.0.0/16, + 110.6.0.0/15, + 110.16.0.0/14, + 110.34.40.0/21, + 110.40.0.0/15, + 110.42.0.0/20, + 110.42.16.0/24, + 110.42.18.0/23, + 110.42.20.0/22, + 110.42.24.0/21, + 110.42.32.0/19, + 110.42.64.0/18, + 110.42.128.0/17, + 110.43.0.0/16, + 110.44.12.0/22, + 110.44.144.0/20, + 110.48.0.0/16, + 110.51.0.0/16, + 110.52.0.0/15, + 110.56.0.0/13, + 110.64.0.0/15, + 110.72.0.0/15, + 110.75.0.0/16, + 110.76.0.0/18, + 110.76.132.0/22, + 110.76.156.0/22, + 110.76.184.0/22, + 110.76.192.0/18, + 110.77.0.0/17, + 110.80.0.0/13, + 110.88.0.0/14, + 110.92.68.0/22, + 110.93.32.0/19, + 110.94.0.0/15, + 110.96.0.0/15, + 110.98.0.0/16, + 110.100.0.0/14, + 110.104.0.0/13, + 110.112.0.0/13, + 110.120.0.0/16, + 110.121.0.0/22, + 110.121.8.0/21, + 110.121.16.0/20, + 110.121.32.0/19, + 110.121.64.0/18, + 110.121.128.0/17, + 110.122.0.0/15, + 110.124.0.0/14, + 110.152.0.0/14, + 110.156.0.0/15, + 110.165.32.0/19, + 110.166.0.0/15, + 110.172.192.0/18, + 110.173.0.0/19, + 110.173.32.0/20, + 110.173.64.0/18, + 110.173.192.0/19, + 110.176.0.0/12, + 110.192.0.0/11, + 110.228.0.0/14, + 110.232.32.0/19, + 110.236.0.0/15, + 110.240.0.0/12, + 111.0.0.0/10, + 111.66.0.0/21, + 111.66.12.0/22, + 111.66.16.0/22, + 111.66.28.0/22, + 111.66.32.0/21, + 111.66.44.0/22, + 111.66.52.0/22, + 111.66.60.0/22, + 111.66.64.0/18, + 111.66.128.0/19, + 111.66.160.0/20, + 111.66.176.0/23, + 111.66.178.0/24, + 111.66.180.0/22, + 111.66.184.0/21, + 111.67.192.0/20, + 111.68.64.0/19, + 111.72.0.0/13, + 111.85.0.0/16, + 111.92.248.0/21, + 111.112.0.0/14, + 111.116.0.0/15, + 111.118.200.0/21, + 111.119.64.0/18, + 111.119.128.0/19, + 111.120.0.0/14, + 111.124.0.0/16, + 111.126.0.0/15, + 111.128.0.0/11, + 111.160.0.0/13, + 111.170.0.0/16, + 111.172.0.0/14, + 111.176.0.0/13, + 111.186.0.0/15, + 111.192.0.0/12, + 111.208.0.0/13, + 111.221.28.0/24, + 111.221.128.0/17, + 111.222.0.0/16, + 111.223.4.0/22, + 111.223.12.0/22, + 111.223.16.0/22, + 111.223.240.0/22, + 111.223.248.0/22, + 111.224.0.0/13, + 111.235.96.0/19, + 111.235.156.0/22, + 111.235.160.0/19, + 112.0.0.0/10, + 112.64.0.0/14, + 112.73.232.0/24, + 112.74.0.0/15, + 112.80.0.0/12, + 112.96.0.0/13, + 112.109.128.0/17, + 112.111.0.0/16, + 112.112.0.0/14, + 112.116.0.0/15, + 112.122.0.0/15, + 112.124.0.0/14, + 112.128.0.0/14, + 112.132.0.0/16, + 112.137.48.0/21, + 112.192.0.0/14, + 112.224.0.0/11, + 113.0.0.0/13, + 113.8.0.0/15, + 113.11.192.0/19, + 113.12.0.0/14, + 113.16.0.0/15, + 113.18.0.0/16, + 113.21.232.0/21, + 113.24.0.0/14, + 113.31.0.0/16, + 113.44.0.0/14, + 113.48.0.0/14, + 113.52.160.0/19, + 113.52.228.0/22, + 113.54.0.0/15, + 113.56.0.0/15, + 113.58.0.0/16, + 113.59.0.0/17, + 113.59.224.0/22, + 113.62.0.0/15, + 113.64.0.0/10, + 113.128.0.0/15, + 113.130.96.0/20, + 113.130.112.0/21, + 113.132.0.0/14, + 113.136.0.0/13, + 113.192.40.0/23, + 113.194.0.0/15, + 113.197.100.0/22, + 113.200.0.0/15, + 113.202.0.0/16, + 113.204.0.0/14, + 113.208.96.0/19, + 113.208.128.0/17, + 113.209.0.0/16, + 113.212.0.0/18, + 113.212.100.0/22, + 113.212.184.0/21, + 113.213.0.0/17, + 113.214.0.0/15, + 113.218.0.0/15, + 113.220.0.0/14, + 113.224.0.0/12, + 113.240.0.0/13, + 113.248.0.0/14, + 114.28.0.0/17, + 114.28.128.0/18, + 114.28.193.0/24, + 114.28.194.0/24, + 114.28.197.0/24, + 114.28.198.0/23, + 114.28.201.0/24, + 114.28.203.0/24, + 114.28.205.0/24, + 114.28.206.0/23, + 114.28.208.0/23, + 114.28.210.0/24, + 114.28.216.0/23, + 114.28.220.0/22, + 114.28.225.0/24, + 114.28.230.0/24, + 114.28.232.0/24, + 114.28.235.0/24, + 114.28.236.0/23, + 114.28.239.0/24, + 114.28.240.0/20, + 114.31.64.0/21, + 114.54.0.0/15, + 114.60.0.0/14, + 114.64.0.0/15, + 114.66.0.0/17, + 114.66.224.0/22, + 114.66.236.0/22, + 114.66.240.0/20, + 114.67.0.0/16, + 114.68.0.0/16, + 114.79.64.0/18, + 114.80.0.0/12, + 114.96.0.0/13, + 114.104.0.0/14, + 114.110.0.0/20, + 114.110.64.0/18, + 114.111.0.0/19, + 114.111.160.0/20, + 114.111.176.0/22, + 114.111.190.0/23, + 114.112.4.0/22, + 114.112.8.0/22, + 114.112.12.0/23, + 114.112.16.0/23, + 114.112.20.0/22, + 114.112.24.0/21, + 114.112.32.0/19, + 114.112.64.0/18, + 114.112.128.0/18, + 114.112.192.0/19, + 114.112.224.0/22, + 114.112.228.0/24, + 114.112.230.0/23, + 114.112.234.0/23, + 114.112.240.0/23, + 114.112.245.0/24, + 114.112.247.0/24, + 114.112.248.0/22, + 114.112.252.0/23, + 114.113.0.0/17, + 114.113.128.0/21, + 114.113.140.0/22, + 114.113.144.0/20, + 114.113.160.0/19, + 114.113.192.0/19, + 114.113.224.0/20, + 114.113.242.0/24, + 114.113.246.0/23, + 114.113.248.0/21, + 114.114.0.0/15, + 114.116.0.0/15, + 114.118.0.0/16, + 114.119.0.0/17, + 114.119.192.0/21, + 114.119.204.0/22, + 114.119.208.0/20, + 114.119.224.0/19, + 114.132.0.0/16, + 114.135.0.0/16, + 114.138.0.0/15, + 114.141.64.0/21, + 114.141.80.0/21, + 114.141.128.0/18, + 114.142.136.0/21, + 114.196.0.0/15, + 114.198.248.0/21, + 114.208.0.0/12, + 114.224.0.0/11, + 115.24.0.0/14, + 115.28.0.0/15, + 115.31.64.0/20, + 115.32.0.0/14, + 115.42.56.0/22, + 115.44.0.0/14, + 115.48.0.0/12, + 115.69.64.0/20, + 115.84.0.0/18, + 115.85.192.0/18, + 115.100.0.0/14, + 115.104.0.0/14, + 115.120.0.0/14, + 115.124.16.0/20, + 115.148.0.0/14, + 115.152.0.0/13, + 115.166.64.0/19, + 115.168.0.0/13, + 115.180.0.0/14, + 115.187.0.0/20, + 115.190.0.0/15, + 115.192.0.0/11, + 115.224.0.0/12, + 116.0.8.0/21, + 116.0.24.0/21, + 116.1.0.0/16, + 116.2.0.0/15, + 116.4.0.0/14, + 116.8.0.0/14, + 116.13.0.0/16, + 116.16.0.0/12, + 116.50.0.0/20, + 116.52.0.0/14, + 116.56.0.0/15, + 116.58.128.0/20, + 116.58.208.0/20, + 116.60.0.0/14, + 116.66.0.0/17, + 116.68.136.0/21, + 116.68.176.0/21, + 116.69.0.0/16, + 116.70.0.0/17, + 116.76.0.0/14, + 116.85.0.0/16, + 116.89.144.0/20, + 116.90.80.0/20, + 116.90.184.0/21, + 116.95.0.0/16, + 116.112.0.0/14, + 116.116.0.0/15, + 116.128.0.0/10, + 116.192.0.0/16, + 116.193.16.0/20, + 116.193.32.0/19, + 116.193.176.0/21, + 116.194.0.0/15, + 116.196.0.0/21, + 116.196.8.0/22, + 116.196.12.0/23, + 116.196.14.0/24, + 116.196.16.0/20, + 116.196.32.0/19, + 116.196.64.0/18, + 116.196.128.0/17, + 116.197.160.0/21, + 116.198.0.0/16, + 116.199.0.0/17, + 116.199.128.0/20, + 116.199.144.0/21, + 116.204.0.0/17, + 116.205.0.0/16, + 116.207.0.0/16, + 116.208.0.0/14, + 116.212.160.0/20, + 116.213.44.0/24, + 116.213.46.0/23, + 116.213.64.0/18, + 116.213.128.0/17, + 116.214.32.0/19, + 116.214.64.0/20, + 116.214.128.0/17, + 116.215.0.0/16, + 116.216.0.0/14, + 116.224.0.0/12, + 116.242.0.0/15, + 116.244.0.0/14, + 116.248.0.0/15, + 116.252.0.0/15, + 116.254.104.0/21, + 116.254.128.0/17, + 116.255.128.0/17, + 117.8.0.0/13, + 117.21.0.0/16, + 117.22.0.0/15, + 117.24.0.0/13, + 117.32.0.0/13, + 117.40.0.0/14, + 117.44.0.0/15, + 117.48.0.0/15, + 117.50.0.0/16, + 117.51.128.0/17, + 117.53.48.0/20, + 117.53.176.0/20, + 117.57.0.0/16, + 117.58.0.0/17, + 117.59.0.0/16, + 117.60.0.0/14, + 117.64.0.0/13, + 117.72.0.0/15, + 117.74.64.0/19, + 117.74.128.0/17, + 117.75.0.0/16, + 117.76.0.0/14, + 117.80.0.0/12, + 117.100.0.0/15, + 117.103.16.0/20, + 117.103.40.0/21, + 117.103.72.0/21, + 117.103.128.0/20, + 117.104.168.0/21, + 117.106.0.0/15, + 117.112.0.0/13, + 117.120.64.0/18, + 117.120.148.0/22, + 117.120.164.0/22, + 117.120.184.0/21, + 117.120.208.0/21, + 117.120.216.0/22, + 117.120.248.0/21, + 117.121.0.0/17, + 117.121.128.0/18, + 117.121.192.0/21, + 117.122.128.0/17, + 117.124.0.0/14, + 117.128.0.0/14, + 117.132.0.0/15, + 117.134.0.0/17, + 117.134.128.0/18, + 117.134.207.0/24, + 117.134.208.0/23, + 117.134.212.0/23, + 117.134.216.0/22, + 117.134.222.0/23, + 117.134.232.0/21, + 117.134.240.0/20, + 117.135.0.0/16, + 117.136.0.0/13, + 117.144.0.0/12, + 117.160.0.0/11, + 118.24.0.0/15, + 118.26.0.0/19, + 118.26.32.0/22, + 118.26.40.0/21, + 118.26.48.0/21, + 118.26.64.0/19, + 118.26.96.0/21, + 118.26.112.0/20, + 118.26.128.0/22, + 118.26.133.0/24, + 118.26.134.0/23, + 118.26.136.0/21, + 118.26.160.0/20, + 118.26.188.0/22, + 118.26.192.0/18, + 118.28.0.0/14, + 118.64.0.0/15, + 118.66.0.0/16, + 118.67.112.0/20, + 118.72.0.0/13, + 118.80.0.0/15, + 118.84.0.0/15, + 118.88.32.0/19, + 118.88.64.0/18, + 118.88.128.0/17, + 118.89.0.0/16, + 118.91.240.0/20, + 118.102.16.0/20, + 118.102.32.0/21, + 118.103.164.0/22, + 118.103.168.0/21, + 118.103.176.0/22, + 118.112.0.0/13, + 118.120.0.0/14, + 118.124.0.0/15, + 118.126.0.0/16, + 118.127.128.0/19, + 118.132.0.0/14, + 118.144.0.0/14, + 118.178.0.0/16, + 118.180.0.0/14, + 118.184.0.0/16, + 118.186.0.0/15, + 118.188.0.0/16, + 118.190.0.0/15, + 118.192.0.0/16, + 118.193.0.0/20, + 118.193.48.0/21, + 118.193.96.0/19, + 118.193.128.0/17, + 118.194.0.0/17, + 118.194.128.0/18, + 118.194.192.0/19, + 118.194.227.0/24, + 118.194.240.0/21, + 118.195.0.0/16, + 118.196.0.0/14, + 118.202.0.0/15, + 118.204.0.0/14, + 118.212.0.0/15, + 118.215.192.0/18, + 118.224.0.0/14, + 118.228.0.0/15, + 118.230.0.0/16, + 118.239.0.0/16, + 118.242.0.0/16, + 118.244.0.0/14, + 118.248.0.0/13, + 119.0.0.0/15, + 119.2.0.0/19, + 119.2.128.0/17, + 119.3.0.0/16, + 119.4.0.0/14, + 119.10.0.0/17, + 119.15.136.0/21, + 119.16.0.0/16, + 119.18.192.0/20, + 119.18.208.0/21, + 119.18.224.0/19, + 119.19.0.0/16, + 119.20.0.0/14, + 119.27.64.0/18, + 119.27.128.0/17, + 119.28.28.0/24, + 119.28.240.0/20, + 119.29.0.0/16, + 119.30.48.0/20, + 119.31.192.0/19, + 119.32.0.0/13, + 119.40.0.0/18, + 119.40.64.0/20, + 119.40.128.0/17, + 119.41.0.0/16, + 119.42.0.0/19, + 119.42.128.0/20, + 119.42.224.0/19, + 119.44.0.0/15, + 119.48.0.0/13, + 119.57.0.0/16, + 119.58.0.0/16, + 119.59.128.0/24, + 119.59.130.0/24, + 119.59.132.0/22, + 119.59.136.0/21, + 119.59.144.0/20, + 119.59.160.0/19, + 119.59.192.0/18, + 119.60.0.0/15, + 119.62.0.0/16, + 119.63.32.0/19, + 119.75.208.0/20, + 119.78.0.0/15, + 119.80.0.0/16, + 119.82.208.0/20, + 119.84.0.0/14, + 119.88.0.0/14, + 119.96.0.0/13, + 119.108.0.0/15, + 119.112.0.0/12, + 119.128.0.0/12, + 119.144.0.0/14, + 119.148.160.0/19, + 119.151.192.0/18, + 119.160.200.0/21, + 119.161.120.0/21, + 119.161.128.0/21, + 119.161.144.0/21, + 119.161.152.0/23, + 119.161.160.0/19, + 119.161.192.0/18, + 119.162.0.0/15, + 119.164.0.0/14, + 119.176.0.0/12, + 119.232.0.0/15, + 119.235.128.0/18, + 119.248.0.0/14, + 119.252.96.0/21, + 119.252.240.0/21, + 119.252.248.0/23, + 119.252.250.0/24, + 119.252.252.0/23, + 119.252.255.0/24, + 119.253.0.0/16, + 119.254.0.0/15, + 120.0.0.0/12, + 120.24.0.0/14, + 120.30.0.0/15, + 120.32.0.0/12, + 120.48.0.0/15, + 120.52.0.0/14, + 120.64.0.0/13, + 120.72.32.0/19, + 120.72.128.0/17, + 120.76.0.0/14, + 120.80.0.0/13, + 120.88.8.0/21, + 120.90.0.0/15, + 120.92.0.0/16, + 120.94.0.0/15, + 120.128.0.0/13, + 120.136.16.0/21, + 120.136.128.0/18, + 120.137.0.0/17, + 120.143.128.0/19, + 120.192.0.0/10, + 121.0.8.0/21, + 121.0.16.0/20, + 121.4.0.0/15, + 121.8.0.0/13, + 121.16.0.0/12, + 121.32.0.0/13, + 121.40.0.0/14, + 121.46.0.0/18, + 121.46.76.0/22, + 121.46.128.0/21, + 121.46.141.0/24, + 121.46.160.0/19, + 121.46.192.0/18, + 121.47.0.0/16, + 121.48.0.0/15, + 121.50.8.0/21, + 121.51.0.0/16, + 121.52.160.0/19, + 121.52.208.0/20, + 121.52.224.0/19, + 121.54.176.0/21, + 121.55.0.0/18, + 121.56.0.0/15, + 121.58.0.0/17, + 121.58.136.0/21, + 121.58.144.0/20, + 121.58.160.0/21, + 121.59.0.0/19, + 121.59.32.0/22, + 121.59.36.0/23, + 121.59.39.0/24, + 121.59.40.0/21, + 121.59.48.0/20, + 121.59.64.0/19, + 121.59.96.0/22, + 121.59.101.0/24, + 121.59.102.0/23, + 121.59.104.0/23, + 121.59.107.0/24, + 121.59.108.0/24, + 121.59.110.0/23, + 121.59.112.0/21, + 121.59.121.0/24, + 121.59.123.0/24, + 121.59.125.0/24, + 121.59.126.0/23, + 121.59.128.0/21, + 121.59.136.0/22, + 121.59.141.0/24, + 121.59.142.0/23, + 121.59.144.0/20, + 121.59.160.0/19, + 121.59.192.0/18, + 121.60.0.0/14, + 121.68.0.0/14, + 121.76.0.0/15, + 121.79.128.0/18, + 121.89.0.0/16, + 121.91.104.0/21, + 121.100.128.0/17, + 121.101.0.0/18, + 121.101.208.0/20, + 121.192.0.0/13, + 121.200.192.0/21, + 121.201.0.0/16, + 121.204.0.0/14, + 121.224.0.0/12, + 121.248.0.0/14, + 121.255.0.0/16, + 122.0.64.0/18, + 122.0.128.0/17, + 122.4.0.0/14, + 122.8.52.0/22, + 122.8.57.0/24, + 122.8.58.0/23, + 122.8.60.0/23, + 122.8.63.0/24, + 122.8.68.0/22, + 122.8.76.0/22, + 122.8.80.0/23, + 122.8.112.0/21, + 122.8.124.0/22, + 122.8.192.0/18, + 122.9.0.0/16, + 122.10.129.0/24, + 122.10.131.0/24, + 122.10.132.0/23, + 122.10.136.0/23, + 122.10.164.0/22, + 122.10.168.0/21, + 122.10.176.0/20, + 122.10.192.0/22, + 122.10.200.0/22, + 122.10.205.0/24, + 122.10.206.0/23, + 122.10.208.0/21, + 122.10.216.0/22, + 122.10.228.0/22, + 122.10.232.0/21, + 122.10.240.0/22, + 122.11.0.0/17, + 122.12.0.0/15, + 122.14.0.0/17, + 122.14.128.0/20, + 122.14.144.0/22, + 122.14.148.0/24, + 122.14.150.0/23, + 122.14.152.0/23, + 122.14.155.0/24, + 122.14.157.0/24, + 122.14.158.0/23, + 122.14.160.0/19, + 122.14.192.0/18, + 122.48.0.0/16, + 122.49.0.0/18, + 122.51.0.0/16, + 122.64.0.0/11, + 122.96.0.0/15, + 122.102.0.0/20, + 122.102.64.0/19, + 122.112.0.0/18, + 122.112.64.0/19, + 122.112.96.0/20, + 122.112.112.0/21, + 122.112.120.0/23, + 122.112.125.0/24, + 122.112.126.0/23, + 122.112.128.0/17, + 122.113.0.0/19, + 122.113.32.0/20, + 122.113.48.0/23, + 122.113.51.0/24, + 122.113.52.0/22, + 122.113.56.0/21, + 122.113.64.0/18, + 122.113.128.0/17, + 122.114.0.0/16, + 122.115.0.0/18, + 122.115.64.0/22, + 122.115.70.0/23, + 122.115.72.0/24, + 122.115.74.0/23, + 122.115.76.0/22, + 122.115.80.0/20, + 122.115.96.0/19, + 122.115.128.0/17, + 122.119.0.0/16, + 122.128.100.0/22, + 122.128.120.0/21, + 122.136.0.0/13, + 122.144.128.0/17, + 122.152.192.0/18, + 122.156.0.0/14, + 122.188.0.0/14, + 122.192.0.0/14, + 122.198.0.0/16, + 122.200.40.0/21, + 122.200.64.0/18, + 122.201.48.0/20, + 122.204.0.0/14, + 122.224.0.0/12, + 122.240.0.0/13, + 122.248.24.0/21, + 122.248.48.0/20, + 122.255.64.0/21, + 123.0.128.0/18, + 123.4.0.0/14, + 123.8.0.0/13, + 123.49.128.0/18, + 123.49.192.0/23, + 123.49.194.0/24, + 123.49.197.0/24, + 123.49.198.0/23, + 123.49.200.0/21, + 123.49.208.0/20, + 123.49.224.0/21, + 123.49.232.0/22, + 123.49.236.0/24, + 123.49.240.0/24, + 123.49.242.0/23, + 123.49.248.0/21, + 123.50.160.0/19, + 123.52.0.0/14, + 123.56.0.0/15, + 123.58.0.0/17, + 123.58.128.0/18, + 123.58.224.0/19, + 123.59.0.0/16, + 123.60.0.0/15, + 123.62.0.0/16, + 123.64.0.0/11, + 123.96.0.0/15, + 123.98.0.0/21, + 123.98.12.0/22, + 123.98.24.0/21, + 123.98.32.0/19, + 123.98.64.0/18, + 123.99.128.0/19, + 123.99.160.0/22, + 123.99.164.0/24, + 123.99.166.0/23, + 123.99.168.0/21, + 123.99.176.0/20, + 123.99.192.0/18, + 123.100.0.0/19, + 123.101.0.0/16, + 123.103.0.0/17, + 123.108.88.0/23, + 123.108.130.0/23, + 123.108.132.0/22, + 123.108.136.0/21, + 123.108.208.0/20, + 123.112.0.0/12, + 123.128.0.0/13, + 123.136.88.0/22, + 123.137.0.0/16, + 123.138.0.0/15, + 123.144.0.0/12, + 123.160.0.0/12, + 123.176.60.0/22, + 123.176.80.0/20, + 123.177.0.0/16, + 123.178.0.0/15, + 123.180.0.0/14, + 123.184.0.0/13, + 123.196.0.0/15, + 123.199.128.0/17, + 123.206.0.0/15, + 123.232.0.0/14, + 123.242.0.0/17, + 123.242.192.0/21, + 123.244.0.0/14, + 123.249.0.0/16, + 123.253.52.0/23, + 123.254.96.0/21, + 124.6.64.0/18, + 124.14.0.0/15, + 124.16.0.0/15, + 124.20.0.0/16, + 124.21.32.0/19, + 124.21.64.0/18, + 124.21.128.0/17, + 124.22.0.0/15, + 124.28.192.0/18, + 124.29.0.0/17, + 124.31.0.0/16, + 124.40.112.0/20, + 124.40.128.0/18, + 124.40.192.0/19, + 124.40.240.0/22, + 124.42.0.0/16, + 124.47.0.0/18, + 124.64.0.0/15, + 124.66.0.0/17, + 124.67.0.0/16, + 124.68.16.0/20, + 124.68.64.0/18, + 124.68.176.0/20, + 124.68.224.0/20, + 124.68.244.0/22, + 124.68.248.0/21, + 124.69.0.0/16, + 124.70.0.0/16, + 124.71.0.0/17, + 124.71.128.0/18, + 124.71.192.0/19, + 124.71.224.0/20, + 124.71.240.0/21, + 124.71.250.0/23, + 124.71.254.0/23, + 124.72.0.0/13, + 124.88.0.0/13, + 124.108.8.0/21, + 124.108.40.0/21, + 124.109.96.0/21, + 124.112.0.0/13, + 124.126.0.0/15, + 124.128.0.0/13, + 124.147.128.0/17, + 124.150.137.0/24, + 124.151.0.0/16, + 124.152.0.0/16, + 124.160.0.0/13, + 124.172.87.0/24, + 124.172.88.0/23, + 124.172.91.0/24, + 124.172.93.0/24, + 124.172.128.0/17, + 124.173.64.0/18, + 124.173.128.0/18, + 124.173.205.0/24, + 124.174.0.0/15, + 124.192.0.0/15, + 124.196.0.0/16, + 124.197.56.0/21, + 124.200.0.0/13, + 124.220.0.0/14, + 124.224.0.0/12, + 124.240.0.0/17, + 124.240.128.0/18, + 124.242.0.0/16, + 124.243.192.0/18, + 124.248.0.0/17, + 124.249.0.0/16, + 124.250.0.0/15, + 124.254.0.0/18, + 125.31.192.0/18, + 125.32.0.0/12, + 125.58.128.0/17, + 125.61.128.0/17, + 125.62.0.0/18, + 125.64.0.0/12, + 125.80.0.0/13, + 125.88.0.0/14, + 125.92.0.0/16, + 125.93.0.0/20, + 125.93.16.0/21, + 125.93.28.0/22, + 125.93.32.0/19, + 125.93.64.0/18, + 125.93.128.0/17, + 125.94.0.0/15, + 125.96.0.0/15, + 125.98.0.0/16, + 125.104.0.0/13, + 125.112.0.0/12, + 125.169.0.0/16, + 125.171.0.0/16, + 125.208.0.0/18, + 125.210.0.0/15, + 125.213.0.0/17, + 125.214.96.0/19, + 125.215.0.0/18, + 125.216.0.0/13, + 125.254.128.0/17, + 128.108.0.0/16, + 129.28.0.0/16, + 129.204.0.0/16, + 129.211.0.0/16, + 132.232.0.0/16, + 132.237.134.0/24, + 134.49.240.0/22, + 134.175.0.0/16, + 137.59.59.0/24, + 137.59.88.0/22, + 138.252.72.0/23, + 138.252.78.0/23, + 138.252.96.0/24, + 138.252.118.0/23, + 138.252.162.0/23, + 139.5.56.0/21, + 139.5.80.0/22, + 139.5.92.0/22, + 139.5.128.0/22, + 139.5.160.0/22, + 139.5.192.0/22, + 139.5.204.0/22, + 139.5.212.0/22, + 139.5.244.0/22, + 139.9.0.0/16, + 139.129.0.0/16, + 139.148.0.0/16, + 139.155.0.0/16, + 139.159.0.0/19, + 139.159.32.0/21, + 139.159.40.0/22, + 139.159.52.0/22, + 139.159.56.0/21, + 139.159.64.0/19, + 139.159.96.0/20, + 139.159.112.0/22, + 139.159.116.0/23, + 139.159.120.0/21, + 139.159.128.0/17, + 139.170.0.0/16, + 139.176.0.0/16, + 139.183.0.0/18, + 139.183.64.0/19, + 139.183.96.0/22, + 139.183.101.0/24, + 139.183.102.0/23, + 139.183.104.0/21, + 139.183.112.0/20, + 139.183.128.0/18, + 139.183.192.0/20, + 139.183.208.0/23, + 139.183.211.0/24, + 139.183.212.0/22, + 139.183.216.0/21, + 139.183.224.0/19, + 139.186.0.0/16, + 139.189.0.0/18, + 139.189.64.0/19, + 139.189.96.0/20, + 139.189.112.0/21, + 139.189.120.0/22, + 139.189.124.0/23, + 139.189.126.0/24, + 139.189.128.0/17, + 139.196.0.0/15, + 139.198.0.0/18, + 139.198.66.0/23, + 139.198.68.0/22, + 139.198.72.0/21, + 139.198.80.0/20, + 139.198.96.0/19, + 139.198.128.0/17, + 139.199.0.0/16, + 139.200.0.0/13, + 139.208.0.0/13, + 139.217.0.0/16, + 139.219.0.0/16, + 139.220.0.0/15, + 139.224.0.0/16, + 139.226.0.0/15, + 140.75.0.0/16, + 140.143.0.0/16, + 140.179.0.0/16, + 140.205.0.0/16, + 140.206.0.0/15, + 140.210.0.0/20, + 140.210.16.0/21, + 140.210.24.0/22, + 140.210.28.0/23, + 140.210.30.0/24, + 140.210.32.0/19, + 140.210.64.0/18, + 140.210.128.0/17, + 140.224.0.0/16, + 140.237.0.0/16, + 140.240.0.0/16, + 140.243.0.0/16, + 140.246.0.0/16, + 140.249.0.0/16, + 140.250.0.0/16, + 140.255.0.0/16, + 142.86.192.0/19, + 142.86.224.0/20, + 142.86.240.0/21, + 142.86.248.0/22, + 142.86.254.0/23, + 143.64.0.0/16, + 144.0.0.0/16, + 144.7.0.0/16, + 144.12.0.0/16, + 144.36.147.0/24, + 144.48.64.0/22, + 144.48.88.0/22, + 144.48.156.0/22, + 144.48.180.0/22, + 144.48.184.0/22, + 144.48.204.0/22, + 144.48.208.0/21, + 144.52.0.0/16, + 144.79.4.0/23, + 144.79.10.0/23, + 144.79.28.0/23, + 144.79.42.0/23, + 144.79.54.0/23, + 144.79.84.0/23, + 144.79.148.0/23, + 144.123.0.0/16, + 144.255.0.0/16, + 145.14.71.0/24, + 145.14.72.0/22, + 145.14.81.0/24, + 145.14.82.0/24, + 145.14.84.0/24, + 145.14.86.0/24, + 145.14.88.0/24, + 145.14.90.0/24, + 146.56.192.0/18, + 146.196.56.0/22, + 146.196.68.0/22, + 146.196.72.0/22, + 146.196.92.0/22, + 146.196.112.0/21, + 146.196.124.0/22, + 146.222.79.0/24, + 146.222.81.0/24, + 146.222.94.0/24, + 147.78.132.0/24, + 147.78.134.0/24, + 148.70.0.0/16, + 149.41.64.0/18, + 149.41.192.0/19, + 150.0.0.0/16, + 150.115.0.0/16, + 150.121.0.0/16, + 150.122.0.0/16, + 150.129.136.0/22, + 150.129.192.0/22, + 150.129.252.0/22, + 150.138.0.0/15, + 150.158.0.0/16, + 150.223.0.0/16, + 150.242.0.0/21, + 150.242.8.0/22, + 150.242.28.0/22, + 150.242.44.0/22, + 150.242.48.0/21, + 150.242.56.0/22, + 150.242.76.0/22, + 150.242.80.0/22, + 150.242.92.0/22, + 150.242.96.0/22, + 150.242.112.0/21, + 150.242.120.0/22, + 150.242.152.0/22, + 150.242.156.0/23, + 150.242.160.0/21, + 150.242.168.0/22, + 150.242.184.0/21, + 150.242.192.0/22, + 150.242.212.0/24, + 150.242.215.0/24, + 150.242.224.0/22, + 150.242.232.0/21, + 150.242.240.0/21, + 150.242.248.0/22, + 150.248.0.0/16, + 150.255.0.0/16, + 151.158.14.0/23, + 151.158.78.0/23, + 151.158.80.0/23, + 151.158.122.0/23, + 152.104.128.0/17, + 152.136.0.0/16, + 153.0.0.0/16, + 153.3.0.0/16, + 153.34.0.0/15, + 153.36.0.0/15, + 153.99.0.0/16, + 153.101.0.0/16, + 153.118.0.0/15, + 154.8.128.0/17, + 154.72.44.0/24, + 154.72.47.0/24, + 155.126.176.0/23, + 156.107.160.0/23, + 156.107.170.0/24, + 156.107.179.0/24, + 156.107.181.0/24, + 157.0.0.0/16, + 157.10.34.0/23, + 157.10.36.0/23, + 157.10.112.0/23, + 157.10.118.0/23, + 157.10.130.0/23, + 157.10.218.0/23, + 157.10.220.0/23, + 157.10.246.0/23, + 157.15.74.0/23, + 157.15.94.0/23, + 157.15.100.0/22, + 157.15.104.0/23, + 157.18.0.0/16, + 157.20.136.0/23, + 157.20.194.0/23, + 157.20.246.0/23, + 157.61.0.0/16, + 157.66.42.0/23, + 157.66.70.0/23, + 157.66.88.0/21, + 157.66.164.0/23, + 157.66.244.0/23, + 157.119.8.0/21, + 157.119.16.0/22, + 157.119.28.0/22, + 157.119.132.0/22, + 157.119.136.0/21, + 157.119.144.0/20, + 157.119.160.0/21, + 157.119.172.0/22, + 157.119.192.0/21, + 157.119.240.0/22, + 157.119.252.0/22, + 157.122.0.0/16, + 157.148.0.0/16, + 157.156.0.0/16, + 157.255.0.0/16, + 158.60.0.0/16, + 158.79.0.0/16, + 158.140.252.0/23, + 158.140.255.0/24, + 159.27.0.0/16, + 159.75.0.0/16, + 159.226.0.0/16, + 160.19.76.0/23, + 160.19.82.0/23, + 160.19.208.0/21, + 160.19.216.0/22, + 160.20.16.0/22, + 160.20.48.0/22, + 160.20.130.0/23, + 160.22.58.0/23, + 160.22.82.0/23, + 160.22.148.0/23, + 160.22.188.0/23, + 160.22.224.0/23, + 160.22.230.0/23, + 160.22.244.0/22, + 160.25.10.0/23, + 160.25.12.0/23, + 160.25.194.0/23, + 160.30.40.0/23, + 160.30.194.0/23, + 160.30.196.0/22, + 160.30.230.0/23, + 160.83.110.0/24, + 160.187.223.0/24, + 160.187.252.0/22, + 160.191.0.0/23, + 160.191.104.0/23, + 160.191.110.0/23, + 160.202.60.0/23, + 160.202.62.0/24, + 160.202.148.0/22, + 160.202.152.0/22, + 160.202.212.0/22, + 160.202.216.0/21, + 160.202.224.0/19, + 160.236.78.0/23, + 160.236.148.0/23, + 160.236.162.0/23, + 160.236.235.0/24, + 160.236.250.0/23, + 160.250.14.0/23, + 160.250.16.0/22, + 160.250.84.0/23, + 160.250.90.0/23, + 160.250.102.0/23, + 160.250.104.0/23, + 160.250.160.0/23, + 160.250.170.0/23, + 160.250.214.0/23, + 160.250.252.0/23, + 161.163.0.0/21, + 161.163.28.0/23, + 161.189.0.0/16, + 161.207.0.0/16, + 161.248.20.0/23, + 161.248.42.0/23, + 161.248.92.0/23, + 161.248.110.0/23, + 161.248.112.0/24, + 161.248.136.0/24, + 162.4.54.0/23, + 162.4.168.0/23, + 162.4.198.0/23, + 162.4.202.0/23, + 162.4.230.0/23, + 162.14.0.0/19, + 162.14.32.0/21, + 162.14.43.0/24, + 162.14.44.0/22, + 162.14.52.0/22, + 162.14.56.0/22, + 162.14.64.0/18, + 162.14.128.0/17, + 162.105.0.0/16, + 163.0.0.0/16, + 163.47.4.0/22, + 163.52.28.0/23, + 163.52.76.0/23, + 163.52.108.0/23, + 163.53.0.0/20, + 163.53.36.0/22, + 163.53.40.0/21, + 163.53.48.0/20, + 163.53.64.0/22, + 163.53.88.0/21, + 163.53.96.0/19, + 163.53.128.0/21, + 163.53.136.0/22, + 163.53.160.0/20, + 163.53.188.0/22, + 163.53.240.0/22, + 163.61.63.0/24, + 163.61.178.0/23, + 163.61.202.0/23, + 163.61.214.0/23, + 163.125.0.0/16, + 163.128.30.0/23, + 163.128.152.0/23, + 163.128.236.0/23, + 163.128.254.0/23, + 163.142.0.0/16, + 163.177.0.0/16, + 163.179.0.0/16, + 163.204.0.0/16, + 163.223.28.0/23, + 163.223.32.0/23, + 163.223.72.0/23, + 163.223.124.0/23, + 163.223.217.0/24, + 163.223.222.0/23, + 163.227.34.0/23, + 163.227.40.0/23, + 163.227.74.0/23, + 163.227.76.0/22, + 163.227.80.0/23, + 163.228.0.0/16, + 163.244.246.0/24, + 164.52.5.0/24, + 164.52.61.0/24, + 164.52.66.0/24, + 164.52.75.0/24, + 164.52.78.0/24, + 164.52.85.0/24, + 164.52.99.0/24, + 165.101.4.0/23, + 165.101.66.0/23, + 165.101.70.0/24, + 165.101.144.0/23, + 165.101.170.0/23, + 165.101.208.0/23, + 165.101.244.0/23, + 166.111.0.0/16, + 167.139.0.0/16, + 167.189.0.0/16, + 167.220.244.0/22, + 168.160.0.0/16, + 170.179.0.0/16, + 171.8.0.0/13, + 171.34.0.0/15, + 171.36.0.0/14, + 171.40.0.0/13, + 171.80.0.0/12, + 171.104.0.0/13, + 171.112.0.0/12, + 171.208.0.0/12, + 172.81.192.0/18, + 175.0.0.0/12, + 175.16.0.0/13, + 175.24.0.0/14, + 175.30.0.0/15, + 175.42.0.0/15, + 175.44.0.0/16, + 175.46.0.0/15, + 175.48.0.0/12, + 175.64.0.0/11, + 175.102.0.0/16, + 175.106.128.0/17, + 175.111.144.0/20, + 175.111.160.0/20, + 175.111.184.0/22, + 175.146.0.0/15, + 175.148.0.0/14, + 175.152.0.0/14, + 175.158.96.0/22, + 175.160.0.0/12, + 175.176.156.0/22, + 175.176.176.0/22, + 175.176.188.0/22, + 175.178.0.0/16, + 175.184.128.0/18, + 175.185.0.0/16, + 175.186.0.0/15, + 175.188.0.0/14, + 177.201.160.0/19, + 177.203.96.0/19, + 177.203.224.0/19, + 178.236.234.0/24, + 179.236.0.0/19, + 180.76.16.0/20, + 180.76.32.0/19, + 180.76.64.0/18, + 180.76.128.0/17, + 180.77.0.0/16, + 180.78.0.0/15, + 180.84.0.0/15, + 180.86.0.0/16, + 180.88.0.0/14, + 180.94.56.0/21, + 180.94.96.0/20, + 180.94.120.0/21, + 180.95.128.0/17, + 180.96.0.0/11, + 180.129.128.0/17, + 180.130.0.0/16, + 180.136.0.0/13, + 180.148.16.0/21, + 180.148.152.0/21, + 180.148.216.0/21, + 180.148.224.0/19, + 180.149.128.0/19, + 180.150.160.0/19, + 180.152.0.0/13, + 180.160.0.0/12, + 180.178.112.0/21, + 180.178.192.0/18, + 180.184.0.0/14, + 180.188.0.0/17, + 180.189.148.0/22, + 180.200.252.0/22, + 180.201.0.0/16, + 180.202.0.0/15, + 180.208.0.0/15, + 180.210.212.0/22, + 180.212.0.0/15, + 180.222.224.0/19, + 180.233.0.0/18, + 180.233.64.0/19, + 180.233.144.0/22, + 180.235.64.0/19, + 180.235.112.0/22, + 182.16.144.0/21, + 182.16.192.0/19, + 182.18.0.0/17, + 182.23.184.0/21, + 182.23.200.0/21, + 182.32.0.0/12, + 182.48.96.0/19, + 182.49.0.0/16, + 182.50.0.0/22, + 182.50.6.0/24, + 182.50.8.0/21, + 182.50.112.0/20, + 182.51.0.0/16, + 182.54.0.0/17, + 182.54.244.0/22, + 182.61.0.0/17, + 182.61.128.0/19, + 182.61.192.0/18, + 182.80.0.0/13, + 182.88.0.0/14, + 182.92.0.0/16, + 182.96.0.0/11, + 182.128.0.0/12, + 182.144.0.0/13, + 182.157.0.0/16, + 182.160.64.0/19, + 182.174.0.0/15, + 182.200.0.0/13, + 182.236.128.0/17, + 182.237.24.0/21, + 182.238.0.0/16, + 182.239.0.0/19, + 182.240.0.0/13, + 182.254.0.0/16, + 182.255.60.0/22, + 183.0.0.0/10, + 183.64.0.0/13, + 183.78.160.0/21, + 183.78.180.0/22, + 183.81.172.0/24, + 183.81.180.0/22, + 183.84.0.0/15, + 183.91.37.0/24, + 183.91.39.0/24, + 183.91.40.0/21, + 183.91.48.0/23, + 183.91.50.0/24, + 183.91.52.0/22, + 183.91.56.0/24, + 183.91.61.0/24, + 183.91.63.0/24, + 183.91.128.0/22, + 183.91.136.0/21, + 183.91.144.0/20, + 183.92.0.0/14, + 183.128.0.0/11, + 183.160.0.0/13, + 183.168.0.0/15, + 183.170.0.0/16, + 183.172.0.0/14, + 183.182.0.0/21, + 183.182.8.0/23, + 183.182.11.0/24, + 183.182.12.0/22, + 183.182.16.0/23, + 183.182.23.0/24, + 183.182.24.0/21, + 183.184.0.0/13, + 183.192.0.0/10, + 185.2.48.0/24, + 185.75.173.0/24, + 185.75.174.0/24, + 185.192.56.0/22, + 188.131.128.0/17, + 192.51.172.0/23, + 192.55.46.0/23, + 192.55.68.0/22, + 192.102.204.0/22, + 192.124.154.0/24, + 192.140.128.0/21, + 192.140.136.0/22, + 192.140.156.0/22, + 192.140.160.0/19, + 192.140.192.0/20, + 192.140.208.0/21, + 192.144.128.0/17, + 192.163.11.0/24, + 192.232.34.0/23, + 192.232.44.0/22, + 192.232.97.0/24, + 193.112.0.0/16, + 193.119.0.0/19, + 194.138.202.0/23, + 194.138.245.0/24, + 198.175.100.0/22, + 198.208.17.0/24, + 198.208.19.0/24, + 198.208.30.0/24, + 198.208.61.0/24, + 198.208.63.0/24, + 198.208.67.0/24, + 198.208.112.0/23, + 199.182.239.0/24, + 199.244.144.0/24, + 202.0.100.0/23, + 202.0.122.0/23, + 202.0.176.0/23, + 202.0.178.0/24, + 202.3.128.0/23, + 202.4.128.0/19, + 202.4.252.0/22, + 202.5.208.0/21, + 202.5.216.0/22, + 202.6.6.0/23, + 202.6.66.0/23, + 202.6.72.0/23, + 202.6.87.0/24, + 202.6.88.0/23, + 202.6.92.0/23, + 202.6.103.0/24, + 202.6.108.0/24, + 202.6.110.0/23, + 202.6.114.0/24, + 202.6.176.0/20, + 202.6.202.0/23, + 202.8.0.0/24, + 202.8.2.0/23, + 202.8.4.0/23, + 202.8.12.0/24, + 202.8.24.0/24, + 202.8.77.0/24, + 202.8.128.0/19, + 202.8.192.0/20, + 202.9.32.0/24, + 202.9.34.0/23, + 202.9.48.0/23, + 202.9.51.0/24, + 202.9.52.0/23, + 202.9.54.0/24, + 202.9.57.0/24, + 202.9.58.0/23, + 202.10.64.0/20, + 202.10.112.0/20, + 202.12.1.0/24, + 202.12.2.0/24, + 202.12.17.0/24, + 202.12.18.0/24, + 202.12.72.0/24, + 202.12.84.0/23, + 202.12.96.0/24, + 202.12.98.0/23, + 202.12.106.0/24, + 202.12.111.0/24, + 202.12.116.0/24, + 202.14.64.0/23, + 202.14.69.0/24, + 202.14.73.0/24, + 202.14.74.0/23, + 202.14.76.0/24, + 202.14.78.0/23, + 202.14.88.0/24, + 202.14.97.0/24, + 202.14.104.0/23, + 202.14.108.0/23, + 202.14.111.0/24, + 202.14.114.0/23, + 202.14.118.0/23, + 202.14.124.0/23, + 202.14.127.0/24, + 202.14.129.0/24, + 202.14.135.0/24, + 202.14.136.0/24, + 202.14.149.0/24, + 202.14.151.0/24, + 202.14.157.0/24, + 202.14.158.0/23, + 202.14.169.0/24, + 202.14.170.0/23, + 202.14.172.0/22, + 202.14.176.0/24, + 202.14.184.0/23, + 202.14.208.0/23, + 202.14.213.0/24, + 202.14.219.0/24, + 202.14.220.0/24, + 202.14.222.0/23, + 202.14.225.0/24, + 202.14.226.0/23, + 202.14.231.0/24, + 202.14.235.0/24, + 202.14.236.0/22, + 202.14.246.0/24, + 202.14.251.0/24, + 202.20.66.0/24, + 202.20.79.0/24, + 202.20.87.0/24, + 202.20.88.0/23, + 202.20.90.0/24, + 202.20.94.0/23, + 202.20.114.0/24, + 202.20.117.0/24, + 202.20.120.0/24, + 202.20.125.0/24, + 202.20.126.0/23, + 202.21.48.0/20, + 202.21.131.0/24, + 202.21.132.0/24, + 202.21.141.0/24, + 202.21.142.0/24, + 202.21.147.0/24, + 202.21.148.0/24, + 202.21.150.0/23, + 202.21.152.0/23, + 202.21.154.0/24, + 202.21.156.0/24, + 202.22.248.0/21, + 202.27.12.0/24, + 202.27.14.0/24, + 202.27.136.0/23, + 202.36.226.0/24, + 202.38.0.0/22, + 202.38.8.0/21, + 202.38.48.0/20, + 202.38.64.0/18, + 202.38.128.0/21, + 202.38.136.0/23, + 202.38.140.0/22, + 202.38.146.0/23, + 202.38.149.0/24, + 202.38.150.0/23, + 202.38.152.0/22, + 202.38.156.0/24, + 202.38.158.0/23, + 202.38.164.0/22, + 202.38.168.0/23, + 202.38.170.0/24, + 202.38.176.0/23, + 202.38.184.0/21, + 202.38.192.0/18, + 202.40.4.0/23, + 202.40.7.0/24, + 202.40.15.0/24, + 202.40.135.0/24, + 202.40.136.0/24, + 202.40.140.0/24, + 202.40.143.0/24, + 202.40.144.0/23, + 202.40.150.0/24, + 202.40.155.0/24, + 202.40.156.0/24, + 202.40.158.0/23, + 202.40.162.0/24, + 202.41.8.0/23, + 202.41.11.0/24, + 202.41.12.0/23, + 202.41.128.0/24, + 202.41.130.0/23, + 202.41.152.0/21, + 202.41.192.0/24, + 202.41.196.0/22, + 202.41.200.0/22, + 202.41.240.0/20, + 202.43.76.0/22, + 202.43.144.0/20, + 202.44.16.0/20, + 202.44.48.0/22, + 202.44.67.0/24, + 202.44.74.0/24, + 202.44.97.0/24, + 202.44.129.0/24, + 202.44.132.0/23, + 202.44.146.0/23, + 202.45.0.0/23, + 202.45.2.0/24, + 202.45.15.0/24, + 202.45.16.0/20, + 202.46.16.0/23, + 202.46.18.0/24, + 202.46.20.0/23, + 202.46.35.0/24, + 202.46.36.0/23, + 202.46.38.0/24, + 202.46.40.0/24, + 202.46.43.0/24, + 202.46.44.0/24, + 202.46.46.0/23, + 202.46.128.0/24, + 202.46.224.0/20, + 202.47.82.0/23, + 202.47.96.0/20, + 202.47.126.0/24, + 202.47.128.0/24, + 202.47.130.0/23, + 202.52.33.0/24, + 202.52.34.0/24, + 202.52.47.0/24, + 202.52.143.0/24, + 202.53.140.0/24, + 202.53.143.0/24, + 202.55.0.0/23, + 202.55.4.0/24, + 202.55.18.0/23, + 202.55.20.0/22, + 202.55.24.0/22, + 202.55.28.0/23, + 202.55.31.0/24, + 202.57.192.0/20, + 202.57.212.0/22, + 202.57.216.0/22, + 202.57.240.0/20, + 202.58.0.0/24, + 202.58.112.0/22, + 202.58.122.0/23, + 202.59.0.0/23, + 202.59.212.0/22, + 202.59.236.0/24, + 202.59.240.0/24, + 202.60.48.0/21, + 202.60.96.0/21, + 202.60.112.0/20, + 202.60.132.0/22, + 202.60.136.0/21, + 202.60.144.0/20, + 202.61.68.0/22, + 202.61.76.0/22, + 202.61.88.0/22, + 202.61.123.0/24, + 202.61.127.0/24, + 202.62.112.0/22, + 202.62.248.0/22, + 202.62.252.0/24, + 202.62.255.0/24, + 202.63.80.0/20, + 202.63.160.0/19, + 202.63.248.0/22, + 202.63.253.0/24, + 202.65.0.0/21, + 202.65.8.0/23, + 202.65.96.0/20, + 202.66.169.0/24, + 202.66.170.0/23, + 202.67.0.0/22, + 202.69.4.0/23, + 202.69.16.0/20, + 202.70.0.0/19, + 202.70.96.0/20, + 202.70.192.0/20, + 202.71.32.0/20, + 202.72.40.0/21, + 202.72.80.0/20, + 202.72.112.0/20, + 202.73.128.0/22, + 202.73.240.0/20, + 202.74.8.0/21, + 202.74.36.0/24, + 202.74.42.0/24, + 202.74.52.0/24, + 202.74.80.0/20, + 202.74.254.0/23, + 202.75.208.0/20, + 202.75.252.0/22, + 202.76.252.0/22, + 202.77.80.0/21, + 202.77.92.0/22, + 202.78.8.0/21, + 202.79.224.0/21, + 202.79.248.0/22, + 202.80.192.0/20, + 202.81.0.0/22, + 202.81.176.0/20, + 202.83.252.0/22, + 202.84.0.0/20, + 202.84.16.0/23, + 202.84.22.0/24, + 202.84.24.0/21, + 202.85.208.0/20, + 202.86.249.0/24, + 202.86.252.0/22, + 202.87.80.0/20, + 202.88.32.0/22, + 202.89.8.0/21, + 202.89.96.0/22, + 202.89.108.0/22, + 202.89.119.0/24, + 202.89.232.0/21, + 202.90.16.0/20, + 202.90.37.0/24, + 202.90.96.0/19, + 202.90.193.0/24, + 202.90.196.0/24, + 202.90.205.0/24, + 202.90.224.0/20, + 202.91.0.0/22, + 202.91.96.0/20, + 202.91.176.0/20, + 202.91.224.0/19, + 202.92.8.0/21, + 202.92.48.0/20, + 202.92.252.0/22, + 202.93.252.0/22, + 202.94.74.0/24, + 202.94.81.0/24, + 202.94.92.0/22, + 202.95.0.0/24, + 202.95.2.0/23, + 202.95.24.0/21, + 202.95.240.0/21, + 202.96.0.0/12, + 202.112.0.0/13, + 202.120.0.0/15, + 202.122.0.0/21, + 202.122.32.0/21, + 202.122.64.0/19, + 202.122.112.0/20, + 202.122.132.0/24, + 202.123.96.0/20, + 202.123.116.0/22, + 202.123.120.0/22, + 202.124.16.0/21, + 202.124.24.0/22, + 202.125.107.0/24, + 202.125.109.0/24, + 202.125.112.0/20, + 202.125.176.0/20, + 202.127.0.0/21, + 202.127.12.0/22, + 202.127.16.0/20, + 202.127.40.0/21, + 202.127.48.0/20, + 202.127.112.0/20, + 202.127.128.0/19, + 202.127.192.0/20, + 202.127.212.0/22, + 202.127.216.0/21, + 202.127.224.0/19, + 202.129.208.0/24, + 202.130.0.0/19, + 202.130.39.0/24, + 202.130.224.0/19, + 202.131.16.0/21, + 202.131.48.0/20, + 202.131.208.0/20, + 202.133.32.0/20, + 202.134.58.0/24, + 202.134.128.0/20, + 202.134.208.0/20, + 202.136.48.0/20, + 202.136.72.0/23, + 202.136.80.0/23, + 202.136.208.0/20, + 202.136.224.0/20, + 202.136.248.0/22, + 202.136.255.0/24, + 202.137.231.0/24, + 202.140.140.0/22, + 202.140.144.0/20, + 202.141.160.0/19, + 202.142.16.0/20, + 202.143.4.0/22, + 202.143.16.0/20, + 202.143.32.0/20, + 202.143.56.0/21, + 202.143.100.0/22, + 202.143.104.0/22, + 202.144.196.0/23, + 202.144.198.0/24, + 202.146.160.0/20, + 202.146.184.0/23, + 202.146.186.0/24, + 202.146.188.0/22, + 202.146.196.0/22, + 202.146.200.0/21, + 202.147.144.0/20, + 202.148.32.0/20, + 202.148.64.0/18, + 202.149.32.0/19, + 202.149.160.0/19, + 202.149.224.0/20, + 202.149.240.0/21, + 202.149.248.0/22, + 202.149.252.0/23, + 202.149.255.0/24, + 202.150.16.0/20, + 202.150.32.0/20, + 202.150.56.0/22, + 202.150.192.0/20, + 202.150.224.0/19, + 202.151.0.0/22, + 202.151.33.0/24, + 202.151.128.0/19, + 202.152.176.0/20, + 202.153.0.0/22, + 202.153.7.0/24, + 202.153.48.0/20, + 202.157.192.0/19, + 202.158.160.0/19, + 202.158.242.0/24, + 202.160.140.0/23, + 202.160.143.0/24, + 202.160.156.0/22, + 202.162.67.0/24, + 202.162.75.0/24, + 202.164.0.0/20, + 202.164.96.0/19, + 202.165.176.0/20, + 202.165.208.0/21, + 202.165.239.0/24, + 202.165.240.0/23, + 202.165.243.0/24, + 202.165.245.0/24, + 202.165.251.0/24, + 202.165.252.0/22, + 202.166.224.0/19, + 202.168.80.0/22, + 202.168.128.0/20, + 202.168.160.0/19, + 202.170.128.0/19, + 202.170.216.0/21, + 202.170.224.0/19, + 202.171.216.0/21, + 202.171.232.0/24, + 202.171.235.0/24, + 202.172.0.0/22, + 202.172.7.0/24, + 202.173.0.0/22, + 202.173.6.0/24, + 202.173.8.0/21, + 202.173.112.0/22, + 202.173.224.0/19, + 202.174.64.0/20, + 202.174.124.0/22, + 202.176.224.0/19, + 202.179.160.0/20, + 202.179.240.0/21, + 202.180.128.0/19, + 202.180.208.0/21, + 202.181.8.0/22, + 202.181.28.0/22, + 202.181.112.0/20, + 202.182.32.0/20, + 202.182.192.0/19, + 202.189.0.0/18, + 202.189.80.0/20, + 202.189.184.0/21, + 202.191.0.0/24, + 202.191.68.0/22, + 202.191.72.0/21, + 202.191.80.0/20, + 202.192.0.0/12, + 203.0.4.0/22, + 203.0.10.0/23, + 203.0.18.0/24, + 203.0.24.0/24, + 203.0.42.0/23, + 203.0.45.0/24, + 203.0.46.0/23, + 203.0.81.0/24, + 203.0.82.0/23, + 203.0.90.0/23, + 203.0.96.0/23, + 203.0.104.0/21, + 203.0.114.0/23, + 203.0.122.0/24, + 203.0.128.0/24, + 203.0.130.0/23, + 203.0.132.0/22, + 203.0.137.0/24, + 203.0.142.0/24, + 203.0.144.0/24, + 203.0.146.0/24, + 203.0.148.0/24, + 203.0.150.0/23, + 203.0.152.0/24, + 203.0.177.0/24, + 203.0.224.0/24, + 203.1.4.0/22, + 203.1.18.0/24, + 203.1.26.0/23, + 203.1.65.0/24, + 203.1.66.0/23, + 203.1.70.0/23, + 203.1.76.0/23, + 203.1.90.0/24, + 203.1.97.0/24, + 203.1.98.0/23, + 203.1.100.0/22, + 203.1.108.0/24, + 203.1.253.0/24, + 203.1.254.0/24, + 203.2.64.0/21, + 203.2.73.0/24, + 203.2.112.0/21, + 203.2.126.0/23, + 203.2.140.0/24, + 203.2.150.0/24, + 203.2.152.0/22, + 203.2.156.0/23, + 203.2.160.0/21, + 203.2.180.0/23, + 203.2.196.0/23, + 203.2.209.0/24, + 203.2.214.0/23, + 203.2.226.0/23, + 203.2.229.0/24, + 203.2.236.0/23, + 203.3.68.0/24, + 203.3.72.0/23, + 203.3.75.0/24, + 203.3.80.0/21, + 203.3.96.0/22, + 203.3.105.0/24, + 203.3.112.0/21, + 203.3.120.0/24, + 203.3.123.0/24, + 203.3.135.0/24, + 203.3.139.0/24, + 203.3.143.0/24, + 203.4.132.0/23, + 203.4.134.0/24, + 203.4.151.0/24, + 203.4.152.0/22, + 203.4.174.0/23, + 203.4.180.0/24, + 203.4.186.0/24, + 203.4.205.0/24, + 203.4.208.0/22, + 203.4.227.0/24, + 203.4.230.0/23, + 203.5.4.0/23, + 203.5.7.0/24, + 203.5.8.0/23, + 203.5.11.0/24, + 203.5.21.0/24, + 203.5.22.0/24, + 203.5.44.0/24, + 203.5.46.0/23, + 203.5.52.0/22, + 203.5.56.0/23, + 203.5.60.0/23, + 203.5.114.0/23, + 203.5.118.0/24, + 203.5.120.0/24, + 203.5.172.0/24, + 203.5.180.0/23, + 203.5.182.0/24, + 203.5.185.0/24, + 203.5.186.0/24, + 203.5.188.0/23, + 203.5.190.0/24, + 203.5.195.0/24, + 203.5.214.0/23, + 203.5.218.0/23, + 203.6.131.0/24, + 203.6.136.0/24, + 203.6.138.0/23, + 203.6.142.0/24, + 203.6.150.0/23, + 203.6.157.0/24, + 203.6.159.0/24, + 203.6.224.0/20, + 203.6.248.0/23, + 203.7.129.0/24, + 203.7.138.0/23, + 203.7.147.0/24, + 203.7.150.0/23, + 203.7.158.0/24, + 203.7.192.0/23, + 203.7.200.0/24, + 203.8.0.0/24, + 203.8.8.0/24, + 203.8.23.0/24, + 203.8.25.0/24, + 203.8.70.0/24, + 203.8.82.0/24, + 203.8.86.0/23, + 203.8.91.0/24, + 203.8.110.0/23, + 203.8.115.0/24, + 203.8.166.0/23, + 203.8.169.0/24, + 203.8.173.0/24, + 203.8.184.0/24, + 203.8.186.0/23, + 203.8.190.0/23, + 203.8.192.0/24, + 203.8.197.0/24, + 203.8.198.0/23, + 203.8.203.0/24, + 203.8.209.0/24, + 203.8.210.0/23, + 203.8.212.0/22, + 203.8.217.0/24, + 203.8.220.0/24, + 203.9.32.0/24, + 203.9.36.0/23, + 203.9.57.0/24, + 203.9.63.0/24, + 203.9.65.0/24, + 203.9.70.0/23, + 203.9.72.0/24, + 203.9.75.0/24, + 203.9.76.0/23, + 203.9.96.0/22, + 203.9.100.0/23, + 203.9.108.0/24, + 203.9.158.0/24, + 203.10.34.0/24, + 203.10.56.0/24, + 203.10.74.0/23, + 203.10.84.0/22, + 203.10.88.0/24, + 203.10.95.0/24, + 203.10.125.0/24, + 203.11.70.0/24, + 203.11.76.0/22, + 203.11.82.0/24, + 203.11.84.0/22, + 203.11.100.0/22, + 203.11.109.0/24, + 203.11.117.0/24, + 203.11.122.0/24, + 203.11.126.0/24, + 203.11.136.0/22, + 203.11.141.0/24, + 203.11.142.0/23, + 203.11.180.0/22, + 203.11.208.0/22, + 203.12.16.0/24, + 203.12.19.0/24, + 203.12.24.0/24, + 203.12.57.0/24, + 203.12.65.0/24, + 203.12.66.0/24, + 203.12.70.0/23, + 203.12.87.0/24, + 203.12.91.0/24, + 203.12.93.0/24, + 203.12.95.0/24, + 203.12.100.0/23, + 203.12.103.0/24, + 203.12.114.0/24, + 203.12.118.0/24, + 203.12.130.0/24, + 203.12.137.0/24, + 203.12.196.0/22, + 203.12.211.0/24, + 203.12.219.0/24, + 203.12.226.0/24, + 203.12.240.0/22, + 203.13.18.0/24, + 203.13.24.0/24, + 203.13.44.0/23, + 203.13.81.0/24, + 203.13.88.0/23, + 203.13.92.0/22, + 203.13.173.0/24, + 203.13.224.0/23, + 203.13.227.0/24, + 203.13.233.0/24, + 203.14.24.0/22, + 203.14.33.0/24, + 203.14.56.0/24, + 203.14.61.0/24, + 203.14.62.0/24, + 203.14.104.0/24, + 203.14.114.0/23, + 203.14.118.0/24, + 203.14.162.0/24, + 203.14.192.0/24, + 203.14.194.0/23, + 203.14.214.0/24, + 203.14.231.0/24, + 203.14.246.0/24, + 203.15.0.0/20, + 203.15.20.0/23, + 203.15.22.0/24, + 203.15.87.0/24, + 203.15.88.0/23, + 203.15.105.0/24, + 203.15.112.0/24, + 203.15.114.0/23, + 203.15.119.0/24, + 203.15.130.0/23, + 203.15.149.0/24, + 203.15.151.0/24, + 203.15.156.0/22, + 203.15.174.0/24, + 203.15.227.0/24, + 203.15.232.0/21, + 203.15.240.0/23, + 203.15.246.0/24, + 203.16.10.0/24, + 203.16.12.0/23, + 203.16.16.0/21, + 203.16.27.0/24, + 203.16.38.0/24, + 203.16.49.0/24, + 203.16.50.0/23, + 203.16.58.0/24, + 203.16.63.0/24, + 203.16.133.0/24, + 203.16.161.0/24, + 203.16.162.0/24, + 203.16.186.0/23, + 203.16.228.0/24, + 203.16.238.0/24, + 203.16.240.0/24, + 203.16.245.0/24, + 203.17.2.0/24, + 203.17.18.0/24, + 203.17.28.0/24, + 203.17.39.0/24, + 203.17.56.0/24, + 203.17.74.0/23, + 203.17.88.0/23, + 203.17.136.0/24, + 203.17.164.0/24, + 203.17.187.0/24, + 203.17.190.0/23, + 203.17.231.0/24, + 203.17.233.0/24, + 203.17.248.0/23, + 203.17.255.0/24, + 203.18.2.0/23, + 203.18.4.0/24, + 203.18.7.0/24, + 203.18.31.0/24, + 203.18.37.0/24, + 203.18.48.0/23, + 203.18.52.0/24, + 203.18.72.0/22, + 203.18.80.0/23, + 203.18.87.0/24, + 203.18.100.0/23, + 203.18.105.0/24, + 203.18.107.0/24, + 203.18.110.0/24, + 203.18.129.0/24, + 203.18.131.0/24, + 203.18.132.0/23, + 203.18.144.0/24, + 203.18.153.0/24, + 203.18.199.0/24, + 203.18.208.0/24, + 203.18.211.0/24, + 203.18.215.0/24, + 203.19.1.0/24, + 203.19.18.0/24, + 203.19.24.0/24, + 203.19.30.0/24, + 203.19.36.0/24, + 203.19.39.0/24, + 203.19.41.0/24, + 203.19.44.0/23, + 203.19.46.0/24, + 203.19.58.0/24, + 203.19.60.0/23, + 203.19.64.0/24, + 203.19.68.0/24, + 203.19.72.0/24, + 203.19.101.0/24, + 203.19.111.0/24, + 203.19.131.0/24, + 203.19.133.0/24, + 203.19.144.0/24, + 203.19.147.0/24, + 203.19.149.0/24, + 203.19.156.0/24, + 203.19.176.0/24, + 203.19.178.0/23, + 203.19.208.0/24, + 203.19.228.0/22, + 203.19.233.0/24, + 203.19.242.0/24, + 203.19.248.0/23, + 203.19.255.0/24, + 203.20.17.0/24, + 203.20.40.0/23, + 203.20.44.0/24, + 203.20.48.0/24, + 203.20.61.0/24, + 203.20.65.0/24, + 203.20.84.0/23, + 203.20.89.0/24, + 203.20.106.0/23, + 203.20.115.0/24, + 203.20.117.0/24, + 203.20.118.0/23, + 203.20.122.0/24, + 203.20.126.0/23, + 203.20.135.0/24, + 203.20.138.0/23, + 203.20.150.0/24, + 203.20.230.0/24, + 203.20.232.0/24, + 203.20.236.0/24, + 203.21.0.0/23, + 203.21.2.0/24, + 203.21.8.0/24, + 203.21.10.0/24, + 203.21.18.0/24, + 203.21.33.0/24, + 203.21.34.0/24, + 203.21.41.0/24, + 203.21.44.0/24, + 203.21.68.0/24, + 203.21.82.0/24, + 203.21.96.0/22, + 203.21.124.0/24, + 203.21.136.0/23, + 203.21.145.0/24, + 203.21.206.0/24, + 203.22.24.0/24, + 203.22.28.0/23, + 203.22.31.0/24, + 203.22.68.0/24, + 203.22.76.0/24, + 203.22.78.0/24, + 203.22.84.0/24, + 203.22.87.0/24, + 203.22.92.0/22, + 203.22.99.0/24, + 203.22.106.0/24, + 203.22.122.0/23, + 203.22.131.0/24, + 203.22.163.0/24, + 203.22.166.0/24, + 203.22.170.0/24, + 203.22.176.0/21, + 203.22.194.0/24, + 203.22.242.0/23, + 203.22.245.0/24, + 203.22.246.0/24, + 203.22.252.0/23, + 203.23.0.0/24, + 203.23.47.0/24, + 203.23.61.0/24, + 203.23.62.0/23, + 203.23.73.0/24, + 203.23.85.0/24, + 203.23.92.0/22, + 203.23.98.0/24, + 203.23.107.0/24, + 203.23.112.0/24, + 203.23.130.0/24, + 203.23.140.0/23, + 203.23.172.0/24, + 203.23.182.0/24, + 203.23.186.0/23, + 203.23.192.0/24, + 203.23.197.0/24, + 203.23.198.0/24, + 203.23.204.0/22, + 203.23.224.0/24, + 203.23.226.0/23, + 203.23.228.0/22, + 203.23.249.0/24, + 203.23.251.0/24, + 203.24.13.0/24, + 203.24.18.0/24, + 203.24.27.0/24, + 203.24.43.0/24, + 203.24.56.0/24, + 203.24.58.0/24, + 203.24.67.0/24, + 203.24.74.0/24, + 203.24.79.0/24, + 203.24.80.0/23, + 203.24.84.0/23, + 203.24.86.0/24, + 203.24.90.0/24, + 203.24.111.0/24, + 203.24.112.0/24, + 203.24.116.0/24, + 203.24.122.0/23, + 203.24.145.0/24, + 203.24.152.0/23, + 203.24.157.0/24, + 203.24.161.0/24, + 203.24.167.0/24, + 203.24.186.0/23, + 203.24.199.0/24, + 203.24.202.0/24, + 203.24.212.0/23, + 203.24.217.0/24, + 203.24.219.0/24, + 203.24.244.0/24, + 203.25.19.0/24, + 203.25.20.0/23, + 203.25.46.0/24, + 203.25.48.0/21, + 203.25.64.0/23, + 203.25.91.0/24, + 203.25.99.0/24, + 203.25.100.0/24, + 203.25.106.0/24, + 203.25.131.0/24, + 203.25.135.0/24, + 203.25.138.0/24, + 203.25.147.0/24, + 203.25.153.0/24, + 203.25.154.0/23, + 203.25.164.0/24, + 203.25.166.0/24, + 203.25.174.0/23, + 203.25.180.0/24, + 203.25.182.0/24, + 203.25.191.0/24, + 203.25.199.0/24, + 203.25.200.0/24, + 203.25.202.0/23, + 203.25.208.0/20, + 203.25.229.0/24, + 203.25.235.0/24, + 203.25.236.0/24, + 203.25.242.0/24, + 203.26.12.0/24, + 203.26.34.0/24, + 203.26.49.0/24, + 203.26.50.0/24, + 203.26.55.0/24, + 203.26.56.0/23, + 203.26.60.0/24, + 203.26.65.0/24, + 203.26.68.0/24, + 203.26.76.0/24, + 203.26.80.0/24, + 203.26.84.0/24, + 203.26.97.0/24, + 203.26.102.0/23, + 203.26.115.0/24, + 203.26.116.0/24, + 203.26.129.0/24, + 203.26.143.0/24, + 203.26.144.0/24, + 203.26.148.0/23, + 203.26.154.0/24, + 203.26.158.0/23, + 203.26.170.0/24, + 203.26.173.0/24, + 203.26.176.0/24, + 203.26.185.0/24, + 203.26.202.0/23, + 203.26.210.0/24, + 203.26.214.0/24, + 203.26.222.0/24, + 203.26.224.0/24, + 203.26.228.0/24, + 203.26.232.0/24, + 203.27.0.0/24, + 203.27.10.0/24, + 203.27.15.0/24, + 203.27.16.0/24, + 203.27.20.0/24, + 203.27.22.0/23, + 203.27.40.0/24, + 203.27.45.0/24, + 203.27.53.0/24, + 203.27.65.0/24, + 203.27.66.0/24, + 203.27.81.0/24, + 203.27.88.0/24, + 203.27.102.0/24, + 203.27.109.0/24, + 203.27.117.0/24, + 203.27.121.0/24, + 203.27.122.0/23, + 203.27.125.0/24, + 203.27.200.0/24, + 203.27.202.0/24, + 203.27.233.0/24, + 203.27.241.0/24, + 203.27.250.0/24, + 203.28.10.0/24, + 203.28.12.0/24, + 203.28.33.0/24, + 203.28.34.0/23, + 203.28.43.0/24, + 203.28.44.0/24, + 203.28.54.0/24, + 203.28.56.0/24, + 203.28.73.0/24, + 203.28.74.0/24, + 203.28.76.0/24, + 203.28.86.0/24, + 203.28.88.0/24, + 203.28.112.0/24, + 203.28.131.0/24, + 203.28.136.0/24, + 203.28.140.0/24, + 203.28.145.0/24, + 203.28.165.0/24, + 203.28.169.0/24, + 203.28.170.0/24, + 203.28.178.0/23, + 203.28.185.0/24, + 203.28.187.0/24, + 203.28.196.0/24, + 203.28.226.0/23, + 203.28.239.0/24, + 203.29.2.0/24, + 203.29.8.0/23, + 203.29.13.0/24, + 203.29.14.0/24, + 203.29.28.0/24, + 203.29.46.0/24, + 203.29.57.0/24, + 203.29.61.0/24, + 203.29.63.0/24, + 203.29.69.0/24, + 203.29.73.0/24, + 203.29.81.0/24, + 203.29.90.0/24, + 203.29.95.0/24, + 203.29.100.0/24, + 203.29.103.0/24, + 203.29.112.0/24, + 203.29.120.0/22, + 203.29.182.0/23, + 203.29.187.0/24, + 203.29.189.0/24, + 203.29.190.0/24, + 203.29.205.0/24, + 203.29.210.0/24, + 203.29.217.0/24, + 203.29.227.0/24, + 203.29.231.0/24, + 203.29.233.0/24, + 203.29.234.0/24, + 203.29.248.0/24, + 203.29.254.0/23, + 203.30.16.0/23, + 203.30.25.0/24, + 203.30.27.0/24, + 203.30.29.0/24, + 203.30.66.0/24, + 203.30.81.0/24, + 203.30.87.0/24, + 203.30.111.0/24, + 203.30.121.0/24, + 203.30.123.0/24, + 203.30.152.0/24, + 203.30.156.0/24, + 203.30.162.0/24, + 203.30.173.0/24, + 203.30.175.0/24, + 203.30.187.0/24, + 203.30.194.0/24, + 203.30.217.0/24, + 203.30.220.0/24, + 203.30.222.0/24, + 203.30.232.0/23, + 203.30.235.0/24, + 203.30.240.0/23, + 203.30.246.0/24, + 203.30.250.0/23, + 203.31.45.0/24, + 203.31.46.0/24, + 203.31.49.0/24, + 203.31.51.0/24, + 203.31.54.0/23, + 203.31.69.0/24, + 203.31.72.0/24, + 203.31.80.0/24, + 203.31.85.0/24, + 203.31.97.0/24, + 203.31.105.0/24, + 203.31.106.0/24, + 203.31.108.0/23, + 203.31.124.0/24, + 203.31.162.0/24, + 203.31.174.0/24, + 203.31.177.0/24, + 203.31.181.0/24, + 203.31.187.0/24, + 203.31.189.0/24, + 203.31.204.0/24, + 203.31.220.0/24, + 203.31.222.0/23, + 203.31.225.0/24, + 203.31.229.0/24, + 203.31.248.0/23, + 203.31.253.0/24, + 203.32.20.0/24, + 203.32.48.0/23, + 203.32.56.0/24, + 203.32.60.0/24, + 203.32.62.0/24, + 203.32.68.0/23, + 203.32.76.0/24, + 203.32.81.0/24, + 203.32.84.0/23, + 203.32.95.0/24, + 203.32.102.0/24, + 203.32.105.0/24, + 203.32.130.0/24, + 203.32.133.0/24, + 203.32.140.0/24, + 203.32.152.0/24, + 203.32.186.0/23, + 203.32.192.0/24, + 203.32.196.0/24, + 203.32.203.0/24, + 203.32.204.0/23, + 203.32.212.0/24, + 203.33.4.0/24, + 203.33.7.0/24, + 203.33.11.0/24, + 203.33.12.0/22, + 203.33.21.0/24, + 203.33.26.0/24, + 203.33.32.0/24, + 203.33.63.0/24, + 203.33.64.0/24, + 203.33.67.0/24, + 203.33.68.0/24, + 203.33.73.0/24, + 203.33.79.0/24, + 203.33.100.0/24, + 203.33.122.0/24, + 203.33.129.0/24, + 203.33.131.0/24, + 203.33.145.0/24, + 203.33.156.0/24, + 203.33.158.0/23, + 203.33.174.0/24, + 203.33.185.0/24, + 203.33.200.0/24, + 203.33.202.0/23, + 203.33.204.0/24, + 203.33.206.0/23, + 203.33.214.0/23, + 203.33.224.0/23, + 203.33.226.0/24, + 203.33.233.0/24, + 203.33.243.0/24, + 203.33.250.0/24, + 203.34.4.0/24, + 203.34.21.0/24, + 203.34.27.0/24, + 203.34.39.0/24, + 203.34.48.0/23, + 203.34.54.0/24, + 203.34.56.0/23, + 203.34.67.0/24, + 203.34.69.0/24, + 203.34.76.0/24, + 203.34.92.0/24, + 203.34.106.0/24, + 203.34.113.0/24, + 203.34.147.0/24, + 203.34.150.0/24, + 203.34.152.0/23, + 203.34.161.0/24, + 203.34.162.0/24, + 203.34.187.0/24, + 203.34.192.0/23, + 203.34.195.0/24, + 203.34.198.0/24, + 203.34.204.0/22, + 203.34.232.0/24, + 203.34.240.0/24, + 203.34.242.0/24, + 203.34.245.0/24, + 203.34.251.0/24, + 203.55.2.0/23, + 203.55.4.0/24, + 203.55.10.0/24, + 203.55.13.0/24, + 203.55.22.0/24, + 203.55.30.0/24, + 203.55.93.0/24, + 203.55.101.0/24, + 203.55.109.0/24, + 203.55.110.0/24, + 203.55.116.0/23, + 203.55.119.0/24, + 203.55.128.0/23, + 203.55.146.0/23, + 203.55.192.0/24, + 203.55.196.0/24, + 203.55.218.0/23, + 203.55.221.0/24, + 203.55.224.0/24, + 203.56.1.0/24, + 203.56.4.0/24, + 203.56.12.0/24, + 203.56.24.0/24, + 203.56.38.0/24, + 203.56.40.0/24, + 203.56.46.0/24, + 203.56.48.0/23, + 203.56.51.0/24, + 203.56.52.0/23, + 203.56.54.0/24, + 203.56.68.0/23, + 203.56.82.0/23, + 203.56.84.0/23, + 203.56.95.0/24, + 203.56.110.0/24, + 203.56.121.0/24, + 203.56.161.0/24, + 203.56.169.0/24, + 203.56.172.0/23, + 203.56.175.0/24, + 203.56.183.0/24, + 203.56.185.0/24, + 203.56.187.0/24, + 203.56.192.0/24, + 203.56.198.0/24, + 203.56.201.0/24, + 203.56.208.0/23, + 203.56.210.0/24, + 203.56.214.0/24, + 203.56.216.0/24, + 203.56.227.0/24, + 203.56.228.0/24, + 203.56.231.0/24, + 203.56.232.0/24, + 203.56.240.0/24, + 203.56.252.0/24, + 203.56.254.0/24, + 203.57.5.0/24, + 203.57.6.0/24, + 203.57.12.0/23, + 203.57.28.0/24, + 203.57.39.0/24, + 203.57.46.0/24, + 203.57.58.0/24, + 203.57.61.0/24, + 203.57.66.0/24, + 203.57.69.0/24, + 203.57.70.0/23, + 203.57.73.0/24, + 203.57.90.0/24, + 203.57.101.0/24, + 203.57.109.0/24, + 203.57.123.0/24, + 203.57.157.0/24, + 203.57.200.0/24, + 203.57.202.0/24, + 203.57.206.0/24, + 203.57.222.0/24, + 203.57.224.0/20, + 203.57.246.0/23, + 203.57.249.0/24, + 203.57.253.0/24, + 203.57.254.0/23, + 203.62.2.0/24, + 203.62.131.0/24, + 203.62.139.0/24, + 203.62.161.0/24, + 203.62.197.0/24, + 203.62.228.0/22, + 203.62.234.0/24, + 203.62.246.0/24, + 203.65.240.0/22, + 203.76.160.0/22, + 203.76.168.0/22, + 203.76.208.0/21, + 203.76.216.0/22, + 203.76.240.0/22, + 203.77.180.0/22, + 203.78.48.0/20, + 203.78.156.0/22, + 203.79.0.0/20, + 203.80.4.0/23, + 203.80.32.0/20, + 203.80.57.0/24, + 203.80.129.0/24, + 203.80.132.0/22, + 203.80.140.0/24, + 203.80.142.0/23, + 203.80.144.0/20, + 203.81.1.0/24, + 203.81.2.0/24, + 203.81.5.0/24, + 203.81.16.0/20, + 203.81.244.0/22, + 203.82.0.0/23, + 203.82.16.0/24, + 203.82.20.0/23, + 203.82.22.0/24, + 203.82.112.0/20, + 203.82.224.0/20, + 203.83.0.0/22, + 203.83.12.0/22, + 203.83.56.0/21, + 203.83.224.0/20, + 203.86.0.0/18, + 203.86.64.0/19, + 203.86.96.0/22, + 203.86.100.0/23, + 203.86.102.0/24, + 203.86.104.0/23, + 203.86.107.0/24, + 203.86.108.0/24, + 203.86.110.0/24, + 203.86.112.0/24, + 203.86.114.0/24, + 203.86.116.0/23, + 203.86.120.0/24, + 203.86.250.0/24, + 203.86.254.0/23, + 203.88.32.0/19, + 203.88.192.0/19, + 203.89.8.0/23, + 203.89.13.0/24, + 203.89.14.0/23, + 203.89.133.0/24, + 203.89.136.0/22, + 203.89.144.0/24, + 203.90.8.0/21, + 203.90.128.0/18, + 203.90.192.0/19, + 203.91.32.0/19, + 203.91.96.0/20, + 203.91.120.0/21, + 203.92.0.0/22, + 203.92.6.0/24, + 203.92.160.0/19, + 203.93.0.0/16, + 203.94.0.0/19, + 203.95.0.0/21, + 203.95.96.0/19, + 203.95.128.0/18, + 203.95.200.0/21, + 203.95.208.0/22, + 203.95.224.0/19, + 203.99.8.0/23, + 203.99.12.0/24, + 203.99.14.0/23, + 203.99.16.0/23, + 203.99.19.0/24, + 203.99.20.0/22, + 203.99.24.0/21, + 203.99.80.0/20, + 203.100.32.0/20, + 203.100.48.0/22, + 203.100.53.0/24, + 203.100.55.0/24, + 203.100.58.0/24, + 203.100.60.0/24, + 203.100.63.0/24, + 203.100.80.0/20, + 203.100.96.0/19, + 203.100.192.0/20, + 203.104.32.0/20, + 203.105.96.0/19, + 203.105.128.0/19, + 203.107.0.0/23, + 203.107.2.0/24, + 203.107.4.0/22, + 203.107.8.0/21, + 203.107.16.0/20, + 203.107.32.0/19, + 203.107.69.0/24, + 203.107.70.0/23, + 203.107.72.0/21, + 203.107.80.0/20, + 203.107.96.0/19, + 203.110.160.0/19, + 203.110.208.0/20, + 203.110.232.0/23, + 203.110.234.0/24, + 203.114.80.0/20, + 203.114.244.0/22, + 203.118.192.0/19, + 203.118.241.0/24, + 203.118.248.0/22, + 203.119.24.0/21, + 203.119.32.0/22, + 203.119.80.0/24, + 203.119.83.0/24, + 203.119.85.0/24, + 203.119.113.0/24, + 203.119.114.0/23, + 203.119.116.0/22, + 203.119.121.0/24, + 203.119.122.0/23, + 203.119.126.0/23, + 203.119.128.0/17, + 203.123.58.0/24, + 203.128.32.0/19, + 203.128.96.0/19, + 203.128.225.0/24, + 203.128.226.0/23, + 203.128.228.0/24, + 203.128.231.0/24, + 203.129.8.0/24, + 203.129.10.0/23, + 203.129.12.0/22, + 203.130.32.0/19, + 203.132.32.0/19, + 203.134.240.0/21, + 203.135.96.0/19, + 203.135.160.0/20, + 203.142.219.0/24, + 203.142.224.0/19, + 203.144.96.0/22, + 203.144.100.0/24, + 203.144.102.0/24, + 203.144.106.0/23, + 203.144.108.0/22, + 203.144.112.0/20, + 203.145.0.0/19, + 203.148.0.0/18, + 203.148.64.0/20, + 203.148.80.0/22, + 203.148.86.0/23, + 203.149.92.0/22, + 203.152.64.0/19, + 203.152.128.0/19, + 203.153.0.0/22, + 203.156.192.0/18, + 203.158.16.0/21, + 203.160.104.0/21, + 203.160.129.0/24, + 203.160.192.0/19, + 203.161.0.0/22, + 203.161.183.0/24, + 203.161.192.0/19, + 203.166.160.0/19, + 203.167.28.0/22, + 203.168.0.0/19, + 203.170.58.0/23, + 203.171.0.0/22, + 203.171.208.0/24, + 203.171.224.0/20, + 203.174.4.0/24, + 203.174.6.0/24, + 203.174.96.0/19, + 203.175.128.0/19, + 203.175.192.0/18, + 203.176.0.0/18, + 203.176.64.0/19, + 203.176.168.0/21, + 203.184.80.0/20, + 203.185.189.0/24, + 203.187.160.0/19, + 203.189.0.0/23, + 203.189.6.0/23, + 203.189.112.0/22, + 203.189.192.0/19, + 203.189.240.0/22, + 203.190.96.0/20, + 203.190.249.0/24, + 203.191.0.0/23, + 203.191.2.0/24, + 203.191.5.0/24, + 203.191.7.0/24, + 203.191.16.0/20, + 203.191.64.0/18, + 203.191.133.0/24, + 203.191.144.0/20, + 203.192.0.0/19, + 203.193.224.0/19, + 203.194.120.0/21, + 203.195.64.0/19, + 203.195.112.0/23, + 203.195.114.0/24, + 203.195.116.0/22, + 203.195.128.0/17, + 203.196.0.0/21, + 203.196.28.0/22, + 203.201.181.0/24, + 203.201.182.0/24, + 203.202.236.0/22, + 203.205.64.0/19, + 203.205.130.0/23, + 203.205.132.0/23, + 203.205.148.0/22, + 203.205.152.0/23, + 203.205.154.0/24, + 203.205.156.0/24, + 203.205.158.0/23, + 203.205.160.0/20, + 203.205.176.0/21, + 203.205.184.0/22, + 203.205.189.0/24, + 203.205.190.0/24, + 203.205.200.0/21, + 203.205.208.0/21, + 203.205.216.0/23, + 203.205.225.0/24, + 203.205.226.0/23, + 203.205.228.0/22, + 203.205.240.0/21, + 203.207.64.0/18, + 203.207.128.0/17, + 203.208.32.0/19, + 203.209.224.0/19, + 203.212.0.0/20, + 203.212.80.0/20, + 203.215.232.0/21, + 203.217.164.0/22, + 203.223.16.0/24, + 203.223.21.0/24, + 203.223.22.0/23, + 206.109.70.0/23, + 206.109.75.0/24, + 210.2.0.0/19, + 210.5.0.0/19, + 210.5.56.0/24, + 210.5.58.0/23, + 210.5.60.0/22, + 210.5.128.0/19, + 210.7.56.0/21, + 210.12.0.0/15, + 210.14.64.0/19, + 210.14.112.0/20, + 210.14.128.0/17, + 210.15.0.0/17, + 210.15.128.0/18, + 210.16.128.0/18, + 210.21.0.0/16, + 210.22.0.0/16, + 210.23.32.0/19, + 210.25.0.0/16, + 210.26.0.0/15, + 210.28.0.0/14, + 210.32.0.0/12, + 210.51.0.0/16, + 210.52.0.0/15, + 210.56.192.0/19, + 210.72.0.0/14, + 210.76.0.0/15, + 210.78.0.0/16, + 210.79.64.0/18, + 210.79.224.0/19, + 210.82.0.0/15, + 210.87.72.0/23, + 210.87.114.0/23, + 210.87.128.0/18, + 210.185.192.0/18, + 210.192.96.0/19, + 211.64.0.0/13, + 211.80.0.0/12, + 211.96.0.0/15, + 211.98.0.0/16, + 211.99.0.0/19, + 211.99.64.0/18, + 211.99.128.0/17, + 211.100.0.0/15, + 211.102.0.0/17, + 211.102.128.0/21, + 211.102.144.0/21, + 211.102.160.0/20, + 211.102.192.0/18, + 211.103.0.0/16, + 211.136.0.0/13, + 211.144.0.0/14, + 211.148.0.0/16, + 211.149.0.0/19, + 211.149.48.0/21, + 211.149.64.0/21, + 211.149.80.0/21, + 211.149.96.0/19, + 211.149.128.0/17, + 211.150.0.0/15, + 211.152.0.0/17, + 211.152.128.0/20, + 211.152.144.0/22, + 211.152.150.0/23, + 211.152.152.0/21, + 211.152.160.0/19, + 211.152.192.0/18, + 211.153.0.0/16, + 211.154.0.0/16, + 211.155.0.0/18, + 211.155.66.0/23, + 211.155.68.0/22, + 211.155.72.0/21, + 211.155.80.0/20, + 211.155.96.0/19, + 211.155.128.0/18, + 211.155.208.0/20, + 211.155.224.0/19, + 211.156.0.0/14, + 211.160.0.0/14, + 211.164.0.0/15, + 211.166.0.0/16, + 211.167.0.0/19, + 211.167.64.0/18, + 211.167.128.0/17, + 212.64.0.0/17, + 212.129.128.0/17, + 218.0.0.0/12, + 218.16.0.0/13, + 218.24.0.0/14, + 218.28.0.0/15, + 218.30.0.0/19, + 218.30.32.0/22, + 218.30.36.0/23, + 218.30.38.0/24, + 218.30.40.0/22, + 218.30.45.0/24, + 218.30.46.0/23, + 218.30.48.0/21, + 218.30.56.0/22, + 218.30.60.0/23, + 218.30.64.0/18, + 218.30.128.0/17, + 218.31.0.0/16, + 218.56.0.0/13, + 218.64.0.0/11, + 218.96.0.0/16, + 218.97.0.0/17, + 218.97.160.0/19, + 218.97.192.0/18, + 218.98.0.0/15, + 218.100.88.0/21, + 218.100.96.0/19, + 218.100.128.0/17, + 218.104.0.0/14, + 218.108.0.0/15, + 218.185.192.0/19, + 218.185.240.0/24, + 218.185.242.0/24, + 218.185.244.0/24, + 218.185.246.0/23, + 218.192.0.0/12, + 218.240.0.0/14, + 218.244.0.0/15, + 218.246.0.0/16, + 218.247.0.0/18, + 218.247.96.0/19, + 218.247.128.0/17, + 218.249.0.0/16, + 219.72.0.0/16, + 219.82.0.0/16, + 219.83.128.0/17, + 219.90.68.0/22, + 219.90.72.0/21, + 219.128.0.0/11, + 219.216.0.0/13, + 219.224.0.0/13, + 219.232.0.0/15, + 219.234.10.0/23, + 219.234.12.0/22, + 219.234.32.0/19, + 219.234.64.0/18, + 219.234.128.0/17, + 219.235.0.0/16, + 219.236.0.0/14, + 219.242.0.0/15, + 219.244.0.0/14, + 220.101.192.0/18, + 220.112.0.0/14, + 220.152.128.0/17, + 220.154.0.0/15, + 220.158.240.0/22, + 220.160.0.0/13, + 220.168.0.0/14, + 220.172.0.0/15, + 220.174.0.0/20, + 220.174.16.0/21, + 220.174.24.0/23, + 220.174.28.0/22, + 220.174.32.0/19, + 220.174.64.0/20, + 220.174.80.0/21, + 220.174.88.0/22, + 220.174.92.0/23, + 220.174.96.0/19, + 220.174.128.0/17, + 220.175.0.0/16, + 220.176.0.0/12, + 220.192.0.0/12, + 220.231.0.0/18, + 220.231.128.0/17, + 220.232.64.0/18, + 220.234.0.0/16, + 220.242.0.0/22, + 220.242.4.0/23, + 220.242.6.0/24, + 220.242.8.0/21, + 220.242.16.0/20, + 220.242.32.0/19, + 220.242.64.0/18, + 220.242.128.0/17, + 220.243.0.0/16, + 220.247.136.0/23, + 220.247.142.0/23, + 220.248.0.0/14, + 220.252.0.0/16, + 221.0.0.0/13, + 221.8.0.0/14, + 221.12.0.0/17, + 221.12.128.0/18, + 221.13.0.0/16, + 221.14.0.0/15, + 221.122.0.0/15, + 221.128.132.0/22, + 221.128.136.0/21, + 221.128.144.0/20, + 221.128.160.0/19, + 221.128.192.0/23, + 221.128.194.0/24, + 221.128.196.0/22, + 221.128.200.0/21, + 221.128.208.0/20, + 221.128.224.0/21, + 221.128.232.0/22, + 221.128.238.0/23, + 221.129.0.0/16, + 221.130.0.0/15, + 221.133.224.0/19, + 221.136.0.0/15, + 221.172.0.0/14, + 221.176.0.0/13, + 221.192.0.0/14, + 221.196.0.0/15, + 221.198.0.0/16, + 221.199.0.0/17, + 221.199.128.0/18, + 221.199.192.0/20, + 221.199.224.0/19, + 221.200.0.0/13, + 221.208.0.0/12, + 221.224.0.0/12, + 222.16.0.0/12, + 222.32.0.0/11, + 222.64.0.0/11, + 222.125.0.0/16, + 222.126.128.0/17, + 222.128.0.0/12, + 222.160.0.0/14, + 222.168.0.0/13, + 222.176.0.0/12, + 222.192.0.0/11, + 222.240.0.0/13, + 222.248.0.0/15, + 223.0.0.0/12, + 223.20.0.0/15, + 223.27.184.0/22, + 223.29.208.0/22, + 223.29.252.0/24, + 223.29.255.0/24, + 223.64.0.0/11, + 223.96.0.0/12, + 223.112.0.0/14, + 223.116.0.0/15, + 223.121.128.0/17, + 223.122.4.0/22, + 223.122.12.0/22, + 223.122.18.0/23, + 223.122.20.0/22, + 223.122.26.0/23, + 223.122.28.0/22, + 223.123.196.0/22, + 223.123.204.0/22, + 223.123.208.0/20, + 223.123.224.0/19, + 223.124.0.0/14, + 223.128.0.0/15, + 223.144.0.0/12, + 223.160.0.0/14, + 223.166.0.0/15, + 223.192.0.0/15, + 223.198.0.0/15, + 223.201.0.0/16, + 223.202.0.0/15, + 223.208.0.0/13, + 223.220.0.0/15, + 223.223.176.0/20, + 223.223.192.0/20, + 223.240.0.0/13, + 223.248.0.0/14, + 223.252.128.0/17, + 223.254.0.0/17, + 223.254.160.0/19, + 223.254.192.0/18, + 223.255.0.0/17, + 223.255.236.0/22, + 223.255.252.0/23 + } +} diff --git a/clashoo/files/usr/share/clashoo/rpc/rpc_async.sh b/clashoo/files/usr/share/clashoo/rpc/rpc_async.sh new file mode 100644 index 000000000..ee00dfd43 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/rpc/rpc_async.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +ACTION="$1" +LOG_FILE="/tmp/clash_update.txt" + +log_line() { + printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOG_FILE" +} + +run_init_async() { + local action="$1" + mkdir -p /tmp/lock >/dev/null 2>&1 + (flock /tmp/lock/clashoo_rpc_async.lock /etc/init.d/clashoo "$action" >/dev/null 2>&1 >"$LOG_FILE" 2>&1 &-; nohup /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1 &2 + exit 1 + ;; +esac diff --git a/clashoo/files/usr/share/clashoo/ruleset/cn-ip.srs b/clashoo/files/usr/share/clashoo/ruleset/cn-ip.srs new file mode 100644 index 000000000..bc53b98ff Binary files /dev/null and b/clashoo/files/usr/share/clashoo/ruleset/cn-ip.srs differ diff --git a/clashoo/files/usr/share/clashoo/ruleset/cn.mrs b/clashoo/files/usr/share/clashoo/ruleset/cn.mrs new file mode 100644 index 000000000..24308eda4 Binary files /dev/null and b/clashoo/files/usr/share/clashoo/ruleset/cn.mrs differ diff --git a/clashoo/files/usr/share/clashoo/ruleset/geolocation-!cn.srs b/clashoo/files/usr/share/clashoo/ruleset/geolocation-!cn.srs new file mode 100644 index 000000000..17ba01545 Binary files /dev/null and b/clashoo/files/usr/share/clashoo/ruleset/geolocation-!cn.srs differ diff --git a/clashoo/files/usr/share/clashoo/ruleset/geosite-cn.srs b/clashoo/files/usr/share/clashoo/ruleset/geosite-cn.srs new file mode 100644 index 000000000..52341a40d Binary files /dev/null and b/clashoo/files/usr/share/clashoo/ruleset/geosite-cn.srs differ diff --git a/clashoo/files/usr/share/clashoo/ruleset/private-ip.srs b/clashoo/files/usr/share/clashoo/ruleset/private-ip.srs new file mode 100644 index 000000000..7fa17b9af Binary files /dev/null and b/clashoo/files/usr/share/clashoo/ruleset/private-ip.srs differ diff --git a/clashoo/files/usr/share/clashoo/runtime/backup.sh b/clashoo/files/usr/share/clashoo/runtime/backup.sh new file mode 100755 index 000000000..85bcc17c5 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/runtime/backup.sh @@ -0,0 +1,21 @@ +#!/bin/sh +. /lib/functions.sh + +CURL_GROUP_CACHE="/usr/share/clashbackup/clash_gorup.json" +CURL_NOW_CACHE="/usr/share/clashbackup/clash_now.json" +CURL_CACHE="/usr/share/clashbackup/clash_curl.json" +HISTORY_PATH="/usr/share/clashbackup/history" +SECRET=$(uci get clashoo.config.dash_pass 2>/dev/null) +LAN_IP=$(uci get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null) +PORT=$(uci get clashoo.config.dash_port 2>/dev/null) + +curl -m 5 --retry 2 -w %{http_code}"\n" -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X GET http://"$LAN_IP":"$PORT"/proxies > "$CURL_CACHE" 2>/dev/null +if [ "$(sed -n '$p' "$CURL_CACHE" 2>/dev/null)" = "200" ]; then + if [ ! -d /usr/share/clashbackup ];then + mkdir -p /usr/share/clashbackup 2>/dev/null + fi + cat "$CURL_CACHE" |jsonfilter -e '@["proxies"][@.type="Selector"]["name"]' > "$CURL_GROUP_CACHE" 2>/dev/null + cat "$CURL_CACHE" |jsonfilter -e '@["proxies"][@.type="Selector"]["now"]' > "$CURL_NOW_CACHE" 2>/dev/null + awk 'NR==FNR{a[i]=$0;i++}NR>FNR{print a[j]"#*#"$0;j++}' "$CURL_GROUP_CACHE" "$CURL_NOW_CACHE" > "$HISTORY_PATH" 2>/dev/null +fi +rm -rf /usr/share/clashbackup/clash_*.json 2>/dev/null diff --git a/clashoo/files/usr/share/clashoo/runtime/clashoo.sh b/clashoo/files/usr/share/clashoo/runtime/clashoo.sh new file mode 100755 index 000000000..32c3c777c --- /dev/null +++ b/clashoo/files/usr/share/clashoo/runtime/clashoo.sh @@ -0,0 +1,347 @@ +#!/bin/sh + +REAL_LOG="/usr/share/clashoo/clashoo_real.txt" +UPDATE_LOG="/tmp/clash_update.txt" +LIST_FILE="/usr/share/clashbackup/confit_list.conf" +SUB_DIR="/usr/share/clashoo/config/sub" +TMP_PREFIX="/tmp/clash_sub_$$" +DEFAULT_SUB_UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36' + +subtype="$(uci -q get clashoo.config.subcri 2>/dev/null)" +config_name_raw="$(uci -q get clashoo.config.config_name 2>/dev/null)" +lang="$(uci -q get luci.main.lang 2>/dev/null)" + +log_text() { + if [ "$lang" = "en" ]; then + echo "$1" >"$REAL_LOG" + else + echo "$2" >"$REAL_LOG" + fi +} + +log_update() { + printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG" +} + +sanitize_name() { + local name + name="$1" + name="$(printf '%s' "$name" | tr 'A-Z' 'a-z')" + name="$(printf '%s' "$name" | sed -e 's/\.yaml$//' -e 's/\.yml$//')" + name="$(printf '%s' "$name" | tr ' /' '--')" + name="$(printf '%s' "$name" | sed -e 's/[^a-z0-9._-]/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')" + printf '%s' "$name" +} + +sanitize_custom_name() { + local name + name="$1" + name="$(printf '%s' "$name" | sed -e 's/\.yaml$//' -e 's/\.yml$//')" + name="$(printf '%s' "$name" | tr ' /' '--')" + name="$(printf '%s' "$name" | sed -e 's/[\\]//g' -e 's/\.\.+/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')" + printf '%s' "$name" +} + +url_decode() { + printf '%s' "$1" | sed 's/%/\\x/g' | xargs -0 printf '%b' 2>/dev/null || printf '%s' "$1" +} + +url_to_name() { + local url host qname decoded + url="$1" + + qname="$(printf '%s' "$url" | sed -n 's/.*[?&]filename=\([^&#]*\).*/\1/p')" + [ -n "$qname" ] || qname="$(printf '%s' "$url" | sed -n 's/.*[?&]name=\([^&#]*\).*/\1/p')" + if [ -n "$qname" ]; then + # URL decode then strip non-filename chars, keep CJK and alphanumeric + decoded="$(url_decode "$qname")" + decoded="$(printf '%s' "$decoded" | tr -d '\r\n' | sed -e 's/[[:space:]]/-/g' -e 's/[/\\:*?"<>|]//g' -e 's/\.yaml$//' -e 's/\.yml$//')" + [ -n "$decoded" ] && printf '%s' "$decoded" && return + fi + qname="$(sanitize_name "$qname")" + if [ -n "$qname" ]; then + printf '%s' "$qname" + return + fi + + host="$(printf '%s' "$url" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's/:.*$//' -e 's#\..*$##')" + host="$(sanitize_name "$host")" + [ -n "$host" ] || host="sub" + printf '%s' "$host" +} + +next_available_name() { + local base try idx + base="$1" + base="$(printf '%s' "$base" | sed -e 's/\.yaml$//' -e 's/\.yml$//')" + base="$(printf '%s' "$base" | tr ' /' '--')" + base="$(printf '%s' "$base" | sed -e 's/[\\]//g' -e 's/\.\.+/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')" + [ -n "$base" ] || base="sub" + + if [ ! -f "$SUB_DIR/${base}.yaml" ]; then + printf '%s' "$base" + return + fi + + idx=2 + while :; do + try="${base}-${idx}" + if [ ! -f "$SUB_DIR/${try}.yaml" ]; then + printf '%s' "$try" + return + fi + idx=$((idx + 1)) + done +} + +get_subscription_urls() { + uci -q show clashoo.config 2>/dev/null | awk -F"'" ' + /^clashoo.config.subscribe_url=/ { + if (NF >= 3) { + for (i = 2; i <= NF; i += 2) { + if (length($i) > 0) print $i + } + } else { + sub(/^clashoo.config.subscribe_url=/, "", $0) + if (length($0) > 0) print $0 + } + } + ' +} + +ensure_system_dns() { + local test_host + test_host="github.com" + if nslookup "$test_host" 127.0.0.1 >/dev/null 2>&1 || nslookup "$test_host" >/dev/null 2>&1; then + return 0 + fi + + uci delete dhcp.@dnsmasq[0].server >/dev/null 2>&1 + uci set dhcp.@dnsmasq[0].noresolv='0' >/dev/null 2>&1 + uci del_list dhcp.@dnsmasq[0].server='127.0.0.1#' >/dev/null 2>&1 + uci del_list dhcp.@dnsmasq[0].server='127.0.0.1#5300' >/dev/null 2>&1 + 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 + uci commit dhcp >/dev/null 2>&1 + /etc/init.d/dnsmasq restart >/dev/null 2>&1 + sleep 2 +} + +extract_host() { + printf '%s' "$1" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's#:.*$##' -e 's#.*@##' +} + +resolve_via() { + local host dns + host="$1" + dns="$2" + nslookup "$host" "$dns" 2>/dev/null | awk ' + /^Address/ { + ip = $NF + if (ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ && + ip !~ /^127\./ && + ip !~ /^0\./ && + ip !~ /^198\.18\./ && + ip != "8.8.8.8" && ip != "8.8.4.4" && + ip != "1.1.1.1" && ip != "1.0.0.1" && + ip != "223.5.5.5" && ip != "223.6.6.6" && + ip != "119.29.29.29" && ip != "114.114.114.114") { + print ip + exit + } + }' +} + +curl_subscription() { + local url tmp hdr err ua extra_resolve http_code rc + url="$1" + tmp="$2" + hdr="$3" + err="$4" + ua="$5" + extra_resolve="$6" + + rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1 + + # shellcheck disable=SC2086 + http_code="$(curl -sSL --connect-timeout 15 --max-time 60 \ + --speed-time 30 --speed-limit 1 --retry 2 \ + -H "User-Agent: ${ua}" -D "$hdr" -o "$tmp" \ + $extra_resolve \ + -w '%{http_code}' "$url" 2>"$err")" + rc=$? + printf '%s\n' "$http_code" + return "$rc" +} + +download_subscription() { + local url target tmp hdr err rc http_code info_line ua err_msg + local host ip dns extra + url="$1" + target="$2" + tmp="${TMP_PREFIX}.yaml" + hdr="${TMP_PREFIX}.hdr" + err="${TMP_PREFIX}.err" + + ua="$(uci -q get clashoo.config.sub_ua 2>/dev/null)" + [ -n "$ua" ] || ua="$DEFAULT_SUB_UA" + + if command -v curl >/dev/null 2>&1; then + http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "")" + rc=$? + + if [ "$rc" -ne 0 ] || [ "$http_code" = "000" ]; then + host="$(extract_host "$url")" + if [ -n "$host" ]; then + for dns in 223.5.5.5 119.29.29.29 1.1.1.1 8.8.8.8; do + ip="$(resolve_via "$host" "$dns")" + [ -n "$ip" ] || continue + log_update "DNS 回退:${host} -> ${ip} (@${dns})" + extra="--resolve ${host}:443:${ip} --resolve ${host}:80:${ip}" + http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "$extra")" + rc=$? + [ "$rc" -eq 0 ] && [ "$http_code" = "200" ] && break + done + fi + fi + else + wget -q --tries=4 --timeout=20 \ + --user-agent="$ua" "$url" -O "$tmp" 2>"$err" + rc=$? + http_code="" + fi + + if [ "$rc" -ne 0 ]; then + err_msg="$(grep -a 'curl:' "$err" 2>/dev/null | tail -1)" + [ -z "$err_msg" ] && err_msg="$(tail -1 "$err" 2>/dev/null)" + log_update "下载失败:$(basename "$target") rc=${rc} ${err_msg}" + rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1 + return 1 + fi + + if [ -n "$http_code" ] && [ "$http_code" != "200" ]; then + log_update "下载失败:$(basename "$target") HTTP ${http_code}" + rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1 + return 1 + fi + + if ! grep -Eq '^(proxies|proxy-providers):' "$tmp" 2>/dev/null; then + log_update "校验失败:$(basename "$target") 内容不含 proxies/proxy-providers" + rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1 + return 1 + fi + + info_line="$(grep -i 'subscription-userinfo:' "$hdr" 2>/dev/null | head -1 | \ + sed 's/^[Ss]ubscription-[Uu]serinfo:[[:space:]]*//' | tr -d '\r')" + [ -n "$info_line" ] && printf '%s\n' "$info_line" > "${target}.info" || \ + rm -f "${target}.info" >/dev/null 2>&1 + + rm -f "$hdr" "$err" >/dev/null 2>&1 + mv "$tmp" "$target" >/dev/null 2>&1 || { + rm -f "$tmp" >/dev/null 2>&1 + return 1 + } + printf '%s\n' "$ua" >"${target}.ua" + + return 0 +} + +upsert_meta() { + local filename url typ tmpf + filename="$1" + url="$2" + typ="$3" + tmpf="${TMP_PREFIX}.list" + + [ -f "$LIST_FILE" ] || touch "$LIST_FILE" + awk -F '#' -v n="$filename" '$1 != n { print $0 }' "$LIST_FILE" >"$tmpf" + printf '%s#%s#%s\n' "$filename" "$url" "$typ" >>"$tmpf" + mv "$tmpf" "$LIST_FILE" +} + +cleanup_tmp() { + rm -f "${TMP_PREFIX}.yaml" "${TMP_PREFIX}.urls" "${TMP_PREFIX}.list" \ + "${TMP_PREFIX}.hdr" "${TMP_PREFIX}.err" >/dev/null 2>&1 +} + +trap cleanup_tmp EXIT INT TERM + +[ "$subtype" = "clash" ] || [ "$subtype" = "meta" ] || subtype="clash" + +mkdir -p "$SUB_DIR" /usr/share/clashbackup >/dev/null 2>&1 +[ -f "$LIST_FILE" ] || touch "$LIST_FILE" + +URLS_FILE="${TMP_PREFIX}.urls" +get_subscription_urls | sed '/^[[:space:]]*$/d' | awk '!seen[$0]++' >"$URLS_FILE" + +url_count="$(wc -l <"$URLS_FILE" 2>/dev/null | tr -d ' ')" +if [ -z "$url_count" ] || [ "$url_count" -eq 0 ]; then + log_update "未找到订阅链接" + log_text "No subscription URL found" "未找到订阅链接" + sleep 2 + log_text "Clash for OpenWRT" "Clash for OpenWRT" + exit 1 +fi + +ensure_system_dns +log_update "开始下载订阅(共 ${url_count} 条)" +log_text "Downloading subscription..." "开始下载订阅..." + +base_name="$(sanitize_custom_name "$config_name_raw")" +timestamp="$(date +%Y%m%d)" + +success=0 +failed=0 +idx=0 +first_file="" + +while IFS= read -r url; do + [ -n "$url" ] || continue + idx=$((idx + 1)) + + if [ -n "$base_name" ]; then + if [ "$url_count" -gt 1 ]; then + name_candidate="$(sanitize_name "${base_name}-${idx}")" + file_base="$(next_available_name "$name_candidate")" + else + file_base="$base_name" + fi + else + name_candidate="$(url_to_name "$url")-${timestamp}" + if [ "$url_count" -gt 1 ]; then + name_candidate="${name_candidate}-${idx}" + fi + file_base="$(next_available_name "$name_candidate")" + fi + + target_file="$SUB_DIR/${file_base}.yaml" + if download_subscription "$url" "$target_file"; then + upsert_meta "${file_base}.yaml" "$url" "$subtype" + log_update "订阅下载成功:${file_base}.yaml" + [ -n "$first_file" ] || first_file="$target_file" + success=$((success + 1)) + else + log_update "订阅下载失败:${file_base}.yaml" + failed=$((failed + 1)) + fi +done <"$URLS_FILE" + +if [ "$success" -gt 0 ]; then + use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)" + if [ -z "$use_config" ] || [ ! -f "$use_config" ]; then + uci set clashoo.config.use_config="$first_file" + uci set clashoo.config.config_type='1' + uci commit clashoo + fi + log_text "Subscription download completed: ${success} success, ${failed} failed" "订阅下载完成:成功 ${success} 个,失败 ${failed} 个" + log_update "订阅下载完成:成功 ${success} 个,失败 ${failed} 个" + ret=0 +else + log_text "All subscription downloads failed" "订阅下载失败" + log_update "订阅下载失败:全部链接失败" + ret=1 +fi + +sleep 2 +log_text "Clash for OpenWRT" "Clash for OpenWRT" +exit "$ret" diff --git a/clashoo/files/usr/share/clashoo/runtime/dns_helpers.sh b/clashoo/files/usr/share/clashoo/runtime/dns_helpers.sh new file mode 100755 index 000000000..823fef247 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/runtime/dns_helpers.sh @@ -0,0 +1,451 @@ +#!/bin/sh +# Shared DNS helpers for ClashOO runtime generators. Keep POSIX sh compatible. + +dns_trim() { + printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' +} + +dns_has_scheme() { + printf '%s' "$1" | grep -Eq '^[A-Za-z][A-Za-z0-9+.-]*://' +} + +dns_norm_protocol() { + case "$(dns_trim "${1:-}")" in + ''|'none') printf '' ;; + udp|'udp://') printf 'udp://' ;; + tcp|'tcp://') printf 'tcp://' ;; + dot|tls|'tls://') printf 'tls://' ;; + doh|https|'https://') printf 'https://' ;; + doq|quic|'quic://') printf 'quic://' ;; + *) printf '%s' "$1" ;; + esac +} + +dns_normalize_server() { + local address protocol port prefix + address=$(dns_trim "${1:-}") + protocol=$(dns_trim "${2:-}") + port=$(dns_trim "${3:-}") + + [ -n "$address" ] || return 0 + + if dns_has_scheme "$address"; then + printf '%s' "$address" + return 0 + fi + + prefix=$(dns_norm_protocol "$protocol") + if [ -n "$port" ]; then + printf '%s%s:%s' "$prefix" "$address" "$port" + else + printf '%s%s' "$prefix" "$address" + fi +} + +dns_yaml_sq() { + printf "%s" "$1" | sed "s/'/''/g" +} + +dns_yaml_list_item() { + local value + value=$(dns_yaml_sq "$1") + printf " - '%s'\n" "$value" +} + +# Route DNS upstreams by rules and preserve the original setting. +dns_mihomo_apply_respect_rules() { + local cfg="$1" enabled="${2:-0}" tmp="${1}.$$" + [ -f "$cfg" ] || return 0 + + awk -v enabled="$enabled" ' + function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) } + function spaces(n, out,i) { + out = "" + for (i = 0; i < n; i++) + out = out " " + return out + } + function trim_value(s) { + sub(/^[[:space:]]*/, "", s) + sub(/[[:space:]]*$/, "", s) + return s + } + function flush_dns( i, sp) { + if (!in_dns) + return + sp = spaces(child_indent > 0 ? child_indent : dns_indent + 2) + print dns_lines[1] + if (enabled == "1") { + print sp "# >>> clashoo:dns_leak_protect_respect_rules" + print sp "# original-respect-rules: " (original_value != "" ? original_value : "absent") + print sp "respect-rules: true" + print sp "# <<< clashoo:dns_leak_protect_respect_rules" + } else if (original_value != "" && original_value != "absent") { + print sp "respect-rules: " original_value + } + for (i = 2; i <= dns_n; i++) + print dns_lines[i] + in_dns = 0 + dns_n = 0 + } + + /^dns:[[:space:]]*$/ { + flush_dns() + in_dns = 1 + dns_indent = indent_len($0) + child_indent = -1 + dns_n = 1 + dns_lines[dns_n] = $0 + original_value = "" + in_marker = 0 + next + } + in_dns { + cur_indent = indent_len($0) + if ($0 ~ /^[^[:space:]#][^:]*:/ && cur_indent <= dns_indent) { + flush_dns() + print + next + } + if ($0 ~ /^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_respect_rules[[:space:]]*$/) { + in_marker = 1 + if (child_indent < 0) child_indent = cur_indent + next + } + if (in_marker) { + if ($0 ~ /^[[:space:]]*#[[:space:]]*original-respect-rules:[[:space:]]*/) { + value = $0 + sub(/^[[:space:]]*#[[:space:]]*original-respect-rules:[[:space:]]*/, "", value) + original_value = trim_value(value) + } + if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_respect_rules[[:space:]]*$/) + in_marker = 0 + next + } + if ($0 ~ /^[[:space:]]*respect-rules:[[:space:]]*/) { + if (original_value == "") { + value = $0 + sub(/^[[:space:]]*respect-rules:[[:space:]]*/, "", value) + original_value = trim_value(value) + } + if (child_indent < 0) child_indent = cur_indent + next + } + if ($0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && cur_indent > dns_indent && child_indent < 0) + child_indent = cur_indent + dns_lines[++dns_n] = $0 + next + } + { print } + END { flush_dns() } + ' "$cfg" > "$tmp" && mv "$tmp" "$cfg" + rm -f "$tmp" 2>/dev/null +} + +# Use fallback DNS for queries; keep bootstrap DNS unchanged. +dns_mihomo_apply_leak_nameservers() { + local cfg="$1" enabled="${2:-0}" state="${1}.dns-leak-nameserver" + local selected="${1}.dns-leak-selected" current="${1}.dns-leak-current" tmp="${1}.$$" + [ -f "$cfg" ] || return 0 + command -v yq >/dev/null 2>&1 || return 0 + + if [ "$enabled" = "1" ]; then + yq e -r '.dns.fallback[]' "$cfg" > "$selected" 2>/dev/null || true + if [ ! -s "$selected" ]; then + printf '%s\n' 'https://1.1.1.1/dns-query' 'https://8.8.8.8/dns-query' > "$selected" + fi + yq e -r '.dns.nameserver[]' "$cfg" > "$current" 2>/dev/null || true + + if [ ! -f "$state" ] || + ! grep -q 'clashoo:dns_leak_protect_respect_rules' "$cfg" || + ! cmp -s "$current" "$selected"; then + awk ' + function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) } + /^dns:[[:space:]]*$/ { in_dns = 1; dns_indent = indent_len($0); next } + in_dns && /^[^[:space:]#][^:]*:/ { exit } + in_dns && /^[[:space:]]*nameserver:/ { + print + rest = $0 + sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest) + sub(/[[:space:]]*(#.*)?$/, "", rest) + if (rest == "") { in_ns = 1; ns_indent = indent_len($0) } + next + } + in_ns { + cur_indent = indent_len($0) + if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) { + print + next + } + exit + } + ' "$cfg" > "$state" + [ -s "$state" ] || printf '__ABSENT__\n' > "$state" + fi + + awk -v selected="$selected" ' + function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) } + function spaces(n, out,i) { + out = "" + for (i = 0; i < n; i++) out = out " " + return out + } + function print_selected(sp, line) { + print sp "nameserver:" + while ((getline line < selected) > 0) + if (line != "") print sp " - " line + close(selected) + inserted = 1 + } + /^dns:[[:space:]]*$/ { + in_dns = 1 + dns_indent = indent_len($0) + child_indent = -1 + print + next + } + in_dns && /^[^[:space:]#][^:]*:/ { + if (!inserted) print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2)) + in_dns = 0 + print + next + } + in_dns && /^[[:space:]]*[^#[:space:]][^:]*:/ && child_indent < 0 { + child_indent = indent_len($0) + } + in_dns && /^[[:space:]]*nameserver:/ { + ns_indent = indent_len($0) + rest = $0 + sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest) + sub(/[[:space:]]*(#.*)?$/, "", rest) + print_selected(spaces(ns_indent)) + if (rest == "") skip_ns = 1 + next + } + skip_ns { + cur_indent = indent_len($0) + if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next + skip_ns = 0 + } + { print } + END { + if (in_dns && !inserted) + print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2)) + } + ' "$cfg" > "$tmp" && mv "$tmp" "$cfg" + elif [ -f "$state" ]; then + awk -v state="$state" ' + function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) } + function print_original( line) { + if ((getline line < state) > 0 && line != "__ABSENT__") { + print line + while ((getline line < state) > 0) print line + } + close(state) + restored = 1 + } + /^dns:[[:space:]]*$/ { in_dns = 1; print; next } + in_dns && /^[^[:space:]#][^:]*:/ { + if (!restored) print_original() + in_dns = 0 + print + next + } + in_dns && /^[[:space:]]*nameserver:/ { + ns_indent = indent_len($0) + rest = $0 + sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest) + sub(/[[:space:]]*(#.*)?$/, "", rest) + print_original() + if (rest == "") skip_ns = 1 + next + } + skip_ns { + cur_indent = indent_len($0) + if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next + skip_ns = 0 + } + { print } + END { if (in_dns && !restored) print_original() } + ' "$cfg" > "$tmp" && mv "$tmp" "$cfg" + rm -f "$state" + fi + rm -f "$selected" "$current" "$tmp" 2>/dev/null +} + + +# handle mihomo dns block: +# - strip previous clashoo:dns_leak_protect injection markers (incl. the old +# geosite:gfw block written by older versions) +# - rewrite the ipv6: line from ipv6_value (skip when empty) +# No longer injects fallback-filter.geosite:gfw — mihomo deprecated it and a +# GeoSite.dat missing the gfw category froze startup (issue #25). See flush_dns. +# (The fallback-filter parsing vars below are now vestigial; clean up separately.) +dns_mihomo_apply_leak_dns_block() { + local cfg="$1" enabled="${2:-0}" ipv6_value="${3:-}" tmp="${1}.$$" + [ -f "$cfg" ] || return 0 + + awk -v enabled="$enabled" -v ipv6_value="$ipv6_value" ' + function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) } + function spaces(n, out,i) { + out = "" + for (i = 0; i < n; i++) + out = out " " + return out + } + function flush_dns( i, sp, ff_sp) { + if (!in_dns) + return + if (in_ff) ff_end = dns_n + sp = spaces(child_indent > 0 ? child_indent : dns_indent + 2) + ff_sp = (ff_child_indent > 0 ? spaces(ff_child_indent) : sp " ") + + print dns_lines[1] + if (ipv6_value != "") + print sp "ipv6: " ipv6_value + # NOTE: clashoo used to inject `fallback-filter.geosite: [gfw]` here, but + # mihomo deprecated fallback-filter.geosite ("replace with nameserver-policy, + # will be removed") AND it hard-fails preflight + freezes startup (~90s) when + # the downloaded GeoSite.dat lacks the gfw category (issue #25). Stop + # injecting it. DNS-leak protection still works via respect-rules / leak + # nameservers / DST-PORT,853,REJECT handled elsewhere. The marker strip below + # still removes any gfw block injected by older versions, so disabling / + # upgrading cleans it up. + for (i = 2; i <= dns_n; i++) + print dns_lines[i] + in_dns = 0 + dns_n = 0 + } + + /^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect[[:space:]]*$/ { + in_marker = 1 + next + } + in_marker { + if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect[[:space:]]*$/) + in_marker = 0 + next + } + + /^dns:[[:space:]]*$/ { + flush_dns() + in_dns = 1 + dns_indent = indent_len($0) + child_indent = -1 + dns_n = 1 + dns_lines[dns_n] = $0 + has_ff = 0 + in_ff = 0 + in_ff_geosite = 0 + ff_indent = -1 + ff_child_indent = -1 + ff_geosite_indent = -1 + ff_has_gfw = 0 + ff_end = -1 + next + } + in_dns { + cur_indent = indent_len($0) + if ($0 ~ /^[^[:space:]#][^:]*:/ && cur_indent <= dns_indent) { + flush_dns() + print + next + } + if ($0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && cur_indent > dns_indent && child_indent < 0) + child_indent = cur_indent + if (in_ff && cur_indent <= ff_indent) { + ff_end = dns_n + in_ff = 0 + in_ff_geosite = 0 + } + if (in_ff_geosite && cur_indent <= ff_geosite_indent) + in_ff_geosite = 0 + if ($0 ~ /^[[:space:]]*fallback-filter:[[:space:]]*$/) { + has_ff = 1 + in_ff = 1 + ff_indent = cur_indent + ff_child_indent = -1 + } else if (in_ff && $0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && ff_child_indent < 0) { + ff_child_indent = cur_indent + } + if (in_ff && $0 ~ /^[[:space:]]*geosite:[[:space:]]*$/) { + in_ff_geosite = 1 + ff_geosite_indent = cur_indent + } else if (in_ff_geosite && $0 ~ /^[[:space:]]*-[[:space:]]*gfw([[:space:]]*(#.*)?)?$/) { + ff_has_gfw = 1 + } + if ($0 ~ /^[[:space:]]*ipv6:[[:space:]]*/) + next + dns_lines[++dns_n] = $0 + next + } + { print } + END { flush_dns() } + ' "$cfg" > "$tmp" && mv "$tmp" "$cfg" + rm -f "$tmp" 2>/dev/null +} + +# handle DST-PORT,853,REJECT injection into rules block +# only remove Clashoo marker block, never user original rules +dns_mihomo_apply_leak_rule() { + local cfg="$1" enabled="${2:-0}" tmp="${1}.$$" has_user_853 + [ -f "$cfg" ] || return 0 + + has_user_853=$(awk ' + /^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/ { + in_marker = 1 + next + } + in_marker { + if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/) + in_marker = 0 + next + } + /^[[:space:]]*-[[:space:]]*DST-PORT,853,REJECT([[:space:]]*(#.*)?)?$/ { found = 1 } + END { print found ? 1 : 0 } + ' "$cfg") + + awk -v enabled="$enabled" -v has_user_853="$has_user_853" ' + BEGIN { inserted = 0; saw_rules = 0 } + /^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/ { + in_marker = 1 + next + } + in_marker { + if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/) + in_marker = 0 + next + } + /^rules:[[:space:]]*$/ { + saw_rules = 1 + print + if (enabled == "1" && has_user_853 != "1") { + print " # >>> clashoo:dns_leak_protect_rule" + print " - DST-PORT,853,REJECT" + print " # <<< clashoo:dns_leak_protect_rule" + inserted = 1 + } + next + } + { print } + END { + if (enabled == "1" && has_user_853 != "1" && !inserted && !saw_rules) { + print "" + print "rules:" + print " # >>> clashoo:dns_leak_protect_rule" + print " - DST-PORT,853,REJECT" + print " # <<< clashoo:dns_leak_protect_rule" + } + } + ' "$cfg" > "$tmp" && mv "$tmp" "$cfg" + rm -f "$tmp" 2>/dev/null +} + +dns_mihomo_apply_leak_protect() { + local cfg="$1" enabled="${2:-0}" ipv6_value="${3:-}" + [ -f "$cfg" ] || return 0 + dns_mihomo_apply_leak_nameservers "$cfg" "$enabled" + dns_mihomo_apply_respect_rules "$cfg" "$enabled" + dns_mihomo_apply_leak_dns_block "$cfg" "$enabled" "$ipv6_value" + dns_mihomo_apply_leak_rule "$cfg" "$enabled" +} diff --git a/clashoo/files/usr/share/clashoo/runtime/dns_loopback_adapter.yq b/clashoo/files/usr/share/clashoo/runtime/dns_loopback_adapter.yq new file mode 100644 index 000000000..12d5851e6 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/runtime/dns_loopback_adapter.yq @@ -0,0 +1,17 @@ +. as $document | +strenv(CLASHOO_DNS_OLD_PORT) as $old_port | +strenv(CLASHOO_DNS_HOST_PATTERN) as $host_pattern | +(strenv(CLASHOO_DNS_REPLACEMENT) | split(" ") | map(select(length > 0))) as $replacement | +("(?i)^(?:[a-z][a-z0-9+.-]*://)?(?:" + $host_pattern + "):" + $old_port + "(?:$|[/#?])") as $old_loopback | +( + $document | + select((.dns."proxy-server-nameserver" | type) == "!!seq") | + ([.dns."proxy-server-nameserver"[] | select(type == "!!str" and test($old_loopback))] | length) as $match_count | + select($match_count > 0) | + .dns."proxy-server-nameserver" = + ([ + .dns."proxy-server-nameserver"[] | + select((type == "!!str" and test($old_loopback)) | not) | + select(. as $server | ($replacement | contains([$server])) | not) + ] + $replacement) +) // $document diff --git a/clashoo/files/usr/share/clashoo/runtime/iprules.sh b/clashoo/files/usr/share/clashoo/runtime/iprules.sh new file mode 100755 index 000000000..29b52b970 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/runtime/iprules.sh @@ -0,0 +1,98 @@ +#!/bin/sh +. /lib/functions.sh + + +TMP_PREFIX="/tmp/clashoo_iprules.$$" +CUSTOM_RULE_FILE="${TMP_PREFIX}.ipadd.conf" +RULE="${TMP_PREFIX}.rules_conf.yaml" +CLASH="${TMP_PREFIX}.conf.yaml" +CONFIG_YAML="/etc/clashoo/config.yaml" +CLASH_CONFIG="${TMP_PREFIX}.config.yaml" +trap 'rm -f "$CUSTOM_RULE_FILE" "$RULE" "$CLASH" "$CLASH_CONFIG"' EXIT + +[ -f "$CONFIG_YAML" ] || exit 0 + +. /usr/share/clashoo/runtime/primary_group.sh + + +# __PROXY__ placeholder -> primary proxy group (cached by RPC); +# fallback to GLOBAL (built-in mihomo group) +CACHED_PRIMARY_GROUP="$(uci -q get clashoo.config.primary_proxy_group)" +PRIMARY_GROUP="$(clashoo_resolve_primary_group "$CONFIG_YAML" "$CACHED_PRIMARY_GROUP")" +[ -n "$PRIMARY_GROUP" ] || PRIMARY_GROUP="GLOBAL" + +append=$(uci get clashoo.config.append_rules 2>/dev/null) +if [ "${append:-0}" -eq 1 ];then + + if [ -f $CLASH_CONFIG ];then + rm -rf $CLASH_CONFIG 2>/dev/null + fi + + cp $CONFIG_YAML $CLASH_CONFIG 2>/dev/null + if [ ! -z "$(grep "^Rule:" "$CLASH_CONFIG")" ]; then + sed -i "/^Rule:/i\#RULESTART#" $CLASH_CONFIG 2>/dev/null + elif [ ! -z "$(grep "^rules:" "$CLASH_CONFIG")" ]; then + sed -i "/^rules:/i\#RULESTART#" $CLASH_CONFIG 2>/dev/null + fi + sed -i -e "\$a#RULEEND#" $CLASH_CONFIG 2>/dev/null + + awk '/#RULESTART#/,/#RULEEND#/{print}' "$CLASH_CONFIG" 2>/dev/null |sed "s/\'//g" 2>/dev/null |sed 's/\"//g' 2>/dev/null |sed 's/\t/ /g' 2>/dev/null |grep '^ \{0,\}- '|awk -F '- ' '{print "- "$2}' | sed 's/^ \{0,\}//' 2>/dev/null |sed 's/ \{0,\}$//' 2>/dev/null >$RULE 2>&1 + + sed -i '/#RULESTART#/,/#RULEEND#/d' "$CLASH_CONFIG" 2>/dev/null + + sed -i -e "\$a " $CLASH_CONFIG 2>/dev/null + sed -i "1i\rules:" $RULE 2>/dev/null + cat $CLASH_CONFIG $RULE >$CLASH 2>/dev/null + mv $CLASH $CONFIG_YAML 2>/dev/null + rm -f $RULE $CLASH_CONFIG 2>/dev/null +fi + +# ===== custom routing rules (UCI: config addtype), decoupled ===== +# fields: type(DOMAIN|SUFFIX|KEYWORD|IP-CIDR), ipaaddr, pgroup, res(no-resolve) +# pgroup: DIRECT or __PROXY__ (resolved at injection) +rm -f "$CUSTOM_RULE_FILE" 2>/dev/null + +ipadd() +{ + local section="$1" + config_get "pgroup" "$section" "pgroup" "" + config_get "ipaaddr" "$section" "ipaaddr" "" + config_get "type" "$section" "type" "" + config_get "res" "$section" "res" "" + + [ -z "$type" ] && return + [ -z "$ipaaddr" ] && return + [ -z "$pgroup" ] && return + + [ "$pgroup" = "__PROXY__" ] && pgroup="$PRIMARY_GROUP" + + # quote whole rule (group name may contain spaces/emoji) + # so mixed indent with subscription rules does not break YAML parsing + if [ "${res}" = "1" ];then + echo "${RULE_INDENT}- \"$type,$ipaaddr,$pgroup,no-resolve\"" >> "$CUSTOM_RULE_FILE" + else + echo "${RULE_INDENT}- \"$type,$ipaaddr,$pgroup\"" >> "$CUSTOM_RULE_FILE" + fi +} + +# detect existing rules indent for alignment (default 2) +RULE_INDENT="$(awk '/^[[:space:]]*rules:/{f=1;next} f&&/^[[:space:]]*-/{match($0,/^[[:space:]]*/);print substr($0,1,RLENGTH);exit}' "$CONFIG_YAML" 2>/dev/null)" +[ -n "$RULE_INDENT" ] || RULE_INDENT=" " + +config_load "clashoo" +config_foreach ipadd "addtype" + +# clean previous injection block (idempotent) +sed -i '/#CUSTOMRULESTART#/,/#CUSTOMRULEEND#/d' "$CONFIG_YAML" 2>/dev/null + +if [ -f "$CUSTOM_RULE_FILE" ];then + sed -i -e "\$a#CUSTOMRULEEND#" "$CUSTOM_RULE_FILE" 2>/dev/null + if [ ! -z "$(grep "^ \{0,\}rules:" "$CONFIG_YAML")" ]; then + # 插在 rules: 段最前面,优先于订阅自带规则命中(纠错) + sed -i '/^ \{0,\}rules:/a\#CUSTOMRULESTART#' "$CONFIG_YAML" 2>/dev/null + else + echo "rules:" >> "$CONFIG_YAML" 2>/dev/null + echo "#CUSTOMRULESTART#" >> "$CONFIG_YAML" 2>/dev/null + fi + sed -i "/#CUSTOMRULESTART#/r${CUSTOM_RULE_FILE}" "$CONFIG_YAML" 2>/dev/null +fi diff --git a/clashoo/files/usr/share/clashoo/runtime/mixin.uc b/clashoo/files/usr/share/clashoo/runtime/mixin.uc new file mode 100644 index 000000000..221a2fa9d --- /dev/null +++ b/clashoo/files/usr/share/clashoo/runtime/mixin.uc @@ -0,0 +1,317 @@ +#!/usr/bin/ucode +'use strict'; + +import { cursor } from 'uci'; +const uci = cursor(); + +function b(v) { + if (v == null || v == '' || v == '0' || v == 'false') return false; + if (v == '1' || v == 'true') return true; + return v; +} +function i(v, d) { let n = v != null ? int(v) : null; return n != null ? n : d; } +function s(v, d) { let t = trim(v || ''); return length(t) ? t : d; } +function a(k) { return uci.get('clashoo', 'config', k); } +function ab(k) { return b(a(k)); } + +const cfg = {}; + +/* ── 端口与基本设置 ─────────────────────────────────────── */ +let v; +v = i(a('http_port')); if (v != null) cfg['port'] = v; +v = i(a('socks_port')); if (v != null) cfg['socks-port'] = v; +v = i(a('redir_port')); if (v != null) cfg['redir-port'] = v; +v = i(a('mixed_port')); if (v != null) cfg['mixed-port'] = v; +v = i(a('tproxy_port')); if (v != null) cfg['tproxy-port'] = v; + +v = a('bind_addr'); +if (!v || v == '*') v = null; +if (v) cfg['bind-address'] = v; + +v = a('p_mode'); +if (v == 'script') v = 'rule'; +if (v) cfg['mode'] = v; + +cfg['log-level'] = s(a('level'), 'info'); +cfg['allow-lan'] = ab('allow_lan'); +cfg['ipv6'] = ab('enable_ipv6'); +cfg['routing-mark'] = 6666; +cfg['external-controller'] = '0.0.0.0:' + i(a('dash_port'), 9090); +cfg['external-ui'] = './dashboard'; +cfg['secret'] = s(a('dash_pass'), ''); + +v = a('interf'); +if (v && v != '0') cfg['interface-name'] = v; + +/* ── TUN ───────────────────────────────────────────────── */ +let tun_mode = i(a('tun_mode'), 0); +let tcp_mode = s(a('tcp_mode'), 'redirect'); +let udp_mode = s(a('udp_mode'), 'tproxy'); +let tun_enabled = (tun_mode == 1 || tcp_mode == 'tun' || udp_mode == 'tun'); +let acl_enabled = false; +uci.foreach('clashoo', 'lan_acl', function(sec) { + if (b(sec.enabled) != false) acl_enabled = true; +}); +if (!ab('acl_migrated')) { + let access_control = i(a('access_control'), 0); + acl_enabled = access_control == 1 || access_control == 2; +} +let tun_acl = tun_enabled && acl_enabled; + +cfg['tun'] = { + enable: tun_enabled, + stack: s(a('stack'), 'gvisor'), + 'auto-route': true, + 'auto-redirect': !tun_acl, + 'auto-detect-interface': true, +}; +let tun_mtu = a('tun_mtu'); +if (tun_mtu != null) cfg['tun']['mtu'] = int(tun_mtu); +if (ab('tun_gso')) { + cfg['tun']['gso'] = true; + cfg['tun']['gso-max-size'] = i(a('tun_gso_max_size'), 65536); +} +if (ab('tun_dns_hijack')) { + cfg['tun']['dns-hijack'] = ['any:53', 'tcp://any:53']; +} +if (!tun_enabled) { + cfg['tun']['enable'] = false; +} + +/* ── DNS ───────────────────────────────────────────────── */ +let dns_port = i(a('listen_port'), 1053); +cfg['dns'] = { + enable: b(a('enable_dns')) != false, + listen: '0.0.0.0:' + dns_port, + 'enhanced-mode': s(a('enhanced_mode'), 'fake-ip'), + 'fake-ip-range': s(a('fake_ip_range'), '198.18.0.1/16'), + 'fake-ip-filter': [], + ipv6: ab('enable_ipv6'), +}; +if (cfg['dns']['enhanced-mode'] == 'fake-ip' && ab('enable_ipv6')) + cfg['dns']['fake-ip-range6'] = s(a('fake_ip_range6'), 'fc00::/18'); +/* fake-ip-filter-mode: blacklist (default) | whitelist | rule */ +let filter_mode = s(a('fake_ip_filter_mode'), 'blacklist'); +if (filter_mode != 'blacklist') cfg['dns']['fake-ip-filter-mode'] = filter_mode; + +/* map geosite:cn to bundled cn.mrs rule-set so mihomo skips the 10MB geosite.dat */ +let need_cn_rs = false; +let keep_fakeip = length(s(getenv('CLASHOO_KEEP_FAKEIP_FILTER'), '')); +if (keep_fakeip) { + delete cfg['dns']['fake-ip-filter']; +} else { + let push_filter = function(f) { + if (f == 'geosite:cn') { f = 'rule-set:cn_domain'; need_cn_rs = true; } + else if (f == 'rule-set:cn_domain') need_cn_rs = true; + push(cfg['dns']['fake-ip-filter'], f); + }; + let filters = a('fake_ip_filter'); + if (type(filters) == 'array') { for (let f in filters) push_filter(f); } + else if (filters != null) push_filter(filters); +} + +/* fallback-filter(默认 geoip:false,防止冷启动依赖 MMDB) */ +cfg['dns']['fallback-filter'] = { geoip: ab('fallback_filter_geoip') }; + +let dns_present = {}; +for (let f in split(s(getenv('CLASHOO_DNS_PRESENT'), ''), ',')) + if (length(trim(f))) dns_present[trim(f)] = true; +let dns_force = {}; +for (let f in split(s(getenv('CLASHOO_DNS_FORCE'), ''), ',')) + if (length(trim(f))) dns_force[trim(f)] = true; +let dns_inherit_mode = s(getenv('CLASHOO_DNS_INHERIT_MODE'), ''); +let inherited_nameserver = json(s(getenv('CLASHOO_DNS_INHERIT_NAMESERVER'), '[]')); +if (type(inherited_nameserver) != 'array') { + if (inherited_nameserver != null && inherited_nameserver != '') + inherited_nameserver = [inherited_nameserver]; + else + inherited_nameserver = []; +} +let dns_role_fields = ['nameserver', 'proxy-server-nameserver', 'direct-nameserver', + 'default-nameserver', 'nameserver-policy', 'respect-rules']; +let had_user_dns_roles = false; +for (let f in dns_role_fields) + if (dns_present[f]) had_user_dns_roles = true; +if (length(keys(dns_force)) || !had_user_dns_roles) + cfg['dns']['x-clashoo-managed-dns'] = true; + +function dns_scheme(protocol) { + switch (trim(protocol || '')) { + case '': + case 'none': return ''; + case 'udp': case 'udp://': return 'udp://'; + case 'tcp': case 'tcp://': return 'tcp://'; + case 'dot': case 'tls': case 'tls://': return 'tls://'; + case 'doh': case 'https': case 'https://': return 'https://'; + case 'doq': case 'quic': case 'quic://': return 'quic://'; + default: return protocol; + } +} + +function dns_server(address, protocol, port) { + let addr = trim(address || ''); + if (!length(addr)) return null; + if (match(addr, /^[A-Za-z][A-Za-z0-9+.-]*:\/\//)) return addr; + let prefix = dns_scheme(protocol); + return length(trim(port || '')) ? prefix + addr + ':' + trim(port) : prefix + addr; +} + +let dns_roles = {}; +uci.foreach('clashoo', 'dnsservers', function(sec) { + if (b(sec.enabled) == false) return; + let role = s(sec.ser_type, 'nameserver'); + if (role == 'fallback' && cfg['dns']['enhanced-mode'] == 'fake-ip') return; + let srv = dns_server(sec.ser_address, sec.protocol, sec.ser_port); + if (!srv) return; + if (!dns_roles[role]) dns_roles[role] = []; + push(dns_roles[role], srv); +}); +for (let role in keys(dns_roles)) { + if (role == 'proxy-server-nameserver' && (dns_inherit_mode == 'inherit' || dns_inherit_mode == 'skip') && + !dns_present[role] && !dns_force[role]) continue; + if (!dns_present[role] || dns_force[role]) cfg['dns'][role] = dns_roles[role]; +} + +if (dns_inherit_mode == 'inherit' && length(inherited_nameserver) && + !dns_present['proxy-server-nameserver'] && !dns_force['proxy-server-nameserver']) + cfg['dns']['proxy-server-nameserver'] = inherited_nameserver; + +let bootstrap = a('default_nameserver'); +if (bootstrap == null) bootstrap = a('defaul_nameserver'); +if (type(bootstrap) != 'array') bootstrap = bootstrap != null ? [bootstrap] : []; +if (length(bootstrap) && (!dns_present['default-nameserver'] || dns_force['default-nameserver'])) + cfg['dns']['default-nameserver'] = bootstrap; + +let policies = {}; +uci.foreach('clashoo', 'dns_policy', function(sec) { + if (b(sec.enabled) == false) return; + if (s(sec.policy_type, 'nameserver-policy') != 'nameserver-policy') return; + let matcher = trim(sec.matcher || ''); + let servers = sec.nameserver; + if (type(servers) != 'array') servers = servers != null ? [servers] : []; + if (!length(matcher) || !length(servers)) return; + if (matcher == 'geosite:cn') { matcher = 'rule-set:cn_domain'; need_cn_rs = true; } + policies[matcher] = servers; +}); +if (length(keys(policies)) && (!dns_present['nameserver-policy'] || dns_force['nameserver-policy'])) + cfg['dns']['nameserver-policy'] = policies; + +let respect_rules = a('dns_respect_rules') == null ? true : ab('dns_respect_rules'); +if (dns_force['respect-rules']) + cfg['dns']['respect-rules'] = respect_rules; +else if (respect_rules && length(cfg['dns']['proxy-server-nameserver'] || []) + && !dns_present['respect-rules'] && !dns_present['prefer-h3']) + cfg['dns']['respect-rules'] = true; + +/* profile */ +let store_selected = ab('selection_cache'); +/* fake-ip cache defaults ON to survive restarts; opt out with '0' */ +let store_fake = a('fake_ip_cache') == null ? true : ab('fake_ip_cache'); +if (store_selected || store_fake) { + cfg['profile'] = {}; + if (store_selected) cfg['profile']['store-selected'] = true; + if (store_fake) cfg['profile']['store-fake-ip'] = true; +} + +/* bundled cn.mrs rule-provider, referenced by fake-ip-filter rule-set:cn_domain */ +if (need_cn_rs) { + if (!cfg['rule-providers']) cfg['rule-providers'] = {}; + cfg['rule-providers']['cn_domain'] = { + type: 'file', + path: './ruleset/cn.mrs', + format: 'mrs', + behavior: 'domain', + }; +} + + + +/* ── authentication ────────────────────────────────── */ +if (ab('authentication')) { + cfg['authentication'] = []; + uci.foreach('clashoo', 'authentication', function(sec) { + if (b(sec.enabled) == false) return; + let user = trim(sec.username || ''); + let pass = trim(sec.password || ''); + if (user) push(cfg['authentication'], user + ':' + pass); + }); +} + +/* ── hosts ────────────────────────────────────────── */ +cfg['hosts'] = {}; +uci.foreach('clashoo', 'hosts', function(sec) { + if (b(sec.enabled) == false) return; + let domain = trim(sec.adress || ''); + let ip = trim(sec.ip || ''); + if (domain && ip) cfg['hosts'][domain] = ip; +}); +if (length(keys(cfg['hosts'])) == 0) delete cfg['hosts']; + +/* ── sniffer ──────────────────────────────────────── */ +if (ab('sniffer_streaming') && !length(s(getenv('CLASHOO_KEEP_SNIFFER'), ''))) { + cfg['sniffer'] = { + enable: true, + 'force-dns-mapping': true, + 'parse-pure-ip': true, + sniff: { + HTTP: { ports: [80, 8080], 'override-destination': true }, + TLS: { ports: [443, 8443], 'override-destination': true }, + QUIC: { ports: [443, 8443], 'override-destination': true }, + }, + 'force-domain': [ + '+.youtube.com', + '+.googlevideo.com', + '+.netflix.com', + '+.nflxvideo.net', + '+.disneyplus.com', + '+.hulu.com', + '+.hbomax.com', + ], + 'skip-domain': [ + '+.lan', + '+.local', + ], + }; +} + +/* ── Smart kernel injection ───────────────────────── */ +if (ab('smart_auto_switch')) { + /* Smart 策略注入:通过 proxy-groups 覆盖实现 */ +} + +/* ── ECS ──────────────────────────────────────────── */ +let ecs = s(a('dns_ecs')); +if (ecs) cfg['dns']['edns-client-subnet'] = ecs; + +/* ── cache_file / experimental ──────────────────── */ +cfg['experimental'] = { + 'cache_file': { enabled: true }, + 'clash_api': { + 'external-controller': '0.0.0.0:' + i(a('dash_port'), 9090), + 'external-ui': './dashboard', + 'secret': s(a('dash_pass'), ''), + }, +}; + + +/* ── 输出 JSON (不含 null/空对象) ─────────────────── */ +function clean(obj) { + if (type(obj) == 'array') { + let r = []; + for (let e in obj) { + e = clean(e); + if (e != null) push(r, e); + } + return length(r) ? r : null; + } + if (type(obj) == 'object') { + let r = {}; + for (let k in keys(obj)) { + let v = clean(obj[k]); + if (v != null) r[k] = v; + } + return length(keys(r)) ? r : null; + } + return obj; +} +print(clean(cfg)); diff --git a/clashoo/files/usr/share/clashoo/runtime/primary_group.sh b/clashoo/files/usr/share/clashoo/runtime/primary_group.sh new file mode 100755 index 000000000..997e941bd --- /dev/null +++ b/clashoo/files/usr/share/clashoo/runtime/primary_group.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +clashoo_resolve_primary_group() { + local config_yaml="$1" + local cached_group="$2" + local group_rows + local resolved_group + + if [ "$cached_group" = "GLOBAL" ]; then + printf '%s\n' "GLOBAL" + return 0 + fi + + if [ ! -f "$config_yaml" ] || ! command -v yq >/dev/null 2>&1; then + printf '%s\n' "GLOBAL" + return 0 + fi + + group_rows="$(yq -M e -r '(."proxy-groups" // [])[] | [.name, .type] | @tsv' "$config_yaml" 2>/dev/null)" || { + printf '%s\n' "GLOBAL" + return 0 + } + + if [ -n "$cached_group" ] && printf '%s\n' "$group_rows" | + awk -F '\t' -v wanted="$cached_group" '$1 == wanted { found = 1; exit } END { exit !found }' + then + printf '%s\n' "$cached_group" + return 0 + fi + + resolved_group="$(printf '%s\n' "$group_rows" | awk -F '\t' ' + tolower($2) == "select" && + (index($1, "节点选择") || index($1, "代理") || index($1, "🚀") || tolower($1) ~ /proxy/) { + print $1 + exit + } + ')" + + if [ -z "$resolved_group" ]; then + resolved_group="$(printf '%s\n' "$group_rows" | awk -F '\t' ' + tolower($2) == "select" { + print $1 + exit + } + ')" + fi + + printf '%s\n' "${resolved_group:-GLOBAL}" +} diff --git a/clashoo/files/usr/share/clashoo/runtime/quic_fallback_guard.yq b/clashoo/files/usr/share/clashoo/runtime/quic_fallback_guard.yq new file mode 100644 index 000000000..59ee6074a --- /dev/null +++ b/clashoo/files/usr/share/clashoo/runtime/quic_fallback_guard.yq @@ -0,0 +1,9 @@ +# If every configured rule points to a proxy without UDP support, mihomo falls +# through to DIRECT. Reject only that otherwise-unmatched QUIC traffic so the +# client can retry over TCP; rules with a usable UDP target still win first. +with( + select(has("rules")) | + select((.rules | type) == "!!seq") | + select(([.rules[] | select(. == "AND,((NETWORK,UDP),(DST-PORT,443)),REJECT")] | length) == 0); + .rules += ["AND,((NETWORK,UDP),(DST-PORT,443)),REJECT"] +) diff --git a/clashoo/files/usr/share/clashoo/runtime/restore.sh b/clashoo/files/usr/share/clashoo/runtime/restore.sh new file mode 100755 index 000000000..4ce19ca62 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/runtime/restore.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +if [ -f /usr/share/clashbackup/history ];then + +HISTORY_PATH="/usr/share/clashbackup/history" +SECRET=$(uci get clashoo.config.dash_pass 2>/dev/null) +LAN_IP=$(uci get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null) +PORT=$(uci get clashoo.config.dash_port 2>/dev/null) +urlencode() { + local data + if [ "$#" != 1 ]; then + return 1 + fi + data=$(curl -s -o /dev/null -w %{url_effective} --get --data-urlencode "$1" "") + if [ ! -z "$data" ]; then + echo "${data##/?}" + fi + return 0 +} +cat $HISTORY_PATH |while read line +do + if [ -z "$(echo $line |grep "#*#")" ]; then + continue + else + GORUP_NAME=$(urlencode "$(echo $line |awk -F '#*#' '{print $1}')") + NOW_NAME=$(echo $line |awk -F '#*#' '{print $3}') + curl -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X PUT -d '{"name":"'"$NOW_NAME"'"}' http://"$LAN_IP":"$PORT"/proxies/"$GORUP_NAME" >/dev/null 2>&1 + fi +done >/dev/null 2>&1 +curl -m 5 --retry 2 -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X DELETE http://"$LAN_IP":"$PORT"/connections >/dev/null 2>&1 +fi \ No newline at end of file diff --git a/clashoo/files/usr/share/clashoo/runtime/sniffer_overrides.yq b/clashoo/files/usr/share/clashoo/runtime/sniffer_overrides.yq new file mode 100644 index 000000000..885d81afb --- /dev/null +++ b/clashoo/files/usr/share/clashoo/runtime/sniffer_overrides.yq @@ -0,0 +1,28 @@ +# Preserve every explicit user choice while restoring missing per-protocol defaults. +with( + .sniffer | + select((has("enable") | not) or (.enable == true)) | + select((has("override-destination") | not) or (."override-destination" == true)); + with( + select(has("force-dns-mapping") | not); + ."force-dns-mapping" = true + ) | + with( + select(has("parse-pure-ip") | not); + ."parse-pure-ip" = true + ) | + with( + .sniff | select(has("TLS")); + with( + .TLS | select(type == "!!map" and (has("override-destination") | not)); + ."override-destination" = true + ) + ) | + with( + .sniff | select(has("QUIC")); + with( + .QUIC | select(type == "!!map" and (has("override-destination") | not)); + ."override-destination" = true + ) + ) +) diff --git a/clashoo/files/usr/share/clashoo/server.list b/clashoo/files/usr/share/clashoo/server.list new file mode 100755 index 000000000..23dfb38d7 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/server.list @@ -0,0 +1,63 @@ +time.windows.com +time.nist.gov +time.apple.com +time.asia.apple.com +cn.ntp.org.cn +edu.ntp.org.cn +hk.ntp.org.cn +tw.ntp.org.cn +us.ntp.org.cn +sgp.ntp.org.cn +kr.ntp.org.cn +jp.ntp.org.cn +de.ntp.org.cn +ina.ntp.org.cn +0.openwrt.pool.ntp.org +1.openwrt.pool.ntp.org +2.openwrt.pool.ntp.org +3.openwrt.pool.ntp.org +ntp.aliyun.com +ntp1.aliyun.com +ntp2.aliyun.com +ntp3.aliyun.com +ntp4.aliyun.com +ntp5.aliyun.com +ntp6.aliyun.com +ntp7.aliyun.com +time1.aliyun.com +time2.aliyun.com +time3.aliyun.com +time4.aliyun.com +time5.aliyun.com +time6.aliyun.com +time7.aliyun.com +s1c.time.edu.cn +s2m.time.edu.cn +s1b.time.edu.cn +s1e.time.edu.cn +s2a.time.edu.cn +s2b.time.edu.cn +time1.apple.com +time2.apple.com +time3.apple.com +time4.apple.com +time5.apple.com +time6.apple.com +time7.apple.com +time1.google.com +time2.google.com +time3.google.com +time4.google.com +.music.163.com +.music.126.net +man.netease.com +api.iplay.163.com +ac.dun.163yun.com +mr.da.netease.com +crash.163.com +imap.163.com +msftconnecttest.com +msftncsi.com +captive.qq.com +ssl.ptlogin2.qq.com +login.qq.com diff --git a/clashoo/files/usr/share/clashoo/update/component_update.sh b/clashoo/files/usr/share/clashoo/update/component_update.sh new file mode 100755 index 000000000..2ad2eb2a3 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/component_update.sh @@ -0,0 +1,642 @@ +#!/bin/sh + +LOG_FILE="/tmp/clashoo_component_update.log" +RUN_FILE="/var/run/clashoo_component_update" +STATE_FILE="/tmp/clashoo_component_update_state" +TMP_DIR="/tmp/clashoo-component-update" +APK_UPGRADE_FLAG="/var/run/clashoo_package_upgrade" +FEED_BASE_URL="https://down.dllkids.xyz/openwrt-feed/clashoo" +GITHUB_API_URL="https://api.github.com/repos/kenzok8/openwrt-clashoo/releases/latest" +ENV_GITHUB_PROXY_PREFIX="${GITHUB_PROXY_PREFIX:-}" +UCI_GITHUB_PROXY_PREFIX="$(uci -q get clashoo.config.core_mirror_prefix 2>/dev/null)" +GITHUB_PROXY_PREFIX="${UCI_GITHUB_PROXY_PREFIX:-${ENV_GITHUB_PROXY_PREFIX:-https://ghfast.top/}}" +case "$GITHUB_PROXY_PREFIX" in + */) ;; + *) GITHUB_PROXY_PREFIX="${GITHUB_PROXY_PREFIX}/" ;; +esac +CONNECT_TIMEOUT="${CONNECT_TIMEOUT:-8}" +REQUEST_TIMEOUT="${REQUEST_TIMEOUT:-20}" +LOW_SPEED_TIME="${LOW_SPEED_TIME:-30}" +LOW_SPEED_LIMIT="${LOW_SPEED_LIMIT:-1024}" + +trap 'rm -f "$APK_UPGRADE_FLAG"' EXIT + +log() { + mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$STATE_FILE")" >/dev/null 2>&1 + printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$LOG_FILE" + printf 'component=%s\nstatus=%s\nmessage=%s\n' "${COMPONENT:-}" "${STATUS:-running}" "$*" >"$STATE_FILE" +} + +finish() { + rc="$1" + msg="$2" + STATUS="success" + [ "$rc" -eq 0 ] || STATUS="failed" + log "$msg" + rm -f "$RUN_FILE" "$APK_UPGRADE_FLAG" + exit "$rc" +} + +# In kernel-only mode there is no transparent proxy, so component downloads +# would go out direct and stall behind the GFW. Route them through the running +# core (shared logic in proxy_lib.sh). Normal mode returns empty -> TPROXY. +# Guard the source: on a stale install missing proxy_lib.sh, an unguarded `.` +# under `set -e` kills the whole update silently (empty log, nothing happens). +# Fall back to no proxy detection (direct / TPROXY) instead of dying. +if [ -f /usr/share/clashoo/update/proxy_lib.sh ]; then + . /usr/share/clashoo/update/proxy_lib.sh +else + clashoo_detect_proxy() { :; } +fi +detect_proxy() { clashoo_detect_proxy; } + +fetch_text() { + url="$1" + proxy="$(detect_proxy)" + if command -v curl >/dev/null 2>&1; then + curl -fsSL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$REQUEST_TIMEOUT" ${proxy:+--proxy "$proxy"} "$url" + return $? + fi + if command -v wget >/dev/null 2>&1; then + [ -n "$proxy" ] && { http_proxy="$proxy" https_proxy="$proxy" wget -qO- --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"; return $?; } + wget -qO- --timeout="$REQUEST_TIMEOUT" --tries=1 "$url" + return $? + fi + return 127 +} + +download_file() { + url="$1" + out="$2" + proxy="$(detect_proxy)" + if command -v curl >/dev/null 2>&1; then + curl -fL --connect-timeout "$CONNECT_TIMEOUT" --speed-time "$LOW_SPEED_TIME" --speed-limit "$LOW_SPEED_LIMIT" ${proxy:+--proxy "$proxy"} "$url" -o "$out" + return $? + fi + if [ -n "$proxy" ]; then + http_proxy="$proxy" https_proxy="$proxy" wget -qO "$out" --timeout="$REQUEST_TIMEOUT" --tries=1 "$url" + return $? + fi + wget -qO "$out" --timeout="$REQUEST_TIMEOUT" --tries=1 "$url" +} + +download_url() { + url="$1" + case "$url" in + https://github.com/*) + printf '%s%s\n' "$GITHUB_PROXY_PREFIX" "$url" + ;; + *) + printf '%s\n' "$url" + ;; + esac +} + +root_free_kb() { + df -k / 2>/dev/null | awk 'NR==2 {print $4}' +} + +kb_to_mib() { + awk "BEGIN {printf \"%.0f\", (${1:-0}) / 1024}" +} + +ensure_root_space() { + need_kb="$1" + free_kb="$(root_free_kb)" + [ -n "$free_kb" ] || return 0 + [ "$free_kb" -ge "$need_kb" ] && return 0 + finish 1 "根分区空间不足:可用 $(kb_to_mib "$free_kb") MiB,至少需要 $(kb_to_mib "$need_kb") MiB" +} + +log_install_error() { + file="$1" + [ -s "$file" ] || return 0 + tail -8 "$file" 2>/dev/null | while IFS= read -r line; do + [ -n "$line" ] && log "$line" + done +} + +detect_manager() { + if command -v opkg >/dev/null 2>&1; then + echo opkg + return + fi + if command -v apk >/dev/null 2>&1; then + echo apk + return + fi + echo unsupported +} + +installed_package_version() { + pkg="$1" + if command -v opkg >/dev/null 2>&1; then + opkg status "$pkg" 2>/dev/null | awk -F': ' '/^Version:/{print $2; exit}' + return + fi + if command -v apk >/dev/null 2>&1; then + apk list -I "$pkg" 2>/dev/null | awk '{print $1; exit}' | sed -n "s/^${pkg}-//p" + fi +} + +package_installed() { + pkg="$1" + if command -v opkg >/dev/null 2>&1; then + opkg status "$pkg" 2>/dev/null | awk ' + $1 == "Status:" && $2 == "install" && $NF == "installed" { found = 1 } + END { exit !found } + ' + return + fi + apk info -e "$pkg" >/dev/null 2>&1 +} + +apk_install() { + proxy="$(detect_proxy)" + if [ -n "$proxy" ]; then + http_proxy="$proxy" https_proxy="$proxy" apk add --allow-untrusted --timeout 30 "$@" + else + apk add --allow-untrusted --timeout 30 "$@" + fi +} + +apk_bundle_install() { + printf '%s\n' "$$" >"$APK_UPGRADE_FLAG" + apk_install "$@" + rc=$? + rm -f "$APK_UPGRADE_FLAG" + return "$rc" +} + +verify_downloaded_package() { + file="$1" + [ -s "$file" ] || return 1 + if [ "$PM" = "apk" ]; then + apk verify --allow-untrusted "$file" >/dev/null 2>&1 + return + fi + gzip -t "$file" >/dev/null 2>&1 +} + +download_package_file() { + url="$1" + out="$2" + name="$3" + download_file "$(download_url "$url")" "$out" || finish 1 "下载 ${name} 失败" + verify_downloaded_package "$out" || finish 1 "${name} 文件校验失败" +} + +detect_arch() { + pm="$1" + if [ "$pm" = "opkg" ]; then + opkg print-architecture 2>/dev/null | awk '/^arch / {print $2}' | tail -n 1 + return + fi + # apk: DISTRIB_ARCH is authoritative (e.g. aarch64_cortex-a53); apk --print-arch + # only gives the generic "aarch64" which won't match the .apk package suffix. + if [ -r /etc/openwrt_release ]; then + a="$(sed -n "s/^DISTRIB_ARCH=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)" + [ -n "$a" ] && { printf '%s\n' "$a"; return; } + fi + apk --print-arch 2>/dev/null +} + +detect_sdk() { + [ -r /etc/openwrt_release ] || return 1 + release="$(sed -n "s/^DISTRIB_RELEASE=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)" + [ -n "$release" ] || return 1 + printf '%s\n' "$release" | grep -Eo '[0-9]+\.[0-9]+' | head -n 1 +} + +append_unique_word() { + value="$1" + list="$2" + [ -n "$value" ] || { + printf '%s\n' "$list" + return + } + case " $list " in + *" $value "*) ;; + *) list="${list}${list:+ }${value}" ;; + esac + printf '%s\n' "$list" +} + +build_sdk_candidates() { + pm="$1" + candidates="" + detected_sdk="$(detect_sdk || true)" + candidates="$(append_unique_word "$detected_sdk" "$candidates")" + + if [ "$pm" = "opkg" ]; then + for sdk in 24.10 23.05 22.03 21.02; do + candidates="$(append_unique_word "$sdk" "$candidates")" + done + else + for sdk in 25.12 24.10; do + candidates="$(append_unique_word "$sdk" "$candidates")" + done + fi + + printf '%s\n' "$candidates" +} + +find_manifest_value() { + key="$1" + manifest_text="$2" + printf '%s\n' "$manifest_text" | sed -n "s/^${key}=//p" | head -n 1 +} + +load_manifest_urls() { + sdk="$1" + arch="$2" + manifest_url="${FEED_BASE_URL}/${sdk}/${arch}/manifest-clashoo.txt" + manifest_text="$(fetch_text "$manifest_url" || true)" + [ -n "$manifest_text" ] || return 1 + + core_file="$(find_manifest_value "core" "$manifest_text")" + luci_file="$(find_manifest_value "luci" "$manifest_text")" + i18n_file="$(find_manifest_value "i18n" "$manifest_text")" + [ -n "$core_file" ] || return 1 + [ -n "$luci_file" ] || return 1 + case "$core_file" in *."$EXT") ;; *) return 1 ;; esac + case "$luci_file" in *."$EXT") ;; *) return 1 ;; esac + case "$i18n_file" in ""|*."$EXT") ;; *) return 1 ;; esac + + CORE_URL="${FEED_BASE_URL}/${sdk}/${arch}/${core_file}" + LUCI_URL="${FEED_BASE_URL}/${sdk}/${arch}/${luci_file}" + I18N_URL="" + [ -n "$i18n_file" ] && I18N_URL="${FEED_BASE_URL}/${sdk}/${arch}/${i18n_file}" + SOURCE_LABEL="R2 feed ${sdk}/${arch}" + return 0 +} + +load_github_urls() { + arch="$1" + ext="$2" + payload="$(fetch_text "$GITHUB_API_URL" || true)" + [ -n "$payload" ] || return 1 + + urls="$(printf '%s\n' "$payload" | sed -n 's/.*"browser_download_url":[[:space:]]*"\([^"]*\)".*/\1/p')" + [ -n "$urls" ] || return 1 + + if [ "$ext" = "apk" ]; then + CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo-[^-]+.*-r[0-9]+-'"$arch"'\.apk$' | head -n 1)" + LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)" + I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)" + else + CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo_.*_'"$arch"'\.ipk$' | head -n 1)" + LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo_.*_all\.ipk$' | head -n 1)" + I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn_.*_all\.ipk$' | head -n 1)" + fi + + [ -n "$CORE_URL" ] || return 1 + [ -n "$LUCI_URL" ] || return 1 + SOURCE_LABEL="GitHub latest" + return 0 +} + +package_version_from_url() { + file="${1##*/}" + case "$file" in + clashoo_*_"$ARCH".ipk) + v="${file#clashoo_}" + v="${v%_${ARCH}.ipk}" + printf '%s\n' "$v" | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/' + ;; + clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^clashoo_\(.*\)_[^_][^_]*\.ipk$/\1/p' | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/' ;; + luci-app-clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^luci-app-clashoo_\(.*\)_all\.ipk$/\1/p' ;; + luci-i18n-clashoo-zh-cn_*.ipk) printf '%s\n' "$file" | sed -n 's/^luci-i18n-clashoo-zh-cn_\(.*\)_all\.ipk$/\1/p' ;; + clashoo-*.apk) + v="$(printf '%s\n' "$file" | sed -n -e 's/^clashoo-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^clashoo-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1)" + printf '%s\n' "$v" | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/' + ;; + luci-app-clashoo-*.apk) printf '%s\n' "$file" | sed -n -e 's/^luci-app-clashoo-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^luci-app-clashoo-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1 ;; + luci-i18n-clashoo-zh-cn-*.apk) printf '%s\n' "$file" | sed -n -e 's/^luci-i18n-clashoo-zh-cn-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^luci-i18n-clashoo-zh-cn-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1 ;; + *) printf '%s\n' "$file" ;; + esac +} + +resolve_bundle_urls() { + PM="$(detect_manager)" + [ "$PM" != "unsupported" ] || return 1 + ARCH="$(detect_arch "$PM")" + [ -n "$ARCH" ] || return 1 + EXT="ipk" + [ "$PM" = "apk" ] && EXT="apk" + + CORE_URL="" + LUCI_URL="" + I18N_URL="" + SOURCE_LABEL="" + + SDK_CANDIDATES="$(build_sdk_candidates "$PM")" + for sdk in $SDK_CANDIDATES; do + if load_manifest_urls "$sdk" "$ARCH"; then + return 0 + fi + done + + load_github_urls "$ARCH" "$EXT" +} + +backup_config() { + ts="$(date '+%Y%m%d-%H%M%S')" + BACKUP_DIR="/etc/clashoo/backup/component-upgrade-${ts}" + mkdir -p "$BACKUP_DIR/etc-config" "$BACKUP_DIR/etc-clashoo" >/dev/null 2>&1 || return 1 + [ -r /etc/config/clashoo ] && cp -a /etc/config/clashoo "$BACKUP_DIR/etc-config/clashoo" + # 只备份用户配置;Model.bin 等可重新下载的大文件不入备份,避免每次更新 + # 复制 ~5MB 的同一模型,把备份目录撑大、堆满本就紧张的根分区。 + for path in \ + /etc/clashoo/config.yaml \ + /etc/clashoo/config.json \ + /etc/clashoo/*.yaml \ + /etc/clashoo/*.yml \ + /etc/clashoo/*.json + do + [ -e "$path" ] || continue + cp -a "$path" "$BACKUP_DIR/etc-clashoo/" 2>/dev/null || true + done + log "已备份配置:${BACKUP_DIR}" + + # 只保留最近 1 个备份,删除更早的,避免无限堆积占满磁盘。 + ls -dt /etc/clashoo/backup/component-upgrade-* 2>/dev/null | tail -n +2 | while read -r _old; do + [ -n "$_old" ] && rm -rf "$_old" 2>/dev/null + done +} + +restore_config_backup() { + [ -n "${BACKUP_DIR:-}" ] || return 0 + [ -r "$BACKUP_DIR/etc-config/clashoo" ] && cp -a "$BACKUP_DIR/etc-config/clashoo" /etc/config/clashoo + if [ -d "$BACKUP_DIR/etc-clashoo" ]; then + mkdir -p /etc/clashoo + cp -a "$BACKUP_DIR/etc-clashoo/." /etc/clashoo/ 2>/dev/null || true + fi + uci commit clashoo >/dev/null 2>&1 || true +} + +clashoo_was_running() { + /etc/init.d/clashoo status >/dev/null 2>&1 && return 0 + return 1 +} + +restart_web_stack() { + # reload (not restart) rpcd so the LuCI login session survives a plugin + # update — rpcd restart drops every ubus session and forces a re-login + # (issue #12). reload still picks up the new ucode RPC backend. uhttpd needs + # nothing: updated static assets are served on the next request. + /etc/init.d/rpcd reload >/dev/null 2>&1 || true +} + +# which: clashoo(仅核心)/ luci(luci-app + 语言包)。两者拆开,clashoo +# 核心更新频率高于 LuCI,分别更新便于定位失败。 +run_pkg_update() { + which="$1" + log "正在检查组件包" + if ! resolve_bundle_urls; then + finish 1 "未找到适配当前设备的组件包" + fi + log "更新来源:${SOURCE_LABEL}" + + core_ver="$(package_version_from_url "$CORE_URL")" + luci_ver="$(package_version_from_url "$LUCI_URL")" + i18n_ver="" + [ -z "$I18N_URL" ] || i18n_ver="$(package_version_from_url "$I18N_URL")" + core_ver_before="$(installed_package_version clashoo)" + + apk_core_ready() { + package_installed clashoo && [ "$(installed_package_version clashoo)" = "$core_ver" ] \ + && [ -x /etc/init.d/clashoo ] && [ -x /usr/share/clashoo/update/component_update.sh ] + } + + apk_luci_ready() { + package_installed luci-app-clashoo && [ "$(installed_package_version luci-app-clashoo)" = "$luci_ver" ] \ + && { [ -z "$I18N_URL" ] || { package_installed luci-i18n-clashoo-zh-cn \ + && [ "$(installed_package_version luci-i18n-clashoo-zh-cn)" = "$i18n_ver" ]; }; } \ + && [ -r /usr/share/luci/menu.d/luci-app-clashoo.json ] \ + && [ -r /usr/share/rpcd/ucode/luci.clashoo ] + } + + apk_bundle_ready() { + apk_core_ready && apk_luci_ready + } + + apk_update_ready() { + if [ "$which" = "clashoo" ]; then + apk_bundle_ready + else + package_installed clashoo && apk_luci_ready + fi + } + + if [ "$which" = "clashoo" ]; then + log "目标版本:Clashoo 核心 ${core_ver}" + cur_ver="$(installed_package_version clashoo)" + if [ "$PM" = "apk" ] && apk_update_ready; then + finish 0 "Clashoo 核心已是最新版本" + elif [ "$PM" != "apk" ] && [ -n "$core_ver" ] && [ "$cur_ver" = "$core_ver" ]; then + finish 0 "Clashoo 核心已是最新版本" + fi + else + log "目标版本:客户端 ${luci_ver}" + cur_ver="$(installed_package_version luci-app-clashoo)" + if [ "$PM" = "apk" ] && apk_update_ready; then + finish 0 "客户端已是最新版本" + elif [ "$PM" != "apk" ] && [ -n "$luci_ver" ] && [ "$cur_ver" = "$luci_ver" ]; then + finish 0 "客户端已是最新版本" + fi + fi + + rm -rf "$TMP_DIR" + mkdir -p "$TMP_DIR" || finish 1 "创建临时目录失败" + + was_running=0 + clashoo_was_running && was_running=1 + backup_config || finish 1 "备份配置失败" + apk_core_file="" + + if [ "$PM" = "apk" ]; then + log "正在下载组件包" + if [ "$which" = "clashoo" ] || ! package_installed clashoo; then + ensure_root_space 98304 + download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo" + apk_core_file="$TMP_DIR/core.${EXT}" + fi + download_package_file "$LUCI_URL" "$TMP_DIR/luci.${EXT}" "luci-app-clashoo" + if [ -n "$I18N_URL" ]; then + download_package_file "$I18N_URL" "$TMP_DIR/i18n.${EXT}" "语言包" + fi + elif [ "$which" = "clashoo" ]; then + log "正在下载 Clashoo 核心" + download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo" + else + log "正在下载客户端(LuCI + 语言包)" + download_package_file "$LUCI_URL" "$TMP_DIR/luci.${EXT}" "luci-app-clashoo" + if [ -n "$I18N_URL" ]; then + download_package_file "$I18N_URL" "$TMP_DIR/i18n.${EXT}" "语言包" + fi + fi + + if [ "$PM" != "apk" ] && [ "$which" = "clashoo" ]; then + ensure_root_space 98304 + fi + + if [ "$which" = "clashoo" ]; then + log "正在安装 Clashoo 核心" + if [ "$PM" = "opkg" ]; then + opkg install --force-downgrade "$TMP_DIR/core.${EXT}" >"$TMP_DIR/install.log" 2>&1; rc=$? + else + apk_bundle_install ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$? + fi + else + log "正在安装客户端" + if [ "$PM" = "opkg" ]; then + opkg install --force-downgrade "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$? + else + apk_bundle_install ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$? + fi + fi + + if [ "$rc" -ne 0 ]; then + if [ "$PM" = "apk" ] && apk_update_ready; then + log "apk 报告了其它软件包的错误,本次组件已升级到目标版本" + rc=0 + else + log_install_error "$TMP_DIR/install.log" + log "安装失败,正在恢复配置备份" + restore_config_backup + finish "$rc" "组件更新失败,已停止操作,请查看组件更新日志" + fi + fi + + validation_failed() { + log "安装校验失败,正在恢复配置备份" + restore_config_backup + finish 1 "组件安装校验失败,已停止操作,请查看组件更新日志" + } + + if [ "$PM" = "apk" ]; then + if ! apk_update_ready; then + log "安装校验失败,正在安全重装组件包" + if [ -z "$apk_core_file" ] && ! package_installed clashoo; then + ensure_root_space 98304 + download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo" + apk_core_file="$TMP_DIR/core.${EXT}" + fi + apk_bundle_install --force-reinstall ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" \ + ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >>"$TMP_DIR/install.log" 2>&1; repair_rc=$? + if [ "$repair_rc" -ne 0 ]; then + log_install_error "$TMP_DIR/install.log" + validation_failed + fi + apk_update_ready || validation_failed + fi + elif [ "$which" = "clashoo" ]; then + package_installed clashoo && [ "$(installed_package_version clashoo)" = "$core_ver" ] \ + && [ -x /etc/init.d/clashoo ] && [ -x /usr/share/clashoo/update/component_update.sh ] \ + || validation_failed + else + package_installed luci-app-clashoo && [ "$(installed_package_version luci-app-clashoo)" = "$luci_ver" ] \ + && [ -r /usr/share/luci/menu.d/luci-app-clashoo.json ] \ + && [ -r /usr/share/rpcd/ucode/luci.clashoo ] \ + || validation_failed + [ -z "$I18N_URL" ] || package_installed luci-i18n-clashoo-zh-cn || validation_failed + fi + + # APK component updates install the LuCI bundle in the same transaction. + if [ "$which" = "luci" ] || [ "$PM" = "apk" ]; then + log "正在刷新 LuCI 服务" + restart_web_stack + fi + core_ver_after="$(installed_package_version clashoo)" + restart_clashoo=0 + [ "$which" = "clashoo" ] && restart_clashoo=1 + [ "$PM" = "apk" ] && [ "$core_ver_before" != "$core_ver_after" ] && restart_clashoo=1 + if [ "$restart_clashoo" -eq 1 ] && [ "$was_running" -eq 1 ] && [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then + log "Clashoo 原本运行中,正在重启服务" + sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1 || /etc/init.d/clashoo restart >/dev/null 2>&1 || true + elif [ "$restart_clashoo" -eq 1 ]; then + log "Clashoo 原本未运行,保持停止状态" + fi + + finish 0 "组件更新完成" +} + +run_core_update() { + dcore="$1" + label="$2" + log "正在更新 ${label}" + # Pass the target as an arg so core_download refreshes only that binary + # without switching the active kernel. It restarts only if the target is the + # currently-running core (see core_download.sh finalize). Switching kernels + # stays a separate, explicit user action. + touch /var/run/core_update + sh /usr/share/clashoo/update/core_download.sh "$dcore" >>/tmp/clash_update.txt 2>&1 + rc=$? + rm -f /var/run/core_update + [ "$rc" -eq 0 ] || finish "$rc" "${label} 更新失败" + finish 0 "${label} 更新完成" +} + +run_lgbm_update() { + log "正在更新 Smart LightGBM 模型" + sh /usr/share/clashoo/update/lgbm_update.sh >/tmp/lgbm_update.log 2>&1 + rc=$? + tail -5 /tmp/lgbm_update.log 2>/dev/null | while IFS= read -r line; do + [ -n "$line" ] && log "$line" + done + [ "$rc" -eq 0 ] || finish "$rc" "LightGBM 模型更新失败" + finish 0 "LightGBM 模型更新完成" +} + +run_china_update() { + log "正在更新大陆白名单" + sh /usr/share/clashoo/update/update_china_ip.sh >>/tmp/clash_update.txt 2>&1 + rc=$? + tail -6 /tmp/clash_update.txt 2>/dev/null | while IFS= read -r line; do + [ -n "$line" ] && printf '%s\n' "$line" | grep -q '白名单' && log "$line" + done + [ "$rc" -eq 0 ] || finish "$rc" "大陆白名单更新失败" + finish 0 "大陆白名单更新完成" +} + +run_geoip_update() { + log "正在更新 GeoIP / GeoSite" + touch /var/run/geoip_update + sh /usr/share/clashoo/update/geoip.sh >/tmp/geoip_update.txt 2>&1 + rc=$? + rm -f /var/run/geoip_update + tail -8 /tmp/geoip_update.txt 2>/dev/null | while IFS= read -r line; do + [ -n "$line" ] && log "$line" + done + [ "$rc" -eq 0 ] || finish "$rc" "GeoIP / GeoSite 更新失败" + finish 0 "GeoIP / GeoSite 更新完成" +} + +COMPONENT="${1:-}" +VARIANT="${2:-}" # mihomo/singbox: stable | alpha +[ -n "$COMPONENT" ] || { + echo "usage: $0 [stable|alpha]" + exit 2 +} + +mkdir -p "$(dirname "$RUN_FILE")" >/dev/null 2>&1 +printf '%s\n' "$COMPONENT" >"$RUN_FILE" +STATUS="running" +log "组件更新任务启动:${COMPONENT}${VARIANT:+ ($VARIANT)}" + +case "$COMPONENT" in + clashoo) run_pkg_update clashoo ;; + luci) run_pkg_update luci ;; + mihomo) + if [ "$VARIANT" = "alpha" ]; then run_core_update 3 "mihomo Alpha 版" + else run_core_update 2 "mihomo 稳定版"; fi ;; + singbox) + if [ "$VARIANT" = "alpha" ]; then run_core_update 5 "sing-box Alpha 版" + else run_core_update 4 "sing-box 稳定版"; fi ;; + smart) run_core_update 1 "mihomo Smart 版" ;; + lgbm) run_lgbm_update ;; + china) run_china_update ;; + geoip) run_geoip_update ;; + *) finish 2 "未知组件:${COMPONENT}" ;; +esac diff --git a/clashoo/files/usr/share/clashoo/update/core_download.sh b/clashoo/files/usr/share/clashoo/update/core_download.sh new file mode 100755 index 000000000..69e0f6f06 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/core_download.sh @@ -0,0 +1,1233 @@ +#!/bin/sh + +LOG_FILE="/tmp/clash_update.txt" +MODELTYPE=$(uci get clashoo.config.download_core 2>/dev/null) +# When invoked with a target dcore arg (the component "core update" button), +# download that core WITHOUT switching the active kernel — see finalize(). +# No arg = legacy callers (manual download_core RPC / boot) use the active dcore. +TARGET_DCORE="$1" +CORETYPE=${TARGET_DCORE:-$(uci get clashoo.config.dcore 2>/dev/null)} +MIRROR_PREFIX=$(uci get clashoo.config.core_mirror_prefix 2>/dev/null) +CUSTOM_CORE_URL=$(uci get clashoo.config.core_download_url 2>/dev/null) +SINGBOX_STABLE_TARGET="${CLASHOO_SINGBOX_STABLE_TARGET:-/usr/share/clashoo/bin/sing-box-stable}" +SINGBOX_ALPHA_TARGET="${CLASHOO_SINGBOX_ALPHA_TARGET:-/usr/share/clashoo/bin/sing-box-alpha}" +SINGBOX_TMP_TEMPLATE="${CLASHOO_SINGBOX_TMP_TEMPLATE:-/tmp/clashoo-singbox.XXXXXX}" +CORE_DOWNLOAD_LOCK_DIR="${CLASHOO_CORE_DOWNLOAD_LOCK_DIR:-/var/lock/clashoo-core-download.lock}" +CORE_DOWNLOAD_LOCK_HELD=0 +CORE_INSTALLED=0 +CONNECT_TIMEOUT=15 +REQUEST_TIMEOUT=30 +DOWNLOAD_TIMEOUT=60 +LOW_SPEED_TIME=15 +LOW_SPEED_LIMIT=32768 +ATTEMPTS_PER_MIRROR=1 +MAX_TOTAL_SECONDS=200 +TAG_FETCH_RETRIES=2 +START_TS=$(date +%s) + +write_log() { + echo " $(date "+%Y-%m-%d %H:%M:%S") - $1" >> "$LOG_FILE" +} + +core_download_pid_alive() { + local pid + pid="$1" + [ -d "/proc/$pid" ] && return 0 + kill -0 "$pid" >/dev/null 2>&1 +} + +acquire_core_download_lock() { + local owner parent + parent="$(dirname "$CORE_DOWNLOAD_LOCK_DIR")" + mkdir -p "$parent" >/dev/null 2>&1 || return 1 + while :; do + if mkdir "$CORE_DOWNLOAD_LOCK_DIR" 2>/dev/null; then + printf '%s\n' "$$" > "$CORE_DOWNLOAD_LOCK_DIR/pid" || { + rmdir "$CORE_DOWNLOAD_LOCK_DIR" >/dev/null 2>&1 || true + return 1 + } + CORE_DOWNLOAD_LOCK_HELD=1 + return 0 + fi + + if [ ! -e "$CORE_DOWNLOAD_LOCK_DIR/pid" ]; then + # A fresh owner creates the directory before writing its PID. Only + # remove an actually empty directory; otherwise treat it as busy. + if rmdir "$CORE_DOWNLOAD_LOCK_DIR" >/dev/null 2>&1; then + continue + fi + return 1 + fi + owner="$(sed -n '1p' "$CORE_DOWNLOAD_LOCK_DIR/pid" 2>/dev/null)" + case "$owner" in + '' ) return 1 ;; + *[!0-9]*) + rm -f "$CORE_DOWNLOAD_LOCK_DIR/pid" >/dev/null 2>&1 || return 1 + rmdir "$CORE_DOWNLOAD_LOCK_DIR" >/dev/null 2>&1 || return 1 + ;; + *) + if core_download_pid_alive "$owner"; then + return 1 + fi + rm -f "$CORE_DOWNLOAD_LOCK_DIR/pid" >/dev/null 2>&1 || return 1 + rmdir "$CORE_DOWNLOAD_LOCK_DIR" >/dev/null 2>&1 || return 1 + ;; + esac + done +} + +release_core_download_lock() { + local owner + [ "$CORE_DOWNLOAD_LOCK_HELD" = "1" ] || return 0 + owner="$(sed -n '1p' "$CORE_DOWNLOAD_LOCK_DIR/pid" 2>/dev/null)" + if [ "$owner" = "$$" ]; then + rm -f "$CORE_DOWNLOAD_LOCK_DIR/pid" >/dev/null 2>&1 + rmdir "$CORE_DOWNLOAD_LOCK_DIR" >/dev/null 2>&1 || true + fi + CORE_DOWNLOAD_LOCK_HELD=0 +} + +finalize() { + if [ "$CORE_INSTALLED" = "1" ]; then + if [ -n "$TARGET_DCORE" ]; then + # "core update" path: only refresh the binary. Apply (restart) only + # when we updated the kernel that is actually running, so updating + # smart / sing-box never hijacks the active kernel — and updating + # several cores doesn't rotate through a restart for each. Switching + # kernels stays a separate user action (set_core handles strategy). + if [ "$TARGET_DCORE" = "$(uci get clashoo.config.dcore 2>/dev/null)" ]; then + write_log "当前内核已更新,重启 Clashoo 生效" + if [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then + /etc/init.d/clashoo restart >/dev/null 2>&1 || write_log "重启 Clashoo 失败" + fi + else + write_log "内核二进制已更新(非当前内核,不切换、不重启)" + fi + else + # legacy callers (manual download_core RPC / boot): unchanged behavior + if [ "$CORETYPE" = "1" ]; then + uci set clashoo.config.core_type="mihomo" 2>/dev/null + uci set clashoo.config.smart_auto_switch="1" 2>/dev/null + uci commit clashoo 2>/dev/null + write_log "Smart 内核已就绪,自动启用 Smart 策略" + fi + write_log "内核已替换,重启 Clashoo" + if [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then + /etc/init.d/clashoo restart >/dev/null 2>&1 || write_log "重启 Clashoo 失败" + fi + fi + fi + rm -f /var/run/core_update >/dev/null 2>&1 + release_core_download_lock +} + +timed_out() { + now_ts=$(date +%s) + [ $((now_ts - START_TS)) -ge "$MAX_TOTAL_SECONDS" ] +} + +ensure_not_timed_out() { + if timed_out; then + write_log "下载超时(超过 ${MAX_TOTAL_SECONDS} 秒)" + return 1 + fi + return 0 +} + +normalize_prefix() { + p="$1" + [ -z "$p" ] && return + case "$p" in + */) printf '%s\n' "$p" ;; + *) printf '%s/\n' "$p" ;; + esac +} + +mirror_prefixes() { + # 默认源按 2026-04 实测排序:mirror.ghproxy.com 已挂、hub.gitmirror.com/git.886.be 全死、 + # gh.idayer.com/ghproxy.net 限速截断;只保留 gh-proxy.com + ghfast.top 两条快通道。 + # 末尾会自动追加裸 GitHub(download_with_mirrors 末尾会 push 一个空 prefix)。 + custom="$(normalize_prefix "$MIRROR_PREFIX")" + prefixes="" + for prefix in "$custom" "https://gh-proxy.com/" "https://ghfast.top/"; do + [ -n "$prefix" ] || continue + case " $prefixes " in + *" $prefix "*) ;; + *) prefixes="${prefixes}${prefixes:+ }${prefix}" ;; + esac + done + printf '%s\n' "$prefixes" +} + +prefixed_url() { + prefix="$1" + base_url="$2" + if [ -z "$prefix" ]; then + echo "$base_url" + else + echo "${prefix}${base_url}" + fi +} + +fetch_url_try() { + url="$1" + if command -v curl >/dev/null 2>&1; then + curl -fsSL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$REQUEST_TIMEOUT" -A "Clash/OpenWRT" "$url" + return $? + fi + if command -v wget >/dev/null 2>&1; then + wget -qO- --timeout="$CONNECT_TIMEOUT" --no-check-certificate --user-agent="Clash/OpenWRT" "$url" + return $? + fi + return 127 +} + +# Route core downloads through the running core in kernel-only mode (shared +# logic in proxy_lib.sh: handles smart core + sing-box's own profile port). +if [ "${CLASHOO_CORE_DOWNLOAD_LIB_ONLY:-0}" = "1" ]; then + detect_proxy() { :; } +else + . /usr/share/clashoo/update/proxy_lib.sh + detect_proxy() { clashoo_detect_proxy; } +fi + +download_file_try() { + url="$1" + out="$2" + proxy="$3" + if command -v curl >/dev/null 2>&1; then + if [ -n "$proxy" ]; then + curl -fsSL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$DOWNLOAD_TIMEOUT" --speed-time "$LOW_SPEED_TIME" --speed-limit "$LOW_SPEED_LIMIT" --retry 0 -A "Clash/OpenWRT" --proxy "$proxy" "$url" -o "$out" + else + curl -fsSL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$DOWNLOAD_TIMEOUT" --speed-time "$LOW_SPEED_TIME" --speed-limit "$LOW_SPEED_LIMIT" --retry 0 -A "Clash/OpenWRT" "$url" -o "$out" + fi + return $? + fi + if command -v wget >/dev/null 2>&1; then + wget -q --timeout="$REQUEST_TIMEOUT" --tries=1 --no-check-certificate --user-agent="Clash/OpenWRT" "$url" -O "$out" + return $? + fi + return 127 +} + +head_url_try() { + url="$1" + if command -v curl >/dev/null 2>&1; then + curl -fsSIL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$REQUEST_TIMEOUT" -A "Clash/OpenWRT" "$url" >/dev/null 2>&1 + return $? + fi + wget -q --spider --timeout="$CONNECT_TIMEOUT" --no-check-certificate --user-agent="Clash/OpenWRT" "$url" +} + +map_mihomo_arch() { + # 上游 amd64 默认 v3 微架构(要 AVX2)跑不起来;default 给 compatible 兜底 + case "$1" in + amd64-compatible) echo "linux-amd64-compatible" ;; + amd64-v1) echo "linux-amd64-v1" ;; + amd64-v2) echo "linux-amd64-v2" ;; + amd64-v3) echo "linux-amd64-v3" ;; + amd64|x86_64) echo "linux-amd64-compatible" ;; + arm64|aarch64*) echo "linux-arm64" ;; + armv7|arm_cortex-a[7-9]*|arm_cortex-a1[0-9]*) echo "linux-armv7" ;; + armv6|arm_cortex-a[56]*|arm_arm1176*) echo "linux-armv6" ;; + armv5|arm_cortex-a5*|arm_arm926*) echo "linux-armv5" ;; + 386|i[3-6]86|i386*) echo "linux-386" ;; + mipsle|mipsel_24kc) echo "linux-mipsle-softfloat" ;; + mips|mips_24kc) echo "linux-mips-softfloat" ;; + mips64le|mips64el_mips64r2) echo "linux-mips64le" ;; + mips64|mips64_mips64r2) echo "linux-mips64" ;; + riscv64) echo "linux-riscv64" ;; + *) echo "linux-amd64-compatible" ;; + esac +} + +map_singbox_arch() { + case "$1" in + amd64|amd64-compatible|amd64-v1|amd64-v2|amd64-v3) echo "amd64" ;; + arm64|aarch64*) echo "arm64" ;; + armv7|arm_cortex-a[7-9]*|arm_cortex-a1[0-9]*) echo "armv7" ;; + armv6|arm_cortex-a[56]*|arm_arm1176*) echo "armv6" ;; + armv5|arm_cortex-a5*|arm_arm926*) echo "armv5" ;; + 386|i[3-6]86|i386*) echo "386" ;; + mips|mips_24kc) echo "mips" ;; + mipsle|mipsel_24kc) echo "mipsle" ;; + mips64|mips64_mips64r2) echo "mips64" ;; + mips64le|mips64el_mips64r2) echo "mips64le" ;; + riscv64) echo "riscv64" ;; + *) echo "amd64" ;; + esac +} + +map_openwrt_arch() { + case "$1" in + amd64|amd64-compatible|amd64-v1|amd64-v2|amd64-v3|x86_64) echo "x86_64" ;; + arm64) echo "aarch64_generic" ;; + armv7) echo "arm_cortex-a7_neon-vfpv4" ;; + armv6) echo "arm_arm1176jzf-s_vfp" ;; + armv5) echo "arm_arm926ej-s" ;; + 386) echo "i386_pentium4" ;; + mipsle) echo "mipsel_24kc" ;; + mips) echo "mips_24kc" ;; + mips64le) echo "mips64el_mips64r2" ;; + mips64) echo "mips64_mips64r2" ;; + *) echo "" ;; + esac +} + +# Auto-detect normalized arch (MODELTYPE format) from the running device. +# Tries opkg → DISTRIB_ARCH → apk --print-arch → uname -m. +# Normalizes to MODELTYPE format (same vocabulary as luci.clashoo and system.js). +_auto_detect_modeltype() { + local raw norm + if command -v opkg >/dev/null 2>&1; then + raw="$(opkg print-architecture 2>/dev/null | awk '$1=="arch" && $2!="all" && $2!="noarch" {a=$2} END{print a}')" + fi + if [ -z "$raw" ] && [ -r /etc/openwrt_release ]; then + raw="$(sed -n "s/^DISTRIB_ARCH=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)" + fi + if [ -z "$raw" ] && command -v apk >/dev/null 2>&1; then + raw="$(apk --print-arch 2>/dev/null)" + fi + [ -z "$raw" ] && raw="$(uname -m 2>/dev/null)" + [ -z "$raw" ] && return 1 + case "$raw" in + x86_64|amd64) norm="amd64-compatible" ;; + aarch64*|arm64) norm="arm64" ;; + armv7*|arm_cortex-a[7-9]*|arm_cortex-a1[0-9]*) norm="armv7" ;; + armv6*|arm_cortex-a[56]*|arm_arm1176*) norm="armv6" ;; + armv5*|arm_cortex-a5*|arm_arm926*) norm="armv5" ;; + arm*) norm="armv5" ;; + i[3-6]86|i386*) norm="386" ;; + mips64el*) norm="mips64le" ;; + mips64*) norm="mips64" ;; + mipsel*) norm="mipsle" ;; + mips*) norm="mips" ;; + riscv64*) norm="riscv64" ;; + esac + [ -z "$norm" ] && return 1 + echo "$norm" + return 0 +} + +_is_amd64_arch() { + case "$1" in + amd64-compatible|amd64-v1|amd64-v2|amd64-v3) return 0 ;; + *) return 1 ;; + esac +} + +append_unique_line() { + list="$1" + line="$2" + [ -z "$line" ] && { + printf '%s' "$list" + return + } + case " +$list +" in + *" +$line +"*) + printf '%s' "$list" + ;; + *) + if [ -n "$list" ]; then + printf '%s\n%s' "$list" "$line" + else + printf '%s' "$line" + fi + ;; + esac +} + +pick_highest_stable_tag() { + printf '%s\n' "$1" | sed '/^$/d;s/\r$//' | grep -E '^v[0-9]+([.][0-9]+){1,3}$' | sort -V | tail -n 1 +} + +pick_highest_prerelease_tag() { + printf '%s\n' "$1" | sed '/^$/d;s/\r$//' | grep -E '^v[0-9]+([.][0-9]+){1,3}-[0-9A-Za-z.-]+$' | sort -V | tail -n 1 +} + +extract_release_tags_by_prerelease() { + payload="$1" + is_prerelease="$2" + + if command -v jsonfilter >/dev/null 2>&1; then + printf '%s' "$payload" | jsonfilter -e "@[@.prerelease=${is_prerelease}].tag_name" 2>/dev/null + return + fi + + tags="$(printf '%s' "$payload" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" + flags="$(printf '%s' "$payload" | grep -o '"prerelease"[[:space:]]*:[[:space:]]*[a-z]*' | sed 's/.*:[[:space:]]*//')" + [ -z "$tags" ] && return + [ -z "$flags" ] && return + + tags_file="/tmp/clashoo-tags-$$" + flags_file="/tmp/clashoo-pre-$$" + printf '%s\n' "$tags" > "$tags_file" + printf '%s\n' "$flags" > "$flags_file" + awk -v want="$is_prerelease" 'NR==FNR { t[FNR]=$0; next } $0==want { print t[FNR] }' "$tags_file" "$flags_file" + rm -f "$tags_file" "$flags_file" >/dev/null 2>&1 +} + +detect_openwrt_arch() { + local arch + if command -v opkg >/dev/null 2>&1; then + arch="$(opkg print-architecture 2>/dev/null | awk '$1=="arch" && $2!="all" && $2!="noarch" {a=$2} END{print a}')" + [ -n "$arch" ] && { echo "$arch"; return 0; } + fi + # DISTRIB_ARCH is authoritative for OpenWrt package arch naming. + # apk --print-arch may return "aarch64" while sing-box release assets + # use the full sub-arch (e.g. aarch64_generic / aarch64_cortex-a53). + if [ -r /etc/openwrt_release ]; then + arch="$(sed -n "s/^DISTRIB_ARCH=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)" + [ -n "$arch" ] && { echo "$arch"; return 0; } + fi + if command -v apk >/dev/null 2>&1; then + arch="$(apk --print-arch 2>/dev/null)" + [ -n "$arch" ] && { echo "$arch"; return 0; } + fi + map_openwrt_arch "$MODELTYPE" +} + +singbox_target_path() { + case "$CORETYPE" in + 5) printf '%s\n' "$SINGBOX_ALPHA_TARGET" ;; + *) printf '%s\n' "$SINGBOX_STABLE_TARGET" ;; + esac +} + +extract_tar_file() { + local archive dest + archive="$1" + dest="$2" + case "$archive" in + *.gz) tar -xzf "$archive" -C "$dest" >/dev/null 2>&1 ;; + *.xz) tar -xJf "$archive" -C "$dest" >/dev/null 2>&1 || tar -xf "$archive" -C "$dest" >/dev/null 2>&1 ;; + *.zst) + if command -v unzstd >/dev/null 2>&1; then + unzstd -c "$archive" 2>/dev/null | tar -xf - -C "$dest" >/dev/null 2>&1 + elif command -v zstd >/dev/null 2>&1; then + zstd -dc "$archive" 2>/dev/null | tar -xf - -C "$dest" >/dev/null 2>&1 + else + return 1 + fi + ;; + *) tar -xf "$archive" -C "$dest" >/dev/null 2>&1 || tar -xzf "$archive" -C "$dest" >/dev/null 2>&1 ;; + esac +} + +extract_ar_data_tar() { + local archive dest offset name size member + archive="$1" + dest="$2" + offset=8 + [ "$(dd if="$archive" bs=8 count=1 2>/dev/null)" = '!' ] || return 1 + while :; do + # Read fixed-width ar fields directly. Do not put the full 60-byte + # header in command substitution: shell command substitution strips + # trailing newlines/spaces and makes length checks unreliable in ash. + name="$(dd if="$archive" bs=1 skip="$offset" count=16 2>/dev/null | sed 's/[[:space:]].*$//')" + name="${name%/}" + size="$(dd if="$archive" bs=1 skip=$((offset + 48)) count=10 2>/dev/null | tr -d '[:space:]')" + case "$size" in + ''|*[!0-9]*) return 1 ;; + esac + member="$dest/$name" + case "$name" in + data.tar.*) + dd if="$archive" of="$member" bs=1 skip=$((offset + 60)) count="$size" 2>/dev/null || return 1 + printf '%s\n' "$member" + return 0 + ;; + esac + offset=$((offset + 60 + size)) + [ $((offset % 2)) -eq 0 ] || offset=$((offset + 1)) + done +} + +extract_singbox_payload() { + local pkg rootdir outer workdir payload data_tar candidate + pkg="$1" + rootdir="$2" + outer="${rootdir}.outer" + workdir="$(dirname "$rootdir")" + payload="$workdir/sing-box.payload" + data_tar="" + rm -f "$payload" 2>/dev/null + rm -rf "$rootdir" "$outer" 2>/dev/null + if ! mkdir -p "$rootdir" "$outer"; then + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + fi + + # IPK assets are ar/tar containers with a compressed data.tar.* member; + # official fallback archives contain sing-box directly below a version dir. + if tar -tf "$pkg" 2>/dev/null | grep -Eq '(^|/)data\.tar\.(gz|xz|zst)$'; then + extract_tar_file "$pkg" "$outer" || { + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + } + for candidate in "$outer"/data.tar.*; do + [ -f "$candidate" ] || continue + data_tar="$candidate" + break + done + if [ -z "$data_tar" ] || ! extract_tar_file "$data_tar" "$rootdir"; then + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + fi + else + data_tar="$(extract_ar_data_tar "$pkg" "$outer" 2>/dev/null || true)" + if [ -n "$data_tar" ]; then + if ! extract_tar_file "$data_tar" "$rootdir"; then + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + fi + else + if ! extract_tar_file "$pkg" "$rootdir"; then + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + fi + fi + fi + + candidate="$(find "$rootdir" -type f -name sing-box 2>/dev/null | head -n 1)" + if [ -n "$candidate" ] && [ -f "$candidate" ]; then + if ! cp -f "$candidate" "$payload"; then + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 + fi + rm -rf "$rootdir" "$outer" 2>/dev/null + printf '%s\n' "$payload" + return 0 + fi + rm -rf "$rootdir" "$outer" 2>/dev/null + return 1 +} + +cleanup_singbox_extract_workdir() { + local workdir rootdir + workdir="$1" + rootdir="$workdir/root" + rm -rf "$rootdir" "${rootdir}.outer" "$workdir" 2>/dev/null +} + +install_singbox_openwrt_asset() { + local pkg target workdir rootdir bin extract_rc rc + pkg="$1" + target="${2:-$(singbox_target_path)}" + workdir="$(mktemp -d "$SINGBOX_TMP_TEMPLATE" 2>/dev/null)" || return 1 + rootdir="$workdir/root" + bin="$(extract_singbox_payload "$pkg" "$rootdir")" + extract_rc=$? + if [ "$extract_rc" -eq 0 ]; then + install_with_rollback "$bin" "$target" "${VER:-}" + rc=$? + else + rc=1 + fi + cleanup_singbox_extract_workdir "$workdir" + return "$rc" +} + +install_singbox_tar_asset() { + local archive target workdir rootdir bin extract_rc rc + archive="$1" + target="${2:-$(singbox_target_path)}" + workdir="$(mktemp -d "$SINGBOX_TMP_TEMPLATE" 2>/dev/null)" || return 1 + rootdir="$workdir/root" + bin="$(extract_singbox_payload "$archive" "$rootdir")" + extract_rc=$? + if [ "$extract_rc" -eq 0 ]; then + install_with_rollback "$bin" "$target" "${VER:-}" + rc=$? + else + rc=1 + fi + cleanup_singbox_extract_workdir "$workdir" + return "$rc" +} + +fetch_latest_tag() { + repo="$1" + api_url="https://api.github.com/repos/${repo}/releases/latest" + list_api="https://api.github.com/repos/${repo}/releases?per_page=100" + web_url="https://github.com/${repo}/releases/latest" + candidates="" + + # Fast path: prefer official GitHub API first to avoid stale mirror cache. + p="" + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$api_url")" + i=1 + while [ "$i" -le "$TAG_FETCH_RETRIES" ]; do + ensure_not_timed_out || return 1 + tag="$(fetch_url_try "$u" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)" + case "$tag" in + v*[-]*) + : + ;; + *) + [ -n "$tag" ] && echo "$tag" && return 0 + ;; + esac + sleep 1 + i=$((i + 1)) + done + + for p in $(mirror_prefixes); do + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$api_url")" + i=1 + while [ "$i" -le "$TAG_FETCH_RETRIES" ]; do + ensure_not_timed_out || return 1 + tag="$(fetch_url_try "$u" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)" + case "$tag" in + v*[-]*) + # stable channel should ignore prerelease-like tags from stale mirrors + : + ;; + *) + candidates="$(append_unique_line "$candidates" "$tag")" + ;; + esac + sleep 1 + i=$((i + 1)) + done + done + + p="" + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$list_api")" + i=1 + while [ "$i" -le "$TAG_FETCH_RETRIES" ]; do + ensure_not_timed_out || return 1 + payload="$({ fetch_url_try "$u" || true; })" + tags="$(extract_release_tags_by_prerelease "$payload" false)" + best="$(pick_highest_stable_tag "$tags")" + [ -n "$best" ] && echo "$best" && return 0 + sleep 1 + i=$((i + 1)) + done + + for p in $(mirror_prefixes); do + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$list_api")" + i=1 + while [ "$i" -le "$TAG_FETCH_RETRIES" ]; do + ensure_not_timed_out || return 1 + payload="$({ fetch_url_try "$u" || true; })" + tags="$(extract_release_tags_by_prerelease "$payload" false)" + for tag in $tags; do + candidates="$(append_unique_line "$candidates" "$tag")" + done + sleep 1 + i=$((i + 1)) + done + done + + for p in $(mirror_prefixes); do + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$web_url")" + i=1 + while [ "$i" -le "$TAG_FETCH_RETRIES" ]; do + ensure_not_timed_out || return 1 + if command -v curl >/dev/null 2>&1; then + tag="$(curl -fsSIL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$REQUEST_TIMEOUT" -A "Clash/OpenWRT" "$u" 2>/dev/null | sed -n 's#^[Ll]ocation: .*/releases/tag/\([^[:space:]\r]*\).*#\1#p' | head -n 1)" + else + tag="$(wget -S --spider --timeout="$CONNECT_TIMEOUT" --no-check-certificate --user-agent="Clash/OpenWRT" "$u" 2>&1 | sed -n 's#^ Location: .*/releases/tag/\([^[:space:]]*\).*#\1#p' | head -n 1)" + fi + case "$tag" in + v*[-]*) + : + ;; + *) + candidates="$(append_unique_line "$candidates" "$tag")" + ;; + esac + sleep 1 + i=$((i + 1)) + done + done + + releases_url="https://github.com/${repo}/releases" + for p in $(mirror_prefixes); do + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$releases_url")" + tag="$(fetch_url_try "$u" | sed -n 's#.*releases/tag/\(v[0-9][^\"/?]*\).*#\1#p' | head -n 1)" + case "$tag" in + v*[-]*) + : + ;; + *) + candidates="$(append_unique_line "$candidates" "$tag")" + ;; + esac + done + + # 终极兜底:直连 GitHub 爬取 releases 页面(镜像都失效时备用) + ensure_not_timed_out || return 1 + p="" + u="$(prefixed_url "$p" "$releases_url")" + tag="$(fetch_url_try "$u" 2>/dev/null | sed -n 's#.*releases/tag/\(v[0-9][^"/?]*\).*#\1#p' | head -n 1)" + case "$tag" in + v*[-]*) + ;; + *) + [ -n "$tag" ] && { echo "$tag"; return 0; } + ;; + esac + # 再试一次 releases/latest 的 302 跳转 + u="$(prefixed_url "$p" "$web_url")" + if command -v curl >/dev/null 2>&1; then + tag="$(curl -fsSIL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$REQUEST_TIMEOUT" -A "Clash/OpenWRT" "$u" 2>/dev/null | sed -n 's#^[Ll]ocation: .*/releases/tag/\([^[:space:][:cntrl:]]*\).*#\1#p' | head -n 1)" + else + tag="$(wget -S --spider --timeout="$CONNECT_TIMEOUT" --no-check-certificate --user-agent="Clash/OpenWRT" "$u" 2>&1 | sed -n 's#^ Location: .*/releases/tag/\([^[:space:]]*\).*#\1#p' | head -n 1)" + fi + [ -n "$tag" ] && { echo "$tag"; return 0; } + +best="$(pick_highest_stable_tag "$candidates")" + [ -n "$best" ] && { + echo "$best" + return 0 + } + fallback="$(printf '%s\n' "$candidates" | sed '/^$/d' | head -n 1)" + [ -n "$fallback" ] && { + echo "$fallback" + return 0 + } + + return 1 +} + +fetch_prerelease_tag() { + repo="$1" + api_url="https://api.github.com/repos/${repo}/releases?per_page=100" + candidates="" + + # Fast path: official GitHub API first. + p="" + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$api_url")" + i=1 + while [ "$i" -le "$TAG_FETCH_RETRIES" ]; do + ensure_not_timed_out || return 1 + payload="$({ fetch_url_try "$u" || true; })" + tags="$(extract_release_tags_by_prerelease "$payload" true)" + best="$(pick_highest_prerelease_tag "$tags")" + [ -n "$best" ] && echo "$best" && return 0 + sleep 1 + i=$((i + 1)) + done + + for p in $(mirror_prefixes); do + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$api_url")" + i=1 + while [ "$i" -le "$TAG_FETCH_RETRIES" ]; do + ensure_not_timed_out || return 1 + payload="$({ fetch_url_try "$u" || true; })" + tags="$(extract_release_tags_by_prerelease "$payload" true)" + for tag in $tags; do + candidates="$(append_unique_line "$candidates" "$tag")" + done + sleep 1 + i=$((i + 1)) + done + done + + best="$(pick_highest_prerelease_tag "$candidates")" + [ -n "$best" ] && { + echo "$best" + return 0 + } + fallback="$(printf '%s\n' "$candidates" | sed '/^$/d' | head -n 1)" + [ -n "$fallback" ] && { + echo "$fallback" + return 0 + } + + return 1 +} + +fetch_expanded_asset_names() { + repo="$1" + tag="$2" + url="https://github.com/${repo}/releases/expanded_assets/${tag}" + + for p in $(mirror_prefixes) ""; do + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$url")" + assets="$(fetch_url_try "$u" | sed -n "s#.*releases/download/${tag}/\\([^\\\"?]*\\)\\\".*#\\1#p")" + [ -n "$assets" ] && printf '%s\n' "$assets" && return 0 + done + + return 1 +} + +pick_reachable_release_url() { + base_url="$1" + for p in $(mirror_prefixes) ""; do + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$base_url")" + if head_url_try "$u"; then + echo "$u" + return 0 + fi + done + return 1 +} + +pick_mihomo_asset_by_url() { + repo="$1" + tag="$2" + arch="$3" + channel="$4" + ver="${tag#v}" + candidates="" + + if [ "$channel" = "alpha" ] || [ "$channel" = "smart" ]; then + if [ "$arch" = "linux-amd64" ]; then + if [ "$channel" = "smart" ]; then + candidates="mihomo-linux-amd64-alpha-smart-*.gz mihomo-linux-amd64-compatible-alpha-smart-*.gz" + else + candidates="mihomo-linux-amd64-alpha-*.gz mihomo-linux-amd64-compatible-alpha-*.gz" + fi + else + [ "$channel" = "smart" ] && candidates="mihomo-${arch}-alpha-smart-*.gz" || candidates="mihomo-${arch}-alpha-*.gz" + fi + assets_html="$(fetch_expanded_asset_names "$repo" "$tag")" + if [ -n "$assets_html" ]; then + for a in $(printf '%s\n' "$assets_html"); do + for pat in $candidates; do + case "$a" in + $pat) + u="https://github.com/${repo}/releases/download/${tag}/${a}" + if pick_reachable_release_url "$u" >/dev/null 2>&1; then + echo "$a" + return 0 + fi + ;; + esac + done + done + fi + return 1 + fi + + if [ "$arch" = "linux-amd64" ]; then + candidates="mihomo-linux-amd64-v${ver}.gz mihomo-linux-amd64-compatible-v${ver}.gz" + else + candidates="mihomo-${arch}-v${ver}.gz" + fi + + for a in $candidates; do + u="https://github.com/${repo}/releases/download/${tag}/${a}" + if pick_reachable_release_url "$u" >/dev/null 2>&1; then + echo "$a" + return 0 + fi + done + + return 1 +} + +download_with_mirrors() { + base_url="$1" + outfile="$2" + verify_mode="${3:-gzip}" + proxy="$(detect_proxy)" + if [ -n "$proxy" ]; then + download_prefixes="__direct__ $(mirror_prefixes)" + else + download_prefixes="$(mirror_prefixes) __direct__" + fi + for p in $download_prefixes; do + [ "$p" = "__direct__" ] && p="" + ensure_not_timed_out || return 1 + u="$(prefixed_url "$p" "$base_url")" + i=1 + while [ "$i" -le "$ATTEMPTS_PER_MIRROR" ]; do + ensure_not_timed_out || return 1 + write_log "Downloading from ${u}" + rm -f "$outfile" 2>/dev/null + if download_file_try "$u" "$outfile" "$proxy"; then + case "$verify_mode" in + raw) + if [ -s "$outfile" ]; then + return 0 + fi + write_log "Downloaded file is empty from ${u}" + ;; + *) + if gzip -t "$outfile" >/dev/null 2>&1; then + return 0 + fi + write_log "Downloaded file is invalid gzip from ${u}" + ;; + esac + fi + sleep 1 + i=$((i + 1)) + done + done + return 1 +} + +pick_mihomo_asset() { + repo="$1" + tag="$2" + arch="$3" + channel="$4" + ver="${tag#v}" + candidates="" + release_assets="$(fetch_expanded_asset_names "$repo" "$tag")" + [ -z "$release_assets" ] && pick_mihomo_asset_by_url "$repo" "$tag" "$arch" "$channel" && return 0 + [ -z "$release_assets" ] && return 1 + + if [ "$channel" = "alpha" ] || [ "$channel" = "smart" ]; then + if [ "$arch" = "linux-amd64" ]; then + if [ "$channel" = "smart" ]; then + asset="$(printf '%s\n' "$release_assets" | sed -n 's#^\(mihomo-linux-amd64-alpha-smart-[0-9a-fA-F]\{7,\}\.gz\)$#\1#p' | head -n 1)" + [ -z "$asset" ] && asset="$(printf '%s\n' "$release_assets" | sed -n 's#^\(mihomo-linux-amd64-compatible-alpha-smart-[0-9a-fA-F]\{7,\}\.gz\)$#\1#p' | head -n 1)" + else + asset="$(printf '%s\n' "$release_assets" | sed -n 's#^\(mihomo-linux-amd64-alpha-[0-9a-fA-F]\{7,\}\.gz\)$#\1#p' | head -n 1)" + [ -z "$asset" ] && asset="$(printf '%s\n' "$release_assets" | sed -n 's#^\(mihomo-linux-amd64-compatible-alpha-[0-9a-fA-F]\{7,\}\.gz\)$#\1#p' | head -n 1)" + fi + else + if [ "$channel" = "smart" ]; then + asset="$(printf '%s\n' "$release_assets" | sed -n "s#^\\(mihomo-${arch}-alpha-smart-[0-9a-fA-F]\\{7,\\}\\.gz\\)$#\\1#p" | head -n 1)" + else + asset="$(printf '%s\n' "$release_assets" | sed -n "s#^\\(mihomo-${arch}-alpha-[0-9a-fA-F]\\{7,\\}\\.gz\\)$#\\1#p" | head -n 1)" + fi + fi + [ -n "$asset" ] && echo "$asset" && return 0 + pick_mihomo_asset_by_url "$repo" "$tag" "$arch" "$channel" + return $? + fi + + if [ "$arch" = "linux-amd64" ]; then + candidates="mihomo-linux-amd64-v${ver}.gz mihomo-linux-amd64-compatible-v${ver}.gz" + else + candidates="mihomo-${arch}-v${ver}.gz" + fi + + for a in $candidates; do + if printf '%s\n' "$release_assets" | grep -qx "$a"; then + echo "$a" + return 0 + fi + done + + pick_mihomo_asset_by_url "$repo" "$tag" "$arch" "$channel" + return $? + +} + +install_binary() { + local src dst dir staged + src="$1" + dst="$2" + dir="$(dirname "$dst")" + staged="$dir/.$(basename "$dst").tmp.$$" + mkdir -p "$dir" || return 1 + rm -f "$staged" + cp -f "$src" "$staged" || { + rm -f "$staged" + return 1 + } + chmod 755 "$staged" || { + rm -f "$staged" + return 1 + } + mv -f "$staged" "$dst" || { + rm -f "$staged" + return 1 + } +} + +backup_binary() { + dst="$1" + bak="${dst}.bak" + if [ -x "$dst" ]; then + cp -f "$dst" "$bak" 2>/dev/null || true + [ -f "$bak" ] || { write_log "备份旧内核失败(磁盘满?),跳过备份"; } + fi +} + +restore_binary() { + dst="$1" + bak="${dst}.bak" + if [ -f "$bak" ]; then + cp -f "$bak" "$dst" 2>/dev/null || return 1 + chmod 755 "$dst" 2>/dev/null || true + return 0 + fi + return 1 +} + +verify_binary() { + local bin + bin="$1" + [ -x "$bin" ] || return 1 + "$bin" -v >/dev/null 2>&1 && return 0 + "$bin" version >/dev/null 2>&1 && return 0 + return 1 +} + +verify_binary_version() { + local bin expected output actual + bin="$1" + expected="$2" + [ -n "$expected" ] || return 0 + output="$({ "$bin" version 2>/dev/null || "$bin" -v 2>/dev/null; } || true)" + [ -n "$output" ] || return 1 + actual="$(printf '%s\n' "$output" | sed -n 's/^[[:space:]]*sing-box[[:space:]][[:space:]]*version[[:space:]][[:space:]]*\([^[:space:]][^[:space:]]*\).*/\1/p' | head -n 1)" + [ -n "$actual" ] || return 1 + [ "$actual" = "$expected" ] +} + +installed_version_matches() { + version_file="$1" + expected_version="$2" + [ -r "$version_file" ] || return 1 + [ -n "$expected_version" ] || return 1 + [ "$(sed -n '1p' "$version_file" 2>/dev/null)" = "$expected_version" ] +} + +install_with_rollback() { + local tmpfile target expected_version + tmpfile="$1" + target="$2" + expected_version="${3:-}" + + chmod 755 "$tmpfile" 2>/dev/null + if ! verify_binary "$tmpfile" || ! verify_binary_version "$tmpfile" "$expected_version"; then + write_log "新内核预热校验失败,保留当前内核" + rm -f "$tmpfile" 2>/dev/null + return 1 + fi + + backup_binary "$target" + if ! install_binary "$tmpfile" "$target"; then + write_log "内核替换失败,回滚旧版本" + restore_binary "$target" >/dev/null 2>&1 || true + return 1 + fi + + rm -f "${target}.bak" 2>/dev/null + return 0 +} + +# Unit tests source the extraction helpers without entering the normal UCI and +# network download path. This hook has no effect in production execution. +if [ "${CLASHOO_CORE_DOWNLOAD_LIB_ONLY:-0}" = "1" ]; then + return 0 2>/dev/null || exit 0 +fi + +if ! acquire_core_download_lock; then + write_log "已有内核下载任务运行,拒绝并发执行" + exit 1 +fi +trap finalize EXIT + +# UCI template defaults to amd64-compatible. Auto-detect when MODELTYPE +# is missing, or when MODELTYPE is an amd64 variant but the device is not. +if [ -z "$MODELTYPE" ]; then + DETECTED="$(_auto_detect_modeltype)" + if [ -n "$DETECTED" ] && [ "$DETECTED" != "$MODELTYPE" ]; then + write_log "自动识别处理器架构:${DETECTED}" + MODELTYPE="$DETECTED" + uci set clashoo.config.download_core="$DETECTED" 2>/dev/null + uci commit clashoo 2>/dev/null + fi +elif _is_amd64_arch "$MODELTYPE"; then + DETECTED="$(_auto_detect_modeltype)" + if [ -n "$DETECTED" ] && [ "$DETECTED" != "$MODELTYPE" ] && ! _is_amd64_arch "$DETECTED"; then + write_log "UCI 架构为 ${MODELTYPE},但设备检测为 ${DETECTED},自动修正" + MODELTYPE="$DETECTED" + uci set clashoo.config.download_core="$DETECTED" 2>/dev/null + uci commit clashoo 2>/dev/null + fi +fi + +rm -f /tmp/clash.gz /tmp/clash /usr/share/clashoo/core_down_complete 2>/dev/null +touch /var/run/core_update 2>/dev/null +write_log "内核下载任务启动" +write_log "下载架构:${MODELTYPE:-未设置}" + +if [ -n "$CUSTOM_CORE_URL" ]; then + write_log "使用自定义内核下载链接" + URL="$CUSTOM_CORE_URL" + if [ "$CORETYPE" = "3" ]; then + TARGET="/usr/bin/mihomo" + VERSION_FILE="/usr/share/clashoo/mihomo_version" + else + TARGET="/usr/bin/mihomo-stable" + VERSION_FILE="/usr/share/clashoo/clash_meta_version" + fi + TAG="custom" + ASSET="custom" + VERSION_VALUE="custom-url" + + if ! download_file_try "$URL" /tmp/clash.gz "$(detect_proxy)"; then + write_log "自定义链接下载失败" + exit 1 + fi + + if ! gunzip -f /tmp/clash.gz; then + write_log "自定义内核解压失败" + exit 1 + fi + + if ! install_with_rollback /tmp/clash "$TARGET"; then + exit 1 + fi + CORE_INSTALLED=1 + printf '%s\n' "${VERSION_VALUE}" > "$VERSION_FILE" + touch /usr/share/clashoo/core_down_complete + write_log "内核更新完成(自定义链接)" + exit 0 +fi + +if [ "$CORETYPE" = "4" ] || [ "$CORETYPE" = "5" ]; then + SINGBOX_ARCH="$(map_singbox_arch "$MODELTYPE")" + OPENWRT_ARCH="$(detect_openwrt_arch)" + if [ "$CORETYPE" = "4" ]; then + write_log "已选择 sing-box 稳定版" + write_log "正在获取 GitHub Release 信息..." + TAG=$(fetch_latest_tag "SagerNet/sing-box") + [ -z "$TAG" ] && write_log "获取 sing-box 稳定版版本号失败" && exit 1 + else + write_log "已选择 sing-box 预发布版" + write_log "正在获取 GitHub Release 信息..." + TAG=$(fetch_prerelease_tag "SagerNet/sing-box") + [ -z "$TAG" ] && TAG=$(fetch_latest_tag "SagerNet/sing-box") + [ -z "$TAG" ] && write_log "获取 sing-box 预发布版版本号失败" && exit 1 + fi + VER="${TAG#v}" + SINGBOX_TARGET="$(singbox_target_path)" + installed=0 + + if [ -n "$OPENWRT_ARCH" ]; then + ASSET="sing-box_${VER}_openwrt_${OPENWRT_ARCH}.ipk" + URL="https://github.com/SagerNet/sing-box/releases/download/${TAG}/${ASSET}" + write_log "尝试提取 OpenWrt IPK 包: ${ASSET}" + if download_with_mirrors "$URL" /tmp/singbox-openwrt.ipk raw && install_singbox_openwrt_asset /tmp/singbox-openwrt.ipk "$SINGBOX_TARGET"; then + installed=1 + else + write_log "OpenWrt IPK 提取失败,尝试官方 musl tar 包" + fi + fi + + if [ "$installed" -ne 1 ]; then + for ASSET in "sing-box-${VER}-linux-${SINGBOX_ARCH}-musl.tar.gz" "sing-box-${VER}-linux-${SINGBOX_ARCH}.tar.gz"; do + URL="https://github.com/SagerNet/sing-box/releases/download/${TAG}/${ASSET}" + write_log "尝试下载 sing-box 归档包: ${ASSET}" + if download_with_mirrors "$URL" /tmp/singbox.tar.gz gzip && install_singbox_tar_asset /tmp/singbox.tar.gz "$SINGBOX_TARGET"; then + installed=1 + break + fi + done + fi + + if [ "$installed" -ne 1 ]; then + write_log "sing-box 下载或安装失败(可能是架构不匹配)" + exit 1 + fi + + CORE_INSTALLED=1 + # 稳定版与 Alpha 分路径保存,避免互相覆盖;版本文件保持兼容。 + if [ "$CORETYPE" = "4" ]; then + printf '%s\n' "$TAG" > "/usr/share/clashoo/singbox_stable_version" + else + printf '%s\n' "$TAG" > "/usr/share/clashoo/singbox_alpha_version" + fi + printf '%s\n' "$TAG" > "/usr/share/clashoo/singbox_version" + rm -f /tmp/singbox.tar.gz /tmp/singbox-openwrt.ipk + rm -rf /tmp/singbox-extract /tmp/singbox-openwrt-pkg /tmp/singbox-openwrt-root + touch /usr/share/clashoo/core_down_complete + write_log "sing-box 更新完成: $TAG" + exit 0 +fi + +if [ "$CORETYPE" = "1" ]; then + write_log "已选择内核通道:mihomo Smart 版(vernesong fork)" + TAG="Prerelease-Alpha" + write_log "正在获取 GitHub Release 信息..." + ASSET=$(pick_mihomo_asset "vernesong/mihomo" "$TAG" "$(map_mihomo_arch "$MODELTYPE")" "smart") + if [ -z "$ASSET" ]; then + ALT_TAG=$(fetch_prerelease_tag "vernesong/mihomo") + [ -n "$ALT_TAG" ] && TAG="$ALT_TAG" + ASSET=$(pick_mihomo_asset "vernesong/mihomo" "$TAG" "$(map_mihomo_arch "$MODELTYPE")" "smart") + fi + URL="https://github.com/vernesong/mihomo/releases/download/${TAG}/${ASSET}" + TARGET="/usr/bin/smart" + VERSION_FILE="/usr/share/clashoo/mihomo_version" + VERSION_VALUE="smart-$(printf '%s\n' "$ASSET" | sed -n 's#^mihomo-.*-alpha-smart-\([0-9a-fA-F]\{7,\}\)\.gz$#\1#p' | head -n 1)" + [ "$VERSION_VALUE" = "smart-" ] && VERSION_VALUE="smart-${TAG}" +elif [ "$CORETYPE" = "2" ]; then + write_log "已选择内核通道:稳定版" + write_log "正在获取 GitHub Release 信息..." + TAG=$(fetch_latest_tag "MetaCubeX/mihomo") + [ -z "$TAG" ] && write_log "获取稳定版版本号失败" && exit 1 + ASSET=$(pick_mihomo_asset "MetaCubeX/mihomo" "$TAG" "$(map_mihomo_arch "$MODELTYPE")" "stable") + URL="https://github.com/MetaCubeX/mihomo/releases/download/${TAG}/${ASSET}" + TARGET="/usr/bin/mihomo-stable" + VERSION_FILE="/usr/share/clashoo/clash_meta_version" + VERSION_VALUE="$TAG" +else + write_log "已选择内核通道:预发布版" + TAG="Prerelease-Alpha" + write_log "正在获取 GitHub Release 信息..." + ASSET=$(pick_mihomo_asset "MetaCubeX/mihomo" "$TAG" "$(map_mihomo_arch "$MODELTYPE")" "alpha") + [ -z "$ASSET" ] && TAG=$(fetch_prerelease_tag "MetaCubeX/mihomo") + [ -z "$TAG" ] && TAG=$(fetch_latest_tag "MetaCubeX/mihomo") + [ -z "$TAG" ] && write_log "获取预发布版本号失败" && exit 1 + [ -z "$ASSET" ] && ASSET=$(pick_mihomo_asset "MetaCubeX/mihomo" "$TAG" "$(map_mihomo_arch "$MODELTYPE")" "alpha") + [ -z "$ASSET" ] && ASSET=$(pick_mihomo_asset "MetaCubeX/mihomo" "$TAG" "$(map_mihomo_arch "$MODELTYPE")" "stable") + URL="https://github.com/MetaCubeX/mihomo/releases/download/${TAG}/${ASSET}" + TARGET="/usr/bin/mihomo" + VERSION_FILE="/usr/share/clashoo/mihomo_version" + VERSION_VALUE="$(printf '%s\n' "$ASSET" | sed -n 's#^mihomo-.*-alpha-\([0-9a-fA-F]\{7,\}\)\.gz$#\1#p' | head -n 1)" + [ -z "$VERSION_VALUE" ] && VERSION_VALUE="$TAG" +fi + +if [ -z "$TAG" ]; then + write_log "内核版本检查失败" + exit 1 +fi + +if [ -z "$ASSET" ]; then + write_log "未找到对应架构的内核文件(${MODELTYPE:-unknown} / ${TAG:-unknown})" + exit 1 +fi +write_log "版本标签:$TAG" +write_log "匹配内核文件:$ASSET" + +if [ -n "$TARGET_DCORE" ] && installed_version_matches "$VERSION_FILE" "$VERSION_VALUE" && verify_binary "$TARGET"; then + write_log "内核已是最新版本:$VERSION_VALUE" + exit 0 +fi + +write_log "开始下载内核" +if ! download_with_mirrors "$URL" /tmp/clash.gz; then + write_log "内核下载失败:所有镜像源均不可达(已尝试 gh-proxy.com / ghfast.top / 裸 GitHub)" + write_log "可设置自定义镜像前缀:uci set clashoo.config.core_mirror_prefix='https://your.mirror/' && uci commit clashoo" + exit 1 +fi + +if ! gunzip -f /tmp/clash.gz; then + write_log "内核解压失败" + exit 1 +fi + +if ! install_with_rollback /tmp/clash "$TARGET"; then + exit 1 +fi +CORE_INSTALLED=1 +printf '%s\n' "${VERSION_VALUE:-$TAG}" > "$VERSION_FILE" +touch /usr/share/clashoo/core_down_complete +write_log "内核更新完成" diff --git a/clashoo/files/usr/share/clashoo/update/geoip.sh b/clashoo/files/usr/share/clashoo/update/geoip.sh new file mode 100755 index 000000000..a8bc66e6a --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/geoip.sh @@ -0,0 +1,267 @@ +#!/bin/sh + +LOG_FILE="/tmp/geoip_update.txt" +TMP_DIR="/tmp/clash_geoip_$$" +MMDB_MIN_SIZE=2000000 +GEOSITE_MIN_SIZE=2000000 + +geoip_source="$(uci -q get clashoo.config.geoip_source 2>/dev/null)" +license_key="$(uci -q get clashoo.config.license_key 2>/dev/null)" + +cfg_mmdb_url="$(uci -q get clashoo.config.geoip_mmdb_url 2>/dev/null)" +cfg_geosite_url="$(uci -q get clashoo.config.geosite_url 2>/dev/null)" +cfg_geoip_dat_url="$(uci -q get clashoo.config.geoip_dat_url 2>/dev/null)" +cfg_geoip_asn_url="$(uci -q get clashoo.config.geoip_asn_url 2>/dev/null)" + +DEFAULT_MMDB_URL="https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb" +DEFAULT_GEOSITE_URL="https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/release/geosite.dat" +DEFAULT_GEOIP_DAT_URL="https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/release/geoip.dat" +DEFAULT_GEOIP_ASN_URL="https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb" + +LOYALSOLDIER_MMDB_URL="https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb" +LOYALSOLDIER_GEOSITE_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat" +LOYALSOLDIER_GEOIP_DAT_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat" + +# v2fly ships no Country.mmdb — borrow Loyalsoldier's for the mmdb slot +V2FLY_GEOSITE_URL="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat" +V2FLY_GEOIP_DAT_URL="https://github.com/v2fly/geoip/releases/latest/download/geoip.dat" + +log() { + echo " $(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" +} + +cleanup() { + rm -rf "$TMP_DIR" >/dev/null 2>&1 + rm -f /var/run/geoip_update >/dev/null 2>&1 +} + +config_needs_geosite() { + _cfg="/etc/clashoo/config.yaml" + [ -f "$_cfg" ] || return 1 + grep -Eq "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,|^[[:space:]]*geosite:[[:space:]]*|^[[:space:]]*'geosite:|^[[:space:]]*-[[:space:]]*[\"']?geosite:" "$_cfg" +} + +# Shared port detection (handles smart core + sing-box profile port); keep the +# local anonymity probe so we skip a coexisting plugin's auth-locked proxy. +. /usr/share/clashoo/update/proxy_lib.sh +detect_proxy() { + proxy="$(clashoo_detect_proxy)" + [ -n "$proxy" ] || return 0 + # Only use it if it actually proxies anonymously. A coexisting plugin + # (e.g. OpenClash on the same 7890) may hold this port behind auth and + # answer HTTP 407 — in that case fall through to the mirrors instead. + if command -v curl >/dev/null 2>&1; then + curl -fsS --proxy "$proxy" --connect-timeout 3 --max-time 5 -o /dev/null \ + http://www.gstatic.com/generate_204 >/dev/null 2>&1 || return 0 + fi + echo "$proxy" +} + +_fetch() { + url="$1"; target="$2"; proxy="$3" + if command -v curl >/dev/null 2>&1; then + if [ -n "$proxy" ]; then + curl --proxy "$proxy" -fsSL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$url" -o "$target" + else + curl -fsSL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$url" -o "$target" + fi + return $? + fi + wget -q --timeout=120 --no-check-certificate --user-agent="Clash/OpenWRT" "$url" -O "$target" +} + +# 多源拉取:本机代理 → 国内镜像(gh-proxy / ghfast / jsdelivr)→ 原 URL 兜底 +download_to() { + url="$1"; target="$2" + [ -z "$url" ] && return 1 + + proxy="$(detect_proxy)" + + if [ -n "$proxy" ]; then + _fetch "$url" "$target" "$proxy" && return 0 + fi + + case "$url" in + https://raw.githubusercontent.com/*) + rest="${url#https://raw.githubusercontent.com/}" + owner="${rest%%/*}"; r1="${rest#*/}" + repo="${r1%%/*}"; r2="${r1#*/}" + branch="${r2%%/*}"; path="${r2#*/}" + for m in \ + "https://gh-proxy.com/${url}" \ + "https://ghfast.top/${url}" \ + "https://cdn.jsdelivr.net/gh/${owner}/${repo}@${branch}/${path}"; do + _fetch "$m" "$target" "" && return 0 + done + ;; + https://github.com/*) + for m in \ + "https://gh-proxy.com/${url}" \ + "https://ghfast.top/${url}"; do + _fetch "$m" "$target" "" && return 0 + done + ;; + esac + + _fetch "$url" "$target" "" +} + +download_optional() { + url="$1" + target="$2" + name="$3" + tmp_target="${TMP_DIR}/$(basename "$target").tmp" + if [ -z "$url" ]; then + log "$name skip (url empty)" + return 0 + fi + rm -f "$tmp_target" >/dev/null 2>&1 + if download_to "$url" "$tmp_target"; then + mv -f "$tmp_target" "$target" >/dev/null 2>&1 || return 1 + chmod 644 "$target" >/dev/null 2>&1 + log "$name updated" + return 0 + fi + rm -f "$tmp_target" >/dev/null 2>&1 + log "$name update failed" + return 0 +} + +download_geosite() { + url="$1" + tmp_target="${TMP_DIR}/GeoSite.dat.tmp" + size=0 + if [ -z "$url" ]; then + log "GeoSite.dat skip (url empty)" + return 0 + fi + rm -f "$tmp_target" >/dev/null 2>&1 + if ! download_to "$url" "$tmp_target"; then + rm -f "$tmp_target" >/dev/null 2>&1 + log "GeoSite.dat update failed" + return 0 + fi + size=$(wc -c <"$tmp_target" 2>/dev/null) + if [ -z "$size" ] || [ "$size" -lt "$GEOSITE_MIN_SIZE" ]; then + log "GeoSite.dat download invalid or incomplete (${size:-0} bytes)" + rm -f "$tmp_target" >/dev/null 2>&1 + return 0 + fi + mv -f "$tmp_target" /etc/clashoo/GeoSite.dat >/dev/null 2>&1 || return 1 + cp -f /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1 || true + chmod 644 /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1 + log "GeoSite.dat updated" + return 0 +} + +download_mmdb() { + url="$1" + tmp_target="${TMP_DIR}/Country.mmdb.tmp" + size=0 + rm -f "$tmp_target" >/dev/null 2>&1 + if ! download_to "$url" "$tmp_target"; then + rm -f "$tmp_target" >/dev/null 2>&1 + return 1 + fi + size=$(wc -c <"$tmp_target" 2>/dev/null) + if [ -z "$size" ] || [ "$size" -lt "$MMDB_MIN_SIZE" ]; then + log "Country.mmdb download invalid or incomplete (${size:-0} bytes)" + rm -f "$tmp_target" >/dev/null 2>&1 + return 1 + fi + mv -f "$tmp_target" /etc/clashoo/Country.mmdb >/dev/null 2>&1 || return 1 + chmod 644 /etc/clashoo/Country.mmdb >/dev/null 2>&1 + return 0 +} + +trap cleanup EXIT INT TERM +mkdir -p "$TMP_DIR" /etc/clashoo >/dev/null 2>&1 + +rm -f /var/run/geoip_down_complete >/dev/null 2>&1 +: > /var/run/geoip_update +: > "$LOG_FILE" +log "GeoIP 更新任务启动" + +mmdb_url="" +geosite_url="" +geoip_dat_url="" +geoip_asn_url="" + +case "$geoip_source" in + 1) + if [ -z "$license_key" ]; then + log "MaxMind source selected but license key is empty" + exit 1 + fi + log "Updating Country.mmdb from MaxMind" + if ! download_to "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${license_key}&suffix=tar.gz" "$TMP_DIR/geoip.tar.gz"; then + log "MaxMind download failed" + exit 1 + fi + if ! tar zxf "$TMP_DIR/geoip.tar.gz" -C "$TMP_DIR" >/dev/null 2>&1; then + log "MaxMind archive extract failed" + exit 1 + fi + mmdb_file="$(ls "$TMP_DIR"/GeoLite2-Country_*/GeoLite2-Country.mmdb 2>/dev/null | head -n 1)" + if [ -z "$mmdb_file" ] || [ ! -f "$mmdb_file" ]; then + log "MaxMind Country.mmdb not found in archive" + exit 1 + fi + cp -f "$mmdb_file" /etc/clashoo/Country.mmdb + chmod 644 /etc/clashoo/Country.mmdb >/dev/null 2>&1 + log "Country.mmdb updated" + ;; + 3) + # Loyalsoldier (default) + mmdb_url="$LOYALSOLDIER_MMDB_URL" + geosite_url="$LOYALSOLDIER_GEOSITE_URL" + geoip_dat_url="$LOYALSOLDIER_GEOIP_DAT_URL" + geoip_asn_url="$DEFAULT_GEOIP_ASN_URL" + ;; + 5) + # v2fly (mmdb borrowed from Loyalsoldier) + mmdb_url="$LOYALSOLDIER_MMDB_URL" + geosite_url="$V2FLY_GEOSITE_URL" + geoip_dat_url="$V2FLY_GEOIP_DAT_URL" + geoip_asn_url="$DEFAULT_GEOIP_ASN_URL" + ;; + 4) + # custom — asn falls back to default when left blank + mmdb_url="$cfg_mmdb_url" + geosite_url="$cfg_geosite_url" + geoip_dat_url="$cfg_geoip_dat_url" + geoip_asn_url="${cfg_geoip_asn_url:-$DEFAULT_GEOIP_ASN_URL}" + ;; + *) + mmdb_url="${cfg_mmdb_url:-$DEFAULT_MMDB_URL}" + geosite_url="${cfg_geosite_url:-$DEFAULT_GEOSITE_URL}" + geoip_dat_url="${cfg_geoip_dat_url:-$DEFAULT_GEOIP_DAT_URL}" + geoip_asn_url="${cfg_geoip_asn_url:-$DEFAULT_GEOIP_ASN_URL}" + ;; +esac + +if [ "$geoip_source" != "1" ]; then + log "Updating Country.mmdb" + if ! download_mmdb "$mmdb_url"; then + log "Country.mmdb download failed" + exit 1 + fi + log "Country.mmdb updated" + + if config_needs_geosite; then + download_geosite "$geosite_url" + else + log "GeoSite.dat skip (no geosite reference in config)" + fi + download_optional "$geoip_dat_url" /etc/clashoo/geoip.dat "geoip.dat" + download_optional "$geoip_asn_url" /etc/clashoo/GeoLite2-ASN.mmdb "GeoLite2-ASN.mmdb" +fi + +touch /var/run/geoip_down_complete >/dev/null 2>&1 + +if pidof mihomo >/dev/null 2>&1 || pidof clash-meta >/dev/null 2>&1; then + log "GeoIP update completed, apply on next Clashoo restart" +fi + +log "GeoIP update completed" +exit 0 diff --git a/clashoo/files/usr/share/clashoo/update/lgbm_update.sh b/clashoo/files/usr/share/clashoo/update/lgbm_update.sh new file mode 100755 index 000000000..6b96dbf0e --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/lgbm_update.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +MODEL_PATH="/etc/clashoo/Model.bin" +TMP_PATH="/tmp/clash_Model.bin" +LOG_FILE="/tmp/lgbm_update.log" + +log() { + echo " $(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" +} + +: > "$LOG_FILE" + +DOWNLOAD_URL=$(uci get clashoo.config.smart_lgbm_url 2>/dev/null) +[ -z "$DOWNLOAD_URL" ] && DOWNLOAD_URL="https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin" + +log "Start downloading LightGBM model from: $DOWNLOAD_URL" + +mkdir -p /etc/clashoo + +rm -f "$TMP_PATH" 2>/dev/null +if command -v curl >/dev/null 2>&1; then + curl -fL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$DOWNLOAD_URL" -o "$TMP_PATH" 2>/dev/null + dl_rc=$? +elif command -v wget >/dev/null 2>&1; then + wget -q --timeout=30 --tries=2 --no-check-certificate -U "Clash/OpenWRT" "$DOWNLOAD_URL" -O "$TMP_PATH" 2>/dev/null + dl_rc=$? +else + log "No curl or wget found" + exit 1 +fi + +if [ "$dl_rc" -ne 0 ] || [ ! -s "$TMP_PATH" ]; then + log "Download failed (rc=$dl_rc)" + rm -f "$TMP_PATH" + exit 1 +fi + +if [ -f "$MODEL_PATH" ] && cmp -s "$TMP_PATH" "$MODEL_PATH"; then + log "Model unchanged, no update needed" + rm -f "$TMP_PATH" + exit 0 +fi + +mv "$TMP_PATH" "$MODEL_PATH" 2>/dev/null +if [ $? -ne 0 ]; then + log "Failed to install model to $MODEL_PATH" + rm -f "$TMP_PATH" + exit 1 +fi + +log "LightGBM model updated successfully" +exit 0 diff --git a/clashoo/files/usr/share/clashoo/update/maintenance_cron.sh b/clashoo/files/usr/share/clashoo/update/maintenance_cron.sh new file mode 100755 index 000000000..e0e4affbc --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/maintenance_cron.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +STATE_DIR="${CLASHOO_STATE_DIR:-/usr/share/clashbackup}" +UPDATE_SCRIPT="${CLASHOO_UPDATE_SCRIPT:-/usr/share/clashoo/update/update_all.sh}" +LOG_FILE="${CLASHOO_LOG_FILE:-/usr/share/clashoo/clashoo.txt}" +LOCK_DIR="${CLASHOO_LOCK_DIR:-/tmp/clashoo_maintenance.lock}" +NOW="${CLASHOO_NOW:-$(date +%s)}" + +stamp() { + tmp="$STATE_DIR/$1.tmp.$$" + printf 'last_run=%s\n' "$NOW" >"$tmp" && mv -f "$tmp" "$STATE_DIR/$1" +} + +due() { + hours="$(uci -q get "clashoo.config.$2")" + echo "$hours" | grep -Eq '^[0-9]+$' && [ "$hours" -ge 1 ] || hours=12 + last="$(sed -n 's/^last_run=//p' "$STATE_DIR/$1" 2>/dev/null | head -1)" + if ! echo "$last" | grep -Eq '^[0-9]+$' || [ "$last" -gt "$NOW" ]; then + stamp "$1" + return 1 + fi + [ $((NOW - last)) -ge $((hours * 3600)) ] +} + +mkdir -p "$STATE_DIR" || exit 0 +mkdir "$LOCK_DIR" 2>/dev/null || exit 0 +trap 'rmdir "$LOCK_DIR" >/dev/null 2>&1' EXIT INT TERM + +if [ "$(uci -q get clashoo.config.auto_update)" = "1" ] && due rule_update.status auto_update_time; then + CLASHOO_STATE_DIR="$STATE_DIR" sh "$UPDATE_SCRIPT" >/dev/null 2>&1 +fi + +if [ "$(uci -q get clashoo.config.auto_clear_log)" = "1" ] && due log_cleanup.status clear_time; then + : >"$LOG_FILE" + stamp log_cleanup.status +fi diff --git a/clashoo/files/usr/share/clashoo/update/panel_download.sh b/clashoo/files/usr/share/clashoo/update/panel_download.sh new file mode 100755 index 000000000..4519c4833 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/panel_download.sh @@ -0,0 +1,176 @@ +#!/bin/sh + +REAL_LOG="/usr/share/clashoo/clashoo_real.txt" +panel="$1" +lang="$(uci -q get luci.main.lang 2>/dev/null)" +STATE_FILE="/tmp/clash_panel_download_state" +RUN_FILE="/var/run/panel_downloading" +DASHBOARD_LINK="/etc/clashoo/dashboard" + +[ -n "$panel" ] || panel="$(uci -q get clashoo.config.dashboard_panel 2>/dev/null)" +[ -n "$panel" ] || panel="zashboard" + +case "$panel" in + metacubexd) + URLS="https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip" + TARGET_DIR="/etc/clashoo/dashboard-metacubexd" + ;; + yacd) + URLS="https://github.com/MetaCubeX/Yacd-meta/archive/refs/heads/gh-pages.zip https://github.com/haishanh/yacd/archive/refs/heads/gh-pages.zip" + TARGET_DIR="/etc/clashoo/dashboard-yacd" + ;; + zashboard) + URLS="https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip" + TARGET_DIR="/etc/clashoo/dashboard-zashboard" + ;; + razord) + URLS="https://github.com/MetaCubeX/Razord-meta/archive/refs/heads/gh-pages.zip https://github.com/ayanamist/clash-dashboard/archive/refs/heads/gh-pages.zip" + TARGET_DIR="/etc/clashoo/dashboard-razord" + ;; + *) + URLS="https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip" + TARGET_DIR="/etc/clashoo/dashboard-zashboard" + panel="zashboard" + ;; +esac + +TMP_ROOT="/tmp/clash_panel_${panel}_$$" +ZIP_FILE="$TMP_ROOT/panel.zip" +UNPACK_DIR="$TMP_ROOT/unpack" + +UPDATE_LOG="/tmp/clash_update.txt" + +update_log() { + [ -n "$1" ] || return 0 + printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG" +} + +log_msg() { + if [ "$lang" = "en" ]; then + echo "$1" >"$REAL_LOG" + [ "$3" = "1" ] || update_log "$1" + else + echo "$2" >"$REAL_LOG" + [ "$3" = "1" ] || update_log "$2" + fi +} + +set_state() { + state="$1" + msg="$2" + echo "${state}:${panel}:${msg}" >"$STATE_FILE" +} + +cleanup() { + rm -rf "$TMP_ROOT" >/dev/null 2>&1 + rm -f "$RUN_FILE" >/dev/null 2>&1 +} + +# Route the panel download through the running core in kernel-only mode +# (shared logic in proxy_lib.sh). Normal mode returns empty -> TPROXY. +. /usr/share/clashoo/update/proxy_lib.sh +detect_proxy() { clashoo_detect_proxy; } + +download_zip() { + proxy="$(detect_proxy)" + for url in $URLS; do + if [ -n "$proxy" ] && command -v curl >/dev/null 2>&1 && + curl -fsSL --connect-timeout 15 --max-time 300 --retry 1 \ + --proxy "$proxy" -A "Clash/OpenWRT" "$url" -o "$ZIP_FILE"; then + return 0 + fi + if wget -q --timeout=60 --no-check-certificate --user-agent="Clash/OpenWRT" "$url" -O "$ZIP_FILE"; then + return 0 + fi + done + return 1 +} + +extract_zip() { + if command -v unzip >/dev/null 2>&1; then + unzip -oq "$ZIP_FILE" -d "$UNPACK_DIR" >/dev/null 2>&1 + return $? + fi + + if command -v busybox >/dev/null 2>&1 && busybox --list 2>/dev/null | grep -qx "unzip"; then + busybox unzip -oq "$ZIP_FILE" -d "$UNPACK_DIR" >/dev/null 2>&1 + return $? + fi + + if command -v bsdtar >/dev/null 2>&1; then + bsdtar -xf "$ZIP_FILE" -C "$UNPACK_DIR" >/dev/null 2>&1 + return $? + fi + + return 2 +} + +find_web_root() { + if [ -f "$UNPACK_DIR/index.html" ]; then + echo "$UNPACK_DIR" + return 0 + fi + + index_file="$(find "$UNPACK_DIR" -type f -name index.html 2>/dev/null | head -n 1)" + if [ -n "$index_file" ]; then + dirname "$index_file" + return 0 + fi + + return 1 +} + +activate_panel() { + rm -rf "$DASHBOARD_LINK" >/dev/null 2>&1 + ln -s "$TARGET_DIR" "$DASHBOARD_LINK" >/dev/null 2>&1 || cp -a "$TARGET_DIR" "$DASHBOARD_LINK" >/dev/null 2>&1 + rm -rf /www/luci-static/yacd >/dev/null 2>&1 + if [ "$panel" = "yacd" ]; then + ln -s "$TARGET_DIR" /www/luci-static/yacd >/dev/null 2>&1 + fi +} + +trap cleanup EXIT INT TERM +mkdir -p "$UNPACK_DIR" "$TARGET_DIR" >/dev/null 2>&1 +touch "$RUN_FILE" +set_state "downloading" "开始下载" + +log_msg "Downloading dashboard panel..." "正在下载面板..." +if ! download_zip; then + set_state "error" "下载失败" + log_msg "Dashboard panel download failed" "面板下载失败" + exit 1 +fi + +extract_zip +extract_rc=$? +if [ "$extract_rc" -ne 0 ]; then + if [ "$extract_rc" -eq 2 ]; then + set_state "error" "缺少解压工具" + log_msg "Panel unzip tool not found" "缺少解压工具,无法安装面板" + else + set_state "error" "解压失败" + log_msg "Dashboard panel unzip failed" "面板解压失败" + fi + exit 1 +fi + +SRC_DIR="$(find_web_root)" +if [ -z "$SRC_DIR" ] || [ ! -f "$SRC_DIR/index.html" ]; then + set_state "error" "文件无效" + log_msg "Dashboard panel content invalid" "面板文件无效" + exit 1 +fi + +rm -rf "$TARGET_DIR"/* >/dev/null 2>&1 +cp -a "$SRC_DIR"/. "$TARGET_DIR"/ >/dev/null 2>&1 + +activate_panel + +uci set clashoo.config.dashboard_panel="$panel" >/dev/null 2>&1 +uci commit clashoo >/dev/null 2>&1 + +set_state "success" "安装成功" +log_msg "Dashboard panel installed" "面板安装完成" +sleep 1 +log_msg "Clashoo" "Clashoo" 1 +exit 0 diff --git a/clashoo/files/usr/share/clashoo/update/proxy_lib.sh b/clashoo/files/usr/share/clashoo/update/proxy_lib.sh new file mode 100644 index 000000000..e9664420d --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/proxy_lib.sh @@ -0,0 +1,80 @@ +# shellcheck shell=sh +# Clashoo's sing-box is private; HomeProxy owns the public system binary. +CLASHOO_SINGBOX_BIN_DIR="/usr/share/clashoo/bin" +CLASHOO_SINGBOX_RUNTIME_CONFIG="/var/run/clashoo/sing-box/config.json" + +# Shared proxy detection for clashoo's own outbound requests. +# +# clashoo's self-initiated downloads (update checks, component/panel/core/geoip +# downloads, subscriptions) and health checks must reach GitHub through the +# running core, otherwise they leak out direct and stall behind the GFW. Route +# them through the core's mixed port whenever a core is running — in BOTH modes: +# - core-only mode deliberately skips TPROXY to coexist with other plugins; +# - normal mode used to rely on transparent redirect, but the router's OWN +# output redirect is fragile (e.g. PPPoE/GL.iNet stacks where Redirect to +# loopback never completes — issue #25), so don't depend on it. The explicit +# mixed port works on every network stack. +# When no core is running (e.g. bootstrapping the very first core download), +# returns empty so the caller falls back to direct / mirror sources. +# +# Port source depends on the running Clashoo kernel. The liveness gate uses the +# procd owner instead of a binary name, because other proxy plugins may run the +# same binaries: +# sing-box -> private runtime config mixed inbound +# (an imported profile may carry its own port) +# mihomo / clash-meta / smart -> /etc/clashoo/config.yaml mixed-port +# (a custom config's port may differ from uci) +# uci mixed_port is the last-resort fallback for both. +clashoo_running_core_type() { + _cdp_type="$(uci -q get clashoo.config.core_type 2>/dev/null)" + case "$_cdp_type" in + singbox) + _cdp_service="clashoo" + _cdp_filter='@.clashoo.instances.singbox.pid' + ;; + *) + _cdp_type="mihomo" + _cdp_service="clashoo" + _cdp_filter='@.clashoo.instances.*.pid' + ;; + esac + + _cdp_pid="$(ubus call service list "{\"name\":\"$_cdp_service\",\"verbose\":true}" 2>/dev/null \ + | jsonfilter -e "$_cdp_filter" 2>/dev/null | head -n1)" + [ -n "$_cdp_pid" ] && [ -d "/proc/$_cdp_pid" ] || return 1 + if [ "$_cdp_type" = "singbox" ]; then + local _cdp_exe _cdp_cmdline + _cdp_exe="$(readlink -f "/proc/$_cdp_pid/exe" 2>/dev/null)" + case "$_cdp_exe" in + "$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha") ;; + *) + _cdp_cmdline="$(tr '\000' ' ' < "/proc/$_cdp_pid/cmdline" 2>/dev/null)" + case "$_cdp_cmdline" in + "$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable "*|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha "*) ;; + *) return 1 ;; + esac + ;; + esac + fi + printf '%s' "$_cdp_type" +} + +clashoo_detect_proxy() { + _cdp_type="$(clashoo_running_core_type)" || return 0 + _cdp_port="" + if [ "$_cdp_type" = "singbox" ]; then + _cdp_port="$(jsonfilter -i "$CLASHOO_SINGBOX_RUNTIME_CONFIG" \ + -e '@.inbounds[@.type="mixed"].listen_port' 2>/dev/null | head -n 1)" + else + # prefer mixed-port (HTTP+SOCKS) over plain port; a single two-pattern + # sed would return whichever appears first by line order, so loop by key + for _cdp_key in mixed-port port socks-port; do + _cdp_port="$(sed -n "s/^[[:space:]]*${_cdp_key}:[[:space:]]*\([0-9][0-9]*\).*/\1/p" \ + /etc/clashoo/config.yaml 2>/dev/null | head -n 1)" + [ -n "$_cdp_port" ] && break + done + fi + [ -n "$_cdp_port" ] || _cdp_port="$(uci -q get clashoo.config.mixed_port 2>/dev/null)" + [ -n "$_cdp_port" ] || return 0 + printf 'http://127.0.0.1:%s' "$_cdp_port" +} diff --git a/clashoo/files/usr/share/clashoo/update/subscription_update.sh b/clashoo/files/usr/share/clashoo/update/subscription_update.sh new file mode 100755 index 000000000..49ef064f3 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/subscription_update.sh @@ -0,0 +1,386 @@ +#!/bin/sh + +SUB_DIR="${CLASHOO_SUB_DIR:-/usr/share/clashoo/config/sub}" +SINGBOX_DIR="${CLASHOO_SINGBOX_DIR:-/usr/share/clashoo/config/singbox}" +BACKUP_DIR="${CLASHOO_BACKUP_DIR:-/usr/share/clashbackup}" +TEMPLATE_DIR="${CLASHOO_TEMPLATE_DIR:-/usr/share/clashoo/config/custom}" +LIST_FILE="${CLASHOO_LIST_FILE:-$BACKUP_DIR/confit_list.conf}" +BIND_FILE="${CLASHOO_BIND_FILE:-$BACKUP_DIR/template_bindings.conf}" +STATUS_FILE="${CLASHOO_STATUS_FILE:-$BACKUP_DIR/subscription_update.status}" +LOCK_DIR="${CLASHOO_LOCK_DIR:-/tmp/clashoo_subscription_update.lock}" +UPDATE_LOG="${CLASHOO_UPDATE_LOG:-/tmp/clash_update.txt}" +SERVICE_CMD="${CLASHOO_SERVICE_CMD:-/etc/init.d/clashoo}" +TMP_DIR="${CLASHOO_TMP_DIR:-/tmp}" +DEFAULT_SUB_UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36' +DOWNLOAD_ERROR="" + +# Subscriptions are fetched direct first (many airports are domestic / geo-fence +# foreign exit IPs). Only used as a last-resort fallback, see download_to. +. /usr/share/clashoo/update/proxy_lib.sh + +updated=0 +unchanged=0 +failed=0 +skipped=0 +restart_needed=0 +started_at="$(date +%s)" +message="" +record_status=0 +[ "$1" = "--all" ] && record_status=1 + +log_update() { + printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG" +} + +write_status() { + local tmp + mkdir -p "$(dirname "$STATUS_FILE")" >/dev/null 2>&1 + tmp="${STATUS_FILE}.tmp.$$" + cat >"$tmp" </dev/null 2>&1 +} + +if ! mkdir "$LOCK_DIR" >/dev/null 2>&1; then + exit 75 +fi +trap cleanup EXIT INT TERM +[ "$record_status" = "1" ] && write_status 1 0 + +safe_name() { + case "$1" in + ''|*/*|*..*) return 1 ;; + esac + return 0 +} + +extract_host() { + printf '%s' "$1" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's#:.*$##' -e 's#.*@##' +} + +resolve_via() { + local host dns + host="$1" + dns="$2" + nslookup "$host" "$dns" 2>/dev/null | awk ' + /^Address/ { + ip = $NF + if (ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ && + ip !~ /^127\./ && ip !~ /^0\./ && ip !~ /^198\.18\./ && + ip != "8.8.8.8" && ip != "1.1.1.1" && + ip != "223.5.5.5" && ip != "119.29.29.29") { + print ip + exit + } + }' +} + +curl_download() { + local url out hdr ua host ip + url="$1" + out="$2" + hdr="$3" + ua="$4" + host="$5" + ip="$6" + if [ -n "$host" ] && [ -n "$ip" ]; then + curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \ + --speed-limit 1 --retry 2 -A "$ua" -D "$hdr" -o "$out" \ + --resolve "$host:443:$ip" --resolve "$host:80:$ip" \ + -w '%{http_code}' "$url" 2>/dev/null + else + curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \ + --speed-limit 1 --retry 2 -A "$ua" -D "$hdr" -o "$out" \ + -w '%{http_code}' "$url" 2>/dev/null + fi +} + +subscription_ua() { + local target ua + target="$1" + ua="" + [ -r "${target}.ua" ] && ua="$(sed -n '1p' "${target}.ua" 2>/dev/null)" + [ -n "$ua" ] || ua="$(uci -q get clashoo.config.sub_ua 2>/dev/null)" + [ -n "$ua" ] || ua="$DEFAULT_SUB_UA" + printf '%s' "$ua" +} + +save_subscription_ua() { + printf '%s\n' "$2" >"${1}.ua" +} + +curl_error() { + case "$1" in + 6) printf '%s' "DNS 解析失败" ;; + 7) printf '%s' "连接失败" ;; + 28) printf '%s' "连接超时" ;; + 35) printf '%s' "TLS 握手失败" ;; + 60) printf '%s' "证书校验失败" ;; + *) printf '网络错误(curl rc=%s)' "${1:-unknown}" ;; + esac +} + +download_to() { + local url out hdr ua code rc last_rc host dns ip proxy + url="$1" + out="$2" + hdr="$3" + ua="$4" + [ -n "$ua" ] || ua="$DEFAULT_SUB_UA" + DOWNLOAD_ERROR="" + + rm -f "$out" "$hdr" >/dev/null 2>&1 + if command -v curl >/dev/null 2>&1; then + code="$(curl_download "$url" "$out" "$hdr" "$ua" "" "")" + rc=$? + if [ "$rc" -eq 0 ] && [ "$code" = "200" ]; then + return 0 + fi + if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then + DOWNLOAD_ERROR="HTTP ${code}" + return 1 + fi + last_rc="$rc" + host="$(extract_host "$url")" + for dns in 223.5.5.5 119.29.29.29 1.1.1.1 8.8.8.8; do + ip="$(resolve_via "$host" "$dns")" + [ -n "$ip" ] || continue + log_update "DNS 回退:${host} -> ${ip} (@${dns})" + code="$(curl_download "$url" "$out" "$hdr" "$ua" "$host" "$ip")" + rc=$? + [ "$rc" -eq 0 ] && [ "$code" = "200" ] && return 0 + if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then + DOWNLOAD_ERROR="HTTP ${code}" + return 1 + fi + last_rc="$rc" + done + # Last resort: direct + DNS-override both failed, so the source may be + # GFW-blocked (e.g. a github-hosted sub). In kernel-only mode try once + # through the running core — nothing left to lose, and it won't disturb + # any airport that already answered direct. + proxy="$(clashoo_detect_proxy)" + if [ -n "$proxy" ]; then + log_update "代理兜底:通过本地核心重试订阅" + code="$(curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \ + --speed-limit 1 --retry 1 -A "$ua" -D "$hdr" -o "$out" \ + --proxy "$proxy" -w '%{http_code}' "$url" 2>/dev/null)" + rc=$? + [ "$rc" -eq 0 ] && [ "$code" = "200" ] && return 0 + if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then + DOWNLOAD_ERROR="HTTP ${code}" + return 1 + fi + last_rc="$rc" + fi + DOWNLOAD_ERROR="$(curl_error "$last_rc")" + return 1 + fi + wget -q --tries=4 --timeout=20 --user-agent="$ua" "$url" -O "$out" + rc=$? + [ "$rc" -eq 0 ] && return 0 + DOWNLOAD_ERROR="网络错误(wget rc=${rc})" + return 1 +} + +update_info() { + local hdr target info + hdr="$1" + target="$2" + info="$(grep -i 'subscription-userinfo:' "$hdr" 2>/dev/null | head -1 | \ + sed 's/^[Ss]ubscription-[Uu]serinfo:[[:space:]]*//' | tr -d '\r')" + if [ -n "$info" ]; then + printf '%s\n' "$info" >"${target}.info" + else + rm -f "${target}.info" >/dev/null 2>&1 + fi +} + +service_running() { + [ -n "${CLASHOO_SERVICE_CMD:-}" ] && return 0 + "$SERVICE_CMD" status >/dev/null 2>&1 +} + +template_output_name() { + local sub tpl + sub="$(printf '%s' "$1" | sed -e 's/\.[Yy][Aa][Mm][Ll]$//' -e 's/\.[Yy][Mm][Ll]$//' -e 's/[^A-Za-z0-9._-]/-/g')" + tpl="$(printf '%s' "$2" | sed -e 's/\.[Yy][Aa][Mm][Ll]$//' -e 's/\.[Yy][Mm][Ll]$//' -e 's/[^A-Za-z0-9._-]/-/g')" + printf '_merged_%s__%s.yaml' "${sub:-sub}" "${tpl:-template}" +} + +apply_template() { + local name target template merged merged_path use_config + name="$1" + target="$2" + [ -r "$BIND_FILE" ] || return 0 + [ -x /usr/share/clashoo/update/template_merge.sh ] || return 0 + template="$(awk -F '#' -v n="$name" '$1==n && ($3=="1" || $3=="true") {print $2; exit}' "$BIND_FILE")" + [ -n "$template" ] && [ -r "$TEMPLATE_DIR/$template" ] || return 0 + merged="$(template_output_name "$name" "$template")" + merged_path="$TEMPLATE_DIR/$merged" + if sh /usr/share/clashoo/update/template_merge.sh "$target" "$TEMPLATE_DIR/$template" "$merged_path" >/dev/null 2>&1; then + use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)" + if [ "$use_config" = "$target" ] || [ "$use_config" = "$merged_path" ]; then + uci -q set clashoo.config.use_config="$merged_path" + uci -q set clashoo.config.config_type='3' + uci -q commit clashoo >/dev/null 2>&1 + restart_needed=1 + fi + else + log_update "模板生成失败:${name} <- ${template}" + fi +} + +update_mihomo() { + local name url typ target tmp hdr ua use_config config_type + name="$1" + url="$2" + typ="$3" + safe_name "$name" || return 1 + case "$typ" in clash|meta) ;; *) skipped=$((skipped + 1)); return 0 ;; esac + target="$SUB_DIR/$name" + [ -f "$target" ] || { skipped=$((skipped + 1)); return 0; } + tmp="$TMP_DIR/clashoo_sub_$$.yaml" + hdr="$TMP_DIR/clashoo_sub_$$.hdr" + ua="$(subscription_ua "$target")" + if ! download_to "$url" "$tmp" "$hdr" "$ua"; then + failed=$((failed + 1)) + log_update "更新失败(${DOWNLOAD_ERROR:-下载失败}):$name" + rm -f "$tmp" "$hdr" + return 1 + fi + if ! grep -Eq '^(proxies|proxy-providers):' "$tmp" 2>/dev/null; then + failed=$((failed + 1)) + log_update "更新失败(无效 Mihomo 配置):$name" + rm -f "$tmp" "$hdr" + return 1 + fi + if cmp -s "$tmp" "$target"; then + update_info "$hdr" "$target" + save_subscription_ua "$target" "$ua" + unchanged=$((unchanged + 1)) + rm -f "$tmp" "$hdr" + return 0 + fi + if ! mv "$tmp" "$target"; then + failed=$((failed + 1)) + rm -f "$hdr" + return 1 + fi + update_info "$hdr" "$target" + save_subscription_ua "$target" "$ua" + rm -f "$hdr" + updated=$((updated + 1)) + use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)" + config_type="$(uci -q get clashoo.config.config_type 2>/dev/null)" + [ "$config_type" = "1" ] && [ "$use_config" = "$target" ] && restart_needed=1 + apply_template "$name" "$target" + log_update "更新完成:$name" +} + +valid_singbox() { + ucode -e 'import { readfile } from "fs"; let c = json(readfile(ARGV[0])); exit(type(c) == "object" && type(c.outbounds) == "array" && length(c.outbounds) > 0 ? 0 : 1);' "$1" >/dev/null 2>&1 +} + +update_singbox() { + local name target url tmp hdr ua active + name="$1" + safe_name "$name" || return 1 + target="$SINGBOX_DIR/$name" + [ -f "$target" ] && [ -r "$target.url" ] || { skipped=$((skipped + 1)); return 0; } + url="$(sed -n '1p' "$target.url")" + [ -n "$url" ] || { skipped=$((skipped + 1)); return 0; } + tmp="$TMP_DIR/clashoo_sb_$$.json" + hdr="$TMP_DIR/clashoo_sb_$$.hdr" + ua="$(subscription_ua "$target")" + if ! download_to "$url" "$tmp" "$hdr" "$ua"; then + failed=$((failed + 1)) + log_update "更新失败(${DOWNLOAD_ERROR:-下载失败}):$name" + rm -f "$tmp" "$hdr" + return 1 + fi + if ! valid_singbox "$tmp"; then + failed=$((failed + 1)) + log_update "更新失败(无效 sing-box 配置):$name" + rm -f "$tmp" "$hdr" + return 1 + fi + if cmp -s "$tmp" "$target"; then + update_info "$hdr" "$target" + save_subscription_ua "$target" "$ua" + unchanged=$((unchanged + 1)) + rm -f "$tmp" "$hdr" + return 0 + fi + if ! mv "$tmp" "$target"; then + failed=$((failed + 1)) + rm -f "$hdr" + return 1 + fi + update_info "$hdr" "$target" + save_subscription_ua "$target" "$ua" + rm -f "$hdr" + updated=$((updated + 1)) + active="$(uci -q get clashoo.config.singbox_active 2>/dev/null)" + [ "$active" = "$name" ] && restart_needed=1 + log_update "更新完成:$name" +} + +update_all() { + local name url typ url_file + if [ -r "$LIST_FILE" ]; then + while IFS='#' read -r name url typ _rest; do + [ -n "$name" ] && [ -n "$url" ] || continue + update_mihomo "$name" "$url" "$typ" || true + done <"$LIST_FILE" + fi + for url_file in "$SINGBOX_DIR"/*.url; do + [ -f "$url_file" ] || continue + name="$(basename "$url_file" .url)" + update_singbox "$name" || true + done +} + +case "$1" in + --all) + update_all + ;; + --mihomo) + line="$(awk -F '#' -v n="$2" '$1==n {print; exit}' "$LIST_FILE" 2>/dev/null)" + [ -n "$line" ] || { failed=1; message="subscription not found"; } + if [ -n "$line" ]; then + name="$(printf '%s' "$line" | awk -F '#' '{print $1}')" + url="$(printf '%s' "$line" | awk -F '#' '{print $2}')" + typ="$(printf '%s' "$line" | awk -F '#' '{print $3}')" + update_mihomo "$name" "$url" "$typ" || true + fi + ;; + --singbox) + update_singbox "$2" || true + ;; + *) + failed=1 + message="invalid arguments" + ;; +esac + +if [ "$restart_needed" = "1" ] && service_running; then + "$SERVICE_CMD" restart >/dev/null 2>&1 +fi + +finished_at="$(date +%s)" +[ -n "$message" ] || message="updated=$updated unchanged=$unchanged failed=$failed skipped=$skipped" +[ "$record_status" = "1" ] && write_status 0 "$finished_at" +[ "$failed" -eq 0 ] diff --git a/clashoo/files/usr/share/clashoo/update/subscription_update_cron.sh b/clashoo/files/usr/share/clashoo/update/subscription_update_cron.sh new file mode 100755 index 000000000..1f83307c7 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/subscription_update_cron.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +STATUS_FILE="${CLASHOO_STATUS_FILE:-/usr/share/clashbackup/subscription_update.status}" +UPDATER="${CLASHOO_SUBSCRIPTION_UPDATER:-/usr/share/clashoo/update/subscription_update.sh}" +SERVICE_CMD="${CLASHOO_SERVICE_CMD:-/etc/init.d/clashoo}" +interval="$(uci -q get clashoo.config.subscription_update_interval 2>/dev/null)" +now="$(date +%s)" +last=0 + +[ "$(uci -q get clashoo.config.auto_subscription_update 2>/dev/null)" = "1" ] || exit 0 +"$SERVICE_CMD" status >/dev/null 2>&1 || exit 0 +echo "$interval" | grep -Eq '^[0-9]+$' || interval=72 +[ "$interval" -gt 0 ] || interval=72 +[ -r "$STATUS_FILE" ] && last="$(sed -n 's/^last_run=//p' "$STATUS_FILE" | head -1)" +echo "$last" | grep -Eq '^[0-9]+$' || last=0 + +[ $((now - last)) -ge $((interval * 3600)) ] || exit 0 +sh "$UPDATER" --all diff --git a/clashoo/files/usr/share/clashoo/update/template_merge.sh b/clashoo/files/usr/share/clashoo/update/template_merge.sh new file mode 100755 index 000000000..63be16c1b --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/template_merge.sh @@ -0,0 +1,101 @@ +#!/bin/sh + +set -eu + +SUB_FILE="${1:-}" +TEMPLATE_FILE="${2:-}" +OUT_FILE="${3:-}" +TMP_FILE="/tmp/clash_template_merge_$$.yaml" +TMP_FILE2="${TMP_FILE}.norm" +LOG_FILE="/tmp/clash_update.txt" + +log() { + printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$LOG_FILE" +} + +cleanup() { + rm -f "$TMP_FILE" "$TMP_FILE2" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +[ -n "$SUB_FILE" ] || { log "模板复写失败:缺少订阅文件参数"; exit 1; } +[ -n "$TEMPLATE_FILE" ] || { log "模板复写失败:缺少模板文件参数"; exit 1; } +[ -n "$OUT_FILE" ] || { log "模板复写失败:缺少输出文件参数"; exit 1; } + +[ -f "$SUB_FILE" ] || { log "模板复写失败:订阅文件不存在"; exit 1; } +[ -f "$TEMPLATE_FILE" ] || { log "模板复写失败:模板文件不存在"; exit 1; } + +if ! command -v yq >/dev/null 2>&1; then + log "模板复写失败:缺少 yq" + exit 1 +fi + +core_bin="" +for b in mihomo clash-meta clash; do + if command -v "$b" >/dev/null 2>&1; then + core_bin="$b" + break + fi +done + +mkdir -p "$(dirname "$OUT_FILE")" >/dev/null 2>&1 + +log "开始模板复写:$(basename "$SUB_FILE") <- $(basename "$TEMPLATE_FILE")" +export SUB_FILE TEMPLATE_FILE + +# template is base; load() forces single-doc; explode anchors first (yq 4.53+ breaks "<<") +yq -n ' + (load(strenv(SUB_FILE)) | explode(.)) as $sub | + (load(strenv(TEMPLATE_FILE)) | explode(.)) as $tpl | + ( + ($tpl * { + "proxies": (((($tpl.proxies // []) + ($sub.proxies // [])) | unique_by(.name))), + "proxy-providers": (($tpl."proxy-providers" // {}) * ($sub."proxy-providers" // {})) + }) + | .dns = ((.dns // {}) * { + "proxy-server-nameserver": ( + .dns."proxy-server-nameserver" + // $sub.dns."proxy-server-nameserver" + // $sub.dns.nameserver + // ["223.5.5.5", "119.29.29.29"] + ) + }) + ) +' >"$TMP_FILE" 2>/dev/null || { + log "模板复写失败:YAML 合并错误" + exit 1 +} + +# 兼容第三方模板中使用“直连”字样,统一转换为 Clash 内置 DIRECT。 +yq e ' + (.. | select(tag == "!!str")) |= sub("^直连$", "DIRECT") + | .rules = ((.rules // []) | map(sub(",直连,", ",DIRECT,") | sub(",直连$", ",DIRECT"))) +' "$TMP_FILE" >"$TMP_FILE2" 2>/dev/null || { + log "模板复写失败:直连别名标准化失败" + exit 1 +} +mv -f "$TMP_FILE2" "$TMP_FILE" >/dev/null 2>&1 || { + log "模板复写失败:中间文件写入失败" + exit 1 +} + +yq e '.' "$TMP_FILE" >/dev/null 2>&1 || { + log "模板复写失败:合并结果 YAML 无效" + exit 1 +} + +if [ -n "$core_bin" ]; then + "$core_bin" -t -f "$TMP_FILE" >/dev/null 2>&1 || { + log "模板复写失败:内核校验不通过" + exit 1 + } +fi + +mv -f "$TMP_FILE" "$OUT_FILE" >/dev/null 2>&1 || { + log "模板复写失败:写入输出文件失败" + exit 1 +} + +chmod 644 "$OUT_FILE" >/dev/null 2>&1 || true +log "模板复写完成:$(basename "$OUT_FILE")" +exit 0 diff --git a/clashoo/files/usr/share/clashoo/update/update.sh b/clashoo/files/usr/share/clashoo/update/update.sh new file mode 100755 index 000000000..0b0a89360 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/update.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +config_name="${1:-$(uci -q get clashoo.config.config_update_name 2>/dev/null)}" +[ -n "$config_name" ] || exit 1 +exec sh /usr/share/clashoo/update/subscription_update.sh --mihomo "$config_name" diff --git a/clashoo/files/usr/share/clashoo/update/update_all.sh b/clashoo/files/usr/share/clashoo/update/update_all.sh new file mode 100755 index 000000000..b743529d0 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/update_all.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +UPDATE_LOG="/tmp/clash_update.txt" +STATE_DIR="${CLASHOO_STATE_DIR:-/usr/share/clashbackup}" + +log_update() { + printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG" +} + +log_update "更新大陆白名单" +sh /usr/share/clashoo/update/update_china_ip.sh >> "$UPDATE_LOG" 2>&1 + +log_update "更新 GeoIP / GeoSite" +sh /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1 + +mkdir -p "$STATE_DIR" +tmp="$STATE_DIR/rule_update.status.tmp.$$" +printf 'last_run=%s\n' "$(date +%s)" >"$tmp" && mv -f "$tmp" "$STATE_DIR/rule_update.status" + +exit 0 diff --git a/clashoo/files/usr/share/clashoo/update/update_china_ip.sh b/clashoo/files/usr/share/clashoo/update/update_china_ip.sh new file mode 100755 index 000000000..3797e8f85 --- /dev/null +++ b/clashoo/files/usr/share/clashoo/update/update_china_ip.sh @@ -0,0 +1,204 @@ +#!/bin/sh + +set -eu + +LOG_FILE="${LOG_FILE:-/tmp/clash_update.txt}" +NFT_DIR="${NFT_DIR:-/usr/share/clashoo/nftables}" +TARGET_V4="${NFT_DIR}/geoip_cn.nft" +TARGET_V6="${NFT_DIR}/geoip6_cn.nft" +FW4_SCRIPT="${FW4_SCRIPT:-/usr/share/clashoo/net/fw4.sh}" +PROC_ROOT="${PROC_ROOT:-/proc}" +SINGBOX_BIN_DIR="${SINGBOX_BIN_DIR:-/usr/share/clashoo/bin}" +TMP_V4="/tmp/china_ip.txt.$$" +TMP_V6="/tmp/china_ipv6.txt.$$" +OUT_V4="/tmp/geoip_cn.nft.$$" +OUT_V6="/tmp/geoip6_cn.nft.$$" + +log() { + printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOG_FILE" +} + +bool_enabled() { + case "$1" in + 1|true|TRUE|yes|on) return 0 ;; + *) return 1 ;; + esac +} + +download_with_fallback() { + local url="$1" + local output="$2" + local ip + + if curl -fsSL "$url" -o "$output"; then + return 0 + fi + + case "$url" in + https://ispip.clang.cn/*) + for ip in 182.247.248.127 103.220.64.183; do + if curl -fsSL --resolve "ispip.clang.cn:443:${ip}" "$url" -o "$output"; then + log "DNS 异常,已使用 --resolve(${ip}) 回源下载" + return 0 + fi + done + ;; + esac + + return 1 +} + +cleanup() { + rm -f "$TMP_V4" "$TMP_V6" "$OUT_V4" "$OUT_V6" +} + +trap cleanup EXIT INT TERM + +render_nft_set() { + local source_file="$1" + local output_file="$2" + local set_name="$3" + local set_type="$4" + + awk -v set_name="$set_name" -v set_type="$set_type" ' + BEGIN { + print "set " set_name " {" + print "\ttype " set_type ";" + print "\tflags interval;" + print "\tauto-merge;" + print "\telements = {" + first = 1 + } + !/^[[:space:]]*$/ && !/^[[:space:]]*#/ { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0) + if ($0 == "") + next + if (!first) + print "," + printf "\t\t%s", $0 + first = 0 + } + END { + if (!first) + print "" + print "\t}" + print "}" + } + ' "$source_file" > "$output_file" +} + +url4="$(uci -q get clashoo.config.china_ip_url 2>/dev/null || true)" +url6="$(uci -q get clashoo.config.china_ipv6_url 2>/dev/null || true)" +bypass_china="$(uci -q get clashoo.config.bypass_china 2>/dev/null || true)" +bypass_china_ipv6="$(uci -q get clashoo.config.bypass_china_ipv6 2>/dev/null || true)" +[ -n "$bypass_china_ipv6" ] || bypass_china_ipv6="$bypass_china" + +[ -n "$url4" ] || url4='https://ispip.clang.cn/all_cn.txt' +[ -n "$url6" ] || url6='https://ispip.clang.cn/all_cn_ipv6.txt' + +mkdir -p "$NFT_DIR" + +log '开始更新大陆白名单' + +download_with_fallback "$url4" "$TMP_V4" +[ -s "$TMP_V4" ] || { + log '大陆 IPv4 白名单下载失败:返回为空' + exit 1 +} +changed=0 + +render_nft_set "$TMP_V4" "$OUT_V4" clashoo_china ipv4_addr +if cmp -s "$OUT_V4" "$TARGET_V4"; then + log '大陆 IPv4 白名单内容无变化' +else + mv "$OUT_V4" "$TARGET_V4" + chmod 644 "$TARGET_V4" >/dev/null 2>&1 || true + changed=1 + log '大陆 IPv4 白名单更新完成' +fi + +if download_with_fallback "$url6" "$TMP_V6"; then + if [ -s "$TMP_V6" ]; then + render_nft_set "$TMP_V6" "$OUT_V6" clashoo_china6 ipv6_addr + if cmp -s "$OUT_V6" "$TARGET_V6"; then + log '大陆 IPv6 白名单内容无变化' + else + mv "$OUT_V6" "$TARGET_V6" + chmod 600 "$TARGET_V6" >/dev/null 2>&1 || true + changed=1 + log '大陆 IPv6 白名单更新完成' + fi + else + log '大陆 IPv6 白名单下载为空,保留原文件' + fi +else + log '大陆 IPv6 白名单下载失败,保留原文件' +fi + +instance_pid() { + local service="$1" + local instance="${2:-}" + local filter + if [ -n "$instance" ]; then + filter="@.${service}.instances.${instance}.pid" + else + filter="@[\"${service}\"].instances.*.pid" + fi + ubus call service list "{\"name\":\"$service\",\"verbose\":true}" 2>/dev/null | + jsonfilter -e "$filter" 2>/dev/null | head -n1 +} + +pid_running() { + [ -n "$1" ] && [ -d "$PROC_ROOT/$1" ] +} + +singbox_pid_owns_private_binary() { + local pid="$1" exe cmdline + pid_running "$pid" || return 1 + exe="$(readlink -f "$PROC_ROOT/$pid/exe" 2>/dev/null)" + case "$exe" in + "$SINGBOX_BIN_DIR/sing-box-stable"|"$SINGBOX_BIN_DIR/sing-box-alpha") return 0 ;; + esac + cmdline="$(tr '\000' ' ' < "$PROC_ROOT/$pid/cmdline" 2>/dev/null)" + case "$cmdline" in + "$SINGBOX_BIN_DIR/sing-box-stable"|"$SINGBOX_BIN_DIR/sing-box-stable "*|"$SINGBOX_BIN_DIR/sing-box-alpha"|"$SINGBOX_BIN_DIR/sing-box-alpha "*) return 0 ;; + esac + return 1 +} + +selected_core_running() { + local name pid + if [ "$(uci -q get clashoo.config.core_type 2>/dev/null)" = "singbox" ]; then + name="clashoo" + pid="$(instance_pid "$name" singbox)" + singbox_pid_owns_private_binary "$pid" + return $? + else + name="clashoo" + fi + pid="$(instance_pid "$name")" + pid_running "$pid" +} + +firewall_managed() { + bool_enabled "$(uci -q get clashoo.config.enable 2>/dev/null)" || return 1 + [ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ] && return 1 + selected_core_running +} + +if [ "$changed" -eq 0 ]; then + log '白名单内容未变,跳过防火墙重载' +elif ! firewall_managed; then + log '未接管防火墙(服务未运行或仅内核模式),仅更新白名单文件' +elif bool_enabled "$bypass_china" || bool_enabled "$bypass_china_ipv6"; then + if "$FW4_SCRIPT" apply >/dev/null 2>&1; then + log "大陆白名单规则已重载(IPv4=${bypass_china:-0},IPv6=${bypass_china_ipv6:-0})" + else + log '大陆白名单规则重载失败' + exit 1 + fi +else + log '大陆 IPv4/IPv6 绕过均未启用,仅更新白名单文件' +fi + +log '大陆白名单更新流程完成' diff --git a/clashoo/patches/001-skip-invalid-fakeip-domains.patch b/clashoo/patches/001-skip-invalid-fakeip-domains.patch new file mode 100644 index 000000000..9e3b042e5 --- /dev/null +++ b/clashoo/patches/001-skip-invalid-fakeip-domains.patch @@ -0,0 +1,91 @@ +--- a/dns/middleware.go ++++ b/dns/middleware.go +@@ -12,6 +12,7 @@ import ( + icontext "github.com/metacubex/mihomo/context" + "github.com/metacubex/mihomo/log" + ++ M "github.com/metacubex/sing/common/metadata" + D "github.com/miekg/dns" + ) + +@@ -152,6 +153,9 @@ func withFakeIP(skipper *fakeip.Skipper, + q := r.Question[0] + + host := strings.TrimRight(q.Name, ".") ++ if !M.IsDomainName(host) { ++ return next(ctx, r) ++ } + if skipper.ShouldSkipped(host) { + return next(ctx, r) + } +--- /dev/null ++++ b/dns/middleware_test.go +@@ -0,0 +1,68 @@ ++package dns ++ ++import ( ++ "context" ++ "net/netip" ++ "testing" ++ ++ "github.com/metacubex/mihomo/component/fakeip" ++ icontext "github.com/metacubex/mihomo/context" ++ ++ D "github.com/miekg/dns" ++ "github.com/stretchr/testify/require" ++) ++ ++func TestWithFakeIPSkipsInvalidDomain(t *testing.T) { ++ pool, err := fakeip.New(fakeip.Options{ ++ IPNet: netip.MustParsePrefix("198.18.0.1/16"), ++ Size: 10, ++ }) ++ require.NoError(t, err) ++ ++ downstreamCalled := false ++ next := func(_ *icontext.DNSContext, request *D.Msg) (*D.Msg, error) { ++ downstreamCalled = true ++ response := new(D.Msg) ++ response.SetReply(request) ++ return response, nil ++ } ++ ++ request := new(D.Msg) ++ request.SetQuestion("n-relay-ipc-txc-nj-00.tplinkcloud.com.cn\\152.", D.TypeA) ++ response, err := withFakeIP(&fakeip.Skipper{}, pool, nil, 1)(next)(icontext.NewDNSContext(context.Background()), request) ++ ++ require.NoError(t, err) ++ require.True(t, downstreamCalled) ++ require.Empty(t, response.Answer) ++ _, mapped := pool.LookBack(netip.MustParseAddr("198.18.0.4")) ++ require.False(t, mapped) ++} ++ ++func TestWithFakeIPKeepsValidDomain(t *testing.T) { ++ pool, err := fakeip.New(fakeip.Options{ ++ IPNet: netip.MustParsePrefix("198.18.0.1/16"), ++ Size: 10, ++ }) ++ require.NoError(t, err) ++ ++ downstreamCalled := false ++ next := func(_ *icontext.DNSContext, request *D.Msg) (*D.Msg, error) { ++ downstreamCalled = true ++ response := new(D.Msg) ++ response.SetReply(request) ++ return response, nil ++ } ++ ++ request := new(D.Msg) ++ request.SetQuestion("n-relay-ipc-txc-nj-00.tplinkcloud.com.cn.", D.TypeA) ++ dnsCtx := icontext.NewDNSContext(context.Background()) ++ response, err := withFakeIP(&fakeip.Skipper{}, pool, nil, 1)(next)(dnsCtx, request) ++ ++ require.NoError(t, err) ++ require.False(t, downstreamCalled) ++ require.Len(t, response.Answer, 1) ++ require.Equal(t, icontext.DNSTypeFakeIP, dnsCtx.Type()) ++ host, mapped := pool.LookBack(netip.MustParseAddr("198.18.0.4")) ++ require.True(t, mapped) ++ require.Equal(t, "n-relay-ipc-txc-nj-00.tplinkcloud.com.cn", host) ++} diff --git a/luci-app-clashoo/Makefile b/luci-app-clashoo/Makefile new file mode 100644 index 000000000..357d3e149 --- /dev/null +++ b/luci-app-clashoo/Makefile @@ -0,0 +1,100 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-clashoo +PKG_VERSION:=1.30.0 +PKG_RELEASE:=1 +PKG_MAINTAINER:=kenzok8 +PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) + +include $(INCLUDE_DIR)/package.mk + +define Package/luci-app-clashoo + SECTION:=luci + CATEGORY:=LuCI + SUBMENU:=3. Applications + TITLE:=LuCI app for Clashoo + DEPENDS:=+luci-base +clashoo + PKGARCH:=all + MAINTAINER:=kenzok8 +endef + +define Package/luci-app-clashoo/description + LuCI web interface for Clashoo +endef + +define Build/Configure +endef + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + po2lmo ./po/zh-cn/clashoo.po $(PKG_BUILD_DIR)/clashoo.zh-cn.lmo +endef + +define Build/Compile +endef + +define Package/luci-app-clashoo/install + $(INSTALL_DIR) $(1)/usr/share/rpcd/ucode + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DIR) $(1)/usr/share/clashoo/rpc + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DIR) $(1)/www/luci-static/resources/tools + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/clashoo + $(INSTALL_DIR) $(1)/www/luci-static/resources/cm/mode + $(INSTALL_DIR) $(1)/www/luci-static/resources/cm/addon + $(INSTALL_DIR) $(1)/www/luci-static/clashoo + + $(INSTALL_BIN) ./root/usr/share/rpcd/ucode/luci.clashoo $(1)/usr/share/rpcd/ucode/luci.clashoo + $(INSTALL_BIN) ./root/usr/share/clashoo/rpc/backup_archive.sh $(1)/usr/share/clashoo/rpc/backup_archive.sh + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-clashoo.json $(1)/usr/share/rpcd/acl.d/luci-app-clashoo.json + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-clashoo.json $(1)/usr/share/luci/menu.d/luci-app-clashoo.json + + $(INSTALL_DATA) ./htdocs/luci-static/resources/tools/clashoo.js $(1)/www/luci-static/resources/tools/clashoo.js + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/clashoo/* $(1)/www/luci-static/resources/view/clashoo/ + $(INSTALL_DATA) ./htdocs/luci-static/resources/cm/codemirror.js $(1)/www/luci-static/resources/cm/codemirror.js + $(INSTALL_DATA) ./htdocs/luci-static/resources/cm/codemirror.css $(1)/www/luci-static/resources/cm/codemirror.css + $(INSTALL_DATA) ./htdocs/luci-static/resources/cm/mode/* $(1)/www/luci-static/resources/cm/mode/ + $(INSTALL_DATA) ./htdocs/luci-static/resources/cm/addon/* $(1)/www/luci-static/resources/cm/addon/ + $(INSTALL_DATA) ./htdocs/luci-static/clashoo/logo.png $(1)/www/luci-static/clashoo/logo.png +endef + +define Package/luci-app-clashoo/postinst +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + rm -rf /tmp/luci-indexcache /tmp/luci-modulecache + # reload (not restart) rpcd so the LuCI login session survives the upgrade — + # rpcd restart drops every ubus session and forces a re-login (issue #12). + # reload still picks up the new ucode backend + ACL. uhttpd needs nothing: + # updated static assets are served on the next request. + /etc/init.d/rpcd reload >/dev/null 2>&1 || true +fi +exit 0 +endef + +define Package/luci-i18n-clashoo-zh-cn + SECTION:=luci + CATEGORY:=LuCI + TITLE:=luci-app-clashoo - Simplified Chinese (zh-cn) + DEPENDS:=luci-app-clashoo + DEFAULT:=LUCI_LANG_zh_Hans||(ALL&&m) + HIDDEN:=1 + PKGARCH:=all +endef + +define Package/luci-i18n-clashoo-zh-cn/install + $(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n + $(INSTALL_DATA) $(PKG_BUILD_DIR)/clashoo.zh-cn.lmo $(1)/usr/lib/lua/luci/i18n/clashoo.zh-cn.lmo +endef + +define Package/luci-i18n-clashoo-zh-cn/postinst +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + # just clear the LuCI cache so the new translations load on next request; + # no service restart needed (and none that would drop the login session). + rm -rf /tmp/luci* +fi +exit 0 +endef + +$(eval $(call BuildPackage,luci-app-clashoo)) +$(eval $(call BuildPackage,luci-i18n-clashoo-zh-cn)) diff --git a/luci-app-clashoo/htdocs/luci-static/clashoo/logo.png b/luci-app-clashoo/htdocs/luci-static/clashoo/logo.png new file mode 100644 index 000000000..021e4c692 Binary files /dev/null and b/luci-app-clashoo/htdocs/luci-static/clashoo/logo.png differ diff --git a/luci-app-clashoo/htdocs/luci-static/resources/cm/addon/matchbrackets.js b/luci-app-clashoo/htdocs/luci-static/resources/cm/addon/matchbrackets.js new file mode 100644 index 000000000..a3dc955f9 --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/cm/addon/matchbrackets.js @@ -0,0 +1 @@ +!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],t):t(CodeMirror)}(function(r){var u=/MSIE \d/.test(navigator.userAgent)&&(null==document.documentMode||document.documentMode<8),k=r.Pos,p={"(":")>",")":"(<","[":"]>","]":"[<","{":"}>","}":"{<","<":">>",">":"<<"};function y(t){return t&&t.bracketRegex||/[(){}[\]]/}function f(t,e,n){var r=t.getLineHandle(e.line),i=e.ch-1,c=n&&n.afterCursor,a=(null==c&&(c=/(^| )cm-fat-cursor($| )/.test(t.getWrapperElement().className)),y(n)),c=!c&&0<=i&&a.test(r.text.charAt(i))&&p[r.text.charAt(i)]||a.test(r.text.charAt(i+1))&&p[r.text.charAt(++i)];if(!c)return null;a=">"==c.charAt(1)?1:-1;if(n&&n.strict&&0c))for(s==e.line&&(f=e.ch-(n<0?1:0));f!=m;f+=n){var g=u.charAt(f);if(h.test(g)&&(void 0===r||(t.getTokenTypeAt(k(s,f+1))||"")==(r||""))){var d=p[g];if(d&&">"==d.charAt(1)==0span::selection,.cm-fat-cursor .CodeMirror-line>span>span::selection{background:0 0}.cm-fat-cursor .CodeMirror-line::-moz-selection,.cm-fat-cursor .CodeMirror-line>span::-moz-selection,.cm-fat-cursor .CodeMirror-line>span>span::-moz-selection{background:0 0}.cm-fat-cursor{caret-color:transparent}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:0;position:relative;z-index:0}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none;outline:0}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0} \ No newline at end of file diff --git a/luci-app-clashoo/htdocs/luci-static/resources/cm/codemirror.js b/luci-app-clashoo/htdocs/luci-static/resources/cm/codemirror.js new file mode 100644 index 000000000..9c78bd464 --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/cm/codemirror.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).CodeMirror=t()}(this,function(){"use strict";var e=navigator.userAgent,l=navigator.platform,d=/gecko\/\d/i.test(e),s=/MSIE \d/.test(e),a=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(e),u=/Edge\/(\d+)/.exec(e),w=s||a||u,v=w&&(s?document.documentMode||6:+(u||a)[1]),x=!u&&/WebKit\//.test(e),s=x&&/Qt\/\d+\.\d+/.test(e),m=!u&&/Chrome\/(\d+)/.exec(e),V=m&&+m[1],K=/Opera\//.test(e),j=/Apple Computer/.test(navigator.vendor),c=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(e),X=/PhantomJS/.test(e),Y=j&&(/Mobile\/\w+/.test(e)||2t)return i;o.to==t&&(o.from!=o.to&&"before"==n?r=i:Fe=i),o.from==t&&(o.from!=o.to&&"before"!=n?r=i:Fe=i)}return null!=r?r:Fe}Ee=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,Re=/[stwN]/,ze=/[LRr]/,Ie=/[Lb1n]/,Be=/[1n]/;var Ee,Re,ze,Ie,Be,Ge=function(e,t){var n="ltr"==t?"L":"R";if(0==e.length||"ltr"==t&&!Ee.test(e))return!1;for(var r,i=e.length,o=[],l=0;l=e.size)throw new Error("There is no line "+(t+e.first)+" in the document.");for(var n=e;!n.lines;)for(var r=0;;++r){var i=n.children[r],o=i.chunkSize();if(t=e.first&&tn?F(n,W(e,n).text.length):(e=W(e,(n=t).line).text.length,null==(t=n.ch)||e=this.string.length},g.prototype.sol=function(){return this.pos==this.lineStart},g.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},g.prototype.next=function(){if(this.post},g.prototype.eatSpace=function(){for(var e=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e},g.prototype.skipToEnd=function(){this.pos=this.string.length},g.prototype.skipTo=function(e){e=this.string.indexOf(e,this.pos);if(-1e.options.maxHighlightLength&&ft(e.doc.mode,r.state),o=At(e,t,r),i&&(r.state=i),t.stateAfter=r.save(!i),t.styles=o.styles,o.classes?t.styleClasses=o.classes:t.styleClasses&&(t.styleClasses=null),n===e.doc.highlightFrontier&&(e.doc.modeFrontier=Math.max(e.doc.modeFrontier,++e.doc.highlightFrontier))),t.styles}function Wt(n,r,e){var t=n.doc,i=n.display;if(!t.mode.startState)return new Ot(t,!0,r);var o=function(e,t,n){for(var r,i,o=e.doc,l=n?-1:t-(e.doc.mode.innerMode?1e3:100),s=t;lt.first&&W(t,o-1).stateAfter,s=l?Ot.fromSaved(t,l,o):new Ot(t,gt(t.mode),o);return t.iter(o,r,function(e){Ht(n,e.text,s);var t=s.line;e.stateAfter=t==r-1||t%5==0||t>=i.viewFrom&&tt.start)return o}throw new Error("Mode "+e.name+" failed to advance stream.")}Ot.prototype.lookAhead=function(e){var t=this.doc.getLine(this.line+e);return null!=t&&e>this.maxLookAhead&&(this.maxLookAhead=e),t},Ot.prototype.baseToken=function(e){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=e;)this.baseTokenPos+=2;var t=this.baseTokens[this.baseTokenPos+1];return{type:t&&t.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-e}},Ot.prototype.nextLine=function(){this.line++,0e.options.maxHighlightLength?(s=!1,l&&Ht(e,t,r,c.pos),c.pos=t.length,null):zt(Pt(n,c,r.state,h),o);if(!h||(d=h[0].name)&&(f="m-"+(f?d+" "+f:d)),!s||u!=f){for(;a=t:l.to>t),(r=r||[]).push(new Ut(s,l.from,o?null:l.to)))}return r}(n,r,o),s=function(e,t,n){var r;if(e)for(var i=0;i=t:l.to>t))&&(l.from!=t||"bookmark"!=s.type||n&&!l.marker.insertLeft)||(o=null==l.from||(s.inclusiveLeft?l.from<=t:l.frome.lastLine())return t;var n,r=W(e,t);if(!on(e,r))return t;for(;n=Jt(r);)r=n.find(1,!0).line;return H(r)+1}function on(e,t){var n=Gt&&t.markedSpans;if(n)for(var r,i=0;in.maxLineLength&&(n.maxLineLength=t,n.maxLine=e)})}var un=function(e,t,n){this.text=e,Yt(this,t),this.height=n?n(this):1};un.prototype.lineNo=function(){return H(this)},$e(un);var cn={},hn={};function dn(e,t){if(!e||/^\s*$/.test(e))return null;t=t.addModeClass?hn:cn;return t[e]||(t[e]=e.replace(/\S+/g,"cm-$&"))}function fn(e,t){var n=ne("span",null,null,x?"padding-right: .1px":null),r={pre:ne("pre",[n],"CodeMirror-line"),content:n,col:0,pos:0,cm:e,trailingSpace:!1,splitSpaces:e.getOption("lineWrapping")};t.measure={};for(var i=0;i<=(t.rest?t.rest.length:0);i++){var o=i?t.rest[i-1]:t.line,l=void 0,l=(r.pos=0,r.addToken=gn,function(e){if(null!=tt)return tt;var t=y(e,document.createTextNode("AخA")),n=le(t,0,1).getBoundingClientRect(),t=le(t,1,2).getBoundingClientRect();return te(e),n&&n.left!=n.right&&(tt=t.right-n.right<3)}(e.display.measure)&&(l=Ve(o,e.doc.direction))&&(r.addToken=function(h,d){return function(e,t,n,r,i,o,l){n=n?n+" cm-force-border":"cm-force-border";for(var s=e.pos,a=s+t.length;;){for(var u=void 0,c=0;cs&&u.from<=s);c++);if(u.to>=a)return h(e,t,n,r,i,o,l);h(e,t.slice(0,u.to-s),n,r,null,o,l),r=null,t=t.slice(u.to-s),s=u.to}}}(r.addToken,l)),r.map=[],t!=e.display.externalMeasured&&H(o));!function(e,t,n){var r=e.markedSpans,i=e.text,o=0;if(r)for(var l,s,a,u,c,h,d,f=i.length,p=0,g=1,m="",v=0;;){if(v==p){a=u=c=s="",h=d=null,v=1/0;for(var y=[],b=void 0,w=0;wp||C.collapsed&&x.to==p&&x.from==p)){if(null!=x.to&&x.to!=p&&v>x.to&&(v=x.to,u=""),C.className&&(a+=" "+C.className),C.css&&(s=(s?s+";":"")+C.css),C.startStyle&&x.from==p&&(c+=" "+C.startStyle),C.endStyle&&x.to==v&&(b=b||[]).push(C.endStyle,x.to),C.title&&((d=d||{}).title=C.title),C.attributes)for(var S in C.attributes)(d=d||{})[S]=C.attributes[S];C.collapsed&&(!h||qt(h.marker,C)<0)&&(h=x)}else x.from>p&&v>x.from&&(v=x.from)}if(b)for(var L=0;Ln)return{map:e.measure.maps[i],cache:e.measure.caches[i],before:!0}}}function zn(e,t,n,r){return Gn(e,Bn(e,t),n,r)}function In(e,t){if(t>=e.display.viewFrom&&t=e.lineN&&tt)&&(i=(o=a-s)-1,a<=t&&(l="right")),null!=i){if(r=e[u+2],s==a&&n==(r.insertLeft?"left":"right")&&(l=n),"left"==n&&0==i)for(;u&&e[u-2]==e[u-3]&&e[u-1].insertLeft;)r=e[2+(u-=3)],l="left";if("right"==n&&i==a-s)for(;u=i.text.length?(t=i.text.length,e="before"):t<=0&&(t=0,e="after"),!a)return s("before"==e?t-1:t,"before"==e);function u(e,t,n){return s(n?e-1:e,1==a[t].level!=n)}var c=Pe(a,t,e),h=Fe,c=u(t,c,"before"==e);return null!=h&&(c.other=u(t,h,"before"!=e)),c}function tr(e,t){var n=0,t=(t=E(e.doc,t),e.options.lineWrapping||(n=cr(e.display)*t.ch),W(e.doc,t.line)),e=ln(t)+Dn(e.display);return{left:n,right:n,top:e,bottom:e+t.height}}function nr(e,t,n,r,i){e=F(e,t,n);return e.xRel=i,r&&(e.outside=r),e}function rr(e,t,n){var r=e.doc;if((n+=e.display.viewOffset)<0)return nr(r.first,0,null,-1,-1);var i=bt(r,n),o=r.first+r.size-1;if(o=a.bottom?1:0)}return c=We(e.text,c,1),nr(t,c,g,f,r-p)}(e,l,i,t,n),a=function(e,t){var n,r=Gt&&e.markedSpans;if(r)for(var i=0;it)&&(!n||qt(n,o.marker)<0)&&(n=o.marker)}return n}(l,s.ch+(0r},i,e)}}function or(e,t,n,r){return ir(e,t,n=n||Bn(e,t),Zn(e,t,Gn(e,n,r),"line").top)}function lr(e,t,n,r){return!(e.bottom<=n)&&(e.top>n||(r?e.left:e.right)>t)}function sr(n,r,i,o,l,s,a){var e,t=He(function(e){var e=l[e],t=1!=e.level;return lr(er(n,F(i,t?e.to:e.from,t?"before":"after"),"line",r,o),s,a,!0)},0,l.length-1),u=l[t];return 0a&&(u=l[t-1])),u}function ar(e,t,n,r,i,o,l){for(var l=ir(e,t,r,l),s=l.begin,a=l.end,u=(/\s/.test(t.text.charAt(a-1))&&a--,null),c=null,h=0;h=a||f.to<=s||(d=(d=Gn(e,r,1!=f.level?Math.min(a,f.to)-1:Math.max(s,f.from)).right)a?{from:u.from,to:a,level:u.level}:u}function ur(e){if(null!=e.cachedTextHeight)return e.cachedTextHeight;if(null==Un){Un=M("pre",null,"CodeMirror-line-like");for(var t=0;t<49;++t)Un.appendChild(document.createTextNode("x")),Un.appendChild(M("br"));Un.appendChild(document.createTextNode("x"))}y(e.measure,Un);var n=Un.offsetHeight/50;return 3=e.display.viewTo)return null;if((t-=e.display.viewFrom)<0)return null;for(var n=e.display.view,r=0;rt)&&(o.updateLineNumbers=t),e.curOp.viewChanged=!0,t>=o.viewTo?Gt&&nn(e.doc,t)o.viewFrom?yr(e):(o.viewFrom+=r,o.viewTo+=r):t<=o.viewFrom&&n>=o.viewTo?yr(e):t<=o.viewFrom?(l=br(e,n,n+r,1))?(o.view=o.view.slice(l.index),o.viewFrom=l.lineN,o.viewTo+=r):yr(e):n>=o.viewTo?(l=br(e,t,t,-1))?(o.view=o.view.slice(0,l.index),o.viewTo=l.lineN):yr(e):(l=br(e,t,t,-1),i=br(e,n,n+r,1),l&&i?(o.view=o.view.slice(0,l.index).concat(yn(e,l.lineN,i.lineN)).concat(o.view.slice(i.index)),o.viewTo+=r):yr(e)),o.externalMeasured);l&&(n=i.lineN&&t=r.viewTo||null!=(i=r.view[mr(e,t)]).node&&-1==L(r=i.changes||(i.changes=[]),n)&&r.push(n)}function yr(e){e.display.viewFrom=e.display.viewTo=e.doc.first,e.display.view=[],e.display.viewOffset=0}function br(e,t,n,r){var i,o=mr(e,t),l=e.display.view;if(!Gt||n==e.doc.first+e.doc.size)return{index:o,lineN:n};for(var s=e.display.viewFrom,a=0;a=e.display.viewTo||s.to().linet||t==n&&l.to==t)&&(r(Math.max(l.from,t),Math.min(l.to,n),1==l.level?"rtl":"ltr",o),i=!0)}i||r(t,n,"ltr")}(C,g||0,null==m?b:m,function(e,t,n,r){var i,o,l,s,a,u="ltr"==n,c=w(e,u?"left":"right"),h=w(t-1,u?"right":"left"),d=null==g&&0==e,f=null==m&&t==b,p=0==r,r=!C||r==C.length-1;h.top-c.top<=3?(i=(k?d:f)&&p?S:(u?c:h).left,a=(k?f:d)&&r?L:(u?h:c).right,T(i,c.top,a-i,c.bottom)):(a=u?(o=k&&d&&p?S:c.left,l=k?L:x(e,n,"before"),s=k?S:x(t,n,"after"),k&&f&&r?L:h.right):(o=k?x(e,n,"before"):S,l=!k&&d&&p?L:c.right,s=!k&&f&&r?S:h.left,k?x(t,n,"after"):L),T(o,c.top,l-o,c.bottom),c.bottome.display.sizerWidth&&((a=Math.ceil(c/cr(e.display)))>e.display.maxLineLength&&(e.display.maxLineLength=a,e.display.maxLine=s.line,e.display.maxLineChanged=!0))}}2=o&&(i=bt(t,ln(W(t,n))-e.wrapper.clientHeight),o=n)),{from:i,to:Math.max(o,i+1)}}function Hr(e,t){var n=e.display,r=ur(e.display),i=(t.top<0&&(t.top=0),(e.curOp&&null!=e.curOp.scrollTop?e.curOp:n.scroller).scrollTop),o=En(e),l={},s=(t.bottom-t.top>o&&(t.bottom=t.top+o),e.doc.height+Wn(n)),a=t.tops-r,r=(t.topi+o&&((a=Math.min(t.top,(r?s:t.bottom)-o))!=i&&(l.scrollTop=a)),e.options.fixedGutter?0:n.gutters.offsetWidth),s=e.curOp&&null!=e.curOp.scrollLeft?e.curOp.scrollLeft:n.scroller.scrollLeft-r,o=Pn(e)-n.gutters.offsetWidth,i=t.right-t.left>o;return i&&(t.right=t.left+o),t.left<10?l.scrollLeft=0:t.lefto+s-3&&(l.scrollLeft=t.right+(i?0:10)-o),l}function Fr(e,t){null!=t&&(Rr(e),e.curOp.scrollTop=(null==e.curOp.scrollTop?e.doc:e.curOp).scrollTop+t)}function Pr(e){Rr(e);var t=e.getCursor();e.curOp.scrollToPos={from:t,to:t,margin:e.options.cursorScrollMargin}}function Er(e,t,n){null==t&&null==n||Rr(e),null!=t&&(e.curOp.scrollLeft=t),null!=n&&(e.curOp.scrollTop=n)}function Rr(e){var t=e.curOp.scrollToPos;t&&(e.curOp.scrollToPos=null,zr(e,tr(e,t.from),tr(e,t.to),t.margin))}function zr(e,t,n,r){t=Hr(e,{left:Math.min(t.left,n.left),top:Math.min(t.top,n.top)-r,right:Math.max(t.right,n.right),bottom:Math.max(t.bottom,n.bottom)+r});Er(e,t.scrollLeft,t.scrollTop)}function Ir(e,t){Math.abs(e.doc.scrollTop-t)<2||(d||ri(e,{top:t}),Br(e,t,!0),d&&ri(e),Qr(e,100))}function Br(e,t,n){t=Math.max(0,Math.min(e.display.scroller.scrollHeight-e.display.scroller.clientHeight,t)),e.display.scroller.scrollTop==t&&!n||(e.doc.scrollTop=t,e.display.scrollbars.setScrollTop(t),e.display.scroller.scrollTop!=t&&(e.display.scroller.scrollTop=t))}function Gr(e,t,n,r){t=Math.max(0,Math.min(t,e.display.scroller.scrollWidth-e.display.scroller.clientWidth)),(n?t==e.doc.scrollLeft:Math.abs(e.doc.scrollLeft-t)<2)&&!r||(e.doc.scrollLeft=t,li(e),e.display.scroller.scrollLeft!=t&&(e.display.scroller.scrollLeft=t),e.display.scrollbars.setScrollLeft(t))}function Ur(e){var t=e.display,n=t.gutters.offsetWidth,r=Math.round(e.doc.height+Wn(e.display));return{clientHeight:t.scroller.clientHeight,viewHeight:t.wrapper.clientHeight,scrollWidth:t.scroller.scrollWidth,clientWidth:t.scroller.clientWidth,viewWidth:t.wrapper.clientWidth,barLeft:e.options.fixedGutter?n:0,docHeight:r,scrollHeight:r+Fn(e)+t.barHeight,nativeBarWidth:t.nativeBarWidth,gutterWidth:n}}function Vr(e,t,n){this.cm=n;var r=this.vert=M("div",[M("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),i=this.horiz=M("div",[M("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");r.tabIndex=i.tabIndex=-1,e(r),e(i),k(r,"scroll",function(){r.clientHeight&&t(r.scrollTop,"vertical")}),k(i,"scroll",function(){i.clientWidth&&t(i.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,w&&v<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")}function Kr(){}Vr.prototype.update=function(e){var t,n=e.scrollWidth>e.clientWidth+1,r=e.scrollHeight>e.clientHeight+1,i=e.nativeBarWidth;return r?(this.vert.style.display="block",this.vert.style.bottom=n?i+"px":"0",t=e.viewHeight-(n?i:0),this.vert.firstChild.style.height=Math.max(0,e.scrollHeight-e.clientHeight+t)+"px"):(this.vert.scrollTop=0,this.vert.style.display="",this.vert.firstChild.style.height="0"),n?(this.horiz.style.display="block",this.horiz.style.right=r?i+"px":"0",this.horiz.style.left=e.barLeft+"px",t=e.viewWidth-e.barLeft-(r?i:0),this.horiz.firstChild.style.width=Math.max(0,e.scrollWidth-e.clientWidth+t)+"px"):(this.horiz.style.display="",this.horiz.firstChild.style.width="0"),!this.checkedZeroWidth&&0=l.viewTo)||l.maxLineChanged&&o.options.lineWrapping,i.update=i.mustUpdate&&new ei(o,i.mustUpdate&&{top:i.scrollTop,ensure:i.scrollToPos},i.forceUpdate)}for(var s=0;s(i.defaultView.innerHeight||i.documentElement.clientHeight)&&(r=!1),null==r||X||(o=M("div","​",null,"position: absolute;\n top: "+(t.top-n.viewOffset-Dn(e.display))+"px;\n height: "+(t.bottom-t.top+Fn(e)+n.barHeight)+"px;\n left: "+t.left+"px; width: "+Math.max(2,t.right-t.left)+"px;"),e.display.lineSpace.appendChild(o),o.scrollIntoView(r),e.display.lineSpace.removeChild(o)))}(w,v));var S=b.maybeHiddenMarkers,L=b.maybeUnhiddenMarkers;if(S)for(var k=0;k=l.display.viewTo||(s=+new Date+l.options.workTime,a=Wt(l,c.highlightFrontier),u=[],c.iter(a.line,Math.min(c.first+c.size,l.display.viewTo+500),function(e){if(a.line>=l.display.viewFrom){for(var t=e.styles,n=e.text.length>l.options.maxHighlightLength?ft(c.mode,a.state):null,r=At(l,e,a,!0),n=(n&&(a.state=n),e.styles=r.styles,e.styleClasses),r=r.classes,i=(r?e.styleClasses=r:n&&(e.styleClasses=null),!t||t.length!=e.styles.length||n!=r&&(!n||!r||n.bgClass!=r.bgClass||n.textClass!=r.textClass)),o=0;!i&&os)return Qr(l,l.options.workDelay),!0}),c.highlightFrontier=a.line,c.modeFrontier=Math.max(c.modeFrontier,a.line),u.length&&h(l,function(){for(var e=0;e=n.viewFrom&&t.visible.to<=n.viewTo&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo)&&n.renderedView==n.view&&0==wr(e))return!1;si(e)&&(yr(e),t.dims=hr(e));var i=r.first+r.size,o=Math.max(t.visible.from-e.options.viewportMargin,r.first),l=Math.min(i,t.visible.to+e.options.viewportMargin),r=(n.viewFroml&&n.viewTo-l<20&&(l=Math.min(i,n.viewTo)),Gt&&(o=nn(e.doc,o),l=rn(e.doc,l)),o!=n.viewFrom||l!=n.viewTo||n.lastWrapHeight!=t.wrapperHeight||n.lastWrapWidth!=t.wrapperWidth),i=(i=o,o=l,0==(c=(l=e).display).view.length||i>=c.viewTo||o<=c.viewFrom?(c.view=yn(l,i,o),c.viewFrom=i):(c.viewFrom>i?c.view=yn(l,i,c.viewFrom).concat(c.view):c.viewFromo&&(c.view=c.view.slice(0,mr(l,o)))),c.viewTo=o,n.viewOffset=ln(W(e.doc,n.viewFrom)),e.display.mover.style.top=n.viewOffset+"px",wr(e));if(!r&&0==i&&!t.force&&n.renderedView==n.view&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo))return!1;var l=function(e){if(e.hasFocus())return null;if(!(n=N(ue(e)))||!re(e.display.lineDiv,n))return null;var t,n={activeElt:n};return window.getSelection&&(t=he(e).getSelection()).anchorNode&&t.extend&&re(e.display.lineDiv,t.anchorNode)&&(n.anchorNode=t.anchorNode,n.anchorOffset=t.anchorOffset,n.focusNode=t.focusNode,n.focusOffset=t.focusOffset),n}(e),s=(4=e.display.viewFrom&&t.visible.to<=e.display.viewTo)break;if(!ti(e,t))break;Ar(e);var i=Ur(e);xr(e),jr(e,i),oi(e,i),t.force=!1}t.signal(e,"update",e),e.display.viewFrom==e.display.reportedViewFrom&&e.display.viewTo==e.display.reportedViewTo||(t.signal(e,"viewportChange",e,e.display.viewFrom,e.display.viewTo),e.display.reportedViewFrom=e.display.viewFrom,e.display.reportedViewTo=e.display.viewTo)}function ri(e,t){var n,t=new ei(e,t);ti(e,t)&&(Ar(e),ni(e,t),n=Ur(e),xr(e),jr(e,n),oi(e,n),t.finish())}function ii(e){var t=e.gutters.offsetWidth;e.sizer.style.marginLeft=t+"px",b(e,"gutterChanged",e)}function oi(e,t){e.display.sizer.style.minHeight=t.docHeight+"px",e.display.heightForcer.style.top=t.docHeight+"px",e.display.gutters.style.height=t.docHeight+e.display.barHeight+Fn(e)+"px"}function li(e){var t=e.display,n=t.view;if(t.alignWidgets||t.gutters.firstChild&&e.options.fixedGutter){for(var r=dr(t)-t.scroller.scrollLeft+e.doc.scrollLeft,i=t.gutters.offsetWidth,o=r+"px",l=0;ll.clientWidth,a=l.scrollHeight>l.clientHeight;if(r&&s||n&&a){if(n&&C&&x)e:for(var u=t.target,c=o.view;u!=l;u=u.parentNode)for(var h=0;hs-(e.cm?e.cm.options.historyEventDelay:500)||"*"==t.origin.charAt(0)))&&(o=(o=l).lastOp==r?(Wi(o.done),z(o.done)):o.done.length&&!z(o.done).ranges?z(o.done):1l.undoDepth;)l.done.shift(),l.done[0].ranges||l.done.shift()}l.done.push(n),l.generation=++l.maxGeneration,l.lastModTime=l.lastSelTime=s,l.lastOp=l.lastSelOp=r,l.lastOrigin=l.lastSelOrigin=t.origin,i||O(e,"historyAdded")}function Fi(e,t,n,r){var i,o,l,s=e.history,a=r&&r.origin;n==s.lastSelOp||a&&s.lastSelOrigin==a&&(s.lastModTime==s.lastSelTime&&s.lastOrigin==a||(e=e,i=a,o=z(s.done),l=t,"*"==(i=i.charAt(0))||"+"==i&&o.ranges.length==l.ranges.length&&o.somethingSelected()==l.somethingSelected()&&new Date-e.history.lastSelTime<=(e.cm?e.cm.options.historyEventDelay:500)))?s.done[s.done.length-1]=t:Pi(t,s.done),s.lastSelTime=+new Date,s.lastSelOrigin=a,s.lastSelOp=n,r&&!1!==r.clearRedo&&Wi(s.undone)}function Pi(e,t){var n=z(t);n&&n.ranges&&n.equals(e)||t.push(e)}function Ei(t,n,e,r){var i=n["spans_"+t.id],o=0;t.iter(Math.max(t.first,e),Math.min(t.first+t.size,r),function(e){e.markedSpans&&((i=i||(n["spans_"+t.id]={}))[o]=e.markedSpans),++o})}function Ri(e,t){var n=t["spans_"+e.id];if(!n)return null;for(var r=[],i=0;i=t.ch:s.to>t.ch))){if(i&&(O(a,"beforeCursorEnter"),a.explicitlyCleared)){if(o.markedSpans){--l;continue}break}if(a.atomic){if(n){var s=a.find(r<0?1:-1),h=void 0;if((s=(r<0?c:u)?Qi(e,s,-r,s&&s.line==t.line?o:null):s)&&s.line==t.line&&(h=P(s,n))&&(r<0?h<0:0e.first?E(e,F(t.line-1)):null:0e.lastLine())){t.from.linei?{from:t.from,to:F(i,W(e,i).text.length),text:[t.text[0]],origin:t.origin}:t).removed=mt(e,t.from,t.to),n=n||xi(e,t),e.cm){var i=e.cm,o=t,l=r,s=i.doc,a=i.display,u=o.from,c=o.to,h=!1,d=u.line,f=(i.options.lineWrapping||(d=H(tn(W(s,u.line))),s.iter(d,c.line+1,function(e){if(e==a.maxLine)return h=!0})),-1a.maxLineLength&&(a.maxLine=e,a.maxLineLength=t,a.maxLineChanged=!0,h=!1)}),h&&(i.curOp.updateMaxLine=!0)),s),p=u.line;if(f.modeFrontier=Math.min(f.modeFrontier,p),!(f.highlightFrontiert.display.maxLineLength&&(t.display.maxLine=u,t.display.maxLineLength=c,t.display.maxLineChanged=!0)}null!=r&&t&&this.collapsed&&R(t,r,i+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,t&&$i(t.doc)),t&&b(t,"markerCleared",t,this,r,i),n&&Zr(t),this.parent&&this.parent.clear()}},mo.prototype.find=function(e,t){var n,r;null==e&&"bookmark"==this.type&&(e=1);for(var i=0;i=e.ch)&&t.push(i.marker.parent||i.marker)}return t},findMarks:function(i,o,l){i=E(this,i),o=E(this,o);var s=[],a=i.line;return this.iter(i.line,o.line+1,function(e){var t=e.markedSpans;if(t)for(var n=0;n=r.to||null==r.from&&a!=i.line||null!=r.from&&a==o.line&&r.from>=o.ch||l&&!l(r.marker)||s.push(r.marker.parent||r.marker)}++a}),s},getAllMarks:function(){var r=[];return this.iter(function(e){var t=e.markedSpans;if(t)for(var n=0;nt&&(t=e.from),null!=e.to&&e.toe.text.length?null:t}function Ko(e,t,n){e=Vo(e,t.ch,n);return null==e?null:new F(t.line,e,n<0?"after":"before")}function jo(e,t,n,r,i){if(e){"rtl"==t.doc.direction&&(i=-i);var o,l,s,a,e=Ve(n,t.doc.direction);if(e)return o=i<0==(1==(e=i<0?z(e):e[0]).level)?"after":"before",0=n.text.length?(s.ch=n.text.length,s.sticky="before"):s.ch<=0&&(s.ch=0,s.sticky="after");var r=Pe(a,s.ch,s.sticky),i=a[r];if("ltr"==t.doc.direction&&i.level%2==0&&(0s.ch:i.from=i.from&&d>=c.begin))return new F(s.line,d,h?"before":"after")}function f(e,t,n){for(var r=function(e,t){return t?new F(s.line,u(e,1),"before"):new F(s.line,e,"after")};0<=e&&el.doc.first&&((n=W(l.doc,e.line-1).text)&&(e=new F(e.line,1),l.replaceRange(t.charAt(0)+l.doc.lineSeparator()+n.charAt(n.length-1),F(e.line-1,n.length-1),e,"+transpose")))),i.push(new G(e,e)));l.setSelections(i)})},newlineAndIndent:function(r){return h(r,function(){for(var e=(t=r.listSelections()).length-1;0<=e;e--)r.replaceRange(r.doc.lineSeparator(),t[e].anchor,t[e].head,"+input");for(var t=r.listSelections(),n=0;nc&&t.push(new G(F(s,c),F(s,we(u,l,n))))}t.length||t.push(new G(f,f)),U(g,vi(d,y.ranges.slice(0,v).concat(t),v),{origin:"*mouse",scroll:!1}),d.scrollIntoView(e)}else{var h,r=m,i=ul(d,e,p.unit),e=r.anchor,e=0=n.to||o.linea.bottom?20:0)&&setTimeout(I(d,function(){u==i&&(l.scroller.scrollTop+=r,e(t))}),50))}:n)(e)}),i=I(d,n);d.state.selectingText=i,k(l.wrapper.ownerDocument,"mousemove",r),k(l.wrapper.ownerDocument,"mouseup",i)})(i,s,o,a)):Qe(e)==h.scroller&&D(e):2==n?(t&&Gi(c.doc,t),setTimeout(function(){return h.input.focus()},20)):3==n&&(Q?c.display.input.onContextMenu(e):Mr(c)))))}function ul(e,t,n){if("char"==n)return new G(t,t);if("word"==n)return e.findWordAt(t);if("line"==n)return new G(F(t.line,0),E(e.doc,F(t.line+1,0)));n=n(e,t);return new G(n.from,n.to)}function cl(e,t,n,r){var i,o;if(t.touches)i=t.touches[0].clientX,o=t.touches[0].clientY;else try{i=t.clientX,o=t.clientY}catch(e){return!1}if(i>=Math.floor(e.display.gutters.getBoundingClientRect().right))return!1;r&&D(t);var l=e.display,r=l.lineDiv.getBoundingClientRect();if(o>r.bottom||!Ye(e,n))return qe(t);o-=r.top-l.viewOffset;for(var s=0;s=i)return O(e,n,e,bt(e.doc,o),e.display.gutterSpecs[s].className,t),qe(t)}}function hl(e,t){return cl(e,t,"gutterClick",!0)}function dl(e,t){var n,r;An(e.display,t)||(r=t,Ye(n=e,"gutterContextMenu")&&cl(n,r,"gutterContextMenu",!1))||A(e,t,"contextmenu")||Q||e.display.input.onContextMenu(t)}function fl(e){e.display.wrapper.className=e.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+e.options.theme.replace(/(^|\s)\s*/g," cm-s-"),Yn(e)}ol.prototype.compare=function(e,t,n){return this.time+400>e&&0==P(t,this.pos)&&n==this.button};var pl={toString:function(){return"CodeMirror.Init"}},gl={},ml={};function vl(e,t,n){!t!=!(n&&n!=pl)&&(n=e.display.dragFunctions,(t=t?k:T)(e.display.scroller,"dragstart",n.start),t(e.display.scroller,"dragenter",n.enter),t(e.display.scroller,"dragover",n.over),t(e.display.scroller,"dragleave",n.leave),t(e.display.scroller,"drop",n.drop))}function yl(e){e.options.lineWrapping?(ie(e.display.wrapper,"CodeMirror-wrap"),e.display.sizer.style.minWidth="",e.display.sizerWidth=null):(ee(e.display.wrapper,"CodeMirror-wrap"),an(e)),pr(e),R(e),Yn(e),setTimeout(function(){return jr(e)},100)}function p(e,t){var n=this;if(!(this instanceof p))return new p(e,t);this.options=t=t?fe(t):{},fe(gl,t,!1);var r,i=t.value,o=("string"==typeof i?i=new f(i,t.mode,null,t.lineSeparator,t.direction):t.mode&&(i.modeOption=t.mode),this.doc=i,new p.inputStyles[t.inputStyle](this)),e=this.display=new hi(e,i,o,t),l=(fl(e.wrapper.CodeMirror=this),t.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),$r(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:-1,cutIncoming:-1,selectingText:!1,draggingText:!1,highlight:new pe,keySeq:null,specialChars:null},t.autofocus&&!_&&e.input.focus(),w&&v<11&&setTimeout(function(){return n.display.input.reset(!0)},20),this),s=l.display;k(s.scroller,"mousedown",I(l,al)),k(s.scroller,"dblclick",w&&v<11?I(l,function(e){var t;A(l,e)||(!(t=gr(l,e))||hl(l,e)||An(l.display,e)||(D(e),e=l.findWordAt(t),Gi(l.doc,e.anchor,e.head)))}):function(e){return A(l,e)||D(e)}),k(s.scroller,"contextmenu",function(e){return dl(l,e)}),k(s.input.getField(),"contextmenu",function(e){s.scroller.contains(e.target)||dl(l,e)});var a,u={end:0};function c(){s.activeTouch&&(a=setTimeout(function(){return s.activeTouch=null},1e3),(u=s.activeTouch).end=+new Date)}function h(e,t){if(null==t.left)return 1;var n=t.left-e.left,t=t.top-e.top;return 400o.first?S(W(o,t-1).text,null,l):0:"add"==n?c=a+e.options.indentUnit:"subtract"==n?c=a-e.options.indentUnit:"number"==typeof n&&(c=a+n);var c=Math.max(0,c),h="",d=0;if(e.options.indentWithTabs)for(var f=Math.floor(c/l);f;--f)d+=l,h+="\t";if(dl,a=rt(t),u=null;if(s&&1l?"cut":"+input")});to(e.doc,f),b(e,"inputRead",e,f)}t&&!s&&kl(e,t),Pr(e),e.curOp.updateInput<2&&(e.curOp.updateInput=h),e.curOp.typing=!0,e.state.pasteIncoming=e.state.cutIncoming=-1}function Ll(e,t){var n=e.clipboardData&&e.clipboardData.getData("Text");return n&&(e.preventDefault(),t.isReadOnly()||t.options.disableInput||!t.hasFocus()||h(t,function(){return Sl(t,n,0,null,"paste")}),1)}function kl(e,t){if(e.options.electricChars&&e.options.smartIndent)for(var n=e.doc.sel,r=n.ranges.length-1;0<=r;r--){var i=n.ranges[r];if(!(100=n.first+n.size||(r=new F(e,r.ch,r.sticky),!(s=W(n,e))))return;r=jo(l,n.cm,s,r.line,a)}else r=t;return 1}if("char"==o||"codepoint"==o)u();else if("column"==o)u(!0);else if("word"==o||"group"==o)for(var c=null,h="group"==o,d=n.cm&&n.cm.getHelper(r,"wordChars"),f=!0;!(i<0)||u(!f);f=!1){var p=s.text.charAt(r.ch)||"\n",p=Ne(p,d)?"w":h&&"\n"==p?"n":!h||/\s/.test(p)?null:"p";if(!h||f||p||(p="s"),c&&c!=p){i<0&&(i=1,u(),r.sticky="after");break}if(p&&(c=p),0=s.height){l.hitSide=!0;break}o+=5*n}return l}function r(e){this.cm=e,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new pe,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null}function Dl(e,t){var n=In(e,t.line);if(!n||n.hidden)return null;var r=W(e.doc,t.line),n=Rn(n,r,t.line),r=Ve(r,e.doc.direction),e="left",r=(r&&(e=Pe(r,t.ch)%2?"right":"left"),Kn(n.map,t.ch,e));return r.offset="right"==r.collapse?r.end:r.start,r}function Wl(e,t){return t&&(e.bad=!0),e}function Hl(e,t,n){var r;if(t==e.display.lineDiv){if(!(r=e.display.lineDiv.childNodes[n]))return Wl(e.clipPos(F(e.display.viewTo-1)),!0);t=null,n=0}else for(r=t;;r=r.parentNode){if(!r||r==e.display.lineDiv)return null;if(r.parentNode&&r.parentNode==e.display.lineDiv)break}for(var i=0;i=t.display.viewTo||n.line=t.display.viewFrom&&Dl(t,r)||{node:i[0].measure.map[2],offset:0},r=n.linet.firstLine()&&(i=F(i.line-1,W(t.doc,i.line-1).length)),r.ch==W(t.doc,r.line).text.length&&r.linen.viewTo-1)return!1;var o,l=i.line==n.viewFrom||0==(l=mr(t,i.line))?(e=H(n.view[0].line),n.view[0].node):(e=H(n.view[l].line),n.view[l-1].node.nextSibling),r=mr(t,r.line),n=r==n.view.length-1?(o=n.viewTo-1,n.lineDiv.lastChild):(o=H(n.view[r+1].line)-1,n.view[r+1].node.previousSibling);if(!l)return!1;for(var s=t.doc.splitLines(function(o,e,t,l,s){var n="",a=!1,u=o.doc.lineSeparator(),c=!1;function h(){a&&(n+=u,c&&(n+=u),a=c=!1)}function d(e){e&&(h(),n+=e)}for(;!function e(t){if(1==t.nodeType){var n=t.getAttribute("cm-text");if(n)d(n);else if(n=t.getAttribute("cm-marker"))(n=o.findMarks(F(l,0),F(s+1,0),(i=+n,function(e){return e.id==i}))).length&&(n=n[0].find(0))&&d(mt(o.doc,n.from,n.to).join(u));else if("false"!=t.getAttribute("contenteditable")&&(n=/^(pre|div|p|li|table|br)$/i.test(t.nodeName),/^br$/i.test(t.nodeName)||0!=t.textContent.length)){n&&h();for(var r=0;ri.ch&&p.charCodeAt(p.length-c-1)==g.charCodeAt(g.length-c-1);)u--,c++;s[s.length-1]=p.slice(0,p.length-c).replace(/^\u200b+/,""),s[0]=s[0].slice(u).replace(/\u200b+$/,"");r=F(e,u),l=F(o,a.length?z(a).length-c:0);return 1n&&(wl(this,i.head.line,e,!0),n=i.head.line,r==this.doc.sel.primIndex&&Pr(this));else{for(var o=i.from(),i=i.to(),l=Math.max(n,o.line),n=Math.min(this.lastLine(),i.line-(i.ch?0:1))+1,s=l;s>1;if((l?n[2*l-1]:0)>=o)i=l;else{if(!(n[2*l+1]l)&&e.top>t.offsetHeight?a=e.top-t.offsetHeight:e.bottom+t.offsetHeight<=l&&(a=e.bottom),u+t.offsetWidth>o&&(u=o-t.offsetWidth)),t.style.top=a+"px",t.style.left=t.style.right="","right"==i?(u=s.sizer.clientWidth-t.offsetWidth,t.style.right="0px"):("left"==i?u=0:"middle"==i&&(u=(s.sizer.clientWidth-t.offsetWidth)/2),t.style.left=u+"px"),n&&(r=this,l={left:u,top:a,right:u+t.offsetWidth,bottom:a+t.offsetHeight},null!=(l=Hr(r,l)).scrollTop&&Ir(r,l.scrollTop),null!=l.scrollLeft&&Gr(r,l.scrollLeft))},triggerOnKeyDown:t(nl),triggerOnKeyPress:t(il),triggerOnKeyUp:rl,triggerOnMouseDown:t(al),execCommand:function(e){if(Yo.hasOwnProperty(e))return Yo[e].call(null,this)},triggerElectric:t(function(e){kl(this,e)}),findPosH:function(e,t,n,r){for(var i=1,o=(t<0&&(i=-1,t=-t),E(this.doc,e)),l=0;l!?|~^@]/,J=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;function i(e,t,r){return H=e,D=r,t}function d(e,t){var a,r=e.next();if('"'==r||"'"==r)return t.tokenize=(a=r,function(e,t){var r,n=!1;if(U&&"@"==e.peek()&&e.match(J))return t.tokenize=d,i("jsonld-keyword","meta");for(;null!=(r=e.next())&&(r!=a||n);)n=!n&&"\\"==r;return n||(t.tokenize=d),i("string","string")}),t.tokenize(e,t);if("."==r&&e.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/))return i("number","number");if("."==r&&e.match(".."))return i("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(r))return i(r);if("="==r&&e.eat(">"))return i("=>","operator");if("0"==r&&e.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/))return i("number","number");if(/\d/.test(r))return e.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/),i("number","number");if("/"==r)return e.eat("*")?(t.tokenize=K)(e,t):e.eat("/")?(e.skipToEnd(),i("comment","comment")):tt(e,t,1)?(function(e){for(var t,r=!1,n=!1;null!=(t=e.next());){if(!r){if("/"==t&&!n)return;"["==t?n=!0:n&&"]"==t&&(n=!1)}r=!r&&"\\"==t}}(e),e.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/),i("regexp","string-2")):(e.eat("="),i("operator","operator",e.current()));if("`"==r)return(t.tokenize=L)(e,t);if("#"==r&&"!"==e.peek())return e.skipToEnd(),i("meta","meta");if("#"==r&&e.eatWhile(B))return i("variable","property");if("<"==r&&e.match("!--")||"-"==r&&e.match("->")&&!/\S/.test(e.string.slice(0,e.start)))return e.skipToEnd(),i("comment","comment");if(G.test(r))return">"==r&&t.lexical&&">"==t.lexical.type||(e.eat("=")?"!"!=r&&"="!=r||e.eat("="):/[<>*+\-|&?]/.test(r)&&(e.eat(r),">"==r&&e.eat(r))),"?"==r&&e.eat(".")?i("."):i("operator","operator",e.current());if(B.test(r)){e.eatWhile(B);r=e.current();if("."!=t.lastType){if(F.propertyIsEnumerable(r))return i((t=F[r]).type,t.style,r);if("async"==r&&e.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/,!1))return i("async","keyword",r)}return i("variable","variable",r)}}function K(e,t){for(var r,n=!1;r=e.next();){if("/"==r&&n){t.tokenize=d;break}n="*"==r}return i("comment","comment")}function L(e,t){for(var r,n=!1;null!=(r=e.next());){if(!n&&("`"==r||"$"==r&&e.eat("{"))){t.tokenize=d;break}n=!n&&"\\"==r}return i("quasi","string-2",e.current())}function Q(e,t){t.fatArrowAt&&(t.fatArrowAt=null);var r=e.string.indexOf("=>",e.start);if(!(r<0)){!u||(n=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(e.string.slice(e.start,r)))&&(r=n.index);for(var n,a=0,i=!1,o=r-1;0<=o;--o){var c=e.string.charAt(o),s="([{}])".indexOf(c);if(0<=s&&s<3){if(!a){++o;break}if(0==--a){"("==c&&(i=!0);break}}else if(3<=s&&s<6)++a;else if(B.test(c))i=!0;else if(/["'\/`]/.test(c))for(;;--o){if(0==o)return;if(e.string.charAt(o-1)==c&&"\\"!=e.string.charAt(o-2)){o--;break}}else if(i&&!a){++o;break}}i&&!a&&(t.fatArrowAt=o)}}var R={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,this:!0,import:!0,"jsonld-keyword":!0};function X(e,t,r,n,a,i){this.indented=e,this.column=t,this.type=r,this.prev=a,this.info=i,null!=n&&(this.align=n)}function Y(e,t,r,n,a){var i=e.cc;for(c.state=e,c.stream=a,c.marked=null,c.cc=i,c.style=t,e.lexical.hasOwnProperty("align")||(e.lexical.align=!0);;)if((i.length?i.pop():o?x:b)(r,n)){for(;i.length&&i[i.length-1].lex;)i.pop()();return c.marked?c.marked:"variable"==r&&function(e,t){if(W){for(var r=e.localVars;r;r=r.next)if(r.name==t)return 1;for(var n=e.context;n;n=n.prev)for(r=n.vars;r;r=r.next)if(r.name==t)return 1}}(e,n)?"variable-2":t}}var c={state:null,column:null,marked:null,cc:null};function s(){for(var e=arguments.length-1;0<=e;e--)c.cc.push(arguments[e])}function p(){return s.apply(null,arguments),!0}function Z(e,t){for(var r=t;r;r=r.next)if(r.name==e)return 1}function a(e){var t=c.state;if(c.marked="def",W){if(t.context)if("var"==t.lexical.info&&t.context&&t.context.block){var r=function e(t,r){{var n;return r?r.block?(n=e(t,r.prev))?n==r.prev?r:new te(n,r.vars,!0):null:Z(t,r.vars)?r:new te(r.prev,new re(t,r.vars),!1):null}}(e,t.context);if(null!=r)return void(t.context=r)}else if(!Z(e,t.localVars))return void(t.localVars=new re(e,t.localVars));l.globalVars&&!Z(e,t.globalVars)&&(t.globalVars=new re(e,t.globalVars))}}function ee(e){return"public"==e||"private"==e||"protected"==e||"abstract"==e||"readonly"==e}function te(e,t,r){this.prev=e,this.vars=t,this.block=r}function re(e,t){this.name=e,this.next=t}var ne=new re("this",new re("arguments",null));function m(){c.state.context=new te(c.state.context,c.state.localVars,!1),c.state.localVars=ne}function ae(){c.state.context=new te(c.state.context,c.state.localVars,!0),c.state.localVars=null}function k(){c.state.localVars=c.state.context.vars,c.state.context=c.state.context.prev}function v(n,a){function e(){var e=c.state,t=e.indented;if("stat"==e.lexical.type)t=e.lexical.indented;else for(var r=e.lexical;r&&")"==r.type&&r.align;r=r.prev)t=r.indented;e.lexical=new X(t,c.stream.column(),n,null,e.lexical,a)}return e.lex=!0,e}function y(){var e=c.state;e.lexical.prev&&(")"==e.lexical.type&&(e.indented=e.lexical.indented),e.lexical=e.lexical.prev)}function w(r){return function e(t){return t==r?p():";"==r||"}"==t||")"==t||"]"==t?s():p(e)}}function b(e,t){return"var"==e?p(v("vardef",t),qe,w(";"),y):"keyword a"==e?p(v("form"),oe,b,y):"keyword b"==e?p(v("form"),b,y):"keyword d"==e?c.stream.match(/^\s*$/,!1)?p():p(v("stat"),g,w(";"),y):"debugger"==e?p(w(";")):"{"==e?p(v("}"),ae,be,y,k):";"==e?p():"if"==e?("else"==c.state.lexical.info&&c.state.cc[c.state.cc.length-1]==y&&c.state.cc.pop()(),p(v("form"),oe,b,y,Oe)):"function"==e?p(q):"for"==e?p(v("form"),ae,Pe,b,k,y):"class"==e||u&&"interface"==t?(c.marked="keyword",p(v("form","class"==e?e:t),Fe,y)):"variable"==e?u&&"declare"==t?(c.marked="keyword",p(b)):u&&("module"==t||"enum"==t||"type"==t)&&c.stream.match(/^\s*\w/,!1)?(c.marked="keyword","enum"==t?p(Ze):"type"==t?p(We,w("operator"),z,w(";")):p(v("form"),T,w("{"),v("}"),be,y,y)):u&&"namespace"==t?(c.marked="keyword",p(v("form"),x,b,y)):u&&"abstract"==t?(c.marked="keyword",p(b)):p(v("stat"),me):"switch"==e?p(v("form"),oe,w("{"),v("}","switch"),ae,be,y,y,k):"case"==e?p(x,w(":")):"default"==e?p(w(":")):"catch"==e?p(v("form"),m,ie,b,y,k):"export"==e?p(v("stat"),Ge,y):"import"==e?p(v("stat"),Ke,y):"async"==e?p(b):"@"==t?p(x,b):s(v("stat"),x,w(";"),y)}function ie(e){if("("==e)return p(S,w(")"))}function x(e,t){return ce(e,t,!1)}function h(e,t){return ce(e,t,!0)}function oe(e){return"("!=e?s():p(v(")"),g,w(")"),y)}function ce(e,t,r){if(c.state.fatArrowAt==c.stream.start){var n=r?fe:le;if("("==e)return p(m,v(")"),V(S,")"),y,w("=>"),n,k);if("variable"==e)return s(m,T,w("=>"),n,k)}var a,n=r?M:j;return R.hasOwnProperty(e)?p(n):"function"==e?p(q,n):"class"==e||u&&"interface"==t?(c.marked="keyword",p(v("form"),Be,y)):"keyword c"==e||"async"==e?p(r?h:x):"("==e?p(v(")"),g,w(")"),y,n):"operator"==e||"spread"==e?p(r?h:x):"["==e?p(v("]"),Ye,y,n):"{"==e?we(ve,"}",null,n):"quasi"==e?s(se,n):"new"==e?p((a=r,function(e){return"."==e?p(a?pe:de):"variable"==e&&u?p(Ie,a?M:j):s(a?h:x)})):p()}function g(e){return e.match(/[;\}\)\],]/)?s():s(x)}function j(e,t){return","==e?p(g):M(e,t,!1)}function M(e,t,r){var n=0==r?j:M,a=0==r?x:h;return"=>"==e?p(m,r?fe:le,k):"operator"==e?/\+\+|--/.test(t)||u&&"!"==t?p(n):u&&"<"==t&&c.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/,!1)?p(v(">"),V(z,">"),y,n):"?"==t?p(x,w(":"),a):p(a):"quasi"==e?s(se,n):";"!=e?"("==e?we(h,")","call",n):"."==e?p(ke,n):"["==e?p(v("]"),g,w("]"),y,n):u&&"as"==t?(c.marked="keyword",p(z,n)):"regexp"==e?(c.state.lastType=c.marked="operator",c.stream.backUp(c.stream.pos-c.stream.start-1),p(a)):void 0:void 0}function se(e,t){return"quasi"!=e?s():"${"!=t.slice(t.length-2)?p(se):p(g,ue)}function ue(e){if("}"==e)return c.marked="string-2",c.state.tokenize=L,p(se)}function le(e){return Q(c.stream,c.state),s("{"==e?b:x)}function fe(e){return Q(c.stream,c.state),s("{"==e?b:h)}function de(e,t){if("target"==t)return c.marked="keyword",p(j)}function pe(e,t){if("target"==t)return c.marked="keyword",p(M)}function me(e){return":"==e?p(y,b):s(j,w(";"),y)}function ke(e){if("variable"==e)return c.marked="property",p()}function ve(e,t){return"async"==e?(c.marked="property",p(ve)):"variable"!=e&&"keyword"!=c.style?"number"==e||"string"==e?(c.marked=U?"property":c.style+" property",p(A)):"jsonld-keyword"==e?p(A):u&&ee(t)?(c.marked="keyword",p(ve)):"["==e?p(x,E,w("]"),A):"spread"==e?p(h,A):"*"==t?(c.marked="keyword",p(ve)):":"==e?s(A):void 0:(c.marked="property","get"==t||"set"==t?p(ye):(u&&c.state.fatArrowAt==c.stream.start&&(e=c.stream.match(/^\s*:\s*/,!1))&&(c.state.fatArrowAt=c.stream.pos+e[0].length),p(A)))}function ye(e){return"variable"!=e?s(A):(c.marked="property",p(q))}function A(e){return":"==e?p(h):"("==e?s(q):void 0}function V(n,a,i){function o(e,t){var r;return(i?-1"),z):"quasi"==e?s(Ve,I):void 0}function je(e){if("=>"==e)return p(z)}function Me(e){return e.match(/[\}\)\]]/)?p():","==e||";"==e?p(Me):s(Ae,Me)}function Ae(e,t){return"variable"==e||"keyword"==c.style?(c.marked="property",p(Ae)):"?"==t||"number"==e||"string"==e?p(Ae):":"==e?p(z):"["==e?p(w("variable"),xe,w("]"),Ae):"("==e?s(C,Ae):e.match(/[;\}\)\],]/)?void 0:p()}function Ve(e,t){return"quasi"!=e?s():"${"!=t.slice(t.length-2)?p(Ve):p(z,Ee)}function Ee(e){if("}"==e)return c.marked="string-2",c.state.tokenize=L,p(Ve)}function ze(e,t){return"variable"==e&&c.stream.match(/^\s*[?:]/,!1)||"?"==t?p(ze):":"==e?p(z):"spread"==e?p(ze):s(z)}function I(e,t){return"<"==t?p(v(">"),V(z,">"),y,I):"|"==t||"."==e||"&"==t?p(z):"["==e?p(z,w("]"),I):"extends"==t||"implements"==t?(c.marked="keyword",p(z)):"?"==t?p(z,w(":"),z):void 0}function Ie(e,t){if("<"==t)return p(v(">"),V(z,">"),y,I)}function Te(){return s(z,$e)}function $e(e,t){if("="==t)return p(z)}function qe(e,t){return"enum"==t?(c.marked="keyword",p(Ze)):s(T,E,$,_e)}function T(e,t){return u&&ee(t)?(c.marked="keyword",p(T)):"variable"==e?(a(t),p()):"spread"==e?p(T):"["==e?we(Se,"]"):"{"==e?we(Ce,"}"):void 0}function Ce(e,t){return"variable"!=e||c.stream.match(/^\s*:/,!1)?("variable"==e&&(c.marked="property"),"spread"==e?p(T):"}"==e?s():"["==e?p(x,w("]"),w(":"),Ce):p(w(":"),T,$)):(a(t),p($))}function Se(){return s(T,$)}function $(e,t){if("="==t)return p(h)}function _e(e){if(","==e)return p(qe)}function Oe(e,t){if("keyword b"==e&&"else"==t)return p(v("form","else"),b,y)}function Pe(e,t){return"await"==t?p(Pe):"("==e?p(v(")"),Ne,y):void 0}function Ne(e){return"var"==e?p(qe,Ue):("variable"==e?p:s)(Ue)}function Ue(e,t){return")"==e?p():";"==e?p(Ue):"in"==t||"of"==t?(c.marked="keyword",p(x,Ue)):s(x,Ue)}function q(e,t){return"*"==t?(c.marked="keyword",p(q)):"variable"==e?(a(t),p(q)):"("==e?p(m,v(")"),V(S,")"),y,he,b,k):u&&"<"==t?p(v(">"),V(Te,">"),y,q):void 0}function C(e,t){return"*"==t?(c.marked="keyword",p(C)):"variable"==e?(a(t),p(C)):"("==e?p(m,v(")"),V(S,")"),y,he,k):u&&"<"==t?p(v(">"),V(Te,">"),y,C):void 0}function We(e,t){return"keyword"==e||"variable"==e?(c.marked="type",p(We)):"<"==t?p(v(">"),V(Te,">"),y):void 0}function S(e,t){return"@"==t&&p(x,S),"spread"==e?p(S):u&&ee(t)?(c.marked="keyword",p(S)):u&&"this"==e?p(E,$):s(T,E,$)}function Be(e,t){return("variable"==e?Fe:He)(e,t)}function Fe(e,t){if("variable"==e)return a(t),p(He)}function He(e,t){return"<"==t?p(v(">"),V(Te,">"),y,He):"extends"==t||"implements"==t||u&&","==e?("implements"==t&&(c.marked="keyword"),p(u?z:x,He)):"{"==e?p(v("}"),_,y):void 0}function _(e,t){return"async"==e||"variable"==e&&("static"==t||"get"==t||"set"==t||u&&ee(t))&&c.stream.match(/^\s+#?[\w$\xa1-\uffff]/,!1)?(c.marked="keyword",p(_)):"variable"==e||"keyword"==c.style?(c.marked="property",p(De,_)):"number"==e||"string"==e?p(De,_):"["==e?p(x,E,w("]"),De,_):"*"==t?(c.marked="keyword",p(_)):u&&"("==e?s(C,_):";"==e||","==e?p(_):"}"==e?p():"@"==t?p(x,_):void 0}function De(e,t){if("!"==t)return p(De);if("?"==t)return p(De);if(":"==e)return p(z,$);if("="==t)return p(h);e=c.state.lexical.prev;return s(e&&"interface"==e.info?C:q)}function Ge(e,t){return"*"==t?(c.marked="keyword",p(Xe,w(";"))):"default"==t?(c.marked="keyword",p(x,w(";"))):"{"==e?p(V(Je,"}"),Xe,w(";")):s(b)}function Je(e,t){return"as"==t?(c.marked="keyword",p(w("variable"))):"variable"==e?s(h,Je):void 0}function Ke(e){return"string"==e?p():"("==e?s(x):"."==e?s(j):s(Le,Qe,Xe)}function Le(e,t){return"{"==e?we(Le,"}"):("variable"==e&&a(t),"*"==t&&(c.marked="keyword"),p(Re))}function Qe(e){if(","==e)return p(Le,Qe)}function Re(e,t){if("as"==t)return c.marked="keyword",p(Le)}function Xe(e,t){if("from"==t)return c.marked="keyword",p(x)}function Ye(e){return"]"==e?p():s(V(h,"]"))}function Ze(){return s(v("form"),T,w("{"),v("}"),V(et,"}"),y,y)}function et(){return s(T,$)}function tt(e,t,r){return t.tokenize==d&&/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(t.lastType)||"quasi"==t.lastType&&/\{\s*$/.test(e.string.slice(0,e.pos-(r||0)))}return m.lex=ae.lex=!0,y.lex=k.lex=!0,{startState:function(e){e={tokenize:d,lastType:"sof",cc:[],lexical:new X((e||0)-f,0,"block",!1),localVars:l.localVars,context:l.localVars&&new te(null,null,!1),indented:e||0};return l.globalVars&&"object"==typeof l.globalVars&&(e.globalVars=l.globalVars),e},token:function(e,t){if(e.sol()&&(t.lexical.hasOwnProperty("align")||(t.lexical.align=!1),t.indented=e.indentation(),Q(e,t)),t.tokenize!=K&&e.eatSpace())return null;var r=t.tokenize(e,t);return"comment"==H?r:(t.lastType="operator"!=H||"++"!=D&&"--"!=D?H:"incdec",Y(t,r,H,D,e))},indent:function(e,t){if(e.tokenize==K||e.tokenize==L)return rt.Pass;if(e.tokenize!=d)return 0;var r,n=t&&t.charAt(0),a=e.lexical;if(!/^\s*else\b/.test(t))for(var i=e.cc.length-1;0<=i;--i){var o=e.cc[i];if(o==y)a=a.prev;else if(o!=Oe&&o!=k)break}for(;("stat"==a.type||"form"==a.type)&&("}"==n||(r=e.cc[e.cc.length-1])&&(r==j||r==M)&&!/^[,\.=+\-*:?[\(]/.test(t));)a=a.prev;var c,s=(a=N&&")"==a.type&&"stat"==a.prev.type?a.prev:a).type,u=n==s;return"vardef"==s?a.indented+("operator"==e.lastType||","==e.lastType?a.info.length+1:0):"form"==s&&"{"==n?a.indented:"form"==s?a.indented+f:"stat"==s?a.indented+(s=t,"operator"==(c=e).lastType||","==c.lastType||G.test(s.charAt(0))||/[,.]/.test(s.charAt(0))?N||f:0):"switch"!=a.info||u||0==l.doubleIndentSwitch?a.align?a.column+(u?0:1):a.indented+(u?0:f):a.indented+(/^(?:case|default)\b/.test(t)?f:2*f)},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:o?null:"/*",blockCommentEnd:o?null:"*/",blockCommentContinue:o?null:" * ",lineComment:o?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:o?"json":"javascript",jsonldMode:U,jsonMode:o,expressionAllowed:tt,skipExpression:function(e){Y(e,"atom","atom","true",new rt.StringStream("",2,null))}}}),rt.registerHelper("wordChars","javascript",/[\w$]/),rt.defineMIME("text/javascript","javascript"),rt.defineMIME("text/ecmascript","javascript"),rt.defineMIME("application/javascript","javascript"),rt.defineMIME("application/x-javascript","javascript"),rt.defineMIME("application/ecmascript","javascript"),rt.defineMIME("application/json",{name:"javascript",json:!0}),rt.defineMIME("application/x-json",{name:"javascript",json:!0}),rt.defineMIME("application/manifest+json",{name:"javascript",json:!0}),rt.defineMIME("application/ld+json",{name:"javascript",jsonld:!0}),rt.defineMIME("text/typescript",{name:"javascript",typescript:!0}),rt.defineMIME("application/typescript",{name:"javascript",typescript:!0})}); \ No newline at end of file diff --git a/luci-app-clashoo/htdocs/luci-static/resources/cm/mode/yaml.js b/luci-app-clashoo/htdocs/luci-static/resources/cm/mode/yaml.js new file mode 100644 index 000000000..a38d604c4 --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/cm/mode/yaml.js @@ -0,0 +1 @@ +!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(e){"use strict";e.defineMode("yaml",function(){var n=new RegExp("\\b(("+["true","false","on","off","yes","no"].join(")|(")+"))$","i");return{token:function(e,i){var t=e.peek(),r=i.escaped;if(i.escaped=!1,"#"==t&&(0==e.pos||/\s/.test(e.string.charAt(e.pos-1))))return e.skipToEnd(),"comment";if(e.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/))return"string";if(i.literal&&e.indentation()>i.keyCol)return e.skipToEnd(),"string";if(i.literal&&(i.literal=!1),e.sol()){if(i.keyCol=0,i.pair=!1,i.pairStart=!1,e.match("---"))return"def";if(e.match("..."))return"def";if(e.match(/\s*-\s+/))return"meta"}if(e.match(/^(\{|\}|\[|\])/))return"{"==t?i.inlinePairs++:"}"==t?i.inlinePairs--:"["==t?i.inlineList++:i.inlineList--,"meta";if(0)\s*/))return i.literal=!0,"meta";if(e.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i))return"variable-2";if(0==i.inlinePairs&&e.match(/^\s*-?[0-9\.\,]+\s?$/))return"number";if(0'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)?(i.pair=!0,i.keyCol=e.indentation(),"atom"):i.pair&&e.match(/^:\s*/)?(i.pairStart=!0,"meta"):(i.pairStart=!1,i.escaped="\\"==t,e.next(),null)},startState:function(){return{pair:!1,pairStart:!1,keyCol:0,inlinePairs:0,inlineList:0,literal:!1,escaped:!1}},lineComment:"#",fold:"indent"}}),e.defineMIME("text/x-yaml","yaml"),e.defineMIME("text/yaml","yaml")}); \ No newline at end of file diff --git a/luci-app-clashoo/htdocs/luci-static/resources/tools/clashoo.js b/luci-app-clashoo/htdocs/luci-static/resources/tools/clashoo.js new file mode 100644 index 000000000..940623222 --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/tools/clashoo.js @@ -0,0 +1,264 @@ +'use strict'; +'require baseclass'; +'require rpc'; +'require ui'; + +(function () { + if (document.getElementById('clashoo-toast-style')) return; + var css = [ + '#clashoo-toast-stack{position:fixed;top:20px;right:20px;z-index:99999;display:flex;flex-direction:column;gap:8px;max-width:380px;pointer-events:none;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}', + '.clashoo-toast{background:rgba(33,37,41,.92);color:#fff;font-size:13px;line-height:1.5;padding:10px 14px;border-radius:8px;box-shadow:0 4px 14px rgba(0,0,0,.18);opacity:0;transform:translateX(24px);transition:opacity .25s ease,transform .25s ease;pointer-events:auto;cursor:pointer;word-break:break-word}', + '.clashoo-toast.show{opacity:1;transform:translateX(0)}', + '.clashoo-toast.err{background:rgba(198,40,40,.94)}', + '.clashoo-toast.ok{background:rgba(46,125,50,.94)}', + '.clashoo-toast a{color:#8ec5ff;text-decoration:underline}', + '.clashoo-toast a:hover{color:#b3d7ff}' + ].join(''); + var s = document.createElement('style'); + s.id = 'clashoo-toast-style'; + s.textContent = css; + document.head.appendChild(s); +})(); + +function _clashooToastStack() { + var el = document.getElementById('clashoo-toast-stack'); + if (!el) { + el = document.createElement('div'); + el.id = 'clashoo-toast-stack'; + document.body.appendChild(el); + } + return el; +} + +function _clashooLinkify(safeText) { + var logUrl = L.url('admin/services/clashoo/system'); + return safeText.replace(/(进度|日志)/g, + '$1'); +} + +function _clashooToast(msg, opts) { + opts = opts || {}; + var stack = _clashooToastStack(); + var el = document.createElement('div'); + el.className = 'clashoo-toast' + (opts.kind ? ' ' + opts.kind : ''); + var text; + if (msg && msg.nodeType === 1) text = msg.textContent || ''; + else if (typeof msg === 'string') text = msg; + else text = String(msg || ''); + var safe = text.replace(/&/g, '&').replace(//g, '>'); + el.innerHTML = _clashooLinkify(safe); + stack.appendChild(el); + requestAnimationFrame(function () { el.classList.add('show'); }); + + var dur = opts.duration || 3500; + var timer = null, remain = dur, start = 0; + function close() { + if (timer) { clearTimeout(timer); timer = null; } + el.classList.remove('show'); + setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 280); + } + function arm() { start = Date.now(); timer = setTimeout(close, remain); } + el.addEventListener('mouseenter', function () { + if (timer) { clearTimeout(timer); timer = null; remain = Math.max(800, remain - (Date.now() - start)); } + }); + el.addEventListener('mouseleave', arm); + el.addEventListener('click', function (ev) { + if (ev.target && ev.target.tagName === 'A') return; + close(); + }); + arm(); + return { close: close }; +} + +(function () { + if (!ui || ui._clashooPatched) return; + ui._clashooPatched = true; + var orig = ui.addNotification.bind(ui); + ui.addNotification = function (title, children, classes) { + try { + var text = ''; + var collect = function (c) { + if (c == null) return; + if (Array.isArray(c)) { c.forEach(collect); return; } + if (c.nodeType === 1) { text += (c.textContent || '') + ' '; return; } + text += String(c) + ' '; + }; + if (title) text += title + ' '; + collect(children); + text = text.trim(); + + var kind = ''; + var cls = classes ? String(classes) : ''; + if (/error|danger/i.test(cls) || /失败|错误/.test(text)) kind = 'err'; + else if (/success/i.test(cls) || /成功/.test(text)) kind = 'ok'; + + _clashooToast(text, { kind: kind }); + return null; + } catch (e) { + return orig(title, children, classes); + } + }; +})(); + +const callStatus = rpc.declare({ object: 'luci.clashoo', method: 'status', expect: {} }); +const callStart = rpc.declare({ object: 'luci.clashoo', method: 'start', expect: {} }); +const callStop = rpc.declare({ object: 'luci.clashoo', method: 'stop', expect: {} }); +const callReload = rpc.declare({ object: 'luci.clashoo', method: 'reload', expect: {} }); +const callRestart = rpc.declare({ object: 'luci.clashoo', method: 'restart', expect: {} }); +const callVersion = rpc.declare({ object: 'luci.clashoo', method: 'version', expect: {} }); +const callListProf = rpc.declare({ object: 'luci.clashoo', method: 'list_profiles', expect: {} }); +const callListConfigs = rpc.declare({ object: 'luci.clashoo', method: 'list_configs', expect: {} }); +const callSetConfig = rpc.declare({ object: 'luci.clashoo', method: 'set_config', params: ['name'], expect: {} }); +const callSetMode = rpc.declare({ object: 'luci.clashoo', method: 'set_mode', params: ['mode'], expect: {} }); +const callSetProxyMode = rpc.declare({ object: 'luci.clashoo', method: 'set_proxy_mode', params: ['mode'], expect: {} }); +const callSetCore = rpc.declare({ object: 'luci.clashoo', method: 'set_core', params: ['core', 'dcore', 'action'], expect: {} }); +const callSetPanel = rpc.declare({ object: 'luci.clashoo', method: 'set_panel', params: ['name'], expect: {} }); +const callUpdatePanel = rpc.declare({ object: 'luci.clashoo', method: 'update_panel', params: ['name'], expect: {} }); +const callPanelStatus = rpc.declare({ object: 'luci.clashoo', method: 'panel_status', expect: {} }); +const callReadLog = rpc.declare({ object: 'luci.clashoo', method: 'read_log', expect: {} }); +const callReadRealLog = rpc.declare({ object: 'luci.clashoo', method: 'read_real_log', expect: {} }); +const callClearLog = rpc.declare({ object: 'luci.clashoo', method: 'clear_log', expect: {} }); +const callReadUpdateLog = rpc.declare({ object: 'luci.clashoo', method: 'read_update_log', expect: {} }); +const callClearUpdateLog= rpc.declare({ object: 'luci.clashoo', method: 'clear_update_log', expect: {} }); +const callReadGeoipLog = rpc.declare({ object: 'luci.clashoo', method: 'read_geoip_log', expect: {} }); +const callClearGeoipLog = rpc.declare({ object: 'luci.clashoo', method: 'clear_geoip_log', expect: {} }); +const callReadCoreLog = rpc.declare({ object: 'luci.clashoo', method: 'read_core_log', expect: {} }); +const callClearCoreLog = rpc.declare({ object: 'luci.clashoo', method: 'clear_core_log', expect: {} }); +const callReadUpdateMergedLog = rpc.declare({ object: 'luci.clashoo', method: 'read_update_merged_log', expect: {} }); +const callClearUpdateMergedLog = rpc.declare({ object: 'luci.clashoo', method: 'clear_update_merged_log', expect: {} }); +const callGetCpuArch = rpc.declare({ object: 'luci.clashoo', method: 'get_cpu_arch', expect: {} }); +const callDownloadCore = rpc.declare({ object: 'luci.clashoo', method: 'download_core', expect: {} }); +const callUpdateGeoip = rpc.declare({ object: 'luci.clashoo', method: 'update_geoip', expect: {} }); +const callGetGeoipVersion = rpc.declare({ object: 'luci.clashoo', method: 'get_geoip_version', expect: {} }); +const callComponentStatus = rpc.declare({ object: 'luci.clashoo', method: 'component_status', expect: {} }); +const callComponentUpdate = rpc.declare({ object: 'luci.clashoo', method: 'component_update', params: ['component', 'variant'], expect: {} }); +const callComponentUpdateLog = rpc.declare({ object: 'luci.clashoo', method: 'component_update_log', expect: {} }); +const callComponentCheckUpdates = rpc.declare({ object: 'luci.clashoo', method: 'component_check_updates', expect: {} }); +const callUpdateChinaIp = rpc.declare({ object: 'luci.clashoo', method: 'update_china_ip', expect: {} }); +const callGetLogStatus = rpc.declare({ object: 'luci.clashoo', method: 'get_log_status', expect: {} }); +const callAccessCheck = rpc.declare({ object: 'luci.clashoo', method: 'access_check', expect: {} }); +const callAccessCheckRefresh= rpc.declare({ object: 'luci.clashoo', method: 'access_check_refresh',expect: {} }); +const callOverviewStats = rpc.declare({ object: 'luci.clashoo', method: 'overview_stats', expect: {} }); +const callOverview = rpc.declare({ object: 'luci.clashoo', method: 'overview', expect: {} }); +const callSmartFlushCache = rpc.declare({ object: 'luci.clashoo', method: 'smart_flush_cache', expect: {} }); +const callListSingboxProfiles = rpc.declare({ object: 'luci.clashoo', method: 'list_singbox_profiles', expect: {} }); +const callGetSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'get_singbox_profile', params: ['name'], expect: {} }); +const callSaveSingboxProfileChunk = rpc.declare({ object: 'luci.clashoo', method: 'save_singbox_profile_chunk', params: ['name', 'content', 'index', 'total'], expect: {} }); +const callSetSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'set_singbox_profile', params: ['name'], expect: {} }); +const callDeleteSingboxProfile = rpc.declare({ object: 'luci.clashoo', method: 'delete_singbox_profile', params: ['name'], expect: {} }); +const callCreateSingboxConfig = rpc.declare({ object: 'luci.clashoo', method: 'create_singbox_config', params: ['sub_url', 'name'], expect: {} }); +const callCommitConfig = rpc.declare({ object: 'luci.clashoo', method: 'commit_config', expect: {} }); +const callDnsAutoSetup = rpc.declare({ object: 'luci.clashoo', method: 'dns_auto_setup', expect: {} }); +const callFetchSingboxNative = rpc.declare({ object: 'luci.clashoo', method: 'fetch_singbox_native', params: ['url', 'name'], expect: {} }); +const callUpdateSingboxNative = rpc.declare({ object: 'luci.clashoo', method: 'update_singbox_native', params: ['name'], expect: {} }); + +// 把脚本输出的英文 log 行翻成中文(去掉前导时间戳) +function localizeLogLine(line) { + var msg = String(line || '').replace(/^\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s*-?\s*/, ''); + var rules = [ + // LightGBM 模型更新 + [/^Start downloading LightGBM model from:?\s*(.*)$/i, _("Start downloading LightGBM model: $1")], + [/^Model unchanged, no update needed$/i, _("Model is already up to date")], + [/^LightGBM model updated successfully$/i, _("LightGBM model updated successfully")], + [/^Download failed \(rc=(\d+)\)$/i, _("Download failed (error code $1)")], + [/^Failed to install model to (.*)$/i, _("Model install failed: $1")], + [/^No curl or wget found$/i, _("curl or wget not found")], + // GeoIP / GeoSite 更新 + [/^Updating (.+)$/i, _("Updating $1")], + [/^(.+) updated$/i, _("$1 updated")], + [/^GeoIP update completed, apply on next Clashoo restart$/i, _("GeoIP update completed, applies after Clashoo restarts")], + [/^GeoIP update completed$/i, _("GeoIP update completed")], + [/^GeoIP update failed$/i, _("GeoIP update failed")], + // 通用 + [/^Download succeeded$/i, _("Download succeeded")], + [/^Already up to date$/i, _("Already up to date")] + ]; + for (var i = 0; i < rules.length; i++) { + if (rules[i][0].test(msg)) return msg.replace(rules[i][0], rules[i][1]); + } + return msg; +} + +return baseclass.extend({ + localizeLogLine: localizeLogLine, + status: function () { return L.resolveDefault(callStatus(), {}); }, + start: function () { return L.resolveDefault(callStart(), {}); }, + stop: function () { return L.resolveDefault(callStop(), {}); }, + reload: function () { return L.resolveDefault(callReload(), {}); }, + restart: function () { return L.resolveDefault(callRestart(), {}); }, + version: function () { return L.resolveDefault(callVersion(), {}); }, + + listProfiles: function () { return L.resolveDefault(callListProf(), { profiles: [] }).then(r => r.profiles || []); }, + listConfigs: function () { return L.resolveDefault(callListConfigs(), { configs: [], current: '' }); }, + setConfig: function (name) { return L.resolveDefault(callSetConfig(name), {}); }, + setMode: function (mode) { return L.resolveDefault(callSetMode(mode), {}); }, + setProxyMode: function (mode) { return L.resolveDefault(callSetProxyMode(mode), {}); }, + setCore: function (core, dcore, action) { return L.resolveDefault(callSetCore(core, dcore, action), {}); }, + setPanel: function (name) { return L.resolveDefault(callSetPanel(name), {}); }, + updatePanel: function (name) { return L.resolveDefault(callUpdatePanel(name || 'zashboard'), {}); }, + panelStatus: function () { return L.resolveDefault(callPanelStatus(), {}); }, + + readLog: function () { return L.resolveDefault(callReadLog(), { content: '' }).then(r => r.content || ''); }, + readRealLog: function () { return L.resolveDefault(callReadRealLog(), { content: '' }).then(r => r.content || ''); }, + clearLog: function () { return L.resolveDefault(callClearLog(), {}); }, + readUpdateLog: function () { return L.resolveDefault(callReadUpdateLog(), { content: '' }).then(r => r.content || ''); }, + clearUpdateLog: function () { return L.resolveDefault(callClearUpdateLog(), {}); }, + readGeoipLog: function () { return L.resolveDefault(callReadGeoipLog(), { content: '' }).then(r => r.content || ''); }, + clearGeoipLog: function () { return L.resolveDefault(callClearGeoipLog(), {}); }, + readCoreLog: function () { return L.resolveDefault(callReadCoreLog(), { content: '' }).then(r => r.content || ''); }, + clearCoreLog: function () { return L.resolveDefault(callClearCoreLog(), {}); }, + readUpdateMergedLog: function () { return L.resolveDefault(callReadUpdateMergedLog(), { content: '' }).then(r => r.content || ''); }, + clearUpdateMergedLog: function () { return L.resolveDefault(callClearUpdateMergedLog(), {}); }, + + getCpuArch: function () { return L.resolveDefault(callGetCpuArch(), { arch: '' }).then(r => r.arch || ''); }, + downloadCore: function (dcore, arch) { return L.resolveDefault(callDownloadCore({ dcore: dcore, arch: arch }), {}); }, + updateGeoip: function () { return L.resolveDefault(callUpdateGeoip(), {}); }, + getGeoipVersion: function () { return L.resolveDefault(callGetGeoipVersion(), { version: '' }); }, + componentStatus: function () { return L.resolveDefault(callComponentStatus(), { running: false, components: [] }); }, + componentUpdate: function (component, variant) { return L.resolveDefault(callComponentUpdate(component, variant || ''), { success: false }); }, + componentUpdateLog: function () { return L.resolveDefault(callComponentUpdateLog(), { running: false, content: '' }); }, + componentCheckUpdates: function () { return L.resolveDefault(callComponentCheckUpdates(), { ok: false, latest: {} }); }, + updateChinaIp: function () { return L.resolveDefault(callUpdateChinaIp(), {}); }, + getLogStatus: function () { return L.resolveDefault(callGetLogStatus(), {}); }, + accessCheck: function () { return L.resolveDefault(callAccessCheck(), {}); }, + accessCheckRefresh: function () { return L.resolveDefault(callAccessCheckRefresh(), { success: false }); }, + overviewStats: function () { return L.resolveDefault(callOverviewStats(), {}); }, + overview: function () { + return L.resolveDefault(callOverview(), { + status: {}, + stats: {}, + configs: { configs: [], current: '', core_type: '' }, + access: {} + }); + }, + smartFlushCache: function () { return L.resolveDefault(callSmartFlushCache(), { success: false }); }, + + listSingboxProfiles: function () { return L.resolveDefault(callListSingboxProfiles(), { profiles: [], active: '' }); }, + getSingboxProfile: function (name) { return L.resolveDefault(callGetSingboxProfile(name), {}); }, + saveSingboxProfile: function (name, content) { + var chunkSize = 24576; + var total = Math.max(1, Math.ceil((content || '').length / chunkSize)); + var index = 0; + + function sendNext() { + var chunk = (content || '').slice(index * chunkSize, (index + 1) * chunkSize); + return L.resolveDefault(callSaveSingboxProfileChunk(name, chunk, String(index), String(total)), {}).then(function (r) { + if (!r || !r.success) + return { success: false, error: (r && r.error) || 'upload_failed', message: (r && (r.message || r.error)) || _('Upload failed') }; + index++; + return index < total ? sendNext() : r; + }); + } + + return sendNext(); + }, + setSingboxProfile: function (name) { return L.resolveDefault(callSetSingboxProfile(name), {}); }, + deleteSingboxProfile: function (name) { return L.resolveDefault(callDeleteSingboxProfile(name), {}); }, + createSingboxConfig: function (url, name) { return L.resolveDefault(callCreateSingboxConfig(url, name), {}); }, + commitConfig: function () { return L.resolveDefault(callCommitConfig(), { success: false }); }, + dnsAutoSetup: function () { return L.resolveDefault(callDnsAutoSetup(), { success: false }); }, + fetchSingboxNative: function (url, name) { return L.resolveDefault(callFetchSingboxNative(url, name), {}); }, + updateSingboxNative: function (name) { return L.resolveDefault(callUpdateSingboxNative(name), {}); }, + + toast: _clashooToast +}); diff --git a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/bytedance.svg b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/bytedance.svg new file mode 100644 index 000000000..6bc2d5ecb --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/bytedance.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/clashoo.css b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/clashoo.css new file mode 100644 index 000000000..5587a0896 --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/clashoo.css @@ -0,0 +1,2097 @@ +.cl-wrap { + --cl-primary-rgb: 0, 122, 255; + --primary-rgb: var(--cl-primary-rgb); + --cl-primary: var(--primary-color, rgba(var(--primary-rgb), 0.9)); + --cl-primary-border: rgba(var(--primary-rgb), 0.3); + --cl-primary-soft: rgba(var(--primary-rgb), 0.08); + --cl-primary-soft-hover: rgba(var(--primary-rgb), 0.14); + --cl-card-bg: rgba(128, 128, 128, 0.04); + --cl-card-border: rgba(128, 128, 128, 0.14); + --cl-surface-border: rgba(128, 128, 128, 0.14); + --cl-card-shadow: var(--card-shadow, 0 4px 20px rgba(0, 0, 0, 0.05)); + --cl-card-shadow-hover: var(--card-shadow, 0 4px 20px rgba(0, 0, 0, 0.05)); + --cl-radius: var(--border-radius, 12px); + --cl-title-color: var(--title-color, inherit); + --cl-field-color: var(--text-color, inherit); + --cl-placeholder: rgba(80, 88, 100, 0.55); + --cl-control-border: rgba(128, 128, 128, 0.18); + --cl-control-bg: rgba(128, 128, 128, 0.05); + --cl-input-bg: rgba(128, 128, 128, 0.05); + --cl-input-border: rgba(128, 128, 128, 0.2); + --cl-input-color: var(--text-color, inherit); + --cl-label-muted: var(--text-color, inherit); + --cl-live-shell-bg: rgba(128, 128, 128, 0.04); + --cl-live-card-bg: rgba(128, 128, 128, 0.05); + --cl-live-border: rgba(128, 128, 128, 0.14); + --cl-live-title: var(--title-color, inherit); + --cl-live-main: var(--text-color, inherit); + --cl-live-sub: var(--text-color, inherit); + --cl-live-foot: var(--text-color, inherit); + --cl-live-muted: rgba(var(--primary-rgb), 0.35); + --cl-live-zero: rgba(128, 128, 128, 0.5); + --cl-live-dot: var(--primary-color, rgba(var(--primary-rgb), 0.9)); + --cl-warn-color: var(--warning-color, var(--primary-color, rgba(var(--primary-rgb), 0.9))); + --cl-danger-color: var(--error-color, rgba(214, 92, 74, 0.95)); + --cl-chart-up: rgba(var(--primary-rgb), 0.86); + --cl-chart-down: rgba(var(--primary-rgb), 0.72); + --cl-chart-conn: rgba(var(--primary-rgb), 0.6); +} + +.cl-wrap.cl-theme-dark { + --cl-card-bg: rgba(255, 255, 255, 0.04); + --cl-card-border: rgba(255, 255, 255, 0.1); + --cl-surface-border: rgba(255, 255, 255, 0.14); + --cl-control-bg: rgba(255, 255, 255, 0.05); + --cl-input-bg: rgba(255, 255, 255, 0.05); + --cl-input-border: rgba(255, 255, 255, 0.16); + --cl-placeholder: rgba(200, 210, 220, 0.48); + --cl-live-shell-bg: rgba(255, 255, 255, 0.03); + --cl-live-card-bg: rgba(255, 255, 255, 0.04); + --cl-live-border: rgba(255, 255, 255, 0.12); +} + +.cl-wrap.cl-theme-light { + --cl-card-bg: #ffffff; + --cl-card-border: rgba(0, 0, 0, 0.12); + --cl-surface-border: rgba(0, 0, 0, 0.1); + --cl-control-bg: rgba(0, 0, 0, 0.03); + --cl-input-bg: #ffffff; + --cl-input-border: #c5c9d2; + --cl-live-shell-bg: rgba(0, 0, 0, 0.02); + --cl-live-card-bg: rgba(0, 0, 0, 0.03); + --cl-live-border: rgba(0, 0, 0, 0.12); + --cl-chart-up: rgba(120, 130, 145, 0.55); + --cl-chart-down: rgba(120, 130, 145, 0.4); + --cl-chart-conn: rgba(120, 130, 145, 0.32); +} + +@media (prefers-color-scheme: dark) { + .cl-wrap:not(.cl-theme-light) { + --cl-card-bg: rgba(255, 255, 255, 0.04); + --cl-card-border: rgba(255, 255, 255, 0.1); + --cl-surface-border: rgba(255, 255, 255, 0.14); + --cl-control-bg: rgba(255, 255, 255, 0.05); + --cl-input-bg: rgba(255, 255, 255, 0.05); + --cl-input-border: rgba(255, 255, 255, 0.16); + --cl-live-shell-bg: rgba(255, 255, 255, 0.03); + --cl-live-card-bg: rgba(255, 255, 255, 0.04); + --cl-live-border: rgba(255, 255, 255, 0.12); + } +} + +.cl-wrap .cl-card { + background: var(--cl-card-bg) !important; + border: 1px solid var(--cl-surface-border) !important; + border-radius: var(--border-radius, var(--cl-radius, 12px)) !important; + padding: 20px; + box-shadow: var(--card-shadow, var(--cl-card-shadow)) !important; + transition: all 0.3s ease; +} + +.cl-wrap .cl-card:hover { + box-shadow: var(--cl-card-shadow-hover); +} + +.cl-wrap.cl-overview-page .cl-cards > .cl-card { + padding: 14px 16px; +} + +.cl-wrap .cbi-map, +.cl-wrap .cbi-tabcontainer, +.cl-wrap .cbi-page-actions { + background: transparent; +} + +.cl-wrap .cbi-section > h3, +.cl-wrap .cl-section > h4 { + background: transparent !important; + border: 0 !important; + box-shadow: none !important; +} + +.cl-wrap .cl-ctrl, +.cl-wrap .cl-live-box, +.cl-wrap .cl-live-card { + border: 1px solid var(--cl-surface-border) !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03); +} + +.cl-wrap .cl-card-status .cl-card-head { + align-items: flex-start; +} + +/* 内核切换分段控件:紧凑分段,选中段实心主色高亮 + 白字。明暗自适应。 */ +.cl-wrap .cl-status-kernel { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 2px; + border-radius: 8px; + border: 0; + background: rgba(128, 128, 128, 0.1); + box-shadow: none !important; +} + +.cl-wrap .cl-status-kernel .cl-core-btn { + height: 24px; + line-height: 24px; + min-width: 60px; + padding: 0 10px; + border: 0; + border-radius: 6px; + background: transparent; + box-shadow: none !important; + font-size: 11px; + font-weight: 500; + color: var(--cl-label-muted); + opacity: 0.6; + cursor: pointer; + transition: background-color 0.18s ease, color 0.18s ease, opacity 0.18s ease; +} + +.cl-wrap .cl-status-kernel .cl-core-btn:hover { + opacity: 0.85; +} + +/* 选中段实心色:固定青色,四主题(Bootstrap / Argon × 明暗)统一, + 略带向下变暗的渐变增加层次。 */ +.cl-wrap .cl-status-kernel .cl-core-btn.active, +.cl-wrap .cl-status-kernel .cl-core-btn.active:hover { + background: linear-gradient(#3886a1, #2f7288); + border-color: transparent; + color: #fff; + opacity: 1; + font-weight: 600; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) !important; +} + +.cl-wrap .cl-status-kernel .cl-core-btn:disabled { + cursor: not-allowed; +} + +.cl-wrap .cbi-checkbox input:disabled + label { + opacity: 0.4; + cursor: not-allowed; +} + +.cl-wrap .cl-status-kernel.is-busy { + opacity: 0.6; +} + +.cl-wrap .btn.cbi-button-action, +.cl-wrap .cbi-button-action { + background: var(--cl-primary-soft) !important; + border-color: var(--cl-primary-border) !important; + color: var(--cl-primary) !important; +} + +.cl-wrap .btn.cbi-button-action:hover, +.cl-wrap .cbi-button-action:hover { + background: rgba(var(--cl-primary-rgb), 0.2) !important; + border-color: transparent !important; + color: var(--cl-primary) !important; +} + +.cl-wrap .cl-live-box { + padding: 10px 12px; + border-color: var(--cl-live-border); + background: var(--cl-live-shell-bg); +} + +.cl-wrap .cbi-value-title, +.cl-wrap .cl-card .lbl, +.cl-wrap .cl-section h4 { + color: var(--cl-title-color); +} + +.cl-wrap .cbi-value-field input, +.cl-wrap .cbi-value-field select, +.cl-wrap .cbi-value-field textarea, +.cl-wrap input, +.cl-wrap select, +.cl-wrap textarea { + color: var(--cl-field-color); +} + +.cl-wrap input::placeholder, +.cl-wrap textarea::placeholder { + color: var(--cl-placeholder); +} + +.cl-wrap .cl-control-wrap { + width: min(100%, 400px); + max-width: 400px; +} + +.cl-wrap .cl-control-wrap .cbi-input-text, +.cl-wrap .cl-control-wrap .cbi-input-select, +.cl-wrap .cl-control-wrap select, +.cl-wrap .cl-control-wrap input[type="text"], +.cl-wrap .cl-control-wrap input[type="number"], +.cl-wrap .cl-control-wrap input[type="password"], +.cl-wrap .cl-control-wrap textarea { + max-width: 400px; + height: 38px; + box-sizing: border-box; + border: 1px solid var(--cl-control-border); + border-radius: 8px; + padding: 0 12px; + background: var(--cl-control-bg); +} + +.cl-wrap .cl-control-wrap textarea { + min-height: 76px; + height: auto; + padding: 8px 12px; +} + +.cl-wrap .cl-control-wrap .cbi-input-select, +.cl-wrap .cl-control-wrap select { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cl-wrap .cl-live-grid { + gap: 14px; +} + +.cl-wrap .cl-live-card, +.cl-wrap .cl-stat-card { + height: 120px; + min-height: 120px; + max-height: 120px; + box-sizing: border-box; + border-color: var(--cl-live-border); + background: var(--cl-live-card-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); + backdrop-filter: blur(2px); +} + +.cl-wrap .cl-live-title { + color: var(--cl-live-title); +} + +.cl-wrap .cl-card-mode .val { + display: block; + min-height: 0; + line-height: 1.35; +} + +.cl-wrap.cl-system-page .cbi-section, +.cl-wrap .cbi-section.cl-form-card { + background: var(--cl-card-bg); + border: 1px solid var(--cl-card-border); + border-radius: 12px; + padding: 20px; + margin-bottom: 20px !important; + box-shadow: var(--cl-card-shadow); + transition: all 0.3s ease; +} + +.cl-wrap.cl-system-page .cbi-section:hover, +.cl-wrap .cbi-section.cl-form-card:hover { + box-shadow: var(--cl-card-shadow-hover); +} + +.cl-wrap .cl-file-actions { + justify-content: flex-end; +} + +.cl-wrap .cl-mode-stack { + display: flex; + flex-direction: column; + gap: 6px; + justify-content: flex-start; + align-items: flex-start; + width: 100%; +} + +.cl-wrap .cl-mode-tag { + display: flex; + align-items: center; + justify-content: flex-start; + padding: 0; + border-radius: 0; + font-size: 11px; + line-height: 1.35; + background: transparent; + color: inherit; + opacity: 0.5; + width: max-content; + max-width: 100%; + text-align: left; +} + +.cl-wrap .cl-mode-tag-degraded { + color: var(--cl-warn-color); +} + +.cl-wrap .cl-mode-row { + display: flex; + justify-content: space-between; + gap: 8px; + align-items: baseline; + width: 100%; + max-width: 250px; +} + +.cl-wrap .cl-mode-proto { + font-size: 11px; + line-height: 1.35; + opacity: 0.78; + color: rgba(var(--cl-primary-rgb), 0.84); + text-align: left; + letter-spacing: 0.2px; + min-width: 42px; + flex: 0 0 42px; +} + +.cl-wrap .cl-mode-name { + font-size: 11px; + line-height: 1.35; + font-weight: 700; + color: var(--cl-title-color); + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 1 auto; +} + +.cl-wrap .cl-mode-suffix { + font-size: 11px; + line-height: 1.35; + text-align: right; + opacity: 0.5; + min-width: 34px; + flex: 0 0 34px; +} + +.cl-wrap .cl-mode-stack-line { + font-size: 11px; + line-height: 1.35; + font-weight: 600; + opacity: 0.78; +} + +.cl-wrap .cl-mode-degraded { + font-size: 10px; + line-height: 1.3; + color: #b56f00; + opacity: 0.92; +} + +.cl-wrap .cl-mode-meta { + font-size: 10px; + line-height: 1.3; + opacity: 0.65; +} + +body.dark .cl-wrap .cl-mode-tag, +body.dark .cl-wrap .cl-mode-tag, +body[data-theme="dark"] .cl-wrap .cl-mode-tag, +body[data-darkmode="1"] .cl-wrap .cl-mode-tag, +body[data-darkmode="true"] .cl-wrap .cl-mode-tag, +html.dark .cl-wrap .cl-mode-tag, +html.dark .cl-wrap .cl-mode-tag, +html[data-theme="dark"] .cl-wrap .cl-mode-tag, +html[data-bs-theme="dark"] .cl-wrap .cl-mode-tag, +html[data-darkmode="1"] .cl-wrap .cl-mode-tag, +html[data-darkmode="true"] .cl-wrap .cl-mode-tag { + background: transparent; + color: inherit; + opacity: 0.5; +} + +.cl-wrap.cl-config-page .cl-fixed-600 { + width: min(100%, 600px); + max-width: 600px; +} + +.cl-wrap.cl-config-page .cl-file-list { + width: 100%; +} + +.cl-wrap.cl-config-page .cl-panel .cbi-section { + background: transparent !important; + border: 0 !important; + border-bottom: 1px solid var(--cl-card-border) !important; + border-radius: 0 !important; + padding: 14px 0 18px; + margin-bottom: 12px !important; + box-shadow: none !important; +} + +.cl-wrap.cl-config-page .cl-section.cl-card, +.cl-wrap.cl-config-page .cl-panel.cl-card { + background: transparent !important; + border: 0 !important; + border-bottom: 1px solid var(--cl-card-border) !important; + border-radius: 0 !important; + box-shadow: none !important; + padding: 14px 0 18px !important; +} + +.cl-wrap.cl-config-page .cl-panel .cbi-section > h3 { + font-size: 1.15rem !important; + font-weight: 600 !important; + color: var(--title-color, var(--cl-title-color)); + margin-bottom: 12px !important; +} + +/* Title typography normalization: match template on Config(Proxy/DNS) + System pages */ +.cl-wrap.cl-config-page .cl-section h4, +.cl-wrap.cl-config-page .cl-panel .cbi-section > h3 { + font-size: 1.15rem !important; + line-height: 1.35 !important; + font-weight: 600 !important; + color: var(--title-color, var(--cl-title-color)) !important; + opacity: 1 !important; + letter-spacing: 0.1px !important; +} + +.cl-wrap.cl-system-page .cl-section h4, +.cl-wrap.cl-system-page .cl-panel .cbi-section > h3 { + font-size: 15px !important; + line-height: 1.35 !important; + font-weight: 700 !important; + color: var(--cl-title-color, inherit) !important; + opacity: 1 !important; + letter-spacing: 0 !important; +} + +.cl-wrap.cl-config-page .cbi-value-title { + color: var(--cl-label-muted) !important; + font-size: 13px !important; +} + +/* 表单说明文字统一弱化:明显比标题与字段标签暗,拉开视觉层次 */ +.cl-wrap.cl-config-page .cbi-value-description, +.cl-wrap.cl-config-page .cbi-section-descr, +.cl-wrap.cl-config-page .cbi-value-helptext, +.cl-wrap.cl-system-page .cbi-value-description, +.cl-wrap.cl-system-page .cbi-section-descr, +.cl-wrap.cl-system-page .cbi-value-helptext { + color: rgba(120, 130, 150, 0.9) !important; + font-size: 12px !important; +} + +.cl-wrap.cl-system-page .cl-tab, +.cl-wrap.cl-config-page .cl-tab { + font-size: 14px !important; + font-weight: 600 !important; + color: var(--title-color, var(--cl-title-color)) !important; + opacity: 1 !important; +} + +.cl-wrap.cl-system-page .cl-tab.active, +.cl-wrap.cl-config-page .cl-tab.active { + color: var(--title-color, var(--cl-title-color)) !important; + font-weight: 700 !important; +} + +.cl-wrap.cl-config-page .cl-panel .cbi-value-title { + color: var(--cl-label-muted); +} + +.cl-wrap.cl-config-page .cl-sub-url, +.cl-wrap.cl-config-page .cl-ctrl select, +.cl-wrap.cl-config-page .cl-ctrl .cbi-input-select, +.cl-wrap.cl-config-page .cl-control-wrap .cbi-input-text, +.cl-wrap.cl-config-page .cl-control-wrap .cbi-input-select, +.cl-wrap.cl-config-page .cl-control-wrap select, +.cl-wrap.cl-config-page .cl-control-wrap input[type="text"], +.cl-wrap.cl-config-page .cl-control-wrap input[type="number"], +.cl-wrap.cl-config-page .cl-control-wrap input[type="password"], +.cl-wrap.cl-config-page .cl-control-wrap textarea, +.cl-wrap.cl-config-page .cbi-value-field input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-config-page .cbi-value-field select, +.cl-wrap.cl-config-page .cbi-value-field textarea { + background: rgba(128, 128, 128, 0.05) !important; + border: 0 !important; + border-bottom: 1px solid rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.2) !important; + color: var(--cl-input-color) !important; + border-radius: 0 !important; + box-shadow: none !important; +} + +.cl-wrap.cl-config-page .cl-sub-url, +.cl-wrap.cl-config-page .cl-ctrl select, +.cl-wrap.cl-config-page .cl-ctrl .cbi-input-select { + padding: 0 12px !important; + height: 38px !important; +} + +.cl-wrap.cl-config-page .cl-panel, +.cl-wrap.cl-system-page .cl-panel, +.cl-wrap.cl-config-page .cl-panel .cbi-section-node, +.cl-wrap.cl-system-page .cl-panel .cbi-section-node { + background: transparent !important; +} + +.cl-wrap.cl-system-page .cbi-section, +.cl-wrap.cl-system-page .cl-panel .cbi-section { + background: transparent !important; + border: 0 !important; + border-bottom: 1px solid var(--cl-card-border) !important; + border-radius: 0 !important; + box-shadow: none !important; + padding: 14px 0 18px !important; + margin-bottom: 12px !important; +} + +.cl-wrap.cl-config-page .cbi-value-field, +.cl-wrap.cl-system-page .cbi-value-field, +.cl-wrap.cl-config-page .cl-control-wrap, +.cl-wrap.cl-system-page .cl-control-wrap { + max-width: 600px !important; +} + +.cl-wrap.cl-config-page .cbi-value-title, +.cl-wrap.cl-system-page .cbi-value-title { + min-height: 38px; + display: flex; + align-items: center; +} + +.cl-wrap.cl-config-page .cbi-value-field input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-config-page .cbi-value-field select, +.cl-wrap.cl-config-page .cbi-value-field textarea, +.cl-wrap.cl-config-page .cl-control-wrap .cbi-input-text, +.cl-wrap.cl-config-page .cl-control-wrap .cbi-input-select, +.cl-wrap.cl-config-page .cl-control-wrap select, +.cl-wrap.cl-config-page .cl-control-wrap input[type="text"], +.cl-wrap.cl-config-page .cl-control-wrap input[type="number"], +.cl-wrap.cl-config-page .cl-control-wrap input[type="password"], +.cl-wrap.cl-config-page .cl-control-wrap textarea { + max-width: 480px !important; + box-sizing: border-box; + padding: 0 10px !important; + border: 0 !important; + border-bottom: 1px solid rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.2) !important; + border-radius: 0 !important; + color: var(--cl-field-color) !important; + background: rgba(128, 128, 128, 0.05) !important; + box-shadow: none !important; +} + +.cl-wrap.cl-config-page .cbi-value-field textarea, +.cl-wrap.cl-system-page .cbi-value-field textarea { + min-height: 84px; + height: auto; + padding: 8px 10px !important; +} + +.cl-wrap.cl-config-page .cbi-value-field select, +.cl-wrap.cl-system-page .cbi-value-field select { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.cl-wrap.cl-config-page .btn, +.cl-wrap.cl-config-page .cbi-button, +.cl-wrap.cl-system-page .btn, +.cl-wrap.cl-system-page .cbi-button { + border-radius: 8px !important; + box-shadow: none !important; +} + +.cl-wrap.cl-config-page .cbi-button-action, +.cl-wrap.cl-config-page .btn.cbi-button-action, +.cl-wrap.cl-system-page .cbi-button-action, +.cl-wrap.cl-system-page .btn.cbi-button-action { + background: var(--cl-primary-soft) !important; + border-color: var(--cl-primary-border) !important; + color: var(--cl-primary) !important; +} + +.cl-wrap.cl-config-page .cbi-button-add, +.cl-wrap.cl-config-page .btn.cbi-button-add, +.cl-wrap.cl-system-page .cbi-button-add, +.cl-wrap.cl-system-page .btn.cbi-button-add, +.cl-wrap.cl-config-page .cbi-button-positive, +.cl-wrap.cl-config-page .btn.cbi-button-positive, +.cl-wrap.cl-system-page .cbi-button-positive, +.cl-wrap.cl-system-page .btn.cbi-button-positive { + background: rgba(66, 186, 117, 0.12) !important; + border-color: rgba(66, 186, 117, 0.38) !important; + color: rgba(32, 132, 75, 0.94) !important; +} + +.cl-wrap.cl-config-page .cbi-button-negative, +.cl-wrap.cl-config-page .btn.cbi-button-negative, +.cl-wrap.cl-system-page .cbi-button-negative, +.cl-wrap.cl-system-page .btn.cbi-button-negative { + background: rgba(222, 86, 86, 0.08) !important; + border-color: rgba(222, 86, 86, 0.46) !important; + color: rgba(190, 62, 62, 0.92) !important; +} + +/* 中性按钮(检查更新 / 导出备份 / 导入还原):浅灰描边, + 避免落到主题原生深灰填充(Argon 浅色下尤为突兀)。明暗自适应。 */ +.cl-wrap.cl-config-page .cbi-button-neutral, +.cl-wrap.cl-config-page .btn.cbi-button-neutral, +.cl-wrap.cl-system-page .cbi-button-neutral, +.cl-wrap.cl-system-page .btn.cbi-button-neutral { + background: rgba(128, 128, 128, 0.1) !important; + border: 1px solid rgba(128, 128, 128, 0.24) !important; + color: var(--cl-label-muted) !important; + box-shadow: none !important; +} + +.cl-wrap.cl-config-page .cbi-button-neutral:hover, +.cl-wrap.cl-config-page .btn.cbi-button-neutral:hover, +.cl-wrap.cl-system-page .cbi-button-neutral:hover, +.cl-wrap.cl-system-page .btn.cbi-button-neutral:hover { + background: rgba(128, 128, 128, 0.18) !important; + border-color: rgba(128, 128, 128, 0.32) !important; +} + +/* Config page de-colorization: keep primary accents only */ +.cl-wrap.cl-config-page .cbi-button-action, +.cl-wrap.cl-config-page .btn.cbi-button-action, +.cl-wrap.cl-config-page .cbi-button-add, +.cl-wrap.cl-config-page .btn.cbi-button-add, +.cl-wrap.cl-config-page .cbi-button-positive, +.cl-wrap.cl-config-page .btn.cbi-button-positive, +.cl-wrap.cl-config-page .cbi-button-negative, +.cl-wrap.cl-config-page .btn.cbi-button-negative, +.cl-wrap.cl-config-page .cl-btn-update-sub, +.cl-wrap.cl-config-page .cl-btn-edit, +.cl-wrap.cl-config-page .cl-btn-switch, +.cl-wrap.cl-config-page .cl-btn-delete, +.cl-wrap.cl-config-page .cl-btn-template-upload, +.cl-wrap.cl-config-page .cl-btn-upload-config, +.cl-wrap.cl-config-page .cl-btn-sb-upload { + background: rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.1) !important; + border: 1px solid rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.28) !important; + color: var(--cl-primary) !important; + box-shadow: none !important; +} + +.cl-wrap.cl-config-page .cbi-button-action:hover, +.cl-wrap.cl-config-page .btn.cbi-button-action:hover, +.cl-wrap.cl-config-page .cbi-button-add:hover, +.cl-wrap.cl-config-page .btn.cbi-button-add:hover, +.cl-wrap.cl-config-page .cbi-button-positive:hover, +.cl-wrap.cl-config-page .btn.cbi-button-positive:hover, +.cl-wrap.cl-config-page .cbi-button-negative:hover, +.cl-wrap.cl-config-page .btn.cbi-button-negative:hover, +.cl-wrap.cl-config-page .cl-btn-update-sub:hover, +.cl-wrap.cl-config-page .cl-btn-edit:hover, +.cl-wrap.cl-config-page .cl-btn-switch:hover, +.cl-wrap.cl-config-page .cl-btn-delete:hover, +.cl-wrap.cl-config-page .cl-btn-template-upload:hover, +.cl-wrap.cl-config-page .cl-btn-upload-config:hover, +.cl-wrap.cl-config-page .cl-btn-sb-upload:hover { + background: rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.16) !important; + border-color: rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.4) !important; + color: var(--cl-primary) !important; +} + +.cl-wrap.cl-config-page .cl-file-item { + width: 100%; + box-sizing: border-box; + background: rgba(128, 128, 128, 0.04) !important; + border: 1px solid rgba(128, 128, 128, 0.16) !important; + box-shadow: none !important; +} + +.cl-wrap.cl-config-page .cl-file-item.is-active { + background: rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.08) !important; + border-color: rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.24) !important; +} + +.cl-wrap.cl-config-page .cl-file-meta { + justify-content: flex-start; +} + +.cl-wrap.cl-config-page .cl-file-name { + flex: 1; +} + +.cl-wrap.cl-config-page .cl-file-name-text { + color: var(--cl-label-muted) !important; +} + +.cl-wrap.cl-config-page .cl-file-size { + color: rgba(128, 128, 128, 0.62) !important; +} + +.cl-wrap.cl-config-page .cl-file-actions { + margin-left: auto; +} + +.cl-wrap.cl-config-page .cl-rewrite-wrap { + max-width: 600px; + background: transparent !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + padding: 0 !important; +} + +.cl-wrap.cl-config-page .cl-rewrite-wrap .cl-sub-url, +.cl-wrap.cl-config-page .cl-rewrite-wrap .cbi-input-select, +.cl-wrap.cl-config-page .cl-rewrite-wrap select { + width: 100%; + max-width: 100%; +} + +.cl-wrap.cl-config-page .cl-template-row { + display: flex; + align-items: center; + gap: 10px; + width: 100%; +} + +.cl-wrap.cl-config-page .cl-template-select-wrap { + flex: 1; + min-width: 0; +} + +.cl-wrap.cl-config-page .cl-template-select { + width: 100%; + max-width: 100%; + text-overflow: ellipsis; +} + +.cl-wrap .cl-active-badge { + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; + flex-shrink: 0; + background: rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.12); + color: var(--cl-primary); + border: 1px solid rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.24); +} + +body.dark .cl-wrap .cl-active-badge, +body.dark .cl-wrap .cl-active-badge, +body[data-theme="dark"] .cl-wrap .cl-active-badge, +html.dark .cl-wrap .cl-active-badge, +html.dark .cl-wrap .cl-active-badge, +html[data-theme="dark"] .cl-wrap .cl-active-badge, +html[data-bs-theme="dark"] .cl-wrap .cl-active-badge { + background: rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.18); + color: rgba(209, 227, 255, 0.95); + border-color: rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.3); +} + +.cl-wrap .cl-live-num { + font-size: 18px; + color: rgba(40, 48, 62, 0.88); +} + +.cl-wrap .cl-pass-wrap { + display: flex; + align-items: center; + gap: 8px; + /* 322 = 输入框 210(与面板端口/面板 UI 对齐)+ 两个 48px 按钮 + 两段 8px 间距 */ + width: min(100%, 322px); +} + +.cl-wrap .cl-pass-wrap input[type="password"], +.cl-wrap .cl-pass-wrap input[type="text"] { + flex: 1; + min-width: 0; +} + +.cl-wrap .cl-pass-btn { + flex-shrink: 0; + min-width: 48px; + padding: 4px 10px !important; + border-radius: 8px; +} + +.cl-wrap .cl-arch-badge-row { + display: inline-flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-top: 2px; +} + +.cl-wrap .cl-arch-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + line-height: 1.3; +} + +.cl-wrap .cl-arch-badge-sys { + background: rgba(0, 122, 255, 0.1); + color: rgba(0, 98, 214, 0.92); + border: 1px solid rgba(0, 122, 255, 0.24); +} + +.cl-wrap .cl-arch-badge-rec { + background: rgba(46, 170, 92, 0.12); + color: rgba(22, 122, 63, 0.92); + border: 1px solid rgba(46, 170, 92, 0.26); +} + +.cl-wrap .cl-arch-arrow { + font-size: 12px; + opacity: 0.62; +} + +.cl-wrap .cl-check-icon-wrap { + width: 18px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + background: transparent !important; + border: none !important; + box-shadow: none !important; + border-radius: 0 !important; + overflow: hidden; + flex-shrink: 0; +} + +.cl-wrap .cl-check-icon-wrap .cl-check-icon { + width: 18px; + height: 18px; + object-fit: contain; + display: block; + background: transparent !important; + border: none !important; + box-shadow: none !important; +} + +.cl-wrap .cl-check-item img, +.cl-wrap .cl-check-main img { + background: transparent !important; + border: 0 !important; + box-shadow: none !important; + outline: 0 !important; +} + +.cl-wrap .cl-check-icon-wrap .cl-check-icon-fallback { + background: transparent; + border: 1px solid rgba(130, 140, 155, 0.4); + color: rgba(130, 140, 155, 0.9); +} + +body.dark .cl-wrap .cl-check-icon-wrap .cl-check-icon, +body.dark .cl-wrap .cl-check-icon-wrap .cl-check-icon, +body[data-theme="dark"] .cl-wrap .cl-check-icon-wrap .cl-check-icon, +html.dark .cl-wrap .cl-check-icon-wrap .cl-check-icon, +html.dark .cl-wrap .cl-check-icon-wrap .cl-check-icon, +html[data-theme="dark"] .cl-wrap .cl-check-icon-wrap .cl-check-icon, +html[data-bs-theme="dark"] .cl-wrap .cl-check-icon-wrap .cl-check-icon { + filter: saturate(0.82) brightness(1.08); +} + +body.dark .cl-wrap.cl-system-page .cl-section h4, +body.dark .cl-wrap.cl-system-page .cl-panel .cbi-section > h3, +body.dark .cl-wrap.cl-system-page .cl-section h4, +body.dark .cl-wrap.cl-system-page .cl-panel .cbi-section > h3, +html[data-theme="dark"] .cl-wrap.cl-system-page .cl-section h4, +html[data-theme="dark"] .cl-wrap.cl-system-page .cl-panel .cbi-section > h3, +html[data-bs-theme="dark"] .cl-wrap.cl-system-page .cl-section h4, +html[data-bs-theme="dark"] .cl-wrap.cl-system-page .cl-panel .cbi-section > h3 { + color: rgba(219, 227, 241, 0.92) !important; +} + +body.dark .cl-wrap.cl-system-page .cl-tab, +body.dark .cl-wrap.cl-config-page .cl-tab, +body.dark .cl-wrap.cl-system-page .cl-tab, +body.dark .cl-wrap.cl-config-page .cl-tab, +html[data-theme="dark"] .cl-wrap.cl-system-page .cl-tab, +html[data-theme="dark"] .cl-wrap.cl-config-page .cl-tab, +html[data-bs-theme="dark"] .cl-wrap.cl-system-page .cl-tab, +html[data-bs-theme="dark"] .cl-wrap.cl-config-page .cl-tab { + color: var(--title-color, var(--cl-title-color)) !important; + opacity: 0.88; +} + +body.dark .cl-wrap.cl-system-page .cl-tab.active, +body.dark .cl-wrap.cl-config-page .cl-tab.active, +body.dark .cl-wrap.cl-system-page .cl-tab.active, +body.dark .cl-wrap.cl-config-page .cl-tab.active, +html[data-theme="dark"] .cl-wrap.cl-system-page .cl-tab.active, +html[data-theme="dark"] .cl-wrap.cl-config-page .cl-tab.active, +html[data-bs-theme="dark"] .cl-wrap.cl-system-page .cl-tab.active, +html[data-bs-theme="dark"] .cl-wrap.cl-config-page .cl-tab.active { + color: var(--title-color, var(--cl-title-color)) !important; + opacity: 1; +} + +body.dark .cl-wrap .cl-arch-badge-sys, +body.dark .cl-wrap .cl-arch-badge-sys, +body[data-theme="dark"] .cl-wrap .cl-arch-badge-sys, +html.dark .cl-wrap .cl-arch-badge-sys, +html.dark .cl-wrap .cl-arch-badge-sys, +html[data-theme="dark"] .cl-wrap .cl-arch-badge-sys, +html[data-bs-theme="dark"] .cl-wrap .cl-arch-badge-sys { + background: rgba(122, 180, 255, 0.16); + color: rgba(208, 227, 255, 0.95); + border-color: rgba(122, 180, 255, 0.3); +} + +body.dark .cl-wrap .cl-arch-badge-rec, +body.dark .cl-wrap .cl-arch-badge-rec, +body[data-theme="dark"] .cl-wrap .cl-arch-badge-rec, +html.dark .cl-wrap .cl-arch-badge-rec, +html.dark .cl-wrap .cl-arch-badge-rec, +html[data-theme="dark"] .cl-wrap .cl-arch-badge-rec, +html[data-bs-theme="dark"] .cl-wrap .cl-arch-badge-rec { + background: rgba(104, 199, 135, 0.16); + color: rgba(198, 246, 211, 0.95); + border-color: rgba(104, 199, 135, 0.3); +} + +.cl-wrap.cl-system-page .cl-component-arch-sel { + background: rgba(46, 170, 92, 0.12) !important; + color: #24844d !important; + border-color: rgba(46, 170, 92, 0.28) !important; + font-weight: 700 !important; +} + +.cl-wrap.cl-system-page.cl-theme-dark .cl-component-arch-sel, +body.dark .cl-wrap.cl-system-page .cl-component-arch-sel, +body[data-theme="dark"] .cl-wrap.cl-system-page .cl-component-arch-sel, +html.dark .cl-wrap.cl-system-page .cl-component-arch-sel, +html[data-theme="dark"] .cl-wrap.cl-system-page .cl-component-arch-sel, +html[data-bs-theme="dark"] .cl-wrap.cl-system-page .cl-component-arch-sel { + background: rgba(104, 199, 135, 0.16) !important; + color: rgba(198, 246, 211, 0.95) !important; + border-color: rgba(104, 199, 135, 0.3) !important; +} + +.cl-wrap .cl-live-unit { + font-size: 11px; + margin-left: 3px; + color: var(--cl-live-sub); +} + +.cl-wrap .cl-live-foot { + color: var(--cl-live-foot); +} + +.cl-wrap .cl-live-line.up { + stroke: var(--cl-chart-up); +} + +.cl-wrap .cl-live-area.up { + fill: var(--cl-chart-up); +} + +.cl-wrap .cl-live-line.down { + stroke: var(--cl-chart-down); +} + +.cl-wrap .cl-live-area.down { + fill: var(--cl-chart-down); +} + +.cl-wrap .cl-live-line.conn { + stroke: var(--cl-chart-conn); +} + +.cl-wrap .cl-live-area.conn { + fill: var(--cl-chart-conn); +} + +.cl-wrap .cl-live-card.is-zero .cl-live-num, +.cl-wrap .cl-live-card.is-zero .cl-live-unit, +.cl-wrap .cl-live-card.is-zero .cl-live-foot { + opacity: 1; + color: var(--cl-live-zero); +} + +.cl-wrap .cl-live-card.is-zero .cl-live-num { + font-size: 16px; + text-shadow: none; +} + +.cl-wrap .cl-live-card.is-zero .cl-live-bg-chart { + opacity: 0; +} + +.cl-wrap .cl-live-card.is-active .cl-live-bg-chart { + opacity: 0.9; +} + +.cl-wrap .cl-live-card-up.is-active .cl-live-num { + text-shadow: 0 0 12px rgba(98, 176, 255, 0.34); +} + +.cl-wrap .cl-live-card-down.is-active .cl-live-num { + text-shadow: 0 0 12px rgba(155, 134, 255, 0.34); +} + +.cl-wrap .cl-live-card-conn.is-active .cl-live-num { + text-shadow: 0 0 12px rgba(102, 213, 155, 0.34); +} + +.cl-wrap .cl-live-card-up.is-hot .cl-live-num { + text-shadow: 0 0 18px rgba(98, 176, 255, 0.54); +} + +.cl-wrap .cl-live-card-down.is-hot .cl-live-num { + text-shadow: 0 0 18px rgba(155, 134, 255, 0.54); +} + +.cl-wrap .cl-live-card-conn.is-hot .cl-live-num { + text-shadow: 0 0 18px rgba(102, 213, 155, 0.54); +} + +.cl-wrap .cl-live-card.is-hot .cl-live-unit, +.cl-wrap .cl-live-card.is-hot .cl-live-foot { + color: rgba(245, 250, 255, 0.82); +} + +body.dark .cl-wrap .cl-live-num, +body.dark .cl-wrap .cl-live-num, +html[data-theme="dark"] .cl-wrap .cl-live-num, +html[data-bs-theme="dark"] .cl-wrap .cl-live-num { + color: rgba(243, 247, 255, 0.85); +} + +.cl-wrap .cl-check-latency { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-variant-numeric: tabular-nums; +} + +.cl-wrap .cl-live-card.is-offline .cl-live-title, +.cl-wrap .cl-live-card.is-offline .cl-live-foot { + color: var(--cl-live-foot); +} + +@media (max-width: 640px) { + .cl-wrap .cl-live-grid { + grid-template-columns: 1fr !important; + gap: 10px; + } + + .cl-wrap .cl-live-card { + height: 120px; + min-height: 120px; + max-height: 120px; + } +} + +@media (max-width: 768px) { + .cl-wrap { + --cl-primary: rgba(88, 166, 255, 0.92); + --cl-primary-border: rgba(88, 166, 255, 0.48); + --cl-primary-soft: rgba(88, 166, 255, 0.12); + --cl-primary-soft-hover: rgba(88, 166, 255, 0.2); + } + + .cl-wrap input, + .cl-wrap select, + .cl-wrap textarea, + .cl-wrap .cbi-input-text, + .cl-wrap .cbi-input-select { + font-size: 13px !important; + height: auto !important; + line-height: 1.35 !important; + padding: 5px 8px !important; + } + + .cl-wrap .cbi-value-title, + .cl-wrap .cl-card .lbl, + .cl-wrap .cl-section h4 { + font-size: 14px !important; + font-weight: 700 !important; + margin-bottom: 5px !important; + } + + .cl-wrap.cl-form-page .cbi-value-field { + width: 100% !important; + } + + .cl-wrap.cl-form-page .cl-control-wrap { + width: 100% !important; + max-width: 100% !important; + } + + .cl-wrap.cl-form-page .cl-control-wrap .cbi-input-text, + .cl-wrap.cl-form-page .cl-control-wrap .cbi-input-select, + .cl-wrap.cl-form-page .cl-control-wrap select, + .cl-wrap.cl-form-page .cl-control-wrap input[type="text"], + .cl-wrap.cl-form-page .cl-control-wrap input[type="number"], + .cl-wrap.cl-form-page .cl-control-wrap input[type="password"], + .cl-wrap.cl-form-page .cl-control-wrap textarea { + width: 100% !important; + max-width: 100% !important; + padding: 5px 8px !important; + } + + .cl-wrap.cl-form-page .cl-control-wrap .cbi-input-select, + .cl-wrap.cl-form-page .cl-control-wrap select { + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + } + + .cl-wrap.cl-config-page .cl-fixed-600, + .cl-wrap.cl-config-page .cl-rewrite-wrap, + .cl-wrap.cl-config-page .cl-template-row, + .cl-wrap.cl-config-page .cl-template-select-wrap { + width: 100% !important; + max-width: 100% !important; + } + + .cl-wrap.cl-config-page .cl-template-row { + flex-wrap: wrap; + } + + .cl-wrap.cl-config-page .cl-btn-template-upload { + flex-shrink: 0; + } + + .cl-wrap .cl-active-badge { + white-space: nowrap !important; + flex-shrink: 0 !important; + } + + .cl-wrap .cl-pass-wrap { + width: 100% !important; + } +} + +/* ===== Denoise pass (small scope): config + overview ===== */ +.cl-wrap.cl-config-page, +.cl-wrap.cl-overview-page { + color: var(--text-color, inherit); +} + +.cl-wrap.cl-config-page .cl-card, +.cl-wrap.cl-config-page .cl-file-item, +.cl-wrap.cl-overview-page .cl-card, +.cl-wrap.cl-overview-page .cl-live-box, +.cl-wrap.cl-overview-page .cl-live-card, +.cl-wrap.cl-overview-page .cl-ctrl { + background: var(--cl-card-bg) !important; +} + +.cl-wrap.cl-config-page .cl-section h4, +.cl-wrap.cl-config-page .cbi-section > h3, +.cl-wrap.cl-overview-page .cl-card .lbl { + color: var(--title-color, var(--cl-title-color)) !important; + font-size: 0.95rem !important; + font-weight: 500 !important; + letter-spacing: 0 !important; +} + +.cl-wrap.cl-config-page .cl-section h4, +.cl-wrap.cl-config-page .cbi-section > h3 { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px !important; +} + +.cl-wrap.cl-config-page .cl-section h4::before, +.cl-wrap.cl-config-page .cbi-section > h3::before { + content: ''; + width: 3px; + height: 14px; + border-radius: 2px; + background: var(--primary-color, var(--cl-primary)); + flex: 0 0 3px; +} + +.cl-wrap.cl-config-page input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-config-page select, +.cl-wrap.cl-config-page textarea, +.cl-wrap.cl-overview-page input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-overview-page select, +.cl-wrap.cl-overview-page textarea { + background: var(--cl-input-bg) !important; + border: 1px solid var(--cl-input-border) !important; + border-radius: var(--border-radius, 8px) !important; + box-shadow: none !important; + color: var(--text-color, inherit) !important; +} + +.cl-wrap.cl-config-page .cl-file-name-text { + color: var(--text-color, inherit) !important; + opacity: 0.7; +} + +.cl-wrap.cl-config-page .cl-btn-template-upload, +.cl-wrap.cl-config-page .cl-btn-upload-config, +.cl-wrap.cl-config-page .cl-btn-sb-upload, +.cl-wrap.cl-config-page .cbi-button-action { + background: rgba(128, 128, 128, 0.1) !important; + color: var(--primary-color, var(--cl-primary)) !important; + border: 1px solid rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.28) !important; + box-shadow: none !important; +} + +.cl-wrap.cl-overview-page .cl-cards { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; +} + +.cl-wrap.cl-overview-page .cl-card-traffic .val { + font-size: inherit; + font-weight: 400; + word-break: normal; +} + +.cl-wrap.cl-overview-page .cl-card-traffic .cl-live-box { + padding: 0 !important; + border: 0 !important; + box-shadow: none !important; + background: transparent !important; +} + +.cl-wrap.cl-overview-page .cl-check-item img, +.cl-wrap.cl-overview-page .cl-check-main img, +.cl-wrap.cl-overview-page .cl-check-icon-wrap, +.cl-wrap.cl-overview-page .cl-check-icon { + background: transparent !important; + border: 0 !important; + box-shadow: none !important; +} + +.cl-wrap.cl-overview-page .cl-access-wrap { + margin-bottom: 12px; +} + +.cl-wrap.cl-overview-page .cl-status-note { + font-size: 11px; + margin-left: 6px; + color: var(--text-color, inherit); + opacity: 0.72; +} + +.cl-wrap.cl-overview-page .cl-status-note-pass { + color: #4aa065; + opacity: 0.92; +} + +.cl-wrap.cl-overview-page .cl-status-note-fail, +.cl-wrap.cl-overview-page .cl-status-note-degraded { + color: #b56f00; + opacity: 0.92; +} + +@media (max-width: 900px) { + .cl-wrap.cl-overview-page .cl-cards { + grid-template-columns: 1fr !important; + } +} + +/* ===== Overview unified theme-adaptive surfaces ===== */ +.cl-wrap.cl-overview-page { + background: transparent !important; + color: var(--text-color, inherit) !important; +} + +.cl-wrap.cl-overview-page .cl-card, +.cl-wrap.cl-overview-page .cl-live-box, +.cl-wrap.cl-overview-page .cl-live-card, +.cl-wrap.cl-overview-page .cl-ctrl, +.cl-wrap.cl-overview-page .cl-access-wrap { + background: var(--cl-card-bg) !important; + border: 1px solid var(--cl-surface-border) !important; + border-radius: var(--border-radius, 12px) !important; + box-shadow: var(--card-shadow, none) !important; +} + +.cl-wrap.cl-overview-page input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-overview-page select, +.cl-wrap.cl-overview-page textarea, +.cl-wrap.cl-overview-page .cbi-input-select { + background: var(--cl-input-bg) !important; + border: 1px solid var(--cl-input-border) !important; + border-radius: var(--border-radius, 8px) !important; + color: var(--text-color, inherit) !important; +} + +.cl-wrap.cl-overview-page .cl-ctrl-row { + display: flex; + align-items: center; + gap: 8px; +} + +/* inline result of panel update (replaces the old toast) */ +.cl-wrap.cl-overview-page .cl-panel-status { + font-size: 13px; + line-height: 1.2; + white-space: nowrap; + opacity: 0; + transition: opacity .2s ease; +} +.cl-wrap.cl-overview-page .cl-panel-status.ok, +.cl-wrap.cl-overview-page .cl-panel-status.err { + opacity: 1; +} +.cl-wrap.cl-overview-page .cl-panel-status.ok { color: #16a34a; } +.cl-wrap.cl-overview-page .cl-panel-status.err { color: #dc2626; } + +.cl-wrap.cl-overview-page .cl-ctrl-row select, +.cl-wrap.cl-overview-page .cl-ctrl-row .cbi-input-select, +.cl-wrap.cl-overview-page .cl-btn-panel-update, +.cl-wrap.cl-overview-page .cl-btn-panel-open { + height: 32px !important; + line-height: 32px !important; + min-height: 32px !important; + margin: 0 !important; + box-sizing: border-box !important; +} + +.cl-wrap.cl-overview-page .cl-ctrl-row select, +.cl-wrap.cl-overview-page .cl-ctrl-row .cbi-input-select { + padding: 0 10px !important; + line-height: 28px !important; + font-size: 14px !important; + vertical-align: middle !important; +} + +.cl-wrap.cl-overview-page .cl-btn-panel-update, +.cl-wrap.cl-overview-page .cl-btn-panel-open { + padding: 0 12px !important; + border: 1px solid var(--cl-input-border) !important; + border-radius: 7px !important; + background: var(--cl-input-bg) !important; + color: var(--text-color, inherit) !important; + box-shadow: none !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + text-decoration: none !important; +} + +.cl-wrap.cl-overview-page .cl-controls .cl-ctrl select, +.cl-wrap.cl-overview-page .cl-controls .cl-ctrl .cbi-input-select { + background: var(--cl-input-bg) !important; + border: 1px solid var(--cl-input-border) !important; + color: var(--text-color, inherit) !important; + height: 32px !important; + line-height: 32px !important; + min-height: 32px !important; + padding: 0 10px !important; + box-sizing: border-box !important; +} + +.cl-wrap.cl-overview-page .cl-card-mode .cl-mode-row { + display: grid !important; + grid-template-columns: 142px 83px 30px !important; + align-items: center !important; + column-gap: 4px !important; + width: 263px !important; + max-width: 100% !important; + margin: 0 !important; +} + +.cl-wrap.cl-overview-page .cl-card-mode .cl-mode-proto { + text-align: left !important; +} + +.cl-wrap.cl-overview-page .cl-card-mode .cl-mode-name { + text-align: left !important; + padding-right: 0 !important; + border-left: 0 !important; +} + +.cl-wrap.cl-overview-page .cl-card-mode .cl-mode-suffix { + text-align: left !important; + padding-right: 0 !important; + padding-left: 2px !important; + opacity: 0.62 !important; +} + +/* Unified title/body colors for overview (light + dark via theme vars) */ +.cl-wrap.cl-overview-page .cl-card .lbl, +.cl-wrap.cl-overview-page .cl-live-title, +.cl-wrap.cl-overview-page .cl-status-note, +.cl-wrap.cl-overview-page .cl-check-updated { + color: var(--title-color, var(--text-color, inherit)) !important; + opacity: 1 !important; +} + +.cl-wrap.cl-overview-page .cl-card .lbl { + font-size: 13px !important; + font-weight: 600 !important; +} + +.cl-wrap.cl-overview-page .cl-mode-name, +.cl-wrap.cl-overview-page .cl-live-num, +.cl-wrap.cl-overview-page .cl-live-unit, +.cl-wrap.cl-overview-page .cl-live-foot, +.cl-wrap.cl-overview-page .cl-check-latency, +.cl-wrap.cl-overview-page .cl-check-name, +.cl-wrap.cl-overview-page .cl-badge, +.cl-wrap.cl-overview-page .cl-ctrl label, +.cl-wrap.cl-overview-page .cl-ctrl, +.cl-wrap.cl-overview-page .cl-card .val, +.cl-wrap.cl-overview-page .cl-controls .cl-ctrl select, +.cl-wrap.cl-overview-page .cl-controls .cl-ctrl .cbi-input-select, +.cl-wrap.cl-overview-page .cl-btn-panel-update, +.cl-wrap.cl-overview-page .cl-btn-panel-open { + color: var(--text-color, inherit) !important; +} + +/* ===== Theme-system unification (light + dark) ===== */ +.cl-wrap .cl-card, +.cl-wrap .cl-live-box, +.cl-wrap .cl-live-card, +.cl-wrap .cl-ctrl, +.cl-wrap .cl-access-wrap { + background: var(--cl-card-bg) !important; + border: 1px solid var(--cl-surface-border) !important; + border-radius: var(--border-radius, var(--cl-radius, 12px)) !important; + box-shadow: var(--card-shadow, var(--cl-card-shadow)) !important; +} + +.cl-wrap input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap select, +.cl-wrap textarea, +.cl-wrap .cbi-input-text, +.cl-wrap .cbi-input-select { + background: var(--cl-input-bg) !important; + border: 1px solid var(--cl-input-border) !important; + color: var(--text-color, inherit) !important; +} + +.cl-wrap .cbi-value-title, +.cl-wrap .cl-card .lbl, +.cl-wrap .cl-live-title, +.cl-wrap .cl-section > h4 { + color: var(--title-color, var(--text-color, inherit)) !important; +} + +.cl-wrap .cbi-section-descr, +.cl-wrap .cl-card .val, +.cl-wrap .cl-ctrl, +.cl-wrap .cl-status-note, +.cl-wrap .cl-check-updated { + color: var(--text-color, inherit) !important; +} + +.cl-wrap.cl-overview-page #cl-status-val .cl-status-note-fail, +.cl-wrap.cl-overview-page #cl-status-val .cl-status-note-degraded, +.cl-wrap.cl-overview-page .cl-card-mode .cl-mode-degraded { + color: #b56f00 !important; + opacity: 0.92 !important; +} + +.cl-wrap.cl-overview-page #cl-status-val .cl-status-note-pass { + color: #4aa065 !important; + opacity: 0.92 !important; +} + +.cl-wrap.cl-overview-page .cl-ctrl-row select, +.cl-wrap.cl-overview-page .cl-ctrl-row .cbi-input-select, +.cl-wrap.cl-overview-page .cl-btn-panel-update, +.cl-wrap.cl-overview-page .cl-btn-panel-open { + height: 32px !important; + min-height: 32px !important; + line-height: 32px !important; + vertical-align: middle !important; +} + +.cl-wrap.cl-overview-page .cl-config-row { + display: flex !important; + align-items: center !important; + gap: 8px !important; +} + +.cl-wrap.cl-overview-page .cl-config-row select, +.cl-wrap.cl-overview-page .cl-config-row .cbi-input-select { + flex: 1 1 auto !important; + min-width: 0 !important; +} + +.cl-wrap.cl-overview-page .cl-config-row .cl-btn-update-sub { + height: 32px !important; + min-height: 32px !important; + line-height: 32px !important; + padding: 0 12px !important; + margin: 0 !important; + border: 1px solid var(--cl-input-border) !important; + border-radius: var(--border-radius, 8px) !important; + background: var(--cl-input-bg) !important; + color: var(--text-color, inherit) !important; + box-shadow: none !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + white-space: nowrap !important; +} + +/* ===== Config page consistency pass (subs/proxy/dns + sing-box profiles/wizard) ===== */ +.cl-wrap.cl-config-page #cl-panel-subs .cl-section.cl-card, +.cl-wrap.cl-config-page #cl-panel-profiles .cl-section.cl-card, +.cl-wrap.cl-config-page #cl-panel-wizard .cl-section.cl-card, +.cl-wrap.cl-config-page #cl-panel-proxy .cbi-section, +.cl-wrap.cl-config-page #cl-panel-dns .cbi-section { + background: var(--cl-card-bg) !important; + border: 1px solid var(--cl-surface-border) !important; + border-bottom: 1px solid var(--cl-surface-border) !important; + border-radius: var(--border-radius, var(--cl-radius, 12px)) !important; + box-shadow: var(--card-shadow, var(--cl-card-shadow)) !important; + padding: 16px 18px !important; + margin: 0 0 14px !important; +} + +.cl-wrap.cl-config-page #cl-panel-subs .cl-section > h4, +.cl-wrap.cl-config-page #cl-panel-profiles .cl-section > h4, +.cl-wrap.cl-config-page #cl-panel-wizard .cl-section > h4, +.cl-wrap.cl-config-page #cl-panel-proxy .cbi-section > h3, +.cl-wrap.cl-config-page #cl-panel-dns .cbi-section > h3 { + display: flex !important; + align-items: center !important; + gap: 8px !important; + margin: 0 0 12px !important; + background: transparent !important; + border: 0 !important; + box-shadow: none !important; + padding: 16px 20px !important; + font-size: 0.95rem !important; + font-weight: 600 !important; + color: var(--title-color, var(--cl-title-color)) !important; + opacity: 1 !important; +} + +.cl-wrap.cl-config-page #cl-panel-subs .cl-section > h4::before, +.cl-wrap.cl-config-page #cl-panel-profiles .cl-section > h4::before, +.cl-wrap.cl-config-page #cl-panel-wizard .cl-section > h4::before, +.cl-wrap.cl-config-page #cl-panel-proxy .cbi-section > h3::before, +.cl-wrap.cl-config-page #cl-panel-dns .cbi-section > h3::before { + content: '' !important; + width: 3px !important; + height: 14px !important; + border-radius: 2px !important; + background: var(--primary-color, var(--cl-primary)) !important; + flex: 0 0 3px !important; +} + +.cl-wrap.cl-config-page #cl-panel-proxy .cbi-value-title, +.cl-wrap.cl-config-page #cl-panel-dns .cbi-value-title { + color: var(--cl-label-muted) !important; +} + +.cl-wrap.cl-config-page #cl-panel-subs .cl-fixed-600, +.cl-wrap.cl-config-page #cl-panel-subs .cl-rewrite-wrap, +.cl-wrap.cl-config-page #cl-panel-profiles .cl-fixed-600, +.cl-wrap.cl-config-page #cl-panel-profiles .cl-sb-form, +.cl-wrap.cl-config-page #cl-panel-wizard .cl-fixed-600, +.cl-wrap.cl-config-page #cl-panel-wizard .cl-sb-form, +.cl-wrap.cl-config-page #cl-panel-proxy .cbi-value-field, +.cl-wrap.cl-config-page #cl-panel-dns .cbi-value-field, +.cl-wrap.cl-config-page #cl-panel-proxy .cl-control-wrap, +.cl-wrap.cl-config-page #cl-panel-dns .cl-control-wrap { + max-width: 600px !important; +} + +.cl-wrap.cl-config-page #cl-panel-subs .cl-sub-url, +.cl-wrap.cl-config-page #cl-panel-subs .cl-rewrite-wrap .cbi-input-select, +.cl-wrap.cl-config-page #cl-panel-subs .cl-rewrite-wrap select, +.cl-wrap.cl-config-page #cl-panel-wizard .cl-sub-url, +.cl-wrap.cl-config-page #cl-panel-profiles .cl-json-editor, +.cl-wrap.cl-config-page #cl-panel-proxy .cbi-value-field input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-config-page #cl-panel-proxy .cbi-value-field select, +.cl-wrap.cl-config-page #cl-panel-proxy .cbi-value-field textarea, +.cl-wrap.cl-config-page #cl-panel-dns .cbi-value-field input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-config-page #cl-panel-dns .cbi-value-field select, +.cl-wrap.cl-config-page #cl-panel-dns .cbi-value-field textarea { + background: var(--cl-input-bg) !important; + border: 1px solid var(--cl-input-border) !important; + border-bottom: 1px solid var(--cl-input-border) !important; + border-radius: var(--border-radius, 8px) !important; + color: var(--cl-input-color) !important; + box-shadow: none !important; + min-height: 38px !important; + padding: 0 12px !important; +} + +.cl-wrap.cl-config-page #cl-panel-subs .cl-rewrite-wrap textarea, +.cl-wrap.cl-config-page #cl-panel-proxy .cbi-value-field textarea, +.cl-wrap.cl-config-page #cl-panel-dns .cbi-value-field textarea { + min-height: 84px !important; + height: auto !important; + padding: 8px 12px !important; +} + +/* 大文件回退的原生 textarea 编辑器尺寸(CM 自带尺寸,不受此规则影响)*/ +.cl-wrap.cl-config-page #cl-panel-profiles .cl-json-editor, +.cl-wrap.cl-config-page #cl-panel-subs .cl-json-editor { + min-height: 420px !important; + height: auto !important; + padding: 10px 14px !important; + font-family: ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, monospace !important; + font-size: 12.5px !important; + line-height: 1.6 !important; + resize: vertical !important; + white-space: pre !important; +} + +.cl-wrap.cl-config-page #cl-panel-subs .cl-file-item, +.cl-wrap.cl-config-page #cl-panel-profiles .cl-sub-list { + background: var(--cl-card-bg) !important; + border: 1px solid var(--cl-surface-border) !important; + border-radius: var(--border-radius, 8px) !important; + box-shadow: none !important; +} + +.cl-wrap.cl-config-page #cl-panel-subs .cl-rewrite-wrap, +.cl-wrap.cl-config-page #cl-panel-subs .cl-rewrite-group { + background: transparent !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + padding-left: 0 !important; + padding-right: 0 !important; +} + +.cl-wrap.cl-config-page #cl-panel-subs .cl-rewrite-group-template, +.cl-wrap.cl-config-page #cl-panel-subs .cl-rewrite-group-input, +.cl-wrap.cl-config-page #cl-panel-subs .cl-template-row, +.cl-wrap.cl-config-page #cl-panel-subs .cl-template-select-wrap { + background: transparent !important; + border: 0 !important; + box-shadow: none !important; +} + +/* ===== System page consistency pass (kernel/rules/logs) ===== */ +.cl-wrap.cl-system-page #cl-panel-kernel .cbi-section, +.cl-wrap.cl-system-page #cl-panel-rules .cbi-section, +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-card, +.cl-wrap.cl-system-page #cl-panel-logs .cl-section.cl-card { + background: var(--cl-card-bg) !important; + border: 1px solid var(--cl-surface-border) !important; + border-bottom: 1px solid var(--cl-surface-border) !important; + border-radius: var(--border-radius, var(--cl-radius, 12px)) !important; + box-shadow: var(--card-shadow, var(--cl-card-shadow)) !important; + padding: 16px 18px !important; + margin: 0 0 14px !important; +} + +/* 组件更新外层容器:保持透明,让内容卡片与其他页面满宽对齐 */ +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-map { + background: transparent !important; + padding: 0 !important; + border-radius: 0 !important; + margin: 0 0 14px !important; +} + +.cl-wrap.cl-system-page #cl-panel-kernel .cbi-section > h3, +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-head > div > h4, +.cl-wrap.cl-system-page #cl-panel-rules .cbi-section > h3, +.cl-wrap.cl-system-page #cl-panel-logs .cl-section > h4 { + display: flex !important; + align-items: center !important; + gap: 8px !important; + margin: 0 0 12px !important; + padding: 0 !important; + background: transparent !important; + border: 0 !important; + box-shadow: none !important; + font-size: 0.95rem !important; + font-weight: 600 !important; + color: var(--title-color, var(--cl-title-color)) !important; + opacity: 1 !important; +} + +.cl-wrap.cl-system-page #cl-panel-kernel .cbi-section > h3::before, +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-head > div > h4::before, +.cl-wrap.cl-system-page #cl-panel-rules .cbi-section > h3::before, +.cl-wrap.cl-system-page #cl-panel-logs .cl-section > h4::before { + content: '' !important; + width: 3px !important; + height: 14px !important; + border-radius: 2px !important; + background: var(--primary-color, var(--cl-primary)) !important; + flex: 0 0 3px !important; +} + +.cl-wrap.cl-system-page #cl-panel-kernel .cbi-value-title, +.cl-wrap.cl-system-page #cl-panel-rules .cbi-value-title, +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-desc { + color: var(--cl-label-muted) !important; +} + +/* 备份与还原:按钮行,自动换行以适配移动端 */ +.cl-wrap.cl-system-page #cl-panel-kernel .cl-backup-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.cl-wrap.cl-system-page #cl-panel-kernel .cbi-value-field, +.cl-wrap.cl-system-page #cl-panel-rules .cbi-value-field, +.cl-wrap.cl-system-page #cl-panel-kernel .cl-control-wrap, +.cl-wrap.cl-system-page #cl-panel-rules .cl-control-wrap { + max-width: 600px !important; +} + +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-arch-row, +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-row, +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-log { + background: var(--cl-card-bg) !important; + border: 1px solid var(--cl-surface-border) !important; + border-radius: var(--border-radius, 8px) !important; + box-shadow: none !important; +} + +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-adv-bar { + background: var(--cl-control-bg) !important; + border: 1px solid var(--cl-control-border) !important; + border-radius: var(--border-radius, 8px) !important; +} + +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-adv-bar:hover { + background: var(--cl-primary-soft) !important; +} + +.cl-wrap.cl-system-page #cl-panel-kernel .cbi-value-field input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-system-page #cl-panel-kernel .cbi-value-field select, +.cl-wrap.cl-system-page #cl-panel-kernel .cbi-value-field textarea, +.cl-wrap.cl-system-page #cl-panel-rules .cbi-value-field input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-system-page #cl-panel-rules .cbi-value-field select, +.cl-wrap.cl-system-page #cl-panel-rules .cbi-value-field textarea { + background: var(--cl-input-bg) !important; + border: 1px solid var(--cl-input-border) !important; + border-bottom: 1px solid var(--cl-input-border) !important; + border-radius: var(--border-radius, 8px) !important; + color: var(--cl-input-color) !important; + box-shadow: none !important; + min-height: 38px !important; + padding: 0 12px !important; +} + +.cl-wrap.cl-system-page #cl-panel-kernel .cbi-value-field textarea, +.cl-wrap.cl-system-page #cl-panel-rules .cbi-value-field textarea { + min-height: 84px !important; + height: auto !important; + padding: 8px 12px !important; +} + +.cl-wrap.cl-system-page #cl-panel-logs .cl-log-area { + background: var(--cl-input-bg) !important; + border: 1px solid var(--cl-input-border) !important; + border-radius: var(--border-radius, 8px) !important; + color: var(--cl-field-color) !important; + box-shadow: none !important; +} + +.cl-wrap.cl-system-page #cl-panel-logs .cl-log-tab { + background: var(--cl-input-bg) !important; + border: 1px solid var(--cl-input-border) !important; + color: var(--cl-field-color) !important; + box-shadow: none !important; +} + +.cl-wrap.cl-system-page #cl-panel-logs .cl-log-tab.active { + background: rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.12) !important; + border-color: rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.24) !important; + color: var(--cl-primary) !important; +} + +/* ===== Final polish: remove traffic inner borders + minimalist selects ===== */ +.cl-wrap.cl-overview-page .cl-card-traffic .cl-live-box, +.cl-wrap.cl-overview-page .cl-card-traffic .cl-live-grid, +.cl-wrap.cl-overview-page .cl-card-traffic .cl-live-card, +.cl-wrap.cl-overview-page .cl-card-traffic .cl-stat-card { + border: none !important; + box-shadow: none !important; +} + +.cl-wrap.cl-overview-page .cl-card-traffic .cl-live-card, +.cl-wrap.cl-overview-page .cl-card-traffic .cl-stat-card { + background: var(--cl-live-card-bg) !important; +} + +.cl-wrap select, +.cl-wrap .cbi-input-select { + border-top: none !important; + border-left: none !important; + border-right: none !important; + border-bottom: 1px solid rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.2) !important; + border-radius: 0 !important; + box-shadow: none !important; +} + +.cl-wrap.cl-overview-page .cbi-input-select, +.cl-wrap.cl-overview-page .cl-ctrl-row select, +.cl-wrap.cl-overview-page .cl-controls .cl-ctrl select, +.cl-wrap.cl-overview-page .cl-controls .cl-ctrl .cbi-input-select, +.cl-wrap.cl-config-page #cl-panel-subs .cbi-input-select, +.cl-wrap.cl-config-page #cl-panel-subs select, +.cl-wrap.cl-config-page #cl-panel-proxy .cbi-value-field select, +.cl-wrap.cl-config-page #cl-panel-dns .cbi-value-field select, +.cl-wrap.cl-system-page #cl-panel-kernel .cbi-value-field select, +.cl-wrap.cl-system-page #cl-panel-rules .cbi-value-field select { + border-top: none !important; + border-left: none !important; + border-right: none !important; + border-bottom: 1px solid rgba(var(--primary-rgb, var(--cl-primary-rgb)), 0.2) !important; + border-radius: 0 !important; + box-shadow: none !important; +} + +.cl-wrap.cl-system-page #cl-panel-kernel .cl-component-arch-row .cl-component-arch-sel { + background: rgba(46, 170, 92, 0.12) !important; + color: #24844d !important; + border: 1px solid rgba(46, 170, 92, 0.28) !important; + border-radius: 6px !important; + box-shadow: none !important; + font-weight: 700 !important; +} + +.cl-wrap.cl-system-page.cl-theme-dark #cl-panel-kernel .cl-component-arch-row .cl-component-arch-sel, +body.dark .cl-wrap.cl-system-page #cl-panel-kernel .cl-component-arch-row .cl-component-arch-sel, +body[data-theme="dark"] .cl-wrap.cl-system-page #cl-panel-kernel .cl-component-arch-row .cl-component-arch-sel, +html.dark .cl-wrap.cl-system-page #cl-panel-kernel .cl-component-arch-row .cl-component-arch-sel, +html[data-theme="dark"] .cl-wrap.cl-system-page #cl-panel-kernel .cl-component-arch-row .cl-component-arch-sel, +html[data-bs-theme="dark"] .cl-wrap.cl-system-page #cl-panel-kernel .cl-component-arch-row .cl-component-arch-sel { + background: rgba(104, 199, 135, 0.16) !important; + color: rgba(198, 246, 211, 0.95) !important; + border-color: rgba(104, 199, 135, 0.3) !important; +} + +/* ===== Overview status spacing + semantic run state ===== */ +.cl-wrap.cl-overview-page .cl-card-head { + margin-bottom: 10px !important; +} + +.cl-wrap.cl-overview-page .cl-card .val { + display: block; + line-height: 1.55 !important; +} + +.cl-wrap.cl-overview-page #cl-status-val { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.cl-wrap.cl-overview-page #cl-status-val .cl-status-note { + margin-left: 0 !important; +} + +.cl-wrap.cl-overview-page .cl-status-state { + display: inline-flex; + align-items: center; + padding: 0 !important; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; + color: inherit; + font-size: 11px; + font-weight: 500; + line-height: 1.35; +} + +.cl-wrap.cl-overview-page .cl-status-state-run { + color: #24945f !important; +} + +.cl-wrap.cl-overview-page .cl-status-state-stop { + color: rgba(128, 128, 128, 0.9) !important; +} + +/* ─── Theme-override: force transparent bg on LuCI form elements ─── + 不论哪个主题(Argon / Bootstrap / Material 等),只要给表格行 / + form 行 / 列表项上了斑马纹,透过我们的透明元素就会显出底色杂色。 + 这里统一 reset 所有 LuCI 核心容器的背景,主题无关。 */ +.cl-wrap table tbody tr, +.cl-wrap table tbody tr:nth-of-type(2n), +.cl-wrap .table .tbody .tr, +.cl-wrap .table .tbody .tr:nth-of-type(2n), +.cl-wrap .cbi-value, +.cl-wrap .cbi-value:nth-of-type(2n), +.cl-wrap .cbi-rowstyle-1, +.cl-wrap .cbi-rowstyle-2, +.cl-wrap .cbi-section-table-row, +.cl-wrap .cbi-dynlist, +.cl-wrap .cbi-dynlist .item, +.cl-wrap .cbi-dynlist .item:nth-of-type(2n), +.cl-wrap .cbi-dynlist .add-item { + background: transparent !important; +} + +/* DynamicList items: config 页面不加外框(避免双框),system 页面 IP 主机需要外框 */ +.cl-wrap .cbi-dynlist .item { + color: inherit !important; + margin-bottom: 4px !important; +} +.cl-wrap.cl-system-page .cbi-dynlist .item { + border: 1px solid rgba(128, 128, 128, 0.22) !important; + border-radius: 6px !important; + padding: 4px 10px !important; +} +.cl-wrap .cbi-dynlist .item.flash { + animation: none !important; + background: rgba(var(--primary-rgb, 0, 122, 255), 0.08) !important; +} + +/* DynamicList 删除/添加按钮在暗色下对比度 */ +.cl-wrap .cbi-dynlist .item > span.close, +.cl-wrap .cbi-dynlist .item > .close { + color: #c96a5a !important; + opacity: 0.8; +} + +/* ─── Bug 4: 限制内联 Value 输入框宽度,不撑满父容器 ─── */ +.cl-wrap .cbi-value input[type="text"], +.cl-wrap .cbi-value input[type="password"], +.cl-wrap .cbi-value-field > .cbi-input-text, +.cl-wrap .cbi-value-field > input.cbi-input-text { + max-width: 480px; + box-sizing: border-box; +} + +/* issue #14: DynamicList 窄屏自适应 — 行宽跟容器走、输入框可收缩, + 避免删除(×)/添加(+)按钮在窄手机上被挤出屏幕。 + 仅在窄屏触发;桌面端沿用 LuCI 默认尺寸,避免不同 section 的父容器宽度差异 + 把 DynamicList 撑到 600/1450 等不同长度,造成同页上下输入框宽窄不一。 */ +@media (max-width: 768px) { + .cl-wrap .cbi-dynlist { + display: flex !important; + flex-direction: column; + width: 100%; + max-width: 100%; + box-sizing: border-box; + } + .cl-wrap .cbi-dynlist .item, + .cl-wrap .cbi-dynlist .add-item { + width: 100%; + max-width: 100%; + box-sizing: border-box; + } + .cl-wrap .cbi-dynlist .item > span { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + } + .cl-wrap .cbi-dynlist .add-item { + display: flex; + align-items: center; + gap: 6px; + } + .cl-wrap .cbi-dynlist .add-item > .cbi-input-text, + .cl-wrap .cbi-dynlist .add-item > input[type="text"] { + flex: 1 1 auto; + min-width: 0; + max-width: 100% !important; + } +} + +/* 一键测速并应用:状态文案 — 浅色主题下与同页 .cbi-value-description + (Fake-IP / Redir-Host 单选描述)保持同色,视觉一致 */ +.cl-wrap.cl-theme-light .cl-dns-auto-status { + color: rgba(120, 130, 150, 0.9) !important; +} + +/* Smart 策略设置 — 行动按钮 + 当前版本说明(桌面端:横排,移动端:换行) */ +.cl-smart-action-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} +.cl-smart-action-row .cl-hint { + word-break: break-word; + overflow-wrap: anywhere; +} +@media (max-width: 480px) { + .cl-smart-action-row { + gap: 8px; + } + .cl-smart-action-row .btn { + flex: 0 0 auto; + } + .cl-smart-action-row .cl-hint { + flex: 1 1 100%; + font-size: 12px; + line-height: 1.4; + } +} + +/* 按钮 + 版本时间戳纵向堆叠(按钮上方,版本浅绿小字在下) */ +.cl-btn-ver-wrap { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; +} +.cl-ver-tag { + font-size: 12px; + word-break: break-word; +} +.cl-ver-label { color: rgba(128, 128, 128, 0.6); } +.cl-ver-value { color: #4aa065; } + +/* GeoIP / 内核下载:按钮 + 版本文字横向同行 */ +.cl-btn-ver-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +/* ══════════════════════════════════════════════════════════ + 浅色主题对比度修复 (Argon light / Bootstrap light) + - getThemeClass() 优先检测 dark.css 是否加载,返回 cl-theme-light + - overview 页面不在选择器内,不受影响 + - section 使用白卡片,外层容器保持透明满宽 + ══════════════════════════════════════════════════════════ */ + +/* 1. CSS 变量覆盖 — 实色值取代半透明,确保白底上清晰可见 */ +.cl-wrap.cl-theme-light.cl-config-page, +.cl-wrap.cl-theme-light.cl-system-page { + --cl-card-bg: #ffffff; + --cl-card-border: #d8dce6; + --cl-surface-border: #d8dce6; + --cl-card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + --cl-input-bg: #ffffff; + --cl-input-border: #c5c9d2; +} + +/* 2. 表单外层透明满宽:与订阅页一致,不额外压缩内容区 */ +.cl-wrap.cl-theme-light.cl-config-page .cbi-map, +.cl-wrap.cl-theme-light.cl-system-page .cbi-map { + background: transparent !important; + padding: 0 !important; + border-radius: 0 !important; + margin-top: 0 !important; +} + +/* 3. Section 白底 — config & system 页面共用 */ +.cl-wrap.cl-theme-light.cl-config-page .cl-panel .cbi-section, +.cl-wrap.cl-theme-light.cl-config-page .cl-section.cl-card, +.cl-wrap.cl-theme-light.cl-config-page .cl-panel.cl-card, +.cl-wrap.cl-theme-light.cl-system-page .cl-panel .cbi-section, +.cl-wrap.cl-theme-light.cl-system-page .cl-section.cl-card, +.cl-wrap.cl-theme-light.cl-system-page .cl-panel.cl-card { + background: #ffffff !important; + border: 1px solid #d8dce6 !important; + border-radius: 10px !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07) !important; + padding: 14px 18px 18px !important; + margin-bottom: 12px !important; +} + +/* 4. 输入框 / 下拉框 / 文本域 — 明确边框,白底文字清晰 */ +.cl-wrap.cl-theme-light.cl-config-page input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-theme-light.cl-config-page select, +.cl-wrap.cl-theme-light.cl-config-page textarea, +.cl-wrap.cl-theme-light.cl-system-page input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="file"]), +.cl-wrap.cl-theme-light.cl-system-page select, +.cl-wrap.cl-theme-light.cl-system-page textarea { + background: #ffffff !important; + border: 1px solid #c5c9d2 !important; + border-radius: 6px !important; + color: inherit !important; +} diff --git a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/config.js b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/config.js new file mode 100644 index 000000000..8a9bb6b5d --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/config.js @@ -0,0 +1,1896 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; +'require ui'; +'require rpc'; +'require tools.clashoo as clashoo'; + +function getThemeClass() { + var h = document.documentElement; + // Bootstrap: explicit data-bs-theme attribute wins first + if (h.dataset.bsTheme === 'dark') return 'cl-theme-dark'; + if (h.dataset.bsTheme === 'light') return 'cl-theme-light'; + // Argon: explicit darkmode attribute + if (h.dataset.darkmode === 'true') return 'cl-theme-dark'; + // Argon: dark.css loaded in document = dark mode active (most reliable) + var links = document.querySelectorAll('link[rel="stylesheet"]'); + for (var i = 0; i < links.length; i++) { + if (links[i].href && links[i].href.indexOf('dark.css') !== -1) return 'cl-theme-dark'; + } + // Generic: .dark class on root element + if (h.classList.contains('dark')) return 'cl-theme-dark'; + // Body background luminance (skip transparent/rgba backgrounds) + var bg = window.getComputedStyle(document.body).backgroundColor; + if (bg && bg.indexOf('rgba') === -1 && bg.indexOf('rgb') !== -1) { + var m = bg.match(/\d+/g); + if (m && m.length >= 3 && (parseInt(m[0]) + parseInt(m[1]) + parseInt(m[2])) / 3 < 100) return 'cl-theme-dark'; + } + return 'cl-theme-light'; +} + +var CSS = [ + '.cl-wrap{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;--cl-card-border:rgba(128,128,128,.22);--cl-card-bg:rgba(128,128,128,.08);--cl-card-shadow:0 4px 12px rgba(0,0,0,.08);--cl-muted:rgba(74,85,104,.9);--cl-meta:var(--cl-muted);--cl-primary:rgba(0,122,255,.8);--cl-primary-border:rgba(0,122,255,.45);--cl-primary-soft:rgba(0,122,255,.08)}', + '.cl-tabs{display:flex;border-bottom:2px solid rgba(128,128,128,.15);margin-bottom:18px}', + '.cl-tab{padding:10px 20px;cursor:pointer;font-size:13px;opacity:.55;border-bottom:2px solid transparent;margin-bottom:-2px;transition:opacity .15s}', + '.cl-tab.active{opacity:1;border-bottom-color:currentColor;font-weight:600}', + '.cl-panel{display:none}.cl-panel.active{display:block}', + '.cl-sub-list{width:100%;border-collapse:collapse;margin:12px 0;font-size:13px;table-layout:fixed}', + '.cl-sub-list th,.cl-sub-list td{padding:8px 10px;border-bottom:1px solid rgba(128,128,128,.15);text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', + '.cl-sub-list th{font-size:11px;opacity:.55;font-weight:600}', + '.cl-sub-list th:nth-child(2),.cl-sub-list td:nth-child(2){width:72px}', + '.cl-sub-list th:nth-child(3),.cl-sub-list td:nth-child(3){width:220px;text-align:right;white-space:nowrap}', + '.cl-sub-url{border:1px solid rgba(128,128,128,.3);border-radius:6px;padding:8px 10px;width:100%;box-sizing:border-box;font-size:13px;margin-bottom:8px}', + '.cl-sub-traffic{margin-top:4px;font-size:11px;color:var(--cl-meta,var(--cl-muted,#666))}', + '.cl-sub-traffic-bar{height:4px;border-radius:2px;background:rgba(128,128,128,.18);margin-top:3px;overflow:hidden}', + '.cl-sub-traffic-fill{height:100%;border-radius:2px;background:var(--primary-color,#0b68dd);transition:width .3s}', + '.cl-sub-traffic-fill.cl-traffic-warn{background:#f59e0b}', + '.cl-sub-traffic-fill.cl-traffic-danger{background:#ef4444}', + '.cl-sub-expire{font-size:11px;color:var(--cl-meta,var(--cl-muted,#666));margin-top:2px}', + '.cl-sub-expire.cl-expire-soon{color:#f59e0b}', + '.cl-btn-sm{padding:4px 10px;font-size:12px;border-radius:4px;cursor:pointer}', + '.cl-section{margin-bottom:24px}', + '.cl-section h4{font-size:1.15rem;font-weight:600;margin-bottom:10px;color:var(--title-color,var(--cl-muted));opacity:.95}', + /* constrain form inputs on desktop, table stays full-width */ + '.cl-form-wrap{max-width:640px}', + '.cl-file-list{display:flex;flex-direction:column;gap:10px;margin-top:10px}', + '.cl-file-item{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid var(--cl-card-border);border-radius:8px;background:var(--cl-card-bg);box-shadow:var(--cl-card-shadow)}', + '.cl-file-item.is-active{background:rgba(var(--primary-rgb,0,122,255),.08);border-color:rgba(var(--primary-rgb,0,122,255),.2)}', + '.cl-file-meta{display:flex;flex:1;min-width:0;align-items:center;justify-content:space-between;gap:12px}', + '.cl-file-name{display:flex;align-items:center;gap:8px;min-width:0}', + '.cl-file-name-text{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', + '.cl-sb-file-name{display:flex;align-items:center;gap:8px;min-width:0;white-space:normal !important;overflow:visible !important;text-overflow:clip !important}', + '.cl-sb-file-name .cl-file-name-text{min-width:0}', + '.cl-file-size{font-size:12px;color:var(--cl-meta);white-space:nowrap}', + '.cl-file-actions{display:flex;align-items:center;gap:8px;flex-shrink:0}', + '.cl-rewrite-wrap{max-width:760px;padding:0;border:0;background:transparent;box-shadow:none}', + '.cl-rewrite-group{display:flex;flex-direction:column;gap:8px}', + '.cl-rewrite-group-title{font-size:12px;font-weight:700;opacity:.68}', + '.cl-rw-divider{height:1px;background:rgba(128,128,128,.18);margin:10px 0}', + '.cl-rewrite-wrap .cl-sub-url,.cl-rewrite-wrap .cbi-input-select{padding:8px 12px;border-radius:6px;width:100%;box-sizing:border-box;margin-bottom:0}', + '.cl-rewrite-actions{margin-top:4px}', + '.cl-actions{display:flex;gap:8px;flex-wrap:wrap}', + '.cl-btn-update-sub{border-color:var(--cl-primary-border);color:var(--cl-primary)}', + '.cl-btn-switch{background:var(--cl-primary-soft);border:1px solid var(--cl-primary-border);color:var(--cl-primary)}', + '.cl-btn-switch:hover{background:rgba(0,122,255,.14);border-color:rgba(0,122,255,.62);color:rgba(0,96,220,.92)}', + '.cl-btn-delete{border:1px solid rgba(var(--primary-rgb,0,122,255),.32);color:var(--cl-primary);background:rgba(var(--primary-rgb,0,122,255),.1)}', + '.cl-btn-delete:hover{background:rgba(var(--primary-rgb,0,122,255),.16);border-color:rgba(var(--primary-rgb,0,122,255),.4);color:var(--cl-primary)}', + '.cl-btn-generate-switch{box-shadow:0 4px 10px rgba(0,0,128,.2)}', + '.cl-save-bar{display:flex;gap:8px;margin-top:14px;padding-top:12px;border-top:1px solid rgba(128,128,128,.15)}', + '.cl-dns-auto{display:flex;flex-direction:column;gap:8px;max-width:640px}', + '.cl-dns-auto-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}', + '.cl-dns-auto-status{font-size:12px;color:var(--cl-meta);line-height:1.55}', + '.cl-dns-auto-result{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:6px 14px;padding:8px 10px;border:1px solid var(--cl-card-border);border-radius:8px;background:var(--cl-card-bg);color:var(--cl-meta);font-size:12px;line-height:1.5}', + '.cl-dns-auto-result b{font-weight:700;opacity:.72;margin-right:4px}', + '.cl-sub-schedule{display:flex;flex-direction:column;gap:5px;max-width:760px;box-sizing:border-box;margin:2px 0 14px;padding:8px 2px 4px 6px}', + '.cl-sub-schedule-row{display:flex;align-items:center;gap:10px;flex-wrap:nowrap}', + '.cl-sub-schedule-toggle{display:inline-flex;align-items:center;gap:8px;height:30px;font-size:13px;font-weight:600;line-height:30px;cursor:pointer;white-space:nowrap}', + '.cl-sub-schedule-toggle input[type="checkbox"]{appearance:none!important;-webkit-appearance:none!important;position:static!important;top:auto!important;bottom:auto!important;width:16px!important;height:16px!important;min-width:16px!important;min-height:16px!important;margin:0!important;padding:0!important;border:1px solid rgba(128,128,128,.7)!important;border-radius:3px!important;background:transparent!important;box-shadow:none!important;vertical-align:middle!important;flex:0 0 16px}', + '.cl-sub-schedule-toggle input[type="checkbox"]:checked{border-color:var(--cl-primary)!important;background:var(--cl-primary) url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Cpath fill=%27none%27 stroke=%27white%27 stroke-width=%272.2%27 d=%27M3 8l3 3 7-7%27/%3E%3C/svg%3E") center/12px 12px no-repeat!important}', + '.cl-sub-schedule-status{font-size:11px;color:var(--cl-meta);line-height:1.45}', + '.cl-sub-schedule-interval{display:inline-flex;align-items:center;gap:6px;height:30px;font-size:12px;line-height:30px;color:var(--cl-meta);white-space:nowrap}', + '.cl-sub-schedule-interval input[type="number"]{width:52px!important;height:30px!important;min-height:30px!important;box-sizing:border-box!important;margin:0!important;padding:2px 6px!important;line-height:24px!important;text-align:center}', + '.cl-sub-schedule-row .btn,.cl-sub-schedule-row .cbi-button{height:30px!important;min-height:30px!important;box-sizing:border-box!important;padding:3px 10px!important;line-height:22px!important}', + '.cl-sub-schedule-row :disabled{cursor:not-allowed;opacity:.62}', + '.cl-section-toggle{font-size:12px;cursor:pointer;flex-shrink:0;margin-left:auto}', + '.cl-collapsible.cl-closed>*:not(h3){display:none!important}', + /* 折叠标题:通过 .clashoo-section-header wrapper class 上色,不裸改 LuCI 默认 h3 样式 */ + '.clashoo-section-header{display:flex!important;align-items:center;gap:8px;position:relative}', + '.clashoo-section-header::before{content:"";flex:0 0 3px;width:3px;height:14px;border-radius:2px;background:var(--primary-color,var(--cl-primary,#007aff))}', + '.cl-wrap .cbi-section-remove.right{background:transparent!important}', + '.cl-json-editor{width:100%;height:340px;font-family:monospace;font-size:11px;border:1px solid rgba(128,128,128,.25);border-radius:8px;padding:10px;box-sizing:border-box;resize:vertical;background:rgba(0,0,0,.02)}', + '.cl-editor-hdr{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:12px;font-weight:600}', + /* CodeMirror 编辑器外观(小文件套 CM,大文件回退 cl-json-editor textarea)*/ + '.cl-cm-wrap{width:100%}', + '.cl-cm-host .CodeMirror{height:340px;border:1px solid rgba(128,128,128,.25);border-radius:8px;font-size:12px;font-family:monospace}', + '.cl-cm-host.cl-cm-dark .CodeMirror{background:#1e2228;color:#d4d4d4}', + '.cl-cm-host.cl-cm-dark .CodeMirror-gutters{background:#23282f;border-right:1px solid rgba(255,255,255,.08)}', + '.cl-cm-host.cl-cm-dark .CodeMirror-linenumber{color:#5c6370}', + '.cl-cm-host.cl-cm-dark .CodeMirror-cursor{border-left-color:#d4d4d4}', + '.cl-cm-host.cl-cm-dark .CodeMirror-selected{background:rgba(255,255,255,.12)}', + '.cl-cm-host.cl-cm-dark .CodeMirror-activeline-background{background:rgba(255,255,255,.04)}', + '.cl-cm-host.cl-cm-dark .cm-string{color:#ce9178}', + '.cl-cm-host.cl-cm-dark .cm-number{color:#b5cea8}', + '.cl-cm-host.cl-cm-dark .cm-keyword,.cl-cm-host.cl-cm-dark .cm-atom{color:#569cd6}', + '.cl-cm-host.cl-cm-dark .cm-property,.cl-cm-host.cl-cm-dark .cm-attribute{color:#9cdcfe}', + '.cl-cm-host.cl-cm-dark .cm-comment{color:#6a9955}', + '.cl-cm-host.cl-cm-dark .cm-meta,.cl-cm-host.cl-cm-dark .cm-def{color:#dcdcaa}', + '.cl-active-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:10px;background:rgba(var(--primary-rgb,0,122,255),.12);color:var(--cl-primary)}', + '.cl-hint{font-size:11px;opacity:.45;margin-left:auto}', + /* hide auto-generated section IDs in TypedSection */ + '.cbi-section-table-titles .cbi-section-table-cell:first-child{display:none}', + '.cbi-section-table-row .cbi-section-table-cell:first-child{display:none}', + '.cl-mode-tabs{display:inline-flex;gap:4px;margin:6px 0}', + '.cl-mode-tab-active{font-weight:700}', + '.cl-panel .cbi-section>h3{font-size:13px !important;font-weight:600;margin-bottom:8px}', + '.cl-panel .cbi-value-title{font-size:13px !important}', + '.cl-panel .cbi-value-field input,.cl-panel .cbi-value-field select,.cl-panel .cbi-value-field textarea{font-size:13px !important}', + '.cl-panel .cbi-section-descr,.cl-panel .cbi-value-helptext{font-size:12px !important}', + '.cl-panel .cbi-section{margin-bottom:12px}', + '.cl-wrap .cbi-section>h3,.cl-wrap .cbi-value-title,.cl-wrap .cbi-section-descr,.cl-wrap .cbi-value-helptext{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif !important}', + '.cl-wrap .cbi-input-text,.cl-wrap .cbi-input-select,.cl-wrap select,.cl-wrap input,.cl-wrap textarea,.cl-wrap .btn,.cl-wrap .cbi-button{font-size:13px !important;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif !important}', + '.cl-wrap .btn,.cl-wrap .cbi-button{padding:4px 10px;line-height:1.35}', + '.cl-sb-list td,.cl-sb-size{color:var(--cl-meta)}', + '@media(prefers-color-scheme:dark){.cl-wrap{--cl-card-border:rgba(255,255,255,.14);--cl-card-bg:rgba(255,255,255,.04);--cl-card-shadow:0 4px 12px rgba(0,0,0,.28);--cl-muted:rgba(220,228,244,.58);--cl-meta:rgba(220,228,244,.72)}}', + '.cl-theme-dark{--cl-card-border:rgba(255,255,255,.14);--cl-card-bg:rgba(255,255,255,.04);--cl-card-shadow:0 4px 12px rgba(0,0,0,.28);--cl-muted:rgba(220,228,244,.58);--cl-meta:rgba(220,228,244,.72)}', + '@media(max-width:680px){.cl-wrap{--cl-meta:#4b5870}.cl-sub-list.cl-sb-list td:first-child{white-space:normal;overflow:visible;text-overflow:clip}}', + '@media(max-width:680px){.cl-file-name-text,.cl-file-size,.cl-sub-traffic,.cl-sub-expire,.cl-sb-list td,.cl-sb-size,.cl-dns-auto-status,.cl-dns-auto-result{color:#4b5870!important}}', + '@media(max-width:680px){html body .cl-wrap .cl-file-item .cl-file-meta .cl-file-size{color:#4b5870!important}}', + '@media(max-width:680px){.cl-file-item{flex-direction:column;align-items:stretch;gap:8px}.cl-file-meta{flex-direction:column;align-items:stretch;gap:6px}.cl-file-name-text{white-space:normal;overflow-wrap:anywhere}.cl-file-actions{justify-content:flex-end}.cl-sub-expire,.cl-sub-traffic{white-space:normal}}', + '@media(max-width:680px){.cl-form-wrap{max-width:100%}}', + '@media(max-width:680px){.cl-dns-auto,.cl-dns-auto-result{max-width:100%;width:100%;box-sizing:border-box}.cl-dns-auto-result{grid-template-columns:1fr}}', + '@media(max-width:680px){.cl-sub-schedule{width:100%;padding:8px 0 4px 4px}.cl-sub-schedule-row{gap:8px;flex-wrap:wrap}.cl-sub-schedule-toggle{width:100%}.cl-sub-schedule-interval{gap:5px}.cl-sub-schedule-row .cbi-button-action{white-space:nowrap}}' +].join(''); + +var callListSubs = rpc.declare({ object: 'luci.clashoo', method: 'list_subscriptions', expect: {} }); +var callListDir = rpc.declare({ object: 'luci.clashoo', method: 'list_dir_files', params: ['type'], expect: {} }); +var callDownloadSubs = rpc.declare({ object: 'luci.clashoo', method: 'download_subs', expect: {} }); +var callDownloadSubsStatus = rpc.declare({ object: 'luci.clashoo', method: 'download_subs_status', expect: {} }); +var callUpdateSub = rpc.declare({ object: 'luci.clashoo', method: 'update_sub', params: ['name'], expect: {} }); +var callSubscriptionUpdateAll = rpc.declare({ object: 'luci.clashoo', method: 'subscription_update_all', expect: {} }); +var callSubscriptionUpdateStatus = rpc.declare({ object: 'luci.clashoo', method: 'subscription_update_status', expect: {} }); +var callSetSubscriptionUpdateSchedule = rpc.declare({ object: 'luci.clashoo', method: 'set_subscription_update_schedule', params: ['enabled', 'interval'], expect: {} }); +var callSetConfig = rpc.declare({ object: 'luci.clashoo', method: 'set_config', params: ['name'], expect: {} }); +var callDeleteCfg = rpc.declare({ object: 'luci.clashoo', method: 'delete_config', params: ['name', 'type'], expect: {} }); +var callUploadConfigChunk = rpc.declare({ object: 'luci.clashoo', method: 'upload_config_chunk', params: ['name', 'content', 'type', 'index', 'total'], expect: {} }); +var callReadOtherConfig = rpc.declare({ object: 'luci.clashoo', method: 'read_other_config', params: ['name', 'type'], expect: {} }); +var callListTemplates = rpc.declare({ object: 'luci.clashoo', method: 'list_templates', expect: {} }); +var callUploadTemplate= rpc.declare({ object: 'luci.clashoo', method: 'upload_template', params: ['name', 'content'], expect: {} }); +var callApplyRewrite = rpc.declare({ object: 'luci.clashoo', method: 'apply_rewrite', params: ['base_type','base_name','rewrite_type','rewrite_name','output_name','set_active'], expect: {} }); +var callFetchUrl = rpc.declare({ object: 'luci.clashoo', method: 'fetch_rewrite_url', params: ['url','name'], expect: {} }); +var callApplyTplUrl = rpc.declare({ object: 'luci.clashoo', method: 'apply_template_with_url', params: ['template_source','sub_url','output_name','set_active'], expect: {} }); +var callMigrateSbProfile = rpc.declare({ object: 'luci.clashoo', method: 'migrate_singbox_profile', params: ['name'], expect: {} }); +var callSmartFlushCache = rpc.declare({ object: 'luci.clashoo', method: 'smart_flush_cache', expect: {} }); +var callDetectPrimaryGroup = rpc.declare({ object: 'luci.clashoo', method: 'detect_primary_group', expect: {} }); + +function fastResolve(promise, timeoutMs, fallback) { + var t = new Promise(function (resolve) { + setTimeout(function () { resolve(fallback); }, timeoutMs); + }); + return Promise.race([L.resolveDefault(promise, fallback), t]); +} + +function uploadConfigContent(name, content, type) { + var chunkSize = 24576; + var total = Math.max(1, Math.ceil((content || '').length / chunkSize)); + var index = 0; + + function sendNext() { + var chunk = (content || '').slice(index * chunkSize, (index + 1) * chunkSize); + return L.resolveDefault(callUploadConfigChunk(name, chunk, type || '2', String(index), String(total)), {}).then(function (r) { + if (!r || !r.success) + throw new Error((r && (r.message || r.error)) || _("Upload failed")); + index++; + return index < total ? sendNext() : r; + }); + } + + return sendNext(); +} + +function loadUiState() { + return L.resolveDefault(uci.load('clashoo'), null).then(function () { + return { + core_type: uci.get('clashoo', 'config', 'core_type') || 'mihomo', + subscribe_url: uci.get('clashoo', 'config', 'subscribe_url') || '', + config_name: uci.get('clashoo', 'config', 'config_name') || '', + sub_ua: uci.get('clashoo', 'config', 'sub_ua') || '', + auto_subscription_update: uci.get('clashoo', 'config', 'auto_subscription_update') || '0', + subscription_update_interval: uci.get('clashoo', 'config', 'subscription_update_interval') || '72' + }; + }); +} + +function buildSubscriptionSchedule(uiData, initialStatus) { + var enabled = E('input', { + type: 'checkbox', + checked: uiData.auto_subscription_update === '1' ? '' : null + }); + var interval = E('input', { + type: 'number', + min: '1', + max: '8760', + value: uiData.subscription_update_interval || '72' + }); + var statusEl = E('div', { 'class': 'cl-sub-schedule-status' }); + var saveBtn, runBtn, timer; + + function syncScheduleState() { + interval.disabled = !enabled.checked; + } + + enabled.addEventListener('change', syncScheduleState); + + function renderStatus(st) { + st = st || {}; + if (st.running) { + statusEl.textContent = _("Updating all subscriptions…"); + return; + } + if (!st.last_run) { + statusEl.textContent = _("Only subscriptions with recorded source URLs are overwritten; no duplicate files are created."); + return; + } + var when = new Date(st.last_run * 1000).toLocaleString(); + statusEl.textContent = _("Last run: ") + when + _("; updated ") + (st.updated || 0) + + _(", unchanged ") + (st.unchanged || 0) + _(", failed ") + (st.failed || 0) + + _(", skipped ") + (st.skipped || 0) + '.'; + } + + function pollStatus() { + L.resolveDefault(callSubscriptionUpdateStatus(), {}).then(function (st) { + renderStatus(st); + if (!st.running) { + if (timer) clearInterval(timer); + timer = null; + runBtn.disabled = false; + runBtn.textContent = _("Update All Now"); + } + }); + } + + saveBtn = E('button', { + 'class': 'btn cbi-button cl-btn-sm', + click: function () { + var hours = parseInt(interval.value, 10); + if (!hours || hours < 1 || hours > 8760) { + ui.addNotification(null, E('p', _("Update interval must be 1 to 8760 hours"))); + return; + } + saveBtn.disabled = true; + L.resolveDefault(callSetSubscriptionUpdateSchedule(enabled.checked ? '1' : '0', String(hours)), {}).then(function (r) { + saveBtn.disabled = false; + ui.addNotification(null, E('p', r && r.success ? _("Scheduled update settings saved") : (_("Save failed: ") + ((r && r.message) || '')))); + }); + } + }, _("Save")); + + runBtn = E('button', { + 'class': 'btn cbi-button-action cl-btn-sm', + click: function () { + runBtn.disabled = true; + runBtn.textContent = _("Updating…"); + L.resolveDefault(callSubscriptionUpdateAll(), {}).then(function (r) { + if (!r || (!r.success && !r.running)) { + runBtn.disabled = false; + runBtn.textContent = _("Update All Now"); + ui.addNotification(null, E('p', _("Start failed: ") + ((r && r.message) || ''))); + return; + } + renderStatus({ running: true }); + timer = setInterval(pollStatus, 2000); + setTimeout(pollStatus, 500); + }); + } + }, _("Update All Now")); + + syncScheduleState(); + renderStatus(initialStatus); + return E('div', { 'class': 'cl-sub-schedule' }, [ + E('div', { 'class': 'cl-sub-schedule-row' }, [ + E('label', { 'class': 'cl-sub-schedule-toggle' }, [enabled, E('span', {}, _("Scheduled Subscription Update"))]), + E('div', { 'class': 'cl-sub-schedule-interval' }, [ + E('span', {}, _("Every")), + interval, + E('span', {}, _("hours")) + ]), + saveBtn, + runBtn + ]), + statusEl + ]); +} + +var SUB_UA_PRESETS = ['', 'clash', 'clash.meta', 'mihomo']; + +// 构造 UA 选择器:select 预设 + custom 切到 input +// 返回 { wrap, getValue() } +function buildUaPicker(initialValue) { + var presets = SUB_UA_PRESETS; + var isPreset = presets.indexOf(initialValue) !== -1; + var customVal = (!initialValue || isPreset) ? '' : initialValue; + var pickerVal = isPreset ? initialValue : (initialValue ? '__custom__' : ''); + + var customInput = E('input', { + 'class': 'cl-sub-url', + type: 'text', + placeholder: _("Custom User-Agent"), + value: customVal, + style: 'margin-top:0;' + (pickerVal === '__custom__' ? '' : 'display:none') + }); + + var selectEl = E('select', { + 'class': 'cbi-input-select cl-sub-url', + style: 'margin-top:0', + change: function (ev) { + customInput.style.display = (ev.target.value === '__custom__') ? '' : 'none'; + if (ev.target.value === '__custom__') customInput.focus(); + } + }, [ + E('option', { value: '', selected: pickerVal === '' ? '' : null }, _("Default")), + E('option', { value: 'clash', selected: pickerVal === 'clash' ? '' : null }, 'clash'), + E('option', { value: 'clash.meta', selected: pickerVal === 'clash.meta' ? '' : null }, 'clash.meta'), + E('option', { value: 'mihomo', selected: pickerVal === 'mihomo' ? '' : null }, 'mihomo'), + E('option', { value: '__custom__', selected: pickerVal === '__custom__' ? '' : null }, _("Customize...")) + ]); + + return { + wrap: E('div', { 'class': 'cl-ua-picker', style: 'display:flex;flex-direction:column;gap:6px;margin-top:0' }, [ + E('label', { style: 'font-size:12px;color:var(--text-secondary, #888)' }, _("User-Agent (UA)")), + selectEl, customInput, + E('div', { style: 'font-size:12px;color:var(--text-secondary, #888)' }, _("Keep the default if unsure.")) + ]), + getValue: function () { + return selectEl.value === '__custom__' ? customInput.value.trim() : selectEl.value; + } + }; +} + +function readSavedTab(key, fallback, allowed) { + var raw = ''; + if (window.location.hash) + raw = window.location.hash.replace(/^#/, ''); + if (!raw) { + try { raw = window.localStorage.getItem(key) || ''; } catch (e) {} + } + return allowed.indexOf(raw) >= 0 ? raw : fallback; +} + +function rememberTab(key, id) { + try { window.localStorage.setItem(key, id); } catch (e) {} + if (window.history && window.history.replaceState) + window.history.replaceState(null, '', '#' + id); + else + window.location.hash = id; +} + +function fmtBytes(b) { + b = parseInt(b) || 0; + if (b >= 1e12) return (b / 1e12).toFixed(2) + ' TB'; + if (b >= 1e9) return (b / 1e9).toFixed(2) + ' GB'; + if (b >= 1e6) return (b / 1e6).toFixed(1) + ' MB'; + return Math.round(b / 1e3) + ' KB'; +} +function fmtExpireDate(ts) { + if (!ts || ts === 0) return ''; + var d = new Date(ts * 1000); + var pad2 = function (n) { return n < 10 ? '0' + n : '' + n; }; + return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); +} +/* 渲染流量条;返回 DOM 节点或 null */ +function renderTrafficBar(used, total) { + total = parseInt(total) || 0; + if (total <= 0) return null; + used = parseInt(used) || 0; + var pct = Math.min(100, Math.round(used / total * 100)); + var fillCls = 'cl-sub-traffic-fill' + (pct >= 90 ? ' cl-traffic-danger' : pct >= 75 ? ' cl-traffic-warn' : ''); + return E('div', { 'class': 'cl-sub-traffic' }, [ + fmtBytes(used) + ' / ' + fmtBytes(total) + ' (' + pct + '%)', + E('div', { 'class': 'cl-sub-traffic-bar' }, [ + E('div', { 'class': fillCls, 'style': 'width:' + pct + '%' }) + ]) + ]); +} +/* 渲染到期文本;返回 DOM 节点或 null */ +function renderExpire(ts) { + if (!ts) return null; + var daysLeft = Math.ceil((ts * 1000 - Date.now()) / 86400000); + var cls = 'cl-sub-expire' + (daysLeft <= 7 ? ' cl-expire-soon' : ''); + return E('div', { 'class': cls }, + _("Expires: ") + fmtExpireDate(ts) + (daysLeft > 0 ? ' (' + daysLeft + _(" days)") : _(" (expired)"))); +} + +function decorateControlWraps(root) { + if (!root || !root.querySelectorAll) + return; + var fields = root.querySelectorAll('.cbi-value-field'); + for (var i = 0; i < fields.length; i++) { + if (fields[i] && fields[i].classList) + fields[i].classList.add('cl-control-wrap'); + } +} + +function clearClashooDirty() { + var applyPromise; + try { + applyPromise = (L.uci && typeof L.uci.callApply === 'function') + ? Promise.resolve(L.uci.callApply(0, false)).catch(function () {}) + : Promise.resolve(); + } catch (e) { applyPromise = Promise.resolve(); } + return applyPromise.then(function () { + try { + if (L.ui && L.ui.changes && L.ui.changes.changes) { + delete L.ui.changes.changes.clashoo; + var n = Object.keys(L.ui.changes.changes).length; + if (typeof L.ui.changes.renderChangeIndicator === 'function') + L.ui.changes.renderChangeIndicator(n); + else if (typeof L.ui.changes.setIndicator === 'function') + L.ui.changes.setIndicator(n); + } + } catch (e) {} + }); +} + +function saveCommitApplyMaybeReload(m, runningMsg, stoppedMsg) { + return clashoo.status() + .then(function (st) { return !!(st && st.running); }) + .catch(function () { return false; }) + .then(function (running) { + /* silent=true so the caller's catch reports validation errors once (no double popup) */ + return m.save(null, true) + .then(function () { return clashoo.commitConfig(); }) + .then(function () { + return running ? clashoo.reload() : { success: true, skipped: true }; + }) + .then(function () { return clearClashooDirty(); }) + .then(function () { + ui.addNotification(null, E('p', running ? runningMsg : stoppedMsg)); + window.setTimeout(function () { location.reload(); }, 300); + }); + }); +} + +function dnsAutoSummaryNode(res) { + if (!res || !res.success) + return E('div', { 'class': 'cl-dns-auto-status' }, _("No auto setup result yet")); + var elapsed = Math.max(0, parseInt(res.elapsed_ms || 0, 10)); + var failed = parseInt(res.failed_count || 0, 10); + return E('div', { 'class': 'cl-dns-auto-result' }, [ + E('span', [E('b', _("Domestic")), res.nameserver || '-']), + E('span', [E('b', _("Proxy")), res.proxy_nameserver || '-']), + E('span', [E('b', _("Fallback")), res.fallback || '-']), + E('span', [E('b', _("Bootstrap")), res.bootstrap || res.direct_nameserver || '-']), + E('span', [E('b', _("Time")), elapsed ? (elapsed / 1000).toFixed(1) + _(" seconds") : '-']), + E('span', [E('b', _("Failed")), failed + _(" candidates")]) + ]); +} + +function dnsAutoResultMessage(res) { + if (!res || !res.success) + return (res && res.message) || _("DNS auto setup failed"); + return res.restarted + ? _("DNS auto setup applied, service is restarting") + : _("DNS auto setup saved, service is not running"); +} + +function storeDnsAutoResult(res) { + try { + res._saved_at = Date.now(); + window.localStorage.setItem('clashoo.dns_auto.last', JSON.stringify(res)); + } catch (e) {} +} + +function readDnsAutoResult() { + try { + var raw = window.localStorage.getItem('clashoo.dns_auto.last') || ''; + return raw ? JSON.parse(raw) : null; + } catch (e) { + return null; + } +} + +function clearDnsAutoResult() { + try { window.localStorage.removeItem('clashoo.dns_auto.last'); } catch (e) {} +} + +function setDnsAutoStatus(statusEl, nodeOrText) { + statusEl.textContent = ''; + if (nodeOrText && nodeOrText.nodeType) + statusEl.appendChild(nodeOrText); + else + statusEl.textContent = nodeOrText || ''; +} + +function findCbiSection(root, title) { + var heads = root.querySelectorAll('.cbi-section > h3'); + for (var i = 0; i < heads.length; i++) { + if ((heads[i].textContent || '').trim() === title) + return heads[i].parentNode; + } + return null; +} + +function makeSectionCollapsible(root, title, open) { + var section = findCbiSection(root, title); + if (!section || section.classList.contains('cl-collapsible')) + return; + section.classList.add('cl-collapsible'); + if (!open) section.classList.add('cl-closed'); + var btn = E('button', { + 'class': 'btn cbi-button cl-section-toggle', + click: function (ev) { + ev.preventDefault(); + section.classList.toggle('cl-closed'); + btn.textContent = section.classList.contains('cl-closed') ? _("Expand") : _("Collapse"); + } + }, open ? _("Collapse") : _("Expand")); + var h3 = section.querySelector('h3'); + if (h3) { + h3.classList.add('clashoo-section-header'); + h3.appendChild(btn); + } +} + +/* ── CodeMirror 懒加载(资源打包在 luci-static/resources/cm/)── */ +var _cmLoad = null; +function loadCodeMirror() { + if (_cmLoad) return _cmLoad; + _cmLoad = new Promise(function (resolve, reject) { + var base = L.resource('cm'), ver = '?v=20260520'; + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = base + '/codemirror.css' + ver; + document.head.appendChild(link); + var inject = function (src) { + return new Promise(function (res, rej) { + var s = document.createElement('script'); + s.src = src; s.async = false; + s.onload = res; + s.onerror = function () { rej(new Error('load failed: ' + src)); }; + document.head.appendChild(s); + }); + }; + /* 核心先就位,mode/addon 注册在全局 CodeMirror 上 */ + inject(base + '/codemirror.js' + ver) + .then(function () { + return Promise.all([ + inject(base + '/mode/yaml.js' + ver), + inject(base + '/mode/javascript.js' + ver), + inject(base + '/addon/matchbrackets.js' + ver) + ]); + }) + .then(function () { + if (window.CodeMirror) resolve(); + else reject(new Error('CodeMirror not available')); + }) + .catch(reject); + }); + return _cmLoad; +} + +/* CodeMirror for files ≤512KB; fallback to textarea for oversized or failed load */ +function createConfigEditor(mode) { + var LARGE = 512 * 1024; + var ta = E('textarea', { + 'class': 'cl-json-editor', + placeholder: _("Configuration content appears here after selecting a file…") + }); + var host = E('div', { 'class': 'cl-cm-host', style: 'display:none' }); + var wrap = E('div', { 'class': 'cl-cm-wrap' }, [host, ta]); + var cm = null, usingCM = false; + + function showTextarea(content) { + usingCM = false; + ta.value = content; + ta.style.display = ''; + host.style.display = 'none'; + } + function showCM(content) { + usingCM = true; + cm.setValue(content); + ta.style.display = 'none'; + host.style.display = ''; + setTimeout(function () { cm.refresh(); }, 0); + } + + return { + el: wrap, + textarea: ta, + setValue: function (content) { + content = (content == null) ? '' : String(content); + if (content.length > LARGE) { showTextarea(content); return; } + if (cm) { showCM(content); return; } + loadCodeMirror().then(function () { + if (!cm && window.CodeMirror) { + cm = window.CodeMirror(host, { + value: '', mode: mode, + lineNumbers: true, matchBrackets: true, + lineWrapping: true, tabSize: 2, indentUnit: 2, + viewportMargin: 60 + }); + if (getThemeClass().indexOf('dark') >= 0) + host.classList.add('cl-cm-dark'); + } + if (cm) showCM(content); else showTextarea(content); + }).catch(function () { showTextarea(content); }); + }, + getValue: function () { + return (usingCM && cm) ? cm.getValue() : ta.value; + } + }; +} + +// 全局锁:内核操作互斥(切换配置 / 更新订阅 / 删除) +function clKernelBusy() { return !!document.body.getAttribute('data-cl-kernel-busy'); } +function clKernelLock() { + document.body.setAttribute('data-cl-kernel-busy', '1'); + document.querySelectorAll('.cl-btn-switch, .cl-btn-update-sub, .cl-btn-delete').forEach(function (b) { b.disabled = true; }); +} +function clKernelUnlock() { + document.body.removeAttribute('data-cl-kernel-busy'); + document.querySelectorAll('.cl-btn-switch, .cl-btn-update-sub, .cl-btn-delete').forEach(function (b) { b.disabled = false; }); +} + +return view.extend({ + _tab: null, + _sbTab: null, + + load: function () { + return Promise.all([ + fastResolve(callListSubs(), 1200, { subs: [], url: '' }), + fastResolve(callListDir('1'), 1200, { files: [] }), + fastResolve(callListDir('2'), 1200, { files: [] }), + fastResolve(callListDir('3'), 1200, { files: [] }), + fastResolve(callListTemplates(), 1200, { files: [] }), + fastResolve(loadUiState(), 1200, { core_type: 'mihomo', subscribe_url: '', config_name: '', sub_ua: '' }), + fastResolve(clashoo.listSingboxProfiles(), 1200, { profiles: [], active: '' }), + fastResolve(callSubscriptionUpdateStatus(), 1200, {}) + ]); + }, + + render: function (data) { + var self = this; + var subsData = data[0] || {}; + var subFiles = (data[1] && data[1].files) || []; + var upFiles = (data[2] && data[2].files) || []; + var customFiles= (data[3] && data[3].files) || []; + var tplFiles = (data[4] && data[4].files) || []; + var uiData = data[5] || { core_type: 'mihomo', subscribe_url: '', config_name: '', sub_ua: '' }; + var sbData = data[6] || { profiles: [], active: '' }; + var subscriptionUpdateStatus = data[7] || {}; + var coreType = uiData.core_type || 'mihomo'; + + if (!document.getElementById('cl-css')) { + var s = document.createElement('style'); + s.id = 'cl-css'; s.textContent = CSS; + document.head.appendChild(s); + } + if (!document.getElementById('cl-css-ext')) { + var link = document.createElement('link'); + link.id = 'cl-css-ext'; + link.rel = 'stylesheet'; + link.href = L.resource('view/clashoo/clashoo.css') + '?v=20260806a1'; + document.head.appendChild(link); + } else { + document.getElementById('cl-css-ext').href = L.resource('view/clashoo/clashoo.css') + '?v=20260806a1'; + } + + if (coreType === 'singbox') return this._renderSingbox(sbData, uiData, subscriptionUpdateStatus); + + var tabs = [ + { id: 'subs', label: _("Subscription") }, + { id: 'proxy', label: _("Proxy") }, + { id: 'dns', label: 'DNS' } + ]; + var allowedTabs = tabs.map(function (t) { return t.id; }); + this._tab = readSavedTab('clashoo.config.tab', this._tab || 'subs', allowedTabs); + rememberTab('clashoo.config.tab', this._tab); + var tabEls = {}; + var panelEls = {}; + + var subPanel = E('div', { 'class': 'cl-panel' + (this._tab === 'subs' ? ' active' : ''), id: 'cl-panel-subs' }, + this._buildSubsPanel(subsData, subFiles, upFiles, customFiles, tplFiles, uiData, subscriptionUpdateStatus) + ); + panelEls['subs'] = subPanel; + + var proxyPanel = E('div', { 'class': 'cl-panel' + (this._tab === 'proxy' ? ' active' : ''), id: 'cl-panel-proxy' }); + panelEls['proxy'] = proxyPanel; + + var dnsPanel = E('div', { 'class': 'cl-panel' + (this._tab === 'dns' ? ' active' : ''), id: 'cl-panel-dns' }); + panelEls['dns'] = dnsPanel; + + var built = { subs: true, proxy: false, dns: false }; + var ensureBuilt = function (id) { + if (built[id]) return; + if (id === 'proxy') self._buildProxyForm(proxyPanel); + else if (id === 'dns') self._buildDnsForm(dnsPanel); + built[id] = true; + }; + if (this._tab !== 'subs') ensureBuilt(this._tab); + + var tabBar = E('div', { 'class': 'cl-tabs' }, + tabs.map(function (t) { + var el = E('div', { + 'class': 'cl-tab' + (self._tab === t.id ? ' active' : ''), + click: function () { + ensureBuilt(t.id); + Object.keys(tabEls).forEach(function (k) { + tabEls[k].className = 'cl-tab' + (k === t.id ? ' active' : ''); + panelEls[k].className = 'cl-panel' + (k === t.id ? ' active' : ''); + }); + self._tab = t.id; + rememberTab('clashoo.config.tab', t.id); + } + }, t.label); + tabEls[t.id] = el; + return el; + }) + ); + + return E('div', { 'class': 'cl-wrap clashoo-container cl-config-page cl-form-page ' + getThemeClass() }, [tabBar, subPanel, proxyPanel, dnsPanel]); + }, + + _buildSubsPanel: function (subsData, subFiles, upFiles, customFiles, tplFiles, uiData, subscriptionUpdateStatus) { + var self = this; + var sanitizeText = function (v) { return (v == null || v === 'null') ? '' : String(v); }; + var subUrl = sanitizeText(uiData && uiData.subscribe_url); + var savedName = sanitizeText(uiData && uiData.config_name); + var savedUa = sanitizeText(uiData && uiData.sub_ua); + var subs = subsData.subs || []; + var safeText = function (v) { return (v == null || v === 'null') ? '' : String(v); }; + + var urlInput = E('input', { + 'class': 'cl-sub-url', + type: 'text', + placeholder: _("Subscription URLs (one per line)"), + value: subUrl + }); + + var nameInput = E('input', { + 'class': 'cl-sub-url', + type: 'text', + placeholder: _("File name (optional, blank to auto-generate)"), + value: savedName, + style: 'margin-top:0' + }); + + var uaPicker = buildUaPicker(savedUa); + + var dlStatusEl = E('div', { 'class': 'cl-update-status', style: 'margin-top:6px;font-size:12px;min-height:18px;line-height:1.4' }); + var setDlStatus = function (text, tone) { + dlStatusEl.textContent = text || ''; + dlStatusEl.style.color = tone === 'success' ? 'var(--success-color, #2e7d32)' + : tone === 'error' ? 'var(--error-color, #d32f2f)' + : tone === 'progress'? 'var(--tip-color, #1976d2)' + : ''; + }; + var stopDlPoll = function () { + if (self._subDlTimer) { clearInterval(self._subDlTimer); self._subDlTimer = null; } + }; + + var dlBtn = E('button', { + 'class': 'btn cbi-button-action cl-btn-sm', + click: function () { + if (!urlInput.value.trim()) { + setDlStatus(_("✗ Please enter a subscription URL first"), 'error'); + setTimeout(function () { setDlStatus(''); }, 4000); + return; + } + stopDlPoll(); + dlBtn.disabled = true; + dlBtn.textContent = _("Downloading…"); + setDlStatus(_("⏳ Submitting download task..."), 'progress'); + + var pollCount = 0; + var pollDl = function () { + pollCount++; + callDownloadSubsStatus().then(function (st) { + st = st || {}; + var raw = st.last_line || ''; + var line = clashoo.localizeLogLine(raw); + if (st.running) { + setDlStatus('⏳ ' + (line || _("Downloading subscription...")), 'progress'); + return; + } + /* sh has not flushed yet, wait a few polls */ + if (!raw && pollCount < 4) return; + stopDlPoll(); + /* 收尾行只有三种:「订阅下载完成:成功 N 个,失败 M 个」=成功(含"失败 0 个" + 字样,不能按"失败"判负);「订阅下载失败:全部链接失败」「未找到订阅链接」=失败 */ + var ok = /下载完成|completed/i.test(raw); + /* fail_detail carries the real cause(rc=/HTTP/校验失败): + all fail: show real cause; partial: summary + cause */ + var detail = st.fail_detail ? clashoo.localizeLogLine(st.fail_detail) : ''; + var shown = ok ? (line + (detail ? ' · ' + detail : '')) + : (detail || line || _("Subscription download failed")); + dlBtn.disabled = false; + dlBtn.textContent = _("Download Subscription"); + setDlStatus((ok ? '✓ ' : '✗ ') + shown, ok ? 'success' : 'error'); + setTimeout(function () { location.reload(); }, 1200); + }).catch(function () {}); + }; + + L.resolveDefault(uci.load('clashoo'), null) + .then(function () { + uci.set('clashoo', 'config', 'subscribe_url', urlInput.value); + uci.set('clashoo', 'config', 'config_name', nameInput.value.trim()); + uci.set('clashoo', 'config', 'sub_ua', uaPicker.getValue()); + return uci.save(); + }) + .then(function () { return clashoo.commitConfig(); }) + .then(function () { return clearClashooDirty(); }) + .then(function () { return L.resolveDefault(callDownloadSubs(), {}); }) + .then(function (r) { + if (r && r.running) setDlStatus(_("⏳ A download task is already running..."), 'progress'); + self._subDlTimer = setInterval(pollDl, 2000); + setTimeout(pollDl, 800); + }) + .catch(function (e) { + stopDlPoll(); + dlBtn.disabled = false; + dlBtn.textContent = _("Download Subscription"); + setDlStatus(_("✗ Start failed: ") + (e && e.message || e), 'error'); + }); + } + }, _("Download Subscription")); + + var subCards = subs.map(function (sub) { + var nameNodes = []; + if (sub.active) nameNodes.push(E('span', { 'class': 'cl-active-badge' }, _("Active"))); + nameNodes.push(E('span', { 'class': 'cl-file-name-text' }, safeText(sub.name))); + + var used = (parseInt(sub.sub_upload) || 0) + (parseInt(sub.sub_download) || 0); + var trafficEl = renderTrafficBar(used, sub.sub_total); + var expireEl = renderExpire(sub.sub_expire); + + return E('div', { 'class': 'cl-file-item' + (sub.active ? ' is-active' : '') }, [ + E('div', { 'class': 'cl-file-meta' }, [ + E('div', { 'class': 'cl-file-name' }, nameNodes), + E('div', { 'class': 'cl-file-size' }, safeText(sub.size)), + trafficEl, expireEl + ]), + E('div', { 'class': 'cl-file-actions' }, [ + E('button', { + 'class': 'btn cbi-button cl-btn-sm cl-btn-update-sub', + click: function (ev) { + var btn = ev.target; + if (btn.disabled || clKernelBusy()) return; + clKernelLock(); + btn.disabled = true; + btn.textContent = _("Updating…"); + L.resolveDefault(callUpdateSub(sub.name), {}).then(function (r) { + ui.addNotification(null, E('p', r.success ? sub.name + _(" update succeeded") : _("Update failed"))); + location.reload(); + }).catch(function () { + btn.disabled = false; + btn.textContent = _("Update"); + clKernelUnlock(); + }); + } + }, _("Update")), + E('button', { + 'class': 'btn cbi-button cl-btn-sm cl-btn-switch', + click: function (ev) { + var btn = ev.target; + if (btn.disabled || clKernelBusy()) return; + clKernelLock(); + btn.disabled = true; + btn.textContent = _("Loading…"); + L.resolveDefault(callSetConfig(sub.name), {}).then(function () { + location.reload(); + }).catch(function () { + btn.disabled = false; + btn.textContent = _("Switch Profile"); + clKernelUnlock(); + }); + } + }, _("Switch Profile")), + E('button', { + 'class': 'btn cbi-button cl-btn-sm cl-btn-delete', + click: function () { + if (!confirm(_("Delete ") + sub.name + '?')) return; + L.resolveDefault(callDeleteCfg(sub.name, '1'), {}).then(function () { location.reload(); }); + } + }, _("Delete")) + ]) + ]); + }); + + var uploadInput = E('input', { type: 'file', accept: '.yaml,.yml', style: 'display:none', id: 'cl-upload-input' }); + uploadInput.addEventListener('change', function (ev) { + var file = ev.target.files[0]; + if (!file) return; + var reader = new FileReader(); + reader.onload = function (e) { + uploadConfigContent(file.name, e.target.result, '2').then(function (r) { + ui.addNotification(null, E('p', _("Upload succeeded: ") + r.name)); + location.reload(); + }).catch(function (err) { + ui.addNotification(null, E('p', _("Upload failed: ") + (err && err.message ? err.message : err))); + }); + }; + reader.readAsText(file); + }); + + var mkSel = function (files, placeholder) { + return E('select', { 'class': 'cbi-input-select' }, + [E('option', { value: '' }, placeholder)].concat( + files.map(function (f) { return E('option', { value: f.name }, f.name); }) + ) + ); + }; + + /* ── 模板复写(注入订阅 URL 模式)── */ + var tplSel = mkSel(tplFiles, _("Select local template file")); + tplSel.classList.add('cl-template-select'); + tplSel.setAttribute('title', tplSel.value || ''); + tplSel.addEventListener('change', function () { tplSel.setAttribute('title', tplSel.value || ''); }); + + var tplUploadInput = E('input', { type: 'file', accept: '.yaml,.yml', style: 'display:none', id: 'cl-template-upload-input' }); + tplUploadInput.addEventListener('change', function (ev) { + var file = ev.target.files[0]; + if (!file) return; + var reader = new FileReader(); + reader.onload = function (e) { + L.resolveDefault(callUploadTemplate(file.name, e.target.result), {}).then(function (r) { + if (r && r.success) { + ui.addNotification(null, E('p', _("Template uploaded: ") + (r.name || file.name))); + location.reload(); + return; + } + ui.addNotification(null, E('p', _("Template upload failed: ") + ((r && (r.message || r.error)) || _("Unknown error")))); + }); + }; + reader.readAsText(file); + }); + + var tplUrlIn = E('input', { type: 'text', 'class': 'cl-sub-url', placeholder: _("Enter remote template URL, e.g. https://raw.githubusercontent.com/…/Clash.yaml") }); + var subUrlIn = E('input', { type: 'text', 'class': 'cl-sub-url', placeholder: _("Enter subscription URL (injected into template proxy-providers)") }); + var outNameIn = E('input', { type: 'text', 'class': 'cl-sub-url', placeholder: _("Output file name (without extension, blank to auto-fill)") }); + var rwMode = 'local'; + + function rwAutoFill() { + if (outNameIn.value) return; + var tpl = rwMode === 'local' ? tplSel.value.replace(/\.(yaml|yml)$/, '') : 'remote-tpl'; + if (tpl) outNameIn.value = tpl + '-rewrite'; + } + tplSel.addEventListener('change', rwAutoFill); + + var localPanel = E('div', { 'class': 'cl-rw-pane cl-rw-pane-local' }, [ + tplUploadInput, + E('div', { 'class': 'cl-template-row' }, [ + E('div', { 'class': 'cl-template-select-wrap' }, [tplSel]), + E('button', { + 'class': 'btn cbi-button cl-btn-sm cl-btn-template-upload', + click: function () { document.getElementById('cl-template-upload-input').click(); } + }, _("Upload YAML Template")) + ]) + ]); + var remotePanel = E('div', { 'class': 'cl-rw-pane cl-rw-pane-remote', style: 'display:none' }, [tplUrlIn]); + + var tabLocal = E('button', { 'class': 'btn cbi-button cl-btn-sm cl-mode-tab-active', + click: function () { + rwMode = 'local'; + localPanel.style.display = ''; remotePanel.style.display = 'none'; + tabLocal.classList.add('cl-mode-tab-active'); + tabRemote.classList.remove('cl-mode-tab-active'); + outNameIn.value = ''; + rwAutoFill(); + } + }, _("Local Template")); + var tabRemote = E('button', { 'class': 'btn cbi-button cl-btn-sm', + click: function () { + rwMode = 'remote'; + localPanel.style.display = 'none'; remotePanel.style.display = ''; + tabRemote.classList.add('cl-mode-tab-active'); + tabLocal.classList.remove('cl-mode-tab-active'); + outNameIn.value = ''; + } + }, _("Remote Template")); + + var rwApply = function (setActive) { + var tplSrc = rwMode === 'local' ? tplSel.value : tplUrlIn.value.trim(); + var subUrl = subUrlIn.value.trim(); + var out = outNameIn.value.trim(); + if (!tplSrc) { ui.addNotification(null, E('p', rwMode === 'local' ? _("Please select a local template file") : _("Please enter a remote template URL"))); return; } + if (!subUrl) { ui.addNotification(null, E('p', _("Please enter a subscription URL"))); return; } + if (!out) { ui.addNotification(null, E('p', _("Please enter an output file name"))); return; } + L.resolveDefault(callApplyTplUrl(tplSrc, subUrl, out, setActive ? '1' : '0'), {}).then(function (r) { + ui.addNotification(null, E('p', r && r.success ? (r.message || _("Generated: ") + r.output_name) : (_("Generate failed: ") + (r && r.message || _("Unknown error"))))); + if (r && r.success) location.reload(); + }); + }; + + /* ── 其他配置文件(上传 + 自定义/复写输出)── */ + var otherEditorTitle = E('span', { 'class': 'cl-editor-hdr' }, _("Select a configuration above to edit it here")); + var otherEd = createConfigEditor('yaml'); + var otherSaveBtn = E('button', { + 'class': 'btn cbi-button-action cl-btn-sm', + disabled: '', + click: function () { + var meta = otherEd.textarea.dataset; + if (!meta.name) return; + uploadConfigContent(meta.name, otherEd.getValue(), meta.type).then(function (r) { + if (r && r.success) ui.addNotification(null, E('p', meta.name + _(" saved"))); + else ui.addNotification(null, E('p', _("Save failed: ") + ((r && (r.message || r.error)) || ''))); + }).catch(function (err) { + ui.addNotification(null, E('p', _("Save failed: ") + (err && err.message ? err.message : err))); + }); + } + }, _("Save")); + + var otherEditorBox = E('div', { 'class': 'cl-section cl-card cl-sb-editor' }, [ + otherEditorTitle, + otherEd.el, + E('div', { 'class': 'cl-actions cl-sb-row-actions cl-sb-editor-actions' }, [ + otherSaveBtn, + E('span', { 'class': 'cl-hint' }, _("Click Save after editing; switching configuration automatically restarts the service")) + ]) + ]); + + function loadOtherEditor(name, type) { + otherEditorTitle.textContent = _("Editing: ") + name; + otherSaveBtn.removeAttribute('disabled'); + otherEd.textarea.dataset.name = name; + otherEd.textarea.dataset.type = type; + otherEd.setValue(_("Loading…")); + L.resolveDefault(callReadOtherConfig(name, type), {}).then(function (r) { + otherEd.setValue(r.content || ''); + }); + } + + var makeOtherCards = function (files, type) { + return files.map(function (f) { + var nameNodes = []; + if (f.active) nameNodes.push(E('span', { 'class': 'cl-active-badge' }, _("Active"))); + nameNodes.push(E('span', { 'class': 'cl-file-name-text' }, safeText(f.name))); + + return E('div', { 'class': 'cl-file-item' + (f.active ? ' is-active' : '') }, [ + E('div', { 'class': 'cl-file-meta' }, [ + E('div', { 'class': 'cl-file-name' }, nameNodes), + E('div', { 'class': 'cl-file-size' }, safeText(f.size)) + ]), + E('div', { 'class': 'cl-file-actions' }, [ + E('button', { + 'class': 'btn cbi-button cl-btn-sm cl-btn-edit', + click: function (ev) { + var btn = ev.target; + if (btn.disabled) return; + btn.disabled = true; + var orig = btn.textContent; + btn.textContent = _("Loading…"); + Promise.resolve(loadOtherEditor(f.name, type)).then(function () { + btn.disabled = false; + btn.textContent = orig; + }).catch(function () { + btn.disabled = false; + btn.textContent = orig; + }); + } + }, _("Edit")), + E('button', { + 'class': 'btn cbi-button cl-btn-sm cl-btn-switch', + click: function (ev) { + var btn = ev.target; + if (btn.disabled || clKernelBusy()) return; + clKernelLock(); + btn.disabled = true; + btn.textContent = _("Loading…"); + L.resolveDefault(callSetConfig(f.name), {}).then(function () { + location.reload(); + }).catch(function () { + btn.disabled = false; + btn.textContent = _("Switch Profile"); + clKernelUnlock(); + }); + } + }, _("Switch Profile")), + E('button', { + 'class': 'btn cbi-button cl-btn-sm cl-btn-delete', + click: function (ev) { + var btn = ev.target; + if (btn.disabled || clKernelBusy()) return; + if (!confirm(_("Delete ") + f.name + '?')) return; + clKernelLock(); + btn.disabled = true; + btn.textContent = _("Loading…"); + L.resolveDefault(callDeleteCfg(f.name, type), {}).then(function () { + location.reload(); + }).catch(function () { + btn.disabled = false; + btn.textContent = _("Delete"); + clKernelUnlock(); + }); + } + }, _("Delete")) + ]) + ]); + }); + }; + + var otherFiles = (upFiles || []).map(function(f){ return {f:f, t:'2'}; }) + .concat((customFiles || []).map(function(f){ return {f:f, t:'3'}; })); + + var otherCards = otherFiles.map(function(o) { + return makeOtherCards([o.f], o.t)[0]; + }); + + var sections = [ + E('div', { 'class': 'cl-section cl-card' }, [ + E('h4', {}, _("Subscription URL")), + E('div', { 'class': 'cl-form-wrap cl-fixed-600' }, [urlInput, nameInput, uaPicker.wrap, dlBtn, dlStatusEl]) + ]), + E('div', { 'class': 'cl-section cl-card' }, [ + E('h4', {}, _("Downloaded Subscriptions")), + buildSubscriptionSchedule(uiData, subscriptionUpdateStatus), + subs.length ? E('div', { 'class': 'cl-fixed-600' }, [ + E('div', { 'class': 'cl-file-list' }, subCards) + ]) + : E('p', { style: 'opacity:.5;font-size:13px' }, _("No subscriptions")) + ]), + E('div', { 'class': 'cl-section cl-card' }, [ + E('h4', {}, _("Upload Configuration File")), + uploadInput, + E('button', { + 'class': 'btn cbi-button cl-btn-sm cl-btn-upload-config', + click: function () { document.getElementById('cl-upload-input').click(); } + }, _("Select YAML file to upload")) + ]) + ]; + + if (otherFiles.length) { + sections.push(E('div', { 'class': 'cl-section cl-card' }, [ + E('h4', {}, _("Other Configuration Files (upload / overwrite output)")), + E('div', { 'class': 'cl-fixed-600' }, [ + E('div', { 'class': 'cl-file-list' }, otherCards) + ]) + ])); + sections.push(otherEditorBox); + } + + sections.push( + E('div', { 'class': 'cl-section cl-card' }, [ + E('h4', {}, _("Overwrite Settings")), + E('div', { 'class': 'cl-form-wrap cl-rewrite-wrap cl-fixed-600' }, [ + E('div', { 'class': 'cl-rewrite-group cl-rewrite-group-template' }, [ + E('div', { 'class': 'cl-rewrite-group-title' }, _("Template Selection")), + E('div', { 'class': 'cl-mode-tabs' }, [tabLocal, tabRemote]), + localPanel, + remotePanel + ]), + E('div', { 'class': 'cl-rw-divider' }), + E('div', { 'class': 'cl-rewrite-group cl-rewrite-group-input' }, [ + E('div', { 'class': 'cl-rewrite-group-title' }, _("Information Input")), + subUrlIn, + outNameIn, + E('div', { 'class': 'cl-actions cl-rewrite-actions' }, [ + E('button', { 'class': 'btn cbi-button cl-btn-sm', click: function(){ rwApply(false); } }, _("Generate Configuration")), + E('button', { 'class': 'btn cbi-button-action cl-btn-sm cl-btn-generate-switch', click: function(){ rwApply(true); } }, _("Apply Configuration")) + ]) + ]) + ]) + ]) + ); + + return sections.filter(function (n) { return n !== null && n !== undefined; }); + }, + + _buildProxyForm: function (container) { + var m = new form.Map('clashoo', '', ''); + var s, o; + + s = m.section(form.NamedSection, 'config', 'clashoo', _("Transparent Proxy")); + s.addremove = false; + o = s.option(form.ListValue, 'tcp_mode', _("TCP Mode")); + o.value('redirect', 'Redirect'); o.value('tproxy', 'TPROXY'); o.value('tun', 'TUN'); o.value('off', _("Off")); + o = s.option(form.ListValue, 'udp_mode', _("UDP Mode")); + o.value('tun', 'TUN'); o.value('tproxy', 'TPROXY'); o.value('off', _("Off")); + o = s.option(form.ListValue, 'stack', _("TUN Network Stack")); + o.value('system', 'System'); o.value('gvisor', 'gVisor'); o.value('mixed', 'Mixed'); + o.description = _("TUN stack is used when either TCP or UDP mode is TUN."); + o = s.option(form.Flag, 'disable_quic_gso', _("Disable QUIC GSO")); + o = s.option(form.Flag, 'block_quic', _("Block QUIC")); + o.description = _("Reject proxied UDP 443 so some apps fall back to TCP. Try only when downloads or video stall."); + o = s.option(form.Flag, 'ipv4_dns_hijack', _("IPv4 DNS Hijack")); + o = s.option(form.Flag, 'ipv6_dns_hijack', _("IPv6 DNS Hijack")); + o.description = _("Intercept IPv6 DNS traffic to prevent devices with hard-coded DNS from bypassing the traffic."); + o = s.option(form.Flag, 'ipv4_proxy', _("IPv4 Proxy")); + o = s.option(form.Flag, 'ipv6_proxy', _("IPv6 Proxy")); + o = s.option(form.Flag, 'fake_ip_ping_hijack', _("Virtual IP Ping Hijacking")); + o = s.option(form.Flag, 'dns_leak_protect', _("Prevent DNS Leaks")); + o.description = _("Regular domains use overseas DNS only and follow proxy rules; DoT/DoQ is blocked. Takes effect after restart.
") + + _("Warning: IPv6 resolution is also disabled when IPv6 Proxy is off, to keep the real address from leaking."); + o = s.option(form.Flag, 'core_only', _("Core Only (advanced)")); + o.description = _("Run only the core with your imported configuration, without taking over firewall / DNS / routing. Compatible with nikki, OpenClash, momo and homeproxy configurations.
") + + _("Prerequisite: The configuration must include transparent proxying (TUN auto-route or TProxy inbound). Takes effect after restart."); + + s = m.section(form.NamedSection, 'config', 'clashoo', _("Port Settings")); + s.addremove = false; + o = s.option(form.Flag, 'allow_lan', _("Allow LAN Access")); + o = s.option(form.Value, 'http_port', _("HTTP Port")); + o = s.option(form.Value, 'socks_port', _("SOCKS5 Port")); + o = s.option(form.Value, 'mixed_port', _("Mixed Port")); + o = s.option(form.Value, 'redir_port', _("Redirect Port")); + o = s.option(form.Value, 'tproxy_port', _("TPROXY Port")); + + s = m.section(form.NamedSection, 'config', 'clashoo', _("Smart Policy Settings")); + s.addremove = false; + + o = s.option(form.Flag, 'smart_auto_switch', _("Smart Policy Auto Switch")); + o.description = _("Automatically switch Url-test and Load-balance policy groups to Smart policy groups"); + + o = s.option(form.Value, 'smart_policy_priority', _("Policy Priority (weight bonus)")); + o.default = 'Premium:0.9;SG:1.3'; + o.placeholder = 'Premium:0.9;SG:1.3'; + o.rmempty = true; + o.description = _("Node weight bonus. <1 means lower priority, >1 means higher priority. Modify as needed; leave blank to skip policy-priority injection."); + + o = s.option(form.Flag, 'smart_prefer_asn', _("ASN Priority")); + o.description = _("Force ASN lookup and prefer target ASN information when selecting nodes for a more stable experience"); + + o = s.option(form.Flag, 'smart_uselightgbm', _("Enable LightGBM Model")); + o.description = _("Use LightGBM model to predict weights"); + + o = s.option(form.Flag, 'smart_collectdata', _("Collect Training Data")); + o.description = _("Collect node latency data for LightGBM model training"); + + o = s.option(form.Value, 'smart_collect_size', _("Training Data Size")); + o.datatype = 'uinteger'; + o.placeholder = '100'; + o.description = _("smart-collector-size, maximum retained training samples, default 100"); + + o = s.option(form.Value, 'smart_collect_rate', _("Sample Rate")); + o.datatype = 'uinteger'; + o.placeholder = '1'; + o.description = _("sample-rate, 1 = sample every latency test; increase to reduce sampling frequency"); + + o = s.option(form.Flag, 'smart_lgbm_auto_update', _("Auto Update Model")); + + o = s.option(form.Value, 'smart_lgbm_update_interval', _("Update interval (hours)")); + o.datatype = 'uinteger'; + o.placeholder = '72'; + o.description = _("Period for automatically fetching a new model; also controls the cron trigger frequency, default 72 hours"); + + o = s.option(form.Value, 'smart_lgbm_url', _("Model Download URL")); + o.placeholder = 'https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin'; + o.rmempty = true; + o.description = _("LightGBM model file download URL; leave blank to use the default official URL"); + + var sa = m.section(form.TypedSection, 'authentication', _("Proxy Authentication")); + sa.anonymous = true; sa.addremove = true; + o = sa.option(form.Value, 'username', _("Username")); + o = sa.option(form.Value, 'password', _("Password")); + + var sr = m.section(form.TypedSection, 'addtype', _("Custom Routing Rules"), + _("Correct routing by forcing a domain or IP to Direct or Proxy. These rules take priority over subscription rules and apply to both mihomo and sing-box.")); + sr.anonymous = true; sr.addremove = true; + o = sr.option(form.Value, 'ipaaddr', _("Domain / IP")); + /* allow empty so the default placeholder row never blocks saving other settings; + empty rule rows are pruned on save (pruneEmptyAddtype) instead of erroring */ + o.rmempty = true; + o.placeholder = _("example.com or 1.2.3.0/24"); + o = sr.option(form.ListValue, 'type', _("Type")); + o.value('DOMAIN-SUFFIX', _("Domain suffix (e.g. google.com)")); + o.value('DOMAIN', _("Exact domain (e.g. www.google.com)")); + o.value('DOMAIN-KEYWORD', _("Domain keyword (e.g. youtube)")); + o.value('IP-CIDR', _("IP / CIDR (e.g. 8.8.8.8/32)")); + o.default = 'DOMAIN-SUFFIX'; + o = sr.option(form.ListValue, 'pgroup', _("Target")); + o.value('DIRECT', _("Direct")); + o.value('__PROXY__', _("Proxy")); + o.default = 'DIRECT'; + o = sr.option(form.Flag, 'res', 'no-resolve'); + o.depends('type', 'IP-CIDR'); + o.description = _("IP rules do not trigger DNS resolution"); + + m.render().then(function (node) { + decorateControlWraps(node); + container.appendChild(node); + + // Smart section action buttons + var sections = node.querySelectorAll('.cbi-section'); + var smartSec; + for (var i = 0; i < sections.length; i++) { + var h3 = sections[i].querySelector('h3'); + if (h3 && h3.textContent.indexOf('Smart') >= 0) { smartSec = sections[i]; break; } + } + if (smartSec) { + var flushBtn = E('button', { 'class': 'btn cbi-button', 'click': function () { + flushBtn.disabled = true; + callSmartFlushCache().then(function (res) { + flushBtn.disabled = false; + ui.addNotification(null, E('p', (res && res.success) ? _("Smart cache cleared") : _("Cleanup failed (mihomo may not be running)"))); + }).catch(function () { flushBtn.disabled = false; }); + }}, _("Clear")); + smartSec.appendChild(E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _("Clear Smart Cache")), + E('div', { 'class': 'cbi-value-field' }, [flushBtn]) + ])); + } + makeSectionCollapsible(node, _("Transparent Proxy"), true); + makeSectionCollapsible(node, _("Port Settings"), true); + makeSectionCollapsible(node, _("Smart Policy Settings"), false); + makeSectionCollapsible(node, _("Proxy Authentication"), false); + makeSectionCollapsible(node, _("Custom Routing Rules"), false); + + container.appendChild(E('div', { 'class': 'cl-save-bar' }, [ + E('button', { 'class': 'btn cbi-button', click: function () { + m.save(null, true).then(function () { return clashoo.commitConfig(); }) + .then(function () { return clearClashooDirty(); }) + .then(function () { location.reload(); }) + .catch(function (e) { ui.addNotification(null, E('p', _("Save failed: ") + (e.message || e))); }); + }}, _("Save Configuration")), + E('button', { 'class': 'btn cbi-button-action', click: function () { + // 保存前探测主代理组,供自定义分流规则的「代理」(__PROXY__) 解析(失败忽略,注入器回退 GLOBAL) + callDetectPrimaryGroup().catch(function () {}).then(function () { + return saveCommitApplyMaybeReload(m, _("Proxy configuration saved and service hot-reloaded"), _("Proxy configuration saved, service is not running")); + }).catch(function (e) { ui.addNotification(null, E('p', _("Operation failed: ") + (e.message || e))); }); + }}, _("Apply Configuration")) + ])); + }); + }, + + _buildDnsForm: function (container) { + var m = new form.Map('clashoo', '', ''); + var s, o; + + s = m.section(form.NamedSection, 'config', 'clashoo', _("Basic DNS")); + s.addremove = false; + o = s.option(form.DummyValue, '_dns_auto_setup', _("DNS Auto Setup")); + o.cfgvalue = function () { + var statusEl = E('div', { 'class': 'cl-dns-auto-status' }, []); + var last = readDnsAutoResult(); + if (last && last.success) + setDnsAutoStatus(statusEl, dnsAutoSummaryNode(last)); + else + statusEl.textContent = _("Automatically choose available DNS and update only upstream servers; split policies and advanced settings remain unchanged."); + if (last && last.success) + window.setTimeout(function () { + clearDnsAutoResult(); + statusEl.textContent = _("Automatically choose available DNS and update only upstream servers; split policies and advanced settings remain unchanged."); + }, 6000); + var btn = E('button', { + 'class': 'btn cbi-button-action', + click: function (ev) { + ev.preventDefault(); + btn.disabled = true; + setDnsAutoStatus(statusEl, _("Testing latency and writing upstream DNS…")); + clashoo.dnsAutoSetup() + .then(function (res) { + btn.disabled = false; + if (!res || !res.success) { + setDnsAutoStatus(statusEl, (res && res.message) || _("DNS auto setup failed")); + ui.addNotification(null, E('p', (res && res.message) || _("DNS auto setup failed"))); + return; + } + storeDnsAutoResult(res); + setDnsAutoStatus(statusEl, dnsAutoSummaryNode(res)); + ui.addNotification(null, E('p', dnsAutoResultMessage(res))); + window.setTimeout(function () { + clearDnsAutoResult(); + statusEl.textContent = _("Auto setup completed."); + }, 6000); + }) + .catch(function (e) { + btn.disabled = false; + setDnsAutoStatus(statusEl, _("DNS auto setup failed: ") + (e.message || e)); + ui.addNotification(null, E('p', _("DNS auto setup failed: ") + (e.message || e))); + }); + } + }, _("Test and Apply")); + return E('div', { 'class': 'cl-dns-auto' }, [ + E('div', { 'class': 'cl-dns-auto-actions' }, [btn]), + statusEl + ]); + }; + o.write = function () {}; + o = s.option(form.Flag, 'enable_dns', _("Enable DNS Module")); + o = s.option(form.Value, 'listen_port', _("DNS Listen Port")); + o.datatype = 'port'; + o = s.option(form.ListValue, 'enhanced_mode', _("Enhanced Mode")); + o.value('fake-ip', 'Fake-IP'); o.value('redir-host', 'Redir-Host'); + o.default = 'fake-ip'; + o.description = 'Fake-IP · ' + _("Recommended") + ' ' + + _("Fast resolution and accurate routing; China routing is handled by the core.") + '
' + + 'Redir-Host ' + + _("China traffic bypasses the core at firewall level; DNS behavior is slightly weaker. Choose as needed."); + o = s.option(form.Value, 'fake_ip_range', _("Fake-IP Range")); + o.default = '198.18.0.1/16'; + o.placeholder = '198.18.0.1/16'; + o.depends('enhanced_mode', 'fake-ip'); + o.remove = function () {}; + o = s.option(form.Flag, 'enable_ipv6', 'IPv6 DNS'); + + s = m.section(form.NamedSection, 'config', 'clashoo', _("Advanced DNS")); + s.addremove = false; + o = s.option(form.Flag, 'dnsforwader', _("Force DNS Forwarding")); + o.depends('enable_dns', '1'); + o = s.option(form.ListValue, 'fake_ip_filter_mode', _("Fake-IP Filter Mode")); + o.value('blacklist', _("Blocklist (listed items use real IP, default)")); + o.value('whitelist', _("Allowlist (only listed items use fake-IP)")); + o.value('rule', _("Rule mode (same syntax as rules)")); + o.default = 'blacklist'; + o.depends('enhanced_mode', 'fake-ip'); + + o = s.option(form.DynamicList, 'fake_ip_filter', _("Fake-IP Filter Domains")); + o.placeholder = '+.lan / geosite:cn / RULE-SET,cn_domain,real-ip'; + o.description = _("Blocklist mode: enter domains or shorthand like geosite:cn. Rule mode: entries are converted to rule syntax automatically, such as GEOSITE,cn,real-ip / RULE-SET,xxx,real-ip; usually add MATCH,fake-ip at the end as fallback. geosite:cn automatically uses built-in cn.mrs acceleration and avoids loading the 10MB geosite.dat. Allowlist mode drops the shipped defaults (they mean the opposite there), so enter your own domains."); + o.depends('enhanced_mode', 'fake-ip'); + o.remove = function () {}; + o = s.option(form.DynamicList, 'default_nameserver', 'Bootstrap DNS'); + o.placeholder = '223.5.5.5'; + o.description = _("Used to resolve DoH/DoT/DoQ server domains; plain IP DNS is recommended."); + o = s.option(form.Flag, 'dns_respect_rules', _("DNS Respect Rules")); + o.default = '1'; + o.description = _("DNS queries follow the routing rules, so overseas domains resolve through the proxy instead of a polluted result. Requires proxy-server-nameserver."); + o = s.option(form.Flag, 'dns_loopback_compat', _("OpenClash/Nikki Loopback DNS Compatibility")); + o.default = '0'; + o.description = _("Optional compatibility for normal managed mode. At runtime, replace only proxy node resolvers that point back to the imported configuration's own DNS listener with the proxy-server-nameserver setting. Core Only mode stays verbatim; the source configuration and unrelated local DNS services remain unchanged."); + o.depends('enable_dns', '1'); + o = s.option(form.Value, 'dns_ecs', _("ECS Client Subnet")); + o.placeholder = _("Recommended blank"); + o.description = _("mihomo writes the ecs parameter to DNS URLs; sing-box writes dns.client_subnet. Leave empty to skip."); + o.rmempty = true; + o = s.option(form.Flag, 'dns_ecs_override', _("Force ECS Override")); + o.default = '0'; + o = s.option(form.Flag, 'fallback_filter_geoip', _("Fallback GeoIP Filter")); + o.default = '0'; + o.description = _("Requires MMDB. If enabled while the local GeoIP database is missing, the core may fail to start. Usually not needed."); + o = s.option(form.DynamicList, 'fallback_filter_ipcidr', 'Fallback IP CIDR'); + o.placeholder = '240.0.0.0/4'; + o = s.option(form.Flag, 'singbox_independent_cache', _("sing-box Independent DNS Cache")); + o.default = '0'; + o.description = _("When enabled, direct / proxy / fallback roles each use an independent DNS cache. Useful when the same domain needs different resolver chains by routing. Usually not needed."); + + s = m.section(form.TypedSection, 'dnsservers', _("Upstream DNS")); + s.addremove = true; s.anonymous = true; + o = s.option(form.Flag, 'enabled', _("Enable")); + o.default = '1'; + o = s.option(form.ListValue, 'ser_type', _("Role")); + o.value('nameserver', _("Domestic Upstream DNS")); + o.value('direct-nameserver', _("Direct Domain Resolver")); + o.value('proxy-server-nameserver', _("Proxy Node Domain Resolver")); + o.value('fallback', _("Overseas Encrypted DNS (anti-pollution)")); + o.default = 'nameserver'; + o = s.option(form.Value, 'ser_address', _("DNS Address")); + o.placeholder = 'https://dns.alidns.com/dns-query / https://doh.pub/dns-query'; + o = s.option(form.ListValue, 'protocol', _("Protocol")); + o.value('none', _("Full URL / no protocol prefix")); + o.value('udp://', 'UDP'); + o.value('tcp://', 'TCP'); + o.value('tls://', 'DoT / TLS'); + o.value('https://', 'DoH / HTTPS'); + o.value('quic://', 'DoQ / QUIC'); + o.default = 'none'; + o = s.option(form.Value, 'ser_port', _("Port")); + o.placeholder = '853 / 784'; + o.rmempty = true; + + s = m.section(form.TypedSection, 'dns_policy', _("DNS Policy")); + s.addremove = true; s.anonymous = true; + o = s.option(form.Flag, 'enabled', _("Enable")); + o.default = '1'; + o = s.option(form.ListValue, 'policy_type', _("Policy Type")); + o.value('nameserver-policy', _("Domain Split DNS")); + o.value('proxy-server-nameserver-policy', _("Proxy Domain Split DNS")); + o.default = 'nameserver-policy'; + o = s.option(form.Value, 'matcher', _("Match Rule")); + o.placeholder = 'geosite:cn / domain:example.com / domain-suffix:google.com'; + o = s.option(form.DynamicList, 'nameserver', _("Use DNS")); + o.placeholder = 'udp://223.5.5.5'; + o.description = _("Example: geosite:cn uses https://dns.alidns.com/dns-query; geosite:geolocation-!cn uses https://cloudflare-dns.com/dns-query."); + + m.render().then(function (node) { + decorateControlWraps(node); + makeSectionCollapsible(node, _("Basic DNS"), true); + makeSectionCollapsible(node, _("Advanced DNS"), false); + makeSectionCollapsible(node, _("Upstream DNS"), false); + makeSectionCollapsible(node, _("DNS Policy"), false); + container.appendChild(node); + container.appendChild(E('div', { 'class': 'cl-save-bar' }, [ + E('button', { 'class': 'btn cbi-button', click: function () { + m.save(null, true).then(function () { return clashoo.commitConfig(); }) + .then(function () { return clearClashooDirty(); }) + .then(function () { location.reload(); }) + .catch(function (e) { ui.addNotification(null, E('p', _("Save failed: ") + (e.message || e))); }); + }}, _("Save Configuration")), + E('button', { 'class': 'btn cbi-button-action', click: function () { + saveCommitApplyMaybeReload(m, _("DNS configuration saved and service hot-reloaded"), _("DNS configuration saved, service is not running")) + .catch(function (e) { ui.addNotification(null, E('p', _("Operation failed: ") + (e.message || e))); }); + }}, _("Apply Configuration")) + ])); + }); + }, + + /* ── sing-box UI ── */ + + _renderSingbox: function (sbData, uiData, subscriptionUpdateStatus) { + var self = this; + var profiles = sbData.profiles || []; + var tabEls = {}, panelEls = {}; + + var tabs = [ + { id: 'profiles', label: _("Configuration Files") }, + { id: 'wizard', label: _("Quick Wizard") } + ]; + var allowedTabs = tabs.map(function (t) { return t.id; }); + this._sbTab = readSavedTab('clashoo.config.singbox.tab', this._sbTab || 'profiles', allowedTabs); + rememberTab('clashoo.config.singbox.tab', this._sbTab); + + var tabBar = E('div', { 'class': 'cl-tabs' }, + tabs.map(function (t) { + var el = E('div', { + 'class': 'cl-tab' + (self._sbTab === t.id ? ' active' : ''), + click: function () { + Object.keys(tabEls).forEach(function (k) { + tabEls[k].className = 'cl-tab' + (k === t.id ? ' active' : ''); + panelEls[k].className = 'cl-panel' + (k === t.id ? ' active' : ''); + }); + self._sbTab = t.id; + rememberTab('clashoo.config.singbox.tab', t.id); + } + }, t.label); + tabEls[t.id] = el; + return el; + }) + ); + + var profilesPanel = E('div', { 'class': 'cl-panel' + (this._sbTab === 'profiles' ? ' active' : ''), id: 'cl-panel-profiles' }, + self._buildSbProfilesPanel(profiles, sbData.active || '', uiData, subscriptionUpdateStatus)); + panelEls['profiles'] = profilesPanel; + + var wizardPanel = E('div', { 'class': 'cl-panel' + (this._sbTab === 'wizard' ? ' active' : ''), id: 'cl-panel-wizard' }, + self._buildSbWizardPanel()); + panelEls['wizard'] = wizardPanel; + + return E('div', { 'class': 'cl-wrap clashoo-container cl-config-page cl-form-page ' + getThemeClass() }, [tabBar, profilesPanel, wizardPanel]); + }, + + _buildSbProfilesPanel: function (profiles, activeProfile, uiData, subscriptionUpdateStatus) { + var self = this; + var safeText = function (v) { return (v == null || v === 'null') ? '' : String(v); }; + var formatJsonForEditor = function (content) { + try { + return JSON.stringify(JSON.parse(content || '{}'), null, 2) + '\n'; + } catch (e) { + return content || ''; + } + }; + + /* ── JSON editor (initially hidden) ── */ + var editorTitle = E('span', { 'class': 'cl-editor-hdr' }, _("Select a configuration above to edit it here")); + var ed = createConfigEditor({ name: 'javascript', json: true }); + var saveBtn = E('button', { + 'class': 'btn cbi-button-action cl-btn-sm', + disabled: '', + click: function () { + var name = ed.textarea.dataset.name; + if (!name) return; + clashoo.saveSingboxProfile(name, formatJsonForEditor(ed.getValue())).then(function (r) { + if (r.success) ui.addNotification(null, E('p', name + _(" saved"))); + else ui.addNotification(null, E('p', _("Save failed: ") + (r.message || r.error || ''))); + }); + } + }, _("Save")); + + var migrateBtn = E('button', { + 'class': 'btn cbi-button cl-btn-sm', + disabled: '', + click: function () { + var name = ed.textarea.dataset.name; + if (!name) return; + L.resolveDefault(callMigrateSbProfile(name), {}).then(function (r) { + if (r && r.success) { + var msg = r.changes && r.changes.length ? _("Fixed deprecated fields: ") + r.changes.join(', ') : _("Configuration is up to date; no fixes needed"); + ui.addNotification(null, E('p', msg)); + /* 重新加载编辑器内容 */ + clashoo.getSingboxProfile(name).then(function (gr) { ed.setValue(formatJsonForEditor(gr.content || '')); }); + } else { + ui.addNotification(null, E('p', _("Fix failed: ") + ((r && r.message) || ''))); + } + }); + } + }, _("Fix Deprecated Fields")); + + var editorBox = E('div', { 'class': 'cl-section cl-card cl-sb-card cl-sb-editor' }, [ + editorTitle, + ed.el, + E('div', { 'class': 'cl-actions cl-sb-row-actions cl-sb-editor-actions' }, [ + saveBtn, + migrateBtn, + E('span', { 'class': 'cl-hint' }, _("Click Save after editing; switching configuration automatically restarts the service")) + ]) + ]); + + function loadEditor(name) { + editorTitle.textContent = _("Editing: ") + name; + saveBtn.removeAttribute('disabled'); + migrateBtn.removeAttribute('disabled'); + ed.textarea.dataset.name = name; + ed.setValue(_("Loading…")); + clashoo.getSingboxProfile(name).then(function (r) { + ed.setValue(formatJsonForEditor(r.content || '')); + }); + } + + /* ── Profile table ── */ + var rows = profiles.length + ? profiles.map(function (p) { + var nameCell = [ + E('div', { 'class': 'cl-sb-file-name' }, [ + p.active ? E('span', { 'class': 'cl-active-badge' }, _("Active")) : '', + E('span', { 'class': 'cl-file-name-text' }, safeText(p.name)) + ]) + ]; + var trafficEl = renderTrafficBar(p.sub_used, p.sub_total); + var expireEl = renderExpire(p.sub_expire); + if (trafficEl) nameCell.push(trafficEl); + if (expireEl) nameCell.push(expireEl); + return E('tr', {}, [ + E('td', {}, nameCell), + E('td', { 'class': 'cl-sb-size' }, safeText(p.size) || '—'), + E('td', {}, [ + E('div', { 'class': 'cl-sb-row-actions' }, [ + p.source !== 'native' ? E('button', { + 'class': 'btn cbi-button cl-btn-sm cl-btn-sb-action cl-btn-sb-edit', + click: function () { loadEditor(p.name); } + }, _("Edit")) : '', + E('button', { + 'class': 'btn cbi-button-action cl-btn-sm cl-btn-sb-action cl-btn-sb-switch', + click: function () { + clashoo.setSingboxProfile(p.name).then(function (r) { + ui.addNotification(null, E('p', r.success ? _("Switched to ") + p.name : (_("Switch failed: ") + (r.message || '')))); + if (r.success) location.reload(); + }); + } + }, _("Switch Profile")), + p.source === 'native' && p.sub_url ? E('button', { + 'class': 'btn cbi-button cl-btn-sm cl-btn-sb-action', + click: function (ev) { + var btn = ev.currentTarget; + btn.disabled = true; + btn.textContent = _("Updating…"); + clashoo.updateSingboxNative(p.name).then(function (r) { + btn.disabled = false; + btn.textContent = _("Update"); + ui.addNotification(null, E('p', r.success ? (r.message || p.name + _(" updated")) : (_("Update failed: ") + (r.message || '')))); + if (r.success) location.reload(); + }); + } + }, _("Update")) : '', + E('button', { + 'class': 'btn cbi-button-negative cl-btn-sm cl-btn-sb-action cl-btn-sb-delete', + click: function () { + if (!confirm(_("Delete ") + p.name + '?')) return; + clashoo.deleteSingboxProfile(p.name).then(function () { location.reload(); }); + } + }, _("Delete")) + ]) + ]) + ]); + }) + : [E('tr', {}, [E('td', { 'class': 'cl-sb-empty', colspan: '3' }, _("No configuration files yet. Use Quick Wizard to generate one or upload a JSON file."))])]; + + /* ── Upload ── */ + var uploadInput = E('input', { type: 'file', accept: '.json', style: 'display:none', id: 'sb-upload' }); + uploadInput.addEventListener('change', function (ev) { + var file = ev.target.files[0]; + if (!file) return; + var reader = new FileReader(); + reader.onload = function (e) { + clashoo.saveSingboxProfile(file.name, e.target.result).then(function (r) { + if (r.success) { ui.addNotification(null, E('p', _("Upload succeeded: ") + r.name)); location.reload(); } + else ui.addNotification(null, E('p', _("Upload failed: ") + (r.message || r.error || ''))); + }); + }; + reader.readAsText(file); + }); + + return [ + E('div', { 'class': 'cl-section cl-card cl-sb-card' }, [ + E('h4', {}, _("Subscription Update")), + buildSubscriptionSchedule(uiData || {}, subscriptionUpdateStatus || {}) + ]), + E('div', { 'class': 'cl-section cl-card cl-sb-card' }, [ + E('h4', {}, _("sing-box Configuration Files")), + E('table', { 'class': 'cl-sub-list cl-sb-list' }, [ + E('thead', {}, E('tr', {}, [E('th', {}, _("File Name")), E('th', {}, _("Size")), E('th', {}, _("Actions"))])), + E('tbody', {}, rows) + ]), + uploadInput, + E('div', { 'class': 'cl-actions cl-sb-top-actions' }, [ + E('button', { + 'class': 'btn cbi-button-add cl-btn-sm cl-btn-sb-upload', + click: function () { document.getElementById('sb-upload').click(); } + }, _("Upload JSON Configuration")) + ]) + ]), + editorBox + ]; + }, + + _buildSbWizardPanel: function () { + var urlInput = E('input', { + 'class': 'cl-sub-url', + type: 'text', + placeholder: _("Paste subscription URL (supports vmess / vless / trojan, etc.)") + }); + var nameInput = E('input', { + 'class': 'cl-sub-url', + type: 'text', + placeholder: _("Configuration file name (optional, blank auto-generates singbox.json)"), + style: 'margin-top:0' + }); + var savedUa = ''; + try { savedUa = uci.get('clashoo', 'config', 'sub_ua') || ''; } catch (e) {} + // YAML 订阅转换走 mihomo 订阅流程(拉 yaml → yaml2singbox),UA 与 mihomo 共用 sub_ua + var convertUaPicker = buildUaPicker(savedUa); + + var genBtn, applyBtn; + function setBusy(busy, activeBtn) { + [genBtn, applyBtn].forEach(function (b) { + if (!b) return; + b.disabled = busy ? '' : null; + if (busy) { + if (!b.dataset.label) b.dataset.label = b.textContent; + if (b === activeBtn) + b.textContent = b._clBusyLabel || _("Processing…"); + } else if (b.dataset.label) { + b.textContent = b.dataset.label; + } + }); + } + function doCreate(setActive) { + var url = urlInput.value.trim(); + if (!url) { ui.addNotification(null, E('p', _("Please enter a subscription URL"))); return; } + setBusy(true, setActive ? applyBtn : genBtn); + // 保存 UA 到 UCI,后端 create_singbox_config 会读这个字段 + var uaPromise = L.resolveDefault(uci.load('clashoo'), null).then(function () { + uci.set('clashoo', 'config', 'sub_ua', convertUaPicker.getValue()); + return uci.save() + .then(function () { return clashoo.commitConfig(); }) + .then(function () { return clearClashooDirty(); }); + }).catch(function () {}); + uaPromise.then(function () { + return clashoo.createSingboxConfig(url, nameInput.value.trim()); + }) + .then(function (r) { + /* RPC 超时被 resolveDefault 兜底成 {},r.success 是 undefined。 + * 此时后端可能仍在跑(yaml2singbox 转 90 节点等),让用户直接刷新页 + * 看实际产物,而不是误报"失败"。 */ + if (!r || typeof r.success === 'undefined') { + ui.addNotification(null, E('p', _("Generation is taking longer, refreshing to show results…"))); + setBusy(false); + setTimeout(function () { location.reload(); }, 1500); + return; + } + if (!r.success) { + setBusy(false); + ui.addNotification(null, E('p', _("Generate failed: ") + (r.message || ''))); + return; + } + if (setActive) { + return clashoo.setSingboxProfile(r.name).then(function () { + ui.addNotification(null, E('p', r.message + _(", switched to active configuration"))); + location.reload(); + }); + } + ui.addNotification(null, E('p', r.message)); + location.reload(); + }).catch(function (e) { + setBusy(false); + ui.addNotification(null, E('p', _("Generate error: ") + (e && e.message || e))); + }); + } + + genBtn = E('button', { 'class': 'btn cbi-button cl-btn-sm', click: function () { doCreate(false); } }, _("Generate Configuration")); + applyBtn = E('button', { 'class': 'btn cbi-button-action cl-btn-sm', click: function () { doCreate(true); } }, _("Apply Configuration")); + genBtn._clBusyLabel = _("Generating…"); + applyBtn._clBusyLabel = _("Applying…"); + + /* ── native sing-box subscription card ── */ + // 注:原生 sing-box JSON 订阅不需要 UA 选项(机场按链接参数/路径区分格式) + var nativeUrlInput = E('input', { + 'class': 'cl-sub-url', + type: 'text', + placeholder: _("Paste native sing-box subscription URL (direct JSON response)") + }); + var nativeNameInput = E('input', { + 'class': 'cl-sub-url', + type: 'text', + placeholder: _("File name (optional, blank for automatic name)"), + style: 'margin-top:0' + }); + + var fetchBtn, fetchApplyBtn; + var fetchStatusEl = E('div', { 'class': 'cl-update-status', style: 'margin-top:6px;font-size:12px;min-height:18px;line-height:1.4' }); + var setFetchStatus = function (text, tone) { + fetchStatusEl.textContent = text || ''; + fetchStatusEl.style.color = tone === 'success' ? 'var(--success-color, #2e7d32)' + : tone === 'error' ? 'var(--error-color, #d32f2f)' + : tone === 'progress'? 'var(--tip-color, #1976d2)' + : ''; + }; + function setNativeBusy(busy) { + [fetchBtn, fetchApplyBtn].forEach(function (b) { + if (!b) return; + b.disabled = busy ? '' : null; + if (busy) { + if (!b.dataset.label) b.dataset.label = b.textContent; + b.textContent = _("Fetching…"); + } else if (b.dataset.label) { + b.textContent = b.dataset.label; + } + }); + } + function doFetchNative(setActive) { + var url = nativeUrlInput.value.trim(); + if (!url) { + setFetchStatus(_("✗ Please enter a subscription URL"), 'error'); + setTimeout(function () { setFetchStatus(''); }, 4000); + return; + } + setNativeBusy(true); + setFetchStatus(_("⏳ Fetching configuration..."), 'progress'); + clashoo.fetchSingboxNative(url, nativeNameInput.value.trim()) + .then(function (r) { + setNativeBusy(false); + if (!r || typeof r.success === 'undefined') { + setFetchStatus(_("⏳ Fetch timed out, refreshing to show results..."), 'progress'); + setTimeout(function () { location.reload(); }, 1500); + return; + } + if (!r.success) { + setFetchStatus(_("✗ Fetch failed: ") + (r.message || ''), 'error'); + return; + } + if (setActive) { + setFetchStatus('⏳ ' + r.message + _(", switching to active configuration..."), 'progress'); + return clashoo.setSingboxProfile(r.name).then(function () { + setFetchStatus('✓ ' + r.message + _(", switched to active configuration"), 'success'); + setTimeout(function () { location.reload(); }, 1200); + }); + } + setFetchStatus('✓ ' + r.message, 'success'); + setTimeout(function () { location.reload(); }, 1200); + }).catch(function (e) { + setNativeBusy(false); + setFetchStatus(_("✗ Fetch error: ") + (e && e.message || e), 'error'); + }); + } + + fetchBtn = E('button', { 'class': 'btn cbi-button cl-btn-sm', click: function () { doFetchNative(false); } }, _("Fetch Configuration")); + fetchApplyBtn = E('button', { 'class': 'btn cbi-button-action cl-btn-sm', click: function () { doFetchNative(true); } }, _("Fetch and Apply")); + + return [ + E('div', { 'class': 'cl-section cl-card cl-sb-card' }, [ + E('h4', {}, _("Node Subscription")), + E('div', { 'class': 'cl-form-wrap cl-fixed-600 cl-sb-form' }, [ + nativeUrlInput, nativeNameInput, + E('div', { 'class': 'cl-actions cl-sb-top-actions' }, [fetchBtn, fetchApplyBtn]), + fetchStatusEl + ]), + E('p', { 'class': 'cl-sb-note' }, + _("For providers that directly offer sing-box JSON subscriptions, or links already converted by external tools. After fetching, click Update on the corresponding item in the Configuration Files tab to fetch the latest configuration again.") + ) + ]), + E('div', { 'class': 'cl-section cl-card cl-sb-card' }, [ + E('h4', {}, _("YAML Subscription Conversion")), + E('div', { 'class': 'cl-form-wrap cl-fixed-600 cl-sb-form' }, [ + urlInput, nameInput, convertUaPicker.wrap, + E('div', { 'class': 'cl-actions cl-sb-top-actions' }, [ + genBtn, + applyBtn + ]) + ]), + E('p', { 'class': 'cl-sb-note' }, + _("Convert YAML subscriptions to sing-box JSON, automatically injecting TUN transparent proxy and China direct rules. Files with the same name are overwritten; use the same name when updating.") + ) + ]) + ]; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/index.js b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/index.js new file mode 100644 index 000000000..76167ca18 --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/index.js @@ -0,0 +1,4 @@ +'use strict'; +'require view.clashoo.overview as overview'; + +return overview; diff --git a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js new file mode 100644 index 000000000..504775903 --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js @@ -0,0 +1,2030 @@ +'use strict'; +'require view'; +'require poll'; +'require ui'; +'require rpc'; +'require uci'; +'require tools.clashoo as clashoo'; + +var CSS = [ + '.cl-wrap{padding:8px 0}', + '.cl-wrap{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}', + '.cl-wrap{--cl-live-title-color:rgba(40,50,68,.62);--cl-live-main-color:rgba(84,96,116,.92);--cl-live-sub-color:rgba(74,86,106,.72);--cl-live-foot-color:rgba(74,86,106,.72);--cl-live-zero-color:rgba(128,128,128,.5)}', + 'body.dark .cl-wrap,body.dark .cl-wrap,body[data-theme="dark"] .cl-wrap,body[data-darkmode="1"] .cl-wrap,body[data-darkmode="true"] .cl-wrap,html.dark .cl-wrap,html.dark .cl-wrap,html[data-theme="dark"] .cl-wrap,html[data-bs-theme="dark"] .cl-wrap,html[data-darkmode="1"] .cl-wrap,html[data-darkmode="true"] .cl-wrap{--cl-live-title-color:rgba(230,238,252,.74);--cl-live-main-color:rgba(228,236,248,.9);--cl-live-sub-color:rgba(208,219,238,.72);--cl-live-foot-color:rgba(208,219,238,.72)}', + '.cl-status-kernel{display:inline-flex;align-items:center;gap:2px;padding:2px;border-radius:8px;border:0;background:rgba(128,128,128,.1)}', + '.cl-status-kernel .cl-core-btn{height:24px;line-height:24px;min-width:60px;padding:0 10px;border:0;border-radius:6px;background:transparent;font-size:11px;font-weight:500;color:var(--cl-live-main-color);opacity:.6;cursor:pointer;transition:background-color .18s ease,color .18s ease,opacity .18s ease}', + '.cl-status-kernel .cl-core-btn.active{background:linear-gradient(#3886a1,#2f7288);border:0;color:#fff;opacity:1;font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,.12)}', + '.cl-status-kernel .cl-core-btn:disabled{cursor:not-allowed}', + '.cl-status-kernel.is-busy{opacity:.6}', + '.cl-core-switch-wrap{display:flex;flex-direction:column;align-items:flex-start;gap:6px}', + '.cl-core-switch-hint{font-size:12px;line-height:1.35;color:rgba(74,86,106,.72)}', + 'body.dark .cl-core-switch-hint,html[data-theme="dark"] .cl-core-switch-hint,html[data-bs-theme="dark"] .cl-core-switch-hint{color:rgba(208,219,238,.72)}', + '.cl-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:16px}', + '.cl-card{border:1px solid rgba(0,0,0,.05);border-radius:10px;padding:14px 16px;min-height:70px;box-shadow:0 4px 12px rgba(0,0,0,.03)}', + '.cl-card-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px}', + '.cl-card .lbl{font-size:11px;opacity:.55;margin:0}', + '.cl-card-tools{display:flex;align-items:center;gap:6px}', + '.cl-icon-btn{border:0;background:transparent;padding:0;width:16px;height:16px;line-height:16px;font-size:13px;opacity:.55;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:inherit}', + '.cl-icon-btn:hover{opacity:.9}', + '.cl-icon-btn.is-loading{animation:cl-spin 1s linear infinite;pointer-events:none;opacity:.75}', + '.cl-card .val{font-size:11px;font-weight:500;word-break:break-all}', + '.cl-card-mode .val{line-height:1.45;word-break:normal}', + '.cl-mode-stack{display:flex;flex-direction:column;gap:4px;justify-content:flex-start;align-items:flex-start}', + '.cl-mode-tag{display:flex;align-items:center;justify-content:flex-start;padding:0;border-radius:0;font-size:11px;line-height:1.35;background:transparent;color:inherit;opacity:.5;width:max-content;max-width:100%;text-align:left}', + 'body.dark .cl-mode-tag,body.dark .cl-mode-tag,body[data-theme="dark"] .cl-mode-tag,body[data-darkmode="1"] .cl-mode-tag,body[data-darkmode="true"] .cl-mode-tag,html.dark .cl-mode-tag,html.dark .cl-mode-tag,html[data-theme="dark"] .cl-mode-tag,html[data-bs-theme="dark"] .cl-mode-tag,html[data-darkmode="1"] .cl-mode-tag,html[data-darkmode="true"] .cl-mode-tag{background:transparent;color:inherit;opacity:.5}', + '.cl-mode-tag-degraded{color:#b56f00}', + '.cl-check-note{display:block;margin-top:6px;font-size:11px;line-height:1.45;opacity:.6;font-weight:400}', + '.cl-badge{display:inline-flex;align-items:center;justify-content:center;padding:2px 12px;border-radius:999px;font-size:11px;font-weight:700;line-height:1.2;background:transparent;border:1px solid transparent}', + '.cl-badge-run{background:transparent;color:#4aa065;border-color:rgba(74,160,101,.46)}', + '.cl-badge-stop{background:transparent;color:#df6e6e;border-color:rgba(223,110,110,.52)}', + '.cl-badge-sync{background:rgba(128,128,128,.08);color:rgba(128,128,128,.92);border-color:rgba(128,128,128,.35)}', + '.cl-service-tools{display:inline-flex;align-items:center;gap:8px;white-space:nowrap}', + '.cl-service-label{font-size:11px;font-weight:600;opacity:.62}', + '.cl-service-switch{position:relative;width:48px;height:26px;border:0;border-radius:999px;background:rgba(128,128,128,.22);padding:2px;box-shadow:none;cursor:pointer;transition:background .18s ease,opacity .18s ease}', + '.cl-service-switch .cl-service-knob{position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:rgba(255,255,255,.94);box-shadow:0 1px 4px rgba(0,0,0,.18);transition:transform .18s ease,background .18s ease}', + '.cl-service-switch.is-on{background:rgba(74,160,101,.52)}', + '.cl-service-switch.is-on .cl-service-knob{transform:translateX(22px)}', + '.cl-service-switch:disabled{opacity:.48;cursor:not-allowed}', + '.cl-controls{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}', + '.cl-ctrl{border:0 !important;border-radius:8px;padding:10px 12px;box-shadow:none !important;background:rgba(128,128,128,.04)}', + '.cl-ctrl label{display:block;font-size:11px;opacity:.55;margin-bottom:6px}', + '.cl-ctrl select{width:100%;font-size:13px;box-sizing:border-box}', + '.cl-ctrl-row{display:flex;gap:6px;align-items:center}', + '.cl-ctrl-row select{flex:1;min-width:0}', + '.cl-live-box{border:0 !important;border-radius:10px;padding:12px 14px;box-shadow:none !important;background:transparent}', + '.cl-traffic-wrap{margin-bottom:12px}', + '.cl-live-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}', + '.cl-live-card,.cl-stat-card{position:relative;height:120px;min-height:120px;max-height:120px;box-sizing:border-box;border:0 !important;border-radius:12px;overflow:hidden;box-shadow:0 2px 10px rgba(0,0,0,.03)}', + '.cl-live-content{position:relative;z-index:2;padding:11px 12px}', + '.cl-live-bg-chart{position:absolute;left:8px;right:8px;bottom:8px;height:42px;z-index:1;pointer-events:none;opacity:.92}', + '.cl-live-title{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600;margin-bottom:8px;color:var(--cl-live-title-color)}', + '.cl-live-main{display:flex;align-items:flex-end;gap:6px;line-height:1;margin-bottom:14px}', + '.cl-live-num{font-size:18px;font-weight:800;letter-spacing:-1px;color:var(--cl-live-main-color,rgba(84,96,116,.92));transition:color .18s ease,opacity .18s ease,text-shadow .22s ease,font-size .18s ease}', + '.cl-live-unit{font-size:11px;font-weight:600;margin-left:3px;padding-bottom:2px;color:var(--cl-live-sub-color,rgba(74,86,106,.72));transition:color .18s ease,opacity .18s ease}', + '.cl-live-svg{display:block;width:100%;height:100%}', + '.cl-live-line{fill:none;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}', + '.cl-live-area{opacity:.16}', + '.cl-live-foot{font-size:11px;font-weight:500;color:var(--cl-live-foot-color,rgba(74,86,106,.72));transition:color .18s ease,opacity .18s ease}', + '.cl-live-card.is-active .cl-live-num{color:var(--cl-live-main-color,rgba(84,96,116,.92))!important}', + '.cl-live-card.is-zero .cl-live-num{font-size:16px;color:var(--cl-live-zero-color)!important;opacity:1;text-shadow:none}', + '.cl-live-card.is-zero .cl-live-unit,.cl-live-card.is-zero .cl-live-foot{color:var(--cl-live-zero-color)!important;opacity:1}', + '.cl-live-card.is-zero .cl-live-bg-chart{opacity:0!important}', + '.cl-live-card.is-zero .cl-live-line,.cl-live-card.is-zero .cl-live-area{stroke:transparent!important;fill:transparent!important}', + '.cl-card-access .val{font-size:11px;font-weight:500}', + '.cl-check-modern{display:flex;flex-direction:column;gap:10px}', + '.cl-check-group{display:flex;flex-direction:column;gap:6px}', + '.cl-check-group-title{font-size:11px;line-height:1;opacity:.55;font-weight:600;letter-spacing:.2px}', + '.cl-check-item{display:flex;align-items:center;justify-content:flex-start}', + '.cl-check-main{display:flex;align-items:center;gap:10px;min-width:0}', + '.cl-check-icon-wrap{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;background:transparent!important;border:0!important;border-radius:0!important;box-shadow:none!important;padding:0!important;overflow:visible}', + '.cl-check-icon{width:18px;height:18px;object-fit:contain;display:block;flex-shrink:0;background:transparent!important;border:0!important;box-shadow:none!important;border-radius:0!important;padding:0!important;outline:0!important}', + '.cl-check-item img,.cl-check-main img{background:transparent!important;border:0!important;box-shadow:none!important;border-radius:0!important;padding:0!important;outline:0!important}', + '.cl-check-icon-fallback{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;font-size:10px;font-weight:700;background:rgba(31,157,85,.12);color:#1f9d55;flex-shrink:0}', + '.cl-check-name{font-size:11px;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100px;min-width:100px;max-width:100px;flex:0 0 100px}', + '.cl-check-latency{margin-left:20px;width:78px;min-width:78px;text-align:left;font-size:11px;font-weight:600;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-variant-numeric:tabular-nums}', + '.cl-lat-green{color:#2aa85b}', + '.cl-lat-warn{color:#d39e00}', + '.cl-lat-red{color:#c96a5a}', + '.cl-signal{display:inline-flex;align-items:flex-end;justify-content:flex-start;gap:2px;width:30px;min-width:30px;margin-left:8px}', + '.cl-signal i{display:block;width:4px;border-radius:2px;background:rgba(120,120,120,.25)}', + '.cl-signal i:nth-child(1){height:6px}', + '.cl-signal i:nth-child(2){height:9px}', + '.cl-signal i:nth-child(3){height:12px}', + '.cl-signal i:nth-child(4){height:15px}', + '.cl-signal-green i.active{background:#2aa85b}', + '.cl-signal-warn i.active{background:#d39e00}', + '.cl-signal-red i.active{background:#c96a5a}', + '.cl-signal-offline{opacity:.78;transform:translateX(3px)}', + '.cl-check-updated{margin-top:6px;font-size:10px;line-height:1.3;opacity:.5}', + '.cl-check-skeleton{display:flex;flex-direction:column;gap:10px}', + '.cl-skel-row{display:flex;align-items:center;gap:10px}', + '.cl-skel-icon{width:18px;height:18px;border-radius:4px;background:rgba(128,128,128,.16)}', + '.cl-skel-line{height:10px;border-radius:999px;flex:1;background:linear-gradient(90deg,rgba(128,128,128,.10) 25%,rgba(128,128,128,.22) 50%,rgba(128,128,128,.10) 75%);background-size:220% 100%;animation:cl-shimmer 1.3s linear infinite}', + '.cl-skel-line.short{max-width:90px}', + '.cl-check-empty{font-size:12px;opacity:.55}', + '.cl-wrap .cbi-input-text,.cl-wrap .cbi-input-select,.cl-wrap select,.cl-wrap input,.cl-wrap textarea,.cl-wrap .btn,.cl-wrap .cbi-button{font-size:13px !important;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif !important}', + '.cl-wrap .btn,.cl-wrap .cbi-button{padding:4px 10px;line-height:1.35}', + '@keyframes cl-fadein{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:translateY(0)}}', + '@keyframes cl-spin{to{transform:rotate(360deg)}}', + '@keyframes cl-shimmer{0%{background-position:220% 0}100%{background-position:-220% 0}}', + '.cl-op-msg{font-size:12px;font-weight:500;opacity:.85;animation:cl-fadein .25s ease}', + /* 简易节点面板(模态浮层)*/ + '.cl-pm-open{display:inline-flex;align-items:center;height:24px;line-height:1;padding:0;border:0 !important;background:transparent !important;box-shadow:none !important;font-size:12px;font-weight:500;color:var(--cl-primary,rgba(0,122,255,.9));cursor:pointer;white-space:nowrap}', + '.cl-pm-open:hover{opacity:.7}', + '#cl-core-switch-inline-wrap{display:flex;align-items:center;gap:14px;flex-wrap:wrap}', + '.cl-pm-mask{display:none;position:fixed;inset:0;background:rgba(0,0,0,.52);z-index:99998}', + '.cl-pm-mask.cl-on{display:block}', + '.cl-pm{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:min(900px,calc(100vw - 28px));max-height:calc(100vh - 56px);overflow:auto;background:#fff;color:#2a3142;border:1px solid rgba(0,0,0,.08);border-radius:12px;box-shadow:0 22px 60px rgba(0,0,0,.38);z-index:99999;padding:0 22px 18px;opacity:1}', + '.cl-pm.cl-on{display:block}', + '.cl-pm-mask.cl-theme-dark{background:rgba(0,0,0,.66)}', + '.cl-pm.cl-theme-dark{background:#1f242c;color:#e4ecf8;border-color:rgba(255,255,255,.12);box-shadow:0 22px 68px rgba(0,0,0,.62)}', + '.cl-pm-head{position:sticky;top:0;background:inherit;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:15px 0 11px;border-bottom:1px solid rgba(128,128,128,.16)}', + '.cl-pm.cl-theme-dark .cl-pm-head{border-bottom-color:rgba(255,255,255,.12)}', + '.cl-pm-ttl{font-size:16px;font-weight:600;display:flex;align-items:baseline;gap:10px}', + '.cl-pm-st{font-size:12px;font-weight:500}', + '.cl-pm-acts{display:flex;gap:8px}', + '.cl-pm-close{font-size:12px;padding:5px 14px;border-radius:7px;border:1px solid rgba(128,128,128,.3);background:transparent;color:inherit;cursor:pointer}', + '.cl-pm.cl-theme-dark .cl-pm-close,.cl-pm.cl-theme-dark .cl-pm-test{background:#252a33;color:#e4ecf8;border-color:rgba(255,255,255,.24)}', + '.cl-pm-body{display:flex;flex-direction:column;gap:9px;padding-top:12px}', + '.cl-pm-empty{font-size:13px;opacity:.6;padding:34px 4px;text-align:center}', + '.cl-pm-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}', + '.cl-pm-name{flex:0 0 170px;text-align:right;font-size:13px;font-weight:500;display:flex;justify-content:flex-end;align-items:center;gap:6px;word-break:break-all}', + '.cl-pm-badge{font-size:10px;font-weight:600;padding:1px 6px;border-radius:8px;background:rgba(128,128,128,.16);opacity:.72;flex:none}', + '.cl-pm.cl-theme-dark .cl-pm-badge{background:rgba(255,255,255,.1);color:rgba(228,236,248,.82)}', + '.cl-pm-sel{flex:1;min-width:190px;font-size:13px;padding:7px 10px;border:1px solid rgba(128,128,128,.3);border-radius:8px;background:transparent;color:inherit}', + '.cl-pm-now{flex:1;min-width:190px;font-size:13px;padding:7px 10px;border:1px dashed rgba(128,128,128,.32);border-radius:8px;opacity:.78}', + '.cl-pm.cl-theme-dark .cl-pm-sel,.cl-pm.cl-theme-dark .cl-pm-now{background:#252a33;color:#e4ecf8;border-color:rgba(255,255,255,.22)}', + '.cl-pm.cl-theme-dark .cl-pm-sel option{background:#252a33;color:#e4ecf8}', + '.cl-pm-test{font-size:12px;padding:6px 13px;border-radius:7px;border:1px solid rgba(128,128,128,.3);background:transparent;color:inherit;cursor:pointer;flex:none}', + '.cl-pm-test[disabled]{opacity:.5;cursor:default}', + '.cl-dly{font-size:11px;font-weight:600}', + '.cl-dly-g{color:#2e9b51}', + '.cl-dly-y{color:#c98a16}', + '.cl-dly-r{color:#d65c4a}', + '.cl-dly-x{opacity:.4;font-weight:400}', + '@media(max-width:900px){.cl-cards{grid-template-columns:repeat(2,1fr)}.cl-controls{grid-template-columns:repeat(2,1fr)}}', + '@media(max-width:640px){.cl-live-grid{grid-template-columns:1fr}}', + '@media(max-width:480px){.cl-cards{grid-template-columns:1fr}.cl-controls{grid-template-columns:1fr}.cl-live-grid{grid-template-columns:1fr}}' +].join(''); + +var callUpdateCurrentSubscription = rpc.declare({ object: 'luci.clashoo', method: 'update_current_subscription', expect: {} }); +var callOverview = rpc.declare({ object: 'luci.clashoo', method: 'overview', expect: {} }); +var callProxiesList = rpc.declare({ object: 'luci.clashoo', method: 'proxies_list', expect: {} }); +var callProxySelect = rpc.declare({ object: 'luci.clashoo', method: 'proxy_select', params: ['group', 'name'], expect: {} }); +var callProxyDelay = rpc.declare({ object: 'luci.clashoo', method: 'proxy_delay', params: ['group'], expect: {} }); +function fastResolve(promise, timeoutMs, fallback) { + var t = new Promise(function (resolve) { + setTimeout(function () { resolve(fallback); }, timeoutMs); + }); + return Promise.race([L.resolveDefault(promise, fallback), t]); +} + +function rpcOverview() { + var fallback = { + status: {}, + stats: {}, + configs: { configs: [], current: '', core_type: '' }, + access: {} + }; + return L.resolveDefault(callOverview(), fallback); +} + +/* Progress message sequences — displayed step-by-step during async operations */ +var MSGS = { + mihomo: { + start: [ + _("Starting client"), + _("Checking configuration file"), + _("Setting mihomo network rules"), + _("Setting DNS forwarder / enabling custom DNS"), + _("Setting Cron scheduled tasks and starting process supervisor..."), + _("Restarting Dnsmasq"), + _("mihomo started successfully, please wait for servers to come online.") + ], + stop: [ + _("Stopping client..."), + _("Cleaning mihomo network rules"), + _("Disabling DNS cache"), + _("Stopping mihomo process supervisor"), + _("Removing Cron scheduled tasks"), + _("Restarting Dnsmasq") + ], + restart: [ + _("Restarting client..."), + _("Cleaning mihomo network rules"), + _("Stopping mihomo"), + _("Starting client"), + _("Checking configuration file"), + _("Setting mihomo network rules"), + _("Restarting Dnsmasq"), + _("mihomo restarted successfully, please wait for servers to come online.") + ] + }, + singbox: { + start: [ + _("Starting client"), + _("Checking configuration file"), + _("Setting sing-box network rules"), + _("Setting DNS forwarder / enabling custom DNS"), + _("Setting Cron scheduled tasks and starting process supervisor..."), + _("Restarting Dnsmasq"), + _("sing-box started successfully, please wait for servers to come online.") + ], + stop: [ + _("Stopping client..."), + _("Cleaning sing-box network rules"), + _("Disabling DNS cache"), + _("Stopping sing-box process supervisor"), + _("Removing Cron scheduled tasks"), + _("Restarting Dnsmasq") + ], + restart: [ + _("Restarting client..."), + _("Cleaning sing-box network rules"), + _("Stopping sing-box"), + _("Starting client"), + _("Checking configuration file"), + _("Setting sing-box network rules"), + _("Restarting Dnsmasq"), + _("sing-box restarted successfully, please wait for servers to come online.") + ] + } +}; + +// 全局锁:内核操作互斥(切换配置 / 更新订阅 / 删除) +function clKernelBusy() { return !!document.body.getAttribute('data-cl-kernel-busy'); } +function clKernelLock() { + document.body.setAttribute('data-cl-kernel-busy', '1'); + document.querySelectorAll('.cl-btn-switch, .cl-btn-update-sub, .cl-btn-delete, .cl-kernel-sel').forEach(function (b) { b.disabled = true; }); +} +function clKernelUnlock() { + document.body.removeAttribute('data-cl-kernel-busy'); + document.querySelectorAll('.cl-btn-switch, .cl-btn-update-sub, .cl-btn-delete, .cl-kernel-sel').forEach(function (b) { b.disabled = false; }); +} + +return view.extend({ + _busy: false, + _op: null, /* 'start' | 'stop' | 'restart' | null */ + _opTimers: null, /* array of setTimeout IDs for cleanup */ + _accessRefreshing: false, + _coreSwitchBusy: false, + _coreSwitchMsg: '', + _rtHistory: null, + + _lastAc: null, + _lastSt: null, + _lastCfgData: null, + + load: function () { + return Promise.resolve(null); + }, + + _overviewCacheKey: 'clashoo.overview.last', + + _readCachedOverview: function () { + try { + if (!window.sessionStorage) + return null; + var raw = window.sessionStorage.getItem(this._overviewCacheKey); + if (!raw) + return null; + var cached = JSON.parse(raw); + if (!cached || !cached._ts || (Date.now() - cached._ts) > 60000) + return null; + if (!cached.status || typeof cached.status.running !== 'boolean') + return null; + return cached; + } catch (e) { + return null; + } + }, + + _writeCachedOverview: function (ov) { + try { + if (!window.sessionStorage || !ov || !ov.status || typeof ov.status.running !== 'boolean') + return; + var copy = JSON.parse(JSON.stringify(ov)); + copy._ts = Date.now(); + window.sessionStorage.setItem(this._overviewCacheKey, JSON.stringify(copy)); + } catch (e) {} + }, + + _initialOverview: function () { + var core = uci.get('clashoo', 'config', 'core_type') || 'mihomo'; + var useConfig = uci.get('clashoo', 'config', 'use_config') || ''; + var singboxActive = uci.get('clashoo', 'config', 'singbox_active') || ''; + var slash = useConfig.lastIndexOf('/'); + var configName = slash >= 0 ? useConfig.slice(slash + 1) : useConfig; + if (core === 'singbox' && singboxActive) + configName = singboxActive; + + return { + status: { + loading: true, + running: null, + core_type: core, + dash_port: uci.get('clashoo', 'config', 'dash_port') || '9090', + dash_pass: uci.get('clashoo', 'config', 'dash_pass') || '', + panel_type: uci.get('clashoo', 'config', 'dashboard_panel') || 'zashboard', + proxy_mode: uci.get('clashoo', 'config', 'p_mode') || 'rule', + tcp_mode: uci.get('clashoo', 'config', 'tcp_mode') || 'tun', + udp_mode: uci.get('clashoo', 'config', 'udp_mode') || (uci.get('clashoo', 'config', 'tcp_mode') || 'tun'), + stack: uci.get('clashoo', 'config', 'stack') || 'system', + config: configName, + local_ip: location.hostname, + health_status: 'loading' + }, + stats: {}, + configs: { + configs: configName ? [configName] : [], + current: configName, + core_type: core + }, + access: { + updated_at: '0', + updating: true, + items: [] + } + }; + }, + + render: function (data) { + var ov = this._readCachedOverview() || this._initialOverview(); + var st = ov.status || {}; + var cfgData = ov.configs || { configs: [], current: '' }; + var ac = ov.access || {}; + var stats = ov.stats || {}; + + if (!document.getElementById('cl-css')) { + var s = document.createElement('style'); + s.id = 'cl-css'; s.textContent = CSS; + document.head.appendChild(s); + } + if (!document.getElementById('cl-css-ext')) { + var link = document.createElement('link'); + link.id = 'cl-css-ext'; + link.rel = 'stylesheet'; + link.href = L.resource('view/clashoo/clashoo.css') + '?v=20260806a1'; + document.head.appendChild(link); + } else { + document.getElementById('cl-css-ext').href = L.resource('view/clashoo/clashoo.css') + '?v=20260806a1'; + } + + this._lastSt = st; + this._lastCfgData = cfgData; + this._lastAc = ac; + this._overviewLoaded = false; + + var root = E('div', { 'class': 'cl-wrap clashoo-container cl-overview-page' }, [ + E('div', { 'class': 'cl-cards', id: 'cl-cards' }, this._cards(st, cfgData, ac)), + E('div', { 'class': 'cl-traffic-wrap', id: 'cl-traffic-wrap' }, [ + this._card(_("Traffic Monitor"), this._renderRealtimePanel(), 'cl-card-traffic') + ]), + E('div', { 'class': 'cl-controls', id: 'cl-controls' }, this._controls(st, cfgData)), + this._renderProxyModal() + ]); + this._rootEl = root; + this._applyThemeClass(); + this._observeThemeChanges(); + this._updateRealtimePanel(stats); + this._loadUciInBackground(); + + poll.add(L.bind(this._pollOverview, this), 5); + this._pollOverview(); + + return root; + }, + + _loadUciInBackground: function () { + var self = this; + setTimeout(function () { + L.resolveDefault(uci.load('clashoo'), null).then(function () { + if (self._overviewLoaded || !self._rootEl) + return; + var ov = self._initialOverview(); + self._lastCfgData = ov.configs || { configs: [], current: '' }; + + var controls = document.getElementById('cl-controls'); + if (controls) { + controls.innerHTML = ''; + self._controls(self._lastSt || ov.status || {}, self._lastCfgData).forEach(function (ctrl) { + controls.appendChild(ctrl); + }); + } + }); + }, 0); + }, + + _applyThemeClass: function () { + if (!this._rootEl || !this._rootEl.classList) + return; + var dark = this._isDarkUi(); + this._rootEl.classList.toggle('cl-theme-dark', !!dark); + this._rootEl.classList.toggle('cl-theme-light', !dark); + }, + + _observeThemeChanges: function () { + var self = this; + if (self._themeObserver) { + try { self._themeObserver.disconnect(); } catch (e0) {} + self._themeObserver = null; + } + if (self._themeMedia && self._themeMediaHandler) { + try { + if (self._themeMedia.removeEventListener) + self._themeMedia.removeEventListener('change', self._themeMediaHandler); + else if (self._themeMedia.removeListener) + self._themeMedia.removeListener(self._themeMediaHandler); + } catch (e00) {} + self._themeMedia = null; + self._themeMediaHandler = null; + } + if (typeof MutationObserver === 'undefined') + return self._applyThemeClass(); + + var obs = new MutationObserver(function (mutations) { + for (var i = 0; i < mutations.length; i++) { + var a = mutations[i] && mutations[i].attributeName; + if (!a) + continue; + if (a === 'class' || a === 'data-theme' || a === 'data-bs-theme' || a === 'data-darkmode') { + self._applyThemeClass(); + break; + } + } + }); + + var opt = { attributes: true, attributeFilter: ['class', 'data-theme', 'data-bs-theme', 'data-darkmode'] }; + try { obs.observe(document.documentElement, opt); } catch (e1) {} + if (document.body) { + try { obs.observe(document.body, opt); } catch (e2) {} + } + self._themeObserver = obs; + + if (window.matchMedia) { + var mq = window.matchMedia('(prefers-color-scheme: dark)'); + var handler = function () { self._applyThemeClass(); }; + try { + if (mq.addEventListener) + mq.addEventListener('change', handler); + else if (mq.addListener) + mq.addListener(handler); + } catch (e3) {} + self._themeMedia = mq; + self._themeMediaHandler = handler; + } + + self._applyThemeClass(); + }, + + _isDarkUi: function () { + var docEl = document.documentElement || {}; + var body = document.body || {}; + var theme = String( + (docEl.getAttribute && (docEl.getAttribute('data-theme') || docEl.getAttribute('data-bs-theme'))) || + (body.getAttribute && (body.getAttribute('data-theme') || body.getAttribute('data-bs-theme'))) || + '' + ).toLowerCase(); + var darkMode = String( + (docEl.getAttribute && docEl.getAttribute('data-darkmode')) || + (body.getAttribute && body.getAttribute('data-darkmode')) || + '' + ).toLowerCase(); + + if (theme === 'dark' || darkMode === '1' || darkMode === 'true') + return true; + if (theme === 'light' || darkMode === '0' || darkMode === 'false') + return false; + + var cls = String((docEl.className || '') + ' ' + (body.className || '')).toLowerCase(); + if (/(^|\s)(dark|theme-dark|darkmode)(\s|$)/.test(cls)) + return true; + if (/(^|\s)(light|theme-light)(\s|$)/.test(cls)) + return false; + + try { + var links = document.querySelectorAll('link[rel="stylesheet"]'); + for (var i = 0; i < links.length; i++) { + var href = String(links[i].href || '').toLowerCase(); + if (href.indexOf('dark.css') >= 0 || href.indexOf('darkmode') >= 0 || href.indexOf('argon-dark') >= 0) + return true; + } + } catch (e0) {} + + /* Fallback: inspect sidebar brightness for themes that don't expose standard attrs */ + try { + var byLuminance = function (node) { + if (!node) + return null; + var bg = window.getComputedStyle(node).backgroundColor || ''; + var rgb = String(bg).match(/\d+/g); + if (rgb && rgb.length >= 3) { + var sum = (parseInt(rgb[0], 10) || 0) + + (parseInt(rgb[1], 10) || 0) + + (parseInt(rgb[2], 10) || 0); + return sum < 380; + } + return null; + }; + + var sidebar = document.querySelector('.main-sidebar, .main-left, .sidebar, #mainmenu, #menubar, .left'); + var darkFromSidebar = byLuminance(sidebar); + if (darkFromSidebar != null) + return darkFromSidebar; + + var darkFromBody = byLuminance(body); + if (darkFromBody != null) + return darkFromBody; + } catch (e) {} + + if (window.matchMedia) + return !!window.matchMedia('(prefers-color-scheme: dark)').matches; + + return false; + }, + + _coreLabel: function (family, channel) { + if (!family) + return _("Not running"); + var f = family === 'singbox' ? 'sing-box' : 'mihomo'; + if (channel !== 'smart' && channel !== 'alpha') + return _(family === 'singbox' ? "sing-box Stable" : "mihomo Stable"); + var c = channel === 'smart' ? _("Smart") : _("Alpha"); + return f + ' ' + c; + }, + + _configuredChannel: function (family, st) { + var dcore = (st && st.dcore) || uci.get('clashoo', 'config', 'dcore') || '2'; + if (family === 'singbox') + return dcore === '5' ? 'alpha' : 'stable'; + if (dcore === '1') return 'smart'; + return dcore === '3' ? 'alpha' : 'stable'; + }, + + _currentCoreType: function (st) { + return (st && st.core_type) || uci.get('clashoo', 'config', 'core_type') || 'mihomo'; + }, + + _effectiveCore: function (st) { + var family = this._currentCoreType(st); + if (family === 'singbox') return 'singbox'; + /* 优先用 ubus status 实时返回的 dcore,避免 uci.get LuCI 缓存切换后滞后 */ + var dcore = (st && st.dcore) || uci.get('clashoo', 'config', 'dcore') || '2'; + return dcore === '1' ? 'smart' : 'mihomo'; + }, + + _refreshCoreSwitch: function () { + var wrap = document.getElementById('cl-core-switch-inline-wrap'); + if (!wrap) return; + wrap.innerHTML = ''; + wrap.appendChild(this._renderCoreSwitch(this._lastSt || {})); + wrap.appendChild(this._proxyTriggerBtn()); + }, + + _showCoreSwitchHint: function (msg) { + var self = this; + self._coreSwitchMsg = msg; + self._refreshCoreSwitch(); + setTimeout(function () { + if (self._coreSwitchMsg === msg) { + self._coreSwitchMsg = ''; + self._refreshCoreSwitch(); + } + }, 3500); + }, + + _switchCore: function (targetCore) { + var self = this; + if (self._coreSwitchBusy) + return Promise.resolve(); + + if (targetCore === 'smart' && self._lastSt && self._lastSt.has_smart === false) { + self._showCoreSwitchHint(_("Smart core not detected; updating the model will not install the core. Download the Smart build from the System page.")); + return Promise.resolve(); + } + + var currentEffective = self._effectiveCore(self._lastSt || {}); + if (targetCore === currentEffective) + return Promise.resolve(); + + var currentDcore = (self._lastSt && self._lastSt.dcore) || uci.get('clashoo', 'config', 'dcore') || '2'; + var rpcCore, nextDcore, targetLabel; + if (targetCore === 'smart') { + rpcCore = 'mihomo'; nextDcore = '1'; targetLabel = 'Smart'; + } else if (targetCore === 'singbox') { + rpcCore = 'singbox'; + if (currentDcore === '5' && self._lastSt.has_singbox_alpha) + nextDcore = '5'; + else if (currentDcore === '4' && self._lastSt.has_singbox_stable) + nextDcore = '4'; + else + nextDcore = self._lastSt.has_singbox_stable ? '4' : '5'; + targetLabel = 'sing-box'; + } else { + rpcCore = 'mihomo'; + if (currentDcore === '3' && self._lastSt.has_mihomo_alpha) + nextDcore = '3'; + else if (currentDcore === '2' && self._lastSt.has_mihomo_stable) + nextDcore = '2'; + else + nextDcore = self._lastSt.has_mihomo_stable ? '2' : '3'; + targetLabel = 'mihomo'; + } + + self._coreSwitchBusy = true; + self._coreSwitchMsg = _("Switching to ") + targetLabel + '...'; + self._refreshCoreSwitch(); + + return clashoo.setCore(rpcCore, nextDcore, 'apply') + .then(function (r) { + if (r && r.success === false && r.error === 'smart_core_missing') { + self._showCoreSwitchHint(_("Smart core not detected; updating the model will not install the core. Download the Smart build from the System page.")); + throw { soft: true }; + } + if (r && r.success === false && r.error === 'singbox_core_missing') { + self._showCoreSwitchHint(_("sing-box core not detected; download sing-box from the System page")); + throw { soft: true }; + } + if (r && r.success === false) + throw new Error(r.message || _("Core switch failed")); + self._lastSt = self._lastSt || {}; + self._lastSt.core_type = rpcCore; + self._lastSt.dcore = nextDcore; /* 乐观更新,避免重绘前 uci 缓存滞后 */ + if (!r || !r.restarting) { + self._lastSt.running = false; + self._lastSt.health_status = 'stopped'; + } + return r; + }) + .then(function (r) { + /* set_core auto-enables smart_auto_switch; init.d at boot + * smart_inject 会把 url-test/load-balance 转成 Smart 策略,无需手动操作。 */ + if (!r || !r.restarting) + self._coreSwitchMsg = _("Switched to ") + targetLabel + _(", click Start to take effect"); + self._refreshCoreSwitch(); + return new Promise(function (resolve) { setTimeout(resolve, 500); }); + }) + .then(function () { return self._pollStatus(); }) + .catch(function (e) { + if (e && e.soft) + return; + /* card already surfaces this via _coreSwitchMsg; no toast needed */ + self._coreSwitchMsg = _("Switch failed: ") + (e.message || e); + self._refreshCoreSwitch(); + }) + .then(function () { + self._coreSwitchBusy = false; + setTimeout(function () { + self._coreSwitchMsg = ''; + self._refreshCoreSwitch(); + }, 900); + }); + }, + + /* Inline panel-update feedback: button shows progress, status span shows result. + Replaces the old fire-and-forget toast so success/failure is visible in place. */ + _runPanelUpdate: function (panel, btn, statusEl) { + var self = this; + if (btn.disabled) return; + var label = self._panelLabel(panel); + var orig = btn.textContent; + self._panelUpdateBusy = true; /* pause overview poll so the rebuild won't wipe this feedback */ + btn.disabled = true; + btn.textContent = _("Downloading…"); + statusEl.textContent = ''; + statusEl.className = 'cl-panel-status'; + + var done = function (cls, text) { + self._panelUpdateBusy = false; + btn.disabled = false; + btn.textContent = orig; + statusEl.textContent = text; + statusEl.className = 'cl-panel-status ' + cls; + setTimeout(function () { + statusEl.textContent = ''; + statusEl.className = 'cl-panel-status'; + }, 6000); + }; + + var polls = 0; + var poll = function () { + clashoo.panelStatus().then(function (s) { + s = s || {}; + if (s.state === 'success') done('ok', '✓ ' + label + _(" updated")); + else if (s.state === 'error') done('err', '✗ ' + (s.msg || _("Update failed"))); + else if (polls++ >= 600) done('err', _("✗ Panel update timeout")); + else { + statusEl.textContent = s.msg || _("Downloading…"); + setTimeout(poll, 1200); + } + }); + }; + + clashoo.updatePanel(panel) + .then(function (r) { + if (r && r.busy) { + statusEl.textContent = r.message || _("Panel is downloading"); + setTimeout(poll, 600); + return; + } + if (!r || r.success === false) { + done('err', '✗ ' + ((r && r.message) || _("Submission failed"))); + return; + } + setTimeout(poll, 600); + }) + .catch(function () { done('err', _("✗ Submission failed")); }); + }, + + _renderCoreSwitch: function (st) { + var self = this; + var effective = this._effectiveCore(st); + var note = this._coreSwitchMsg || ''; + + var mkBtn = function (core, label) { + var active = core === effective; + var missingSmart = core === 'smart' && st && st.has_smart === false; + return E('button', { + type: 'button', + title: missingSmart ? _("Smart core not detected; updating the model will not install the core") : null, + 'class': 'cl-core-btn' + (active ? ' active' : ''), + disabled: self._coreSwitchBusy ? '' : null, + click: function (ev) { + ev.preventDefault(); + self._switchCore(core); + } + }, label); + }; + + var children = [ + E('div', { + 'class': 'cl-status-kernel' + (this._coreSwitchBusy ? ' is-busy' : ''), + title: note || null + }, [ + mkBtn('mihomo', 'Mihomo'), + mkBtn('smart', 'Smart'), + mkBtn('singbox', 'Sing-box') + ]) + ]; + if (note) + children.push(E('div', { 'class': 'cl-core-switch-hint' }, note)); + + return E('div', { 'class': 'cl-core-switch-wrap' }, children); + }, + + _proxyModeLabel: function (mode) { + var map = { rule: _("Rule"), global: _("Global"), direct: _("Direct") }; + return map[mode] || mode || '—'; + }, + + _panelLabel: function (name) { + var map = { + metacubexd: 'MetaCubeXD', + yacd: 'YACD', + zashboard: 'Zashboard', + razord: 'Razord' + }; + return map[name] || name || _("Panel"); + }, + + _renderServiceSwitch: function (st) { + var self = this; + var known = st && typeof st.running === 'boolean'; + var running = known && st.running === true; + var disabled = this._busy || !known || this._coreSwitchBusy; + return E('div', { 'class': 'cl-service-tools' }, [ + E('span', { 'class': 'cl-service-label' }, _("Enable Service")), + E('button', { + type: 'button', + title: !known ? _("Syncing runtime state") : (running ? _("Stop Service") : _("Start Service")), + 'aria-pressed': running ? 'true' : 'false', + 'class': 'cl-service-switch' + (running ? ' is-on' : ' is-off'), + disabled: disabled ? '' : null, + click: function (ev) { + ev.preventDefault(); + if (disabled) return; + return running ? self._stop() : self._start(); + } + }, [E('span', { 'class': 'cl-service-knob' })]) + ]); + }, + + _cards: function (st, cfgData, ac) { + var statusKnown = st && typeof st.running === 'boolean'; + var running = statusKnown && st.running === true; + var health = st.health_status || 'unknown'; + var configuredCoreLabel = this._coreLabel(st.core_type, this._configuredChannel(st.core_type, st)); + + var statusChildren = [ + !statusKnown + ? E('span', { 'class': 'cl-status-state cl-status-state-sync' }, _("Syncing")) + : running + ? E('span', { 'class': 'cl-status-state cl-status-state-run' }, _("Running 🟢")) + : E('span', { 'class': 'cl-status-state cl-status-state-stop' }, _("Stopped") + ' ⚪'), + E('span', { 'class': 'cl-status-note cl-status-note-core' }, configuredCoreLabel) + ]; + if (st.runtime_degraded) + statusChildren.push(E('span', { 'class': 'cl-status-note cl-status-note-degraded' }, _("Running degraded"))); + if (running && health === 'fail') + statusChildren.push(E('span', { 'class': 'cl-status-note cl-status-note-fail' }, _("Health check failed"))); + else if (running && health === 'pass') + statusChildren.push(E('span', { 'class': 'cl-status-note cl-status-note-pass' }, _("Health check passed"))); + var statusEl = E('span', { id: 'cl-status-val' }, statusChildren); + + return [ + this._card(_("Runtime Status"), statusEl, 'cl-card-status', + this._renderServiceSwitch(st)), + this._card(_("Core Switch"), + E('div', { id: 'cl-core-switch-inline-wrap' }, [ + this._renderCoreSwitch(st), + this._proxyTriggerBtn() + ]), + 'cl-card-kernel' + ), + this._card(_("Transparent Proxy"), this._renderTpMode(st), 'cl-card-mode'), + this._card(_("Access Check"), this._renderCheckStatus(st, ac), 'cl-card-access', this._renderAccessRefresh(ac)) + ]; + }, + + /* Directly update the status card content without rebuilding all cards */ + _showOpMsg: function (msg) { + var el = document.getElementById('cl-status-val'); + if (!el) return; + el.innerHTML = ''; + el.appendChild(E('span', { 'class': 'cl-op-msg' }, msg)); + }, + + /* Schedule message animation: space messages evenly over (totalMs - tailMs) */ + _startMsgAnim: function (messages, totalMs) { + var self = this; + var tailMs = 2500; /* last message stays visible for tailMs before poll */ + var spanMs = Math.max(totalMs - tailMs, 1000); + var stepMs = messages.length > 1 ? Math.floor(spanMs / (messages.length - 1)) : spanMs; + self._opTimers = []; + messages.forEach(function (msg, i) { + var t = setTimeout(function () { + self._showOpMsg(msg); + }, i * stepMs); + self._opTimers.push(t); + }); + }, + + _clearOpTimers: function () { + if (this._opTimers) { + this._opTimers.forEach(function (t) { clearTimeout(t); }); + this._opTimers = null; + } + }, + + _tpModeName: function (mode) { + var m = String(mode || '').toLowerCase(); + var map = { + redirect: 'Redirect', + redir: 'Redirect', + tproxy: 'TProxy', + tun: 'TUN', + off: _("Off") + }; + if (map[m]) return map[m]; + if (!mode) return '—'; + return String(mode); + }, + + _stackLabel: function (stack) { + var s = String(stack || '').toLowerCase(); + var map = { gvisor: 'gVisor', system: 'System', mixed: 'Mixed' }; + return map[s] || (stack ? String(stack) : 'System'); + }, + + _tpModeRows: function (st) { + var tcpRaw = st.tcp_node || st.effective_tcp_mode || st.tcp_mode || uci.get('clashoo', 'config', 'tcp_mode') || 'off'; + var udpRaw = st.udp_node || st.effective_udp_mode || st.udp_mode || uci.get('clashoo', 'config', 'udp_mode') || tcpRaw; + var stackRaw = st.stack_type || st.stack || uci.get('clashoo', 'config', 'stack') || 'system'; + var tunEnabled = String(tcpRaw).toLowerCase() === 'tun' || String(udpRaw).toLowerCase() === 'tun'; + + return { + rows: [ + { proto: 'TCP', mode: this._tpModeName(tcpRaw), suffix: String(tcpRaw).toLowerCase() === 'off' ? '' : _("Mode") }, + { proto: 'UDP', mode: this._tpModeName(udpRaw), suffix: String(udpRaw).toLowerCase() === 'off' ? '' : _("Mode") }, + { proto: _("Network Stack"), mode: tunEnabled ? this._stackLabel(stackRaw) : _("Not enabled"), suffix: tunEnabled ? _("Mode") : '' } + ] + }; + }, + + _isTpModeDegraded: function (st) { + if (!st) + return false; + + if (st.runtime_degraded) + return true; + + var tcp = String(st.tcp_mode || uci.get('clashoo', 'config', 'tcp_mode') || '').toLowerCase(); + var udp = String(st.udp_mode || uci.get('clashoo', 'config', 'udp_mode') || tcp).toLowerCase(); + var wantsTun = tcp === 'tun' || udp === 'tun'; + return wantsTun && String(st.has_tun_device) === '0'; + }, + + _renderTpMode: function (st) { + var data = this._tpModeRows(st); + var rows = data.rows.map(function (row) { + return E('div', { 'class': 'cl-mode-row' }, [ + E('span', { 'class': 'cl-mode-proto' }, row.proto), + E('span', { 'class': 'cl-mode-name' }, row.mode), + E('span', { 'class': 'cl-mode-suffix' }, row.suffix) + ]); + }); + if (this._isTpModeDegraded(st)) + rows.push(E('div', { 'class': 'cl-check-updated cl-mode-degraded' }, _("Degraded"))); + + return E('div', { id: 'cl-tpmode', 'class': 'cl-mode-stack' }, rows); + }, + + _renderAccessRefresh: function (ac) { + var self = this; + /* 核心未运行时访问检查无意义,强制不转圈,避免守护进程周期探测一直转 */ + var running = !!(this._lastSt && this._lastSt.running); + var loading = running && !!(this._accessRefreshing || (ac && ac.updating)); + return E('button', { + type: 'button', + id: 'cl-access-refresh', + title: _("Refresh Access Check"), + 'class': 'cl-icon-btn' + (loading ? ' is-loading' : ''), + click: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + self._manualRefreshAccess(); + } + }, '↻'); + }, + + _card: function (lbl, val, extraCls, tool) { + var head = [ + E('div', { 'class': 'lbl' }, lbl) + ]; + if (tool) + head.push(E('div', { 'class': 'cl-card-tools' }, [tool])); + + return E('div', { 'class': 'cl-card' + (extraCls ? (' ' + extraCls) : '') }, [ + E('div', { 'class': 'cl-card-head' }, head), + E('div', { 'class': 'val' }, [val]) + ]); + }, + + _toInt: function (v) { + var n = parseInt(v, 10); + return isFinite(n) && n > 0 ? n : 0; + }, + + /* ── 简易节点面板(模态浮层)── */ + _delayBadge: function (ms) { + ms = parseInt(ms, 10) || 0; + if (ms <= 0) return E('span', { 'class': 'cl-dly cl-dly-x' }, '—'); + var cls = ms < 100 ? 'cl-dly-g' : (ms < 300 ? 'cl-dly-y' : 'cl-dly-r'); + return E('span', { 'class': 'cl-dly ' + cls }, ms + 'ms'); + }, + + _pgTypeLabel: function (t) { + var m = { 'Selector': _("Selector"), 'URLTest': _("Url-test"), 'Fallback': _("Fallback"), + 'LoadBalance': _("Load-balance"), 'Relay': _("Relay") }; + return m[t] || t; + }, + + _proxyTriggerBtn: function () { + var self = this; + var btn = E('button', { 'class': 'cl-pm-open' }, _("Node Panel ›")); + btn.addEventListener('click', function () { self._openProxyModal(); }); + return btn; + }, + + _setPmStatus: function (text, tone) { + var el = this._pmStatusEl; + if (!el) return; + el.textContent = text || ''; + el.style.color = tone === 'ok' ? '#2e9b51' : (tone === 'err' ? '#d65c4a' : ''); + }, + + _renderProxyModal: function () { + var self = this; + var statusEl = E('span', { 'class': 'cl-pm-st' }, ''); + var body = E('div', { 'class': 'cl-pm-body', id: 'cl-pm-body' }, []); + this._pmStatusEl = statusEl; + this._pmBodyEl = body; + + var refreshBtn = E('button', { 'class': 'cl-pm-close' }, _("Refresh")); + refreshBtn.addEventListener('click', function () { self._loadProxies(); }); + var closeBtn = E('button', { 'class': 'cl-pm-close' }, _("Off")); + closeBtn.addEventListener('click', function () { self._closeProxyModal(); }); + + var modal = E('div', { 'class': 'cl-pm', id: 'cl-pm' }, [ + E('div', { 'class': 'cl-pm-head' }, [ + E('div', { 'class': 'cl-pm-ttl' }, [E('span', {}, _("Node Selection")), statusEl]), + E('div', { 'class': 'cl-pm-acts' }, [refreshBtn, closeBtn]) + ]), + body + ]); + modal.addEventListener('click', function (ev) { ev.stopPropagation(); }); + + var mask = E('div', { 'class': 'cl-pm-mask', id: 'cl-pm-mask' }, [modal]); + mask.addEventListener('click', function () { self._closeProxyModal(); }); + this._pmMask = mask; + this._pmModal = modal; + return mask; + }, + + _openProxyModal: function () { + var self = this; + if (!this._pmMask) return; + if (this._pmMask.parentNode !== document.body) + document.body.appendChild(this._pmMask); + var dark = this._isDarkUi(); + this._pmMask.classList.toggle('cl-theme-dark', !!dark); + this._pmModal.classList.toggle('cl-theme-dark', !!dark); + this._pmMask.classList.add('cl-on'); + this._pmModal.classList.add('cl-on'); + if (!this._pmEscBound) { + this._pmEscHandler = function (ev) { if (ev.key === 'Escape') self._closeProxyModal(); }; + document.addEventListener('keydown', this._pmEscHandler); + this._pmEscBound = true; + } + this._loadProxies(); + }, + + _closeProxyModal: function () { + if (this._pmMask) this._pmMask.classList.remove('cl-on'); + if (this._pmModal) this._pmModal.classList.remove('cl-on'); + if (this._pmEscBound) { + document.removeEventListener('keydown', this._pmEscHandler); + this._pmEscBound = false; + } + }, + + _loadProxies: function () { + var self = this; + var body = this._pmBodyEl; + if (!body) return; + var st = this._lastSt || {}; + if (!st.running) { + body.innerHTML = ''; + body.appendChild(E('div', { 'class': 'cl-pm-empty' }, _("Core is not running. Start it before switching nodes."))); + this._setPmStatus(_("Not running"), 'err'); + return; + } + body.innerHTML = ''; + body.appendChild(E('div', { 'class': 'cl-pm-empty' }, _("Loading…"))); + this._setPmStatus(_("Loading…"), ''); + callProxiesList().then(function (r) { + var b = self._pmBodyEl; + if (!b) return; + b.innerHTML = ''; + if (!r || !r.ok) { + b.appendChild(E('div', { 'class': 'cl-pm-empty' }, _("Cannot connect to core API. Check runtime status."))); + self._setPmStatus(_("Connection failed"), 'err'); + return; + } + var groups = r.groups || []; + if (!groups.length) { + b.appendChild(E('div', { 'class': 'cl-pm-empty' }, _("Current configuration has no manually switchable proxy groups"))); + self._setPmStatus(_("No proxy groups"), ''); + return; + } + groups.forEach(function (g) { b.appendChild(self._renderProxyGroup(g)); }); + self._setPmStatus(_("Ready · ") + groups.length + _(" groups"), 'ok'); + }).catch(function () { + var b = self._pmBodyEl; + if (b) { b.innerHTML = ''; b.appendChild(E('div', { 'class': 'cl-pm-empty' }, _("Load failed"))); } + self._setPmStatus(_("Load failed"), 'err'); + }); + }, + + _doProxySelect: function (group, name, onOk, onFail) { + callProxySelect(group, name).then(function (r) { + if (r && r.ok) { if (onOk) onOk(); } + else { + ui.addNotification(null, E('p', group + _(" switch failed: ") + ((r && r.message) || ''))); + if (onFail) onFail(); + } + }).catch(function () { + ui.addNotification(null, E('p', group + _(" switch error"))); + if (onFail) onFail(); + }); + }, + + _renderProxyGroup: function (g) { + var self = this; + var row = E('div', { 'class': 'cl-pm-row' }, [ + E('div', { 'class': 'cl-pm-name' }, [ + E('span', {}, g.name), + E('span', { 'class': 'cl-pm-badge' }, this._pgTypeLabel(g.type)) + ]) + ]); + + if (g.selectable) { + var members = g.members || []; + var prevVal = g.now; + var sel = E('select', { 'class': 'cl-pm-sel' }, + members.map(function (m) { + var label = m.name + (m.delay > 0 ? ' · ' + m.delay + 'ms' : ''); + return E('option', { value: m.name, selected: (m.name === g.now) ? '' : null }, label); + }) + ); + sel.addEventListener('change', function () { + var picked = sel.value; + sel.disabled = true; + self._doProxySelect(g.name, picked, + function () { prevVal = picked; sel.disabled = false; }, + function () { sel.value = prevVal; sel.disabled = false; }); + }); + row.appendChild(sel); + } else { + var dmap = {}; + (g.members || []).forEach(function (m) { dmap[m.name] = m.delay || 0; }); + row.appendChild(E('div', { 'class': 'cl-pm-now' }, [ + E('span', {}, (g.now || '—') + ' '), + this._delayBadge(dmap[g.now]) + ])); + } + + var testBtn = E('button', { 'class': 'cl-pm-test' }, _("Test Latency")); + testBtn.addEventListener('click', function () { + testBtn.disabled = true; + testBtn.textContent = _("Testing"); + callProxyDelay(g.name).then(function (r) { + testBtn.disabled = false; + testBtn.textContent = _("Test Latency"); + if (!r || !r.ok) { + ui.addNotification(null, E('p', g.name + _(" latency test failed: ") + ((r && r.message) || ''))); + return; + } + var fresh = { + name: g.name, type: g.type, now: g.now, selectable: g.selectable, + members: (g.members || []).map(function (m) { + return { name: m.name, delay: (r.delays && r.delays[m.name]) || m.delay || 0 }; + }) + }; + var newRow = self._renderProxyGroup(fresh); + if (row.parentNode) row.parentNode.replaceChild(newRow, row); + }).catch(function () { + testBtn.disabled = false; + testBtn.textContent = _("Test Latency"); + }); + }); + row.appendChild(testBtn); + return row; + }, + + _fmtRate: function (bytes) { + var b = this._toInt(bytes); + if (b >= 1024 * 1024) + return { value: (b / (1024 * 1024)).toFixed(2), unit: 'MB/s' }; + if (b >= 1024) + return { value: (b / 1024).toFixed(2), unit: 'kB/s' }; + return { value: '' + b, unit: 'B/s' }; + }, + + _fmtBytes: function (bytes) { + var b = this._toInt(bytes); + if (b >= 1024 * 1024 * 1024) + return (b / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; + if (b >= 1024 * 1024) + return (b / (1024 * 1024)).toFixed(0) + ' MB'; + if (b >= 1024) + return (b / 1024).toFixed(0) + ' KB'; + return b + ' B'; + }, + + _fmtMiB: function (bytes) { + var b = this._toInt(bytes); + return (b / (1024 * 1024)).toFixed(0) + ' MiB'; + }, + + _initRealtimeHistory: function () { + if (!this._rtHistory) { + this._rtHistory = { up: [], down: [], conn: [] }; + } + }, + + _pushRealtimeSample: function (key, value) { + this._initRealtimeHistory(); + var hist = this._rtHistory[key] || (this._rtHistory[key] = []); + hist.push(this._toInt(value)); + if (hist.length > 36) + hist.shift(); + return hist; + }, + + _smoothPath: function (pts) { + if (!pts || !pts.length) + return ''; + if (pts.length === 1) + return 'M ' + pts[0].x.toFixed(1) + ' ' + pts[0].y.toFixed(1); + + var d = 'M ' + pts[0].x.toFixed(1) + ' ' + pts[0].y.toFixed(1); + for (var i = 0; i < pts.length - 1; i++) { + var p0 = i > 0 ? pts[i - 1] : pts[i]; + var p1 = pts[i]; + var p2 = pts[i + 1]; + var p3 = (i !== pts.length - 2) ? pts[i + 2] : p2; + + var cp1x = p1.x + (p2.x - p0.x) / 6; + var cp1y = p1.y + (p2.y - p0.y) / 6; + var cp2x = p2.x - (p3.x - p1.x) / 6; + var cp2y = p2.y - (p3.y - p1.y) / 6; + + d += ' C ' + + cp1x.toFixed(1) + ' ' + cp1y.toFixed(1) + ', ' + + cp2x.toFixed(1) + ' ' + cp2y.toFixed(1) + ', ' + + p2.x.toFixed(1) + ' ' + p2.y.toFixed(1); + } + return d; + }, + + _renderSparkline: function (points, tone) { + var w = 260, h = 42; + var vals = (points && points.length) ? points.slice() : [0]; + if (vals.length < 2) + vals.push(vals[0]); + + var max = 1; + for (var i = 0; i < vals.length; i++) { + if (vals[i] > max) + max = vals[i]; + } + + var n = vals.length; + var pts = []; + for (var j = 0; j < n; j++) { + var x = n === 1 ? 0 : (j * w / (n - 1)); + var y = h - ((vals[j] / max) * (h - 2)) - 1; + pts.push({ x: x, y: y }); + } + + var linePath = this._smoothPath(pts); + var areaPath = linePath + ' L ' + w.toFixed(1) + ' ' + h + ' L 0 ' + h + ' Z'; + + return E('svg', { 'class': 'cl-live-svg', viewBox: '0 0 ' + w + ' ' + h, preserveAspectRatio: 'none' }, [ + E('path', { 'class': 'cl-live-area ' + tone, d: areaPath }), + E('path', { 'class': 'cl-live-line ' + tone, d: linePath }) + ]); + }, + + _renderRealtimeCard: function (name, title) { + var mainId = 'cl-live-' + name + '-main'; + var unitId = 'cl-live-' + name + '-unit'; + var footId = 'cl-live-' + name + '-foot'; + var chartId = 'cl-live-' + name + '-chart'; + var titleChildren = [title]; + + return E('div', { 'class': 'cl-live-card cl-stat-card cl-live-card-' + name + ' is-zero', id: 'cl-live-' + name + '-card' }, [ + E('div', { 'class': 'cl-live-bg-chart', id: chartId }, [this._renderSparkline([0, 0], name)]), + E('div', { 'class': 'cl-live-content' }, [ + E('div', { 'class': 'cl-live-title' }, titleChildren), + E('div', { 'class': 'cl-live-main' }, [ + E('span', { 'class': 'cl-live-num', id: mainId }, '0'), + E('span', { 'class': 'cl-live-unit', id: unitId }, name === 'conn' ? '' : 'B/s') + ]), + E('div', { 'class': 'cl-live-foot', id: footId }, _("Waiting for data...")) + ]) + ]); + }, + + _renderRealtimePanel: function () { + this._initRealtimeHistory(); + return E('div', { 'class': 'cl-live-box' }, [ + E('div', { 'class': 'cl-live-grid' }, [ + this._renderRealtimeCard('up', _("Upload")), + this._renderRealtimeCard('down', _("Download")), + this._renderRealtimeCard('conn', _("Connections")) + ]) + ]); + }, + + _setLiveCardState: function (name, value, hotThreshold, online) { + var card = document.getElementById('cl-live-' + name + '-card'); + if (!card) return; + var cls = 'cl-live-card cl-stat-card cl-live-card-' + name; + if (name === 'conn' && !online) + cls += ' is-offline'; + if (value > 0) + cls += ' is-active'; + else + cls += ' is-zero'; + if (value >= hotThreshold) + cls += ' is-hot'; + card.className = cls; + }, + + _updateRealtimePanel: function (raw) { + var st = raw || {}; + var up = this._toInt(st.up); + var down = this._toInt(st.down); + var upTotal = this._toInt(st.up_total); + var downTotal = this._toInt(st.down_total); + var conn = this._toInt(st.connections); + var mem = this._toInt(st.memory_inuse); + var online = !!(st.ok && st.running); + + var upRate = this._fmtRate(up); + var downRate = this._fmtRate(down); + var upHist = this._pushRealtimeSample('up', up); + var downHist = this._pushRealtimeSample('down', down); + var connHist = this._pushRealtimeSample('conn', conn); + + var setText = function (id, text) { + var el = document.getElementById(id); + if (el) el.textContent = text; + }; + var setChart = L.bind(function (id, points, tone) { + var box = document.getElementById(id); + if (!box) return; + box.innerHTML = ''; + box.appendChild(this._renderSparkline(points, tone)); + }, this); + + setText('cl-live-up-main', upRate.value); + setText('cl-live-up-unit', upRate.unit); + setText('cl-live-up-foot', _("Total %s").replace('%s', this._fmtBytes(upTotal))); + setChart('cl-live-up-chart', upHist, 'up'); + + setText('cl-live-down-main', downRate.value); + setText('cl-live-down-unit', downRate.unit); + setText('cl-live-down-foot', _("Total %s").replace('%s', this._fmtBytes(downTotal))); + setChart('cl-live-down-chart', downHist, 'down'); + + setText('cl-live-conn-main', '' + conn); + setText('cl-live-conn-unit', ''); + setText('cl-live-conn-foot', _("Memory usage %s").replace('%s', this._fmtMiB(mem))); + setChart('cl-live-conn-chart', connHist, 'conn'); + + this._setLiveCardState('up', up, 512 * 1024, online); + this._setLiveCardState('down', down, 1024 * 1024, online); + this._setLiveCardState('conn', conn, 80, online); + + }, + + _probeLabel: function (key, probe) { + if (probe && probe.name) return String(probe.name); + if (probe && probe.title) return String(probe.title); + if (probe && probe.site) return String(probe.site); + var map = { + baidu: 'Baidu', + google: 'Google', + github: 'GitHub', + youtube: 'YouTube', + bytedance: _("ByteDance"), + cloudflare: 'Cloudflare', + bilibili: 'Bilibili', + taobao: 'Taobao' + }; + var k = String(key || '').toLowerCase(); + return map[k] || String(key || 'Unknown'); + }, + + _probeIcon: function (key, probe) { + if (probe && probe.icon) { + if (/^(https?:)?\//.test(probe.icon)) + return probe.icon; + return L.resource('view/clashoo/' + probe.icon); + } + var map = { + baidu: 'baidu.svg', + google: 'google.svg', + github: 'github.svg', + youtube: 'youtube.svg', + bytedance: 'bytedance.svg', + cloudflare: 'cloudflare.svg', + bilibili: 'bilibili.svg', + taobao: 'taobao.svg' + }; + var k = String(key || '').toLowerCase(); + return map[k] ? (L.resource('view/clashoo/' + map[k]) + '?v=20260421p') : null; + }, + + _probeGrade: function (probe) { + var ms = probe && probe.avg_ms != null ? parseInt(probe.avg_ms, 10) : + (probe && probe.ms != null ? parseInt(probe.ms, 10) : + (probe && probe.latency != null ? parseInt(probe.latency, 10) : NaN)); + var ok = !!(probe && (probe.ok === true || probe.state === 'ok' || probe.state === 'high_latency' || probe.state === 'loss')); + if (!ok || !isFinite(ms)) + return { + tone: 'red', + bars: 1, + text: (probe && probe.code === 'skip') ? 'Core down' : + ((probe && probe.code && probe.code !== '000') ? 'Offline' : 'Timeout'), + offline: true + }; + + if (ms <= 400) + return { tone: 'green', bars: 4, text: ms + 'ms' }; + if (ms <= 800) + return { tone: 'warn', bars: 3, text: ms + 'ms' }; + return { tone: 'red', bars: 2, text: ms + 'ms' }; + }, + + _renderSignalBars: function (grade) { + var bars = []; + for (var i = 0; i < 4; i++) { + bars.push(E('i', { 'class': i < grade.bars ? 'active' : '' })); + } + var cls = 'cl-signal cl-signal-' + grade.tone + (grade.offline ? ' cl-signal-offline' : ''); + return E('span', { 'class': cls }, bars); + }, + + _renderProbeIcon: function (icon, label) { + var wrap = E('span', { 'class': 'cl-check-icon-wrap' }); + if (!icon) { + wrap.appendChild(E('span', { 'class': 'cl-check-icon-fallback' }, (label || '?').charAt(0).toUpperCase())); + return wrap; + } + wrap.appendChild(E('img', { + 'class': 'cl-check-icon', + src: icon, + alt: label, + error: function (ev) { + var fb = E('span', { 'class': 'cl-check-icon-fallback' }, (label || '?').charAt(0).toUpperCase()); + if (ev.target && ev.target.parentNode) + ev.target.parentNode.replaceChild(fb, ev.target); + } + })); + return wrap; + }, + + _normalizeCheckItems: function (ac) { + if (!ac) return []; + if ((parseInt(ac.updated_at || '0', 10) || 0) <= 0 && ac.updating) + return []; + var self = this; + + var normalizeItems = function (source) { + if (!source) return []; + + if (Array.isArray(source)) { + return source.map(function (item, idx) { + var key = item.key || item.name || item.title || item.site || ('item_' + idx); + return { + key: key, + label: self._probeLabel(key, item), + icon: self._probeIcon(key, item), + probe: item + }; + }); + } + + var preferred = ['bytedance', 'youtube', 'google', 'github', 'baidu', 'cloudflare']; + var keys = Object.keys(source); + keys.sort(function (a, b) { + var ia = preferred.indexOf(a), ib = preferred.indexOf(b); + if (ia !== -1 || ib !== -1) { + if (ia === -1) return 1; + if (ib === -1) return -1; + return ia - ib; + } + return a.localeCompare(b); + }); + + return keys.map(function (key) { + var probe = source[key] || {}; + return { + key: key, + label: self._probeLabel(key, probe), + icon: self._probeIcon(key, probe), + probe: probe + }; + }); + }; + + if (Array.isArray(ac.items) && ac.items.length) + return normalizeItems(ac.items); + if (ac.proxy) + return normalizeItems(ac.proxy); + if (ac.direct) + return normalizeItems(ac.direct); + return []; + }, + + _renderCheckItem: function (item) { + var grade = this._probeGrade(item.probe || {}); + return E('div', { 'class': 'cl-check-item' }, [ + E('div', { 'class': 'cl-check-main' }, [ + this._renderProbeIcon(item.icon, item.label), + E('span', { 'class': 'cl-check-name site-name' }, item.label) + ]), + E('span', { 'class': 'cl-check-latency latency-val cl-lat-' + grade.tone }, grade.text), + this._renderSignalBars(grade) + ]); + }, + + _formatNumberText: function (tpl, n) { + return String(tpl).replace('%d', String(n)); + }, + + _lastCheckText: function (ts) { + var now = Math.floor(Date.now() / 1000); + var delta = now - (parseInt(ts, 10) || 0); + if (!isFinite(delta) || delta < 0) delta = 0; + if (delta < 5) return _("Last check: just now"); + if (delta < 60) return this._formatNumberText(_("Last check: %d seconds ago"), delta); + if (delta < 3600) return this._formatNumberText(_("Last check: %d minutes ago"), Math.floor(delta / 60)); + return this._formatNumberText(_("Last check: %d hours ago"), Math.floor(delta / 3600)); + }, + + _renderCheckUpdated: function (ac) { + if (!ac) return null; + var updatedAt = parseInt(ac.updated_at || '0', 10) || 0; + if (updatedAt <= 0) { + return E('div', { 'class': 'cl-check-updated' }, _("Last check: initializing")); + } + var msg = this._lastCheckText(updatedAt); + if (ac.updating && this._accessRefreshing) + msg += _(" (refreshing)"); + return E('div', { 'class': 'cl-check-updated' }, msg); + }, + + _renderCheckSkeleton: function () { + return E('div', { 'class': 'cl-check-skeleton' }, [ + E('div', { 'class': 'cl-skel-row' }, [ + E('span', { 'class': 'cl-skel-icon' }), + E('span', { 'class': 'cl-skel-line' }), + E('span', { 'class': 'cl-skel-line short' }) + ]), + E('div', { 'class': 'cl-skel-row' }, [ + E('span', { 'class': 'cl-skel-icon' }), + E('span', { 'class': 'cl-skel-line' }), + E('span', { 'class': 'cl-skel-line short' }) + ]), + E('div', { 'class': 'cl-check-updated' }, _("Last check: initializing")) + ]); + }, + + _renderCheckStatus: function (st, ac) { + var items = this._normalizeCheckItems(ac); + if (!items.length) { + return this._renderCheckSkeleton(); + } + + var self = this; + var children = items.map(function (item) { + return self._renderCheckItem(item); + }); + var updated = this._renderCheckUpdated(ac); + if (updated) children.push(updated); + return E('div', { 'class': 'cl-check-modern' }, children); + }, + + _controls: function (st, cfgData) { + var self = this; + var configs = (cfgData && cfgData.configs) ? cfgData.configs : []; + var current = (cfgData && cfgData.current) || ''; + var proxyMode = st.proxy_mode || 'rule'; + var tcpMode = String(st.tcp_mode || '').toLowerCase(); + var udpMode = String(st.udp_mode || tcpMode).toLowerCase(); + var stackMode = String(st.stack || '').toLowerCase(); + var tpMode = 'custom'; + + if (tcpMode === 'tun' && udpMode === 'tun' && stackMode === 'mixed') + tpMode = 'tun-mixed'; + else if (tcpMode === 'tun' && udpMode === 'tun' && stackMode === 'gvisor') + tpMode = 'tun-gvisor'; + else if (tcpMode === 'tun' && udpMode === 'tun') + tpMode = 'tun-system'; + else if (tcpMode === 'redirect' && udpMode === 'tproxy') + tpMode = 'fake-ip'; + var panelType = st.panel_type || 'zashboard'; + var panelUrl = this._dashboardUrl(st); + var panels = ['metacubexd', 'yacd', 'zashboard', 'razord']; + + var mkSel = function (opts, val, fn, extraCls) { + return E('select', { 'class': 'cbi-input-select' + (extraCls ? ' ' + extraCls : ''), change: fn }, + opts.map(function (o) { + return E('option', { value: o[0], selected: o[0] === val ? '' : null, disabled: o[2] ? '' : null }, o[1]); + })); + }; + var tpModeOptions = [['fake-ip','Fake-IP'],['tun-system','System'],['tun-gvisor','gVisor'],['tun-mixed','Mixed']]; + if (tpMode === 'custom') + tpModeOptions.unshift(['custom',_("Custom")]); + + var panelStatusEl = E('span', { 'class': 'cl-panel-status' }, ''); + + var panelSel = mkSel(panels.map(function (p) { + return [p, self._panelLabel(p)]; + }), panelType, function (ev) { + var next = ev.target.value; + var prev = panelType; + ev.target.disabled = true; + self._lastSt = self._lastSt || {}; + self._lastSt.panel_type = next; + clashoo.setPanel(next).then(function (r) { + if (r && r.error) + throw new Error(r.error); + return self._pollOverview(true); + }).catch(function (e) { + self._lastSt.panel_type = prev; + ev.target.value = prev; + ui.addNotification(null, E('p', _("Panel switch failed: ") + (e.message || e))); + }).then(function () { + ev.target.disabled = false; + }); + }); + + return [ + E('div', { 'class': 'cl-ctrl' }, [ + E('label', {}, _("Proxy Mode")), + mkSel([['rule',_("Rule")],['global',_("Global")],['direct',_("Direct")]], proxyMode, + function (ev) { + var sel = ev.target; + if (sel.disabled || clKernelBusy()) return; + clKernelLock(); + sel.disabled = true; + Promise.resolve(clashoo.setProxyMode(sel.value)).then(function () { + return self._pollOverview(true); + }).catch(function () {}).then(function () { + sel.disabled = false; + clKernelUnlock(); + }); + }, 'cl-kernel-sel') + ]), + E('div', { 'class': 'cl-ctrl' }, [ + E('label', {}, _("Run Mode")), + mkSel(tpModeOptions, tpMode, + function (ev) { + var mode = ev.target.value; + if (mode === 'custom') + return; + var sel = ev.target; + if (sel.disabled || clKernelBusy()) return; + clKernelLock(); + sel.disabled = true; + self._op = 'mode'; + self._lastSt = self._lastSt || {}; + if (mode === 'tun-mixed') { + self._lastSt.tcp_mode = 'tun'; + self._lastSt.udp_mode = 'tun'; + self._lastSt.stack = 'mixed'; + } else if (mode === 'tun-gvisor') { + self._lastSt.tcp_mode = 'tun'; + self._lastSt.udp_mode = 'tun'; + self._lastSt.stack = 'gvisor'; + } else if (mode === 'tun-system') { + self._lastSt.tcp_mode = 'tun'; + self._lastSt.udp_mode = 'tun'; + self._lastSt.stack = 'system'; + } else { + self._lastSt.tcp_mode = 'redirect'; + self._lastSt.udp_mode = 'tproxy'; + } + clashoo.setMode(mode).then(function (r) { + if (r && r.error) + ui.addNotification(null, E('p', _("Run mode setting failed: ") + r.error)); + return self._pollOverview(true); + }).catch(function (e) { + ui.addNotification(null, E('p', _("Run mode setting failed: ") + (e.message || e))); + }).then(function () { + self._op = null; + sel.disabled = false; + clKernelUnlock(); + }); + }, 'cl-kernel-sel') + ]), + E('div', { 'class': 'cl-ctrl' }, [ + E('label', {}, _("Configuration Files")), + E('div', { 'class': 'cl-ctrl-row cl-config-row' }, [ + mkSel(configs.length ? configs.map(function(c){return[c,c];}) : [['',_("(empty)")]], current, + function (ev) { + var sel = ev.target; + if (sel.disabled || clKernelBusy()) return; + var name = sel.value; + var prev = sel.getAttribute('data-cl-prev') || current; + clKernelLock(); + sel.disabled = true; + var setter = (st.core_type === 'singbox') + ? clashoo.setSingboxProfile(name) + : clashoo.setConfig(name); + setter + .then(function (r) { + if (r && r.error) { + ui.addNotification(null, E('p', _("Switch configuration failed: ") + r.error)); + sel.value = prev; + return; + } + sel.setAttribute('data-cl-prev', name); + }) + .catch(function (e) { + ui.addNotification(null, E('p', _("Switch configuration failed: ") + (e.message || e))); + sel.value = prev; + }) + .then(function () { + sel.disabled = false; + clKernelUnlock(); + /* force immediate state refresh so cards get new current */ + return self._pollOverview(true); + }); + }, 'cl-kernel-sel'), + E('button', { + 'class': 'btn cbi-button-action cl-btn-update-sub', + click: L.bind(function (ev) { this._updSubs(ev); }, this) + }, _("Update Subscription")) + ]) + ]), + E('div', { 'class': 'cl-ctrl' }, [ + E('label', {}, _("Dashboard")), + E('div', { 'class': 'cl-ctrl-row' }, [ + panelSel, + E('button', { + 'class': 'btn cbi-button cl-btn-panel-update', + click: function () { + self._runPanelUpdate(panelSel.value, this, panelStatusEl); + } + }, _("Update")), + panelStatusEl, + E('a', { + 'class': 'btn cbi-button cl-btn-panel-open', + href: panelUrl, + target: '_blank', + rel: 'noopener noreferrer', + click: function (ev) { + ev.preventDefault(); + window.open(self._dashboardUrl(self._lastSt || st), '_blank', 'noopener'); + } + }, _("Open Dashboard")) + ]) + ]) + ]; + }, + + _dashboardUrl: function (st) { + st = st || {}; + var dashPort = st.dash_port || + uci.get('clashoo', 'config', 'dash_port') || + uci.get('clashoo', 'config', 'dashboard_port') || + '9090'; + var host = location.hostname || st.local_ip || '127.0.0.1'; + var secret = st.dash_pass || + uci.get('clashoo', 'config', 'dash_pass') || + ''; + var params = [ + ['host', host], + ['hostname', host], + ['port', dashPort], + ['secret', secret] + ].map(function (kv) { + return encodeURIComponent(kv[0]) + '=' + encodeURIComponent(kv[1] || ''); + }).join('&'); + + return 'http://' + host + ':' + dashPort + '/ui/?' + params + '#/proxies'; + }, + + _pollOverview: function (force) { + this._applyThemeClass(); + if (!force && this._op) return Promise.resolve(); + if (!force && this._coreSwitchBusy) return Promise.resolve(); + if (!force && this._panelUpdateBusy) return Promise.resolve(); + if (!force && document.hidden) return Promise.resolve(); + var self = this; + return rpcOverview() + .then(function (ov) { + ov = ov || {}; + var st = ov.status || {}; + var cfgData = ov.configs || { configs: [], current: '' }; + var ac = ov.access || {}; + var stats = ov.stats || {}; + + /* force proxy probes down when stopped (skip daemon refresh) */ + if (!st.running && ac && ac.proxy) { + var downProbe = { ok: false, state: 'down', code: 'skip', ok_count: 0, attempts: 1, loss: 1, avg_ms: 0 }; + Object.keys(ac.proxy).forEach(function (k) { ac.proxy[k] = downProbe; }); + } + + self._overviewLoaded = true; + self._lastSt = st; + self._lastCfgData = cfgData; + self._lastAc = ac; + self._writeCachedOverview(ov); + self._refreshCoreSwitch(); + + /* only rebuild DOM on user-visible field changes, avoid flicker every 5s + 尤其是切换 tab 回到 overview 时缓存数据与首次 poll 数据通常一致 */ + var sig = JSON.stringify({ + running: st.running, + health: st.health_status, + proxy_mode: st.proxy_mode, + tcp_mode: st.tcp_mode, + udp_mode: st.udp_mode, + stack: st.stack_type || st.stack, + config: st.config, + core_type: st.core_type, + panel_type: st.panel_type, + dcore: st.dcore, + configs: cfgData.configs || [], + current: cfgData.current, + ac_updated: ac.updated_at, + ac_updating: ac.updating + }); + var skipRebuild = (sig === self._lastRenderSig); + self._lastRenderSig = sig; + + var cards = document.getElementById('cl-cards'); + if (!cards) return; + if (!skipRebuild) { + var newCards = self._cards(st, cfgData, ac); + cards.innerHTML = ''; + newCards.forEach(function (card) { cards.appendChild(card); }); + + var controls = document.getElementById('cl-controls'); + if (controls) { + controls.innerHTML = ''; + self._controls(st, cfgData).forEach(function (ctrl) { controls.appendChild(ctrl); }); + } + } + + self._updateRealtimePanel(stats); + }); + }, + + /* Compatibility wrapper for existing call sites */ + _pollStatus: function () { + return this._pollOverview(); + }, + + /* Slow poll for connectivity check — every 60s, network probes are expensive */ + _pollAccess: function () { + return this._pollOverview(true); + }, + + _refreshAccessCard: function () { + var cards = document.getElementById('cl-cards'); + if (!cards) return; + var st = this._lastSt || {}; + var ac = this._lastAc || {}; + var newCard = this._card(_("Access Check"), this._renderCheckStatus(st, ac), 'cl-card-access', this._renderAccessRefresh(ac)); + var oldCard = cards.querySelector('.cl-card-access'); + if (oldCard && oldCard.parentNode) { + oldCard.parentNode.replaceChild(newCard, oldCard); + } else { + cards.appendChild(newCard); + } + }, + + _manualRefreshAccess: function () { + var self = this; + if (self._accessRefreshing) return Promise.resolve(); + var previousUpdatedAt = parseInt((self._lastAc && self._lastAc.updated_at) || 0, 10) || 0; + self._accessRefreshing = true; + self._refreshAccessCard(); + + var trigger = (clashoo && typeof clashoo.accessCheckRefresh === 'function') + ? clashoo.accessCheckRefresh() + : Promise.resolve({ success: true }); + + return L.resolveDefault(trigger, { success: false }) + .then(function () { + return self._waitAccessRefresh(previousUpdatedAt); + }) + .catch(function () { + return self._pollAccess(); + }) + .then(function () { + self._accessRefreshing = false; + self._refreshAccessCard(); + }); + }, + + _waitAccessRefresh: function (previousUpdatedAt) { + var self = this; + var started = Date.now(); + var maxWait = 25000; + + function sleep(ms) { + return new Promise(function (resolve) { setTimeout(resolve, ms); }); + } + + function loop() { + return self._pollAccess().then(function () { + var ac = self._lastAc || {}; + var updatedAt = parseInt(ac.updated_at || 0, 10) || 0; + if (updatedAt > previousUpdatedAt && !ac.updating) + return; + if (!ac.updating && updatedAt > 0 && (Date.now() - started) > 2500) + return; + if ((Date.now() - started) >= maxWait) + return; + return sleep(700).then(loop); + }); + } + + return sleep(500).then(loop); + }, + + _pollRealtime: function () { + return this._pollOverview(); + }, + + /* health_detail → 用户看得懂的中文(参考 P2-F 字典;详细状态时只在状态卡上展示) */ + _friendlyHealth: function (detail) { + var map = { + 'boot_disabled': _("Stopped (not enabled at boot)"), + 'service_disabled': _("Service disabled"), + 'service_stopped': _("Service stopped"), + 'preflight:openclash_conflict': _("OpenClash is enabled; disable it first to avoid rule conflicts"), + 'preflight:config_missing': _("No configuration file found. Import a subscription or upload a configuration on the Configuration page first."), + 'preflight:missing_fw4_stack': _("System is missing nftables runtime"), + 'preflight:core_validation_failed': _("Core verification failed. Check System → Core."), + 'start:core_not_running': _("Core did not start within 15 seconds (procd is recovering)"), + 'start:singbox_service_failed': _("sing-box service failed to start. Check System → Log."), + 'init': _("Initializing…") + }; + return map[detail] || ''; + }, + + /* health_detail → 启动 / 停止 进度文字 */ + _opPhase: function (opKey, st) { + var hd = (st && st.health_detail) || ''; + if (opKey === 'stop') { + if (st && st.running === false) return _("Stopped") + ' ✓'; + return _("Stopping…"); + } + if (!st || st.running !== true) return _("Starting core…"); + if (hd === 'init') return _("Checking environment…"); + if (hd.indexOf('preflight:') === 0) return _("Checking environment…"); + if (hd.indexOf('start:') === 0) return this._friendlyHealth(hd) || _("Starting core…"); + if (st.health_status === 'pass') return _("Ready ✓"); + if (st.health_status === 'fail') return _("Started (") + (this._friendlyHealth(hd) || _("Health check failed")) + ')'; + return _("Health check in progress…"); + }, + + /* fn: fire-and-forget RPC + 秒速轮询直到状态到位 */ + /* toggle switch instantly (optimistic), do not wait for RPC */ + _svc: function (fn, opKey) { + if (this._busy) return Promise.resolve(); + + // preflight: refuse start when no profile selected. + // backend select_config also defends but toast is slow, UI would flash. + if (opKey === 'start' || opKey === 'restart') { + var st0 = this._lastSt || {}; + var hasConfig = !!(st0.config || st0.conf_path); + if (!hasConfig) { + ui.addNotification(null, E('p', _("No configuration file selected. Select or upload one on the Configuration page before starting.")), 'warning'); + return Promise.resolve(); + } + } + + this._busy = true; + var self = this; + self._op = opKey; + + /* 立即翻转 switch 视觉 */ + var btn = document.querySelector('.cl-service-switch'); + if (btn) { + var running = btn.getAttribute('aria-pressed') === 'true'; + var newRunning = opKey === 'start'; + if (newRunning !== running) { + btn.setAttribute('aria-pressed', newRunning ? 'true' : 'false'); + btn.className = 'cl-service-switch' + (newRunning ? ' is-on' : ' is-off'); + } + self._showOpMsg(opKey === 'stop' ? _("Stopping…") : _("Starting…")); + } + + var maxWait = opKey === 'stop' ? 15000 : 35000; + fn().catch(function () {}); /* fire-and-forget */ + + var started = Date.now(); + var pollTimer = null; + + function finish(finalMsg) { + if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } + if (finalMsg) self._showOpMsg(finalMsg); + self._busy = false; + self._op = null; + self._pollOverview(true); + } + + function pollOnce() { + L.resolveDefault(clashoo.status(), {}).then(function (st) { + st = st || {}; + var elapsed = Date.now() - started; + if (opKey === 'stop') { + if (st.running === false) return finish(_("Stopped") + ' ⚪'); + } else { + if (st.running === true) return finish(_("Running 🟢")); + if (st.health_status === 'fail') return finish(null); + } + if (elapsed >= maxWait) return finish(null); + pollTimer = setTimeout(pollOnce, 500); + }).catch(function () { + if (Date.now() - started < maxWait) pollTimer = setTimeout(pollOnce, 500); + else finish(null); + }); + } + pollTimer = setTimeout(pollOnce, 100); + }, + + _start: function () { return this._svc(function () { return clashoo.start(); }, 'start'); }, + _stop: function () { return this._svc(function () { return clashoo.stop(); }, 'stop'); }, + _restart: function () { return this._svc(function () { return clashoo.restart(); }, 'restart'); }, + + _updSubs: function (ev) { + var btn = ev && ev.target; + if (btn && btn.disabled) return Promise.resolve(); + if (clKernelBusy()) return Promise.resolve(); + clKernelLock(); + var origText = btn ? btn.textContent : ''; + if (btn) { btn.disabled = true; btn.textContent = _("Updating…"); } + return L.resolveDefault(callUpdateCurrentSubscription(), {}).then(function (r) { + if (r && r.reason === 'not_subscription') { + ui.addNotification(null, E('p', _("Current configuration is a custom file; no subscription update needed")), 'info'); + return; + } + ui.addNotification(null, E('p', r.success ? (r.message || _("Subscription updated successfully")) + : (_("Update failed: ") + (r.message || _("Current configuration has no recorded subscription URL"))))); + }).catch(function () {}).then(function () { + if (btn) { btn.disabled = false; btn.textContent = origText; } + clKernelUnlock(); + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); + +reload_service() { + restart +} diff --git a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/system.js b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/system.js new file mode 100644 index 000000000..c1c982390 --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/system.js @@ -0,0 +1,1431 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; +'require ui'; +'require poll'; +'require rpc'; +'require fs'; +'require tools.clashoo as clashoo'; + +function getThemeClass() { + var h = document.documentElement; + // Bootstrap: explicit data-bs-theme attribute wins first + if (h.dataset.bsTheme === 'dark') return 'cl-theme-dark'; + if (h.dataset.bsTheme === 'light') return 'cl-theme-light'; + // Argon: explicit darkmode attribute + if (h.dataset.darkmode === 'true') return 'cl-theme-dark'; + // Argon: dark.css loaded in document = dark mode active (most reliable) + var links = document.querySelectorAll('link[rel="stylesheet"]'); + for (var i = 0; i < links.length; i++) { + if (links[i].href && links[i].href.indexOf('dark.css') !== -1) return 'cl-theme-dark'; + } + // Generic: .dark class on root element + if (h.classList.contains('dark')) return 'cl-theme-dark'; + // Body background luminance (skip transparent/rgba backgrounds) + var bg = window.getComputedStyle(document.body).backgroundColor; + if (bg && bg.indexOf('rgba') === -1 && bg.indexOf('rgb') !== -1) { + var m = bg.match(/\d+/g); + if (m && m.length >= 3 && (parseInt(m[0]) + parseInt(m[1]) + parseInt(m[2])) / 3 < 100) return 'cl-theme-dark'; + } + return 'cl-theme-light'; +} + +var callHostHints = rpc.declare({ + object: 'luci-rpc', + method: 'getHostHints', + expect: { '': {} } +}); + +var callBackupExportPrepare = rpc.declare({ object: 'luci.clashoo', method: 'backup_export_prepare', expect: {} }); +var callBackupExportCleanup = rpc.declare({ object: 'luci.clashoo', method: 'backup_export_cleanup', params: ['path'], expect: {} }); +var callBackupImportPrepare = rpc.declare({ object: 'luci.clashoo', method: 'backup_import_prepare', expect: {} }); +var callBackupImportChunk = rpc.declare({ object: 'luci.clashoo', method: 'backup_import_chunk', params: ['path', 'offset', 'data'], expect: {} }); +var callBackupImportCleanup = rpc.declare({ object: 'luci.clashoo', method: 'backup_import_cleanup', params: ['path'], expect: {} }); +var callBackupImportFile = rpc.declare({ object: 'luci.clashoo', method: 'backup_import_file', params: ['name', 'path'], expect: {} }); +var callBackupReset = rpc.declare({ object: 'luci.clashoo', method: 'backup_reset', expect: {} }); + +var CSS = [ + '.cl-wrap{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}', + '.cl-tabs{display:flex;border-bottom:2px solid rgba(128,128,128,.15);margin-bottom:18px}', + '.cl-tab{padding:10px 20px;cursor:pointer;font-size:13px;opacity:.55;border-bottom:2px solid transparent;margin-bottom:-2px}', + '.cl-tab.active{opacity:1;border-bottom-color:currentColor;font-weight:600}', + '.cl-panel{display:none}.cl-panel.active{display:block}', + '.cl-section{margin-bottom:20px}', + '.cl-section h4{font-size:.95rem;font-weight:600;margin-bottom:10px;color:var(--title-color,rgba(92,102,120,.72));opacity:.95}', + '.cl-save-bar{display:flex;gap:8px;margin-top:14px;padding-top:12px;border-top:1px solid rgba(128,128,128,.15)}', + '.cl-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}', + '.cl-log-area{font-family:monospace;font-size:11px;opacity:.75;max-height:300px;overflow-y:auto;border:1px solid rgba(128,128,128,.2);border-radius:8px;padding:10px;white-space:pre-wrap;margin-top:8px}', + '.cl-log-tabs{display:flex;gap:8px;margin-bottom:8px}', + '.cl-log-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:8px}', + '.cl-log-toolbar label{font-size:11px;display:inline-flex;align-items:center;gap:4px;cursor:pointer;margin:0}', + /* Argon shifts label>checkbox down (top:.4rem); reset so flex centers it */ + '.cl-log-toolbar input[type=checkbox]{position:static;top:auto;right:auto;margin:0}', + /* unify select + button heights so the row lines up */ + '.cl-log-toolbar>select,.cl-log-toolbar>button{height:26px;box-sizing:border-box;font-size:11px;line-height:1;padding:0 10px;margin:0}', + '.cl-log-tab{padding:4px 12px;border:1px solid rgba(128,128,128,.2);border-radius:20px;font-size:12px;cursor:pointer;opacity:.6}', + '.cl-log-tab.active{opacity:1;font-weight:600;background:rgba(128,128,128,.1)}', + '.cl-log-line{display:block;padding:1px 4px;border-radius:3px}', + /* Light theme: AA+ contrast on white bg (issue #16) */ + '.cl-log-line.cl-log-info{color:#1f6b3f}', + '.cl-log-line.cl-log-warn{color:#8b5a00}', + '.cl-log-line.cl-log-error{color:#c0392b;background:rgba(192,57,43,.06)}', + '.cl-log-line.cl-log-debug{color:#5a6270;opacity:.85}', + '.cl-log-line.cl-log-hidden{display:none}', + '.cl-log-ts{color:#5a6270;margin-right:8px}', + '.cl-log-msg{color:inherit}', + /* Dark theme overrides: original colors tuned for dark bg */ + '.cl-theme-dark .cl-log-line.cl-log-info{color:#7fc7a8}', + '.cl-theme-dark .cl-log-line.cl-log-warn{color:#e8b95a}', + '.cl-theme-dark .cl-log-line.cl-log-error{color:#ea7878;background:rgba(234,120,120,.06)}', + '.cl-theme-dark .cl-log-line.cl-log-debug{color:#7a8290;opacity:.7}', + '.cl-theme-dark .cl-log-ts{color:#6b7480}', + '.cl-dl-hint{margin-top:6px;font-size:12px;min-height:18px;line-height:1.4}', + '.cl-component-card{padding:16px 18px;border:1px solid var(--cl-surface-border,rgba(128,128,128,.14));border-radius:var(--border-radius,var(--cl-radius,12px));background:var(--cl-card-bg,#fff);box-shadow:var(--card-shadow,var(--cl-card-shadow));margin:0 14px 14px}', + '.cl-component-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px}', + '.cl-component-head h4{display:flex;align-items:center;gap:8px;margin:0 0 4px !important;padding:0 !important;font-size:0.95rem;font-weight:600;color:var(--title-color,var(--cl-title-color,inherit));background:transparent !important}', + '.cl-component-head h4:before{content:"";width:3px;height:14px;border-radius:2px;background:var(--primary-color,var(--cl-primary,#2f80ed));flex:0 0 3px}', + '.cl-component-adv{margin-top:10px}', + '.cl-component-adv-bar{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;cursor:pointer;user-select:none;font-size:13px;font-weight:700;background:var(--cl-control-bg,rgba(128,128,128,.04));border:1px solid var(--cl-control-border,rgba(128,128,128,.14));border-radius:8px}', + '.cl-component-adv-bar:hover{background:var(--cl-primary-soft,rgba(0,122,255,.08))}', + '.cl-component-adv-chevron{font-size:16px;font-weight:700;opacity:.55;transition:transform .2s}', + '.cl-component-adv:not(.cl-closed) .cl-component-adv-chevron{transform:rotate(90deg)}', + '.cl-component-adv-body{margin-top:8px}', + '.cl-component-adv.cl-closed .cl-component-adv-body{display:none}', + '.cl-component-sub{font-size:12px;color:rgba(120,130,150,.9);line-height:1.45}', + '.cl-component-list{display:grid;gap:8px}', + '.cl-component-row{display:grid;grid-template-columns:minmax(170px,1.1fr) minmax(180px,1.5fr) minmax(130px,.9fr) auto;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--cl-surface-border,rgba(128,128,128,.14));border-radius:8px;background:var(--cl-card-bg,#fff)}', + '.cl-component-name{font-weight:700;font-size:13px}', + '.cl-component-desc{font-size:12px;color:rgba(120,130,150,.92);margin-top:2px}', + '.cl-component-version{font-size:12px;line-height:1.45;color:rgba(90,100,120,.95);word-break:break-word}', + '.cl-theme-dark .cl-component-version{color:rgba(210,220,235,.86)}', + '.cl-component-status{font-size:12px;line-height:1.45;color:rgba(90,100,120,.95)}', + '.cl-component-status.cl-st-running{color:#2f80ed}.cl-component-status.cl-st-success{color:#239b56}.cl-component-status.cl-st-failed{color:#d43f3a}', + '.cl-component-log{margin-top:10px;font-family:monospace;font-size:11px;white-space:pre-wrap;max-height:140px;overflow:auto;padding:9px;border:1px solid var(--cl-surface-border,rgba(128,128,128,.14));border-radius:8px;background:var(--cl-card-bg,#fff);color:#1f6b3f}', + '.cl-theme-dark .cl-component-log{color:#a3d9ad}', + '.cl-component-arch{margin-bottom:12px}', + '.cl-component-arch-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:9px 12px;border:1px solid var(--cl-surface-border,rgba(128,128,128,.14));border-radius:8px;background:var(--cl-card-bg,#fff)}', + '.cl-component-arch-label{font-size:13px;font-weight:700}', + '.cl-component-arch-auto{font-size:12px;color:rgba(90,100,120,.82);line-height:1.2}', + '.cl-theme-dark .cl-component-arch-auto{color:rgba(210,220,235,.78)}', + '.cl-wrap .cl-component-arch-sel{font-size:12px!important;font-weight:700!important;padding:4px 9px!important;border:1px solid rgba(46,170,92,.28)!important;border-radius:6px!important;background:rgba(46,170,92,.12)!important;color:#24844d!important}', + '.cl-wrap.cl-theme-dark .cl-component-arch-sel{background:rgba(104,199,135,.16)!important;color:#c6f6d3!important;border-color:rgba(104,199,135,.3)!important}', + '.cl-component-arch-sel option{background:var(--background-color,#fff);color:inherit}', + '.cl-component-arch-hint{font-size:12px;color:rgba(120,130,150,.9)}', + '.cl-comp-var-box{display:inline-flex;gap:4px;margin-top:5px}', + '.cl-comp-var{font-size:11px;padding:2px 10px;border-radius:10px;border:1px solid rgba(128,128,128,.3);background:transparent;color:inherit;cursor:pointer;min-width:52px}', + '.cl-comp-var.on{background:rgba(var(--primary-rgb,0,122,255),.18);border-color:rgba(var(--primary-rgb,0,122,255),.64);color:var(--primary-color,#0b68dd);font-weight:800;box-shadow:0 0 0 1px rgba(var(--primary-rgb,0,122,255),.1) inset}', + '.cl-comp-updatable{font-size:10px;font-weight:700;color:#239b56;margin-left:2px}', + '@media(max-width:760px){.cl-component-head{display:block}.cl-component-head .btn{margin-top:10px}.cl-component-row{grid-template-columns:1fr;gap:6px}.cl-component-row .btn{width:auto;justify-self:start;padding-left:18px;padding-right:18px}}', + /* align form.Map font with config page */ + '.cl-panel .cbi-section>h3{font-size:13px !important;font-weight:600;margin-bottom:8px}', + '.cl-panel .cbi-value-title{font-size:13px !important}', + '.cl-panel .cbi-value-field input,.cl-panel .cbi-value-field select,.cl-panel .cbi-value-field textarea{font-size:13px !important}', + '.cl-panel .cbi-section-descr,.cl-panel .cbi-value-helptext{font-size:12px !important}', + '.cl-panel .cbi-section{margin-bottom:12px}', + '.cl-wrap .cbi-section>h3,.cl-wrap .cbi-value-title,.cl-wrap .cbi-section-descr,.cl-wrap .cbi-value-helptext{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif !important}', + '.cl-wrap .cbi-input-text,.cl-wrap .cbi-input-select,.cl-wrap select,.cl-wrap input,.cl-wrap textarea,.cl-wrap .btn,.cl-wrap .cbi-button{font-size:13px !important;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif !important}', + '.cl-wrap .btn,.cl-wrap .cbi-button{padding:4px 10px;line-height:1.35}', + '#cbi-clashoo-lan_acl{overflow-x:visible!important}', + '#cbi-clashoo-lan_acl .cbi-section-table{display:flex;width:100%;overflow:visible!important;flex-direction:column;border:0!important}', + '#cbi-clashoo-lan_acl .cbi-section-thead{display:none}', + '#cbi-clashoo-lan_acl .cbi-section-tbody{display:flex;width:100%;flex-direction:column}', + '#cbi-clashoo-lan_acl .cbi-section-table-row{display:flex;width:100%;padding:14px 0;border-top:1px solid rgba(128,128,128,.16);flex-direction:column;align-items:stretch}', + '#cbi-clashoo-lan_acl .cbi-section-table-row:first-child{border-top:0}', + '#cbi-clashoo-lan_acl .cl-acl-empty-row,#cbi-clashoo-lan_acl .cl-acl-empty-row>*{border:0!important;box-shadow:none!important}', + '#cbi-clashoo-lan_acl .cl-acl-value{width:100%;border:0!important;box-sizing:border-box}', + '#cbi-clashoo-lan_acl .cl-acl-value .cbi-dropdown[open]>ul.dropdown{width:max-content;min-width:100%;max-width:calc(100vw - 48px)}', + '@media(max-width:600px){#cbi-clashoo-lan_acl .cl-acl-value .cbi-dropdown[open]>ul.dropdown{width:100%!important;min-width:0;max-width:100%}#cbi-clashoo-lan_acl .cl-acl-value .cbi-dropdown[open]>ul.dropdown>li{height:auto;white-space:normal;overflow-wrap:anywhere;line-height:1.35}}', + '#cbi-clashoo-lan_acl .cbi-section-actions{display:flex!important;justify-content:flex-end;width:auto!important;padding:8px 0 0!important;border:0!important}', + '#cbi-clashoo-lan_acl .cbi-section-actions>div{display:flex;gap:8px}', + '#cbi-clashoo-lan_acl .cbi-section-create{padding-top:10px}' +].join(''); + +/* component update: main area = clashoo/client only; rest in collapsible */ +var COMPONENT_MAIN_IDS = { clashoo: true, luci: true }; + +function fastResolve(promise, timeoutMs, fallback) { + var t = new Promise(function (resolve) { + setTimeout(function () { resolve(fallback); }, timeoutMs); + }); + return Promise.race([L.resolveDefault(promise, fallback), t]); +} + +function decorateSystemForm(root) { + if (!root || !root.querySelectorAll) + return; + + var fields = root.querySelectorAll('.cbi-value-field'); + for (var i = 0; i < fields.length; i++) { + if (fields[i] && fields[i].classList) + fields[i].classList.add('cl-control-wrap'); + } + + var sections = root.querySelectorAll('.cbi-section'); + for (var j = 0; j < sections.length; j++) { + if (sections[j] && sections[j].classList) + sections[j].classList.add('cl-form-card'); + } +} + +function normalizeLanAclLayout(root) { + var rows = root.querySelectorAll('#cbi-clashoo-lan_acl .cbi-section-table-row'); + for (var r = 0; r < rows.length; r++) { + if (!rows[r].querySelector('input, select, button, .cbi-dropdown')) + rows[r].classList.add('cl-acl-empty-row'); + } + + var cells = root.querySelectorAll('#cbi-clashoo-lan_acl .cbi-section-table-row > td.cbi-value-field:not(.cl-acl-value)'); + for (var i = 0; i < cells.length; i++) { + var cell = cells[i]; + var control = cell.firstElementChild; + if (!control) + continue; + + cell.parentElement.classList.add('cbi-section-node', 'cl-acl-node'); + + var field = document.createElement('div'); + field.className = 'cbi-value-field'; + field.appendChild(control); + + var label = document.createElement('label'); + label.className = 'cbi-value-title'; + label.textContent = cell.getAttribute('data-title') || ''; + + cell.textContent = ''; + cell.className = 'cbi-value cl-acl-value'; + cell.appendChild(label); + cell.appendChild(field); + } +} + +function syncLanAclControlWidth(root) { + var reference = root.querySelector('#cbi-clashoo-config-bypass_port_mode select'); + if (!reference) + return; + + var referenceWidth = reference.getBoundingClientRect().width; + if (!referenceWidth) + return; + + var section = root.querySelector('#cbi-clashoo-lan_acl'); + var sectionRect = section.getBoundingClientRect(); + var sectionStyle = window.getComputedStyle(section); + var contentRight = sectionRect.right - parseFloat(sectionStyle.paddingRight || 0); + var lists = root.querySelectorAll('#cbi-clashoo-lan_acl .cbi-dynlist'); + for (var i = 0; i < lists.length; i++) { + var left = lists[i].getBoundingClientRect().left; + var width = Math.min(referenceWidth, contentRight - left); + lists[i].style.width = Math.max(0, width) + 'px'; + lists[i].style.minWidth = '0'; + lists[i].style.maxWidth = 'none'; + + var controls = lists[i].querySelectorAll('.add-item, .cbi-dropdown'); + for (var j = 0; j < controls.length; j++) { + controls[j].style.width = '100%'; + controls[j].style.minWidth = '0'; + controls[j].style.maxWidth = '100%'; + controls[j].style.boxSizing = 'border-box'; + } + } +} + +function watchLanAclLayout(root) { + normalizeLanAclLayout(root); + syncLanAclControlWidth(root); + var observer = new MutationObserver(function () { + normalizeLanAclLayout(root); + syncLanAclControlWidth(root); + }); + observer.observe(root, { childList: true, subtree: true }); + window.addEventListener('resize', function () { + syncLanAclControlWidth(root); + }); +} + +function randomSecret(len) { + var chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'; + var out = ''; + var n = Math.max(6, parseInt(len, 10) || 6); + for (var i = 0; i < n; i++) { + out += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return out; +} + +function enhanceDashPasswordField(root) { + if (!root || !root.querySelector) + return; + + var input = root.querySelector('input[id$=".dash_pass"], input[name$=".dash_pass"]'); + if (!input || input.dataset.clEnhanced === '1') + return; + + input.dataset.clEnhanced = '1'; + input.type = 'password'; + input.autocomplete = 'new-password'; + + var parent = input.parentNode; + if (!parent) + return; + + /* Remove LuCI default password helper controls (e.g. stray "*" button) */ + var children = parent.children ? Array.prototype.slice.call(parent.children) : []; + children.forEach(function (el) { + if (!el || el === input) + return; + var tag = (el.tagName || '').toUpperCase(); + var isBtnLike = tag === 'BUTTON' + || (tag === 'INPUT' && String(el.type || '').toLowerCase() === 'button') + || ((el.className || '').indexOf('cbi-button') >= 0); + if (isBtnLike) + parent.removeChild(el); + }); + + var wrap = E('div', { 'class': 'cl-pass-wrap' }); + parent.insertBefore(wrap, input); + wrap.appendChild(input); + + var eyeBtn = E('button', { + type: 'button', + 'class': 'btn cbi-button cl-pass-btn', + title: _("Show/Hide"), + click: function (ev) { + ev.preventDefault(); + var show = input.type === 'password'; + input.type = show ? 'text' : 'password'; + eyeBtn.textContent = show ? '🙈' : '👁'; + } + }, '👁'); + + var genBtn = E('button', { + type: 'button', + 'class': 'btn cbi-button-action cl-pass-btn cl-pass-gen', + click: function (ev) { + ev.preventDefault(); + input.type = 'text'; + input.value = randomSecret(6); + eyeBtn.textContent = '🙈'; + input.dispatchEvent(new Event('change', { bubbles: true })); + } + }, _("Random")); + + wrap.appendChild(eyeBtn); + wrap.appendChild(genBtn); +} + +function clearClashooDirty() { + var applyPromise; + try { + applyPromise = (L.uci && typeof L.uci.callApply === 'function') + ? Promise.resolve(L.uci.callApply(0, false)).catch(function () {}) + : Promise.resolve(); + } catch (e) { applyPromise = Promise.resolve(); } + return applyPromise.then(function () { + try { + if (L.ui && L.ui.changes && L.ui.changes.changes) { + delete L.ui.changes.changes.clashoo; + var n = Object.keys(L.ui.changes.changes).length; + if (typeof L.ui.changes.renderChangeIndicator === 'function') + L.ui.changes.renderChangeIndicator(n); + else if (typeof L.ui.changes.setIndicator === 'function') + L.ui.changes.setIndicator(n); + } + } catch (e) {} + }); +} + +function saveCommitApplyMaybeReload(m, runningMsg, stoppedMsg) { + return clashoo.status() + .then(function (st) { return !!(st && st.running); }) + .catch(function () { return false; }) + .then(function (running) { + return m.save() + .then(function () { return clashoo.commitConfig(); }) + .then(function () { + return running ? clashoo.reload() : { success: true, skipped: true }; + }) + .then(function () { return clearClashooDirty(); }) + .then(function () { + ui.addNotification(null, E('p', running ? runningMsg : stoppedMsg)); + window.setTimeout(function () { location.reload(); }, 300); + }); + }); +} + +function coreChoiceFromConfig(coreType, dcore) { + if (coreType === 'singbox') + return dcore === '5' ? 'singbox-alpha' : 'singbox-stable'; + if (dcore === '1') return 'smart'; + if (dcore === '3') return 'mihomo-alpha'; + return 'mihomo-stable'; +} + +function coreChoiceTarget(choice) { + var targets = { + 'mihomo-stable': { core: 'mihomo', dcore: '2' }, + 'mihomo-alpha': { core: 'mihomo', dcore: '3' }, + 'smart': { core: 'mihomo', dcore: '1' }, + 'singbox-stable': { core: 'singbox', dcore: '4' }, + 'singbox-alpha': { core: 'singbox', dcore: '5' } + }; + return targets[choice] || targets['mihomo-stable']; +} + +function waitForCoreTarget(target, attempts, sawRestart) { + attempts = attempts == null ? 50 : attempts; + sawRestart = !!sawRestart; + return clashoo.status().then(function (st) { + st = st || {}; + var selected = st.core_type === target.core && String(st.dcore || '') === target.dcore; + var transitioning = !st.running || st.health_status === 'stopped' || st.health_status === 'starting'; + sawRestart = sawRestart || transitioning; + if (sawRestart && selected && st.running && st.health_status !== 'stopped' && st.health_status !== 'fail') + return st; + if (selected && st.health_status === 'fail') + throw new Error(_("The new core failed to start, please check the core log")); + if (attempts <= 0) + throw new Error(_("Timeout waiting for new core to start, please check the core log")); + return new Promise(function (resolve) { + setTimeout(resolve, 600); + }).then(function () { return waitForCoreTarget(target, attempts - 1, sawRestart); }); + }); +} + +// tab persistence (hash + localStorage), shared shape with config.js so save/apply +// reloads keep the active tab instead of snapping back to the first one +function readSavedTab(key, fallback, allowed) { + var raw = ''; + if (window.location.hash) + raw = window.location.hash.replace(/^#/, ''); + if (!raw) { + try { raw = window.localStorage.getItem(key) || ''; } catch (e) {} + } + return allowed.indexOf(raw) >= 0 ? raw : fallback; +} + +function rememberTab(key, id) { + try { window.localStorage.setItem(key, id); } catch (e) {} + if (window.history && window.history.replaceState) + window.history.replaceState(null, '', '#' + id); + else + window.location.hash = id; +} + +function readBackupSliceBase64(file, start, end) { + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onload = function () { + var result = String(reader.result || ''); + var comma = result.indexOf(','); + if (comma < 0) + reject(new Error(_("Unable to read backup file"))); + else + resolve(result.substring(comma + 1)); + }; + reader.onerror = function () { + reject(reader.error || new Error(_("Unable to read backup file"))); + }; + reader.readAsDataURL(file.slice(start, end)); + }); +} + +function uploadBackupChunks(file, path) { + var offset = 0; + var chunkSize = 32 * 1024; + + function next() { + if (offset >= file.size) + return Promise.resolve(); + var end = Math.min(offset + chunkSize, file.size); + var expected = end; + return readBackupSliceBase64(file, offset, end) + .then(function (data) { return callBackupImportChunk(path, offset, data); }) + .then(function (r) { + if (!r || !r.success) + throw new Error((r && r.message) || _("Upload failed")); + if (parseInt(r.offset, 10) !== expected) + throw new Error(_("Upload offset mismatch")); + offset = expected; + return next(); + }); + } + + return next(); +} + +return view.extend({ + _tab: 'kernel', + _logTab: 'plugin', + + load: function () { + return Promise.all([ + fastResolve(clashoo.getCpuArch(), 5000, ''), + fastResolve(clashoo.getLogStatus(), 1200, {}), + fastResolve(clashoo.readLog(), 1200, ''), + uci.load('clashoo'), + fastResolve(L.resolveDefault(callHostHints(), {}), 1500, {}) + ]); + }, + + render: function (data) { + var self = this; + var cpuArch = data[0] || ''; + var logStatus = data[1] || {}; + var runLog = data[2] || ''; + var hostHints = data[4] || {}; + this._hostHints = hostHints; + + if (!document.getElementById('cl-css')) { + var s = document.createElement('style'); + s.id = 'cl-css'; s.textContent = CSS; + document.head.appendChild(s); + } + if (!document.getElementById('cl-css-ext')) { + var link = document.createElement('link'); + link.id = 'cl-css-ext'; + link.rel = 'stylesheet'; + link.href = L.resource('view/clashoo/clashoo.css') + '?v=20260806a1'; + document.head.appendChild(link); + } else { + document.getElementById('cl-css-ext').href = L.resource('view/clashoo/clashoo.css') + '?v=20260806a1'; + } + + // restore last-viewed tab so save/apply reloads stay put instead of jumping to kernel + this._tab = readSavedTab('clashoo.system.tab', this._tab || 'kernel', ['kernel', 'rules', 'logs']); + rememberTab('clashoo.system.tab', this._tab); + var tabs = [ + { id: 'kernel', label: _("Core and Data") }, + { id: 'rules', label: _("Rules and Control") }, + { id: 'logs', label: _("Log") } + ]; + var tabEls = {}, panelEls = {}; + + var tabBar = E('div', { 'class': 'cl-tabs' }, + tabs.map(function (t) { + var el = E('div', { + 'class': 'cl-tab' + (self._tab === t.id ? ' active' : ''), + click: function () { + Object.keys(tabEls).forEach(function (k) { + tabEls[k].className = 'cl-tab' + (k === t.id ? ' active' : ''); + panelEls[k].className = 'cl-panel' + (k === t.id ? ' active' : ''); + }); + self._tab = t.id; + rememberTab('clashoo.system.tab', t.id); + } + }, t.label); + tabEls[t.id] = el; + return el; + }) + ); + + var kernelPanel = E('div', { 'class': 'cl-panel' + (this._tab === 'kernel' ? ' active' : ''), id: 'cl-panel-kernel' }); + panelEls['kernel'] = kernelPanel; + this._buildComponentUpdatePanel(kernelPanel, cpuArch); + this._buildBackupPanel(kernelPanel); + this._buildKernelPanel(kernelPanel, cpuArch); + + var rulesPanel = E('div', { 'class': 'cl-panel' + (this._tab === 'rules' ? ' active' : ''), id: 'cl-panel-rules' }); + panelEls['rules'] = rulesPanel; + this._buildRulesForm(rulesPanel); + + var logsPanel = E('div', { 'class': 'cl-panel' + (this._tab === 'logs' ? ' active' : ''), id: 'cl-panel-logs' }, + this._buildLogsPanel(runLog) + ); + panelEls['logs'] = logsPanel; + + this._tabEls = tabEls; + this._panelEls = panelEls; + poll.add(L.bind(this._pollLogs, this), 8); + + return E('div', { 'class': 'cl-wrap clashoo-container cl-system-page cl-form-page ' + getThemeClass() }, [tabBar, kernelPanel, rulesPanel, logsPanel]); + }, + + _detectMihomoArch: function (raw) { + if (!raw) return ''; + if (raw === 'x86_64') return 'amd64-compatible'; + if (/^aarch64/.test(raw)) return 'arm64'; + if (/^armv7|^arm_cortex-a[7-9]|^arm_cortex-a1[0-9]/.test(raw)) return 'armv7'; + if (/^armv6|^arm_cortex-a[56]/.test(raw)) return 'armv6'; + if (/^arm/.test(raw)) return 'armv5'; + if (/^i[3-6]86/.test(raw)) return '386'; + if (/^mips64el/.test(raw)) return 'mips64le'; + if (/^mips64/.test(raw)) return 'mips64'; + if (/^mipsel/.test(raw)) return 'mipsle'; + if (/^mips/.test(raw)) return 'mips'; + return ''; + }, + + _buildComponentUpdatePanel: function (container, cpuArch) { + var self = this; + this._compCpuArch = cpuArch || ''; + this._compVariant = this._compVariant || {}; + this._compLatest = this._compLatest || {}; + var listEl = E('div', { 'class': 'cl-component-list' }); + var advListEl = E('div', { 'class': 'cl-component-list' }); + this._compAdvListEl = advListEl; + var logEl = E('div', { 'class': 'cl-component-log', style: 'display:none' }, ''); + this._compLogEl = logEl; + var archWrap = E('div', { 'class': 'cl-component-arch' }); + this._compArchWrap = archWrap; + var refreshBtn = E('button', { + 'class': 'btn cbi-button cbi-button-neutral', + click: function (ev) { + ev.preventDefault(); + refreshBtn.disabled = true; + refreshBtn.textContent = _("Checking…"); + self._refreshComponentUpdatePanel(listEl, logEl, true); + } + }, _("Check Updates")); + this._compRefreshBtn = refreshBtn; + + /* advanced: core/data, collapsed by default to keep page compact. + CPU arch stays outside — it decides core binary arch */ + var advWrap = E('div', { 'class': 'cl-component-adv cl-closed' }, [ + E('div', { 'class': 'cl-component-adv-bar' }, [ + E('span', {}, _("Advanced Settings (core / data components)")), + E('span', { 'class': 'cl-component-adv-chevron' }, '›') + ]), + E('div', { 'class': 'cl-component-adv-body' }, [advListEl]) + ]); + advWrap.firstChild.addEventListener('click', function () { + advWrap.classList.toggle('cl-closed'); + }); + + /* wrap in cl-component-map, reuse LuCI .cbi-map container + 浅色出现灰托盘、暗色满宽对齐,与下方表单卡片同构 */ + container.appendChild(E('div', { 'class': 'cl-component-map' }, [ + E('div', { 'class': 'cl-component-card' }, [ + E('div', { 'class': 'cl-component-head' }, [ + E('div', {}, [ + E('h4', {}, _("Component Updates")), + E('div', { 'class': 'cl-component-sub' }, _("Update components individually. Click Check Updates to refresh versions.")) + ]), + refreshBtn + ]), + archWrap, + listEl, + advWrap, + logEl + ]) + ])); + + this._refreshComponentUpdatePanel(listEl, logEl, false); + }, + + /* 备份与还原:折腾内核/配置前的兜底。导出当前配置为压缩包,避免 RPC 大包限制; + 还原默认配置则把 UCI 设置重置为出厂值(订阅/配置文件保留)。仅含配置,不含内核。 */ + _buildBackupPanel: function (container) { + var exportBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral' }, _("Export Backup")); + exportBtn.addEventListener('click', function (ev) { + ev.preventDefault(); + exportBtn.disabled = true; + var orig = exportBtn.textContent; + exportBtn.textContent = _("Exporting…"); + var done = function () { exportBtn.disabled = false; exportBtn.textContent = orig; }; + var exportPath = ''; + callBackupExportPrepare().then(function (r) { + if (!r || !r.success) { + throw new Error((r && r.message) || _("Unknown error")); + } + exportPath = r.path; + return fs.read_direct(r.path, 'blob').then(function (blob) { + return { blob: blob, filename: r.filename || 'clashoo-backup.tar.gz' }; + }); + }).then(function (download) { + var url = URL.createObjectURL(download.blob); + var a = E('a', { href: url, download: download.filename }); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(function () { URL.revokeObjectURL(url); }, 1000); + clashoo.toast(_("Backup exported"), { kind: 'ok' }); + }).catch(function (e) { + clashoo.toast(_("Export failed: ") + (e.message || e), { kind: 'err' }); + }).finally(function () { + done(); + if (exportPath) + callBackupExportCleanup(exportPath).catch(function () {}); + }); + }); + + var importInput = E('input', { type: 'file', accept: '.tar.gz,.json', style: 'display:none' }); + var importBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral' }, _("Import Restore")); + importBtn.addEventListener('click', function (ev) { + ev.preventDefault(); + importInput.value = ''; + importInput.click(); + }); + importInput.addEventListener('change', function () { + var file = importInput.files && importInput.files[0]; + if (!file) + return; + var lower = String(file.name || '').toLowerCase(); + var maxSize = /\.json$/.test(lower) ? 8 * 1024 * 1024 + : /\.tar\.gz$/.test(lower) ? 32 * 1024 * 1024 : 0; + if (!maxSize) { + clashoo.toast(_("Import failed: ") + _("Only .tar.gz and legacy .json backup files are supported"), { kind: 'err' }); + importInput.value = ''; + return; + } + if (file.size > maxSize) { + clashoo.toast(_("Import failed: ") + _("Backup file is too large"), { kind: 'err' }); + importInput.value = ''; + return; + } + if (!confirm(_("Importing will overwrite all current configuration and settings with the backup. Existing subscriptions/configuration files will be replaced. Continue?"))) { + importInput.value = ''; + return; + } + importBtn.disabled = true; + var uploadPath = ''; + callBackupImportPrepare().then(function (r) { + if (!r || !r.success) + throw new Error((r && r.message) || _("Unknown error")); + uploadPath = r.path; + return uploadBackupChunks(file, uploadPath); + }).then(function () { + return callBackupImportFile(file.name, uploadPath); + }).then(function (r) { + if (r && r.success) + clashoo.toast(r.message || _("Backup restored, restarting service"), { kind: 'ok' }); + else + throw new Error((r && r.message) || _("Unknown error")); + }).catch(function (err) { + clashoo.toast(_("Import failed: ") + (err.message || err), { kind: 'err' }); + }).finally(function () { + if (uploadPath) + callBackupImportCleanup(uploadPath).catch(function () {}); + importBtn.disabled = false; + importInput.value = ''; + }); + }); + + var resetBtn = E('button', { 'class': 'btn cbi-button cbi-button-negative' }, _("Restore Defaults")); + resetBtn.addEventListener('click', function (ev) { + ev.preventDefault(); + if (!confirm(_("Restore Clashoo settings to factory defaults (subscriptions and configuration files are kept). Continue?"))) + return; + resetBtn.disabled = true; + var orig = resetBtn.textContent; + resetBtn.textContent = _("Restoring…"); + var done = function () { resetBtn.disabled = false; resetBtn.textContent = orig; }; + callBackupReset().then(function (r) { + done(); + if (r && r.success) + clashoo.toast(r.message || _("Factory defaults restored"), { kind: 'ok' }); + else + clashoo.toast(_("Restore failed: ") + ((r && r.message) || _("Unknown error")), { kind: 'err' }); + }).catch(function (e) { + done(); + clashoo.toast(_("Restore failed: ") + (e.message || e), { kind: 'err' }); + }); + }); + + container.appendChild(E('div', { 'class': 'cl-component-map' }, [ + E('div', { 'class': 'cl-component-card' }, [ + E('div', { 'class': 'cl-component-head' }, [ + E('div', {}, [ + E('h4', {}, _("Backup and Restore")), + E('div', { 'class': 'cl-component-sub' }, [ + _("Export a backup before changing cores or configuration, so it can be restored if needed. Includes configuration and settings only, not cores."), + E('br'), + E('strong', {}, _("Backup files contain subscription and node credentials. Store them securely.")) + ]) + ]) + ]), + importInput, + E('div', { 'class': 'cl-backup-actions' }, [exportBtn, importBtn, resetBtn]) + ]) + ])); + }, + + _replaceChildren: function (node, children) { + while (node.firstChild) + node.removeChild(node.firstChild); + children.forEach(function (child) { if (child) node.appendChild(child); }); + }, + + /* latest map key: mihomo/sing-box with variant; smart fixed; pkg by id */ + _compLatestKey: function (comp, variant) { + if (comp.id === 'smart') return 'smart'; + if (comp.variant) return comp.id + '_' + (variant || 'stable'); + return comp.id; + }, + + _compNorm: function (s) { + return String(s || '').toLowerCase().replace(/^v/, '').replace(/~/g, '.'); + }, + + _compVariantOf: function (comp) { + if (!comp.variant) return ''; + if (this._compVariant[comp.id]) return this._compVariant[comp.id]; + var inst = String(comp.installed_version || '').toLowerCase(); + return /(?:alpha|beta|rc|pre)/.test(inst) ? 'alpha' : 'stable'; + }, + + _compInstalledVersion: function (comp, variant) { + if (comp && comp.installed_versions && comp.installed_versions[variant]) + return comp.installed_versions[variant]; + if (comp && comp.variant) + return ''; + return (comp && comp.installed_version) || ''; + }, + + /* install != latest = updatable (alpha compared by hash)*/ + _compUpdatable: function (comp, latestMap) { + if (!comp) return false; + /* lgbm has no version; compare local vs remote model sha256 (kind=data skips generic path) */ + if (comp.id === 'lgbm') { + var rem = latestMap.lgbm, ins = comp.installed_version; + return !!(rem && ins && this._compNorm(rem) !== this._compNorm(ins)); + } + if (comp.kind === 'data') return false; + var variant = this._compVariantOf(comp); + var inst = this._compInstalledVersion(comp, variant); + if (!inst || inst === _("Not installed") || inst === _("Unknown")) return false; + var latest = latestMap[this._compLatestKey(comp, variant)]; + if (!latest) return false; + return this._compNorm(latest) !== this._compNorm(inst); + }, + + _componentStatusText: function (comp, globalRunning) { + if (comp.status === 'running') + return comp.message || _("Updating"); + if (comp.status === 'success') + return comp.message || _("Completed"); + if (comp.status === 'failed') + return comp.message || _("Failed"); + if (globalRunning) + return _("Wait for the current task to complete"); + return ''; + }, + + /* CPU arch: auto-detected + manual override (download_core UCI)*/ + _renderComponentArchRow: function (arch) { + var self = this; + arch = arch || {}; + var sys = arch.system || _("Unknown"); + var detected = this._detectMihomoArch(this._compCpuArch || sys) || ''; + var cur = arch.download_core || detected || ''; + var archList = ['amd64-compatible', 'amd64-v1', 'amd64-v2', 'amd64-v3', 'arm64', 'armv7', 'armv6', 'armv5', '386', 'mips', 'mipsle', 'mips64', 'mips64le']; + var opts = archList.map(function (a) { + return E('option', { value: a, selected: a === cur ? '' : null }, a); + }); + if (!cur) + opts.unshift(E('option', { value: '', selected: '' }, _("Not detected, please select"))); + var sel = E('select', { 'class': 'cl-component-arch-sel' }, opts); + sel.addEventListener('change', function () { + uci.set('clashoo', 'config', 'download_core', sel.value); + uci.save() + .then(function () { return clashoo.commitConfig(); }) + .then(function () { return clearClashooDirty(); }) + .then(function () { clashoo.toast(_("CPU architecture set to ") + sel.value, { kind: 'info' }); }) + .catch(function () {}); + /* re-render row: show hint when mismatched */ + if (self._compArchWrap) + self._replaceChildren(self._compArchWrap, + [self._renderComponentArchRow({ system: sys, download_core: sel.value })]); + }); + var row = [ + E('span', { 'class': 'cl-component-arch-label' }, _("CPU Architecture")), + E('span', { 'class': 'cl-component-arch-auto' }, _("Auto-detected")), + sel + ]; + /* no hint when auto-detected matches; only show when manual choice differs */ + if (detected && cur !== detected) + row.push(E('span', { 'class': 'cl-component-arch-hint' }, _("Recommended ") + detected + _(" (auto-detected by device)"))); + else if (!detected && !cur) + row.push(E('span', { 'class': 'cl-component-arch-hint' }, _("Could not detect the device architecture; select it manually before updating the core."))); + return E('div', { 'class': 'cl-component-arch-row' }, row); + }, + + _renderComponentRow: function (comp, globalRunning, latestMap, listEl, logEl) { + var self = this; + var statusText = this._componentStatusText(comp, globalRunning); + var inst = comp.installed_version || ''; + var instStable = !/(?:alpha|beta|rc|pre)/i.test(inst); + + /* mihomo / sing-box:行内稳定 / Alpha 切换 */ + var variant = ''; + var variantBox = null; + if (comp.variant) { + if (!self._compVariant[comp.id]) + self._compVariant[comp.id] = instStable ? 'stable' : 'alpha'; + variant = self._compVariant[comp.id]; + var mkV = function (v, label) { + var active = variant === v; + var b = E('button', { 'class': 'cl-comp-var' + (active ? ' on' : '') }, (active ? '✓ ' : '') + label); + b.addEventListener('click', function (ev) { + ev.preventDefault(); + self._compVariant[comp.id] = v; + self._refreshComponentUpdatePanel(listEl, logEl, false); + }); + return b; + }; + variantBox = E('div', { 'class': 'cl-comp-var-box' }, [mkV('stable', _("Stable")), mkV('alpha', 'Alpha')]); + inst = this._compInstalledVersion(comp, variant) || _("Not installed"); + } + var latest = latestMap[this._compLatestKey(comp, variant || this._compVariantOf(comp))]; + if (!statusText && latest) + statusText = _("Remote: %s").replace('%s', latest); + + /* 绿色「可更新」徽标:最新版与已装不一致时显示 */ + var badge = this._compUpdatable(comp, latestMap) + ? E('span', { 'class': 'cl-comp-updatable' }, _("Update available")) + : null; + + var btn = E('button', { + 'class': 'btn cbi-button cbi-button-apply', + disabled: globalRunning ? 'disabled' : null, + click: function (ev) { + ev.preventDefault(); + btn.disabled = true; + btn.textContent = _("Submitting"); + if (self._compLogEl) self._compLogEl.style.display = ''; + clashoo.componentUpdate(comp.id, variant).then(function (res) { + if (res && res.success) + clashoo.toast(_("Component update task submitted"), { kind: 'info' }); + else + clashoo.toast((res && res.message) || _("Failed to submit component update task"), { kind: 'error' }); + setTimeout(function () { self._refreshComponentUpdatePanel(listEl, logEl, false); }, 700); + }); + } + }, comp.status === 'running' ? _("Updating") : _("Update")); + + var verChildren = [E('span', {}, _("Current: %s").replace('%s', inst || _("Unknown")) + ' ')]; + if (badge) verChildren.push(badge); + var verBlock = [E('div', {}, verChildren)]; + if (variantBox) verBlock.push(variantBox); + + return E('div', { 'class': 'cl-component-row' }, [ + E('div', {}, [ + E('div', { 'class': 'cl-component-name' }, comp.name || comp.id), + E('div', { 'class': 'cl-component-desc' }, comp.description || '') + ]), + E('div', { 'class': 'cl-component-version' }, verBlock), + E('div', { 'class': 'cl-component-status cl-st-' + (comp.status || 'idle') }, statusText), + btn + ]); + }, + + _refreshComponentUpdatePanel: function (listEl, logEl, doCheck) { + var self = this; + if (this._componentPollTimer) { + clearTimeout(this._componentPollTimer); + this._componentPollTimer = null; + } + + clashoo.componentStatus().then(function (data) { + var comps = data.components || []; + if (self._compArchWrap) + self._replaceChildren(self._compArchWrap, [self._renderComponentArchRow(data.arch)]); + + var paint = function () { + var mainComps = [], advComps = []; + comps.forEach(function (comp) { + (COMPONENT_MAIN_IDS[comp.id] ? mainComps : advComps).push(comp); + }); + var render = function (comp) { + return self._renderComponentRow(comp, !!data.running, self._compLatest || {}, listEl, logEl); + }; + self._replaceChildren(listEl, mainComps.map(render)); + if (self._compAdvListEl) + self._replaceChildren(self._compAdvListEl, advComps.map(render)); + }; + paint(); + logEl.textContent = data.log || data.last_log || _("No component update logs yet"); + + if (data.running) { + /* 有任务运行:展开日志,取消收起计时 */ + if (self._compLogHideTimer) { + clearTimeout(self._compLogHideTimer); + self._compLogHideTimer = null; + } + logEl.style.display = ''; + self._componentPollTimer = setTimeout(function () { + self._refreshComponentUpdatePanel(listEl, logEl, false); + }, 2000); + } else { + /* 任务结束:日志若展开着,8 秒后自动收起 */ + if (logEl.style.display !== 'none' && !self._compLogHideTimer) { + self._compLogHideTimer = setTimeout(function () { + logEl.style.display = 'none'; + self._compLogHideTimer = null; + }, 8000); + } + if (doCheck) { + clashoo.componentCheckUpdates().then(function (r) { + self._compLatest = (r && r.latest) || {}; + paint(); + var n = comps.reduce(function (acc, c) { + return acc + (self._compUpdatable(c, self._compLatest) ? 1 : 0); + }, 0); + if (self._compRefreshBtn) { + self._compRefreshBtn.disabled = false; + self._compRefreshBtn.textContent = _("Check Updates"); + } + clashoo.toast(n > 0 ? _("%d updates available").replace('%d', String(n)) : _("All components are up to date"), + { kind: n > 0 ? 'info' : 'success' }); + }).catch(function () { + if (self._compRefreshBtn) { + self._compRefreshBtn.disabled = false; + self._compRefreshBtn.textContent = _("Check Updates"); + } + clashoo.toast(_("Failed to check updates, please check the network"), { kind: 'error' }); + }); + } + } + }); + }, + + _buildKernelPanel: function (container, cpuArch) { + var self = this; + var detectedArch = this._detectMihomoArch(cpuArch); + var m = new form.Map('clashoo', '', ''); + var s, o; + var originalCoreChoice = coreChoiceFromConfig( + uci.get('clashoo', 'config', 'core_type') || 'mihomo', + uci.get('clashoo', 'config', 'dcore') || '2' + ); + var selectedCoreChoice = originalCoreChoice; + + s = m.section(form.NamedSection, 'config', 'clashoo', _("Backend Core")); + s.addremove = false; + o = s.option(form.ListValue, '_core_choice', _("Core Type")); + o.value('mihomo-stable', _("mihomo Stable")); + o.value('mihomo-alpha', 'mihomo Alpha'); + o.value('smart', 'Smart'); + o.value('singbox-stable', _("sing-box Stable")); + o.value('singbox-alpha', 'sing-box Alpha'); + o.cfgvalue = function () { return originalCoreChoice; }; + o.write = function (sectionId, value) { selectedCoreChoice = value; }; + o.remove = function () {}; + o.description = ''; + var coreChoiceOption = o; + + s = m.section(form.NamedSection, 'config', 'clashoo', _("Dashboard Settings")); + s.addremove = false; + o = s.option(form.Value, 'dash_port', _("Dashboard Port")); + o.placeholder = '9090'; + o = s.option(form.Value, 'dash_pass', _("Access Secret")); + o.placeholder = 'clashoo'; + o = s.option(form.ListValue, 'dashboard_panel', _("Panel UI")); + ['metacubexd','yacd','zashboard','razord'].forEach(function(p){ o.value(p,p); }); + + s = m.section(form.NamedSection, 'config', 'clashoo', _("Core Download")); + s.addremove = false; + o = s.option(form.Value, 'core_mirror_prefix', _("GitHub acceleration prefix")); + o.placeholder = 'https://gh-proxy.com/'; + o.description = _("When downloading core/data components, this prefix will be used first (a slash is required at the end). Leave blank to use the built-in gh-proxy.com and ghfast.top alternative sources."); + o.rmempty = true; + + m.render().then(function (node) { + decorateSystemForm(node); + enhanceDashPasswordField(node); + container.appendChild(node); + + var saveKernelForm = function (applyNow) { + var choice = coreChoiceOption.formvalue('config') || selectedCoreChoice || originalCoreChoice; + var target = coreChoiceTarget(choice); + var changed = choice !== originalCoreChoice; + + return m.save() + .then(function () { return clashoo.commitConfig(); }) + .then(function () { + if (applyNow) + return clashoo.setCore(target.core, target.dcore, 'apply'); + if (changed) + return clashoo.setCore(target.core, target.dcore, 'save'); + return { success: true }; + }) + .then(function (res) { + if (!res || res.success === false) + throw new Error((res && res.message) || _("Failed to save core configuration")); + if (applyNow && res.restarting) + return waitForCoreTarget(target).then(function () { return res; }); + return res; + }) + .then(function (res) { + return clearClashooDirty().then(function () { return res; }); + }) + .then(function (res) { + var msg; + if (!applyNow) + msg = changed ? _("Core type saved, takes effect on next start or restart") : _("Configuration saved"); + else if (res.restarting) + msg = _("Configuration saved, switched and started selected core"); + else + msg = _("Configuration saved, service remains stopped"); + ui.addNotification(null, E('p', msg)); + window.setTimeout(function () { location.reload(); }, 300); + }); + }; + + container.appendChild(E('div', { 'class': 'cl-save-bar' }, [ + E('button', { 'class': 'cbi-button', click: function () { + saveKernelForm(false) + .catch(function (e) { ui.addNotification(null, E('p', _("Save failed: ") + (e.message || e))); }); + }}, _("Save Configuration")), + E('button', { 'class': 'btn cbi-button-action', click: function () { + saveKernelForm(true) + .catch(function (e) { ui.addNotification(null, E('p', _("Operation failed: ") + (e.message || e))); }); + }}, _("Apply Configuration")) + ])); + }); + }, + + _buildRulesForm: function (container) { + var m = new form.Map('clashoo', '', ''); + var s, o; + + s = m.section(form.NamedSection, 'config', 'clashoo', _("Bypass Rules")); + s.addremove = false; + o = s.option(form.Flag, 'bypass_china', _("Bypass China IPv4")); + o.default = '0'; + o.rmempty = false; + + o = s.option(form.Flag, 'bypass_china_ipv6', _("Bypass China IPv6")); + o.default = '0'; + o.rmempty = false; + o.cfgvalue = function (section_id) { + var value = uci.get('clashoo', section_id, 'bypass_china_ipv6'); + return (value != null) ? value : uci.get('clashoo', section_id, 'bypass_china'); + }; + + o = s.option(form.ListValue, 'bypass_port_mode', _("Bypass Ports")); + o.value('all', _("All Ports")); + o.value('common', _("Common Ports")); + o.value('custom', _("Custom")); + o.default = 'all'; + + o = s.option(form.Value, 'bypass_port_custom', _("Custom Ports")); + o.depends('bypass_port_mode', 'custom'); + o.placeholder = '22,53,80,443,8080,8443'; + o.datatype = 'string'; + o.rmempty = true; + + o = s.option(form.Flag, 'sniffer_streaming', _("Sniffer (streaming compatibility)")); + o.default = '1'; + o.rmempty = false; + o.description = _("When enabled, sniffer configuration is injected automatically to improve streaming domain detection and routing stability."); + + s = m.section(form.TableSection, 'lan_acl', _("Access Control")); + s.addremove = true; + s.anonymous = true; + s.sortable = true; + s.description = _("Rules are matched from top to bottom. Unmatched devices use DNS takeover and proxy by default. A rule with IPv4, IPv6 and MAC all empty matches every device."); + + var hints = this._hostHints || {}; + var hostOptions = [], host6Options = [], macOptions = []; + var seen = {}, seen6 = {}; + Object.keys(hints).forEach(function (mac) { + var h = hints[mac] || {}; + var macU = mac.toUpperCase(); + var addrs = h.ipaddrs || (h.ipv4 ? [h.ipv4] : []); + var addrs6 = h.ip6addrs || []; + var label = h.name || addrs[0] || addrs6[0] || ''; + addrs.forEach(function (ip) { + if (ip && !seen[ip]) { + seen[ip] = true; + hostOptions.push([ip, ip + ' (' + macU + ')']); + } + }); + addrs6.forEach(function (ip) { + if (ip && !seen6[ip]) { + seen6[ip] = true; + host6Options.push([ip, ip + ' (' + macU + ')']); + } + }); + macOptions.push([macU, label ? macU + ' (' + label + ')' : macU]); + }); + + o = s.option(form.Flag, 'enabled', _("Enable")); + o.default = '1'; + o.rmempty = false; + + o = s.option(form.DynamicList, 'ip', _("IPv4")); + o.placeholder = '192.168.1.100'; + o.datatype = 'ip4addr'; + hostOptions.forEach(function (kv) { o.value(kv[0], kv[1]); }); + + o = s.option(form.DynamicList, 'ip6', _("IPv6")); + o.placeholder = 'fd00::100'; + o.datatype = 'ip6addr'; + host6Options.forEach(function (kv) { o.value(kv[0], kv[1]); }); + + o = s.option(form.DynamicList, 'mac', _("MAC")); + o.placeholder = '00:11:22:33:44:55'; + o.datatype = 'macaddr'; + macOptions.forEach(function (kv) { o.value(kv[0], kv[1]); }); + + o = s.option(form.Flag, 'dns', _("DNS Takeover")); + o.default = '1'; + o.rmempty = false; + + o = s.option(form.Flag, 'proxy', _("Proxy")); + o.default = '1'; + o.rmempty = false; + + s = m.section(form.NamedSection, 'config', 'clashoo', _("Automation Tasks")); + s.addremove = false; + o = s.option(form.Flag, 'auto_update', _("Regularly update rule data")); + o.description = _("Rule data includes China allowlist (direct IP ranges) and GeoIP / GeoSite databases"); + o = s.option(form.Value, 'auto_update_time', _("Update interval (hours)")); + o = s.option(form.Flag, 'auto_clear_log', _("Scheduled Log Cleanup")); + o = s.option(form.Value, 'clear_time',_("Cleanup interval (hours)")); + o = s.option(form.ListValue, 'geoip_source', _("GeoIP Source")); + o.value('3', 'Loyalsoldier'); o.value('5', 'v2fly'); o.value('4', _("Custom")); + o.default = '3'; + o = s.option(form.Value, 'geoip_mmdb_url', 'Country.mmdb URL'); + o.depends('geoip_source', '4'); + o.placeholder = 'https://…/Country.mmdb'; + o = s.option(form.Value, 'geosite_url', 'geosite.dat URL'); + o.depends('geoip_source', '4'); + o.placeholder = 'https://…/geosite.dat'; + o = s.option(form.Value, 'geoip_dat_url', 'geoip.dat URL'); + o.depends('geoip_source', '4'); + o.placeholder = 'https://…/geoip.dat'; + + m.render().then(function (node) { + normalizeLanAclLayout(node); + decorateSystemForm(node); + container.appendChild(node); + watchLanAclLayout(node); + container.appendChild(E('div', { 'class': 'cl-save-bar' }, [ + E('button', { 'class': 'cbi-button', click: function () { + m.save().then(function () { return clashoo.commitConfig(); }) + .then(function () { return clearClashooDirty(); }) + .then(function () { location.reload(); }) + .catch(function (e) { ui.addNotification(null, E('p', _("Save failed: ") + (e.message || e))); }); + }}, _("Save Configuration")), + E('button', { 'class': 'btn cbi-button-action', click: function () { + saveCommitApplyMaybeReload(m, _("Configuration saved and service hot-reloaded"), _("Configuration saved, service is not running")) + .catch(function (e) { ui.addNotification(null, E('p', _("Operation failed: ") + (e.message || e))); }); + }}, _("Apply Configuration")) + ])); + }); + }, + + _buildLogsPanel: function (runLog) { + var self = this; + var logTypes = [ + { id: 'plugin', label: _("Plugin Log"), read: clashoo.readLog.bind(clashoo), clear: clashoo.clearLog.bind(clashoo) }, + { id: 'core', label: _("Core Log"), read: clashoo.readCoreLog.bind(clashoo), clear: clashoo.clearCoreLog.bind(clashoo) }, + { id: 'update', label: _("Update Log"), read: clashoo.readUpdateMergedLog.bind(clashoo), clear: clashoo.clearUpdateMergedLog.bind(clashoo) } + ]; + var MAX_LINES = 5000; + + var state = { autoScroll: true, paused: false, filter: '' }; + + function esc(s) { + return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + + /* Expose helpers for _pollLogs */ + self._buildLogLine = buildLine; + self._applyLogFilter = applyFilter; + + /* parse level token; no keyword (e.g. mihomo info lines with [info] stripped) => info */ + function detectLevelToken(ln) { + var m = ln.match(/\b(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\b/i); + if (!m) return 'info'; + var l = m[1].toUpperCase(); + if (l === 'DEBUG') return 'debug'; + if (l === 'INFO') return 'info'; + if (l.startsWith('WARN')) return 'warn'; + if (l === 'FATAL' || l === 'PANIC') return 'fatal'; + return 'error'; + } + function levelClass(token) { + if (token === 'debug') return 'cl-log-debug'; + if (token === 'warn') return 'cl-log-warn'; + if (token === 'error' || token === 'fatal') return 'cl-log-error'; + return 'cl-log-info'; + } + + var MONTHS = { Jan:'01',Feb:'02',Mar:'03',Apr:'04',May:'05',Jun:'06',Jul:'07',Aug:'08',Sep:'09',Oct:'10',Nov:'11',Dec:'12' }; + /* render a single line as colored HTML */ + function buildLine(ln) { + var token = detectLevelToken(ln); + var cls = levelClass(token); + var ts = '', msg = ln; + /* core syslog: Mon DD HH:MM:SS YYYY → MM-DD HH:MM:SS */ + var cm = ln.match(/^(\w{3})\s+(\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+\d{4}\s+\S+\.(\S+)\s+\S+\[\d+\]:\s+(.*)$/); + if (cm) { ts = (MONTHS[cm[1]] || cm[1]) + '-' + cm[2].padStart(2,'0') + ' ' + cm[3]; msg = cm[5] || ln; } + /* plugin / update: MM-DD HH:MM:SS [warn] message */ + else if (/^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s/.test(ln)) { + ts = ln.substring(0,14); + msg = ln.substring(15); + /* update: MM-DD HH:MM:SS - message → strip dash */ + if (msg.indexOf(' - ') === 0) msg = msg.substring(3); + } + if (ts) { + return '
' + esc(ts) + '' + esc(msg) + '
'; + } + return '
' + esc(ln) + '
'; + } + + function renderLines(text) { + var lines = text.split('\n').filter(function(l) { return l; }); + var html = ''; + for (var i = 0; i < lines.length; i++) { + html += buildLine(lines[i]); + } + return html; + } + + function applyFilter() { + var f = state.filter; /* '' = all, else exact level: info/warn/error/fatal */ + var els = document.querySelectorAll('#cl-log-area .cl-log-line'); + for (var i = 0; i < els.length; i++) { + if (!f || els[i].getAttribute('data-level') === f) + els[i].classList.remove('cl-log-hidden'); + else + els[i].classList.add('cl-log-hidden'); + } + } + + var logTabEls = {}; + var logArea = E('div', { 'class': 'cl-log-area', id: 'cl-log-area' }); + logArea.innerHTML = runLog ? renderLines(runLog) : _("(empty)"); + + var clearBtn = null; + + function activateLogTab(id) { + var logType = logTypes.find(function (lt) { return lt.id === id; }) || logTypes[0]; + Object.keys(logTabEls).forEach(function (k) { + logTabEls[k].className = 'cl-log-tab' + (k === logType.id ? ' active' : ''); + }); + self._logTab = logType.id; + syncClearButton(); + return logType.read().then(function (content) { + logArea.innerHTML = (content && content.trim()) ? renderLines(content) : _("(empty)"); + applyFilter(); + if (state.autoScroll) logArea.scrollTop = logArea.scrollHeight; + }); + } + this._activateLogTab = activateLogTab; + + var logTabBar = E('div', { 'class': 'cl-log-tabs' }, + logTypes.map(function (lt) { + var el = E('span', { + 'class': 'cl-log-tab' + (self._logTab === lt.id ? ' active' : ''), + click: function () { activateLogTab(lt.id); } + }, lt.label); + logTabEls[lt.id] = el; + return el; + }) + ); + + var currentType = function () { + return logTypes.find(function (lt) { return lt.id === self._logTab; }) || logTypes[0]; + }; + + function syncClearButton() { + if (!clearBtn) return; + var ct = currentType(); + clearBtn.disabled = !ct.clear; + } + + /* ---- toolbar ---- */ + var cbAuto = E('input', { type: 'checkbox', checked: 'checked' }); + cbAuto.addEventListener('change', function () { state.autoScroll = cbAuto.checked; }); + + var cbPause = E('input', { type: 'checkbox' }); + cbPause.addEventListener('change', function () { state.paused = cbPause.checked; }); + + var selFilter = E('select', { 'class': 'cbi-button' }, [ + E('option', { value: '' }, _("All")), + E('option', { value: 'info' }, 'INFO'), + E('option', { value: 'warn' }, 'WARN'), + E('option', { value: 'error' }, 'ERROR'), + E('option', { value: 'fatal' }, 'FATAL') + ]); + selFilter.addEventListener('change', function () { + state.filter = selFilter.value; + applyFilter(); + }); + + var btnClearView = E('button', { 'class': 'cbi-button' }, _("Clear View")); + btnClearView.addEventListener('click', function () { logArea.innerHTML = ''; }); + + var btnDownload = E('button', { 'class': 'cbi-button' }, _("Download")); + btnDownload.addEventListener('click', function () { + var blob = new Blob([logArea.textContent || ''], { type: 'text/plain' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'clashoo-' + new Date().toISOString().replace(/[:.]/g, '-') + '.log'; + document.body.appendChild(a); a.click(); document.body.removeChild(a); + URL.revokeObjectURL(url); + }); + + clearBtn = E('button', { 'class': 'cbi-button' }, _("Clear File")); + clearBtn.addEventListener('click', function () { + var ct = currentType(); + if (!ct.clear) return; + if (!confirm(_("Clear the current log file? This cannot be undone."))) return; + ct.clear().then(function () { logArea.innerHTML = ''; }); + }); + syncClearButton(); + + var toolbar = E('div', { 'class': 'cl-log-toolbar' }, [ + E('label', {}, [cbAuto, _("Auto Scroll")]), + E('label', {}, [cbPause, _("Pause")]), + selFilter, + btnClearView, + btnDownload, + clearBtn + ]); + + return E('div', { 'class': 'cl-section cl-card cl-log-card' }, [ + E('h4', {}, _("Log")), + logTabBar, + toolbar, + logArea + ]); + }, + + _pollLogs: function () { + if (this._tab !== 'logs') return Promise.resolve(); + var self = this; + var el = document.getElementById('cl-log-area'); + if (!el) return Promise.resolve(); + var cbPause = el.parentNode.querySelector('input[type="checkbox"]:nth-of-type(2)'); + if (cbPause && cbPause.checked) return Promise.resolve(); + var logFns = { + plugin: clashoo.readLog.bind(clashoo), + core: clashoo.readCoreLog.bind(clashoo), + update: clashoo.readUpdateMergedLog.bind(clashoo) + }; + var readFn = logFns[this._logTab] || logFns.plugin; + return readFn().then(function (content) { + if (!content || !content.trim()) { el.innerHTML = _("(empty)"); return; } + var html = ''; + var lines = content.split('\n').filter(function(l) { return l; }); + for (var i = 0; i < lines.length; i++) { + html += self._buildLogLine(lines[i]); + } + el.innerHTML = html; + self._applyLogFilter(); + var cbAuto = el.parentNode.querySelector('input[type="checkbox"]:first-of-type'); + if (!cbAuto || cbAuto.checked) el.scrollTop = el.scrollHeight; + }); + }, + + _switchTab: function (id) { + var tabEls = this._tabEls || {}; + var panelEls = this._panelEls || {}; + Object.keys(tabEls).forEach(function (k) { + tabEls[k].className = 'cl-tab' + (k === id ? ' active' : ''); + panelEls[k].className = 'cl-panel' + (k === id ? ' active' : ''); + }); + this._tab = id; + rememberTab('clashoo.system.tab', id); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/youtube.svg b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/youtube.svg new file mode 100644 index 000000000..18ddde453 --- /dev/null +++ b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/youtube.svg @@ -0,0 +1,4 @@ + + + + diff --git a/luci-app-clashoo/po/templates/clashoo.pot b/luci-app-clashoo/po/templates/clashoo.pot new file mode 100644 index 000000000..d31f14df6 --- /dev/null +++ b/luci-app-clashoo/po/templates/clashoo.pot @@ -0,0 +1,1572 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" + +msgid " (auto-detected by device)" +msgstr "" + +msgid " (expired)" +msgstr "" + +msgid " (refreshing)" +msgstr "" + +msgid " candidates" +msgstr "" + +msgid " days)" +msgstr "" + +msgid " files)" +msgstr "" + +msgid " groups" +msgstr "" + +msgid " latency test failed: " +msgstr "" + +msgid " saved" +msgstr "" + +msgid " seconds" +msgstr "" + +msgid " switch error" +msgstr "" + +msgid " switch failed: " +msgstr "" + +msgid " update succeeded" +msgstr "" + +msgid " updated" +msgstr "" + +msgid "$1 updated" +msgstr "" + +msgid "(empty)" +msgstr "" + +msgid ", click Start to take effect" +msgstr "" + +msgid ", failed " +msgstr "" + +msgid ", skipped " +msgstr "" + +msgid ", switched to active configuration" +msgstr "" + +msgid ", switching to active configuration..." +msgstr "" + +msgid ", unchanged " +msgstr "" + +msgid "; updated " +msgstr "" + +msgid "(empty)" +msgstr "" + +msgid "China traffic bypasses the core at firewall level; DNS behavior is slightly weaker. Choose as needed." +msgstr "" + +msgid "Fast resolution and accurate routing; China routing is handled by the core." +msgstr "" + +msgid "Note: Proxy node domains still use direct DNS for startup resolution, not for normal client domain queries.
" +msgstr "" + +msgid "Prerequisite: The configuration must include transparent proxying (TUN auto-route or TProxy inbound). Restart after switching." +msgstr "" + +msgid "Warning: After enabling, IPv6 sites can only be accessed over IPv4; pure IPv6 networks may lose connectivity." +msgstr "" + +msgid "Access Check" +msgstr "" + +msgid "Access Control" +msgstr "" + +msgid "Access Secret" +msgstr "" + +msgid "Actions" +msgstr "" + +msgid "Active" +msgstr "" + +msgid "Advanced DNS" +msgstr "" + +msgid "Advanced Settings (core / data components)" +msgstr "" + +msgid "For providers that directly offer sing-box JSON subscriptions, or links already converted by external tools. After fetching, click Update on the corresponding item in the Configuration Files tab to fetch the latest configuration again." +msgstr "" + +msgid "All" +msgstr "" + +msgid "All components are up to date" +msgstr "" + +msgid "All Devices" +msgstr "" + +msgid "All Ports" +msgstr "" + +msgid "Allow LAN Access" +msgstr "" + +msgid "Allowlist" +msgstr "" + +msgid "Allowlist (only listed items use fake-IP)" +msgstr "" + +msgid "Alpha" +msgstr "" + +msgid "Already up to date" +msgstr "" + +msgid "Apply Configuration" +msgstr "" + +msgid "Applying…" +msgstr "" + +msgid "ASN Priority" +msgstr "" + +msgid "Auto Scroll" +msgstr "" + +msgid "Auto setup completed." +msgstr "" + +msgid "Auto Update Model" +msgstr "" + +msgid "Auto-detected" +msgstr "" + +msgid "Automatically choose available DNS and update only upstream servers; split policies and advanced settings remain unchanged." +msgstr "" + +msgid "Automatically switch Url-test and Load-balance policy groups to Smart policy groups" +msgstr "" + +msgid "Automation Tasks" +msgstr "" + +msgid "Backend Core" +msgstr "" + +msgid "Backup and Restore" +msgstr "" + +msgid "Backup file is too large" +msgstr "" + +msgid "Backup exported" +msgstr "" + +msgid "Backup exported (" +msgstr "" + +msgid "Backup files contain subscription and node credentials. Store them securely." +msgstr "" + +msgid "Backup restored, restarting service" +msgstr "" + +msgid "Basic DNS" +msgstr "" + +msgid "Block QUIC" +msgstr "" + +msgid "Blocklist" +msgstr "" + +msgid "Blocklist (listed items use real IP, default)" +msgstr "" + +msgid "Blocklist mode: enter domains or shorthand like geosite:cn. Rule mode: entries are converted to rule syntax automatically, such as GEOSITE,cn,real-ip / RULE-SET,xxx,real-ip; usually add MATCH,fake-ip at the end as fallback. geosite:cn automatically uses built-in cn.mrs acceleration and avoids loading the 10MB geosite.dat. Allowlist mode drops the shipped defaults (they mean the opposite there), so enter your own domains." +msgstr "" + +msgid "Bypass China IPv4" +msgstr "" + +msgid "Bypass China IPv6" +msgstr "" + +msgid "Bypass Ports" +msgstr "" + +msgid "Bypass Rules" +msgstr "" + +msgid "ByteDance" +msgstr "" + +msgid "Cannot connect to core API. Check runtime status." +msgstr "" + +msgid "Check and Update" +msgstr "" + +msgid "Check Updates" +msgstr "" + +msgid "Checking configuration file" +msgstr "" + +msgid "Checking environment…" +msgstr "" + +msgid "Checking…" +msgstr "" + +msgid "Cleaning mihomo network rules" +msgstr "" + +msgid "Cleaning sing-box network rules" +msgstr "" + +msgid "Cleanup failed (mihomo may not be running)" +msgstr "" + +msgid "Cleanup interval (hours)" +msgstr "" + +msgid "Clear" +msgstr "" + +msgid "Clear File" +msgstr "" + +msgid "Clear Smart Cache" +msgstr "" + +msgid "Clear the current log file? This cannot be undone." +msgstr "" + +msgid "Clear View" +msgstr "" + +msgid "Click Save after editing; switching configuration automatically restarts the service" +msgstr "" + +msgid "Collapse" +msgstr "" + +msgid "Collect node latency data for LightGBM model training" +msgstr "" + +msgid "Collect Training Data" +msgstr "" + +msgid "Common Ports" +msgstr "" + +msgid "Completed" +msgstr "" + +msgid "Component update task submitted" +msgstr "" + +msgid "Component Updates" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Configuration content appears here after selecting a file…" +msgstr "" + +msgid "Configuration file name (optional, blank auto-generates singbox.json)" +msgstr "" + +msgid "Configuration Files" +msgstr "" + +msgid "Configuration is up to date; no fixes needed" +msgstr "" + +msgid "Configuration saved" +msgstr "" + +msgid "Configuration saved and service hot-reloaded" +msgstr "" + +msgid "Configuration saved, service is not running" +msgstr "" + +msgid "Configuration saved, service remains stopped" +msgstr "" + +msgid "Configuration saved, switched and started selected core" +msgstr "" + +msgid "Connection failed" +msgstr "" + +msgid "Connections" +msgstr "" + +msgid "Convert YAML subscriptions to sing-box JSON, automatically injecting TUN transparent proxy and China direct rules. Files with the same name are overwritten; use the same name when updating." +msgstr "" + +msgid "Core and Data" +msgstr "" + +msgid "Core did not start within 15 seconds (procd is recovering)" +msgstr "" + +msgid "Core Download" +msgstr "" + +msgid "Core is not running. Start it before switching nodes." +msgstr "" + +msgid "Core Log" +msgstr "" + +msgid "Core Only (advanced)" +msgstr "" + +msgid "Core Switch" +msgstr "" + +msgid "Core switch failed" +msgstr "" + +msgid "Core Type" +msgstr "" + +msgid "Core type saved, takes effect on next start or restart" +msgstr "" + +msgid "Core verification failed. Check System → Core." +msgstr "" + +msgid "Correct routing by forcing a domain or IP to Direct or Proxy. These rules take priority over subscription rules and apply to both mihomo and sing-box." +msgstr "" + +msgid "CPU Architecture" +msgstr "" + +msgid "CPU architecture set to " +msgstr "" + +msgid "curl or wget not found" +msgstr "" + +msgid "Current configuration has no manually switchable proxy groups" +msgstr "" + +msgid "Current configuration has no recorded subscription URL" +msgstr "" + +msgid "Current configuration is a custom file; no subscription update needed" +msgstr "" + +msgid "Current version: " +msgstr "" + +msgid "Current: %s" +msgstr "" + +msgid "Custom" +msgstr "" + +msgid "Custom Ports" +msgstr "" + +msgid "Custom Routing Rules" +msgstr "" + +msgid "Custom User-Agent" +msgstr "" + +msgid "Customize..." +msgstr "" + +msgid "Dashboard" +msgstr "" + +msgid "Dashboard Port" +msgstr "" + +msgid "Dashboard Settings" +msgstr "" + +msgid "Default" +msgstr "" + +msgid "Degraded" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Delete " +msgstr "" + +msgid "Direct" +msgstr "" + +msgid "Direct Domain Resolver" +msgstr "" + +msgid "Disable QUIC GSO" +msgstr "" + +msgid "Disabling DNS cache" +msgstr "" + +msgid "DNS Address" +msgstr "" + +msgid "DNS Auto Setup" +msgstr "" + +msgid "DNS auto setup applied, service is restarting" +msgstr "" + +msgid "DNS auto setup failed" +msgstr "" + +msgid "DNS auto setup failed: " +msgstr "" + +msgid "DNS auto setup saved, service is not running" +msgstr "" + +msgid "DNS configuration saved and service hot-reloaded" +msgstr "" + +msgid "DNS configuration saved, service is not running" +msgstr "" + +msgid "DNS Listen Port" +msgstr "" + +msgid "DNS Policy" +msgstr "" + +msgid "DNS queries follow the routing rules, so overseas domains resolve through the proxy instead of a polluted result. Requires proxy-server-nameserver." +msgstr "" + +msgid "DNS Respect Rules" +msgstr "" + +msgid "Domain / IP" +msgstr "" + +msgid "Domain keyword (e.g. youtube)" +msgstr "" + +msgid "Domain Split DNS" +msgstr "" + +msgid "Domain suffix (e.g. google.com)" +msgstr "" + +msgid "Domestic" +msgstr "" + +msgid "Domestic Upstream DNS" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Download failed (error code $1)" +msgstr "" + +msgid "Download Subscription" +msgstr "" + +msgid "Download succeeded" +msgstr "" + +msgid "Downloaded Subscriptions" +msgstr "" + +msgid "Downloading LightGBM model..." +msgstr "" + +msgid "Downloading subscription..." +msgstr "" + +msgid "Downloading..." +msgstr "" + +msgid "Downloading…" +msgstr "" + +msgid "ECS Client Subnet" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Editing: " +msgstr "" + +msgid "Enable" +msgstr "" + +msgid "Enable DNS Module" +msgstr "" + +msgid "Enable LightGBM Model" +msgstr "" + +msgid "Enable Service" +msgstr "" + +msgid "Enhanced Mode" +msgstr "" + +msgid "Enter remote template URL, e.g. https://raw.githubusercontent.com/…/Clash.yaml" +msgstr "" + +msgid "Enter subscription URL (injected into template proxy-providers)" +msgstr "" + +msgid "Every" +msgstr "" + +msgid "Exact domain (e.g. www.google.com)" +msgstr "" + +msgid "example.com or 1.2.3.0/24" +msgstr "" + +msgid "Example: geosite:cn uses https://dns.alidns.com/dns-query; geosite:geolocation-!cn uses https://cloudflare-dns.com/dns-query." +msgstr "" + +msgid "Expand" +msgstr "" + +msgid "Expires: " +msgstr "" + +msgid "Export a backup before changing cores or configuration, so it can be restored if needed. Includes configuration and settings only, not cores." +msgstr "" + +msgid "Export Backup" +msgstr "" + +msgid "Export failed: " +msgstr "" + +msgid "Exporting…" +msgstr "" + +msgid "Factory defaults restored" +msgstr "" + +msgid "Failed" +msgstr "" + +msgid "Failed to check updates, please check the network" +msgstr "" + +msgid "Failed to save core configuration" +msgstr "" + +msgid "Failed to submit component update task" +msgstr "" + +msgid "Fake-IP Filter Domains" +msgstr "" + +msgid "Fake-IP Filter Mode" +msgstr "" + +msgid "Fake-IP Range" +msgstr "" + +msgid "Fallback" +msgstr "" + +msgid "Fallback GeoIP Filter" +msgstr "" + +msgid "Bootstrap" +msgstr "" + +msgid "Fetch and Apply" +msgstr "" + +msgid "Fetch Configuration" +msgstr "" + +msgid "Fetching…" +msgstr "" + +msgid "File Name" +msgstr "" + +msgid "File name (optional, blank for automatic name)" +msgstr "" + +msgid "File name (optional, blank to auto-generate)" +msgstr "" + +msgid "Fix Deprecated Fields" +msgstr "" + +msgid "Fix failed: " +msgstr "" + +msgid "Fixed deprecated fields: " +msgstr "" + +msgid "Force ASN lookup and prefer target ASN information when selecting nodes for a more stable experience" +msgstr "" + +msgid "Force DNS Forwarding" +msgstr "" + +msgid "Force ECS Override" +msgstr "" + +msgid "Full URL / no protocol prefix" +msgstr "" + +msgid "Generate Configuration" +msgstr "" + +msgid "Generate error: " +msgstr "" + +msgid "Generate failed: " +msgstr "" + +msgid "Generated: " +msgstr "" + +msgid "Generating…" +msgstr "" + +msgid "Generation is taking longer, refreshing to show results…" +msgstr "" + +msgid "GeoIP Source" +msgstr "" + +msgid "GeoIP update completed" +msgstr "" + +msgid "GeoIP update completed, applies after Clashoo restarts" +msgstr "" + +msgid "GeoIP update failed" +msgstr "" + +msgid "GitHub acceleration prefix" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Health check failed" +msgstr "" + +msgid "Health check in progress…" +msgstr "" + +msgid "Health check passed" +msgstr "" + +msgid "hours" +msgstr "" + +msgid "HTTP Port" +msgstr "" + +msgid "Import failed: " +msgstr "" + +msgid "Import Restore" +msgstr "" + +msgid "Importing will overwrite all current configuration and settings with the backup. Existing subscriptions/configuration files will be replaced. Continue?" +msgstr "" + +msgid "Information Input" +msgstr "" + +msgid "Initializing…" +msgstr "" + +msgid "Intercept IPv6 DNS traffic to prevent devices with hard-coded DNS from bypassing the traffic." +msgstr "" + +msgid "IP / CIDR (e.g. 8.8.8.8/32)" +msgstr "" + +msgid "IP Allowlist" +msgstr "" + +msgid "IP Blocklist" +msgstr "" + +msgid "IP rules do not trigger DNS resolution" +msgstr "" + +msgid "IPv4 DNS Hijack" +msgstr "" + +msgid "IPv4 Proxy" +msgstr "" + +msgid "IPv6 DNS Hijack" +msgstr "" + +msgid "IPv6 Proxy" +msgstr "" + +msgid "just now" +msgstr "" + +msgid "Keep the default if unsure." +msgstr "" + +msgid "LAN Control" +msgstr "" + +msgid "%d updates available" +msgstr "" + +msgid "Last check: initializing" +msgstr "" + +msgid "Last check: just now" +msgstr "" + +msgid "Last check: %d seconds ago" +msgstr "" + +msgid "Last check: %d minutes ago" +msgstr "" + +msgid "Last check: %d hours ago" +msgstr "" + +msgid "Last run: " +msgstr "" + +msgid "LightGBM model file download URL; leave blank to use the default official URL" +msgstr "" + +msgid "LightGBM model updated successfully" +msgstr "" + +msgid "Load failed" +msgstr "" + +msgid "Load-balance" +msgstr "" + +msgid "Loading…" +msgstr "" + +msgid "Local Template" +msgstr "" + +msgid "Log" +msgstr "" + +msgid "Match Rule" +msgstr "" + +msgid "Memory usage %s" +msgstr "" + +msgid "mihomo restarted successfully, please wait for servers to come online." +msgstr "" + +msgid "mihomo runs as-is for nikki/OpenClash compatibility; sing-box automatically upgrades old formats for momo/homeproxy compatibility.
" +msgstr "" + +msgid "mihomo Stable" +msgstr "" + +msgid "mihomo started successfully, please wait for servers to come online." +msgstr "" + +msgid "mihomo writes the ecs parameter to DNS URLs; sing-box writes dns.client_subnet. Leave empty to skip." +msgstr "" + +msgid "Mixed Mode" +msgstr "" + +msgid "Mixed Port" +msgstr "" + +msgid "Mode" +msgstr "" + +msgid "Model Download URL" +msgstr "" + +msgid "Model install failed: $1" +msgstr "" + +msgid "Model is already up to date" +msgstr "" + +msgid "Model not installed" +msgstr "" + +msgid "Network Stack" +msgstr "" + +msgid "Network Stack Type" +msgstr "" + +msgid "No auto setup result yet" +msgstr "" + +msgid "No component update logs yet" +msgstr "" + +msgid "No configuration file found. Import a subscription or upload a configuration on the Configuration page first." +msgstr "" + +msgid "No configuration file selected. Select or upload one on the Configuration page before starting." +msgstr "" + +msgid "No configuration files yet. Use Quick Wizard to generate one or upload a JSON file." +msgstr "" + +msgid "No proxy groups" +msgstr "" + +msgid "No subscriptions" +msgstr "" + +msgid "Node Panel ›" +msgstr "" + +msgid "Node Selection" +msgstr "" + +msgid "Node Subscription" +msgstr "" + +msgid "Node weight bonus. <1 means lower priority, >1 means higher priority. Modify as needed; leave blank to skip policy-priority injection." +msgstr "" + +msgid "Not installed" +msgstr "" + +msgid "Not enabled" +msgstr "" + +msgid "Not running" +msgstr "" + +msgid "Off" +msgstr "" + +msgid "Only subscriptions with recorded source URLs are overwritten; no duplicate files are created." +msgstr "" + +msgid "Only .tar.gz and legacy .json backup files are supported" +msgstr "" + +msgid "Open Dashboard" +msgstr "" + +msgid "TUN stack is used when either TCP or UDP mode is TUN." +msgstr "" + +msgid "OpenClash is enabled; disable it first to avoid rule conflicts" +msgstr "" + +msgid "OpenClash/Nikki Loopback DNS Compatibility" +msgstr "" + +msgid "Operation failed: " +msgstr "" + +msgid "Optional compatibility for normal managed mode. At runtime, replace only proxy node resolvers that point back to the imported configuration's own DNS listener with the proxy-server-nameserver setting. Core Only mode stays verbatim; the source configuration and unrelated local DNS services remain unchanged." +msgstr "" + +msgid "Other Configuration Files (upload / overwrite output)" +msgstr "" + +msgid "Output file name (without extension, blank to auto-fill)" +msgstr "" + +msgid "Overseas" +msgstr "" + +msgid "Overseas Encrypted DNS (anti-pollution)" +msgstr "" + +msgid "Overview" +msgstr "" + +msgid "Overwrite Settings" +msgstr "" + +msgid "Panel" +msgstr "" + +msgid "Panel is downloading" +msgstr "" + +msgid "Panel switch failed: " +msgstr "" + +msgid "Panel UI" +msgstr "" + +msgid "Password" +msgstr "" + +msgid "Paste native sing-box subscription URL (direct JSON response)" +msgstr "" + +msgid "Paste subscription URL (supports vmess / vless / trojan, etc.)" +msgstr "" + +msgid "Pause" +msgstr "" + +msgid "Period for automatically fetching a new model; also controls the cron trigger frequency, default 72 hours" +msgstr "" + +msgid "Please enter a remote template URL" +msgstr "" + +msgid "Please enter a subscription URL" +msgstr "" + +msgid "Please enter an output file name" +msgstr "" + +msgid "Please select a local template file" +msgstr "" + +msgid "Plugin Log" +msgstr "" + +msgid "Policy Priority (weight bonus)" +msgstr "" + +msgid "Policy Type" +msgstr "" + +msgid "Port" +msgstr "" + +msgid "Port Settings" +msgstr "" + +msgid "Prevent DNS Leaks" +msgstr "" + +msgid "Processing…" +msgstr "" + +msgid "Protocol" +msgstr "" + +msgid "Proxy" +msgstr "" + +msgid "DNS Takeover" +msgstr "" + +msgid "IPv4" +msgstr "" + +msgid "IPv6" +msgstr "" + +msgid "MAC" +msgstr "" + +msgid "Rules are matched from top to bottom. Unmatched devices use DNS takeover and proxy by default." +msgstr "" + +msgid "Proxy Authentication" +msgstr "" + +msgid "Proxy configuration saved and service hot-reloaded" +msgstr "" + +msgid "Proxy configuration saved, service is not running" +msgstr "" + +msgid "Proxy Domain Split DNS" +msgstr "" + +msgid "Proxy Mode" +msgstr "" + +msgid "Proxy Node Domain Resolver" +msgstr "" + +msgid "Quick Wizard" +msgstr "" + +msgid "Random" +msgstr "" + +msgid "Ready · " +msgstr "" + +msgid "Ready ✓" +msgstr "" + +msgid "Recommended " +msgstr "" + +msgid "Recommended" +msgstr "" + +msgid "Recommended blank" +msgstr "" + +msgid "Redirect Port" +msgstr "" + +msgid "Refresh" +msgstr "" + +msgid "Refresh Access Check" +msgstr "" + +msgid "Regular domains only use overseas DNS and DNS connections follow proxy rules; DoT/DoQ (port 853) is blocked and IPv6 resolution is disabled. Takes effect after restart.
" +msgstr "" + +msgid "Regularly update rule data" +msgstr "" + +msgid "Reject proxied UDP 443 so some apps fall back to TCP. Try only when downloads or video stall." +msgstr "" + +msgid "Relay" +msgstr "" + +msgid "Remote Template" +msgstr "" + +msgid "Remote: %s" +msgstr "" + +msgid "Removing Cron scheduled tasks" +msgstr "" + +msgid "Requires MMDB. If enabled while the local GeoIP database is missing, the core may fail to start. Usually not needed." +msgstr "" + +msgid "Restarting client..." +msgstr "" + +msgid "Restarting Dnsmasq" +msgstr "" + +msgid "Restore Clashoo settings to factory defaults (subscriptions and configuration files are kept). Continue?" +msgstr "" + +msgid "Restore Defaults" +msgstr "" + +msgid "Restore failed: " +msgstr "" + +msgid "Restoring…" +msgstr "" + +msgid "Role" +msgstr "" + +msgid "Rule" +msgstr "" + +msgid "Rule data includes China allowlist (direct IP ranges) and GeoIP / GeoSite databases" +msgstr "" + +msgid "Rule mode (same syntax as rules)" +msgstr "" + +msgid "Rules and Control" +msgstr "" + +msgid "Run Mode" +msgstr "" + +msgid "Run mode setting failed: " +msgstr "" + +msgid "Run only the core with your imported configuration, without taking over firewall / DNS / routing.
" +msgstr "" + +msgid "Running degraded" +msgstr "" + +msgid "Running 🟢" +msgstr "" + +msgid "Runtime Status" +msgstr "" + +msgid "Sample Rate" +msgstr "" + +msgid "sample-rate, 1 = sample every latency test; increase to reduce sampling frequency" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Save Configuration" +msgstr "" + +msgid "Save failed: " +msgstr "" + +msgid "Scheduled Log Cleanup" +msgstr "" + +msgid "Scheduled Subscription Update" +msgstr "" + +msgid "Scheduled update settings saved" +msgstr "" + +msgid "Select a configuration above to edit it here" +msgstr "" + +msgid "Select local template file" +msgstr "" + +msgid "Select YAML file to upload" +msgstr "" + +msgid "Selector" +msgstr "" + +msgid "Service disabled" +msgstr "" + +msgid "Service stopped" +msgstr "" + +msgid "Setting Cron scheduled tasks and starting process supervisor..." +msgstr "" + +msgid "Setting DNS forwarder / enabling custom DNS" +msgstr "" + +msgid "Setting mihomo network rules" +msgstr "" + +msgid "Setting sing-box network rules" +msgstr "" + +msgid "Show/Hide" +msgstr "" + +msgid "sing-box Configuration Files" +msgstr "" + +msgid "sing-box core not detected; download sing-box from the System page" +msgstr "" + +msgid "sing-box Independent DNS Cache" +msgstr "" + +msgid "sing-box restarted successfully, please wait for servers to come online." +msgstr "" + +msgid "sing-box service failed to start. Check System → Log." +msgstr "" + +msgid "sing-box Stable" +msgstr "" + +msgid "sing-box started successfully, please wait for servers to come online." +msgstr "" + +msgid "Size" +msgstr "" + +msgid "Smart" +msgstr "" + +msgid "Smart cache cleared" +msgstr "" + +msgid "Smart core not detected; updating the model will not install the core" +msgstr "" + +msgid "Smart core not detected; updating the model will not install the core. Download the Smart build from the System page." +msgstr "" + +msgid "Smart Policy Auto Switch" +msgstr "" + +msgid "Smart Policy Settings" +msgstr "" + +msgid "smart-collector-size, maximum retained training samples, default 100" +msgstr "" + +msgid "Sniffer (streaming compatibility)" +msgstr "" + +msgid "SOCKS5 Port" +msgstr "" + +msgid "Stable" +msgstr "" + +msgid "Start downloading LightGBM model: $1" +msgstr "" + +msgid "Start failed: " +msgstr "" + +msgid "Start Service" +msgstr "" + +msgid "Started (" +msgstr "" + +msgid "Starting client" +msgstr "" + +msgid "Starting core…" +msgstr "" + +msgid "Starting…" +msgstr "" + +msgid "Stop Service" +msgstr "" + +msgid "Stopped (not enabled at boot)" +msgstr "" + +msgid "Stopped" +msgstr "" + +msgid "Stopping client..." +msgstr "" + +msgid "Stopping mihomo" +msgstr "" + +msgid "Stopping mihomo process supervisor" +msgstr "" + +msgid "Stopping sing-box" +msgstr "" + +msgid "Stopping sing-box process supervisor" +msgstr "" + +msgid "Stopping…" +msgstr "" + +msgid "Submission failed" +msgstr "" + +msgid "Submitting" +msgstr "" + +msgid "Subscription" +msgstr "" + +msgid "Subscription download failed" +msgstr "" + +msgid "Subscription Update" +msgstr "" + +msgid "Subscription updated successfully" +msgstr "" + +msgid "Subscription URL" +msgstr "" + +msgid "Subscription URLs (one per line)" +msgstr "" + +msgid "Switch" +msgstr "" + +msgid "Switch Profile" +msgstr "" + +msgid "Switch configuration failed: " +msgstr "" + +msgid "Switch failed: " +msgstr "" + +msgid "Switched to " +msgstr "" + +msgid "Switching to " +msgstr "" + +msgid "Syncing" +msgstr "" + +msgid "Syncing runtime state" +msgstr "" + +msgid "System" +msgstr "" + +msgid "System is missing nftables runtime" +msgstr "" + +msgid "Target" +msgstr "" + +msgid "TCP Mode" +msgstr "" + +msgid "Template Selection" +msgstr "" + +msgid "Template upload failed: " +msgstr "" + +msgid "Template uploaded: " +msgstr "" + +msgid "Test and Apply" +msgstr "" + +msgid "Test Latency" +msgstr "" + +msgid "Testing" +msgstr "" + +msgid "Testing latency and writing upstream DNS…" +msgstr "" + +msgid "The new core failed to start, please check the core log" +msgstr "" + +msgid "Time" +msgstr "" + +msgid "Timeout waiting for new core to start, please check the core log" +msgstr "" + +msgid "Total %s" +msgstr "" + +msgid "TPROXY Port" +msgstr "" + +msgid "Traffic Monitor" +msgstr "" + +msgid "Training Data Size" +msgstr "" + +msgid "Transparent Proxy" +msgstr "" + +msgid "TUN Mode" +msgstr "" + +msgid "TUN Network Stack" +msgstr "" + +msgid "Type" +msgstr "" + +msgid "UDP Mode" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Unknown error" +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update All Now" +msgstr "" + +msgid "Update available" +msgstr "" + +msgid "Update components individually. Click Check Updates to refresh versions." +msgstr "" + +msgid "Update failed" +msgstr "" + +msgid "Update failed: " +msgstr "" + +msgid "Update interval (hours)" +msgstr "" + +msgid "Update interval must be 1 to 8760 hours" +msgstr "" + +msgid "Update Log" +msgstr "" + +msgid "Update Model" +msgstr "" + +msgid "Update Subscription" +msgstr "" + +msgid "Update succeeded" +msgstr "" + +msgid "Updating" +msgstr "" + +msgid "Updating $1" +msgstr "" + +msgid "Updating all subscriptions…" +msgstr "" + +msgid "Updating…" +msgstr "" + +msgid "Unable to read backup file" +msgstr "" + +msgid "Upload" +msgstr "" + +msgid "Upload Configuration File" +msgstr "" + +msgid "Upload failed" +msgstr "" + +msgid "Upload failed: " +msgstr "" + +msgid "Upload offset mismatch" +msgstr "" + +msgid "Upload JSON Configuration" +msgstr "" + +msgid "Upload succeeded: " +msgstr "" + +msgid "Upload YAML Template" +msgstr "" + +msgid "Upstream DNS" +msgstr "" + +msgid "Url-test" +msgstr "" + +msgid "Use DNS" +msgstr "" + +msgid "Use LightGBM model to predict weights" +msgstr "" + +msgid "Used to resolve DoH/DoT/DoQ server domains; plain IP DNS is recommended." +msgstr "" + +msgid "User-Agent (UA)" +msgstr "" + +msgid "Username" +msgstr "" + +msgid "Virtual IP Ping Hijacking" +msgstr "" + +msgid "Wait for the current task to complete" +msgstr "" + +msgid "Waiting for data..." +msgstr "" + +msgid "When downloading core/data components, this prefix will be used first (a slash is required at the end). Leave blank to use the built-in gh-proxy.com and ghfast.top alternative sources." +msgstr "" + +msgid "When enabled, direct / proxy / fallback roles each use an independent DNS cache. Useful when the same domain needs different resolver chains by routing. Usually not needed." +msgstr "" + +msgid "When enabled, sniffer configuration is injected automatically to improve streaming domain detection and routing stability." +msgstr "" + +msgid "YAML Subscription Conversion" +msgstr "" + +msgid "⏳ A download task is already running..." +msgstr "" + +msgid "⏳ Fetch timed out, refreshing to show results..." +msgstr "" + +msgid "⏳ Fetching configuration..." +msgstr "" + +msgid "⏳ Starting update task..." +msgstr "" + +msgid "⏳ Submitting download task..." +msgstr "" + +msgid "✗ Fetch error: " +msgstr "" + +msgid "✗ Fetch failed: " +msgstr "" + +msgid "✗ Panel update timeout" +msgstr "" + +msgid "✗ Please enter a subscription URL" +msgstr "" + +msgid "✗ Please enter a subscription URL first" +msgstr "" + +msgid "✗ Start failed" +msgstr "" + +msgid "✗ Start failed: " +msgstr "" + +msgid "✗ Submission failed" +msgstr "" diff --git a/luci-app-clashoo/po/zh-cn/clashoo.po b/luci-app-clashoo/po/zh-cn/clashoo.po new file mode 100644 index 000000000..fe30cb1b9 --- /dev/null +++ b/luci-app-clashoo/po/zh-cn/clashoo.po @@ -0,0 +1,1579 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Project-Id-Version: luci-app-clashoo\n" +"PO-Revision-Date: 2026-07-10 09:23+0800\n" +"Last-Translator: kenzok8 \n" +"Language-Team: Chinese (Simplified)\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid " (auto-detected by device)" +msgstr "(按设备自动检测)" + +msgid " (expired)" +msgstr "(已过期)" + +msgid " (refreshing)" +msgstr "(刷新中)" + +msgid " candidates" +msgstr " 个候选" + +msgid " days)" +msgstr " 天)" + +msgid " files)" +msgstr " 个文件)" + +msgid " groups" +msgstr " 组" + +msgid " latency test failed: " +msgstr " 测速失败: " + +msgid " saved" +msgstr " 已保存" + +msgid " seconds" +msgstr " 秒" + +msgid " switch error" +msgstr " 切换异常" + +msgid " switch failed: " +msgstr " 切换失败: " + +msgid " update succeeded" +msgstr " 更新成功" + +msgid " updated" +msgstr " 已更新" + +msgid "$1 updated" +msgstr "$1 已更新" + +msgid "(empty)" +msgstr "(空)" + +msgid ", click Start to take effect" +msgstr ",点击启动后生效" + +msgid ", failed " +msgstr ",失败 " + +msgid ", skipped " +msgstr ",跳过 " + +msgid ", switched to active configuration" +msgstr ",已切换为活动配置" + +msgid ", switching to active configuration..." +msgstr ",正在切换为活动配置..." + +msgid ", unchanged " +msgstr ",未变化 " + +msgid "; updated " +msgstr ";更新 " + +msgid "(empty)" +msgstr "(空)" + +msgid "China traffic bypasses the core at firewall level; DNS behavior is slightly weaker. Choose as needed." +msgstr "大陆流量在防火墙层直接绕过核心,DNS 体验略弱。按需选择。" + +msgid "Fast resolution and accurate routing; China routing is handled by the core." +msgstr "解析快、分流准,大陆分流由内核完成。" + +msgid "Prerequisite: The configuration must include transparent proxying (TUN auto-route or TProxy inbound). Takes effect after restart." +msgstr "前提:配置自带透明代理(TUN auto-route 或 TProxy 入站),重启生效。" + +msgid "Warning: IPv6 resolution is also disabled when IPv6 Proxy is off, to keep the real address from leaking." +msgstr "注意:「IPv6 代理」关闭时会一并停用 IPv6 解析,避免真实地址泄漏。" + +msgid "Not detected, please select" +msgstr "未检测到,请手动选择" + +msgid "Could not detect the device architecture; select it manually before updating the core." +msgstr "未能识别设备架构,更新内核前请先手动选择。" + +msgid "Access Check" +msgstr "访问检查" + +msgid "Access Control" +msgstr "访问控制" + +msgid "Access Secret" +msgstr "访问密钥" + +msgid "Actions" +msgstr "操作" + +msgid "Active" +msgstr "使用中" + +msgid "Advanced DNS" +msgstr "高级 DNS" + +msgid "Advanced Settings (core / data components)" +msgstr "高级设置(内核 / 数据组件)" + +msgid "For providers that directly offer sing-box JSON subscriptions, or links already converted by external tools. After fetching, click Update on the corresponding item in the Configuration Files tab to fetch the latest configuration again." +msgstr "适用于机场直接提供 sing-box JSON 格式订阅、或已用外部工具转换好的链接。拉取后可在「配置文件」标签的对应条目点击「更新」重新拉取最新配置。" + +msgid "All" +msgstr "全部" + +msgid "All components are up to date" +msgstr "所有组件已是最新" + +msgid "All Devices" +msgstr "所有设备" + +msgid "All Ports" +msgstr "所有端口" + +msgid "Allow LAN Access" +msgstr "允许局域网连接" + +msgid "Allowlist" +msgstr "白名单" + +msgid "Allowlist (only listed items use fake-IP)" +msgstr "白名单(仅列入走 fake-IP)" + +msgid "Alpha" +msgstr "Alpha 版" + +msgid "Already up to date" +msgstr "已是最新版本" + +msgid "Apply Configuration" +msgstr "应用配置" + +msgid "Applying…" +msgstr "应用中…" + +msgid "ASN Priority" +msgstr "ASN 优先" + +msgid "Auto Scroll" +msgstr "自动滚动" + +msgid "Auto setup completed." +msgstr "自动配置已完成。" + +msgid "Auto Update Model" +msgstr "自动更新模型" + +msgid "Auto-detected" +msgstr "自动识别" + +msgid "Automatically choose available DNS and update only upstream servers; split policies and advanced settings remain unchanged." +msgstr "自动选择可用 DNS,仅更新上游服务器;分流策略和高级设置保持不变。" + +msgid "Automatically switch Url-test and Load-balance policy groups to Smart policy groups" +msgstr "自动切换 Url-test、Load-balance 策略组到 Smart 策略组" + +msgid "Automation Tasks" +msgstr "自动化任务" + +msgid "Backend Core" +msgstr "后端核心" + +msgid "Backup and Restore" +msgstr "备份与还原" + +msgid "Backup file is too large" +msgstr "备份文件过大" + +msgid "Backup exported" +msgstr "备份已导出" + +msgid "Backup exported (" +msgstr "备份已导出(" + +msgid "Backup files contain subscription and node credentials. Store them securely." +msgstr "备份文件包含订阅和节点凭据,请妥善保管。" + +msgid "Backup restored, restarting service" +msgstr "已还原备份,正在重启服务" + +msgid "Basic DNS" +msgstr "基础 DNS" + +msgid "Block QUIC" +msgstr "阻断 QUIC" + +msgid "Blocklist" +msgstr "黑名单" + +msgid "Blocklist (listed items use real IP, default)" +msgstr "黑名单(列入走真 IP,默认)" + +msgid "Blocklist mode: enter domains or shorthand like geosite:cn. Rule mode: entries are converted to rule syntax automatically, such as GEOSITE,cn,real-ip / RULE-SET,xxx,real-ip; usually add MATCH,fake-ip at the end as fallback. geosite:cn automatically uses built-in cn.mrs acceleration and avoids loading the 10MB geosite.dat. Allowlist mode drops the shipped defaults (they mean the opposite there), so enter your own domains." +msgstr "黑名单模式:填域名或 geosite:cn 简写。规则模式:条目会自动转成 GEOSITE,cn,real-ip / RULE-SET,xxx,real-ip 这种与 rules 同语法的写法,末尾通常加一条 MATCH,fake-ip 兜底。填 geosite:cn 会自动改用内置 cn.mrs 加速,免加载 10MB geosite.dat。白名单模式下随包自带的默认项含义相反,会被跳过,请自行填写需要 Fake-IP 的域名。" + +msgid "Bypass China IPv4" +msgstr "大陆 IPv4 绕过" + +msgid "Bypass China IPv6" +msgstr "大陆 IPv6 绕过" + +msgid "Bypass Ports" +msgstr "绕过端口" + +msgid "Bypass Rules" +msgstr "绕过规则" + +msgid "ByteDance" +msgstr "字节跳动" + +msgid "Cannot connect to core API. Check runtime status." +msgstr "无法连接核心 API,请检查运行状态" + +msgid "Check and Update" +msgstr "检查并更新" + +msgid "Check Updates" +msgstr "检查更新" + +msgid "Checking configuration file" +msgstr "正在检查配置文件" + +msgid "Checking environment…" +msgstr "检查环境…" + +msgid "Checking…" +msgstr "检查中…" + +msgid "Cleaning mihomo network rules" +msgstr "清理 mihomo 网络规则" + +msgid "Cleaning sing-box network rules" +msgstr "清理 sing-box 网络规则" + +msgid "Cleanup failed (mihomo may not be running)" +msgstr "清理失败(mihomo 可能未运行)" + +msgid "Cleanup interval (hours)" +msgstr "清理间隔(小时)" + +msgid "Clear" +msgstr "清理" + +msgid "Clear File" +msgstr "清空文件" + +msgid "Clear Smart Cache" +msgstr "清理 Smart 缓存" + +msgid "Clear the current log file? This cannot be undone." +msgstr "确定要清空当前日志文件吗?此操作不可恢复。" + +msgid "Clear View" +msgstr "清空显示" + +msgid "Click Save after editing; switching configuration automatically restarts the service" +msgstr "编辑后点击保存;切换配置后服务将自动重启" + +msgid "Collapse" +msgstr "折叠" + +msgid "Collect node latency data for LightGBM model training" +msgstr "收集节点延迟数据供 LightGBM 模型训练" + +msgid "Collect Training Data" +msgstr "收集训练数据" + +msgid "Common Ports" +msgstr "常用端口" + +msgid "Completed" +msgstr "已完成" + +msgid "Component update task submitted" +msgstr "组件更新任务已提交" + +msgid "Component Updates" +msgstr "组件更新" + +msgid "Configuration" +msgstr "配置" + +msgid "Configuration content appears here after selecting a file…" +msgstr "选择配置文件后内容将显示在这里…" + +msgid "Configuration file name (optional, blank auto-generates singbox.json)" +msgstr "配置文件名(选填,留空自动生成 singbox.json)" + +msgid "Configuration Files" +msgstr "配置文件" + +msgid "Configuration is up to date; no fixes needed" +msgstr "配置已是最新,无需修复" + +msgid "Configuration saved" +msgstr "配置已保存" + +msgid "Configuration saved and service hot-reloaded" +msgstr "配置已保存并热重载服务" + +msgid "Configuration saved, service is not running" +msgstr "配置已保存,服务未启动" + +msgid "Configuration saved, service remains stopped" +msgstr "配置已保存,服务保持停止" + +msgid "Configuration saved, switched and started selected core" +msgstr "配置已保存,已切换并启动所选内核" + +msgid "Connection failed" +msgstr "连接失败" + +msgid "Connections" +msgstr "连接" + +msgid "Convert YAML subscriptions to sing-box JSON, automatically injecting TUN transparent proxy and China direct rules. Files with the same name are overwritten; use the same name when updating." +msgstr "将 YAML 订阅转换为 sing-box JSON,自动注入 TUN 透明代理与大陆直连规则。同名文件直接覆盖,更新时填相同名称即可。" + +msgid "Core and Data" +msgstr "内核与数据" + +msgid "Core did not start within 15 seconds (procd is recovering)" +msgstr "内核未在 15 秒内启动(procd 自愈中)" + +msgid "Core Download" +msgstr "内核下载" + +msgid "Core is not running. Start it before switching nodes." +msgstr "核心未运行,启动后可切换节点" + +msgid "Core Log" +msgstr "核心日志" + +msgid "Core Only (advanced)" +msgstr "仅内核(进阶)" + +msgid "Core Switch" +msgstr "内核切换" + +msgid "Core switch failed" +msgstr "切换内核失败" + +msgid "Core Type" +msgstr "核心类型" + +msgid "Core type saved, takes effect on next start or restart" +msgstr "内核类型已保存,下次启动或重启后生效" + +msgid "Core verification failed. Check System → Core." +msgstr "内核校验未通过,请到「系统 → 内核」检查" + +msgid "Correct routing by forcing a domain or IP to Direct or Proxy. These rules take priority over subscription rules and apply to both mihomo and sing-box." +msgstr "纠正分流:把某个域名或 IP 强制走「直连」或「代理」,规则优先于订阅自带规则,mihomo 与 sing-box 均生效。" + +msgid "CPU Architecture" +msgstr "处理器架构" + +msgid "CPU architecture set to " +msgstr "处理器架构已设为 " + +msgid "curl or wget not found" +msgstr "未找到 curl 或 wget" + +msgid "Current configuration has no manually switchable proxy groups" +msgstr "当前配置没有可手动切换的节点组" + +msgid "Current configuration has no recorded subscription URL" +msgstr "当前配置未记录订阅链接" + +msgid "Current configuration is a custom file; no subscription update needed" +msgstr "当前配置为自定义文件,无需更新订阅" + +msgid "Current version: " +msgstr "当前版本: " + +msgid "Current: %s" +msgstr "当前:%s" + +msgid "Custom" +msgstr "自定义" + +msgid "Custom Ports" +msgstr "自定义端口" + +msgid "Custom Routing Rules" +msgstr "自定义分流规则" + +msgid "Custom User-Agent" +msgstr "自定义 User-Agent" + +msgid "Customize..." +msgstr "自定义..." + +msgid "Dashboard" +msgstr "管理面板" + +msgid "Dashboard Port" +msgstr "面板端口" + +msgid "Dashboard Settings" +msgstr "管理面板配置" + +msgid "Default" +msgstr "默认" + +msgid "Degraded" +msgstr "降级运行" + +msgid "Delete" +msgstr "删除" + +msgid "Delete " +msgstr "删除 " + +msgid "Direct" +msgstr "直连" + +msgid "Direct Domain Resolver" +msgstr "直连域名解析" + +msgid "Disable QUIC GSO" +msgstr "禁用 QUIC GSO" + +msgid "Disabling DNS cache" +msgstr "禁用 DNS 缓存" + +msgid "DNS Address" +msgstr "DNS 地址" + +msgid "DNS Auto Setup" +msgstr "DNS 自动配置" + +msgid "DNS auto setup applied, service is restarting" +msgstr "DNS 自动配置已应用,服务正在重启" + +msgid "DNS auto setup failed" +msgstr "DNS 自动配置失败" + +msgid "DNS auto setup failed: " +msgstr "DNS 自动配置失败: " + +msgid "DNS auto setup saved, service is not running" +msgstr "DNS 自动配置已保存,服务未启动" + +msgid "DNS configuration saved and service hot-reloaded" +msgstr "DNS 配置已保存并热重载服务" + +msgid "DNS configuration saved, service is not running" +msgstr "DNS 配置已保存,服务未启动" + +msgid "DNS Listen Port" +msgstr "DNS 监听端口" + +msgid "DNS Policy" +msgstr "分流解析策略" + +msgid "DNS queries follow the routing rules, so overseas domains resolve through the proxy instead of a polluted result. Requires proxy-server-nameserver." +msgstr "DNS 查询按分流规则走,境外域名经代理解析,避免拿到污染结果。需要配置节点域名解析(proxy-server-nameserver)。" + +msgid "DNS Respect Rules" +msgstr "DNS 遵循分流规则" + +msgid "Domain / IP" +msgstr "域名 / IP" + +msgid "Domain keyword (e.g. youtube)" +msgstr "域名关键词(如 youtube)" + +msgid "Domain Split DNS" +msgstr "域名分流解析" + +msgid "Domain suffix (e.g. google.com)" +msgstr "域名后缀(如 google.com)" + +msgid "Domestic" +msgstr "国内" + +msgid "Domestic Upstream DNS" +msgstr "国内上游 DNS" + +msgid "Download" +msgstr "下载" + +msgid "Download failed (error code $1)" +msgstr "下载失败(错误码 $1)" + +msgid "Download Subscription" +msgstr "下载订阅" + +msgid "Download succeeded" +msgstr "下载成功" + +msgid "Downloaded Subscriptions" +msgstr "已下载订阅" + +msgid "Downloading LightGBM model..." +msgstr "正在下载 LightGBM 模型..." + +msgid "Downloading subscription..." +msgstr "正在下载订阅..." + +msgid "Downloading..." +msgstr "下载中..." + +msgid "Downloading…" +msgstr "下载中…" + +msgid "ECS Client Subnet" +msgstr "ECS 客户端子网" + +msgid "Edit" +msgstr "编辑" + +msgid "Editing: " +msgstr "编辑:" + +msgid "Enable" +msgstr "启用" + +msgid "Enable DNS Module" +msgstr "启用 DNS 模块" + +msgid "Enable LightGBM Model" +msgstr "启用 LightGBM 模型" + +msgid "Enable Service" +msgstr "启用服务" + +msgid "Enhanced Mode" +msgstr "增强模式" + +msgid "Enter remote template URL, e.g. https://raw.githubusercontent.com/…/Clash.yaml" +msgstr "输入远程模板 URL,例如 https://raw.githubusercontent.com/…/Clash.yaml" + +msgid "Enter subscription URL (injected into template proxy-providers)" +msgstr "输入订阅链接 URL(注入到模板的 proxy-providers)" + +msgid "Every" +msgstr "每" + +msgid "Exact domain (e.g. www.google.com)" +msgstr "精确域名(如 www.google.com)" + +msgid "example.com or 1.2.3.0/24" +msgstr "example.com 或 1.2.3.0/24" + +msgid "Example: geosite:cn uses https://dns.alidns.com/dns-query; geosite:geolocation-!cn uses https://cloudflare-dns.com/dns-query." +msgstr "示例:geosite:cn 使用 https://dns.alidns.com/dns-query;geosite:geolocation-!cn 使用 https://cloudflare-dns.com/dns-query。" + +msgid "Expand" +msgstr "展开" + +msgid "Expires: " +msgstr "到期:" + +msgid "Export a backup before changing cores or configuration, so it can be restored if needed. Includes configuration and settings only, not cores." +msgstr "折腾内核或配置前先导出一份,改坏了能一键导入还原。仅含配置与设置,不含内核。" + +msgid "Export Backup" +msgstr "导出备份" + +msgid "Export failed: " +msgstr "导出失败:" + +msgid "Exporting…" +msgstr "导出中…" + +msgid "Factory defaults restored" +msgstr "已还原出厂默认设置" + +msgid "Failed" +msgstr "失败" + +msgid "Failed to check updates, please check the network" +msgstr "检查更新失败,请检查网络" + +msgid "Failed to save core configuration" +msgstr "内核配置保存失败" + +msgid "Failed to submit component update task" +msgstr "组件更新任务提交失败" + +msgid "Fake-IP Filter Domains" +msgstr "Fake-IP 过滤域名" + +msgid "Fake-IP Filter Mode" +msgstr "Fake-IP 过滤模式" + +msgid "Fake-IP Range" +msgstr "Fake-IP 网段" + +msgid "Fallback" +msgstr "故障转移" + +msgid "Fallback GeoIP Filter" +msgstr "Fallback GeoIP 过滤" + +msgid "Bootstrap" +msgstr "引导 DNS" + +msgid "Fetch and Apply" +msgstr "拉取并应用" + +msgid "Fetch Configuration" +msgstr "拉取配置" + +msgid "Fetching…" +msgstr "拉取中…" + +msgid "File Name" +msgstr "文件名" + +msgid "File name (optional, blank for automatic name)" +msgstr "文件名(选填,留空自动命名)" + +msgid "File name (optional, blank to auto-generate)" +msgstr "文件名(选填,留空自动生成)" + +msgid "Fix Deprecated Fields" +msgstr "修复废弃字段" + +msgid "Fix failed: " +msgstr "修复失败: " + +msgid "Fixed deprecated fields: " +msgstr "已修复废弃字段: " + +msgid "Force ASN lookup and prefer target ASN information when selecting nodes for a more stable experience" +msgstr "选择节点时强制查找并优先使用目标的 ASN 信息,以获得更稳定的体验" + +msgid "Force DNS Forwarding" +msgstr "强制转发 DNS" + +msgid "Force ECS Override" +msgstr "强制覆盖 ECS" + +msgid "Full URL / no protocol prefix" +msgstr "完整 URL / 不补协议" + +msgid "Generate Configuration" +msgstr "生成配置" + +msgid "Generate error: " +msgstr "生成异常: " + +msgid "Generate failed: " +msgstr "生成失败: " + +msgid "Generated: " +msgstr "生成成功: " + +msgid "Generating…" +msgstr "生成中…" + +msgid "Generation is taking longer, refreshing to show results…" +msgstr "生成时间较长,正在刷新查看结果…" + +msgid "GeoIP Source" +msgstr "GeoIP 数据源" + +msgid "GeoIP update completed" +msgstr "GeoIP 更新完成" + +msgid "GeoIP update completed, applies after Clashoo restarts" +msgstr "GeoIP 更新完成,重启 Clashoo 后生效" + +msgid "GeoIP update failed" +msgstr "GeoIP 更新失败" + +msgid "GitHub acceleration prefix" +msgstr "GitHub 加速前缀" + +msgid "Global" +msgstr "全局" + +msgid "Health check failed" +msgstr "健康检查失败" + +msgid "Health check in progress…" +msgstr "健康检查中…" + +msgid "Health check passed" +msgstr "健康检查通过" + +msgid "hours" +msgstr "小时" + +msgid "HTTP Port" +msgstr "HTTP 端口" + +msgid "Import failed: " +msgstr "导入失败:" + +msgid "Import Restore" +msgstr "导入还原" + +msgid "Importing will overwrite all current configuration and settings with the backup. Existing subscriptions/configuration files will be replaced. Continue?" +msgstr "导入将用备份覆盖当前的全部配置与设置,原有的订阅/配置文件会被替换。确定继续吗?" + +msgid "Information Input" +msgstr "信息录入" + +msgid "Initializing…" +msgstr "初始化中…" + +msgid "Intercept IPv6 DNS traffic to prevent devices with hard-coded DNS from bypassing the traffic." +msgstr "拦截 IPv6 DNS 流量,防止写死 DNS 的设备绕过分流。" + +msgid "IP / CIDR (e.g. 8.8.8.8/32)" +msgstr "IP / CIDR(如 8.8.8.8/32)" + +msgid "IP Allowlist" +msgstr "IP白名单" + +msgid "IP Blocklist" +msgstr "IP黑名单" + +msgid "IP rules do not trigger DNS resolution" +msgstr "IP 规则不触发 DNS 解析" + +msgid "IPv4 DNS Hijack" +msgstr "IPv4 DNS 劫持" + +msgid "IPv4 Proxy" +msgstr "IPv4 代理" + +msgid "IPv6 DNS Hijack" +msgstr "IPv6 DNS 劫持" + +msgid "IPv6 Proxy" +msgstr "IPv6 代理" + +msgid "just now" +msgstr "刚刚" + +msgid "Keep the default if unsure." +msgstr "不确定时保持默认。" + +msgid "LAN Control" +msgstr "局域网控制" + +msgid "%d updates available" +msgstr "发现 %d 项可更新" + +msgid "Last check: initializing" +msgstr "上次检查:初始化中" + +msgid "Last check: just now" +msgstr "上次检查:刚刚" + +msgid "Last check: %d seconds ago" +msgstr "上次检查:%d 秒前" + +msgid "Last check: %d minutes ago" +msgstr "上次检查:%d 分钟前" + +msgid "Last check: %d hours ago" +msgstr "上次检查:%d 小时前" + +msgid "Last run: " +msgstr "上次执行:" + +msgid "LightGBM model file download URL; leave blank to use the default official URL" +msgstr "LightGBM 模型文件下载地址,留空使用默认官方地址" + +msgid "LightGBM model updated successfully" +msgstr "LightGBM 模型更新成功" + +msgid "Load failed" +msgstr "加载失败" + +msgid "Load-balance" +msgstr "负载均衡" + +msgid "Loading…" +msgstr "加载中…" + +msgid "Local Template" +msgstr "本地模板" + +msgid "Log" +msgstr "日志" + +msgid "Match Rule" +msgstr "匹配规则" + +msgid "Memory usage %s" +msgstr "内存使用 %s" + +msgid "mihomo restarted successfully, please wait for servers to come online." +msgstr "mihomo 重启成功,请等待服务器上线!" + +msgid "mihomo Stable" +msgstr "mihomo 稳定版" + +msgid "mihomo started successfully, please wait for servers to come online." +msgstr "mihomo 启动成功,请等待服务器上线!" + +msgid "mihomo writes the ecs parameter to DNS URLs; sing-box writes dns.client_subnet. Leave empty to skip." +msgstr "Mihomo 写入 DNS URL 的 ecs 参数;sing-box 写入 dns.client_subnet。清空则不写入。" + +msgid "Mixed Mode" +msgstr "Mixed 模式" + +msgid "Mixed Port" +msgstr "混合端口" + +msgid "Mode" +msgstr "模式" + +msgid "Model Download URL" +msgstr "模型下载 URL" + +msgid "Model install failed: $1" +msgstr "模型安装失败:$1" + +msgid "Model is already up to date" +msgstr "模型已是最新版本,无需更新" + +msgid "Model not installed" +msgstr "模型未安装" + +msgid "Network Stack" +msgstr "网络栈" + +msgid "Network Stack Type" +msgstr "网络栈类型" + +msgid "No auto setup result yet" +msgstr "暂无自动配置结果" + +msgid "No component update logs yet" +msgstr "暂无组件更新日志" + +msgid "No configuration file found. Import a subscription or upload a configuration on the Configuration page first." +msgstr "未找到配置文件,请先在「配置」页导入订阅或上传配置" + +msgid "No configuration file selected. Select or upload one on the Configuration page before starting." +msgstr "未选择配置文件,请先到「配置」页选择或上传后选中再启动" + +msgid "No configuration files yet. Use Quick Wizard to generate one or upload a JSON file." +msgstr "暂无配置文件,请使用快速向导生成或上传 JSON 文件" + +msgid "No proxy groups" +msgstr "无节点组" + +msgid "No subscriptions" +msgstr "暂无订阅" + +msgid "Node Panel ›" +msgstr "节点面板 ›" + +msgid "Node Selection" +msgstr "节点选择" + +msgid "Node Subscription" +msgstr "节点订阅" + +msgid "Node weight bonus. <1 means lower priority, >1 means higher priority. Modify as needed; leave blank to skip policy-priority injection." +msgstr "节点权重加成,<1 表示较低优先级,>1 表示较高优先级;可按需修改,留空则不注入 policy-priority" + +msgid "Not installed" +msgstr "未安装" + +msgid "Not enabled" +msgstr "未启用" + +msgid "Not running" +msgstr "未运行" + +msgid "Off" +msgstr "关闭" + +msgid "Only subscriptions with recorded source URLs are overwritten; no duplicate files are created." +msgstr "仅覆盖已记录来源链接的订阅,不会生成重复文件。" + +msgid "Only .tar.gz and legacy .json backup files are supported" +msgstr "仅支持 .tar.gz 和旧版 .json 备份文件" + +msgid "Open Dashboard" +msgstr "打开面板" + +msgid "TUN stack is used when either TCP or UDP mode is TUN." +msgstr "TCP 或 UDP 任意一个选择 TUN 时,该栈设置才会生效。" + +msgid "OpenClash is enabled; disable it first to avoid rule conflicts" +msgstr "OpenClash 已启用,请先停用以避免规则冲突" + +msgid "OpenClash/Nikki Loopback DNS Compatibility" +msgstr "OpenClash/Nikki 回环 DNS 兼容" + +msgid "Optional compatibility for normal managed mode. At runtime, replace only proxy node resolvers that point back to the imported configuration's own DNS listener with the proxy-server-nameserver setting. Core Only mode stays verbatim; the source configuration and unrelated local DNS services remain unchanged." +msgstr "用于普通接管模式的可选兼容功能。运行时仅把指回导入配置自身 DNS 监听的节点解析器替换为 proxy-server-nameserver(节点域名解析)的设置;仅内核模式保持原样,源配置和其他本地 DNS 服务不受影响。" + +msgid "Operation failed: " +msgstr "操作失败: " + +msgid "Other Configuration Files (upload / overwrite output)" +msgstr "其他配置文件(上传 / 覆写输出)" + +msgid "Output file name (without extension, blank to auto-fill)" +msgstr "输出文件名(不含扩展名,留空自动填写)" + +msgid "Overseas" +msgstr "境外" + +msgid "Overseas Encrypted DNS (anti-pollution)" +msgstr "国外加密 DNS(防污染)" + +msgid "Overview" +msgstr "概览" + +msgid "Overwrite Settings" +msgstr "覆写设置" + +msgid "Panel" +msgstr "面板" + +msgid "Panel is downloading" +msgstr "面板正在下载" + +msgid "Panel switch failed: " +msgstr "面板切换失败: " + +msgid "Panel UI" +msgstr "面板 UI" + +msgid "Password" +msgstr "密码" + +msgid "Paste native sing-box subscription URL (direct JSON response)" +msgstr "粘贴原生 sing-box 订阅链接(直接返回 JSON 的链接)" + +msgid "Paste subscription URL (supports vmess / vless / trojan, etc.)" +msgstr "粘贴订阅链接(支持 vmess / vless / trojan 等)" + +msgid "Pause" +msgstr "暂停" + +msgid "Period for automatically fetching a new model; also controls the cron trigger frequency, default 72 hours" +msgstr "自动拉取新模型的周期,同时决定 cron 触发频率,默认 72 小时" + +msgid "Please enter a remote template URL" +msgstr "请输入远程模板 URL" + +msgid "Please enter a subscription URL" +msgstr "请输入订阅链接 URL" + +msgid "Please enter an output file name" +msgstr "请填写输出文件名" + +msgid "Please select a local template file" +msgstr "请选择本地模板文件" + +msgid "Plugin Log" +msgstr "插件日志" + +msgid "Policy Priority (weight bonus)" +msgstr "Policy Priority(权重加成)" + +msgid "Policy Type" +msgstr "策略类型" + +msgid "Port" +msgstr "端口" + +msgid "Port Settings" +msgstr "端口配置" + +msgid "Prevent DNS Leaks" +msgstr "防 DNS 泄漏" + +msgid "Processing…" +msgstr "处理中…" + +msgid "Protocol" +msgstr "协议" + +msgid "Proxy" +msgstr "代理" + +msgid "DNS Takeover" +msgstr "DNS 接管" + +msgid "IPv4" +msgstr "IPv4" + +msgid "IPv6" +msgstr "IPv6" + +msgid "MAC" +msgstr "MAC" + +msgid "Rules are matched from top to bottom. Unmatched devices use DNS takeover and proxy by default. A rule with IPv4, IPv6 and MAC all empty matches every device." +msgstr "规则按从上到下的顺序匹配;未命中的设备默认接管 DNS 并走代理。IPv4、IPv6、MAC 全部留空的规则匹配所有设备。" + +msgid "Proxy Authentication" +msgstr "代理认证" + +msgid "Proxy configuration saved and service hot-reloaded" +msgstr "代理配置已保存并热重载服务" + +msgid "Proxy configuration saved, service is not running" +msgstr "代理配置已保存,服务未启动" + +msgid "Proxy Domain Split DNS" +msgstr "代理域名分流解析" + +msgid "Proxy Mode" +msgstr "代理模式" + +msgid "Proxy Node Domain Resolver" +msgstr "节点域名解析专用" + +msgid "Quick Wizard" +msgstr "快速向导" + +msgid "Random" +msgstr "随机" + +msgid "Ready · " +msgstr "就绪 · " + +msgid "Ready ✓" +msgstr "已就绪 ✓" + +msgid "Recommended " +msgstr "推荐 " + +msgid "Recommended" +msgstr "推荐" + +msgid "Recommended blank" +msgstr "推荐留空" + +msgid "Redirect Port" +msgstr "Redirect 端口" + +msgid "Refresh" +msgstr "刷新" + +msgid "Refresh Access Check" +msgstr "刷新访问检查" + +msgid "Regular domains use overseas DNS only and follow proxy rules; DoT/DoQ is blocked. Takes effect after restart.
" +msgstr "普通域名只走境外 DNS 并遵守代理规则,阻断 DoT/DoQ。重启生效。
" + +msgid "Regularly update rule data" +msgstr "定时更新规则数据" + +msgid "Reject proxied UDP 443 so some apps fall back to TCP. Try only when downloads or video stall." +msgstr "拒绝代理链路中的 UDP 443,让部分 App 回落 TCP。仅在下载/视频卡住时尝试开启。" + +msgid "Relay" +msgstr "链式代理" + +msgid "Remote Template" +msgstr "远程模板" + +msgid "Remote: %s" +msgstr "远端:%s" + +msgid "Removing Cron scheduled tasks" +msgstr "删除 Cron → 计划任务" + +msgid "Requires MMDB. If enabled while the local GeoIP database is missing, the core may fail to start. Usually not needed." +msgstr "依赖 MMDB;若启用但本地 GeoIP 数据库缺失,可能导致内核启动失败。一般无需开启。" + +msgid "Restarting client..." +msgstr "正在重启客户端..." + +msgid "Restarting Dnsmasq" +msgstr "重启 Dnsmasq 程序" + +msgid "Restore Clashoo settings to factory defaults (subscriptions and configuration files are kept). Continue?" +msgstr "将把 clashoo 的设置恢复为出厂默认值(订阅与配置文件保留)。确定继续吗?" + +msgid "Restore Defaults" +msgstr "还原默认配置" + +msgid "Restore failed: " +msgstr "还原失败:" + +msgid "Restoring…" +msgstr "还原中…" + +msgid "Role" +msgstr "角色" + +msgid "Rule" +msgstr "规则" + +msgid "Rule data includes China allowlist (direct IP ranges) and GeoIP / GeoSite databases" +msgstr "规则数据包含:大陆白名单(直连 IP 段)与 GeoIP / GeoSite 地理库" + +msgid "Rule mode (same syntax as rules)" +msgstr "规则模式(与 rules 同语法)" + +msgid "Rules and Control" +msgstr "规则与控制" + +msgid "Run Mode" +msgstr "运行模式" + +msgid "Run mode setting failed: " +msgstr "运行模式设置失败: " + +msgid "Run only the core with your imported configuration, without taking over firewall / DNS / routing. Compatible with nikki, OpenClash, momo and homeproxy configurations.
" +msgstr "只用你导入的配置跑内核,不接管防火墙 / DNS / 路由,兼容 nikki、OpenClash、momo、homeproxy 配置。
" + +msgid "Running degraded" +msgstr "已降级运行" + +msgid "Running 🟢" +msgstr "运行中 🟢" + +msgid "Runtime Status" +msgstr "运行状态" + +msgid "Sample Rate" +msgstr "采样率" + +msgid "sample-rate, 1 = sample every latency test; increase to reduce sampling frequency" +msgstr "sample-rate,1 = 每次测速都采样,调大可降低采集频率" + +msgid "Save" +msgstr "保存" + +msgid "Save Configuration" +msgstr "保存配置" + +msgid "Save failed: " +msgstr "保存失败: " + +msgid "Scheduled Log Cleanup" +msgstr "定时清理日志" + +msgid "Scheduled Subscription Update" +msgstr "定时更新订阅" + +msgid "Scheduled update settings saved" +msgstr "定时更新设置已保存" + +msgid "Select a configuration above to edit it here" +msgstr "选择上方配置后可在此处编辑" + +msgid "Select local template file" +msgstr "选择本地模板文件" + +msgid "Select YAML file to upload" +msgstr "选择 YAML 文件上传" + +msgid "Selector" +msgstr "选择器" + +msgid "Service disabled" +msgstr "服务已禁用" + +msgid "Service stopped" +msgstr "服务已停止" + +msgid "Setting Cron scheduled tasks and starting process supervisor..." +msgstr "设置 Cron → 计划任务,启动进程守护程序..." + +msgid "Setting DNS forwarder / enabling custom DNS" +msgstr "设置 DNS 转发器 / 启用自定义 DNS" + +msgid "Setting mihomo network rules" +msgstr "设置 mihomo 网络规则" + +msgid "Setting sing-box network rules" +msgstr "设置 sing-box 网络规则" + +msgid "Show/Hide" +msgstr "显示/隐藏" + +msgid "sing-box Configuration Files" +msgstr "sing-box 配置文件" + +msgid "sing-box core not detected; download sing-box from the System page" +msgstr "未检测到 sing-box 内核;请到系统页下载 sing-box" + +msgid "sing-box Independent DNS Cache" +msgstr "sing-box 独立 DNS 缓存" + +msgid "sing-box restarted successfully, please wait for servers to come online." +msgstr "sing-box 重启成功,请等待服务器上线!" + +msgid "sing-box service failed to start. Check System → Log." +msgstr "sing-box 服务启动失败,请到「系统 → 日志」查看" + +msgid "sing-box Stable" +msgstr "sing-box 稳定版" + +msgid "sing-box started successfully, please wait for servers to come online." +msgstr "sing-box 启动成功,请等待服务器上线!" + +msgid "Size" +msgstr "大小" + +msgid "Smart" +msgstr "Smart 版" + +msgid "Smart cache cleared" +msgstr "Smart 缓存已清理" + +msgid "Smart core not detected; updating the model will not install the core" +msgstr "未检测到 Smart 内核;更新模型不会安装内核" + +msgid "Smart core not detected; updating the model will not install the core. Download the Smart build from the System page." +msgstr "未检测到 Smart 内核;更新模型不会安装内核,请到系统页下载 Smart 版" + +msgid "Smart Policy Auto Switch" +msgstr "Smart 策略自动切换" + +msgid "Smart Policy Settings" +msgstr "Smart 策略设置" + +msgid "smart-collector-size, maximum retained training samples, default 100" +msgstr "smart-collector-size,最多保留的训练样本数,默认 100" + +msgid "Sniffer (streaming compatibility)" +msgstr "嗅探功能(流媒体兼容)" + +msgid "SOCKS5 Port" +msgstr "SOCKS5 端口" + +msgid "Stable" +msgstr "稳定版" + +msgid "Start downloading LightGBM model: $1" +msgstr "开始下载 LightGBM 模型:$1" + +msgid "Start failed: " +msgstr "启动失败: " + +msgid "Start Service" +msgstr "启动服务" + +msgid "Started (" +msgstr "已启动(" + +msgid "Starting client" +msgstr "启动客户端" + +msgid "Starting core…" +msgstr "启动内核…" + +msgid "Starting…" +msgstr "启动中…" + +msgid "Stop Service" +msgstr "停止服务" + +msgid "Stopped (not enabled at boot)" +msgstr "已停止(开机不自启动)" + +msgid "Stopped" +msgstr "已停止" + +msgid "Stopping client..." +msgstr "正在停止客户端..." + +msgid "Stopping mihomo" +msgstr "停止 mihomo" + +msgid "Stopping mihomo process supervisor" +msgstr "mihomo 停止进程守护程序" + +msgid "Stopping sing-box" +msgstr "停止 sing-box" + +msgid "Stopping sing-box process supervisor" +msgstr "sing-box 停止进程守护程序" + +msgid "Stopping…" +msgstr "停止中…" + +msgid "Submission failed" +msgstr "提交失败" + +msgid "Submitting" +msgstr "提交中" + +msgid "Subscription" +msgstr "订阅" + +msgid "Subscription download failed" +msgstr "订阅下载失败" + +msgid "Subscription Update" +msgstr "订阅更新" + +msgid "Subscription updated successfully" +msgstr "订阅更新成功" + +msgid "Subscription URL" +msgstr "订阅链接" + +msgid "Subscription URLs (one per line)" +msgstr "订阅链接(多条用换行分隔)" + +msgid "Switch" +msgstr "切换" + +msgid "Switch Profile" +msgstr "切换" + +msgid "Switch configuration failed: " +msgstr "切换配置失败: " + +msgid "Switch failed: " +msgstr "切换失败: " + +msgid "Switched to " +msgstr "已切换至 " + +msgid "Switching to " +msgstr "正在切换到 " + +msgid "Syncing" +msgstr "同步中" + +msgid "Syncing runtime state" +msgstr "正在同步运行状态" + +msgid "System" +msgstr "系统" + +msgid "System is missing nftables runtime" +msgstr "系统缺少 nftables 运行时" + +msgid "Target" +msgstr "走向" + +msgid "TCP Mode" +msgstr "TCP 模式" + +msgid "Template Selection" +msgstr "模板选择" + +msgid "Template upload failed: " +msgstr "模板上传失败: " + +msgid "Template uploaded: " +msgstr "模板上传成功: " + +msgid "Test and Apply" +msgstr "一键测速并应用" + +msgid "Test Latency" +msgstr "测速" + +msgid "Testing" +msgstr "测速中" + +msgid "Testing latency and writing upstream DNS…" +msgstr "正在测速并写入 DNS 上游…" + +msgid "The new core failed to start, please check the core log" +msgstr "新内核启动失败,请查看内核日志" + +msgid "Time" +msgstr "耗时" + +msgid "Timeout waiting for new core to start, please check the core log" +msgstr "等待新内核启动超时,请查看内核日志" + +msgid "Total %s" +msgstr "总计 %s" + +msgid "TPROXY Port" +msgstr "TPROXY 端口" + +msgid "Traffic Monitor" +msgstr "流量监控" + +msgid "Training Data Size" +msgstr "训练数据量" + +msgid "Transparent Proxy" +msgstr "透明代理" + +msgid "TUN Mode" +msgstr "TUN 模式" + +msgid "TUN Network Stack" +msgstr "TUN 网络栈" + +msgid "Type" +msgstr "类型" + +msgid "UDP Mode" +msgstr "UDP 模式" + +msgid "Unknown" +msgstr "未知" + +msgid "Unknown error" +msgstr "未知错误" + +msgid "Update" +msgstr "更新" + +msgid "Update All Now" +msgstr "立即更新全部" + +msgid "Update available" +msgstr "可更新" + +msgid "Update components individually. Click Check Updates to refresh versions." +msgstr "按组件单独更新,便于定位失败。点击「检查更新」刷新版本。" + +msgid "Update failed" +msgstr "更新失败" + +msgid "Update failed: " +msgstr "更新失败: " + +msgid "Update interval (hours)" +msgstr "更新间隔(小时)" + +msgid "Update interval must be 1 to 8760 hours" +msgstr "更新间隔必须为 1 到 8760 小时" + +msgid "Update Log" +msgstr "更新日志" + +msgid "Update Model" +msgstr "更新模型" + +msgid "Update Subscription" +msgstr "更新订阅" + +msgid "Update succeeded" +msgstr "更新成功" + +msgid "Updating" +msgstr "更新中" + +msgid "Updating $1" +msgstr "正在更新 $1" + +msgid "Updating all subscriptions…" +msgstr "正在更新全部订阅…" + +msgid "Updating…" +msgstr "更新中…" + +msgid "Unable to read backup file" +msgstr "无法读取备份文件" + +msgid "Upload" +msgstr "上传" + +msgid "Upload Configuration File" +msgstr "上传配置文件" + +msgid "Upload failed" +msgstr "上传失败" + +msgid "Upload failed: " +msgstr "上传失败: " + +msgid "Upload offset mismatch" +msgstr "上传偏移不匹配" + +msgid "Upload JSON Configuration" +msgstr "上传 JSON 配置" + +msgid "Upload succeeded: " +msgstr "上传成功: " + +msgid "Upload YAML Template" +msgstr "上传 YAML 模板" + +msgid "Upstream DNS" +msgstr "上游 DNS" + +msgid "Url-test" +msgstr "自动测速" + +msgid "Use DNS" +msgstr "使用 DNS" + +msgid "Use LightGBM model to predict weights" +msgstr "使用 LightGBM 模型来预测权重" + +msgid "Used to resolve DoH/DoT/DoQ server domains; plain IP DNS is recommended." +msgstr "用于解析 DoH/DoT/DoQ 服务器域名,建议填写纯 IP DNS。" + +msgid "User-Agent (UA)" +msgstr "用户代理(UA)" + +msgid "Username" +msgstr "用户名" + +msgid "Virtual IP Ping Hijacking" +msgstr "Fake-IP Ping 劫持" + +msgid "Wait for the current task to complete" +msgstr "等待当前任务完成" + +msgid "Waiting for data..." +msgstr "等待数据..." + +msgid "When downloading core/data components, this prefix will be used first (a slash is required at the end). Leave blank to use the built-in gh-proxy.com and ghfast.top alternative sources." +msgstr "内核 / 数据组件下载时优先走该前缀(末尾需带斜杠)。留空则使用内置 gh-proxy.com 与 ghfast.top 备选源。" + +msgid "When enabled, direct / proxy / fallback roles each use an independent DNS cache. Useful when the same domain needs different resolver chains by routing. Usually not needed." +msgstr "开启后,direct / proxy / fallback 等角色各自独立 DNS 缓存;适用于同一域名按分流走不同解析链的场景。一般无需开启。" + +msgid "When enabled, sniffer configuration is injected automatically to improve streaming domain detection and routing stability." +msgstr "启用后自动注入 sniffer 配置,提升流媒体域名识别与分流稳定性。" + +msgid "YAML Subscription Conversion" +msgstr "YAML 订阅转换" + +msgid "⏳ A download task is already running..." +msgstr "⏳ 已有下载任务进行中..." + +msgid "⏳ Fetch timed out, refreshing to show results..." +msgstr "⏳ 拉取超时,正在刷新查看结果..." + +msgid "⏳ Fetching configuration..." +msgstr "⏳ 正在拉取配置..." + +msgid "⏳ Starting update task..." +msgstr "⏳ 正在启动更新任务..." + +msgid "⏳ Submitting download task..." +msgstr "⏳ 正在提交下载任务..." + +msgid "✗ Fetch error: " +msgstr "✗ 拉取异常: " + +msgid "✗ Fetch failed: " +msgstr "✗ 拉取失败: " + +msgid "✗ Panel update timeout" +msgstr "✗ 面板更新超时" + +msgid "✗ Please enter a subscription URL" +msgstr "✗ 请填写订阅链接" + +msgid "✗ Please enter a subscription URL first" +msgstr "✗ 请先填写订阅链接" + +msgid "✗ Start failed" +msgstr "✗ 启动失败" + +msgid "✗ Start failed: " +msgstr "✗ 启动失败: " + +msgid "✗ Submission failed" +msgstr "✗ 提交失败" diff --git a/luci-app-clashoo/po/zh_Hans b/luci-app-clashoo/po/zh_Hans new file mode 120000 index 000000000..41451e4a1 --- /dev/null +++ b/luci-app-clashoo/po/zh_Hans @@ -0,0 +1 @@ +zh-cn \ No newline at end of file diff --git a/luci-app-clashoo/root/usr/share/clashoo/rpc/backup_archive.sh b/luci-app-clashoo/root/usr/share/clashoo/rpc/backup_archive.sh new file mode 100755 index 000000000..c600b36f6 --- /dev/null +++ b/luci-app-clashoo/root/usr/share/clashoo/rpc/backup_archive.sh @@ -0,0 +1,421 @@ +#!/bin/sh + +set -eu + +TMP_ROOT='/tmp/clashoo-backup' +RESTORE_LOCK_DIR="$TMP_ROOT/restore.lock" +IMPORT_LOCK_DIR="$TMP_ROOT/import.lock" +IMPORT_OWNER_FILE="$IMPORT_LOCK_DIR/path" +EXPORT_GUARD_DIR="$TMP_ROOT/export.lock" +MAX_ARCHIVE_BYTES=33554432 +MAX_EXTRACTED_BYTES=67108864 +MAX_TAR_BYTES=71303168 +MAX_EXTRACTED_KB=65536 +MAX_ENTRIES=2048 +WORK_DIR='' +EXTRACT_DIR='' +RESTORE_LOCKED=0 +EXPORT_GUARD_LOCKED=0 + +die() { + printf '%s\n' "$*" >&2 + exit 1 +} + +cleanup() { + case "$WORK_DIR" in + "$TMP_ROOT"/*) rm -rf "$WORK_DIR" ;; + esac + if [ "$RESTORE_LOCKED" -eq 1 ]; then + rmdir "$RESTORE_LOCK_DIR" >/dev/null 2>&1 || true + fi + if [ "$EXPORT_GUARD_LOCKED" -eq 1 ]; then + rmdir "$EXPORT_GUARD_DIR" >/dev/null 2>&1 || true + fi +} + +trap cleanup EXIT INT TERM + +is_safe_name() { + local name="$1" + [ -n "$name" ] || return 1 + case "$name" in + .*|*..*|*/*|*\\*|*' '*|*'#'*|*'?'*|*'*'*|*':'*) return 1 ;; + esac + ! printf '%s' "$name" | LC_ALL=C grep -q '[[:cntrl:]]' +} + +is_allowed_entry() { + local entry="$1" base + case "$entry" in + manifest.json|uci|meta/confit_list.conf|meta/template_bindings.conf) + return 0 + ;; + meta|config|config/sub|config/upload|config/custom|config/singbox|templates) + return 0 + ;; + config/sub/*) + base="${entry#config/sub/}" + is_safe_name "$base" + ;; + config/upload/*) + base="${entry#config/upload/}" + is_safe_name "$base" + ;; + config/custom/*) + base="${entry#config/custom/}" + is_safe_name "$base" + ;; + config/singbox/*) + base="${entry#config/singbox/}" + is_safe_name "$base" + ;; + templates/*) + base="${entry#templates/}" + is_safe_name "$base" + ;; + *) + return 1 + ;; + esac +} + +normalise_entry() { + local entry="$1" + while [ "${entry#./}" != "$entry" ]; do + entry="${entry#./}" + done + entry="${entry%/}" + printf '%s\n' "$entry" +} + +require_archive_path() { + case "$1" in + "$TMP_ROOT"/*) ;; + *) die '备份临时路径无效' ;; + esac +} + +ensure_tmp_root() { + local owner + [ ! -L "$TMP_ROOT" ] || die '备份临时目录不能是符号链接' + if [ -e "$TMP_ROOT" ]; then + [ -d "$TMP_ROOT" ] || die '备份临时路径不是目录' + owner="$(LC_ALL=C ls -nd "$TMP_ROOT" 2>/dev/null | awk '{ print $3 }')" + [ "$owner" = '0' ] || die '备份临时目录所有者异常' + else + mkdir -m 700 "$TMP_ROOT" || die '无法创建备份临时目录' + fi + chmod 700 "$TMP_ROOT" || die '无法保护备份临时目录' +} + +cleanup_stale_upload() { + local upload='' + if [ -d "$IMPORT_LOCK_DIR" ]; then + if [ -f "$IMPORT_OWNER_FILE" ] && + find "$IMPORT_OWNER_FILE" -mmin +15 | grep -q .; then + upload="$(sed -n '1p' "$IMPORT_OWNER_FILE" 2>/dev/null || true)" + case "$upload" in + "$TMP_ROOT"/import.??????) rm -f "$upload" "$upload.result" ;; + esac + rm -rf "$IMPORT_LOCK_DIR" + elif [ ! -f "$IMPORT_OWNER_FILE" ] && + find "$IMPORT_LOCK_DIR" -mmin +15 | grep -q .; then + rm -rf "$IMPORT_LOCK_DIR" + fi + fi + find "$TMP_ROOT" -maxdepth 1 -type f -name 'import.*' -mmin +15 \ + -exec rm -f {} \; 2>/dev/null || true +} + +new_upload() { + local upload + cleanup_stale_upload + mkdir "$IMPORT_LOCK_DIR" 2>/dev/null || die '已有备份正在上传,请稍后重试' + if ! upload="$(mktemp "$TMP_ROOT/import.XXXXXX")"; then + rmdir "$IMPORT_LOCK_DIR" >/dev/null 2>&1 || true + die '无法创建上传临时文件' + fi + if ! chmod 600 "$upload" || + ! printf '%s\n' "$upload" > "$IMPORT_OWNER_FILE" || + ! chmod 600 "$IMPORT_OWNER_FILE"; then + rm -f "$upload" "$IMPORT_OWNER_FILE" + rmdir "$IMPORT_LOCK_DIR" >/dev/null 2>&1 || true + die '无法保护上传临时文件' + fi + printf '%s\n' "$upload" +} + +new_export() { + local count file + if [ -d "$EXPORT_GUARD_DIR" ] && + find "$EXPORT_GUARD_DIR" -mmin +1 | grep -q .; then + rmdir "$EXPORT_GUARD_DIR" >/dev/null 2>&1 || true + fi + if ! mkdir "$EXPORT_GUARD_DIR" 2>/dev/null; then + sleep 1 + mkdir "$EXPORT_GUARD_DIR" 2>/dev/null || die '正在准备其他备份,请稍后重试' + fi + EXPORT_GUARD_LOCKED=1 + find "$TMP_ROOT" -maxdepth 1 -type f -name 'export.*' -mmin +15 \ + -exec rm -f {} \; 2>/dev/null || true + count="$(find "$TMP_ROOT" -maxdepth 1 -type f -name 'export.??????' | wc -l | tr -d ' ')" + [ "$count" -lt 2 ] || die '已有多个备份等待下载,请稍后重试' + file="$(mktemp "$TMP_ROOT/export.XXXXXX")" || die '无法创建导出临时文件' + chmod 600 "$file" || die '无法保护导出临时文件' + rmdir "$EXPORT_GUARD_DIR" || die '无法释放导出准备锁' + EXPORT_GUARD_LOCKED=0 + printf '%s\n' "$file" +} + +lock_restore() { + mkdir "$RESTORE_LOCK_DIR" 2>/dev/null || die '已有备份正在还原,请稍后重试' +} + +unlock_restore() { + rmdir "$RESTORE_LOCK_DIR" >/dev/null 2>&1 || true +} + +copy_regular() { + local root="$1" source="$2" relative="$3" parent + [ -f "$source" ] || return 0 + [ ! -L "$source" ] || die "拒绝备份符号链接: $source" + parent="${relative%/*}" + [ "$parent" = "$relative" ] || mkdir -p "$root/$parent" + cp "$source" "$root/$relative" +} + +copy_directory() { + local root="$1" source_dir="$2" prefix="$3" source base + [ -d "$source_dir" ] || return 0 + mkdir -p "$root/$prefix" + for source in "$source_dir"/*; do + [ -f "$source" ] || continue + [ ! -L "$source" ] || continue + base="${source##*/}" + is_safe_name "$base" || continue + cp "$source" "$root/$prefix/$base" + done +} + +copy_current_tree() { + local root="$1" + mkdir -p "$root" + copy_regular "$root" /etc/config/clashoo uci + copy_regular "$root" /usr/share/clashbackup/confit_list.conf meta/confit_list.conf + copy_regular "$root" /usr/share/clashbackup/template_bindings.conf meta/template_bindings.conf + copy_directory "$root" /usr/share/clashoo/config/sub config/sub + copy_directory "$root" /usr/share/clashoo/config/upload config/upload + copy_directory "$root" /usr/share/clashoo/config/custom config/custom + copy_directory "$root" /usr/share/clashoo/config/singbox config/singbox + copy_directory "$root" /etc/clashoo/templates templates +} + +write_manifest() { + local root="$1" host version created file + host="$(uname -n 2>/dev/null || true)" + version="$(sed -n '1p' /usr/share/clashoo/luci_version 2>/dev/null || true)" + created="$(date +%s)" + # jshn uses optional globals internally and is not nounset-safe. + set +u + . /usr/share/libubox/jshn.sh + json_init + json_add_string marker 'clashoo-backup-v2' + json_add_int created_at "$created" + json_add_string host "$host" + json_add_string app_version "$version" + json_add_array files + for file in $(cd "$root" && find . -type f | sed 's#^\./##' | sort); do + json_add_string '' "$file" + done + json_close_array + json_dump > "$root/manifest.json" + set -u +} + +validate_archive() { + local archive="$1" size validation_dir fifo tar_file gzip_pid head_rc gzip_rc tar_size list verbose raw entry mode type manifest_seen=0 entry_count=0 extracted_bytes extracted_kb marker + require_archive_path "$archive" + [ -f "$archive" ] || die '备份文件不存在' + size="$(wc -c < "$archive" | tr -d ' ')" + [ "$size" -gt 0 ] || die '备份文件为空' + [ "$size" -le "$MAX_ARCHIVE_BYTES" ] || die '备份文件超过 32MB' + + if [ -z "$WORK_DIR" ]; then + WORK_DIR="$(mktemp -d "$TMP_ROOT/validate.XXXXXX")" + fi + validation_dir="$WORK_DIR/validation" + mkdir -p "$validation_dir" + fifo="$validation_dir/archive.pipe" + tar_file="$validation_dir/archive.tar" + mkfifo "$fifo" || die '无法创建备份校验管道' + gzip -dc "$archive" > "$fifo" 2>/dev/null & + gzip_pid=$! + set +e + head -c "$((MAX_TAR_BYTES + 1))" "$fifo" > "$tar_file" 2>/dev/null + head_rc=$? + wait "$gzip_pid" + gzip_rc=$? + set -e + rm -f "$fifo" + tar_size="$(wc -c < "$tar_file" | tr -d ' ')" + [ "$tar_size" -le "$MAX_TAR_BYTES" ] || die '备份解压后超过 64MB' + [ "$head_rc" -eq 0 ] && [ "$gzip_rc" -eq 0 ] || die '备份压缩包损坏' + list="$validation_dir/list" + verbose="$validation_dir/verbose" + tar -tf "$tar_file" > "$list" 2>/dev/null || die '无法读取备份文件清单' + tar -tvf "$tar_file" > "$verbose" 2>/dev/null || die '无法读取备份文件类型' + + while IFS= read -r raw; do + entry_count=$((entry_count + 1)) + [ "$entry_count" -le "$MAX_ENTRIES" ] || die '备份文件条目过多' + entry="$(normalise_entry "$raw")" + [ -n "$entry" ] || continue + case "$entry" in + /*|*\\*) die '备份文件包含非法路径' ;; + esac + case "/$entry/" in + */../*|*/./*) die '备份文件包含目录穿越路径' ;; + esac + is_allowed_entry "$entry" || die "备份文件包含未知路径: $entry" + [ "$entry" = 'manifest.json' ] && manifest_seen=1 + done < "$list" + [ "$manifest_seen" -eq 1 ] || die '备份文件缺少 manifest.json' + + while IFS= read -r raw; do + mode="${raw%% *}" + type="$(printf '%s' "$mode" | cut -c 1)" + case "$type" in + -|d) ;; + *) die '备份文件包含链接或特殊文件' ;; + esac + done < "$verbose" + extracted_bytes="$(awk '$1 ~ /^-/ { total += $3 } END { printf "%.0f\n", total }' "$verbose")" + [ "$extracted_bytes" -le "$MAX_EXTRACTED_BYTES" ] || die '备份解压后超过 64MB' + + EXTRACT_DIR="$validation_dir/extract" + mkdir -p "$EXTRACT_DIR" + tar -xf "$tar_file" -C "$EXTRACT_DIR" >/dev/null 2>&1 || die '备份文件解压失败' + if find "$EXTRACT_DIR" -type l | grep -q .; then + die '备份文件包含符号链接' + fi + extracted_kb="$(du -sk "$EXTRACT_DIR" | cut -f 1)" + [ "$extracted_kb" -le "$MAX_EXTRACTED_KB" ] || die '备份解压后超过 64MB' + [ -f "$EXTRACT_DIR/manifest.json" ] || die '备份清单不是普通文件' + [ -f "$EXTRACT_DIR/uci" ] || die '备份文件缺少 UCI 配置' + marker="$(jsonfilter -i "$EXTRACT_DIR/manifest.json" -e '@.marker' 2>/dev/null || true)" + [ "$marker" = 'clashoo-backup-v2' ] || die '备份格式标记无效' +} + +clear_target_directories() { + local directory + for directory in \ + /usr/share/clashoo/config/sub \ + /usr/share/clashoo/config/upload \ + /usr/share/clashoo/config/custom \ + /usr/share/clashoo/config/singbox \ + /etc/clashoo/templates; do + mkdir -p "$directory" || return 1 + rm -rf "$directory"/* "$directory"/.[!.]* "$directory"/..?* || return 1 + done +} + +copy_tree_to_system() { + local root="$1" source base + clear_target_directories || return 1 + [ -f "$root/uci" ] && cp "$root/uci" /etc/config/clashoo || return 1 + rm -f /usr/share/clashbackup/confit_list.conf \ + /usr/share/clashbackup/template_bindings.conf || return 1 + if [ -f "$root/meta/confit_list.conf" ]; then + mkdir -p /usr/share/clashbackup || return 1 + cp "$root/meta/confit_list.conf" /usr/share/clashbackup/confit_list.conf || return 1 + fi + if [ -f "$root/meta/template_bindings.conf" ]; then + mkdir -p /usr/share/clashbackup || return 1 + cp "$root/meta/template_bindings.conf" /usr/share/clashbackup/template_bindings.conf || return 1 + fi + for source in "$root/config/sub"/*; do + [ -f "$source" ] || continue + base="${source##*/}" + cp "$source" "/usr/share/clashoo/config/sub/$base" || return 1 + done + for source in "$root/config/upload"/*; do + [ -f "$source" ] || continue + base="${source##*/}" + cp "$source" "/usr/share/clashoo/config/upload/$base" || return 1 + done + for source in "$root/config/custom"/*; do + [ -f "$source" ] || continue + base="${source##*/}" + cp "$source" "/usr/share/clashoo/config/custom/$base" || return 1 + done + for source in "$root/config/singbox"/*; do + [ -f "$source" ] || continue + base="${source##*/}" + cp "$source" "/usr/share/clashoo/config/singbox/$base" || return 1 + done + for source in "$root/templates"/*; do + [ -f "$source" ] || continue + base="${source##*/}" + cp "$source" "/etc/clashoo/templates/$base" || return 1 + done +} + +export_archive() { + local archive="$1" stage + require_archive_path "$archive" + WORK_DIR="$(mktemp -d "$TMP_ROOT/export.XXXXXX")" + stage="$WORK_DIR/stage" + copy_current_tree "$stage" + write_manifest "$stage" + tar -C "$stage" -czf "$archive" . + chmod 600 "$archive" + if ! sh "$0" validate "$archive"; then + rm -f "$archive" + die '生成的备份不符合还原限制' + fi + printf 'files=%s\n' "$(find "$stage" -type f | wc -l | tr -d ' ')" +} + +restore_archive() { + local upload="$1" lock_mode="${2:-acquire}" archive rollback restored + require_archive_path "$upload" + if [ "$lock_mode" = 'acquire' ]; then + lock_restore + RESTORE_LOCKED=1 + else + [ -d "$RESTORE_LOCK_DIR" ] || die '备份还原锁不存在' + fi + WORK_DIR="$(mktemp -d "$TMP_ROOT/restore.XXXXXX")" + archive="$WORK_DIR/import.tar.gz" + mv "$upload" "$archive" || die '无法接管已上传的备份文件' + validate_archive "$archive" + rollback="$WORK_DIR/rollback" + copy_current_tree "$rollback" + if ! copy_tree_to_system "$EXTRACT_DIR"; then + if copy_tree_to_system "$rollback" >/dev/null 2>&1; then + die '恢复写入失败,已回滚原配置' + fi + die '恢复写入失败,自动回滚也失败,请检查存储空间' + fi + restored="$(find "$EXTRACT_DIR" -type f ! -name manifest.json | wc -l | tr -d ' ')" + printf 'restored=%s\n' "$restored" +} + +ensure_tmp_root + +command="${1:-}" +archive="${2:-}" + +case "$command" in + prepare) cleanup_stale_upload ;; + new-upload) new_upload ;; + new-export) new_export ;; + lock) lock_restore ;; + unlock) unlock_restore ;; + export) export_archive "$archive" ;; + validate) validate_archive "$archive" ;; + restore) restore_archive "$archive" ;; + restore-held) restore_archive "$archive" held ;; + *) die '不支持的备份操作' ;; +esac diff --git a/luci-app-clashoo/root/usr/share/luci/menu.d/luci-app-clashoo.json b/luci-app-clashoo/root/usr/share/luci/menu.d/luci-app-clashoo.json new file mode 100644 index 000000000..c0c4761f8 --- /dev/null +++ b/luci-app-clashoo/root/usr/share/luci/menu.d/luci-app-clashoo.json @@ -0,0 +1,45 @@ +{ + "admin/services/clashoo": { + "title": "Clashoo", + "order": 90, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": ["luci-app-clashoo"] + } + }, + "admin/services/clashoo/overview": { + "title": "Overview", + "order": 10, + "action": { + "type": "view", + "path": "clashoo/overview" + }, + "depends": { + "acl": ["luci-app-clashoo"] + } + }, + "admin/services/clashoo/config": { + "title": "Configuration", + "order": 20, + "action": { + "type": "view", + "path": "clashoo/config" + }, + "depends": { + "acl": ["luci-app-clashoo"] + } + }, + "admin/services/clashoo/system": { + "title": "System", + "order": 30, + "action": { + "type": "view", + "path": "clashoo/system" + }, + "depends": { + "acl": ["luci-app-clashoo"] + } + } +} diff --git a/luci-app-clashoo/root/usr/share/rpcd/acl.d/luci-app-clashoo.json b/luci-app-clashoo/root/usr/share/rpcd/acl.d/luci-app-clashoo.json new file mode 100644 index 000000000..8fc522aae --- /dev/null +++ b/luci-app-clashoo/root/usr/share/rpcd/acl.d/luci-app-clashoo.json @@ -0,0 +1,21 @@ +{ + "luci-app-clashoo": { + "description": "Grant access for luci-app-clashoo", + "read": { + "cgi-io": [ "download" ], + "file": { + "/tmp/clashoo-backup/export.*": [ "read" ] + }, + "uci": [ "clashoo" ], + "ubus": { + "luci.clashoo": [ "*" ] + } + }, + "write": { + "uci": [ "clashoo" ], + "ubus": { + "luci.clashoo": [ "*" ] + } + } + } +} diff --git a/luci-app-clashoo/root/usr/share/rpcd/ucode/luci.clashoo b/luci-app-clashoo/root/usr/share/rpcd/ucode/luci.clashoo new file mode 100755 index 000000000..2a9884d03 --- /dev/null +++ b/luci-app-clashoo/root/usr/share/rpcd/ucode/luci.clashoo @@ -0,0 +1,4649 @@ +#!/usr/bin/ucode + +'use strict'; + +import { access, popen, glob, readfile, writefile, stat, unlink, open } from 'fs'; +import { cursor } from 'uci'; + +const BINARY_PATHS = [ +'/usr/bin/mihomo-stable', +'/usr/bin/mihomo', +'/usr/bin/clash-meta', +'/usr/bin/clash', +'/etc/clashoo/clash' +]; + +const SMART_BINARY_PATHS = [ +'/usr/bin/smart', +'/etc/clashoo/clash', +'/usr/bin/clash' +]; + +const SINGBOX_BIN_DIR = '/usr/share/clashoo/bin'; +const SINGBOX_RUNTIME_DIR = '/var/run/clashoo/sing-box'; +const SINGBOX_RUNTIME_CONFIG = SINGBOX_RUNTIME_DIR + '/config.json'; +const SINGBOX_WORKDIR = SINGBOX_RUNTIME_DIR + '/work'; +const SINGBOX_BINARY_PATHS = [ +SINGBOX_BIN_DIR + '/sing-box-stable', +SINGBOX_BIN_DIR + '/sing-box-alpha' +]; + +const LOG_PATHS = [ +'/usr/share/clashoo/clashoo.txt', +'/var/log/clash.log', +'/var/log/clash/clash.log', +'/tmp/clash.log' +]; +const CORE_LOG_PATH = '/var/log/clashoo/core.log'; + +const CONFIG_DIRS = [ +{ path: '/usr/share/clashoo/config/sub', type: '1' }, +{ path: '/usr/share/clashoo/config/upload', type: '2' }, +{ path: '/usr/share/clashoo/config/custom', type: '3' } +]; + +const LIST_FILE = '/usr/share/clashbackup/confit_list.conf'; +const SUB_DIR = '/usr/share/clashoo/config/sub'; +const TEMPLATE_DIR = '/usr/share/clashoo/config/custom'; +const TEMPLATE_USER_DIR = '/etc/clashoo/templates'; +const TEMPLATE_BIND_FILE = '/usr/share/clashbackup/template_bindings.conf'; +const SINGBOX_DIR = '/usr/share/clashoo/config/singbox'; +const ACCESS_CACHE_FILE = '/tmp/clashoo_check_cache'; +const ACCESS_UPDATING_FLAG = '/tmp/clashoo/access_check_updating'; +const ACCESS_DAEMON_PID_FILE = '/tmp/clashoo/access_check_daemon.pid'; +const ACCESS_CACHE_TTL = 30; +const ACCESS_UPDATING_TTL = 60; +const OVERVIEW_STATS_CACHE_FILE = '/tmp/clashoo_overview_stats_cache.json'; +const COMPONENT_UPDATE_SCRIPT = '/usr/share/clashoo/update/component_update.sh'; +const COMPONENT_UPDATE_RUNNER = '/tmp/clashoo_component_update_runner.sh'; +const COMPONENT_UPDATE_RUN_FILE = '/var/run/clashoo_component_update'; +const COMPONENT_UPDATE_LOG_FILE = '/tmp/clashoo_component_update.log'; +const COMPONENT_UPDATE_STATE_FILE = '/tmp/clashoo_component_update_state'; +const SUBSCRIPTION_UPDATE_SCRIPT = '/usr/share/clashoo/update/subscription_update.sh'; +const SUBSCRIPTION_UPDATE_STATUS_FILE = '/usr/share/clashbackup/subscription_update.status'; +const SUBSCRIPTION_UPDATE_LOCK_DIR = '/tmp/clashoo_subscription_update.lock'; +const BACKUP_TMP_DIR = '/tmp/clashoo-backup'; +const BACKUP_IMPORT_LOCK_DIR = BACKUP_TMP_DIR + '/import.lock'; +const BACKUP_IMPORT_OWNER_FILE = BACKUP_IMPORT_LOCK_DIR + '/path'; +const BACKUP_JSON_ROLLBACK_FILE = BACKUP_TMP_DIR + '/json-rollback.tar.gz'; +const BACKUP_ARCHIVE_SCRIPT = '/usr/share/clashoo/rpc/backup_archive.sh'; +const BACKUP_MAX_ARCHIVE_SIZE = 32 * 1024 * 1024; +const BACKUP_MAX_JSON_SIZE = 8 * 1024 * 1024; +const BACKUP_MAX_CHUNK_SIZE = 40 * 1024; +const BACKUP_MAX_CHUNK_BASE64_SIZE = 55000; +const BACKUP_MAX_ENTRIES = 2048; + +function uci_get(pkg, sec, opt) { +let c = cursor(); +c.load(pkg); +return c.get(pkg, sec, opt) || ''; +} + +function shell_quote(s) { +return "'" + replace(s || '', /'/g, "'\\''") + "'"; +} + +function backup_tmp_ready() { +return system('sh ' + shell_quote(BACKUP_ARCHIVE_SCRIPT) + + ' prepare >/dev/null 2>&1') == 0; +} + +function safe_backup_temp_path(path, kind) { +if (kind != 'import' && kind != 'export') return ''; +let prefix = BACKUP_TMP_DIR + '/' + kind + '.'; +if (substr(path || '', 0, length(prefix)) != prefix) return ''; +let suffix = substr(path, length(prefix)); +if (length(suffix) != 6 || match(suffix, /[^A-Za-z0-9]/)) return ''; +return path; +} + +function safe_backup_upload_path(path) { +return safe_backup_temp_path(path, 'import'); +} + +function safe_backup_export_path(path) { +return safe_backup_temp_path(path, 'export'); +} + +function new_backup_temp_path(kind) { +if (kind != 'import' && kind != 'export') return ''; +let command = kind == 'import' ? 'new-upload' : 'new-export'; +let pp = popen('sh ' + shell_quote(BACKUP_ARCHIVE_SCRIPT) + ' ' + command + ' 2>/dev/null'); +if (!pp) return ''; +let path = trim(pp.read('all')); +pp.close(); +return safe_backup_temp_path(path, kind); +} + +function active_backup_upload(path) { +return !!path && trim(readfile(BACKUP_IMPORT_OWNER_FILE) || '') == path; +} + +function refresh_backup_upload(path) { +return active_backup_upload(path) && + writefile(BACKUP_IMPORT_OWNER_FILE, path + '\n') !== null; +} + +function release_backup_upload(path) { +if (!path) return; +unlink(path); +unlink(path + '.result'); +if (active_backup_upload(path)) { + unlink(BACKUP_IMPORT_OWNER_FILE); + system('rmdir ' + shell_quote(BACKUP_IMPORT_LOCK_DIR) + ' >/dev/null 2>&1'); +} +} + +function subscription_update_status_data() { +let out = { running: !!access(SUBSCRIPTION_UPDATE_LOCK_DIR, 'r') }; +if (!access(SUBSCRIPTION_UPDATE_STATUS_FILE, 'r')) return out; +let raw = readfile(SUBSCRIPTION_UPDATE_STATUS_FILE) || ''; +for (let line in split(raw, '\n')) { + let pos = index(line, '='); + if (pos <= 0) continue; + let key = substr(line, 0, pos); + let value = substr(line, pos + 1); + if (key == 'last_run' || key == 'finished_at' || key == 'updated' || + key == 'unchanged' || key == 'failed' || key == 'skipped') + out[key] = int(value) || 0; + else if (key == 'message') out.message = value; +} +return out; +} + +function dashboard_panel_dir(name) { +if (name == 'metacubexd') return '/etc/clashoo/dashboard-metacubexd'; +if (name == 'yacd') return '/etc/clashoo/dashboard-yacd'; +if (name == 'zashboard') return '/etc/clashoo/dashboard-zashboard'; +if (name == 'razord') return '/etc/clashoo/dashboard-razord'; +return '/etc/clashoo/dashboard-zashboard'; +} + +function activate_dashboard_panel(name) { +let dir = dashboard_panel_dir(name); +if (!access(dir + '/index.html', 'r')) +return false; +system('rm -rf /etc/clashoo/dashboard /www/luci-static/yacd >/dev/null 2>&1'); +system('ln -s ' + shell_quote(dir) + ' /etc/clashoo/dashboard >/dev/null 2>&1 || cp -a ' + shell_quote(dir) + ' /etc/clashoo/dashboard >/dev/null 2>&1'); +if (name == 'yacd') +system('ln -s ' + shell_quote(dir) + ' /www/luci-static/yacd >/dev/null 2>&1'); +return true; +} + +function get_core_type() { +let core_type = uci_get('clashoo', 'config', 'core_type'); +if (core_type == 'singbox') return 'singbox'; +return 'mihomo'; +} + +function get_core_channel(family) { +let dcore = uci_get('clashoo', 'config', 'dcore'); +let legacy = uci_get('clashoo', 'config', 'core'); +family = family || get_core_type(); + +if (family == 'singbox') +return dcore == '5' ? 'alpha' : 'stable'; + +if (dcore == '1' || legacy == '1') +return 'smart'; + +if (dcore == '3' || legacy == '3') +return 'alpha'; + +return 'stable'; +} + +function get_core_label(family, channel) { +family = family || get_core_type(); +channel = channel || get_core_channel(family); +let name = family == 'singbox' ? 'sing-box' : 'mihomo'; +return name + ' ' + (channel == 'smart' ? 'smart' : channel == 'alpha' ? 'alpha' : 'stable'); +} + +function get_mihomo_service_name() { +for (let name in ['clashoo', 'clash']) { + if (access('/etc/init.d/' + name, 'x')) return name; +} +return 'clash'; +} + +function get_service_name() { +return get_core_type() === 'singbox' ? 'clashoo' : get_mihomo_service_name(); +} + +// 只看 procd 自家实例 PID。pidof 会撞 openclash / nikki / passwall 的同名进程, +// 用 ubus 走 procd 真相来源;instance 为空时保留旧的 wildcard 行为。 +function procd_instance_pid(service, instance) { +let filter = instance ? '@.' + service + '.instances.' + instance + '.pid' : + '@["' + service + '"].instances.*.pid'; +let query = '{"name":"' + service + '","verbose":true}'; +let p = popen('ubus call service list ' + shell_quote(query) + + ' 2>/dev/null | jsonfilter -e ' + shell_quote(filter) + ' 2>/dev/null | head -n1'); +if (!p) return 0; +let s = trim(p.read('all')); +p.close(); +let pid = int(s) || 0; +if (pid > 0 && !access('/proc/' + pid, 'r')) pid = 0; +return pid; +} + +function clashoo_running() { return procd_instance_pid('clashoo') > 0; } + +function singbox_binary_path(path) { +return path == SINGBOX_BINARY_PATHS[0] || path == SINGBOX_BINARY_PATHS[1]; +} + +function singbox_pid_owns_private_binary(pid) { +if (!pid || !access('/proc/' + pid, 'r')) return false; +let p = popen('readlink -f ' + shell_quote('/proc/' + pid + '/exe') + ' 2>/dev/null'); +let exe = ''; +if (p) { + exe = trim(p.read('all') || ''); + p.close(); +} +if (singbox_binary_path(exe)) return true; +let cmdline = replace(readfile('/proc/' + pid + '/cmdline') || '', /\x00/g, ' '); +for (let path in SINGBOX_BINARY_PATHS) + if (cmdline == path || substr(cmdline, 0, length(path) + 1) == path + ' ') + return true; +return false; +} + +function singbox_running() { +let pid = procd_instance_pid('clashoo', 'singbox'); +return pid > 0 && singbox_pid_owns_private_binary(pid); +} + +function running_core_family() { +if (singbox_running()) return 'singbox'; +if (clashoo_running()) return 'mihomo'; +return ''; +} + +function find_binary_for_family(family) { +if (family === 'mihomo') { + let pid = procd_instance_pid('clashoo'); + if (pid > 0) { + let p = popen('readlink -f /proc/' + pid + '/exe 2>/dev/null'); + if (p) { + let running = trim(p.read('all')); + p.close(); + if (running && access(running, 'x')) return running; + } + } + let channel = get_core_channel('mihomo'); + if (channel == 'stable' && access('/usr/bin/mihomo-stable', 'x')) return '/usr/bin/mihomo-stable'; + if (channel == 'alpha' && access('/usr/bin/mihomo', 'x')) return '/usr/bin/mihomo'; + if (channel == 'smart') { + for (let path in SMART_BINARY_PATHS) + if (access(path, 'x')) return path; + } +} +let paths = family === 'singbox' ? SINGBOX_BINARY_PATHS : BINARY_PATHS; +for (let path in paths) +if (access(path, 'x')) return path; +return ''; +} + +function find_smart_binary() { +for (let path in SMART_BINARY_PATHS) +if (access(path, 'x')) return path; +return ''; +} + +function find_binary() { +let family = running_core_family() || get_core_type(); +let binary = find_binary_for_family(family); +if (binary) return binary; +if (family != get_core_type()) return find_binary_for_family(get_core_type()); +return ''; +} + +function read_first_line(path) { +if (!access(path, 'r')) return ''; +let content = readfile(path); +return content ? trim(split(content, '\n')[0] || '') : ''; +} + +function runtime_state_get(key) { +if (!key || !access('/tmp/clashoo/runtime_state', 'r')) return ''; +let content = readfile('/tmp/clashoo/runtime_state') || ''; +for (let line in split(content, '\n')) { +let pos = index(line, '='); +if (pos <= 0) continue; +if (substr(line, 0, pos) == key) +return trim(substr(line, pos + 1)); +} +return ''; +} + +function is_running() { +return running_core_family() != ''; +} + +function get_local_ip() { +let p = popen("ip -4 addr show br-lan 2>/dev/null | awk '/inet /{sub(/\\/.*/,\"\",$2); print $2; exit}'"); +if (!p) return ''; +let ip = trim(p.read('all')); +p.close(); +if (ip) return ip; +p = popen("dev=$(ip -4 route show default 2>/dev/null | awk '{print $5; exit}'); [ -n \"$dev\" ] && ip -4 addr show dev \"$dev\" 2>/dev/null | awk '/inet /{sub(/\\/.*/,\"\",$2); print $2; exit}'"); +if (!p) return ''; +ip = trim(p.read('all')); +p.close(); +return ip; +} + +function get_core_version(binary) { +if (!binary) return ''; +let cmd = match(binary, /sing-box(-[a-z]+)?$/) +? (shell_quote(binary) + ' version 2>&1 | head -1') +: (shell_quote(binary) + ' -v 2>&1 | head -1'); +let p = popen(cmd); +if (!p) return ''; +let v = trim(p.read('all')); +p.close(); +return v; +} + +function sync_legacy_core_fields(c) { +let family = c.get('clashoo', 'config', 'core_type') || 'mihomo'; +let dcore = c.get('clashoo', 'config', 'dcore') || '2'; +let legacy = '2'; + +if (family == 'singbox') +legacy = dcore == '5' ? '5' : '4'; +else +legacy = dcore == '1' ? '1' : dcore == '3' ? '3' : '2'; + +c.set('clashoo', 'config', 'core', legacy); +} + +function has_mihomo_stable() { +if (access('/usr/bin/mihomo-stable', 'x')) return true; +if (system("[ -x /usr/bin/clash-meta ] && [ ! -L /usr/bin/clash-meta ]") != 0) return false; +let version = get_core_version('/usr/bin/clash-meta'); +return !!version && !match(version, /(alpha|beta|rc|pre|smart)/i); +} + +function core_target_error(core, dcore) { +if (core == 'mihomo' && dcore == '1') { + if (find_smart_binary()) return null; + return { error: 'smart_core_missing', message: '未检测到 Smart 内核,请先在“组件更新”中安装 Smart' }; +} +if (core == 'mihomo' && dcore == '2') { + if (has_mihomo_stable()) return null; + return { error: 'mihomo_stable_missing', message: '未检测到 mihomo 稳定版,请先在“组件更新”中更新稳定版' }; +} +if (core == 'mihomo' && dcore == '3') { + if (access('/usr/bin/mihomo', 'x')) return null; + return { error: 'mihomo_alpha_missing', message: '未检测到 mihomo Alpha,请先在“组件更新”中更新 Alpha' }; +} +if (core == 'singbox' && dcore == '4') { + if (access(SINGBOX_BINARY_PATHS[0], 'x')) return null; + return { error: 'singbox_stable_missing', message: '未检测到 sing-box 稳定版,请先在“组件更新”中更新稳定版' }; +} +if (core == 'singbox' && dcore == '5') { + if (access(SINGBOX_BINARY_PATHS[1], 'x')) return null; + return { error: 'singbox_alpha_missing', message: '未检测到 sing-box Alpha,请先在“组件更新”中更新 Alpha' }; +} +return { error: 'invalid_core_target', message: '无效的内核类型与版本组合' }; +} + +function prepare_singbox_runtime() { +let active = uci_get('clashoo', 'config', 'singbox_active') || ''; +let src = active ? (SINGBOX_DIR + '/' + active) : SINGBOX_RUNTIME_CONFIG; +if (!access(src, 'r')) src = SINGBOX_RUNTIME_CONFIG; +if (!access(src, 'r')) + return { success: false, message: '请先生成或选择 sing-box 配置文件' }; + +system('mkdir -p ' + shell_quote(SINGBOX_RUNTIME_DIR) + ' ' + shell_quote(SINGBOX_WORKDIR) + ' >/dev/null 2>&1'); +let tmp = shell_read('mktemp ' + shell_quote(SINGBOX_RUNTIME_CONFIG + '.tmp.XXXXXX') + ' 2>/dev/null'); +if (!tmp) + return { success: false, message: '无法创建 sing-box 临时运行配置' }; + +let raw = readfile(src) || ''; +let cfg = raw ? json(raw) : null; +if (!cfg || type(cfg) != 'object') { + unlink(tmp); + return { success: false, message: 'sing-box 配置 JSON 无法解析' }; +} +cfg = migrate_singbox(cfg); +if (writefile(tmp, sprintf('%J\n', cfg)) === null) { + unlink(tmp); + return { success: false, message: '无法写入 sing-box 临时运行配置' }; +} + +if (uci_get('clashoo', 'config', 'core_only') != '1') { + if (system('command -v ucode >/dev/null 2>&1') != 0) { + unlink(tmp); + return { success: false, message: 'ucode is required to normalize sing-box runtime config' }; + } + let redir_port = uci_get('clashoo', 'config', 'redir_port') || '7891'; + let tproxy_port = uci_get('clashoo', 'config', 'tproxy_port') || '7982'; + let mixed_port = uci_get('clashoo', 'config', 'mixed_port') || '7890'; + let dns_port = uci_get('clashoo', 'config', 'listen_port') || '1053'; + let dash_port = uci_get('clashoo', 'config', 'dash_port') || '9090'; + let dash_pass = uci_get('clashoo', 'config', 'dash_pass') || ''; + let has_tun = system('(ip tuntap add mode tun name cotuntest >/dev/null 2>&1 && ip link del cotuntest >/dev/null 2>&1)') == 0 ? '1' : '0'; + let normalize_cmd = 'ucode /usr/share/clashoo/lib/normalize_singbox_config.uc ' + + shell_quote(tmp) + ' ' + + shell_quote(redir_port) + ' ' + + shell_quote(tproxy_port) + ' ' + + shell_quote(mixed_port) + ' ' + + shell_quote(has_tun) + ' ' + + shell_quote('6666') + ' ' + + shell_quote(dns_port) + ' ' + + shell_quote(dash_port) + ' ' + + shell_quote(dash_pass) + + ' >/dev/null 2>&1'; + if (system(normalize_cmd) != 0) { + unlink(tmp); + return { success: false, message: 'sing-box 运行配置规范化失败' }; + } +} + +if (system('mv -f ' + shell_quote(tmp) + ' ' + shell_quote(SINGBOX_RUNTIME_CONFIG) + ' >/dev/null 2>&1') != 0) { + unlink(tmp); + return { success: false, message: '无法提交 sing-box 运行配置' }; +} +return { success: true }; +} + +function detect_firewall_backend() { +if (system('command -v fw4 >/dev/null 2>&1') == 0) return 'fw4'; +if (system('command -v firewall >/dev/null 2>&1') == 0) return 'fw3'; +return ''; +} + +function detect_missing_fw4_tools() { +let missing = []; +if (system('command -v fw4 >/dev/null 2>&1') != 0) push(missing, 'fw4'); +if (system('command -v nft >/dev/null 2>&1') != 0) push(missing, 'nft'); +if (system('command -v ip >/dev/null 2>&1') != 0) push(missing, 'ip'); +return missing; +} + +function get_profiles() { +let profiles = []; +let dirs = ['/etc/clashoo/profiles']; + +function has_profile_name(arr, name) { +for (let i = 0; i < length(arr); i++) +if (arr[i] == name) return true; +return false; +} + +for (let dir in dirs) { +if (!access(dir, 'r')) continue; +for (let f in (glob(dir + '/*.yaml') || [])) { +let name = replace(f, dir + '/', ''); +if (name && name != 'config.yaml' && !has_profile_name(profiles, name)) +push(profiles, name); +} +if (length(profiles) > 0) break; +} +return profiles; +} + +function format_log_dates(text) { +return replace(text || '', /\d{4}-(\d{2}-\d{2}) /g, '$1 '); +} + +function format_update_log(text) { +let out = []; +for (let line in split(format_log_dates(text || ''), '\n')) { +line = trim(line || ''); +if (!line) continue; +if (index(line, '启动任务已触发') >= 0 || + index(line, '停止任务已触发') >= 0 || + index(line, '重启任务已触发') >= 0) + continue; +push(out, line); +} +return join('\n', out); +} + +function reformat_core_line(line) { +if (match(line, /^(===|---)/)) return line; +// Extract local time+date "May 26 23:16:18 2026" → "05-26 23:16:18" +let local_t = ''; +let syslog_tm = match(line, /^\w{3} (\d{1,2}) (\d{2}:\d{2}:\d{2}) \d{4}/); +if (syslog_tm) { + let months = { Jan:'01',Feb:'02',Mar:'03',Apr:'04',May:'05',Jun:'06',Jul:'07',Aug:'08',Sep:'09',Oct:'10',Nov:'11',Dec:'12' }; + let mon_match = match(line, /^\w{3}/); + let mon = mon_match ? months[mon_match[0]] || mon_match[0] : '??'; + local_t = mon + '-' + syslog_tm[1].padStart(2,'0') + ' ' + syslog_tm[2]; +} +// Strip syslog prefix "...process[pid]: " +let msg = replace(line, /^.*?\[\d+\]:\s*/, ''); +if (msg === line) msg = line; +// mihomo: time="YYYY-MM-DDTHH:MM:SS..." level=LEVEL msg="TEXT" +let mm = match(msg, /^time="\d{4}-(\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})[^"]*"\s+level=(\w+)\s+msg="(.*)"\s*$/); +if (mm) { + let t = local_t ? local_t : mm[1] + ' '; + let h = +substr(mm[2], 0, 2); t += sprintf('%02d', (h + 8) % 24) + substr(mm[2], 2); + if (mm[3] === 'info') return t + ' ' + mm[4]; + return t + ' [' + mm[3] + '] ' + mm[4]; +} +// sing-box ISO "T" format: "YYYY-MM-DDTHH:MM:SS... LEVEL message" +let sm = match(msg, /^\d{4}-(\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})[^ ]* +(\w+) +(.*)/); +if (sm) { + let t = local_t ? local_t : sm[1] + ' '; + let h = +substr(sm[2], 0, 2); t += sprintf('%02d', (h + 8) % 24) + substr(sm[2], 2); + let lvl = lc(sm[3]); + if (lvl === 'info') return t + ' ' + sm[4]; + return t + ' [' + lvl + '] ' + sm[4]; +} +// sing-box space format: "[+0000] YYYY-MM-DD HH:MM:SS LEVEL message" +let ss = match(msg, /^[+-]\d{4} \d{4}-\d{2}-\d{2} (\d{2}:\d{2}:\d{2}) +(\w+) +(.*)/); +if (ss) { + let t = local_t; + if (!t) { let h = +substr(ss[1], 0, 2); t = sprintf('%02d', (h + 8) % 24) + substr(ss[1], 2); } + let lvl = lc(ss[2]); + if (lvl === 'info') return t + ' ' + ss[3]; + return t + ' [' + lvl + '] ' + ss[3]; +} +// fallback: strip ISO timestamp, keep syslog time +msg = replace(msg, /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^ ]*\s*/, ''); +return (local_t ? local_t + ' ' : '') + trim(msg); +} + +function get_log_content() { +// 统一用 awk 脚本简化 mihomo 原生日志 + log_msg 行格式 +let formatter = "awk -f /usr/share/clashoo/net/log_format.awk 2>/dev/null"; + +for (let path in LOG_PATHS) { +if (!access(path, 'r')) continue; +let p = popen("tail -300 '" + path + "' | " + formatter); +if (p) { let c = p.read('all'); p.close(); return c; } +} +if (get_core_type() == 'singbox') { +let p0 = popen("logread -e 'sing-box' 2>/dev/null | tail -300"); +if (p0) { +let c0 = trim(p0.read('all')); +p0.close(); +if (c0) return c0; +} +} +return ''; +} + +function format_core_log(content) { +content = content || ''; +let out = ['=== 核心日志 ===']; +// No server-side noise filtering: the UI level dropdown (全部/INFO/WARN/ERROR/FATAL) +// is the single filter, so "全部" really means all. Keep a generous cap so errors +// aren't pushed out by connection chatter. +let max_lines = 400; + +for (let line in split(content, '\n')) { +line = replace(line || '', /\x1b\[[0-9;]*m/g, ''); +line = trim(line || ''); +if (!line) continue; + +push(out, reformat_core_line(line)); +if (length(out) > max_lines + 1) shift(out); +} + + if (length(out) == 1) + push(out, '暂无核心日志'); + return join('\n', out); +} + +function list_config_files() { +let configs = []; +let subs = read_subscriptions(); + +function has_name(arr, n) { +for (let i = 0; i < length(arr); i++) +if (arr[i] == n) return true; +return false; +} + +function sub_has_name(n) { +for (let i = 0; i < length(subs); i++) { +if (subs[i].name == n) return true; +} +return false; +} + +for (let dir in CONFIG_DIRS) { +for (let f in (glob(dir.path + '/*.yaml') || [])) { +let name = replace(f, dir.path + '/', ''); +if (dir.type == '1') { +if (!sub_has_name(name)) +continue; +} +if (!has_name(configs, name)) push(configs, name); +} + +for (let f in (glob(dir.path + '/*.yml') || [])) { +let name = replace(f, dir.path + '/', ''); +if (dir.type == '1') { +if (!sub_has_name(name)) +continue; +} +if (!has_name(configs, name)) push(configs, name); +} +} + +let cur = get_current_config(); +if (cur && !has_name(configs, cur)) +push(configs, cur); + +return configs; +} + +function first_available_config(exclude_path) { +for (let dir in CONFIG_DIRS) { +for (let f in (glob(dir.path + '/*.yaml') || [])) { +if (f != exclude_path) +return { path: f, type: dir.type }; +} +for (let f in (glob(dir.path + '/*.yml') || [])) { +if (f != exclude_path) +return { path: f, type: dir.type }; +} +} +return null; +} + +function get_current_config(core_family) { +core_family = core_family || get_core_type(); + +if (core_family == 'singbox') { + let active = uci_get('clashoo', 'config', 'singbox_active') || ''; + let active_name = replace(active, /^.*\//, ''); + /* 文件已被删除时不返回 stale 名 */ + if (active_name && access(SINGBOX_DIR + '/' + active_name, 'r')) return active_name; + if (access(SINGBOX_RUNTIME_CONFIG, 'r')) return 'config.json'; + return ''; +} + +let use_config = uci_get('clashoo', 'config', 'use_config'); +let name = replace(use_config, /^.*\//, ''); +if (name && use_config && !access(use_config, 'r')) return ''; +return name; +} + +function get_display_config_name(core_family) { +core_family = core_family || get_core_type(); + +if (core_family == 'singbox') { + let active = uci_get('clashoo', 'config', 'singbox_active') || ''; + let active_name = replace(active, /^.*\//, ''); + if (active_name) { + if (match(active_name, /\.singbox\.json$/)) + return replace(active_name, /\.singbox\.json$/, '.json'); + return active_name; + } + if (access(SINGBOX_RUNTIME_CONFIG, 'r')) + return 'config.json'; + return 'config.json'; +} + +return get_current_config('mihomo'); +} + +function now_epoch_sec() { +let p = popen('date +%s 2>/dev/null'); +if (!p) return 0; +let raw = trim(p.read('all')); +p.close(); +return raw ? int(raw) : 0; +} + +function default_probe_result() { +return { +ok: false, +state: 'pending', +code: '000', +ok_count: 0, +attempts: 0, +loss: 0, +avg_ms: 0 +}; +} + +function default_access_payload(proxy_port, tcp_mode, udp_mode) { +return { +proxy_port: proxy_port || '7890', +tcp_mode: tcp_mode || 'redirect', +udp_mode: udp_mode || (tcp_mode || 'redirect'), +updated_at: 0, +stale: true, +updating: true, +last_checked_ago: -1, +direct: { +bytedance: default_probe_result(), +youtube: default_probe_result() +}, +proxy: { +bytedance: default_probe_result(), +youtube: default_probe_result() +} +}; +} + +function read_access_check_cache() { +if (!access(ACCESS_CACHE_FILE, 'r')) return null; +let raw = trim(readfile(ACCESS_CACHE_FILE) || ''); +if (!raw) return null; +let parsed = json(raw); +return parsed ? parsed : null; +} + +function access_cache_is_stale(cache, now) { +if (!cache || !cache.updated_at) return true; +let updated = int(cache.updated_at); +return updated <= 0 || (updated + ACCESS_CACHE_TTL) < now; +} + +function trigger_access_cache_refresh() { +system('mkdir -p /tmp/clashoo 2>/dev/null; : > ' + ACCESS_UPDATING_FLAG + ' 2>/dev/null; nice -n 19 sh /usr/share/clashoo/net/access_check_cache.sh >/dev/null 2>&1 &'); +} + +function access_check_is_updating(now) { +if (!access(ACCESS_UPDATING_FLAG, 'r')) return false; +let st = stat(ACCESS_UPDATING_FLAG); +let mtime = st ? int(st.mtime || 0) : 0; +if (mtime > 0 && now > 0 && (mtime + ACCESS_UPDATING_TTL) < now) { +unlink(ACCESS_UPDATING_FLAG); +return false; +} +return true; +} + +function access_check_daemon_running() { +if (!access(ACCESS_DAEMON_PID_FILE, 'r')) return false; +let pid = trim(readfile(ACCESS_DAEMON_PID_FILE) || ''); +if (!pid) return false; +if (!access('/proc/' + pid, 'r')) return false; +let cmd = readfile('/proc/' + pid + '/cmdline') || ''; +return index(cmd, 'access_check_daemon.sh') >= 0; +} + +function ensure_access_check_daemon() { +if (access_check_daemon_running()) return true; +system('nohup nice -n 19 sh /usr/share/clashoo/net/access_check_daemon.sh >/dev/null 2>&1 0 ? (now_epoch_sec() - payload.updated_at) : -1; +return payload; +} + +function get_cpu_arch() { +let p = popen("opkg status libc 2>/dev/null | grep Architecture | awk -F': ' '{print $2}'"); +if (!p) return ''; +let arch = trim(p.read('all')); +p.close(); +if (arch) return arch; +p = popen("apk --print-arch 2>/dev/null"); +if (!p) return ''; +arch = trim(p.read('all')); +p.close(); +return arch; +} + +function shell_read(cmd) { +let p = popen(cmd); +if (!p) return ''; +let out = trim(p.read('all') || ''); +p.close(); +return out; +} + +function pkg_version(pkg) { +let q = shell_quote(pkg); +return shell_read("if command -v opkg >/dev/null 2>&1; then opkg status " + q + " 2>/dev/null | awk -F': ' '/^Version:/{print $2; exit}'; elif command -v apk >/dev/null 2>&1; then apk list -I " + q + " 2>/dev/null | awk '{print $1; exit}' | sed -n 's/^" + pkg + "-//p'; fi"); +} + +// lgbm has no version; use a short sha256 of the model as its identifier (like Smart's hash) +function lgbm_local_ver() { +if (!stat('/etc/clashoo/Model.bin')) return ''; +return trim(shell_read("sha256sum /etc/clashoo/Model.bin 2>/dev/null | cut -c1-12") || ''); +} + +function file_mtime_label(path) { +let s = stat(path); +if (!s) return ''; +let ts = shell_read('date -d @' + s.mtime + ' "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r ' + shell_quote(path) + ' "+%Y-%m-%d %H:%M:%S" 2>/dev/null'); +return ts || ('' + s.mtime); +} + +function read_component_state() { +let state = {}; +if (!access(COMPONENT_UPDATE_STATE_FILE, 'r')) return state; +let content = readfile(COMPONENT_UPDATE_STATE_FILE) || ''; +for (let line in split(content, '\n')) { +let pos = index(line, '='); +if (pos <= 0) continue; +state[substr(line, 0, pos)] = trim(substr(line, pos + 1)); +} +return state; +} + +function component_log_tail(lines) { +lines = lines || 80; +return shell_read('tail -' + int(lines) + ' ' + shell_quote(COMPONENT_UPDATE_LOG_FILE) + ' 2>/dev/null'); +} + +function component_last_log() { +return shell_read('tail -1 ' + shell_quote(COMPONENT_UPDATE_LOG_FILE) + ' 2>/dev/null'); +} + +// 从 `-v` / `version` 完整输出里抽短版本号: +// mihomo/smart "Mihomo Meta linux ..." → ;sing-box "sing-box version " → +function core_short_version(binary) { +let full = get_core_version(binary); +if (!full) return ''; +let m = match(full, /Meta[ \t]+([^ \t]+)/); +if (m) return m[1]; +m = match(full, /version[ \t]+([^ \t]+)/); +if (m) return m[1]; +return full; +} + +function component_installed_label(id) { +if (id == 'clashoo') return pkg_version('clashoo') || '未安装'; +if (id == 'luci') return pkg_version('luci-app-clashoo') || '未安装'; +if (id == 'mihomo') { +let bin = find_binary_for_family('mihomo'); +return bin ? (core_short_version(bin) || '已安装') : '未安装'; +} +if (id == 'smart') { +let bin = find_smart_binary(); +return bin ? (core_short_version(bin) || '已安装') : '未安装'; +} +if (id == 'singbox') { +let bin = find_binary_for_family('singbox'); +return bin ? (core_short_version(bin) || '已安装') : '未安装'; +} +if (id == 'lgbm') return lgbm_local_ver() || '未安装'; +if (id == 'china') return file_mtime_label('/usr/share/clashoo/nftables/geoip_cn.nft') || '未更新'; +if (id == 'geoip') return file_mtime_label('/etc/clashoo/Country.mmdb') || '未安装'; +return ''; +} + +function mihomo_alpha_label(ver) { +ver = ver || ''; +if (!ver) return ''; +if (match(ver, /^alpha-/)) return ver; +if (match(ver, /^[0-9a-fA-F]{7,}$/)) return 'alpha-' + ver; +return ver; +} + +function is_prerelease_label(ver) { +return !!match(ver || '', /(alpha|beta|rc|pre|smart)/i); +} + +function component_installed_versions(id) { +let out = {}; +if (id == 'mihomo') { + let stable = access('/usr/bin/mihomo-stable', 'x') ? read_first_line('/usr/share/clashoo/clash_meta_version') : ''; + if (!stable && access('/usr/bin/mihomo-stable', 'x')) stable = core_short_version('/usr/bin/mihomo-stable'); + if (is_prerelease_label(stable)) stable = ''; + let alpha = read_first_line('/usr/share/clashoo/mihomo_version'); + if (match(alpha, /^smart-/)) alpha = ''; + if (!alpha && access('/usr/bin/mihomo', 'x')) alpha = core_short_version('/usr/bin/mihomo'); + out.stable = stable || ''; + out.alpha = mihomo_alpha_label(alpha); + return out; +} +if (id == 'singbox') { + let stable = read_first_line('/usr/share/clashoo/singbox_stable_version'); + if (!stable && access(SINGBOX_BINARY_PATHS[0], 'x')) stable = core_short_version(SINGBOX_BINARY_PATHS[0]); + if (is_prerelease_label(stable)) stable = ''; + let alpha = read_first_line('/usr/share/clashoo/singbox_alpha_version'); + if (!alpha && access(SINGBOX_BINARY_PATHS[1], 'x')) alpha = core_short_version(SINGBOX_BINARY_PATHS[1]); + out.stable = stable || ''; + out.alpha = alpha || ''; + return out; +} +return out; +} + +const FEED_BASE = 'https://down.dllkids.xyz/openwrt-feed/clashoo'; +// bucket 多项目共用,按项目区分文件名免互相 sync 覆盖 +const FEED_MANIFEST_NAME = 'manifest-clashoo.txt'; +const GH_CLASHOO_REPO = 'kenzok8/openwrt-clashoo'; + +// 上游 amd64 默认是 AMD64-v3(要 AVX2),LXC/KVM 跑不起来;老值统一兜底 compatible +function normalize_mihomo_arch(a) { +if (!a) a = get_cpu_arch(); +if (!a) return 'amd64-compatible'; +if (a == 'amd64-compatible' || a == 'amd64-v1' || a == 'amd64-v2' || a == 'amd64-v3') return a; +if (a == 'amd64' || a == 'x86_64') return 'amd64-compatible'; +if (match(a, /^aarch64/)) return 'arm64'; +if (match(a, /^armv7|^arm_cortex-a[7-9]|^arm_cortex-a1[0-9]/)) return 'armv7'; +if (match(a, /^armv6|^arm_cortex-a[56]/)) return 'armv6'; +if (match(a, /^arm/) && a != 'arm64' && a != 'armv7' && a != 'armv6' && a != 'armv5') return 'armv5'; +if (match(a, /^i[3-6]86/)) return '386'; +if (match(a, /^mips64el/)) return 'mips64le'; +if (match(a, /^mips64/)) return 'mips64'; +if (match(a, /^mipsel/)) return 'mipsle'; +if (match(a, /^mips/) && a != 'mips64' && a != 'mipsle' && a != 'mips64le') return 'mips'; +return a; +} + +// alpha / smart 滚动版按 hash 比对;needle 用 -alpha- 边界,免 amd64 撞 amd64-compatible +function alpha_asset_ver(html, arch, kind) { +if (!html) return ''; +let needle = (kind == 'smart') + ? 'mihomo-linux-' + arch + '-alpha-smart-' + : 'mihomo-linux-' + arch + '-alpha-'; +for (let line in split(html, '\n')) { + if (index(line, needle) < 0) continue; + if (kind != 'smart' && index(line, '-alpha-smart-') >= 0) continue; + let m = (kind == 'smart') + ? match(line, /-(alpha-smart-[0-9A-Za-z]{6,})\.gz/) + : match(line, /-(alpha-[0-9a-f]{7,})\.gz/); + if (m) return m[1]; +} +return ''; +} + +// 从 release expanded_assets HTML 抽 clashoo / luci 版本号 +function gh_assets_versions(html) { +let out = {}; +if (!html) return out; +for (let line in split(html, '\n')) { + let m; + if (!out.clashoo) { + m = match(line, /(^|[\/" >])(clashoo_[^"<>\/ ]+_[^"<>\/ ]+\.ipk)/) || match(line, /(^|[\/" >])(clashoo-[^"<>\/ ]+\.apk)/); + if (m) { + let f = m[2]; + let v = match(f, /^clashoo_([^_]+)_.+\.ipk$/) || match(f, /^clashoo-(.+-r[0-9]+)-.+\.apk$/); + if (v) out.clashoo = v[1]; + } + } + if (!out.luci) { + m = match(line, /(^|[\/" >])(luci-app-clashoo_[^"<>\/ ]+_all\.ipk)/) || match(line, /(^|[\/" >])(luci-app-clashoo-[^"<>\/ ]+\.apk)/); + if (m) { + let f = m[2]; + let v = match(f, /^luci-app-clashoo_(.+)_all\.ipk$/) || match(f, /^luci-app-clashoo-(.+-r[0-9]+)-.+\.apk$/); + if (v) out.luci = v[1]; + } + } + if (out.clashoo && out.luci) break; +} +return out; +} + +// 并行抓取全部上游版本 —— 6+ 个 curl 串行会超 ubus 30s 上限,故全部 +// 后台并发,wait 后统一解析(总耗时 ≈ 最慢的单个请求)。 +// DISTRIB_RELEASE 可能是自定义串,不能当 SDK 版本;按包管理器试多个候选。 +function component_latest_versions() { +let latest = {}; +let core_arch = normalize_mihomo_arch(uci_get('clashoo', 'config', 'download_core') || ''); +let pkg_arch = trim(shell_read("opkg print-architecture 2>/dev/null | awk '/^arch /{print $2}' | tail -1") || ''); +if (!pkg_arch) pkg_arch = trim(shell_read('apk --print-arch 2>/dev/null') || ''); +let is_apk = !!trim(shell_read('command -v apk 2>/dev/null') || ''); +let sdks = is_apk ? ['25.12', '24.10'] : ['24.10', '23.05', '22.03', '21.02']; + +let dir = trim(shell_read('mktemp -d 2>/dev/null') || ''); +if (!dir) return latest; + +let GH = 'https://github.com/'; +// geoip 远端版本跟随当前 geoip_source:从生效的 mmdb 源 URL 解析出 GitHub 仓库; +// source=1 是 MaxMind(无 GitHub release),跳过 +let geoip_src = uci_get('clashoo', 'config', 'geoip_source') || ''; +let geoip_repo = ''; +if (geoip_src != '1') { + let mmdb_url = uci_get('clashoo', 'config', 'geoip_mmdb_url') || 'https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb'; + let gm = match(mmdb_url, /github[a-z]*\.com\/([^\/]+)\/([^\/]+)/); + if (gm) geoip_repo = gm[1] + '/' + gm[2]; +} +// In kernel-only mode there is no TPROXY, so these github/api/B2 probes would +// leak out direct and stall at their -m timeout behind the GFW (the "检查更新 +// 半天不出反馈"). Route them through the running core, same as the download +// scripts (shared logic in proxy_lib.sh). Normal mode -> empty -> TPROXY. +let cdp = trim(shell_read('. /usr/share/clashoo/update/proxy_lib.sh; clashoo_detect_proxy 2>/dev/null') || ''); +let px = cdp ? ('--proxy ' + shell_quote(cdp) + ' ') : ''; +let cmd = ''; +cmd += "curl -fsS --connect-timeout 4 -o /dev/null -w '%{redirect_url}' -m 8 " + shell_quote(GH + 'MetaCubeX/mihomo/releases/latest') + ' >' + shell_quote(dir + '/ms') + ' 2>/dev/null &\n'; +cmd += "curl -fsS --connect-timeout 4 -o /dev/null -w '%{redirect_url}' -m 8 " + shell_quote(GH + 'SagerNet/sing-box/releases/latest') + ' >' + shell_quote(dir + '/ss') + ' 2>/dev/null &\n'; +cmd += 'curl -fsSL --connect-timeout 4 -m 8 ' + shell_quote(GH + 'MetaCubeX/mihomo/releases/expanded_assets/Prerelease-Alpha') + ' >' + shell_quote(dir + '/ma') + ' 2>/dev/null &\n'; +cmd += 'curl -fsSL --connect-timeout 4 -m 8 ' + shell_quote(GH + 'vernesong/mihomo/releases/expanded_assets/Prerelease-Alpha') + ' >' + shell_quote(dir + '/sm') + ' 2>/dev/null &\n'; +cmd += 'curl -fsSL --connect-timeout 4 -m 8 ' + shell_quote(GH + 'SagerNet/sing-box/releases.atom') + ' >' + shell_quote(dir + '/sa') + ' 2>/dev/null &\n'; +if (pkg_arch) + for (let sdk in sdks) + cmd += 'curl -fsSL --connect-timeout 4 -m 8 ' + shell_quote(FEED_BASE + '/' + sdk + '/' + pkg_arch + '/' + FEED_MANIFEST_NAME) + ' >' + shell_quote(dir + '/b_' + sdk) + ' 2>/dev/null &\n'; +// GitHub API 兜底,避免 expanded_assets 在部分网络下超时 +cmd += 'curl -fsSL --connect-timeout 4 -m 8 ' + shell_quote('https://api.github.com/repos/' + GH_CLASHOO_REPO + '/releases/latest') + ' >' + shell_quote(dir + '/gc') + ' 2>/dev/null &\n'; +// lgbm —— remote model sha256 from the release assets page (each asset lists its digest) +cmd += 'curl -fsSL --connect-timeout 4 -m 8 ' + shell_quote(GH + 'vernesong/mihomo/releases/expanded_assets/LightGBM-Model') + ' >' + shell_quote(dir + '/lg') + ' 2>/dev/null &\n'; +// geoip —— 源仓库 releases/latest 的 302 tag(多为日期串) +if (geoip_repo) + cmd += "curl -fsS --connect-timeout 4 -o /dev/null -w '%{redirect_url}' -m 8 " + shell_quote(GH + geoip_repo + '/releases/latest') + ' >' + shell_quote(dir + '/gi') + ' 2>/dev/null &\n'; +// inject the local-core proxy into every probe above (no-op when px is empty) +if (px) cmd = replace(cmd, 'curl ', 'curl ' + px); +cmd += 'wait\n'; +let pp = popen(cmd); +if (pp) { pp.read('all'); pp.close(); } + +let m; +// 稳定版 tag —— releases/latest 302 重定向 +m = match(readfile(dir + '/ms') || '', /\/tag\/([^\/?#\s]+)/); if (m) latest.mihomo_stable = m[1]; +m = match(readfile(dir + '/ss') || '', /\/tag\/([^\/?#\s]+)/); if (m) latest.singbox_stable = m[1]; +// sing-box alpha —— atom feed 第一个 prerelease tag(带 -alpha/-beta/-rc); +// SagerNet 稳定与 alpha 并行发布,atom 按时间排序,最新 entry 可能是稳定版(如 v1.13.13), +// 直接取第一个 entry 会让 alpha 通道误显示稳定版本号 +m = match(readfile(dir + '/sa') || '', /\/releases\/tag\/(v[^"?#\s]*-[^"?#\s]+)/); if (m) latest.singbox_alpha = m[1]; +// lgbm —— locate the configured asset in the assets page, then its sha256 digest +let lg_html = readfile(dir + '/lg') || ''; +let lg_url = uci_get('clashoo', 'config', 'smart_lgbm_url') || 'Model.bin'; +let lg_parts = split(lg_url, '/'); +let lg_asset = lg_parts[length(lg_parts) - 1] || 'Model.bin'; +let lg_pos = index(lg_html, '/' + lg_asset + '"'); +if (lg_pos >= 0) { + let lg_sha = match(substr(lg_html, lg_pos), /sha256:([0-9a-f]{12})/); + if (lg_sha) latest.lgbm = lg_sha[1]; +} +// geoip —— 源仓库最新 release tag(多为日期串 YYYYMMDDhhmm)→ YYYY-MM-DD +let gt = match(readfile(dir + '/gi') || '', /\/tag\/([^\/?#\s]+)/); +if (gt) { + let d8 = match(gt[1], /^(\d{4})(\d{2})(\d{2})/); + latest.geoip = d8 ? (d8[1] + '-' + d8[2] + '-' + d8[3]) : gt[1]; +} +// mihomo alpha / smart —— Prerelease-Alpha 资源清单的 hash +let v; +v = alpha_asset_ver(readfile(dir + '/ma') || '', core_arch, 'alpha'); if (v) latest.mihomo_alpha = v; +v = alpha_asset_ver(readfile(dir + '/sm') || '', core_arch, 'smart'); if (v) latest.smart = v; +// clashoo / luci —— R2 feed manifest,按 SDK 候选取第一个命中 +if (pkg_arch) +for (let sdk in sdks) { + let txt = readfile(dir + '/b_' + sdk); + if (!txt) continue; + let hit = false; + for (let line in split(txt, '\n')) { + let kv = match(trim(line), /^(core|luci)=(.+)$/); + if (!kv) continue; + let f = kv[2], mv; + if (kv[1] == 'core') { + mv = match(f, /^clashoo_([^_]+)_.+\.ipk$/) || match(f, /^clashoo-(.+-r[0-9]+)-.+\.apk$/); + if (mv) { latest.clashoo = mv[1]; hit = true; } + } else { + mv = match(f, /^luci-app-clashoo_([^_]+)_.+\.ipk$/) || match(f, /^luci-app-clashoo-(.+-r[0-9]+)-.+\.apk$/); + if (mv) latest.luci = mv[1]; + } + } + if (hit) break; +} +// R2 feed 缺啥用 GitHub 兜底 +if (!latest.clashoo || !latest.luci) { + let gh = gh_assets_versions(readfile(dir + '/gc') || ''); + if (!latest.clashoo && gh.clashoo) latest.clashoo = gh.clashoo; + if (!latest.luci && gh.luci) latest.luci = gh.luci; +} + +let rp = popen('rm -rf ' + shell_quote(dir)); if (rp) rp.close(); +return latest; +} + +function component_defs() { +return [ +{ id: 'clashoo', name: 'Clashoo 核心', description: 'mihomo 运行时与脚本', kind: 'pkg' }, +{ id: 'luci', name: '客户端', description: 'LuCI 界面与中文语言包', kind: 'pkg' }, +{ id: 'mihomo', name: 'mihomo 内核', description: 'Clash Meta 核心', kind: 'core', variant: true }, +{ id: 'singbox', name: 'sing-box 内核', description: 'sing-box 核心', kind: 'core', variant: true }, +{ id: 'smart', name: 'mihomo Smart', description: 'vernesong Smart 核心', kind: 'core' }, +{ id: 'lgbm', name: 'LightGBM 模型', description: 'Smart 策略模型文件', kind: 'data' }, +{ id: 'china', name: '大陆白名单', description: '大陆 IP 直连白名单', kind: 'data' }, +{ id: 'geoip', name: 'GeoIP / GeoSite', description: 'GeoIP / GeoSite 数据', kind: 'data' } +]; +} + +function valid_component_id(id) { +for (let comp in component_defs()) +if (comp.id == id) return true; +return false; +} + +function config_dir_by_type(type) { +if (type == '1' || type == 'sub') return '/usr/share/clashoo/config/sub'; +if (type == '2' || type == 'upload') return '/usr/share/clashoo/config/upload'; +if (type == '3' || type == 'custom') return '/usr/share/clashoo/config/custom'; +return ''; +} + +function safe_name(name) { +name = name || ''; +if (!name) return ''; +/* 拒绝前后导空白(不静默 trim,避免与可见名称偏差) */ +if (trim(name) != name) return ''; +/* 路径遍历防护 */ +if (index(name, '/') >= 0 || index(name, '\\') >= 0) return ''; +if (index(name, '..') >= 0) return ''; +if (substr(name, 0, 1) == '.') return ''; +/* 黑名单:# 注释、空格、? * : 等不安全/系统保留字符 */ +if (index(name, '#') >= 0) return ''; +if (index(name, ' ') >= 0) return ''; +if (index(name, '?') >= 0) return ''; +if (index(name, '*') >= 0) return ''; +if (index(name, ':') >= 0) return ''; +/* 拒绝 NUL 与换行/制表等控制字符(逐字节扫描,ucode 正则不支持 \x 转义) */ +let n = length(name); +for (let i = 0; i < n; i++) { +let ch = ord(name, i); +if (ch < 0x20) return ''; +} +/* 允许 Unicode(CJK 等)—— 不再用纯 ASCII 白名单 */ +return name; +} + +function ensure_yaml_name(name) { +name = safe_name(name); +if (!name) return ''; +if (!match(name, /\.(yaml|yml)$/)) +name = name + '.yaml'; +return name; +} + +function file_name_from_url(url) { +url = trim(url || ''); +if (!url) return ''; +url = replace(url, /[?#].*$/, ''); +let name = replace(url, /^.*\//, ''); +return ensure_yaml_name(name); +} + +function template_output_name(sub_name, tpl_name) { +let s = replace((sub_name || ''), /\.(yaml|yml)$/, ''); +let t = replace((tpl_name || ''), /\.(yaml|yml)$/, ''); +s = replace(s, /[^A-Za-z0-9._-]/g, '-'); +t = replace(t, /[^A-Za-z0-9._-]/g, '-'); +if (!s) s = 'sub'; +if (!t) t = 'template'; +return '_merged_' + s + '__' + t + '.yaml'; +} + +function template_search_dirs() { +return [TEMPLATE_DIR, '/usr/share/clashoo/config/template']; +} + +function find_template_path(name) { +name = ensure_yaml_name(name || ''); +if (!name) return ''; +for (let dir in template_search_dirs()) { +let p = dir + '/' + name; +if (access(p, 'r')) return p; +} +return ''; +} + +function trim_quotes(v) { +v = trim(v || ''); +v = replace(v, /^["']/, ''); +v = replace(v, /["']$/, ''); +return v; +} + +function parse_controller_port(value, fallback) { +value = trim(value || ''); +if (!value) return fallback || '9090'; +value = replace(value, /^https?:\/\//, ''); +value = replace(value, /\/.*$/, ''); +let m = match(value, /:([0-9]+)$/); +if (m && m[1]) return m[1]; +if (match(value, /^[0-9]+$/)) return value; +return fallback || '9090'; +} + +function parse_mihomo_api_auth(path) { +let info = { has_port: false, port: '', has_secret: false, secret: '' }; +if (!access(path, 'r')) return info; +let raw = readfile(path) || ''; +if (!raw) return info; + +for (let line in split(raw, '\n')) { + line = trim(line || ''); + if (!line) continue; + + let m_controller = match(line, /^external-controller:\s*(.+)$/); + if (m_controller && m_controller[1]) { + info.has_port = true; + info.port = parse_controller_port(trim_quotes(m_controller[1]), '9090'); + } + + let m_secret = match(line, /^secret:\s*(.*)$/); + if (m_secret) { + info.has_secret = true; + let secret = trim(m_secret[1] || ''); + info.secret = trim_quotes(secret); + } +} + +return info; +} + +function parse_singbox_api_auth(path) { +let info = { has_port: false, port: '', has_secret: false, secret: '' }; +if (!access(path, 'r')) return info; +let raw = readfile(path) || ''; +if (!raw) return info; + +let cfg = null; +try { + cfg = json(raw); +} catch (e) { + return info; +} + +let api = cfg?.experimental?.clash_api; +if (!api || type(api) != 'object') return info; + +let controller = api.external_controller || ''; +if (controller) { + info.has_port = true; + info.port = parse_controller_port(controller, '9090'); +} + +if (type(api.secret) != 'undefined' && api.secret !== null) { + info.has_secret = true; + info.secret = '' + api.secret; +} + +return info; +} + +function get_clash_api_auth() { +let c = cursor(); +c.load('clashoo'); +let auth = { + port: c.get('clashoo', 'config', 'dash_port') || '9090', + pass: c.get('clashoo', 'config', 'dash_pass') || '' +}; + +let family = running_core_family() || get_core_type(); +if (family == 'singbox') { + let sb = parse_singbox_api_auth(SINGBOX_RUNTIME_CONFIG); + if (sb.has_port) auth.port = sb.port || auth.port; + if (sb.has_secret) auth.pass = sb.secret || ''; +} else { + let mm = parse_mihomo_api_auth('/etc/clashoo/config.yaml'); + if (mm.has_port) auth.port = mm.port || auth.port; + if (mm.has_secret) auth.pass = mm.secret || ''; +} + +return auth; +} + +function api_unauthorized_payload(obj) { +if (!obj || type(obj) != 'object') return false; +let msg = '' + (obj.message || obj.error || ''); +return !!match(msg, /[Uu]nauthorized|[Ff]orbidden|401/); +} + +function mihomo_api_post(path) { +let api = get_clash_api_auth(); +let dash_port = api.port; +let dash_pass = api.pass; +let auth = dash_pass ? ('-H ' + shell_quote('Authorization: Bearer ' + dash_pass) + ' ') : ''; +let url = 'http://127.0.0.1:' + dash_port + path; +let cmd = 'curl -s -o /dev/null -w "%{http_code}" -m 10 -X POST ' + auth + shell_quote(url) + ' 2>/dev/null'; +let p = popen(cmd); +if (!p) return '000'; +let code = trim(p.read('all')); +p.close(); +return code || '000'; +} + +function clash_api_get(path, timeout) { +let api = get_clash_api_auth(); +let dash_port = api.port; +let dash_pass = api.pass; +let auth = dash_pass ? ('-H ' + shell_quote('Authorization: Bearer ' + dash_pass) + ' ') : ''; +let t = timeout || 4; +let url = 'http://127.0.0.1:' + dash_port + path; +let cmd = 'curl -s -m ' + t + ' ' + auth + shell_quote(url) + ' 2>/dev/null'; +let p = popen(cmd); +if (!p) return ''; +let raw = trim(p.read('all')); +p.close(); +return raw; +} + +function clash_api_json(path, timeout) { +let raw = clash_api_get(path, timeout); +if (!raw) return null; +try { + let obj = json(raw); + return obj ? obj : null; +} catch (e) { + /* Some endpoints can stream multiple JSON frames; parse first valid line */ + for (let line in split(raw, '\n')) { + line = trim(line || ''); + if (!line) continue; + try { + let obj2 = json(line); + if (obj2) return obj2; + } catch (e2) {} + } + return null; +} +} + +/* 百分号编码:clash API 路径里的组名常含 emoji/中文/空格,curl 不会自动编码 */ +function uri_encode(s) { +s = '' + (s || ''); +let out = ''; +for (let i = 0; i < length(s); i++) { + let ch = substr(s, i, 1); + let o = ord(ch); + if ((o >= 48 && o <= 57) || (o >= 65 && o <= 90) || (o >= 97 && o <= 122) || + o == 45 || o == 46 || o == 95 || o == 126) + out += ch; + else + out += sprintf('%%%02X', o); +} +return out; +} + +/* PUT + JSON body,用于切换 Selector 组(成功返回 204);返回 HTTP 状态码字符串 */ +function clash_api_put_json(path, body) { +let api = get_clash_api_auth(); +let auth = api.pass ? ('-H ' + shell_quote('Authorization: Bearer ' + api.pass) + ' ') : ''; +let url = 'http://127.0.0.1:' + api.port + path; +let payload = sprintf('%J', body); +let cmd = 'curl -s -o /dev/null -w "%{http_code}" -m 8 -X PUT ' + auth + + '-H ' + shell_quote('Content-Type: application/json') + ' ' + + '-d ' + shell_quote(payload) + ' ' + shell_quote(url) + ' 2>/dev/null'; +let p = popen(cmd); +if (!p) return '000'; +let code = trim(p.read('all')); +p.close(); +return code || '000'; +} + +function read_overview_stats_cache() { +let raw = trim(readfile(OVERVIEW_STATS_CACHE_FILE) || ''); +if (!raw) return {}; +try { + let obj = json(raw); + return obj ? obj : {}; +} catch (e) { + return {}; +} +} + +function write_overview_stats_cache(payload) { +if (!payload) return; +writefile(OVERVIEW_STATS_CACHE_FILE, sprintf('%J', payload)); +} + +function parse_sub_userinfo(raw) { +let info = { upload: 0, download: 0, total: 0, expire: 0 }; +if (!raw) return info; +for (let part in split(raw, ';')) { +let kv = split(trim(part), '='); +if (length(kv) == 2) { +let k = trim(kv[0]); +let v = int(trim(kv[1])) || 0; +if (k == 'upload' || k == 'download' || k == 'total' || k == 'expire') +info[k] = v; +} +} +return info; +} + +function read_subscriptions() { +let rows = []; +if (!access(LIST_FILE, 'r')) return rows; +let content = readfile(LIST_FILE) || ''; +for (let line in split(content, '\n')) { +line = trim(line); +if (!line) continue; +let parts = split(line, '#'); +if (length(parts) < 2) continue; +let name = trim(parts[0]); +let url = trim(parts[1]); +let typ = length(parts) > 2 ? trim(parts[2]) : 'clashoo'; +if (!name || !url) continue; +let info_path = SUB_DIR + '/' + name + '.info'; +let raw_info = access(info_path, 'r') ? trim(readfile(info_path) || '') : ''; +let ui = parse_sub_userinfo(raw_info); +push(rows, { name, url, type: typ, +sub_upload: ui.upload, sub_download: ui.download, +sub_total: ui.total, sub_expire: ui.expire }); +} +return rows; +} + +function build_configs_payload() { +let configs = []; +let subs = read_subscriptions(); +let core_type = get_core_type(); +let current = get_current_config(core_type); +let is_singbox = (core_type == 'singbox'); + +function has_cfg(n) { +for (let i = 0; i < length(configs); i++) +if (configs[i] == n) return true; +return false; +} + +if (is_singbox) { +/* sing-box 内核:只列出 .json 配置 */ +for (let f in (glob(SINGBOX_DIR + '/*.json') || [])) { +let n = replace(f, SINGBOX_DIR + '/', ''); +if (n && !has_cfg(n)) push(configs, n); +} +} else { +/* mihomo / smart 内核:只列出 .yaml / .yml 配置 */ +for (let row in subs) { +let n = row.name; +if (!n) continue; +if (!access('/usr/share/clashoo/config/sub/' + n, 'r')) continue; +if (!has_cfg(n)) push(configs, n); +} + +for (let f in (glob('/usr/share/clashoo/config/upload/*.yaml') || [])) { +let n = replace(f, '/usr/share/clashoo/config/upload/', ''); +if (n && !has_cfg(n)) push(configs, n); +} +for (let f in (glob('/usr/share/clashoo/config/upload/*.yml') || [])) { +let n = replace(f, '/usr/share/clashoo/config/upload/', ''); +if (n && !has_cfg(n)) push(configs, n); +} + +for (let f in (glob('/usr/share/clashoo/config/custom/*.yaml') || [])) { +let n = replace(f, '/usr/share/clashoo/config/custom/', ''); +if (n && !has_cfg(n)) push(configs, n); +} +for (let f in (glob('/usr/share/clashoo/config/custom/*.yml') || [])) { +let n = replace(f, '/usr/share/clashoo/config/custom/', ''); +if (n && !has_cfg(n)) push(configs, n); +} +} + +/* 仅当 current 真实存在于任一标准目录时才补到列表,避免删除文件后下拉框残留 */ +if (current && !has_cfg(current)) { +let exists = access('/usr/share/clashoo/config/sub/' + current, 'r') || + access('/usr/share/clashoo/config/upload/' + current, 'r') || + access('/usr/share/clashoo/config/custom/' + current, 'r') || + access(SINGBOX_DIR + '/' + current, 'r'); +if (exists) push(configs, current); +else current = ''; +} + +return { configs: configs, current: current, core_type: core_type }; +} + +function get_access_payload() { +let proxy_port = uci_get('clashoo', 'config', 'mixed_port') || uci_get('clashoo', 'config', 'http_port') || '7890'; +let tcp_mode = uci_get('clashoo', 'config', 'tcp_mode') || 'redirect'; +let udp_mode = uci_get('clashoo', 'config', 'udp_mode') || tcp_mode; +ensure_access_check_daemon(); +let now = now_epoch_sec(); +let cache = read_access_check_cache(); +let stale = access_cache_is_stale(cache, now); +let updating = access_check_is_updating(now); + +if ((stale || !cache) && !updating) { +trigger_access_cache_refresh(); +updating = true; +} + +if (!cache) +return default_access_payload(proxy_port, tcp_mode, udp_mode); + +return ensure_access_payload_shape(cache, proxy_port, tcp_mode, udp_mode, stale, updating); +} + +function read_template_bindings() { +let rows = []; +if (!access(TEMPLATE_BIND_FILE, 'r')) return rows; +let content = readfile(TEMPLATE_BIND_FILE) || ''; +for (let line in split(content, '\n')) { +line = trim(line || ''); +if (!line) continue; +let parts = split(line, '#'); +if (length(parts) < 2) continue; +let sub_name = ensure_yaml_name(parts[0] || ''); +let template_name = ensure_yaml_name(parts[1] || ''); +let enabled = length(parts) > 2 ? trim(parts[2] || '') : '1'; +if (!sub_name || !template_name) continue; +push(rows, { +sub_name, +template_name, +enabled: (enabled == '1' || enabled == 'true') ? '1' : '0' +}); +} +return rows; +} + +function write_template_bindings(rows) { +let lines = []; +for (let row in (rows || [])) { +if (!row || !row.sub_name || !row.template_name) continue; +push(lines, row.sub_name + '#' + row.template_name + '#' + (row.enabled == '1' ? '1' : '0')); +} +system('mkdir -p /usr/share/clashbackup'); +return writefile(TEMPLATE_BIND_FILE, join('\n', lines) + '\n') !== null; +} + +function find_template_binding(sub_name) { +for (let row in read_template_bindings()) { +if (row.sub_name == sub_name) return row; +} +return null; +} + +// Migrate sing-box config from pre-1.12 deprecated formats to 1.12+ format +function migrate_singbox(cfg) { + // --- Fix DNS servers: address-based → type-based, remove detour --- + if (cfg.dns && cfg.dns.servers) { + cfg.dns.servers = map(cfg.dns.servers || [], function(srv) { + let addr = srv.address || ''; + let new_srv = {}; + let skip = { address: 1, detour: 1, address_resolver: 1, address_strategy: 1 }; + for (let k in srv) if (!skip[k]) new_srv[k] = srv[k]; + + if (addr === 'fakeip') { + new_srv.type = 'fakeip'; + } else if (addr === 'local' || addr === '') { + new_srv.type = new_srv.type || 'local'; + } else if (match(addr, /^rcode:\/\//) || addr === 'rcode') { + new_srv._rcode = true; // mark for removal, handled via dns rule action + } else if (match(addr, /^h3:\/\//)) { + let host = replace(replace(addr, /^h3:\/\//, ''), /\/.*$/, ''); + new_srv.type = 'h3'; new_srv.server = host; + } else if (match(addr, /^https:\/\//)) { + let host = replace(replace(addr, /^https:\/\//, ''), /\/.*$/, ''); + new_srv.type = 'https'; new_srv.server = host; + } else if (match(addr, /^tls:\/\//)) { + new_srv.type = 'tls'; new_srv.server = replace(addr, /^tls:\/\//, ''); + } else if (match(addr, /^tcp:\/\//)) { + new_srv.type = 'tcp'; new_srv.server = replace(addr, /^tcp:\/\//, ''); + } else if (addr && !new_srv.type) { + new_srv.type = 'udp'; new_srv.server = addr; // plain IP → UDP + } else if (addr) { + new_srv.address = addr; // unknown format, keep as-is + } + return new_srv; + }); + } + + // --- Remove rcode servers (unsupported in 1.12+ new format), convert referencing rules to reject --- + let rcode_tags = {}; + if (cfg.dns && cfg.dns.servers) { + let good = []; + for (let srv in cfg.dns.servers) { + if (srv._rcode) rcode_tags[srv.tag] = true; + else push(good, srv); + } + cfg.dns.servers = good; + } + if (cfg.dns && cfg.dns.rules && length(keys(rcode_tags)) > 0) { + cfg.dns.rules = map(cfg.dns.rules || [], function(rule) { + if (rule.server && rcode_tags[rule.server]) { + let r = {}; + for (let k in rule) if (k !== 'server' && k !== 'action') r[k] = rule[k]; + r.action = 'reject'; + return r; + } + return rule; + }); + } + + // --- Remove legacy dns.fakeip.enabled (1.12+ fakeip is configured via type:"fakeip" server) --- + if (cfg.dns && cfg.dns.fakeip) { + let fp = cfg.dns.fakeip; + let new_fp = {}; + for (let k in fp) if (k !== 'enabled') new_fp[k] = fp[k]; + if (length(keys(new_fp)) > 0) cfg.dns.fakeip = new_fp; + else delete cfg.dns['fakeip']; + } + + // --- Fix DNS rules: add action:"route" when server is set --- + if (cfg.dns && cfg.dns.rules) { + cfg.dns.rules = map(cfg.dns.rules || [], function(rule) { + if (rule.server && !rule.action) { + let r = {}; + for (let k in rule) r[k] = rule[k]; + r.action = 'route'; + return r; + } + return rule; + }); + } + + // --- Fix legacy special outbounds (block / dns types) --- + let special = {}; + if (cfg.outbounds) { + let kept = []; + for (let ob in cfg.outbounds) { + if (ob.type === 'block' || ob.type === 'dns') { + special[ob.tag] = ob.type; + } else { + push(kept, ob); + } + } + cfg.outbounds = kept; + } + + // Patch route rules that pointed to removed special outbounds + if (cfg.route && cfg.route.rules && length(keys(special)) > 0) { + cfg.route.rules = map(cfg.route.rules || [], function(rule) { + let sp = rule.outbound && special[rule.outbound]; + if (!sp) return rule; + let r = {}; + for (let k in rule) if (k !== 'outbound') r[k] = rule[k]; + r.action = (sp === 'dns') ? 'hijack-dns' : 'reject'; + return r; + }); + } + + // --- Migrate geoip/geosite database refs to rule_set (removed in sing-box 1.12) --- + let needed_rule_sets = {}; + + let migrate_geo_rule = function(rule) { + let has_geo = rule.geoip || rule.geosite; + if (!has_geo) return rule; + let r = {}; + let new_rs = []; + for (let k in rule) { + if (k === 'geoip') { + for (let n in rule[k]) { + let tag = 'geoip-' + n; + needed_rule_sets[tag] = 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-' + n + '.srs'; + push(new_rs, tag); + } + } else if (k === 'geosite') { + for (let n in rule[k]) { + let tag = 'geosite-' + n; + needed_rule_sets[tag] = 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-' + n + '.srs'; + push(new_rs, tag); + } + } else { + r[k] = rule[k]; + } + } + // Merge new rule_set tags with any existing ones + let existing_rs = type(r.rule_set) === 'array' ? r.rule_set : + (r.rule_set ? [r.rule_set] : []); + for (let t in new_rs) push(existing_rs, t); + r.rule_set = existing_rs; + return r; + }; + + if (cfg.route && cfg.route.rules) + cfg.route.rules = map(cfg.route.rules, migrate_geo_rule); + if (cfg.dns && cfg.dns.rules) + cfg.dns.rules = map(cfg.dns.rules, migrate_geo_rule); + + // download_detour must be DIRECT: at first start rule-sets aren't loaded, so + // routing it via a proxy deadlocks (DNS recursion -> sing-box FATAL). + let pick_dl_detour = function() { + if (cfg.outbounds) { + for (let ob in cfg.outbounds) { + if (ob && ob.type === 'direct' && ob.tag) return ob.tag; + } + } + return 'DIRECT'; + }; + + // Add rule_set download entries for each referenced geo tag + if (length(keys(needed_rule_sets)) > 0) { + if (!cfg.route) cfg.route = {}; + if (!cfg.route.rule_set) cfg.route.rule_set = []; + let existing_tags = {}; + for (let rs in cfg.route.rule_set) existing_tags[rs.tag] = true; + let dl_detour = pick_dl_detour(); + for (let tag in keys(needed_rule_sets)) { + if (!existing_tags[tag]) { + push(cfg.route.rule_set, { + tag: tag, + type: 'remote', + format: 'binary', + url: needed_rule_sets[tag], + download_detour: dl_detour + }); + } + } + } + + // --- Remove deprecated 'outbound' DNS rule items (fatal in sing-box 1.13) --- + // Replacement: set route.default_domain_resolver to the plain IP-based resolver + let dns_resolver_tag = ''; + if (cfg.dns && cfg.dns.servers) { + for (let srv in cfg.dns.servers) { + if ((srv.type === 'udp' || srv.type === 'local') && srv.server && + match(srv.server, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { + dns_resolver_tag = srv.tag; + break; + } + } + } + if (cfg.dns && cfg.dns.rules) { + let filtered = []; + for (let rule in cfg.dns.rules) { + if (!rule.outbound) push(filtered, rule); + } + cfg.dns.rules = filtered; + } + // Set route.default_domain_resolver (replaces outbound:any DNS routing) + if (dns_resolver_tag) { + if (!cfg.route) cfg.route = {}; + if (!cfg.route.default_domain_resolver) + cfg.route.default_domain_resolver = dns_resolver_tag; + } + + // --- Fix DNS servers with domain-name server field: add domain_resolver --- + // sing-box 1.12+ requires domain_resolver when server= is a hostname (not IP) + if (cfg.dns && cfg.dns.servers && dns_resolver_tag) { + let domain_types = { https: 1, h3: 1, tls: 1, tcp: 1 }; + cfg.dns.servers = map(cfg.dns.servers, function(srv) { + if (!domain_types[srv.type]) return srv; + if (!srv.server || match(srv.server, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return srv; + if (srv.domain_resolver) return srv; + let r = {}; + for (let k in srv) r[k] = srv[k]; + r.domain_resolver = dns_resolver_tag; + return r; + }); + } + + // --- Fix DNS rules referencing non-existent server tags → action: reject --- + if (cfg.dns && cfg.dns.servers && cfg.dns.rules) { + let valid_servers = {}; + for (let srv in cfg.dns.servers) valid_servers[srv.tag] = true; + cfg.dns.rules = map(cfg.dns.rules, function(rule) { + if (!rule.server || valid_servers[rule.server]) return rule; + let r = {}; + for (let k in rule) if (k !== 'server' && k !== 'action') r[k] = rule[k]; + r.action = 'reject'; + return r; + }); + } + + // --- Fix reject method: sing-box 1.14 rejects "dropped", only "default"/"drop" --- + if (cfg.dns && cfg.dns.rules) { + cfg.dns.rules = map(cfg.dns.rules, function(rule) { + if (rule.action !== 'reject' || !rule.method) return rule; + let m = rule.method; + if (m === 'dropped') m = 'drop'; + if (m !== 'default' && m !== 'drop') { + // unknown value: drop it, sing-box defaults to NXDOMAIN + let r = {}; + for (let k in rule) if (k !== 'method') r[k] = rule[k]; + return r; + } + if (m === rule.method) return rule; + let r = {}; + for (let k in rule) r[k] = rule[k]; + r.method = m; + return r; + }); + } + + // --- Migrate TUN inet4_address/inet6_address -> address array (1.12+), and + // strip legacy inbound fields removed in 1.13 (sniff / sniff_override_destination / + // sniff_timeout / domain_strategy). If any inbound had sniff on, prepend a + // { "action": "sniff" } route rule as the equivalent. --- + let had_sniff = false; + if (cfg.inbounds) { + cfg.inbounds = map(cfg.inbounds, function(ib) { + let r = {}; + let addrs = []; + for (let k in ib) { + if (k === 'inet4_address') { + if (type(ib[k]) === 'array') { for (let a in ib[k]) push(addrs, a); } + else push(addrs, ib[k]); + } else if (k === 'inet6_address') { + if (type(ib[k]) === 'array') { for (let a in ib[k]) push(addrs, a); } + else push(addrs, ib[k]); + } else if (k === 'sniff') { + if (ib[k]) had_sniff = true; + } else if (k === 'sniff_override_destination' || k === 'sniff_timeout' || + k === 'domain_strategy') { + // drop — moved to route.rules actions in 1.13 + } else { + r[k] = ib[k]; + } + } + if (length(addrs) > 0) r.address = addrs; + return r; + }); + } + if (had_sniff) { + if (!cfg.route) cfg.route = {}; + if (!cfg.route.rules) cfg.route.rules = []; + let has_sniff_rule = false; + for (let rl in cfg.route.rules) + if (rl.action === 'sniff') { has_sniff_rule = true; break; } + if (!has_sniff_rule) { + let new_rules = [ { action: 'sniff' } ]; + for (let rl in cfg.route.rules) push(new_rules, rl); + cfg.route.rules = new_rules; + } + } + + // --- sing-box 1.14 requires route.default_domain_resolver; add one if missing, + // preferring dns.final, else the first dns.servers entry with a tag. --- + if (cfg.route && !cfg.route.default_domain_resolver) { + let resolver_tag = ''; + if (cfg.dns && cfg.dns.final) resolver_tag = cfg.dns.final; + else if (cfg.dns && cfg.dns.servers) { + for (let srv in cfg.dns.servers) + if (srv.tag) { resolver_tag = srv.tag; break; } + } + if (resolver_tag) + cfg.route.default_domain_resolver = { server: resolver_tag }; + } + + // --- Remove dangling outbound refs from selectors/urltest, and drop airline + // pseudo-nodes. Two cases: (1) subconverter may reference a missing tag (e.g. + // REJECT); (2) "Traffic:.../Expire:.../quota" pseudo-nodes are real SS/Vmess + // outbounds (kept so the UI can read traffic/expiry) but don't forward — in a + // selector/urltest they win (0ms) and swallow all foreign traffic. --- + let is_pseudo_tag = function(t) { + if (!t) return false; + return match(t, /^Traffic[::]/) || + match(t, /^Expire[::]/) || + match(t, /剩余流量|剩余[::]/) || + match(t, /距离下次重置/) || + match(t, /到期(时间|日期)?[::]/) || + match(t, /官网[::]|网站[::]|套餐[::]?|客服[::]/) || + match(t, /QQ[群]?[::]/) || + match(t, /Telegram|TG群|官方群/) || + match(t, /续费|订阅地址|流量重置/); + }; + if (cfg.outbounds) { + let defined_tags = {}; + for (let ob in cfg.outbounds) defined_tags[ob.tag] = true; + cfg.outbounds = map(cfg.outbounds, function(ob) { + if ((ob.type !== 'selector' && ob.type !== 'urltest' && ob.type !== 'loadbalance') || + !ob.outbounds) return ob; + let valid = []; + for (let t in ob.outbounds) { + if (!defined_tags[t]) continue; + if (is_pseudo_tag(t)) continue; + push(valid, t); + } + if (length(valid) === length(ob.outbounds)) return ob; + let r = {}; + for (let k in ob) r[k] = ob[k]; + r.outbounds = valid; + return r; + }); + } + + // --- Force download_detour=DIRECT on every remote rule_set --- + // subconverter often emits download_detour="auto", but the proxy isn't ready at + // first start -> urltest deadlock ("fetch rule-set: deadline exceeded") -> FATAL. + // The srs URLs already use a CN-reachable mirror, so DIRECT is always correct. + if (cfg.route && cfg.route.rule_set) { + let dl_detour = pick_dl_detour(); + cfg.route.rule_set = map(cfg.route.rule_set, function(rs) { + if (!rs || rs.type !== 'remote') return rs; + let r = {}; + for (let k in rs) r[k] = rs[k]; + r.download_detour = dl_detour; + return r; + }); + } + + return cfg; +} + +function restore_backup_json_locked(raw) { +let rollback_ready = false; +try { +if (!raw) return { success: false, message: '备份内容为空' }; +if (length(raw) > BACKUP_MAX_JSON_SIZE) + return { success: false, message: '旧版 JSON 备份超过 8MB' }; +let obj = null; +try { obj = json(raw); } catch (e) { obj = null; } +if (type(obj) != 'object') + return { success: false, message: '备份文件格式错误,无法解析' }; +let manifest = obj.manifest; +if (type(manifest) != 'object' || manifest.marker != 'clashoo-backup-v1') + return { success: false, message: '这不是有效的 clashoo 备份文件' }; +let files = obj.files; +if (type(files) != 'object') + return { success: false, message: '备份文件缺少配置数据' }; + +let BACKUP_DIR_MAP = { + 'config/sub': '/usr/share/clashoo/config/sub', + 'config/upload': '/usr/share/clashoo/config/upload', + 'config/custom': '/usr/share/clashoo/config/custom', + 'config/singbox': SINGBOX_DIR, + 'templates': TEMPLATE_USER_DIR +}; +let BACKUP_FIXED_MAP = { + 'uci': '/etc/config/clashoo', + 'meta/confit_list.conf': LIST_FILE, + 'meta/template_bindings.conf': TEMPLATE_BIND_FILE +}; + +let plan = []; +let entry_count = 0; +for (let rel in files) { + entry_count++; + if (entry_count > BACKUP_MAX_ENTRIES) + return { success: false, message: '备份文件条目超过 2048 个' }; + let content = files[rel]; + if (type(content) != 'string') + return { success: false, message: '备份文件含非法数据项: ' + rel }; + if (BACKUP_FIXED_MAP[rel]) { + push(plan, { path: BACKUP_FIXED_MAP[rel], content: content }); + continue; + } + let slash = rindex(rel, '/'); + if (slash < 0) + return { success: false, message: '备份文件含未知数据项: ' + rel }; + let prefix = substr(rel, 0, slash); + let fname = substr(rel, slash + 1); + let dir = BACKUP_DIR_MAP[prefix]; + if (!dir) + return { success: false, message: '备份文件含未知数据项: ' + rel }; + if (!fname || safe_name(fname) != fname) + return { success: false, message: '备份文件含非法文件名: ' + rel }; + push(plan, { path: dir + '/' + fname, content: content }); +} + +unlink(BACKUP_JSON_ROLLBACK_FILE); +if (system('sh ' + shell_quote(BACKUP_ARCHIVE_SCRIPT) + ' export ' + + shell_quote(BACKUP_JSON_ROLLBACK_FILE) + ' >/dev/null 2>&1') != 0) { + unlink(BACKUP_JSON_ROLLBACK_FILE); + return { success: false, message: '无法创建还原前安全快照,未修改当前配置' }; +} +rollback_ready = true; + +let apply_error = ''; +for (let prefix in BACKUP_DIR_MAP) { + let d = BACKUP_DIR_MAP[prefix]; + if (system('mkdir -p ' + shell_quote(d)) != 0 || + system('rm -f ' + shell_quote(d) + '/* >/dev/null 2>&1') != 0) { + apply_error = '无法清空配置目录: ' + d; + break; + } +} +let written = 0; +for (let item in plan) { + if (apply_error) break; + let p = item.path; + let ps = rindex(p, '/'); + if (ps > 0 && system('mkdir -p ' + shell_quote(substr(p, 0, ps))) != 0) { + apply_error = '无法创建配置目录: ' + p; + break; + } + if (writefile(p, item.content) === null) { + apply_error = '写入失败: ' + p; + break; + } + written++; +} + +if (apply_error) { + let rollback_rc = system('sh ' + shell_quote(BACKUP_ARCHIVE_SCRIPT) + ' restore-held ' + + shell_quote(BACKUP_JSON_ROLLBACK_FILE) + ' >/dev/null 2>&1'); + unlink(BACKUP_JSON_ROLLBACK_FILE); + rollback_ready = false; + return { success: false, message: apply_error + + (rollback_rc == 0 ? ';已回滚原配置' : ';自动回滚失败,请检查存储空间') }; +} + +unlink(BACKUP_JSON_ROLLBACK_FILE); +rollback_ready = false; +system('sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1'); +return { success: true, restored: written, + message: '已还原 ' + written + ' 个文件,正在重启服务' }; +} catch (e) { +if (rollback_ready) { + system('sh ' + shell_quote(BACKUP_ARCHIVE_SCRIPT) + ' restore-held ' + + shell_quote(BACKUP_JSON_ROLLBACK_FILE) + ' >/dev/null 2>&1'); + unlink(BACKUP_JSON_ROLLBACK_FILE); +} +return { success: false, message: '导入失败: ' + e }; +} +} + +function restore_backup_json(raw) { +if (!backup_tmp_ready()) + return { success: false, message: '备份临时目录不安全或无法创建' }; +if (system('sh ' + shell_quote(BACKUP_ARCHIVE_SCRIPT) + ' lock >/dev/null 2>&1') != 0) + return { success: false, message: '已有备份正在还原,请稍后重试' }; +let result = null; +try { + result = restore_backup_json_locked(raw); +} catch (e) { + result = { success: false, message: '导入失败: ' + e }; +} +system('sh ' + shell_quote(BACKUP_ARCHIVE_SCRIPT) + ' unlock >/dev/null 2>&1'); +return result; +} + +const methods = { +version: { +call: function() { +try { +let binary = find_binary(); +return { +app: read_first_line('/usr/share/clashoo/luci_version'), +core: get_core_version(binary), +binary: binary +}; +} catch (e) { +return { success: false, error: 'version_failed', message: '' + e }; +} +} +}, + +status: { +call: function() { +try { +let c = cursor(); +c.load('clashoo'); +let configured_core = get_core_type(); +let running = is_running(); +let proxy_mode = c.get('clashoo', 'config', 'p_mode') || 'rule'; +let conf_path = c.get('clashoo', 'config', 'use_config') || ''; +let panel_type = c.get('clashoo', 'config', 'dashboard_panel')|| 'zashboard'; +let tcp_mode = c.get('clashoo', 'config', 'tcp_mode') || 'tun'; +let udp_mode = c.get('clashoo', 'config', 'udp_mode') || tcp_mode; +let stack = c.get('clashoo', 'config', 'stack') || 'system'; +let api = get_clash_api_auth(); +let dash_port = api.port || (c.get('clashoo', 'config', 'dash_port') || '9090'); +let dash_pass = api.pass || ''; +let effective_tcp_mode = runtime_state_get('effective_tcp_mode') || tcp_mode; +let effective_udp_mode = runtime_state_get('effective_udp_mode') || udp_mode; +let runtime_degraded = runtime_state_get('degraded') == '1'; +let runtime_degrade_reason = runtime_state_get('degrade_reason') || ''; +let health_status = runtime_state_get('health_status') || (running ? 'unknown' : 'stopped'); +let health_detail = runtime_state_get('health_detail') || ''; +if (!running) { + health_status = 'stopped'; + if (health_detail != 'boot_disabled' && health_detail != 'service_disabled') + health_detail = 'service_stopped'; +} +let has_tun_device = runtime_state_get('has_tun_device'); +let local_ip = get_local_ip(); +let config = get_display_config_name(configured_core); +let slash = rindex(conf_path, '/'); +let conf_name = slash >= 0 ? substr(conf_path, slash + 1) : conf_path; +if (configured_core == 'singbox' && config) + conf_name = config; +let dcore = c.get('clashoo', 'config', 'dcore') || '2'; +return { running, dash_port, dash_pass, proxy_mode, conf_path: conf_name, config, + panel_type, tcp_mode, udp_mode, stack, + effective_tcp_mode, effective_udp_mode, + runtime_degraded, runtime_degrade_reason, + health_status, health_detail, has_tun_device, + core_type: configured_core, dcore, local_ip, + has_smart: !!find_smart_binary() }; +} catch (e) { +return { success: false, error: 'status_failed', message: '' + e }; +} +} +}, + +reload: { +call: function() { +let svc = get_service_name(); +return { success: system('service ' + svc + ' reload >/dev/null 2>&1') == 0 }; +} +}, + +restart: { +call: function() { +return { success: system('sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1') == 0 }; +} +}, + +start: { +call: function() { +let c = cursor(); +c.load('clashoo'); +c.set('clashoo', 'config', 'enable', '1'); +c.commit('clashoo'); +system('/etc/init.d/clashoo enable >/dev/null 2>&1'); +system('sh /usr/share/clashoo/rpc/rpc_async.sh start >/dev/null 2>&1'); +return { success: true }; +} +}, + +stop: { +call: function() { +let c = cursor(); +c.load('clashoo'); +c.set('clashoo', 'config', 'enable', '0'); +c.commit('clashoo'); +system('sh /usr/share/clashoo/rpc/rpc_async.sh stop >/dev/null 2>&1'); +return { success: true }; +} +}, + +commit_config: { +call: function() { +let c = cursor(); +c.load('clashoo'); +sync_legacy_core_fields(c); +c.commit('clashoo'); +let rc = system('uci commit clashoo >/dev/null 2>&1'); +return { success: rc === 0 }; +} +}, + +dns_auto_setup: { +call: function() { +let running = is_running(); +let p = popen('sh /usr/share/clashoo/net/dns_auto_setup.sh --apply 2>/dev/null'); +if (!p) return { success: false, message: 'DNS 自动配置启动失败' }; +let raw = trim(p.read('all') || ''); +p.close(); +let res = null; +try { + res = raw ? json(raw) : null; +} catch (e) {} +if (!res || !res.success) +return res || { success: false, message: 'DNS 自动配置失败' }; +res.restarted = false; +if (running) { + system('sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1'); + res.restarted = true; +} +return res; +} +}, + +set_mode: { +args: { mode: 'mode' }, +call: function(req) { + let mode = req.args?.mode || 'fake-ip'; + if (mode != 'fake-ip' && mode != 'tun-system' && mode != 'tun-gvisor' && mode != 'tun-mixed') + return { error: 'invalid mode' }; + let c = cursor(); c.load('clashoo'); + if (mode == 'tun-system' || mode == 'tun-gvisor' || mode == 'tun-mixed') { + c.set('clashoo', 'config', 'tun_mode', '1'); + c.set('clashoo', 'config', 'tcp_mode', 'tun'); + c.set('clashoo', 'config', 'udp_mode', 'tun'); + c.set('clashoo', 'config', 'stack', mode == 'tun-mixed' ? 'mixed' : (mode == 'tun-gvisor' ? 'gvisor' : 'system')); + c.set('clashoo', 'config', 'enable_udp', '1'); + c.set('clashoo', 'config', 'enhanced_mode', 'fake-ip'); +} else { + c.set('clashoo', 'config', 'tun_mode', '0'); + c.set('clashoo', 'config', 'tcp_mode', 'redirect'); + c.set('clashoo', 'config', 'udp_mode', 'tproxy'); + c.set('clashoo', 'config', 'enhanced_mode', 'fake-ip'); +} +c.commit('clashoo'); +system('uci commit clashoo >/dev/null 2>&1'); +if (is_running()) system('sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1'); +return { success: true }; +} +}, + +set_proxy_mode: { +args: { mode: 'mode' }, +call: function(req) { +let mode = req.args?.mode || 'rule'; +if (mode != 'rule' && mode != 'global' && mode != 'direct') +return { error: 'invalid mode' }; +let c = cursor(); c.load('clashoo'); +c.set('clashoo', 'config', 'p_mode', mode); +c.commit('clashoo'); +if (is_running()) system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); +return { success: true }; +} +}, + +set_core: { +args: { core: 'core', dcore: 'dcore', action: 'action' }, +call: function(req) { +let core = req.args?.core || 'mihomo'; +if (core == 'sing-box') core = 'singbox'; +let dcore = req.args?.dcore || ''; +let action = req.args?.action || 'stop'; +if (action != 'stop' && action != 'save' && action != 'apply') +return { success: false, error: 'invalid_action', message: '无效的内核切换动作' }; +let target_error = core_target_error(core, dcore); +if (target_error) +return { success: false, error: target_error.error, message: target_error.message }; + +let running_before = is_running(); + +let c = cursor(); c.load('clashoo'); +c.set('clashoo', 'config', 'core_type', core); +c.set('clashoo', 'config', 'dcore', dcore); +/* Smart 策略仅 Smart 内核(dcore=1)有意义。切到 Smart 时自动开启 + * smart_auto_switch;切回普通 mihomo / sing-box 时自动关闭,避免 + * Smart 模型字段误注入到非 Smart 配置。 */ +c.set('clashoo', 'config', 'smart_auto_switch', dcore === '1' ? '1' : '0'); +sync_legacy_core_fields(c); +c.commit('clashoo'); +system('uci commit clashoo >/dev/null 2>&1'); + +// Overview 旧调用默认 stop;System 页可显式 save 或 apply。 +// 所有耗时 init 操作仍由 rpc_async.sh + flock 串行化。 +let restarting = false; +if (action == 'save') { + // Persist only. The selected core takes effect on the next start/restart. +} +else if (running_before && action == 'stop') + system('sh /usr/share/clashoo/rpc/rpc_async.sh stop >/dev/null 2>&1'); +else if (running_before && action == 'apply') { + system('sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1'); + restarting = true; +} + +return { success: true, core_type: core, dcore, + running_before, restarting, stopped: running_before && action == 'stop' }; +} +}, + +set_panel: { +args: { name: 'name' }, +call: function(req) { +let name = req.args?.name || 'zashboard'; +if (name != 'metacubexd' && name != 'yacd' && name != 'zashboard' && name != 'razord') +name = 'zashboard'; +let c = cursor(); c.load('clashoo'); +c.set('clashoo', 'config', 'dashboard_panel', name); +c.commit('clashoo'); +system('uci commit clashoo >/dev/null 2>&1'); +let activated = activate_dashboard_panel(name); +return { success: true, activated }; +} +}, + +update_panel: { +args: { name: 'name' }, +call: function(req) { +if (access('/var/run/panel_downloading', 'r')) +return { success: false, busy: true, message: '面板正在下载' }; +let name = req.args?.name || 'zashboard'; +if (name != 'metacubexd' && name != 'yacd' && name != 'zashboard' && name != 'razord') +name = 'zashboard'; +let c = cursor(); c.load('clashoo'); +c.set('clashoo', 'config', 'dashboard_panel', name); +c.commit('clashoo'); +system('uci commit clashoo >/dev/null 2>&1'); +let pretty = name; +if (name == 'metacubexd') pretty = 'MetaCubeXD'; +else if (name == 'yacd') pretty = 'YACD'; +else if (name == 'zashboard') pretty = 'Zashboard'; +else if (name == 'razord') pretty = 'Razord'; +system("printf '%s - 面板更新任务已提交: " + shell_quote(pretty) + "\\n' \"$(date '+%Y-%m-%d %H:%M:%S')\" >>/tmp/clash_update.txt"); +// seed an immediate state so the UI poller never reads a stale prior result +system("touch /var/run/panel_downloading; printf 'downloading:" + shell_quote(name) + ":开始下载' >/tmp/clash_panel_download_state"); +system("nohup sh /usr/share/clashoo/update/panel_download.sh " + shell_quote(name) + " >>/tmp/clash_update.txt 2>&1 = 0) { +state = substr(line, 0, i1); +let rest = substr(line, i1 + 1); +let i2 = index(rest, ':'); +if (i2 >= 0) { panel = substr(rest, 0, i2); msg = substr(rest, i2 + 1); } +else panel = rest; +} +return { downloading: downloading, state: state, panel: panel, msg: msg }; +} +}, + +list_configs: { +call: function() { +return build_configs_payload(); +} +}, + +set_config: { +args: { name: 'name' }, +call: function(req) { +let name = safe_name(req.args?.name || ''); +if (!name) return { error: 'missing name' }; + +if (match(name, /\.json$/)) { +let profile = SINGBOX_DIR + '/' + name; +if (!access(profile, 'r')) return { error: 'not found' }; + +let c = cursor(); +c.load('clashoo'); +c.set('clashoo', 'config', 'singbox_active', name); +c.commit('clashoo'); + +if (is_running() && (running_core_family() == 'singbox' || get_core_type() == 'singbox')) +system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); + +return { success: true, family: 'singbox', current: name }; +} + +let found_path = ''; +let found_type = ''; +for (let dir in CONFIG_DIRS) { +let candidate = dir.path + '/' + name; +if (access(candidate, 'r')) { found_path = candidate; found_type = dir.type; break; } +} +if (!found_path) return { error: 'not found' }; +let c = cursor(); c.load('clashoo'); +c.set('clashoo', 'config', 'use_config', found_path); +c.set('clashoo', 'config', 'config_type', found_type); +c.commit('clashoo'); +if (is_running()) system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); +return { success: true, family: 'mihomo', current: name }; +} +}, + +get_cpu_arch: { +call: function() { +return { arch: get_cpu_arch() }; +} +}, + +download_core: { +call: function(req) { +if (access('/var/run/core_update', 'r')) +return { success: true, started: false, running: true }; +let dc = req.args?.dcore || ''; +let arch = req.args?.arch || ''; +if (dc && dc != '0' && match(dc, /^[1-5]$/)) { + let c = cursor(); c.load('clashoo'); + c.set('clashoo', 'config', 'dcore', dc); + if (arch) c.set('clashoo', 'config', 'download_core', arch); + c.commit('clashoo'); + system('uci commit clashoo >/dev/null 2>&1'); +} +system("printf '%s - 内核下载任务已触发\\n' \"\$(date '+%Y-%m-%d %H:%M:%S')\" >>/tmp/clash_update.txt"); +system("nohup sh -c 'touch /var/run/core_update; sh /usr/share/clashoo/update/core_download.sh; rc=$?; rm -f /var/run/core_update; exit $rc' >/dev/null 2>&1 /dev/null 2>&1') == 0 }; +} +}, + +get_geoip_version: { +call: function() { +let f = '/etc/clashoo/Country.mmdb'; +let s = stat(f); +if (!s) return { version: '' }; +let p = popen('date -d @' + s.mtime + ' "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r ' + shell_quote(f) + ' "+%Y-%m-%d %H:%M:%S" 2>/dev/null'); +let ts = ''; +if (p) { ts = trim(p.read('all')); p.close(); } +return { version: ts || ('' + s.mtime) }; +} +}, + +get_log_status: { +call: function() { +let core_updating = !!access('/var/run/core_update', 'r'); +let geoip_updating = !!access('/var/run/geoip_update', 'r'); +let p = popen("tail -8 /tmp/clash_update.txt 2>/dev/null"); +let core_log = ''; if (p) { core_log = trim(p.read('all')); p.close(); } +let p2 = popen("tail -5 /tmp/geoip_update.txt 2>/dev/null"); +let geoip_log = ''; if (p2) { geoip_log = trim(p2.read('all')); p2.close(); } +return { core_updating, geoip_updating, core_log, geoip_log }; +} +}, + +component_status: { +call: function() { +let running = !!access(COMPONENT_UPDATE_RUN_FILE, 'r'); +let state = read_component_state(); +let current = ''; +if (access(COMPONENT_UPDATE_RUN_FILE, 'r')) +current = trim(readfile(COMPONENT_UPDATE_RUN_FILE) || ''); +if (!current) current = state.component || ''; + +let comps = []; +for (let def in component_defs()) { +let status = 'idle'; +let message = ''; +if (running && current == def.id) { +status = 'running'; +message = state.message || component_last_log(); +} else if (!running && state.component == def.id && state.status) { +status = state.status; +message = state.message || ''; +} +push(comps, { +id: def.id, +name: def.name, +description: def.description, +kind: def.kind || '', +variant: !!def.variant, +installed_version: component_installed_label(def.id), +installed_versions: def.variant ? component_installed_versions(def.id) : {}, +status, +message +}); +} +return { +running, +current, +components: comps, + arch: { + system: trim(shell_read('uname -m 2>/dev/null') || ''), + // 老 arch 值归一化后再给前端 + download_core: uci_get('clashoo', 'config', 'download_core') + ? normalize_mihomo_arch(uci_get('clashoo', 'config', 'download_core')) + : '' + }, +last_log: component_last_log(), +log: component_log_tail(40) +}; +} +}, + +component_update: { +args: { component: 'component', variant: 'variant' }, +call: function(req) { +let component = req.args?.component || ''; +let variant = req.args?.variant || ''; +if (variant != 'stable' && variant != 'alpha') variant = ''; +if (!valid_component_id(component)) +return { success: false, error: 'invalid_component', message: '未知组件' }; +if (!access(COMPONENT_UPDATE_SCRIPT, 'x')) +return { success: false, error: 'missing_script', message: '组件更新脚本不存在' }; +if (access(COMPONENT_UPDATE_RUN_FILE, 'r')) +return { success: true, started: false, running: true, message: '已有组件更新任务运行中' }; + +if (writefile(COMPONENT_UPDATE_RUN_FILE, component + '\n') === null) +return { success: false, error: 'lock_failed', message: '无法创建组件更新锁' }; +system("printf '%s - 组件更新任务已提交: " + shell_quote(component) + "\\n' \"$(date '+%Y-%m-%d %H:%M:%S')\" >>" + shell_quote(COMPONENT_UPDATE_LOG_FILE)); +let cmd = 'cp ' + shell_quote(COMPONENT_UPDATE_SCRIPT) + ' ' + shell_quote(COMPONENT_UPDATE_RUNNER) + + ' && chmod +x ' + shell_quote(COMPONENT_UPDATE_RUNNER) + + ' && nohup sh ' + shell_quote(COMPONENT_UPDATE_RUNNER) + ' ' + shell_quote(component) + + (variant ? ' ' + shell_quote(variant) : '') + ' >/dev/null 2>&1 /dev/null"); +let content = ''; +if (p) { content = p.read('all'); p.close(); } +return { content }; +} +}, + +clear_update_log: { +call: function() { +return { success: writefile('/tmp/clash_update.txt', '') !== null }; +} +}, + +read_geoip_log: { +call: function() { +let p = popen("tail -300 /tmp/geoip_update.txt 2>/dev/null"); +let content = ''; +if (p) { content = p.read('all'); p.close(); } +return { content }; +} +}, + +clear_geoip_log: { +call: function() { +return { success: writefile('/tmp/geoip_update.txt', '') !== null }; +} +}, + +read_core_log: { +call: function() { +if (access(CORE_LOG_PATH, 'r')) { + let p0 = popen("tail -400 '" + CORE_LOG_PATH + "' 2>/dev/null"); + let c0 = ''; + if (p0) { c0 = p0.read('all'); p0.close(); } + return { content: format_core_log(c0) }; +} +let family = running_core_family() || get_core_type(); +let tag = (family === 'singbox') ? 'sing-box' : 'mihomo'; +let p = popen("logread -e '" + tag + "' 2>/dev/null | tail -400"); +let content = ''; +if (p) { content = p.read('all'); p.close(); } +if (!content) { +let p2 = popen("logread 2>/dev/null | grep -E '(sing-box|mihomo|clashoo)' | tail -400"); +if (p2) { content = p2.read('all'); p2.close(); } +} +return { content: format_core_log(content) }; +} +}, + +clear_core_log: { +call: function() { +let dir = '/var/log/clashoo'; +system('mkdir -p ' + shell_quote(dir) + ' >/dev/null 2>&1'); +return { success: writefile(CORE_LOG_PATH, '') !== null }; +} +}, + +read_update_merged_log: { +call: function() { +let p1 = popen("tail -200 /tmp/clash_update.txt 2>/dev/null"); +let c1 = ''; if (p1) { c1 = trim(p1.read('all')); p1.close(); } +let p2 = popen("tail -200 /tmp/geoip_update.txt 2>/dev/null"); +let c2 = ''; if (p2) { c2 = trim(p2.read('all')); p2.close(); } +let parts = []; +if (c1) push(parts, '=== 内核/面板/订阅更新 ===\n' + format_update_log(c1)); +if (c2) push(parts, '=== GeoIP 更新 ===\n' + format_update_log(c2)); +return { content: join('\n\n', parts) }; +} +}, + +clear_update_merged_log: { +call: function() { +writefile('/tmp/clash_update.txt', ''); +writefile('/tmp/geoip_update.txt', ''); +return { success: true }; +} +}, + +access_check: { +call: function() { +return get_access_payload(); +} +}, + +access_check_refresh: { +call: function() { +ensure_access_check_daemon(); +let updating = access_check_is_updating(now_epoch_sec()); +if (!updating) +trigger_access_cache_refresh(); +return { success: true, started: !updating }; +} +}, + +overview_stats: { +call: function() { +try { + let running = is_running(); + if (!running) + return { ok: false, running: false }; + + /* Fast path: /connections already contains totals + memory on current kernels */ + let conns0 = clash_api_json('/connections', 2); + if (api_unauthorized_payload(conns0)) conns0 = null; + + let conns = conns0 || {}; + let up_total = int(conns.uploadTotal || conns.upload_total || 0); + let down_total = int(conns.downloadTotal || conns.download_total || 0); + let up = int(conns.up || conns.upload || conns.upSpeed || conns.uploadSpeed || 0); + let down = int(conns.down || conns.download || conns.downSpeed || conns.downloadSpeed || 0); + let mem_inuse = int(conns.memory || conns.memory_inuse || conns.inuse || 0); + + let conn_count = 0; + if (conns && conns.connections) { + let n = int(conns.connections || 0); + if (n > 0) + conn_count = n; + else + for (let _ in conns.connections) + conn_count++; + } + if (conn_count <= 0) + conn_count = int(conns.connection || conns.connections_count || 0); + + /* Fallback: derive instantaneous rates from total deltas */ + if ((up <= 0 && down <= 0) && (up_total > 0 || down_total > 0)) { + let now = now_epoch_sec(); + let prev = read_overview_stats_cache(); + let prev_ts = int(prev.ts || 0); + let dt = now - prev_ts; + if (dt > 0 && dt < 120) { + let prev_up_total = int(prev.up_total || 0); + let prev_down_total = int(prev.down_total || 0); + if (up_total >= prev_up_total) + up = int((up_total - prev_up_total) / dt); + if (down_total >= prev_down_total) + down = int((down_total - prev_down_total) / dt); + } + write_overview_stats_cache({ + ts: now, + up_total: up_total, + down_total: down_total + }); + } + + let ok = !!(conns0 || up_total > 0 || down_total > 0 || conn_count > 0 || mem_inuse > 0); + return { + ok, + running: true, + up, + down, + up_total, + down_total, + connections: conn_count, + memory_inuse: mem_inuse + }; +} catch (e) { + return { + ok: false, + running: is_running(), + error: 'overview_stats_failed', + message: '' + e + }; +} +} +}, + +/* Aggregate poll endpoint: returns status + realtime stats in ONE rpcd invocation. + Critical: rpcd cold-starts ucode per call (~4s for this 2500-line script), + so combining avoids a 3x penalty when the page polls. */ +overview: { +call: function() { +try { + let c = cursor(); + c.load('clashoo'); + let configured_core = get_core_type(); + let running = is_running(); + let proxy_mode = c.get('clashoo', 'config', 'p_mode') || 'rule'; + let conf_path = c.get('clashoo', 'config', 'use_config') || ''; +let panel_type = c.get('clashoo', 'config', 'dashboard_panel')|| 'zashboard'; +let tcp_mode = c.get('clashoo', 'config', 'tcp_mode') || 'tun'; +let udp_mode = c.get('clashoo', 'config', 'udp_mode') || tcp_mode; +let stack = c.get('clashoo', 'config', 'stack') || 'system'; +let api = get_clash_api_auth(); + let dash_port = api.port || (c.get('clashoo', 'config', 'dash_port') || '9090'); + let dash_pass = api.pass || ''; + let effective_tcp_mode = runtime_state_get('effective_tcp_mode') || tcp_mode; + let effective_udp_mode = runtime_state_get('effective_udp_mode') || udp_mode; + let runtime_degraded = runtime_state_get('degraded') == '1'; + let runtime_degrade_reason = runtime_state_get('degrade_reason') || ''; + let health_status = runtime_state_get('health_status') || (running ? 'unknown' : 'stopped'); + let health_detail = runtime_state_get('health_detail') || ''; + if (!running) { + health_status = 'stopped'; + if (health_detail != 'boot_disabled' && health_detail != 'service_disabled') + health_detail = 'service_stopped'; + } + let has_tun_device = runtime_state_get('has_tun_device'); + let local_ip = get_local_ip(); + let config = get_display_config_name(configured_core); + let slash = rindex(conf_path, '/'); + let conf_name = slash >= 0 ? substr(conf_path, slash + 1) : conf_path; + if (configured_core == 'singbox' && config) conf_name = config; + let dcore = c.get('clashoo', 'config', 'dcore') || '2'; + + let status_payload = { running, dash_port, dash_pass, proxy_mode, conf_path: conf_name, config, + panel_type, tcp_mode, udp_mode, stack, + effective_tcp_mode, effective_udp_mode, + runtime_degraded, runtime_degrade_reason, + health_status, health_detail, has_tun_device, + core_type: configured_core, dcore, local_ip, + has_mihomo_stable: has_mihomo_stable(), + has_mihomo_alpha: !!access('/usr/bin/mihomo', 'x'), + has_singbox_stable: !!access(SINGBOX_BINARY_PATHS[0], 'x'), + has_singbox_alpha: !!access(SINGBOX_BINARY_PATHS[1], 'x'), + has_smart: !!find_smart_binary() }; + + let stats_payload = { ok: false, running: running }; + if (running) { + let conns0 = clash_api_json('/connections', 1); + if (api_unauthorized_payload(conns0)) conns0 = null; + let conns = conns0 || {}; + let up_total = int(conns.uploadTotal || conns.upload_total || 0); + let down_total = int(conns.downloadTotal || conns.download_total || 0); + let up = int(conns.up || conns.upload || conns.upSpeed || conns.uploadSpeed || 0); + let down = int(conns.down || conns.download || conns.downSpeed || conns.downloadSpeed || 0); + let mem_inuse = int(conns.memory || conns.memory_inuse || conns.inuse || 0); + let conn_count = 0; + if (conns && conns.connections) { + let n = int(conns.connections || 0); + if (n > 0) conn_count = n; + else for (let _ in conns.connections) conn_count++; + } + if (conn_count <= 0) conn_count = int(conns.connection || conns.connections_count || 0); + if ((up <= 0 && down <= 0) && (up_total > 0 || down_total > 0)) { + let now = now_epoch_sec(); + let prev = read_overview_stats_cache(); + let prev_ts = int(prev.ts || 0); + let dt = now - prev_ts; + if (dt > 0 && dt < 120) { + let prev_up_total = int(prev.up_total || 0); + let prev_down_total = int(prev.down_total || 0); + if (up_total >= prev_up_total) up = int((up_total - prev_up_total) / dt); + if (down_total >= prev_down_total) down = int((down_total - prev_down_total) / dt); + } + write_overview_stats_cache({ ts: now, up_total: up_total, down_total: down_total }); + } + stats_payload = { + ok: !!(conns0 || up_total > 0 || down_total > 0 || conn_count > 0 || mem_inuse > 0), + running: true, + up, down, up_total, down_total, + connections: conn_count, + memory_inuse: mem_inuse + }; + } + + let configs_payload = build_configs_payload(); + let access_payload = get_access_payload(); + + return { + status: status_payload, + stats: stats_payload, + configs: configs_payload, + access: access_payload + }; +} catch (e) { + return { + status: { running: false }, + stats: { ok: false, running: false }, + configs: { configs: [], current: '', core_type: get_core_type() }, + access: default_access_payload('7890', 'redirect', 'redirect'), + error: 'overview_failed', + message: '' + e + }; +} +} +}, + +capabilities: { +call: function() { +return { +backend: detect_firewall_backend(), +missing_fw4_tools: detect_missing_fw4_tools(), +has_singbox: !!find_binary_for_family('singbox'), +has_mihomo: !!access('/usr/bin/mihomo', 'x'), +has_mihomo_stable: has_mihomo_stable(), +has_smart: !!find_smart_binary(), +has_clash_meta: !!access('/usr/bin/clash-meta', 'x'), +has_builtin_core: !!access('/etc/clashoo/clash', 'x') +}; +} +}, + +list_subscriptions: { +call: function() { +let active = uci_get('clashoo', 'config', 'use_config'); +let active_name = replace(active, /^.*\//, ''); +let rows = read_subscriptions(); +for (let row in rows) { +let fi = stat(SUB_DIR + '/' + row.name); +row.mtime = fi ? ('' + fi.mtime) : null; +row.size = fi ? (int(fi.size / 1024) + ' KB') : null; +row.has_file = !!fi; +row.active = (row.name == active_name); +} +return { subs: rows, active: active_name }; +} +}, + +list_templates: { +call: function() { +let files = []; +let seen = {}; +for (let dir in template_search_dirs()) { +for (let f in (glob(dir + '/*.yaml') || [])) { +let name = replace(f, dir + '/', ''); +if (index(name, '_merged_') == 0 || seen[name]) continue; +seen[name] = 1; +let fi = stat(f); +push(files, { name, mtime: fi ? ('' + fi.mtime) : null, size: fi ? (int(fi.size / 1024) + ' KB') : null }); +} +for (let f in (glob(dir + '/*.yml') || [])) { +let name = replace(f, dir + '/', ''); +if (index(name, '_merged_') == 0 || seen[name]) continue; +seen[name] = 1; +let fi = stat(f); +push(files, { name, mtime: fi ? ('' + fi.mtime) : null, size: fi ? (int(fi.size / 1024) + ' KB') : null }); +} +} +return { files }; +} +}, + +list_template_bindings: { +call: function() { +return { bindings: read_template_bindings() }; +} +}, + +set_template_binding: { +args: { sub_name: 'sub_name', template_name: 'template_name', enabled: 'enabled' }, +call: function(req) { +let sub_name = ensure_yaml_name(req.args?.sub_name || ''); +let template_name = ensure_yaml_name(req.args?.template_name || ''); +let enabled = (req.args?.enabled || '') == '0' ? '0' : '1'; + +if (!sub_name) +return { success: false, error: 'invalid_sub_name', message: '订阅名称无效' }; + +if (!template_name) { +let keep = []; +for (let row in read_template_bindings()) { +if (row.sub_name != sub_name) +push(keep, row); +} +if (!write_template_bindings(keep)) +return { success: false, error: 'write_failed', message: '保存绑定失败' }; +return { success: true, message: '已移除模板绑定' }; +} + +if (!access(SUB_DIR + '/' + sub_name, 'r')) +return { success: false, error: 'sub_not_found', message: '订阅文件不存在' }; +if (!find_template_path(template_name)) +return { success: false, error: 'template_not_found', message: '模板文件不存在' }; + +let rows = read_template_bindings(); +let updated = false; +for (let i = 0; i < length(rows); i++) { +if (rows[i].sub_name != sub_name) continue; +rows[i].template_name = template_name; +rows[i].enabled = enabled; +updated = true; +break; +} +if (!updated) +push(rows, { sub_name, template_name, enabled }); + +if (!write_template_bindings(rows)) +return { success: false, error: 'write_failed', message: '保存绑定失败' }; + +return { success: true, message: '模板绑定已保存' }; +} +}, + +apply_template_for_sub: { +args: { sub_name: 'sub_name', template_name: 'template_name', set_active: 'set_active' }, +call: function(req) { +let sub_name = ensure_yaml_name(req.args?.sub_name || ''); +let template_name = ensure_yaml_name(req.args?.template_name || ''); +let set_active = (req.args?.set_active || '') == '1'; + +if (!sub_name) +return { success: false, error: 'invalid_sub_name', message: '订阅名称无效' }; + +let sub_path = SUB_DIR + '/' + sub_name; +if (!access(sub_path, 'r')) +return { success: false, error: 'sub_not_found', message: '订阅文件不存在' }; + +if (!template_name) { +let b = find_template_binding(sub_name); +if (!b || b.enabled != '1') +return { success: false, error: 'binding_not_found', message: '未找到启用的模板绑定' }; +template_name = b.template_name; +} + +let template_path = find_template_path(template_name); +if (!template_path) +return { success: false, error: 'template_not_found', message: '模板文件不存在' }; + +let output_name = template_output_name(sub_name, template_name); +let out_path = TEMPLATE_DIR + '/' + output_name; +let cmd = 'sh /usr/share/clashoo/update/template_merge.sh ' + + shell_quote(sub_path) + ' ' + + shell_quote(template_path) + ' ' + + shell_quote(out_path); + +if (system(cmd + ' >/dev/null 2>&1') != 0) +return { success: false, error: 'merge_failed', message: '模板复写生成失败,请检查模板内容' }; + +if (set_active) { +let c = cursor(); +c.load('clashoo'); +c.set('clashoo', 'config', 'use_config', out_path); +c.set('clashoo', 'config', 'config_type', '3'); +c.commit('clashoo'); +if (is_running()) +system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); +} + +return { +success: true, +output_name, +output_path: out_path, +template_name, +message: '模板复写生成成功: ' + output_name +}; +} +}, + +apply_enabled_template_bindings: { +args: { set_active: 'set_active' }, +call: function(req) { +let set_active = (req.args?.set_active || '') == '1'; +let rows = read_template_bindings(); +let done = []; +let failed = []; + +for (let row in rows) { +if (row.enabled != '1') continue; +let sub_path = SUB_DIR + '/' + row.sub_name; +let template_path = find_template_path(row.template_name); +if (!access(sub_path, 'r') || !template_path) { +push(failed, row.sub_name); +continue; +} + +let output_name = template_output_name(row.sub_name, row.template_name); +let out_path = TEMPLATE_DIR + '/' + output_name; +let cmd = 'sh /usr/share/clashoo/update/template_merge.sh ' + + shell_quote(sub_path) + ' ' + + shell_quote(template_path) + ' ' + + shell_quote(out_path) + + ' >/dev/null 2>&1'; + +if (system(cmd) == 0) { +push(done, row.sub_name); +if (set_active) { +let current = uci_get('clashoo', 'config', 'use_config'); +if (replace(current || '', /^.*\//, '') == row.sub_name) { +let c = cursor(); +c.load('clashoo'); +c.set('clashoo', 'config', 'use_config', out_path); +c.set('clashoo', 'config', 'config_type', '3'); +c.commit('clashoo'); +} +} +} +else { +push(failed, row.sub_name); +} +} + +if (set_active && is_running()) +system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); + +return { +success: length(failed) == 0, +done, +failed, +message: '模板生成完成: 成功 ' + length(done) + ',失败 ' + length(failed) +}; +} +}, + +list_dir_files: { +args: { type: 'type' }, +call: function(req) { +let type_map = { '1': '/usr/share/clashoo/config/sub', '2': '/usr/share/clashoo/config/upload', '3': '/usr/share/clashoo/config/custom' }; +let dir = type_map[req.args?.type || '2']; +if (!dir) return { files: [] }; +let active = uci_get('clashoo', 'config', 'use_config'); +let active_name = replace(active, /^.*\//, ''); +let files = []; +for (let f in (glob(dir + '/*.yaml') || [])) { +let name = replace(f, dir + '/', ''); +let fi = stat(f); +push(files, { +name, +mtime: fi ? ('' + fi.mtime) : null, +size: fi ? (int(fi.size / 1024) + ' KB') : null, +active: name == active_name +}); +} +for (let f in (glob(dir + '/*.yml') || [])) { +let name = replace(f, dir + '/', ''); +let fi = stat(f); +push(files, { +name, +mtime: fi ? ('' + fi.mtime) : null, +size: fi ? (int(fi.size / 1024) + ' KB') : null, +active: name == active_name +}); +} +return { files, active: active_name }; +} +}, + +delete_config: { +args: { name: 'name', type: 'type' }, +call: function(req) { +let name = safe_name(req.args?.name || ''); +let type = req.args?.type || '1'; +if (!name) return { error: 'missing name' }; +let type_map = { '1': SUB_DIR, '2': '/usr/share/clashoo/config/upload', '3': '/usr/share/clashoo/config/custom' }; +let dir = type_map[type] || SUB_DIR; +let target = dir + '/' + name; +let file_exists = !!access(target, 'r'); +let alt_name = name; +let alt_target = target; +let mirrored_template = null; + +if (type == '1' && match(name, /_new\.(yaml|yml)$/)) { +alt_name = replace(name, /_new\.(yaml|yml)$/, '.yaml'); +alt_target = dir + '/' + alt_name; +} + +if (type == '3' && file_exists) { +let user_template = TEMPLATE_USER_DIR + '/' + name; +if (access(user_template, 'r') && readfile(user_template) == readfile(target)) +mirrored_template = user_template; +} + +if (file_exists) { +if (unlink(target) === null) return { success: false, error: 'delete failed' }; +if (type == '1') { +unlink(target + '.info'); +unlink(target + '.url'); +unlink(target + '.ua'); +} +} +else if (type != '1') { +return { error: 'not found' }; +} + +if (mirrored_template) +unlink(mirrored_template); + +if (type == '1' && alt_target != target && access(alt_target, 'r')) { +unlink(alt_target); +unlink(alt_target + '.info'); +unlink(alt_target + '.url'); +unlink(alt_target + '.ua'); +} + +let c = cursor(); +c.load('clashoo'); +let active = c.get('clashoo', 'config', 'use_config') || ''; +if (active == target || (type == '1' && active == alt_target)) { +let fallback = first_available_config(target); +if (fallback) { +c.set('clashoo', 'config', 'use_config', fallback.path); +c.set('clashoo', 'config', 'config_type', fallback.type); +} +else { +c.delete('clashoo', 'config', 'use_config'); +c.delete('clashoo', 'config', 'config_type'); +} +c.commit('clashoo'); +if (is_running()) system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); +} + +if (type == '1' && access(LIST_FILE, 'r')) { +let content = readfile(LIST_FILE) || ''; +let lines = []; +for (let l in split(content, '\n')) { +if (index(l, name + '#') == 0) +continue; +if (alt_name != name && index(l, alt_name + '#') == 0) +continue; +push(lines, l); +} +writefile(LIST_FILE, join('\n', lines)); +} +return { success: true }; +} +}, + +update_china_ip: { +call: function() { +/* Surface CN whitelist update progress in update log panel. */ + return { success: system('sh /usr/share/clashoo/rpc/rpc_async.sh update_china_ip >/dev/null 2>&1') == 0 }; +} +}, + +download_subs: { +call: function() { +// 已有下载任务在跑则不重复触发 +let pb = popen('ps w 2>/dev/null | grep "[c]lashoo/runtime/clashoo.sh" | head -1'); +let busy = ''; if (pb) { busy = trim(pb.read('all')); pb.close(); } +if (busy != '') return { success: true, started: false, running: true }; +// 清空进度日志后后台执行,前端轮询 download_subs_status 取实时进度 +system("printf '' >/tmp/clash_update.txt 2>/dev/null"); +system('sh /usr/share/clashoo/runtime/clashoo.sh >/dev/null 2>&1 &'); +return { success: true, started: true }; +} +}, + +download_subs_status: { +call: function() { +let p = popen('ps w 2>/dev/null | grep "[c]lashoo/runtime/clashoo.sh" | head -1'); +let line = ''; if (p) { line = trim(p.read('all')); p.close(); } +let running = (line && line != ''); +let last_line = ''; +let p2 = popen('tail -1 /tmp/clash_update.txt 2>/dev/null'); +if (p2) { last_line = trim(p2.read('all')); p2.close(); } +// 收尾行「订阅下载失败:全部链接失败」太笼统,额外捞最近一条带真因的 +// 失败行(下载失败 rc=/HTTP、校验失败 格式不符),让前端能显示具体原因 +let fail_detail = ''; +let p3 = popen("grep -aE '下载失败:.*(rc=|HTTP )|校验失败:' /tmp/clash_update.txt 2>/dev/null | tail -1"); +if (p3) { fail_detail = trim(p3.read('all')); p3.close(); } +return { running, last_line, fail_detail }; +} +}, + +/* ── 简易代理面板:代理 clash API(前端不直接碰 9090 / secret)── */ +proxies_list: { +call: function() { +let data = clash_api_json('/proxies', 6); +if (!data || !data.proxies) + return { ok: false, reason: 'unreachable' }; +let proxies = data.proxies; +let switchable = { 'Selector': 1 }; +let readonly = { 'URLTest': 1, 'Fallback': 1, 'LoadBalance': 1, 'Relay': 1 }; +let groups = []; +let order = []; +let global = proxies['GLOBAL']; +if (global && global.all && length(global.all)) { + for (let i = 0; i < length(global.all); i++) { + if (global.all[i] != 'GLOBAL') push(order, global.all[i]); + } +} else { + for (let gname in proxies) { + if (gname != 'GLOBAL') push(order, gname); + } +} +for (let i = 0; i < length(order); i++) { + let gname = order[i]; + let g = proxies[gname]; + if (!g || type(g) != 'object') continue; + let gt = g.type || ''; + if (!switchable[gt] && !readonly[gt]) continue; + if (gname == 'GLOBAL') continue; + let members = []; + for (let mn in (g.all || [])) { + let mp = proxies[mn]; + let delay = 0; + if (mp && mp.history && length(mp.history) > 0) + delay = mp.history[length(mp.history) - 1].delay || 0; + push(members, { name: mn, delay: delay }); + } + push(groups, { + name: gname, + type: gt, + now: g.now || '', + selectable: !!switchable[gt], + members: members + }); +} +return { ok: true, groups: groups }; +} +}, + +/* ── 探测主代理组:供自定义分流规则的「代理」(__PROXY__) 解析用 ── + * 取一个可切换的 Selector 组(优先名字像主代理组的,排除 GLOBAL),写入 + * clashoo.config.primary_proxy_group,iprules.sh 注入 mihomo 规则时引用。 */ +detect_primary_group: { +call: function() { +let data = clash_api_json('/proxies', 6); +if (!data || !data.proxies) + return { ok: false, reason: 'unreachable' }; +let proxies = data.proxies; +let first = ''; +let preferred = ''; +for (let gname in proxies) { + let g = proxies[gname]; + if (!g || type(g) != 'object') continue; + if ((g.type || '') != 'Selector') continue; + if (gname == 'GLOBAL') continue; + if (first == '') first = gname; + if (preferred == '' && match(gname, /节点选择|代理|Proxy|PROXY|🚀/)) + preferred = gname; +} +let grp = preferred != '' ? preferred : first; +if (grp == '') + return { ok: false, reason: 'no_selector' }; +let c = cursor(); +c.set('clashoo', 'config', 'primary_proxy_group', grp); +c.commit('clashoo'); +return { ok: true, group: grp }; +} +}, + +proxy_select: { +args: { group: 'group', name: 'name' }, +call: function(req) { +let group = trim(req.args?.group || ''); +let name = trim(req.args?.name || ''); +if (!group || !name) return { ok: false, message: '参数缺失' }; +let code = clash_api_put_json('/proxies/' + uri_encode(group), { name: name }); +if (code == '204' || code == '200') return { ok: true }; +if (code == '400') return { ok: false, code: code, message: '该节点不可选(组类型不支持或节点无效)' }; +return { ok: false, code: code, message: '切换失败 (HTTP ' + code + ')' }; +} +}, + +proxy_delay: { +args: { group: 'group' }, +call: function(req) { +let group = trim(req.args?.group || ''); +if (!group) return { ok: false, message: '参数缺失' }; +let path = '/group/' + uri_encode(group) + '/delay' + + '?url=' + uri_encode('https://www.gstatic.com/generate_204') + + '&timeout=3000'; +let data = clash_api_json(path, 14); +if (!data || type(data) != 'object') + return { ok: false, message: '测速失败或超时' }; +if (data.message) + return { ok: false, message: '' + data.message }; +return { ok: true, delays: data }; +} +}, + +update_sub: { +args: { name: 'name' }, +call: function(req) { +let name = req.args?.name || ''; +if (!name) return { error: 'missing name' }; +let rc = system('sh ' + shell_quote(SUBSCRIPTION_UPDATE_SCRIPT) + ' --mihomo ' + shell_quote(name) + ' >/dev/null 2>&1'); +if (rc != 0) { +return { success: false, code: rc, message: '更新失败: ' + name }; +} +return { success: true, code: rc, message: '更新完成: ' + name }; +} +}, + +update_current_subscription: { +call: function() { +let c = cursor(); c.load('clashoo'); +if (get_core_type() == 'singbox') { + let name = c.get('clashoo', 'config', 'singbox_active') || ''; + let path = SINGBOX_DIR + '/' + name; + if (!name || !access(path + '.url', 'r') || !trim(readfile(path + '.url') || '')) + return { success: false, reason: 'not_subscription', message: '当前配置未记录订阅链接' }; + let rc = system('sh ' + shell_quote(SUBSCRIPTION_UPDATE_SCRIPT) + ' --singbox ' + shell_quote(name) + ' >/dev/null 2>&1'); + return { success: rc == 0, code: rc, message: rc == 0 ? '当前订阅更新完成' : '当前订阅更新失败' }; +} + +let path = c.get('clashoo', 'config', 'use_config') || ''; +let slash = rindex(path, '/'); +let name = slash >= 0 ? substr(path, slash + 1) : path; +let found = false; +for (let sub in read_subscriptions()) { + if (sub.name == name && sub.url) { + found = true; + break; + } +} +if (!found) + return { success: false, reason: 'not_subscription', message: '当前配置未记录订阅链接' }; +let rc = system('sh ' + shell_quote(SUBSCRIPTION_UPDATE_SCRIPT) + ' --mihomo ' + shell_quote(name) + ' >/dev/null 2>&1'); +return { success: rc == 0, code: rc, message: rc == 0 ? '当前订阅更新完成' : '当前订阅更新失败' }; +} +}, + +subscription_update_all: { +call: function() { +if (access(SUBSCRIPTION_UPDATE_LOCK_DIR, 'r')) + return { success: false, running: true, message: '订阅更新正在运行' }; +let rc = system('sh ' + shell_quote(SUBSCRIPTION_UPDATE_SCRIPT) + ' --all >/dev/null 2>&1 &'); +return { success: rc == 0, running: rc == 0, message: rc == 0 ? '已开始更新全部订阅' : '启动失败' }; +} +}, + +subscription_update_status: { +call: function() { +return subscription_update_status_data(); +} +}, + +set_subscription_update_schedule: { +args: { enabled: 'enabled', interval: 'interval' }, +call: function(req) { +let enabled = req.args?.enabled == '1' ? '1' : '0'; +let interval_raw = '' + (req.args?.interval || ''); +if (!match(interval_raw, /^[0-9]+$/)) + return { success: false, message: '更新间隔必须为数字' }; +let interval = int(interval_raw); +if (interval < 1 || interval > 8760) + return { success: false, message: '更新间隔必须为 1 到 8760 小时' }; +let c = cursor(); c.load('clashoo'); +c.set('clashoo', 'config', 'auto_subscription_update', enabled); +c.set('clashoo', 'config', 'subscription_update_interval', '' + interval); +c.commit('clashoo'); +system('/etc/init.d/clashoo refresh_cron >/dev/null 2>&1'); +return { success: true, enabled: enabled, interval: interval }; +} +}, + +apply_template_with_url: { +args: { +template_source: 'template_source', +sub_url: 'sub_url', +output_name: 'output_name', +set_active: 'set_active' +}, +call: function(req) { +let tpl_src = trim(req.args?.template_source || ''); +let sub_url = trim(req.args?.sub_url || ''); +let output_name = ensure_yaml_name(req.args?.output_name || ''); +if (!output_name) output_name = 'tpl-rewrite.yaml'; +let uniq_name = replace(output_name, /\.(yaml|yml)$/, ''); +let set_active = (req.args?.set_active || '') == '1'; + +if (!tpl_src) +return { success: false, error: 'invalid_template', message: '模板来源不能为空' }; +if (!sub_url || !match(sub_url, /^https?:\/\//)) +return { success: false, error: 'invalid_sub_url', message: '订阅 URL 无效' }; + +let tmp_tpl = '/tmp/clashoo_tpl_in.yaml'; +let tmp_out = '/tmp/clashoo_tpl_out.yaml'; +let out_dir = '/usr/share/clashoo/config/custom'; +system('mkdir -p ' + shell_quote(out_dir)); + +/* 获取模板:远程 URL 或本地文件 */ +if (match(tpl_src, /^https?:\/\//)) { +let dl_cmd = '(curl -fL --connect-timeout 15 --max-time 60 ' + shell_quote(tpl_src) ++ ' -o ' + shell_quote(tmp_tpl) ++ ' || wget -O ' + shell_quote(tmp_tpl) + ' ' + shell_quote(tpl_src) ++ ') >/dev/null 2>&1'; +if (system(dl_cmd) != 0 || !access(tmp_tpl, 'r')) +return { success: false, error: 'download_failed', message: '远程模板下载失败' }; +} else { +let safe = safe_name(tpl_src); +let local_path = null; +let search_dirs = template_search_dirs(); +for (let i = 0; i < length(search_dirs); i++) { +if (access(search_dirs[i] + '/' + safe, 'r')) { local_path = search_dirs[i] + '/' + safe; break; } +} +if (!local_path) +return { success: false, error: 'template_not_found', message: '本地模板文件不存在' }; +if (system('cp -f ' + shell_quote(local_path) + ' ' + shell_quote(tmp_tpl)) != 0) +return { success: false, error: 'copy_failed', message: '读取本地模板失败' }; +} + +/* 用 yq 把所有 proxy-providers 条目的 url 替换为订阅链接 */ +if (system('command -v yq >/dev/null 2>&1') != 0) +return { success: false, error: 'missing_yq', message: '缺少 yq,无法注入订阅 URL' }; + + /* 先展开 YAML anchor(<<: *xxx),再注入订阅 URL */ + let exploded = tmp_tpl + ".exploded"; + system("yq ea \"explode(.)\" " + shell_quote(tmp_tpl) + " > " + shell_quote(exploded) + " 2>/dev/null"); + if (!access(exploded, "r")) { + system("cp -f " + shell_quote(tmp_tpl) + " " + shell_quote(exploded)); + } + /* 替换 proxy-providers.url 并把 provider 名改成唯一名(同步 proxy-groups 的 use)*/ + let inject_cmd = "URL=" + shell_quote(sub_url) + " NAME=" + shell_quote(uniq_name) + + " yq e '.[\"proxy-providers\"][].url = env(URL) | (.[\"proxy-providers\"] | keys | map({\"key\": ., \"value\": strenv(NAME) + \"-\" + .}) | from_entries) as $rename | .[\"proxy-providers\"] |= with_entries(.key = $rename[.key]) | (.[\"proxy-groups\"][] | select(has(\"use\")) | .use) |= map($rename[.] // .)' " + + shell_quote(exploded) + " > " + shell_quote(tmp_out); + if (system(inject_cmd) != 0 || !access(tmp_out, "r")) { + system("rm -f " + shell_quote(exploded)); + return { success: false, error: "inject_failed", message: "订阅 URL 注入失败" }; + } + system("rm -f " + shell_quote(exploded)); + +let out_path = out_dir + '/' + output_name; +if (system('mv -f ' + shell_quote(tmp_out) + ' ' + shell_quote(out_path)) != 0) +return { success: false, error: 'write_failed', message: '写入结果文件失败' }; + +/* 删除 respect-rules(模板可能设了但没提供 proxy-server-nameserver) */ +system("sed -i '/respect-rules/d' " + shell_quote(out_path) + " 2>/dev/null"); +system('rm -f ' + shell_quote(tmp_tpl)); + /* 只把独立引用项"直连"转成 DIRECT,组名里的"直连"不动 */ + system("sed -i 's/^\\( *- *\\)直连$/\\1DIRECT/' " + shell_quote(out_path) + " 2>/dev/null"); + system("yq e '(.proxy-groups[] | select(has(\"proxies\")) | .proxies) |= map(sub(\"^直连$\", \"DIRECT\"))' " + shell_quote(out_path) + " -i 2>/dev/null"); + /* 用 mixin.uc + yq merge 叠加 UCI 覆盖(端口、routing-mark、dns 等) */ + let mixin_script = "/usr/share/clashoo/runtime/mixin.uc"; + if (access(mixin_script, "r") && system("command -v yq >/dev/null 2>&1") == 0) { + let mixin_yaml = "/tmp/_clashoo_mixin.yaml"; + let merged_out = out_path + ".merged"; + system("ucode " + shell_quote(mixin_script) + " 2>/dev/null | yq -M -p json -o yaml > " + shell_quote(mixin_yaml) + " 2>/dev/null"); + system("yq -M eval-all '(. as $item ireduce ({}; . * $item)) as $m | [\"port\",\"socks-port\",\"redir-port\",\"tproxy-port\",\"mixed-port\",\"allow-lan\",\"bind-address\",\"mode\",\"log-level\",\"ipv6\",\"interface-name\",\"routing-mark\",\"external-controller\",\"external-ui\",\"secret\",\"tun\",\"listeners\",\"tunnels\",\"dns\",\"hosts\",\"profile\",\"geodata-mode\",\"geodata-loader\",\"geox-url\",\"proxy-providers\",\"proxies\",\"proxy-groups\",\"rule-providers\",\"rules\",\"sub-rules\",\"ntp\",\"authentication\",\"sniffer\",\"experimental\"] as $order | ($order | to_entries | map({\"key\": .value, \"value\": .key}) | from_entries) as $rank | $m | to_entries | sort_by(.key as $k | $rank[$k] // 999) | from_entries' " + shell_quote(out_path) + " " + shell_quote(mixin_yaml) + " > " + shell_quote(merged_out) + " 2>/dev/null"); + if (access(merged_out, "r")) { + system("mv -f " + shell_quote(merged_out) + " " + shell_quote(out_path)); + } + system("rm -f " + shell_quote(mixin_yaml)); + } + + + /* ── mihomo -t 预校验(搜所有 mihomo 变体)── */ + let _core_bin = ''; + for (let _cm in ['mihomo', 'clash-meta', 'clash']) { + if (system('command -v ' + _cm + ' >/dev/null 2>&1') == 0) { + _core_bin = _cm; + break; + } + } + if (_core_bin) { + let _ret = system(_core_bin + " -t -d /etc/clashoo -f " + shell_quote(out_path) + " 2>/tmp/clashoo_tpl_verify.txt"); + if (_ret != 0) { + let _err = trim(readfile('/tmp/clashoo_tpl_verify.txt') || '配置校验失败'); + system('rm -f ' + shell_quote(out_path) + ' /tmp/clashoo_tpl_verify.txt 2>/dev/null'); + return { success: false, error: 'validation_failed', message: '模板校验失败: ' + _err }; + } + system('rm -f /tmp/clashoo_tpl_verify.txt 2>/dev/null'); + } + +if (set_active) { +let c = cursor(); +c.load('clashoo'); +c.set('clashoo', 'config', 'use_config', out_path); +c.set('clashoo', 'config', 'config_type', '3'); +c.commit('clashoo'); +if (is_running()) +system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); +} + +return { success: true, output_name, output_path: out_path, message: '模板已应用: ' + output_name }; +} +}, + +apply_rewrite: { +args: { +base_type: 'base_type', +base_name: 'base_name', +rewrite_type: 'rewrite_type', +rewrite_name: 'rewrite_name', +output_name: 'output_name', +set_active: 'set_active' +}, +call: function(req) { +let base_type = req.args?.base_type || ''; +let base_name = safe_name(req.args?.base_name || ''); +let rewrite_type = req.args?.rewrite_type || ''; +let rewrite_name = safe_name(req.args?.rewrite_name || ''); +let output_name = ensure_yaml_name(req.args?.output_name || ''); +let set_active = (req.args?.set_active || '') == '1'; + +let base_dir = config_dir_by_type(base_type); +let rewrite_dir = config_dir_by_type(rewrite_type); + +if (!base_dir || !base_name) +return { success: false, error: 'invalid_base', message: '主配置无效' }; +if (!rewrite_dir || !rewrite_name) +return { success: false, error: 'invalid_rewrite', message: '复写配置无效' }; + +let base_path = base_dir + '/' + base_name; +let rewrite_path = rewrite_dir + '/' + rewrite_name; + +if (!access(base_path, 'r')) +return { success: false, error: 'base_not_found', message: '主配置文件不存在' }; +if (!access(rewrite_path, 'r')) +return { success: false, error: 'rewrite_not_found', message: '复写配置文件不存在' }; + +if (!output_name) { +let stem = replace(base_name, /\.(yaml|yml)$/, ''); +output_name = stem + '_rewrite.yaml'; +} + +let out_dir = '/usr/share/clashoo/config/custom'; +let out_path = out_dir + '/' + output_name; +let tmp_path = '/tmp/clash_rewrite_merged.yaml'; + +system('mkdir -p ' + shell_quote(out_dir)); + +/* Template mode: when rewrite file comes from custom dir, always prefer template merge workflow. */ +if (rewrite_type == '3' || rewrite_type == 'custom' || rewrite_dir == '/usr/share/clashoo/config/custom') { +let tcmd = 'sh /usr/share/clashoo/update/template_merge.sh ' + + shell_quote(base_path) + ' ' + + shell_quote(rewrite_path) + ' ' + + shell_quote(out_path) + + ' >/dev/null 2>&1'; +if (system(tcmd) != 0) +return { success: false, error: 'merge_failed', message: '模板复写失败,请检查模板内容与订阅格式' }; +} +else { +if (system('command -v yq >/dev/null 2>&1') != 0) +return { success: false, error: 'missing_yq', message: '缺少 yq,无法执行复写' }; + +let cmd = "yq eval-all '(select(fileIndex == 0) | explode(.)) * (select(fileIndex == 1) | explode(.))' " ++ shell_quote(base_path) + ' ' + shell_quote(rewrite_path) ++ ' > ' + shell_quote(tmp_path); + +if (system(cmd) != 0 || !access(tmp_path, 'r')) +return { success: false, error: 'merge_failed', message: '复写合并失败,请检查 YAML 格式' }; + +if (system('yq e \'.\' ' + shell_quote(tmp_path) + ' >/dev/null 2>&1') != 0) { +system('rm -f ' + shell_quote(tmp_path)); +return { success: false, error: 'invalid_yaml', message: '复写结果 YAML 无效,请检查锚点/别名配置' }; +} + +let binary = find_binary(); +if (binary && system(shell_quote(binary) + ' -t -f ' + shell_quote(tmp_path) + ' >/dev/null 2>&1') != 0) { +system('rm -f ' + shell_quote(tmp_path)); +return { success: false, error: 'invalid_runtime_yaml', message: '复写结果与内核不兼容,请检查复写内容' }; +} + +if (system('mv -f ' + shell_quote(tmp_path) + ' ' + shell_quote(out_path)) != 0) +return { success: false, error: 'write_failed', message: '写入复写文件失败' }; +} + +if (set_active) { +let c = cursor(); +c.load('clashoo'); +c.set('clashoo', 'config', 'use_config', out_path); +c.set('clashoo', 'config', 'config_type', '3'); +c.commit('clashoo'); +if (is_running()) +system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); +} + +return { +success: true, +output_name, +output_path: out_path, +message: '复写成功: ' + output_name +}; +} +}, + +fetch_rewrite_url: { +args: { url: 'url', name: 'name' }, +call: function(req) { +let url = trim(req.args?.url || ''); +let name = ensure_yaml_name(req.args?.name || ''); + +if (!url || !match(url, /^https?:\/\//)) +return { success: false, error: 'invalid_url', message: '无效的 URL' }; + +if (!name) +name = file_name_from_url(url); +if (!name) +name = 'rewrite-remote.yaml'; + +let out_dir = '/usr/share/clashoo/config/custom'; +let out_path = out_dir + '/' + name; +let tmp_path = '/tmp/rewrite_remote.yaml'; + +system('mkdir -p ' + shell_quote(out_dir)); + +let dl_cmd = '(curl -fL --connect-timeout 15 --max-time 90 ' + shell_quote(url) + ' -o ' + shell_quote(tmp_path) + + ' || wget -O ' + shell_quote(tmp_path) + ' ' + shell_quote(url) + ') >/dev/null 2>&1'; + +if (system(dl_cmd) != 0 || !access(tmp_path, 'r')) +return { success: false, error: 'download_failed', message: '远程复写文件下载失败' }; + +if (system('yq e \'.\' ' + shell_quote(tmp_path) + ' >/dev/null 2>&1') != 0) { +system('rm -f ' + shell_quote(tmp_path)); +return { success: false, error: 'invalid_yaml', message: '远程复写文件 YAML 格式无效' }; +} + +if (system('mv -f ' + shell_quote(tmp_path) + ' ' + shell_quote(out_path)) != 0) +return { success: false, error: 'write_failed', message: '保存远程复写文件失败' }; + +return { +success: true, +name, +path: out_path, +message: '远程复写文件已保存: ' + name +}; +} +}, + +read_other_config: { +args: { name: 'name', type: 'type' }, +call: function(req) { +let name = safe_name(req.args?.name || ''); +let type = req.args?.type || '2'; +if (!name) + return { error: 'invalid name' }; +let dir = config_dir_by_type(type); +if (!dir) return { error: 'invalid type' }; +let path = dir + '/' + name; +if (!access(path, 'r')) return { error: 'not found' }; +let s = stat(path); +if (s && s.size > 524288) return { error: 'too large' }; +return { content: readfile(path) || '', name }; +} +}, + +upload_config: { +args: { name: 'name', content: 'content', type: 'type' }, +call: function(req) { +let name = safe_name(ensure_yaml_name(req.args?.name || '')); +let content = req.args?.content || ''; +let type = req.args?.type || '2'; +if (!name) return { success: false, error: 'invalid_name', message: '文件名无效' }; +if (!content) return { success: false, error: 'empty_content', message: '文件内容为空' }; +let dir = config_dir_by_type(type); +if (!dir) dir = '/usr/share/clashoo/config/upload'; +system('mkdir -p ' + shell_quote(dir)); +let path = dir + '/' + name; +if (writefile(path, content) === null) +return { success: false, error: 'write_failed', message: '写入文件失败' }; +return { success: true, name, path }; +} +}, + +upload_config_chunk: { +args: { name: 'name', content: 'content', type: 'type', index: 'index', total: 'total' }, +call: function(req) { +let name = safe_name(ensure_yaml_name(req.args?.name || '')); +let content = req.args?.content; +let cfg_type = req.args?.type || '2'; +let index = int(req.args?.index || 0); +let total = int(req.args?.total || 0); +if (!name) return { success: false, error: 'invalid_name', message: '文件名无效' }; +if (type(content) != 'string') return { success: false, error: 'invalid_content', message: '文件内容无效' }; +if (total < 1 || index < 0 || index >= total) return { success: false, error: 'invalid_chunk', message: '上传分片无效' }; +if (total == 1 && !content) return { success: false, error: 'empty_content', message: '文件内容为空' }; +let dir = config_dir_by_type(cfg_type); +if (!dir) dir = '/usr/share/clashoo/config/upload'; +system('mkdir -p ' + shell_quote(dir)); +let path = dir + '/' + name; + +if (index == 0) { + if (writefile(path, content) === null) + return { success: false, error: 'write_failed', message: '写入文件失败' }; +} else { + let fp = open(path, 'a'); + if (!fp) return { success: false, error: 'write_failed', message: '打开文件失败' }; + let ok = fp.write(content); + fp.close(); + if (ok === null) return { success: false, error: 'write_failed', message: '写入文件失败' }; +} + +return { success: true, name, path, index, total, complete: index + 1 >= total }; +} +}, + +upload_template: { +args: { name: 'name', content: 'content' }, +call: function(req) { +let name = safe_name(ensure_yaml_name(req.args?.name || '')); +let content = req.args?.content || ''; +if (!name) +return { success: false, error: 'invalid_name', message: '模板文件名无效' }; +if (!content) +return { success: false, error: 'empty_content', message: '模板内容为空' }; + +system('mkdir -p ' + shell_quote(TEMPLATE_USER_DIR) + ' ' + shell_quote(TEMPLATE_DIR)); +let user_path = TEMPLATE_USER_DIR + '/' + name; +if (writefile(user_path, content) === null) +return { success: false, error: 'write_failed', message: '写入模板失败' }; + +let custom_path = TEMPLATE_DIR + '/' + name; +if (writefile(custom_path, content) === null) +return { success: false, error: 'mirror_failed', message: '写入模板镜像失败' }; + +return { success: true, name, path: user_path, mirror: custom_path }; +} +}, + +smart_flush_cache: { +call: function() { +let code = mihomo_api_post('/cache/smart/flush'); +return { success: (code == '204' || code == '200'), code }; +} +}, + +/* ── sing-box profile management ── */ + +list_singbox_profiles: { +call: function() { +let dir = '/usr/share/clashoo/config/singbox'; +system('mkdir -p ' + shell_quote(dir) + ' >/dev/null 2>&1'); +let active = uci_get('clashoo', 'config', 'singbox_active') || ''; + +// 把 "211.98 GB" / "1000 MB" 之类解析成字节数;返回 0 表示无法识别 +let parse_size_bytes = function(num_str, unit_str) { + let n = +num_str; + if (n != n) return 0; // NaN + let u = uc(unit_str || ''); + let mul = 1; + if (u == 'KB' || u == 'K') mul = 1024; + else if (u == 'MB' || u == 'M') mul = 1024 * 1024; + else if (u == 'GB' || u == 'G') mul = 1024 * 1024 * 1024; + else if (u == 'TB' || u == 'T') mul = 1024 * 1024 * 1024 * 1024; + return int(n * mul); +}; + +// busybox date 把 yyyy-mm-dd 转 unix ts;失败返回 0 +let ymd_to_ts = function(s) { + let p = popen('date -D %Y-%m-%d -d ' + shell_quote(s) + ' +%s 2>/dev/null'); + if (!p) return 0; + let v = trim(p.read('all')); + p.close(); + let n = +v; + return (n == n && n > 0) ? int(n) : 0; +}; + +// 优先读 .info 文件(Subscription-Userinfo 响应头),其次从 outbound tag 提取 +// 常见 tag:"Traffic: 211.98 GB | 1000 GB"、"Expire: 2026-08-15" +let extract_meta = function(path) { + // Primary: .info file saved during download (same format as mihomo) + let info_path = path + '.info'; + if (access(info_path, 'r')) { + let raw_info = trim(readfile(info_path) || ''); + if (raw_info) { + let ui = parse_sub_userinfo(raw_info); + let r = {}; + if (ui.total) { r.sub_total = ui.total; r.sub_used = ui.upload + ui.download; } + if (ui.expire) r.sub_expire = ui.expire; + if (r.sub_total || r.sub_expire) return r; + } + } + // Fallback: parse from outbound tags in JSON + let st = stat(path); + if (!st || st.size > 524288) return {}; + let raw = readfile(path); + if (!raw) return {}; + let obj = json(raw); + if (!obj || type(obj.outbounds) != 'array') return {}; + let used = 0, total = 0, expire = 0; + for (let ob in obj.outbounds) { + let tag = (ob && ob.tag) ? ob.tag : ''; + if (!tag) continue; + if (!total) { + let m = match(tag, /Traffic[::]\s*([0-9.]+)\s*([KMGT]?B)\s*[|\/]\s*([0-9.]+)\s*([KMGT]?B)/); + if (m) { + used = parse_size_bytes(m[1], m[2]); + total = parse_size_bytes(m[3], m[4]); + } + } + if (!expire) { + let m2 = match(tag, /Expire[::]\s*([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})/); + if (m2) expire = ymd_to_ts(m2[1]); + } + if (total && expire) break; + } + let r = {}; + if (total) { r.sub_total = total; r.sub_used = used; } + if (expire) r.sub_expire = expire; + return r; +}; + +let files = []; +for (let f in (glob(dir + '/*.json') || [])) { + let name = replace(f, dir + '/', ''); + let fi = stat(f); + let entry = { + name, + mtime: fi ? ('' + fi.mtime) : null, + size: fi ? (int(fi.size / 1024) + ' KB') : null, + active: name == active + }; + let meta = extract_meta(f); + for (let k in meta) entry[k] = meta[k]; + let url_path = f + '.url'; + if (access(url_path, 'r')) { + entry.sub_url = trim(readfile(url_path) || ''); + entry.source = 'native'; + } else { + entry.source = 'local'; + } + push(files, entry); +} +return { profiles: files, active }; +} +}, + +get_singbox_profile: { +args: { name: 'name' }, +call: function(req) { +let name = req.args?.name || ''; +if (!name || index(name, '/') >= 0 || index(name, '..') >= 0) + return { error: 'invalid name' }; +let path = '/usr/share/clashoo/config/singbox/' + name; +if (!access(path, 'r')) return { error: 'not found' }; +let s = stat(path); +if (s && s.size > 524288) return { error: 'too large' }; +return { content: readfile(path) || '', name }; +} +}, + +save_singbox_profile: { +args: { name: 'name', content: 'content' }, +call: function(req) { +let name = req.args?.name || ''; +let content = req.args?.content || ''; +if (!name || index(name, '/') >= 0 || index(name, '..') >= 0) + return { success: false, error: 'invalid name' }; +if (!content) return { success: false, error: 'empty content' }; +if (!match(name, /\.json$/)) name = name + '.json'; +let dir = '/usr/share/clashoo/config/singbox'; +system('mkdir -p ' + shell_quote(dir) + ' >/dev/null 2>&1'); +let tmp = '/tmp/clashoo_sb_validate.json'; +writefile(tmp, content); +let binary = find_binary(); +if (binary && match(binary, /sing-box/)) { + if (system(shell_quote(binary) + ' check -c ' + shell_quote(tmp) + ' >/dev/null 2>&1') != 0) { + system('rm -f ' + shell_quote(tmp)); + return { success: false, error: 'invalid_config', message: 'sing-box 配置验证失败,请检查 JSON 格式' }; + } +} +system('rm -f ' + shell_quote(tmp)); +if (writefile(dir + '/' + name, content) === null) + return { success: false, error: 'write_failed', message: '写入文件失败' }; +return { success: true, name }; +} +}, + +save_singbox_profile_chunk: { +args: { name: 'name', content: 'content', index: 'index', total: 'total' }, +call: function(req) { +let name = req.args?.name || ''; +let content = req.args?.content; +let idx = int(req.args?.index || 0); +let total = int(req.args?.total || 0); +if (!name || index(name, '/') >= 0 || index(name, '..') >= 0) + return { success: false, error: 'invalid name' }; +if (!match(name, /\.json$/)) name = name + '.json'; +if (type(content) != 'string') return { success: false, error: 'invalid content' }; +if (total < 1 || idx < 0 || idx >= total) return { success: false, error: 'invalid chunk' }; +if (total == 1 && !content) return { success: false, error: 'empty content' }; +let dir = '/usr/share/clashoo/config/singbox'; +system('mkdir -p ' + shell_quote(dir) + ' >/dev/null 2>&1'); +let tmp = '/tmp/clashoo_sb_upload_' + name; +if (idx == 0) { + if (writefile(tmp, content) === null) + return { success: false, error: 'write_failed', message: '写入文件失败' }; +} else { + let fp = open(tmp, 'a'); + if (!fp) return { success: false, error: 'write_failed', message: '打开文件失败' }; + let ok = fp.write(content); + fp.close(); + if (ok === null) return { success: false, error: 'write_failed', message: '写入文件失败' }; +} +if (idx + 1 < total) + return { success: true, name, index: idx, total, complete: false }; +let binary = find_binary(); +if (binary && match(binary, /sing-box/)) { + if (system(shell_quote(binary) + ' check -c ' + shell_quote(tmp) + ' >/dev/null 2>&1') != 0) { + system('rm -f ' + shell_quote(tmp)); + return { success: false, error: 'invalid_config', message: 'sing-box 配置验证失败,请检查 JSON 格式' }; + } +} +if (system('cp -f ' + shell_quote(tmp) + ' ' + shell_quote(dir + '/' + name) + ' >/dev/null 2>&1') != 0) { + system('rm -f ' + shell_quote(tmp)); + return { success: false, error: 'write_failed', message: '写入文件失败' }; +} +system('rm -f ' + shell_quote(tmp)); +return { success: true, name, index: idx, total, complete: true }; +} +}, + +set_singbox_profile: { +args: { name: 'name' }, +call: function(req) { +let name = req.args?.name || ''; +if (!name || index(name, '/') >= 0 || index(name, '..') >= 0) + return { success: false, error: 'invalid name' }; +let src = '/usr/share/clashoo/config/singbox/' + name; +if (!access(src, 'r')) return { success: false, error: 'not found' }; +let c = cursor(); c.load('clashoo'); +c.set('clashoo', 'config', 'core_type', 'singbox'); +c.set('clashoo', 'config', 'singbox_active', name); +c.set('clashoo', 'config', 'enable', '1'); +sync_legacy_core_fields(c); +c.commit('clashoo'); +let prepared = prepare_singbox_runtime(); +if (!prepared.success) + return { success: false, error: 'prepare_failed', message: prepared.message || '无法准备 sing-box 运行环境' }; +if (is_running()) system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); +return { success: true }; +} +}, + +delete_singbox_profile: { +args: { name: 'name' }, +call: function(req) { +let name = req.args?.name || ''; +if (!name || index(name, '/') >= 0 || index(name, '..') >= 0) + return { success: false, error: 'invalid name' }; +let path = '/usr/share/clashoo/config/singbox/' + name; +if (!access(path, 'r')) return { success: false, error: 'not found' }; +if (unlink(path) === null) return { success: false, error: 'delete_failed' }; +unlink(path + '.info'); +unlink(path + '.url'); +unlink(path + '.ua'); +let c = cursor(); c.load('clashoo'); +if (c.get('clashoo', 'config', 'singbox_active') == name) { + c.delete('clashoo', 'config', 'singbox_active'); + c.commit('clashoo'); +} +return { success: true }; +} +}, + +fetch_singbox_native: { +args: { url: 'url', name: 'name' }, +call: function(req) { +let sub_url = trim(req.args?.url || ''); +let name = trim(req.args?.name || ''); +if (!sub_url || !match(sub_url, /^https?:\/\//)) + return { success: false, message: '请输入有效的订阅链接(须以 http:// 或 https:// 开头)' }; +if (!name) name = 'singbox-native'; +name = replace(name, /\.json$/, ''); +name = replace(name, /[^A-Za-z0-9_.一-龥-]/g, '-'); +name = replace(name, /-+/g, '-'); +name = replace(name, /^[-.]|[-.]$/g, '') || 'singbox-native'; +let dir = '/usr/share/clashoo/config/singbox'; +system('mkdir -p ' + shell_quote(dir) + ' >/dev/null 2>&1'); +let final_name = name + '.json'; +let path = dir + '/' + final_name; +let tmp_json = '/tmp/clashoo_sb_native.json'; +let tmp_hdr = '/tmp/clashoo_sb_native.hdr'; +system('rm -f ' + shell_quote(tmp_json) + ' ' + shell_quote(tmp_hdr) + ' >/dev/null 2>&1'); +let sub_ua = uci_get('clashoo', 'config', 'sub_ua') || 'clash.meta'; +let dl_cmd = 'curl -fsSL --connect-timeout 8 --max-time 20' + + ' -D ' + shell_quote(tmp_hdr) + + ' -A ' + shell_quote(sub_ua) + + ' ' + shell_quote(sub_url) + + ' -o ' + shell_quote(tmp_json) + + ' >/dev/null 2>&1'; +if (system(dl_cmd) != 0 || !access(tmp_json, 'r')) { + system('rm -f ' + shell_quote(tmp_json) + ' ' + shell_quote(tmp_hdr) + ' >/dev/null 2>&1'); + return { success: false, message: '下载失败,请检查链接是否可访问' }; +} +let raw = readfile(tmp_json) || ''; +if (length(raw) < 10 || substr(trim(raw), 0, 1) !== '{') { + system('rm -f ' + shell_quote(tmp_json) + ' ' + shell_quote(tmp_hdr) + ' >/dev/null 2>&1'); + return { success: false, message: '下载内容不是 JSON,该链接可能不返回原生 sing-box 配置' }; +} +let cfg = json(raw); +if (!cfg || type(cfg) !== 'object') { + system('rm -f ' + shell_quote(tmp_json) + ' ' + shell_quote(tmp_hdr) + ' >/dev/null 2>&1'); + return { success: false, message: 'JSON 解析失败' }; +} +if (type(cfg.outbounds) !== 'array' || length(cfg.outbounds) == 0) { + system('rm -f ' + shell_quote(tmp_json) + ' ' + shell_quote(tmp_hdr) + ' >/dev/null 2>&1'); + return { success: false, message: '配置缺少 outbounds 字段,可能不是原生 sing-box 格式' }; +} +if (writefile(path, raw) === null) { + system('rm -f ' + shell_quote(tmp_json) + ' ' + shell_quote(tmp_hdr) + ' >/dev/null 2>&1'); + return { success: false, message: '保存失败' }; +} +writefile(path + '.url', sub_url); +writefile(path + '.ua', sub_ua); +if (access(tmp_hdr, 'r')) { + let hdr_raw = readfile(tmp_hdr) || ''; + for (let line in split(hdr_raw, '\n')) { + let ll = lc(trim(line)); + if (substr(ll, 0, 22) == 'subscription-userinfo:') { + let info_val = trim(substr(line, 22)); + if (info_val) writefile(path + '.info', info_val); + break; + } + } +} +system('rm -f ' + shell_quote(tmp_json) + ' ' + shell_quote(tmp_hdr) + ' >/dev/null 2>&1'); +return { success: true, name: final_name, message: '已拉取 ' + final_name }; +} +}, + +update_singbox_native: { +args: { name: 'name' }, +call: function(req) { +let name = req.args?.name || ''; +if (!name || index(name, '/') >= 0 || index(name, '..') >= 0) + return { success: false, message: 'invalid name' }; +let rc = system('sh ' + shell_quote(SUBSCRIPTION_UPDATE_SCRIPT) + ' --singbox ' + shell_quote(name) + ' >/dev/null 2>&1'); +return { success: rc == 0, code: rc, message: rc == 0 ? name + ' 已更新' : name + ' 更新失败' }; +} +}, + +migrate_singbox_profile: { +args: { name: 'name' }, +call: function(req) { +let name = req.args?.name || ''; +if (!name || index(name, '/') >= 0 || index(name, '..') >= 0) + return { success: false, error: 'invalid name' }; +let path = '/usr/share/clashoo/config/singbox/' + name; +if (!access(path, 'r')) return { success: false, error: 'not found' }; +let raw = readfile(path); +if (!raw) return { success: false, error: 'read_failed' }; +let cfg = json(raw); +if (!cfg) return { success: false, message: 'JSON 解析失败' }; + +let before = sprintf('%J', cfg); +cfg = migrate_singbox(cfg); +let after = sprintf('%J', cfg); + +/* 无变化 */ +if (before === after) { + let active_same = uci_get('clashoo', 'config', 'singbox_active') || ''; + if (active_same === name) { + let prepared_same = prepare_singbox_runtime(); + if (!prepared_same.success) + return { success: false, message: prepared_same.message || '运行配置准备失败' }; + if (is_running()) system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); + } + return { success: true, changes: [] }; +} + +/* 写回 */ +if (writefile(path, after) === null) + return { success: false, message: '写入文件失败' }; + +/* 如果是激活配置,同步到私有运行时;init 仍会在重启时再次规范化。 */ +let active = uci_get('clashoo', 'config', 'singbox_active') || ''; +if (active === name) { + let prepared = prepare_singbox_runtime(); + if (!prepared.success) + return { success: false, message: prepared.message || '运行配置准备失败' }; + if (is_running()) system('service ' + get_service_name() + ' restart >/dev/null 2>&1'); +} + +return { success: true, changes: ['deprecated fields removed/updated'] }; +} +}, + +create_singbox_config: { +args: { sub_url: 'sub_url', name: 'name' }, +call: function(req) { +let sub_url = trim(req.args?.sub_url || ''); +let name = trim(req.args?.name || ''); +// dashboard 字段(port/secret)由 normalize_singbox_config.uc 在启动时强制覆盖为 UCI 值, +// 这里写入仅作占位,无实际生效途径,因此直接读 UCI 即可。 +let secret = uci_get('clashoo', 'config', 'dash_pass') || ''; +let ext_port = uci_get('clashoo', 'config', 'dash_port') || '9090'; + +if (!sub_url || !match(sub_url, /^https?:\/\//)) + return { success: false, error: 'invalid_url', message: '请输入有效的订阅链接' }; + +if (!name) name = 'singbox'; +name = replace(name, /\.json$/, ''); +name = replace(name, /[^A-Za-z0-9_.\u4e00-\u9fa5-]/g, '-'); +name = replace(name, /-+/g, '-'); +name = replace(name, /^[-.]|[-.]$/g, '') || 'singbox'; + +let dir = '/usr/share/clashoo/config/singbox'; +system('mkdir -p ' + shell_quote(dir) + ' >/dev/null 2>&1'); + +let final_name = name + '.json'; + +// Write sub_url to temp file to avoid shell injection in curl call +let tmp_url = '/tmp/.clashoo-sb-url'; +writefile(tmp_url, sub_url); + +let config_obj = null; +let downloaded_yaml = ''; +let preferred_yaml = uci_get('clashoo', 'config', 'use_config') || ''; +let prefer_template = (preferred_yaml && access(preferred_yaml, 'r') && match(preferred_yaml, /\.(yaml|yml)$/)) || + match(sub_url, /flag=Clash/i) || match(sub_url, /format=clash/i); + +function has_proxy_outbounds(obj) { + if (!obj || type(obj) !== 'object' || type(obj.outbounds) !== 'array') + return false; + for (let ob in obj.outbounds) { + let t = ob?.type || ''; + if (t && t != 'selector' && t != 'urltest' && t != 'direct' && t != 'block' && t != 'dns') + return true; + } + return false; +} + +// Step 1: Try URL variants that might return native sing-box JSON +// Many providers support flag=SingBox / format=singbox even if the URL has flag=Clash +let try_urls = [sub_url]; +// If URL has a Clash flag, also try the sing-box variant +if (match(sub_url, /flag=Clash/i)) + push(try_urls, replace(sub_url, /flag=Clash/i, 'flag=SingBox')); +else if (match(sub_url, /format=clash/i)) + push(try_urls, replace(sub_url, /format=clash/i, 'format=singbox')); + +if (!prefer_template) { + let probe_ua = uci_get('clashoo', 'config', 'sub_ua') || 'clash.meta'; + for (let try_url in try_urls) { + let p0 = popen('curl -sL --max-time 12 -A ' + shell_quote(probe_ua) + ' ' + shell_quote(try_url) + ' 2>/dev/null'); + let raw0 = p0 ? trim(p0.read('all')) : ''; + if (p0) p0.close(); + if (length(raw0) > 50 && substr(raw0, 0, 1) === '{') { + let obj = json(raw0); + if (has_proxy_outbounds(obj)) { + config_obj = obj; + break; + } + } + } +} + +// Step 2 (本地最快路径): 把 yaml 喂给 yaml2singbox,避免外部 subconverter 30s × 3 +// 把它前置后,常规 yaml 订阅在 5s 内就生成完,不会触发 LuCI RPC ~30s 超时。 +if (!config_obj && system('command -v ucode >/dev/null 2>&1') == 0 && + access('/usr/share/clashoo/lib/yaml2singbox.uc', 'r') && + access('/usr/share/clashoo/lib/templates/default.json', 'r')) { + let yaml_src = ''; + if (!(yaml_src && access(yaml_src, 'r') && match(yaml_src, /\.(yaml|yml)$/))) { + let tmp_yaml = '/tmp/clashoo_sb_sub.yaml'; + let tmp_hdr = '/tmp/clashoo_sb_sub.hdr'; + system('rm -f ' + shell_quote(tmp_hdr) + ' >/dev/null 2>&1'); + let conv_ua = uci_get('clashoo', 'config', 'sub_ua') || 'clash.meta'; + let dl_cmd = 'curl -fsSL --connect-timeout 8 --max-time 20 -D ' + shell_quote(tmp_hdr) + ' -A ' + shell_quote(conv_ua) + ' ' + shell_quote(sub_url) + ' -o ' + shell_quote(tmp_yaml) + ' >/dev/null 2>&1 || wget -q --user-agent=' + shell_quote(conv_ua) + ' -O ' + shell_quote(tmp_yaml) + ' ' + shell_quote(sub_url) + ' >/dev/null 2>&1'; + if (system(dl_cmd) == 0 && access(tmp_yaml, 'r')) { + yaml_src = tmp_yaml; + downloaded_yaml = tmp_yaml; + } + } + + if (yaml_src && access(yaml_src, 'r')) { + let tmp_json = '/tmp/clashoo_sb_from_yaml.json'; + let cv_cmd = 'ucode /usr/share/clashoo/lib/yaml2singbox.uc ' + + shell_quote(yaml_src) + ' ' + + shell_quote('/usr/share/clashoo/lib/templates/default.json') + ' ' + + shell_quote(tmp_json) + ' >/tmp/clashoo_yaml2sb.log 2>&1'; + if (system(cv_cmd) == 0 && access(tmp_json, 'r')) { + let raw3 = readfile(tmp_json); + let obj3 = raw3 ? json(raw3) : null; + if (has_proxy_outbounds(obj3)) + config_obj = obj3; + } + system('rm -f ' + shell_quote(tmp_json)); + } +} + +// Step 3: 本地 yaml2singbox 失败再走外部 subconverter(base64 ss/vmess 链接列表 +// yaml2singbox 不识别,要靠外部转换器)。每个 max-time 12s,最坏 36s 兜底。 +if (!config_obj) { + let converters = [ + ['https://sub.kidsqq.cn/singbox?selectedRules=%5B%22Location%3ACN%22%2C%22Private%22%2C%22Non-China%22%2C%22Github%22%2C%22Google%22%2C%22Youtube%22%2C%22AI%2BServices%22%2C%22Telegram%22%2C%22Ad%2BBlock%22%2C%22Streaming%22%2C%22Apple%22%2C%22Social%2BMedia%22%5D&customRules=%5B%5D&group_by_country=true', 'config'], + ['https://sub.xeton.dev/sub?target=singbox', 'url'], + ['https://api.v1.mk/sub?target=singbox', 'url'], + ]; + for (let cv in converters) { + let base = cv[0], param = cv[1]; + let p = popen('curl -sL --max-time 12 -G --data-urlencode ' + + shell_quote(param + '@' + tmp_url) + ' ' + + shell_quote(base) + ' 2>/dev/null'); + let raw = p ? trim(p.read('all')) : ''; + if (p) p.close(); + if (length(raw) < 50 || substr(raw, 0, 1) !== '{') continue; + let obj = json(raw); + if (has_proxy_outbounds(obj)) { + config_obj = obj; + break; + } + } +} + +system('rm -f ' + shell_quote(tmp_url)); +if (downloaded_yaml) + system('rm -f ' + shell_quote(downloaded_yaml)); + + + +let content; +let from_converter = false; + +if (config_obj) { + from_converter = true; + // Migrate deprecated fields to sing-box 1.12+ format + config_obj = migrate_singbox(config_obj); + // Patch experimental.clash_api into the converted config + if (!config_obj.experimental) config_obj.experimental = {}; + config_obj.experimental.clash_api = { + external_controller: '0.0.0.0:' + ext_port, + external_ui: '/etc/clashoo/dashboard', + secret: secret + }; + // cache_file disabled - can cause "timeout" on first run without pre-existing cache + // config_obj.experimental.cache_file = { enabled: true }; + if (!config_obj.log) config_obj.log = { level: 'info', timestamp: true }; + // OpenWrt owns time sync; avoid sing-box NTP IPv6 noise on IPv4-only routers. + config_obj.ntp = config_obj.ntp || {}; + config_obj.ntp.enabled = false; + + // Fix rule_set URLs: rewrite to jsDelivr CDN (accessible from China) + // Pattern: https://[gh-proxy.com/]https://github.com/{owner}/{repo}/raw/refs/heads/{branch}/{path} + // → https://cdn.jsdelivr.net/gh/{owner}/{repo}@{branch}/{path} + if (config_obj.route && config_obj.route.rule_set) { + config_obj.route.rule_set = map(config_obj.route.rule_set, function(rs) { + if (rs.type !== 'remote' || !rs.url) return rs; + let url = rs.url; + // Strip gh-proxy.com wrapper if present + url = replace(url, /^https:\/\/gh-proxy\.com\//, ''); + // Convert raw.githubusercontent.com/{owner}/{repo}/{branch}/{path} → jsDelivr + let m = match(url, /^https:\/\/raw\.githubusercontent\.com\/([^\/]+)\/([^\/]+)\/([^\/]+)\/(.+)$/); + if (m) { + url = 'https://cdn.jsdelivr.net/gh/' + m[1] + '/' + m[2] + '@' + m[3] + '/' + m[4]; + } + // Convert github.com/{owner}/{repo}/raw/refs/heads/{branch}/{path} → jsDelivr + if (!m) { + m = match(url, /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)$/); + if (m) { + url = 'https://cdn.jsdelivr.net/gh/' + m[1] + '/' + m[2] + '@' + m[3] + '/' + m[4]; + } + } + if (url === rs.url) return rs; + let r = {}; + for (let k in rs) r[k] = rs[k]; + r.url = url; + return r; + }); + } + content = sprintf('%J\n', config_obj); +} else { + // Fallback: generate a skeleton sing-box config (no proxies) + // User should upload a proper sing-box JSON config instead + function je(s) { + s = replace(s || '', /\\/g, '\\\\'); + s = replace(s, /"/g, '\\"'); + return s; + } + // 没拿到任何代理节点时给一个最小可启动骨架: + // 不依赖任何远程 rule_set(路由器纯净环境 DIRECT 拉 jsdelivr 会卡 → FATAL), + // 等用户在 LuCI 里替换/上传带节点的配置后再启用规则。 + content = +'{\n' + +' "log": { "level": "info", "timestamp": true },\n' + +' "_note": "subconverter unavailable - replace outbounds with your proxies",\n' + +' "_sub_url": "' + je(sub_url) + '",\n' + +' "dns": {\n' + +' "servers": [\n' + +' { "tag": "remote", "type": "https", "server": "1.1.1.1" },\n' + +' { "tag": "local", "type": "udp", "server": "223.5.5.5" }\n' + +' ],\n' + +' "rules": [\n' + +' { "clash_mode": "Global", "action": "route", "server": "remote" },\n' + +' { "clash_mode": "Direct", "action": "route", "server": "local" }\n' + +' ],\n' + +' "final": "local"\n' + +' },\n' + +' "inbounds": [\n' + +' { "type": "tun", "tag": "tun-in",\n' + +' "address": ["172.19.0.1/30", "fdfe:dcba:9876::1/126"],\n' + +' "auto_route": true, "strict_route": true }\n' + +' ],\n' + +' "outbounds": [\n' + +' { "type": "direct", "tag": "DIRECT" }\n' + +' ],\n' + +' "route": {\n' + +' "default_domain_resolver": { "server": "local" },\n' + +' "rules": [\n' + +' { "action": "sniff" },\n' + +' { "protocol": "dns", "action": "hijack-dns" },\n' + +' { "clash_mode": "Direct", "outbound": "DIRECT" }\n' + +' ],\n' + +' "final": "DIRECT",\n' + +' "auto_detect_interface": true\n' + +' },\n' + +' "experimental": {\n' + +' "clash_api": {\n' + +' "external_controller": "0.0.0.0:' + je(ext_port) + '",\n' + +' "external_ui": "/etc/clashoo/dashboard",\n' + +' "secret": "' + je(secret) + '"\n' + +' },\n' + +' "cache_file": { "enabled": true, "store_fakeip": true }\n' + +' }\n' + +'}\n'; +} + +let path = dir + '/' + final_name; +if (writefile(path, content) === null) + return { success: false, error: 'write_failed', message: '写入配置文件失败' }; + +// Save Subscription-Userinfo from captured HTTP headers as .json.info +let sb_hdr_file = '/tmp/clashoo_sb_sub.hdr'; +if (access(sb_hdr_file, 'r')) { + let hdr_raw = readfile(sb_hdr_file) || ''; + for (let line in split(hdr_raw, '\n')) { + let ll = lc(trim(line)); + if (substr(ll, 0, 22) == 'subscription-userinfo:') { + let info_val = trim(substr(line, 22)); + if (info_val) writefile(path + '.info', info_val); + break; + } + } + system('rm -f ' + shell_quote(sb_hdr_file) + ' >/dev/null 2>&1'); +} + +let msg = from_converter + ? '配置已从订阅生成: ' + final_name + : '订阅转换失败,已生成基础模板(请手动添加代理): ' + final_name; +return { success: true, name: final_name, path, message: msg }; +} +}, + +/* 备份导出:把 UCI 配置 + 各类配置文件打包成一个 JSON 对象返回。 + 仅含纯文本,前端存成 clashoo-backup-*.json。内核/GeoIP 体积大且可重下,不备。 */ +backup_export: { +call: function() { +try { +let BACKUP_DIR_MAP = { + 'config/sub': '/usr/share/clashoo/config/sub', + 'config/upload': '/usr/share/clashoo/config/upload', + 'config/custom': '/usr/share/clashoo/config/custom', + 'config/singbox': SINGBOX_DIR, + 'templates': TEMPLATE_USER_DIR +}; +let BACKUP_FIXED_MAP = { + 'uci': '/etc/config/clashoo', + 'meta/confit_list.conf': LIST_FILE, + 'meta/template_bindings.conf': TEMPLATE_BIND_FILE +}; +let MAX_TOTAL = 480 * 1024; +let files = {}; +let total = 0; +let add_file = function(rel, path) { + if (!access(path, 'r')) return true; + let s = stat(path); + if (s && s.type != 'file') return true; + let content = readfile(path); + if (content === null) return true; + total += length(content); + if (total > MAX_TOTAL) return false; + files[rel] = content; + return true; +}; + +for (let rel in BACKUP_FIXED_MAP) + if (!add_file(rel, BACKUP_FIXED_MAP[rel])) + return { success: false, message: '备份内容过大(超过 480KB)' }; + +for (let prefix in BACKUP_DIR_MAP) { + let d = BACKUP_DIR_MAP[prefix]; + for (let f in (glob(d + '/*') || [])) { + let base = substr(f, length(d) + 1); + if (!add_file(prefix + '/' + base, f)) + return { success: false, message: '备份内容过大(超过 480KB)' }; + } +} + +let host = ''; +let hp = popen('uname -n 2>/dev/null'); +if (hp) { host = trim(hp.read('all')); hp.close(); } + +return { + success: true, + manifest: { + marker: 'clashoo-backup-v1', + created_at: now_epoch_sec(), + host: host, + app_version: read_first_line('/usr/share/clashoo/luci_version') + }, + files: files +}; +} catch (e) { +return { success: false, message: '导出失败: ' + e }; +} +} +}, + +/* 大文件备份走 cgi-download:每次导出使用独立的随机路径。 */ +backup_export_prepare: { +call: function() { +let export_path = ''; +let result_path = ''; +try { + if (!backup_tmp_ready()) + return { success: false, message: '备份临时目录不安全或无法创建' }; + export_path = new_backup_temp_path('export'); + if (!export_path) + return { success: false, message: '无法创建导出临时文件' }; + result_path = export_path + '.result'; + let cmd = 'sh ' + shell_quote(BACKUP_ARCHIVE_SCRIPT) + + ' export ' + shell_quote(export_path) + + ' >' + shell_quote(result_path) + ' 2>&1'; + if (system(cmd) != 0) { + let detail = trim(readfile(result_path) || ''); + unlink(export_path); + unlink(result_path); + return { success: false, message: detail || '生成备份文件失败' }; + } + let info = stat(export_path); + if (!info || info.type != 'file' || info.size <= 0) { + unlink(export_path); + unlink(result_path); + return { success: false, message: '生成的备份文件无效' }; + } + let stamp = ''; + let dp = popen('date +%Y%m%d-%H%M 2>/dev/null'); + if (dp) { stamp = trim(dp.read('all')); dp.close(); } + unlink(result_path); + return { + success: true, + path: export_path, + filename: 'clashoo-backup-' + (stamp || now_epoch_sec()) + '.tar.gz', + size: info.size + }; +} catch (e) { + if (export_path) unlink(export_path); + if (result_path) unlink(result_path); + return { success: false, message: '导出失败: ' + e }; +} +} +}, + +backup_export_cleanup: { +args: { path: 'path' }, +call: function(req) { + if (!backup_tmp_ready()) + return { success: false }; + let export_path = safe_backup_export_path(req.args?.path || ''); + if (!export_path) + return { success: false }; + unlink(export_path); + unlink(export_path + '.result'); + return { success: true }; +} +}, + +backup_import_prepare: { +call: function() { + if (!backup_tmp_ready()) + return { success: false, message: '备份临时目录不安全或无法创建' }; + let path = new_backup_temp_path('import'); + return path + ? { success: true, path: path } + : { success: false, message: '无法创建上传临时文件' }; +} +}, + +/* 分块写入在 RPC 后端逐次校验偏移和总大小,超限时立即删除。 */ +backup_import_chunk: { +args: { path: 'path', offset: 0, data: 'data' }, +call: function(req) { +try { + let upload_path = safe_backup_upload_path(req.args?.path || ''); + let offset = int(req.args?.offset || 0); + let encoded = req.args?.data || ''; + if (!upload_path || offset < 0) + return { success: false, message: '上传参数无效' }; + if (!backup_tmp_ready()) + return { success: false, message: '备份临时目录不安全或无法访问' }; + if (!active_backup_upload(upload_path)) { + release_backup_upload(upload_path); + return { success: false, message: '上传会话已失效,请重新选择备份文件' }; + } + let info = stat(upload_path); + if (!info || info.type != 'file' || info.size != offset) + return { success: false, message: '上传偏移不匹配,请重新选择备份文件' }; + if (!encoded || length(encoded) > BACKUP_MAX_CHUNK_BASE64_SIZE) { + release_backup_upload(upload_path); + return { success: false, message: '上传分块过大' }; + } + let chunk = null; + try { chunk = b64dec(encoded); } catch (e) { chunk = null; } + if (chunk === null || length(chunk) <= 0 || length(chunk) > BACKUP_MAX_CHUNK_SIZE) { + release_backup_upload(upload_path); + return { success: false, message: '上传分块无效' }; + } + let next_offset = offset + length(chunk); + if (next_offset > BACKUP_MAX_ARCHIVE_SIZE) { + release_backup_upload(upload_path); + return { success: false, message: '备份文件超过 32MB' }; + } + let fp = open(upload_path, 'a'); + if (!fp) { + release_backup_upload(upload_path); + return { success: false, message: '无法写入上传临时文件' }; + } + let written = fp.write(chunk); + fp.close(); + if (written != length(chunk)) { + release_backup_upload(upload_path); + return { success: false, message: '上传分块写入不完整' }; + } + if (!refresh_backup_upload(upload_path)) { + release_backup_upload(upload_path); + return { success: false, message: '无法刷新上传会话' }; + } + return { success: true, offset: next_offset }; +} catch (e) { + let upload_path = safe_backup_upload_path(req.args?.path || ''); + if (upload_path && backup_tmp_ready()) release_backup_upload(upload_path); + return { success: false, message: '上传失败: ' + e }; +} +} +}, + +backup_import_cleanup: { +args: { path: 'path' }, +call: function(req) { + if (!backup_tmp_ready()) + return { success: false }; + let upload_path = safe_backup_upload_path(req.args?.path || ''); + if (!upload_path) + return { success: false }; + release_backup_upload(upload_path); + return { success: true }; +} +}, + +/* 备份导入:用备份快照覆盖当前配置。先全量校验路径白名单(拒绝目录遍历), + 通过后才清空目标目录并重写,最后异步重启服务。 */ +backup_import: { +args: { data: 'data' }, +call: function(req) { + return restore_backup_json(req.args?.data || ''); +} +}, + +/* 上传内容落到后端生成的随机临时路径,前端不能指定其他位置。 */ +backup_import_file: { +args: { name: 'name', path: 'path' }, +call: function(req) { +try { + let name = req.args?.name || ''; + let upload_path = safe_backup_upload_path(req.args?.path || ''); + if (!upload_path) + return { success: false, message: '上传临时路径无效' }; + if (!backup_tmp_ready()) + return { success: false, message: '备份临时目录不安全或无法访问' }; + if (!active_backup_upload(upload_path)) { + release_backup_upload(upload_path); + return { success: false, message: '上传会话已失效,请重新选择备份文件' }; + } + let info = stat(upload_path); + if (!info || info.type != 'file') { + release_backup_upload(upload_path); + return { success: false, message: '没有找到已上传的备份文件' }; + } + if (info.size <= 0) { + release_backup_upload(upload_path); + return { success: false, message: '备份文件为空' }; + } + + if (match(lc(name), /\.json$/)) { + if (info.size > BACKUP_MAX_JSON_SIZE) { + release_backup_upload(upload_path); + return { success: false, message: '旧版 JSON 备份超过 8MB' }; + } + let raw = readfile(upload_path); + release_backup_upload(upload_path); + if (raw === null) + return { success: false, message: '读取备份文件失败' }; + return restore_backup_json(raw); + } + + if (!match(lc(name), /\.tar\.gz$/)) { + release_backup_upload(upload_path); + return { success: false, message: '仅支持 .tar.gz 和旧版 .json 备份文件' }; + } + + let result_path = upload_path + '.result'; + unlink(result_path); + let cmd = 'sh ' + shell_quote(BACKUP_ARCHIVE_SCRIPT) + + ' restore ' + shell_quote(upload_path) + + ' >' + shell_quote(result_path) + ' 2>&1'; + let rc = system(cmd); + let detail = trim(readfile(result_path) || ''); + release_backup_upload(upload_path); + if (rc != 0) + return { success: false, message: detail || '还原备份失败' }; + system('sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1'); + return { success: true, message: '备份已还原,正在重启服务' }; +} catch (e) { + let upload_path = safe_backup_upload_path(req.args?.path || ''); + if (upload_path && backup_tmp_ready()) release_backup_upload(upload_path); + return { success: false, message: '导入失败: ' + e }; +} +} +}, + +/* 还原默认:把出厂默认 UCI 覆盖回 /etc/config/clashoo,只重置设置, + 不动订阅/配置文件。出厂副本由 clashoo 包装在非 conffile 路径,永远最新。 */ +backup_reset: { +call: function() { +try { +let def = '/usr/share/clashoo/clashoo.default'; +if (!access(def, 'r')) + return { success: false, message: '出厂默认配置文件缺失,无法还原' }; +if (system('cp ' + shell_quote(def) + ' /etc/config/clashoo') != 0) + return { success: false, message: '写入默认配置失败' }; +system('sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1'); +return { success: true, message: '已还原出厂默认设置,正在重启服务' }; +} catch (e) { +return { success: false, message: '还原失败: ' + e }; +} +} +} +}; + +return { 'luci.clashoo': methods }; + +reload_service() { + restart +} diff --git a/scripts/gen-cn-nft.sh b/scripts/gen-cn-nft.sh new file mode 100644 index 000000000..ea9055c99 --- /dev/null +++ b/scripts/gen-cn-nft.sh @@ -0,0 +1,85 @@ +#!/bin/sh + +set -eu + +ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +OUT_DIR="${ROOT_DIR}/clashoo/files/usr/share/clashoo/nftables" +TMP_DIR="$(mktemp -d)" +V2DAT_BIN="${V2DAT_BIN:-}" +GEOIP_DAT_URL="${GEOIP_DAT_URL:-https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat}" +V2DAT_PKG="${V2DAT_PKG:-github.com/kenzok8/v2dat}" + +cleanup() { + rm -rf "$TMP_DIR" +} + +trap cleanup EXIT INT TERM + +ensure_v2dat() { + if [ -n "$V2DAT_BIN" ] && [ -x "$V2DAT_BIN" ]; then + return 0 + fi + if command -v v2dat >/dev/null 2>&1; then + V2DAT_BIN="$(command -v v2dat)" + return 0 + fi + export GOBIN="${TMP_DIR}/bin" + mkdir -p "$GOBIN" + go install "${V2DAT_PKG}@latest" + V2DAT_BIN="${GOBIN}/v2dat" +} + +render_nft_set() { + local family="$1" + local input_file="$2" + local output_file="$3" + local set_name="$4" + + awk -v set_name="$set_name" -v set_type="$family" ' + BEGIN { + print "set " set_name " {" + print "\ttype " set_type ";" + print "\tflags interval;" + print "\tauto-merge;" + print "\telements = {" + first = 1 + } + !/^[[:space:]]*$/ && !/^[[:space:]]*#/ { + gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0) + if ($0 == "") + next + if (!first) + print "," + printf "\t\t%s", $0 + first = 0 + } + END { + if (!first) + print "" + print "\t}" + print "}" + } + ' "$input_file" > "$output_file" +} + +mkdir -p "$OUT_DIR" "${TMP_DIR}/out" +ensure_v2dat + +curl -fsSL "$GEOIP_DAT_URL" -o "${TMP_DIR}/geoip.dat" +"$V2DAT_BIN" unpack geoip -o "${TMP_DIR}/out" -f cn "${TMP_DIR}/geoip.dat" + +CN_TXT="$(find "${TMP_DIR}/out" -type f -name '*_cn.txt' | head -1)" +[ -n "${CN_TXT}" ] && [ -f "${CN_TXT}" ] || { + echo "failed to unpack CN CIDRs from geoip.dat" >&2 + exit 1 +} + +awk 'index($0, ":") == 0 { print }' "${CN_TXT}" > "${TMP_DIR}/cn_ipv4.txt" +awk 'index($0, ":") != 0 { print }' "${CN_TXT}" > "${TMP_DIR}/cn_ipv6.txt" + +render_nft_set ipv4_addr "${TMP_DIR}/cn_ipv4.txt" "${OUT_DIR}/geoip_cn.nft" clashoo_china +render_nft_set ipv6_addr "${TMP_DIR}/cn_ipv6.txt" "${OUT_DIR}/geoip6_cn.nft" clashoo_china6 + +echo "generated:" +echo " ${OUT_DIR}/geoip_cn.nft" +echo " ${OUT_DIR}/geoip6_cn.nft" diff --git a/scripts/gen-singbox-rulesets.sh b/scripts/gen-singbox-rulesets.sh new file mode 100755 index 000000000..ce5a7c805 --- /dev/null +++ b/scripts/gen-singbox-rulesets.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +set -eu + +ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +RULESET_DIR="${ROOT_DIR}/clashoo/files/usr/share/clashoo/ruleset" +TMP_DIR="$(mktemp -d)" +GEOSITE_BASE="${GEOSITE_BASE:-https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite}" +GEOIP_BASE="${GEOIP_BASE:-https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip}" +# mihomo .mrs lives on the meta branch (binary, not .srs) +META_GEOSITE_BASE="${META_GEOSITE_BASE:-https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@meta/geo/geosite}" + +cleanup() { + rm -rf "$TMP_DIR" +} + +trap cleanup EXIT INT TERM + +fetch_ruleset() { + tag="$1" + base="$2" + remote_name="${3:-$tag}" + url="${base}/${remote_name}.srs" + tmp_file="${TMP_DIR}/${tag}.srs" + out_file="${RULESET_DIR}/${tag}.srs" + + echo "fetch ${tag}.srs" + curl -fsSL --retry 3 --retry-delay 2 "$url" -o "$tmp_file" + [ -s "$tmp_file" ] || { + echo "empty ruleset: $url" >&2 + exit 1 + } + mv "$tmp_file" "$out_file" +} + +mkdir -p "$RULESET_DIR" +rm -f "${RULESET_DIR}"/*.srs + +# Minimal built-in set: +# - geolocation-!cn: required by fake-ip DNS and broad non-CN routing. +# - geosite-cn: local alias for subscription tags geolocation-cn/cn. +# - cn-ip/private-ip: keep China/private IP direct without remote downloads. +fetch_ruleset "geolocation-!cn" "$GEOSITE_BASE" +fetch_ruleset "geosite-cn" "$GEOSITE_BASE" geolocation-cn +fetch_ruleset "private-ip" "$GEOIP_BASE" private +fetch_ruleset "cn-ip" "$GEOIP_BASE" cn + +# mihomo cn.mrs replaces geosite:cn in fake-ip-filter so the core skips geosite.dat +echo "fetch cn.mrs" +curl -fsSL --retry 3 --retry-delay 2 "${META_GEOSITE_BASE}/cn.mrs" -o "${TMP_DIR}/cn.mrs" +[ -s "${TMP_DIR}/cn.mrs" ] || { + echo "empty ruleset: ${META_GEOSITE_BASE}/cn.mrs" >&2 + exit 1 +} +mv "${TMP_DIR}/cn.mrs" "${RULESET_DIR}/cn.mrs" + +echo "generated sing-box rule sets in ${RULESET_DIR}" diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 000000000..a36bed277 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,281 @@ +#!/bin/sh + +set -eu + +FEED_BASE_URL="https://down.dllkids.xyz/openwrt-feed/clashoo" +GITHUB_API_URL="https://api.github.com/repos/kenzok8/openwrt-clashoo/releases/latest" +GITHUB_PROXY_PREFIX="${GITHUB_PROXY_PREFIX:-https://ghfast.top/}" +TMP_DIR="/tmp/clashoo-install" + +fetch_text() { + url="$1" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$url" + return $? + fi + if command -v wget >/dev/null 2>&1; then + wget -qO- "$url" + return $? + fi + return 127 +} + +download_file() { + url="$1" + out="$2" + if command -v curl >/dev/null 2>&1; then + curl -fL "$url" -o "$out" + return $? + fi + wget -qO "$out" "$url" +} + +download_url() { + url="$1" + case "$url" in + https://github.com/*) + printf '%s%s\n' "$GITHUB_PROXY_PREFIX" "$url" + ;; + *) + printf '%s\n' "$url" + ;; + esac +} + +detect_manager() { + if command -v opkg >/dev/null 2>&1; then + echo opkg + return + fi + if command -v apk >/dev/null 2>&1; then + echo apk + return + fi + echo "unsupported" +} + +detect_arch() { + pm="$1" + if [ "$pm" = "opkg" ]; then + opkg print-architecture | awk '/^arch / {print $2}' | tail -n 1 + return + fi + # apk --print-arch 只回 CPU family(如 aarch64),丢掉 subtarget 后缀; + # feed/release 产物用完整 target arch(aarch64_cortex-a53),优先读 DISTRIB_ARCH + distrib_arch="$(sed -n "s/^DISTRIB_ARCH=['\"]\([^'\"]*\)['\"].*/\1/p" /etc/openwrt_release 2>/dev/null | head -n 1)" + if [ -n "$distrib_arch" ]; then + printf '%s\n' "$distrib_arch" + else + apk --print-arch + fi +} + +detect_sdk() { + if [ ! -r /etc/openwrt_release ]; then + return 1 + fi + + release="$(sed -n "s/^DISTRIB_RELEASE=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)" + [ -n "$release" ] || return 1 + + sdk="$(printf '%s\n' "$release" | grep -Eo '[0-9]+\.[0-9]+' | head -n 1)" + [ -n "$sdk" ] || return 1 + printf '%s\n' "$sdk" +} + +find_feed_filename() { + package_name="$1" + packages_text="$2" + printf '%s\n' "$packages_text" | awk -v pkg="$package_name" ' + $0 == "Package: " pkg { + in_pkg = 1 + next + } + in_pkg && /^Filename: / { + sub(/^Filename: /, "") + print + exit + } + in_pkg && /^$/ { + in_pkg = 0 + } + ' +} + +find_manifest_value() { + key="$1" + manifest_text="$2" + printf '%s\n' "$manifest_text" | sed -n "s/^${key}=//p" | head -n 1 +} + +append_unique_word() { + value="$1" + list="$2" + [ -n "$value" ] || { + printf '%s\n' "$list" + return + } + case " $list " in + *" $value "*) ;; + *) list="${list}${list:+ }${value}" ;; + esac + printf '%s\n' "$list" +} + +build_sdk_candidates() { + pm="$1" + candidates="" + detected_sdk="$(detect_sdk || true)" + candidates="$(append_unique_word "$detected_sdk" "$candidates")" + + if [ "$pm" = "opkg" ]; then + for sdk in 24.10 23.05 22.03 21.02; do + candidates="$(append_unique_word "$sdk" "$candidates")" + done + else + for sdk in 25.12 24.10; do + candidates="$(append_unique_word "$sdk" "$candidates")" + done + fi + + printf '%s\n' "$candidates" +} + +# Feed 结构:/// —— 与 dllkids setup.sh 对齐,不嵌 feedname 子目录 +feed_base_for() { + printf '%s/%s/%s' "$FEED_BASE_URL" "$1" "$2" +} + +load_manifest_urls() { + sdk="$1" + arch="$2" + base="$(feed_base_for "$sdk" "$arch")" + manifest_url="${base}/manifest-clashoo.txt" + manifest_text="$(fetch_text "$manifest_url" || true)" + [ -n "$manifest_text" ] || return 1 + + core_file="$(find_manifest_value "core" "$manifest_text")" + luci_file="$(find_manifest_value "luci" "$manifest_text")" + i18n_file="$(find_manifest_value "i18n" "$manifest_text")" + + [ -n "$core_file" ] || return 1 + [ -n "$luci_file" ] || return 1 + + CORE_URL="${base}/${core_file}" + LUCI_URL="${base}/${luci_file}" + I18N_URL="" + [ -n "$i18n_file" ] && I18N_URL="${base}/${i18n_file}" + return 0 +} + +load_opkg_feed_urls() { + sdk="$1" + arch="$2" + base="$(feed_base_for "$sdk" "$arch")" + packages_url="${base}/Packages.gz" + packages_text="$(fetch_text "$packages_url" || true)" + if ! printf '%s' "$packages_text" | grep -q '^Package: '; then + return 1 + fi + + core_file="$(find_feed_filename "clashoo" "$packages_text")" + luci_file="$(find_feed_filename "luci-app-clashoo" "$packages_text")" + i18n_file="$(find_feed_filename "luci-i18n-clashoo-zh-cn" "$packages_text")" + + [ -n "$core_file" ] || return 1 + [ -n "$luci_file" ] || return 1 + + CORE_URL="${base}/${core_file}" + LUCI_URL="${base}/${luci_file}" + I18N_URL="" + [ -n "$i18n_file" ] && I18N_URL="${base}/${i18n_file}" + return 0 +} + +load_github_urls() { + arch="$1" + ext="$2" + payload="$(fetch_text "$GITHUB_API_URL" || true)" + [ -n "$payload" ] || return 1 + + urls="$(printf '%s\n' "$payload" | sed -n 's/.*"browser_download_url":[[:space:]]*"\([^"]*\)".*/\1/p')" + [ -n "$urls" ] || return 1 + + if [ "$ext" = "apk" ]; then + CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo-[^-]+.*-r[0-9]+-'"$arch"'\.apk$' | head -n 1)" + LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)" + I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)" + else + CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo_.*_'"$arch"'\.ipk$' | head -n 1)" + LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo_.*_all\.ipk$' | head -n 1)" + I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn_.*_all\.ipk$' | head -n 1)" + fi + + [ -n "$CORE_URL" ] || return 1 + [ -n "$LUCI_URL" ] || return 1 + return 0 +} + +PM="$(detect_manager)" +if [ "$PM" = "unsupported" ]; then + echo "No supported package manager found (opkg/apk)." + exit 1 +fi + +ARCH="$(detect_arch "$PM")" +[ -n "$ARCH" ] || { echo "Cannot detect architecture"; exit 1; } + +SDK_CANDIDATES="$(build_sdk_candidates "$PM")" + +EXT="ipk" +[ "$PM" = "apk" ] && EXT="apk" + +CORE_URL="" +LUCI_URL="" +I18N_URL="" + +if [ -n "$SDK_CANDIDATES" ]; then + for SDK in $SDK_CANDIDATES; do + if load_manifest_urls "$SDK" "$ARCH"; then + echo "Using R2 feed manifest: ${SDK}/${ARCH}" + break + fi + if [ "$PM" = "opkg" ] && load_opkg_feed_urls "$SDK" "$ARCH"; then + echo "Using R2 feed: ${SDK}/${ARCH}" + break + fi + done +fi + +if [ -z "$CORE_URL" ] || [ -z "$LUCI_URL" ]; then + if load_github_urls "$ARCH" "$EXT"; then + echo "Using GitHub latest release" + else + echo "Cannot find required packages for arch: $ARCH" + echo "Detected package manager: $PM" + echo "Tried SDKs: $SDK_CANDIDATES" + exit 1 + fi +fi + +rm -rf "$TMP_DIR" +mkdir -p "$TMP_DIR" + +echo "Downloading packages..." +download_file "$(download_url "$CORE_URL")" "$TMP_DIR/core.${EXT}" +download_file "$(download_url "$LUCI_URL")" "$TMP_DIR/luci.${EXT}" +if [ -n "$I18N_URL" ]; then + download_file "$(download_url "$I18N_URL")" "$TMP_DIR/i18n.${EXT}" +fi + +echo "Installing packages with $PM..." +if [ "$PM" = "opkg" ]; then + # Refresh package lists so deps (yq, coreutils-nohup, kmod-*) resolve on fresh systems + opkg update || echo "Warning: opkg update failed, dependency resolution may break" + opkg install "$TMP_DIR/core.${EXT}" "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} +else + apk update || echo "Warning: apk update failed, dependency resolution may break" + apk add --allow-untrusted "$TMP_DIR/core.${EXT}" "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} +fi + +echo "Install complete." diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100755 index 000000000..8f65c41fa --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +set -eu + +echo "Stopping clashoo service..." +/etc/init.d/clashoo stop >/dev/null 2>&1 || true +/etc/init.d/clashoo disable >/dev/null 2>&1 || true + +core_still_installed() { + if command -v apk >/dev/null 2>&1; then + apk list -I 2>/dev/null | grep -q "^clashoo-[0-9]" + elif command -v opkg >/dev/null 2>&1; then + opkg list-installed 2>/dev/null | grep -q "^clashoo " + else + return 1 + fi +} + +if command -v opkg >/dev/null 2>&1; then + echo "Removing packages with opkg..." + opkg remove luci-i18n-clashoo-zh-cn luci-app-clashoo clashoo >/dev/null 2>&1 || true +elif command -v apk >/dev/null 2>&1; then + echo "Removing packages with apk..." + apk del luci-i18n-clashoo-zh-cn luci-app-clashoo clashoo >/dev/null 2>&1 || true +else + echo "No supported package manager found (opkg/apk), skip package removal." +fi + +# Always safe: purely runtime/temp files, never owned by the package manager. +echo "Cleaning runtime temp files..." +rm -rf /tmp/clashoo /tmp/clashoo-install /tmp/clashoo_* /tmp/luci* 2>/dev/null || true + +# CRITICAL: only delete package-owned files (config / init.d / dirs) when the +# core package was actually removed. If it's still installed - e.g. nikki, which +# depends on the clashoo-provided `mihomo`, blocks `apk del clashoo` - deleting +# these behind the package manager's back corrupts apk's state: the db keeps +# owning files that no longer exist, so a later same-version reinstall only +# writes `.apk-new` and never restores them, leaving the UI broken +# ("kernel not installed" / start stuck). Leave them in place instead. +if core_still_installed; then + echo "WARNING: 'clashoo' is still installed because other packages depend on it" + echo " (e.g. nikki depends on the clashoo-provided 'mihomo')." + echo " Kept /etc/config/clashoo, /etc/init.d/clashoo and /etc/clashoo intact" + echo " to avoid corrupting package state. Remove nikki first for a full" + echo " uninstall, or just reinstall clashoo to refresh it." +else + echo "Cleaning package files..." + rm -rf /etc/clashoo /usr/share/clashoo /usr/share/clashbackup 2>/dev/null || true + rm -f /etc/init.d/clashoo /etc/config/clashoo 2>/dev/null || true +fi + +/etc/init.d/rpcd restart >/dev/null 2>&1 || true +/etc/init.d/uhttpd restart >/dev/null 2>&1 || true +/etc/init.d/dnsmasq restart >/dev/null 2>&1 || true +/etc/init.d/firewall restart >/dev/null 2>&1 || true + +echo "Uninstall & reset complete."