mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-09-14 20:34:11 +08:00
Compare commits
102
Commits
4e37f23d47
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcd7d31b29 | ||
|
|
87faec277a | ||
|
|
d341a782d4 | ||
|
|
205be274cd | ||
|
|
622dd92021 | ||
|
|
141863e6d4 | ||
|
|
2e55e363ac | ||
|
|
86d911baf4 | ||
|
|
65075cb38f | ||
|
|
aff13cf0c6 | ||
|
|
7497dff620 | ||
|
|
639738b05d | ||
|
|
419c580042 | ||
|
|
7a00e14e57 | ||
|
|
8daa9142b3 | ||
|
|
42846bd15f | ||
|
|
e964d3fab4 | ||
|
|
7a1c066f8e | ||
|
|
7f998ecef4 | ||
|
|
eb846719e6 | ||
|
|
11dbae0135 | ||
|
|
52a65b099b | ||
|
|
eda104d43a | ||
|
|
be31648233 | ||
|
|
542084a121 | ||
|
|
fe313e7d23 | ||
|
|
09f0910b78 | ||
|
|
8778465a55 | ||
|
|
b81fb0f1c5 | ||
|
|
f12aa6537d | ||
|
|
9e7a5c5caf | ||
|
|
28c0067490 | ||
|
|
2ff8b68fd7 | ||
|
|
e7e55d191d | ||
|
|
344aea7ec2 | ||
|
|
4a4dfac1a2 | ||
|
|
7b5d12b8ec | ||
|
|
51ed2622f8 | ||
|
|
35c80a77bc | ||
|
|
ec44f6fe2e | ||
|
|
37eb246de8 | ||
|
|
47831f020d | ||
|
|
02766f9a2f | ||
|
|
825070ba95 | ||
|
|
ccd677de3a | ||
|
|
0b8a2fa292 | ||
|
|
063eca5fe2 | ||
|
|
f3074ed22d | ||
|
|
995946de02 | ||
|
|
2cd4730a13 | ||
|
|
440cac507b | ||
|
|
ec01a08d9d | ||
|
|
32c534ae26 | ||
|
|
d0a6e27e6e | ||
|
|
b37f8cba7c | ||
|
|
d97c0d5630 | ||
|
|
6c8d00d18c | ||
|
|
e28fd0631a | ||
|
|
029903be2e | ||
|
|
ae2ca8d80b | ||
|
|
c0693a7678 | ||
|
|
3441d66762 | ||
|
|
3003d5612b | ||
|
|
beae9d285e | ||
|
|
86b8972e8e | ||
|
|
e9e78405e1 | ||
|
|
030ff01603 | ||
|
|
5eaaccaf0c | ||
|
|
6041d81065 | ||
|
|
61fbf62840 | ||
|
|
b7127b27d1 | ||
|
|
d53ac45366 | ||
|
|
f959ff8b68 | ||
|
|
be2557c9dc | ||
|
|
414512419c | ||
|
|
72ace44e3b | ||
|
|
4fe683a80b | ||
|
|
0751a1e7d3 | ||
|
|
a8ecc43b8a | ||
|
|
b09780b109 | ||
|
|
a93b4d6f7f | ||
|
|
8e9cbe68d9 | ||
|
|
0ef940bda7 | ||
|
|
79f234d9d7 | ||
|
|
1de7860148 | ||
|
|
587815b344 | ||
|
|
9a3f194de9 | ||
|
|
377119697a | ||
|
|
f5a34d6fcf | ||
|
|
1bfa02b7c5 | ||
|
|
d64e131165 | ||
|
|
cf3357ea3a | ||
|
|
ba70403e29 | ||
|
|
51af012771 | ||
|
|
d6f55c5f93 | ||
|
|
d16e9820fe | ||
|
|
a3b0b250ea | ||
|
|
a438ed414d | ||
|
|
e11e6e5941 | ||
|
|
58ae8797b2 | ||
|
|
c37bb21446 | ||
|
|
0f2308381e |
@@ -114,7 +114,7 @@ jobs:
|
||||
git_clone https://github.com/peter-tank/luci-app-autorepeater
|
||||
git_clone https://github.com/sirpdboy/luci-app-cupsd cupsd1 && mv -n cupsd1/{luci-app-cupsd,cups} ./ ; rm -rf cupsd1
|
||||
git_sparse_clone v5 https://github.com/sbwml/luci-app-mosdns \
|
||||
mosdns luci-app-mosdns v2dat
|
||||
mosdns luci-app-mosdns geo2txt
|
||||
) &
|
||||
(
|
||||
git_clone https://github.com/esirplayground/LingTiGameAcc
|
||||
@@ -149,10 +149,12 @@ jobs:
|
||||
) &
|
||||
(
|
||||
git_clone https://github.com/QiuSimons/vmlinux-btf
|
||||
git_sparse_clone kix https://github.com/QiuSimons/luci-app-daed \
|
||||
luci-app-daed daed
|
||||
git_sparse_clone kix https://github.com/QiuSimons/luci-app-dae \
|
||||
luci-app-dae dae
|
||||
# git_sparse_clone kix https://github.com/QiuSimons/luci-app-daed \
|
||||
# luci-app-daed daed
|
||||
# git_sparse_clone kix https://github.com/QiuSimons/luci-app-dae \
|
||||
# luci-app-dae dae
|
||||
git_sparse_clone master https://github.com/kenzok8/small \
|
||||
dae daed luci-app-daede
|
||||
git_sparse_clone main https://github.com/djylb/nps-openwrt \
|
||||
npc luci-app-npc
|
||||
git_sparse_clone main https://github.com/smallprogram/openwrt-ghfu \
|
||||
@@ -175,6 +177,8 @@ jobs:
|
||||
msd_lite luci-app-msd_lite
|
||||
git_sparse_clone master https://github.com/fengqi/luci-app-uugamebooster \
|
||||
uugamebooster luci-app-uugamebooster
|
||||
git_sparse_clone master https://github.com/kenzok8/small \
|
||||
clashoo luci-app-clashoo
|
||||
) &
|
||||
(
|
||||
git_clone https://github.com/muink/luci-app-dnsproxy
|
||||
|
||||
+4
-4
@@ -7,13 +7,13 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=alist
|
||||
PKG_VERSION:=3.63.0
|
||||
PKG_WEB_VERSION:=3.63.0
|
||||
PKG_VERSION:=3.64.0
|
||||
PKG_WEB_VERSION:=3.64.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/AlistGo/alist/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=a850506424e695bbbf411a928711588bcb8274d64b88029ecf0856cc4c21763b
|
||||
PKG_HASH:=599065ec4b26375e4012cf212415f4195c9b92864bdcde4105356261920563f4
|
||||
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
PKG_LICENSE_FILE:=LICENSE
|
||||
@@ -23,7 +23,7 @@ define Download/$(PKG_NAME)-web
|
||||
FILE:=$(PKG_NAME)-web-$(PKG_WEB_VERSION).tar.gz
|
||||
URL_FILE:=dist.tar.gz
|
||||
URL:=https://github.com/AlistGo/alist-web/releases/download/$(PKG_WEB_VERSION)/
|
||||
HASH:=336f86ec867045c8b401a5d80a1e51af495d7d2487e23f48b925030bbebffe2e
|
||||
HASH:=b80550662de42a2f8a72d35bca8ed66ec9ad0c24a9a02ceec6a01bf5038cf6f4
|
||||
endef
|
||||
|
||||
PKG_BUILD_DEPENDS:=golang/host
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=BaiduPCS-Go
|
||||
PKG_VERSION:=3.9.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/qjfoidnh/BaiduPCS-Go/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
From: coolsnowwolf <coolsnowwolf@gmail.com>
|
||||
Subject: [PATCH] cachepool: stop using runtime.rawbyteslice
|
||||
|
||||
Recent Go releases reject external linkname references to rawbyteslice.
|
||||
Use the language-provided slice allocator for the zeroed allocation path.
|
||||
|
||||
Signed-off-by: coolsnowwolf <coolsnowwolf@gmail.com>
|
||||
---
|
||||
--- a/pcsutil/cachepool/malloc.go
|
||||
+++ b/pcsutil/cachepool/malloc.go
|
||||
@@ -8,12 +8,9 @@
|
||||
//go:linkname mallocgc runtime.mallocgc
|
||||
func mallocgc(size uintptr, typ uintptr, needzero bool) unsafe.Pointer
|
||||
|
||||
-//go:linkname rawbyteslice runtime.rawbyteslice
|
||||
-func rawbyteslice(size int) (b []byte)
|
||||
-
|
||||
-// RawByteSlice point to runtime.rawbyteslice
|
||||
+// RawByteSlice allocates a zeroed byte slice.
|
||||
func RawByteSlice(size int) (b []byte) {
|
||||
- return rawbyteslice(size)
|
||||
+ return make([]byte, size)
|
||||
}
|
||||
|
||||
// RawMalloc allocates a new slice. The slice is not zeroed.
|
||||
@@ -0,0 +1,111 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=clashoo
|
||||
PKG_SHORT_SHA:=dc66a8a
|
||||
PKG_COMMIT_DATE:=2026.09.14
|
||||
|
||||
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
|
||||
PKG_SOURCE_VERSION:=dc66a8a180c0fa1c2b91cf5413426e68dbca320e
|
||||
PKG_HASH:=e7003359dc55a62a6f31b45f9123fc14a0a58f66c8cc4a3514ab15e94f08008b
|
||||
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
|
||||
|
||||
PKG_LICENSE:=GPL3.0+
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
|
||||
PKG_BUILD_DEPENDS:=golang/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
||||
|
||||
GO_PKG:=github.com/metacubex/mihomo
|
||||
GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
|
||||
GO_PKG_TAGS:=with_gvisor
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
define Package/clashoo
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
TITLE:=Clashoo (Mihomo)
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle +yq +firewall4 +ip-full +coreutils-nohup +coreutils-timeout +kmod-inet-diag +kmod-nft-socket +kmod-nft-tproxy +kmod-tun +kmod-dummy +unzip
|
||||
PROVIDES:=mihomo clash-meta
|
||||
endef
|
||||
|
||||
define Package/clashoo/description
|
||||
Mihomo proxy core with runtime environment for Clashoo.
|
||||
endef
|
||||
|
||||
define Package/clashoo/conffiles
|
||||
/etc/config/clashoo
|
||||
endef
|
||||
|
||||
define Package/clashoo/install
|
||||
$(call GoPackage/Package/Install/Bin,$(1))
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
ln -sf /usr/bin/mihomo $(1)/usr/bin/clash-meta
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/etc/clashoo
|
||||
$(INSTALL_DIR) $(1)/etc/clashoo/provider
|
||||
$(INSTALL_DIR) $(1)/etc/clashoo/proxyprovider
|
||||
$(INSTALL_DIR) $(1)/etc/clashoo/ruleprovider
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashoo
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashbackup
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashoo/config
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/sub
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/upload
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/custom
|
||||
|
||||
$(INSTALL_BIN) $(CURDIR)/files/etc/init.d/clashoo $(1)/etc/init.d/clashoo
|
||||
$(INSTALL_CONF) $(CURDIR)/files/etc/config/clashoo $(1)/etc/config/clashoo
|
||||
$(INSTALL_DATA) $(CURDIR)/files/etc/config/clashoo $(1)/usr/share/clashoo/clashoo.default
|
||||
$(CP) $(CURDIR)/files/usr/share/clashoo/* $(1)/usr/share/clashoo/
|
||||
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/migrate_lan_acl.sh $(1)/usr/share/clashoo/migrate_lan_acl.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/migrate_fake_ip_filter.sh $(1)/usr/share/clashoo/migrate_fake_ip_filter.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/detect_core_arch.sh $(1)/usr/share/clashoo/detect_core_arch.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/diagnose_bypass.sh $(1)/usr/share/clashoo/diagnose_bypass.sh
|
||||
|
||||
$(RM) $(1)/usr/share/clashoo/dashboard
|
||||
$(RM) $(1)/usr/share/clashoo/yacd
|
||||
$(RM) $(1)/usr/share/clashoo/update.log
|
||||
$(RM) $(1)/usr/share/clashoo/geoip.log
|
||||
$(RM) $(1)/usr/share/clashoo/core_down_complete
|
||||
endef
|
||||
|
||||
define Package/clashoo/postinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
[ -x /usr/bin/mihomo ] && [ ! -x /usr/bin/clash-meta ] && ln -sf /usr/bin/mihomo /usr/bin/clash-meta
|
||||
rm -f /usr/share/clashoo/check_luci_version.sh /usr/share/clashoo/check_clash_meta_version.sh /usr/share/clashoo/check_mihomo_core_version.sh
|
||||
rm -f /usr/share/clashoo/cuslist.sh /usr/share/clashoo/groups.sh /usr/share/clashoo/load_groups.sh /usr/share/clashoo/panel_diag.sh
|
||||
rm -f /usr/share/clashoo/rmlist.sh /usr/share/clashoo/uplist.sh /usr/share/clashoo/rpc_restart_async.sh /usr/share/clashoo/rpc_update_china_ip_async.sh
|
||||
rm -rf /usr/share/clashoo/create
|
||||
/usr/share/clashoo/migrate_lan_acl.sh >/dev/null 2>&1 || true
|
||||
/usr/share/clashoo/migrate_fake_ip_filter.sh >/dev/null 2>&1 || true
|
||||
/usr/share/clashoo/detect_core_arch.sh >/dev/null 2>&1 || true
|
||||
if [ "$$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then
|
||||
/etc/init.d/clashoo enable 2>/dev/null || true
|
||||
else
|
||||
/etc/init.d/clashoo disable 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
$(Build/Prepare/Default)
|
||||
$(RM) -r $(PKG_BUILD_DIR)/rules/logic_test
|
||||
endef
|
||||
|
||||
$(eval $(call GoBinPackage,clashoo))
|
||||
$(eval $(call BuildPackage,clashoo))
|
||||
@@ -0,0 +1,158 @@
|
||||
|
||||
config clashoo 'config'
|
||||
option redir_port '7891'
|
||||
option http_port '8080'
|
||||
option socks_port '1080'
|
||||
option mixed_port '7890'
|
||||
option enable_ipv6 'true'
|
||||
option enable '0'
|
||||
option dash_port '9090'
|
||||
option dashboard_panel 'zashboard'
|
||||
option level 'info'
|
||||
option core '3'
|
||||
option allow_lan '1'
|
||||
option dnsforwader '1'
|
||||
option subcri 'meta'
|
||||
option cusrule '0'
|
||||
option dnscache '1'
|
||||
option p_mode 'rule'
|
||||
option append_rules '0'
|
||||
option enhanced_mode 'fake-ip'
|
||||
option fake_ip_range '198.18.0.1/16'
|
||||
option fake_ip_range6 'fc00::/18'
|
||||
list fake_ip_filter '+.lan'
|
||||
list fake_ip_filter '+.local'
|
||||
list fake_ip_filter 'localhost.ptlogin2.qq.com'
|
||||
list fake_ip_filter 'rule-set:cn_domain'
|
||||
option fake_ip_filter_migrated '1'
|
||||
option listen_port '1053'
|
||||
option interf '0'
|
||||
option download_core 'amd64-compatible'
|
||||
option dcore '3'
|
||||
option same_tag '1'
|
||||
option geoip_source '3'
|
||||
option geoip_format 'mmdb'
|
||||
option geodata_loader 'standard'
|
||||
option tproxy_port '7982'
|
||||
option access_control '0'
|
||||
option acl_migrated '1'
|
||||
option tcp_mode 'redirect'
|
||||
option tun_mode '0'
|
||||
option core_mirror_prefix 'https://gh-proxy.com/'
|
||||
option core_download_advanced '0'
|
||||
list default_nameserver '223.5.5.5'
|
||||
list default_nameserver '119.29.29.29'
|
||||
option dns_ecs ''
|
||||
option dns_ecs_override '0'
|
||||
option fallback_filter_geoip '0'
|
||||
list fallback_filter_ipcidr '240.0.0.0/4'
|
||||
option dns_respect_rules '1'
|
||||
option dns_loopback_compat '0'
|
||||
list dns_loopback_compat_resolver 'udp://223.5.5.5:53'
|
||||
list dns_loopback_compat_resolver 'udp://119.29.29.29:53'
|
||||
option dns_migrated '1'
|
||||
option block_quic '0'
|
||||
option singbox_independent_cache '0'
|
||||
option udp_mode 'tproxy'
|
||||
option ipv4_dns_hijack '1'
|
||||
option ipv6_dns_hijack '1'
|
||||
option ipv4_proxy '1'
|
||||
option ipv6_proxy '1'
|
||||
option fake_ip_ping_hijack '1'
|
||||
option dns_leak_protect '0'
|
||||
option geoip_mmdb_url 'https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb'
|
||||
option geosite_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat'
|
||||
option geoip_dat_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat'
|
||||
option enable_udp '1'
|
||||
option stack 'gvisor'
|
||||
option bypass_china '1'
|
||||
option bypass_china_ipv6 '1'
|
||||
option bypass_port_mode 'common'
|
||||
option sniffer_streaming '1'
|
||||
option selection_cache '1'
|
||||
option fake_ip_cache '1'
|
||||
option enable_dns '1'
|
||||
option disable_quic_gso '1'
|
||||
option smart_policy_priority 'Premium:0.9;SG:1.3'
|
||||
option smart_prefer_asn '1'
|
||||
option smart_uselightgbm '1'
|
||||
option smart_collectdata '1'
|
||||
option smart_collect_size '100'
|
||||
option smart_collect_rate '1'
|
||||
option smart_lgbm_auto_update '1'
|
||||
option smart_lgbm_update_interval '72'
|
||||
option smart_lgbm_url 'https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin'
|
||||
list defaul_nameserver '114.114.114.114'
|
||||
list defaul_nameserver '223.5.5.5'
|
||||
option auto_update '1'
|
||||
option auto_update_time '72'
|
||||
option auto_subscription_update '0'
|
||||
option subscription_update_interval '72'
|
||||
option auto_clear_log '1'
|
||||
option clear_time '72'
|
||||
option core_type 'mihomo'
|
||||
option smart_auto_switch '0'
|
||||
option core_only '0'
|
||||
|
||||
config hosts
|
||||
option enabled '0'
|
||||
option adress '*.clash.dev'
|
||||
option ip '127.0.0.1'
|
||||
|
||||
config hosts
|
||||
option enabled '0'
|
||||
option adress 'alpha.clash.dev'
|
||||
option ip '::1'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'nameserver'
|
||||
option ser_address 'https://dns.alidns.com/dns-query'
|
||||
option protocol 'none'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'nameserver'
|
||||
option ser_address 'https://doh.pub/dns-query'
|
||||
option protocol 'none'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'proxy-server-nameserver'
|
||||
option ser_address '1.1.1.1'
|
||||
option protocol 'tls://'
|
||||
option ser_port '853'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'fallback'
|
||||
option ser_address 'https://cloudflare-dns.com/dns-query'
|
||||
option protocol 'none'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'fallback'
|
||||
option ser_address 'https://dns.google/dns-query'
|
||||
option protocol 'none'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'direct-nameserver'
|
||||
option ser_address '223.5.5.5'
|
||||
option protocol 'udp://'
|
||||
|
||||
config dns_policy
|
||||
option enabled '1'
|
||||
option policy_type 'nameserver-policy'
|
||||
option matcher 'geosite:cn'
|
||||
list nameserver 'https://dns.alidns.com/dns-query'
|
||||
list nameserver 'https://doh.pub/dns-query'
|
||||
|
||||
config authentication
|
||||
|
||||
# default empty custom-rule row so the section shows the input form, not just an add
|
||||
# button; injectors skip rules with an empty ipaaddr, so it generates nothing until filled
|
||||
config addtype
|
||||
option type 'DOMAIN-SUFFIX'
|
||||
option pgroup 'DIRECT'
|
||||
option ipaaddr ''
|
||||
Executable
+2403
File diff suppressed because it is too large
Load Diff
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ -n "$(uci -q get clashoo.config.download_core 2>/dev/null)" ] && exit 0
|
||||
|
||||
raw="$(uname -m 2>/dev/null)"
|
||||
[ -n "$raw" ] || raw="$(apk --print-arch 2>/dev/null)"
|
||||
[ -n "$raw" ] || raw="$(opkg status libc 2>/dev/null | awk -F': ' '/^Architecture/{print $2; exit}')"
|
||||
[ -n "$raw" ] || exit 0
|
||||
|
||||
case "$raw" in
|
||||
x86_64|amd64) arch="amd64-compatible" ;;
|
||||
aarch64*|arm64) arch="arm64" ;;
|
||||
armv7*|arm_cortex-a[7-9]*|arm_cortex-a1[0-9]*) arch="armv7" ;;
|
||||
armv6*|arm_cortex-a[56]*) arch="armv6" ;;
|
||||
arm*) arch="armv5" ;;
|
||||
i[3-6]86) arch="386" ;;
|
||||
mips64el*) arch="mips64le" ;;
|
||||
mips64*) arch="mips64" ;;
|
||||
mipsel*) arch="mipsle" ;;
|
||||
mips*) arch="mips" ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
uci -q set clashoo.config.download_core="$arch"
|
||||
uci -q commit clashoo
|
||||
exit 0
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/bin/sh
|
||||
|
||||
umask 077
|
||||
|
||||
CONFIG_YAML="${CONFIG_YAML:-/etc/clashoo/config.yaml}"
|
||||
NFT_V4="${NFT_V4:-/usr/share/clashoo/nftables/geoip_cn.nft}"
|
||||
NFT_V6="${NFT_V6:-/usr/share/clashoo/nftables/geoip6_cn.nft}"
|
||||
MAX_CONN=200
|
||||
|
||||
TMPDIR_D="$(mktemp -d /tmp/.clashoo_diag.XXXXXX 2>/dev/null)" || exit 1
|
||||
trap 'rm -rf "$TMPDIR_D" 2>/dev/null' EXIT INT TERM
|
||||
|
||||
uci_get() {
|
||||
uci -q get "clashoo.config.$1" 2>/dev/null
|
||||
}
|
||||
|
||||
section() {
|
||||
printf '\n===== %s =====\n' "$1"
|
||||
}
|
||||
|
||||
yesno() {
|
||||
[ -n "$1" ] && [ "$1" != "0" ] && [ "$1" != "false" ] && echo "on" || echo "off"
|
||||
}
|
||||
|
||||
bool_on() {
|
||||
case "$1" in
|
||||
1|true|TRUE|yes|on) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
bypass4="$(uci_get bypass_china)"
|
||||
bypass6="$(uci_get bypass_china_ipv6)"
|
||||
[ -n "$bypass6" ] || bypass6="$bypass4"
|
||||
|
||||
section "配置摘要"
|
||||
printf '服务开关 : %s\n' "$(yesno "$(uci_get enable)")"
|
||||
printf '运行模式 : %s\n' "$(uci_get enhanced_mode)"
|
||||
printf 'TCP / UDP : %s / %s\n' "$(uci_get tcp_mode)" "$(uci_get udp_mode)"
|
||||
printf '大陆绕过 v4 : %s\n' "$(yesno "$bypass4")"
|
||||
printf '大陆绕过 v6 : %s%s\n' "$(yesno "$bypass6")" \
|
||||
"$([ -z "$(uci_get bypass_china_ipv6)" ] && echo '(未设置,跟随 v4)')"
|
||||
printf '接管端口 : %s' "$(uci_get bypass_port_mode)"
|
||||
[ "$(uci_get bypass_port_mode)" = "custom" ] && printf ' (%s)' "$(uci_get bypass_port_custom)"
|
||||
printf '\n'
|
||||
printf 'IPv6 代理 : %s\n' "$(yesno "$(uci_get ipv6_proxy)")"
|
||||
printf '阻断 QUIC : %s\n' "$(yesno "$(uci_get block_quic)")"
|
||||
|
||||
if [ "$(uci_get bypass_port_mode)" = "common" ]; then
|
||||
printf '\n注意: 接管端口为 common,只代理 22/53/80/443/8080/8443,\n'
|
||||
printf ' 游戏与 P2P 常用的高位端口不会走代理。\n'
|
||||
fi
|
||||
|
||||
section "fake-ip 过滤"
|
||||
if [ -s "$CONFIG_YAML" ]; then
|
||||
if grep -qE '^[[:space:]]*fake-ip-filter:' "$CONFIG_YAML" 2>/dev/null; then
|
||||
filter_mode="$(uci_get fake_ip_filter_mode)"
|
||||
[ -n "$filter_mode" ] || filter_mode="$(yq -M e '(.dns."fake-ip-filter-mode" // "blacklist") | tostring' "$CONFIG_YAML" 2>/dev/null)"
|
||||
if [ "$filter_mode" = "whitelist" ]; then
|
||||
echo "白名单模式:列表中的域名使用 fake-IP"
|
||||
elif grep -qiE 'rule-set:cn_domain|RULE-SET,[[:space:]]*cn_domain,[[:space:]]*real-ip' "$CONFIG_YAML" 2>/dev/null; then
|
||||
echo "含 rule-set:cn_domain (国内域名返回真实 IP)"
|
||||
else
|
||||
echo "缺少 rule-set:cn_domain"
|
||||
echo " 国内域名会拿到 fake-ip 走代理,与硬编码国内 IP 分成两条路。"
|
||||
fi
|
||||
else
|
||||
echo "配置中无 fake-ip-filter"
|
||||
fi
|
||||
else
|
||||
echo "找不到 $CONFIG_YAML"
|
||||
fi
|
||||
|
||||
section "内核分流规则"
|
||||
if [ -s "$CONFIG_YAML" ]; then
|
||||
echo "-- rules 末尾 5 条 --"
|
||||
grep -E '^[[:space:]]*-[[:space:]]*["'"'"']?(GEOIP|RULE-SET|MATCH|IP-CIDR|DOMAIN)' "$CONFIG_YAML" 2>/dev/null | tail -5
|
||||
fi
|
||||
|
||||
section "IP 库"
|
||||
for f in "$NFT_V4" "$NFT_V6"; do
|
||||
if [ -s "$f" ]; then
|
||||
printf '%-46s %6s 条 %s\n' "$f" \
|
||||
"$(grep -cE '^[[:space:]]*[0-9a-fA-F].*/' "$f" 2>/dev/null)" \
|
||||
"$(date -r "$f" '+%Y-%m-%d %H:%M' 2>/dev/null)"
|
||||
else
|
||||
printf '%-46s 缺失\n' "$f"
|
||||
fi
|
||||
done
|
||||
for f in /etc/clashoo/Country.mmdb /etc/clashoo/geoip.metadb; do
|
||||
[ -f "$f" ] && printf '%-46s %s\n' "$f" "$(ls -lh "$f" 2>/dev/null | awk '{print $5}')"
|
||||
done
|
||||
|
||||
set_count() {
|
||||
nft list set inet fw4 "$1" 2>/dev/null | tr ',' '\n' | grep -cE '[0-9a-fA-F]+[.:].*/'
|
||||
}
|
||||
|
||||
section "防火墙绕过规则"
|
||||
if nft list sets inet fw4 2>/dev/null | grep -q clashoo_china; then
|
||||
for ch in dstnat mangle_prerouting; do
|
||||
nft -a list chain inet fw4 "$ch" 2>/dev/null |
|
||||
grep -E '@clashoo_china6? ' | sed "s|^[[:space:]]*| ${ch}: |"
|
||||
done
|
||||
echo
|
||||
echo "已加载元素数(nft auto-merge 会合并相邻网段,少于文件行数属正常):"
|
||||
bool_on "$bypass4" && printf ' clashoo_china : %s\n' "$(set_count clashoo_china)"
|
||||
bool_on "$bypass6" && printf ' clashoo_china6 : %s\n' "$(set_count clashoo_china6)"
|
||||
else
|
||||
echo "当前 nftables 中没有 clashoo_china 集合(绕过未启用或服务未运行)"
|
||||
fi
|
||||
|
||||
section "绕过命中检查"
|
||||
if ! bool_on "$bypass4" && ! bool_on "$bypass6"; then
|
||||
echo "两个绕过均未启用,所有被接管的流量都交给内核判定,无需检查"
|
||||
else
|
||||
SECRET="$(uci_get dash_pass)"
|
||||
PORT="$(uci_get dash_port)"
|
||||
LAN_IP="$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F/ '{print $1}')"
|
||||
[ -n "$LAN_IP" ] || LAN_IP="127.0.0.1"
|
||||
CONN_JSON="$TMPDIR_D/connections.json"
|
||||
|
||||
if [ -z "$PORT" ]; then
|
||||
echo "未配置控制端口,跳过"
|
||||
elif ! curl -m 5 -s -H "Authorization: Bearer ${SECRET}" \
|
||||
"http://${LAN_IP}:${PORT}/connections" -o "$CONN_JSON" 2>/dev/null; then
|
||||
echo "无法访问内核 API (http://${LAN_IP}:${PORT}),跳过"
|
||||
elif [ ! -s "$CONN_JSON" ]; then
|
||||
echo "内核 API 返回为空,跳过"
|
||||
else
|
||||
jsonfilter -i "$CONN_JSON" -e '@["connections"][*]["metadata"]["destinationIP"]' \
|
||||
2>/dev/null > "${CONN_JSON}.ip"
|
||||
jsonfilter -i "$CONN_JSON" -e '@["connections"][*]["chains"][0]' \
|
||||
2>/dev/null > "${CONN_JSON}.ch"
|
||||
|
||||
total="$(wc -l < "${CONN_JSON}.ip" 2>/dev/null | tr -d ' ')"
|
||||
[ -n "$total" ] || total=0
|
||||
|
||||
if [ "$total" -eq 0 ]; then
|
||||
echo "当前无活动连接"
|
||||
elif [ "$total" != "$(wc -l < "${CONN_JSON}.ch" 2>/dev/null | tr -d ' ')" ]; then
|
||||
echo "内核 API 字段数不一致,跳过"
|
||||
else
|
||||
awk 'NR==FNR { a[FNR] = $0; next } { print a[FNR] "\t" $0 }' \
|
||||
"${CONN_JSON}.ip" "${CONN_JSON}.ch" 2>/dev/null \
|
||||
| awk -F'\t' '$2 != "DIRECT" && $1 != "" && $1 !~ /^(198\.18\.|::|fc00:|fd)/ { print $1 "\t" $2 }' \
|
||||
| sort -u | head -n "$MAX_CONN" > "${CONN_JSON}.chk"
|
||||
|
||||
hit=0
|
||||
: > "${CONN_JSON}.bad"
|
||||
while IFS="$(printf '\t')" read -r dip chain; do
|
||||
[ -n "$dip" ] || continue
|
||||
case "$dip" in
|
||||
*:*) bool_on "$bypass6" || continue
|
||||
set_name=clashoo_china6 ;;
|
||||
*) bool_on "$bypass4" || continue
|
||||
set_name=clashoo_china ;;
|
||||
esac
|
||||
if nft get element inet fw4 "$set_name" "{ $dip }" >/dev/null 2>&1; then
|
||||
hit=$((hit + 1))
|
||||
printf ' %-40s -> %s\n' "$dip" "$chain" >> "${CONN_JSON}.bad"
|
||||
fi
|
||||
done < "${CONN_JSON}.chk"
|
||||
|
||||
printf '活动连接 %s 条,其中走代理的去重目的地 %s 个(上限 %s)\n' \
|
||||
"$total" "$(wc -l < "${CONN_JSON}.chk" 2>/dev/null | tr -d ' ')" "$MAX_CONN"
|
||||
if [ "$hit" -gt 0 ]; then
|
||||
printf '命中绕过白名单却仍走代理: %s 个\n\n' "$hit"
|
||||
head -20 "${CONN_JSON}.bad"
|
||||
echo
|
||||
echo "这些目的地在防火墙白名单内,本应直连,却出现在代理链路上。"
|
||||
echo "可能是连接建立于规则变更之前,或该地址族的绕过规则未生效。"
|
||||
else
|
||||
echo "未发现白名单地址走代理的连接"
|
||||
fi
|
||||
rm -f "${CONN_JSON}.chk" "${CONN_JSON}.bad" 2>/dev/null
|
||||
fi
|
||||
rm -f "${CONN_JSON}.ip" "${CONN_JSON}.ch" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile } from 'fs';
|
||||
|
||||
// Standalone sing-box config version-migration, used by the init.d core-only
|
||||
// path to upgrade an imported config (momo/homeproxy/etc.) to the format the
|
||||
// current sing-box core accepts, WITHOUT clashoo normalize/takeover.
|
||||
//
|
||||
// IMPORTANT: migrate_singbox() below is kept byte-for-byte in sync with the
|
||||
// copy in luci-app-clashoo/.../luci.clashoo (the UI "migrate" button). If you
|
||||
// change one, change the other.
|
||||
|
||||
function migrate_singbox(cfg) {
|
||||
// --- Fix DNS servers: address-based → type-based, remove detour ---
|
||||
if (cfg.dns && cfg.dns.servers) {
|
||||
cfg.dns.servers = map(cfg.dns.servers || [], function(srv) {
|
||||
let addr = srv.address || '';
|
||||
let new_srv = {};
|
||||
let skip = { address: 1, detour: 1, address_resolver: 1, address_strategy: 1 };
|
||||
for (let k in srv) if (!skip[k]) new_srv[k] = srv[k];
|
||||
|
||||
if (addr === 'fakeip') {
|
||||
new_srv.type = 'fakeip';
|
||||
} else if (addr === 'local' || addr === '') {
|
||||
new_srv.type = new_srv.type || 'local';
|
||||
} else if (match(addr, /^rcode:\/\//) || addr === 'rcode') {
|
||||
new_srv._rcode = true; // mark for removal, handled via dns rule action
|
||||
} else if (match(addr, /^h3:\/\//)) {
|
||||
let host = replace(replace(addr, /^h3:\/\//, ''), /\/.*$/, '');
|
||||
new_srv.type = 'h3'; new_srv.server = host;
|
||||
} else if (match(addr, /^https:\/\//)) {
|
||||
let host = replace(replace(addr, /^https:\/\//, ''), /\/.*$/, '');
|
||||
new_srv.type = 'https'; new_srv.server = host;
|
||||
} else if (match(addr, /^tls:\/\//)) {
|
||||
new_srv.type = 'tls'; new_srv.server = replace(addr, /^tls:\/\//, '');
|
||||
} else if (match(addr, /^tcp:\/\//)) {
|
||||
new_srv.type = 'tcp'; new_srv.server = replace(addr, /^tcp:\/\//, '');
|
||||
} else if (addr && !new_srv.type) {
|
||||
new_srv.type = 'udp'; new_srv.server = addr; // plain IP → UDP
|
||||
} else if (addr) {
|
||||
new_srv.address = addr; // unknown format, keep as-is
|
||||
}
|
||||
return new_srv;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Remove rcode servers (unsupported in 1.12+ new format), convert referencing rules to reject ---
|
||||
let rcode_tags = {};
|
||||
if (cfg.dns && cfg.dns.servers) {
|
||||
let good = [];
|
||||
for (let srv in cfg.dns.servers) {
|
||||
if (srv._rcode) rcode_tags[srv.tag] = true;
|
||||
else push(good, srv);
|
||||
}
|
||||
cfg.dns.servers = good;
|
||||
}
|
||||
if (cfg.dns && cfg.dns.rules && length(keys(rcode_tags)) > 0) {
|
||||
cfg.dns.rules = map(cfg.dns.rules || [], function(rule) {
|
||||
if (rule.server && rcode_tags[rule.server]) {
|
||||
let r = {};
|
||||
for (let k in rule) if (k !== 'server' && k !== 'action') r[k] = rule[k];
|
||||
r.action = 'reject';
|
||||
return r;
|
||||
}
|
||||
return rule;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Remove legacy dns.fakeip.enabled (1.12+ fakeip is configured via type:"fakeip" server) ---
|
||||
if (cfg.dns && cfg.dns.fakeip) {
|
||||
let fp = cfg.dns.fakeip;
|
||||
let new_fp = {};
|
||||
for (let k in fp) if (k !== 'enabled') new_fp[k] = fp[k];
|
||||
if (length(keys(new_fp)) > 0) cfg.dns.fakeip = new_fp;
|
||||
else delete cfg.dns['fakeip'];
|
||||
}
|
||||
|
||||
// --- Fix DNS rules: add action:"route" when server is set ---
|
||||
if (cfg.dns && cfg.dns.rules) {
|
||||
cfg.dns.rules = map(cfg.dns.rules || [], function(rule) {
|
||||
if (rule.server && !rule.action) {
|
||||
let r = {};
|
||||
for (let k in rule) r[k] = rule[k];
|
||||
r.action = 'route';
|
||||
return r;
|
||||
}
|
||||
return rule;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Fix legacy special outbounds (block / dns types) ---
|
||||
let special = {};
|
||||
if (cfg.outbounds) {
|
||||
let kept = [];
|
||||
for (let ob in cfg.outbounds) {
|
||||
if (ob.type === 'block' || ob.type === 'dns') {
|
||||
special[ob.tag] = ob.type;
|
||||
} else {
|
||||
push(kept, ob);
|
||||
}
|
||||
}
|
||||
cfg.outbounds = kept;
|
||||
}
|
||||
|
||||
// Patch route rules that pointed to removed special outbounds
|
||||
if (cfg.route && cfg.route.rules && length(keys(special)) > 0) {
|
||||
cfg.route.rules = map(cfg.route.rules || [], function(rule) {
|
||||
let sp = rule.outbound && special[rule.outbound];
|
||||
if (!sp) return rule;
|
||||
let r = {};
|
||||
for (let k in rule) if (k !== 'outbound') r[k] = rule[k];
|
||||
r.action = (sp === 'dns') ? 'hijack-dns' : 'reject';
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Migrate geoip/geosite database refs to rule_set (removed in sing-box 1.12) ---
|
||||
let needed_rule_sets = {};
|
||||
|
||||
let migrate_geo_rule = function(rule) {
|
||||
let has_geo = rule.geoip || rule.geosite;
|
||||
if (!has_geo) return rule;
|
||||
let r = {};
|
||||
let new_rs = [];
|
||||
for (let k in rule) {
|
||||
if (k === 'geoip') {
|
||||
for (let n in rule[k]) {
|
||||
let tag = 'geoip-' + n;
|
||||
needed_rule_sets[tag] = 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-' + n + '.srs';
|
||||
push(new_rs, tag);
|
||||
}
|
||||
} else if (k === 'geosite') {
|
||||
for (let n in rule[k]) {
|
||||
let tag = 'geosite-' + n;
|
||||
needed_rule_sets[tag] = 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-' + n + '.srs';
|
||||
push(new_rs, tag);
|
||||
}
|
||||
} else {
|
||||
r[k] = rule[k];
|
||||
}
|
||||
}
|
||||
// Merge new rule_set tags with any existing ones
|
||||
let existing_rs = type(r.rule_set) === 'array' ? r.rule_set :
|
||||
(r.rule_set ? [r.rule_set] : []);
|
||||
for (let t in new_rs) push(existing_rs, t);
|
||||
r.rule_set = existing_rs;
|
||||
return r;
|
||||
};
|
||||
|
||||
if (cfg.route && cfg.route.rules)
|
||||
cfg.route.rules = map(cfg.route.rules, migrate_geo_rule);
|
||||
if (cfg.dns && cfg.dns.rules)
|
||||
cfg.dns.rules = map(cfg.dns.rules, migrate_geo_rule);
|
||||
|
||||
// download_detour must be DIRECT: at first start rule-sets aren't loaded, so
|
||||
// routing it via a proxy deadlocks (DNS recursion -> sing-box FATAL).
|
||||
let pick_dl_detour = function() {
|
||||
if (cfg.outbounds) {
|
||||
for (let ob in cfg.outbounds) {
|
||||
if (ob && ob.type === 'direct' && ob.tag) return ob.tag;
|
||||
}
|
||||
}
|
||||
return 'DIRECT';
|
||||
};
|
||||
|
||||
// Add rule_set download entries for each referenced geo tag
|
||||
if (length(keys(needed_rule_sets)) > 0) {
|
||||
if (!cfg.route) cfg.route = {};
|
||||
if (!cfg.route.rule_set) cfg.route.rule_set = [];
|
||||
let existing_tags = {};
|
||||
for (let rs in cfg.route.rule_set) existing_tags[rs.tag] = true;
|
||||
let dl_detour = pick_dl_detour();
|
||||
for (let tag in keys(needed_rule_sets)) {
|
||||
if (!existing_tags[tag]) {
|
||||
push(cfg.route.rule_set, {
|
||||
tag: tag,
|
||||
type: 'remote',
|
||||
format: 'binary',
|
||||
url: needed_rule_sets[tag],
|
||||
download_detour: dl_detour
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Remove deprecated 'outbound' DNS rule items (fatal in sing-box 1.13) ---
|
||||
// Replacement: set route.default_domain_resolver to the plain IP-based resolver
|
||||
let dns_resolver_tag = '';
|
||||
if (cfg.dns && cfg.dns.servers) {
|
||||
for (let srv in cfg.dns.servers) {
|
||||
if ((srv.type === 'udp' || srv.type === 'local') && srv.server &&
|
||||
match(srv.server, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
||||
dns_resolver_tag = srv.tag;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cfg.dns && cfg.dns.rules) {
|
||||
let filtered = [];
|
||||
for (let rule in cfg.dns.rules) {
|
||||
if (!rule.outbound) push(filtered, rule);
|
||||
}
|
||||
cfg.dns.rules = filtered;
|
||||
}
|
||||
// Set route.default_domain_resolver (replaces outbound:any DNS routing)
|
||||
if (dns_resolver_tag) {
|
||||
if (!cfg.route) cfg.route = {};
|
||||
if (!cfg.route.default_domain_resolver)
|
||||
cfg.route.default_domain_resolver = dns_resolver_tag;
|
||||
}
|
||||
|
||||
// --- Fix DNS servers with domain-name server field: add domain_resolver ---
|
||||
// sing-box 1.12+ requires domain_resolver when server= is a hostname (not IP)
|
||||
if (cfg.dns && cfg.dns.servers && dns_resolver_tag) {
|
||||
let domain_types = { https: 1, h3: 1, tls: 1, tcp: 1 };
|
||||
cfg.dns.servers = map(cfg.dns.servers, function(srv) {
|
||||
if (!domain_types[srv.type]) return srv;
|
||||
if (!srv.server || match(srv.server, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return srv;
|
||||
if (srv.domain_resolver) return srv;
|
||||
let r = {};
|
||||
for (let k in srv) r[k] = srv[k];
|
||||
r.domain_resolver = dns_resolver_tag;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Fix DNS rules referencing non-existent server tags → action: reject ---
|
||||
if (cfg.dns && cfg.dns.servers && cfg.dns.rules) {
|
||||
let valid_servers = {};
|
||||
for (let srv in cfg.dns.servers) valid_servers[srv.tag] = true;
|
||||
cfg.dns.rules = map(cfg.dns.rules, function(rule) {
|
||||
if (!rule.server || valid_servers[rule.server]) return rule;
|
||||
let r = {};
|
||||
for (let k in rule) if (k !== 'server' && k !== 'action') r[k] = rule[k];
|
||||
r.action = 'reject';
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Fix reject method: sing-box 1.14 rejects "dropped", only "default"/"drop" ---
|
||||
if (cfg.dns && cfg.dns.rules) {
|
||||
cfg.dns.rules = map(cfg.dns.rules, function(rule) {
|
||||
if (rule.action !== 'reject' || !rule.method) return rule;
|
||||
let m = rule.method;
|
||||
if (m === 'dropped') m = 'drop';
|
||||
if (m !== 'default' && m !== 'drop') {
|
||||
// unknown value: drop it, sing-box defaults to NXDOMAIN
|
||||
let r = {};
|
||||
for (let k in rule) if (k !== 'method') r[k] = rule[k];
|
||||
return r;
|
||||
}
|
||||
if (m === rule.method) return rule;
|
||||
let r = {};
|
||||
for (let k in rule) r[k] = rule[k];
|
||||
r.method = m;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Migrate TUN inet4_address/inet6_address -> address array (1.12+), and
|
||||
// strip legacy inbound fields removed in 1.13 (sniff / sniff_override_destination /
|
||||
// sniff_timeout / domain_strategy). If any inbound had sniff on, prepend a
|
||||
// { "action": "sniff" } route rule as the equivalent. ---
|
||||
let had_sniff = false;
|
||||
if (cfg.inbounds) {
|
||||
cfg.inbounds = map(cfg.inbounds, function(ib) {
|
||||
let r = {};
|
||||
let addrs = [];
|
||||
for (let k in ib) {
|
||||
if (k === 'inet4_address') {
|
||||
if (type(ib[k]) === 'array') { for (let a in ib[k]) push(addrs, a); }
|
||||
else push(addrs, ib[k]);
|
||||
} else if (k === 'inet6_address') {
|
||||
if (type(ib[k]) === 'array') { for (let a in ib[k]) push(addrs, a); }
|
||||
else push(addrs, ib[k]);
|
||||
} else if (k === 'sniff') {
|
||||
if (ib[k]) had_sniff = true;
|
||||
} else if (k === 'sniff_override_destination' || k === 'sniff_timeout' ||
|
||||
k === 'domain_strategy') {
|
||||
// drop — moved to route.rules actions in 1.13
|
||||
} else {
|
||||
r[k] = ib[k];
|
||||
}
|
||||
}
|
||||
if (length(addrs) > 0) r.address = addrs;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
if (had_sniff) {
|
||||
if (!cfg.route) cfg.route = {};
|
||||
if (!cfg.route.rules) cfg.route.rules = [];
|
||||
let has_sniff_rule = false;
|
||||
for (let rl in cfg.route.rules)
|
||||
if (rl.action === 'sniff') { has_sniff_rule = true; break; }
|
||||
if (!has_sniff_rule) {
|
||||
let new_rules = [ { action: 'sniff' } ];
|
||||
for (let rl in cfg.route.rules) push(new_rules, rl);
|
||||
cfg.route.rules = new_rules;
|
||||
}
|
||||
}
|
||||
|
||||
// --- sing-box 1.14 requires route.default_domain_resolver; add one if missing,
|
||||
// preferring dns.final, else the first dns.servers entry with a tag. ---
|
||||
if (cfg.route && !cfg.route.default_domain_resolver) {
|
||||
let resolver_tag = '';
|
||||
if (cfg.dns && cfg.dns.final) resolver_tag = cfg.dns.final;
|
||||
else if (cfg.dns && cfg.dns.servers) {
|
||||
for (let srv in cfg.dns.servers)
|
||||
if (srv.tag) { resolver_tag = srv.tag; break; }
|
||||
}
|
||||
if (resolver_tag)
|
||||
cfg.route.default_domain_resolver = { server: resolver_tag };
|
||||
}
|
||||
|
||||
// --- Remove dangling outbound refs from selectors/urltest, and drop airline
|
||||
// pseudo-nodes. Two cases: (1) subconverter may reference a missing tag (e.g.
|
||||
// REJECT); (2) "Traffic:.../Expire:.../quota" pseudo-nodes are real SS/Vmess
|
||||
// outbounds (kept so the UI can read traffic/expiry) but don't forward — in a
|
||||
// selector/urltest they win (0ms) and swallow all foreign traffic. ---
|
||||
let is_pseudo_tag = function(t) {
|
||||
if (!t) return false;
|
||||
return match(t, /^Traffic[::]/) ||
|
||||
match(t, /^Expire[::]/) ||
|
||||
match(t, /剩余流量|剩余[::]/) ||
|
||||
match(t, /距离下次重置/) ||
|
||||
match(t, /到期(时间|日期)?[::]/) ||
|
||||
match(t, /官网[::]|网站[::]|套餐[::]?|客服[::]/) ||
|
||||
match(t, /QQ[群]?[::]/) ||
|
||||
match(t, /Telegram|TG群|官方群/) ||
|
||||
match(t, /续费|订阅地址|流量重置/);
|
||||
};
|
||||
if (cfg.outbounds) {
|
||||
let defined_tags = {};
|
||||
for (let ob in cfg.outbounds) defined_tags[ob.tag] = true;
|
||||
cfg.outbounds = map(cfg.outbounds, function(ob) {
|
||||
if ((ob.type !== 'selector' && ob.type !== 'urltest' && ob.type !== 'loadbalance') ||
|
||||
!ob.outbounds) return ob;
|
||||
let valid = [];
|
||||
for (let t in ob.outbounds) {
|
||||
if (!defined_tags[t]) continue;
|
||||
if (is_pseudo_tag(t)) continue;
|
||||
push(valid, t);
|
||||
}
|
||||
if (length(valid) === length(ob.outbounds)) return ob;
|
||||
let r = {};
|
||||
for (let k in ob) r[k] = ob[k];
|
||||
r.outbounds = valid;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Force download_detour=DIRECT on every remote rule_set ---
|
||||
// subconverter often emits download_detour="auto", but the proxy isn't ready at
|
||||
// first start -> urltest deadlock ("fetch rule-set: deadline exceeded") -> FATAL.
|
||||
// The srs URLs already use a CN-reachable mirror, so DIRECT is always correct.
|
||||
if (cfg.route && cfg.route.rule_set) {
|
||||
let dl_detour = pick_dl_detour();
|
||||
cfg.route.rule_set = map(cfg.route.rule_set, function(rs) {
|
||||
if (!rs || rs.type !== 'remote') return rs;
|
||||
let r = {};
|
||||
for (let k in rs) r[k] = rs[k];
|
||||
r.download_detour = dl_detour;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
|
||||
// ---- CLI entry: ucode migrate_singbox.uc <config.json> ----
|
||||
let _path = ARGV[0] || '';
|
||||
if (!_path) { print("missing path\n"); exit(1); }
|
||||
let _raw = readfile(_path);
|
||||
if (!_raw) { print("read failed\n"); exit(1); }
|
||||
let _cfg = json(_raw);
|
||||
if (!_cfg) { print("json parse failed\n"); exit(1); }
|
||||
let _before = sprintf('%J', _cfg);
|
||||
_cfg = migrate_singbox(_cfg);
|
||||
let _after = sprintf('%J', _cfg);
|
||||
if (_before === _after) { print("nochange\n"); exit(0); }
|
||||
if (writefile(_path, _after) === null) { print("write failed\n"); exit(1); }
|
||||
print("migrated\n");
|
||||
@@ -0,0 +1,939 @@
|
||||
#!/usr/bin/ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile, access, popen } from 'fs';
|
||||
|
||||
let path = ARGV[0] || '';
|
||||
let redir_port = +(ARGV[1] || '7891');
|
||||
let tproxy_port = +(ARGV[2] || '7982');
|
||||
let mixed_port = +(ARGV[3] || '7890');
|
||||
let has_tun_device = (ARGV[4] || '1') == '1';
|
||||
if (has_tun_device && system('(ip tuntap add mode tun name cotuntest >/dev/null 2>&1 && ip link del cotuntest >/dev/null 2>&1)') != 0)
|
||||
has_tun_device = false;
|
||||
|
||||
// default 6666 must match fw4.sh CORE_ROUTING_MARK (0x1a0a)
|
||||
let routing_mark = +(ARGV[5] || '6666');
|
||||
let dns_port = +(ARGV[6] || '1053');
|
||||
let dash_port = +(ARGV[7] || '9090');
|
||||
let dash_secret = ARGV[8] != null ? (ARGV[8] + '') : '';
|
||||
|
||||
if (!path) {
|
||||
print("missing path\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let raw = readfile(path);
|
||||
if (!raw) {
|
||||
print("read failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let cfg = json(raw);
|
||||
if (!cfg) {
|
||||
print("json parse failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function s_len(s) {
|
||||
return length(s || '');
|
||||
}
|
||||
|
||||
function s_sub(s, start, count) {
|
||||
if (count == null)
|
||||
return substr(s || '', start);
|
||||
return substr(s || '', start, count);
|
||||
}
|
||||
|
||||
function is_space(ch) {
|
||||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
|
||||
}
|
||||
|
||||
function trim_s(s) {
|
||||
s = (s == null) ? '' : (s + '');
|
||||
while (s_len(s) > 0 && is_space(s_sub(s, 0, 1)))
|
||||
s = s_sub(s, 1);
|
||||
while (s_len(s) > 0 && is_space(s_sub(s, s_len(s) - 1, 1)))
|
||||
s = s_sub(s, 0, s_len(s) - 1);
|
||||
return s;
|
||||
}
|
||||
|
||||
let default_interface = '';
|
||||
let _default_if_pipe = popen("ip -4 route show default 2>/dev/null | awk '{print $5; exit}'");
|
||||
if (_default_if_pipe) {
|
||||
default_interface = trim_s(_default_if_pipe.read('all'));
|
||||
_default_if_pipe.close();
|
||||
}
|
||||
|
||||
function starts_with(s, prefix) {
|
||||
return s_sub(s || '', 0, s_len(prefix)) == prefix;
|
||||
}
|
||||
|
||||
function find_char(s, ch) {
|
||||
for (let i = 0; i < s_len(s); i++)
|
||||
if (s_sub(s, i, 1) == ch)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function find_last_char(s, ch) {
|
||||
for (let i = s_len(s) - 1; i >= 0; i--)
|
||||
if (s_sub(s, i, 1) == ch)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function split_uci_words(line) {
|
||||
let out = [], cur = '', quote = '', esc = false;
|
||||
for (let i = 0; i < s_len(line); i++) {
|
||||
let ch = s_sub(line, i, 1);
|
||||
if (esc) {
|
||||
cur += ch;
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (ch == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (quote) {
|
||||
if (ch == quote)
|
||||
quote = '';
|
||||
else
|
||||
cur += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch == '"' || ch == "'") {
|
||||
quote = ch;
|
||||
continue;
|
||||
}
|
||||
if (is_space(ch)) {
|
||||
if (s_len(cur) > 0) {
|
||||
push(out, cur);
|
||||
cur = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
cur += ch;
|
||||
}
|
||||
if (s_len(cur) > 0)
|
||||
push(out, cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
function load_clashoo_uci() {
|
||||
let uci_path = ARGV[9] || '/etc/config/clashoo';
|
||||
let txt = readfile(uci_path) || '';
|
||||
let sections = [], cur = null;
|
||||
for (let line in split(txt, '\n')) {
|
||||
line = trim_s(line);
|
||||
if (!s_len(line) || starts_with(line, '#'))
|
||||
continue;
|
||||
let words = split_uci_words(line);
|
||||
if (!length(words))
|
||||
continue;
|
||||
if (words[0] == 'config') {
|
||||
cur = { type: words[1] || '', name: words[2] || '', options: {}, lists: {} };
|
||||
push(sections, cur);
|
||||
continue;
|
||||
}
|
||||
if (!cur || length(words) < 3)
|
||||
continue;
|
||||
if (words[0] == 'option')
|
||||
cur.options[words[1]] = words[2];
|
||||
else if (words[0] == 'list') {
|
||||
if (cur.lists[words[1]] == null)
|
||||
cur.lists[words[1]] = [];
|
||||
push(cur.lists[words[1]], words[2]);
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
let clashoo_uci = load_clashoo_uci();
|
||||
|
||||
function uci_config_section() {
|
||||
for (let s in clashoo_uci)
|
||||
if (s.type == 'clashoo' && (s.name == 'config' || s.name == ''))
|
||||
return s;
|
||||
return {};
|
||||
}
|
||||
|
||||
let uci_cfg = uci_config_section();
|
||||
|
||||
function uci_opt(key, def) {
|
||||
if (uci_cfg.options && uci_cfg.options[key] != null)
|
||||
return uci_cfg.options[key];
|
||||
return def;
|
||||
}
|
||||
|
||||
function uci_list(key) {
|
||||
if (uci_cfg.lists && uci_cfg.lists[key] != null)
|
||||
return uci_cfg.lists[key];
|
||||
if (uci_cfg.options && uci_cfg.options[key] != null)
|
||||
return [ uci_cfg.options[key] ];
|
||||
return [];
|
||||
}
|
||||
|
||||
function uci_sections(type_name) {
|
||||
let out = [];
|
||||
for (let s in clashoo_uci)
|
||||
if (s.type == type_name)
|
||||
push(out, s);
|
||||
return out;
|
||||
}
|
||||
|
||||
function opt_bool(v, def) {
|
||||
if (v == null || v == '')
|
||||
return def;
|
||||
return v === true || v == '1' || v == 'true' || v == 'yes' || v == 'on';
|
||||
}
|
||||
|
||||
function normalize_dns_uri(address, protocol, port) {
|
||||
address = trim_s(address || '');
|
||||
protocol = trim_s(protocol || '');
|
||||
port = trim_s(port || '');
|
||||
if (!s_len(address))
|
||||
return '';
|
||||
if (find_char(address, ':') >= 0 && find_char(address, '/') >= 0)
|
||||
return address;
|
||||
if (protocol == 'none')
|
||||
protocol = '';
|
||||
if (protocol == 'dot')
|
||||
protocol = 'tls://';
|
||||
else if (protocol == 'doh')
|
||||
protocol = 'https://';
|
||||
else if (protocol == 'doq')
|
||||
protocol = 'quic://';
|
||||
if (s_len(protocol) && !starts_with(protocol, 'udp://') && !starts_with(protocol, 'tcp://') &&
|
||||
!starts_with(protocol, 'tls://') && !starts_with(protocol, 'https://') && !starts_with(protocol, 'quic://'))
|
||||
protocol += '://';
|
||||
return protocol + address + (s_len(port) ? ':' + port : '');
|
||||
}
|
||||
|
||||
function direct_outbound_tag() {
|
||||
for (let ob in (cfg.outbounds || []))
|
||||
if (ob && ob.type == 'direct' && s_len(ob.tag || ''))
|
||||
return ob.tag;
|
||||
return 'DIRECT';
|
||||
}
|
||||
|
||||
function dns_server_obj(uri, tag, fallback_type) {
|
||||
uri = normalize_dns_uri(uri || '', '', '');
|
||||
if (!s_len(uri))
|
||||
return null;
|
||||
let scheme = fallback_type || 'udp';
|
||||
let rest = uri;
|
||||
let p = -1;
|
||||
for (let i = 0; i < s_len(uri) - 2; i++) {
|
||||
if (s_sub(uri, i, 3) == '://') {
|
||||
p = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (p >= 0) {
|
||||
scheme = s_sub(uri, 0, p);
|
||||
rest = s_sub(uri, p + 3);
|
||||
}
|
||||
if (scheme == 'dot')
|
||||
scheme = 'tls';
|
||||
else if (scheme == 'doh')
|
||||
scheme = 'https';
|
||||
else if (scheme == 'doq')
|
||||
scheme = 'quic';
|
||||
|
||||
let path = '';
|
||||
let slash = find_char(rest, '/');
|
||||
if (slash >= 0) {
|
||||
path = s_sub(rest, slash);
|
||||
rest = s_sub(rest, 0, slash);
|
||||
}
|
||||
|
||||
let server = rest, server_port = null;
|
||||
let colon = find_last_char(rest, ':');
|
||||
if (colon > 0 && find_char(rest, ']') < 0) {
|
||||
server = s_sub(rest, 0, colon);
|
||||
let n = +(s_sub(rest, colon + 1));
|
||||
if (n === n)
|
||||
server_port = n;
|
||||
}
|
||||
|
||||
let obj = { type: scheme, tag: tag, server: server };
|
||||
if (server_port != null)
|
||||
obj.server_port = server_port;
|
||||
if (path && (scheme == 'https' || scheme == 'h3'))
|
||||
obj.path = path;
|
||||
if ((scheme == 'https' || scheme == 'tls' || scheme == 'quic') && tag != 'dns_resolver')
|
||||
obj.domain_resolver = 'dns_resolver';
|
||||
/* sing-box 1.12+ DNS servers detour to the proxy by default, so a server
|
||||
* resolving its own domain via dns_resolver loops -> "deadline exceeded".
|
||||
* Force direct/resolver servers to DIRECT; only dns_proxy may use the proxy. */
|
||||
if (tag == 'dns_resolver' || tag == 'dns_direct' || tag == 'dns_foreign')
|
||||
obj.detour = direct_outbound_tag();
|
||||
return obj;
|
||||
}
|
||||
|
||||
function dns_servers_by_role(role) {
|
||||
let out = [];
|
||||
for (let s in uci_sections('dnsservers')) {
|
||||
if (!opt_bool(s.options.enabled, true))
|
||||
continue;
|
||||
if ((s.options.ser_type || 'nameserver') != role)
|
||||
continue;
|
||||
let uri = normalize_dns_uri(s.options.ser_address || '', s.options.protocol || '', s.options.ser_port || '');
|
||||
if (s_len(uri))
|
||||
push(out, uri);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function first_or(arr, fallback) {
|
||||
return length(arr) ? arr[0] : fallback;
|
||||
}
|
||||
|
||||
function local_rule_set_path(tag) {
|
||||
if (!s_len(tag))
|
||||
return '';
|
||||
let path = '/usr/share/clashoo/ruleset/' + tag + '.srs';
|
||||
if (access(path, 'r'))
|
||||
return path;
|
||||
if (tag == 'geolocation-cn' || tag == 'cn') {
|
||||
path = '/usr/share/clashoo/ruleset/geosite-cn.srs';
|
||||
if (access(path, 'r'))
|
||||
return path;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function keep_remote_rule_set(rs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function normalize_rule_set_url(url) {
|
||||
url = url || '';
|
||||
url = replace(url, /^https:\/\/gh-proxy\.com\//, '');
|
||||
let m = match(url, /^https:\/\/raw\.githubusercontent\.com\/([^\/]+)\/([^\/]+)\/([^\/]+)\/(.+)$/);
|
||||
if (m)
|
||||
return 'https://cdn.jsdelivr.net/gh/' + m[1] + '/' + m[2] + '@' + m[3] + '/' + m[4];
|
||||
m = match(url, /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)$/);
|
||||
if (m)
|
||||
return 'https://cdn.jsdelivr.net/gh/' + m[1] + '/' + m[2] + '@' + m[3] + '/' + m[4];
|
||||
return url;
|
||||
}
|
||||
|
||||
function has_route_rule_set(tag) {
|
||||
for (let rs in (cfg.route || {}).rule_set || [])
|
||||
if (rs && rs.tag == tag)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function add_remote_rule_set(tag, url) {
|
||||
cfg.route = cfg.route || {};
|
||||
cfg.route.rule_set = cfg.route.rule_set || [];
|
||||
if (has_route_rule_set(tag))
|
||||
return;
|
||||
push(cfg.route.rule_set, {
|
||||
tag: tag,
|
||||
type: 'remote',
|
||||
format: 'binary',
|
||||
url: url
|
||||
});
|
||||
}
|
||||
|
||||
function array_has_value(arr, val) {
|
||||
if (type(arr) != 'array')
|
||||
return false;
|
||||
for (let item in arr)
|
||||
if (item == val)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
let dnsmasq_uid = null;
|
||||
let _dnsmasq_uid_pipe = popen("id -u dnsmasq 2>/dev/null");
|
||||
if (_dnsmasq_uid_pipe) {
|
||||
let value = trim_s(_dnsmasq_uid_pipe.read('all'));
|
||||
_dnsmasq_uid_pipe.close();
|
||||
if (match(value, /^[0-9]+$/))
|
||||
dnsmasq_uid = +value;
|
||||
}
|
||||
|
||||
function ensure_tun_dnsmasq_exclude(ib) {
|
||||
if (dnsmasq_uid == null)
|
||||
return;
|
||||
let excludes = ib.exclude_uid;
|
||||
if (type(excludes) != 'array')
|
||||
excludes = excludes != null ? [ excludes ] : [];
|
||||
if (!array_has_value(excludes, dnsmasq_uid))
|
||||
push(excludes, dnsmasq_uid);
|
||||
ib.exclude_uid = excludes;
|
||||
}
|
||||
|
||||
function ensure_tun_cn_exclude(ib) {
|
||||
let excludes = ib.route_exclude_address_set;
|
||||
if (type(excludes) != 'array')
|
||||
excludes = excludes ? [ excludes ] : [];
|
||||
if (!array_has_value(excludes, 'cn-ip'))
|
||||
push(excludes, 'cn-ip');
|
||||
ib.route_exclude_address_set = excludes;
|
||||
}
|
||||
|
||||
function prune_tun_cn_exclude_if_missing() {
|
||||
if (has_route_rule_set('cn-ip'))
|
||||
return;
|
||||
for (let ib in cfg.inbounds || []) {
|
||||
if (!ib || ib.type != 'tun')
|
||||
continue;
|
||||
let excludes = [];
|
||||
for (let item in ib.route_exclude_address_set || [])
|
||||
if (item != 'cn-ip')
|
||||
push(excludes, item);
|
||||
if (length(excludes))
|
||||
ib.route_exclude_address_set = excludes;
|
||||
else
|
||||
delete ib.route_exclude_address_set;
|
||||
}
|
||||
}
|
||||
|
||||
function matcher_rule(matcher, server_tag) {
|
||||
let r = { server: server_tag };
|
||||
if (starts_with(matcher, 'geosite:'))
|
||||
r.rule_set = s_sub(matcher, 8);
|
||||
else if (starts_with(matcher, 'rule_set:'))
|
||||
r.rule_set = s_sub(matcher, 9);
|
||||
else if (starts_with(matcher, 'domain:'))
|
||||
r.domain = s_sub(matcher, 7);
|
||||
else if (starts_with(matcher, 'domain-suffix:'))
|
||||
r.domain_suffix = s_sub(matcher, 14);
|
||||
else
|
||||
r.domain_suffix = matcher;
|
||||
return r;
|
||||
}
|
||||
|
||||
function apply_dns_from_uci() {
|
||||
cfg.dns = cfg.dns || {};
|
||||
/* strip mihomo-style DNS fields (enable/ipv6/listen/fake-ip-filter/nameserver...)
|
||||
* that leak in from YAML conversion; sing-box rejects them (Fatal). */
|
||||
let _mihomo_dns_fields = ['enable', 'ipv6', 'listen', 'fake-ip-filter', 'fake-ip-range',
|
||||
'enhanced-mode', 'nameserver', 'fallback', 'fallback-filter',
|
||||
'use-hosts', 'default-nameserver', 'proxy-server-nameserver',
|
||||
'direct-nameserver', 'nameserver-policy'];
|
||||
for (let _i = 0; _i < length(_mihomo_dns_fields); _i++)
|
||||
delete cfg.dns[_mihomo_dns_fields[_i]];
|
||||
/* strip mihomo-style experimental fields */
|
||||
let _mihomo_exp = ['sniff-tls-sni', 'sniff', 'sniffer'];
|
||||
for (let _me = 0; _me < length(_mihomo_exp); _me++)
|
||||
if (cfg.experimental && cfg.experimental[_mihomo_exp[_me]] != null)
|
||||
delete cfg.experimental[_mihomo_exp[_me]];
|
||||
/* strip non-sing-box root-level fields */
|
||||
let _mihomo_root = ['clash-for-android', 'cfw-bypass', 'sniffer', 'profile',
|
||||
'geodata-mode', 'geodata-loader', 'geox-url', 'geo-auto-update',
|
||||
'geo-update-interval', 'tun', 'ipv6', 'interface-name',
|
||||
'port', 'socks-port', 'mixed-port', 'redir-port', 'tproxy-port', 'mode', 'allow-lan', 'log-level', 'external-controller', 'secret', 'bind-address', 'routing-mark', 'find-process-mode', 'tcp-concurrent', 'unified-delay',
|
||||
'keep-alive-interval', 'keep-alive-idle', 'disable-keep-alive'];
|
||||
/* strip subconverter note fields and non-standard keys */
|
||||
let _subconv_fields = ['_note', '_sub_url', 'hosts', 'script', 'enable', 'fake-ip-filter', 'fake-ip-range'];
|
||||
for (let _sf = 0; _sf < length(_subconv_fields); _sf++)
|
||||
if (cfg[_subconv_fields[_sf]] != null)
|
||||
delete cfg[_subconv_fields[_sf]];
|
||||
for (let _mr = 0; _mr < length(_mihomo_root); _mr++)
|
||||
if (cfg[_mihomo_root[_mr]] != null)
|
||||
delete cfg[_mihomo_root[_mr]];
|
||||
let bootstrap = uci_list('default_nameserver');
|
||||
if (!length(bootstrap))
|
||||
bootstrap = uci_list('defaul_nameserver');
|
||||
let resolver_uri = first_or(bootstrap, '223.5.5.5');
|
||||
let direct_uri = first_or(dns_servers_by_role('direct-nameserver'), first_or(dns_servers_by_role('proxy-server-nameserver'), 'https://doh.pub/dns-query'));
|
||||
let proxy_uri = first_or(dns_servers_by_role('nameserver'), first_or(dns_servers_by_role('fallback'), 'https://1.1.1.1/dns-query'));
|
||||
|
||||
let servers = [];
|
||||
push(servers, dns_server_obj(resolver_uri, 'dns_resolver', 'udp'));
|
||||
push(servers, dns_server_obj(direct_uri, 'dns_direct', 'udp'));
|
||||
push(servers, dns_server_obj(proxy_uri, 'dns_proxy', 'tls'));
|
||||
|
||||
let enhanced = uci_opt('enhanced_mode', 'fake-ip');
|
||||
if (enhanced == 'fake-ip') {
|
||||
let fake_range = uci_opt('fake_ip_range', '198.18.0.1/16');
|
||||
push(servers, {
|
||||
type: 'fakeip',
|
||||
tag: 'dns_fakeip',
|
||||
inet4_range: fake_range || '198.18.0.1/16',
|
||||
inet6_range: 'fc00::/18'
|
||||
});
|
||||
}
|
||||
|
||||
let rules = [];
|
||||
push(servers, { type: 'udp', tag: 'dns_foreign', server: '1.1.1.1' });
|
||||
if (enhanced == 'fake-ip') {
|
||||
push(rules, {
|
||||
rule_set: 'geolocation-!cn',
|
||||
server: 'dns_fakeip'
|
||||
});
|
||||
}
|
||||
|
||||
let clean_servers = [];
|
||||
for (let s in servers)
|
||||
if (s)
|
||||
push(clean_servers, s);
|
||||
cfg.dns.servers = clean_servers;
|
||||
cfg.dns.rules = rules;
|
||||
|
||||
/* Keep bootstrap direct; proxy unmatched queries in strict mode. */
|
||||
if (opt_bool(uci_opt('dns_leak_protect', '0'), false)) {
|
||||
cfg.dns.final = 'dns_proxy';
|
||||
if (!opt_bool(uci_opt('ipv6_proxy', '0'), false))
|
||||
unshift(cfg.dns.rules, { query_type: ['AAAA'], action: 'reject', method: 'drop' });
|
||||
} else
|
||||
cfg.dns.final = 'dns_direct';
|
||||
|
||||
let ecs = trim_s(uci_opt('dns_ecs', ''));
|
||||
if (s_len(ecs))
|
||||
cfg.dns.client_subnet = ecs;
|
||||
else
|
||||
delete cfg.dns.client_subnet;
|
||||
|
||||
if (opt_bool(uci_opt('singbox_independent_cache', '0'), false))
|
||||
cfg.dns.independent_cache = true;
|
||||
else
|
||||
delete cfg.dns.independent_cache;
|
||||
|
||||
/* sing-box 1.14 requires this (else Fatal); dns_resolver is DIRECT so first-start node lookups use direct DNS */
|
||||
cfg.route.default_domain_resolver = 'dns_resolver';
|
||||
}
|
||||
|
||||
let inbounds = cfg.inbounds || [];
|
||||
let normalized = [];
|
||||
let has_redirect = false;
|
||||
let has_tproxy = false;
|
||||
let has_mixed = false;
|
||||
let has_tun = false;
|
||||
let has_dns_in = false;
|
||||
let wants_tun = has_tun_device && (uci_opt('tcp_mode', '') == 'tun' || uci_opt('udp_mode', '') == 'tun');
|
||||
let tun_stack = uci_opt('stack', 'mixed') || 'mixed';
|
||||
|
||||
if (wants_tun) {
|
||||
add_remote_rule_set('cn-ip',
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/cn.srs');
|
||||
}
|
||||
|
||||
for (let ib in inbounds) {
|
||||
if (!ib)
|
||||
continue;
|
||||
|
||||
if (ib.type == 'tun' || ib.tag == 'tun-in') {
|
||||
/* Keep tun inbound only when the selected proxy mode actually uses TUN. */
|
||||
if (wants_tun && !has_tun) {
|
||||
ib.type = 'tun';
|
||||
ib.tag = ib.tag || 'tun-in';
|
||||
if (!ib.address)
|
||||
ib.address = [ '172.19.0.1/30', 'fdfe:dcba:9876::1/126' ];
|
||||
ib.auto_route = true;
|
||||
ib.auto_redirect = true;
|
||||
ib.auto_redirect_input_mark = '0x2023';
|
||||
ib.auto_redirect_output_mark = '0x2024';
|
||||
ib.strict_route = true;
|
||||
ib.stack = tun_stack;
|
||||
ensure_tun_dnsmasq_exclude(ib);
|
||||
ensure_tun_cn_exclude(ib);
|
||||
push(normalized, ib);
|
||||
has_tun = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ib.tag == 'redirect-in' || ib.type == 'redirect') {
|
||||
if (has_redirect)
|
||||
continue;
|
||||
ib.type = 'redirect';
|
||||
ib.tag = 'redirect-in';
|
||||
ib.listen = '0.0.0.0';
|
||||
ib.listen_port = redir_port;
|
||||
has_redirect = true;
|
||||
push(normalized, ib);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ib.tag == 'tproxy-in' || ib.type == 'tproxy') {
|
||||
if (has_tproxy)
|
||||
continue;
|
||||
ib.type = 'tproxy';
|
||||
ib.tag = 'tproxy-in';
|
||||
ib.listen = '0.0.0.0';
|
||||
ib.listen_port = tproxy_port;
|
||||
ib.network = 'udp';
|
||||
has_tproxy = true;
|
||||
push(normalized, ib);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ib.tag == 'mixed-in' || ib.type == 'mixed') {
|
||||
if (has_mixed)
|
||||
continue;
|
||||
ib.type = 'mixed';
|
||||
ib.tag = 'mixed-in';
|
||||
ib.listen = '0.0.0.0';
|
||||
ib.listen_port = mixed_port;
|
||||
has_mixed = true;
|
||||
push(normalized, ib);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ib.tag == 'dns-in') {
|
||||
if (has_dns_in)
|
||||
continue;
|
||||
ib.type = 'direct';
|
||||
ib.tag = 'dns-in';
|
||||
ib.listen = '0.0.0.0';
|
||||
ib.listen_port = dns_port;
|
||||
has_dns_in = true;
|
||||
push(normalized, ib);
|
||||
continue;
|
||||
}
|
||||
|
||||
push(normalized, ib);
|
||||
}
|
||||
|
||||
if (!has_redirect) {
|
||||
push(normalized, {
|
||||
type: 'redirect',
|
||||
tag: 'redirect-in',
|
||||
listen: '0.0.0.0',
|
||||
listen_port: redir_port
|
||||
});
|
||||
}
|
||||
|
||||
if (!has_mixed) {
|
||||
push(normalized, {
|
||||
type: 'mixed',
|
||||
tag: 'mixed-in',
|
||||
listen: '0.0.0.0',
|
||||
listen_port: mixed_port
|
||||
});
|
||||
}
|
||||
|
||||
if (wants_tun && !has_tun) {
|
||||
let tun_ib = {
|
||||
type: 'tun',
|
||||
tag: 'tun-in',
|
||||
address: [ '172.19.0.1/30', 'fdfe:dcba:9876::1/126' ],
|
||||
auto_route: true,
|
||||
auto_redirect: true,
|
||||
auto_redirect_input_mark: '0x2023',
|
||||
auto_redirect_output_mark: '0x2024',
|
||||
strict_route: true,
|
||||
stack: tun_stack
|
||||
};
|
||||
ensure_tun_dnsmasq_exclude(tun_ib);
|
||||
ensure_tun_cn_exclude(tun_ib);
|
||||
push(normalized, tun_ib);
|
||||
}
|
||||
|
||||
if (!has_tproxy) {
|
||||
push(normalized, {
|
||||
type: 'tproxy',
|
||||
tag: 'tproxy-in',
|
||||
listen: '0.0.0.0',
|
||||
listen_port: tproxy_port,
|
||||
network: 'udp'
|
||||
});
|
||||
}
|
||||
|
||||
if (!has_dns_in) {
|
||||
push(normalized, {
|
||||
type: 'direct',
|
||||
tag: 'dns-in',
|
||||
listen: '0.0.0.0',
|
||||
listen_port: dns_port
|
||||
});
|
||||
}
|
||||
|
||||
cfg.inbounds = normalized;
|
||||
|
||||
for (let ob in (cfg.outbounds || [])) {
|
||||
if (!ob || type(ob) != 'object')
|
||||
continue;
|
||||
|
||||
let t = ob.type || '';
|
||||
if (t == 'selector' || t == 'urltest' || t == 'fallback' || t == 'load_balance' || t == 'dns' || t == 'block')
|
||||
continue;
|
||||
|
||||
/* Clash SS obfs is plugin=obfs + plugin-opts object; sing-box wants
|
||||
* plugin=obfs-local + "obfs=http;obfs-host=..." string. */
|
||||
if (ob.plugin == 'obfs')
|
||||
ob.plugin = 'obfs-local';
|
||||
if (ob.plugin_opts != null && type(ob.plugin_opts) == 'object') {
|
||||
let _parts = [];
|
||||
let _map = { mode: 'obfs', host: 'obfs-host', uri: 'obfs-uri' };
|
||||
for (let _k in ob.plugin_opts) {
|
||||
let _v = ob.plugin_opts[_k];
|
||||
if (type(_v) == 'object' || type(_v) == 'array') continue;
|
||||
push(_parts, (_map[_k] || _k) + '=' + _v);
|
||||
}
|
||||
ob.plugin_opts = join(';', _parts);
|
||||
}
|
||||
if (ob.plugin != null && ob.plugin != 'obfs-local' && ob.plugin != 'v2ray-plugin' && ob.plugin != 'shadow-tls') {
|
||||
delete ob.plugin;
|
||||
delete ob.plugin_opts;
|
||||
}
|
||||
|
||||
/* Force ipv4_only on DIRECT outbound: most devices are IPv4-only egress,
|
||||
* a CN domain resolving AAAA stalls on "network is unreachable". */
|
||||
if (ob.type == 'direct' && ob.tag == 'DIRECT')
|
||||
ob.domain_resolver = { server: 'dns_resolver', strategy: 'ipv4_only' };
|
||||
|
||||
if (wants_tun) {
|
||||
delete ob.routing_mark;
|
||||
if (ob.type == 'direct' && ob.tag == 'DIRECT' && default_interface)
|
||||
ob.bind_interface = ob.bind_interface || default_interface;
|
||||
} else if (ob.routing_mark == null)
|
||||
ob.routing_mark = routing_mark;
|
||||
}
|
||||
|
||||
/* Drop airline pseudo-nodes (Traffic:/Expire:/quota/官网/QQ/etc.) from selector/urltest lists.
|
||||
* They're real SS/Vmess (kept so the UI reads traffic/expiry) but don't forward; in a
|
||||
* selector/urltest they win and swallow foreign traffic ("CN ok, foreign out"). */
|
||||
let _is_pseudo_tag = function(t) {
|
||||
if (!t) return false;
|
||||
return match(t, /^Traffic[::]/) || match(t, /^Expire[::]/) ||
|
||||
match(t, /剩余流量|剩余[::]/) || match(t, /距离下次重置/) ||
|
||||
match(t, /到期(时间|日期)?[::]/) ||
|
||||
match(t, /官网[::]|网站[::]|套餐[::]?|客服[::]/) ||
|
||||
match(t, /QQ[群]?[::]/) || match(t, /Telegram|TG群|官方群/) ||
|
||||
match(t, /续费|订阅地址|流量重置/);
|
||||
};
|
||||
for (let ob in (cfg.outbounds || [])) {
|
||||
if (!ob || type(ob) != 'object') continue;
|
||||
let t = ob.type || '';
|
||||
if (t != 'selector' && t != 'urltest' && t != 'fallback' && t != 'load_balance') continue;
|
||||
if (type(ob.outbounds) != 'array') continue;
|
||||
let cleaned = [];
|
||||
for (let tag in ob.outbounds) if (!_is_pseudo_tag(tag)) push(cleaned, tag);
|
||||
ob.outbounds = cleaned;
|
||||
}
|
||||
|
||||
cfg.route = cfg.route || {};
|
||||
cfg.route.rules = cfg.route.rules || [];
|
||||
let has_dns_hijack = false;
|
||||
for (let rule in cfg.route.rules) {
|
||||
if (!rule || type(rule) != 'object')
|
||||
continue;
|
||||
if (rule.inbound == 'dns-in' && rule.action == 'hijack-dns') {
|
||||
has_dns_hijack = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_dns_hijack) {
|
||||
unshift(cfg.route.rules, {
|
||||
inbound: 'dns-in',
|
||||
action: 'hijack-dns'
|
||||
});
|
||||
}
|
||||
cfg.route.auto_detect_interface = true;
|
||||
apply_dns_from_uci();
|
||||
|
||||
/* DNS leak protection: reject DoT/DoQ (853) to force DNS through the core; after hijack-dns so the dns inbound survives */
|
||||
if (opt_bool(uci_opt('dns_leak_protect', '0'), false)) {
|
||||
let _has_853 = false;
|
||||
for (let _r in cfg.route.rules) {
|
||||
if (_r && type(_r) == 'object' && _r.action == 'reject' && _r.port) {
|
||||
if (type(_r.port) == 'array') {
|
||||
for (let _p in _r.port) if (_p == 853) { _has_853 = true; break; }
|
||||
} else if (_r.port == 853) {
|
||||
_has_853 = true;
|
||||
}
|
||||
}
|
||||
if (_has_853) break;
|
||||
}
|
||||
if (!_has_853) {
|
||||
let _new_rules = [];
|
||||
let _inserted = false;
|
||||
for (let _ri = 0; _ri < length(cfg.route.rules); _ri++) {
|
||||
push(_new_rules, cfg.route.rules[_ri]);
|
||||
if (!_inserted && cfg.route.rules[_ri] && cfg.route.rules[_ri].action == 'hijack-dns') {
|
||||
push(_new_rules, { port: [853], action: 'reject' });
|
||||
_inserted = true;
|
||||
}
|
||||
}
|
||||
if (!_inserted) unshift(_new_rules, { port: [853], action: 'reject' });
|
||||
cfg.route.rules = _new_rules;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Custom routing rules (UCI: config addtype) -> route.rules, mirrors mihomo iprules.sh =====
|
||||
* User "domain/IP -> direct/proxy" rules from LuCI, inserted after the DNS funnel
|
||||
* (hijack-dns / 853-reject) and before other rules, so overrides win without bypassing it. */
|
||||
let _proxy_outbound_tag = function() {
|
||||
for (let ob in (cfg.outbounds || []))
|
||||
if (ob && ob.tag == '🚀 节点选择' && (ob.type == 'selector' || ob.type == 'urltest'))
|
||||
return ob.tag;
|
||||
for (let ob in (cfg.outbounds || []))
|
||||
if (ob && ob.type == 'selector' && s_len(ob.tag || '') && !_is_pseudo_tag(ob.tag))
|
||||
return ob.tag;
|
||||
return direct_outbound_tag();
|
||||
};
|
||||
let _load_addtype_rules = function() {
|
||||
let out = [];
|
||||
let dtag = direct_outbound_tag();
|
||||
let ptag = null;
|
||||
for (let s in uci_sections('addtype')) {
|
||||
let o = s.options || {};
|
||||
let addr = trim_s(o.ipaaddr || '');
|
||||
let rtype = trim_s(o.type || '');
|
||||
let pg = trim_s(o.pgroup || '');
|
||||
if (!s_len(addr) || !s_len(rtype) || !s_len(pg))
|
||||
continue;
|
||||
let r = {};
|
||||
if (rtype == 'DOMAIN')
|
||||
r.domain = addr;
|
||||
else if (rtype == 'DOMAIN-SUFFIX')
|
||||
r.domain_suffix = addr;
|
||||
else if (rtype == 'DOMAIN-KEYWORD')
|
||||
r.domain_keyword = addr;
|
||||
else if (rtype == 'IP-CIDR' || rtype == 'IP-CIDR6')
|
||||
r.ip_cidr = addr;
|
||||
else
|
||||
continue;
|
||||
if (pg == 'DIRECT')
|
||||
r.outbound = dtag;
|
||||
else {
|
||||
if (ptag == null)
|
||||
ptag = _proxy_outbound_tag();
|
||||
r.outbound = ptag;
|
||||
}
|
||||
push(out, r);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
let _addtype_rules = _load_addtype_rules();
|
||||
if (length(_addtype_rules)) {
|
||||
let _nr = [];
|
||||
let _ins = false;
|
||||
for (let _i = 0; _i < length(cfg.route.rules); _i++) {
|
||||
let _ru = cfg.route.rules[_i];
|
||||
let _dns_front = _ru && type(_ru) == 'object' &&
|
||||
(_ru.action == 'hijack-dns' || (_ru.action == 'reject' && _ru.port));
|
||||
if (!_ins && !_dns_front) {
|
||||
for (let _cr in _addtype_rules)
|
||||
push(_nr, _cr);
|
||||
_ins = true;
|
||||
}
|
||||
push(_nr, _ru);
|
||||
}
|
||||
if (!_ins)
|
||||
for (let _cr in _addtype_rules)
|
||||
push(_nr, _cr);
|
||||
cfg.route.rules = _nr;
|
||||
}
|
||||
|
||||
if (uci_opt('enhanced_mode', 'fake-ip') == 'fake-ip') {
|
||||
add_remote_rule_set('geolocation-!cn',
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-!cn.srs');
|
||||
}
|
||||
|
||||
/* Local .srs -> local; remaining remote rule_sets must keep/get a download_detour.
|
||||
* CN first-start deadlock: no srs -> fetch rules -> via auto-select -> urltest dials
|
||||
* the proxy -> resolves its domain -> pulled into an unloaded rule_set by DNS rules -> deadline.
|
||||
* The srs URLs use a CN-reachable mirror, so download_detour
|
||||
* must be DIRECT (real direct-outbound tag if any, else 'DIRECT'). */
|
||||
let _pick_dl_detour = function() {
|
||||
if (cfg.outbounds) {
|
||||
for (let ob in cfg.outbounds) {
|
||||
if (ob && ob.type == 'direct' && ob.tag) return ob.tag;
|
||||
}
|
||||
}
|
||||
return 'DIRECT';
|
||||
};
|
||||
let _dl_detour = _pick_dl_detour();
|
||||
for (let rs in (cfg.route || {}).rule_set || []) {
|
||||
if (!rs) continue;
|
||||
if (rs.type != 'remote') { delete rs.download_detour; continue; }
|
||||
let path = local_rule_set_path(rs.tag || '');
|
||||
if (rs.tag && access(path, 'r')) {
|
||||
delete rs.url; delete rs.download_detour; rs.type = 'local'; rs.path = path;
|
||||
continue;
|
||||
}
|
||||
if (keep_remote_rule_set(rs) && rs.url) {
|
||||
rs.url = normalize_rule_set_url(rs.url);
|
||||
rs.download_detour = _dl_detour;
|
||||
continue;
|
||||
}
|
||||
/* No local cache -> skip this remote rule_set so a failed download doesn't block start.
|
||||
* Rule-sets are fetched to /usr/share/clashoo/ruleset/ once proxied. */
|
||||
continue;
|
||||
|
||||
}
|
||||
/* filter out remote rule_sets with no local cache */
|
||||
let _clean_rs = [];
|
||||
for (let _rsi = 0; _rsi < length(cfg.route.rule_set); _rsi++) {
|
||||
let _rs = cfg.route.rule_set[_rsi];
|
||||
if (!_rs) continue;
|
||||
if (_rs.type == 'remote' && _rs.url != null) {
|
||||
let _local_path = local_rule_set_path(_rs.tag || '');
|
||||
if (!_rs.tag || !access(_local_path, 'r'))
|
||||
if (keep_remote_rule_set(_rs) && _rs.url) {
|
||||
_rs.url = normalize_rule_set_url(_rs.url);
|
||||
_rs.download_detour = _dl_detour;
|
||||
push(_clean_rs, _rs);
|
||||
continue;
|
||||
} else {
|
||||
continue; /* no local cache -> drop from final list */
|
||||
}
|
||||
/* has local cache -> convert to local */
|
||||
delete _rs.url; delete _rs.download_detour; _rs.type = 'local'; _rs.path = _local_path;
|
||||
}
|
||||
push(_clean_rs, _rs);
|
||||
}
|
||||
cfg.route.rule_set = _clean_rs;
|
||||
prune_tun_cn_exclude_if_missing();
|
||||
|
||||
/* drop DNS/route rules referencing a removed rule_set, else sing-box FATAL: rule-set not found */
|
||||
let _existing_tags = {};
|
||||
for (let _rs in cfg.route.rule_set || []) if (_rs && _rs.tag) _existing_tags[_rs.tag] = true;
|
||||
let _rule_has_ref = function(_r) {
|
||||
if (!_r || type(_r) != 'object') return false;
|
||||
if (_r.rule_set) {
|
||||
if (type(_r.rule_set) == 'array') { for (let _t in _r.rule_set) if (!_existing_tags[_t]) return false; }
|
||||
else if (!_existing_tags[_r.rule_set]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
/* clean dns.rules */
|
||||
if (cfg.dns && type(cfg.dns.rules) == 'array') {
|
||||
let _clean_dns_rules = [];
|
||||
for (let _dr in cfg.dns.rules) if (_rule_has_ref(_dr)) push(_clean_dns_rules, _dr);
|
||||
cfg.dns.rules = _clean_dns_rules;
|
||||
}
|
||||
/* clean route.rules */
|
||||
if (cfg.route && type(cfg.route.rules) == 'array') {
|
||||
let _clean_rt_rules = [];
|
||||
for (let _rr in cfg.route.rules) if (_rule_has_ref(_rr)) push(_clean_rt_rules, _rr);
|
||||
cfg.route.rules = _clean_rt_rules;
|
||||
}
|
||||
cfg.experimental = cfg.experimental || {};
|
||||
cfg.experimental.clash_api = cfg.experimental.clash_api || {};
|
||||
cfg.experimental.clash_api.external_controller = '0.0.0.0:' + dash_port;
|
||||
cfg.experimental.clash_api.external_ui = '/etc/clashoo/dashboard';
|
||||
cfg.experimental.clash_api.secret = dash_secret;
|
||||
|
||||
cfg.experimental.cache_file = {
|
||||
enabled: true,
|
||||
store_fakeip: true
|
||||
};
|
||||
|
||||
/* OpenWrt already owns system time sync. Keep sing-box NTP disabled by
|
||||
* default to avoid noisy IPv6 UDP/123 failures on IPv4-only routers. */
|
||||
cfg.ntp = cfg.ntp || {};
|
||||
cfg.ntp.enabled = false;
|
||||
|
||||
cfg.log = cfg.log || {};
|
||||
cfg.log.output = '/var/log/clashoo/core.log';
|
||||
if (!cfg.log.level)
|
||||
cfg.log.level = 'info';
|
||||
|
||||
if (writefile(path, sprintf('%J', cfg)) === null) {
|
||||
print("write failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
print("normalized\n");
|
||||
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info",
|
||||
"timestamp": true
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{ "type": "tcp", "tag": "dns_proxy", "server": "1.1.1.1", "domain_resolver": "dns_resolver" },
|
||||
{ "type": "https", "tag": "dns_direct", "server": "dns.alidns.com", "domain_resolver": "dns_resolver" },
|
||||
{ "type": "udp", "tag": "dns_resolver", "server": "223.5.5.5" },
|
||||
{ "type": "fakeip", "tag": "dns_fakeip", "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" }
|
||||
],
|
||||
"rules": [
|
||||
{ "rule_set": "geolocation-!cn", "query_type": ["A", "AAAA", "CNAME"], "server": "dns_fakeip" },
|
||||
{ "query_type": ["A", "AAAA", "CNAME"], "invert": true, "action": "reject" }
|
||||
],
|
||||
"final": "dns_direct"
|
||||
},
|
||||
"ntp": {
|
||||
"enabled": false,
|
||||
"server": "time.apple.com",
|
||||
"server_port": 123,
|
||||
"interval": "30m"
|
||||
},
|
||||
"inbounds": [
|
||||
{ "type": "mixed", "tag": "mixed-in", "listen": "0.0.0.0", "listen_port": 2080 },
|
||||
{ "type": "tun", "tag": "tun-in", "address": ["172.19.0.1/30"], "auto_route": true, "auto_redirect": true, "strict_route": true, "stack": "mixed", "route_exclude_address_set": ["cn-ip"] }
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "selector", "tag": "🚀 节点选择",
|
||||
"outbounds": ["🌍 全部节点","♻️ 自动选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","DIRECT"],
|
||||
"default": "🌍 全部节点"
|
||||
},
|
||||
{ "type": "selector", "tag": "▶️ YouTube", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🔍 Google", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🔮 OpenAI", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🧠 Claude", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🤖 Gemini", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🐦 Twitter", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "📦 GitHub", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "Ⓜ️ Microsoft", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🎵 TikTok", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "📺 Netflix", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🎬 流媒体", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "💳 PayPal", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "📲 电报消息", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🍏 苹果服务", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] },
|
||||
{ "type": "selector", "tag": "🎯 全球直连", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] },
|
||||
{ "type": "selector", "tag": "🏠 私有网络", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] },
|
||||
{ "type": "selector", "tag": "🌐 非中国", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🐟 漏网之鱼", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
|
||||
{ "type": "selector", "tag": "🇭🇰 香港节点", "outbounds": ["__NODES_HK__"] },
|
||||
{ "type": "selector", "tag": "🇯🇵 日本节点", "outbounds": ["__NODES_JP__"] },
|
||||
{ "type": "selector", "tag": "🇺🇲 美国节点", "outbounds": ["__NODES_US__"] },
|
||||
{ "type": "selector", "tag": "🇸🇬 新加坡节点","outbounds": ["__NODES_SG__"] },
|
||||
{ "type": "selector", "tag": "🇺🇳 集合节点", "outbounds": ["__NODES_OTHER__"] },
|
||||
{ "type": "selector", "tag": "🌍 全部节点", "outbounds": ["__NODES__"] },
|
||||
|
||||
{ "type": "urltest", "tag": "♻️ 香港自动", "outbounds": ["__NODES_HK__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
{ "type": "urltest", "tag": "♻️ 日本自动", "outbounds": ["__NODES_JP__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
{ "type": "urltest", "tag": "♻️ 美国自动", "outbounds": ["__NODES_US__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
{ "type": "urltest", "tag": "♻️ 新加坡自动", "outbounds": ["__NODES_SG__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
{ "type": "urltest", "tag": "♻️ 集合自动", "outbounds": ["__NODES_OTHER__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
{ "type": "urltest", "tag": "♻️ 自动选择", "outbounds": ["__NODES__"], "url": "https://www.gstatic.com/generate_204", "interval": "10m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
|
||||
{ "type": "direct", "tag": "DIRECT" }
|
||||
],
|
||||
"route": {
|
||||
"default_domain_resolver": "dns_resolver",
|
||||
"rule_set": [
|
||||
{ "tag": "openai", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/openai.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "anthropic", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/anthropic.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "google-gemini", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/google-gemini.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "youtube", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/youtube.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "google", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/google.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "twitter", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/twitter.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "github", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/github.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "gitlab", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/gitlab.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "microsoft", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/microsoft.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "tiktok", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/tiktok.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "netflix", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/netflix.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "hulu", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/hulu.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "disney", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/disney.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "hbo", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/hbo.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "amazon", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/amazon.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "bahamut", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/bahamut.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "paypal", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/paypal.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "apple", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/apple.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "geolocation-cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-cn.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/cn.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "geolocation-!cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-!cn.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "google-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/google.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "private-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/private.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "cn-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/cn.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "telegram-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/telegram.srs", "download_detour": "DIRECT" }
|
||||
],
|
||||
"rules": [
|
||||
{ "action": "sniff" },
|
||||
{ "clash_mode": "direct", "outbound": "DIRECT" },
|
||||
{ "clash_mode": "global", "outbound": "♻️ 自动选择" },
|
||||
{ "protocol": "dns", "action": "hijack-dns" },
|
||||
{ "rule_set": "openai", "outbound": "🔮 OpenAI" },
|
||||
{ "rule_set": "anthropic", "outbound": "🧠 Claude" },
|
||||
{ "rule_set": "google-gemini", "outbound": "🤖 Gemini" },
|
||||
{ "rule_set": "youtube", "outbound": "▶️ YouTube" },
|
||||
{ "rule_set": "google", "outbound": "🔍 Google" },
|
||||
{ "rule_set": "twitter", "outbound": "🐦 Twitter" },
|
||||
{ "rule_set": "tiktok", "outbound": "🎵 TikTok" },
|
||||
{ "rule_set": "netflix", "outbound": "📺 Netflix" },
|
||||
{ "rule_set": ["hulu", "disney", "hbo", "amazon", "bahamut"], "outbound": "🎬 流媒体" },
|
||||
{ "rule_set": "paypal", "outbound": "💳 PayPal" },
|
||||
{ "rule_set": "microsoft", "outbound": "Ⓜ️ Microsoft" },
|
||||
{ "rule_set": "apple", "outbound": "🍏 苹果服务" },
|
||||
{ "rule_set": ["github", "gitlab"], "outbound": "📦 GitHub" },
|
||||
{ "rule_set": "telegram-ip", "outbound": "📲 电报消息" },
|
||||
{ "rule_set": "private-ip", "outbound": "🏠 私有网络" },
|
||||
{ "rule_set": ["geolocation-cn", "cn"], "outbound": "🎯 全球直连" },
|
||||
{ "rule_set": "cn-ip", "outbound": "🎯 全球直连" },
|
||||
{ "domain_suffix": ["cn"], "outbound": "🎯 全球直连" },
|
||||
{ "rule_set": "geolocation-!cn", "outbound": "🌐 非中国" },
|
||||
{ "rule_set": "google-ip", "outbound": "🔍 Google" }
|
||||
],
|
||||
"auto_detect_interface": true,
|
||||
"final": "🐟 漏网之鱼"
|
||||
},
|
||||
"experimental": {
|
||||
"cache_file": { "enabled": true, "store_fakeip": true },
|
||||
"clash_api": {
|
||||
"external_controller": "0.0.0.0:9090",
|
||||
"external_ui": "dashboard",
|
||||
"secret": "clashoo",
|
||||
"external_ui_download_url": "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip",
|
||||
"external_ui_download_detour": "DIRECT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
#!/usr/bin/ucode
|
||||
/* yaml2singbox.uc — convert mihomo/clash YAML proxies to sing-box outbounds
|
||||
* Usage: ucode yaml2singbox.uc <yaml-in> [template-json] [json-out]
|
||||
* Supports: ss, vmess, vless, trojan, hysteria2, tuic. Others are skipped.
|
||||
* "__NODES__" in the template selector outbounds is expanded to all node tags. */
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile, popen } from 'fs';
|
||||
|
||||
const TPL_DEFAULT = '/usr/share/clashoo/lib/templates/default.json';
|
||||
const SUPPORTED = {
|
||||
'ss': 'shadowsocks',
|
||||
'shadowsocks':'shadowsocks',
|
||||
'vmess': 'vmess',
|
||||
'vless': 'vless',
|
||||
'trojan': 'trojan',
|
||||
'hysteria2': 'hysteria2',
|
||||
'hy2': 'hysteria2',
|
||||
'tuic': 'tuic'
|
||||
};
|
||||
|
||||
function logerr(msg) {
|
||||
warn(sprintf("[yaml2singbox] %s\n", msg));
|
||||
}
|
||||
|
||||
function die(msg, code) {
|
||||
logerr(msg);
|
||||
exit(code || 1);
|
||||
}
|
||||
|
||||
/* ---------- YAML read (via yq) ---------- */
|
||||
function quote_sh(s) {
|
||||
/* single-quote for shell; escape embedded quotes */
|
||||
return "'" + replace(s, "'", "'\\''") + "'";
|
||||
}
|
||||
|
||||
function read_yaml_as_json(path) {
|
||||
const cmd = sprintf("yq -o=json eval . %s 2>/dev/null", quote_sh(path));
|
||||
const h = popen(cmd, "r");
|
||||
if (!h)
|
||||
die(sprintf("popen failed: %s", cmd));
|
||||
let buf = "", chunk;
|
||||
while ((chunk = h.read(65536)))
|
||||
buf += chunk;
|
||||
h.close();
|
||||
if (!length(buf))
|
||||
die(sprintf("yq returned empty for %s (yaml loadable?)", path));
|
||||
const data = json(buf);
|
||||
if (data === null)
|
||||
die("json parse of yq output failed");
|
||||
return data;
|
||||
}
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
function pick(obj, ...keys) {
|
||||
for (let k in keys)
|
||||
if (obj[k] != null)
|
||||
return obj[k];
|
||||
return null;
|
||||
}
|
||||
|
||||
function tobool(v) {
|
||||
if (v === true || v === 'true' || v === 1 || v === '1') return true;
|
||||
if (v === false || v === 'false' || v === 0 || v === '0') return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
function toint(v) {
|
||||
if (v == null) return null;
|
||||
let n = +v;
|
||||
return (n === n) ? n : null; /* NaN check */
|
||||
}
|
||||
|
||||
function strip_null(obj) {
|
||||
if (type(obj) !== 'object') return obj;
|
||||
const out = {};
|
||||
for (let k in obj) {
|
||||
const v = obj[k];
|
||||
if (v == null) continue;
|
||||
if (type(v) === 'object') {
|
||||
const sv = strip_null(v);
|
||||
if (length(sv) > 0) out[k] = sv;
|
||||
} else if (type(v) === 'array') {
|
||||
const arr = map(v, (x) => (type(x) === 'object') ? strip_null(x) : x);
|
||||
if (length(arr) > 0) out[k] = arr;
|
||||
} else {
|
||||
out[k] = v;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ---------- TLS block assembly ---------- */
|
||||
function build_tls(p) {
|
||||
const enabled = tobool(p.tls) || (p.sni && length(p.sni) > 0) || (p.servername && length(p.servername) > 0);
|
||||
if (!enabled) return null;
|
||||
const tls = {
|
||||
enabled: true,
|
||||
server_name: pick(p, 'sni', 'servername'),
|
||||
insecure: tobool(p['skip-cert-verify']) === true ? true : null,
|
||||
alpn: p.alpn
|
||||
};
|
||||
const reality = p['reality-opts'];
|
||||
if (reality && type(reality) === 'object') {
|
||||
tls.reality = {
|
||||
enabled: true,
|
||||
public_key: reality['public-key'],
|
||||
short_id: reality['short-id']
|
||||
};
|
||||
}
|
||||
const client_fp = p['client-fingerprint'];
|
||||
if (client_fp)
|
||||
tls.utls = { enabled: true, fingerprint: client_fp };
|
||||
return strip_null(tls);
|
||||
}
|
||||
|
||||
/* ---------- transport (ws / grpc / http) ---------- */
|
||||
function build_transport(p) {
|
||||
const net = p.network;
|
||||
if (!net || net === 'tcp') return null;
|
||||
if (net === 'ws') {
|
||||
const opts = p['ws-opts'] || {};
|
||||
return strip_null({
|
||||
type: 'ws',
|
||||
path: opts.path,
|
||||
headers: opts.headers,
|
||||
max_early_data: toint(opts['max-early-data']),
|
||||
early_data_header_name: opts['early-data-header-name']
|
||||
});
|
||||
}
|
||||
if (net === 'grpc') {
|
||||
const opts = p['grpc-opts'] || {};
|
||||
return { type: 'grpc', service_name: opts['grpc-service-name'] };
|
||||
}
|
||||
if (net === 'http' || net === 'h2') {
|
||||
const opts = p['h2-opts'] || p['http-opts'] || {};
|
||||
return strip_null({
|
||||
type: 'http',
|
||||
host: opts.host || opts.Host,
|
||||
path: opts.path
|
||||
});
|
||||
}
|
||||
if (net === 'httpupgrade') {
|
||||
const opts = p['http-upgrade-opts'] || {};
|
||||
return strip_null({ type: 'httpupgrade', path: opts.path, host: opts.host });
|
||||
}
|
||||
logerr(sprintf("unknown transport network '%s' for node '%s', ignored", net, p.name));
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---------- per-protocol conversion ---------- */
|
||||
function convert_ss(p) {
|
||||
const plugin = p.plugin, popts = p['plugin-opts'] || {};
|
||||
let plugin_opts = null;
|
||||
if (plugin && type(popts) === 'object') {
|
||||
/* Field mapping: Clash simple-obfs -> sing-box obfs-local
|
||||
* Clash: mode=http;host=xxx → sing-box: obfs=http;obfs-host=xxx */
|
||||
const FIELD_MAP = {
|
||||
'mode': 'obfs',
|
||||
'host': 'obfs-host',
|
||||
'uri': 'obfs-uri'
|
||||
};
|
||||
const parts = [];
|
||||
for (let k in popts) {
|
||||
const v = popts[k];
|
||||
if (type(v) === 'object') continue;
|
||||
push(parts, (FIELD_MAP[k] || k) + '=' + v);
|
||||
}
|
||||
plugin_opts = join(';', parts);
|
||||
}
|
||||
/* Clash "obfs" maps to sing-box plugin "obfs-local" */
|
||||
let sb_plugin = null;
|
||||
if (plugin === 'obfs') sb_plugin = 'obfs-local';
|
||||
else if (plugin === 'v2ray-plugin') sb_plugin = 'v2ray-plugin';
|
||||
else if (plugin === 'shadow-tls') sb_plugin = 'shadow-tls';
|
||||
else if (plugin) sb_plugin = plugin;
|
||||
|
||||
return strip_null({
|
||||
type: 'shadowsocks',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
method: p.cipher,
|
||||
password: p.password,
|
||||
plugin: sb_plugin,
|
||||
plugin_opts: plugin_opts
|
||||
});
|
||||
}
|
||||
|
||||
function convert_vmess(p) {
|
||||
return strip_null({
|
||||
type: 'vmess',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
uuid: p.uuid,
|
||||
alter_id: toint(p.alterId) || 0,
|
||||
security: p.cipher || 'auto',
|
||||
tls: build_tls(p),
|
||||
transport: build_transport(p)
|
||||
});
|
||||
}
|
||||
|
||||
function convert_vless(p) {
|
||||
return strip_null({
|
||||
type: 'vless',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
uuid: p.uuid,
|
||||
flow: p.flow,
|
||||
tls: build_tls(p),
|
||||
transport: build_transport(p)
|
||||
});
|
||||
}
|
||||
|
||||
function convert_trojan(p) {
|
||||
return strip_null({
|
||||
type: 'trojan',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
password: p.password,
|
||||
tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server },
|
||||
transport: build_transport(p)
|
||||
});
|
||||
}
|
||||
|
||||
function convert_hysteria2(p) {
|
||||
const obfs_pass = p['obfs-password'];
|
||||
return strip_null({
|
||||
type: 'hysteria2',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
password: pick(p, 'password', 'auth'),
|
||||
up_mbps: toint(p.up),
|
||||
down_mbps: toint(p.down),
|
||||
obfs: (p.obfs && obfs_pass) ? { type: p.obfs, password: obfs_pass } : null,
|
||||
tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server }
|
||||
});
|
||||
}
|
||||
|
||||
function convert_tuic(p) {
|
||||
return strip_null({
|
||||
type: 'tuic',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
uuid: p.uuid,
|
||||
password: p.password,
|
||||
congestion_control: p['congestion-controller'] || p.congestion,
|
||||
udp_relay_mode: p['udp-relay-mode'],
|
||||
tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server, alpn: p.alpn }
|
||||
});
|
||||
}
|
||||
|
||||
function convert_proxy(p) {
|
||||
const sb_type = SUPPORTED[p.type];
|
||||
if (!sb_type) {
|
||||
if (p.type && p.type !== 'select')
|
||||
logerr(sprintf("skip unsupported type '%s' for '%s'", p.type, p.name));
|
||||
return null;
|
||||
}
|
||||
if (!p.server || !p.port || !p.name) {
|
||||
logerr(sprintf("skip proxy missing server/port/name (type=%s)", p.type));
|
||||
return null;
|
||||
}
|
||||
switch (p.type) {
|
||||
case 'ss':
|
||||
case 'shadowsocks': return convert_ss(p);
|
||||
case 'vmess': return convert_vmess(p);
|
||||
case 'vless': return convert_vless(p);
|
||||
case 'trojan': return convert_trojan(p);
|
||||
case 'hysteria2':
|
||||
case 'hy2': return convert_hysteria2(p);
|
||||
case 'tuic': return convert_tuic(p);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---------- dedup (tag must be unique) ---------- */
|
||||
function dedupe_tags(nodes) {
|
||||
const seen = {};
|
||||
for (let n in nodes) {
|
||||
let base = n.tag, t = base, i = 2;
|
||||
while (seen[t]) {
|
||||
t = base + '_' + i;
|
||||
i++;
|
||||
}
|
||||
n.tag = t;
|
||||
seen[t] = true;
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/* Drop airline pseudo-nodes (Traffic:/Expire:/quota/官网/QQ etc.) from selectors
|
||||
* and urltests. They're real outbounds (so the UI can read traffic/expiry) but
|
||||
* don't forward — in a selector/urltest they win (0ms) and swallow all traffic. */
|
||||
function is_pseudo_node_tag(tag) {
|
||||
if (!tag) return false;
|
||||
const t = '' + tag;
|
||||
const patterns = [
|
||||
/^Traffic[::]/i,
|
||||
/^Expire[::]/i,
|
||||
/剩余流量|剩余[::]/,
|
||||
/距离下次重置/,
|
||||
/到期(时间|日期)?[::]/,
|
||||
/官网[::]/,
|
||||
/网站[::]/,
|
||||
/套餐[::]?/,
|
||||
/客服[::]/,
|
||||
/QQ[群]?[::]/i,
|
||||
/Telegram|TG群|官方群/i,
|
||||
/续费|订阅地址|流量重置/,
|
||||
];
|
||||
for (let re in patterns) if (match(t, re)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---------- detect region from tag -> 'HK'/'JP'/'US'/'SG'/'OTHER'/'' ---------- */
|
||||
function region_of(tag) {
|
||||
if (!tag) return '';
|
||||
const t = '' + tag;
|
||||
if (match(t, /港|🇭🇰|HK[^A-Za-z]|[^A-Za-z]HK|^HK$|[Hh]ong[Kk]/)) return 'HK';
|
||||
if (match(t, /日|🇯🇵|JP[^A-Za-z]|[^A-Za-z]JP|^JP$|[Jj]apan/)) return 'JP';
|
||||
if (match(t, /美|🇺🇸|US[^A-Za-z]|[^A-Za-z]US|^US$|[Uu]nited.?[Ss]tates|[Aa]merica/)) return 'US';
|
||||
if (match(t, /新加坡|🇸🇬|SG[^A-Za-z]|[^A-Za-z]SG|^SG$|[Ss]ingapore/)) return 'SG';
|
||||
if (match(t, /台湾|台|🇹🇼|TW[^A-Za-z]|[^A-Za-z]TW|^TW$|[Tt]aiwan|韩国|韩|🇰🇷|KR[^A-Za-z]|[^A-Za-z]KR|^KR$|[Kk]orea/)) return 'OTHER';
|
||||
return '';
|
||||
}
|
||||
|
||||
/* ---------- expand __NODES__/__NODES_XX__ placeholders to real tags (dropping pseudo-nodes) ---------- */
|
||||
function expand_node_placeholder(outbounds, node_tags) {
|
||||
const real_tags = [];
|
||||
for (let t in node_tags) if (!is_pseudo_node_tag(t)) push(real_tags, t);
|
||||
|
||||
/* bucket by region */
|
||||
let tags_hk = [], tags_jp = [], tags_us = [], tags_sg = [], tags_other = [];
|
||||
for (let t in real_tags) {
|
||||
const r = region_of(t);
|
||||
if (r === 'HK') push(tags_hk, t);
|
||||
else if (r === 'JP') push(tags_jp, t);
|
||||
else if (r === 'US') push(tags_us, t);
|
||||
else if (r === 'SG') push(tags_sg, t);
|
||||
else if (r === 'OTHER') push(tags_other, t);
|
||||
}
|
||||
/* when a region has no nodes, fall back to all nodes (avoid empty outbounds) */
|
||||
if (!length(tags_hk)) tags_hk = real_tags;
|
||||
if (!length(tags_jp)) tags_jp = real_tags;
|
||||
if (!length(tags_us)) tags_us = real_tags;
|
||||
if (!length(tags_sg)) tags_sg = real_tags;
|
||||
if (!length(tags_other)) tags_other = real_tags;
|
||||
|
||||
for (let ob in outbounds) {
|
||||
if (ob.type !== 'selector' && ob.type !== 'urltest') continue;
|
||||
if (!ob.outbounds || type(ob.outbounds) !== 'array') continue;
|
||||
const expanded = [];
|
||||
for (let item in ob.outbounds) {
|
||||
let list = null;
|
||||
if (item === '__NODES__') list = real_tags;
|
||||
else if (item === '__NODES_HK__') list = tags_hk;
|
||||
else if (item === '__NODES_JP__') list = tags_jp;
|
||||
else if (item === '__NODES_US__') list = tags_us;
|
||||
else if (item === '__NODES_SG__') list = tags_sg;
|
||||
else if (item === '__NODES_OTHER__') list = tags_other;
|
||||
if (list !== null) { for (let t in list) push(expanded, t); }
|
||||
else { push(expanded, item); }
|
||||
}
|
||||
ob.outbounds = expanded;
|
||||
}
|
||||
return outbounds;
|
||||
}
|
||||
|
||||
/* ---------- proxy-providers ---------- */
|
||||
function resolve_providers(yaml) {
|
||||
if (type(yaml['proxy-providers']) !== 'object')
|
||||
return [];
|
||||
const all = [];
|
||||
for (let name in yaml['proxy-providers']) {
|
||||
const p = yaml['proxy-providers'][name];
|
||||
if (p.type !== 'http' || !p.url) {
|
||||
logerr(sprintf("provider '%s': skip (not http or no url)", name));
|
||||
continue;
|
||||
}
|
||||
const cmd = sprintf("wget -q -O- --timeout=20 %s 2>/dev/null | yq -o=json eval . 2>/dev/null", quote_sh(p.url));
|
||||
const h = popen(cmd, "r");
|
||||
if (!h) { logerr(sprintf("provider '%s': download failed", name)); continue; }
|
||||
let buf = "", chunk;
|
||||
while ((chunk = h.read(65536))) buf += chunk;
|
||||
h.close();
|
||||
if (!length(buf)) { logerr(sprintf("provider '%s': empty response", name)); continue; }
|
||||
const data = json(buf);
|
||||
if (!data || type(data.proxies) !== 'array') {
|
||||
logerr(sprintf("provider '%s': no proxies array in response", name));
|
||||
continue;
|
||||
}
|
||||
logerr(sprintf("provider '%s': %d proxies", name, length(data.proxies)));
|
||||
for (let px in data.proxies) push(all, px);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/* ---------- main ---------- */
|
||||
const yaml_path = ARGV[0];
|
||||
const tpl_path = ARGV[1] || TPL_DEFAULT;
|
||||
const out_path = ARGV[2];
|
||||
|
||||
if (!yaml_path)
|
||||
die("usage: ucode yaml2singbox.uc <yaml-in> [template-json] [json-out]");
|
||||
|
||||
const yaml = read_yaml_as_json(yaml_path);
|
||||
|
||||
let proxies = [];
|
||||
if (type(yaml.proxies) === 'array')
|
||||
proxies = yaml.proxies;
|
||||
else
|
||||
logerr("no inline proxies array, checking proxy-providers...");
|
||||
|
||||
if (type(yaml['proxy-providers']) === 'object') {
|
||||
const pproxies = resolve_providers(yaml);
|
||||
for (let p in pproxies) push(proxies, p);
|
||||
}
|
||||
|
||||
if (!length(proxies))
|
||||
die(sprintf("no proxies found in %s (inline or via providers)", yaml_path));
|
||||
|
||||
const tpl_raw = readfile(tpl_path);
|
||||
if (!tpl_raw)
|
||||
die(sprintf("cannot read template %s", tpl_path));
|
||||
const tpl = json(tpl_raw);
|
||||
if (!tpl || type(tpl.outbounds) !== 'array')
|
||||
die(sprintf("template %s has no outbounds[]", tpl_path));
|
||||
|
||||
/* convert proxies */
|
||||
const nodes = [];
|
||||
let skipped = 0;
|
||||
for (let p in proxies) {
|
||||
const o = convert_proxy(p);
|
||||
if (o) push(nodes, o); else skipped++;
|
||||
}
|
||||
dedupe_tags(nodes);
|
||||
|
||||
if (!length(nodes))
|
||||
die(sprintf("no usable nodes converted from %d proxies (skipped=%d)", length(proxies), skipped));
|
||||
|
||||
logerr(sprintf("converted=%d skipped=%d", length(nodes), skipped));
|
||||
|
||||
/* prepend converted nodes to template outbounds */
|
||||
const final_outbounds = [];
|
||||
for (let n in nodes) push(final_outbounds, n);
|
||||
for (let ob in tpl.outbounds) push(final_outbounds, ob);
|
||||
|
||||
/* expand __NODES__ placeholders */
|
||||
const node_tags = map(nodes, (n) => n.tag);
|
||||
expand_node_placeholder(final_outbounds, node_tags);
|
||||
|
||||
tpl.outbounds = final_outbounds;
|
||||
|
||||
const out = sprintf("%.J\n", tpl);
|
||||
if (out_path) {
|
||||
if (!writefile(out_path, out))
|
||||
die(sprintf("writefile failed: %s", out_path));
|
||||
logerr(sprintf("wrote %s (%d bytes, %d nodes)", out_path, length(out), length(nodes)));
|
||||
} else {
|
||||
print(out);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -f
|
||||
|
||||
[ "$(uci -q get clashoo.config.fake_ip_filter_migrated 2>/dev/null)" = "1" ] && exit 0
|
||||
|
||||
filters="$(uci -q get clashoo.config.fake_ip_filter 2>/dev/null)"
|
||||
has_legacy=0
|
||||
for filter in $filters; do
|
||||
[ "$filter" = "*.lan" ] && has_legacy=1
|
||||
done
|
||||
|
||||
if [ "$has_legacy" = "1" ]; then
|
||||
uci -q delete clashoo.config.fake_ip_filter
|
||||
added_lan=0
|
||||
added_local=0
|
||||
for filter in $filters; do
|
||||
case "$filter" in
|
||||
'*.lan'|'+.lan')
|
||||
if [ "$added_lan" = "0" ]; then
|
||||
uci -q add_list clashoo.config.fake_ip_filter='+.lan'
|
||||
added_lan=1
|
||||
fi
|
||||
;;
|
||||
'+.local')
|
||||
if [ "$added_local" = "0" ]; then
|
||||
uci -q add_list clashoo.config.fake_ip_filter='+.local'
|
||||
added_local=1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
uci -q add_list clashoo.config.fake_ip_filter="$filter"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [ "$added_local" = "0" ]; then
|
||||
uci -q add_list clashoo.config.fake_ip_filter='+.local'
|
||||
fi
|
||||
fi
|
||||
|
||||
uci -q set clashoo.config.fake_ip_filter_migrated='1'
|
||||
uci -q commit clashoo
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$(uci -q get clashoo.config.acl_migrated 2>/dev/null)" = "1" ] && exit 0
|
||||
if uci -q show clashoo 2>/dev/null | grep -q '=lan_acl$'; then
|
||||
uci -q set clashoo.config.acl_migrated='1'
|
||||
uci -q commit clashoo
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mode="$(uci -q get clashoo.config.access_control 2>/dev/null)"
|
||||
|
||||
add_group() {
|
||||
local dns="$1" proxy="$2" list_key="${3:-}" section value
|
||||
|
||||
section="$(uci -q add clashoo lan_acl)" || exit 1
|
||||
uci -q set clashoo."$section".enabled='1'
|
||||
uci -q set clashoo."$section".dns="$dns"
|
||||
uci -q set clashoo."$section".proxy="$proxy"
|
||||
if [ -n "$list_key" ]; then
|
||||
for value in $(uci -q get clashoo.config."$list_key" 2>/dev/null); do
|
||||
uci -q add_list clashoo."$section".ip="$value"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
case "$mode" in
|
||||
1)
|
||||
add_group 1 1 proxy_lan_ips
|
||||
add_group 0 0
|
||||
;;
|
||||
2)
|
||||
add_group 0 0 reject_lan_ips
|
||||
add_group 1 1
|
||||
;;
|
||||
esac
|
||||
|
||||
uci -q set clashoo.config.acl_migrated='1'
|
||||
uci -q commit clashoo
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/sh
|
||||
# clashoo access check
|
||||
# 用法: access_check.sh <url> [mode]
|
||||
# mode = direct | proxy(默认 proxy)
|
||||
# direct: 经 openwrt 主干直连,不走任何代理
|
||||
# proxy: 走 clashoo 本地代理端口,验证核心出站
|
||||
|
||||
url="$1"
|
||||
mode="${2:-proxy}"
|
||||
[ -n "$url" ] || exit 1
|
||||
|
||||
attempts=1
|
||||
ok=0
|
||||
sum_ms=0
|
||||
last_code=000
|
||||
|
||||
url_host() {
|
||||
printf '%s' "$1" | sed -n 's#^[a-zA-Z][a-zA-Z0-9+.-]*://\([^/:?#]*\).*$#\1#p'
|
||||
}
|
||||
|
||||
url_port() {
|
||||
_u="$1"
|
||||
_p="$(printf '%s' "$_u" | sed -n 's#^[a-zA-Z][a-zA-Z0-9+.-]*://[^/:?#]*:\([0-9][0-9]*\).*$#\1#p')"
|
||||
[ -n "$_p" ] && { printf '%s' "$_p"; return; }
|
||||
case "$_u" in
|
||||
https://*) printf '443' ;;
|
||||
http://*) printf '80' ;;
|
||||
*) printf '443' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_with_timeout() {
|
||||
_secs="$1"
|
||||
shift
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "$_secs" "$@" 2>/dev/null
|
||||
else
|
||||
"$@" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_real_ip() {
|
||||
_host="$1"
|
||||
[ -n "$_host" ] || return 0
|
||||
for _dns in 223.5.5.5 119.29.29.29 1.1.1.1; do
|
||||
_ip="$(run_with_timeout 1 nslookup "$_host" "$_dns" | awk '/^Address: /{print $2} /^Address [0-9]+: /{print $3}' | grep -E '^[0-9]+(\.[0-9]+){3}$' | tail -n1)"
|
||||
[ -n "$_ip" ] && { printf '%s' "$_ip"; return; }
|
||||
done
|
||||
}
|
||||
|
||||
set -- -4 -L --max-time 5 --connect-timeout 3 -s -o /dev/null -w '%{http_code} %{time_total}'
|
||||
if [ "$mode" = "direct" ]; then
|
||||
# 强制不走任何代理,避免误读 http_proxy 环境变量
|
||||
set -- "$@" --noproxy '*'
|
||||
host="$(url_host "$url")"
|
||||
port="$(url_port "$url")"
|
||||
real_ip="$(resolve_real_ip "$host")"
|
||||
[ -n "$host" ] && [ -n "$port" ] && [ -n "$real_ip" ] && set -- "$@" --resolve "${host}:${port}:${real_ip}"
|
||||
else
|
||||
proxy_port="$(uci get clashoo.config.mixed_port 2>/dev/null)"
|
||||
[ -z "$proxy_port" ] && proxy_port="$(uci get clashoo.config.http_port 2>/dev/null)"
|
||||
[ -z "$proxy_port" ] && proxy_port=7890
|
||||
set -- "$@" -x "http://127.0.0.1:${proxy_port}"
|
||||
fi
|
||||
set -- "$@" "$url"
|
||||
|
||||
i=1
|
||||
while [ "$i" -le "$attempts" ]; do
|
||||
out="$(curl "$@" 2>/dev/null || true)"
|
||||
code="$(printf '%s' "$out" | awk '{print $1}')"
|
||||
time_s="$(printf '%s' "$out" | awk '{print $2}')"
|
||||
[ -n "$code" ] || code=000
|
||||
[ -n "$time_s" ] || time_s=0
|
||||
ms="$(awk -v t="$time_s" 'BEGIN{printf "%d", t*1000}')"
|
||||
last_code="$code"
|
||||
if [ "$code" = "200" ] || [ "$code" = "204" ] || [ "$code" = "301" ] || [ "$code" = "302" ]; then
|
||||
ok=$((ok + 1))
|
||||
sum_ms=$((sum_ms + ms))
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
loss=$((attempts - ok))
|
||||
avg_ms=0
|
||||
if [ "$ok" -gt 0 ]; then
|
||||
avg_ms=$((sum_ms / ok))
|
||||
fi
|
||||
|
||||
echo "ok=$ok attempts=$attempts loss=$loss avg_ms=$avg_ms code=$last_code"
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
CACHE_DIR="/tmp/clashoo"
|
||||
CACHE_FILE="/tmp/clashoo_check_cache"
|
||||
LOCK_DIR="${CACHE_DIR}/access_check.lock"
|
||||
LOCK_PID_FILE="${LOCK_DIR}/pid"
|
||||
UPDATING_FLAG="${CACHE_DIR}/access_check_updating"
|
||||
TMP_FILE="${CACHE_FILE}.tmp.$$"
|
||||
DIAG_LOG="/tmp/clashoo_access_check.log"
|
||||
|
||||
take_lock() {
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || return 1
|
||||
printf '%s\n' "$$" > "$LOCK_PID_FILE" 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
pid_is_cache_worker() {
|
||||
[ -r "/proc/$1/cmdline" ] || return 1
|
||||
tr '\0' ' ' <"/proc/$1/cmdline" 2>/dev/null | grep -q 'access_check_cache\.sh'
|
||||
}
|
||||
|
||||
lock_is_dead() {
|
||||
_pid="$(cat "$LOCK_PID_FILE" 2>/dev/null)"
|
||||
case "$_pid" in
|
||||
''|*[!0-9]*)
|
||||
[ -n "$(find "$LOCK_DIR" -maxdepth 0 -mmin +3 2>/dev/null)" ]
|
||||
return $?
|
||||
;;
|
||||
esac
|
||||
[ ! -d "/proc/$_pid" ] && return 0
|
||||
! pid_is_cache_worker "$_pid"
|
||||
}
|
||||
|
||||
mkdir -p "$CACHE_DIR"
|
||||
if ! take_lock; then
|
||||
lock_is_dead || exit 0
|
||||
rm -rf "$LOCK_DIR" >/dev/null 2>&1
|
||||
take_lock || exit 0
|
||||
fi
|
||||
trap 'rm -rf "$LOCK_DIR" "$UPDATING_FLAG" "$TMP_FILE"' EXIT INT TERM
|
||||
: > "$UPDATING_FLAG"
|
||||
|
||||
safe_int() {
|
||||
case "${1:-}" in
|
||||
''|*[!0-9]*)
|
||||
printf '0'
|
||||
;;
|
||||
*)
|
||||
printf '%s' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
safe_code() {
|
||||
case "${1:-}" in
|
||||
''|*[!0-9A-Za-z]*)
|
||||
printf '000'
|
||||
;;
|
||||
*)
|
||||
printf '%s' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
parse_field() {
|
||||
_line="$1"
|
||||
_key="$2"
|
||||
printf '%s\n' "$_line" | sed -n "s/.*${_key}=\\([^ ]*\\).*/\\1/p"
|
||||
}
|
||||
|
||||
probe_json() {
|
||||
_line="$1"
|
||||
_ok="$(safe_int "$(parse_field "$_line" "ok")")"
|
||||
_attempts="$(safe_int "$(parse_field "$_line" "attempts")")"
|
||||
_loss="$(safe_int "$(parse_field "$_line" "loss")")"
|
||||
_avg_ms="$(safe_int "$(parse_field "$_line" "avg_ms")")"
|
||||
_code="$(safe_code "$(parse_field "$_line" "code")")"
|
||||
|
||||
_state="down"
|
||||
if [ "$_ok" -ge "$_attempts" ] && [ "$_attempts" -gt 0 ]; then
|
||||
if [ "$_avg_ms" -ge 2500 ]; then
|
||||
_state="high_latency"
|
||||
else
|
||||
_state="ok"
|
||||
fi
|
||||
elif [ "$_ok" -gt 0 ]; then
|
||||
_state="loss"
|
||||
fi
|
||||
|
||||
_ok_bool=false
|
||||
[ "$_ok" -gt 0 ] && _ok_bool=true
|
||||
|
||||
printf '{"ok":%s,"state":"%s","code":"%s","ok_count":%s,"attempts":%s,"loss":%s,"avg_ms":%s}' \
|
||||
"$_ok_bool" "$_state" "$_code" "$_ok" "$_attempts" "$_loss" "$_avg_ms"
|
||||
}
|
||||
|
||||
log_diag() {
|
||||
_line="$(date '+%Y-%m-%d %H:%M:%S') $1"
|
||||
printf '%s\n' "$_line" >>"$DIAG_LOG" 2>/dev/null
|
||||
_n="$(wc -l <"$DIAG_LOG" 2>/dev/null || echo 0)"
|
||||
if [ "${_n:-0}" -gt 200 ]; then
|
||||
tail -n 100 "$DIAG_LOG" >"${DIAG_LOG}.tmp" 2>/dev/null && mv "${DIAG_LOG}.tmp" "$DIAG_LOG" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
probe_run() {
|
||||
_url="$1"
|
||||
_mode="$2"
|
||||
# nice + ionice:探测 IO/CPU 都低优先级,避免抢占 LuCI dispatcher
|
||||
nice -n 19 /usr/share/clashoo/net/access_check.sh "$_url" "$_mode" 2>/dev/null || true
|
||||
}
|
||||
|
||||
proxy_port="$(uci -q get clashoo.config.mixed_port)"
|
||||
[ -z "$proxy_port" ] && proxy_port="$(uci -q get clashoo.config.http_port)"
|
||||
[ -z "$proxy_port" ] && proxy_port="7890"
|
||||
tcp_mode="$(uci -q get clashoo.config.tcp_mode)"
|
||||
[ -z "$tcp_mode" ] && tcp_mode="redirect"
|
||||
udp_mode="$(uci -q get clashoo.config.udp_mode)"
|
||||
[ -z "$udp_mode" ] && udp_mode="$tcp_mode"
|
||||
|
||||
proxy_listening() {
|
||||
# 检测 mixed-port 是否在 LISTEN(clashoo 未运行时立即标 proxy down,避免显示假绿)
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -tln 2>/dev/null | awk -v p=":${proxy_port}" '$0 ~ "LISTEN" && index($4, p) {found=1; exit} END{exit !found}'
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
netstat -tln 2>/dev/null | awk -v p=":${proxy_port}" '$0 ~ "LISTEN" && index($4, p) {found=1; exit} END{exit !found}'
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 并行探测,把 CPU 抢占窗口从串行 2s+ 压缩到最慢一路的耗时
|
||||
f_db="${TMP_FILE}.db"
|
||||
f_dy="${TMP_FILE}.dy"
|
||||
f_pb="${TMP_FILE}.pb"
|
||||
f_py="${TMP_FILE}.py"
|
||||
probe_run "https://www.douyin.com/generate_204" "direct" >"$f_db" &
|
||||
probe_run "https://www.youtube.com/generate_204" "direct" >"$f_dy" &
|
||||
if proxy_listening; then
|
||||
proxy_skipped=0
|
||||
probe_run "https://www.douyin.com/generate_204" "proxy" >"$f_pb" &
|
||||
probe_run "https://www.youtube.com/generate_204" "proxy" >"$f_py" &
|
||||
else
|
||||
# 代理端口未监听(clashoo 已停止),跳过探测,直接标 down
|
||||
proxy_skipped=1
|
||||
echo "ok=0 attempts=1 loss=1 avg_ms=0 code=skip" >"$f_pb"
|
||||
echo "ok=0 attempts=1 loss=1 avg_ms=0 code=skip" >"$f_py"
|
||||
fi
|
||||
wait
|
||||
direct_bytedance="$(cat "$f_db" 2>/dev/null)"
|
||||
direct_youtube="$(cat "$f_dy" 2>/dev/null)"
|
||||
proxy_bytedance="$(cat "$f_pb" 2>/dev/null)"
|
||||
proxy_youtube="$(cat "$f_py" 2>/dev/null)"
|
||||
rm -f "$f_db" "$f_dy" "$f_pb" "$f_py"
|
||||
|
||||
updated_at="$(date +%s)"
|
||||
|
||||
probe_all_down() {
|
||||
for _p in "$direct_bytedance" "$direct_youtube" "$proxy_bytedance" "$proxy_youtube"; do
|
||||
case "$_p" in
|
||||
''|*"ok=0 "*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ "$(uci -q get clashoo.config.access_check_debug)" = "1" ] || probe_all_down; then
|
||||
log_diag "port=${proxy_port} proxy_skipped=${proxy_skipped} direct_bd=[${direct_bytedance}] direct_yt=[${direct_youtube}] proxy_bd=[${proxy_bytedance}] proxy_yt=[${proxy_youtube}]"
|
||||
fi
|
||||
|
||||
cat >"$TMP_FILE" <<EOF
|
||||
{
|
||||
"proxy_port": "${proxy_port}",
|
||||
"tcp_mode": "${tcp_mode}",
|
||||
"udp_mode": "${udp_mode}",
|
||||
"updated_at": ${updated_at},
|
||||
"proxy_skipped": ${proxy_skipped},
|
||||
"stale": false,
|
||||
"updating": false,
|
||||
"direct": {
|
||||
"bytedance": $(probe_json "$direct_bytedance"),
|
||||
"youtube": $(probe_json "$direct_youtube")
|
||||
},
|
||||
"proxy": {
|
||||
"bytedance": $(probe_json "$proxy_bytedance"),
|
||||
"youtube": $(probe_json "$proxy_youtube")
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
mv -f "$TMP_FILE" "$CACHE_FILE"
|
||||
exit 0
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
RUNDIR="/tmp/clashoo"
|
||||
PID_FILE="${RUNDIR}/access_check_daemon.pid"
|
||||
LOCK_DIR="${RUNDIR}/access_check_daemon.lock"
|
||||
# 周期:默认 30s,ACCESS_CHECK_INTERVAL 环境变量可固定覆盖
|
||||
INTERVAL_FIXED="${ACCESS_CHECK_INTERVAL:-}"
|
||||
|
||||
mkdir -p "$RUNDIR"
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
old_pid="$(cat "$PID_FILE" 2>/dev/null)"
|
||||
if [ -n "$old_pid" ] && [ -d "/proc/$old_pid" ] && \
|
||||
tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null | grep -q 'access_check_daemon\.sh'; then
|
||||
exit 0
|
||||
fi
|
||||
rm -rf "$LOCK_DIR" "$PID_FILE" >/dev/null 2>&1
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$LOCK_DIR" "$PID_FILE"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'cleanup; exit 0' INT TERM
|
||||
|
||||
echo "$$" >"$PID_FILE"
|
||||
|
||||
while :; do
|
||||
[ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ] || break
|
||||
/usr/share/clashoo/net/access_check_cache.sh >/dev/null 2>&1 || true
|
||||
if [ -n "$INTERVAL_FIXED" ]; then
|
||||
sleep "$INTERVAL_FIXED"
|
||||
else
|
||||
sleep 30
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/bin/sh
|
||||
|
||||
APPLY=0
|
||||
TIMEOUT="${CLASHOO_DNS_TEST_TIMEOUT:-2}"
|
||||
START_TS="$(date +%s 2>/dev/null || echo 0)"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--apply) APPLY=1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
json_pair() {
|
||||
printf '"%s":"%s"' "$1" "$(json_escape "$2")"
|
||||
}
|
||||
|
||||
is_ipv4() {
|
||||
echo "$1" | grep -Eq '^[0-9]+(\.[0-9]+){3}$'
|
||||
}
|
||||
|
||||
append_unique() {
|
||||
list="$1"
|
||||
item="$2"
|
||||
[ -n "$item" ] || { printf '%s' "$list"; return; }
|
||||
case " $list " in
|
||||
*" $item "*) printf '%s' "$list" ;;
|
||||
*) printf '%s %s' "$list" "$item" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
current_default_nameservers() {
|
||||
uci -q get clashoo.config.default_nameserver 2>/dev/null || true
|
||||
}
|
||||
|
||||
current_dns_servers() {
|
||||
uci -q show clashoo 2>/dev/null | awk -F= '
|
||||
function clean(v) {
|
||||
gsub(/^'\''|'\''$/, "", v);
|
||||
return v;
|
||||
}
|
||||
/^clashoo\.@dnsservers\[[0-9]+\]\./ {
|
||||
line=$1;
|
||||
val=clean($2);
|
||||
sub(/^clashoo\.@dnsservers\[/, "", line);
|
||||
idx=line;
|
||||
sub(/\].*$/, "", idx);
|
||||
opt=line;
|
||||
sub(/^[0-9]+\]\./, "", opt);
|
||||
data[idx, opt]=val;
|
||||
seen[idx]=1;
|
||||
}
|
||||
END {
|
||||
for (idx in seen) {
|
||||
if (data[idx, "enabled"] == "0") continue;
|
||||
role=data[idx, "ser_type"];
|
||||
addr=data[idx, "ser_address"];
|
||||
proto=data[idx, "protocol"];
|
||||
port=data[idx, "ser_port"];
|
||||
if (addr == "") continue;
|
||||
if (proto != "" && proto != "none" && addr !~ /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//) {
|
||||
addr=proto addr;
|
||||
if (port != "" && addr !~ /:[0-9]+$/) addr=addr ":" port;
|
||||
}
|
||||
print role "|" addr;
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
servers_for_role() {
|
||||
role="$1"
|
||||
current_dns_servers | awk -F'|' -v role="$role" '$1 == role { print $2 }'
|
||||
}
|
||||
|
||||
candidate_host_port() {
|
||||
uri="$1"
|
||||
default_port="$2"
|
||||
hostport="${uri#*://}"
|
||||
hostport="${hostport%%/*}"
|
||||
host="${hostport%%:*}"
|
||||
port="${hostport##*:}"
|
||||
[ "$port" != "$hostport" ] || port="$default_port"
|
||||
printf '%s %s\n' "$host" "$port"
|
||||
}
|
||||
|
||||
probe_tls() {
|
||||
uri="$1"
|
||||
set -- $(candidate_host_port "$uri" 853)
|
||||
host="$1"
|
||||
port="$2"
|
||||
command -v nc >/dev/null 2>&1 || return 1
|
||||
nc -w "$TIMEOUT" "$host" "$port" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
probe_ip_dns() {
|
||||
ip="$1"
|
||||
domain="$2"
|
||||
command -v nslookup >/dev/null 2>&1 || return 1
|
||||
nslookup "$domain" "$ip" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
probe_latency_ms() {
|
||||
candidate="$1"
|
||||
domain="$2"
|
||||
if [ "${candidate#https://}" != "$candidate" ]; then
|
||||
if printf '%s' "$candidate" | grep -q '?'; then
|
||||
q="$candidate&name=$domain&type=A"
|
||||
else
|
||||
q="$candidate?name=$domain&type=A"
|
||||
fi
|
||||
command -v curl >/dev/null 2>&1 || return 1
|
||||
t="$(curl -fsS --connect-timeout "$TIMEOUT" --max-time "$TIMEOUT" \
|
||||
-H 'accept: application/dns-json' -o /dev/null -w '%{time_total}' "$q" 2>/dev/null)" || return 1
|
||||
printf '%s' "$t" | awk '{ms=$1*1000; if (ms<1) ms=1; printf "%d", ms}'
|
||||
elif [ "${candidate#tls://}" != "$candidate" ]; then
|
||||
probe_tls "$candidate" || return 1
|
||||
printf '900'
|
||||
elif [ "${candidate#udp://}" != "$candidate" ]; then
|
||||
probe_ip_dns "${candidate#udp://}" "$domain" || return 1
|
||||
printf '950'
|
||||
elif [ "${candidate#tcp://}" != "$candidate" ]; then
|
||||
probe_ip_dns "${candidate#tcp://}" "$domain" || return 1
|
||||
printf '950'
|
||||
else
|
||||
is_ipv4 "$candidate" && probe_ip_dns "$candidate" "$domain" || return 1
|
||||
printf '950'
|
||||
fi
|
||||
}
|
||||
|
||||
select_best() {
|
||||
list="$1"
|
||||
domain="$2"
|
||||
fallback="$3"
|
||||
best=""
|
||||
best_score=999999
|
||||
order=0
|
||||
for candidate in $list; do
|
||||
order=$((order + 1))
|
||||
ms="$(probe_latency_ms "$candidate" "$domain")" || { FAILED_COUNT=$((FAILED_COUNT + 1)); continue; }
|
||||
score=$((ms * 100 + order))
|
||||
if [ "$score" -lt "$best_score" ]; then
|
||||
best="$candidate"
|
||||
best_score="$score"
|
||||
fi
|
||||
done
|
||||
[ -n "$best" ] || best="$fallback"
|
||||
printf '%s' "$best"
|
||||
}
|
||||
|
||||
add_dnsserver() {
|
||||
role="$1"
|
||||
addr="$2"
|
||||
proto="$3"
|
||||
port="$4"
|
||||
sec="$(uci -q add clashoo dnsservers)" || return 1
|
||||
[ -n "$sec" ] || return 1
|
||||
uci -q set clashoo."$sec".enabled='1' || return 1
|
||||
uci -q set clashoo."$sec".ser_type="$role" || return 1
|
||||
uci -q set clashoo."$sec".ser_address="$addr" || return 1
|
||||
uci -q set clashoo."$sec".protocol="$proto" || return 1
|
||||
if [ -n "$port" ]; then
|
||||
uci -q set clashoo."$sec".ser_port="$port" || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
set_default_nameserver() {
|
||||
uci -q delete clashoo.config.default_nameserver >/dev/null 2>&1 || true
|
||||
uci -q add_list clashoo.config.default_nameserver="$1" || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_result() {
|
||||
uci -q set clashoo.config.enable_dns='1' || { uci -q revert clashoo; return 1; }
|
||||
set_default_nameserver "$BOOTSTRAP" || { uci -q revert clashoo; return 1; }
|
||||
while uci -q delete clashoo.@dnsservers[0] >/dev/null 2>&1; do :; done
|
||||
add_dnsserver 'nameserver' "$NAMESERVER" 'none' '' || { uci -q revert clashoo; return 1; }
|
||||
add_dnsserver 'direct-nameserver' "$BOOTSTRAP" 'udp://' '' || { uci -q revert clashoo; return 1; }
|
||||
add_dnsserver 'proxy-server-nameserver' "$PROXY_NS" 'none' '' || { uci -q revert clashoo; return 1; }
|
||||
add_dnsserver 'fallback' "$FALLBACK_NS" 'none' '' || { uci -q revert clashoo; return 1; }
|
||||
uci -q commit clashoo || { uci -q revert clashoo; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
is_domestic_dns() {
|
||||
printf '%s' "$1" | grep -qE 'alidns|doh\.pub|dnspod|223\.5\.5\.5|223\.6\.6\.6|119\.29\.29\.29|119\.28\.28\.28|114\.114|180\.184|onedns'
|
||||
}
|
||||
is_overseas_dns() {
|
||||
printf '%s' "$1" | grep -qE 'cloudflare|1\.1\.1\.1|1\.0\.0\.1|one\.one\.one\.one|dns\.google|8\.8\.8\.8|8\.8\.4\.4|9\.9\.9\.9|quad9|opendns|208\.67\.'
|
||||
}
|
||||
|
||||
FAILED_COUNT=0
|
||||
BOOTSTRAP_CANDIDATES="223.5.5.5 119.29.29.29 180.184.1.1"
|
||||
DOMESTIC_CANDIDATES="https://dns.alidns.com/dns-query https://doh.pub/dns-query udp://223.5.5.5 udp://119.29.29.29"
|
||||
FALLBACK_CANDIDATES="https://cloudflare-dns.com/dns-query https://dns.google/dns-query tls://1.1.1.1:853"
|
||||
|
||||
for ns in $(current_default_nameservers); do
|
||||
is_ipv4 "$ns" && BOOTSTRAP_CANDIDATES="$(append_unique "$BOOTSTRAP_CANDIDATES" "$ns")"
|
||||
done
|
||||
for ns in $(servers_for_role 'nameserver') $(servers_for_role 'direct-nameserver'); do
|
||||
is_domestic_dns "$ns" && DOMESTIC_CANDIDATES="$(append_unique "$DOMESTIC_CANDIDATES" "$ns")"
|
||||
done
|
||||
for ns in $(servers_for_role 'fallback'); do
|
||||
is_overseas_dns "$ns" && FALLBACK_CANDIDATES="$(append_unique "$FALLBACK_CANDIDATES" "$ns")"
|
||||
done
|
||||
|
||||
BOOTSTRAP="$(select_best "$BOOTSTRAP_CANDIDATES" www.baidu.com 223.5.5.5)"
|
||||
NAMESERVER="$(select_best "$DOMESTIC_CANDIDATES" www.baidu.com https://dns.alidns.com/dns-query)"
|
||||
PROXY_NS="tls://1.1.1.1:853"
|
||||
FALLBACK_NS="$(select_best "$FALLBACK_CANDIDATES" www.google.com https://cloudflare-dns.com/dns-query)"
|
||||
|
||||
APPLIED=false
|
||||
if [ "$APPLY" = "1" ]; then
|
||||
if apply_result; then
|
||||
APPLIED=true
|
||||
else
|
||||
printf '{"success":false,"error":"apply_failed","message":"DNS 自动配置写入失败"}\n'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
END_TS="$(date +%s 2>/dev/null || echo "$START_TS")"
|
||||
ELAPSED_MS=$(( (END_TS - START_TS) * 1000 ))
|
||||
|
||||
printf '{'
|
||||
printf '"success":true,'
|
||||
printf '"applied":%s,' "$APPLIED"
|
||||
printf '"restarted":false,'
|
||||
json_pair bootstrap "$BOOTSTRAP"; printf ','
|
||||
json_pair nameserver "$NAMESERVER"; printf ','
|
||||
json_pair direct_nameserver "$BOOTSTRAP"; printf ','
|
||||
json_pair proxy_nameserver "$PROXY_NS"; printf ','
|
||||
json_pair fallback "$FALLBACK_NS"; printf ','
|
||||
printf '"failed_count":%s,' "$FAILED_COUNT"
|
||||
printf '"elapsed_ms":%s' "$ELAPSED_MS"
|
||||
printf '}\n'
|
||||
Executable
+898
@@ -0,0 +1,898 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
NFT_DIR="/var/run/clash"
|
||||
SETS_RULES="${NFT_DIR}/fw4_sets.nft"
|
||||
DSTNAT_RULES="${NFT_DIR}/fw4_dstnat.nft"
|
||||
MANGLE_RULES="${NFT_DIR}/fw4_mangle.nft"
|
||||
OUTPUT_RULES="${NFT_DIR}/fw4_output.nft"
|
||||
BUILTIN_NFT_DIR="/usr/share/clashoo/nftables"
|
||||
GEOIP_CN_NFT="${BUILTIN_NFT_DIR}/geoip_cn.nft"
|
||||
GEOIP6_CN_NFT="${BUILTIN_NFT_DIR}/geoip6_cn.nft"
|
||||
LOCAL_OUTPUT_TABLE="clashoo_local"
|
||||
QUIC_BLOCK_TABLE="clashoo_quic"
|
||||
# PROXY_FWMARK: inbound TPROXY mark, ip rule -> table PROXY_ROUTE_TABLE -> lo.
|
||||
# CORE_ROUTING_MARK: mihomo outbound SO_MARK (= routing-mark in config).
|
||||
# Must differ: otherwise mihomo egress is pulled to lo -> unreachable.
|
||||
# When changing CORE_ROUTING_MARK, also update yum_change.sh routing_mark_dec.
|
||||
PROXY_FWMARK="0x162"
|
||||
PROXY_ROUTE_TABLE="0x162"
|
||||
CORE_ROUTING_MARK="0x1a0a" # = 6666
|
||||
ACL_BYPASS_FWMARK="0x163"
|
||||
SINGBOX_BYPASS_FWMARK="0x2024"
|
||||
ACL_BYPASS_PREF="8998"
|
||||
DNSMASQ_BYPASS_PREF="8999"
|
||||
|
||||
uci_get() {
|
||||
uci -q get "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
bool_enabled() {
|
||||
case "$1" in
|
||||
1|true|TRUE|yes|on) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
acl_bool() {
|
||||
[ -n "$1" ] || return 0
|
||||
bool_enabled "$1"
|
||||
}
|
||||
|
||||
tun_available() {
|
||||
ip tuntap add mode tun name cotuntest >/dev/null 2>&1 || return 1
|
||||
ip link del cotuntest >/dev/null 2>&1 || true
|
||||
return 0
|
||||
}
|
||||
|
||||
config_redir_port() {
|
||||
uci_get clashoo.config.redir_port
|
||||
}
|
||||
|
||||
config_tproxy_port() {
|
||||
local port
|
||||
port="$(uci_get clashoo.config.tproxy_port)"
|
||||
if [ -n "$port" ]; then
|
||||
printf '%s\n' "$port"
|
||||
else
|
||||
config_redir_port
|
||||
fi
|
||||
}
|
||||
|
||||
config_tcp_mode() {
|
||||
uci_get clashoo.config.tcp_mode
|
||||
}
|
||||
|
||||
config_udp_mode() {
|
||||
uci_get clashoo.config.udp_mode
|
||||
}
|
||||
|
||||
config_access_control() {
|
||||
[ "$(uci_get clashoo.config.acl_migrated)" = "1" ] && {
|
||||
printf '0\n'
|
||||
return
|
||||
}
|
||||
uci_get clashoo.config.access_control
|
||||
}
|
||||
|
||||
acl_sections() {
|
||||
uci -q show clashoo 2>/dev/null | sed -n 's/^clashoo\.\([^.=]*\)=lan_acl$/\1/p'
|
||||
}
|
||||
|
||||
acl_list() {
|
||||
uci_get "$1"
|
||||
}
|
||||
|
||||
acl_sections_catchall_last() {
|
||||
local section
|
||||
for section in $(acl_sections); do
|
||||
[ -z "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] || printf '%s\n' "$section"
|
||||
done
|
||||
for section in $(acl_sections); do
|
||||
[ -n "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] || printf '%s\n' "$section"
|
||||
done
|
||||
}
|
||||
|
||||
grouped_acl_enabled() {
|
||||
local section
|
||||
[ "$(uci_get clashoo.config.acl_migrated)" = "1" ] || return 1
|
||||
for section in $(acl_sections); do
|
||||
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
acl_has_catchall() {
|
||||
local section
|
||||
for section in $(acl_sections); do
|
||||
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue
|
||||
[ -z "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
lan_acl_enabled() {
|
||||
grouped_acl_enabled && return 0
|
||||
case "$(config_access_control)" in
|
||||
1|2) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
config_enable_dns() {
|
||||
uci_get clashoo.config.enable_dns
|
||||
}
|
||||
|
||||
config_dns_port() {
|
||||
local port=""
|
||||
if [ "$(uci_get clashoo.config.core_type)" != "singbox" ]; then
|
||||
port="$(sed -n 's/^[[:space:]]*listen:.*:[[:space:]]*['"'"'"]*\([0-9]\{1,\}\)['"'"'"]*[[:space:]]*$/\1/p' /etc/clashoo/config.yaml 2>/dev/null | head -n1)"
|
||||
fi
|
||||
[ -n "$port" ] || port="$(uci_get clashoo.config.listen_port)"
|
||||
printf '%s' "$port" | grep -Eq '^[0-9]+$' || port=1053
|
||||
printf '%s\n' "$port"
|
||||
}
|
||||
|
||||
lan_dns_split_enabled() {
|
||||
lan_acl_enabled || return 1
|
||||
bool_enabled "$(config_enable_dns)" || return 1
|
||||
bool_enabled "$(config_ipv4_dns_hijack)" || bool_enabled "$(config_ipv6_dns_hijack)"
|
||||
}
|
||||
|
||||
tun_acl_enabled() {
|
||||
lan_acl_enabled || return 1
|
||||
[ "$(config_tcp_mode)" = "tun" ] || [ "$(config_udp_mode)" = "tun" ]
|
||||
}
|
||||
|
||||
config_ipv4_dns_hijack() {
|
||||
uci_get clashoo.config.ipv4_dns_hijack
|
||||
}
|
||||
|
||||
config_ipv6_dns_hijack() {
|
||||
uci_get clashoo.config.ipv6_dns_hijack
|
||||
}
|
||||
|
||||
config_ipv4_proxy() {
|
||||
uci_get clashoo.config.ipv4_proxy
|
||||
}
|
||||
|
||||
config_ipv6_proxy() {
|
||||
uci_get clashoo.config.ipv6_proxy
|
||||
}
|
||||
|
||||
singbox_tun_active() {
|
||||
[ "$(uci_get clashoo.config.core_type)" = "singbox" ] || return 1
|
||||
[ "$(config_tcp_mode)" = "tun" ] || [ "$(config_udp_mode)" = "tun" ]
|
||||
}
|
||||
|
||||
config_bypass_china() {
|
||||
uci_get clashoo.config.bypass_china
|
||||
}
|
||||
|
||||
config_bypass_china_ipv6() {
|
||||
local value
|
||||
value="$(uci_get clashoo.config.bypass_china_ipv6)"
|
||||
if [ -n "$value" ]; then
|
||||
printf '%s\n' "$value"
|
||||
else
|
||||
config_bypass_china
|
||||
fi
|
||||
}
|
||||
|
||||
config_block_quic() {
|
||||
uci_get clashoo.config.block_quic
|
||||
}
|
||||
|
||||
config_bypass_port_mode() {
|
||||
uci_get clashoo.config.bypass_port_mode
|
||||
}
|
||||
|
||||
config_bypass_port_custom() {
|
||||
uci_get clashoo.config.bypass_port_custom
|
||||
}
|
||||
|
||||
config_legacy_bypass_port() {
|
||||
uci_list clashoo.config.bypass_port
|
||||
}
|
||||
|
||||
config_proxy_tcp_dport() {
|
||||
local mode custom legacy value
|
||||
mode="$(config_bypass_port_mode)"
|
||||
custom="$(config_bypass_port_custom)"
|
||||
legacy="$(uci_get clashoo.config.proxy_tcp_dport)"
|
||||
[ -z "$legacy" ] && legacy="$(config_legacy_bypass_port)"
|
||||
|
||||
case "$mode" in
|
||||
all)
|
||||
# 空值在 render_port_match 中表示该协议的全部端口
|
||||
printf '%s\n' ''
|
||||
;;
|
||||
common)
|
||||
printf '%s\n' '22,53,80,443,8080,8443'
|
||||
;;
|
||||
custom)
|
||||
value="$custom"
|
||||
[ -z "$value" ] && value="$legacy"
|
||||
printf '%s\n' "$value"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$legacy"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
config_proxy_udp_dport() {
|
||||
local mode custom legacy value
|
||||
mode="$(config_bypass_port_mode)"
|
||||
custom="$(config_bypass_port_custom)"
|
||||
legacy="$(uci_get clashoo.config.proxy_udp_dport)"
|
||||
[ -z "$legacy" ] && legacy="$(config_legacy_bypass_port)"
|
||||
|
||||
case "$mode" in
|
||||
all)
|
||||
printf '%s\n' ''
|
||||
;;
|
||||
common)
|
||||
printf '%s\n' '22,53,80,443,8080,8443'
|
||||
;;
|
||||
custom)
|
||||
value="$custom"
|
||||
[ -z "$value" ] && value="$legacy"
|
||||
printf '%s\n' "$value"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$legacy"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
config_bypass_dscp() {
|
||||
uci_list clashoo.config.bypass_dscp
|
||||
}
|
||||
|
||||
config_bypass_fwmark() {
|
||||
uci_list clashoo.config.bypass_fwmark
|
||||
}
|
||||
|
||||
config_fake_ip_range6() {
|
||||
local value
|
||||
value="$(uci_get clashoo.config.fake_ip_range6)"
|
||||
[ -n "$value" ] || value="fc00::/18"
|
||||
[ "$(uci_get clashoo.config.enhanced_mode)" = "redir-host" ] && return 0
|
||||
bool_enabled "$(uci_get clashoo.config.enable_ipv6)" || return 0
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
config_fake_ip_range() {
|
||||
local value
|
||||
value="$(uci_get clashoo.config.fake_ip_range)"
|
||||
[ -n "$value" ] && {
|
||||
printf '%s\n' "$value"
|
||||
return
|
||||
}
|
||||
printf '198.18.0.1/16\n'
|
||||
}
|
||||
|
||||
uci_list() {
|
||||
local key="$1"
|
||||
uci -q show "$key" 2>/dev/null | sed -n "s/^${key}=//p" | sed "s/'//g"
|
||||
}
|
||||
|
||||
ensure_firewall_include() {
|
||||
local name="$1"
|
||||
local path="$2"
|
||||
local chain="${3:-}"
|
||||
local position="${4:-chain-pre}"
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set firewall.${name}=include
|
||||
set firewall.${name}.type='nftables'
|
||||
set firewall.${name}.path='${path}'
|
||||
set firewall.${name}.position='${position}'
|
||||
$( [ -n "$chain" ] && printf "set firewall.%s.chain='%s'\n" "$name" "$chain" )
|
||||
commit firewall
|
||||
EOF
|
||||
}
|
||||
|
||||
remove_firewall_include() {
|
||||
local name="$1"
|
||||
uci -q delete firewall."${name}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
render_common_returns() {
|
||||
local fake6
|
||||
fake6="$(config_fake_ip_range6)"
|
||||
printf '%s\n' 'meta nfproto ipv4 ip daddr { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 } return'
|
||||
if [ -n "$fake6" ]; then
|
||||
printf 'meta nfproto ipv6 ip6 daddr { ::1/128, fc00::/7, fe80::/10, ff00::/8 } ip6 daddr != %s return\n' "$fake6"
|
||||
else
|
||||
printf '%s\n' 'meta nfproto ipv6 ip6 daddr { ::1/128, fc00::/7, fe80::/10, ff00::/8 } return'
|
||||
fi
|
||||
}
|
||||
|
||||
render_ip_elements() {
|
||||
local list="$1"
|
||||
local first=1 entry
|
||||
for entry in $list; do
|
||||
if [ "$first" -eq 0 ]; then
|
||||
printf ', '
|
||||
fi
|
||||
printf '%s' "$entry"
|
||||
first=0
|
||||
done
|
||||
}
|
||||
|
||||
render_token_elements() {
|
||||
printf '%s\n' "$1" | tr ',\t' ' ' | awk '
|
||||
BEGIN { first = 1 }
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i == "")
|
||||
continue
|
||||
if (!first)
|
||||
printf ", "
|
||||
printf "%s", $i
|
||||
first = 0
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
detect_coexist_fwmarks() {
|
||||
# Merge co-installed proxy plugin fwmarks into bypass to avoid intercepting
|
||||
# their traffic. Only when init.d exists (passwall:0x1 passwall2:0xff
|
||||
# nikki:tproxy 0x80/tun 0x81 mask 0xff).
|
||||
local marks=""
|
||||
[ -x /etc/init.d/passwall ] && marks="$marks 0x1"
|
||||
[ -x /etc/init.d/passwall2 ] && marks="$marks 0xff"
|
||||
if [ -x /etc/init.d/nikki ]; then
|
||||
local nm
|
||||
nm="$(uci -q get nikki.routing.tproxy_fw_mark) $(uci -q get nikki.routing.tun_fw_mark)"
|
||||
[ -z "$(echo "$nm" | tr -d ' ')" ] && nm="0x80 0x81"
|
||||
marks="$marks $nm"
|
||||
fi
|
||||
printf '%s\n' "$marks"
|
||||
}
|
||||
|
||||
merge_fwmark_tokens() {
|
||||
# Always bypass PROXY_FWMARK (inbound TPROXY) and CORE_ROUTING_MARK (core egress)
|
||||
# so nft never traps them. Also merge co-installed plugin fwmarks for coexistence.
|
||||
local coexist
|
||||
coexist="$(detect_coexist_fwmarks)"
|
||||
printf '%s %s %s %s\n' "$1" "$PROXY_FWMARK" "$CORE_ROUTING_MARK" "$coexist" | tr ',\t' ' ' | awk '
|
||||
BEGIN { first = 1 }
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i == "" || seen[$i]++)
|
||||
continue
|
||||
if (!first)
|
||||
printf ", "
|
||||
printf "%s", $i
|
||||
first = 0
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
render_port_match() {
|
||||
local proto="$1"
|
||||
local ports="$2"
|
||||
local port_elements
|
||||
|
||||
port_elements="$(render_token_elements "$ports")"
|
||||
if [ -n "$port_elements" ]; then
|
||||
printf 'meta l4proto %s %s dport { %s }' "$proto" "$proto" "$port_elements"
|
||||
else
|
||||
printf 'meta l4proto %s' "$proto"
|
||||
fi
|
||||
}
|
||||
|
||||
render_acl_dns_rules() {
|
||||
local redirect_port="$1" section ipv4 ipv6 mac ipv4_elements ipv6_elements mac_elements action
|
||||
for section in $(acl_sections_catchall_last); do
|
||||
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue
|
||||
if acl_bool "$(uci_get "clashoo.${section}.dns")"; then
|
||||
action="counter redirect to :${redirect_port}"
|
||||
elif singbox_tun_active; then
|
||||
action="meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return"
|
||||
else
|
||||
action="counter return"
|
||||
fi
|
||||
ipv4="$(acl_list "clashoo.${section}.ip")"
|
||||
ipv6="$(acl_list "clashoo.${section}.ip6")"
|
||||
mac="$(acl_list "clashoo.${section}.mac")"
|
||||
ipv4_elements="$(render_ip_elements "$ipv4")"
|
||||
ipv6_elements="$(render_ip_elements "$ipv6")"
|
||||
mac_elements="$(render_ip_elements "$mac")"
|
||||
|
||||
if [ -z "$ipv4_elements$ipv6_elements$mac_elements" ]; then
|
||||
bool_enabled "$(config_ipv4_dns_hijack)" && printf 'meta nfproto ipv4 meta l4proto { tcp, udp } th dport 53 %s\n' "$action"
|
||||
bool_enabled "$(config_ipv6_dns_hijack)" && printf 'meta nfproto ipv6 meta l4proto { tcp, udp } th dport 53 %s\n' "$action"
|
||||
continue
|
||||
fi
|
||||
if [ -n "$ipv4_elements" ] && bool_enabled "$(config_ipv4_dns_hijack)"; then
|
||||
printf 'meta nfproto ipv4 ip saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$ipv4_elements" "$action"
|
||||
fi
|
||||
if [ -n "$ipv6_elements" ] && bool_enabled "$(config_ipv6_dns_hijack)"; then
|
||||
printf 'meta nfproto ipv6 ip6 saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$ipv6_elements" "$action"
|
||||
fi
|
||||
if [ -n "$mac_elements" ]; then
|
||||
bool_enabled "$(config_ipv4_dns_hijack)" && printf 'meta nfproto ipv4 ether saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$mac_elements" "$action"
|
||||
bool_enabled "$(config_ipv6_dns_hijack)" && printf 'meta nfproto ipv6 ether saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$mac_elements" "$action"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
render_acl_proxy_rules() {
|
||||
local mode="$1" proto="$2" port_match="$3" target_port="$4"
|
||||
local section ipv4 ipv6 mac ipv4_elements ipv6_elements mac_elements enabled action4 action6
|
||||
|
||||
for section in $(acl_sections_catchall_last); do
|
||||
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue
|
||||
enabled="$(uci_get "clashoo.${section}.proxy")"
|
||||
case "$mode" in
|
||||
redirect)
|
||||
if acl_bool "$enabled"; then
|
||||
if singbox_tun_active; then
|
||||
action4="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} counter redirect to :${target_port}"
|
||||
else
|
||||
action4="${port_match} counter redirect to :${target_port}"
|
||||
fi
|
||||
action6="$action4"
|
||||
elif singbox_tun_active; then
|
||||
action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return"
|
||||
action6="$action4"
|
||||
else
|
||||
action4="meta l4proto ${proto} counter return"
|
||||
action6="$action4"
|
||||
fi
|
||||
;;
|
||||
tproxy)
|
||||
if acl_bool "$enabled"; then
|
||||
if singbox_tun_active; then
|
||||
action4="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} tproxy ip to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
|
||||
action6="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} tproxy ip6 to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
|
||||
else
|
||||
action4="${port_match} tproxy ip to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
|
||||
action6="${port_match} tproxy ip6 to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
|
||||
fi
|
||||
elif singbox_tun_active; then
|
||||
action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return"
|
||||
action6="$action4"
|
||||
else
|
||||
action4="meta l4proto ${proto} counter return"
|
||||
action6="$action4"
|
||||
fi
|
||||
;;
|
||||
tun)
|
||||
if acl_bool "$enabled"; then
|
||||
action4="meta l4proto ${proto} counter return"
|
||||
elif singbox_tun_active; then
|
||||
action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter accept"
|
||||
else
|
||||
action4="meta l4proto ${proto} meta mark set ${ACL_BYPASS_FWMARK} counter accept"
|
||||
fi
|
||||
action6="$action4"
|
||||
;;
|
||||
esac
|
||||
|
||||
ipv4="$(acl_list "clashoo.${section}.ip")"
|
||||
ipv6="$(acl_list "clashoo.${section}.ip6")"
|
||||
mac="$(acl_list "clashoo.${section}.mac")"
|
||||
ipv4_elements="$(render_ip_elements "$ipv4")"
|
||||
ipv6_elements="$(render_ip_elements "$ipv6")"
|
||||
mac_elements="$(render_ip_elements "$mac")"
|
||||
|
||||
if [ -z "$ipv4_elements$ipv6_elements$mac_elements" ]; then
|
||||
bool_enabled "$(config_ipv4_proxy)" && printf 'meta nfproto ipv4 %s\n' "$action4"
|
||||
bool_enabled "$(config_ipv6_proxy)" && printf 'meta nfproto ipv6 %s\n' "$action6"
|
||||
continue
|
||||
fi
|
||||
if [ -n "$ipv4_elements" ] && bool_enabled "$(config_ipv4_proxy)"; then
|
||||
printf 'meta nfproto ipv4 ip saddr { %s } %s\n' "$ipv4_elements" "$action4"
|
||||
fi
|
||||
if [ -n "$ipv6_elements" ] && bool_enabled "$(config_ipv6_proxy)"; then
|
||||
printf 'meta nfproto ipv6 ip6 saddr { %s } %s\n' "$ipv6_elements" "$action6"
|
||||
fi
|
||||
if [ -n "$mac_elements" ]; then
|
||||
bool_enabled "$(config_ipv4_proxy)" && printf 'meta nfproto ipv4 ether saddr { %s } %s\n' "$mac_elements" "$action4"
|
||||
bool_enabled "$(config_ipv6_proxy)" && printf 'meta nfproto ipv6 ether saddr { %s } %s\n' "$mac_elements" "$action6"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Split proxied clients to the core DNS while ACL-bypassed clients use dnsmasq.
|
||||
# Gated on UCI IPv4/IPv6 toggles.
|
||||
# return 0: no output when both off, avoid set -e false-positive.
|
||||
render_dns_hijack() {
|
||||
local access_control="$1" redirect_port=53
|
||||
if lan_dns_split_enabled; then
|
||||
redirect_port="$(config_dns_port)"
|
||||
fi
|
||||
if grouped_acl_enabled; then
|
||||
render_acl_dns_rules "$redirect_port"
|
||||
acl_has_catchall && return 0
|
||||
else
|
||||
[ "$access_control" = "1" ] && printf '%s\n' 'ip saddr != @clash_proxy_lan return'
|
||||
[ "$access_control" = "2" ] && printf '%s\n' 'ip saddr @clash_reject_lan return'
|
||||
fi
|
||||
bool_enabled "$(config_ipv4_dns_hijack)" && \
|
||||
printf '%s\n' "meta nfproto ipv4 meta l4proto { tcp, udp } th dport 53 counter redirect to :${redirect_port}"
|
||||
bool_enabled "$(config_ipv6_dns_hijack)" && \
|
||||
printf '%s\n' "meta nfproto ipv6 meta l4proto { tcp, udp } th dport 53 counter redirect to :${redirect_port}"
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_local_output_rule() {
|
||||
local redir_port fake_ip_range tcp_mode bypass_fwmark bypass_china
|
||||
local fwmark_elements fwmark_rule china_set china_rule dnsmasq_rule dnsmasq_uid
|
||||
redir_port="$(config_redir_port)"
|
||||
fake_ip_range="$(config_fake_ip_range)"
|
||||
tcp_mode="$(config_tcp_mode)"
|
||||
bypass_fwmark="$(config_bypass_fwmark)"
|
||||
bypass_china="$(config_bypass_china)"
|
||||
|
||||
# Keep local-output redirect usable when tun mode is selected but tun
|
||||
# device is unavailable on the system.
|
||||
if [ "$tcp_mode" = "tun" ] && ! tun_available; then
|
||||
tcp_mode="redirect"
|
||||
fi
|
||||
|
||||
nft delete table ip ${LOCAL_OUTPUT_TABLE} >/dev/null 2>&1 || true
|
||||
|
||||
# Only apply local output redirect when tcp_mode is redirect
|
||||
[ "$tcp_mode" != "redirect" ] && return 0
|
||||
|
||||
# bypass mihomo's own marks (prevent self-hijack -> dead loop)
|
||||
fwmark_rule=""
|
||||
fwmark_elements="$(merge_fwmark_tokens "$bypass_fwmark")"
|
||||
[ -n "$fwmark_elements" ] && fwmark_rule="meta mark { ${fwmark_elements} } return"
|
||||
|
||||
# CN IP bypass (uses clashoo_china set from geoip_cn.nft)
|
||||
china_set=""
|
||||
china_rule=""
|
||||
if bool_enabled "$bypass_china" && [ -s "$GEOIP_CN_NFT" ]; then
|
||||
china_set="$(cat "$GEOIP_CN_NFT")"
|
||||
china_rule="ip daddr @clashoo_china return"
|
||||
fi
|
||||
|
||||
dnsmasq_rule=""
|
||||
if lan_dns_split_enabled; then
|
||||
dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)"
|
||||
printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$' && dnsmasq_rule="meta skuid ${dnsmasq_uid} return"
|
||||
fi
|
||||
|
||||
nft -f - <<EOF
|
||||
table ip ${LOCAL_OUTPUT_TABLE} {
|
||||
set clashoo_localnetwork {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8,
|
||||
169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16,
|
||||
224.0.0.0/4, 240.0.0.0/4 }
|
||||
}
|
||||
${china_set}
|
||||
chain output {
|
||||
type nat hook output priority dstnat; policy accept;
|
||||
${dnsmasq_rule}
|
||||
${fwmark_rule}
|
||||
ip daddr @clashoo_localnetwork return
|
||||
${china_rule}
|
||||
ip daddr ${fake_ip_range} tcp dport != 53 redirect to :${redir_port}
|
||||
meta l4proto tcp redirect to :${redir_port}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
remove_local_output_rule() {
|
||||
nft delete table ip ${LOCAL_OUTPUT_TABLE} >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
remove_acl_bypass_rules() {
|
||||
local dnsmasq_uid
|
||||
ip rule del pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
|
||||
ip -6 rule del pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
|
||||
dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)"
|
||||
if printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$'; then
|
||||
ip rule del pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
|
||||
ip -6 rule del pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
apply_acl_bypass_rules() {
|
||||
local dnsmasq_uid
|
||||
remove_acl_bypass_rules
|
||||
tun_acl_enabled || return 0
|
||||
ip rule add pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
|
||||
ip -6 rule add pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
|
||||
dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)"
|
||||
if printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$'; then
|
||||
ip rule add pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
|
||||
ip -6 rule add pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
apply_block_quic_rule() {
|
||||
local fake_ip_range china_set china_rule scope
|
||||
nft delete table inet ${QUIC_BLOCK_TABLE} >/dev/null 2>&1 || true
|
||||
bool_enabled "$(config_block_quic)" || return 0
|
||||
|
||||
fake_ip_range="$(config_fake_ip_range)"
|
||||
china_set=""
|
||||
scope="ip daddr ${fake_ip_range}"
|
||||
if [ "$(uci_get clashoo.config.enhanced_mode)" != "fake-ip" ]; then
|
||||
if bool_enabled "$(config_bypass_china)" && [ -s "$GEOIP_CN_NFT" ]; then
|
||||
china_set="$(cat "$GEOIP_CN_NFT")"
|
||||
china_rule="ip daddr @clashoo_china return"
|
||||
fi
|
||||
scope=""
|
||||
fi
|
||||
|
||||
nft -f - <<EOF
|
||||
table inet ${QUIC_BLOCK_TABLE} {
|
||||
${china_set}
|
||||
chain prerouting {
|
||||
type filter hook prerouting priority mangle - 1; policy accept;
|
||||
${china_rule:-}
|
||||
${scope} udp dport 443 counter reject with icmpx type port-unreachable
|
||||
}
|
||||
|
||||
chain forward {
|
||||
type filter hook forward priority -10; policy accept;
|
||||
${china_rule:-}
|
||||
${scope} udp dport 443 counter reject with icmpx type port-unreachable
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
remove_block_quic_rule() {
|
||||
nft delete table inet ${QUIC_BLOCK_TABLE} >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
write_empty_set() {
|
||||
local set_name="$1"
|
||||
local set_type="$2"
|
||||
|
||||
printf 'set %s {\n\ttype %s;\n\tflags interval;\n\tauto-merge;\n}\n\n' "$set_name" "$set_type"
|
||||
}
|
||||
|
||||
append_set_from_file_or_empty() {
|
||||
local file_path="$1"
|
||||
local set_name="$2"
|
||||
local set_type="$3"
|
||||
|
||||
if [ -s "$file_path" ]; then
|
||||
cat "$file_path"
|
||||
printf '\n'
|
||||
else
|
||||
write_empty_set "$set_name" "$set_type"
|
||||
fi
|
||||
}
|
||||
|
||||
generate_rules() {
|
||||
local redir_port tproxy_port tcp_mode udp_mode access_control fake_ip_range proxy_lan_ips reject_lan_ips
|
||||
local proxy_tcp_dport proxy_udp_dport bypass_dscp bypass_fwmark bypass_china bypass_china_ipv6
|
||||
local acl_catchall=0
|
||||
redir_port="$(config_redir_port)"
|
||||
tproxy_port="$(config_tproxy_port)"
|
||||
tcp_mode="$(config_tcp_mode)"
|
||||
udp_mode="$(config_udp_mode)"
|
||||
access_control="$(config_access_control)"
|
||||
grouped_acl_enabled && acl_has_catchall && acl_catchall=1
|
||||
bypass_china="$(config_bypass_china)"
|
||||
bypass_china_ipv6="$(config_bypass_china_ipv6)"
|
||||
proxy_tcp_dport="$(config_proxy_tcp_dport)"
|
||||
proxy_udp_dport="$(config_proxy_udp_dport)"
|
||||
bypass_dscp="$(config_bypass_dscp)"
|
||||
bypass_fwmark="$(config_bypass_fwmark)"
|
||||
fake_ip_range="$(config_fake_ip_range)"
|
||||
proxy_lan_ips="$(uci_list clashoo.config.proxy_lan_ips)"
|
||||
reject_lan_ips="$(uci_list clashoo.config.reject_lan_ips)"
|
||||
|
||||
# When tun device is unavailable, fall back to non-tun transparent modes
|
||||
# so routing rules still take effect for sing-box redirect/tproxy inbounds.
|
||||
if [ "$tcp_mode" = "tun" ] || [ "$udp_mode" = "tun" ]; then
|
||||
if ! tun_available; then
|
||||
[ "$tcp_mode" = "tun" ] && tcp_mode="redirect"
|
||||
[ "$udp_mode" = "tun" ] && udp_mode="tproxy"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$NFT_DIR"
|
||||
|
||||
# Build optional elements lines (nftables rejects empty elements = {})
|
||||
local proxy_elements reject_elements dscp_elements fwmark_elements
|
||||
proxy_elements="$(render_ip_elements "$proxy_lan_ips")"
|
||||
reject_elements="$(render_ip_elements "$reject_lan_ips")"
|
||||
dscp_elements="$(render_token_elements "$bypass_dscp")"
|
||||
fwmark_elements="$(merge_fwmark_tokens "$bypass_fwmark")"
|
||||
tcp_match="$(render_port_match tcp "$proxy_tcp_dport")"
|
||||
udp_match="$(render_port_match udp "$proxy_udp_dport")"
|
||||
|
||||
{
|
||||
printf 'set clashoo_localnetwork {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n'
|
||||
printf '\telements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 }\n}\n\n'
|
||||
|
||||
append_set_from_file_or_empty "$GEOIP_CN_NFT" clashoo_china ipv4_addr
|
||||
append_set_from_file_or_empty "$GEOIP6_CN_NFT" clashoo_china6 ipv6_addr
|
||||
|
||||
printf 'set clash_proxy_lan {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n'
|
||||
[ -n "$proxy_elements" ] && printf '\telements = { %s }\n' "$proxy_elements"
|
||||
printf '}\n\n'
|
||||
|
||||
printf 'set clash_reject_lan {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n'
|
||||
[ -n "$reject_elements" ] && printf '\telements = { %s }\n' "$reject_elements"
|
||||
printf '}\n\n'
|
||||
|
||||
if grouped_acl_enabled && [ "$tcp_mode" = "tun" ]; then
|
||||
printf 'chain clashoo_acl_tun_tcp {\n'
|
||||
render_acl_proxy_rules tun tcp "$tcp_match" "$tproxy_port"
|
||||
printf '}\n\n'
|
||||
fi
|
||||
if grouped_acl_enabled && [ "$udp_mode" = "tun" ]; then
|
||||
printf 'chain clashoo_acl_tun_udp {\n'
|
||||
render_acl_proxy_rules tun udp "$udp_match" "$tproxy_port"
|
||||
printf '}\n'
|
||||
fi
|
||||
} > "$SETS_RULES"
|
||||
|
||||
: > "$OUTPUT_RULES"
|
||||
|
||||
|
||||
# DNS hijack rules go atop DSTNAT_RULES: must run for all TCP modes
|
||||
# (redirect/tproxy/tun) and BEFORE proxy redirect rules so port 53 is not stolen.
|
||||
render_dns_hijack "$access_control" > "$DSTNAT_RULES"
|
||||
|
||||
|
||||
# TCP rules: redirect mode appends proxy redirect; tproxy/tun skip this chain
|
||||
case "$tcp_mode" in
|
||||
redirect)
|
||||
tcp_match="$(render_port_match tcp "$proxy_tcp_dport")"
|
||||
cat >> "$DSTNAT_RULES" <<EOF
|
||||
$( render_common_returns )
|
||||
$( bool_enabled "$bypass_china_ipv6" && printf '%s\n' 'ip6 daddr @clashoo_china6 return' )
|
||||
$( bool_enabled "$bypass_china" && printf '%s\n' 'ip daddr @clashoo_china return' )
|
||||
$( [ -n "$dscp_elements" ] && printf '%s\n' "ip dscp { ${dscp_elements} } return" )
|
||||
$( [ -n "$dscp_elements" ] && printf '%s\n' "ip6 dscp { ${dscp_elements} } return" )
|
||||
$( [ -n "$fwmark_elements" ] && printf '%s\n' "meta mark { ${fwmark_elements} } return" )
|
||||
$( ! bool_enabled "$(config_ipv4_proxy)" && printf '%s\n' 'meta nfproto ipv4 return' )
|
||||
$( ! bool_enabled "$(config_ipv6_proxy)" && printf '%s\n' 'meta nfproto ipv6 return' )
|
||||
$( grouped_acl_enabled && render_acl_proxy_rules redirect tcp "$tcp_match" "$redir_port" )
|
||||
$( [ "$access_control" = "1" ] && printf '%s\n' 'ip saddr != @clash_proxy_lan return' )
|
||||
$( [ "$access_control" = "2" ] && printf '%s\n' 'ip saddr @clash_reject_lan return' )
|
||||
$( [ "$acl_catchall" -eq 0 ] && { singbox_tun_active && printf '%s ct mark set %s redirect to :%s\n' "$tcp_match" "$SINGBOX_BYPASS_FWMARK" "$redir_port" || printf '%s redirect to :%s\n' "$tcp_match" "$redir_port"; } )
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
|
||||
# UDP rules: tproxy via mangle. TUN uses mangle only for LAN ACL bypass.
|
||||
# Also handle TCP tproxy mode here (both TCP+UDP in mangle).
|
||||
local need_mangle=0 tun_acl=0
|
||||
[ "$tcp_mode" = "tproxy" ] && need_mangle=1
|
||||
[ "$udp_mode" = "tproxy" ] && need_mangle=1
|
||||
if tun_acl_enabled "$access_control"; then
|
||||
need_mangle=1
|
||||
tun_acl=1
|
||||
fi
|
||||
|
||||
if [ "$need_mangle" -eq 1 ]; then
|
||||
tcp_match="$(render_port_match tcp "$proxy_tcp_dport")"
|
||||
udp_match="$(render_port_match udp "$proxy_udp_dport")"
|
||||
{
|
||||
render_common_returns
|
||||
if bool_enabled "$bypass_china_ipv6"; then
|
||||
printf 'meta nfproto ipv6 ip6 daddr @clashoo_china6 return\n'
|
||||
fi
|
||||
if bool_enabled "$bypass_china"; then
|
||||
printf 'ip daddr @clashoo_china return\n'
|
||||
fi
|
||||
if [ -n "$dscp_elements" ]; then
|
||||
printf 'ip dscp { %s } return\n' "$dscp_elements"
|
||||
printf 'ip6 dscp { %s } return\n' "$dscp_elements"
|
||||
fi
|
||||
if [ -n "$fwmark_elements" ]; then
|
||||
printf 'meta mark { %s } return\n' "$fwmark_elements"
|
||||
fi
|
||||
bool_enabled "$(config_ipv4_proxy)" || printf 'meta nfproto ipv4 return\n'
|
||||
bool_enabled "$(config_ipv6_proxy)" || printf 'meta nfproto ipv6 return\n'
|
||||
if grouped_acl_enabled; then
|
||||
[ "$tcp_mode" = "tproxy" ] && render_acl_proxy_rules tproxy tcp "$tcp_match" "$tproxy_port"
|
||||
[ "$tcp_mode" = "tun" ] && printf 'meta l4proto tcp jump clashoo_acl_tun_tcp\n'
|
||||
[ "$udp_mode" = "tproxy" ] && render_acl_proxy_rules tproxy udp "$udp_match" "$tproxy_port"
|
||||
[ "$udp_mode" = "tun" ] && printf 'meta l4proto udp jump clashoo_acl_tun_udp\n'
|
||||
elif [ "$tun_acl" -eq 1 ]; then
|
||||
[ "$access_control" = "1" ] && printf 'ip saddr != @clash_proxy_lan meta mark set %s return\n' "$ACL_BYPASS_FWMARK"
|
||||
[ "$access_control" = "2" ] && printf 'ip saddr @clash_reject_lan meta mark set %s return\n' "$ACL_BYPASS_FWMARK"
|
||||
else
|
||||
[ "$access_control" = "1" ] && printf 'ip saddr != @clash_proxy_lan return\n'
|
||||
[ "$access_control" = "2" ] && printf 'ip saddr @clash_reject_lan return\n'
|
||||
fi
|
||||
if bool_enabled "$(config_block_quic)"; then
|
||||
printf 'meta l4proto udp udp dport 443 return\n'
|
||||
fi
|
||||
if [ "$tcp_mode" = "tproxy" ] && [ "$acl_catchall" -eq 0 ]; then
|
||||
if singbox_tun_active; then
|
||||
printf '%s ct mark set %s tproxy to :%s meta mark set %s accept\n' "$tcp_match" "$SINGBOX_BYPASS_FWMARK" "$tproxy_port" "$PROXY_FWMARK"
|
||||
else
|
||||
printf '%s tproxy to :%s meta mark set %s accept\n' "$tcp_match" "$tproxy_port" "$PROXY_FWMARK"
|
||||
fi
|
||||
fi
|
||||
if [ "$udp_mode" = "tproxy" ] && [ "$acl_catchall" -eq 0 ]; then
|
||||
if singbox_tun_active; then
|
||||
printf '%s ct mark set %s tproxy to :%s meta mark set %s accept\n' "$udp_match" "$SINGBOX_BYPASS_FWMARK" "$tproxy_port" "$PROXY_FWMARK"
|
||||
else
|
||||
printf '%s tproxy to :%s meta mark set %s accept\n' "$udp_match" "$tproxy_port" "$PROXY_FWMARK"
|
||||
fi
|
||||
fi
|
||||
} > "$MANGLE_RULES"
|
||||
else
|
||||
: > "$MANGLE_RULES"
|
||||
fi
|
||||
}
|
||||
|
||||
apply_rules() {
|
||||
generate_rules
|
||||
ensure_firewall_include clash_fw4_sets "$SETS_RULES" '' table-pre
|
||||
ensure_firewall_include clash_fw4_dstnat "$DSTNAT_RULES" dstnat
|
||||
remove_firewall_include clash_fw4_output
|
||||
if [ -s "$MANGLE_RULES" ]; then
|
||||
_route_table_dec="$((PROXY_ROUTE_TABLE))"
|
||||
ensure_firewall_include clash_fw4_mangle "$MANGLE_RULES" mangle_prerouting
|
||||
ip rule show 2>/dev/null | grep -q "fwmark ${PROXY_FWMARK}.*lookup ${_route_table_dec}" ||
|
||||
ip rule add fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip route show table "$PROXY_ROUTE_TABLE" 2>/dev/null | grep -q 'local 0.0.0.0/0 dev lo' ||
|
||||
ip route add local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
if bool_enabled "$(config_ipv6_proxy)"; then
|
||||
ip -6 rule show 2>/dev/null | grep -q "fwmark ${PROXY_FWMARK}.*lookup ${_route_table_dec}" ||
|
||||
ip -6 rule add fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 route show table "$PROXY_ROUTE_TABLE" 2>/dev/null | grep -q 'local default dev lo' ||
|
||||
ip -6 route add local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
else
|
||||
ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
remove_firewall_include clash_fw4_mangle
|
||||
ip rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip route del local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
fi
|
||||
/etc/init.d/firewall restart >/dev/null 2>&1 || /etc/init.d/firewall reload >/dev/null 2>&1 || true
|
||||
apply_acl_bypass_rules
|
||||
apply_local_output_rule
|
||||
apply_block_quic_rule
|
||||
}
|
||||
|
||||
remove_rules() {
|
||||
remove_acl_bypass_rules
|
||||
remove_local_output_rule
|
||||
remove_block_quic_rule
|
||||
remove_firewall_include clash_fw4_sets
|
||||
remove_firewall_include clash_fw4_dstnat
|
||||
remove_firewall_include clash_fw4_output
|
||||
remove_firewall_include clash_fw4_mangle
|
||||
remove_firewall_include clash_fw4_forward
|
||||
uci commit firewall >/dev/null 2>&1 || true
|
||||
rm -f "$SETS_RULES" "$DSTNAT_RULES" "$OUTPUT_RULES" "$MANGLE_RULES"
|
||||
ip rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip route del local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
/etc/init.d/firewall restart >/dev/null 2>&1 || /etc/init.d/firewall reload >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
apply)
|
||||
apply_rules
|
||||
;;
|
||||
remove)
|
||||
remove_rules
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {apply|remove}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,43 @@
|
||||
# clashoo 运行日志格式化
|
||||
# 输出: MM-DD HH:MM:SS msg
|
||||
|
||||
# mihomo 原生行: time="YYYY-MM-DDTHH:MM:SS..." level=... msg="..."
|
||||
/^time="[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/ {
|
||||
# extract YYYY-MM-DD and HH:MM:SS
|
||||
ts_date = substr($0, 12, 5) # MM-DD
|
||||
ts_time = substr($0, 18, 8) # HH:MM:SS
|
||||
utc_h = substr(ts_time, 1, 2) + 0
|
||||
cst_h = (utc_h + 8) % 24
|
||||
ts = sprintf("%s %02d:%s", ts_date, cst_h, substr(ts_time, 4))
|
||||
|
||||
prefix = ""
|
||||
if (match($0, /level=warning /)) prefix = " [warn]"
|
||||
else if (match($0, /level=error /)) prefix = " [err]"
|
||||
else if (match($0, /level=fatal /)) prefix = " [fatal]"
|
||||
|
||||
i = index($0, "msg=\"")
|
||||
if (i > 0) {
|
||||
rest = substr($0, i + 5)
|
||||
sub(/"[[:space:]]*$/, "", rest)
|
||||
print ts prefix " " rest
|
||||
next
|
||||
}
|
||||
print ts " " $0
|
||||
next
|
||||
}
|
||||
|
||||
# log_msg 行: " YYYY-MM-DD HH:MM:SS - msg" → "MM-DD HH:MM:SS msg"
|
||||
/^[[:space:]]+[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][[:space:]]+[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/ {
|
||||
gsub(/^[[:space:]]+/, "") # trim leading space
|
||||
ts_date = substr($0, 6, 5) # MM-DD
|
||||
ts_time = substr($0, 12, 8) # HH:MM:SS
|
||||
# remove "YYYY-MM-DD HH:MM:SS - " prefix
|
||||
sub(/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][[:space:]]+[0-9][0-9]:[0-9][0-9]:[0-9][0-9][[:space:]]*-[[:space:]]*/, "")
|
||||
print ts_date " " ts_time " " $0
|
||||
next
|
||||
}
|
||||
|
||||
# 空行丢弃
|
||||
NF == 0 { next }
|
||||
|
||||
{ print }
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
|
||||
ACTION="$1"
|
||||
LOG_FILE="/tmp/clash_update.txt"
|
||||
|
||||
log_line() {
|
||||
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
run_init_async() {
|
||||
local action="$1"
|
||||
mkdir -p /tmp/lock >/dev/null 2>&1
|
||||
(flock /tmp/lock/clashoo_rpc_async.lock /etc/init.d/clashoo "$action" >/dev/null 2>&1 </dev/null &)
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
start)
|
||||
run_init_async start
|
||||
exit 0
|
||||
;;
|
||||
stop)
|
||||
run_init_async stop
|
||||
exit 0
|
||||
;;
|
||||
restart)
|
||||
# Fire-and-forget restart used by LuCI RPC to avoid blocking UI apply flow.
|
||||
run_init_async restart
|
||||
exit 0
|
||||
;;
|
||||
update_china_ip)
|
||||
log_line "[china-ip] task started"
|
||||
nohup /usr/share/clashoo/update/update_china_ip.sh >>"$LOG_FILE" 2>&1 </dev/null &
|
||||
exit 0
|
||||
;;
|
||||
update_geoip)
|
||||
log_line "GeoIP 更新任务已触发"
|
||||
(exec 1000>&-; nohup /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1 </dev/null &)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 {start|stop|restart|update_china_ip|update_geoip}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
. /lib/functions.sh
|
||||
|
||||
CURL_GROUP_CACHE="/usr/share/clashbackup/clash_gorup.json"
|
||||
CURL_NOW_CACHE="/usr/share/clashbackup/clash_now.json"
|
||||
CURL_CACHE="/usr/share/clashbackup/clash_curl.json"
|
||||
HISTORY_PATH="/usr/share/clashbackup/history"
|
||||
SECRET=$(uci get clashoo.config.dash_pass 2>/dev/null)
|
||||
LAN_IP=$(uci get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null)
|
||||
PORT=$(uci get clashoo.config.dash_port 2>/dev/null)
|
||||
|
||||
curl -m 5 --retry 2 -w %{http_code}"\n" -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X GET http://"$LAN_IP":"$PORT"/proxies > "$CURL_CACHE" 2>/dev/null
|
||||
if [ "$(sed -n '$p' "$CURL_CACHE" 2>/dev/null)" = "200" ]; then
|
||||
if [ ! -d /usr/share/clashbackup ];then
|
||||
mkdir -p /usr/share/clashbackup 2>/dev/null
|
||||
fi
|
||||
cat "$CURL_CACHE" |jsonfilter -e '@["proxies"][@.type="Selector"]["name"]' > "$CURL_GROUP_CACHE" 2>/dev/null
|
||||
cat "$CURL_CACHE" |jsonfilter -e '@["proxies"][@.type="Selector"]["now"]' > "$CURL_NOW_CACHE" 2>/dev/null
|
||||
awk 'NR==FNR{a[i]=$0;i++}NR>FNR{print a[j]"#*#"$0;j++}' "$CURL_GROUP_CACHE" "$CURL_NOW_CACHE" > "$HISTORY_PATH" 2>/dev/null
|
||||
fi
|
||||
rm -rf /usr/share/clashbackup/clash_*.json 2>/dev/null
|
||||
+347
@@ -0,0 +1,347 @@
|
||||
#!/bin/sh
|
||||
|
||||
REAL_LOG="/usr/share/clashoo/clashoo_real.txt"
|
||||
UPDATE_LOG="/tmp/clash_update.txt"
|
||||
LIST_FILE="/usr/share/clashbackup/confit_list.conf"
|
||||
SUB_DIR="/usr/share/clashoo/config/sub"
|
||||
TMP_PREFIX="/tmp/clash_sub_$$"
|
||||
DEFAULT_SUB_UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
|
||||
|
||||
subtype="$(uci -q get clashoo.config.subcri 2>/dev/null)"
|
||||
config_name_raw="$(uci -q get clashoo.config.config_name 2>/dev/null)"
|
||||
lang="$(uci -q get luci.main.lang 2>/dev/null)"
|
||||
|
||||
log_text() {
|
||||
if [ "$lang" = "en" ]; then
|
||||
echo "$1" >"$REAL_LOG"
|
||||
else
|
||||
echo "$2" >"$REAL_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
log_update() {
|
||||
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
|
||||
}
|
||||
|
||||
sanitize_name() {
|
||||
local name
|
||||
name="$1"
|
||||
name="$(printf '%s' "$name" | tr 'A-Z' 'a-z')"
|
||||
name="$(printf '%s' "$name" | sed -e 's/\.yaml$//' -e 's/\.yml$//')"
|
||||
name="$(printf '%s' "$name" | tr ' /' '--')"
|
||||
name="$(printf '%s' "$name" | sed -e 's/[^a-z0-9._-]/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')"
|
||||
printf '%s' "$name"
|
||||
}
|
||||
|
||||
sanitize_custom_name() {
|
||||
local name
|
||||
name="$1"
|
||||
name="$(printf '%s' "$name" | sed -e 's/\.yaml$//' -e 's/\.yml$//')"
|
||||
name="$(printf '%s' "$name" | tr ' /' '--')"
|
||||
name="$(printf '%s' "$name" | sed -e 's/[\\]//g' -e 's/\.\.+/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')"
|
||||
printf '%s' "$name"
|
||||
}
|
||||
|
||||
url_decode() {
|
||||
printf '%s' "$1" | sed 's/%/\\x/g' | xargs -0 printf '%b' 2>/dev/null || printf '%s' "$1"
|
||||
}
|
||||
|
||||
url_to_name() {
|
||||
local url host qname decoded
|
||||
url="$1"
|
||||
|
||||
qname="$(printf '%s' "$url" | sed -n 's/.*[?&]filename=\([^&#]*\).*/\1/p')"
|
||||
[ -n "$qname" ] || qname="$(printf '%s' "$url" | sed -n 's/.*[?&]name=\([^&#]*\).*/\1/p')"
|
||||
if [ -n "$qname" ]; then
|
||||
# URL decode then strip non-filename chars, keep CJK and alphanumeric
|
||||
decoded="$(url_decode "$qname")"
|
||||
decoded="$(printf '%s' "$decoded" | tr -d '\r\n' | sed -e 's/[[:space:]]/-/g' -e 's/[/\\:*?"<>|]//g' -e 's/\.yaml$//' -e 's/\.yml$//')"
|
||||
[ -n "$decoded" ] && printf '%s' "$decoded" && return
|
||||
fi
|
||||
qname="$(sanitize_name "$qname")"
|
||||
if [ -n "$qname" ]; then
|
||||
printf '%s' "$qname"
|
||||
return
|
||||
fi
|
||||
|
||||
host="$(printf '%s' "$url" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's/:.*$//' -e 's#\..*$##')"
|
||||
host="$(sanitize_name "$host")"
|
||||
[ -n "$host" ] || host="sub"
|
||||
printf '%s' "$host"
|
||||
}
|
||||
|
||||
next_available_name() {
|
||||
local base try idx
|
||||
base="$1"
|
||||
base="$(printf '%s' "$base" | sed -e 's/\.yaml$//' -e 's/\.yml$//')"
|
||||
base="$(printf '%s' "$base" | tr ' /' '--')"
|
||||
base="$(printf '%s' "$base" | sed -e 's/[\\]//g' -e 's/\.\.+/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')"
|
||||
[ -n "$base" ] || base="sub"
|
||||
|
||||
if [ ! -f "$SUB_DIR/${base}.yaml" ]; then
|
||||
printf '%s' "$base"
|
||||
return
|
||||
fi
|
||||
|
||||
idx=2
|
||||
while :; do
|
||||
try="${base}-${idx}"
|
||||
if [ ! -f "$SUB_DIR/${try}.yaml" ]; then
|
||||
printf '%s' "$try"
|
||||
return
|
||||
fi
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
}
|
||||
|
||||
get_subscription_urls() {
|
||||
uci -q show clashoo.config 2>/dev/null | awk -F"'" '
|
||||
/^clashoo.config.subscribe_url=/ {
|
||||
if (NF >= 3) {
|
||||
for (i = 2; i <= NF; i += 2) {
|
||||
if (length($i) > 0) print $i
|
||||
}
|
||||
} else {
|
||||
sub(/^clashoo.config.subscribe_url=/, "", $0)
|
||||
if (length($0) > 0) print $0
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
ensure_system_dns() {
|
||||
local test_host
|
||||
test_host="github.com"
|
||||
if nslookup "$test_host" 127.0.0.1 >/dev/null 2>&1 || nslookup "$test_host" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
uci delete dhcp.@dnsmasq[0].server >/dev/null 2>&1
|
||||
uci set dhcp.@dnsmasq[0].noresolv='0' >/dev/null 2>&1
|
||||
uci del_list dhcp.@dnsmasq[0].server='127.0.0.1#' >/dev/null 2>&1
|
||||
uci del_list dhcp.@dnsmasq[0].server='127.0.0.1#5300' >/dev/null 2>&1
|
||||
uci add_list dhcp.@dnsmasq[0].server='119.29.29.29' >/dev/null 2>&1
|
||||
uci add_list dhcp.@dnsmasq[0].server='223.5.5.5' >/dev/null 2>&1
|
||||
uci commit dhcp >/dev/null 2>&1
|
||||
/etc/init.d/dnsmasq restart >/dev/null 2>&1
|
||||
sleep 2
|
||||
}
|
||||
|
||||
extract_host() {
|
||||
printf '%s' "$1" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's#:.*$##' -e 's#.*@##'
|
||||
}
|
||||
|
||||
resolve_via() {
|
||||
local host dns
|
||||
host="$1"
|
||||
dns="$2"
|
||||
nslookup "$host" "$dns" 2>/dev/null | awk '
|
||||
/^Address/ {
|
||||
ip = $NF
|
||||
if (ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ &&
|
||||
ip !~ /^127\./ &&
|
||||
ip !~ /^0\./ &&
|
||||
ip !~ /^198\.18\./ &&
|
||||
ip != "8.8.8.8" && ip != "8.8.4.4" &&
|
||||
ip != "1.1.1.1" && ip != "1.0.0.1" &&
|
||||
ip != "223.5.5.5" && ip != "223.6.6.6" &&
|
||||
ip != "119.29.29.29" && ip != "114.114.114.114") {
|
||||
print ip
|
||||
exit
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
curl_subscription() {
|
||||
local url tmp hdr err ua extra_resolve http_code rc
|
||||
url="$1"
|
||||
tmp="$2"
|
||||
hdr="$3"
|
||||
err="$4"
|
||||
ua="$5"
|
||||
extra_resolve="$6"
|
||||
|
||||
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
http_code="$(curl -sSL --connect-timeout 15 --max-time 60 \
|
||||
--speed-time 30 --speed-limit 1 --retry 2 \
|
||||
-H "User-Agent: ${ua}" -D "$hdr" -o "$tmp" \
|
||||
$extra_resolve \
|
||||
-w '%{http_code}' "$url" 2>"$err")"
|
||||
rc=$?
|
||||
printf '%s\n' "$http_code"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
download_subscription() {
|
||||
local url target tmp hdr err rc http_code info_line ua err_msg
|
||||
local host ip dns extra
|
||||
url="$1"
|
||||
target="$2"
|
||||
tmp="${TMP_PREFIX}.yaml"
|
||||
hdr="${TMP_PREFIX}.hdr"
|
||||
err="${TMP_PREFIX}.err"
|
||||
|
||||
ua="$(uci -q get clashoo.config.sub_ua 2>/dev/null)"
|
||||
[ -n "$ua" ] || ua="$DEFAULT_SUB_UA"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "")"
|
||||
rc=$?
|
||||
|
||||
if [ "$rc" -ne 0 ] || [ "$http_code" = "000" ]; then
|
||||
host="$(extract_host "$url")"
|
||||
if [ -n "$host" ]; then
|
||||
for dns in 223.5.5.5 119.29.29.29 1.1.1.1 8.8.8.8; do
|
||||
ip="$(resolve_via "$host" "$dns")"
|
||||
[ -n "$ip" ] || continue
|
||||
log_update "DNS 回退:${host} -> ${ip} (@${dns})"
|
||||
extra="--resolve ${host}:443:${ip} --resolve ${host}:80:${ip}"
|
||||
http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "$extra")"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] && [ "$http_code" = "200" ] && break
|
||||
done
|
||||
fi
|
||||
fi
|
||||
else
|
||||
wget -q --tries=4 --timeout=20 \
|
||||
--user-agent="$ua" "$url" -O "$tmp" 2>"$err"
|
||||
rc=$?
|
||||
http_code=""
|
||||
fi
|
||||
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
err_msg="$(grep -a 'curl:' "$err" 2>/dev/null | tail -1)"
|
||||
[ -z "$err_msg" ] && err_msg="$(tail -1 "$err" 2>/dev/null)"
|
||||
log_update "下载失败:$(basename "$target") rc=${rc} ${err_msg}"
|
||||
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$http_code" ] && [ "$http_code" != "200" ]; then
|
||||
log_update "下载失败:$(basename "$target") HTTP ${http_code}"
|
||||
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! grep -Eq '^(proxies|proxy-providers):' "$tmp" 2>/dev/null; then
|
||||
log_update "校验失败:$(basename "$target") 内容不含 proxies/proxy-providers"
|
||||
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
|
||||
info_line="$(grep -i 'subscription-userinfo:' "$hdr" 2>/dev/null | head -1 | \
|
||||
sed 's/^[Ss]ubscription-[Uu]serinfo:[[:space:]]*//' | tr -d '\r')"
|
||||
[ -n "$info_line" ] && printf '%s\n' "$info_line" > "${target}.info" || \
|
||||
rm -f "${target}.info" >/dev/null 2>&1
|
||||
|
||||
rm -f "$hdr" "$err" >/dev/null 2>&1
|
||||
mv "$tmp" "$target" >/dev/null 2>&1 || {
|
||||
rm -f "$tmp" >/dev/null 2>&1
|
||||
return 1
|
||||
}
|
||||
printf '%s\n' "$ua" >"${target}.ua"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
upsert_meta() {
|
||||
local filename url typ tmpf
|
||||
filename="$1"
|
||||
url="$2"
|
||||
typ="$3"
|
||||
tmpf="${TMP_PREFIX}.list"
|
||||
|
||||
[ -f "$LIST_FILE" ] || touch "$LIST_FILE"
|
||||
awk -F '#' -v n="$filename" '$1 != n { print $0 }' "$LIST_FILE" >"$tmpf"
|
||||
printf '%s#%s#%s\n' "$filename" "$url" "$typ" >>"$tmpf"
|
||||
mv "$tmpf" "$LIST_FILE"
|
||||
}
|
||||
|
||||
cleanup_tmp() {
|
||||
rm -f "${TMP_PREFIX}.yaml" "${TMP_PREFIX}.urls" "${TMP_PREFIX}.list" \
|
||||
"${TMP_PREFIX}.hdr" "${TMP_PREFIX}.err" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
trap cleanup_tmp EXIT INT TERM
|
||||
|
||||
[ "$subtype" = "clash" ] || [ "$subtype" = "meta" ] || subtype="clash"
|
||||
|
||||
mkdir -p "$SUB_DIR" /usr/share/clashbackup >/dev/null 2>&1
|
||||
[ -f "$LIST_FILE" ] || touch "$LIST_FILE"
|
||||
|
||||
URLS_FILE="${TMP_PREFIX}.urls"
|
||||
get_subscription_urls | sed '/^[[:space:]]*$/d' | awk '!seen[$0]++' >"$URLS_FILE"
|
||||
|
||||
url_count="$(wc -l <"$URLS_FILE" 2>/dev/null | tr -d ' ')"
|
||||
if [ -z "$url_count" ] || [ "$url_count" -eq 0 ]; then
|
||||
log_update "未找到订阅链接"
|
||||
log_text "No subscription URL found" "未找到订阅链接"
|
||||
sleep 2
|
||||
log_text "Clash for OpenWRT" "Clash for OpenWRT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_system_dns
|
||||
log_update "开始下载订阅(共 ${url_count} 条)"
|
||||
log_text "Downloading subscription..." "开始下载订阅..."
|
||||
|
||||
base_name="$(sanitize_custom_name "$config_name_raw")"
|
||||
timestamp="$(date +%Y%m%d)"
|
||||
|
||||
success=0
|
||||
failed=0
|
||||
idx=0
|
||||
first_file=""
|
||||
|
||||
while IFS= read -r url; do
|
||||
[ -n "$url" ] || continue
|
||||
idx=$((idx + 1))
|
||||
|
||||
if [ -n "$base_name" ]; then
|
||||
if [ "$url_count" -gt 1 ]; then
|
||||
name_candidate="$(sanitize_name "${base_name}-${idx}")"
|
||||
file_base="$(next_available_name "$name_candidate")"
|
||||
else
|
||||
file_base="$base_name"
|
||||
fi
|
||||
else
|
||||
name_candidate="$(url_to_name "$url")-${timestamp}"
|
||||
if [ "$url_count" -gt 1 ]; then
|
||||
name_candidate="${name_candidate}-${idx}"
|
||||
fi
|
||||
file_base="$(next_available_name "$name_candidate")"
|
||||
fi
|
||||
|
||||
target_file="$SUB_DIR/${file_base}.yaml"
|
||||
if download_subscription "$url" "$target_file"; then
|
||||
upsert_meta "${file_base}.yaml" "$url" "$subtype"
|
||||
log_update "订阅下载成功:${file_base}.yaml"
|
||||
[ -n "$first_file" ] || first_file="$target_file"
|
||||
success=$((success + 1))
|
||||
else
|
||||
log_update "订阅下载失败:${file_base}.yaml"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done <"$URLS_FILE"
|
||||
|
||||
if [ "$success" -gt 0 ]; then
|
||||
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
|
||||
if [ -z "$use_config" ] || [ ! -f "$use_config" ]; then
|
||||
uci set clashoo.config.use_config="$first_file"
|
||||
uci set clashoo.config.config_type='1'
|
||||
uci commit clashoo
|
||||
fi
|
||||
log_text "Subscription download completed: ${success} success, ${failed} failed" "订阅下载完成:成功 ${success} 个,失败 ${failed} 个"
|
||||
log_update "订阅下载完成:成功 ${success} 个,失败 ${failed} 个"
|
||||
ret=0
|
||||
else
|
||||
log_text "All subscription downloads failed" "订阅下载失败"
|
||||
log_update "订阅下载失败:全部链接失败"
|
||||
ret=1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
log_text "Clash for OpenWRT" "Clash for OpenWRT"
|
||||
exit "$ret"
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
#!/bin/sh
|
||||
# Shared DNS helpers for ClashOO runtime generators. Keep POSIX sh compatible.
|
||||
|
||||
dns_trim() {
|
||||
printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
}
|
||||
|
||||
dns_has_scheme() {
|
||||
printf '%s' "$1" | grep -Eq '^[A-Za-z][A-Za-z0-9+.-]*://'
|
||||
}
|
||||
|
||||
dns_norm_protocol() {
|
||||
case "$(dns_trim "${1:-}")" in
|
||||
''|'none') printf '' ;;
|
||||
udp|'udp://') printf 'udp://' ;;
|
||||
tcp|'tcp://') printf 'tcp://' ;;
|
||||
dot|tls|'tls://') printf 'tls://' ;;
|
||||
doh|https|'https://') printf 'https://' ;;
|
||||
doq|quic|'quic://') printf 'quic://' ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
dns_normalize_server() {
|
||||
local address protocol port prefix
|
||||
address=$(dns_trim "${1:-}")
|
||||
protocol=$(dns_trim "${2:-}")
|
||||
port=$(dns_trim "${3:-}")
|
||||
|
||||
[ -n "$address" ] || return 0
|
||||
|
||||
if dns_has_scheme "$address"; then
|
||||
printf '%s' "$address"
|
||||
return 0
|
||||
fi
|
||||
|
||||
prefix=$(dns_norm_protocol "$protocol")
|
||||
if [ -n "$port" ]; then
|
||||
printf '%s%s:%s' "$prefix" "$address" "$port"
|
||||
else
|
||||
printf '%s%s' "$prefix" "$address"
|
||||
fi
|
||||
}
|
||||
|
||||
dns_yaml_sq() {
|
||||
printf "%s" "$1" | sed "s/'/''/g"
|
||||
}
|
||||
|
||||
dns_yaml_list_item() {
|
||||
local value
|
||||
value=$(dns_yaml_sq "$1")
|
||||
printf " - '%s'\n" "$value"
|
||||
}
|
||||
|
||||
# Route DNS upstreams by rules and preserve the original setting.
|
||||
dns_mihomo_apply_respect_rules() {
|
||||
local cfg="$1" enabled="${2:-0}" tmp="${1}.$$"
|
||||
[ -f "$cfg" ] || return 0
|
||||
|
||||
awk -v enabled="$enabled" '
|
||||
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
|
||||
function spaces(n, out,i) {
|
||||
out = ""
|
||||
for (i = 0; i < n; i++)
|
||||
out = out " "
|
||||
return out
|
||||
}
|
||||
function trim_value(s) {
|
||||
sub(/^[[:space:]]*/, "", s)
|
||||
sub(/[[:space:]]*$/, "", s)
|
||||
return s
|
||||
}
|
||||
function flush_dns( i, sp) {
|
||||
if (!in_dns)
|
||||
return
|
||||
sp = spaces(child_indent > 0 ? child_indent : dns_indent + 2)
|
||||
print dns_lines[1]
|
||||
if (enabled == "1") {
|
||||
print sp "# >>> clashoo:dns_leak_protect_respect_rules"
|
||||
print sp "# original-respect-rules: " (original_value != "" ? original_value : "absent")
|
||||
print sp "respect-rules: true"
|
||||
print sp "# <<< clashoo:dns_leak_protect_respect_rules"
|
||||
} else if (original_value != "" && original_value != "absent") {
|
||||
print sp "respect-rules: " original_value
|
||||
}
|
||||
for (i = 2; i <= dns_n; i++)
|
||||
print dns_lines[i]
|
||||
in_dns = 0
|
||||
dns_n = 0
|
||||
}
|
||||
|
||||
/^dns:[[:space:]]*$/ {
|
||||
flush_dns()
|
||||
in_dns = 1
|
||||
dns_indent = indent_len($0)
|
||||
child_indent = -1
|
||||
dns_n = 1
|
||||
dns_lines[dns_n] = $0
|
||||
original_value = ""
|
||||
in_marker = 0
|
||||
next
|
||||
}
|
||||
in_dns {
|
||||
cur_indent = indent_len($0)
|
||||
if ($0 ~ /^[^[:space:]#][^:]*:/ && cur_indent <= dns_indent) {
|
||||
flush_dns()
|
||||
print
|
||||
next
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_respect_rules[[:space:]]*$/) {
|
||||
in_marker = 1
|
||||
if (child_indent < 0) child_indent = cur_indent
|
||||
next
|
||||
}
|
||||
if (in_marker) {
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*original-respect-rules:[[:space:]]*/) {
|
||||
value = $0
|
||||
sub(/^[[:space:]]*#[[:space:]]*original-respect-rules:[[:space:]]*/, "", value)
|
||||
original_value = trim_value(value)
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_respect_rules[[:space:]]*$/)
|
||||
in_marker = 0
|
||||
next
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*respect-rules:[[:space:]]*/) {
|
||||
if (original_value == "") {
|
||||
value = $0
|
||||
sub(/^[[:space:]]*respect-rules:[[:space:]]*/, "", value)
|
||||
original_value = trim_value(value)
|
||||
}
|
||||
if (child_indent < 0) child_indent = cur_indent
|
||||
next
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && cur_indent > dns_indent && child_indent < 0)
|
||||
child_indent = cur_indent
|
||||
dns_lines[++dns_n] = $0
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END { flush_dns() }
|
||||
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
}
|
||||
|
||||
# Use fallback DNS for queries; keep bootstrap DNS unchanged.
|
||||
dns_mihomo_apply_leak_nameservers() {
|
||||
local cfg="$1" enabled="${2:-0}" state="${1}.dns-leak-nameserver"
|
||||
local selected="${1}.dns-leak-selected" current="${1}.dns-leak-current" tmp="${1}.$$"
|
||||
[ -f "$cfg" ] || return 0
|
||||
command -v yq >/dev/null 2>&1 || return 0
|
||||
|
||||
if [ "$enabled" = "1" ]; then
|
||||
yq e -r '.dns.fallback[]' "$cfg" > "$selected" 2>/dev/null || true
|
||||
if [ ! -s "$selected" ]; then
|
||||
printf '%s\n' 'https://1.1.1.1/dns-query' 'https://8.8.8.8/dns-query' > "$selected"
|
||||
fi
|
||||
yq e -r '.dns.nameserver[]' "$cfg" > "$current" 2>/dev/null || true
|
||||
|
||||
if [ ! -f "$state" ] ||
|
||||
! grep -q 'clashoo:dns_leak_protect_respect_rules' "$cfg" ||
|
||||
! cmp -s "$current" "$selected"; then
|
||||
awk '
|
||||
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
|
||||
/^dns:[[:space:]]*$/ { in_dns = 1; dns_indent = indent_len($0); next }
|
||||
in_dns && /^[^[:space:]#][^:]*:/ { exit }
|
||||
in_dns && /^[[:space:]]*nameserver:/ {
|
||||
print
|
||||
rest = $0
|
||||
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
|
||||
sub(/[[:space:]]*(#.*)?$/, "", rest)
|
||||
if (rest == "") { in_ns = 1; ns_indent = indent_len($0) }
|
||||
next
|
||||
}
|
||||
in_ns {
|
||||
cur_indent = indent_len($0)
|
||||
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) {
|
||||
print
|
||||
next
|
||||
}
|
||||
exit
|
||||
}
|
||||
' "$cfg" > "$state"
|
||||
[ -s "$state" ] || printf '__ABSENT__\n' > "$state"
|
||||
fi
|
||||
|
||||
awk -v selected="$selected" '
|
||||
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
|
||||
function spaces(n, out,i) {
|
||||
out = ""
|
||||
for (i = 0; i < n; i++) out = out " "
|
||||
return out
|
||||
}
|
||||
function print_selected(sp, line) {
|
||||
print sp "nameserver:"
|
||||
while ((getline line < selected) > 0)
|
||||
if (line != "") print sp " - " line
|
||||
close(selected)
|
||||
inserted = 1
|
||||
}
|
||||
/^dns:[[:space:]]*$/ {
|
||||
in_dns = 1
|
||||
dns_indent = indent_len($0)
|
||||
child_indent = -1
|
||||
print
|
||||
next
|
||||
}
|
||||
in_dns && /^[^[:space:]#][^:]*:/ {
|
||||
if (!inserted) print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
|
||||
in_dns = 0
|
||||
print
|
||||
next
|
||||
}
|
||||
in_dns && /^[[:space:]]*[^#[:space:]][^:]*:/ && child_indent < 0 {
|
||||
child_indent = indent_len($0)
|
||||
}
|
||||
in_dns && /^[[:space:]]*nameserver:/ {
|
||||
ns_indent = indent_len($0)
|
||||
rest = $0
|
||||
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
|
||||
sub(/[[:space:]]*(#.*)?$/, "", rest)
|
||||
print_selected(spaces(ns_indent))
|
||||
if (rest == "") skip_ns = 1
|
||||
next
|
||||
}
|
||||
skip_ns {
|
||||
cur_indent = indent_len($0)
|
||||
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next
|
||||
skip_ns = 0
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (in_dns && !inserted)
|
||||
print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
|
||||
}
|
||||
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
||||
elif [ -f "$state" ]; then
|
||||
awk -v state="$state" '
|
||||
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
|
||||
function print_original( line) {
|
||||
if ((getline line < state) > 0 && line != "__ABSENT__") {
|
||||
print line
|
||||
while ((getline line < state) > 0) print line
|
||||
}
|
||||
close(state)
|
||||
restored = 1
|
||||
}
|
||||
/^dns:[[:space:]]*$/ { in_dns = 1; print; next }
|
||||
in_dns && /^[^[:space:]#][^:]*:/ {
|
||||
if (!restored) print_original()
|
||||
in_dns = 0
|
||||
print
|
||||
next
|
||||
}
|
||||
in_dns && /^[[:space:]]*nameserver:/ {
|
||||
ns_indent = indent_len($0)
|
||||
rest = $0
|
||||
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
|
||||
sub(/[[:space:]]*(#.*)?$/, "", rest)
|
||||
print_original()
|
||||
if (rest == "") skip_ns = 1
|
||||
next
|
||||
}
|
||||
skip_ns {
|
||||
cur_indent = indent_len($0)
|
||||
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next
|
||||
skip_ns = 0
|
||||
}
|
||||
{ print }
|
||||
END { if (in_dns && !restored) print_original() }
|
||||
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
||||
rm -f "$state"
|
||||
fi
|
||||
rm -f "$selected" "$current" "$tmp" 2>/dev/null
|
||||
}
|
||||
|
||||
|
||||
# handle mihomo dns block:
|
||||
# - strip previous clashoo:dns_leak_protect injection markers (incl. the old
|
||||
# geosite:gfw block written by older versions)
|
||||
# - rewrite the ipv6: line from ipv6_value (skip when empty)
|
||||
# No longer injects fallback-filter.geosite:gfw — mihomo deprecated it and a
|
||||
# GeoSite.dat missing the gfw category froze startup (issue #25). See flush_dns.
|
||||
# (The fallback-filter parsing vars below are now vestigial; clean up separately.)
|
||||
dns_mihomo_apply_leak_dns_block() {
|
||||
local cfg="$1" enabled="${2:-0}" ipv6_value="${3:-}" tmp="${1}.$$"
|
||||
[ -f "$cfg" ] || return 0
|
||||
|
||||
awk -v enabled="$enabled" -v ipv6_value="$ipv6_value" '
|
||||
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
|
||||
function spaces(n, out,i) {
|
||||
out = ""
|
||||
for (i = 0; i < n; i++)
|
||||
out = out " "
|
||||
return out
|
||||
}
|
||||
function flush_dns( i, sp, ff_sp) {
|
||||
if (!in_dns)
|
||||
return
|
||||
if (in_ff) ff_end = dns_n
|
||||
sp = spaces(child_indent > 0 ? child_indent : dns_indent + 2)
|
||||
ff_sp = (ff_child_indent > 0 ? spaces(ff_child_indent) : sp " ")
|
||||
|
||||
print dns_lines[1]
|
||||
if (ipv6_value != "")
|
||||
print sp "ipv6: " ipv6_value
|
||||
# NOTE: clashoo used to inject `fallback-filter.geosite: [gfw]` here, but
|
||||
# mihomo deprecated fallback-filter.geosite ("replace with nameserver-policy,
|
||||
# will be removed") AND it hard-fails preflight + freezes startup (~90s) when
|
||||
# the downloaded GeoSite.dat lacks the gfw category (issue #25). Stop
|
||||
# injecting it. DNS-leak protection still works via respect-rules / leak
|
||||
# nameservers / DST-PORT,853,REJECT handled elsewhere. The marker strip below
|
||||
# still removes any gfw block injected by older versions, so disabling /
|
||||
# upgrading cleans it up.
|
||||
for (i = 2; i <= dns_n; i++)
|
||||
print dns_lines[i]
|
||||
in_dns = 0
|
||||
dns_n = 0
|
||||
}
|
||||
|
||||
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect[[:space:]]*$/ {
|
||||
in_marker = 1
|
||||
next
|
||||
}
|
||||
in_marker {
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect[[:space:]]*$/)
|
||||
in_marker = 0
|
||||
next
|
||||
}
|
||||
|
||||
/^dns:[[:space:]]*$/ {
|
||||
flush_dns()
|
||||
in_dns = 1
|
||||
dns_indent = indent_len($0)
|
||||
child_indent = -1
|
||||
dns_n = 1
|
||||
dns_lines[dns_n] = $0
|
||||
has_ff = 0
|
||||
in_ff = 0
|
||||
in_ff_geosite = 0
|
||||
ff_indent = -1
|
||||
ff_child_indent = -1
|
||||
ff_geosite_indent = -1
|
||||
ff_has_gfw = 0
|
||||
ff_end = -1
|
||||
next
|
||||
}
|
||||
in_dns {
|
||||
cur_indent = indent_len($0)
|
||||
if ($0 ~ /^[^[:space:]#][^:]*:/ && cur_indent <= dns_indent) {
|
||||
flush_dns()
|
||||
print
|
||||
next
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && cur_indent > dns_indent && child_indent < 0)
|
||||
child_indent = cur_indent
|
||||
if (in_ff && cur_indent <= ff_indent) {
|
||||
ff_end = dns_n
|
||||
in_ff = 0
|
||||
in_ff_geosite = 0
|
||||
}
|
||||
if (in_ff_geosite && cur_indent <= ff_geosite_indent)
|
||||
in_ff_geosite = 0
|
||||
if ($0 ~ /^[[:space:]]*fallback-filter:[[:space:]]*$/) {
|
||||
has_ff = 1
|
||||
in_ff = 1
|
||||
ff_indent = cur_indent
|
||||
ff_child_indent = -1
|
||||
} else if (in_ff && $0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && ff_child_indent < 0) {
|
||||
ff_child_indent = cur_indent
|
||||
}
|
||||
if (in_ff && $0 ~ /^[[:space:]]*geosite:[[:space:]]*$/) {
|
||||
in_ff_geosite = 1
|
||||
ff_geosite_indent = cur_indent
|
||||
} else if (in_ff_geosite && $0 ~ /^[[:space:]]*-[[:space:]]*gfw([[:space:]]*(#.*)?)?$/) {
|
||||
ff_has_gfw = 1
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*ipv6:[[:space:]]*/)
|
||||
next
|
||||
dns_lines[++dns_n] = $0
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END { flush_dns() }
|
||||
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
}
|
||||
|
||||
# handle DST-PORT,853,REJECT injection into rules block
|
||||
# only remove Clashoo marker block, never user original rules
|
||||
dns_mihomo_apply_leak_rule() {
|
||||
local cfg="$1" enabled="${2:-0}" tmp="${1}.$$" has_user_853
|
||||
[ -f "$cfg" ] || return 0
|
||||
|
||||
has_user_853=$(awk '
|
||||
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/ {
|
||||
in_marker = 1
|
||||
next
|
||||
}
|
||||
in_marker {
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/)
|
||||
in_marker = 0
|
||||
next
|
||||
}
|
||||
/^[[:space:]]*-[[:space:]]*DST-PORT,853,REJECT([[:space:]]*(#.*)?)?$/ { found = 1 }
|
||||
END { print found ? 1 : 0 }
|
||||
' "$cfg")
|
||||
|
||||
awk -v enabled="$enabled" -v has_user_853="$has_user_853" '
|
||||
BEGIN { inserted = 0; saw_rules = 0 }
|
||||
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/ {
|
||||
in_marker = 1
|
||||
next
|
||||
}
|
||||
in_marker {
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/)
|
||||
in_marker = 0
|
||||
next
|
||||
}
|
||||
/^rules:[[:space:]]*$/ {
|
||||
saw_rules = 1
|
||||
print
|
||||
if (enabled == "1" && has_user_853 != "1") {
|
||||
print " # >>> clashoo:dns_leak_protect_rule"
|
||||
print " - DST-PORT,853,REJECT"
|
||||
print " # <<< clashoo:dns_leak_protect_rule"
|
||||
inserted = 1
|
||||
}
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (enabled == "1" && has_user_853 != "1" && !inserted && !saw_rules) {
|
||||
print ""
|
||||
print "rules:"
|
||||
print " # >>> clashoo:dns_leak_protect_rule"
|
||||
print " - DST-PORT,853,REJECT"
|
||||
print " # <<< clashoo:dns_leak_protect_rule"
|
||||
}
|
||||
}
|
||||
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
}
|
||||
|
||||
dns_mihomo_apply_leak_protect() {
|
||||
local cfg="$1" enabled="${2:-0}" ipv6_value="${3:-}"
|
||||
[ -f "$cfg" ] || return 0
|
||||
dns_mihomo_apply_leak_nameservers "$cfg" "$enabled"
|
||||
dns_mihomo_apply_respect_rules "$cfg" "$enabled"
|
||||
dns_mihomo_apply_leak_dns_block "$cfg" "$enabled" "$ipv6_value"
|
||||
dns_mihomo_apply_leak_rule "$cfg" "$enabled"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
. as $document |
|
||||
strenv(CLASHOO_DNS_OLD_PORT) as $old_port |
|
||||
strenv(CLASHOO_DNS_HOST_PATTERN) as $host_pattern |
|
||||
(strenv(CLASHOO_DNS_REPLACEMENT) | split(" ") | map(select(length > 0))) as $replacement |
|
||||
("(?i)^(?:[a-z][a-z0-9+.-]*://)?(?:" + $host_pattern + "):" + $old_port + "(?:$|[/#?])") as $old_loopback |
|
||||
(
|
||||
$document |
|
||||
select((.dns."proxy-server-nameserver" | type) == "!!seq") |
|
||||
([.dns."proxy-server-nameserver"[] | select(type == "!!str" and test($old_loopback))] | length) as $match_count |
|
||||
select($match_count > 0) |
|
||||
.dns."proxy-server-nameserver" =
|
||||
([
|
||||
.dns."proxy-server-nameserver"[] |
|
||||
select((type == "!!str" and test($old_loopback)) | not) |
|
||||
select(. as $server | ($replacement | contains([$server])) | not)
|
||||
] + $replacement)
|
||||
) // $document
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
#!/bin/sh
|
||||
. /lib/functions.sh
|
||||
|
||||
|
||||
TMP_PREFIX="/tmp/clashoo_iprules.$$"
|
||||
CUSTOM_RULE_FILE="${TMP_PREFIX}.ipadd.conf"
|
||||
RULE="${TMP_PREFIX}.rules_conf.yaml"
|
||||
CLASH="${TMP_PREFIX}.conf.yaml"
|
||||
CONFIG_YAML="/etc/clashoo/config.yaml"
|
||||
CLASH_CONFIG="${TMP_PREFIX}.config.yaml"
|
||||
trap 'rm -f "$CUSTOM_RULE_FILE" "$RULE" "$CLASH" "$CLASH_CONFIG"' EXIT
|
||||
|
||||
[ -f "$CONFIG_YAML" ] || exit 0
|
||||
|
||||
. /usr/share/clashoo/runtime/primary_group.sh
|
||||
|
||||
|
||||
# __PROXY__ placeholder -> primary proxy group (cached by RPC);
|
||||
# fallback to GLOBAL (built-in mihomo group)
|
||||
CACHED_PRIMARY_GROUP="$(uci -q get clashoo.config.primary_proxy_group)"
|
||||
PRIMARY_GROUP="$(clashoo_resolve_primary_group "$CONFIG_YAML" "$CACHED_PRIMARY_GROUP")"
|
||||
[ -n "$PRIMARY_GROUP" ] || PRIMARY_GROUP="GLOBAL"
|
||||
|
||||
append=$(uci get clashoo.config.append_rules 2>/dev/null)
|
||||
if [ "${append:-0}" -eq 1 ];then
|
||||
|
||||
if [ -f $CLASH_CONFIG ];then
|
||||
rm -rf $CLASH_CONFIG 2>/dev/null
|
||||
fi
|
||||
|
||||
cp $CONFIG_YAML $CLASH_CONFIG 2>/dev/null
|
||||
if [ ! -z "$(grep "^Rule:" "$CLASH_CONFIG")" ]; then
|
||||
sed -i "/^Rule:/i\#RULESTART#" $CLASH_CONFIG 2>/dev/null
|
||||
elif [ ! -z "$(grep "^rules:" "$CLASH_CONFIG")" ]; then
|
||||
sed -i "/^rules:/i\#RULESTART#" $CLASH_CONFIG 2>/dev/null
|
||||
fi
|
||||
sed -i -e "\$a#RULEEND#" $CLASH_CONFIG 2>/dev/null
|
||||
|
||||
awk '/#RULESTART#/,/#RULEEND#/{print}' "$CLASH_CONFIG" 2>/dev/null |sed "s/\'//g" 2>/dev/null |sed 's/\"//g' 2>/dev/null |sed 's/\t/ /g' 2>/dev/null |grep '^ \{0,\}- '|awk -F '- ' '{print "- "$2}' | sed 's/^ \{0,\}//' 2>/dev/null |sed 's/ \{0,\}$//' 2>/dev/null >$RULE 2>&1
|
||||
|
||||
sed -i '/#RULESTART#/,/#RULEEND#/d' "$CLASH_CONFIG" 2>/dev/null
|
||||
|
||||
sed -i -e "\$a " $CLASH_CONFIG 2>/dev/null
|
||||
sed -i "1i\rules:" $RULE 2>/dev/null
|
||||
cat $CLASH_CONFIG $RULE >$CLASH 2>/dev/null
|
||||
mv $CLASH $CONFIG_YAML 2>/dev/null
|
||||
rm -f $RULE $CLASH_CONFIG 2>/dev/null
|
||||
fi
|
||||
|
||||
# ===== custom routing rules (UCI: config addtype), decoupled =====
|
||||
# fields: type(DOMAIN|SUFFIX|KEYWORD|IP-CIDR), ipaaddr, pgroup, res(no-resolve)
|
||||
# pgroup: DIRECT or __PROXY__ (resolved at injection)
|
||||
rm -f "$CUSTOM_RULE_FILE" 2>/dev/null
|
||||
|
||||
ipadd()
|
||||
{
|
||||
local section="$1"
|
||||
config_get "pgroup" "$section" "pgroup" ""
|
||||
config_get "ipaaddr" "$section" "ipaaddr" ""
|
||||
config_get "type" "$section" "type" ""
|
||||
config_get "res" "$section" "res" ""
|
||||
|
||||
[ -z "$type" ] && return
|
||||
[ -z "$ipaaddr" ] && return
|
||||
[ -z "$pgroup" ] && return
|
||||
|
||||
[ "$pgroup" = "__PROXY__" ] && pgroup="$PRIMARY_GROUP"
|
||||
|
||||
# quote whole rule (group name may contain spaces/emoji)
|
||||
# so mixed indent with subscription rules does not break YAML parsing
|
||||
if [ "${res}" = "1" ];then
|
||||
echo "${RULE_INDENT}- \"$type,$ipaaddr,$pgroup,no-resolve\"" >> "$CUSTOM_RULE_FILE"
|
||||
else
|
||||
echo "${RULE_INDENT}- \"$type,$ipaaddr,$pgroup\"" >> "$CUSTOM_RULE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# detect existing rules indent for alignment (default 2)
|
||||
RULE_INDENT="$(awk '/^[[:space:]]*rules:/{f=1;next} f&&/^[[:space:]]*-/{match($0,/^[[:space:]]*/);print substr($0,1,RLENGTH);exit}' "$CONFIG_YAML" 2>/dev/null)"
|
||||
[ -n "$RULE_INDENT" ] || RULE_INDENT=" "
|
||||
|
||||
config_load "clashoo"
|
||||
config_foreach ipadd "addtype"
|
||||
|
||||
# clean previous injection block (idempotent)
|
||||
sed -i '/#CUSTOMRULESTART#/,/#CUSTOMRULEEND#/d' "$CONFIG_YAML" 2>/dev/null
|
||||
|
||||
if [ -f "$CUSTOM_RULE_FILE" ];then
|
||||
sed -i -e "\$a#CUSTOMRULEEND#" "$CUSTOM_RULE_FILE" 2>/dev/null
|
||||
if [ ! -z "$(grep "^ \{0,\}rules:" "$CONFIG_YAML")" ]; then
|
||||
# 插在 rules: 段最前面,优先于订阅自带规则命中(纠错)
|
||||
sed -i '/^ \{0,\}rules:/a\#CUSTOMRULESTART#' "$CONFIG_YAML" 2>/dev/null
|
||||
else
|
||||
echo "rules:" >> "$CONFIG_YAML" 2>/dev/null
|
||||
echo "#CUSTOMRULESTART#" >> "$CONFIG_YAML" 2>/dev/null
|
||||
fi
|
||||
sed -i "/#CUSTOMRULESTART#/r${CUSTOM_RULE_FILE}" "$CONFIG_YAML" 2>/dev/null
|
||||
fi
|
||||
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/ucode
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
const uci = cursor();
|
||||
|
||||
function b(v) {
|
||||
if (v == null || v == '' || v == '0' || v == 'false') return false;
|
||||
if (v == '1' || v == 'true') return true;
|
||||
return v;
|
||||
}
|
||||
function i(v, d) { let n = v != null ? int(v) : null; return n != null ? n : d; }
|
||||
function s(v, d) { let t = trim(v || ''); return length(t) ? t : d; }
|
||||
function a(k) { return uci.get('clashoo', 'config', k); }
|
||||
function ab(k) { return b(a(k)); }
|
||||
|
||||
const cfg = {};
|
||||
|
||||
/* ── 端口与基本设置 ─────────────────────────────────────── */
|
||||
let v;
|
||||
v = i(a('http_port')); if (v != null) cfg['port'] = v;
|
||||
v = i(a('socks_port')); if (v != null) cfg['socks-port'] = v;
|
||||
v = i(a('redir_port')); if (v != null) cfg['redir-port'] = v;
|
||||
v = i(a('mixed_port')); if (v != null) cfg['mixed-port'] = v;
|
||||
v = i(a('tproxy_port')); if (v != null) cfg['tproxy-port'] = v;
|
||||
|
||||
v = a('bind_addr');
|
||||
if (!v || v == '*') v = null;
|
||||
if (v) cfg['bind-address'] = v;
|
||||
|
||||
v = a('p_mode');
|
||||
if (v == 'script') v = 'rule';
|
||||
if (v) cfg['mode'] = v;
|
||||
|
||||
cfg['log-level'] = s(a('level'), 'info');
|
||||
cfg['allow-lan'] = ab('allow_lan');
|
||||
cfg['ipv6'] = ab('enable_ipv6');
|
||||
cfg['routing-mark'] = 6666;
|
||||
cfg['external-controller'] = '0.0.0.0:' + i(a('dash_port'), 9090);
|
||||
cfg['external-ui'] = './dashboard';
|
||||
cfg['secret'] = s(a('dash_pass'), '');
|
||||
|
||||
v = a('interf');
|
||||
if (v && v != '0') cfg['interface-name'] = v;
|
||||
|
||||
/* ── TUN ───────────────────────────────────────────────── */
|
||||
let tun_mode = i(a('tun_mode'), 0);
|
||||
let tcp_mode = s(a('tcp_mode'), 'redirect');
|
||||
let udp_mode = s(a('udp_mode'), 'tproxy');
|
||||
let tun_enabled = (tun_mode == 1 || tcp_mode == 'tun' || udp_mode == 'tun');
|
||||
let acl_enabled = false;
|
||||
uci.foreach('clashoo', 'lan_acl', function(sec) {
|
||||
if (b(sec.enabled) != false) acl_enabled = true;
|
||||
});
|
||||
if (!ab('acl_migrated')) {
|
||||
let access_control = i(a('access_control'), 0);
|
||||
acl_enabled = access_control == 1 || access_control == 2;
|
||||
}
|
||||
let tun_acl = tun_enabled && acl_enabled;
|
||||
|
||||
cfg['tun'] = {
|
||||
enable: tun_enabled,
|
||||
stack: s(a('stack'), 'gvisor'),
|
||||
'auto-route': true,
|
||||
'auto-redirect': !tun_acl,
|
||||
'auto-detect-interface': true,
|
||||
};
|
||||
let tun_mtu = a('tun_mtu');
|
||||
if (tun_mtu != null) cfg['tun']['mtu'] = int(tun_mtu);
|
||||
if (ab('tun_gso')) {
|
||||
cfg['tun']['gso'] = true;
|
||||
cfg['tun']['gso-max-size'] = i(a('tun_gso_max_size'), 65536);
|
||||
}
|
||||
if (ab('tun_dns_hijack')) {
|
||||
cfg['tun']['dns-hijack'] = ['any:53', 'tcp://any:53'];
|
||||
}
|
||||
if (!tun_enabled) {
|
||||
cfg['tun']['enable'] = false;
|
||||
}
|
||||
|
||||
/* ── DNS ───────────────────────────────────────────────── */
|
||||
let dns_port = i(a('listen_port'), 1053);
|
||||
cfg['dns'] = {
|
||||
enable: b(a('enable_dns')) != false,
|
||||
listen: '0.0.0.0:' + dns_port,
|
||||
'enhanced-mode': s(a('enhanced_mode'), 'fake-ip'),
|
||||
'fake-ip-range': s(a('fake_ip_range'), '198.18.0.1/16'),
|
||||
'fake-ip-filter': [],
|
||||
ipv6: ab('enable_ipv6'),
|
||||
};
|
||||
if (cfg['dns']['enhanced-mode'] == 'fake-ip' && ab('enable_ipv6'))
|
||||
cfg['dns']['fake-ip-range6'] = s(a('fake_ip_range6'), 'fc00::/18');
|
||||
/* fake-ip-filter-mode: blacklist (default) | whitelist | rule */
|
||||
let filter_mode = s(a('fake_ip_filter_mode'), 'blacklist');
|
||||
if (filter_mode != 'blacklist') cfg['dns']['fake-ip-filter-mode'] = filter_mode;
|
||||
|
||||
/* map geosite:cn to bundled cn.mrs rule-set so mihomo skips the 10MB geosite.dat */
|
||||
let need_cn_rs = false;
|
||||
let keep_fakeip = length(s(getenv('CLASHOO_KEEP_FAKEIP_FILTER'), ''));
|
||||
if (keep_fakeip) {
|
||||
delete cfg['dns']['fake-ip-filter'];
|
||||
} else {
|
||||
let push_filter = function(f) {
|
||||
if (f == 'geosite:cn') { f = 'rule-set:cn_domain'; need_cn_rs = true; }
|
||||
else if (f == 'rule-set:cn_domain') need_cn_rs = true;
|
||||
push(cfg['dns']['fake-ip-filter'], f);
|
||||
};
|
||||
let filters = a('fake_ip_filter');
|
||||
if (type(filters) == 'array') { for (let f in filters) push_filter(f); }
|
||||
else if (filters != null) push_filter(filters);
|
||||
}
|
||||
|
||||
/* fallback-filter(默认 geoip:false,防止冷启动依赖 MMDB) */
|
||||
cfg['dns']['fallback-filter'] = { geoip: ab('fallback_filter_geoip') };
|
||||
|
||||
let dns_present = {};
|
||||
for (let f in split(s(getenv('CLASHOO_DNS_PRESENT'), ''), ','))
|
||||
if (length(trim(f))) dns_present[trim(f)] = true;
|
||||
let dns_force = {};
|
||||
for (let f in split(s(getenv('CLASHOO_DNS_FORCE'), ''), ','))
|
||||
if (length(trim(f))) dns_force[trim(f)] = true;
|
||||
let dns_inherit_mode = s(getenv('CLASHOO_DNS_INHERIT_MODE'), '');
|
||||
let inherited_nameserver = json(s(getenv('CLASHOO_DNS_INHERIT_NAMESERVER'), '[]'));
|
||||
if (type(inherited_nameserver) != 'array') {
|
||||
if (inherited_nameserver != null && inherited_nameserver != '')
|
||||
inherited_nameserver = [inherited_nameserver];
|
||||
else
|
||||
inherited_nameserver = [];
|
||||
}
|
||||
let dns_role_fields = ['nameserver', 'proxy-server-nameserver', 'direct-nameserver',
|
||||
'default-nameserver', 'nameserver-policy', 'respect-rules'];
|
||||
let had_user_dns_roles = false;
|
||||
for (let f in dns_role_fields)
|
||||
if (dns_present[f]) had_user_dns_roles = true;
|
||||
if (length(keys(dns_force)) || !had_user_dns_roles)
|
||||
cfg['dns']['x-clashoo-managed-dns'] = true;
|
||||
|
||||
function dns_scheme(protocol) {
|
||||
switch (trim(protocol || '')) {
|
||||
case '':
|
||||
case 'none': return '';
|
||||
case 'udp': case 'udp://': return 'udp://';
|
||||
case 'tcp': case 'tcp://': return 'tcp://';
|
||||
case 'dot': case 'tls': case 'tls://': return 'tls://';
|
||||
case 'doh': case 'https': case 'https://': return 'https://';
|
||||
case 'doq': case 'quic': case 'quic://': return 'quic://';
|
||||
default: return protocol;
|
||||
}
|
||||
}
|
||||
|
||||
function dns_server(address, protocol, port) {
|
||||
let addr = trim(address || '');
|
||||
if (!length(addr)) return null;
|
||||
if (match(addr, /^[A-Za-z][A-Za-z0-9+.-]*:\/\//)) return addr;
|
||||
let prefix = dns_scheme(protocol);
|
||||
return length(trim(port || '')) ? prefix + addr + ':' + trim(port) : prefix + addr;
|
||||
}
|
||||
|
||||
let dns_roles = {};
|
||||
uci.foreach('clashoo', 'dnsservers', function(sec) {
|
||||
if (b(sec.enabled) == false) return;
|
||||
let role = s(sec.ser_type, 'nameserver');
|
||||
if (role == 'fallback' && cfg['dns']['enhanced-mode'] == 'fake-ip') return;
|
||||
let srv = dns_server(sec.ser_address, sec.protocol, sec.ser_port);
|
||||
if (!srv) return;
|
||||
if (!dns_roles[role]) dns_roles[role] = [];
|
||||
push(dns_roles[role], srv);
|
||||
});
|
||||
for (let role in keys(dns_roles)) {
|
||||
if (role == 'proxy-server-nameserver' && (dns_inherit_mode == 'inherit' || dns_inherit_mode == 'skip') &&
|
||||
!dns_present[role] && !dns_force[role]) continue;
|
||||
if (!dns_present[role] || dns_force[role]) cfg['dns'][role] = dns_roles[role];
|
||||
}
|
||||
|
||||
if (dns_inherit_mode == 'inherit' && length(inherited_nameserver) &&
|
||||
!dns_present['proxy-server-nameserver'] && !dns_force['proxy-server-nameserver'])
|
||||
cfg['dns']['proxy-server-nameserver'] = inherited_nameserver;
|
||||
|
||||
let bootstrap = a('default_nameserver');
|
||||
if (bootstrap == null) bootstrap = a('defaul_nameserver');
|
||||
if (type(bootstrap) != 'array') bootstrap = bootstrap != null ? [bootstrap] : [];
|
||||
if (length(bootstrap) && (!dns_present['default-nameserver'] || dns_force['default-nameserver']))
|
||||
cfg['dns']['default-nameserver'] = bootstrap;
|
||||
|
||||
let policies = {};
|
||||
uci.foreach('clashoo', 'dns_policy', function(sec) {
|
||||
if (b(sec.enabled) == false) return;
|
||||
if (s(sec.policy_type, 'nameserver-policy') != 'nameserver-policy') return;
|
||||
let matcher = trim(sec.matcher || '');
|
||||
let servers = sec.nameserver;
|
||||
if (type(servers) != 'array') servers = servers != null ? [servers] : [];
|
||||
if (!length(matcher) || !length(servers)) return;
|
||||
if (matcher == 'geosite:cn') { matcher = 'rule-set:cn_domain'; need_cn_rs = true; }
|
||||
policies[matcher] = servers;
|
||||
});
|
||||
if (length(keys(policies)) && (!dns_present['nameserver-policy'] || dns_force['nameserver-policy']))
|
||||
cfg['dns']['nameserver-policy'] = policies;
|
||||
|
||||
let respect_rules = a('dns_respect_rules') == null ? true : ab('dns_respect_rules');
|
||||
if (dns_force['respect-rules'])
|
||||
cfg['dns']['respect-rules'] = respect_rules;
|
||||
else if (respect_rules && length(cfg['dns']['proxy-server-nameserver'] || [])
|
||||
&& !dns_present['respect-rules'] && !dns_present['prefer-h3'])
|
||||
cfg['dns']['respect-rules'] = true;
|
||||
|
||||
/* profile */
|
||||
let store_selected = ab('selection_cache');
|
||||
/* fake-ip cache defaults ON to survive restarts; opt out with '0' */
|
||||
let store_fake = a('fake_ip_cache') == null ? true : ab('fake_ip_cache');
|
||||
if (store_selected || store_fake) {
|
||||
cfg['profile'] = {};
|
||||
if (store_selected) cfg['profile']['store-selected'] = true;
|
||||
if (store_fake) cfg['profile']['store-fake-ip'] = true;
|
||||
}
|
||||
|
||||
/* bundled cn.mrs rule-provider, referenced by fake-ip-filter rule-set:cn_domain */
|
||||
if (need_cn_rs) {
|
||||
if (!cfg['rule-providers']) cfg['rule-providers'] = {};
|
||||
cfg['rule-providers']['cn_domain'] = {
|
||||
type: 'file',
|
||||
path: './ruleset/cn.mrs',
|
||||
format: 'mrs',
|
||||
behavior: 'domain',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ── authentication ────────────────────────────────── */
|
||||
if (ab('authentication')) {
|
||||
cfg['authentication'] = [];
|
||||
uci.foreach('clashoo', 'authentication', function(sec) {
|
||||
if (b(sec.enabled) == false) return;
|
||||
let user = trim(sec.username || '');
|
||||
let pass = trim(sec.password || '');
|
||||
if (user) push(cfg['authentication'], user + ':' + pass);
|
||||
});
|
||||
}
|
||||
|
||||
/* ── hosts ────────────────────────────────────────── */
|
||||
cfg['hosts'] = {};
|
||||
uci.foreach('clashoo', 'hosts', function(sec) {
|
||||
if (b(sec.enabled) == false) return;
|
||||
let domain = trim(sec.adress || '');
|
||||
let ip = trim(sec.ip || '');
|
||||
if (domain && ip) cfg['hosts'][domain] = ip;
|
||||
});
|
||||
if (length(keys(cfg['hosts'])) == 0) delete cfg['hosts'];
|
||||
|
||||
/* ── sniffer ──────────────────────────────────────── */
|
||||
if (ab('sniffer_streaming') && !length(s(getenv('CLASHOO_KEEP_SNIFFER'), ''))) {
|
||||
cfg['sniffer'] = {
|
||||
enable: true,
|
||||
'force-dns-mapping': true,
|
||||
'parse-pure-ip': true,
|
||||
sniff: {
|
||||
HTTP: { ports: [80, 8080], 'override-destination': true },
|
||||
TLS: { ports: [443, 8443], 'override-destination': true },
|
||||
QUIC: { ports: [443, 8443], 'override-destination': true },
|
||||
},
|
||||
'force-domain': [
|
||||
'+.youtube.com',
|
||||
'+.googlevideo.com',
|
||||
'+.netflix.com',
|
||||
'+.nflxvideo.net',
|
||||
'+.disneyplus.com',
|
||||
'+.hulu.com',
|
||||
'+.hbomax.com',
|
||||
],
|
||||
'skip-domain': [
|
||||
'+.lan',
|
||||
'+.local',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/* ── Smart kernel injection ───────────────────────── */
|
||||
if (ab('smart_auto_switch')) {
|
||||
/* Smart 策略注入:通过 proxy-groups 覆盖实现 */
|
||||
}
|
||||
|
||||
/* ── ECS ──────────────────────────────────────────── */
|
||||
let ecs = s(a('dns_ecs'));
|
||||
if (ecs) cfg['dns']['edns-client-subnet'] = ecs;
|
||||
|
||||
/* ── cache_file / experimental ──────────────────── */
|
||||
cfg['experimental'] = {
|
||||
'cache_file': { enabled: true },
|
||||
'clash_api': {
|
||||
'external-controller': '0.0.0.0:' + i(a('dash_port'), 9090),
|
||||
'external-ui': './dashboard',
|
||||
'secret': s(a('dash_pass'), ''),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/* ── 输出 JSON (不含 null/空对象) ─────────────────── */
|
||||
function clean(obj) {
|
||||
if (type(obj) == 'array') {
|
||||
let r = [];
|
||||
for (let e in obj) {
|
||||
e = clean(e);
|
||||
if (e != null) push(r, e);
|
||||
}
|
||||
return length(r) ? r : null;
|
||||
}
|
||||
if (type(obj) == 'object') {
|
||||
let r = {};
|
||||
for (let k in keys(obj)) {
|
||||
let v = clean(obj[k]);
|
||||
if (v != null) r[k] = v;
|
||||
}
|
||||
return length(keys(r)) ? r : null;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
print(clean(cfg));
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
|
||||
clashoo_resolve_primary_group() {
|
||||
local config_yaml="$1"
|
||||
local cached_group="$2"
|
||||
local group_rows
|
||||
local resolved_group
|
||||
|
||||
if [ "$cached_group" = "GLOBAL" ]; then
|
||||
printf '%s\n' "GLOBAL"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$config_yaml" ] || ! command -v yq >/dev/null 2>&1; then
|
||||
printf '%s\n' "GLOBAL"
|
||||
return 0
|
||||
fi
|
||||
|
||||
group_rows="$(yq -M e -r '(."proxy-groups" // [])[] | [.name, .type] | @tsv' "$config_yaml" 2>/dev/null)" || {
|
||||
printf '%s\n' "GLOBAL"
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ -n "$cached_group" ] && printf '%s\n' "$group_rows" |
|
||||
awk -F '\t' -v wanted="$cached_group" '$1 == wanted { found = 1; exit } END { exit !found }'
|
||||
then
|
||||
printf '%s\n' "$cached_group"
|
||||
return 0
|
||||
fi
|
||||
|
||||
resolved_group="$(printf '%s\n' "$group_rows" | awk -F '\t' '
|
||||
tolower($2) == "select" &&
|
||||
(index($1, "节点选择") || index($1, "代理") || index($1, "🚀") || tolower($1) ~ /proxy/) {
|
||||
print $1
|
||||
exit
|
||||
}
|
||||
')"
|
||||
|
||||
if [ -z "$resolved_group" ]; then
|
||||
resolved_group="$(printf '%s\n' "$group_rows" | awk -F '\t' '
|
||||
tolower($2) == "select" {
|
||||
print $1
|
||||
exit
|
||||
}
|
||||
')"
|
||||
fi
|
||||
|
||||
printf '%s\n' "${resolved_group:-GLOBAL}"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# If every configured rule points to a proxy without UDP support, mihomo falls
|
||||
# through to DIRECT. Reject only that otherwise-unmatched QUIC traffic so the
|
||||
# client can retry over TCP; rules with a usable UDP target still win first.
|
||||
with(
|
||||
select(has("rules")) |
|
||||
select((.rules | type) == "!!seq") |
|
||||
select(([.rules[] | select(. == "AND,((NETWORK,UDP),(DST-PORT,443)),REJECT")] | length) == 0);
|
||||
.rules += ["AND,((NETWORK,UDP),(DST-PORT,443)),REJECT"]
|
||||
)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ -f /usr/share/clashbackup/history ];then
|
||||
|
||||
HISTORY_PATH="/usr/share/clashbackup/history"
|
||||
SECRET=$(uci get clashoo.config.dash_pass 2>/dev/null)
|
||||
LAN_IP=$(uci get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null)
|
||||
PORT=$(uci get clashoo.config.dash_port 2>/dev/null)
|
||||
urlencode() {
|
||||
local data
|
||||
if [ "$#" != 1 ]; then
|
||||
return 1
|
||||
fi
|
||||
data=$(curl -s -o /dev/null -w %{url_effective} --get --data-urlencode "$1" "")
|
||||
if [ ! -z "$data" ]; then
|
||||
echo "${data##/?}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
cat $HISTORY_PATH |while read line
|
||||
do
|
||||
if [ -z "$(echo $line |grep "#*#")" ]; then
|
||||
continue
|
||||
else
|
||||
GORUP_NAME=$(urlencode "$(echo $line |awk -F '#*#' '{print $1}')")
|
||||
NOW_NAME=$(echo $line |awk -F '#*#' '{print $3}')
|
||||
curl -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X PUT -d '{"name":"'"$NOW_NAME"'"}' http://"$LAN_IP":"$PORT"/proxies/"$GORUP_NAME" >/dev/null 2>&1
|
||||
fi
|
||||
done >/dev/null 2>&1
|
||||
curl -m 5 --retry 2 -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X DELETE http://"$LAN_IP":"$PORT"/connections >/dev/null 2>&1
|
||||
fi
|
||||
@@ -0,0 +1,28 @@
|
||||
# Preserve every explicit user choice while restoring missing per-protocol defaults.
|
||||
with(
|
||||
.sniffer |
|
||||
select((has("enable") | not) or (.enable == true)) |
|
||||
select((has("override-destination") | not) or (."override-destination" == true));
|
||||
with(
|
||||
select(has("force-dns-mapping") | not);
|
||||
."force-dns-mapping" = true
|
||||
) |
|
||||
with(
|
||||
select(has("parse-pure-ip") | not);
|
||||
."parse-pure-ip" = true
|
||||
) |
|
||||
with(
|
||||
.sniff | select(has("TLS"));
|
||||
with(
|
||||
.TLS | select(type == "!!map" and (has("override-destination") | not));
|
||||
."override-destination" = true
|
||||
)
|
||||
) |
|
||||
with(
|
||||
.sniff | select(has("QUIC"));
|
||||
with(
|
||||
.QUIC | select(type == "!!map" and (has("override-destination") | not));
|
||||
."override-destination" = true
|
||||
)
|
||||
)
|
||||
)
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
time.windows.com
|
||||
time.nist.gov
|
||||
time.apple.com
|
||||
time.asia.apple.com
|
||||
cn.ntp.org.cn
|
||||
edu.ntp.org.cn
|
||||
hk.ntp.org.cn
|
||||
tw.ntp.org.cn
|
||||
us.ntp.org.cn
|
||||
sgp.ntp.org.cn
|
||||
kr.ntp.org.cn
|
||||
jp.ntp.org.cn
|
||||
de.ntp.org.cn
|
||||
ina.ntp.org.cn
|
||||
0.openwrt.pool.ntp.org
|
||||
1.openwrt.pool.ntp.org
|
||||
2.openwrt.pool.ntp.org
|
||||
3.openwrt.pool.ntp.org
|
||||
ntp.aliyun.com
|
||||
ntp1.aliyun.com
|
||||
ntp2.aliyun.com
|
||||
ntp3.aliyun.com
|
||||
ntp4.aliyun.com
|
||||
ntp5.aliyun.com
|
||||
ntp6.aliyun.com
|
||||
ntp7.aliyun.com
|
||||
time1.aliyun.com
|
||||
time2.aliyun.com
|
||||
time3.aliyun.com
|
||||
time4.aliyun.com
|
||||
time5.aliyun.com
|
||||
time6.aliyun.com
|
||||
time7.aliyun.com
|
||||
s1c.time.edu.cn
|
||||
s2m.time.edu.cn
|
||||
s1b.time.edu.cn
|
||||
s1e.time.edu.cn
|
||||
s2a.time.edu.cn
|
||||
s2b.time.edu.cn
|
||||
time1.apple.com
|
||||
time2.apple.com
|
||||
time3.apple.com
|
||||
time4.apple.com
|
||||
time5.apple.com
|
||||
time6.apple.com
|
||||
time7.apple.com
|
||||
time1.google.com
|
||||
time2.google.com
|
||||
time3.google.com
|
||||
time4.google.com
|
||||
.music.163.com
|
||||
.music.126.net
|
||||
man.netease.com
|
||||
api.iplay.163.com
|
||||
ac.dun.163yun.com
|
||||
mr.da.netease.com
|
||||
crash.163.com
|
||||
imap.163.com
|
||||
msftconnecttest.com
|
||||
msftncsi.com
|
||||
captive.qq.com
|
||||
ssl.ptlogin2.qq.com
|
||||
login.qq.com
|
||||
+643
@@ -0,0 +1,643 @@
|
||||
#!/bin/sh
|
||||
|
||||
LOG_FILE="/tmp/clashoo_component_update.log"
|
||||
RUN_FILE="/var/run/clashoo_component_update"
|
||||
STATE_FILE="/tmp/clashoo_component_update_state"
|
||||
TMP_DIR="/tmp/clashoo-component-update"
|
||||
APK_UPGRADE_FLAG="/var/run/clashoo_package_upgrade"
|
||||
FEED_BASE_URL="https://down.dllkids.xyz/openwrt-feed/clashoo"
|
||||
GITHUB_API_URL="https://api.github.com/repos/kenzok8/openwrt-clashoo/releases/latest"
|
||||
ENV_GITHUB_PROXY_PREFIX="${GITHUB_PROXY_PREFIX:-}"
|
||||
UCI_GITHUB_PROXY_PREFIX="$(uci -q get clashoo.config.core_mirror_prefix 2>/dev/null)"
|
||||
GITHUB_PROXY_PREFIX="${UCI_GITHUB_PROXY_PREFIX:-${ENV_GITHUB_PROXY_PREFIX:-https://ghfast.top/}}"
|
||||
case "$GITHUB_PROXY_PREFIX" in
|
||||
*/) ;;
|
||||
*) GITHUB_PROXY_PREFIX="${GITHUB_PROXY_PREFIX}/" ;;
|
||||
esac
|
||||
CONNECT_TIMEOUT="${CONNECT_TIMEOUT:-8}"
|
||||
REQUEST_TIMEOUT="${REQUEST_TIMEOUT:-20}"
|
||||
LOW_SPEED_TIME="${LOW_SPEED_TIME:-30}"
|
||||
LOW_SPEED_LIMIT="${LOW_SPEED_LIMIT:-1024}"
|
||||
|
||||
trap 'rm -f "$APK_UPGRADE_FLAG"' EXIT
|
||||
|
||||
log() {
|
||||
mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$STATE_FILE")" >/dev/null 2>&1
|
||||
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$LOG_FILE"
|
||||
printf 'component=%s\nstatus=%s\nmessage=%s\n' "${COMPONENT:-}" "${STATUS:-running}" "$*" >"$STATE_FILE"
|
||||
}
|
||||
|
||||
finish() {
|
||||
rc="$1"
|
||||
msg="$2"
|
||||
STATUS="success"
|
||||
[ "$rc" -eq 0 ] || STATUS="failed"
|
||||
log "$msg"
|
||||
rm -f "$RUN_FILE" "$APK_UPGRADE_FLAG"
|
||||
exit "$rc"
|
||||
}
|
||||
|
||||
# In kernel-only mode there is no transparent proxy, so component downloads
|
||||
# would go out direct and stall behind the GFW. Route them through the running
|
||||
# core (shared logic in proxy_lib.sh). Normal mode returns empty -> TPROXY.
|
||||
# Guard the source: on a stale install missing proxy_lib.sh, an unguarded `.`
|
||||
# under `set -e` kills the whole update silently (empty log, nothing happens).
|
||||
# Fall back to no proxy detection (direct / TPROXY) instead of dying.
|
||||
if [ -f /usr/share/clashoo/update/proxy_lib.sh ]; then
|
||||
. /usr/share/clashoo/update/proxy_lib.sh
|
||||
else
|
||||
clashoo_detect_proxy() { :; }
|
||||
fi
|
||||
detect_proxy() { clashoo_detect_proxy; }
|
||||
|
||||
fetch_text() {
|
||||
url="$1"
|
||||
proxy="$(detect_proxy)"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$REQUEST_TIMEOUT" ${proxy:+--proxy "$proxy"} "$url"
|
||||
return $?
|
||||
fi
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
[ -n "$proxy" ] && { http_proxy="$proxy" https_proxy="$proxy" wget -qO- --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"; return $?; }
|
||||
wget -qO- --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"
|
||||
return $?
|
||||
fi
|
||||
return 127
|
||||
}
|
||||
|
||||
download_file() {
|
||||
url="$1"
|
||||
out="$2"
|
||||
proxy="$(detect_proxy)"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fL --connect-timeout "$CONNECT_TIMEOUT" --speed-time "$LOW_SPEED_TIME" --speed-limit "$LOW_SPEED_LIMIT" ${proxy:+--proxy "$proxy"} "$url" -o "$out"
|
||||
return $?
|
||||
fi
|
||||
if [ -n "$proxy" ]; then
|
||||
http_proxy="$proxy" https_proxy="$proxy" wget -qO "$out" --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"
|
||||
return $?
|
||||
fi
|
||||
wget -qO "$out" --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"
|
||||
}
|
||||
|
||||
download_url() {
|
||||
url="$1"
|
||||
case "$url" in
|
||||
https://github.com/*)
|
||||
printf '%s%s\n' "$GITHUB_PROXY_PREFIX" "$url"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$url"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
root_free_kb() {
|
||||
df -k / 2>/dev/null | awk 'NR==2 {print $4}'
|
||||
}
|
||||
|
||||
kb_to_mib() {
|
||||
awk "BEGIN {printf \"%.0f\", (${1:-0}) / 1024}"
|
||||
}
|
||||
|
||||
ensure_root_space() {
|
||||
need_kb="$1"
|
||||
free_kb="$(root_free_kb)"
|
||||
[ -n "$free_kb" ] || return 0
|
||||
[ "$free_kb" -ge "$need_kb" ] && return 0
|
||||
finish 1 "根分区空间不足:可用 $(kb_to_mib "$free_kb") MiB,至少需要 $(kb_to_mib "$need_kb") MiB"
|
||||
}
|
||||
|
||||
log_install_error() {
|
||||
file="$1"
|
||||
[ -s "$file" ] || return 0
|
||||
tail -8 "$file" 2>/dev/null | while IFS= read -r line; do
|
||||
[ -n "$line" ] && log "$line"
|
||||
done
|
||||
}
|
||||
|
||||
detect_manager() {
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
echo opkg
|
||||
return
|
||||
fi
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
echo apk
|
||||
return
|
||||
fi
|
||||
echo unsupported
|
||||
}
|
||||
|
||||
installed_package_version() {
|
||||
pkg="$1"
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
opkg status "$pkg" 2>/dev/null | awk -F': ' '/^Version:/{print $2; exit}'
|
||||
return
|
||||
fi
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk list -I "$pkg" 2>/dev/null | awk '{print $1; exit}' | sed -n "s/^${pkg}-//p"
|
||||
fi
|
||||
}
|
||||
|
||||
package_installed() {
|
||||
pkg="$1"
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
opkg status "$pkg" 2>/dev/null | awk '
|
||||
$1 == "Status:" && $2 == "install" && $NF == "installed" { found = 1 }
|
||||
END { exit !found }
|
||||
'
|
||||
return
|
||||
fi
|
||||
apk info -e "$pkg" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
apk_install() {
|
||||
proxy="$(detect_proxy)"
|
||||
if [ -n "$proxy" ]; then
|
||||
http_proxy="$proxy" https_proxy="$proxy" apk add --allow-untrusted --timeout 30 "$@"
|
||||
else
|
||||
apk add --allow-untrusted --timeout 30 "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
apk_bundle_install() {
|
||||
printf '%s\n' "$$" >"$APK_UPGRADE_FLAG"
|
||||
apk_install "$@"
|
||||
rc=$?
|
||||
rm -f "$APK_UPGRADE_FLAG"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
verify_downloaded_package() {
|
||||
file="$1"
|
||||
[ -s "$file" ] || return 1
|
||||
if [ "$PM" = "apk" ]; then
|
||||
apk verify --allow-untrusted "$file" >/dev/null 2>&1
|
||||
return
|
||||
fi
|
||||
gzip -t "$file" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
download_package_file() {
|
||||
url="$1"
|
||||
out="$2"
|
||||
name="$3"
|
||||
download_file "$(download_url "$url")" "$out" || finish 1 "下载 ${name} 失败"
|
||||
verify_downloaded_package "$out" || finish 1 "${name} 文件校验失败"
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
pm="$1"
|
||||
if [ "$pm" = "opkg" ]; then
|
||||
opkg print-architecture 2>/dev/null | awk '/^arch / {print $2}' | tail -n 1
|
||||
return
|
||||
fi
|
||||
# apk: DISTRIB_ARCH is authoritative (e.g. aarch64_cortex-a53); apk --print-arch
|
||||
# only gives the generic "aarch64" which won't match the .apk package suffix.
|
||||
if [ -r /etc/openwrt_release ]; then
|
||||
a="$(sed -n "s/^DISTRIB_ARCH=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)"
|
||||
[ -n "$a" ] && { printf '%s\n' "$a"; return; }
|
||||
fi
|
||||
apk --print-arch 2>/dev/null
|
||||
}
|
||||
|
||||
detect_sdk() {
|
||||
[ -r /etc/openwrt_release ] || return 1
|
||||
release="$(sed -n "s/^DISTRIB_RELEASE=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)"
|
||||
[ -n "$release" ] || return 1
|
||||
printf '%s\n' "$release" | grep -Eo '[0-9]+\.[0-9]+' | head -n 1
|
||||
}
|
||||
|
||||
append_unique_word() {
|
||||
value="$1"
|
||||
list="$2"
|
||||
[ -n "$value" ] || {
|
||||
printf '%s\n' "$list"
|
||||
return
|
||||
}
|
||||
case " $list " in
|
||||
*" $value "*) ;;
|
||||
*) list="${list}${list:+ }${value}" ;;
|
||||
esac
|
||||
printf '%s\n' "$list"
|
||||
}
|
||||
|
||||
build_sdk_candidates() {
|
||||
pm="$1"
|
||||
candidates=""
|
||||
detected_sdk="$(detect_sdk || true)"
|
||||
candidates="$(append_unique_word "$detected_sdk" "$candidates")"
|
||||
|
||||
if [ "$pm" = "opkg" ]; then
|
||||
for sdk in 24.10 23.05 22.03 21.02; do
|
||||
candidates="$(append_unique_word "$sdk" "$candidates")"
|
||||
done
|
||||
else
|
||||
for sdk in 25.12 24.10; do
|
||||
candidates="$(append_unique_word "$sdk" "$candidates")"
|
||||
done
|
||||
fi
|
||||
|
||||
printf '%s\n' "$candidates"
|
||||
}
|
||||
|
||||
find_manifest_value() {
|
||||
key="$1"
|
||||
manifest_text="$2"
|
||||
printf '%s\n' "$manifest_text" | sed -n "s/^${key}=//p" | head -n 1
|
||||
}
|
||||
|
||||
load_manifest_urls() {
|
||||
sdk="$1"
|
||||
arch="$2"
|
||||
manifest_url="${FEED_BASE_URL}/${sdk}/${arch}/manifest-clashoo.txt"
|
||||
manifest_text="$(fetch_text "$manifest_url" || true)"
|
||||
[ -n "$manifest_text" ] || return 1
|
||||
|
||||
core_file="$(find_manifest_value "core" "$manifest_text")"
|
||||
luci_file="$(find_manifest_value "luci" "$manifest_text")"
|
||||
i18n_file="$(find_manifest_value "i18n" "$manifest_text")"
|
||||
[ -n "$core_file" ] || return 1
|
||||
[ -n "$luci_file" ] || return 1
|
||||
case "$core_file" in *."$EXT") ;; *) return 1 ;; esac
|
||||
case "$luci_file" in *."$EXT") ;; *) return 1 ;; esac
|
||||
case "$i18n_file" in ""|*."$EXT") ;; *) return 1 ;; esac
|
||||
|
||||
CORE_URL="${FEED_BASE_URL}/${sdk}/${arch}/${core_file}"
|
||||
LUCI_URL="${FEED_BASE_URL}/${sdk}/${arch}/${luci_file}"
|
||||
I18N_URL=""
|
||||
[ -n "$i18n_file" ] && I18N_URL="${FEED_BASE_URL}/${sdk}/${arch}/${i18n_file}"
|
||||
SOURCE_LABEL="R2 feed ${sdk}/${arch}"
|
||||
return 0
|
||||
}
|
||||
|
||||
load_github_urls() {
|
||||
arch="$1"
|
||||
ext="$2"
|
||||
payload="$(fetch_text "$GITHUB_API_URL" || true)"
|
||||
[ -n "$payload" ] || return 1
|
||||
|
||||
urls="$(printf '%s\n' "$payload" | sed -n 's/.*"browser_download_url":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
||||
[ -n "$urls" ] || return 1
|
||||
|
||||
if [ "$ext" = "apk" ]; then
|
||||
CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo-[^-]+.*-r[0-9]+-'"$arch"'\.apk$' | head -n 1)"
|
||||
LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)"
|
||||
I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)"
|
||||
else
|
||||
CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo_.*_'"$arch"'\.ipk$' | head -n 1)"
|
||||
LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo_.*_all\.ipk$' | head -n 1)"
|
||||
I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn_.*_all\.ipk$' | head -n 1)"
|
||||
fi
|
||||
|
||||
[ -n "$CORE_URL" ] || return 1
|
||||
[ -n "$LUCI_URL" ] || return 1
|
||||
SOURCE_LABEL="GitHub latest"
|
||||
return 0
|
||||
}
|
||||
|
||||
package_version_from_url() {
|
||||
file="${1##*/}"
|
||||
case "$file" in
|
||||
clashoo_*_"$ARCH".ipk)
|
||||
v="${file#clashoo_}"
|
||||
v="${v%_${ARCH}.ipk}"
|
||||
printf '%s\n' "$v" | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/'
|
||||
;;
|
||||
clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^clashoo_\(.*\)_[^_][^_]*\.ipk$/\1/p' | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/' ;;
|
||||
luci-app-clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^luci-app-clashoo_\(.*\)_all\.ipk$/\1/p' ;;
|
||||
luci-i18n-clashoo-zh-cn_*.ipk) printf '%s\n' "$file" | sed -n 's/^luci-i18n-clashoo-zh-cn_\(.*\)_all\.ipk$/\1/p' ;;
|
||||
clashoo-*.apk)
|
||||
v="$(printf '%s\n' "$file" | sed -n -e 's/^clashoo-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^clashoo-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1)"
|
||||
printf '%s\n' "$v" | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/'
|
||||
;;
|
||||
luci-app-clashoo-*.apk) printf '%s\n' "$file" | sed -n -e 's/^luci-app-clashoo-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^luci-app-clashoo-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1 ;;
|
||||
luci-i18n-clashoo-zh-cn-*.apk) printf '%s\n' "$file" | sed -n -e 's/^luci-i18n-clashoo-zh-cn-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^luci-i18n-clashoo-zh-cn-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1 ;;
|
||||
*) printf '%s\n' "$file" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_bundle_urls() {
|
||||
PM="$(detect_manager)"
|
||||
[ "$PM" != "unsupported" ] || return 1
|
||||
ARCH="$(detect_arch "$PM")"
|
||||
[ -n "$ARCH" ] || return 1
|
||||
EXT="ipk"
|
||||
[ "$PM" = "apk" ] && EXT="apk"
|
||||
|
||||
CORE_URL=""
|
||||
LUCI_URL=""
|
||||
I18N_URL=""
|
||||
SOURCE_LABEL=""
|
||||
|
||||
SDK_CANDIDATES="$(build_sdk_candidates "$PM")"
|
||||
for sdk in $SDK_CANDIDATES; do
|
||||
if load_manifest_urls "$sdk" "$ARCH"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
load_github_urls "$ARCH" "$EXT"
|
||||
}
|
||||
|
||||
backup_config() {
|
||||
ts="$(date '+%Y%m%d-%H%M%S')"
|
||||
BACKUP_DIR="/etc/clashoo/backup/component-upgrade-${ts}"
|
||||
mkdir -p "$BACKUP_DIR/etc-config" "$BACKUP_DIR/etc-clashoo" >/dev/null 2>&1 || return 1
|
||||
[ -r /etc/config/clashoo ] && cp -a /etc/config/clashoo "$BACKUP_DIR/etc-config/clashoo"
|
||||
# 只备份用户配置;Model.bin 等可重新下载的大文件不入备份,避免每次更新
|
||||
# 复制 ~5MB 的同一模型,把备份目录撑大、堆满本就紧张的根分区。
|
||||
for path in \
|
||||
/etc/clashoo/config.yaml \
|
||||
/etc/clashoo/config.json \
|
||||
/etc/clashoo/*.yaml \
|
||||
/etc/clashoo/*.yml \
|
||||
/etc/clashoo/*.json
|
||||
do
|
||||
[ -e "$path" ] || continue
|
||||
cp -a "$path" "$BACKUP_DIR/etc-clashoo/" 2>/dev/null || true
|
||||
done
|
||||
log "已备份配置:${BACKUP_DIR}"
|
||||
|
||||
# 只保留最近 1 个备份,删除更早的,避免无限堆积占满磁盘。
|
||||
ls -dt /etc/clashoo/backup/component-upgrade-* 2>/dev/null | tail -n +2 | while read -r _old; do
|
||||
[ -n "$_old" ] && rm -rf "$_old" 2>/dev/null
|
||||
done
|
||||
}
|
||||
|
||||
restore_config_backup() {
|
||||
[ -n "${BACKUP_DIR:-}" ] || return 0
|
||||
[ -r "$BACKUP_DIR/etc-config/clashoo" ] && cp -a "$BACKUP_DIR/etc-config/clashoo" /etc/config/clashoo
|
||||
if [ -d "$BACKUP_DIR/etc-clashoo" ]; then
|
||||
mkdir -p /etc/clashoo
|
||||
cp -a "$BACKUP_DIR/etc-clashoo/." /etc/clashoo/ 2>/dev/null || true
|
||||
fi
|
||||
uci commit clashoo >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
clashoo_was_running() {
|
||||
/etc/init.d/clashoo status >/dev/null 2>&1 && return 0
|
||||
/etc/init.d/sing-box status >/dev/null 2>&1 && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
restart_web_stack() {
|
||||
# reload (not restart) rpcd so the LuCI login session survives a plugin
|
||||
# update — rpcd restart drops every ubus session and forces a re-login
|
||||
# (issue #12). reload still picks up the new ucode RPC backend. uhttpd needs
|
||||
# nothing: updated static assets are served on the next request.
|
||||
/etc/init.d/rpcd reload >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# which: clashoo(仅核心)/ luci(luci-app + 语言包)。两者拆开,clashoo
|
||||
# 核心更新频率高于 LuCI,分别更新便于定位失败。
|
||||
run_pkg_update() {
|
||||
which="$1"
|
||||
log "正在检查组件包"
|
||||
if ! resolve_bundle_urls; then
|
||||
finish 1 "未找到适配当前设备的组件包"
|
||||
fi
|
||||
log "更新来源:${SOURCE_LABEL}"
|
||||
|
||||
core_ver="$(package_version_from_url "$CORE_URL")"
|
||||
luci_ver="$(package_version_from_url "$LUCI_URL")"
|
||||
i18n_ver=""
|
||||
[ -z "$I18N_URL" ] || i18n_ver="$(package_version_from_url "$I18N_URL")"
|
||||
core_ver_before="$(installed_package_version clashoo)"
|
||||
|
||||
apk_core_ready() {
|
||||
package_installed clashoo && [ "$(installed_package_version clashoo)" = "$core_ver" ] \
|
||||
&& [ -x /etc/init.d/clashoo ] && [ -x /usr/share/clashoo/update/component_update.sh ]
|
||||
}
|
||||
|
||||
apk_luci_ready() {
|
||||
package_installed luci-app-clashoo && [ "$(installed_package_version luci-app-clashoo)" = "$luci_ver" ] \
|
||||
&& { [ -z "$I18N_URL" ] || { package_installed luci-i18n-clashoo-zh-cn \
|
||||
&& [ "$(installed_package_version luci-i18n-clashoo-zh-cn)" = "$i18n_ver" ]; }; } \
|
||||
&& [ -r /usr/share/luci/menu.d/luci-app-clashoo.json ] \
|
||||
&& [ -r /usr/share/rpcd/ucode/luci.clashoo ]
|
||||
}
|
||||
|
||||
apk_bundle_ready() {
|
||||
apk_core_ready && apk_luci_ready
|
||||
}
|
||||
|
||||
apk_update_ready() {
|
||||
if [ "$which" = "clashoo" ]; then
|
||||
apk_bundle_ready
|
||||
else
|
||||
package_installed clashoo && apk_luci_ready
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$which" = "clashoo" ]; then
|
||||
log "目标版本:Clashoo 核心 ${core_ver}"
|
||||
cur_ver="$(installed_package_version clashoo)"
|
||||
if [ "$PM" = "apk" ] && apk_update_ready; then
|
||||
finish 0 "Clashoo 核心已是最新版本"
|
||||
elif [ "$PM" != "apk" ] && [ -n "$core_ver" ] && [ "$cur_ver" = "$core_ver" ]; then
|
||||
finish 0 "Clashoo 核心已是最新版本"
|
||||
fi
|
||||
else
|
||||
log "目标版本:客户端 ${luci_ver}"
|
||||
cur_ver="$(installed_package_version luci-app-clashoo)"
|
||||
if [ "$PM" = "apk" ] && apk_update_ready; then
|
||||
finish 0 "客户端已是最新版本"
|
||||
elif [ "$PM" != "apk" ] && [ -n "$luci_ver" ] && [ "$cur_ver" = "$luci_ver" ]; then
|
||||
finish 0 "客户端已是最新版本"
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
mkdir -p "$TMP_DIR" || finish 1 "创建临时目录失败"
|
||||
|
||||
was_running=0
|
||||
clashoo_was_running && was_running=1
|
||||
backup_config || finish 1 "备份配置失败"
|
||||
apk_core_file=""
|
||||
|
||||
if [ "$PM" = "apk" ]; then
|
||||
log "正在下载组件包"
|
||||
if [ "$which" = "clashoo" ] || ! package_installed clashoo; then
|
||||
ensure_root_space 98304
|
||||
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
|
||||
apk_core_file="$TMP_DIR/core.${EXT}"
|
||||
fi
|
||||
download_package_file "$LUCI_URL" "$TMP_DIR/luci.${EXT}" "luci-app-clashoo"
|
||||
if [ -n "$I18N_URL" ]; then
|
||||
download_package_file "$I18N_URL" "$TMP_DIR/i18n.${EXT}" "语言包"
|
||||
fi
|
||||
elif [ "$which" = "clashoo" ]; then
|
||||
log "正在下载 Clashoo 核心"
|
||||
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
|
||||
else
|
||||
log "正在下载客户端(LuCI + 语言包)"
|
||||
download_package_file "$LUCI_URL" "$TMP_DIR/luci.${EXT}" "luci-app-clashoo"
|
||||
if [ -n "$I18N_URL" ]; then
|
||||
download_package_file "$I18N_URL" "$TMP_DIR/i18n.${EXT}" "语言包"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$PM" != "apk" ] && [ "$which" = "clashoo" ]; then
|
||||
ensure_root_space 98304
|
||||
fi
|
||||
|
||||
if [ "$which" = "clashoo" ]; then
|
||||
log "正在安装 Clashoo 核心"
|
||||
if [ "$PM" = "opkg" ]; then
|
||||
opkg install --force-downgrade "$TMP_DIR/core.${EXT}" >"$TMP_DIR/install.log" 2>&1; rc=$?
|
||||
else
|
||||
apk_bundle_install ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
|
||||
fi
|
||||
else
|
||||
log "正在安装客户端"
|
||||
if [ "$PM" = "opkg" ]; then
|
||||
opkg install --force-downgrade "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
|
||||
else
|
||||
apk_bundle_install ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
if [ "$PM" = "apk" ] && apk_update_ready; then
|
||||
log "apk 报告了其它软件包的错误,本次组件已升级到目标版本"
|
||||
rc=0
|
||||
else
|
||||
log_install_error "$TMP_DIR/install.log"
|
||||
log "安装失败,正在恢复配置备份"
|
||||
restore_config_backup
|
||||
finish "$rc" "组件更新失败,已停止操作,请查看组件更新日志"
|
||||
fi
|
||||
fi
|
||||
|
||||
validation_failed() {
|
||||
log "安装校验失败,正在恢复配置备份"
|
||||
restore_config_backup
|
||||
finish 1 "组件安装校验失败,已停止操作,请查看组件更新日志"
|
||||
}
|
||||
|
||||
if [ "$PM" = "apk" ]; then
|
||||
if ! apk_update_ready; then
|
||||
log "安装校验失败,正在安全重装组件包"
|
||||
if [ -z "$apk_core_file" ] && ! package_installed clashoo; then
|
||||
ensure_root_space 98304
|
||||
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
|
||||
apk_core_file="$TMP_DIR/core.${EXT}"
|
||||
fi
|
||||
apk_bundle_install --force-reinstall ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" \
|
||||
${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >>"$TMP_DIR/install.log" 2>&1; repair_rc=$?
|
||||
if [ "$repair_rc" -ne 0 ]; then
|
||||
log_install_error "$TMP_DIR/install.log"
|
||||
validation_failed
|
||||
fi
|
||||
apk_update_ready || validation_failed
|
||||
fi
|
||||
elif [ "$which" = "clashoo" ]; then
|
||||
package_installed clashoo && [ "$(installed_package_version clashoo)" = "$core_ver" ] \
|
||||
&& [ -x /etc/init.d/clashoo ] && [ -x /usr/share/clashoo/update/component_update.sh ] \
|
||||
|| validation_failed
|
||||
else
|
||||
package_installed luci-app-clashoo && [ "$(installed_package_version luci-app-clashoo)" = "$luci_ver" ] \
|
||||
&& [ -r /usr/share/luci/menu.d/luci-app-clashoo.json ] \
|
||||
&& [ -r /usr/share/rpcd/ucode/luci.clashoo ] \
|
||||
|| validation_failed
|
||||
[ -z "$I18N_URL" ] || package_installed luci-i18n-clashoo-zh-cn || validation_failed
|
||||
fi
|
||||
|
||||
# APK component updates install the LuCI bundle in the same transaction.
|
||||
if [ "$which" = "luci" ] || [ "$PM" = "apk" ]; then
|
||||
log "正在刷新 LuCI 服务"
|
||||
restart_web_stack
|
||||
fi
|
||||
core_ver_after="$(installed_package_version clashoo)"
|
||||
restart_clashoo=0
|
||||
[ "$which" = "clashoo" ] && restart_clashoo=1
|
||||
[ "$PM" = "apk" ] && [ "$core_ver_before" != "$core_ver_after" ] && restart_clashoo=1
|
||||
if [ "$restart_clashoo" -eq 1 ] && [ "$was_running" -eq 1 ] && [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then
|
||||
log "Clashoo 原本运行中,正在重启服务"
|
||||
sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1 || /etc/init.d/clashoo restart >/dev/null 2>&1 || true
|
||||
elif [ "$restart_clashoo" -eq 1 ]; then
|
||||
log "Clashoo 原本未运行,保持停止状态"
|
||||
fi
|
||||
|
||||
finish 0 "组件更新完成"
|
||||
}
|
||||
|
||||
run_core_update() {
|
||||
dcore="$1"
|
||||
label="$2"
|
||||
log "正在更新 ${label}"
|
||||
# Pass the target as an arg so core_download refreshes only that binary
|
||||
# without switching the active kernel. It restarts only if the target is the
|
||||
# currently-running core (see core_download.sh finalize). Switching kernels
|
||||
# stays a separate, explicit user action.
|
||||
touch /var/run/core_update
|
||||
sh /usr/share/clashoo/update/core_download.sh "$dcore" >>/tmp/clash_update.txt 2>&1
|
||||
rc=$?
|
||||
rm -f /var/run/core_update
|
||||
[ "$rc" -eq 0 ] || finish "$rc" "${label} 更新失败"
|
||||
finish 0 "${label} 更新完成"
|
||||
}
|
||||
|
||||
run_lgbm_update() {
|
||||
log "正在更新 Smart LightGBM 模型"
|
||||
sh /usr/share/clashoo/update/lgbm_update.sh >/tmp/lgbm_update.log 2>&1
|
||||
rc=$?
|
||||
tail -5 /tmp/lgbm_update.log 2>/dev/null | while IFS= read -r line; do
|
||||
[ -n "$line" ] && log "$line"
|
||||
done
|
||||
[ "$rc" -eq 0 ] || finish "$rc" "LightGBM 模型更新失败"
|
||||
finish 0 "LightGBM 模型更新完成"
|
||||
}
|
||||
|
||||
run_china_update() {
|
||||
log "正在更新大陆白名单"
|
||||
sh /usr/share/clashoo/update/update_china_ip.sh >>/tmp/clash_update.txt 2>&1
|
||||
rc=$?
|
||||
tail -6 /tmp/clash_update.txt 2>/dev/null | while IFS= read -r line; do
|
||||
[ -n "$line" ] && printf '%s\n' "$line" | grep -q '白名单' && log "$line"
|
||||
done
|
||||
[ "$rc" -eq 0 ] || finish "$rc" "大陆白名单更新失败"
|
||||
finish 0 "大陆白名单更新完成"
|
||||
}
|
||||
|
||||
run_geoip_update() {
|
||||
log "正在更新 GeoIP / GeoSite"
|
||||
touch /var/run/geoip_update
|
||||
sh /usr/share/clashoo/update/geoip.sh >/tmp/geoip_update.txt 2>&1
|
||||
rc=$?
|
||||
rm -f /var/run/geoip_update
|
||||
tail -8 /tmp/geoip_update.txt 2>/dev/null | while IFS= read -r line; do
|
||||
[ -n "$line" ] && log "$line"
|
||||
done
|
||||
[ "$rc" -eq 0 ] || finish "$rc" "GeoIP / GeoSite 更新失败"
|
||||
finish 0 "GeoIP / GeoSite 更新完成"
|
||||
}
|
||||
|
||||
COMPONENT="${1:-}"
|
||||
VARIANT="${2:-}" # mihomo/singbox: stable | alpha
|
||||
[ -n "$COMPONENT" ] || {
|
||||
echo "usage: $0 <clashoo|luci|mihomo|singbox|smart|lgbm|china|geoip> [stable|alpha]"
|
||||
exit 2
|
||||
}
|
||||
|
||||
mkdir -p "$(dirname "$RUN_FILE")" >/dev/null 2>&1
|
||||
printf '%s\n' "$COMPONENT" >"$RUN_FILE"
|
||||
STATUS="running"
|
||||
log "组件更新任务启动:${COMPONENT}${VARIANT:+ ($VARIANT)}"
|
||||
|
||||
case "$COMPONENT" in
|
||||
clashoo) run_pkg_update clashoo ;;
|
||||
luci) run_pkg_update luci ;;
|
||||
mihomo)
|
||||
if [ "$VARIANT" = "alpha" ]; then run_core_update 3 "mihomo Alpha 版"
|
||||
else run_core_update 2 "mihomo 稳定版"; fi ;;
|
||||
singbox)
|
||||
if [ "$VARIANT" = "alpha" ]; then run_core_update 5 "sing-box Alpha 版"
|
||||
else run_core_update 4 "sing-box 稳定版"; fi ;;
|
||||
smart) run_core_update 1 "mihomo Smart 版" ;;
|
||||
lgbm) run_lgbm_update ;;
|
||||
china) run_china_update ;;
|
||||
geoip) run_geoip_update ;;
|
||||
*) finish 2 "未知组件:${COMPONENT}" ;;
|
||||
esac
|
||||
+1036
File diff suppressed because it is too large
Load Diff
+267
@@ -0,0 +1,267 @@
|
||||
#!/bin/sh
|
||||
|
||||
LOG_FILE="/tmp/geoip_update.txt"
|
||||
TMP_DIR="/tmp/clash_geoip_$$"
|
||||
MMDB_MIN_SIZE=2000000
|
||||
GEOSITE_MIN_SIZE=2000000
|
||||
|
||||
geoip_source="$(uci -q get clashoo.config.geoip_source 2>/dev/null)"
|
||||
license_key="$(uci -q get clashoo.config.license_key 2>/dev/null)"
|
||||
|
||||
cfg_mmdb_url="$(uci -q get clashoo.config.geoip_mmdb_url 2>/dev/null)"
|
||||
cfg_geosite_url="$(uci -q get clashoo.config.geosite_url 2>/dev/null)"
|
||||
cfg_geoip_dat_url="$(uci -q get clashoo.config.geoip_dat_url 2>/dev/null)"
|
||||
cfg_geoip_asn_url="$(uci -q get clashoo.config.geoip_asn_url 2>/dev/null)"
|
||||
|
||||
DEFAULT_MMDB_URL="https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
|
||||
DEFAULT_GEOSITE_URL="https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/release/geosite.dat"
|
||||
DEFAULT_GEOIP_DAT_URL="https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/release/geoip.dat"
|
||||
DEFAULT_GEOIP_ASN_URL="https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb"
|
||||
|
||||
LOYALSOLDIER_MMDB_URL="https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
|
||||
LOYALSOLDIER_GEOSITE_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat"
|
||||
LOYALSOLDIER_GEOIP_DAT_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat"
|
||||
|
||||
# v2fly ships no Country.mmdb — borrow Loyalsoldier's for the mmdb slot
|
||||
V2FLY_GEOSITE_URL="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"
|
||||
V2FLY_GEOIP_DAT_URL="https://github.com/v2fly/geoip/releases/latest/download/geoip.dat"
|
||||
|
||||
log() {
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR" >/dev/null 2>&1
|
||||
rm -f /var/run/geoip_update >/dev/null 2>&1
|
||||
}
|
||||
|
||||
config_needs_geosite() {
|
||||
_cfg="/etc/clashoo/config.yaml"
|
||||
[ -f "$_cfg" ] || return 1
|
||||
grep -Eq "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,|^[[:space:]]*geosite:[[:space:]]*|^[[:space:]]*'geosite:|^[[:space:]]*-[[:space:]]*[\"']?geosite:" "$_cfg"
|
||||
}
|
||||
|
||||
# Shared port detection (handles smart core + sing-box profile port); keep the
|
||||
# local anonymity probe so we skip a coexisting plugin's auth-locked proxy.
|
||||
. /usr/share/clashoo/update/proxy_lib.sh
|
||||
detect_proxy() {
|
||||
proxy="$(clashoo_detect_proxy)"
|
||||
[ -n "$proxy" ] || return 0
|
||||
# Only use it if it actually proxies anonymously. A coexisting plugin
|
||||
# (e.g. OpenClash on the same 7890) may hold this port behind auth and
|
||||
# answer HTTP 407 — in that case fall through to the mirrors instead.
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsS --proxy "$proxy" --connect-timeout 3 --max-time 5 -o /dev/null \
|
||||
http://www.gstatic.com/generate_204 >/dev/null 2>&1 || return 0
|
||||
fi
|
||||
echo "$proxy"
|
||||
}
|
||||
|
||||
_fetch() {
|
||||
url="$1"; target="$2"; proxy="$3"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "$proxy" ]; then
|
||||
curl --proxy "$proxy" -fsSL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$url" -o "$target"
|
||||
else
|
||||
curl -fsSL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$url" -o "$target"
|
||||
fi
|
||||
return $?
|
||||
fi
|
||||
wget -q --timeout=120 --no-check-certificate --user-agent="Clash/OpenWRT" "$url" -O "$target"
|
||||
}
|
||||
|
||||
# 多源拉取:本机代理 → 国内镜像(gh-proxy / ghfast / jsdelivr)→ 原 URL 兜底
|
||||
download_to() {
|
||||
url="$1"; target="$2"
|
||||
[ -z "$url" ] && return 1
|
||||
|
||||
proxy="$(detect_proxy)"
|
||||
|
||||
if [ -n "$proxy" ]; then
|
||||
_fetch "$url" "$target" "$proxy" && return 0
|
||||
fi
|
||||
|
||||
case "$url" in
|
||||
https://raw.githubusercontent.com/*)
|
||||
rest="${url#https://raw.githubusercontent.com/}"
|
||||
owner="${rest%%/*}"; r1="${rest#*/}"
|
||||
repo="${r1%%/*}"; r2="${r1#*/}"
|
||||
branch="${r2%%/*}"; path="${r2#*/}"
|
||||
for m in \
|
||||
"https://gh-proxy.com/${url}" \
|
||||
"https://ghfast.top/${url}" \
|
||||
"https://cdn.jsdelivr.net/gh/${owner}/${repo}@${branch}/${path}"; do
|
||||
_fetch "$m" "$target" "" && return 0
|
||||
done
|
||||
;;
|
||||
https://github.com/*)
|
||||
for m in \
|
||||
"https://gh-proxy.com/${url}" \
|
||||
"https://ghfast.top/${url}"; do
|
||||
_fetch "$m" "$target" "" && return 0
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
_fetch "$url" "$target" ""
|
||||
}
|
||||
|
||||
download_optional() {
|
||||
url="$1"
|
||||
target="$2"
|
||||
name="$3"
|
||||
tmp_target="${TMP_DIR}/$(basename "$target").tmp"
|
||||
if [ -z "$url" ]; then
|
||||
log "$name skip (url empty)"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
if download_to "$url" "$tmp_target"; then
|
||||
mv -f "$tmp_target" "$target" >/dev/null 2>&1 || return 1
|
||||
chmod 644 "$target" >/dev/null 2>&1
|
||||
log "$name updated"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
log "$name update failed"
|
||||
return 0
|
||||
}
|
||||
|
||||
download_geosite() {
|
||||
url="$1"
|
||||
tmp_target="${TMP_DIR}/GeoSite.dat.tmp"
|
||||
size=0
|
||||
if [ -z "$url" ]; then
|
||||
log "GeoSite.dat skip (url empty)"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
if ! download_to "$url" "$tmp_target"; then
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
log "GeoSite.dat update failed"
|
||||
return 0
|
||||
fi
|
||||
size=$(wc -c <"$tmp_target" 2>/dev/null)
|
||||
if [ -z "$size" ] || [ "$size" -lt "$GEOSITE_MIN_SIZE" ]; then
|
||||
log "GeoSite.dat download invalid or incomplete (${size:-0} bytes)"
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
return 0
|
||||
fi
|
||||
mv -f "$tmp_target" /etc/clashoo/GeoSite.dat >/dev/null 2>&1 || return 1
|
||||
cp -f /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1 || true
|
||||
chmod 644 /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1
|
||||
log "GeoSite.dat updated"
|
||||
return 0
|
||||
}
|
||||
|
||||
download_mmdb() {
|
||||
url="$1"
|
||||
tmp_target="${TMP_DIR}/Country.mmdb.tmp"
|
||||
size=0
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
if ! download_to "$url" "$tmp_target"; then
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
size=$(wc -c <"$tmp_target" 2>/dev/null)
|
||||
if [ -z "$size" ] || [ "$size" -lt "$MMDB_MIN_SIZE" ]; then
|
||||
log "Country.mmdb download invalid or incomplete (${size:-0} bytes)"
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
mv -f "$tmp_target" /etc/clashoo/Country.mmdb >/dev/null 2>&1 || return 1
|
||||
chmod 644 /etc/clashoo/Country.mmdb >/dev/null 2>&1
|
||||
return 0
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
mkdir -p "$TMP_DIR" /etc/clashoo >/dev/null 2>&1
|
||||
|
||||
rm -f /var/run/geoip_down_complete >/dev/null 2>&1
|
||||
: > /var/run/geoip_update
|
||||
: > "$LOG_FILE"
|
||||
log "GeoIP 更新任务启动"
|
||||
|
||||
mmdb_url=""
|
||||
geosite_url=""
|
||||
geoip_dat_url=""
|
||||
geoip_asn_url=""
|
||||
|
||||
case "$geoip_source" in
|
||||
1)
|
||||
if [ -z "$license_key" ]; then
|
||||
log "MaxMind source selected but license key is empty"
|
||||
exit 1
|
||||
fi
|
||||
log "Updating Country.mmdb from MaxMind"
|
||||
if ! download_to "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${license_key}&suffix=tar.gz" "$TMP_DIR/geoip.tar.gz"; then
|
||||
log "MaxMind download failed"
|
||||
exit 1
|
||||
fi
|
||||
if ! tar zxf "$TMP_DIR/geoip.tar.gz" -C "$TMP_DIR" >/dev/null 2>&1; then
|
||||
log "MaxMind archive extract failed"
|
||||
exit 1
|
||||
fi
|
||||
mmdb_file="$(ls "$TMP_DIR"/GeoLite2-Country_*/GeoLite2-Country.mmdb 2>/dev/null | head -n 1)"
|
||||
if [ -z "$mmdb_file" ] || [ ! -f "$mmdb_file" ]; then
|
||||
log "MaxMind Country.mmdb not found in archive"
|
||||
exit 1
|
||||
fi
|
||||
cp -f "$mmdb_file" /etc/clashoo/Country.mmdb
|
||||
chmod 644 /etc/clashoo/Country.mmdb >/dev/null 2>&1
|
||||
log "Country.mmdb updated"
|
||||
;;
|
||||
3)
|
||||
# Loyalsoldier (default)
|
||||
mmdb_url="$LOYALSOLDIER_MMDB_URL"
|
||||
geosite_url="$LOYALSOLDIER_GEOSITE_URL"
|
||||
geoip_dat_url="$LOYALSOLDIER_GEOIP_DAT_URL"
|
||||
geoip_asn_url="$DEFAULT_GEOIP_ASN_URL"
|
||||
;;
|
||||
5)
|
||||
# v2fly (mmdb borrowed from Loyalsoldier)
|
||||
mmdb_url="$LOYALSOLDIER_MMDB_URL"
|
||||
geosite_url="$V2FLY_GEOSITE_URL"
|
||||
geoip_dat_url="$V2FLY_GEOIP_DAT_URL"
|
||||
geoip_asn_url="$DEFAULT_GEOIP_ASN_URL"
|
||||
;;
|
||||
4)
|
||||
# custom — asn falls back to default when left blank
|
||||
mmdb_url="$cfg_mmdb_url"
|
||||
geosite_url="$cfg_geosite_url"
|
||||
geoip_dat_url="$cfg_geoip_dat_url"
|
||||
geoip_asn_url="${cfg_geoip_asn_url:-$DEFAULT_GEOIP_ASN_URL}"
|
||||
;;
|
||||
*)
|
||||
mmdb_url="${cfg_mmdb_url:-$DEFAULT_MMDB_URL}"
|
||||
geosite_url="${cfg_geosite_url:-$DEFAULT_GEOSITE_URL}"
|
||||
geoip_dat_url="${cfg_geoip_dat_url:-$DEFAULT_GEOIP_DAT_URL}"
|
||||
geoip_asn_url="${cfg_geoip_asn_url:-$DEFAULT_GEOIP_ASN_URL}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$geoip_source" != "1" ]; then
|
||||
log "Updating Country.mmdb"
|
||||
if ! download_mmdb "$mmdb_url"; then
|
||||
log "Country.mmdb download failed"
|
||||
exit 1
|
||||
fi
|
||||
log "Country.mmdb updated"
|
||||
|
||||
if config_needs_geosite; then
|
||||
download_geosite "$geosite_url"
|
||||
else
|
||||
log "GeoSite.dat skip (no geosite reference in config)"
|
||||
fi
|
||||
download_optional "$geoip_dat_url" /etc/clashoo/geoip.dat "geoip.dat"
|
||||
download_optional "$geoip_asn_url" /etc/clashoo/GeoLite2-ASN.mmdb "GeoLite2-ASN.mmdb"
|
||||
fi
|
||||
|
||||
touch /var/run/geoip_down_complete >/dev/null 2>&1
|
||||
|
||||
if pidof mihomo >/dev/null 2>&1 || pidof clash-meta >/dev/null 2>&1; then
|
||||
log "GeoIP update completed, apply on next Clashoo restart"
|
||||
fi
|
||||
|
||||
log "GeoIP update completed"
|
||||
exit 0
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
#!/bin/sh
|
||||
|
||||
MODEL_PATH="/etc/clashoo/Model.bin"
|
||||
TMP_PATH="/tmp/clash_Model.bin"
|
||||
LOG_FILE="/tmp/lgbm_update.log"
|
||||
|
||||
log() {
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
: > "$LOG_FILE"
|
||||
|
||||
DOWNLOAD_URL=$(uci get clashoo.config.smart_lgbm_url 2>/dev/null)
|
||||
[ -z "$DOWNLOAD_URL" ] && DOWNLOAD_URL="https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin"
|
||||
|
||||
log "Start downloading LightGBM model from: $DOWNLOAD_URL"
|
||||
|
||||
mkdir -p /etc/clashoo
|
||||
|
||||
rm -f "$TMP_PATH" 2>/dev/null
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$DOWNLOAD_URL" -o "$TMP_PATH" 2>/dev/null
|
||||
dl_rc=$?
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q --timeout=30 --tries=2 --no-check-certificate -U "Clash/OpenWRT" "$DOWNLOAD_URL" -O "$TMP_PATH" 2>/dev/null
|
||||
dl_rc=$?
|
||||
else
|
||||
log "No curl or wget found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$dl_rc" -ne 0 ] || [ ! -s "$TMP_PATH" ]; then
|
||||
log "Download failed (rc=$dl_rc)"
|
||||
rm -f "$TMP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$MODEL_PATH" ] && cmp -s "$TMP_PATH" "$MODEL_PATH"; then
|
||||
log "Model unchanged, no update needed"
|
||||
rm -f "$TMP_PATH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mv "$TMP_PATH" "$MODEL_PATH" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log "Failed to install model to $MODEL_PATH"
|
||||
rm -f "$TMP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "LightGBM model updated successfully"
|
||||
exit 0
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
|
||||
STATE_DIR="${CLASHOO_STATE_DIR:-/usr/share/clashbackup}"
|
||||
UPDATE_SCRIPT="${CLASHOO_UPDATE_SCRIPT:-/usr/share/clashoo/update/update_all.sh}"
|
||||
LOG_FILE="${CLASHOO_LOG_FILE:-/usr/share/clashoo/clashoo.txt}"
|
||||
LOCK_DIR="${CLASHOO_LOCK_DIR:-/tmp/clashoo_maintenance.lock}"
|
||||
NOW="${CLASHOO_NOW:-$(date +%s)}"
|
||||
|
||||
stamp() {
|
||||
tmp="$STATE_DIR/$1.tmp.$$"
|
||||
printf 'last_run=%s\n' "$NOW" >"$tmp" && mv -f "$tmp" "$STATE_DIR/$1"
|
||||
}
|
||||
|
||||
due() {
|
||||
hours="$(uci -q get "clashoo.config.$2")"
|
||||
echo "$hours" | grep -Eq '^[0-9]+$' && [ "$hours" -ge 1 ] || hours=12
|
||||
last="$(sed -n 's/^last_run=//p' "$STATE_DIR/$1" 2>/dev/null | head -1)"
|
||||
if ! echo "$last" | grep -Eq '^[0-9]+$' || [ "$last" -gt "$NOW" ]; then
|
||||
stamp "$1"
|
||||
return 1
|
||||
fi
|
||||
[ $((NOW - last)) -ge $((hours * 3600)) ]
|
||||
}
|
||||
|
||||
mkdir -p "$STATE_DIR" || exit 0
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
|
||||
trap 'rmdir "$LOCK_DIR" >/dev/null 2>&1' EXIT INT TERM
|
||||
|
||||
if [ "$(uci -q get clashoo.config.auto_update)" = "1" ] && due rule_update.status auto_update_time; then
|
||||
CLASHOO_STATE_DIR="$STATE_DIR" sh "$UPDATE_SCRIPT" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [ "$(uci -q get clashoo.config.auto_clear_log)" = "1" ] && due log_cleanup.status clear_time; then
|
||||
: >"$LOG_FILE"
|
||||
stamp log_cleanup.status
|
||||
fi
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
#!/bin/sh
|
||||
|
||||
REAL_LOG="/usr/share/clashoo/clashoo_real.txt"
|
||||
panel="$1"
|
||||
lang="$(uci -q get luci.main.lang 2>/dev/null)"
|
||||
STATE_FILE="/tmp/clash_panel_download_state"
|
||||
RUN_FILE="/var/run/panel_downloading"
|
||||
DASHBOARD_LINK="/etc/clashoo/dashboard"
|
||||
|
||||
[ -n "$panel" ] || panel="$(uci -q get clashoo.config.dashboard_panel 2>/dev/null)"
|
||||
[ -n "$panel" ] || panel="zashboard"
|
||||
|
||||
case "$panel" in
|
||||
metacubexd)
|
||||
URLS="https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip"
|
||||
TARGET_DIR="/etc/clashoo/dashboard-metacubexd"
|
||||
;;
|
||||
yacd)
|
||||
URLS="https://github.com/MetaCubeX/Yacd-meta/archive/refs/heads/gh-pages.zip https://github.com/haishanh/yacd/archive/refs/heads/gh-pages.zip"
|
||||
TARGET_DIR="/etc/clashoo/dashboard-yacd"
|
||||
;;
|
||||
zashboard)
|
||||
URLS="https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
TARGET_DIR="/etc/clashoo/dashboard-zashboard"
|
||||
;;
|
||||
razord)
|
||||
URLS="https://github.com/MetaCubeX/Razord-meta/archive/refs/heads/gh-pages.zip https://github.com/ayanamist/clash-dashboard/archive/refs/heads/gh-pages.zip"
|
||||
TARGET_DIR="/etc/clashoo/dashboard-razord"
|
||||
;;
|
||||
*)
|
||||
URLS="https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
TARGET_DIR="/etc/clashoo/dashboard-zashboard"
|
||||
panel="zashboard"
|
||||
;;
|
||||
esac
|
||||
|
||||
TMP_ROOT="/tmp/clash_panel_${panel}_$$"
|
||||
ZIP_FILE="$TMP_ROOT/panel.zip"
|
||||
UNPACK_DIR="$TMP_ROOT/unpack"
|
||||
|
||||
UPDATE_LOG="/tmp/clash_update.txt"
|
||||
|
||||
update_log() {
|
||||
[ -n "$1" ] || return 0
|
||||
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
|
||||
}
|
||||
|
||||
log_msg() {
|
||||
if [ "$lang" = "en" ]; then
|
||||
echo "$1" >"$REAL_LOG"
|
||||
[ "$3" = "1" ] || update_log "$1"
|
||||
else
|
||||
echo "$2" >"$REAL_LOG"
|
||||
[ "$3" = "1" ] || update_log "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
set_state() {
|
||||
state="$1"
|
||||
msg="$2"
|
||||
echo "${state}:${panel}:${msg}" >"$STATE_FILE"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_ROOT" >/dev/null 2>&1
|
||||
rm -f "$RUN_FILE" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Route the panel download through the running core in kernel-only mode
|
||||
# (shared logic in proxy_lib.sh). Normal mode returns empty -> TPROXY.
|
||||
. /usr/share/clashoo/update/proxy_lib.sh
|
||||
detect_proxy() { clashoo_detect_proxy; }
|
||||
|
||||
download_zip() {
|
||||
proxy="$(detect_proxy)"
|
||||
for url in $URLS; do
|
||||
if [ -n "$proxy" ] && command -v curl >/dev/null 2>&1 &&
|
||||
curl -fsSL --connect-timeout 15 --max-time 300 --retry 1 \
|
||||
--proxy "$proxy" -A "Clash/OpenWRT" "$url" -o "$ZIP_FILE"; then
|
||||
return 0
|
||||
fi
|
||||
if wget -q --timeout=60 --no-check-certificate --user-agent="Clash/OpenWRT" "$url" -O "$ZIP_FILE"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_zip() {
|
||||
if command -v unzip >/dev/null 2>&1; then
|
||||
unzip -oq "$ZIP_FILE" -d "$UNPACK_DIR" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
if command -v busybox >/dev/null 2>&1 && busybox --list 2>/dev/null | grep -qx "unzip"; then
|
||||
busybox unzip -oq "$ZIP_FILE" -d "$UNPACK_DIR" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
if command -v bsdtar >/dev/null 2>&1; then
|
||||
bsdtar -xf "$ZIP_FILE" -C "$UNPACK_DIR" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
find_web_root() {
|
||||
if [ -f "$UNPACK_DIR/index.html" ]; then
|
||||
echo "$UNPACK_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
index_file="$(find "$UNPACK_DIR" -type f -name index.html 2>/dev/null | head -n 1)"
|
||||
if [ -n "$index_file" ]; then
|
||||
dirname "$index_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
activate_panel() {
|
||||
rm -rf "$DASHBOARD_LINK" >/dev/null 2>&1
|
||||
ln -s "$TARGET_DIR" "$DASHBOARD_LINK" >/dev/null 2>&1 || cp -a "$TARGET_DIR" "$DASHBOARD_LINK" >/dev/null 2>&1
|
||||
rm -rf /www/luci-static/yacd >/dev/null 2>&1
|
||||
if [ "$panel" = "yacd" ]; then
|
||||
ln -s "$TARGET_DIR" /www/luci-static/yacd >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
mkdir -p "$UNPACK_DIR" "$TARGET_DIR" >/dev/null 2>&1
|
||||
touch "$RUN_FILE"
|
||||
set_state "downloading" "开始下载"
|
||||
|
||||
log_msg "Downloading dashboard panel..." "正在下载面板..."
|
||||
if ! download_zip; then
|
||||
set_state "error" "下载失败"
|
||||
log_msg "Dashboard panel download failed" "面板下载失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
extract_zip
|
||||
extract_rc=$?
|
||||
if [ "$extract_rc" -ne 0 ]; then
|
||||
if [ "$extract_rc" -eq 2 ]; then
|
||||
set_state "error" "缺少解压工具"
|
||||
log_msg "Panel unzip tool not found" "缺少解压工具,无法安装面板"
|
||||
else
|
||||
set_state "error" "解压失败"
|
||||
log_msg "Dashboard panel unzip failed" "面板解压失败"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SRC_DIR="$(find_web_root)"
|
||||
if [ -z "$SRC_DIR" ] || [ ! -f "$SRC_DIR/index.html" ]; then
|
||||
set_state "error" "文件无效"
|
||||
log_msg "Dashboard panel content invalid" "面板文件无效"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$TARGET_DIR"/* >/dev/null 2>&1
|
||||
cp -a "$SRC_DIR"/. "$TARGET_DIR"/ >/dev/null 2>&1
|
||||
|
||||
activate_panel
|
||||
|
||||
uci set clashoo.config.dashboard_panel="$panel" >/dev/null 2>&1
|
||||
uci commit clashoo >/dev/null 2>&1
|
||||
|
||||
set_state "success" "安装成功"
|
||||
log_msg "Dashboard panel installed" "面板安装完成"
|
||||
sleep 1
|
||||
log_msg "Clashoo" "Clashoo" 1
|
||||
exit 0
|
||||
@@ -0,0 +1,65 @@
|
||||
# shellcheck shell=sh
|
||||
# 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 -> /etc/sing-box/config.json 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)
|
||||
# Clashoo owns this service only after it has configured the
|
||||
# packaged sing-box init script for its own runtime config.
|
||||
[ "$(uci -q get sing-box.main.conffile 2>/dev/null)" = "/etc/sing-box/config.json" ] || return 1
|
||||
_cdp_service="sing-box"
|
||||
_cdp_filter='@["sing-box"].instances.*.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
|
||||
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 /etc/sing-box/config.json \
|
||||
-e '@.inbounds[@.type="mixed"].listen_port' 2>/dev/null | head -n 1)"
|
||||
else
|
||||
# prefer mixed-port (HTTP+SOCKS) over plain port; a single two-pattern
|
||||
# sed would return whichever appears first by line order, so loop by key
|
||||
for _cdp_key in mixed-port port socks-port; do
|
||||
_cdp_port="$(sed -n "s/^[[:space:]]*${_cdp_key}:[[:space:]]*\([0-9][0-9]*\).*/\1/p" \
|
||||
/etc/clashoo/config.yaml 2>/dev/null | head -n 1)"
|
||||
[ -n "$_cdp_port" ] && break
|
||||
done
|
||||
fi
|
||||
[ -n "$_cdp_port" ] || _cdp_port="$(uci -q get clashoo.config.mixed_port 2>/dev/null)"
|
||||
[ -n "$_cdp_port" ] || return 0
|
||||
printf 'http://127.0.0.1:%s' "$_cdp_port"
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
#!/bin/sh
|
||||
|
||||
SUB_DIR="${CLASHOO_SUB_DIR:-/usr/share/clashoo/config/sub}"
|
||||
SINGBOX_DIR="${CLASHOO_SINGBOX_DIR:-/usr/share/clashoo/config/singbox}"
|
||||
BACKUP_DIR="${CLASHOO_BACKUP_DIR:-/usr/share/clashbackup}"
|
||||
TEMPLATE_DIR="${CLASHOO_TEMPLATE_DIR:-/usr/share/clashoo/config/custom}"
|
||||
LIST_FILE="${CLASHOO_LIST_FILE:-$BACKUP_DIR/confit_list.conf}"
|
||||
BIND_FILE="${CLASHOO_BIND_FILE:-$BACKUP_DIR/template_bindings.conf}"
|
||||
STATUS_FILE="${CLASHOO_STATUS_FILE:-$BACKUP_DIR/subscription_update.status}"
|
||||
LOCK_DIR="${CLASHOO_LOCK_DIR:-/tmp/clashoo_subscription_update.lock}"
|
||||
UPDATE_LOG="${CLASHOO_UPDATE_LOG:-/tmp/clash_update.txt}"
|
||||
SERVICE_CMD="${CLASHOO_SERVICE_CMD:-/etc/init.d/clashoo}"
|
||||
TMP_DIR="${CLASHOO_TMP_DIR:-/tmp}"
|
||||
DEFAULT_SUB_UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
|
||||
DOWNLOAD_ERROR=""
|
||||
|
||||
# Subscriptions are fetched direct first (many airports are domestic / geo-fence
|
||||
# foreign exit IPs). Only used as a last-resort fallback, see download_to.
|
||||
. /usr/share/clashoo/update/proxy_lib.sh
|
||||
|
||||
updated=0
|
||||
unchanged=0
|
||||
failed=0
|
||||
skipped=0
|
||||
restart_needed=0
|
||||
started_at="$(date +%s)"
|
||||
message=""
|
||||
record_status=0
|
||||
[ "$1" = "--all" ] && record_status=1
|
||||
|
||||
log_update() {
|
||||
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
|
||||
}
|
||||
|
||||
write_status() {
|
||||
local tmp
|
||||
mkdir -p "$(dirname "$STATUS_FILE")" >/dev/null 2>&1
|
||||
tmp="${STATUS_FILE}.tmp.$$"
|
||||
cat >"$tmp" <<EOF
|
||||
running=$1
|
||||
last_run=$started_at
|
||||
finished_at=$2
|
||||
updated=$updated
|
||||
unchanged=$unchanged
|
||||
failed=$failed
|
||||
skipped=$skipped
|
||||
message=$message
|
||||
EOF
|
||||
mv "$tmp" "$STATUS_FILE"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$LOCK_DIR" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
if ! mkdir "$LOCK_DIR" >/dev/null 2>&1; then
|
||||
exit 75
|
||||
fi
|
||||
trap cleanup EXIT INT TERM
|
||||
[ "$record_status" = "1" ] && write_status 1 0
|
||||
|
||||
safe_name() {
|
||||
case "$1" in
|
||||
''|*/*|*..*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
extract_host() {
|
||||
printf '%s' "$1" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's#:.*$##' -e 's#.*@##'
|
||||
}
|
||||
|
||||
resolve_via() {
|
||||
local host dns
|
||||
host="$1"
|
||||
dns="$2"
|
||||
nslookup "$host" "$dns" 2>/dev/null | awk '
|
||||
/^Address/ {
|
||||
ip = $NF
|
||||
if (ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ &&
|
||||
ip !~ /^127\./ && ip !~ /^0\./ && ip !~ /^198\.18\./ &&
|
||||
ip != "8.8.8.8" && ip != "1.1.1.1" &&
|
||||
ip != "223.5.5.5" && ip != "119.29.29.29") {
|
||||
print ip
|
||||
exit
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
curl_download() {
|
||||
local url out hdr ua host ip
|
||||
url="$1"
|
||||
out="$2"
|
||||
hdr="$3"
|
||||
ua="$4"
|
||||
host="$5"
|
||||
ip="$6"
|
||||
if [ -n "$host" ] && [ -n "$ip" ]; then
|
||||
curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \
|
||||
--speed-limit 1 --retry 2 -A "$ua" -D "$hdr" -o "$out" \
|
||||
--resolve "$host:443:$ip" --resolve "$host:80:$ip" \
|
||||
-w '%{http_code}' "$url" 2>/dev/null
|
||||
else
|
||||
curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \
|
||||
--speed-limit 1 --retry 2 -A "$ua" -D "$hdr" -o "$out" \
|
||||
-w '%{http_code}' "$url" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
subscription_ua() {
|
||||
local target ua
|
||||
target="$1"
|
||||
ua=""
|
||||
[ -r "${target}.ua" ] && ua="$(sed -n '1p' "${target}.ua" 2>/dev/null)"
|
||||
[ -n "$ua" ] || ua="$(uci -q get clashoo.config.sub_ua 2>/dev/null)"
|
||||
[ -n "$ua" ] || ua="$DEFAULT_SUB_UA"
|
||||
printf '%s' "$ua"
|
||||
}
|
||||
|
||||
save_subscription_ua() {
|
||||
printf '%s\n' "$2" >"${1}.ua"
|
||||
}
|
||||
|
||||
curl_error() {
|
||||
case "$1" in
|
||||
6) printf '%s' "DNS 解析失败" ;;
|
||||
7) printf '%s' "连接失败" ;;
|
||||
28) printf '%s' "连接超时" ;;
|
||||
35) printf '%s' "TLS 握手失败" ;;
|
||||
60) printf '%s' "证书校验失败" ;;
|
||||
*) printf '网络错误(curl rc=%s)' "${1:-unknown}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
download_to() {
|
||||
local url out hdr ua code rc last_rc host dns ip proxy
|
||||
url="$1"
|
||||
out="$2"
|
||||
hdr="$3"
|
||||
ua="$4"
|
||||
[ -n "$ua" ] || ua="$DEFAULT_SUB_UA"
|
||||
DOWNLOAD_ERROR=""
|
||||
|
||||
rm -f "$out" "$hdr" >/dev/null 2>&1
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
code="$(curl_download "$url" "$out" "$hdr" "$ua" "" "")"
|
||||
rc=$?
|
||||
if [ "$rc" -eq 0 ] && [ "$code" = "200" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then
|
||||
DOWNLOAD_ERROR="HTTP ${code}"
|
||||
return 1
|
||||
fi
|
||||
last_rc="$rc"
|
||||
host="$(extract_host "$url")"
|
||||
for dns in 223.5.5.5 119.29.29.29 1.1.1.1 8.8.8.8; do
|
||||
ip="$(resolve_via "$host" "$dns")"
|
||||
[ -n "$ip" ] || continue
|
||||
log_update "DNS 回退:${host} -> ${ip} (@${dns})"
|
||||
code="$(curl_download "$url" "$out" "$hdr" "$ua" "$host" "$ip")"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] && [ "$code" = "200" ] && return 0
|
||||
if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then
|
||||
DOWNLOAD_ERROR="HTTP ${code}"
|
||||
return 1
|
||||
fi
|
||||
last_rc="$rc"
|
||||
done
|
||||
# Last resort: direct + DNS-override both failed, so the source may be
|
||||
# GFW-blocked (e.g. a github-hosted sub). In kernel-only mode try once
|
||||
# through the running core — nothing left to lose, and it won't disturb
|
||||
# any airport that already answered direct.
|
||||
proxy="$(clashoo_detect_proxy)"
|
||||
if [ -n "$proxy" ]; then
|
||||
log_update "代理兜底:通过本地核心重试订阅"
|
||||
code="$(curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \
|
||||
--speed-limit 1 --retry 1 -A "$ua" -D "$hdr" -o "$out" \
|
||||
--proxy "$proxy" -w '%{http_code}' "$url" 2>/dev/null)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] && [ "$code" = "200" ] && return 0
|
||||
if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then
|
||||
DOWNLOAD_ERROR="HTTP ${code}"
|
||||
return 1
|
||||
fi
|
||||
last_rc="$rc"
|
||||
fi
|
||||
DOWNLOAD_ERROR="$(curl_error "$last_rc")"
|
||||
return 1
|
||||
fi
|
||||
wget -q --tries=4 --timeout=20 --user-agent="$ua" "$url" -O "$out"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] && return 0
|
||||
DOWNLOAD_ERROR="网络错误(wget rc=${rc})"
|
||||
return 1
|
||||
}
|
||||
|
||||
update_info() {
|
||||
local hdr target info
|
||||
hdr="$1"
|
||||
target="$2"
|
||||
info="$(grep -i 'subscription-userinfo:' "$hdr" 2>/dev/null | head -1 | \
|
||||
sed 's/^[Ss]ubscription-[Uu]serinfo:[[:space:]]*//' | tr -d '\r')"
|
||||
if [ -n "$info" ]; then
|
||||
printf '%s\n' "$info" >"${target}.info"
|
||||
else
|
||||
rm -f "${target}.info" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
service_running() {
|
||||
[ -n "${CLASHOO_SERVICE_CMD:-}" ] && return 0
|
||||
"$SERVICE_CMD" status >/dev/null 2>&1
|
||||
}
|
||||
|
||||
template_output_name() {
|
||||
local sub tpl
|
||||
sub="$(printf '%s' "$1" | sed -e 's/\.[Yy][Aa][Mm][Ll]$//' -e 's/\.[Yy][Mm][Ll]$//' -e 's/[^A-Za-z0-9._-]/-/g')"
|
||||
tpl="$(printf '%s' "$2" | sed -e 's/\.[Yy][Aa][Mm][Ll]$//' -e 's/\.[Yy][Mm][Ll]$//' -e 's/[^A-Za-z0-9._-]/-/g')"
|
||||
printf '_merged_%s__%s.yaml' "${sub:-sub}" "${tpl:-template}"
|
||||
}
|
||||
|
||||
apply_template() {
|
||||
local name target template merged merged_path use_config
|
||||
name="$1"
|
||||
target="$2"
|
||||
[ -r "$BIND_FILE" ] || return 0
|
||||
[ -x /usr/share/clashoo/update/template_merge.sh ] || return 0
|
||||
template="$(awk -F '#' -v n="$name" '$1==n && ($3=="1" || $3=="true") {print $2; exit}' "$BIND_FILE")"
|
||||
[ -n "$template" ] && [ -r "$TEMPLATE_DIR/$template" ] || return 0
|
||||
merged="$(template_output_name "$name" "$template")"
|
||||
merged_path="$TEMPLATE_DIR/$merged"
|
||||
if sh /usr/share/clashoo/update/template_merge.sh "$target" "$TEMPLATE_DIR/$template" "$merged_path" >/dev/null 2>&1; then
|
||||
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
|
||||
if [ "$use_config" = "$target" ] || [ "$use_config" = "$merged_path" ]; then
|
||||
uci -q set clashoo.config.use_config="$merged_path"
|
||||
uci -q set clashoo.config.config_type='3'
|
||||
uci -q commit clashoo >/dev/null 2>&1
|
||||
restart_needed=1
|
||||
fi
|
||||
else
|
||||
log_update "模板生成失败:${name} <- ${template}"
|
||||
fi
|
||||
}
|
||||
|
||||
update_mihomo() {
|
||||
local name url typ target tmp hdr ua use_config config_type
|
||||
name="$1"
|
||||
url="$2"
|
||||
typ="$3"
|
||||
safe_name "$name" || return 1
|
||||
case "$typ" in clash|meta) ;; *) skipped=$((skipped + 1)); return 0 ;; esac
|
||||
target="$SUB_DIR/$name"
|
||||
[ -f "$target" ] || { skipped=$((skipped + 1)); return 0; }
|
||||
tmp="$TMP_DIR/clashoo_sub_$$.yaml"
|
||||
hdr="$TMP_DIR/clashoo_sub_$$.hdr"
|
||||
ua="$(subscription_ua "$target")"
|
||||
if ! download_to "$url" "$tmp" "$hdr" "$ua"; then
|
||||
failed=$((failed + 1))
|
||||
log_update "更新失败(${DOWNLOAD_ERROR:-下载失败}):$name"
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 1
|
||||
fi
|
||||
if ! grep -Eq '^(proxies|proxy-providers):' "$tmp" 2>/dev/null; then
|
||||
failed=$((failed + 1))
|
||||
log_update "更新失败(无效 Mihomo 配置):$name"
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 1
|
||||
fi
|
||||
if cmp -s "$tmp" "$target"; then
|
||||
update_info "$hdr" "$target"
|
||||
save_subscription_ua "$target" "$ua"
|
||||
unchanged=$((unchanged + 1))
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 0
|
||||
fi
|
||||
if ! mv "$tmp" "$target"; then
|
||||
failed=$((failed + 1))
|
||||
rm -f "$hdr"
|
||||
return 1
|
||||
fi
|
||||
update_info "$hdr" "$target"
|
||||
save_subscription_ua "$target" "$ua"
|
||||
rm -f "$hdr"
|
||||
updated=$((updated + 1))
|
||||
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
|
||||
config_type="$(uci -q get clashoo.config.config_type 2>/dev/null)"
|
||||
[ "$config_type" = "1" ] && [ "$use_config" = "$target" ] && restart_needed=1
|
||||
apply_template "$name" "$target"
|
||||
log_update "更新完成:$name"
|
||||
}
|
||||
|
||||
valid_singbox() {
|
||||
ucode -e 'import { readfile } from "fs"; let c = json(readfile(ARGV[0])); exit(type(c) == "object" && type(c.outbounds) == "array" && length(c.outbounds) > 0 ? 0 : 1);' "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
update_singbox() {
|
||||
local name target url tmp hdr ua active
|
||||
name="$1"
|
||||
safe_name "$name" || return 1
|
||||
target="$SINGBOX_DIR/$name"
|
||||
[ -f "$target" ] && [ -r "$target.url" ] || { skipped=$((skipped + 1)); return 0; }
|
||||
url="$(sed -n '1p' "$target.url")"
|
||||
[ -n "$url" ] || { skipped=$((skipped + 1)); return 0; }
|
||||
tmp="$TMP_DIR/clashoo_sb_$$.json"
|
||||
hdr="$TMP_DIR/clashoo_sb_$$.hdr"
|
||||
ua="$(subscription_ua "$target")"
|
||||
if ! download_to "$url" "$tmp" "$hdr" "$ua"; then
|
||||
failed=$((failed + 1))
|
||||
log_update "更新失败(${DOWNLOAD_ERROR:-下载失败}):$name"
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 1
|
||||
fi
|
||||
if ! valid_singbox "$tmp"; then
|
||||
failed=$((failed + 1))
|
||||
log_update "更新失败(无效 sing-box 配置):$name"
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 1
|
||||
fi
|
||||
if cmp -s "$tmp" "$target"; then
|
||||
update_info "$hdr" "$target"
|
||||
save_subscription_ua "$target" "$ua"
|
||||
unchanged=$((unchanged + 1))
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 0
|
||||
fi
|
||||
if ! mv "$tmp" "$target"; then
|
||||
failed=$((failed + 1))
|
||||
rm -f "$hdr"
|
||||
return 1
|
||||
fi
|
||||
update_info "$hdr" "$target"
|
||||
save_subscription_ua "$target" "$ua"
|
||||
rm -f "$hdr"
|
||||
updated=$((updated + 1))
|
||||
active="$(uci -q get clashoo.config.singbox_active 2>/dev/null)"
|
||||
[ "$active" = "$name" ] && restart_needed=1
|
||||
log_update "更新完成:$name"
|
||||
}
|
||||
|
||||
update_all() {
|
||||
local name url typ url_file
|
||||
if [ -r "$LIST_FILE" ]; then
|
||||
while IFS='#' read -r name url typ _rest; do
|
||||
[ -n "$name" ] && [ -n "$url" ] || continue
|
||||
update_mihomo "$name" "$url" "$typ" || true
|
||||
done <"$LIST_FILE"
|
||||
fi
|
||||
for url_file in "$SINGBOX_DIR"/*.url; do
|
||||
[ -f "$url_file" ] || continue
|
||||
name="$(basename "$url_file" .url)"
|
||||
update_singbox "$name" || true
|
||||
done
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
--all)
|
||||
update_all
|
||||
;;
|
||||
--mihomo)
|
||||
line="$(awk -F '#' -v n="$2" '$1==n {print; exit}' "$LIST_FILE" 2>/dev/null)"
|
||||
[ -n "$line" ] || { failed=1; message="subscription not found"; }
|
||||
if [ -n "$line" ]; then
|
||||
name="$(printf '%s' "$line" | awk -F '#' '{print $1}')"
|
||||
url="$(printf '%s' "$line" | awk -F '#' '{print $2}')"
|
||||
typ="$(printf '%s' "$line" | awk -F '#' '{print $3}')"
|
||||
update_mihomo "$name" "$url" "$typ" || true
|
||||
fi
|
||||
;;
|
||||
--singbox)
|
||||
update_singbox "$2" || true
|
||||
;;
|
||||
*)
|
||||
failed=1
|
||||
message="invalid arguments"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$restart_needed" = "1" ] && service_running; then
|
||||
"$SERVICE_CMD" restart >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
finished_at="$(date +%s)"
|
||||
[ -n "$message" ] || message="updated=$updated unchanged=$unchanged failed=$failed skipped=$skipped"
|
||||
[ "$record_status" = "1" ] && write_status 0 "$finished_at"
|
||||
[ "$failed" -eq 0 ]
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
STATUS_FILE="${CLASHOO_STATUS_FILE:-/usr/share/clashbackup/subscription_update.status}"
|
||||
UPDATER="${CLASHOO_SUBSCRIPTION_UPDATER:-/usr/share/clashoo/update/subscription_update.sh}"
|
||||
SERVICE_CMD="${CLASHOO_SERVICE_CMD:-/etc/init.d/clashoo}"
|
||||
interval="$(uci -q get clashoo.config.subscription_update_interval 2>/dev/null)"
|
||||
now="$(date +%s)"
|
||||
last=0
|
||||
|
||||
[ "$(uci -q get clashoo.config.auto_subscription_update 2>/dev/null)" = "1" ] || exit 0
|
||||
"$SERVICE_CMD" status >/dev/null 2>&1 || exit 0
|
||||
echo "$interval" | grep -Eq '^[0-9]+$' || interval=72
|
||||
[ "$interval" -gt 0 ] || interval=72
|
||||
[ -r "$STATUS_FILE" ] && last="$(sed -n 's/^last_run=//p' "$STATUS_FILE" | head -1)"
|
||||
echo "$last" | grep -Eq '^[0-9]+$' || last=0
|
||||
|
||||
[ $((now - last)) -ge $((interval * 3600)) ] || exit 0
|
||||
sh "$UPDATER" --all
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
SUB_FILE="${1:-}"
|
||||
TEMPLATE_FILE="${2:-}"
|
||||
OUT_FILE="${3:-}"
|
||||
TMP_FILE="/tmp/clash_template_merge_$$.yaml"
|
||||
TMP_FILE2="${TMP_FILE}.norm"
|
||||
LOG_FILE="/tmp/clash_update.txt"
|
||||
|
||||
log() {
|
||||
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$LOG_FILE"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_FILE" "$TMP_FILE2" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
[ -n "$SUB_FILE" ] || { log "模板复写失败:缺少订阅文件参数"; exit 1; }
|
||||
[ -n "$TEMPLATE_FILE" ] || { log "模板复写失败:缺少模板文件参数"; exit 1; }
|
||||
[ -n "$OUT_FILE" ] || { log "模板复写失败:缺少输出文件参数"; exit 1; }
|
||||
|
||||
[ -f "$SUB_FILE" ] || { log "模板复写失败:订阅文件不存在"; exit 1; }
|
||||
[ -f "$TEMPLATE_FILE" ] || { log "模板复写失败:模板文件不存在"; exit 1; }
|
||||
|
||||
if ! command -v yq >/dev/null 2>&1; then
|
||||
log "模板复写失败:缺少 yq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
core_bin=""
|
||||
for b in mihomo clash-meta clash; do
|
||||
if command -v "$b" >/dev/null 2>&1; then
|
||||
core_bin="$b"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p "$(dirname "$OUT_FILE")" >/dev/null 2>&1
|
||||
|
||||
log "开始模板复写:$(basename "$SUB_FILE") <- $(basename "$TEMPLATE_FILE")"
|
||||
export SUB_FILE TEMPLATE_FILE
|
||||
|
||||
# template is base; load() forces single-doc; explode anchors first (yq 4.53+ breaks "<<")
|
||||
yq -n '
|
||||
(load(strenv(SUB_FILE)) | explode(.)) as $sub |
|
||||
(load(strenv(TEMPLATE_FILE)) | explode(.)) as $tpl |
|
||||
(
|
||||
($tpl * {
|
||||
"proxies": (((($tpl.proxies // []) + ($sub.proxies // [])) | unique_by(.name))),
|
||||
"proxy-providers": (($tpl."proxy-providers" // {}) * ($sub."proxy-providers" // {}))
|
||||
})
|
||||
| .dns = ((.dns // {}) * {
|
||||
"proxy-server-nameserver": (
|
||||
.dns."proxy-server-nameserver"
|
||||
// $sub.dns."proxy-server-nameserver"
|
||||
// $sub.dns.nameserver
|
||||
// ["223.5.5.5", "119.29.29.29"]
|
||||
)
|
||||
})
|
||||
)
|
||||
' >"$TMP_FILE" 2>/dev/null || {
|
||||
log "模板复写失败:YAML 合并错误"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 兼容第三方模板中使用“直连”字样,统一转换为 Clash 内置 DIRECT。
|
||||
yq e '
|
||||
(.. | select(tag == "!!str")) |= sub("^直连$", "DIRECT")
|
||||
| .rules = ((.rules // []) | map(sub(",直连,", ",DIRECT,") | sub(",直连$", ",DIRECT")))
|
||||
' "$TMP_FILE" >"$TMP_FILE2" 2>/dev/null || {
|
||||
log "模板复写失败:直连别名标准化失败"
|
||||
exit 1
|
||||
}
|
||||
mv -f "$TMP_FILE2" "$TMP_FILE" >/dev/null 2>&1 || {
|
||||
log "模板复写失败:中间文件写入失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
yq e '.' "$TMP_FILE" >/dev/null 2>&1 || {
|
||||
log "模板复写失败:合并结果 YAML 无效"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -n "$core_bin" ]; then
|
||||
"$core_bin" -t -f "$TMP_FILE" >/dev/null 2>&1 || {
|
||||
log "模板复写失败:内核校验不通过"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
mv -f "$TMP_FILE" "$OUT_FILE" >/dev/null 2>&1 || {
|
||||
log "模板复写失败:写入输出文件失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
chmod 644 "$OUT_FILE" >/dev/null 2>&1 || true
|
||||
log "模板复写完成:$(basename "$OUT_FILE")"
|
||||
exit 0
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
config_name="${1:-$(uci -q get clashoo.config.config_update_name 2>/dev/null)}"
|
||||
[ -n "$config_name" ] || exit 1
|
||||
exec sh /usr/share/clashoo/update/subscription_update.sh --mihomo "$config_name"
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
UPDATE_LOG="/tmp/clash_update.txt"
|
||||
STATE_DIR="${CLASHOO_STATE_DIR:-/usr/share/clashbackup}"
|
||||
|
||||
log_update() {
|
||||
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
|
||||
}
|
||||
|
||||
log_update "更新大陆白名单"
|
||||
sh /usr/share/clashoo/update/update_china_ip.sh >> "$UPDATE_LOG" 2>&1
|
||||
|
||||
log_update "更新 GeoIP / GeoSite"
|
||||
sh /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
tmp="$STATE_DIR/rule_update.status.tmp.$$"
|
||||
printf 'last_run=%s\n' "$(date +%s)" >"$tmp" && mv -f "$tmp" "$STATE_DIR/rule_update.status"
|
||||
|
||||
exit 0
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
#!/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}"
|
||||
RUNTIME_STATE_FILE="${RUNTIME_STATE_FILE:-/tmp/clashoo/runtime_state}"
|
||||
PROC_ROOT="${PROC_ROOT:-/proc}"
|
||||
TMP_V4="/tmp/china_ip.txt.$$"
|
||||
TMP_V6="/tmp/china_ipv6.txt.$$"
|
||||
OUT_V4="/tmp/geoip_cn.nft.$$"
|
||||
OUT_V6="/tmp/geoip6_cn.nft.$$"
|
||||
|
||||
log() {
|
||||
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
bool_enabled() {
|
||||
case "$1" in
|
||||
1|true|TRUE|yes|on) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
download_with_fallback() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
local ip
|
||||
|
||||
if curl -fsSL "$url" -o "$output"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$url" in
|
||||
https://ispip.clang.cn/*)
|
||||
for ip in 182.247.248.127 103.220.64.183; do
|
||||
if curl -fsSL --resolve "ispip.clang.cn:443:${ip}" "$url" -o "$output"; then
|
||||
log "DNS 异常,已使用 --resolve(${ip}) 回源下载"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_V4" "$TMP_V6" "$OUT_V4" "$OUT_V6"
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
render_nft_set() {
|
||||
local source_file="$1"
|
||||
local output_file="$2"
|
||||
local set_name="$3"
|
||||
local set_type="$4"
|
||||
|
||||
awk -v set_name="$set_name" -v set_type="$set_type" '
|
||||
BEGIN {
|
||||
print "set " set_name " {"
|
||||
print "\ttype " set_type ";"
|
||||
print "\tflags interval;"
|
||||
print "\tauto-merge;"
|
||||
print "\telements = {"
|
||||
first = 1
|
||||
}
|
||||
!/^[[:space:]]*$/ && !/^[[:space:]]*#/ {
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0)
|
||||
if ($0 == "")
|
||||
next
|
||||
if (!first)
|
||||
print ","
|
||||
printf "\t\t%s", $0
|
||||
first = 0
|
||||
}
|
||||
END {
|
||||
if (!first)
|
||||
print ""
|
||||
print "\t}"
|
||||
print "}"
|
||||
}
|
||||
' "$source_file" > "$output_file"
|
||||
}
|
||||
|
||||
url4="$(uci -q get clashoo.config.china_ip_url 2>/dev/null || true)"
|
||||
url6="$(uci -q get clashoo.config.china_ipv6_url 2>/dev/null || true)"
|
||||
bypass_china="$(uci -q get clashoo.config.bypass_china 2>/dev/null || true)"
|
||||
bypass_china_ipv6="$(uci -q get clashoo.config.bypass_china_ipv6 2>/dev/null || true)"
|
||||
[ -n "$bypass_china_ipv6" ] || bypass_china_ipv6="$bypass_china"
|
||||
|
||||
[ -n "$url4" ] || url4='https://ispip.clang.cn/all_cn.txt'
|
||||
[ -n "$url6" ] || url6='https://ispip.clang.cn/all_cn_ipv6.txt'
|
||||
|
||||
mkdir -p "$NFT_DIR"
|
||||
|
||||
log '开始更新大陆白名单'
|
||||
|
||||
download_with_fallback "$url4" "$TMP_V4"
|
||||
[ -s "$TMP_V4" ] || {
|
||||
log '大陆 IPv4 白名单下载失败:返回为空'
|
||||
exit 1
|
||||
}
|
||||
changed=0
|
||||
|
||||
render_nft_set "$TMP_V4" "$OUT_V4" clashoo_china ipv4_addr
|
||||
if cmp -s "$OUT_V4" "$TARGET_V4"; then
|
||||
log '大陆 IPv4 白名单内容无变化'
|
||||
else
|
||||
mv "$OUT_V4" "$TARGET_V4"
|
||||
chmod 644 "$TARGET_V4" >/dev/null 2>&1 || true
|
||||
changed=1
|
||||
log '大陆 IPv4 白名单更新完成'
|
||||
fi
|
||||
|
||||
if download_with_fallback "$url6" "$TMP_V6"; then
|
||||
if [ -s "$TMP_V6" ]; then
|
||||
render_nft_set "$TMP_V6" "$OUT_V6" clashoo_china6 ipv6_addr
|
||||
if cmp -s "$OUT_V6" "$TARGET_V6"; then
|
||||
log '大陆 IPv6 白名单内容无变化'
|
||||
else
|
||||
mv "$OUT_V6" "$TARGET_V6"
|
||||
chmod 600 "$TARGET_V6" >/dev/null 2>&1 || true
|
||||
changed=1
|
||||
log '大陆 IPv6 白名单更新完成'
|
||||
fi
|
||||
else
|
||||
log '大陆 IPv6 白名单下载为空,保留原文件'
|
||||
fi
|
||||
else
|
||||
log '大陆 IPv6 白名单下载失败,保留原文件'
|
||||
fi
|
||||
|
||||
instance_pid() {
|
||||
ubus call service list "{\"name\":\"$1\",\"verbose\":true}" 2>/dev/null |
|
||||
jsonfilter -e "@[\"$1\"].instances.*.pid" 2>/dev/null | head -n1
|
||||
}
|
||||
|
||||
pid_running() {
|
||||
[ -n "$1" ] && [ -d "$PROC_ROOT/$1" ]
|
||||
}
|
||||
|
||||
clashoo_manages_singbox() {
|
||||
[ -r "$RUNTIME_STATE_FILE" ] || return 1
|
||||
grep -qx 'configured_core=singbox' "$RUNTIME_STATE_FILE" 2>/dev/null || return 1
|
||||
case "$(sed -n 's/^health_detail=//p' "$RUNTIME_STATE_FILE" 2>/dev/null | head -n1)" in
|
||||
service_stopped|service_disabled|boot_disabled|preflight:*|start:*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
selected_core_running() {
|
||||
local name pid
|
||||
if [ "$(uci -q get clashoo.config.core_type 2>/dev/null)" = "singbox" ]; then
|
||||
bool_enabled "$(uci -q get sing-box.main.enabled 2>/dev/null)" || return 1
|
||||
clashoo_manages_singbox || return 1
|
||||
name="sing-box"
|
||||
else
|
||||
name="clashoo"
|
||||
fi
|
||||
pid="$(instance_pid "$name")"
|
||||
pid_running "$pid"
|
||||
}
|
||||
|
||||
firewall_managed() {
|
||||
bool_enabled "$(uci -q get clashoo.config.enable 2>/dev/null)" || return 1
|
||||
[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ] && return 1
|
||||
selected_core_running
|
||||
}
|
||||
|
||||
if [ "$changed" -eq 0 ]; then
|
||||
log '白名单内容未变,跳过防火墙重载'
|
||||
elif ! firewall_managed; then
|
||||
log '未接管防火墙(服务未运行或仅内核模式),仅更新白名单文件'
|
||||
elif bool_enabled "$bypass_china" || bool_enabled "$bypass_china_ipv6"; then
|
||||
if "$FW4_SCRIPT" apply >/dev/null 2>&1; then
|
||||
log "大陆白名单规则已重载(IPv4=${bypass_china:-0},IPv6=${bypass_china_ipv6:-0})"
|
||||
else
|
||||
log '大陆白名单规则重载失败'
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log '大陆 IPv4/IPv6 绕过均未启用,仅更新白名单文件'
|
||||
fi
|
||||
|
||||
log '大陆白名单更新流程完成'
|
||||
@@ -0,0 +1,91 @@
|
||||
--- a/dns/middleware.go
|
||||
+++ b/dns/middleware.go
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
icontext "github.com/metacubex/mihomo/context"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
|
||||
+ M "github.com/metacubex/sing/common/metadata"
|
||||
D "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
@@ -152,6 +153,9 @@ func withFakeIP(skipper *fakeip.Skipper,
|
||||
q := r.Question[0]
|
||||
|
||||
host := strings.TrimRight(q.Name, ".")
|
||||
+ if !M.IsDomainName(host) {
|
||||
+ return next(ctx, r)
|
||||
+ }
|
||||
if skipper.ShouldSkipped(host) {
|
||||
return next(ctx, r)
|
||||
}
|
||||
--- /dev/null
|
||||
+++ b/dns/middleware_test.go
|
||||
@@ -0,0 +1,68 @@
|
||||
+package dns
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "net/netip"
|
||||
+ "testing"
|
||||
+
|
||||
+ "github.com/metacubex/mihomo/component/fakeip"
|
||||
+ icontext "github.com/metacubex/mihomo/context"
|
||||
+
|
||||
+ D "github.com/miekg/dns"
|
||||
+ "github.com/stretchr/testify/require"
|
||||
+)
|
||||
+
|
||||
+func TestWithFakeIPSkipsInvalidDomain(t *testing.T) {
|
||||
+ pool, err := fakeip.New(fakeip.Options{
|
||||
+ IPNet: netip.MustParsePrefix("198.18.0.1/16"),
|
||||
+ Size: 10,
|
||||
+ })
|
||||
+ require.NoError(t, err)
|
||||
+
|
||||
+ downstreamCalled := false
|
||||
+ next := func(_ *icontext.DNSContext, request *D.Msg) (*D.Msg, error) {
|
||||
+ downstreamCalled = true
|
||||
+ response := new(D.Msg)
|
||||
+ response.SetReply(request)
|
||||
+ return response, nil
|
||||
+ }
|
||||
+
|
||||
+ request := new(D.Msg)
|
||||
+ request.SetQuestion("n-relay-ipc-txc-nj-00.tplinkcloud.com.cn\\152.", D.TypeA)
|
||||
+ response, err := withFakeIP(&fakeip.Skipper{}, pool, nil, 1)(next)(icontext.NewDNSContext(context.Background()), request)
|
||||
+
|
||||
+ require.NoError(t, err)
|
||||
+ require.True(t, downstreamCalled)
|
||||
+ require.Empty(t, response.Answer)
|
||||
+ _, mapped := pool.LookBack(netip.MustParseAddr("198.18.0.4"))
|
||||
+ require.False(t, mapped)
|
||||
+}
|
||||
+
|
||||
+func TestWithFakeIPKeepsValidDomain(t *testing.T) {
|
||||
+ pool, err := fakeip.New(fakeip.Options{
|
||||
+ IPNet: netip.MustParsePrefix("198.18.0.1/16"),
|
||||
+ Size: 10,
|
||||
+ })
|
||||
+ require.NoError(t, err)
|
||||
+
|
||||
+ downstreamCalled := false
|
||||
+ next := func(_ *icontext.DNSContext, request *D.Msg) (*D.Msg, error) {
|
||||
+ downstreamCalled = true
|
||||
+ response := new(D.Msg)
|
||||
+ response.SetReply(request)
|
||||
+ return response, nil
|
||||
+ }
|
||||
+
|
||||
+ request := new(D.Msg)
|
||||
+ request.SetQuestion("n-relay-ipc-txc-nj-00.tplinkcloud.com.cn.", D.TypeA)
|
||||
+ dnsCtx := icontext.NewDNSContext(context.Background())
|
||||
+ response, err := withFakeIP(&fakeip.Skipper{}, pool, nil, 1)(next)(dnsCtx, request)
|
||||
+
|
||||
+ require.NoError(t, err)
|
||||
+ require.False(t, downstreamCalled)
|
||||
+ require.Len(t, response.Answer, 1)
|
||||
+ require.Equal(t, icontext.DNSTypeFakeIP, dnsCtx.Type())
|
||||
+ host, mapped := pool.LookBack(netip.MustParseAddr("198.18.0.4"))
|
||||
+ require.True(t, mapped)
|
||||
+ require.Equal(t, "n-relay-ipc-txc-nj-00.tplinkcloud.com.cn", host)
|
||||
+}
|
||||
+41
-29
@@ -5,51 +5,60 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=2026.08.08
|
||||
PKG_VERSION:=2026.09.12
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_VERSION:=1d52465dd455d9702a75486bc4dadde684d6f782
|
||||
PKG_SOURCE_URL:=https://github.com/olicesx/dae.git
|
||||
PKG_MIRROR_HASH:=skip
|
||||
PKG_SOURCE:=dae-src-2026.09.12-187058462a1f.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=187058462a1fd9eeb9885f4971ccf4bc81358533e71730d8509bd7968624c1c7
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
PKG_LICENSE_FILE:=LICENSE
|
||||
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
|
||||
DAE_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_BUILD_DIR:=$(DAE_BUILD_DIR)/core
|
||||
|
||||
PKG_BUILD_DEPENDS:=golang/host bpf-headers
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
# armv7 lacks a CO-RE vmlinux.h for the trace eBPF, so trace's bpf2go fails to
|
||||
# build there. Apply sbwml/openwrt_helloworld's arm patches (add vmlinux-arm.h
|
||||
# + drop the kprobe that doesn't work) to let dae build on arm targets.
|
||||
ifeq ($(ARCH),arm)
|
||||
PATCH_DIR:=$(CURDIR)/patches_arm
|
||||
endif
|
||||
|
||||
GO_PKG:=github.com/daeuniverse/dae
|
||||
GO_PKG_EXCLUDES:=control/kern/tests
|
||||
|
||||
GO_PKG_LDFLAGS:= \
|
||||
-s -w -buildid= \
|
||||
-linkmode external -extldflags '-static -Wl,-s'
|
||||
GO_PKG_LDFLAGS_X:= \
|
||||
$(GO_PKG)/cmd.Version=$(PKG_VERSION) \
|
||||
$(GO_PKG)/common/consts.MaxMatchSetLen_=1024
|
||||
GO_PKG_TAGS:=trace
|
||||
|
||||
PGO_PROFILE:=$(CURDIR)/pprof/default.pgo
|
||||
GO_PKG_TAGS:=trace,timetzdata
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/bpf.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
GO_PKG_BUILD_VARS+= \
|
||||
GOFLAGS="-trimpath -buildvcs=false -pgo=auto"
|
||||
|
||||
GO_PKG_BUILD_VARS+= GOFLAGS="-trimpath -buildvcs=false -pgo=auto"
|
||||
GO_PKG_TARGET_VARS+= \
|
||||
CGO_LDFLAGS="$(TARGET_LDFLAGS) -static -Wl,-s" \
|
||||
GOEXPERIMENT=newinliner,simd
|
||||
|
||||
define Package/dae
|
||||
define Package/dae/Default
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
URL:=https://github.com/daeuniverse/dae
|
||||
endef
|
||||
|
||||
define Package/dae
|
||||
$(call Package/dae/Default)
|
||||
TITLE:=A lightweight and high-performance transparent proxy solution
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS) \
|
||||
+ca-bundle +kmod-sched-core +kmod-sched-bpf \
|
||||
@@ -73,8 +82,13 @@ define Package/dae/config
|
||||
endchoice
|
||||
endef
|
||||
|
||||
define Package/dae/description
|
||||
dae, means goose, is a lightweight and high-performance transparent
|
||||
proxy solution.
|
||||
endef
|
||||
|
||||
define Package/dae/conffiles
|
||||
/etc/dae/
|
||||
/etc/dae/config.dae
|
||||
/etc/config/dae
|
||||
endef
|
||||
|
||||
@@ -83,17 +97,16 @@ DAE_CFLAGS:= \
|
||||
-DMAX_MATCH_SET_LEN=1024 \
|
||||
-I$(BPF_HEADERS_DIR)/tools/lib \
|
||||
-I$(BPF_HEADERS_DIR)/arch/$(BPF_KARCH)/include/asm/mach-generic
|
||||
|
||||
|
||||
define Build/Prepare
|
||||
$(call Build/Prepare/Default)
|
||||
[ -f "$(PGO_PROFILE)" ] && $(CP) "$(PGO_PROFILE)" "$(PKG_BUILD_DIR)/default.pgo" || true
|
||||
mkdir -p $(DAE_BUILD_DIR)
|
||||
$(TAR) --strip-components=1 -C $(DAE_BUILD_DIR) -xzf $(DL_DIR)/$(PKG_SOURCE)
|
||||
$(if $(wildcard $(PATCH_DIR)/*.patch),$(call PatchDir,$(PKG_BUILD_DIR),$(PATCH_DIR),))
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
( \
|
||||
pushd $(PKG_BUILD_DIR) ; \
|
||||
go get -u=patch ; \
|
||||
go mod tidy ; \
|
||||
export \
|
||||
$(GO_GENERAL_BUILD_CONFIG_VARS) \
|
||||
$(GO_PKG_BUILD_CONFIG_VARS) \
|
||||
@@ -112,17 +125,16 @@ endef
|
||||
|
||||
define Package/dae/install
|
||||
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
|
||||
$(INSTALL_DIR) $(1)/usr/bin/
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/dae $(1)/usr/bin/dae
|
||||
$(INSTALL_DIR) $(1)/etc/dae/
|
||||
$(INSTALL_CONF) $(PKG_BUILD_DIR)/example.dae $(1)/etc/dae/
|
||||
$(INSTALL_CONF) $(CURDIR)/files/config.dae $(1)/etc/dae/config.dae
|
||||
$(INSTALL_DIR) $(1)/etc/dae/config.d/
|
||||
$(INSTALL_CONF) $(CURDIR)/files/config.d/dns.dae $(1)/etc/dae/config.d/dns.dae
|
||||
$(INSTALL_CONF) $(CURDIR)/files/config.d/node.dae $(1)/etc/dae/config.d/node.dae
|
||||
$(INSTALL_CONF) $(CURDIR)/files/config.d/route.dae $(1)/etc/dae/config.d/route.dae
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/dae
|
||||
$(INSTALL_CONF) $(PKG_BUILD_DIR)/example.dae $(1)/etc/dae/example.dae
|
||||
$(SED) 's|^[[:space:]]*#lan_interface: docker0| lan_interface: br-lan|' $(1)/etc/dae/example.dae
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) $(CURDIR)/files/dae.config $(1)/etc/config/dae
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/dae.init $(1)/etc/init.d/dae
|
||||
endef
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# dns.dae
|
||||
dns {
|
||||
optimistic_cache_ttl: 86400
|
||||
max_cache_size: 4096
|
||||
upstream {
|
||||
localdns: 'udp://127.0.0.1:53'
|
||||
overseadns: 'tcp+udp://1.0.0.1:53'
|
||||
nodedns: 'udp://111.222.333.444:53'
|
||||
}
|
||||
routing {
|
||||
request {
|
||||
sub(sub_name) -> overseadns
|
||||
subnode(sub_name) && subnode(name_keyword: 114514) -> nodedns
|
||||
qtype(https) -> reject
|
||||
qname(geosite:gfw) -> overseadns
|
||||
fallback: localdns
|
||||
}
|
||||
response {
|
||||
upstream(overseadns) -> accept
|
||||
qname(geosite:private) -> accept
|
||||
ip(geoip:cn) -> accept
|
||||
fallback: overseadns
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
# node.dae
|
||||
node {
|
||||
node1: 'xxx'
|
||||
node2: 'xxx'
|
||||
}
|
||||
subscription {
|
||||
my_sub: 'https://www.example.com/subscription/link'
|
||||
}
|
||||
group {
|
||||
my_group {
|
||||
filter: subtag(my_sub) && !name(keyword: 'ExpireAt:')
|
||||
filter: subtag(my_sub2)
|
||||
policy: min_moving_avg
|
||||
}
|
||||
local_group {
|
||||
filter: name(node1, node2)
|
||||
policy: fixed(0)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
# route.dae
|
||||
routing {
|
||||
pname(dnsmasq, zerotier-one) -> must_direct
|
||||
dip(224.0.0.0/3, 'ff00::/8', geoip:private) -> direct
|
||||
|
||||
domain(geosite:category-bank-cn, geosite:boc@!cn, geosite:synology) -> direct
|
||||
|
||||
domain(geosite:category-ai-!cn, geosite:google, geosite:category-entertainment, geosite:gfw, geosite:github, geosite:spotify) && l4proto(udp) && dport(443) -> block
|
||||
|
||||
domain(geosite:spotify) -> proxy
|
||||
domain(geosite:category-entertainment) -> proxy
|
||||
domain(geosite:category-ai-!cn, geosite:google, geosite:github) -> proxy
|
||||
domain(geosite:gfw) -> proxy
|
||||
|
||||
dip(geoip:telegram) -> proxy
|
||||
|
||||
dip(geoip:cn) -> direct
|
||||
|
||||
l4proto(udp) && dport(443) -> block
|
||||
|
||||
fallback: proxy
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
# config.dae
|
||||
# load all dae files placed in ./config.d/
|
||||
include {
|
||||
config.d/*.dae
|
||||
}
|
||||
global {
|
||||
log_level:warn
|
||||
check_interval:600s
|
||||
check_tolerance:20ms
|
||||
lan_interface:"br-lan"
|
||||
wan_interface:auto
|
||||
auto_config_kernel_parameter:true
|
||||
sniffing_timeout:300ms
|
||||
udp_check_dns: "dns.google:53,8.8.8.8,2001:4860:4860::8888"
|
||||
tcp_check_url: "http://cp.cloudflare.com,1.1.1.1,2606:4700:4700::1111"
|
||||
dial_mode: domain
|
||||
allow_insecure: true
|
||||
}
|
||||
@@ -2,5 +2,6 @@
|
||||
config dae 'config'
|
||||
option enabled '0'
|
||||
option config_file '/etc/dae/config.dae'
|
||||
option lan_interface 'br-lan'
|
||||
option log_maxbackups '1'
|
||||
option log_maxsize '1'
|
||||
|
||||
Executable → Regular
+45
-117
@@ -1,141 +1,69 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2023 Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
USE_PROCD=1
|
||||
START=96
|
||||
STOP=10
|
||||
START=99
|
||||
|
||||
extra_command "hot_reload" "Hot-reload service"
|
||||
|
||||
CONF="dae"
|
||||
PROG="/usr/bin/dae"
|
||||
LOG="/var/log/dae/dae.log"
|
||||
DAE_HOST_IFACE="dae0"
|
||||
DAE_PEER_IFACE="dae0peer"
|
||||
DAE_NETNS_NAME="daens"
|
||||
DAENETNS="/run/netns/$DAE_NETNS_NAME"
|
||||
RESOLV_DAE="/tmp/resolv.conf.dae"
|
||||
|
||||
set_chmod_640() {
|
||||
mkdir -p /etc/dae/config.d
|
||||
mkdir -p /var/log/dae
|
||||
chmod -R 640 /etc/dae/ 2>/dev/null || true
|
||||
}
|
||||
|
||||
update_cron() {
|
||||
local temp_cron="/tmp/root.cron"
|
||||
|
||||
if [ -f /etc/crontabs/root ]; then
|
||||
grep -v '/etc/init.d/dae hot_reload' /etc/crontabs/root > "$temp_cron" 2>/dev/null
|
||||
fi
|
||||
: >> "$temp_cron"
|
||||
|
||||
local auto_update
|
||||
auto_update=$(uci -q get dae.config.subscribe_auto_update)
|
||||
if [ "$1" = "add" ] && [ "${auto_update:-0}" -eq 1 ]; then
|
||||
local min=$((RANDOM % 60))
|
||||
local hr=$(uci -q get dae.config.subscribe_update_day_time)
|
||||
local wk=$(uci -q get dae.config.subscribe_update_week_time)
|
||||
echo "$min ${hr:-*} * * ${wk:-*} /etc/init.d/dae hot_reload >/dev/null 2>&1" >> "$temp_cron"
|
||||
fi
|
||||
|
||||
crontab "$temp_cron" 2>/dev/null
|
||||
rm -f "$temp_cron"
|
||||
}
|
||||
|
||||
cleanup_netns() {
|
||||
tc qdisc del dev "$DAE_HOST_IFACE" clsact 2>/dev/null || true
|
||||
ip link del "$DAE_HOST_IFACE" 2>/dev/null || true
|
||||
ip link del "$DAE_PEER_IFACE" 2>/dev/null || true
|
||||
ip netns del "$DAE_NETNS_NAME" 2>/dev/null || true
|
||||
umount "$DAENETNS" 2>/dev/null || true
|
||||
rm -f "$DAENETNS" 2>/dev/null || true
|
||||
}
|
||||
|
||||
hijack_resolv_conf() {
|
||||
grep -q ' /tmp/resolv\.conf ' /proc/mounts && return 0
|
||||
|
||||
grep -E '^(search|domain) ' /tmp/resolv.conf > "$RESOLV_DAE" 2>/dev/null
|
||||
|
||||
local dns_list LOGICAL_WAN
|
||||
. /lib/functions/network.sh
|
||||
network_find_wan LOGICAL_WAN 2>/dev/null
|
||||
if [ -n "$LOGICAL_WAN" ]; then
|
||||
dns_list=$(ubus call "network.interface.$LOGICAL_WAN" status 2>/dev/null | \
|
||||
jsonfilter -e '@["dns-server"][*]' 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -z "$dns_list" ] && [ -f "/tmp/resolv.conf.d/resolv.conf.auto" ]; then
|
||||
dns_list=$(awk '/^nameserver/{print $2}' "/tmp/resolv.conf.d/resolv.conf.auto")
|
||||
fi
|
||||
|
||||
[ -z "$dns_list" ] && dns_list="119.29.29.29 223.5.5.5"
|
||||
|
||||
for dns in $dns_list; do
|
||||
echo "nameserver $dns" >> "$RESOLV_DAE"
|
||||
done
|
||||
|
||||
mount --bind "$RESOLV_DAE" /tmp/resolv.conf
|
||||
mount -o remount,ro,bind /tmp/resolv.conf
|
||||
}
|
||||
|
||||
restore_resolv_conf() {
|
||||
grep -q ' /tmp/resolv\.conf ' /proc/mounts || return 0
|
||||
mount -o remount,rw,bind /tmp/resolv.conf 2>/dev/null
|
||||
umount /tmp/resolv.conf 2>/dev/null
|
||||
rm -f "$RESOLV_DAE"
|
||||
}
|
||||
LOG_DIR="/var/log/dae"
|
||||
|
||||
start_service() {
|
||||
local enabled config_file log_maxbackups log_maxsize
|
||||
config_load "$CONF"
|
||||
|
||||
uci commit "$CONF" 2>/dev/null
|
||||
config_load "$CONF"
|
||||
config_get_bool enabled "config" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
local enabled
|
||||
config_get_bool enabled "config" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
|
||||
config_get config_file "config" "config_file" "/etc/dae/config.dae"
|
||||
local config_file
|
||||
config_get config_file "config" "config_file" "/etc/dae/config.dae"
|
||||
|
||||
set_chmod_640
|
||||
"$PROG" validate -c "$config_file" || return 1
|
||||
update_cron "add"
|
||||
hijack_resolv_conf
|
||||
cleanup_netns
|
||||
"$PROG" validate -c "$config_file" || return 1
|
||||
|
||||
config_get log_maxbackups "config" "log_maxbackups" "1"
|
||||
config_get log_maxsize "config" "log_maxsize" "1"
|
||||
local log_maxbackups log_maxsize
|
||||
config_get log_maxbackups "config" "log_maxbackups" "1"
|
||||
config_get log_maxsize "config" "log_maxsize" "1"
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
uname -r | grep -Eq "^6\.6" && procd_set_param env "QUIC_GO_DISABLE_GSO"="true"
|
||||
procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray"
|
||||
procd_set_param command "$PROG" run
|
||||
procd_append_param command --config "$config_file"
|
||||
procd_append_param command --disable-timestamp
|
||||
procd_append_param command --logfile "$LOG"
|
||||
procd_append_param command --logfile-maxbackups "$log_maxbackups"
|
||||
procd_append_param command --logfile-maxsize "$log_maxsize"
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param respawn
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"
|
||||
procd_set_param command "$PROG" run
|
||||
procd_append_param command --config "$config_file"
|
||||
procd_append_param command --logfile "$LOG_DIR/dae.log"
|
||||
procd_append_param command --logfile-maxbackups "$log_maxbackups"
|
||||
procd_append_param command --logfile-maxsize "$log_maxsize"
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param respawn
|
||||
# procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_stopped() {
|
||||
update_cron "remove"
|
||||
restore_resolv_conf
|
||||
cleanup_netns
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
stop_service() {
|
||||
rm -rf "$LOG_DIR"
|
||||
# kill lingering procs in daens: TERM first, then KILL for stubborn ones
|
||||
for pid in $(ip netns pids daens 2>/dev/null); do kill "$pid" 2>/dev/null; done
|
||||
if [ -n "$(ip netns pids daens 2>/dev/null)" ]; then
|
||||
sleep 1
|
||||
for pid in $(ip netns pids daens 2>/dev/null); do kill -9 "$pid" 2>/dev/null; done
|
||||
fi
|
||||
# cleanup daens netns; ip-full needed for netns, busybox fallback via umount
|
||||
if ! ip netns del daens 2>/dev/null; then
|
||||
umount /run/netns/daens 2>/dev/null
|
||||
rm -f /run/netns/daens
|
||||
fi
|
||||
# clean orphaned dae0
|
||||
ip link del dae0 2>/dev/null || true
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
procd_add_reload_trigger "$CONF"
|
||||
}
|
||||
|
||||
hot_reload() {
|
||||
set_chmod_640
|
||||
/etc/init.d/dae running && "$PROG" reload
|
||||
/etc/init.d/dae running && "$PROG" reload "$(cat /var/run/dae.pid)"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
diff --git a/cmd/reload_manager.go b/cmd/reload_manager.go
|
||||
index c78910c..bcc1a37 100644
|
||||
--- a/cmd/reload_manager.go
|
||||
+++ b/cmd/reload_manager.go
|
||||
@@ -423,5 +423,8 @@ func dnsConfigFingerprint(dns config.Dns) string {
|
||||
b.WriteString("max_cache_size=")
|
||||
b.WriteString(strconv.Itoa(dns.MaxCacheSize))
|
||||
b.WriteByte(';')
|
||||
+ b.WriteString("response_ttl=")
|
||||
+ b.WriteString(strconv.Itoa(dns.ResponseTtl))
|
||||
+ b.WriteByte(';')
|
||||
return b.String()
|
||||
}
|
||||
diff --git a/cmd/run_shutdown_test.go b/cmd/run_shutdown_test.go
|
||||
index f8f731f..4dcdcff 100644
|
||||
--- a/cmd/run_shutdown_test.go
|
||||
+++ b/cmd/run_shutdown_test.go
|
||||
@@ -609,6 +609,7 @@ func TestDNSConfigFingerprintCoversAllDnsFields(t *testing.T) {
|
||||
"OptimisticCache": {},
|
||||
"OptimisticCacheTtl": {},
|
||||
"MaxCacheSize": {},
|
||||
+ "ResponseTtl": {},
|
||||
}
|
||||
|
||||
dnsType := reflect.TypeOf(config.Dns{})
|
||||
diff --git a/config/config.go b/config/config.go
|
||||
index e688e88..af28da5 100644
|
||||
--- a/config/config.go
|
||||
+++ b/config/config.go
|
||||
@@ -160,6 +160,7 @@ type Dns struct {
|
||||
OptimisticCache bool `mapstructure:"optimistic_cache" default:"true"`
|
||||
OptimisticCacheTtl int `mapstructure:"optimistic_cache_ttl" default:"60"`
|
||||
MaxCacheSize int `mapstructure:"max_cache_size" default:"0"`
|
||||
+ ResponseTtl int `mapstructure:"response_ttl" default:"0"`
|
||||
}
|
||||
|
||||
type Routing struct {
|
||||
diff --git a/config/desc.go b/config/desc.go
|
||||
index 513cf7e..9b3c1a2 100644
|
||||
--- a/config/desc.go
|
||||
+++ b/config/desc.go
|
||||
@@ -66,6 +66,7 @@ var GlobalDesc = Desc{
|
||||
var DnsDesc = Desc{
|
||||
"ipversion_prefer": "For example, if ipversion_prefer is 4 and the domain name has both type A and type AAAA records, the dae will only respond to type A queries and response empty answer to type AAAA queries.",
|
||||
"fixed_domain_ttl": "Give a fixed ttl for domains. Zero means that dae will request to upstream every time and not cache DNS results for these domains.",
|
||||
+ "response_ttl": "Override the TTL returned to downstream DNS clients. Zero keeps dae's default behavior.",
|
||||
"upstream": "Value can be scheme://host:port, where the scheme can be tcp/udp/tcp+udp.\nIf host is a domain and has both IPv4 and IPv6 record, dae will automatically choose IPv4 or IPv6 to use according to group policy (such as min latency policy).\nPlease make sure DNS traffic will go through and be forwarded by dae, which is REQUIRED for domain routing.\nIf dial_mode is \"ip\", the upstream DNS answer SHOULD NOT be polluted, so domestic public DNS is not recommended.",
|
||||
"request": `DNS request routing for ordinary client traffic uses qname and qtype.
|
||||
Built-in outbounds for ordinary DNS requests: asis, reject.
|
||||
diff --git a/control/control_plane.go b/control/control_plane.go
|
||||
index 5df98db..a543f3b 100644
|
||||
--- a/control/control_plane.go
|
||||
+++ b/control/control_plane.go
|
||||
@@ -771,6 +771,7 @@ func newControlPlaneWithContextOptions(
|
||||
}
|
||||
plane.dnsRouting = dnsUpstream
|
||||
plane.dnsFixedDomainTtl = fixedDomainTtl
|
||||
+ plane.dnsResponseTtl = dnsConfig.ResponseTtl
|
||||
dnsControllerOption := plane.dnsControllerOption()
|
||||
dnsControllerOption.OptimisticCache = dnsConfig.OptimisticCache
|
||||
dnsControllerOption.OptimisticCacheTtl = dnsConfig.OptimisticCacheTtl
|
||||
@@ -1256,6 +1257,7 @@ func (c *ControlPlane) dnsControllerOption() *DnsControllerOption {
|
||||
Log: c.log,
|
||||
LifecycleContext: c.ctx,
|
||||
ConcurrencyLimit: 0,
|
||||
+ ResponseTtl: c.dnsResponseTtl,
|
||||
CacheAccessCallback: func(cache *DnsCache) (err error) {
|
||||
if err = c.core.BatchUpdateDomainRouting(cache); err != nil {
|
||||
return fmt.Errorf("BatchUpdateDomainRouting: %w", err)
|
||||
diff --git a/control/dns_control.go b/control/dns_control.go
|
||||
index d5bf872..b52132d 100644
|
||||
--- a/control/dns_control.go
|
||||
+++ b/control/dns_control.go
|
||||
@@ -82,6 +82,7 @@ type DnsControllerOption struct {
|
||||
OptimisticCache bool
|
||||
OptimisticCacheTtl int // 0 means never expire (rely on LRU eviction)
|
||||
MaxCacheSize int // maximum number of cache entries (0 = unlimited)
|
||||
+ ResponseTtl int
|
||||
}
|
||||
|
||||
type dnsControllerRuntimeState struct {
|
||||
@@ -94,6 +95,7 @@ type dnsControllerRuntimeState struct {
|
||||
bestDialerChooser func(ctx context.Context, req *udpRequest, upstream *dns.Upstream) (*dialArgument, error)
|
||||
timeoutExceedCallback func(dialArgument *dialArgument, err error)
|
||||
fixedDomainTtl map[string]int
|
||||
+ responseTtl int
|
||||
}
|
||||
|
||||
type dnsControllerStore struct {
|
||||
@@ -170,6 +172,9 @@ func normalizeDnsRuntimeBehavior(option *DnsControllerOption) (qtypePrefer uint1
|
||||
if err != nil {
|
||||
return 0, false, 0, 0, err
|
||||
}
|
||||
+ if option.ResponseTtl < 0 {
|
||||
+ return 0, false, 0, 0, fmt.Errorf("response_ttl must be greater than or equal to 0")
|
||||
+ }
|
||||
optimisticCacheTtl = option.OptimisticCacheTtl
|
||||
maxCacheSize = option.MaxCacheSize
|
||||
if optimisticCacheTtl == 0 && maxCacheSize == 0 {
|
||||
@@ -422,6 +427,7 @@ func (c *DnsController) updateRuntime(option *DnsControllerOption, routing *dns.
|
||||
bestDialerChooser: option.BestDialerChooser,
|
||||
timeoutExceedCallback: option.TimeoutExceedCallback,
|
||||
fixedDomainTtl: option.FixedDomainTtl,
|
||||
+ responseTtl: option.ResponseTtl,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -1547,10 +1553,16 @@ func (c *DnsController) NormalizeAndCacheDnsResp_(msg *dnsmessage.Msg, responseC
|
||||
ttl = 31536000
|
||||
}
|
||||
|
||||
- // For A/AAAA records, we set TTL to 0 to prevent downstream caching while we manage it.
|
||||
+ responseTtl := 0
|
||||
+ if rt := c.runtime(); rt != nil {
|
||||
+ responseTtl = rt.responseTtl
|
||||
+ }
|
||||
+
|
||||
+ // For A/AAAA records, we set TTL to 0 by default to prevent downstream caching while we manage it.
|
||||
+ // When response_ttl is set, use it as the downstream-visible TTL.
|
||||
if q.Qtype == dnsmessage.TypeA || q.Qtype == dnsmessage.TypeAAAA {
|
||||
for i := range msg.Answer {
|
||||
- msg.Answer[i].Header().Ttl = 0
|
||||
+ msg.Answer[i].Header().Ttl = uint32(responseTtl)
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/control/dns_response_ttl_test.go b/control/dns_response_ttl_test.go
|
||||
new file mode 100644
|
||||
index 0000000..0e5406e
|
||||
--- /dev/null
|
||||
+++ b/control/dns_response_ttl_test.go
|
||||
@@ -0,0 +1,64 @@
|
||||
+/*
|
||||
+ * SPDX-License-Identifier: AGPL-3.0-only
|
||||
+ * Copyright (c) 2022-2026, daeuniverse Organization <dae@v2raya.org>
|
||||
+ */
|
||||
+
|
||||
+package control
|
||||
+
|
||||
+import (
|
||||
+ "net"
|
||||
+ "testing"
|
||||
+ "time"
|
||||
+
|
||||
+ dnsmessage "github.com/miekg/dns"
|
||||
+ "github.com/stretchr/testify/require"
|
||||
+)
|
||||
+
|
||||
+func TestDnsController_ResponseTtlOverride(t *testing.T) {
|
||||
+ for _, tt := range []struct {
|
||||
+ name string
|
||||
+ responseTtl int
|
||||
+ wantTtl uint32
|
||||
+ }{
|
||||
+ {name: "default_zero", responseTtl: 0, wantTtl: 0},
|
||||
+ {name: "override", responseTtl: 60, wantTtl: 60},
|
||||
+ } {
|
||||
+ t.Run(tt.name, func(t *testing.T) {
|
||||
+ ctrl := setTestDnsControllerRuntime(newTestDnsController(), func(rt *dnsControllerRuntimeState) {
|
||||
+ rt.responseTtl = tt.responseTtl
|
||||
+ rt.newCache = func(fqdn string, answers, ns, extra []dnsmessage.RR, deadline time.Time, originalDeadline time.Time) (*DnsCache, error) {
|
||||
+ return &DnsCache{
|
||||
+ Answer: answers,
|
||||
+ NS: ns,
|
||||
+ Extra: extra,
|
||||
+ Deadline: deadline,
|
||||
+ OriginalDeadline: originalDeadline,
|
||||
+ }, nil
|
||||
+ }
|
||||
+ })
|
||||
+
|
||||
+ msg := new(dnsmessage.Msg)
|
||||
+ msg.SetReply(&dnsmessage.Msg{
|
||||
+ Question: []dnsmessage.Question{{
|
||||
+ Name: "example.com.",
|
||||
+ Qtype: dnsmessage.TypeA,
|
||||
+ Qclass: dnsmessage.ClassINET,
|
||||
+ }},
|
||||
+ })
|
||||
+ msg.Answer = []dnsmessage.RR{
|
||||
+ &dnsmessage.A{
|
||||
+ Hdr: dnsmessage.RR_Header{
|
||||
+ Name: "example.com.",
|
||||
+ Rrtype: dnsmessage.TypeA,
|
||||
+ Class: dnsmessage.ClassINET,
|
||||
+ Ttl: 300,
|
||||
+ },
|
||||
+ A: net.IPv4(93, 184, 216, 34),
|
||||
+ },
|
||||
+ }
|
||||
+
|
||||
+ require.NoError(t, ctrl.NormalizeAndCacheDnsResp_(msg, "example.com.:1"))
|
||||
+ require.Equal(t, tt.wantTtl, msg.Answer[0].Header().Ttl)
|
||||
+ })
|
||||
+ }
|
||||
+}
|
||||
diff --git a/control/dns_runtime.go b/control/dns_runtime.go
|
||||
index b871cc3..0c1d9c6 100644
|
||||
--- a/control/dns_runtime.go
|
||||
+++ b/control/dns_runtime.go
|
||||
@@ -20,6 +20,7 @@ type controlPlaneDNSRuntime struct {
|
||||
dnsController *DnsController
|
||||
dnsRouting *dns.Dns
|
||||
dnsFixedDomainTtl map[string]int
|
||||
+ dnsResponseTtl int
|
||||
dnsListener *DNSListener
|
||||
dnsListenerStopRegistered bool
|
||||
delayDNSListenerStart bool
|
||||
@@ -262,6 +263,7 @@ func (r *controlPlaneDNSRuntime) releaseRetainedState() {
|
||||
r.dnsController = nil
|
||||
r.dnsRouting = nil
|
||||
r.dnsFixedDomainTtl = nil
|
||||
+ r.dnsResponseTtl = 0
|
||||
r.dnsListener = nil
|
||||
r.dnsListenerStopRegistered = false
|
||||
r.delayDNSListenerStart = false
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
--- a/trace/kern/trace.c
|
||||
+++ b/trace/kern/trace.c
|
||||
@@ -228,7 +228,7 @@ KPROBE_SKB_AT(1)
|
||||
KPROBE_SKB_AT(2)
|
||||
KPROBE_SKB_AT(3)
|
||||
KPROBE_SKB_AT(4)
|
||||
-KPROBE_SKB_AT(5)
|
||||
+//KPROBE_SKB_AT(5)
|
||||
|
||||
SEC("kprobe/skb_lifetime_termination")
|
||||
int kprobe_skb_lifetime_termination(struct pt_regs *ctx)
|
||||
--- a/trace/trace.go
|
||||
+++ b/trace/trace.go
|
||||
@@ -212,8 +212,8 @@ func attachBpfToTargets(objs *bpfObjects
|
||||
kp, err = link.Kprobe(fn, objs.KprobeSkb3, nil)
|
||||
case 4:
|
||||
kp, err = link.Kprobe(fn, objs.KprobeSkb4, nil)
|
||||
- case 5:
|
||||
- kp, err = link.Kprobe(fn, objs.KprobeSkb5, nil)
|
||||
+ //case 5:
|
||||
+ // kp, err = link.Kprobe(fn, objs.KprobeSkb5, nil)
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Debugf("failed to attach kprobe to %s: %+v\n", fn, err)
|
||||
Binary file not shown.
+27
-62
@@ -5,23 +5,17 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=daed
|
||||
PKG_VERSION:=2026.07.31
|
||||
DAED_VERSION:=daed-671e65d
|
||||
WING_VERSION:=wing-dc50308
|
||||
CORE_VERSION:=core-caa6f5e
|
||||
WING_HASH_SHORT:=$(shell echo $(WING_VERSION) | cut -d- -f2)
|
||||
CORE_HASH_SHORT:=$(shell echo $(CORE_VERSION) | cut -d- -f2)
|
||||
PKG_VERSION:=2026.09.12
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_VERSION:=671e65d2fdcd62fe6a3ec18ecda209c5addea898
|
||||
PKG_SOURCE_URL:=https://github.com/daeuniverse/daed.git
|
||||
PKG_MIRROR_HASH:=skip
|
||||
PKG_SOURCE:=daed-src-2026.09.12-a0181f729855.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=a0181f7298552497ed193e683a54b1df77f2d5441524d61989f3e3e3cf6eac51
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0-only MIT
|
||||
PKG_LICENSE_FILES:=LICENSE wing/LICENSE
|
||||
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
|
||||
DAED_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_BUILD_DIR:=$(DAED_BUILD_DIR)/wing
|
||||
@@ -30,6 +24,13 @@ PKG_BUILD_DEPENDS:=golang/host bpf-headers
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
# armv7 lacks a CO-RE vmlinux.h for the trace eBPF (under wing/dae-core), so
|
||||
# trace's bpf2go fails there. Apply sbwml/openwrt_helloworld's arm patches
|
||||
# (add vmlinux-arm.h + drop the kprobe that doesn't work) to build on arm.
|
||||
ifeq ($(ARCH),arm)
|
||||
PATCH_DIR:=$(CURDIR)/patches_arm
|
||||
endif
|
||||
|
||||
GO_PKG:=github.com/daeuniverse/dae-wing
|
||||
|
||||
GO_PKG_LDFLAGS:= \
|
||||
@@ -38,16 +39,14 @@ GO_PKG_LDFLAGS:= \
|
||||
-X '$(GO_PKG)/db.AppDescription=$(PKG_NAME) is a integration solution of dae, API and UI.'
|
||||
GO_PKG_LDFLAGS_X:= \
|
||||
$(GO_PKG)/db.AppName=$(PKG_NAME) \
|
||||
$(GO_PKG)/db.AppVersion=$(DAED_VERSION)_$(WING_VERSION)_$(CORE_VERSION)
|
||||
GO_PKG_TAGS:=embedallowed,trace
|
||||
$(GO_PKG)/db.AppVersion=$(PKG_VERSION)
|
||||
GO_PKG_TAGS:=embedallowed,trace,timetzdata
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/bpf.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
GO_PKG_BUILD_VARS+= \
|
||||
GOFLAGS="-trimpath -buildvcs=false -pgo=auto"
|
||||
|
||||
GO_PKG_BUILD_VARS+= GOFLAGS="-trimpath -buildvcs=false -pgo=auto"
|
||||
GO_PKG_TARGET_VARS+= \
|
||||
CGO_LDFLAGS="$(TARGET_LDFLAGS) -static -Wl,-s" \
|
||||
GOEXPERIMENT=newinliner,simd
|
||||
@@ -94,52 +93,11 @@ define Package/daed/conffiles
|
||||
/etc/config/daed
|
||||
endef
|
||||
|
||||
NODE_VERSION:=v24.12.0
|
||||
NODE_DIST:=node-$(NODE_VERSION)-linux-x64
|
||||
NODE_URL:=https://nodejs.org/dist/$(NODE_VERSION)/$(NODE_DIST).tar.xz
|
||||
|
||||
define Build/Prepare
|
||||
( \
|
||||
rm -rf $(DAED_BUILD_DIR) ; \
|
||||
mkdir -p $(DAED_BUILD_DIR) ; \
|
||||
$(TAR) --strip-components=1 -C $(DAED_BUILD_DIR) -xzf $(DL_DIR)/$(PKG_NAME)-$(PKG_VERSION).tar.gz ; \
|
||||
git clone https://github.com/daeuniverse/dae-wing $(PKG_BUILD_DIR) && \
|
||||
git -C $(PKG_BUILD_DIR) checkout $(WING_HASH_SHORT) ; \
|
||||
rm -rf $(PKG_BUILD_DIR)/dae-core ; \
|
||||
git clone https://github.com/daeuniverse/dae $(PKG_BUILD_DIR)/dae-core && \
|
||||
git -C $(PKG_BUILD_DIR)/dae-core checkout $(CORE_HASH_SHORT) ; \
|
||||
pushd $(PKG_BUILD_DIR)/dae-core ; \
|
||||
git submodule update --init ; \
|
||||
go get -u=patch ; \
|
||||
go mod tidy ; \
|
||||
popd ; \
|
||||
pushd $(PKG_BUILD_DIR) ; \
|
||||
go get -u=patch ; \
|
||||
go mod tidy ; \
|
||||
wget -qO default.pgo "https://github.com/QiuSimons/luci-app-dae/raw/refs/heads/kix/dae/pprof/default.pgo" ; \
|
||||
popd ; \
|
||||
mkdir -p $(DAED_BUILD_DIR)/.node_tmp/config ; \
|
||||
wget -qO - "$(NODE_URL)" | tar -xJ -C $(DAED_BUILD_DIR)/.node_tmp --strip-components=1 ; \
|
||||
export PATH="$(DAED_BUILD_DIR)/.node_tmp/bin:$$$$PATH" ; \
|
||||
export XDG_CONFIG_HOME="$(DAED_BUILD_DIR)/.node_tmp/config" ; \
|
||||
npm install -g pnpm ; \
|
||||
pushd $(DAED_BUILD_DIR) ; \
|
||||
pnpm install ; \
|
||||
pnpm build --filter daed ; \
|
||||
popd ; \
|
||||
mkdir -p $(PKG_BUILD_DIR)/webrender/web ; \
|
||||
cp -rf $(DAED_BUILD_DIR)/apps/web/dist/* $(PKG_BUILD_DIR)/webrender/web ; \
|
||||
find $(PKG_BUILD_DIR)/webrender/web -name "*.map" -type f -delete ; \
|
||||
find $(PKG_BUILD_DIR)/webrender/web -type f -size +4k ! -name "*.gz" ! -name "*.woff" ! -name "*.woff2" -exec sh -c '\
|
||||
gzip -9 -k "{}"; \
|
||||
if [ "$$$$(stat -c %s {})" -lt "$$$$(stat -c %s {}.gz)" ]; then \
|
||||
rm {}.gz; \
|
||||
else \
|
||||
rm {}; \
|
||||
fi' \
|
||||
";" ; \
|
||||
rm -rf $(DAED_BUILD_DIR)/.node_tmp ; \
|
||||
)
|
||||
mkdir -p $(DAED_BUILD_DIR)
|
||||
$(TAR) --strip-components=1 -C $(DAED_BUILD_DIR) -xzf $(DL_DIR)/$(PKG_SOURCE)
|
||||
$(if $(wildcard $(CURDIR)/patches/*.patch),$(call PatchDir,$(PKG_BUILD_DIR),$(CURDIR)/patches,))
|
||||
$(if $(filter arm,$(ARCH)),$(if $(wildcard $(CURDIR)/patches_arm/*.patch),$(call PatchDir,$(PKG_BUILD_DIR),$(CURDIR)/patches_arm,)))
|
||||
endef
|
||||
|
||||
DAE_CFLAGS:= \
|
||||
@@ -174,12 +132,19 @@ define Package/daed/install
|
||||
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/dae-wing $(1)/usr/bin/daed
|
||||
$(INSTALL_BIN) $(CURDIR)/files/daed-guard $(1)/usr/bin/daed-guard
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/daed
|
||||
$(INSTALL_DATA) $(CURDIR)/files/daed-cleanup.sh $(1)/usr/share/daed/cleanup.sh
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) $(CURDIR)/files/daed.config $(1)/etc/config/daed
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/daed.init $(1)/etc/init.d/daed
|
||||
|
||||
$(INSTALL_DIR) $(1)/lib/upgrade/keep.d
|
||||
$(INSTALL_DATA) $(CURDIR)/files/daed.keep $(1)/lib/upgrade/keep.d/daed
|
||||
endef
|
||||
|
||||
$(eval $(call GoBinPackage,daed))
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/bin/sh
|
||||
# daed-cleanup.sh — reaper for stale daed kernel state.
|
||||
#
|
||||
# Removes:
|
||||
# 1. Processes inside the `daens` netns (SIGTERM, then SIGKILL after 1s)
|
||||
# 2. The `daens` network namespace itself (ip netns del, with
|
||||
# umount+rm as fallback for zombie nsfs mounts)
|
||||
# 3. The `dae0` veth pair in the host netns
|
||||
#
|
||||
# daed itself owns TC clsact detach. This opkg-side helper only
|
||||
# reaps stale daens/dae0 state and never removes /sys/fs/bpf/daed.
|
||||
# The pin directory is created during normal startup, so it is not
|
||||
# a reliable leak indicator and must not block service lifecycle.
|
||||
#
|
||||
# The function stays sourceable by both init.d/daed and daed-guard.
|
||||
|
||||
daed_process_probe() {
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
pgrep -f '^/usr/bin/daed([[:space:]]|$)' >/dev/null 2>&1
|
||||
case "$?" in
|
||||
0) return 0 ;;
|
||||
1) return 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if command -v pidof >/dev/null 2>&1; then
|
||||
pidof daed >/dev/null 2>&1
|
||||
case "$?" in
|
||||
0) return 0 ;;
|
||||
1) return 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
daed_cleanup_runtime() {
|
||||
local pid rc=0 probe_rc
|
||||
|
||||
# If daed userspace is currently running, do not touch the
|
||||
# netns, the veth, or the eBPF dataplane. They are in active
|
||||
# use; removing them would make every connection through dae
|
||||
# hang.
|
||||
daed_process_probe
|
||||
probe_rc=$?
|
||||
case "$probe_rc" in
|
||||
0)
|
||||
if [ "${DAED_GUARD_CLEANUP:-start}" = "start" ]; then
|
||||
logger -t daed-init "cleanup: pre-start skipped because /usr/bin/daed is still running; refusing a second instance"
|
||||
return 1
|
||||
fi
|
||||
logger -t daed-init "cleanup: post-exit skipped because another /usr/bin/daed instance is still running"
|
||||
return 0
|
||||
;;
|
||||
1) ;;
|
||||
*)
|
||||
logger -t daed-init "cleanup: cannot determine whether daed is running; refusing to remove netns/veth"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 1. Kill processes inside daens.
|
||||
for pid in $(ip netns pids daens 2>/dev/null); do
|
||||
kill "$pid" 2>/dev/null
|
||||
done
|
||||
|
||||
if [ -n "$(ip netns pids daens 2>/dev/null)" ]; then
|
||||
sleep 1
|
||||
for pid in $(ip netns pids daens 2>/dev/null); do
|
||||
kill -9 "$pid" 2>/dev/null
|
||||
done
|
||||
fi
|
||||
|
||||
# 2. Remove the daens netns. ip netns del can fail if a
|
||||
# process still references it via /proc/<pid>/ns/net or
|
||||
# because the umount has already happened. Try the
|
||||
# umount/rm fallback.
|
||||
if ! ip netns del daens 2>/dev/null; then
|
||||
umount -l /run/netns/daens 2>/dev/null
|
||||
if [ -e /run/netns/daens ] && ! rm -f /run/netns/daens 2>/dev/null; then
|
||||
logger -t daed-init "cleanup: failed to remove /run/netns/daens (resource busy); a reboot may be required"
|
||||
rc=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Remove the dae0 veth pair.
|
||||
if ip link show dae0 >/dev/null 2>&1; then
|
||||
if ! ip link del dae0 2>/dev/null; then
|
||||
logger -t daed-init "cleanup: failed to remove dae0 veth pair"
|
||||
rc=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. The pin root is normal persistent state. Never remove it or
|
||||
# make its existence change the cleanup result.
|
||||
if [ -e /sys/fs/bpf/daed ]; then
|
||||
logger -t daed-init "cleanup: /sys/fs/bpf/daed exists; leaving normal pin root unchanged"
|
||||
fi
|
||||
|
||||
# Final verification covers only netns and veth state.
|
||||
[ ! -e /run/netns/daens ] || return 1
|
||||
! ip netns list 2>/dev/null | grep -Eq '^daens([[:space:]]|$)' || return 1
|
||||
! ip link show dae0 >/dev/null 2>&1 || return 1
|
||||
|
||||
return $rc
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/bin/sh
|
||||
# Keep daed as a child so signals can be forwarded and stale netns/veth
|
||||
# state can be cleaned before start and after exit. daed owns TC detach;
|
||||
# /sys/fs/bpf/daed is normal persistent state and is never removed here.
|
||||
|
||||
. /usr/share/daed/cleanup.sh
|
||||
|
||||
# Pre-start cleanup refuses to remove runtime state while another
|
||||
# daed instance is active, and fails closed if that probe is broken.
|
||||
DAED_GUARD_CLEANUP=start
|
||||
if ! daed_cleanup_runtime; then
|
||||
echo "daed: stale /usr/bin/daed or netns state could not be verified or removed; refusing to start. Check process and netns state." >&2
|
||||
logger -t daed-init "pre-start cleanup failed: refusing to start daed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Keep daed as a child so post-exit cleanup runs before procd can
|
||||
# respawn it.
|
||||
child_pid=
|
||||
pending_signal=
|
||||
shutdown_signal=
|
||||
shutdown_elapsed=0
|
||||
forced_kill=0
|
||||
child_term_timeout=20
|
||||
|
||||
forward_signal() {
|
||||
local sig="$1"
|
||||
if [ -z "$child_pid" ]; then
|
||||
pending_signal="$sig"
|
||||
logger -t daed-init "signal $sig received before daed child started; launch cancelled"
|
||||
return 0
|
||||
fi
|
||||
case "$sig" in
|
||||
TERM|INT|QUIT)
|
||||
if [ -z "$shutdown_signal" ]; then
|
||||
shutdown_signal="$sig"
|
||||
shutdown_elapsed=0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
kill -"$sig" "$child_pid" 2>/dev/null
|
||||
}
|
||||
|
||||
exit_with_signal() {
|
||||
local sig="$1"
|
||||
trap - TERM INT HUP QUIT
|
||||
kill -"$sig" "$$" 2>/dev/null
|
||||
exit 1
|
||||
}
|
||||
|
||||
child_is_running() {
|
||||
local state
|
||||
kill -0 "$child_pid" 2>/dev/null || return 1
|
||||
state=$(awk '{ print $3 }' "/proc/$child_pid/stat" 2>/dev/null)
|
||||
[ "$state" != "Z" ]
|
||||
}
|
||||
trap 'forward_signal TERM' TERM
|
||||
trap 'forward_signal INT' INT
|
||||
trap 'forward_signal HUP' HUP
|
||||
trap 'forward_signal QUIT' QUIT
|
||||
|
||||
start_child() {
|
||||
local sig
|
||||
# Keep this check inside the function as well as at the call site:
|
||||
# it closes the ordinary pre-start window, while the pending-signal
|
||||
# path below handles a signal arriving during the background fork.
|
||||
[ -z "$pending_signal" ] || return 125
|
||||
/usr/bin/daed "$@" &
|
||||
child_pid=$!
|
||||
if [ -n "$pending_signal" ]; then
|
||||
sig="$pending_signal"
|
||||
pending_signal=
|
||||
forward_signal "$sig"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -n "$pending_signal" ]; then
|
||||
logger -t daed-init "refusing to start daed after pending signal $pending_signal"
|
||||
exit_with_signal "$pending_signal"
|
||||
fi
|
||||
|
||||
# Best-effort OOM preference; failure must not block startup.
|
||||
if ! echo -16 > /proc/self/oom_score_adj 2>/dev/null; then
|
||||
logger -t daed-init "warn: failed to set /proc/self/oom_score_adj; continuing without OOM preference"
|
||||
fi
|
||||
if ! start_child "$@"; then
|
||||
logger -t daed-init "refusing to start daed after pending signal $pending_signal"
|
||||
if [ -n "$pending_signal" ]; then
|
||||
exit_with_signal "$pending_signal"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
status=0
|
||||
reaped=0
|
||||
while [ "$reaped" -eq 0 ]; do
|
||||
if [ -n "$shutdown_signal" ] && child_is_running; then
|
||||
if [ "$shutdown_elapsed" -ge "$child_term_timeout" ]; then
|
||||
if [ "$forced_kill" -eq 0 ]; then
|
||||
logger -t daed-init "daed did not exit within ${child_term_timeout}s after $shutdown_signal; sending KILL"
|
||||
kill -KILL "$child_pid" 2>/dev/null
|
||||
forced_kill=1
|
||||
fi
|
||||
else
|
||||
sleep 1
|
||||
shutdown_elapsed=$((shutdown_elapsed + 1))
|
||||
continue
|
||||
fi
|
||||
sleep 1
|
||||
continue
|
||||
fi
|
||||
wait "$child_pid" 2>/dev/null
|
||||
status=$?
|
||||
if ! child_is_running; then
|
||||
reaped=1
|
||||
fi
|
||||
done
|
||||
child_pid=
|
||||
trap - TERM INT HUP QUIT
|
||||
|
||||
# Post-exit cleanup runs after the child is reaped.
|
||||
DAED_GUARD_CLEANUP=post-exit
|
||||
cleanup_status=0
|
||||
daed_cleanup_runtime || cleanup_status=$?
|
||||
if [ "$cleanup_status" -ne 0 ]; then
|
||||
echo "daed: runtime cleanup after exit failed" >&2
|
||||
logger -t daed-init "post-exit cleanup failed; check ip netns / ip link show"
|
||||
fi
|
||||
if [ "$status" -ne 0 ]; then
|
||||
exit "$status"
|
||||
fi
|
||||
exit "$cleanup_status"
|
||||
@@ -4,4 +4,6 @@ config daed 'config'
|
||||
option listen_addr '0.0.0.0:2023'
|
||||
option log_maxbackups '1'
|
||||
option log_maxsize '5'
|
||||
|
||||
option subscribe_auto_update '0'
|
||||
option subscribe_update_cycle 'daily'
|
||||
option subscribe_update_hour '4'
|
||||
|
||||
Executable → Regular
+40
-13
@@ -1,36 +1,41 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2023 Tianling Shen <cnsztl@immortalwrt.org>
|
||||
# daed-guard handles bounded child shutdown and post-exit netns/veth cleanup.
|
||||
# Keep the log file and a pre-stop state snapshot for diagnostics.
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
|
||||
CONF="daed"
|
||||
PROG="/usr/bin/daed"
|
||||
PROG="/usr/bin/daed-guard"
|
||||
LOG="/var/log/daed/daed.log"
|
||||
|
||||
. /usr/share/daed/cleanup.sh
|
||||
|
||||
cleanup_netns() {
|
||||
if [ -e "/run/netns/daens" ]; then
|
||||
logger -t daed "Cleaning up residual netns 'daens'..."
|
||||
ip netns del daens 2>/dev/null
|
||||
rm -f /run/netns/daens 2>/dev/null
|
||||
fi
|
||||
log() {
|
||||
logger -t daed-init "$@"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
log "start: begin"
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
config_get_bool enabled "config" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
|
||||
cleanup_netns
|
||||
if [ "$enabled" -ne "1" ]; then
|
||||
log "start: config disabled, exit"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local listen_addr log_maxbackups log_maxsize
|
||||
config_get listen_addr "config" "listen_addr" "0.0.0.0:2023"
|
||||
config_get log_maxbackups "config" "log_maxbackups" "1"
|
||||
config_get log_maxsize "config" "log_maxsize" "5"
|
||||
|
||||
log "start: listen=$listen_addr log_maxbackups=$log_maxbackups log_maxsize=$log_maxsize"
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"
|
||||
procd_set_param command "$PROG" run
|
||||
procd_append_param command --config "/etc/daed/"
|
||||
procd_append_param command --listen "$listen_addr"
|
||||
@@ -40,15 +45,37 @@ start_service() {
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param respawn
|
||||
# daed-guard escalates its child after 20 seconds. Leave time for
|
||||
# reap and post-exit cleanup before procd kills the wrapper.
|
||||
procd_set_param term_timeout 30
|
||||
# procd_set_param respawn: arguments are (threshold, timeout, retry).
|
||||
# threshold = runtime that resets the short-lived exit counter
|
||||
# timeout = seconds to wait between retries
|
||||
# retry = maximum short-lived exits before procd gives up
|
||||
# Reset the counter after one hour of stable runtime; otherwise retry
|
||||
# after 5 seconds and stop after 10 failures.
|
||||
procd_set_param respawn 3600 5 10
|
||||
# daed-guard sets oom_score_adj before forking; procd has no
|
||||
# oom_adj/oom_score_adj parameter.
|
||||
# procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_close_instance
|
||||
log "start: procd_open_instance done"
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
procd_kill "$CONF" ""
|
||||
cleanup_netns
|
||||
log "stop: begin"
|
||||
# Cleanup runs in daed-guard after its child exits. Record only a
|
||||
# pre-stop snapshot here; pin entries do not prove TC attachment.
|
||||
local pinned="" ns_left=""
|
||||
if [ -d /sys/fs/bpf/daed ]; then
|
||||
pinned=$(ls /sys/fs/bpf/daed 2>/dev/null | tr '\n' ' ')
|
||||
fi
|
||||
if ip netns list 2>/dev/null | grep -q '^daens'; then
|
||||
ns_left="daens"
|
||||
fi
|
||||
log "stop: pre-stop state — bpf_pin_entries=[${pinned:-none}] netns_left=[${ns_left:-none}]"
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/etc/daed/wing.db-wal
|
||||
@@ -0,0 +1,23 @@
|
||||
From: kenzok8 <kenzok8@noreply>
|
||||
Date: 2026-06-23
|
||||
Subject: [PATCH] daed: detach BPF hooks when stopping from dashboard
|
||||
|
||||
Detach BPF hooks when the dashboard stops dae through EmptyConfig.
|
||||
Reported: https://github.com/kenzok8/openwrt-daede/issues/13
|
||||
---
|
||||
--- a/dae/run.go
|
||||
+++ b/dae/run.go
|
||||
@@ -138,6 +138,13 @@
|
||||
/* dae-wing start */
|
||||
newConf := newReloadMsg.Config
|
||||
/* dae-wing end */
|
||||
+
|
||||
+ // openwrt-daede: dashboard stop reloads to EmptyConfig; detach hooks.
|
||||
+ if newConf == EmptyConfig {
|
||||
+ if e := c.DetachBpfHooks(); e != nil {
|
||||
+ log.Errorln("[Stop] DetachBpfHooks:", e)
|
||||
+ }
|
||||
+ }
|
||||
// New logger.
|
||||
oldLogOutput := log.Out
|
||||
log = logrus.New()
|
||||
@@ -0,0 +1,18 @@
|
||||
From: kenzok8 <kenzok8@noreply>
|
||||
Date: 2026-06-29
|
||||
Subject: [PATCH] daed: use bounded probes for manual latency tests
|
||||
|
||||
Use the existing bounded probe for manual dashboard latency tests.
|
||||
Reported: https://github.com/kenzok8/openwrt-daede/issues/20
|
||||
---
|
||||
--- a/graphql/service/node/latency_mutation_utils.go
|
||||
+++ b/graphql/service/node/latency_mutation_utils.go
|
||||
@@ -108,7 +108,7 @@ func testSingleNodeLatency(option *diale
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
- result, err := d.ProbeLatency()
|
||||
+ result, err := d.ProbeLatencyFast()
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
resolver.MessageV = &msg
|
||||
@@ -0,0 +1,19 @@
|
||||
From: kenzok8 <kenzok8@noreply>
|
||||
Date: 2026-06-30
|
||||
Subject: [PATCH] daed: raise manual latency probe concurrency to 16
|
||||
|
||||
Raise concurrent probes 8->16 so manual latency tests finish faster
|
||||
when many nodes (incl. dead/timeout ones) are present.
|
||||
Reported: https://github.com/kenzok8/openwrt-daede/issues/20
|
||||
---
|
||||
--- a/graphql/service/node/latency_mutation_utils.go
|
||||
+++ b/graphql/service/node/latency_mutation_utils.go
|
||||
@@ -19,7 +19,7 @@
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
-const latencyProbeConcurrency = 8
|
||||
+const latencyProbeConcurrency = 16
|
||||
|
||||
func TestLatencies(ctx context.Context, ids *[]graphql.ID) ([]*LatencyResolver, error) {
|
||||
option, err := latencyProbeOption(ctx)
|
||||
@@ -0,0 +1,63 @@
|
||||
diff --git a/graphql/service/config/mutation_utils.go b/graphql/service/config/mutation_utils.go
|
||||
index 7a3c33f..fd61554 100644
|
||||
--- a/graphql/service/config/mutation_utils.go
|
||||
+++ b/graphql/service/config/mutation_utils.go
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
+ "os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
+ "time"
|
||||
|
||||
"github.com/daeuniverse/dae-wing/common"
|
||||
"github.com/daeuniverse/dae-wing/dae"
|
||||
@@ -258,6 +260,15 @@ func Rename(ctx context.Context, _id graphql.ID, name string) (n int32, err erro
|
||||
|
||||
var runLock sync.Mutex
|
||||
|
||||
+const reloadWaitTimeout = 75 * time.Second
|
||||
+
|
||||
+func restartAfterReloadTimeout() {
|
||||
+ go func() {
|
||||
+ time.Sleep(time.Second)
|
||||
+ os.Exit(1)
|
||||
+ }()
|
||||
+}
|
||||
+
|
||||
func Run(d *gorm.DB, noLoad bool) (n int32, err error) {
|
||||
if ok := runLock.TryLock(); !ok {
|
||||
return 0, fmt.Errorf("the last request didn't complete; make a cup of tea and take a break")
|
||||
@@ -455,13 +466,24 @@ func Run(d *gorm.DB, noLoad bool) (n int32, err error) {
|
||||
|
||||
/// Reload with current config.
|
||||
chReloadCallback := make(chan error)
|
||||
- dae.ChReloadConfigs <- &dae.ReloadMessage{
|
||||
+ reloadMsg := &dae.ReloadMessage{
|
||||
Config: c,
|
||||
Callback: chReloadCallback,
|
||||
}
|
||||
- errReload := <-chReloadCallback
|
||||
- if errReload != nil {
|
||||
- return 0, fmt.Errorf("failed to load new config: %w; see more in log", errReload)
|
||||
+ select {
|
||||
+ case dae.ChReloadConfigs <- reloadMsg:
|
||||
+ case <-time.After(reloadWaitTimeout):
|
||||
+ restartAfterReloadTimeout()
|
||||
+ return 0, fmt.Errorf("failed to submit reload within %s; restarting daed to recover runtime", reloadWaitTimeout)
|
||||
+ }
|
||||
+ select {
|
||||
+ case errReload := <-chReloadCallback:
|
||||
+ if errReload != nil {
|
||||
+ return 0, fmt.Errorf("failed to load new config: %w; see more in log", errReload)
|
||||
+ }
|
||||
+ case <-time.After(reloadWaitTimeout):
|
||||
+ restartAfterReloadTimeout()
|
||||
+ return 0, fmt.Errorf("reload timed out after %s; restarting daed to recover runtime", reloadWaitTimeout)
|
||||
}
|
||||
|
||||
// Save running status
|
||||
@@ -0,0 +1,88 @@
|
||||
diff --git a/dae/run.go b/dae/run.go
|
||||
index d8f86c2..a0a665d 100644
|
||||
--- a/dae/run.go
|
||||
+++ b/dae/run.go
|
||||
@@ -32,7 +32,8 @@ var ChReloadConfigs = make(chan *ReloadMessage)
|
||||
var GracefullyExit = make(chan struct{})
|
||||
var EmptyConfig *daeConfig.Config
|
||||
var c *control.ControlPlane
|
||||
-var onceWaitingNetwork sync.Once
|
||||
+var waitingNetworkMu sync.Mutex
|
||||
+var networkReady bool
|
||||
|
||||
func init() {
|
||||
sections, err := config_parser.Parse(`global{} routing{}`)
|
||||
@@ -52,6 +53,19 @@ func ControlPlane() (*control.ControlPlane, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
+func waitForNetwork(log *logrus.Logger) error {
|
||||
+ waitingNetworkMu.Lock()
|
||||
+ defer waitingNetworkMu.Unlock()
|
||||
+ if networkReady {
|
||||
+ return nil
|
||||
+ }
|
||||
+ if err := WaitForNetwork(log); err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ networkReady = true
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
func Run(log *logrus.Logger, conf *daeConfig.Config, externGeoDataDirs []string, disableTimestamp bool, dry bool) (err error) {
|
||||
defer close(GracefullyExit)
|
||||
// Not really run dae.
|
||||
@@ -221,6 +235,9 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
|
||||
|
||||
// Deep copy to prevent modification.
|
||||
conf = deepcopy.Copy(conf).(*daeConfig.Config)
|
||||
+ if bpf == nil {
|
||||
+ control.PurgeStaleTCFilters(log)
|
||||
+ }
|
||||
|
||||
// Init Direct Dialers.
|
||||
direct.InitDirectDialers(conf.Global.FallbackResolver)
|
||||
@@ -228,9 +245,9 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
|
||||
|
||||
if !conf.Global.DisableWaitingNetwork && len(conf.Global.WanInterface) > 0 {
|
||||
// Wait for network for WAN ready.
|
||||
- onceWaitingNetwork.Do(func() {
|
||||
- WaitForNetwork(log)
|
||||
- })
|
||||
+ if err = waitForNetwork(log); err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
}
|
||||
|
||||
/// Get subscription -> nodeList mapping.
|
||||
diff --git a/dae/utils.go b/dae/utils.go
|
||||
index 65965a2..dc43cbc 100644
|
||||
--- a/dae/utils.go
|
||||
+++ b/dae/utils.go
|
||||
@@ -94,8 +94,9 @@ func preprocessWanInterfaceAuto(params *daeConfig.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
-func WaitForNetwork(log *logrus.Logger) {
|
||||
+func WaitForNetwork(log *logrus.Logger) error {
|
||||
epo := 5 * time.Second
|
||||
+ deadline := time.Now().Add(60 * time.Second)
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (c net.Conn, err error) {
|
||||
@@ -115,6 +116,9 @@ func WaitForNetwork(log *logrus.Logger) {
|
||||
}
|
||||
log.Infoln("Waiting for network...")
|
||||
for i := 0; ; i++ {
|
||||
+ if time.Now().After(deadline) {
|
||||
+ return fmt.Errorf("network unavailable after 60 seconds")
|
||||
+ }
|
||||
resp, err := client.Get(cmd.CheckNetworkLinks[i%len(cmd.CheckNetworkLinks)])
|
||||
if err != nil {
|
||||
log.Debugln("CheckNetwork:", err)
|
||||
@@ -134,4 +138,5 @@ func WaitForNetwork(log *logrus.Logger) {
|
||||
time.Sleep(epo)
|
||||
}
|
||||
log.Infoln("Network online.")
|
||||
+ return nil
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
Subject: [PATCH] daed: strictly reconcile subscription updates
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
订阅更新按节点链接精确对账,普通组里的失效节点删除,fixed 组固定节点转成独立节点保留。
|
||||
|
||||
数据库提交后按运行状态应用配置,补齐并发地址校验、导入失败回滚和 reload 超时恢复。
|
||||
---
|
||||
cmd/run.go | 6 +-
|
||||
graphql/mutation.go | 9 +-
|
||||
graphql/root_schema.go | 2 +-
|
||||
graphql/service/config/mutation_utils.go | 59 ++++-
|
||||
.../service/subscription/mutation_utils.go | 181 +++++++++++----
|
||||
.../subscription/mutation_utils_test.go | 212 ++++++++++++++++++
|
||||
6 files changed, 408 insertions(+), 61 deletions(-)
|
||||
create mode 100644 graphql/service/subscription/mutation_utils_test.go
|
||||
|
||||
diff --git a/cmd/run.go b/cmd/run.go
|
||||
index 7dce577..978f46c 100644
|
||||
--- a/cmd/run.go
|
||||
+++ b/cmd/run.go
|
||||
@@ -161,11 +161,8 @@ func restoreRunningState() (err error) {
|
||||
if !reload {
|
||||
return nil
|
||||
}
|
||||
- tx := db.BeginTx(context.TODO())
|
||||
// Reload.
|
||||
- if _, err = config.Run(tx, false); err != nil {
|
||||
- tx.Rollback()
|
||||
-
|
||||
+ if _, err = config.Run(context.TODO(), false); err != nil {
|
||||
// Another tx.
|
||||
// Set running = false.
|
||||
tx2 := db.BeginTx(context.TODO())
|
||||
@@ -183,7 +180,6 @@ func restoreRunningState() (err error) {
|
||||
tx2.Commit()
|
||||
return err
|
||||
}
|
||||
- tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
diff --git a/graphql/mutation.go b/graphql/mutation.go
|
||||
index fcbf440..adb747c 100644
|
||||
--- a/graphql/mutation.go
|
||||
+++ b/graphql/mutation.go
|
||||
@@ -256,14 +256,7 @@ func (r *MutationResolver) SelectConfig(args *struct {
|
||||
func (r *MutationResolver) Run(args *struct {
|
||||
Dry bool
|
||||
}) (int32, error) {
|
||||
- tx := db.BeginTx(context.TODO())
|
||||
- ret, err := config.Run(tx, args.Dry)
|
||||
- if err != nil {
|
||||
- tx.Rollback()
|
||||
- return 0, err
|
||||
- }
|
||||
- tx.Commit()
|
||||
- return ret, nil
|
||||
+ return config.Run(context.TODO(), args.Dry)
|
||||
}
|
||||
|
||||
func (r *MutationResolver) CreateDns(args *struct {
|
||||
diff --git a/graphql/root_schema.go b/graphql/root_schema.go
|
||||
index 8b82db6..a96cace 100644
|
||||
--- a/graphql/root_schema.go
|
||||
+++ b/graphql/root_schema.go
|
||||
@@ -121,7 +121,7 @@ type Mutation {
|
||||
# tagSubscription is to give the subscription a new tag.
|
||||
tagSubscription(id: ID!, tag: String!): Int! @hasRole(role: ADMIN)
|
||||
|
||||
- # updateSubscription is to re-fetch subscription and resolve subscription into nodes. Old nodes that independently belong to any groups will not be removed.
|
||||
+ # updateSubscription re-fetches and strictly reconciles subscription nodes. Stale nodes pinned by fixed groups become independent nodes.
|
||||
updateSubscription(id: ID!): Subscription! @hasRole(role: ADMIN)
|
||||
|
||||
# updateSubscriptionLink is to update the subscription link without re-fetching nodes.
|
||||
diff --git a/graphql/service/config/mutation_utils.go b/graphql/service/config/mutation_utils.go
|
||||
index fd61554..feb94d3 100644
|
||||
--- a/graphql/service/config/mutation_utils.go
|
||||
+++ b/graphql/service/config/mutation_utils.go
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/daeuniverse/dae-wing/dae"
|
||||
"github.com/daeuniverse/dae-wing/db"
|
||||
"github.com/daeuniverse/dae-wing/graphql/service/config/global"
|
||||
+ "github.com/daeuniverse/dae-wing/graphql/service/general"
|
||||
daeConfig "github.com/daeuniverse/dae/config"
|
||||
"github.com/daeuniverse/dae/pkg/config_parser"
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
@@ -269,21 +270,69 @@ func restartAfterReloadTimeout() {
|
||||
}()
|
||||
}
|
||||
|
||||
-func Run(d *gorm.DB, noLoad bool) (n int32, err error) {
|
||||
+func runTransaction(ctx context.Context, noLoad bool) (n int32, err error) {
|
||||
+ tx := db.BeginTx(ctx)
|
||||
+ if tx.Error != nil {
|
||||
+ return 0, tx.Error
|
||||
+ }
|
||||
+ n, err = runLocked(tx, noLoad)
|
||||
+ if err != nil {
|
||||
+ tx.Rollback()
|
||||
+ return 0, err
|
||||
+ }
|
||||
+ if err = tx.Commit().Error; err != nil {
|
||||
+ return 0, err
|
||||
+ }
|
||||
+ return n, nil
|
||||
+}
|
||||
+
|
||||
+func Run(ctx context.Context, noLoad bool) (n int32, err error) {
|
||||
if ok := runLock.TryLock(); !ok {
|
||||
return 0, fmt.Errorf("the last request didn't complete; make a cup of tea and take a break")
|
||||
}
|
||||
defer runLock.Unlock()
|
||||
+ return runTransaction(ctx, noLoad)
|
||||
+}
|
||||
+
|
||||
+func ApplyIfRunning(ctx context.Context) error {
|
||||
+ runLock.Lock()
|
||||
+ defer runLock.Unlock()
|
||||
+
|
||||
+ daeResolver := general.DaeResolver{Ctx: ctx}
|
||||
+ running, err := daeResolver.Running()
|
||||
+ if err != nil || !running {
|
||||
+ return err
|
||||
+ }
|
||||
+ modified, err := daeResolver.Modified()
|
||||
+ if err != nil || !modified {
|
||||
+ return err
|
||||
+ }
|
||||
+ _, err = runTransaction(ctx, false)
|
||||
+ return err
|
||||
+}
|
||||
+
|
||||
+func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
|
||||
//// Dry run.
|
||||
if noLoad {
|
||||
ch := make(chan error)
|
||||
- dae.ChReloadConfigs <- &dae.ReloadMessage{
|
||||
+ reloadMsg := &dae.ReloadMessage{
|
||||
Config: dae.EmptyConfig,
|
||||
Callback: ch,
|
||||
}
|
||||
- err = <-ch
|
||||
- if err != nil {
|
||||
- return 0, fmt.Errorf("failed to dryrun: %w; see more in log and report bugs", err)
|
||||
+ select {
|
||||
+ case dae.ChReloadConfigs <- reloadMsg:
|
||||
+ case <-time.After(reloadWaitTimeout):
|
||||
+ restartAfterReloadTimeout()
|
||||
+ return 0, fmt.Errorf("failed to submit dryrun within %s; restarting daed to recover runtime", reloadWaitTimeout)
|
||||
+ }
|
||||
+ select {
|
||||
+ case err = <-ch:
|
||||
+ if err != nil {
|
||||
+ return 0, fmt.Errorf("failed to dryrun: %w; see more in log and report bugs", err)
|
||||
+ }
|
||||
+ case <-time.After(reloadWaitTimeout):
|
||||
+ restartAfterReloadTimeout()
|
||||
+ return 0, fmt.Errorf("dryrun timed out after %s; restarting daed to recover runtime", reloadWaitTimeout)
|
||||
}
|
||||
|
||||
// Running -> false
|
||||
diff --git a/graphql/service/subscription/mutation_utils.go b/graphql/service/subscription/mutation_utils.go
|
||||
index e55e65b..e01a342 100644
|
||||
--- a/graphql/service/subscription/mutation_utils.go
|
||||
+++ b/graphql/service/subscription/mutation_utils.go
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/daeuniverse/dae-wing/dae"
|
||||
"github.com/daeuniverse/dae-wing/db"
|
||||
"github.com/daeuniverse/dae-wing/graphql/internal"
|
||||
+ "github.com/daeuniverse/dae-wing/graphql/service/config"
|
||||
"github.com/daeuniverse/dae-wing/graphql/service/node"
|
||||
"github.com/daeuniverse/dae/common/subscription"
|
||||
"github.com/go-co-op/gocron"
|
||||
@@ -171,8 +172,9 @@ func AutoUpdateVersionByIds(d *gorm.DB, ids []uint) (err error) {
|
||||
}
|
||||
|
||||
var (
|
||||
- schedulerCache = make(map[uint]*gocron.Scheduler)
|
||||
- schedulerMu sync.RWMutex
|
||||
+ schedulerCache = make(map[uint]*gocron.Scheduler)
|
||||
+ schedulerMu sync.RWMutex
|
||||
+ subscriptionUpdateMu sync.Mutex
|
||||
)
|
||||
|
||||
func UpdateAll(ctx context.Context) {
|
||||
@@ -258,68 +260,161 @@ func Update(ctx context.Context, _id graphql.ID) (r *Resolver, err error) {
|
||||
return &Resolver{Subscription: m}, nil
|
||||
}
|
||||
|
||||
-func UpdateById(ctx context.Context, subId uint) (sub *db.Subscription, err error) {
|
||||
- // Fetch node links.
|
||||
- var m db.Subscription
|
||||
- if err = db.DB(ctx).Where(&db.Subscription{ID: subId}).First(&m).Error; err != nil {
|
||||
- return nil, err
|
||||
+func reconcileSubscriptionNodes(tx *gorm.DB, subId uint, links []string) error {
|
||||
+ var existing []db.Node
|
||||
+ if err := tx.Where("subscription_id = ?", subId).Find(&existing).Error; err != nil {
|
||||
+ return err
|
||||
}
|
||||
- links, err := fetchLinks(m.Link)
|
||||
- if err != nil {
|
||||
- return nil, err
|
||||
+
|
||||
+ incoming := make(map[string]struct{}, len(links))
|
||||
+ for _, link := range links {
|
||||
+ incoming[link] = struct{}{}
|
||||
}
|
||||
|
||||
- tx := db.BeginTx(ctx)
|
||||
- defer func() {
|
||||
- if err == nil {
|
||||
- tx.Commit()
|
||||
+ current := make(map[string]struct{}, len(existing))
|
||||
+ var staleIds []uint
|
||||
+ for _, n := range existing {
|
||||
+ if _, ok := incoming[n.Link]; ok {
|
||||
+ current[n.Link] = struct{}{}
|
||||
+ continue
|
||||
+ }
|
||||
+ staleIds = append(staleIds, n.ID)
|
||||
+ }
|
||||
+
|
||||
+ fixed := make(map[uint]struct{})
|
||||
+ if len(staleIds) > 0 {
|
||||
+ var fixedIds []uint
|
||||
+ if err := tx.Table("group_nodes").
|
||||
+ Distinct("group_nodes.node_id").
|
||||
+ Joins("INNER JOIN groups ON groups.id = group_nodes.group_id").
|
||||
+ Where("groups.policy = ?", "fixed").
|
||||
+ Where("group_nodes.node_id IN ?", staleIds).
|
||||
+ Pluck("group_nodes.node_id", &fixedIds).Error; err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ for _, id := range fixedIds {
|
||||
+ fixed[id] = struct{}{}
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ var detachIds, deleteIds []uint
|
||||
+ for _, id := range staleIds {
|
||||
+ if _, ok := fixed[id]; ok {
|
||||
+ detachIds = append(detachIds, id)
|
||||
} else {
|
||||
- tx.Rollback()
|
||||
+ deleteIds = append(deleteIds, id)
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if len(staleIds) > 0 {
|
||||
+ if err := node.AutoUpdateVersionByIds(tx, staleIds); err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ }
|
||||
+ if len(detachIds) > 0 {
|
||||
+ if err := tx.Model(&db.Node{}).
|
||||
+ Where("id IN ?", detachIds).
|
||||
+ Update("subscription_id", nil).Error; err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ }
|
||||
+ if len(deleteIds) > 0 {
|
||||
+ if err := tx.Exec("DELETE FROM group_nodes WHERE node_id IN ?", deleteIds).Error; err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ if err := tx.Where("id IN ?", deleteIds).Delete(&db.Node{}).Error; err != nil {
|
||||
+ return err
|
||||
}
|
||||
- }()
|
||||
- // Remove those nodes whose subscription are independent from any groups.
|
||||
- subQuery := tx.Raw(`select nodes.id as id
|
||||
- from nodes
|
||||
- inner join group_nodes on group_nodes.node_id = nodes.id
|
||||
- where subscription_id = ?`, subId)
|
||||
-
|
||||
- if err = tx.Where("subscription_id = ?", subId).
|
||||
- Where("id not in (?)", subQuery).
|
||||
- Select(clause.Associations).
|
||||
- Delete(&db.Node{}).Error; err != nil {
|
||||
- return nil, err
|
||||
}
|
||||
- // Import node links.
|
||||
+
|
||||
+ seen := make(map[string]struct{}, len(links))
|
||||
var args []*internal.ImportArgument
|
||||
for _, link := range links {
|
||||
+ if _, ok := seen[link]; ok {
|
||||
+ continue
|
||||
+ }
|
||||
+ seen[link] = struct{}{}
|
||||
+ if _, ok := current[link]; ok {
|
||||
+ continue
|
||||
+ }
|
||||
args = append(args, &internal.ImportArgument{Link: link})
|
||||
}
|
||||
- result, err := node.Import(tx, false, &subId, args)
|
||||
+ result, err := node.Import(tx, true, &subId, args)
|
||||
if err != nil {
|
||||
- return nil, err
|
||||
+ return errors.New("failed to import subscription node")
|
||||
}
|
||||
- hasAnyCandidate := false
|
||||
for _, r := range result {
|
||||
- if r.Error == nil {
|
||||
- hasAnyCandidate = true
|
||||
- break
|
||||
+ if r.Error != nil {
|
||||
+ return errors.New("failed to import subscription node")
|
||||
}
|
||||
}
|
||||
- if !hasAnyCandidate {
|
||||
- return nil, fmt.Errorf("interrupt to update subscription: no any valid node can be imported")
|
||||
+
|
||||
+ var count int64
|
||||
+ if err := tx.Model(&db.Node{}).Where("subscription_id = ?", subId).Count(&count).Error; err != nil {
|
||||
+ return err
|
||||
}
|
||||
- // Update updated_at and return the latest version.
|
||||
- if err = tx.Model(&m).
|
||||
+ if count == 0 {
|
||||
+ return fmt.Errorf("interrupt to update subscription: no any valid node can be imported")
|
||||
+ }
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
+func updateSubscriptionTx(ctx context.Context, m *db.Subscription, subId uint, links []string) (err error) {
|
||||
+ subscriptionUpdateMu.Lock()
|
||||
+ defer subscriptionUpdateMu.Unlock()
|
||||
+
|
||||
+ tx := db.BeginTx(ctx)
|
||||
+ if tx.Error != nil {
|
||||
+ return tx.Error
|
||||
+ }
|
||||
+ defer func() {
|
||||
+ if err != nil {
|
||||
+ tx.Rollback()
|
||||
+ }
|
||||
+ }()
|
||||
+ var current db.Subscription
|
||||
+ if err = tx.Where(&db.Subscription{ID: subId}).First(¤t).Error; err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ if current.Link != m.Link {
|
||||
+ return fmt.Errorf("subscription changed during update; retry")
|
||||
+ }
|
||||
+ *m = current
|
||||
+
|
||||
+ if err = reconcileSubscriptionNodes(tx, subId, links); err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ q := tx.Model(m).
|
||||
Clauses(clause.Returning{}).
|
||||
Where(&db.Subscription{ID: subId}).
|
||||
- Update("updated_at", time.Now()).Error; err != nil {
|
||||
- return nil, err
|
||||
+ Update("updated_at", time.Now())
|
||||
+ if q.Error != nil {
|
||||
+ return q.Error
|
||||
+ }
|
||||
+ if q.RowsAffected == 0 {
|
||||
+ return fmt.Errorf("no such subscription")
|
||||
}
|
||||
-
|
||||
- // Update modified if subscription is referenced by running config.
|
||||
if err = AutoUpdateVersionByIds(tx, []uint{subId}); err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ return tx.Commit().Error
|
||||
+}
|
||||
+
|
||||
+func UpdateById(ctx context.Context, subId uint) (sub *db.Subscription, err error) {
|
||||
+ var m db.Subscription
|
||||
+ if err = db.DB(ctx).Where(&db.Subscription{ID: subId}).First(&m).Error; err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ links, err := fetchLinks(m.Link)
|
||||
+ if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
+ if err = updateSubscriptionTx(ctx, &m, subId, links); err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ if err = config.ApplyIfRunning(ctx); err != nil {
|
||||
+ return nil, fmt.Errorf("subscription updated but failed to apply runtime: %w", err)
|
||||
+ }
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
@@ -399,6 +494,8 @@ func UpdateLink(ctx context.Context, _id graphql.ID, link string) (r *Resolver,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
+ subscriptionUpdateMu.Lock()
|
||||
+ defer subscriptionUpdateMu.Unlock()
|
||||
|
||||
tx := db.BeginTx(ctx)
|
||||
defer func() {
|
||||
diff --git a/graphql/service/subscription/mutation_utils_test.go b/graphql/service/subscription/mutation_utils_test.go
|
||||
new file mode 100644
|
||||
index 0000000..18ff9b9
|
||||
--- /dev/null
|
||||
+++ b/graphql/service/subscription/mutation_utils_test.go
|
||||
@@ -0,0 +1,212 @@
|
||||
+/*
|
||||
+ * SPDX-License-Identifier: AGPL-3.0-only
|
||||
+ * Copyright (c) 2023, daeuniverse Organization <team@v2raya.org>
|
||||
+ */
|
||||
+
|
||||
+package subscription
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "fmt"
|
||||
+ "testing"
|
||||
+ "time"
|
||||
+
|
||||
+ "github.com/daeuniverse/dae-wing/db"
|
||||
+)
|
||||
+
|
||||
+func mustNoError(t *testing.T, err error) {
|
||||
+ t.Helper()
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func assertError(t *testing.T, err error) {
|
||||
+ t.Helper()
|
||||
+ if err == nil {
|
||||
+ t.Fatal("expected error")
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func assertEqualValues(t *testing.T, want, got interface{}) {
|
||||
+ t.Helper()
|
||||
+ if fmt.Sprint(want) != fmt.Sprint(got) {
|
||||
+ t.Fatalf("want %v, got %v", want, got)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func assertNotNil(t *testing.T, value *uint) {
|
||||
+ t.Helper()
|
||||
+ if value == nil {
|
||||
+ t.Fatal("expected non-nil value")
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func assertNil(t *testing.T, value *uint) {
|
||||
+ t.Helper()
|
||||
+ if value != nil {
|
||||
+ t.Fatalf("expected nil, got %v", *value)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func assertZero(t *testing.T, value int64) {
|
||||
+ t.Helper()
|
||||
+ if value != 0 {
|
||||
+ t.Fatalf("expected zero, got %d", value)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func initReconcileTestDB(t *testing.T) context.Context {
|
||||
+ t.Helper()
|
||||
+ mustNoError(t, db.InitDatabase(t.TempDir()))
|
||||
+ return context.Background()
|
||||
+}
|
||||
+
|
||||
+func createTestSubscription(t *testing.T, ctx context.Context, links ...string) (db.Subscription, []db.Node) {
|
||||
+ t.Helper()
|
||||
+ sub := db.Subscription{
|
||||
+ UpdatedAt: time.Now(),
|
||||
+ Link: "https://example.invalid/subscription",
|
||||
+ Status: "",
|
||||
+ Info: "",
|
||||
+ }
|
||||
+ mustNoError(t, db.DB(ctx).Create(&sub).Error)
|
||||
+
|
||||
+ nodes := make([]db.Node, 0, len(links))
|
||||
+ for i, link := range links {
|
||||
+ node := db.Node{
|
||||
+ Link: link,
|
||||
+ Name: "node",
|
||||
+ Address: "192.0.2.1",
|
||||
+ Protocol: "ss",
|
||||
+ SubscriptionID: &sub.ID,
|
||||
+ }
|
||||
+ node.Name += string(rune('A' + i))
|
||||
+ mustNoError(t, db.DB(ctx).Create(&node).Error)
|
||||
+ nodes = append(nodes, node)
|
||||
+ }
|
||||
+ return sub, nodes
|
||||
+}
|
||||
+
|
||||
+func createTestGroup(t *testing.T, ctx context.Context, policy string, systemID *uint, nodes ...db.Node) db.Group {
|
||||
+ t.Helper()
|
||||
+ group := db.Group{Name: policy + time.Now().Format("150405.000000000"), Policy: policy, SystemID: systemID}
|
||||
+ mustNoError(t, db.DB(ctx).Create(&group).Error)
|
||||
+ mustNoError(t, db.DB(ctx).Model(&group).Association("Node").Append(nodes))
|
||||
+ return group
|
||||
+}
|
||||
+
|
||||
+func countGroupNode(t *testing.T, ctx context.Context, groupID, nodeID uint) int64 {
|
||||
+ t.Helper()
|
||||
+ var count int64
|
||||
+ mustNoError(t, db.DB(ctx).Table("group_nodes").
|
||||
+ Where("group_id = ? AND node_id = ?", groupID, nodeID).
|
||||
+ Count(&count).Error)
|
||||
+ return count
|
||||
+}
|
||||
+
|
||||
+func TestReconcileKeepsCurrentReferencedNode(t *testing.T) {
|
||||
+ ctx := initReconcileTestDB(t)
|
||||
+ sub, nodes := createTestSubscription(t, ctx, "ss://current")
|
||||
+ group := createTestGroup(t, ctx, "min_moving_avg", nil, nodes[0])
|
||||
+
|
||||
+ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://current"}))
|
||||
+
|
||||
+ var got db.Node
|
||||
+ mustNoError(t, db.DB(ctx).First(&got, nodes[0].ID).Error)
|
||||
+ assertNotNil(t, got.SubscriptionID)
|
||||
+ assertEqualValues(t, sub.ID, *got.SubscriptionID)
|
||||
+ assertEqualValues(t, 1, countGroupNode(t, ctx, group.ID, got.ID))
|
||||
+}
|
||||
+
|
||||
+func TestReconcileDeletesStaleUnreferencedNode(t *testing.T) {
|
||||
+ ctx := initReconcileTestDB(t)
|
||||
+ sub, nodes := createTestSubscription(t, ctx, "ss://current", "ss://stale")
|
||||
+
|
||||
+ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://current"}))
|
||||
+
|
||||
+ var count int64
|
||||
+ mustNoError(t, db.DB(ctx).Model(&db.Node{}).Where("id = ?", nodes[1].ID).Count(&count).Error)
|
||||
+ assertZero(t, count)
|
||||
+}
|
||||
+
|
||||
+func TestReconcileDeletesStaleNormalGroupNode(t *testing.T) {
|
||||
+ ctx := initReconcileTestDB(t)
|
||||
+ system := db.System{Running: true}
|
||||
+ mustNoError(t, db.DB(ctx).Create(&system).Error)
|
||||
+ sub, nodes := createTestSubscription(t, ctx, "ss://current", "ss://stale")
|
||||
+ group := createTestGroup(t, ctx, "min_moving_avg", &system.ID, nodes...)
|
||||
+
|
||||
+ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://current"}))
|
||||
+
|
||||
+ var gotGroup db.Group
|
||||
+ mustNoError(t, db.DB(ctx).First(&gotGroup, group.ID).Error)
|
||||
+ assertEqualValues(t, 1, gotGroup.Version)
|
||||
+ assertEqualValues(t, 0, countGroupNode(t, ctx, group.ID, nodes[1].ID))
|
||||
+}
|
||||
+
|
||||
+func TestReconcileDetachesStaleFixedNode(t *testing.T) {
|
||||
+ ctx := initReconcileTestDB(t)
|
||||
+ sub, nodes := createTestSubscription(t, ctx, "ss://current", "ss://fixed-stale")
|
||||
+ group := createTestGroup(t, ctx, "fixed", nil, nodes[1])
|
||||
+
|
||||
+ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://current"}))
|
||||
+
|
||||
+ var got db.Node
|
||||
+ mustNoError(t, db.DB(ctx).First(&got, nodes[1].ID).Error)
|
||||
+ assertNil(t, got.SubscriptionID)
|
||||
+ assertEqualValues(t, 1, countGroupNode(t, ctx, group.ID, got.ID))
|
||||
+}
|
||||
+
|
||||
+func TestReconcilePreservesNodeReferencedByFixedAndNormalGroups(t *testing.T) {
|
||||
+ ctx := initReconcileTestDB(t)
|
||||
+ sub, nodes := createTestSubscription(t, ctx, "ss://current", "ss://shared-stale")
|
||||
+ fixed := createTestGroup(t, ctx, "fixed", nil, nodes[1])
|
||||
+ normal := createTestGroup(t, ctx, "min", nil, nodes[1])
|
||||
+
|
||||
+ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://current"}))
|
||||
+
|
||||
+ var got db.Node
|
||||
+ mustNoError(t, db.DB(ctx).First(&got, nodes[1].ID).Error)
|
||||
+ assertNil(t, got.SubscriptionID)
|
||||
+ assertEqualValues(t, 1, countGroupNode(t, ctx, fixed.ID, got.ID))
|
||||
+ assertEqualValues(t, 1, countGroupNode(t, ctx, normal.ID, got.ID))
|
||||
+}
|
||||
+
|
||||
+func TestReconcileAllLinksAlreadyExistSucceeds(t *testing.T) {
|
||||
+ ctx := initReconcileTestDB(t)
|
||||
+ sub, nodes := createTestSubscription(t, ctx, "ss://one", "ss://two")
|
||||
+
|
||||
+ mustNoError(t, reconcileSubscriptionNodes(db.DB(ctx), sub.ID, []string{"ss://one", "ss://two", "ss://two"}))
|
||||
+
|
||||
+ var count int64
|
||||
+ mustNoError(t, db.DB(ctx).Model(&db.Node{}).Where("subscription_id = ?", sub.ID).Count(&count).Error)
|
||||
+ assertEqualValues(t, len(nodes), count)
|
||||
+}
|
||||
+
|
||||
+func TestReconcileRollsBackWhenAnyIncomingNodeIsInvalid(t *testing.T) {
|
||||
+ ctx := initReconcileTestDB(t)
|
||||
+ sub, nodes := createTestSubscription(t, ctx, "ss://current", "ss://stale")
|
||||
+ tx := db.BeginTx(ctx)
|
||||
+
|
||||
+ assertError(t, reconcileSubscriptionNodes(tx, sub.ID, []string{"ss://current", "not-a-node-link"}))
|
||||
+ mustNoError(t, tx.Rollback().Error)
|
||||
+
|
||||
+ var count int64
|
||||
+ mustNoError(t, db.DB(ctx).Model(&db.Node{}).Where("id IN ?", []uint{nodes[0].ID, nodes[1].ID}).Count(&count).Error)
|
||||
+ assertEqualValues(t, 2, count)
|
||||
+}
|
||||
+
|
||||
+func TestUpdateSubscriptionRejectsChangedLink(t *testing.T) {
|
||||
+ ctx := initReconcileTestDB(t)
|
||||
+ sub, nodes := createTestSubscription(t, ctx, "ss://current")
|
||||
+ snapshot := sub
|
||||
+ mustNoError(t, db.DB(ctx).Model(&sub).Update("link", "https://example.invalid/changed").Error)
|
||||
+
|
||||
+ assertError(t, updateSubscriptionTx(ctx, &snapshot, sub.ID, []string{"ss://replacement"}))
|
||||
+
|
||||
+ var got db.Node
|
||||
+ mustNoError(t, db.DB(ctx).First(&got, nodes[0].ID).Error)
|
||||
+ assertNotNil(t, got.SubscriptionID)
|
||||
+ assertEqualValues(t, sub.ID, *got.SubscriptionID)
|
||||
+}
|
||||
@@ -0,0 +1,24 @@
|
||||
diff --git a/graphql/service/subscription/mutation_utils.go b/graphql/service/subscription/mutation_utils.go
|
||||
index e01a342..c6bc3e1 100644
|
||||
--- a/graphql/service/subscription/mutation_utils.go
|
||||
+++ b/graphql/service/subscription/mutation_utils.go
|
||||
@@ -171,6 +171,8 @@ func AutoUpdateVersionByIds(d *gorm.DB, ids []uint) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
+const scheduledUpdateTimeout = 3 * time.Minute
|
||||
+
|
||||
var (
|
||||
schedulerCache = make(map[uint]*gocron.Scheduler)
|
||||
schedulerMu sync.RWMutex
|
||||
@@ -224,7 +226,9 @@ func AddUpdateScheduler(ctx context.Context, id uint) {
|
||||
}
|
||||
logrus.Info("Subscription " + tag + " update task enabled, with exp " + sub.CronExp)
|
||||
_, err := s.Cron(sub.CronExp).Do(func() {
|
||||
- if _, err := UpdateById(context.Background(), sub.ID); err != nil {
|
||||
+ ctx, cancel := context.WithTimeout(context.Background(), scheduledUpdateTimeout)
|
||||
+ defer cancel()
|
||||
+ if _, err := UpdateById(ctx, sub.ID); err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
Subject: [PATCH] daed: open sqlite database in WAL mode
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
订阅自动更新写库时,WebUI 登录查询被锁 5 秒后报 database is locked。
|
||||
|
||||
开 WAL 并把 busy_timeout 提到 30 秒,读不再被写事务阻塞;WAL 附带的两个文件跟主库一样收紧到 0640。
|
||||
---
|
||||
db/db.go | 17 ++++++++++++-----
|
||||
pkg/sqlite/sqlite_mipsarch.go | 4 +++-
|
||||
pkg/sqlite/sqlite_others.go | 4 +++-
|
||||
3 files changed, 18 insertions(+), 7 deletions(-)
|
||||
|
||||
diff --git a/db/db.go b/db/db.go
|
||||
index a79022b..6145443 100644
|
||||
--- a/db/db.go
|
||||
+++ b/db/db.go
|
||||
@@ -50,13 +50,20 @@ func InitDatabase(configDir string) (err error) {
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
- if fi, err := os.Stat(path); err != nil {
|
||||
- return err
|
||||
- } else if fi.Mode()&0037 > 0 {
|
||||
- // Too open, chmod it to 0640.
|
||||
- if err = os.Chmod(path, 0640); err != nil {
|
||||
+ for _, p := range []string{path, path + "-wal", path + "-shm"} {
|
||||
+ fi, err := os.Stat(p)
|
||||
+ if os.IsNotExist(err) {
|
||||
+ continue
|
||||
+ }
|
||||
+ if err != nil {
|
||||
return err
|
||||
}
|
||||
+ if fi.Mode()&0037 > 0 {
|
||||
+ // Too open, chmod it to 0640.
|
||||
+ if err = os.Chmod(p, 0640); err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
return nil
|
||||
diff --git a/pkg/sqlite/sqlite_mipsarch.go b/pkg/sqlite/sqlite_mipsarch.go
|
||||
index 7b8c1b4..8c275e5 100644
|
||||
--- a/pkg/sqlite/sqlite_mipsarch.go
|
||||
+++ b/pkg/sqlite/sqlite_mipsarch.go
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
+const pragmas = "?_journal_mode=WAL&_busy_timeout=30000&_synchronous=FULL"
|
||||
+
|
||||
func Open(dsn string) gorm.Dialector {
|
||||
- return sqlite.Open(dsn)
|
||||
+ return sqlite.Open(dsn + pragmas)
|
||||
}
|
||||
diff --git a/pkg/sqlite/sqlite_others.go b/pkg/sqlite/sqlite_others.go
|
||||
index 4598651..83d8b80 100644
|
||||
--- a/pkg/sqlite/sqlite_others.go
|
||||
+++ b/pkg/sqlite/sqlite_others.go
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
+const pragmas = "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(FULL)"
|
||||
+
|
||||
func Open(dsn string) gorm.Dialector {
|
||||
- return sqlite.Open(dsn)
|
||||
+ return sqlite.Open(dsn + pragmas)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
Subject: [PATCH] daed: reload dae outside the database transaction
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
配置生效前先提交读事务,reload 完再用短事务写运行状态。
|
||||
|
||||
原来整个 reload 都在事务里,最长可占着数据库锁一百多秒;写状态时按 ID 重查仍存在的分组,避免把 reload 期间被删的分组重新插回去。
|
||||
---
|
||||
graphql/service/config/mutation_utils.go | 77 ++++++++++++++++--------
|
||||
1 file changed, 53 insertions(+), 24 deletions(-)
|
||||
|
||||
diff --git a/graphql/service/config/mutation_utils.go b/graphql/service/config/mutation_utils.go
|
||||
index feb94d3..59f0ebd 100644
|
||||
--- a/graphql/service/config/mutation_utils.go
|
||||
+++ b/graphql/service/config/mutation_utils.go
|
||||
@@ -270,28 +270,12 @@ func restartAfterReloadTimeout() {
|
||||
}()
|
||||
}
|
||||
|
||||
-func runTransaction(ctx context.Context, noLoad bool) (n int32, err error) {
|
||||
- tx := db.BeginTx(ctx)
|
||||
- if tx.Error != nil {
|
||||
- return 0, tx.Error
|
||||
- }
|
||||
- n, err = runLocked(tx, noLoad)
|
||||
- if err != nil {
|
||||
- tx.Rollback()
|
||||
- return 0, err
|
||||
- }
|
||||
- if err = tx.Commit().Error; err != nil {
|
||||
- return 0, err
|
||||
- }
|
||||
- return n, nil
|
||||
-}
|
||||
-
|
||||
func Run(ctx context.Context, noLoad bool) (n int32, err error) {
|
||||
if ok := runLock.TryLock(); !ok {
|
||||
return 0, fmt.Errorf("the last request didn't complete; make a cup of tea and take a break")
|
||||
}
|
||||
defer runLock.Unlock()
|
||||
- return runTransaction(ctx, noLoad)
|
||||
+ return runLocked(ctx, noLoad)
|
||||
}
|
||||
|
||||
func ApplyIfRunning(ctx context.Context) error {
|
||||
@@ -307,11 +291,11 @@ func ApplyIfRunning(ctx context.Context) error {
|
||||
if err != nil || !modified {
|
||||
return err
|
||||
}
|
||||
- _, err = runTransaction(ctx, false)
|
||||
+ _, err = runLocked(ctx, false)
|
||||
return err
|
||||
}
|
||||
|
||||
-func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
|
||||
+func runLocked(ctx context.Context, noLoad bool) (n int32, err error) {
|
||||
//// Dry run.
|
||||
if noLoad {
|
||||
ch := make(chan error)
|
||||
@@ -336,18 +320,38 @@ func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
|
||||
}
|
||||
|
||||
// Running -> false
|
||||
+ tx := db.BeginTx(ctx)
|
||||
+ if tx.Error != nil {
|
||||
+ return 0, tx.Error
|
||||
+ }
|
||||
var sys db.System
|
||||
- if err = d.Model(&db.System{}).FirstOrCreate(&sys).Error; err != nil {
|
||||
+ if err = tx.Model(&db.System{}).FirstOrCreate(&sys).Error; err != nil {
|
||||
+ tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
- if err = d.Model(&sys).Updates(map[string]interface{}{
|
||||
+ if err = tx.Model(&sys).Updates(map[string]interface{}{
|
||||
"running": false,
|
||||
}).Error; err != nil {
|
||||
+ tx.Rollback()
|
||||
+ return 0, err
|
||||
+ }
|
||||
+ if err = tx.Commit().Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
+ d := db.BeginTx(ctx)
|
||||
+ if d.Error != nil {
|
||||
+ return 0, d.Error
|
||||
+ }
|
||||
+ readTx := d
|
||||
+ defer func() {
|
||||
+ if readTx != nil {
|
||||
+ readTx.Rollback()
|
||||
+ }
|
||||
+ }()
|
||||
+
|
||||
//// Run selected global+dns+routing.
|
||||
/// Get them from database and parse them to daeConfig.
|
||||
var mConfig db.Config
|
||||
@@ -513,6 +517,11 @@ func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
|
||||
c.Node = append(c.Node, daeConfig.KeyableString(fmt.Sprintf("%v:%v", node.uniqueName, node.dbNode.Link)))
|
||||
}
|
||||
|
||||
+ if err = readTx.Commit().Error; err != nil {
|
||||
+ return 0, err
|
||||
+ }
|
||||
+ readTx = nil
|
||||
+
|
||||
/// Reload with current config.
|
||||
chReloadCallback := make(chan error)
|
||||
reloadMsg := &dae.ReloadMessage{
|
||||
@@ -536,20 +545,37 @@ func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
|
||||
}
|
||||
|
||||
// Save running status
|
||||
+ tx := db.BeginTx(context.WithoutCancel(ctx))
|
||||
+ if tx.Error != nil {
|
||||
+ return 0, tx.Error
|
||||
+ }
|
||||
+ defer func() {
|
||||
+ if err != nil {
|
||||
+ tx.Rollback()
|
||||
+ }
|
||||
+ }()
|
||||
var sys db.System
|
||||
- if err = d.Model(&db.System{}).FirstOrCreate(&sys).Error; err != nil {
|
||||
+ if err = tx.Model(&db.System{}).FirstOrCreate(&sys).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var gvs uint
|
||||
var gids []string
|
||||
+ groupIds := make([]uint, 0, len(groups))
|
||||
for _, g := range groups {
|
||||
gvs += g.Version
|
||||
gids = append(gids, fmt.Sprintf("%x", g.ID))
|
||||
+ groupIds = append(groupIds, g.ID)
|
||||
+ }
|
||||
+ liveGroups := make([]db.Group, 0, len(groupIds))
|
||||
+ if len(groupIds) > 0 {
|
||||
+ if err = tx.Where("id IN ?", groupIds).Find(&liveGroups).Error; err != nil {
|
||||
+ return 0, err
|
||||
+ }
|
||||
}
|
||||
sort.Slice(gids, func(i, j int) bool {
|
||||
return gids[i] < gids[j]
|
||||
})
|
||||
- if err = d.Model(&sys).Updates(map[string]interface{}{
|
||||
+ if err = tx.Model(&sys).Updates(map[string]interface{}{
|
||||
"running": true,
|
||||
"running_config_id": mConfig.ID,
|
||||
"running_config_version": mConfig.Version,
|
||||
@@ -562,7 +588,10 @@ func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
|
||||
}).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
- if err = d.Model(&sys).Association("RunningGroups").Replace(groups); err != nil {
|
||||
+ if err = tx.Model(&sys).Association("RunningGroups").Replace(liveGroups); err != nil {
|
||||
+ return 0, err
|
||||
+ }
|
||||
+ if err = tx.Commit().Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
Subject: [PATCH] daed: serialize scheduled subscription updates
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
订阅更新的入口读也纳入同一把锁,整轮更新串行,排到才开始计时。
|
||||
|
||||
多个订阅定时任务撞在同一分钟时会互相锁死。
|
||||
---
|
||||
.../service/subscription/mutation_utils.go | 29 +++++++++++++------
|
||||
1 file changed, 20 insertions(+), 9 deletions(-)
|
||||
|
||||
diff --git a/graphql/service/subscription/mutation_utils.go b/graphql/service/subscription/mutation_utils.go
|
||||
index c6bc3e1..e2aec32 100644
|
||||
--- a/graphql/service/subscription/mutation_utils.go
|
||||
+++ b/graphql/service/subscription/mutation_utils.go
|
||||
@@ -225,12 +225,8 @@ func AddUpdateScheduler(ctx context.Context, id uint) {
|
||||
tag = *sub.Tag
|
||||
}
|
||||
logrus.Info("Subscription " + tag + " update task enabled, with exp " + sub.CronExp)
|
||||
- _, err := s.Cron(sub.CronExp).Do(func() {
|
||||
- ctx, cancel := context.WithTimeout(context.Background(), scheduledUpdateTimeout)
|
||||
- defer cancel()
|
||||
- if _, err := UpdateById(ctx, sub.ID); err != nil {
|
||||
- logrus.Error(err)
|
||||
- }
|
||||
+ _, err := s.Cron(sub.CronExp).SingletonMode().Do(func() {
|
||||
+ runScheduledUpdate(sub.ID)
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to schedule subscription %d update: invalid cron expression '%s': %v", sub.ID, sub.CronExp, err)
|
||||
@@ -364,9 +360,6 @@ func reconcileSubscriptionNodes(tx *gorm.DB, subId uint, links []string) error {
|
||||
}
|
||||
|
||||
func updateSubscriptionTx(ctx context.Context, m *db.Subscription, subId uint, links []string) (err error) {
|
||||
- subscriptionUpdateMu.Lock()
|
||||
- defer subscriptionUpdateMu.Unlock()
|
||||
-
|
||||
tx := db.BeginTx(ctx)
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
@@ -404,7 +397,25 @@ func updateSubscriptionTx(ctx context.Context, m *db.Subscription, subId uint, l
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
+func runScheduledUpdate(subId uint) {
|
||||
+ subscriptionUpdateMu.Lock()
|
||||
+ defer subscriptionUpdateMu.Unlock()
|
||||
+
|
||||
+ ctx, cancel := context.WithTimeout(context.Background(), scheduledUpdateTimeout)
|
||||
+ defer cancel()
|
||||
+ if _, err := updateByIdLocked(ctx, subId); err != nil {
|
||||
+ logrus.Error(err)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
func UpdateById(ctx context.Context, subId uint) (sub *db.Subscription, err error) {
|
||||
+ subscriptionUpdateMu.Lock()
|
||||
+ defer subscriptionUpdateMu.Unlock()
|
||||
+
|
||||
+ return updateByIdLocked(ctx, subId)
|
||||
+}
|
||||
+
|
||||
+func updateByIdLocked(ctx context.Context, subId uint) (sub *db.Subscription, err error) {
|
||||
var m db.Subscription
|
||||
if err = db.DB(ctx).Where(&db.Subscription{ID: subId}).First(&m).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -0,0 +1,44 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
Subject: [PATCH] daed: apply subscription cron changes without restart
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
改 cron 后先提交事务再重建调度器。
|
||||
|
||||
原来在事务里就去重建,调度器另开连接读到的还是旧表达式,必须重启 daed 才生效。
|
||||
---
|
||||
graphql/service/subscription/mutation_utils.go | 12 +++++++++---
|
||||
1 file changed, 9 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/graphql/service/subscription/mutation_utils.go b/graphql/service/subscription/mutation_utils.go
|
||||
index e2aec32..4f6bcfc 100644
|
||||
--- a/graphql/service/subscription/mutation_utils.go
|
||||
+++ b/graphql/service/subscription/mutation_utils.go
|
||||
@@ -561,10 +561,12 @@ func UpdateCron(ctx context.Context, _id graphql.ID, cronExp string, cronEnable
|
||||
}
|
||||
|
||||
tx := db.BeginTx(ctx)
|
||||
+ if tx.Error != nil {
|
||||
+ return nil, tx.Error
|
||||
+ }
|
||||
+ committed := false
|
||||
defer func() {
|
||||
- if err == nil {
|
||||
- tx.Commit()
|
||||
- } else {
|
||||
+ if !committed {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
@@ -583,6 +585,10 @@ func UpdateCron(ctx context.Context, _id graphql.ID, cronExp string, cronEnable
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
+ if err = tx.Commit().Error; err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ committed = true
|
||||
|
||||
// Update scheduler
|
||||
RemoveUpdateScheduler(id)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
--- a/dae-core/trace/kern/trace.c
|
||||
+++ b/dae-core/trace/kern/trace.c
|
||||
@@ -228,7 +228,7 @@ KPROBE_SKB_AT(1)
|
||||
KPROBE_SKB_AT(2)
|
||||
KPROBE_SKB_AT(3)
|
||||
KPROBE_SKB_AT(4)
|
||||
-KPROBE_SKB_AT(5)
|
||||
+//KPROBE_SKB_AT(5)
|
||||
|
||||
SEC("kprobe/skb_lifetime_termination")
|
||||
int kprobe_skb_lifetime_termination(struct pt_regs *ctx)
|
||||
--- a/dae-core/trace/trace.go
|
||||
+++ b/dae-core/trace/trace.go
|
||||
@@ -206,8 +206,8 @@ func attachBpfToTargets(objs *bpfObjects
|
||||
kp, err = link.Kprobe(fn, objs.KprobeSkb3, nil)
|
||||
case 4:
|
||||
kp, err = link.Kprobe(fn, objs.KprobeSkb4, nil)
|
||||
- case 5:
|
||||
- kp, err = link.Kprobe(fn, objs.KprobeSkb5, nil)
|
||||
+ //case 5:
|
||||
+ // kp, err = link.Kprobe(fn, objs.KprobeSkb5, nil)
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Debugf("failed to attach kprobe to %s: %+v\n", fn, err)
|
||||
+13
-11
@@ -1,21 +1,19 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#
|
||||
# Copyright (C) 2021-2023 sirpdboy <herboy2008@gmail.com>
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
# Copyright (C) 2021-2026 sirpdboy <herboy2008@gmail.com>
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=ddns-go
|
||||
PKG_VERSION:=6.17.6
|
||||
PKG_VERSION:=6.17.7
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_VERSION:=6.17.7
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=5fd986644132678b6e80be6dfa5d57253b1640d661053e4820daa57320b42720
|
||||
PKG_HASH:=f7001004e092d9641aad5a94158e0b4cae4a53a7f5c7d96d5c6af3d246c56fcc
|
||||
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_HASH:=f7001004e092d9641aad5a94158e0b4cae4a53a7f5c7d96d5c6af3d246c56fcc
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
@@ -44,14 +42,18 @@ define Package/ddns-go/description
|
||||
support Alidns Dnspod Cloudflare Hicloud Callback Baiducloud porkbun GoDaddy Google Domains.
|
||||
endef
|
||||
|
||||
define Package/ddns-go/conffiles
|
||||
/etc/config/ddns-go
|
||||
/etc/ddns-go/ddns-go-config.yaml
|
||||
endef
|
||||
|
||||
define Package/ddns-go/install
|
||||
$(call GoPackage/Package/Install/Bin,$(1))
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/file/ddns-go.init $(1)/etc/init.d/ddns-go
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) $(CURDIR)/file/luci-ddns-go.uci-default $(1)/etc/uci-defaults/luci-ddns-go
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_BIN) $(CURDIR)/files/ddns-go.init $(1)/etc/init.d/ddns-go
|
||||
$(INSTALL_CONF) $(CURDIR)/files/ddns-go.conf $(1)/etc/config/ddns-go
|
||||
endef
|
||||
|
||||
$(eval $(call GoBinPackage,ddns-go))
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
#
|
||||
# Copyright (C) 2021-2023 sirpdboy <herboy2008@gmail.com> https://github.com/sirpdboy/luci-app-ddns-go
|
||||
#
|
||||
# This file is part of ddns-go .
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/bin/ddns-go
|
||||
CONFDIR=/etc/ddns-go
|
||||
CONF=$CONFDIR/ddns-go-config.yaml
|
||||
|
||||
get_config() {
|
||||
config_get_bool enabled $1 enabled 1
|
||||
config_get_bool logger $1 logger 1
|
||||
config_get port $1 port 9876
|
||||
config_get time $1 time 300
|
||||
}
|
||||
|
||||
init_yaml(){
|
||||
[ -d $CONFDIR ] || mkdir -p $CONFDIR 2>/dev/null
|
||||
cat /usr/share/ddns-go/ddns-go-default.yaml > $CONF
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load ddns-go
|
||||
config_foreach get_config basic
|
||||
[ x$enabled == x1 ] || return 1
|
||||
[ -s ${CONF} ] || init_yaml
|
||||
logger -t ddns-go -p warn "ddns-go is start."
|
||||
echo "ddns-go is start."
|
||||
procd_open_instance
|
||||
procd_set_param command $PROG -l :$port -f $time -c "$CONF"
|
||||
[ "x$logger" == x1 ] && procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "ddns-go"
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ -s "/etc/ddns-go/localtime" ] && mv -f /etc/ddns-go/localtime /etc/localtime
|
||||
/etc/init.d/ddns-go enable
|
||||
/etc/init.d/ddns-go start
|
||||
rm -f /tmp/luci*
|
||||
exit 0
|
||||
@@ -0,0 +1,9 @@
|
||||
config basic 'config'
|
||||
option enabled '0'
|
||||
option logger '1'
|
||||
option port '9876'
|
||||
option time '300'
|
||||
option ctimes '5'
|
||||
option skipverify '0'
|
||||
option delay '0'
|
||||
option dns '223.5.5.5'
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
#
|
||||
# Copyright (C) 2021-2026 sirpdboy <herboy2008@gmail.com>
|
||||
#
|
||||
# This file is part of ddns-go .
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
NAME=ddns-go
|
||||
PROG=/usr/bin/ddns-go
|
||||
CONFDIR=/etc/ddns-go
|
||||
CONF=$CONFDIR/ddns-go-config.yaml
|
||||
|
||||
init_yaml() {
|
||||
[ -d "$CONFDIR" ] || mkdir -p "$CONFDIR"
|
||||
chown -R ddns-go:ddns-go "$CONFDIR"
|
||||
chmod 755 "$CONFDIR"
|
||||
[ -f "$CONF" ] && chmod 644 "$CONF"
|
||||
}
|
||||
|
||||
build_args() {
|
||||
local cfg="$1"
|
||||
local args="-c $CONF"
|
||||
|
||||
config_get port "$cfg" port '9876'
|
||||
args="$args -l :$port"
|
||||
|
||||
config_get time "$cfg" time '300'
|
||||
[ -n "$time" ] && args="$args -f $time"
|
||||
|
||||
config_get ctimes "$cfg" ctimes '5'
|
||||
[ -n "$ctimes" ] && args="$args -cacheTimes $ctimes"
|
||||
|
||||
config_get dns "$cfg" dns '223.5.5.5'
|
||||
[ -n "$dns" ] && args="$args -dns $dns"
|
||||
|
||||
config_get_bool noweb "$cfg" noweb 0
|
||||
[ "$noweb" -eq 1 ] && args="$args -noweb"
|
||||
|
||||
config_get_bool skipverify "$cfg" skipverify 0
|
||||
[ "$skipverify" -eq 1 ] && args="$args -skipVerify"
|
||||
|
||||
echo "$args"
|
||||
}
|
||||
|
||||
start_instance() {
|
||||
local cfg="$1"
|
||||
local logger
|
||||
config_get_bool enabled "$cfg" enabled 0
|
||||
[ "$enabled" -eq 0 ] && return 0
|
||||
|
||||
config_get delay "$cfg" delay 0
|
||||
if [ "$delay" -gt 0 ]; then
|
||||
local uptime=$(awk -F. '{print $1}' /proc/uptime)
|
||||
[ "$uptime" -lt 120 ] && sleep "$delay"
|
||||
fi
|
||||
|
||||
init_yaml
|
||||
|
||||
local args=$(build_args "$cfg")
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command $PROG $args
|
||||
|
||||
config_get_bool logger "$cfg" logger 1
|
||||
procd_set_param stdout "$logger"
|
||||
procd_set_param stderr "$logger"
|
||||
|
||||
procd_set_param user ddns-go
|
||||
procd_set_param respawn
|
||||
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$NAME"
|
||||
config_foreach start_instance 'basic'
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$NAME"
|
||||
}
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=docker
|
||||
PKG_VERSION:=29.7.2
|
||||
PKG_VERSION:=29.8.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/docker/cli/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=225b7ab2a15f5230b482df8461069cd4bce38891266fb9898d4188d0a3cbf54a
|
||||
PKG_HASH:=c5fadbc00c02dbecb1b7c9936e188baf9c80421a9107e7e9ad36a0923a0fc764
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/cli-$(PKG_VERSION)
|
||||
PKG_GIT_SHORT_COMMIT:=$(shell $(CURDIR)/git-short-commit.sh 'github.com/docker/cli' 'v$(PKG_VERSION)' '$(TMP_DIR)/git-short-commit/$(PKG_NAME)-$(PKG_VERSION)')
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dockerd
|
||||
PKG_VERSION:=29.7.2
|
||||
PKG_VERSION:=29.8.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
@@ -10,7 +10,7 @@ PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_GIT_URL:=github.com/moby/moby
|
||||
PKG_GIT_REF:=docker-v$(PKG_VERSION)
|
||||
PKG_SOURCE_URL:=https://codeload.$(PKG_GIT_URL)/tar.gz/$(PKG_GIT_REF)?
|
||||
PKG_HASH:=3a93a88bff41ffa6f4dca9f4ed9fc05e7fdb08e0f9014cf1d8177f85ecbc0683
|
||||
PKG_HASH:=e75ffb5d2ddc1fd98138fdb5e29f707b59f415ec8697e73b8bbdf8bbbb4be8eb
|
||||
PKG_GIT_SHORT_COMMIT:=$(shell $(CURDIR)/git-short-commit.sh '$(PKG_GIT_URL)' '$(PKG_GIT_REF)' '$(TMP_DIR)/git-short-commit/$(PKG_NAME)-$(PKG_VERSION)')
|
||||
|
||||
PKG_MAINTAINER:=Gerard Ryan <G.M0N3Y.2503@gmail.com>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
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))
|
||||
@@ -0,0 +1,5 @@
|
||||
config filebrowser 'config'
|
||||
option enabled '0'
|
||||
option listen_port '8787'
|
||||
option root_path '/'
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/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"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#
|
||||
# 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))
|
||||
+3
-2
@@ -8,12 +8,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=go-ethereum
|
||||
PKG_VERSION:=1.10.20
|
||||
PKG_VERSION:=1.17.5
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/ethereum/go-ethereum/tar.gz/v${PKG_VERSION}?
|
||||
PKG_HASH:=15ff54f0a4444eb9faa7c1f6219d3a1db5d547178b4eef6679bb601abc681f9d
|
||||
PKG_HASH:=8428049b30e76efcd19507225aa67c67d5d98c10a0f3a4ea339dfbba285bac7d
|
||||
|
||||
PKG_MAINTAINER:=Mislav Novakovic <mislav.novakovic@sartura.hr>
|
||||
PKG_LICENSE:=GPL-3.0-or-later LGPL-3.0-or-later
|
||||
@@ -26,6 +26,7 @@ PKG_CONFIG_DEPENDS:=CONFIG_BUILD_NLS
|
||||
|
||||
GO_PKG:=github.com/ethereum/go-ethereum
|
||||
GO_PKG_BUILD_PKG:=github.com/ethereum/go-ethereum/cmd/geth
|
||||
GO_PKG_TAGS:=untested_go_version
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/nls.mk
|
||||
|
||||
@@ -9,7 +9,7 @@ PROG=/usr/bin/geth
|
||||
start_service() {
|
||||
procd_open_instance
|
||||
procd_set_param command ${PROG}
|
||||
procd_append_param command --syncmode "light" --cache 1024
|
||||
procd_append_param command --syncmode "snap" --cache 1024
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
+3
-3
@@ -9,9 +9,9 @@ PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://gn.googlesource.com/gn.git
|
||||
PKG_SOURCE_DATE:=2026-08-19
|
||||
PKG_SOURCE_VERSION:=58933a7cdbc90f70f2381f0c72e76d29be1d43a9
|
||||
PKG_MIRROR_HASH:=b4fd70abf2fe8e3017ac3c4e025fd25cd4364dbe2470035b3daf58a129f9f54f
|
||||
PKG_SOURCE_DATE:=2026-09-11
|
||||
PKG_SOURCE_VERSION:=cfcd774b98f3433e18b722f9a7ff06119825b8eb
|
||||
PKG_MIRROR_HASH:=d8bea9ac89f9e87f36874601588f0d8d32221bf1ad8488f2fd9775abcddf4860
|
||||
|
||||
PKG_LICENSE:=BSD 3-Clause
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#ifndef OUT_LAST_COMMIT_POSITION_H_
|
||||
#define OUT_LAST_COMMIT_POSITION_H_
|
||||
|
||||
#define LAST_COMMIT_POSITION_NUM 2528
|
||||
#define LAST_COMMIT_POSITION "2528 (58933a7cdbc9)"
|
||||
#define LAST_COMMIT_POSITION_NUM 2563
|
||||
#define LAST_COMMIT_POSITION "2563 (cfcd774b98f3)"
|
||||
|
||||
#endif // OUT_LAST_COMMIT_POSITION_H_
|
||||
|
||||
+2
-2
@@ -5,12 +5,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gost
|
||||
PKG_VERSION:=3.2.6
|
||||
PKG_VERSION:=3.3.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/go-gost/gost/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=79874354530b899576dd4866d3b1400651d0b17c1e7a90ad30c44686a0642600
|
||||
PKG_HASH:=2a65e2da14fef6b6da8d4e32a8bc62e39970dbb141db42bc6f5821f90ac1e9a3
|
||||
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user