mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-05 01:44:24 +08:00
update 2026-11-04 05:27:41
This commit is contained in:
1 parent
0350fcc2e5
commit
1b82056d99
1124 files changed
+3
-574287
No files matched your search
@@ -3,17 +3,17 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=adguardhome
|
||||
PKG_VERSION:=0.107.79
|
||||
PKG_VERSION:=0.107.80
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/AdguardTeam/AdGuardHome/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=5f4fa119f24ea741bb8cecfa74203165fe6b86b904cef05ff429fb227b07d425
|
||||
PKG_HASH:=c67cf289f1d4b05c78994f4d1d169962540814a212244eb191cd599795d90278
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/AdGuardHome-$(PKG_VERSION)
|
||||
|
||||
FRONTEND_DEST:=$(PKG_NAME)-frontend-$(PKG_VERSION).tar.gz
|
||||
FRONTEND_URL:=https://github.com/AdguardTeam/AdGuardHome/releases/download/v$(PKG_VERSION)/
|
||||
FRONTEND_HASH:=30184c157d4aefe5371b575a43c0ab5a7314aef039a820cc421ea67bab994ce8
|
||||
FRONTEND_HASH:=f01f3c7713a8190dd0aaaf18c3f9a0e38235c21d696325bfca9dc681e389d894
|
||||
|
||||
PKG_LICENSE:=GPL-3.0-only
|
||||
PKG_LICENSE_FILES:=LICENSE.txt
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=clashoo
|
||||
PKG_UPSTREAM_VERSION:=1.19.33
|
||||
PKG_UPSTREAM_TAG:=v$(PKG_UPSTREAM_VERSION)
|
||||
PKG_SHORT_SHA:=3f27ef3
|
||||
|
||||
PKG_VERSION:=2026.10.25.$(PKG_UPSTREAM_VERSION)
|
||||
PKG_RELEASE:=5
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA).tar.gz
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA)
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR)
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
|
||||
PKG_SOURCE_VERSION:=3f27ef39f8c96bbfbc8b0b9bb10f5c9ef60149d8
|
||||
PKG_HASH:=1ef7a4087f3805bd2624a940e2e43a0bc23719a25465c9f503aa6c11572f1321
|
||||
PKG_BUILD_VERSION:=$(PKG_UPSTREAM_TAG)
|
||||
|
||||
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/libexec/clashoo
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
mv -f $(1)/usr/bin/mihomo $(1)/usr/libexec/clashoo/mihomo-stable
|
||||
ln -sf /usr/libexec/clashoo/mihomo-stable $(1)/usr/bin/mihomo
|
||||
ln -sf /usr/libexec/clashoo/mihomo-stable $(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/preinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
save_core() {
|
||||
src="$$1"
|
||||
dst="$$2"
|
||||
[ -f "$$src" ] && [ ! -L "$$src" ] || return 0
|
||||
if [ -e "$$dst" ] || [ -L "$$dst" ]; then return 0; fi
|
||||
mkdir -p "$$(dirname "$$dst")" || return 1
|
||||
tmp="$$dst.tmp.$$$$"
|
||||
cp "$$src" "$$tmp" && chmod 0755 "$$tmp" && cmp -s "$$src" "$$tmp" || {
|
||||
rm -f "$$tmp"
|
||||
return 1
|
||||
}
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
old_hash="$$(sha256sum "$$src" | awk '{print $$1}')"
|
||||
new_hash="$$(sha256sum "$$tmp" | awk '{print $$1}')"
|
||||
[ "$$old_hash" = "$$new_hash" ] || {
|
||||
rm -f "$$tmp"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
mv -f "$$tmp" "$$dst"
|
||||
}
|
||||
save_core /usr/bin/mihomo /usr/share/clashoo/bin/mihomo-alpha || exit 1
|
||||
save_core /usr/bin/mihomo-stable /usr/share/clashoo/bin/mihomo-stable || exit 1
|
||||
if [ -x /usr/bin/clash-meta ] && [ ! -L /usr/bin/clash-meta ] && [ ! -e /usr/share/clashoo/bin/mihomo-stable ]; then
|
||||
if ! /usr/bin/clash-meta -v 2>/dev/null | grep -Eiq '(alpha|beta|rc|pre|smart)'; then
|
||||
save_core /usr/bin/clash-meta /usr/share/clashoo/bin/mihomo-stable || exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/clashoo/postinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
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
|
||||
# 旧方案把 stable 内核放在 /usr/bin/mihomo-stable,现方案改用
|
||||
# /usr/libexec/clashoo/mihomo-stable(随包)+ /usr/share/clashoo/bin/(自定义下载)。
|
||||
# preinst 已把它抄到 /usr/share/clashoo/bin/mihomo-stable,确认内容相同后才删
|
||||
# 旧位置;目标已有其他自定义内核时保留旧文件,避免误删。
|
||||
if [ -f /usr/share/clashoo/bin/mihomo-stable ] && [ -f /usr/bin/mihomo-stable ] && [ ! -L /usr/bin/mihomo-stable ] && cmp -s /usr/bin/mihomo-stable /usr/share/clashoo/bin/mihomo-stable; then
|
||||
rm -f /usr/bin/mihomo-stable
|
||||
fi
|
||||
/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))
|
||||
@@ -1,156 +0,0 @@
|
||||
|
||||
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 '2'
|
||||
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 '2'
|
||||
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 ''
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,26 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,181 +0,0 @@
|
||||
#!/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'
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/sh
|
||||
# clashoo core log size guard
|
||||
# 用法: core_log_guard.sh [logfile]
|
||||
#
|
||||
# core.log 落在 /var/log(= /tmp 下的 tmpfs),没有大小上限时一次 accept
|
||||
# 失败风暴就能把它写到几百 MB,直接把 /tmp 写满(dnsmasq、配置生成、
|
||||
# cron 全部失效)。这里在超过上限时就地裁剪,只保留尾部内容。
|
||||
# 内核进程持有该文件的 O_APPEND fd,所以必须原地重写,不能替换 inode。
|
||||
|
||||
set -u
|
||||
|
||||
LOG="${1:-/var/log/clashoo/core.log}"
|
||||
MAX_KB="${CLASHOO_CORE_LOG_MAX_KB:-16384}"
|
||||
KEEP_KB="${CLASHOO_CORE_LOG_KEEP_KB:-1024}"
|
||||
|
||||
[ -f "$LOG" ] || exit 0
|
||||
size="$(wc -c <"$LOG" 2>/dev/null)"
|
||||
case "${size:-}" in ''|*[!0-9]*) exit 0 ;; esac
|
||||
[ "$size" -gt $((MAX_KB * 1024)) ] || exit 0
|
||||
|
||||
tmp="${LOG}.trim.$$"
|
||||
if tail -c $((KEEP_KB * 1024)) "$LOG" >"$tmp" 2>/dev/null && cat "$tmp" >"$LOG" 2>/dev/null; then
|
||||
rm -f "$tmp"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
: >"$LOG" 2>/dev/null || true
|
||||
fi
|
||||
exit 0
|
||||
@@ -1,385 +0,0 @@
|
||||
#!/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");
|
||||
@@ -1,939 +0,0 @@
|
||||
#!/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");
|
||||
@@ -1,138 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,468 +0,0 @@
|
||||
#!/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);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,89 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,195 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/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
|
||||
# 每轮顺手卡一下 core.log 大小(tmpfs 很小,跑飞会写满 /tmp)
|
||||
sh /usr/share/clashoo/lib/core_log_guard.sh >/dev/null 2>&1 || true
|
||||
if [ -n "$INTERVAL_FIXED" ]; then
|
||||
sleep "$INTERVAL_FIXED"
|
||||
else
|
||||
sleep 30
|
||||
fi
|
||||
done
|
||||
@@ -1,240 +0,0 @@
|
||||
#!/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'
|
||||
@@ -1,898 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,43 +0,0 @@
|
||||
# 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
@@ -1,44 +0,0 @@
|
||||
#!/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.
@@ -1,21 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,353 +0,0 @@
|
||||
#!/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 host ip http_code rc
|
||||
url="$1"
|
||||
tmp="$2"
|
||||
hdr="$3"
|
||||
err="$4"
|
||||
ua="$5"
|
||||
host="$6"
|
||||
ip="$7"
|
||||
|
||||
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
|
||||
|
||||
set --
|
||||
if [ -n "$host" ] && [ -n "$ip" ]; then
|
||||
set -- --resolve "$host:443:$ip" --resolve "$host:80:$ip"
|
||||
fi
|
||||
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" \
|
||||
"$@" \
|
||||
-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
|
||||
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")"
|
||||
case "$host" in
|
||||
''|[-.]*|*[!A-Za-z0-9.-]*) host='' ;;
|
||||
esac
|
||||
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})"
|
||||
http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "$host" "$ip")"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] && [ "$http_code" = "200" ] && break
|
||||
done
|
||||
fi
|
||||
fi
|
||||
else
|
||||
wget -q --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"
|
||||
@@ -1,389 +0,0 @@
|
||||
#!/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" tmp="${1}.$$"
|
||||
# The config is rebuilt from the selected subscription on every run.
|
||||
# A saved nameserver from an earlier run must never replace its DNS.
|
||||
rm -f "$state" 2>/dev/null
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
[ -f "$cfg" ] || return 0
|
||||
command -v yq >/dev/null 2>&1 || return 0
|
||||
|
||||
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
|
||||
|
||||
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"
|
||||
rm -f "$selected" "$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"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
. 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
|
||||
@@ -1,118 +0,0 @@
|
||||
#!/usr/bin/ucode
|
||||
'use strict';
|
||||
|
||||
import { readfile } from 'fs';
|
||||
|
||||
// Mirror node-domain DNS policies from the subscription into
|
||||
// proxy-server-nameserver-policy (issue #73).
|
||||
//
|
||||
// Airports publish their own resolver for node domains, e.g.
|
||||
// nameserver-policy:
|
||||
// "+.chylink.xyz": [tls://112.74.48.57:10853]
|
||||
// which answers with domestic relay IPs. mihomo resolves *proxy server* domains
|
||||
// through proxy-server-nameserver(-policy) and ignores nameserver-policy there,
|
||||
// so that resolver is never used and node domains fall back to public DNS -
|
||||
// which may return overseas CDN IPs that some lines cannot reach at all, leaving
|
||||
// every node (AnyTLS is TCP-only) timing out.
|
||||
//
|
||||
// Input : CLASHOO_NODE_DNS_SOURCE_FILE = path to {"policies": {...}, "servers": [...]}
|
||||
// (CLASHOO_NODE_DNS_SOURCE with the JSON itself is still accepted)
|
||||
// Output: JSON map with only the policy entries matching a node server domain
|
||||
// (nothing is printed when there is no match)
|
||||
|
||||
let source = '';
|
||||
let source_file = getenv('CLASHOO_NODE_DNS_SOURCE_FILE') || '';
|
||||
|
||||
if (length(source_file)) {
|
||||
try {
|
||||
source = readfile(source_file) || '';
|
||||
}
|
||||
catch (e) {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
source = getenv('CLASHOO_NODE_DNS_SOURCE') || '';
|
||||
}
|
||||
|
||||
if (!length(source))
|
||||
exit(0);
|
||||
|
||||
let parsed;
|
||||
|
||||
try {
|
||||
parsed = json(source);
|
||||
}
|
||||
catch (e) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (type(parsed) != 'object')
|
||||
exit(0);
|
||||
|
||||
let policies = parsed.policies;
|
||||
let servers = parsed.servers;
|
||||
|
||||
if (type(policies) != 'object' || type(servers) != 'array')
|
||||
exit(0);
|
||||
|
||||
let domains = [];
|
||||
let seen = {};
|
||||
|
||||
for (let server in servers) {
|
||||
if (type(server) != 'string' || !length(server))
|
||||
continue;
|
||||
server = lc(server);
|
||||
if (seen[server])
|
||||
continue;
|
||||
seen[server] = true;
|
||||
push(domains, server);
|
||||
}
|
||||
|
||||
if (!length(domains))
|
||||
exit(0);
|
||||
|
||||
// suffix match on a label boundary: node.example.com matches +.example.com
|
||||
function domain_matches(key, domain) {
|
||||
let key_len = length(key);
|
||||
let domain_len = length(domain);
|
||||
|
||||
if (!key_len || domain_len < key_len)
|
||||
return false;
|
||||
if (domain == key)
|
||||
return true;
|
||||
if (substr(domain, domain_len - key_len) != key)
|
||||
return false;
|
||||
return substr(domain, domain_len - key_len - 1, 1) == '.';
|
||||
}
|
||||
|
||||
let result = {};
|
||||
|
||||
for (let key, value in policies) {
|
||||
if (type(key) != 'string' || !length(key))
|
||||
continue;
|
||||
|
||||
let lowered = lc(key);
|
||||
let is_pattern = match(lowered, /^[+*]?\./) != null;
|
||||
let domain = replace(lowered, /^\+\./, '');
|
||||
|
||||
domain = replace(domain, /^\*\./, '');
|
||||
domain = replace(domain, /^\./, '');
|
||||
|
||||
let matched = false;
|
||||
|
||||
for (let candidate in domains) {
|
||||
if (is_pattern ? domain_matches(domain, candidate) : (candidate == lowered)) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched)
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
if (!length(keys(result)))
|
||||
exit(0);
|
||||
|
||||
print(sprintf('%J', result));
|
||||
@@ -1,98 +0,0 @@
|
||||
#!/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="${CLASHOO_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
|
||||
@@ -1,339 +0,0 @@
|
||||
#!/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) {
|
||||
f = trim(f || '');
|
||||
if (!length(f)) return;
|
||||
if (filter_mode == 'rule') {
|
||||
if (match(f, /,(fake-ip|real-ip)$/)) { push(cfg['dns']['fake-ip-filter'], f); return; }
|
||||
if (index(f, ',') >= 0) { push(cfg['dns']['fake-ip-filter'], f + ',real-ip'); return; }
|
||||
if (f == 'geosite:cn') { f = 'RULE-SET,cn_domain,real-ip'; need_cn_rs = true; }
|
||||
else if (match(f, /^geosite:/)) { f = 'GEOSITE,' + substr(f, 8) + ',real-ip'; }
|
||||
else if (match(f, /^rule-set:/)) {
|
||||
let rs = substr(f, 9);
|
||||
if (rs == 'cn_domain') need_cn_rs = true;
|
||||
f = 'RULE-SET,' + rs + ',real-ip';
|
||||
}
|
||||
else if (match(f, /^\+\./)) { f = 'DOMAIN-SUFFIX,' + substr(f, 2) + ',real-ip'; }
|
||||
else if (match(f, /\*/)) { f = 'DOMAIN-WILDCARD,' + f + ',real-ip'; }
|
||||
else { f = 'DOMAIN,' + f + ',real-ip'; }
|
||||
} else {
|
||||
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', 'proxy-server-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 = {};
|
||||
let proxy_policies = {};
|
||||
uci.foreach('clashoo', 'dns_policy', function(sec) {
|
||||
if (b(sec.enabled) == false) return;
|
||||
let role = s(sec.policy_type, 'nameserver-policy');
|
||||
if (role != 'nameserver-policy' && role != 'proxy-server-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; }
|
||||
if (role == 'nameserver-policy') policies[matcher] = servers;
|
||||
else proxy_policies[matcher] = servers;
|
||||
});
|
||||
if (length(keys(policies)) && (!dns_present['nameserver-policy'] || dns_force['nameserver-policy']))
|
||||
cfg['dns']['nameserver-policy'] = policies;
|
||||
if (length(keys(proxy_policies)) && (!dns_present['proxy-server-nameserver-policy'] || dns_force['proxy-server-nameserver-policy']))
|
||||
cfg['dns']['proxy-server-nameserver-policy'] = proxy_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));
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/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}"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# 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"]
|
||||
)
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/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() {
|
||||
if [ "$#" != 1 ]; then
|
||||
return 1
|
||||
fi
|
||||
printf '%s' "$1" | hexdump -v -e '1/1 "%u "' | awk '
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
b = $i + 0
|
||||
if ((b >= 48 && b <= 57) || (b >= 65 && b <= 90) ||
|
||||
(b >= 97 && b <= 122) || b == 45 || b == 46 ||
|
||||
b == 95 || b == 126)
|
||||
printf "%c", b
|
||||
else
|
||||
printf "%%%02X", b
|
||||
}
|
||||
}'
|
||||
}
|
||||
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
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1,63 +0,0 @@
|
||||
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
|
||||
@@ -1,649 +0,0 @@
|
||||
#!/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" "$url"; return $?; }
|
||||
wget -qO- --timeout="$REQUEST_TIMEOUT" "$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" "$url"
|
||||
return $?
|
||||
fi
|
||||
wget -qO "$out" --timeout="$REQUEST_TIMEOUT" "$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
|
||||
}
|
||||
|
||||
# `~` 是版本里的预发布分隔符,而 release 资产文件名会把它换成 `.`
|
||||
# (clashoo_2026.10.25.6780eee-r3_x86_64.ipk 对应已安装的 2026.10.25~6780eee-r3)。
|
||||
# 只比分隔符就会误判成「校验失败」,所以比较前统一归一化,解析时保持文件名原样。
|
||||
version_equal() {
|
||||
[ -n "$1" ] && [ "$(printf '%s' "$1" | tr '~' '.')" = "$(printf '%s' "$2" | tr '~' '.')" ]
|
||||
}
|
||||
|
||||
package_version_from_url() {
|
||||
file="${1##*/}"
|
||||
case "$file" in
|
||||
clashoo_*_"$ARCH".ipk)
|
||||
v="${file#clashoo_}"
|
||||
printf '%s\n' "${v%_${ARCH}.ipk}"
|
||||
;;
|
||||
# 架构名本身可能带下划线(aarch64_generic / arm_cortex-a7_neon-vfpv4),
|
||||
# 只截到版本号为止,否则尾巴会混进版本串。
|
||||
clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^clashoo_\([^_]*\)_.*\.ipk$/\1/p' ;;
|
||||
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)
|
||||
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
|
||||
;;
|
||||
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 && version_equal "$(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 && version_equal "$(installed_package_version luci-app-clashoo)" "$luci_ver" \
|
||||
&& { [ -z "$I18N_URL" ] || { package_installed luci-i18n-clashoo-zh-cn \
|
||||
&& version_equal "$(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" ] && version_equal "$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" ] && version_equal "$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 && version_equal "$(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 && version_equal "$(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
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,282 +0,0 @@
|
||||
#!/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)"
|
||||
require_geosite=0
|
||||
[ "$1" = "--require-geosite" ] && require_geosite=1
|
||||
|
||||
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="$(uci -q get clashoo.config.use_config 2>/dev/null)"
|
||||
if [ -s "$_cfg" ] && grep -Eq "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,|^[[:space:]]*geosite:[[:space:]]*|^[[:space:]]*'geosite:|^[[:space:]]*-[[:space:]]*[\"']?geosite:" "$_cfg"; then
|
||||
return 0
|
||||
fi
|
||||
_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 --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 1
|
||||
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 1
|
||||
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 1
|
||||
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 [ "$require_geosite" -eq 1 ] || config_needs_geosite; then
|
||||
if ! download_geosite "$geosite_url"; then
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
|
||||
if [ "$geoip_source" = "1" ] && { [ "$require_geosite" -eq 1 ] || config_needs_geosite; }; then
|
||||
geosite_url="${cfg_geosite_url:-$DEFAULT_GEOSITE_URL}"
|
||||
if ! download_geosite "$geosite_url"; then
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/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 -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
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/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"
|
||||
sh /usr/share/clashoo/lib/core_log_guard.sh >/dev/null 2>&1
|
||||
stamp log_cleanup.status
|
||||
fi
|
||||
@@ -1,204 +0,0 @@
|
||||
#!/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 --user-agent="Clash/OpenWRT" "$url" -O "$ZIP_FILE"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
validate_zip_entries() {
|
||||
entries="$TMP_ROOT/entries.txt"
|
||||
if command -v unzip >/dev/null 2>&1; then
|
||||
unzip -Z1 "$ZIP_FILE" >"$entries" 2>/dev/null || return 1
|
||||
elif command -v bsdtar >/dev/null 2>&1; then
|
||||
bsdtar -tf "$ZIP_FILE" >"$entries" 2>/dev/null || return 1
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
[ -s "$entries" ] || return 1
|
||||
awk '
|
||||
/^\// || /^[A-Za-z]:/ || /(^|\/)\.\.(\/|$)/ || /\\/ || /\r/ { bad = 1 }
|
||||
END { exit bad }
|
||||
' "$entries"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if ! validate_zip_entries; then
|
||||
set_state "error" "压缩包路径无效"
|
||||
log_msg "Dashboard panel archive has unsafe entries" "面板压缩包含不安全路径"
|
||||
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
|
||||
|
||||
if find "$UNPACK_DIR" ! -type d ! -type f | grep -q .; then
|
||||
set_state "error" "压缩包文件类型无效"
|
||||
log_msg "Dashboard panel archive has unsafe file types" "面板压缩包含不安全文件类型"
|
||||
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
|
||||
@@ -1,80 +0,0 @@
|
||||
# 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"
|
||||
}
|
||||
@@ -1,385 +0,0 @@
|
||||
#!/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 --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() {
|
||||
[ "$("$SERVICE_CMD" status 2>/dev/null)" = "running" ]
|
||||
}
|
||||
|
||||
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 ]
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,101 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,237 +0,0 @@
|
||||
#!/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_ROOT="$(mktemp -d /tmp/clashoo_china_ip.XXXXXX)"
|
||||
TMP_V4="$TMP_ROOT/china_ip.txt"
|
||||
TMP_V6="$TMP_ROOT/china_ipv6.txt"
|
||||
OUT_V4="$TMP_ROOT/geoip_cn.nft"
|
||||
OUT_V6="$TMP_ROOT/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 --connect-timeout 10 --max-time 120 "$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 --connect-timeout 10 --max-time 120 --resolve "ispip.clang.cn:443:${ip}" "$url" -o "$output"; then
|
||||
log "DNS 异常,已使用 --resolve(${ip}) 回源下载"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_ROOT"
|
||||
}
|
||||
|
||||
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" '
|
||||
function valid_cidr(value, parts, octets, i, prefix) {
|
||||
if (split(value, parts, "/") != 2 || parts[2] !~ /^[0-9]+$/)
|
||||
return 0
|
||||
prefix = parts[2] + 0
|
||||
if (set_type == "ipv4_addr") {
|
||||
if (prefix > 32 || split(parts[1], octets, ".") != 4)
|
||||
return 0
|
||||
for (i = 1; i <= 4; i++)
|
||||
if (octets[i] !~ /^[0-9]+$/ || length(octets[i]) > 3 || octets[i] + 0 > 255)
|
||||
return 0
|
||||
return 1
|
||||
}
|
||||
return set_type == "ipv6_addr" && prefix <= 128 &&
|
||||
parts[1] ~ /^[0-9A-Fa-f:]+$/ && index(parts[1], ":") > 0
|
||||
}
|
||||
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 (!valid_cidr($0)) {
|
||||
if (!invalid)
|
||||
printf "invalid CIDR at line %d\n", NR > "/dev/stderr"
|
||||
invalid = 1
|
||||
next
|
||||
}
|
||||
if (!first)
|
||||
print ","
|
||||
printf "\t\t%s", $0
|
||||
first = 0
|
||||
}
|
||||
END {
|
||||
if (!first)
|
||||
print ""
|
||||
print "\t}"
|
||||
print "}"
|
||||
if (invalid || first)
|
||||
exit 1
|
||||
}
|
||||
' "$source_file" > "$output_file" || return 1
|
||||
# nft catches malformed IPv6 forms and invalid network prefixes before replacement.
|
||||
{ printf 'table inet clashoo_validate {\n'; cat "$output_file"; printf '\n}\n'; } |
|
||||
nft -c -f - >/dev/null 2>&1
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if ! render_nft_set "$TMP_V4" "$OUT_V4" clashoo_china ipv4_addr; then
|
||||
log '大陆 IPv4 白名单包含无效 CIDR,保留原文件'
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
if ! render_nft_set "$TMP_V6" "$OUT_V6" clashoo_china6 ipv6_addr; then
|
||||
log '大陆 IPv6 白名单包含无效 CIDR,保留原文件'
|
||||
exit 1
|
||||
fi
|
||||
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 '大陆白名单更新流程完成'
|
||||
@@ -1,91 +0,0 @@
|
||||
--- 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)
|
||||
+}
|
||||
@@ -1,78 +0,0 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=filebrowser-q
|
||||
PKG_VERSION:=1.5.6-stable
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/filebrowser-$(PKG_VERSION)
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=kiddin9
|
||||
|
||||
PKG_BUILD_DEPENDS:=golang/host node/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
GO_PKG:=github.com/gtsteffaniak/filebrowser/backend
|
||||
GO_PKG_BUILD_DIR:=$(PKG_BUILD_DIR)/backend
|
||||
GO_PKG_LDFLAGS_X:= \
|
||||
$(GO_PKG)/internal/version.Version=v$(PKG_VERSION) \
|
||||
$(GO_PKG)/internal/version.CommitSHA=$(PKG_VERSION)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
define Package/filebrowser-q
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
SUBMENU:=Filesystem
|
||||
TITLE:=FileBrowser Quantum - Modern Web File Manager
|
||||
URL:=https://github.com/gtsteffaniak/filebrowser
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS)
|
||||
endef
|
||||
|
||||
define Package/filebrowser-q/description
|
||||
FileBrowser Quantum provides a modern, responsive web-based file management
|
||||
interface with multi-source, real-time search, and enhanced preview features.
|
||||
endef
|
||||
|
||||
define Package/filebrowser-q/conffiles
|
||||
/etc/filebrowser-q/
|
||||
/etc/config/filebrowser-q
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
$(call Build/Prepare/Default)
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
( \
|
||||
pushd $(PKG_BUILD_DIR)/frontend && \
|
||||
npm install && \
|
||||
npm run build ; \
|
||||
)
|
||||
( \
|
||||
cd $(PKG_BUILD_DIR)/backend && \
|
||||
$(GO_PKG_VARS) \
|
||||
go build \
|
||||
-trimpath \
|
||||
-ldflags="-w -s" \
|
||||
-o $(PKG_BUILD_DIR)/filebrowser . ; \
|
||||
)
|
||||
endef
|
||||
|
||||
define Package/filebrowser-q/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/filebrowser $(1)/usr/bin/filebrowser-q
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser-q
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser-q
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,filebrowser-q))
|
||||
@@ -1,5 +0,0 @@
|
||||
config filebrowser 'config'
|
||||
option enabled '0'
|
||||
option listen_port '8787'
|
||||
option root_path '/'
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
|
||||
CONF="filebrowser-q"
|
||||
PROG="/usr/bin/filebrowser-q"
|
||||
CONF_PATH="/etc/filebrowser-q/config.yaml"
|
||||
DB_PATH="/etc/filebrowser-q/database.db"
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
config_get_bool enabled "config" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
|
||||
local listen_port root_path root_name
|
||||
config_get listen_port "config" "listen_port" "8787"
|
||||
config_get root_path "config" "root_path" "/"
|
||||
|
||||
root_name=""
|
||||
[ "$root_path" = "/" ] && root_name="root"
|
||||
|
||||
if [ ! -f "$CONF_PATH" ]; then
|
||||
mkdir -p "$(dirname "$CONF_PATH")"
|
||||
cat <<EOF > "$CONF_PATH"
|
||||
server:
|
||||
port: $listen_port
|
||||
database: "$DB_PATH"
|
||||
sources:
|
||||
- path: "$root_path"
|
||||
name: "$root_name"
|
||||
config:
|
||||
defaultEnabled: true
|
||||
auth:
|
||||
adminUsername: "admin"
|
||||
adminPassword: "admin"
|
||||
userDefaults:
|
||||
ui:
|
||||
locale: "zhCN"
|
||||
EOF
|
||||
else
|
||||
grep -q " name:" "$CONF_PATH" || sed -i "s,.*- path:.*,&\n name: \"\"," "$CONF_PATH"
|
||||
sed -e "s/ port:.*/ port: $listen_port/" \
|
||||
-e "s, - path:.*, - path: \"$root_path\"," \
|
||||
-e "s/ name: \".*\"/ name: \"$root_name\"/" \
|
||||
-i "$CONF_PATH"
|
||||
fi
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG"
|
||||
procd_append_param command -c "$CONF_PATH"
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
procd_close_instance
|
||||
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2015-2016 OpenWrt.org
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v3.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=geo2txt
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/sbwml/geo2txt/tar.gz/$(PKG_VERSION)?
|
||||
PKG_HASH:=b777d9c67b36cd8ce68a0555200fae85751435c888364c921a361062ffee4409
|
||||
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
PKG_LICENSE_FILE:=LICENSE
|
||||
PKG_MAINTAINER:=sbwml <admin@cooluc.com>
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/geo2txt
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
TITLE:=V2ray geosite & geoip unpacking CLI tool
|
||||
URL:=https://github.com/sbwml/geo2txt
|
||||
endef
|
||||
|
||||
define Package/quickfile/description
|
||||
A lightweight pure C utility to unpack v2ray geosite.dat and geoip.dat.
|
||||
endef
|
||||
|
||||
define Package/geo2txt/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/geo2txt $(1)/usr/bin/geo2txt
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,geo2txt))
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#
|
||||
# Copyright (C) 2026 kenzok8
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=honk
|
||||
PKG_VERSION:=2026.11.03
|
||||
PKG_RELEASE:=1
|
||||
|
||||
# Pinned honk-core revision (upstream daeuniverse/honk main), cross-compiled by
|
||||
# ci/build-core.sh. Every release run publishes its core archives under an
|
||||
# immutable source-<run_id> tag; the tag and SHA-256 values below are the exact
|
||||
# assets behind CORE_COMMIT, so a plain feed build downloads and verifies them
|
||||
# without any generated file. A staged run may still override them through
|
||||
# honk/generated-stage.mk, which is generated and never committed.
|
||||
CORE_COMMIT:=77176ff8e8cc2b75385a7030dc376536c382574a
|
||||
|
||||
# Immutable source asset tag holding this pin's core archives.
|
||||
HONK_SOURCE_TAG?=source-41594075839
|
||||
HONK_SOURCE_URL?=https://github.com/kenzok8/openwrt-honk/releases/download/$(HONK_SOURCE_TAG)
|
||||
|
||||
-include $(CURDIR)/generated-stage.mk
|
||||
|
||||
HONK_ASSET_X86_64?=honk-core-$(CORE_COMMIT)-x86_64.tar.gz
|
||||
HONK_ASSET_AARCH64?=honk-core-$(CORE_COMMIT)-aarch64.tar.gz
|
||||
HONK_ASSET_ARMV7?=honk-core-$(CORE_COMMIT)-armv7.tar.gz
|
||||
HONK_HASH_X86_64?=ad4801757f8b47505aa99cbdc47e4d606b8afe1a6269547d8073f68c20904a0e
|
||||
HONK_HASH_AARCH64?=91a9a9929ec32d24de3c105f2a51d83a4975913a68e1446e57c2b1faf63adce6
|
||||
HONK_HASH_ARMV7?=a5b288c677698a44cc45084baf0bb67b1fb2b9ca624744a08d7a469093fc9870
|
||||
|
||||
# ARCH_PACKAGES is the package arch (e.g. arm_cortex-a7_neon-vfpv4);
|
||||
# ARCH is the CPU arch (x86_64/aarch64/arm) and coincides for 64-bit.
|
||||
ifeq ($(ARCH_PACKAGES),x86_64)
|
||||
HONK_ASSET:=$(HONK_ASSET_X86_64)
|
||||
HONK_HASH:=$(HONK_HASH_X86_64)
|
||||
else ifeq ($(filter aarch64%,$(ARCH_PACKAGES)),$(ARCH_PACKAGES))
|
||||
HONK_ASSET:=$(HONK_ASSET_AARCH64)
|
||||
HONK_HASH:=$(HONK_HASH_AARCH64)
|
||||
else ifeq ($(filter arm_%,$(ARCH_PACKAGES)),$(ARCH_PACKAGES))
|
||||
HONK_ASSET:=$(HONK_ASSET_ARMV7)
|
||||
HONK_HASH:=$(HONK_HASH_ARMV7)
|
||||
endif
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
|
||||
PKG_SOURCE:=$(HONK_ASSET)
|
||||
PKG_SOURCE_URL?=$(HONK_SOURCE_URL)/
|
||||
PKG_HASH:=$(HONK_HASH)
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/honk
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=Honk transparent proxy core
|
||||
URL:=https://github.com/daeuniverse/honk
|
||||
# 不直接依赖 libubox:包名带 ABI 日期戳,会锁死固件;脚本只用 jshn 提供的 jshn.sh。
|
||||
DEPENDS:=@(x86_64||aarch64||arm) +ca-bundle +ip-full +ubus +kmod-sched-core +kmod-sched-bpf +kmod-veth +kmod-nft-queue +jsonfilter +jshn +sqlite3-cli +usign +coreutils-stat +v2ray-geoip +v2ray-geosite +@KERNEL_DEBUG_INFO_BTF
|
||||
endef
|
||||
|
||||
define Package/honk/description
|
||||
Honk is a transparent proxy core for OpenWrt (x86_64, aarch64, armv7).
|
||||
endef
|
||||
|
||||
define Package/honk/conffiles
|
||||
/etc/config/honk
|
||||
/etc/honk/config.dae
|
||||
/etc/honk/system.dae
|
||||
/etc/honk/manual-config.dae
|
||||
/etc/honk/managed-config.dae
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
$(TAR) -xzf $(DL_DIR)/$(PKG_SOURCE) -C $(PKG_BUILD_DIR) --strip-components=1
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/honk/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/honk-core $(1)/usr/bin/honk-core
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/licenses/honk
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/LICENSE $(1)/usr/share/licenses/honk/LICENSE
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) $(CURDIR)/files/honk.config $(1)/etc/config/honk
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/honk
|
||||
$(INSTALL_CONF) $(CURDIR)/files/config.dae $(1)/etc/honk/config.dae
|
||||
$(INSTALL_CONF) $(CURDIR)/files/system.dae $(1)/etc/honk/system.dae
|
||||
chmod 0600 $(1)/etc/honk/system.dae
|
||||
$(INSTALL_DIR) $(1)/etc/honk/state
|
||||
chmod 0700 $(1)/etc/honk/state
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/honk.init $(1)/etc/init.d/honk
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) $(CURDIR)/files/90-honk-boot $(1)/etc/uci-defaults/90-honk
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/honk
|
||||
$(INSTALL_DATA) $(CURDIR)/files/config.dae $(1)/usr/share/honk/default-config.dae
|
||||
# Provenance is only generated by a source build; a plain feed build has
|
||||
# no such file and must still install cleanly.
|
||||
if [ -f $(CURDIR)/generated-provenance.json ]; then \
|
||||
$(INSTALL_DATA) $(CURDIR)/generated-provenance.json $(1)/usr/share/honk/provenance.json; \
|
||||
fi
|
||||
$(INSTALL_BIN) $(CURDIR)/files/control.sh $(1)/usr/share/honk/control.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/lock.sh $(1)/usr/share/honk/lock.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/lifecycle.sh $(1)/usr/share/honk/lifecycle.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/maintenance.sh $(1)/usr/share/honk/maintenance.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/job.sh $(1)/usr/share/honk/job.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/update.sh $(1)/usr/share/honk/update.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/redact-log.sh $(1)/usr/share/honk/redact-log.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/log-level-filter.sh $(1)/usr/share/honk/log-level-filter.sh
|
||||
$(INSTALL_DATA) $(CURDIR)/files/log-lines.sh $(1)/usr/share/honk/log-lines.sh
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,honk))
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
# 开机自启是内置行为:装上就 enable,是否真正启动由 honk.main.enabled 决定,
|
||||
# 不再提供单独的 boot_enabled 开关。
|
||||
/etc/init.d/honk enable
|
||||
|
||||
# 配置模式已取消:只清遗留键,活动配置与启停状态一概不动。
|
||||
legacy_mode=$(uci -q get honk.main.config_mode 2>/dev/null)
|
||||
if [ -n "$legacy_mode" ]; then
|
||||
uci -q delete honk.main.config_mode && uci -q commit honk
|
||||
fi
|
||||
exit 0
|
||||
@@ -1,34 +0,0 @@
|
||||
include {
|
||||
system.dae
|
||||
}
|
||||
|
||||
group {
|
||||
proxy {
|
||||
policy: min_moving_avg
|
||||
}
|
||||
}
|
||||
|
||||
dns {
|
||||
upstream {
|
||||
default: 'https://223.5.5.5/dns-query?tls_server_name=dns.alidns.com' -> direct
|
||||
remote: 'https://8.8.8.8/dns-query?tls_server_name=dns.google' -> proxy
|
||||
}
|
||||
routing {
|
||||
request {
|
||||
qname(geosite: private) -> asis
|
||||
qname(geosite: cn) -> default
|
||||
fallback: remote
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routing {
|
||||
dip(224.0.0.0/3, 'ff00::/8') -> direct
|
||||
# 路由器自身地址直连,管理面板不被代理;服务启动时按本机地址重写下面区块
|
||||
# honk:local-direct-begin
|
||||
# honk:local-direct-end
|
||||
dip(geoip: private) && !dport(53) -> direct(must)
|
||||
dip(geoip: cn) && !dport(53) -> direct(must)
|
||||
domain(geosite: cn) -> direct
|
||||
fallback: direct
|
||||
}
|
||||
@@ -1,979 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export PATH
|
||||
|
||||
HONK_CORE=/usr/bin/honk-core
|
||||
HONK_MAINT_DIR=/etc/.honk-maintenance
|
||||
HONK_MAINT_JOURNAL=$HONK_MAINT_DIR/journal
|
||||
HONK_MAINT_CAP_FILE=$HONK_MAINT_DIR/capability
|
||||
|
||||
. /lib/functions.sh
|
||||
. /usr/share/honk/lock.sh
|
||||
. /usr/share/honk/lifecycle.sh
|
||||
|
||||
fail() {
|
||||
echo "honk: $*" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
load_settings() {
|
||||
config_load honk
|
||||
config_get_bool enabled main enabled 0
|
||||
config_get_bool boot_enabled main boot_enabled 0
|
||||
config_get_bool initialized main initialized 0
|
||||
config_get config_file main config_file /etc/honk/config.dae
|
||||
config_get listen_port main listen_port 9527
|
||||
config_get_bool clash_api_enabled main clash_api_enabled 0
|
||||
config_get clash_api_port main clash_api_port 9090
|
||||
config_get clash_api_secret main clash_api_secret ''
|
||||
config_get lan_network main lan_network lan
|
||||
settings_owner=$(config_owner "$config_file")
|
||||
}
|
||||
|
||||
valid_clash_api() {
|
||||
[ "$clash_api_enabled" = 1 ] || return 0
|
||||
case "$clash_api_port" in ''|*[!0-9]*) return 1 ;; esac
|
||||
[ "$clash_api_port" -ge 1024 ] && [ "$clash_api_port" -le 65535 ] || return 1
|
||||
[ "$clash_api_port" != "$listen_port" ] || return 1
|
||||
case "$clash_api_secret" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
|
||||
[ "${#clash_api_secret}" -ge 6 ] && [ "${#clash_api_secret}" -le 128 ]
|
||||
}
|
||||
|
||||
valid_network_name() {
|
||||
case "$lan_network" in
|
||||
''|*[!A-Za-z0-9_.-]*) return 1 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
module_loaded() {
|
||||
[ -d "/sys/module/$1" ] || grep -q "^$1 " /proc/modules
|
||||
}
|
||||
|
||||
module_available() {
|
||||
module_loaded "$1" && return 0
|
||||
module_path=$(printf '%s' "$1" | tr '_' '-')
|
||||
module_dir=/lib/modules/$(uname -r)
|
||||
[ -r "$module_dir/modules.builtin" ] && grep -Eq "/${1}\.ko$|/${module_path}\.ko$" "$module_dir/modules.builtin" && return 0
|
||||
find "$module_dir" -type f \( -name "$1.ko" -o -name "$1.ko.gz" -o -name "$1.ko.xz" -o -name "$1.ko.zst" \
|
||||
-o -name "$module_path.ko" -o -name "$module_path.ko.gz" -o -name "$module_path.ko.xz" -o -name "$module_path.ko.zst" \) -print 2>/dev/null | grep -q .
|
||||
}
|
||||
|
||||
system_config_readonly() {
|
||||
permissions=$(ls -ldn /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||||
[ "$permissions" = '-rw-------' ] && return 0
|
||||
[ "$permissions" = '-r--r--r--' ] && ! grep -q 'clash_api {' /etc/honk/system.dae
|
||||
}
|
||||
|
||||
no_honk_resources() {
|
||||
# honk-core removes dae0/dae0peer and the daens netns when it exits cleanly. A
|
||||
# crash or SIGKILL leaves them behind, and preflight then refuses every later
|
||||
# start, so the service can never come back without hand surgery. Reclaim them
|
||||
# here, but only when no core is running to own them.
|
||||
if ! pidof honk-core >/dev/null 2>&1; then
|
||||
for stale_link in dae0 dae0peer; do
|
||||
ip link show "$stale_link" >/dev/null 2>&1 && ip link del "$stale_link" >/dev/null 2>&1
|
||||
done
|
||||
if ip netns list 2>/dev/null | awk '{print $1}' | grep -qx daens; then
|
||||
ip netns del daens >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
for link in dae0 dae0peer; do
|
||||
ip link show "$link" >/dev/null 2>&1 && { fail "$link 已存在(上次没有正常退出),请先停止服务或执行系统恢复"; return 1; }
|
||||
done
|
||||
ip netns list 2>/dev/null | awk '{print $1}' | grep -qx daens && { fail "daens 网络命名空间已存在,请先停止服务或执行系统恢复"; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
check_port() {
|
||||
checked_port=${1:-$listen_port}
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
netstat -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)${checked_port}$" && { fail "监听端口已被占用,请换一个端口"; return 1; }
|
||||
elif command -v ss >/dev/null 2>&1; then
|
||||
ss -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)${checked_port}$" && { fail "监听端口已被占用,请换一个端口"; return 1; }
|
||||
else
|
||||
fail "缺少 netstat/ss,无法检查端口占用(装 ip-full 或 net-tools)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
check_network() {
|
||||
valid_network_name || { fail "LAN 网络名包含非法字符"; return 1; }
|
||||
command -v ubus >/dev/null 2>&1 || { fail "ubus 不可用,无法校验网络设置"; return 1; }
|
||||
ubus call "network.interface.$lan_network" status >/dev/null 2>&1 || { fail "选择的 LAN 网络在 netifd 里不存在,请在服务设置里重新选择"; return 1; }
|
||||
}
|
||||
|
||||
# 本机 LAN/WAN 设备地址(跳过链路本地),与 honk 的 local_direct_cidrs 对齐
|
||||
local_direct_addresses() {
|
||||
list=''
|
||||
. /lib/functions/network.sh
|
||||
for net in "$lan_network" wan; do
|
||||
network_get_device dev "$net" || continue
|
||||
case "$dev" in ''|*[!A-Za-z0-9_.-]*) continue ;; esac
|
||||
for addr in $(ip -o addr show dev "$dev" 2>/dev/null | awk '/inet6? / { split($4, a, "/"); print a[1] }'); do
|
||||
case "$addr" in fe80:*|169.254.*) continue ;; esac
|
||||
case "$addr" in ''|*[!0-9a-fA-F:.]*) continue ;; esac
|
||||
[ "${#addr}" -le 45 ] || continue
|
||||
case " $list " in *" $addr "*) continue ;; esac
|
||||
list="${list:+$list, }$addr"
|
||||
done
|
||||
done
|
||||
printf '%s\n' "$list"
|
||||
}
|
||||
|
||||
# 把标记区块换成当前本机地址,honk 的 LAN self-protection 检查才认
|
||||
sync_local_direct() {
|
||||
begin=' # honk:local-direct-begin'
|
||||
end=' # honk:local-direct-end'
|
||||
grep -qxF "$begin" /etc/honk/config.dae || return 0
|
||||
grep -qxF "$end" /etc/honk/config.dae || return 0
|
||||
ips=$(local_direct_addresses)
|
||||
[ -n "$ips" ] && rule=" dip($ips) && !dport(53) -> direct(must)" || rule=''
|
||||
# honk-core 只认 .dae 后缀,临时文件必须带它
|
||||
temp=/etc/honk/.config.$$.dae
|
||||
( umask 077; : > "$temp" ) || return 1
|
||||
awk -v begin="$begin" -v end="$end" -v rule="$rule" '
|
||||
$0 == begin { print; if (rule != "") print rule; skip = 1; next }
|
||||
skip && $0 == end { print; skip = 0; next }
|
||||
skip { next }
|
||||
{ print }
|
||||
' /etc/honk/config.dae > "$temp" || { rm -f "$temp"; return 1; }
|
||||
cmp -s "$temp" /etc/honk/config.dae && { rm -f "$temp"; return 0; }
|
||||
"$HONK_CORE" config validate --config "$temp" >/dev/null 2>&1 || { rm -f "$temp"; return 1; }
|
||||
chmod 0600 "$temp"
|
||||
mv -f "$temp" /etc/honk/config.dae || { rm -f "$temp"; return 1; }
|
||||
}
|
||||
|
||||
check_config() {
|
||||
[ "$config_file" = /etc/honk/config.dae ] || { fail "配置路径必须为 /etc/honk/config.dae"; return 1; }
|
||||
[ -f /etc/honk/config.dae ] && [ ! -L /etc/honk/config.dae ] || { fail "/etc/honk/config.dae 不是普通文件(可能被替换成软链)"; return 1; }
|
||||
"$HONK_CORE" config validate --config /etc/honk/config.dae >/dev/null || { fail "config.dae 未通过静态校验,请先修正配置"; return 1; }
|
||||
[ -r /usr/share/doona/index.html ] || { fail "缺少 Doona 静态文件(/usr/share/doona/index.html),请重新安装 luci-app-honk"; return 1; }
|
||||
[ "$settings_owner" = system ] || return 0
|
||||
[ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] || { fail "/etc/honk/system.dae 不是普通文件(可能被替换成软链)"; return 1; }
|
||||
system_config_readonly || { fail "/etc/honk/system.dae 权限不对,应为 0600"; return 1; }
|
||||
case "$listen_port" in
|
||||
''|*[!0-9]*) fail "监听端口必须是数字"; return 1 ;;
|
||||
esac
|
||||
[ "$listen_port" -ge 1024 ] && [ "$listen_port" -le 65535 ] || { fail "监听端口必须在 1024-65535 之间"; return 1; }
|
||||
valid_clash_api || { fail "Clash API 设置不合法(端口 1024-65535,密钥 6-128 位)"; return 1; }
|
||||
}
|
||||
|
||||
check_kernel() {
|
||||
kernel=$(uname -r | cut -d- -f1)
|
||||
major=$(printf '%s' "$kernel" | cut -d. -f1)
|
||||
minor=$(printf '%s' "$kernel" | cut -d. -f2)
|
||||
[ "$major" -gt 6 ] || { [ "$major" -eq 6 ] && [ "$minor" -ge 12 ]; } || { fail "内核版本过低,需要 Linux 6.12 或更新"; return 1; }
|
||||
[ -r /sys/kernel/btf/vmlinux ] || { fail "内核缺少 BTF(/sys/kernel/btf/vmlinux),请换用带 BTF 的固件"; return 1; }
|
||||
[ -d /sys/fs/bpf ] && grep -q ' /sys/fs/bpf bpf ' /proc/mounts || { fail "bpffs 未挂载到 /sys/fs/bpf,请检查内核或手工挂载"; return 1; }
|
||||
command -v ip >/dev/null 2>&1 || { fail "缺少 ip 命令(装 ip-full)"; return 1; }
|
||||
command -v modprobe >/dev/null 2>&1 || { fail "缺少 modprobe 命令(装 kmod)"; return 1; }
|
||||
[ -r /sys/kernel/btf/vmlinux ] || { fail "内核缺少 BTF(/sys/kernel/btf/vmlinux),请换用带 BTF 的固件"; return 1; }
|
||||
for module in sch_ingress cls_bpf veth nft_queue nfnetlink_queue; do
|
||||
module_available "$module" || { fail "缺少内核模块 $module,请确认 kmod-sched-core / kmod-sched-bpf / kmod-veth / kmod-nft-queue 已安装"; return 1; }
|
||||
done
|
||||
}
|
||||
|
||||
check_conflicts() {
|
||||
for process in dae daed clashoo mihomo sing-box; do
|
||||
pidof "$process" >/dev/null 2>&1 && { fail "检测到冲突进程 $process 正在运行,请先停掉它(多代理同时开会互相抢流量)"; return 1; }
|
||||
done
|
||||
for service in dae daed clashoo mihomo sing-box; do
|
||||
if [ -x "/etc/init.d/$service" ] && "/etc/init.d/$service" running >/dev/null 2>&1; then
|
||||
fail "检测到冲突服务 $service 正在运行,请先停用它(多代理同时开会互相抢流量)"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
maintenance_value() {
|
||||
awk -F= -v key="$1" '$1 == key { value = substr($0, length($1) + 2); count++ } END { if (count == 1) print value; else exit 1 }' "$2"
|
||||
}
|
||||
|
||||
proc_starttime() {
|
||||
[ -r "/proc/$1/stat" ] || return 1
|
||||
awk '{ sub(/^.*\) /, ""); split($0, fields, /[[:space:]]+/); if (fields[20] ~ /^[0-9]+$/) print fields[20]; else exit 1 }' "/proc/$1/stat" 2>/dev/null
|
||||
}
|
||||
|
||||
protected_root_file() {
|
||||
[ -f "$1" ] && [ ! -L "$1" ] || return 1
|
||||
protected_permissions=$(ls -ln "$1" 2>/dev/null | awk 'NR == 1 { print $1 ":" $3 ":" $4 }')
|
||||
[ "$protected_permissions" = '-rw-------:0:0' ]
|
||||
}
|
||||
|
||||
consume_maintenance_capability() {
|
||||
[ -n "${HONK_MAINT_CAP:-}" ] || return 1
|
||||
[ -d "$HONK_MAINT_DIR" ] && [ ! -L "$HONK_MAINT_DIR" ] || return 1
|
||||
protected_root_file "$HONK_MAINT_JOURNAL" && protected_root_file "$HONK_MAINT_CAP_FILE" || return 1
|
||||
awk -F= 'BEGIN { split("format nonce txn_id phase owner_pid owner_starttime", keys, " ") } NR > 6 || index($0, "=") == 0 || $1 != keys[NR] { exit 1 } END { if (NR != 6) exit 1 }' "$HONK_MAINT_CAP_FILE" || return 1
|
||||
consumed_cap=$HONK_MAINT_DIR/.capability-consumed.$$
|
||||
[ ! -e "$consumed_cap" ] || return 1
|
||||
mv "$HONK_MAINT_CAP_FILE" "$consumed_cap" 2>/dev/null || return 1
|
||||
# Chain with any existing EXIT trap instead of overwriting it.
|
||||
_prev_exit_trap=$(trap -p EXIT | sed "s/^trap -- '//;s/' EXIT$//")
|
||||
trap 'rm -f "$consumed_cap"; [ -n "$_prev_exit_trap" ] && eval "$_prev_exit_trap"' 0
|
||||
cap_format=$(maintenance_value format "$consumed_cap") || return 1
|
||||
cap_nonce=$(maintenance_value nonce "$consumed_cap") || return 1
|
||||
cap_txn=$(maintenance_value txn_id "$consumed_cap") || return 1
|
||||
cap_phase=$(maintenance_value phase "$consumed_cap") || return 1
|
||||
cap_pid=$(maintenance_value owner_pid "$consumed_cap") || return 1
|
||||
cap_starttime=$(maintenance_value owner_starttime "$consumed_cap") || return 1
|
||||
journal_format=$(maintenance_value format "$HONK_MAINT_JOURNAL") || return 1
|
||||
journal_operation=$(maintenance_value operation "$HONK_MAINT_JOURNAL") || return 1
|
||||
journal_phase=$(maintenance_value phase "$HONK_MAINT_JOURNAL") || return 1
|
||||
journal_txn=$(maintenance_value txn_id "$HONK_MAINT_JOURNAL") || return 1
|
||||
case "$cap_nonce:$cap_txn:$cap_pid:$cap_starttime" in *[!0-9a-f:]*|::*|:|'' ) return 1 ;; esac
|
||||
case "$cap_nonce" in *[!0-9a-f]*|'') return 1 ;; esac
|
||||
[ "${#cap_nonce}" -eq 32 ] && [ "${#cap_txn}" -eq 32 ] || return 1
|
||||
[ "$cap_format" = 1 ] && [ "$journal_format" = 1 ] || return 1
|
||||
case "$cap_pid:$cap_starttime" in *[!0-9:]*) return 1 ;; esac
|
||||
[ "$cap_pid" -gt 1 ] && [ "$cap_starttime" -gt 0 ] || return 1
|
||||
[ "$HONK_MAINT_CAP" = "$cap_nonce" ] && [ "$cap_txn" = "$journal_txn" ] || return 1
|
||||
case "$cap_phase:$journal_operation:$journal_phase" in
|
||||
candidate_health_start:restore:new_installed|candidate_health_start:import_apply:new_installed|rollback_old_start:restore:rollback_old_start|rollback_old_start:reset:rollback_old_start|rollback_old_start:import_apply:rollback_old_start) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
read -r lock_pid lock_starttime < "$HONK_LOCK_DIR/pid" || return 1
|
||||
[ "$lock_pid:$lock_starttime" = "$cap_pid:$cap_starttime" ] || return 1
|
||||
[ "$(proc_starttime "$cap_pid")" = "$cap_starttime" ] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
maintenance_start_guard() {
|
||||
if [ -e "$HONK_MAINT_JOURNAL" ] || [ -L "$HONK_MAINT_JOURNAL" ]; then
|
||||
consume_maintenance_capability || { fail "maintenance_recovery_required"; return 1; }
|
||||
elif [ -n "${HONK_MAINT_CAP:-}" ]; then
|
||||
fail "maintenance_capability_without_journal"
|
||||
return 1
|
||||
fi
|
||||
if lock_owner_invalid || honk_lock_is_stale; then
|
||||
# The recorded owner is gone, so nothing is in flight. Reclaim the lock
|
||||
# instead of refusing every later start: a caller that timed out cannot
|
||||
# release it, and leaving it wedges the service until someone SSHes in.
|
||||
# A live lock still blocks below through honk_lock_acquire.
|
||||
logger -t honk "reclaiming stale operation lock" 2>/dev/null || :
|
||||
rm -rf "$HONK_LOCK_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
lock_owner_invalid() {
|
||||
[ -d "$HONK_LOCK_DIR" ] || return 1
|
||||
lock_owner=$(cat "$HONK_LOCK_DIR/pid" 2>/dev/null)
|
||||
# pid file now stores "pid starttime"; only the leading PID matters here.
|
||||
lock_owner=${lock_owner%% *}
|
||||
case "$lock_owner" in ''|*[!0-9]*) return 0 ;; esac
|
||||
return 1
|
||||
}
|
||||
|
||||
preflight() {
|
||||
load_settings
|
||||
[ "$enabled" = 1 ] && [ "$initialized" = 1 ] || { fail "请先在 LuCI 里启用并完成初始化"; return 1; }
|
||||
# 只有 system.dae 负责这些字段时才检查 UCI 端口与网络;完整配置自行定义时不碰。
|
||||
if [ "$settings_owner" = system ]; then
|
||||
maintenance_start_guard && check_config && check_network && check_kernel && no_honk_resources && check_conflicts && check_port && {
|
||||
[ "$clash_api_enabled" = 0 ] || check_port "$clash_api_port"
|
||||
}
|
||||
return $?
|
||||
fi
|
||||
maintenance_start_guard && check_config && check_kernel && no_honk_resources && check_conflicts
|
||||
}
|
||||
|
||||
prepare_runtime() {
|
||||
preflight || return 1
|
||||
for module in sch_ingress cls_bpf veth nft_queue nfnetlink_queue; do
|
||||
module_loaded "$module" || modprobe "$module" || { fail "无法加载内核模块 $module,请确认对应 kmod 包已安装"; return 1; }
|
||||
done
|
||||
mkdir -p /etc/honk/state || { fail "无法创建状态目录(/etc/honk/state),请检查磁盘空间与权限"; return 1; }
|
||||
chmod 0700 /etc/honk/state || return 1
|
||||
}
|
||||
|
||||
valid_username() {
|
||||
case "$1" in
|
||||
''|*[!A-Za-z0-9_.-]*) return 1 ;;
|
||||
esac
|
||||
[ "$(printf '%s' "$1" | wc -c)" -le 64 ]
|
||||
}
|
||||
|
||||
valid_password() {
|
||||
password_bytes=$(printf '%s' "$1" | wc -c)
|
||||
[ "$password_bytes" -ge 8 ] && [ "$password_bytes" -le 512 ]
|
||||
}
|
||||
|
||||
read_credentials() {
|
||||
json_file=$(mktemp /tmp/honk-credentials.XXXXXX) || return 1
|
||||
chmod 0600 "$json_file" || { rm -f "$json_file"; return 1; }
|
||||
cat > "$json_file" || { rm -f "$json_file"; return 1; }
|
||||
json_bytes=$(wc -c < "$json_file")
|
||||
[ "$json_bytes" -le 2048 ] || { rm -f "$json_file"; return 1; }
|
||||
grep -Fq '\u0000' "$json_file" && { rm -f "$json_file"; return 1; }
|
||||
newline='
|
||||
'
|
||||
username_raw=$(jsonfilter -i "$json_file" -e '@.username' 2>/dev/null; printf '__HNK_END__')
|
||||
password_raw=$(jsonfilter -i "$json_file" -e '@.password' 2>/dev/null; printf '__HNK_END__')
|
||||
username=${username_raw%__HNK_END__}
|
||||
password=${password_raw%__HNK_END__}
|
||||
username=${username%"$newline"}
|
||||
password=${password%"$newline"}
|
||||
rm -f "$json_file"
|
||||
[ -n "$username" ] && [ -n "$password" ] || return 1
|
||||
valid_username "$username" && valid_password "$password"
|
||||
}
|
||||
|
||||
network_settings() {
|
||||
. /lib/functions/network.sh
|
||||
network_get_device lan_device "$lan_network" || return 1
|
||||
network_get_ipaddr lan_ip "$lan_network" || return 1
|
||||
case "$lan_device" in
|
||||
''|*[!A-Za-z0-9_.-]*) return 1 ;;
|
||||
esac
|
||||
[ "$(printf '%s' "$lan_device" | wc -c)" -le 15 ] || return 1
|
||||
printf '%s\n' "$lan_ip" | awk -F. 'NF == 4 { for (i=1;i<=4;i++) if ($i !~ /^[0-9]+$/ || $i > 255) exit 1; exit 0 } { exit 1 }' || return 1
|
||||
}
|
||||
|
||||
ensure_loopback_system() {
|
||||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||||
[ "$current_hash" = "$system_written_hash" ] || { fail "初始化期间 system.dae 被其他进程改动,已中止"; return 1; }
|
||||
current_listen=$(sed -n "s/^[[:space:]]*listen:[[:space:]]*'\([^']*\)'.*/\1/p" /etc/honk/system.dae | head -n 1)
|
||||
[ "$current_listen" = "127.0.0.1:$listen_port" ] && return 0
|
||||
temp_system=$(mktemp /etc/honk/.system.dae.XXXXXX) || return 1
|
||||
sed "s/^[[:space:]]*listen:[[:space:]]*'[^']*'.*/ listen: '127.0.0.1:$listen_port'/" /etc/honk/system.dae > "$temp_system" || { rm -f "$temp_system"; return 1; }
|
||||
chmod 0600 "$temp_system" || { rm -f "$temp_system"; return 1; }
|
||||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||||
[ "$current_hash" = "$system_written_hash" ] || { rm -f "$temp_system"; fail "初始化期间 system.dae 被其他进程改动,已中止"; return 1; }
|
||||
mv -f "$temp_system" /etc/honk/system.dae || { rm -f "$temp_system"; return 1; }
|
||||
system_written_hash=$(sha256sum /etc/honk/system.dae | awk '{print $1}')
|
||||
}
|
||||
|
||||
restore_system() {
|
||||
[ -n "$system_written_hash" ] || return 0
|
||||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||||
[ "$current_hash" = "$system_written_hash" ] || { fail "回滚期间 system.dae 被其他进程改动,已中止"; return 1; }
|
||||
[ "$current_hash" != "$original_system_hash" ] || { system_written_hash=""; return 0; }
|
||||
if [ -z "$original_system_hash" ]; then
|
||||
rm -f /etc/honk/system.dae || return 1
|
||||
system_written_hash=""
|
||||
return 0
|
||||
fi
|
||||
temp_restore=$(mktemp /etc/honk/.system.dae.restore.XXXXXX) || return 1
|
||||
cp -p "$old_system" "$temp_restore" || { rm -f "$temp_restore"; return 1; }
|
||||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||||
[ "$current_hash" = "$system_written_hash" ] || { rm -f "$temp_restore"; fail "回滚期间 system.dae 被其他进程改动,已中止"; return 1; }
|
||||
mv -f "$temp_restore" /etc/honk/system.dae || { rm -f "$temp_restore"; return 1; }
|
||||
system_written_hash=""
|
||||
}
|
||||
|
||||
cleanup_initialize() {
|
||||
if mock_stop >/dev/null 2>&1; then
|
||||
if [ -n "$system_written_hash" ] && ! restore_system; then
|
||||
logger -t honk "system config rollback refused or failed; inspect /etc/honk/system.dae"
|
||||
fi
|
||||
rm -rf "$temp_dir"
|
||||
honk_lock_release >/dev/null 2>&1
|
||||
else
|
||||
logger -t honk "mock process cleanup failed; keeping operation lock and temporary files"
|
||||
fi
|
||||
}
|
||||
|
||||
write_runtime_system() {
|
||||
lan_device=$1
|
||||
lan_ip=$2
|
||||
system_port=${3:-$listen_port}
|
||||
system_listen=${4:-$lan_ip}
|
||||
valid_clash_api || { fail "Clash API 设置不合法(端口 1024-65535,密钥 6-128 位)"; return 1; }
|
||||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||||
[ "$current_hash" = "$system_written_hash" ] || { fail "写入运行配置时 system.dae 被其他进程改动,已中止"; return 1; }
|
||||
temp_system=$(mktemp /etc/honk/.system.dae.XXXXXX) || return 1
|
||||
{
|
||||
printf "global {\n wan_interface: auto\n lan_interface: '%s'\n" "$lan_device"
|
||||
printf " log_level: info\n dial_mode: domain\n allow_insecure: false\n"
|
||||
printf " auto_config_kernel_parameter: true\n data_dir: '/etc/honk'\n"
|
||||
printf " store_subscribe: true\n nfqueue_enable: true\n}\n\n"
|
||||
printf "experimental {\n native_api {\n enabled: true\n listen: '%s:%s'\n" "$system_listen" "$system_port"
|
||||
printf " password_auth: true\n allow_anonymous_loopback: false\n"
|
||||
printf " config_write: true\n ui: '/usr/share/doona'\n }\n"
|
||||
if [ "$clash_api_enabled" = 1 ]; then
|
||||
printf " clash_api {\n external_controller: '%s:%s'\n external_ui: '/usr/share/honk/zashboard'\n secret: '%s'\n default_mode: Rule\n }\n" "$system_listen" "$clash_api_port" "$clash_api_secret"
|
||||
fi
|
||||
printf "}\n"
|
||||
} > "$temp_system" || { rm -f "$temp_system"; return 1; }
|
||||
chmod 0600 "$temp_system" || { rm -f "$temp_system"; return 1; }
|
||||
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||||
[ "$current_hash" = "$system_written_hash" ] || { rm -f "$temp_system"; fail "写入运行配置时 system.dae 被其他进程改动,已中止"; return 1; }
|
||||
mv -f "$temp_system" /etc/honk/system.dae || { rm -f "$temp_system"; return 1; }
|
||||
system_written_hash=$(sha256sum /etc/honk/system.dae | awk '{print $1}')
|
||||
}
|
||||
|
||||
managed_core_pid() {
|
||||
honk_lifecycle_managed_pid
|
||||
}
|
||||
|
||||
configured_system_port() {
|
||||
sed -n "s/^[[:space:]]*listen:[[:space:]]*'[^:]*:\([0-9][0-9]*\)'.*/\1/p" /etc/honk/system.dae 2>/dev/null | head -n 1
|
||||
}
|
||||
|
||||
configured_clash_port() {
|
||||
sed -n "s/^[[:space:]]*external_controller:[[:space:]]*'[^:]*:\([0-9][0-9]*\)'.*/\1/p" /etc/honk/system.dae 2>/dev/null | head -n 1
|
||||
}
|
||||
|
||||
sync_boot_link() {
|
||||
if [ "$1" = 1 ]; then
|
||||
/etc/init.d/honk enable
|
||||
else
|
||||
/etc/init.d/honk disable
|
||||
fi
|
||||
}
|
||||
|
||||
restore_old_runtime() {
|
||||
[ "$old_running" = 1 ] || return 0
|
||||
runtime_enabled=$(uci -q get honk.main.enabled || echo 0)
|
||||
if [ "$runtime_enabled" != 1 ]; then uci set honk.main.enabled=1 && uci commit honk || return 1; fi
|
||||
honk_lifecycle_start 30 || {
|
||||
[ "$runtime_enabled" = 1 ] || { uci set "honk.main.enabled=$runtime_enabled" && uci commit honk; }
|
||||
return 1
|
||||
}
|
||||
if [ "$runtime_enabled" != 1 ]; then uci set "honk.main.enabled=$runtime_enabled" && uci commit honk || return 1; fi
|
||||
}
|
||||
|
||||
settings_rollback() {
|
||||
rollback_ok=1
|
||||
honk_lifecycle_stop 30 || return 1
|
||||
if [ -n "$system_written_hash" ] && ! restore_system; then rollback_ok=0; fi
|
||||
if ! uci import honk < "$old_uci" || ! uci commit honk; then rollback_ok=0; fi
|
||||
if ! sync_boot_link "$old_boot_enabled"; then rollback_ok=0; fi
|
||||
restore_old_runtime || rollback_ok=0
|
||||
[ "$rollback_ok" = 1 ]
|
||||
}
|
||||
|
||||
settings() {
|
||||
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
|
||||
load_settings
|
||||
[ "$settings_owner" = system ] || { echo '{"ok":false,"message":"config_settings_owned_by_file"}'; return 1; }
|
||||
. /usr/share/libubox/jshn.sh || { echo '{"ok":false,"message":"jshn_unavailable"}'; return 1; }
|
||||
# Bound stdin to 64KiB (DoS protection); read one extra byte to detect truncation.
|
||||
settings_json=$(head -c 65537)
|
||||
if [ "${#settings_json}" -gt 65536 ]; then
|
||||
echo '{"ok":false,"message":"settings_too_large"}'; return 1
|
||||
fi
|
||||
json_load "$settings_json" || { echo '{"ok":false,"message":"invalid_settings_json"}'; return 1; }
|
||||
json_get_var new_network lan_network
|
||||
json_get_type network_type lan_network
|
||||
json_get_var new_port listen_port
|
||||
json_get_type port_type listen_port
|
||||
json_get_var new_boot_enabled boot_enabled
|
||||
json_get_type boot_enabled_type boot_enabled
|
||||
json_get_var new_clash_enabled clash_api_enabled
|
||||
json_get_type clash_enabled_type clash_api_enabled
|
||||
json_get_var new_clash_port clash_api_port
|
||||
json_get_type clash_port_type clash_api_port
|
||||
json_get_var new_clash_secret clash_api_secret
|
||||
json_get_type clash_secret_type clash_api_secret
|
||||
[ "$network_type" = string ] || { echo '{"ok":false,"message":"invalid_lan_network"}'; return 1; }
|
||||
case "$port_type" in int|integer) ;; *) echo '{"ok":false,"message":"invalid_listen_port"}'; return 1 ;; esac
|
||||
# boot_enabled 已内置(恒为 1),前端不再提交,缺省就按当前值处理
|
||||
case "$boot_enabled_type" in ''|null|boolean) ;; *) echo '{"ok":false,"message":"invalid_boolean_settings"}'; return 1 ;; esac
|
||||
case "$clash_enabled_type" in ''|null|boolean) ;; *) echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1 ;; esac
|
||||
case "$clash_port_type" in ''|null|int|integer) ;; *) echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1 ;; esac
|
||||
case "$clash_secret_type" in ''|null|string) ;; *) echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1 ;; esac
|
||||
case "$new_network" in ''|*[!A-Za-z0-9_.-]*) echo '{"ok":false,"message":"invalid_lan_network"}'; return 1 ;; esac
|
||||
case "$new_port" in ''|*[!0-9]*) echo '{"ok":false,"message":"invalid_listen_port"}'; return 1 ;; esac
|
||||
[ "$new_port" -ge 1024 ] && [ "$new_port" -le 65535 ] || { echo '{"ok":false,"message":"invalid_listen_port"}'; return 1; }
|
||||
load_settings
|
||||
case "$boot_enabled_type" in ''|null) new_boot_enabled=1 ;; esac
|
||||
new_boot_enabled=1
|
||||
case "$clash_enabled_type" in ''|null) new_clash_enabled=$clash_api_enabled ;; esac
|
||||
case "$clash_port_type" in ''|null) new_clash_port=$clash_api_port ;; esac
|
||||
[ -n "$new_clash_secret" ] || new_clash_secret=$clash_api_secret
|
||||
new_enabled=$enabled
|
||||
old_network=$lan_network
|
||||
old_port=$listen_port
|
||||
old_enabled=$enabled
|
||||
old_boot_enabled=$boot_enabled
|
||||
old_initialized=$initialized
|
||||
old_clash_enabled=$clash_api_enabled
|
||||
old_clash_port=$clash_api_port
|
||||
old_clash_secret=$clash_api_secret
|
||||
[ "$initialized" = 1 ] || { [ "$new_enabled" = 0 ] || { echo '{"ok":false,"message":"initialize_before_enabling_service"}'; return 1; }; }
|
||||
lan_network=$new_network
|
||||
listen_port=$new_port
|
||||
clash_api_enabled=$new_clash_enabled
|
||||
clash_api_port=$new_clash_port
|
||||
clash_api_secret=$new_clash_secret
|
||||
valid_clash_api || { echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1; }
|
||||
check_network || { echo '{"ok":false,"message":"lan_network_unavailable"}'; return 1; }
|
||||
network_settings || { echo '{"ok":false,"message":"lan_ipv4_unavailable"}'; return 1; }
|
||||
old_running=0
|
||||
if managed_core_pid >/dev/null 2>&1; then
|
||||
old_running=1
|
||||
elif pidof honk-core >/dev/null 2>&1; then
|
||||
echo '{"ok":false,"message":"unmanaged_core_running"}'
|
||||
return 1
|
||||
fi
|
||||
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$new_port" ]; }; then
|
||||
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
|
||||
fi
|
||||
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$new_clash_port" ]; }; then
|
||||
check_port "$new_clash_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
|
||||
fi
|
||||
honk_lock_acquire || { echo '{"ok":false,"message":"operation_in_progress"}'; return 1; }
|
||||
trap 'honk_lock_release >/dev/null 2>&1' 0
|
||||
load_settings
|
||||
[ "$lan_network" = "$old_network" ] && [ "$listen_port" = "$old_port" ] && [ "$enabled" = "$old_enabled" ] && [ "$boot_enabled" = "$old_boot_enabled" ] && [ "$initialized" = "$old_initialized" ] && [ "$clash_api_enabled" = "$old_clash_enabled" ] && [ "$clash_api_port" = "$old_clash_port" ] && [ "$clash_api_secret" = "$old_clash_secret" ] || { echo '{"ok":false,"message":"settings_changed_concurrently"}'; return 1; }
|
||||
new_enabled=$enabled
|
||||
managed_core_pid >/dev/null 2>&1 && recheck_running=1 || recheck_running=0
|
||||
[ "$recheck_running" = "$old_running" ] || { echo '{"ok":false,"message":"service_state_changed_concurrently"}'; return 1; }
|
||||
old_running=$recheck_running
|
||||
lan_network=$new_network
|
||||
listen_port=$new_port
|
||||
clash_api_enabled=$new_clash_enabled
|
||||
clash_api_port=$new_clash_port
|
||||
clash_api_secret=$new_clash_secret
|
||||
check_network && network_settings || { echo '{"ok":false,"message":"lan_network_changed_during_settings"}'; return 1; }
|
||||
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$new_port" ]; }; then
|
||||
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
|
||||
fi
|
||||
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$new_clash_port" ]; }; then
|
||||
check_port "$new_clash_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
|
||||
fi
|
||||
if [ "$initialized" = 1 ] && ! check_config; then echo '{"ok":false,"message":"package_config_unavailable"}'; return 1; fi
|
||||
umask 077
|
||||
settings_dir=$(mktemp -d /tmp/honk-settings.XXXXXX) || { echo '{"ok":false,"message":"temporary_storage_unavailable"}'; return 1; }
|
||||
old_uci=$settings_dir/honk.uci
|
||||
uci export honk > "$old_uci" || { rm -rf "$settings_dir"; echo '{"ok":false,"message":"uci_snapshot_failed"}'; return 1; }
|
||||
old_system=$settings_dir/system.dae
|
||||
system_written_hash=""
|
||||
if [ "$initialized" = 1 ]; then
|
||||
[ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] || { rm -rf "$settings_dir"; echo '{"ok":false,"message":"system_config_unavailable"}'; return 1; }
|
||||
cp -p /etc/honk/system.dae "$old_system" || { rm -rf "$settings_dir"; echo '{"ok":false,"message":"system_snapshot_failed"}'; return 1; }
|
||||
original_system_hash=$(sha256sum "$old_system" | awk '{print $1}')
|
||||
system_written_hash=$original_system_hash
|
||||
fi
|
||||
if [ "$old_running" = 1 ]; then
|
||||
honk_lifecycle_stop 30 || {
|
||||
settings_rollback; rollback_status=$?
|
||||
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"service_stop_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"service_stop_and_rollback_failed_snapshot_retained"}'; fi
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
if [ "$initialized" = 1 ] && ! write_runtime_system "$lan_device" "$lan_ip" "$new_port"; then
|
||||
settings_rollback; rollback_status=$?
|
||||
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"system_config_write_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"system_config_write_and_rollback_failed_snapshot_retained"}'; fi
|
||||
return 1
|
||||
fi
|
||||
uci set "honk.main.lan_network=$new_network" && uci set "honk.main.listen_port=$new_port" && uci set "honk.main.enabled=$new_enabled" && uci set "honk.main.boot_enabled=$new_boot_enabled" && uci set "honk.main.clash_api_enabled=$new_clash_enabled" && uci set "honk.main.clash_api_port=$new_clash_port" && uci set "honk.main.clash_api_secret=$new_clash_secret" && uci commit honk && chmod 0600 /etc/config/honk || {
|
||||
settings_rollback; rollback_status=$?
|
||||
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"settings_commit_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"settings_commit_and_rollback_failed_snapshot_retained"}'; fi
|
||||
return 1
|
||||
}
|
||||
if ! sync_boot_link "$new_boot_enabled"; then
|
||||
settings_rollback; rollback_status=$?
|
||||
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"boot_link_update_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"boot_link_update_and_rollback_failed_snapshot_retained"}'; fi
|
||||
return 1
|
||||
fi
|
||||
if [ "$initialized" = 1 ]; then
|
||||
service_ok=0
|
||||
if [ "$new_enabled" = 1 ]; then
|
||||
honk_lifecycle_start 30 || service_ok=1
|
||||
else
|
||||
honk_lifecycle_stop 30 || service_ok=1
|
||||
fi
|
||||
if [ "$service_ok" != 0 ]; then
|
||||
settings_rollback; rollback_status=$?
|
||||
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"service_apply_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"service_apply_and_rollback_failed_snapshot_retained"}'; fi
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
system_written_hash=""
|
||||
rm -rf "$settings_dir"
|
||||
echo '{"ok":true,"message":"settings_saved"}'
|
||||
}
|
||||
|
||||
diagnose() {
|
||||
load_settings
|
||||
errors=""
|
||||
managed_core_pid >/dev/null 2>&1 && running=1 || running=0
|
||||
check_config >/dev/null 2>&1 || errors="$errors invalid_package_config"
|
||||
check_kernel >/dev/null 2>&1 || errors="$errors kernel_requirements_unmet"
|
||||
if [ "$settings_owner" != system ]; then
|
||||
if [ "$running" != 1 ]; then
|
||||
no_honk_resources >/dev/null 2>&1 || errors="$errors honk_resources_occupied"
|
||||
pidof honk-core >/dev/null 2>&1 && errors="$errors unmanaged_core_running"
|
||||
fi
|
||||
else
|
||||
check_network >/dev/null 2>&1 || errors="$errors lan_network_unavailable"
|
||||
network_settings >/dev/null 2>&1 || errors="$errors lan_ipv4_unavailable"
|
||||
command -v curl >/dev/null 2>&1 || errors="$errors curl_missing"
|
||||
if [ "$running" != 1 ]; then
|
||||
no_honk_resources >/dev/null 2>&1 || errors="$errors honk_resources_occupied"
|
||||
pidof honk-core >/dev/null 2>&1 && errors="$errors unmanaged_core_running"
|
||||
check_port >/dev/null 2>&1 || errors="$errors listen_port_occupied"
|
||||
if [ "$clash_api_enabled" = 1 ]; then
|
||||
check_port "$clash_api_port" >/dev/null 2>&1 || errors="$errors clash_api_port_occupied"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
check_conflicts >/dev/null 2>&1 || errors="$errors proxy_conflict"
|
||||
if [ -d "$HONK_LOCK_DIR" ]; then
|
||||
if lock_owner_invalid || honk_lock_is_stale; then
|
||||
errors="$errors stale_operation_lock maintenance_recovery_required"
|
||||
else
|
||||
errors="$errors operation_in_progress"
|
||||
fi
|
||||
fi
|
||||
{ [ -e "$HONK_MAINT_JOURNAL" ] || [ -L "$HONK_MAINT_JOURNAL" ]; } && errors="$errors maintenance_recovery_required"
|
||||
if [ -n "$errors" ]; then
|
||||
printf '%s\n' "$errors" | sed 's/^ *//'
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
main_has_system_include() {
|
||||
awk '
|
||||
/^[[:space:]]*include[[:space:]]*\{/ { inside = 1 }
|
||||
inside && /(^|[^[:alnum:]_.-])system\.dae([^[:alnum:]_.-]|$)/ { found = 1 }
|
||||
inside && /\}/ { inside = 0 }
|
||||
END { exit !found }
|
||||
' /etc/honk/config.dae
|
||||
}
|
||||
|
||||
clash_system_matches() {
|
||||
if [ "$clash_api_enabled" = 0 ]; then
|
||||
! grep -q 'clash_api {' /etc/honk/system.dae
|
||||
return
|
||||
fi
|
||||
permissions=$(ls -ldn /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
|
||||
[ "$permissions" = '-rw-------' ] || return 1
|
||||
grep -Fqx " external_controller: '$system_host:$clash_api_port'" /etc/honk/system.dae || return 1
|
||||
grep -Fqx " external_ui: '/usr/share/honk/zashboard'" /etc/honk/system.dae || return 1
|
||||
system_secret=$(sed -n "s/^ secret: '\([^']*\)'$/\1/p" /etc/honk/system.dae)
|
||||
[ "$system_secret" = "$clash_api_secret" ]
|
||||
}
|
||||
|
||||
repair() {
|
||||
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
|
||||
load_settings
|
||||
if [ "$settings_owner" != system ]; then
|
||||
check_config && { echo '{"ok":true,"message":"config_valid"}'; return 0; }
|
||||
echo '{"ok":false,"message":"config_invalid"}'
|
||||
return 1
|
||||
fi
|
||||
old_network=$lan_network
|
||||
old_port=$listen_port
|
||||
old_enabled=$enabled
|
||||
old_boot_enabled=$boot_enabled
|
||||
old_initialized=$initialized
|
||||
old_clash_enabled=$clash_api_enabled
|
||||
old_clash_port=$clash_api_port
|
||||
old_clash_secret=$clash_api_secret
|
||||
[ "$config_file" = /etc/honk/config.dae ] || { echo '{"ok":false,"message":"invalid_config_path"}'; return 1; }
|
||||
[ -f /etc/honk/config.dae ] && [ ! -L /etc/honk/config.dae ] || { echo '{"ok":false,"message":"main_config_unavailable"}'; return 1; }
|
||||
main_hash=$(sha256sum /etc/honk/config.dae | awk '{print $1}')
|
||||
main_has_system_include || { echo '{"ok":false,"message":"required_system_include_missing"}'; return 1; }
|
||||
valid_network_name || { echo '{"ok":false,"message":"invalid_lan_network"}'; return 1; }
|
||||
case "$listen_port" in ''|*[!0-9]*) echo '{"ok":false,"message":"invalid_listen_port"}'; return 1 ;; esac
|
||||
[ "$listen_port" -ge 1024 ] && [ "$listen_port" -le 65535 ] || { echo '{"ok":false,"message":"invalid_listen_port"}'; return 1; }
|
||||
valid_clash_api || { echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1; }
|
||||
check_network || { echo '{"ok":false,"message":"lan_network_unavailable"}'; return 1; }
|
||||
network_settings || { echo '{"ok":false,"message":"lan_ipv4_unavailable"}'; return 1; }
|
||||
old_running=0
|
||||
managed_core_pid >/dev/null 2>&1 && old_running=1
|
||||
if pidof honk-core >/dev/null 2>&1 && [ "$old_running" = 0 ]; then echo '{"ok":false,"message":"unmanaged_core_running"}'; return 1; fi
|
||||
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$listen_port" ]; }; then
|
||||
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
|
||||
fi
|
||||
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$clash_api_port" ]; }; then
|
||||
check_port "$clash_api_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
|
||||
fi
|
||||
honk_lock_acquire || { echo '{"ok":false,"message":"operation_in_progress"}'; return 1; }
|
||||
trap 'honk_lock_release >/dev/null 2>&1' 0
|
||||
load_settings
|
||||
[ "$lan_network" = "$old_network" ] && [ "$listen_port" = "$old_port" ] && [ "$enabled" = "$old_enabled" ] && [ "$boot_enabled" = "$old_boot_enabled" ] && [ "$initialized" = "$old_initialized" ] && [ "$clash_api_enabled" = "$old_clash_enabled" ] && [ "$clash_api_port" = "$old_clash_port" ] && [ "$clash_api_secret" = "$old_clash_secret" ] || { echo '{"ok":false,"message":"settings_changed_concurrently"}'; return 1; }
|
||||
managed_core_pid >/dev/null 2>&1 && recheck_running=1 || recheck_running=0
|
||||
[ "$recheck_running" = "$old_running" ] || { echo '{"ok":false,"message":"service_state_changed_concurrently"}'; return 1; }
|
||||
[ "$(sha256sum /etc/honk/config.dae | awk '{print $1}')" = "$main_hash" ] || { echo '{"ok":false,"message":"main_config_changed_concurrently"}'; return 1; }
|
||||
check_network && network_settings || { echo '{"ok":false,"message":"lan_network_changed_during_repair"}'; return 1; }
|
||||
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$listen_port" ]; }; then
|
||||
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
|
||||
fi
|
||||
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$clash_api_port" ]; }; then
|
||||
check_port "$clash_api_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
|
||||
fi
|
||||
if [ "$initialized" = 1 ]; then system_host=$lan_ip; else system_host=127.0.0.1; fi
|
||||
if [ -r /usr/share/doona/index.html ] && [ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] && system_config_readonly && \
|
||||
grep -Fqx ' wan_interface: auto' /etc/honk/system.dae && grep -Fqx " lan_interface: '$lan_device'" /etc/honk/system.dae && \
|
||||
grep -Fqx " data_dir: '/etc/honk'" /etc/honk/system.dae && grep -Fqx ' nfqueue_enable: true' /etc/honk/system.dae && \
|
||||
grep -Fqx " listen: '$system_host:$listen_port'" /etc/honk/system.dae && \
|
||||
grep -Fqx " ui: '/usr/share/doona'" /etc/honk/system.dae && grep -Fqx ' password_auth: true' /etc/honk/system.dae && \
|
||||
grep -Fqx ' allow_anonymous_loopback: false' /etc/honk/system.dae && grep -Fqx ' config_write: true' /etc/honk/system.dae && clash_system_matches; then
|
||||
echo '{"ok":true,"message":"system_config_healthy"}'
|
||||
return 0
|
||||
fi
|
||||
if [ -e /etc/honk/system.dae ] && { [ ! -f /etc/honk/system.dae ] || [ -L /etc/honk/system.dae ]; }; then
|
||||
echo '{"ok":false,"message":"system_config_not_regular_file"}'
|
||||
return 1
|
||||
fi
|
||||
[ -r /usr/share/doona/index.html ] || { echo '{"ok":false,"message":"ui_assets_missing"}'; return 1; }
|
||||
umask 077
|
||||
repair_dir=$(mktemp -d /tmp/honk-repair.XXXXXX) || { echo '{"ok":false,"message":"temporary_storage_unavailable"}'; return 1; }
|
||||
old_system=$repair_dir/system.dae
|
||||
original_system_hash=""
|
||||
if [ -f /etc/honk/system.dae ]; then
|
||||
cp -p /etc/honk/system.dae "$old_system" || { rm -rf "$repair_dir"; echo '{"ok":false,"message":"system_snapshot_failed"}'; return 1; }
|
||||
original_system_hash=$(sha256sum "$old_system" | awk '{print $1}')
|
||||
fi
|
||||
system_written_hash=$original_system_hash
|
||||
if [ "$old_running" = 1 ]; then
|
||||
if ! honk_lifecycle_stop 30; then
|
||||
logger -t honk "repair stop/drain unconfirmed; no restart attempted; snapshot retained at $repair_dir"
|
||||
echo '{"ok":false,"message":"service_stop_unconfirmed_snapshot_retained"}'
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if [ "$initialized" = 1 ]; then system_listen=$lan_ip; else system_listen=127.0.0.1; fi
|
||||
if ! write_runtime_system "$lan_device" "$lan_ip" "$listen_port" "$system_listen"; then
|
||||
repair_rollback_ok=1
|
||||
[ -n "$system_written_hash" ] && ! restore_system && repair_rollback_ok=0
|
||||
restore_old_runtime || repair_rollback_ok=0
|
||||
if [ "$repair_rollback_ok" = 1 ]; then
|
||||
rm -rf "$repair_dir"
|
||||
echo '{"ok":false,"message":"system_config_repair_failed_rolled_back"}'
|
||||
else
|
||||
logger -t honk "repair rollback failed; snapshot retained at $repair_dir"
|
||||
echo '{"ok":false,"message":"system_config_repair_rollback_failed_snapshot_retained"}'
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
if [ "$initialized" = 1 ] && { [ "$old_running" = 1 ] || [ "$enabled" = 1 ]; }; then
|
||||
if ! honk_lifecycle_start 30; then
|
||||
repair_rollback_ok=1
|
||||
honk_lifecycle_stop 30 || repair_rollback_ok=0
|
||||
if [ "$repair_rollback_ok" = 1 ]; then restore_system || repair_rollback_ok=0; fi
|
||||
restore_old_runtime || repair_rollback_ok=0
|
||||
if [ "$repair_rollback_ok" = 1 ]; then
|
||||
rm -rf "$repair_dir"
|
||||
echo '{"ok":false,"message":"repair_restart_failed_rolled_back"}'
|
||||
else
|
||||
logger -t honk "repair restart rollback failed; snapshot retained at $repair_dir"
|
||||
echo '{"ok":false,"message":"repair_restart_and_rollback_failed_snapshot_retained"}'
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
system_written_hash=""
|
||||
rm -rf "$repair_dir"
|
||||
echo '{"ok":true,"message":"system_config_repaired"}'
|
||||
}
|
||||
|
||||
api_discovery() {
|
||||
curl -fsS --noproxy '*' --connect-timeout 1 --max-time 3 "$1/api" -o "$2"
|
||||
}
|
||||
|
||||
mock_start() {
|
||||
mock_config=$1
|
||||
mock_log=$2
|
||||
DAE_LOCATION_ASSET=/usr/share/v2ray "$HONK_CORE" --config "$mock_config" --mock-ebpf > "$mock_log" 2>&1 &
|
||||
mock_pid=$!
|
||||
mock_identity=""
|
||||
attempt=0
|
||||
while [ "$attempt" -lt 20 ]; do
|
||||
if ! kill -0 "$mock_pid" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
if api_discovery "$3" "$4" 2>/dev/null; then
|
||||
mock_identity=$(tr '\000' ' ' < "/proc/$mock_pid/cmdline" 2>/dev/null)
|
||||
case "$mock_identity" in
|
||||
*"$mock_config"*--mock-ebpf*)
|
||||
[ "$(readlink -f "/proc/$mock_pid/exe" 2>/dev/null)" = "$HONK_CORE" ] && return 0
|
||||
;;
|
||||
esac
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
mock_stop() {
|
||||
[ -n "$mock_pid" ] || return 0
|
||||
[ -r "/proc/$mock_pid/cmdline" ] || { mock_pid=""; return 0; }
|
||||
current=$(tr '\000' ' ' < "/proc/$mock_pid/cmdline" 2>/dev/null)
|
||||
case "$current" in
|
||||
*"$mock_config"*--mock-ebpf*)
|
||||
[ "$(readlink -f "/proc/$mock_pid/exe" 2>/dev/null)" = "$HONK_CORE" ] || return 1
|
||||
kill -TERM "$mock_pid" 2>/dev/null || return 1
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
attempt=0
|
||||
while kill -0 "$mock_pid" 2>/dev/null && [ "$attempt" -lt 10 ]; do sleep 1; attempt=$((attempt + 1)); done
|
||||
if kill -0 "$mock_pid" 2>/dev/null; then return 1; fi
|
||||
wait "$mock_pid" 2>/dev/null || true
|
||||
mock_pid=""
|
||||
}
|
||||
|
||||
post_credentials() {
|
||||
endpoint=$1
|
||||
response_file=$2
|
||||
json_init
|
||||
json_add_string username "$username"
|
||||
json_add_string password "$password"
|
||||
json_dump | curl -sS --noproxy '*' --connect-timeout 1 --max-time 5 -o "$response_file" -w '%{http_code}' \
|
||||
-H 'Content-Type: application/json' --data-binary @- "$endpoint"
|
||||
}
|
||||
|
||||
initialize_preflight() {
|
||||
load_settings
|
||||
[ "$enabled" = 0 ] && [ "$initialized" = 0 ] || { echo '{"ok":false,"message":"disable_service_before_initialization"}'; return 1; }
|
||||
[ "$config_file" = /etc/honk/config.dae ] || { echo '{"ok":false,"message":"invalid_config_path"}'; return 1; }
|
||||
[ -r /usr/share/doona/index.html ] || { echo '{"ok":false,"message":"ui_assets_missing"}'; return 1; }
|
||||
if [ "$1" = read_credentials ]; then
|
||||
read_credentials || { echo '{"ok":false,"message":"invalid_credentials"}'; return 1; }
|
||||
fi
|
||||
[ -x "$HONK_CORE" ] || { echo '{"ok":false,"message":"core_missing"}'; return 1; }
|
||||
command -v curl >/dev/null 2>&1 || { echo '{"ok":false,"message":"curl_missing"}'; return 1; }
|
||||
pidof honk-core >/dev/null 2>&1 && { echo '{"ok":false,"message":"core_already_running"}'; return 1; }
|
||||
no_honk_resources || { echo '{"ok":false,"message":"honk_resources_already_exist"}'; return 1; }
|
||||
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
|
||||
check_network || { echo '{"ok":false,"message":"lan_network_unavailable"}'; return 1; }
|
||||
network_settings || { echo '{"ok":false,"message":"lan_ipv4_unavailable"}'; return 1; }
|
||||
check_config || { echo '{"ok":false,"message":"invalid_package_config"}'; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
|
||||
. /usr/share/libubox/jshn.sh || return 1
|
||||
initialize_preflight read_credentials || return 1
|
||||
honk_lock_acquire || { echo '{"ok":false,"message":"operation_in_progress"}'; return 1; }
|
||||
trap 'honk_lock_release >/dev/null 2>&1' 0
|
||||
initialize_preflight || return 1
|
||||
|
||||
umask 077
|
||||
temp_dir=$(mktemp -d /tmp/honk-init.XXXXXX) || { echo '{"ok":false,"message":"temporary_storage_unavailable"}'; return 1; }
|
||||
mock_pid=""
|
||||
trap cleanup_initialize 0
|
||||
trap 'exit 1' HUP INT TERM
|
||||
old_system=$temp_dir/system.dae
|
||||
cp -p /etc/honk/system.dae "$old_system" || { echo '{"ok":false,"message":"system_config_backup_failed"}'; return 1; }
|
||||
original_system_hash=$(sha256sum "$old_system" | awk '{print $1}')
|
||||
system_written_hash=$original_system_hash
|
||||
ensure_loopback_system || { echo '{"ok":false,"message":"loopback_listener_write_failed"}'; return 1; }
|
||||
mock_config=$temp_dir/mock.dae
|
||||
mock_log=$temp_dir/core.log
|
||||
response_file=$temp_dir/response.json
|
||||
cat > "$mock_config" <<EOF
|
||||
global {
|
||||
wan_interface: auto
|
||||
lan_interface: ''
|
||||
data_dir: '/etc/honk'
|
||||
nfqueue_enable: false
|
||||
store_subscribe: false
|
||||
}
|
||||
routing {
|
||||
fallback: direct
|
||||
}
|
||||
experimental {
|
||||
native_api {
|
||||
enabled: true
|
||||
listen: '127.0.0.1:$listen_port'
|
||||
password_auth: true
|
||||
allow_anonymous_loopback: false
|
||||
config_write: true
|
||||
}
|
||||
}
|
||||
EOF
|
||||
chmod 0600 "$mock_config" "$mock_log" 2>/dev/null || true
|
||||
loopback_url="http://127.0.0.1:$listen_port"
|
||||
if ! mock_start "$mock_config" "$mock_log" "$loopback_url" "$response_file"; then
|
||||
echo '{"ok":false,"message":"mock_core_start_failed"}'
|
||||
return 1
|
||||
fi
|
||||
setup_required=$(jsonfilter -i "$response_file" -e '@.auth.setup_required' 2>/dev/null)
|
||||
case "$setup_required" in
|
||||
true)
|
||||
status=$(post_credentials "$loopback_url/api/v1/auth/setup" "$response_file")
|
||||
[ "$status" = 201 ] || { echo '{"ok":false,"message":"administrator_setup_failed"}'; return 1; }
|
||||
;;
|
||||
false)
|
||||
status=$(post_credentials "$loopback_url/api/v1/auth/login" "$response_file")
|
||||
[ "$status" = 200 ] || { echo '{"ok":false,"message":"administrator_exists_reuse_credentials_or_reset_state"}'; return 1; }
|
||||
;;
|
||||
*) echo '{"ok":false,"message":"native_api_setup_state_unavailable"}'; return 1 ;;
|
||||
esac
|
||||
api_discovery "$loopback_url" "$response_file" || { echo '{"ok":false,"message":"setup_verification_failed"}'; return 1; }
|
||||
setup_required=$(jsonfilter -i "$response_file" -e '@.auth.setup_required' 2>/dev/null)
|
||||
[ "$setup_required" = false ] || { echo '{"ok":false,"message":"setup_verification_failed"}'; return 1; }
|
||||
mock_stop || { echo '{"ok":false,"message":"mock_core_stop_failed"}'; return 1; }
|
||||
if ! mock_start "$mock_config" "$mock_log" "$loopback_url" "$response_file"; then
|
||||
echo '{"ok":false,"message":"mock_core_restart_failed"}'
|
||||
return 1
|
||||
fi
|
||||
api_discovery "$loopback_url" "$response_file" || { echo '{"ok":false,"message":"persistence_verification_failed"}'; return 1; }
|
||||
setup_required=$(jsonfilter -i "$response_file" -e '@.auth.setup_required' 2>/dev/null)
|
||||
[ "$setup_required" = false ] || { echo '{"ok":false,"message":"persistence_verification_failed"}'; return 1; }
|
||||
status=$(post_credentials "$loopback_url/api/v1/auth/login" "$response_file")
|
||||
[ "$status" = 200 ] || { echo '{"ok":false,"message":"persistence_login_failed"}'; return 1; }
|
||||
mock_stop || { echo '{"ok":false,"message":"mock_core_stop_failed"}'; return 1; }
|
||||
|
||||
write_runtime_system "$lan_device" "$lan_ip" || { restore_system; echo '{"ok":false,"message":"system_config_write_failed"}'; return 1; }
|
||||
lan_url="http://$lan_ip:$listen_port"
|
||||
if ! mock_start /etc/honk/config.dae "$mock_log" "$lan_url" "$response_file"; then
|
||||
restore_system
|
||||
echo '{"ok":false,"message":"generated_config_invalid_restored_loopback"}'
|
||||
return 1
|
||||
fi
|
||||
api_discovery "$lan_url" "$response_file" || {
|
||||
mock_stop >/dev/null 2>&1
|
||||
restore_system
|
||||
echo '{"ok":false,"message":"lan_api_verification_failed_restored_loopback"}'
|
||||
return 1
|
||||
}
|
||||
mock_stop || {
|
||||
restore_system
|
||||
echo '{"ok":false,"message":"mock_core_stop_failed_restored_loopback"}'
|
||||
return 1
|
||||
}
|
||||
uci set honk.main.initialized='1' && uci commit honk || {
|
||||
restore_system
|
||||
uci set honk.main.initialized='0' >/dev/null 2>&1
|
||||
uci commit honk >/dev/null 2>&1
|
||||
echo '{"ok":false,"message":"uci_commit_failed_restored_loopback"}'
|
||||
return 1
|
||||
}
|
||||
system_written_hash=""
|
||||
password=""
|
||||
username=""
|
||||
echo '{"ok":true,"message":"administrator_initialized"}'
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
check)
|
||||
preflight
|
||||
;;
|
||||
diagnose)
|
||||
diagnose
|
||||
;;
|
||||
preflight)
|
||||
prepare_runtime
|
||||
;;
|
||||
local-direct)
|
||||
load_settings
|
||||
sync_local_direct
|
||||
;;
|
||||
initialize)
|
||||
initialize
|
||||
;;
|
||||
settings)
|
||||
settings
|
||||
;;
|
||||
repair)
|
||||
repair
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 {check|diagnose|preflight|local-direct|initialize|settings|repair}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -1,14 +0,0 @@
|
||||
config honk 'main'
|
||||
option enabled '0'
|
||||
option boot_enabled '0'
|
||||
option initialized '0'
|
||||
option config_file '/etc/honk/config.dae'
|
||||
option listen_port '9527'
|
||||
option clash_api_enabled '0'
|
||||
option clash_api_port '9090'
|
||||
option clash_api_secret ''
|
||||
option lan_network 'lan'
|
||||
option geoip_url ''
|
||||
option geosite_url ''
|
||||
option geo_auto '0'
|
||||
option geo_auto_freq 'daily'
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
START=95
|
||||
STOP=10
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
HONK_CONTROL=/usr/share/honk/control.sh
|
||||
HONK_CONFIG=/etc/honk/config.dae
|
||||
|
||||
boot() {
|
||||
# 开机自启内置:只要服务是"已启用 + 已初始化"就起来,
|
||||
# 用户主动停掉(enabled=0)后重启也不会被强行拉起。
|
||||
start
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled initialized config_file ready_token ready_hash ready_runtime ready_api_enabled ready_api_listen ready_clash_api_listen ready_clash_api_secret_set old_ready_token
|
||||
config_load honk
|
||||
config_get_bool enabled main enabled 0
|
||||
config_get_bool initialized main initialized 0
|
||||
[ "$enabled" -eq 1 ] && [ "$initialized" -eq 1 ] || {
|
||||
logger -t honk "refusing to start before explicit initialization and enablement"
|
||||
return 1
|
||||
}
|
||||
config_get config_file main config_file "$HONK_CONFIG"
|
||||
[ "$config_file" = "$HONK_CONFIG" ] || {
|
||||
logger -t honk "refusing non-default config path"
|
||||
return 1
|
||||
}
|
||||
"$HONK_CONTROL" preflight || return 1
|
||||
"$HONK_CONTROL" local-direct || logger -t honk "local-direct sync skipped"
|
||||
unset HONK_MAINT_CAP
|
||||
|
||||
# 所有配置都走 readiness 标记:状态页据此判断真正就绪,而不是只看进程在不在。
|
||||
ready_token=$(cat /proc/sys/kernel/random/uuid 2>/dev/null) || return 1
|
||||
case "$ready_token" in *[!0-9a-f-]*|'') return 1 ;; esac
|
||||
ready_hash=$(sha256sum "$HONK_CONFIG" 2>/dev/null | awk '{print $1}') || return 1
|
||||
case "$ready_hash" in *[!0-9a-f]*|'') return 1 ;; esac
|
||||
ready_runtime=$(/usr/bin/honk-core config validate --config "$HONK_CONFIG" 2>/dev/null) || return 1
|
||||
ready_api_enabled=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.native_api_enabled' 2>/dev/null)
|
||||
ready_api_listen=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.native_api_listen' 2>/dev/null)
|
||||
ready_clash_api_listen=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.clash_api_listen' 2>/dev/null)
|
||||
ready_clash_api_secret_set=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.clash_api_secret_set' 2>/dev/null)
|
||||
case "$ready_api_enabled" in true|false) ;; *) return 1 ;; esac
|
||||
case "$ready_api_listen" in *[!A-Za-z0-9:._-]*) return 1 ;; esac
|
||||
case "$ready_clash_api_listen" in *[!A-Za-z0-9:._-]*) return 1 ;; esac
|
||||
case "$ready_clash_api_secret_set" in true|false) ;; *) return 1 ;; esac
|
||||
mkdir -p /var/run/honk || return 1
|
||||
chmod 0700 /var/run/honk || return 1
|
||||
if [ -f /var/run/honk/readiness ] && [ ! -L /var/run/honk/readiness ]; then
|
||||
read -r old_ready_token _ < /var/run/honk/readiness
|
||||
case "$old_ready_token" in ''|*[!0-9a-f-]*) ;; *) rm -f "/var/run/honk/core-ready-$old_ready_token" ;; esac
|
||||
fi
|
||||
printf '%s %s\n' "$ready_token" "$ready_hash" > /var/run/honk/readiness || return 1
|
||||
chmod 0600 /var/run/honk/readiness || return 1
|
||||
procd_open_instance main
|
||||
procd_set_param command /usr/bin/honk-core --config "$HONK_CONFIG" --disable-timestamp
|
||||
# One env call, no empty values: procd drops the whole list when a KEY has an
|
||||
# empty value, which silently stripped the readiness hash the core requires.
|
||||
ready_env="DAE_LOCATION_ASSET=/usr/share/v2ray"
|
||||
ready_env="$ready_env HONK_READY_TOKEN=$ready_token HONK_READY_CONFIG_HASH=$ready_hash HONK_READY_API_ENABLED=$ready_api_enabled HONK_READY_CLASH_API_SECRET_SET=$ready_clash_api_secret_set"
|
||||
[ -n "$ready_api_listen" ] && ready_env="$ready_env HONK_READY_API_LISTEN=$ready_api_listen"
|
||||
[ -n "$ready_clash_api_listen" ] && ready_env="$ready_env HONK_READY_CLASH_API_LISTEN=$ready_clash_api_listen"
|
||||
# shellcheck disable=SC2086
|
||||
procd_set_param env $ready_env
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn 3600 5 5
|
||||
procd_set_param term_timeout 20
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
# honk-core has no hot-reload API wired here; a reload is an explicit
|
||||
# restart so existing connections are cycled. The UI only exposes
|
||||
# restart, this exists so manual `/etc/init.d/honk reload` is defined.
|
||||
logger -t honk "reload requested, restarting (no hot-reload)"
|
||||
restart
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export PATH
|
||||
umask 077
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
JOBS=/tmp/honk-jobs
|
||||
MAINT=/usr/share/honk/maintenance.sh
|
||||
|
||||
write_status() {
|
||||
status_state=$1
|
||||
status_phase=$2
|
||||
status_ok=$3
|
||||
status_message=$4
|
||||
# jshn 的布尔只认 1/0,传 "true" 会被写进状态文件成 false。
|
||||
case "$status_ok" in true|1) status_ok=1 ;; *) status_ok=0 ;; esac
|
||||
status_tmp=$job_dir/status.new
|
||||
{
|
||||
json_init
|
||||
json_add_string state "$status_state"
|
||||
json_add_string phase "$status_phase"
|
||||
json_add_boolean ok "$status_ok"
|
||||
json_add_string message "$status_message"
|
||||
json_dump
|
||||
} > "$status_tmp" || return 1
|
||||
chmod 0600 "$status_tmp" || return 1
|
||||
mv -f "$status_tmp" "$job_dir/status" || return 1
|
||||
}
|
||||
|
||||
job_id=$1
|
||||
operation=$2
|
||||
case "$job_id" in
|
||||
*[!0-9a-f]*|'') exit 2 ;;
|
||||
esac
|
||||
[ "${#job_id}" -eq 32 ] || exit 2
|
||||
case "$operation" in backup|restore|reset|update_check|update_apply) ;; *) exit 2 ;; esac
|
||||
|
||||
job_dir=$JOBS/$job_id
|
||||
[ -d "$JOBS" ] && [ ! -L "$JOBS" ] && [ -d "$job_dir" ] && [ ! -L "$job_dir" ] || exit 2
|
||||
[ "$(stat -c '%u:%a' "$JOBS" 2>/dev/null)" = 0:700 ] && [ "$(stat -c '%u:%a' "$job_dir" 2>/dev/null)" = 0:700 ] || exit 2
|
||||
[ "$(id -u)" = 0 ] || exit 2
|
||||
write_status running preparing true preparing || exit 1
|
||||
: > "$job_dir/output" || exit 1
|
||||
chmod 0600 "$job_dir/output" || exit 1
|
||||
# stderr 单独存:混进 output 会让结果 JSON 解析失败,报成“操作失败”。
|
||||
: > "$job_dir/stderr" || exit 1
|
||||
chmod 0600 "$job_dir/stderr" || exit 1
|
||||
if [ "$operation" = update_apply ]; then
|
||||
/usr/share/honk/update.sh apply > "$job_dir/output" 2> "$job_dir/stderr" &
|
||||
elif [ "$operation" = update_check ]; then
|
||||
/usr/share/honk/update.sh check > "$job_dir/output" 2> "$job_dir/stderr" &
|
||||
else
|
||||
"$MAINT" "$operation" > "$job_dir/output" 2> "$job_dir/stderr" &
|
||||
fi
|
||||
maint_pid=$!
|
||||
logger -t honk-maintenance "$operation started" 2>/dev/null || :
|
||||
# Total watchdog: never poll forever if maintenance.sh hangs.
|
||||
_poll_deadline=$(($(date +%s) + 1800))
|
||||
while kill -0 "$maint_pid" 2>/dev/null; do
|
||||
if [ "$(date +%s)" -ge "$_poll_deadline" ]; then
|
||||
logger -t honk-maintenance "$operation timed out after 1800s, killing $maint_pid" 2>/dev/null || :
|
||||
kill -9 "$maint_pid" 2>/dev/null || :
|
||||
break
|
||||
fi
|
||||
journal_phase=$(sed -n 's/^phase=//p' /etc/.honk-maintenance/journal 2>/dev/null | sed -n '1p')
|
||||
if [ "$operation" = update_apply ]; then
|
||||
update_phase=$(cat /etc/.honk-update/phase 2>/dev/null)
|
||||
case "$update_phase" in
|
||||
downloading) status_phase=downloading ;;
|
||||
verifying) status_phase=validating ;;
|
||||
stopping) status_phase=stopping ;;
|
||||
installing) status_phase=applying ;;
|
||||
starting) status_phase=starting ;;
|
||||
*) status_phase=preparing ;;
|
||||
esac
|
||||
elif [ "$operation" = update_check ]; then
|
||||
status_phase=checking_feed
|
||||
else case "$journal_phase" in
|
||||
prepared) status_phase=preparing ;;
|
||||
stopped) status_phase=stopping ;;
|
||||
old_moved|new_installed) status_phase=applying ;;
|
||||
uci_applied) status_phase=validating ;;
|
||||
service_started) status_phase=starting ;;
|
||||
rollback_old_start) status_phase=rolling_back ;;
|
||||
committed) status_phase=committing ;;
|
||||
*) status_phase=preparing ;;
|
||||
esac; fi
|
||||
write_status running "$status_phase" true "$status_phase" || :
|
||||
sleep 1
|
||||
done
|
||||
wait "$maint_pid"
|
||||
operation_rc=$?
|
||||
json_load "$(cat "$job_dir/output" 2>/dev/null)" 2>/dev/null || {
|
||||
if [ -e /etc/.honk-maintenance/journal ] || [ -L /etc/.honk-maintenance/journal ] || [ -d /run/honk.lock ]; then
|
||||
write_status rollback_required recovery false maintenance_recovery_required
|
||||
else
|
||||
write_status failed complete false operation_failed
|
||||
fi
|
||||
logger -t honk-maintenance "$operation failed (invalid result)" 2>/dev/null || :
|
||||
exit 1
|
||||
}
|
||||
json_get_var operation_ok ok
|
||||
json_get_var operation_message message
|
||||
if [ "$operation_rc" -eq 0 ] && [ "$operation_ok" = 1 ]; then
|
||||
if [ "$operation" = update_check ]; then
|
||||
json_add_string state succeeded
|
||||
json_add_string phase complete
|
||||
json_add_string job_id "$job_id"
|
||||
json_dump > "$job_dir/status.new" && chmod 0600 "$job_dir/status.new" && mv -f "$job_dir/status.new" "$job_dir/status" || exit 1
|
||||
logger -t honk-maintenance "$operation completed" 2>/dev/null || :
|
||||
exit 0
|
||||
fi
|
||||
write_status succeeded complete true "$operation_message"
|
||||
logger -t honk-maintenance "$operation completed" 2>/dev/null || :
|
||||
exit 0
|
||||
fi
|
||||
case "$operation_message" in
|
||||
*rollback_failed*|*recovery_required*|*snapshot_retained*)
|
||||
write_status rollback_required recovery false "$operation_message"
|
||||
logger -t honk-maintenance "$operation requires recovery" 2>/dev/null || :
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
write_status failed complete false "${operation_message:-operation_failed}"
|
||||
logger -t honk-maintenance "$operation failed" 2>/dev/null || :
|
||||
exit 1
|
||||
@@ -1,282 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
HONK_LIFECYCLE_CORE=/usr/bin/honk-core
|
||||
|
||||
# 字段归属:自带 global/experimental → file;只 include system.dae(且写法规范)→ system;
|
||||
# 其余一律 unknown。写法不规范时不猜,直接拒绝。
|
||||
config_owner() {
|
||||
config_owner_file=${1:-/etc/honk/config.dae}
|
||||
[ -f "$config_owner_file" ] && [ ! -L "$config_owner_file" ] || { echo unknown; return 0; }
|
||||
own=$(awk '/^[[:space:]]*(global|experimental)[[:space:]]*\{/ { print "1"; exit }' "$config_owner_file")
|
||||
inc=$(awk '
|
||||
{
|
||||
line = $0
|
||||
sub(/\r$/, "", line)
|
||||
if (!in_block) {
|
||||
# 只接受「include {」独占一行;同行带内容一律算不规范
|
||||
if (line ~ /^[[:space:]]*include[[:space:]]*\{[[:space:]]*$/) { in_block = 1; next }
|
||||
if (line ~ /^[[:space:]]*include[[:space:]]*\{/) { print "irregular"; next }
|
||||
next
|
||||
}
|
||||
if (line ~ /^[[:space:]]*\}[[:space:]]*$/) { in_block = 0; next }
|
||||
if (line ~ /^[[:space:]]*\}/ || line ~ /\{/) { print "irregular"; next }
|
||||
entry = line
|
||||
sub(/#.*/, "", entry)
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", entry)
|
||||
gsub(/["\047]/, "", entry)
|
||||
if (entry == "") next
|
||||
if (entry == "system.dae") print "system"
|
||||
else if (entry ~ /^[A-Za-z0-9._-]+$/) print "other"
|
||||
else print "irregular"
|
||||
}
|
||||
END { if (in_block) print "irregular" }
|
||||
' "$config_owner_file")
|
||||
has_system=
|
||||
has_bad=
|
||||
case "$inc" in *system*) has_system=1 ;; esac
|
||||
case "$inc" in *other*|*irregular*) has_bad=1 ;; esac
|
||||
if [ -n "$own" ]; then
|
||||
[ -z "$inc" ] && echo file || echo unknown
|
||||
elif [ -n "$has_bad" ]; then
|
||||
echo unknown
|
||||
elif [ -n "$has_system" ]; then
|
||||
echo system
|
||||
else
|
||||
echo unknown
|
||||
fi
|
||||
}
|
||||
|
||||
honk_lifecycle_procd_pid() { ubus call service list '{"name":"honk"}' 2>/dev/null | jsonfilter -e '@.honk.instances.main.pid' 2>/dev/null
|
||||
}
|
||||
|
||||
honk_lifecycle_procd_running() {
|
||||
ubus call service list '{"name":"honk"}' 2>/dev/null | jsonfilter -e '@.honk.instances.main.running' 2>/dev/null
|
||||
}
|
||||
|
||||
honk_lifecycle_managed_pid() {
|
||||
hlp_pid=$(honk_lifecycle_procd_pid)
|
||||
[ -n "$hlp_pid" ] && [ -r "/proc/$hlp_pid/cmdline" ] || return 1
|
||||
hlp_cmd=$(tr '\000' ' ' < "/proc/$hlp_pid/cmdline" 2>/dev/null) || return 1
|
||||
case "$hlp_cmd" in
|
||||
*/usr/bin/honk-core\ --config\ /etc/honk/config.dae\ --disable-timestamp*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
case "$hlp_cmd" in *--mock-ebpf*) return 1 ;; esac
|
||||
[ "$(readlink -f "/proc/$hlp_pid/exe" 2>/dev/null)" = "$HONK_LIFECYCLE_CORE" ] || return 1
|
||||
printf '%s\n' "$hlp_pid"
|
||||
}
|
||||
|
||||
honk_lifecycle_starttime() {
|
||||
[ -r "/proc/$1/stat" ] || return 1
|
||||
awk '{ sub(/^.*\) /, ""); split($0, fields, /[[:space:]]+/); if (fields[20] ~ /^[0-9]+$/) print fields[20]; else exit 1 }' "/proc/$1/stat" 2>/dev/null
|
||||
}
|
||||
|
||||
honk_lifecycle_listen() {
|
||||
[ -f /var/run/honk/readiness ] && [ ! -L /var/run/honk/readiness ] || return 1
|
||||
read -r hlp_token _ < /var/run/honk/readiness || return 1
|
||||
case "$hlp_token" in ''|*[!0-9a-f-]*) return 1 ;; esac
|
||||
sed -n 's/^api_listen=//p' "/var/run/honk/core-ready-$hlp_token" 2>/dev/null
|
||||
}
|
||||
|
||||
honk_lifecycle_parse_listen() {
|
||||
HONK_LIFECYCLE_LISTEN=$(honk_lifecycle_listen)
|
||||
if [ -z "$HONK_LIFECYCLE_LISTEN" ]; then
|
||||
HONK_LIFECYCLE_PORT=0
|
||||
return 0
|
||||
fi
|
||||
case "$HONK_LIFECYCLE_LISTEN" in *:*) ;; *) return 1 ;; esac
|
||||
HONK_LIFECYCLE_PORT=${HONK_LIFECYCLE_LISTEN##*:}
|
||||
case "$HONK_LIFECYCLE_LISTEN" in ''|*[!A-Za-z0-9:._-]*) return 1 ;; esac
|
||||
case "$HONK_LIFECYCLE_PORT" in ''|*[!0-9]*) return 1 ;; esac
|
||||
[ "$HONK_LIFECYCLE_PORT" -ge 1024 ] && [ "$HONK_LIFECYCLE_PORT" -le 65535 ]
|
||||
}
|
||||
|
||||
honk_lifecycle_ready_owned() {
|
||||
hlp_pid=$1
|
||||
[ -f /var/run/honk/readiness ] && [ ! -L /var/run/honk/readiness ] || return 1
|
||||
read -r hlp_token hlp_hash < /var/run/honk/readiness || return 1
|
||||
case "$hlp_token:$hlp_hash" in *[!0-9a-f:-]*|:*|:|*:) return 1 ;; esac
|
||||
hlp_marker=/var/run/honk/core-ready-$hlp_token
|
||||
[ -f "$hlp_marker" ] && [ ! -L "$hlp_marker" ] || return 1
|
||||
grep -Fqx "pid=$hlp_pid" "$hlp_marker" && \
|
||||
grep -Fqx "token=$hlp_token" "$hlp_marker" && \
|
||||
grep -Fqx "config_hash=$hlp_hash" "$hlp_marker" && \
|
||||
grep -Eq '^api_enabled=(true|false)$' "$hlp_marker" && \
|
||||
grep -Eq '^clash_api_secret_set=(true|false)$' "$hlp_marker"
|
||||
}
|
||||
|
||||
honk_lifecycle_port_open() {
|
||||
hlp_port=$1
|
||||
case "$hlp_port" in ''|*[!0-9]*) return 1 ;; esac
|
||||
awk -v port="$hlp_port" 'BEGIN { printf "%04X\n", port }' | {
|
||||
IFS= read -r hlp_hex || exit 1
|
||||
for hlp_table in /proc/net/tcp /proc/net/tcp6; do
|
||||
[ -r "$hlp_table" ] || continue
|
||||
awk -v port="$hlp_hex" 'NR > 1 && toupper($2) ~ (":" port "$") && $4 == "0A" { found = 1 } END { exit !found }' "$hlp_table" && exit 0
|
||||
done
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
honk_lifecycle_listener_owned() {
|
||||
hlp_pid=$1
|
||||
hlp_port=$2
|
||||
hlp_hex=$(awk -v port="$hlp_port" 'BEGIN { printf "%04X", port }') || return 1
|
||||
for hlp_table in /proc/net/tcp /proc/net/tcp6; do
|
||||
[ -r "$hlp_table" ] || continue
|
||||
for hlp_inode in $(awk -v port="$hlp_hex" 'NR > 1 && toupper($2) ~ (":" port "$") && $4 == "0A" { print $10 }' "$hlp_table"); do
|
||||
for hlp_fd in /proc/"$hlp_pid"/fd/*; do
|
||||
[ "$(readlink "$hlp_fd" 2>/dev/null)" = "socket:[$hlp_inode]" ] && return 0
|
||||
done
|
||||
done
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
honk_lifecycle_api_owned() {
|
||||
hlp_pid=$1
|
||||
honk_lifecycle_parse_listen || return 1
|
||||
[ "$HONK_LIFECYCLE_PORT" -gt 0 ] || return 1
|
||||
honk_lifecycle_listener_owned "$hlp_pid" "$HONK_LIFECYCLE_PORT" && \
|
||||
curl -fsS --noproxy '*' --connect-timeout 1 --max-time 2 "http://$HONK_LIFECYCLE_LISTEN/api" -o /dev/null 2>/dev/null
|
||||
}
|
||||
|
||||
honk_lifecycle_runtime_owned() {
|
||||
honk_lifecycle_ready_owned "$1"
|
||||
}
|
||||
|
||||
honk_lifecycle_resources_clear() {
|
||||
command -v ip >/dev/null 2>&1 || return 1
|
||||
for hlp_link in dae0 dae0peer; do
|
||||
ip link show "$hlp_link" >/dev/null 2>&1 && return 1
|
||||
done
|
||||
! ip netns list 2>/dev/null | awk '{print $1}' | grep -qx daens
|
||||
}
|
||||
|
||||
honk_lifecycle_drain() {
|
||||
hlp_timeout=${1:-30}
|
||||
while [ "$hlp_timeout" -gt 0 ]; do
|
||||
hlp_wait=0
|
||||
if [ -n "${HONK_LIFECYCLE_STOP_PID:-}" ] && \
|
||||
[ "$(honk_lifecycle_starttime "$HONK_LIFECYCLE_STOP_PID" 2>/dev/null)" = "$HONK_LIFECYCLE_STOP_STARTTIME" ]; then
|
||||
hlp_wait=1
|
||||
fi
|
||||
hlp_procd=$(honk_lifecycle_procd_pid)
|
||||
[ -z "$hlp_procd" ] || hlp_wait=1
|
||||
hlp_running=$(honk_lifecycle_procd_running)
|
||||
case "$hlp_running" in true|1) hlp_wait=1 ;; esac
|
||||
hlp_all=$(pidof honk-core 2>/dev/null)
|
||||
if [ -n "$hlp_all" ]; then
|
||||
for hlp_pid in $hlp_all; do
|
||||
if [ "$hlp_pid" != "${HONK_LIFECYCLE_STOP_PID:-}" ] || \
|
||||
[ "$(honk_lifecycle_starttime "$hlp_pid" 2>/dev/null)" != "${HONK_LIFECYCLE_STOP_STARTTIME:-}" ]; then
|
||||
echo "honk lifecycle: unmanaged honk-core process remains" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
hlp_wait=1
|
||||
fi
|
||||
honk_lifecycle_resources_clear || hlp_wait=1
|
||||
honk_lifecycle_parse_listen || { echo "honk lifecycle: invalid configured listener" >&2; return 1; }
|
||||
hlp_port=$HONK_LIFECYCLE_PORT
|
||||
if honk_lifecycle_port_open "$hlp_port"; then hlp_wait=1; fi
|
||||
if [ "$hlp_wait" = 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
hlp_timeout=$((hlp_timeout - 1))
|
||||
done
|
||||
echo "honk lifecycle: old process or network resources did not drain" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
honk_lifecycle_stop() {
|
||||
hlp_timeout=${1:-30}
|
||||
HONK_LIFECYCLE_STOP_PID=$(honk_lifecycle_managed_pid 2>/dev/null)
|
||||
if [ -n "$HONK_LIFECYCLE_STOP_PID" ]; then
|
||||
HONK_LIFECYCLE_STOP_STARTTIME=$(honk_lifecycle_starttime "$HONK_LIFECYCLE_STOP_PID") || return 1
|
||||
else
|
||||
[ -z "$(pidof honk-core 2>/dev/null)" ] || { echo "honk lifecycle: refusing to stop unmanaged honk-core" >&2; return 1; }
|
||||
[ -z "$(honk_lifecycle_procd_pid)" ] || { echo "honk lifecycle: procd instance is not the expected core" >&2; return 1; }
|
||||
case "$(honk_lifecycle_procd_running)" in true|1) echo "honk lifecycle: procd instance is still running" >&2; return 1 ;; esac
|
||||
HONK_LIFECYCLE_STOP_STARTTIME=
|
||||
fi
|
||||
if [ -n "$HONK_LIFECYCLE_STOP_PID" ]; then /etc/init.d/honk stop >/dev/null 2>&1 || :; fi
|
||||
honk_lifecycle_drain "$hlp_timeout"
|
||||
}
|
||||
|
||||
honk_lifecycle_start() {
|
||||
hlp_timeout=${1:-30}
|
||||
[ -z "$(pidof honk-core 2>/dev/null)" ] || { echo "honk lifecycle: refusing start while a core process exists" >&2; return 1; }
|
||||
[ -z "$(honk_lifecycle_procd_pid)" ] || { echo "honk lifecycle: procd instance has not stopped" >&2; return 1; }
|
||||
case "$(honk_lifecycle_procd_running)" in true|1) echo "honk lifecycle: procd instance is still running" >&2; return 1 ;; esac
|
||||
honk_lifecycle_resources_clear || { echo "honk lifecycle: network resources have not drained" >&2; return 1; }
|
||||
honk_lifecycle_parse_listen || { echo "honk lifecycle: invalid configured listener" >&2; return 1; }
|
||||
hlp_listen=$HONK_LIFECYCLE_LISTEN
|
||||
hlp_port=$HONK_LIFECYCLE_PORT
|
||||
if [ -n "${HONK_MAINT_CAP:-}" ]; then export HONK_MAINT_CAP; fi
|
||||
/etc/init.d/honk start >/dev/null 2>&1
|
||||
hlp_start_status=$?
|
||||
unset HONK_MAINT_CAP
|
||||
[ "$hlp_start_status" = 0 ] || { echo "honk lifecycle: init start failed" >&2; return 1; }
|
||||
hlp_stable=0
|
||||
hlp_pid=
|
||||
hlp_starttime=
|
||||
# A config the core cannot run crash-loops through procd: every attempt gets a
|
||||
# new PID, so a fixed timeout would burn the whole budget before the caller can
|
||||
# be told the start failed. Count distinct PIDs and give up early instead.
|
||||
hlp_seen_pid=
|
||||
hlp_attempts=0
|
||||
while [ "$hlp_timeout" -gt 0 ]; do
|
||||
hlp_new_pid=$(honk_lifecycle_managed_pid 2>/dev/null)
|
||||
if [ -n "$hlp_new_pid" ] && [ "$hlp_new_pid" != "$hlp_seen_pid" ]; then
|
||||
hlp_seen_pid=$hlp_new_pid
|
||||
hlp_attempts=$((hlp_attempts + 1))
|
||||
if [ "$hlp_attempts" -gt 3 ]; then
|
||||
echo "honk lifecycle: core keeps restarting, giving up" >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if [ -n "$hlp_new_pid" ]; then
|
||||
hlp_new_starttime=$(honk_lifecycle_starttime "$hlp_new_pid") || hlp_new_starttime=
|
||||
if [ -n "$hlp_new_starttime" ] && \
|
||||
{ [ -z "${HONK_LIFECYCLE_STOP_PID:-}" ] || [ "$hlp_new_pid:$hlp_new_starttime" != "$HONK_LIFECYCLE_STOP_PID:$HONK_LIFECYCLE_STOP_STARTTIME" ]; } && \
|
||||
{ honk_lifecycle_ready_owned "$hlp_new_pid"; }; then
|
||||
if [ "$hlp_pid:$hlp_starttime" = "$hlp_new_pid:$hlp_new_starttime" ]; then
|
||||
hlp_stable=$((hlp_stable + 1))
|
||||
else
|
||||
hlp_pid=$hlp_new_pid
|
||||
hlp_starttime=$hlp_new_starttime
|
||||
hlp_stable=1
|
||||
fi
|
||||
if [ "$hlp_stable" -ge 3 ]; then
|
||||
HONK_LIFECYCLE_STOP_PID=$hlp_new_pid
|
||||
HONK_LIFECYCLE_STOP_STARTTIME=$hlp_new_starttime
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
hlp_stable=0
|
||||
hlp_pid=
|
||||
hlp_starttime=
|
||||
fi
|
||||
else
|
||||
hlp_stable=0
|
||||
hlp_pid=
|
||||
hlp_starttime=
|
||||
fi
|
||||
sleep 1
|
||||
hlp_timeout=$((hlp_timeout - 1))
|
||||
done
|
||||
echo "honk lifecycle: new managed core did not become stable and own its API listener" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
honk_lifecycle_restart() {
|
||||
if [ -n "$(honk_lifecycle_managed_pid 2>/dev/null)" ]; then
|
||||
HONK_LIFECYCLE_WAS_RUNNING=1
|
||||
else
|
||||
[ -z "$(pidof honk-core 2>/dev/null)" ] || { echo "honk lifecycle: refusing unmanaged honk-core" >&2; return 1; }
|
||||
HONK_LIFECYCLE_WAS_RUNNING=0
|
||||
fi
|
||||
honk_lifecycle_stop "${1:-30}" && honk_lifecycle_start "${1:-30}"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
HONK_LOCK_DIR=/run/honk.lock
|
||||
|
||||
# PID alone is not enough to identify a process (PID reuse race). We store
|
||||
# "pid starttime" where starttime is field 22 of /proc/pid/stat.
|
||||
_honk_proc_starttime() {
|
||||
cut -d' ' -f22 "/proc/$1/stat" 2>/dev/null
|
||||
}
|
||||
|
||||
honk_lock_acquire() {
|
||||
mkdir "$HONK_LOCK_DIR" 2>/dev/null || return 1
|
||||
chmod 0700 "$HONK_LOCK_DIR" || { rmdir "$HONK_LOCK_DIR"; return 1; }
|
||||
printf '%s %s\n' "$$" "$(_honk_proc_starttime $$)" > "$HONK_LOCK_DIR/pid" || { rm -f "$HONK_LOCK_DIR/pid"; rmdir "$HONK_LOCK_DIR"; return 1; }
|
||||
}
|
||||
|
||||
honk_lock_is_stale() {
|
||||
[ -r "$HONK_LOCK_DIR/pid" ] || return 1
|
||||
read -r owner owner_start < "$HONK_LOCK_DIR/pid" || return 1
|
||||
case "$owner" in ''|*[!0-9]*) return 1 ;; esac
|
||||
# Dead if the PID is gone, or if it was reused by a newer process.
|
||||
! kill -0 "$owner" 2>/dev/null && return 0
|
||||
[ "$(_honk_proc_starttime "$owner")" = "$owner_start" ] || return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
honk_lock_release() {
|
||||
[ -r "$HONK_LOCK_DIR/pid" ] || return 1
|
||||
read -r owner owner_start < "$HONK_LOCK_DIR/pid" || return 1
|
||||
[ "$owner" = "$$" ] && [ "$owner_start" = "$(_honk_proc_starttime $$)" ] || return 1
|
||||
rm -f "$HONK_LOCK_DIR/pid" && rmdir "$HONK_LOCK_DIR"
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
requested=$1
|
||||
case "$requested" in all|debug|info|warn|error) ;; *) exit 2 ;; esac
|
||||
|
||||
awk -v requested="$requested" '
|
||||
function rank(level) {
|
||||
if (level == "DEBUG" || level == "TRACE") return 0
|
||||
if (level == "INFO" || level == "NOTICE") return 1
|
||||
if (level == "WARN" || level == "WARNING") return 2
|
||||
if (level == "ERROR" || level == "FATAL" || level == "CRIT" || level == "CRITICAL" || level == "ALERT" || level == "EMERG") return 3
|
||||
return -1
|
||||
}
|
||||
function from_syslog(header, token, parts, count, level) {
|
||||
count = split(header, parts, /[[:space:]]+/)
|
||||
for (i = 1; i <= count; i++) {
|
||||
token = parts[i]
|
||||
if (token ~ /^[[:alnum:]_-]+\.(debug|info|notice|warn|warning|err|error|crit|critical|alert|emerg)$/) {
|
||||
sub(/^.*\./, "", token)
|
||||
if (token == "err") token = "error"
|
||||
return rank(toupper(token))
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
{
|
||||
if (requested == "all" || requested == "debug") { print; next }
|
||||
line = $0
|
||||
header = line
|
||||
message = ""
|
||||
separator = index(line, "]: ")
|
||||
if (separator > 0) {
|
||||
header = substr(line, 1, separator + 1)
|
||||
message = substr(line, separator + 3)
|
||||
} else {
|
||||
separator = index(line, ": ")
|
||||
if (separator > 0) {
|
||||
header = substr(line, 1, separator - 1)
|
||||
message = substr(line, separator + 2)
|
||||
}
|
||||
}
|
||||
sub(/^[0-9][0-9][0-9][0-9]-[0-9T:.+Z-]+[[:space:]]+/, "", message)
|
||||
sub(/^[[:space:]]+/, "", message)
|
||||
if (match(message, /^(TRACE|DEBUG|INFO|NOTICE|WARN|WARNING|ERROR|FATAL|CRIT|CRITICAL|ALERT|EMERG)([[:space:]:]|$)/)) {
|
||||
level = message
|
||||
sub(/[[:space:]:].*$/, "", level)
|
||||
severity = rank(level)
|
||||
} else if (match(line, /^<[0-7]>/)) {
|
||||
priority = substr(line, 2, 1) + 0
|
||||
severity = priority <= 3 ? 3 : priority == 4 ? 2 : priority <= 6 ? 1 : 0
|
||||
} else {
|
||||
severity = from_syslog(header)
|
||||
}
|
||||
threshold = requested == "info" ? 1 : requested == "warn" ? 2 : 3
|
||||
if (severity >= threshold) print
|
||||
}'
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
honk_add_log_lines() {
|
||||
[ -s "$1" ] || return 0
|
||||
while IFS= read -r line; do
|
||||
json_add_string "" "$line"
|
||||
done < "$1"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec sed -E \
|
||||
-e 's#([[:alnum:]+.-]+://)[^[:space:]"<>]+#\1[redacted]#g' \
|
||||
-e 's/([Bb]earer[[:space:]]+)[^[:space:],;]+/\1[redacted]/g' \
|
||||
-e 's/([Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Tt][Oo][Kk][Ee][Nn]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Aa][Uu][Tt][Hh][Oo][Rr][Ii][Zz][Aa][Tt][Ii][Oo][Nn])[^:=]*[=:][[:space:]]*.*/\1=[redacted]/'
|
||||
@@ -1,22 +0,0 @@
|
||||
global {
|
||||
wan_interface: auto
|
||||
lan_interface: br-lan
|
||||
log_level: info
|
||||
dial_mode: domain
|
||||
allow_insecure: false
|
||||
auto_config_kernel_parameter: true
|
||||
data_dir: '/etc/honk'
|
||||
store_subscribe: true
|
||||
nfqueue_enable: true
|
||||
}
|
||||
|
||||
experimental {
|
||||
native_api {
|
||||
enabled: true
|
||||
listen: '127.0.0.1:9527'
|
||||
password_auth: true
|
||||
allow_anonymous_loopback: false
|
||||
config_write: true
|
||||
ui: '/usr/share/doona'
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export PATH
|
||||
umask 077
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
UPDATE_ROOT=/tmp/honk-update
|
||||
TRUST_ROOT=/etc/honk/update-trust
|
||||
FEED_FILE=$TRUST_ROOT/feed.url
|
||||
MANIFEST_KEY=$TRUST_ROOT/manifest.pub
|
||||
MANIFEST=$UPDATE_ROOT/manifest.json
|
||||
SIGNATURE=$UPDATE_ROOT/manifest.json.sig
|
||||
RECORDS=$UPDATE_ROOT/packages.tsv
|
||||
UPDATE_LOCK_OWNED=0
|
||||
CHECK_DIR=
|
||||
|
||||
update_lock_acquire() {
|
||||
. /usr/share/honk/lock.sh || return 1
|
||||
honk_lock_acquire || return 1
|
||||
UPDATE_LOCK_OWNED=1
|
||||
}
|
||||
|
||||
update_lock_release() {
|
||||
[ "$UPDATE_LOCK_OWNED" = 1 ] || return 0
|
||||
honk_lock_release >/dev/null 2>&1 || return 1
|
||||
UPDATE_LOCK_OWNED=0
|
||||
}
|
||||
|
||||
discard_check_dir() {
|
||||
case "$CHECK_DIR" in
|
||||
"$UPDATE_ROOT"/.check.*)
|
||||
[ -d "$CHECK_DIR" ] && [ ! -L "$CHECK_DIR" ] && rm -rf "$CHECK_DIR"
|
||||
;;
|
||||
esac
|
||||
CHECK_DIR=
|
||||
}
|
||||
|
||||
cleanup_update() {
|
||||
discard_check_dir
|
||||
update_lock_release || :
|
||||
}
|
||||
|
||||
emit_update() {
|
||||
json_init
|
||||
json_add_boolean ok "$1"
|
||||
json_add_boolean available "$2"
|
||||
json_add_boolean apply_enabled "$3"
|
||||
json_add_string message "$4"
|
||||
json_add_string reason "$5"
|
||||
[ -z "$6" ] || json_add_string version "$6"
|
||||
json_dump
|
||||
}
|
||||
|
||||
safe_file() {
|
||||
[ -f "$1" ] && [ ! -L "$1" ] && [ "$(stat -c '%u:%a' "$1" 2>/dev/null)" = "0:$2" ]
|
||||
}
|
||||
|
||||
prepare_update_root() {
|
||||
if [ -e "$UPDATE_ROOT" ] || [ -L "$UPDATE_ROOT" ]; then
|
||||
[ -d "$UPDATE_ROOT" ] && [ ! -L "$UPDATE_ROOT" ] && [ "$(stat -c '%u:%a' "$UPDATE_ROOT" 2>/dev/null)" = 0:700 ] || return 1
|
||||
else
|
||||
mkdir "$UPDATE_ROOT" && chmod 0700 "$UPDATE_ROOT" || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
read_trust() {
|
||||
[ -d "$TRUST_ROOT" ] && [ ! -L "$TRUST_ROOT" ] && [ "$(stat -c '%u:%a' "$TRUST_ROOT" 2>/dev/null)" = 0:700 ] || return 1
|
||||
safe_file "$FEED_FILE" 600 && safe_file "$MANIFEST_KEY" 600 || return 1
|
||||
[ "$(wc -c < "$FEED_FILE" | tr -d ' ')" -le 512 ] || return 1
|
||||
feed_url=$(cat "$FEED_FILE") || return 1
|
||||
[ -n "$feed_url" ] || return 1
|
||||
case "$feed_url" in *'?'*|*'#'*|*'@'*|*' '*|*'\'*|*'"'*|*"'"*) return 1 ;; esac
|
||||
echo "$feed_url" | grep -Eq '^https://[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._~/%+-]*)?$' || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
manifest_field() {
|
||||
jsonfilter -i "$MANIFEST" -e "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
installed_version() {
|
||||
awk -v wanted="$1" '
|
||||
$0 == "P:" wanted { found=1; next }
|
||||
found && /^V:/ { print substr($0, 3); exit }
|
||||
found && /^P:/ { exit }
|
||||
' /lib/apk/db/installed 2>/dev/null
|
||||
}
|
||||
|
||||
validate_manifest() {
|
||||
validate_url=$1
|
||||
validate_json=$(cat "$MANIFEST") || return 1
|
||||
json_load "$validate_json" 2>/dev/null || return 1
|
||||
json_get_type validate_packages_type packages
|
||||
[ "$validate_packages_type" = array ] || return 1
|
||||
json_select packages || return 1
|
||||
json_get_keys validate_package_keys
|
||||
[ "$(printf '%s\n' "$validate_package_keys" | wc -w | tr -d ' ')" = 2 ] || return 1
|
||||
json_select .. || return 1
|
||||
[ "$(manifest_field '@.schema')" = 1 ] || return 1
|
||||
[ "$(manifest_field '@.product')" = openwrt-honk ] || return 1
|
||||
# Target must match this device: map uname -m to manifest arch names.
|
||||
case "$(uname -m)" in
|
||||
x86_64) _local_arch=x86_64 ;;
|
||||
aarch64) _local_arch=aarch64 ;;
|
||||
armv7l) _local_arch=armv7 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
[ "$(manifest_field '@.target.architecture')" = "$_local_arch" ] || return 1
|
||||
# OpenWrt version must match: 24.10 (opkg/ipk) or 25.12 (apk).
|
||||
_local_owrt=$(sed -n 's/^DISTRIB_RELEASE="\([0-9]*\.[0-9]*\).*/\1/p' /etc/openwrt_release 2>/dev/null | head -n 1)
|
||||
case "$_local_owrt" in 24.10|25.12) ;; *) return 1 ;; esac
|
||||
[ "$(manifest_field '@.target.openwrt')" = "$_local_owrt" ] || return 1
|
||||
case "$_local_owrt" in
|
||||
24.10) [ "$(manifest_field '@.target.package_format')" = ipk ] || return 1 ;;
|
||||
25.12) [ "$(manifest_field '@.target.package_format')" = apk ] || return 1 ;;
|
||||
esac
|
||||
[ "$(manifest_field '@.target.kernel_min')" = 6.12 ] || return 1
|
||||
[ "$(manifest_field '@.target.requires_btf')" = true ] || return 1
|
||||
[ "$(manifest_field '@.min_updater_api')" = 1 ] || return 1
|
||||
[ "$(manifest_field '@.state_schema')" = 2 ] || return 1
|
||||
# (arch and OpenWrt version already validated against local system above)
|
||||
[ -r /sys/kernel/btf/vmlinux ] || return 1
|
||||
kernel_version=$(uname -r | sed 's/-.*//')
|
||||
kernel_major=${kernel_version%%.*}
|
||||
kernel_minor=${kernel_version#*.}
|
||||
kernel_minor=${kernel_minor%%.*}
|
||||
case "$kernel_major:$kernel_minor" in *[!0-9:]*|:*) return 1 ;; esac
|
||||
[ "$kernel_major" -gt 6 ] || { [ "$kernel_major" = 6 ] && [ "$kernel_minor" -ge 12 ]; } || return 1
|
||||
validate_commit=$(manifest_field '@.core.commit')
|
||||
case "$validate_commit" in *[!0-9a-f]*|'') return 1 ;; esac
|
||||
[ "${#validate_commit}" -eq 40 ] || return 1
|
||||
validate_commit=$(manifest_field '@.doona.commit')
|
||||
case "$validate_commit" in *[!0-9a-f]*|'') return 1 ;; esac
|
||||
[ "${#validate_commit}" -eq 40 ] || return 1
|
||||
for validate_sha in \
|
||||
"$(manifest_field '@.core.artifact_sha256')" \
|
||||
"$(manifest_field '@.doona.patch_sha256')" \
|
||||
"$(manifest_field '@.doona.artifact_sha256')" \
|
||||
"$(manifest_field '@.sdk_sha256')"; do
|
||||
case "$validate_sha" in *[!0-9a-f]*|'') return 1 ;; esac
|
||||
[ "${#validate_sha}" -eq 64 ] || return 1
|
||||
done
|
||||
validate_version=$(manifest_field '@.packages[0].version')
|
||||
case "$validate_version" in ''|*[!A-Za-z0-9.+~-]*) return 1 ;; esac
|
||||
: > "$RECORDS" || return 1
|
||||
validate_seen=' '
|
||||
validate_available=0
|
||||
for validate_index in 0 1; do
|
||||
validate_name=$(manifest_field "@.packages[$validate_index].name")
|
||||
validate_version=$(manifest_field "@.packages[$validate_index].version")
|
||||
validate_arch=$(manifest_field "@.packages[$validate_index].arch")
|
||||
validate_filename=$(manifest_field "@.packages[$validate_index].filename")
|
||||
validate_size=$(manifest_field "@.packages[$validate_index].size")
|
||||
validate_sha=$(manifest_field "@.packages[$validate_index].sha256")
|
||||
validate_package_url=$(manifest_field "@.packages[$validate_index].url")
|
||||
case "$validate_name" in honk|luci-app-honk) ;; *) return 1 ;; esac
|
||||
case "$validate_seen" in *" $validate_name "*) return 1 ;; esac
|
||||
validate_seen="$validate_seen$validate_name "
|
||||
case "$validate_version" in ''|*[!A-Za-z0-9.+~-]*) return 1 ;; esac
|
||||
case "$validate_arch" in x86_64|aarch64|armv7|noarch) ;; *) return 1 ;; esac
|
||||
case "$validate_name:$validate_arch" in honk:x86_64|honk:aarch64|honk:armv7|luci-app-honk:x86_64|luci-app-honk:aarch64|luci-app-honk:armv7|luci-app-honk:noarch) ;; *) return 1 ;; esac
|
||||
[ "$validate_filename" = "$validate_name-$validate_version.apk" ] || [ "$validate_filename" = "${validate_name}_${validate_version}_${validate_arch}.ipk" ] || return 1
|
||||
case "$validate_filename" in *'/'*|*'..'*|*[!A-Za-z0-9.+_-]*) return 1 ;; esac
|
||||
case "$validate_size" in ''|*[!0-9]*) return 1 ;; esac
|
||||
[ "$validate_size" -gt 0 ] && [ "$validate_size" -le 134217728 ] || return 1
|
||||
case "$validate_sha" in *[!0-9a-f]*|'') return 1 ;; esac
|
||||
[ "${#validate_sha}" -eq 64 ] || return 1
|
||||
[ "$validate_package_url" = "${validate_url%/}/packages/$validate_filename" ] || return 1
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$validate_name" "$validate_version" "$validate_arch" "$validate_filename" "$validate_size" "$validate_sha" "$validate_package_url" >> "$RECORDS" || return 1
|
||||
[ "$(installed_version "$validate_name")" = "$validate_version" ] || validate_available=1
|
||||
done
|
||||
[ "$(wc -l < "$RECORDS" | tr -d ' ')" = 2 ] || return 1
|
||||
for validate_required in honk luci-app-honk; do
|
||||
case "$validate_seen" in *" $validate_required "*) ;; *) return 1 ;; esac
|
||||
done
|
||||
update_version=$(awk -F '\t' '$1 == "honk" { print $2; exit }' "$RECORDS")
|
||||
return 0
|
||||
}
|
||||
|
||||
verify_signed_manifest() {
|
||||
read_trust || return 2
|
||||
prepare_update_root || return 3
|
||||
CHECK_DIR=$(mktemp -d "$UPDATE_ROOT/.check.XXXXXX") || return 3
|
||||
check_dir=$CHECK_DIR
|
||||
chmod 0700 "$check_dir" || { discard_check_dir; return 3; }
|
||||
check_manifest=$check_dir/manifest.json
|
||||
check_signature=$check_dir/manifest.json.sig
|
||||
if ! curl -fLsS --proto '=https' --proto-redir '=https' --tlsv1.2 --connect-timeout 4 --max-time 12 --max-filesize 65536 \
|
||||
"${feed_url%/}/manifest.json" -o "$check_manifest" 2>/dev/null ||
|
||||
! curl -fLsS --proto '=https' --proto-redir '=https' --tlsv1.2 --connect-timeout 4 --max-time 8 --max-filesize 4096 \
|
||||
"${feed_url%/}/manifest.json.sig" -o "$check_signature" 2>/dev/null ||
|
||||
! usign -V -m "$check_manifest" -p "$MANIFEST_KEY" -x "$check_signature" >/dev/null 2>&1; then
|
||||
discard_check_dir
|
||||
return 4
|
||||
fi
|
||||
MANIFEST=$check_manifest
|
||||
RECORDS=$check_dir/packages.tsv
|
||||
if ! validate_manifest "$feed_url"; then
|
||||
discard_check_dir
|
||||
return 5
|
||||
fi
|
||||
cp "$check_manifest" "$UPDATE_ROOT/manifest.json.new" && cp "$check_signature" "$UPDATE_ROOT/manifest.json.sig.new" || { discard_check_dir; return 3; }
|
||||
cp "$RECORDS" "$UPDATE_ROOT/packages.tsv.new" || { discard_check_dir; return 3; }
|
||||
printf '%s\n' "$(date +%s)" > "$UPDATE_ROOT/checked-at.new" || { discard_check_dir; return 3; }
|
||||
chmod 0600 "$UPDATE_ROOT/manifest.json.new" "$UPDATE_ROOT/manifest.json.sig.new" "$UPDATE_ROOT/packages.tsv.new" "$UPDATE_ROOT/checked-at.new" || { discard_check_dir; return 3; }
|
||||
mv -f "$UPDATE_ROOT/manifest.json.new" "$UPDATE_ROOT/manifest.json" &&
|
||||
mv -f "$UPDATE_ROOT/manifest.json.sig.new" "$UPDATE_ROOT/manifest.json.sig" &&
|
||||
mv -f "$UPDATE_ROOT/packages.tsv.new" "$UPDATE_ROOT/packages.tsv" &&
|
||||
mv -f "$UPDATE_ROOT/checked-at.new" "$UPDATE_ROOT/checked-at" || { discard_check_dir; return 3; }
|
||||
discard_check_dir
|
||||
return 0
|
||||
}
|
||||
|
||||
update_check() {
|
||||
read_trust || { emit_update true false false no_release trusted_feed_unavailable; return 0; }
|
||||
update_lock_acquire || { emit_update false false false update_check_failed operation_in_progress; return 1; }
|
||||
verify_signed_manifest
|
||||
check_rc=$?
|
||||
update_lock_release
|
||||
case "$check_rc" in
|
||||
2) emit_update true false false no_release trusted_feed_unavailable; return 0 ;;
|
||||
4) emit_update false false false update_check_failed signed_manifest_unavailable; return 1 ;;
|
||||
3) emit_update false false false update_check_failed update_storage_unavailable; return 1 ;;
|
||||
5) emit_update false false false update_check_failed manifest_incompatible; return 1 ;;
|
||||
0) ;;
|
||||
*) emit_update false false false update_check_failed manifest_invalid; return 1 ;;
|
||||
esac
|
||||
if [ "$validate_available" = 1 ]; then
|
||||
emit_update true true false update_available rollback_tuple_unavailable "$update_version"
|
||||
else
|
||||
emit_update true false false no_update already_current "$update_version"
|
||||
fi
|
||||
}
|
||||
|
||||
update_gate() {
|
||||
read_trust || { emit_update true false false no_release trusted_feed_unavailable; return 0; }
|
||||
update_lock_acquire || { emit_update false false false update_check_failed operation_in_progress; return 1; }
|
||||
gate_ok=1
|
||||
gate_available=0
|
||||
gate_reason=
|
||||
prepare_update_root || { gate_ok=0; gate_reason=update_storage_unavailable; }
|
||||
if [ "$gate_ok" = 1 ]; then
|
||||
for gate_file in "$UPDATE_ROOT/manifest.json" "$UPDATE_ROOT/manifest.json.sig" "$UPDATE_ROOT/packages.tsv" "$UPDATE_ROOT/checked-at"; do
|
||||
safe_file "$gate_file" 600 || { gate_reason=signed_manifest_missing; break; }
|
||||
done
|
||||
fi
|
||||
if [ "$gate_ok" = 1 ] && [ -z "$gate_reason" ]; then
|
||||
checked_at=$(cat "$UPDATE_ROOT/checked-at")
|
||||
case "$checked_at" in ''|*[!0-9]*) gate_reason=signed_manifest_expired ;; esac
|
||||
if [ -z "$gate_reason" ] && [ $(( $(date +%s) - checked_at )) -gt 600 ]; then gate_reason=signed_manifest_expired; fi
|
||||
fi
|
||||
if [ "$gate_ok" = 1 ] && [ -z "$gate_reason" ]; then
|
||||
usign -V -m "$UPDATE_ROOT/manifest.json" -p "$MANIFEST_KEY" -x "$UPDATE_ROOT/manifest.json.sig" >/dev/null 2>&1 || { gate_ok=0; gate_reason=manifest_signature_invalid; }
|
||||
fi
|
||||
if [ "$gate_ok" = 1 ] && [ -z "$gate_reason" ]; then
|
||||
MANIFEST=$UPDATE_ROOT/manifest.json
|
||||
RECORDS=$UPDATE_ROOT/packages.tsv
|
||||
validate_manifest "$feed_url" || { gate_ok=0; gate_reason=manifest_incompatible; }
|
||||
fi
|
||||
gate_available=${validate_available:-0}
|
||||
gate_version=${update_version:-}
|
||||
update_lock_release
|
||||
if [ "$gate_ok" != 1 ]; then emit_update false false false update_check_failed "$gate_reason"; return 1; fi
|
||||
if [ -n "$gate_reason" ]; then emit_update true false false no_release "$gate_reason"; return 0; fi
|
||||
if [ "$gate_available" = 1 ]; then
|
||||
emit_update true true false update_available rollback_tuple_unavailable "$gate_version"
|
||||
else
|
||||
emit_update true false false no_update already_current "$gate_version"
|
||||
fi
|
||||
}
|
||||
|
||||
update_apply() {
|
||||
update_gate >/dev/null || { emit_update false false false update_apply_failed trusted_manifest_invalid; return 1; }
|
||||
[ "$validate_available" = 1 ] || { emit_update false false false update_apply_failed no_update; return 1; }
|
||||
# No previous signed package tuple or exercised downgrade path is available yet.
|
||||
# Keep the backend gate closed even when the update manifest itself is compatible.
|
||||
emit_update false false false update_apply_failed rollback_tuple_unavailable
|
||||
return 1
|
||||
}
|
||||
|
||||
trap cleanup_update EXIT
|
||||
trap 'exit 129' HUP
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
case "$1" in
|
||||
check) update_check ;;
|
||||
gate) update_gate ;;
|
||||
apply) update_apply ;;
|
||||
*) emit_update false false false update_check_failed invalid_action; exit 2 ;;
|
||||
esac
|
||||
@@ -1,70 +0,0 @@
|
||||
name: build-apk
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- 'dev-*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
SDK_DIR: /builder
|
||||
PKG_DIR_NAME: luci-app-adguardhome-tk
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
matrix:
|
||||
sdk: [x86-64-25.12.6]
|
||||
container:
|
||||
image: ghcr.io/openwrt/sdk:${{ matrix.sdk }}
|
||||
options: --user root
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 缓存依赖源码下载
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ env.SDK_DIR }}/dl
|
||||
key: adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-${{ hashFiles('Makefile', '.github/workflows/build-apk.yaml', 'scripts/*.sh') }}
|
||||
restore-keys: |
|
||||
adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-
|
||||
|
||||
- name: 准备 SDK
|
||||
run: bash scripts/prepare-sdk.sh
|
||||
|
||||
- name: 构建包
|
||||
run: runuser -u buildbot -- bash scripts/build-apk.sh
|
||||
|
||||
- name: 整理 APK 产物
|
||||
run: bash scripts/collect-apks.sh
|
||||
|
||||
- name: 上传 APK 压缩包
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: adguardhome-apks
|
||||
path: dist/*.apk
|
||||
archive: true
|
||||
if-no-files-found: error
|
||||
|
||||
- name: 发布到 Releases
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
prerelease: ${{ startsWith(github.ref_name, 'dev-') }}
|
||||
make_latest: 'false'
|
||||
fail_on_unmatched_files: true
|
||||
files: dist/*.apk
|
||||
@@ -1,2 +0,0 @@
|
||||
.DS_Store
|
||||
.local
|
||||
@@ -1,109 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2025 w9315273
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-adguardhome-tk
|
||||
PKG_VERSION:=2.6.0
|
||||
PKG_RELEASE:=3
|
||||
|
||||
PKG_MAINTAINER:=w9315273
|
||||
PKG_BUILD_DEPENDS:=luci-base/host
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/luci-app-adguardhome-tk
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=LuCI app for AdGuardHome
|
||||
PKG_MAINTAINER:=https://github.com/w9315273/luci-app-adguardhome
|
||||
PKGARCH:=all
|
||||
PROVIDES:=luci-app-adguardhome
|
||||
CONFLICTS:=luci-app-adguardhome
|
||||
DEPENDS:=+luci-base +rpcd-mod-ucode +uclient-fetch +jsonfilter +flock +ca-bundle +!(PACKAGE_libustream-openssl||PACKAGE_libustream-wolfssl):libustream-mbedtls +tar
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome-tk/description
|
||||
LuCI support for AdGuardHome (nftables-based DNS redirect).
|
||||
This package does NOT include the AdGuardHome core binary.
|
||||
endef
|
||||
|
||||
define Package/luci-i18n-adguardhome-tk-zh-cn
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
HIDDEN:=1
|
||||
TITLE:=AdGuardHome - Simplified Chinese translation
|
||||
PKGARCH:=all
|
||||
PROVIDES:=luci-i18n-adguardhome-zh-cn
|
||||
CONFLICTS:=luci-i18n-adguardhome-zh-cn
|
||||
DEPENDS:=luci-app-adguardhome-tk
|
||||
DEFAULT:=LUCI_LANG_zh_Hans
|
||||
endef
|
||||
|
||||
define Package/luci-i18n-adguardhome-tk-zh-cn/description
|
||||
Simplified Chinese translation for luci-app-adguardhome-tk.
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome-tk/conffiles
|
||||
/etc/config/AdGuardHome
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome-tk/install
|
||||
cp -pR ./root/* $(1)/
|
||||
endef
|
||||
|
||||
define Package/luci-i18n-adguardhome-tk-zh-cn/install
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
echo "uci set luci.languages.zh_cn='简体中文 (Simplified Chinese)'; uci commit luci" \
|
||||
> $(1)/etc/uci-defaults/luci-i18n-adguardhome-tk-zh-cn
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
|
||||
po2lmo ./po/zh-cn/AdGuardHome.po \
|
||||
$(1)/usr/lib/lua/luci/i18n/AdGuardHome.zh-cn.lmo
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome-tk/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
|
||||
/etc/init.d/AdGuardHome enable >/dev/null 2>&1
|
||||
|
||||
enable=$$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)
|
||||
if [ "$$enable" = "1" ]; then
|
||||
/etc/init.d/AdGuardHome reload
|
||||
fi
|
||||
|
||||
# Remove the former menu definition when upgrading.
|
||||
rm -f /usr/share/luci/menu.d/luci-app-adguardhome-js.json
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
rm -f /tmp/luci-modulecache/*
|
||||
/etc/init.d/rpcd reload
|
||||
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome-tk/prerm
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
/etc/init.d/AdGuardHome disable
|
||||
/etc/init.d/AdGuardHome stop 2>/dev/null || true
|
||||
|
||||
uci -q batch <<-EOF >/dev/null 2>&1
|
||||
delete ucitrack.@AdGuardHome[-1]
|
||||
commit ucitrack
|
||||
EOF
|
||||
fi
|
||||
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-adguardhome-tk))
|
||||
|
||||
$(eval $(call BuildPackage,luci-i18n-adguardhome-tk-zh-cn))
|
||||
@@ -1,35 +0,0 @@
|
||||
# AdGuard Home for LuCI
|
||||
|
||||
Manage AdGuard Home in OpenWrt's LuCI interface. LuCI pages are rewritten in JavaScript, with an rpcd + ucode backend and nftables-based DNS forwarding.
|
||||
|
||||
[简体中文](README.md) · [English](README.en.md)
|
||||
|
||||
## Features
|
||||
|
||||
- Service status and core updates
|
||||
- DNS handling through firewall forwarding, dnsmasq upstream, or port 53 replacement
|
||||
- YAML configuration editing, validation, and web login password changes
|
||||
- Runtime logs with reverse order, local timestamps, download, and a verbose logging toggle
|
||||
|
||||
> [!TIP]
|
||||
> **Quick installation**
|
||||
>
|
||||
> Run in the terminal of an OpenWrt router using the APK package manager:
|
||||
>
|
||||
> ```sh
|
||||
> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh
|
||||
> ```
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Basic settings
|
||||
|
||||

|
||||
|
||||
### Manual configuration
|
||||
|
||||

|
||||
|
||||
### Logs
|
||||
|
||||

|
||||
@@ -1,35 +0,0 @@
|
||||
# AdGuard Home for LuCI
|
||||
|
||||
在 OpenWrt 的 LuCI 界面中管理 AdGuard Home。使用 JavaScript 重写 LuCI 页面,后端采用 rpcd + ucode,并适配 nftables DNS 转发。
|
||||
|
||||
[简体中文](README.md) · [English](README.en.md)
|
||||
|
||||
## 功能
|
||||
|
||||
- 服务状态查看与核心更新
|
||||
- 防火墙转发、dnsmasq 上游、替代 dnsmasq 使用 53 端口等 DNS 接入方式
|
||||
- YAML 配置编辑与校验、网页登录密码修改
|
||||
- 运行日志查看,支持逆序、本地时间、下载和详细日志开关
|
||||
|
||||
> [!TIP]
|
||||
> **一键安装**
|
||||
>
|
||||
> 在使用 APK 包管理器的 OpenWrt 路由器终端执行:
|
||||
>
|
||||
> ```sh
|
||||
> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh zh-cn
|
||||
> ```
|
||||
|
||||
## 截图
|
||||
|
||||
### 基础设置
|
||||
|
||||

|
||||
|
||||
### 手动设置
|
||||
|
||||

|
||||
|
||||
### 日志
|
||||
|
||||

|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 140 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 227 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 227 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 169 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 164 KiB |
@@ -1,231 +0,0 @@
|
||||
#/cgi-bin/luci/admin/services/AdGuardHome
|
||||
msgid "Base Setting"
|
||||
msgstr "基础设置"
|
||||
|
||||
msgid "Log"
|
||||
msgstr "日志"
|
||||
|
||||
msgid "Basic Settings"
|
||||
msgstr "基础设置"
|
||||
|
||||
msgid "Manual Config"
|
||||
msgstr "手动设置"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Browser management port"
|
||||
msgstr "网页管理端口"
|
||||
|
||||
msgid "DNS handling mode"
|
||||
msgstr "DNS 接管方式"
|
||||
|
||||
msgid "Do not handle DNS"
|
||||
msgstr "不接管 DNS"
|
||||
|
||||
msgid "Run as dnsmasq upstream server"
|
||||
msgstr "作为 dnsmasq 的上游服务器"
|
||||
|
||||
msgid "Forward port 53 requests through the firewall"
|
||||
msgstr "通过防火墙转发 53 端口请求"
|
||||
|
||||
msgid "Use port 53 instead of dnsmasq"
|
||||
msgstr "使用 53 端口替代 dnsmasq"
|
||||
|
||||
msgid "Network devices excluded from forwarding"
|
||||
msgstr "不参与转发的网络设备"
|
||||
|
||||
msgid "DNS requests arriving on these network devices are not forwarded to AdGuardHome"
|
||||
msgstr "从这些网络设备进入的 DNS 请求不转发到 AdGuardHome"
|
||||
|
||||
msgid "Bin Path"
|
||||
msgstr "执行文件路径"
|
||||
|
||||
msgid "Config Path"
|
||||
msgstr "配置文件路径"
|
||||
|
||||
msgid "Work dir"
|
||||
msgstr "工作目录"
|
||||
|
||||
msgid "Runtime log file"
|
||||
msgstr "运行日志文件"
|
||||
|
||||
msgid "If set to \"syslog\", write to system log; if empty, disable file logging"
|
||||
msgstr "设为 \"syslog\" 时写入系统日志;留空则禁用文件日志"
|
||||
|
||||
msgid "Verbose log"
|
||||
msgstr "详细日志"
|
||||
|
||||
msgid "Upgrade Core"
|
||||
msgstr "更新核心"
|
||||
|
||||
msgid "Update core version"
|
||||
msgstr "更新核心版本"
|
||||
|
||||
msgid "Force update"
|
||||
msgstr "强制更新"
|
||||
|
||||
msgid "Check..."
|
||||
msgstr "检查中..."
|
||||
|
||||
msgid "Updated"
|
||||
msgstr "已更新"
|
||||
|
||||
msgid "Update failed"
|
||||
msgstr "更新失败"
|
||||
|
||||
msgid "Update in progress"
|
||||
msgstr "更新正在进行中"
|
||||
|
||||
msgid "Unable to start update"
|
||||
msgstr "无法启动更新"
|
||||
|
||||
msgid "Update status unavailable"
|
||||
msgstr "更新状态不可用"
|
||||
|
||||
msgid "AdGuardHome Web"
|
||||
msgstr "AdGuardHome 仪表盘"
|
||||
|
||||
msgid "Change browser management password"
|
||||
msgstr "修改网页登录密码"
|
||||
|
||||
msgid "New password"
|
||||
msgstr "新密码"
|
||||
|
||||
msgid "Change password"
|
||||
msgstr "修改密码"
|
||||
|
||||
msgid "is empty"
|
||||
msgstr "为空"
|
||||
|
||||
msgid "Changing password..."
|
||||
msgstr "正在修改密码..."
|
||||
|
||||
msgid "Password changed"
|
||||
msgstr "密码已修改"
|
||||
|
||||
msgid "Password change failed"
|
||||
msgstr "密码修改失败"
|
||||
|
||||
msgid "Core"
|
||||
msgstr "核心"
|
||||
|
||||
msgid "core error"
|
||||
msgstr "核心错误"
|
||||
|
||||
msgid "no core"
|
||||
msgstr "没有核心"
|
||||
|
||||
msgid "no config"
|
||||
msgstr "没有配置文件"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "Redirect"
|
||||
msgstr "重定向"
|
||||
|
||||
msgid "Redirected"
|
||||
msgstr "已重定向"
|
||||
|
||||
msgid "Not redirect"
|
||||
msgstr "未重定向"
|
||||
|
||||
msgid "Collecting data..."
|
||||
msgstr "获取数据中..."
|
||||
|
||||
msgid "Status unavailable"
|
||||
msgstr "状态不可用"
|
||||
|
||||
msgid "reverse"
|
||||
msgstr "逆序"
|
||||
|
||||
msgid "localtime"
|
||||
msgstr "本地时间"
|
||||
|
||||
msgid "Please add log path in config to enable log"
|
||||
msgstr "请在设置中填写日志路径以启用日志"
|
||||
|
||||
msgid "dellog"
|
||||
msgstr "删除日志"
|
||||
|
||||
msgid "download log"
|
||||
msgstr "下载日志"
|
||||
|
||||
msgid "Use template"
|
||||
msgstr "使用模板"
|
||||
|
||||
msgid "Reload Config"
|
||||
msgstr "重新载入配置"
|
||||
|
||||
msgid "Save & Apply"
|
||||
msgstr "保存并应用"
|
||||
|
||||
msgid "Unable to read config"
|
||||
msgstr "无法读取配置"
|
||||
|
||||
msgid "Unable to load editor"
|
||||
msgstr "无法加载编辑器"
|
||||
|
||||
msgid "Saving..."
|
||||
msgstr "正在保存..."
|
||||
|
||||
msgid "Saved"
|
||||
msgstr "已保存"
|
||||
|
||||
msgid "Save failed"
|
||||
msgstr "保存失败"
|
||||
|
||||
msgid "Core is missing; configuration cannot be validated before saving."
|
||||
msgstr "核心文件不存在,保存前无法校验配置。"
|
||||
|
||||
msgid "Saved without validation (core missing)."
|
||||
msgstr "已保存,但因核心文件不存在而未校验。"
|
||||
|
||||
msgid "Configuration validation failed."
|
||||
msgstr "配置校验失败。"
|
||||
|
||||
msgid "Service reload failed; previous config restored."
|
||||
msgstr "服务重载失败,已恢复原配置。"
|
||||
|
||||
msgid "Restore failed; backup at"
|
||||
msgstr "恢复失败,备份文件位于"
|
||||
|
||||
msgid "Config changed on disk; reload before saving."
|
||||
msgstr "磁盘上的配置已变化,请重新读取后再保存。"
|
||||
|
||||
msgid "Unable to read runtime log"
|
||||
msgstr "无法读取运行日志"
|
||||
|
||||
msgid "Switching"
|
||||
msgstr "正在切换"
|
||||
|
||||
msgid "Checking service…"
|
||||
msgstr "正在确认服务状态…"
|
||||
|
||||
msgid "Switch not completed; check the log"
|
||||
msgstr "切换尚未完成,请检查日志"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "已停用"
|
||||
|
||||
msgid "Unable to read switch status"
|
||||
msgstr "无法读取切换状态"
|
||||
|
||||
msgid "Forward port 53 requests through the firewall (recommended)"
|
||||
msgstr "通过防火墙转发 53 端口请求(推荐)"
|
||||
|
||||
msgid "Log settings"
|
||||
msgstr "日志设置"
|
||||
|
||||
msgid "Applying verbose logging…"
|
||||
msgstr "正在应用详细日志设置…"
|
||||
|
||||
msgid "Saved, but service reload failed; check the log"
|
||||
msgstr "已保存,但服务重载失败,请检查日志"
|
||||
|
||||
msgid "Unable to apply verbose logging"
|
||||
msgstr "无法应用详细日志设置"
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
config AdGuardHome 'AdGuardHome'
|
||||
option enabled '0'
|
||||
option httpport '3000'
|
||||
option redirect 'none'
|
||||
option configpath '/etc/AdGuardHome.yaml'
|
||||
option workdir '/tmp/AdGuardHome'
|
||||
option logfile '/tmp/AdGuardHome.log'
|
||||
option verbose '0'
|
||||
option binpath '/usr/bin/AdGuardHome/AdGuardHome'
|
||||
list wan_ifname 'eth0'
|
||||
@@ -1,653 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
|
||||
START=95
|
||||
STOP=01
|
||||
|
||||
CONFIGURATION=AdGuardHome
|
||||
NFT_RULES_FILE="/var/etc/adguardhome.nft"
|
||||
NFT_RULES_TPL="/usr/share/AdGuardHome/adguardhome.nft.tpl"
|
||||
NFT_TABLE="adguardhome"
|
||||
|
||||
EXTRA_COMMANDS="do_redirect force_reload isrunning"
|
||||
EXTRA_HELP=" do_redirect 0 or 1\
|
||||
force_reload
|
||||
isrunning"
|
||||
|
||||
get_wan_return_line() {
|
||||
local wan
|
||||
wan="$(uci -q get AdGuardHome.AdGuardHome.wan_ifname 2>/dev/null)"
|
||||
|
||||
[ -z "$wan" ] && { echo ""; return 0; }
|
||||
|
||||
local wan_set
|
||||
wan_set="\"$(printf '%s' "$wan" | sed 's/ /\", \"/g')\""
|
||||
echo "iifname { $wan_set } return"
|
||||
}
|
||||
|
||||
set_nft_redirect() {
|
||||
local port="$1"
|
||||
case "$port" in
|
||||
*[!0-9]*|"") return 1 ;;
|
||||
esac
|
||||
[ ! -f "$NFT_RULES_TPL" ] && return 1
|
||||
|
||||
local return_line
|
||||
return_line="$(get_wan_return_line)"
|
||||
|
||||
awk -v port="$port" -v wan_line="$return_line" '{
|
||||
gsub("__AGH_PORT__", port)
|
||||
gsub("__AGH_WAN_RETURN_LINE__", wan_line)
|
||||
print
|
||||
}' "$NFT_RULES_TPL" > "$NFT_RULES_FILE" || return 1
|
||||
|
||||
[ ! -s "$NFT_RULES_FILE" ] && return 1
|
||||
|
||||
nft delete table inet "$NFT_TABLE" 2>/dev/null
|
||||
nft -f "$NFT_RULES_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
clear_nft_redirect() {
|
||||
uci -q delete firewall.adguardhome_nft_include
|
||||
uci -q commit firewall
|
||||
|
||||
[ -f "$NFT_RULES_FILE" ] && rm -f "$NFT_RULES_FILE"
|
||||
nft delete table inet "$NFT_TABLE" 2>/dev/null
|
||||
}
|
||||
|
||||
wait_for_pid_exit() {
|
||||
local pid="$1" attempts=0
|
||||
case "$pid" in ''|*[!0-9]*) return 0 ;; esac
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
[ "$attempts" -lt 15 ] || return 1
|
||||
sleep 1
|
||||
attempts=$((attempts + 1))
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_dns_port_release() {
|
||||
local port="$1" attempts=0 tables="" table busy
|
||||
for table in /proc/net/tcp /proc/net/tcp6 /proc/net/udp /proc/net/udp6; do
|
||||
[ -r "$table" ] && tables="$tables $table"
|
||||
done
|
||||
[ -n "$tables" ] || return 1
|
||||
port="$(printf '%04X' "$port")" || return 1
|
||||
while :; do
|
||||
busy="$(awk -v port="$port" '
|
||||
FNR > 1 {
|
||||
split($2, address, ":")
|
||||
if (toupper(address[2]) == port &&
|
||||
((FILENAME ~ /tcp/ && $4 == "0A") ||
|
||||
(FILENAME ~ /udp/ && $4 == "07"))) found = 1
|
||||
}
|
||||
END { print found ? 1 : 0 }
|
||||
' $tables)" || return 1
|
||||
[ "$busy" = "0" ] && return 0
|
||||
[ "$attempts" -lt 15 ] || return 1
|
||||
sleep 1
|
||||
attempts=$((attempts + 1))
|
||||
done
|
||||
}
|
||||
|
||||
restart_dnsmasq() {
|
||||
local released_port="$1" service pids pid
|
||||
service="$(ubus call service list '{"name":"dnsmasq"}' 2>/dev/null)" || return 1
|
||||
pids="$(printf '%s' "$service" | jsonfilter -e '@.dnsmasq.instances.*.pid')"
|
||||
/etc/init.d/dnsmasq stop 8>&- 1000>&- || return 1
|
||||
for pid in $pids; do
|
||||
if ! wait_for_pid_exit "$pid"; then
|
||||
logger -t AdGuardHome 'dnsmasq did not stop; refusing to reuse its old jail.'
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
if [ -n "$released_port" ] && ! wait_for_dns_port_release "$released_port"; then
|
||||
logger -t AdGuardHome "DNS port $released_port is still occupied after stopping dnsmasq."
|
||||
/etc/init.d/dnsmasq start 8>&- 1000>&-
|
||||
return 1
|
||||
fi
|
||||
/etc/init.d/dnsmasq start 8>&- 1000>&- || return 1
|
||||
if [ -n "$released_port" ] && ! wait_for_dns_port_release "$released_port"; then
|
||||
logger -t AdGuardHome "DNS port $released_port was occupied again after starting dnsmasq."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
save_dnsmasq_upstream_config() {
|
||||
local backup="AdGuardHome.dnsmasq_backup" option value server
|
||||
[ "$(uci -q get "$backup.ready")" = "1" ] && return 0
|
||||
|
||||
uci -q delete "$backup"
|
||||
uci set "$backup=dnsmasq_backup" || return 1
|
||||
value="$(uci -q get dhcp.@dnsmasq[0].server)"
|
||||
for server in $value; do
|
||||
uci add_list "$backup.server=$server" || return 1
|
||||
done
|
||||
for option in resolvfile noresolv; do
|
||||
if value="$(uci -q get "dhcp.@dnsmasq[0].$option")"; then
|
||||
uci set "$backup.$option=$value" || return 1
|
||||
fi
|
||||
done
|
||||
uci set "$backup.ready=1" || return 1
|
||||
if ! uci commit AdGuardHome; then
|
||||
uci -q revert "$backup"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
apply_dnsmasq_config() {
|
||||
[ "${AGH_DEFER_DNSMASQ_RESTART:-0}" = "1" ] && return 0
|
||||
restart_dnsmasq "$@"
|
||||
}
|
||||
|
||||
is_scoped_dnsmasq_server() {
|
||||
local domains="${1%/*}"
|
||||
case "$1" in
|
||||
/*) case "$domains/" in */\#/*) return 1 ;; esac; return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
set_forward_dnsmasq() (
|
||||
set -f
|
||||
local addr="127.0.0.1#$1" servers desired server
|
||||
|
||||
if [ "$(uci -q get AdGuardHome.dnsmasq_backup.ready)" != "1" ] &&
|
||||
[ "$2" = "dnsmasq-upstream" ] && [ "$3" = "1" ]; then
|
||||
stop_forward_dnsmasq "$4" || return 1
|
||||
fi
|
||||
save_dnsmasq_upstream_config || return 1
|
||||
|
||||
servers="$(uci -q get dhcp.@dnsmasq[0].server)"
|
||||
desired="$addr"
|
||||
for server in $servers; do
|
||||
is_scoped_dnsmasq_server "$server" && desired="$desired $server"
|
||||
done
|
||||
if [ "$servers" = "$desired" ] && [ "$(uci -q get dhcp.@dnsmasq[0].noresolv)" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
uci -q delete dhcp.@dnsmasq[0].server
|
||||
for server in $desired; do
|
||||
uci add_list "dhcp.@dnsmasq[0].server=$server" || return 1
|
||||
done
|
||||
uci set dhcp.@dnsmasq[0].noresolv=1 || return 1
|
||||
uci commit dhcp || return 1
|
||||
apply_dnsmasq_config >/dev/null 2>&1
|
||||
)
|
||||
|
||||
stop_forward_dnsmasq() (
|
||||
set -f
|
||||
local backup="AdGuardHome.dnsmasq_backup" option value server
|
||||
local addr="127.0.0.1#$1" found=0 has_general=0
|
||||
|
||||
if [ "$(uci -q get "$backup.ready")" = "1" ]; then
|
||||
uci -q delete dhcp.@dnsmasq[0].server
|
||||
value="$(uci -q get "$backup.server")"
|
||||
for server in $value; do
|
||||
uci add_list "dhcp.@dnsmasq[0].server=$server" || return 1
|
||||
done
|
||||
for option in resolvfile noresolv; do
|
||||
if value="$(uci -q get "$backup.$option")"; then
|
||||
uci set "dhcp.@dnsmasq[0].$option=$value" || return 1
|
||||
else
|
||||
uci -q delete "dhcp.@dnsmasq[0].$option"
|
||||
fi
|
||||
done
|
||||
uci commit dhcp || return 1
|
||||
apply_dnsmasq_config >/dev/null 2>&1 || return 1
|
||||
[ "${AGH_DEFER_DNSMASQ_RESTART:-0}" = "1" ] && return 0
|
||||
uci -q delete "$backup"
|
||||
uci commit AdGuardHome
|
||||
return $?
|
||||
fi
|
||||
|
||||
value="$(uci -q get dhcp.@dnsmasq[0].server)"
|
||||
for server in $value; do
|
||||
if [ "$server" = "$addr" ]; then
|
||||
found=1
|
||||
elif ! is_scoped_dnsmasq_server "$server"; then
|
||||
has_general=1
|
||||
fi
|
||||
done
|
||||
[ "$found" = "1" ] || return 0
|
||||
uci -q del_list "dhcp.@dnsmasq[0].server=$addr" || return 1
|
||||
if [ "$has_general" = "0" ]; then
|
||||
if ! uci -q get dhcp.@dnsmasq[0].resolvfile >/dev/null; then
|
||||
value="/tmp/resolv.conf.d/resolv.conf.auto"
|
||||
[ -f "$value" ] || value="/tmp/resolv.conf.auto"
|
||||
uci set "dhcp.@dnsmasq[0].resolvfile=$value" || return 1
|
||||
fi
|
||||
uci -q delete dhcp.@dnsmasq[0].noresolv
|
||||
fi
|
||||
logger -t AdGuardHome 'No DNS snapshot from previous version; removed old upstream, original resolver settings are unavailable.'
|
||||
uci commit dhcp || return 1
|
||||
apply_dnsmasq_config >/dev/null 2>&1
|
||||
)
|
||||
|
||||
stop_core() {
|
||||
local service instance pid
|
||||
service="$(ubus call service list '{"name":"AdGuardHome"}' 2>/dev/null)" || return 1
|
||||
instance="$(printf '%s' "$service" | jsonfilter -e '@.AdGuardHome.instances.instance1')"
|
||||
[ -n "$instance" ] || return 0
|
||||
pid="$(printf '%s' "$instance" | jsonfilter -e '@.pid')"
|
||||
ubus call service delete '{"name":"AdGuardHome","instance":"instance1"}' >/dev/null || return 1
|
||||
if ! wait_for_pid_exit "$pid"; then
|
||||
logger -t AdGuardHome 'Core did not stop; DNS port settings were not changed.'
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
agh_reload() {
|
||||
[ "${AGH_REDIRECT_RELOAD:-1}" = "1" ] || return 0
|
||||
if [ "${AGH_DEFER_DNSMASQ_RESTART:-0}" = "1" ]; then
|
||||
AGH_RELOAD_PENDING=1
|
||||
return 0
|
||||
fi
|
||||
ubus call service event '{"type":"config.change","data":{"package":"AdGuardHome"}}' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
rm_port53() {
|
||||
local configpath
|
||||
config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
|
||||
|
||||
local AdGuardHome_PORT dnsmasq_port
|
||||
AdGuardHome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
|
||||
dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
|
||||
[ -z "$dnsmasq_port" ] && dnsmasq_port="53"
|
||||
|
||||
if [ "$dnsmasq_port" = "$AdGuardHome_PORT" ]; then
|
||||
[ "$dnsmasq_port" = "53" ] && dnsmasq_port="1745"
|
||||
elif [ "$dnsmasq_port" = "53" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
stop_core || return 1
|
||||
config_editor "dns.port" "$dnsmasq_port" "$configpath"
|
||||
[ "$(config_editor "dns.port" "" "$configpath" "1")" = "$dnsmasq_port" ] || return 1
|
||||
uci set dhcp.@dnsmasq[0].port="53" || return 1
|
||||
uci commit dhcp || return 1
|
||||
apply_dnsmasq_config "$dnsmasq_port" || return 1
|
||||
agh_reload
|
||||
}
|
||||
|
||||
use_port53() {
|
||||
local configpath
|
||||
config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
|
||||
|
||||
local AdGuardHome_PORT dnsmasq_port
|
||||
AdGuardHome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
|
||||
dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
|
||||
[ -z "$dnsmasq_port" ] && dnsmasq_port="53"
|
||||
|
||||
if [ "$dnsmasq_port" = "$AdGuardHome_PORT" ]; then
|
||||
[ "$dnsmasq_port" = "53" ] && AdGuardHome_PORT="1745"
|
||||
elif [ "$AdGuardHome_PORT" = "53" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
stop_core || return 1
|
||||
config_editor "dns.port" "53" "$configpath"
|
||||
[ "$(config_editor "dns.port" "" "$configpath" "1")" = "53" ] || return 1
|
||||
uci set dhcp.@dnsmasq[0].port="$AdGuardHome_PORT" || return 1
|
||||
uci commit dhcp || return 1
|
||||
apply_dnsmasq_config "$dnsmasq_port" || return 1
|
||||
agh_reload
|
||||
}
|
||||
|
||||
mark_redirect_flag() {
|
||||
local enabled="$1" redirect="$2" agh_port="$3"
|
||||
|
||||
local configpath
|
||||
config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
|
||||
|
||||
local flag=0
|
||||
|
||||
if [ "$enabled" = "1" ] && [ "$redirect" != "none" ]; then
|
||||
flag=1
|
||||
|
||||
if [ "$redirect" = "redirect" ]; then
|
||||
nft list table inet "$NFT_TABLE" >/dev/null 2>&1 || flag=0
|
||||
|
||||
elif [ "$redirect" = "dnsmasq-upstream" ]; then
|
||||
local s
|
||||
s="$(uci -q get dhcp.@dnsmasq[0].server | tr '\n' ' ')"
|
||||
printf '%s\n' "$s" | grep -q -E "(^|[[:space:]])127\.0\.0\.1#${agh_port}([[:space:]]|$)" || flag=0
|
||||
|
||||
elif [ "$redirect" = "exchange" ]; then
|
||||
local cfgp dport
|
||||
cfgp="$(config_editor "dns.port" "" "$configpath" "1")"
|
||||
dport="$(uci -q get dhcp.@dnsmasq[0].port)"
|
||||
if [ "$cfgp" != "53" ] || [ "$dport" = "53" ] ; then
|
||||
flag=0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -n "$flag" > /var/run/AdGredir
|
||||
}
|
||||
|
||||
finish_dnsmasq_config() {
|
||||
case "$old_redirect:$redirect" in
|
||||
*dnsmasq-upstream*|*exchange*)
|
||||
if [ "$dnsmasq_before" != "$(uci -q export dhcp)" ] || [ "$previous_flag" != "1" ] || [ "$enabled" = "0" ]; then
|
||||
local released_port=""
|
||||
if [ "$enabled" = "1" ] && [ "$redirect" = "exchange" ]; then
|
||||
released_port=53
|
||||
elif [ "$dnsmasq_previous_port" != "$(uci -q get dhcp.@dnsmasq[0].port)" ]; then
|
||||
released_port="$dnsmasq_previous_port"
|
||||
fi
|
||||
restart_dnsmasq "$released_port" || return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
if { [ "$enabled" = "0" ] || [ "$redirect" != "dnsmasq-upstream" ]; } &&
|
||||
[ "$(uci -q get AdGuardHome.dnsmasq_backup.ready)" = "1" ]; then
|
||||
uci -q delete AdGuardHome.dnsmasq_backup
|
||||
uci commit AdGuardHome || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
_do_redirect() (
|
||||
exec 8>/var/lock/AdGuardHome-redirect.lock || return 1
|
||||
flock -w 10 8 || return 1
|
||||
local AGH_DEFER_DNSMASQ_RESTART=1 AGH_RELOAD_PENDING=0
|
||||
local dnsmasq_before dnsmasq_previous_port previous_flag
|
||||
dnsmasq_before="$(uci -q export dhcp)" || return 1
|
||||
dnsmasq_previous_port="$(uci -q get dhcp.@dnsmasq[0].port)"
|
||||
dnsmasq_previous_port="${dnsmasq_previous_port:-53}"
|
||||
previous_flag="$(cat /var/run/AdGredir 2>/dev/null)"
|
||||
echo -n "0" > /var/run/AdGredir
|
||||
local section="$CONFIGURATION"
|
||||
args=""
|
||||
ipv6_server=1
|
||||
tcp_server=0
|
||||
enabled="$1"
|
||||
AGH_REDIRECT_RELOAD="${2:-$enabled}"
|
||||
|
||||
config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
|
||||
AdGuardHome_PORT="$(config_editor "dns.port" "" "$configpath" "1")"
|
||||
[ -z "$AdGuardHome_PORT" ] && AdGuardHome_PORT="0"
|
||||
|
||||
config_get redirect "$section" redirect "none"
|
||||
config_get old_redirect "$section" old_redirect "none"
|
||||
config_get old_port "$section" old_port "0"
|
||||
config_get old_enabled "$section" old_enabled "0"
|
||||
|
||||
uci get dhcp.@dnsmasq[0].port >/dev/null 2>&1 || uci set dhcp.@dnsmasq[0].port="53" >/dev/null 2>&1
|
||||
uci commit dhcp
|
||||
|
||||
if [ "$old_enabled" = "1" ] && [ "$old_redirect" = "exchange" ]; then
|
||||
AdGuardHome_PORT="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)"
|
||||
fi
|
||||
|
||||
if [ "$old_redirect" != "$redirect" ] || [ "$old_port" != "$AdGuardHome_PORT" ] || { [ "$old_enabled" = "1" ] && [ "$enabled" = "0" ]; }; then
|
||||
if [ "$old_redirect" != "none" ]; then
|
||||
if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then
|
||||
clear_nft_redirect
|
||||
elif [ "$old_redirect" = "dnsmasq-upstream" ]; then
|
||||
if [ "$old_enabled" = "1" ] || [ "$(uci -q get AdGuardHome.dnsmasq_backup.ready)" = "1" ]; then
|
||||
if ! stop_forward_dnsmasq "$old_port"; then
|
||||
echo -n "0" > /var/run/AdGredir
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
old_enabled=0
|
||||
elif [ "$old_redirect" = "exchange" ]; then
|
||||
if ! rm_port53; then
|
||||
echo -n "0" > /var/run/AdGredir
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
elif [ "$old_enabled" = "1" ] && [ "$enabled" = "1" ]; then
|
||||
if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then
|
||||
clear_nft_redirect
|
||||
fi
|
||||
fi
|
||||
|
||||
uci delete AdGuardHome.@AdGuardHome[0].old_redirect 2>/dev/null
|
||||
uci delete AdGuardHome.@AdGuardHome[0].old_port 2>/dev/null
|
||||
uci delete AdGuardHome.@AdGuardHome[0].old_enabled 2>/dev/null
|
||||
uci add_list AdGuardHome.@AdGuardHome[0].old_redirect="$redirect" 2>/dev/null
|
||||
uci add_list AdGuardHome.@AdGuardHome[0].old_port="$AdGuardHome_PORT" 2>/dev/null
|
||||
uci add_list AdGuardHome.@AdGuardHome[0].old_enabled="$enabled" 2>/dev/null
|
||||
uci commit AdGuardHome
|
||||
|
||||
if [ "$enabled" = "0" ] || [ "$AdGuardHome_PORT" = "0" ]; then
|
||||
finish_dnsmasq_config || return 1
|
||||
echo -n "0" > /var/run/AdGredir
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$redirect" = "redirect" ]; then
|
||||
set_nft_redirect "$AdGuardHome_PORT"
|
||||
elif [ "$redirect" = "dnsmasq-upstream" ]; then
|
||||
if ! set_forward_dnsmasq "$AdGuardHome_PORT" "$old_redirect" "$old_enabled" "$old_port"; then
|
||||
echo -n "0" > /var/run/AdGredir
|
||||
return 1
|
||||
fi
|
||||
elif [ "$redirect" = "exchange" ] && [ "$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" = "53" ]; then
|
||||
if ! use_port53; then
|
||||
echo -n "0" > /var/run/AdGredir
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
finish_dnsmasq_config || return 1
|
||||
mark_redirect_flag "$enabled" "$redirect" "$AdGuardHome_PORT" || return 1
|
||||
AGH_DEFER_DNSMASQ_RESTART=0
|
||||
[ "$AGH_RELOAD_PENDING" = "0" ] || agh_reload
|
||||
return 0
|
||||
)
|
||||
|
||||
do_redirect() {
|
||||
config_load "${CONFIGURATION}"
|
||||
_do_redirect "$1"
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONFIGURATION"
|
||||
}
|
||||
|
||||
isrunning() {
|
||||
config_load "${CONFIGURATION}"
|
||||
_isrunning
|
||||
local r=$?
|
||||
[ "$r" = "0" ] && echo "running" || ([ "$r" = "1" ] && echo "not run" || echo "no bin")
|
||||
return $r
|
||||
}
|
||||
|
||||
_isrunning() {
|
||||
config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome"
|
||||
[ ! -f "$binpath" ] && return 2
|
||||
pgrep -f "$(basename "$binpath")" >/dev/null 2>&1 && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
force_reload() {
|
||||
config_load "${CONFIGURATION}"
|
||||
_isrunning && procd_send_signal "$CONFIGURATION" || start
|
||||
}
|
||||
|
||||
get_tz() {
|
||||
SET_TZ=""
|
||||
[ -e "/etc/localtime" ] && return
|
||||
for tzfile in /etc/TZ /var/etc/TZ; do
|
||||
[ -e "$tzfile" ] && tz="$(cat "$tzfile" 2>/dev/null)" && break
|
||||
done
|
||||
[ -n "$tz" ] && SET_TZ=$tz
|
||||
}
|
||||
|
||||
get_filesystem() {
|
||||
echo "$1" | awk '
|
||||
BEGIN{
|
||||
while (("mount"| getline ret) > 0) {
|
||||
split(ret,d); fs[d[3]]=d[5];
|
||||
m=index(d[1],":");
|
||||
if (m==0) { pt[d[3]]=d[1] } else { pt[d[3]]=substr(d[1],m+1) }
|
||||
}
|
||||
}
|
||||
{
|
||||
split($0,d,"/");
|
||||
if ("/" in fs) { result1=fs["/"]; }
|
||||
if ("/" in pt) { result2=pt["/"]; }
|
||||
for (i=2;i<=length(d);i++) {
|
||||
p[i]=p[i-1]"/"d[i];
|
||||
if (p[i] in fs) { result1=fs[p[i]]; result2=pt[p[i]]; }
|
||||
}
|
||||
if (result2 in fs) { result=fs[result2] } else { result=result1 }
|
||||
print(result);
|
||||
}'
|
||||
}
|
||||
|
||||
config_editor() {
|
||||
awk -v yaml="$1" -v value="$2" -v file="$3" -v ro="$4" '
|
||||
BEGIN{split(yaml,part,"\.");s="";i=1;l=length(part);}
|
||||
{
|
||||
if (match($0,s""part[i]":")) {
|
||||
if (i==l) {
|
||||
split($0,t,": ");
|
||||
if (ro==""){
|
||||
system("sed -i '\''"FNR"c\\" t[1] ": " value "'\'' " file);
|
||||
} else {
|
||||
print(t[2]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
s=s"[- ]{2}";
|
||||
i++;
|
||||
}
|
||||
}' "$3"
|
||||
}
|
||||
|
||||
boot_service() {
|
||||
rm -f /var/run/AdGserverdis 2>/dev/null
|
||||
config_load "${CONFIGURATION}"
|
||||
config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome"
|
||||
[ -f "$binpath" ] && start_service
|
||||
}
|
||||
|
||||
start_service() {
|
||||
stop_core || return 1
|
||||
rm -f /var/run/AdGserverdis 2>/dev/null
|
||||
config_load "${CONFIGURATION}"
|
||||
config_get hashpass $CONFIGURATION hashpass ""
|
||||
config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml"
|
||||
if [ -n "$hashpass" ]; then
|
||||
config_editor "users.password" "$hashpass" "$configpath"
|
||||
uci set $CONFIGURATION.$CONFIGURATION.hashpass=""
|
||||
uci commit $CONFIGURATION
|
||||
fi
|
||||
local enabled
|
||||
config_get_bool enabled $CONFIGURATION enabled 0
|
||||
if [ "$enabled" = "0" ]; then
|
||||
_do_redirect 0
|
||||
return
|
||||
fi
|
||||
config_get workdir $CONFIGURATION workdir "/usr/bin/AdGuardHome"
|
||||
|
||||
mkdir -p "$workdir/data"
|
||||
local cwdfs
|
||||
cwdfs=$(get_filesystem "$workdir")
|
||||
echo "workdir is a $cwdfs filesystem"
|
||||
if [ "$cwdfs" = "jffs2" ]; then
|
||||
echo "fs error ln db to tmp $workdir $cwdfs"
|
||||
logger "AdGuardHome" "warning db redirect to tmp"
|
||||
touch "$workdir/data/stats.db"
|
||||
if [ ! -L "$workdir/data/stats.db" ]; then
|
||||
mv -f "$workdir/data/stats.db" /tmp/stats.db 2>/dev/null
|
||||
ln -s /tmp/stats.db "$workdir/data/stats.db" 2>/dev/null
|
||||
fi
|
||||
touch "$workdir/data/sessions.db"
|
||||
if [ ! -L "$workdir/data/sessions.db" ]; then
|
||||
mv -f "$workdir/data/sessions.db" /tmp/sessions.db 2>/dev/null
|
||||
ln -s /tmp/sessions.db "$workdir/data/sessions.db" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
local ADDITIONAL_ARGS=""
|
||||
config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome"
|
||||
|
||||
mkdir -p "${binpath%/*}"
|
||||
ADDITIONAL_ARGS="$ADDITIONAL_ARGS -c $configpath"
|
||||
ADDITIONAL_ARGS="$ADDITIONAL_ARGS -w $workdir"
|
||||
config_get httpport $CONFIGURATION httpport 3000
|
||||
ADDITIONAL_ARGS="$ADDITIONAL_ARGS -p $httpport"
|
||||
|
||||
printf '%s\n' "$binpath" "$configpath" > /lib/upgrade/keep.d/luci-app-adguardhome
|
||||
|
||||
config_get logfile $CONFIGURATION logfile ""
|
||||
if [ -n "$logfile" ]; then
|
||||
ADDITIONAL_ARGS="$ADDITIONAL_ARGS -l $logfile"
|
||||
fi
|
||||
|
||||
if [ ! -f "$binpath" ]; then
|
||||
_do_redirect 0
|
||||
/usr/share/AdGuardHome/update_core.sh >/dev/null 2>&1 &
|
||||
exit 0
|
||||
fi
|
||||
|
||||
config_get_bool verbose $CONFIGURATION verbose 0
|
||||
if [ "$verbose" -eq 1 ]; then
|
||||
ADDITIONAL_ARGS="$ADDITIONAL_ARGS -v"
|
||||
fi
|
||||
|
||||
if [ "$(uci -q get bypass.@global[0].global_server)" ] && [ "$(uci -q get bypass.@global[0].adguardhome)" = "1" ] && [ "$(uci -q get dhcp.@dnsmasq[0].port)" = "53" ]; then
|
||||
uci -q set AdGuardHome.AdGuardHome.redirect='exchange'
|
||||
uci -q commit AdGuardHome
|
||||
config_load "${CONFIGURATION}"
|
||||
fi
|
||||
|
||||
if [ -f "$configpath" ]; then
|
||||
if ! _do_redirect 1 0; then
|
||||
[ "$(config_editor "dns.port" "" "$configpath" "1")" = "0" ] || return 1
|
||||
fi
|
||||
else
|
||||
_do_redirect 0 0
|
||||
fi
|
||||
|
||||
procd_open_instance
|
||||
get_tz
|
||||
if [ -n "$SET_TZ" ]; then
|
||||
procd_set_param env TZ="$SET_TZ"
|
||||
fi
|
||||
procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5}
|
||||
procd_set_param limits core="unlimited" nofile="65535 65535"
|
||||
procd_set_param stderr 1
|
||||
procd_set_param command "$binpath" $ADDITIONAL_ARGS
|
||||
procd_set_param file "$configpath" "/etc/hosts" "/etc/config/AdGuardHome"
|
||||
procd_close_instance
|
||||
if [ ! -f "$configpath" ]; then
|
||||
config_get "redirect" "AdGuardHome" "redirect" "none"
|
||||
if [ "$redirect" != "none" ]; then
|
||||
procd_open_instance "waitconfig"
|
||||
procd_set_param command "/usr/share/AdGuardHome/watchconfig.sh"
|
||||
procd_close_instance
|
||||
echo "no config start watching"
|
||||
fi
|
||||
fi
|
||||
echo "AdGuardHome service enabled"
|
||||
echo "luci enable switch=$enabled"
|
||||
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
rm -f /var/run/AdGlucitest 2>/dev/null
|
||||
echo "AdGuardHome reloading"
|
||||
start
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
stop_core || return 1
|
||||
config_load "${CONFIGURATION}"
|
||||
_do_redirect 0
|
||||
echo "AdGuardHome service disabled"
|
||||
touch /var/run/AdGserverdis
|
||||
}
|
||||
|
||||
boot() {
|
||||
rc_procd boot_service "$@"
|
||||
eval "type service_started" 2>/dev/null >/dev/null && service_started
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
chmod +x /etc/init.d/AdGuardHome
|
||||
uci -q batch <<-EOF >/dev/null 2>&1
|
||||
delete ucitrack.@AdGuardHome[-1]
|
||||
add ucitrack AdGuardHome
|
||||
set ucitrack.@AdGuardHome[-1].init=AdGuardHome
|
||||
commit ucitrack
|
||||
EOF
|
||||
|
||||
# Remove the former menu and both Lua and ucode menu caches.
|
||||
rm -f /usr/share/luci/menu.d/luci-app-adguardhome-js.json
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
rm -f /tmp/luci-modulecache/*
|
||||
exit 0
|
||||
@@ -1 +0,0 @@
|
||||
/etc/AdGuardHome.yaml
|
||||
@@ -1,189 +0,0 @@
|
||||
# 模板用户名为 admin 密码为 admin
|
||||
# The username is admin and the password is admin.
|
||||
# 保存并应用就可以直接用的配置模板文件
|
||||
# a template config can be use with a apply
|
||||
http:
|
||||
pprof:
|
||||
port: 6060
|
||||
enabled: false
|
||||
address: 0.0.0.0:3000
|
||||
session_ttl: 720h
|
||||
users:
|
||||
- name: admin
|
||||
password: $2y$10$iGkTW98QtGEZn4I/GC0J4eQhfuLQ53v7L4DDVauEJyWylJrXWwWeO
|
||||
auth_attempts: 5
|
||||
block_auth_min: 15
|
||||
http_proxy: ""
|
||||
language: ""
|
||||
theme: auto
|
||||
dns:
|
||||
bind_hosts:
|
||||
- 0.0.0.0
|
||||
port: 3053
|
||||
anonymize_client_ip: false
|
||||
ratelimit: 20
|
||||
ratelimit_subnet_len_ipv4: 24
|
||||
ratelimit_subnet_len_ipv6: 56
|
||||
ratelimit_whitelist: []
|
||||
refuse_any: true
|
||||
upstream_dns:
|
||||
- https://dns.cloudflare.com/dns-query
|
||||
- '#'
|
||||
- '# 对 ts.net 相关域名启用 DNS 分流, 交由 Tailscale 解析, 如不使用 Tailscale 可以删除.'
|
||||
- '# Enable DNS conditional forwarding for ts.net via Tailscale; remove if Tailscale is not used.'
|
||||
- '[/ts.net/]100.100.100.100'
|
||||
upstream_dns_file: ""
|
||||
bootstrap_dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
fallback_dns:
|
||||
- quic://dns.adguard-dns.com
|
||||
upstream_mode: parallel
|
||||
fastest_timeout: 1s
|
||||
allowed_clients: []
|
||||
disallowed_clients: []
|
||||
blocked_hosts:
|
||||
- version.bind
|
||||
- id.server
|
||||
- hostname.bind
|
||||
trusted_proxies:
|
||||
- 127.0.0.0/8
|
||||
- ::1/128
|
||||
cache_enabled: true
|
||||
cache_size: 4194304
|
||||
cache_ttl_min: 0
|
||||
cache_ttl_max: 0
|
||||
cache_optimistic: false
|
||||
cache_optimistic_answer_ttl: 30s
|
||||
cache_optimistic_max_age: 12h
|
||||
bogus_nxdomain: []
|
||||
aaaa_disabled: false
|
||||
enable_dnssec: false
|
||||
edns_client_subnet:
|
||||
custom_ip: ""
|
||||
enabled: false
|
||||
use_custom: false
|
||||
max_goroutines: 300
|
||||
handle_ddr: true
|
||||
ipset: []
|
||||
ipset_file: ""
|
||||
bootstrap_prefer_ipv6: false
|
||||
upstream_timeout: 10s
|
||||
private_networks: []
|
||||
use_private_ptr_resolvers: true
|
||||
local_ptr_upstreams: []
|
||||
use_dns64: false
|
||||
dns64_prefixes: []
|
||||
serve_http3: false
|
||||
use_http3_upstreams: false
|
||||
serve_plain_dns: true
|
||||
hostsfile_enabled: true
|
||||
pending_requests:
|
||||
enabled: true
|
||||
tls:
|
||||
enabled: false
|
||||
server_name: ""
|
||||
force_https: false
|
||||
port_https: 443
|
||||
port_dns_over_tls: 853
|
||||
port_dns_over_quic: 853
|
||||
port_dnscrypt: 0
|
||||
dnscrypt_config_file: ""
|
||||
allow_unencrypted_doh: false
|
||||
certificate_chain: ""
|
||||
private_key: ""
|
||||
certificate_path: ""
|
||||
private_key_path: ""
|
||||
strict_sni_check: false
|
||||
querylog:
|
||||
dir_path: ""
|
||||
ignored: []
|
||||
interval: 2160h
|
||||
size_memory: 1000
|
||||
enabled: true
|
||||
file_enabled: true
|
||||
statistics:
|
||||
dir_path: ""
|
||||
ignored: []
|
||||
interval: 24h
|
||||
enabled: true
|
||||
filters:
|
||||
- enabled: true
|
||||
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
|
||||
name: AdGuard DNS filter
|
||||
id: 1
|
||||
whitelist_filters: []
|
||||
user_rules: []
|
||||
dhcp:
|
||||
enabled: false
|
||||
interface_name: ""
|
||||
local_domain_name: lan
|
||||
dhcpv4:
|
||||
gateway_ip: ""
|
||||
subnet_mask: ""
|
||||
range_start: ""
|
||||
range_end: ""
|
||||
lease_duration: 86400
|
||||
icmp_timeout_msec: 1000
|
||||
options: []
|
||||
dhcpv6:
|
||||
range_start: ""
|
||||
lease_duration: 86400
|
||||
ra_slaac_only: false
|
||||
ra_allow_slaac: false
|
||||
filtering:
|
||||
blocking_ipv4: ""
|
||||
blocking_ipv6: ""
|
||||
blocked_services:
|
||||
schedule:
|
||||
time_zone: UTC
|
||||
ids: []
|
||||
protection_disabled_until: null
|
||||
safe_search:
|
||||
enabled: false
|
||||
bing: true
|
||||
duckduckgo: true
|
||||
ecosia: true
|
||||
google: true
|
||||
pixabay: true
|
||||
yandex: true
|
||||
youtube: true
|
||||
blocking_mode: default
|
||||
parental_block_host: family-block.dns.adguard.com
|
||||
safebrowsing_block_host: standard-block.dns.adguard.com
|
||||
rewrites: []
|
||||
safe_fs_patterns:
|
||||
- /tmp/AdGuardHome/userfilters/*
|
||||
safebrowsing_cache_size: 1048576
|
||||
safesearch_cache_size: 1048576
|
||||
parental_cache_size: 1048576
|
||||
cache_time: 30
|
||||
filters_update_interval: 24
|
||||
blocked_response_ttl: 10
|
||||
filtering_enabled: true
|
||||
rewrites_enabled: true
|
||||
parental_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
protection_enabled: true
|
||||
clients:
|
||||
runtime_sources:
|
||||
whois: true
|
||||
arp: true
|
||||
rdns: true
|
||||
dhcp: true
|
||||
hosts: true
|
||||
persistent: []
|
||||
log:
|
||||
enabled: true
|
||||
file: ""
|
||||
max_backups: 0
|
||||
max_size: 100
|
||||
max_age: 3
|
||||
compress: false
|
||||
local_time: false
|
||||
verbose: false
|
||||
os:
|
||||
group: ""
|
||||
user: ""
|
||||
rlimit_nofile: 0
|
||||
schema_version: 32
|
||||
@@ -1,10 +0,0 @@
|
||||
table inet adguardhome {
|
||||
chain prerouting {
|
||||
type nat hook prerouting priority dstnat; policy accept;
|
||||
|
||||
__AGH_WAN_RETURN_LINE__
|
||||
|
||||
fib daddr type local udp dport 53 redirect to :__AGH_PORT__
|
||||
fib daddr type local tcp dport 53 redirect to :__AGH_PORT__
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
logread -e AdGuardHome > /tmp/AdGuardHometmp.log
|
||||
logread -e AdGuardHome -f >> /tmp/AdGuardHometmp.log &
|
||||
pid=$!
|
||||
echo "1" > /var/run/AdGuardHomesyslog
|
||||
while true
|
||||
do
|
||||
sleep 12
|
||||
watchdog=$(cat /var/run/AdGuardHomesyslog 2>/dev/null)
|
||||
if [ "${watchdog}x" = "0x" ]; then
|
||||
kill "$pid"
|
||||
rm /tmp/AdGuardHometmp.log
|
||||
rm /var/run/AdGuardHomesyslog
|
||||
exit 0
|
||||
else
|
||||
echo "0" > /var/run/AdGuardHomesyslog
|
||||
fi
|
||||
done
|
||||
@@ -1,164 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
update_mode=$1
|
||||
|
||||
# Keep the lock file: unlinking it would allow a second lock on a new inode.
|
||||
exec 9>/var/lock/AdGuardHome-update.lock || exit 1
|
||||
# Allow a brief status probe to finish before treating the lock as busy.
|
||||
if ! flock -w 1 9; then
|
||||
printf '%s\n' '已经有更新任务正在运行,请等待其结束。'
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Only the lock owner may reset the log and update status.
|
||||
exec >/tmp/AdGuardHome_update.log 2>&1
|
||||
cleanup() {
|
||||
local result=$?
|
||||
trap - EXIT
|
||||
[ "$result" -ne 0 ] && touch /var/run/update_core_error
|
||||
rm -f /var/run/update_core
|
||||
exit "$result"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 1' TERM INT HUP
|
||||
rm -f /var/run/update_core_error
|
||||
touch /var/run/update_core
|
||||
binpath=$(uci get AdGuardHome.AdGuardHome.binpath 2>/dev/null)
|
||||
if [ -z "$binpath" ]; then
|
||||
uci set AdGuardHome.AdGuardHome.binpath="/tmp/AdGuardHome/AdGuardHome"
|
||||
binpath="/tmp/AdGuardHome/AdGuardHome"
|
||||
fi
|
||||
[ ! -d "${binpath%/*}" ] && mkdir -p "${binpath%/*}"
|
||||
enabled=$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)
|
||||
core_api_url="https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest"
|
||||
|
||||
Check_Updates(){
|
||||
downloader_cmd="uclient-fetch"
|
||||
downloader_opts="-T 5 -q -O"
|
||||
downloader_quiet_opts="-q -O -"
|
||||
printf "%s\n" "开始检查更新, 请耐心等待 ..."
|
||||
Cloud_Version="$(${downloader_cmd} ${downloader_quiet_opts} "$core_api_url" 2>/dev/null | sed -n 's/.*"tag_name":[ ]*"\(v[^"]*\)".*/\1/p' | head -n 1)"
|
||||
[ -z "$Cloud_Version" ] && printf "\n检查更新失败, 请检查网络或稍后重试!\n" && EXIT 1
|
||||
if [ -f "$binpath" ]; then
|
||||
Current_Version="$($binpath --version 2>/dev/null | sed -n 's/.*\(v[0-9][0-9.]*\).*/\1/p' | head -n 1)"
|
||||
else
|
||||
Current_Version="未知"
|
||||
fi
|
||||
[ -z "$Current_Version" ] && Current_Version="未知"
|
||||
printf "\n执行文件路径: %s\n\n正在检查更新, 请耐心等待 ...\n" "${binpath%/*}"
|
||||
printf "\n当前 AdGuardHome 版本: %s\n云端 AdGuardHome 版本: %s\n" "$Current_Version" "$Cloud_Version"
|
||||
if [ "$Cloud_Version" != "$Current_Version" ] || [ "$1" = force ]; then
|
||||
Update_Core
|
||||
else
|
||||
printf "\n已是最新版本, 无需更新!\n"
|
||||
EXIT 0
|
||||
fi
|
||||
EXIT 0
|
||||
}
|
||||
|
||||
Update_Core(){
|
||||
rm -rf /tmp/AdGuardHome_Update > /dev/null 2>&1
|
||||
mkdir -p "/tmp/AdGuardHome_Update"
|
||||
GET_Arch
|
||||
link="https://github.com/AdguardTeam/AdGuardHome/releases/download/${Cloud_Version}/AdGuardHome_linux_${Arch}.tar.gz"
|
||||
printf "%s\n" "下载链接:${link}"
|
||||
printf "%s\n" "文件名称:${link##*/}"
|
||||
printf "\n开始下载 AdGuardHome 核心文件 ...\n\n"
|
||||
${downloader_cmd} ${downloader_opts} "/tmp/AdGuardHome_Update/${link##*/}" "$link" > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
printf "\nAdGuardHome 核心下载失败,请检查网络、系统时间及 CA 证书 ...\n"
|
||||
rm -rf /tmp/AdGuardHome_Update
|
||||
EXIT 1
|
||||
fi
|
||||
if [ "${link##*.}" = "gz" ]; then
|
||||
printf "\n正在解压 AdGuardHome ...\n"
|
||||
tar -zxf "/tmp/AdGuardHome_Update/${link##*/}" -C "/tmp/AdGuardHome_Update/"
|
||||
if [ ! -e /tmp/AdGuardHome_Update/AdGuardHome ]
|
||||
then
|
||||
printf "%s\n" "AdGuardHome 核心解压失败!"
|
||||
rm -rf "/tmp/AdGuardHome_Update" > /dev/null 2>&1
|
||||
EXIT 1
|
||||
fi
|
||||
downloadbin="/tmp/AdGuardHome_Update/AdGuardHome/AdGuardHome"
|
||||
else
|
||||
downloadbin="/tmp/AdGuardHome_Update/${link##*/}"
|
||||
fi
|
||||
chmod +x "$downloadbin"
|
||||
size_bytes=$(wc -c < "$downloadbin" 2>/dev/null || echo 0)
|
||||
printf "\nAdGuardHome 核心体积: %s\n" "$(awk -v bytes="$size_bytes" 'BEGIN{printf "%.2fMB", bytes/1000000}')"
|
||||
/etc/init.d/AdGuardHome stop 9>&- > /dev/null 2>&1
|
||||
printf "\n移动 AdGuardHome 核心文件到 %s ...\n" "${binpath%/*}"
|
||||
if ! mv -f "$downloadbin" "$binpath" > /dev/null 2>&1 || [ ! -s "$binpath" ]; then
|
||||
printf "AdGuardHome 核心移动失败!\n可能是设备空间不足导致, 请尝试更改 [执行文件路径] 为 /tmp/AdGuardHome\n"
|
||||
EXIT 1
|
||||
fi
|
||||
rm -rf /tmp/AdGuardHome_Update
|
||||
chmod +x "$binpath"
|
||||
if [ "$enabled" = 1 ]; then
|
||||
printf "\n正在重启 AdGuardHome 服务...\n"
|
||||
/etc/init.d/AdGuardHome restart 9>&- > /dev/null 2>&1
|
||||
fi
|
||||
printf "\nAdGuardHome 核心更新成功!\n"
|
||||
}
|
||||
|
||||
GET_Arch() {
|
||||
if command -v apk > /dev/null 2>&1; then
|
||||
PM="apk"
|
||||
elif command -v opkg > /dev/null 2>&1; then
|
||||
PM="opkg"
|
||||
else
|
||||
printf "%s\n" "未找到包管理器 (apk/opkg)!" && EXIT 1
|
||||
fi
|
||||
case "${PM}" in
|
||||
apk)
|
||||
Archt="$(apk info -a kernel 2>/dev/null | grep Architecture | awk -F ":[ ]*" '{print $2}')"
|
||||
[ -z "$Archt" ] && Archt="$(uname -m)"
|
||||
;;
|
||||
opkg)
|
||||
Archt="$(opkg info kernel | grep Architecture | awk -F "[ _]" '{print($2)}')"
|
||||
;;
|
||||
esac
|
||||
case "${Archt}" in
|
||||
i386)
|
||||
Arch=i386
|
||||
;;
|
||||
i686)
|
||||
Arch=i386
|
||||
;;
|
||||
x86|x86_64)
|
||||
Arch=amd64
|
||||
;;
|
||||
mipsel)
|
||||
Arch=mipsle_softfloat
|
||||
;;
|
||||
mips)
|
||||
Arch=mips_softfloat
|
||||
;;
|
||||
mips64el)
|
||||
Arch=mips64le_softfloat
|
||||
;;
|
||||
mips64)
|
||||
Arch=mips64_softfloat
|
||||
;;
|
||||
arm)
|
||||
Arch=arm
|
||||
;;
|
||||
armeb)
|
||||
Arch=armeb
|
||||
;;
|
||||
aarch64)
|
||||
Arch=arm64
|
||||
;;
|
||||
*)
|
||||
printf "\nAdGuardHome 暂不支持当前的设备架构: [%s]!\n" "$Archt"
|
||||
EXIT 1
|
||||
esac
|
||||
printf "\n当前设备架构: %s\n" "$Arch"
|
||||
}
|
||||
|
||||
EXIT(){
|
||||
exit "$1"
|
||||
}
|
||||
|
||||
Check_Updates "$update_mode"
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
configpath=$(uci get AdGuardHome.AdGuardHome.configpath)
|
||||
while :
|
||||
do
|
||||
sleep 10
|
||||
if [ -f "$configpath" ]; then
|
||||
/etc/init.d/AdGuardHome do_redirect 1
|
||||
break
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"admin/services/AdGuardHome": {
|
||||
"title": "AdGuard Home",
|
||||
"order": 12,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ]
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/base": {
|
||||
"title": "Base Setting",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "adguardhome/base"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ]
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/log": {
|
||||
"title": "Log",
|
||||
"order": 3,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "adguardhome/log"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ]
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/manual": {
|
||||
"title": "Manual Config",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "adguardhome/manual"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"luci-app-adguardhome": {
|
||||
"description": "Grant UCI access for luci-app-adguardhome",
|
||||
"read": {
|
||||
"file": {
|
||||
"/var/run/AdGredir": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ],
|
||||
"luci.adguardhome": [ "getCoreInfo", "getRedirectStatus", "getCoreUpdate", "getInterfaces", "getRuntimeLog", "getManualConfig" ]
|
||||
},
|
||||
"uci": [ "AdGuardHome" ]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.adguardhome": [ "setVerboseLog", "startCoreUpdate", "changeWebPassword", "clearRuntimeLog", "setManualConfig" ]
|
||||
},
|
||||
"uci": [ "AdGuardHome" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
#!/usr/bin/ucode
|
||||
'use strict';
|
||||
|
||||
import { stat, open, popen, readfile, writefile, unlink, lsdir } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
function shellQuote(value) {
|
||||
return "'" + replace(value, /'/g, "'\\''") + "'";
|
||||
}
|
||||
|
||||
function coreUpdateRunning() {
|
||||
// The kernel releases the lock even if the updater is killed.
|
||||
const result = system(['/usr/bin/flock', '-n', '-E', '2',
|
||||
'/var/lock/AdGuardHome-update.lock', '/bin/true'], 3000);
|
||||
if (result != 0 && result != 2)
|
||||
die('Unable to check core update lock');
|
||||
return result == 2;
|
||||
}
|
||||
|
||||
return {
|
||||
'luci.adguardhome': {
|
||||
getRedirectStatus: {
|
||||
call: function() {
|
||||
const conf = cursor();
|
||||
function option(name, fallback) {
|
||||
const value = conf.get('AdGuardHome', 'AdGuardHome', name);
|
||||
return (type(value) == 'array' ? value[0] : value) || fallback;
|
||||
}
|
||||
const result = {
|
||||
mode: option('redirect', 'none'),
|
||||
enabled: option('enabled', '0') == '1',
|
||||
applied_mode: option('old_redirect', 'none'),
|
||||
applied_enabled: option('old_enabled', '0') == '1',
|
||||
binpath: option('binpath', '/usr/bin/AdGuardHome/AdGuardHome'),
|
||||
redirected: trim(readfile('/var/run/AdGredir') || '') == '1'
|
||||
};
|
||||
conf.unload('AdGuardHome');
|
||||
const lock = system(['/usr/bin/flock', '-n', '-E', '2',
|
||||
'/var/lock/AdGuardHome-redirect.lock', '/bin/true'], 3000);
|
||||
if (lock != 0 && lock != 2)
|
||||
die('Unable to check redirect status');
|
||||
result.busy = lock == 2;
|
||||
return result;
|
||||
}
|
||||
},
|
||||
getManualConfig: {
|
||||
call: function() {
|
||||
const conf = cursor();
|
||||
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
|
||||
'/etc/AdGuardHome.yaml';
|
||||
conf.unload('AdGuardHome');
|
||||
|
||||
const template = readfile('/usr/share/AdGuardHome/AdGuardHome_template.yaml') || '';
|
||||
const current = readfile(configpath);
|
||||
if (current != null)
|
||||
return { content: current, template: template, source: 'current' };
|
||||
if (stat(configpath) != null)
|
||||
return { content: '', template: template, source: 'error' };
|
||||
return {
|
||||
content: template,
|
||||
template: template,
|
||||
source: template ? 'template' : 'missing'
|
||||
};
|
||||
}
|
||||
},
|
||||
setManualConfig: {
|
||||
args: { content: '' },
|
||||
call: function(request) {
|
||||
const content = request.args.content;
|
||||
if (content == null)
|
||||
return { saved: false, reason: 'invalid_content' };
|
||||
|
||||
const conf = cursor();
|
||||
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
|
||||
'/etc/AdGuardHome.yaml';
|
||||
const binpath = conf.get('AdGuardHome', 'AdGuardHome', 'binpath') ||
|
||||
'/usr/bin/AdGuardHome/AdGuardHome';
|
||||
conf.unload('AdGuardHome');
|
||||
|
||||
const temporaryConfig = '/tmp/AdGuardHometmpconfig.yaml';
|
||||
const validationLog = '/tmp/AdGuardHometest.log';
|
||||
if (writefile(temporaryConfig, replace(content, /\r\n/g, '\n')) == null)
|
||||
return { saved: false, reason: 'write_failed' };
|
||||
|
||||
if (stat(binpath)?.type == 'file') {
|
||||
const valid = system(['/bin/sh', '-c', '"$1" -c "$2" --check-config > "$3" 2>&1',
|
||||
'sh', binpath, temporaryConfig, validationLog], 30000) == 0;
|
||||
if (!valid)
|
||||
return { saved: false, reason: 'validation_failed', log: readfile(validationLog) || '' };
|
||||
}
|
||||
|
||||
if (writefile(configpath, readfile(temporaryConfig) || '') == null)
|
||||
return { saved: false, reason: 'save_failed' };
|
||||
unlink(temporaryConfig);
|
||||
if (system(['/etc/init.d/AdGuardHome', 'reload'], 30000) != 0)
|
||||
return { saved: false, reason: 'reload_failed' };
|
||||
return { saved: true };
|
||||
}
|
||||
},
|
||||
getInterfaces: {
|
||||
call: function() {
|
||||
return { interfaces: lsdir('/sys/class/net') || [] };
|
||||
}
|
||||
},
|
||||
getCoreInfo: {
|
||||
call: function() {
|
||||
const conf = cursor();
|
||||
const binpath = conf.get('AdGuardHome', 'AdGuardHome', 'binpath') ||
|
||||
'/usr/bin/AdGuardHome/AdGuardHome';
|
||||
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
|
||||
'/etc/AdGuardHome.yaml';
|
||||
const result = {
|
||||
core_exists: stat(binpath)?.type == 'file',
|
||||
config_exists: stat(configpath)?.type == 'file',
|
||||
version: null
|
||||
};
|
||||
|
||||
if (result.core_exists) {
|
||||
// Pass the configured path as a positional argument; capture both output streams.
|
||||
const proc = popen('/bin/sh -c \'"$1" --version 2>&1\' sh ' + shellQuote(binpath), 'r');
|
||||
if (proc) {
|
||||
const output = proc.read('all') || '';
|
||||
proc.close();
|
||||
result.version = match(output, /version ([^, \t\r\n]+)/)?.[1] || null;
|
||||
}
|
||||
}
|
||||
|
||||
conf.unload('AdGuardHome');
|
||||
return result;
|
||||
}
|
||||
},
|
||||
getCoreUpdate: {
|
||||
call: function() {
|
||||
const running = coreUpdateRunning();
|
||||
const failed = stat('/var/run/update_core_error') != null ||
|
||||
(!running && stat('/var/run/update_core') != null);
|
||||
const log = readfile('/tmp/AdGuardHome_update.log', 131072) || '';
|
||||
|
||||
return {
|
||||
state: running ? 'running' : failed ? 'failure' : log ? 'finished' : 'idle',
|
||||
log: log
|
||||
};
|
||||
}
|
||||
},
|
||||
startCoreUpdate: {
|
||||
args: { force: false },
|
||||
call: function(request) {
|
||||
if (coreUpdateRunning())
|
||||
return { started: false, reason: 'busy' };
|
||||
|
||||
const command = request.args.force
|
||||
? '/bin/sh /usr/share/AdGuardHome/update_core.sh force >/dev/null 2>&1 </dev/null &'
|
||||
: '/bin/sh /usr/share/AdGuardHome/update_core.sh >/dev/null 2>&1 </dev/null &';
|
||||
|
||||
return { started: system(['/bin/sh', '-c', command], 3000) == 0 };
|
||||
}
|
||||
},
|
||||
changeWebPassword: {
|
||||
args: { hash: '' },
|
||||
call: function(request) {
|
||||
const hash = request.args.hash;
|
||||
if (!match(hash, /^\$2[aby]\$(0[4-9]|[12][0-9]|3[01])\$[.\/A-Za-z0-9]{53}$/))
|
||||
return { changed: false, reason: 'invalid_hash' };
|
||||
|
||||
const conf = cursor();
|
||||
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
|
||||
'/etc/AdGuardHome.yaml';
|
||||
if (stat(configpath)?.type != 'file')
|
||||
return { changed: false, reason: 'no_config' };
|
||||
|
||||
if (!conf.set('AdGuardHome', 'AdGuardHome', 'hashpass', hash) ||
|
||||
!conf.commit('AdGuardHome'))
|
||||
return { changed: false, reason: 'save_failed' };
|
||||
conf.unload('AdGuardHome');
|
||||
|
||||
if (system(['/etc/init.d/AdGuardHome', 'reload'], 30000) != 0)
|
||||
return { changed: false, reason: 'reload_failed' };
|
||||
|
||||
if (index(readfile(configpath) || '', hash) < 0)
|
||||
return { changed: false, reason: 'verify_failed' };
|
||||
return { changed: true };
|
||||
}
|
||||
},
|
||||
setVerboseLog: {
|
||||
args: { enabled: false },
|
||||
call: function(request) {
|
||||
const conf = cursor();
|
||||
if (!conf.set('AdGuardHome', 'AdGuardHome', 'verbose', request.args.enabled ? '1' : '0') ||
|
||||
!conf.commit('AdGuardHome'))
|
||||
return { saved: false, applied: false };
|
||||
conf.unload('AdGuardHome');
|
||||
return {
|
||||
saved: true,
|
||||
applied: system(['/etc/init.d/AdGuardHome', 'reload'], 30000) == 0
|
||||
};
|
||||
}
|
||||
},
|
||||
getRuntimeLog: {
|
||||
args: { offset: 0, identity: '' },
|
||||
call: function(request) {
|
||||
const conf = cursor();
|
||||
const logfile = conf.get('AdGuardHome', 'AdGuardHome', 'logfile') || '';
|
||||
conf.unload('AdGuardHome');
|
||||
if (!logfile)
|
||||
return { enabled: false, localtime: false, content: '', offset: 0 };
|
||||
|
||||
let source = logfile;
|
||||
if (logfile == 'syslog') {
|
||||
source = '/tmp/AdGuardHometmp.log';
|
||||
writefile('/var/run/AdGuardHomesyslog', '1');
|
||||
if (stat(source)?.type != 'file')
|
||||
system(['/bin/sh', '-c', '/bin/sh /usr/share/AdGuardHome/getsyslog.sh >/dev/null 2>&1 &'], 3000);
|
||||
}
|
||||
|
||||
// Bound memory and start with the most recent 256 KiB on a new view.
|
||||
const limit = 262144;
|
||||
const info = stat(source);
|
||||
const identity = info ? source + ':' + info.dev.major + ':' + info.dev.minor + ':' + info.inode : '';
|
||||
let offset = int(request.args.offset) || 0;
|
||||
let reset = request.args.identity != identity || offset < 0 || offset > (info?.size || 0);
|
||||
if (reset)
|
||||
offset = 0;
|
||||
if (info && info.size - offset > limit) {
|
||||
offset = info.size - limit;
|
||||
reset = true;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
if (info?.type == 'file') {
|
||||
const file = open(source, 'r');
|
||||
if (!file)
|
||||
die('Unable to open runtime log');
|
||||
const positioned = file.seek(offset);
|
||||
const chunk = positioned ? file.read(limit) : null;
|
||||
file.close();
|
||||
if (chunk == null)
|
||||
die('Unable to read runtime log');
|
||||
content = chunk;
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
localtime: logfile != 'syslog',
|
||||
// ubus strings stop at NUL; cleared files can contain sparse zero-filled gaps.
|
||||
content_b64: b64enc(content),
|
||||
offset: offset + length(content),
|
||||
identity: identity,
|
||||
reset: reset
|
||||
};
|
||||
}
|
||||
},
|
||||
clearRuntimeLog: {
|
||||
call: function() {
|
||||
const conf = cursor();
|
||||
const logfile = conf.get('AdGuardHome', 'AdGuardHome', 'logfile') || '';
|
||||
conf.unload('AdGuardHome');
|
||||
if (!logfile)
|
||||
return { cleared: false, reason: 'no_logfile' };
|
||||
|
||||
const source = logfile == 'syslog' ? '/tmp/AdGuardHometmp.log' : logfile;
|
||||
return { cleared: writefile(source, '') != null };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
.CodeMirror{font-family:monospace;height:500px;color:black;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{background-color:white}.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:black}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid black;border-right:0;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-mark{background-color:rgba(20,255,20,0.5);-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-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:blue}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:bold}.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-variable-3,.cm-s-default .cm-type{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:white}.CodeMirror-scroll{overflow:scroll !important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{position:absolute;z-index:6;display:none}.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:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:none !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:transparent;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-scroll,.CodeMirror-sizer,.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber{-moz-box-siLine truncated
|
||||
-1
@@ -1 +0,0 @@
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.CodeMirror=t()}(this,function(){"use strict";var e=navigator.userAgent,t=navigator.platform,r=/gecko\/\d/i.test(e),n=/MSIE \d/.test(e),i=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(e),o=/Edge\/(\d+)/.exec(e),l=n||i||o,s=l&&(n?document.documentMode||6:+(o||i)[1]),a=!o&&/WebKit\//.test(e),u=a&&/Qt\/\d+\.\d+/.test(e),c=!o&&/Chrome\//.test(e),h=/Opera\//.test(e),f=/Apple Computer/.test(navigator.vendor),d=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(e),p=/PhantomJS/.test(e),g=!o&&/AppleWebKit/.test(e)&&/Mobile\/\w+/.test(e),v=/Android/.test(e),m=g||v||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(e),y=g||/Mac/.test(t),b=/\bCrOS\b/.test(e),w=/win/i.test(t),x=h&&e.match(/Version\/(\d*\.\d*)/);x&&(x=Number(x[1])),x&&x>=15&&(h=!1,a=!0);var C=y&&(u||h&&(null==x||x<12.11)),S=r||l&&s>=9;function L(e){return new RegExp("(^|\\s)"+e+"(?:$|\\s)\\s*")}var k,T=function(e,t){var r=e.className,n=L(t).exec(r);if(n){var i=r.slice(n.index+n[0].length);e.className=r.slice(0,n.index)+(i?n[1]+i:"")}};function M(e){for(var t=e.childNodes.length;t>0;--t)e.removeChild(e.firstChild);return e}function N(e,t){return M(e).appendChild(t)}function O(e,t,r,n){var i=document.createElement(e);if(r&&(i.className=r),n&&(i.style.cssText=n),"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 A(e,t,r,n){var i=O(e,t,r,n);return i.setAttribute("role","presentation"),i}function D(e,t){if(3==t.nodeType&&(t=t.parentNode),e.contains)return e.contains(t);do{if(11==t.nodeType&&(t=t.host),t==e)return!0}while(t=t.parentNode)}function W(){var e;try{e=document.activeElement}catch(t){e=document.body||null}for(;e&&e.shadowRoot&&e.shadowRoot.activeElement;)e=e.shadowRoot.activeElement;return e}function H(e,t){var r=e.className;L(t).test(r)||(e.className+=(r?" ":"")+t)}function F(e,t){for(var r=e.split(" "),n=0;n<r.length;n++)r[n]&&!L(r[n]).test(t)&&(t+=" "+r[n]);return t}k=document.createRange?function(e,t,r,n){var i=document.createRange();return i.setEnd(n||e,r),i.setStart(e,t),i}:function(e,t,r){var n=document.body.createTextRange();try{n.moveToElementText(e.parentNode)}catch(e){return n}return n.collapse(!0),n.moveEnd("character",r),n.moveStart("character",t),n};var P=function(e){e.select()};function E(e){var t=Array.prototype.slice.call(arguments,1);return function(){return e.apply(null,t)}}function I(e,t,r){for(var n in t||(t={}),e)!e.hasOwnProperty(n)||!1===r&&t.hasOwnProperty(n)||(t[n]=e[n]);return t}function z(e,t,r,n,i){null==t&&-1==(t=e.search(/[^\s\u00a0]/))&&(t=e.length);for(var o=n||0,l=i||0;;){var s=e.indexOf("\t",o);if(s<0||s>=t)return l+(t-o);l+=s-o,l+=r-l%r,o=s+1}}g?P=function(e){e.selectionStart=0,e.selectionEnd=e.value.length}:l&&(P=function(e){try{e.select()}catch(e){}});var R=function(){this.id=null,this.f=null,this.time=0,this.handler=E(this.onTimeout,this)};function B(e,t){for(var r=0;r<e.length;++r)if(e[r]==t)return r;return-1}R.prototype.onTimeout=function(e){e.id=0,e.time<=+new Date?e.f():setTimeout(e.handler,e.time-+new Date)},R.prototype.set=function(e,t){this.f=t;var r=+new Date+e;(!this.id||r<this.time)&&(clearTimeout(this.id),this.id=setTimeout(this.handler,e),this.time=r)};var G=30,U={toString:function(){return"CodeMirror.Pass"}},V={scroll:!1},K={origin:"*mouse"},j={origin:"+move"};function X(e,t,r){for(var n=0,i=0;;){var o=e.indexOf("\t",n);-1==o&&(o=e.length);var l=o-n;if(o==e.length||i+l>=t)return n+Math.min(l,t-i);if(i+=o-n,n=o+1,(i+=r-i%r)>=t)return n}}var Y=[""];function _(e){for(;Y.length<=e;)Y.push($(Y)+" ");return Y[e]}function $(e){return e[e.length-1]}function q(e,t){for(var r=[],n=0;n<e.length;n++)r[n]=t(e[n],n);return r}function Z(){}function Q(e,t){var r;return Object.create?r=Object.create(e):(Z.prototype=e,r=new Z),t&&I(t,r),r}var J=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;function ee(e){return/\w/.test(e)||e>""&&(e.toUpperCase()!=e.toLowerCase()||J.test(e))}function te(e,t){return t?!!(t.source.indexOf("\\w")>-1&&ee(e))||t.test(e):ee(e)}function re(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t])return!1;return!0}var ne=/[\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\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cdLine truncated
|
||||
-1
@@ -1 +0,0 @@
|
||||
!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 e=new RegExp("\\b(("+["true","false","on","off","yes","no"].join(")|(")+"))$","i");return{token:function(i,t){var r=i.peek(),n=t.escaped;if(t.escaped=!1,"#"==r&&(0==i.pos||/\s/.test(i.string.charAt(i.pos-1))))return i.skipToEnd(),"comment";if(i.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/))return"string";if(t.literal&&i.indentation()>t.keyCol)return i.skipToEnd(),"string";if(t.literal&&(t.literal=!1),i.sol()){if(t.keyCol=0,t.pair=!1,t.pairStart=!1,i.match(/---/))return"def";if(i.match(/\.\.\./))return"def";if(i.match(/\s*-\s+/))return"meta"}if(i.match(/^(\{|\}|\[|\])/))return"{"==r?t.inlinePairs++:"}"==r?t.inlinePairs--:"["==r?t.inlineList++:t.inlineList--,"meta";if(t.inlineList>0&&!n&&","==r)return i.next(),"meta";if(t.inlinePairs>0&&!n&&","==r)return t.keyCol=0,t.pair=!1,t.pairStart=!1,i.next(),"meta";if(t.pairStart){if(i.match(/^\s*(\||\>)\s*/))return t.literal=!0,"meta";if(i.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i))return"variable-2";if(0==t.inlinePairs&&i.match(/^\s*-?[0-9\.\,]+\s?$/))return"number";if(t.inlinePairs>0&&i.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/))return"number";if(i.match(e))return"keyword"}return!t.pair&&i.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)?(t.pair=!0,t.keyCol=i.indentation(),"atom"):t.pair&&i.match(/^:\s*/)?(t.pairStart=!0,"meta"):(t.pairStart=!1,t.escaped="\\"==r,i.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")});
|
||||
-1
@@ -1 +0,0 @@
|
||||
.cm-s-dracula.CodeMirror,.cm-s-dracula .CodeMirror-gutters{background-color:#282a36 !important;color:#f8f8f2 !important;border:0}.cm-s-dracula .CodeMirror-gutters{color:#282a36}.cm-s-dracula .CodeMirror-cursor{border-left:solid thin #f8f8f0}.cm-s-dracula .CodeMirror-linenumber{color:#6d8a88}.cm-s-dracula .CodeMirror-selected{background:rgba(255,255,255,0.10)}.cm-s-dracula .CodeMirror-line::selection,.cm-s-dracula .CodeMirror-line>span::selection,.cm-s-dracula .CodeMirror-line>span>span::selection{background:rgba(255,255,255,0.10)}.cm-s-dracula .CodeMirror-line::-moz-selection,.cm-s-dracula .CodeMirror-line>span::-moz-selection,.cm-s-dracula .CodeMirror-line>span>span::-moz-selection{background:rgba(255,255,255,0.10)}.cm-s-dracula span.cm-comment{color:#6272a4}.cm-s-dracula span.cm-string,.cm-s-dracula span.cm-string-2{color:#f1fa8c}.cm-s-dracula span.cm-number{color:#bd93f9}.cm-s-dracula span.cm-variable{color:#50fa7b}.cm-s-dracula span.cm-variable-2{color:white}.cm-s-dracula span.cm-def{color:#50fa7b}.cm-s-dracula span.cm-operator{color:#ff79c6}.cm-s-dracula span.cm-keyword{color:#ff79c6}.cm-s-dracula span.cm-atom{color:#bd93f9}.cm-s-dracula span.cm-meta{color:#f8f8f2}.cm-s-dracula span.cm-tag{color:#ff79c6}.cm-s-dracula span.cm-attribute{color:#50fa7b}.cm-s-dracula span.cm-qualifier{color:#50fa7b}.cm-s-dracula span.cm-property{color:#66d9ef}.cm-s-dracula span.cm-builtin{color:#50fa7b}.cm-s-dracula span.cm-variable-3,.cm-s-dracula span.cm-type{color:#ffb86c}.cm-s-dracula .CodeMirror-activeline-background{background:rgba(255,255,255,0.1)}.cm-s-dracula .CodeMirror-matchingbracket{text-decoration:underline;color:white !important}
|
||||
Loaded 100 of 1124 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user