mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-10-29 14:05:13 +08:00
update 2026-09-26 17:00:06
This commit is contained in:
1 parent
97ae9f68da
commit
bcc9ceb04a
4756 files changed
+1670713
No files matched your search
@@ -0,0 +1,149 @@
|
||||
#
|
||||
# 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))
|
||||
@@ -0,0 +1,201 @@
|
||||
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.
|
||||
@@ -0,0 +1,56 @@
|
||||
#
|
||||
# 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)))
|
||||
@@ -0,0 +1,3 @@
|
||||
# LingTiGameAcc
|
||||
|
||||
This Tools can be functioning `independently` without luci app.
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/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."
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<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仓库所有者!
|
||||
@@ -0,0 +1,83 @@
|
||||
# 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))
|
||||
@@ -0,0 +1,14 @@
|
||||
-----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-----
|
||||
@@ -0,0 +1,15 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICRDCCAcqgAwIBAgIUeVqRrT2mHG5Mc8JD+ErphiAmlgkwCgYIKoZIzj0EAwMw
|
||||
SDELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRlYXNlTXVzaWMgUm9v
|
||||
dCBDQTETMBEGA1UECgwKMTcxNTE3MzMyOTAeFw0yMTA0MzAwNzIzMDJaFw0yMjA0
|
||||
MzAwNzIzMDJaMHsxCzAJBgNVBAYTAkNOMREwDwYDVQQHDAhIYW5nemhvdTEsMCoG
|
||||
A1UECgwjTmV0RWFzZSAoSGFuZ3pob3UpIE5ldHdvcmsgQ28uLCBMdGQxETAPBgNV
|
||||
BAsMCElUIERlcHQuMRgwFgYDVQQDDA8qLm11c2ljLjE2My5jb20wdjAQBgcqhkjO
|
||||
PQIBBgUrgQQAIgNiAAQTPyU9RQ1pAFMLmozi+c4pEC1rrxAlPGwO9Em+qV+a5qLW
|
||||
gQjjsJeabMqJ/UQ7hDtdKVxWuXiAjMiDcXwL63I71MZKPTAEKXdCmNQwb4kXvRUn
|
||||
oOR4r7BMxEpGlf0CULWjQjBAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMCkGA1UdEQQi
|
||||
MCCCDW11c2ljLjE2My5jb22CDyoubXVzaWMuMTYzLmNvbTAKBggqhkjOPQQDAwNo
|
||||
ADBlAjEAs5bdgnNP/DiK919RiWscC0kyuY0ugG1C8m8F2Yod4MI3oTyrkVcag21o
|
||||
NSzm802uAjBoPuKEbjjFP4ics0BQdICiVd6WCVAsE69FnlmqRteAJqxvdKGpVLi+
|
||||
Qi3arfomrrc=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,9 @@
|
||||
-----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-----
|
||||
@@ -0,0 +1,144 @@
|
||||
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
|
||||
@@ -0,0 +1,29 @@
|
||||
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
|
||||
@@ -0,0 +1,47 @@
|
||||
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)))
|
||||
@@ -0,0 +1,91 @@
|
||||
# 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))
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
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'
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/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
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# quic-go expects larger UDP send/receive buffers
|
||||
net.core.rmem_max = 7500000
|
||||
net.core.wmem_max = 7500000
|
||||
@@ -0,0 +1,96 @@
|
||||
#
|
||||
# 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)))
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
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'
|
||||
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/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
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,54 @@
|
||||
# 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))
|
||||
@@ -0,0 +1,21 @@
|
||||
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'
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
#!/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"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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
@@ -0,0 +1,134 @@
|
||||
#
|
||||
# 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))
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
# 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'
|
||||
@@ -0,0 +1,364 @@
|
||||
#!/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
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
--- /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);
|
||||
@@ -0,0 +1,61 @@
|
||||
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
|
||||
@@ -0,0 +1,239 @@
|
||||
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
|
||||
@@ -0,0 +1,35 @@
|
||||
#
|
||||
# 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))
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,35 @@
|
||||
#
|
||||
# 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))
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,46 @@
|
||||
# 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))
|
||||
@@ -0,0 +1,25 @@
|
||||
From: coolsnowwolf <coolsnowwolf@gmail.com>
|
||||
Subject: [PATCH] cachepool: stop using runtime.rawbyteslice
|
||||
|
||||
Recent Go releases reject external linkname references to rawbyteslice.
|
||||
Use the language-provided slice allocator for the zeroed allocation path.
|
||||
|
||||
Signed-off-by: coolsnowwolf <coolsnowwolf@gmail.com>
|
||||
---
|
||||
--- a/pcsutil/cachepool/malloc.go
|
||||
+++ b/pcsutil/cachepool/malloc.go
|
||||
@@ -8,12 +8,9 @@
|
||||
//go:linkname mallocgc runtime.mallocgc
|
||||
func mallocgc(size uintptr, typ uintptr, needzero bool) unsafe.Pointer
|
||||
|
||||
-//go:linkname rawbyteslice runtime.rawbyteslice
|
||||
-func rawbyteslice(size int) (b []byte)
|
||||
-
|
||||
-// RawByteSlice point to runtime.rawbyteslice
|
||||
+// RawByteSlice allocates a zeroed byte slice.
|
||||
func RawByteSlice(size int) (b []byte) {
|
||||
- return rawbyteslice(size)
|
||||
+ return make([]byte, size)
|
||||
}
|
||||
|
||||
// RawMalloc allocates a new slice. The slice is not zeroed.
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
__
|
||||
___ ___ ___ ___ _ ______/ /_
|
||||
/ _ \/ _ \/ -_) _ \ |/|/ / __/ __/
|
||||
\___/ .__/\__/_//_/__,__/_/ \__/
|
||||
/_/
|
||||
--------------------------------
|
||||
%D %V, %C
|
||||
--------------------------------
|
||||
@@ -0,0 +1,57 @@
|
||||
#
|
||||
# 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))
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ -d /usr/lib/cups/filter ] && chmod 0754 /usr/lib/cups/filter/*
|
||||
@@ -0,0 +1,45 @@
|
||||
# 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))
|
||||
@@ -0,0 +1,72 @@
|
||||
# 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))
|
||||
@@ -0,0 +1,111 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=clashoo
|
||||
PKG_SHORT_SHA:=f103639
|
||||
PKG_COMMIT_DATE:=2026.09.25
|
||||
|
||||
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:=f103639c808d93a2c34cae56757b458862871b22
|
||||
PKG_HASH:=e4e338a157400319557dd5b4268d2c68b47dce981889af3bedc7852990b176f0
|
||||
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
|
||||
|
||||
PKG_LICENSE:=GPL3.0+
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
|
||||
PKG_BUILD_DEPENDS:=golang/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
||||
|
||||
GO_PKG:=github.com/metacubex/mihomo
|
||||
GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
|
||||
GO_PKG_TAGS:=with_gvisor
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
define Package/clashoo
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
TITLE:=Clashoo (Mihomo)
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle +yq +firewall4 +ip-full +coreutils-nohup +coreutils-timeout +kmod-inet-diag +kmod-nft-socket +kmod-nft-tproxy +kmod-tun +kmod-dummy +unzip
|
||||
PROVIDES:=mihomo clash-meta
|
||||
endef
|
||||
|
||||
define Package/clashoo/description
|
||||
Mihomo proxy core with runtime environment for Clashoo.
|
||||
endef
|
||||
|
||||
define Package/clashoo/conffiles
|
||||
/etc/config/clashoo
|
||||
endef
|
||||
|
||||
define Package/clashoo/install
|
||||
$(call GoPackage/Package/Install/Bin,$(1))
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
ln -sf /usr/bin/mihomo $(1)/usr/bin/clash-meta
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/etc/clashoo
|
||||
$(INSTALL_DIR) $(1)/etc/clashoo/provider
|
||||
$(INSTALL_DIR) $(1)/etc/clashoo/proxyprovider
|
||||
$(INSTALL_DIR) $(1)/etc/clashoo/ruleprovider
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashoo
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashbackup
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashoo/config
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/sub
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/upload
|
||||
$(INSTALL_DIR) $(1)/usr/share/clashoo/config/custom
|
||||
|
||||
$(INSTALL_BIN) $(CURDIR)/files/etc/init.d/clashoo $(1)/etc/init.d/clashoo
|
||||
$(INSTALL_CONF) $(CURDIR)/files/etc/config/clashoo $(1)/etc/config/clashoo
|
||||
$(INSTALL_DATA) $(CURDIR)/files/etc/config/clashoo $(1)/usr/share/clashoo/clashoo.default
|
||||
$(CP) $(CURDIR)/files/usr/share/clashoo/* $(1)/usr/share/clashoo/
|
||||
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/migrate_lan_acl.sh $(1)/usr/share/clashoo/migrate_lan_acl.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/migrate_fake_ip_filter.sh $(1)/usr/share/clashoo/migrate_fake_ip_filter.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/detect_core_arch.sh $(1)/usr/share/clashoo/detect_core_arch.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/usr/share/clashoo/diagnose_bypass.sh $(1)/usr/share/clashoo/diagnose_bypass.sh
|
||||
|
||||
$(RM) $(1)/usr/share/clashoo/dashboard
|
||||
$(RM) $(1)/usr/share/clashoo/yacd
|
||||
$(RM) $(1)/usr/share/clashoo/update.log
|
||||
$(RM) $(1)/usr/share/clashoo/geoip.log
|
||||
$(RM) $(1)/usr/share/clashoo/core_down_complete
|
||||
endef
|
||||
|
||||
define Package/clashoo/postinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
[ -x /usr/bin/mihomo ] && [ ! -x /usr/bin/clash-meta ] && ln -sf /usr/bin/mihomo /usr/bin/clash-meta
|
||||
rm -f /usr/share/clashoo/check_luci_version.sh /usr/share/clashoo/check_clash_meta_version.sh /usr/share/clashoo/check_mihomo_core_version.sh
|
||||
rm -f /usr/share/clashoo/cuslist.sh /usr/share/clashoo/groups.sh /usr/share/clashoo/load_groups.sh /usr/share/clashoo/panel_diag.sh
|
||||
rm -f /usr/share/clashoo/rmlist.sh /usr/share/clashoo/uplist.sh /usr/share/clashoo/rpc_restart_async.sh /usr/share/clashoo/rpc_update_china_ip_async.sh
|
||||
rm -rf /usr/share/clashoo/create
|
||||
/usr/share/clashoo/migrate_lan_acl.sh >/dev/null 2>&1 || true
|
||||
/usr/share/clashoo/migrate_fake_ip_filter.sh >/dev/null 2>&1 || true
|
||||
/usr/share/clashoo/detect_core_arch.sh >/dev/null 2>&1 || true
|
||||
if [ "$$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then
|
||||
/etc/init.d/clashoo enable 2>/dev/null || true
|
||||
else
|
||||
/etc/init.d/clashoo disable 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
$(Build/Prepare/Default)
|
||||
$(RM) -r $(PKG_BUILD_DIR)/rules/logic_test
|
||||
endef
|
||||
|
||||
$(eval $(call GoBinPackage,clashoo))
|
||||
$(eval $(call BuildPackage,clashoo))
|
||||
@@ -0,0 +1,156 @@
|
||||
|
||||
config clashoo 'config'
|
||||
option redir_port '7891'
|
||||
option http_port '8080'
|
||||
option socks_port '1080'
|
||||
option mixed_port '7890'
|
||||
option enable_ipv6 'true'
|
||||
option enable '0'
|
||||
option dash_port '9090'
|
||||
option dashboard_panel 'zashboard'
|
||||
option level 'info'
|
||||
option core '3'
|
||||
option allow_lan '1'
|
||||
option dnsforwader '1'
|
||||
option subcri 'meta'
|
||||
option cusrule '0'
|
||||
option dnscache '1'
|
||||
option p_mode 'rule'
|
||||
option append_rules '0'
|
||||
option enhanced_mode 'fake-ip'
|
||||
option fake_ip_range '198.18.0.1/16'
|
||||
option fake_ip_range6 'fc00::/18'
|
||||
list fake_ip_filter '+.lan'
|
||||
list fake_ip_filter '+.local'
|
||||
list fake_ip_filter 'localhost.ptlogin2.qq.com'
|
||||
list fake_ip_filter 'rule-set:cn_domain'
|
||||
option fake_ip_filter_migrated '1'
|
||||
option listen_port '1053'
|
||||
option interf '0'
|
||||
option download_core 'amd64-compatible'
|
||||
option dcore '3'
|
||||
option same_tag '1'
|
||||
option geoip_source '3'
|
||||
option geoip_format 'mmdb'
|
||||
option geodata_loader 'standard'
|
||||
option tproxy_port '7982'
|
||||
option access_control '0'
|
||||
option acl_migrated '1'
|
||||
option tcp_mode 'redirect'
|
||||
option tun_mode '0'
|
||||
option core_mirror_prefix 'https://gh-proxy.com/'
|
||||
option core_download_advanced '0'
|
||||
list default_nameserver '223.5.5.5'
|
||||
list default_nameserver '119.29.29.29'
|
||||
option dns_ecs ''
|
||||
option dns_ecs_override '0'
|
||||
option fallback_filter_geoip '0'
|
||||
list fallback_filter_ipcidr '240.0.0.0/4'
|
||||
option dns_respect_rules '1'
|
||||
option dns_loopback_compat '0'
|
||||
option dns_migrated '1'
|
||||
option block_quic '0'
|
||||
option singbox_independent_cache '0'
|
||||
option udp_mode 'tproxy'
|
||||
option ipv4_dns_hijack '1'
|
||||
option ipv6_dns_hijack '1'
|
||||
option ipv4_proxy '1'
|
||||
option ipv6_proxy '1'
|
||||
option fake_ip_ping_hijack '1'
|
||||
option dns_leak_protect '0'
|
||||
option geoip_mmdb_url 'https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb'
|
||||
option geosite_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat'
|
||||
option geoip_dat_url 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat'
|
||||
option enable_udp '1'
|
||||
option stack 'gvisor'
|
||||
option bypass_china '1'
|
||||
option bypass_china_ipv6 '1'
|
||||
option bypass_port_mode 'common'
|
||||
option sniffer_streaming '1'
|
||||
option selection_cache '1'
|
||||
option fake_ip_cache '1'
|
||||
option enable_dns '1'
|
||||
option disable_quic_gso '1'
|
||||
option smart_policy_priority 'Premium:0.9;SG:1.3'
|
||||
option smart_prefer_asn '1'
|
||||
option smart_uselightgbm '1'
|
||||
option smart_collectdata '1'
|
||||
option smart_collect_size '100'
|
||||
option smart_collect_rate '1'
|
||||
option smart_lgbm_auto_update '1'
|
||||
option smart_lgbm_update_interval '72'
|
||||
option smart_lgbm_url 'https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin'
|
||||
list defaul_nameserver '114.114.114.114'
|
||||
list defaul_nameserver '223.5.5.5'
|
||||
option auto_update '1'
|
||||
option auto_update_time '72'
|
||||
option auto_subscription_update '0'
|
||||
option subscription_update_interval '72'
|
||||
option auto_clear_log '1'
|
||||
option clear_time '72'
|
||||
option core_type 'mihomo'
|
||||
option smart_auto_switch '0'
|
||||
option core_only '0'
|
||||
|
||||
config hosts
|
||||
option enabled '0'
|
||||
option adress '*.clash.dev'
|
||||
option ip '127.0.0.1'
|
||||
|
||||
config hosts
|
||||
option enabled '0'
|
||||
option adress 'alpha.clash.dev'
|
||||
option ip '::1'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'nameserver'
|
||||
option ser_address 'https://dns.alidns.com/dns-query'
|
||||
option protocol 'none'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'nameserver'
|
||||
option ser_address 'https://doh.pub/dns-query'
|
||||
option protocol 'none'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'proxy-server-nameserver'
|
||||
option ser_address '1.1.1.1'
|
||||
option protocol 'tls://'
|
||||
option ser_port '853'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'fallback'
|
||||
option ser_address 'https://cloudflare-dns.com/dns-query'
|
||||
option protocol 'none'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'fallback'
|
||||
option ser_address 'https://dns.google/dns-query'
|
||||
option protocol 'none'
|
||||
|
||||
config dnsservers
|
||||
option enabled '1'
|
||||
option ser_type 'direct-nameserver'
|
||||
option ser_address '223.5.5.5'
|
||||
option protocol 'udp://'
|
||||
|
||||
config dns_policy
|
||||
option enabled '1'
|
||||
option policy_type 'nameserver-policy'
|
||||
option matcher 'geosite:cn'
|
||||
list nameserver 'https://dns.alidns.com/dns-query'
|
||||
list nameserver 'https://doh.pub/dns-query'
|
||||
|
||||
config authentication
|
||||
|
||||
# default empty custom-rule row so the section shows the input form, not just an add
|
||||
# button; injectors skip rules with an empty ipaaddr, so it generates nothing until filled
|
||||
config addtype
|
||||
option type 'DOMAIN-SUFFIX'
|
||||
option pgroup 'DIRECT'
|
||||
option ipaaddr ''
|
||||
Executable
+2507
File diff suppressed because it is too large.
Load diff
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ -n "$(uci -q get clashoo.config.download_core 2>/dev/null)" ] && exit 0
|
||||
|
||||
raw="$(uname -m 2>/dev/null)"
|
||||
[ -n "$raw" ] || raw="$(apk --print-arch 2>/dev/null)"
|
||||
[ -n "$raw" ] || raw="$(opkg status libc 2>/dev/null | awk -F': ' '/^Architecture/{print $2; exit}')"
|
||||
[ -n "$raw" ] || exit 0
|
||||
|
||||
case "$raw" in
|
||||
x86_64|amd64) arch="amd64-compatible" ;;
|
||||
aarch64*|arm64) arch="arm64" ;;
|
||||
armv7*|arm_cortex-a[7-9]*|arm_cortex-a1[0-9]*) arch="armv7" ;;
|
||||
armv6*|arm_cortex-a[56]*) arch="armv6" ;;
|
||||
arm*) arch="armv5" ;;
|
||||
i[3-6]86) arch="386" ;;
|
||||
mips64el*) arch="mips64le" ;;
|
||||
mips64*) arch="mips64" ;;
|
||||
mipsel*) arch="mipsle" ;;
|
||||
mips*) arch="mips" ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
uci -q set clashoo.config.download_core="$arch"
|
||||
uci -q commit clashoo
|
||||
exit 0
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/bin/sh
|
||||
|
||||
umask 077
|
||||
|
||||
CONFIG_YAML="${CONFIG_YAML:-/etc/clashoo/config.yaml}"
|
||||
NFT_V4="${NFT_V4:-/usr/share/clashoo/nftables/geoip_cn.nft}"
|
||||
NFT_V6="${NFT_V6:-/usr/share/clashoo/nftables/geoip6_cn.nft}"
|
||||
MAX_CONN=200
|
||||
|
||||
TMPDIR_D="$(mktemp -d /tmp/.clashoo_diag.XXXXXX 2>/dev/null)" || exit 1
|
||||
trap 'rm -rf "$TMPDIR_D" 2>/dev/null' EXIT INT TERM
|
||||
|
||||
uci_get() {
|
||||
uci -q get "clashoo.config.$1" 2>/dev/null
|
||||
}
|
||||
|
||||
section() {
|
||||
printf '\n===== %s =====\n' "$1"
|
||||
}
|
||||
|
||||
yesno() {
|
||||
[ -n "$1" ] && [ "$1" != "0" ] && [ "$1" != "false" ] && echo "on" || echo "off"
|
||||
}
|
||||
|
||||
bool_on() {
|
||||
case "$1" in
|
||||
1|true|TRUE|yes|on) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
bypass4="$(uci_get bypass_china)"
|
||||
bypass6="$(uci_get bypass_china_ipv6)"
|
||||
[ -n "$bypass6" ] || bypass6="$bypass4"
|
||||
|
||||
section "配置摘要"
|
||||
printf '服务开关 : %s\n' "$(yesno "$(uci_get enable)")"
|
||||
printf '运行模式 : %s\n' "$(uci_get enhanced_mode)"
|
||||
printf 'TCP / UDP : %s / %s\n' "$(uci_get tcp_mode)" "$(uci_get udp_mode)"
|
||||
printf '大陆绕过 v4 : %s\n' "$(yesno "$bypass4")"
|
||||
printf '大陆绕过 v6 : %s%s\n' "$(yesno "$bypass6")" \
|
||||
"$([ -z "$(uci_get bypass_china_ipv6)" ] && echo '(未设置,跟随 v4)')"
|
||||
printf '接管端口 : %s' "$(uci_get bypass_port_mode)"
|
||||
[ "$(uci_get bypass_port_mode)" = "custom" ] && printf ' (%s)' "$(uci_get bypass_port_custom)"
|
||||
printf '\n'
|
||||
printf 'IPv6 代理 : %s\n' "$(yesno "$(uci_get ipv6_proxy)")"
|
||||
printf '阻断 QUIC : %s\n' "$(yesno "$(uci_get block_quic)")"
|
||||
|
||||
if [ "$(uci_get bypass_port_mode)" = "common" ]; then
|
||||
printf '\n注意: 接管端口为 common,只代理 22/53/80/443/8080/8443,\n'
|
||||
printf ' 游戏与 P2P 常用的高位端口不会走代理。\n'
|
||||
fi
|
||||
|
||||
section "fake-ip 过滤"
|
||||
if [ -s "$CONFIG_YAML" ]; then
|
||||
if grep -qE '^[[:space:]]*fake-ip-filter:' "$CONFIG_YAML" 2>/dev/null; then
|
||||
filter_mode="$(uci_get fake_ip_filter_mode)"
|
||||
[ -n "$filter_mode" ] || filter_mode="$(yq -M e '(.dns."fake-ip-filter-mode" // "blacklist") | tostring' "$CONFIG_YAML" 2>/dev/null)"
|
||||
if [ "$filter_mode" = "whitelist" ]; then
|
||||
echo "白名单模式:列表中的域名使用 fake-IP"
|
||||
elif grep -qiE 'rule-set:cn_domain|RULE-SET,[[:space:]]*cn_domain,[[:space:]]*real-ip' "$CONFIG_YAML" 2>/dev/null; then
|
||||
echo "含 rule-set:cn_domain (国内域名返回真实 IP)"
|
||||
else
|
||||
echo "缺少 rule-set:cn_domain"
|
||||
echo " 国内域名会拿到 fake-ip 走代理,与硬编码国内 IP 分成两条路。"
|
||||
fi
|
||||
else
|
||||
echo "配置中无 fake-ip-filter"
|
||||
fi
|
||||
else
|
||||
echo "找不到 $CONFIG_YAML"
|
||||
fi
|
||||
|
||||
section "内核分流规则"
|
||||
if [ -s "$CONFIG_YAML" ]; then
|
||||
echo "-- rules 末尾 5 条 --"
|
||||
grep -E '^[[:space:]]*-[[:space:]]*["'"'"']?(GEOIP|RULE-SET|MATCH|IP-CIDR|DOMAIN)' "$CONFIG_YAML" 2>/dev/null | tail -5
|
||||
fi
|
||||
|
||||
section "IP 库"
|
||||
for f in "$NFT_V4" "$NFT_V6"; do
|
||||
if [ -s "$f" ]; then
|
||||
printf '%-46s %6s 条 %s\n' "$f" \
|
||||
"$(grep -cE '^[[:space:]]*[0-9a-fA-F].*/' "$f" 2>/dev/null)" \
|
||||
"$(date -r "$f" '+%Y-%m-%d %H:%M' 2>/dev/null)"
|
||||
else
|
||||
printf '%-46s 缺失\n' "$f"
|
||||
fi
|
||||
done
|
||||
for f in /etc/clashoo/Country.mmdb /etc/clashoo/geoip.metadb; do
|
||||
[ -f "$f" ] && printf '%-46s %s\n' "$f" "$(ls -lh "$f" 2>/dev/null | awk '{print $5}')"
|
||||
done
|
||||
|
||||
set_count() {
|
||||
nft list set inet fw4 "$1" 2>/dev/null | tr ',' '\n' | grep -cE '[0-9a-fA-F]+[.:].*/'
|
||||
}
|
||||
|
||||
section "防火墙绕过规则"
|
||||
if nft list sets inet fw4 2>/dev/null | grep -q clashoo_china; then
|
||||
for ch in dstnat mangle_prerouting; do
|
||||
nft -a list chain inet fw4 "$ch" 2>/dev/null |
|
||||
grep -E '@clashoo_china6? ' | sed "s|^[[:space:]]*| ${ch}: |"
|
||||
done
|
||||
echo
|
||||
echo "已加载元素数(nft auto-merge 会合并相邻网段,少于文件行数属正常):"
|
||||
bool_on "$bypass4" && printf ' clashoo_china : %s\n' "$(set_count clashoo_china)"
|
||||
bool_on "$bypass6" && printf ' clashoo_china6 : %s\n' "$(set_count clashoo_china6)"
|
||||
else
|
||||
echo "当前 nftables 中没有 clashoo_china 集合(绕过未启用或服务未运行)"
|
||||
fi
|
||||
|
||||
section "绕过命中检查"
|
||||
if ! bool_on "$bypass4" && ! bool_on "$bypass6"; then
|
||||
echo "两个绕过均未启用,所有被接管的流量都交给内核判定,无需检查"
|
||||
else
|
||||
SECRET="$(uci_get dash_pass)"
|
||||
PORT="$(uci_get dash_port)"
|
||||
LAN_IP="$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F/ '{print $1}')"
|
||||
[ -n "$LAN_IP" ] || LAN_IP="127.0.0.1"
|
||||
CONN_JSON="$TMPDIR_D/connections.json"
|
||||
|
||||
if [ -z "$PORT" ]; then
|
||||
echo "未配置控制端口,跳过"
|
||||
elif ! curl -m 5 -s -H "Authorization: Bearer ${SECRET}" \
|
||||
"http://${LAN_IP}:${PORT}/connections" -o "$CONN_JSON" 2>/dev/null; then
|
||||
echo "无法访问内核 API (http://${LAN_IP}:${PORT}),跳过"
|
||||
elif [ ! -s "$CONN_JSON" ]; then
|
||||
echo "内核 API 返回为空,跳过"
|
||||
else
|
||||
jsonfilter -i "$CONN_JSON" -e '@["connections"][*]["metadata"]["destinationIP"]' \
|
||||
2>/dev/null > "${CONN_JSON}.ip"
|
||||
jsonfilter -i "$CONN_JSON" -e '@["connections"][*]["chains"][0]' \
|
||||
2>/dev/null > "${CONN_JSON}.ch"
|
||||
|
||||
total="$(wc -l < "${CONN_JSON}.ip" 2>/dev/null | tr -d ' ')"
|
||||
[ -n "$total" ] || total=0
|
||||
|
||||
if [ "$total" -eq 0 ]; then
|
||||
echo "当前无活动连接"
|
||||
elif [ "$total" != "$(wc -l < "${CONN_JSON}.ch" 2>/dev/null | tr -d ' ')" ]; then
|
||||
echo "内核 API 字段数不一致,跳过"
|
||||
else
|
||||
awk 'NR==FNR { a[FNR] = $0; next } { print a[FNR] "\t" $0 }' \
|
||||
"${CONN_JSON}.ip" "${CONN_JSON}.ch" 2>/dev/null \
|
||||
| awk -F'\t' '$2 != "DIRECT" && $1 != "" && $1 !~ /^(198\.18\.|::|fc00:|fd)/ { print $1 "\t" $2 }' \
|
||||
| sort -u | head -n "$MAX_CONN" > "${CONN_JSON}.chk"
|
||||
|
||||
hit=0
|
||||
: > "${CONN_JSON}.bad"
|
||||
while IFS="$(printf '\t')" read -r dip chain; do
|
||||
[ -n "$dip" ] || continue
|
||||
case "$dip" in
|
||||
*:*) bool_on "$bypass6" || continue
|
||||
set_name=clashoo_china6 ;;
|
||||
*) bool_on "$bypass4" || continue
|
||||
set_name=clashoo_china ;;
|
||||
esac
|
||||
if nft get element inet fw4 "$set_name" "{ $dip }" >/dev/null 2>&1; then
|
||||
hit=$((hit + 1))
|
||||
printf ' %-40s -> %s\n' "$dip" "$chain" >> "${CONN_JSON}.bad"
|
||||
fi
|
||||
done < "${CONN_JSON}.chk"
|
||||
|
||||
printf '活动连接 %s 条,其中走代理的去重目的地 %s 个(上限 %s)\n' \
|
||||
"$total" "$(wc -l < "${CONN_JSON}.chk" 2>/dev/null | tr -d ' ')" "$MAX_CONN"
|
||||
if [ "$hit" -gt 0 ]; then
|
||||
printf '命中绕过白名单却仍走代理: %s 个\n\n' "$hit"
|
||||
head -20 "${CONN_JSON}.bad"
|
||||
echo
|
||||
echo "这些目的地在防火墙白名单内,本应直连,却出现在代理链路上。"
|
||||
echo "可能是连接建立于规则变更之前,或该地址族的绕过规则未生效。"
|
||||
else
|
||||
echo "未发现白名单地址走代理的连接"
|
||||
fi
|
||||
rm -f "${CONN_JSON}.chk" "${CONN_JSON}.bad" 2>/dev/null
|
||||
fi
|
||||
rm -f "${CONN_JSON}.ip" "${CONN_JSON}.ch" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
@@ -0,0 +1,385 @@
|
||||
#!/usr/bin/ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile } from 'fs';
|
||||
|
||||
// Standalone sing-box config version-migration, used by the init.d core-only
|
||||
// path to upgrade an imported config (momo/homeproxy/etc.) to the format the
|
||||
// current sing-box core accepts, WITHOUT clashoo normalize/takeover.
|
||||
//
|
||||
// IMPORTANT: migrate_singbox() below is kept byte-for-byte in sync with the
|
||||
// copy in luci-app-clashoo/.../luci.clashoo (the UI "migrate" button). If you
|
||||
// change one, change the other.
|
||||
|
||||
function migrate_singbox(cfg) {
|
||||
// --- Fix DNS servers: address-based → type-based, remove detour ---
|
||||
if (cfg.dns && cfg.dns.servers) {
|
||||
cfg.dns.servers = map(cfg.dns.servers || [], function(srv) {
|
||||
let addr = srv.address || '';
|
||||
let new_srv = {};
|
||||
let skip = { address: 1, detour: 1, address_resolver: 1, address_strategy: 1 };
|
||||
for (let k in srv) if (!skip[k]) new_srv[k] = srv[k];
|
||||
|
||||
if (addr === 'fakeip') {
|
||||
new_srv.type = 'fakeip';
|
||||
} else if (addr === 'local' || addr === '') {
|
||||
new_srv.type = new_srv.type || 'local';
|
||||
} else if (match(addr, /^rcode:\/\//) || addr === 'rcode') {
|
||||
new_srv._rcode = true; // mark for removal, handled via dns rule action
|
||||
} else if (match(addr, /^h3:\/\//)) {
|
||||
let host = replace(replace(addr, /^h3:\/\//, ''), /\/.*$/, '');
|
||||
new_srv.type = 'h3'; new_srv.server = host;
|
||||
} else if (match(addr, /^https:\/\//)) {
|
||||
let host = replace(replace(addr, /^https:\/\//, ''), /\/.*$/, '');
|
||||
new_srv.type = 'https'; new_srv.server = host;
|
||||
} else if (match(addr, /^tls:\/\//)) {
|
||||
new_srv.type = 'tls'; new_srv.server = replace(addr, /^tls:\/\//, '');
|
||||
} else if (match(addr, /^tcp:\/\//)) {
|
||||
new_srv.type = 'tcp'; new_srv.server = replace(addr, /^tcp:\/\//, '');
|
||||
} else if (addr && !new_srv.type) {
|
||||
new_srv.type = 'udp'; new_srv.server = addr; // plain IP → UDP
|
||||
} else if (addr) {
|
||||
new_srv.address = addr; // unknown format, keep as-is
|
||||
}
|
||||
return new_srv;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Remove rcode servers (unsupported in 1.12+ new format), convert referencing rules to reject ---
|
||||
let rcode_tags = {};
|
||||
if (cfg.dns && cfg.dns.servers) {
|
||||
let good = [];
|
||||
for (let srv in cfg.dns.servers) {
|
||||
if (srv._rcode) rcode_tags[srv.tag] = true;
|
||||
else push(good, srv);
|
||||
}
|
||||
cfg.dns.servers = good;
|
||||
}
|
||||
if (cfg.dns && cfg.dns.rules && length(keys(rcode_tags)) > 0) {
|
||||
cfg.dns.rules = map(cfg.dns.rules || [], function(rule) {
|
||||
if (rule.server && rcode_tags[rule.server]) {
|
||||
let r = {};
|
||||
for (let k in rule) if (k !== 'server' && k !== 'action') r[k] = rule[k];
|
||||
r.action = 'reject';
|
||||
return r;
|
||||
}
|
||||
return rule;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Remove legacy dns.fakeip.enabled (1.12+ fakeip is configured via type:"fakeip" server) ---
|
||||
if (cfg.dns && cfg.dns.fakeip) {
|
||||
let fp = cfg.dns.fakeip;
|
||||
let new_fp = {};
|
||||
for (let k in fp) if (k !== 'enabled') new_fp[k] = fp[k];
|
||||
if (length(keys(new_fp)) > 0) cfg.dns.fakeip = new_fp;
|
||||
else delete cfg.dns['fakeip'];
|
||||
}
|
||||
|
||||
// --- Fix DNS rules: add action:"route" when server is set ---
|
||||
if (cfg.dns && cfg.dns.rules) {
|
||||
cfg.dns.rules = map(cfg.dns.rules || [], function(rule) {
|
||||
if (rule.server && !rule.action) {
|
||||
let r = {};
|
||||
for (let k in rule) r[k] = rule[k];
|
||||
r.action = 'route';
|
||||
return r;
|
||||
}
|
||||
return rule;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Fix legacy special outbounds (block / dns types) ---
|
||||
let special = {};
|
||||
if (cfg.outbounds) {
|
||||
let kept = [];
|
||||
for (let ob in cfg.outbounds) {
|
||||
if (ob.type === 'block' || ob.type === 'dns') {
|
||||
special[ob.tag] = ob.type;
|
||||
} else {
|
||||
push(kept, ob);
|
||||
}
|
||||
}
|
||||
cfg.outbounds = kept;
|
||||
}
|
||||
|
||||
// Patch route rules that pointed to removed special outbounds
|
||||
if (cfg.route && cfg.route.rules && length(keys(special)) > 0) {
|
||||
cfg.route.rules = map(cfg.route.rules || [], function(rule) {
|
||||
let sp = rule.outbound && special[rule.outbound];
|
||||
if (!sp) return rule;
|
||||
let r = {};
|
||||
for (let k in rule) if (k !== 'outbound') r[k] = rule[k];
|
||||
r.action = (sp === 'dns') ? 'hijack-dns' : 'reject';
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Migrate geoip/geosite database refs to rule_set (removed in sing-box 1.12) ---
|
||||
let needed_rule_sets = {};
|
||||
|
||||
let migrate_geo_rule = function(rule) {
|
||||
let has_geo = rule.geoip || rule.geosite;
|
||||
if (!has_geo) return rule;
|
||||
let r = {};
|
||||
let new_rs = [];
|
||||
for (let k in rule) {
|
||||
if (k === 'geoip') {
|
||||
for (let n in rule[k]) {
|
||||
let tag = 'geoip-' + n;
|
||||
needed_rule_sets[tag] = 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-' + n + '.srs';
|
||||
push(new_rs, tag);
|
||||
}
|
||||
} else if (k === 'geosite') {
|
||||
for (let n in rule[k]) {
|
||||
let tag = 'geosite-' + n;
|
||||
needed_rule_sets[tag] = 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-' + n + '.srs';
|
||||
push(new_rs, tag);
|
||||
}
|
||||
} else {
|
||||
r[k] = rule[k];
|
||||
}
|
||||
}
|
||||
// Merge new rule_set tags with any existing ones
|
||||
let existing_rs = type(r.rule_set) === 'array' ? r.rule_set :
|
||||
(r.rule_set ? [r.rule_set] : []);
|
||||
for (let t in new_rs) push(existing_rs, t);
|
||||
r.rule_set = existing_rs;
|
||||
return r;
|
||||
};
|
||||
|
||||
if (cfg.route && cfg.route.rules)
|
||||
cfg.route.rules = map(cfg.route.rules, migrate_geo_rule);
|
||||
if (cfg.dns && cfg.dns.rules)
|
||||
cfg.dns.rules = map(cfg.dns.rules, migrate_geo_rule);
|
||||
|
||||
// download_detour must be DIRECT: at first start rule-sets aren't loaded, so
|
||||
// routing it via a proxy deadlocks (DNS recursion -> sing-box FATAL).
|
||||
let pick_dl_detour = function() {
|
||||
if (cfg.outbounds) {
|
||||
for (let ob in cfg.outbounds) {
|
||||
if (ob && ob.type === 'direct' && ob.tag) return ob.tag;
|
||||
}
|
||||
}
|
||||
return 'DIRECT';
|
||||
};
|
||||
|
||||
// Add rule_set download entries for each referenced geo tag
|
||||
if (length(keys(needed_rule_sets)) > 0) {
|
||||
if (!cfg.route) cfg.route = {};
|
||||
if (!cfg.route.rule_set) cfg.route.rule_set = [];
|
||||
let existing_tags = {};
|
||||
for (let rs in cfg.route.rule_set) existing_tags[rs.tag] = true;
|
||||
let dl_detour = pick_dl_detour();
|
||||
for (let tag in keys(needed_rule_sets)) {
|
||||
if (!existing_tags[tag]) {
|
||||
push(cfg.route.rule_set, {
|
||||
tag: tag,
|
||||
type: 'remote',
|
||||
format: 'binary',
|
||||
url: needed_rule_sets[tag],
|
||||
download_detour: dl_detour
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Remove deprecated 'outbound' DNS rule items (fatal in sing-box 1.13) ---
|
||||
// Replacement: set route.default_domain_resolver to the plain IP-based resolver
|
||||
let dns_resolver_tag = '';
|
||||
if (cfg.dns && cfg.dns.servers) {
|
||||
for (let srv in cfg.dns.servers) {
|
||||
if ((srv.type === 'udp' || srv.type === 'local') && srv.server &&
|
||||
match(srv.server, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
||||
dns_resolver_tag = srv.tag;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cfg.dns && cfg.dns.rules) {
|
||||
let filtered = [];
|
||||
for (let rule in cfg.dns.rules) {
|
||||
if (!rule.outbound) push(filtered, rule);
|
||||
}
|
||||
cfg.dns.rules = filtered;
|
||||
}
|
||||
// Set route.default_domain_resolver (replaces outbound:any DNS routing)
|
||||
if (dns_resolver_tag) {
|
||||
if (!cfg.route) cfg.route = {};
|
||||
if (!cfg.route.default_domain_resolver)
|
||||
cfg.route.default_domain_resolver = dns_resolver_tag;
|
||||
}
|
||||
|
||||
// --- Fix DNS servers with domain-name server field: add domain_resolver ---
|
||||
// sing-box 1.12+ requires domain_resolver when server= is a hostname (not IP)
|
||||
if (cfg.dns && cfg.dns.servers && dns_resolver_tag) {
|
||||
let domain_types = { https: 1, h3: 1, tls: 1, tcp: 1 };
|
||||
cfg.dns.servers = map(cfg.dns.servers, function(srv) {
|
||||
if (!domain_types[srv.type]) return srv;
|
||||
if (!srv.server || match(srv.server, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return srv;
|
||||
if (srv.domain_resolver) return srv;
|
||||
let r = {};
|
||||
for (let k in srv) r[k] = srv[k];
|
||||
r.domain_resolver = dns_resolver_tag;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Fix DNS rules referencing non-existent server tags → action: reject ---
|
||||
if (cfg.dns && cfg.dns.servers && cfg.dns.rules) {
|
||||
let valid_servers = {};
|
||||
for (let srv in cfg.dns.servers) valid_servers[srv.tag] = true;
|
||||
cfg.dns.rules = map(cfg.dns.rules, function(rule) {
|
||||
if (!rule.server || valid_servers[rule.server]) return rule;
|
||||
let r = {};
|
||||
for (let k in rule) if (k !== 'server' && k !== 'action') r[k] = rule[k];
|
||||
r.action = 'reject';
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Fix reject method: sing-box 1.14 rejects "dropped", only "default"/"drop" ---
|
||||
if (cfg.dns && cfg.dns.rules) {
|
||||
cfg.dns.rules = map(cfg.dns.rules, function(rule) {
|
||||
if (rule.action !== 'reject' || !rule.method) return rule;
|
||||
let m = rule.method;
|
||||
if (m === 'dropped') m = 'drop';
|
||||
if (m !== 'default' && m !== 'drop') {
|
||||
// unknown value: drop it, sing-box defaults to NXDOMAIN
|
||||
let r = {};
|
||||
for (let k in rule) if (k !== 'method') r[k] = rule[k];
|
||||
return r;
|
||||
}
|
||||
if (m === rule.method) return rule;
|
||||
let r = {};
|
||||
for (let k in rule) r[k] = rule[k];
|
||||
r.method = m;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Migrate TUN inet4_address/inet6_address -> address array (1.12+), and
|
||||
// strip legacy inbound fields removed in 1.13 (sniff / sniff_override_destination /
|
||||
// sniff_timeout / domain_strategy). If any inbound had sniff on, prepend a
|
||||
// { "action": "sniff" } route rule as the equivalent. ---
|
||||
let had_sniff = false;
|
||||
if (cfg.inbounds) {
|
||||
cfg.inbounds = map(cfg.inbounds, function(ib) {
|
||||
let r = {};
|
||||
let addrs = [];
|
||||
for (let k in ib) {
|
||||
if (k === 'inet4_address') {
|
||||
if (type(ib[k]) === 'array') { for (let a in ib[k]) push(addrs, a); }
|
||||
else push(addrs, ib[k]);
|
||||
} else if (k === 'inet6_address') {
|
||||
if (type(ib[k]) === 'array') { for (let a in ib[k]) push(addrs, a); }
|
||||
else push(addrs, ib[k]);
|
||||
} else if (k === 'sniff') {
|
||||
if (ib[k]) had_sniff = true;
|
||||
} else if (k === 'sniff_override_destination' || k === 'sniff_timeout' ||
|
||||
k === 'domain_strategy') {
|
||||
// drop — moved to route.rules actions in 1.13
|
||||
} else {
|
||||
r[k] = ib[k];
|
||||
}
|
||||
}
|
||||
if (length(addrs) > 0) r.address = addrs;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
if (had_sniff) {
|
||||
if (!cfg.route) cfg.route = {};
|
||||
if (!cfg.route.rules) cfg.route.rules = [];
|
||||
let has_sniff_rule = false;
|
||||
for (let rl in cfg.route.rules)
|
||||
if (rl.action === 'sniff') { has_sniff_rule = true; break; }
|
||||
if (!has_sniff_rule) {
|
||||
let new_rules = [ { action: 'sniff' } ];
|
||||
for (let rl in cfg.route.rules) push(new_rules, rl);
|
||||
cfg.route.rules = new_rules;
|
||||
}
|
||||
}
|
||||
|
||||
// --- sing-box 1.14 requires route.default_domain_resolver; add one if missing,
|
||||
// preferring dns.final, else the first dns.servers entry with a tag. ---
|
||||
if (cfg.route && !cfg.route.default_domain_resolver) {
|
||||
let resolver_tag = '';
|
||||
if (cfg.dns && cfg.dns.final) resolver_tag = cfg.dns.final;
|
||||
else if (cfg.dns && cfg.dns.servers) {
|
||||
for (let srv in cfg.dns.servers)
|
||||
if (srv.tag) { resolver_tag = srv.tag; break; }
|
||||
}
|
||||
if (resolver_tag)
|
||||
cfg.route.default_domain_resolver = { server: resolver_tag };
|
||||
}
|
||||
|
||||
// --- Remove dangling outbound refs from selectors/urltest, and drop airline
|
||||
// pseudo-nodes. Two cases: (1) subconverter may reference a missing tag (e.g.
|
||||
// REJECT); (2) "Traffic:.../Expire:.../quota" pseudo-nodes are real SS/Vmess
|
||||
// outbounds (kept so the UI can read traffic/expiry) but don't forward — in a
|
||||
// selector/urltest they win (0ms) and swallow all foreign traffic. ---
|
||||
let is_pseudo_tag = function(t) {
|
||||
if (!t) return false;
|
||||
return match(t, /^Traffic[::]/) ||
|
||||
match(t, /^Expire[::]/) ||
|
||||
match(t, /剩余流量|剩余[::]/) ||
|
||||
match(t, /距离下次重置/) ||
|
||||
match(t, /到期(时间|日期)?[::]/) ||
|
||||
match(t, /官网[::]|网站[::]|套餐[::]?|客服[::]/) ||
|
||||
match(t, /QQ[群]?[::]/) ||
|
||||
match(t, /Telegram|TG群|官方群/) ||
|
||||
match(t, /续费|订阅地址|流量重置/);
|
||||
};
|
||||
if (cfg.outbounds) {
|
||||
let defined_tags = {};
|
||||
for (let ob in cfg.outbounds) defined_tags[ob.tag] = true;
|
||||
cfg.outbounds = map(cfg.outbounds, function(ob) {
|
||||
if ((ob.type !== 'selector' && ob.type !== 'urltest' && ob.type !== 'loadbalance') ||
|
||||
!ob.outbounds) return ob;
|
||||
let valid = [];
|
||||
for (let t in ob.outbounds) {
|
||||
if (!defined_tags[t]) continue;
|
||||
if (is_pseudo_tag(t)) continue;
|
||||
push(valid, t);
|
||||
}
|
||||
if (length(valid) === length(ob.outbounds)) return ob;
|
||||
let r = {};
|
||||
for (let k in ob) r[k] = ob[k];
|
||||
r.outbounds = valid;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Force download_detour=DIRECT on every remote rule_set ---
|
||||
// subconverter often emits download_detour="auto", but the proxy isn't ready at
|
||||
// first start -> urltest deadlock ("fetch rule-set: deadline exceeded") -> FATAL.
|
||||
// The srs URLs already use a CN-reachable mirror, so DIRECT is always correct.
|
||||
if (cfg.route && cfg.route.rule_set) {
|
||||
let dl_detour = pick_dl_detour();
|
||||
cfg.route.rule_set = map(cfg.route.rule_set, function(rs) {
|
||||
if (!rs || rs.type !== 'remote') return rs;
|
||||
let r = {};
|
||||
for (let k in rs) r[k] = rs[k];
|
||||
r.download_detour = dl_detour;
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
|
||||
// ---- CLI entry: ucode migrate_singbox.uc <config.json> ----
|
||||
let _path = ARGV[0] || '';
|
||||
if (!_path) { print("missing path\n"); exit(1); }
|
||||
let _raw = readfile(_path);
|
||||
if (!_raw) { print("read failed\n"); exit(1); }
|
||||
let _cfg = json(_raw);
|
||||
if (!_cfg) { print("json parse failed\n"); exit(1); }
|
||||
let _before = sprintf('%J', _cfg);
|
||||
_cfg = migrate_singbox(_cfg);
|
||||
let _after = sprintf('%J', _cfg);
|
||||
if (_before === _after) { print("nochange\n"); exit(0); }
|
||||
if (writefile(_path, _after) === null) { print("write failed\n"); exit(1); }
|
||||
print("migrated\n");
|
||||
@@ -0,0 +1,939 @@
|
||||
#!/usr/bin/ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile, access, popen } from 'fs';
|
||||
|
||||
let path = ARGV[0] || '';
|
||||
let redir_port = +(ARGV[1] || '7891');
|
||||
let tproxy_port = +(ARGV[2] || '7982');
|
||||
let mixed_port = +(ARGV[3] || '7890');
|
||||
let has_tun_device = (ARGV[4] || '1') == '1';
|
||||
if (has_tun_device && system('(ip tuntap add mode tun name cotuntest >/dev/null 2>&1 && ip link del cotuntest >/dev/null 2>&1)') != 0)
|
||||
has_tun_device = false;
|
||||
|
||||
// default 6666 must match fw4.sh CORE_ROUTING_MARK (0x1a0a)
|
||||
let routing_mark = +(ARGV[5] || '6666');
|
||||
let dns_port = +(ARGV[6] || '1053');
|
||||
let dash_port = +(ARGV[7] || '9090');
|
||||
let dash_secret = ARGV[8] != null ? (ARGV[8] + '') : '';
|
||||
|
||||
if (!path) {
|
||||
print("missing path\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let raw = readfile(path);
|
||||
if (!raw) {
|
||||
print("read failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let cfg = json(raw);
|
||||
if (!cfg) {
|
||||
print("json parse failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function s_len(s) {
|
||||
return length(s || '');
|
||||
}
|
||||
|
||||
function s_sub(s, start, count) {
|
||||
if (count == null)
|
||||
return substr(s || '', start);
|
||||
return substr(s || '', start, count);
|
||||
}
|
||||
|
||||
function is_space(ch) {
|
||||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
|
||||
}
|
||||
|
||||
function trim_s(s) {
|
||||
s = (s == null) ? '' : (s + '');
|
||||
while (s_len(s) > 0 && is_space(s_sub(s, 0, 1)))
|
||||
s = s_sub(s, 1);
|
||||
while (s_len(s) > 0 && is_space(s_sub(s, s_len(s) - 1, 1)))
|
||||
s = s_sub(s, 0, s_len(s) - 1);
|
||||
return s;
|
||||
}
|
||||
|
||||
let default_interface = '';
|
||||
let _default_if_pipe = popen("ip -4 route show default 2>/dev/null | awk '{print $5; exit}'");
|
||||
if (_default_if_pipe) {
|
||||
default_interface = trim_s(_default_if_pipe.read('all'));
|
||||
_default_if_pipe.close();
|
||||
}
|
||||
|
||||
function starts_with(s, prefix) {
|
||||
return s_sub(s || '', 0, s_len(prefix)) == prefix;
|
||||
}
|
||||
|
||||
function find_char(s, ch) {
|
||||
for (let i = 0; i < s_len(s); i++)
|
||||
if (s_sub(s, i, 1) == ch)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function find_last_char(s, ch) {
|
||||
for (let i = s_len(s) - 1; i >= 0; i--)
|
||||
if (s_sub(s, i, 1) == ch)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
function split_uci_words(line) {
|
||||
let out = [], cur = '', quote = '', esc = false;
|
||||
for (let i = 0; i < s_len(line); i++) {
|
||||
let ch = s_sub(line, i, 1);
|
||||
if (esc) {
|
||||
cur += ch;
|
||||
esc = false;
|
||||
continue;
|
||||
}
|
||||
if (ch == '\\') {
|
||||
esc = true;
|
||||
continue;
|
||||
}
|
||||
if (quote) {
|
||||
if (ch == quote)
|
||||
quote = '';
|
||||
else
|
||||
cur += ch;
|
||||
continue;
|
||||
}
|
||||
if (ch == '"' || ch == "'") {
|
||||
quote = ch;
|
||||
continue;
|
||||
}
|
||||
if (is_space(ch)) {
|
||||
if (s_len(cur) > 0) {
|
||||
push(out, cur);
|
||||
cur = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
cur += ch;
|
||||
}
|
||||
if (s_len(cur) > 0)
|
||||
push(out, cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
function load_clashoo_uci() {
|
||||
let uci_path = ARGV[9] || '/etc/config/clashoo';
|
||||
let txt = readfile(uci_path) || '';
|
||||
let sections = [], cur = null;
|
||||
for (let line in split(txt, '\n')) {
|
||||
line = trim_s(line);
|
||||
if (!s_len(line) || starts_with(line, '#'))
|
||||
continue;
|
||||
let words = split_uci_words(line);
|
||||
if (!length(words))
|
||||
continue;
|
||||
if (words[0] == 'config') {
|
||||
cur = { type: words[1] || '', name: words[2] || '', options: {}, lists: {} };
|
||||
push(sections, cur);
|
||||
continue;
|
||||
}
|
||||
if (!cur || length(words) < 3)
|
||||
continue;
|
||||
if (words[0] == 'option')
|
||||
cur.options[words[1]] = words[2];
|
||||
else if (words[0] == 'list') {
|
||||
if (cur.lists[words[1]] == null)
|
||||
cur.lists[words[1]] = [];
|
||||
push(cur.lists[words[1]], words[2]);
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
let clashoo_uci = load_clashoo_uci();
|
||||
|
||||
function uci_config_section() {
|
||||
for (let s in clashoo_uci)
|
||||
if (s.type == 'clashoo' && (s.name == 'config' || s.name == ''))
|
||||
return s;
|
||||
return {};
|
||||
}
|
||||
|
||||
let uci_cfg = uci_config_section();
|
||||
|
||||
function uci_opt(key, def) {
|
||||
if (uci_cfg.options && uci_cfg.options[key] != null)
|
||||
return uci_cfg.options[key];
|
||||
return def;
|
||||
}
|
||||
|
||||
function uci_list(key) {
|
||||
if (uci_cfg.lists && uci_cfg.lists[key] != null)
|
||||
return uci_cfg.lists[key];
|
||||
if (uci_cfg.options && uci_cfg.options[key] != null)
|
||||
return [ uci_cfg.options[key] ];
|
||||
return [];
|
||||
}
|
||||
|
||||
function uci_sections(type_name) {
|
||||
let out = [];
|
||||
for (let s in clashoo_uci)
|
||||
if (s.type == type_name)
|
||||
push(out, s);
|
||||
return out;
|
||||
}
|
||||
|
||||
function opt_bool(v, def) {
|
||||
if (v == null || v == '')
|
||||
return def;
|
||||
return v === true || v == '1' || v == 'true' || v == 'yes' || v == 'on';
|
||||
}
|
||||
|
||||
function normalize_dns_uri(address, protocol, port) {
|
||||
address = trim_s(address || '');
|
||||
protocol = trim_s(protocol || '');
|
||||
port = trim_s(port || '');
|
||||
if (!s_len(address))
|
||||
return '';
|
||||
if (find_char(address, ':') >= 0 && find_char(address, '/') >= 0)
|
||||
return address;
|
||||
if (protocol == 'none')
|
||||
protocol = '';
|
||||
if (protocol == 'dot')
|
||||
protocol = 'tls://';
|
||||
else if (protocol == 'doh')
|
||||
protocol = 'https://';
|
||||
else if (protocol == 'doq')
|
||||
protocol = 'quic://';
|
||||
if (s_len(protocol) && !starts_with(protocol, 'udp://') && !starts_with(protocol, 'tcp://') &&
|
||||
!starts_with(protocol, 'tls://') && !starts_with(protocol, 'https://') && !starts_with(protocol, 'quic://'))
|
||||
protocol += '://';
|
||||
return protocol + address + (s_len(port) ? ':' + port : '');
|
||||
}
|
||||
|
||||
function direct_outbound_tag() {
|
||||
for (let ob in (cfg.outbounds || []))
|
||||
if (ob && ob.type == 'direct' && s_len(ob.tag || ''))
|
||||
return ob.tag;
|
||||
return 'DIRECT';
|
||||
}
|
||||
|
||||
function dns_server_obj(uri, tag, fallback_type) {
|
||||
uri = normalize_dns_uri(uri || '', '', '');
|
||||
if (!s_len(uri))
|
||||
return null;
|
||||
let scheme = fallback_type || 'udp';
|
||||
let rest = uri;
|
||||
let p = -1;
|
||||
for (let i = 0; i < s_len(uri) - 2; i++) {
|
||||
if (s_sub(uri, i, 3) == '://') {
|
||||
p = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (p >= 0) {
|
||||
scheme = s_sub(uri, 0, p);
|
||||
rest = s_sub(uri, p + 3);
|
||||
}
|
||||
if (scheme == 'dot')
|
||||
scheme = 'tls';
|
||||
else if (scheme == 'doh')
|
||||
scheme = 'https';
|
||||
else if (scheme == 'doq')
|
||||
scheme = 'quic';
|
||||
|
||||
let path = '';
|
||||
let slash = find_char(rest, '/');
|
||||
if (slash >= 0) {
|
||||
path = s_sub(rest, slash);
|
||||
rest = s_sub(rest, 0, slash);
|
||||
}
|
||||
|
||||
let server = rest, server_port = null;
|
||||
let colon = find_last_char(rest, ':');
|
||||
if (colon > 0 && find_char(rest, ']') < 0) {
|
||||
server = s_sub(rest, 0, colon);
|
||||
let n = +(s_sub(rest, colon + 1));
|
||||
if (n === n)
|
||||
server_port = n;
|
||||
}
|
||||
|
||||
let obj = { type: scheme, tag: tag, server: server };
|
||||
if (server_port != null)
|
||||
obj.server_port = server_port;
|
||||
if (path && (scheme == 'https' || scheme == 'h3'))
|
||||
obj.path = path;
|
||||
if ((scheme == 'https' || scheme == 'tls' || scheme == 'quic') && tag != 'dns_resolver')
|
||||
obj.domain_resolver = 'dns_resolver';
|
||||
/* sing-box 1.12+ DNS servers detour to the proxy by default, so a server
|
||||
* resolving its own domain via dns_resolver loops -> "deadline exceeded".
|
||||
* Force direct/resolver servers to DIRECT; only dns_proxy may use the proxy. */
|
||||
if (tag == 'dns_resolver' || tag == 'dns_direct' || tag == 'dns_foreign')
|
||||
obj.detour = direct_outbound_tag();
|
||||
return obj;
|
||||
}
|
||||
|
||||
function dns_servers_by_role(role) {
|
||||
let out = [];
|
||||
for (let s in uci_sections('dnsservers')) {
|
||||
if (!opt_bool(s.options.enabled, true))
|
||||
continue;
|
||||
if ((s.options.ser_type || 'nameserver') != role)
|
||||
continue;
|
||||
let uri = normalize_dns_uri(s.options.ser_address || '', s.options.protocol || '', s.options.ser_port || '');
|
||||
if (s_len(uri))
|
||||
push(out, uri);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function first_or(arr, fallback) {
|
||||
return length(arr) ? arr[0] : fallback;
|
||||
}
|
||||
|
||||
function local_rule_set_path(tag) {
|
||||
if (!s_len(tag))
|
||||
return '';
|
||||
let path = '/usr/share/clashoo/ruleset/' + tag + '.srs';
|
||||
if (access(path, 'r'))
|
||||
return path;
|
||||
if (tag == 'geolocation-cn' || tag == 'cn') {
|
||||
path = '/usr/share/clashoo/ruleset/geosite-cn.srs';
|
||||
if (access(path, 'r'))
|
||||
return path;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function keep_remote_rule_set(rs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function normalize_rule_set_url(url) {
|
||||
url = url || '';
|
||||
url = replace(url, /^https:\/\/gh-proxy\.com\//, '');
|
||||
let m = match(url, /^https:\/\/raw\.githubusercontent\.com\/([^\/]+)\/([^\/]+)\/([^\/]+)\/(.+)$/);
|
||||
if (m)
|
||||
return 'https://cdn.jsdelivr.net/gh/' + m[1] + '/' + m[2] + '@' + m[3] + '/' + m[4];
|
||||
m = match(url, /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/raw\/refs\/heads\/([^\/]+)\/(.+)$/);
|
||||
if (m)
|
||||
return 'https://cdn.jsdelivr.net/gh/' + m[1] + '/' + m[2] + '@' + m[3] + '/' + m[4];
|
||||
return url;
|
||||
}
|
||||
|
||||
function has_route_rule_set(tag) {
|
||||
for (let rs in (cfg.route || {}).rule_set || [])
|
||||
if (rs && rs.tag == tag)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function add_remote_rule_set(tag, url) {
|
||||
cfg.route = cfg.route || {};
|
||||
cfg.route.rule_set = cfg.route.rule_set || [];
|
||||
if (has_route_rule_set(tag))
|
||||
return;
|
||||
push(cfg.route.rule_set, {
|
||||
tag: tag,
|
||||
type: 'remote',
|
||||
format: 'binary',
|
||||
url: url
|
||||
});
|
||||
}
|
||||
|
||||
function array_has_value(arr, val) {
|
||||
if (type(arr) != 'array')
|
||||
return false;
|
||||
for (let item in arr)
|
||||
if (item == val)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
let dnsmasq_uid = null;
|
||||
let _dnsmasq_uid_pipe = popen("id -u dnsmasq 2>/dev/null");
|
||||
if (_dnsmasq_uid_pipe) {
|
||||
let value = trim_s(_dnsmasq_uid_pipe.read('all'));
|
||||
_dnsmasq_uid_pipe.close();
|
||||
if (match(value, /^[0-9]+$/))
|
||||
dnsmasq_uid = +value;
|
||||
}
|
||||
|
||||
function ensure_tun_dnsmasq_exclude(ib) {
|
||||
if (dnsmasq_uid == null)
|
||||
return;
|
||||
let excludes = ib.exclude_uid;
|
||||
if (type(excludes) != 'array')
|
||||
excludes = excludes != null ? [ excludes ] : [];
|
||||
if (!array_has_value(excludes, dnsmasq_uid))
|
||||
push(excludes, dnsmasq_uid);
|
||||
ib.exclude_uid = excludes;
|
||||
}
|
||||
|
||||
function ensure_tun_cn_exclude(ib) {
|
||||
let excludes = ib.route_exclude_address_set;
|
||||
if (type(excludes) != 'array')
|
||||
excludes = excludes ? [ excludes ] : [];
|
||||
if (!array_has_value(excludes, 'cn-ip'))
|
||||
push(excludes, 'cn-ip');
|
||||
ib.route_exclude_address_set = excludes;
|
||||
}
|
||||
|
||||
function prune_tun_cn_exclude_if_missing() {
|
||||
if (has_route_rule_set('cn-ip'))
|
||||
return;
|
||||
for (let ib in cfg.inbounds || []) {
|
||||
if (!ib || ib.type != 'tun')
|
||||
continue;
|
||||
let excludes = [];
|
||||
for (let item in ib.route_exclude_address_set || [])
|
||||
if (item != 'cn-ip')
|
||||
push(excludes, item);
|
||||
if (length(excludes))
|
||||
ib.route_exclude_address_set = excludes;
|
||||
else
|
||||
delete ib.route_exclude_address_set;
|
||||
}
|
||||
}
|
||||
|
||||
function matcher_rule(matcher, server_tag) {
|
||||
let r = { server: server_tag };
|
||||
if (starts_with(matcher, 'geosite:'))
|
||||
r.rule_set = s_sub(matcher, 8);
|
||||
else if (starts_with(matcher, 'rule_set:'))
|
||||
r.rule_set = s_sub(matcher, 9);
|
||||
else if (starts_with(matcher, 'domain:'))
|
||||
r.domain = s_sub(matcher, 7);
|
||||
else if (starts_with(matcher, 'domain-suffix:'))
|
||||
r.domain_suffix = s_sub(matcher, 14);
|
||||
else
|
||||
r.domain_suffix = matcher;
|
||||
return r;
|
||||
}
|
||||
|
||||
function apply_dns_from_uci() {
|
||||
cfg.dns = cfg.dns || {};
|
||||
/* strip mihomo-style DNS fields (enable/ipv6/listen/fake-ip-filter/nameserver...)
|
||||
* that leak in from YAML conversion; sing-box rejects them (Fatal). */
|
||||
let _mihomo_dns_fields = ['enable', 'ipv6', 'listen', 'fake-ip-filter', 'fake-ip-range',
|
||||
'enhanced-mode', 'nameserver', 'fallback', 'fallback-filter',
|
||||
'use-hosts', 'default-nameserver', 'proxy-server-nameserver',
|
||||
'direct-nameserver', 'nameserver-policy'];
|
||||
for (let _i = 0; _i < length(_mihomo_dns_fields); _i++)
|
||||
delete cfg.dns[_mihomo_dns_fields[_i]];
|
||||
/* strip mihomo-style experimental fields */
|
||||
let _mihomo_exp = ['sniff-tls-sni', 'sniff', 'sniffer'];
|
||||
for (let _me = 0; _me < length(_mihomo_exp); _me++)
|
||||
if (cfg.experimental && cfg.experimental[_mihomo_exp[_me]] != null)
|
||||
delete cfg.experimental[_mihomo_exp[_me]];
|
||||
/* strip non-sing-box root-level fields */
|
||||
let _mihomo_root = ['clash-for-android', 'cfw-bypass', 'sniffer', 'profile',
|
||||
'geodata-mode', 'geodata-loader', 'geox-url', 'geo-auto-update',
|
||||
'geo-update-interval', 'tun', 'ipv6', 'interface-name',
|
||||
'port', 'socks-port', 'mixed-port', 'redir-port', 'tproxy-port', 'mode', 'allow-lan', 'log-level', 'external-controller', 'secret', 'bind-address', 'routing-mark', 'find-process-mode', 'tcp-concurrent', 'unified-delay',
|
||||
'keep-alive-interval', 'keep-alive-idle', 'disable-keep-alive'];
|
||||
/* strip subconverter note fields and non-standard keys */
|
||||
let _subconv_fields = ['_note', '_sub_url', 'hosts', 'script', 'enable', 'fake-ip-filter', 'fake-ip-range'];
|
||||
for (let _sf = 0; _sf < length(_subconv_fields); _sf++)
|
||||
if (cfg[_subconv_fields[_sf]] != null)
|
||||
delete cfg[_subconv_fields[_sf]];
|
||||
for (let _mr = 0; _mr < length(_mihomo_root); _mr++)
|
||||
if (cfg[_mihomo_root[_mr]] != null)
|
||||
delete cfg[_mihomo_root[_mr]];
|
||||
let bootstrap = uci_list('default_nameserver');
|
||||
if (!length(bootstrap))
|
||||
bootstrap = uci_list('defaul_nameserver');
|
||||
let resolver_uri = first_or(bootstrap, '223.5.5.5');
|
||||
let direct_uri = first_or(dns_servers_by_role('direct-nameserver'), first_or(dns_servers_by_role('proxy-server-nameserver'), 'https://doh.pub/dns-query'));
|
||||
let proxy_uri = first_or(dns_servers_by_role('nameserver'), first_or(dns_servers_by_role('fallback'), 'https://1.1.1.1/dns-query'));
|
||||
|
||||
let servers = [];
|
||||
push(servers, dns_server_obj(resolver_uri, 'dns_resolver', 'udp'));
|
||||
push(servers, dns_server_obj(direct_uri, 'dns_direct', 'udp'));
|
||||
push(servers, dns_server_obj(proxy_uri, 'dns_proxy', 'tls'));
|
||||
|
||||
let enhanced = uci_opt('enhanced_mode', 'fake-ip');
|
||||
if (enhanced == 'fake-ip') {
|
||||
let fake_range = uci_opt('fake_ip_range', '198.18.0.1/16');
|
||||
push(servers, {
|
||||
type: 'fakeip',
|
||||
tag: 'dns_fakeip',
|
||||
inet4_range: fake_range || '198.18.0.1/16',
|
||||
inet6_range: 'fc00::/18'
|
||||
});
|
||||
}
|
||||
|
||||
let rules = [];
|
||||
push(servers, { type: 'udp', tag: 'dns_foreign', server: '1.1.1.1' });
|
||||
if (enhanced == 'fake-ip') {
|
||||
push(rules, {
|
||||
rule_set: 'geolocation-!cn',
|
||||
server: 'dns_fakeip'
|
||||
});
|
||||
}
|
||||
|
||||
let clean_servers = [];
|
||||
for (let s in servers)
|
||||
if (s)
|
||||
push(clean_servers, s);
|
||||
cfg.dns.servers = clean_servers;
|
||||
cfg.dns.rules = rules;
|
||||
|
||||
/* Keep bootstrap direct; proxy unmatched queries in strict mode. */
|
||||
if (opt_bool(uci_opt('dns_leak_protect', '0'), false)) {
|
||||
cfg.dns.final = 'dns_proxy';
|
||||
if (!opt_bool(uci_opt('ipv6_proxy', '0'), false))
|
||||
unshift(cfg.dns.rules, { query_type: ['AAAA'], action: 'reject', method: 'drop' });
|
||||
} else
|
||||
cfg.dns.final = 'dns_direct';
|
||||
|
||||
let ecs = trim_s(uci_opt('dns_ecs', ''));
|
||||
if (s_len(ecs))
|
||||
cfg.dns.client_subnet = ecs;
|
||||
else
|
||||
delete cfg.dns.client_subnet;
|
||||
|
||||
if (opt_bool(uci_opt('singbox_independent_cache', '0'), false))
|
||||
cfg.dns.independent_cache = true;
|
||||
else
|
||||
delete cfg.dns.independent_cache;
|
||||
|
||||
/* sing-box 1.14 requires this (else Fatal); dns_resolver is DIRECT so first-start node lookups use direct DNS */
|
||||
cfg.route.default_domain_resolver = 'dns_resolver';
|
||||
}
|
||||
|
||||
let inbounds = cfg.inbounds || [];
|
||||
let normalized = [];
|
||||
let has_redirect = false;
|
||||
let has_tproxy = false;
|
||||
let has_mixed = false;
|
||||
let has_tun = false;
|
||||
let has_dns_in = false;
|
||||
let wants_tun = has_tun_device && (uci_opt('tcp_mode', '') == 'tun' || uci_opt('udp_mode', '') == 'tun');
|
||||
let tun_stack = uci_opt('stack', 'mixed') || 'mixed';
|
||||
|
||||
if (wants_tun) {
|
||||
add_remote_rule_set('cn-ip',
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/cn.srs');
|
||||
}
|
||||
|
||||
for (let ib in inbounds) {
|
||||
if (!ib)
|
||||
continue;
|
||||
|
||||
if (ib.type == 'tun' || ib.tag == 'tun-in') {
|
||||
/* Keep tun inbound only when the selected proxy mode actually uses TUN. */
|
||||
if (wants_tun && !has_tun) {
|
||||
ib.type = 'tun';
|
||||
ib.tag = ib.tag || 'tun-in';
|
||||
if (!ib.address)
|
||||
ib.address = [ '172.19.0.1/30', 'fdfe:dcba:9876::1/126' ];
|
||||
ib.auto_route = true;
|
||||
ib.auto_redirect = true;
|
||||
ib.auto_redirect_input_mark = '0x2023';
|
||||
ib.auto_redirect_output_mark = '0x2024';
|
||||
ib.strict_route = true;
|
||||
ib.stack = tun_stack;
|
||||
ensure_tun_dnsmasq_exclude(ib);
|
||||
ensure_tun_cn_exclude(ib);
|
||||
push(normalized, ib);
|
||||
has_tun = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ib.tag == 'redirect-in' || ib.type == 'redirect') {
|
||||
if (has_redirect)
|
||||
continue;
|
||||
ib.type = 'redirect';
|
||||
ib.tag = 'redirect-in';
|
||||
ib.listen = '0.0.0.0';
|
||||
ib.listen_port = redir_port;
|
||||
has_redirect = true;
|
||||
push(normalized, ib);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ib.tag == 'tproxy-in' || ib.type == 'tproxy') {
|
||||
if (has_tproxy)
|
||||
continue;
|
||||
ib.type = 'tproxy';
|
||||
ib.tag = 'tproxy-in';
|
||||
ib.listen = '0.0.0.0';
|
||||
ib.listen_port = tproxy_port;
|
||||
ib.network = 'udp';
|
||||
has_tproxy = true;
|
||||
push(normalized, ib);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ib.tag == 'mixed-in' || ib.type == 'mixed') {
|
||||
if (has_mixed)
|
||||
continue;
|
||||
ib.type = 'mixed';
|
||||
ib.tag = 'mixed-in';
|
||||
ib.listen = '0.0.0.0';
|
||||
ib.listen_port = mixed_port;
|
||||
has_mixed = true;
|
||||
push(normalized, ib);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ib.tag == 'dns-in') {
|
||||
if (has_dns_in)
|
||||
continue;
|
||||
ib.type = 'direct';
|
||||
ib.tag = 'dns-in';
|
||||
ib.listen = '0.0.0.0';
|
||||
ib.listen_port = dns_port;
|
||||
has_dns_in = true;
|
||||
push(normalized, ib);
|
||||
continue;
|
||||
}
|
||||
|
||||
push(normalized, ib);
|
||||
}
|
||||
|
||||
if (!has_redirect) {
|
||||
push(normalized, {
|
||||
type: 'redirect',
|
||||
tag: 'redirect-in',
|
||||
listen: '0.0.0.0',
|
||||
listen_port: redir_port
|
||||
});
|
||||
}
|
||||
|
||||
if (!has_mixed) {
|
||||
push(normalized, {
|
||||
type: 'mixed',
|
||||
tag: 'mixed-in',
|
||||
listen: '0.0.0.0',
|
||||
listen_port: mixed_port
|
||||
});
|
||||
}
|
||||
|
||||
if (wants_tun && !has_tun) {
|
||||
let tun_ib = {
|
||||
type: 'tun',
|
||||
tag: 'tun-in',
|
||||
address: [ '172.19.0.1/30', 'fdfe:dcba:9876::1/126' ],
|
||||
auto_route: true,
|
||||
auto_redirect: true,
|
||||
auto_redirect_input_mark: '0x2023',
|
||||
auto_redirect_output_mark: '0x2024',
|
||||
strict_route: true,
|
||||
stack: tun_stack
|
||||
};
|
||||
ensure_tun_dnsmasq_exclude(tun_ib);
|
||||
ensure_tun_cn_exclude(tun_ib);
|
||||
push(normalized, tun_ib);
|
||||
}
|
||||
|
||||
if (!has_tproxy) {
|
||||
push(normalized, {
|
||||
type: 'tproxy',
|
||||
tag: 'tproxy-in',
|
||||
listen: '0.0.0.0',
|
||||
listen_port: tproxy_port,
|
||||
network: 'udp'
|
||||
});
|
||||
}
|
||||
|
||||
if (!has_dns_in) {
|
||||
push(normalized, {
|
||||
type: 'direct',
|
||||
tag: 'dns-in',
|
||||
listen: '0.0.0.0',
|
||||
listen_port: dns_port
|
||||
});
|
||||
}
|
||||
|
||||
cfg.inbounds = normalized;
|
||||
|
||||
for (let ob in (cfg.outbounds || [])) {
|
||||
if (!ob || type(ob) != 'object')
|
||||
continue;
|
||||
|
||||
let t = ob.type || '';
|
||||
if (t == 'selector' || t == 'urltest' || t == 'fallback' || t == 'load_balance' || t == 'dns' || t == 'block')
|
||||
continue;
|
||||
|
||||
/* Clash SS obfs is plugin=obfs + plugin-opts object; sing-box wants
|
||||
* plugin=obfs-local + "obfs=http;obfs-host=..." string. */
|
||||
if (ob.plugin == 'obfs')
|
||||
ob.plugin = 'obfs-local';
|
||||
if (ob.plugin_opts != null && type(ob.plugin_opts) == 'object') {
|
||||
let _parts = [];
|
||||
let _map = { mode: 'obfs', host: 'obfs-host', uri: 'obfs-uri' };
|
||||
for (let _k in ob.plugin_opts) {
|
||||
let _v = ob.plugin_opts[_k];
|
||||
if (type(_v) == 'object' || type(_v) == 'array') continue;
|
||||
push(_parts, (_map[_k] || _k) + '=' + _v);
|
||||
}
|
||||
ob.plugin_opts = join(';', _parts);
|
||||
}
|
||||
if (ob.plugin != null && ob.plugin != 'obfs-local' && ob.plugin != 'v2ray-plugin' && ob.plugin != 'shadow-tls') {
|
||||
delete ob.plugin;
|
||||
delete ob.plugin_opts;
|
||||
}
|
||||
|
||||
/* Force ipv4_only on DIRECT outbound: most devices are IPv4-only egress,
|
||||
* a CN domain resolving AAAA stalls on "network is unreachable". */
|
||||
if (ob.type == 'direct' && ob.tag == 'DIRECT')
|
||||
ob.domain_resolver = { server: 'dns_resolver', strategy: 'ipv4_only' };
|
||||
|
||||
if (wants_tun) {
|
||||
delete ob.routing_mark;
|
||||
if (ob.type == 'direct' && ob.tag == 'DIRECT' && default_interface)
|
||||
ob.bind_interface = ob.bind_interface || default_interface;
|
||||
} else if (ob.routing_mark == null)
|
||||
ob.routing_mark = routing_mark;
|
||||
}
|
||||
|
||||
/* Drop airline pseudo-nodes (Traffic:/Expire:/quota/官网/QQ/etc.) from selector/urltest lists.
|
||||
* They're real SS/Vmess (kept so the UI reads traffic/expiry) but don't forward; in a
|
||||
* selector/urltest they win and swallow foreign traffic ("CN ok, foreign out"). */
|
||||
let _is_pseudo_tag = function(t) {
|
||||
if (!t) return false;
|
||||
return match(t, /^Traffic[::]/) || match(t, /^Expire[::]/) ||
|
||||
match(t, /剩余流量|剩余[::]/) || match(t, /距离下次重置/) ||
|
||||
match(t, /到期(时间|日期)?[::]/) ||
|
||||
match(t, /官网[::]|网站[::]|套餐[::]?|客服[::]/) ||
|
||||
match(t, /QQ[群]?[::]/) || match(t, /Telegram|TG群|官方群/) ||
|
||||
match(t, /续费|订阅地址|流量重置/);
|
||||
};
|
||||
for (let ob in (cfg.outbounds || [])) {
|
||||
if (!ob || type(ob) != 'object') continue;
|
||||
let t = ob.type || '';
|
||||
if (t != 'selector' && t != 'urltest' && t != 'fallback' && t != 'load_balance') continue;
|
||||
if (type(ob.outbounds) != 'array') continue;
|
||||
let cleaned = [];
|
||||
for (let tag in ob.outbounds) if (!_is_pseudo_tag(tag)) push(cleaned, tag);
|
||||
ob.outbounds = cleaned;
|
||||
}
|
||||
|
||||
cfg.route = cfg.route || {};
|
||||
cfg.route.rules = cfg.route.rules || [];
|
||||
let has_dns_hijack = false;
|
||||
for (let rule in cfg.route.rules) {
|
||||
if (!rule || type(rule) != 'object')
|
||||
continue;
|
||||
if (rule.inbound == 'dns-in' && rule.action == 'hijack-dns') {
|
||||
has_dns_hijack = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_dns_hijack) {
|
||||
unshift(cfg.route.rules, {
|
||||
inbound: 'dns-in',
|
||||
action: 'hijack-dns'
|
||||
});
|
||||
}
|
||||
cfg.route.auto_detect_interface = true;
|
||||
apply_dns_from_uci();
|
||||
|
||||
/* DNS leak protection: reject DoT/DoQ (853) to force DNS through the core; after hijack-dns so the dns inbound survives */
|
||||
if (opt_bool(uci_opt('dns_leak_protect', '0'), false)) {
|
||||
let _has_853 = false;
|
||||
for (let _r in cfg.route.rules) {
|
||||
if (_r && type(_r) == 'object' && _r.action == 'reject' && _r.port) {
|
||||
if (type(_r.port) == 'array') {
|
||||
for (let _p in _r.port) if (_p == 853) { _has_853 = true; break; }
|
||||
} else if (_r.port == 853) {
|
||||
_has_853 = true;
|
||||
}
|
||||
}
|
||||
if (_has_853) break;
|
||||
}
|
||||
if (!_has_853) {
|
||||
let _new_rules = [];
|
||||
let _inserted = false;
|
||||
for (let _ri = 0; _ri < length(cfg.route.rules); _ri++) {
|
||||
push(_new_rules, cfg.route.rules[_ri]);
|
||||
if (!_inserted && cfg.route.rules[_ri] && cfg.route.rules[_ri].action == 'hijack-dns') {
|
||||
push(_new_rules, { port: [853], action: 'reject' });
|
||||
_inserted = true;
|
||||
}
|
||||
}
|
||||
if (!_inserted) unshift(_new_rules, { port: [853], action: 'reject' });
|
||||
cfg.route.rules = _new_rules;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Custom routing rules (UCI: config addtype) -> route.rules, mirrors mihomo iprules.sh =====
|
||||
* User "domain/IP -> direct/proxy" rules from LuCI, inserted after the DNS funnel
|
||||
* (hijack-dns / 853-reject) and before other rules, so overrides win without bypassing it. */
|
||||
let _proxy_outbound_tag = function() {
|
||||
for (let ob in (cfg.outbounds || []))
|
||||
if (ob && ob.tag == '🚀 节点选择' && (ob.type == 'selector' || ob.type == 'urltest'))
|
||||
return ob.tag;
|
||||
for (let ob in (cfg.outbounds || []))
|
||||
if (ob && ob.type == 'selector' && s_len(ob.tag || '') && !_is_pseudo_tag(ob.tag))
|
||||
return ob.tag;
|
||||
return direct_outbound_tag();
|
||||
};
|
||||
let _load_addtype_rules = function() {
|
||||
let out = [];
|
||||
let dtag = direct_outbound_tag();
|
||||
let ptag = null;
|
||||
for (let s in uci_sections('addtype')) {
|
||||
let o = s.options || {};
|
||||
let addr = trim_s(o.ipaaddr || '');
|
||||
let rtype = trim_s(o.type || '');
|
||||
let pg = trim_s(o.pgroup || '');
|
||||
if (!s_len(addr) || !s_len(rtype) || !s_len(pg))
|
||||
continue;
|
||||
let r = {};
|
||||
if (rtype == 'DOMAIN')
|
||||
r.domain = addr;
|
||||
else if (rtype == 'DOMAIN-SUFFIX')
|
||||
r.domain_suffix = addr;
|
||||
else if (rtype == 'DOMAIN-KEYWORD')
|
||||
r.domain_keyword = addr;
|
||||
else if (rtype == 'IP-CIDR' || rtype == 'IP-CIDR6')
|
||||
r.ip_cidr = addr;
|
||||
else
|
||||
continue;
|
||||
if (pg == 'DIRECT')
|
||||
r.outbound = dtag;
|
||||
else {
|
||||
if (ptag == null)
|
||||
ptag = _proxy_outbound_tag();
|
||||
r.outbound = ptag;
|
||||
}
|
||||
push(out, r);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
let _addtype_rules = _load_addtype_rules();
|
||||
if (length(_addtype_rules)) {
|
||||
let _nr = [];
|
||||
let _ins = false;
|
||||
for (let _i = 0; _i < length(cfg.route.rules); _i++) {
|
||||
let _ru = cfg.route.rules[_i];
|
||||
let _dns_front = _ru && type(_ru) == 'object' &&
|
||||
(_ru.action == 'hijack-dns' || (_ru.action == 'reject' && _ru.port));
|
||||
if (!_ins && !_dns_front) {
|
||||
for (let _cr in _addtype_rules)
|
||||
push(_nr, _cr);
|
||||
_ins = true;
|
||||
}
|
||||
push(_nr, _ru);
|
||||
}
|
||||
if (!_ins)
|
||||
for (let _cr in _addtype_rules)
|
||||
push(_nr, _cr);
|
||||
cfg.route.rules = _nr;
|
||||
}
|
||||
|
||||
if (uci_opt('enhanced_mode', 'fake-ip') == 'fake-ip') {
|
||||
add_remote_rule_set('geolocation-!cn',
|
||||
'https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-!cn.srs');
|
||||
}
|
||||
|
||||
/* Local .srs -> local; remaining remote rule_sets must keep/get a download_detour.
|
||||
* CN first-start deadlock: no srs -> fetch rules -> via auto-select -> urltest dials
|
||||
* the proxy -> resolves its domain -> pulled into an unloaded rule_set by DNS rules -> deadline.
|
||||
* The srs URLs use a CN-reachable mirror, so download_detour
|
||||
* must be DIRECT (real direct-outbound tag if any, else 'DIRECT'). */
|
||||
let _pick_dl_detour = function() {
|
||||
if (cfg.outbounds) {
|
||||
for (let ob in cfg.outbounds) {
|
||||
if (ob && ob.type == 'direct' && ob.tag) return ob.tag;
|
||||
}
|
||||
}
|
||||
return 'DIRECT';
|
||||
};
|
||||
let _dl_detour = _pick_dl_detour();
|
||||
for (let rs in (cfg.route || {}).rule_set || []) {
|
||||
if (!rs) continue;
|
||||
if (rs.type != 'remote') { delete rs.download_detour; continue; }
|
||||
let path = local_rule_set_path(rs.tag || '');
|
||||
if (rs.tag && access(path, 'r')) {
|
||||
delete rs.url; delete rs.download_detour; rs.type = 'local'; rs.path = path;
|
||||
continue;
|
||||
}
|
||||
if (keep_remote_rule_set(rs) && rs.url) {
|
||||
rs.url = normalize_rule_set_url(rs.url);
|
||||
rs.download_detour = _dl_detour;
|
||||
continue;
|
||||
}
|
||||
/* No local cache -> skip this remote rule_set so a failed download doesn't block start.
|
||||
* Rule-sets are fetched to /usr/share/clashoo/ruleset/ once proxied. */
|
||||
continue;
|
||||
|
||||
}
|
||||
/* filter out remote rule_sets with no local cache */
|
||||
let _clean_rs = [];
|
||||
for (let _rsi = 0; _rsi < length(cfg.route.rule_set); _rsi++) {
|
||||
let _rs = cfg.route.rule_set[_rsi];
|
||||
if (!_rs) continue;
|
||||
if (_rs.type == 'remote' && _rs.url != null) {
|
||||
let _local_path = local_rule_set_path(_rs.tag || '');
|
||||
if (!_rs.tag || !access(_local_path, 'r'))
|
||||
if (keep_remote_rule_set(_rs) && _rs.url) {
|
||||
_rs.url = normalize_rule_set_url(_rs.url);
|
||||
_rs.download_detour = _dl_detour;
|
||||
push(_clean_rs, _rs);
|
||||
continue;
|
||||
} else {
|
||||
continue; /* no local cache -> drop from final list */
|
||||
}
|
||||
/* has local cache -> convert to local */
|
||||
delete _rs.url; delete _rs.download_detour; _rs.type = 'local'; _rs.path = _local_path;
|
||||
}
|
||||
push(_clean_rs, _rs);
|
||||
}
|
||||
cfg.route.rule_set = _clean_rs;
|
||||
prune_tun_cn_exclude_if_missing();
|
||||
|
||||
/* drop DNS/route rules referencing a removed rule_set, else sing-box FATAL: rule-set not found */
|
||||
let _existing_tags = {};
|
||||
for (let _rs in cfg.route.rule_set || []) if (_rs && _rs.tag) _existing_tags[_rs.tag] = true;
|
||||
let _rule_has_ref = function(_r) {
|
||||
if (!_r || type(_r) != 'object') return false;
|
||||
if (_r.rule_set) {
|
||||
if (type(_r.rule_set) == 'array') { for (let _t in _r.rule_set) if (!_existing_tags[_t]) return false; }
|
||||
else if (!_existing_tags[_r.rule_set]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
/* clean dns.rules */
|
||||
if (cfg.dns && type(cfg.dns.rules) == 'array') {
|
||||
let _clean_dns_rules = [];
|
||||
for (let _dr in cfg.dns.rules) if (_rule_has_ref(_dr)) push(_clean_dns_rules, _dr);
|
||||
cfg.dns.rules = _clean_dns_rules;
|
||||
}
|
||||
/* clean route.rules */
|
||||
if (cfg.route && type(cfg.route.rules) == 'array') {
|
||||
let _clean_rt_rules = [];
|
||||
for (let _rr in cfg.route.rules) if (_rule_has_ref(_rr)) push(_clean_rt_rules, _rr);
|
||||
cfg.route.rules = _clean_rt_rules;
|
||||
}
|
||||
cfg.experimental = cfg.experimental || {};
|
||||
cfg.experimental.clash_api = cfg.experimental.clash_api || {};
|
||||
cfg.experimental.clash_api.external_controller = '0.0.0.0:' + dash_port;
|
||||
cfg.experimental.clash_api.external_ui = '/etc/clashoo/dashboard';
|
||||
cfg.experimental.clash_api.secret = dash_secret;
|
||||
|
||||
cfg.experimental.cache_file = {
|
||||
enabled: true,
|
||||
store_fakeip: true
|
||||
};
|
||||
|
||||
/* OpenWrt already owns system time sync. Keep sing-box NTP disabled by
|
||||
* default to avoid noisy IPv6 UDP/123 failures on IPv4-only routers. */
|
||||
cfg.ntp = cfg.ntp || {};
|
||||
cfg.ntp.enabled = false;
|
||||
|
||||
cfg.log = cfg.log || {};
|
||||
cfg.log.output = '/var/log/clashoo/core.log';
|
||||
if (!cfg.log.level)
|
||||
cfg.log.level = 'info';
|
||||
|
||||
if (writefile(path, sprintf('%J', cfg)) === null) {
|
||||
print("write failed\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
print("normalized\n");
|
||||
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "info",
|
||||
"timestamp": true
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{ "type": "tcp", "tag": "dns_proxy", "server": "1.1.1.1", "domain_resolver": "dns_resolver" },
|
||||
{ "type": "https", "tag": "dns_direct", "server": "dns.alidns.com", "domain_resolver": "dns_resolver" },
|
||||
{ "type": "udp", "tag": "dns_resolver", "server": "223.5.5.5" },
|
||||
{ "type": "fakeip", "tag": "dns_fakeip", "inet4_range": "198.18.0.0/15", "inet6_range": "fc00::/18" }
|
||||
],
|
||||
"rules": [
|
||||
{ "rule_set": "geolocation-!cn", "query_type": ["A", "AAAA", "CNAME"], "server": "dns_fakeip" },
|
||||
{ "query_type": ["A", "AAAA", "CNAME"], "invert": true, "action": "reject" }
|
||||
],
|
||||
"final": "dns_direct"
|
||||
},
|
||||
"ntp": {
|
||||
"enabled": false,
|
||||
"server": "time.apple.com",
|
||||
"server_port": 123,
|
||||
"interval": "30m"
|
||||
},
|
||||
"inbounds": [
|
||||
{ "type": "mixed", "tag": "mixed-in", "listen": "0.0.0.0", "listen_port": 2080 },
|
||||
{ "type": "tun", "tag": "tun-in", "address": ["172.19.0.1/30"], "auto_route": true, "auto_redirect": true, "strict_route": true, "stack": "mixed", "route_exclude_address_set": ["cn-ip"] }
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"type": "selector", "tag": "🚀 节点选择",
|
||||
"outbounds": ["🌍 全部节点","♻️ 自动选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","DIRECT"],
|
||||
"default": "🌍 全部节点"
|
||||
},
|
||||
{ "type": "selector", "tag": "▶️ YouTube", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🔍 Google", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🔮 OpenAI", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🧠 Claude", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🤖 Gemini", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🐦 Twitter", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "📦 GitHub", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "Ⓜ️ Microsoft", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🎵 TikTok", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "📺 Netflix", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🎬 流媒体", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "💳 PayPal", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "📲 电报消息", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🍏 苹果服务", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] },
|
||||
{ "type": "selector", "tag": "🎯 全球直连", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] },
|
||||
{ "type": "selector", "tag": "🏠 私有网络", "outbounds": ["DIRECT","🚀 节点选择","♻️ 自动选择"] },
|
||||
{ "type": "selector", "tag": "🌐 非中国", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
{ "type": "selector", "tag": "🐟 漏网之鱼", "outbounds": ["🚀 节点选择","♻️ 香港自动","♻️ 日本自动","♻️ 美国自动","♻️ 新加坡自动","♻️ 集合自动","♻️ 自动选择","🇭🇰 香港节点","🇯🇵 日本节点","🇺🇲 美国节点","🇸🇬 新加坡节点","🇺🇳 集合节点","🌍 全部节点","DIRECT"] },
|
||||
|
||||
{ "type": "selector", "tag": "🇭🇰 香港节点", "outbounds": ["__NODES_HK__"] },
|
||||
{ "type": "selector", "tag": "🇯🇵 日本节点", "outbounds": ["__NODES_JP__"] },
|
||||
{ "type": "selector", "tag": "🇺🇲 美国节点", "outbounds": ["__NODES_US__"] },
|
||||
{ "type": "selector", "tag": "🇸🇬 新加坡节点","outbounds": ["__NODES_SG__"] },
|
||||
{ "type": "selector", "tag": "🇺🇳 集合节点", "outbounds": ["__NODES_OTHER__"] },
|
||||
{ "type": "selector", "tag": "🌍 全部节点", "outbounds": ["__NODES__"] },
|
||||
|
||||
{ "type": "urltest", "tag": "♻️ 香港自动", "outbounds": ["__NODES_HK__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
{ "type": "urltest", "tag": "♻️ 日本自动", "outbounds": ["__NODES_JP__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
{ "type": "urltest", "tag": "♻️ 美国自动", "outbounds": ["__NODES_US__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
{ "type": "urltest", "tag": "♻️ 新加坡自动", "outbounds": ["__NODES_SG__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
{ "type": "urltest", "tag": "♻️ 集合自动", "outbounds": ["__NODES_OTHER__"], "url": "https://www.gstatic.com/generate_204", "interval": "5m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
{ "type": "urltest", "tag": "♻️ 自动选择", "outbounds": ["__NODES__"], "url": "https://www.gstatic.com/generate_204", "interval": "10m", "tolerance": 50, "interrupt_exist_connections": true },
|
||||
|
||||
{ "type": "direct", "tag": "DIRECT" }
|
||||
],
|
||||
"route": {
|
||||
"default_domain_resolver": "dns_resolver",
|
||||
"rule_set": [
|
||||
{ "tag": "openai", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/openai.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "anthropic", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/anthropic.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "google-gemini", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/google-gemini.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "youtube", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/youtube.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "google", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/google.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "twitter", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/twitter.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "github", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/github.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "gitlab", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/gitlab.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "microsoft", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/microsoft.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "tiktok", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/tiktok.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "netflix", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/netflix.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "hulu", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/hulu.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "disney", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/disney.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "hbo", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/hbo.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "amazon", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/amazon.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "bahamut", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/bahamut.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "paypal", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/paypal.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "apple", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/apple.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "geolocation-cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-cn.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/cn.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "geolocation-!cn", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/geolocation-!cn.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "google-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/google.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "private-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/private.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "cn-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/cn.srs", "download_detour": "DIRECT" },
|
||||
{ "tag": "telegram-ip", "type": "remote", "format": "binary", "url": "https://github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/telegram.srs", "download_detour": "DIRECT" }
|
||||
],
|
||||
"rules": [
|
||||
{ "action": "sniff" },
|
||||
{ "clash_mode": "direct", "outbound": "DIRECT" },
|
||||
{ "clash_mode": "global", "outbound": "♻️ 自动选择" },
|
||||
{ "protocol": "dns", "action": "hijack-dns" },
|
||||
{ "rule_set": "openai", "outbound": "🔮 OpenAI" },
|
||||
{ "rule_set": "anthropic", "outbound": "🧠 Claude" },
|
||||
{ "rule_set": "google-gemini", "outbound": "🤖 Gemini" },
|
||||
{ "rule_set": "youtube", "outbound": "▶️ YouTube" },
|
||||
{ "rule_set": "google", "outbound": "🔍 Google" },
|
||||
{ "rule_set": "twitter", "outbound": "🐦 Twitter" },
|
||||
{ "rule_set": "tiktok", "outbound": "🎵 TikTok" },
|
||||
{ "rule_set": "netflix", "outbound": "📺 Netflix" },
|
||||
{ "rule_set": ["hulu", "disney", "hbo", "amazon", "bahamut"], "outbound": "🎬 流媒体" },
|
||||
{ "rule_set": "paypal", "outbound": "💳 PayPal" },
|
||||
{ "rule_set": "microsoft", "outbound": "Ⓜ️ Microsoft" },
|
||||
{ "rule_set": "apple", "outbound": "🍏 苹果服务" },
|
||||
{ "rule_set": ["github", "gitlab"], "outbound": "📦 GitHub" },
|
||||
{ "rule_set": "telegram-ip", "outbound": "📲 电报消息" },
|
||||
{ "rule_set": "private-ip", "outbound": "🏠 私有网络" },
|
||||
{ "rule_set": ["geolocation-cn", "cn"], "outbound": "🎯 全球直连" },
|
||||
{ "rule_set": "cn-ip", "outbound": "🎯 全球直连" },
|
||||
{ "domain_suffix": ["cn"], "outbound": "🎯 全球直连" },
|
||||
{ "rule_set": "geolocation-!cn", "outbound": "🌐 非中国" },
|
||||
{ "rule_set": "google-ip", "outbound": "🔍 Google" }
|
||||
],
|
||||
"auto_detect_interface": true,
|
||||
"final": "🐟 漏网之鱼"
|
||||
},
|
||||
"experimental": {
|
||||
"cache_file": { "enabled": true, "store_fakeip": true },
|
||||
"clash_api": {
|
||||
"external_controller": "0.0.0.0:9090",
|
||||
"external_ui": "dashboard",
|
||||
"secret": "clashoo",
|
||||
"external_ui_download_url": "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip",
|
||||
"external_ui_download_detour": "DIRECT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
#!/usr/bin/ucode
|
||||
/* yaml2singbox.uc — convert mihomo/clash YAML proxies to sing-box outbounds
|
||||
* Usage: ucode yaml2singbox.uc <yaml-in> [template-json] [json-out]
|
||||
* Supports: ss, vmess, vless, trojan, hysteria2, tuic. Others are skipped.
|
||||
* "__NODES__" in the template selector outbounds is expanded to all node tags. */
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile, popen } from 'fs';
|
||||
|
||||
const TPL_DEFAULT = '/usr/share/clashoo/lib/templates/default.json';
|
||||
const SUPPORTED = {
|
||||
'ss': 'shadowsocks',
|
||||
'shadowsocks':'shadowsocks',
|
||||
'vmess': 'vmess',
|
||||
'vless': 'vless',
|
||||
'trojan': 'trojan',
|
||||
'hysteria2': 'hysteria2',
|
||||
'hy2': 'hysteria2',
|
||||
'tuic': 'tuic'
|
||||
};
|
||||
|
||||
function logerr(msg) {
|
||||
warn(sprintf("[yaml2singbox] %s\n", msg));
|
||||
}
|
||||
|
||||
function die(msg, code) {
|
||||
logerr(msg);
|
||||
exit(code || 1);
|
||||
}
|
||||
|
||||
/* ---------- YAML read (via yq) ---------- */
|
||||
function quote_sh(s) {
|
||||
/* single-quote for shell; escape embedded quotes */
|
||||
return "'" + replace(s, "'", "'\\''") + "'";
|
||||
}
|
||||
|
||||
function read_yaml_as_json(path) {
|
||||
const cmd = sprintf("yq -o=json eval . %s 2>/dev/null", quote_sh(path));
|
||||
const h = popen(cmd, "r");
|
||||
if (!h)
|
||||
die(sprintf("popen failed: %s", cmd));
|
||||
let buf = "", chunk;
|
||||
while ((chunk = h.read(65536)))
|
||||
buf += chunk;
|
||||
h.close();
|
||||
if (!length(buf))
|
||||
die(sprintf("yq returned empty for %s (yaml loadable?)", path));
|
||||
const data = json(buf);
|
||||
if (data === null)
|
||||
die("json parse of yq output failed");
|
||||
return data;
|
||||
}
|
||||
|
||||
/* ---------- helpers ---------- */
|
||||
function pick(obj, ...keys) {
|
||||
for (let k in keys)
|
||||
if (obj[k] != null)
|
||||
return obj[k];
|
||||
return null;
|
||||
}
|
||||
|
||||
function tobool(v) {
|
||||
if (v === true || v === 'true' || v === 1 || v === '1') return true;
|
||||
if (v === false || v === 'false' || v === 0 || v === '0') return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
function toint(v) {
|
||||
if (v == null) return null;
|
||||
let n = +v;
|
||||
return (n === n) ? n : null; /* NaN check */
|
||||
}
|
||||
|
||||
function strip_null(obj) {
|
||||
if (type(obj) !== 'object') return obj;
|
||||
const out = {};
|
||||
for (let k in obj) {
|
||||
const v = obj[k];
|
||||
if (v == null) continue;
|
||||
if (type(v) === 'object') {
|
||||
const sv = strip_null(v);
|
||||
if (length(sv) > 0) out[k] = sv;
|
||||
} else if (type(v) === 'array') {
|
||||
const arr = map(v, (x) => (type(x) === 'object') ? strip_null(x) : x);
|
||||
if (length(arr) > 0) out[k] = arr;
|
||||
} else {
|
||||
out[k] = v;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ---------- TLS block assembly ---------- */
|
||||
function build_tls(p) {
|
||||
const enabled = tobool(p.tls) || (p.sni && length(p.sni) > 0) || (p.servername && length(p.servername) > 0);
|
||||
if (!enabled) return null;
|
||||
const tls = {
|
||||
enabled: true,
|
||||
server_name: pick(p, 'sni', 'servername'),
|
||||
insecure: tobool(p['skip-cert-verify']) === true ? true : null,
|
||||
alpn: p.alpn
|
||||
};
|
||||
const reality = p['reality-opts'];
|
||||
if (reality && type(reality) === 'object') {
|
||||
tls.reality = {
|
||||
enabled: true,
|
||||
public_key: reality['public-key'],
|
||||
short_id: reality['short-id']
|
||||
};
|
||||
}
|
||||
const client_fp = p['client-fingerprint'];
|
||||
if (client_fp)
|
||||
tls.utls = { enabled: true, fingerprint: client_fp };
|
||||
return strip_null(tls);
|
||||
}
|
||||
|
||||
/* ---------- transport (ws / grpc / http) ---------- */
|
||||
function build_transport(p) {
|
||||
const net = p.network;
|
||||
if (!net || net === 'tcp') return null;
|
||||
if (net === 'ws') {
|
||||
const opts = p['ws-opts'] || {};
|
||||
return strip_null({
|
||||
type: 'ws',
|
||||
path: opts.path,
|
||||
headers: opts.headers,
|
||||
max_early_data: toint(opts['max-early-data']),
|
||||
early_data_header_name: opts['early-data-header-name']
|
||||
});
|
||||
}
|
||||
if (net === 'grpc') {
|
||||
const opts = p['grpc-opts'] || {};
|
||||
return { type: 'grpc', service_name: opts['grpc-service-name'] };
|
||||
}
|
||||
if (net === 'http' || net === 'h2') {
|
||||
const opts = p['h2-opts'] || p['http-opts'] || {};
|
||||
return strip_null({
|
||||
type: 'http',
|
||||
host: opts.host || opts.Host,
|
||||
path: opts.path
|
||||
});
|
||||
}
|
||||
if (net === 'httpupgrade') {
|
||||
const opts = p['http-upgrade-opts'] || {};
|
||||
return strip_null({ type: 'httpupgrade', path: opts.path, host: opts.host });
|
||||
}
|
||||
logerr(sprintf("unknown transport network '%s' for node '%s', ignored", net, p.name));
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---------- per-protocol conversion ---------- */
|
||||
function convert_ss(p) {
|
||||
const plugin = p.plugin, popts = p['plugin-opts'] || {};
|
||||
let plugin_opts = null;
|
||||
if (plugin && type(popts) === 'object') {
|
||||
/* Field mapping: Clash simple-obfs -> sing-box obfs-local
|
||||
* Clash: mode=http;host=xxx → sing-box: obfs=http;obfs-host=xxx */
|
||||
const FIELD_MAP = {
|
||||
'mode': 'obfs',
|
||||
'host': 'obfs-host',
|
||||
'uri': 'obfs-uri'
|
||||
};
|
||||
const parts = [];
|
||||
for (let k in popts) {
|
||||
const v = popts[k];
|
||||
if (type(v) === 'object') continue;
|
||||
push(parts, (FIELD_MAP[k] || k) + '=' + v);
|
||||
}
|
||||
plugin_opts = join(';', parts);
|
||||
}
|
||||
/* Clash "obfs" maps to sing-box plugin "obfs-local" */
|
||||
let sb_plugin = null;
|
||||
if (plugin === 'obfs') sb_plugin = 'obfs-local';
|
||||
else if (plugin === 'v2ray-plugin') sb_plugin = 'v2ray-plugin';
|
||||
else if (plugin === 'shadow-tls') sb_plugin = 'shadow-tls';
|
||||
else if (plugin) sb_plugin = plugin;
|
||||
|
||||
return strip_null({
|
||||
type: 'shadowsocks',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
method: p.cipher,
|
||||
password: p.password,
|
||||
plugin: sb_plugin,
|
||||
plugin_opts: plugin_opts
|
||||
});
|
||||
}
|
||||
|
||||
function convert_vmess(p) {
|
||||
return strip_null({
|
||||
type: 'vmess',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
uuid: p.uuid,
|
||||
alter_id: toint(p.alterId) || 0,
|
||||
security: p.cipher || 'auto',
|
||||
tls: build_tls(p),
|
||||
transport: build_transport(p)
|
||||
});
|
||||
}
|
||||
|
||||
function convert_vless(p) {
|
||||
return strip_null({
|
||||
type: 'vless',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
uuid: p.uuid,
|
||||
flow: p.flow,
|
||||
tls: build_tls(p),
|
||||
transport: build_transport(p)
|
||||
});
|
||||
}
|
||||
|
||||
function convert_trojan(p) {
|
||||
return strip_null({
|
||||
type: 'trojan',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
password: p.password,
|
||||
tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server },
|
||||
transport: build_transport(p)
|
||||
});
|
||||
}
|
||||
|
||||
function convert_hysteria2(p) {
|
||||
const obfs_pass = p['obfs-password'];
|
||||
return strip_null({
|
||||
type: 'hysteria2',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
password: pick(p, 'password', 'auth'),
|
||||
up_mbps: toint(p.up),
|
||||
down_mbps: toint(p.down),
|
||||
obfs: (p.obfs && obfs_pass) ? { type: p.obfs, password: obfs_pass } : null,
|
||||
tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server }
|
||||
});
|
||||
}
|
||||
|
||||
function convert_tuic(p) {
|
||||
return strip_null({
|
||||
type: 'tuic',
|
||||
tag: p.name,
|
||||
server: p.server,
|
||||
server_port: toint(p.port),
|
||||
uuid: p.uuid,
|
||||
password: p.password,
|
||||
congestion_control: p['congestion-controller'] || p.congestion,
|
||||
udp_relay_mode: p['udp-relay-mode'],
|
||||
tls: build_tls(p) || { enabled: true, server_name: p.sni || p.server, alpn: p.alpn }
|
||||
});
|
||||
}
|
||||
|
||||
function convert_proxy(p) {
|
||||
const sb_type = SUPPORTED[p.type];
|
||||
if (!sb_type) {
|
||||
if (p.type && p.type !== 'select')
|
||||
logerr(sprintf("skip unsupported type '%s' for '%s'", p.type, p.name));
|
||||
return null;
|
||||
}
|
||||
if (!p.server || !p.port || !p.name) {
|
||||
logerr(sprintf("skip proxy missing server/port/name (type=%s)", p.type));
|
||||
return null;
|
||||
}
|
||||
switch (p.type) {
|
||||
case 'ss':
|
||||
case 'shadowsocks': return convert_ss(p);
|
||||
case 'vmess': return convert_vmess(p);
|
||||
case 'vless': return convert_vless(p);
|
||||
case 'trojan': return convert_trojan(p);
|
||||
case 'hysteria2':
|
||||
case 'hy2': return convert_hysteria2(p);
|
||||
case 'tuic': return convert_tuic(p);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ---------- dedup (tag must be unique) ---------- */
|
||||
function dedupe_tags(nodes) {
|
||||
const seen = {};
|
||||
for (let n in nodes) {
|
||||
let base = n.tag, t = base, i = 2;
|
||||
while (seen[t]) {
|
||||
t = base + '_' + i;
|
||||
i++;
|
||||
}
|
||||
n.tag = t;
|
||||
seen[t] = true;
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/* Drop airline pseudo-nodes (Traffic:/Expire:/quota/官网/QQ etc.) from selectors
|
||||
* and urltests. They're real outbounds (so the UI can read traffic/expiry) but
|
||||
* don't forward — in a selector/urltest they win (0ms) and swallow all traffic. */
|
||||
function is_pseudo_node_tag(tag) {
|
||||
if (!tag) return false;
|
||||
const t = '' + tag;
|
||||
const patterns = [
|
||||
/^Traffic[::]/i,
|
||||
/^Expire[::]/i,
|
||||
/剩余流量|剩余[::]/,
|
||||
/距离下次重置/,
|
||||
/到期(时间|日期)?[::]/,
|
||||
/官网[::]/,
|
||||
/网站[::]/,
|
||||
/套餐[::]?/,
|
||||
/客服[::]/,
|
||||
/QQ[群]?[::]/i,
|
||||
/Telegram|TG群|官方群/i,
|
||||
/续费|订阅地址|流量重置/,
|
||||
];
|
||||
for (let re in patterns) if (match(t, re)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---------- detect region from tag -> 'HK'/'JP'/'US'/'SG'/'OTHER'/'' ---------- */
|
||||
function region_of(tag) {
|
||||
if (!tag) return '';
|
||||
const t = '' + tag;
|
||||
if (match(t, /港|🇭🇰|HK[^A-Za-z]|[^A-Za-z]HK|^HK$|[Hh]ong[Kk]/)) return 'HK';
|
||||
if (match(t, /日|🇯🇵|JP[^A-Za-z]|[^A-Za-z]JP|^JP$|[Jj]apan/)) return 'JP';
|
||||
if (match(t, /美|🇺🇸|US[^A-Za-z]|[^A-Za-z]US|^US$|[Uu]nited.?[Ss]tates|[Aa]merica/)) return 'US';
|
||||
if (match(t, /新加坡|🇸🇬|SG[^A-Za-z]|[^A-Za-z]SG|^SG$|[Ss]ingapore/)) return 'SG';
|
||||
if (match(t, /台湾|台|🇹🇼|TW[^A-Za-z]|[^A-Za-z]TW|^TW$|[Tt]aiwan|韩国|韩|🇰🇷|KR[^A-Za-z]|[^A-Za-z]KR|^KR$|[Kk]orea/)) return 'OTHER';
|
||||
return '';
|
||||
}
|
||||
|
||||
/* ---------- expand __NODES__/__NODES_XX__ placeholders to real tags (dropping pseudo-nodes) ---------- */
|
||||
function expand_node_placeholder(outbounds, node_tags) {
|
||||
const real_tags = [];
|
||||
for (let t in node_tags) if (!is_pseudo_node_tag(t)) push(real_tags, t);
|
||||
|
||||
/* bucket by region */
|
||||
let tags_hk = [], tags_jp = [], tags_us = [], tags_sg = [], tags_other = [];
|
||||
for (let t in real_tags) {
|
||||
const r = region_of(t);
|
||||
if (r === 'HK') push(tags_hk, t);
|
||||
else if (r === 'JP') push(tags_jp, t);
|
||||
else if (r === 'US') push(tags_us, t);
|
||||
else if (r === 'SG') push(tags_sg, t);
|
||||
else if (r === 'OTHER') push(tags_other, t);
|
||||
}
|
||||
/* when a region has no nodes, fall back to all nodes (avoid empty outbounds) */
|
||||
if (!length(tags_hk)) tags_hk = real_tags;
|
||||
if (!length(tags_jp)) tags_jp = real_tags;
|
||||
if (!length(tags_us)) tags_us = real_tags;
|
||||
if (!length(tags_sg)) tags_sg = real_tags;
|
||||
if (!length(tags_other)) tags_other = real_tags;
|
||||
|
||||
for (let ob in outbounds) {
|
||||
if (ob.type !== 'selector' && ob.type !== 'urltest') continue;
|
||||
if (!ob.outbounds || type(ob.outbounds) !== 'array') continue;
|
||||
const expanded = [];
|
||||
for (let item in ob.outbounds) {
|
||||
let list = null;
|
||||
if (item === '__NODES__') list = real_tags;
|
||||
else if (item === '__NODES_HK__') list = tags_hk;
|
||||
else if (item === '__NODES_JP__') list = tags_jp;
|
||||
else if (item === '__NODES_US__') list = tags_us;
|
||||
else if (item === '__NODES_SG__') list = tags_sg;
|
||||
else if (item === '__NODES_OTHER__') list = tags_other;
|
||||
if (list !== null) { for (let t in list) push(expanded, t); }
|
||||
else { push(expanded, item); }
|
||||
}
|
||||
ob.outbounds = expanded;
|
||||
}
|
||||
return outbounds;
|
||||
}
|
||||
|
||||
/* ---------- proxy-providers ---------- */
|
||||
function resolve_providers(yaml) {
|
||||
if (type(yaml['proxy-providers']) !== 'object')
|
||||
return [];
|
||||
const all = [];
|
||||
for (let name in yaml['proxy-providers']) {
|
||||
const p = yaml['proxy-providers'][name];
|
||||
if (p.type !== 'http' || !p.url) {
|
||||
logerr(sprintf("provider '%s': skip (not http or no url)", name));
|
||||
continue;
|
||||
}
|
||||
const cmd = sprintf("wget -q -O- --timeout=20 %s 2>/dev/null | yq -o=json eval . 2>/dev/null", quote_sh(p.url));
|
||||
const h = popen(cmd, "r");
|
||||
if (!h) { logerr(sprintf("provider '%s': download failed", name)); continue; }
|
||||
let buf = "", chunk;
|
||||
while ((chunk = h.read(65536))) buf += chunk;
|
||||
h.close();
|
||||
if (!length(buf)) { logerr(sprintf("provider '%s': empty response", name)); continue; }
|
||||
const data = json(buf);
|
||||
if (!data || type(data.proxies) !== 'array') {
|
||||
logerr(sprintf("provider '%s': no proxies array in response", name));
|
||||
continue;
|
||||
}
|
||||
logerr(sprintf("provider '%s': %d proxies", name, length(data.proxies)));
|
||||
for (let px in data.proxies) push(all, px);
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/* ---------- main ---------- */
|
||||
const yaml_path = ARGV[0];
|
||||
const tpl_path = ARGV[1] || TPL_DEFAULT;
|
||||
const out_path = ARGV[2];
|
||||
|
||||
if (!yaml_path)
|
||||
die("usage: ucode yaml2singbox.uc <yaml-in> [template-json] [json-out]");
|
||||
|
||||
const yaml = read_yaml_as_json(yaml_path);
|
||||
|
||||
let proxies = [];
|
||||
if (type(yaml.proxies) === 'array')
|
||||
proxies = yaml.proxies;
|
||||
else
|
||||
logerr("no inline proxies array, checking proxy-providers...");
|
||||
|
||||
if (type(yaml['proxy-providers']) === 'object') {
|
||||
const pproxies = resolve_providers(yaml);
|
||||
for (let p in pproxies) push(proxies, p);
|
||||
}
|
||||
|
||||
if (!length(proxies))
|
||||
die(sprintf("no proxies found in %s (inline or via providers)", yaml_path));
|
||||
|
||||
const tpl_raw = readfile(tpl_path);
|
||||
if (!tpl_raw)
|
||||
die(sprintf("cannot read template %s", tpl_path));
|
||||
const tpl = json(tpl_raw);
|
||||
if (!tpl || type(tpl.outbounds) !== 'array')
|
||||
die(sprintf("template %s has no outbounds[]", tpl_path));
|
||||
|
||||
/* convert proxies */
|
||||
const nodes = [];
|
||||
let skipped = 0;
|
||||
for (let p in proxies) {
|
||||
const o = convert_proxy(p);
|
||||
if (o) push(nodes, o); else skipped++;
|
||||
}
|
||||
dedupe_tags(nodes);
|
||||
|
||||
if (!length(nodes))
|
||||
die(sprintf("no usable nodes converted from %d proxies (skipped=%d)", length(proxies), skipped));
|
||||
|
||||
logerr(sprintf("converted=%d skipped=%d", length(nodes), skipped));
|
||||
|
||||
/* prepend converted nodes to template outbounds */
|
||||
const final_outbounds = [];
|
||||
for (let n in nodes) push(final_outbounds, n);
|
||||
for (let ob in tpl.outbounds) push(final_outbounds, ob);
|
||||
|
||||
/* expand __NODES__ placeholders */
|
||||
const node_tags = map(nodes, (n) => n.tag);
|
||||
expand_node_placeholder(final_outbounds, node_tags);
|
||||
|
||||
tpl.outbounds = final_outbounds;
|
||||
|
||||
const out = sprintf("%.J\n", tpl);
|
||||
if (out_path) {
|
||||
if (!writefile(out_path, out))
|
||||
die(sprintf("writefile failed: %s", out_path));
|
||||
logerr(sprintf("wrote %s (%d bytes, %d nodes)", out_path, length(out), length(nodes)));
|
||||
} else {
|
||||
print(out);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -f
|
||||
|
||||
[ "$(uci -q get clashoo.config.fake_ip_filter_migrated 2>/dev/null)" = "1" ] && exit 0
|
||||
|
||||
filters="$(uci -q get clashoo.config.fake_ip_filter 2>/dev/null)"
|
||||
has_legacy=0
|
||||
for filter in $filters; do
|
||||
[ "$filter" = "*.lan" ] && has_legacy=1
|
||||
done
|
||||
|
||||
if [ "$has_legacy" = "1" ]; then
|
||||
uci -q delete clashoo.config.fake_ip_filter
|
||||
added_lan=0
|
||||
added_local=0
|
||||
for filter in $filters; do
|
||||
case "$filter" in
|
||||
'*.lan'|'+.lan')
|
||||
if [ "$added_lan" = "0" ]; then
|
||||
uci -q add_list clashoo.config.fake_ip_filter='+.lan'
|
||||
added_lan=1
|
||||
fi
|
||||
;;
|
||||
'+.local')
|
||||
if [ "$added_local" = "0" ]; then
|
||||
uci -q add_list clashoo.config.fake_ip_filter='+.local'
|
||||
added_local=1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
uci -q add_list clashoo.config.fake_ip_filter="$filter"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [ "$added_local" = "0" ]; then
|
||||
uci -q add_list clashoo.config.fake_ip_filter='+.local'
|
||||
fi
|
||||
fi
|
||||
|
||||
uci -q set clashoo.config.fake_ip_filter_migrated='1'
|
||||
uci -q commit clashoo
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$(uci -q get clashoo.config.acl_migrated 2>/dev/null)" = "1" ] && exit 0
|
||||
if uci -q show clashoo 2>/dev/null | grep -q '=lan_acl$'; then
|
||||
uci -q set clashoo.config.acl_migrated='1'
|
||||
uci -q commit clashoo
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mode="$(uci -q get clashoo.config.access_control 2>/dev/null)"
|
||||
|
||||
add_group() {
|
||||
local dns="$1" proxy="$2" list_key="${3:-}" section value
|
||||
|
||||
section="$(uci -q add clashoo lan_acl)" || exit 1
|
||||
uci -q set clashoo."$section".enabled='1'
|
||||
uci -q set clashoo."$section".dns="$dns"
|
||||
uci -q set clashoo."$section".proxy="$proxy"
|
||||
if [ -n "$list_key" ]; then
|
||||
for value in $(uci -q get clashoo.config."$list_key" 2>/dev/null); do
|
||||
uci -q add_list clashoo."$section".ip="$value"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
case "$mode" in
|
||||
1)
|
||||
add_group 1 1 proxy_lan_ips
|
||||
add_group 0 0
|
||||
;;
|
||||
2)
|
||||
add_group 0 0 reject_lan_ips
|
||||
add_group 1 1
|
||||
;;
|
||||
esac
|
||||
|
||||
uci -q set clashoo.config.acl_migrated='1'
|
||||
uci -q commit clashoo
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/sh
|
||||
# clashoo access check
|
||||
# 用法: access_check.sh <url> [mode]
|
||||
# mode = direct | proxy(默认 proxy)
|
||||
# direct: 经 openwrt 主干直连,不走任何代理
|
||||
# proxy: 走 clashoo 本地代理端口,验证核心出站
|
||||
|
||||
url="$1"
|
||||
mode="${2:-proxy}"
|
||||
[ -n "$url" ] || exit 1
|
||||
|
||||
attempts=1
|
||||
ok=0
|
||||
sum_ms=0
|
||||
last_code=000
|
||||
|
||||
url_host() {
|
||||
printf '%s' "$1" | sed -n 's#^[a-zA-Z][a-zA-Z0-9+.-]*://\([^/:?#]*\).*$#\1#p'
|
||||
}
|
||||
|
||||
url_port() {
|
||||
_u="$1"
|
||||
_p="$(printf '%s' "$_u" | sed -n 's#^[a-zA-Z][a-zA-Z0-9+.-]*://[^/:?#]*:\([0-9][0-9]*\).*$#\1#p')"
|
||||
[ -n "$_p" ] && { printf '%s' "$_p"; return; }
|
||||
case "$_u" in
|
||||
https://*) printf '443' ;;
|
||||
http://*) printf '80' ;;
|
||||
*) printf '443' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_with_timeout() {
|
||||
_secs="$1"
|
||||
shift
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "$_secs" "$@" 2>/dev/null
|
||||
else
|
||||
"$@" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_real_ip() {
|
||||
_host="$1"
|
||||
[ -n "$_host" ] || return 0
|
||||
for _dns in 223.5.5.5 119.29.29.29 1.1.1.1; do
|
||||
_ip="$(run_with_timeout 1 nslookup "$_host" "$_dns" | awk '/^Address: /{print $2} /^Address [0-9]+: /{print $3}' | grep -E '^[0-9]+(\.[0-9]+){3}$' | tail -n1)"
|
||||
[ -n "$_ip" ] && { printf '%s' "$_ip"; return; }
|
||||
done
|
||||
}
|
||||
|
||||
set -- -4 -L --max-time 5 --connect-timeout 3 -s -o /dev/null -w '%{http_code} %{time_total}'
|
||||
if [ "$mode" = "direct" ]; then
|
||||
# 强制不走任何代理,避免误读 http_proxy 环境变量
|
||||
set -- "$@" --noproxy '*'
|
||||
host="$(url_host "$url")"
|
||||
port="$(url_port "$url")"
|
||||
real_ip="$(resolve_real_ip "$host")"
|
||||
[ -n "$host" ] && [ -n "$port" ] && [ -n "$real_ip" ] && set -- "$@" --resolve "${host}:${port}:${real_ip}"
|
||||
else
|
||||
proxy_port="$(uci get clashoo.config.mixed_port 2>/dev/null)"
|
||||
[ -z "$proxy_port" ] && proxy_port="$(uci get clashoo.config.http_port 2>/dev/null)"
|
||||
[ -z "$proxy_port" ] && proxy_port=7890
|
||||
set -- "$@" -x "http://127.0.0.1:${proxy_port}"
|
||||
fi
|
||||
set -- "$@" "$url"
|
||||
|
||||
i=1
|
||||
while [ "$i" -le "$attempts" ]; do
|
||||
out="$(curl "$@" 2>/dev/null || true)"
|
||||
code="$(printf '%s' "$out" | awk '{print $1}')"
|
||||
time_s="$(printf '%s' "$out" | awk '{print $2}')"
|
||||
[ -n "$code" ] || code=000
|
||||
[ -n "$time_s" ] || time_s=0
|
||||
ms="$(awk -v t="$time_s" 'BEGIN{printf "%d", t*1000}')"
|
||||
last_code="$code"
|
||||
if [ "$code" = "200" ] || [ "$code" = "204" ] || [ "$code" = "301" ] || [ "$code" = "302" ]; then
|
||||
ok=$((ok + 1))
|
||||
sum_ms=$((sum_ms + ms))
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
loss=$((attempts - ok))
|
||||
avg_ms=0
|
||||
if [ "$ok" -gt 0 ]; then
|
||||
avg_ms=$((sum_ms / ok))
|
||||
fi
|
||||
|
||||
echo "ok=$ok attempts=$attempts loss=$loss avg_ms=$avg_ms code=$last_code"
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
CACHE_DIR="/tmp/clashoo"
|
||||
CACHE_FILE="/tmp/clashoo_check_cache"
|
||||
LOCK_DIR="${CACHE_DIR}/access_check.lock"
|
||||
LOCK_PID_FILE="${LOCK_DIR}/pid"
|
||||
UPDATING_FLAG="${CACHE_DIR}/access_check_updating"
|
||||
TMP_FILE="${CACHE_FILE}.tmp.$$"
|
||||
DIAG_LOG="/tmp/clashoo_access_check.log"
|
||||
|
||||
take_lock() {
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || return 1
|
||||
printf '%s\n' "$$" > "$LOCK_PID_FILE" 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
pid_is_cache_worker() {
|
||||
[ -r "/proc/$1/cmdline" ] || return 1
|
||||
tr '\0' ' ' <"/proc/$1/cmdline" 2>/dev/null | grep -q 'access_check_cache\.sh'
|
||||
}
|
||||
|
||||
lock_is_dead() {
|
||||
_pid="$(cat "$LOCK_PID_FILE" 2>/dev/null)"
|
||||
case "$_pid" in
|
||||
''|*[!0-9]*)
|
||||
[ -n "$(find "$LOCK_DIR" -maxdepth 0 -mmin +3 2>/dev/null)" ]
|
||||
return $?
|
||||
;;
|
||||
esac
|
||||
[ ! -d "/proc/$_pid" ] && return 0
|
||||
! pid_is_cache_worker "$_pid"
|
||||
}
|
||||
|
||||
mkdir -p "$CACHE_DIR"
|
||||
if ! take_lock; then
|
||||
lock_is_dead || exit 0
|
||||
rm -rf "$LOCK_DIR" >/dev/null 2>&1
|
||||
take_lock || exit 0
|
||||
fi
|
||||
trap 'rm -rf "$LOCK_DIR" "$UPDATING_FLAG" "$TMP_FILE"' EXIT INT TERM
|
||||
: > "$UPDATING_FLAG"
|
||||
|
||||
safe_int() {
|
||||
case "${1:-}" in
|
||||
''|*[!0-9]*)
|
||||
printf '0'
|
||||
;;
|
||||
*)
|
||||
printf '%s' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
safe_code() {
|
||||
case "${1:-}" in
|
||||
''|*[!0-9A-Za-z]*)
|
||||
printf '000'
|
||||
;;
|
||||
*)
|
||||
printf '%s' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
parse_field() {
|
||||
_line="$1"
|
||||
_key="$2"
|
||||
printf '%s\n' "$_line" | sed -n "s/.*${_key}=\\([^ ]*\\).*/\\1/p"
|
||||
}
|
||||
|
||||
probe_json() {
|
||||
_line="$1"
|
||||
_ok="$(safe_int "$(parse_field "$_line" "ok")")"
|
||||
_attempts="$(safe_int "$(parse_field "$_line" "attempts")")"
|
||||
_loss="$(safe_int "$(parse_field "$_line" "loss")")"
|
||||
_avg_ms="$(safe_int "$(parse_field "$_line" "avg_ms")")"
|
||||
_code="$(safe_code "$(parse_field "$_line" "code")")"
|
||||
|
||||
_state="down"
|
||||
if [ "$_ok" -ge "$_attempts" ] && [ "$_attempts" -gt 0 ]; then
|
||||
if [ "$_avg_ms" -ge 2500 ]; then
|
||||
_state="high_latency"
|
||||
else
|
||||
_state="ok"
|
||||
fi
|
||||
elif [ "$_ok" -gt 0 ]; then
|
||||
_state="loss"
|
||||
fi
|
||||
|
||||
_ok_bool=false
|
||||
[ "$_ok" -gt 0 ] && _ok_bool=true
|
||||
|
||||
printf '{"ok":%s,"state":"%s","code":"%s","ok_count":%s,"attempts":%s,"loss":%s,"avg_ms":%s}' \
|
||||
"$_ok_bool" "$_state" "$_code" "$_ok" "$_attempts" "$_loss" "$_avg_ms"
|
||||
}
|
||||
|
||||
log_diag() {
|
||||
_line="$(date '+%Y-%m-%d %H:%M:%S') $1"
|
||||
printf '%s\n' "$_line" >>"$DIAG_LOG" 2>/dev/null
|
||||
_n="$(wc -l <"$DIAG_LOG" 2>/dev/null || echo 0)"
|
||||
if [ "${_n:-0}" -gt 200 ]; then
|
||||
tail -n 100 "$DIAG_LOG" >"${DIAG_LOG}.tmp" 2>/dev/null && mv "${DIAG_LOG}.tmp" "$DIAG_LOG" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
probe_run() {
|
||||
_url="$1"
|
||||
_mode="$2"
|
||||
# nice + ionice:探测 IO/CPU 都低优先级,避免抢占 LuCI dispatcher
|
||||
nice -n 19 /usr/share/clashoo/net/access_check.sh "$_url" "$_mode" 2>/dev/null || true
|
||||
}
|
||||
|
||||
proxy_port="$(uci -q get clashoo.config.mixed_port)"
|
||||
[ -z "$proxy_port" ] && proxy_port="$(uci -q get clashoo.config.http_port)"
|
||||
[ -z "$proxy_port" ] && proxy_port="7890"
|
||||
tcp_mode="$(uci -q get clashoo.config.tcp_mode)"
|
||||
[ -z "$tcp_mode" ] && tcp_mode="redirect"
|
||||
udp_mode="$(uci -q get clashoo.config.udp_mode)"
|
||||
[ -z "$udp_mode" ] && udp_mode="$tcp_mode"
|
||||
|
||||
proxy_listening() {
|
||||
# 检测 mixed-port 是否在 LISTEN(clashoo 未运行时立即标 proxy down,避免显示假绿)
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -tln 2>/dev/null | awk -v p=":${proxy_port}" '$0 ~ "LISTEN" && index($4, p) {found=1; exit} END{exit !found}'
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
netstat -tln 2>/dev/null | awk -v p=":${proxy_port}" '$0 ~ "LISTEN" && index($4, p) {found=1; exit} END{exit !found}'
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 并行探测,把 CPU 抢占窗口从串行 2s+ 压缩到最慢一路的耗时
|
||||
f_db="${TMP_FILE}.db"
|
||||
f_dy="${TMP_FILE}.dy"
|
||||
f_pb="${TMP_FILE}.pb"
|
||||
f_py="${TMP_FILE}.py"
|
||||
probe_run "https://www.douyin.com/generate_204" "direct" >"$f_db" &
|
||||
probe_run "https://www.youtube.com/generate_204" "direct" >"$f_dy" &
|
||||
if proxy_listening; then
|
||||
proxy_skipped=0
|
||||
probe_run "https://www.douyin.com/generate_204" "proxy" >"$f_pb" &
|
||||
probe_run "https://www.youtube.com/generate_204" "proxy" >"$f_py" &
|
||||
else
|
||||
# 代理端口未监听(clashoo 已停止),跳过探测,直接标 down
|
||||
proxy_skipped=1
|
||||
echo "ok=0 attempts=1 loss=1 avg_ms=0 code=skip" >"$f_pb"
|
||||
echo "ok=0 attempts=1 loss=1 avg_ms=0 code=skip" >"$f_py"
|
||||
fi
|
||||
wait
|
||||
direct_bytedance="$(cat "$f_db" 2>/dev/null)"
|
||||
direct_youtube="$(cat "$f_dy" 2>/dev/null)"
|
||||
proxy_bytedance="$(cat "$f_pb" 2>/dev/null)"
|
||||
proxy_youtube="$(cat "$f_py" 2>/dev/null)"
|
||||
rm -f "$f_db" "$f_dy" "$f_pb" "$f_py"
|
||||
|
||||
updated_at="$(date +%s)"
|
||||
|
||||
probe_all_down() {
|
||||
for _p in "$direct_bytedance" "$direct_youtube" "$proxy_bytedance" "$proxy_youtube"; do
|
||||
case "$_p" in
|
||||
''|*"ok=0 "*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ "$(uci -q get clashoo.config.access_check_debug)" = "1" ] || probe_all_down; then
|
||||
log_diag "port=${proxy_port} proxy_skipped=${proxy_skipped} direct_bd=[${direct_bytedance}] direct_yt=[${direct_youtube}] proxy_bd=[${proxy_bytedance}] proxy_yt=[${proxy_youtube}]"
|
||||
fi
|
||||
|
||||
cat >"$TMP_FILE" <<EOF
|
||||
{
|
||||
"proxy_port": "${proxy_port}",
|
||||
"tcp_mode": "${tcp_mode}",
|
||||
"udp_mode": "${udp_mode}",
|
||||
"updated_at": ${updated_at},
|
||||
"proxy_skipped": ${proxy_skipped},
|
||||
"stale": false,
|
||||
"updating": false,
|
||||
"direct": {
|
||||
"bytedance": $(probe_json "$direct_bytedance"),
|
||||
"youtube": $(probe_json "$direct_youtube")
|
||||
},
|
||||
"proxy": {
|
||||
"bytedance": $(probe_json "$proxy_bytedance"),
|
||||
"youtube": $(probe_json "$proxy_youtube")
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
mv -f "$TMP_FILE" "$CACHE_FILE"
|
||||
exit 0
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
RUNDIR="/tmp/clashoo"
|
||||
PID_FILE="${RUNDIR}/access_check_daemon.pid"
|
||||
LOCK_DIR="${RUNDIR}/access_check_daemon.lock"
|
||||
# 周期:默认 30s,ACCESS_CHECK_INTERVAL 环境变量可固定覆盖
|
||||
INTERVAL_FIXED="${ACCESS_CHECK_INTERVAL:-}"
|
||||
|
||||
mkdir -p "$RUNDIR"
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
old_pid="$(cat "$PID_FILE" 2>/dev/null)"
|
||||
if [ -n "$old_pid" ] && [ -d "/proc/$old_pid" ] && \
|
||||
tr '\0' ' ' <"/proc/$old_pid/cmdline" 2>/dev/null | grep -q 'access_check_daemon\.sh'; then
|
||||
exit 0
|
||||
fi
|
||||
rm -rf "$LOCK_DIR" "$PID_FILE" >/dev/null 2>&1
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$LOCK_DIR" "$PID_FILE"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'cleanup; exit 0' INT TERM
|
||||
|
||||
echo "$$" >"$PID_FILE"
|
||||
|
||||
while :; do
|
||||
[ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ] || break
|
||||
/usr/share/clashoo/net/access_check_cache.sh >/dev/null 2>&1 || true
|
||||
if [ -n "$INTERVAL_FIXED" ]; then
|
||||
sleep "$INTERVAL_FIXED"
|
||||
else
|
||||
sleep 30
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,240 @@
|
||||
#!/bin/sh
|
||||
|
||||
APPLY=0
|
||||
TIMEOUT="${CLASHOO_DNS_TEST_TIMEOUT:-2}"
|
||||
START_TS="$(date +%s 2>/dev/null || echo 0)"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--apply) APPLY=1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
json_escape() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
json_pair() {
|
||||
printf '"%s":"%s"' "$1" "$(json_escape "$2")"
|
||||
}
|
||||
|
||||
is_ipv4() {
|
||||
echo "$1" | grep -Eq '^[0-9]+(\.[0-9]+){3}$'
|
||||
}
|
||||
|
||||
append_unique() {
|
||||
list="$1"
|
||||
item="$2"
|
||||
[ -n "$item" ] || { printf '%s' "$list"; return; }
|
||||
case " $list " in
|
||||
*" $item "*) printf '%s' "$list" ;;
|
||||
*) printf '%s %s' "$list" "$item" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
current_default_nameservers() {
|
||||
uci -q get clashoo.config.default_nameserver 2>/dev/null || true
|
||||
}
|
||||
|
||||
current_dns_servers() {
|
||||
uci -q show clashoo 2>/dev/null | awk -F= '
|
||||
function clean(v) {
|
||||
gsub(/^'\''|'\''$/, "", v);
|
||||
return v;
|
||||
}
|
||||
/^clashoo\.@dnsservers\[[0-9]+\]\./ {
|
||||
line=$1;
|
||||
val=clean($2);
|
||||
sub(/^clashoo\.@dnsservers\[/, "", line);
|
||||
idx=line;
|
||||
sub(/\].*$/, "", idx);
|
||||
opt=line;
|
||||
sub(/^[0-9]+\]\./, "", opt);
|
||||
data[idx, opt]=val;
|
||||
seen[idx]=1;
|
||||
}
|
||||
END {
|
||||
for (idx in seen) {
|
||||
if (data[idx, "enabled"] == "0") continue;
|
||||
role=data[idx, "ser_type"];
|
||||
addr=data[idx, "ser_address"];
|
||||
proto=data[idx, "protocol"];
|
||||
port=data[idx, "ser_port"];
|
||||
if (addr == "") continue;
|
||||
if (proto != "" && proto != "none" && addr !~ /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//) {
|
||||
addr=proto addr;
|
||||
if (port != "" && addr !~ /:[0-9]+$/) addr=addr ":" port;
|
||||
}
|
||||
print role "|" addr;
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
servers_for_role() {
|
||||
role="$1"
|
||||
current_dns_servers | awk -F'|' -v role="$role" '$1 == role { print $2 }'
|
||||
}
|
||||
|
||||
candidate_host_port() {
|
||||
uri="$1"
|
||||
default_port="$2"
|
||||
hostport="${uri#*://}"
|
||||
hostport="${hostport%%/*}"
|
||||
host="${hostport%%:*}"
|
||||
port="${hostport##*:}"
|
||||
[ "$port" != "$hostport" ] || port="$default_port"
|
||||
printf '%s %s\n' "$host" "$port"
|
||||
}
|
||||
|
||||
probe_tls() {
|
||||
uri="$1"
|
||||
set -- $(candidate_host_port "$uri" 853)
|
||||
host="$1"
|
||||
port="$2"
|
||||
command -v nc >/dev/null 2>&1 || return 1
|
||||
nc -w "$TIMEOUT" "$host" "$port" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
probe_ip_dns() {
|
||||
ip="$1"
|
||||
domain="$2"
|
||||
command -v nslookup >/dev/null 2>&1 || return 1
|
||||
nslookup "$domain" "$ip" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
probe_latency_ms() {
|
||||
candidate="$1"
|
||||
domain="$2"
|
||||
if [ "${candidate#https://}" != "$candidate" ]; then
|
||||
if printf '%s' "$candidate" | grep -q '?'; then
|
||||
q="$candidate&name=$domain&type=A"
|
||||
else
|
||||
q="$candidate?name=$domain&type=A"
|
||||
fi
|
||||
command -v curl >/dev/null 2>&1 || return 1
|
||||
t="$(curl -fsS --connect-timeout "$TIMEOUT" --max-time "$TIMEOUT" \
|
||||
-H 'accept: application/dns-json' -o /dev/null -w '%{time_total}' "$q" 2>/dev/null)" || return 1
|
||||
printf '%s' "$t" | awk '{ms=$1*1000; if (ms<1) ms=1; printf "%d", ms}'
|
||||
elif [ "${candidate#tls://}" != "$candidate" ]; then
|
||||
probe_tls "$candidate" || return 1
|
||||
printf '900'
|
||||
elif [ "${candidate#udp://}" != "$candidate" ]; then
|
||||
probe_ip_dns "${candidate#udp://}" "$domain" || return 1
|
||||
printf '950'
|
||||
elif [ "${candidate#tcp://}" != "$candidate" ]; then
|
||||
probe_ip_dns "${candidate#tcp://}" "$domain" || return 1
|
||||
printf '950'
|
||||
else
|
||||
is_ipv4 "$candidate" && probe_ip_dns "$candidate" "$domain" || return 1
|
||||
printf '950'
|
||||
fi
|
||||
}
|
||||
|
||||
select_best() {
|
||||
list="$1"
|
||||
domain="$2"
|
||||
fallback="$3"
|
||||
best=""
|
||||
best_score=999999
|
||||
order=0
|
||||
for candidate in $list; do
|
||||
order=$((order + 1))
|
||||
ms="$(probe_latency_ms "$candidate" "$domain")" || { FAILED_COUNT=$((FAILED_COUNT + 1)); continue; }
|
||||
score=$((ms * 100 + order))
|
||||
if [ "$score" -lt "$best_score" ]; then
|
||||
best="$candidate"
|
||||
best_score="$score"
|
||||
fi
|
||||
done
|
||||
[ -n "$best" ] || best="$fallback"
|
||||
printf '%s' "$best"
|
||||
}
|
||||
|
||||
add_dnsserver() {
|
||||
role="$1"
|
||||
addr="$2"
|
||||
proto="$3"
|
||||
port="$4"
|
||||
sec="$(uci -q add clashoo dnsservers)" || return 1
|
||||
[ -n "$sec" ] || return 1
|
||||
uci -q set clashoo."$sec".enabled='1' || return 1
|
||||
uci -q set clashoo."$sec".ser_type="$role" || return 1
|
||||
uci -q set clashoo."$sec".ser_address="$addr" || return 1
|
||||
uci -q set clashoo."$sec".protocol="$proto" || return 1
|
||||
if [ -n "$port" ]; then
|
||||
uci -q set clashoo."$sec".ser_port="$port" || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
set_default_nameserver() {
|
||||
uci -q delete clashoo.config.default_nameserver >/dev/null 2>&1 || true
|
||||
uci -q add_list clashoo.config.default_nameserver="$1" || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_result() {
|
||||
uci -q set clashoo.config.enable_dns='1' || { uci -q revert clashoo; return 1; }
|
||||
set_default_nameserver "$BOOTSTRAP" || { uci -q revert clashoo; return 1; }
|
||||
while uci -q delete clashoo.@dnsservers[0] >/dev/null 2>&1; do :; done
|
||||
add_dnsserver 'nameserver' "$NAMESERVER" 'none' '' || { uci -q revert clashoo; return 1; }
|
||||
add_dnsserver 'direct-nameserver' "$BOOTSTRAP" 'udp://' '' || { uci -q revert clashoo; return 1; }
|
||||
add_dnsserver 'proxy-server-nameserver' "$PROXY_NS" 'none' '' || { uci -q revert clashoo; return 1; }
|
||||
add_dnsserver 'fallback' "$FALLBACK_NS" 'none' '' || { uci -q revert clashoo; return 1; }
|
||||
uci -q commit clashoo || { uci -q revert clashoo; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
is_domestic_dns() {
|
||||
printf '%s' "$1" | grep -qE 'alidns|doh\.pub|dnspod|223\.5\.5\.5|223\.6\.6\.6|119\.29\.29\.29|119\.28\.28\.28|114\.114|180\.184|onedns'
|
||||
}
|
||||
is_overseas_dns() {
|
||||
printf '%s' "$1" | grep -qE 'cloudflare|1\.1\.1\.1|1\.0\.0\.1|one\.one\.one\.one|dns\.google|8\.8\.8\.8|8\.8\.4\.4|9\.9\.9\.9|quad9|opendns|208\.67\.'
|
||||
}
|
||||
|
||||
FAILED_COUNT=0
|
||||
BOOTSTRAP_CANDIDATES="223.5.5.5 119.29.29.29 180.184.1.1"
|
||||
DOMESTIC_CANDIDATES="https://dns.alidns.com/dns-query https://doh.pub/dns-query udp://223.5.5.5 udp://119.29.29.29"
|
||||
FALLBACK_CANDIDATES="https://cloudflare-dns.com/dns-query https://dns.google/dns-query tls://1.1.1.1:853"
|
||||
|
||||
for ns in $(current_default_nameservers); do
|
||||
is_ipv4 "$ns" && BOOTSTRAP_CANDIDATES="$(append_unique "$BOOTSTRAP_CANDIDATES" "$ns")"
|
||||
done
|
||||
for ns in $(servers_for_role 'nameserver') $(servers_for_role 'direct-nameserver'); do
|
||||
is_domestic_dns "$ns" && DOMESTIC_CANDIDATES="$(append_unique "$DOMESTIC_CANDIDATES" "$ns")"
|
||||
done
|
||||
for ns in $(servers_for_role 'fallback'); do
|
||||
is_overseas_dns "$ns" && FALLBACK_CANDIDATES="$(append_unique "$FALLBACK_CANDIDATES" "$ns")"
|
||||
done
|
||||
|
||||
BOOTSTRAP="$(select_best "$BOOTSTRAP_CANDIDATES" www.baidu.com 223.5.5.5)"
|
||||
NAMESERVER="$(select_best "$DOMESTIC_CANDIDATES" www.baidu.com https://dns.alidns.com/dns-query)"
|
||||
PROXY_NS="tls://1.1.1.1:853"
|
||||
FALLBACK_NS="$(select_best "$FALLBACK_CANDIDATES" www.google.com https://cloudflare-dns.com/dns-query)"
|
||||
|
||||
APPLIED=false
|
||||
if [ "$APPLY" = "1" ]; then
|
||||
if apply_result; then
|
||||
APPLIED=true
|
||||
else
|
||||
printf '{"success":false,"error":"apply_failed","message":"DNS 自动配置写入失败"}\n'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
END_TS="$(date +%s 2>/dev/null || echo "$START_TS")"
|
||||
ELAPSED_MS=$(( (END_TS - START_TS) * 1000 ))
|
||||
|
||||
printf '{'
|
||||
printf '"success":true,'
|
||||
printf '"applied":%s,' "$APPLIED"
|
||||
printf '"restarted":false,'
|
||||
json_pair bootstrap "$BOOTSTRAP"; printf ','
|
||||
json_pair nameserver "$NAMESERVER"; printf ','
|
||||
json_pair direct_nameserver "$BOOTSTRAP"; printf ','
|
||||
json_pair proxy_nameserver "$PROXY_NS"; printf ','
|
||||
json_pair fallback "$FALLBACK_NS"; printf ','
|
||||
printf '"failed_count":%s,' "$FAILED_COUNT"
|
||||
printf '"elapsed_ms":%s' "$ELAPSED_MS"
|
||||
printf '}\n'
|
||||
Executable
+898
@@ -0,0 +1,898 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
NFT_DIR="/var/run/clash"
|
||||
SETS_RULES="${NFT_DIR}/fw4_sets.nft"
|
||||
DSTNAT_RULES="${NFT_DIR}/fw4_dstnat.nft"
|
||||
MANGLE_RULES="${NFT_DIR}/fw4_mangle.nft"
|
||||
OUTPUT_RULES="${NFT_DIR}/fw4_output.nft"
|
||||
BUILTIN_NFT_DIR="/usr/share/clashoo/nftables"
|
||||
GEOIP_CN_NFT="${BUILTIN_NFT_DIR}/geoip_cn.nft"
|
||||
GEOIP6_CN_NFT="${BUILTIN_NFT_DIR}/geoip6_cn.nft"
|
||||
LOCAL_OUTPUT_TABLE="clashoo_local"
|
||||
QUIC_BLOCK_TABLE="clashoo_quic"
|
||||
# PROXY_FWMARK: inbound TPROXY mark, ip rule -> table PROXY_ROUTE_TABLE -> lo.
|
||||
# CORE_ROUTING_MARK: mihomo outbound SO_MARK (= routing-mark in config).
|
||||
# Must differ: otherwise mihomo egress is pulled to lo -> unreachable.
|
||||
# When changing CORE_ROUTING_MARK, also update yum_change.sh routing_mark_dec.
|
||||
PROXY_FWMARK="0x162"
|
||||
PROXY_ROUTE_TABLE="0x162"
|
||||
CORE_ROUTING_MARK="0x1a0a" # = 6666
|
||||
ACL_BYPASS_FWMARK="0x163"
|
||||
SINGBOX_BYPASS_FWMARK="0x2024"
|
||||
ACL_BYPASS_PREF="8998"
|
||||
DNSMASQ_BYPASS_PREF="8999"
|
||||
|
||||
uci_get() {
|
||||
uci -q get "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
bool_enabled() {
|
||||
case "$1" in
|
||||
1|true|TRUE|yes|on) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
acl_bool() {
|
||||
[ -n "$1" ] || return 0
|
||||
bool_enabled "$1"
|
||||
}
|
||||
|
||||
tun_available() {
|
||||
ip tuntap add mode tun name cotuntest >/dev/null 2>&1 || return 1
|
||||
ip link del cotuntest >/dev/null 2>&1 || true
|
||||
return 0
|
||||
}
|
||||
|
||||
config_redir_port() {
|
||||
uci_get clashoo.config.redir_port
|
||||
}
|
||||
|
||||
config_tproxy_port() {
|
||||
local port
|
||||
port="$(uci_get clashoo.config.tproxy_port)"
|
||||
if [ -n "$port" ]; then
|
||||
printf '%s\n' "$port"
|
||||
else
|
||||
config_redir_port
|
||||
fi
|
||||
}
|
||||
|
||||
config_tcp_mode() {
|
||||
uci_get clashoo.config.tcp_mode
|
||||
}
|
||||
|
||||
config_udp_mode() {
|
||||
uci_get clashoo.config.udp_mode
|
||||
}
|
||||
|
||||
config_access_control() {
|
||||
[ "$(uci_get clashoo.config.acl_migrated)" = "1" ] && {
|
||||
printf '0\n'
|
||||
return
|
||||
}
|
||||
uci_get clashoo.config.access_control
|
||||
}
|
||||
|
||||
acl_sections() {
|
||||
uci -q show clashoo 2>/dev/null | sed -n 's/^clashoo\.\([^.=]*\)=lan_acl$/\1/p'
|
||||
}
|
||||
|
||||
acl_list() {
|
||||
uci_get "$1"
|
||||
}
|
||||
|
||||
acl_sections_catchall_last() {
|
||||
local section
|
||||
for section in $(acl_sections); do
|
||||
[ -z "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] || printf '%s\n' "$section"
|
||||
done
|
||||
for section in $(acl_sections); do
|
||||
[ -n "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] || printf '%s\n' "$section"
|
||||
done
|
||||
}
|
||||
|
||||
grouped_acl_enabled() {
|
||||
local section
|
||||
[ "$(uci_get clashoo.config.acl_migrated)" = "1" ] || return 1
|
||||
for section in $(acl_sections); do
|
||||
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
acl_has_catchall() {
|
||||
local section
|
||||
for section in $(acl_sections); do
|
||||
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue
|
||||
[ -z "$(acl_list "clashoo.${section}.ip")$(acl_list "clashoo.${section}.ip6")$(acl_list "clashoo.${section}.mac")" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
lan_acl_enabled() {
|
||||
grouped_acl_enabled && return 0
|
||||
case "$(config_access_control)" in
|
||||
1|2) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
config_enable_dns() {
|
||||
uci_get clashoo.config.enable_dns
|
||||
}
|
||||
|
||||
config_dns_port() {
|
||||
local port=""
|
||||
if [ "$(uci_get clashoo.config.core_type)" != "singbox" ]; then
|
||||
port="$(sed -n 's/^[[:space:]]*listen:.*:[[:space:]]*['"'"'"]*\([0-9]\{1,\}\)['"'"'"]*[[:space:]]*$/\1/p' /etc/clashoo/config.yaml 2>/dev/null | head -n1)"
|
||||
fi
|
||||
[ -n "$port" ] || port="$(uci_get clashoo.config.listen_port)"
|
||||
printf '%s' "$port" | grep -Eq '^[0-9]+$' || port=1053
|
||||
printf '%s\n' "$port"
|
||||
}
|
||||
|
||||
lan_dns_split_enabled() {
|
||||
lan_acl_enabled || return 1
|
||||
bool_enabled "$(config_enable_dns)" || return 1
|
||||
bool_enabled "$(config_ipv4_dns_hijack)" || bool_enabled "$(config_ipv6_dns_hijack)"
|
||||
}
|
||||
|
||||
tun_acl_enabled() {
|
||||
lan_acl_enabled || return 1
|
||||
[ "$(config_tcp_mode)" = "tun" ] || [ "$(config_udp_mode)" = "tun" ]
|
||||
}
|
||||
|
||||
config_ipv4_dns_hijack() {
|
||||
uci_get clashoo.config.ipv4_dns_hijack
|
||||
}
|
||||
|
||||
config_ipv6_dns_hijack() {
|
||||
uci_get clashoo.config.ipv6_dns_hijack
|
||||
}
|
||||
|
||||
config_ipv4_proxy() {
|
||||
uci_get clashoo.config.ipv4_proxy
|
||||
}
|
||||
|
||||
config_ipv6_proxy() {
|
||||
uci_get clashoo.config.ipv6_proxy
|
||||
}
|
||||
|
||||
singbox_tun_active() {
|
||||
[ "$(uci_get clashoo.config.core_type)" = "singbox" ] || return 1
|
||||
[ "$(config_tcp_mode)" = "tun" ] || [ "$(config_udp_mode)" = "tun" ]
|
||||
}
|
||||
|
||||
config_bypass_china() {
|
||||
uci_get clashoo.config.bypass_china
|
||||
}
|
||||
|
||||
config_bypass_china_ipv6() {
|
||||
local value
|
||||
value="$(uci_get clashoo.config.bypass_china_ipv6)"
|
||||
if [ -n "$value" ]; then
|
||||
printf '%s\n' "$value"
|
||||
else
|
||||
config_bypass_china
|
||||
fi
|
||||
}
|
||||
|
||||
config_block_quic() {
|
||||
uci_get clashoo.config.block_quic
|
||||
}
|
||||
|
||||
config_bypass_port_mode() {
|
||||
uci_get clashoo.config.bypass_port_mode
|
||||
}
|
||||
|
||||
config_bypass_port_custom() {
|
||||
uci_get clashoo.config.bypass_port_custom
|
||||
}
|
||||
|
||||
config_legacy_bypass_port() {
|
||||
uci_list clashoo.config.bypass_port
|
||||
}
|
||||
|
||||
config_proxy_tcp_dport() {
|
||||
local mode custom legacy value
|
||||
mode="$(config_bypass_port_mode)"
|
||||
custom="$(config_bypass_port_custom)"
|
||||
legacy="$(uci_get clashoo.config.proxy_tcp_dport)"
|
||||
[ -z "$legacy" ] && legacy="$(config_legacy_bypass_port)"
|
||||
|
||||
case "$mode" in
|
||||
all)
|
||||
# 空值在 render_port_match 中表示该协议的全部端口
|
||||
printf '%s\n' ''
|
||||
;;
|
||||
common)
|
||||
printf '%s\n' '22,53,80,443,8080,8443'
|
||||
;;
|
||||
custom)
|
||||
value="$custom"
|
||||
[ -z "$value" ] && value="$legacy"
|
||||
printf '%s\n' "$value"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$legacy"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
config_proxy_udp_dport() {
|
||||
local mode custom legacy value
|
||||
mode="$(config_bypass_port_mode)"
|
||||
custom="$(config_bypass_port_custom)"
|
||||
legacy="$(uci_get clashoo.config.proxy_udp_dport)"
|
||||
[ -z "$legacy" ] && legacy="$(config_legacy_bypass_port)"
|
||||
|
||||
case "$mode" in
|
||||
all)
|
||||
printf '%s\n' ''
|
||||
;;
|
||||
common)
|
||||
printf '%s\n' '22,53,80,443,8080,8443'
|
||||
;;
|
||||
custom)
|
||||
value="$custom"
|
||||
[ -z "$value" ] && value="$legacy"
|
||||
printf '%s\n' "$value"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$legacy"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
config_bypass_dscp() {
|
||||
uci_list clashoo.config.bypass_dscp
|
||||
}
|
||||
|
||||
config_bypass_fwmark() {
|
||||
uci_list clashoo.config.bypass_fwmark
|
||||
}
|
||||
|
||||
config_fake_ip_range6() {
|
||||
local value
|
||||
value="$(uci_get clashoo.config.fake_ip_range6)"
|
||||
[ -n "$value" ] || value="fc00::/18"
|
||||
[ "$(uci_get clashoo.config.enhanced_mode)" = "redir-host" ] && return 0
|
||||
bool_enabled "$(uci_get clashoo.config.enable_ipv6)" || return 0
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
config_fake_ip_range() {
|
||||
local value
|
||||
value="$(uci_get clashoo.config.fake_ip_range)"
|
||||
[ -n "$value" ] && {
|
||||
printf '%s\n' "$value"
|
||||
return
|
||||
}
|
||||
printf '198.18.0.1/16\n'
|
||||
}
|
||||
|
||||
uci_list() {
|
||||
local key="$1"
|
||||
uci -q show "$key" 2>/dev/null | sed -n "s/^${key}=//p" | sed "s/'//g"
|
||||
}
|
||||
|
||||
ensure_firewall_include() {
|
||||
local name="$1"
|
||||
local path="$2"
|
||||
local chain="${3:-}"
|
||||
local position="${4:-chain-pre}"
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set firewall.${name}=include
|
||||
set firewall.${name}.type='nftables'
|
||||
set firewall.${name}.path='${path}'
|
||||
set firewall.${name}.position='${position}'
|
||||
$( [ -n "$chain" ] && printf "set firewall.%s.chain='%s'\n" "$name" "$chain" )
|
||||
commit firewall
|
||||
EOF
|
||||
}
|
||||
|
||||
remove_firewall_include() {
|
||||
local name="$1"
|
||||
uci -q delete firewall."${name}" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
render_common_returns() {
|
||||
local fake6
|
||||
fake6="$(config_fake_ip_range6)"
|
||||
printf '%s\n' 'meta nfproto ipv4 ip daddr { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 } return'
|
||||
if [ -n "$fake6" ]; then
|
||||
printf 'meta nfproto ipv6 ip6 daddr { ::1/128, fc00::/7, fe80::/10, ff00::/8 } ip6 daddr != %s return\n' "$fake6"
|
||||
else
|
||||
printf '%s\n' 'meta nfproto ipv6 ip6 daddr { ::1/128, fc00::/7, fe80::/10, ff00::/8 } return'
|
||||
fi
|
||||
}
|
||||
|
||||
render_ip_elements() {
|
||||
local list="$1"
|
||||
local first=1 entry
|
||||
for entry in $list; do
|
||||
if [ "$first" -eq 0 ]; then
|
||||
printf ', '
|
||||
fi
|
||||
printf '%s' "$entry"
|
||||
first=0
|
||||
done
|
||||
}
|
||||
|
||||
render_token_elements() {
|
||||
printf '%s\n' "$1" | tr ',\t' ' ' | awk '
|
||||
BEGIN { first = 1 }
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i == "")
|
||||
continue
|
||||
if (!first)
|
||||
printf ", "
|
||||
printf "%s", $i
|
||||
first = 0
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
detect_coexist_fwmarks() {
|
||||
# Merge co-installed proxy plugin fwmarks into bypass to avoid intercepting
|
||||
# their traffic. Only when init.d exists (passwall:0x1 passwall2:0xff
|
||||
# nikki:tproxy 0x80/tun 0x81 mask 0xff).
|
||||
local marks=""
|
||||
[ -x /etc/init.d/passwall ] && marks="$marks 0x1"
|
||||
[ -x /etc/init.d/passwall2 ] && marks="$marks 0xff"
|
||||
if [ -x /etc/init.d/nikki ]; then
|
||||
local nm
|
||||
nm="$(uci -q get nikki.routing.tproxy_fw_mark) $(uci -q get nikki.routing.tun_fw_mark)"
|
||||
[ -z "$(echo "$nm" | tr -d ' ')" ] && nm="0x80 0x81"
|
||||
marks="$marks $nm"
|
||||
fi
|
||||
printf '%s\n' "$marks"
|
||||
}
|
||||
|
||||
merge_fwmark_tokens() {
|
||||
# Always bypass PROXY_FWMARK (inbound TPROXY) and CORE_ROUTING_MARK (core egress)
|
||||
# so nft never traps them. Also merge co-installed plugin fwmarks for coexistence.
|
||||
local coexist
|
||||
coexist="$(detect_coexist_fwmarks)"
|
||||
printf '%s %s %s %s\n' "$1" "$PROXY_FWMARK" "$CORE_ROUTING_MARK" "$coexist" | tr ',\t' ' ' | awk '
|
||||
BEGIN { first = 1 }
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i == "" || seen[$i]++)
|
||||
continue
|
||||
if (!first)
|
||||
printf ", "
|
||||
printf "%s", $i
|
||||
first = 0
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
render_port_match() {
|
||||
local proto="$1"
|
||||
local ports="$2"
|
||||
local port_elements
|
||||
|
||||
port_elements="$(render_token_elements "$ports")"
|
||||
if [ -n "$port_elements" ]; then
|
||||
printf 'meta l4proto %s %s dport { %s }' "$proto" "$proto" "$port_elements"
|
||||
else
|
||||
printf 'meta l4proto %s' "$proto"
|
||||
fi
|
||||
}
|
||||
|
||||
render_acl_dns_rules() {
|
||||
local redirect_port="$1" section ipv4 ipv6 mac ipv4_elements ipv6_elements mac_elements action
|
||||
for section in $(acl_sections_catchall_last); do
|
||||
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue
|
||||
if acl_bool "$(uci_get "clashoo.${section}.dns")"; then
|
||||
action="counter redirect to :${redirect_port}"
|
||||
elif singbox_tun_active; then
|
||||
action="meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return"
|
||||
else
|
||||
action="counter return"
|
||||
fi
|
||||
ipv4="$(acl_list "clashoo.${section}.ip")"
|
||||
ipv6="$(acl_list "clashoo.${section}.ip6")"
|
||||
mac="$(acl_list "clashoo.${section}.mac")"
|
||||
ipv4_elements="$(render_ip_elements "$ipv4")"
|
||||
ipv6_elements="$(render_ip_elements "$ipv6")"
|
||||
mac_elements="$(render_ip_elements "$mac")"
|
||||
|
||||
if [ -z "$ipv4_elements$ipv6_elements$mac_elements" ]; then
|
||||
bool_enabled "$(config_ipv4_dns_hijack)" && printf 'meta nfproto ipv4 meta l4proto { tcp, udp } th dport 53 %s\n' "$action"
|
||||
bool_enabled "$(config_ipv6_dns_hijack)" && printf 'meta nfproto ipv6 meta l4proto { tcp, udp } th dport 53 %s\n' "$action"
|
||||
continue
|
||||
fi
|
||||
if [ -n "$ipv4_elements" ] && bool_enabled "$(config_ipv4_dns_hijack)"; then
|
||||
printf 'meta nfproto ipv4 ip saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$ipv4_elements" "$action"
|
||||
fi
|
||||
if [ -n "$ipv6_elements" ] && bool_enabled "$(config_ipv6_dns_hijack)"; then
|
||||
printf 'meta nfproto ipv6 ip6 saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$ipv6_elements" "$action"
|
||||
fi
|
||||
if [ -n "$mac_elements" ]; then
|
||||
bool_enabled "$(config_ipv4_dns_hijack)" && printf 'meta nfproto ipv4 ether saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$mac_elements" "$action"
|
||||
bool_enabled "$(config_ipv6_dns_hijack)" && printf 'meta nfproto ipv6 ether saddr { %s } meta l4proto { tcp, udp } th dport 53 %s\n' "$mac_elements" "$action"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
render_acl_proxy_rules() {
|
||||
local mode="$1" proto="$2" port_match="$3" target_port="$4"
|
||||
local section ipv4 ipv6 mac ipv4_elements ipv6_elements mac_elements enabled action4 action6
|
||||
|
||||
for section in $(acl_sections_catchall_last); do
|
||||
[ "$(uci_get "clashoo.${section}.enabled")" != "0" ] || continue
|
||||
enabled="$(uci_get "clashoo.${section}.proxy")"
|
||||
case "$mode" in
|
||||
redirect)
|
||||
if acl_bool "$enabled"; then
|
||||
if singbox_tun_active; then
|
||||
action4="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} counter redirect to :${target_port}"
|
||||
else
|
||||
action4="${port_match} counter redirect to :${target_port}"
|
||||
fi
|
||||
action6="$action4"
|
||||
elif singbox_tun_active; then
|
||||
action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return"
|
||||
action6="$action4"
|
||||
else
|
||||
action4="meta l4proto ${proto} counter return"
|
||||
action6="$action4"
|
||||
fi
|
||||
;;
|
||||
tproxy)
|
||||
if acl_bool "$enabled"; then
|
||||
if singbox_tun_active; then
|
||||
action4="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} tproxy ip to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
|
||||
action6="${port_match} ct mark set ${SINGBOX_BYPASS_FWMARK} tproxy ip6 to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
|
||||
else
|
||||
action4="${port_match} tproxy ip to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
|
||||
action6="${port_match} tproxy ip6 to :${target_port} meta mark set ${PROXY_FWMARK} counter accept"
|
||||
fi
|
||||
elif singbox_tun_active; then
|
||||
action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter return"
|
||||
action6="$action4"
|
||||
else
|
||||
action4="meta l4proto ${proto} counter return"
|
||||
action6="$action4"
|
||||
fi
|
||||
;;
|
||||
tun)
|
||||
if acl_bool "$enabled"; then
|
||||
action4="meta l4proto ${proto} counter return"
|
||||
elif singbox_tun_active; then
|
||||
action4="meta l4proto ${proto} meta mark set ${SINGBOX_BYPASS_FWMARK} ct mark set meta mark counter accept"
|
||||
else
|
||||
action4="meta l4proto ${proto} meta mark set ${ACL_BYPASS_FWMARK} counter accept"
|
||||
fi
|
||||
action6="$action4"
|
||||
;;
|
||||
esac
|
||||
|
||||
ipv4="$(acl_list "clashoo.${section}.ip")"
|
||||
ipv6="$(acl_list "clashoo.${section}.ip6")"
|
||||
mac="$(acl_list "clashoo.${section}.mac")"
|
||||
ipv4_elements="$(render_ip_elements "$ipv4")"
|
||||
ipv6_elements="$(render_ip_elements "$ipv6")"
|
||||
mac_elements="$(render_ip_elements "$mac")"
|
||||
|
||||
if [ -z "$ipv4_elements$ipv6_elements$mac_elements" ]; then
|
||||
bool_enabled "$(config_ipv4_proxy)" && printf 'meta nfproto ipv4 %s\n' "$action4"
|
||||
bool_enabled "$(config_ipv6_proxy)" && printf 'meta nfproto ipv6 %s\n' "$action6"
|
||||
continue
|
||||
fi
|
||||
if [ -n "$ipv4_elements" ] && bool_enabled "$(config_ipv4_proxy)"; then
|
||||
printf 'meta nfproto ipv4 ip saddr { %s } %s\n' "$ipv4_elements" "$action4"
|
||||
fi
|
||||
if [ -n "$ipv6_elements" ] && bool_enabled "$(config_ipv6_proxy)"; then
|
||||
printf 'meta nfproto ipv6 ip6 saddr { %s } %s\n' "$ipv6_elements" "$action6"
|
||||
fi
|
||||
if [ -n "$mac_elements" ]; then
|
||||
bool_enabled "$(config_ipv4_proxy)" && printf 'meta nfproto ipv4 ether saddr { %s } %s\n' "$mac_elements" "$action4"
|
||||
bool_enabled "$(config_ipv6_proxy)" && printf 'meta nfproto ipv6 ether saddr { %s } %s\n' "$mac_elements" "$action6"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Split proxied clients to the core DNS while ACL-bypassed clients use dnsmasq.
|
||||
# Gated on UCI IPv4/IPv6 toggles.
|
||||
# return 0: no output when both off, avoid set -e false-positive.
|
||||
render_dns_hijack() {
|
||||
local access_control="$1" redirect_port=53
|
||||
if lan_dns_split_enabled; then
|
||||
redirect_port="$(config_dns_port)"
|
||||
fi
|
||||
if grouped_acl_enabled; then
|
||||
render_acl_dns_rules "$redirect_port"
|
||||
acl_has_catchall && return 0
|
||||
else
|
||||
[ "$access_control" = "1" ] && printf '%s\n' 'ip saddr != @clash_proxy_lan return'
|
||||
[ "$access_control" = "2" ] && printf '%s\n' 'ip saddr @clash_reject_lan return'
|
||||
fi
|
||||
bool_enabled "$(config_ipv4_dns_hijack)" && \
|
||||
printf '%s\n' "meta nfproto ipv4 meta l4proto { tcp, udp } th dport 53 counter redirect to :${redirect_port}"
|
||||
bool_enabled "$(config_ipv6_dns_hijack)" && \
|
||||
printf '%s\n' "meta nfproto ipv6 meta l4proto { tcp, udp } th dport 53 counter redirect to :${redirect_port}"
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_local_output_rule() {
|
||||
local redir_port fake_ip_range tcp_mode bypass_fwmark bypass_china
|
||||
local fwmark_elements fwmark_rule china_set china_rule dnsmasq_rule dnsmasq_uid
|
||||
redir_port="$(config_redir_port)"
|
||||
fake_ip_range="$(config_fake_ip_range)"
|
||||
tcp_mode="$(config_tcp_mode)"
|
||||
bypass_fwmark="$(config_bypass_fwmark)"
|
||||
bypass_china="$(config_bypass_china)"
|
||||
|
||||
# Keep local-output redirect usable when tun mode is selected but tun
|
||||
# device is unavailable on the system.
|
||||
if [ "$tcp_mode" = "tun" ] && ! tun_available; then
|
||||
tcp_mode="redirect"
|
||||
fi
|
||||
|
||||
nft delete table ip ${LOCAL_OUTPUT_TABLE} >/dev/null 2>&1 || true
|
||||
|
||||
# Only apply local output redirect when tcp_mode is redirect
|
||||
[ "$tcp_mode" != "redirect" ] && return 0
|
||||
|
||||
# bypass mihomo's own marks (prevent self-hijack -> dead loop)
|
||||
fwmark_rule=""
|
||||
fwmark_elements="$(merge_fwmark_tokens "$bypass_fwmark")"
|
||||
[ -n "$fwmark_elements" ] && fwmark_rule="meta mark { ${fwmark_elements} } return"
|
||||
|
||||
# CN IP bypass (uses clashoo_china set from geoip_cn.nft)
|
||||
china_set=""
|
||||
china_rule=""
|
||||
if bool_enabled "$bypass_china" && [ -s "$GEOIP_CN_NFT" ]; then
|
||||
china_set="$(cat "$GEOIP_CN_NFT")"
|
||||
china_rule="ip daddr @clashoo_china return"
|
||||
fi
|
||||
|
||||
dnsmasq_rule=""
|
||||
if lan_dns_split_enabled; then
|
||||
dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)"
|
||||
printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$' && dnsmasq_rule="meta skuid ${dnsmasq_uid} return"
|
||||
fi
|
||||
|
||||
nft -f - <<EOF
|
||||
table ip ${LOCAL_OUTPUT_TABLE} {
|
||||
set clashoo_localnetwork {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8,
|
||||
169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16,
|
||||
224.0.0.0/4, 240.0.0.0/4 }
|
||||
}
|
||||
${china_set}
|
||||
chain output {
|
||||
type nat hook output priority dstnat; policy accept;
|
||||
${dnsmasq_rule}
|
||||
${fwmark_rule}
|
||||
ip daddr @clashoo_localnetwork return
|
||||
${china_rule}
|
||||
ip daddr ${fake_ip_range} tcp dport != 53 redirect to :${redir_port}
|
||||
meta l4proto tcp redirect to :${redir_port}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
remove_local_output_rule() {
|
||||
nft delete table ip ${LOCAL_OUTPUT_TABLE} >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
remove_acl_bypass_rules() {
|
||||
local dnsmasq_uid
|
||||
ip rule del pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
|
||||
ip -6 rule del pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
|
||||
dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)"
|
||||
if printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$'; then
|
||||
ip rule del pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
|
||||
ip -6 rule del pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
apply_acl_bypass_rules() {
|
||||
local dnsmasq_uid
|
||||
remove_acl_bypass_rules
|
||||
tun_acl_enabled || return 0
|
||||
ip rule add pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
|
||||
ip -6 rule add pref "$ACL_BYPASS_PREF" fwmark "$ACL_BYPASS_FWMARK" lookup main >/dev/null 2>&1 || true
|
||||
dnsmasq_uid="$(id -u dnsmasq 2>/dev/null)"
|
||||
if printf '%s' "$dnsmasq_uid" | grep -Eq '^[0-9]+$'; then
|
||||
ip rule add pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
|
||||
ip -6 rule add pref "$DNSMASQ_BYPASS_PREF" uidrange "${dnsmasq_uid}-${dnsmasq_uid}" lookup main >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
apply_block_quic_rule() {
|
||||
local fake_ip_range china_set china_rule scope
|
||||
nft delete table inet ${QUIC_BLOCK_TABLE} >/dev/null 2>&1 || true
|
||||
bool_enabled "$(config_block_quic)" || return 0
|
||||
|
||||
fake_ip_range="$(config_fake_ip_range)"
|
||||
china_set=""
|
||||
scope="ip daddr ${fake_ip_range}"
|
||||
if [ "$(uci_get clashoo.config.enhanced_mode)" != "fake-ip" ]; then
|
||||
if bool_enabled "$(config_bypass_china)" && [ -s "$GEOIP_CN_NFT" ]; then
|
||||
china_set="$(cat "$GEOIP_CN_NFT")"
|
||||
china_rule="ip daddr @clashoo_china return"
|
||||
fi
|
||||
scope=""
|
||||
fi
|
||||
|
||||
nft -f - <<EOF
|
||||
table inet ${QUIC_BLOCK_TABLE} {
|
||||
${china_set}
|
||||
chain prerouting {
|
||||
type filter hook prerouting priority mangle - 1; policy accept;
|
||||
${china_rule:-}
|
||||
${scope} udp dport 443 counter reject with icmpx type port-unreachable
|
||||
}
|
||||
|
||||
chain forward {
|
||||
type filter hook forward priority -10; policy accept;
|
||||
${china_rule:-}
|
||||
${scope} udp dport 443 counter reject with icmpx type port-unreachable
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
remove_block_quic_rule() {
|
||||
nft delete table inet ${QUIC_BLOCK_TABLE} >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
write_empty_set() {
|
||||
local set_name="$1"
|
||||
local set_type="$2"
|
||||
|
||||
printf 'set %s {\n\ttype %s;\n\tflags interval;\n\tauto-merge;\n}\n\n' "$set_name" "$set_type"
|
||||
}
|
||||
|
||||
append_set_from_file_or_empty() {
|
||||
local file_path="$1"
|
||||
local set_name="$2"
|
||||
local set_type="$3"
|
||||
|
||||
if [ -s "$file_path" ]; then
|
||||
cat "$file_path"
|
||||
printf '\n'
|
||||
else
|
||||
write_empty_set "$set_name" "$set_type"
|
||||
fi
|
||||
}
|
||||
|
||||
generate_rules() {
|
||||
local redir_port tproxy_port tcp_mode udp_mode access_control fake_ip_range proxy_lan_ips reject_lan_ips
|
||||
local proxy_tcp_dport proxy_udp_dport bypass_dscp bypass_fwmark bypass_china bypass_china_ipv6
|
||||
local acl_catchall=0
|
||||
redir_port="$(config_redir_port)"
|
||||
tproxy_port="$(config_tproxy_port)"
|
||||
tcp_mode="$(config_tcp_mode)"
|
||||
udp_mode="$(config_udp_mode)"
|
||||
access_control="$(config_access_control)"
|
||||
grouped_acl_enabled && acl_has_catchall && acl_catchall=1
|
||||
bypass_china="$(config_bypass_china)"
|
||||
bypass_china_ipv6="$(config_bypass_china_ipv6)"
|
||||
proxy_tcp_dport="$(config_proxy_tcp_dport)"
|
||||
proxy_udp_dport="$(config_proxy_udp_dport)"
|
||||
bypass_dscp="$(config_bypass_dscp)"
|
||||
bypass_fwmark="$(config_bypass_fwmark)"
|
||||
fake_ip_range="$(config_fake_ip_range)"
|
||||
proxy_lan_ips="$(uci_list clashoo.config.proxy_lan_ips)"
|
||||
reject_lan_ips="$(uci_list clashoo.config.reject_lan_ips)"
|
||||
|
||||
# When tun device is unavailable, fall back to non-tun transparent modes
|
||||
# so routing rules still take effect for sing-box redirect/tproxy inbounds.
|
||||
if [ "$tcp_mode" = "tun" ] || [ "$udp_mode" = "tun" ]; then
|
||||
if ! tun_available; then
|
||||
[ "$tcp_mode" = "tun" ] && tcp_mode="redirect"
|
||||
[ "$udp_mode" = "tun" ] && udp_mode="tproxy"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$NFT_DIR"
|
||||
|
||||
# Build optional elements lines (nftables rejects empty elements = {})
|
||||
local proxy_elements reject_elements dscp_elements fwmark_elements
|
||||
proxy_elements="$(render_ip_elements "$proxy_lan_ips")"
|
||||
reject_elements="$(render_ip_elements "$reject_lan_ips")"
|
||||
dscp_elements="$(render_token_elements "$bypass_dscp")"
|
||||
fwmark_elements="$(merge_fwmark_tokens "$bypass_fwmark")"
|
||||
tcp_match="$(render_port_match tcp "$proxy_tcp_dport")"
|
||||
udp_match="$(render_port_match udp "$proxy_udp_dport")"
|
||||
|
||||
{
|
||||
printf 'set clashoo_localnetwork {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n'
|
||||
printf '\telements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 }\n}\n\n'
|
||||
|
||||
append_set_from_file_or_empty "$GEOIP_CN_NFT" clashoo_china ipv4_addr
|
||||
append_set_from_file_or_empty "$GEOIP6_CN_NFT" clashoo_china6 ipv6_addr
|
||||
|
||||
printf 'set clash_proxy_lan {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n'
|
||||
[ -n "$proxy_elements" ] && printf '\telements = { %s }\n' "$proxy_elements"
|
||||
printf '}\n\n'
|
||||
|
||||
printf 'set clash_reject_lan {\n\ttype ipv4_addr;\n\tflags interval;\n\tauto-merge;\n'
|
||||
[ -n "$reject_elements" ] && printf '\telements = { %s }\n' "$reject_elements"
|
||||
printf '}\n\n'
|
||||
|
||||
if grouped_acl_enabled && [ "$tcp_mode" = "tun" ]; then
|
||||
printf 'chain clashoo_acl_tun_tcp {\n'
|
||||
render_acl_proxy_rules tun tcp "$tcp_match" "$tproxy_port"
|
||||
printf '}\n\n'
|
||||
fi
|
||||
if grouped_acl_enabled && [ "$udp_mode" = "tun" ]; then
|
||||
printf 'chain clashoo_acl_tun_udp {\n'
|
||||
render_acl_proxy_rules tun udp "$udp_match" "$tproxy_port"
|
||||
printf '}\n'
|
||||
fi
|
||||
} > "$SETS_RULES"
|
||||
|
||||
: > "$OUTPUT_RULES"
|
||||
|
||||
|
||||
# DNS hijack rules go atop DSTNAT_RULES: must run for all TCP modes
|
||||
# (redirect/tproxy/tun) and BEFORE proxy redirect rules so port 53 is not stolen.
|
||||
render_dns_hijack "$access_control" > "$DSTNAT_RULES"
|
||||
|
||||
|
||||
# TCP rules: redirect mode appends proxy redirect; tproxy/tun skip this chain
|
||||
case "$tcp_mode" in
|
||||
redirect)
|
||||
tcp_match="$(render_port_match tcp "$proxy_tcp_dport")"
|
||||
cat >> "$DSTNAT_RULES" <<EOF
|
||||
$( render_common_returns )
|
||||
$( bool_enabled "$bypass_china_ipv6" && printf '%s\n' 'ip6 daddr @clashoo_china6 return' )
|
||||
$( bool_enabled "$bypass_china" && printf '%s\n' 'ip daddr @clashoo_china return' )
|
||||
$( [ -n "$dscp_elements" ] && printf '%s\n' "ip dscp { ${dscp_elements} } return" )
|
||||
$( [ -n "$dscp_elements" ] && printf '%s\n' "ip6 dscp { ${dscp_elements} } return" )
|
||||
$( [ -n "$fwmark_elements" ] && printf '%s\n' "meta mark { ${fwmark_elements} } return" )
|
||||
$( ! bool_enabled "$(config_ipv4_proxy)" && printf '%s\n' 'meta nfproto ipv4 return' )
|
||||
$( ! bool_enabled "$(config_ipv6_proxy)" && printf '%s\n' 'meta nfproto ipv6 return' )
|
||||
$( grouped_acl_enabled && render_acl_proxy_rules redirect tcp "$tcp_match" "$redir_port" )
|
||||
$( [ "$access_control" = "1" ] && printf '%s\n' 'ip saddr != @clash_proxy_lan return' )
|
||||
$( [ "$access_control" = "2" ] && printf '%s\n' 'ip saddr @clash_reject_lan return' )
|
||||
$( [ "$acl_catchall" -eq 0 ] && { singbox_tun_active && printf '%s ct mark set %s redirect to :%s\n' "$tcp_match" "$SINGBOX_BYPASS_FWMARK" "$redir_port" || printf '%s redirect to :%s\n' "$tcp_match" "$redir_port"; } )
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
|
||||
# UDP rules: tproxy via mangle. TUN uses mangle only for LAN ACL bypass.
|
||||
# Also handle TCP tproxy mode here (both TCP+UDP in mangle).
|
||||
local need_mangle=0 tun_acl=0
|
||||
[ "$tcp_mode" = "tproxy" ] && need_mangle=1
|
||||
[ "$udp_mode" = "tproxy" ] && need_mangle=1
|
||||
if tun_acl_enabled "$access_control"; then
|
||||
need_mangle=1
|
||||
tun_acl=1
|
||||
fi
|
||||
|
||||
if [ "$need_mangle" -eq 1 ]; then
|
||||
tcp_match="$(render_port_match tcp "$proxy_tcp_dport")"
|
||||
udp_match="$(render_port_match udp "$proxy_udp_dport")"
|
||||
{
|
||||
render_common_returns
|
||||
if bool_enabled "$bypass_china_ipv6"; then
|
||||
printf 'meta nfproto ipv6 ip6 daddr @clashoo_china6 return\n'
|
||||
fi
|
||||
if bool_enabled "$bypass_china"; then
|
||||
printf 'ip daddr @clashoo_china return\n'
|
||||
fi
|
||||
if [ -n "$dscp_elements" ]; then
|
||||
printf 'ip dscp { %s } return\n' "$dscp_elements"
|
||||
printf 'ip6 dscp { %s } return\n' "$dscp_elements"
|
||||
fi
|
||||
if [ -n "$fwmark_elements" ]; then
|
||||
printf 'meta mark { %s } return\n' "$fwmark_elements"
|
||||
fi
|
||||
bool_enabled "$(config_ipv4_proxy)" || printf 'meta nfproto ipv4 return\n'
|
||||
bool_enabled "$(config_ipv6_proxy)" || printf 'meta nfproto ipv6 return\n'
|
||||
if grouped_acl_enabled; then
|
||||
[ "$tcp_mode" = "tproxy" ] && render_acl_proxy_rules tproxy tcp "$tcp_match" "$tproxy_port"
|
||||
[ "$tcp_mode" = "tun" ] && printf 'meta l4proto tcp jump clashoo_acl_tun_tcp\n'
|
||||
[ "$udp_mode" = "tproxy" ] && render_acl_proxy_rules tproxy udp "$udp_match" "$tproxy_port"
|
||||
[ "$udp_mode" = "tun" ] && printf 'meta l4proto udp jump clashoo_acl_tun_udp\n'
|
||||
elif [ "$tun_acl" -eq 1 ]; then
|
||||
[ "$access_control" = "1" ] && printf 'ip saddr != @clash_proxy_lan meta mark set %s return\n' "$ACL_BYPASS_FWMARK"
|
||||
[ "$access_control" = "2" ] && printf 'ip saddr @clash_reject_lan meta mark set %s return\n' "$ACL_BYPASS_FWMARK"
|
||||
else
|
||||
[ "$access_control" = "1" ] && printf 'ip saddr != @clash_proxy_lan return\n'
|
||||
[ "$access_control" = "2" ] && printf 'ip saddr @clash_reject_lan return\n'
|
||||
fi
|
||||
if bool_enabled "$(config_block_quic)"; then
|
||||
printf 'meta l4proto udp udp dport 443 return\n'
|
||||
fi
|
||||
if [ "$tcp_mode" = "tproxy" ] && [ "$acl_catchall" -eq 0 ]; then
|
||||
if singbox_tun_active; then
|
||||
printf '%s ct mark set %s tproxy to :%s meta mark set %s accept\n' "$tcp_match" "$SINGBOX_BYPASS_FWMARK" "$tproxy_port" "$PROXY_FWMARK"
|
||||
else
|
||||
printf '%s tproxy to :%s meta mark set %s accept\n' "$tcp_match" "$tproxy_port" "$PROXY_FWMARK"
|
||||
fi
|
||||
fi
|
||||
if [ "$udp_mode" = "tproxy" ] && [ "$acl_catchall" -eq 0 ]; then
|
||||
if singbox_tun_active; then
|
||||
printf '%s ct mark set %s tproxy to :%s meta mark set %s accept\n' "$udp_match" "$SINGBOX_BYPASS_FWMARK" "$tproxy_port" "$PROXY_FWMARK"
|
||||
else
|
||||
printf '%s tproxy to :%s meta mark set %s accept\n' "$udp_match" "$tproxy_port" "$PROXY_FWMARK"
|
||||
fi
|
||||
fi
|
||||
} > "$MANGLE_RULES"
|
||||
else
|
||||
: > "$MANGLE_RULES"
|
||||
fi
|
||||
}
|
||||
|
||||
apply_rules() {
|
||||
generate_rules
|
||||
ensure_firewall_include clash_fw4_sets "$SETS_RULES" '' table-pre
|
||||
ensure_firewall_include clash_fw4_dstnat "$DSTNAT_RULES" dstnat
|
||||
remove_firewall_include clash_fw4_output
|
||||
if [ -s "$MANGLE_RULES" ]; then
|
||||
_route_table_dec="$((PROXY_ROUTE_TABLE))"
|
||||
ensure_firewall_include clash_fw4_mangle "$MANGLE_RULES" mangle_prerouting
|
||||
ip rule show 2>/dev/null | grep -q "fwmark ${PROXY_FWMARK}.*lookup ${_route_table_dec}" ||
|
||||
ip rule add fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip route show table "$PROXY_ROUTE_TABLE" 2>/dev/null | grep -q 'local 0.0.0.0/0 dev lo' ||
|
||||
ip route add local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
if bool_enabled "$(config_ipv6_proxy)"; then
|
||||
ip -6 rule show 2>/dev/null | grep -q "fwmark ${PROXY_FWMARK}.*lookup ${_route_table_dec}" ||
|
||||
ip -6 rule add fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 route show table "$PROXY_ROUTE_TABLE" 2>/dev/null | grep -q 'local default dev lo' ||
|
||||
ip -6 route add local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
else
|
||||
ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
remove_firewall_include clash_fw4_mangle
|
||||
ip rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip route del local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
fi
|
||||
/etc/init.d/firewall restart >/dev/null 2>&1 || /etc/init.d/firewall reload >/dev/null 2>&1 || true
|
||||
apply_acl_bypass_rules
|
||||
apply_local_output_rule
|
||||
apply_block_quic_rule
|
||||
}
|
||||
|
||||
remove_rules() {
|
||||
remove_acl_bypass_rules
|
||||
remove_local_output_rule
|
||||
remove_block_quic_rule
|
||||
remove_firewall_include clash_fw4_sets
|
||||
remove_firewall_include clash_fw4_dstnat
|
||||
remove_firewall_include clash_fw4_output
|
||||
remove_firewall_include clash_fw4_mangle
|
||||
remove_firewall_include clash_fw4_forward
|
||||
uci commit firewall >/dev/null 2>&1 || true
|
||||
rm -f "$SETS_RULES" "$DSTNAT_RULES" "$OUTPUT_RULES" "$MANGLE_RULES"
|
||||
ip rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip route del local 0.0.0.0/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 rule del fwmark "$PROXY_FWMARK" table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
ip -6 route del local ::/0 dev lo table "$PROXY_ROUTE_TABLE" >/dev/null 2>&1 || true
|
||||
/etc/init.d/firewall restart >/dev/null 2>&1 || /etc/init.d/firewall reload >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
apply)
|
||||
apply_rules
|
||||
;;
|
||||
remove)
|
||||
remove_rules
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {apply|remove}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,43 @@
|
||||
# clashoo 运行日志格式化
|
||||
# 输出: MM-DD HH:MM:SS msg
|
||||
|
||||
# mihomo 原生行: time="YYYY-MM-DDTHH:MM:SS..." level=... msg="..."
|
||||
/^time="[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/ {
|
||||
# extract YYYY-MM-DD and HH:MM:SS
|
||||
ts_date = substr($0, 12, 5) # MM-DD
|
||||
ts_time = substr($0, 18, 8) # HH:MM:SS
|
||||
utc_h = substr(ts_time, 1, 2) + 0
|
||||
cst_h = (utc_h + 8) % 24
|
||||
ts = sprintf("%s %02d:%s", ts_date, cst_h, substr(ts_time, 4))
|
||||
|
||||
prefix = ""
|
||||
if (match($0, /level=warning /)) prefix = " [warn]"
|
||||
else if (match($0, /level=error /)) prefix = " [err]"
|
||||
else if (match($0, /level=fatal /)) prefix = " [fatal]"
|
||||
|
||||
i = index($0, "msg=\"")
|
||||
if (i > 0) {
|
||||
rest = substr($0, i + 5)
|
||||
sub(/"[[:space:]]*$/, "", rest)
|
||||
print ts prefix " " rest
|
||||
next
|
||||
}
|
||||
print ts " " $0
|
||||
next
|
||||
}
|
||||
|
||||
# log_msg 行: " YYYY-MM-DD HH:MM:SS - msg" → "MM-DD HH:MM:SS msg"
|
||||
/^[[:space:]]+[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][[:space:]]+[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/ {
|
||||
gsub(/^[[:space:]]+/, "") # trim leading space
|
||||
ts_date = substr($0, 6, 5) # MM-DD
|
||||
ts_time = substr($0, 12, 8) # HH:MM:SS
|
||||
# remove "YYYY-MM-DD HH:MM:SS - " prefix
|
||||
sub(/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][[:space:]]+[0-9][0-9]:[0-9][0-9]:[0-9][0-9][[:space:]]*-[[:space:]]*/, "")
|
||||
print ts_date " " ts_time " " $0
|
||||
next
|
||||
}
|
||||
|
||||
# 空行丢弃
|
||||
NF == 0 { next }
|
||||
|
||||
{ print }
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
|
||||
ACTION="$1"
|
||||
LOG_FILE="/tmp/clash_update.txt"
|
||||
|
||||
log_line() {
|
||||
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
run_init_async() {
|
||||
local action="$1"
|
||||
mkdir -p /tmp/lock >/dev/null 2>&1
|
||||
(flock /tmp/lock/clashoo_rpc_async.lock /etc/init.d/clashoo "$action" >/dev/null 2>&1 </dev/null &)
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
start)
|
||||
run_init_async start
|
||||
exit 0
|
||||
;;
|
||||
stop)
|
||||
run_init_async stop
|
||||
exit 0
|
||||
;;
|
||||
restart)
|
||||
# Fire-and-forget restart used by LuCI RPC to avoid blocking UI apply flow.
|
||||
run_init_async restart
|
||||
exit 0
|
||||
;;
|
||||
update_china_ip)
|
||||
log_line "[china-ip] task started"
|
||||
nohup /usr/share/clashoo/update/update_china_ip.sh >>"$LOG_FILE" 2>&1 </dev/null &
|
||||
exit 0
|
||||
;;
|
||||
update_geoip)
|
||||
log_line "GeoIP 更新任务已触发"
|
||||
(exec 1000>&-; nohup /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1 </dev/null &)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 {start|stop|restart|update_china_ip|update_geoip}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
. /lib/functions.sh
|
||||
|
||||
CURL_GROUP_CACHE="/usr/share/clashbackup/clash_gorup.json"
|
||||
CURL_NOW_CACHE="/usr/share/clashbackup/clash_now.json"
|
||||
CURL_CACHE="/usr/share/clashbackup/clash_curl.json"
|
||||
HISTORY_PATH="/usr/share/clashbackup/history"
|
||||
SECRET=$(uci get clashoo.config.dash_pass 2>/dev/null)
|
||||
LAN_IP=$(uci get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null)
|
||||
PORT=$(uci get clashoo.config.dash_port 2>/dev/null)
|
||||
|
||||
curl -m 5 --retry 2 -w %{http_code}"\n" -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X GET http://"$LAN_IP":"$PORT"/proxies > "$CURL_CACHE" 2>/dev/null
|
||||
if [ "$(sed -n '$p' "$CURL_CACHE" 2>/dev/null)" = "200" ]; then
|
||||
if [ ! -d /usr/share/clashbackup ];then
|
||||
mkdir -p /usr/share/clashbackup 2>/dev/null
|
||||
fi
|
||||
cat "$CURL_CACHE" |jsonfilter -e '@["proxies"][@.type="Selector"]["name"]' > "$CURL_GROUP_CACHE" 2>/dev/null
|
||||
cat "$CURL_CACHE" |jsonfilter -e '@["proxies"][@.type="Selector"]["now"]' > "$CURL_NOW_CACHE" 2>/dev/null
|
||||
awk 'NR==FNR{a[i]=$0;i++}NR>FNR{print a[j]"#*#"$0;j++}' "$CURL_GROUP_CACHE" "$CURL_NOW_CACHE" > "$HISTORY_PATH" 2>/dev/null
|
||||
fi
|
||||
rm -rf /usr/share/clashbackup/clash_*.json 2>/dev/null
|
||||
+347
@@ -0,0 +1,347 @@
|
||||
#!/bin/sh
|
||||
|
||||
REAL_LOG="/usr/share/clashoo/clashoo_real.txt"
|
||||
UPDATE_LOG="/tmp/clash_update.txt"
|
||||
LIST_FILE="/usr/share/clashbackup/confit_list.conf"
|
||||
SUB_DIR="/usr/share/clashoo/config/sub"
|
||||
TMP_PREFIX="/tmp/clash_sub_$$"
|
||||
DEFAULT_SUB_UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
|
||||
|
||||
subtype="$(uci -q get clashoo.config.subcri 2>/dev/null)"
|
||||
config_name_raw="$(uci -q get clashoo.config.config_name 2>/dev/null)"
|
||||
lang="$(uci -q get luci.main.lang 2>/dev/null)"
|
||||
|
||||
log_text() {
|
||||
if [ "$lang" = "en" ]; then
|
||||
echo "$1" >"$REAL_LOG"
|
||||
else
|
||||
echo "$2" >"$REAL_LOG"
|
||||
fi
|
||||
}
|
||||
|
||||
log_update() {
|
||||
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
|
||||
}
|
||||
|
||||
sanitize_name() {
|
||||
local name
|
||||
name="$1"
|
||||
name="$(printf '%s' "$name" | tr 'A-Z' 'a-z')"
|
||||
name="$(printf '%s' "$name" | sed -e 's/\.yaml$//' -e 's/\.yml$//')"
|
||||
name="$(printf '%s' "$name" | tr ' /' '--')"
|
||||
name="$(printf '%s' "$name" | sed -e 's/[^a-z0-9._-]/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')"
|
||||
printf '%s' "$name"
|
||||
}
|
||||
|
||||
sanitize_custom_name() {
|
||||
local name
|
||||
name="$1"
|
||||
name="$(printf '%s' "$name" | sed -e 's/\.yaml$//' -e 's/\.yml$//')"
|
||||
name="$(printf '%s' "$name" | tr ' /' '--')"
|
||||
name="$(printf '%s' "$name" | sed -e 's/[\\]//g' -e 's/\.\.+/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')"
|
||||
printf '%s' "$name"
|
||||
}
|
||||
|
||||
url_decode() {
|
||||
printf '%s' "$1" | sed 's/%/\\x/g' | xargs -0 printf '%b' 2>/dev/null || printf '%s' "$1"
|
||||
}
|
||||
|
||||
url_to_name() {
|
||||
local url host qname decoded
|
||||
url="$1"
|
||||
|
||||
qname="$(printf '%s' "$url" | sed -n 's/.*[?&]filename=\([^&#]*\).*/\1/p')"
|
||||
[ -n "$qname" ] || qname="$(printf '%s' "$url" | sed -n 's/.*[?&]name=\([^&#]*\).*/\1/p')"
|
||||
if [ -n "$qname" ]; then
|
||||
# URL decode then strip non-filename chars, keep CJK and alphanumeric
|
||||
decoded="$(url_decode "$qname")"
|
||||
decoded="$(printf '%s' "$decoded" | tr -d '\r\n' | sed -e 's/[[:space:]]/-/g' -e 's/[/\\:*?"<>|]//g' -e 's/\.yaml$//' -e 's/\.yml$//')"
|
||||
[ -n "$decoded" ] && printf '%s' "$decoded" && return
|
||||
fi
|
||||
qname="$(sanitize_name "$qname")"
|
||||
if [ -n "$qname" ]; then
|
||||
printf '%s' "$qname"
|
||||
return
|
||||
fi
|
||||
|
||||
host="$(printf '%s' "$url" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's/:.*$//' -e 's#\..*$##')"
|
||||
host="$(sanitize_name "$host")"
|
||||
[ -n "$host" ] || host="sub"
|
||||
printf '%s' "$host"
|
||||
}
|
||||
|
||||
next_available_name() {
|
||||
local base try idx
|
||||
base="$1"
|
||||
base="$(printf '%s' "$base" | sed -e 's/\.yaml$//' -e 's/\.yml$//')"
|
||||
base="$(printf '%s' "$base" | tr ' /' '--')"
|
||||
base="$(printf '%s' "$base" | sed -e 's/[\\]//g' -e 's/\.\.+/-/g' -e 's/--\+/-/g' -e 's/^[._-]*//' -e 's/[._-]*$//')"
|
||||
[ -n "$base" ] || base="sub"
|
||||
|
||||
if [ ! -f "$SUB_DIR/${base}.yaml" ]; then
|
||||
printf '%s' "$base"
|
||||
return
|
||||
fi
|
||||
|
||||
idx=2
|
||||
while :; do
|
||||
try="${base}-${idx}"
|
||||
if [ ! -f "$SUB_DIR/${try}.yaml" ]; then
|
||||
printf '%s' "$try"
|
||||
return
|
||||
fi
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
}
|
||||
|
||||
get_subscription_urls() {
|
||||
uci -q show clashoo.config 2>/dev/null | awk -F"'" '
|
||||
/^clashoo.config.subscribe_url=/ {
|
||||
if (NF >= 3) {
|
||||
for (i = 2; i <= NF; i += 2) {
|
||||
if (length($i) > 0) print $i
|
||||
}
|
||||
} else {
|
||||
sub(/^clashoo.config.subscribe_url=/, "", $0)
|
||||
if (length($0) > 0) print $0
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
ensure_system_dns() {
|
||||
local test_host
|
||||
test_host="github.com"
|
||||
if nslookup "$test_host" 127.0.0.1 >/dev/null 2>&1 || nslookup "$test_host" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
uci delete dhcp.@dnsmasq[0].server >/dev/null 2>&1
|
||||
uci set dhcp.@dnsmasq[0].noresolv='0' >/dev/null 2>&1
|
||||
uci del_list dhcp.@dnsmasq[0].server='127.0.0.1#' >/dev/null 2>&1
|
||||
uci del_list dhcp.@dnsmasq[0].server='127.0.0.1#5300' >/dev/null 2>&1
|
||||
uci add_list dhcp.@dnsmasq[0].server='119.29.29.29' >/dev/null 2>&1
|
||||
uci add_list dhcp.@dnsmasq[0].server='223.5.5.5' >/dev/null 2>&1
|
||||
uci commit dhcp >/dev/null 2>&1
|
||||
/etc/init.d/dnsmasq restart >/dev/null 2>&1
|
||||
sleep 2
|
||||
}
|
||||
|
||||
extract_host() {
|
||||
printf '%s' "$1" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's#:.*$##' -e 's#.*@##'
|
||||
}
|
||||
|
||||
resolve_via() {
|
||||
local host dns
|
||||
host="$1"
|
||||
dns="$2"
|
||||
nslookup "$host" "$dns" 2>/dev/null | awk '
|
||||
/^Address/ {
|
||||
ip = $NF
|
||||
if (ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ &&
|
||||
ip !~ /^127\./ &&
|
||||
ip !~ /^0\./ &&
|
||||
ip !~ /^198\.18\./ &&
|
||||
ip != "8.8.8.8" && ip != "8.8.4.4" &&
|
||||
ip != "1.1.1.1" && ip != "1.0.0.1" &&
|
||||
ip != "223.5.5.5" && ip != "223.6.6.6" &&
|
||||
ip != "119.29.29.29" && ip != "114.114.114.114") {
|
||||
print ip
|
||||
exit
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
curl_subscription() {
|
||||
local url tmp hdr err ua extra_resolve http_code rc
|
||||
url="$1"
|
||||
tmp="$2"
|
||||
hdr="$3"
|
||||
err="$4"
|
||||
ua="$5"
|
||||
extra_resolve="$6"
|
||||
|
||||
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
http_code="$(curl -sSL --connect-timeout 15 --max-time 60 \
|
||||
--speed-time 30 --speed-limit 1 --retry 2 \
|
||||
-H "User-Agent: ${ua}" -D "$hdr" -o "$tmp" \
|
||||
$extra_resolve \
|
||||
-w '%{http_code}' "$url" 2>"$err")"
|
||||
rc=$?
|
||||
printf '%s\n' "$http_code"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
download_subscription() {
|
||||
local url target tmp hdr err rc http_code info_line ua err_msg
|
||||
local host ip dns extra
|
||||
url="$1"
|
||||
target="$2"
|
||||
tmp="${TMP_PREFIX}.yaml"
|
||||
hdr="${TMP_PREFIX}.hdr"
|
||||
err="${TMP_PREFIX}.err"
|
||||
|
||||
ua="$(uci -q get clashoo.config.sub_ua 2>/dev/null)"
|
||||
[ -n "$ua" ] || ua="$DEFAULT_SUB_UA"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "")"
|
||||
rc=$?
|
||||
|
||||
if [ "$rc" -ne 0 ] || [ "$http_code" = "000" ]; then
|
||||
host="$(extract_host "$url")"
|
||||
if [ -n "$host" ]; then
|
||||
for dns in 223.5.5.5 119.29.29.29 1.1.1.1 8.8.8.8; do
|
||||
ip="$(resolve_via "$host" "$dns")"
|
||||
[ -n "$ip" ] || continue
|
||||
log_update "DNS 回退:${host} -> ${ip} (@${dns})"
|
||||
extra="--resolve ${host}:443:${ip} --resolve ${host}:80:${ip}"
|
||||
http_code="$(curl_subscription "$url" "$tmp" "$hdr" "$err" "$ua" "$extra")"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] && [ "$http_code" = "200" ] && break
|
||||
done
|
||||
fi
|
||||
fi
|
||||
else
|
||||
wget -q --tries=4 --timeout=20 \
|
||||
--user-agent="$ua" "$url" -O "$tmp" 2>"$err"
|
||||
rc=$?
|
||||
http_code=""
|
||||
fi
|
||||
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
err_msg="$(grep -a 'curl:' "$err" 2>/dev/null | tail -1)"
|
||||
[ -z "$err_msg" ] && err_msg="$(tail -1 "$err" 2>/dev/null)"
|
||||
log_update "下载失败:$(basename "$target") rc=${rc} ${err_msg}"
|
||||
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$http_code" ] && [ "$http_code" != "200" ]; then
|
||||
log_update "下载失败:$(basename "$target") HTTP ${http_code}"
|
||||
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! grep -Eq '^(proxies|proxy-providers):' "$tmp" 2>/dev/null; then
|
||||
log_update "校验失败:$(basename "$target") 内容不含 proxies/proxy-providers"
|
||||
rm -f "$tmp" "$hdr" "$err" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
|
||||
info_line="$(grep -i 'subscription-userinfo:' "$hdr" 2>/dev/null | head -1 | \
|
||||
sed 's/^[Ss]ubscription-[Uu]serinfo:[[:space:]]*//' | tr -d '\r')"
|
||||
[ -n "$info_line" ] && printf '%s\n' "$info_line" > "${target}.info" || \
|
||||
rm -f "${target}.info" >/dev/null 2>&1
|
||||
|
||||
rm -f "$hdr" "$err" >/dev/null 2>&1
|
||||
mv "$tmp" "$target" >/dev/null 2>&1 || {
|
||||
rm -f "$tmp" >/dev/null 2>&1
|
||||
return 1
|
||||
}
|
||||
printf '%s\n' "$ua" >"${target}.ua"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
upsert_meta() {
|
||||
local filename url typ tmpf
|
||||
filename="$1"
|
||||
url="$2"
|
||||
typ="$3"
|
||||
tmpf="${TMP_PREFIX}.list"
|
||||
|
||||
[ -f "$LIST_FILE" ] || touch "$LIST_FILE"
|
||||
awk -F '#' -v n="$filename" '$1 != n { print $0 }' "$LIST_FILE" >"$tmpf"
|
||||
printf '%s#%s#%s\n' "$filename" "$url" "$typ" >>"$tmpf"
|
||||
mv "$tmpf" "$LIST_FILE"
|
||||
}
|
||||
|
||||
cleanup_tmp() {
|
||||
rm -f "${TMP_PREFIX}.yaml" "${TMP_PREFIX}.urls" "${TMP_PREFIX}.list" \
|
||||
"${TMP_PREFIX}.hdr" "${TMP_PREFIX}.err" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
trap cleanup_tmp EXIT INT TERM
|
||||
|
||||
[ "$subtype" = "clash" ] || [ "$subtype" = "meta" ] || subtype="clash"
|
||||
|
||||
mkdir -p "$SUB_DIR" /usr/share/clashbackup >/dev/null 2>&1
|
||||
[ -f "$LIST_FILE" ] || touch "$LIST_FILE"
|
||||
|
||||
URLS_FILE="${TMP_PREFIX}.urls"
|
||||
get_subscription_urls | sed '/^[[:space:]]*$/d' | awk '!seen[$0]++' >"$URLS_FILE"
|
||||
|
||||
url_count="$(wc -l <"$URLS_FILE" 2>/dev/null | tr -d ' ')"
|
||||
if [ -z "$url_count" ] || [ "$url_count" -eq 0 ]; then
|
||||
log_update "未找到订阅链接"
|
||||
log_text "No subscription URL found" "未找到订阅链接"
|
||||
sleep 2
|
||||
log_text "Clash for OpenWRT" "Clash for OpenWRT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_system_dns
|
||||
log_update "开始下载订阅(共 ${url_count} 条)"
|
||||
log_text "Downloading subscription..." "开始下载订阅..."
|
||||
|
||||
base_name="$(sanitize_custom_name "$config_name_raw")"
|
||||
timestamp="$(date +%Y%m%d)"
|
||||
|
||||
success=0
|
||||
failed=0
|
||||
idx=0
|
||||
first_file=""
|
||||
|
||||
while IFS= read -r url; do
|
||||
[ -n "$url" ] || continue
|
||||
idx=$((idx + 1))
|
||||
|
||||
if [ -n "$base_name" ]; then
|
||||
if [ "$url_count" -gt 1 ]; then
|
||||
name_candidate="$(sanitize_name "${base_name}-${idx}")"
|
||||
file_base="$(next_available_name "$name_candidate")"
|
||||
else
|
||||
file_base="$base_name"
|
||||
fi
|
||||
else
|
||||
name_candidate="$(url_to_name "$url")-${timestamp}"
|
||||
if [ "$url_count" -gt 1 ]; then
|
||||
name_candidate="${name_candidate}-${idx}"
|
||||
fi
|
||||
file_base="$(next_available_name "$name_candidate")"
|
||||
fi
|
||||
|
||||
target_file="$SUB_DIR/${file_base}.yaml"
|
||||
if download_subscription "$url" "$target_file"; then
|
||||
upsert_meta "${file_base}.yaml" "$url" "$subtype"
|
||||
log_update "订阅下载成功:${file_base}.yaml"
|
||||
[ -n "$first_file" ] || first_file="$target_file"
|
||||
success=$((success + 1))
|
||||
else
|
||||
log_update "订阅下载失败:${file_base}.yaml"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
done <"$URLS_FILE"
|
||||
|
||||
if [ "$success" -gt 0 ]; then
|
||||
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
|
||||
if [ -z "$use_config" ] || [ ! -f "$use_config" ]; then
|
||||
uci set clashoo.config.use_config="$first_file"
|
||||
uci set clashoo.config.config_type='1'
|
||||
uci commit clashoo
|
||||
fi
|
||||
log_text "Subscription download completed: ${success} success, ${failed} failed" "订阅下载完成:成功 ${success} 个,失败 ${failed} 个"
|
||||
log_update "订阅下载完成:成功 ${success} 个,失败 ${failed} 个"
|
||||
ret=0
|
||||
else
|
||||
log_text "All subscription downloads failed" "订阅下载失败"
|
||||
log_update "订阅下载失败:全部链接失败"
|
||||
ret=1
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
log_text "Clash for OpenWRT" "Clash for OpenWRT"
|
||||
exit "$ret"
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
#!/bin/sh
|
||||
# Shared DNS helpers for ClashOO runtime generators. Keep POSIX sh compatible.
|
||||
|
||||
dns_trim() {
|
||||
printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
||||
}
|
||||
|
||||
dns_has_scheme() {
|
||||
printf '%s' "$1" | grep -Eq '^[A-Za-z][A-Za-z0-9+.-]*://'
|
||||
}
|
||||
|
||||
dns_norm_protocol() {
|
||||
case "$(dns_trim "${1:-}")" in
|
||||
''|'none') printf '' ;;
|
||||
udp|'udp://') printf 'udp://' ;;
|
||||
tcp|'tcp://') printf 'tcp://' ;;
|
||||
dot|tls|'tls://') printf 'tls://' ;;
|
||||
doh|https|'https://') printf 'https://' ;;
|
||||
doq|quic|'quic://') printf 'quic://' ;;
|
||||
*) printf '%s' "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
dns_normalize_server() {
|
||||
local address protocol port prefix
|
||||
address=$(dns_trim "${1:-}")
|
||||
protocol=$(dns_trim "${2:-}")
|
||||
port=$(dns_trim "${3:-}")
|
||||
|
||||
[ -n "$address" ] || return 0
|
||||
|
||||
if dns_has_scheme "$address"; then
|
||||
printf '%s' "$address"
|
||||
return 0
|
||||
fi
|
||||
|
||||
prefix=$(dns_norm_protocol "$protocol")
|
||||
if [ -n "$port" ]; then
|
||||
printf '%s%s:%s' "$prefix" "$address" "$port"
|
||||
else
|
||||
printf '%s%s' "$prefix" "$address"
|
||||
fi
|
||||
}
|
||||
|
||||
dns_yaml_sq() {
|
||||
printf "%s" "$1" | sed "s/'/''/g"
|
||||
}
|
||||
|
||||
dns_yaml_list_item() {
|
||||
local value
|
||||
value=$(dns_yaml_sq "$1")
|
||||
printf " - '%s'\n" "$value"
|
||||
}
|
||||
|
||||
# Route DNS upstreams by rules and preserve the original setting.
|
||||
dns_mihomo_apply_respect_rules() {
|
||||
local cfg="$1" enabled="${2:-0}" tmp="${1}.$$"
|
||||
[ -f "$cfg" ] || return 0
|
||||
|
||||
awk -v enabled="$enabled" '
|
||||
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
|
||||
function spaces(n, out,i) {
|
||||
out = ""
|
||||
for (i = 0; i < n; i++)
|
||||
out = out " "
|
||||
return out
|
||||
}
|
||||
function trim_value(s) {
|
||||
sub(/^[[:space:]]*/, "", s)
|
||||
sub(/[[:space:]]*$/, "", s)
|
||||
return s
|
||||
}
|
||||
function flush_dns( i, sp) {
|
||||
if (!in_dns)
|
||||
return
|
||||
sp = spaces(child_indent > 0 ? child_indent : dns_indent + 2)
|
||||
print dns_lines[1]
|
||||
if (enabled == "1") {
|
||||
print sp "# >>> clashoo:dns_leak_protect_respect_rules"
|
||||
print sp "# original-respect-rules: " (original_value != "" ? original_value : "absent")
|
||||
print sp "respect-rules: true"
|
||||
print sp "# <<< clashoo:dns_leak_protect_respect_rules"
|
||||
} else if (original_value != "" && original_value != "absent") {
|
||||
print sp "respect-rules: " original_value
|
||||
}
|
||||
for (i = 2; i <= dns_n; i++)
|
||||
print dns_lines[i]
|
||||
in_dns = 0
|
||||
dns_n = 0
|
||||
}
|
||||
|
||||
/^dns:[[:space:]]*$/ {
|
||||
flush_dns()
|
||||
in_dns = 1
|
||||
dns_indent = indent_len($0)
|
||||
child_indent = -1
|
||||
dns_n = 1
|
||||
dns_lines[dns_n] = $0
|
||||
original_value = ""
|
||||
in_marker = 0
|
||||
next
|
||||
}
|
||||
in_dns {
|
||||
cur_indent = indent_len($0)
|
||||
if ($0 ~ /^[^[:space:]#][^:]*:/ && cur_indent <= dns_indent) {
|
||||
flush_dns()
|
||||
print
|
||||
next
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_respect_rules[[:space:]]*$/) {
|
||||
in_marker = 1
|
||||
if (child_indent < 0) child_indent = cur_indent
|
||||
next
|
||||
}
|
||||
if (in_marker) {
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*original-respect-rules:[[:space:]]*/) {
|
||||
value = $0
|
||||
sub(/^[[:space:]]*#[[:space:]]*original-respect-rules:[[:space:]]*/, "", value)
|
||||
original_value = trim_value(value)
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_respect_rules[[:space:]]*$/)
|
||||
in_marker = 0
|
||||
next
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*respect-rules:[[:space:]]*/) {
|
||||
if (original_value == "") {
|
||||
value = $0
|
||||
sub(/^[[:space:]]*respect-rules:[[:space:]]*/, "", value)
|
||||
original_value = trim_value(value)
|
||||
}
|
||||
if (child_indent < 0) child_indent = cur_indent
|
||||
next
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && cur_indent > dns_indent && child_indent < 0)
|
||||
child_indent = cur_indent
|
||||
dns_lines[++dns_n] = $0
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END { flush_dns() }
|
||||
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
}
|
||||
|
||||
# Use fallback DNS for queries; keep bootstrap DNS unchanged.
|
||||
dns_mihomo_apply_leak_nameservers() {
|
||||
local cfg="$1" enabled="${2:-0}" state="${1}.dns-leak-nameserver"
|
||||
local selected="${1}.dns-leak-selected" current="${1}.dns-leak-current" tmp="${1}.$$"
|
||||
[ -f "$cfg" ] || return 0
|
||||
command -v yq >/dev/null 2>&1 || return 0
|
||||
|
||||
if [ "$enabled" = "1" ]; then
|
||||
yq e -r '.dns.fallback[]' "$cfg" > "$selected" 2>/dev/null || true
|
||||
if [ ! -s "$selected" ]; then
|
||||
printf '%s\n' 'https://1.1.1.1/dns-query' 'https://8.8.8.8/dns-query' > "$selected"
|
||||
fi
|
||||
yq e -r '.dns.nameserver[]' "$cfg" > "$current" 2>/dev/null || true
|
||||
|
||||
if [ ! -f "$state" ] ||
|
||||
! grep -q 'clashoo:dns_leak_protect_respect_rules' "$cfg" ||
|
||||
! cmp -s "$current" "$selected"; then
|
||||
awk '
|
||||
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
|
||||
/^dns:[[:space:]]*$/ { in_dns = 1; dns_indent = indent_len($0); next }
|
||||
in_dns && /^[^[:space:]#][^:]*:/ { exit }
|
||||
in_dns && /^[[:space:]]*nameserver:/ {
|
||||
print
|
||||
rest = $0
|
||||
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
|
||||
sub(/[[:space:]]*(#.*)?$/, "", rest)
|
||||
if (rest == "") { in_ns = 1; ns_indent = indent_len($0) }
|
||||
next
|
||||
}
|
||||
in_ns {
|
||||
cur_indent = indent_len($0)
|
||||
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) {
|
||||
print
|
||||
next
|
||||
}
|
||||
exit
|
||||
}
|
||||
' "$cfg" > "$state"
|
||||
[ -s "$state" ] || printf '__ABSENT__\n' > "$state"
|
||||
fi
|
||||
|
||||
awk -v selected="$selected" '
|
||||
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
|
||||
function spaces(n, out,i) {
|
||||
out = ""
|
||||
for (i = 0; i < n; i++) out = out " "
|
||||
return out
|
||||
}
|
||||
function print_selected(sp, line) {
|
||||
print sp "nameserver:"
|
||||
while ((getline line < selected) > 0)
|
||||
if (line != "") print sp " - " line
|
||||
close(selected)
|
||||
inserted = 1
|
||||
}
|
||||
/^dns:[[:space:]]*$/ {
|
||||
in_dns = 1
|
||||
dns_indent = indent_len($0)
|
||||
child_indent = -1
|
||||
print
|
||||
next
|
||||
}
|
||||
in_dns && /^[^[:space:]#][^:]*:/ {
|
||||
if (!inserted) print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
|
||||
in_dns = 0
|
||||
print
|
||||
next
|
||||
}
|
||||
in_dns && /^[[:space:]]*[^#[:space:]][^:]*:/ && child_indent < 0 {
|
||||
child_indent = indent_len($0)
|
||||
}
|
||||
in_dns && /^[[:space:]]*nameserver:/ {
|
||||
ns_indent = indent_len($0)
|
||||
rest = $0
|
||||
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
|
||||
sub(/[[:space:]]*(#.*)?$/, "", rest)
|
||||
print_selected(spaces(ns_indent))
|
||||
if (rest == "") skip_ns = 1
|
||||
next
|
||||
}
|
||||
skip_ns {
|
||||
cur_indent = indent_len($0)
|
||||
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next
|
||||
skip_ns = 0
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (in_dns && !inserted)
|
||||
print_selected(spaces(child_indent > 0 ? child_indent : dns_indent + 2))
|
||||
}
|
||||
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
||||
elif [ -f "$state" ]; then
|
||||
awk -v state="$state" '
|
||||
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
|
||||
function print_original( line) {
|
||||
if ((getline line < state) > 0 && line != "__ABSENT__") {
|
||||
print line
|
||||
while ((getline line < state) > 0) print line
|
||||
}
|
||||
close(state)
|
||||
restored = 1
|
||||
}
|
||||
/^dns:[[:space:]]*$/ { in_dns = 1; print; next }
|
||||
in_dns && /^[^[:space:]#][^:]*:/ {
|
||||
if (!restored) print_original()
|
||||
in_dns = 0
|
||||
print
|
||||
next
|
||||
}
|
||||
in_dns && /^[[:space:]]*nameserver:/ {
|
||||
ns_indent = indent_len($0)
|
||||
rest = $0
|
||||
sub(/^[[:space:]]*nameserver:[[:space:]]*/, "", rest)
|
||||
sub(/[[:space:]]*(#.*)?$/, "", rest)
|
||||
print_original()
|
||||
if (rest == "") skip_ns = 1
|
||||
next
|
||||
}
|
||||
skip_ns {
|
||||
cur_indent = indent_len($0)
|
||||
if ($0 ~ /^[[:space:]]*$/ || cur_indent > ns_indent) next
|
||||
skip_ns = 0
|
||||
}
|
||||
{ print }
|
||||
END { if (in_dns && !restored) print_original() }
|
||||
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
||||
rm -f "$state"
|
||||
fi
|
||||
rm -f "$selected" "$current" "$tmp" 2>/dev/null
|
||||
}
|
||||
|
||||
|
||||
# handle mihomo dns block:
|
||||
# - strip previous clashoo:dns_leak_protect injection markers (incl. the old
|
||||
# geosite:gfw block written by older versions)
|
||||
# - rewrite the ipv6: line from ipv6_value (skip when empty)
|
||||
# No longer injects fallback-filter.geosite:gfw — mihomo deprecated it and a
|
||||
# GeoSite.dat missing the gfw category froze startup (issue #25). See flush_dns.
|
||||
# (The fallback-filter parsing vars below are now vestigial; clean up separately.)
|
||||
dns_mihomo_apply_leak_dns_block() {
|
||||
local cfg="$1" enabled="${2:-0}" ipv6_value="${3:-}" tmp="${1}.$$"
|
||||
[ -f "$cfg" ] || return 0
|
||||
|
||||
awk -v enabled="$enabled" -v ipv6_value="$ipv6_value" '
|
||||
function indent_len(s) { match(s, /[^ ]/); return RSTART ? RSTART - 1 : length(s) }
|
||||
function spaces(n, out,i) {
|
||||
out = ""
|
||||
for (i = 0; i < n; i++)
|
||||
out = out " "
|
||||
return out
|
||||
}
|
||||
function flush_dns( i, sp, ff_sp) {
|
||||
if (!in_dns)
|
||||
return
|
||||
if (in_ff) ff_end = dns_n
|
||||
sp = spaces(child_indent > 0 ? child_indent : dns_indent + 2)
|
||||
ff_sp = (ff_child_indent > 0 ? spaces(ff_child_indent) : sp " ")
|
||||
|
||||
print dns_lines[1]
|
||||
if (ipv6_value != "")
|
||||
print sp "ipv6: " ipv6_value
|
||||
# NOTE: clashoo used to inject `fallback-filter.geosite: [gfw]` here, but
|
||||
# mihomo deprecated fallback-filter.geosite ("replace with nameserver-policy,
|
||||
# will be removed") AND it hard-fails preflight + freezes startup (~90s) when
|
||||
# the downloaded GeoSite.dat lacks the gfw category (issue #25). Stop
|
||||
# injecting it. DNS-leak protection still works via respect-rules / leak
|
||||
# nameservers / DST-PORT,853,REJECT handled elsewhere. The marker strip below
|
||||
# still removes any gfw block injected by older versions, so disabling /
|
||||
# upgrading cleans it up.
|
||||
for (i = 2; i <= dns_n; i++)
|
||||
print dns_lines[i]
|
||||
in_dns = 0
|
||||
dns_n = 0
|
||||
}
|
||||
|
||||
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect[[:space:]]*$/ {
|
||||
in_marker = 1
|
||||
next
|
||||
}
|
||||
in_marker {
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect[[:space:]]*$/)
|
||||
in_marker = 0
|
||||
next
|
||||
}
|
||||
|
||||
/^dns:[[:space:]]*$/ {
|
||||
flush_dns()
|
||||
in_dns = 1
|
||||
dns_indent = indent_len($0)
|
||||
child_indent = -1
|
||||
dns_n = 1
|
||||
dns_lines[dns_n] = $0
|
||||
has_ff = 0
|
||||
in_ff = 0
|
||||
in_ff_geosite = 0
|
||||
ff_indent = -1
|
||||
ff_child_indent = -1
|
||||
ff_geosite_indent = -1
|
||||
ff_has_gfw = 0
|
||||
ff_end = -1
|
||||
next
|
||||
}
|
||||
in_dns {
|
||||
cur_indent = indent_len($0)
|
||||
if ($0 ~ /^[^[:space:]#][^:]*:/ && cur_indent <= dns_indent) {
|
||||
flush_dns()
|
||||
print
|
||||
next
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && cur_indent > dns_indent && child_indent < 0)
|
||||
child_indent = cur_indent
|
||||
if (in_ff && cur_indent <= ff_indent) {
|
||||
ff_end = dns_n
|
||||
in_ff = 0
|
||||
in_ff_geosite = 0
|
||||
}
|
||||
if (in_ff_geosite && cur_indent <= ff_geosite_indent)
|
||||
in_ff_geosite = 0
|
||||
if ($0 ~ /^[[:space:]]*fallback-filter:[[:space:]]*$/) {
|
||||
has_ff = 1
|
||||
in_ff = 1
|
||||
ff_indent = cur_indent
|
||||
ff_child_indent = -1
|
||||
} else if (in_ff && $0 ~ /^[[:space:]]*[^#[:space:]][^:]*:/ && ff_child_indent < 0) {
|
||||
ff_child_indent = cur_indent
|
||||
}
|
||||
if (in_ff && $0 ~ /^[[:space:]]*geosite:[[:space:]]*$/) {
|
||||
in_ff_geosite = 1
|
||||
ff_geosite_indent = cur_indent
|
||||
} else if (in_ff_geosite && $0 ~ /^[[:space:]]*-[[:space:]]*gfw([[:space:]]*(#.*)?)?$/) {
|
||||
ff_has_gfw = 1
|
||||
}
|
||||
if ($0 ~ /^[[:space:]]*ipv6:[[:space:]]*/)
|
||||
next
|
||||
dns_lines[++dns_n] = $0
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END { flush_dns() }
|
||||
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
}
|
||||
|
||||
# handle DST-PORT,853,REJECT injection into rules block
|
||||
# only remove Clashoo marker block, never user original rules
|
||||
dns_mihomo_apply_leak_rule() {
|
||||
local cfg="$1" enabled="${2:-0}" tmp="${1}.$$" has_user_853
|
||||
[ -f "$cfg" ] || return 0
|
||||
|
||||
has_user_853=$(awk '
|
||||
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/ {
|
||||
in_marker = 1
|
||||
next
|
||||
}
|
||||
in_marker {
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/)
|
||||
in_marker = 0
|
||||
next
|
||||
}
|
||||
/^[[:space:]]*-[[:space:]]*DST-PORT,853,REJECT([[:space:]]*(#.*)?)?$/ { found = 1 }
|
||||
END { print found ? 1 : 0 }
|
||||
' "$cfg")
|
||||
|
||||
awk -v enabled="$enabled" -v has_user_853="$has_user_853" '
|
||||
BEGIN { inserted = 0; saw_rules = 0 }
|
||||
/^[[:space:]]*#[[:space:]]*>>>[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/ {
|
||||
in_marker = 1
|
||||
next
|
||||
}
|
||||
in_marker {
|
||||
if ($0 ~ /^[[:space:]]*#[[:space:]]*<<<[[:space:]]*clashoo:dns_leak_protect_rule[[:space:]]*$/)
|
||||
in_marker = 0
|
||||
next
|
||||
}
|
||||
/^rules:[[:space:]]*$/ {
|
||||
saw_rules = 1
|
||||
print
|
||||
if (enabled == "1" && has_user_853 != "1") {
|
||||
print " # >>> clashoo:dns_leak_protect_rule"
|
||||
print " - DST-PORT,853,REJECT"
|
||||
print " # <<< clashoo:dns_leak_protect_rule"
|
||||
inserted = 1
|
||||
}
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (enabled == "1" && has_user_853 != "1" && !inserted && !saw_rules) {
|
||||
print ""
|
||||
print "rules:"
|
||||
print " # >>> clashoo:dns_leak_protect_rule"
|
||||
print " - DST-PORT,853,REJECT"
|
||||
print " # <<< clashoo:dns_leak_protect_rule"
|
||||
}
|
||||
}
|
||||
' "$cfg" > "$tmp" && mv "$tmp" "$cfg"
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
}
|
||||
|
||||
dns_mihomo_apply_leak_protect() {
|
||||
local cfg="$1" enabled="${2:-0}" ipv6_value="${3:-}"
|
||||
[ -f "$cfg" ] || return 0
|
||||
dns_mihomo_apply_leak_nameservers "$cfg" "$enabled"
|
||||
dns_mihomo_apply_respect_rules "$cfg" "$enabled"
|
||||
dns_mihomo_apply_leak_dns_block "$cfg" "$enabled" "$ipv6_value"
|
||||
dns_mihomo_apply_leak_rule "$cfg" "$enabled"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
. as $document |
|
||||
strenv(CLASHOO_DNS_OLD_PORT) as $old_port |
|
||||
strenv(CLASHOO_DNS_HOST_PATTERN) as $host_pattern |
|
||||
(strenv(CLASHOO_DNS_REPLACEMENT) | split(" ") | map(select(length > 0))) as $replacement |
|
||||
("(?i)^(?:[a-z][a-z0-9+.-]*://)?(?:" + $host_pattern + "):" + $old_port + "(?:$|[/#?])") as $old_loopback |
|
||||
(
|
||||
$document |
|
||||
select((.dns."proxy-server-nameserver" | type) == "!!seq") |
|
||||
([.dns."proxy-server-nameserver"[] | select(type == "!!str" and test($old_loopback))] | length) as $match_count |
|
||||
select($match_count > 0) |
|
||||
.dns."proxy-server-nameserver" =
|
||||
([
|
||||
.dns."proxy-server-nameserver"[] |
|
||||
select((type == "!!str" and test($old_loopback)) | not) |
|
||||
select(. as $server | ($replacement | contains([$server])) | not)
|
||||
] + $replacement)
|
||||
) // $document
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
#!/bin/sh
|
||||
. /lib/functions.sh
|
||||
|
||||
|
||||
TMP_PREFIX="/tmp/clashoo_iprules.$$"
|
||||
CUSTOM_RULE_FILE="${TMP_PREFIX}.ipadd.conf"
|
||||
RULE="${TMP_PREFIX}.rules_conf.yaml"
|
||||
CLASH="${TMP_PREFIX}.conf.yaml"
|
||||
CONFIG_YAML="/etc/clashoo/config.yaml"
|
||||
CLASH_CONFIG="${TMP_PREFIX}.config.yaml"
|
||||
trap 'rm -f "$CUSTOM_RULE_FILE" "$RULE" "$CLASH" "$CLASH_CONFIG"' EXIT
|
||||
|
||||
[ -f "$CONFIG_YAML" ] || exit 0
|
||||
|
||||
. /usr/share/clashoo/runtime/primary_group.sh
|
||||
|
||||
|
||||
# __PROXY__ placeholder -> primary proxy group (cached by RPC);
|
||||
# fallback to GLOBAL (built-in mihomo group)
|
||||
CACHED_PRIMARY_GROUP="$(uci -q get clashoo.config.primary_proxy_group)"
|
||||
PRIMARY_GROUP="$(clashoo_resolve_primary_group "$CONFIG_YAML" "$CACHED_PRIMARY_GROUP")"
|
||||
[ -n "$PRIMARY_GROUP" ] || PRIMARY_GROUP="GLOBAL"
|
||||
|
||||
append=$(uci get clashoo.config.append_rules 2>/dev/null)
|
||||
if [ "${append:-0}" -eq 1 ];then
|
||||
|
||||
if [ -f $CLASH_CONFIG ];then
|
||||
rm -rf $CLASH_CONFIG 2>/dev/null
|
||||
fi
|
||||
|
||||
cp $CONFIG_YAML $CLASH_CONFIG 2>/dev/null
|
||||
if [ ! -z "$(grep "^Rule:" "$CLASH_CONFIG")" ]; then
|
||||
sed -i "/^Rule:/i\#RULESTART#" $CLASH_CONFIG 2>/dev/null
|
||||
elif [ ! -z "$(grep "^rules:" "$CLASH_CONFIG")" ]; then
|
||||
sed -i "/^rules:/i\#RULESTART#" $CLASH_CONFIG 2>/dev/null
|
||||
fi
|
||||
sed -i -e "\$a#RULEEND#" $CLASH_CONFIG 2>/dev/null
|
||||
|
||||
awk '/#RULESTART#/,/#RULEEND#/{print}' "$CLASH_CONFIG" 2>/dev/null |sed "s/\'//g" 2>/dev/null |sed 's/\"//g' 2>/dev/null |sed 's/\t/ /g' 2>/dev/null |grep '^ \{0,\}- '|awk -F '- ' '{print "- "$2}' | sed 's/^ \{0,\}//' 2>/dev/null |sed 's/ \{0,\}$//' 2>/dev/null >$RULE 2>&1
|
||||
|
||||
sed -i '/#RULESTART#/,/#RULEEND#/d' "$CLASH_CONFIG" 2>/dev/null
|
||||
|
||||
sed -i -e "\$a " $CLASH_CONFIG 2>/dev/null
|
||||
sed -i "1i\rules:" $RULE 2>/dev/null
|
||||
cat $CLASH_CONFIG $RULE >$CLASH 2>/dev/null
|
||||
mv $CLASH $CONFIG_YAML 2>/dev/null
|
||||
rm -f $RULE $CLASH_CONFIG 2>/dev/null
|
||||
fi
|
||||
|
||||
# ===== custom routing rules (UCI: config addtype), decoupled =====
|
||||
# fields: type(DOMAIN|SUFFIX|KEYWORD|IP-CIDR), ipaaddr, pgroup, res(no-resolve)
|
||||
# pgroup: DIRECT or __PROXY__ (resolved at injection)
|
||||
rm -f "$CUSTOM_RULE_FILE" 2>/dev/null
|
||||
|
||||
ipadd()
|
||||
{
|
||||
local section="$1"
|
||||
config_get "pgroup" "$section" "pgroup" ""
|
||||
config_get "ipaaddr" "$section" "ipaaddr" ""
|
||||
config_get "type" "$section" "type" ""
|
||||
config_get "res" "$section" "res" ""
|
||||
|
||||
[ -z "$type" ] && return
|
||||
[ -z "$ipaaddr" ] && return
|
||||
[ -z "$pgroup" ] && return
|
||||
|
||||
[ "$pgroup" = "__PROXY__" ] && pgroup="$PRIMARY_GROUP"
|
||||
|
||||
# quote whole rule (group name may contain spaces/emoji)
|
||||
# so mixed indent with subscription rules does not break YAML parsing
|
||||
if [ "${res}" = "1" ];then
|
||||
echo "${RULE_INDENT}- \"$type,$ipaaddr,$pgroup,no-resolve\"" >> "$CUSTOM_RULE_FILE"
|
||||
else
|
||||
echo "${RULE_INDENT}- \"$type,$ipaaddr,$pgroup\"" >> "$CUSTOM_RULE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# detect existing rules indent for alignment (default 2)
|
||||
RULE_INDENT="$(awk '/^[[:space:]]*rules:/{f=1;next} f&&/^[[:space:]]*-/{match($0,/^[[:space:]]*/);print substr($0,1,RLENGTH);exit}' "$CONFIG_YAML" 2>/dev/null)"
|
||||
[ -n "$RULE_INDENT" ] || RULE_INDENT=" "
|
||||
|
||||
config_load "clashoo"
|
||||
config_foreach ipadd "addtype"
|
||||
|
||||
# clean previous injection block (idempotent)
|
||||
sed -i '/#CUSTOMRULESTART#/,/#CUSTOMRULEEND#/d' "$CONFIG_YAML" 2>/dev/null
|
||||
|
||||
if [ -f "$CUSTOM_RULE_FILE" ];then
|
||||
sed -i -e "\$a#CUSTOMRULEEND#" "$CUSTOM_RULE_FILE" 2>/dev/null
|
||||
if [ ! -z "$(grep "^ \{0,\}rules:" "$CONFIG_YAML")" ]; then
|
||||
# 插在 rules: 段最前面,优先于订阅自带规则命中(纠错)
|
||||
sed -i '/^ \{0,\}rules:/a\#CUSTOMRULESTART#' "$CONFIG_YAML" 2>/dev/null
|
||||
else
|
||||
echo "rules:" >> "$CONFIG_YAML" 2>/dev/null
|
||||
echo "#CUSTOMRULESTART#" >> "$CONFIG_YAML" 2>/dev/null
|
||||
fi
|
||||
sed -i "/#CUSTOMRULESTART#/r${CUSTOM_RULE_FILE}" "$CONFIG_YAML" 2>/dev/null
|
||||
fi
|
||||
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/ucode
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
const uci = cursor();
|
||||
|
||||
function b(v) {
|
||||
if (v == null || v == '' || v == '0' || v == 'false') return false;
|
||||
if (v == '1' || v == 'true') return true;
|
||||
return v;
|
||||
}
|
||||
function i(v, d) { let n = v != null ? int(v) : null; return n != null ? n : d; }
|
||||
function s(v, d) { let t = trim(v || ''); return length(t) ? t : d; }
|
||||
function a(k) { return uci.get('clashoo', 'config', k); }
|
||||
function ab(k) { return b(a(k)); }
|
||||
|
||||
const cfg = {};
|
||||
|
||||
/* ── 端口与基本设置 ─────────────────────────────────────── */
|
||||
let v;
|
||||
v = i(a('http_port')); if (v != null) cfg['port'] = v;
|
||||
v = i(a('socks_port')); if (v != null) cfg['socks-port'] = v;
|
||||
v = i(a('redir_port')); if (v != null) cfg['redir-port'] = v;
|
||||
v = i(a('mixed_port')); if (v != null) cfg['mixed-port'] = v;
|
||||
v = i(a('tproxy_port')); if (v != null) cfg['tproxy-port'] = v;
|
||||
|
||||
v = a('bind_addr');
|
||||
if (!v || v == '*') v = null;
|
||||
if (v) cfg['bind-address'] = v;
|
||||
|
||||
v = a('p_mode');
|
||||
if (v == 'script') v = 'rule';
|
||||
if (v) cfg['mode'] = v;
|
||||
|
||||
cfg['log-level'] = s(a('level'), 'info');
|
||||
cfg['allow-lan'] = ab('allow_lan');
|
||||
cfg['ipv6'] = ab('enable_ipv6');
|
||||
cfg['routing-mark'] = 6666;
|
||||
cfg['external-controller'] = '0.0.0.0:' + i(a('dash_port'), 9090);
|
||||
cfg['external-ui'] = './dashboard';
|
||||
cfg['secret'] = s(a('dash_pass'), '');
|
||||
|
||||
v = a('interf');
|
||||
if (v && v != '0') cfg['interface-name'] = v;
|
||||
|
||||
/* ── TUN ───────────────────────────────────────────────── */
|
||||
let tun_mode = i(a('tun_mode'), 0);
|
||||
let tcp_mode = s(a('tcp_mode'), 'redirect');
|
||||
let udp_mode = s(a('udp_mode'), 'tproxy');
|
||||
let tun_enabled = (tun_mode == 1 || tcp_mode == 'tun' || udp_mode == 'tun');
|
||||
let acl_enabled = false;
|
||||
uci.foreach('clashoo', 'lan_acl', function(sec) {
|
||||
if (b(sec.enabled) != false) acl_enabled = true;
|
||||
});
|
||||
if (!ab('acl_migrated')) {
|
||||
let access_control = i(a('access_control'), 0);
|
||||
acl_enabled = access_control == 1 || access_control == 2;
|
||||
}
|
||||
let tun_acl = tun_enabled && acl_enabled;
|
||||
|
||||
cfg['tun'] = {
|
||||
enable: tun_enabled,
|
||||
stack: s(a('stack'), 'gvisor'),
|
||||
'auto-route': true,
|
||||
'auto-redirect': !tun_acl,
|
||||
'auto-detect-interface': true,
|
||||
};
|
||||
let tun_mtu = a('tun_mtu');
|
||||
if (tun_mtu != null) cfg['tun']['mtu'] = int(tun_mtu);
|
||||
if (ab('tun_gso')) {
|
||||
cfg['tun']['gso'] = true;
|
||||
cfg['tun']['gso-max-size'] = i(a('tun_gso_max_size'), 65536);
|
||||
}
|
||||
if (ab('tun_dns_hijack')) {
|
||||
cfg['tun']['dns-hijack'] = ['any:53', 'tcp://any:53'];
|
||||
}
|
||||
if (!tun_enabled) {
|
||||
cfg['tun']['enable'] = false;
|
||||
}
|
||||
|
||||
/* ── DNS ───────────────────────────────────────────────── */
|
||||
let dns_port = i(a('listen_port'), 1053);
|
||||
cfg['dns'] = {
|
||||
enable: b(a('enable_dns')) != false,
|
||||
listen: '0.0.0.0:' + dns_port,
|
||||
'enhanced-mode': s(a('enhanced_mode'), 'fake-ip'),
|
||||
'fake-ip-range': s(a('fake_ip_range'), '198.18.0.1/16'),
|
||||
'fake-ip-filter': [],
|
||||
ipv6: ab('enable_ipv6'),
|
||||
};
|
||||
if (cfg['dns']['enhanced-mode'] == 'fake-ip' && ab('enable_ipv6'))
|
||||
cfg['dns']['fake-ip-range6'] = s(a('fake_ip_range6'), 'fc00::/18');
|
||||
/* fake-ip-filter-mode: blacklist (default) | whitelist | rule */
|
||||
let filter_mode = s(a('fake_ip_filter_mode'), 'blacklist');
|
||||
if (filter_mode != 'blacklist') cfg['dns']['fake-ip-filter-mode'] = filter_mode;
|
||||
|
||||
/* map geosite:cn to bundled cn.mrs rule-set so mihomo skips the 10MB geosite.dat */
|
||||
let need_cn_rs = false;
|
||||
let keep_fakeip = length(s(getenv('CLASHOO_KEEP_FAKEIP_FILTER'), ''));
|
||||
if (keep_fakeip) {
|
||||
delete cfg['dns']['fake-ip-filter'];
|
||||
} else {
|
||||
let push_filter = function(f) {
|
||||
if (f == 'geosite:cn') { f = 'rule-set:cn_domain'; need_cn_rs = true; }
|
||||
else if (f == 'rule-set:cn_domain') need_cn_rs = true;
|
||||
push(cfg['dns']['fake-ip-filter'], f);
|
||||
};
|
||||
let filters = a('fake_ip_filter');
|
||||
if (type(filters) == 'array') { for (let f in filters) push_filter(f); }
|
||||
else if (filters != null) push_filter(filters);
|
||||
}
|
||||
|
||||
/* fallback-filter(默认 geoip:false,防止冷启动依赖 MMDB) */
|
||||
cfg['dns']['fallback-filter'] = { geoip: ab('fallback_filter_geoip') };
|
||||
|
||||
let dns_present = {};
|
||||
for (let f in split(s(getenv('CLASHOO_DNS_PRESENT'), ''), ','))
|
||||
if (length(trim(f))) dns_present[trim(f)] = true;
|
||||
let dns_force = {};
|
||||
for (let f in split(s(getenv('CLASHOO_DNS_FORCE'), ''), ','))
|
||||
if (length(trim(f))) dns_force[trim(f)] = true;
|
||||
let dns_inherit_mode = s(getenv('CLASHOO_DNS_INHERIT_MODE'), '');
|
||||
let inherited_nameserver = json(s(getenv('CLASHOO_DNS_INHERIT_NAMESERVER'), '[]'));
|
||||
if (type(inherited_nameserver) != 'array') {
|
||||
if (inherited_nameserver != null && inherited_nameserver != '')
|
||||
inherited_nameserver = [inherited_nameserver];
|
||||
else
|
||||
inherited_nameserver = [];
|
||||
}
|
||||
let dns_role_fields = ['nameserver', 'proxy-server-nameserver', 'direct-nameserver',
|
||||
'default-nameserver', 'nameserver-policy', 'respect-rules'];
|
||||
let had_user_dns_roles = false;
|
||||
for (let f in dns_role_fields)
|
||||
if (dns_present[f]) had_user_dns_roles = true;
|
||||
if (length(keys(dns_force)) || !had_user_dns_roles)
|
||||
cfg['dns']['x-clashoo-managed-dns'] = true;
|
||||
|
||||
function dns_scheme(protocol) {
|
||||
switch (trim(protocol || '')) {
|
||||
case '':
|
||||
case 'none': return '';
|
||||
case 'udp': case 'udp://': return 'udp://';
|
||||
case 'tcp': case 'tcp://': return 'tcp://';
|
||||
case 'dot': case 'tls': case 'tls://': return 'tls://';
|
||||
case 'doh': case 'https': case 'https://': return 'https://';
|
||||
case 'doq': case 'quic': case 'quic://': return 'quic://';
|
||||
default: return protocol;
|
||||
}
|
||||
}
|
||||
|
||||
function dns_server(address, protocol, port) {
|
||||
let addr = trim(address || '');
|
||||
if (!length(addr)) return null;
|
||||
if (match(addr, /^[A-Za-z][A-Za-z0-9+.-]*:\/\//)) return addr;
|
||||
let prefix = dns_scheme(protocol);
|
||||
return length(trim(port || '')) ? prefix + addr + ':' + trim(port) : prefix + addr;
|
||||
}
|
||||
|
||||
let dns_roles = {};
|
||||
uci.foreach('clashoo', 'dnsservers', function(sec) {
|
||||
if (b(sec.enabled) == false) return;
|
||||
let role = s(sec.ser_type, 'nameserver');
|
||||
if (role == 'fallback' && cfg['dns']['enhanced-mode'] == 'fake-ip') return;
|
||||
let srv = dns_server(sec.ser_address, sec.protocol, sec.ser_port);
|
||||
if (!srv) return;
|
||||
if (!dns_roles[role]) dns_roles[role] = [];
|
||||
push(dns_roles[role], srv);
|
||||
});
|
||||
for (let role in keys(dns_roles)) {
|
||||
if (role == 'proxy-server-nameserver' && (dns_inherit_mode == 'inherit' || dns_inherit_mode == 'skip') &&
|
||||
!dns_present[role] && !dns_force[role]) continue;
|
||||
if (!dns_present[role] || dns_force[role]) cfg['dns'][role] = dns_roles[role];
|
||||
}
|
||||
|
||||
if (dns_inherit_mode == 'inherit' && length(inherited_nameserver) &&
|
||||
!dns_present['proxy-server-nameserver'] && !dns_force['proxy-server-nameserver'])
|
||||
cfg['dns']['proxy-server-nameserver'] = inherited_nameserver;
|
||||
|
||||
let bootstrap = a('default_nameserver');
|
||||
if (bootstrap == null) bootstrap = a('defaul_nameserver');
|
||||
if (type(bootstrap) != 'array') bootstrap = bootstrap != null ? [bootstrap] : [];
|
||||
if (length(bootstrap) && (!dns_present['default-nameserver'] || dns_force['default-nameserver']))
|
||||
cfg['dns']['default-nameserver'] = bootstrap;
|
||||
|
||||
let policies = {};
|
||||
uci.foreach('clashoo', 'dns_policy', function(sec) {
|
||||
if (b(sec.enabled) == false) return;
|
||||
if (s(sec.policy_type, 'nameserver-policy') != 'nameserver-policy') return;
|
||||
let matcher = trim(sec.matcher || '');
|
||||
let servers = sec.nameserver;
|
||||
if (type(servers) != 'array') servers = servers != null ? [servers] : [];
|
||||
if (!length(matcher) || !length(servers)) return;
|
||||
if (matcher == 'geosite:cn') { matcher = 'rule-set:cn_domain'; need_cn_rs = true; }
|
||||
policies[matcher] = servers;
|
||||
});
|
||||
if (length(keys(policies)) && (!dns_present['nameserver-policy'] || dns_force['nameserver-policy']))
|
||||
cfg['dns']['nameserver-policy'] = policies;
|
||||
|
||||
let respect_rules = a('dns_respect_rules') == null ? true : ab('dns_respect_rules');
|
||||
if (dns_force['respect-rules'])
|
||||
cfg['dns']['respect-rules'] = respect_rules;
|
||||
else if (respect_rules && length(cfg['dns']['proxy-server-nameserver'] || [])
|
||||
&& !dns_present['respect-rules'] && !dns_present['prefer-h3'])
|
||||
cfg['dns']['respect-rules'] = true;
|
||||
|
||||
/* profile */
|
||||
let store_selected = ab('selection_cache');
|
||||
/* fake-ip cache defaults ON to survive restarts; opt out with '0' */
|
||||
let store_fake = a('fake_ip_cache') == null ? true : ab('fake_ip_cache');
|
||||
if (store_selected || store_fake) {
|
||||
cfg['profile'] = {};
|
||||
if (store_selected) cfg['profile']['store-selected'] = true;
|
||||
if (store_fake) cfg['profile']['store-fake-ip'] = true;
|
||||
}
|
||||
|
||||
/* bundled cn.mrs rule-provider, referenced by fake-ip-filter rule-set:cn_domain */
|
||||
if (need_cn_rs) {
|
||||
if (!cfg['rule-providers']) cfg['rule-providers'] = {};
|
||||
cfg['rule-providers']['cn_domain'] = {
|
||||
type: 'file',
|
||||
path: './ruleset/cn.mrs',
|
||||
format: 'mrs',
|
||||
behavior: 'domain',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ── authentication ────────────────────────────────── */
|
||||
if (ab('authentication')) {
|
||||
cfg['authentication'] = [];
|
||||
uci.foreach('clashoo', 'authentication', function(sec) {
|
||||
if (b(sec.enabled) == false) return;
|
||||
let user = trim(sec.username || '');
|
||||
let pass = trim(sec.password || '');
|
||||
if (user) push(cfg['authentication'], user + ':' + pass);
|
||||
});
|
||||
}
|
||||
|
||||
/* ── hosts ────────────────────────────────────────── */
|
||||
cfg['hosts'] = {};
|
||||
uci.foreach('clashoo', 'hosts', function(sec) {
|
||||
if (b(sec.enabled) == false) return;
|
||||
let domain = trim(sec.adress || '');
|
||||
let ip = trim(sec.ip || '');
|
||||
if (domain && ip) cfg['hosts'][domain] = ip;
|
||||
});
|
||||
if (length(keys(cfg['hosts'])) == 0) delete cfg['hosts'];
|
||||
|
||||
/* ── sniffer ──────────────────────────────────────── */
|
||||
if (ab('sniffer_streaming') && !length(s(getenv('CLASHOO_KEEP_SNIFFER'), ''))) {
|
||||
cfg['sniffer'] = {
|
||||
enable: true,
|
||||
'force-dns-mapping': true,
|
||||
'parse-pure-ip': true,
|
||||
sniff: {
|
||||
HTTP: { ports: [80, 8080], 'override-destination': true },
|
||||
TLS: { ports: [443, 8443], 'override-destination': true },
|
||||
QUIC: { ports: [443, 8443], 'override-destination': true },
|
||||
},
|
||||
'force-domain': [
|
||||
'+.youtube.com',
|
||||
'+.googlevideo.com',
|
||||
'+.netflix.com',
|
||||
'+.nflxvideo.net',
|
||||
'+.disneyplus.com',
|
||||
'+.hulu.com',
|
||||
'+.hbomax.com',
|
||||
],
|
||||
'skip-domain': [
|
||||
'+.lan',
|
||||
'+.local',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/* ── Smart kernel injection ───────────────────────── */
|
||||
if (ab('smart_auto_switch')) {
|
||||
/* Smart 策略注入:通过 proxy-groups 覆盖实现 */
|
||||
}
|
||||
|
||||
/* ── ECS ──────────────────────────────────────────── */
|
||||
let ecs = s(a('dns_ecs'));
|
||||
if (ecs) cfg['dns']['edns-client-subnet'] = ecs;
|
||||
|
||||
/* ── cache_file / experimental ──────────────────── */
|
||||
cfg['experimental'] = {
|
||||
'cache_file': { enabled: true },
|
||||
'clash_api': {
|
||||
'external-controller': '0.0.0.0:' + i(a('dash_port'), 9090),
|
||||
'external-ui': './dashboard',
|
||||
'secret': s(a('dash_pass'), ''),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/* ── 输出 JSON (不含 null/空对象) ─────────────────── */
|
||||
function clean(obj) {
|
||||
if (type(obj) == 'array') {
|
||||
let r = [];
|
||||
for (let e in obj) {
|
||||
e = clean(e);
|
||||
if (e != null) push(r, e);
|
||||
}
|
||||
return length(r) ? r : null;
|
||||
}
|
||||
if (type(obj) == 'object') {
|
||||
let r = {};
|
||||
for (let k in keys(obj)) {
|
||||
let v = clean(obj[k]);
|
||||
if (v != null) r[k] = v;
|
||||
}
|
||||
return length(keys(r)) ? r : null;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
print(clean(cfg));
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
|
||||
clashoo_resolve_primary_group() {
|
||||
local config_yaml="$1"
|
||||
local cached_group="$2"
|
||||
local group_rows
|
||||
local resolved_group
|
||||
|
||||
if [ "$cached_group" = "GLOBAL" ]; then
|
||||
printf '%s\n' "GLOBAL"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$config_yaml" ] || ! command -v yq >/dev/null 2>&1; then
|
||||
printf '%s\n' "GLOBAL"
|
||||
return 0
|
||||
fi
|
||||
|
||||
group_rows="$(yq -M e -r '(."proxy-groups" // [])[] | [.name, .type] | @tsv' "$config_yaml" 2>/dev/null)" || {
|
||||
printf '%s\n' "GLOBAL"
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ -n "$cached_group" ] && printf '%s\n' "$group_rows" |
|
||||
awk -F '\t' -v wanted="$cached_group" '$1 == wanted { found = 1; exit } END { exit !found }'
|
||||
then
|
||||
printf '%s\n' "$cached_group"
|
||||
return 0
|
||||
fi
|
||||
|
||||
resolved_group="$(printf '%s\n' "$group_rows" | awk -F '\t' '
|
||||
tolower($2) == "select" &&
|
||||
(index($1, "节点选择") || index($1, "代理") || index($1, "🚀") || tolower($1) ~ /proxy/) {
|
||||
print $1
|
||||
exit
|
||||
}
|
||||
')"
|
||||
|
||||
if [ -z "$resolved_group" ]; then
|
||||
resolved_group="$(printf '%s\n' "$group_rows" | awk -F '\t' '
|
||||
tolower($2) == "select" {
|
||||
print $1
|
||||
exit
|
||||
}
|
||||
')"
|
||||
fi
|
||||
|
||||
printf '%s\n' "${resolved_group:-GLOBAL}"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# If every configured rule points to a proxy without UDP support, mihomo falls
|
||||
# through to DIRECT. Reject only that otherwise-unmatched QUIC traffic so the
|
||||
# client can retry over TCP; rules with a usable UDP target still win first.
|
||||
with(
|
||||
select(has("rules")) |
|
||||
select((.rules | type) == "!!seq") |
|
||||
select(([.rules[] | select(. == "AND,((NETWORK,UDP),(DST-PORT,443)),REJECT")] | length) == 0);
|
||||
.rules += ["AND,((NETWORK,UDP),(DST-PORT,443)),REJECT"]
|
||||
)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ -f /usr/share/clashbackup/history ];then
|
||||
|
||||
HISTORY_PATH="/usr/share/clashbackup/history"
|
||||
SECRET=$(uci get clashoo.config.dash_pass 2>/dev/null)
|
||||
LAN_IP=$(uci get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null)
|
||||
PORT=$(uci get clashoo.config.dash_port 2>/dev/null)
|
||||
urlencode() {
|
||||
local data
|
||||
if [ "$#" != 1 ]; then
|
||||
return 1
|
||||
fi
|
||||
data=$(curl -s -o /dev/null -w %{url_effective} --get --data-urlencode "$1" "")
|
||||
if [ ! -z "$data" ]; then
|
||||
echo "${data##/?}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
cat $HISTORY_PATH |while read line
|
||||
do
|
||||
if [ -z "$(echo $line |grep "#*#")" ]; then
|
||||
continue
|
||||
else
|
||||
GORUP_NAME=$(urlencode "$(echo $line |awk -F '#*#' '{print $1}')")
|
||||
NOW_NAME=$(echo $line |awk -F '#*#' '{print $3}')
|
||||
curl -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X PUT -d '{"name":"'"$NOW_NAME"'"}' http://"$LAN_IP":"$PORT"/proxies/"$GORUP_NAME" >/dev/null 2>&1
|
||||
fi
|
||||
done >/dev/null 2>&1
|
||||
curl -m 5 --retry 2 -H "Authorization: Bearer ${SECRET}" -H "Content-Type:application/json" -X DELETE http://"$LAN_IP":"$PORT"/connections >/dev/null 2>&1
|
||||
fi
|
||||
@@ -0,0 +1,28 @@
|
||||
# Preserve every explicit user choice while restoring missing per-protocol defaults.
|
||||
with(
|
||||
.sniffer |
|
||||
select((has("enable") | not) or (.enable == true)) |
|
||||
select((has("override-destination") | not) or (."override-destination" == true));
|
||||
with(
|
||||
select(has("force-dns-mapping") | not);
|
||||
."force-dns-mapping" = true
|
||||
) |
|
||||
with(
|
||||
select(has("parse-pure-ip") | not);
|
||||
."parse-pure-ip" = true
|
||||
) |
|
||||
with(
|
||||
.sniff | select(has("TLS"));
|
||||
with(
|
||||
.TLS | select(type == "!!map" and (has("override-destination") | not));
|
||||
."override-destination" = true
|
||||
)
|
||||
) |
|
||||
with(
|
||||
.sniff | select(has("QUIC"));
|
||||
with(
|
||||
.QUIC | select(type == "!!map" and (has("override-destination") | not));
|
||||
."override-destination" = true
|
||||
)
|
||||
)
|
||||
)
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
time.windows.com
|
||||
time.nist.gov
|
||||
time.apple.com
|
||||
time.asia.apple.com
|
||||
cn.ntp.org.cn
|
||||
edu.ntp.org.cn
|
||||
hk.ntp.org.cn
|
||||
tw.ntp.org.cn
|
||||
us.ntp.org.cn
|
||||
sgp.ntp.org.cn
|
||||
kr.ntp.org.cn
|
||||
jp.ntp.org.cn
|
||||
de.ntp.org.cn
|
||||
ina.ntp.org.cn
|
||||
0.openwrt.pool.ntp.org
|
||||
1.openwrt.pool.ntp.org
|
||||
2.openwrt.pool.ntp.org
|
||||
3.openwrt.pool.ntp.org
|
||||
ntp.aliyun.com
|
||||
ntp1.aliyun.com
|
||||
ntp2.aliyun.com
|
||||
ntp3.aliyun.com
|
||||
ntp4.aliyun.com
|
||||
ntp5.aliyun.com
|
||||
ntp6.aliyun.com
|
||||
ntp7.aliyun.com
|
||||
time1.aliyun.com
|
||||
time2.aliyun.com
|
||||
time3.aliyun.com
|
||||
time4.aliyun.com
|
||||
time5.aliyun.com
|
||||
time6.aliyun.com
|
||||
time7.aliyun.com
|
||||
s1c.time.edu.cn
|
||||
s2m.time.edu.cn
|
||||
s1b.time.edu.cn
|
||||
s1e.time.edu.cn
|
||||
s2a.time.edu.cn
|
||||
s2b.time.edu.cn
|
||||
time1.apple.com
|
||||
time2.apple.com
|
||||
time3.apple.com
|
||||
time4.apple.com
|
||||
time5.apple.com
|
||||
time6.apple.com
|
||||
time7.apple.com
|
||||
time1.google.com
|
||||
time2.google.com
|
||||
time3.google.com
|
||||
time4.google.com
|
||||
.music.163.com
|
||||
.music.126.net
|
||||
man.netease.com
|
||||
api.iplay.163.com
|
||||
ac.dun.163yun.com
|
||||
mr.da.netease.com
|
||||
crash.163.com
|
||||
imap.163.com
|
||||
msftconnecttest.com
|
||||
msftncsi.com
|
||||
captive.qq.com
|
||||
ssl.ptlogin2.qq.com
|
||||
login.qq.com
|
||||
+642
@@ -0,0 +1,642 @@
|
||||
#!/bin/sh
|
||||
|
||||
LOG_FILE="/tmp/clashoo_component_update.log"
|
||||
RUN_FILE="/var/run/clashoo_component_update"
|
||||
STATE_FILE="/tmp/clashoo_component_update_state"
|
||||
TMP_DIR="/tmp/clashoo-component-update"
|
||||
APK_UPGRADE_FLAG="/var/run/clashoo_package_upgrade"
|
||||
FEED_BASE_URL="https://down.dllkids.xyz/openwrt-feed/clashoo"
|
||||
GITHUB_API_URL="https://api.github.com/repos/kenzok8/openwrt-clashoo/releases/latest"
|
||||
ENV_GITHUB_PROXY_PREFIX="${GITHUB_PROXY_PREFIX:-}"
|
||||
UCI_GITHUB_PROXY_PREFIX="$(uci -q get clashoo.config.core_mirror_prefix 2>/dev/null)"
|
||||
GITHUB_PROXY_PREFIX="${UCI_GITHUB_PROXY_PREFIX:-${ENV_GITHUB_PROXY_PREFIX:-https://ghfast.top/}}"
|
||||
case "$GITHUB_PROXY_PREFIX" in
|
||||
*/) ;;
|
||||
*) GITHUB_PROXY_PREFIX="${GITHUB_PROXY_PREFIX}/" ;;
|
||||
esac
|
||||
CONNECT_TIMEOUT="${CONNECT_TIMEOUT:-8}"
|
||||
REQUEST_TIMEOUT="${REQUEST_TIMEOUT:-20}"
|
||||
LOW_SPEED_TIME="${LOW_SPEED_TIME:-30}"
|
||||
LOW_SPEED_LIMIT="${LOW_SPEED_LIMIT:-1024}"
|
||||
|
||||
trap 'rm -f "$APK_UPGRADE_FLAG"' EXIT
|
||||
|
||||
log() {
|
||||
mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$STATE_FILE")" >/dev/null 2>&1
|
||||
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$LOG_FILE"
|
||||
printf 'component=%s\nstatus=%s\nmessage=%s\n' "${COMPONENT:-}" "${STATUS:-running}" "$*" >"$STATE_FILE"
|
||||
}
|
||||
|
||||
finish() {
|
||||
rc="$1"
|
||||
msg="$2"
|
||||
STATUS="success"
|
||||
[ "$rc" -eq 0 ] || STATUS="failed"
|
||||
log "$msg"
|
||||
rm -f "$RUN_FILE" "$APK_UPGRADE_FLAG"
|
||||
exit "$rc"
|
||||
}
|
||||
|
||||
# In kernel-only mode there is no transparent proxy, so component downloads
|
||||
# would go out direct and stall behind the GFW. Route them through the running
|
||||
# core (shared logic in proxy_lib.sh). Normal mode returns empty -> TPROXY.
|
||||
# Guard the source: on a stale install missing proxy_lib.sh, an unguarded `.`
|
||||
# under `set -e` kills the whole update silently (empty log, nothing happens).
|
||||
# Fall back to no proxy detection (direct / TPROXY) instead of dying.
|
||||
if [ -f /usr/share/clashoo/update/proxy_lib.sh ]; then
|
||||
. /usr/share/clashoo/update/proxy_lib.sh
|
||||
else
|
||||
clashoo_detect_proxy() { :; }
|
||||
fi
|
||||
detect_proxy() { clashoo_detect_proxy; }
|
||||
|
||||
fetch_text() {
|
||||
url="$1"
|
||||
proxy="$(detect_proxy)"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL --connect-timeout "$CONNECT_TIMEOUT" --max-time "$REQUEST_TIMEOUT" ${proxy:+--proxy "$proxy"} "$url"
|
||||
return $?
|
||||
fi
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
[ -n "$proxy" ] && { http_proxy="$proxy" https_proxy="$proxy" wget -qO- --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"; return $?; }
|
||||
wget -qO- --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"
|
||||
return $?
|
||||
fi
|
||||
return 127
|
||||
}
|
||||
|
||||
download_file() {
|
||||
url="$1"
|
||||
out="$2"
|
||||
proxy="$(detect_proxy)"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fL --connect-timeout "$CONNECT_TIMEOUT" --speed-time "$LOW_SPEED_TIME" --speed-limit "$LOW_SPEED_LIMIT" ${proxy:+--proxy "$proxy"} "$url" -o "$out"
|
||||
return $?
|
||||
fi
|
||||
if [ -n "$proxy" ]; then
|
||||
http_proxy="$proxy" https_proxy="$proxy" wget -qO "$out" --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"
|
||||
return $?
|
||||
fi
|
||||
wget -qO "$out" --timeout="$REQUEST_TIMEOUT" --tries=1 "$url"
|
||||
}
|
||||
|
||||
download_url() {
|
||||
url="$1"
|
||||
case "$url" in
|
||||
https://github.com/*)
|
||||
printf '%s%s\n' "$GITHUB_PROXY_PREFIX" "$url"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$url"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
root_free_kb() {
|
||||
df -k / 2>/dev/null | awk 'NR==2 {print $4}'
|
||||
}
|
||||
|
||||
kb_to_mib() {
|
||||
awk "BEGIN {printf \"%.0f\", (${1:-0}) / 1024}"
|
||||
}
|
||||
|
||||
ensure_root_space() {
|
||||
need_kb="$1"
|
||||
free_kb="$(root_free_kb)"
|
||||
[ -n "$free_kb" ] || return 0
|
||||
[ "$free_kb" -ge "$need_kb" ] && return 0
|
||||
finish 1 "根分区空间不足:可用 $(kb_to_mib "$free_kb") MiB,至少需要 $(kb_to_mib "$need_kb") MiB"
|
||||
}
|
||||
|
||||
log_install_error() {
|
||||
file="$1"
|
||||
[ -s "$file" ] || return 0
|
||||
tail -8 "$file" 2>/dev/null | while IFS= read -r line; do
|
||||
[ -n "$line" ] && log "$line"
|
||||
done
|
||||
}
|
||||
|
||||
detect_manager() {
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
echo opkg
|
||||
return
|
||||
fi
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
echo apk
|
||||
return
|
||||
fi
|
||||
echo unsupported
|
||||
}
|
||||
|
||||
installed_package_version() {
|
||||
pkg="$1"
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
opkg status "$pkg" 2>/dev/null | awk -F': ' '/^Version:/{print $2; exit}'
|
||||
return
|
||||
fi
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk list -I "$pkg" 2>/dev/null | awk '{print $1; exit}' | sed -n "s/^${pkg}-//p"
|
||||
fi
|
||||
}
|
||||
|
||||
package_installed() {
|
||||
pkg="$1"
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
opkg status "$pkg" 2>/dev/null | awk '
|
||||
$1 == "Status:" && $2 == "install" && $NF == "installed" { found = 1 }
|
||||
END { exit !found }
|
||||
'
|
||||
return
|
||||
fi
|
||||
apk info -e "$pkg" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
apk_install() {
|
||||
proxy="$(detect_proxy)"
|
||||
if [ -n "$proxy" ]; then
|
||||
http_proxy="$proxy" https_proxy="$proxy" apk add --allow-untrusted --timeout 30 "$@"
|
||||
else
|
||||
apk add --allow-untrusted --timeout 30 "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
apk_bundle_install() {
|
||||
printf '%s\n' "$$" >"$APK_UPGRADE_FLAG"
|
||||
apk_install "$@"
|
||||
rc=$?
|
||||
rm -f "$APK_UPGRADE_FLAG"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
verify_downloaded_package() {
|
||||
file="$1"
|
||||
[ -s "$file" ] || return 1
|
||||
if [ "$PM" = "apk" ]; then
|
||||
apk verify --allow-untrusted "$file" >/dev/null 2>&1
|
||||
return
|
||||
fi
|
||||
gzip -t "$file" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
download_package_file() {
|
||||
url="$1"
|
||||
out="$2"
|
||||
name="$3"
|
||||
download_file "$(download_url "$url")" "$out" || finish 1 "下载 ${name} 失败"
|
||||
verify_downloaded_package "$out" || finish 1 "${name} 文件校验失败"
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
pm="$1"
|
||||
if [ "$pm" = "opkg" ]; then
|
||||
opkg print-architecture 2>/dev/null | awk '/^arch / {print $2}' | tail -n 1
|
||||
return
|
||||
fi
|
||||
# apk: DISTRIB_ARCH is authoritative (e.g. aarch64_cortex-a53); apk --print-arch
|
||||
# only gives the generic "aarch64" which won't match the .apk package suffix.
|
||||
if [ -r /etc/openwrt_release ]; then
|
||||
a="$(sed -n "s/^DISTRIB_ARCH=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)"
|
||||
[ -n "$a" ] && { printf '%s\n' "$a"; return; }
|
||||
fi
|
||||
apk --print-arch 2>/dev/null
|
||||
}
|
||||
|
||||
detect_sdk() {
|
||||
[ -r /etc/openwrt_release ] || return 1
|
||||
release="$(sed -n "s/^DISTRIB_RELEASE=['\"]\\([^'\"]*\\)['\"]$/\\1/p" /etc/openwrt_release | head -n 1)"
|
||||
[ -n "$release" ] || return 1
|
||||
printf '%s\n' "$release" | grep -Eo '[0-9]+\.[0-9]+' | head -n 1
|
||||
}
|
||||
|
||||
append_unique_word() {
|
||||
value="$1"
|
||||
list="$2"
|
||||
[ -n "$value" ] || {
|
||||
printf '%s\n' "$list"
|
||||
return
|
||||
}
|
||||
case " $list " in
|
||||
*" $value "*) ;;
|
||||
*) list="${list}${list:+ }${value}" ;;
|
||||
esac
|
||||
printf '%s\n' "$list"
|
||||
}
|
||||
|
||||
build_sdk_candidates() {
|
||||
pm="$1"
|
||||
candidates=""
|
||||
detected_sdk="$(detect_sdk || true)"
|
||||
candidates="$(append_unique_word "$detected_sdk" "$candidates")"
|
||||
|
||||
if [ "$pm" = "opkg" ]; then
|
||||
for sdk in 24.10 23.05 22.03 21.02; do
|
||||
candidates="$(append_unique_word "$sdk" "$candidates")"
|
||||
done
|
||||
else
|
||||
for sdk in 25.12 24.10; do
|
||||
candidates="$(append_unique_word "$sdk" "$candidates")"
|
||||
done
|
||||
fi
|
||||
|
||||
printf '%s\n' "$candidates"
|
||||
}
|
||||
|
||||
find_manifest_value() {
|
||||
key="$1"
|
||||
manifest_text="$2"
|
||||
printf '%s\n' "$manifest_text" | sed -n "s/^${key}=//p" | head -n 1
|
||||
}
|
||||
|
||||
load_manifest_urls() {
|
||||
sdk="$1"
|
||||
arch="$2"
|
||||
manifest_url="${FEED_BASE_URL}/${sdk}/${arch}/manifest-clashoo.txt"
|
||||
manifest_text="$(fetch_text "$manifest_url" || true)"
|
||||
[ -n "$manifest_text" ] || return 1
|
||||
|
||||
core_file="$(find_manifest_value "core" "$manifest_text")"
|
||||
luci_file="$(find_manifest_value "luci" "$manifest_text")"
|
||||
i18n_file="$(find_manifest_value "i18n" "$manifest_text")"
|
||||
[ -n "$core_file" ] || return 1
|
||||
[ -n "$luci_file" ] || return 1
|
||||
case "$core_file" in *."$EXT") ;; *) return 1 ;; esac
|
||||
case "$luci_file" in *."$EXT") ;; *) return 1 ;; esac
|
||||
case "$i18n_file" in ""|*."$EXT") ;; *) return 1 ;; esac
|
||||
|
||||
CORE_URL="${FEED_BASE_URL}/${sdk}/${arch}/${core_file}"
|
||||
LUCI_URL="${FEED_BASE_URL}/${sdk}/${arch}/${luci_file}"
|
||||
I18N_URL=""
|
||||
[ -n "$i18n_file" ] && I18N_URL="${FEED_BASE_URL}/${sdk}/${arch}/${i18n_file}"
|
||||
SOURCE_LABEL="R2 feed ${sdk}/${arch}"
|
||||
return 0
|
||||
}
|
||||
|
||||
load_github_urls() {
|
||||
arch="$1"
|
||||
ext="$2"
|
||||
payload="$(fetch_text "$GITHUB_API_URL" || true)"
|
||||
[ -n "$payload" ] || return 1
|
||||
|
||||
urls="$(printf '%s\n' "$payload" | sed -n 's/.*"browser_download_url":[[:space:]]*"\([^"]*\)".*/\1/p')"
|
||||
[ -n "$urls" ] || return 1
|
||||
|
||||
if [ "$ext" = "apk" ]; then
|
||||
CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo-[^-]+.*-r[0-9]+-'"$arch"'\.apk$' | head -n 1)"
|
||||
LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)"
|
||||
I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn-[^-]+.*-r[0-9]+-('"$arch"'|all)\.apk$' | head -n 1)"
|
||||
else
|
||||
CORE_URL="$(printf '%s\n' "$urls" | grep -E '/clashoo_.*_'"$arch"'\.ipk$' | head -n 1)"
|
||||
LUCI_URL="$(printf '%s\n' "$urls" | grep -E '/luci-app-clashoo_.*_all\.ipk$' | head -n 1)"
|
||||
I18N_URL="$(printf '%s\n' "$urls" | grep -E '/luci-i18n-clashoo-zh-cn_.*_all\.ipk$' | head -n 1)"
|
||||
fi
|
||||
|
||||
[ -n "$CORE_URL" ] || return 1
|
||||
[ -n "$LUCI_URL" ] || return 1
|
||||
SOURCE_LABEL="GitHub latest"
|
||||
return 0
|
||||
}
|
||||
|
||||
package_version_from_url() {
|
||||
file="${1##*/}"
|
||||
case "$file" in
|
||||
clashoo_*_"$ARCH".ipk)
|
||||
v="${file#clashoo_}"
|
||||
v="${v%_${ARCH}.ipk}"
|
||||
printf '%s\n' "$v" | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/'
|
||||
;;
|
||||
clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^clashoo_\(.*\)_[^_][^_]*\.ipk$/\1/p' | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/' ;;
|
||||
luci-app-clashoo_*.ipk) printf '%s\n' "$file" | sed -n 's/^luci-app-clashoo_\(.*\)_all\.ipk$/\1/p' ;;
|
||||
luci-i18n-clashoo-zh-cn_*.ipk) printf '%s\n' "$file" | sed -n 's/^luci-i18n-clashoo-zh-cn_\(.*\)_all\.ipk$/\1/p' ;;
|
||||
clashoo-*.apk)
|
||||
v="$(printf '%s\n' "$file" | sed -n -e 's/^clashoo-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^clashoo-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1)"
|
||||
printf '%s\n' "$v" | sed 's/^\([0-9][0-9][0-9][0-9]\.[0-9][0-9]*\.[0-9][0-9]*\)\./\1~/'
|
||||
;;
|
||||
luci-app-clashoo-*.apk) printf '%s\n' "$file" | sed -n -e 's/^luci-app-clashoo-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^luci-app-clashoo-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1 ;;
|
||||
luci-i18n-clashoo-zh-cn-*.apk) printf '%s\n' "$file" | sed -n -e 's/^luci-i18n-clashoo-zh-cn-\(.*-r[0-9][0-9]*\)-.*\.apk$/\1/p' -e 's/^luci-i18n-clashoo-zh-cn-\(.*-r[0-9][0-9]*\)\.apk$/\1/p' | head -n 1 ;;
|
||||
*) printf '%s\n' "$file" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_bundle_urls() {
|
||||
PM="$(detect_manager)"
|
||||
[ "$PM" != "unsupported" ] || return 1
|
||||
ARCH="$(detect_arch "$PM")"
|
||||
[ -n "$ARCH" ] || return 1
|
||||
EXT="ipk"
|
||||
[ "$PM" = "apk" ] && EXT="apk"
|
||||
|
||||
CORE_URL=""
|
||||
LUCI_URL=""
|
||||
I18N_URL=""
|
||||
SOURCE_LABEL=""
|
||||
|
||||
SDK_CANDIDATES="$(build_sdk_candidates "$PM")"
|
||||
for sdk in $SDK_CANDIDATES; do
|
||||
if load_manifest_urls "$sdk" "$ARCH"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
load_github_urls "$ARCH" "$EXT"
|
||||
}
|
||||
|
||||
backup_config() {
|
||||
ts="$(date '+%Y%m%d-%H%M%S')"
|
||||
BACKUP_DIR="/etc/clashoo/backup/component-upgrade-${ts}"
|
||||
mkdir -p "$BACKUP_DIR/etc-config" "$BACKUP_DIR/etc-clashoo" >/dev/null 2>&1 || return 1
|
||||
[ -r /etc/config/clashoo ] && cp -a /etc/config/clashoo "$BACKUP_DIR/etc-config/clashoo"
|
||||
# 只备份用户配置;Model.bin 等可重新下载的大文件不入备份,避免每次更新
|
||||
# 复制 ~5MB 的同一模型,把备份目录撑大、堆满本就紧张的根分区。
|
||||
for path in \
|
||||
/etc/clashoo/config.yaml \
|
||||
/etc/clashoo/config.json \
|
||||
/etc/clashoo/*.yaml \
|
||||
/etc/clashoo/*.yml \
|
||||
/etc/clashoo/*.json
|
||||
do
|
||||
[ -e "$path" ] || continue
|
||||
cp -a "$path" "$BACKUP_DIR/etc-clashoo/" 2>/dev/null || true
|
||||
done
|
||||
log "已备份配置:${BACKUP_DIR}"
|
||||
|
||||
# 只保留最近 1 个备份,删除更早的,避免无限堆积占满磁盘。
|
||||
ls -dt /etc/clashoo/backup/component-upgrade-* 2>/dev/null | tail -n +2 | while read -r _old; do
|
||||
[ -n "$_old" ] && rm -rf "$_old" 2>/dev/null
|
||||
done
|
||||
}
|
||||
|
||||
restore_config_backup() {
|
||||
[ -n "${BACKUP_DIR:-}" ] || return 0
|
||||
[ -r "$BACKUP_DIR/etc-config/clashoo" ] && cp -a "$BACKUP_DIR/etc-config/clashoo" /etc/config/clashoo
|
||||
if [ -d "$BACKUP_DIR/etc-clashoo" ]; then
|
||||
mkdir -p /etc/clashoo
|
||||
cp -a "$BACKUP_DIR/etc-clashoo/." /etc/clashoo/ 2>/dev/null || true
|
||||
fi
|
||||
uci commit clashoo >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
clashoo_was_running() {
|
||||
/etc/init.d/clashoo status >/dev/null 2>&1 && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
restart_web_stack() {
|
||||
# reload (not restart) rpcd so the LuCI login session survives a plugin
|
||||
# update — rpcd restart drops every ubus session and forces a re-login
|
||||
# (issue #12). reload still picks up the new ucode RPC backend. uhttpd needs
|
||||
# nothing: updated static assets are served on the next request.
|
||||
/etc/init.d/rpcd reload >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# which: clashoo(仅核心)/ luci(luci-app + 语言包)。两者拆开,clashoo
|
||||
# 核心更新频率高于 LuCI,分别更新便于定位失败。
|
||||
run_pkg_update() {
|
||||
which="$1"
|
||||
log "正在检查组件包"
|
||||
if ! resolve_bundle_urls; then
|
||||
finish 1 "未找到适配当前设备的组件包"
|
||||
fi
|
||||
log "更新来源:${SOURCE_LABEL}"
|
||||
|
||||
core_ver="$(package_version_from_url "$CORE_URL")"
|
||||
luci_ver="$(package_version_from_url "$LUCI_URL")"
|
||||
i18n_ver=""
|
||||
[ -z "$I18N_URL" ] || i18n_ver="$(package_version_from_url "$I18N_URL")"
|
||||
core_ver_before="$(installed_package_version clashoo)"
|
||||
|
||||
apk_core_ready() {
|
||||
package_installed clashoo && [ "$(installed_package_version clashoo)" = "$core_ver" ] \
|
||||
&& [ -x /etc/init.d/clashoo ] && [ -x /usr/share/clashoo/update/component_update.sh ]
|
||||
}
|
||||
|
||||
apk_luci_ready() {
|
||||
package_installed luci-app-clashoo && [ "$(installed_package_version luci-app-clashoo)" = "$luci_ver" ] \
|
||||
&& { [ -z "$I18N_URL" ] || { package_installed luci-i18n-clashoo-zh-cn \
|
||||
&& [ "$(installed_package_version luci-i18n-clashoo-zh-cn)" = "$i18n_ver" ]; }; } \
|
||||
&& [ -r /usr/share/luci/menu.d/luci-app-clashoo.json ] \
|
||||
&& [ -r /usr/share/rpcd/ucode/luci.clashoo ]
|
||||
}
|
||||
|
||||
apk_bundle_ready() {
|
||||
apk_core_ready && apk_luci_ready
|
||||
}
|
||||
|
||||
apk_update_ready() {
|
||||
if [ "$which" = "clashoo" ]; then
|
||||
apk_bundle_ready
|
||||
else
|
||||
package_installed clashoo && apk_luci_ready
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$which" = "clashoo" ]; then
|
||||
log "目标版本:Clashoo 核心 ${core_ver}"
|
||||
cur_ver="$(installed_package_version clashoo)"
|
||||
if [ "$PM" = "apk" ] && apk_update_ready; then
|
||||
finish 0 "Clashoo 核心已是最新版本"
|
||||
elif [ "$PM" != "apk" ] && [ -n "$core_ver" ] && [ "$cur_ver" = "$core_ver" ]; then
|
||||
finish 0 "Clashoo 核心已是最新版本"
|
||||
fi
|
||||
else
|
||||
log "目标版本:客户端 ${luci_ver}"
|
||||
cur_ver="$(installed_package_version luci-app-clashoo)"
|
||||
if [ "$PM" = "apk" ] && apk_update_ready; then
|
||||
finish 0 "客户端已是最新版本"
|
||||
elif [ "$PM" != "apk" ] && [ -n "$luci_ver" ] && [ "$cur_ver" = "$luci_ver" ]; then
|
||||
finish 0 "客户端已是最新版本"
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
mkdir -p "$TMP_DIR" || finish 1 "创建临时目录失败"
|
||||
|
||||
was_running=0
|
||||
clashoo_was_running && was_running=1
|
||||
backup_config || finish 1 "备份配置失败"
|
||||
apk_core_file=""
|
||||
|
||||
if [ "$PM" = "apk" ]; then
|
||||
log "正在下载组件包"
|
||||
if [ "$which" = "clashoo" ] || ! package_installed clashoo; then
|
||||
ensure_root_space 98304
|
||||
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
|
||||
apk_core_file="$TMP_DIR/core.${EXT}"
|
||||
fi
|
||||
download_package_file "$LUCI_URL" "$TMP_DIR/luci.${EXT}" "luci-app-clashoo"
|
||||
if [ -n "$I18N_URL" ]; then
|
||||
download_package_file "$I18N_URL" "$TMP_DIR/i18n.${EXT}" "语言包"
|
||||
fi
|
||||
elif [ "$which" = "clashoo" ]; then
|
||||
log "正在下载 Clashoo 核心"
|
||||
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
|
||||
else
|
||||
log "正在下载客户端(LuCI + 语言包)"
|
||||
download_package_file "$LUCI_URL" "$TMP_DIR/luci.${EXT}" "luci-app-clashoo"
|
||||
if [ -n "$I18N_URL" ]; then
|
||||
download_package_file "$I18N_URL" "$TMP_DIR/i18n.${EXT}" "语言包"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$PM" != "apk" ] && [ "$which" = "clashoo" ]; then
|
||||
ensure_root_space 98304
|
||||
fi
|
||||
|
||||
if [ "$which" = "clashoo" ]; then
|
||||
log "正在安装 Clashoo 核心"
|
||||
if [ "$PM" = "opkg" ]; then
|
||||
opkg install --force-downgrade "$TMP_DIR/core.${EXT}" >"$TMP_DIR/install.log" 2>&1; rc=$?
|
||||
else
|
||||
apk_bundle_install ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
|
||||
fi
|
||||
else
|
||||
log "正在安装客户端"
|
||||
if [ "$PM" = "opkg" ]; then
|
||||
opkg install --force-downgrade "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
|
||||
else
|
||||
apk_bundle_install ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" ${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >"$TMP_DIR/install.log" 2>&1; rc=$?
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
if [ "$PM" = "apk" ] && apk_update_ready; then
|
||||
log "apk 报告了其它软件包的错误,本次组件已升级到目标版本"
|
||||
rc=0
|
||||
else
|
||||
log_install_error "$TMP_DIR/install.log"
|
||||
log "安装失败,正在恢复配置备份"
|
||||
restore_config_backup
|
||||
finish "$rc" "组件更新失败,已停止操作,请查看组件更新日志"
|
||||
fi
|
||||
fi
|
||||
|
||||
validation_failed() {
|
||||
log "安装校验失败,正在恢复配置备份"
|
||||
restore_config_backup
|
||||
finish 1 "组件安装校验失败,已停止操作,请查看组件更新日志"
|
||||
}
|
||||
|
||||
if [ "$PM" = "apk" ]; then
|
||||
if ! apk_update_ready; then
|
||||
log "安装校验失败,正在安全重装组件包"
|
||||
if [ -z "$apk_core_file" ] && ! package_installed clashoo; then
|
||||
ensure_root_space 98304
|
||||
download_package_file "$CORE_URL" "$TMP_DIR/core.${EXT}" "clashoo"
|
||||
apk_core_file="$TMP_DIR/core.${EXT}"
|
||||
fi
|
||||
apk_bundle_install --force-reinstall ${apk_core_file:+"$apk_core_file"} "$TMP_DIR/luci.${EXT}" \
|
||||
${I18N_URL:+"$TMP_DIR/i18n.${EXT}"} >>"$TMP_DIR/install.log" 2>&1; repair_rc=$?
|
||||
if [ "$repair_rc" -ne 0 ]; then
|
||||
log_install_error "$TMP_DIR/install.log"
|
||||
validation_failed
|
||||
fi
|
||||
apk_update_ready || validation_failed
|
||||
fi
|
||||
elif [ "$which" = "clashoo" ]; then
|
||||
package_installed clashoo && [ "$(installed_package_version clashoo)" = "$core_ver" ] \
|
||||
&& [ -x /etc/init.d/clashoo ] && [ -x /usr/share/clashoo/update/component_update.sh ] \
|
||||
|| validation_failed
|
||||
else
|
||||
package_installed luci-app-clashoo && [ "$(installed_package_version luci-app-clashoo)" = "$luci_ver" ] \
|
||||
&& [ -r /usr/share/luci/menu.d/luci-app-clashoo.json ] \
|
||||
&& [ -r /usr/share/rpcd/ucode/luci.clashoo ] \
|
||||
|| validation_failed
|
||||
[ -z "$I18N_URL" ] || package_installed luci-i18n-clashoo-zh-cn || validation_failed
|
||||
fi
|
||||
|
||||
# APK component updates install the LuCI bundle in the same transaction.
|
||||
if [ "$which" = "luci" ] || [ "$PM" = "apk" ]; then
|
||||
log "正在刷新 LuCI 服务"
|
||||
restart_web_stack
|
||||
fi
|
||||
core_ver_after="$(installed_package_version clashoo)"
|
||||
restart_clashoo=0
|
||||
[ "$which" = "clashoo" ] && restart_clashoo=1
|
||||
[ "$PM" = "apk" ] && [ "$core_ver_before" != "$core_ver_after" ] && restart_clashoo=1
|
||||
if [ "$restart_clashoo" -eq 1 ] && [ "$was_running" -eq 1 ] && [ "$(uci -q get clashoo.config.enable 2>/dev/null)" = "1" ]; then
|
||||
log "Clashoo 原本运行中,正在重启服务"
|
||||
sh /usr/share/clashoo/rpc/rpc_async.sh restart >/dev/null 2>&1 || /etc/init.d/clashoo restart >/dev/null 2>&1 || true
|
||||
elif [ "$restart_clashoo" -eq 1 ]; then
|
||||
log "Clashoo 原本未运行,保持停止状态"
|
||||
fi
|
||||
|
||||
finish 0 "组件更新完成"
|
||||
}
|
||||
|
||||
run_core_update() {
|
||||
dcore="$1"
|
||||
label="$2"
|
||||
log "正在更新 ${label}"
|
||||
# Pass the target as an arg so core_download refreshes only that binary
|
||||
# without switching the active kernel. It restarts only if the target is the
|
||||
# currently-running core (see core_download.sh finalize). Switching kernels
|
||||
# stays a separate, explicit user action.
|
||||
touch /var/run/core_update
|
||||
sh /usr/share/clashoo/update/core_download.sh "$dcore" >>/tmp/clash_update.txt 2>&1
|
||||
rc=$?
|
||||
rm -f /var/run/core_update
|
||||
[ "$rc" -eq 0 ] || finish "$rc" "${label} 更新失败"
|
||||
finish 0 "${label} 更新完成"
|
||||
}
|
||||
|
||||
run_lgbm_update() {
|
||||
log "正在更新 Smart LightGBM 模型"
|
||||
sh /usr/share/clashoo/update/lgbm_update.sh >/tmp/lgbm_update.log 2>&1
|
||||
rc=$?
|
||||
tail -5 /tmp/lgbm_update.log 2>/dev/null | while IFS= read -r line; do
|
||||
[ -n "$line" ] && log "$line"
|
||||
done
|
||||
[ "$rc" -eq 0 ] || finish "$rc" "LightGBM 模型更新失败"
|
||||
finish 0 "LightGBM 模型更新完成"
|
||||
}
|
||||
|
||||
run_china_update() {
|
||||
log "正在更新大陆白名单"
|
||||
sh /usr/share/clashoo/update/update_china_ip.sh >>/tmp/clash_update.txt 2>&1
|
||||
rc=$?
|
||||
tail -6 /tmp/clash_update.txt 2>/dev/null | while IFS= read -r line; do
|
||||
[ -n "$line" ] && printf '%s\n' "$line" | grep -q '白名单' && log "$line"
|
||||
done
|
||||
[ "$rc" -eq 0 ] || finish "$rc" "大陆白名单更新失败"
|
||||
finish 0 "大陆白名单更新完成"
|
||||
}
|
||||
|
||||
run_geoip_update() {
|
||||
log "正在更新 GeoIP / GeoSite"
|
||||
touch /var/run/geoip_update
|
||||
sh /usr/share/clashoo/update/geoip.sh >/tmp/geoip_update.txt 2>&1
|
||||
rc=$?
|
||||
rm -f /var/run/geoip_update
|
||||
tail -8 /tmp/geoip_update.txt 2>/dev/null | while IFS= read -r line; do
|
||||
[ -n "$line" ] && log "$line"
|
||||
done
|
||||
[ "$rc" -eq 0 ] || finish "$rc" "GeoIP / GeoSite 更新失败"
|
||||
finish 0 "GeoIP / GeoSite 更新完成"
|
||||
}
|
||||
|
||||
COMPONENT="${1:-}"
|
||||
VARIANT="${2:-}" # mihomo/singbox: stable | alpha
|
||||
[ -n "$COMPONENT" ] || {
|
||||
echo "usage: $0 <clashoo|luci|mihomo|singbox|smart|lgbm|china|geoip> [stable|alpha]"
|
||||
exit 2
|
||||
}
|
||||
|
||||
mkdir -p "$(dirname "$RUN_FILE")" >/dev/null 2>&1
|
||||
printf '%s\n' "$COMPONENT" >"$RUN_FILE"
|
||||
STATUS="running"
|
||||
log "组件更新任务启动:${COMPONENT}${VARIANT:+ ($VARIANT)}"
|
||||
|
||||
case "$COMPONENT" in
|
||||
clashoo) run_pkg_update clashoo ;;
|
||||
luci) run_pkg_update luci ;;
|
||||
mihomo)
|
||||
if [ "$VARIANT" = "alpha" ]; then run_core_update 3 "mihomo Alpha 版"
|
||||
else run_core_update 2 "mihomo 稳定版"; fi ;;
|
||||
singbox)
|
||||
if [ "$VARIANT" = "alpha" ]; then run_core_update 5 "sing-box Alpha 版"
|
||||
else run_core_update 4 "sing-box 稳定版"; fi ;;
|
||||
smart) run_core_update 1 "mihomo Smart 版" ;;
|
||||
lgbm) run_lgbm_update ;;
|
||||
china) run_china_update ;;
|
||||
geoip) run_geoip_update ;;
|
||||
*) finish 2 "未知组件:${COMPONENT}" ;;
|
||||
esac
|
||||
+1233
File diff suppressed because it is too large.
Load diff
+267
@@ -0,0 +1,267 @@
|
||||
#!/bin/sh
|
||||
|
||||
LOG_FILE="/tmp/geoip_update.txt"
|
||||
TMP_DIR="/tmp/clash_geoip_$$"
|
||||
MMDB_MIN_SIZE=2000000
|
||||
GEOSITE_MIN_SIZE=2000000
|
||||
|
||||
geoip_source="$(uci -q get clashoo.config.geoip_source 2>/dev/null)"
|
||||
license_key="$(uci -q get clashoo.config.license_key 2>/dev/null)"
|
||||
|
||||
cfg_mmdb_url="$(uci -q get clashoo.config.geoip_mmdb_url 2>/dev/null)"
|
||||
cfg_geosite_url="$(uci -q get clashoo.config.geosite_url 2>/dev/null)"
|
||||
cfg_geoip_dat_url="$(uci -q get clashoo.config.geoip_dat_url 2>/dev/null)"
|
||||
cfg_geoip_asn_url="$(uci -q get clashoo.config.geoip_asn_url 2>/dev/null)"
|
||||
|
||||
DEFAULT_MMDB_URL="https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
|
||||
DEFAULT_GEOSITE_URL="https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/release/geosite.dat"
|
||||
DEFAULT_GEOIP_DAT_URL="https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/release/geoip.dat"
|
||||
DEFAULT_GEOIP_ASN_URL="https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb"
|
||||
|
||||
LOYALSOLDIER_MMDB_URL="https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb"
|
||||
LOYALSOLDIER_GEOSITE_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat"
|
||||
LOYALSOLDIER_GEOIP_DAT_URL="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat"
|
||||
|
||||
# v2fly ships no Country.mmdb — borrow Loyalsoldier's for the mmdb slot
|
||||
V2FLY_GEOSITE_URL="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat"
|
||||
V2FLY_GEOIP_DAT_URL="https://github.com/v2fly/geoip/releases/latest/download/geoip.dat"
|
||||
|
||||
log() {
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR" >/dev/null 2>&1
|
||||
rm -f /var/run/geoip_update >/dev/null 2>&1
|
||||
}
|
||||
|
||||
config_needs_geosite() {
|
||||
_cfg="/etc/clashoo/config.yaml"
|
||||
[ -f "$_cfg" ] || return 1
|
||||
grep -Eq "^[[:space:]]*-[[:space:]]*[\"']?GEOSITE,|^[[:space:]]*geosite:[[:space:]]*|^[[:space:]]*'geosite:|^[[:space:]]*-[[:space:]]*[\"']?geosite:" "$_cfg"
|
||||
}
|
||||
|
||||
# Shared port detection (handles smart core + sing-box profile port); keep the
|
||||
# local anonymity probe so we skip a coexisting plugin's auth-locked proxy.
|
||||
. /usr/share/clashoo/update/proxy_lib.sh
|
||||
detect_proxy() {
|
||||
proxy="$(clashoo_detect_proxy)"
|
||||
[ -n "$proxy" ] || return 0
|
||||
# Only use it if it actually proxies anonymously. A coexisting plugin
|
||||
# (e.g. OpenClash on the same 7890) may hold this port behind auth and
|
||||
# answer HTTP 407 — in that case fall through to the mirrors instead.
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsS --proxy "$proxy" --connect-timeout 3 --max-time 5 -o /dev/null \
|
||||
http://www.gstatic.com/generate_204 >/dev/null 2>&1 || return 0
|
||||
fi
|
||||
echo "$proxy"
|
||||
}
|
||||
|
||||
_fetch() {
|
||||
url="$1"; target="$2"; proxy="$3"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "$proxy" ]; then
|
||||
curl --proxy "$proxy" -fsSL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$url" -o "$target"
|
||||
else
|
||||
curl -fsSL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$url" -o "$target"
|
||||
fi
|
||||
return $?
|
||||
fi
|
||||
wget -q --timeout=120 --no-check-certificate --user-agent="Clash/OpenWRT" "$url" -O "$target"
|
||||
}
|
||||
|
||||
# 多源拉取:本机代理 → 国内镜像(gh-proxy / ghfast / jsdelivr)→ 原 URL 兜底
|
||||
download_to() {
|
||||
url="$1"; target="$2"
|
||||
[ -z "$url" ] && return 1
|
||||
|
||||
proxy="$(detect_proxy)"
|
||||
|
||||
if [ -n "$proxy" ]; then
|
||||
_fetch "$url" "$target" "$proxy" && return 0
|
||||
fi
|
||||
|
||||
case "$url" in
|
||||
https://raw.githubusercontent.com/*)
|
||||
rest="${url#https://raw.githubusercontent.com/}"
|
||||
owner="${rest%%/*}"; r1="${rest#*/}"
|
||||
repo="${r1%%/*}"; r2="${r1#*/}"
|
||||
branch="${r2%%/*}"; path="${r2#*/}"
|
||||
for m in \
|
||||
"https://gh-proxy.com/${url}" \
|
||||
"https://ghfast.top/${url}" \
|
||||
"https://cdn.jsdelivr.net/gh/${owner}/${repo}@${branch}/${path}"; do
|
||||
_fetch "$m" "$target" "" && return 0
|
||||
done
|
||||
;;
|
||||
https://github.com/*)
|
||||
for m in \
|
||||
"https://gh-proxy.com/${url}" \
|
||||
"https://ghfast.top/${url}"; do
|
||||
_fetch "$m" "$target" "" && return 0
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
_fetch "$url" "$target" ""
|
||||
}
|
||||
|
||||
download_optional() {
|
||||
url="$1"
|
||||
target="$2"
|
||||
name="$3"
|
||||
tmp_target="${TMP_DIR}/$(basename "$target").tmp"
|
||||
if [ -z "$url" ]; then
|
||||
log "$name skip (url empty)"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
if download_to "$url" "$tmp_target"; then
|
||||
mv -f "$tmp_target" "$target" >/dev/null 2>&1 || return 1
|
||||
chmod 644 "$target" >/dev/null 2>&1
|
||||
log "$name updated"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
log "$name update failed"
|
||||
return 0
|
||||
}
|
||||
|
||||
download_geosite() {
|
||||
url="$1"
|
||||
tmp_target="${TMP_DIR}/GeoSite.dat.tmp"
|
||||
size=0
|
||||
if [ -z "$url" ]; then
|
||||
log "GeoSite.dat skip (url empty)"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
if ! download_to "$url" "$tmp_target"; then
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
log "GeoSite.dat update failed"
|
||||
return 0
|
||||
fi
|
||||
size=$(wc -c <"$tmp_target" 2>/dev/null)
|
||||
if [ -z "$size" ] || [ "$size" -lt "$GEOSITE_MIN_SIZE" ]; then
|
||||
log "GeoSite.dat download invalid or incomplete (${size:-0} bytes)"
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
return 0
|
||||
fi
|
||||
mv -f "$tmp_target" /etc/clashoo/GeoSite.dat >/dev/null 2>&1 || return 1
|
||||
cp -f /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1 || true
|
||||
chmod 644 /etc/clashoo/GeoSite.dat /etc/clashoo/geosite.dat >/dev/null 2>&1
|
||||
log "GeoSite.dat updated"
|
||||
return 0
|
||||
}
|
||||
|
||||
download_mmdb() {
|
||||
url="$1"
|
||||
tmp_target="${TMP_DIR}/Country.mmdb.tmp"
|
||||
size=0
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
if ! download_to "$url" "$tmp_target"; then
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
size=$(wc -c <"$tmp_target" 2>/dev/null)
|
||||
if [ -z "$size" ] || [ "$size" -lt "$MMDB_MIN_SIZE" ]; then
|
||||
log "Country.mmdb download invalid or incomplete (${size:-0} bytes)"
|
||||
rm -f "$tmp_target" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
mv -f "$tmp_target" /etc/clashoo/Country.mmdb >/dev/null 2>&1 || return 1
|
||||
chmod 644 /etc/clashoo/Country.mmdb >/dev/null 2>&1
|
||||
return 0
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
mkdir -p "$TMP_DIR" /etc/clashoo >/dev/null 2>&1
|
||||
|
||||
rm -f /var/run/geoip_down_complete >/dev/null 2>&1
|
||||
: > /var/run/geoip_update
|
||||
: > "$LOG_FILE"
|
||||
log "GeoIP 更新任务启动"
|
||||
|
||||
mmdb_url=""
|
||||
geosite_url=""
|
||||
geoip_dat_url=""
|
||||
geoip_asn_url=""
|
||||
|
||||
case "$geoip_source" in
|
||||
1)
|
||||
if [ -z "$license_key" ]; then
|
||||
log "MaxMind source selected but license key is empty"
|
||||
exit 1
|
||||
fi
|
||||
log "Updating Country.mmdb from MaxMind"
|
||||
if ! download_to "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${license_key}&suffix=tar.gz" "$TMP_DIR/geoip.tar.gz"; then
|
||||
log "MaxMind download failed"
|
||||
exit 1
|
||||
fi
|
||||
if ! tar zxf "$TMP_DIR/geoip.tar.gz" -C "$TMP_DIR" >/dev/null 2>&1; then
|
||||
log "MaxMind archive extract failed"
|
||||
exit 1
|
||||
fi
|
||||
mmdb_file="$(ls "$TMP_DIR"/GeoLite2-Country_*/GeoLite2-Country.mmdb 2>/dev/null | head -n 1)"
|
||||
if [ -z "$mmdb_file" ] || [ ! -f "$mmdb_file" ]; then
|
||||
log "MaxMind Country.mmdb not found in archive"
|
||||
exit 1
|
||||
fi
|
||||
cp -f "$mmdb_file" /etc/clashoo/Country.mmdb
|
||||
chmod 644 /etc/clashoo/Country.mmdb >/dev/null 2>&1
|
||||
log "Country.mmdb updated"
|
||||
;;
|
||||
3)
|
||||
# Loyalsoldier (default)
|
||||
mmdb_url="$LOYALSOLDIER_MMDB_URL"
|
||||
geosite_url="$LOYALSOLDIER_GEOSITE_URL"
|
||||
geoip_dat_url="$LOYALSOLDIER_GEOIP_DAT_URL"
|
||||
geoip_asn_url="$DEFAULT_GEOIP_ASN_URL"
|
||||
;;
|
||||
5)
|
||||
# v2fly (mmdb borrowed from Loyalsoldier)
|
||||
mmdb_url="$LOYALSOLDIER_MMDB_URL"
|
||||
geosite_url="$V2FLY_GEOSITE_URL"
|
||||
geoip_dat_url="$V2FLY_GEOIP_DAT_URL"
|
||||
geoip_asn_url="$DEFAULT_GEOIP_ASN_URL"
|
||||
;;
|
||||
4)
|
||||
# custom — asn falls back to default when left blank
|
||||
mmdb_url="$cfg_mmdb_url"
|
||||
geosite_url="$cfg_geosite_url"
|
||||
geoip_dat_url="$cfg_geoip_dat_url"
|
||||
geoip_asn_url="${cfg_geoip_asn_url:-$DEFAULT_GEOIP_ASN_URL}"
|
||||
;;
|
||||
*)
|
||||
mmdb_url="${cfg_mmdb_url:-$DEFAULT_MMDB_URL}"
|
||||
geosite_url="${cfg_geosite_url:-$DEFAULT_GEOSITE_URL}"
|
||||
geoip_dat_url="${cfg_geoip_dat_url:-$DEFAULT_GEOIP_DAT_URL}"
|
||||
geoip_asn_url="${cfg_geoip_asn_url:-$DEFAULT_GEOIP_ASN_URL}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$geoip_source" != "1" ]; then
|
||||
log "Updating Country.mmdb"
|
||||
if ! download_mmdb "$mmdb_url"; then
|
||||
log "Country.mmdb download failed"
|
||||
exit 1
|
||||
fi
|
||||
log "Country.mmdb updated"
|
||||
|
||||
if config_needs_geosite; then
|
||||
download_geosite "$geosite_url"
|
||||
else
|
||||
log "GeoSite.dat skip (no geosite reference in config)"
|
||||
fi
|
||||
download_optional "$geoip_dat_url" /etc/clashoo/geoip.dat "geoip.dat"
|
||||
download_optional "$geoip_asn_url" /etc/clashoo/GeoLite2-ASN.mmdb "GeoLite2-ASN.mmdb"
|
||||
fi
|
||||
|
||||
touch /var/run/geoip_down_complete >/dev/null 2>&1
|
||||
|
||||
if pidof mihomo >/dev/null 2>&1 || pidof clash-meta >/dev/null 2>&1; then
|
||||
log "GeoIP update completed, apply on next Clashoo restart"
|
||||
fi
|
||||
|
||||
log "GeoIP update completed"
|
||||
exit 0
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
#!/bin/sh
|
||||
|
||||
MODEL_PATH="/etc/clashoo/Model.bin"
|
||||
TMP_PATH="/tmp/clash_Model.bin"
|
||||
LOG_FILE="/tmp/lgbm_update.log"
|
||||
|
||||
log() {
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
: > "$LOG_FILE"
|
||||
|
||||
DOWNLOAD_URL=$(uci get clashoo.config.smart_lgbm_url 2>/dev/null)
|
||||
[ -z "$DOWNLOAD_URL" ] && DOWNLOAD_URL="https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin"
|
||||
|
||||
log "Start downloading LightGBM model from: $DOWNLOAD_URL"
|
||||
|
||||
mkdir -p /etc/clashoo
|
||||
|
||||
rm -f "$TMP_PATH" 2>/dev/null
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fL --connect-timeout 15 --max-time 120 -A "Clash/OpenWRT" "$DOWNLOAD_URL" -o "$TMP_PATH" 2>/dev/null
|
||||
dl_rc=$?
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q --timeout=30 --tries=2 --no-check-certificate -U "Clash/OpenWRT" "$DOWNLOAD_URL" -O "$TMP_PATH" 2>/dev/null
|
||||
dl_rc=$?
|
||||
else
|
||||
log "No curl or wget found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$dl_rc" -ne 0 ] || [ ! -s "$TMP_PATH" ]; then
|
||||
log "Download failed (rc=$dl_rc)"
|
||||
rm -f "$TMP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$MODEL_PATH" ] && cmp -s "$TMP_PATH" "$MODEL_PATH"; then
|
||||
log "Model unchanged, no update needed"
|
||||
rm -f "$TMP_PATH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mv "$TMP_PATH" "$MODEL_PATH" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log "Failed to install model to $MODEL_PATH"
|
||||
rm -f "$TMP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "LightGBM model updated successfully"
|
||||
exit 0
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
|
||||
STATE_DIR="${CLASHOO_STATE_DIR:-/usr/share/clashbackup}"
|
||||
UPDATE_SCRIPT="${CLASHOO_UPDATE_SCRIPT:-/usr/share/clashoo/update/update_all.sh}"
|
||||
LOG_FILE="${CLASHOO_LOG_FILE:-/usr/share/clashoo/clashoo.txt}"
|
||||
LOCK_DIR="${CLASHOO_LOCK_DIR:-/tmp/clashoo_maintenance.lock}"
|
||||
NOW="${CLASHOO_NOW:-$(date +%s)}"
|
||||
|
||||
stamp() {
|
||||
tmp="$STATE_DIR/$1.tmp.$$"
|
||||
printf 'last_run=%s\n' "$NOW" >"$tmp" && mv -f "$tmp" "$STATE_DIR/$1"
|
||||
}
|
||||
|
||||
due() {
|
||||
hours="$(uci -q get "clashoo.config.$2")"
|
||||
echo "$hours" | grep -Eq '^[0-9]+$' && [ "$hours" -ge 1 ] || hours=12
|
||||
last="$(sed -n 's/^last_run=//p' "$STATE_DIR/$1" 2>/dev/null | head -1)"
|
||||
if ! echo "$last" | grep -Eq '^[0-9]+$' || [ "$last" -gt "$NOW" ]; then
|
||||
stamp "$1"
|
||||
return 1
|
||||
fi
|
||||
[ $((NOW - last)) -ge $((hours * 3600)) ]
|
||||
}
|
||||
|
||||
mkdir -p "$STATE_DIR" || exit 0
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
|
||||
trap 'rmdir "$LOCK_DIR" >/dev/null 2>&1' EXIT INT TERM
|
||||
|
||||
if [ "$(uci -q get clashoo.config.auto_update)" = "1" ] && due rule_update.status auto_update_time; then
|
||||
CLASHOO_STATE_DIR="$STATE_DIR" sh "$UPDATE_SCRIPT" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [ "$(uci -q get clashoo.config.auto_clear_log)" = "1" ] && due log_cleanup.status clear_time; then
|
||||
: >"$LOG_FILE"
|
||||
stamp log_cleanup.status
|
||||
fi
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
#!/bin/sh
|
||||
|
||||
REAL_LOG="/usr/share/clashoo/clashoo_real.txt"
|
||||
panel="$1"
|
||||
lang="$(uci -q get luci.main.lang 2>/dev/null)"
|
||||
STATE_FILE="/tmp/clash_panel_download_state"
|
||||
RUN_FILE="/var/run/panel_downloading"
|
||||
DASHBOARD_LINK="/etc/clashoo/dashboard"
|
||||
|
||||
[ -n "$panel" ] || panel="$(uci -q get clashoo.config.dashboard_panel 2>/dev/null)"
|
||||
[ -n "$panel" ] || panel="zashboard"
|
||||
|
||||
case "$panel" in
|
||||
metacubexd)
|
||||
URLS="https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip"
|
||||
TARGET_DIR="/etc/clashoo/dashboard-metacubexd"
|
||||
;;
|
||||
yacd)
|
||||
URLS="https://github.com/MetaCubeX/Yacd-meta/archive/refs/heads/gh-pages.zip https://github.com/haishanh/yacd/archive/refs/heads/gh-pages.zip"
|
||||
TARGET_DIR="/etc/clashoo/dashboard-yacd"
|
||||
;;
|
||||
zashboard)
|
||||
URLS="https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
TARGET_DIR="/etc/clashoo/dashboard-zashboard"
|
||||
;;
|
||||
razord)
|
||||
URLS="https://github.com/MetaCubeX/Razord-meta/archive/refs/heads/gh-pages.zip https://github.com/ayanamist/clash-dashboard/archive/refs/heads/gh-pages.zip"
|
||||
TARGET_DIR="/etc/clashoo/dashboard-razord"
|
||||
;;
|
||||
*)
|
||||
URLS="https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
TARGET_DIR="/etc/clashoo/dashboard-zashboard"
|
||||
panel="zashboard"
|
||||
;;
|
||||
esac
|
||||
|
||||
TMP_ROOT="/tmp/clash_panel_${panel}_$$"
|
||||
ZIP_FILE="$TMP_ROOT/panel.zip"
|
||||
UNPACK_DIR="$TMP_ROOT/unpack"
|
||||
|
||||
UPDATE_LOG="/tmp/clash_update.txt"
|
||||
|
||||
update_log() {
|
||||
[ -n "$1" ] || return 0
|
||||
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
|
||||
}
|
||||
|
||||
log_msg() {
|
||||
if [ "$lang" = "en" ]; then
|
||||
echo "$1" >"$REAL_LOG"
|
||||
[ "$3" = "1" ] || update_log "$1"
|
||||
else
|
||||
echo "$2" >"$REAL_LOG"
|
||||
[ "$3" = "1" ] || update_log "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
set_state() {
|
||||
state="$1"
|
||||
msg="$2"
|
||||
echo "${state}:${panel}:${msg}" >"$STATE_FILE"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_ROOT" >/dev/null 2>&1
|
||||
rm -f "$RUN_FILE" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Route the panel download through the running core in kernel-only mode
|
||||
# (shared logic in proxy_lib.sh). Normal mode returns empty -> TPROXY.
|
||||
. /usr/share/clashoo/update/proxy_lib.sh
|
||||
detect_proxy() { clashoo_detect_proxy; }
|
||||
|
||||
download_zip() {
|
||||
proxy="$(detect_proxy)"
|
||||
for url in $URLS; do
|
||||
if [ -n "$proxy" ] && command -v curl >/dev/null 2>&1 &&
|
||||
curl -fsSL --connect-timeout 15 --max-time 300 --retry 1 \
|
||||
--proxy "$proxy" -A "Clash/OpenWRT" "$url" -o "$ZIP_FILE"; then
|
||||
return 0
|
||||
fi
|
||||
if wget -q --timeout=60 --no-check-certificate --user-agent="Clash/OpenWRT" "$url" -O "$ZIP_FILE"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_zip() {
|
||||
if command -v unzip >/dev/null 2>&1; then
|
||||
unzip -oq "$ZIP_FILE" -d "$UNPACK_DIR" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
if command -v busybox >/dev/null 2>&1 && busybox --list 2>/dev/null | grep -qx "unzip"; then
|
||||
busybox unzip -oq "$ZIP_FILE" -d "$UNPACK_DIR" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
if command -v bsdtar >/dev/null 2>&1; then
|
||||
bsdtar -xf "$ZIP_FILE" -C "$UNPACK_DIR" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
find_web_root() {
|
||||
if [ -f "$UNPACK_DIR/index.html" ]; then
|
||||
echo "$UNPACK_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
index_file="$(find "$UNPACK_DIR" -type f -name index.html 2>/dev/null | head -n 1)"
|
||||
if [ -n "$index_file" ]; then
|
||||
dirname "$index_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
activate_panel() {
|
||||
rm -rf "$DASHBOARD_LINK" >/dev/null 2>&1
|
||||
ln -s "$TARGET_DIR" "$DASHBOARD_LINK" >/dev/null 2>&1 || cp -a "$TARGET_DIR" "$DASHBOARD_LINK" >/dev/null 2>&1
|
||||
rm -rf /www/luci-static/yacd >/dev/null 2>&1
|
||||
if [ "$panel" = "yacd" ]; then
|
||||
ln -s "$TARGET_DIR" /www/luci-static/yacd >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
mkdir -p "$UNPACK_DIR" "$TARGET_DIR" >/dev/null 2>&1
|
||||
touch "$RUN_FILE"
|
||||
set_state "downloading" "开始下载"
|
||||
|
||||
log_msg "Downloading dashboard panel..." "正在下载面板..."
|
||||
if ! download_zip; then
|
||||
set_state "error" "下载失败"
|
||||
log_msg "Dashboard panel download failed" "面板下载失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
extract_zip
|
||||
extract_rc=$?
|
||||
if [ "$extract_rc" -ne 0 ]; then
|
||||
if [ "$extract_rc" -eq 2 ]; then
|
||||
set_state "error" "缺少解压工具"
|
||||
log_msg "Panel unzip tool not found" "缺少解压工具,无法安装面板"
|
||||
else
|
||||
set_state "error" "解压失败"
|
||||
log_msg "Dashboard panel unzip failed" "面板解压失败"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SRC_DIR="$(find_web_root)"
|
||||
if [ -z "$SRC_DIR" ] || [ ! -f "$SRC_DIR/index.html" ]; then
|
||||
set_state "error" "文件无效"
|
||||
log_msg "Dashboard panel content invalid" "面板文件无效"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$TARGET_DIR"/* >/dev/null 2>&1
|
||||
cp -a "$SRC_DIR"/. "$TARGET_DIR"/ >/dev/null 2>&1
|
||||
|
||||
activate_panel
|
||||
|
||||
uci set clashoo.config.dashboard_panel="$panel" >/dev/null 2>&1
|
||||
uci commit clashoo >/dev/null 2>&1
|
||||
|
||||
set_state "success" "安装成功"
|
||||
log_msg "Dashboard panel installed" "面板安装完成"
|
||||
sleep 1
|
||||
log_msg "Clashoo" "Clashoo" 1
|
||||
exit 0
|
||||
@@ -0,0 +1,80 @@
|
||||
# shellcheck shell=sh
|
||||
# Clashoo's sing-box is private; HomeProxy owns the public system binary.
|
||||
CLASHOO_SINGBOX_BIN_DIR="/usr/share/clashoo/bin"
|
||||
CLASHOO_SINGBOX_RUNTIME_CONFIG="/var/run/clashoo/sing-box/config.json"
|
||||
|
||||
# Shared proxy detection for clashoo's own outbound requests.
|
||||
#
|
||||
# clashoo's self-initiated downloads (update checks, component/panel/core/geoip
|
||||
# downloads, subscriptions) and health checks must reach GitHub through the
|
||||
# running core, otherwise they leak out direct and stall behind the GFW. Route
|
||||
# them through the core's mixed port whenever a core is running — in BOTH modes:
|
||||
# - core-only mode deliberately skips TPROXY to coexist with other plugins;
|
||||
# - normal mode used to rely on transparent redirect, but the router's OWN
|
||||
# output redirect is fragile (e.g. PPPoE/GL.iNet stacks where Redirect to
|
||||
# loopback never completes — issue #25), so don't depend on it. The explicit
|
||||
# mixed port works on every network stack.
|
||||
# When no core is running (e.g. bootstrapping the very first core download),
|
||||
# returns empty so the caller falls back to direct / mirror sources.
|
||||
#
|
||||
# Port source depends on the running Clashoo kernel. The liveness gate uses the
|
||||
# procd owner instead of a binary name, because other proxy plugins may run the
|
||||
# same binaries:
|
||||
# sing-box -> private runtime config mixed inbound
|
||||
# (an imported profile may carry its own port)
|
||||
# mihomo / clash-meta / smart -> /etc/clashoo/config.yaml mixed-port
|
||||
# (a custom config's port may differ from uci)
|
||||
# uci mixed_port is the last-resort fallback for both.
|
||||
clashoo_running_core_type() {
|
||||
_cdp_type="$(uci -q get clashoo.config.core_type 2>/dev/null)"
|
||||
case "$_cdp_type" in
|
||||
singbox)
|
||||
_cdp_service="clashoo"
|
||||
_cdp_filter='@.clashoo.instances.singbox.pid'
|
||||
;;
|
||||
*)
|
||||
_cdp_type="mihomo"
|
||||
_cdp_service="clashoo"
|
||||
_cdp_filter='@.clashoo.instances.*.pid'
|
||||
;;
|
||||
esac
|
||||
|
||||
_cdp_pid="$(ubus call service list "{\"name\":\"$_cdp_service\",\"verbose\":true}" 2>/dev/null \
|
||||
| jsonfilter -e "$_cdp_filter" 2>/dev/null | head -n1)"
|
||||
[ -n "$_cdp_pid" ] && [ -d "/proc/$_cdp_pid" ] || return 1
|
||||
if [ "$_cdp_type" = "singbox" ]; then
|
||||
local _cdp_exe _cdp_cmdline
|
||||
_cdp_exe="$(readlink -f "/proc/$_cdp_pid/exe" 2>/dev/null)"
|
||||
case "$_cdp_exe" in
|
||||
"$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha") ;;
|
||||
*)
|
||||
_cdp_cmdline="$(tr '\000' ' ' < "/proc/$_cdp_pid/cmdline" 2>/dev/null)"
|
||||
case "$_cdp_cmdline" in
|
||||
"$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-stable "*|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha"|"$CLASHOO_SINGBOX_BIN_DIR/sing-box-alpha "*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
printf '%s' "$_cdp_type"
|
||||
}
|
||||
|
||||
clashoo_detect_proxy() {
|
||||
_cdp_type="$(clashoo_running_core_type)" || return 0
|
||||
_cdp_port=""
|
||||
if [ "$_cdp_type" = "singbox" ]; then
|
||||
_cdp_port="$(jsonfilter -i "$CLASHOO_SINGBOX_RUNTIME_CONFIG" \
|
||||
-e '@.inbounds[@.type="mixed"].listen_port' 2>/dev/null | head -n 1)"
|
||||
else
|
||||
# prefer mixed-port (HTTP+SOCKS) over plain port; a single two-pattern
|
||||
# sed would return whichever appears first by line order, so loop by key
|
||||
for _cdp_key in mixed-port port socks-port; do
|
||||
_cdp_port="$(sed -n "s/^[[:space:]]*${_cdp_key}:[[:space:]]*\([0-9][0-9]*\).*/\1/p" \
|
||||
/etc/clashoo/config.yaml 2>/dev/null | head -n 1)"
|
||||
[ -n "$_cdp_port" ] && break
|
||||
done
|
||||
fi
|
||||
[ -n "$_cdp_port" ] || _cdp_port="$(uci -q get clashoo.config.mixed_port 2>/dev/null)"
|
||||
[ -n "$_cdp_port" ] || return 0
|
||||
printf 'http://127.0.0.1:%s' "$_cdp_port"
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
#!/bin/sh
|
||||
|
||||
SUB_DIR="${CLASHOO_SUB_DIR:-/usr/share/clashoo/config/sub}"
|
||||
SINGBOX_DIR="${CLASHOO_SINGBOX_DIR:-/usr/share/clashoo/config/singbox}"
|
||||
BACKUP_DIR="${CLASHOO_BACKUP_DIR:-/usr/share/clashbackup}"
|
||||
TEMPLATE_DIR="${CLASHOO_TEMPLATE_DIR:-/usr/share/clashoo/config/custom}"
|
||||
LIST_FILE="${CLASHOO_LIST_FILE:-$BACKUP_DIR/confit_list.conf}"
|
||||
BIND_FILE="${CLASHOO_BIND_FILE:-$BACKUP_DIR/template_bindings.conf}"
|
||||
STATUS_FILE="${CLASHOO_STATUS_FILE:-$BACKUP_DIR/subscription_update.status}"
|
||||
LOCK_DIR="${CLASHOO_LOCK_DIR:-/tmp/clashoo_subscription_update.lock}"
|
||||
UPDATE_LOG="${CLASHOO_UPDATE_LOG:-/tmp/clash_update.txt}"
|
||||
SERVICE_CMD="${CLASHOO_SERVICE_CMD:-/etc/init.d/clashoo}"
|
||||
TMP_DIR="${CLASHOO_TMP_DIR:-/tmp}"
|
||||
DEFAULT_SUB_UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
|
||||
DOWNLOAD_ERROR=""
|
||||
|
||||
# Subscriptions are fetched direct first (many airports are domestic / geo-fence
|
||||
# foreign exit IPs). Only used as a last-resort fallback, see download_to.
|
||||
. /usr/share/clashoo/update/proxy_lib.sh
|
||||
|
||||
updated=0
|
||||
unchanged=0
|
||||
failed=0
|
||||
skipped=0
|
||||
restart_needed=0
|
||||
started_at="$(date +%s)"
|
||||
message=""
|
||||
record_status=0
|
||||
[ "$1" = "--all" ] && record_status=1
|
||||
|
||||
log_update() {
|
||||
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
|
||||
}
|
||||
|
||||
write_status() {
|
||||
local tmp
|
||||
mkdir -p "$(dirname "$STATUS_FILE")" >/dev/null 2>&1
|
||||
tmp="${STATUS_FILE}.tmp.$$"
|
||||
cat >"$tmp" <<EOF
|
||||
running=$1
|
||||
last_run=$started_at
|
||||
finished_at=$2
|
||||
updated=$updated
|
||||
unchanged=$unchanged
|
||||
failed=$failed
|
||||
skipped=$skipped
|
||||
message=$message
|
||||
EOF
|
||||
mv "$tmp" "$STATUS_FILE"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$LOCK_DIR" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
if ! mkdir "$LOCK_DIR" >/dev/null 2>&1; then
|
||||
exit 75
|
||||
fi
|
||||
trap cleanup EXIT INT TERM
|
||||
[ "$record_status" = "1" ] && write_status 1 0
|
||||
|
||||
safe_name() {
|
||||
case "$1" in
|
||||
''|*/*|*..*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
extract_host() {
|
||||
printf '%s' "$1" | sed -e 's#^[a-zA-Z0-9+.-]*://##' -e 's#/.*$##' -e 's#:.*$##' -e 's#.*@##'
|
||||
}
|
||||
|
||||
resolve_via() {
|
||||
local host dns
|
||||
host="$1"
|
||||
dns="$2"
|
||||
nslookup "$host" "$dns" 2>/dev/null | awk '
|
||||
/^Address/ {
|
||||
ip = $NF
|
||||
if (ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ &&
|
||||
ip !~ /^127\./ && ip !~ /^0\./ && ip !~ /^198\.18\./ &&
|
||||
ip != "8.8.8.8" && ip != "1.1.1.1" &&
|
||||
ip != "223.5.5.5" && ip != "119.29.29.29") {
|
||||
print ip
|
||||
exit
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
curl_download() {
|
||||
local url out hdr ua host ip
|
||||
url="$1"
|
||||
out="$2"
|
||||
hdr="$3"
|
||||
ua="$4"
|
||||
host="$5"
|
||||
ip="$6"
|
||||
if [ -n "$host" ] && [ -n "$ip" ]; then
|
||||
curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \
|
||||
--speed-limit 1 --retry 2 -A "$ua" -D "$hdr" -o "$out" \
|
||||
--resolve "$host:443:$ip" --resolve "$host:80:$ip" \
|
||||
-w '%{http_code}' "$url" 2>/dev/null
|
||||
else
|
||||
curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \
|
||||
--speed-limit 1 --retry 2 -A "$ua" -D "$hdr" -o "$out" \
|
||||
-w '%{http_code}' "$url" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
subscription_ua() {
|
||||
local target ua
|
||||
target="$1"
|
||||
ua=""
|
||||
[ -r "${target}.ua" ] && ua="$(sed -n '1p' "${target}.ua" 2>/dev/null)"
|
||||
[ -n "$ua" ] || ua="$(uci -q get clashoo.config.sub_ua 2>/dev/null)"
|
||||
[ -n "$ua" ] || ua="$DEFAULT_SUB_UA"
|
||||
printf '%s' "$ua"
|
||||
}
|
||||
|
||||
save_subscription_ua() {
|
||||
printf '%s\n' "$2" >"${1}.ua"
|
||||
}
|
||||
|
||||
curl_error() {
|
||||
case "$1" in
|
||||
6) printf '%s' "DNS 解析失败" ;;
|
||||
7) printf '%s' "连接失败" ;;
|
||||
28) printf '%s' "连接超时" ;;
|
||||
35) printf '%s' "TLS 握手失败" ;;
|
||||
60) printf '%s' "证书校验失败" ;;
|
||||
*) printf '网络错误(curl rc=%s)' "${1:-unknown}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
download_to() {
|
||||
local url out hdr ua code rc last_rc host dns ip proxy
|
||||
url="$1"
|
||||
out="$2"
|
||||
hdr="$3"
|
||||
ua="$4"
|
||||
[ -n "$ua" ] || ua="$DEFAULT_SUB_UA"
|
||||
DOWNLOAD_ERROR=""
|
||||
|
||||
rm -f "$out" "$hdr" >/dev/null 2>&1
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
code="$(curl_download "$url" "$out" "$hdr" "$ua" "" "")"
|
||||
rc=$?
|
||||
if [ "$rc" -eq 0 ] && [ "$code" = "200" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then
|
||||
DOWNLOAD_ERROR="HTTP ${code}"
|
||||
return 1
|
||||
fi
|
||||
last_rc="$rc"
|
||||
host="$(extract_host "$url")"
|
||||
for dns in 223.5.5.5 119.29.29.29 1.1.1.1 8.8.8.8; do
|
||||
ip="$(resolve_via "$host" "$dns")"
|
||||
[ -n "$ip" ] || continue
|
||||
log_update "DNS 回退:${host} -> ${ip} (@${dns})"
|
||||
code="$(curl_download "$url" "$out" "$hdr" "$ua" "$host" "$ip")"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] && [ "$code" = "200" ] && return 0
|
||||
if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then
|
||||
DOWNLOAD_ERROR="HTTP ${code}"
|
||||
return 1
|
||||
fi
|
||||
last_rc="$rc"
|
||||
done
|
||||
# Last resort: direct + DNS-override both failed, so the source may be
|
||||
# GFW-blocked (e.g. a github-hosted sub). In kernel-only mode try once
|
||||
# through the running core — nothing left to lose, and it won't disturb
|
||||
# any airport that already answered direct.
|
||||
proxy="$(clashoo_detect_proxy)"
|
||||
if [ -n "$proxy" ]; then
|
||||
log_update "代理兜底:通过本地核心重试订阅"
|
||||
code="$(curl -sSL --connect-timeout 15 --max-time 60 --speed-time 30 \
|
||||
--speed-limit 1 --retry 1 -A "$ua" -D "$hdr" -o "$out" \
|
||||
--proxy "$proxy" -w '%{http_code}' "$url" 2>/dev/null)"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] && [ "$code" = "200" ] && return 0
|
||||
if [ "$rc" -eq 0 ] && [ -n "$code" ] && [ "$code" != "000" ]; then
|
||||
DOWNLOAD_ERROR="HTTP ${code}"
|
||||
return 1
|
||||
fi
|
||||
last_rc="$rc"
|
||||
fi
|
||||
DOWNLOAD_ERROR="$(curl_error "$last_rc")"
|
||||
return 1
|
||||
fi
|
||||
wget -q --tries=4 --timeout=20 --user-agent="$ua" "$url" -O "$out"
|
||||
rc=$?
|
||||
[ "$rc" -eq 0 ] && return 0
|
||||
DOWNLOAD_ERROR="网络错误(wget rc=${rc})"
|
||||
return 1
|
||||
}
|
||||
|
||||
update_info() {
|
||||
local hdr target info
|
||||
hdr="$1"
|
||||
target="$2"
|
||||
info="$(grep -i 'subscription-userinfo:' "$hdr" 2>/dev/null | head -1 | \
|
||||
sed 's/^[Ss]ubscription-[Uu]serinfo:[[:space:]]*//' | tr -d '\r')"
|
||||
if [ -n "$info" ]; then
|
||||
printf '%s\n' "$info" >"${target}.info"
|
||||
else
|
||||
rm -f "${target}.info" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
service_running() {
|
||||
[ -n "${CLASHOO_SERVICE_CMD:-}" ] && return 0
|
||||
"$SERVICE_CMD" status >/dev/null 2>&1
|
||||
}
|
||||
|
||||
template_output_name() {
|
||||
local sub tpl
|
||||
sub="$(printf '%s' "$1" | sed -e 's/\.[Yy][Aa][Mm][Ll]$//' -e 's/\.[Yy][Mm][Ll]$//' -e 's/[^A-Za-z0-9._-]/-/g')"
|
||||
tpl="$(printf '%s' "$2" | sed -e 's/\.[Yy][Aa][Mm][Ll]$//' -e 's/\.[Yy][Mm][Ll]$//' -e 's/[^A-Za-z0-9._-]/-/g')"
|
||||
printf '_merged_%s__%s.yaml' "${sub:-sub}" "${tpl:-template}"
|
||||
}
|
||||
|
||||
apply_template() {
|
||||
local name target template merged merged_path use_config
|
||||
name="$1"
|
||||
target="$2"
|
||||
[ -r "$BIND_FILE" ] || return 0
|
||||
[ -x /usr/share/clashoo/update/template_merge.sh ] || return 0
|
||||
template="$(awk -F '#' -v n="$name" '$1==n && ($3=="1" || $3=="true") {print $2; exit}' "$BIND_FILE")"
|
||||
[ -n "$template" ] && [ -r "$TEMPLATE_DIR/$template" ] || return 0
|
||||
merged="$(template_output_name "$name" "$template")"
|
||||
merged_path="$TEMPLATE_DIR/$merged"
|
||||
if sh /usr/share/clashoo/update/template_merge.sh "$target" "$TEMPLATE_DIR/$template" "$merged_path" >/dev/null 2>&1; then
|
||||
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
|
||||
if [ "$use_config" = "$target" ] || [ "$use_config" = "$merged_path" ]; then
|
||||
uci -q set clashoo.config.use_config="$merged_path"
|
||||
uci -q set clashoo.config.config_type='3'
|
||||
uci -q commit clashoo >/dev/null 2>&1
|
||||
restart_needed=1
|
||||
fi
|
||||
else
|
||||
log_update "模板生成失败:${name} <- ${template}"
|
||||
fi
|
||||
}
|
||||
|
||||
update_mihomo() {
|
||||
local name url typ target tmp hdr ua use_config config_type
|
||||
name="$1"
|
||||
url="$2"
|
||||
typ="$3"
|
||||
safe_name "$name" || return 1
|
||||
case "$typ" in clash|meta) ;; *) skipped=$((skipped + 1)); return 0 ;; esac
|
||||
target="$SUB_DIR/$name"
|
||||
[ -f "$target" ] || { skipped=$((skipped + 1)); return 0; }
|
||||
tmp="$TMP_DIR/clashoo_sub_$$.yaml"
|
||||
hdr="$TMP_DIR/clashoo_sub_$$.hdr"
|
||||
ua="$(subscription_ua "$target")"
|
||||
if ! download_to "$url" "$tmp" "$hdr" "$ua"; then
|
||||
failed=$((failed + 1))
|
||||
log_update "更新失败(${DOWNLOAD_ERROR:-下载失败}):$name"
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 1
|
||||
fi
|
||||
if ! grep -Eq '^(proxies|proxy-providers):' "$tmp" 2>/dev/null; then
|
||||
failed=$((failed + 1))
|
||||
log_update "更新失败(无效 Mihomo 配置):$name"
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 1
|
||||
fi
|
||||
if cmp -s "$tmp" "$target"; then
|
||||
update_info "$hdr" "$target"
|
||||
save_subscription_ua "$target" "$ua"
|
||||
unchanged=$((unchanged + 1))
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 0
|
||||
fi
|
||||
if ! mv "$tmp" "$target"; then
|
||||
failed=$((failed + 1))
|
||||
rm -f "$hdr"
|
||||
return 1
|
||||
fi
|
||||
update_info "$hdr" "$target"
|
||||
save_subscription_ua "$target" "$ua"
|
||||
rm -f "$hdr"
|
||||
updated=$((updated + 1))
|
||||
use_config="$(uci -q get clashoo.config.use_config 2>/dev/null)"
|
||||
config_type="$(uci -q get clashoo.config.config_type 2>/dev/null)"
|
||||
[ "$config_type" = "1" ] && [ "$use_config" = "$target" ] && restart_needed=1
|
||||
apply_template "$name" "$target"
|
||||
log_update "更新完成:$name"
|
||||
}
|
||||
|
||||
valid_singbox() {
|
||||
ucode -e 'import { readfile } from "fs"; let c = json(readfile(ARGV[0])); exit(type(c) == "object" && type(c.outbounds) == "array" && length(c.outbounds) > 0 ? 0 : 1);' "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
update_singbox() {
|
||||
local name target url tmp hdr ua active
|
||||
name="$1"
|
||||
safe_name "$name" || return 1
|
||||
target="$SINGBOX_DIR/$name"
|
||||
[ -f "$target" ] && [ -r "$target.url" ] || { skipped=$((skipped + 1)); return 0; }
|
||||
url="$(sed -n '1p' "$target.url")"
|
||||
[ -n "$url" ] || { skipped=$((skipped + 1)); return 0; }
|
||||
tmp="$TMP_DIR/clashoo_sb_$$.json"
|
||||
hdr="$TMP_DIR/clashoo_sb_$$.hdr"
|
||||
ua="$(subscription_ua "$target")"
|
||||
if ! download_to "$url" "$tmp" "$hdr" "$ua"; then
|
||||
failed=$((failed + 1))
|
||||
log_update "更新失败(${DOWNLOAD_ERROR:-下载失败}):$name"
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 1
|
||||
fi
|
||||
if ! valid_singbox "$tmp"; then
|
||||
failed=$((failed + 1))
|
||||
log_update "更新失败(无效 sing-box 配置):$name"
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 1
|
||||
fi
|
||||
if cmp -s "$tmp" "$target"; then
|
||||
update_info "$hdr" "$target"
|
||||
save_subscription_ua "$target" "$ua"
|
||||
unchanged=$((unchanged + 1))
|
||||
rm -f "$tmp" "$hdr"
|
||||
return 0
|
||||
fi
|
||||
if ! mv "$tmp" "$target"; then
|
||||
failed=$((failed + 1))
|
||||
rm -f "$hdr"
|
||||
return 1
|
||||
fi
|
||||
update_info "$hdr" "$target"
|
||||
save_subscription_ua "$target" "$ua"
|
||||
rm -f "$hdr"
|
||||
updated=$((updated + 1))
|
||||
active="$(uci -q get clashoo.config.singbox_active 2>/dev/null)"
|
||||
[ "$active" = "$name" ] && restart_needed=1
|
||||
log_update "更新完成:$name"
|
||||
}
|
||||
|
||||
update_all() {
|
||||
local name url typ url_file
|
||||
if [ -r "$LIST_FILE" ]; then
|
||||
while IFS='#' read -r name url typ _rest; do
|
||||
[ -n "$name" ] && [ -n "$url" ] || continue
|
||||
update_mihomo "$name" "$url" "$typ" || true
|
||||
done <"$LIST_FILE"
|
||||
fi
|
||||
for url_file in "$SINGBOX_DIR"/*.url; do
|
||||
[ -f "$url_file" ] || continue
|
||||
name="$(basename "$url_file" .url)"
|
||||
update_singbox "$name" || true
|
||||
done
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
--all)
|
||||
update_all
|
||||
;;
|
||||
--mihomo)
|
||||
line="$(awk -F '#' -v n="$2" '$1==n {print; exit}' "$LIST_FILE" 2>/dev/null)"
|
||||
[ -n "$line" ] || { failed=1; message="subscription not found"; }
|
||||
if [ -n "$line" ]; then
|
||||
name="$(printf '%s' "$line" | awk -F '#' '{print $1}')"
|
||||
url="$(printf '%s' "$line" | awk -F '#' '{print $2}')"
|
||||
typ="$(printf '%s' "$line" | awk -F '#' '{print $3}')"
|
||||
update_mihomo "$name" "$url" "$typ" || true
|
||||
fi
|
||||
;;
|
||||
--singbox)
|
||||
update_singbox "$2" || true
|
||||
;;
|
||||
*)
|
||||
failed=1
|
||||
message="invalid arguments"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$restart_needed" = "1" ] && service_running; then
|
||||
"$SERVICE_CMD" restart >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
finished_at="$(date +%s)"
|
||||
[ -n "$message" ] || message="updated=$updated unchanged=$unchanged failed=$failed skipped=$skipped"
|
||||
[ "$record_status" = "1" ] && write_status 0 "$finished_at"
|
||||
[ "$failed" -eq 0 ]
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
STATUS_FILE="${CLASHOO_STATUS_FILE:-/usr/share/clashbackup/subscription_update.status}"
|
||||
UPDATER="${CLASHOO_SUBSCRIPTION_UPDATER:-/usr/share/clashoo/update/subscription_update.sh}"
|
||||
SERVICE_CMD="${CLASHOO_SERVICE_CMD:-/etc/init.d/clashoo}"
|
||||
interval="$(uci -q get clashoo.config.subscription_update_interval 2>/dev/null)"
|
||||
now="$(date +%s)"
|
||||
last=0
|
||||
|
||||
[ "$(uci -q get clashoo.config.auto_subscription_update 2>/dev/null)" = "1" ] || exit 0
|
||||
"$SERVICE_CMD" status >/dev/null 2>&1 || exit 0
|
||||
echo "$interval" | grep -Eq '^[0-9]+$' || interval=72
|
||||
[ "$interval" -gt 0 ] || interval=72
|
||||
[ -r "$STATUS_FILE" ] && last="$(sed -n 's/^last_run=//p' "$STATUS_FILE" | head -1)"
|
||||
echo "$last" | grep -Eq '^[0-9]+$' || last=0
|
||||
|
||||
[ $((now - last)) -ge $((interval * 3600)) ] || exit 0
|
||||
sh "$UPDATER" --all
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
SUB_FILE="${1:-}"
|
||||
TEMPLATE_FILE="${2:-}"
|
||||
OUT_FILE="${3:-}"
|
||||
TMP_FILE="/tmp/clash_template_merge_$$.yaml"
|
||||
TMP_FILE2="${TMP_FILE}.norm"
|
||||
LOG_FILE="/tmp/clash_update.txt"
|
||||
|
||||
log() {
|
||||
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$LOG_FILE"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_FILE" "$TMP_FILE2" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
[ -n "$SUB_FILE" ] || { log "模板复写失败:缺少订阅文件参数"; exit 1; }
|
||||
[ -n "$TEMPLATE_FILE" ] || { log "模板复写失败:缺少模板文件参数"; exit 1; }
|
||||
[ -n "$OUT_FILE" ] || { log "模板复写失败:缺少输出文件参数"; exit 1; }
|
||||
|
||||
[ -f "$SUB_FILE" ] || { log "模板复写失败:订阅文件不存在"; exit 1; }
|
||||
[ -f "$TEMPLATE_FILE" ] || { log "模板复写失败:模板文件不存在"; exit 1; }
|
||||
|
||||
if ! command -v yq >/dev/null 2>&1; then
|
||||
log "模板复写失败:缺少 yq"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
core_bin=""
|
||||
for b in mihomo clash-meta clash; do
|
||||
if command -v "$b" >/dev/null 2>&1; then
|
||||
core_bin="$b"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p "$(dirname "$OUT_FILE")" >/dev/null 2>&1
|
||||
|
||||
log "开始模板复写:$(basename "$SUB_FILE") <- $(basename "$TEMPLATE_FILE")"
|
||||
export SUB_FILE TEMPLATE_FILE
|
||||
|
||||
# template is base; load() forces single-doc; explode anchors first (yq 4.53+ breaks "<<")
|
||||
yq -n '
|
||||
(load(strenv(SUB_FILE)) | explode(.)) as $sub |
|
||||
(load(strenv(TEMPLATE_FILE)) | explode(.)) as $tpl |
|
||||
(
|
||||
($tpl * {
|
||||
"proxies": (((($tpl.proxies // []) + ($sub.proxies // [])) | unique_by(.name))),
|
||||
"proxy-providers": (($tpl."proxy-providers" // {}) * ($sub."proxy-providers" // {}))
|
||||
})
|
||||
| .dns = ((.dns // {}) * {
|
||||
"proxy-server-nameserver": (
|
||||
.dns."proxy-server-nameserver"
|
||||
// $sub.dns."proxy-server-nameserver"
|
||||
// $sub.dns.nameserver
|
||||
// ["223.5.5.5", "119.29.29.29"]
|
||||
)
|
||||
})
|
||||
)
|
||||
' >"$TMP_FILE" 2>/dev/null || {
|
||||
log "模板复写失败:YAML 合并错误"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 兼容第三方模板中使用“直连”字样,统一转换为 Clash 内置 DIRECT。
|
||||
yq e '
|
||||
(.. | select(tag == "!!str")) |= sub("^直连$", "DIRECT")
|
||||
| .rules = ((.rules // []) | map(sub(",直连,", ",DIRECT,") | sub(",直连$", ",DIRECT")))
|
||||
' "$TMP_FILE" >"$TMP_FILE2" 2>/dev/null || {
|
||||
log "模板复写失败:直连别名标准化失败"
|
||||
exit 1
|
||||
}
|
||||
mv -f "$TMP_FILE2" "$TMP_FILE" >/dev/null 2>&1 || {
|
||||
log "模板复写失败:中间文件写入失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
yq e '.' "$TMP_FILE" >/dev/null 2>&1 || {
|
||||
log "模板复写失败:合并结果 YAML 无效"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -n "$core_bin" ]; then
|
||||
"$core_bin" -t -f "$TMP_FILE" >/dev/null 2>&1 || {
|
||||
log "模板复写失败:内核校验不通过"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
mv -f "$TMP_FILE" "$OUT_FILE" >/dev/null 2>&1 || {
|
||||
log "模板复写失败:写入输出文件失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
chmod 644 "$OUT_FILE" >/dev/null 2>&1 || true
|
||||
log "模板复写完成:$(basename "$OUT_FILE")"
|
||||
exit 0
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
config_name="${1:-$(uci -q get clashoo.config.config_update_name 2>/dev/null)}"
|
||||
[ -n "$config_name" ] || exit 1
|
||||
exec sh /usr/share/clashoo/update/subscription_update.sh --mihomo "$config_name"
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
UPDATE_LOG="/tmp/clash_update.txt"
|
||||
STATE_DIR="${CLASHOO_STATE_DIR:-/usr/share/clashbackup}"
|
||||
|
||||
log_update() {
|
||||
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >>"$UPDATE_LOG"
|
||||
}
|
||||
|
||||
log_update "更新大陆白名单"
|
||||
sh /usr/share/clashoo/update/update_china_ip.sh >> "$UPDATE_LOG" 2>&1
|
||||
|
||||
log_update "更新 GeoIP / GeoSite"
|
||||
sh /usr/share/clashoo/update/geoip.sh >/dev/null 2>&1
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
tmp="$STATE_DIR/rule_update.status.tmp.$$"
|
||||
printf 'last_run=%s\n' "$(date +%s)" >"$tmp" && mv -f "$tmp" "$STATE_DIR/rule_update.status"
|
||||
|
||||
exit 0
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
LOG_FILE="${LOG_FILE:-/tmp/clash_update.txt}"
|
||||
NFT_DIR="${NFT_DIR:-/usr/share/clashoo/nftables}"
|
||||
TARGET_V4="${NFT_DIR}/geoip_cn.nft"
|
||||
TARGET_V6="${NFT_DIR}/geoip6_cn.nft"
|
||||
FW4_SCRIPT="${FW4_SCRIPT:-/usr/share/clashoo/net/fw4.sh}"
|
||||
PROC_ROOT="${PROC_ROOT:-/proc}"
|
||||
SINGBOX_BIN_DIR="${SINGBOX_BIN_DIR:-/usr/share/clashoo/bin}"
|
||||
TMP_V4="/tmp/china_ip.txt.$$"
|
||||
TMP_V6="/tmp/china_ipv6.txt.$$"
|
||||
OUT_V4="/tmp/geoip_cn.nft.$$"
|
||||
OUT_V6="/tmp/geoip6_cn.nft.$$"
|
||||
|
||||
log() {
|
||||
printf ' %s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
bool_enabled() {
|
||||
case "$1" in
|
||||
1|true|TRUE|yes|on) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
download_with_fallback() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
local ip
|
||||
|
||||
if curl -fsSL "$url" -o "$output"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$url" in
|
||||
https://ispip.clang.cn/*)
|
||||
for ip in 182.247.248.127 103.220.64.183; do
|
||||
if curl -fsSL --resolve "ispip.clang.cn:443:${ip}" "$url" -o "$output"; then
|
||||
log "DNS 异常,已使用 --resolve(${ip}) 回源下载"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_V4" "$TMP_V6" "$OUT_V4" "$OUT_V6"
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
render_nft_set() {
|
||||
local source_file="$1"
|
||||
local output_file="$2"
|
||||
local set_name="$3"
|
||||
local set_type="$4"
|
||||
|
||||
awk -v set_name="$set_name" -v set_type="$set_type" '
|
||||
BEGIN {
|
||||
print "set " set_name " {"
|
||||
print "\ttype " set_type ";"
|
||||
print "\tflags interval;"
|
||||
print "\tauto-merge;"
|
||||
print "\telements = {"
|
||||
first = 1
|
||||
}
|
||||
!/^[[:space:]]*$/ && !/^[[:space:]]*#/ {
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0)
|
||||
if ($0 == "")
|
||||
next
|
||||
if (!first)
|
||||
print ","
|
||||
printf "\t\t%s", $0
|
||||
first = 0
|
||||
}
|
||||
END {
|
||||
if (!first)
|
||||
print ""
|
||||
print "\t}"
|
||||
print "}"
|
||||
}
|
||||
' "$source_file" > "$output_file"
|
||||
}
|
||||
|
||||
url4="$(uci -q get clashoo.config.china_ip_url 2>/dev/null || true)"
|
||||
url6="$(uci -q get clashoo.config.china_ipv6_url 2>/dev/null || true)"
|
||||
bypass_china="$(uci -q get clashoo.config.bypass_china 2>/dev/null || true)"
|
||||
bypass_china_ipv6="$(uci -q get clashoo.config.bypass_china_ipv6 2>/dev/null || true)"
|
||||
[ -n "$bypass_china_ipv6" ] || bypass_china_ipv6="$bypass_china"
|
||||
|
||||
[ -n "$url4" ] || url4='https://ispip.clang.cn/all_cn.txt'
|
||||
[ -n "$url6" ] || url6='https://ispip.clang.cn/all_cn_ipv6.txt'
|
||||
|
||||
mkdir -p "$NFT_DIR"
|
||||
|
||||
log '开始更新大陆白名单'
|
||||
|
||||
download_with_fallback "$url4" "$TMP_V4"
|
||||
[ -s "$TMP_V4" ] || {
|
||||
log '大陆 IPv4 白名单下载失败:返回为空'
|
||||
exit 1
|
||||
}
|
||||
changed=0
|
||||
|
||||
render_nft_set "$TMP_V4" "$OUT_V4" clashoo_china ipv4_addr
|
||||
if cmp -s "$OUT_V4" "$TARGET_V4"; then
|
||||
log '大陆 IPv4 白名单内容无变化'
|
||||
else
|
||||
mv "$OUT_V4" "$TARGET_V4"
|
||||
chmod 644 "$TARGET_V4" >/dev/null 2>&1 || true
|
||||
changed=1
|
||||
log '大陆 IPv4 白名单更新完成'
|
||||
fi
|
||||
|
||||
if download_with_fallback "$url6" "$TMP_V6"; then
|
||||
if [ -s "$TMP_V6" ]; then
|
||||
render_nft_set "$TMP_V6" "$OUT_V6" clashoo_china6 ipv6_addr
|
||||
if cmp -s "$OUT_V6" "$TARGET_V6"; then
|
||||
log '大陆 IPv6 白名单内容无变化'
|
||||
else
|
||||
mv "$OUT_V6" "$TARGET_V6"
|
||||
chmod 600 "$TARGET_V6" >/dev/null 2>&1 || true
|
||||
changed=1
|
||||
log '大陆 IPv6 白名单更新完成'
|
||||
fi
|
||||
else
|
||||
log '大陆 IPv6 白名单下载为空,保留原文件'
|
||||
fi
|
||||
else
|
||||
log '大陆 IPv6 白名单下载失败,保留原文件'
|
||||
fi
|
||||
|
||||
instance_pid() {
|
||||
local service="$1"
|
||||
local instance="${2:-}"
|
||||
local filter
|
||||
if [ -n "$instance" ]; then
|
||||
filter="@.${service}.instances.${instance}.pid"
|
||||
else
|
||||
filter="@[\"${service}\"].instances.*.pid"
|
||||
fi
|
||||
ubus call service list "{\"name\":\"$service\",\"verbose\":true}" 2>/dev/null |
|
||||
jsonfilter -e "$filter" 2>/dev/null | head -n1
|
||||
}
|
||||
|
||||
pid_running() {
|
||||
[ -n "$1" ] && [ -d "$PROC_ROOT/$1" ]
|
||||
}
|
||||
|
||||
singbox_pid_owns_private_binary() {
|
||||
local pid="$1" exe cmdline
|
||||
pid_running "$pid" || return 1
|
||||
exe="$(readlink -f "$PROC_ROOT/$pid/exe" 2>/dev/null)"
|
||||
case "$exe" in
|
||||
"$SINGBOX_BIN_DIR/sing-box-stable"|"$SINGBOX_BIN_DIR/sing-box-alpha") return 0 ;;
|
||||
esac
|
||||
cmdline="$(tr '\000' ' ' < "$PROC_ROOT/$pid/cmdline" 2>/dev/null)"
|
||||
case "$cmdline" in
|
||||
"$SINGBOX_BIN_DIR/sing-box-stable"|"$SINGBOX_BIN_DIR/sing-box-stable "*|"$SINGBOX_BIN_DIR/sing-box-alpha"|"$SINGBOX_BIN_DIR/sing-box-alpha "*) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
selected_core_running() {
|
||||
local name pid
|
||||
if [ "$(uci -q get clashoo.config.core_type 2>/dev/null)" = "singbox" ]; then
|
||||
name="clashoo"
|
||||
pid="$(instance_pid "$name" singbox)"
|
||||
singbox_pid_owns_private_binary "$pid"
|
||||
return $?
|
||||
else
|
||||
name="clashoo"
|
||||
fi
|
||||
pid="$(instance_pid "$name")"
|
||||
pid_running "$pid"
|
||||
}
|
||||
|
||||
firewall_managed() {
|
||||
bool_enabled "$(uci -q get clashoo.config.enable 2>/dev/null)" || return 1
|
||||
[ "$(uci -q get clashoo.config.core_only 2>/dev/null)" = "1" ] && return 1
|
||||
selected_core_running
|
||||
}
|
||||
|
||||
if [ "$changed" -eq 0 ]; then
|
||||
log '白名单内容未变,跳过防火墙重载'
|
||||
elif ! firewall_managed; then
|
||||
log '未接管防火墙(服务未运行或仅内核模式),仅更新白名单文件'
|
||||
elif bool_enabled "$bypass_china" || bool_enabled "$bypass_china_ipv6"; then
|
||||
if "$FW4_SCRIPT" apply >/dev/null 2>&1; then
|
||||
log "大陆白名单规则已重载(IPv4=${bypass_china:-0},IPv6=${bypass_china_ipv6:-0})"
|
||||
else
|
||||
log '大陆白名单规则重载失败'
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log '大陆 IPv4/IPv6 绕过均未启用,仅更新白名单文件'
|
||||
fi
|
||||
|
||||
log '大陆白名单更新流程完成'
|
||||
@@ -0,0 +1,91 @@
|
||||
--- a/dns/middleware.go
|
||||
+++ b/dns/middleware.go
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
icontext "github.com/metacubex/mihomo/context"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
|
||||
+ M "github.com/metacubex/sing/common/metadata"
|
||||
D "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
@@ -152,6 +153,9 @@ func withFakeIP(skipper *fakeip.Skipper,
|
||||
q := r.Question[0]
|
||||
|
||||
host := strings.TrimRight(q.Name, ".")
|
||||
+ if !M.IsDomainName(host) {
|
||||
+ return next(ctx, r)
|
||||
+ }
|
||||
if skipper.ShouldSkipped(host) {
|
||||
return next(ctx, r)
|
||||
}
|
||||
--- /dev/null
|
||||
+++ b/dns/middleware_test.go
|
||||
@@ -0,0 +1,68 @@
|
||||
+package dns
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "net/netip"
|
||||
+ "testing"
|
||||
+
|
||||
+ "github.com/metacubex/mihomo/component/fakeip"
|
||||
+ icontext "github.com/metacubex/mihomo/context"
|
||||
+
|
||||
+ D "github.com/miekg/dns"
|
||||
+ "github.com/stretchr/testify/require"
|
||||
+)
|
||||
+
|
||||
+func TestWithFakeIPSkipsInvalidDomain(t *testing.T) {
|
||||
+ pool, err := fakeip.New(fakeip.Options{
|
||||
+ IPNet: netip.MustParsePrefix("198.18.0.1/16"),
|
||||
+ Size: 10,
|
||||
+ })
|
||||
+ require.NoError(t, err)
|
||||
+
|
||||
+ downstreamCalled := false
|
||||
+ next := func(_ *icontext.DNSContext, request *D.Msg) (*D.Msg, error) {
|
||||
+ downstreamCalled = true
|
||||
+ response := new(D.Msg)
|
||||
+ response.SetReply(request)
|
||||
+ return response, nil
|
||||
+ }
|
||||
+
|
||||
+ request := new(D.Msg)
|
||||
+ request.SetQuestion("n-relay-ipc-txc-nj-00.tplinkcloud.com.cn\\152.", D.TypeA)
|
||||
+ response, err := withFakeIP(&fakeip.Skipper{}, pool, nil, 1)(next)(icontext.NewDNSContext(context.Background()), request)
|
||||
+
|
||||
+ require.NoError(t, err)
|
||||
+ require.True(t, downstreamCalled)
|
||||
+ require.Empty(t, response.Answer)
|
||||
+ _, mapped := pool.LookBack(netip.MustParseAddr("198.18.0.4"))
|
||||
+ require.False(t, mapped)
|
||||
+}
|
||||
+
|
||||
+func TestWithFakeIPKeepsValidDomain(t *testing.T) {
|
||||
+ pool, err := fakeip.New(fakeip.Options{
|
||||
+ IPNet: netip.MustParsePrefix("198.18.0.1/16"),
|
||||
+ Size: 10,
|
||||
+ })
|
||||
+ require.NoError(t, err)
|
||||
+
|
||||
+ downstreamCalled := false
|
||||
+ next := func(_ *icontext.DNSContext, request *D.Msg) (*D.Msg, error) {
|
||||
+ downstreamCalled = true
|
||||
+ response := new(D.Msg)
|
||||
+ response.SetReply(request)
|
||||
+ return response, nil
|
||||
+ }
|
||||
+
|
||||
+ request := new(D.Msg)
|
||||
+ request.SetQuestion("n-relay-ipc-txc-nj-00.tplinkcloud.com.cn.", D.TypeA)
|
||||
+ dnsCtx := icontext.NewDNSContext(context.Background())
|
||||
+ response, err := withFakeIP(&fakeip.Skipper{}, pool, nil, 1)(next)(dnsCtx, request)
|
||||
+
|
||||
+ require.NoError(t, err)
|
||||
+ require.False(t, downstreamCalled)
|
||||
+ require.Len(t, response.Answer, 1)
|
||||
+ require.Equal(t, icontext.DNSTypeFakeIP, dnsCtx.Type())
|
||||
+ host, mapped := pool.LookBack(netip.MustParseAddr("198.18.0.4"))
|
||||
+ require.True(t, mapped)
|
||||
+ require.Equal(t, "n-relay-ipc-txc-nj-00.tplinkcloud.com.cn", host)
|
||||
+}
|
||||
Loaded 100 of 4756 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user