update 2026-09-24 10:46:53

This commit is contained in:
action committed 2026-09-24 10:46:53 +08:00
1 parent 907a158127
commit b31a5c3878
4914 files changed
-1716179

No files matched your search

-149
View File
@@ -1,149 +0,0 @@
#
# Copyright (C) 2015-2016 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE.txt for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=AdGuardHome
PKG_VERSION:=0.107.79
PKG_RELEASE:=1
# 上游自 v0.107 起为“单二进制 + go:embed 前端”:前端由 client_v2 (npm/webpack)
# 构建到源码根的 build/ 目录,再由 main.go 的 //go:embed build 打包进二进制。
# 这里使用官方 codeload 源码包(比 git clone 快,且可用 PKG_HASH 校验收包)。
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_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
PKG_LICENSE:=GPL-3.0-only
PKG_LICENSE_FILES:=LICENSE.txt
PKG_MAINTAINER:=Yang Feng <yangfeng229@gmail.com>
PKG_CONFIG_DEPENDS:=CONFIG_ADGUARDHOME_COMPRESS_UPX
# ---------------------------------------------------------------------------
# 工具链说明
#
# 本包不使用 OpenWrt 的 golang/host 与 node/host:
# * AdGuardHome 的 go.mod 要求 go >= 1.26.6,而 OpenWrt 24.10 的 golang feed
# 仍是 1.23.x;
# * node/host 需要从源码编译 Node,代价极高。
# 因此直接调用外部工具链(默认优先取容器内预装的 ~/.openwrt/tools),
# 可用下列变量覆盖:
# ADGH_GO Go 可执行文件(需 >= 1.26)
# ADGH_NODE_DIR 含 node/npm 的目录
# ADGH_NPM npm 可执行文件
# ADGH_NPM_REGISTRY npm 源(默认 npmmirror,国内更快)
# ADGH_GOPROXY Go 模块代理(默认 goproxy.cn)
# 工具链准备方式见 .devcontainer/README.md。
# ---------------------------------------------------------------------------
ADGH_TOOLS_DIR ?= $(HOME)/.openwrt/tools
ADGH_GO ?= $(if $(wildcard $(ADGH_TOOLS_DIR)/go/bin/go),$(ADGH_TOOLS_DIR)/go/bin/go,go)
ADGH_NODE_DIR ?= $(if $(wildcard $(ADGH_TOOLS_DIR)/node/bin/node),$(ADGH_TOOLS_DIR)/node/bin,$(if $(wildcard /usr/bin/node),/usr/bin,/usr/local/bin))
ADGH_NPM ?= $(ADGH_NODE_DIR)/npm
ADGH_NPM_REGISTRY ?= https://registry.npmmirror.com
ADGH_GOPROXY ?= https://goproxy.cn,direct
# OpenWrt ARCH -> GOARCH:纯 Go 交叉编译(CGO_ENABLED=0),无需 target 工具链
ifeq ($(ARCH),x86_64)
ADGH_GOARCH:=amd64
else ifeq ($(ARCH),i386)
ADGH_GOARCH:=386
else ifneq ($(filter aarch64%,$(ARCH)),)
ADGH_GOARCH:=arm64
else ifneq ($(filter arm%,$(ARCH)),)
ADGH_GOARCH:=arm
ADGH_GOARM:=$(if $(findstring _v5,$(ARCH)),5,$(if $(findstring _v6,$(ARCH)),6,7))
else ifneq ($(filter mips64el%,$(ARCH)),)
ADGH_GOARCH:=mips64le
ADGH_GOMIPS:=softfloat
else ifneq ($(filter mipsel%,$(ARCH)),)
ADGH_GOARCH:=mipsle
ADGH_GOMIPS:=softfloat
else ifneq ($(filter mips64%,$(ARCH)),)
ADGH_GOARCH:=mips64
ADGH_GOMIPS:=softfloat
else ifneq ($(filter mips%,$(ARCH)),)
ADGH_GOARCH:=mips
ADGH_GOMIPS:=softfloat
else ifneq ($(filter riscv64%,$(ARCH)),)
ADGH_GOARCH:=riscv64
else ifneq ($(filter powerpc64%,$(ARCH)),)
ADGH_GOARCH:=ppc64
else ifneq ($(filter powerpc%,$(ARCH)),)
ADGH_GOARCH:=ppc
else
ADGH_GOARCH:=unsupported
endif
ADGH_VERSION_PKG:=github.com/AdguardTeam/AdGuardHome/internal/version
ADGH_LDFLAGS:=-s -w \
-X $(ADGH_VERSION_PKG).version=v$(PKG_VERSION) \
-X $(ADGH_VERSION_PKG).channel=release \
-X $(ADGH_VERSION_PKG).committime=$(if $(SOURCE_DATE_EPOCH),$(SOURCE_DATE_EPOCH),0) \
$(if $(ADGH_GOARM),-X $(ADGH_VERSION_PKG).goarm=$(ADGH_GOARM)) \
$(if $(ADGH_GOMIPS),-X $(ADGH_VERSION_PKG).gomips=$(ADGH_GOMIPS))
include $(INCLUDE_DIR)/package.mk
define Package/AdGuardHome
SECTION:=net
CATEGORY:=Network
TITLE:=Network-wide ads and trackers blocking DNS server
URL:=https://github.com/AdguardTeam/AdGuardHome
DEPENDS:=+ca-bundle
endef
define Package/AdGuardHome/description
Free and open source, powerful network-wide ads and trackers blocking DNS server.
endef
define Package/$(PKG_NAME)/config
config ADGUARDHOME_COMPRESS_UPX
bool "Compress executable files with UPX"
default n
endef
define Build/Compile
@if ! $(ADGH_GO) version 2>/dev/null | grep -qE 'go1\.(2[6-9]|[3-9][0-9])'; then \
echo "ERROR: AdGuardHome $(PKG_VERSION) 需要 Go >= 1.26(go.mod 要求 1.26.6)"; \
echo " 当前工具链: $$($(ADGH_GO) version 2>&1)"; \
echo " 可用 ADGH_GO=/path/to/go 指定,准备方法见 .devcontainer/README.md"; \
exit 1; \
fi
@echo "AdGuardHome: 构建前端 client_v2 (npm)"
cd $(PKG_BUILD_DIR)/client_v2 && \
PATH="$(ADGH_NODE_DIR):$(PATH)" $(ADGH_NPM) ci \
--registry=$(ADGH_NPM_REGISTRY) --no-audit --no-fund && \
PATH="$(ADGH_NODE_DIR):$(PATH)" $(ADGH_NPM) run build-prod
@echo "AdGuardHome: 编译后端 (GOARCH=$(ADGH_GOARCH))"
cd $(PKG_BUILD_DIR) && \
CGO_ENABLED=0 GOOS=linux GOARCH=$(ADGH_GOARCH) \
GOARM=$(ADGH_GOARM) GOMIPS=$(ADGH_GOMIPS) \
GOPROXY=$(ADGH_GOPROXY) GOTOOLCHAIN=local \
$(ADGH_GO) build -trimpath -ldflags "$(ADGH_LDFLAGS)" \
-o $(PKG_BUILD_DIR)/AdGuardHome .
ifeq ($(CONFIG_ADGUARDHOME_COMPRESS_UPX),y)
@if [ -x $(STAGING_DIR_HOST)/bin/upx ]; then \
$(STAGING_DIR_HOST)/bin/upx --lzma --best $(PKG_BUILD_DIR)/AdGuardHome; \
else \
echo "WARNING: upx 不可用,跳过压缩"; \
fi
endif
endef
# luci 插件默认使用 /etc/config/adGuardConfig/AdGuardHome(uci: binpath),
# 因此二进制装到该路径;/usr/bin/AdGuardHome 以符号链接提供命令行入口,
# 避免在 ipk 里重复打包 40MB 的二进制。
define Package/AdGuardHome/install
$(INSTALL_DIR) $(1)/etc/config/adGuardConfig
$(INSTALL_BIN) $(PKG_BUILD_DIR)/AdGuardHome $(1)/etc/config/adGuardConfig/AdGuardHome
$(INSTALL_DIR) $(1)/usr/bin
$(LN) /etc/config/adGuardConfig/AdGuardHome $(1)/usr/bin/AdGuardHome
endef
$(eval $(call BuildPackage,AdGuardHome))
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-56
View File
@@ -1,56 +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:=LingTiGameAcc
PKG_VERSION:=2023
PKG_RELEASE:=1.7.1
PKG_LICENSE:=GPLv3
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=eSir Playground
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
CATEGORY:=eSir Playground
SUBMENU:=2. GameAcc
TITLE:=LingTi Game Accelerator
URL:=https://github.com/esirplayground/LingTiGameAcc
DEPENDS:=+kmod-tun
endef
define Package/$(PKG_NAME)/description
LingTi Game Acc is a Game Accelerator which is paid service.
endef
DIR_ARCH:=$(ARCH)
define Build/Prepare
mkdir $(PKG_BUILD_DIR)/$(PKG_NAME)
cp -r ./files/$(DIR_ARCH)/* $(PKG_BUILD_DIR)/$(PKG_NAME)/lingti
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/$(PKG_NAME)/lingti $(1)/usr/bin
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./root/etc/init.d/lingti $(1)/etc/init.d
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
chmod +x $(1)/usr/bin/lingti
chmod +x $(1)/etc/init.d/lingti
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
-3
View File
@@ -1,3 +0,0 @@
# LingTiGameAcc
This Tools can be functioning `independently` without luci app.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-28
View File
@@ -1,28 +0,0 @@
#!/bin/sh /etc/rc.common
START=99
[ -d /var/bin ] || mkdir -p /var/bin
[ -f /var/bin/lingti ] || cp /usr/bin/lingti /var/bin/lingti
start() {
state=`pgrep -f "/var/bin/lingti"`
if [ ! -z "$state" ]; then
restart
else
/var/bin/lingti &
fi
echo "LingTiGameAcc Started..."
}
stop() {
kill -9 `pgrep -f "/var/bin/lingti"` 2>/dev/null
echo "LingTiGameAcc Stopped ..."
}
restart() {
stop
sleep 1
start
echo "Restarted."
}
-28
View File
@@ -1,28 +0,0 @@
<div align="center">
<img src="https://github-readme-stats.vercel.app/api?username=kenzok8&show_icons=true&theme=default&hide_border=true&cache_seconds=86400" alt="kenzok8 github stats" height="165">
<img src="https://github-readme-stats.vercel.app/api/top-langs/?username=kenzok8&layout=compact&theme=default&hide_border=true&cache_seconds=86400" alt="kenzok8 top langs" height="165">
</div>
<div align="center">
<h1 align="center">wall仓库</h1>
<img src="https://img.shields.io/github/issues/kenzok8/wall?color=green">
<img src="https://img.shields.io/github/stars/kenzok8/wall?color=yellow">
<img src="https://img.shields.io/github/forks/kenzok8/wall?color=orange">
<img src="https://img.shields.io/github/license/kenzok8/wall?color=ff69b4">
<img src="https://img.shields.io/github/languages/code-size/kenzok8/wall?color=blueviolet">
</div>
#### wall
* 集合了常用的几个插件更新,
* adguardhome
* alist
* filebrowser
* gost
* lucky
* mosdns
* smartdns
* UnblockNeteaseMusic
* 感谢以上github仓库所有者!
-83
View File
@@ -1,83 +0,0 @@
# SPDX-Identifier-License: GPL-3.0-only
#
# Copyright (C) 2021 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=UnblockNeteaseMusic-Go
PKG_VERSION:=0.2.13
PKG_RELEASE:=$(AUTORELEASE)
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/cnsilvan/UnblockNeteaseMusic/tar.gz/$(PKG_VERSION)?
PKG_HASH:=92201b7f04ab1015c806c672b98a29b97d0f137d9b60e6d35d279c2064ed86a4
PKG_LICENSE:=GPL-3.0
PKG_LICENSE_FILE:=LICENSE
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_CONFIG_DEPENDS:= \
CONFIG_UNBLOCKNETEASEMUSIC_GO_COMPRESS_GOPROXY \
CONFIG_UNBLOCKNETEASEMUSIC_GO_COMPRESS_UPX
PKG_BUILD_DIR:=$(BUILD_DIR)/$(firstword $(subst -, ,$(PKG_NAME)))-$(PKG_VERSION)
PKG_BUILD_DEPENDS:=golang/host upx/host
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
GO_PKG:=github.com/cnsilvan/UnblockNeteaseMusic
GO_PKG_BUILD_PKG:=$$(GO_PKG)
GO_PKG_LDFLAGS:=-s -w
COMPILE_TIME:= $(shell TZ=UTC-8 date '+%Y-%m-%d %H:%M:%S')
GO_PKG_LDFLAGS+= \
-X '$(GO_PKG)/version.Version=$(PKG_VERSION)' \
-X '$(GO_PKG)/version.BuildTime=$(COMPILE_TIME)' \
-X '$(GO_PKG)/version.ExGoVersionInfo=$(GO_ARM) $(GO_MIPS)$(GO_MIPS64)'
include $(INCLUDE_DIR)/package.mk
include ../../lang/golang/golang-package.mk
define Package/UnblockNeteaseMusic-Go/config
config UNBLOCKNETEASEMUSIC_GO_COMPRESS_GOPROXY
bool "Compiling with GOPROXY proxy"
default n
config UNBLOCKNETEASEMUSIC_GO_COMPRESS_UPX
bool "Compress executable files with UPX"
depends on !mips64
default n
endef
ifeq ($(CONFIG_UNBLOCKNETEASEMUSIC_GO_COMPRESS_GOPROXY),y)
export GO111MODULE=on
export GOPROXY=https://goproxy.io
endif
define Package/UnblockNeteaseMusic-Go
SECTION:=multimedia
CATEGORY:=Multimedia
TITLE:=Revive Netease Cloud Music (Golang)
URL:=https://github.com/cnsilvan/UnblockNeteaseMusic
DEPENDS:=$(GO_ARCH_DEPENDS)
endef
define Build/Compile
$(call GoPackage/Build/Compile)
ifeq ($(CONFIG_UNBLOCKNETEASEMUSIC_GO_COMPRESS_UPX),y)
$(STAGING_DIR_HOST)/bin/upx --lzma --best $(GO_PKG_BUILD_BIN_DIR)/UnblockNeteaseMusic
endif
endef
define Package/UnblockNeteaseMusic-Go/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/UnblockNeteaseMusic $(1)/usr/bin/UnblockNeteaseMusic
$(INSTALL_DIR) $(1)/usr/share/UnblockNeteaseMusicGo
$(CP) ./files/* $(1)/usr/share/UnblockNeteaseMusicGo/
endef
$(eval $(call GoBinPackage,UnblockNeteaseMusic-Go))
$(eval $(call BuildPackage,UnblockNeteaseMusic-Go))
-14
View File
@@ -1,14 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICIjCCAaigAwIBAgIUTc9HQDej5hLCQ74u436a5yE4MDcwCgYIKoZIzj0EAwMw
SDELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRlYXNlTXVzaWMgUm9v
dCBDQTETMBEGA1UECgwKMTcxNTE3MzMyOTAeFw0yMTA0MzAwNzIzMDJaFw0yNjA0
MjkwNzIzMDJaMEgxCzAJBgNVBAYTAkNOMSQwIgYDVQQDDBtVbmJsb2NrTmV0ZWFz
ZU11c2ljIFJvb3QgQ0ExEzARBgNVBAoMCjE3MTUxNzMzMjkwdjAQBgcqhkjOPQIB
BgUrgQQAIgNiAASIyI7lYgGlq49qWtY1O2/XNDeowYf7W/Z+l7C14bphxAJ9jSDo
tLwbFPWy5VPENc0rB0/yeHA2z7LU67POL2gGgp+17y7scLkkBk3Q7wRMETrtP44Z
ITBstZ0wzVyyQEKjUzBRMB0GA1UdDgQWBBQ2F7+t8cPHJaWuCD8RHTSdLugKYzAf
BgNVHSMEGDAWgBQ2F7+t8cPHJaWuCD8RHTSdLugKYzAPBgNVHRMBAf8EBTADAQH/
MAoGCCqGSM49BAMDA2gAMGUCMQDqaRX2e01e0U+f0As/KUKDhmG5ElkK5CjYK9jk
kXLNGFXJLGta6CDvjtMLBPc20qkCMBvDs+JnJKVBEJNZVsRBBs+v2YxNU/u2aYJa
dMwXuFveSDWOS7mBeRztX/geEggiSw==
-----END CERTIFICATE-----
-15
View File
@@ -1,15 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICRDCCAcqgAwIBAgIUeVqRrT2mHG5Mc8JD+ErphiAmlgkwCgYIKoZIzj0EAwMw
SDELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRlYXNlTXVzaWMgUm9v
dCBDQTETMBEGA1UECgwKMTcxNTE3MzMyOTAeFw0yMTA0MzAwNzIzMDJaFw0yMjA0
MzAwNzIzMDJaMHsxCzAJBgNVBAYTAkNOMREwDwYDVQQHDAhIYW5nemhvdTEsMCoG
A1UECgwjTmV0RWFzZSAoSGFuZ3pob3UpIE5ldHdvcmsgQ28uLCBMdGQxETAPBgNV
BAsMCElUIERlcHQuMRgwFgYDVQQDDA8qLm11c2ljLjE2My5jb20wdjAQBgcqhkjO
PQIBBgUrgQQAIgNiAAQTPyU9RQ1pAFMLmozi+c4pEC1rrxAlPGwO9Em+qV+a5qLW
gQjjsJeabMqJ/UQ7hDtdKVxWuXiAjMiDcXwL63I71MZKPTAEKXdCmNQwb4kXvRUn
oOR4r7BMxEpGlf0CULWjQjBAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMCkGA1UdEQQi
MCCCDW11c2ljLjE2My5jb22CDyoubXVzaWMuMTYzLmNvbTAKBggqhkjOPQQDAwNo
ADBlAjEAs5bdgnNP/DiK919RiWscC0kyuY0ugG1C8m8F2Yod4MI3oTyrkVcag21o
NSzm802uAjBoPuKEbjjFP4ics0BQdICiVd6WCVAsE69FnlmqRteAJqxvdKGpVLi+
Qi3arfomrrc=
-----END CERTIFICATE-----
-9
View File
@@ -1,9 +0,0 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDBfW3twxGaQmMzP9p0/UU5EvHFVCbBw4piVFJ+pm/uFY6CKZkC5LGMa
Uc9vn/KiewGgBwYFK4EEACKhZANiAAQTPyU9RQ1pAFMLmozi+c4pEC1rrxAlPGwO
9Em+qV+a5qLWgQjjsJeabMqJ/UQ7hDtdKVxWuXiAjMiDcXwL63I71MZKPTAEKXdC
mNQwb4kXvRUnoOR4r7BMxEpGlf0CULU=
-----END EC PRIVATE KEY-----
@@ -1,144 +0,0 @@
From f4f5d11b578a1ab2c3d089bbe5453052b43892bb Mon Sep 17 00:00:00 2001
From: tofuliang <tofuliang@gmail.com>
Date: Mon, 24 Jan 2022 18:53:11 +0800
Subject: [PATCH] fix block ad,add web traffic logs
---
app.go | 1 +
config/config.go | 1 +
processor/processor.go | 55 +++++++++++++++++++++++++++++++++---------
3 files changed, 45 insertions(+), 12 deletions(-)
diff --git a/app.go b/app.go
index 73a6070..1018d75 100644
--- a/app.go
+++ b/app.go
@@ -45,6 +45,7 @@ func main() {
log.Println("EnableLocalVip=", *config.EnableLocalVip)
log.Println("UnlockSoundEffects=", *config.UnlockSoundEffects)
log.Println("QQCookieFile=", *config.QQCookieFile)
+ log.Println("LogWebTraffic=", *config.LogWebTraffic)
if host.InitHosts() == nil {
//go func() {
// // // terminal: $ go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
diff --git a/config/config.go b/config/config.go
index 6c07873..a653cdf 100644
--- a/config/config.go
+++ b/config/config.go
@@ -31,6 +31,7 @@ var (
EnableLocalVip = flag.Bool("lv", false, "enable local vip")
UnlockSoundEffects = flag.Bool("sef", false, "unlock SoundEffects")
QQCookieFile = flag.String("qc", "./qq.cookie", "specify cookies file ,such as : \"qq.cookie\"")
+ LogWebTraffic = flag.Bool("wl", false, "log request url and response")
)
func ValidParams() bool {
diff --git a/processor/processor.go b/processor/processor.go
index 8d09dbf..d07b9d3 100644
--- a/processor/processor.go
+++ b/processor/processor.go
@@ -6,14 +6,6 @@ import (
"crypto/md5"
"encoding/hex"
"encoding/json"
- "github.com/cnsilvan/UnblockNeteaseMusic/cache"
- "github.com/cnsilvan/UnblockNeteaseMusic/common"
- "github.com/cnsilvan/UnblockNeteaseMusic/config"
- "github.com/cnsilvan/UnblockNeteaseMusic/network"
- "github.com/cnsilvan/UnblockNeteaseMusic/processor/crypto"
- "github.com/cnsilvan/UnblockNeteaseMusic/provider"
- "github.com/cnsilvan/UnblockNeteaseMusic/utils"
- "golang.org/x/text/width"
"io"
"io/ioutil"
"log"
@@ -22,6 +14,15 @@ import (
"regexp"
"strconv"
"strings"
+
+ "github.com/cnsilvan/UnblockNeteaseMusic/cache"
+ "github.com/cnsilvan/UnblockNeteaseMusic/common"
+ "github.com/cnsilvan/UnblockNeteaseMusic/config"
+ "github.com/cnsilvan/UnblockNeteaseMusic/network"
+ "github.com/cnsilvan/UnblockNeteaseMusic/processor/crypto"
+ "github.com/cnsilvan/UnblockNeteaseMusic/provider"
+ "github.com/cnsilvan/UnblockNeteaseMusic/utils"
+ "golang.org/x/text/width"
)
var (
@@ -188,6 +189,9 @@ func RequestAfter(request *http.Request, response *http.Response, netease *Netea
if ok {
code = codeN.String()
}
+
+ logResponse(netease)
+
if strings.EqualFold(netease.Path, "/api/osx/version") {
modified = disableUpdate(netease)
} else if strings.Contains(netease.Path, "/usertool/sound/") {
@@ -197,9 +201,24 @@ func RequestAfter(request *http.Request, response *http.Response, netease *Netea
for key, resp := range netease.JsonBody {
if strings.Contains(key, "/usertool/sound/") {
modified = unblockSoundEffects(resp.(map[string]interface{}))
- } else if *config.BlockAds && strings.Contains(netease.Path, "api/ad/") {
+ } else if *config.BlockAds && strings.Contains(key, "api/ad/") {
+ log.Println("block Ad has been triggered(" + key + ").")
resp = &common.MapType{}
modified = true
+ } else if *config.BlockAds && strings.EqualFold(key, "/api/v2/banner/get") {
+ newInfo := make(common.SliceType, 0)
+ info := netease.JsonBody[key]
+ for _, data := range info.(common.MapType)["banners"].(common.SliceType) {
+ if banner, ok := data.(common.MapType); ok {
+ if banner["adid"] == nil {
+ newInfo = append(newInfo, banner)
+ } else {
+ log.Println("block banner Ad has been triggered.")
+ modified = true
+ }
+ }
+ }
+ info.(common.MapType)["banners"] = newInfo
}
}
} else if !netease.Web && (code == "401" || code == "512") && strings.Contains(netease.Path, "manipulate") {
@@ -220,7 +239,9 @@ func RequestAfter(request *http.Request, response *http.Response, netease *Netea
// log.Println("NeedRepackage")
modifiedJson, _ := json.Marshal(netease.JsonBody)
// log.Println(netease)
- // log.Println(string(modifiedJson))
+ if *config.LogWebTraffic {
+ log.Println("modified =>\n" + string(modifiedJson))
+ }
if netease.Encrypted {
modifiedJson = crypto.AesEncryptECB(modifiedJson, []byte(aeskey))
}
@@ -258,14 +279,24 @@ func disableUpdate(netease *Netease) bool {
if len(value.(common.SliceType)) > 0 {
modified = true
jsonBody["updateFiles"] = make(common.SliceType, 0)
+ log.Println("disable update has been triggered.")
}
default:
}
}
- // modifiedJson, _ := json.Marshal(jsonBody)
- // log.Println(string(modifiedJson))
return modified
}
+
+func logResponse(netease *Netease) {
+ if *config.LogWebTraffic {
+ reqUrl := netease.Path
+ jsonBody := netease.JsonBody
+ modifiedJson, _ := json.Marshal(jsonBody)
+ sep := "===================================\n"
+ log.Println(sep + reqUrl + " => \n" + string(modifiedJson) + "\n")
+ }
+}
+
func localVIP(netease *Netease) bool {
if !*config.EnableLocalVip {
return false
@@ -1,29 +0,0 @@
From 6c009953d357d1cc03478cf65fc05701fb1966d6 Mon Sep 17 00:00:00 2001
From: ameansone <ameansone@outlook.com>
Date: Sun, 5 Dec 2021 19:18:20 +0800
Subject: [PATCH] fix(processor): avoid unnecessary decryption
---
processor/processor.go | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/processor/processor.go b/processor/processor.go
index 8d09dbf..011571b 100644
--- a/processor/processor.go
+++ b/processor/processor.go
@@ -177,9 +177,13 @@ func RequestAfter(request *http.Request, response *http.Response, netease *Netea
if netease.Forward {
aeskey = linuxApiKey
}
- decryptECBBytes, encrypted := crypto.AesDecryptECB(decryptECBBytes, []byte(aeskey))
- netease.Encrypted = encrypted
result := utils.ParseJson(decryptECBBytes)
+ netease.Encrypted = false;
+ if result == nil {
+ decryptECBBytes, encrypted := crypto.AesDecryptECB(decryptECBBytes, []byte(aeskey))
+ netease.Encrypted = encrypted
+ result = utils.ParseJson(decryptECBBytes)
+ }
netease.JsonBody = result
modified := false
-47
View File
@@ -1,47 +0,0 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=UnblockNeteaseMusic
PKG_VERSION:=0.28.0
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/UnblockNeteaseMusic/server/tar.gz/v${PKG_VERSION}?
PKG_HASH:=0a1e843ffaa7b8ce65cc221daec07b63754473f0430019acb228f5b254602318
PKG_SOURCE_SUBDIR:=$(PKG_NAME)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR)
PKG_BUILD_PARALLEL:=1
include $(INCLUDE_DIR)/package.mk
define Package/UnblockNeteaseMusic
SECTION:=multimedia
CATEGORY:=Multimedia
TITLE:=Revive Netease Cloud Music (NodeJS)
URL:=https://github.com/nondanee/UnblockNeteaseMusic
DEPENDS:=+node
PKGARCH:=all
endef
define Package/$(PKG_NAME)/description
Revive Netease Cloud Music (NodeJS)
endef
define Build/Prepare
tar -xzvf $(DL_DIR)/$(PKG_SOURCE) -C $(PKG_BUILD_DIR)
mkdir -p $(PKG_BUILD_DIR)/$(PKG_NAME)
echo -e $(PKG_VERSION) > $(PKG_BUILD_DIR)/$(PKG_NAME)/core_ver
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/share/$(PKG_NAME)
$(CP) $(PKG_BUILD_DIR)/$(PKG_NAME)/* $(1)/usr/share/$(PKG_NAME)
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
-91
View File
@@ -1,91 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-only
include $(TOPDIR)/rules.mk
PKG_NAME:=adguardhome
PKG_VERSION:=0.107.79
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_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
PKG_LICENSE:=GPL-3.0-only
PKG_LICENSE_FILES:=LICENSE.txt
PKG_CPE_ID:=cpe:/a:adguard:adguardhome
PKG_MAINTAINER:=Dobroslaw Kijowski <dobo90@gmail.com>, George Sapkin <george@sapk.in>
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/AdguardTeam/AdGuardHome
GO_PKG_BUILD_PKG:=$(GO_PKG)
GO_PKG_LDFLAGS_X:= \
$(GO_PKG)/internal/version.channel=release \
$(GO_PKG)/internal/version.version=v$(PKG_VERSION) \
$(GO_PKG)/internal/version.committime=$(SOURCE_DATE_EPOCH) \
$(GO_PKG)/internal/version.goarm=$(GO_ARM) \
$(GO_PKG)/internal/version.gomips=$(GO_MIPS)
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/adguardhome
SECTION:=net
CATEGORY:=Network
TITLE:=Network-wide ads and trackers blocking DNS server
URL:=https://github.com/AdguardTeam/AdGuardHome
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
USERID:=adguardhome=853:adguardhome=853
endef
define Package/adguardhome/conffiles
/etc/adguardhome/adguardhome.yaml
/etc/config/adguardhome
endef
define Package/adguardhome/description
Free and open source, powerful network-wide ads and trackers blocking DNS server.
endef
define Download/adguardhome-frontend
URL:=$(FRONTEND_URL)
URL_FILE:=AdGuardHome_frontend.tar.gz
FILE:=$(FRONTEND_DEST)
HASH:=$(FRONTEND_HASH)
endef
define Build/Prepare
$(call Build/Prepare/Default)
gzip -dc $(DL_DIR)/$(FRONTEND_DEST) | $(HOST_TAR) -C $(PKG_BUILD_DIR)/ $(TAR_OPTIONS)
endef
define Package/adguardhome/install
$(call GoPackage/Package/Install/Bin,$(1))
$(INSTALL_DIR) $(1)/etc/capabilities
$(INSTALL_CONF) ./files/adguardhome.json $(1)/etc/capabilities/adguardhome.json
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/adguardhome.conf $(1)/etc/config/adguardhome
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/adguardhome.init $(1)/etc/init.d/adguardhome
$(INSTALL_DIR) $(1)/etc/sysctl.d
$(INSTALL_CONF) ./files/adguardhome.sysctl $(1)/etc/sysctl.d/50-adguardhome.conf
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) ./files/adguardhome.defaults $(1)/etc/uci-defaults/adguardhome
endef
$(eval $(call Download,adguardhome-frontend))
$(eval $(call GoBinPackage,adguardhome))
$(eval $(call BuildPackage,adguardhome))
-30
View File
@@ -1,30 +0,0 @@
config adguardhome 'config'
option enabled '0'
# All paths must be readable by the configured user
option config_file '/etc/adguardhome/adguardhome.yaml'
# Where to store persistent data by AdGuard Home
option work_dir '/var/lib/adguardhome'
option user 'adguardhome'
option group 'adguardhome'
option verbose '0'
# Files and directories that AdGuard Home has read-only access to
# list jail_mount '/etc/ssl/adguardhome.crt'
# list jail_mount '/etc/ssl/adguardhome.key'
# Files and directories that AdGuard Home has read-write access to
# list jail_mount_rw '/path/to/dir'
# Advanced options. Modify at your own risk.
# More info: https://go.dev/doc/gc-guide#GOGC
option gc '0'
# Max number of OS threads to use
# 0 to match the number of CPUs (default)
# >0 to explicitly specify concurrency
option maxprocs '0'
# Soft memory limit in MB, 0 to disable
option memlimit '0'
-109
View File
@@ -1,109 +0,0 @@
#!/bin/sh
#
# SPDX-License-Identifier: GPL-2.0-only
# Migrate old config format only
OLD_CONFIG_FILE=$(uci -q get adguardhome.config.config)
OLD_CONFIG_FILE=${OLD_CONFIG_FILE:-/etc/adguardhome.yaml}
OLD_CONFIG_NEW_OPT_FILE=$(uci -q get adguardhome.config.config_file)
NEW_CONFIG_DIR=/etc/adguardhome
NEW_CONFIG_FILE="$NEW_CONFIG_DIR/adguardhome.yaml"
start_service() {
if ! /etc/init.d/adguardhome running; then
/etc/init.d/adguardhome start
fi
}
stop_service() {
if /etc/init.d/adguardhome running; then
/etc/init.d/adguardhome stop
fi
}
if [ -f "$OLD_CONFIG_NEW_OPT_FILE" ] && [ "$OLD_CONFIG_NEW_OPT_FILE" != "$NEW_CONFIG_FILE" ]; then
echo "Old AdGuard Home config found in '$OLD_CONFIG_NEW_OPT_FILE'"
USER=$(uci -q get adguardhome.config.user)
USER=${USER:-adguardhome}
echo "AdGuard Home config is stored in a non-default path."
echo "Ensure configured service user '$USER' can access it."
elif [ -f "$OLD_CONFIG_FILE" ] && [ "$OLD_CONFIG_FILE" != "$NEW_CONFIG_FILE" ]; then
echo "Old AdGuard Home config found in '$OLD_CONFIG_FILE'"
OLD_CONFIG_DIR=$(dirname "$OLD_CONFIG_FILE")
USER=$(uci -q get adguardhome.config.user)
USER=${USER:-adguardhome}
GROUP=$(uci -q get adguardhome.config.group)
GROUP=${GROUP:-adguardhome}
CUR_CONFIG_FILE="$OLD_CONFIG_FILE"
if [ "$OLD_CONFIG_DIR" = "/etc" ]; then
echo "AdGuard Home config must be stored in its own directory. Migrating..."
stop_service
echo "Using $USER:$GROUP for file ownership."
[ -d "$NEW_CONFIG_DIR" ] || mkdir -m 0700 -p "$NEW_CONFIG_DIR"
mv "$OLD_CONFIG_FILE" "$NEW_CONFIG_FILE"
chown -R "$USER":"$GROUP" "$NEW_CONFIG_DIR"
CUR_CONFIG_FILE="$NEW_CONFIG_FILE"
echo "Config migrated to '$NEW_CONFIG_FILE'"
else
echo "AdGuard Home config is stored in a non-default path."
echo "Ensure configured service user '$USER' can access it."
fi
uci set adguardhome.config.config_file="$CUR_CONFIG_FILE"
uci -q delete adguardhome.config.config
# Use awk to split match on :, remove double quotes and trim leading and
# trailing spaces
cert_path=$(grep certificate_path: "$CUR_CONFIG_FILE" \
| awk -F':' '{gsub(/"/, "", $2); gsub(/^ +| +$/, "", $2); print $2}')
if [ -n "$cert_path" ]; then
echo "Found custom 'certificate_path' pointing to '$cert_path'." \
+ "Ensure configured service user '$USER' can access it."
stop_service
if ! uci -q show adguardhome.config.jail_mount | grep -q "$cert_path"; then
uci add_list adguardhome.config.jail_mount="$cert_path"
fi
fi
private_key_path=$(grep private_key_path: "$CUR_CONFIG_FILE" \
| awk -F':' '{gsub(/"/, "", $2); gsub(/^ +| +$/, "", $2); print $2}')
if [ -n "$private_key_path" ]; then
echo "Found custom 'private_key_path' pointing to '$private_key_path'." \
+ "Ensure configured service user '$USER' can access it."
stop_service
if ! uci -q show adguardhome.config.jail_mount | grep -q "$private_key_path"; then
uci add_list adguardhome.config.jail_mount="$private_key_path"
fi
fi
uci commit adguardhome
start_service
elif [ -z "$OLD_CONFIG_NEW_OPT_FILE" ] && [ "$OLD_CONFIG_FILE" != "$NEW_CONFIG_FILE" ]; then
echo "Old AdGuard Home config not found in '$OLD_CONFIG_FILE'"
stop_service
# Service script will create the new config directory
uci set adguardhome.config.config_file="$NEW_CONFIG_FILE"
uci -q delete adguardhome.config.config
echo "Config path changed to '$NEW_CONFIG_FILE'"
uci commit adguardhome
start_service
else
echo "AdGuard Home config is in its default path '$NEW_CONFIG_FILE'"
fi
-111
View File
@@ -1,111 +0,0 @@
#!/bin/sh /etc/rc.common
#
# SPDX-License-Identifier: GPL-2.0-only
#
# shellcheck disable=SC3043 # ash supports local
# matches dnsmasq
START=19
# stops before networking stops
STOP=89
PROG=/usr/bin/AdGuardHome
USE_PROCD=1
boot() {
ADGUARDHOME_BOOT=1
start "$@"
}
start_service() {
if [ -n "$ADGUARDHOME_BOOT" ]; then
# Do not start yet, wait for triggers
return 0
fi
config_load 'adguardhome'
local config_name='config'
local config_file config group user verbose work_dir workdir
local enabled gc maxprocs memlimit
uci_validate_section 'adguardhome' 'adguardhome' "$config_name" \
'enabled:bool:0' \
'gc:uinteger:0' \
'group:string:adguardhome' \
'config:string' \
'config_file:string:/etc/adguardhome/adguardhome.yaml' \
'jail_mount:list(string)' \
'jail_mount_rw:list(string)' \
'maxprocs:uinteger:0' \
'memlimit:uinteger:0' \
'user:string:adguardhome' \
'verbose:bool:0' \
'workdir:string' \
'work_dir:string:/var/lib/adguardhome'
[ "$enabled" -eq "1" ] || return 1
# Compatibility with older configs
[ -n "$config" ] && config_file="$config"
[ -n "$workdir" ] && work_dir="$workdir"
local config_dir
config_dir=$(dirname "$config_file")
if [ "$config_dir" = '/etc' ]; then
echo 'AdGuard Home config must be stored in its own directory, and not in /etc' >&2
return 1
fi
mkdir -m 0700 -p "$config_dir"
chown -R "$user":"$group" "$config_dir"
mkdir -m 0700 -p "$work_dir"
chown -R "$user":"$group" "$work_dir"
procd_open_instance adguardhome
procd_set_param command "$PROG"
[ "$gc" -le 0 ] || procd_append_param env GOGC="$gc"
[ "$maxprocs" -le 0 ] || procd_append_param env GOMAXPROCS="$maxprocs"
[ "$memlimit" -le 0 ] || procd_append_param env GOMEMLIMIT="$memlimit"
procd_append_param command --config "$config_file"
procd_append_param command --logfile syslog
procd_append_param command --no-check-update
[ "$verbose" = 1 ] && procd_append_param command --verbose
procd_append_param command --work-dir "$work_dir"
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param user "$user"
procd_set_param group "$group"
procd_set_param capabilities '/etc/capabilities/adguardhome.json'
procd_set_param no_new_privs 1
procd_set_param respawn
# log is needed for logging to syslog instead of stdout
# procfs is needed to readlink /proc/self/exe
procd_add_jail adguardhome log procfs
# config directory must be writable to write new config files
procd_add_jail_mount_rw "$config_dir"
procd_add_jail_mount_rw "$work_dir"
procd_add_jail_mount '/etc/hosts'
procd_add_jail_mount '/etc/ssl/certs'
config_list_foreach "$config_name" jail_mount procd_add_jail_mount
config_list_foreach "$config_name" jail_mount_rw procd_add_jail_mount_rw
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger adguardhome
if [ -n "$ADGUARDHOME_BOOT" ]; then
# Wait for interfaces to be up before starting AdGuard Home for real.
# Prevents issues like https://github.com/openwrt/packages/issues/21868.
procd_add_raw_trigger 'interface.*.up' 5000 /etc/init.d/adguardhome restart
fi
}
-22
View File
@@ -1,22 +0,0 @@
{
"bounding": [
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW"
],
"effective": [
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW"
],
"ambient": [
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW"
],
"permitted": [
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW"
],
"inheritable": [
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW"
]
}
-3
View File
@@ -1,3 +0,0 @@
# quic-go expects larger UDP send/receive buffers
net.core.rmem_max = 7500000
net.core.wmem_max = 7500000
-96
View File
@@ -1,96 +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:=alist
PKG_VERSION:=3.64.0
PKG_WEB_VERSION:=3.64.0
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/AlistGo/alist/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=599065ec4b26375e4012cf212415f4195c9b92864bdcde4105356261920563f4
PKG_LICENSE:=GPL-3.0
PKG_LICENSE_FILE:=LICENSE
PKG_MAINTAINER:=sbwml <admin@cooluc.com>
define Download/$(PKG_NAME)-web
FILE:=$(PKG_NAME)-web-$(PKG_WEB_VERSION).tar.gz
URL_FILE:=dist.tar.gz
URL:=https://github.com/AlistGo/alist-web/releases/download/$(PKG_WEB_VERSION)/
HASH:=b80550662de42a2f8a72d35bca8ed66ec9ad0c24a9a02ceec6a01bf5038cf6f4
endef
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/alist-org/alist/v3
GO_PKG_LDFLAGS:= \
-X '$(GO_PKG)/internal/conf.BuiltAt=$(shell date '+%Y-%m-%d %H:%M:%S %z')' \
-X '$(GO_PKG)/internal/conf.GoVersion=$(shell $(STAGING_DIR_HOSTPKG)/bin/go version | sed 's/go version //')' \
-X '$(GO_PKG)/internal/conf.GitAuthor=Xhofe <i@nn.ci>' \
-X '$(GO_PKG)/internal/conf.GitCommit=tarball/$(shell echo $(PKG_HASH) | cut -c 1-7)' \
-X '$(GO_PKG)/internal/conf.Version=v$(PKG_VERSION) (OpenWrt $(ARCH_PACKAGES))' \
-X '$(GO_PKG)/internal/conf.WebVersion=$(PKG_WEB_VERSION)'
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/$(PKG_NAME)
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
TITLE:=A file list program that supports multiple storage
URL:=https://alist.nn.ci/
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
endef
define Package/$(PKG_NAME)/conffiles
/etc/alist
/etc/config/alist
endef
define Package/$(PKG_NAME)/description
A file list program that supports multiple storage, powered by Gin and Solidjs.
endef
ifeq ($(ARCH),arm)
ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE))))
ifeq ($(ARM_CPU_FEATURES),)
TARGET_CFLAGS:=
TARGET_LDFLAGS:=
endif
endif
ifneq ($(CONFIG_USE_MUSL),)
TARGET_CFLAGS += -D_LARGEFILE64_SOURCE
endif
define Build/Prepare
$(call Build/Prepare/Default)
$(TAR) --strip-components=1 -C $(PKG_BUILD_DIR)/public/dist -xzf $(DL_DIR)/$(PKG_NAME)-web-$(PKG_WEB_VERSION).tar.gz
$(SED) 's_https://jsd.nn.ci/gh/alist-org/logo@main/logo.png_/assets/logo.png_g' $(PKG_BUILD_DIR)/public/dist/index.html
ifneq ($(CONFIG_ARCH_64BIT),y)
$(RM) -rf $(PKG_BUILD_DIR)/drivers/{lark,lark.go}
endif
endef
define Package/$(PKG_NAME)/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/alist $(1)/usr/bin
$(INSTALL_DIR) $(1)/etc/config $(1)/etc/init.d $(1)/etc/alist
$(INSTALL_CONF) $(CURDIR)/files/alist.config $(1)/etc/config/alist
$(INSTALL_BIN) $(CURDIR)/files/alist.init $(1)/etc/init.d/alist
$(INSTALL_DATA) $(CURDIR)/files/data.db $(1)/etc/alist/data.db
endef
$(eval $(call Download,$(PKG_NAME)-web))
$(eval $(call BuildPackage,$(PKG_NAME)))
-31
View File
@@ -1,31 +0,0 @@
config alist
option enabled '0'
option port '5244'
option delayed_start '0'
option allow_wan '0'
option force '1'
option token_expires_in '48'
option max_connections '0'
option tls_insecure_skip_verify '1'
option data_dir '/etc/alist'
option temp_dir '/tmp/alist'
option log '1'
option log_max_size '10'
option log_max_backups '5'
option log_max_age '28'
option log_compress '0'
option database_type 'sqlite3'
option ssl '0'
option download_workers '5'
option download_max_retry '1'
option transfer_workers '5'
option transfer_max_retry '2'
option upload_workers '5'
option upload_max_retry '0'
option copy_workers '5'
option copy_max_retry '2'
option cors_allow_origins '*'
option cors_allow_methods '*'
option cors_allow_headers '*'
option s3 '0'
-246
View File
@@ -1,246 +0,0 @@
#!/bin/sh /etc/rc.common
. /usr/share/libubox/jshn.sh
START=99
USE_PROCD=1
PROG=/usr/bin/alist
LOG_FILE=/var/log/alist.log
get_config() {
config_get_bool enabled $1 enabled 1
config_get port $1 port 5244
config_get allow_wan $1 allow_wan 0
config_get delayed_start $1 delayed_start 0
config_get force $1 force 1
config_get site_url $1 site_url ""
config_get cdn $1 cdn ""
config_get jwt_secret $1 jwt_secret ""
config_get data_dir $1 data_dir "/etc/alist"
config_get temp_dir $1 temp_dir "/tmp/alist"
config_get token_expires_in $1 token_expires_in 48
config_get max_connections $1 max_connections 0
config_get tls_insecure_skip_verify $1 tls_insecure_skip_verify 1
# log
config_get log $1 log 1
config_get log_max_size $1 log_max_size 10
config_get log_max_backups $1 log_max_backups 5
config_get log_max_age $1 log_max_age 28
config_get log_compress $1 log_compress 0
# scheme
config_get ssl $1 ssl 0
config_get force_https $1 force_https 0
config_get ssl_cert $1 ssl_cert ""
config_get ssl_key $1 ssl_key ""
# database
config_get database_type $1 database_type "sqlite3"
config_get mysql_host $1 mysql_host ""
config_get mysql_port $1 mysql_port "3306"
config_get mysql_username $1 mysql_username ""
config_get mysql_password $1 mysql_password ""
config_get mysql_database $1 mysql_database ""
config_get mysql_table_prefix $1 mysql_table_prefix "x_"
config_get mysql_ssl_mode $1 mysql_ssl_mode ""
config_get mysql_dsn $1 mysql_dsn ""
# tasks
config_get download_workers $1 download_workers 5
config_get download_max_retry $1 download_max_retry 1
config_get transfer_workers $1 transfer_workers 5
config_get transfer_max_retry $1 transfer_max_retry 2
config_get upload_workers $1 upload_workers 5
config_get upload_max_retry $1 upload_max_retry 0
config_get copy_workers $1 copy_workers 5
config_get copy_max_retry $1 copy_max_retry 2
# cors
config_get cors_allow_origins $1 cors_allow_origins '*'
config_get cors_allow_methods $1 cors_allow_methods '*'
config_get cors_allow_headers $1 cors_allow_headers '*'
# s3
config_get s3 $1 s3 0
config_get s3_port $1 s3_port 5246
config_get s3_ssl $1 s3_ssl 0
config_load network
config_get lan_addr lan ipaddr "0.0.0.0"
if echo "${lan_addr}" | grep -Fq ' '; then
lan_addr="0.0.0.0"
else
lan_addr=${lan_addr%%/*}
fi
# init jwt_secret
[ -z "$jwt_secret" ] && jwt_secret=$(tr -cd "a-zA-Z0-9" < "/dev/urandom" | head -c16)
uci -q set alist.@alist[0].jwt_secret="$jwt_secret"
uci commit alist
}
set_firewall() {
if [ "$external_access" = "allow" ]; then
uci -q delete firewall.alist
uci set firewall.alist=rule
uci set firewall.alist.name="alist"
uci set firewall.alist.target="ACCEPT"
uci set firewall.alist.src="wan"
uci set firewall.alist.proto="tcp"
uci set firewall.alist.dest_port="$port"
uci set firewall.alist.enabled="1"
uci commit firewall
/etc/init.d/firewall reload >/dev/null 2>&1
elif [ "$external_access" = "deny" ]; then
uci -q delete firewall.alist
uci commit firewall
/etc/init.d/firewall reload >/dev/null 2>&1
fi
}
start_service() {
config_load alist
config_foreach get_config alist
[ $enabled -ne 1 ] && return 1
mkdir -p "$temp_dir" "$data_dir"
[ "$ssl" -eq 1 ] && https_port=$port http_port="-1" || https_port="-1" http_port=$port
if [ -e /proc/uptime ]; then
[ $(awk -F. '{print $1}' /proc/uptime) -lt "120" ] && delayed_start=$delayed_start || delayed_start=0
else
delayed_start=$delayed_start
fi
if [ "$allow_wan" -eq "1" ]; then
listen_addr="0.0.0.0"
external_access="allow"
else
listen_addr=$lan_addr
external_access="deny"
fi
set_firewall
> $LOG_FILE
# init config
json_init
json_add_boolean "force" "$force"
json_add_string "site_url" "$site_url"
json_add_string "cdn" "$cdn"
json_add_string "jwt_secret" "$jwt_secret"
json_add_int "token_expires_in" "$token_expires_in"
# database
json_add_object 'database'
json_add_string "type" "$database_type"
json_add_string "host" "$mysql_host"
json_add_int "port" "$mysql_port"
json_add_string "user" "$mysql_username"
json_add_string "password" "$mysql_password"
json_add_string "name" "$mysql_database"
json_add_string "db_file" "$data_dir/data.db"
json_add_string "table_prefix" "$mysql_table_prefix"
json_add_string "ssl_mode" "$mysql_ssl_mode"
json_add_string "dsn" "$mysql_dsn"
json_close_object
# meilisearch
json_add_object "meilisearch"
json_add_string "host" "http://localhost:7700"
json_add_string "api_key" ""
json_add_string "index_prefix" ""
json_close_object
# scheme
json_add_object "scheme"
json_add_string "address" "$listen_addr"
json_add_int "http_port" "$http_port"
json_add_int "https_port" "$https_port"
json_add_boolean "force_https" "$force_https"
json_add_string "cert_file" "$ssl_cert"
json_add_string "key_file" "$ssl_key"
json_add_string "unix_file" ""
json_add_string "unix_file_perm" ""
json_close_object
json_add_string "temp_dir" "$temp_dir"
json_add_string "bleve_dir" "$data_dir/bleve"
json_add_string "dist_dir" ""
# log
json_add_object "log"
json_add_boolean "enable" "$log"
json_add_string "name" "$LOG_FILE"
json_add_int "max_size" "$log_max_size"
json_add_int "max_backups" "$log_max_backups"
json_add_int "max_age" "$log_max_age"
json_add_boolean "compress" "$log_compress"
json_close_object
json_add_int "delayed_start" "$delayed_start"
json_add_int "max_connections" "$max_connections"
json_add_boolean "tls_insecure_skip_verify" "$tls_insecure_skip_verify"
# tasks
json_add_object "tasks"
json_add_object "download"
json_add_int "workers" "$download_workers"
json_add_int "max_retry" "$download_max_retry"
json_close_object
json_add_object "transfer"
json_add_int "workers" "$transfer_workers"
json_add_int "max_retry" "$transfer_max_retry"
json_close_object
json_add_object "upload"
json_add_int "workers" "$upload_workers"
json_add_int "max_retry" "$upload_max_retry"
json_close_object
json_add_object "copy"
json_add_int "workers" "$copy_workers"
json_add_int "max_retry" "$copy_max_retry"
json_close_object
json_close_object
# cors
json_add_object "cors"
json_add_array "allow_origins"
json_add_string "" "$cors_allow_origins"
json_close_array
json_add_array "allow_methods"
json_add_string "" "$cors_allow_methods"
json_close_array
json_add_array "allow_headers"
json_add_string "" "$cors_allow_headers"
json_close_array
json_close_object
# s3
json_add_object "s3"
json_add_boolean "enable" "$s3"
json_add_int "port" "$s3_port"
json_add_boolean "ssl" "$s3_ssl"
json_close_object
json_dump > "$data_dir/config.json"
procd_open_instance alist
procd_set_param command $PROG
procd_append_param command server
procd_append_param command --data "$data_dir"
procd_set_param file "$data_dir/config.json"
procd_set_param stdout 0
procd_set_param stderr 0
procd_set_param respawn
procd_set_param limits core="unlimited"
procd_set_param limits nofile="200000 200000"
procd_close_instance alist
}
service_triggers() {
procd_add_reload_trigger "alist"
}
stop_service() {
external_access="deny"
set_firewall
}
Binary file not shown.
-10
View File
@@ -1,10 +0,0 @@
<svg width="1252" height="1252" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g>
<g id="#70c6beff">
<path id="svg_2" d="m634.37,138.38c11.88,-1.36 24.25,1.3 34.18,8.09c14.96,9.66 25.55,24.41 34.49,39.51c40.59,68.03 81.45,135.91 122.02,203.96c54.02,90.99 108.06,181.97 161.94,273.06c37.28,63 74.65,125.96 112.18,188.82c24.72,41.99 50.21,83.54 73.84,126.16c10.18,17.84 15.77,38.44 14.93,59.03c-0.59,15.92 -3.48,32.28 -11.84,46.08c-11.73,19.46 -31.39,33.2 -52.71,40.36c-11.37,4.09 -23.3,6.87 -35.43,6.89c-132.32,-0.05 -264.64,0.04 -396.95,0.03c-11.38,-0.29 -22.95,-1.6 -33.63,-5.72c-7.81,-3.33 -15.5,-7.43 -21.61,-13.42c-10.43,-10.32 -17.19,-24.96 -15.38,-39.83c0.94,-10.39 3.48,-20.64 7.76,-30.16c4.15,-9.77 9.99,-18.67 15.06,-27.97c22.13,-39.47 45.31,-78.35 69.42,-116.65c7.72,-12.05 14.44,-25.07 25.12,-34.87c11.35,-10.39 25.6,-18.54 41.21,-19.6c12.55,-0.52 24.89,3.82 35.35,10.55c11.8,6.92 21.09,18.44 24.2,31.88c4.49,17.01 -0.34,34.88 -7.55,50.42c-8.09,17.65 -19.62,33.67 -25.81,52.18c-1.13,4.21 -2.66,9.52 0.48,13.23c3.19,3 7.62,4.18 11.77,5.22c12,2.67 24.38,1.98 36.59,2.06c45,-0.01 90,0 135,0c8.91,-0.15 17.83,0.3 26.74,-0.22c6.43,-0.74 13.44,-1.79 18.44,-6.28c3.3,-2.92 3.71,-7.85 2.46,-11.85c-2.74,-8.86 -7.46,-16.93 -12.12,-24.89c-119.99,-204.91 -239.31,-410.22 -360.56,-614.4c-3.96,-6.56 -7.36,-13.68 -13.03,-18.98c-2.8,-2.69 -6.95,-4.22 -10.77,-3.11c-3.25,1.17 -5.45,4.03 -7.61,6.57c-5.34,6.81 -10.12,14.06 -14.51,21.52c-20.89,33.95 -40.88,68.44 -61.35,102.64c-117.9,198.43 -235.82,396.85 -353.71,595.29c-7.31,13.46 -15.09,26.67 -23.57,39.43c-7.45,10.96 -16.49,21.23 -28.14,27.83c-13.73,7.94 -30.69,11.09 -46.08,6.54c-11.23,-3.47 -22.09,-9.12 -30.13,-17.84c-10.18,-10.08 -14.69,-24.83 -14.17,-38.94c0.52,-14.86 5.49,-29.34 12.98,-42.1c71.58,-121.59 143.62,-242.92 215.93,-364.09c37.2,-62.8 74.23,-125.69 111.64,-188.36c37.84,-63.5 75.77,-126.94 113.44,-190.54c21.02,-35.82 42.19,-71.56 64.28,-106.74c6.79,-11.15 15.58,-21.15 26.16,-28.85c8.68,-5.92 18.42,-11 29.05,-11.94z" fill="#70c6be"/>
</g>
<g id="#1ba0d8ff">
<path id="svg_3" d="m628.35,608.38c17.83,-2.87 36.72,1.39 51.5,11.78c11.22,8.66 19.01,21.64 21.26,35.65c1.53,10.68 0.49,21.75 -3.44,31.84c-3.02,8.73 -7.35,16.94 -12.17,24.81c-68.76,115.58 -137.5,231.17 -206.27,346.75c-8.8,14.47 -16.82,29.47 -26.96,43.07c-7.37,9.11 -16.58,16.85 -27.21,21.89c-22.47,11.97 -51.79,4.67 -68.88,-13.33c-8.66,-8.69 -13.74,-20.63 -14.4,-32.84c-0.98,-12.64 1.81,-25.42 7.53,-36.69c5.03,-10.96 10.98,-21.45 17.19,-31.77c30.22,-50.84 60.17,-101.84 90.3,-152.73c41.24,-69.98 83.16,-139.55 124.66,-209.37c4.41,-7.94 9.91,-15.26 16.09,-21.9c8.33,-8.46 18.9,-15.3 30.8,-17.16z" fill="#1ba0d8"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

-54
View File
@@ -1,54 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2017-2020 Yousong Zhou <yszhou4tech@gmail.com>
# Copyright (C) 2021-2023 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=aliyundrive-webdav
PKG_VERSION:=2.3.3
PKG_RELEASE:=1
PKG_SOURCE_PROTO:=git
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/messense/aliyundrive-webdav.git
PKG_SOURCE_VERSION:=6e8eba62b4e50acf89681e3a67a3a693186dcd05
PKG_MIRROR_HASH:=b6b1099c90818c5eb322bdae105d3df8d7a954d6e9ee7798c3c2412eed528640
PKG_LICENSE:=MIT
PKG_MAINTAINER:=messense <messense@icloud.com>
PKG_BUILD_DEPENDS:=rust/host
RUST_PKG_FEATURES:=rustls-tls atomic64 native-tls native-tls-vendored
include $(INCLUDE_DIR)/package.mk
include ../../lang/rust/rust-package.mk
define Package/aliyundrive-webdav
SECTION:=multimedia
CATEGORY:=Multimedia
TITLE:=WebDAV server for AliyunDrive
URL:=https://github.com/messense/aliyundrive-webdav
DEPENDS:=$$(RUST_ARCH_DEPENDS)
endef
define Package/aliyundrive-webdav/description
WebDAV server for AliyunDrive.
endef
define Package/aliyundrive-webdav/conffiles
/etc/config/aliyundrive-webdav
endef
define Package/aliyundrive-webdav/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/target/$(RUSTC_TARGET_ARCH)/release/aliyundrive-webdav $(1)/usr/bin/
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/aliyundrive-webdav.init $(1)/etc/init.d/aliyundrive-webdav
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/aliyundrive-webdav.config $(1)/etc/config/aliyundrive-webdav
endef
$(eval $(call BuildPackage,aliyundrive-webdav))
@@ -1,21 +0,0 @@
config server
option enable '0'
option debug '0'
option refresh_token ''
option host '0.0.0.0'
option port '8080'
option drive_type ''
option auth_user ''
option auth_password ''
option read_buffer_size '10485760'
option upload_buffer_size '16777216'
option cache_size '1000'
option cache_ttl '600'
option root '/'
option no_trash '0'
option read_only '0'
option tls_cert ''
option tls_key ''
option skip_upload_same_size '0'
option prefer_http_download '0'
option redirect '1'
@@ -1,97 +0,0 @@
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=99
STOP=15
NAME=aliyundrive-webdav
uci_get_by_type() {
local ret=$(uci get $NAME.@$1[0].$2 2>/dev/null)
echo ${ret:=$3}
}
start_service() {
local enable=$(uci_get_by_type server enable)
case "$enable" in
1|on|true|yes|enabled)
local refresh_token=$(uci_get_by_type server refresh_token)
local auth_user=$(uci_get_by_type server auth_user)
local auth_password=$(uci_get_by_type server auth_password)
local read_buf_size=$(uci_get_by_type server read_buffer_size 10485760)
local upload_buf_size=$(uci_get_by_type server upload_buffer_size 16777216)
local cache_size=$(uci_get_by_type server cache_size 1000)
local cache_ttl=$(uci_get_by_type server cache_ttl 600)
local host=$(uci_get_by_type server host 127.0.0.1)
local port=$(uci_get_by_type server port 8080)
local root=$(uci_get_by_type server root /)
local tls_cert=$(uci_get_by_type server tls_cert)
local tls_key=$(uci_get_by_type server tls_key)
local drive_type=$(uci_get_by_type server drive_type)
local extra_options="--auto-index"
case "$(uci_get_by_type server no_trash 0)" in
1|on|true|yes|enabled)
extra_options="$extra_options --no-trash"
;;
*) ;;
esac
case "$(uci_get_by_type server read_only 0)" in
1|on|true|yes|enabled)
extra_options="$extra_options --read-only"
;;
*) ;;
esac
case "$(uci_get_by_type server skip_upload_same_size 0)" in
1|on|true|yes|enabled)
extra_options="$extra_options --skip-upload-same-size"
;;
*) ;;
esac
case "$(uci_get_by_type server prefer_http_download 0)" in
1|on|true|yes|enabled)
extra_options="$extra_options --prefer-http-download"
;;
*) ;;
esac
case "$(uci_get_by_type server redirect 0)" in
1|on|true|yes|enabled)
extra_options="$extra_options --redirect"
;;
*) ;;
esac
if [[ ! -z "$tls_cert" && ! -z "$tls_key" ]]; then
extra_options="$extra_options --tls-cert $tls_cert --tls-key $tls_key"
fi
if [[ ! -z "$drive_type" ]]; then
extra_options="$extra_options --drive-type $drive_type"
fi
procd_open_instance
procd_set_param command /bin/sh -c "/usr/bin/$NAME $extra_options --host $host --port $port --root $root --read-buffer-size $read_buf_size --upload-buffer-size $upload_buf_size --cache-size $cache_size --cache-ttl $cache_ttl --workdir /var/run/$NAME >>/var/log/$NAME.log 2>&1"
procd_set_param pidfile /var/run/$NAME.pid
procd_set_param env REFRESH_TOKEN="$refresh_token"
[[ ! -z "$auth_user" ]] && procd_append_param env WEBDAV_AUTH_USER="$auth_user"
[[ ! -z "$auth_password" ]] && procd_append_param env WEBDAV_AUTH_PASSWORD="$auth_password"
case $(uci_get_by_type server debug) in
1|on|true|yes|enabled)
procd_append_param env RUST_LOG="aliyundrive_webdav=debug" ;;
*) ;;
esac
procd_close_instance ;;
*)
stop_service ;;
esac
}
service_triggers() {
procd_add_reload_trigger "aliyundrive-webdav"
}
-83
View File
@@ -1,83 +0,0 @@
menu "Aria2 Configuration"
depends on PACKAGE_aria2
choice
prompt "SSL Library"
default ARIA2_OPENSSL
config ARIA2_OPENSSL
bool "OpenSSL"
config ARIA2_GNUTLS
bool "GnuTLS"
config ARIA2_NOSSL
bool "No SSL Support"
endchoice
choice
prompt "Crypto Library"
depends on !ARIA2_OPENSSL
default ARIA2_NOCRYPTO
config ARIA2_NETTLE
bool "Nettle"
config ARIA2_LIBGCRYPT
bool "Libgcrypt"
config ARIA2_NOCRYPTO
bool "No Crypto Library"
endchoice
choice
prompt "XML Library"
default ARIA2_NOXML
config ARIA2_LIBXML2
bool "Libxml2"
config ARIA2_EXPAT
bool "Expat"
config ARIA2_NOXML
bool "No XML Library"
endchoice
config ARIA2_GMP
bool "GNU Multiple Precision Arithmetic Library"
depends on ARIA2_NETTLE
default n
config ARIA2_BITTORRENT
bool "Enable Bittorrent Support"
depends on ARIA2_OPENSSL || ARIA2_LIBGCRYPT || \
(ARIA2_NETTLE && ARIA2_GMP)
default y
config ARIA2_METALINK
bool "Enable Metalink Support"
depends on !ARIA2_NOXML
default n
config ARIA2_SFTP
bool "Enable SFTP Support"
default n
config ARIA2_ASYNC_DNS
bool "Enable Async DNS Support"
default n
config ARIA2_COOKIE
bool "Enable Firefox3/Chromium Cookie Support"
default n
config ARIA2_WEBSOCKET
bool "Enable JSON-RPC over WebSocket Support"
depends on ARIA2_OPENSSL || ARIA2_LIBGCRYPT || ARIA2_NETTLE
default y
endmenu
-134
View File
@@ -1,134 +0,0 @@
#
# Copyright (C) 2012-2023 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=aria2
PKG_VERSION:=1.37.0
PKG_RELEASE:=9
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/aria2/aria2/releases/download/release-$(PKG_VERSION)/
PKG_HASH:=skip
PKG_INSTALL:=1
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=gc-sections lto
PKG_MAINTAINER:=Imre Kaloz <kaloz@openwrt.org>, \
Hsing-Wang Liao <kuoruan@gmail.com>
PKG_LICENSE:=GPLv2
PKG_LICENSE_FILES:=COPYING
PKG_CPE_ID:=cpe:/a:aria2_project:aria2
PKG_CONFIG_DEPENDS := \
CONFIG_ARIA2_NOSSL \
CONFIG_ARIA2_OPENSSL \
CONFIG_ARIA2_GNUTLS \
CONFIG_ARIA2_NOCRYPTO \
CONFIG_ARIA2_NETTLE \
CONFIG_ARIA2_LIBGCRYPT \
CONFIG_ARIA2_LIBXML2 \
CONFIG_ARIA2_EXPAT \
CONFIG_ARIA2_GMP \
CONFIG_ARIA2_BITTORRENT \
CONFIG_ARIA2_METALINK \
CONFIG_ARIA2_SFTP \
CONFIG_ARIA2_ASYNC_DNS \
CONFIG_ARIA2_COOKIE \
CONFIG_ARIA2_WEBSOCKET
include $(INCLUDE_DIR)/package.mk
include $(INCLUDE_DIR)/nls.mk
define Package/aria2-openssl
SECTION:=net
CATEGORY:=Network
SUBMENU:=File Transfer
TITLE:=lightweight download utility (OpenSSL dependencies)
DEPENDS:=+libopenssl +libopenssl-legacy
HIDDEN:=1
endef
define Package/aria2/config
source "$(SOURCE)/Config.in"
endef
define Package/aria2
SECTION:=net
CATEGORY:=Network
SUBMENU:=File Transfer
TITLE:=lightweight download utility
URL:=https://aria2.github.io/
DEPENDS:=+zlib +libstdcpp +ARIA2_OPENSSL:aria2-openssl +ARIA2_GNUTLS:libgnutls \
+ARIA2_NETTLE:libnettle +ARIA2_LIBGCRYPT:libgcrypt +ARIA2_GMP:libgmp \
+ARIA2_LIBXML2:libxml2 +ARIA2_EXPAT:libexpat +ARIA2_SFTP:libssh2 \
+ARIA2_ASYNC_DNS:libcares +ARIA2_COOKIE:libsqlite3
USERID:=aria2=6800:aria2=6800
endef
define Package/aria2/description
aria2 is a lightweight multi-protocol & multi-source command-line download
utility
endef
CONFIGURE_ARGS += \
--disable-nls \
$(if $(CONFIG_ARIA2_NOSSL),--disable,--enable)-ssl \
$(if $(CONFIG_ARIA2_BITTORRENT),--enable,--disable)-bittorrent \
$(if $(CONFIG_ARIA2_METALINK),--enable,--disable)-metalink \
$(if $(CONFIG_ARIA2_WEBSOCKET),--enable,--disable)-websocket \
$(if $(CONFIG_ARIA2_OPENSSL),--with,--without)-openssl \
$(if $(CONFIG_ARIA2_GNUTLS),--with,--without)-gnutls \
$(if $(CONFIG_ARIA2_NETTLE),--with,--without)-libnettle \
$(if $(CONFIG_ARIA2_LIBGCRYPT),--with,--without)-libgcrypt \
$(if $(CONFIG_ARIA2_GMP),--with,--without)-libgmp \
$(if $(CONFIG_ARIA2_LIBXML2),--with,--without)-libxml2 \
$(if $(CONFIG_ARIA2_EXPAT),--with,--without)-libexpat \
$(if $(CONFIG_ARIA2_SFTP),--with,--without)-libssh2 \
$(if $(CONFIG_ARIA2_ASYNC_DNS),--with,--without)-libcares \
$(if $(CONFIG_ARIA2_COOKIE),--with,--without)-sqlite3 \
--without-libuv \
--with-libz
define Package/aria2/conffiles
/etc/config/aria2
endef
define Download/aria2.conf
URL:=https://github.com/P3TERX/aria2.conf/archive
URL_FILE:=master.zip
FILE:=aria2.conf
HASH:=skip
endef
define Build/Prepare
$(call Build/Prepare/Default)
unzip $(DL_DIR)/aria2.conf -d $(PKG_BUILD_DIR)/
sed -i '/rpc-secret/d' $(PKG_BUILD_DIR)/aria2.conf-master/*
[ -x $(PKG_BUILD_DIR)/aria2.conf-master/tracker.sh ] && { cd $(PKG_BUILD_DIR)/aria2.conf-master && sh ./tracker.sh >/dev/null 2>&1; }
endef
define Package/aria2/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/aria2c $(1)/usr/bin
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/aria2.init $(1)/etc/init.d/aria2
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/aria2.conf $(1)/etc/config/aria2
$(INSTALL_DIR) $(1)/usr/share/aria2
$(INSTALL_BIN) $(PKG_BUILD_DIR)/aria2.conf-master/* $(1)/usr/share/aria2/
endef
define Package/aria2-openssl/install
true
endef
$(eval $(call Download,aria2.conf))
$(eval $(call BuildPackage,aria2))
$(eval $(call BuildPackage,aria2-openssl))
-23
View File
@@ -1,23 +0,0 @@
# You can use most aria2 command-line options, replace '-' with '_'.
# eg. 'rpc-secret' ==> 'rpc_secret'
#
# We do not support all options at this time. But you can add any option
# with 'list extra_settings'.
#
# You can also add new config sections to define multi instance.
#
config aria2 'main'
option enabled '0'
option user 'aria2'
option dir '/mnt/sda1/aria2'
option config_dir '/var/etc/aria2'
option bt_enable_lpd 'true'
option enable_dht 'true'
option follow_torrent 'true'
option file_allocation 'none'
option save_session_interval '30'
option split '128'
option min_split_size '100K'
option max_connection_per_server '128'
list extra_settings 'dht-file-path=/usr/share/aria2/dht.dat'
-364
View File
@@ -1,364 +0,0 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2016-2017 Hsing-wang Liao <kuoruan@gmail.com>
# Licensed to the public under the Apache License 2.0.
START=99
USE_PROCD=1
NAME=aria2
PROG=/usr/bin/aria2c
_info() {
logger -p daemon.info -t "$NAME" "$*"
}
_err() {
logger -p daemon.err -t "$NAME" "$*"
}
_make_dir() {
local d
for d in "$@"; do
if [ ! -d "$d" ]; then
mkdir -p "$d" 2>/dev/null || return 1
fi
done
return 0
}
_create_file() {
touch "$@" 2>/dev/null
}
_change_owner() {
local u="$1"; shift
local d
for d in "$@"; do
if [ -f "$d" ]; then
chown "$u" "$d" 2>/dev/null || return 1
elif [ -d "$d" ]; then
chown -R "$u" "$d" 2>/dev/null || return 1
fi
done
return 0
}
_change_file_mode() {
local mod="$1"; shift
chmod "$mod" "$@" 2>/dev/null
}
_reset_dir_mode() {
local d
for d in "$@"; do
if [ -d "$d" ]; then
find "$d" -type d -exec chmod 755 {} \; 2>/dev/null
find "$d" -type f -exec chmod 644 {} \; 2>/dev/null
fi
done
}
append_options() {
local o; local v
for o in "$@"; do
v="$(eval echo "\$$o")"
[ -n "$v" ] && \
echo "${o//_/-}=$v" >>"$config_file_tmp"
done
}
append_setting() {
local s="$1"
[ -n "$s" ] && \
echo "$s" >>"$config_file_tmp"
}
append_header() {
local h="$1"
[ -n "$h" ] && \
echo "header=\"$h\"" >>"$config_file_tmp"
}
aria2_validate() {
uci_load_validate "$NAME" aria2 "$1" "$2" \
'enabled:bool:0' \
'enable_logging:bool' \
'enable_proxy:bool' \
'config_dir:string:/var/etc/aria2' \
'user:string' \
'all_proxy:string' \
'all_proxy_passwd:string' \
'all_proxy_user:string' \
'auto_save_interval:range(0,600)' \
'bt_enable_lpd:or("true","false")' \
'bt_detach_seed_only:or("true","false")' \
'bt_load_saved_metadata:or("true","false")' \
'bt_prioritize_piece:string' \
'bt_max_open_files:uinteger' \
'bt_max_peers:uinteger' \
'bt_remove_unselected_file:or("true","false")' \
'bt_request_peer_speed_limit:string' \
'bt_save_metadata:or("true","false")' \
'bt_seed_unverified:or("true","false")' \
'bt_stop_timeout:uinteger' \
'bt_tracker:list(string)' \
'ca_certificate:file' \
'certificate:file' \
'check_certificate:or("true","false"):true' \
'check_integrity:or("true","false")' \
'connect_timeout:uinteger' \
'dht_listen_port:string' \
'dir:string' \
'disable_ipv6:or("true","false")' \
'disk_cache:string' \
'enable_dht:or("true","false"):true' \
'enable_dht6:or("true","false")' \
'enable_peer_exchange:or("true","false")' \
'event_poll:or("epoll","kqueue","port","poll","select")' \
'file_allocation:or("none","prealloc","trunc","falloc")' \
'follow_torrent:or("true","false","mem")' \
'force_save:or("true","false")' \
'http_accept_gzip:or("true","false")' \
'http_no_cache:or("true","false")' \
'listen_port:string' \
'log:string' \
'log_level:or("debug","info","notice","warn","error")' \
'lowest_speed_limit:string' \
'max_concurrent_downloads:uinteger' \
'max_connection_per_server:uinteger' \
'max_download_limit:string' \
'max_overall_download_limit:string' \
'max_overall_upload_limit:string' \
'max_tries:uinteger' \
'max_upload_limit:string' \
'min_split_size:string' \
'pause:or("true","false")' \
'pause_metadata:or("true","false")' \
'peer_id_prefix:string' \
'private_key:file' \
'retry_wait:uinteger' \
'rpc_auth_method:or("none","user_pass","token")' \
'rpc_certificate:file' \
'rpc_listen_port:range(1024,65535)' \
'rpc_passwd:string' \
'rpc_private_key:file' \
'rpc_secret:string' \
'rpc_secure:or("true","false")' \
'rpc_user:string' \
'save_session_interval:uinteger' \
'seed_ratio:ufloat' \
'seed_time:ufloat' \
'split:uinteger' \
'timeout:uinteger' \
'user_agent:string'
}
aria2_start() {
local section="$1"
[ "$2" = "0" ] || { _err "Validation failed."; return 1; }
[ "$enabled" = "1" ] || { _info "Instance \"$section\" disabled."; return 1; }
[ -n "$dir" ] || { _err "Please set download dir."; return 1; }
[ -d "$dir" ] || _make_dir "$dir" || {
_err "Can't create download dir: $dir"
return 1
}
config_file="$config_dir/$NAME.conf.$section"
config_file_tmp="$config_dir/$NAME.conf.tmp"
session_file="$config_dir/$NAME.session.$section"
_make_dir "$config_dir" || {
_err "Can't create config dir: $config_dir"
return 1
}
_create_file "$session_file" "$config_file" "$config_file_tmp" || {
_err "Can't create files: $session_file, $config_file, $config_file_tmp"
return 1
}
# create tmp file
cat >"$config_file_tmp" <<-EOF
# Auto generated file, changes to this file will be lost.
EOF
append_setting "dir=$dir"
append_setting "enable-rpc=true"
append_setting "rpc-allow-origin-all=true"
append_setting "rpc-listen-all=true"
append_setting "quiet=true"
append_setting "continue=true"
append_setting "input-file=$session_file"
append_setting "save-session=$session_file"
if [ -z "$enable_logging" ]; then
append_options "log" "log_level"
elif [ "$enable_logging" = "1" ]; then
log=${log:-"/var/log/aria2.log"}
local log_dir
log_dir="$(dirname "$log")"
_make_dir "$log_dir" || {
_err "Can't create log dir: $log_dir"
return 1
}
# create or clear log file
echo >"$log"
append_setting "log=$log"
append_options "log_level"
fi
if [ -z "$enable_proxy" ] || [ "$enable_proxy" = "1" ]; then
append_options "all_proxy" "all_proxy_user" "all_proxy_passwd"
fi
unset_auth_method() {
uci -q batch <<-EOF
set $NAME.$section.rpc_auth_method=""
commit $NAME
EOF
}
if [ -z "$rpc_auth_method" ]; then
if [ -n "$rpc_secret" ]; then
append_setting "rpc-secret=$rpc_secret"
elif [ -n "$rpc_user" ]; then
append_setting "rpc-user=$rpc_user"
append_setting "rpc-passwd=$rpc_passwd"
else
_info "It is recommended to set RPC secret."
fi
elif [ "$rpc_auth_method" = "token" ]; then
if [ -n "$rpc_secret" ]; then
append_setting "rpc-secret=$rpc_secret"
else
unset_auth_method
fi
elif [ "$rpc_auth_method" = "user_pass" ]; then
if [ -n "$rpc_user" ]; then
append_setting "rpc-user=$rpc_user"
append_setting "rpc-passwd=$rpc_passwd"
else
_info "Please set RPC user."
unset_auth_method
fi
fi
if [ ."$rpc_secure" = ."true" ] && [ -n "$rpc_certificate" ]; then
append_setting "rpc-secure=true"
append_options "rpc_certificate" "rpc_private_key"
fi
if [ ."$check_certificate" = ."true" ]; then
append_setting "check-certificate=true"
append_options "ca_certificate"
elif [ ."$check_certificate" = ."false" ]; then
append_setting "check-certificate=false"
fi
if [ ."$enable_dht" = ."true" ]; then
dht_file="$config_dir/dht.dat.$section"
_create_file "$dht_file" || {
_err "Can't create DHT file: $dht_file"
return 1
}
append_setting "enable-dht=true"
append_setting "dht-file-path=$dht_file"
fi
if [ ."$enable_dht6" = ."true" ] && [ ."$disable_ipv6" != ."true" ]; then
dht6_file="$config_dir/dht6.dat.$section"
_create_file "$dht6_file" || {
_err "Can't create DHT6 file: $dht6_file"
return 1
}
append_setting "enable-dht6=true"
append_setting "dht-file-path6=$dht6_file"
fi
if [ -n "$bt_tracker" ]; then
local bt_tracker_list; local t
for t in $bt_tracker; do
if [ -z "$bt_tracker_list" ]; then
bt_tracker_list="$t"
else
bt_tracker_list="$bt_tracker_list,$t"
fi
done
append_setting "bt-tracker=$bt_tracker_list"
fi
append_options "auto_save_interval" "bt_enable_lpd" "bt_max_open_files" "bt_max_peers" \
"bt_remove_unselected_file" "bt_request_peer_speed_limit" "bt_prioritize_piece" \
"bt_stop_timeout" "bt_detach_seed_only" "bt_save_metadata" "bt_load_saved_metadata" \
"bt_seed_unverified" "certificate" "check_integrity" "connect_timeout" "dht_listen_port" \
"disable_ipv6" "disk_cache" "enable_peer_exchange" "event_poll" "file_allocation" \
"follow_torrent" "force_save" "http_accept_gzip" "http_no_cache" "listen_port" \
"lowest_speed_limit" "max_concurrent_downloads" "max_connection_per_server" \
"max_download_limit" "max_overall_download_limit" "max_overall_upload_limit" "max_tries" \
"max_upload_limit" "min_split_size" "pause" "pause_metadata" "peer_id_prefix" "private_key" \
"retry_wait" "rpc_listen_port" "save_session_interval" "seed_ratio" "seed_time" "split" "timeout" \
"user_agent"
config_list_foreach "$section" "header" append_header
config_list_foreach "$section" "extra_settings" append_setting
cd /usr/share/aria2 && sh ./tracker.sh >/dev/null 2>&1 &
cat /usr/share/aria2/aria2.conf > "$config_file"
echo '' >> "$config_file"
sed '/^$/d' "$config_file_tmp" >> "$config_file"
rm -f "$config_file_tmp"
_reset_dir_mode "$config_dir"
_change_file_mode 600 "$config_file"
if [ -n "$user" ]; then
if ( user_exists "$user" && _change_owner "$user" "$config_dir" "$log" ); then
_info "Aria2 will run with user '$user'."
if [ "$user" != "root" ]; then
_info "Please make sure user '$user' has write access to download dir: $dir"
fi
else
_info "Setting run user to '$user' failed, default user will be used."
user=
fi
fi
procd_open_instance "$NAME.$section"
procd_set_param command "$PROG"
procd_append_param command --conf-path="$config_file"
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param file "$config_file"
[ -n "$user" ] && \
procd_set_param user "$user"
procd_add_jail "$NAME.$section" log
procd_add_jail_mount "/usr/lib/ossl-modules"
procd_add_jail_mount "$ca_certificate" "$certificate" "$rpc_certificate" "$rpc_private_key"
procd_add_jail_mount_rw "$dir" "$config_dir" "$log"
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "$NAME"
procd_add_validation aria2_validate
}
start_service() {
config_load "$NAME"
config_foreach aria2_validate "aria2" aria2_start
}
@@ -1,29 +0,0 @@
--- /src/OptionHandlerFactory.cc 2018-05-15 20:33:25.000000000 +0800
+++ /src/OptionHandlerFactory.cc 2018-07-05 10:40:25.000000000 +0800
@@ -440,7 +440,7 @@
{
OptionHandler* op(new NumberOptionHandler(PREF_MAX_CONNECTION_PER_SERVER,
TEXT_MAX_CONNECTION_PER_SERVER,
- "1", 1, 16, 'x'));
+ "64", 1, -1, 'x'));
op->addTag(TAG_BASIC);
op->addTag(TAG_FTP);
op->addTag(TAG_HTTP);
@@ -501,7 +501,7 @@
}
{
OptionHandler* op(new UnitNumberOptionHandler(
- PREF_MIN_SPLIT_SIZE, TEXT_MIN_SPLIT_SIZE, "20M", 1_m, 1_g, 'k'));
+ PREF_MIN_SPLIT_SIZE, TEXT_MIN_SPLIT_SIZE, "1M", 1_k, 1_g, 'k'));
op->addTag(TAG_BASIC);
op->addTag(TAG_FTP);
op->addTag(TAG_HTTP);
@@ -905,7 +905,7 @@ std::vector<OptionHandler*> OptionHandlerFactory::createOptionHandlers()
}
{
OptionHandler* op(new UnitNumberOptionHandler(
- PREF_PIECE_LENGTH, TEXT_PIECE_LENGTH, "1M", 1_m, 1_g));
+ PREF_PIECE_LENGTH, TEXT_PIECE_LENGTH, "1M", 1_k, 1_g));
op->addTag(TAG_ADVANCED);
op->addTag(TAG_FTP);
op->addTag(TAG_HTTP);
@@ -1,61 +0,0 @@
From 66524bee738e98742c908d93c993d0a78a2d9891 Mon Sep 17 00:00:00 2001
From: myfreeer <myfreeer@users.noreply.github.com>
Date: Sat, 18 Nov 2017 11:55:04 +0800
Subject: [PATCH 3/4] download: retry on slow speed and conection close
This would provide better speed on bad network conditions
---
src/DownloadCommand.cc | 2 +-
src/SocketBuffer.cc | 3 ++-
src/SocketCore.cc | 2 +-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/DownloadCommand.cc b/src/DownloadCommand.cc
index 2db41e4..f49eb80 100644
--- a/src/DownloadCommand.cc
+++ b/src/DownloadCommand.cc
@@ -306,7 +306,7 @@ void DownloadCommand::checkLowestDownloadSpeed() const
startupIdleTime_) {
int nowSpeed = peerStat_->calculateDownloadSpeed();
if (nowSpeed <= lowestDownloadSpeedLimit_) {
- throw DL_ABORT_EX2(fmt(EX_TOO_SLOW_DOWNLOAD_SPEED, nowSpeed,
+ throw DL_RETRY_EX2(fmt(EX_TOO_SLOW_DOWNLOAD_SPEED, nowSpeed,
lowestDownloadSpeedLimit_,
getRequest()->getHost().c_str()),
error_code::TOO_SLOW_DOWNLOAD_SPEED);
diff --git a/src/SocketBuffer.cc b/src/SocketBuffer.cc
index 62862ff..1906173 100644
--- a/src/SocketBuffer.cc
+++ b/src/SocketBuffer.cc
@@ -39,6 +39,7 @@
#include "SocketCore.h"
#include "DlAbortEx.h"
+#include "DlRetryEx.h"
#include "message.h"
#include "fmt.h"
#include "LogFactory.h"
@@ -158,7 +159,7 @@ ssize_t SocketBuffer::send()
}
ssize_t slen = socket_->writeVector(iov, num);
if (slen == 0 && !socket_->wantRead() && !socket_->wantWrite()) {
- throw DL_ABORT_EX(fmt(EX_SOCKET_SEND, "Connection closed."));
+ throw DL_RETRY_EX(fmt(EX_SOCKET_SEND, "Connection closed."));
}
// A2_LOG_NOTICE(fmt("num=%zu, amount=%d, bufq.size()=%zu, SEND=%d",
// num, amount, bufq_.size(), slen));
diff --git a/src/SocketCore.cc b/src/SocketCore.cc
index 77dc30c..537375a 100644
--- a/src/SocketCore.cc
+++ b/src/SocketCore.cc
@@ -1009,7 +1009,7 @@ bool SocketCore::tlsHandshake(TLSContext* tlsctx, const std::string& hostname)
if (rv == TLS_ERR_ERROR) {
// Damn those error.
- throw DL_ABORT_EX(fmt("SSL/TLS handshake failure: %s",
+ throw DL_RETRY_EX(fmt("SSL/TLS handshake failure: %s",
handshakeError.empty()
? tlsSession_->getLastErrorString().c_str()
: handshakeError.c_str()));
--
2.17.1
@@ -1,239 +0,0 @@
From 8adbc01dc5975a64c55fe594d8c758c71e8183b3 Mon Sep 17 00:00:00 2001
From: myfreeer <myfreeer@users.noreply.github.com>
Date: Sun, 22 Jul 2018 19:59:02 +0800
Subject: [PATCH] option: add option to retry on http 400, 403, 406, or unknown
--retry-on-400[=true|false] Configure whether retry or not when
HTTP server returns 400 Bad Request.
Only effective if retry-wait > 0.
Possible Values: true, false
Default: false
Tags: #advanced, #http
--retry-on-403[=true|false] Configure whether retry or not when
HTTP server returns 403 Forbidden.
Only effective if retry-wait > 0.
Possible Values: true, false
Default: false
Tags: #advanced, #http
--retry-on-406[=true|false] Configure whether retry or not when
HTTP server returns 406 Not Acceptable.
Only effective if retry-wait > 0.
Possible Values: true, false
Default: false
Tags: #advanced, #http
--retry-on-unknown[=true|false] Configure whether retry or not when
HTTP server returns unknown status code.
Only effective if retry-wait > 0.
Possible Values: true, false
Default: false
Tags: #advanced, #http
---
src/HttpSkipResponseCommand.cc | 42 +++++++++++++++++++++++++++++-----
src/OptionHandlerFactory.cc | 40 ++++++++++++++++++++++++++++++++
src/prefs.cc | 8 +++++++
src/prefs.h | 8 +++++++
src/usage_text.h | 16 +++++++++++++
5 files changed, 108 insertions(+), 6 deletions(-)
diff --git a/src/HttpSkipResponseCommand.cc b/src/HttpSkipResponseCommand.cc
index a722d77..de4ad6c 100644
--- a/src/HttpSkipResponseCommand.cc
+++ b/src/HttpSkipResponseCommand.cc
@@ -204,7 +204,7 @@ bool HttpSkipResponseCommand::processResponse()
auto statusCode = httpResponse_->getStatusCode();
if (statusCode >= 400) {
switch (statusCode) {
- case 401:
+ case 401: // Unauthorized
if (getOption()->getAsBool(PREF_HTTP_AUTH_CHALLENGE) &&
!httpResponse_->getHttpRequest()->authenticationUsed() &&
getDownloadEngine()->getAuthConfigFactory()->activateBasicCred(
@@ -213,15 +213,41 @@ bool HttpSkipResponseCommand::processResponse()
return prepareForRetry(0);
}
throw DL_ABORT_EX2(EX_AUTH_FAILED, error_code::HTTP_AUTH_FAILED);
- case 404:
+ case 404: // Not Found
if (getOption()->getAsInt(PREF_MAX_FILE_NOT_FOUND) == 0) {
throw DL_ABORT_EX2(MSG_RESOURCE_NOT_FOUND,
error_code::RESOURCE_NOT_FOUND);
}
throw DL_RETRY_EX2(MSG_RESOURCE_NOT_FOUND,
error_code::RESOURCE_NOT_FOUND);
- case 502:
- case 503:
+ case 400: // Bad Request
+ if (getOption()->getAsBool(PREF_RETRY_ON_400)
+ && getOption()->getAsInt(PREF_RETRY_WAIT) > 0) {
+ throw DL_RETRY_EX2(fmt(EX_BAD_STATUS, statusCode),
+ error_code::HTTP_PROTOCOL_ERROR);
+ }
+ break;
+ case 403: // Forbidden
+ if (getOption()->getAsBool(PREF_RETRY_ON_403)
+ && getOption()->getAsInt(PREF_RETRY_WAIT) > 0) {
+ throw DL_RETRY_EX2(fmt(EX_BAD_STATUS, statusCode),
+ error_code::HTTP_PROTOCOL_ERROR);
+ }
+ break;
+ case 406: // Not Acceptable
+ if (getOption()->getAsBool(PREF_RETRY_ON_406)
+ && getOption()->getAsInt(PREF_RETRY_WAIT) > 0) {
+ throw DL_RETRY_EX2(fmt(EX_BAD_STATUS, statusCode),
+ error_code::HTTP_PROTOCOL_ERROR);
+ }
+ break;
+ case 408: // Request Timeout
+ case 429: // Too Many Requests
+ case 502: // Bad Gateway
+ case 503: // Service Unavailable
+ case 507: // Insufficient Storage
+ case 520: // https://github.com/aria2/aria2/issues/1229
+ case 521: // https://github.com/aria2/aria2/issues/1229
// Only retry if pretry-wait > 0. Hammering 'busy' server is not
// a good idea.
if (getOption()->getAsInt(PREF_RETRY_WAIT) > 0) {
@@ -230,12 +256,16 @@ bool HttpSkipResponseCommand::processResponse()
}
throw DL_ABORT_EX2(fmt(EX_BAD_STATUS, statusCode),
error_code::HTTP_SERVICE_UNAVAILABLE);
- case 504:
+ case 504: // Gateway Timeout
// This is Gateway Timeout, so try again
throw DL_RETRY_EX2(fmt(EX_BAD_STATUS, statusCode),
error_code::HTTP_SERVICE_UNAVAILABLE);
};
-
+ if (getOption()->getAsBool(PREF_RETRY_ON_UNKNOWN)
+ && getOption()->getAsInt(PREF_RETRY_WAIT) > 0) {
+ throw DL_RETRY_EX2(fmt(EX_BAD_STATUS, statusCode),
+ error_code::HTTP_PROTOCOL_ERROR);
+ }
throw DL_ABORT_EX2(fmt(EX_BAD_STATUS, statusCode),
error_code::HTTP_PROTOCOL_ERROR);
}
diff --git a/src/OptionHandlerFactory.cc b/src/OptionHandlerFactory.cc
index 5768f7b..decb03e 100644
--- a/src/OptionHandlerFactory.cc
+++ b/src/OptionHandlerFactory.cc
@@ -934,6 +934,46 @@ std::vector<OptionHandler*> OptionHandlerFactory::createOptionHandlers()
op->setChangeOptionForReserved(true);
handlers.push_back(op);
}
+ {
+ OptionHandler* op(new BooleanOptionHandler(
+ PREF_RETRY_ON_400, TEXT_RETRY_ON_400, A2_V_FALSE, OptionHandler::OPT_ARG));
+ op->addTag(TAG_ADVANCED);
+ op->addTag(TAG_HTTP);
+ op->setInitialOption(true);
+ op->setChangeGlobalOption(true);
+ op->setChangeOptionForReserved(true);
+ handlers.push_back(op);
+ }
+ {
+ OptionHandler* op(new BooleanOptionHandler(
+ PREF_RETRY_ON_403, TEXT_RETRY_ON_403, A2_V_FALSE, OptionHandler::OPT_ARG));
+ op->addTag(TAG_ADVANCED);
+ op->addTag(TAG_HTTP);
+ op->setInitialOption(true);
+ op->setChangeGlobalOption(true);
+ op->setChangeOptionForReserved(true);
+ handlers.push_back(op);
+ }
+ {
+ OptionHandler* op(new BooleanOptionHandler(
+ PREF_RETRY_ON_406, TEXT_RETRY_ON_406, A2_V_FALSE, OptionHandler::OPT_ARG));
+ op->addTag(TAG_ADVANCED);
+ op->addTag(TAG_HTTP);
+ op->setInitialOption(true);
+ op->setChangeGlobalOption(true);
+ op->setChangeOptionForReserved(true);
+ handlers.push_back(op);
+ }
+ {
+ OptionHandler* op(new BooleanOptionHandler(
+ PREF_RETRY_ON_UNKNOWN, TEXT_RETRY_ON_UNKNOWN, A2_V_FALSE, OptionHandler::OPT_ARG));
+ op->addTag(TAG_ADVANCED);
+ op->addTag(TAG_HTTP);
+ op->setInitialOption(true);
+ op->setChangeGlobalOption(true);
+ op->setChangeOptionForReserved(true);
+ handlers.push_back(op);
+ }
{
OptionHandler* op(new BooleanOptionHandler(
PREF_REUSE_URI, TEXT_REUSE_URI, A2_V_TRUE, OptionHandler::OPT_ARG));
diff --git a/src/prefs.cc b/src/prefs.cc
index 937e927..33eff91 100644
--- a/src/prefs.cc
+++ b/src/prefs.cc
@@ -327,6 +327,14 @@ PrefPtr PREF_ENABLE_ASYNC_DNS6 = makePref("enable-async-dns6");
PrefPtr PREF_MAX_DOWNLOAD_RESULT = makePref("max-download-result");
// value: 1*digit
PrefPtr PREF_RETRY_WAIT = makePref("retry-wait");
+// value: true | false
+PrefPtr PREF_RETRY_ON_400 = makePref("retry-on-400");
+// value: true | false
+PrefPtr PREF_RETRY_ON_403 = makePref("retry-on-403");
+// value: true | false
+PrefPtr PREF_RETRY_ON_406 = makePref("retry-on-406");
+// value: true | false
+PrefPtr PREF_RETRY_ON_UNKNOWN = makePref("retry-on-unknown");
// value: string
PrefPtr PREF_ASYNC_DNS_SERVER = makePref("async-dns-server");
// value: true | false
diff --git a/src/prefs.h b/src/prefs.h
index e1f8397..019e774 100644
--- a/src/prefs.h
+++ b/src/prefs.h
@@ -280,6 +280,14 @@ extern PrefPtr PREF_ENABLE_ASYNC_DNS6;
extern PrefPtr PREF_MAX_DOWNLOAD_RESULT;
// value: 1*digit
extern PrefPtr PREF_RETRY_WAIT;
+// value: true | false
+extern PrefPtr PREF_RETRY_ON_400;
+// value: true | false
+extern PrefPtr PREF_RETRY_ON_403;
+// value: true | false
+extern PrefPtr PREF_RETRY_ON_406;
+// value: true | false
+extern PrefPtr PREF_RETRY_ON_UNKNOWN;
// value: string
extern PrefPtr PREF_ASYNC_DNS_SERVER;
// value: true | false
diff --git a/src/usage_text.h b/src/usage_text.h
index d73b50d..75d34a0 100644
--- a/src/usage_text.h
+++ b/src/usage_text.h
@@ -64,6 +64,22 @@
_(" --retry-wait=SEC Set the seconds to wait between retries. \n" \
" With SEC > 0, aria2 will retry download when the\n" \
" HTTP server returns 503 response.")
+#define TEXT_RETRY_ON_400 \
+ _(" --retry-on-400[=true|false] Configure whether retry or not when\n" \
+ " HTTP server returns 400 Bad Request.\n" \
+ " Only effective if retry-wait > 0.")
+#define TEXT_RETRY_ON_403 \
+ _(" --retry-on-403[=true|false] Configure whether retry or not when\n" \
+ " HTTP server returns 403 Forbidden.\n" \
+ " Only effective if retry-wait > 0.")
+#define TEXT_RETRY_ON_406 \
+ _(" --retry-on-406[=true|false] Configure whether retry or not when\n" \
+ " HTTP server returns 406 Not Acceptable.\n" \
+ " Only effective if retry-wait > 0.")
+#define TEXT_RETRY_ON_UNKNOWN \
+ _(" --retry-on-unknown[=true|false] Configure whether retry or not when\n" \
+ " HTTP server returns unknown status code.\n" \
+ " Only effective if retry-wait > 0.")
#define TEXT_TIMEOUT \
_(" -t, --timeout=SEC Set timeout in seconds.")
#define TEXT_MAX_TRIES \
--
2.18.0
-35
View File
@@ -1,35 +0,0 @@
#
# Copyright (C) 2010-2011 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=autoshare-ksmbd
PKG_VERSION:=1
PKG_RELEASE:=11
PKG_ARCH:=all
include $(INCLUDE_DIR)/package.mk
define Package/autoshare-ksmbd
TITLE:=Samba autoconfig hotplug script.
MAINTAINER:=Lean
DEPENDS:=+wsdd2
endef
define Package/autoshare-ksmbd/description
A hotplug script to config Samba share automatically.
endef
define Build/Compile
endef
define Package/autoshare-ksmbd/install
$(INSTALL_DIR) $(1)/etc/hotplug.d/block
$(INSTALL_BIN) ./files/20-smb-ksmbd $(1)/etc/hotplug.d/block/20-smb-ksmbd
endef
$(eval $(call BuildPackage,autoshare-ksmbd))
-120
View File
@@ -1,120 +0,0 @@
#!/bin/sh
. /lib/functions.sh
. /lib/functions/service.sh
global=0
config_file="/etc/config/ksmbd"
[ ! -f /etc/config/ksmbd ] && {
echo "
config globals
option workgroup 'WORKGROUP'
option charset 'UTF-8'
option description 'Ksmbd on OpenWRT'
" > $config_file
}
wait_for_init() {
for i in `seq 30`
do
[ -e /tmp/procd.done ] || {
sleep 1; continue;
}
return
done
}
smb_handle() {
local path
config_get path "$1" path
if [ -n "$path" ]; then
case "$path" in
"$2" | "$2"/*) global=1 ;;
esac
case "$2" in
"$path" | "$path"/*) global=1 ;;
esac
fi
}
chk_en() {
config_get_bool autoshare "$1" autoshare 1
[ "$autoshare" -eq 0 ] && exit
}
config_load ksmbd
config_foreach chk_en ksmbd
device=`basename "$DEVPATH"`
case "$ACTION" in
add)
case "$device" in
sd*);;
md*);;
hd*);;
mmcblk*);;
nvme*);;
*) return;;
esac
path="/dev/$device"
wait_for_init
grep -v '/boot\|/opt' /proc/mounts | while read -r j
do
str=${j%% *}
if [ "$str" = "$path" ];then
strr=${j#* }
target=${strr%% *}
global=0
config_load ksmbd
config_foreach smb_handle share "$target"
name=${target#*/mnt/}
name=$(echo "$name" | sed -e "s/^\///")
if [ "$global" -eq 0 ] ;then
echo -e "\n\nconfig share" >> $config_file
echo -e "\toption auto '1'" >> $config_file
echo -e "\toption name '$name'" >> $config_file
echo -e "\toption path '$target'" >> $config_file
echo -e "\toption read_only 'no'" >> $config_file
echo -e "\toption guest_ok 'yes'" >> $config_file
echo -e "\toption create_mask '0666'" >> $config_file
echo -e "\toption dir_mask '0777'" >> $config_file
echo -e "\toption device '$device'" >> $config_file
echo -e "\toption force_root '1'" >> $config_file
/etc/init.d/ksmbd reload
return
fi
fi
done
;;
remove)
i=0
while true
do
if ! uci -q get ksmbd.@share[$i] >/dev/null; then
break
fi
dev=$(uci -q get ksmbd.@share[$i].device)
if [ "$dev" = "$device" ]; then
auto=$(uci -q get ksmbd.@share[$i].auto)
if [ "$auto" = "1" ]; then
uci delete ksmbd.@share[$i]
uci commit ksmbd
/etc/init.d/ksmbd reload
return
fi
fi
let i+=1
done
;;
esac
-35
View File
@@ -1,35 +0,0 @@
#
# Copyright (C) 2010-2011 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=autoshare-samba
PKG_VERSION:=1
PKG_RELEASE:=11
PKG_ARCH:=all
include $(INCLUDE_DIR)/package.mk
define Package/autoshare-samba
TITLE:=Samba autoconfig hotplug script.
MAINTAINER:=Lean
DEPENDS:=+wsdd2
endef
define Package/autoshare-samba/description
A hotplug script to config Samba share automatically.
endef
define Build/Compile
endef
define Package/autoshare-samba/install
$(INSTALL_DIR) $(1)/etc/hotplug.d/block
$(INSTALL_BIN) ./files/20-smb $(1)/etc/hotplug.d/block/20-smb
endef
$(eval $(call BuildPackage,autoshare-samba))
-120
View File
@@ -1,120 +0,0 @@
#!/bin/sh
. /lib/functions.sh
. /lib/functions/service.sh
global=0
config_file="/etc/config/samba4"
[ ! -f /etc/config/samba4 ] && {
echo "
config samba
option workgroup 'WORKGROUP'
option charset 'UTF-8'
option description 'Samba on OpenWRT'
" > $config_file
}
wait_for_init() {
for i in `seq 30`
do
[ -e /tmp/procd.done ] || {
sleep 1; continue;
}
return
done
}
smb_handle() {
local path
config_get path "$1" path
if [ -n "$path" ]; then
case "$path" in
"$2" | "$2"/*) global=1 ;;
esac
case "$2" in
"$path" | "$path"/*) global=1 ;;
esac
fi
}
chk_en() {
config_get_bool autoshare "$1" autoshare 1
[ "$autoshare" -eq 0 ] && exit
}
config_load samba4
config_foreach chk_en samba4
device=`basename "$DEVPATH"`
case "$ACTION" in
add)
case "$device" in
sd*);;
md*);;
hd*);;
mmcblk*);;
nvme*);;
*) return;;
esac
path="/dev/$device"
wait_for_init
grep -v '/boot\|/opt' /proc/mounts | while read -r j
do
str=${j%% *}
if [ "$str" = "$path" ];then
strr=${j#* }
target=${strr%% *}
global=0
config_load samba4
config_foreach smb_handle sambashare "$target"
name=${target#*/mnt/}
name=$(echo "$name" | sed -e "s/^\///")
if [ "$global" -eq 0 ] ;then
echo -e "\n\nconfig sambashare" >> $config_file
echo -e "\toption auto '1'" >> $config_file
echo -e "\toption name '$name'" >> $config_file
echo -e "\toption path '$target'" >> $config_file
echo -e "\toption read_only 'no'" >> $config_file
echo -e "\toption guest_ok 'yes'" >> $config_file
echo -e "\toption create_mask '0666'" >> $config_file
echo -e "\toption dir_mask '0777'" >> $config_file
echo -e "\toption device '$device'" >> $config_file
echo -e "\toption force_root '1'" >> $config_file
/etc/init.d/samba4 reload
return
fi
fi
done
;;
remove)
i=0
while true
do
if ! uci -q get samba4.@sambashare[$i] >/dev/null; then
break
fi
dev=$(uci -q get samba4.@sambashare[$i].device)
if [ "$dev" = "$device" ]; then
auto=$(uci -q get samba4.@sambashare[$i].auto)
if [ "$auto" = "1" ]; then
uci delete samba4.@sambashare[$i]
uci commit samba4
/etc/init.d/samba4 reload
return
fi
fi
let i+=1
done
;;
esac
-46
View File
@@ -1,46 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2009-2013 OpenWrt.org
#
include $(TOPDIR)/rules.mk
PKG_NAME:=BaiduPCS-Go
PKG_VERSION:=3.9.0
PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/qjfoidnh/BaiduPCS-Go/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=de71666e961a3eeb90f8aca7b11934c3f0296d1870e036a7c086ba1decfca8ab
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
GO_PKG:=github.com/qjfoidnh/BaiduPCS-Go
GO_PKG_LDFLAGS_X:=main.Version=v$(PKG_VERSION)
include $(INCLUDE_DIR)/package.mk
include ../../lang/golang/golang-package.mk
define Package/BaiduPCS-Go
SECTION:=net
CATEGORY:=Network
SUBMENU:=Download Manager
TITLE:=Terminal utility for Baidu Network Disk
URL:=https://github.com/qjfoidnh/BaiduPCS-Go
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
endef
define Package/BaiduPCS-Go/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(INSTALL_DIR) $(1)/usr/bin/
$(CP) $(PKG_INSTALL_DIR)/usr/bin/BaiduPCS-Go $(1)/usr/bin/
endef
$(eval $(call GoBinPackage,BaiduPCS-Go))
$(eval $(call BuildPackage,BaiduPCS-Go))
@@ -1,25 +0,0 @@
From: coolsnowwolf <coolsnowwolf@gmail.com>
Subject: [PATCH] cachepool: stop using runtime.rawbyteslice
Recent Go releases reject external linkname references to rawbyteslice.
Use the language-provided slice allocator for the zeroed allocation path.
Signed-off-by: coolsnowwolf <coolsnowwolf@gmail.com>
---
--- a/pcsutil/cachepool/malloc.go
+++ b/pcsutil/cachepool/malloc.go
@@ -8,12 +8,9 @@
//go:linkname mallocgc runtime.mallocgc
func mallocgc(size uintptr, typ uintptr, needzero bool) unsafe.Pointer
-//go:linkname rawbyteslice runtime.rawbyteslice
-func rawbyteslice(size int) (b []byte)
-
-// RawByteSlice point to runtime.rawbyteslice
+// RawByteSlice allocates a zeroed byte slice.
func RawByteSlice(size int) (b []byte) {
- return rawbyteslice(size)
+ return make([]byte, size)
}
// RawMalloc allocates a new slice. The slice is not zeroed.
-8
View File
@@ -1,8 +0,0 @@
__
___ ___ ___ ___ _ ______/ /_
/ _ \/ _ \/ -_) _ \ |/|/ / __/ __/
\___/ .__/\__/_//_/__,__/_/ \__/
/_/
--------------------------------
%D %V, %C
--------------------------------
-57
View File
@@ -1,57 +0,0 @@
#
# Copyright (C) 2006-2009 OpenWrt.org
# Copyright (C) 2005-2016 Weijie Gao <hackpascal@gmail.com>
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=brlaser
PKG_VERSION:=v3
PKG_RELEASE:=1
PKG_SOURCE_URL:=https://github.com/pdewacht/brlaser.git
PKG_SOURCE_PROTO:=git
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_SOURCE_VERSION:=2a49e3287c70c254e7e3ac9dabe9d6a07218c3fa
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(PKG_SOURCE_VERSION).tar.gz
PKG_FIXUP:=autoreconf
PKG_INSTALL:=1
include $(INCLUDE_DIR)/package.mk
include $(INCLUDE_DIR)/cmake.mk
define Package/brlaser
SECTION:=print
CATEGORY:=Printing and Imaging
TITLE:=Driver for (some) Brother laster printers
DEPENDS:=+libcupsimage +libstdcpp
MAINTAINER:=Weijie Gao <hackpascal@gmail.com>
URL:=https://github.com/pdewacht/brlaser
endef
define Package/brlaser/description
Driver for Brother DCP-7030 / Brother DCP-7055 / Brother DCP-7065DN
endef
define Package/brlaser/install
$(INSTALL_DIR) $(1)/usr/lib/cups/filter
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/lib/cups/filter/rastertobrlaser $(1)/usr/lib/cups/filter/
$(INSTALL_DIR) $(1)/usr/share/cups/drv
$(INSTALL_DATA) $(PKG_INSTALL_DIR)/usr/share/cups/drv/brlaser.drv $(1)/usr/share/cups/drv/
(cd $(PKG_BUILD_DIR) && ppdc brlaser.drv)
$(INSTALL_DIR) $(1)/usr/share/ppd/Brother
$(CP) $(PKG_BUILD_DIR)/ppd/*.ppd $(1)/usr/share/ppd/Brother
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) ./files/brlaser-set-cups-filter-permission $(1)/etc/uci-defaults/
endef
$(eval $(call BuildPackage,brlaser))
@@ -1,3 +0,0 @@
#!/bin/sh
[ -d /usr/lib/cups/filter ] && chmod 0754 /usr/lib/cups/filter/*
-45
View File
@@ -1,45 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021-2023 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=brook
PKG_VERSION:=20270101
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/txthinking/brook/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=43d8e5476918daa2d35fc63e8b0c94c0c1df8577f1d09103a3ab0f6141f29c0f
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_LICENSE:=GPL-3.0
PKG_LICENSE_FILES:=LICENSE
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/txthinking/brook
GO_PKG_BUILD_PKG:=$(GO_PKG)/cli/brook
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/brook
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
TITLE:=A cross-platform proxy software
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
URL:=https://github.com/txthinking/brook
endef
define Package/brook/description
Brook is a cross-platform strong encryption and not detectable proxy.
Zero-Configuration.
endef
$(eval $(call GoBinPackage,brook))
$(eval $(call BuildPackage,brook))
-72
View File
@@ -1,72 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-only
include $(TOPDIR)/rules.mk
PKG_NAME:=chinadns-ng
PKG_VERSION:=2025.08.09
PKG_RELEASE:=1
ifeq ($(ARCH),aarch64)
ifeq ($(BOARD),rockchip)
PKG_ARCH:=chinadns-ng+wolfssl@aarch64-linux-musl@generic+v8a@fast+lto
PKG_HASH:=3fe0217615dd7060b7287d2b6b31d2a0b364137398bfb335a03bead322eac716
else
PKG_ARCH:=chinadns-ng+wolfssl_noasm@aarch64-linux-musl@generic+v8a@fast+lto
PKG_HASH:=42ddd494200ec6d88b35902927688d316bc23e06e6c08d9e01eb2412196ab845
endif
else ifeq ($(ARCH),arm)
ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE))))
ifeq ($(ARM_CPU_FEATURES),)
PKG_ARCH:=chinadns-ng+wolfssl@arm-linux-musleabi@generic+v6+soft_float@fast+lto
PKG_HASH:=0a401d1dc11129481b2baf86f847d55d66bd7e725cba4bf57875fdad27ef0052
else
PKG_ARCH:=chinadns-ng+wolfssl@arm-linux-musleabihf@generic+v7a@fast+lto
PKG_HASH:=dfa1f6ba80fb0925613822f4c4e00df8da68e7b8b772048d26a0d1a9d07d346b
endif
else ifeq ($(ARCH),mips)
PKG_ARCH:=chinadns-ng+wolfssl@mips-linux-musl@mips32+soft_float@fast+lto
PKG_HASH:=b610821a8f61b0ed3c8c7e82e10d401348a9de17f900988589024a37c4099c8e
else ifeq ($(ARCH),mipsel)
PKG_ARCH:=chinadns-ng+wolfssl@mipsel-linux-musl@mips32+soft_float@fast+lto
PKG_HASH:=760544a88724e3b1b9eac79c9400231e81aa8786f8f00a979229e175811ffe6d
else ifeq ($(ARCH),mips64)
PKG_ARCH:=chinadns-ng+wolfssl@mips64-linux-musl@mips64+soft_float@fast+lto
PKG_HASH:=2d0fce18a7ef1d74fdc12738767e66998a52c2b30d8790da760933853fe8726e
else ifeq ($(ARCH),i386)
PKG_ARCH:=chinadns-ng+wolfssl@i386-linux-musl@i686@fast+lto
PKG_HASH:=85e057dd0a0e8913b30471737436ab8b71834c494ed9f9e53544261b1ffdc8d6
else ifeq ($(ARCH),x86_64)
PKG_ARCH:=chinadns-ng+wolfssl@x86_64-linux-musl@x86_64@fast+lto
PKG_HASH:=842ea4e9816efd91d39bc76ead5c4a42e79011757e37c521b4270b675cfcb30c
else
PKG_HASH:=dummy
endif
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(PKG_ARCH)
PKG_SOURCE_URL:=https://github.com/zfl9/chinadns-ng/releases/download/$(PKG_VERSION)/$(PKG_ARCH)?
UNPACK_CMD=$(CP) $(DL_DIR)/$(PKG_SOURCE) $(PKG_BUILD_DIR)/$(PKG_NAME)
PKG_LICENSE:=AGPL-3.0-only
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=sbwml <admin@cooluc.com>
include $(INCLUDE_DIR)/package.mk
define Package/chinadns-ng
SECTION:=net
CATEGORY:=Network
SUBMENU:=IP Addresses and Names
TITLE:=ChinaDNS next generation, refactoring with epoll and ipset.
URL:=https://github.com/zfl9/chinadns-ng
DEPENDS:=@(aarch64||arm||i386||mips||mipsel||mips64||x86_64) +ipset
endef
define Build/Compile
endef
define Package/chinadns-ng/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/chinadns-ng $(1)/usr/bin
endef
$(eval $(call BuildPackage,chinadns-ng))
-111
View File
@@ -1,111 +0,0 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=clashoo
PKG_SHORT_SHA:=3c947c7
PKG_COMMIT_DATE:=2026.09.22
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
PKG_SOURCE_VERSION:=3c947c76d4f48a938aace935a30a1da1364a1b1b
PKG_HASH:=6f557ac33177ca56ca477e22f4bf4196483bb487105ec45c28a4219522c4e652
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
PKG_LICENSE:=GPL3.0+
PKG_MAINTAINER:=kenzok8
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
GO_PKG:=github.com/metacubex/mihomo
GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
GO_PKG_TAGS:=with_gvisor
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/clashoo
SECTION:=net
CATEGORY:=Network
TITLE:=Clashoo (Mihomo)
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle +yq +firewall4 +ip-full +coreutils-nohup +coreutils-timeout +kmod-inet-diag +kmod-nft-socket +kmod-nft-tproxy +kmod-tun +kmod-dummy +unzip
PROVIDES:=mihomo clash-meta
endef
define Package/clashoo/description
Mihomo proxy core with runtime environment for Clashoo.
endef
define Package/clashoo/conffiles
/etc/config/clashoo
endef
define Package/clashoo/install
$(call GoPackage/Package/Install/Bin,$(1))
$(INSTALL_DIR) $(1)/usr/bin
ln -sf /usr/bin/mihomo $(1)/usr/bin/clash-meta
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_DIR) $(1)/etc/clashoo
$(INSTALL_DIR) $(1)/etc/clashoo/provider
$(INSTALL_DIR) $(1)/etc/clashoo/proxyprovider
$(INSTALL_DIR) $(1)/etc/clashoo/ruleprovider
$(INSTALL_DIR) $(1)/usr/share/clashoo
$(INSTALL_DIR) $(1)/usr/share/clashbackup
$(INSTALL_DIR) $(1)/usr/share/clashoo/config
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/sub
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/upload
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/custom
$(INSTALL_BIN) $(CURDIR)/files/etc/init.d/clashoo $(1)/etc/init.d/clashoo
$(INSTALL_CONF) $(CURDIR)/files/etc/config/clashoo $(1)/etc/config/clashoo
$(INSTALL_DATA) $(CURDIR)/files/etc/config/clashoo $(1)/usr/share/clashoo/clashoo.default
$(CP) $(CURDIR)/files/usr/share/clashoo/* $(1)/usr/share/clashoo/
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/migrate_lan_acl.sh $(1)/usr/share/clashoo/migrate_lan_acl.sh
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/migrate_fake_ip_filter.sh $(1)/usr/share/clashoo/migrate_fake_ip_filter.sh
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/detect_core_arch.sh $(1)/usr/share/clashoo/detect_core_arch.sh
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/diagnose_bypass.sh $(1)/usr/share/clashoo/diagnose_bypass.sh
$(RM) $(1)/usr/share/clashoo/dashboard
$(RM) $(1)/usr/share/clashoo/yacd
$(RM) $(1)/usr/share/clashoo/update.log
$(RM) $(1)/usr/share/clashoo/geoip.log
$(RM) $(1)/usr/share/clashoo/core_down_complete
endef
define Package/clashoo/postinst
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
[ -x /usr/bin/mihomo ] && [ ! -x /usr/bin/clash-meta ] && ln -sf /usr/bin/mihomo /usr/bin/clash-meta
rm -f /usr/share/clashoo/check_luci_version.sh /usr/share/clashoo/check_clash_meta_version.sh /usr/share/clashoo/check_mihomo_core_version.sh
rm -f /usr/share/clashoo/cuslist.sh /usr/share/clashoo/groups.sh /usr/share/clashoo/load_groups.sh /usr/share/clashoo/panel_diag.sh
rm -f /usr/share/clashoo/rmlist.sh /usr/share/clashoo/uplist.sh /usr/share/clashoo/rpc_restart_async.sh /usr/share/clashoo/rpc_update_china_ip_async.sh
rm -rf /usr/share/clashoo/create
/usr/share/clashoo/migrate_lan_acl.sh >/dev/null 2>&1 || true
/usr/share/clashoo/migrate_fake_ip_filter.sh >/dev/null 2>&1 || true
/usr/share/clashoo/detect_core_arch.sh >/dev/null 2>&1 || true
if [ "$$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then
/etc/init.d/clashoo enable 2>/dev/null || true
else
/etc/init.d/clashoo disable 2>/dev/null || true
fi
fi
exit 0
endef
define Build/Prepare
$(Build/Prepare/Default)
$(RM) -r $(PKG_BUILD_DIR)/rules/logic_test
endef
$(eval $(call GoBinPackage,clashoo))
$(eval $(call BuildPackage,clashoo))
-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 '3'
option allow_lan '1'
option dnsforwader '1'
option subcri 'meta'
option cusrule '0'
option dnscache '1'
option p_mode 'rule'
option append_rules '0'
option enhanced_mode 'fake-ip'
option fake_ip_range '198.18.0.1/16'
option fake_ip_range6 'fc00::/18'
list fake_ip_filter '+.lan'
list fake_ip_filter '+.local'
list fake_ip_filter 'localhost.ptlogin2.qq.com'
list fake_ip_filter 'rule-set:cn_domain'
option fake_ip_filter_migrated '1'
option listen_port '1053'
option interf '0'
option download_core 'amd64-compatible'
option dcore '3'
option same_tag '1'
option geoip_source '3'
option geoip_format 'mmdb'
option geodata_loader 'standard'
option tproxy_port '7982'
option access_control '0'
option acl_migrated '1'
option tcp_mode 'redirect'
option tun_mode '0'
option core_mirror_prefix 'https://gh-proxy.com/'
option core_download_advanced '0'
list default_nameserver '223.5.5.5'
list default_nameserver '119.29.29.29'
option dns_ecs ''
option dns_ecs_override '0'
option fallback_filter_geoip '0'
list fallback_filter_ipcidr '240.0.0.0/4'
option dns_respect_rules '1'
option dns_loopback_compat '0'
option dns_migrated '1'
option block_quic '0'
option singbox_independent_cache '0'
option udp_mode 'tproxy'
option ipv4_dns_hijack '1'
option ipv6_dns_hijack '1'
option ipv4_proxy '1'
option ipv6_proxy '1'
option fake_ip_ping_hijack '1'
option dns_leak_protect '0'
option geoip_mmdb_url 'https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb'
option geosite_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat'
option geoip_dat_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat'
option enable_udp '1'
option stack 'gvisor'
option bypass_china '1'
option bypass_china_ipv6 '1'
option bypass_port_mode 'common'
option sniffer_streaming '1'
option selection_cache '1'
option fake_ip_cache '1'
option enable_dns '1'
option disable_quic_gso '1'
option smart_policy_priority 'Premium:0.9;SG:1.3'
option smart_prefer_asn '1'
option smart_uselightgbm '1'
option smart_collectdata '1'
option smart_collect_size '100'
option smart_collect_rate '1'
option smart_lgbm_auto_update '1'
option smart_lgbm_update_interval '72'
option smart_lgbm_url 'https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin'
list defaul_nameserver '114.114.114.114'
list defaul_nameserver '223.5.5.5'
option auto_update '1'
option auto_update_time '72'
option auto_subscription_update '0'
option subscription_update_interval '72'
option auto_clear_log '1'
option clear_time '72'
option core_type 'mihomo'
option smart_auto_switch '0'
option core_only '0'
config hosts
option enabled '0'
option adress '*.clash.dev'
option ip '127.0.0.1'
config hosts
option enabled '0'
option adress 'alpha.clash.dev'
option ip '::1'
config dnsservers
option enabled '1'
option ser_type 'nameserver'
option ser_address 'https://dns.alidns.com/dns-query'
option protocol 'none'
config dnsservers
option enabled '1'
option ser_type 'nameserver'
option ser_address 'https://doh.pub/dns-query'
option protocol 'none'
config dnsservers
option enabled '1'
option ser_type 'proxy-server-nameserver'
option ser_address '1.1.1.1'
option protocol 'tls://'
option ser_port '853'
config dnsservers
option enabled '1'
option ser_type 'fallback'
option ser_address 'https://cloudflare-dns.com/dns-query'
option protocol 'none'
config dnsservers
option enabled '1'
option ser_type 'fallback'
option ser_address 'https://dns.google/dns-query'
option protocol 'none'
config dnsservers
option enabled '1'
option ser_type 'direct-nameserver'
option ser_address '223.5.5.5'
option protocol 'udp://'
config dns_policy
option enabled '1'
option policy_type 'nameserver-policy'
option matcher 'geosite:cn'
list nameserver 'https://dns.alidns.com/dns-query'
list nameserver 'https://doh.pub/dns-query'
config authentication
# default empty custom-rule row so the section shows the input form, not just an add
# button; injectors skip rules with an empty ipaaddr, so it generates nothing until filled
config addtype
option type 'DOMAIN-SUFFIX'
option pgroup 'DIRECT'
option ipaaddr ''
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,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,38 +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
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,347 +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 extra_resolve http_code rc
url="$1"
tmp="$2"
hdr="$3"
err="$4"
ua="$5"
extra_resolve="$6"
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
# shellcheck disable=SC2086
http_code="$(curl -sSL --connect-timeout 15 --max-time 60 \
--speed-time 30 --speed-limit 1 --retry 2 \
-H "User-Agent: ${ua}" -D "$hdr" -o "$tmp" \
$extra_resolve \
-w '%{http_code}' "$url" 2>"$err")"
rc=$?
printf '%s\n' "$http_code"
return "$rc"
}
download_subscription() {
local url target tmp hdr err rc http_code info_line ua err_msg
local host ip dns extra
url="$1"
target="$2"
tmp="${TMP_PREFIX}.yaml"
hdr="${TMP_PREFIX}.hdr"
err="${TMP_PREFIX}.err"
ua="$(uci -q get clashoo.config.sub_ua 2>/dev/null)"
[ -n "$ua" ] || ua="$DEFAULT_SUB_UA"
if command -v curl >/dev/null 2>&1; then
http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "")"
rc=$?
if [ "$rc" -ne 0 ] || [ "$http_code" = "000" ]; then
host="$(extract_host "$url")"
if [ -n "$host" ]; then
for dns in 223.5.5.5 119.29.29.29 1.1.1.1 8.8.8.8; do
ip="$(resolve_via "$host" "$dns")"
[ -n "$ip" ] || continue
log_update "DNS 回退:${host} -> ${ip} (@${dns})"
extra="--resolve ${host}:443:${ip} --resolve ${host}:80:${ip}"
http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "$extra")"
rc=$?
[ "$rc" -eq 0 ] && [ "$http_code" = "200" ] && break
done
fi
fi
else
wget -q --tries=4 --timeout=20 \
--user-agent="$ua" "$url" -O "$tmp" 2>"$err"
rc=$?
http_code=""
fi
if [ "$rc" -ne 0 ]; then
err_msg="$(grep -a 'curl:' "$err" 2>/dev/null | tail -1)"
[ -z "$err_msg" ] && err_msg="$(tail -1 "$err" 2>/dev/null)"
log_update "下载失败:$(basename "$target") rc=${rc} ${err_msg}"
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
return 1
fi
if [ -n "$http_code" ] && [ "$http_code" != "200" ]; then
log_update "下载失败:$(basename "$target") HTTP ${http_code}"
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
return 1
fi
if ! grep -Eq '^(proxies|proxy-providers):' "$tmp" 2>/dev/null; then
log_update "校验失败:$(basename "$target") 内容不含 proxies/proxy-providers"
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
return 1
fi
info_line="$(grep -i 'subscription-userinfo:' "$hdr" 2>/dev/null | head -1 | \
sed 's/^[Ss]ubscription-[Uu]serinfo:[[:space:]]*//' | tr -d '\r')"
[ -n "$info_line" ] && printf '%s\n' "$info_line" > "${target}.info" || \
rm -f "${target}.info" >/dev/null 2>&1
rm -f "$hdr" "$err" >/dev/null 2>&1
mv "$tmp" "$target" >/dev/null 2>&1 || {
rm -f "$tmp" >/dev/null 2>&1
return 1
}
printf '%s\n' "$ua" >"${target}.ua"
return 0
}
upsert_meta() {
local filename url typ tmpf
filename="$1"
url="$2"
typ="$3"
tmpf="${TMP_PREFIX}.list"
[ -f "$LIST_FILE" ] || touch "$LIST_FILE"
awk -F '#' -v n="$filename" '$1 != n { print $0 }' "$LIST_FILE" >"$tmpf"
printf '%s#%s#%s\n' "$filename" "$url" "$typ" >>"$tmpf"
mv "$tmpf" "$LIST_FILE"
}
cleanup_tmp() {
rm -f "${TMP_PREFIX}.yaml" "${TMP_PREFIX}.urls" "${TMP_PREFIX}.list" \
"${TMP_PREFIX}.hdr" "${TMP_PREFIX}.err" >/dev/null 2>&1
}
trap cleanup_tmp EXIT INT TERM
[ "$subtype" = "clash" ] || [ "$subtype" = "meta" ] || subtype="clash"
mkdir -p "$SUB_DIR" /usr/share/clashbackup >/dev/null 2>&1
[ -f "$LIST_FILE" ] || touch "$LIST_FILE"
URLS_FILE="${TMP_PREFIX}.urls"
get_subscription_urls | sed '/^[[:space:]]*$/d' | awk '!seen[$0]++' >"$URLS_FILE"
url_count="$(wc -l <"$URLS_FILE" 2>/dev/null | tr -d ' ')"
if [ -z "$url_count" ] || [ "$url_count" -eq 0 ]; then
log_update "未找到订阅链接"
log_text "No subscription URL found" "未找到订阅链接"
sleep 2
log_text "Clash for OpenWRT" "Clash for OpenWRT"
exit 1
fi
ensure_system_dns
log_update "开始下载订阅(共 ${url_count} 条)"
log_text "Downloading subscription..." "开始下载订阅..."
base_name="$(sanitize_custom_name "$config_name_raw")"
timestamp="$(date +%Y%m%d)"
success=0
failed=0
idx=0
first_file=""
while IFS= read -r url; do
[ -n "$url" ] || continue
idx=$((idx + 1))
if [ -n "$base_name" ]; then
if [ "$url_count" -gt 1 ]; then
name_candidate="$(sanitize_name "${base_name}-${idx}")"
file_base="$(next_available_name "$name_candidate")"
else
file_base="$base_name"
fi
else
name_candidate="$(url_to_name "$url")-${timestamp}"
if [ "$url_count" -gt 1 ]; then
name_candidate="${name_candidate}-${idx}"
fi
file_base="$(next_available_name "$name_candidate")"
fi
target_file="$SUB_DIR/${file_base}.yaml"
if download_subscription "$url" "$target_file"; then
upsert_meta "${file_base}.yaml" "$url" "$subtype"
log_update "订阅下载成功:${file_base}.yaml"
[ -n "$first_file" ] || first_file="$target_file"
success=$((success + 1))
else
log_update "订阅下载失败:${file_base}.yaml"
failed=$((failed + 1))
fi
done <"$URLS_FILE"
if [ "$success" -gt 0 ]; then
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
if [ -z "$use_config" ] || [ ! -f "$use_config" ]; then
uci set clashoo.config.use_config="$first_file"
uci set clashoo.config.config_type='1'
uci commit clashoo
fi
log_text "Subscription download completed: ${success} success, ${failed} failed" "订阅下载完成:成功 ${success} 个,失败 ${failed} 个"
log_update "订阅下载完成:成功 ${success} 个,失败 ${failed} 个"
ret=0
else
log_text "All subscription downloads failed" "订阅下载失败"
log_update "订阅下载失败:全部链接失败"
ret=1
fi
sleep 2
log_text "Clash for OpenWRT" "Clash for OpenWRT"
exit "$ret"
@@ -1,451 +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" current="${1}.dns-leak-current" tmp="${1}.$$"
[ -f "$cfg" ] || return 0
command -v yq >/dev/null 2>&1 || return 0
if [ "$enabled" = "1" ]; then
yq e -r '.dns.fallback[]' "$cfg" > "$selected" 2>/dev/null || true
if [ ! -s "$selected" ]; then
printf '%s\n' 'https://1.1.1.1/dns-query' 'https://8.8.8.8/dns-query' > "$selected"
fi
yq e -r '.dns.nameserver[]' "$cfg" > "$current" 2>/dev/null || true
if [ ! -f "$state" ] ||
! grep -q 'clashoo:dns_leak_protect_respect_rules' "$cfg" ||
! cmp -s "$current" "$selected"; then
awk '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
/^dns:[[:space:]]*$/ { in_dns = 1; dns_indent = indent_len($0); next }
in_dns && /^[^[:space:]#][^:]*:/ { exit }
in_dns && /^[[:space:]]*nameserver:/ {
print
rest = $0
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
sub(/[[:space:]]*(#.*)?$/, "", rest)
if (rest == "") { in_ns = 1; ns_indent = indent_len($0) }
next
}
in_ns {
cur_indent = indent_len($0)
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) {
print
next
}
exit
}
' "$cfg" > "$state"
[ -s "$state" ] || printf '__ABSENT__\n' > "$state"
fi
awk -v selected="$selected" '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
function spaces(n, out,i) {
out = ""
for (i = 0; i < n; i++) out = out " "
return out
}
function print_selected(sp, line) {
print sp "nameserver:"
while ((getline line < selected) > 0)
if (line != "") print sp " - " line
close(selected)
inserted = 1
}
/^dns:[[:space:]]*$/ {
in_dns = 1
dns_indent = indent_len($0)
child_indent = -1
print
next
}
in_dns && /^[^[:space:]#][^:]*:/ {
if (!inserted) print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
in_dns = 0
print
next
}
in_dns && /^[[:space:]]*[^#[:space:]][^:]*:/ && child_indent < 0 {
child_indent = indent_len($0)
}
in_dns && /^[[:space:]]*nameserver:/ {
ns_indent = indent_len($0)
rest = $0
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
sub(/[[:space:]]*(#.*)?$/, "", rest)
print_selected(spaces(ns_indent))
if (rest == "") skip_ns = 1
next
}
skip_ns {
cur_indent = indent_len($0)
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next
skip_ns = 0
}
{ print }
END {
if (in_dns && !inserted)
print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
}
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
elif [ -f "$state" ]; then
awk -v state="$state" '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
function print_original( line) {
if ((getline line < state) > 0 && line != "__ABSENT__") {
print line
while ((getline line < state) > 0) print line
}
close(state)
restored = 1
}
/^dns:[[:space:]]*$/ { in_dns = 1; print; next }
in_dns && /^[^[:space:]#][^:]*:/ {
if (!restored) print_original()
in_dns = 0
print
next
}
in_dns && /^[[:space:]]*nameserver:/ {
ns_indent = indent_len($0)
rest = $0
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
sub(/[[:space:]]*(#.*)?$/, "", rest)
print_original()
if (rest == "") skip_ns = 1
next
}
skip_ns {
cur_indent = indent_len($0)
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next
skip_ns = 0
}
{ print }
END { if (in_dns && !restored) print_original() }
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
rm -f "$state"
fi
rm -f "$selected" "$current" "$tmp" 2>/dev/null
}
# handle mihomo dns block:
# - strip previous clashoo:dns_leak_protect injection markers (incl. the old
# geosite:gfw block written by older versions)
# - rewrite the ipv6: line from ipv6_value (skip when empty)
# No longer injects fallback-filter.geosite:gfw — mihomo deprecated it and a
# GeoSite.dat missing the gfw category froze startup (issue #25). See flush_dns.
# (The fallback-filter parsing vars below are now vestigial; clean up separately.)
dns_mihomo_apply_leak_dns_block() {
local cfg="$1" enabled="${2:-0}" ipv6_value="${3:-}" tmp="${1}.$$"
[ -f "$cfg" ] || return 0
awk -v enabled="$enabled" -v ipv6_value="$ipv6_value" '
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
function spaces(n, out,i) {
out = ""
for (i = 0; i < n; i++)
out = out " "
return out
}
function flush_dns( i, sp, ff_sp) {
if (!in_dns)
return
if (in_ff) ff_end = dns_n
sp = spaces(child_indent > 0 ? child_indent : dns_indent + 2)
ff_sp = (ff_child_indent > 0 ? spaces(ff_child_indent) : sp " ")
print dns_lines[1]
if (ipv6_value != "")
print sp "ipv6: " ipv6_value
# NOTE: clashoo used to inject `fallback-filter.geosite: [gfw]` here, but
# mihomo deprecated fallback-filter.geosite ("replace with nameserver-policy,
# will be removed") AND it hard-fails preflight + freezes startup (~90s) when
# the downloaded GeoSite.dat lacks the gfw category (issue #25). Stop
# injecting it. DNS-leak protection still works via respect-rules / leak
# nameservers / DST-PORT,853,REJECT handled elsewhere. The marker strip below
# still removes any gfw block injected by older versions, so disabling /
# upgrading cleans it up.
for (i = 2; i <= dns_n; i++)
print dns_lines[i]
in_dns = 0
dns_n = 0
}
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect[[:space:]]*$/ {
in_marker = 1
next
}
in_marker {
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect[[:space:]]*$/)
in_marker = 0
next
}
/^dns:[[:space:]]*$/ {
flush_dns()
in_dns = 1
dns_indent = indent_len($0)
child_indent = -1
dns_n = 1
dns_lines[dns_n] = $0
has_ff = 0
in_ff = 0
in_ff_geosite = 0
ff_indent = -1
ff_child_indent = -1
ff_geosite_indent = -1
ff_has_gfw = 0
ff_end = -1
next
}
in_dns {
cur_indent = indent_len($0)
if ($0 ~ /^[^[:space:]#][^:]*:/ && cur_indent <= dns_indent) {
flush_dns()
print
next
}
if ($0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && cur_indent > dns_indent && child_indent < 0)
child_indent = cur_indent
if (in_ff && cur_indent <= ff_indent) {
ff_end = dns_n
in_ff = 0
in_ff_geosite = 0
}
if (in_ff_geosite && cur_indent <= ff_geosite_indent)
in_ff_geosite = 0
if ($0 ~ /^[[:space:]]*fallback-filter:[[:space:]]*$/) {
has_ff = 1
in_ff = 1
ff_indent = cur_indent
ff_child_indent = -1
} else if (in_ff && $0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && ff_child_indent < 0) {
ff_child_indent = cur_indent
}
if (in_ff && $0 ~ /^[[:space:]]*geosite:[[:space:]]*$/) {
in_ff_geosite = 1
ff_geosite_indent = cur_indent
} else if (in_ff_geosite && $0 ~ /^[[:space:]]*-[[:space:]]*gfw([[:space:]]*(#.*)?)?$/) {
ff_has_gfw = 1
}
if ($0 ~ /^[[:space:]]*ipv6:[[:space:]]*/)
next
dns_lines[++dns_n] = $0
next
}
{ print }
END { flush_dns() }
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
rm -f "$tmp" 2>/dev/null
}
# handle DST-PORT,853,REJECT injection into rules block
# only remove Clashoo marker block, never user original rules
dns_mihomo_apply_leak_rule() {
local cfg="$1" enabled="${2:-0}" tmp="${1}.$$" has_user_853
[ -f "$cfg" ] || return 0
has_user_853=$(awk '
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/ {
in_marker = 1
next
}
in_marker {
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/)
in_marker = 0
next
}
/^[[:space:]]*-[[:space:]]*DST-PORT,853,REJECT([[:space:]]*(#.*)?)?$/ { found = 1 }
END { print found ? 1 : 0 }
' "$cfg")
awk -v enabled="$enabled" -v has_user_853="$has_user_853" '
BEGIN { inserted = 0; saw_rules = 0 }
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/ {
in_marker = 1
next
}
in_marker {
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/)
in_marker = 0
next
}
/^rules:[[:space:]]*$/ {
saw_rules = 1
print
if (enabled == "1" && has_user_853 != "1") {
print " # >>> clashoo:dns_leak_protect_rule"
print " - DST-PORT,853,REJECT"
print " # <<< clashoo:dns_leak_protect_rule"
inserted = 1
}
next
}
{ print }
END {
if (enabled == "1" && has_user_853 != "1" && !inserted && !saw_rules) {
print ""
print "rules:"
print " # >>> clashoo:dns_leak_protect_rule"
print " - DST-PORT,853,REJECT"
print " # <<< clashoo:dns_leak_protect_rule"
}
}
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
rm -f "$tmp" 2>/dev/null
}
dns_mihomo_apply_leak_protect() {
local cfg="$1" enabled="${2:-0}" ipv6_value="${3:-}"
[ -f "$cfg" ] || return 0
dns_mihomo_apply_leak_nameservers "$cfg" "$enabled"
dns_mihomo_apply_respect_rules "$cfg" "$enabled"
dns_mihomo_apply_leak_dns_block "$cfg" "$enabled" "$ipv6_value"
dns_mihomo_apply_leak_rule "$cfg" "$enabled"
}
@@ -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,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="/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,317 +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) {
if (f == 'geosite:cn') { f = 'rule-set:cn_domain'; need_cn_rs = true; }
else if (f == 'rule-set:cn_domain') need_cn_rs = true;
push(cfg['dns']['fake-ip-filter'], f);
};
let filters = a('fake_ip_filter');
if (type(filters) == 'array') { for (let f in filters) push_filter(f); }
else if (filters != null) push_filter(filters);
}
/* fallback-filter(默认 geoip:false,防止冷启动依赖 MMDB) */
cfg['dns']['fallback-filter'] = { geoip: ab('fallback_filter_geoip') };
let dns_present = {};
for (let f in split(s(getenv('CLASHOO_DNS_PRESENT'), ''), ','))
if (length(trim(f))) dns_present[trim(f)] = true;
let dns_force = {};
for (let f in split(s(getenv('CLASHOO_DNS_FORCE'), ''), ','))
if (length(trim(f))) dns_force[trim(f)] = true;
let dns_inherit_mode = s(getenv('CLASHOO_DNS_INHERIT_MODE'), '');
let inherited_nameserver = json(s(getenv('CLASHOO_DNS_INHERIT_NAMESERVER'), '[]'));
if (type(inherited_nameserver) != 'array') {
if (inherited_nameserver != null && inherited_nameserver != '')
inherited_nameserver = [inherited_nameserver];
else
inherited_nameserver = [];
}
let dns_role_fields = ['nameserver', 'proxy-server-nameserver', 'direct-nameserver',
'default-nameserver', 'nameserver-policy', 'respect-rules'];
let had_user_dns_roles = false;
for (let f in dns_role_fields)
if (dns_present[f]) had_user_dns_roles = true;
if (length(keys(dns_force)) || !had_user_dns_roles)
cfg['dns']['x-clashoo-managed-dns'] = true;
function dns_scheme(protocol) {
switch (trim(protocol || '')) {
case '':
case 'none': return '';
case 'udp': case 'udp://': return 'udp://';
case 'tcp': case 'tcp://': return 'tcp://';
case 'dot': case 'tls': case 'tls://': return 'tls://';
case 'doh': case 'https': case 'https://': return 'https://';
case 'doq': case 'quic': case 'quic://': return 'quic://';
default: return protocol;
}
}
function dns_server(address, protocol, port) {
let addr = trim(address || '');
if (!length(addr)) return null;
if (match(addr, /^[A-Za-z][A-Za-z0-9+.-]*:\/\//)) return addr;
let prefix = dns_scheme(protocol);
return length(trim(port || '')) ? prefix + addr + ':' + trim(port) : prefix + addr;
}
let dns_roles = {};
uci.foreach('clashoo', 'dnsservers', function(sec) {
if (b(sec.enabled) == false) return;
let role = s(sec.ser_type, 'nameserver');
if (role == 'fallback' && cfg['dns']['enhanced-mode'] == 'fake-ip') return;
let srv = dns_server(sec.ser_address, sec.protocol, sec.ser_port);
if (!srv) return;
if (!dns_roles[role]) dns_roles[role] = [];
push(dns_roles[role], srv);
});
for (let role in keys(dns_roles)) {
if (role == 'proxy-server-nameserver' && (dns_inherit_mode == 'inherit' || dns_inherit_mode == 'skip') &&
!dns_present[role] && !dns_force[role]) continue;
if (!dns_present[role] || dns_force[role]) cfg['dns'][role] = dns_roles[role];
}
if (dns_inherit_mode == 'inherit' && length(inherited_nameserver) &&
!dns_present['proxy-server-nameserver'] && !dns_force['proxy-server-nameserver'])
cfg['dns']['proxy-server-nameserver'] = inherited_nameserver;
let bootstrap = a('default_nameserver');
if (bootstrap == null) bootstrap = a('defaul_nameserver');
if (type(bootstrap) != 'array') bootstrap = bootstrap != null ? [bootstrap] : [];
if (length(bootstrap) && (!dns_present['default-nameserver'] || dns_force['default-nameserver']))
cfg['dns']['default-nameserver'] = bootstrap;
let policies = {};
uci.foreach('clashoo', 'dns_policy', function(sec) {
if (b(sec.enabled) == false) return;
if (s(sec.policy_type, 'nameserver-policy') != 'nameserver-policy') return;
let matcher = trim(sec.matcher || '');
let servers = sec.nameserver;
if (type(servers) != 'array') servers = servers != null ? [servers] : [];
if (!length(matcher) || !length(servers)) return;
if (matcher == 'geosite:cn') { matcher = 'rule-set:cn_domain'; need_cn_rs = true; }
policies[matcher] = servers;
});
if (length(keys(policies)) && (!dns_present['nameserver-policy'] || dns_force['nameserver-policy']))
cfg['dns']['nameserver-policy'] = policies;
let respect_rules = a('dns_respect_rules') == null ? true : ab('dns_respect_rules');
if (dns_force['respect-rules'])
cfg['dns']['respect-rules'] = respect_rules;
else if (respect_rules && length(cfg['dns']['proxy-server-nameserver'] || [])
&& !dns_present['respect-rules'] && !dns_present['prefer-h3'])
cfg['dns']['respect-rules'] = true;
/* profile */
let store_selected = ab('selection_cache');
/* fake-ip cache defaults ON to survive restarts; opt out with '0' */
let store_fake = a('fake_ip_cache') == null ? true : ab('fake_ip_cache');
if (store_selected || store_fake) {
cfg['profile'] = {};
if (store_selected) cfg['profile']['store-selected'] = true;
if (store_fake) cfg['profile']['store-fake-ip'] = true;
}
/* bundled cn.mrs rule-provider, referenced by fake-ip-filter rule-set:cn_domain */
if (need_cn_rs) {
if (!cfg['rule-providers']) cfg['rule-providers'] = {};
cfg['rule-providers']['cn_domain'] = {
type: 'file',
path: './ruleset/cn.mrs',
format: 'mrs',
behavior: 'domain',
};
}
/* ── authentication ────────────────────────────────── */
if (ab('authentication')) {
cfg['authentication'] = [];
uci.foreach('clashoo', 'authentication', function(sec) {
if (b(sec.enabled) == false) return;
let user = trim(sec.username || '');
let pass = trim(sec.password || '');
if (user) push(cfg['authentication'], user + ':' + pass);
});
}
/* ── hosts ────────────────────────────────────────── */
cfg['hosts'] = {};
uci.foreach('clashoo', 'hosts', function(sec) {
if (b(sec.enabled) == false) return;
let domain = trim(sec.adress || '');
let ip = trim(sec.ip || '');
if (domain && ip) cfg['hosts'][domain] = ip;
});
if (length(keys(cfg['hosts'])) == 0) delete cfg['hosts'];
/* ── sniffer ──────────────────────────────────────── */
if (ab('sniffer_streaming') && !length(s(getenv('CLASHOO_KEEP_SNIFFER'), ''))) {
cfg['sniffer'] = {
enable: true,
'force-dns-mapping': true,
'parse-pure-ip': true,
sniff: {
HTTP: { ports: [80, 8080], 'override-destination': true },
TLS: { ports: [443, 8443], 'override-destination': true },
QUIC: { ports: [443, 8443], 'override-destination': true },
},
'force-domain': [
'+.youtube.com',
'+.googlevideo.com',
'+.netflix.com',
'+.nflxvideo.net',
'+.disneyplus.com',
'+.hulu.com',
'+.hbomax.com',
],
'skip-domain': [
'+.lan',
'+.local',
],
};
}
/* ── Smart kernel injection ───────────────────────── */
if (ab('smart_auto_switch')) {
/* Smart 策略注入:通过 proxy-groups 覆盖实现 */
}
/* ── ECS ──────────────────────────────────────────── */
let ecs = s(a('dns_ecs'));
if (ecs) cfg['dns']['edns-client-subnet'] = ecs;
/* ── cache_file / experimental ──────────────────── */
cfg['experimental'] = {
'cache_file': { enabled: true },
'clash_api': {
'external-controller': '0.0.0.0:' + i(a('dash_port'), 9090),
'external-ui': './dashboard',
'secret': s(a('dash_pass'), ''),
},
};
/* ── 输出 JSON (不含 null/空对象) ─────────────────── */
function clean(obj) {
if (type(obj) == 'array') {
let r = [];
for (let e in obj) {
e = clean(e);
if (e != null) push(r, e);
}
return length(r) ? r : null;
}
if (type(obj) == 'object') {
let r = {};
for (let k in keys(obj)) {
let v = clean(obj[k]);
if (v != null) r[k] = v;
}
return length(keys(r)) ? r : null;
}
return obj;
}
print(clean(cfg));
@@ -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,31 +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() {
local data
if [ "$#" != 1 ]; then
return 1
fi
data=$(curl -s -o /dev/null -w %{url_effective} --get --data-urlencode "$1" "")
if [ ! -z "$data" ]; then
echo "${data##/?}"
fi
return 0
}
cat $HISTORY_PATH |while read line
do
if [ -z "$(echo $line |grep "#*#")" ]; then
continue
else
GORUP_NAME=$(urlencode "$(echo $line |awk -F '#*#' '{print $1}')")
NOW_NAME=$(echo $line |awk -F '#*#' '{print $3}')
curl -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X PUT -d '{"name":"'"$NOW_NAME"'"}' http://"$LAN_IP":"$PORT"/proxies/"$GORUP_NAME" >/dev/null 2>&1
fi
done >/dev/null 2>&1
curl -m 5 --retry 2 -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X DELETE http://"$LAN_IP":"$PORT"/connections >/dev/null 2>&1
fi
@@ -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,642 +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" --tries=1 "$url"; return $?; }
wget -qO- --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"
return $?
fi
return 127
}
download_file() {
url="$1"
out="$2"
proxy="$(detect_proxy)"
if command -v curl >/dev/null 2>&1; then
curl -fL --connect-timeout "$CONNECT_TIMEOUT" --speed-time "$LOW_SPEED_TIME" --speed-limit "$LOW_SPEED_LIMIT" ${proxy:+--proxy "$proxy"} "$url" -o "$out"
return $?
fi
if [ -n "$proxy" ]; then
http_proxy="$proxy" https_proxy="$proxy" wget -qO "$out" --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"
return $?
fi
wget -qO "$out" --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"
}
download_url() {
url="$1"
case "$url" in
https://github.com/*)
printf '%s%s\n' "$GITHUB_PROXY_PREFIX" "$url"
;;
*)
printf '%s\n' "$url"
;;
esac
}
root_free_kb() {
df -k / 2>/dev/null | awk 'NR==2 {print $4}'
}
kb_to_mib() {
awk "BEGIN {printf \"%.0f\", (${1:-0}) / 1024}"
}
ensure_root_space() {
need_kb="$1"
free_kb="$(root_free_kb)"
[ -n "$free_kb" ] || return 0
[ "$free_kb" -ge "$need_kb" ] && return 0
finish 1 "根分区空间不足:可用 $(kb_to_mib "$free_kb") MiB,至少需要 $(kb_to_mib "$need_kb") MiB"
}
log_install_error() {
file="$1"
[ -s "$file" ] || return 0
tail -8 "$file" 2>/dev/null | while IFS= read -r line; do
[ -n "$line" ] && log "$line"
done
}
detect_manager() {
if command -v opkg >/dev/null 2>&1; then
echo opkg
return
fi
if command -v apk >/dev/null 2>&1; then
echo apk
return
fi
echo unsupported
}
installed_package_version() {
pkg="$1"
if command -v opkg >/dev/null 2>&1; then
opkg status "$pkg" 2>/dev/null | awk -F': ' '/^Version:/{print $2; exit}'
return
fi
if command -v apk >/dev/null 2>&1; then
apk list -I "$pkg" 2>/dev/null | awk '{print $1; exit}' | sed -n "s/^${pkg}-//p"
fi
}
package_installed() {
pkg="$1"
if command -v opkg >/dev/null 2>&1; then
opkg status "$pkg" 2>/dev/null | awk '
$1 == "Status:" && $2 == "install" && $NF == "installed" { found = 1 }
END { exit !found }
'
return
fi
apk info -e "$pkg" >/dev/null 2>&1
}
apk_install() {
proxy="$(detect_proxy)"
if [ -n "$proxy" ]; then
http_proxy="$proxy" https_proxy="$proxy" apk add --allow-untrusted --timeout 30 "$@"
else
apk add --allow-untrusted --timeout 30 "$@"
fi
}
apk_bundle_install() {
printf '%s\n' "$$" >"$APK_UPGRADE_FLAG"
apk_install "$@"
rc=$?
rm -f "$APK_UPGRADE_FLAG"
return "$rc"
}
verify_downloaded_package() {
file="$1"
[ -s "$file" ] || return 1
if [ "$PM" = "apk" ]; then
apk verify --allow-untrusted "$file" >/dev/null 2>&1
return
fi
gzip -t "$file" >/dev/null 2>&1
}
download_package_file() {
url="$1"
out="$2"
name="$3"
download_file "$(download_url "$url")" "$out" || finish 1 "下载 ${name} 失败"
verify_downloaded_package "$out" || finish 1 "${name} 文件校验失败"
}
detect_arch() {
pm="$1"
if [ "$pm" = "opkg" ]; then
opkg print-architecture 2>/dev/null | awk '/^arch / {print $2}' | tail -n 1
return
fi
# apk: DISTRIB_ARCH is authoritative (e.g. aarch64_cortex-a53); apk --print-arch
# only gives the generic "aarch64" which won't match the .apk package suffix.
if [ -r /etc/openwrt_release ]; then
a="$(sed -n "s/^DISTRIB_ARCH=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)"
[ -n "$a" ] && { printf '%s\n' "$a"; return; }
fi
apk --print-arch 2>/dev/null
}
detect_sdk() {
[ -r /etc/openwrt_release ] || return 1
release="$(sed -n "s/^DISTRIB_RELEASE=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)"
[ -n "$release" ] || return 1
printf '%s\n' "$release" | grep -Eo '[0-9]+\.[0-9]+' | head -n 1
}
append_unique_word() {
value="$1"
list="$2"
[ -n "$value" ] || {
printf '%s\n' "$list"
return
}
case " $list " in
*" $value "*) ;;
*) list="${list}${list:+ }${value}" ;;
esac
printf '%s\n' "$list"
}
build_sdk_candidates() {
pm="$1"
candidates=""
detected_sdk="$(detect_sdk || true)"
candidates="$(append_unique_word "$detected_sdk" "$candidates")"
if [ "$pm" = "opkg" ]; then
for sdk in 24.10 23.05 22.03 21.02; do
candidates="$(append_unique_word "$sdk" "$candidates")"
done
else
for sdk in 25.12 24.10; do
candidates="$(append_unique_word "$sdk" "$candidates")"
done
fi
printf '%s\n' "$candidates"
}
find_manifest_value() {
key="$1"
manifest_text="$2"
printf '%s\n' "$manifest_text" | sed -n "s/^${key}=//p" | head -n 1
}
load_manifest_urls() {
sdk="$1"
arch="$2"
manifest_url="${FEED_BASE_URL}/${sdk}/${arch}/manifest-clashoo.txt"
manifest_text="$(fetch_text "$manifest_url" || true)"
[ -n "$manifest_text" ] || return 1
core_file="$(find_manifest_value "core" "$manifest_text")"
luci_file="$(find_manifest_value "luci" "$manifest_text")"
i18n_file="$(find_manifest_value "i18n" "$manifest_text")"
[ -n "$core_file" ] || return 1
[ -n "$luci_file" ] || return 1
case "$core_file" in *."$EXT") ;; *) return 1 ;; esac
case "$luci_file" in *."$EXT") ;; *) return 1 ;; esac
case "$i18n_file" in ""|*."$EXT") ;; *) return 1 ;; esac
CORE_URL="${FEED_BASE_URL}/${sdk}/${arch}/${core_file}"
LUCI_URL="${FEED_BASE_URL}/${sdk}/${arch}/${luci_file}"
I18N_URL=""
[ -n "$i18n_file" ] && I18N_URL="${FEED_BASE_URL}/${sdk}/${arch}/${i18n_file}"
SOURCE_LABEL="R2 feed ${sdk}/${arch}"
return 0
}
load_github_urls() {
arch="$1"
ext="$2"
payload="$(fetch_text "$GITHUB_API_URL" || true)"
[ -n "$payload" ] || return 1
urls="$(printf '%s\n' "$payload" | sed -n 's/.*"browser_download_url":[[:space:]]*"\([^"]*\)".*/\1/p')"
[ -n "$urls" ] || return 1
if [ "$ext" = "apk" ]; then
CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo-[^-]+.*-r[0-9]+-'"$arch"'\.apk$' | head -n 1)"
LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)"
I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)"
else
CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo_.*_'"$arch"'\.ipk$' | head -n 1)"
LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo_.*_all\.ipk$' | head -n 1)"
I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn_.*_all\.ipk$' | head -n 1)"
fi
[ -n "$CORE_URL" ] || return 1
[ -n "$LUCI_URL" ] || return 1
SOURCE_LABEL="GitHub latest"
return 0
}
package_version_from_url() {
file="${1##*/}"
case "$file" in
clashoo_*_"$ARCH".ipk)
v="${file#clashoo_}"
v="${v%_${ARCH}.ipk}"
printf '%s\n' "$v" | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/'
;;
clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^clashoo_\(.*\)_[^_][^_]*\.ipk$/\1/p' | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/' ;;
luci-app-clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^luci-app-clashoo_\(.*\)_all\.ipk$/\1/p' ;;
luci-i18n-clashoo-zh-cn_*.ipk) printf '%s\n' "$file" | sed -n 's/^luci-i18n-clashoo-zh-cn_\(.*\)_all\.ipk$/\1/p' ;;
clashoo-*.apk)
v="$(printf '%s\n' "$file" | sed -n -e 's/^clashoo-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^clashoo-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1)"
printf '%s\n' "$v" | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/'
;;
luci-app-clashoo-*.apk) printf '%s\n' "$file" | sed -n -e 's/^luci-app-clashoo-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^luci-app-clashoo-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1 ;;
luci-i18n-clashoo-zh-cn-*.apk) printf '%s\n' "$file" | sed -n -e 's/^luci-i18n-clashoo-zh-cn-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^luci-i18n-clashoo-zh-cn-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1 ;;
*) printf '%s\n' "$file" ;;
esac
}
resolve_bundle_urls() {
PM="$(detect_manager)"
[ "$PM" != "unsupported" ] || return 1
ARCH="$(detect_arch "$PM")"
[ -n "$ARCH" ] || return 1
EXT="ipk"
[ "$PM" = "apk" ] && EXT="apk"
CORE_URL=""
LUCI_URL=""
I18N_URL=""
SOURCE_LABEL=""
SDK_CANDIDATES="$(build_sdk_candidates "$PM")"
for sdk in $SDK_CANDIDATES; do
if load_manifest_urls "$sdk" "$ARCH"; then
return 0
fi
done
load_github_urls "$ARCH" "$EXT"
}
backup_config() {
ts="$(date '+%Y%m%d-%H%M%S')"
BACKUP_DIR="/etc/clashoo/backup/component-upgrade-${ts}"
mkdir -p "$BACKUP_DIR/etc-config" "$BACKUP_DIR/etc-clashoo" >/dev/null 2>&1 || return 1
[ -r /etc/config/clashoo ] && cp -a /etc/config/clashoo "$BACKUP_DIR/etc-config/clashoo"
# 只备份用户配置;Model.bin 等可重新下载的大文件不入备份,避免每次更新
# 复制 ~5MB 的同一模型,把备份目录撑大、堆满本就紧张的根分区。
for path in \
/etc/clashoo/config.yaml \
/etc/clashoo/config.json \
/etc/clashoo/*.yaml \
/etc/clashoo/*.yml \
/etc/clashoo/*.json
do
[ -e "$path" ] || continue
cp -a "$path" "$BACKUP_DIR/etc-clashoo/" 2>/dev/null || true
done
log "已备份配置:${BACKUP_DIR}"
# 只保留最近 1 个备份,删除更早的,避免无限堆积占满磁盘。
ls -dt /etc/clashoo/backup/component-upgrade-* 2>/dev/null | tail -n +2 | while read -r _old; do
[ -n "$_old" ] && rm -rf "$_old" 2>/dev/null
done
}
restore_config_backup() {
[ -n "${BACKUP_DIR:-}" ] || return 0
[ -r "$BACKUP_DIR/etc-config/clashoo" ] && cp -a "$BACKUP_DIR/etc-config/clashoo" /etc/config/clashoo
if [ -d "$BACKUP_DIR/etc-clashoo" ]; then
mkdir -p /etc/clashoo
cp -a "$BACKUP_DIR/etc-clashoo/." /etc/clashoo/ 2>/dev/null || true
fi
uci commit clashoo >/dev/null 2>&1 || true
}
clashoo_was_running() {
/etc/init.d/clashoo status >/dev/null 2>&1 && return 0
return 1
}
restart_web_stack() {
# reload (not restart) rpcd so the LuCI login session survives a plugin
# update — rpcd restart drops every ubus session and forces a re-login
# (issue #12). reload still picks up the new ucode RPC backend. uhttpd needs
# nothing: updated static assets are served on the next request.
/etc/init.d/rpcd reload >/dev/null 2>&1 || true
}
# which: clashoo(仅核心)/ luci(luci-app + 语言包)。两者拆开,clashoo
# 核心更新频率高于 LuCI,分别更新便于定位失败。
run_pkg_update() {
which="$1"
log "正在检查组件包"
if ! resolve_bundle_urls; then
finish 1 "未找到适配当前设备的组件包"
fi
log "更新来源:${SOURCE_LABEL}"
core_ver="$(package_version_from_url "$CORE_URL")"
luci_ver="$(package_version_from_url "$LUCI_URL")"
i18n_ver=""
[ -z "$I18N_URL" ] || i18n_ver="$(package_version_from_url "$I18N_URL")"
core_ver_before="$(installed_package_version clashoo)"
apk_core_ready() {
package_installed clashoo && [ "$(installed_package_version clashoo)" = "$core_ver" ] \
&& [ -x /etc/init.d/clashoo ] && [ -x /usr/share/clashoo/update/component_update.sh ]
}
apk_luci_ready() {
package_installed luci-app-clashoo && [ "$(installed_package_version luci-app-clashoo)" = "$luci_ver" ] \
&& { [ -z "$I18N_URL" ] || { package_installed luci-i18n-clashoo-zh-cn \
&& [ "$(installed_package_version luci-i18n-clashoo-zh-cn)" = "$i18n_ver" ]; }; } \
&& [ -r /usr/share/luci/menu.d/luci-app-clashoo.json ] \
&& [ -r /usr/share/rpcd/ucode/luci.clashoo ]
}
apk_bundle_ready() {
apk_core_ready && apk_luci_ready
}
apk_update_ready() {
if [ "$which" = "clashoo" ]; then
apk_bundle_ready
else
package_installed clashoo && apk_luci_ready
fi
}
if [ "$which" = "clashoo" ]; then
log "目标版本:Clashoo 核心 ${core_ver}"
cur_ver="$(installed_package_version clashoo)"
if [ "$PM" = "apk" ] && apk_update_ready; then
finish 0 "Clashoo 核心已是最新版本"
elif [ "$PM" != "apk" ] && [ -n "$core_ver" ] && [ "$cur_ver" = "$core_ver" ]; then
finish 0 "Clashoo 核心已是最新版本"
fi
else
log "目标版本:客户端 ${luci_ver}"
cur_ver="$(installed_package_version luci-app-clashoo)"
if [ "$PM" = "apk" ] && apk_update_ready; then
finish 0 "客户端已是最新版本"
elif [ "$PM" != "apk" ] && [ -n "$luci_ver" ] && [ "$cur_ver" = "$luci_ver" ]; then
finish 0 "客户端已是最新版本"
fi
fi
rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR" || finish 1 "创建临时目录失败"
was_running=0
clashoo_was_running && was_running=1
backup_config || finish 1 "备份配置失败"
apk_core_file=""
if [ "$PM" = "apk" ]; then
log "正在下载组件包"
if [ "$which" = "clashoo" ] || ! package_installed clashoo; then
ensure_root_space 98304
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
apk_core_file="$TMP_DIR/core.${EXT}"
fi
download_package_file "$LUCI_URL" "$TMP_DIR/luci.${EXT}" "luci-app-clashoo"
if [ -n "$I18N_URL" ]; then
download_package_file "$I18N_URL" "$TMP_DIR/i18n.${EXT}" "语言包"
fi
elif [ "$which" = "clashoo" ]; then
log "正在下载 Clashoo 核心"
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
else
log "正在下载客户端(LuCI + 语言包)"
download_package_file "$LUCI_URL" "$TMP_DIR/luci.${EXT}" "luci-app-clashoo"
if [ -n "$I18N_URL" ]; then
download_package_file "$I18N_URL" "$TMP_DIR/i18n.${EXT}" "语言包"
fi
fi
if [ "$PM" != "apk" ] && [ "$which" = "clashoo" ]; then
ensure_root_space 98304
fi
if [ "$which" = "clashoo" ]; then
log "正在安装 Clashoo 核心"
if [ "$PM" = "opkg" ]; then
opkg install --force-downgrade "$TMP_DIR/core.${EXT}" >"$TMP_DIR/install.log" 2>&1; rc=$?
else
apk_bundle_install ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
fi
else
log "正在安装客户端"
if [ "$PM" = "opkg" ]; then
opkg install --force-downgrade "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
else
apk_bundle_install ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
fi
fi
if [ "$rc" -ne 0 ]; then
if [ "$PM" = "apk" ] && apk_update_ready; then
log "apk 报告了其它软件包的错误,本次组件已升级到目标版本"
rc=0
else
log_install_error "$TMP_DIR/install.log"
log "安装失败,正在恢复配置备份"
restore_config_backup
finish "$rc" "组件更新失败,已停止操作,请查看组件更新日志"
fi
fi
validation_failed() {
log "安装校验失败,正在恢复配置备份"
restore_config_backup
finish 1 "组件安装校验失败,已停止操作,请查看组件更新日志"
}
if [ "$PM" = "apk" ]; then
if ! apk_update_ready; then
log "安装校验失败,正在安全重装组件包"
if [ -z "$apk_core_file" ] && ! package_installed clashoo; then
ensure_root_space 98304
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
apk_core_file="$TMP_DIR/core.${EXT}"
fi
apk_bundle_install --force-reinstall ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" \
${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >>"$TMP_DIR/install.log" 2>&1; repair_rc=$?
if [ "$repair_rc" -ne 0 ]; then
log_install_error "$TMP_DIR/install.log"
validation_failed
fi
apk_update_ready || validation_failed
fi
elif [ "$which" = "clashoo" ]; then
package_installed clashoo && [ "$(installed_package_version clashoo)" = "$core_ver" ] \
&& [ -x /etc/init.d/clashoo ] && [ -x /usr/share/clashoo/update/component_update.sh ] \
|| validation_failed
else
package_installed luci-app-clashoo && [ "$(installed_package_version luci-app-clashoo)" = "$luci_ver" ] \
&& [ -r /usr/share/luci/menu.d/luci-app-clashoo.json ] \
&& [ -r /usr/share/rpcd/ucode/luci.clashoo ] \
|| validation_failed
[ -z "$I18N_URL" ] || package_installed luci-i18n-clashoo-zh-cn || validation_failed
fi
# APK component updates install the LuCI bundle in the same transaction.
if [ "$which" = "luci" ] || [ "$PM" = "apk" ]; then
log "正在刷新 LuCI 服务"
restart_web_stack
fi
core_ver_after="$(installed_package_version clashoo)"
restart_clashoo=0
[ "$which" = "clashoo" ] && restart_clashoo=1
[ "$PM" = "apk" ] && [ "$core_ver_before" != "$core_ver_after" ] && restart_clashoo=1
if [ "$restart_clashoo" -eq 1 ] && [ "$was_running" -eq 1 ] && [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then
log "Clashoo 原本运行中,正在重启服务"
sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1 || /etc/init.d/clashoo restart >/dev/null 2>&1 || true
elif [ "$restart_clashoo" -eq 1 ]; then
log "Clashoo 原本未运行,保持停止状态"
fi
finish 0 "组件更新完成"
}
run_core_update() {
dcore="$1"
label="$2"
log "正在更新 ${label}"
# Pass the target as an arg so core_download refreshes only that binary
# without switching the active kernel. It restarts only if the target is the
# currently-running core (see core_download.sh finalize). Switching kernels
# stays a separate, explicit user action.
touch /var/run/core_update
sh /usr/share/clashoo/update/core_download.sh "$dcore" >>/tmp/clash_update.txt 2>&1
rc=$?
rm -f /var/run/core_update
[ "$rc" -eq 0 ] || finish "$rc" "${label} 更新失败"
finish 0 "${label} 更新完成"
}
run_lgbm_update() {
log "正在更新 Smart LightGBM 模型"
sh /usr/share/clashoo/update/lgbm_update.sh >/tmp/lgbm_update.log 2>&1
rc=$?
tail -5 /tmp/lgbm_update.log 2>/dev/null | while IFS= read -r line; do
[ -n "$line" ] && log "$line"
done
[ "$rc" -eq 0 ] || finish "$rc" "LightGBM 模型更新失败"
finish 0 "LightGBM 模型更新完成"
}
run_china_update() {
log "正在更新大陆白名单"
sh /usr/share/clashoo/update/update_china_ip.sh >>/tmp/clash_update.txt 2>&1
rc=$?
tail -6 /tmp/clash_update.txt 2>/dev/null | while IFS= read -r line; do
[ -n "$line" ] && printf '%s\n' "$line" | grep -q '白名单' && log "$line"
done
[ "$rc" -eq 0 ] || finish "$rc" "大陆白名单更新失败"
finish 0 "大陆白名单更新完成"
}
run_geoip_update() {
log "正在更新 GeoIP / GeoSite"
touch /var/run/geoip_update
sh /usr/share/clashoo/update/geoip.sh >/tmp/geoip_update.txt 2>&1
rc=$?
rm -f /var/run/geoip_update
tail -8 /tmp/geoip_update.txt 2>/dev/null | while IFS= read -r line; do
[ -n "$line" ] && log "$line"
done
[ "$rc" -eq 0 ] || finish "$rc" "GeoIP / GeoSite 更新失败"
finish 0 "GeoIP / GeoSite 更新完成"
}
COMPONENT="${1:-}"
VARIANT="${2:-}" # mihomo/singbox: stable | alpha
[ -n "$COMPONENT" ] || {
echo "usage: $0 <clashoo|luci|mihomo|singbox|smart|lgbm|china|geoip> [stable|alpha]"
exit 2
}
mkdir -p "$(dirname "$RUN_FILE")" >/dev/null 2>&1
printf '%s\n' "$COMPONENT" >"$RUN_FILE"
STATUS="running"
log "组件更新任务启动:${COMPONENT}${VARIANT:+ ($VARIANT)}"
case "$COMPONENT" in
clashoo) run_pkg_update clashoo ;;
luci) run_pkg_update luci ;;
mihomo)
if [ "$VARIANT" = "alpha" ]; then run_core_update 3 "mihomo Alpha 版"
else run_core_update 2 "mihomo 稳定版"; fi ;;
singbox)
if [ "$VARIANT" = "alpha" ]; then run_core_update 5 "sing-box Alpha 版"
else run_core_update 4 "sing-box 稳定版"; fi ;;
smart) run_core_update 1 "mihomo Smart 版" ;;
lgbm) run_lgbm_update ;;
china) run_china_update ;;
geoip) run_geoip_update ;;
*) finish 2 "未知组件:${COMPONENT}" ;;
esac
File diff suppressed because it is too large. Load diff
@@ -1,267 +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)"
DEFAULT_MMDB_URL="https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
DEFAULT_GEOSITE_URL="https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/release/geosite.dat"
DEFAULT_GEOIP_DAT_URL="https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/release/geoip.dat"
DEFAULT_GEOIP_ASN_URL="https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb"
LOYALSOLDIER_MMDB_URL="https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
LOYALSOLDIER_GEOSITE_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat"
LOYALSOLDIER_GEOIP_DAT_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat"
# v2fly ships no Country.mmdb — borrow Loyalsoldier's for the mmdb slot
V2FLY_GEOSITE_URL="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"
V2FLY_GEOIP_DAT_URL="https://github.com/v2fly/geoip/releases/latest/download/geoip.dat"
log() {
echo " $(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
cleanup() {
rm -rf "$TMP_DIR" >/dev/null 2>&1
rm -f /var/run/geoip_update >/dev/null 2>&1
}
config_needs_geosite() {
_cfg="/etc/clashoo/config.yaml"
[ -f "$_cfg" ] || return 1
grep -Eq "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,|^[[:space:]]*geosite:[[:space:]]*|^[[:space:]]*'geosite:|^[[:space:]]*-[[:space:]]*[\"']?geosite:" "$_cfg"
}
# Shared port detection (handles smart core + sing-box profile port); keep the
# local anonymity probe so we skip a coexisting plugin's auth-locked proxy.
. /usr/share/clashoo/update/proxy_lib.sh
detect_proxy() {
proxy="$(clashoo_detect_proxy)"
[ -n "$proxy" ] || return 0
# Only use it if it actually proxies anonymously. A coexisting plugin
# (e.g. OpenClash on the same 7890) may hold this port behind auth and
# answer HTTP 407 — in that case fall through to the mirrors instead.
if command -v curl >/dev/null 2>&1; then
curl -fsS --proxy "$proxy" --connect-timeout 3 --max-time 5 -o /dev/null \
http://www.gstatic.com/generate_204 >/dev/null 2>&1 || return 0
fi
echo "$proxy"
}
_fetch() {
url="$1"; target="$2"; proxy="$3"
if command -v curl >/dev/null 2>&1; then
if [ -n "$proxy" ]; then
curl --proxy "$proxy" -fsSL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$url" -o "$target"
else
curl -fsSL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$url" -o "$target"
fi
return $?
fi
wget -q --timeout=120 --no-check-certificate --user-agent="Clash/OpenWRT" "$url" -O "$target"
}
# 多源拉取:本机代理 → 国内镜像(gh-proxy / ghfast / jsdelivr)→ 原 URL 兜底
download_to() {
url="$1"; target="$2"
[ -z "$url" ] && return 1
proxy="$(detect_proxy)"
if [ -n "$proxy" ]; then
_fetch "$url" "$target" "$proxy" && return 0
fi
case "$url" in
https://raw.githubusercontent.com/*)
rest="${url#https://raw.githubusercontent.com/}"
owner="${rest%%/*}"; r1="${rest#*/}"
repo="${r1%%/*}"; r2="${r1#*/}"
branch="${r2%%/*}"; path="${r2#*/}"
for m in \
"https://gh-proxy.com/${url}" \
"https://ghfast.top/${url}" \
"https://cdn.jsdelivr.net/gh/${owner}/${repo}@${branch}/${path}"; do
_fetch "$m" "$target" "" && return 0
done
;;
https://github.com/*)
for m in \
"https://gh-proxy.com/${url}" \
"https://ghfast.top/${url}"; do
_fetch "$m" "$target" "" && return 0
done
;;
esac
_fetch "$url" "$target" ""
}
download_optional() {
url="$1"
target="$2"
name="$3"
tmp_target="${TMP_DIR}/$(basename "$target").tmp"
if [ -z "$url" ]; then
log "$name skip (url empty)"
return 0
fi
rm -f "$tmp_target" >/dev/null 2>&1
if download_to "$url" "$tmp_target"; then
mv -f "$tmp_target" "$target" >/dev/null 2>&1 || return 1
chmod 644 "$target" >/dev/null 2>&1
log "$name updated"
return 0
fi
rm -f "$tmp_target" >/dev/null 2>&1
log "$name update failed"
return 0
}
download_geosite() {
url="$1"
tmp_target="${TMP_DIR}/GeoSite.dat.tmp"
size=0
if [ -z "$url" ]; then
log "GeoSite.dat skip (url empty)"
return 0
fi
rm -f "$tmp_target" >/dev/null 2>&1
if ! download_to "$url" "$tmp_target"; then
rm -f "$tmp_target" >/dev/null 2>&1
log "GeoSite.dat update failed"
return 0
fi
size=$(wc -c <"$tmp_target" 2>/dev/null)
if [ -z "$size" ] || [ "$size" -lt "$GEOSITE_MIN_SIZE" ]; then
log "GeoSite.dat download invalid or incomplete (${size:-0} bytes)"
rm -f "$tmp_target" >/dev/null 2>&1
return 0
fi
mv -f "$tmp_target" /etc/clashoo/GeoSite.dat >/dev/null 2>&1 || return 1
cp -f /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1 || true
chmod 644 /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1
log "GeoSite.dat updated"
return 0
}
download_mmdb() {
url="$1"
tmp_target="${TMP_DIR}/Country.mmdb.tmp"
size=0
rm -f "$tmp_target" >/dev/null 2>&1
if ! download_to "$url" "$tmp_target"; then
rm -f "$tmp_target" >/dev/null 2>&1
return 1
fi
size=$(wc -c <"$tmp_target" 2>/dev/null)
if [ -z "$size" ] || [ "$size" -lt "$MMDB_MIN_SIZE" ]; then
log "Country.mmdb download invalid or incomplete (${size:-0} bytes)"
rm -f "$tmp_target" >/dev/null 2>&1
return 1
fi
mv -f "$tmp_target" /etc/clashoo/Country.mmdb >/dev/null 2>&1 || return 1
chmod 644 /etc/clashoo/Country.mmdb >/dev/null 2>&1
return 0
}
trap cleanup EXIT INT TERM
mkdir -p "$TMP_DIR" /etc/clashoo >/dev/null 2>&1
rm -f /var/run/geoip_down_complete >/dev/null 2>&1
: > /var/run/geoip_update
: > "$LOG_FILE"
log "GeoIP 更新任务启动"
mmdb_url=""
geosite_url=""
geoip_dat_url=""
geoip_asn_url=""
case "$geoip_source" in
1)
if [ -z "$license_key" ]; then
log "MaxMind source selected but license key is empty"
exit 1
fi
log "Updating Country.mmdb from MaxMind"
if ! download_to "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${license_key}&suffix=tar.gz" "$TMP_DIR/geoip.tar.gz"; then
log "MaxMind download failed"
exit 1
fi
if ! tar zxf "$TMP_DIR/geoip.tar.gz" -C "$TMP_DIR" >/dev/null 2>&1; then
log "MaxMind archive extract failed"
exit 1
fi
mmdb_file="$(ls "$TMP_DIR"/GeoLite2-Country_*/GeoLite2-Country.mmdb 2>/dev/null | head -n 1)"
if [ -z "$mmdb_file" ] || [ ! -f "$mmdb_file" ]; then
log "MaxMind Country.mmdb not found in archive"
exit 1
fi
cp -f "$mmdb_file" /etc/clashoo/Country.mmdb
chmod 644 /etc/clashoo/Country.mmdb >/dev/null 2>&1
log "Country.mmdb updated"
;;
3)
# Loyalsoldier (default)
mmdb_url="$LOYALSOLDIER_MMDB_URL"
geosite_url="$LOYALSOLDIER_GEOSITE_URL"
geoip_dat_url="$LOYALSOLDIER_GEOIP_DAT_URL"
geoip_asn_url="$DEFAULT_GEOIP_ASN_URL"
;;
5)
# v2fly (mmdb borrowed from Loyalsoldier)
mmdb_url="$LOYALSOLDIER_MMDB_URL"
geosite_url="$V2FLY_GEOSITE_URL"
geoip_dat_url="$V2FLY_GEOIP_DAT_URL"
geoip_asn_url="$DEFAULT_GEOIP_ASN_URL"
;;
4)
# custom — asn falls back to default when left blank
mmdb_url="$cfg_mmdb_url"
geosite_url="$cfg_geosite_url"
geoip_dat_url="$cfg_geoip_dat_url"
geoip_asn_url="${cfg_geoip_asn_url:-$DEFAULT_GEOIP_ASN_URL}"
;;
*)
mmdb_url="${cfg_mmdb_url:-$DEFAULT_MMDB_URL}"
geosite_url="${cfg_geosite_url:-$DEFAULT_GEOSITE_URL}"
geoip_dat_url="${cfg_geoip_dat_url:-$DEFAULT_GEOIP_DAT_URL}"
geoip_asn_url="${cfg_geoip_asn_url:-$DEFAULT_GEOIP_ASN_URL}"
;;
esac
if [ "$geoip_source" != "1" ]; then
log "Updating Country.mmdb"
if ! download_mmdb "$mmdb_url"; then
log "Country.mmdb download failed"
exit 1
fi
log "Country.mmdb updated"
if config_needs_geosite; then
download_geosite "$geosite_url"
else
log "GeoSite.dat skip (no geosite reference in config)"
fi
download_optional "$geoip_dat_url" /etc/clashoo/geoip.dat "geoip.dat"
download_optional "$geoip_asn_url" /etc/clashoo/GeoLite2-ASN.mmdb "GeoLite2-ASN.mmdb"
fi
touch /var/run/geoip_down_complete >/dev/null 2>&1
if pidof mihomo >/dev/null 2>&1 || pidof clash-meta >/dev/null 2>&1; then
log "GeoIP update completed, apply on next Clashoo restart"
fi
log "GeoIP update completed"
exit 0
@@ -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 --tries=2 --no-check-certificate -U "Clash/OpenWRT" "$DOWNLOAD_URL" -O "$TMP_PATH" 2>/dev/null
dl_rc=$?
else
log "No curl or wget found"
exit 1
fi
if [ "$dl_rc" -ne 0 ] || [ ! -s "$TMP_PATH" ]; then
log "Download failed (rc=$dl_rc)"
rm -f "$TMP_PATH"
exit 1
fi
if [ -f "$MODEL_PATH" ] && cmp -s "$TMP_PATH" "$MODEL_PATH"; then
log "Model unchanged, no update needed"
rm -f "$TMP_PATH"
exit 0
fi
mv "$TMP_PATH" "$MODEL_PATH" 2>/dev/null
if [ $? -ne 0 ]; then
log "Failed to install model to $MODEL_PATH"
rm -f "$TMP_PATH"
exit 1
fi
log "LightGBM model updated successfully"
exit 0
@@ -1,36 +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"
stamp log_cleanup.status
fi
@@ -1,176 +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 --no-check-certificate --user-agent="Clash/OpenWRT" "$url" -O "$ZIP_FILE"; then
return 0
fi
done
return 1
}
extract_zip() {
if command -v unzip >/dev/null 2>&1; then
unzip -oq "$ZIP_FILE" -d "$UNPACK_DIR" >/dev/null 2>&1
return $?
fi
if command -v busybox >/dev/null 2>&1 && busybox --list 2>/dev/null | grep -qx "unzip"; then
busybox unzip -oq "$ZIP_FILE" -d "$UNPACK_DIR" >/dev/null 2>&1
return $?
fi
if command -v bsdtar >/dev/null 2>&1; then
bsdtar -xf "$ZIP_FILE" -C "$UNPACK_DIR" >/dev/null 2>&1
return $?
fi
return 2
}
find_web_root() {
if [ -f "$UNPACK_DIR/index.html" ]; then
echo "$UNPACK_DIR"
return 0
fi
index_file="$(find "$UNPACK_DIR" -type f -name index.html 2>/dev/null | head -n 1)"
if [ -n "$index_file" ]; then
dirname "$index_file"
return 0
fi
return 1
}
activate_panel() {
rm -rf "$DASHBOARD_LINK" >/dev/null 2>&1
ln -s "$TARGET_DIR" "$DASHBOARD_LINK" >/dev/null 2>&1 || cp -a "$TARGET_DIR" "$DASHBOARD_LINK" >/dev/null 2>&1
rm -rf /www/luci-static/yacd >/dev/null 2>&1
if [ "$panel" = "yacd" ]; then
ln -s "$TARGET_DIR" /www/luci-static/yacd >/dev/null 2>&1
fi
}
trap cleanup EXIT INT TERM
mkdir -p "$UNPACK_DIR" "$TARGET_DIR" >/dev/null 2>&1
touch "$RUN_FILE"
set_state "downloading" "开始下载"
log_msg "Downloading dashboard panel..." "正在下载面板..."
if ! download_zip; then
set_state "error" "下载失败"
log_msg "Dashboard panel download failed" "面板下载失败"
exit 1
fi
extract_zip
extract_rc=$?
if [ "$extract_rc" -ne 0 ]; then
if [ "$extract_rc" -eq 2 ]; then
set_state "error" "缺少解压工具"
log_msg "Panel unzip tool not found" "缺少解压工具,无法安装面板"
else
set_state "error" "解压失败"
log_msg "Dashboard panel unzip failed" "面板解压失败"
fi
exit 1
fi
SRC_DIR="$(find_web_root)"
if [ -z "$SRC_DIR" ] || [ ! -f "$SRC_DIR/index.html" ]; then
set_state "error" "文件无效"
log_msg "Dashboard panel content invalid" "面板文件无效"
exit 1
fi
rm -rf "$TARGET_DIR"/* >/dev/null 2>&1
cp -a "$SRC_DIR"/. "$TARGET_DIR"/ >/dev/null 2>&1
activate_panel
uci set clashoo.config.dashboard_panel="$panel" >/dev/null 2>&1
uci commit clashoo >/dev/null 2>&1
set_state "success" "安装成功"
log_msg "Dashboard panel installed" "面板安装完成"
sleep 1
log_msg "Clashoo" "Clashoo" 1
exit 0
@@ -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,386 +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 --tries=4 --timeout=20 --user-agent="$ua" "$url" -O "$out"
rc=$?
[ "$rc" -eq 0 ] && return 0
DOWNLOAD_ERROR="网络错误(wget rc=${rc})"
return 1
}
update_info() {
local hdr target info
hdr="$1"
target="$2"
info="$(grep -i 'subscription-userinfo:' "$hdr" 2>/dev/null | head -1 | \
sed 's/^[Ss]ubscription-[Uu]serinfo:[[:space:]]*//' | tr -d '\r')"
if [ -n "$info" ]; then
printf '%s\n' "$info" >"${target}.info"
else
rm -f "${target}.info" >/dev/null 2>&1
fi
}
service_running() {
[ -n "${CLASHOO_SERVICE_CMD:-}" ] && return 0
"$SERVICE_CMD" status >/dev/null 2>&1
}
template_output_name() {
local sub tpl
sub="$(printf '%s' "$1" | sed -e 's/\.[Yy][Aa][Mm][Ll]$//' -e 's/\.[Yy][Mm][Ll]$//' -e 's/[^A-Za-z0-9._-]/-/g')"
tpl="$(printf '%s' "$2" | sed -e 's/\.[Yy][Aa][Mm][Ll]$//' -e 's/\.[Yy][Mm][Ll]$//' -e 's/[^A-Za-z0-9._-]/-/g')"
printf '_merged_%s__%s.yaml' "${sub:-sub}" "${tpl:-template}"
}
apply_template() {
local name target template merged merged_path use_config
name="$1"
target="$2"
[ -r "$BIND_FILE" ] || return 0
[ -x /usr/share/clashoo/update/template_merge.sh ] || return 0
template="$(awk -F '#' -v n="$name" '$1==n && ($3=="1" || $3=="true") {print $2; exit}' "$BIND_FILE")"
[ -n "$template" ] && [ -r "$TEMPLATE_DIR/$template" ] || return 0
merged="$(template_output_name "$name" "$template")"
merged_path="$TEMPLATE_DIR/$merged"
if sh /usr/share/clashoo/update/template_merge.sh "$target" "$TEMPLATE_DIR/$template" "$merged_path" >/dev/null 2>&1; then
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
if [ "$use_config" = "$target" ] || [ "$use_config" = "$merged_path" ]; then
uci -q set clashoo.config.use_config="$merged_path"
uci -q set clashoo.config.config_type='3'
uci -q commit clashoo >/dev/null 2>&1
restart_needed=1
fi
else
log_update "模板生成失败:${name} <- ${template}"
fi
}
update_mihomo() {
local name url typ target tmp hdr ua use_config config_type
name="$1"
url="$2"
typ="$3"
safe_name "$name" || return 1
case "$typ" in clash|meta) ;; *) skipped=$((skipped + 1)); return 0 ;; esac
target="$SUB_DIR/$name"
[ -f "$target" ] || { skipped=$((skipped + 1)); return 0; }
tmp="$TMP_DIR/clashoo_sub_$$.yaml"
hdr="$TMP_DIR/clashoo_sub_$$.hdr"
ua="$(subscription_ua "$target")"
if ! download_to "$url" "$tmp" "$hdr" "$ua"; then
failed=$((failed + 1))
log_update "更新失败(${DOWNLOAD_ERROR:-下载失败}):$name"
rm -f "$tmp" "$hdr"
return 1
fi
if ! grep -Eq '^(proxies|proxy-providers):' "$tmp" 2>/dev/null; then
failed=$((failed + 1))
log_update "更新失败(无效 Mihomo 配置):$name"
rm -f "$tmp" "$hdr"
return 1
fi
if cmp -s "$tmp" "$target"; then
update_info "$hdr" "$target"
save_subscription_ua "$target" "$ua"
unchanged=$((unchanged + 1))
rm -f "$tmp" "$hdr"
return 0
fi
if ! mv "$tmp" "$target"; then
failed=$((failed + 1))
rm -f "$hdr"
return 1
fi
update_info "$hdr" "$target"
save_subscription_ua "$target" "$ua"
rm -f "$hdr"
updated=$((updated + 1))
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
config_type="$(uci -q get clashoo.config.config_type 2>/dev/null)"
[ "$config_type" = "1" ] && [ "$use_config" = "$target" ] && restart_needed=1
apply_template "$name" "$target"
log_update "更新完成:$name"
}
valid_singbox() {
ucode -e 'import { readfile } from "fs"; let c = json(readfile(ARGV[0])); exit(type(c) == "object" && type(c.outbounds) == "array" && length(c.outbounds) > 0 ? 0 : 1);' "$1" >/dev/null 2>&1
}
update_singbox() {
local name target url tmp hdr ua active
name="$1"
safe_name "$name" || return 1
target="$SINGBOX_DIR/$name"
[ -f "$target" ] && [ -r "$target.url" ] || { skipped=$((skipped + 1)); return 0; }
url="$(sed -n '1p' "$target.url")"
[ -n "$url" ] || { skipped=$((skipped + 1)); return 0; }
tmp="$TMP_DIR/clashoo_sb_$$.json"
hdr="$TMP_DIR/clashoo_sb_$$.hdr"
ua="$(subscription_ua "$target")"
if ! download_to "$url" "$tmp" "$hdr" "$ua"; then
failed=$((failed + 1))
log_update "更新失败(${DOWNLOAD_ERROR:-下载失败}):$name"
rm -f "$tmp" "$hdr"
return 1
fi
if ! valid_singbox "$tmp"; then
failed=$((failed + 1))
log_update "更新失败(无效 sing-box 配置):$name"
rm -f "$tmp" "$hdr"
return 1
fi
if cmp -s "$tmp" "$target"; then
update_info "$hdr" "$target"
save_subscription_ua "$target" "$ua"
unchanged=$((unchanged + 1))
rm -f "$tmp" "$hdr"
return 0
fi
if ! mv "$tmp" "$target"; then
failed=$((failed + 1))
rm -f "$hdr"
return 1
fi
update_info "$hdr" "$target"
save_subscription_ua "$target" "$ua"
rm -f "$hdr"
updated=$((updated + 1))
active="$(uci -q get clashoo.config.singbox_active 2>/dev/null)"
[ "$active" = "$name" ] && restart_needed=1
log_update "更新完成:$name"
}
update_all() {
local name url typ url_file
if [ -r "$LIST_FILE" ]; then
while IFS='#' read -r name url typ _rest; do
[ -n "$name" ] && [ -n "$url" ] || continue
update_mihomo "$name" "$url" "$typ" || true
done <"$LIST_FILE"
fi
for url_file in "$SINGBOX_DIR"/*.url; do
[ -f "$url_file" ] || continue
name="$(basename "$url_file" .url)"
update_singbox "$name" || true
done
}
case "$1" in
--all)
update_all
;;
--mihomo)
line="$(awk -F '#' -v n="$2" '$1==n {print; exit}' "$LIST_FILE" 2>/dev/null)"
[ -n "$line" ] || { failed=1; message="subscription not found"; }
if [ -n "$line" ]; then
name="$(printf '%s' "$line" | awk -F '#' '{print $1}')"
url="$(printf '%s' "$line" | awk -F '#' '{print $2}')"
typ="$(printf '%s' "$line" | awk -F '#' '{print $3}')"
update_mihomo "$name" "$url" "$typ" || true
fi
;;
--singbox)
update_singbox "$2" || true
;;
*)
failed=1
message="invalid arguments"
;;
esac
if [ "$restart_needed" = "1" ] && service_running; then
"$SERVICE_CMD" restart >/dev/null 2>&1
fi
finished_at="$(date +%s)"
[ -n "$message" ] || message="updated=$updated unchanged=$unchanged failed=$failed skipped=$skipped"
[ "$record_status" = "1" ] && write_status 0 "$finished_at"
[ "$failed" -eq 0 ]
@@ -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,204 +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_V4="/tmp/china_ip.txt.$$"
TMP_V6="/tmp/china_ipv6.txt.$$"
OUT_V4="/tmp/geoip_cn.nft.$$"
OUT_V6="/tmp/geoip6_cn.nft.$$"
log() {
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOG_FILE"
}
bool_enabled() {
case "$1" in
1|true|TRUE|yes|on) return 0 ;;
*) return 1 ;;
esac
}
download_with_fallback() {
local url="$1"
local output="$2"
local ip
if curl -fsSL "$url" -o "$output"; then
return 0
fi
case "$url" in
https://ispip.clang.cn/*)
for ip in 182.247.248.127 103.220.64.183; do
if curl -fsSL --resolve "ispip.clang.cn:443:${ip}" "$url" -o "$output"; then
log "DNS 异常,已使用 --resolve(${ip}) 回源下载"
return 0
fi
done
;;
esac
return 1
}
cleanup() {
rm -f "$TMP_V4" "$TMP_V6" "$OUT_V4" "$OUT_V6"
}
trap cleanup EXIT INT TERM
render_nft_set() {
local source_file="$1"
local output_file="$2"
local set_name="$3"
local set_type="$4"
awk -v set_name="$set_name" -v set_type="$set_type" '
BEGIN {
print "set " set_name " {"
print "\ttype " set_type ";"
print "\tflags interval;"
print "\tauto-merge;"
print "\telements = {"
first = 1
}
!/^[[:space:]]*$/ && !/^[[:space:]]*#/ {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0)
if ($0 == "")
next
if (!first)
print ","
printf "\t\t%s", $0
first = 0
}
END {
if (!first)
print ""
print "\t}"
print "}"
}
' "$source_file" > "$output_file"
}
url4="$(uci -q get clashoo.config.china_ip_url 2>/dev/null || true)"
url6="$(uci -q get clashoo.config.china_ipv6_url 2>/dev/null || true)"
bypass_china="$(uci -q get clashoo.config.bypass_china 2>/dev/null || true)"
bypass_china_ipv6="$(uci -q get clashoo.config.bypass_china_ipv6 2>/dev/null || true)"
[ -n "$bypass_china_ipv6" ] || bypass_china_ipv6="$bypass_china"
[ -n "$url4" ] || url4='https://ispip.clang.cn/all_cn.txt'
[ -n "$url6" ] || url6='https://ispip.clang.cn/all_cn_ipv6.txt'
mkdir -p "$NFT_DIR"
log '开始更新大陆白名单'
download_with_fallback "$url4" "$TMP_V4"
[ -s "$TMP_V4" ] || {
log '大陆 IPv4 白名单下载失败:返回为空'
exit 1
}
changed=0
render_nft_set "$TMP_V4" "$OUT_V4" clashoo_china ipv4_addr
if cmp -s "$OUT_V4" "$TARGET_V4"; then
log '大陆 IPv4 白名单内容无变化'
else
mv "$OUT_V4" "$TARGET_V4"
chmod 644 "$TARGET_V4" >/dev/null 2>&1 || true
changed=1
log '大陆 IPv4 白名单更新完成'
fi
if download_with_fallback "$url6" "$TMP_V6"; then
if [ -s "$TMP_V6" ]; then
render_nft_set "$TMP_V6" "$OUT_V6" clashoo_china6 ipv6_addr
if cmp -s "$OUT_V6" "$TARGET_V6"; then
log '大陆 IPv6 白名单内容无变化'
else
mv "$OUT_V6" "$TARGET_V6"
chmod 600 "$TARGET_V6" >/dev/null 2>&1 || true
changed=1
log '大陆 IPv6 白名单更新完成'
fi
else
log '大陆 IPv6 白名单下载为空,保留原文件'
fi
else
log '大陆 IPv6 白名单下载失败,保留原文件'
fi
instance_pid() {
local service="$1"
local instance="${2:-}"
local filter
if [ -n "$instance" ]; then
filter="@.${service}.instances.${instance}.pid"
else
filter="@[\"${service}\"].instances.*.pid"
fi
ubus call service list "{\"name\":\"$service\",\"verbose\":true}" 2>/dev/null |
jsonfilter -e "$filter" 2>/dev/null | head -n1
}
pid_running() {
[ -n "$1" ] && [ -d "$PROC_ROOT/$1" ]
}
singbox_pid_owns_private_binary() {
local pid="$1" exe cmdline
pid_running "$pid" || return 1
exe="$(readlink -f "$PROC_ROOT/$pid/exe" 2>/dev/null)"
case "$exe" in
"$SINGBOX_BIN_DIR/sing-box-stable"|"$SINGBOX_BIN_DIR/sing-box-alpha") return 0 ;;
esac
cmdline="$(tr '\000' ' ' < "$PROC_ROOT/$pid/cmdline" 2>/dev/null)"
case "$cmdline" in
"$SINGBOX_BIN_DIR/sing-box-stable"|"$SINGBOX_BIN_DIR/sing-box-stable "*|"$SINGBOX_BIN_DIR/sing-box-alpha"|"$SINGBOX_BIN_DIR/sing-box-alpha "*) return 0 ;;
esac
return 1
}
selected_core_running() {
local name pid
if [ "$(uci -q get clashoo.config.core_type 2>/dev/null)" = "singbox" ]; then
name="clashoo"
pid="$(instance_pid "$name" singbox)"
singbox_pid_owns_private_binary "$pid"
return $?
else
name="clashoo"
fi
pid="$(instance_pid "$name")"
pid_running "$pid"
}
firewall_managed() {
bool_enabled "$(uci -q get clashoo.config.enable 2>/dev/null)" || return 1
[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ] && return 1
selected_core_running
}
if [ "$changed" -eq 0 ]; then
log '白名单内容未变,跳过防火墙重载'
elif ! firewall_managed; then
log '未接管防火墙(服务未运行或仅内核模式),仅更新白名单文件'
elif bool_enabled "$bypass_china" || bool_enabled "$bypass_china_ipv6"; then
if "$FW4_SCRIPT" apply >/dev/null 2>&1; then
log "大陆白名单规则已重载(IPv4=${bypass_china:-0},IPv6=${bypass_china_ipv6:-0})"
else
log '大陆白名单规则重载失败'
exit 1
fi
else
log '大陆 IPv4/IPv6 绕过均未启用,仅更新白名单文件'
fi
log '大陆白名单更新流程完成'
@@ -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)
+}
Loaded 100 of 4914 files, more files were not shown because too many files have changed in this diff. Show more