mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 20:22:51 +08:00
🎉 Sync 2026-09-29 23:46:13
This commit is contained in:
1 parent
bb21104c70
commit
d720f47888
76 files changed
+37517
No files matched your search
@@ -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))
|
||||||
@@ -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 ''
|
||||||
Executable
+2507
File diff suppressed because it is too large.
Load diff
+26
@@ -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
|
||||||
@@ -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'
|
||||||
@@ -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 <config.json> ----
|
||||||
|
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");
|
||||||
@@ -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");
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
#!/usr/bin/ucode
|
||||||
|
/* yaml2singbox.uc — convert mihomo/clash YAML proxies to sing-box outbounds
|
||||||
|
* Usage: ucode yaml2singbox.uc <yaml-in> [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 <yaml-in> [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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# clashoo access check
|
||||||
|
# 用法: access_check.sh <url> [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"
|
||||||
+195
@@ -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" <<EOF
|
||||||
|
{
|
||||||
|
"proxy_port": "${proxy_port}",
|
||||||
|
"tcp_mode": "${tcp_mode}",
|
||||||
|
"udp_mode": "${udp_mode}",
|
||||||
|
"updated_at": ${updated_at},
|
||||||
|
"proxy_skipped": ${proxy_skipped},
|
||||||
|
"stale": false,
|
||||||
|
"updating": false,
|
||||||
|
"direct": {
|
||||||
|
"bytedance": $(probe_json "$direct_bytedance"),
|
||||||
|
"youtube": $(probe_json "$direct_youtube")
|
||||||
|
},
|
||||||
|
"proxy": {
|
||||||
|
"bytedance": $(probe_json "$proxy_bytedance"),
|
||||||
|
"youtube": $(probe_json "$proxy_youtube")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mv -f "$TMP_FILE" "$CACHE_FILE"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
RUNDIR="/tmp/clashoo"
|
||||||
|
PID_FILE="${RUNDIR}/access_check_daemon.pid"
|
||||||
|
LOCK_DIR="${RUNDIR}/access_check_daemon.lock"
|
||||||
|
# 周期:默认 30s,ACCESS_CHECK_INTERVAL 环境变量可固定覆盖
|
||||||
|
INTERVAL_FIXED="${ACCESS_CHECK_INTERVAL:-}"
|
||||||
|
|
||||||
|
mkdir -p "$RUNDIR"
|
||||||
|
if ! mkdir "$LOCK_DIR" 2>/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
|
||||||
@@ -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'
|
||||||
Executable
+898
@@ -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 - <<EOF
|
||||||
|
table ip ${LOCAL_OUTPUT_TABLE} {
|
||||||
|
set clashoo_localnetwork {
|
||||||
|
type ipv4_addr
|
||||||
|
flags interval
|
||||||
|
auto-merge
|
||||||
|
elements = { 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 }
|
||||||
|
}
|
||||||
|
${china_set}
|
||||||
|
chain output {
|
||||||
|
type nat hook output priority dstnat; policy accept;
|
||||||
|
${dnsmasq_rule}
|
||||||
|
${fwmark_rule}
|
||||||
|
ip daddr @clashoo_localnetwork return
|
||||||
|
${china_rule}
|
||||||
|
ip daddr ${fake_ip_range} tcp dport != 53 redirect to :${redir_port}
|
||||||
|
meta l4proto tcp redirect to :${redir_port}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_local_output_rule() {
|
||||||
|
nft delete table ip ${LOCAL_OUTPUT_TABLE} >/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 - <<EOF
|
||||||
|
table inet ${QUIC_BLOCK_TABLE} {
|
||||||
|
${china_set}
|
||||||
|
chain prerouting {
|
||||||
|
type filter hook prerouting priority mangle - 1; policy accept;
|
||||||
|
${china_rule:-}
|
||||||
|
${scope} udp dport 443 counter reject with icmpx type port-unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority -10; policy accept;
|
||||||
|
${china_rule:-}
|
||||||
|
${scope} udp dport 443 counter reject with icmpx type port-unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_block_quic_rule() {
|
||||||
|
nft delete table inet ${QUIC_BLOCK_TABLE} >/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" <<EOF
|
||||||
|
$( render_common_returns )
|
||||||
|
$( bool_enabled "$bypass_china_ipv6" && printf '%s\n' 'ip6 daddr @clashoo_china6 return' )
|
||||||
|
$( bool_enabled "$bypass_china" && printf '%s\n' 'ip daddr @clashoo_china return' )
|
||||||
|
$( [ -n "$dscp_elements" ] && printf '%s\n' "ip dscp { ${dscp_elements} } return" )
|
||||||
|
$( [ -n "$dscp_elements" ] && printf '%s\n' "ip6 dscp { ${dscp_elements} } return" )
|
||||||
|
$( [ -n "$fwmark_elements" ] && printf '%s\n' "meta mark { ${fwmark_elements} } return" )
|
||||||
|
$( ! bool_enabled "$(config_ipv4_proxy)" && printf '%s\n' 'meta nfproto ipv4 return' )
|
||||||
|
$( ! bool_enabled "$(config_ipv6_proxy)" && printf '%s\n' 'meta nfproto ipv6 return' )
|
||||||
|
$( grouped_acl_enabled && render_acl_proxy_rules redirect tcp "$tcp_match" "$redir_port" )
|
||||||
|
$( [ "$access_control" = "1" ] && printf '%s\n' 'ip saddr != @clash_proxy_lan return' )
|
||||||
|
$( [ "$access_control" = "2" ] && printf '%s\n' 'ip saddr @clash_reject_lan return' )
|
||||||
|
$( [ "$acl_catchall" -eq 0 ] && { singbox_tun_active && printf '%s ct mark set %s redirect to :%s\n' "$tcp_match" "$SINGBOX_BYPASS_FWMARK" "$redir_port" || printf '%s redirect to :%s\n' "$tcp_match" "$redir_port"; } )
|
||||||
|
EOF
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# UDP rules: tproxy via mangle. TUN uses mangle only for LAN ACL bypass.
|
||||||
|
# Also handle TCP tproxy mode here (both TCP+UDP in mangle).
|
||||||
|
local need_mangle=0 tun_acl=0
|
||||||
|
[ "$tcp_mode" = "tproxy" ] && need_mangle=1
|
||||||
|
[ "$udp_mode" = "tproxy" ] && need_mangle=1
|
||||||
|
if tun_acl_enabled "$access_control"; then
|
||||||
|
need_mangle=1
|
||||||
|
tun_acl=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$need_mangle" -eq 1 ]; then
|
||||||
|
tcp_match="$(render_port_match tcp "$proxy_tcp_dport")"
|
||||||
|
udp_match="$(render_port_match udp "$proxy_udp_dport")"
|
||||||
|
{
|
||||||
|
render_common_returns
|
||||||
|
if bool_enabled "$bypass_china_ipv6"; then
|
||||||
|
printf 'meta nfproto ipv6 ip6 daddr @clashoo_china6 return\n'
|
||||||
|
fi
|
||||||
|
if bool_enabled "$bypass_china"; then
|
||||||
|
printf 'ip daddr @clashoo_china return\n'
|
||||||
|
fi
|
||||||
|
if [ -n "$dscp_elements" ]; then
|
||||||
|
printf 'ip dscp { %s } return\n' "$dscp_elements"
|
||||||
|
printf 'ip6 dscp { %s } return\n' "$dscp_elements"
|
||||||
|
fi
|
||||||
|
if [ -n "$fwmark_elements" ]; then
|
||||||
|
printf 'meta mark { %s } return\n' "$fwmark_elements"
|
||||||
|
fi
|
||||||
|
bool_enabled "$(config_ipv4_proxy)" || printf 'meta nfproto ipv4 return\n'
|
||||||
|
bool_enabled "$(config_ipv6_proxy)" || printf 'meta nfproto ipv6 return\n'
|
||||||
|
if grouped_acl_enabled; then
|
||||||
|
[ "$tcp_mode" = "tproxy" ] && render_acl_proxy_rules tproxy tcp "$tcp_match" "$tproxy_port"
|
||||||
|
[ "$tcp_mode" = "tun" ] && printf 'meta l4proto tcp jump clashoo_acl_tun_tcp\n'
|
||||||
|
[ "$udp_mode" = "tproxy" ] && render_acl_proxy_rules tproxy udp "$udp_match" "$tproxy_port"
|
||||||
|
[ "$udp_mode" = "tun" ] && printf 'meta l4proto udp jump clashoo_acl_tun_udp\n'
|
||||||
|
elif [ "$tun_acl" -eq 1 ]; then
|
||||||
|
[ "$access_control" = "1" ] && printf 'ip saddr != @clash_proxy_lan meta mark set %s return\n' "$ACL_BYPASS_FWMARK"
|
||||||
|
[ "$access_control" = "2" ] && printf 'ip saddr @clash_reject_lan meta mark set %s return\n' "$ACL_BYPASS_FWMARK"
|
||||||
|
else
|
||||||
|
[ "$access_control" = "1" ] && printf 'ip saddr != @clash_proxy_lan return\n'
|
||||||
|
[ "$access_control" = "2" ] && printf 'ip saddr @clash_reject_lan return\n'
|
||||||
|
fi
|
||||||
|
if bool_enabled "$(config_block_quic)"; then
|
||||||
|
printf 'meta l4proto udp udp dport 443 return\n'
|
||||||
|
fi
|
||||||
|
if [ "$tcp_mode" = "tproxy" ] && [ "$acl_catchall" -eq 0 ]; then
|
||||||
|
if singbox_tun_active; then
|
||||||
|
printf '%s ct mark set %s tproxy to :%s meta mark set %s accept\n' "$tcp_match" "$SINGBOX_BYPASS_FWMARK" "$tproxy_port" "$PROXY_FWMARK"
|
||||||
|
else
|
||||||
|
printf '%s tproxy to :%s meta mark set %s accept\n' "$tcp_match" "$tproxy_port" "$PROXY_FWMARK"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$udp_mode" = "tproxy" ] && [ "$acl_catchall" -eq 0 ]; then
|
||||||
|
if singbox_tun_active; then
|
||||||
|
printf '%s ct mark set %s tproxy to :%s meta mark set %s accept\n' "$udp_match" "$SINGBOX_BYPASS_FWMARK" "$tproxy_port" "$PROXY_FWMARK"
|
||||||
|
else
|
||||||
|
printf '%s tproxy to :%s meta mark set %s accept\n' "$udp_match" "$tproxy_port" "$PROXY_FWMARK"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
} > "$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
|
||||||
@@ -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 }
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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 </dev/null &)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
start)
|
||||||
|
run_init_async start
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
run_init_async stop
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
# Fire-and-forget restart used by LuCI RPC to avoid blocking UI apply flow.
|
||||||
|
run_init_async restart
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
update_china_ip)
|
||||||
|
log_line "[china-ip] task started"
|
||||||
|
nohup /usr/share/clashoo/update/update_china_ip.sh >>"$LOG_FILE" 2>&1 </dev/null &
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
update_geoip)
|
||||||
|
log_line "GeoIP 更新任务已触发"
|
||||||
|
(exec 1000>&-; nohup /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1 </dev/null &)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "usage: $0 {start|stop|restart|update_china_ip|update_geoip}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
@@ -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
|
||||||
+347
@@ -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"
|
||||||
+451
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
+98
@@ -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
|
||||||
@@ -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));
|
||||||
@@ -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}"
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
|
)
|
||||||
+31
@@ -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
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
Executable
+63
@@ -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
|
||||||
+642
@@ -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 <clashoo|luci|mihomo|singbox|smart|lgbm|china|geoip> [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
|
||||||
+1233
File diff suppressed because it is too large.
Load diff
+267
@@ -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
|
||||||
+52
@@ -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
|
||||||
@@ -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
|
||||||
+176
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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" <<EOF
|
||||||
|
running=$1
|
||||||
|
last_run=$started_at
|
||||||
|
finished_at=$2
|
||||||
|
updated=$updated
|
||||||
|
unchanged=$unchanged
|
||||||
|
failed=$failed
|
||||||
|
skipped=$skipped
|
||||||
|
message=$message
|
||||||
|
EOF
|
||||||
|
mv "$tmp" "$STATUS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$LOCK_DIR" >/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 ]
|
||||||
@@ -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
|
||||||
+101
@@ -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
|
||||||
+5
@@ -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"
|
||||||
+20
@@ -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
|
||||||
+204
@@ -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 '大陆白名单更新流程完成'
|
||||||
@@ -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)
|
||||||
|
+}
|
||||||
@@ -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))
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
@@ -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&&0<a!=(i==e.ch))return null;r=t.getTokenTypeAt(k(e.line,i+1)),t=o(t,k(e.line,i+(0<a?1:0)),a,r,n);return null==t?null:{from:k(e.line,i),to:t&&t.pos,match:t&&t.ch==c.charAt(0),forward:0<a}}function o(t,e,n,r,i){for(var c=i&&i.maxScanLineLength||1e4,a=i&&i.maxScanLines||1e3,o=[],h=y(i),l=0<n?Math.min(e.line+a,t.lastLine()+1):Math.max(t.firstLine()-1,e.line-a),s=e.line;s!=l;s+=n){var u=t.getLine(s);if(u){var f=0<n?0:u.length-1,m=0<n?u.length:-1;if(!(u.length>c))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)==0<n)o.push(g);else{if(!o.length)return{pos:k(s,f),ch:g};o.pop()}}}}}return s-n!=(0<n?t.lastLine():t.firstLine())&&null}function e(t,e,n){for(var r=t.state.matchBrackets.maxHighlightLineLength||1e3,i=n&&n.highlightNonMatching,c=[],a=t.listSelections(),o=0;o<a.length;o++){var h,l=a[o].empty()&&f(t,a[o].head,n);l&&(l.match||!1!==i)&&t.getLine(l.from.line).length<=r&&(h=l.match?"CodeMirror-matchingbracket":"CodeMirror-nonmatchingbracket",c.push(t.markText(l.from,k(l.from.line,l.from.ch+1),{className:h})),l.to&&t.getLine(l.to.line).length<=r&&c.push(t.markText(l.to,k(l.to.line,l.to.ch+1),{className:h})))}if(c.length){u&&t.state.focused&&t.focus();function s(){t.operation(function(){for(var t=0;t<c.length;t++)c[t].clear()})}if(!e)return s;setTimeout(s,800)}}function i(t){t.operation(function(){t.state.matchBrackets.currentlyHighlighted&&(t.state.matchBrackets.currentlyHighlighted(),t.state.matchBrackets.currentlyHighlighted=null),t.state.matchBrackets.currentlyHighlighted=e(t,!1,t.state.matchBrackets)})}function c(t){t.state.matchBrackets&&t.state.matchBrackets.currentlyHighlighted&&(t.state.matchBrackets.currentlyHighlighted(),t.state.matchBrackets.currentlyHighlighted=null)}r.defineOption("matchBrackets",!1,function(t,e,n){n&&n!=r.Init&&(t.off("cursorActivity",i),t.off("focus",i),t.off("blur",c),c(t)),e&&(t.state.matchBrackets="object"==typeof e?e:{},t.on("cursorActivity",i),t.on("focus",i),t.on("blur",c))}),r.defineExtension("matchBrackets",function(){e(this,!0)}),r.defineExtension("findMatchingBracket",function(t,e,n){return f(this,t,e=!n&&"boolean"!=typeof e?e:n?(n.strict=e,n):e?{strict:!0}:null)}),r.defineExtension("scanForBracket",function(t,e,n,r){return o(this,t,e,n,r)})});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor .CodeMirror-line::selection,.cm-fat-cursor .CodeMirror-line>span::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:contentLine truncated
|
||||||
@@ -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)||2<navigator.maxTouchPoints),$=/Android/.test(e),_=Y||$||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(e),C=Y||/Mac/.test(l),q=/\bCrOS\b/.test(e),a=/win/i.test(l),u=K&&e.match(/Version\/(\d*\.\d*)/),Z=((u=u&&Number(u[1]))&&15<=u&&(x=!(K=!1)),C&&(s||K&&(null==u||u<12.11))),Q=d||w&&9<=v;function J(e){return new RegExp("(^|\\s)"+e+"(?:$|\\s)\\s*")}var ee=function(e,t){var n,r=e.className,t=J(t).exec(r);t&&(n=r.slice(t.index+t[0].length),e.className=r.slice(0,t.index)+(n?t[1]+n:""))};function te(e){for(var t=e.childNodes.length;0<t;--t)e.removeChild(e.firstChild);return e}function y(e,t){return te(e).appendChild(t)}function M(e,t,n,r){var i=document.createElement(e);if(n&&(i.className=n),r&&(i.style.cssText=r),"string"==typeof t)i.appendChild(document.createTextNode(t));else if(t)for(var o=0;o<t.length;++o)i.appendChild(t[o]);return i}function ne(e,t,n,r){e=M(e,t,n,r);return e.setAttribute("role","presentation"),e}function re(e,t){if(3==t.nodeType&&(t=t.parentNode),e.contains)return e.contains(t);do{if((t=11==t.nodeType?t.host:t)==e)return!0}while(t=t.parentNode)}function N(e){var t,n=e.ownerDocument||e;try{t=e.activeElement}catch(e){t=n.body||null}for(;t&&t.shadowRoot&&t.shadowRoot.activeElement;)t=t.shadowRoot.activeElement;return t}function ie(e,t){var n=e.className;J(t).test(n)||(e.className+=(n?" ":"")+t)}function oe(e,t){for(var n=e.split(" "),r=0;r<n.length;r++)n[r]&&!J(n[r]).test(t)&&(t+=" "+n[r]);return t}var le=document.createRange?function(e,t,n,r){var i=document.createRange();return i.setEnd(r||e,n),i.setStart(e,t),i}:function(e,t,n){var r=document.body.createTextRange();try{r.moveToElementText(e.parentNode)}catch(e){return r}return r.collapse(!0),r.moveEnd("character",n),r.moveStart("character",t),r},se=function(e){e.select()};function ae(e){return e.display.wrapper.ownerDocument}function ue(e){return ce(e.display.wrapper)}function ce(e){return e.getRootNode?e.getRootNode():e.ownerDocument}function he(e){return ae(e).defaultView}function de(e){var t=Array.prototype.slice.call(arguments,1);return function(){return e.apply(null,t)}}function fe(e,t,n){for(var r in t=t||{},e)!e.hasOwnProperty(r)||!1===n&&t.hasOwnProperty(r)||(t[r]=e[r]);return t}function S(e,t,n,r,i){null==t&&-1==(t=e.search(/[^\s\u00a0]/))&&(t=e.length);for(var o=r||0,l=i||0;;){var s=e.indexOf("\t",o);if(s<0||t<=s)return l+(t-o);l=(l+=s-o)+(n-l%n),o=s+1}}Y?se=function(e){e.selectionStart=0,e.selectionEnd=e.value.length}:w&&(se=function(e){try{e.select()}catch(e){}});function pe(){this.id=null,this.f=null,this.time=0,this.handler=de(this.onTimeout,this)}function L(e,t){for(var n=0;n<e.length;++n)if(e[n]==t)return n;return-1}pe.prototype.onTimeout=function(e){e.id=0,e.time<=+new Date?e.f():setTimeout(e.handler,e.time-+new Date)};var ge=50,me={toString:function(){return"CodeMirror.Pass"}},ve={scroll:!(pe.prototype.set=function(e,t){this.f=t;t=+new Date+e;(!this.id||t<this.time)&&(clearTimeout(this.id),this.id=setTimeout(this.handler,e),this.time=t)})},ye={origin:"*mouse"},be={origin:"+move"};function we(e,t,n){for(var r=0,i=0;;){var o=e.indexOf("\t",r),l=(o=-1==o?e.length:o)-r;if(o==e.length||t<=i+l)return r+Math.min(l,t-i);if(i+=o-r,r=o+1,t<=(i+=n-i%n))return r}}var xe=[""];function Ce(e){for(;xe.length<=e;)xe.push(z(xe)+" ");return xe[e]}function z(e){return e[e.length-1]}function Se(e,t){for(var n=[],r=0;r<e.length;r++)n[r]=t(e[r],r);return n}function Le(){}function ke(e,t){e=Object.create?Object.create(e):(Le.prototype=e,new Le);return t&&fe(t,e),e}var Te=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;function Me(e){return/\w/.test(e)||""<e&&(e.toUpperCase()!=e.toLowerCase()||Te.test(e))}function Ne(e,t){return t?!!(-1<t.source.indexOf("\\w")&&Me(e))||t.test(e):Me(e)}function Oe(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t])return;return 1}var Ae=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0acLine truncated
|
||||||
@@ -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(rt){"use strict";rt.defineMode("javascript",function(e,l){var t,r,O,P,f=e.indentUnit,N=l.statementIndent,U=l.jsonld,o=l.json||U,W=!1!==l.trackScope,u=l.typescript,B=l.wordCharacters||/[\w$\xa1-\uffff]/,F=(e=n("keyword a"),t=n("keyword b"),r=n("keyword c"),O=n("keyword d"),P=n("operator"),{if:n("if"),while:e,with:e,else:t,do:t,try:t,finally:t,return:O,break:O,continue:O,new:n("new"),delete:r,void:r,throw:r,debugger:n("debugger"),var:n("var"),const:n("var"),let:n("var"),function:n("function"),catch:n("catch"),for:n("for"),switch:n("switch"),case:n("case"),default:n("default"),in:P,typeof:P,instanceof:P,true:e={type:"atom",style:"atom"},false:e,null:e,undefined:e,NaN:e,Infinity:e,this:n("this"),class:n("class"),super:n("atom"),yield:r,export:n("export"),import:n("import"),extends:r,await:r});function n(e){return{type:e,style:"keyword"}}var H,D,G=/[+\-*&%=<>!?|~^@]/,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.varLine truncated
|
||||||
@@ -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<i.inlineList&&!r&&","==t)return e.next(),"meta";if(0<i.inlinePairs&&!r&&","==t)return i.keyCol=0,i.pair=!1,i.pairStart=!1,e.next(),"meta";if(i.pairStart){if(e.match(/^\s*(\||\>)\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<i.inlinePairs&&e.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/))return"number";if(e.match(n))return"keyword"}return!i.pair&&e.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\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")});
|
||||||
@@ -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,
|
||||||
|
'<a href="' + logUrl + '">$1</a>');
|
||||||
|
}
|
||||||
|
|
||||||
|
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, '<').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
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Bytedance">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bdg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#3B82F6"/>
|
||||||
|
<stop offset="100%" stop-color="#2563EB"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="12" y="14" width="7" height="36" rx="3.5" fill="url(#bdg)"/>
|
||||||
|
<rect x="23" y="20" width="7" height="30" rx="3.5" fill="url(#bdg)" opacity="0.92"/>
|
||||||
|
<rect x="34" y="10" width="7" height="40" rx="3.5" fill="url(#bdg)" opacity="0.96"/>
|
||||||
|
<path d="M46 25c3.3 0 6 2.7 6 6s-2.7 6-6 6h-2v-12h2z" fill="#1D4ED8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 624 B |
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,4 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view.clashoo.overview as overview';
|
||||||
|
|
||||||
|
return overview;
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="YouTube">
|
||||||
|
<rect x="10" y="18" width="44" height="28" rx="9" fill="#FF0000"/>
|
||||||
|
<path d="M28 24l14 8-14 8z" fill="#FFFFFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 216 B |
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
zh-cn
|
||||||
@@ -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
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": [ "*" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4649
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||||
Executable
+57
@@ -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}"
|
||||||
Executable
+281
@@ -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 结构:<base>/<sdk>/<arch>/<files> —— 与 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."
|
||||||
Executable
+57
@@ -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."
|
||||||
Reference in new issue
Block a user