update 2026-11-04 05:27:41

This commit is contained in:
action committed 2026-11-04 05:27:41 +08:00
1 parent 0350fcc2e5
commit 1b82056d99
1124 files changed
+3 -574287

No files matched your search

+3 -3
View File
@@ -3,17 +3,17 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=adguardhome
PKG_VERSION:=0.107.79
PKG_VERSION:=0.107.80
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/AdguardTeam/AdGuardHome/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=5f4fa119f24ea741bb8cecfa74203165fe6b86b904cef05ff429fb227b07d425
PKG_HASH:=c67cf289f1d4b05c78994f4d1d169962540814a212244eb191cd599795d90278
PKG_BUILD_DIR:=$(BUILD_DIR)/AdGuardHome-$(PKG_VERSION)
FRONTEND_DEST:=$(PKG_NAME)-frontend-$(PKG_VERSION).tar.gz
FRONTEND_URL:=https://github.com/AdguardTeam/AdGuardHome/releases/download/v$(PKG_VERSION)/
FRONTEND_HASH:=30184c157d4aefe5371b575a43c0ab5a7314aef039a820cc421ea67bab994ce8
FRONTEND_HASH:=f01f3c7713a8190dd0aaaf18c3f9a0e38235c21d696325bfca9dc681e389d894
PKG_LICENSE:=GPL-3.0-only
PKG_LICENSE_FILES:=LICENSE.txt
-156
View File
@@ -1,156 +0,0 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=clashoo
PKG_UPSTREAM_VERSION:=1.19.33
PKG_UPSTREAM_TAG:=v$(PKG_UPSTREAM_VERSION)
PKG_SHORT_SHA:=3f27ef3
PKG_VERSION:=2026.10.25.$(PKG_UPSTREAM_VERSION)
PKG_RELEASE:=5
PKG_SOURCE:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-stable-$(PKG_UPSTREAM_TAG)-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
PKG_SOURCE_VERSION:=3f27ef39f8c96bbfbc8b0b9bb10f5c9ef60149d8
PKG_HASH:=1ef7a4087f3805bd2624a940e2e43a0bc23719a25465c9f503aa6c11572f1321
PKG_BUILD_VERSION:=$(PKG_UPSTREAM_TAG)
PKG_LICENSE:=GPL3.0+
PKG_MAINTAINER:=kenzok8
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
GO_PKG:=github.com/metacubex/mihomo
GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
GO_PKG_TAGS:=with_gvisor
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/clashoo
SECTION:=net
CATEGORY:=Network
TITLE:=Clashoo (Mihomo)
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle +yq +firewall4 +ip-full +coreutils-nohup +coreutils-timeout +kmod-inet-diag +kmod-nft-socket +kmod-nft-tproxy +kmod-tun +kmod-dummy +unzip
PROVIDES:=mihomo clash-meta
endef
define Package/clashoo/description
Mihomo proxy core with runtime environment for Clashoo.
endef
define Package/clashoo/conffiles
/etc/config/clashoo
endef
define Package/clashoo/install
$(call GoPackage/Package/Install/Bin,$(1))
$(INSTALL_DIR) $(1)/usr/libexec/clashoo
$(INSTALL_DIR) $(1)/usr/bin
mv -f $(1)/usr/bin/mihomo $(1)/usr/libexec/clashoo/mihomo-stable
ln -sf /usr/libexec/clashoo/mihomo-stable $(1)/usr/bin/mihomo
ln -sf /usr/libexec/clashoo/mihomo-stable $(1)/usr/bin/clash-meta
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_DIR) $(1)/etc/clashoo
$(INSTALL_DIR) $(1)/etc/clashoo/provider
$(INSTALL_DIR) $(1)/etc/clashoo/proxyprovider
$(INSTALL_DIR) $(1)/etc/clashoo/ruleprovider
$(INSTALL_DIR) $(1)/usr/share/clashoo
$(INSTALL_DIR) $(1)/usr/share/clashbackup
$(INSTALL_DIR) $(1)/usr/share/clashoo/config
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/sub
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/upload
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/custom
$(INSTALL_BIN) $(CURDIR)/files/etc/init.d/clashoo $(1)/etc/init.d/clashoo
$(INSTALL_CONF) $(CURDIR)/files/etc/config/clashoo $(1)/etc/config/clashoo
$(INSTALL_DATA) $(CURDIR)/files/etc/config/clashoo $(1)/usr/share/clashoo/clashoo.default
$(CP) $(CURDIR)/files/usr/share/clashoo/* $(1)/usr/share/clashoo/
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/migrate_lan_acl.sh $(1)/usr/share/clashoo/migrate_lan_acl.sh
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/migrate_fake_ip_filter.sh $(1)/usr/share/clashoo/migrate_fake_ip_filter.sh
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/detect_core_arch.sh $(1)/usr/share/clashoo/detect_core_arch.sh
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/diagnose_bypass.sh $(1)/usr/share/clashoo/diagnose_bypass.sh
$(RM) $(1)/usr/share/clashoo/dashboard
$(RM) $(1)/usr/share/clashoo/yacd
$(RM) $(1)/usr/share/clashoo/update.log
$(RM) $(1)/usr/share/clashoo/geoip.log
$(RM) $(1)/usr/share/clashoo/core_down_complete
endef
define Package/clashoo/preinst
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
save_core() {
src="$$1"
dst="$$2"
[ -f "$$src" ] && [ ! -L "$$src" ] || return 0
if [ -e "$$dst" ] || [ -L "$$dst" ]; then return 0; fi
mkdir -p "$$(dirname "$$dst")" || return 1
tmp="$$dst.tmp.$$$$"
cp "$$src" "$$tmp" && chmod 0755 "$$tmp" && cmp -s "$$src" "$$tmp" || {
rm -f "$$tmp"
return 1
}
if command -v sha256sum >/dev/null 2>&1; then
old_hash="$$(sha256sum "$$src" | awk '{print $$1}')"
new_hash="$$(sha256sum "$$tmp" | awk '{print $$1}')"
[ "$$old_hash" = "$$new_hash" ] || {
rm -f "$$tmp"
return 1
}
fi
mv -f "$$tmp" "$$dst"
}
save_core /usr/bin/mihomo /usr/share/clashoo/bin/mihomo-alpha || exit 1
save_core /usr/bin/mihomo-stable /usr/share/clashoo/bin/mihomo-stable || exit 1
if [ -x /usr/bin/clash-meta ] && [ ! -L /usr/bin/clash-meta ] && [ ! -e /usr/share/clashoo/bin/mihomo-stable ]; then
if ! /usr/bin/clash-meta -v 2>/dev/null | grep -Eiq '(alpha|beta|rc|pre|smart)'; then
save_core /usr/bin/clash-meta /usr/share/clashoo/bin/mihomo-stable || exit 1
fi
fi
fi
exit 0
endef
define Package/clashoo/postinst
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
rm -f /usr/share/clashoo/check_luci_version.sh /usr/share/clashoo/check_clash_meta_version.sh /usr/share/clashoo/check_mihomo_core_version.sh
rm -f /usr/share/clashoo/cuslist.sh /usr/share/clashoo/groups.sh /usr/share/clashoo/load_groups.sh /usr/share/clashoo/panel_diag.sh
rm -f /usr/share/clashoo/rmlist.sh /usr/share/clashoo/uplist.sh /usr/share/clashoo/rpc_restart_async.sh /usr/share/clashoo/rpc_update_china_ip_async.sh
rm -rf /usr/share/clashoo/create
# 旧方案把 stable 内核放在 /usr/bin/mihomo-stable,现方案改用
# /usr/libexec/clashoo/mihomo-stable(随包)+ /usr/share/clashoo/bin/(自定义下载)。
# preinst 已把它抄到 /usr/share/clashoo/bin/mihomo-stable,确认内容相同后才删
# 旧位置;目标已有其他自定义内核时保留旧文件,避免误删。
if [ -f /usr/share/clashoo/bin/mihomo-stable ] && [ -f /usr/bin/mihomo-stable ] && [ ! -L /usr/bin/mihomo-stable ] && cmp -s /usr/bin/mihomo-stable /usr/share/clashoo/bin/mihomo-stable; then
rm -f /usr/bin/mihomo-stable
fi
/usr/share/clashoo/migrate_lan_acl.sh >/dev/null 2>&1 || true
/usr/share/clashoo/migrate_fake_ip_filter.sh >/dev/null 2>&1 || true
/usr/share/clashoo/detect_core_arch.sh >/dev/null 2>&1 || true
if [ "$$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then
/etc/init.d/clashoo enable 2>/dev/null || true
else
/etc/init.d/clashoo disable 2>/dev/null || true
fi
fi
exit 0
endef
define Build/Prepare
$(Build/Prepare/Default)
$(RM) -r $(PKG_BUILD_DIR)/rules/logic_test
endef
$(eval $(call GoBinPackage,clashoo))
$(eval $(call BuildPackage,clashoo))
-156
View File
@@ -1,156 +0,0 @@
config clashoo 'config'
option redir_port '7891'
option http_port '8080'
option socks_port '1080'
option mixed_port '7890'
option enable_ipv6 'true'
option enable '0'
option dash_port '9090'
option dashboard_panel 'zashboard'
option level 'info'
option core '2'
option allow_lan '1'
option dnsforwader '1'
option subcri 'meta'
option cusrule '0'
option dnscache '1'
option p_mode 'rule'
option append_rules '0'
option enhanced_mode 'fake-ip'
option fake_ip_range '198.18.0.1/16'
option fake_ip_range6 'fc00::/18'
list fake_ip_filter '+.lan'
list fake_ip_filter '+.local'
list fake_ip_filter 'localhost.ptlogin2.qq.com'
list fake_ip_filter 'rule-set:cn_domain'
option fake_ip_filter_migrated '1'
option listen_port '1053'
option interf '0'
option download_core 'amd64-compatible'
option dcore '2'
option same_tag '1'
option geoip_source '3'
option geoip_format 'mmdb'
option geodata_loader 'standard'
option tproxy_port '7982'
option access_control '0'
option acl_migrated '1'
option tcp_mode 'redirect'
option tun_mode '0'
option core_mirror_prefix 'https://gh-proxy.com/'
option core_download_advanced '0'
list default_nameserver '223.5.5.5'
list default_nameserver '119.29.29.29'
option dns_ecs ''
option dns_ecs_override '0'
option fallback_filter_geoip '0'
list fallback_filter_ipcidr '240.0.0.0/4'
option dns_respect_rules '1'
option dns_loopback_compat '0'
option dns_migrated '1'
option block_quic '0'
option singbox_independent_cache '0'
option udp_mode 'tproxy'
option ipv4_dns_hijack '1'
option ipv6_dns_hijack '1'
option ipv4_proxy '1'
option ipv6_proxy '1'
option fake_ip_ping_hijack '1'
option dns_leak_protect '0'
option geoip_mmdb_url 'https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb'
option geosite_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat'
option geoip_dat_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat'
option enable_udp '1'
option stack 'gvisor'
option bypass_china '1'
option bypass_china_ipv6 '1'
option bypass_port_mode 'common'
option sniffer_streaming '1'
option selection_cache '1'
option fake_ip_cache '1'
option enable_dns '1'
option disable_quic_gso '1'
option smart_policy_priority 'Premium:0.9;SG:1.3'
option smart_prefer_asn '1'
option smart_uselightgbm '1'
option smart_collectdata '1'
option smart_collect_size '100'
option smart_collect_rate '1'
option smart_lgbm_auto_update '1'
option smart_lgbm_update_interval '72'
option smart_lgbm_url 'https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin'
list defaul_nameserver '114.114.114.114'
list defaul_nameserver '223.5.5.5'
option auto_update '1'
option auto_update_time '72'
option auto_subscription_update '0'
option subscription_update_interval '72'
option auto_clear_log '1'
option clear_time '72'
option core_type 'mihomo'
option smart_auto_switch '0'
option core_only '0'
config hosts
option enabled '0'
option adress '*.clash.dev'
option ip '127.0.0.1'
config hosts
option enabled '0'
option adress 'alpha.clash.dev'
option ip '::1'
config dnsservers
option enabled '1'
option ser_type 'nameserver'
option ser_address 'https://dns.alidns.com/dns-query'
option protocol 'none'
config dnsservers
option enabled '1'
option ser_type 'nameserver'
option ser_address 'https://doh.pub/dns-query'
option protocol 'none'
config dnsservers
option enabled '1'
option ser_type 'proxy-server-nameserver'
option ser_address '1.1.1.1'
option protocol 'tls://'
option ser_port '853'
config dnsservers
option enabled '1'
option ser_type 'fallback'
option ser_address 'https://cloudflare-dns.com/dns-query'
option protocol 'none'
config dnsservers
option enabled '1'
option ser_type 'fallback'
option ser_address 'https://dns.google/dns-query'
option protocol 'none'
config dnsservers
option enabled '1'
option ser_type 'direct-nameserver'
option ser_address '223.5.5.5'
option protocol 'udp://'
config dns_policy
option enabled '1'
option policy_type 'nameserver-policy'
option matcher 'geosite:cn'
list nameserver 'https://dns.alidns.com/dns-query'
list nameserver 'https://doh.pub/dns-query'
config authentication
# default empty custom-rule row so the section shows the input form, not just an add
# button; injectors skip rules with an empty ipaaddr, so it generates nothing until filled
config addtype
option type 'DOMAIN-SUFFIX'
option pgroup 'DIRECT'
option ipaaddr ''
File diff suppressed because it is too large. Load diff
@@ -1,26 +0,0 @@
#!/bin/sh
[ -n "$(uci -q get clashoo.config.download_core 2>/dev/null)" ] && exit 0
raw="$(uname -m 2>/dev/null)"
[ -n "$raw" ] || raw="$(apk --print-arch 2>/dev/null)"
[ -n "$raw" ] || raw="$(opkg status libc 2>/dev/null | awk -F': ' '/^Architecture/{print $2; exit}')"
[ -n "$raw" ] || exit 0
case "$raw" in
x86_64|amd64) arch="amd64-compatible" ;;
aarch64*|arm64) arch="arm64" ;;
armv7*|arm_cortex-a[7-9]*|arm_cortex-a1[0-9]*) arch="armv7" ;;
armv6*|arm_cortex-a[56]*) arch="armv6" ;;
arm*) arch="armv5" ;;
i[3-6]86) arch="386" ;;
mips64el*) arch="mips64le" ;;
mips64*) arch="mips64" ;;
mipsel*) arch="mipsle" ;;
mips*) arch="mips" ;;
*) exit 0 ;;
esac
uci -q set clashoo.config.download_core="$arch"
uci -q commit clashoo
exit 0
@@ -1,181 +0,0 @@
#!/bin/sh
umask 077
CONFIG_YAML="${CONFIG_YAML:-/etc/clashoo/config.yaml}"
NFT_V4="${NFT_V4:-/usr/share/clashoo/nftables/geoip_cn.nft}"
NFT_V6="${NFT_V6:-/usr/share/clashoo/nftables/geoip6_cn.nft}"
MAX_CONN=200
TMPDIR_D="$(mktemp -d /tmp/.clashoo_diag.XXXXXX 2>/dev/null)" || exit 1
trap 'rm -rf "$TMPDIR_D" 2>/dev/null' EXIT INT TERM
uci_get() {
uci -q get "clashoo.config.$1" 2>/dev/null
}
section() {
printf '\n===== %s =====\n' "$1"
}
yesno() {
[ -n "$1" ] && [ "$1" != "0" ] && [ "$1" != "false" ] && echo "on" || echo "off"
}
bool_on() {
case "$1" in
1|true|TRUE|yes|on) return 0 ;;
*) return 1 ;;
esac
}
bypass4="$(uci_get bypass_china)"
bypass6="$(uci_get bypass_china_ipv6)"
[ -n "$bypass6" ] || bypass6="$bypass4"
section "配置摘要"
printf '服务开关 : %s\n' "$(yesno "$(uci_get enable)")"
printf '运行模式 : %s\n' "$(uci_get enhanced_mode)"
printf 'TCP / UDP : %s / %s\n' "$(uci_get tcp_mode)" "$(uci_get udp_mode)"
printf '大陆绕过 v4 : %s\n' "$(yesno "$bypass4")"
printf '大陆绕过 v6 : %s%s\n' "$(yesno "$bypass6")" \
"$([ -z "$(uci_get bypass_china_ipv6)" ] && echo '(未设置,跟随 v4)')"
printf '接管端口 : %s' "$(uci_get bypass_port_mode)"
[ "$(uci_get bypass_port_mode)" = "custom" ] && printf ' (%s)' "$(uci_get bypass_port_custom)"
printf '\n'
printf 'IPv6 代理 : %s\n' "$(yesno "$(uci_get ipv6_proxy)")"
printf '阻断 QUIC : %s\n' "$(yesno "$(uci_get block_quic)")"
if [ "$(uci_get bypass_port_mode)" = "common" ]; then
printf '\n注意: 接管端口为 common,只代理 22/53/80/443/8080/8443,\n'
printf ' 游戏与 P2P 常用的高位端口不会走代理。\n'
fi
section "fake-ip 过滤"
if [ -s "$CONFIG_YAML" ]; then
if grep -qE '^[[:space:]]*fake-ip-filter:' "$CONFIG_YAML" 2>/dev/null; then
filter_mode="$(uci_get fake_ip_filter_mode)"
[ -n "$filter_mode" ] || filter_mode="$(yq -M e '(.dns."fake-ip-filter-mode" // "blacklist") | tostring' "$CONFIG_YAML" 2>/dev/null)"
if [ "$filter_mode" = "whitelist" ]; then
echo "白名单模式:列表中的域名使用 fake-IP"
elif grep -qiE 'rule-set:cn_domain|RULE-SET,[[:space:]]*cn_domain,[[:space:]]*real-ip' "$CONFIG_YAML" 2>/dev/null; then
echo "含 rule-set:cn_domain (国内域名返回真实 IP)"
else
echo "缺少 rule-set:cn_domain"
echo " 国内域名会拿到 fake-ip 走代理,与硬编码国内 IP 分成两条路。"
fi
else
echo "配置中无 fake-ip-filter"
fi
else
echo "找不到 $CONFIG_YAML"
fi
section "内核分流规则"
if [ -s "$CONFIG_YAML" ]; then
echo "-- rules 末尾 5 条 --"
grep -E '^[[:space:]]*-[[:space:]]*["'"'"']?(GEOIP|RULE-SET|MATCH|IP-CIDR|DOMAIN)' "$CONFIG_YAML" 2>/dev/null | tail -5
fi
section "IP 库"
for f in "$NFT_V4" "$NFT_V6"; do
if [ -s "$f" ]; then
printf '%-46s %6s 条 %s\n' "$f" \
"$(grep -cE '^[[:space:]]*[0-9a-fA-F].*/' "$f" 2>/dev/null)" \
"$(date -r "$f" '+%Y-%m-%d %H:%M' 2>/dev/null)"
else
printf '%-46s 缺失\n' "$f"
fi
done
for f in /etc/clashoo/Country.mmdb /etc/clashoo/geoip.metadb; do
[ -f "$f" ] && printf '%-46s %s\n' "$f" "$(ls -lh "$f" 2>/dev/null | awk '{print $5}')"
done
set_count() {
nft list set inet fw4 "$1" 2>/dev/null | tr ',' '\n' | grep -cE '[0-9a-fA-F]+[.:].*/'
}
section "防火墙绕过规则"
if nft list sets inet fw4 2>/dev/null | grep -q clashoo_china; then
for ch in dstnat mangle_prerouting; do
nft -a list chain inet fw4 "$ch" 2>/dev/null |
grep -E '@clashoo_china6? ' | sed "s|^[[:space:]]*| ${ch}: |"
done
echo
echo "已加载元素数(nft auto-merge 会合并相邻网段,少于文件行数属正常):"
bool_on "$bypass4" && printf ' clashoo_china : %s\n' "$(set_count clashoo_china)"
bool_on "$bypass6" && printf ' clashoo_china6 : %s\n' "$(set_count clashoo_china6)"
else
echo "当前 nftables 中没有 clashoo_china 集合(绕过未启用或服务未运行)"
fi
section "绕过命中检查"
if ! bool_on "$bypass4" && ! bool_on "$bypass6"; then
echo "两个绕过均未启用,所有被接管的流量都交给内核判定,无需检查"
else
SECRET="$(uci_get dash_pass)"
PORT="$(uci_get dash_port)"
LAN_IP="$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F/ '{print $1}')"
[ -n "$LAN_IP" ] || LAN_IP="127.0.0.1"
CONN_JSON="$TMPDIR_D/connections.json"
if [ -z "$PORT" ]; then
echo "未配置控制端口,跳过"
elif ! curl -m 5 -s -H "Authorization: Bearer ${SECRET}" \
"http://${LAN_IP}:${PORT}/connections" -o "$CONN_JSON" 2>/dev/null; then
echo "无法访问内核 API (http://${LAN_IP}:${PORT}),跳过"
elif [ ! -s "$CONN_JSON" ]; then
echo "内核 API 返回为空,跳过"
else
jsonfilter -i "$CONN_JSON" -e '@["connections"][*]["metadata"]["destinationIP"]' \
2>/dev/null > "${CONN_JSON}.ip"
jsonfilter -i "$CONN_JSON" -e '@["connections"][*]["chains"][0]' \
2>/dev/null > "${CONN_JSON}.ch"
total="$(wc -l < "${CONN_JSON}.ip" 2>/dev/null | tr -d ' ')"
[ -n "$total" ] || total=0
if [ "$total" -eq 0 ]; then
echo "当前无活动连接"
elif [ "$total" != "$(wc -l < "${CONN_JSON}.ch" 2>/dev/null | tr -d ' ')" ]; then
echo "内核 API 字段数不一致,跳过"
else
awk 'NR==FNR { a[FNR] = $0; next } { print a[FNR] "\t" $0 }' \
"${CONN_JSON}.ip" "${CONN_JSON}.ch" 2>/dev/null \
| awk -F'\t' '$2 != "DIRECT" && $1 != "" && $1 !~ /^(198\.18\.|::|fc00:|fd)/ { print $1 "\t" $2 }' \
| sort -u | head -n "$MAX_CONN" > "${CONN_JSON}.chk"
hit=0
: > "${CONN_JSON}.bad"
while IFS="$(printf '\t')" read -r dip chain; do
[ -n "$dip" ] || continue
case "$dip" in
*:*) bool_on "$bypass6" || continue
set_name=clashoo_china6 ;;
*) bool_on "$bypass4" || continue
set_name=clashoo_china ;;
esac
if nft get element inet fw4 "$set_name" "{ $dip }" >/dev/null 2>&1; then
hit=$((hit + 1))
printf ' %-40s -> %s\n' "$dip" "$chain" >> "${CONN_JSON}.bad"
fi
done < "${CONN_JSON}.chk"
printf '活动连接 %s 条,其中走代理的去重目的地 %s 个(上限 %s)\n' \
"$total" "$(wc -l < "${CONN_JSON}.chk" 2>/dev/null | tr -d ' ')" "$MAX_CONN"
if [ "$hit" -gt 0 ]; then
printf '命中绕过白名单却仍走代理: %s 个\n\n' "$hit"
head -20 "${CONN_JSON}.bad"
echo
echo "这些目的地在防火墙白名单内,本应直连,却出现在代理链路上。"
echo "可能是连接建立于规则变更之前,或该地址族的绕过规则未生效。"
else
echo "未发现白名单地址走代理的连接"
fi
rm -f "${CONN_JSON}.chk" "${CONN_JSON}.bad" 2>/dev/null
fi
rm -f "${CONN_JSON}.ip" "${CONN_JSON}.ch" 2>/dev/null
fi
fi
printf '\n'
@@ -1,28 +0,0 @@
#!/bin/sh
# clashoo core log size guard
# 用法: core_log_guard.sh [logfile]
#
# core.log 落在 /var/log(= /tmp 下的 tmpfs),没有大小上限时一次 accept
# 失败风暴就能把它写到几百 MB,直接把 /tmp 写满(dnsmasq、配置生成、
# cron 全部失效)。这里在超过上限时就地裁剪,只保留尾部内容。
# 内核进程持有该文件的 O_APPEND fd,所以必须原地重写,不能替换 inode。
set -u
LOG="${1:-/var/log/clashoo/core.log}"
MAX_KB="${CLASHOO_CORE_LOG_MAX_KB:-16384}"
KEEP_KB="${CLASHOO_CORE_LOG_KEEP_KB:-1024}"
[ -f "$LOG" ] || exit 0
size="$(wc -c <"$LOG" 2>/dev/null)"
case "${size:-}" in ''|*[!0-9]*) exit 0 ;; esac
[ "$size" -gt $((MAX_KB * 1024)) ] || exit 0
tmp="${LOG}.trim.$$"
if tail -c $((KEEP_KB * 1024)) "$LOG" >"$tmp" 2>/dev/null && cat "$tmp" >"$LOG" 2>/dev/null; then
rm -f "$tmp"
else
rm -f "$tmp"
: >"$LOG" 2>/dev/null || true
fi
exit 0
@@ -1,385 +0,0 @@
#!/usr/bin/ucode
'use strict';
import { readfile, writefile } from 'fs';
// Standalone sing-box config version-migration, used by the init.d core-only
// path to upgrade an imported config (momo/homeproxy/etc.) to the format the
// current sing-box core accepts, WITHOUT clashoo normalize/takeover.
//
// IMPORTANT: migrate_singbox() below is kept byte-for-byte in sync with the
// copy in luci-app-clashoo/.../luci.clashoo (the UI "migrate" button). If you
// change one, change the other.
function migrate_singbox(cfg) {
// --- Fix DNS servers: address-based → type-based, remove detour ---
if (cfg.dns && cfg.dns.servers) {
cfg.dns.servers = map(cfg.dns.servers || [], function(srv) {
let addr = srv.address || '';
let new_srv = {};
let skip = { address: 1, detour: 1, address_resolver: 1, address_strategy: 1 };
for (let k in srv) if (!skip[k]) new_srv[k] = srv[k];
if (addr === 'fakeip') {
new_srv.type = 'fakeip';
} else if (addr === 'local' || addr === '') {
new_srv.type = new_srv.type || 'local';
} else if (match(addr, /^rcode:\/\//) || addr === 'rcode') {
new_srv._rcode = true; // mark for removal, handled via dns rule action
} else if (match(addr, /^h3:\/\//)) {
let host = replace(replace(addr, /^h3:\/\//, ''), /\/.*$/, '');
new_srv.type = 'h3'; new_srv.server = host;
} else if (match(addr, /^https:\/\//)) {
let host = replace(replace(addr, /^https:\/\//, ''), /\/.*$/, '');
new_srv.type = 'https'; new_srv.server = host;
} else if (match(addr, /^tls:\/\//)) {
new_srv.type = 'tls'; new_srv.server = replace(addr, /^tls:\/\//, '');
} else if (match(addr, /^tcp:\/\//)) {
new_srv.type = 'tcp'; new_srv.server = replace(addr, /^tcp:\/\//, '');
} else if (addr && !new_srv.type) {
new_srv.type = 'udp'; new_srv.server = addr; // plain IP → UDP
} else if (addr) {
new_srv.address = addr; // unknown format, keep as-is
}
return new_srv;
});
}
// --- Remove rcode servers (unsupported in 1.12+ new format), convert referencing rules to reject ---
let rcode_tags = {};
if (cfg.dns && cfg.dns.servers) {
let good = [];
for (let srv in cfg.dns.servers) {
if (srv._rcode) rcode_tags[srv.tag] = true;
else push(good, srv);
}
cfg.dns.servers = good;
}
if (cfg.dns && cfg.dns.rules && length(keys(rcode_tags)) > 0) {
cfg.dns.rules = map(cfg.dns.rules || [], function(rule) {
if (rule.server && rcode_tags[rule.server]) {
let r = {};
for (let k in rule) if (k !== 'server' && k !== 'action') r[k] = rule[k];
r.action = 'reject';
return r;
}
return rule;
});
}
// --- Remove legacy dns.fakeip.enabled (1.12+ fakeip is configured via type:"fakeip" server) ---
if (cfg.dns && cfg.dns.fakeip) {
let fp = cfg.dns.fakeip;
let new_fp = {};
for (let k in fp) if (k !== 'enabled') new_fp[k] = fp[k];
if (length(keys(new_fp)) > 0) cfg.dns.fakeip = new_fp;
else delete cfg.dns['fakeip'];
}
// --- Fix DNS rules: add action:"route" when server is set ---
if (cfg.dns && cfg.dns.rules) {
cfg.dns.rules = map(cfg.dns.rules || [], function(rule) {
if (rule.server && !rule.action) {
let r = {};
for (let k in rule) r[k] = rule[k];
r.action = 'route';
return r;
}
return rule;
});
}
// --- Fix legacy special outbounds (block / dns types) ---
let special = {};
if (cfg.outbounds) {
let kept = [];
for (let ob in cfg.outbounds) {
if (ob.type === 'block' || ob.type === 'dns') {
special[ob.tag] = ob.type;
} else {
push(kept, ob);
}
}
cfg.outbounds = kept;
}
// Patch route rules that pointed to removed special outbounds
if (cfg.route && cfg.route.rules && length(keys(special)) > 0) {
cfg.route.rules = map(cfg.route.rules || [], function(rule) {
let sp = rule.outbound && special[rule.outbound];
if (!sp) return rule;
let r = {};
for (let k in rule) if (k !== 'outbound') r[k] = rule[k];
r.action = (sp === 'dns') ? 'hijack-dns' : 'reject';
return r;
});
}
// --- Migrate geoip/geosite database refs to rule_set (removed in sing-box 1.12) ---
let needed_rule_sets = {};
let migrate_geo_rule = function(rule) {
let has_geo = rule.geoip || rule.geosite;
if (!has_geo) return rule;
let r = {};
let new_rs = [];
for (let k in rule) {
if (k === 'geoip') {
for (let n in rule[k]) {
let tag = 'geoip-' + n;
needed_rule_sets[tag] = 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-' + n + '.srs';
push(new_rs, tag);
}
} else if (k === 'geosite') {
for (let n in rule[k]) {
let tag = 'geosite-' + n;
needed_rule_sets[tag] = 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-' + n + '.srs';
push(new_rs, tag);
}
} else {
r[k] = rule[k];
}
}
// Merge new rule_set tags with any existing ones
let existing_rs = type(r.rule_set) === 'array' ? r.rule_set :
(r.rule_set ? [r.rule_set] : []);
for (let t in new_rs) push(existing_rs, t);
r.rule_set = existing_rs;
return r;
};
if (cfg.route && cfg.route.rules)
cfg.route.rules = map(cfg.route.rules, migrate_geo_rule);
if (cfg.dns && cfg.dns.rules)
cfg.dns.rules = map(cfg.dns.rules, migrate_geo_rule);
// download_detour must be DIRECT: at first start rule-sets aren't loaded, so
// routing it via a proxy deadlocks (DNS recursion -> sing-box FATAL).
let pick_dl_detour = function() {
if (cfg.outbounds) {
for (let ob in cfg.outbounds) {
if (ob && ob.type === 'direct' && ob.tag) return ob.tag;
}
}
return 'DIRECT';
};
// Add rule_set download entries for each referenced geo tag
if (length(keys(needed_rule_sets)) > 0) {
if (!cfg.route) cfg.route = {};
if (!cfg.route.rule_set) cfg.route.rule_set = [];
let existing_tags = {};
for (let rs in cfg.route.rule_set) existing_tags[rs.tag] = true;
let dl_detour = pick_dl_detour();
for (let tag in keys(needed_rule_sets)) {
if (!existing_tags[tag]) {
push(cfg.route.rule_set, {
tag: tag,
type: 'remote',
format: 'binary',
url: needed_rule_sets[tag],
download_detour: dl_detour
});
}
}
}
// --- Remove deprecated 'outbound' DNS rule items (fatal in sing-box 1.13) ---
// Replacement: set route.default_domain_resolver to the plain IP-based resolver
let dns_resolver_tag = '';
if (cfg.dns && cfg.dns.servers) {
for (let srv in cfg.dns.servers) {
if ((srv.type === 'udp' || srv.type === 'local') && srv.server &&
match(srv.server, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
dns_resolver_tag = srv.tag;
break;
}
}
}
if (cfg.dns && cfg.dns.rules) {
let filtered = [];
for (let rule in cfg.dns.rules) {
if (!rule.outbound) push(filtered, rule);
}
cfg.dns.rules = filtered;
}
// Set route.default_domain_resolver (replaces outbound:any DNS routing)
if (dns_resolver_tag) {
if (!cfg.route) cfg.route = {};
if (!cfg.route.default_domain_resolver)
cfg.route.default_domain_resolver = dns_resolver_tag;
}
// --- Fix DNS servers with domain-name server field: add domain_resolver ---
// sing-box 1.12+ requires domain_resolver when server= is a hostname (not IP)
if (cfg.dns && cfg.dns.servers && dns_resolver_tag) {
let domain_types = { https: 1, h3: 1, tls: 1, tcp: 1 };
cfg.dns.servers = map(cfg.dns.servers, function(srv) {
if (!domain_types[srv.type]) return srv;
if (!srv.server || match(srv.server, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return srv;
if (srv.domain_resolver) return srv;
let r = {};
for (let k in srv) r[k] = srv[k];
r.domain_resolver = dns_resolver_tag;
return r;
});
}
// --- Fix DNS rules referencing non-existent server tags → action: reject ---
if (cfg.dns && cfg.dns.servers && cfg.dns.rules) {
let valid_servers = {};
for (let srv in cfg.dns.servers) valid_servers[srv.tag] = true;
cfg.dns.rules = map(cfg.dns.rules, function(rule) {
if (!rule.server || valid_servers[rule.server]) return rule;
let r = {};
for (let k in rule) if (k !== 'server' && k !== 'action') r[k] = rule[k];
r.action = 'reject';
return r;
});
}
// --- Fix reject method: sing-box 1.14 rejects "dropped", only "default"/"drop" ---
if (cfg.dns && cfg.dns.rules) {
cfg.dns.rules = map(cfg.dns.rules, function(rule) {
if (rule.action !== 'reject' || !rule.method) return rule;
let m = rule.method;
if (m === 'dropped') m = 'drop';
if (m !== 'default' && m !== 'drop') {
// unknown value: drop it, sing-box defaults to NXDOMAIN
let r = {};
for (let k in rule) if (k !== 'method') r[k] = rule[k];
return r;
}
if (m === rule.method) return rule;
let r = {};
for (let k in rule) r[k] = rule[k];
r.method = m;
return r;
});
}
// --- Migrate TUN inet4_address/inet6_address -> address array (1.12+), and
// strip legacy inbound fields removed in 1.13 (sniff / sniff_override_destination /
// sniff_timeout / domain_strategy). If any inbound had sniff on, prepend a
// { "action": "sniff" } route rule as the equivalent. ---
let had_sniff = false;
if (cfg.inbounds) {
cfg.inbounds = map(cfg.inbounds, function(ib) {
let r = {};
let addrs = [];
for (let k in ib) {
if (k === 'inet4_address') {
if (type(ib[k]) === 'array') { for (let a in ib[k]) push(addrs, a); }
else push(addrs, ib[k]);
} else if (k === 'inet6_address') {
if (type(ib[k]) === 'array') { for (let a in ib[k]) push(addrs, a); }
else push(addrs, ib[k]);
} else if (k === 'sniff') {
if (ib[k]) had_sniff = true;
} else if (k === 'sniff_override_destination' || k === 'sniff_timeout' ||
k === 'domain_strategy') {
// drop — moved to route.rules actions in 1.13
} else {
r[k] = ib[k];
}
}
if (length(addrs) > 0) r.address = addrs;
return r;
});
}
if (had_sniff) {
if (!cfg.route) cfg.route = {};
if (!cfg.route.rules) cfg.route.rules = [];
let has_sniff_rule = false;
for (let rl in cfg.route.rules)
if (rl.action === 'sniff') { has_sniff_rule = true; break; }
if (!has_sniff_rule) {
let new_rules = [ { action: 'sniff' } ];
for (let rl in cfg.route.rules) push(new_rules, rl);
cfg.route.rules = new_rules;
}
}
// --- sing-box 1.14 requires route.default_domain_resolver; add one if missing,
// preferring dns.final, else the first dns.servers entry with a tag. ---
if (cfg.route && !cfg.route.default_domain_resolver) {
let resolver_tag = '';
if (cfg.dns && cfg.dns.final) resolver_tag = cfg.dns.final;
else if (cfg.dns && cfg.dns.servers) {
for (let srv in cfg.dns.servers)
if (srv.tag) { resolver_tag = srv.tag; break; }
}
if (resolver_tag)
cfg.route.default_domain_resolver = { server: resolver_tag };
}
// --- Remove dangling outbound refs from selectors/urltest, and drop airline
// pseudo-nodes. Two cases: (1) subconverter may reference a missing tag (e.g.
// REJECT); (2) "Traffic:.../Expire:.../quota" pseudo-nodes are real SS/Vmess
// outbounds (kept so the UI can read traffic/expiry) but don't forward — in a
// selector/urltest they win (0ms) and swallow all foreign traffic. ---
let is_pseudo_tag = function(t) {
if (!t) return false;
return match(t, /^Traffic[::]/) ||
match(t, /^Expire[::]/) ||
match(t, /剩余流量|剩余[::]/) ||
match(t, /距离下次重置/) ||
match(t, /到期(时间|日期)?[::]/) ||
match(t, /官网[::]|网站[::]|套餐[::]?|客服[::]/) ||
match(t, /QQ[群]?[::]/) ||
match(t, /Telegram|TG群|官方群/) ||
match(t, /续费|订阅地址|流量重置/);
};
if (cfg.outbounds) {
let defined_tags = {};
for (let ob in cfg.outbounds) defined_tags[ob.tag] = true;
cfg.outbounds = map(cfg.outbounds, function(ob) {
if ((ob.type !== 'selector' && ob.type !== 'urltest' && ob.type !== 'loadbalance') ||
!ob.outbounds) return ob;
let valid = [];
for (let t in ob.outbounds) {
if (!defined_tags[t]) continue;
if (is_pseudo_tag(t)) continue;
push(valid, t);
}
if (length(valid) === length(ob.outbounds)) return ob;
let r = {};
for (let k in ob) r[k] = ob[k];
r.outbounds = valid;
return r;
});
}
// --- Force download_detour=DIRECT on every remote rule_set ---
// subconverter often emits download_detour="auto", but the proxy isn't ready at
// first start -> urltest deadlock ("fetch rule-set: deadline exceeded") -> FATAL.
// The srs URLs already use a CN-reachable mirror, so DIRECT is always correct.
if (cfg.route && cfg.route.rule_set) {
let dl_detour = pick_dl_detour();
cfg.route.rule_set = map(cfg.route.rule_set, function(rs) {
if (!rs || rs.type !== 'remote') return rs;
let r = {};
for (let k in rs) r[k] = rs[k];
r.download_detour = dl_detour;
return r;
});
}
return cfg;
}
// ---- CLI entry: ucode migrate_singbox.uc <config.json> ----
let _path = ARGV[0] || '';
if (!_path) { print("missing path\n"); exit(1); }
let _raw = readfile(_path);
if (!_raw) { print("read failed\n"); exit(1); }
let _cfg = json(_raw);
if (!_cfg) { print("json parse failed\n"); exit(1); }
let _before = sprintf('%J', _cfg);
_cfg = migrate_singbox(_cfg);
let _after = sprintf('%J', _cfg);
if (_before === _after) { print("nochange\n"); exit(0); }
if (writefile(_path, _after) === null) { print("write failed\n"); exit(1); }
print("migrated\n");
@@ -1,939 +0,0 @@
#!/usr/bin/ucode
'use strict';
import { readfile, writefile, access, popen } from 'fs';
let path = ARGV[0] || '';
let redir_port = +(ARGV[1] || '7891');
let tproxy_port = +(ARGV[2] || '7982');
let mixed_port = +(ARGV[3] || '7890');
let has_tun_device = (ARGV[4] || '1') == '1';
if (has_tun_device && system('(ip tuntap add mode tun name cotuntest >/dev/null 2>&1 && ip link del cotuntest >/dev/null 2>&1)') != 0)
has_tun_device = false;
// default 6666 must match fw4.sh CORE_ROUTING_MARK (0x1a0a)
let routing_mark = +(ARGV[5] || '6666');
let dns_port = +(ARGV[6] || '1053');
let dash_port = +(ARGV[7] || '9090');
let dash_secret = ARGV[8] != null ? (ARGV[8] + '') : '';
if (!path) {
print("missing path\n");
exit(1);
}
let raw = readfile(path);
if (!raw) {
print("read failed\n");
exit(1);
}
let cfg = json(raw);
if (!cfg) {
print("json parse failed\n");
exit(1);
}
function s_len(s) {
return length(s || '');
}
function s_sub(s, start, count) {
if (count == null)
return substr(s || '', start);
return substr(s || '', start, count);
}
function is_space(ch) {
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
}
function trim_s(s) {
s = (s == null) ? '' : (s + '');
while (s_len(s) > 0 && is_space(s_sub(s, 0, 1)))
s = s_sub(s, 1);
while (s_len(s) > 0 && is_space(s_sub(s, s_len(s) - 1, 1)))
s = s_sub(s, 0, s_len(s) - 1);
return s;
}
let default_interface = '';
let _default_if_pipe = popen("ip -4 route show default 2>/dev/null | awk '{print $5; exit}'");
if (_default_if_pipe) {
default_interface = trim_s(_default_if_pipe.read('all'));
_default_if_pipe.close();
}
function starts_with(s, prefix) {
return s_sub(s || '', 0, s_len(prefix)) == prefix;
}
function find_char(s, ch) {
for (let i = 0; i < s_len(s); i++)
if (s_sub(s, i, 1) == ch)
return i;
return -1;
}
function find_last_char(s, ch) {
for (let i = s_len(s) - 1; i >= 0; i--)
if (s_sub(s, i, 1) == ch)
return i;
return -1;
}
function split_uci_words(line) {
let out = [], cur = '', quote = '', esc = false;
for (let i = 0; i < s_len(line); i++) {
let ch = s_sub(line, i, 1);
if (esc) {
cur += ch;
esc = false;
continue;
}
if (ch == '\\') {
esc = true;
continue;
}
if (quote) {
if (ch == quote)
quote = '';
else
cur += ch;
continue;
}
if (ch == '"' || ch == "'") {
quote = ch;
continue;
}
if (is_space(ch)) {
if (s_len(cur) > 0) {
push(out, cur);
cur = '';
}
continue;
}
cur += ch;
}
if (s_len(cur) > 0)
push(out, cur);
return out;
}
function load_clashoo_uci() {
let uci_path = ARGV[9] || '/etc/config/clashoo';
let txt = readfile(uci_path) || '';
let sections = [], cur = null;
for (let line in split(txt, '\n')) {
line = trim_s(line);
if (!s_len(line) || starts_with(line, '#'))
continue;
let words = split_uci_words(line);
if (!length(words))
continue;
if (words[0] == 'config') {
cur = { type: words[1] || '', name: words[2] || '', options: {}, lists: {} };
push(sections, cur);
continue;
}
if (!cur || length(words) < 3)
continue;
if (words[0] == 'option')
cur.options[words[1]] = words[2];
else if (words[0] == 'list') {
if (cur.lists[words[1]] == null)
cur.lists[words[1]] = [];
push(cur.lists[words[1]], words[2]);
}
}
return sections;
}
let clashoo_uci = load_clashoo_uci();
function uci_config_section() {
for (let s in clashoo_uci)
if (s.type == 'clashoo' && (s.name == 'config' || s.name == ''))
return s;
return {};
}
let uci_cfg = uci_config_section();
function uci_opt(key, def) {
if (uci_cfg.options && uci_cfg.options[key] != null)
return uci_cfg.options[key];
return def;
}
function uci_list(key) {
if (uci_cfg.lists && uci_cfg.lists[key] != null)
return uci_cfg.lists[key];
if (uci_cfg.options && uci_cfg.options[key] != null)
return [ uci_cfg.options[key] ];
return [];
}
function uci_sections(type_name) {
let out = [];
for (let s in clashoo_uci)
if (s.type == type_name)
push(out, s);
return out;
}
function opt_bool(v, def) {
if (v == null || v == '')
return def;
return v === true || v == '1' || v == 'true' || v == 'yes' || v == 'on';
}
function normalize_dns_uri(address, protocol, port) {
address = trim_s(address || '');
protocol = trim_s(protocol || '');
port = trim_s(port || '');
if (!s_len(address))
return '';
if (find_char(address, ':') >= 0 && find_char(address, '/') >= 0)
return address;
if (protocol == 'none')
protocol = '';
if (protocol == 'dot')
protocol = 'tls://';
else if (protocol == 'doh')
protocol = 'https://';
else if (protocol == 'doq')
protocol = 'quic://';
if (s_len(protocol) && !starts_with(protocol, 'udp://') && !starts_with(protocol, 'tcp://') &&
!starts_with(protocol, 'tls://') && !starts_with(protocol, 'https://') && !starts_with(protocol, 'quic://'))
protocol += '://';
return protocol + address + (s_len(port) ? ':' + port : '');
}
function direct_outbound_tag() {
for (let ob in (cfg.outbounds || []))
if (ob && ob.type == 'direct' && s_len(ob.tag || ''))
return ob.tag;
return 'DIRECT';
}
function dns_server_obj(uri, tag, fallback_type) {
uri = normalize_dns_uri(uri || '', '', '');
if (!s_len(uri))
return null;
let scheme = fallback_type || 'udp';
let rest = uri;
let p = -1;
for (let i = 0; i < s_len(uri) - 2; i++) {
if (s_sub(uri, i, 3) == '://') {
p = i;
break;
}
}
if (p >= 0) {
scheme = s_sub(uri, 0, p);
rest = s_sub(uri, p + 3);
}
if (scheme == 'dot')
scheme = 'tls';
else if (scheme == 'doh')
scheme = 'https';
else if (scheme == 'doq')
scheme = 'quic';
let path = '';
let slash = find_char(rest, '/');
if (slash >= 0) {
path = s_sub(rest, slash);
rest = s_sub(rest, 0, slash);
}
let server = rest, server_port = null;
let colon = find_last_char(rest, ':');
if (colon > 0 && find_char(rest, ']') < 0) {
server = s_sub(rest, 0, colon);
let n = +(s_sub(rest, colon + 1));
if (n === n)
server_port = n;
}
let obj = { type: scheme, tag: tag, server: server };
if (server_port != null)
obj.server_port = server_port;
if (path && (scheme == 'https' || scheme == 'h3'))
obj.path = path;
if ((scheme == 'https' || scheme == 'tls' || scheme == 'quic') && tag != 'dns_resolver')
obj.domain_resolver = 'dns_resolver';
/* sing-box 1.12+ DNS servers detour to the proxy by default, so a server
* resolving its own domain via dns_resolver loops -> "deadline exceeded".
* Force direct/resolver servers to DIRECT; only dns_proxy may use the proxy. */
if (tag == 'dns_resolver' || tag == 'dns_direct' || tag == 'dns_foreign')
obj.detour = direct_outbound_tag();
return obj;
}
function dns_servers_by_role(role) {
let out = [];
for (let s in uci_sections('dnsservers')) {
if (!opt_bool(s.options.enabled, true))
continue;
if ((s.options.ser_type || 'nameserver') != role)
continue;
let uri = normalize_dns_uri(s.options.ser_address || '', s.options.protocol || '', s.options.ser_port || '');
if (s_len(uri))
push(out, uri);
}
return out;
}
function first_or(arr, fallback) {
return length(arr) ? arr[0] : fallback;
}
function local_rule_set_path(tag) {
if (!s_len(tag))
return '';
let path = '/usr/share/clashoo/ruleset/' + tag + '.srs';
if (access(path, 'r'))
return path;
if (tag == 'geolocation-cn' || tag == 'cn') {
path = '/usr/share/clashoo/ruleset/geosite-cn.srs';
if (access(path, 'r'))
return path;
}
return '';
}
function keep_remote_rule_set(rs) {
return false;
}
function normalize_rule_set_url(url) {
url = url || '';
url = replace(url, /^https:\/\/gh-proxy\.com\//, '');
let m = match(url, /^https:\/\/raw\.githubusercontent\.com\/([^\/]+)\/([^\/]+)\/([^\/]+)\/(.+)$/);
if (m)
return 'https://cdn.jsdelivr.net/gh/' + m[1] + '/' + m[2] + '@' + m[3] + '/' + m[4];
m = match(url, /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)$/);
if (m)
return 'https://cdn.jsdelivr.net/gh/' + m[1] + '/' + m[2] + '@' + m[3] + '/' + m[4];
return url;
}
function has_route_rule_set(tag) {
for (let rs in (cfg.route || {}).rule_set || [])
if (rs && rs.tag == tag)
return true;
return false;
}
function add_remote_rule_set(tag, url) {
cfg.route = cfg.route || {};
cfg.route.rule_set = cfg.route.rule_set || [];
if (has_route_rule_set(tag))
return;
push(cfg.route.rule_set, {
tag: tag,
type: 'remote',
format: 'binary',
url: url
});
}
function array_has_value(arr, val) {
if (type(arr) != 'array')
return false;
for (let item in arr)
if (item == val)
return true;
return false;
}
let dnsmasq_uid = null;
let _dnsmasq_uid_pipe = popen("id -u dnsmasq 2>/dev/null");
if (_dnsmasq_uid_pipe) {
let value = trim_s(_dnsmasq_uid_pipe.read('all'));
_dnsmasq_uid_pipe.close();
if (match(value, /^[0-9]+$/))
dnsmasq_uid = +value;
}
function ensure_tun_dnsmasq_exclude(ib) {
if (dnsmasq_uid == null)
return;
let excludes = ib.exclude_uid;
if (type(excludes) != 'array')
excludes = excludes != null ? [ excludes ] : [];
if (!array_has_value(excludes, dnsmasq_uid))
push(excludes, dnsmasq_uid);
ib.exclude_uid = excludes;
}
function ensure_tun_cn_exclude(ib) {
let excludes = ib.route_exclude_address_set;
if (type(excludes) != 'array')
excludes = excludes ? [ excludes ] : [];
if (!array_has_value(excludes, 'cn-ip'))
push(excludes, 'cn-ip');
ib.route_exclude_address_set = excludes;
}
function prune_tun_cn_exclude_if_missing() {
if (has_route_rule_set('cn-ip'))
return;
for (let ib in cfg.inbounds || []) {
if (!ib || ib.type != 'tun')
continue;
let excludes = [];
for (let item in ib.route_exclude_address_set || [])
if (item != 'cn-ip')
push(excludes, item);
if (length(excludes))
ib.route_exclude_address_set = excludes;
else
delete ib.route_exclude_address_set;
}
}
function matcher_rule(matcher, server_tag) {
let r = { server: server_tag };
if (starts_with(matcher, 'geosite:'))
r.rule_set = s_sub(matcher, 8);
else if (starts_with(matcher, 'rule_set:'))
r.rule_set = s_sub(matcher, 9);
else if (starts_with(matcher, 'domain:'))
r.domain = s_sub(matcher, 7);
else if (starts_with(matcher, 'domain-suffix:'))
r.domain_suffix = s_sub(matcher, 14);
else
r.domain_suffix = matcher;
return r;
}
function apply_dns_from_uci() {
cfg.dns = cfg.dns || {};
/* strip mihomo-style DNS fields (enable/ipv6/listen/fake-ip-filter/nameserver...)
* that leak in from YAML conversion; sing-box rejects them (Fatal). */
let _mihomo_dns_fields = ['enable', 'ipv6', 'listen', 'fake-ip-filter', 'fake-ip-range',
'enhanced-mode', 'nameserver', 'fallback', 'fallback-filter',
'use-hosts', 'default-nameserver', 'proxy-server-nameserver',
'direct-nameserver', 'nameserver-policy'];
for (let _i = 0; _i < length(_mihomo_dns_fields); _i++)
delete cfg.dns[_mihomo_dns_fields[_i]];
/* strip mihomo-style experimental fields */
let _mihomo_exp = ['sniff-tls-sni', 'sniff', 'sniffer'];
for (let _me = 0; _me < length(_mihomo_exp); _me++)
if (cfg.experimental && cfg.experimental[_mihomo_exp[_me]] != null)
delete cfg.experimental[_mihomo_exp[_me]];
/* strip non-sing-box root-level fields */
let _mihomo_root = ['clash-for-android', 'cfw-bypass', 'sniffer', 'profile',
'geodata-mode', 'geodata-loader', 'geox-url', 'geo-auto-update',
'geo-update-interval', 'tun', 'ipv6', 'interface-name',
'port', 'socks-port', 'mixed-port', 'redir-port', 'tproxy-port', 'mode', 'allow-lan', 'log-level', 'external-controller', 'secret', 'bind-address', 'routing-mark', 'find-process-mode', 'tcp-concurrent', 'unified-delay',
'keep-alive-interval', 'keep-alive-idle', 'disable-keep-alive'];
/* strip subconverter note fields and non-standard keys */
let _subconv_fields = ['_note', '_sub_url', 'hosts', 'script', 'enable', 'fake-ip-filter', 'fake-ip-range'];
for (let _sf = 0; _sf < length(_subconv_fields); _sf++)
if (cfg[_subconv_fields[_sf]] != null)
delete cfg[_subconv_fields[_sf]];
for (let _mr = 0; _mr < length(_mihomo_root); _mr++)
if (cfg[_mihomo_root[_mr]] != null)
delete cfg[_mihomo_root[_mr]];
let bootstrap = uci_list('default_nameserver');
if (!length(bootstrap))
bootstrap = uci_list('defaul_nameserver');
let resolver_uri = first_or(bootstrap, '223.5.5.5');
let direct_uri = first_or(dns_servers_by_role('direct-nameserver'), first_or(dns_servers_by_role('proxy-server-nameserver'), 'https://doh.pub/dns-query'));
let proxy_uri = first_or(dns_servers_by_role('nameserver'), first_or(dns_servers_by_role('fallback'), 'https://1.1.1.1/dns-query'));
let servers = [];
push(servers, dns_server_obj(resolver_uri, 'dns_resolver', 'udp'));
push(servers, dns_server_obj(direct_uri, 'dns_direct', 'udp'));
push(servers, dns_server_obj(proxy_uri, 'dns_proxy', 'tls'));
let enhanced = uci_opt('enhanced_mode', 'fake-ip');
if (enhanced == 'fake-ip') {
let fake_range = uci_opt('fake_ip_range', '198.18.0.1/16');
push(servers, {
type: 'fakeip',
tag: 'dns_fakeip',
inet4_range: fake_range || '198.18.0.1/16',
inet6_range: 'fc00::/18'
});
}
let rules = [];
push(servers, { type: 'udp', tag: 'dns_foreign', server: '1.1.1.1' });
if (enhanced == 'fake-ip') {
push(rules, {
rule_set: 'geolocation-!cn',
server: 'dns_fakeip'
});
}
let clean_servers = [];
for (let s in servers)
if (s)
push(clean_servers, s);
cfg.dns.servers = clean_servers;
cfg.dns.rules = rules;
/* Keep bootstrap direct; proxy unmatched queries in strict mode. */
if (opt_bool(uci_opt('dns_leak_protect', '0'), false)) {
cfg.dns.final = 'dns_proxy';
if (!opt_bool(uci_opt('ipv6_proxy', '0'), false))
unshift(cfg.dns.rules, { query_type: ['AAAA'], action: 'reject', method: 'drop' });
} else
cfg.dns.final = 'dns_direct';
let ecs = trim_s(uci_opt('dns_ecs', ''));
if (s_len(ecs))
cfg.dns.client_subnet = ecs;
else
delete cfg.dns.client_subnet;
if (opt_bool(uci_opt('singbox_independent_cache', '0'), false))
cfg.dns.independent_cache = true;
else
delete cfg.dns.independent_cache;
/* sing-box 1.14 requires this (else Fatal); dns_resolver is DIRECT so first-start node lookups use direct DNS */
cfg.route.default_domain_resolver = 'dns_resolver';
}
let inbounds = cfg.inbounds || [];
let normalized = [];
let has_redirect = false;
let has_tproxy = false;
let has_mixed = false;
let has_tun = false;
let has_dns_in = false;
let wants_tun = has_tun_device && (uci_opt('tcp_mode', '') == 'tun' || uci_opt('udp_mode', '') == 'tun');
let tun_stack = uci_opt('stack', 'mixed') || 'mixed';
if (wants_tun) {
add_remote_rule_set('cn-ip',
'https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/cn.srs');
}
for (let ib in inbounds) {
if (!ib)
continue;
if (ib.type == 'tun' || ib.tag == 'tun-in') {
/* Keep tun inbound only when the selected proxy mode actually uses TUN. */
if (wants_tun && !has_tun) {
ib.type = 'tun';
ib.tag = ib.tag || 'tun-in';
if (!ib.address)
ib.address = [ '172.19.0.1/30', 'fdfe:dcba:9876::1/126' ];
ib.auto_route = true;
ib.auto_redirect = true;
ib.auto_redirect_input_mark = '0x2023';
ib.auto_redirect_output_mark = '0x2024';
ib.strict_route = true;
ib.stack = tun_stack;
ensure_tun_dnsmasq_exclude(ib);
ensure_tun_cn_exclude(ib);
push(normalized, ib);
has_tun = true;
}
continue;
}
if (ib.tag == 'redirect-in' || ib.type == 'redirect') {
if (has_redirect)
continue;
ib.type = 'redirect';
ib.tag = 'redirect-in';
ib.listen = '0.0.0.0';
ib.listen_port = redir_port;
has_redirect = true;
push(normalized, ib);
continue;
}
if (ib.tag == 'tproxy-in' || ib.type == 'tproxy') {
if (has_tproxy)
continue;
ib.type = 'tproxy';
ib.tag = 'tproxy-in';
ib.listen = '0.0.0.0';
ib.listen_port = tproxy_port;
ib.network = 'udp';
has_tproxy = true;
push(normalized, ib);
continue;
}
if (ib.tag == 'mixed-in' || ib.type == 'mixed') {
if (has_mixed)
continue;
ib.type = 'mixed';
ib.tag = 'mixed-in';
ib.listen = '0.0.0.0';
ib.listen_port = mixed_port;
has_mixed = true;
push(normalized, ib);
continue;
}
if (ib.tag == 'dns-in') {
if (has_dns_in)
continue;
ib.type = 'direct';
ib.tag = 'dns-in';
ib.listen = '0.0.0.0';
ib.listen_port = dns_port;
has_dns_in = true;
push(normalized, ib);
continue;
}
push(normalized, ib);
}
if (!has_redirect) {
push(normalized, {
type: 'redirect',
tag: 'redirect-in',
listen: '0.0.0.0',
listen_port: redir_port
});
}
if (!has_mixed) {
push(normalized, {
type: 'mixed',
tag: 'mixed-in',
listen: '0.0.0.0',
listen_port: mixed_port
});
}
if (wants_tun && !has_tun) {
let tun_ib = {
type: 'tun',
tag: 'tun-in',
address: [ '172.19.0.1/30', 'fdfe:dcba:9876::1/126' ],
auto_route: true,
auto_redirect: true,
auto_redirect_input_mark: '0x2023',
auto_redirect_output_mark: '0x2024',
strict_route: true,
stack: tun_stack
};
ensure_tun_dnsmasq_exclude(tun_ib);
ensure_tun_cn_exclude(tun_ib);
push(normalized, tun_ib);
}
if (!has_tproxy) {
push(normalized, {
type: 'tproxy',
tag: 'tproxy-in',
listen: '0.0.0.0',
listen_port: tproxy_port,
network: 'udp'
});
}
if (!has_dns_in) {
push(normalized, {
type: 'direct',
tag: 'dns-in',
listen: '0.0.0.0',
listen_port: dns_port
});
}
cfg.inbounds = normalized;
for (let ob in (cfg.outbounds || [])) {
if (!ob || type(ob) != 'object')
continue;
let t = ob.type || '';
if (t == 'selector' || t == 'urltest' || t == 'fallback' || t == 'load_balance' || t == 'dns' || t == 'block')
continue;
/* Clash SS obfs is plugin=obfs + plugin-opts object; sing-box wants
* plugin=obfs-local + "obfs=http;obfs-host=..." string. */
if (ob.plugin == 'obfs')
ob.plugin = 'obfs-local';
if (ob.plugin_opts != null && type(ob.plugin_opts) == 'object') {
let _parts = [];
let _map = { mode: 'obfs', host: 'obfs-host', uri: 'obfs-uri' };
for (let _k in ob.plugin_opts) {
let _v = ob.plugin_opts[_k];
if (type(_v) == 'object' || type(_v) == 'array') continue;
push(_parts, (_map[_k] || _k) + '=' + _v);
}
ob.plugin_opts = join(';', _parts);
}
if (ob.plugin != null && ob.plugin != 'obfs-local' && ob.plugin != 'v2ray-plugin' && ob.plugin != 'shadow-tls') {
delete ob.plugin;
delete ob.plugin_opts;
}
/* Force ipv4_only on DIRECT outbound: most devices are IPv4-only egress,
* a CN domain resolving AAAA stalls on "network is unreachable". */
if (ob.type == 'direct' && ob.tag == 'DIRECT')
ob.domain_resolver = { server: 'dns_resolver', strategy: 'ipv4_only' };
if (wants_tun) {
delete ob.routing_mark;
if (ob.type == 'direct' && ob.tag == 'DIRECT' && default_interface)
ob.bind_interface = ob.bind_interface || default_interface;
} else if (ob.routing_mark == null)
ob.routing_mark = routing_mark;
}
/* Drop airline pseudo-nodes (Traffic:/Expire:/quota/官网/QQ/etc.) from selector/urltest lists.
* They're real SS/Vmess (kept so the UI reads traffic/expiry) but don't forward; in a
* selector/urltest they win and swallow foreign traffic ("CN ok, foreign out"). */
let _is_pseudo_tag = function(t) {
if (!t) return false;
return match(t, /^Traffic[::]/) || match(t, /^Expire[::]/) ||
match(t, /剩余流量|剩余[::]/) || match(t, /距离下次重置/) ||
match(t, /到期(时间|日期)?[::]/) ||
match(t, /官网[::]|网站[::]|套餐[::]?|客服[::]/) ||
match(t, /QQ[群]?[::]/) || match(t, /Telegram|TG群|官方群/) ||
match(t, /续费|订阅地址|流量重置/);
};
for (let ob in (cfg.outbounds || [])) {
if (!ob || type(ob) != 'object') continue;
let t = ob.type || '';
if (t != 'selector' && t != 'urltest' && t != 'fallback' && t != 'load_balance') continue;
if (type(ob.outbounds) != 'array') continue;
let cleaned = [];
for (let tag in ob.outbounds) if (!_is_pseudo_tag(tag)) push(cleaned, tag);
ob.outbounds = cleaned;
}
cfg.route = cfg.route || {};
cfg.route.rules = cfg.route.rules || [];
let has_dns_hijack = false;
for (let rule in cfg.route.rules) {
if (!rule || type(rule) != 'object')
continue;
if (rule.inbound == 'dns-in' && rule.action == 'hijack-dns') {
has_dns_hijack = true;
break;
}
}
if (!has_dns_hijack) {
unshift(cfg.route.rules, {
inbound: 'dns-in',
action: 'hijack-dns'
});
}
cfg.route.auto_detect_interface = true;
apply_dns_from_uci();
/* DNS leak protection: reject DoT/DoQ (853) to force DNS through the core; after hijack-dns so the dns inbound survives */
if (opt_bool(uci_opt('dns_leak_protect', '0'), false)) {
let _has_853 = false;
for (let _r in cfg.route.rules) {
if (_r && type(_r) == 'object' && _r.action == 'reject' && _r.port) {
if (type(_r.port) == 'array') {
for (let _p in _r.port) if (_p == 853) { _has_853 = true; break; }
} else if (_r.port == 853) {
_has_853 = true;
}
}
if (_has_853) break;
}
if (!_has_853) {
let _new_rules = [];
let _inserted = false;
for (let _ri = 0; _ri < length(cfg.route.rules); _ri++) {
push(_new_rules, cfg.route.rules[_ri]);
if (!_inserted && cfg.route.rules[_ri] && cfg.route.rules[_ri].action == 'hijack-dns') {
push(_new_rules, { port: [853], action: 'reject' });
_inserted = true;
}
}
if (!_inserted) unshift(_new_rules, { port: [853], action: 'reject' });
cfg.route.rules = _new_rules;
}
}
/* ===== Custom routing rules (UCI: config addtype) -> route.rules, mirrors mihomo iprules.sh =====
* User "domain/IP -> direct/proxy" rules from LuCI, inserted after the DNS funnel
* (hijack-dns / 853-reject) and before other rules, so overrides win without bypassing it. */
let _proxy_outbound_tag = function() {
for (let ob in (cfg.outbounds || []))
if (ob && ob.tag == '🚀 节点选择' && (ob.type == 'selector' || ob.type == 'urltest'))
return ob.tag;
for (let ob in (cfg.outbounds || []))
if (ob && ob.type == 'selector' && s_len(ob.tag || '') && !_is_pseudo_tag(ob.tag))
return ob.tag;
return direct_outbound_tag();
};
let _load_addtype_rules = function() {
let out = [];
let dtag = direct_outbound_tag();
let ptag = null;
for (let s in uci_sections('addtype')) {
let o = s.options || {};
let addr = trim_s(o.ipaaddr || '');
let rtype = trim_s(o.type || '');
let pg = trim_s(o.pgroup || '');
if (!s_len(addr) || !s_len(rtype) || !s_len(pg))
continue;
let r = {};
if (rtype == 'DOMAIN')
r.domain = addr;
else if (rtype == 'DOMAIN-SUFFIX')
r.domain_suffix = addr;
else if (rtype == 'DOMAIN-KEYWORD')
r.domain_keyword = addr;
else if (rtype == 'IP-CIDR' || rtype == 'IP-CIDR6')
r.ip_cidr = addr;
else
continue;
if (pg == 'DIRECT')
r.outbound = dtag;
else {
if (ptag == null)
ptag = _proxy_outbound_tag();
r.outbound = ptag;
}
push(out, r);
}
return out;
};
let _addtype_rules = _load_addtype_rules();
if (length(_addtype_rules)) {
let _nr = [];
let _ins = false;
for (let _i = 0; _i < length(cfg.route.rules); _i++) {
let _ru = cfg.route.rules[_i];
let _dns_front = _ru && type(_ru) == 'object' &&
(_ru.action == 'hijack-dns' || (_ru.action == 'reject' && _ru.port));
if (!_ins && !_dns_front) {
for (let _cr in _addtype_rules)
push(_nr, _cr);
_ins = true;
}
push(_nr, _ru);
}
if (!_ins)
for (let _cr in _addtype_rules)
push(_nr, _cr);
cfg.route.rules = _nr;
}
if (uci_opt('enhanced_mode', 'fake-ip') == 'fake-ip') {
add_remote_rule_set('geolocation-!cn',
'https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-!cn.srs');
}
/* Local .srs -> local; remaining remote rule_sets must keep/get a download_detour.
* CN first-start deadlock: no srs -> fetch rules -> via auto-select -> urltest dials
* the proxy -> resolves its domain -> pulled into an unloaded rule_set by DNS rules -> deadline.
* The srs URLs use a CN-reachable mirror, so download_detour
* must be DIRECT (real direct-outbound tag if any, else 'DIRECT'). */
let _pick_dl_detour = function() {
if (cfg.outbounds) {
for (let ob in cfg.outbounds) {
if (ob && ob.type == 'direct' && ob.tag) return ob.tag;
}
}
return 'DIRECT';
};
let _dl_detour = _pick_dl_detour();
for (let rs in (cfg.route || {}).rule_set || []) {
if (!rs) continue;
if (rs.type != 'remote') { delete rs.download_detour; continue; }
let path = local_rule_set_path(rs.tag || '');
if (rs.tag && access(path, 'r')) {
delete rs.url; delete rs.download_detour; rs.type = 'local'; rs.path = path;
continue;
}
if (keep_remote_rule_set(rs) && rs.url) {
rs.url = normalize_rule_set_url(rs.url);
rs.download_detour = _dl_detour;
continue;
}
/* No local cache -> skip this remote rule_set so a failed download doesn't block start.
* Rule-sets are fetched to /usr/share/clashoo/ruleset/ once proxied. */
continue;
}
/* filter out remote rule_sets with no local cache */
let _clean_rs = [];
for (let _rsi = 0; _rsi < length(cfg.route.rule_set); _rsi++) {
let _rs = cfg.route.rule_set[_rsi];
if (!_rs) continue;
if (_rs.type == 'remote' && _rs.url != null) {
let _local_path = local_rule_set_path(_rs.tag || '');
if (!_rs.tag || !access(_local_path, 'r'))
if (keep_remote_rule_set(_rs) && _rs.url) {
_rs.url = normalize_rule_set_url(_rs.url);
_rs.download_detour = _dl_detour;
push(_clean_rs, _rs);
continue;
} else {
continue; /* no local cache -> drop from final list */
}
/* has local cache -> convert to local */
delete _rs.url; delete _rs.download_detour; _rs.type = 'local'; _rs.path = _local_path;
}
push(_clean_rs, _rs);
}
cfg.route.rule_set = _clean_rs;
prune_tun_cn_exclude_if_missing();
/* drop DNS/route rules referencing a removed rule_set, else sing-box FATAL: rule-set not found */
let _existing_tags = {};
for (let _rs in cfg.route.rule_set || []) if (_rs && _rs.tag) _existing_tags[_rs.tag] = true;
let _rule_has_ref = function(_r) {
if (!_r || type(_r) != 'object') return false;
if (_r.rule_set) {
if (type(_r.rule_set) == 'array') { for (let _t in _r.rule_set) if (!_existing_tags[_t]) return false; }
else if (!_existing_tags[_r.rule_set]) return false;
}
return true;
};
/* clean dns.rules */
if (cfg.dns && type(cfg.dns.rules) == 'array') {
let _clean_dns_rules = [];
for (let _dr in cfg.dns.rules) if (_rule_has_ref(_dr)) push(_clean_dns_rules, _dr);
cfg.dns.rules = _clean_dns_rules;
}
/* clean route.rules */
if (cfg.route && type(cfg.route.rules) == 'array') {
let _clean_rt_rules = [];
for (let _rr in cfg.route.rules) if (_rule_has_ref(_rr)) push(_clean_rt_rules, _rr);
cfg.route.rules = _clean_rt_rules;
}
cfg.experimental = cfg.experimental || {};
cfg.experimental.clash_api = cfg.experimental.clash_api || {};
cfg.experimental.clash_api.external_controller = '0.0.0.0:' + dash_port;
cfg.experimental.clash_api.external_ui = '/etc/clashoo/dashboard';
cfg.experimental.clash_api.secret = dash_secret;
cfg.experimental.cache_file = {
enabled: true,
store_fakeip: true
};
/* OpenWrt already owns system time sync. Keep sing-box NTP disabled by
* default to avoid noisy IPv6 UDP/123 failures on IPv4-only routers. */
cfg.ntp = cfg.ntp || {};
cfg.ntp.enabled = false;
cfg.log = cfg.log || {};
cfg.log.output = '/var/log/clashoo/core.log';
if (!cfg.log.level)
cfg.log.level = 'info';
if (writefile(path, sprintf('%J', cfg)) === null) {
print("write failed\n");
exit(1);
}
print("normalized\n");
@@ -1,138 +0,0 @@
{
"log": {
"level": "info",
"timestamp": true
},
"dns": {
"servers": [
{ "type": "tcp", "tag": "dns_proxy", "server": "1.1.1.1", "domain_resolver": "dns_resolver" },
{ "type": "https", "tag": "dns_direct", "server": "dns.alidns.com", "domain_resolver": "dns_resolver" },
{ "type": "udp", "tag": "dns_resolver", "server": "223.5.5.5" },
{ "type": "fakeip", "tag": "dns_fakeip", "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" }
],
"rules": [
{ "rule_set": "geolocation-!cn", "query_type": ["A", "AAAA", "CNAME"], "server": "dns_fakeip" },
{ "query_type": ["A", "AAAA", "CNAME"], "invert": true, "action": "reject" }
],
"final": "dns_direct"
},
"ntp": {
"enabled": false,
"server": "time.apple.com",
"server_port": 123,
"interval": "30m"
},
"inbounds": [
{ "type": "mixed", "tag": "mixed-in", "listen": "0.0.0.0", "listen_port": 2080 },
{ "type": "tun", "tag": "tun-in", "address": ["172.19.0.1/30"], "auto_route": true, "auto_redirect": true, "strict_route": true, "stack": "mixed", "route_exclude_address_set": ["cn-ip"] }
],
"outbounds": [
{
"type": "selector", "tag": "🚀 节点选择",
"outbounds": ["🌍 全部节点","♻️ 自动选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","DIRECT"],
"default": "🌍 全部节点"
},
{ "type": "selector", "tag": "▶️ YouTube", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "🔍 Google", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "🔮 OpenAI", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "🧠 Claude", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "🤖 Gemini", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "🐦 Twitter", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "📦 GitHub", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "Ⓜ️ Microsoft", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "🎵 TikTok", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "📺 Netflix", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "🎬 流媒体", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "💳 PayPal", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "📲 电报消息", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "🍏 苹果服务", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] },
{ "type": "selector", "tag": "🎯 全球直连", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] },
{ "type": "selector", "tag": "🏠 私有网络", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] },
{ "type": "selector", "tag": "🌐 非中国", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "🐟 漏网之鱼", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
{ "type": "selector", "tag": "🇭🇰 香港节点", "outbounds": ["__NODES_HK__"] },
{ "type": "selector", "tag": "🇯🇵 日本节点", "outbounds": ["__NODES_JP__"] },
{ "type": "selector", "tag": "🇺🇲 美国节点", "outbounds": ["__NODES_US__"] },
{ "type": "selector", "tag": "🇸🇬 新加坡节点","outbounds": ["__NODES_SG__"] },
{ "type": "selector", "tag": "🇺🇳 集合节点", "outbounds": ["__NODES_OTHER__"] },
{ "type": "selector", "tag": "🌍 全部节点", "outbounds": ["__NODES__"] },
{ "type": "urltest", "tag": "♻️ 香港自动", "outbounds": ["__NODES_HK__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
{ "type": "urltest", "tag": "♻️ 日本自动", "outbounds": ["__NODES_JP__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
{ "type": "urltest", "tag": "♻️ 美国自动", "outbounds": ["__NODES_US__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
{ "type": "urltest", "tag": "♻️ 新加坡自动", "outbounds": ["__NODES_SG__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
{ "type": "urltest", "tag": "♻️ 集合自动", "outbounds": ["__NODES_OTHER__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
{ "type": "urltest", "tag": "♻️ 自动选择", "outbounds": ["__NODES__"], "url": "https://www.gstatic.com/generate_204", "interval": "10m", "tolerance": 50, "interrupt_exist_connections": true },
{ "type": "direct", "tag": "DIRECT" }
],
"route": {
"default_domain_resolver": "dns_resolver",
"rule_set": [
{ "tag": "openai", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/openai.srs", "download_detour": "DIRECT" },
{ "tag": "anthropic", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/anthropic.srs", "download_detour": "DIRECT" },
{ "tag": "google-gemini", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/google-gemini.srs", "download_detour": "DIRECT" },
{ "tag": "youtube", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/youtube.srs", "download_detour": "DIRECT" },
{ "tag": "google", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/google.srs", "download_detour": "DIRECT" },
{ "tag": "twitter", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/twitter.srs", "download_detour": "DIRECT" },
{ "tag": "github", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/github.srs", "download_detour": "DIRECT" },
{ "tag": "gitlab", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/gitlab.srs", "download_detour": "DIRECT" },
{ "tag": "microsoft", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/microsoft.srs", "download_detour": "DIRECT" },
{ "tag": "tiktok", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/tiktok.srs", "download_detour": "DIRECT" },
{ "tag": "netflix", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/netflix.srs", "download_detour": "DIRECT" },
{ "tag": "hulu", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/hulu.srs", "download_detour": "DIRECT" },
{ "tag": "disney", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/disney.srs", "download_detour": "DIRECT" },
{ "tag": "hbo", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/hbo.srs", "download_detour": "DIRECT" },
{ "tag": "amazon", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/amazon.srs", "download_detour": "DIRECT" },
{ "tag": "bahamut", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/bahamut.srs", "download_detour": "DIRECT" },
{ "tag": "paypal", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/paypal.srs", "download_detour": "DIRECT" },
{ "tag": "apple", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/apple.srs", "download_detour": "DIRECT" },
{ "tag": "geolocation-cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-cn.srs", "download_detour": "DIRECT" },
{ "tag": "cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/cn.srs", "download_detour": "DIRECT" },
{ "tag": "geolocation-!cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-!cn.srs", "download_detour": "DIRECT" },
{ "tag": "google-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/google.srs", "download_detour": "DIRECT" },
{ "tag": "private-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/private.srs", "download_detour": "DIRECT" },
{ "tag": "cn-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/cn.srs", "download_detour": "DIRECT" },
{ "tag": "telegram-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/telegram.srs", "download_detour": "DIRECT" }
],
"rules": [
{ "action": "sniff" },
{ "clash_mode": "direct", "outbound": "DIRECT" },
{ "clash_mode": "global", "outbound": "♻️ 自动选择" },
{ "protocol": "dns", "action": "hijack-dns" },
{ "rule_set": "openai", "outbound": "🔮 OpenAI" },
{ "rule_set": "anthropic", "outbound": "🧠 Claude" },
{ "rule_set": "google-gemini", "outbound": "🤖 Gemini" },
{ "rule_set": "youtube", "outbound": "▶️ YouTube" },
{ "rule_set": "google", "outbound": "🔍 Google" },
{ "rule_set": "twitter", "outbound": "🐦 Twitter" },
{ "rule_set": "tiktok", "outbound": "🎵 TikTok" },
{ "rule_set": "netflix", "outbound": "📺 Netflix" },
{ "rule_set": ["hulu", "disney", "hbo", "amazon", "bahamut"], "outbound": "🎬 流媒体" },
{ "rule_set": "paypal", "outbound": "💳 PayPal" },
{ "rule_set": "microsoft", "outbound": "Ⓜ️ Microsoft" },
{ "rule_set": "apple", "outbound": "🍏 苹果服务" },
{ "rule_set": ["github", "gitlab"], "outbound": "📦 GitHub" },
{ "rule_set": "telegram-ip", "outbound": "📲 电报消息" },
{ "rule_set": "private-ip", "outbound": "🏠 私有网络" },
{ "rule_set": ["geolocation-cn", "cn"], "outbound": "🎯 全球直连" },
{ "rule_set": "cn-ip", "outbound": "🎯 全球直连" },
{ "domain_suffix": ["cn"], "outbound": "🎯 全球直连" },
{ "rule_set": "geolocation-!cn", "outbound": "🌐 非中国" },
{ "rule_set": "google-ip", "outbound": "🔍 Google" }
],
"auto_detect_interface": true,
"final": "🐟 漏网之鱼"
},
"experimental": {
"cache_file": { "enabled": true, "store_fakeip": true },
"clash_api": {
"external_controller": "0.0.0.0:9090",
"external_ui": "dashboard",
"secret": "clashoo",
"external_ui_download_url": "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip",
"external_ui_download_detour": "DIRECT"
}
}
}
@@ -1,468 +0,0 @@
#!/usr/bin/ucode
/* yaml2singbox.uc — convert mihomo/clash YAML proxies to sing-box outbounds
* Usage: ucode yaml2singbox.uc <yaml-in> [template-json] [json-out]
* Supports: ss, vmess, vless, trojan, hysteria2, tuic. Others are skipped.
* "__NODES__" in the template selector outbounds is expanded to all node tags. */
'use strict';
import { readfile, writefile, popen } from 'fs';
const TPL_DEFAULT = '/usr/share/clashoo/lib/templates/default.json';
const SUPPORTED = {
'ss': 'shadowsocks',
'shadowsocks':'shadowsocks',
'vmess': 'vmess',
'vless': 'vless',
'trojan': 'trojan',
'hysteria2': 'hysteria2',
'hy2': 'hysteria2',
'tuic': 'tuic'
};
function logerr(msg) {
warn(sprintf("[yaml2singbox] %s\n", msg));
}
function die(msg, code) {
logerr(msg);
exit(code || 1);
}
/* ---------- YAML read (via yq) ---------- */
function quote_sh(s) {
/* single-quote for shell; escape embedded quotes */
return "'" + replace(s, "'", "'\\''") + "'";
}
function read_yaml_as_json(path) {
const cmd = sprintf("yq -o=json eval . %s 2>/dev/null", quote_sh(path));
const h = popen(cmd, "r");
if (!h)
die(sprintf("popen failed: %s", cmd));
let buf = "", chunk;
while ((chunk = h.read(65536)))
buf += chunk;
h.close();
if (!length(buf))
die(sprintf("yq returned empty for %s (yaml loadable?)", path));
const data = json(buf);
if (data === null)
die("json parse of yq output failed");
return data;
}
/* ---------- helpers ---------- */
function pick(obj, ...keys) {
for (let k in keys)
if (obj[k] != null)
return obj[k];
return null;
}
function tobool(v) {
if (v === true || v === 'true' || v === 1 || v === '1') return true;
if (v === false || v === 'false' || v === 0 || v === '0') return false;
return null;
}
function toint(v) {
if (v == null) return null;
let n = +v;
return (n === n) ? n : null; /* NaN check */
}
function strip_null(obj) {
if (type(obj) !== 'object') return obj;
const out = {};
for (let k in obj) {
const v = obj[k];
if (v == null) continue;
if (type(v) === 'object') {
const sv = strip_null(v);
if (length(sv) > 0) out[k] = sv;
} else if (type(v) === 'array') {
const arr = map(v, (x) => (type(x) === 'object') ? strip_null(x) : x);
if (length(arr) > 0) out[k] = arr;
} else {
out[k] = v;
}
}
return out;
}
/* ---------- TLS block assembly ---------- */
function build_tls(p) {
const enabled = tobool(p.tls) || (p.sni && length(p.sni) > 0) || (p.servername && length(p.servername) > 0);
if (!enabled) return null;
const tls = {
enabled: true,
server_name: pick(p, 'sni', 'servername'),
insecure: tobool(p['skip-cert-verify']) === true ? true : null,
alpn: p.alpn
};
const reality = p['reality-opts'];
if (reality && type(reality) === 'object') {
tls.reality = {
enabled: true,
public_key: reality['public-key'],
short_id: reality['short-id']
};
}
const client_fp = p['client-fingerprint'];
if (client_fp)
tls.utls = { enabled: true, fingerprint: client_fp };
return strip_null(tls);
}
/* ---------- transport (ws / grpc / http) ---------- */
function build_transport(p) {
const net = p.network;
if (!net || net === 'tcp') return null;
if (net === 'ws') {
const opts = p['ws-opts'] || {};
return strip_null({
type: 'ws',
path: opts.path,
headers: opts.headers,
max_early_data: toint(opts['max-early-data']),
early_data_header_name: opts['early-data-header-name']
});
}
if (net === 'grpc') {
const opts = p['grpc-opts'] || {};
return { type: 'grpc', service_name: opts['grpc-service-name'] };
}
if (net === 'http' || net === 'h2') {
const opts = p['h2-opts'] || p['http-opts'] || {};
return strip_null({
type: 'http',
host: opts.host || opts.Host,
path: opts.path
});
}
if (net === 'httpupgrade') {
const opts = p['http-upgrade-opts'] || {};
return strip_null({ type: 'httpupgrade', path: opts.path, host: opts.host });
}
logerr(sprintf("unknown transport network '%s' for node '%s', ignored", net, p.name));
return null;
}
/* ---------- per-protocol conversion ---------- */
function convert_ss(p) {
const plugin = p.plugin, popts = p['plugin-opts'] || {};
let plugin_opts = null;
if (plugin && type(popts) === 'object') {
/* Field mapping: Clash simple-obfs -> sing-box obfs-local
* Clash: mode=http;host=xxx → sing-box: obfs=http;obfs-host=xxx */
const FIELD_MAP = {
'mode': 'obfs',
'host': 'obfs-host',
'uri': 'obfs-uri'
};
const parts = [];
for (let k in popts) {
const v = popts[k];
if (type(v) === 'object') continue;
push(parts, (FIELD_MAP[k] || k) + '=' + v);
}
plugin_opts = join(';', parts);
}
/* Clash "obfs" maps to sing-box plugin "obfs-local" */
let sb_plugin = null;
if (plugin === 'obfs') sb_plugin = 'obfs-local';
else if (plugin === 'v2ray-plugin') sb_plugin = 'v2ray-plugin';
else if (plugin === 'shadow-tls') sb_plugin = 'shadow-tls';
else if (plugin) sb_plugin = plugin;
return strip_null({
type: 'shadowsocks',
tag: p.name,
server: p.server,
server_port: toint(p.port),
method: p.cipher,
password: p.password,
plugin: sb_plugin,
plugin_opts: plugin_opts
});
}
function convert_vmess(p) {
return strip_null({
type: 'vmess',
tag: p.name,
server: p.server,
server_port: toint(p.port),
uuid: p.uuid,
alter_id: toint(p.alterId) || 0,
security: p.cipher || 'auto',
tls: build_tls(p),
transport: build_transport(p)
});
}
function convert_vless(p) {
return strip_null({
type: 'vless',
tag: p.name,
server: p.server,
server_port: toint(p.port),
uuid: p.uuid,
flow: p.flow,
tls: build_tls(p),
transport: build_transport(p)
});
}
function convert_trojan(p) {
return strip_null({
type: 'trojan',
tag: p.name,
server: p.server,
server_port: toint(p.port),
password: p.password,
tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server },
transport: build_transport(p)
});
}
function convert_hysteria2(p) {
const obfs_pass = p['obfs-password'];
return strip_null({
type: 'hysteria2',
tag: p.name,
server: p.server,
server_port: toint(p.port),
password: pick(p, 'password', 'auth'),
up_mbps: toint(p.up),
down_mbps: toint(p.down),
obfs: (p.obfs && obfs_pass) ? { type: p.obfs, password: obfs_pass } : null,
tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server }
});
}
function convert_tuic(p) {
return strip_null({
type: 'tuic',
tag: p.name,
server: p.server,
server_port: toint(p.port),
uuid: p.uuid,
password: p.password,
congestion_control: p['congestion-controller'] || p.congestion,
udp_relay_mode: p['udp-relay-mode'],
tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server, alpn: p.alpn }
});
}
function convert_proxy(p) {
const sb_type = SUPPORTED[p.type];
if (!sb_type) {
if (p.type && p.type !== 'select')
logerr(sprintf("skip unsupported type '%s' for '%s'", p.type, p.name));
return null;
}
if (!p.server || !p.port || !p.name) {
logerr(sprintf("skip proxy missing server/port/name (type=%s)", p.type));
return null;
}
switch (p.type) {
case 'ss':
case 'shadowsocks': return convert_ss(p);
case 'vmess': return convert_vmess(p);
case 'vless': return convert_vless(p);
case 'trojan': return convert_trojan(p);
case 'hysteria2':
case 'hy2': return convert_hysteria2(p);
case 'tuic': return convert_tuic(p);
}
return null;
}
/* ---------- dedup (tag must be unique) ---------- */
function dedupe_tags(nodes) {
const seen = {};
for (let n in nodes) {
let base = n.tag, t = base, i = 2;
while (seen[t]) {
t = base + '_' + i;
i++;
}
n.tag = t;
seen[t] = true;
}
return nodes;
}
/* Drop airline pseudo-nodes (Traffic:/Expire:/quota/官网/QQ etc.) from selectors
* and urltests. They're real outbounds (so the UI can read traffic/expiry) but
* don't forward — in a selector/urltest they win (0ms) and swallow all traffic. */
function is_pseudo_node_tag(tag) {
if (!tag) return false;
const t = '' + tag;
const patterns = [
/^Traffic[::]/i,
/^Expire[::]/i,
/剩余流量|剩余[::]/,
/距离下次重置/,
/到期(时间|日期)?[::]/,
/官网[::]/,
/网站[::]/,
/套餐[::]?/,
/客服[::]/,
/QQ[群]?[::]/i,
/Telegram|TG群|官方群/i,
/续费|订阅地址|流量重置/,
];
for (let re in patterns) if (match(t, re)) return true;
return false;
}
/* ---------- detect region from tag -> 'HK'/'JP'/'US'/'SG'/'OTHER'/'' ---------- */
function region_of(tag) {
if (!tag) return '';
const t = '' + tag;
if (match(t, /港|🇭🇰|HK[^A-Za-z]|[^A-Za-z]HK|^HK$|[Hh]ong[Kk]/)) return 'HK';
if (match(t, /日|🇯🇵|JP[^A-Za-z]|[^A-Za-z]JP|^JP$|[Jj]apan/)) return 'JP';
if (match(t, /美|🇺🇸|US[^A-Za-z]|[^A-Za-z]US|^US$|[Uu]nited.?[Ss]tates|[Aa]merica/)) return 'US';
if (match(t, /新加坡|🇸🇬|SG[^A-Za-z]|[^A-Za-z]SG|^SG$|[Ss]ingapore/)) return 'SG';
if (match(t, /台湾|台|🇹🇼|TW[^A-Za-z]|[^A-Za-z]TW|^TW$|[Tt]aiwan|韩国|韩|🇰🇷|KR[^A-Za-z]|[^A-Za-z]KR|^KR$|[Kk]orea/)) return 'OTHER';
return '';
}
/* ---------- expand __NODES__/__NODES_XX__ placeholders to real tags (dropping pseudo-nodes) ---------- */
function expand_node_placeholder(outbounds, node_tags) {
const real_tags = [];
for (let t in node_tags) if (!is_pseudo_node_tag(t)) push(real_tags, t);
/* bucket by region */
let tags_hk = [], tags_jp = [], tags_us = [], tags_sg = [], tags_other = [];
for (let t in real_tags) {
const r = region_of(t);
if (r === 'HK') push(tags_hk, t);
else if (r === 'JP') push(tags_jp, t);
else if (r === 'US') push(tags_us, t);
else if (r === 'SG') push(tags_sg, t);
else if (r === 'OTHER') push(tags_other, t);
}
/* when a region has no nodes, fall back to all nodes (avoid empty outbounds) */
if (!length(tags_hk)) tags_hk = real_tags;
if (!length(tags_jp)) tags_jp = real_tags;
if (!length(tags_us)) tags_us = real_tags;
if (!length(tags_sg)) tags_sg = real_tags;
if (!length(tags_other)) tags_other = real_tags;
for (let ob in outbounds) {
if (ob.type !== 'selector' && ob.type !== 'urltest') continue;
if (!ob.outbounds || type(ob.outbounds) !== 'array') continue;
const expanded = [];
for (let item in ob.outbounds) {
let list = null;
if (item === '__NODES__') list = real_tags;
else if (item === '__NODES_HK__') list = tags_hk;
else if (item === '__NODES_JP__') list = tags_jp;
else if (item === '__NODES_US__') list = tags_us;
else if (item === '__NODES_SG__') list = tags_sg;
else if (item === '__NODES_OTHER__') list = tags_other;
if (list !== null) { for (let t in list) push(expanded, t); }
else { push(expanded, item); }
}
ob.outbounds = expanded;
}
return outbounds;
}
/* ---------- proxy-providers ---------- */
function resolve_providers(yaml) {
if (type(yaml['proxy-providers']) !== 'object')
return [];
const all = [];
for (let name in yaml['proxy-providers']) {
const p = yaml['proxy-providers'][name];
if (p.type !== 'http' || !p.url) {
logerr(sprintf("provider '%s': skip (not http or no url)", name));
continue;
}
const cmd = sprintf("wget -q -O- --timeout=20 %s 2>/dev/null | yq -o=json eval . 2>/dev/null", quote_sh(p.url));
const h = popen(cmd, "r");
if (!h) { logerr(sprintf("provider '%s': download failed", name)); continue; }
let buf = "", chunk;
while ((chunk = h.read(65536))) buf += chunk;
h.close();
if (!length(buf)) { logerr(sprintf("provider '%s': empty response", name)); continue; }
const data = json(buf);
if (!data || type(data.proxies) !== 'array') {
logerr(sprintf("provider '%s': no proxies array in response", name));
continue;
}
logerr(sprintf("provider '%s': %d proxies", name, length(data.proxies)));
for (let px in data.proxies) push(all, px);
}
return all;
}
/* ---------- main ---------- */
const yaml_path = ARGV[0];
const tpl_path = ARGV[1] || TPL_DEFAULT;
const out_path = ARGV[2];
if (!yaml_path)
die("usage: ucode yaml2singbox.uc <yaml-in> [template-json] [json-out]");
const yaml = read_yaml_as_json(yaml_path);
let proxies = [];
if (type(yaml.proxies) === 'array')
proxies = yaml.proxies;
else
logerr("no inline proxies array, checking proxy-providers...");
if (type(yaml['proxy-providers']) === 'object') {
const pproxies = resolve_providers(yaml);
for (let p in pproxies) push(proxies, p);
}
if (!length(proxies))
die(sprintf("no proxies found in %s (inline or via providers)", yaml_path));
const tpl_raw = readfile(tpl_path);
if (!tpl_raw)
die(sprintf("cannot read template %s", tpl_path));
const tpl = json(tpl_raw);
if (!tpl || type(tpl.outbounds) !== 'array')
die(sprintf("template %s has no outbounds[]", tpl_path));
/* convert proxies */
const nodes = [];
let skipped = 0;
for (let p in proxies) {
const o = convert_proxy(p);
if (o) push(nodes, o); else skipped++;
}
dedupe_tags(nodes);
if (!length(nodes))
die(sprintf("no usable nodes converted from %d proxies (skipped=%d)", length(proxies), skipped));
logerr(sprintf("converted=%d skipped=%d", length(nodes), skipped));
/* prepend converted nodes to template outbounds */
const final_outbounds = [];
for (let n in nodes) push(final_outbounds, n);
for (let ob in tpl.outbounds) push(final_outbounds, ob);
/* expand __NODES__ placeholders */
const node_tags = map(nodes, (n) => n.tag);
expand_node_placeholder(final_outbounds, node_tags);
tpl.outbounds = final_outbounds;
const out = sprintf("%.J\n", tpl);
if (out_path) {
if (!writefile(out_path, out))
die(sprintf("writefile failed: %s", out_path));
logerr(sprintf("wrote %s (%d bytes, %d nodes)", out_path, length(out), length(nodes)));
} else {
print(out);
}
@@ -1,42 +0,0 @@
#!/bin/sh
set -f
[ "$(uci -q get clashoo.config.fake_ip_filter_migrated 2>/dev/null)" = "1" ] && exit 0
filters="$(uci -q get clashoo.config.fake_ip_filter 2>/dev/null)"
has_legacy=0
for filter in $filters; do
[ "$filter" = "*.lan" ] && has_legacy=1
done
if [ "$has_legacy" = "1" ]; then
uci -q delete clashoo.config.fake_ip_filter
added_lan=0
added_local=0
for filter in $filters; do
case "$filter" in
'*.lan'|'+.lan')
if [ "$added_lan" = "0" ]; then
uci -q add_list clashoo.config.fake_ip_filter='+.lan'
added_lan=1
fi
;;
'+.local')
if [ "$added_local" = "0" ]; then
uci -q add_list clashoo.config.fake_ip_filter='+.local'
added_local=1
fi
;;
*)
uci -q add_list clashoo.config.fake_ip_filter="$filter"
;;
esac
done
if [ "$added_local" = "0" ]; then
uci -q add_list clashoo.config.fake_ip_filter='+.local'
fi
fi
uci -q set clashoo.config.fake_ip_filter_migrated='1'
uci -q commit clashoo
@@ -1,38 +0,0 @@
#!/bin/sh
[ "$(uci -q get clashoo.config.acl_migrated 2>/dev/null)" = "1" ] && exit 0
if uci -q show clashoo 2>/dev/null | grep -q '=lan_acl$'; then
uci -q set clashoo.config.acl_migrated='1'
uci -q commit clashoo
exit 0
fi
mode="$(uci -q get clashoo.config.access_control 2>/dev/null)"
add_group() {
local dns="$1" proxy="$2" list_key="${3:-}" section value
section="$(uci -q add clashoo lan_acl)" || exit 1
uci -q set clashoo."$section".enabled='1'
uci -q set clashoo."$section".dns="$dns"
uci -q set clashoo."$section".proxy="$proxy"
if [ -n "$list_key" ]; then
for value in $(uci -q get clashoo.config."$list_key" 2>/dev/null); do
uci -q add_list clashoo."$section".ip="$value"
done
fi
}
case "$mode" in
1)
add_group 1 1 proxy_lan_ips
add_group 0 0
;;
2)
add_group 0 0 reject_lan_ips
add_group 1 1
;;
esac
uci -q set clashoo.config.acl_migrated='1'
uci -q commit clashoo
@@ -1,89 +0,0 @@
#!/bin/sh
# clashoo access check
# 用法: access_check.sh <url> [mode]
# mode = direct | proxy(默认 proxy)
# direct: 经 openwrt 主干直连,不走任何代理
# proxy: 走 clashoo 本地代理端口,验证核心出站
url="$1"
mode="${2:-proxy}"
[ -n "$url" ] || exit 1
attempts=1
ok=0
sum_ms=0
last_code=000
url_host() {
printf '%s' "$1" | sed -n 's#^[a-zA-Z][a-zA-Z0-9+.-]*://\([^/:?#]*\).*$#\1#p'
}
url_port() {
_u="$1"
_p="$(printf '%s' "$_u" | sed -n 's#^[a-zA-Z][a-zA-Z0-9+.-]*://[^/:?#]*:\([0-9][0-9]*\).*$#\1#p')"
[ -n "$_p" ] && { printf '%s' "$_p"; return; }
case "$_u" in
https://*) printf '443' ;;
http://*) printf '80' ;;
*) printf '443' ;;
esac
}
run_with_timeout() {
_secs="$1"
shift
if command -v timeout >/dev/null 2>&1; then
timeout "$_secs" "$@" 2>/dev/null
else
"$@" 2>/dev/null
fi
}
resolve_real_ip() {
_host="$1"
[ -n "$_host" ] || return 0
for _dns in 223.5.5.5 119.29.29.29 1.1.1.1; do
_ip="$(run_with_timeout 1 nslookup "$_host" "$_dns" | awk '/^Address: /{print $2} /^Address [0-9]+: /{print $3}' | grep -E '^[0-9]+(\.[0-9]+){3}$' | tail -n1)"
[ -n "$_ip" ] && { printf '%s' "$_ip"; return; }
done
}
set -- -4 -L --max-time 5 --connect-timeout 3 -s -o /dev/null -w '%{http_code} %{time_total}'
if [ "$mode" = "direct" ]; then
# 强制不走任何代理,避免误读 http_proxy 环境变量
set -- "$@" --noproxy '*'
host="$(url_host "$url")"
port="$(url_port "$url")"
real_ip="$(resolve_real_ip "$host")"
[ -n "$host" ] && [ -n "$port" ] && [ -n "$real_ip" ] && set -- "$@" --resolve "${host}:${port}:${real_ip}"
else
proxy_port="$(uci get clashoo.config.mixed_port 2>/dev/null)"
[ -z "$proxy_port" ] && proxy_port="$(uci get clashoo.config.http_port 2>/dev/null)"
[ -z "$proxy_port" ] && proxy_port=7890
set -- "$@" -x "http://127.0.0.1:${proxy_port}"
fi
set -- "$@" "$url"
i=1
while [ "$i" -le "$attempts" ]; do
out="$(curl "$@" 2>/dev/null || true)"
code="$(printf '%s' "$out" | awk '{print $1}')"
time_s="$(printf '%s' "$out" | awk '{print $2}')"
[ -n "$code" ] || code=000
[ -n "$time_s" ] || time_s=0
ms="$(awk -v t="$time_s" 'BEGIN{printf "%d", t*1000}')"
last_code="$code"
if [ "$code" = "200" ] || [ "$code" = "204" ] || [ "$code" = "301" ] || [ "$code" = "302" ]; then
ok=$((ok + 1))
sum_ms=$((sum_ms + ms))
fi
i=$((i + 1))
done
loss=$((attempts - ok))
avg_ms=0
if [ "$ok" -gt 0 ]; then
avg_ms=$((sum_ms / ok))
fi
echo "ok=$ok attempts=$attempts loss=$loss avg_ms=$avg_ms code=$last_code"
@@ -1,195 +0,0 @@
#!/bin/sh
set -eu
CACHE_DIR="/tmp/clashoo"
CACHE_FILE="/tmp/clashoo_check_cache"
LOCK_DIR="${CACHE_DIR}/access_check.lock"
LOCK_PID_FILE="${LOCK_DIR}/pid"
UPDATING_FLAG="${CACHE_DIR}/access_check_updating"
TMP_FILE="${CACHE_FILE}.tmp.$$"
DIAG_LOG="/tmp/clashoo_access_check.log"
take_lock() {
mkdir "$LOCK_DIR" 2>/dev/null || return 1
printf '%s\n' "$$" > "$LOCK_PID_FILE" 2>/dev/null
return 0
}
pid_is_cache_worker() {
[ -r "/proc/$1/cmdline" ] || return 1
tr '\0' ' ' <"/proc/$1/cmdline" 2>/dev/null | grep -q 'access_check_cache\.sh'
}
lock_is_dead() {
_pid="$(cat "$LOCK_PID_FILE" 2>/dev/null)"
case "$_pid" in
''|*[!0-9]*)
[ -n "$(find "$LOCK_DIR" -maxdepth 0 -mmin +3 2>/dev/null)" ]
return $?
;;
esac
[ ! -d "/proc/$_pid" ] && return 0
! pid_is_cache_worker "$_pid"
}
mkdir -p "$CACHE_DIR"
if ! take_lock; then
lock_is_dead || exit 0
rm -rf "$LOCK_DIR" >/dev/null 2>&1
take_lock || exit 0
fi
trap 'rm -rf "$LOCK_DIR" "$UPDATING_FLAG" "$TMP_FILE"' EXIT INT TERM
: > "$UPDATING_FLAG"
safe_int() {
case "${1:-}" in
''|*[!0-9]*)
printf '0'
;;
*)
printf '%s' "$1"
;;
esac
}
safe_code() {
case "${1:-}" in
''|*[!0-9A-Za-z]*)
printf '000'
;;
*)
printf '%s' "$1"
;;
esac
}
parse_field() {
_line="$1"
_key="$2"
printf '%s\n' "$_line" | sed -n "s/.*${_key}=\\([^ ]*\\).*/\\1/p"
}
probe_json() {
_line="$1"
_ok="$(safe_int "$(parse_field "$_line" "ok")")"
_attempts="$(safe_int "$(parse_field "$_line" "attempts")")"
_loss="$(safe_int "$(parse_field "$_line" "loss")")"
_avg_ms="$(safe_int "$(parse_field "$_line" "avg_ms")")"
_code="$(safe_code "$(parse_field "$_line" "code")")"
_state="down"
if [ "$_ok" -ge "$_attempts" ] && [ "$_attempts" -gt 0 ]; then
if [ "$_avg_ms" -ge 2500 ]; then
_state="high_latency"
else
_state="ok"
fi
elif [ "$_ok" -gt 0 ]; then
_state="loss"
fi
_ok_bool=false
[ "$_ok" -gt 0 ] && _ok_bool=true
printf '{"ok":%s,"state":"%s","code":"%s","ok_count":%s,"attempts":%s,"loss":%s,"avg_ms":%s}' \
"$_ok_bool" "$_state" "$_code" "$_ok" "$_attempts" "$_loss" "$_avg_ms"
}
log_diag() {
_line="$(date '+%Y-%m-%d %H:%M:%S') $1"
printf '%s\n' "$_line" >>"$DIAG_LOG" 2>/dev/null
_n="$(wc -l <"$DIAG_LOG" 2>/dev/null || echo 0)"
if [ "${_n:-0}" -gt 200 ]; then
tail -n 100 "$DIAG_LOG" >"${DIAG_LOG}.tmp" 2>/dev/null && mv "${DIAG_LOG}.tmp" "$DIAG_LOG" 2>/dev/null
fi
}
probe_run() {
_url="$1"
_mode="$2"
# nice + ionice:探测 IO/CPU 都低优先级,避免抢占 LuCI dispatcher
nice -n 19 /usr/share/clashoo/net/access_check.sh "$_url" "$_mode" 2>/dev/null || true
}
proxy_port="$(uci -q get clashoo.config.mixed_port)"
[ -z "$proxy_port" ] && proxy_port="$(uci -q get clashoo.config.http_port)"
[ -z "$proxy_port" ] && proxy_port="7890"
tcp_mode="$(uci -q get clashoo.config.tcp_mode)"
[ -z "$tcp_mode" ] && tcp_mode="redirect"
udp_mode="$(uci -q get clashoo.config.udp_mode)"
[ -z "$udp_mode" ] && udp_mode="$tcp_mode"
proxy_listening() {
# 检测 mixed-port 是否在 LISTEN(clashoo 未运行时立即标 proxy down,避免显示假绿)
if command -v ss >/dev/null 2>&1; then
ss -tln 2>/dev/null | awk -v p=":${proxy_port}" '$0 ~ "LISTEN" && index($4, p) {found=1; exit} END{exit !found}'
elif command -v netstat >/dev/null 2>&1; then
netstat -tln 2>/dev/null | awk -v p=":${proxy_port}" '$0 ~ "LISTEN" && index($4, p) {found=1; exit} END{exit !found}'
else
return 0
fi
}
# 并行探测,把 CPU 抢占窗口从串行 2s+ 压缩到最慢一路的耗时
f_db="${TMP_FILE}.db"
f_dy="${TMP_FILE}.dy"
f_pb="${TMP_FILE}.pb"
f_py="${TMP_FILE}.py"
probe_run "https://www.douyin.com/generate_204" "direct" >"$f_db" &
probe_run "https://www.youtube.com/generate_204" "direct" >"$f_dy" &
if proxy_listening; then
proxy_skipped=0
probe_run "https://www.douyin.com/generate_204" "proxy" >"$f_pb" &
probe_run "https://www.youtube.com/generate_204" "proxy" >"$f_py" &
else
# 代理端口未监听(clashoo 已停止),跳过探测,直接标 down
proxy_skipped=1
echo "ok=0 attempts=1 loss=1 avg_ms=0 code=skip" >"$f_pb"
echo "ok=0 attempts=1 loss=1 avg_ms=0 code=skip" >"$f_py"
fi
wait
direct_bytedance="$(cat "$f_db" 2>/dev/null)"
direct_youtube="$(cat "$f_dy" 2>/dev/null)"
proxy_bytedance="$(cat "$f_pb" 2>/dev/null)"
proxy_youtube="$(cat "$f_py" 2>/dev/null)"
rm -f "$f_db" "$f_dy" "$f_pb" "$f_py"
updated_at="$(date +%s)"
probe_all_down() {
for _p in "$direct_bytedance" "$direct_youtube" "$proxy_bytedance" "$proxy_youtube"; do
case "$_p" in
''|*"ok=0 "*) ;;
*) return 1 ;;
esac
done
return 0
}
if [ "$(uci -q get clashoo.config.access_check_debug)" = "1" ] || probe_all_down; then
log_diag "port=${proxy_port} proxy_skipped=${proxy_skipped} direct_bd=[${direct_bytedance}] direct_yt=[${direct_youtube}] proxy_bd=[${proxy_bytedance}] proxy_yt=[${proxy_youtube}]"
fi
cat >"$TMP_FILE" <<EOF
{
"proxy_port": "${proxy_port}",
"tcp_mode": "${tcp_mode}",
"udp_mode": "${udp_mode}",
"updated_at": ${updated_at},
"proxy_skipped": ${proxy_skipped},
"stale": false,
"updating": false,
"direct": {
"bytedance": $(probe_json "$direct_bytedance"),
"youtube": $(probe_json "$direct_youtube")
},
"proxy": {
"bytedance": $(probe_json "$proxy_bytedance"),
"youtube": $(probe_json "$proxy_youtube")
}
}
EOF
mv -f "$TMP_FILE" "$CACHE_FILE"
exit 0
@@ -1,40 +0,0 @@
#!/bin/sh
set -eu
RUNDIR="/tmp/clashoo"
PID_FILE="${RUNDIR}/access_check_daemon.pid"
LOCK_DIR="${RUNDIR}/access_check_daemon.lock"
# 周期:默认 30s,ACCESS_CHECK_INTERVAL 环境变量可固定覆盖
INTERVAL_FIXED="${ACCESS_CHECK_INTERVAL:-}"
mkdir -p "$RUNDIR"
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
old_pid="$(cat "$PID_FILE" 2>/dev/null)"
if [ -n "$old_pid" ] && [ -d "/proc/$old_pid" ] && \
tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null | grep -q 'access_check_daemon\.sh'; then
exit 0
fi
rm -rf "$LOCK_DIR" "$PID_FILE" >/dev/null 2>&1
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
fi
cleanup() {
rm -rf "$LOCK_DIR" "$PID_FILE"
}
trap cleanup EXIT
trap 'cleanup; exit 0' INT TERM
echo "$$" >"$PID_FILE"
while :; do
[ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ] || break
/usr/share/clashoo/net/access_check_cache.sh >/dev/null 2>&1 || true
# 每轮顺手卡一下 core.log 大小(tmpfs 很小,跑飞会写满 /tmp)
sh /usr/share/clashoo/lib/core_log_guard.sh >/dev/null 2>&1 || true
if [ -n "$INTERVAL_FIXED" ]; then
sleep "$INTERVAL_FIXED"
else
sleep 30
fi
done
@@ -1,240 +0,0 @@
#!/bin/sh
APPLY=0
TIMEOUT="${CLASHOO_DNS_TEST_TIMEOUT:-2}"
START_TS="$(date +%s 2>/dev/null || echo 0)"
while [ $# -gt 0 ]; do
case "$1" in
--apply) APPLY=1 ;;
esac
shift
done
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
json_pair() {
printf '"%s":"%s"' "$1" "$(json_escape "$2")"
}
is_ipv4() {
echo "$1" | grep -Eq '^[0-9]+(\.[0-9]+){3}$'
}
append_unique() {
list="$1"
item="$2"
[ -n "$item" ] || { printf '%s' "$list"; return; }
case " $list " in
*" $item "*) printf '%s' "$list" ;;
*) printf '%s %s' "$list" "$item" ;;
esac
}
current_default_nameservers() {
uci -q get clashoo.config.default_nameserver 2>/dev/null || true
}
current_dns_servers() {
uci -q show clashoo 2>/dev/null | awk -F= '
function clean(v) {
gsub(/^'\''|'\''$/, "", v);
return v;
}
/^clashoo\.@dnsservers\[[0-9]+\]\./ {
line=$1;
val=clean($2);
sub(/^clashoo\.@dnsservers\[/, "", line);
idx=line;
sub(/\].*$/, "", idx);
opt=line;
sub(/^[0-9]+\]\./, "", opt);
data[idx, opt]=val;
seen[idx]=1;
}
END {
for (idx in seen) {
if (data[idx, "enabled"] == "0") continue;
role=data[idx, "ser_type"];
addr=data[idx, "ser_address"];
proto=data[idx, "protocol"];
port=data[idx, "ser_port"];
if (addr == "") continue;
if (proto != "" && proto != "none" && addr !~ /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//) {
addr=proto addr;
if (port != "" && addr !~ /:[0-9]+$/) addr=addr ":" port;
}
print role "|" addr;
}
}'
}
servers_for_role() {
role="$1"
current_dns_servers | awk -F'|' -v role="$role" '$1 == role { print $2 }'
}
candidate_host_port() {
uri="$1"
default_port="$2"
hostport="${uri#*://}"
hostport="${hostport%%/*}"
host="${hostport%%:*}"
port="${hostport##*:}"
[ "$port" != "$hostport" ] || port="$default_port"
printf '%s %s\n' "$host" "$port"
}
probe_tls() {
uri="$1"
set -- $(candidate_host_port "$uri" 853)
host="$1"
port="$2"
command -v nc >/dev/null 2>&1 || return 1
nc -w "$TIMEOUT" "$host" "$port" >/dev/null 2>&1
}
probe_ip_dns() {
ip="$1"
domain="$2"
command -v nslookup >/dev/null 2>&1 || return 1
nslookup "$domain" "$ip" >/dev/null 2>&1
}
probe_latency_ms() {
candidate="$1"
domain="$2"
if [ "${candidate#https://}" != "$candidate" ]; then
if printf '%s' "$candidate" | grep -q '?'; then
q="$candidate&name=$domain&type=A"
else
q="$candidate?name=$domain&type=A"
fi
command -v curl >/dev/null 2>&1 || return 1
t="$(curl -fsS --connect-timeout "$TIMEOUT" --max-time "$TIMEOUT" \
-H 'accept: application/dns-json' -o /dev/null -w '%{time_total}' "$q" 2>/dev/null)" || return 1
printf '%s' "$t" | awk '{ms=$1*1000; if (ms<1) ms=1; printf "%d", ms}'
elif [ "${candidate#tls://}" != "$candidate" ]; then
probe_tls "$candidate" || return 1
printf '900'
elif [ "${candidate#udp://}" != "$candidate" ]; then
probe_ip_dns "${candidate#udp://}" "$domain" || return 1
printf '950'
elif [ "${candidate#tcp://}" != "$candidate" ]; then
probe_ip_dns "${candidate#tcp://}" "$domain" || return 1
printf '950'
else
is_ipv4 "$candidate" && probe_ip_dns "$candidate" "$domain" || return 1
printf '950'
fi
}
select_best() {
list="$1"
domain="$2"
fallback="$3"
best=""
best_score=999999
order=0
for candidate in $list; do
order=$((order + 1))
ms="$(probe_latency_ms "$candidate" "$domain")" || { FAILED_COUNT=$((FAILED_COUNT + 1)); continue; }
score=$((ms * 100 + order))
if [ "$score" -lt "$best_score" ]; then
best="$candidate"
best_score="$score"
fi
done
[ -n "$best" ] || best="$fallback"
printf '%s' "$best"
}
add_dnsserver() {
role="$1"
addr="$2"
proto="$3"
port="$4"
sec="$(uci -q add clashoo dnsservers)" || return 1
[ -n "$sec" ] || return 1
uci -q set clashoo."$sec".enabled='1' || return 1
uci -q set clashoo."$sec".ser_type="$role" || return 1
uci -q set clashoo."$sec".ser_address="$addr" || return 1
uci -q set clashoo."$sec".protocol="$proto" || return 1
if [ -n "$port" ]; then
uci -q set clashoo."$sec".ser_port="$port" || return 1
fi
return 0
}
set_default_nameserver() {
uci -q delete clashoo.config.default_nameserver >/dev/null 2>&1 || true
uci -q add_list clashoo.config.default_nameserver="$1" || return 1
return 0
}
apply_result() {
uci -q set clashoo.config.enable_dns='1' || { uci -q revert clashoo; return 1; }
set_default_nameserver "$BOOTSTRAP" || { uci -q revert clashoo; return 1; }
while uci -q delete clashoo.@dnsservers[0] >/dev/null 2>&1; do :; done
add_dnsserver 'nameserver' "$NAMESERVER" 'none' '' || { uci -q revert clashoo; return 1; }
add_dnsserver 'direct-nameserver' "$BOOTSTRAP" 'udp://' '' || { uci -q revert clashoo; return 1; }
add_dnsserver 'proxy-server-nameserver' "$PROXY_NS" 'none' '' || { uci -q revert clashoo; return 1; }
add_dnsserver 'fallback' "$FALLBACK_NS" 'none' '' || { uci -q revert clashoo; return 1; }
uci -q commit clashoo || { uci -q revert clashoo; return 1; }
return 0
}
is_domestic_dns() {
printf '%s' "$1" | grep -qE 'alidns|doh\.pub|dnspod|223\.5\.5\.5|223\.6\.6\.6|119\.29\.29\.29|119\.28\.28\.28|114\.114|180\.184|onedns'
}
is_overseas_dns() {
printf '%s' "$1" | grep -qE 'cloudflare|1\.1\.1\.1|1\.0\.0\.1|one\.one\.one\.one|dns\.google|8\.8\.8\.8|8\.8\.4\.4|9\.9\.9\.9|quad9|opendns|208\.67\.'
}
FAILED_COUNT=0
BOOTSTRAP_CANDIDATES="223.5.5.5 119.29.29.29 180.184.1.1"
DOMESTIC_CANDIDATES="https://dns.alidns.com/dns-query https://doh.pub/dns-query udp://223.5.5.5 udp://119.29.29.29"
FALLBACK_CANDIDATES="https://cloudflare-dns.com/dns-query https://dns.google/dns-query tls://1.1.1.1:853"
for ns in $(current_default_nameservers); do
is_ipv4 "$ns" && BOOTSTRAP_CANDIDATES="$(append_unique "$BOOTSTRAP_CANDIDATES" "$ns")"
done
for ns in $(servers_for_role 'nameserver') $(servers_for_role 'direct-nameserver'); do
is_domestic_dns "$ns" && DOMESTIC_CANDIDATES="$(append_unique "$DOMESTIC_CANDIDATES" "$ns")"
done
for ns in $(servers_for_role 'fallback'); do
is_overseas_dns "$ns" && FALLBACK_CANDIDATES="$(append_unique "$FALLBACK_CANDIDATES" "$ns")"
done
BOOTSTRAP="$(select_best "$BOOTSTRAP_CANDIDATES" www.baidu.com 223.5.5.5)"
NAMESERVER="$(select_best "$DOMESTIC_CANDIDATES" www.baidu.com https://dns.alidns.com/dns-query)"
PROXY_NS="tls://1.1.1.1:853"
FALLBACK_NS="$(select_best "$FALLBACK_CANDIDATES" www.google.com https://cloudflare-dns.com/dns-query)"
APPLIED=false
if [ "$APPLY" = "1" ]; then
if apply_result; then
APPLIED=true
else
printf '{"success":false,"error":"apply_failed","message":"DNS 自动配置写入失败"}\n'
exit 1
fi
fi
END_TS="$(date +%s 2>/dev/null || echo "$START_TS")"
ELAPSED_MS=$(( (END_TS - START_TS) * 1000 ))
printf '{'
printf '"success":true,'
printf '"applied":%s,' "$APPLIED"
printf '"restarted":false,'
json_pair bootstrap "$BOOTSTRAP"; printf ','
json_pair nameserver "$NAMESERVER"; printf ','
json_pair direct_nameserver "$BOOTSTRAP"; printf ','
json_pair proxy_nameserver "$PROXY_NS"; printf ','
json_pair fallback "$FALLBACK_NS"; printf ','
printf '"failed_count":%s,' "$FAILED_COUNT"
printf '"elapsed_ms":%s' "$ELAPSED_MS"
printf '}\n'
-898
View File
@@ -1,898 +0,0 @@
#!/bin/sh
set -eu
NFT_DIR="/var/run/clash"
SETS_RULES="${NFT_DIR}/fw4_sets.nft"
DSTNAT_RULES="${NFT_DIR}/fw4_dstnat.nft"
MANGLE_RULES="${NFT_DIR}/fw4_mangle.nft"
OUTPUT_RULES="${NFT_DIR}/fw4_output.nft"
BUILTIN_NFT_DIR="/usr/share/clashoo/nftables"
GEOIP_CN_NFT="${BUILTIN_NFT_DIR}/geoip_cn.nft"
GEOIP6_CN_NFT="${BUILTIN_NFT_DIR}/geoip6_cn.nft"
LOCAL_OUTPUT_TABLE="clashoo_local"
QUIC_BLOCK_TABLE="clashoo_quic"
# PROXY_FWMARK: inbound TPROXY mark, ip rule -> table PROXY_ROUTE_TABLE -> lo.
# CORE_ROUTING_MARK: mihomo outbound SO_MARK (= routing-mark in config).
# Must differ: otherwise mihomo egress is pulled to lo -> unreachable.
# When changing CORE_ROUTING_MARK, also update yum_change.sh routing_mark_dec.
PROXY_FWMARK="0x162"
PROXY_ROUTE_TABLE="0x162"
CORE_ROUTING_MARK="0x1a0a" # = 6666
ACL_BYPASS_FWMARK="0x163"
SINGBOX_BYPASS_FWMARK="0x2024"
ACL_BYPASS_PREF="8998"
DNSMASQ_BYPASS_PREF="8999"
uci_get() {
uci -q get "$1" 2>/dev/null || true
}
bool_enabled() {
case "$1" in
1|true|TRUE|yes|on) return 0 ;;
*) return 1 ;;
esac
}
acl_bool() {
[ -n "$1" ] || return 0
bool_enabled "$1"
}
tun_available() {
ip tuntap add mode tun name cotuntest >/dev/null 2>&1 || return 1
ip link del cotuntest >/dev/null 2>&1 || true
return 0
}
config_redir_port() {
uci_get clashoo.config.redir_port
}
config_tproxy_port() {
local port
port="$(uci_get clashoo.config.tproxy_port)"
if [ -n "$port" ]; then
printf '%s\n' "$port"
else
config_redir_port
fi
}
config_tcp_mode() {
uci_get clashoo.config.tcp_mode
}
config_udp_mode() {
uci_get clashoo.config.udp_mode
}
config_access_control() {
[ "$(uci_get clashoo.config.acl_migrated)" = "1" ] && {
printf '0\n'
return
}
uci_get clashoo.config.access_control
}
acl_sections() {
uci -q show clashoo 2>/dev/null | sed -n 's/^clashoo\.\([^.=]*\)=lan_acl$/\1/p'
}
acl_list() {
uci_get "$1"
}
acl_sections_catchall_last() {
local section
for section in $(acl_sections); do
[ -z "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] || printf '%s\n' "$section"
done
for section in $(acl_sections); do
[ -n "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] || printf '%s\n' "$section"
done
}
grouped_acl_enabled() {
local section
[ "$(uci_get clashoo.config.acl_migrated)" = "1" ] || return 1
for section in $(acl_sections); do
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] && return 0
done
return 1
}
acl_has_catchall() {
local section
for section in $(acl_sections); do
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue
[ -z "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] && return 0
done
return 1
}
lan_acl_enabled() {
grouped_acl_enabled && return 0
case "$(config_access_control)" in
1|2) return 0 ;;
esac
return 1
}
config_enable_dns() {
uci_get clashoo.config.enable_dns
}
config_dns_port() {
local port=""
if [ "$(uci_get clashoo.config.core_type)" != "singbox" ]; then
port="$(sed -n 's/^[[:space:]]*listen:.*:[[:space:]]*['"'"'"]*\([0-9]\{1,\}\)['"'"'"]*[[:space:]]*$/\1/p' /etc/clashoo/config.yaml 2>/dev/null | head -n1)"
fi
[ -n "$port" ] || port="$(uci_get clashoo.config.listen_port)"
printf '%s' "$port" | grep -Eq '^[0-9]+$' || port=1053
printf '%s\n' "$port"
}
lan_dns_split_enabled() {
lan_acl_enabled || return 1
bool_enabled "$(config_enable_dns)" || return 1
bool_enabled "$(config_ipv4_dns_hijack)" || bool_enabled "$(config_ipv6_dns_hijack)"
}
tun_acl_enabled() {
lan_acl_enabled || return 1
[ "$(config_tcp_mode)" = "tun" ] || [ "$(config_udp_mode)" = "tun" ]
}
config_ipv4_dns_hijack() {
uci_get clashoo.config.ipv4_dns_hijack
}
config_ipv6_dns_hijack() {
uci_get clashoo.config.ipv6_dns_hijack
}
config_ipv4_proxy() {
uci_get clashoo.config.ipv4_proxy
}
config_ipv6_proxy() {
uci_get clashoo.config.ipv6_proxy
}
singbox_tun_active() {
[ "$(uci_get clashoo.config.core_type)" = "singbox" ] || return 1
[ "$(config_tcp_mode)" = "tun" ] || [ "$(config_udp_mode)" = "tun" ]
}
config_bypass_china() {
uci_get clashoo.config.bypass_china
}
config_bypass_china_ipv6() {
local value
value="$(uci_get clashoo.config.bypass_china_ipv6)"
if [ -n "$value" ]; then
printf '%s\n' "$value"
else
config_bypass_china
fi
}
config_block_quic() {
uci_get clashoo.config.block_quic
}
config_bypass_port_mode() {
uci_get clashoo.config.bypass_port_mode
}
config_bypass_port_custom() {
uci_get clashoo.config.bypass_port_custom
}
config_legacy_bypass_port() {
uci_list clashoo.config.bypass_port
}
config_proxy_tcp_dport() {
local mode custom legacy value
mode="$(config_bypass_port_mode)"
custom="$(config_bypass_port_custom)"
legacy="$(uci_get clashoo.config.proxy_tcp_dport)"
[ -z "$legacy" ] && legacy="$(config_legacy_bypass_port)"
case "$mode" in
all)
# 空值在 render_port_match 中表示该协议的全部端口
printf '%s\n' ''
;;
common)
printf '%s\n' '22,53,80,443,8080,8443'
;;
custom)
value="$custom"
[ -z "$value" ] && value="$legacy"
printf '%s\n' "$value"
;;
*)
printf '%s\n' "$legacy"
;;
esac
}
config_proxy_udp_dport() {
local mode custom legacy value
mode="$(config_bypass_port_mode)"
custom="$(config_bypass_port_custom)"
legacy="$(uci_get clashoo.config.proxy_udp_dport)"
[ -z "$legacy" ] && legacy="$(config_legacy_bypass_port)"
case "$mode" in
all)
printf '%s\n' ''
;;
common)
printf '%s\n' '22,53,80,443,8080,8443'
;;
custom)
value="$custom"
[ -z "$value" ] && value="$legacy"
printf '%s\n' "$value"
;;
*)
printf '%s\n' "$legacy"
;;
esac
}
config_bypass_dscp() {
uci_list clashoo.config.bypass_dscp
}
config_bypass_fwmark() {
uci_list clashoo.config.bypass_fwmark
}
config_fake_ip_range6() {
local value
value="$(uci_get clashoo.config.fake_ip_range6)"
[ -n "$value" ] || value="fc00::/18"
[ "$(uci_get clashoo.config.enhanced_mode)" = "redir-host" ] && return 0
bool_enabled "$(uci_get clashoo.config.enable_ipv6)" || return 0
printf '%s\n' "$value"
}
config_fake_ip_range() {
local value
value="$(uci_get clashoo.config.fake_ip_range)"
[ -n "$value" ] && {
printf '%s\n' "$value"
return
}
printf '198.18.0.1/16\n'
}
uci_list() {
local key="$1"
uci -q show "$key" 2>/dev/null | sed -n "s/^${key}=//p" | sed "s/'//g"
}
ensure_firewall_include() {
local name="$1"
local path="$2"
local chain="${3:-}"
local position="${4:-chain-pre}"
uci -q batch <<-EOF >/dev/null
set firewall.${name}=include
set firewall.${name}.type='nftables'
set firewall.${name}.path='${path}'
set firewall.${name}.position='${position}'
$( [ -n "$chain" ] && printf "set firewall.%s.chain='%s'\n" "$name" "$chain" )
commit firewall
EOF
}
remove_firewall_include() {
local name="$1"
uci -q delete firewall."${name}" >/dev/null 2>&1 || true
}
render_common_returns() {
local fake6
fake6="$(config_fake_ip_range6)"
printf '%s\n' 'meta nfproto ipv4 ip daddr { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 } return'
if [ -n "$fake6" ]; then
printf 'meta nfproto ipv6 ip6 daddr { ::1/128, fc00::/7, fe80::/10, ff00::/8 } ip6 daddr != %s return\n' "$fake6"
else
printf '%s\n' 'meta nfproto ipv6 ip6 daddr { ::1/128, fc00::/7, fe80::/10, ff00::/8 } return'
fi
}
render_ip_elements() {
local list="$1"
local first=1 entry
for entry in $list; do
if [ "$first" -eq 0 ]; then
printf ', '
fi
printf '%s' "$entry"
first=0
done
}
render_token_elements() {
printf '%s\n' "$1" | tr ',\t' ' ' | awk '
BEGIN { first = 1 }
{
for (i = 1; i <= NF; i++) {
if ($i == "")
continue
if (!first)
printf ", "
printf "%s", $i
first = 0
}
}'
}
detect_coexist_fwmarks() {
# Merge co-installed proxy plugin fwmarks into bypass to avoid intercepting
# their traffic. Only when init.d exists (passwall:0x1 passwall2:0xff
# nikki:tproxy 0x80/tun 0x81 mask 0xff).
local marks=""
[ -x /etc/init.d/passwall ] && marks="$marks 0x1"
[ -x /etc/init.d/passwall2 ] && marks="$marks 0xff"
if [ -x /etc/init.d/nikki ]; then
local nm
nm="$(uci -q get nikki.routing.tproxy_fw_mark) $(uci -q get nikki.routing.tun_fw_mark)"
[ -z "$(echo "$nm" | tr -d ' ')" ] && nm="0x80 0x81"
marks="$marks $nm"
fi
printf '%s\n' "$marks"
}
merge_fwmark_tokens() {
# Always bypass PROXY_FWMARK (inbound TPROXY) and CORE_ROUTING_MARK (core egress)
# so nft never traps them. Also merge co-installed plugin fwmarks for coexistence.
local coexist
coexist="$(detect_coexist_fwmarks)"
printf '%s %s %s %s\n' "$1" "$PROXY_FWMARK" "$CORE_ROUTING_MARK" "$coexist" | tr ',\t' ' ' | awk '
BEGIN { first = 1 }
{
for (i = 1; i <= NF; i++) {
if ($i == "" || seen[$i]++)
continue
if (!first)
printf ", "
printf "%s", $i
first = 0
}
}'
}
render_port_match() {
local proto="$1"
local ports="$2"
local port_elements
port_elements="$(render_token_elements "$ports")"
if [ -n "$port_elements" ]; then
printf 'meta l4proto %s %s dport { %s }' "$proto" "$proto" "$port_elements"
else
printf 'meta l4proto %s' "$proto"
fi
}
render_acl_dns_rules() {
local redirect_port="$1" section ipv4 ipv6 mac ipv4_elements ipv6_elements mac_elements action
for section in $(acl_sections_catchall_last); do
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue
if acl_bool "$(uci_get "clashoo.${section}.dns")"; then
action="counter redirect to :${redirect_port}"
elif singbox_tun_active; then
action="meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return"
else
action="counter return"
fi
ipv4="$(acl_list "clashoo.${section}.ip")"
ipv6="$(acl_list "clashoo.${section}.ip6")"
mac="$(acl_list "clashoo.${section}.mac")"
ipv4_elements="$(render_ip_elements "$ipv4")"
ipv6_elements="$(render_ip_elements "$ipv6")"
mac_elements="$(render_ip_elements "$mac")"
if [ -z "$ipv4_elements$ipv6_elements$mac_elements" ]; then
bool_enabled "$(config_ipv4_dns_hijack)" && printf 'meta nfproto ipv4 meta l4proto { tcp, udp } th dport 53 %s\n' "$action"
bool_enabled "$(config_ipv6_dns_hijack)" && printf 'meta nfproto ipv6 meta l4proto { tcp, udp } th dport 53 %s\n' "$action"
continue
fi
if [ -n "$ipv4_elements" ] && bool_enabled "$(config_ipv4_dns_hijack)"; then
printf 'meta nfproto ipv4 ip saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$ipv4_elements" "$action"
fi
if [ -n "$ipv6_elements" ] && bool_enabled "$(config_ipv6_dns_hijack)"; then
printf 'meta nfproto ipv6 ip6 saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$ipv6_elements" "$action"
fi
if [ -n "$mac_elements" ]; then
bool_enabled "$(config_ipv4_dns_hijack)" && printf 'meta nfproto ipv4 ether saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$mac_elements" "$action"
bool_enabled "$(config_ipv6_dns_hijack)" && printf 'meta nfproto ipv6 ether saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$mac_elements" "$action"
fi
done
}
render_acl_proxy_rules() {
local mode="$1" proto="$2" port_match="$3" target_port="$4"
local section ipv4 ipv6 mac ipv4_elements ipv6_elements mac_elements enabled action4 action6
for section in $(acl_sections_catchall_last); do
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue
enabled="$(uci_get "clashoo.${section}.proxy")"
case "$mode" in
redirect)
if acl_bool "$enabled"; then
if singbox_tun_active; then
action4="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} counter redirect to :${target_port}"
else
action4="${port_match} counter redirect to :${target_port}"
fi
action6="$action4"
elif singbox_tun_active; then
action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return"
action6="$action4"
else
action4="meta l4proto ${proto} counter return"
action6="$action4"
fi
;;
tproxy)
if acl_bool "$enabled"; then
if singbox_tun_active; then
action4="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} tproxy ip to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
action6="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} tproxy ip6 to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
else
action4="${port_match} tproxy ip to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
action6="${port_match} tproxy ip6 to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
fi
elif singbox_tun_active; then
action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return"
action6="$action4"
else
action4="meta l4proto ${proto} counter return"
action6="$action4"
fi
;;
tun)
if acl_bool "$enabled"; then
action4="meta l4proto ${proto} counter return"
elif singbox_tun_active; then
action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter accept"
else
action4="meta l4proto ${proto} meta mark set ${ACL_BYPASS_FWMARK} counter accept"
fi
action6="$action4"
;;
esac
ipv4="$(acl_list "clashoo.${section}.ip")"
ipv6="$(acl_list "clashoo.${section}.ip6")"
mac="$(acl_list "clashoo.${section}.mac")"
ipv4_elements="$(render_ip_elements "$ipv4")"
ipv6_elements="$(render_ip_elements "$ipv6")"
mac_elements="$(render_ip_elements "$mac")"
if [ -z "$ipv4_elements$ipv6_elements$mac_elements" ]; then
bool_enabled "$(config_ipv4_proxy)" && printf 'meta nfproto ipv4 %s\n' "$action4"
bool_enabled "$(config_ipv6_proxy)" && printf 'meta nfproto ipv6 %s\n' "$action6"
continue
fi
if [ -n "$ipv4_elements" ] && bool_enabled "$(config_ipv4_proxy)"; then
printf 'meta nfproto ipv4 ip saddr { %s } %s\n' "$ipv4_elements" "$action4"
fi
if [ -n "$ipv6_elements" ] && bool_enabled "$(config_ipv6_proxy)"; then
printf 'meta nfproto ipv6 ip6 saddr { %s } %s\n' "$ipv6_elements" "$action6"
fi
if [ -n "$mac_elements" ]; then
bool_enabled "$(config_ipv4_proxy)" && printf 'meta nfproto ipv4 ether saddr { %s } %s\n' "$mac_elements" "$action4"
bool_enabled "$(config_ipv6_proxy)" && printf 'meta nfproto ipv6 ether saddr { %s } %s\n' "$mac_elements" "$action6"
fi
done
}
# Split proxied clients to the core DNS while ACL-bypassed clients use dnsmasq.
# Gated on UCI IPv4/IPv6 toggles.
# return 0: no output when both off, avoid set -e false-positive.
render_dns_hijack() {
local access_control="$1" redirect_port=53
if lan_dns_split_enabled; then
redirect_port="$(config_dns_port)"
fi
if grouped_acl_enabled; then
render_acl_dns_rules "$redirect_port"
acl_has_catchall && return 0
else
[ "$access_control" = "1" ] && printf '%s\n' 'ip saddr != @clash_proxy_lan return'
[ "$access_control" = "2" ] && printf '%s\n' 'ip saddr @clash_reject_lan return'
fi
bool_enabled "$(config_ipv4_dns_hijack)" && \
printf '%s\n' "meta nfproto ipv4 meta l4proto { tcp, udp } th dport 53 counter redirect to :${redirect_port}"
bool_enabled "$(config_ipv6_dns_hijack)" && \
printf '%s\n' "meta nfproto ipv6 meta l4proto { tcp, udp } th dport 53 counter redirect to :${redirect_port}"
return 0
}
apply_local_output_rule() {
local redir_port fake_ip_range tcp_mode bypass_fwmark bypass_china
local fwmark_elements fwmark_rule china_set china_rule dnsmasq_rule dnsmasq_uid
redir_port="$(config_redir_port)"
fake_ip_range="$(config_fake_ip_range)"
tcp_mode="$(config_tcp_mode)"
bypass_fwmark="$(config_bypass_fwmark)"
bypass_china="$(config_bypass_china)"
# Keep local-output redirect usable when tun mode is selected but tun
# device is unavailable on the system.
if [ "$tcp_mode" = "tun" ] && ! tun_available; then
tcp_mode="redirect"
fi
nft delete table ip ${LOCAL_OUTPUT_TABLE} >/dev/null 2>&1 || true
# Only apply local output redirect when tcp_mode is redirect
[ "$tcp_mode" != "redirect" ] && return 0
# bypass mihomo's own marks (prevent self-hijack -> dead loop)
fwmark_rule=""
fwmark_elements="$(merge_fwmark_tokens "$bypass_fwmark")"
[ -n "$fwmark_elements" ] && fwmark_rule="meta mark { ${fwmark_elements} } return"
# CN IP bypass (uses clashoo_china set from geoip_cn.nft)
china_set=""
china_rule=""
if bool_enabled "$bypass_china" && [ -s "$GEOIP_CN_NFT" ]; then
china_set="$(cat "$GEOIP_CN_NFT")"
china_rule="ip daddr @clashoo_china return"
fi
dnsmasq_rule=""
if lan_dns_split_enabled; then
dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)"
printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$' && dnsmasq_rule="meta skuid ${dnsmasq_uid} return"
fi
nft -f - <<EOF
table ip ${LOCAL_OUTPUT_TABLE} {
set clashoo_localnetwork {
type ipv4_addr
flags interval
auto-merge
elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8,
169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16,
224.0.0.0/4, 240.0.0.0/4 }
}
${china_set}
chain output {
type nat hook output priority dstnat; policy accept;
${dnsmasq_rule}
${fwmark_rule}
ip daddr @clashoo_localnetwork return
${china_rule}
ip daddr ${fake_ip_range} tcp dport != 53 redirect to :${redir_port}
meta l4proto tcp redirect to :${redir_port}
}
}
EOF
}
remove_local_output_rule() {
nft delete table ip ${LOCAL_OUTPUT_TABLE} >/dev/null 2>&1 || true
}
remove_acl_bypass_rules() {
local dnsmasq_uid
ip rule del pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
ip -6 rule del pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)"
if printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$'; then
ip rule del pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
ip -6 rule del pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
fi
}
apply_acl_bypass_rules() {
local dnsmasq_uid
remove_acl_bypass_rules
tun_acl_enabled || return 0
ip rule add pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
ip -6 rule add pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)"
if printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$'; then
ip rule add pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
ip -6 rule add pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
fi
}
apply_block_quic_rule() {
local fake_ip_range china_set china_rule scope
nft delete table inet ${QUIC_BLOCK_TABLE} >/dev/null 2>&1 || true
bool_enabled "$(config_block_quic)" || return 0
fake_ip_range="$(config_fake_ip_range)"
china_set=""
scope="ip daddr ${fake_ip_range}"
if [ "$(uci_get clashoo.config.enhanced_mode)" != "fake-ip" ]; then
if bool_enabled "$(config_bypass_china)" && [ -s "$GEOIP_CN_NFT" ]; then
china_set="$(cat "$GEOIP_CN_NFT")"
china_rule="ip daddr @clashoo_china return"
fi
scope=""
fi
nft -f - <<EOF
table inet ${QUIC_BLOCK_TABLE} {
${china_set}
chain prerouting {
type filter hook prerouting priority mangle - 1; policy accept;
${china_rule:-}
${scope} udp dport 443 counter reject with icmpx type port-unreachable
}
chain forward {
type filter hook forward priority -10; policy accept;
${china_rule:-}
${scope} udp dport 443 counter reject with icmpx type port-unreachable
}
}
EOF
}
remove_block_quic_rule() {
nft delete table inet ${QUIC_BLOCK_TABLE} >/dev/null 2>&1 || true
}
write_empty_set() {
local set_name="$1"
local set_type="$2"
printf 'set %s {\n\ttype %s;\n\tflags interval;\n\tauto-merge;\n}\n\n' "$set_name" "$set_type"
}
append_set_from_file_or_empty() {
local file_path="$1"
local set_name="$2"
local set_type="$3"
if [ -s "$file_path" ]; then
cat "$file_path"
printf '\n'
else
write_empty_set "$set_name" "$set_type"
fi
}
generate_rules() {
local redir_port tproxy_port tcp_mode udp_mode access_control fake_ip_range proxy_lan_ips reject_lan_ips
local proxy_tcp_dport proxy_udp_dport bypass_dscp bypass_fwmark bypass_china bypass_china_ipv6
local acl_catchall=0
redir_port="$(config_redir_port)"
tproxy_port="$(config_tproxy_port)"
tcp_mode="$(config_tcp_mode)"
udp_mode="$(config_udp_mode)"
access_control="$(config_access_control)"
grouped_acl_enabled && acl_has_catchall && acl_catchall=1
bypass_china="$(config_bypass_china)"
bypass_china_ipv6="$(config_bypass_china_ipv6)"
proxy_tcp_dport="$(config_proxy_tcp_dport)"
proxy_udp_dport="$(config_proxy_udp_dport)"
bypass_dscp="$(config_bypass_dscp)"
bypass_fwmark="$(config_bypass_fwmark)"
fake_ip_range="$(config_fake_ip_range)"
proxy_lan_ips="$(uci_list clashoo.config.proxy_lan_ips)"
reject_lan_ips="$(uci_list clashoo.config.reject_lan_ips)"
# When tun device is unavailable, fall back to non-tun transparent modes
# so routing rules still take effect for sing-box redirect/tproxy inbounds.
if [ "$tcp_mode" = "tun" ] || [ "$udp_mode" = "tun" ]; then
if ! tun_available; then
[ "$tcp_mode" = "tun" ] && tcp_mode="redirect"
[ "$udp_mode" = "tun" ] && udp_mode="tproxy"
fi
fi
mkdir -p "$NFT_DIR"
# Build optional elements lines (nftables rejects empty elements = {})
local proxy_elements reject_elements dscp_elements fwmark_elements
proxy_elements="$(render_ip_elements "$proxy_lan_ips")"
reject_elements="$(render_ip_elements "$reject_lan_ips")"
dscp_elements="$(render_token_elements "$bypass_dscp")"
fwmark_elements="$(merge_fwmark_tokens "$bypass_fwmark")"
tcp_match="$(render_port_match tcp "$proxy_tcp_dport")"
udp_match="$(render_port_match udp "$proxy_udp_dport")"
{
printf 'set clashoo_localnetwork {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n'
printf '\telements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 }\n}\n\n'
append_set_from_file_or_empty "$GEOIP_CN_NFT" clashoo_china ipv4_addr
append_set_from_file_or_empty "$GEOIP6_CN_NFT" clashoo_china6 ipv6_addr
printf 'set clash_proxy_lan {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n'
[ -n "$proxy_elements" ] && printf '\telements = { %s }\n' "$proxy_elements"
printf '}\n\n'
printf 'set clash_reject_lan {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n'
[ -n "$reject_elements" ] && printf '\telements = { %s }\n' "$reject_elements"
printf '}\n\n'
if grouped_acl_enabled && [ "$tcp_mode" = "tun" ]; then
printf 'chain clashoo_acl_tun_tcp {\n'
render_acl_proxy_rules tun tcp "$tcp_match" "$tproxy_port"
printf '}\n\n'
fi
if grouped_acl_enabled && [ "$udp_mode" = "tun" ]; then
printf 'chain clashoo_acl_tun_udp {\n'
render_acl_proxy_rules tun udp "$udp_match" "$tproxy_port"
printf '}\n'
fi
} > "$SETS_RULES"
: > "$OUTPUT_RULES"
# DNS hijack rules go atop DSTNAT_RULES: must run for all TCP modes
# (redirect/tproxy/tun) and BEFORE proxy redirect rules so port 53 is not stolen.
render_dns_hijack "$access_control" > "$DSTNAT_RULES"
# TCP rules: redirect mode appends proxy redirect; tproxy/tun skip this chain
case "$tcp_mode" in
redirect)
tcp_match="$(render_port_match tcp "$proxy_tcp_dport")"
cat >> "$DSTNAT_RULES" <<EOF
$( render_common_returns )
$( bool_enabled "$bypass_china_ipv6" && printf '%s\n' 'ip6 daddr @clashoo_china6 return' )
$( bool_enabled "$bypass_china" && printf '%s\n' 'ip daddr @clashoo_china return' )
$( [ -n "$dscp_elements" ] && printf '%s\n' "ip dscp { ${dscp_elements} } return" )
$( [ -n "$dscp_elements" ] && printf '%s\n' "ip6 dscp { ${dscp_elements} } return" )
$( [ -n "$fwmark_elements" ] && printf '%s\n' "meta mark { ${fwmark_elements} } return" )
$( ! bool_enabled "$(config_ipv4_proxy)" && printf '%s\n' 'meta nfproto ipv4 return' )
$( ! bool_enabled "$(config_ipv6_proxy)" && printf '%s\n' 'meta nfproto ipv6 return' )
$( grouped_acl_enabled && render_acl_proxy_rules redirect tcp "$tcp_match" "$redir_port" )
$( [ "$access_control" = "1" ] && printf '%s\n' 'ip saddr != @clash_proxy_lan return' )
$( [ "$access_control" = "2" ] && printf '%s\n' 'ip saddr @clash_reject_lan return' )
$( [ "$acl_catchall" -eq 0 ] && { singbox_tun_active && printf '%s ct mark set %s redirect to :%s\n' "$tcp_match" "$SINGBOX_BYPASS_FWMARK" "$redir_port" || printf '%s redirect to :%s\n' "$tcp_match" "$redir_port"; } )
EOF
;;
esac
# UDP rules: tproxy via mangle. TUN uses mangle only for LAN ACL bypass.
# Also handle TCP tproxy mode here (both TCP+UDP in mangle).
local need_mangle=0 tun_acl=0
[ "$tcp_mode" = "tproxy" ] && need_mangle=1
[ "$udp_mode" = "tproxy" ] && need_mangle=1
if tun_acl_enabled "$access_control"; then
need_mangle=1
tun_acl=1
fi
if [ "$need_mangle" -eq 1 ]; then
tcp_match="$(render_port_match tcp "$proxy_tcp_dport")"
udp_match="$(render_port_match udp "$proxy_udp_dport")"
{
render_common_returns
if bool_enabled "$bypass_china_ipv6"; then
printf 'meta nfproto ipv6 ip6 daddr @clashoo_china6 return\n'
fi
if bool_enabled "$bypass_china"; then
printf 'ip daddr @clashoo_china return\n'
fi
if [ -n "$dscp_elements" ]; then
printf 'ip dscp { %s } return\n' "$dscp_elements"
printf 'ip6 dscp { %s } return\n' "$dscp_elements"
fi
if [ -n "$fwmark_elements" ]; then
printf 'meta mark { %s } return\n' "$fwmark_elements"
fi
bool_enabled "$(config_ipv4_proxy)" || printf 'meta nfproto ipv4 return\n'
bool_enabled "$(config_ipv6_proxy)" || printf 'meta nfproto ipv6 return\n'
if grouped_acl_enabled; then
[ "$tcp_mode" = "tproxy" ] && render_acl_proxy_rules tproxy tcp "$tcp_match" "$tproxy_port"
[ "$tcp_mode" = "tun" ] && printf 'meta l4proto tcp jump clashoo_acl_tun_tcp\n'
[ "$udp_mode" = "tproxy" ] && render_acl_proxy_rules tproxy udp "$udp_match" "$tproxy_port"
[ "$udp_mode" = "tun" ] && printf 'meta l4proto udp jump clashoo_acl_tun_udp\n'
elif [ "$tun_acl" -eq 1 ]; then
[ "$access_control" = "1" ] && printf 'ip saddr != @clash_proxy_lan meta mark set %s return\n' "$ACL_BYPASS_FWMARK"
[ "$access_control" = "2" ] && printf 'ip saddr @clash_reject_lan meta mark set %s return\n' "$ACL_BYPASS_FWMARK"
else
[ "$access_control" = "1" ] && printf 'ip saddr != @clash_proxy_lan return\n'
[ "$access_control" = "2" ] && printf 'ip saddr @clash_reject_lan return\n'
fi
if bool_enabled "$(config_block_quic)"; then
printf 'meta l4proto udp udp dport 443 return\n'
fi
if [ "$tcp_mode" = "tproxy" ] && [ "$acl_catchall" -eq 0 ]; then
if singbox_tun_active; then
printf '%s ct mark set %s tproxy to :%s meta mark set %s accept\n' "$tcp_match" "$SINGBOX_BYPASS_FWMARK" "$tproxy_port" "$PROXY_FWMARK"
else
printf '%s tproxy to :%s meta mark set %s accept\n' "$tcp_match" "$tproxy_port" "$PROXY_FWMARK"
fi
fi
if [ "$udp_mode" = "tproxy" ] && [ "$acl_catchall" -eq 0 ]; then
if singbox_tun_active; then
printf '%s ct mark set %s tproxy to :%s meta mark set %s accept\n' "$udp_match" "$SINGBOX_BYPASS_FWMARK" "$tproxy_port" "$PROXY_FWMARK"
else
printf '%s tproxy to :%s meta mark set %s accept\n' "$udp_match" "$tproxy_port" "$PROXY_FWMARK"
fi
fi
} > "$MANGLE_RULES"
else
: > "$MANGLE_RULES"
fi
}
apply_rules() {
generate_rules
ensure_firewall_include clash_fw4_sets "$SETS_RULES" '' table-pre
ensure_firewall_include clash_fw4_dstnat "$DSTNAT_RULES" dstnat
remove_firewall_include clash_fw4_output
if [ -s "$MANGLE_RULES" ]; then
_route_table_dec="$((PROXY_ROUTE_TABLE))"
ensure_firewall_include clash_fw4_mangle "$MANGLE_RULES" mangle_prerouting
ip rule show 2>/dev/null | grep -q "fwmark ${PROXY_FWMARK}.*lookup ${_route_table_dec}" ||
ip rule add fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
ip route show table "$PROXY_ROUTE_TABLE" 2>/dev/null | grep -q 'local 0.0.0.0/0 dev lo' ||
ip route add local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
if bool_enabled "$(config_ipv6_proxy)"; then
ip -6 rule show 2>/dev/null | grep -q "fwmark ${PROXY_FWMARK}.*lookup ${_route_table_dec}" ||
ip -6 rule add fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
ip -6 route show table "$PROXY_ROUTE_TABLE" 2>/dev/null | grep -q 'local default dev lo' ||
ip -6 route add local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
else
ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
fi
else
remove_firewall_include clash_fw4_mangle
ip rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
ip route del local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
fi
/etc/init.d/firewall restart >/dev/null 2>&1 || /etc/init.d/firewall reload >/dev/null 2>&1 || true
apply_acl_bypass_rules
apply_local_output_rule
apply_block_quic_rule
}
remove_rules() {
remove_acl_bypass_rules
remove_local_output_rule
remove_block_quic_rule
remove_firewall_include clash_fw4_sets
remove_firewall_include clash_fw4_dstnat
remove_firewall_include clash_fw4_output
remove_firewall_include clash_fw4_mangle
remove_firewall_include clash_fw4_forward
uci commit firewall >/dev/null 2>&1 || true
rm -f "$SETS_RULES" "$DSTNAT_RULES" "$OUTPUT_RULES" "$MANGLE_RULES"
ip rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
ip route del local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
/etc/init.d/firewall restart >/dev/null 2>&1 || /etc/init.d/firewall reload >/dev/null 2>&1 || true
}
case "${1:-}" in
apply)
apply_rules
;;
remove)
remove_rules
;;
*)
echo "Usage: $0 {apply|remove}" >&2
exit 1
;;
esac
@@ -1,43 +0,0 @@
# clashoo 运行日志格式化
# 输出: MM-DD HH:MM:SS msg
# mihomo 原生行: time="YYYY-MM-DDTHH:MM:SS..." level=... msg="..."
/^time="[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/ {
# extract YYYY-MM-DD and HH:MM:SS
ts_date = substr($0, 12, 5) # MM-DD
ts_time = substr($0, 18, 8) # HH:MM:SS
utc_h = substr(ts_time, 1, 2) + 0
cst_h = (utc_h + 8) % 24
ts = sprintf("%s %02d:%s", ts_date, cst_h, substr(ts_time, 4))
prefix = ""
if (match($0, /level=warning /)) prefix = " [warn]"
else if (match($0, /level=error /)) prefix = " [err]"
else if (match($0, /level=fatal /)) prefix = " [fatal]"
i = index($0, "msg=\"")
if (i > 0) {
rest = substr($0, i + 5)
sub(/"[[:space:]]*$/, "", rest)
print ts prefix " " rest
next
}
print ts " " $0
next
}
# log_msg 行: " YYYY-MM-DD HH:MM:SS - msg" → "MM-DD HH:MM:SS msg"
/^[[:space:]]+[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][[:space:]]+[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/ {
gsub(/^[[:space:]]+/, "") # trim leading space
ts_date = substr($0, 6, 5) # MM-DD
ts_time = substr($0, 12, 8) # HH:MM:SS
# remove "YYYY-MM-DD HH:MM:SS - " prefix
sub(/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][[:space:]]+[0-9][0-9]:[0-9][0-9]:[0-9][0-9][[:space:]]*-[[:space:]]*/, "")
print ts_date " " ts_time " " $0
next
}
# 空行丢弃
NF == 0 { next }
{ print }
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,44 +0,0 @@
#!/bin/sh
ACTION="$1"
LOG_FILE="/tmp/clash_update.txt"
log_line() {
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOG_FILE"
}
run_init_async() {
local action="$1"
mkdir -p /tmp/lock >/dev/null 2>&1
(flock /tmp/lock/clashoo_rpc_async.lock /etc/init.d/clashoo "$action" >/dev/null 2>&1 </dev/null &)
}
case "$ACTION" in
start)
run_init_async start
exit 0
;;
stop)
run_init_async stop
exit 0
;;
restart)
# Fire-and-forget restart used by LuCI RPC to avoid blocking UI apply flow.
run_init_async restart
exit 0
;;
update_china_ip)
log_line "[china-ip] task started"
nohup /usr/share/clashoo/update/update_china_ip.sh >>"$LOG_FILE" 2>&1 </dev/null &
exit 0
;;
update_geoip)
log_line "GeoIP 更新任务已触发"
(exec 1000>&-; nohup /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1 </dev/null &)
exit 0
;;
*)
echo "usage: $0 {start|stop|restart|update_china_ip|update_geoip}" >&2
exit 1
;;
esac
Binary file not shown.
Binary file not shown.
@@ -1,21 +0,0 @@
#!/bin/sh
. /lib/functions.sh
CURL_GROUP_CACHE="/usr/share/clashbackup/clash_gorup.json"
CURL_NOW_CACHE="/usr/share/clashbackup/clash_now.json"
CURL_CACHE="/usr/share/clashbackup/clash_curl.json"
HISTORY_PATH="/usr/share/clashbackup/history"
SECRET=$(uci get clashoo.config.dash_pass 2>/dev/null)
LAN_IP=$(uci get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null)
PORT=$(uci get clashoo.config.dash_port 2>/dev/null)
curl -m 5 --retry 2 -w %{http_code}"\n" -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X GET http://"$LAN_IP":"$PORT"/proxies > "$CURL_CACHE" 2>/dev/null
if [ "$(sed -n '$p' "$CURL_CACHE" 2>/dev/null)" = "200" ]; then
if [ ! -d /usr/share/clashbackup ];then
mkdir -p /usr/share/clashbackup 2>/dev/null
fi
cat "$CURL_CACHE" |jsonfilter -e '@["proxies"][@.type="Selector"]["name"]' > "$CURL_GROUP_CACHE" 2>/dev/null
cat "$CURL_CACHE" |jsonfilter -e '@["proxies"][@.type="Selector"]["now"]' > "$CURL_NOW_CACHE" 2>/dev/null
awk 'NR==FNR{a[i]=$0;i++}NR>FNR{print a[j]"#*#"$0;j++}' "$CURL_GROUP_CACHE" "$CURL_NOW_CACHE" > "$HISTORY_PATH" 2>/dev/null
fi
rm -rf /usr/share/clashbackup/clash_*.json 2>/dev/null
@@ -1,353 +0,0 @@
#!/bin/sh
REAL_LOG="/usr/share/clashoo/clashoo_real.txt"
UPDATE_LOG="/tmp/clash_update.txt"
LIST_FILE="/usr/share/clashbackup/confit_list.conf"
SUB_DIR="/usr/share/clashoo/config/sub"
TMP_PREFIX="/tmp/clash_sub_$$"
DEFAULT_SUB_UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
subtype="$(uci -q get clashoo.config.subcri 2>/dev/null)"
config_name_raw="$(uci -q get clashoo.config.config_name 2>/dev/null)"
lang="$(uci -q get luci.main.lang 2>/dev/null)"
log_text() {
if [ "$lang" = "en" ]; then
echo "$1" >"$REAL_LOG"
else
echo "$2" >"$REAL_LOG"
fi
}
log_update() {
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
}
sanitize_name() {
local name
name="$1"
name="$(printf '%s' "$name" | tr 'A-Z' 'a-z')"
name="$(printf '%s' "$name" | sed -e 's/\.yaml$//' -e 's/\.yml$//')"
name="$(printf '%s' "$name" | tr ' /' '--')"
name="$(printf '%s' "$name" | sed -e 's/[^a-z0-9._-]/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')"
printf '%s' "$name"
}
sanitize_custom_name() {
local name
name="$1"
name="$(printf '%s' "$name" | sed -e 's/\.yaml$//' -e 's/\.yml$//')"
name="$(printf '%s' "$name" | tr ' /' '--')"
name="$(printf '%s' "$name" | sed -e 's/[\\]//g' -e 's/\.\.+/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')"
printf '%s' "$name"
}
url_decode() {
printf '%s' "$1" | sed 's/%/\\x/g' | xargs -0 printf '%b' 2>/dev/null || printf '%s' "$1"
}
url_to_name() {
local url host qname decoded
url="$1"
qname="$(printf '%s' "$url" | sed -n 's/.*[?&]filename=\([^&#]*\).*/\1/p')"
[ -n "$qname" ] || qname="$(printf '%s' "$url" | sed -n 's/.*[?&]name=\([^&#]*\).*/\1/p')"
if [ -n "$qname" ]; then
# URL decode then strip non-filename chars, keep CJK and alphanumeric
decoded="$(url_decode "$qname")"
decoded="$(printf '%s' "$decoded" | tr -d '\r\n' | sed -e 's/[[:space:]]/-/g' -e 's/[/\\:*?"<>|]//g' -e 's/\.yaml$//' -e 's/\.yml$//')"
[ -n "$decoded" ] && printf '%s' "$decoded" && return
fi
qname="$(sanitize_name "$qname")"
if [ -n "$qname" ]; then
printf '%s' "$qname"
return
fi
host="$(printf '%s' "$url" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's/:.*$//' -e 's#\..*$##')"
host="$(sanitize_name "$host")"
[ -n "$host" ] || host="sub"
printf '%s' "$host"
}
next_available_name() {
local base try idx
base="$1"
base="$(printf '%s' "$base" | sed -e 's/\.yaml$//' -e 's/\.yml$//')"
base="$(printf '%s' "$base" | tr ' /' '--')"
base="$(printf '%s' "$base" | sed -e 's/[\\]//g' -e 's/\.\.+/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')"
[ -n "$base" ] || base="sub"
if [ ! -f "$SUB_DIR/${base}.yaml" ]; then
printf '%s' "$base"
return
fi
idx=2
while :; do
try="${base}-${idx}"
if [ ! -f "$SUB_DIR/${try}.yaml" ]; then
printf '%s' "$try"
return
fi
idx=$((idx + 1))
done
}
get_subscription_urls() {
uci -q show clashoo.config 2>/dev/null | awk -F"'" '
/^clashoo.config.subscribe_url=/ {
if (NF >= 3) {
for (i = 2; i <= NF; i += 2) {
if (length($i) > 0) print $i
}
} else {
sub(/^clashoo.config.subscribe_url=/, "", $0)
if (length($0) > 0) print $0
}
}
'
}
ensure_system_dns() {
local test_host
test_host="github.com"
if nslookup "$test_host" 127.0.0.1 >/dev/null 2>&1 || nslookup "$test_host" >/dev/null 2>&1; then
return 0
fi
uci delete dhcp.@dnsmasq[0].server >/dev/null 2>&1
uci set dhcp.@dnsmasq[0].noresolv='0' >/dev/null 2>&1
uci del_list dhcp.@dnsmasq[0].server='127.0.0.1#' >/dev/null 2>&1
uci del_list dhcp.@dnsmasq[0].server='127.0.0.1#5300' >/dev/null 2>&1
uci add_list dhcp.@dnsmasq[0].server='119.29.29.29' >/dev/null 2>&1
uci add_list dhcp.@dnsmasq[0].server='223.5.5.5' >/dev/null 2>&1
uci commit dhcp >/dev/null 2>&1
/etc/init.d/dnsmasq restart >/dev/null 2>&1
sleep 2
}
extract_host() {
printf '%s' "$1" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's#:.*$##' -e 's#.*@##'
}
resolve_via() {
local host dns
host="$1"
dns="$2"
nslookup "$host" "$dns" 2>/dev/null | awk '
/^Address/ {
ip = $NF
if (ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ &&
ip !~ /^127\./ &&
ip !~ /^0\./ &&
ip !~ /^198\.18\./ &&
ip != "8.8.8.8" && ip != "8.8.4.4" &&
ip != "1.1.1.1" && ip != "1.0.0.1" &&
ip != "223.5.5.5" && ip != "223.6.6.6" &&
ip != "119.29.29.29" && ip != "114.114.114.114") {
print ip
exit
}
}'
}
curl_subscription() {
local url tmp hdr err ua host ip http_code rc
url="$1"
tmp="$2"
hdr="$3"
err="$4"
ua="$5"
host="$6"
ip="$7"
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
set --
if [ -n "$host" ] && [ -n "$ip" ]; then
set -- --resolve "$host:443:$ip" --resolve "$host:80:$ip"
fi
http_code="$(curl -sSL --connect-timeout 15 --max-time 60 \
--speed-time 30 --speed-limit 1 --retry 2 \
-H "User-Agent: ${ua}" -D "$hdr" -o "$tmp" \
"$@" \
-w '%{http_code}' "$url" 2>"$err")"
rc=$?
printf '%s\n' "$http_code"
return "$rc"
}
download_subscription() {
local url target tmp hdr err rc http_code info_line ua err_msg
local host ip dns
url="$1"
target="$2"
tmp="${TMP_PREFIX}.yaml"
hdr="${TMP_PREFIX}.hdr"
err="${TMP_PREFIX}.err"
ua="$(uci -q get clashoo.config.sub_ua 2>/dev/null)"
[ -n "$ua" ] || ua="$DEFAULT_SUB_UA"
if command -v curl >/dev/null 2>&1; then
http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "" "")"
rc=$?
if [ "$rc" -ne 0 ] || [ "$http_code" = "000" ]; then
host="$(extract_host "$url")"
case "$host" in
''|[-.]*|*[!A-Za-z0-9.-]*) host='' ;;
esac
if [ -n "$host" ]; then
for dns in 223.5.5.5 119.29.29.29 1.1.1.1 8.8.8.8; do
ip="$(resolve_via "$host" "$dns")"
[ -n "$ip" ] || continue
log_update "DNS 回退:${host} -> ${ip} (@${dns})"
http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "$host" "$ip")"
rc=$?
[ "$rc" -eq 0 ] && [ "$http_code" = "200" ] && break
done
fi
fi
else
wget -q --timeout=20 \
--user-agent="$ua" "$url" -O "$tmp" 2>"$err"
rc=$?
http_code=""
fi
if [ "$rc" -ne 0 ]; then
err_msg="$(grep -a 'curl:' "$err" 2>/dev/null | tail -1)"
[ -z "$err_msg" ] && err_msg="$(tail -1 "$err" 2>/dev/null)"
log_update "下载失败:$(basename "$target") rc=${rc} ${err_msg}"
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
return 1
fi
if [ -n "$http_code" ] && [ "$http_code" != "200" ]; then
log_update "下载失败:$(basename "$target") HTTP ${http_code}"
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
return 1
fi
if ! grep -Eq '^(proxies|proxy-providers):' "$tmp" 2>/dev/null; then
log_update "校验失败:$(basename "$target") 内容不含 proxies/proxy-providers"
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
return 1
fi
info_line="$(grep -i 'subscription-userinfo:' "$hdr" 2>/dev/null | head -1 | \
sed 's/^[Ss]ubscription-[Uu]serinfo:[[:space:]]*//' | tr -d '\r')"
[ -n "$info_line" ] && printf '%s\n' "$info_line" > "${target}.info" || \
rm -f "${target}.info" >/dev/null 2>&1
rm -f "$hdr" "$err" >/dev/null 2>&1
mv "$tmp" "$target" >/dev/null 2>&1 || {
rm -f "$tmp" >/dev/null 2>&1
return 1
}
printf '%s\n' "$ua" >"${target}.ua"
return 0
}
upsert_meta() {
local filename url typ tmpf
filename="$1"
url="$2"
typ="$3"
tmpf="${TMP_PREFIX}.list"
[ -f "$LIST_FILE" ] || touch "$LIST_FILE"
awk -F '#' -v n="$filename" '$1 != n { print $0 }' "$LIST_FILE" >"$tmpf"
printf '%s#%s#%s\n' "$filename" "$url" "$typ" >>"$tmpf"
mv "$tmpf" "$LIST_FILE"
}
cleanup_tmp() {
rm -f "${TMP_PREFIX}.yaml" "${TMP_PREFIX}.urls" "${TMP_PREFIX}.list" \
"${TMP_PREFIX}.hdr" "${TMP_PREFIX}.err" >/dev/null 2>&1
}
trap cleanup_tmp EXIT INT TERM
[ "$subtype" = "clash" ] || [ "$subtype" = "meta" ] || subtype="clash"
mkdir -p "$SUB_DIR" /usr/share/clashbackup >/dev/null 2>&1
[ -f "$LIST_FILE" ] || touch "$LIST_FILE"
URLS_FILE="${TMP_PREFIX}.urls"
get_subscription_urls | sed '/^[[:space:]]*$/d' | awk '!seen[$0]++' >"$URLS_FILE"
url_count="$(wc -l <"$URLS_FILE" 2>/dev/null | tr -d ' ')"
if [ -z "$url_count" ] || [ "$url_count" -eq 0 ]; then
log_update "未找到订阅链接"
log_text "No subscription URL found" "未找到订阅链接"
sleep 2
log_text "Clash for OpenWRT" "Clash for OpenWRT"
exit 1
fi
ensure_system_dns
log_update "开始下载订阅(共 ${url_count} 条)"
log_text "Downloading subscription..." "开始下载订阅..."
base_name="$(sanitize_custom_name "$config_name_raw")"
timestamp="$(date +%Y%m%d)"
success=0
failed=0
idx=0
first_file=""
while IFS= read -r url; do
[ -n "$url" ] || continue
idx=$((idx + 1))
if [ -n "$base_name" ]; then
if [ "$url_count" -gt 1 ]; then
name_candidate="$(sanitize_name "${base_name}-${idx}")"
file_base="$(next_available_name "$name_candidate")"
else
file_base="$base_name"
fi
else
name_candidate="$(url_to_name "$url")-${timestamp}"
if [ "$url_count" -gt 1 ]; then
name_candidate="${name_candidate}-${idx}"
fi
file_base="$(next_available_name "$name_candidate")"
fi
target_file="$SUB_DIR/${file_base}.yaml"
if download_subscription "$url" "$target_file"; then
upsert_meta "${file_base}.yaml" "$url" "$subtype"
log_update "订阅下载成功:${file_base}.yaml"
[ -n "$first_file" ] || first_file="$target_file"
success=$((success + 1))
else
log_update "订阅下载失败:${file_base}.yaml"
failed=$((failed + 1))
fi
done <"$URLS_FILE"
if [ "$success" -gt 0 ]; then
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
if [ -z "$use_config" ] || [ ! -f "$use_config" ]; then
uci set clashoo.config.use_config="$first_file"
uci set clashoo.config.config_type='1'
uci commit clashoo
fi
log_text "Subscription download completed: ${success} success, ${failed} failed" "订阅下载完成:成功 ${success} 个,失败 ${failed} 个"
log_update "订阅下载完成:成功 ${success} 个,失败 ${failed} 个"
ret=0
else
log_text "All subscription downloads failed" "订阅下载失败"
log_update "订阅下载失败:全部链接失败"
ret=1
fi
sleep 2
log_text "Clash for OpenWRT" "Clash for OpenWRT"
exit "$ret"
@@ -1,389 +0,0 @@
#!/bin/sh
# Shared DNS helpers for ClashOO runtime generators. Keep POSIX sh compatible.
dns_trim() {
printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}
dns_has_scheme() {
printf '%s' "$1" | grep -Eq '^[A-Za-z][A-Za-z0-9+.-]*://'
}
dns_norm_protocol() {
case "$(dns_trim "${1:-}")" in
''|'none') printf '' ;;
udp|'udp://') printf 'udp://' ;;
tcp|'tcp://') printf 'tcp://' ;;
dot|tls|'tls://') printf 'tls://' ;;
doh|https|'https://') printf 'https://' ;;
doq|quic|'quic://') printf 'quic://' ;;
*) printf '%s' "$1" ;;
esac
}
dns_normalize_server() {
local address protocol port prefix
address=$(dns_trim "${1:-}")
protocol=$(dns_trim "${2:-}")
port=$(dns_trim "${3:-}")
[ -n "$address" ] || return 0
if dns_has_scheme "$address"; then
printf '%s' "$address"
return 0
fi
prefix=$(dns_norm_protocol "$protocol")
if [ -n "$port" ]; then
printf '%s%s:%s' "$prefix" "$address" "$port"
else
printf '%s%s' "$prefix" "$address"
fi
}
dns_yaml_sq() {
printf "%s" "$1" | sed "s/'/''/g"
}
dns_yaml_list_item() {
local value
value=$(dns_yaml_sq "$1")
printf " - '%s'\n" "$value"
}
# Route DNS upstreams by rules and preserve the original setting.
dns_mihomo_apply_respect_rules() {
local cfg="$1" enabled="${2:-0}" tmp="${1}.$$"
[ -f "$cfg" ] || return 0
awk -v enabled="$enabled" '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
function spaces(n, out,i) {
out = ""
for (i = 0; i < n; i++)
out = out " "
return out
}
function trim_value(s) {
sub(/^[[:space:]]*/, "", s)
sub(/[[:space:]]*$/, "", s)
return s
}
function flush_dns( i, sp) {
if (!in_dns)
return
sp = spaces(child_indent > 0 ? child_indent : dns_indent + 2)
print dns_lines[1]
if (enabled == "1") {
print sp "# >>> clashoo:dns_leak_protect_respect_rules"
print sp "# original-respect-rules: " (original_value != "" ? original_value : "absent")
print sp "respect-rules: true"
print sp "# <<< clashoo:dns_leak_protect_respect_rules"
} else if (original_value != "" && original_value != "absent") {
print sp "respect-rules: " original_value
}
for (i = 2; i <= dns_n; i++)
print dns_lines[i]
in_dns = 0
dns_n = 0
}
/^dns:[[:space:]]*$/ {
flush_dns()
in_dns = 1
dns_indent = indent_len($0)
child_indent = -1
dns_n = 1
dns_lines[dns_n] = $0
original_value = ""
in_marker = 0
next
}
in_dns {
cur_indent = indent_len($0)
if ($0 ~ /^[^[:space:]#][^:]*:/ && cur_indent <= dns_indent) {
flush_dns()
print
next
}
if ($0 ~ /^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_respect_rules[[:space:]]*$/) {
in_marker = 1
if (child_indent < 0) child_indent = cur_indent
next
}
if (in_marker) {
if ($0 ~ /^[[:space:]]*#[[:space:]]*original-respect-rules:[[:space:]]*/) {
value = $0
sub(/^[[:space:]]*#[[:space:]]*original-respect-rules:[[:space:]]*/, "", value)
original_value = trim_value(value)
}
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_respect_rules[[:space:]]*$/)
in_marker = 0
next
}
if ($0 ~ /^[[:space:]]*respect-rules:[[:space:]]*/) {
if (original_value == "") {
value = $0
sub(/^[[:space:]]*respect-rules:[[:space:]]*/, "", value)
original_value = trim_value(value)
}
if (child_indent < 0) child_indent = cur_indent
next
}
if ($0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && cur_indent > dns_indent && child_indent < 0)
child_indent = cur_indent
dns_lines[++dns_n] = $0
next
}
{ print }
END { flush_dns() }
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
rm -f "$tmp" 2>/dev/null
}
# Use fallback DNS for queries; keep bootstrap DNS unchanged.
dns_mihomo_apply_leak_nameservers() {
local cfg="$1" enabled="${2:-0}" state="${1}.dns-leak-nameserver"
local selected="${1}.dns-leak-selected" tmp="${1}.$$"
# The config is rebuilt from the selected subscription on every run.
# A saved nameserver from an earlier run must never replace its DNS.
rm -f "$state" 2>/dev/null
[ "$enabled" = "1" ] || return 0
[ -f "$cfg" ] || return 0
command -v yq >/dev/null 2>&1 || return 0
yq e -r '.dns.fallback[]' "$cfg" > "$selected" 2>/dev/null || true
if [ ! -s "$selected" ]; then
printf '%s\n' 'https://1.1.1.1/dns-query' 'https://8.8.8.8/dns-query' > "$selected"
fi
awk -v selected="$selected" '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
function spaces(n, out,i) {
out = ""
for (i = 0; i < n; i++) out = out " "
return out
}
function print_selected(sp, line) {
print sp "nameserver:"
while ((getline line < selected) > 0)
if (line != "") print sp " - " line
close(selected)
inserted = 1
}
/^dns:[[:space:]]*$/ {
in_dns = 1
dns_indent = indent_len($0)
child_indent = -1
print
next
}
in_dns && /^[^[:space:]#][^:]*:/ {
if (!inserted) print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
in_dns = 0
print
next
}
in_dns && /^[[:space:]]*[^#[:space:]][^:]*:/ && child_indent < 0 {
child_indent = indent_len($0)
}
in_dns && /^[[:space:]]*nameserver:/ {
ns_indent = indent_len($0)
rest = $0
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
sub(/[[:space:]]*(#.*)?$/, "", rest)
print_selected(spaces(ns_indent))
if (rest == "") skip_ns = 1
next
}
skip_ns {
cur_indent = indent_len($0)
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next
skip_ns = 0
}
{ print }
END {
if (in_dns && !inserted)
print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
}
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
rm -f "$selected" "$tmp" 2>/dev/null
}
# handle mihomo dns block:
# - strip previous clashoo:dns_leak_protect injection markers (incl. the old
# geosite:gfw block written by older versions)
# - rewrite the ipv6: line from ipv6_value (skip when empty)
# No longer injects fallback-filter.geosite:gfw — mihomo deprecated it and a
# GeoSite.dat missing the gfw category froze startup (issue #25). See flush_dns.
# (The fallback-filter parsing vars below are now vestigial; clean up separately.)
dns_mihomo_apply_leak_dns_block() {
local cfg="$1" enabled="${2:-0}" ipv6_value="${3:-}" tmp="${1}.$$"
[ -f "$cfg" ] || return 0
awk -v enabled="$enabled" -v ipv6_value="$ipv6_value" '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
function spaces(n, out,i) {
out = ""
for (i = 0; i < n; i++)
out = out " "
return out
}
function flush_dns( i, sp, ff_sp) {
if (!in_dns)
return
if (in_ff) ff_end = dns_n
sp = spaces(child_indent > 0 ? child_indent : dns_indent + 2)
ff_sp = (ff_child_indent > 0 ? spaces(ff_child_indent) : sp " ")
print dns_lines[1]
if (ipv6_value != "")
print sp "ipv6: " ipv6_value
# NOTE: clashoo used to inject `fallback-filter.geosite: [gfw]` here, but
# mihomo deprecated fallback-filter.geosite ("replace with nameserver-policy,
# will be removed") AND it hard-fails preflight + freezes startup (~90s) when
# the downloaded GeoSite.dat lacks the gfw category (issue #25). Stop
# injecting it. DNS-leak protection still works via respect-rules / leak
# nameservers / DST-PORT,853,REJECT handled elsewhere. The marker strip below
# still removes any gfw block injected by older versions, so disabling /
# upgrading cleans it up.
for (i = 2; i <= dns_n; i++)
print dns_lines[i]
in_dns = 0
dns_n = 0
}
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect[[:space:]]*$/ {
in_marker = 1
next
}
in_marker {
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect[[:space:]]*$/)
in_marker = 0
next
}
/^dns:[[:space:]]*$/ {
flush_dns()
in_dns = 1
dns_indent = indent_len($0)
child_indent = -1
dns_n = 1
dns_lines[dns_n] = $0
has_ff = 0
in_ff = 0
in_ff_geosite = 0
ff_indent = -1
ff_child_indent = -1
ff_geosite_indent = -1
ff_has_gfw = 0
ff_end = -1
next
}
in_dns {
cur_indent = indent_len($0)
if ($0 ~ /^[^[:space:]#][^:]*:/ && cur_indent <= dns_indent) {
flush_dns()
print
next
}
if ($0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && cur_indent > dns_indent && child_indent < 0)
child_indent = cur_indent
if (in_ff && cur_indent <= ff_indent) {
ff_end = dns_n
in_ff = 0
in_ff_geosite = 0
}
if (in_ff_geosite && cur_indent <= ff_geosite_indent)
in_ff_geosite = 0
if ($0 ~ /^[[:space:]]*fallback-filter:[[:space:]]*$/) {
has_ff = 1
in_ff = 1
ff_indent = cur_indent
ff_child_indent = -1
} else if (in_ff && $0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && ff_child_indent < 0) {
ff_child_indent = cur_indent
}
if (in_ff && $0 ~ /^[[:space:]]*geosite:[[:space:]]*$/) {
in_ff_geosite = 1
ff_geosite_indent = cur_indent
} else if (in_ff_geosite && $0 ~ /^[[:space:]]*-[[:space:]]*gfw([[:space:]]*(#.*)?)?$/) {
ff_has_gfw = 1
}
if ($0 ~ /^[[:space:]]*ipv6:[[:space:]]*/)
next
dns_lines[++dns_n] = $0
next
}
{ print }
END { flush_dns() }
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
rm -f "$tmp" 2>/dev/null
}
# handle DST-PORT,853,REJECT injection into rules block
# only remove Clashoo marker block, never user original rules
dns_mihomo_apply_leak_rule() {
local cfg="$1" enabled="${2:-0}" tmp="${1}.$$" has_user_853
[ -f "$cfg" ] || return 0
has_user_853=$(awk '
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/ {
in_marker = 1
next
}
in_marker {
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/)
in_marker = 0
next
}
/^[[:space:]]*-[[:space:]]*DST-PORT,853,REJECT([[:space:]]*(#.*)?)?$/ { found = 1 }
END { print found ? 1 : 0 }
' "$cfg")
awk -v enabled="$enabled" -v has_user_853="$has_user_853" '
BEGIN { inserted = 0; saw_rules = 0 }
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/ {
in_marker = 1
next
}
in_marker {
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/)
in_marker = 0
next
}
/^rules:[[:space:]]*$/ {
saw_rules = 1
print
if (enabled == "1" && has_user_853 != "1") {
print " # >>> clashoo:dns_leak_protect_rule"
print " - DST-PORT,853,REJECT"
print " # <<< clashoo:dns_leak_protect_rule"
inserted = 1
}
next
}
{ print }
END {
if (enabled == "1" && has_user_853 != "1" && !inserted && !saw_rules) {
print ""
print "rules:"
print " # >>> clashoo:dns_leak_protect_rule"
print " - DST-PORT,853,REJECT"
print " # <<< clashoo:dns_leak_protect_rule"
}
}
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
rm -f "$tmp" 2>/dev/null
}
dns_mihomo_apply_leak_protect() {
local cfg="$1" enabled="${2:-0}" ipv6_value="${3:-}"
[ -f "$cfg" ] || return 0
dns_mihomo_apply_leak_nameservers "$cfg" "$enabled"
dns_mihomo_apply_respect_rules "$cfg" "$enabled"
dns_mihomo_apply_leak_dns_block "$cfg" "$enabled" "$ipv6_value"
dns_mihomo_apply_leak_rule "$cfg" "$enabled"
}
@@ -1,17 +0,0 @@
. as $document |
strenv(CLASHOO_DNS_OLD_PORT) as $old_port |
strenv(CLASHOO_DNS_HOST_PATTERN) as $host_pattern |
(strenv(CLASHOO_DNS_REPLACEMENT) | split(" ") | map(select(length > 0))) as $replacement |
("(?i)^(?:[a-z][a-z0-9+.-]*://)?(?:" + $host_pattern + "):" + $old_port + "(?:$|[/#?])") as $old_loopback |
(
$document |
select((.dns."proxy-server-nameserver" | type) == "!!seq") |
([.dns."proxy-server-nameserver"[] | select(type == "!!str" and test($old_loopback))] | length) as $match_count |
select($match_count > 0) |
.dns."proxy-server-nameserver" =
([
.dns."proxy-server-nameserver"[] |
select((type == "!!str" and test($old_loopback)) | not) |
select(. as $server | ($replacement | contains([$server])) | not)
] + $replacement)
) // $document
@@ -1,118 +0,0 @@
#!/usr/bin/ucode
'use strict';
import { readfile } from 'fs';
// Mirror node-domain DNS policies from the subscription into
// proxy-server-nameserver-policy (issue #73).
//
// Airports publish their own resolver for node domains, e.g.
// nameserver-policy:
// "+.chylink.xyz": [tls://112.74.48.57:10853]
// which answers with domestic relay IPs. mihomo resolves *proxy server* domains
// through proxy-server-nameserver(-policy) and ignores nameserver-policy there,
// so that resolver is never used and node domains fall back to public DNS -
// which may return overseas CDN IPs that some lines cannot reach at all, leaving
// every node (AnyTLS is TCP-only) timing out.
//
// Input : CLASHOO_NODE_DNS_SOURCE_FILE = path to {"policies": {...}, "servers": [...]}
// (CLASHOO_NODE_DNS_SOURCE with the JSON itself is still accepted)
// Output: JSON map with only the policy entries matching a node server domain
// (nothing is printed when there is no match)
let source = '';
let source_file = getenv('CLASHOO_NODE_DNS_SOURCE_FILE') || '';
if (length(source_file)) {
try {
source = readfile(source_file) || '';
}
catch (e) {
exit(0);
}
}
else {
source = getenv('CLASHOO_NODE_DNS_SOURCE') || '';
}
if (!length(source))
exit(0);
let parsed;
try {
parsed = json(source);
}
catch (e) {
exit(0);
}
if (type(parsed) != 'object')
exit(0);
let policies = parsed.policies;
let servers = parsed.servers;
if (type(policies) != 'object' || type(servers) != 'array')
exit(0);
let domains = [];
let seen = {};
for (let server in servers) {
if (type(server) != 'string' || !length(server))
continue;
server = lc(server);
if (seen[server])
continue;
seen[server] = true;
push(domains, server);
}
if (!length(domains))
exit(0);
// suffix match on a label boundary: node.example.com matches +.example.com
function domain_matches(key, domain) {
let key_len = length(key);
let domain_len = length(domain);
if (!key_len || domain_len < key_len)
return false;
if (domain == key)
return true;
if (substr(domain, domain_len - key_len) != key)
return false;
return substr(domain, domain_len - key_len - 1, 1) == '.';
}
let result = {};
for (let key, value in policies) {
if (type(key) != 'string' || !length(key))
continue;
let lowered = lc(key);
let is_pattern = match(lowered, /^[+*]?\./) != null;
let domain = replace(lowered, /^\+\./, '');
domain = replace(domain, /^\*\./, '');
domain = replace(domain, /^\./, '');
let matched = false;
for (let candidate in domains) {
if (is_pattern ? domain_matches(domain, candidate) : (candidate == lowered)) {
matched = true;
break;
}
}
if (matched)
result[key] = value;
}
if (!length(keys(result)))
exit(0);
print(sprintf('%J', result));
@@ -1,98 +0,0 @@
#!/bin/sh
. /lib/functions.sh
TMP_PREFIX="/tmp/clashoo_iprules.$$"
CUSTOM_RULE_FILE="${TMP_PREFIX}.ipadd.conf"
RULE="${TMP_PREFIX}.rules_conf.yaml"
CLASH="${TMP_PREFIX}.conf.yaml"
CONFIG_YAML="${CLASHOO_CONFIG_YAML:-/etc/clashoo/config.yaml}"
CLASH_CONFIG="${TMP_PREFIX}.config.yaml"
trap 'rm -f "$CUSTOM_RULE_FILE" "$RULE" "$CLASH" "$CLASH_CONFIG"' EXIT
[ -f "$CONFIG_YAML" ] || exit 0
. /usr/share/clashoo/runtime/primary_group.sh
# __PROXY__ placeholder -> primary proxy group (cached by RPC);
# fallback to GLOBAL (built-in mihomo group)
CACHED_PRIMARY_GROUP="$(uci -q get clashoo.config.primary_proxy_group)"
PRIMARY_GROUP="$(clashoo_resolve_primary_group "$CONFIG_YAML" "$CACHED_PRIMARY_GROUP")"
[ -n "$PRIMARY_GROUP" ] || PRIMARY_GROUP="GLOBAL"
append=$(uci get clashoo.config.append_rules 2>/dev/null)
if [ "${append:-0}" -eq 1 ];then
if [ -f $CLASH_CONFIG ];then
rm -rf $CLASH_CONFIG 2>/dev/null
fi
cp $CONFIG_YAML $CLASH_CONFIG 2>/dev/null
if [ ! -z "$(grep "^Rule:" "$CLASH_CONFIG")" ]; then
sed -i "/^Rule:/i\#RULESTART#" $CLASH_CONFIG 2>/dev/null
elif [ ! -z "$(grep "^rules:" "$CLASH_CONFIG")" ]; then
sed -i "/^rules:/i\#RULESTART#" $CLASH_CONFIG 2>/dev/null
fi
sed -i -e "\$a#RULEEND#" $CLASH_CONFIG 2>/dev/null
awk '/#RULESTART#/,/#RULEEND#/{print}' "$CLASH_CONFIG" 2>/dev/null |sed "s/\'//g" 2>/dev/null |sed 's/\"//g' 2>/dev/null |sed 's/\t/ /g' 2>/dev/null |grep '^ \{0,\}- '|awk -F '- ' '{print "- "$2}' | sed 's/^ \{0,\}//' 2>/dev/null |sed 's/ \{0,\}$//' 2>/dev/null >$RULE 2>&1
sed -i '/#RULESTART#/,/#RULEEND#/d' "$CLASH_CONFIG" 2>/dev/null
sed -i -e "\$a " $CLASH_CONFIG 2>/dev/null
sed -i "1i\rules:" $RULE 2>/dev/null
cat $CLASH_CONFIG $RULE >$CLASH 2>/dev/null
mv $CLASH $CONFIG_YAML 2>/dev/null
rm -f $RULE $CLASH_CONFIG 2>/dev/null
fi
# ===== custom routing rules (UCI: config addtype), decoupled =====
# fields: type(DOMAIN|SUFFIX|KEYWORD|IP-CIDR), ipaaddr, pgroup, res(no-resolve)
# pgroup: DIRECT or __PROXY__ (resolved at injection)
rm -f "$CUSTOM_RULE_FILE" 2>/dev/null
ipadd()
{
local section="$1"
config_get "pgroup" "$section" "pgroup" ""
config_get "ipaaddr" "$section" "ipaaddr" ""
config_get "type" "$section" "type" ""
config_get "res" "$section" "res" ""
[ -z "$type" ] && return
[ -z "$ipaaddr" ] && return
[ -z "$pgroup" ] && return
[ "$pgroup" = "__PROXY__" ] && pgroup="$PRIMARY_GROUP"
# quote whole rule (group name may contain spaces/emoji)
# so mixed indent with subscription rules does not break YAML parsing
if [ "${res}" = "1" ];then
echo "${RULE_INDENT}- \"$type,$ipaaddr,$pgroup,no-resolve\"" >> "$CUSTOM_RULE_FILE"
else
echo "${RULE_INDENT}- \"$type,$ipaaddr,$pgroup\"" >> "$CUSTOM_RULE_FILE"
fi
}
# detect existing rules indent for alignment (default 2)
RULE_INDENT="$(awk '/^[[:space:]]*rules:/{f=1;next} f&&/^[[:space:]]*-/{match($0,/^[[:space:]]*/);print substr($0,1,RLENGTH);exit}' "$CONFIG_YAML" 2>/dev/null)"
[ -n "$RULE_INDENT" ] || RULE_INDENT=" "
config_load "clashoo"
config_foreach ipadd "addtype"
# clean previous injection block (idempotent)
sed -i '/#CUSTOMRULESTART#/,/#CUSTOMRULEEND#/d' "$CONFIG_YAML" 2>/dev/null
if [ -f "$CUSTOM_RULE_FILE" ];then
sed -i -e "\$a#CUSTOMRULEEND#" "$CUSTOM_RULE_FILE" 2>/dev/null
if [ ! -z "$(grep "^ \{0,\}rules:" "$CONFIG_YAML")" ]; then
# 插在 rules: 段最前面,优先于订阅自带规则命中(纠错)
sed -i '/^ \{0,\}rules:/a\#CUSTOMRULESTART#' "$CONFIG_YAML" 2>/dev/null
else
echo "rules:" >> "$CONFIG_YAML" 2>/dev/null
echo "#CUSTOMRULESTART#" >> "$CONFIG_YAML" 2>/dev/null
fi
sed -i "/#CUSTOMRULESTART#/r${CUSTOM_RULE_FILE}" "$CONFIG_YAML" 2>/dev/null
fi
@@ -1,339 +0,0 @@
#!/usr/bin/ucode
'use strict';
import { cursor } from 'uci';
const uci = cursor();
function b(v) {
if (v == null || v == '' || v == '0' || v == 'false') return false;
if (v == '1' || v == 'true') return true;
return v;
}
function i(v, d) { let n = v != null ? int(v) : null; return n != null ? n : d; }
function s(v, d) { let t = trim(v || ''); return length(t) ? t : d; }
function a(k) { return uci.get('clashoo', 'config', k); }
function ab(k) { return b(a(k)); }
const cfg = {};
/* ── 端口与基本设置 ─────────────────────────────────────── */
let v;
v = i(a('http_port')); if (v != null) cfg['port'] = v;
v = i(a('socks_port')); if (v != null) cfg['socks-port'] = v;
v = i(a('redir_port')); if (v != null) cfg['redir-port'] = v;
v = i(a('mixed_port')); if (v != null) cfg['mixed-port'] = v;
v = i(a('tproxy_port')); if (v != null) cfg['tproxy-port'] = v;
v = a('bind_addr');
if (!v || v == '*') v = null;
if (v) cfg['bind-address'] = v;
v = a('p_mode');
if (v == 'script') v = 'rule';
if (v) cfg['mode'] = v;
cfg['log-level'] = s(a('level'), 'info');
cfg['allow-lan'] = ab('allow_lan');
cfg['ipv6'] = ab('enable_ipv6');
cfg['routing-mark'] = 6666;
cfg['external-controller'] = '0.0.0.0:' + i(a('dash_port'), 9090);
cfg['external-ui'] = './dashboard';
cfg['secret'] = s(a('dash_pass'), '');
v = a('interf');
if (v && v != '0') cfg['interface-name'] = v;
/* ── TUN ───────────────────────────────────────────────── */
let tun_mode = i(a('tun_mode'), 0);
let tcp_mode = s(a('tcp_mode'), 'redirect');
let udp_mode = s(a('udp_mode'), 'tproxy');
let tun_enabled = (tun_mode == 1 || tcp_mode == 'tun' || udp_mode == 'tun');
let acl_enabled = false;
uci.foreach('clashoo', 'lan_acl', function(sec) {
if (b(sec.enabled) != false) acl_enabled = true;
});
if (!ab('acl_migrated')) {
let access_control = i(a('access_control'), 0);
acl_enabled = access_control == 1 || access_control == 2;
}
let tun_acl = tun_enabled && acl_enabled;
cfg['tun'] = {
enable: tun_enabled,
stack: s(a('stack'), 'gvisor'),
'auto-route': true,
'auto-redirect': !tun_acl,
'auto-detect-interface': true,
};
let tun_mtu = a('tun_mtu');
if (tun_mtu != null) cfg['tun']['mtu'] = int(tun_mtu);
if (ab('tun_gso')) {
cfg['tun']['gso'] = true;
cfg['tun']['gso-max-size'] = i(a('tun_gso_max_size'), 65536);
}
if (ab('tun_dns_hijack')) {
cfg['tun']['dns-hijack'] = ['any:53', 'tcp://any:53'];
}
if (!tun_enabled) {
cfg['tun']['enable'] = false;
}
/* ── DNS ───────────────────────────────────────────────── */
let dns_port = i(a('listen_port'), 1053);
cfg['dns'] = {
enable: b(a('enable_dns')) != false,
listen: '0.0.0.0:' + dns_port,
'enhanced-mode': s(a('enhanced_mode'), 'fake-ip'),
'fake-ip-range': s(a('fake_ip_range'), '198.18.0.1/16'),
'fake-ip-filter': [],
ipv6: ab('enable_ipv6'),
};
if (cfg['dns']['enhanced-mode'] == 'fake-ip' && ab('enable_ipv6'))
cfg['dns']['fake-ip-range6'] = s(a('fake_ip_range6'), 'fc00::/18');
/* fake-ip-filter-mode: blacklist (default) | whitelist | rule */
let filter_mode = s(a('fake_ip_filter_mode'), 'blacklist');
if (filter_mode != 'blacklist') cfg['dns']['fake-ip-filter-mode'] = filter_mode;
/* map geosite:cn to bundled cn.mrs rule-set so mihomo skips the 10MB geosite.dat */
let need_cn_rs = false;
let keep_fakeip = length(s(getenv('CLASHOO_KEEP_FAKEIP_FILTER'), ''));
if (keep_fakeip) {
delete cfg['dns']['fake-ip-filter'];
} else {
let push_filter = function(f) {
f = trim(f || '');
if (!length(f)) return;
if (filter_mode == 'rule') {
if (match(f, /,(fake-ip|real-ip)$/)) { push(cfg['dns']['fake-ip-filter'], f); return; }
if (index(f, ',') >= 0) { push(cfg['dns']['fake-ip-filter'], f + ',real-ip'); return; }
if (f == 'geosite:cn') { f = 'RULE-SET,cn_domain,real-ip'; need_cn_rs = true; }
else if (match(f, /^geosite:/)) { f = 'GEOSITE,' + substr(f, 8) + ',real-ip'; }
else if (match(f, /^rule-set:/)) {
let rs = substr(f, 9);
if (rs == 'cn_domain') need_cn_rs = true;
f = 'RULE-SET,' + rs + ',real-ip';
}
else if (match(f, /^\+\./)) { f = 'DOMAIN-SUFFIX,' + substr(f, 2) + ',real-ip'; }
else if (match(f, /\*/)) { f = 'DOMAIN-WILDCARD,' + f + ',real-ip'; }
else { f = 'DOMAIN,' + f + ',real-ip'; }
} else {
if (f == 'geosite:cn') { f = 'rule-set:cn_domain'; need_cn_rs = true; }
else if (f == 'rule-set:cn_domain') need_cn_rs = true;
}
push(cfg['dns']['fake-ip-filter'], f);
};
let filters = a('fake_ip_filter');
if (type(filters) == 'array') { for (let f in filters) push_filter(f); }
else if (filters != null) push_filter(filters);
}
/* fallback-filter(默认 geoip:false,防止冷启动依赖 MMDB) */
cfg['dns']['fallback-filter'] = { geoip: ab('fallback_filter_geoip') };
let dns_present = {};
for (let f in split(s(getenv('CLASHOO_DNS_PRESENT'), ''), ','))
if (length(trim(f))) dns_present[trim(f)] = true;
let dns_force = {};
for (let f in split(s(getenv('CLASHOO_DNS_FORCE'), ''), ','))
if (length(trim(f))) dns_force[trim(f)] = true;
let dns_inherit_mode = s(getenv('CLASHOO_DNS_INHERIT_MODE'), '');
let inherited_nameserver = json(s(getenv('CLASHOO_DNS_INHERIT_NAMESERVER'), '[]'));
if (type(inherited_nameserver) != 'array') {
if (inherited_nameserver != null && inherited_nameserver != '')
inherited_nameserver = [inherited_nameserver];
else
inherited_nameserver = [];
}
let dns_role_fields = ['nameserver', 'proxy-server-nameserver', 'direct-nameserver',
'default-nameserver', 'nameserver-policy', 'proxy-server-nameserver-policy', 'respect-rules'];
let had_user_dns_roles = false;
for (let f in dns_role_fields)
if (dns_present[f]) had_user_dns_roles = true;
if (length(keys(dns_force)) || !had_user_dns_roles)
cfg['dns']['x-clashoo-managed-dns'] = true;
function dns_scheme(protocol) {
switch (trim(protocol || '')) {
case '':
case 'none': return '';
case 'udp': case 'udp://': return 'udp://';
case 'tcp': case 'tcp://': return 'tcp://';
case 'dot': case 'tls': case 'tls://': return 'tls://';
case 'doh': case 'https': case 'https://': return 'https://';
case 'doq': case 'quic': case 'quic://': return 'quic://';
default: return protocol;
}
}
function dns_server(address, protocol, port) {
let addr = trim(address || '');
if (!length(addr)) return null;
if (match(addr, /^[A-Za-z][A-Za-z0-9+.-]*:\/\//)) return addr;
let prefix = dns_scheme(protocol);
return length(trim(port || '')) ? prefix + addr + ':' + trim(port) : prefix + addr;
}
let dns_roles = {};
uci.foreach('clashoo', 'dnsservers', function(sec) {
if (b(sec.enabled) == false) return;
let role = s(sec.ser_type, 'nameserver');
if (role == 'fallback' && cfg['dns']['enhanced-mode'] == 'fake-ip') return;
let srv = dns_server(sec.ser_address, sec.protocol, sec.ser_port);
if (!srv) return;
if (!dns_roles[role]) dns_roles[role] = [];
push(dns_roles[role], srv);
});
for (let role in keys(dns_roles)) {
if (role == 'proxy-server-nameserver' && (dns_inherit_mode == 'inherit' || dns_inherit_mode == 'skip') &&
!dns_present[role] && !dns_force[role]) continue;
if (!dns_present[role] || dns_force[role]) cfg['dns'][role] = dns_roles[role];
}
if (dns_inherit_mode == 'inherit' && length(inherited_nameserver) &&
!dns_present['proxy-server-nameserver'] && !dns_force['proxy-server-nameserver'])
cfg['dns']['proxy-server-nameserver'] = inherited_nameserver;
let bootstrap = a('default_nameserver');
if (bootstrap == null) bootstrap = a('defaul_nameserver');
if (type(bootstrap) != 'array') bootstrap = bootstrap != null ? [bootstrap] : [];
if (length(bootstrap) && (!dns_present['default-nameserver'] || dns_force['default-nameserver']))
cfg['dns']['default-nameserver'] = bootstrap;
let policies = {};
let proxy_policies = {};
uci.foreach('clashoo', 'dns_policy', function(sec) {
if (b(sec.enabled) == false) return;
let role = s(sec.policy_type, 'nameserver-policy');
if (role != 'nameserver-policy' && role != 'proxy-server-nameserver-policy') return;
let matcher = trim(sec.matcher || '');
let servers = sec.nameserver;
if (type(servers) != 'array') servers = servers != null ? [servers] : [];
if (!length(matcher) || !length(servers)) return;
if (matcher == 'geosite:cn') { matcher = 'rule-set:cn_domain'; need_cn_rs = true; }
if (role == 'nameserver-policy') policies[matcher] = servers;
else proxy_policies[matcher] = servers;
});
if (length(keys(policies)) && (!dns_present['nameserver-policy'] || dns_force['nameserver-policy']))
cfg['dns']['nameserver-policy'] = policies;
if (length(keys(proxy_policies)) && (!dns_present['proxy-server-nameserver-policy'] || dns_force['proxy-server-nameserver-policy']))
cfg['dns']['proxy-server-nameserver-policy'] = proxy_policies;
let respect_rules = a('dns_respect_rules') == null ? true : ab('dns_respect_rules');
if (dns_force['respect-rules'])
cfg['dns']['respect-rules'] = respect_rules;
else if (respect_rules && length(cfg['dns']['proxy-server-nameserver'] || [])
&& !dns_present['respect-rules'] && !dns_present['prefer-h3'])
cfg['dns']['respect-rules'] = true;
/* profile */
let store_selected = ab('selection_cache');
/* fake-ip cache defaults ON to survive restarts; opt out with '0' */
let store_fake = a('fake_ip_cache') == null ? true : ab('fake_ip_cache');
if (store_selected || store_fake) {
cfg['profile'] = {};
if (store_selected) cfg['profile']['store-selected'] = true;
if (store_fake) cfg['profile']['store-fake-ip'] = true;
}
/* bundled cn.mrs rule-provider, referenced by fake-ip-filter rule-set:cn_domain */
if (need_cn_rs) {
if (!cfg['rule-providers']) cfg['rule-providers'] = {};
cfg['rule-providers']['cn_domain'] = {
type: 'file',
path: './ruleset/cn.mrs',
format: 'mrs',
behavior: 'domain',
};
}
/* ── authentication ────────────────────────────────── */
if (ab('authentication')) {
cfg['authentication'] = [];
uci.foreach('clashoo', 'authentication', function(sec) {
if (b(sec.enabled) == false) return;
let user = trim(sec.username || '');
let pass = trim(sec.password || '');
if (user) push(cfg['authentication'], user + ':' + pass);
});
}
/* ── hosts ────────────────────────────────────────── */
cfg['hosts'] = {};
uci.foreach('clashoo', 'hosts', function(sec) {
if (b(sec.enabled) == false) return;
let domain = trim(sec.adress || '');
let ip = trim(sec.ip || '');
if (domain && ip) cfg['hosts'][domain] = ip;
});
if (length(keys(cfg['hosts'])) == 0) delete cfg['hosts'];
/* ── sniffer ──────────────────────────────────────── */
if (ab('sniffer_streaming') && !length(s(getenv('CLASHOO_KEEP_SNIFFER'), ''))) {
cfg['sniffer'] = {
enable: true,
'force-dns-mapping': true,
'parse-pure-ip': true,
sniff: {
HTTP: { ports: [80, 8080], 'override-destination': true },
TLS: { ports: [443, 8443], 'override-destination': true },
QUIC: { ports: [443, 8443], 'override-destination': true },
},
'force-domain': [
'+.youtube.com',
'+.googlevideo.com',
'+.netflix.com',
'+.nflxvideo.net',
'+.disneyplus.com',
'+.hulu.com',
'+.hbomax.com',
],
'skip-domain': [
'+.lan',
'+.local',
],
};
}
/* ── Smart kernel injection ───────────────────────── */
if (ab('smart_auto_switch')) {
/* Smart 策略注入:通过 proxy-groups 覆盖实现 */
}
/* ── ECS ──────────────────────────────────────────── */
let ecs = s(a('dns_ecs'));
if (ecs) cfg['dns']['edns-client-subnet'] = ecs;
/* ── cache_file / experimental ──────────────────── */
cfg['experimental'] = {
'cache_file': { enabled: true },
'clash_api': {
'external-controller': '0.0.0.0:' + i(a('dash_port'), 9090),
'external-ui': './dashboard',
'secret': s(a('dash_pass'), ''),
},
};
/* ── 输出 JSON (不含 null/空对象) ─────────────────── */
function clean(obj) {
if (type(obj) == 'array') {
let r = [];
for (let e in obj) {
e = clean(e);
if (e != null) push(r, e);
}
return length(r) ? r : null;
}
if (type(obj) == 'object') {
let r = {};
for (let k in keys(obj)) {
let v = clean(obj[k]);
if (v != null) r[k] = v;
}
return length(keys(r)) ? r : null;
}
return obj;
}
print(clean(cfg));
@@ -1,49 +0,0 @@
#!/bin/sh
clashoo_resolve_primary_group() {
local config_yaml="$1"
local cached_group="$2"
local group_rows
local resolved_group
if [ "$cached_group" = "GLOBAL" ]; then
printf '%s\n' "GLOBAL"
return 0
fi
if [ ! -f "$config_yaml" ] || ! command -v yq >/dev/null 2>&1; then
printf '%s\n' "GLOBAL"
return 0
fi
group_rows="$(yq -M e -r '(."proxy-groups" // [])[] | [.name, .type] | @tsv' "$config_yaml" 2>/dev/null)" || {
printf '%s\n' "GLOBAL"
return 0
}
if [ -n "$cached_group" ] && printf '%s\n' "$group_rows" |
awk -F '\t' -v wanted="$cached_group" '$1 == wanted { found = 1; exit } END { exit !found }'
then
printf '%s\n' "$cached_group"
return 0
fi
resolved_group="$(printf '%s\n' "$group_rows" | awk -F '\t' '
tolower($2) == "select" &&
(index($1, "节点选择") || index($1, "代理") || index($1, "🚀") || tolower($1) ~ /proxy/) {
print $1
exit
}
')"
if [ -z "$resolved_group" ]; then
resolved_group="$(printf '%s\n' "$group_rows" | awk -F '\t' '
tolower($2) == "select" {
print $1
exit
}
')"
fi
printf '%s\n' "${resolved_group:-GLOBAL}"
}
@@ -1,9 +0,0 @@
# If every configured rule points to a proxy without UDP support, mihomo falls
# through to DIRECT. Reject only that otherwise-unmatched QUIC traffic so the
# client can retry over TCP; rules with a usable UDP target still win first.
with(
select(has("rules")) |
select((.rules | type) == "!!seq") |
select(([.rules[] | select(. == "AND,((NETWORK,UDP),(DST-PORT,443)),REJECT")] | length) == 0);
.rules += ["AND,((NETWORK,UDP),(DST-PORT,443)),REJECT"]
)
@@ -1,37 +0,0 @@
#!/bin/sh
if [ -f /usr/share/clashbackup/history ];then
HISTORY_PATH="/usr/share/clashbackup/history"
SECRET=$(uci get clashoo.config.dash_pass 2>/dev/null)
LAN_IP=$(uci get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null)
PORT=$(uci get clashoo.config.dash_port 2>/dev/null)
urlencode() {
if [ "$#" != 1 ]; then
return 1
fi
printf '%s' "$1" | hexdump -v -e '1/1 "%u "' | awk '
{
for (i = 1; i <= NF; i++) {
b = $i + 0
if ((b >= 48 && b <= 57) || (b >= 65 && b <= 90) ||
(b >= 97 && b <= 122) || b == 45 || b == 46 ||
b == 95 || b == 126)
printf "%c", b
else
printf "%%%02X", b
}
}'
}
cat $HISTORY_PATH |while read line
do
if [ -z "$(echo $line |grep "#*#")" ]; then
continue
else
GORUP_NAME=$(urlencode "$(echo $line |awk -F '#*#' '{print $1}')")
NOW_NAME=$(echo $line |awk -F '#*#' '{print $3}')
curl -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X PUT -d '{"name":"'"$NOW_NAME"'"}' http://"$LAN_IP":"$PORT"/proxies/"$GORUP_NAME" >/dev/null 2>&1
fi
done >/dev/null 2>&1
curl -m 5 --retry 2 -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X DELETE http://"$LAN_IP":"$PORT"/connections >/dev/null 2>&1
fi
@@ -1,28 +0,0 @@
# Preserve every explicit user choice while restoring missing per-protocol defaults.
with(
.sniffer |
select((has("enable") | not) or (.enable == true)) |
select((has("override-destination") | not) or (."override-destination" == true));
with(
select(has("force-dns-mapping") | not);
."force-dns-mapping" = true
) |
with(
select(has("parse-pure-ip") | not);
."parse-pure-ip" = true
) |
with(
.sniff | select(has("TLS"));
with(
.TLS | select(type == "!!map" and (has("override-destination") | not));
."override-destination" = true
)
) |
with(
.sniff | select(has("QUIC"));
with(
.QUIC | select(type == "!!map" and (has("override-destination") | not));
."override-destination" = true
)
)
)
@@ -1,63 +0,0 @@
time.windows.com
time.nist.gov
time.apple.com
time.asia.apple.com
cn.ntp.org.cn
edu.ntp.org.cn
hk.ntp.org.cn
tw.ntp.org.cn
us.ntp.org.cn
sgp.ntp.org.cn
kr.ntp.org.cn
jp.ntp.org.cn
de.ntp.org.cn
ina.ntp.org.cn
0.openwrt.pool.ntp.org
1.openwrt.pool.ntp.org
2.openwrt.pool.ntp.org
3.openwrt.pool.ntp.org
ntp.aliyun.com
ntp1.aliyun.com
ntp2.aliyun.com
ntp3.aliyun.com
ntp4.aliyun.com
ntp5.aliyun.com
ntp6.aliyun.com
ntp7.aliyun.com
time1.aliyun.com
time2.aliyun.com
time3.aliyun.com
time4.aliyun.com
time5.aliyun.com
time6.aliyun.com
time7.aliyun.com
s1c.time.edu.cn
s2m.time.edu.cn
s1b.time.edu.cn
s1e.time.edu.cn
s2a.time.edu.cn
s2b.time.edu.cn
time1.apple.com
time2.apple.com
time3.apple.com
time4.apple.com
time5.apple.com
time6.apple.com
time7.apple.com
time1.google.com
time2.google.com
time3.google.com
time4.google.com
.music.163.com
.music.126.net
man.netease.com
api.iplay.163.com
ac.dun.163yun.com
mr.da.netease.com
crash.163.com
imap.163.com
msftconnecttest.com
msftncsi.com
captive.qq.com
ssl.ptlogin2.qq.com
login.qq.com
@@ -1,649 +0,0 @@
#!/bin/sh
LOG_FILE="/tmp/clashoo_component_update.log"
RUN_FILE="/var/run/clashoo_component_update"
STATE_FILE="/tmp/clashoo_component_update_state"
TMP_DIR="/tmp/clashoo-component-update"
APK_UPGRADE_FLAG="/var/run/clashoo_package_upgrade"
FEED_BASE_URL="https://down.dllkids.xyz/openwrt-feed/clashoo"
GITHUB_API_URL="https://api.github.com/repos/kenzok8/openwrt-clashoo/releases/latest"
ENV_GITHUB_PROXY_PREFIX="${GITHUB_PROXY_PREFIX:-}"
UCI_GITHUB_PROXY_PREFIX="$(uci -q get clashoo.config.core_mirror_prefix 2>/dev/null)"
GITHUB_PROXY_PREFIX="${UCI_GITHUB_PROXY_PREFIX:-${ENV_GITHUB_PROXY_PREFIX:-https://ghfast.top/}}"
case "$GITHUB_PROXY_PREFIX" in
*/) ;;
*) GITHUB_PROXY_PREFIX="${GITHUB_PROXY_PREFIX}/" ;;
esac
CONNECT_TIMEOUT="${CONNECT_TIMEOUT:-8}"
REQUEST_TIMEOUT="${REQUEST_TIMEOUT:-20}"
LOW_SPEED_TIME="${LOW_SPEED_TIME:-30}"
LOW_SPEED_LIMIT="${LOW_SPEED_LIMIT:-1024}"
trap 'rm -f "$APK_UPGRADE_FLAG"' EXIT
log() {
mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$STATE_FILE")" >/dev/null 2>&1
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$LOG_FILE"
printf 'component=%s\nstatus=%s\nmessage=%s\n' "${COMPONENT:-}" "${STATUS:-running}" "$*" >"$STATE_FILE"
}
finish() {
rc="$1"
msg="$2"
STATUS="success"
[ "$rc" -eq 0 ] || STATUS="failed"
log "$msg"
rm -f "$RUN_FILE" "$APK_UPGRADE_FLAG"
exit "$rc"
}
# In kernel-only mode there is no transparent proxy, so component downloads
# would go out direct and stall behind the GFW. Route them through the running
# core (shared logic in proxy_lib.sh). Normal mode returns empty -> TPROXY.
# Guard the source: on a stale install missing proxy_lib.sh, an unguarded `.`
# under `set -e` kills the whole update silently (empty log, nothing happens).
# Fall back to no proxy detection (direct / TPROXY) instead of dying.
if [ -f /usr/share/clashoo/update/proxy_lib.sh ]; then
. /usr/share/clashoo/update/proxy_lib.sh
else
clashoo_detect_proxy() { :; }
fi
detect_proxy() { clashoo_detect_proxy; }
fetch_text() {
url="$1"
proxy="$(detect_proxy)"
if command -v curl >/dev/null 2>&1; then
curl -fsSL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$REQUEST_TIMEOUT" ${proxy:+--proxy "$proxy"} "$url"
return $?
fi
if command -v wget >/dev/null 2>&1; then
[ -n "$proxy" ] && { http_proxy="$proxy" https_proxy="$proxy" wget -qO- --timeout="$REQUEST_TIMEOUT" "$url"; return $?; }
wget -qO- --timeout="$REQUEST_TIMEOUT" "$url"
return $?
fi
return 127
}
download_file() {
url="$1"
out="$2"
proxy="$(detect_proxy)"
if command -v curl >/dev/null 2>&1; then
curl -fL --connect-timeout "$CONNECT_TIMEOUT" --speed-time "$LOW_SPEED_TIME" --speed-limit "$LOW_SPEED_LIMIT" ${proxy:+--proxy "$proxy"} "$url" -o "$out"
return $?
fi
if [ -n "$proxy" ]; then
http_proxy="$proxy" https_proxy="$proxy" wget -qO "$out" --timeout="$REQUEST_TIMEOUT" "$url"
return $?
fi
wget -qO "$out" --timeout="$REQUEST_TIMEOUT" "$url"
}
download_url() {
url="$1"
case "$url" in
https://github.com/*)
printf '%s%s\n' "$GITHUB_PROXY_PREFIX" "$url"
;;
*)
printf '%s\n' "$url"
;;
esac
}
root_free_kb() {
df -k / 2>/dev/null | awk 'NR==2 {print $4}'
}
kb_to_mib() {
awk "BEGIN {printf \"%.0f\", (${1:-0}) / 1024}"
}
ensure_root_space() {
need_kb="$1"
free_kb="$(root_free_kb)"
[ -n "$free_kb" ] || return 0
[ "$free_kb" -ge "$need_kb" ] && return 0
finish 1 "根分区空间不足:可用 $(kb_to_mib "$free_kb") MiB,至少需要 $(kb_to_mib "$need_kb") MiB"
}
log_install_error() {
file="$1"
[ -s "$file" ] || return 0
tail -8 "$file" 2>/dev/null | while IFS= read -r line; do
[ -n "$line" ] && log "$line"
done
}
detect_manager() {
if command -v opkg >/dev/null 2>&1; then
echo opkg
return
fi
if command -v apk >/dev/null 2>&1; then
echo apk
return
fi
echo unsupported
}
installed_package_version() {
pkg="$1"
if command -v opkg >/dev/null 2>&1; then
opkg status "$pkg" 2>/dev/null | awk -F': ' '/^Version:/{print $2; exit}'
return
fi
if command -v apk >/dev/null 2>&1; then
apk list -I "$pkg" 2>/dev/null | awk '{print $1; exit}' | sed -n "s/^${pkg}-//p"
fi
}
package_installed() {
pkg="$1"
if command -v opkg >/dev/null 2>&1; then
opkg status "$pkg" 2>/dev/null | awk '
$1 == "Status:" && $2 == "install" && $NF == "installed" { found = 1 }
END { exit !found }
'
return
fi
apk info -e "$pkg" >/dev/null 2>&1
}
apk_install() {
proxy="$(detect_proxy)"
if [ -n "$proxy" ]; then
http_proxy="$proxy" https_proxy="$proxy" apk add --allow-untrusted --timeout 30 "$@"
else
apk add --allow-untrusted --timeout 30 "$@"
fi
}
apk_bundle_install() {
printf '%s\n' "$$" >"$APK_UPGRADE_FLAG"
apk_install "$@"
rc=$?
rm -f "$APK_UPGRADE_FLAG"
return "$rc"
}
verify_downloaded_package() {
file="$1"
[ -s "$file" ] || return 1
if [ "$PM" = "apk" ]; then
apk verify --allow-untrusted "$file" >/dev/null 2>&1
return
fi
gzip -t "$file" >/dev/null 2>&1
}
download_package_file() {
url="$1"
out="$2"
name="$3"
download_file "$(download_url "$url")" "$out" || finish 1 "下载 ${name} 失败"
verify_downloaded_package "$out" || finish 1 "${name} 文件校验失败"
}
detect_arch() {
pm="$1"
if [ "$pm" = "opkg" ]; then
opkg print-architecture 2>/dev/null | awk '/^arch / {print $2}' | tail -n 1
return
fi
# apk: DISTRIB_ARCH is authoritative (e.g. aarch64_cortex-a53); apk --print-arch
# only gives the generic "aarch64" which won't match the .apk package suffix.
if [ -r /etc/openwrt_release ]; then
a="$(sed -n "s/^DISTRIB_ARCH=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)"
[ -n "$a" ] && { printf '%s\n' "$a"; return; }
fi
apk --print-arch 2>/dev/null
}
detect_sdk() {
[ -r /etc/openwrt_release ] || return 1
release="$(sed -n "s/^DISTRIB_RELEASE=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)"
[ -n "$release" ] || return 1
printf '%s\n' "$release" | grep -Eo '[0-9]+\.[0-9]+' | head -n 1
}
append_unique_word() {
value="$1"
list="$2"
[ -n "$value" ] || {
printf '%s\n' "$list"
return
}
case " $list " in
*" $value "*) ;;
*) list="${list}${list:+ }${value}" ;;
esac
printf '%s\n' "$list"
}
build_sdk_candidates() {
pm="$1"
candidates=""
detected_sdk="$(detect_sdk || true)"
candidates="$(append_unique_word "$detected_sdk" "$candidates")"
if [ "$pm" = "opkg" ]; then
for sdk in 24.10 23.05 22.03 21.02; do
candidates="$(append_unique_word "$sdk" "$candidates")"
done
else
for sdk in 25.12 24.10; do
candidates="$(append_unique_word "$sdk" "$candidates")"
done
fi
printf '%s\n' "$candidates"
}
find_manifest_value() {
key="$1"
manifest_text="$2"
printf '%s\n' "$manifest_text" | sed -n "s/^${key}=//p" | head -n 1
}
load_manifest_urls() {
sdk="$1"
arch="$2"
manifest_url="${FEED_BASE_URL}/${sdk}/${arch}/manifest-clashoo.txt"
manifest_text="$(fetch_text "$manifest_url" || true)"
[ -n "$manifest_text" ] || return 1
core_file="$(find_manifest_value "core" "$manifest_text")"
luci_file="$(find_manifest_value "luci" "$manifest_text")"
i18n_file="$(find_manifest_value "i18n" "$manifest_text")"
[ -n "$core_file" ] || return 1
[ -n "$luci_file" ] || return 1
case "$core_file" in *."$EXT") ;; *) return 1 ;; esac
case "$luci_file" in *."$EXT") ;; *) return 1 ;; esac
case "$i18n_file" in ""|*."$EXT") ;; *) return 1 ;; esac
CORE_URL="${FEED_BASE_URL}/${sdk}/${arch}/${core_file}"
LUCI_URL="${FEED_BASE_URL}/${sdk}/${arch}/${luci_file}"
I18N_URL=""
[ -n "$i18n_file" ] && I18N_URL="${FEED_BASE_URL}/${sdk}/${arch}/${i18n_file}"
SOURCE_LABEL="R2 feed ${sdk}/${arch}"
return 0
}
load_github_urls() {
arch="$1"
ext="$2"
payload="$(fetch_text "$GITHUB_API_URL" || true)"
[ -n "$payload" ] || return 1
urls="$(printf '%s\n' "$payload" | sed -n 's/.*"browser_download_url":[[:space:]]*"\([^"]*\)".*/\1/p')"
[ -n "$urls" ] || return 1
if [ "$ext" = "apk" ]; then
CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo-[^-]+.*-r[0-9]+-'"$arch"'\.apk$' | head -n 1)"
LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)"
I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)"
else
CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo_.*_'"$arch"'\.ipk$' | head -n 1)"
LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo_.*_all\.ipk$' | head -n 1)"
I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn_.*_all\.ipk$' | head -n 1)"
fi
[ -n "$CORE_URL" ] || return 1
[ -n "$LUCI_URL" ] || return 1
SOURCE_LABEL="GitHub latest"
return 0
}
# `~` 是版本里的预发布分隔符,而 release 资产文件名会把它换成 `.`
# (clashoo_2026.10.25.6780eee-r3_x86_64.ipk 对应已安装的 2026.10.25~6780eee-r3)。
# 只比分隔符就会误判成「校验失败」,所以比较前统一归一化,解析时保持文件名原样。
version_equal() {
[ -n "$1" ] && [ "$(printf '%s' "$1" | tr '~' '.')" = "$(printf '%s' "$2" | tr '~' '.')" ]
}
package_version_from_url() {
file="${1##*/}"
case "$file" in
clashoo_*_"$ARCH".ipk)
v="${file#clashoo_}"
printf '%s\n' "${v%_${ARCH}.ipk}"
;;
# 架构名本身可能带下划线(aarch64_generic / arm_cortex-a7_neon-vfpv4),
# 只截到版本号为止,否则尾巴会混进版本串。
clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^clashoo_\([^_]*\)_.*\.ipk$/\1/p' ;;
luci-app-clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^luci-app-clashoo_\(.*\)_all\.ipk$/\1/p' ;;
luci-i18n-clashoo-zh-cn_*.ipk) printf '%s\n' "$file" | sed -n 's/^luci-i18n-clashoo-zh-cn_\(.*\)_all\.ipk$/\1/p' ;;
clashoo-*.apk)
printf '%s\n' "$file" | sed -n -e 's/^clashoo-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^clashoo-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1
;;
luci-app-clashoo-*.apk) printf '%s\n' "$file" | sed -n -e 's/^luci-app-clashoo-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^luci-app-clashoo-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1 ;;
luci-i18n-clashoo-zh-cn-*.apk) printf '%s\n' "$file" | sed -n -e 's/^luci-i18n-clashoo-zh-cn-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^luci-i18n-clashoo-zh-cn-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1 ;;
*) printf '%s\n' "$file" ;;
esac
}
resolve_bundle_urls() {
PM="$(detect_manager)"
[ "$PM" != "unsupported" ] || return 1
ARCH="$(detect_arch "$PM")"
[ -n "$ARCH" ] || return 1
EXT="ipk"
[ "$PM" = "apk" ] && EXT="apk"
CORE_URL=""
LUCI_URL=""
I18N_URL=""
SOURCE_LABEL=""
SDK_CANDIDATES="$(build_sdk_candidates "$PM")"
for sdk in $SDK_CANDIDATES; do
if load_manifest_urls "$sdk" "$ARCH"; then
return 0
fi
done
load_github_urls "$ARCH" "$EXT"
}
backup_config() {
ts="$(date '+%Y%m%d-%H%M%S')"
BACKUP_DIR="/etc/clashoo/backup/component-upgrade-${ts}"
mkdir -p "$BACKUP_DIR/etc-config" "$BACKUP_DIR/etc-clashoo" >/dev/null 2>&1 || return 1
[ -r /etc/config/clashoo ] && cp -a /etc/config/clashoo "$BACKUP_DIR/etc-config/clashoo"
# 只备份用户配置;Model.bin 等可重新下载的大文件不入备份,避免每次更新
# 复制 ~5MB 的同一模型,把备份目录撑大、堆满本就紧张的根分区。
for path in \
/etc/clashoo/config.yaml \
/etc/clashoo/config.json \
/etc/clashoo/*.yaml \
/etc/clashoo/*.yml \
/etc/clashoo/*.json
do
[ -e "$path" ] || continue
cp -a "$path" "$BACKUP_DIR/etc-clashoo/" 2>/dev/null || true
done
log "已备份配置:${BACKUP_DIR}"
# 只保留最近 1 个备份,删除更早的,避免无限堆积占满磁盘。
ls -dt /etc/clashoo/backup/component-upgrade-* 2>/dev/null | tail -n +2 | while read -r _old; do
[ -n "$_old" ] && rm -rf "$_old" 2>/dev/null
done
}
restore_config_backup() {
[ -n "${BACKUP_DIR:-}" ] || return 0
[ -r "$BACKUP_DIR/etc-config/clashoo" ] && cp -a "$BACKUP_DIR/etc-config/clashoo" /etc/config/clashoo
if [ -d "$BACKUP_DIR/etc-clashoo" ]; then
mkdir -p /etc/clashoo
cp -a "$BACKUP_DIR/etc-clashoo/." /etc/clashoo/ 2>/dev/null || true
fi
uci commit clashoo >/dev/null 2>&1 || true
}
clashoo_was_running() {
/etc/init.d/clashoo status >/dev/null 2>&1 && return 0
return 1
}
restart_web_stack() {
# reload (not restart) rpcd so the LuCI login session survives a plugin
# update — rpcd restart drops every ubus session and forces a re-login
# (issue #12). reload still picks up the new ucode RPC backend. uhttpd needs
# nothing: updated static assets are served on the next request.
/etc/init.d/rpcd reload >/dev/null 2>&1 || true
}
# which: clashoo(仅核心)/ luci(luci-app + 语言包)。两者拆开,clashoo
# 核心更新频率高于 LuCI,分别更新便于定位失败。
run_pkg_update() {
which="$1"
log "正在检查组件包"
if ! resolve_bundle_urls; then
finish 1 "未找到适配当前设备的组件包"
fi
log "更新来源:${SOURCE_LABEL}"
core_ver="$(package_version_from_url "$CORE_URL")"
luci_ver="$(package_version_from_url "$LUCI_URL")"
i18n_ver=""
[ -z "$I18N_URL" ] || i18n_ver="$(package_version_from_url "$I18N_URL")"
core_ver_before="$(installed_package_version clashoo)"
apk_core_ready() {
package_installed clashoo && version_equal "$(installed_package_version clashoo)" "$core_ver" \
&& [ -x /etc/init.d/clashoo ] && [ -x /usr/share/clashoo/update/component_update.sh ]
}
apk_luci_ready() {
package_installed luci-app-clashoo && version_equal "$(installed_package_version luci-app-clashoo)" "$luci_ver" \
&& { [ -z "$I18N_URL" ] || { package_installed luci-i18n-clashoo-zh-cn \
&& version_equal "$(installed_package_version luci-i18n-clashoo-zh-cn)" "$i18n_ver"; }; } \
&& [ -r /usr/share/luci/menu.d/luci-app-clashoo.json ] \
&& [ -r /usr/share/rpcd/ucode/luci.clashoo ]
}
apk_bundle_ready() {
apk_core_ready && apk_luci_ready
}
apk_update_ready() {
if [ "$which" = "clashoo" ]; then
apk_bundle_ready
else
package_installed clashoo && apk_luci_ready
fi
}
if [ "$which" = "clashoo" ]; then
log "目标版本:Clashoo 核心 ${core_ver}"
cur_ver="$(installed_package_version clashoo)"
if [ "$PM" = "apk" ] && apk_update_ready; then
finish 0 "Clashoo 核心已是最新版本"
elif [ "$PM" != "apk" ] && version_equal "$cur_ver" "$core_ver"; then
finish 0 "Clashoo 核心已是最新版本"
fi
else
log "目标版本:客户端 ${luci_ver}"
cur_ver="$(installed_package_version luci-app-clashoo)"
if [ "$PM" = "apk" ] && apk_update_ready; then
finish 0 "客户端已是最新版本"
elif [ "$PM" != "apk" ] && version_equal "$cur_ver" "$luci_ver"; then
finish 0 "客户端已是最新版本"
fi
fi
rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR" || finish 1 "创建临时目录失败"
was_running=0
clashoo_was_running && was_running=1
backup_config || finish 1 "备份配置失败"
apk_core_file=""
if [ "$PM" = "apk" ]; then
log "正在下载组件包"
if [ "$which" = "clashoo" ] || ! package_installed clashoo; then
ensure_root_space 98304
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
apk_core_file="$TMP_DIR/core.${EXT}"
fi
download_package_file "$LUCI_URL" "$TMP_DIR/luci.${EXT}" "luci-app-clashoo"
if [ -n "$I18N_URL" ]; then
download_package_file "$I18N_URL" "$TMP_DIR/i18n.${EXT}" "语言包"
fi
elif [ "$which" = "clashoo" ]; then
log "正在下载 Clashoo 核心"
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
else
log "正在下载客户端(LuCI + 语言包)"
download_package_file "$LUCI_URL" "$TMP_DIR/luci.${EXT}" "luci-app-clashoo"
if [ -n "$I18N_URL" ]; then
download_package_file "$I18N_URL" "$TMP_DIR/i18n.${EXT}" "语言包"
fi
fi
if [ "$PM" != "apk" ] && [ "$which" = "clashoo" ]; then
ensure_root_space 98304
fi
if [ "$which" = "clashoo" ]; then
log "正在安装 Clashoo 核心"
if [ "$PM" = "opkg" ]; then
opkg install --force-downgrade "$TMP_DIR/core.${EXT}" >"$TMP_DIR/install.log" 2>&1; rc=$?
else
apk_bundle_install ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
fi
else
log "正在安装客户端"
if [ "$PM" = "opkg" ]; then
opkg install --force-downgrade "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
else
apk_bundle_install ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
fi
fi
if [ "$rc" -ne 0 ]; then
if [ "$PM" = "apk" ] && apk_update_ready; then
log "apk 报告了其它软件包的错误,本次组件已升级到目标版本"
rc=0
else
log_install_error "$TMP_DIR/install.log"
log "安装失败,正在恢复配置备份"
restore_config_backup
finish "$rc" "组件更新失败,已停止操作,请查看组件更新日志"
fi
fi
validation_failed() {
log "安装校验失败,正在恢复配置备份"
restore_config_backup
finish 1 "组件安装校验失败,已停止操作,请查看组件更新日志"
}
if [ "$PM" = "apk" ]; then
if ! apk_update_ready; then
log "安装校验失败,正在安全重装组件包"
if [ -z "$apk_core_file" ] && ! package_installed clashoo; then
ensure_root_space 98304
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
apk_core_file="$TMP_DIR/core.${EXT}"
fi
apk_bundle_install --force-reinstall ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" \
${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >>"$TMP_DIR/install.log" 2>&1; repair_rc=$?
if [ "$repair_rc" -ne 0 ]; then
log_install_error "$TMP_DIR/install.log"
validation_failed
fi
apk_update_ready || validation_failed
fi
elif [ "$which" = "clashoo" ]; then
package_installed clashoo && version_equal "$(installed_package_version clashoo)" "$core_ver" \
&& [ -x /etc/init.d/clashoo ] && [ -x /usr/share/clashoo/update/component_update.sh ] \
|| validation_failed
else
package_installed luci-app-clashoo && version_equal "$(installed_package_version luci-app-clashoo)" "$luci_ver" \
&& [ -r /usr/share/luci/menu.d/luci-app-clashoo.json ] \
&& [ -r /usr/share/rpcd/ucode/luci.clashoo ] \
|| validation_failed
[ -z "$I18N_URL" ] || package_installed luci-i18n-clashoo-zh-cn || validation_failed
fi
# APK component updates install the LuCI bundle in the same transaction.
if [ "$which" = "luci" ] || [ "$PM" = "apk" ]; then
log "正在刷新 LuCI 服务"
restart_web_stack
fi
core_ver_after="$(installed_package_version clashoo)"
restart_clashoo=0
[ "$which" = "clashoo" ] && restart_clashoo=1
[ "$PM" = "apk" ] && [ "$core_ver_before" != "$core_ver_after" ] && restart_clashoo=1
if [ "$restart_clashoo" -eq 1 ] && [ "$was_running" -eq 1 ] && [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then
log "Clashoo 原本运行中,正在重启服务"
sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1 || /etc/init.d/clashoo restart >/dev/null 2>&1 || true
elif [ "$restart_clashoo" -eq 1 ]; then
log "Clashoo 原本未运行,保持停止状态"
fi
finish 0 "组件更新完成"
}
run_core_update() {
dcore="$1"
label="$2"
log "正在更新 ${label}"
# Pass the target as an arg so core_download refreshes only that binary
# without switching the active kernel. It restarts only if the target is the
# currently-running core (see core_download.sh finalize). Switching kernels
# stays a separate, explicit user action.
touch /var/run/core_update
sh /usr/share/clashoo/update/core_download.sh "$dcore" >>/tmp/clash_update.txt 2>&1
rc=$?
rm -f /var/run/core_update
[ "$rc" -eq 0 ] || finish "$rc" "${label} 更新失败"
finish 0 "${label} 更新完成"
}
run_lgbm_update() {
log "正在更新 Smart LightGBM 模型"
sh /usr/share/clashoo/update/lgbm_update.sh >/tmp/lgbm_update.log 2>&1
rc=$?
tail -5 /tmp/lgbm_update.log 2>/dev/null | while IFS= read -r line; do
[ -n "$line" ] && log "$line"
done
[ "$rc" -eq 0 ] || finish "$rc" "LightGBM 模型更新失败"
finish 0 "LightGBM 模型更新完成"
}
run_china_update() {
log "正在更新大陆白名单"
sh /usr/share/clashoo/update/update_china_ip.sh >>/tmp/clash_update.txt 2>&1
rc=$?
tail -6 /tmp/clash_update.txt 2>/dev/null | while IFS= read -r line; do
[ -n "$line" ] && printf '%s\n' "$line" | grep -q '白名单' && log "$line"
done
[ "$rc" -eq 0 ] || finish "$rc" "大陆白名单更新失败"
finish 0 "大陆白名单更新完成"
}
run_geoip_update() {
log "正在更新 GeoIP / GeoSite"
touch /var/run/geoip_update
sh /usr/share/clashoo/update/geoip.sh >/tmp/geoip_update.txt 2>&1
rc=$?
rm -f /var/run/geoip_update
tail -8 /tmp/geoip_update.txt 2>/dev/null | while IFS= read -r line; do
[ -n "$line" ] && log "$line"
done
[ "$rc" -eq 0 ] || finish "$rc" "GeoIP / GeoSite 更新失败"
finish 0 "GeoIP / GeoSite 更新完成"
}
COMPONENT="${1:-}"
VARIANT="${2:-}" # mihomo/singbox: stable | alpha
[ -n "$COMPONENT" ] || {
echo "usage: $0 <clashoo|luci|mihomo|singbox|smart|lgbm|china|geoip> [stable|alpha]"
exit 2
}
mkdir -p "$(dirname "$RUN_FILE")" >/dev/null 2>&1
printf '%s\n' "$COMPONENT" >"$RUN_FILE"
STATUS="running"
log "组件更新任务启动:${COMPONENT}${VARIANT:+ ($VARIANT)}"
case "$COMPONENT" in
clashoo) run_pkg_update clashoo ;;
luci) run_pkg_update luci ;;
mihomo)
if [ "$VARIANT" = "alpha" ]; then run_core_update 3 "mihomo Alpha 版"
else run_core_update 2 "mihomo 稳定版"; fi ;;
singbox)
if [ "$VARIANT" = "alpha" ]; then run_core_update 5 "sing-box Alpha 版"
else run_core_update 4 "sing-box 稳定版"; fi ;;
smart) run_core_update 1 "mihomo Smart 版" ;;
lgbm) run_lgbm_update ;;
china) run_china_update ;;
geoip) run_geoip_update ;;
*) finish 2 "未知组件:${COMPONENT}" ;;
esac
File diff suppressed because it is too large. Load diff
@@ -1,282 +0,0 @@
#!/bin/sh
LOG_FILE="/tmp/geoip_update.txt"
TMP_DIR="/tmp/clash_geoip_$$"
MMDB_MIN_SIZE=2000000
GEOSITE_MIN_SIZE=2000000
geoip_source="$(uci -q get clashoo.config.geoip_source 2>/dev/null)"
license_key="$(uci -q get clashoo.config.license_key 2>/dev/null)"
cfg_mmdb_url="$(uci -q get clashoo.config.geoip_mmdb_url 2>/dev/null)"
cfg_geosite_url="$(uci -q get clashoo.config.geosite_url 2>/dev/null)"
cfg_geoip_dat_url="$(uci -q get clashoo.config.geoip_dat_url 2>/dev/null)"
cfg_geoip_asn_url="$(uci -q get clashoo.config.geoip_asn_url 2>/dev/null)"
require_geosite=0
[ "$1" = "--require-geosite" ] && require_geosite=1
DEFAULT_MMDB_URL="https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
DEFAULT_GEOSITE_URL="https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/release/geosite.dat"
DEFAULT_GEOIP_DAT_URL="https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/release/geoip.dat"
DEFAULT_GEOIP_ASN_URL="https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb"
LOYALSOLDIER_MMDB_URL="https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
LOYALSOLDIER_GEOSITE_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat"
LOYALSOLDIER_GEOIP_DAT_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat"
# v2fly ships no Country.mmdb — borrow Loyalsoldier's for the mmdb slot
V2FLY_GEOSITE_URL="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"
V2FLY_GEOIP_DAT_URL="https://github.com/v2fly/geoip/releases/latest/download/geoip.dat"
log() {
echo " $(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
cleanup() {
rm -rf "$TMP_DIR" >/dev/null 2>&1
rm -f /var/run/geoip_update >/dev/null 2>&1
}
config_needs_geosite() {
_cfg="$(uci -q get clashoo.config.use_config 2>/dev/null)"
if [ -s "$_cfg" ] && grep -Eq "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,|^[[:space:]]*geosite:[[:space:]]*|^[[:space:]]*'geosite:|^[[:space:]]*-[[:space:]]*[\"']?geosite:" "$_cfg"; then
return 0
fi
_cfg="/etc/clashoo/config.yaml"
[ -f "$_cfg" ] || return 1
grep -Eq "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,|^[[:space:]]*geosite:[[:space:]]*|^[[:space:]]*'geosite:|^[[:space:]]*-[[:space:]]*[\"']?geosite:" "$_cfg"
}
# Shared port detection (handles smart core + sing-box profile port); keep the
# local anonymity probe so we skip a coexisting plugin's auth-locked proxy.
. /usr/share/clashoo/update/proxy_lib.sh
detect_proxy() {
proxy="$(clashoo_detect_proxy)"
[ -n "$proxy" ] || return 0
# Only use it if it actually proxies anonymously. A coexisting plugin
# (e.g. OpenClash on the same 7890) may hold this port behind auth and
# answer HTTP 407 — in that case fall through to the mirrors instead.
if command -v curl >/dev/null 2>&1; then
curl -fsS --proxy "$proxy" --connect-timeout 3 --max-time 5 -o /dev/null \
http://www.gstatic.com/generate_204 >/dev/null 2>&1 || return 0
fi
echo "$proxy"
}
_fetch() {
url="$1"; target="$2"; proxy="$3"
if command -v curl >/dev/null 2>&1; then
if [ -n "$proxy" ]; then
curl --proxy "$proxy" -fsSL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$url" -o "$target"
else
curl -fsSL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$url" -o "$target"
fi
return $?
fi
wget -q --timeout=120 --user-agent="Clash/OpenWRT" "$url" -O "$target"
}
# 多源拉取:本机代理 → 国内镜像(gh-proxy / ghfast / jsdelivr)→ 原 URL 兜底
download_to() {
url="$1"; target="$2"
[ -z "$url" ] && return 1
proxy="$(detect_proxy)"
if [ -n "$proxy" ]; then
_fetch "$url" "$target" "$proxy" && return 0
fi
case "$url" in
https://raw.githubusercontent.com/*)
rest="${url#https://raw.githubusercontent.com/}"
owner="${rest%%/*}"; r1="${rest#*/}"
repo="${r1%%/*}"; r2="${r1#*/}"
branch="${r2%%/*}"; path="${r2#*/}"
for m in \
"https://gh-proxy.com/${url}" \
"https://ghfast.top/${url}" \
"https://cdn.jsdelivr.net/gh/${owner}/${repo}@${branch}/${path}"; do
_fetch "$m" "$target" "" && return 0
done
;;
https://github.com/*)
for m in \
"https://gh-proxy.com/${url}" \
"https://ghfast.top/${url}"; do
_fetch "$m" "$target" "" && return 0
done
;;
esac
_fetch "$url" "$target" ""
}
download_optional() {
url="$1"
target="$2"
name="$3"
tmp_target="${TMP_DIR}/$(basename "$target").tmp"
if [ -z "$url" ]; then
log "$name skip (url empty)"
return 0
fi
rm -f "$tmp_target" >/dev/null 2>&1
if download_to "$url" "$tmp_target"; then
mv -f "$tmp_target" "$target" >/dev/null 2>&1 || return 1
chmod 644 "$target" >/dev/null 2>&1
log "$name updated"
return 0
fi
rm -f "$tmp_target" >/dev/null 2>&1
log "$name update failed"
return 0
}
download_geosite() {
url="$1"
tmp_target="${TMP_DIR}/GeoSite.dat.tmp"
size=0
if [ -z "$url" ]; then
log "GeoSite.dat skip (url empty)"
return 1
fi
rm -f "$tmp_target" >/dev/null 2>&1
if ! download_to "$url" "$tmp_target"; then
rm -f "$tmp_target" >/dev/null 2>&1
log "GeoSite.dat update failed"
return 1
fi
size=$(wc -c <"$tmp_target" 2>/dev/null)
if [ -z "$size" ] || [ "$size" -lt "$GEOSITE_MIN_SIZE" ]; then
log "GeoSite.dat download invalid or incomplete (${size:-0} bytes)"
rm -f "$tmp_target" >/dev/null 2>&1
return 1
fi
mv -f "$tmp_target" /etc/clashoo/GeoSite.dat >/dev/null 2>&1 || return 1
cp -f /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1 || true
chmod 644 /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1
log "GeoSite.dat updated"
return 0
}
download_mmdb() {
url="$1"
tmp_target="${TMP_DIR}/Country.mmdb.tmp"
size=0
rm -f "$tmp_target" >/dev/null 2>&1
if ! download_to "$url" "$tmp_target"; then
rm -f "$tmp_target" >/dev/null 2>&1
return 1
fi
size=$(wc -c <"$tmp_target" 2>/dev/null)
if [ -z "$size" ] || [ "$size" -lt "$MMDB_MIN_SIZE" ]; then
log "Country.mmdb download invalid or incomplete (${size:-0} bytes)"
rm -f "$tmp_target" >/dev/null 2>&1
return 1
fi
mv -f "$tmp_target" /etc/clashoo/Country.mmdb >/dev/null 2>&1 || return 1
chmod 644 /etc/clashoo/Country.mmdb >/dev/null 2>&1
return 0
}
trap cleanup EXIT INT TERM
mkdir -p "$TMP_DIR" /etc/clashoo >/dev/null 2>&1
rm -f /var/run/geoip_down_complete >/dev/null 2>&1
: > /var/run/geoip_update
: > "$LOG_FILE"
log "GeoIP 更新任务启动"
mmdb_url=""
geosite_url=""
geoip_dat_url=""
geoip_asn_url=""
case "$geoip_source" in
1)
if [ -z "$license_key" ]; then
log "MaxMind source selected but license key is empty"
exit 1
fi
log "Updating Country.mmdb from MaxMind"
if ! download_to "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${license_key}&suffix=tar.gz" "$TMP_DIR/geoip.tar.gz"; then
log "MaxMind download failed"
exit 1
fi
if ! tar zxf "$TMP_DIR/geoip.tar.gz" -C "$TMP_DIR" >/dev/null 2>&1; then
log "MaxMind archive extract failed"
exit 1
fi
mmdb_file="$(ls "$TMP_DIR"/GeoLite2-Country_*/GeoLite2-Country.mmdb 2>/dev/null | head -n 1)"
if [ -z "$mmdb_file" ] || [ ! -f "$mmdb_file" ]; then
log "MaxMind Country.mmdb not found in archive"
exit 1
fi
cp -f "$mmdb_file" /etc/clashoo/Country.mmdb
chmod 644 /etc/clashoo/Country.mmdb >/dev/null 2>&1
log "Country.mmdb updated"
;;
3)
# Loyalsoldier (default)
mmdb_url="$LOYALSOLDIER_MMDB_URL"
geosite_url="$LOYALSOLDIER_GEOSITE_URL"
geoip_dat_url="$LOYALSOLDIER_GEOIP_DAT_URL"
geoip_asn_url="$DEFAULT_GEOIP_ASN_URL"
;;
5)
# v2fly (mmdb borrowed from Loyalsoldier)
mmdb_url="$LOYALSOLDIER_MMDB_URL"
geosite_url="$V2FLY_GEOSITE_URL"
geoip_dat_url="$V2FLY_GEOIP_DAT_URL"
geoip_asn_url="$DEFAULT_GEOIP_ASN_URL"
;;
4)
# custom — asn falls back to default when left blank
mmdb_url="$cfg_mmdb_url"
geosite_url="$cfg_geosite_url"
geoip_dat_url="$cfg_geoip_dat_url"
geoip_asn_url="${cfg_geoip_asn_url:-$DEFAULT_GEOIP_ASN_URL}"
;;
*)
mmdb_url="${cfg_mmdb_url:-$DEFAULT_MMDB_URL}"
geosite_url="${cfg_geosite_url:-$DEFAULT_GEOSITE_URL}"
geoip_dat_url="${cfg_geoip_dat_url:-$DEFAULT_GEOIP_DAT_URL}"
geoip_asn_url="${cfg_geoip_asn_url:-$DEFAULT_GEOIP_ASN_URL}"
;;
esac
if [ "$geoip_source" != "1" ]; then
log "Updating Country.mmdb"
if ! download_mmdb "$mmdb_url"; then
log "Country.mmdb download failed"
exit 1
fi
log "Country.mmdb updated"
if [ "$require_geosite" -eq 1 ] || config_needs_geosite; then
if ! download_geosite "$geosite_url"; then
exit 1
fi
else
log "GeoSite.dat skip (no geosite reference in config)"
fi
download_optional "$geoip_dat_url" /etc/clashoo/geoip.dat "geoip.dat"
download_optional "$geoip_asn_url" /etc/clashoo/GeoLite2-ASN.mmdb "GeoLite2-ASN.mmdb"
fi
if [ "$geoip_source" = "1" ] && { [ "$require_geosite" -eq 1 ] || config_needs_geosite; }; then
geosite_url="${cfg_geosite_url:-$DEFAULT_GEOSITE_URL}"
if ! download_geosite "$geosite_url"; then
exit 1
fi
fi
touch /var/run/geoip_down_complete >/dev/null 2>&1
if pidof mihomo >/dev/null 2>&1 || pidof clash-meta >/dev/null 2>&1; then
log "GeoIP update completed, apply on next Clashoo restart"
fi
log "GeoIP update completed"
exit 0
@@ -1,52 +0,0 @@
#!/bin/sh
MODEL_PATH="/etc/clashoo/Model.bin"
TMP_PATH="/tmp/clash_Model.bin"
LOG_FILE="/tmp/lgbm_update.log"
log() {
echo " $(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
: > "$LOG_FILE"
DOWNLOAD_URL=$(uci get clashoo.config.smart_lgbm_url 2>/dev/null)
[ -z "$DOWNLOAD_URL" ] && DOWNLOAD_URL="https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin"
log "Start downloading LightGBM model from: $DOWNLOAD_URL"
mkdir -p /etc/clashoo
rm -f "$TMP_PATH" 2>/dev/null
if command -v curl >/dev/null 2>&1; then
curl -fL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$DOWNLOAD_URL" -o "$TMP_PATH" 2>/dev/null
dl_rc=$?
elif command -v wget >/dev/null 2>&1; then
wget -q --timeout=30 -U "Clash/OpenWRT" "$DOWNLOAD_URL" -O "$TMP_PATH" 2>/dev/null
dl_rc=$?
else
log "No curl or wget found"
exit 1
fi
if [ "$dl_rc" -ne 0 ] || [ ! -s "$TMP_PATH" ]; then
log "Download failed (rc=$dl_rc)"
rm -f "$TMP_PATH"
exit 1
fi
if [ -f "$MODEL_PATH" ] && cmp -s "$TMP_PATH" "$MODEL_PATH"; then
log "Model unchanged, no update needed"
rm -f "$TMP_PATH"
exit 0
fi
mv "$TMP_PATH" "$MODEL_PATH" 2>/dev/null
if [ $? -ne 0 ]; then
log "Failed to install model to $MODEL_PATH"
rm -f "$TMP_PATH"
exit 1
fi
log "LightGBM model updated successfully"
exit 0
@@ -1,37 +0,0 @@
#!/bin/sh
STATE_DIR="${CLASHOO_STATE_DIR:-/usr/share/clashbackup}"
UPDATE_SCRIPT="${CLASHOO_UPDATE_SCRIPT:-/usr/share/clashoo/update/update_all.sh}"
LOG_FILE="${CLASHOO_LOG_FILE:-/usr/share/clashoo/clashoo.txt}"
LOCK_DIR="${CLASHOO_LOCK_DIR:-/tmp/clashoo_maintenance.lock}"
NOW="${CLASHOO_NOW:-$(date +%s)}"
stamp() {
tmp="$STATE_DIR/$1.tmp.$$"
printf 'last_run=%s\n' "$NOW" >"$tmp" && mv -f "$tmp" "$STATE_DIR/$1"
}
due() {
hours="$(uci -q get "clashoo.config.$2")"
echo "$hours" | grep -Eq '^[0-9]+$' && [ "$hours" -ge 1 ] || hours=12
last="$(sed -n 's/^last_run=//p' "$STATE_DIR/$1" 2>/dev/null | head -1)"
if ! echo "$last" | grep -Eq '^[0-9]+$' || [ "$last" -gt "$NOW" ]; then
stamp "$1"
return 1
fi
[ $((NOW - last)) -ge $((hours * 3600)) ]
}
mkdir -p "$STATE_DIR" || exit 0
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
trap 'rmdir "$LOCK_DIR" >/dev/null 2>&1' EXIT INT TERM
if [ "$(uci -q get clashoo.config.auto_update)" = "1" ] && due rule_update.status auto_update_time; then
CLASHOO_STATE_DIR="$STATE_DIR" sh "$UPDATE_SCRIPT" >/dev/null 2>&1
fi
if [ "$(uci -q get clashoo.config.auto_clear_log)" = "1" ] && due log_cleanup.status clear_time; then
: >"$LOG_FILE"
sh /usr/share/clashoo/lib/core_log_guard.sh >/dev/null 2>&1
stamp log_cleanup.status
fi
@@ -1,204 +0,0 @@
#!/bin/sh
REAL_LOG="/usr/share/clashoo/clashoo_real.txt"
panel="$1"
lang="$(uci -q get luci.main.lang 2>/dev/null)"
STATE_FILE="/tmp/clash_panel_download_state"
RUN_FILE="/var/run/panel_downloading"
DASHBOARD_LINK="/etc/clashoo/dashboard"
[ -n "$panel" ] || panel="$(uci -q get clashoo.config.dashboard_panel 2>/dev/null)"
[ -n "$panel" ] || panel="zashboard"
case "$panel" in
metacubexd)
URLS="https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip"
TARGET_DIR="/etc/clashoo/dashboard-metacubexd"
;;
yacd)
URLS="https://github.com/MetaCubeX/Yacd-meta/archive/refs/heads/gh-pages.zip https://github.com/haishanh/yacd/archive/refs/heads/gh-pages.zip"
TARGET_DIR="/etc/clashoo/dashboard-yacd"
;;
zashboard)
URLS="https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
TARGET_DIR="/etc/clashoo/dashboard-zashboard"
;;
razord)
URLS="https://github.com/MetaCubeX/Razord-meta/archive/refs/heads/gh-pages.zip https://github.com/ayanamist/clash-dashboard/archive/refs/heads/gh-pages.zip"
TARGET_DIR="/etc/clashoo/dashboard-razord"
;;
*)
URLS="https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
TARGET_DIR="/etc/clashoo/dashboard-zashboard"
panel="zashboard"
;;
esac
TMP_ROOT="/tmp/clash_panel_${panel}_$$"
ZIP_FILE="$TMP_ROOT/panel.zip"
UNPACK_DIR="$TMP_ROOT/unpack"
UPDATE_LOG="/tmp/clash_update.txt"
update_log() {
[ -n "$1" ] || return 0
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
}
log_msg() {
if [ "$lang" = "en" ]; then
echo "$1" >"$REAL_LOG"
[ "$3" = "1" ] || update_log "$1"
else
echo "$2" >"$REAL_LOG"
[ "$3" = "1" ] || update_log "$2"
fi
}
set_state() {
state="$1"
msg="$2"
echo "${state}:${panel}:${msg}" >"$STATE_FILE"
}
cleanup() {
rm -rf "$TMP_ROOT" >/dev/null 2>&1
rm -f "$RUN_FILE" >/dev/null 2>&1
}
# Route the panel download through the running core in kernel-only mode
# (shared logic in proxy_lib.sh). Normal mode returns empty -> TPROXY.
. /usr/share/clashoo/update/proxy_lib.sh
detect_proxy() { clashoo_detect_proxy; }
download_zip() {
proxy="$(detect_proxy)"
for url in $URLS; do
if [ -n "$proxy" ] && command -v curl >/dev/null 2>&1 &&
curl -fsSL --connect-timeout 15 --max-time 300 --retry 1 \
--proxy "$proxy" -A "Clash/OpenWRT" "$url" -o "$ZIP_FILE"; then
return 0
fi
if wget -q --timeout=60 --user-agent="Clash/OpenWRT" "$url" -O "$ZIP_FILE"; then
return 0
fi
done
return 1
}
validate_zip_entries() {
entries="$TMP_ROOT/entries.txt"
if command -v unzip >/dev/null 2>&1; then
unzip -Z1 "$ZIP_FILE" >"$entries" 2>/dev/null || return 1
elif command -v bsdtar >/dev/null 2>&1; then
bsdtar -tf "$ZIP_FILE" >"$entries" 2>/dev/null || return 1
else
return 1
fi
[ -s "$entries" ] || return 1
awk '
/^\// || /^[A-Za-z]:/ || /(^|\/)\.\.(\/|$)/ || /\\/ || /\r/ { bad = 1 }
END { exit bad }
' "$entries"
}
extract_zip() {
if command -v unzip >/dev/null 2>&1; then
unzip -oq "$ZIP_FILE" -d "$UNPACK_DIR" >/dev/null 2>&1
return $?
fi
if command -v busybox >/dev/null 2>&1 && busybox --list 2>/dev/null | grep -qx "unzip"; then
busybox unzip -oq "$ZIP_FILE" -d "$UNPACK_DIR" >/dev/null 2>&1
return $?
fi
if command -v bsdtar >/dev/null 2>&1; then
bsdtar -xf "$ZIP_FILE" -C "$UNPACK_DIR" >/dev/null 2>&1
return $?
fi
return 2
}
find_web_root() {
if [ -f "$UNPACK_DIR/index.html" ]; then
echo "$UNPACK_DIR"
return 0
fi
index_file="$(find "$UNPACK_DIR" -type f -name index.html 2>/dev/null | head -n 1)"
if [ -n "$index_file" ]; then
dirname "$index_file"
return 0
fi
return 1
}
activate_panel() {
rm -rf "$DASHBOARD_LINK" >/dev/null 2>&1
ln -s "$TARGET_DIR" "$DASHBOARD_LINK" >/dev/null 2>&1 || cp -a "$TARGET_DIR" "$DASHBOARD_LINK" >/dev/null 2>&1
rm -rf /www/luci-static/yacd >/dev/null 2>&1
if [ "$panel" = "yacd" ]; then
ln -s "$TARGET_DIR" /www/luci-static/yacd >/dev/null 2>&1
fi
}
trap cleanup EXIT INT TERM
mkdir -p "$UNPACK_DIR" "$TARGET_DIR" >/dev/null 2>&1
touch "$RUN_FILE"
set_state "downloading" "开始下载"
log_msg "Downloading dashboard panel..." "正在下载面板..."
if ! download_zip; then
set_state "error" "下载失败"
log_msg "Dashboard panel download failed" "面板下载失败"
exit 1
fi
if ! validate_zip_entries; then
set_state "error" "压缩包路径无效"
log_msg "Dashboard panel archive has unsafe entries" "面板压缩包含不安全路径"
exit 1
fi
extract_zip
extract_rc=$?
if [ "$extract_rc" -ne 0 ]; then
if [ "$extract_rc" -eq 2 ]; then
set_state "error" "缺少解压工具"
log_msg "Panel unzip tool not found" "缺少解压工具,无法安装面板"
else
set_state "error" "解压失败"
log_msg "Dashboard panel unzip failed" "面板解压失败"
fi
exit 1
fi
if find "$UNPACK_DIR" ! -type d ! -type f | grep -q .; then
set_state "error" "压缩包文件类型无效"
log_msg "Dashboard panel archive has unsafe file types" "面板压缩包含不安全文件类型"
exit 1
fi
SRC_DIR="$(find_web_root)"
if [ -z "$SRC_DIR" ] || [ ! -f "$SRC_DIR/index.html" ]; then
set_state "error" "文件无效"
log_msg "Dashboard panel content invalid" "面板文件无效"
exit 1
fi
rm -rf "$TARGET_DIR"/* >/dev/null 2>&1
cp -a "$SRC_DIR"/. "$TARGET_DIR"/ >/dev/null 2>&1
activate_panel
uci set clashoo.config.dashboard_panel="$panel" >/dev/null 2>&1
uci commit clashoo >/dev/null 2>&1
set_state "success" "安装成功"
log_msg "Dashboard panel installed" "面板安装完成"
sleep 1
log_msg "Clashoo" "Clashoo" 1
exit 0
@@ -1,80 +0,0 @@
# shellcheck shell=sh
# Clashoo's sing-box is private; HomeProxy owns the public system binary.
CLASHOO_SINGBOX_BIN_DIR="/usr/share/clashoo/bin"
CLASHOO_SINGBOX_RUNTIME_CONFIG="/var/run/clashoo/sing-box/config.json"
# Shared proxy detection for clashoo's own outbound requests.
#
# clashoo's self-initiated downloads (update checks, component/panel/core/geoip
# downloads, subscriptions) and health checks must reach GitHub through the
# running core, otherwise they leak out direct and stall behind the GFW. Route
# them through the core's mixed port whenever a core is running — in BOTH modes:
# - core-only mode deliberately skips TPROXY to coexist with other plugins;
# - normal mode used to rely on transparent redirect, but the router's OWN
# output redirect is fragile (e.g. PPPoE/GL.iNet stacks where Redirect to
# loopback never completes — issue #25), so don't depend on it. The explicit
# mixed port works on every network stack.
# When no core is running (e.g. bootstrapping the very first core download),
# returns empty so the caller falls back to direct / mirror sources.
#
# Port source depends on the running Clashoo kernel. The liveness gate uses the
# procd owner instead of a binary name, because other proxy plugins may run the
# same binaries:
# sing-box -> private runtime config mixed inbound
# (an imported profile may carry its own port)
# mihomo / clash-meta / smart -> /etc/clashoo/config.yaml mixed-port
# (a custom config's port may differ from uci)
# uci mixed_port is the last-resort fallback for both.
clashoo_running_core_type() {
_cdp_type="$(uci -q get clashoo.config.core_type 2>/dev/null)"
case "$_cdp_type" in
singbox)
_cdp_service="clashoo"
_cdp_filter='@.clashoo.instances.singbox.pid'
;;
*)
_cdp_type="mihomo"
_cdp_service="clashoo"
_cdp_filter='@.clashoo.instances.*.pid'
;;
esac
_cdp_pid="$(ubus call service list "{\"name\":\"$_cdp_service\",\"verbose\":true}" 2>/dev/null \
| jsonfilter -e "$_cdp_filter" 2>/dev/null | head -n1)"
[ -n "$_cdp_pid" ] && [ -d "/proc/$_cdp_pid" ] || return 1
if [ "$_cdp_type" = "singbox" ]; then
local _cdp_exe _cdp_cmdline
_cdp_exe="$(readlink -f "/proc/$_cdp_pid/exe" 2>/dev/null)"
case "$_cdp_exe" in
"$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha") ;;
*)
_cdp_cmdline="$(tr '\000' ' ' < "/proc/$_cdp_pid/cmdline" 2>/dev/null)"
case "$_cdp_cmdline" in
"$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable "*|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha "*) ;;
*) return 1 ;;
esac
;;
esac
fi
printf '%s' "$_cdp_type"
}
clashoo_detect_proxy() {
_cdp_type="$(clashoo_running_core_type)" || return 0
_cdp_port=""
if [ "$_cdp_type" = "singbox" ]; then
_cdp_port="$(jsonfilter -i "$CLASHOO_SINGBOX_RUNTIME_CONFIG" \
-e '@.inbounds[@.type="mixed"].listen_port' 2>/dev/null | head -n 1)"
else
# prefer mixed-port (HTTP+SOCKS) over plain port; a single two-pattern
# sed would return whichever appears first by line order, so loop by key
for _cdp_key in mixed-port port socks-port; do
_cdp_port="$(sed -n "s/^[[:space:]]*${_cdp_key}:[[:space:]]*\([0-9][0-9]*\).*/\1/p" \
/etc/clashoo/config.yaml 2>/dev/null | head -n 1)"
[ -n "$_cdp_port" ] && break
done
fi
[ -n "$_cdp_port" ] || _cdp_port="$(uci -q get clashoo.config.mixed_port 2>/dev/null)"
[ -n "$_cdp_port" ] || return 0
printf 'http://127.0.0.1:%s' "$_cdp_port"
}
@@ -1,385 +0,0 @@
#!/bin/sh
SUB_DIR="${CLASHOO_SUB_DIR:-/usr/share/clashoo/config/sub}"
SINGBOX_DIR="${CLASHOO_SINGBOX_DIR:-/usr/share/clashoo/config/singbox}"
BACKUP_DIR="${CLASHOO_BACKUP_DIR:-/usr/share/clashbackup}"
TEMPLATE_DIR="${CLASHOO_TEMPLATE_DIR:-/usr/share/clashoo/config/custom}"
LIST_FILE="${CLASHOO_LIST_FILE:-$BACKUP_DIR/confit_list.conf}"
BIND_FILE="${CLASHOO_BIND_FILE:-$BACKUP_DIR/template_bindings.conf}"
STATUS_FILE="${CLASHOO_STATUS_FILE:-$BACKUP_DIR/subscription_update.status}"
LOCK_DIR="${CLASHOO_LOCK_DIR:-/tmp/clashoo_subscription_update.lock}"
UPDATE_LOG="${CLASHOO_UPDATE_LOG:-/tmp/clash_update.txt}"
SERVICE_CMD="${CLASHOO_SERVICE_CMD:-/etc/init.d/clashoo}"
TMP_DIR="${CLASHOO_TMP_DIR:-/tmp}"
DEFAULT_SUB_UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
DOWNLOAD_ERROR=""
# Subscriptions are fetched direct first (many airports are domestic / geo-fence
# foreign exit IPs). Only used as a last-resort fallback, see download_to.
. /usr/share/clashoo/update/proxy_lib.sh
updated=0
unchanged=0
failed=0
skipped=0
restart_needed=0
started_at="$(date +%s)"
message=""
record_status=0
[ "$1" = "--all" ] && record_status=1
log_update() {
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
}
write_status() {
local tmp
mkdir -p "$(dirname "$STATUS_FILE")" >/dev/null 2>&1
tmp="${STATUS_FILE}.tmp.$$"
cat >"$tmp" <<EOF
running=$1
last_run=$started_at
finished_at=$2
updated=$updated
unchanged=$unchanged
failed=$failed
skipped=$skipped
message=$message
EOF
mv "$tmp" "$STATUS_FILE"
}
cleanup() {
rm -rf "$LOCK_DIR" >/dev/null 2>&1
}
if ! mkdir "$LOCK_DIR" >/dev/null 2>&1; then
exit 75
fi
trap cleanup EXIT INT TERM
[ "$record_status" = "1" ] && write_status 1 0
safe_name() {
case "$1" in
''|*/*|*..*) return 1 ;;
esac
return 0
}
extract_host() {
printf '%s' "$1" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's#:.*$##' -e 's#.*@##'
}
resolve_via() {
local host dns
host="$1"
dns="$2"
nslookup "$host" "$dns" 2>/dev/null | awk '
/^Address/ {
ip = $NF
if (ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ &&
ip !~ /^127\./ && ip !~ /^0\./ && ip !~ /^198\.18\./ &&
ip != "8.8.8.8" && ip != "1.1.1.1" &&
ip != "223.5.5.5" && ip != "119.29.29.29") {
print ip
exit
}
}'
}
curl_download() {
local url out hdr ua host ip
url="$1"
out="$2"
hdr="$3"
ua="$4"
host="$5"
ip="$6"
if [ -n "$host" ] && [ -n "$ip" ]; then
curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \
--speed-limit 1 --retry 2 -A "$ua" -D "$hdr" -o "$out" \
--resolve "$host:443:$ip" --resolve "$host:80:$ip" \
-w '%{http_code}' "$url" 2>/dev/null
else
curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \
--speed-limit 1 --retry 2 -A "$ua" -D "$hdr" -o "$out" \
-w '%{http_code}' "$url" 2>/dev/null
fi
}
subscription_ua() {
local target ua
target="$1"
ua=""
[ -r "${target}.ua" ] && ua="$(sed -n '1p' "${target}.ua" 2>/dev/null)"
[ -n "$ua" ] || ua="$(uci -q get clashoo.config.sub_ua 2>/dev/null)"
[ -n "$ua" ] || ua="$DEFAULT_SUB_UA"
printf '%s' "$ua"
}
save_subscription_ua() {
printf '%s\n' "$2" >"${1}.ua"
}
curl_error() {
case "$1" in
6) printf '%s' "DNS 解析失败" ;;
7) printf '%s' "连接失败" ;;
28) printf '%s' "连接超时" ;;
35) printf '%s' "TLS 握手失败" ;;
60) printf '%s' "证书校验失败" ;;
*) printf '网络错误(curl rc=%s)' "${1:-unknown}" ;;
esac
}
download_to() {
local url out hdr ua code rc last_rc host dns ip proxy
url="$1"
out="$2"
hdr="$3"
ua="$4"
[ -n "$ua" ] || ua="$DEFAULT_SUB_UA"
DOWNLOAD_ERROR=""
rm -f "$out" "$hdr" >/dev/null 2>&1
if command -v curl >/dev/null 2>&1; then
code="$(curl_download "$url" "$out" "$hdr" "$ua" "" "")"
rc=$?
if [ "$rc" -eq 0 ] && [ "$code" = "200" ]; then
return 0
fi
if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then
DOWNLOAD_ERROR="HTTP ${code}"
return 1
fi
last_rc="$rc"
host="$(extract_host "$url")"
for dns in 223.5.5.5 119.29.29.29 1.1.1.1 8.8.8.8; do
ip="$(resolve_via "$host" "$dns")"
[ -n "$ip" ] || continue
log_update "DNS 回退:${host} -> ${ip} (@${dns})"
code="$(curl_download "$url" "$out" "$hdr" "$ua" "$host" "$ip")"
rc=$?
[ "$rc" -eq 0 ] && [ "$code" = "200" ] && return 0
if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then
DOWNLOAD_ERROR="HTTP ${code}"
return 1
fi
last_rc="$rc"
done
# Last resort: direct + DNS-override both failed, so the source may be
# GFW-blocked (e.g. a github-hosted sub). In kernel-only mode try once
# through the running core — nothing left to lose, and it won't disturb
# any airport that already answered direct.
proxy="$(clashoo_detect_proxy)"
if [ -n "$proxy" ]; then
log_update "代理兜底:通过本地核心重试订阅"
code="$(curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \
--speed-limit 1 --retry 1 -A "$ua" -D "$hdr" -o "$out" \
--proxy "$proxy" -w '%{http_code}' "$url" 2>/dev/null)"
rc=$?
[ "$rc" -eq 0 ] && [ "$code" = "200" ] && return 0
if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then
DOWNLOAD_ERROR="HTTP ${code}"
return 1
fi
last_rc="$rc"
fi
DOWNLOAD_ERROR="$(curl_error "$last_rc")"
return 1
fi
wget -q --timeout=20 --user-agent="$ua" "$url" -O "$out"
rc=$?
[ "$rc" -eq 0 ] && return 0
DOWNLOAD_ERROR="网络错误(wget rc=${rc})"
return 1
}
update_info() {
local hdr target info
hdr="$1"
target="$2"
info="$(grep -i 'subscription-userinfo:' "$hdr" 2>/dev/null | head -1 | \
sed 's/^[Ss]ubscription-[Uu]serinfo:[[:space:]]*//' | tr -d '\r')"
if [ -n "$info" ]; then
printf '%s\n' "$info" >"${target}.info"
else
rm -f "${target}.info" >/dev/null 2>&1
fi
}
service_running() {
[ "$("$SERVICE_CMD" status 2>/dev/null)" = "running" ]
}
template_output_name() {
local sub tpl
sub="$(printf '%s' "$1" | sed -e 's/\.[Yy][Aa][Mm][Ll]$//' -e 's/\.[Yy][Mm][Ll]$//' -e 's/[^A-Za-z0-9._-]/-/g')"
tpl="$(printf '%s' "$2" | sed -e 's/\.[Yy][Aa][Mm][Ll]$//' -e 's/\.[Yy][Mm][Ll]$//' -e 's/[^A-Za-z0-9._-]/-/g')"
printf '_merged_%s__%s.yaml' "${sub:-sub}" "${tpl:-template}"
}
apply_template() {
local name target template merged merged_path use_config
name="$1"
target="$2"
[ -r "$BIND_FILE" ] || return 0
[ -x /usr/share/clashoo/update/template_merge.sh ] || return 0
template="$(awk -F '#' -v n="$name" '$1==n && ($3=="1" || $3=="true") {print $2; exit}' "$BIND_FILE")"
[ -n "$template" ] && [ -r "$TEMPLATE_DIR/$template" ] || return 0
merged="$(template_output_name "$name" "$template")"
merged_path="$TEMPLATE_DIR/$merged"
if sh /usr/share/clashoo/update/template_merge.sh "$target" "$TEMPLATE_DIR/$template" "$merged_path" >/dev/null 2>&1; then
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
if [ "$use_config" = "$target" ] || [ "$use_config" = "$merged_path" ]; then
uci -q set clashoo.config.use_config="$merged_path"
uci -q set clashoo.config.config_type='3'
uci -q commit clashoo >/dev/null 2>&1
restart_needed=1
fi
else
log_update "模板生成失败:${name} <- ${template}"
fi
}
update_mihomo() {
local name url typ target tmp hdr ua use_config config_type
name="$1"
url="$2"
typ="$3"
safe_name "$name" || return 1
case "$typ" in clash|meta) ;; *) skipped=$((skipped + 1)); return 0 ;; esac
target="$SUB_DIR/$name"
[ -f "$target" ] || { skipped=$((skipped + 1)); return 0; }
tmp="$TMP_DIR/clashoo_sub_$$.yaml"
hdr="$TMP_DIR/clashoo_sub_$$.hdr"
ua="$(subscription_ua "$target")"
if ! download_to "$url" "$tmp" "$hdr" "$ua"; then
failed=$((failed + 1))
log_update "更新失败(${DOWNLOAD_ERROR:-下载失败}):$name"
rm -f "$tmp" "$hdr"
return 1
fi
if ! grep -Eq '^(proxies|proxy-providers):' "$tmp" 2>/dev/null; then
failed=$((failed + 1))
log_update "更新失败(无效 Mihomo 配置):$name"
rm -f "$tmp" "$hdr"
return 1
fi
if cmp -s "$tmp" "$target"; then
update_info "$hdr" "$target"
save_subscription_ua "$target" "$ua"
unchanged=$((unchanged + 1))
rm -f "$tmp" "$hdr"
return 0
fi
if ! mv "$tmp" "$target"; then
failed=$((failed + 1))
rm -f "$hdr"
return 1
fi
update_info "$hdr" "$target"
save_subscription_ua "$target" "$ua"
rm -f "$hdr"
updated=$((updated + 1))
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
config_type="$(uci -q get clashoo.config.config_type 2>/dev/null)"
[ "$config_type" = "1" ] && [ "$use_config" = "$target" ] && restart_needed=1
apply_template "$name" "$target"
log_update "更新完成:$name"
}
valid_singbox() {
ucode -e 'import { readfile } from "fs"; let c = json(readfile(ARGV[0])); exit(type(c) == "object" && type(c.outbounds) == "array" && length(c.outbounds) > 0 ? 0 : 1);' "$1" >/dev/null 2>&1
}
update_singbox() {
local name target url tmp hdr ua active
name="$1"
safe_name "$name" || return 1
target="$SINGBOX_DIR/$name"
[ -f "$target" ] && [ -r "$target.url" ] || { skipped=$((skipped + 1)); return 0; }
url="$(sed -n '1p' "$target.url")"
[ -n "$url" ] || { skipped=$((skipped + 1)); return 0; }
tmp="$TMP_DIR/clashoo_sb_$$.json"
hdr="$TMP_DIR/clashoo_sb_$$.hdr"
ua="$(subscription_ua "$target")"
if ! download_to "$url" "$tmp" "$hdr" "$ua"; then
failed=$((failed + 1))
log_update "更新失败(${DOWNLOAD_ERROR:-下载失败}):$name"
rm -f "$tmp" "$hdr"
return 1
fi
if ! valid_singbox "$tmp"; then
failed=$((failed + 1))
log_update "更新失败(无效 sing-box 配置):$name"
rm -f "$tmp" "$hdr"
return 1
fi
if cmp -s "$tmp" "$target"; then
update_info "$hdr" "$target"
save_subscription_ua "$target" "$ua"
unchanged=$((unchanged + 1))
rm -f "$tmp" "$hdr"
return 0
fi
if ! mv "$tmp" "$target"; then
failed=$((failed + 1))
rm -f "$hdr"
return 1
fi
update_info "$hdr" "$target"
save_subscription_ua "$target" "$ua"
rm -f "$hdr"
updated=$((updated + 1))
active="$(uci -q get clashoo.config.singbox_active 2>/dev/null)"
[ "$active" = "$name" ] && restart_needed=1
log_update "更新完成:$name"
}
update_all() {
local name url typ url_file
if [ -r "$LIST_FILE" ]; then
while IFS='#' read -r name url typ _rest; do
[ -n "$name" ] && [ -n "$url" ] || continue
update_mihomo "$name" "$url" "$typ" || true
done <"$LIST_FILE"
fi
for url_file in "$SINGBOX_DIR"/*.url; do
[ -f "$url_file" ] || continue
name="$(basename "$url_file" .url)"
update_singbox "$name" || true
done
}
case "$1" in
--all)
update_all
;;
--mihomo)
line="$(awk -F '#' -v n="$2" '$1==n {print; exit}' "$LIST_FILE" 2>/dev/null)"
[ -n "$line" ] || { failed=1; message="subscription not found"; }
if [ -n "$line" ]; then
name="$(printf '%s' "$line" | awk -F '#' '{print $1}')"
url="$(printf '%s' "$line" | awk -F '#' '{print $2}')"
typ="$(printf '%s' "$line" | awk -F '#' '{print $3}')"
update_mihomo "$name" "$url" "$typ" || true
fi
;;
--singbox)
update_singbox "$2" || true
;;
*)
failed=1
message="invalid arguments"
;;
esac
if [ "$restart_needed" = "1" ] && service_running; then
"$SERVICE_CMD" restart >/dev/null 2>&1
fi
finished_at="$(date +%s)"
[ -n "$message" ] || message="updated=$updated unchanged=$unchanged failed=$failed skipped=$skipped"
[ "$record_status" = "1" ] && write_status 0 "$finished_at"
[ "$failed" -eq 0 ]
@@ -1,18 +0,0 @@
#!/bin/sh
STATUS_FILE="${CLASHOO_STATUS_FILE:-/usr/share/clashbackup/subscription_update.status}"
UPDATER="${CLASHOO_SUBSCRIPTION_UPDATER:-/usr/share/clashoo/update/subscription_update.sh}"
SERVICE_CMD="${CLASHOO_SERVICE_CMD:-/etc/init.d/clashoo}"
interval="$(uci -q get clashoo.config.subscription_update_interval 2>/dev/null)"
now="$(date +%s)"
last=0
[ "$(uci -q get clashoo.config.auto_subscription_update 2>/dev/null)" = "1" ] || exit 0
"$SERVICE_CMD" status >/dev/null 2>&1 || exit 0
echo "$interval" | grep -Eq '^[0-9]+$' || interval=72
[ "$interval" -gt 0 ] || interval=72
[ -r "$STATUS_FILE" ] && last="$(sed -n 's/^last_run=//p' "$STATUS_FILE" | head -1)"
echo "$last" | grep -Eq '^[0-9]+$' || last=0
[ $((now - last)) -ge $((interval * 3600)) ] || exit 0
sh "$UPDATER" --all
@@ -1,101 +0,0 @@
#!/bin/sh
set -eu
SUB_FILE="${1:-}"
TEMPLATE_FILE="${2:-}"
OUT_FILE="${3:-}"
TMP_FILE="/tmp/clash_template_merge_$$.yaml"
TMP_FILE2="${TMP_FILE}.norm"
LOG_FILE="/tmp/clash_update.txt"
log() {
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$LOG_FILE"
}
cleanup() {
rm -f "$TMP_FILE" "$TMP_FILE2" >/dev/null 2>&1 || true
}
trap cleanup EXIT INT TERM
[ -n "$SUB_FILE" ] || { log "模板复写失败:缺少订阅文件参数"; exit 1; }
[ -n "$TEMPLATE_FILE" ] || { log "模板复写失败:缺少模板文件参数"; exit 1; }
[ -n "$OUT_FILE" ] || { log "模板复写失败:缺少输出文件参数"; exit 1; }
[ -f "$SUB_FILE" ] || { log "模板复写失败:订阅文件不存在"; exit 1; }
[ -f "$TEMPLATE_FILE" ] || { log "模板复写失败:模板文件不存在"; exit 1; }
if ! command -v yq >/dev/null 2>&1; then
log "模板复写失败:缺少 yq"
exit 1
fi
core_bin=""
for b in mihomo clash-meta clash; do
if command -v "$b" >/dev/null 2>&1; then
core_bin="$b"
break
fi
done
mkdir -p "$(dirname "$OUT_FILE")" >/dev/null 2>&1
log "开始模板复写:$(basename "$SUB_FILE") <- $(basename "$TEMPLATE_FILE")"
export SUB_FILE TEMPLATE_FILE
# template is base; load() forces single-doc; explode anchors first (yq 4.53+ breaks "<<")
yq -n '
(load(strenv(SUB_FILE)) | explode(.)) as $sub |
(load(strenv(TEMPLATE_FILE)) | explode(.)) as $tpl |
(
($tpl * {
"proxies": (((($tpl.proxies // []) + ($sub.proxies // [])) | unique_by(.name))),
"proxy-providers": (($tpl."proxy-providers" // {}) * ($sub."proxy-providers" // {}))
})
| .dns = ((.dns // {}) * {
"proxy-server-nameserver": (
.dns."proxy-server-nameserver"
// $sub.dns."proxy-server-nameserver"
// $sub.dns.nameserver
// ["223.5.5.5", "119.29.29.29"]
)
})
)
' >"$TMP_FILE" 2>/dev/null || {
log "模板复写失败:YAML 合并错误"
exit 1
}
# 兼容第三方模板中使用“直连”字样,统一转换为 Clash 内置 DIRECT。
yq e '
(.. | select(tag == "!!str")) |= sub("^直连$", "DIRECT")
| .rules = ((.rules // []) | map(sub(",直连,", ",DIRECT,") | sub(",直连$", ",DIRECT")))
' "$TMP_FILE" >"$TMP_FILE2" 2>/dev/null || {
log "模板复写失败:直连别名标准化失败"
exit 1
}
mv -f "$TMP_FILE2" "$TMP_FILE" >/dev/null 2>&1 || {
log "模板复写失败:中间文件写入失败"
exit 1
}
yq e '.' "$TMP_FILE" >/dev/null 2>&1 || {
log "模板复写失败:合并结果 YAML 无效"
exit 1
}
if [ -n "$core_bin" ]; then
"$core_bin" -t -f "$TMP_FILE" >/dev/null 2>&1 || {
log "模板复写失败:内核校验不通过"
exit 1
}
fi
mv -f "$TMP_FILE" "$OUT_FILE" >/dev/null 2>&1 || {
log "模板复写失败:写入输出文件失败"
exit 1
}
chmod 644 "$OUT_FILE" >/dev/null 2>&1 || true
log "模板复写完成:$(basename "$OUT_FILE")"
exit 0
@@ -1,5 +0,0 @@
#!/bin/sh
config_name="${1:-$(uci -q get clashoo.config.config_update_name 2>/dev/null)}"
[ -n "$config_name" ] || exit 1
exec sh /usr/share/clashoo/update/subscription_update.sh --mihomo "$config_name"
@@ -1,20 +0,0 @@
#!/bin/sh
UPDATE_LOG="/tmp/clash_update.txt"
STATE_DIR="${CLASHOO_STATE_DIR:-/usr/share/clashbackup}"
log_update() {
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
}
log_update "更新大陆白名单"
sh /usr/share/clashoo/update/update_china_ip.sh >> "$UPDATE_LOG" 2>&1
log_update "更新 GeoIP / GeoSite"
sh /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1
mkdir -p "$STATE_DIR"
tmp="$STATE_DIR/rule_update.status.tmp.$$"
printf 'last_run=%s\n' "$(date +%s)" >"$tmp" && mv -f "$tmp" "$STATE_DIR/rule_update.status"
exit 0
@@ -1,237 +0,0 @@
#!/bin/sh
set -eu
LOG_FILE="${LOG_FILE:-/tmp/clash_update.txt}"
NFT_DIR="${NFT_DIR:-/usr/share/clashoo/nftables}"
TARGET_V4="${NFT_DIR}/geoip_cn.nft"
TARGET_V6="${NFT_DIR}/geoip6_cn.nft"
FW4_SCRIPT="${FW4_SCRIPT:-/usr/share/clashoo/net/fw4.sh}"
PROC_ROOT="${PROC_ROOT:-/proc}"
SINGBOX_BIN_DIR="${SINGBOX_BIN_DIR:-/usr/share/clashoo/bin}"
TMP_ROOT="$(mktemp -d /tmp/clashoo_china_ip.XXXXXX)"
TMP_V4="$TMP_ROOT/china_ip.txt"
TMP_V6="$TMP_ROOT/china_ipv6.txt"
OUT_V4="$TMP_ROOT/geoip_cn.nft"
OUT_V6="$TMP_ROOT/geoip6_cn.nft"
log() {
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOG_FILE"
}
bool_enabled() {
case "$1" in
1|true|TRUE|yes|on) return 0 ;;
*) return 1 ;;
esac
}
download_with_fallback() {
local url="$1"
local output="$2"
local ip
if curl -fsSL --connect-timeout 10 --max-time 120 "$url" -o "$output"; then
return 0
fi
case "$url" in
https://ispip.clang.cn/*)
for ip in 182.247.248.127 103.220.64.183; do
if curl -fsSL --connect-timeout 10 --max-time 120 --resolve "ispip.clang.cn:443:${ip}" "$url" -o "$output"; then
log "DNS 异常,已使用 --resolve(${ip}) 回源下载"
return 0
fi
done
;;
esac
return 1
}
cleanup() {
rm -rf "$TMP_ROOT"
}
trap cleanup EXIT INT TERM
render_nft_set() {
local source_file="$1"
local output_file="$2"
local set_name="$3"
local set_type="$4"
awk -v set_name="$set_name" -v set_type="$set_type" '
function valid_cidr(value, parts, octets, i, prefix) {
if (split(value, parts, "/") != 2 || parts[2] !~ /^[0-9]+$/)
return 0
prefix = parts[2] + 0
if (set_type == "ipv4_addr") {
if (prefix > 32 || split(parts[1], octets, ".") != 4)
return 0
for (i = 1; i <= 4; i++)
if (octets[i] !~ /^[0-9]+$/ || length(octets[i]) > 3 || octets[i] + 0 > 255)
return 0
return 1
}
return set_type == "ipv6_addr" && prefix <= 128 &&
parts[1] ~ /^[0-9A-Fa-f:]+$/ && index(parts[1], ":") > 0
}
BEGIN {
print "set " set_name " {"
print "\ttype " set_type ";"
print "\tflags interval;"
print "\tauto-merge;"
print "\telements = {"
first = 1
}
!/^[[:space:]]*$/ && !/^[[:space:]]*#/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0)
if ($0 == "")
next
if (!valid_cidr($0)) {
if (!invalid)
printf "invalid CIDR at line %d\n", NR > "/dev/stderr"
invalid = 1
next
}
if (!first)
print ","
printf "\t\t%s", $0
first = 0
}
END {
if (!first)
print ""
print "\t}"
print "}"
if (invalid || first)
exit 1
}
' "$source_file" > "$output_file" || return 1
# nft catches malformed IPv6 forms and invalid network prefixes before replacement.
{ printf 'table inet clashoo_validate {\n'; cat "$output_file"; printf '\n}\n'; } |
nft -c -f - >/dev/null 2>&1
}
url4="$(uci -q get clashoo.config.china_ip_url 2>/dev/null || true)"
url6="$(uci -q get clashoo.config.china_ipv6_url 2>/dev/null || true)"
bypass_china="$(uci -q get clashoo.config.bypass_china 2>/dev/null || true)"
bypass_china_ipv6="$(uci -q get clashoo.config.bypass_china_ipv6 2>/dev/null || true)"
[ -n "$bypass_china_ipv6" ] || bypass_china_ipv6="$bypass_china"
[ -n "$url4" ] || url4='https://ispip.clang.cn/all_cn.txt'
[ -n "$url6" ] || url6='https://ispip.clang.cn/all_cn_ipv6.txt'
mkdir -p "$NFT_DIR"
log '开始更新大陆白名单'
download_with_fallback "$url4" "$TMP_V4"
[ -s "$TMP_V4" ] || {
log '大陆 IPv4 白名单下载失败:返回为空'
exit 1
}
changed=0
if ! render_nft_set "$TMP_V4" "$OUT_V4" clashoo_china ipv4_addr; then
log '大陆 IPv4 白名单包含无效 CIDR,保留原文件'
exit 1
fi
if cmp -s "$OUT_V4" "$TARGET_V4"; then
log '大陆 IPv4 白名单内容无变化'
else
mv "$OUT_V4" "$TARGET_V4"
chmod 644 "$TARGET_V4" >/dev/null 2>&1 || true
changed=1
log '大陆 IPv4 白名单更新完成'
fi
if download_with_fallback "$url6" "$TMP_V6"; then
if [ -s "$TMP_V6" ]; then
if ! render_nft_set "$TMP_V6" "$OUT_V6" clashoo_china6 ipv6_addr; then
log '大陆 IPv6 白名单包含无效 CIDR,保留原文件'
exit 1
fi
if cmp -s "$OUT_V6" "$TARGET_V6"; then
log '大陆 IPv6 白名单内容无变化'
else
mv "$OUT_V6" "$TARGET_V6"
chmod 600 "$TARGET_V6" >/dev/null 2>&1 || true
changed=1
log '大陆 IPv6 白名单更新完成'
fi
else
log '大陆 IPv6 白名单下载为空,保留原文件'
fi
else
log '大陆 IPv6 白名单下载失败,保留原文件'
fi
instance_pid() {
local service="$1"
local instance="${2:-}"
local filter
if [ -n "$instance" ]; then
filter="@.${service}.instances.${instance}.pid"
else
filter="@[\"${service}\"].instances.*.pid"
fi
ubus call service list "{\"name\":\"$service\",\"verbose\":true}" 2>/dev/null |
jsonfilter -e "$filter" 2>/dev/null | head -n1
}
pid_running() {
[ -n "$1" ] && [ -d "$PROC_ROOT/$1" ]
}
singbox_pid_owns_private_binary() {
local pid="$1" exe cmdline
pid_running "$pid" || return 1
exe="$(readlink -f "$PROC_ROOT/$pid/exe" 2>/dev/null)"
case "$exe" in
"$SINGBOX_BIN_DIR/sing-box-stable"|"$SINGBOX_BIN_DIR/sing-box-alpha") return 0 ;;
esac
cmdline="$(tr '\000' ' ' < "$PROC_ROOT/$pid/cmdline" 2>/dev/null)"
case "$cmdline" in
"$SINGBOX_BIN_DIR/sing-box-stable"|"$SINGBOX_BIN_DIR/sing-box-stable "*|"$SINGBOX_BIN_DIR/sing-box-alpha"|"$SINGBOX_BIN_DIR/sing-box-alpha "*) return 0 ;;
esac
return 1
}
selected_core_running() {
local name pid
if [ "$(uci -q get clashoo.config.core_type 2>/dev/null)" = "singbox" ]; then
name="clashoo"
pid="$(instance_pid "$name" singbox)"
singbox_pid_owns_private_binary "$pid"
return $?
else
name="clashoo"
fi
pid="$(instance_pid "$name")"
pid_running "$pid"
}
firewall_managed() {
bool_enabled "$(uci -q get clashoo.config.enable 2>/dev/null)" || return 1
[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ] && return 1
selected_core_running
}
if [ "$changed" -eq 0 ]; then
log '白名单内容未变,跳过防火墙重载'
elif ! firewall_managed; then
log '未接管防火墙(服务未运行或仅内核模式),仅更新白名单文件'
elif bool_enabled "$bypass_china" || bool_enabled "$bypass_china_ipv6"; then
if "$FW4_SCRIPT" apply >/dev/null 2>&1; then
log "大陆白名单规则已重载(IPv4=${bypass_china:-0},IPv6=${bypass_china_ipv6:-0})"
else
log '大陆白名单规则重载失败'
exit 1
fi
else
log '大陆 IPv4/IPv6 绕过均未启用,仅更新白名单文件'
fi
log '大陆白名单更新流程完成'
@@ -1,91 +0,0 @@
--- a/dns/middleware.go
+++ b/dns/middleware.go
@@ -12,6 +12,7 @@ import (
icontext "github.com/metacubex/mihomo/context"
"github.com/metacubex/mihomo/log"
+ M "github.com/metacubex/sing/common/metadata"
D "github.com/miekg/dns"
)
@@ -152,6 +153,9 @@ func withFakeIP(skipper *fakeip.Skipper,
q := r.Question[0]
host := strings.TrimRight(q.Name, ".")
+ if !M.IsDomainName(host) {
+ return next(ctx, r)
+ }
if skipper.ShouldSkipped(host) {
return next(ctx, r)
}
--- /dev/null
+++ b/dns/middleware_test.go
@@ -0,0 +1,68 @@
+package dns
+
+import (
+ "context"
+ "net/netip"
+ "testing"
+
+ "github.com/metacubex/mihomo/component/fakeip"
+ icontext "github.com/metacubex/mihomo/context"
+
+ D "github.com/miekg/dns"
+ "github.com/stretchr/testify/require"
+)
+
+func TestWithFakeIPSkipsInvalidDomain(t *testing.T) {
+ pool, err := fakeip.New(fakeip.Options{
+ IPNet: netip.MustParsePrefix("198.18.0.1/16"),
+ Size: 10,
+ })
+ require.NoError(t, err)
+
+ downstreamCalled := false
+ next := func(_ *icontext.DNSContext, request *D.Msg) (*D.Msg, error) {
+ downstreamCalled = true
+ response := new(D.Msg)
+ response.SetReply(request)
+ return response, nil
+ }
+
+ request := new(D.Msg)
+ request.SetQuestion("n-relay-ipc-txc-nj-00.tplinkcloud.com.cn\\152.", D.TypeA)
+ response, err := withFakeIP(&fakeip.Skipper{}, pool, nil, 1)(next)(icontext.NewDNSContext(context.Background()), request)
+
+ require.NoError(t, err)
+ require.True(t, downstreamCalled)
+ require.Empty(t, response.Answer)
+ _, mapped := pool.LookBack(netip.MustParseAddr("198.18.0.4"))
+ require.False(t, mapped)
+}
+
+func TestWithFakeIPKeepsValidDomain(t *testing.T) {
+ pool, err := fakeip.New(fakeip.Options{
+ IPNet: netip.MustParsePrefix("198.18.0.1/16"),
+ Size: 10,
+ })
+ require.NoError(t, err)
+
+ downstreamCalled := false
+ next := func(_ *icontext.DNSContext, request *D.Msg) (*D.Msg, error) {
+ downstreamCalled = true
+ response := new(D.Msg)
+ response.SetReply(request)
+ return response, nil
+ }
+
+ request := new(D.Msg)
+ request.SetQuestion("n-relay-ipc-txc-nj-00.tplinkcloud.com.cn.", D.TypeA)
+ dnsCtx := icontext.NewDNSContext(context.Background())
+ response, err := withFakeIP(&fakeip.Skipper{}, pool, nil, 1)(next)(dnsCtx, request)
+
+ require.NoError(t, err)
+ require.False(t, downstreamCalled)
+ require.Len(t, response.Answer, 1)
+ require.Equal(t, icontext.DNSTypeFakeIP, dnsCtx.Type())
+ host, mapped := pool.LookBack(netip.MustParseAddr("198.18.0.4"))
+ require.True(t, mapped)
+ require.Equal(t, "n-relay-ipc-txc-nj-00.tplinkcloud.com.cn", host)
+}
-78
View File
@@ -1,78 +0,0 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=filebrowser-q
PKG_VERSION:=1.5.6-stable
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
PKG_BUILD_DIR:=$(BUILD_DIR)/filebrowser-$(PKG_VERSION)
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=kiddin9
PKG_BUILD_DEPENDS:=golang/host node/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/gtsteffaniak/filebrowser/backend
GO_PKG_BUILD_DIR:=$(PKG_BUILD_DIR)/backend
GO_PKG_LDFLAGS_X:= \
$(GO_PKG)/internal/version.Version=v$(PKG_VERSION) \
$(GO_PKG)/internal/version.CommitSHA=$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/filebrowser-q
SECTION:=utils
CATEGORY:=Utilities
SUBMENU:=Filesystem
TITLE:=FileBrowser Quantum - Modern Web File Manager
URL:=https://github.com/gtsteffaniak/filebrowser
DEPENDS:=$(GO_ARCH_DEPENDS)
endef
define Package/filebrowser-q/description
FileBrowser Quantum provides a modern, responsive web-based file management
interface with multi-source, real-time search, and enhanced preview features.
endef
define Package/filebrowser-q/conffiles
/etc/filebrowser-q/
/etc/config/filebrowser-q
endef
define Build/Prepare
$(call Build/Prepare/Default)
endef
define Build/Compile
( \
pushd $(PKG_BUILD_DIR)/frontend && \
npm install && \
npm run build ; \
)
( \
cd $(PKG_BUILD_DIR)/backend && \
$(GO_PKG_VARS) \
go build \
-trimpath \
-ldflags="-w -s" \
-o $(PKG_BUILD_DIR)/filebrowser . ; \
)
endef
define Package/filebrowser-q/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/filebrowser $(1)/usr/bin/filebrowser-q
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser-q
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser-q
endef
$(eval $(call BuildPackage,filebrowser-q))
-5
View File
@@ -1,5 +0,0 @@
config filebrowser 'config'
option enabled '0'
option listen_port '8787'
option root_path '/'
-67
View File
@@ -1,67 +0,0 @@
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=99
CONF="filebrowser-q"
PROG="/usr/bin/filebrowser-q"
CONF_PATH="/etc/filebrowser-q/config.yaml"
DB_PATH="/etc/filebrowser-q/database.db"
start_service() {
config_load "$CONF"
local enabled
config_get_bool enabled "config" "enabled" "0"
[ "$enabled" -eq "1" ] || return 1
local listen_port root_path root_name
config_get listen_port "config" "listen_port" "8787"
config_get root_path "config" "root_path" "/"
root_name=""
[ "$root_path" = "/" ] && root_name="root"
if [ ! -f "$CONF_PATH" ]; then
mkdir -p "$(dirname "$CONF_PATH")"
cat <<EOF > "$CONF_PATH"
server:
port: $listen_port
database: "$DB_PATH"
sources:
- path: "$root_path"
name: "$root_name"
config:
defaultEnabled: true
auth:
adminUsername: "admin"
adminPassword: "admin"
userDefaults:
ui:
locale: "zhCN"
EOF
else
grep -q " name:" "$CONF_PATH" || sed -i "s,.*- path:.*,&\n name: \"\"," "$CONF_PATH"
sed -e "s/ port:.*/ port: $listen_port/" \
-e "s, - path:.*, - path: \"$root_path\"," \
-e "s/ name: \".*\"/ name: \"$root_name\"/" \
-i "$CONF_PATH"
fi
procd_open_instance
procd_set_param command "$PROG"
procd_append_param command -c "$CONF_PATH"
procd_set_param limits core="unlimited"
procd_set_param limits nofile="1000000 1000000"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "$CONF"
}
-39
View File
@@ -1,39 +0,0 @@
#
# Copyright (C) 2015-2016 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v3.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=geo2txt
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/sbwml/geo2txt/tar.gz/$(PKG_VERSION)?
PKG_HASH:=b777d9c67b36cd8ce68a0555200fae85751435c888364c921a361062ffee4409
PKG_LICENSE:=GPL-3.0
PKG_LICENSE_FILE:=LICENSE
PKG_MAINTAINER:=sbwml <admin@cooluc.com>
include $(INCLUDE_DIR)/package.mk
define Package/geo2txt
SECTION:=utils
CATEGORY:=Utilities
TITLE:=V2ray geosite & geoip unpacking CLI tool
URL:=https://github.com/sbwml/geo2txt
endef
define Package/quickfile/description
A lightweight pure C utility to unpack v2ray geosite.dat and geoip.dat.
endef
define Package/geo2txt/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/geo2txt $(1)/usr/bin/geo2txt
endef
$(eval $(call BuildPackage,geo2txt))
-127
View File
@@ -1,127 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2026 kenzok8
include $(TOPDIR)/rules.mk
PKG_NAME:=honk
PKG_VERSION:=2026.11.03
PKG_RELEASE:=1
# Pinned honk-core revision (upstream daeuniverse/honk main), cross-compiled by
# ci/build-core.sh. Every release run publishes its core archives under an
# immutable source-<run_id> tag; the tag and SHA-256 values below are the exact
# assets behind CORE_COMMIT, so a plain feed build downloads and verifies them
# without any generated file. A staged run may still override them through
# honk/generated-stage.mk, which is generated and never committed.
CORE_COMMIT:=77176ff8e8cc2b75385a7030dc376536c382574a
# Immutable source asset tag holding this pin's core archives.
HONK_SOURCE_TAG?=source-41594075839
HONK_SOURCE_URL?=https://github.com/kenzok8/openwrt-honk/releases/download/$(HONK_SOURCE_TAG)
-include $(CURDIR)/generated-stage.mk
HONK_ASSET_X86_64?=honk-core-$(CORE_COMMIT)-x86_64.tar.gz
HONK_ASSET_AARCH64?=honk-core-$(CORE_COMMIT)-aarch64.tar.gz
HONK_ASSET_ARMV7?=honk-core-$(CORE_COMMIT)-armv7.tar.gz
HONK_HASH_X86_64?=ad4801757f8b47505aa99cbdc47e4d606b8afe1a6269547d8073f68c20904a0e
HONK_HASH_AARCH64?=91a9a9929ec32d24de3c105f2a51d83a4975913a68e1446e57c2b1faf63adce6
HONK_HASH_ARMV7?=a5b288c677698a44cc45084baf0bb67b1fb2b9ca624744a08d7a469093fc9870
# ARCH_PACKAGES is the package arch (e.g. arm_cortex-a7_neon-vfpv4);
# ARCH is the CPU arch (x86_64/aarch64/arm) and coincides for 64-bit.
ifeq ($(ARCH_PACKAGES),x86_64)
HONK_ASSET:=$(HONK_ASSET_X86_64)
HONK_HASH:=$(HONK_HASH_X86_64)
else ifeq ($(filter aarch64%,$(ARCH_PACKAGES)),$(ARCH_PACKAGES))
HONK_ASSET:=$(HONK_ASSET_AARCH64)
HONK_HASH:=$(HONK_HASH_AARCH64)
else ifeq ($(filter arm_%,$(ARCH_PACKAGES)),$(ARCH_PACKAGES))
HONK_ASSET:=$(HONK_ASSET_ARMV7)
HONK_HASH:=$(HONK_HASH_ARMV7)
endif
PKG_LICENSE:=AGPL-3.0-only
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=kenzok8
PKG_SOURCE:=$(HONK_ASSET)
PKG_SOURCE_URL?=$(HONK_SOURCE_URL)/
PKG_HASH:=$(HONK_HASH)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk
define Package/honk
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
TITLE:=Honk transparent proxy core
URL:=https://github.com/daeuniverse/honk
# 不直接依赖 libubox:包名带 ABI 日期戳,会锁死固件;脚本只用 jshn 提供的 jshn.sh。
DEPENDS:=@(x86_64||aarch64||arm) +ca-bundle +ip-full +ubus +kmod-sched-core +kmod-sched-bpf +kmod-veth +kmod-nft-queue +jsonfilter +jshn +sqlite3-cli +usign +coreutils-stat +v2ray-geoip +v2ray-geosite +@KERNEL_DEBUG_INFO_BTF
endef
define Package/honk/description
Honk is a transparent proxy core for OpenWrt (x86_64, aarch64, armv7).
endef
define Package/honk/conffiles
/etc/config/honk
/etc/honk/config.dae
/etc/honk/system.dae
/etc/honk/manual-config.dae
/etc/honk/managed-config.dae
endef
define Build/Prepare
mkdir -p $(PKG_BUILD_DIR)
$(TAR) -xzf $(DL_DIR)/$(PKG_SOURCE) -C $(PKG_BUILD_DIR) --strip-components=1
endef
define Build/Compile
endef
define Package/honk/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/honk-core $(1)/usr/bin/honk-core
$(INSTALL_DIR) $(1)/usr/share/licenses/honk
$(INSTALL_DATA) $(PKG_BUILD_DIR)/LICENSE $(1)/usr/share/licenses/honk/LICENSE
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/honk.config $(1)/etc/config/honk
$(INSTALL_DIR) $(1)/etc/honk
$(INSTALL_CONF) $(CURDIR)/files/config.dae $(1)/etc/honk/config.dae
$(INSTALL_CONF) $(CURDIR)/files/system.dae $(1)/etc/honk/system.dae
chmod 0600 $(1)/etc/honk/system.dae
$(INSTALL_DIR) $(1)/etc/honk/state
chmod 0700 $(1)/etc/honk/state
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/honk.init $(1)/etc/init.d/honk
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) $(CURDIR)/files/90-honk-boot $(1)/etc/uci-defaults/90-honk
$(INSTALL_DIR) $(1)/usr/share/honk
$(INSTALL_DATA) $(CURDIR)/files/config.dae $(1)/usr/share/honk/default-config.dae
# Provenance is only generated by a source build; a plain feed build has
# no such file and must still install cleanly.
if [ -f $(CURDIR)/generated-provenance.json ]; then \
$(INSTALL_DATA) $(CURDIR)/generated-provenance.json $(1)/usr/share/honk/provenance.json; \
fi
$(INSTALL_BIN) $(CURDIR)/files/control.sh $(1)/usr/share/honk/control.sh
$(INSTALL_BIN) $(CURDIR)/files/lock.sh $(1)/usr/share/honk/lock.sh
$(INSTALL_BIN) $(CURDIR)/files/lifecycle.sh $(1)/usr/share/honk/lifecycle.sh
$(INSTALL_BIN) $(CURDIR)/files/maintenance.sh $(1)/usr/share/honk/maintenance.sh
$(INSTALL_BIN) $(CURDIR)/files/job.sh $(1)/usr/share/honk/job.sh
$(INSTALL_BIN) $(CURDIR)/files/update.sh $(1)/usr/share/honk/update.sh
$(INSTALL_BIN) $(CURDIR)/files/redact-log.sh $(1)/usr/share/honk/redact-log.sh
$(INSTALL_BIN) $(CURDIR)/files/log-level-filter.sh $(1)/usr/share/honk/log-level-filter.sh
$(INSTALL_DATA) $(CURDIR)/files/log-lines.sh $(1)/usr/share/honk/log-lines.sh
endef
$(eval $(call BuildPackage,honk))
-11
View File
@@ -1,11 +0,0 @@
#!/bin/sh
# 开机自启是内置行为:装上就 enable,是否真正启动由 honk.main.enabled 决定,
# 不再提供单独的 boot_enabled 开关。
/etc/init.d/honk enable
# 配置模式已取消:只清遗留键,活动配置与启停状态一概不动。
legacy_mode=$(uci -q get honk.main.config_mode 2>/dev/null)
if [ -n "$legacy_mode" ]; then
uci -q delete honk.main.config_mode && uci -q commit honk
fi
exit 0
-34
View File
@@ -1,34 +0,0 @@
include {
system.dae
}
group {
proxy {
policy: min_moving_avg
}
}
dns {
upstream {
default: 'https://223.5.5.5/dns-query?tls_server_name=dns.alidns.com' -> direct
remote: 'https://8.8.8.8/dns-query?tls_server_name=dns.google' -> proxy
}
routing {
request {
qname(geosite: private) -> asis
qname(geosite: cn) -> default
fallback: remote
}
}
}
routing {
dip(224.0.0.0/3, 'ff00::/8') -> direct
# 路由器自身地址直连,管理面板不被代理;服务启动时按本机地址重写下面区块
# honk:local-direct-begin
# honk:local-direct-end
dip(geoip: private) && !dport(53) -> direct(must)
dip(geoip: cn) && !dport(53) -> direct(must)
domain(geosite: cn) -> direct
fallback: direct
}
-979
View File
@@ -1,979 +0,0 @@
#!/bin/sh
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
HONK_CORE=/usr/bin/honk-core
HONK_MAINT_DIR=/etc/.honk-maintenance
HONK_MAINT_JOURNAL=$HONK_MAINT_DIR/journal
HONK_MAINT_CAP_FILE=$HONK_MAINT_DIR/capability
. /lib/functions.sh
. /usr/share/honk/lock.sh
. /usr/share/honk/lifecycle.sh
fail() {
echo "honk: $*" >&2
return 1
}
load_settings() {
config_load honk
config_get_bool enabled main enabled 0
config_get_bool boot_enabled main boot_enabled 0
config_get_bool initialized main initialized 0
config_get config_file main config_file /etc/honk/config.dae
config_get listen_port main listen_port 9527
config_get_bool clash_api_enabled main clash_api_enabled 0
config_get clash_api_port main clash_api_port 9090
config_get clash_api_secret main clash_api_secret ''
config_get lan_network main lan_network lan
settings_owner=$(config_owner "$config_file")
}
valid_clash_api() {
[ "$clash_api_enabled" = 1 ] || return 0
case "$clash_api_port" in ''|*[!0-9]*) return 1 ;; esac
[ "$clash_api_port" -ge 1024 ] && [ "$clash_api_port" -le 65535 ] || return 1
[ "$clash_api_port" != "$listen_port" ] || return 1
case "$clash_api_secret" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
[ "${#clash_api_secret}" -ge 6 ] && [ "${#clash_api_secret}" -le 128 ]
}
valid_network_name() {
case "$lan_network" in
''|*[!A-Za-z0-9_.-]*) return 1 ;;
*) return 0 ;;
esac
}
module_loaded() {
[ -d "/sys/module/$1" ] || grep -q "^$1 " /proc/modules
}
module_available() {
module_loaded "$1" && return 0
module_path=$(printf '%s' "$1" | tr '_' '-')
module_dir=/lib/modules/$(uname -r)
[ -r "$module_dir/modules.builtin" ] && grep -Eq "/${1}\.ko$|/${module_path}\.ko$" "$module_dir/modules.builtin" && return 0
find "$module_dir" -type f \( -name "$1.ko" -o -name "$1.ko.gz" -o -name "$1.ko.xz" -o -name "$1.ko.zst" \
-o -name "$module_path.ko" -o -name "$module_path.ko.gz" -o -name "$module_path.ko.xz" -o -name "$module_path.ko.zst" \) -print 2>/dev/null | grep -q .
}
system_config_readonly() {
permissions=$(ls -ldn /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
[ "$permissions" = '-rw-------' ] && return 0
[ "$permissions" = '-r--r--r--' ] && ! grep -q 'clash_api {' /etc/honk/system.dae
}
no_honk_resources() {
# honk-core removes dae0/dae0peer and the daens netns when it exits cleanly. A
# crash or SIGKILL leaves them behind, and preflight then refuses every later
# start, so the service can never come back without hand surgery. Reclaim them
# here, but only when no core is running to own them.
if ! pidof honk-core >/dev/null 2>&1; then
for stale_link in dae0 dae0peer; do
ip link show "$stale_link" >/dev/null 2>&1 && ip link del "$stale_link" >/dev/null 2>&1
done
if ip netns list 2>/dev/null | awk '{print $1}' | grep -qx daens; then
ip netns del daens >/dev/null 2>&1
fi
fi
for link in dae0 dae0peer; do
ip link show "$link" >/dev/null 2>&1 && { fail "$link 已存在(上次没有正常退出),请先停止服务或执行系统恢复"; return 1; }
done
ip netns list 2>/dev/null | awk '{print $1}' | grep -qx daens && { fail "daens 网络命名空间已存在,请先停止服务或执行系统恢复"; return 1; }
return 0
}
check_port() {
checked_port=${1:-$listen_port}
if command -v netstat >/dev/null 2>&1; then
netstat -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)${checked_port}$" && { fail "监听端口已被占用,请换一个端口"; return 1; }
elif command -v ss >/dev/null 2>&1; then
ss -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)${checked_port}$" && { fail "监听端口已被占用,请换一个端口"; return 1; }
else
fail "缺少 netstat/ss,无法检查端口占用(装 ip-full 或 net-tools)"
return 1
fi
return 0
}
check_network() {
valid_network_name || { fail "LAN 网络名包含非法字符"; return 1; }
command -v ubus >/dev/null 2>&1 || { fail "ubus 不可用,无法校验网络设置"; return 1; }
ubus call "network.interface.$lan_network" status >/dev/null 2>&1 || { fail "选择的 LAN 网络在 netifd 里不存在,请在服务设置里重新选择"; return 1; }
}
# 本机 LAN/WAN 设备地址(跳过链路本地),与 honk 的 local_direct_cidrs 对齐
local_direct_addresses() {
list=''
. /lib/functions/network.sh
for net in "$lan_network" wan; do
network_get_device dev "$net" || continue
case "$dev" in ''|*[!A-Za-z0-9_.-]*) continue ;; esac
for addr in $(ip -o addr show dev "$dev" 2>/dev/null | awk '/inet6? / { split($4, a, "/"); print a[1] }'); do
case "$addr" in fe80:*|169.254.*) continue ;; esac
case "$addr" in ''|*[!0-9a-fA-F:.]*) continue ;; esac
[ "${#addr}" -le 45 ] || continue
case " $list " in *" $addr "*) continue ;; esac
list="${list:+$list, }$addr"
done
done
printf '%s\n' "$list"
}
# 把标记区块换成当前本机地址,honk 的 LAN self-protection 检查才认
sync_local_direct() {
begin=' # honk:local-direct-begin'
end=' # honk:local-direct-end'
grep -qxF "$begin" /etc/honk/config.dae || return 0
grep -qxF "$end" /etc/honk/config.dae || return 0
ips=$(local_direct_addresses)
[ -n "$ips" ] && rule=" dip($ips) && !dport(53) -> direct(must)" || rule=''
# honk-core 只认 .dae 后缀,临时文件必须带它
temp=/etc/honk/.config.$$.dae
( umask 077; : > "$temp" ) || return 1
awk -v begin="$begin" -v end="$end" -v rule="$rule" '
$0 == begin { print; if (rule != "") print rule; skip = 1; next }
skip && $0 == end { print; skip = 0; next }
skip { next }
{ print }
' /etc/honk/config.dae > "$temp" || { rm -f "$temp"; return 1; }
cmp -s "$temp" /etc/honk/config.dae && { rm -f "$temp"; return 0; }
"$HONK_CORE" config validate --config "$temp" >/dev/null 2>&1 || { rm -f "$temp"; return 1; }
chmod 0600 "$temp"
mv -f "$temp" /etc/honk/config.dae || { rm -f "$temp"; return 1; }
}
check_config() {
[ "$config_file" = /etc/honk/config.dae ] || { fail "配置路径必须为 /etc/honk/config.dae"; return 1; }
[ -f /etc/honk/config.dae ] && [ ! -L /etc/honk/config.dae ] || { fail "/etc/honk/config.dae 不是普通文件(可能被替换成软链)"; return 1; }
"$HONK_CORE" config validate --config /etc/honk/config.dae >/dev/null || { fail "config.dae 未通过静态校验,请先修正配置"; return 1; }
[ -r /usr/share/doona/index.html ] || { fail "缺少 Doona 静态文件(/usr/share/doona/index.html),请重新安装 luci-app-honk"; return 1; }
[ "$settings_owner" = system ] || return 0
[ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] || { fail "/etc/honk/system.dae 不是普通文件(可能被替换成软链)"; return 1; }
system_config_readonly || { fail "/etc/honk/system.dae 权限不对,应为 0600"; return 1; }
case "$listen_port" in
''|*[!0-9]*) fail "监听端口必须是数字"; return 1 ;;
esac
[ "$listen_port" -ge 1024 ] && [ "$listen_port" -le 65535 ] || { fail "监听端口必须在 1024-65535 之间"; return 1; }
valid_clash_api || { fail "Clash API 设置不合法(端口 1024-65535,密钥 6-128 位)"; return 1; }
}
check_kernel() {
kernel=$(uname -r | cut -d- -f1)
major=$(printf '%s' "$kernel" | cut -d. -f1)
minor=$(printf '%s' "$kernel" | cut -d. -f2)
[ "$major" -gt 6 ] || { [ "$major" -eq 6 ] && [ "$minor" -ge 12 ]; } || { fail "内核版本过低,需要 Linux 6.12 或更新"; return 1; }
[ -r /sys/kernel/btf/vmlinux ] || { fail "内核缺少 BTF(/sys/kernel/btf/vmlinux),请换用带 BTF 的固件"; return 1; }
[ -d /sys/fs/bpf ] && grep -q ' /sys/fs/bpf bpf ' /proc/mounts || { fail "bpffs 未挂载到 /sys/fs/bpf,请检查内核或手工挂载"; return 1; }
command -v ip >/dev/null 2>&1 || { fail "缺少 ip 命令(装 ip-full)"; return 1; }
command -v modprobe >/dev/null 2>&1 || { fail "缺少 modprobe 命令(装 kmod)"; return 1; }
[ -r /sys/kernel/btf/vmlinux ] || { fail "内核缺少 BTF(/sys/kernel/btf/vmlinux),请换用带 BTF 的固件"; return 1; }
for module in sch_ingress cls_bpf veth nft_queue nfnetlink_queue; do
module_available "$module" || { fail "缺少内核模块 $module,请确认 kmod-sched-core / kmod-sched-bpf / kmod-veth / kmod-nft-queue 已安装"; return 1; }
done
}
check_conflicts() {
for process in dae daed clashoo mihomo sing-box; do
pidof "$process" >/dev/null 2>&1 && { fail "检测到冲突进程 $process 正在运行,请先停掉它(多代理同时开会互相抢流量)"; return 1; }
done
for service in dae daed clashoo mihomo sing-box; do
if [ -x "/etc/init.d/$service" ] && "/etc/init.d/$service" running >/dev/null 2>&1; then
fail "检测到冲突服务 $service 正在运行,请先停用它(多代理同时开会互相抢流量)"
return 1
fi
done
}
maintenance_value() {
awk -F= -v key="$1" '$1 == key { value = substr($0, length($1) + 2); count++ } END { if (count == 1) print value; else exit 1 }' "$2"
}
proc_starttime() {
[ -r "/proc/$1/stat" ] || return 1
awk '{ sub(/^.*\) /, ""); split($0, fields, /[[:space:]]+/); if (fields[20] ~ /^[0-9]+$/) print fields[20]; else exit 1 }' "/proc/$1/stat" 2>/dev/null
}
protected_root_file() {
[ -f "$1" ] && [ ! -L "$1" ] || return 1
protected_permissions=$(ls -ln "$1" 2>/dev/null | awk 'NR == 1 { print $1 ":" $3 ":" $4 }')
[ "$protected_permissions" = '-rw-------:0:0' ]
}
consume_maintenance_capability() {
[ -n "${HONK_MAINT_CAP:-}" ] || return 1
[ -d "$HONK_MAINT_DIR" ] && [ ! -L "$HONK_MAINT_DIR" ] || return 1
protected_root_file "$HONK_MAINT_JOURNAL" && protected_root_file "$HONK_MAINT_CAP_FILE" || return 1
awk -F= 'BEGIN { split("format nonce txn_id phase owner_pid owner_starttime", keys, " ") } NR > 6 || index($0, "=") == 0 || $1 != keys[NR] { exit 1 } END { if (NR != 6) exit 1 }' "$HONK_MAINT_CAP_FILE" || return 1
consumed_cap=$HONK_MAINT_DIR/.capability-consumed.$$
[ ! -e "$consumed_cap" ] || return 1
mv "$HONK_MAINT_CAP_FILE" "$consumed_cap" 2>/dev/null || return 1
# Chain with any existing EXIT trap instead of overwriting it.
_prev_exit_trap=$(trap -p EXIT | sed "s/^trap -- '//;s/' EXIT$//")
trap 'rm -f "$consumed_cap"; [ -n "$_prev_exit_trap" ] && eval "$_prev_exit_trap"' 0
cap_format=$(maintenance_value format "$consumed_cap") || return 1
cap_nonce=$(maintenance_value nonce "$consumed_cap") || return 1
cap_txn=$(maintenance_value txn_id "$consumed_cap") || return 1
cap_phase=$(maintenance_value phase "$consumed_cap") || return 1
cap_pid=$(maintenance_value owner_pid "$consumed_cap") || return 1
cap_starttime=$(maintenance_value owner_starttime "$consumed_cap") || return 1
journal_format=$(maintenance_value format "$HONK_MAINT_JOURNAL") || return 1
journal_operation=$(maintenance_value operation "$HONK_MAINT_JOURNAL") || return 1
journal_phase=$(maintenance_value phase "$HONK_MAINT_JOURNAL") || return 1
journal_txn=$(maintenance_value txn_id "$HONK_MAINT_JOURNAL") || return 1
case "$cap_nonce:$cap_txn:$cap_pid:$cap_starttime" in *[!0-9a-f:]*|::*|:|'' ) return 1 ;; esac
case "$cap_nonce" in *[!0-9a-f]*|'') return 1 ;; esac
[ "${#cap_nonce}" -eq 32 ] && [ "${#cap_txn}" -eq 32 ] || return 1
[ "$cap_format" = 1 ] && [ "$journal_format" = 1 ] || return 1
case "$cap_pid:$cap_starttime" in *[!0-9:]*) return 1 ;; esac
[ "$cap_pid" -gt 1 ] && [ "$cap_starttime" -gt 0 ] || return 1
[ "$HONK_MAINT_CAP" = "$cap_nonce" ] && [ "$cap_txn" = "$journal_txn" ] || return 1
case "$cap_phase:$journal_operation:$journal_phase" in
candidate_health_start:restore:new_installed|candidate_health_start:import_apply:new_installed|rollback_old_start:restore:rollback_old_start|rollback_old_start:reset:rollback_old_start|rollback_old_start:import_apply:rollback_old_start) ;;
*) return 1 ;;
esac
read -r lock_pid lock_starttime < "$HONK_LOCK_DIR/pid" || return 1
[ "$lock_pid:$lock_starttime" = "$cap_pid:$cap_starttime" ] || return 1
[ "$(proc_starttime "$cap_pid")" = "$cap_starttime" ] || return 1
return 0
}
maintenance_start_guard() {
if [ -e "$HONK_MAINT_JOURNAL" ] || [ -L "$HONK_MAINT_JOURNAL" ]; then
consume_maintenance_capability || { fail "maintenance_recovery_required"; return 1; }
elif [ -n "${HONK_MAINT_CAP:-}" ]; then
fail "maintenance_capability_without_journal"
return 1
fi
if lock_owner_invalid || honk_lock_is_stale; then
# The recorded owner is gone, so nothing is in flight. Reclaim the lock
# instead of refusing every later start: a caller that timed out cannot
# release it, and leaving it wedges the service until someone SSHes in.
# A live lock still blocks below through honk_lock_acquire.
logger -t honk "reclaiming stale operation lock" 2>/dev/null || :
rm -rf "$HONK_LOCK_DIR"
fi
}
lock_owner_invalid() {
[ -d "$HONK_LOCK_DIR" ] || return 1
lock_owner=$(cat "$HONK_LOCK_DIR/pid" 2>/dev/null)
# pid file now stores "pid starttime"; only the leading PID matters here.
lock_owner=${lock_owner%% *}
case "$lock_owner" in ''|*[!0-9]*) return 0 ;; esac
return 1
}
preflight() {
load_settings
[ "$enabled" = 1 ] && [ "$initialized" = 1 ] || { fail "请先在 LuCI 里启用并完成初始化"; return 1; }
# 只有 system.dae 负责这些字段时才检查 UCI 端口与网络;完整配置自行定义时不碰。
if [ "$settings_owner" = system ]; then
maintenance_start_guard && check_config && check_network && check_kernel && no_honk_resources && check_conflicts && check_port && {
[ "$clash_api_enabled" = 0 ] || check_port "$clash_api_port"
}
return $?
fi
maintenance_start_guard && check_config && check_kernel && no_honk_resources && check_conflicts
}
prepare_runtime() {
preflight || return 1
for module in sch_ingress cls_bpf veth nft_queue nfnetlink_queue; do
module_loaded "$module" || modprobe "$module" || { fail "无法加载内核模块 $module,请确认对应 kmod 包已安装"; return 1; }
done
mkdir -p /etc/honk/state || { fail "无法创建状态目录(/etc/honk/state),请检查磁盘空间与权限"; return 1; }
chmod 0700 /etc/honk/state || return 1
}
valid_username() {
case "$1" in
''|*[!A-Za-z0-9_.-]*) return 1 ;;
esac
[ "$(printf '%s' "$1" | wc -c)" -le 64 ]
}
valid_password() {
password_bytes=$(printf '%s' "$1" | wc -c)
[ "$password_bytes" -ge 8 ] && [ "$password_bytes" -le 512 ]
}
read_credentials() {
json_file=$(mktemp /tmp/honk-credentials.XXXXXX) || return 1
chmod 0600 "$json_file" || { rm -f "$json_file"; return 1; }
cat > "$json_file" || { rm -f "$json_file"; return 1; }
json_bytes=$(wc -c < "$json_file")
[ "$json_bytes" -le 2048 ] || { rm -f "$json_file"; return 1; }
grep -Fq '\u0000' "$json_file" && { rm -f "$json_file"; return 1; }
newline='
'
username_raw=$(jsonfilter -i "$json_file" -e '@.username' 2>/dev/null; printf '__HNK_END__')
password_raw=$(jsonfilter -i "$json_file" -e '@.password' 2>/dev/null; printf '__HNK_END__')
username=${username_raw%__HNK_END__}
password=${password_raw%__HNK_END__}
username=${username%"$newline"}
password=${password%"$newline"}
rm -f "$json_file"
[ -n "$username" ] && [ -n "$password" ] || return 1
valid_username "$username" && valid_password "$password"
}
network_settings() {
. /lib/functions/network.sh
network_get_device lan_device "$lan_network" || return 1
network_get_ipaddr lan_ip "$lan_network" || return 1
case "$lan_device" in
''|*[!A-Za-z0-9_.-]*) return 1 ;;
esac
[ "$(printf '%s' "$lan_device" | wc -c)" -le 15 ] || return 1
printf '%s\n' "$lan_ip" | awk -F. 'NF == 4 { for (i=1;i<=4;i++) if ($i !~ /^[0-9]+$/ || $i > 255) exit 1; exit 0 } { exit 1 }' || return 1
}
ensure_loopback_system() {
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
[ "$current_hash" = "$system_written_hash" ] || { fail "初始化期间 system.dae 被其他进程改动,已中止"; return 1; }
current_listen=$(sed -n "s/^[[:space:]]*listen:[[:space:]]*'\([^']*\)'.*/\1/p" /etc/honk/system.dae | head -n 1)
[ "$current_listen" = "127.0.0.1:$listen_port" ] && return 0
temp_system=$(mktemp /etc/honk/.system.dae.XXXXXX) || return 1
sed "s/^[[:space:]]*listen:[[:space:]]*'[^']*'.*/ listen: '127.0.0.1:$listen_port'/" /etc/honk/system.dae > "$temp_system" || { rm -f "$temp_system"; return 1; }
chmod 0600 "$temp_system" || { rm -f "$temp_system"; return 1; }
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
[ "$current_hash" = "$system_written_hash" ] || { rm -f "$temp_system"; fail "初始化期间 system.dae 被其他进程改动,已中止"; return 1; }
mv -f "$temp_system" /etc/honk/system.dae || { rm -f "$temp_system"; return 1; }
system_written_hash=$(sha256sum /etc/honk/system.dae | awk '{print $1}')
}
restore_system() {
[ -n "$system_written_hash" ] || return 0
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
[ "$current_hash" = "$system_written_hash" ] || { fail "回滚期间 system.dae 被其他进程改动,已中止"; return 1; }
[ "$current_hash" != "$original_system_hash" ] || { system_written_hash=""; return 0; }
if [ -z "$original_system_hash" ]; then
rm -f /etc/honk/system.dae || return 1
system_written_hash=""
return 0
fi
temp_restore=$(mktemp /etc/honk/.system.dae.restore.XXXXXX) || return 1
cp -p "$old_system" "$temp_restore" || { rm -f "$temp_restore"; return 1; }
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
[ "$current_hash" = "$system_written_hash" ] || { rm -f "$temp_restore"; fail "回滚期间 system.dae 被其他进程改动,已中止"; return 1; }
mv -f "$temp_restore" /etc/honk/system.dae || { rm -f "$temp_restore"; return 1; }
system_written_hash=""
}
cleanup_initialize() {
if mock_stop >/dev/null 2>&1; then
if [ -n "$system_written_hash" ] && ! restore_system; then
logger -t honk "system config rollback refused or failed; inspect /etc/honk/system.dae"
fi
rm -rf "$temp_dir"
honk_lock_release >/dev/null 2>&1
else
logger -t honk "mock process cleanup failed; keeping operation lock and temporary files"
fi
}
write_runtime_system() {
lan_device=$1
lan_ip=$2
system_port=${3:-$listen_port}
system_listen=${4:-$lan_ip}
valid_clash_api || { fail "Clash API 设置不合法(端口 1024-65535,密钥 6-128 位)"; return 1; }
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
[ "$current_hash" = "$system_written_hash" ] || { fail "写入运行配置时 system.dae 被其他进程改动,已中止"; return 1; }
temp_system=$(mktemp /etc/honk/.system.dae.XXXXXX) || return 1
{
printf "global {\n wan_interface: auto\n lan_interface: '%s'\n" "$lan_device"
printf " log_level: info\n dial_mode: domain\n allow_insecure: false\n"
printf " auto_config_kernel_parameter: true\n data_dir: '/etc/honk'\n"
printf " store_subscribe: true\n nfqueue_enable: true\n}\n\n"
printf "experimental {\n native_api {\n enabled: true\n listen: '%s:%s'\n" "$system_listen" "$system_port"
printf " password_auth: true\n allow_anonymous_loopback: false\n"
printf " config_write: true\n ui: '/usr/share/doona'\n }\n"
if [ "$clash_api_enabled" = 1 ]; then
printf " clash_api {\n external_controller: '%s:%s'\n external_ui: '/usr/share/honk/zashboard'\n secret: '%s'\n default_mode: Rule\n }\n" "$system_listen" "$clash_api_port" "$clash_api_secret"
fi
printf "}\n"
} > "$temp_system" || { rm -f "$temp_system"; return 1; }
chmod 0600 "$temp_system" || { rm -f "$temp_system"; return 1; }
current_hash=$(sha256sum /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
[ "$current_hash" = "$system_written_hash" ] || { rm -f "$temp_system"; fail "写入运行配置时 system.dae 被其他进程改动,已中止"; return 1; }
mv -f "$temp_system" /etc/honk/system.dae || { rm -f "$temp_system"; return 1; }
system_written_hash=$(sha256sum /etc/honk/system.dae | awk '{print $1}')
}
managed_core_pid() {
honk_lifecycle_managed_pid
}
configured_system_port() {
sed -n "s/^[[:space:]]*listen:[[:space:]]*'[^:]*:\([0-9][0-9]*\)'.*/\1/p" /etc/honk/system.dae 2>/dev/null | head -n 1
}
configured_clash_port() {
sed -n "s/^[[:space:]]*external_controller:[[:space:]]*'[^:]*:\([0-9][0-9]*\)'.*/\1/p" /etc/honk/system.dae 2>/dev/null | head -n 1
}
sync_boot_link() {
if [ "$1" = 1 ]; then
/etc/init.d/honk enable
else
/etc/init.d/honk disable
fi
}
restore_old_runtime() {
[ "$old_running" = 1 ] || return 0
runtime_enabled=$(uci -q get honk.main.enabled || echo 0)
if [ "$runtime_enabled" != 1 ]; then uci set honk.main.enabled=1 && uci commit honk || return 1; fi
honk_lifecycle_start 30 || {
[ "$runtime_enabled" = 1 ] || { uci set "honk.main.enabled=$runtime_enabled" && uci commit honk; }
return 1
}
if [ "$runtime_enabled" != 1 ]; then uci set "honk.main.enabled=$runtime_enabled" && uci commit honk || return 1; fi
}
settings_rollback() {
rollback_ok=1
honk_lifecycle_stop 30 || return 1
if [ -n "$system_written_hash" ] && ! restore_system; then rollback_ok=0; fi
if ! uci import honk < "$old_uci" || ! uci commit honk; then rollback_ok=0; fi
if ! sync_boot_link "$old_boot_enabled"; then rollback_ok=0; fi
restore_old_runtime || rollback_ok=0
[ "$rollback_ok" = 1 ]
}
settings() {
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
load_settings
[ "$settings_owner" = system ] || { echo '{"ok":false,"message":"config_settings_owned_by_file"}'; return 1; }
. /usr/share/libubox/jshn.sh || { echo '{"ok":false,"message":"jshn_unavailable"}'; return 1; }
# Bound stdin to 64KiB (DoS protection); read one extra byte to detect truncation.
settings_json=$(head -c 65537)
if [ "${#settings_json}" -gt 65536 ]; then
echo '{"ok":false,"message":"settings_too_large"}'; return 1
fi
json_load "$settings_json" || { echo '{"ok":false,"message":"invalid_settings_json"}'; return 1; }
json_get_var new_network lan_network
json_get_type network_type lan_network
json_get_var new_port listen_port
json_get_type port_type listen_port
json_get_var new_boot_enabled boot_enabled
json_get_type boot_enabled_type boot_enabled
json_get_var new_clash_enabled clash_api_enabled
json_get_type clash_enabled_type clash_api_enabled
json_get_var new_clash_port clash_api_port
json_get_type clash_port_type clash_api_port
json_get_var new_clash_secret clash_api_secret
json_get_type clash_secret_type clash_api_secret
[ "$network_type" = string ] || { echo '{"ok":false,"message":"invalid_lan_network"}'; return 1; }
case "$port_type" in int|integer) ;; *) echo '{"ok":false,"message":"invalid_listen_port"}'; return 1 ;; esac
# boot_enabled 已内置(恒为 1),前端不再提交,缺省就按当前值处理
case "$boot_enabled_type" in ''|null|boolean) ;; *) echo '{"ok":false,"message":"invalid_boolean_settings"}'; return 1 ;; esac
case "$clash_enabled_type" in ''|null|boolean) ;; *) echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1 ;; esac
case "$clash_port_type" in ''|null|int|integer) ;; *) echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1 ;; esac
case "$clash_secret_type" in ''|null|string) ;; *) echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1 ;; esac
case "$new_network" in ''|*[!A-Za-z0-9_.-]*) echo '{"ok":false,"message":"invalid_lan_network"}'; return 1 ;; esac
case "$new_port" in ''|*[!0-9]*) echo '{"ok":false,"message":"invalid_listen_port"}'; return 1 ;; esac
[ "$new_port" -ge 1024 ] && [ "$new_port" -le 65535 ] || { echo '{"ok":false,"message":"invalid_listen_port"}'; return 1; }
load_settings
case "$boot_enabled_type" in ''|null) new_boot_enabled=1 ;; esac
new_boot_enabled=1
case "$clash_enabled_type" in ''|null) new_clash_enabled=$clash_api_enabled ;; esac
case "$clash_port_type" in ''|null) new_clash_port=$clash_api_port ;; esac
[ -n "$new_clash_secret" ] || new_clash_secret=$clash_api_secret
new_enabled=$enabled
old_network=$lan_network
old_port=$listen_port
old_enabled=$enabled
old_boot_enabled=$boot_enabled
old_initialized=$initialized
old_clash_enabled=$clash_api_enabled
old_clash_port=$clash_api_port
old_clash_secret=$clash_api_secret
[ "$initialized" = 1 ] || { [ "$new_enabled" = 0 ] || { echo '{"ok":false,"message":"initialize_before_enabling_service"}'; return 1; }; }
lan_network=$new_network
listen_port=$new_port
clash_api_enabled=$new_clash_enabled
clash_api_port=$new_clash_port
clash_api_secret=$new_clash_secret
valid_clash_api || { echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1; }
check_network || { echo '{"ok":false,"message":"lan_network_unavailable"}'; return 1; }
network_settings || { echo '{"ok":false,"message":"lan_ipv4_unavailable"}'; return 1; }
old_running=0
if managed_core_pid >/dev/null 2>&1; then
old_running=1
elif pidof honk-core >/dev/null 2>&1; then
echo '{"ok":false,"message":"unmanaged_core_running"}'
return 1
fi
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$new_port" ]; }; then
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
fi
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$new_clash_port" ]; }; then
check_port "$new_clash_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
fi
honk_lock_acquire || { echo '{"ok":false,"message":"operation_in_progress"}'; return 1; }
trap 'honk_lock_release >/dev/null 2>&1' 0
load_settings
[ "$lan_network" = "$old_network" ] && [ "$listen_port" = "$old_port" ] && [ "$enabled" = "$old_enabled" ] && [ "$boot_enabled" = "$old_boot_enabled" ] && [ "$initialized" = "$old_initialized" ] && [ "$clash_api_enabled" = "$old_clash_enabled" ] && [ "$clash_api_port" = "$old_clash_port" ] && [ "$clash_api_secret" = "$old_clash_secret" ] || { echo '{"ok":false,"message":"settings_changed_concurrently"}'; return 1; }
new_enabled=$enabled
managed_core_pid >/dev/null 2>&1 && recheck_running=1 || recheck_running=0
[ "$recheck_running" = "$old_running" ] || { echo '{"ok":false,"message":"service_state_changed_concurrently"}'; return 1; }
old_running=$recheck_running
lan_network=$new_network
listen_port=$new_port
clash_api_enabled=$new_clash_enabled
clash_api_port=$new_clash_port
clash_api_secret=$new_clash_secret
check_network && network_settings || { echo '{"ok":false,"message":"lan_network_changed_during_settings"}'; return 1; }
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$new_port" ]; }; then
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
fi
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$new_clash_port" ]; }; then
check_port "$new_clash_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
fi
if [ "$initialized" = 1 ] && ! check_config; then echo '{"ok":false,"message":"package_config_unavailable"}'; return 1; fi
umask 077
settings_dir=$(mktemp -d /tmp/honk-settings.XXXXXX) || { echo '{"ok":false,"message":"temporary_storage_unavailable"}'; return 1; }
old_uci=$settings_dir/honk.uci
uci export honk > "$old_uci" || { rm -rf "$settings_dir"; echo '{"ok":false,"message":"uci_snapshot_failed"}'; return 1; }
old_system=$settings_dir/system.dae
system_written_hash=""
if [ "$initialized" = 1 ]; then
[ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] || { rm -rf "$settings_dir"; echo '{"ok":false,"message":"system_config_unavailable"}'; return 1; }
cp -p /etc/honk/system.dae "$old_system" || { rm -rf "$settings_dir"; echo '{"ok":false,"message":"system_snapshot_failed"}'; return 1; }
original_system_hash=$(sha256sum "$old_system" | awk '{print $1}')
system_written_hash=$original_system_hash
fi
if [ "$old_running" = 1 ]; then
honk_lifecycle_stop 30 || {
settings_rollback; rollback_status=$?
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"service_stop_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"service_stop_and_rollback_failed_snapshot_retained"}'; fi
return 1
}
fi
if [ "$initialized" = 1 ] && ! write_runtime_system "$lan_device" "$lan_ip" "$new_port"; then
settings_rollback; rollback_status=$?
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"system_config_write_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"system_config_write_and_rollback_failed_snapshot_retained"}'; fi
return 1
fi
uci set "honk.main.lan_network=$new_network" && uci set "honk.main.listen_port=$new_port" && uci set "honk.main.enabled=$new_enabled" && uci set "honk.main.boot_enabled=$new_boot_enabled" && uci set "honk.main.clash_api_enabled=$new_clash_enabled" && uci set "honk.main.clash_api_port=$new_clash_port" && uci set "honk.main.clash_api_secret=$new_clash_secret" && uci commit honk && chmod 0600 /etc/config/honk || {
settings_rollback; rollback_status=$?
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"settings_commit_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"settings_commit_and_rollback_failed_snapshot_retained"}'; fi
return 1
}
if ! sync_boot_link "$new_boot_enabled"; then
settings_rollback; rollback_status=$?
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"boot_link_update_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"boot_link_update_and_rollback_failed_snapshot_retained"}'; fi
return 1
fi
if [ "$initialized" = 1 ]; then
service_ok=0
if [ "$new_enabled" = 1 ]; then
honk_lifecycle_start 30 || service_ok=1
else
honk_lifecycle_stop 30 || service_ok=1
fi
if [ "$service_ok" != 0 ]; then
settings_rollback; rollback_status=$?
if [ "$rollback_status" = 0 ]; then rm -rf "$settings_dir"; echo '{"ok":false,"message":"service_apply_failed_rolled_back"}'; else logger -t honk "settings rollback failed; snapshot retained at $settings_dir"; echo '{"ok":false,"message":"service_apply_and_rollback_failed_snapshot_retained"}'; fi
return 1
fi
fi
system_written_hash=""
rm -rf "$settings_dir"
echo '{"ok":true,"message":"settings_saved"}'
}
diagnose() {
load_settings
errors=""
managed_core_pid >/dev/null 2>&1 && running=1 || running=0
check_config >/dev/null 2>&1 || errors="$errors invalid_package_config"
check_kernel >/dev/null 2>&1 || errors="$errors kernel_requirements_unmet"
if [ "$settings_owner" != system ]; then
if [ "$running" != 1 ]; then
no_honk_resources >/dev/null 2>&1 || errors="$errors honk_resources_occupied"
pidof honk-core >/dev/null 2>&1 && errors="$errors unmanaged_core_running"
fi
else
check_network >/dev/null 2>&1 || errors="$errors lan_network_unavailable"
network_settings >/dev/null 2>&1 || errors="$errors lan_ipv4_unavailable"
command -v curl >/dev/null 2>&1 || errors="$errors curl_missing"
if [ "$running" != 1 ]; then
no_honk_resources >/dev/null 2>&1 || errors="$errors honk_resources_occupied"
pidof honk-core >/dev/null 2>&1 && errors="$errors unmanaged_core_running"
check_port >/dev/null 2>&1 || errors="$errors listen_port_occupied"
if [ "$clash_api_enabled" = 1 ]; then
check_port "$clash_api_port" >/dev/null 2>&1 || errors="$errors clash_api_port_occupied"
fi
fi
fi
check_conflicts >/dev/null 2>&1 || errors="$errors proxy_conflict"
if [ -d "$HONK_LOCK_DIR" ]; then
if lock_owner_invalid || honk_lock_is_stale; then
errors="$errors stale_operation_lock maintenance_recovery_required"
else
errors="$errors operation_in_progress"
fi
fi
{ [ -e "$HONK_MAINT_JOURNAL" ] || [ -L "$HONK_MAINT_JOURNAL" ]; } && errors="$errors maintenance_recovery_required"
if [ -n "$errors" ]; then
printf '%s\n' "$errors" | sed 's/^ *//'
return 1
fi
return 0
}
main_has_system_include() {
awk '
/^[[:space:]]*include[[:space:]]*\{/ { inside = 1 }
inside && /(^|[^[:alnum:]_.-])system\.dae([^[:alnum:]_.-]|$)/ { found = 1 }
inside && /\}/ { inside = 0 }
END { exit !found }
' /etc/honk/config.dae
}
clash_system_matches() {
if [ "$clash_api_enabled" = 0 ]; then
! grep -q 'clash_api {' /etc/honk/system.dae
return
fi
permissions=$(ls -ldn /etc/honk/system.dae 2>/dev/null | awk '{print $1}')
[ "$permissions" = '-rw-------' ] || return 1
grep -Fqx " external_controller: '$system_host:$clash_api_port'" /etc/honk/system.dae || return 1
grep -Fqx " external_ui: '/usr/share/honk/zashboard'" /etc/honk/system.dae || return 1
system_secret=$(sed -n "s/^ secret: '\([^']*\)'$/\1/p" /etc/honk/system.dae)
[ "$system_secret" = "$clash_api_secret" ]
}
repair() {
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
load_settings
if [ "$settings_owner" != system ]; then
check_config && { echo '{"ok":true,"message":"config_valid"}'; return 0; }
echo '{"ok":false,"message":"config_invalid"}'
return 1
fi
old_network=$lan_network
old_port=$listen_port
old_enabled=$enabled
old_boot_enabled=$boot_enabled
old_initialized=$initialized
old_clash_enabled=$clash_api_enabled
old_clash_port=$clash_api_port
old_clash_secret=$clash_api_secret
[ "$config_file" = /etc/honk/config.dae ] || { echo '{"ok":false,"message":"invalid_config_path"}'; return 1; }
[ -f /etc/honk/config.dae ] && [ ! -L /etc/honk/config.dae ] || { echo '{"ok":false,"message":"main_config_unavailable"}'; return 1; }
main_hash=$(sha256sum /etc/honk/config.dae | awk '{print $1}')
main_has_system_include || { echo '{"ok":false,"message":"required_system_include_missing"}'; return 1; }
valid_network_name || { echo '{"ok":false,"message":"invalid_lan_network"}'; return 1; }
case "$listen_port" in ''|*[!0-9]*) echo '{"ok":false,"message":"invalid_listen_port"}'; return 1 ;; esac
[ "$listen_port" -ge 1024 ] && [ "$listen_port" -le 65535 ] || { echo '{"ok":false,"message":"invalid_listen_port"}'; return 1; }
valid_clash_api || { echo '{"ok":false,"message":"invalid_clash_api_settings"}'; return 1; }
check_network || { echo '{"ok":false,"message":"lan_network_unavailable"}'; return 1; }
network_settings || { echo '{"ok":false,"message":"lan_ipv4_unavailable"}'; return 1; }
old_running=0
managed_core_pid >/dev/null 2>&1 && old_running=1
if pidof honk-core >/dev/null 2>&1 && [ "$old_running" = 0 ]; then echo '{"ok":false,"message":"unmanaged_core_running"}'; return 1; fi
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$listen_port" ]; }; then
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
fi
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$clash_api_port" ]; }; then
check_port "$clash_api_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
fi
honk_lock_acquire || { echo '{"ok":false,"message":"operation_in_progress"}'; return 1; }
trap 'honk_lock_release >/dev/null 2>&1' 0
load_settings
[ "$lan_network" = "$old_network" ] && [ "$listen_port" = "$old_port" ] && [ "$enabled" = "$old_enabled" ] && [ "$boot_enabled" = "$old_boot_enabled" ] && [ "$initialized" = "$old_initialized" ] && [ "$clash_api_enabled" = "$old_clash_enabled" ] && [ "$clash_api_port" = "$old_clash_port" ] && [ "$clash_api_secret" = "$old_clash_secret" ] || { echo '{"ok":false,"message":"settings_changed_concurrently"}'; return 1; }
managed_core_pid >/dev/null 2>&1 && recheck_running=1 || recheck_running=0
[ "$recheck_running" = "$old_running" ] || { echo '{"ok":false,"message":"service_state_changed_concurrently"}'; return 1; }
[ "$(sha256sum /etc/honk/config.dae | awk '{print $1}')" = "$main_hash" ] || { echo '{"ok":false,"message":"main_config_changed_concurrently"}'; return 1; }
check_network && network_settings || { echo '{"ok":false,"message":"lan_network_changed_during_repair"}'; return 1; }
if ! { [ "$old_running" = 1 ] && [ "$(configured_system_port)" = "$listen_port" ]; }; then
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
fi
if [ "$clash_api_enabled" = 1 ] && ! { [ "$old_running" = 1 ] && [ "$(configured_clash_port)" = "$clash_api_port" ]; }; then
check_port "$clash_api_port" || { echo '{"ok":false,"message":"clash_api_port_occupied"}'; return 1; }
fi
if [ "$initialized" = 1 ]; then system_host=$lan_ip; else system_host=127.0.0.1; fi
if [ -r /usr/share/doona/index.html ] && [ -f /etc/honk/system.dae ] && [ ! -L /etc/honk/system.dae ] && system_config_readonly && \
grep -Fqx ' wan_interface: auto' /etc/honk/system.dae && grep -Fqx " lan_interface: '$lan_device'" /etc/honk/system.dae && \
grep -Fqx " data_dir: '/etc/honk'" /etc/honk/system.dae && grep -Fqx ' nfqueue_enable: true' /etc/honk/system.dae && \
grep -Fqx " listen: '$system_host:$listen_port'" /etc/honk/system.dae && \
grep -Fqx " ui: '/usr/share/doona'" /etc/honk/system.dae && grep -Fqx ' password_auth: true' /etc/honk/system.dae && \
grep -Fqx ' allow_anonymous_loopback: false' /etc/honk/system.dae && grep -Fqx ' config_write: true' /etc/honk/system.dae && clash_system_matches; then
echo '{"ok":true,"message":"system_config_healthy"}'
return 0
fi
if [ -e /etc/honk/system.dae ] && { [ ! -f /etc/honk/system.dae ] || [ -L /etc/honk/system.dae ]; }; then
echo '{"ok":false,"message":"system_config_not_regular_file"}'
return 1
fi
[ -r /usr/share/doona/index.html ] || { echo '{"ok":false,"message":"ui_assets_missing"}'; return 1; }
umask 077
repair_dir=$(mktemp -d /tmp/honk-repair.XXXXXX) || { echo '{"ok":false,"message":"temporary_storage_unavailable"}'; return 1; }
old_system=$repair_dir/system.dae
original_system_hash=""
if [ -f /etc/honk/system.dae ]; then
cp -p /etc/honk/system.dae "$old_system" || { rm -rf "$repair_dir"; echo '{"ok":false,"message":"system_snapshot_failed"}'; return 1; }
original_system_hash=$(sha256sum "$old_system" | awk '{print $1}')
fi
system_written_hash=$original_system_hash
if [ "$old_running" = 1 ]; then
if ! honk_lifecycle_stop 30; then
logger -t honk "repair stop/drain unconfirmed; no restart attempted; snapshot retained at $repair_dir"
echo '{"ok":false,"message":"service_stop_unconfirmed_snapshot_retained"}'
return 1
fi
fi
if [ "$initialized" = 1 ]; then system_listen=$lan_ip; else system_listen=127.0.0.1; fi
if ! write_runtime_system "$lan_device" "$lan_ip" "$listen_port" "$system_listen"; then
repair_rollback_ok=1
[ -n "$system_written_hash" ] && ! restore_system && repair_rollback_ok=0
restore_old_runtime || repair_rollback_ok=0
if [ "$repair_rollback_ok" = 1 ]; then
rm -rf "$repair_dir"
echo '{"ok":false,"message":"system_config_repair_failed_rolled_back"}'
else
logger -t honk "repair rollback failed; snapshot retained at $repair_dir"
echo '{"ok":false,"message":"system_config_repair_rollback_failed_snapshot_retained"}'
fi
return 1
fi
if [ "$initialized" = 1 ] && { [ "$old_running" = 1 ] || [ "$enabled" = 1 ]; }; then
if ! honk_lifecycle_start 30; then
repair_rollback_ok=1
honk_lifecycle_stop 30 || repair_rollback_ok=0
if [ "$repair_rollback_ok" = 1 ]; then restore_system || repair_rollback_ok=0; fi
restore_old_runtime || repair_rollback_ok=0
if [ "$repair_rollback_ok" = 1 ]; then
rm -rf "$repair_dir"
echo '{"ok":false,"message":"repair_restart_failed_rolled_back"}'
else
logger -t honk "repair restart rollback failed; snapshot retained at $repair_dir"
echo '{"ok":false,"message":"repair_restart_and_rollback_failed_snapshot_retained"}'
fi
return 1
fi
fi
system_written_hash=""
rm -rf "$repair_dir"
echo '{"ok":true,"message":"system_config_repaired"}'
}
api_discovery() {
curl -fsS --noproxy '*' --connect-timeout 1 --max-time 3 "$1/api" -o "$2"
}
mock_start() {
mock_config=$1
mock_log=$2
DAE_LOCATION_ASSET=/usr/share/v2ray "$HONK_CORE" --config "$mock_config" --mock-ebpf > "$mock_log" 2>&1 &
mock_pid=$!
mock_identity=""
attempt=0
while [ "$attempt" -lt 20 ]; do
if ! kill -0 "$mock_pid" 2>/dev/null; then
return 1
fi
if api_discovery "$3" "$4" 2>/dev/null; then
mock_identity=$(tr '\000' ' ' < "/proc/$mock_pid/cmdline" 2>/dev/null)
case "$mock_identity" in
*"$mock_config"*--mock-ebpf*)
[ "$(readlink -f "/proc/$mock_pid/exe" 2>/dev/null)" = "$HONK_CORE" ] && return 0
;;
esac
return 1
fi
sleep 1
attempt=$((attempt + 1))
done
return 1
}
mock_stop() {
[ -n "$mock_pid" ] || return 0
[ -r "/proc/$mock_pid/cmdline" ] || { mock_pid=""; return 0; }
current=$(tr '\000' ' ' < "/proc/$mock_pid/cmdline" 2>/dev/null)
case "$current" in
*"$mock_config"*--mock-ebpf*)
[ "$(readlink -f "/proc/$mock_pid/exe" 2>/dev/null)" = "$HONK_CORE" ] || return 1
kill -TERM "$mock_pid" 2>/dev/null || return 1
;;
*) return 1 ;;
esac
attempt=0
while kill -0 "$mock_pid" 2>/dev/null && [ "$attempt" -lt 10 ]; do sleep 1; attempt=$((attempt + 1)); done
if kill -0 "$mock_pid" 2>/dev/null; then return 1; fi
wait "$mock_pid" 2>/dev/null || true
mock_pid=""
}
post_credentials() {
endpoint=$1
response_file=$2
json_init
json_add_string username "$username"
json_add_string password "$password"
json_dump | curl -sS --noproxy '*' --connect-timeout 1 --max-time 5 -o "$response_file" -w '%{http_code}' \
-H 'Content-Type: application/json' --data-binary @- "$endpoint"
}
initialize_preflight() {
load_settings
[ "$enabled" = 0 ] && [ "$initialized" = 0 ] || { echo '{"ok":false,"message":"disable_service_before_initialization"}'; return 1; }
[ "$config_file" = /etc/honk/config.dae ] || { echo '{"ok":false,"message":"invalid_config_path"}'; return 1; }
[ -r /usr/share/doona/index.html ] || { echo '{"ok":false,"message":"ui_assets_missing"}'; return 1; }
if [ "$1" = read_credentials ]; then
read_credentials || { echo '{"ok":false,"message":"invalid_credentials"}'; return 1; }
fi
[ -x "$HONK_CORE" ] || { echo '{"ok":false,"message":"core_missing"}'; return 1; }
command -v curl >/dev/null 2>&1 || { echo '{"ok":false,"message":"curl_missing"}'; return 1; }
pidof honk-core >/dev/null 2>&1 && { echo '{"ok":false,"message":"core_already_running"}'; return 1; }
no_honk_resources || { echo '{"ok":false,"message":"honk_resources_already_exist"}'; return 1; }
check_port || { echo '{"ok":false,"message":"listen_port_occupied"}'; return 1; }
check_network || { echo '{"ok":false,"message":"lan_network_unavailable"}'; return 1; }
network_settings || { echo '{"ok":false,"message":"lan_ipv4_unavailable"}'; return 1; }
check_config || { echo '{"ok":false,"message":"invalid_package_config"}'; return 1; }
return 0
}
initialize() {
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then echo '{"ok":false,"message":"update_recovery_required"}'; return 1; fi
. /usr/share/libubox/jshn.sh || return 1
initialize_preflight read_credentials || return 1
honk_lock_acquire || { echo '{"ok":false,"message":"operation_in_progress"}'; return 1; }
trap 'honk_lock_release >/dev/null 2>&1' 0
initialize_preflight || return 1
umask 077
temp_dir=$(mktemp -d /tmp/honk-init.XXXXXX) || { echo '{"ok":false,"message":"temporary_storage_unavailable"}'; return 1; }
mock_pid=""
trap cleanup_initialize 0
trap 'exit 1' HUP INT TERM
old_system=$temp_dir/system.dae
cp -p /etc/honk/system.dae "$old_system" || { echo '{"ok":false,"message":"system_config_backup_failed"}'; return 1; }
original_system_hash=$(sha256sum "$old_system" | awk '{print $1}')
system_written_hash=$original_system_hash
ensure_loopback_system || { echo '{"ok":false,"message":"loopback_listener_write_failed"}'; return 1; }
mock_config=$temp_dir/mock.dae
mock_log=$temp_dir/core.log
response_file=$temp_dir/response.json
cat > "$mock_config" <<EOF
global {
wan_interface: auto
lan_interface: ''
data_dir: '/etc/honk'
nfqueue_enable: false
store_subscribe: false
}
routing {
fallback: direct
}
experimental {
native_api {
enabled: true
listen: '127.0.0.1:$listen_port'
password_auth: true
allow_anonymous_loopback: false
config_write: true
}
}
EOF
chmod 0600 "$mock_config" "$mock_log" 2>/dev/null || true
loopback_url="http://127.0.0.1:$listen_port"
if ! mock_start "$mock_config" "$mock_log" "$loopback_url" "$response_file"; then
echo '{"ok":false,"message":"mock_core_start_failed"}'
return 1
fi
setup_required=$(jsonfilter -i "$response_file" -e '@.auth.setup_required' 2>/dev/null)
case "$setup_required" in
true)
status=$(post_credentials "$loopback_url/api/v1/auth/setup" "$response_file")
[ "$status" = 201 ] || { echo '{"ok":false,"message":"administrator_setup_failed"}'; return 1; }
;;
false)
status=$(post_credentials "$loopback_url/api/v1/auth/login" "$response_file")
[ "$status" = 200 ] || { echo '{"ok":false,"message":"administrator_exists_reuse_credentials_or_reset_state"}'; return 1; }
;;
*) echo '{"ok":false,"message":"native_api_setup_state_unavailable"}'; return 1 ;;
esac
api_discovery "$loopback_url" "$response_file" || { echo '{"ok":false,"message":"setup_verification_failed"}'; return 1; }
setup_required=$(jsonfilter -i "$response_file" -e '@.auth.setup_required' 2>/dev/null)
[ "$setup_required" = false ] || { echo '{"ok":false,"message":"setup_verification_failed"}'; return 1; }
mock_stop || { echo '{"ok":false,"message":"mock_core_stop_failed"}'; return 1; }
if ! mock_start "$mock_config" "$mock_log" "$loopback_url" "$response_file"; then
echo '{"ok":false,"message":"mock_core_restart_failed"}'
return 1
fi
api_discovery "$loopback_url" "$response_file" || { echo '{"ok":false,"message":"persistence_verification_failed"}'; return 1; }
setup_required=$(jsonfilter -i "$response_file" -e '@.auth.setup_required' 2>/dev/null)
[ "$setup_required" = false ] || { echo '{"ok":false,"message":"persistence_verification_failed"}'; return 1; }
status=$(post_credentials "$loopback_url/api/v1/auth/login" "$response_file")
[ "$status" = 200 ] || { echo '{"ok":false,"message":"persistence_login_failed"}'; return 1; }
mock_stop || { echo '{"ok":false,"message":"mock_core_stop_failed"}'; return 1; }
write_runtime_system "$lan_device" "$lan_ip" || { restore_system; echo '{"ok":false,"message":"system_config_write_failed"}'; return 1; }
lan_url="http://$lan_ip:$listen_port"
if ! mock_start /etc/honk/config.dae "$mock_log" "$lan_url" "$response_file"; then
restore_system
echo '{"ok":false,"message":"generated_config_invalid_restored_loopback"}'
return 1
fi
api_discovery "$lan_url" "$response_file" || {
mock_stop >/dev/null 2>&1
restore_system
echo '{"ok":false,"message":"lan_api_verification_failed_restored_loopback"}'
return 1
}
mock_stop || {
restore_system
echo '{"ok":false,"message":"mock_core_stop_failed_restored_loopback"}'
return 1
}
uci set honk.main.initialized='1' && uci commit honk || {
restore_system
uci set honk.main.initialized='0' >/dev/null 2>&1
uci commit honk >/dev/null 2>&1
echo '{"ok":false,"message":"uci_commit_failed_restored_loopback"}'
return 1
}
system_written_hash=""
password=""
username=""
echo '{"ok":true,"message":"administrator_initialized"}'
}
case "$1" in
check)
preflight
;;
diagnose)
diagnose
;;
preflight)
prepare_runtime
;;
local-direct)
load_settings
sync_local_direct
;;
initialize)
initialize
;;
settings)
settings
;;
repair)
repair
;;
*)
echo "usage: $0 {check|diagnose|preflight|local-direct|initialize|settings|repair}" >&2
exit 2
;;
esac
-14
View File
@@ -1,14 +0,0 @@
config honk 'main'
option enabled '0'
option boot_enabled '0'
option initialized '0'
option config_file '/etc/honk/config.dae'
option listen_port '9527'
option clash_api_enabled '0'
option clash_api_port '9090'
option clash_api_secret ''
option lan_network 'lan'
option geoip_url ''
option geosite_url ''
option geo_auto '0'
option geo_auto_freq 'daily'
-81
View File
@@ -1,81 +0,0 @@
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=95
STOP=10
. /lib/functions.sh
HONK_CONTROL=/usr/share/honk/control.sh
HONK_CONFIG=/etc/honk/config.dae
boot() {
# 开机自启内置:只要服务是"已启用 + 已初始化"就起来,
# 用户主动停掉(enabled=0)后重启也不会被强行拉起。
start
}
start_service() {
local enabled initialized config_file ready_token ready_hash ready_runtime ready_api_enabled ready_api_listen ready_clash_api_listen ready_clash_api_secret_set old_ready_token
config_load honk
config_get_bool enabled main enabled 0
config_get_bool initialized main initialized 0
[ "$enabled" -eq 1 ] && [ "$initialized" -eq 1 ] || {
logger -t honk "refusing to start before explicit initialization and enablement"
return 1
}
config_get config_file main config_file "$HONK_CONFIG"
[ "$config_file" = "$HONK_CONFIG" ] || {
logger -t honk "refusing non-default config path"
return 1
}
"$HONK_CONTROL" preflight || return 1
"$HONK_CONTROL" local-direct || logger -t honk "local-direct sync skipped"
unset HONK_MAINT_CAP
# 所有配置都走 readiness 标记:状态页据此判断真正就绪,而不是只看进程在不在。
ready_token=$(cat /proc/sys/kernel/random/uuid 2>/dev/null) || return 1
case "$ready_token" in *[!0-9a-f-]*|'') return 1 ;; esac
ready_hash=$(sha256sum "$HONK_CONFIG" 2>/dev/null | awk '{print $1}') || return 1
case "$ready_hash" in *[!0-9a-f]*|'') return 1 ;; esac
ready_runtime=$(/usr/bin/honk-core config validate --config "$HONK_CONFIG" 2>/dev/null) || return 1
ready_api_enabled=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.native_api_enabled' 2>/dev/null)
ready_api_listen=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.native_api_listen' 2>/dev/null)
ready_clash_api_listen=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.clash_api_listen' 2>/dev/null)
ready_clash_api_secret_set=$(printf '%s' "$ready_runtime" | jsonfilter -e '@.runtime.clash_api_secret_set' 2>/dev/null)
case "$ready_api_enabled" in true|false) ;; *) return 1 ;; esac
case "$ready_api_listen" in *[!A-Za-z0-9:._-]*) return 1 ;; esac
case "$ready_clash_api_listen" in *[!A-Za-z0-9:._-]*) return 1 ;; esac
case "$ready_clash_api_secret_set" in true|false) ;; *) return 1 ;; esac
mkdir -p /var/run/honk || return 1
chmod 0700 /var/run/honk || return 1
if [ -f /var/run/honk/readiness ] && [ ! -L /var/run/honk/readiness ]; then
read -r old_ready_token _ < /var/run/honk/readiness
case "$old_ready_token" in ''|*[!0-9a-f-]*) ;; *) rm -f "/var/run/honk/core-ready-$old_ready_token" ;; esac
fi
printf '%s %s\n' "$ready_token" "$ready_hash" > /var/run/honk/readiness || return 1
chmod 0600 /var/run/honk/readiness || return 1
procd_open_instance main
procd_set_param command /usr/bin/honk-core --config "$HONK_CONFIG" --disable-timestamp
# One env call, no empty values: procd drops the whole list when a KEY has an
# empty value, which silently stripped the readiness hash the core requires.
ready_env="DAE_LOCATION_ASSET=/usr/share/v2ray"
ready_env="$ready_env HONK_READY_TOKEN=$ready_token HONK_READY_CONFIG_HASH=$ready_hash HONK_READY_API_ENABLED=$ready_api_enabled HONK_READY_CLASH_API_SECRET_SET=$ready_clash_api_secret_set"
[ -n "$ready_api_listen" ] && ready_env="$ready_env HONK_READY_API_LISTEN=$ready_api_listen"
[ -n "$ready_clash_api_listen" ] && ready_env="$ready_env HONK_READY_CLASH_API_LISTEN=$ready_clash_api_listen"
# shellcheck disable=SC2086
procd_set_param env $ready_env
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param respawn 3600 5 5
procd_set_param term_timeout 20
procd_close_instance
}
reload_service() {
# honk-core has no hot-reload API wired here; a reload is an explicit
# restart so existing connections are cycled. The UI only exposes
# restart, this exists so manual `/etc/init.d/honk reload` is defined.
logger -t honk "reload requested, restarting (no hot-reload)"
restart
}
-127
View File
@@ -1,127 +0,0 @@
#!/bin/sh
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
umask 077
. /usr/share/libubox/jshn.sh
JOBS=/tmp/honk-jobs
MAINT=/usr/share/honk/maintenance.sh
write_status() {
status_state=$1
status_phase=$2
status_ok=$3
status_message=$4
# jshn 的布尔只认 1/0,传 "true" 会被写进状态文件成 false。
case "$status_ok" in true|1) status_ok=1 ;; *) status_ok=0 ;; esac
status_tmp=$job_dir/status.new
{
json_init
json_add_string state "$status_state"
json_add_string phase "$status_phase"
json_add_boolean ok "$status_ok"
json_add_string message "$status_message"
json_dump
} > "$status_tmp" || return 1
chmod 0600 "$status_tmp" || return 1
mv -f "$status_tmp" "$job_dir/status" || return 1
}
job_id=$1
operation=$2
case "$job_id" in
*[!0-9a-f]*|'') exit 2 ;;
esac
[ "${#job_id}" -eq 32 ] || exit 2
case "$operation" in backup|restore|reset|update_check|update_apply) ;; *) exit 2 ;; esac
job_dir=$JOBS/$job_id
[ -d "$JOBS" ] && [ ! -L "$JOBS" ] && [ -d "$job_dir" ] && [ ! -L "$job_dir" ] || exit 2
[ "$(stat -c '%u:%a' "$JOBS" 2>/dev/null)" = 0:700 ] && [ "$(stat -c '%u:%a' "$job_dir" 2>/dev/null)" = 0:700 ] || exit 2
[ "$(id -u)" = 0 ] || exit 2
write_status running preparing true preparing || exit 1
: > "$job_dir/output" || exit 1
chmod 0600 "$job_dir/output" || exit 1
# stderr 单独存:混进 output 会让结果 JSON 解析失败,报成“操作失败”。
: > "$job_dir/stderr" || exit 1
chmod 0600 "$job_dir/stderr" || exit 1
if [ "$operation" = update_apply ]; then
/usr/share/honk/update.sh apply > "$job_dir/output" 2> "$job_dir/stderr" &
elif [ "$operation" = update_check ]; then
/usr/share/honk/update.sh check > "$job_dir/output" 2> "$job_dir/stderr" &
else
"$MAINT" "$operation" > "$job_dir/output" 2> "$job_dir/stderr" &
fi
maint_pid=$!
logger -t honk-maintenance "$operation started" 2>/dev/null || :
# Total watchdog: never poll forever if maintenance.sh hangs.
_poll_deadline=$(($(date +%s) + 1800))
while kill -0 "$maint_pid" 2>/dev/null; do
if [ "$(date +%s)" -ge "$_poll_deadline" ]; then
logger -t honk-maintenance "$operation timed out after 1800s, killing $maint_pid" 2>/dev/null || :
kill -9 "$maint_pid" 2>/dev/null || :
break
fi
journal_phase=$(sed -n 's/^phase=//p' /etc/.honk-maintenance/journal 2>/dev/null | sed -n '1p')
if [ "$operation" = update_apply ]; then
update_phase=$(cat /etc/.honk-update/phase 2>/dev/null)
case "$update_phase" in
downloading) status_phase=downloading ;;
verifying) status_phase=validating ;;
stopping) status_phase=stopping ;;
installing) status_phase=applying ;;
starting) status_phase=starting ;;
*) status_phase=preparing ;;
esac
elif [ "$operation" = update_check ]; then
status_phase=checking_feed
else case "$journal_phase" in
prepared) status_phase=preparing ;;
stopped) status_phase=stopping ;;
old_moved|new_installed) status_phase=applying ;;
uci_applied) status_phase=validating ;;
service_started) status_phase=starting ;;
rollback_old_start) status_phase=rolling_back ;;
committed) status_phase=committing ;;
*) status_phase=preparing ;;
esac; fi
write_status running "$status_phase" true "$status_phase" || :
sleep 1
done
wait "$maint_pid"
operation_rc=$?
json_load "$(cat "$job_dir/output" 2>/dev/null)" 2>/dev/null || {
if [ -e /etc/.honk-maintenance/journal ] || [ -L /etc/.honk-maintenance/journal ] || [ -d /run/honk.lock ]; then
write_status rollback_required recovery false maintenance_recovery_required
else
write_status failed complete false operation_failed
fi
logger -t honk-maintenance "$operation failed (invalid result)" 2>/dev/null || :
exit 1
}
json_get_var operation_ok ok
json_get_var operation_message message
if [ "$operation_rc" -eq 0 ] && [ "$operation_ok" = 1 ]; then
if [ "$operation" = update_check ]; then
json_add_string state succeeded
json_add_string phase complete
json_add_string job_id "$job_id"
json_dump > "$job_dir/status.new" && chmod 0600 "$job_dir/status.new" && mv -f "$job_dir/status.new" "$job_dir/status" || exit 1
logger -t honk-maintenance "$operation completed" 2>/dev/null || :
exit 0
fi
write_status succeeded complete true "$operation_message"
logger -t honk-maintenance "$operation completed" 2>/dev/null || :
exit 0
fi
case "$operation_message" in
*rollback_failed*|*recovery_required*|*snapshot_retained*)
write_status rollback_required recovery false "$operation_message"
logger -t honk-maintenance "$operation requires recovery" 2>/dev/null || :
exit 1
;;
esac
write_status failed complete false "${operation_message:-operation_failed}"
logger -t honk-maintenance "$operation failed" 2>/dev/null || :
exit 1
-282
View File
@@ -1,282 +0,0 @@
#!/bin/sh
HONK_LIFECYCLE_CORE=/usr/bin/honk-core
# 字段归属:自带 global/experimental → file;只 include system.dae(且写法规范)→ system;
# 其余一律 unknown。写法不规范时不猜,直接拒绝。
config_owner() {
config_owner_file=${1:-/etc/honk/config.dae}
[ -f "$config_owner_file" ] && [ ! -L "$config_owner_file" ] || { echo unknown; return 0; }
own=$(awk '/^[[:space:]]*(global|experimental)[[:space:]]*\{/ { print "1"; exit }' "$config_owner_file")
inc=$(awk '
{
line = $0
sub(/\r$/, "", line)
if (!in_block) {
# 只接受「include {」独占一行;同行带内容一律算不规范
if (line ~ /^[[:space:]]*include[[:space:]]*\{[[:space:]]*$/) { in_block = 1; next }
if (line ~ /^[[:space:]]*include[[:space:]]*\{/) { print "irregular"; next }
next
}
if (line ~ /^[[:space:]]*\}[[:space:]]*$/) { in_block = 0; next }
if (line ~ /^[[:space:]]*\}/ || line ~ /\{/) { print "irregular"; next }
entry = line
sub(/#.*/, "", entry)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", entry)
gsub(/["\047]/, "", entry)
if (entry == "") next
if (entry == "system.dae") print "system"
else if (entry ~ /^[A-Za-z0-9._-]+$/) print "other"
else print "irregular"
}
END { if (in_block) print "irregular" }
' "$config_owner_file")
has_system=
has_bad=
case "$inc" in *system*) has_system=1 ;; esac
case "$inc" in *other*|*irregular*) has_bad=1 ;; esac
if [ -n "$own" ]; then
[ -z "$inc" ] && echo file || echo unknown
elif [ -n "$has_bad" ]; then
echo unknown
elif [ -n "$has_system" ]; then
echo system
else
echo unknown
fi
}
honk_lifecycle_procd_pid() { ubus call service list '{"name":"honk"}' 2>/dev/null | jsonfilter -e '@.honk.instances.main.pid' 2>/dev/null
}
honk_lifecycle_procd_running() {
ubus call service list '{"name":"honk"}' 2>/dev/null | jsonfilter -e '@.honk.instances.main.running' 2>/dev/null
}
honk_lifecycle_managed_pid() {
hlp_pid=$(honk_lifecycle_procd_pid)
[ -n "$hlp_pid" ] && [ -r "/proc/$hlp_pid/cmdline" ] || return 1
hlp_cmd=$(tr '\000' ' ' < "/proc/$hlp_pid/cmdline" 2>/dev/null) || return 1
case "$hlp_cmd" in
*/usr/bin/honk-core\ --config\ /etc/honk/config.dae\ --disable-timestamp*) ;;
*) return 1 ;;
esac
case "$hlp_cmd" in *--mock-ebpf*) return 1 ;; esac
[ "$(readlink -f "/proc/$hlp_pid/exe" 2>/dev/null)" = "$HONK_LIFECYCLE_CORE" ] || return 1
printf '%s\n' "$hlp_pid"
}
honk_lifecycle_starttime() {
[ -r "/proc/$1/stat" ] || return 1
awk '{ sub(/^.*\) /, ""); split($0, fields, /[[:space:]]+/); if (fields[20] ~ /^[0-9]+$/) print fields[20]; else exit 1 }' "/proc/$1/stat" 2>/dev/null
}
honk_lifecycle_listen() {
[ -f /var/run/honk/readiness ] && [ ! -L /var/run/honk/readiness ] || return 1
read -r hlp_token _ < /var/run/honk/readiness || return 1
case "$hlp_token" in ''|*[!0-9a-f-]*) return 1 ;; esac
sed -n 's/^api_listen=//p' "/var/run/honk/core-ready-$hlp_token" 2>/dev/null
}
honk_lifecycle_parse_listen() {
HONK_LIFECYCLE_LISTEN=$(honk_lifecycle_listen)
if [ -z "$HONK_LIFECYCLE_LISTEN" ]; then
HONK_LIFECYCLE_PORT=0
return 0
fi
case "$HONK_LIFECYCLE_LISTEN" in *:*) ;; *) return 1 ;; esac
HONK_LIFECYCLE_PORT=${HONK_LIFECYCLE_LISTEN##*:}
case "$HONK_LIFECYCLE_LISTEN" in ''|*[!A-Za-z0-9:._-]*) return 1 ;; esac
case "$HONK_LIFECYCLE_PORT" in ''|*[!0-9]*) return 1 ;; esac
[ "$HONK_LIFECYCLE_PORT" -ge 1024 ] && [ "$HONK_LIFECYCLE_PORT" -le 65535 ]
}
honk_lifecycle_ready_owned() {
hlp_pid=$1
[ -f /var/run/honk/readiness ] && [ ! -L /var/run/honk/readiness ] || return 1
read -r hlp_token hlp_hash < /var/run/honk/readiness || return 1
case "$hlp_token:$hlp_hash" in *[!0-9a-f:-]*|:*|:|*:) return 1 ;; esac
hlp_marker=/var/run/honk/core-ready-$hlp_token
[ -f "$hlp_marker" ] && [ ! -L "$hlp_marker" ] || return 1
grep -Fqx "pid=$hlp_pid" "$hlp_marker" && \
grep -Fqx "token=$hlp_token" "$hlp_marker" && \
grep -Fqx "config_hash=$hlp_hash" "$hlp_marker" && \
grep -Eq '^api_enabled=(true|false)$' "$hlp_marker" && \
grep -Eq '^clash_api_secret_set=(true|false)$' "$hlp_marker"
}
honk_lifecycle_port_open() {
hlp_port=$1
case "$hlp_port" in ''|*[!0-9]*) return 1 ;; esac
awk -v port="$hlp_port" 'BEGIN { printf "%04X\n", port }' | {
IFS= read -r hlp_hex || exit 1
for hlp_table in /proc/net/tcp /proc/net/tcp6; do
[ -r "$hlp_table" ] || continue
awk -v port="$hlp_hex" 'NR > 1 && toupper($2) ~ (":" port "$") && $4 == "0A" { found = 1 } END { exit !found }' "$hlp_table" && exit 0
done
exit 1
}
}
honk_lifecycle_listener_owned() {
hlp_pid=$1
hlp_port=$2
hlp_hex=$(awk -v port="$hlp_port" 'BEGIN { printf "%04X", port }') || return 1
for hlp_table in /proc/net/tcp /proc/net/tcp6; do
[ -r "$hlp_table" ] || continue
for hlp_inode in $(awk -v port="$hlp_hex" 'NR > 1 && toupper($2) ~ (":" port "$") && $4 == "0A" { print $10 }' "$hlp_table"); do
for hlp_fd in /proc/"$hlp_pid"/fd/*; do
[ "$(readlink "$hlp_fd" 2>/dev/null)" = "socket:[$hlp_inode]" ] && return 0
done
done
done
return 1
}
honk_lifecycle_api_owned() {
hlp_pid=$1
honk_lifecycle_parse_listen || return 1
[ "$HONK_LIFECYCLE_PORT" -gt 0 ] || return 1
honk_lifecycle_listener_owned "$hlp_pid" "$HONK_LIFECYCLE_PORT" && \
curl -fsS --noproxy '*' --connect-timeout 1 --max-time 2 "http://$HONK_LIFECYCLE_LISTEN/api" -o /dev/null 2>/dev/null
}
honk_lifecycle_runtime_owned() {
honk_lifecycle_ready_owned "$1"
}
honk_lifecycle_resources_clear() {
command -v ip >/dev/null 2>&1 || return 1
for hlp_link in dae0 dae0peer; do
ip link show "$hlp_link" >/dev/null 2>&1 && return 1
done
! ip netns list 2>/dev/null | awk '{print $1}' | grep -qx daens
}
honk_lifecycle_drain() {
hlp_timeout=${1:-30}
while [ "$hlp_timeout" -gt 0 ]; do
hlp_wait=0
if [ -n "${HONK_LIFECYCLE_STOP_PID:-}" ] && \
[ "$(honk_lifecycle_starttime "$HONK_LIFECYCLE_STOP_PID" 2>/dev/null)" = "$HONK_LIFECYCLE_STOP_STARTTIME" ]; then
hlp_wait=1
fi
hlp_procd=$(honk_lifecycle_procd_pid)
[ -z "$hlp_procd" ] || hlp_wait=1
hlp_running=$(honk_lifecycle_procd_running)
case "$hlp_running" in true|1) hlp_wait=1 ;; esac
hlp_all=$(pidof honk-core 2>/dev/null)
if [ -n "$hlp_all" ]; then
for hlp_pid in $hlp_all; do
if [ "$hlp_pid" != "${HONK_LIFECYCLE_STOP_PID:-}" ] || \
[ "$(honk_lifecycle_starttime "$hlp_pid" 2>/dev/null)" != "${HONK_LIFECYCLE_STOP_STARTTIME:-}" ]; then
echo "honk lifecycle: unmanaged honk-core process remains" >&2
return 1
fi
done
hlp_wait=1
fi
honk_lifecycle_resources_clear || hlp_wait=1
honk_lifecycle_parse_listen || { echo "honk lifecycle: invalid configured listener" >&2; return 1; }
hlp_port=$HONK_LIFECYCLE_PORT
if honk_lifecycle_port_open "$hlp_port"; then hlp_wait=1; fi
if [ "$hlp_wait" = 0 ]; then
return 0
fi
sleep 1
hlp_timeout=$((hlp_timeout - 1))
done
echo "honk lifecycle: old process or network resources did not drain" >&2
return 1
}
honk_lifecycle_stop() {
hlp_timeout=${1:-30}
HONK_LIFECYCLE_STOP_PID=$(honk_lifecycle_managed_pid 2>/dev/null)
if [ -n "$HONK_LIFECYCLE_STOP_PID" ]; then
HONK_LIFECYCLE_STOP_STARTTIME=$(honk_lifecycle_starttime "$HONK_LIFECYCLE_STOP_PID") || return 1
else
[ -z "$(pidof honk-core 2>/dev/null)" ] || { echo "honk lifecycle: refusing to stop unmanaged honk-core" >&2; return 1; }
[ -z "$(honk_lifecycle_procd_pid)" ] || { echo "honk lifecycle: procd instance is not the expected core" >&2; return 1; }
case "$(honk_lifecycle_procd_running)" in true|1) echo "honk lifecycle: procd instance is still running" >&2; return 1 ;; esac
HONK_LIFECYCLE_STOP_STARTTIME=
fi
if [ -n "$HONK_LIFECYCLE_STOP_PID" ]; then /etc/init.d/honk stop >/dev/null 2>&1 || :; fi
honk_lifecycle_drain "$hlp_timeout"
}
honk_lifecycle_start() {
hlp_timeout=${1:-30}
[ -z "$(pidof honk-core 2>/dev/null)" ] || { echo "honk lifecycle: refusing start while a core process exists" >&2; return 1; }
[ -z "$(honk_lifecycle_procd_pid)" ] || { echo "honk lifecycle: procd instance has not stopped" >&2; return 1; }
case "$(honk_lifecycle_procd_running)" in true|1) echo "honk lifecycle: procd instance is still running" >&2; return 1 ;; esac
honk_lifecycle_resources_clear || { echo "honk lifecycle: network resources have not drained" >&2; return 1; }
honk_lifecycle_parse_listen || { echo "honk lifecycle: invalid configured listener" >&2; return 1; }
hlp_listen=$HONK_LIFECYCLE_LISTEN
hlp_port=$HONK_LIFECYCLE_PORT
if [ -n "${HONK_MAINT_CAP:-}" ]; then export HONK_MAINT_CAP; fi
/etc/init.d/honk start >/dev/null 2>&1
hlp_start_status=$?
unset HONK_MAINT_CAP
[ "$hlp_start_status" = 0 ] || { echo "honk lifecycle: init start failed" >&2; return 1; }
hlp_stable=0
hlp_pid=
hlp_starttime=
# A config the core cannot run crash-loops through procd: every attempt gets a
# new PID, so a fixed timeout would burn the whole budget before the caller can
# be told the start failed. Count distinct PIDs and give up early instead.
hlp_seen_pid=
hlp_attempts=0
while [ "$hlp_timeout" -gt 0 ]; do
hlp_new_pid=$(honk_lifecycle_managed_pid 2>/dev/null)
if [ -n "$hlp_new_pid" ] && [ "$hlp_new_pid" != "$hlp_seen_pid" ]; then
hlp_seen_pid=$hlp_new_pid
hlp_attempts=$((hlp_attempts + 1))
if [ "$hlp_attempts" -gt 3 ]; then
echo "honk lifecycle: core keeps restarting, giving up" >&2
return 1
fi
fi
if [ -n "$hlp_new_pid" ]; then
hlp_new_starttime=$(honk_lifecycle_starttime "$hlp_new_pid") || hlp_new_starttime=
if [ -n "$hlp_new_starttime" ] && \
{ [ -z "${HONK_LIFECYCLE_STOP_PID:-}" ] || [ "$hlp_new_pid:$hlp_new_starttime" != "$HONK_LIFECYCLE_STOP_PID:$HONK_LIFECYCLE_STOP_STARTTIME" ]; } && \
{ honk_lifecycle_ready_owned "$hlp_new_pid"; }; then
if [ "$hlp_pid:$hlp_starttime" = "$hlp_new_pid:$hlp_new_starttime" ]; then
hlp_stable=$((hlp_stable + 1))
else
hlp_pid=$hlp_new_pid
hlp_starttime=$hlp_new_starttime
hlp_stable=1
fi
if [ "$hlp_stable" -ge 3 ]; then
HONK_LIFECYCLE_STOP_PID=$hlp_new_pid
HONK_LIFECYCLE_STOP_STARTTIME=$hlp_new_starttime
return 0
fi
else
hlp_stable=0
hlp_pid=
hlp_starttime=
fi
else
hlp_stable=0
hlp_pid=
hlp_starttime=
fi
sleep 1
hlp_timeout=$((hlp_timeout - 1))
done
echo "honk lifecycle: new managed core did not become stable and own its API listener" >&2
return 1
}
honk_lifecycle_restart() {
if [ -n "$(honk_lifecycle_managed_pid 2>/dev/null)" ]; then
HONK_LIFECYCLE_WAS_RUNNING=1
else
[ -z "$(pidof honk-core 2>/dev/null)" ] || { echo "honk lifecycle: refusing unmanaged honk-core" >&2; return 1; }
HONK_LIFECYCLE_WAS_RUNNING=0
fi
honk_lifecycle_stop "${1:-30}" && honk_lifecycle_start "${1:-30}"
}
-32
View File
@@ -1,32 +0,0 @@
#!/bin/sh
HONK_LOCK_DIR=/run/honk.lock
# PID alone is not enough to identify a process (PID reuse race). We store
# "pid starttime" where starttime is field 22 of /proc/pid/stat.
_honk_proc_starttime() {
cut -d' ' -f22 "/proc/$1/stat" 2>/dev/null
}
honk_lock_acquire() {
mkdir "$HONK_LOCK_DIR" 2>/dev/null || return 1
chmod 0700 "$HONK_LOCK_DIR" || { rmdir "$HONK_LOCK_DIR"; return 1; }
printf '%s %s\n' "$$" "$(_honk_proc_starttime $$)" > "$HONK_LOCK_DIR/pid" || { rm -f "$HONK_LOCK_DIR/pid"; rmdir "$HONK_LOCK_DIR"; return 1; }
}
honk_lock_is_stale() {
[ -r "$HONK_LOCK_DIR/pid" ] || return 1
read -r owner owner_start < "$HONK_LOCK_DIR/pid" || return 1
case "$owner" in ''|*[!0-9]*) return 1 ;; esac
# Dead if the PID is gone, or if it was reused by a newer process.
! kill -0 "$owner" 2>/dev/null && return 0
[ "$(_honk_proc_starttime "$owner")" = "$owner_start" ] || return 0
return 1
}
honk_lock_release() {
[ -r "$HONK_LOCK_DIR/pid" ] || return 1
read -r owner owner_start < "$HONK_LOCK_DIR/pid" || return 1
[ "$owner" = "$$" ] && [ "$owner_start" = "$(_honk_proc_starttime $$)" ] || return 1
rm -f "$HONK_LOCK_DIR/pid" && rmdir "$HONK_LOCK_DIR"
}
-57
View File
@@ -1,57 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
requested=$1
case "$requested" in all|debug|info|warn|error) ;; *) exit 2 ;; esac
awk -v requested="$requested" '
function rank(level) {
if (level == "DEBUG" || level == "TRACE") return 0
if (level == "INFO" || level == "NOTICE") return 1
if (level == "WARN" || level == "WARNING") return 2
if (level == "ERROR" || level == "FATAL" || level == "CRIT" || level == "CRITICAL" || level == "ALERT" || level == "EMERG") return 3
return -1
}
function from_syslog(header, token, parts, count, level) {
count = split(header, parts, /[[:space:]]+/)
for (i = 1; i <= count; i++) {
token = parts[i]
if (token ~ /^[[:alnum:]_-]+\.(debug|info|notice|warn|warning|err|error|crit|critical|alert|emerg)$/) {
sub(/^.*\./, "", token)
if (token == "err") token = "error"
return rank(toupper(token))
}
}
return -1
}
{
if (requested == "all" || requested == "debug") { print; next }
line = $0
header = line
message = ""
separator = index(line, "]: ")
if (separator > 0) {
header = substr(line, 1, separator + 1)
message = substr(line, separator + 3)
} else {
separator = index(line, ": ")
if (separator > 0) {
header = substr(line, 1, separator - 1)
message = substr(line, separator + 2)
}
}
sub(/^[0-9][0-9][0-9][0-9]-[0-9T:.+Z-]+[[:space:]]+/, "", message)
sub(/^[[:space:]]+/, "", message)
if (match(message, /^(TRACE|DEBUG|INFO|NOTICE|WARN|WARNING|ERROR|FATAL|CRIT|CRITICAL|ALERT|EMERG)([[:space:]:]|$)/)) {
level = message
sub(/[[:space:]:].*$/, "", level)
severity = rank(level)
} else if (match(line, /^<[0-7]>/)) {
priority = substr(line, 2, 1) + 0
severity = priority <= 3 ? 3 : priority == 4 ? 2 : priority <= 6 ? 1 : 0
} else {
severity = from_syslog(header)
}
threshold = requested == "info" ? 1 : requested == "warn" ? 2 : 3
if (severity >= threshold) print
}'
-9
View File
@@ -1,9 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
honk_add_log_lines() {
[ -s "$1" ] || return 0
while IFS= read -r line; do
json_add_string "" "$line"
done < "$1"
}
File diff suppressed because it is too large. Load diff
-6
View File
@@ -1,6 +0,0 @@
#!/bin/sh
exec sed -E \
-e 's#([[:alnum:]+.-]+://)[^[:space:]"<>]+#\1[redacted]#g' \
-e 's/([Bb]earer[[:space:]]+)[^[:space:],;]+/\1[redacted]/g' \
-e 's/([Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Tt][Oo][Kk][Ee][Nn]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Aa][Uu][Tt][Hh][Oo][Rr][Ii][Zz][Aa][Tt][Ii][Oo][Nn])[^:=]*[=:][[:space:]]*.*/\1=[redacted]/'
-22
View File
@@ -1,22 +0,0 @@
global {
wan_interface: auto
lan_interface: br-lan
log_level: info
dial_mode: domain
allow_insecure: false
auto_config_kernel_parameter: true
data_dir: '/etc/honk'
store_subscribe: true
nfqueue_enable: true
}
experimental {
native_api {
enabled: true
listen: '127.0.0.1:9527'
password_auth: true
allow_anonymous_loopback: false
config_write: true
ui: '/usr/share/doona'
}
}
-292
View File
@@ -1,292 +0,0 @@
#!/bin/sh
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
umask 077
. /usr/share/libubox/jshn.sh
UPDATE_ROOT=/tmp/honk-update
TRUST_ROOT=/etc/honk/update-trust
FEED_FILE=$TRUST_ROOT/feed.url
MANIFEST_KEY=$TRUST_ROOT/manifest.pub
MANIFEST=$UPDATE_ROOT/manifest.json
SIGNATURE=$UPDATE_ROOT/manifest.json.sig
RECORDS=$UPDATE_ROOT/packages.tsv
UPDATE_LOCK_OWNED=0
CHECK_DIR=
update_lock_acquire() {
. /usr/share/honk/lock.sh || return 1
honk_lock_acquire || return 1
UPDATE_LOCK_OWNED=1
}
update_lock_release() {
[ "$UPDATE_LOCK_OWNED" = 1 ] || return 0
honk_lock_release >/dev/null 2>&1 || return 1
UPDATE_LOCK_OWNED=0
}
discard_check_dir() {
case "$CHECK_DIR" in
"$UPDATE_ROOT"/.check.*)
[ -d "$CHECK_DIR" ] && [ ! -L "$CHECK_DIR" ] && rm -rf "$CHECK_DIR"
;;
esac
CHECK_DIR=
}
cleanup_update() {
discard_check_dir
update_lock_release || :
}
emit_update() {
json_init
json_add_boolean ok "$1"
json_add_boolean available "$2"
json_add_boolean apply_enabled "$3"
json_add_string message "$4"
json_add_string reason "$5"
[ -z "$6" ] || json_add_string version "$6"
json_dump
}
safe_file() {
[ -f "$1" ] && [ ! -L "$1" ] && [ "$(stat -c '%u:%a' "$1" 2>/dev/null)" = "0:$2" ]
}
prepare_update_root() {
if [ -e "$UPDATE_ROOT" ] || [ -L "$UPDATE_ROOT" ]; then
[ -d "$UPDATE_ROOT" ] && [ ! -L "$UPDATE_ROOT" ] && [ "$(stat -c '%u:%a' "$UPDATE_ROOT" 2>/dev/null)" = 0:700 ] || return 1
else
mkdir "$UPDATE_ROOT" && chmod 0700 "$UPDATE_ROOT" || return 1
fi
}
read_trust() {
[ -d "$TRUST_ROOT" ] && [ ! -L "$TRUST_ROOT" ] && [ "$(stat -c '%u:%a' "$TRUST_ROOT" 2>/dev/null)" = 0:700 ] || return 1
safe_file "$FEED_FILE" 600 && safe_file "$MANIFEST_KEY" 600 || return 1
[ "$(wc -c < "$FEED_FILE" | tr -d ' ')" -le 512 ] || return 1
feed_url=$(cat "$FEED_FILE") || return 1
[ -n "$feed_url" ] || return 1
case "$feed_url" in *'?'*|*'#'*|*'@'*|*' '*|*'\'*|*'"'*|*"'"*) return 1 ;; esac
echo "$feed_url" | grep -Eq '^https://[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._~/%+-]*)?$' || return 1
return 0
}
manifest_field() {
jsonfilter -i "$MANIFEST" -e "$1" 2>/dev/null
}
installed_version() {
awk -v wanted="$1" '
$0 == "P:" wanted { found=1; next }
found && /^V:/ { print substr($0, 3); exit }
found && /^P:/ { exit }
' /lib/apk/db/installed 2>/dev/null
}
validate_manifest() {
validate_url=$1
validate_json=$(cat "$MANIFEST") || return 1
json_load "$validate_json" 2>/dev/null || return 1
json_get_type validate_packages_type packages
[ "$validate_packages_type" = array ] || return 1
json_select packages || return 1
json_get_keys validate_package_keys
[ "$(printf '%s\n' "$validate_package_keys" | wc -w | tr -d ' ')" = 2 ] || return 1
json_select .. || return 1
[ "$(manifest_field '@.schema')" = 1 ] || return 1
[ "$(manifest_field '@.product')" = openwrt-honk ] || return 1
# Target must match this device: map uname -m to manifest arch names.
case "$(uname -m)" in
x86_64) _local_arch=x86_64 ;;
aarch64) _local_arch=aarch64 ;;
armv7l) _local_arch=armv7 ;;
*) return 1 ;;
esac
[ "$(manifest_field '@.target.architecture')" = "$_local_arch" ] || return 1
# OpenWrt version must match: 24.10 (opkg/ipk) or 25.12 (apk).
_local_owrt=$(sed -n 's/^DISTRIB_RELEASE="\([0-9]*\.[0-9]*\).*/\1/p' /etc/openwrt_release 2>/dev/null | head -n 1)
case "$_local_owrt" in 24.10|25.12) ;; *) return 1 ;; esac
[ "$(manifest_field '@.target.openwrt')" = "$_local_owrt" ] || return 1
case "$_local_owrt" in
24.10) [ "$(manifest_field '@.target.package_format')" = ipk ] || return 1 ;;
25.12) [ "$(manifest_field '@.target.package_format')" = apk ] || return 1 ;;
esac
[ "$(manifest_field '@.target.kernel_min')" = 6.12 ] || return 1
[ "$(manifest_field '@.target.requires_btf')" = true ] || return 1
[ "$(manifest_field '@.min_updater_api')" = 1 ] || return 1
[ "$(manifest_field '@.state_schema')" = 2 ] || return 1
# (arch and OpenWrt version already validated against local system above)
[ -r /sys/kernel/btf/vmlinux ] || return 1
kernel_version=$(uname -r | sed 's/-.*//')
kernel_major=${kernel_version%%.*}
kernel_minor=${kernel_version#*.}
kernel_minor=${kernel_minor%%.*}
case "$kernel_major:$kernel_minor" in *[!0-9:]*|:*) return 1 ;; esac
[ "$kernel_major" -gt 6 ] || { [ "$kernel_major" = 6 ] && [ "$kernel_minor" -ge 12 ]; } || return 1
validate_commit=$(manifest_field '@.core.commit')
case "$validate_commit" in *[!0-9a-f]*|'') return 1 ;; esac
[ "${#validate_commit}" -eq 40 ] || return 1
validate_commit=$(manifest_field '@.doona.commit')
case "$validate_commit" in *[!0-9a-f]*|'') return 1 ;; esac
[ "${#validate_commit}" -eq 40 ] || return 1
for validate_sha in \
"$(manifest_field '@.core.artifact_sha256')" \
"$(manifest_field '@.doona.patch_sha256')" \
"$(manifest_field '@.doona.artifact_sha256')" \
"$(manifest_field '@.sdk_sha256')"; do
case "$validate_sha" in *[!0-9a-f]*|'') return 1 ;; esac
[ "${#validate_sha}" -eq 64 ] || return 1
done
validate_version=$(manifest_field '@.packages[0].version')
case "$validate_version" in ''|*[!A-Za-z0-9.+~-]*) return 1 ;; esac
: > "$RECORDS" || return 1
validate_seen=' '
validate_available=0
for validate_index in 0 1; do
validate_name=$(manifest_field "@.packages[$validate_index].name")
validate_version=$(manifest_field "@.packages[$validate_index].version")
validate_arch=$(manifest_field "@.packages[$validate_index].arch")
validate_filename=$(manifest_field "@.packages[$validate_index].filename")
validate_size=$(manifest_field "@.packages[$validate_index].size")
validate_sha=$(manifest_field "@.packages[$validate_index].sha256")
validate_package_url=$(manifest_field "@.packages[$validate_index].url")
case "$validate_name" in honk|luci-app-honk) ;; *) return 1 ;; esac
case "$validate_seen" in *" $validate_name "*) return 1 ;; esac
validate_seen="$validate_seen$validate_name "
case "$validate_version" in ''|*[!A-Za-z0-9.+~-]*) return 1 ;; esac
case "$validate_arch" in x86_64|aarch64|armv7|noarch) ;; *) return 1 ;; esac
case "$validate_name:$validate_arch" in honk:x86_64|honk:aarch64|honk:armv7|luci-app-honk:x86_64|luci-app-honk:aarch64|luci-app-honk:armv7|luci-app-honk:noarch) ;; *) return 1 ;; esac
[ "$validate_filename" = "$validate_name-$validate_version.apk" ] || [ "$validate_filename" = "${validate_name}_${validate_version}_${validate_arch}.ipk" ] || return 1
case "$validate_filename" in *'/'*|*'..'*|*[!A-Za-z0-9.+_-]*) return 1 ;; esac
case "$validate_size" in ''|*[!0-9]*) return 1 ;; esac
[ "$validate_size" -gt 0 ] && [ "$validate_size" -le 134217728 ] || return 1
case "$validate_sha" in *[!0-9a-f]*|'') return 1 ;; esac
[ "${#validate_sha}" -eq 64 ] || return 1
[ "$validate_package_url" = "${validate_url%/}/packages/$validate_filename" ] || return 1
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$validate_name" "$validate_version" "$validate_arch" "$validate_filename" "$validate_size" "$validate_sha" "$validate_package_url" >> "$RECORDS" || return 1
[ "$(installed_version "$validate_name")" = "$validate_version" ] || validate_available=1
done
[ "$(wc -l < "$RECORDS" | tr -d ' ')" = 2 ] || return 1
for validate_required in honk luci-app-honk; do
case "$validate_seen" in *" $validate_required "*) ;; *) return 1 ;; esac
done
update_version=$(awk -F '\t' '$1 == "honk" { print $2; exit }' "$RECORDS")
return 0
}
verify_signed_manifest() {
read_trust || return 2
prepare_update_root || return 3
CHECK_DIR=$(mktemp -d "$UPDATE_ROOT/.check.XXXXXX") || return 3
check_dir=$CHECK_DIR
chmod 0700 "$check_dir" || { discard_check_dir; return 3; }
check_manifest=$check_dir/manifest.json
check_signature=$check_dir/manifest.json.sig
if ! curl -fLsS --proto '=https' --proto-redir '=https' --tlsv1.2 --connect-timeout 4 --max-time 12 --max-filesize 65536 \
"${feed_url%/}/manifest.json" -o "$check_manifest" 2>/dev/null ||
! curl -fLsS --proto '=https' --proto-redir '=https' --tlsv1.2 --connect-timeout 4 --max-time 8 --max-filesize 4096 \
"${feed_url%/}/manifest.json.sig" -o "$check_signature" 2>/dev/null ||
! usign -V -m "$check_manifest" -p "$MANIFEST_KEY" -x "$check_signature" >/dev/null 2>&1; then
discard_check_dir
return 4
fi
MANIFEST=$check_manifest
RECORDS=$check_dir/packages.tsv
if ! validate_manifest "$feed_url"; then
discard_check_dir
return 5
fi
cp "$check_manifest" "$UPDATE_ROOT/manifest.json.new" && cp "$check_signature" "$UPDATE_ROOT/manifest.json.sig.new" || { discard_check_dir; return 3; }
cp "$RECORDS" "$UPDATE_ROOT/packages.tsv.new" || { discard_check_dir; return 3; }
printf '%s\n' "$(date +%s)" > "$UPDATE_ROOT/checked-at.new" || { discard_check_dir; return 3; }
chmod 0600 "$UPDATE_ROOT/manifest.json.new" "$UPDATE_ROOT/manifest.json.sig.new" "$UPDATE_ROOT/packages.tsv.new" "$UPDATE_ROOT/checked-at.new" || { discard_check_dir; return 3; }
mv -f "$UPDATE_ROOT/manifest.json.new" "$UPDATE_ROOT/manifest.json" &&
mv -f "$UPDATE_ROOT/manifest.json.sig.new" "$UPDATE_ROOT/manifest.json.sig" &&
mv -f "$UPDATE_ROOT/packages.tsv.new" "$UPDATE_ROOT/packages.tsv" &&
mv -f "$UPDATE_ROOT/checked-at.new" "$UPDATE_ROOT/checked-at" || { discard_check_dir; return 3; }
discard_check_dir
return 0
}
update_check() {
read_trust || { emit_update true false false no_release trusted_feed_unavailable; return 0; }
update_lock_acquire || { emit_update false false false update_check_failed operation_in_progress; return 1; }
verify_signed_manifest
check_rc=$?
update_lock_release
case "$check_rc" in
2) emit_update true false false no_release trusted_feed_unavailable; return 0 ;;
4) emit_update false false false update_check_failed signed_manifest_unavailable; return 1 ;;
3) emit_update false false false update_check_failed update_storage_unavailable; return 1 ;;
5) emit_update false false false update_check_failed manifest_incompatible; return 1 ;;
0) ;;
*) emit_update false false false update_check_failed manifest_invalid; return 1 ;;
esac
if [ "$validate_available" = 1 ]; then
emit_update true true false update_available rollback_tuple_unavailable "$update_version"
else
emit_update true false false no_update already_current "$update_version"
fi
}
update_gate() {
read_trust || { emit_update true false false no_release trusted_feed_unavailable; return 0; }
update_lock_acquire || { emit_update false false false update_check_failed operation_in_progress; return 1; }
gate_ok=1
gate_available=0
gate_reason=
prepare_update_root || { gate_ok=0; gate_reason=update_storage_unavailable; }
if [ "$gate_ok" = 1 ]; then
for gate_file in "$UPDATE_ROOT/manifest.json" "$UPDATE_ROOT/manifest.json.sig" "$UPDATE_ROOT/packages.tsv" "$UPDATE_ROOT/checked-at"; do
safe_file "$gate_file" 600 || { gate_reason=signed_manifest_missing; break; }
done
fi
if [ "$gate_ok" = 1 ] && [ -z "$gate_reason" ]; then
checked_at=$(cat "$UPDATE_ROOT/checked-at")
case "$checked_at" in ''|*[!0-9]*) gate_reason=signed_manifest_expired ;; esac
if [ -z "$gate_reason" ] && [ $(( $(date +%s) - checked_at )) -gt 600 ]; then gate_reason=signed_manifest_expired; fi
fi
if [ "$gate_ok" = 1 ] && [ -z "$gate_reason" ]; then
usign -V -m "$UPDATE_ROOT/manifest.json" -p "$MANIFEST_KEY" -x "$UPDATE_ROOT/manifest.json.sig" >/dev/null 2>&1 || { gate_ok=0; gate_reason=manifest_signature_invalid; }
fi
if [ "$gate_ok" = 1 ] && [ -z "$gate_reason" ]; then
MANIFEST=$UPDATE_ROOT/manifest.json
RECORDS=$UPDATE_ROOT/packages.tsv
validate_manifest "$feed_url" || { gate_ok=0; gate_reason=manifest_incompatible; }
fi
gate_available=${validate_available:-0}
gate_version=${update_version:-}
update_lock_release
if [ "$gate_ok" != 1 ]; then emit_update false false false update_check_failed "$gate_reason"; return 1; fi
if [ -n "$gate_reason" ]; then emit_update true false false no_release "$gate_reason"; return 0; fi
if [ "$gate_available" = 1 ]; then
emit_update true true false update_available rollback_tuple_unavailable "$gate_version"
else
emit_update true false false no_update already_current "$gate_version"
fi
}
update_apply() {
update_gate >/dev/null || { emit_update false false false update_apply_failed trusted_manifest_invalid; return 1; }
[ "$validate_available" = 1 ] || { emit_update false false false update_apply_failed no_update; return 1; }
# No previous signed package tuple or exercised downgrade path is available yet.
# Keep the backend gate closed even when the update manifest itself is compatible.
emit_update false false false update_apply_failed rollback_tuple_unavailable
return 1
}
trap cleanup_update EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
case "$1" in
check) update_check ;;
gate) update_gate ;;
apply) update_apply ;;
*) emit_update false false false update_check_failed invalid_action; exit 2 ;;
esac
@@ -1,70 +0,0 @@
name: build-apk
on:
push:
tags:
- 'v*'
- 'dev-*'
workflow_dispatch:
permissions:
contents: write
env:
SDK_DIR: /builder
PKG_DIR_NAME: luci-app-adguardhome-tk
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 90
strategy:
matrix:
sdk: [x86-64-25.12.6]
container:
image: ghcr.io/openwrt/sdk:${{ matrix.sdk }}
options: --user root
defaults:
run:
shell: bash
steps:
- name: 检出代码
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: 缓存依赖源码下载
uses: actions/cache@v5
with:
path: ${{ env.SDK_DIR }}/dl
key: adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-${{ hashFiles('Makefile', '.github/workflows/build-apk.yaml', 'scripts/*.sh') }}
restore-keys: |
adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-
- name: 准备 SDK
run: bash scripts/prepare-sdk.sh
- name: 构建包
run: runuser -u buildbot -- bash scripts/build-apk.sh
- name: 整理 APK 产物
run: bash scripts/collect-apks.sh
- name: 上传 APK 压缩包
uses: actions/upload-artifact@v7
with:
name: adguardhome-apks
path: dist/*.apk
archive: true
if-no-files-found: error
- name: 发布到 Releases
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
prerelease: ${{ startsWith(github.ref_name, 'dev-') }}
make_latest: 'false'
fail_on_unmatched_files: true
files: dist/*.apk
-2
View File
@@ -1,2 +0,0 @@
.DS_Store
.local
-109
View File
@@ -1,109 +0,0 @@
#
# Copyright (C) 2025 w9315273
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-adguardhome-tk
PKG_VERSION:=2.6.0
PKG_RELEASE:=3
PKG_MAINTAINER:=w9315273
PKG_BUILD_DEPENDS:=luci-base/host
include $(INCLUDE_DIR)/package.mk
define Package/luci-app-adguardhome-tk
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=LuCI app for AdGuardHome
PKG_MAINTAINER:=https://github.com/w9315273/luci-app-adguardhome
PKGARCH:=all
PROVIDES:=luci-app-adguardhome
CONFLICTS:=luci-app-adguardhome
DEPENDS:=+luci-base +rpcd-mod-ucode +uclient-fetch +jsonfilter +flock +ca-bundle +!(PACKAGE_libustream-openssl||PACKAGE_libustream-wolfssl):libustream-mbedtls +tar
endef
define Package/luci-app-adguardhome-tk/description
LuCI support for AdGuardHome (nftables-based DNS redirect).
This package does NOT include the AdGuardHome core binary.
endef
define Package/luci-i18n-adguardhome-tk-zh-cn
SECTION:=luci
CATEGORY:=LuCI
HIDDEN:=1
TITLE:=AdGuardHome - Simplified Chinese translation
PKGARCH:=all
PROVIDES:=luci-i18n-adguardhome-zh-cn
CONFLICTS:=luci-i18n-adguardhome-zh-cn
DEPENDS:=luci-app-adguardhome-tk
DEFAULT:=LUCI_LANG_zh_Hans
endef
define Package/luci-i18n-adguardhome-tk-zh-cn/description
Simplified Chinese translation for luci-app-adguardhome-tk.
endef
define Build/Prepare
endef
define Build/Compile
endef
define Package/luci-app-adguardhome-tk/conffiles
/etc/config/AdGuardHome
endef
define Package/luci-app-adguardhome-tk/install
cp -pR ./root/* $(1)/
endef
define Package/luci-i18n-adguardhome-tk-zh-cn/install
$(INSTALL_DIR) $(1)/etc/uci-defaults
echo "uci set luci.languages.zh_cn='简体中文 (Simplified Chinese)'; uci commit luci" \
> $(1)/etc/uci-defaults/luci-i18n-adguardhome-tk-zh-cn
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
po2lmo ./po/zh-cn/AdGuardHome.po \
$(1)/usr/lib/lua/luci/i18n/AdGuardHome.zh-cn.lmo
endef
define Package/luci-app-adguardhome-tk/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] && exit 0
/etc/init.d/AdGuardHome enable >/dev/null 2>&1
enable=$$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)
if [ "$$enable" = "1" ]; then
/etc/init.d/AdGuardHome reload
fi
# Remove the former menu definition when upgrading.
rm -f /usr/share/luci/menu.d/luci-app-adguardhome-js.json
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
rm -f /tmp/luci-modulecache/*
/etc/init.d/rpcd reload
exit 0
endef
define Package/luci-app-adguardhome-tk/prerm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
/etc/init.d/AdGuardHome disable
/etc/init.d/AdGuardHome stop 2>/dev/null || true
uci -q batch <<-EOF >/dev/null 2>&1
delete ucitrack.@AdGuardHome[-1]
commit ucitrack
EOF
fi
exit 0
endef
$(eval $(call BuildPackage,luci-app-adguardhome-tk))
$(eval $(call BuildPackage,luci-i18n-adguardhome-tk-zh-cn))
-35
View File
@@ -1,35 +0,0 @@
# AdGuard Home for LuCI
Manage AdGuard Home in OpenWrt's LuCI interface. LuCI pages are rewritten in JavaScript, with an rpcd + ucode backend and nftables-based DNS forwarding.
[简体中文](README.md) · [English](README.en.md)
## Features
- Service status and core updates
- DNS handling through firewall forwarding, dnsmasq upstream, or port 53 replacement
- YAML configuration editing, validation, and web login password changes
- Runtime logs with reverse order, local timestamps, download, and a verbose logging toggle
> [!TIP]
> **Quick installation**
>
> Run in the terminal of an OpenWrt router using the APK package manager:
>
> ```sh
> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh
> ```
## Screenshots
### Basic settings
![Basic settings](screenshots/base-en.png)
### Manual configuration
![Manual configuration](screenshots/manual-en.png)
### Logs
![Runtime logs](screenshots/log-en.png)
-35
View File
@@ -1,35 +0,0 @@
# AdGuard Home for LuCI
在 OpenWrt 的 LuCI 界面中管理 AdGuard Home。使用 JavaScript 重写 LuCI 页面,后端采用 rpcd + ucode,并适配 nftables DNS 转发。
[简体中文](README.md) · [English](README.en.md)
## 功能
- 服务状态查看与核心更新
- 防火墙转发、dnsmasq 上游、替代 dnsmasq 使用 53 端口等 DNS 接入方式
- YAML 配置编辑与校验、网页登录密码修改
- 运行日志查看,支持逆序、本地时间、下载和详细日志开关
> [!TIP]
> **一键安装**
>
> 在使用 APK 包管理器的 OpenWrt 路由器终端执行:
>
> ```sh
> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh zh-cn
> ```
## 截图
### 基础设置
![基础设置](screenshots/base-zh-cn.png)
### 手动设置
![手动设置](screenshots/manual-zh-cn.png)
### 日志
![运行日志](screenshots/log-zh-cn.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

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