update 2025-07-18 00:58:03

This commit is contained in:
actions-user 2025-07-18 00:58:03 +08:00
parent f3474bab37
commit f62bd618f5
779 changed files with 619752 additions and 0 deletions

93
chinadns-ng/Makefile Normal file
View File

@ -0,0 +1,93 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=chinadns-ng
PKG_VERSION:=2025.06.20
PKG_RELEASE:=1
ifeq ($(ARCH),aarch64)
ifeq ($(BOARD),rockchip)
PKG_ARCH:=chinadns-ng+wolfssl@aarch64-linux-musl@generic+v8a@fast+lto
PKG_HASH:=224494b4505ac8dd347a70b298b853b677c0e247887d6edcf46b08bb153de017
else
PKG_ARCH:=chinadns-ng+wolfssl_noasm@aarch64-linux-musl@generic+v8a@fast+lto
PKG_HASH:=7639fae8e77ea36e377352179acb19e2fdf9010c2c373a9e02f03f637e99140c
endif
else ifeq ($(ARCH),arm)
ifeq ($(CONFIG_arm_v6),y)
PKG_ARCH:=chinadns-ng+wolfssl@arm-linux-musleabi@generic+v6+soft_float@fast+lto
PKG_HASH:=00fe09cf6309377a4bc90f3a5b909c5266c63181cc268912b4e32f92f58ec6cd
else ifeq ($(CONFIG_arm_v7),y)
ifeq ($(CONFIG_HAS_FPU),y)
PKG_ARCH:=chinadns-ng+wolfssl@arm-linux-musleabihf@generic+v7a@fast+lto
PKG_HASH:=1acd9129d7efd99bf9952ac6dd932e10108fb27a2a059f03e80bf520300b3741
else
PKG_ARCH:=chinadns-ng+wolfssl@arm-linux-musleabi@generic+v6+soft_float@fast+lto
PKG_HASH:=00fe09cf6309377a4bc90f3a5b909c5266c63181cc268912b4e32f92f58ec6cd
endif
else
PKG_ARCH:=chinadns-ng+wolfssl@arm-linux-musleabi@generic+v5te+soft_float@fast+lto
PKG_HASH:=01b5209a662824bd1c7586dccd95c9999e703cbb4704c623211e09c98becdce4
endif
else ifeq ($(ARCH),mips)
PKG_ARCH:=chinadns-ng+wolfssl@mips-linux-musl@mips32+soft_float@fast+lto
PKG_HASH:=fa468c04e58b47d7f6bb893ed6bdc41f9929d5a2e500c768e82e0f4a459b047c
else ifeq ($(ARCH),mipsel)
ifeq ($(CONFIG_HAS_FPU),)
PKG_ARCH:=chinadns-ng+wolfssl@mipsel-linux-musl@mips32+soft_float@fast+lto
PKG_HASH:=294917a5009a315cf5ad04cdda8f66714c60c928e2186a6846323e04aa4a798f
else
PKG_ARCH:=chinadns-ng+wolfssl@mipsel-linux-musl@mips32@fast+lto
PKG_HASH:=23c40248c3aa301a26c37cdb66e2141d2d73d4f0c5c03c066324263510d959ac
endif
else ifeq ($(ARCH),mips64)
PKG_ARCH:=chinadns-ng+wolfssl@mips64-linux-musl@mips64+soft_float@fast+lto
PKG_HASH:=f65bf9e495384afaf8aadd35395a2b9cdb2eedecb2602a0c522039dc4707350d
else ifeq ($(ARCH),mips64el)
PKG_ARCH:=chinadns-ng+wolfssl@mips64el-linux-musl@mips64+soft_float@fast+lto
PKG_HASH:=44bd1a96d5a0ce02b8c5c5d941339958af2bf2d1c5e5d90ed87214ac0d900543
else ifeq ($(ARCH),i386)
ifneq ($(CONFIG_TARGET_x86_geode)$(CONFIG_TARGET_x86_legacy),)
PKG_ARCH:=chinadns-ng+wolfssl@i386-linux-musl@i686@fast+lto
PKG_HASH:=25a215e1b5c18f8b4f0e7f0a0925fd1c959bbfdfeb3383830d2b645a94077fc6
else
PKG_ARCH:=chinadns-ng+wolfssl@i386-linux-musl@pentium4@fast+lto
PKG_HASH:=e78feedf8fafc60749dfca576acd05b3df8e4b99912aa77a320971011ca77b1d
endif
else ifeq ($(ARCH),x86_64)
PKG_ARCH:=chinadns-ng+wolfssl@x86_64-linux-musl@x86_64@fast+lto
PKG_HASH:=9ad100ef18f7acaecad1aaa07b07029e9e50679fc8f119977878211b1ceb96f0
else ifeq ($(ARCH),riscv64)
PKG_ARCH:=chinadns-ng+wolfssl@riscv64-linux-musl@baseline_rv64@fast+lto
PKG_HASH:=ed4d2cf72c2d48f19dd7678f419ce726bc2a793afc62afd6345e735dd262ca96
else
PKG_HASH:=dummy
endif
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(ARCH_PACKAGES)
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||mips64el||x86_64||riscv64) +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))

160
daed/Makefile Normal file
View File

@ -0,0 +1,160 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2023 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=daed
PKG_VERSION:=2025.07.12
DAED_VERSION:=daed-c3588a9
WING_VERSION:=wing-6df3da2
CORE_VERSION:=core-344666b
WING_HASH_SHORT:=$(shell echo $(WING_VERSION) | cut -d- -f2)
CORE_HASH_SHORT:=$(shell echo $(CORE_VERSION) | cut -d- -f2)
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=c3588a904c932d1fc83ee51096761776003fc25c
PKG_SOURCE_URL:=https://github.com/daeuniverse/daed.git
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=AGPL-3.0-only MIT
PKG_LICENSE_FILES:=LICENSE wing/LICENSE
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_BUILD_DIR=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/wing
PKG_BUILD_DEPENDS:=golang/host bpf-headers node/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/daeuniverse/dae-wing
GO_PKG_LDFLAGS:= \
-s -w -X '$(GO_PKG)/db.AppDescription=$(PKG_NAME) is a integration solution of dae, API and UI.'
GO_PKG_LDFLAGS_X= \
$(GO_PKG)/db.AppName=$(PKG_NAME) \
$(GO_PKG)/db.AppVersion=$(DAED_VERSION)_$(WING_VERSION)_$(CORE_VERSION)
GO_PKG_TAGS:=embedallowed
GO_PKG_TARGET_VARS:=$(filter-out CGO_ENABLED=%,$(GO_PKG_TARGET_VARS)) \
CGO_ENABLED=1 \
GOEXPERIMENT=greenteagc
include $(INCLUDE_DIR)/package.mk
include $(INCLUDE_DIR)/bpf.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
GO_PKG_DEFAULT_LDFLAGS:=-w -s -extldflags "-static"
TAR_CMD=$(HOST_TAR) -C $(BUILD_DIR)/ $(TAR_OPTIONS)
define Package/daed/Default
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
URL:=https://github.com/daeuniverse/daed
endef
define Package/daed
$(call Package/daed/Default)
TITLE:=A Modern Dashboard For dae
# You need enable KERNEL_DEBUG_INFO_BTF and KERNEL_BPF_EVENTS
DEPENDS:=$(GO_ARCH_DEPENDS) $(BPF_DEPENDS) \
+ca-bundle +kmod-sched-core +kmod-sched-bpf +kmod-xdp-sockets-diag \
+kmod-veth +v2ray-geoip +v2ray-geosite
endef
define Package/daed/description
daed is a backend of dae, provides a method to bundle arbitrary
frontend, dae and geodata into one binary.
endef
define Package/daed/conffiles
/etc/daed/wing.db
/etc/config/daed
endef
define Build/Prepare
( \
$(TAR) --strip-components=1 -C $(PKG_BUILD_DIR)/../ -xzf $(DL_DIR)/$(PKG_NAME)-$(PKG_VERSION).tar.gz ; \
rm -rf $(PKG_BUILD_DIR) && git clone https://github.com/daeuniverse/dae-wing $(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/wing && git -C $(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/wing checkout $(WING_HASH_SHORT) ; \
rm -rf $(PKG_BUILD_DIR)/dae-core && git clone https://github.com/daeuniverse/dae $(PKG_BUILD_DIR)/dae-core && git -C $(PKG_BUILD_DIR)/dae-core checkout $(CORE_HASH_SHORT) ; \
pushd $(PKG_BUILD_DIR)/dae-core ; \
git submodule update --init ; \
wget -qO - https://github.com/daeuniverse/dae/pull/763.patch | patch -p1 ; \
wget -qO - https://github.com/daeuniverse/dae/pull/839.patch | patch -p1 ; \
wget -qO - https://github.com/daeuniverse/dae/commit/56fb759.patch | patch -p1 ; \
wget -qO - https://github.com/daeuniverse/dae/commit/af2e2c6.patch | patch -p1 ; \
wget -qO - https://github.com/daeuniverse/dae/commit/d96dc26.patch | patch -p1 ; \
wget -qO - https://github.com/daeuniverse/dae/commit/6ff101c.patch | patch -p1 ; \
wget -qO - https://github.com/daeuniverse/dae/commit/5740b94.patch | patch -p1 ; \
wget -qO - https://github.com/daeuniverse/dae/commit/cec1642.patch | patch -p1 ; \
sed -i 's/Warn("failed to write cached DNS resp")/Debug("failed to write cached DNS resp")/g' control/dns_control.go ; \
sed -i 's/Warn("failed to send DNS response")/Debug("failed to send DNS response")/g' control/dns_control.go ; \
sed -i 's/Warn("failed to send DNS reject response")/Debug("failed to send DNS reject response")/g' control/dns_control.go ; \
go mod tidy ; \
popd ; \
pushd $(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/wing ; \
wget -qO - https://github.com/daeuniverse/dae-wing/commit/ca02961.patch | patch -p1 ; \
go mod tidy ; \
popd ; \
pushd $(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION) ; \
npm install -g pnpm ; \
pnpm install ; \
pnpm build ; \
popd ; \
mkdir -p $(PKG_BUILD_DIR)/webrender/web ; \
cp -rf $(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)/dist/* $(PKG_BUILD_DIR)/webrender/web ; \
find $(PKG_BUILD_DIR)/webrender/web -type f -size +4k ! -name "*.gz" ! -name "*.woff" ! -name "*.woff2" -exec sh -c '\
gzip -9 -k "{}"; \
if [ "$$$$(stat -c %s {})" -lt "$$$$(stat -c %s {}.gz)" ]; then \
rm {}.gz; \
else \
rm {}; \
fi' \
";" ; \
)
endef
DAE_CFLAGS:= \
-O2 -Wall -Werror \
-DMAX_MATCH_SET_LEN=1024 \
-D__UNROLL_ROUTE_LOOP \
-I$(BPF_HEADERS_DIR)/tools/lib \
-I$(BPF_HEADERS_DIR)/arch/$(BPF_KARCH)/include/asm/mach-generic
ifneq ($(CONFIG_USE_MUSL),)
TARGET_CFLAGS += -D_LARGEFILE64_SOURCE
endif
define Build/Compile
( \
pushd $(PKG_BUILD_DIR) ; \
$(MAKE) deps ; \
$(GO_GENERAL_BUILD_CONFIG_VARS) \
$(GO_PKG_BUILD_CONFIG_VARS) \
$(GO_PKG_BUILD_VARS); \
go generate ./... ; \
cd dae-core ; \
BPF_CLANG="$(CLANG)" \
BPF_STRIP_FLAG="-strip=$(LLVM_STRIP)" \
BPF_CFLAGS="$(DAE_CFLAGS)" \
BPF_TARGET="bpfel,bpfeb" \
go generate control/control.go ; \
popd ; \
$(call GoPackage/Build/Compile) ; \
)
endef
define Package/daed/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/dae-wing $(1)/usr/bin/daed
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(CURDIR)/files/daed.config $(1)/etc/config/daed
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/daed.init $(1)/etc/init.d/daed
endef
$(eval $(call GoBinPackage,daed))
$(eval $(call BuildPackage,daed))

7
daed/files/daed.config Normal file
View File

@ -0,0 +1,7 @@
config daed 'config'
option enabled '0'
option listen_addr '0.0.0.0:2023'
option log_maxbackups '1'
option log_maxsize '5'

46
daed/files/daed.init Executable file
View File

@ -0,0 +1,46 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2023 Tianling Shen <cnsztl@immortalwrt.org>
USE_PROCD=1
START=99
CONF="daed"
PROG="/usr/bin/daed"
LOG="/var/log/daed/daed.log"
start_service() {
config_load "$CONF"
local enabled
config_get_bool enabled "config" "enabled" "0"
[ "$enabled" -eq "1" ] || return 1
local listen_addr log_maxbackups log_maxsize
config_get listen_addr "config" "listen_addr" "0.0.0.0:2023"
config_get log_maxbackups "config" "log_maxbackups" "1"
config_get log_maxsize "config" "log_maxsize" "5"
procd_open_instance "$CONF"
procd_set_param command "$PROG" run
procd_append_param command --config "/etc/daed/"
procd_append_param command --listen "$listen_addr"
procd_append_param command --logfile "$LOG"
procd_append_param command --logfile-maxbackups "$log_maxbackups"
procd_append_param command --logfile-maxsize "$log_maxsize"
procd_set_param limits core="unlimited"
procd_set_param limits nofile="1000000 1000000"
procd_set_param respawn
# procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
stop_service() {
rm -f "$LOG"
}
service_triggers() {
procd_add_reload_trigger "$CONF"
}

55
dns2socks-rust/Makefile Normal file
View File

@ -0,0 +1,55 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2017-2024 Zxlhhyccc <zxlhhyccc@gmail.com>
# Copyright (C) 2021-2024 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=dns2socks-rust
PKG_VERSION:=0.2.0
PKG_RELEASE:=1
PKG_SOURCE_PROTO:=git
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/tun2proxy/dns2socks.git
PKG_SOURCE_DATE:=2025-03-19
PKG_SOURCE_VERSION:=5f5805bc5eba6530cec27f76860db1e19c1f2382
PKG_MIRROR_HASH:=f45ff9bff184f6eddbc444fc9a0611a47043e3b3422b7c33459bf7e03f37c37e
PKG_MAINTAINER:=Zxlhhyccc <zxlhhyccc@gmail.com>
PKG_LICENSE:=MIT
PKG_LICENSE_FILES:=LICENSE
PKG_BUILD_PARALLEL:=1
PKG_BUILD_DEPENDS:=rust/host
PKG_BUILD_PARALLEL:=1
#RUST_PKG:=dns2socks
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/rust/rust-package.mk
define Package/dns2socks-rust
SECTION:=net
CATEGORY:=Network
SUBMENU:=IP Addresses and Names
TITLE:=DNS forwards to SOCKS5 server
URL:=https://github.com/tun2proxy/dns2socks.git
DEPENDS:=$$(RUST_ARCH_DEPENDS)
endef
define Package/dns2socks-rust/description
This is a DNS server that forwards DNS requests to a SOCKS5 server.
endef
define Build/Compile
$(call Build/Compile/Cargo,,--all-features)
endef
define Package/dns2socks-rust/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/target/$(RUSTC_TARGET_ARCH)/release/dns2socks $(1)/usr/bin/dns2socks-rust
endef
$(eval $(call BuildPackage,dns2socks-rust))

53
dns2socks/Makefile Normal file
View File

@ -0,0 +1,53 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=dns2socks
PKG_VERSION:=2.1
PKG_RELEASE:=2
PKG_SOURCE:=SourceCode.zip
PKG_SOURCE_URL:=@SF/dns2socks
PKG_SOURCE_DATE:=2020-02-18
PKG_HASH:=406b5003523577d39da66767adfe54f7af9b701374363729386f32f6a3a995f4
PKG_MAINTAINER:=ghostmaker
PKG_LICENSE:=BSD-3-Clause
PKG_LICENSE_FILE:=LICENSE
include $(INCLUDE_DIR)/package.mk
UNZIP_CMD:=unzip -q -d $(PKG_BUILD_DIR) $(DL_DIR)/$(PKG_SOURCE)
define Package/dns2socks
SECTION:=net
CATEGORY:=Network
SUBMENU:=IP Addresses and Names
TITLE:=DNS to SOCKS or HTTP proxy
URL:=http://dns2socks.sourceforge.net/
DEPENDS:=+libpthread
endef
define Package/dns2socks/description
This is a command line utility to resolve DNS requests via
a SOCKS tunnel like Tor or a HTTP proxy.
endef
define Build/Compile
$(TARGET_CC) \
$(TARGET_CFLAGS) \
$(TARGET_CPPFLAGS) \
$(FPIC) \
-o $(PKG_BUILD_DIR)/DNS2SOCKS/dns2socks \
$(PKG_BUILD_DIR)/DNS2SOCKS/DNS2SOCKS.c \
$(TARGET_LDFLAGS) -pthread
endef
define Package/dns2socks/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/DNS2SOCKS/dns2socks $(1)/usr/bin/dns2socks
endef
$(eval $(call BuildPackage,dns2socks))

43
dns2tcp/Makefile Normal file
View File

@ -0,0 +1,43 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2022 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=dns2tcp
PKG_VERSION:=1.1.2
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/zfl9/dns2tcp/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=5e8c6302a1d32c16ae7d4b8e39cd9aad1f2d7e68fe18813e76cb1e48ec5940d2
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_LICENSE:=AGPL-3.0-only
PKG_LICENSE_FILES:=LICENSE
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
PKG_BUILD_FLAGS:=no-mips16
include $(INCLUDE_DIR)/package.mk
define Package/dns2tcp
SECTION:=net
CATEGORY:=Network
SUBMENU:=IP Addresses and Names
TITLE:=utility to convert dns query from udp to tcp
URL:=https://github.com/zfl9/dns2tcp
endef
TARGET_CFLAGS+= $(FPIC) -Wl,--gc-sections -flto
MAKE_FLAGS+= \
CFLAGS="-std=c99 $(TARGET_CFLAGS)" \
EVCFLAGS="$(TARGET_CFLAGS)"
define Package/dns2tcp/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/dns2tcp $(1)/usr/bin/
endef
$(eval $(call BuildPackage,dns2tcp))

48
geoview/Makefile Normal file
View File

@ -0,0 +1,48 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=geoview
PKG_VERSION:=0.1.10
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/snowie2000/geoview/tar.gz/$(PKG_VERSION)?
PKG_HASH:=9221df928df68030893125a60bdf6d8a1fef2b199fa58ed80772cb3c17225fc5
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=snowie2000
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/snowie2000/geoview
GO_PKG_BUILD_PKG:=$(GO_PKG)
GO_PKG_LDFLAGS:=-s -w
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/geoview
TITLE:=A geofile toolkit
URL:=https://github.com/snowie2000/geoview
SECTION:=net
CATEGORY:=Network
SUBMENU:=IP Addresses and Names
DEPENDS+= $(GO_ARCH_DEPENDS)
endef
define Package/geoview/description
geoview is a handy tool to extract useful information from geo* files.
endef
define Package/geoview/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/geoview $(1)/usr/bin/
endef
$(eval $(call GoBinPackage,geoview))
$(eval $(call BuildPackage,geoview))

59
hysteria/Makefile Normal file
View File

@ -0,0 +1,59 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=hysteria
PKG_VERSION:=2.6.2
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/apernet/hysteria/tar.gz/app/v$(PKG_VERSION)?
PKG_HASH:=4699431f0bc826da2bbd3939c0a78c4e7bfc02773fc3a62b24615c37ee89b266
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-app-v$(PKG_VERSION)
PKG_LICENSE:=MIT
PKG_LICENSE_FILE:=LICENSE
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/apernet/hysteria
GO_PKG_BUILD_PKG:=$(GO_PKG)/app/v2
GO_PKG_LDFLAGS_X = \
$(GO_PKG)/app/v2/cmd.appVersion=v$(PKG_VERSION) \
$(GO_PKG)/app/v2/cmd.appType=release \
$(GO_PKG)/app/v2/cmd.appPlatform=$(GO_OS) \
$(GO_PKG)/app/v2/cmd.appArch=$(GO_ARCH)
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/hysteria
SECTION:=net
CATEGORY:=Network
TITLE:=A feature-packed network utility optimized for networks of poor quality
URL:=https://github.com/apernet/hysteria
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
endef
define Package/hysteria/description
Hysteria is a feature-packed network utility optimized for networks
of poor quality (e.g. satellite connections, congested public Wi-Fi,
connecting from China to servers abroad) powered by a custom version
of QUIC protocol.
endef
define Package/hysteria/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/app $(1)/usr/bin/hysteria
endef
$(eval $(call GoBinPackage,hysteria))
$(eval $(call BuildPackage,hysteria))

46
ipt2socks/Makefile Normal file
View File

@ -0,0 +1,46 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=ipt2socks
PKG_VERSION:=1.1.4
PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/zfl9/ipt2socks/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=68dc76e63951d655c2fd9b420e175b5a75a50014d6db6e729398b41f2c988356
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
PKG_BUILD_FLAGS:=no-mips16
PKG_LICENSE:=AGPL-3.0
PKG_LICENSE_FILE:=LICENSE
include $(INCLUDE_DIR)/package.mk
define Package/ipt2socks
SECTION:=net
CATEGORY:=Network
TITLE:=Convert iptables to socks5
URL:=https://github.com/zfl9/ipt2socks
DEPENDS:=+libpthread
endef
define Package/ipt2socks/description
Utility for converting iptables (redirect/tproxy) to socks5.
endef
TARGET_CFLAGS+= $(FPIC) -flto
MAKE_FLAGS+= \
CFLAGS="-std=c99 -pthread $(TARGET_CFLAGS)" \
EVCFLAGS="$(TARGET_CFLAGS)"
define Package/ipt2socks/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/ipt2socks $(1)/usr/bin
endef
$(eval $(call BuildPackage,ipt2socks))

46
libcron/Makefile Normal file
View File

@ -0,0 +1,46 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2021 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=libcron
PKG_RELEASE:=1
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/PerMalmberg/libcron.git
PKG_SOURCE_DATE:=2023-11-14
PKG_SOURCE_VERSION:=41f238ceb09d4179e7346d78584a0c978e5d0059
PKG_MIRROR_HASH:=c7b0651566153b1d641e3b5ece50474c8556d42345779f19a5f22814a8183c38
PKG_LICENSE:=MIT
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_BUILD_PARALLEL:=1
CMAKE_INSTALL:=1
include $(INCLUDE_DIR)/package.mk
include $(INCLUDE_DIR)/cmake.mk
define Package/libcron
SECTION:=lib
CATEGORY:=Libraries
URL:=https://github.com/PerMalmberg/libcron
TITLE:=A C++ scheduling library using cron formatting
DEPENDS:=+libstdcpp
endef
define Package/libcron/description
Libcron offers an easy to use API to add callbacks with corresponding
cron-formatted strings.
endef
CMAKE_OPTIONS+= -DBUILD_SHARED_LIBS=ON
define Package/libcron/install
$(INSTALL_DIR) $(1)/usr/lib/
$(CP) $(PKG_INSTALL_DIR)/usr/lib/liblibcron.so $(1)/usr/lib/
endef
$(eval $(call BuildPackage,libcron))

44
lua-neturl/Makefile Normal file
View File

@ -0,0 +1,44 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2022-2023 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=neturl
PKG_VERSION:=1.1-1
PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/golgote/neturl/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=25f3a94ba9f435ef1395555de2bf17d6f934d789fa515ed965405919e42be27b
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_LICENSE:=MIT
PKG_LICNESE_FILES:=LICENSE.txt
include $(INCLUDE_DIR)/package.mk
define Package/lua-neturl
SUBMENU:=Lua
SECTION:=lang
CATEGORY:=Languages
TITLE:=URL and Query string parser, builder, normalizer for Lua
URL:=https://github.com/golgote/neturl
DEPENDS:=+lua
PKGARCH:=all
endef
define Package/lua-neturl/description
This small Lua library provides a few functions to parse URL with
querystring and build new URL easily.
endef
define Build/Compile
endef
define Package/lua-neturl/install
$(INSTALL_DIR) $(1)/usr/lib/lua
$(CP) $(PKG_BUILD_DIR)/lib/net/url.lua $(1)/usr/lib/lua/
endef
$(eval $(call BuildPackage,lua-neturl))

View File

@ -0,0 +1,20 @@
--- a/lib/net/url.lua
+++ b/lib/net/url.lua
@@ -340,7 +340,7 @@ function M:setAuthority(authority)
self.password = v
return ''
end)
- if string.find(userinfo, "^[%w%+%.]+$") then
+ if string.find(userinfo, "^[%p%w%+%.]+$") then
self.user = userinfo
else
-- incorrect userinfo
@@ -369,7 +369,7 @@ function M.parse(url)
comp.fragment = v
return ''
end)
- url =url:gsub('^([%w][%w%+%-%.]*)%:', function(v)
+ url =url:gsub('^([%w][%w%+%-%_%.]*)%:', function(v)
comp.scheme = v:lower()
return ''
end)

15
luci-app-daed/Makefile Normal file
View File

@ -0,0 +1,15 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daed
PKG_VERSION:=1.3
PKG_RELEASE:=1
LUCI_TITLE:=LuCI Support for DAED
LUCI_DEPENDS:=+daed +curl +libcron +zoneinfo-asia +luci-compat
LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/conffiles
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,34 @@
local sys = require "luci.sys"
local http = require "luci.http"
module("luci.controller.daed", package.seeall)
function index()
if not nixio.fs.access("/etc/config/daed") then
return
end
entry({"admin", "services", "daed"}, alias("admin", "services", "daed", "setting"),_("DAED"), 58).dependent = true
entry({"admin", "services", "daed", "setting"}, cbi("daed/basic"), _("Base Setting"), 1).leaf=true
entry({"admin", "services", "daed", "daed"}, template("daed/daed"), _("Dashboard"), 2).leaf = true
entry({"admin", "services", "daed", "log"}, cbi("daed/log"), _("Logs"), 3).leaf = true
entry({"admin", "services", "daed_status"}, call("act_status"))
entry({"admin", "services", "daed", "get_log"}, call("get_log")).leaf = true
entry({"admin", "services", "daed", "clear_log"}, call("clear_log")).leaf = true
end
function act_status()
local sys = require "luci.sys"
local e = { }
e.running = sys.call("pidof daed >/dev/null") == 0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
function get_log()
http.write(sys.exec("cat /var/log/daed/daed.log"))
end
function clear_log()
sys.call("true > /var/log/daed/daed.log")
end

View File

@ -0,0 +1,62 @@
local m, s ,o
m = Map("daed")
m.title = translate("DAED")
m.description = translate("DAE is a Linux high-performance transparent proxy solution based on eBPF, And DAED is a modern dashboard for dae.")
m:section(SimpleSection).template = "daed/daed_status"
s = m:section(TypedSection, "daed", translate("Global Settings"))
s.addremove = false
s.anonymous = true
o = s:option(Flag,"enabled",translate("Enable"))
o.default = 0
enable = s:option(Flag, "subscribe_auto_update", translate("Enable Auto Subscribe Update"))
enable.rmempty = false
o = s:option(Value, "daed_username", translate("Username"))
o.default = Username
o.password = true
o:depends('subscribe_auto_update', '1')
o = s:option(Value, "daed_password", translate("Password"))
o.default = Password
o.password = true
o:depends('subscribe_auto_update', '1')
o = s:option(ListValue, "subscribe_update_week_time", translate("Update Cycle"))
o:value("*", translate("Every Day"))
o:value("1", translate("Every Monday"))
o:value("2", translate("Every Tuesday"))
o:value("3", translate("Every Wednesday"))
o:value("4", translate("Every Thursday"))
o:value("5", translate("Every Friday"))
o:value("6", translate("Every Saturday"))
o:value("7", translate("Every Sunday"))
o.default = "*"
o:depends('subscribe_auto_update', '1')
update_time = s:option(ListValue, "subscribe_update_day_time", translate("Update Time (Every Day)"))
for t = 0, 23 do
update_time:value(t, t..":00")
end
update_time.default = 0
update_time:depends('subscribe_auto_update', '1')
o = s:option(Value, "log_maxbackups", translate("Logfile retention count"))
o.default = 1
o = s:option(Value, "log_maxsize", translate("Logfile Max Size (MB)"))
o.default = 5
o = s:option(Value, "listen_addr",translate("Set the DAED listen address"))
o.default = '0.0.0.0:2023'
m.apply_on_parse = true
m.on_after_apply = function(self,map)
luci.sys.exec("/etc/init.d/daed restart")
end
return m

View File

@ -0,0 +1,5 @@
m = Map("daed")
m:append(Template("daed/daed_log"))
return m

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,33 @@
<script type="text/javascript">
//<![CDATA[
function clear_log(btn) {
XHR.get('<%=url([[admin]], [[services]], [[daed]], [[clear_log]])%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = "";
log_textarea.scrollTop = log_textarea.scrollHeight;
}
location.reload();
}
);
}
var scrolled = false;
XHR.poll(2, '<%=url([[admin]], [[services]], [[daed]], [[get_log]])%>', null,
function(x, data) {
if(x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = x.responseText;
if (!scrolled) {
log_textarea.scrollTop = log_textarea.scrollHeight;
scrolled = true;
}
}
}
);
//]]>
</script>
<fieldset class="cbi-section" id="_log_fieldset">
<input class="cbi-button cbi-input-remove" type="button" onclick="clear_log()" value="<%:Clear logs%>" style="margin-left: 10px; margin-top: 10px;">
<textarea id="log_textarea" class="cbi-input-textarea" style="width: calc(100% - 20px); height: 645px; margin: 10px;" data-update="change" rows="5" wrap="off" readonly="readonly"></textarea>
</fieldset>

View File

@ -0,0 +1,24 @@
<script type="text/javascript">//<![CDATA[
XHR.poll(3, '<%=url([[admin]], [[services]], [[daed_status]])%>', null,
function(x, data) {
var tb = document.getElementById('daed_status');
if (data && tb)
{
if (data.running)
{
tb.innerHTML = '<em><b style=\"color:green\"><%:DAED%> <%:RUNNING%></b></em>';
}
else
{
tb.innerHTML = '<em><b style=\"color:red\"><%:DAED%> <%:NOT RUNNING%></b></em>';
}
}
}
);
//]]></script>
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
<fieldset class="cbi-section">
<p id="daed_status">
<em><%:Collecting data...%></em>
</p>
</fieldset>

View File

@ -0,0 +1,59 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "DAED"
msgstr "DAED"
msgid "DAE is a Linux high-performance transparent proxy solution based on eBPF, And DAED is a modern dashboard for dae."
msgstr "DAE是一个基于eBPF的Linux高性能透明代理解决方案而DAED是DAE的管理面板。"
msgid "Base Setting"
msgstr "基本设置"
msgid "Dashboard"
msgstr "仪表板"
msgid "Logs"
msgstr "日志"
msgid "Clear logs"
msgstr "清空日志"
msgid "RUNNING"
msgstr "运行中"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "Collecting data..."
msgstr "收集数据..."
msgid "Logfile retention count"
msgstr "日志文件保留数量"
msgid "Logfile Max Size (MB)"
msgstr "日志文件大小MB"
msgid "Set the DAED listen address"
msgstr "设置监听地址"
msgid "DAED is not running"
msgstr "DAED 未运行"
msgid "Please start the DAED service first and try again"
msgstr "请先启动 DAED 服务后重试"
msgid "Enable Auto Subscribe Update"
msgstr "启用订阅自动更新"
msgid "Update Cycle"
msgstr "更新周期"
msgid "Update Time (Every Day)"
msgstr "更新时间(每天)"
msgid "Username"
msgstr "用户名"
msgid "Password"
msgstr "密码"

View File

@ -0,0 +1,36 @@
#!/bin/sh
USERNAME=$(uci -q get daed.config.daed_username)
PASSWORD=$(uci -q get daed.config.daed_password)
PORT=$(echo "$(uci -q get daed.config.listen_addr)" | grep -oE '[0-9]+$' | sed -n '1p')
GRAPHQL_URL="http://127.0.0.1:"$PORT"/graphql"
CRON_FILE="/etc/crontabs/root"
RANDOM_SEED=$RANDOM
RANDOM_NUM=$((RANDOM_SEED % 10 + 1))
login() {
LOGIN=$(curl -s -X POST -H "Content-Type: application/json" -d '{"query":"query Token($username: String!, $password: String!) {\n token(username: $username, password: $password)\n}","variables":{"username":"'"$USERNAME"'","password":"'"$PASSWORD"'"}}' $GRAPHQL_URL)
JSON=${LOGIN#\"}
JSON=${LOGIN%\"}
TOKEN=$(echo $JSON | sed -n 's/.*"token":"\([^"]*\)".*/\1/p')
}
update_subscription() {
SUBSCRIPTION_ID_LIST=$(curl -s -X POST -H "Authorization: $TOKEN" -d '{"query": "query Subscriptions {\n subscriptions {\nid\ntag\nstatus\nlink\ninfo\nupdatedAt\nnodes {\nedges {\nid\nname\nprotocol\nlink\n}\n}\n}\n}", "operationName": "Subscriptions"}' $GRAPHQL_URL | grep -o '"id":"[^"]*","tag"' | grep -o 'id":"[^"]*' | grep -o '[^"]*$')
echo "$SUBSCRIPTION_ID_LIST" | while read -r id; do
curl -X POST -H "Authorization: $TOKEN" -d '{"query":"mutation UpdateSubscription($id: ID!) {\n updateSubscription(id: $id) {\n id\n }\n}","variables":{"id":"'"$id"'"},"operationName":"UpdateSubscription"}' $GRAPHQL_URL
done
}
reload() {
curl -X POST -H "Authorization: $TOKEN" -d '{"query":"mutation Run($dry: Boolean!) {\n run(dry: $dry)\n}","variables":{"dry":false},"operationName":"Run"}' $GRAPHQL_URL
}
resetcron() {
touch $CRON_FILE
sed -i '/daed_sub.sh/d' $CRON_FILE 2>/dev/null
[ "$(uci -q get daed.config.subscribe_auto_update)" -eq 1 ] && echo "${RANDOM_NUM} $(uci -q get daed.config.subscribe_update_day_time) * * $(uci -q get daed.config.subscribe_update_week_time) /etc/daed/daed_sub.sh >/dev/null 2>&1" >>$CRON_FILE
crontab $CRON_FILE
}
login && update_subscription && reload && resetcron

View File

@ -0,0 +1,22 @@
#!/bin/sh
[ "${ACTION}" = "ifup" ] || exit 0
DEVICE=$(logread | grep "link is up" | tail -n 1 | awk -F "'" '{print $2}')
DEVICE_TYPE=$(ip link show dev "$DEVICE")
case "$DEVICE_TYPE" in
*"link/ether"*)
(
LOCK_FILE="/tmp/lock/daed_hotplug_lock"
if [ -f "$LOCK_FILE" ]; then
exit 1
else
echo $$ > "$LOCK_FILE" 2>/dev/null
trap 'rm -f "$LOCK_FILE"' EXIT
sleep 60
/etc/init.d/daed restart 2>&1
fi
) &
;;
esac

View File

@ -0,0 +1,70 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2023 Tianling Shen <cnsztl@immortalwrt.org>
USE_PROCD=0
START=98
CONF="daed"
PROG="/usr/bin/daed"
LOG="/var/log/daed/daed.log"
CRON_FILE="/etc/crontabs/root"
RANDOM_SEED=$RANDOM
RANDOM_NUM=$((RANDOM_SEED % 10 + 1))
setcron() {
touch $CRON_FILE
sed -i '/daed_sub.sh/d' $CRON_FILE 2>/dev/null
[ "$(uci -q get daed.config.subscribe_auto_update)" -eq 1 ] && echo "${RANDOM_NUM} $(uci -q get daed.config.subscribe_update_day_time) * * $(uci -q get daed.config.subscribe_update_week_time) /etc/daed/daed_sub.sh >/dev/null 2>&1" >>$CRON_FILE
crontab $CRON_FILE
}
delcron() {
sed -i '/daed_sub.sh/d' $CRON_FILE 2>/dev/null
crontab $CRON_FILE
}
setlocaluse() {
uci set dhcp.@dnsmasq[0].localuse="1"
uci commit dhcp
/etc/init.d/dnsmasq restart
}
dellocaluse() {
uci set dhcp.@dnsmasq[0].localuse="0"
uci commit dhcp
/etc/init.d/dnsmasq restart
. /lib/functions/network.sh
network_find_wan LOGICAL_WAN || exit 1
dns_list=$(ubus call network.interface.$LOGICAL_WAN status | jsonfilter -e '@["dns-server"]' | sed 's/[]["]//g' | sed 's/,/\n/g' | grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b')
[ -z "$dns_list" ] && dns_list="223.5.5.5 119.29.29.29 180.76.76.76"
grep -v '^nameserver ' /etc/resolv.conf > /tmp/resolv.conf.new.daed 2>/dev/null
for dns in $dns_list; do
echo "nameserver $dns" >> /tmp/resolv.conf.new.daed
done
cat /tmp/resolv.conf.new.daed > /etc/resolv.conf
rm -f /tmp/resolv.conf.new.daed
}
start_service() {
[ -f "/etc/init.d/daed" ] && grep -q "DAE_LOCATION_ASSET" "/etc/init.d/daed" || sed -i '/run/i\ procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray"' "/etc/init.d/daed"
config_load "$CONF"
local enabled
config_get_bool enabled "config" "enabled" "0"
if [ "$enabled" -eq 0 ]; then
delcron
setlocaluse
return 1
fi
setcron
dellocaluse
}
stop_service() {
delcron
setlocaluse
}
service_triggers() {
procd_add_reload_trigger "$CONF"
}

View File

@ -0,0 +1,11 @@
{
"luci-app-daed": {
"description": "Grant UCI access for luci-app-daed",
"read": {
"uci": [ "daed" ]
},
"write": {
"uci": [ "daed" ]
}
}
}

View File

@ -0,0 +1,26 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2022-2023 ImmortalWrt.org
include $(TOPDIR)/rules.mk
LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64
LUCI_PKGARCH:=all
LUCI_DEPENDS:= \
+sing-box \
+firewall4 \
+kmod-nft-tproxy
PKG_NAME:=luci-app-homeproxy
define Package/luci-app-homeproxy/conffiles
/etc/config/homeproxy
/etc/homeproxy/certs/
/etc/homeproxy/ruleset/
/etc/homeproxy/resources/direct_list.txt
/etc/homeproxy/resources/proxy_list.txt
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,340 @@
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2022-2025 ImmortalWrt.org
*/
'use strict';
'require baseclass';
'require form';
'require fs';
'require rpc';
'require uci';
'require ui';
return baseclass.extend({
dns_strategy: {
'': _('Default'),
'prefer_ipv4': _('Prefer IPv4'),
'prefer_ipv6': _('Prefer IPv6'),
'ipv4_only': _('IPv4 only'),
'ipv6_only': _('IPv6 only')
},
shadowsocks_encrypt_length: {
/* AEAD */
'aes-128-gcm': 0,
'aes-192-gcm': 0,
'aes-256-gcm': 0,
'chacha20-ietf-poly1305': 0,
'xchacha20-ietf-poly1305': 0,
/* AEAD 2022 */
'2022-blake3-aes-128-gcm': 16,
'2022-blake3-aes-256-gcm': 32,
'2022-blake3-chacha20-poly1305': 32
},
shadowsocks_encrypt_methods: [
/* Stream */
'none',
/* AEAD */
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305',
'xchacha20-ietf-poly1305',
/* AEAD 2022 */
'2022-blake3-aes-128-gcm',
'2022-blake3-aes-256-gcm',
'2022-blake3-chacha20-poly1305'
],
tls_cipher_suites: [
'TLS_RSA_WITH_AES_128_CBC_SHA',
'TLS_RSA_WITH_AES_256_CBC_SHA',
'TLS_RSA_WITH_AES_128_GCM_SHA256',
'TLS_RSA_WITH_AES_256_GCM_SHA384',
'TLS_AES_128_GCM_SHA256',
'TLS_AES_256_GCM_SHA384',
'TLS_CHACHA20_POLY1305_SHA256',
'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA',
'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA',
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA',
'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256',
'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256'
],
tls_versions: [
'1.0',
'1.1',
'1.2',
'1.3'
],
CBIStaticList: form.DynamicList.extend({
__name__: 'CBI.StaticList',
renderWidget: function(/* ... */) {
var dl = form.DynamicList.prototype.renderWidget.apply(this, arguments);
dl.querySelector('.add-item ul > li[data-value="-"]')?.remove();
return dl;
}
}),
calcStringMD5(e) {
/* Thanks to https://stackoverflow.com/a/41602636 */
function h(a, b) {
var c, d, e, f, g;
e = a & 2147483648;
f = b & 2147483648;
c = a & 1073741824;
d = b & 1073741824;
g = (a & 1073741823) + (b & 1073741823);
return c & d ? g ^ 2147483648 ^ e ^ f : c | d ? g & 1073741824 ? g ^ 3221225472 ^ e ^ f : g ^ 1073741824 ^ e ^ f : g ^ e ^ f;
}
function k(a, b, c, d, e, f, g) { a = h(a, h(h(b & c | ~b & d, e), g)); return h(a << f | a >>> 32 - f, b); }
function l(a, b, c, d, e, f, g) { a = h(a, h(h(b & d | c & ~d, e), g)); return h(a << f | a >>> 32 - f, b); }
function m(a, b, d, c, e, f, g) { a = h(a, h(h(b ^ d ^ c, e), g)); return h(a << f | a >>> 32 - f, b); }
function n(a, b, d, c, e, f, g) { a = h(a, h(h(d ^ (b | ~c), e), g)); return h(a << f | a >>> 32 - f, b); }
function p(a) {
var b = '', d = '';
for (var c = 0; 3 >= c; c++) d = a >>> 8 * c & 255, d = '0' + d.toString(16), b += d.substr(d.length - 2, 2);
return b;
}
var f = [], q, r, s, t, a, b, c, d;
e = function(a) {
a = a.replace(/\r\n/g, '\n');
for (var b = '', d = 0; d < a.length; d++) {
var c = a.charCodeAt(d);
128 > c ? b += String.fromCharCode(c) : (127 < c && 2048 > c ? b += String.fromCharCode(c >> 6 | 192) :
(b += String.fromCharCode(c >> 12 | 224), b += String.fromCharCode(c >> 6 & 63 | 128)),
b += String.fromCharCode(c & 63 | 128))
}
return b;
}(e);
f = function(b) {
var c = b.length, a = c + 8;
for (var d = 16 * ((a - a % 64) / 64 + 1), e = Array(d - 1), f = 0, g = 0; g < c;)
a = (g - g % 4) / 4, f = g % 4 * 8, e[a] |= b.charCodeAt(g) << f, g++;
a = (g - g % 4) / 4; e[a] |= 128 << g % 4 * 8; e[d - 2] = c << 3; e[d - 1] = c >>> 29;
return e;
}(e);
a = 1732584193;
b = 4023233417;
c = 2562383102;
d = 271733878;
for (e = 0; e < f.length; e += 16) q = a, r = b, s = c, t = d,
a = k(a, b, c, d, f[e + 0], 7, 3614090360), d = k(d, a, b, c, f[e + 1], 12, 3905402710),
c = k(c, d, a, b, f[e + 2], 17, 606105819), b = k(b, c, d, a, f[e + 3], 22, 3250441966),
a = k(a, b, c, d, f[e + 4], 7, 4118548399), d = k(d, a, b, c, f[e + 5], 12, 1200080426),
c = k(c, d, a, b, f[e + 6], 17, 2821735955), b = k(b, c, d, a, f[e + 7], 22, 4249261313),
a = k(a, b, c, d, f[e + 8], 7, 1770035416), d = k(d, a, b, c, f[e + 9], 12, 2336552879),
c = k(c, d, a, b, f[e + 10], 17, 4294925233), b = k(b, c, d, a, f[e + 11], 22, 2304563134),
a = k(a, b, c, d, f[e + 12], 7, 1804603682), d = k(d, a, b, c, f[e + 13], 12, 4254626195),
c = k(c, d, a, b, f[e + 14], 17, 2792965006), b = k(b, c, d, a, f[e + 15], 22, 1236535329),
a = l(a, b, c, d, f[e + 1], 5, 4129170786), d = l(d, a, b, c, f[e + 6], 9, 3225465664),
c = l(c, d, a, b, f[e + 11], 14, 643717713), b = l(b, c, d, a, f[e + 0], 20, 3921069994),
a = l(a, b, c, d, f[e + 5], 5, 3593408605), d = l(d, a, b, c, f[e + 10], 9, 38016083),
c = l(c, d, a, b, f[e + 15], 14, 3634488961), b = l(b, c, d, a, f[e + 4], 20, 3889429448),
a = l(a, b, c, d, f[e + 9], 5, 568446438), d = l(d, a, b, c, f[e + 14], 9, 3275163606),
c = l(c, d, a, b, f[e + 3], 14, 4107603335), b = l(b, c, d, a, f[e + 8], 20, 1163531501),
a = l(a, b, c, d, f[e + 13], 5, 2850285829), d = l(d, a, b, c, f[e + 2], 9, 4243563512),
c = l(c, d, a, b, f[e + 7], 14, 1735328473), b = l(b, c, d, a, f[e + 12], 20, 2368359562),
a = m(a, b, c, d, f[e + 5], 4, 4294588738), d = m(d, a, b, c, f[e + 8], 11, 2272392833),
c = m(c, d, a, b, f[e + 11], 16, 1839030562), b = m(b, c, d, a, f[e + 14], 23, 4259657740),
a = m(a, b, c, d, f[e + 1], 4, 2763975236), d = m(d, a, b, c, f[e + 4], 11, 1272893353),
c = m(c, d, a, b, f[e + 7], 16, 4139469664), b = m(b, c, d, a, f[e + 10], 23, 3200236656),
a = m(a, b, c, d, f[e + 13], 4, 681279174), d = m(d, a, b, c, f[e + 0], 11, 3936430074),
c = m(c, d, a, b, f[e + 3], 16, 3572445317), b = m(b, c, d, a, f[e + 6], 23, 76029189),
a = m(a, b, c, d, f[e + 9], 4, 3654602809), d = m(d, a, b, c, f[e + 12], 11, 3873151461),
c = m(c, d, a, b, f[e + 15], 16, 530742520), b = m(b, c, d, a, f[e + 2], 23, 3299628645),
a = n(a, b, c, d, f[e + 0], 6, 4096336452), d = n(d, a, b, c, f[e + 7], 10, 1126891415),
c = n(c, d, a, b, f[e + 14], 15, 2878612391), b = n(b, c, d, a, f[e + 5], 21, 4237533241),
a = n(a, b, c, d, f[e + 12], 6, 1700485571), d = n(d, a, b, c, f[e + 3], 10, 2399980690),
c = n(c, d, a, b, f[e + 10], 15, 4293915773), b = n(b, c, d, a, f[e + 1], 21, 2240044497),
a = n(a, b, c, d, f[e + 8], 6, 1873313359), d = n(d, a, b, c, f[e + 15], 10, 4264355552),
c = n(c, d, a, b, f[e + 6], 15, 2734768916), b = n(b, c, d, a, f[e + 13], 21, 1309151649),
a = n(a, b, c, d, f[e + 4], 6, 4149444226), d = n(d, a, b, c, f[e + 11], 10, 3174756917),
c = n(c, d, a, b, f[e + 2], 15, 718787259), b = n(b, c, d, a, f[e + 9], 21, 3951481745),
a = h(a, q), b = h(b, r), c = h(c, s), d = h(d, t);
return (p(a) + p(b) + p(c) + p(d)).toLowerCase();
},
decodeBase64Str(str) {
if (!str)
return null;
/* Thanks to luci-app-ssr-plus */
str = str.replace(/-/g, '+').replace(/_/g, '/');
var padding = (4 - str.length % 4) % 4;
if (padding)
str = str + Array(padding + 1).join('=');
return decodeURIComponent(Array.prototype.map.call(atob(str), (c) =>
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
).join(''));
},
getBuiltinFeatures() {
const callGetSingBoxFeatures = rpc.declare({
object: 'luci.homeproxy',
method: 'singbox_get_features',
expect: { '': {} }
});
return L.resolveDefault(callGetSingBoxFeatures(), {});
},
generateRand(type, length) {
var byteArr;
if (['base64', 'hex'].includes(type))
byteArr = crypto.getRandomValues(new Uint8Array(length));
switch (type) {
case 'base64':
/* Thanks to https://stackoverflow.com/questions/9267899 */
return btoa(String.fromCharCode.apply(null, byteArr));
case 'hex':
return Array.from(byteArr, (byte) =>
(byte & 255).toString(16).padStart(2, '0')
).join('');
case 'uuid':
/* Thanks to https://stackoverflow.com/a/2117523 */
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
default:
return null;
};
},
loadDefaultLabel(uciconfig, ucisection) {
var label = uci.get(uciconfig, ucisection, 'label');
if (label) {
return label;
} else {
uci.set(uciconfig, ucisection, 'label', ucisection);
return ucisection;
}
},
loadModalTitle(title, addtitle, uciconfig, ucisection) {
var label = uci.get(uciconfig, ucisection, 'label');
return label ? title + ' » ' + label : addtitle;
},
renderSectionAdd(section, extra_class) {
var el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]),
nameEl = el.querySelector('.cbi-section-create-name');
ui.addValidator(nameEl, 'uciname', true, (v) => {
var button = el.querySelector('.cbi-section-create > .cbi-button-add');
var uciconfig = section.uciconfig || section.map.config;
if (!v) {
button.disabled = true;
return true;
} else if (uci.get(uciconfig, v)) {
button.disabled = true;
return _('Expecting: %s').format(_('unique UCI identifier'));
} else {
button.disabled = null;
return true;
}
}, 'blur', 'keyup');
return el;
},
uploadCertificate(_option, type, filename, ev) {
const callWriteCertificate = rpc.declare({
object: 'luci.homeproxy',
method: 'certificate_write',
params: ['filename'],
expect: { '': {} }
});
return ui.uploadFile('/tmp/homeproxy_certificate.tmp', ev.target)
.then(L.bind((_btn, res) => {
return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => {
if (ret.result === true)
ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size)));
else
ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(type, ret.error)));
});
}, this, ev.target))
.catch((e) => { ui.addNotification(null, E('p', e.message)) });
},
validateBase64Key(length, section_id, value) {
/* Thanks to luci-proto-wireguard */
if (section_id && value)
if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length));
return true;
},
validateCertificatePath(section_id, value) {
if (section_id && value)
if (!value.match(/^(\/etc\/homeproxy\/certs\/|\/etc\/acme\/|\/etc\/ssl\/).+$/))
return _('Expecting: %s').format(_('/etc/homeproxy/certs/..., /etc/acme/..., /etc/ssl/...'));
return true;
},
validatePortRange(section_id, value) {
if (section_id && value) {
value = value.match(/^(\d+)?\:(\d+)?$/);
if (value && (value[1] || value[2])) {
if (!value[1])
value[1] = 0;
else if (!value[2])
value[2] = 65535;
if (value[1] < value[2] && value[2] <= 65535)
return true;
}
return _('Expecting: %s').format( _('valid port range (port1:port2)'));
}
return true;
},
validateUniqueValue(uciconfig, ucisection, ucioption, section_id, value) {
if (section_id) {
if (!value)
return _('Expecting: %s').format(_('non-empty value'));
if (ucioption === 'node' && value === 'urltest')
return true;
var duplicate = false;
uci.sections(uciconfig, ucisection, (res) => {
if (res['.name'] !== section_id)
if (res[ucioption] === value)
duplicate = true
});
if (duplicate)
return _('Expecting: %s').format(_('unique value'));
}
return true;
},
validateUUID(section_id, value) {
if (section_id) {
if (!value)
return _('Expecting: %s').format(_('non-empty value'));
else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null)
return _('Expecting: %s').format(_('valid uuid'));
}
return true;
}
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,874 @@
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2022-2025 ImmortalWrt.org
*/
'use strict';
'require form';
'require poll';
'require rpc';
'require uci';
'require ui';
'require view';
'require homeproxy as hp';
const callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: ['name'],
expect: { '': {} }
});
function getServiceStatus() {
return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => {
let isRunning = false;
try {
isRunning = res['homeproxy']['instances']['sing-box-s']['running'];
} catch (e) { }
return isRunning;
});
}
function renderStatus(isRunning, version) {
let spanTemp = '<em><span style="color:%s"><strong>%s (sing-box v%s) %s</strong></span></em>';
let renderHTML;
if (isRunning)
renderHTML = spanTemp.format('green', _('HomeProxy Server'), version, _('RUNNING'));
else
renderHTML = spanTemp.format('red', _('HomeProxy Server'), version, _('NOT RUNNING'));
return renderHTML;
}
function handleGenKey(option) {
let section_id = this.section.section;
let type = this.section.getOption('type')?.formvalue(section_id);
let widget = L.bind(function(option) {
return this.map.findElement('id', 'widget.' + this.cbid(section_id).replace(/\.[^\.]+$/, '.') + option);
}, this);
const callSingBoxGenerator = rpc.declare({
object: 'luci.homeproxy',
method: 'singbox_generator',
params: ['type', 'params'],
expect: { '': {} }
});
if (typeof option === 'object') {
return callSingBoxGenerator(option.type, option.params).then((ret) => {
if (ret.result)
for (let key in option.result)
widget(option.result[key]).value = ret.result[key] || '';
else
ui.addNotification(null, E('p', _('Failed to generate %s, error: %s.').format(type, ret.error)));
});
} else {
let password, required_method;
if (option === 'uuid')
required_method = 'uuid';
else if (type === 'shadowsocks')
required_method = this.section.getOption('shadowsocks_encrypt_method')?.formvalue(section_id);
switch (required_method) {
case 'none':
password = '';
break;
case 'uuid':
password = hp.generateRand('uuid');
break;
default:
password = hp.generateRand('hex', 16);
break;
}
/* AEAD */
(function(length) {
if (length && length > 0)
password = hp.generateRand('base64', length);
}(hp.shadowsocks_encrypt_length[required_method]));
return widget(option).value = password;
}
}
return view.extend({
load() {
return Promise.all([
uci.load('homeproxy'),
hp.getBuiltinFeatures()
]);
},
render(data) {
let m, s, o;
let features = data[1];
m = new form.Map('homeproxy', _('HomeProxy Server'),
_('The modern OpenWrt proxy platform for ARM64/AMD64.'));
s = m.section(form.TypedSection);
s.render = function () {
poll.add(function () {
return L.resolveDefault(getServiceStatus()).then((res) => {
let view = document.getElementById('service_status');
view.innerHTML = renderStatus(res, features.version);
});
});
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
E('p', { id: 'service_status' }, _('Collecting data...'))
]);
}
s = m.section(form.NamedSection, 'server', 'homeproxy', _('Global settings'));
o = s.option(form.Flag, 'enabled', _('Enable'));
o.default = o.disabled;
o.rmempty = false;
s = m.section(form.GridSection, 'server', _('Server settings'));
s.addremove = true;
s.rowcolors = true;
s.sortable = true;
s.nodescriptions = true;
s.modaltitle = L.bind(hp.loadModalTitle, this, _('Server'), _('Add a server'), data[0]);
s.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
s.renderSectionAdd = L.bind(hp.renderSectionAdd, this, s);
o = s.option(form.Value, 'label', _('Label'));
o.load = L.bind(hp.loadDefaultLabel, this, data[0]);
o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'label');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Flag, 'enabled', _('Enable'));
o.default = o.enabled;
o.rmempty = false;
o.editable = true;
o = s.option(form.Flag, 'firewall', _('Firewall'),
_('Allow access from the Internet.'));
o.editable = true;
o = s.option(form.ListValue, 'type', _('Type'));
o.value('http', _('HTTP'));
if (features.with_quic) {
o.value('hysteria', _('Hysteria'));
o.value('hysteria2', _('Hysteria2'));
o.value('naive', _('NaïveProxy'));
}
o.value('mixed', _('Mixed'));
o.value('shadowsocks', _('Shadowsocks'));
o.value('socks', _('Socks'));
o.value('trojan', _('Trojan'));
if (features.with_quic)
o.value('tuic', _('Tuic'));
o.value('vless', _('VLESS'));
o.value('vmess', _('VMess'));
o.rmempty = false;
o = s.option(form.Value, 'address', _('Listen address'));
o.placeholder = '::';
o.datatype = 'ipaddr';
o.modalonly = true;
o = s.option(form.Value, 'port', _('Listen port'),
_('The port must be unique.'));
o.datatype = 'port';
o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'port');
o = s.option(form.Value, 'username', _('Username'));
o.depends('type', 'http');
o.depends('type', 'mixed');
o.depends('type', 'naive');
o.depends('type', 'socks');
o.modalonly = true;
o = s.option(form.Value, 'password', _('Password'));
o.password = true;
o.depends({'type': /^(http|mixed|naive|socks)$/, 'username': /[\s\S]/});
o.depends('type', 'hysteria2');
o.depends('type', 'shadowsocks');
o.depends('type', 'trojan');
o.depends('type', 'tuic');
o.renderWidget = function() {
let node = form.Value.prototype.renderWidget.apply(this, arguments);
(node.querySelector('.control-group') || node).appendChild(E('button', {
'class': 'cbi-button cbi-button-apply',
'title': _('Generate'),
'click': ui.createHandlerFn(this, handleGenKey, this.option)
}, [ _('Generate') ]));
return node;
}
o.validate = function(section_id, value) {
if (section_id) {
let type = this.section.formvalue(section_id, 'type');
let required_type = [ 'http', 'mixed', 'naive', 'socks', 'shadowsocks' ];
if (required_type.includes(type)) {
if (type === 'shadowsocks') {
let encmode = this.section.formvalue(section_id, 'shadowsocks_encrypt_method');
if (encmode === 'none')
return true;
else if (encmode === '2022-blake3-aes-128-gcm')
return hp.validateBase64Key(24, section_id, value);
else if (['2022-blake3-aes-256-gcm', '2022-blake3-chacha20-poly1305'].includes(encmode))
return hp.validateBase64Key(44, section_id, value);
}
if (!value)
return _('Expecting: %s').format(_('non-empty value'));
}
}
return true;
}
o.modalonly = true;
/* Hysteria (2) config start */
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
o.value('udp');
/* WeChat-Video / FakeTCP are unsupported by sing-box currently
o.value('wechat-video');
o.value('faketcp');
*/
o.default = 'udp';
o.depends('type', 'hysteria');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'hysteria_down_mbps', _('Max download speed'),
_('Max download speed in Mbps.'));
o.datatype = 'uinteger';
o.depends('type', 'hysteria');
o.depends('type', 'hysteria2');
o.modalonly = true;
o = s.option(form.Value, 'hysteria_up_mbps', _('Max upload speed'),
_('Max upload speed in Mbps.'));
o.datatype = 'uinteger';
o.depends('type', 'hysteria');
o.depends('type', 'hysteria2');
o.modalonly = true;
o = s.option(form.ListValue, 'hysteria_auth_type', _('Authentication type'));
o.value('', _('Disable'));
o.value('base64', _('Base64'));
o.value('string', _('String'));
o.depends('type', 'hysteria');
o.modalonly = true;
o = s.option(form.Value, 'hysteria_auth_payload', _('Authentication payload'));
o.depends({'type': 'hysteria', 'hysteria_auth_type': /[\s\S]/});
o.rmempty = false;
o.modalonly = true;
o = s.option(form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
o.value('', _('Disable'));
o.value('salamander', _('Salamander'));
o.depends('type', 'hysteria2');
o.modalonly = true;
o = s.option(form.Value, 'hysteria_obfs_password', _('Obfuscate password'));
o.depends('type', 'hysteria');
o.depends({'type': 'hysteria2', 'hysteria_obfs_type': /[\s\S]/});
o.renderWidget = function() {
let node = form.Value.prototype.renderWidget.apply(this, arguments);
(node.querySelector('.control-group') || node).appendChild(E('button', {
'class': 'cbi-button cbi-button-apply',
'title': _('Generate'),
'click': ui.createHandlerFn(this, handleGenKey, this.option)
}, [ _('Generate') ]));
return node;
}
o.modalonly = true;
o = s.option(form.Value, 'hysteria_recv_window_conn', _('QUIC stream receive window'),
_('The QUIC stream-level flow control window for receiving data.'));
o.datatype = 'uinteger';
o.default = '67108864';
o.depends('type', 'hysteria');
o.modalonly = true;
o = s.option(form.Value, 'hysteria_recv_window_client', _('QUIC connection receive window'),
_('The QUIC connection-level flow control window for receiving data.'));
o.datatype = 'uinteger';
o.default = '15728640';
o.depends('type', 'hysteria');
o.modalonly = true;
o = s.option(form.Value, 'hysteria_max_conn_client', _('QUIC maximum concurrent bidirectional streams'),
_('The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.'));
o.datatype = 'uinteger';
o.default = '1024';
o.depends('type', 'hysteria');
o.modalonly = true;
o = s.option(form.Flag, 'hysteria_disable_mtu_discovery', _('Disable Path MTU discovery'),
_('Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.'));
o.default = o.disabled;
o.depends('type', 'hysteria');
o.modalonly = true;
o = s.option(form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'),
_('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.'));
o.default = o.disabled;
o.depends({'type': 'hysteria2', 'hysteria_down_mbps': '', 'hysteria_up_mbps': ''});
o.modalonly = true;
o = s.option(form.Value, 'hysteria_masquerade', _('Masquerade'),
_('HTTP3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
o.depends('type', 'hysteria2');
o.modalonly = true;
/* Hysteria (2) config end */
/* Shadowsocks config */
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
for (let i of hp.shadowsocks_encrypt_methods)
o.value(i);
o.default = 'aes-128-gcm';
o.depends('type', 'shadowsocks');
o.modalonly = true;
/* Tuic config start */
o = s.option(form.Value, 'uuid', _('UUID'));
o.depends('type', 'tuic');
o.depends('type', 'vless');
o.depends('type', 'vmess');
o.renderWidget = function() {
let node = form.Value.prototype.renderWidget.apply(this, arguments);
(node.querySelector('.control-group') || node).appendChild(E('button', {
'class': 'cbi-button cbi-button-apply',
'title': _('Generate'),
'click': ui.createHandlerFn(this, handleGenKey, this.option)
}, [ _('Generate') ]));
return node;
}
o.validate = hp.validateUUID;
o.modalonly = true;
o = s.option(form.ListValue, 'tuic_congestion_control', _('Congestion control algorithm'),
_('QUIC congestion control algorithm.'));
o.value('cubic');
o.value('new_reno');
o.value('bbr');
o.default = 'cubic';
o.depends('type', 'tuic');
o.modalonly = true;
o = s.option(form.Value, 'tuic_auth_timeout', _('Auth timeout'),
_('How long the server should wait for the client to send the authentication command (in seconds).'));
o.datatype = 'uinteger';
o.default = '3';
o.depends('type', 'tuic');
o.modalonly = true;
o = s.option(form.Flag, 'tuic_enable_zero_rtt', _('Enable 0-RTT handshake'),
_('Enable 0-RTT QUIC connection handshake on the client side. This is not impacting much on the performance, as the protocol is fully multiplexed.<br/>' +
'Disabling this is highly recommended, as it is vulnerable to replay attacks.'));
o.default = o.disabled;
o.depends('type', 'tuic');
o.modalonly = true;
o = s.option(form.Value, 'tuic_heartbeat', _('Heartbeat interval'),
_('Interval for sending heartbeat packets for keeping the connection alive (in seconds).'));
o.datatype = 'uinteger';
o.default = '10';
o.depends('type', 'tuic');
o.modalonly = true;
/* Tuic config end */
/* VLESS / VMess config start */
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
o.value('', _('None'));
o.value('xtls-rprx-vision');
o.depends('type', 'vless');
o.modalonly = true;
o = s.option(form.Value, 'vmess_alterid', _('Alter ID'),
_('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.'));
o.datatype = 'uinteger';
o.depends('type', 'vmess');
o.modalonly = true;
/* VMess config end */
/* Transport config start */
o = s.option(form.ListValue, 'transport', _('Transport'),
_('No TCP transport, plain HTTP is merged into the HTTP transport.'));
o.value('', _('None'));
o.value('grpc', _('gRPC'));
o.value('http', _('HTTP'));
o.value('httpupgrade', _('HTTPUpgrade'));
o.value('quic', _('QUIC'));
o.value('ws', _('WebSocket'));
o.depends('type', 'trojan');
o.depends('type', 'vless');
o.depends('type', 'vmess');
o.onchange = function(ev, section_id, value) {
let desc = this.map.findElement('id', 'cbid.homeproxy.%s.transport'.format(section_id)).nextElementSibling;
if (value === 'http')
desc.innerHTML = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.');
else if (value === 'quic')
desc.innerHTML = _('No additional encryption support: It\'s basically duplicate encryption.');
else
desc.innerHTML = _('No TCP transport, plain HTTP is merged into the HTTP transport.');
let tls_element = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
if ((value === 'http' && tls_element.checked) || (value === 'grpc' && !features.with_grpc))
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
_('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.');
else if (value === 'grpc' && features.with_grpc)
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
_('If the transport doesn\'t see any activity after a duration of this time (in seconds), it pings the client to check if the connection is still active.');
}
o.modalonly = true;
/* gRPC config start */
o = s.option(form.Value, 'grpc_servicename', _('gRPC service name'));
o.depends('transport', 'grpc');
o.modalonly = true;
/* gRPC config end */
/* HTTP(Upgrade) config start */
o = s.option(form.DynamicList, 'http_host', _('Host'));
o.datatype = 'hostname';
o.depends('transport', 'http');
o.modalonly = true;
o = s.option(form.Value, 'httpupgrade_host', _('Host'));
o.datatype = 'hostname';
o.depends('transport', 'httpupgrade');
o.modalonly = true;
o = s.option(form.Value, 'http_path', _('Path'));
o.depends('transport', 'http');
o.depends('transport', 'httpupgrade');
o.modalonly = true;
o = s.option(form.Value, 'http_method', _('Method'));
o.depends('transport', 'http');
o.modalonly = true;
o = s.option(form.Value, 'http_idle_timeout', _('Idle timeout'),
_('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.'));
o.datatype = 'uinteger';
o.depends('transport', 'grpc');
o.depends({'transport': 'http', 'tls': '1'});
o.modalonly = true;
if (features.with_grpc) {
o = s.option(form.Value, 'http_ping_timeout', _('Ping timeout'),
_('The timeout (in seconds) that after performing a keepalive check, the client will wait for activity. If no activity is detected, the connection will be closed.'));
o.datatype = 'uinteger';
o.depends('transport', 'grpc');
o.modalonly = true;
}
/* HTTP config end */
/* WebSocket config start */
o = s.option(form.Value, 'ws_host', _('Host'));
o.depends('transport', 'ws');
o.modalonly = true;
o = s.option(form.Value, 'ws_path', _('Path'));
o.depends('transport', 'ws');
o.modalonly = true;
o = s.option(form.Value, 'websocket_early_data', _('Early data'),
_('Allowed payload size is in the request.'));
o.datatype = 'uinteger';
o.value('2048');
o.depends('transport', 'ws');
o.modalonly = true;
o = s.option(form.Value, 'websocket_early_data_header', _('Early data header name'),
_('Early data is sent in path instead of header by default.') +
'<br/>' +
_('To be compatible with Xray-core, set this to <code>Sec-WebSocket-Protocol</code>.'));
o.value('Sec-WebSocket-Protocol');
o.depends('transport', 'ws');
o.modalonly = true;
/* WebSocket config end */
/* Transport config end */
/* Mux config start */
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
o.default = o.disabled;
o.depends('type', 'shadowsocks');
o.depends('type', 'trojan');
o.depends('type', 'vless');
o.depends('type', 'vmess');
o.modalonly = true;
o = s.option(form.Flag, 'multiplex_padding', _('Enable padding'));
o.default = o.disabled;
o.depends('multiplex', '1');
o.modalonly = true;
if (features.hp_has_tcp_brutal) {
o = s.option(form.Flag, 'multiplex_brutal', _('Enable TCP Brutal'),
_('Enable TCP Brutal congestion control algorithm'));
o.default = o.disabled;
o.depends('multiplex', '1');
o.modalonly = true;
o = s.option(form.Value, 'multiplex_brutal_down', _('Download bandwidth'),
_('Download bandwidth in Mbps.'));
o.datatype = 'uinteger';
o.depends('multiplex_brutal', '1');
o.modalonly = true;
o = s.option(form.Value, 'multiplex_brutal_up', _('Upload bandwidth'),
_('Upload bandwidth in Mbps.'));
o.datatype = 'uinteger';
o.depends('multiplex_brutal', '1');
o.modalonly = true;
}
/* Mux config end */
/* TLS config start */
o = s.option(form.Flag, 'tls', _('TLS'));
o.default = o.disabled;
o.depends('type', 'http');
o.depends('type', 'hysteria');
o.depends('type', 'hysteria2');
o.depends('type', 'naive');
o.depends('type', 'trojan');
o.depends('type', 'tuic');
o.depends('type', 'vless');
o.depends('type', 'vmess');
o.rmempty = false;
o.validate = function(section_id, value) {
if (section_id) {
let type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
let tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
if (['hysteria', 'hysteria2', 'tuic'].includes(type)) {
tls.checked = true;
tls.disabled = true;
} else {
tls.disabled = null;
}
}
return true;
}
o.modalonly = true;
o = s.option(form.Value, 'tls_sni', _('TLS SNI'),
_('Used to verify the hostname on the returned certificates unless insecure is given.'));
o.depends('tls', '1');
o.modalonly = true;
o = s.option(form.DynamicList, 'tls_alpn', _('TLS ALPN'),
_('List of supported application level protocols, in order of preference.'));
o.depends('tls', '1');
o.modalonly = true;
o = s.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'),
_('The minimum TLS version that is acceptable.'));
o.value('', _('default'));
for (let i of hp.tls_versions)
o.value(i);
o.depends('tls', '1');
o.modalonly = true;
o = s.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'),
_('The maximum TLS version that is acceptable.'));
o.value('', _('default'));
for (let i of hp.tls_versions)
o.value(i);
o.depends('tls', '1');
o.modalonly = true;
o = s.option(hp.CBIStaticList, 'tls_cipher_suites', _('Cipher suites'),
_('The elliptic curves that will be used in an ECDHE handshake, in preference order. If empty, the default will be used.'));
for (let i of hp.tls_cipher_suites)
o.value(i);
o.depends('tls', '1');
o.optional = true;
o.modalonly = true;
if (features.with_acme) {
o = s.option(form.Flag, 'tls_acme', _('Enable ACME'),
_('Use ACME TLS certificate issuer.'));
o.default = o.disabled;
o.depends('tls', '1');
o.modalonly = true;
o = s.option(form.DynamicList, 'tls_acme_domain', _('Domains'));
o.datatype = 'hostname';
o.depends('tls_acme', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_acme_dsn', _('Default server name'),
_('Server name to use when choosing a certificate if the ClientHello\'s ServerName field is empty.'));
o.depends('tls_acme', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_acme_email', _('Email'),
_('The email address to use when creating or selecting an existing ACME server account.'));
o.depends('tls_acme', '1');
o.validate = function(section_id, value) {
if (section_id) {
if (!value)
return _('Expecting: %s').format('non-empty value');
else if (!value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/))
return _('Expecting: %s').format('valid email address');
}
return true;
}
o.modalonly = true;
o = s.option(form.Value, 'tls_acme_provider', _('CA provider'),
_('The ACME CA provider to use.'));
o.value('letsencrypt', _('Let\'s Encrypt'));
o.value('zerossl', _('ZeroSSL'));
o.depends('tls_acme', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Flag, 'tls_dns01_challenge', _('DNS01 challenge'))
o.default = o.disabled;
o.depends('tls_acme', '1');
o.modalonly = true;
o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider'));
o.value('alidns', _('Alibaba Cloud DNS'));
o.value('cloudflare', _('Cloudflare'));
o.depends('tls_dns01_challenge', '1');
o.default = 'cloudflare';
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_dns01_ali_akid', _('Access key ID'));
o.depends('tls_dns01_provider', 'alidns');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_dns01_ali_aksec', _('Access key secret'));
o.depends('tls_dns01_provider', 'alidns');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_dns01_ali_rid', _('Region ID'));
o.depends('tls_dns01_provider', 'alidns');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_dns01_cf_api_token', _('API token'));
o.depends('tls_dns01_provider', 'cloudflare');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Flag, 'tls_acme_dhc', _('Disable HTTP challenge'));
o.default = o.disabled;
o.depends('tls_dns01_challenge', '0');
o.modalonly = true;
o = s.option(form.Flag, 'tls_acme_dtac', _('Disable TLS ALPN challenge'));
o.default = o.disabled;
o.depends('tls_dns01_challenge', '0');
o.modalonly = true;
o = s.option(form.Value, 'tls_acme_ahp', _('Alternative HTTP port'),
_('The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a listener for the HTTP challenge.'));
o.datatype = 'port';
o.depends('tls_dns01_challenge', '0');
o.modalonly = true;
o = s.option(form.Value, 'tls_acme_atp', _('Alternative TLS port'),
_('The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to succeed.'));
o.datatype = 'port';
o.depends('tls_dns01_challenge', '0');
o.modalonly = true;
o = s.option(form.Flag, 'tls_acme_external_account', _('External Account Binding'),
_('EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known by the CA.' +
'<br/>External account bindings are "used to associate an ACME account with an existing account in a non-ACME system, such as a CA customer database.'));
o.default = o.disabled;
o.depends('tls_acme', '1');
o.modalonly = true;
o = s.option(form.Value, 'tls_acme_ea_keyid', _('External account key ID'));
o.depends('tls_acme_external_account', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_acme_ea_mackey', _('External account MAC key'));
o.depends('tls_acme_external_account', '1');
o.rmempty = false;
o.modalonly = true;
}
if (features.with_reality_server) {
o = s.option(form.Flag, 'tls_reality', _('REALITY'));
o.default = o.disabled;
o.depends({'tls': '1', 'tls_acme': '0', 'type': 'vless'});
o.depends({'tls': '1', 'tls_acme': null, 'type': 'vless'});
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_private_key', _('REALITY private key'));
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.DynamicList, 'tls_reality_short_id', _('REALITY short ID'));
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_max_time_difference', _('Max time difference'),
_('The maximum time difference between the server and the client.'));
o.depends('tls_reality', '1');
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_server_addr', _('Handshake server address'));
o.datatype = 'hostname';
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_server_port', _('Handshake server port'));
o.datatype = 'port';
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
}
o = s.option(form.Value, 'tls_cert_path', _('Certificate path'),
_('The server public key, in PEM format.'));
o.value('/etc/homeproxy/certs/server_publickey.pem');
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
o.validate = hp.validateCertificatePath;
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Button, '_upload_cert', _('Upload certificate'),
_('<strong>Save your configuration before uploading files!</strong>'));
o.inputstyle = 'action';
o.inputtitle = _('Upload...');
o.depends({'tls': '1', 'tls_cert_path': '/etc/homeproxy/certs/server_publickey.pem'});
o.onclick = L.bind(hp.uploadCertificate, this, _('certificate'), 'server_publickey');
o.modalonly = true;
o = s.option(form.Value, 'tls_key_path', _('Key path'),
_('The server private key, in PEM format.'));
o.value('/etc/homeproxy/certs/server_privatekey.pem');
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
o.validate = hp.validateCertificatePath;
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Button, '_upload_key', _('Upload key'),
_('<strong>Save your configuration before uploading files!</strong>'));
o.inputstyle = 'action';
o.inputtitle = _('Upload...');
o.depends({'tls': '1', 'tls_key_path': '/etc/homeproxy/certs/server_privatekey.pem'});
o.onclick = L.bind(hp.uploadCertificate, this, _('private key'), 'server_privatekey');
o.modalonly = true;
o = s.option(form.TextValue, 'tls_ech_key', _('ECH key'));
o.placeholder = '-----BEGIN ECH KEYS-----\nACBE2+piYBLrOywCbRYU+ZpEkk8keeBlUXbKqLRmQ/68FwBL/g0ARwAAIAAgn8HI\n93RfdV/LaDk+LC9H4h+4WhVBFmWKdhiT3vvpGi8ACAABAAEAAQADABRvdXRlci1z\nbmkuYW55LmRvbWFpbgAA\n-----END ECH KEYS-----';
o.monospace = true;
o.cols = 30
o.rows = 3;
o.hp_options = {
type: 'ech-keypair',
params: '',
result: {
ech_key: o.option,
ech_cfg: 'tls_ech_config'
}
}
o.renderWidget = function(section_id, option_index, cfgvalue) {
let node = form.TextValue.prototype.renderWidget.apply(this, arguments);
const cbid = this.cbid(section_id) + '._outer_sni';
node.appendChild(E('div', { 'class': 'control-group' }, [
E('input', {
id: cbid,
class: 'cbi-input-text',
style: 'width: 10em',
placeholder: 'outer-sni.any.domain'
}),
E('button', {
class: 'cbi-button cbi-button-add',
click: ui.createHandlerFn(this, function() {
this.hp_options.params = document.getElementById(cbid).value;
return handleGenKey.call(this, this.hp_options);
})
}, [ _('Generate') ])
]));
return node;
}
o.depends('tls', '1');
o.modalonly = true;
o = s.option(form.TextValue, 'tls_ech_config', _('ECH config'));
o.placeholder = '-----BEGIN ECH CONFIGS-----\nAEv+DQBHAAAgACCfwcj3dF91X8toOT4sL0fiH7haFUEWZYp2GJPe++kaLwAIAAEA\nAQABAAMAFG91dGVyLXNuaS5hbnkuZG9tYWluAAA=\n-----END ECH CONFIGS-----';
o.monospace = true;
o.cols = 30
o.rows = 3;
o.depends('tls', '1');
o.modalonly = true;
/* TLS config end */
/* Extra settings start */
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'),
_('Enable tcp fast open for listener.'));
o.default = o.disabled;
o.depends({'network': 'udp', '!reverse': true});
o.modalonly = true;
o = s.option(form.Flag, 'tcp_multi_path', _('MultiPath TCP'));
o.default = o.disabled;
o.depends({'network': 'udp', '!reverse': true});
o.modalonly = true;
o = s.option(form.Flag, 'udp_fragment', _('UDP Fragment'),
_('Enable UDP fragmentation.'));
o.default = o.disabled;
o.depends({'network': 'tcp', '!reverse': true});
o.modalonly = true;
o = s.option(form.Value, 'udp_timeout', _('UDP NAT expiration time'),
_('In seconds.'));
o.datatype = 'uinteger';
o.placeholder = '300';
o.depends({'network': 'tcp', '!reverse': true});
o.modalonly = true;
o = s.option(form.ListValue, 'network', _('Network'));
o.value('tcp', _('TCP'));
o.value('udp', _('UDP'));
o.value('', _('Both'));
o.depends('type', 'naive');
o.depends('type', 'shadowsocks');
o.modalonly = true;
/* Extra settings end */
return m.render();
}
});

View File

@ -0,0 +1,248 @@
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2022-2025 ImmortalWrt.org
*/
'use strict';
'require dom';
'require form';
'require fs';
'require poll';
'require rpc';
'require uci';
'require ui';
'require view';
/* Thanks to luci-app-aria2 */
const css = ' \
#log_textarea { \
padding: 10px; \
text-align: left; \
} \
#log_textarea pre { \
padding: .5rem; \
word-break: break-all; \
margin: 0; \
} \
.description { \
background-color: #33ccff; \
}';
const hp_dir = '/var/run/homeproxy';
function getConnStat(self, site) {
const callConnStat = rpc.declare({
object: 'luci.homeproxy',
method: 'connection_check',
params: ['site'],
expect: { '': {} }
});
self.default = E('div', { 'style': 'cbi-value-field' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, function() {
return L.resolveDefault(callConnStat(site), {}).then((ret) => {
let ele = self.default.firstElementChild.nextElementSibling;
if (ret.result) {
ele.style.setProperty('color', 'green');
ele.innerHTML = _('passed');
} else {
ele.style.setProperty('color', 'red');
ele.innerHTML = _('failed');
}
});
})
}, [ _('Check') ]),
' ',
E('strong', { 'style': 'color:gray' }, _('unchecked')),
]);
}
function getResVersion(self, type) {
const callResVersion = rpc.declare({
object: 'luci.homeproxy',
method: 'resources_get_version',
params: ['type'],
expect: { '': {} }
});
const callResUpdate = rpc.declare({
object: 'luci.homeproxy',
method: 'resources_update',
params: ['type'],
expect: { '': {} }
});
return L.resolveDefault(callResVersion(type), {}).then((res) => {
let spanTemp = E('div', { 'style': 'cbi-value-field' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, function() {
return L.resolveDefault(callResUpdate(type), {}).then((res) => {
switch (res.status) {
case 0:
self.description = _('Successfully updated.');
break;
case 1:
self.description = _('Update failed.');
break;
case 2:
self.description = _('Already in updating.');
break;
case 3:
self.description = _('Already at the latest version.');
break;
default:
self.description = _('Unknown error.');
break;
}
return self.map.reset();
});
})
}, [ _('Check update') ]),
' ',
E('strong', { 'style': (res.error ? 'color:red' : 'color:green') },
[ res.error ? 'not found' : res.version ]
),
]);
self.default = spanTemp;
});
}
function getRuntimeLog(name, filename) {
const callLogClean = rpc.declare({
object: 'luci.homeproxy',
method: 'log_clean',
params: ['type'],
expect: { '': {} }
});
let log_textarea = E('div', { 'id': 'log_textarea' },
E('img', {
'src': L.resource('icons/loading.svg'),
'alt': _('Loading'),
'style': 'vertical-align:middle'
}, _('Collecting data...'))
);
let log;
poll.add(L.bind(function() {
return fs.read_direct(String.format('%s/%s.log', hp_dir, filename), 'text')
.then(function(res) {
log = E('pre', { 'wrap': 'pre' }, [
res.trim() || _('Log is empty.')
]);
dom.content(log_textarea, log);
}).catch(function(err) {
if (err.toString().includes('NotFoundError'))
log = E('pre', { 'wrap': 'pre' }, [
_('Log file does not exist.')
]);
else
log = E('pre', { 'wrap': 'pre' }, [
_('Unknown error: %s').format(err)
]);
dom.content(log_textarea, log);
});
}));
return E([
E('style', [ css ]),
E('div', {'class': 'cbi-map'}, [
E('h3', {'name': 'content'}, [
_('%s log').format(name),
' ',
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, function() {
return L.resolveDefault(callLogClean(filename), {});
})
}, [ _('Clean log') ])
]),
E('div', {'class': 'cbi-section'}, [
log_textarea,
E('div', {'style': 'text-align:right'},
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
)
])
])
]);
}
return view.extend({
render() {
let m, s, o;
m = new form.Map('homeproxy');
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Connection check'));
s.anonymous = true;
o = s.option(form.DummyValue, '_check_baidu', _('BaiDu'));
o.cfgvalue = function() { return getConnStat(this, 'baidu') };
o = s.option(form.DummyValue, '_check_google', _('Google'));
o.cfgvalue = function() { return getConnStat(this, 'google') };
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
s.anonymous = true;
o = s.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version'));
o.cfgvalue = function() { return getResVersion(this, 'china_ip4') };
o.rawhtml = true;
o = s.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version'));
o.cfgvalue = function() { return getResVersion(this, 'china_ip6') };
o.rawhtml = true;
o = s.option(form.DummyValue, '_china_list_version', _('China list version'));
o.cfgvalue = function() { return getResVersion(this, 'china_list') };
o.rawhtml = true;
o = s.option(form.DummyValue, '_gfw_list_version', _('GFW list version'));
o.cfgvalue = function() { return getResVersion(this, 'gfw_list') };
o.rawhtml = true;
o = s.option(form.Value, 'github_token', _('GitHub token'));
o.password = true;
o.renderWidget = function() {
let node = form.Value.prototype.renderWidget.apply(this, arguments);
(node.querySelector('.control-group') || node).appendChild(E('button', {
'class': 'cbi-button cbi-button-apply',
'title': _('Save'),
'click': ui.createHandlerFn(this, function() {
ui.changes.apply(true);
return this.map.save(null, true);
}, this.option)
}, [ _('Save') ]));
return node;
}
s = m.section(form.NamedSection, 'config', 'homeproxy');
s.anonymous = true;
o = s.option(form.DummyValue, '_homeproxy_logview');
o.render = L.bind(getRuntimeLog, this, _('HomeProxy'), 'homeproxy');
o = s.option(form.DummyValue, '_sing-box-c_logview');
o.render = L.bind(getRuntimeLog, this, _('sing-box client'), 'sing-box-c');
o = s.option(form.DummyValue, '_sing-box-s_logview');
o.render = L.bind(getRuntimeLog, this, _('sing-box server'), 'sing-box-s');
return m.render();
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
{
"bounding": [
"CAP_NET_ADMIN",
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW",
"CAP_SYS_PTRACE"
],
"effective": [
"CAP_NET_ADMIN",
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW",
"CAP_SYS_PTRACE"
],
"ambient": [
"CAP_NET_ADMIN",
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW",
"CAP_SYS_PTRACE"
],
"permitted": [
"CAP_NET_ADMIN",
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW",
"CAP_SYS_PTRACE"
],
"inheritable": [
"CAP_NET_ADMIN",
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW",
"CAP_SYS_PTRACE"
]
}

View File

@ -0,0 +1,78 @@
config homeproxy 'infra'
option __warning 'DO NOT EDIT THIS SECTION, OR YOU ARE ON YOUR OWN!'
option common_port '22,53,80,143,443,465,853,873,993,995,5222,8080,8443,9418'
option mixed_port '5330'
option redirect_port '5331'
option tproxy_port '5332'
option dns_port '5333'
option ntp_server 'time.apple.com'
option sniff_override '1'
option udp_timeout ''
option tun_name 'singtun0'
option tun_addr4 '172.19.0.1/30'
option tun_addr6 'fdfe:dcba:9876::1/126'
option tun_mtu '9000'
option tun_gso '0'
option table_mark '100'
option self_mark '100'
option tproxy_mark '101'
option tun_mark '102'
config homeproxy 'config'
option main_node 'nil'
option main_udp_node 'same'
option dns_server '8.8.8.8'
option routing_mode 'bypass_mainland_china'
option routing_port 'common'
option proxy_mode 'redirect_tproxy'
option ipv6_support '1'
option github_token ''
config homeproxy 'control'
option lan_proxy_mode 'disabled'
list wan_proxy_ipv4_ips '91.105.192.0/23'
list wan_proxy_ipv4_ips '91.108.4.0/22'
list wan_proxy_ipv4_ips '91.108.8.0/22'
list wan_proxy_ipv4_ips '91.108.16.0/22'
list wan_proxy_ipv4_ips '91.108.12.0/22'
list wan_proxy_ipv4_ips '91.108.20.0/22'
list wan_proxy_ipv4_ips '91.108.56.0/22'
list wan_proxy_ipv4_ips '149.154.160.0/20'
list wan_proxy_ipv4_ips '185.76.151.0/24'
list wan_proxy_ipv4_ips '203.208.50.66/32'
list wan_proxy_ipv6_ips '2001:67c:4e8::/48'
list wan_proxy_ipv6_ips '2001:b28:f23c::/48'
list wan_proxy_ipv6_ips '2001:b28:f23d::/48'
list wan_proxy_ipv6_ips '2001:b28:f23f::/48'
list wan_proxy_ipv6_ips '2a0a:f280::/32'
config homeproxy 'routing'
option sniff_override '1'
option default_outbound 'direct-out'
config homeproxy 'dns'
option dns_strategy 'prefer_ipv4'
option default_server 'local-dns'
option disable_cache '0'
option disable_cache_expire '0'
config homeproxy 'subscription'
option auto_update '0'
option allow_insecure '0'
option packet_encoding 'xudp'
option update_via_proxy '0'
option filter_nodes 'blacklist'
list filter_keywords '重置|到期|过期|剩余|套餐'
list filter_keywords 'Expiration|Remaining'
config homeproxy 'server'
option enabled '0'
config dns_rule 'nodes_domain'
option label 'NodesDomain'
option enabled '1'
option mode 'default'
list outbound 'any-out'
option server 'default-dns'

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
20250713035522

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
20250713035522

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
202507122213

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
202507122213

View File

@ -0,0 +1,19 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2022-2023 ImmortalWrt.org
NAME="homeproxy"
log_max_size="10" #KB
main_log_file="/var/run/$NAME/$NAME.log"
singc_log_file="/var/run/$NAME/sing-box-c.log"
sings_log_file="/var/run/$NAME/sing-box-s.log"
while true; do
sleep 180
for i in "$main_log_file" "$singc_log_file" "$sings_log_file"; do
[ -s "$i" ] || continue
[ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i"
done
done

View File

@ -0,0 +1,664 @@
#!/usr/bin/utpl -S
{%-
import { readfile } from 'fs';
import { cursor } from 'uci';
import { isEmpty } from '/etc/homeproxy/scripts/homeproxy.uc';
const fw4 = require('fw4');
function array_to_nftarr(array) {
if (type(array) !== 'array')
return null;
return `{ ${join(', ', uniq(array))} }`;
}
function resolve_ipv6(str) {
if (isEmpty(str))
return null;
let ipv6 = fw4.parse_subnet(str)?.[0];
if (!ipv6 || ipv6.family !== 6)
return null;
if (ipv6.bits > -1)
return `${ipv6.addr}/${ipv6.bits}`;
else
return `& ${ipv6.mask} == ${ipv6.addr}`;
}
function resolve_mark(str) {
if (isEmpty(str))
return null;
let mark = fw4.parse_mark(str);
if (isEmpty(mark))
return null;
if (mark.mask === 0xffffffff)
return fw4.hex(mark.mark);
else if (mark.mark === 0)
return `mark and ${fw4.hex(~mark.mask & 0xffffffff)}`;
else if (mark.mark === mark.mask)
return `mark or ${fw4.hex(mark.mark)}`;
else if (mark.mask === 0)
return `mark xor ${fw4.hex(mark.mark)}`;
else
return `mark and ${fw4.hex(~mark.mask & 0xffffffff)} xor ${fw4.hex(mark.mark)}`;
}
/* Misc config */
const resources_dir = '/etc/homeproxy/resources';
/* UCI config start */
const cfgname = 'homeproxy';
const uci = cursor();
uci.load(cfgname);
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china';
let outbound_node, outbound_udp_node, china_dns_server, bypass_cn_traffic;
if (routing_mode !== 'custom') {
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
outbound_udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil';
china_dns_server = uci.get(cfgname, 'config', 'china_dns_server');
} else {
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
bypass_cn_traffic = uci.get(cfgname, 'routing', 'bypass_cn_traffic') || '0';
}
let routing_port = uci.get(cfgname, 'config', 'routing_port');
if (routing_port === 'common')
routing_port = uci.get(cfgname, 'infra', 'common_port') || '22,53,80,143,443,465,587,853,873,993,995,8080,8443,9418';
const proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy',
ipv6_support = uci.get(cfgname, 'config', 'ipv6_support') || '0';
let self_mark, redirect_port,
tproxy_port, tproxy_mark,
tun_name, tun_mark;
if (match(proxy_mode, /redirect/)) {
self_mark = uci.get(cfgname, 'infra', 'self_mark') || '100';
redirect_port = uci.get(cfgname, 'infra', 'redirect_port') || '5331';
}
if (match(proxy_mode, /tproxy/))
if (outbound_udp_node !== 'nil' || routing_mode === 'custom') {
tproxy_port = uci.get(cfgname, 'infra', 'tproxy_port') || '5332';
tproxy_mark = resolve_mark(uci.get(cfgname, 'infra', 'tproxy_mark') || '101');
}
if (match(proxy_mode, /tun/)) {
tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
tun_mark = resolve_mark(uci.get(cfgname, 'infra', 'tun_mark') || '102');
}
const control_options = [
"listen_interfaces", "lan_proxy_mode",
"lan_direct_mac_addrs", "lan_direct_ipv4_ips", "lan_direct_ipv6_ips",
"lan_proxy_mac_addrs", "lan_proxy_ipv4_ips", "lan_proxy_ipv6_ips",
"lan_gaming_mode_mac_addrs", "lan_gaming_mode_ipv4_ips", "lan_gaming_mode_ipv6_ips",
"lan_global_proxy_mac_addrs", "lan_global_proxy_ipv4_ips", "lan_global_proxy_ipv6_ips",
"wan_proxy_ipv4_ips", "wan_proxy_ipv6_ips",
"wan_direct_ipv4_ips", "wan_direct_ipv6_ips"
];
const control_info = {};
for (let i in control_options)
control_info[i] = uci.get(cfgname, 'control', i);
const dns_hijacked = uci.get('dhcp', '@dnsmasq[0]', 'dns_redirect') || '0',
dns_port = uci.get('dhcp', '@dnsmasq[0]', 'port') || '53';
/* UCI config end */
-%}
{# Reserved addresses -#}
set homeproxy_local_addr_v4 {
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.0.0.0/24,
192.0.2.0/24,
192.31.196.0/24,
192.52.193.0/24,
192.88.99.0/24,
192.168.0.0/16,
192.175.48.0/24,
198.18.0.0/15,
198.51.100.0/24,
203.0.113.0/24,
224.0.0.0/4,
240.0.0.0/4
}
}
{% if (ipv6_support === '1'): %}
set homeproxy_local_addr_v6 {
type ipv6_addr
flags interval
auto-merge
elements = {
::/128,
::1/128,
::ffff:0:0/96,
100::/64,
64:ff9b::/96,
2001::/32,
2001:10::/28,
2001:20::/28,
2001:db8::/28,
2002::/16,
fc00::/7,
fe80::/10,
ff00::/8
}
}
{% endif %}
{% if (routing_mode === 'gfwlist'): %}
set homeproxy_gfw_list_v4 {
type ipv4_addr
flags interval
auto-merge
}
{% if (ipv6_support === '1'): %}
set homeproxy_gfw_list_v6 {
type ipv6_addr
flags interval
auto-merge
}
{% endif /* ipv6_support */ %}
{% elif (match(routing_mode, /mainland_china/) || bypass_cn_traffic === '1'): %}
set homeproxy_mainland_addr_v4 {
type ipv4_addr
flags interval
auto-merge
elements = {
{% for (let cnip4 in split(trim(readfile(resources_dir + '/china_ip4.txt')), /[\r\n]/)): %}
{{ cnip4 }},
{% endfor %}
}
}
{% if ((ipv6_support === '1') || china_dns_server): %}
set homeproxy_mainland_addr_v6 {
type ipv6_addr
flags interval
auto-merge
elements = {
{% for (let cnip6 in split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/)): %}
{{ cnip6 }},
{% endfor %}
}
}
{% endif /* ipv6_support */ %}
{% endif /* routing_mode */ %}
{# WAN ACL addresses #}
set homeproxy_wan_proxy_addr_v4 {
type ipv4_addr
flags interval
auto-merge
{% if (control_info.wan_proxy_ipv4_ips): %}
elements = { {{ join(', ', control_info.wan_proxy_ipv4_ips) }} }
{% endif %}
}
{% if (ipv6_support === '1'): %}
set homeproxy_wan_proxy_addr_v6 {
type ipv6_addr
flags interval
auto-merge
{% if (control_info.wan_proxy_ipv6_ips): %}
elements = { {{ join(', ', control_info.wan_proxy_ipv6_ips) }} }
{% endif /* wan_proxy_ipv6_ips*/ %}
}
{% endif /* ipv6_support */ %}
set homeproxy_wan_direct_addr_v4 {
type ipv4_addr
flags interval
auto-merge
{% if (control_info.wan_direct_ipv4_ips): %}
elements = { {{ join(', ', control_info.wan_direct_ipv4_ips) }} }
{% endif %}
}
{% if (ipv6_support === '1'): %}
set homeproxy_wan_direct_addr_v6 {
type ipv6_addr
flags interval
auto-merge
{% if (control_info.wan_direct_ipv6_ips): %}
elements = { {{ join(', ', control_info.wan_direct_ipv6_ips) }} }
{% endif /* wan_direct_ipv6_ips */ %}
}
{% endif /* ipv6_support */ %}
{% if (routing_port): %}
set homeproxy_routing_port {
type inet_service
flags interval
auto-merge
elements = { {{ join(', ', split(routing_port, ',')) }} }
}
{% endif %}
{# DNS hijack & TCP redirect #}
chain dstnat {
{% if (dns_hijacked !== '1'): %}
{% if (control_info.listen_interfaces): %}
meta iifname {{ array_to_nftarr(control_info.listen_interfaces) }}
{%- endif /* listen_interfaces */ %}
meta nfproto { ipv4, ipv6 } udp dport 53 counter redirect to :{{ dns_port }} comment "!{{ cfgname }}: DNS hijack"
{% endif /* dns_hijacked */ %}
{% if (match(proxy_mode, /redirect/)): %}
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect_lanac
{% endif /* proxy_mode */ %}
}
{# TCP redirect #}
{% if (match(proxy_mode, /redirect/)): %}
chain homeproxy_redirect_proxy {
meta l4proto tcp counter redirect to :{{ redirect_port }}
}
chain homeproxy_redirect_proxy_port {
{% if (routing_port): %}
tcp dport != @homeproxy_routing_port counter return
{% endif %}
goto homeproxy_redirect_proxy
}
chain homeproxy_redirect_lanac {
{% if (control_info.listen_interfaces): %}
meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return
{% endif %}
meta mark {{ self_mark }} counter return
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_redirect
{% endif /* lan_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect
{% endfor /* lan_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_redirect
{% endif /* lan_proxy_mac_addrs */ %}
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
{% endif /* lan_direct_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
{% endfor /* lan_direct_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
{% endif /* lan_direct_mac_addrs */ %}
{% endif /* lan_proxy_mode */ %}
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
counter goto homeproxy_redirect
{% endif %}
}
chain homeproxy_redirect {
meta mark {{ self_mark }} counter return
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_redirect_proxy_port
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_redirect_proxy_port
{% endif %}
ip daddr @homeproxy_local_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_local_addr_v6 counter return
{% endif %}
{% if (routing_mode !== 'custom'): %}
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_redirect_proxy_port
{% endif /* lan_global_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy_port
{% endfor /* lan_global_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_redirect_proxy_port
{% endif /* lan_global_proxy_mac_addrs */ %}
{% endif /* routing_mode */ %}
ip daddr @homeproxy_wan_direct_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% if (routing_mode === 'gfwlist'): %}
ip daddr != @homeproxy_gfw_list_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr != @homeproxy_gfw_list_v6 counter return
{% endif /* ipv6_support */ %}
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
ip daddr @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% elif (routing_mode === 'proxy_mainland_china'): %}
ip daddr != @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* routing_mode */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_redirect_proxy
{% endif /* lan_gaming_mode_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_redirect_proxy
{% endif /* lan_gaming_mode_mac_addrs */ %}
counter goto homeproxy_redirect_proxy_port
}
chain homeproxy_output_redir {
type nat hook output priority filter -105; policy accept
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect
}
{% endif %}
{# UDP tproxy #}
{% if (match(proxy_mode, /tproxy/) && (outbound_udp_node !== 'nil' || routing_mode === 'custom')): %}
chain homeproxy_mangle_tproxy {
meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip to 127.0.0.1:{{ tproxy_port }} counter accept
{% if (ipv6_support === '1'): %}
meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip6 to [::1]:{{ tproxy_port }} counter accept
{% endif %}
}
chain homeproxy_mangle_tproxy_port {
{% if (routing_port): %}
udp dport != @homeproxy_routing_port counter return
{% endif %}
goto homeproxy_mangle_tproxy
}
chain homeproxy_mangle_mark {
{% if (routing_port): %}
udp dport != @homeproxy_routing_port counter return
{% endif %}
meta l4proto udp meta mark set {{ tproxy_mark }} counter accept
}
chain homeproxy_mangle_lanac {
{% if (control_info.listen_interfaces): %}
meta iifname != {{ array_to_nftarr(uniq([...control_info.listen_interfaces, ...['lo']])) }} counter return
{% endif %}
meta iifname != lo udp dport 53 counter return
meta mark {{ self_mark }} counter return
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_prerouting
{% endif /* lan_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_prerouting
{% endfor /* lan_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_prerouting
{% endif /* lan_proxy_mac_addrs */ %}
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
{% endif /* lan_direct_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
{% endfor /* lan_direct_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
{% endif /* lan_direct_mac_addrs */ %}
{% endif /* lan_proxy_mode */ %}
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
counter goto homeproxy_mangle_prerouting
{% endif %}
}
chain homeproxy_mangle_prerouting {
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tproxy_port
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tproxy_port
{% endif %}
ip daddr @homeproxy_local_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_local_addr_v6 counter return
{% endif %}
{% if (routing_mode !== 'custom'): %}
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tproxy_port
{% endif /* lan_global_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy_port
{% endfor /* lan_global_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tproxy_port
{% endif /* lan_global_proxy_mac_addrs */ %}
{% endif /* routing_mode */ %}
ip daddr @homeproxy_wan_direct_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% if (routing_mode === 'gfwlist'): %}
ip daddr != @homeproxy_gfw_list_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr != @homeproxy_gfw_list_v6 counter return
{% endif /* ipv6_support */ %}
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
ip daddr @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% if (routing_mode !== 'custom'): %}
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
{% endif /* routing_mode */ %}
{% elif (routing_mode === 'proxy_mainland_china'): %}
ip daddr != @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* routing_mode */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_mangle_tproxy
{% endif /* lan_gaming_mode_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_mangle_tproxy
{% endif /* lan_gaming_mode_mac_addrs */ %}
counter goto homeproxy_mangle_tproxy_port
}
chain homeproxy_mangle_output {
meta mark {{ self_mark }} counter return
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_mark
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_mark
{% endif %}
ip daddr @homeproxy_local_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_local_addr_v6 counter return
{% endif %}
ip daddr @homeproxy_wan_direct_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% if (routing_mode === 'gfwlist'): %}
ip daddr != @homeproxy_gfw_list_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr != @homeproxy_gfw_list_v6 counter return
{% endif /* ipv6_support */ %}
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
ip daddr @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% elif (routing_mode === 'proxy_mainland_china'): %}
ip daddr != @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* routing_mode */ %}
counter goto homeproxy_mangle_mark
}
chain mangle_prerouting {
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_lanac
}
chain mangle_output {
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output
}
{% endif %}
{# TUN #}
{% if (match(proxy_mode, /tun/)): %}
chain homeproxy_mangle_lanac {
iifname {{ tun_name }} counter return
udp dport 53 counter return
{% if (control_info.listen_interfaces): %}
meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return
{% endif %}
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun
{% endif /* lan_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun
{% endfor /* lan_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun
{% endif /* lan_proxy_mac_addrs */ %}
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
{% endif /* lan_direct_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
{% endfor /* lan_direct_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
{% endif /* lan_direct_mac_addrs */ %}
{% endif /* lan_proxy_mode */ %}
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
counter goto homeproxy_mangle_tun
{% endif %}
}
chain homeproxy_mangle_tun_mark {
{% if (routing_port): %}
{% if (proxy_mode === 'tun'): %}
tcp dport != @homeproxy_routing_port counter return
{% endif /* proxy_mode */ %}
udp dport != @homeproxy_routing_port counter return
{% endif /* routing_port */ %}
counter meta mark set {{ tun_mark }}
}
chain homeproxy_mangle_tun {
iifname {{ tun_name }} counter return
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tun_mark
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tun_mark
{% endif %}
ip daddr @homeproxy_local_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_local_addr_v6 counter return
{% endif %}
{% if (routing_mode !== 'custom'): %}
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun_mark
{% endif /* lan_global_proxy_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun_mark
{% endfor /* lan_global_proxy_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun_mark
{% endif /* lan_global_proxy_mac_addrs */ %}
{% endif /* routing_mode */ %}
{% if (control_info.wan_direct_ipv4_ips): %}
ip daddr {{ array_to_nftarr(control_info.wan_direct_ipv4_ips) }} counter return
{% endif /* wan_direct_ipv4_ips */ %}
{% if (control_info.wan_direct_ipv6_ips): %}
ip6 daddr {{ array_to_nftarr(control_info.wan_direct_ipv6_ips) }} counter return
{% endif /* wan_direct_ipv6_ips */ %}
{% if (routing_mode === 'gfwlist'): %}
ip daddr != @homeproxy_gfw_list_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr != @homeproxy_gfw_list_v6 counter return
{% endif /* ipv6_support */ %}
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
ip daddr @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% if (routing_mode !== 'custom'): %}
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
{% endif /* routing_mode */ %}
{% elif (routing_mode === 'proxy_mainland_china'): %}
ip daddr != @homeproxy_mainland_addr_v4 counter return
{% if (ipv6_support === '1'): %}
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
{% endif /* ipv6_support */ %}
{% endif /* routing_mode */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter meta mark set {{ tun_mark }}
{% endif /* lan_gaming_mode_ipv4_ips */ %}
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
ip6 saddr {{ resolve_ipv6(ipv6) }} counter meta mark set {{ tun_mark }}
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter meta mark set {{ tun_mark }}
{% endif /* lan_gaming_mode_mac_addrs */ %}
counter goto homeproxy_mangle_tun_mark
}
chain mangle_prerouting {
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_lanac
}
chain mangle_output {
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_tun
}
{% endif %}

View File

@ -0,0 +1,51 @@
#!/usr/bin/ucode
'use strict';
import { writefile } from 'fs';
import { cursor } from 'uci';
import { isEmpty, RUN_DIR } from 'homeproxy';
const cfgname = 'homeproxy';
const uci = cursor();
uci.load(cfgname);
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china',
proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy';
let outbound_node, tun_name;
if (match(proxy_mode, /tun/)) {
if (routing_mode === 'custom')
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
else
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
if (outbound_node !== 'nil')
tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
}
const server_enabled = uci.get(cfgname, 'server', 'enabled');
let forward = [],
input = [];
if (tun_name) {
push(forward, `oifname ${tun_name} counter accept comment "!${cfgname}: accept tun forward"`);
push(input ,`iifname ${tun_name} counter accept comment "!${cfgname}: accept tun input"`);
}
if (server_enabled === '1') {
uci.foreach(cfgname, 'server', (s) => {
if (s.enabled !== '1' || s.firewall !== '1')
return;
let proto = s.network || '{ tcp, udp }';
push(input, `meta l4proto ${proto} th dport ${s.port} counter accept comment "!${cfgname}: accept server ${s['.name']}"`);
});
}
if (!isEmpty(forward))
writefile(RUN_DIR + '/fw4_forward.nft', join('\n', forward) + '\n');
if (!isEmpty(input))
writefile(RUN_DIR + '/fw4_input.nft', join('\n', input) + '\n');

View File

@ -0,0 +1,925 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2023-2025 ImmortalWrt.org
*/
'use strict';
import { readfile, writefile } from 'fs';
import { isnan } from 'math';
import { connect } from 'ubus';
import { cursor } from 'uci';
import {
isEmpty, strToBool, strToInt,
removeBlankAttrs, validation,
HP_DIR, RUN_DIR
} from 'homeproxy';
const ubus = connect();
/* const features = ubus.call('luci.homeproxy', 'singbox_get_features') || {}; */
/* UCI config start */
const uci = cursor();
const uciconfig = 'homeproxy';
uci.load(uciconfig);
const uciinfra = 'infra',
ucimain = 'config',
ucicontrol = 'control';
const ucidnssetting = 'dns',
ucidnsserver = 'dns_server',
ucidnsrule = 'dns_rule';
const uciroutingsetting = 'routing',
uciroutingnode = 'routing_node',
uciroutingrule = 'routing_rule';
const ucinode = 'node';
const uciruleset = 'ruleset';
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
let wan_dns = ubus.call('network.interface', 'status', {'interface': 'wan'})?.['dns-server']?.[0];
if (!wan_dns)
wan_dns = (routing_mode in ['proxy_mainland_china', 'global']) ? '8.8.8.8' : '223.5.5.5';
const dns_port = uci.get(uciconfig, uciinfra, 'dns_port') || '5333';
const ntp_server = uci.get(uciconfig, uciinfra, 'ntp_server') || 'time.apple.com';
let main_node, main_udp_node, dedicated_udp_node, default_outbound, domain_strategy, sniff_override,
dns_server, china_dns_server, dns_default_strategy, dns_default_server, dns_disable_cache,
dns_disable_cache_expire, dns_independent_cache, dns_client_subnet, cache_file_store_rdrc,
cache_file_rdrc_timeout, direct_domain_list, proxy_domain_list;
if (routing_mode !== 'custom') {
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
dedicated_udp_node = !isEmpty(main_udp_node) && !(main_udp_node in ['same', main_node]);
dns_server = uci.get(uciconfig, ucimain, 'dns_server');
if (isEmpty(dns_server) || dns_server === 'wan')
dns_server = wan_dns;
if (routing_mode === 'bypass_mainland_china') {
china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
if (isEmpty(china_dns_server) || type(china_dns_server) !== 'string' || china_dns_server === 'wan')
china_dns_server = wan_dns;
}
direct_domain_list = trim(readfile(HP_DIR + '/resources/direct_list.txt'));
if (direct_domain_list)
direct_domain_list = split(direct_domain_list, /[\r\n]/);
proxy_domain_list = trim(readfile(HP_DIR + '/resources/proxy_list.txt'));
if (proxy_domain_list)
proxy_domain_list = split(proxy_domain_list, /[\r\n]/);
sniff_override = uci.get(uciconfig, uciinfra, 'sniff_override') || '1';
} else {
/* DNS settings */
dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_strategy');
dns_default_server = uci.get(uciconfig, ucidnssetting, 'default_server');
dns_disable_cache = uci.get(uciconfig, ucidnssetting, 'disable_cache');
dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire');
dns_independent_cache = uci.get(uciconfig, ucidnssetting, 'independent_cache');
dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet');
cache_file_store_rdrc = uci.get(uciconfig, ucidnssetting, 'cache_file_store_rdrc'),
cache_file_rdrc_timeout = uci.get(uciconfig, ucidnssetting, 'cache_file_rdrc_timeout');
/* Routing settings */
default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil';
domain_strategy = uci.get(uciconfig, uciroutingsetting, 'domain_strategy');
sniff_override = uci.get(uciconfig, uciroutingsetting, 'sniff_override');
}
const proxy_mode = uci.get(uciconfig, ucimain, 'proxy_mode') || 'redirect_tproxy',
ipv6_support = uci.get(uciconfig, ucimain, 'ipv6_support') || '0',
default_interface = uci.get(uciconfig, ucicontrol, 'bind_interface');
const mixed_port = uci.get(uciconfig, uciinfra, 'mixed_port') || '5330';
let self_mark, redirect_port, tproxy_port,
tun_name, tun_addr4, tun_addr6, tun_mtu, tun_gso,
tcpip_stack, endpoint_independent_nat, udp_timeout;
udp_timeout = uci.get(uciconfig, 'infra', 'udp_timeout');
if (routing_mode === 'custom')
udp_timeout = uci.get(uciconfig, uciroutingsetting, 'udp_timeout');
if (match(proxy_mode, /redirect/)) {
self_mark = uci.get(uciconfig, 'infra', 'self_mark') || '100';
redirect_port = uci.get(uciconfig, 'infra', 'redirect_port') || '5331';
}
if (match(proxy_mode), /tproxy/)
if (main_udp_node !== 'nil' || routing_mode === 'custom')
tproxy_port = uci.get(uciconfig, 'infra', 'tproxy_port') || '5332';
if (match(proxy_mode), /tun/) {
tun_name = uci.get(uciconfig, uciinfra, 'tun_name') || 'singtun0';
tun_addr4 = uci.get(uciconfig, uciinfra, 'tun_addr4') || '172.19.0.1/30';
tun_addr6 = uci.get(uciconfig, uciinfra, 'tun_addr6') || 'fdfe:dcba:9876::1/126';
tun_mtu = uci.get(uciconfig, uciinfra, 'tun_mtu') || '9000';
tun_gso = uci.get(uciconfig, uciinfra, 'tun_gso') || '0';
tcpip_stack = 'system';
if (routing_mode === 'custom') {
tun_gso = uci.get(uciconfig, uciroutingsetting, 'tun_gso') || '0';
tcpip_stack = uci.get(uciconfig, uciroutingsetting, 'tcpip_stack') || 'system';
endpoint_independent_nat = uci.get(uciconfig, uciroutingsetting, 'endpoint_independent_nat');
}
}
/* UCI config end */
/* Config helper start */
function parse_port(strport) {
if (type(strport) !== 'array' || isEmpty(strport))
return null;
let ports = [];
for (let i in strport)
push(ports, int(i));
return ports;
}
function parse_dnsquery(strquery) {
if (type(strquery) !== 'array' || isEmpty(strquery))
return null;
let querys = [];
for (let i in strquery)
isnan(int(i)) ? push(querys, i) : push(querys, int(i));
return querys;
}
function generate_endpoint(node) {
if (type(node) !== 'object' || isEmpty(node))
return null;
const endpoint = {
type: node.type,
tag: 'cfg-' + node['.name'] + '-out',
address: node.wireguard_local_address,
mtu: strToInt(node.wireguard_mtu),
private_key: node.wireguard_private_key,
peers: (node.type === 'wireguard') ? [
{
address: node.address,
port: strToInt(node.port),
allowed_ips: [
'0.0.0.0/0',
'::/0'
],
persistent_keepalive_interval: strToInt(node.wireguard_persistent_keepalive_interval),
public_key: node.wireguard_peer_public_key,
pre_shared_key: node.wireguard_pre_shared_key,
reserved: parse_port(node.wireguard_reserved),
}
] : null,
system: (node.type === 'wireguard') ? false : null,
tcp_fast_open: strToBool(node.tcp_fast_open),
tcp_multi_path: strToBool(node.tcp_multi_path),
udp_fragment: strToBool(node.udp_fragment)
};
return endpoint;
}
function generate_outbound(node) {
if (type(node) !== 'object' || isEmpty(node))
return null;
const outbound = {
type: node.type,
tag: 'cfg-' + node['.name'] + '-out',
routing_mark: strToInt(self_mark),
server: node.address,
server_port: strToInt(node.port),
/* Hysteria(2) */
server_ports: node.hysteria_hopping_port,
username: (node.type !== 'ssh') ? node.username : null,
user: (node.type === 'ssh') ? node.username : null,
password: node.password,
/* Direct */
override_address: node.override_address,
override_port: strToInt(node.override_port),
proxy_protocol: strToInt(node.proxy_protocol),
/* Hysteria (2) */
hop_interval: node.hysteria_hop_interval ? (node.hysteria_hop_interval + 's') : null,
up_mbps: strToInt(node.hysteria_up_mbps),
down_mbps: strToInt(node.hysteria_down_mbps),
obfs: node.hysteria_obfs_type ? {
type: node.hysteria_obfs_type,
password: node.hysteria_obfs_password
} : node.hysteria_obfs_password,
auth: (node.hysteria_auth_type === 'base64') ? node.hysteria_auth_payload : null,
auth_str: (node.hysteria_auth_type === 'string') ? node.hysteria_auth_payload : null,
recv_window_conn: strToInt(node.hysteria_recv_window_conn),
recv_window: strToInt(node.hysteria_revc_window),
disable_mtu_discovery: strToBool(node.hysteria_disable_mtu_discovery),
/* Shadowsocks */
method: node.shadowsocks_encrypt_method,
plugin: node.shadowsocks_plugin,
plugin_opts: node.shadowsocks_plugin_opts,
/* ShadowTLS / Socks */
version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : null),
/* SSH */
client_version: node.ssh_client_version,
host_key: node.ssh_host_key,
host_key_algorithms: node.ssh_host_key_algo,
private_key: node.ssh_priv_key,
private_key_passphrase: node.ssh_priv_key_pp,
/* Tuic */
uuid: node.uuid,
congestion_control: node.tuic_congestion_control,
udp_relay_mode: node.tuic_udp_relay_mode,
udp_over_stream: strToBool(node.tuic_udp_over_stream),
zero_rtt_handshake: strToBool(node.tuic_enable_zero_rtt),
heartbeat: node.tuic_heartbeat ? (node.tuic_heartbeat + 's') : null,
/* VLESS / VMess */
flow: node.vless_flow,
alter_id: strToInt(node.vmess_alterid),
security: node.vmess_encrypt,
global_padding: node.vmess_global_padding ? (node.vmess_global_padding === '1') : null,
authenticated_length: node.vmess_authenticated_length ? (node.vmess_authenticated_length === '1') : null,
packet_encoding: node.packet_encoding,
multiplex: (node.multiplex === '1') ? {
enabled: true,
protocol: node.multiplex_protocol,
max_connections: strToInt(node.multiplex_max_connections),
min_streams: strToInt(node.multiplex_min_streams),
max_streams: strToInt(node.multiplex_max_streams),
padding: (node.multiplex_padding === '1'),
brutal: (node.multiplex_brutal === '1') ? {
enabled: true,
up_mbps: strToInt(node.multiplex_brutal_up),
down_mbps: strToInt(node.multiplex_brutal_down)
} : null
} : null,
tls: (node.tls === '1') ? {
enabled: true,
server_name: node.tls_sni,
insecure: (node.tls_insecure === '1'),
alpn: node.tls_alpn,
min_version: node.tls_min_version,
max_version: node.tls_max_version,
cipher_suites: node.tls_cipher_suites,
certificate_path: node.tls_cert_path,
ech: (node.tls_ech === '1') ? {
enabled: true,
pq_signature_schemes_enabled: (node.tls_ech_enable_pqss === '1'),
config: node.tls_ech_config,
config_path: node.tls_ech_config_path
} : null,
utls: !isEmpty(node.tls_utls) ? {
enabled: true,
fingerprint: node.tls_utls
} : null,
reality: (node.tls_reality === '1') ? {
enabled: true,
public_key: node.tls_reality_public_key,
short_id: node.tls_reality_short_id
} : null
} : null,
transport: !isEmpty(node.transport) ? {
type: node.transport,
host: node.http_host || node.httpupgrade_host,
path: node.http_path || node.ws_path,
headers: node.ws_host ? {
Host: node.ws_host
} : null,
method: node.http_method,
max_early_data: strToInt(node.websocket_early_data),
early_data_header_name: node.websocket_early_data_header,
service_name: node.grpc_servicename,
idle_timeout: node.http_idle_timeout ? (node.http_idle_timeout + 's') : null,
ping_timeout: node.http_ping_timeout ? (node.http_ping_timeout + 's') : null,
permit_without_stream: strToBool(node.grpc_permit_without_stream)
} : null,
udp_over_tcp: (node.udp_over_tcp === '1') ? {
enabled: true,
version: strToInt(node.udp_over_tcp_version)
} : null,
tcp_fast_open: strToBool(node.tcp_fast_open),
tcp_multi_path: strToBool(node.tcp_multi_path),
udp_fragment: strToBool(node.udp_fragment)
};
return outbound;
}
function get_outbound(cfg) {
if (isEmpty(cfg))
return null;
if (type(cfg) === 'array') {
if ('any-out' in cfg)
return 'any';
let outbounds = [];
for (let i in cfg)
push(outbounds, get_outbound(i));
return outbounds;
} else {
switch (cfg) {
case 'block-out':
case 'direct-out':
return cfg;
default:
const node = uci.get(uciconfig, cfg, 'node');
if (isEmpty(node))
die(sprintf("%s's node is missing, please check your configuration.", cfg));
else if (node === 'urltest')
return 'cfg-' + cfg + '-out';
else
return 'cfg-' + node + '-out';
}
}
}
function get_resolver(cfg) {
if (isEmpty(cfg))
return null;
switch (cfg) {
case 'block-dns':
case 'default-dns':
case 'system-dns':
return cfg;
default:
return 'cfg-' + cfg + '-dns';
}
}
function get_ruleset(cfg) {
if (isEmpty(cfg))
return null;
let rules = [];
for (let i in cfg)
push(rules, isEmpty(i) ? null : 'cfg-' + i + '-rule');
return rules;
}
/* Config helper end */
const config = {};
/* Log */
config.log = {
disabled: false,
level: 'warn',
output: RUN_DIR + '/sing-box-c.log',
timestamp: true
};
/* NTP */
config.ntp = {
enabled: true,
server: ntp_server,
detour: 'direct-out',
/* TODO: disable this until we have sing-box 1.12 */
/* domain_resolver: 'default-dns', */
};
/* DNS start */
/* Default settings */
config.dns = {
servers: [
{
tag: 'default-dns',
address: wan_dns,
detour: 'direct-out'
},
{
tag: 'system-dns',
address: 'local',
detour: 'direct-out'
},
{
tag: 'block-dns',
address: 'rcode://refused'
}
],
rules: [
/* TODO: remove this once we have sing-box 1.12 */
/* NTP domain must be resolved by default DNS */
{
domain: ntp_server,
action: 'route',
server: 'default-dns'
}
],
strategy: dns_default_strategy,
disable_cache: (dns_disable_cache === '1'),
disable_expire: (dns_disable_cache_expire === '1'),
independent_cache: (dns_independent_cache === '1'),
client_subnet: dns_client_subnet
};
if (!isEmpty(main_node)) {
/* Main DNS */
push(config.dns.servers, {
tag: 'main-dns',
address: !match(dns_server, /:\/\//) ? 'tcp://' + (validation('ip6addr', dns_server) ? `[${dns_server}]` : dns_server) : dns_server,
strategy: (ipv6_support !== '1') ? 'ipv4_only' : null,
address_resolver: 'default-dns',
address_strategy: (ipv6_support !== '1') ? 'ipv4_only' : null,
detour: 'main-out'
});
config.dns.final = 'main-dns';
/* Avoid DNS loop */
push(config.dns.rules, {
outbound: 'any',
action: 'route',
server: 'default-dns'
});
if (length(direct_domain_list))
push(config.dns.rules, {
rule_set: 'direct-domain',
action: 'route',
server: (routing_mode === 'bypass_mainland_china' ) ? 'china-dns' : 'default-dns'
});
/* Filter out SVCB/HTTPS queries for "exquisite" Apple devices */
if (routing_mode === 'gfwlist' || length(proxy_domain_list))
push(config.dns.rules, {
rule_set: (routing_mode !== 'gfwlist') ? 'proxy-domain' : null,
query_type: [64, 65],
action: 'reject'
});
if (routing_mode === 'bypass_mainland_china') {
push(config.dns.servers, {
tag: 'china-dns',
address: china_dns_server,
address_resolver: 'default-dns',
detour: 'direct-out'
});
if (length(proxy_domain_list))
push(config.dns.rules, {
rule_set: 'proxy-domain',
action: 'route',
server: 'main-dns'
});
push(config.dns.rules, {
rule_set: 'geosite-cn',
action: 'route',
server: 'china-dns'
});
push(config.dns.rules, {
type: 'logical',
mode: 'and',
rules: [
{
rule_set: 'geosite-noncn',
invert: true
},
{
rule_set: 'geoip-cn'
}
],
action: 'route',
server: 'china-dns'
});
}
} else if (!isEmpty(default_outbound)) {
/* DNS servers */
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
if (cfg.enabled !== '1')
return;
push(config.dns.servers, {
tag: 'cfg-' + cfg['.name'] + '-dns',
address: cfg.address,
address: cfg.address,
address_resolver: get_resolver(cfg.address_resolver),
address_strategy: cfg.address_strategy,
strategy: cfg.resolve_strategy,
detour: get_outbound(cfg.outbound),
client_subnet: cfg.client_subnet
});
});
/* DNS rules */
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
if (cfg.enabled !== '1')
return;
push(config.dns.rules, {
ip_version: strToInt(cfg.ip_version),
query_type: parse_dnsquery(cfg.query_type),
network: cfg.network,
protocol: cfg.protocol,
domain: cfg.domain,
domain_suffix: cfg.domain_suffix,
domain_keyword: cfg.domain_keyword,
domain_regex: cfg.domain_regex,
port: parse_port(cfg.port),
port_range: cfg.port_range,
source_ip_cidr: cfg.source_ip_cidr,
source_ip_is_private: (cfg.source_ip_is_private === '1') || null,
ip_cidr: cfg.ip_cidr,
ip_is_private: (cfg.ip_is_private === '1') || null,
source_port: parse_port(cfg.source_port),
source_port_range: cfg.source_port_range,
process_name: cfg.process_name,
process_path: cfg.process_path,
process_path_regex: cfg.process_path_regex,
user: cfg.user,
rule_set: get_ruleset(cfg.rule_set),
rule_set_ip_cidr_match_source: (cfg.rule_set_ip_cidr_match_source === '1') || null,
invert: (cfg.invert === '1') || null,
outbound: get_outbound(cfg.outbound),
action: (cfg.server === 'block-dns') ? 'reject' : 'route',
server: get_resolver(cfg.server),
disable_cache: (cfg.dns_disable_cache === '1') || null,
rewrite_ttl: strToInt(cfg.rewrite_ttl),
client_subnet: cfg.client_subnet
});
});
if (isEmpty(config.dns.rules))
config.dns.rules = null;
config.dns.final = get_resolver(dns_default_server);
}
/* DNS end */
/* Inbound start */
config.inbounds = [];
push(config.inbounds, {
type: 'direct',
tag: 'dns-in',
listen: '::',
listen_port: int(dns_port)
});
push(config.inbounds, {
type: 'mixed',
tag: 'mixed-in',
listen: '::',
listen_port: int(mixed_port),
udp_timeout: udp_timeout ? (udp_timeout + 's') : null,
sniff: true,
sniff_override_destination: (sniff_override === '1'),
set_system_proxy: false
});
if (match(proxy_mode, /redirect/))
push(config.inbounds, {
type: 'redirect',
tag: 'redirect-in',
listen: '::',
listen_port: int(redirect_port),
sniff: true,
sniff_override_destination: (sniff_override === '1')
});
if (match(proxy_mode, /tproxy/))
push(config.inbounds, {
type: 'tproxy',
tag: 'tproxy-in',
listen: '::',
listen_port: int(tproxy_port),
network: 'udp',
udp_timeout: udp_timeout ? (udp_timeout + 's') : null,
sniff: true,
sniff_override_destination: (sniff_override === '1')
});
if (match(proxy_mode, /tun/))
push(config.inbounds, {
type: 'tun',
tag: 'tun-in',
interface_name: tun_name,
address: (ipv6_support === '1') ? [tun_addr4, tun_addr6] : [tun_addr4],
mtu: strToInt(tun_mtu),
gso: (tun_gso === '1'),
auto_route: false,
endpoint_independent_nat: strToBool(endpoint_independent_nat),
udp_timeout: udp_timeout ? (udp_timeout + 's') : null,
stack: tcpip_stack,
sniff: true,
sniff_override_destination: (sniff_override === '1')
});
/* Inbound end */
/* Outbound start */
config.endpoints = [];
/* Default outbounds */
config.outbounds = [
{
type: 'direct',
tag: 'direct-out',
routing_mark: strToInt(self_mark)
},
{
type: 'block',
tag: 'block-out'
}
];
/* Main outbounds */
if (!isEmpty(main_node)) {
let urltest_nodes = [];
if (main_node === 'urltest') {
const main_urltest_nodes = uci.get(uciconfig, ucimain, 'main_urltest_nodes') || [];
const main_urltest_interval = uci.get(uciconfig, ucimain, 'main_urltest_interval');
const main_urltest_tolerance = uci.get(uciconfig, ucimain, 'main_urltest_tolerance');
push(config.outbounds, {
type: 'urltest',
tag: 'main-out',
outbounds: map(main_urltest_nodes, (k) => `cfg-${k}-out`),
interval: main_urltest_interval ? (main_urltest_interval + 's') : null,
tolerance: strToInt(main_urltest_tolerance),
idle_timeout: (strToInt(main_urltest_interval) > 1800) ? `${main_urltest_interval * 2}s` : null,
});
urltest_nodes = main_urltest_nodes;
} else {
const main_node_cfg = uci.get_all(uciconfig, main_node) || {};
if (main_node_cfg.type === 'wireguard') {
push(config.endpoints, generate_endpoint(main_node_cfg));
config.endpoints[length(config.endpoints)-1].tag = 'main-out';
} else {
push(config.outbounds, generate_outbound(main_node_cfg));
config.outbounds[length(config.outbounds)-1].domain_strategy = (ipv6_support !== '1') ? 'prefer_ipv4' : null;
config.outbounds[length(config.outbounds)-1].tag = 'main-out';
}
}
if (main_udp_node === 'urltest') {
const main_udp_urltest_nodes = uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes') || [];
const main_udp_urltest_interval = uci.get(uciconfig, ucimain, 'main_udp_urltest_interval');
const main_udp_urltest_tolerance = uci.get(uciconfig, ucimain, 'main_udp_urltest_tolerance');
push(config.outbounds, {
type: 'urltest',
tag: 'main-udp-out',
outbounds: map(main_udp_urltest_nodes, (k) => `cfg-${k}-out`),
interval: main_udp_urltest_interval ? (main_udp_urltest_interval + 's') : null,
tolerance: strToInt(main_udp_urltest_tolerance),
idle_timeout: (strToInt(main_udp_urltest_interval) > 1800) ? `${main_udp_urltest_interval * 2}s` : null,
});
urltest_nodes = [...urltest_nodes, ...filter(main_udp_urltest_nodes, (l) => !~index(urltest_nodes, l))];
} else if (dedicated_udp_node) {
const main_udp_node_cfg = uci.get_all(uciconfig, main_udp_node) || {};
if (main_udp_node_cfg.type === 'wireguard') {
push(config.endpoints, generate_endpoint(main_udp_node_cfg));
config.endpoints[length(config.endpoints)-1].domain_strategy = (ipv6_support !== '1') ? 'prefer_ipv4' : null;
config.endpoints[length(config.endpoints)-1].tag = 'main-udp-out';
} else {
push(config.outbounds, generate_outbound(main_udp_node_cfg));
config.outbounds[length(config.outbounds)-1].domain_strategy = (ipv6_support !== '1') ? 'prefer_ipv4' : null;
config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out';
}
}
for (let i in urltest_nodes) {
const urltest_node = uci.get_all(uciconfig, i) || {};
if (urltest_node.type === 'wireguard') {
push(config.endpoints, generate_endpoint(urltest_node));
config.endpoints[length(config.endpoints)-1].domain_strategy = (ipv6_support !== '1') ? 'prefer_ipv4' : null;
config.endpoints[length(config.endpoints)-1].tag = 'cfg-' + i + '-out';
} else {
push(config.outbounds, generate_outbound(urltest_node));
config.outbounds[length(config.outbounds)-1].domain_strategy = (ipv6_support !== '1') ? 'prefer_ipv4' : null;
config.outbounds[length(config.outbounds)-1].tag = 'cfg-' + i + '-out';
}
}
} else if (!isEmpty(default_outbound)) {
let urltest_nodes = [],
routing_nodes = [];
uci.foreach(uciconfig, uciroutingnode, (cfg) => {
if (cfg.enabled !== '1')
return;
if (cfg.node === 'urltest') {
push(config.outbounds, {
type: 'urltest',
tag: 'cfg-' + cfg['.name'] + '-out',
outbounds: map(cfg.urltest_nodes, (k) => `cfg-${k}-out`),
url: cfg.urltest_url,
interval: cfg.urltest_interval ? (cfg.urltest_interval + 's') : null,
tolerance: strToInt(cfg.urltest_tolerance),
idle_timeout: cfg.urltest_idle_timeout ? (cfg.urltest_idle_timeout + 's') : null,
interrupt_exist_connections: (cfg.urltest_interrupt_exist_connections === '1')
});
urltest_nodes = [...urltest_nodes, ...filter(cfg.urltest_nodes, (l) => !~index(urltest_nodes, l))];
} else {
const outbound = uci.get_all(uciconfig, cfg.node) || {};
if (outbound.type === 'wireguard') {
push(config.endpoints, generate_endpoint(outbound));
config.endpoints[length(config.endpoints)-1].domain_strategy = cfg.domain_strategy;
config.endpoints[length(config.endpoints)-1].bind_interface = cfg.bind_interface;
config.endpoints[length(config.endpoints)-1].detour = get_outbound(cfg.outbound);
} else {
push(config.outbounds, generate_outbound(outbound));
config.outbounds[length(config.outbounds)-1].domain_strategy = cfg.domain_strategy;
config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface;
config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound);
}
push(routing_nodes, cfg.node);
}
});
for (let i in filter(urltest_nodes, (l) => !~index(routing_nodes, l))) {
const urltest_node = uci.get_all(uciconfig, i) || {};
if (urltest_node.type === 'wireguard')
push(config.endpoints, generate_endpoint(urltest_node));
else
push(config.outbounds, generate_outbound(urltest_node));
}
}
if (isEmpty(config.endpoints))
config.endpoints = null;
/* Outbound end */
/* Routing rules start */
/* Default settings */
config.route = {
rules: [
{
inbound: 'dns-in',
action: 'hijack-dns'
}
/*
* leave for sing-box 1.13.0
* {
* action: 'sniff'
* }
*/
],
rule_set: [],
auto_detect_interface: isEmpty(default_interface) ? true : null,
default_interface: default_interface
};
/* Routing rules */
if (!isEmpty(main_node)) {
/* Direct list */
if (length(direct_domain_list))
push(config.route.rules, {
rule_set: 'direct-domain',
action: 'route',
outbound: 'direct-out'
});
/* Main UDP out */
if (dedicated_udp_node)
push(config.route.rules, {
network: 'udp',
action: 'route',
outbound: 'main-udp-out'
});
config.route.final = 'main-out';
/* Rule set */
/* Direct list */
if (length(direct_domain_list))
push(config.route.rule_set, {
type: 'inline',
tag: 'direct-domain',
rules: [
{
domain_keyword: direct_domain_list,
}
]
});
/* Proxy list */
if (length(proxy_domain_list))
push(config.route.rule_set, {
type: 'inline',
tag: 'proxy-domain',
rules: [
{
domain_keyword: proxy_domain_list,
}
]
});
if (routing_mode === 'bypass_mainland_china') {
push(config.route.rule_set, {
type: 'remote',
tag: 'geoip-cn',
format: 'binary',
url: 'https://fastly.jsdelivr.net/gh/1715173329/IPCIDR-CHINA@rule-set/cn.srs',
download_detour: 'main-out'
});
push(config.route.rule_set, {
type: 'remote',
tag: 'geosite-cn',
format: 'binary',
url: 'https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-cn.srs',
download_detour: 'main-out'
});
push(config.route.rule_set, {
type: 'remote',
tag: 'geosite-noncn',
format: 'binary',
url: 'https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-!cn.srs',
download_detour: 'main-out'
});
}
if (isEmpty(config.route.rule_set))
config.route.rule_set = null;
} else if (!isEmpty(default_outbound)) {
if (domain_strategy)
push(config.route.rules, {
action: 'resolve',
strategy: domain_strategy
});
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
if (cfg.enabled !== '1')
return null;
push(config.route.rules, {
ip_version: strToInt(cfg.ip_version),
protocol: cfg.protocol,
network: cfg.network,
domain: cfg.domain,
domain_suffix: cfg.domain_suffix,
domain_keyword: cfg.domain_keyword,
domain_regex: cfg.domain_regex,
source_ip_cidr: cfg.source_ip_cidr,
source_ip_is_private: (cfg.source_ip_is_private === '1') || null,
ip_cidr: cfg.ip_cidr,
ip_is_private: (cfg.ip_is_private === '1') || null,
source_port: parse_port(cfg.source_port),
source_port_range: cfg.source_port_range,
port: parse_port(cfg.port),
port_range: cfg.port_range,
process_name: cfg.process_name,
process_path: cfg.process_path,
process_path_regex: cfg.process_path_regex,
user: cfg.user,
rule_set: get_ruleset(cfg.rule_set),
rule_set_ip_cidr_match_source: (cfg.rule_set_ip_cidr_match_source === '1') || null,
rule_set_ip_cidr_accept_empty: (cfg.rule_set_ip_cidr_accept_empty === '1') || null,
invert: (cfg.invert === '1') || null,
action: (cfg.outbound === 'block-out') ? 'reject' : 'route',
override_address: cfg.override_address,
override_port: strToInt(cfg.override_port),
outbound: get_outbound(cfg.outbound),
});
});
config.route.final = get_outbound(default_outbound);
/* Rule set */
uci.foreach(uciconfig, uciruleset, (cfg) => {
if (cfg.enabled !== '1')
return null;
push(config.route.rule_set, {
type: cfg.type,
tag: 'cfg-' + cfg['.name'] + '-rule',
format: cfg.format,
path: cfg.path,
url: cfg.url,
download_detour: get_outbound(cfg.outbound),
update_interval: cfg.update_interval
});
});
}
/* Routing rules end */
/* Experimental start */
if (routing_mode in ['bypass_mainland_china', 'custom']) {
config.experimental = {
cache_file: {
enabled: true,
path: RUN_DIR + '/cache.db',
store_rdrc: (cache_file_store_rdrc === '1') || null,
rdrc_timeout: cache_file_rdrc_timeout ? (cache_file_rdrc_timeout + 's') : null,
}
};
}
/* Experimental end */
system('mkdir -p ' + RUN_DIR);
writefile(RUN_DIR + '/sing-box-c.json', sprintf('%.J\n', removeBlankAttrs(config)));

View File

@ -0,0 +1,177 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2023 ImmortalWrt.org
*/
'use strict';
import { writefile } from 'fs';
import { cursor } from 'uci';
import {
isEmpty, strToBool, strToInt,
removeBlankAttrs, HP_DIR, RUN_DIR
} from 'homeproxy';
/* UCI config start */
const uci = cursor();
const uciconfig = 'homeproxy';
uci.load(uciconfig);
const uciserver = 'server';
const config = {};
/* Log */
config.log = {
disabled: false,
level: 'warn',
output: RUN_DIR + '/sing-box-s.log',
timestamp: true
};
config.inbounds = [];
uci.foreach(uciconfig, uciserver, (cfg) => {
if (cfg.enabled !== '1')
return;
push(config.inbounds, {
type: cfg.type,
tag: 'cfg-' + cfg['.name'] + '-in',
listen: cfg.address || '::',
listen_port: strToInt(cfg.port),
tcp_fast_open: strToBool(cfg.tcp_fast_open),
tcp_multi_path: strToBool(cfg.tcp_multi_path),
udp_fragment: strToBool(cfg.udp_fragment),
udp_timeout: cfg.udp_timeout ? (cfg.udp_timeout + 's') : null,
network: cfg.network,
/* Hysteria */
up_mbps: strToInt(cfg.hysteria_up_mbps),
down_mbps: strToInt(cfg.hysteria_down_mbps),
obfs: cfg.hysteria_obfs_type ? {
type: cfg.hysteria_obfs_type,
password: cfg.hysteria_obfs_password
} : cfg.hysteria_obfs_password,
recv_window_conn: strToInt(cfg.hysteria_recv_window_conn),
recv_window_client: strToInt(cfg.hysteria_revc_window_client),
max_conn_client: strToInt(cfg.hysteria_max_conn_client),
disable_mtu_discovery: strToBool(cfg.hysteria_disable_mtu_discovery),
ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
masquerade: cfg.hysteria_masquerade,
/* Shadowsocks */
method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null,
password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null,
/* Tuic */
congestion_control: cfg.tuic_congestion_control,
auth_timeout: cfg.tuic_auth_timeout ? (cfg.tuic_auth_timeout + 's') : null,
zero_rtt_handshake: strToBool(cfg.tuic_enable_zero_rtt),
heartbeat: cfg.tuic_heartbeat ? (cfg.tuic_heartbeat + 's') : null,
/* HTTP / Hysteria (2) / Mixed / Socks / Trojan / Tuic / VLESS / VMess */
users: (cfg.type !== 'shadowsocks') ? [
{
name: !(cfg.type in ['http', 'mixed', 'naive', 'socks']) ? 'cfg-' + cfg['.name'] + '-server' : null,
username: cfg.username,
password: cfg.password,
/* Hysteria */
auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null,
auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null,
/* Tuic */
uuid: cfg.uuid,
/* VLESS / VMess */
flow: cfg.vless_flow,
alterId: strToInt(cfg.vmess_alterid)
}
] : null,
multiplex: (cfg.multiplex === '1') ? {
enabled: true,
padding: (cfg.multiplex_padding === '1'),
brutal: (cfg.multiplex_brutal === '1') ? {
enabled: true,
up_mbps: strToInt(cfg.multiplex_brutal_up),
down_mbps: strToInt(cfg.multiplex_brutal_down)
} : null
} : null,
tls: (cfg.tls === '1') ? {
enabled: true,
server_name: cfg.tls_sni,
alpn: cfg.tls_alpn,
min_version: cfg.tls_min_version,
max_version: cfg.tls_max_version,
cipher_suites: cfg.tls_cipher_suites,
certificate_path: cfg.tls_cert_path,
key_path: cfg.tls_key_path,
acme: (cfg.tls_acme === '1') ? {
domain: cfg.tls_acme_domain,
data_directory: HP_DIR + '/certs',
default_server_name: cfg.tls_acme_dsn,
email: cfg.tls_acme_email,
provider: cfg.tls_acme_provider,
disable_http_challenge: (cfg.tls_acme_dhc === '1'),
disable_tls_alpn_challenge: (cfg.tls_acme_dtac === '1'),
alternative_http_port: strToInt(cfg.tls_acme_ahp),
alternative_tls_port: strToInt(cfg.tls_acme_atp),
external_account: (cfg.tls_acme_external_account === '1') ? {
key_id: cfg.tls_acme_ea_keyid,
mac_key: cfg.tls_acme_ea_mackey
} : null,
dns01_challenge: (cfg.tls_dns01_challenge === '1') ? {
provider: cfg.tls_dns01_provider,
access_key_id: cfg.tls_dns01_ali_akid,
access_key_secret: cfg.tls_dns01_ali_aksec,
region_id: cfg.tls_dns01_ali_rid,
api_token: cfg.tls_dns01_cf_api_token
} : null
} : null,
ech: (cfg.tls_ech_key) ? {
enabled: true,
key: split(cfg.tls_ech_key, '\n'),
// config: split(cfg.tls_ech_config, '\n')
} : null,
reality: (cfg.tls_reality === '1') ? {
enabled: true,
private_key: cfg.tls_reality_private_key,
short_id: cfg.tls_reality_short_id,
max_time_difference: cfg.tls_reality_max_time_difference ? (cfg.max_time_difference + 's') : null,
handshake: {
server: cfg.tls_reality_server_addr,
server_port: strToInt(cfg.tls_reality_server_port)
}
} : null
} : null,
transport: !isEmpty(cfg.transport) ? {
type: cfg.transport,
host: cfg.http_host || cfg.httpupgrade_host,
path: cfg.http_path || cfg.ws_path,
headers: cfg.ws_host ? {
Host: cfg.ws_host
} : null,
method: cfg.http_method,
max_early_data: strToInt(cfg.websocket_early_data),
early_data_header_name: cfg.websocket_early_data_header,
service_name: cfg.grpc_servicename,
idle_timeout: cfg.http_idle_timeout ? (cfg.http_idle_timeout + 's') : null,
ping_timeout: cfg.http_ping_timeout ? (cfg.http_ping_timeout + 's') : null
} : null
});
});
if (length(config.inbounds) === 0)
exit(1);
system('mkdir -p ' + RUN_DIR);
writefile(RUN_DIR + '/sing-box-s.json', sprintf('%.J\n', removeBlankAttrs(config)));

View File

@ -0,0 +1,234 @@
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2023 ImmortalWrt.org
*/
import { mkstemp } from 'fs';
import { urldecode_params } from 'luci.http';
/* Global variables start */
export const HP_DIR = '/etc/homeproxy';
export const RUN_DIR = '/var/run/homeproxy';
/* Global variables end */
/* Utilities start */
/* Kanged from luci-app-commands */
export function shellQuote(s) {
return `'${replace(s, "'", "'\\''")}'`;
};
export function isBinary(str) {
for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off))
if (byte <= 8 || (byte >= 14 && byte <= 31))
return true;
return false;
};
export function executeCommand(...args) {
let outfd = mkstemp();
let errfd = mkstemp();
const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
outfd.seek(0);
errfd.seek(0);
const stdout = outfd.read(1024 * 512) ?? '';
const stderr = errfd.read(1024 * 512) ?? '';
outfd.close();
errfd.close();
const binary = isBinary(stdout);
return {
command: join(' ', args),
stdout: binary ? null : stdout,
stderr,
exitcode,
binary
};
};
export function calcStringMD5(str) {
if (!str || type(str) !== 'string')
return null;
const output = executeCommand(`/bin/echo -n ${shellQuote(str)} | /usr/bin/md5sum | /usr/bin/awk '{print $1}'`) || {};
return trim(output.stdout);
};
export function getTime(epoch) {
const local_time = localtime(epoch);
return replace(replace(sprintf(
'%d-%2d-%2d@%2d:%2d:%2d',
local_time.year,
local_time.mon,
local_time.mday,
local_time.hour,
local_time.min,
local_time.sec
), ' ', '0'), '@', ' ');
};
export function wGET(url, ua) {
if (!url || type(url) !== 'string')
return null;
if (!ua)
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
const output = executeCommand(`/usr/bin/wget -qO- --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
return trim(output.stdout);
};
/* Utilities end */
/* String helper start */
export function isEmpty(res) {
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
};
export function strToBool(str) {
return (str === '1') || null;
};
export function strToInt(str) {
return !isEmpty(str) ? (int(str) || null) : null;
};
export function removeBlankAttrs(res) {
let content;
if (type(res) === 'object') {
content = {};
map(keys(res), (k) => {
if (type(res[k]) in ['array', 'object'])
content[k] = removeBlankAttrs(res[k]);
else if (res[k] !== null && res[k] !== '')
content[k] = res[k];
});
} else if (type(res) === 'array') {
content = [];
map(res, (k, i) => {
if (type(k) in ['array', 'object'])
push(content, removeBlankAttrs(k));
else if (k !== null && k !== '')
push(content, k);
});
} else
return res;
return content;
};
export function validateHostname(hostname) {
return (match(hostname, /^[a-zA-Z0-9_]+$/) != null ||
(match(hostname, /^[a-zA-Z0-9_][a-zA-Z0-9_%-.]*[a-zA-Z0-9]$/) &&
match(hostname, /[^0-9.]/)));
};
export function validation(datatype, data) {
if (!datatype || !data)
return null;
const ret = system(`/sbin/validate_data ${shellQuote(datatype)} ${shellQuote(data)} 2>/dev/null`);
return (ret === 0);
};
/* String helper end */
/* String parser start */
export function decodeBase64Str(str) {
if (isEmpty(str))
return null;
str = trim(str);
str = replace(str, '_', '/');
str = replace(str, '-', '+');
const padding = length(str) % 4;
if (padding)
str = str + substr('====', padding);
return b64dec(str);
};
export function parseURL(url) {
if (type(url) !== 'string')
return null;
const services = {
http: '80',
https: '443'
};
const objurl = {};
objurl.href = url;
url = replace(url, /#(.+)$/, (_, val) => {
objurl.hash = val;
return '';
});
url = replace(url, /^(\w[A-Za-z0-9\+\-\.]+):/, (_, val) => {
objurl.protocol = val;
return '';
});
url = replace(url, /\?(.+)/, (_, val) => {
objurl.search = val;
objurl.searchParams = urldecode_params(val);
return '';
});
url = replace(url, /^\/\/([^\/]+)/, (_, val) => {
val = replace(val, /^([^@]+)@/, (_, val) => {
objurl.userinfo = val;
return '';
});
val = replace(val, /:(\d+)$/, (_, val) => {
objurl.port = val;
return '';
});
if (validation('ip4addr', val) ||
validation('ip6addr', replace(val, /\[|\]/g, '')) ||
validation('hostname', val))
objurl.hostname = val;
return '';
});
objurl.pathname = url || '/';
if (!objurl.protocol || !objurl.hostname)
return null;
if (objurl.userinfo) {
objurl.userinfo = replace(objurl.userinfo, /:(.+)$/, (_, val) => {
objurl.password = val;
return '';
});
if (match(objurl.userinfo, /^[A-Za-z0-9\+\-\_\.]+$/)) {
objurl.username = objurl.userinfo;
delete objurl.userinfo;
} else {
delete objurl.userinfo;
delete objurl.password;
}
};
if (!objurl.port)
objurl.port = services[objurl.protocol];
objurl.host = objurl.hostname + (objurl.port ? `:${objurl.port}` : '');
objurl.origin = `${objurl.protocol}://${objurl.host}`;
return objurl;
};
/* String parser end */

View File

@ -0,0 +1,102 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
*/
'use strict';
import { cursor } from 'uci';
import { isEmpty } from 'homeproxy';
const uci = cursor();
const uciconfig = 'homeproxy';
uci.load(uciconfig);
const uciinfra = 'infra',
ucimain = 'config',
ucinode = 'node',
ucidns = 'dns',
ucidnsrule = 'dns_rule',
ucirouting = 'routing',
uciroutingnode = 'routing_node',
uciroutingrule = 'routing_rule',
uciserver = 'server';
/* chinadns-ng has been removed */
if (uci.get(uciconfig, uciinfra, 'china_dns_port'))
uci.delete(uciconfig, uciinfra, 'china_dns_port');
/* chinadns server now only accepts single server */
const china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
if (china_dns_server === 'wan_114')
uci.set(uciconfig, ucimain, 'china_dns_server', '114.114.114.114');
else if (match(china_dns_server, /,/))
uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ',')[0]);
else if (match(china_dns_server, / /))
uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ' ')[0]);
/* github_token option has been moved to config section */
const github_token = uci.get(uciconfig, uciinfra, 'github_token');
if (github_token) {
uci.set(uciconfig, ucimain, 'github_token', github_token);
uci.delete(uciconfig, uciinfra, 'github_token')
}
/* empty value defaults to all ports now */
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
uci.delete(uciconfig, ucimain, 'routing_port');
/* experimental section was removed */
if (uci.get(uciconfig, 'experimental'))
uci.delete(uciconfig, 'experimental');
/* DNS rules options */
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
if (cfg.rule_set_ipcidr_match_source === '1')
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
});
/* nodes options */
uci.foreach(uciconfig, ucinode, (cfg) => {
/* tls_ech_tls_disable_drs is useless and deprecated in sb 1.12 */
if (!isEmpty(cfg.tls_ech_tls_disable_drs))
uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs');
/* wireguard_gso was deprecated in sb 1.11 */
if (!isEmpty(cfg.wireguard_gso))
uci.delete(uciconfig, cfg['.name'], 'wireguard_gso');
});
/* routing rules options */
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
if (cfg.rule_set_ipcidr_match_source === '1')
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
});
/* server options */
/* auto_firewall was moved into server options */
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
if (auto_firewall || auto_firewall === '0')
uci.delete(uciconfig, uciserver, 'auto_firewall');
uci.foreach(uciconfig, uciserver, (cfg) => {
/* auto_firewall was moved into server options */
if (auto_firewall === '1')
uci.set(uciconfig, cfg['.name'], 'firewall' , '1');
/* sniff_override was deprecated in sb 1.11 */
if (!isEmpty(cfg.sniff_override))
uci.delete(uciconfig, cfg['.name'], 'sniff_override');
/* domain_strategy is now pointless without sniff override */
if (!isEmpty(cfg.domain_strategy))
uci.delete(uciconfig, cfg['.name'], 'domain_strategy');
});
if (!isEmpty(uci.changes(uciconfig)))
uci.commit(uciconfig);

View File

@ -0,0 +1,12 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2023 ImmortalWrt.org
SCRIPTS_DIR="/etc/homeproxy/scripts"
for i in "china_ip4" "china_ip6" "gfw_list" "china_list"; do
"$SCRIPTS_DIR"/update_resources.sh "$i"
done
"$SCRIPTS_DIR"/update_subscriptions.uc

View File

@ -0,0 +1,106 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2022-2023 ImmortalWrt.org
NAME="homeproxy"
RESOURCES_DIR="/etc/$NAME/resources"
mkdir -p "$RESOURCES_DIR"
RUN_DIR="/var/run/$NAME"
LOG_PATH="$RUN_DIR/$NAME.log"
mkdir -p "$RUN_DIR"
log() {
echo -e "$(date "+%Y-%m-%d %H:%M:%S") $*" >> "$LOG_PATH"
}
set_lock() {
local act="$1"
local type="$2"
local lock="$RUN_DIR/update_resources-$type.lock"
if [ "$act" = "set" ]; then
if [ -e "$lock" ]; then
log "[$(to_upper "$type")] A task is already running."
exit 2
else
touch "$lock"
fi
elif [ "$act" = "remove" ]; then
rm -f "$lock"
fi
}
to_upper() {
echo -e "$1" | tr "[a-z]" "[A-Z]"
}
check_list_update() {
local listtype="$1"
local listrepo="$2"
local listref="$3"
local listname="$4"
local github_token="$(uci -q get homeproxy.config.github_token)"
local wget="wget --timeout=10 -q"
set_lock "set" "$listtype"
[ -z "$github_token" ] || github_token="--header=Authorization: Bearer $github_token"
local list_info="$($wget "${github_token:--q}" -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1")"
local list_sha="$(echo -e "$list_info" | jsonfilter -qe "@[0].sha")"
local list_ver="$(echo -e "$list_info" | jsonfilter -qe "@[0].commit.message" | grep -Eo "[0-9-]+" | tr -d '-')"
if [ -z "$list_sha" ] || [ -z "$list_ver" ]; then
log "[$(to_upper "$listtype")] Failed to get the latest version, please retry later."
set_lock "remove" "$listtype"
return 1
fi
local local_list_ver="$(cat "$RESOURCES_DIR/$listtype.ver" 2>"/dev/null" || echo "NOT FOUND")"
if [ "$local_list_ver" = "$list_ver" ]; then
log "[$(to_upper "$listtype")] Current version: $list_ver."
log "[$(to_upper "$listtype")] You're already at the latest version."
set_lock "remove" "$listtype"
return 3
else
log "[$(to_upper "$listtype")] Local version: $local_list_ver, latest version: $list_ver."
fi
if ! $wget "https://fastly.jsdelivr.net/gh/$listrepo@$list_sha/$listname" -O "$RUN_DIR/$listname" || [ ! -s "$RUN_DIR/$listname" ]; then
rm -f "$RUN_DIR/$listname"
log "[$(to_upper "$listtype")] Update failed."
set_lock "remove" "$listtype"
return 1
fi
mv -f "$RUN_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}"
echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver"
log "[$(to_upper "$listtype")] Successfully updated."
set_lock "remove" "$listtype"
return 0
}
case "$1" in
"china_ip4")
check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv4.txt"
;;
"china_ip6")
check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv6.txt"
;;
"gfw_list")
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt"
;;
"china_list")
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt" && \
sed -i -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt"
;;
*)
echo -e "Usage: $0 <china_ip4 / china_ip6 / gfw_list / china_list>"
exit 1
;;
esac

View File

@ -0,0 +1,652 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2023 ImmortalWrt.org
*/
'use strict';
import { open } from 'fs';
import { connect } from 'ubus';
import { cursor } from 'uci';
import { urldecode, urlencode } from 'luci.http';
import { init_action } from 'luci.sys';
import {
calcStringMD5, wGET, decodeBase64Str,
getTime, isEmpty, parseURL, validation,
HP_DIR, RUN_DIR
} from 'homeproxy';
/* UCI config start */
const uci = cursor();
const uciconfig = 'homeproxy';
uci.load(uciconfig);
const ucimain = 'config',
ucinode = 'node',
ucisubscription = 'subscription';
const allow_insecure = uci.get(uciconfig, ucisubscription, 'allow_insecure') || '0',
filter_mode = uci.get(uciconfig, ucisubscription, 'filter_nodes') || 'disabled',
filter_keywords = uci.get(uciconfig, ucisubscription, 'filter_keywords') || [],
packet_encoding = uci.get(uciconfig, ucisubscription, 'packet_encoding') || 'xudp',
subscription_urls = uci.get(uciconfig, ucisubscription, 'subscription_url') || [],
user_agent = uci.get(uciconfig, ucisubscription, 'user_agent'),
via_proxy = uci.get(uciconfig, ucisubscription, 'update_via_proxy') || '0';
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainalnd_china';
let main_node, main_udp_node;
if (routing_mode !== 'custom') {
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
}
/* UCI config end */
/* String helper start */
function filter_check(name) {
if (isEmpty(name) || filter_mode === 'disabled' || isEmpty(filter_keywords))
return false;
let ret = false;
for (let i in filter_keywords) {
const patten = regexp(i);
if (match(name, patten))
ret = true;
}
if (filter_mode === 'whitelist')
ret = !ret;
return ret;
}
/* String helper end */
/* Common var start */
const node_cache = {},
node_result = [];
const ubus = connect();
const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {};
/* Common var end */
/* Log */
system(`mkdir -p ${RUN_DIR}`);
function log(...args) {
const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a');
logfile.write(`${getTime()} [SUBSCRIBE] ${join(' ', args)}\n`);
logfile.close();
}
function parse_uri(uri) {
let config, url, params;
if (type(uri) === 'object') {
if (uri.nodetype === 'sip008') {
/* https://shadowsocks.org/guide/sip008.html */
config = {
label: uri.remarks,
type: 'shadowsocks',
address: uri.server,
port: uri.server_port,
shadowsocks_encrypt_method: uri.method,
password: uri.password,
shadowsocks_plugin: uri.plugin,
shadowsocks_plugin_opts: uri.plugin_opts
};
}
} else if (type(uri) === 'string') {
uri = split(trim(uri), '://');
switch (uri[0]) {
case 'http':
case 'https':
url = parseURL('http://' + uri[1]) || {};
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'http',
address: url.hostname,
port: url.port,
username: url.username ? urldecode(url.username) : null,
password: url.password ? urldecode(url.password) : null,
tls: (uri[0] === 'https') ? '1' : '0'
};
break;
case 'hysteria':
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams;
if (!sing_features.with_quic || (params.protocol && params.protocol !== 'udp')) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
if (!sing_features.with_quic)
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'hysteria',
address: url.hostname,
port: url.port,
hysteria_protocol: params.protocol || 'udp',
hysteria_auth_type: params.auth ? 'string' : null,
hysteria_auth_payload: params.auth,
hysteria_obfs_password: params.obfsParam,
hysteria_down_mbps: params.downmbps,
hysteria_up_mbps: params.upmbps,
tls: '1',
tls_insecure: (params.insecure in ['true', '1']) ? '1' : '0',
tls_sni: params.peer,
tls_alpn: params.alpn
};
break;
case 'hysteria2':
case 'hy2':
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams;
if (!sing_features.with_quic) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'hysteria2',
address: url.hostname,
port: url.port,
password: url.username ? (
urldecode(url.username + (url.password ? (':' + url.password) : ''))
) : null,
hysteria_obfs_type: params.obfs,
hysteria_obfs_password: params['obfs-password'],
tls: '1',
tls_insecure: (params.insecure === '1') ? '1' : '0',
tls_sni: params.sni
};
break;
case 'socks':
case 'socks4':
case 'socks4a':
case 'socsk5':
case 'socks5h':
url = parseURL('http://' + uri[1]) || {};
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'socks',
address: url.hostname,
port: url.port,
username: url.username ? urldecode(url.username) : null,
password: url.password ? urldecode(url.password) : null,
socks_version: (match(uri[0], /4/)) ? '4' : '5'
};
break;
case 'ss':
/* "Lovely" Shadowrocket format */
const ss_suri = split(uri[1], '#');
let ss_slabel = '';
if (length(ss_suri) <= 2) {
if (length(ss_suri) === 2)
ss_slabel = '#' + urlencode(ss_suri[1]);
if (decodeBase64Str(ss_suri[0]))
uri[1] = decodeBase64Str(ss_suri[0]) + ss_slabel;
}
/* Legacy format is not supported, it should be never appeared in modern subscriptions */
/* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
/* SIP002 format https://shadowsocks.org/guide/sip002.html */
url = parseURL('http://' + uri[1]) || {};
let ss_userinfo = {};
if (url.username && url.password)
/* User info encoded with URIComponent */
ss_userinfo = [url.username, urldecode(url.password)];
else if (url.username)
/* User info encoded with base64 */
ss_userinfo = split(decodeBase64Str(urldecode(url.username)), ':', 2);
let ss_plugin, ss_plugin_opts;
if (url.search && url.searchParams.plugin) {
const ss_plugin_info = split(url.searchParams.plugin, ';', 2);
ss_plugin = ss_plugin_info[0];
if (ss_plugin === 'simple-obfs')
/* Fix non-standard plugin name */
ss_plugin = 'obfs-local';
ss_plugin_opts = ss_plugin_info[1];
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'shadowsocks',
address: url.hostname,
port: url.port,
shadowsocks_encrypt_method: ss_userinfo[0],
password: ss_userinfo[1],
shadowsocks_plugin: ss_plugin,
shadowsocks_plugin_opts: ss_plugin_opts
};
break;
case 'trojan':
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'trojan',
address: url.hostname,
port: url.port,
password: urldecode(url.username),
transport: (params.type !== 'tcp') ? params.type : null,
tls: '1',
tls_sni: params.sni
};
switch(params.type) {
case 'grpc':
config.grpc_servicename = params.serviceName;
break;
case 'ws':
config.ws_host = params.host ? urldecode(params.host) : null;
config.ws_path = params.path ? urldecode(params.path) : null;
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
config.ws_path = split(config.ws_path, '?ed=')[0];
}
break;
}
break;
case 'tuic':
/* https://github.com/daeuniverse/dae/discussions/182 */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
if (!sing_features.with_quic) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'tuic',
address: url.hostname,
port: url.port,
uuid: url.username,
password: url.password ? urldecode(url.password) : null,
tuic_congestion_control: params.congestion_control,
tuic_udp_relay_mode: params.udp_relay_mode,
tls: '1',
tls_sni: params.sni,
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
};
break;
case 'vless':
/* https://github.com/XTLS/Xray-core/discussions/716 */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams;
/* Unsupported protocol */
if (params.type === 'kcp') {
log(sprintf('Skipping sunsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
return null;
} else if (params.type === 'quic' && ((params.quicSecurity && params.quicSecurity !== 'none') || !sing_features.with_quic)) {
log(sprintf('Skipping sunsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
if (!sing_features.with_quic)
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'vless',
address: url.hostname,
port: url.port,
uuid: url.username,
transport: (params.type !== 'tcp') ? params.type : null,
tls: (params.security in ['tls', 'xtls', 'reality']) ? '1' : '0',
tls_sni: params.sni,
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
tls_reality: (params.security === 'reality') ? '1' : '0',
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
tls_reality_short_id: params.sid,
tls_utls: sing_features.with_utls ? params.fp : null,
vless_flow: (params.security in ['tls', 'reality']) ? params.flow : null
};
switch(params.type) {
case 'grpc':
config.grpc_servicename = params.serviceName;
break;
case 'http':
case 'tcp':
if (params.type === 'http' || params.headerType === 'http') {
config.http_host = params.host ? split(urldecode(params.host), ',') : null;
config.http_path = params.path ? urldecode(params.path) : null;
}
break;
case 'httpupgrade':
config.httpupgrade_host = params.host ? urldecode(params.host) : null;
config.http_path = params.path ? urldecode(params.path) : null;
break;
case 'ws':
config.ws_host = params.host ? urldecode(params.host) : null;
config.ws_path = params.path ? urldecode(params.path) : null;
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
config.ws_path = split(config.ws_path, '?ed=')[0];
}
break;
}
break;
case 'vmess':
/* "Lovely" shadowrocket format */
if (match(uri, /&/)) {
log(sprintf('Skipping unsupported %s format.', uri[0]));
return null;
}
/* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */
try {
uri = json(decodeBase64Str(uri[1])) || {};
} catch(e) {
log(sprintf('Skipping unsupported %s format.', uri[0]));
return null;
}
if (uri.v != '2') {
log(sprintf('Skipping unsupported %s format.', uri[0]));
return null;
/* Unsupported protocol */
} else if (uri.net === 'kcp') {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add));
return null;
} else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || uri.path || !sing_features.with_quic)) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add));
if (!sing_features.with_quic)
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
/*
* https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
* else if (uri.aid && int(uri.aid) !== 0) {
* log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add));
* return null;
* }
*/
config = {
label: uri.ps ? urldecode(uri.ps) : null,
type: 'vmess',
address: uri.add,
port: uri.port,
uuid: uri.id,
vmess_alterid: uri.aid,
vmess_encrypt: uri.scy || 'auto',
vmess_global_padding: '1',
transport: (uri.net !== 'tcp') ? uri.net : null,
tls: (uri.tls === 'tls') ? '1' : '0',
tls_sni: uri.sni || uri.host,
tls_alpn: uri.alpn ? split(uri.alpn, ',') : null,
tls_utls: sing_features.with_utls ? uri.fp : null
};
switch (uri.net) {
case 'grpc':
config.grpc_servicename = uri.path;
break;
case 'h2':
case 'tcp':
if (uri.net === 'h2' || uri.type === 'http') {
config.transport = 'http';
config.http_host = uri.host ? split(uri.host, ',') : null;
config.http_path = uri.path;
}
break;
case 'httpupgrade':
config.httpupgrade_host = uri.host;
config.http_path = uri.path;
break;
case 'ws':
config.ws_host = uri.host;
config.ws_path = uri.path;
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
config.ws_path = split(config.ws_path, '?ed=')[0];
}
break;
}
break;
}
}
if (!isEmpty(config)) {
if (config.address)
config.address = replace(config.address, /\[|\]/g, '');
if (!validation('host', config.address) || !validation('port', config.port)) {
log(sprintf('Skipping invalid %s node: %s.', config.type, config.label || 'NULL'));
return null;
} else if (!config.label)
config.label = (validation('ip6addr', config.address) ?
`[${config.address}]` : config.address) + ':' + config.port;
}
return config;
}
function main() {
if (via_proxy !== '1') {
log('Stopping service...');
init_action('homeproxy', 'stop');
}
for (let url in subscription_urls) {
url = replace(url, /#.*$/, '');
const groupHash = calcStringMD5(url);
node_cache[groupHash] = {};
const res = wGET(url, user_agent);
if (isEmpty(res)) {
log(sprintf('Failed to fetch resources from %s.', url));
continue;
}
let nodes;
try {
nodes = json(res).servers || json(res);
/* Shadowsocks SIP008 format */
if (nodes[0].server && nodes[0].method)
map(nodes, (_, i) => nodes[i].nodetype = 'sip008');
} catch(e) {
nodes = decodeBase64Str(res);
nodes = nodes ? split(trim(replace(nodes, / /g, '_')), '\n') : {};
}
let count = 0;
for (let node in nodes) {
let config;
if (!isEmpty(node))
config = parse_uri(node);
if (isEmpty(config))
continue;
const label = config.label;
config.label = null;
const confHash = calcStringMD5(sprintf('%J', config)),
nameHash = calcStringMD5(label);
config.label = label;
if (filter_check(config.label))
log(sprintf('Skipping blacklist node: %s.', config.label));
else if (node_cache[groupHash][confHash] || node_cache[groupHash][nameHash])
log(sprintf('Skipping duplicate node: %s.', config.label));
else {
if (config.tls === '1' && allow_insecure === '1')
config.tls_insecure = '1';
if (config.type in ['vless', 'vmess'])
config.packet_encoding = packet_encoding;
config.grouphash = groupHash;
push(node_result, []);
push(node_result[length(node_result)-1], config);
node_cache[groupHash][confHash] = config;
node_cache[groupHash][nameHash] = config;
count++;
}
}
if (count == 0)
log(sprintf('No valid node found in %s.', url));
else
log(sprintf('Successfully fetched %s nodes of total %s from %s.', count, length(nodes), url));
}
if (isEmpty(node_result)) {
log('Failed to update subscriptions: no valid node found.');
if (via_proxy !== '1') {
log('Starting service...');
init_action('homeproxy', 'start');
}
return false;
}
let added = 0, removed = 0;
uci.foreach(uciconfig, ucinode, (cfg) => {
/* Nodes created by the user */
if (!cfg.grouphash)
return null;
/* Empty object - failed to fetch nodes */
if (length(node_cache[cfg.grouphash]) === 0)
return null;
if (!node_cache[cfg.grouphash] || !node_cache[cfg.grouphash][cfg['.name']]) {
uci.delete(uciconfig, cfg['.name']);
removed++;
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
} else {
map(keys(node_cache[cfg.grouphash][cfg['.name']]), (v) => {
if (v in node_cache[cfg.grouphash][cfg['.name']])
uci.set(uciconfig, cfg['.name'], v, node_cache[cfg.grouphash][cfg['.name']][v]);
else
uci.delete(uciconfig, cfg['.name'], v);
});
node_cache[cfg.grouphash][cfg['.name']].isExisting = true;
}
});
for (let nodes in node_result)
map(nodes, (node) => {
if (node.isExisting)
return null;
const nameHash = calcStringMD5(node.label);
uci.set(uciconfig, nameHash, 'node');
map(keys(node), (v) => uci.set(uciconfig, nameHash, v, node[v]));
added++;
log(sprintf('Adding node: %s.', node.label));
});
uci.commit(uciconfig);
let need_restart = (via_proxy !== '1');
if (!isEmpty(main_node)) {
const first_server = uci.get_first(uciconfig, ucinode);
if (first_server) {
let main_urltest_nodes;
if (main_node === 'urltest') {
main_urltest_nodes = filter(uci.get(uciconfig, ucimain, 'main_urltest_nodes'), (v) => {
if (!uci.get(uciconfig, v)) {
log(sprintf('Node %s is gone, removing from urltest list.', v));
return false;
}
return true;
});
}
if ((main_node === 'urltest') ? !length(main_urltest_nodes) : !uci.get(uciconfig, main_node)) {
uci.set(uciconfig, ucimain, 'main_node', first_server);
uci.commit(uciconfig);
need_restart = true;
log('Main node is gone, switching to the first node.');
}
if (!isEmpty(main_udp_node) && main_udp_node !== 'same') {
let main_udp_urltest_nodes;
if (main_udp_node === 'urltest') {
main_udp_urltest_nodes = filter(uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes'), (v) => {
if (!uci.get(uciconfig, v)) {
log(sprintf('Node %s is gone, removing from urltest list.', v));
return false;
}
return true;
});
}
if ((main_udp_node === 'urltest') ? !length(main_udp_urltest_nodes) : !uci.get(uciconfig, main_udp_node)) {
uci.set(uciconfig, ucimain, 'main_udp_node', first_server);
uci.commit(uciconfig);
need_restart = true;
log('Main UDP node is gone, switching to the first node.');
}
}
} else {
uci.set(uciconfig, ucimain, 'main_node', 'nil');
uci.set(uciconfig, ucimain, 'main_udp_node', 'nil');
uci.commit(uciconfig);
need_restart = true;
log('No available node, disable tproxy.');
}
}
if (need_restart) {
log('Restarting service...');
init_action('homeproxy', 'stop');
init_action('homeproxy', 'start');
}
log(sprintf('%s nodes added, %s removed.', added, removed));
log('Successfully updated subscriptions.');
}
if (!isEmpty(subscription_urls))
try {
call(main);
} catch(e) {
log('[FATAL ERROR] An error occurred during updating subscriptions:');
log(sprintf('%s: %s', e.type, e.message));
log(e.stacktrace[0].context);
log('Restarting service...');
init_action('homeproxy', 'stop');
init_action('homeproxy', 'start');
}

View File

@ -0,0 +1,330 @@
#!/bin/sh /etc/rc.common
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2022-2023 ImmortalWrt.org
USE_PROCD=1
START=99
STOP=10
CONF="homeproxy"
PROG="/usr/bin/sing-box"
HP_DIR="/etc/homeproxy"
RUN_DIR="/var/run/homeproxy"
LOG_PATH="$RUN_DIR/homeproxy.log"
# we don't know which is the default server, just take the first one
DNSMASQ_UCI_CONFIG="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')"
if [ -f "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG" ]; then
DNSMASQ_DIR="$(awk -F '=' '/^conf-dir=/ {print $2}' "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG")/dnsmasq-homeproxy.d"
else
DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-homeproxy.d"
fi
log() {
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH"
}
start_service() {
config_load "$CONF"
local routing_mode proxy_mode
config_get routing_mode "config" "routing_mode" "bypass_mainland_china"
config_get proxy_mode "config" "proxy_mode" "redirect_tproxy"
local outbound_node
if [ "$routing_mode" != "custom" ]; then
config_get outbound_node "config" "main_node" "nil"
else
config_get outbound_node "routing" "default_outbound" "nil"
fi
local server_enabled
config_get_bool server_enabled "server" "enabled" "0"
if [ "$outbound_node" = "nil" ] && [ "$server_enabled" = "0" ]; then
return 1
fi
mkdir -p "$RUN_DIR"
if [ "$outbound_node" != "nil" ]; then
# Generate/Validate client config
ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH"
if [ ! -e "$RUN_DIR/sing-box-c.json" ]; then
log "Error: failed to generate client configuration."
return 1
elif ! "$PROG" check --config "$RUN_DIR/sing-box-c.json" 2>>"$LOG_PATH"; then
log "Error: wrong client configuration detected."
return 1
fi
# Auto update
local auto_update auto_update_time
config_get_bool auto_update "subscription" "auto_update" "0"
if [ "$auto_update" = "1" ]; then
config_get auto_update_time "subscription" "auto_update_time" "2"
sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>"/dev/null"
echo -e "0 $auto_update_time * * * $HP_DIR/scripts/update_crond.sh" >> "/etc/crontabs/root"
/etc/init.d/cron restart
fi
# DNSMasq rules
local ipv6_support dns_port
config_get_bool ipv6_support "config" "ipv6_support" "0"
config_get dns_port "infra" "dns_port" "5333"
mkdir -p "$DNSMASQ_DIR"
echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf"
case "$routing_mode" in
"bypass_mainland_china"|"custom"|"global")
cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
no-poll
no-resolv
server=127.0.0.1#$dns_port
EOF
;;
"gfwlist")
[ "$ipv6_support" -eq "0" ] || local gfw_nftset_v6=",6#inet#fw4#homeproxy_gfw_list_v6"
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_gfw_list_v4$gfw_nftset_v6/g" \
"$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf"
;;
"proxy_mainland_china")
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \
"$HP_DIR/resources/china_list.txt" > "$DNSMASQ_DIR/china_list.conf"
;;
esac
if [ "$routing_mode" != "custom" ] && [ -s "$HP_DIR/resources/proxy_list.txt" ]; then
[ "$ipv6_support" -eq "0" ] || local wan_nftset_v6=",6#inet#fw4#homeproxy_wan_proxy_addr_v6"
sed -r -e '/^\s*$/d' -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_wan_proxy_addr_v4$wan_nftset_v6/g" \
"$HP_DIR/resources/proxy_list.txt" > "$DNSMASQ_DIR/proxy_list.conf"
fi
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
# Setup routing table
local table_mark
config_get table_mark "infra" "table_mark" "100"
case "$proxy_mode" in
"redirect_tproxy")
local outbound_udp_node
config_get outbound_udp_node "config" "main_udp_node" "nil"
if [ "$outbound_udp_node" != "nil" ] || [ "$routing_mode" = "custom" ]; then
local tproxy_mark
config_get tproxy_mark "infra" "tproxy_mark" "101"
ip rule add fwmark "$tproxy_mark" table "$table_mark"
ip route add local 0.0.0.0/0 dev lo table "$table_mark"
if [ "$ipv6_support" -eq "1" ]; then
ip -6 rule add fwmark "$tproxy_mark" table "$table_mark"
ip -6 route add local ::/0 dev lo table "$table_mark"
fi
fi
;;
"redirect_tun"|"tun")
local tun_name tun_mark
config_get tun_name "infra" "tun_name" "singtun0"
config_get tun_mark "infra" "tun_mark" "102"
ip tuntap add mode tun user root name "$tun_name"
sleep 1s
ip link set "$tun_name" up
ip route replace default dev "$tun_name" table "$table_mark"
ip rule add fwmark "$tun_mark" lookup "$table_mark"
ip -6 route replace default dev "$tun_name" table "$table_mark"
ip -6 rule add fwmark "$tun_mark" lookup "$table_mark"
;;
esac
# sing-box (client)
procd_open_instance "sing-box-c"
procd_set_param command "$PROG"
procd_append_param command run --config "$RUN_DIR/sing-box-c.json"
if [ -x "/sbin/ujail" ] && [ "$routing_mode" != "custom" ] && ! grep -Eq '"type": "(wireguard|tun)"' "$RUN_DIR/sing-box-c.json"; then
procd_add_jail "sing-box-c" log procfs
procd_add_jail_mount "$RUN_DIR/sing-box-c.json"
procd_add_jail_mount_rw "$RUN_DIR/sing-box-c.log"
[ "$routing_mode" != "bypass_mainland_china" ] || procd_add_jail_mount_rw "$RUN_DIR/cache.db"
procd_add_jail_mount "$HP_DIR/certs/"
procd_add_jail_mount "/etc/ssl/"
procd_add_jail_mount "/etc/localtime"
procd_add_jail_mount "/etc/TZ"
procd_set_param capabilities "/etc/capabilities/homeproxy.json"
procd_set_param no_new_privs 1
procd_set_param user sing-box
procd_set_param group sing-box
fi
procd_set_param limits core="unlimited"
procd_set_param limits nofile="1000000 1000000"
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
fi
if [ "$server_enabled" = "1" ]; then
# Generate/Validate server config
ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH"
if [ ! -e "$RUN_DIR/sing-box-s.json" ]; then
log "Error: failed to generate server configuration."
return 1
elif ! "$PROG" check --config "$RUN_DIR/sing-box-s.json" 2>>"$LOG_PATH"; then
log "Error: wrong server configuration detected."
return 1
fi
# sing-box (server)
procd_open_instance "sing-box-s"
procd_set_param command "$PROG"
procd_append_param command run --config "$RUN_DIR/sing-box-s.json"
if [ -x "/sbin/ujail" ]; then
procd_add_jail "sing-box-s" log procfs
procd_add_jail_mount "$RUN_DIR/sing-box-s.json"
procd_add_jail_mount_rw "$RUN_DIR/sing-box-s.log"
procd_add_jail_mount_rw "$HP_DIR/certs/"
procd_add_jail_mount "/etc/acme/"
procd_add_jail_mount "/etc/ssl/"
procd_add_jail_mount "/etc/localtime"
procd_add_jail_mount "/etc/TZ"
procd_set_param capabilities "/etc/capabilities/homeproxy.json"
procd_set_param no_new_privs 1
procd_set_param user sing-box
procd_set_param group sing-box
fi
procd_set_param limits core="unlimited"
procd_set_param limits nofile="1000000 1000000"
procd_set_param stderr 1
procd_set_param respawn
procd_close_instance
fi
# log-cleaner
procd_open_instance "log-cleaner"
procd_set_param command "$HP_DIR/scripts/clean_log.sh"
procd_set_param respawn
procd_close_instance
case "$routing_mode" in
"bypass_mainland_china")
# Prepare cache db
[ -e "$RUN_DIR/cache.db" ] || touch "$RUN_DIR/cache.db"
;;
"custom")
# Prepare ruleset directory
[ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset"
;;
esac
[ "$outbound_node" = "nil" ] || echo > "$RUN_DIR/sing-box-c.log"
if [ "$server_enabled" = "1" ]; then
echo > "$RUN_DIR/sing-box-s.log"
mkdir -p "$HP_DIR/certs"
fi
# Update permissions for ujail
chown -R sing-box:sing-box "$RUN_DIR"
# Setup firewall
ucode "$HP_DIR/scripts/firewall_pre.uc"
[ "$outbound_node" = "nil" ] || utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft"
fw4 reload >"/dev/null" 2>&1
log "$(sing-box version | awk 'NR==1{print $1,$3}') started."
}
stop_service() {
sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>"/dev/null"
/etc/init.d/cron restart >"/dev/null" 2>&1
# Setup firewall
# Load config
config_load "$CONF"
local table_mark tproxy_mark tun_mark tun_name
config_get table_mark "infra" "table_mark" "100"
config_get tproxy_mark "infra" "tproxy_mark" "101"
config_get tun_mark "infra" "tun_mark" "102"
config_get tun_name "infra" "tun_name" "singtun0"
# Tproxy
ip rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
ip route del local 0.0.0.0/0 dev lo table "$table_mark" 2>"/dev/null"
ip -6 rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
ip -6 route del local ::/0 dev lo table "$table_mark" 2>"/dev/null"
# TUN
ip route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
ip rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"
ip -6 route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
ip -6 rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"
# Nftables rules
for i in "homeproxy_dstnat_redir" "homeproxy_output_redir" \
"homeproxy_redirect" "homeproxy_redirect_proxy" \
"homeproxy_redirect_proxy_port" "homeproxy_redirect_lanac" \
"homeproxy_mangle_prerouting" "homeproxy_mangle_output" \
"homeproxy_mangle_tproxy" "homeproxy_mangle_tproxy_port" \
"homeproxy_mangle_tproxy_lanac" "homeproxy_mangle_mark" \
"homeproxy_mangle_tun" "homeproxy_mangle_tun_mark"; do
nft flush chain inet fw4 "$i"
nft delete chain inet fw4 "$i"
done 2>"/dev/null"
for i in "homeproxy_local_addr_v4" "homeproxy_local_addr_v6" \
"homeproxy_gfw_list_v4" "homeproxy_gfw_list_v6" \
"homeproxy_mainland_addr_v4" "homeproxy_mainland_addr_v6" \
"homeproxy_wan_proxy_addr_v4" "homeproxy_wan_proxy_addr_v6" \
"homeproxy_wan_direct_addr_v4" "homeproxy_wan_direct_addr_v6" \
"homeproxy_routing_port"; do
nft flush set inet fw4 "$i"
nft delete set inet fw4 "$i"
done 2>"/dev/null"
echo 2>"/dev/null" > "$RUN_DIR/fw4_forward.nft"
echo 2>"/dev/null" > "$RUN_DIR/fw4_input.nft"
echo 2>"/dev/null" > "$RUN_DIR/fw4_post.nft"
fw4 reload >"/dev/null" 2>&1
# Remove DNS hijack
rm -rf "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" "$DNSMASQ_DIR"
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-c.log" \
"$RUN_DIR/sing-box-s.json" "$RUN_DIR/sing-box-s.log"
log "Service stopped."
}
service_stopped() {
# Load config
config_load "$CONF"
local tun_name
config_get tun_name "infra" "tun_name" "singtun0"
# TUN
ip link set "$tun_name" down 2>"/dev/null"
ip tuntap del mode tun name "$tun_name" 2>"/dev/null"
}
reload_service() {
log "Reloading service..."
stop
start
}
service_triggers() {
procd_add_reload_trigger "$CONF"
procd_add_interface_trigger "interface.*.up" wan /etc/init.d/$CONF reload
}

View File

@ -0,0 +1,31 @@
#!/bin/sh
[ -f "/www/luci-static/resources/icons/loading.gif" ] && \
sed -i "s,/loading.svg,/loading.gif,g" "/www/luci-static/resources/view/homeproxy/status.js"
uci -q batch <<-EOF >"/dev/null"
delete firewall.homeproxy_pre
delete firewall.homeproxy_forward
set firewall.homeproxy_forward=include
set firewall.homeproxy_forward.type=nftables
set firewall.homeproxy_forward.path="/var/run/homeproxy/fw4_forward.nft"
set firewall.homeproxy_forward.position="chain-pre"
set firewall.homeproxy_forward.chain="forward"
delete firewall.homeproxy_input
set firewall.homeproxy_input=include
set firewall.homeproxy_input.type=nftables
set firewall.homeproxy_input.path="/var/run/homeproxy/fw4_input.nft"
set firewall.homeproxy_input.position="chain-pre"
set firewall.homeproxy_input.chain="input"
delete firewall.homeproxy_post
set firewall.homeproxy_post=include
set firewall.homeproxy_post.type=nftables
set firewall.homeproxy_post.path="/var/run/homeproxy/fw4_post.nft"
set firewall.homeproxy_post.position="table-post"
commit firewall
EOF
exit 0

View File

@ -0,0 +1,5 @@
#!/bin/sh
ucode "/etc/homeproxy/scripts/migrate_config.uc"
exit 0

View File

@ -0,0 +1,45 @@
{
"admin/services/homeproxy": {
"title": "HomeProxy",
"order": 10,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-homeproxy" ],
"uci": { "homeproxy": true }
}
},
"admin/services/homeproxy/client": {
"title": "Client Settings",
"order": 10,
"action": {
"type": "view",
"path": "homeproxy/client"
}
},
"admin/services/homeproxy/node": {
"title": "Node Settings",
"order": 15,
"action": {
"type": "view",
"path": "homeproxy/node"
}
},
"admin/services/homeproxy/server": {
"title": "Server Settings",
"order": 20,
"action": {
"type": "view",
"path": "homeproxy/server"
}
},
"admin/services/homeproxy/status": {
"title": "Service Status",
"order": 30,
"action": {
"type": "view",
"path": "homeproxy/status"
}
}
}

View File

@ -0,0 +1,24 @@
{
"luci-app-homeproxy": {
"description": "Grant access to homeproxy configuration",
"read": {
"file": {
"/etc/homeproxy/scripts/update_subscriptions.uc": [ "exec" ],
"/var/run/homeproxy/homeproxy.log": [ "read" ],
"/var/run/homeproxy/sing-box-c.log": [ "read" ],
"/var/run/homeproxy/sing-box-s.log": [ "read" ]
},
"ubus": {
"service": [ "list" ],
"luci.homeproxy": [ "*" ]
},
"uci": [ "homeproxy" ]
},
"write": {
"file": {
"/tmp/homeproxy_certificate.tmp": [ "write" ]
},
"uci": [ "homeproxy" ]
}
}
}

View File

@ -0,0 +1,254 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2023-2024 ImmortalWrt.org
*/
'use strict';
import { access, error, lstat, popen, readfile, writefile } from 'fs';
/* Kanged from ucode/luci */
function shellquote(s) {
return `'${replace(s, "'", "'\\''")}'`;
}
function hasKernelModule(kmod) {
return (system(sprintf('[ -e "/lib/modules/$(uname -r)"/%s ]', shellquote(kmod))) === 0);
}
const HP_DIR = '/etc/homeproxy';
const RUN_DIR = '/var/run/homeproxy';
const methods = {
acllist_read: {
args: { type: 'type' },
call: function(req) {
if (index(['direct_list', 'proxy_list'], req.args?.type) === -1)
return { content: null, error: 'illegal type' };
const filecontent = readfile(`${HP_DIR}/resources/${req.args?.type}.txt`);
return { content: filecontent };
}
},
acllist_write: {
args: { type: 'type', content: 'content' },
call: function(req) {
if (index(['direct_list', 'proxy_list'], req.args?.type) === -1)
return { result: false, error: 'illegal type' };
const file = `${HP_DIR}/resources/${req.args?.type}.txt`;
let content = req.args?.content;
/* Sanitize content */
if (content) {
content = trim(content);
content = replace(content, /\r\n?/g, '\n');
if (!match(content, /\n$/))
content += '\n';
}
system(`mkdir -p ${HP_DIR}/resources`);
writefile(file, content);
return { result: true };
}
},
certificate_write: {
args: { filename: 'filename' },
call: function(req) {
const writeCertificate = function(filename, priv) {
const tmpcert = '/tmp/homeproxy_certificate.tmp';
const filestat = lstat(tmpcert);
if (!filestat || filestat.type !== 'file' || filestat.size <= 0) {
system(`rm -f ${tmpcert}`);
return { result: false, error: 'empty certificate file' };
}
let filecontent = readfile(tmpcert);
if (is_binary(filecontent)) {
system(`rm -f ${tmpcert}`);
return { result: false, error: 'illegal file type: binary' };
}
/* Kanged from luci-proto-openconnect */
const beg = priv ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
end = priv ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
lines = split(trim(filecontent), /[\r\n]/);
let start = false, i;
for (i = 0; i < length(lines); i++) {
if (match(lines[i], beg))
start = true;
else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64)
break;
}
if (!start || i < length(lines) - 1 || !match(lines[i], end)) {
system(`rm -f ${tmpcert}`);
return { result: false, error: 'this does not look like a correct PEM file' };
}
/* Sanitize certificate */
filecontent = trim(filecontent);
filecontent = replace(filecontent, /\r\n?/g, '\n');
if (!match(filecontent, /\n$/))
filecontent += '\n';
system(`mkdir -p ${HP_DIR}/certs`);
writefile(`${HP_DIR}/certs/${filename}.pem`, filecontent);
system(`rm -f ${tmpcert}`);
return { result: true };
};
const filename = req.args?.filename;
switch (filename) {
case 'client_ca':
case 'server_publickey':
return writeCertificate(filename, false);
break;
case 'server_privatekey':
return writeCertificate(filename, true);
break;
default:
return { result: false, error: 'illegal cerificate filename' };
break;
}
}
},
connection_check: {
args: { site: 'site' },
call: function(req) {
let url;
switch(req.args?.site) {
case 'baidu':
url = 'https://www.baidu.com';
break;
case 'google':
url = 'https://www.google.com';
break;
default:
return { result: false, error: 'illegal site' };
break;
}
return { result: (system(`/usr/bin/wget --spider -qT3 ${url} 2>"/dev/null"`, 3100) === 0) };
}
},
log_clean: {
args: { type: 'type' },
call: function(req) {
if (!(req.args?.type in ['homeproxy', 'sing-box-c', 'sing-box-s']))
return { result: false, error: 'illegal type' };
const filestat = lstat(`${RUN_DIR}/${req.args?.type}.log`);
if (filestat)
writefile(`${RUN_DIR}/${req.args?.type}.log`, '');
return { result: true };
}
},
singbox_generator: {
args: { type: 'type', params: 'params' },
call: function(req) {
if (!(req.args?.type in ['ech-keypair', 'uuid', 'reality-keypair', 'vapid-keypair', 'wg-keypair']))
return { result: false, error: 'illegal type' };
const type = req.args?.type;
let result = {};
const fd = popen('/usr/bin/sing-box generate ' + type + ` ${req.args?.params || ''}`);
if (fd) {
let ech_cfg_set = false;
let ech_key_set = false;
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
if (type === 'uuid')
result.uuid = trim(line);
else if (type in ['reality-keypair', 'vapid-keypair', 'wg-keypair']) {
let priv = match(trim(line), /PrivateKey: (.*)/);
if (priv)
result.private_key = priv[1];
let pub = match(trim(line), /PublicKey: (.*)/);
if (pub)
result.public_key = pub[1];
} else if (type in ['ech-keypair']) {
if (trim(line) === '-----BEGIN ECH CONFIGS-----')
ech_cfg_set = true;
else if (trim(line) === '-----BEGIN ECH KEYS-----')
ech_key_set = true;
if (ech_cfg_set)
result.ech_cfg = result.ech_cfg ? result.ech_cfg + '\n' + trim(line) : trim(line) ;
if (ech_key_set)
result.ech_key = result.ech_key ? result.ech_key + '\n' + trim(line) : trim(line) ;
if (trim(line) === '-----END ECH CONFIGS-----')
ech_cfg_set = false;
else if (trim(line) === '-----END ECH KEYS-----')
ech_key_set = false;
}
}
fd.close();
}
return { result };
}
},
singbox_get_features: {
call: function() {
let features = {};
const fd = popen('/usr/bin/sing-box version');
if (fd) {
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
if (match(trim(line), /^sing-box version (.*)/))
features.version = match(trim(line), /^sing-box version (.*)/)[1];
let tags = match(trim(line), /^Tags: (.*)/);
if (tags)
for (let i in split(tags[1], ','))
features[i] = true;
}
fd.close();
}
features.hp_has_ip_full = access('/usr/libexec/ip-full');
features.hp_has_tcp_brutal = hasKernelModule('brutal.ko');
features.hp_has_tproxy = hasKernelModule('nft_tproxy.ko') || access('/etc/modules.d/nft-tproxy');
features.hp_has_tun = hasKernelModule('tun.ko') || access('/etc/modules.d/30-tun');
return features;
}
},
resources_get_version: {
args: { type: 'type' },
call: function(req) {
const version = trim(readfile(`${HP_DIR}/resources/${req.args?.type}.ver`));
return { version: version, error: error() };
}
},
resources_update: {
args: { type: 'type' },
call: function(req) {
if (req.args?.type) {
const type = shellquote(req.args?.type);
const exit_code = system(`${HP_DIR}/scripts/update_resources.sh ${type}`);
return { status: exit_code };
} else
return { status: 255, error: 'illegal type' };
}
}
};
return { 'luci.homeproxy': methods };

10
luci-app-nikki/Makefile Normal file
View File

@ -0,0 +1,10 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=1.23.2
LUCI_TITLE:=LuCI Support for nikki
LUCI_DEPENDS:=+luci-base +nikki
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,185 @@
'use strict';
'require baseclass';
'require uci';
'require fs';
'require rpc';
'require request';
const callRCList = rpc.declare({
object: 'rc',
method: 'list',
params: ['name'],
expect: { '': {} }
});
const callRCInit = rpc.declare({
object: 'rc',
method: 'init',
params: ['name', 'action'],
expect: { '': {} }
});
const callNikkiVersion = rpc.declare({
object: 'luci.nikki',
method: 'version',
expect: { '': {} }
});
const callNikkiProfile = rpc.declare({
object: 'luci.nikki',
method: 'profile',
params: [ 'defaults' ],
expect: { '': {} }
});
const callNikkiUpdateSubscription = rpc.declare({
object: 'luci.nikki',
method: 'update_subscription',
params: ['section_id'],
expect: { '': {} }
});
const callNikkiGetIdentifiers = rpc.declare({
object: 'luci.nikki',
method: 'get_identifiers',
expect: { '': {} }
});
const callNikkiDebug = rpc.declare({
object: 'luci.nikki',
method: 'debug',
expect: { '': {} }
});
const homeDir = '/etc/nikki';
const profilesDir = `${homeDir}/profiles`;
const subscriptionsDir = `${homeDir}/subscriptions`;
const mixinFilePath = `${homeDir}/mixin.yaml`;
const runDir = `${homeDir}/run`;
const runProfilePath = `${runDir}/config.yaml`;
const providersDir = `${runDir}/providers`;
const ruleProvidersDir = `${providersDir}/rule`;
const proxyProvidersDir = `${providersDir}/proxy`;
const logDir = `/var/log/nikki`;
const appLogPath = `${logDir}/app.log`;
const coreLogPath = `${logDir}/core.log`;
const debugLogPath = `${logDir}/debug.log`;
const nftDir = `${homeDir}/nftables`;
const reservedIPNFT = `${nftDir}/reserved_ip.nft`;
const reservedIP6NFT = `${nftDir}/reserved_ip6.nft`;
return baseclass.extend({
homeDir: homeDir,
profilesDir: profilesDir,
subscriptionsDir: subscriptionsDir,
mixinFilePath: mixinFilePath,
runDir: runDir,
runProfilePath: runProfilePath,
ruleProvidersDir: ruleProvidersDir,
proxyProvidersDir: proxyProvidersDir,
appLogPath: appLogPath,
coreLogPath: coreLogPath,
debugLogPath: debugLogPath,
reservedIPNFT: reservedIPNFT,
reservedIP6NFT: reservedIP6NFT,
status: async function () {
return (await callRCList('nikki'))?.nikki?.running;
},
reload: function () {
return callRCInit('nikki', 'reload');
},
restart: function () {
return callRCInit('nikki', 'restart');
},
version: function () {
return callNikkiVersion();
},
profile: function (defaults) {
return callNikkiProfile(defaults);
},
updateSubscription: function (section_id) {
return callNikkiUpdateSubscription(section_id);
},
api: async function (method, path, query, body) {
const profile = await callNikkiProfile({ 'external-controller': null, 'secret': null });
const apiListen = profile['external-controller'];
const apiSecret = profile['secret'] ?? '';
const apiPort = apiListen.substring(apiListen.lastIndexOf(':') + 1);
const url = `http://${window.location.hostname}:${apiPort}${path}`;
return request.request(url, {
method: method,
headers: { 'Authorization': `Bearer ${apiSecret}` },
query: query,
content: body
})
},
openDashboard: async function () {
const profile = await callNikkiProfile({ 'external-ui-name': null, 'external-controller': null, 'secret': null });
const uiName = profile['external-ui-name'];
const apiListen = profile['external-controller'];
const apiSecret = profile['secret'] ?? '';
const apiPort = apiListen.substring(apiListen.lastIndexOf(':') + 1);
const params = {
host: window.location.hostname,
hostname: window.location.hostname,
port: apiPort,
secret: apiSecret
};
const query = new URLSearchParams(params).toString();
let url;
if (uiName) {
url = `http://${window.location.hostname}:${apiPort}/ui/${uiName}/?${query}`;
} else {
url = `http://${window.location.hostname}:${apiPort}/ui/?${query}`;
}
setTimeout(function () { window.open(url, '_blank') }, 0);
},
updateDashboard: function () {
return this.api('POST', '/upgrade/ui');
},
getIdentifiers: function () {
return callNikkiGetIdentifiers();
},
listProfiles: function () {
return L.resolveDefault(fs.list(this.profilesDir), []);
},
listRuleProviders: function () {
return L.resolveDefault(fs.list(this.ruleProvidersDir), []);
},
listProxyProviders: function () {
return L.resolveDefault(fs.list(this.proxyProvidersDir), []);
},
getAppLog: function () {
return L.resolveDefault(fs.read_direct(this.appLogPath));
},
getCoreLog: function () {
return L.resolveDefault(fs.read_direct(this.coreLogPath));
},
clearAppLog: function () {
return fs.write(this.appLogPath);
},
clearCoreLog: function () {
return fs.write(this.coreLogPath);
},
debug: function () {
return callNikkiDebug();
},
})

View File

@ -0,0 +1,155 @@
'use strict';
'require form';
'require view';
'require uci';
'require poll';
'require tools.nikki as nikki';
function renderStatus(running) {
return updateStatus(E('input', { id: 'core_status', style: 'border: unset; font-style: italic; font-weight: bold;', readonly: '' }), running);
}
function updateStatus(element, running) {
if (element) {
element.style.color = running ? 'green' : 'red';
element.value = running ? _('Running') : _('Not Running');
}
return element;
}
return view.extend({
load: function () {
return Promise.all([
uci.load('nikki'),
nikki.version(),
nikki.status(),
nikki.listProfiles()
]);
},
render: function (data) {
const subscriptions = uci.sections('nikki', 'subscription');
const appVersion = data[1].app ?? '';
const coreVersion = data[1].core ?? '';
const running = data[2];
const profiles = data[3];
let m, s, o;
m = new form.Map('nikki', _('Nikki'), `${_('Transparent Proxy with Mihomo on OpenWrt.')} <a href="https://github.com/nikkinikki-org/OpenWrt-nikki/wiki" target="_blank">${_('How To Use')}</a>`);
s = m.section(form.TableSection, 'status', _('Status'));
s.anonymous = true;
o = s.option(form.Value, '_app_version', _('App Version'));
o.readonly = true;
o.load = function () {
return appVersion;
};
o.write = function () { };
o = s.option(form.Value, '_core_version', _('Core Version'));
o.readonly = true;
o.load = function () {
return coreVersion;
};
o.write = function () { };
o = s.option(form.DummyValue, '_core_status', _('Core Status'));
o.cfgvalue = function () {
return renderStatus(running);
};
poll.add(function () {
return L.resolveDefault(nikki.status()).then(function (running) {
updateStatus(document.getElementById('core_status'), running);
});
});
o = s.option(form.Button, 'reload');
o.inputstyle = 'action';
o.inputtitle = _('Reload Service');
o.onclick = function () {
return nikki.reload();
};
o = s.option(form.Button, 'restart');
o.inputstyle = 'negative';
o.inputtitle = _('Restart Service');
o.onclick = function () {
return nikki.restart();
};
o = s.option(form.Button, 'update_dashboard');
o.inputstyle = 'positive';
o.inputtitle = _('Update Dashboard');
o.onclick = function () {
return nikki.updateDashboard();
};
o = s.option(form.Button, 'open_dashboard');
o.inputtitle = _('Open Dashboard');
o.onclick = function () {
return nikki.openDashboard();
};
s = m.section(form.NamedSection, 'config', 'config', _('App Config'));
o = s.option(form.Flag, 'enabled', _('Enable'));
o.rmempty = false;
o = s.option(form.ListValue, 'profile', _('Choose Profile'));
o.optional = true;
for (const profile of profiles) {
o.value('file:' + profile.name, _('File:') + profile.name);
};
for (const subscription of subscriptions) {
o.value('subscription:' + subscription['.name'], _('Subscription:') + subscription.name);
};
o = s.option(form.Value, 'start_delay', _('Start Delay'));
o.datatype = 'uinteger';
o.placeholder = '0';
o = s.option(form.Flag, 'scheduled_restart', _('Scheduled Restart'));
o.rmempty = false;
o = s.option(form.Value, 'cron_expression', _('Cron Expression'));
o.retain = true;
o.rmempty = false;
o.depends('scheduled_restart', '1');
o = s.option(form.Flag, 'test_profile', _('Test Profile'));
o.rmempty = false;
o = s.option(form.Flag, 'fast_reload', _('Fast Reload'));
o.rmempty = false;
o = s.option(form.Flag, 'core_only', _('Core Only'));
o.rmempty = false;
s = m.section(form.NamedSection, 'env', 'env', _('Core Environment Variable Config'));
o = s.option(form.DynamicList, 'safe_paths', _('Safe Paths'));
o.load = function (section_id) {
return this.super('load', section_id)?.split(':');
};
o.write = function (section_id, formvalue) {
this.super('write', section_id, formvalue?.join(':'));
};
o = s.option(form.Flag, 'disable_loopback_detector', _('Disable Loopback Detector'));
o.rmempty = false;
o = s.option(form.Flag, 'disable_quic_go_gso', _('Disable GSO of quic-go'));
o.rmempty = false;
o = s.option(form.Flag, 'disable_quic_go_ecn', _('Disable ECN of quic-go'));
o.rmempty = false;
o = s.option(form.Flag, 'skip_system_ipv6_check', _('Skip System IPv6 Check'));
o.rmempty = false;
return m.render();
}
});

View File

@ -0,0 +1,82 @@
'use strict';
'require form';
'require view';
'require uci';
'require fs';
'require tools.nikki as nikki'
return view.extend({
load: function () {
return Promise.all([
uci.load('nikki'),
nikki.listProfiles(),
nikki.listRuleProviders(),
nikki.listProxyProviders(),
]);
},
render: function (data) {
const subscriptions = uci.sections('nikki', 'subscription');
const profiles = data[1];
const ruleProviders = data[2];
const proxyProviders = data[3];
let m, s, o;
m = new form.Map('nikki');
s = m.section(form.NamedSection, 'editor', 'editor', _('Editor'));
o = s.option(form.ListValue, '_file', _('Choose File'));
o.optional = true;
for (const profile of profiles) {
o.value(nikki.profilesDir + '/' + profile.name, _('File:') + profile.name);
};
for (const subscription of subscriptions) {
o.value(nikki.subscriptionsDir + '/' + subscription['.name'] + '.yaml', _('Subscription:') + subscription.name);
};
for (const ruleProvider of ruleProviders) {
o.value(nikki.ruleProvidersDir + '/' + ruleProvider.name, _('Rule Provider:') + ruleProvider.name);
};
for (const proxyProvider of proxyProviders) {
o.value(nikki.proxyProvidersDir + '/' + proxyProvider.name, _('Proxy Provider:') + proxyProvider.name);
};
o.value(nikki.mixinFilePath, _('File for Mixin'));
o.value(nikki.runProfilePath, _('Profile for Startup'));
o.value(nikki.reservedIPNFT, _('File for Reserved IP'));
o.value(nikki.reservedIP6NFT, _('File for Reserved IP6'));
o.write = function (section_id, formvalue) {
return true;
};
o.onchange = function (event, section_id, value) {
return L.resolveDefault(fs.read_direct(value), '').then(function (content) {
m.lookupOption('_file_content', section_id)[0].getUIElement(section_id).setValue(content);
});
};
o = s.option(form.TextValue, '_file_content',);
o.rows = 25;
o.wrap = false;
o.write = function (section_id, formvalue) {
const path = m.lookupOption('_file', section_id)[0].formvalue(section_id);
return fs.write(path, formvalue);
};
o.remove = function (section_id) {
const path = m.lookupOption('_file', section_id)[0].formvalue(section_id);
return fs.write(path);
};
return m.render();
},
handleSaveApply: function (ev, mode) {
return this.handleSave(ev).finally(function () {
return mode === '0' ? nikki.reload() : nikki.restart();
});
},
handleReset: null
});

View File

@ -0,0 +1,124 @@
'use strict';
'require form';
'require view';
'require uci';
'require fs';
'require poll';
'require tools.nikki as nikki';
return view.extend({
load: function () {
return Promise.all([
uci.load('nikki'),
nikki.getAppLog(),
nikki.getCoreLog()
]);
},
render: function (data) {
const appLog = data[1];
const coreLog = data[2];
let m, s, o;
m = new form.Map('nikki');
s = m.section(form.NamedSection, 'log', 'log', _('Log'));
s.tab('app_log', _('App Log'));
o = s.taboption('app_log', form.Button, 'clear_app_log');
o.inputstyle = 'negative';
o.inputtitle = _('Clear Log');
o.onclick = function (_, section_id) {
m.lookupOption('_app_log', section_id)[0].getUIElement(section_id).setValue('');
return nikki.clearAppLog();
};
o = s.taboption('app_log', form.TextValue, '_app_log');
o.rows = 25;
o.wrap = false;
o.load = function (section_id) {
return appLog;
};
o.write = function (section_id, formvalue) {
return true;
};
poll.add(L.bind(function () {
const option = this;
return L.resolveDefault(nikki.getAppLog()).then(function (log) {
option.getUIElement('log').setValue(log);
});
}, o));
o = s.taboption('app_log', form.Button, 'scroll_app_log_to_bottom');
o.inputtitle = _('Scroll To Bottom');
o.onclick = function (_, section_id) {
const element = m.lookupOption('_app_log', section_id)[0].getUIElement(section_id).node.firstChild;
element.scrollTop = element.scrollHeight;
};
s.tab('core_log', _('Core Log'));
o = s.taboption('core_log', form.Button, 'clear_core_log');
o.inputstyle = 'negative';
o.inputtitle = _('Clear Log');
o.onclick = function (_, section_id) {
m.lookupOption('_core_log', section_id)[0].getUIElement(section_id).setValue('');
return nikki.clearCoreLog();
};
o = s.taboption('core_log', form.TextValue, '_core_log');
o.rows = 25;
o.wrap = false;
o.load = function (section_id) {
return coreLog;
};
o.write = function (section_id, formvalue) {
return true;
};
poll.add(L.bind(function () {
const option = this;
return L.resolveDefault(nikki.getCoreLog()).then(function (log) {
option.getUIElement('log').setValue(log);
});
}, o));
o = s.taboption('core_log', form.Button, 'scroll_core_log_to_bottom');
o.inputtitle = _('Scroll To Bottom');
o.onclick = function (_, section_id) {
const element = m.lookupOption('_core_log', section_id)[0].getUIElement(section_id).node.firstChild;
element.scrollTop = element.scrollHeight;
};
s.tab('debug_log', _('Debug Log'));
o = s.taboption('debug_log', form.Button, '_generate_download_debug_log');
o.inputstyle = 'negative';
o.inputtitle = _('Generate & Download');
o.onclick = function () {
return nikki.debug().then(function () {
fs.read_direct(nikki.debugLogPath, 'blob').then(function (data) {
// create url
const url = window.URL.createObjectURL(data, { type: 'text/markdown' });
// create link
const link = document.createElement('a');
link.href = url;
link.download = 'debug.log';
// append to body
document.body.appendChild(link);
// download
link.click();
// remove from body
document.body.removeChild(link);
// revoke url
window.URL.revokeObjectURL(url);
});
});
};
return m.render();
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});

View File

@ -0,0 +1,572 @@
'use strict';
'require form';
'require view';
'require uci';
'require fs';
'require network';
'require poll';
'require tools.widgets as widgets';
'require tools.nikki as nikki';
return view.extend({
load: function () {
return Promise.all([
uci.load('nikki'),
network.getNetworks(),
]);
},
render: function (data) {
const networks = data[1];
let m, s, o, so;
m = new form.Map('nikki');
s = m.section(form.NamedSection, 'mixin', 'mixin', _('Mixin Option'));
s.tab('general', _('General Config'));
o = s.taboption('general', form.ListValue, 'log_level', _('Log Level'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('silent');
o.value('error');
o.value('warning');
o.value('info');
o.value('debug');
o = s.taboption('general', form.ListValue, 'mode', _('Mode'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('global', _('Global Mode'));
o.value('rule', _('Rule Mode'));
o.value('direct', _('Direct Mode'));
o = s.taboption('general', form.ListValue, 'match_process', _('Match Process'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('off');
o.value('strict');
o.value('always');
o = s.taboption('general', form.ListValue, 'outbound_interface', _('Outbound Interface'));
o.optional = true;
o.placeholder = _('Unmodified');
for (const network of networks) {
if (network.getName() === 'loopback') {
continue;
}
o.value(network.getName());
}
o = s.taboption('general', form.ListValue, 'ipv6', 'IPv6');
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('general', form.ListValue, 'unify_delay', _('Unify Delay'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('general', form.ListValue, 'tcp_concurrent', _('TCP Concurrent'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('general', form.ListValue, 'disable_tcp_keep_alive', _('Disable TCP Keep Alive'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('general', form.Value, 'tcp_keep_alive_idle', _('TCP Keep Alive Idle'));
o.datatype = 'uinteger';
o.placeholder = _('Unmodified');
o = s.taboption('general', form.Value, 'tcp_keep_alive_interval', _('TCP Keep Alive Interval'));
o.datatype = 'uinteger';
o.placeholder = _('Unmodified');
o = s.taboption('general', form.Value, 'global_client_fingerprint', _('Global Client Fingerprint'));
o.placeholder = _('Unmodified');
o.value('random', _('Random'));
o.value('chrome', 'Chrome');
o.value('firefox', 'Firefox');
o.value('safari', 'Safari');
o.value('edge', 'Edge');
s.tab('external_control', _('External Control Config'));
o = s.taboption('external_control', form.Value, 'ui_path', _('UI Path'));
o.placeholder = _('Unmodified');
o = s.taboption('external_control', form.Value, 'ui_name', _('UI Name'));
o.placeholder = _('Unmodified');
o = s.taboption('external_control', form.Value, 'ui_url', _('UI Url'));
o.placeholder = _('Unmodified');
o.value('https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip', 'Zashboard (CDN Fonts)');
o.value('https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip', 'Zashboard');
o.value('https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip', 'MetaCubeXD');
o.value('https://github.com/MetaCubeX/Yacd-meta/archive/refs/heads/gh-pages.zip', 'YACD');
o.value('https://github.com/MetaCubeX/Razord-meta/archive/refs/heads/gh-pages.zip', 'Razord');
o = s.taboption('external_control', form.Value, 'api_listen', '*' + ' ' + _('API Listen'));
o.datatype = 'ipaddrport(1)';
o.placeholder = _('Unmodified');
o.rmempty = false;
o = s.taboption('external_control', form.Value, 'api_secret', _('API Secret'));
o.password = true;
o.placeholder = _('Unmodified');
o = s.taboption('external_control', form.ListValue, 'selection_cache', _('Save Proxy Selection'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
s.tab('inbound', _('Inbound Config'));
o = s.taboption('inbound', form.ListValue, 'allow_lan', _('Allow Lan'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('inbound', form.Value, 'http_port', _('HTTP Port'));
o.datatype = 'port';
o.placeholder = _('Unmodified');
o = s.taboption('inbound', form.Value, 'socks_port', _('SOCKS Port'));
o.datatype = 'port';
o.placeholder = _('Unmodified');
o = s.taboption('inbound', form.Value, 'mixed_port', _('Mixed Port'));
o.datatype = 'port';
o.placeholder = _('Unmodified');
o = s.taboption('inbound', form.Value, 'redir_port', '*' + ' ' + _('Redirect Port'));
o.datatype = 'port';
o.placeholder = _('Unmodified');
o.rmempty = false;
o = s.taboption('inbound', form.Value, 'tproxy_port', '*' + ' ' + _('TPROXY Port'));
o.datatype = 'port';
o.placeholder = _('Unmodified');
o.rmempty = false;
o = s.taboption('inbound', form.Flag, 'authentication', _('Overwrite Authentication'));
o.rmempty = false;
o = s.taboption('inbound', form.SectionValue, '_authentications', form.TableSection, 'authentication', _('Edit Authentications'));
o.retain = true;
o.depends('authentication', '1');
o.subsection.addremove = true;
o.subsection.anonymous = true;
o.subsection.sortable = true;
so = o.subsection.option(form.Flag, 'enabled', _('Enable'));
so.rmempty = false;
so = o.subsection.option(form.Value, 'username', _('Username'));
so.rmempty = false;
so = o.subsection.option(form.Value, 'password', _('Password'));
so.password = true;
so.rmempty = false;
s.tab('tun', _('TUN Config'));
o = s.taboption('tun', form.Value, 'tun_device', '*' + ' ' + _('Device Name'));
o.placeholder = _('Unmodified');
o.rmempty = false;
o = s.taboption('tun', form.ListValue, 'tun_stack', _('Stack'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('system', 'System');
o.value('gvisor', 'gVisor');
o.value('mixed', 'Mixed');
o = s.taboption('tun', form.Value, 'tun_mtu', _('MTU'));
o.datatype = 'uinteger';
o.placeholder = _('Unmodified');
o = s.taboption('tun', form.ListValue, 'tun_gso', _('GSO'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('tun', form.Value, 'tun_gso_max_size', _('GSO Max Size'));
o.datatype = 'uinteger';
o.placeholder = _('Unmodified');
o = s.taboption('tun', form.ListValue, 'tun_endpoint_independent_nat', _('Endpoint Independent NAT'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('tun', form.Flag, 'tun_dns_hijack', _('Overwrite DNS Hijack'));
o.rmempty = false;
o = s.taboption('tun', form.DynamicList, 'tun_dns_hijacks', _('Edit DNS Hijacks'));
o.retain = true;
o.depends('tun_dns_hijack', '1');
o.value('tcp://any:53');
o.value('udp://any:53');
s.tab('dns', _('DNS Config'));
o = s.taboption('dns', form.Value, 'dns_listen', '*' + ' ' + _('DNS Listen'));
o.datatype = 'ipaddrport(1)';
o.placeholder = _('Unmodified');
o.rmempty = false;
o = s.taboption('dns', form.ListValue, 'dns_ipv6', 'IPv6');
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('dns', form.ListValue, 'dns_mode', '*' + ' ' + _('DNS Mode'));
o.placeholder = _('Unmodified');
o.value('redir-host', 'Redir-Host');
o.value('fake-ip', 'Fake-IP');
o = s.taboption('dns', form.Value, 'fake_ip_range', '*' + ' ' + _('Fake-IP Range'));
o.datatype = 'cidr4';
o.placeholder = _('Unmodified');
o.rmempty = false;
o = s.taboption('dns', form.Flag, 'fake_ip_filter', _('Overwrite Fake-IP Filter'));
o.rmempty = false;
o = s.taboption('dns', form.DynamicList, 'fake_ip_filters', _('Edit Fake-IP Filters'));
o.retain = true;
o.depends('fake_ip_filter', '1');
o = s.taboption('dns', form.ListValue, 'fake_ip_filter_mode', _('Fake-IP Filter Mode'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('blacklist', _('Block Mode'));
o.value('whitelist', _('Allow Mode'));
o = s.taboption('dns', form.ListValue, 'fake_ip_cache', _('Fake-IP Cache'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('dns', form.ListValue, 'dns_respect_rules', _('Respect Rules'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('dns', form.ListValue, 'dns_doh_prefer_http3', _('DoH Prefer HTTP/3'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('dns', form.ListValue, 'dns_system_hosts', _('Use System Hosts'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('dns', form.ListValue, 'dns_hosts', _('Use Hosts'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('dns', form.Flag, 'hosts', _('Overwrite Hosts'));
o.rmempty = false;
o = s.taboption('dns', form.SectionValue, '_hosts', form.TableSection, 'hosts', _('Edit Hosts'));
o.retain = true;
o.depends('hosts', '1');
o.subsection.addremove = true;
o.subsection.anonymous = true;
o.subsection.sortable = true;
so = o.subsection.option(form.Flag, 'enabled', _('Enable'));
so.rmempty = false;
so = o.subsection.option(form.Value, 'domain_name', _('Domain Name'));
so.rmempty = false;
so = o.subsection.option(form.DynamicList, 'ip', 'IP');
o = s.taboption('dns', form.Flag, 'dns_nameserver', _('Overwrite Nameserver'));
o.rmempty = false;
o = s.taboption('dns', form.SectionValue, '_dns_nameservers', form.TableSection, 'nameserver', _('Edit Nameservers'));
o.retain = true;
o.depends('dns_nameserver', '1');
o.subsection.addremove = true;
o.subsection.anonymous = true;
o.subsection.sortable = true;
so = o.subsection.option(form.Flag, 'enabled', _('Enable'));
so.rmempty = false;
so = o.subsection.option(form.ListValue, 'type', _('Type'));
so.value('default-nameserver');
so.value('proxy-server-nameserver');
so.value('direct-nameserver');
so.value('nameserver');
so.value('fallback');
so = o.subsection.option(form.DynamicList, 'nameserver', _('Nameserver'));
o = s.taboption('dns', form.Flag, 'dns_nameserver_policy', _('Overwrite Nameserver Policy'));
o.rmempty = false;
o = s.taboption('dns', form.SectionValue, '_dns_nameserver_policies', form.TableSection, 'nameserver_policy', _('Edit Nameserver Policies'));
o.retain = true;
o.depends('dns_nameserver_policy', '1');
o.subsection.addremove = true;
o.subsection.anonymous = true;
o.subsection.sortable = true;
so = o.subsection.option(form.Flag, 'enabled', _('Enable'));
so.rmempty = false;
so = o.subsection.option(form.Value, 'matcher', _('Matcher'));
so.rmempty = false;
so = o.subsection.option(form.DynamicList, 'nameserver', _('Nameserver'));
s.tab('sniffer', _('Sniffer Config'));
o = s.taboption('sniffer', form.ListValue, 'sniffer', _('Enable'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('sniffer', form.ListValue, 'sniffer_sniff_dns_mapping', _('Sniff Redir-Host'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('sniffer', form.ListValue, 'sniffer_sniff_pure_ip', _('Sniff Pure IP'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('sniffer', form.Flag, 'sniffer_force_domain_name', _('Overwrite Force Sniff Domain Name'));
o.rmempty = false;
o = s.taboption('sniffer', form.DynamicList, 'sniffer_force_domain_names', _('Force Sniff Domain Name'));
o.retain = true;
o.depends('sniffer_force_domain_name', '1');
o = s.taboption('sniffer', form.Flag, 'sniffer_ignore_domain_name', _('Overwrite Ignore Sniff Domain Name'));
o.rmempty = false;
o = s.taboption('sniffer', form.DynamicList, 'sniffer_ignore_domain_names', _('Ignore Sniff Domain Name'));
o.retain = true;
o.depends('sniffer_ignore_domain_name', '1');
o = s.taboption('sniffer', form.Flag, 'sniffer_sniff', _('Overwrite Sniff By Protocol'));
o.rmempty = false;
o = s.taboption('sniffer', form.SectionValue, '_sniffer_sniffs', form.TableSection, 'sniff', _('Sniff By Protocol'));
o.retain = true;
o.depends('sniffer_sniff', '1');
o.subsection.anonymous = true;
o.subsection.addremove = false;
so = o.subsection.option(form.Flag, 'enabled', _('Enable'));
so.rmempty = false;
so = o.subsection.option(form.ListValue, 'protocol', _('Protocol'));
so.value('HTTP');
so.value('TLS');
so.value('QUIC');
so.readonly = true;
so = o.subsection.option(form.DynamicList, 'port', _('Port'));
so.datatype = 'portrange';
so = o.subsection.option(form.Flag, 'overwrite_destination', _('Overwrite Destination'));
so.rmempty = false;
s.tab('rule', _('Rule Config'));
o = s.taboption('rule', form.Flag, 'rule_provider', _('Append Rule Provider'));
o.rmempty = false;
o = s.taboption('rule', form.SectionValue, '_rule_providers', form.GridSection, 'rule_provider', _('Edit Rule Providers'));
o.retain = true;
o.depends('rule_provider', '1');
o.subsection.anonymous = true;
o.subsection.addremove = true;
o.subsection.sortable = true;
so = o.subsection.option(form.Flag, 'enabled', _('Enable'));
so.default = 1;
so.editable = true;
so.modalonly = false;
so.rmempty = false;
so = o.subsection.option(form.Value, 'name', _('Name'));
so.rmempty = false;
so = o.subsection.option(form.ListValue, 'type', _('Type'));
so.default = 'http';
so.rmempty = false;
so.value('http');
so.value('file');
so = o.subsection.option(form.Value, 'url', _('Url'));
so.modalonly = true;
so.rmempty = false;
so.depends('type', 'http');
so = o.subsection.option(form.Value, 'node', _('Node'));
so.default = 'DIRECT';
so.modalonly = true;
so.depends('type', 'http');
so.value('GLOBAL');
so.value('DIRECT');
so = o.subsection.option(form.Value, 'file_size_limit', _('File Size Limit'));
so.datatype = 'uinteger';
so.default = 0;
so.modalonly = true;
so.depends('type', 'http');
so = o.subsection.option(form.FileUpload, 'file_path', _('File Path'));
so.modalonly = true;
so.rmempty = false;
so.root_directory = nikki.ruleProvidersDir;
so.depends('type', 'file');
so = o.subsection.option(form.ListValue, 'file_format', _('File Format'));
so.default = 'yaml';
so.value('mrs');
so.value('yaml');
so.value('text');
so = o.subsection.option(form.ListValue, 'behavior', _('Behavior'));
so.default = 'classical';
so.rmempty = false;
so.value('classical');
so.value('domain');
so.value('ipcidr');
so = o.subsection.option(form.Value, 'update_interval', _('Update Interval'));
so.datatype = 'uinteger';
so.default = 0;
so.modalonly = true;
so.depends('type', 'http');
o = s.taboption('rule', form.Flag, 'rule', _('Append Rule'));
o.rmempty = false;
o = s.taboption('rule', form.SectionValue, '_rules', form.TableSection, 'rule', _('Edit Rules'));
o.retain = true;
o.depends('rule', '1');
o.subsection.anonymous = true;
o.subsection.addremove = true;
o.subsection.sortable = true;
so = o.subsection.option(form.Flag, 'enabled', _('Enable'));
so.default = 1;
so.rmempty = false;
so = o.subsection.option(form.Value, 'type', _('Type'));
so.rmempty = false;
so.value('RULE-SET', _('Rule Set'));
so.value('DOMAIN', _('Domain Name'));
so.value('DOMAIN-SUFFIX', _('Domain Name Suffix'));
so.value('DOMAIN-KEYWORD', _('Domain Name Keyword'));
so.value('DOMAIN-REGEX', _('Domain Name Regex'));
so.value('IP-CIDR', _('Destination IP'));
so.value('DST-PORT', _('Destination Port'));
so.value('PROCESS-NAME', _('Process Name'));
so.value('GEOSITE', _('Domain Name Geo'));
so.value('GEOIP', _('Destination IP Geo'));
so = o.subsection.option(form.Value, 'matcher', _('Matcher'));
so.rmempty = false;
so.depends({ 'type': /MATCH/i, '!reverse': true });
so = o.subsection.option(form.Value, 'node', _('Node'));
so.default = 'GLOBAL';
so.value('GLOBAL');
so.value('DIRECT');
so.value('REJECT');
so.value('REJECT-DROP');
so = o.subsection.option(form.Flag, 'no_resolve', _('No Resolve'));
so.rmempty = false;
so.depends('type', /IP-CIDR6?/i);
so.depends('type', /GEOIP/i);
s.tab('geox', _('GeoX Config'));
o = s.taboption('geox', form.ListValue, 'geoip_format', _('GeoIP Format'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('dat', 'DAT');
o.value('mmdb', 'MMDB');
o = s.taboption('geox', form.ListValue, 'geodata_loader', _('GeoData Loader'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('standard', _('Standard Loader'));
o.value('memconservative', _('Memory Conservative Loader'));
o = s.taboption('geox', form.Value, 'geosite_url', _('GeoSite Url'));
o.placeholder = _('Unmodified');
o = s.taboption('geox', form.Value, 'geoip_mmdb_url', _('GeoIP(MMDB) Url'));
o.placeholder = _('Unmodified');
o = s.taboption('geox', form.Value, 'geoip_dat_url', _('GeoIP(DAT) Url'));
o.placeholder = _('Unmodified');
o = s.taboption('geox', form.Value, 'geoip_asn_url', _('GeoIP(ASN) Url'));
o.placeholder = _('Unmodified');
o = s.taboption('geox', form.ListValue, 'geox_auto_update', _('GeoX Auto Update'));
o.optional = true;
o.placeholder = _('Unmodified');
o.value('0', _('Disable'));
o.value('1', _('Enable'));
o = s.taboption('geox', form.Value, 'geox_update_interval', _('GeoX Update Interval'));
o.datatype = 'uinteger';
o.placeholder = _('Unmodified');
s.tab('mixin_file_content', _('Mixin File Content'));
o = s.taboption('mixin_file_content', form.Flag, 'mixin_file_content', _('Enable'), _('Please go to the editor tab to edit the file for mixin'));
o.rmempty = false;
return m.render();
}
});

View File

@ -0,0 +1,86 @@
'use strict';
'require form';
'require view';
'require uci';
'require tools.nikki as nikki';
return view.extend({
load: function () {
return Promise.all([
uci.load('nikki')
]);
},
render: function (data) {
let m, s, o, so;
m = new form.Map('nikki');
s = m.section(form.NamedSection, 'config', 'config', _('Profile'));
o = s.option(form.FileUpload, '_upload_profile', _('Upload Profile'));
o.browser = true;
o.enable_download = true;
o.root_directory = nikki.profilesDir;
o.write = function (section_id, formvalue) {
return true;
};
s = m.section(form.GridSection, 'subscription', _('Subscription'));
s.addremove = true;
s.anonymous = true;
s.sortable = true;
s.modaltitle = _('Edit Subscription');
o = s.option(form.Value, 'name', _('Subscription Name'));
o.rmempty = false;
o = s.option(form.Value, 'used', _('Used'));
o.modalonly = false;
o.optional = true;
o.readonly = true;
o = s.option(form.Value, 'total', _('Total'));
o.modalonly = false;
o.optional = true;
o.readonly = true;
o = s.option(form.Value, 'expire', _('Expire At'));
o.modalonly = false;
o.optional = true;
o.readonly = true;
o = s.option(form.Value, 'update', _('Update At'));
o.modalonly = false;
o.optional = true;
o.readonly = true;
o = s.option(form.Button, 'update_subscription');
o.editable = true;
o.inputstyle = 'positive';
o.inputtitle = _('Update');
o.modalonly = false;
o.onclick = function (_, section_id) {
return nikki.updateSubscription(section_id);
};
o = s.option(form.Value, 'url', _('Subscription Url'));
o.modalonly = true;
o.rmempty = false;
o = s.option(form.Value, 'user_agent', _('User Agent'));
o.default = 'clash';
o.modalonly = true;
o.rmempty = false;
o.value('clash');
o.value('clash.meta');
o.value('mihomo');
o = s.option(form.ListValue, 'prefer', _('Prefer'));
o.default = 'remote';
o.modalonly = true;
o.value('remote', _('Remote'));
o.value('local', _('Local'));
return m.render();
}
});

View File

@ -0,0 +1,182 @@
'use strict';
'require form';
'require view';
'require uci';
'require network';
'require tools.widgets as widgets';
'require tools.nikki as nikki';
return view.extend({
load: function () {
return Promise.all([
uci.load('nikki'),
network.getHostHints(),
network.getNetworks(),
nikki.getIdentifiers(),
]);
},
render: function (data) {
const hosts = data[1].hosts;
const networks = data[2];
const users = data[3]?.users ?? [];
const groups = data[3]?.groups ?? [];
const cgroups = data[3]?.cgroups ?? [];
let m, s, o, so;
m = new form.Map('nikki');
s = m.section(form.NamedSection, 'proxy', 'proxy', _('Proxy Config'));
s.tab('proxy', _('Proxy Config'));
o = s.taboption('proxy', form.Flag, 'enabled', _('Enable'));
o.rmempty = false;
o = s.taboption('proxy', form.ListValue, 'tcp_mode', _('TCP Mode'));
o.optional = true;
o.placeholder = _('Disable');
o.value('redirect', _('Redirect Mode'));
o.value('tproxy', _('TPROXY Mode'));
o.value('tun', _('TUN Mode'));
o = s.taboption('proxy', form.ListValue, 'udp_mode', _('UDP Mode'));
o.optional = true;
o.placeholder = _('Disable');
o.value('tproxy', _('TPROXY Mode'));
o.value('tun', _('TUN Mode'));
o = s.taboption('proxy', form.Flag, 'ipv4_dns_hijack', _('IPv4 DNS Hijack'));
o.rmempty = false;
o = s.taboption('proxy', form.Flag, 'ipv6_dns_hijack', _('IPv6 DNS Hijack'));
o.rmempty = false;
o = s.taboption('proxy', form.Flag, 'ipv4_proxy', _('IPv4 Proxy'));
o.rmempty = false;
o = s.taboption('proxy', form.Flag, 'ipv6_proxy', _('IPv6 Proxy'));
o.rmempty = false;
o = s.taboption('proxy', form.Flag, 'fake_ip_ping_hijack', _('Fake-IP Ping Hijack'));
o.rmempty = false;
s.tab('router', _('Router Proxy'));
o = s.taboption('router', form.Flag, 'router_proxy', _('Enable'));
o.rmempty = false;
o = s.taboption('router', form.SectionValue, '_router_access_control', form.TableSection, 'router_access_control', _('Access Control'));
o.retain = true;
o.depends('router_proxy', '1');
o.subsection.addremove = true;
o.subsection.anonymous = true;
o.subsection.sortable = true;
so = o.subsection.option(form.Flag, 'enabled', _('Enable'));
so.default = '1';
so.rmempty = false;
so = o.subsection.option(form.DynamicList, 'user', _('User'));
for (const user of users) {
so.value(user);
};
so = o.subsection.option(form.DynamicList, 'group', _('Group'));
for (const group of groups) {
so.value(group);
};
so = o.subsection.option(form.DynamicList, 'cgroup', _('CGroup'));
for (const cgroup of cgroups) {
so.value(cgroup);
};
so = o.subsection.option(form.Flag, 'proxy', _('Proxy'));
so.rmempty = false;
s.tab('lan', _('LAN Proxy'));
o = s.taboption('lan', form.Flag, 'lan_proxy', _('Enable'));
o.rmempty = false;
o = s.taboption('lan', form.DynamicList, 'lan_inbound_interface', _('Inbound Interface'));
o.retain = true;
o.rmempty = false;
o.depends('lan_proxy', '1');
for (const network of networks) {
if (network.getName() === 'loopback') {
continue;
}
o.value(network.getName());
}
o = s.taboption('lan', form.SectionValue, '_lan_access_control', form.TableSection, 'lan_access_control', _('Access Control'));
o.retain = true;
o.depends('lan_proxy', '1');
o.subsection.addremove = true;
o.subsection.anonymous = true;
o.subsection.sortable = true;
so = o.subsection.option(form.Flag, 'enabled', _('Enable'));
so.default = '1';
so.rmempty = false;
so = o.subsection.option(form.DynamicList, 'ip', 'IP');
for (const mac in hosts) {
const host = hosts[mac];
for (const ip of host.ipaddrs) {
const hint = host.name ?? mac;
so.value(ip, hint ? '%s (%s)'.format(ip, hint) : ip);
};
};
so = o.subsection.option(form.DynamicList, 'ip6', 'IP6');
for (const mac in hosts) {
const host = hosts[mac];
for (const ip of host.ip6addrs) {
const hint = host.name ?? mac;
so.value(ip, hint ? '%s (%s)'.format(ip, hint) : ip);
};
};
so = o.subsection.option(form.DynamicList, 'mac', 'MAC');
for (const mac in hosts) {
const host = hosts[mac];
const hint = host.name ?? host.ipaddrs[0];
so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
};
so = o.subsection.option(form.Flag, 'proxy', _('Proxy'));
so.rmempty = false;
s.tab('bypass', _('Bypass'));
o = s.taboption('bypass', form.Flag, 'bypass_china_mainland_ip', _('Bypass China Mainland IP'));
o.rmempty = false;
o = s.taboption('bypass', form.Value, 'proxy_tcp_dport', _('Destination TCP Port to Proxy'));
o.rmempty = false;
o.value('0-65535', _('All Port'));
o.value('21 22 80 110 143 194 443 465 853 993 995 8080 8443', _('Commonly Used Port'));
o = s.taboption('bypass', form.Value, 'proxy_udp_dport', _('Destination UDP Port to Proxy'));
o.rmempty = false;
o.value('0-65535', _('All Port'));
o.value('123 443 8443', _('Commonly Used Port'));
o = s.taboption('bypass', form.DynamicList, 'bypass_dscp', _('Bypass DSCP'));
o.datatype = 'range(0, 63)';
return m.render();
}
});

View File

@ -0,0 +1,945 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:120
msgid "API Listen"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:125
msgid "API Secret"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:69
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:119
msgid "Access Control"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:169
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:174
msgid "All Port"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:137
msgid "Allow Lan"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:262
msgid "Allow Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:94
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:13
msgid "App Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:27
msgid "App Log"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:43
msgid "App Version"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:485
msgid "Append Rule"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:416
msgid "Append Rule Provider"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:472
msgid "Behavior"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:261
msgid "Block Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:162
msgid "Bypass"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:164
msgid "Bypass China Mainland IP"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:177
msgid "Bypass DSCP"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:93
msgid "CGroup"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:29
msgid "Choose File"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:99
msgid "Choose Profile"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:31
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:64
msgid "Clear Log"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:170
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:175
msgid "Commonly Used Port"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:131
msgid "Core Environment Variable Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:60
msgid "Core Log"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:128
msgid "Core Only"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:57
msgid "Core Status"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:50
msgid "Core Version"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:117
msgid "Cron Expression"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:228
msgid "DNS Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:230
msgid "DNS Listen"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:241
msgid "DNS Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:93
msgid "Debug Log"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:507
msgid "Destination IP"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:511
msgid "Destination IP Geo"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:508
msgid "Destination Port"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:167
msgid "Destination TCP Port to Proxy"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:172
msgid "Destination UDP Port to Proxy"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:188
msgid "Device Name"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:44
msgid "Direct Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:67
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:73
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:79
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:85
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:132
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:140
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:206
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:216
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:238
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:267
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:273
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:279
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:285
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:291
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:360
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:366
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:372
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:558
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:38
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:45
msgid "Disable"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:147
msgid "Disable ECN of quic-go"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:144
msgid "Disable GSO of quic-go"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:141
msgid "Disable Loopback Detector"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:82
msgid "Disable TCP Keep Alive"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:276
msgid "DoH Prefer HTTP/3"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:308
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:503
msgid "Domain Name"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:510
msgid "Domain Name Geo"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:505
msgid "Domain Name Keyword"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:506
msgid "Domain Name Regex"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:504
msgid "Domain Name Suffix"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:168
msgid "Edit Authentications"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:222
msgid "Edit DNS Hijacks"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:254
msgid "Edit Fake-IP Filters"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:297
msgid "Edit Hosts"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:339
msgid "Edit Nameserver Policies"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:316
msgid "Edit Nameservers"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:419
msgid "Edit Rule Providers"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:488
msgid "Edit Rules"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:32
msgid "Edit Subscription"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:27
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:45
msgid "Editor"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:96
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:68
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:74
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:80
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:86
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:133
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:141
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:176
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:207
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:217
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:239
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:268
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:274
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:280
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:286
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:292
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:305
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:324
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:347
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:357
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:361
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:367
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:373
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:399
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:427
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:496
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:559
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:567
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:33
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:66
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:77
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:104
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:127
msgid "Enable"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:213
msgid "Endpoint Independent NAT"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:47
msgid "Expire At"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:104
msgid "External Control Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:264
msgid "Fake-IP Cache"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:258
msgid "Fake-IP Filter Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:61
msgid "Fake-IP Ping Hijack"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:246
msgid "Fake-IP Range"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:125
msgid "Fast Reload"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:466
msgid "File Format"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:460
msgid "File Path"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:454
msgid "File Size Limit"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:48
msgid "File for Mixin"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:50
msgid "File for Reserved IP"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:51
msgid "File for Reserved IP6"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:103
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:33
msgid "File:"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:378
msgid "Force Sniff Domain Name"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:203
msgid "GSO"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:209
msgid "GSO Max Size"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:28
msgid "General Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:97
msgid "Generate & Download"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:537
msgid "GeoData Loader"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:531
msgid "GeoIP Format"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:552
msgid "GeoIP(ASN) Url"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:549
msgid "GeoIP(DAT) Url"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:546
msgid "GeoIP(MMDB) Url"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:543
msgid "GeoSite Url"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:555
msgid "GeoX Auto Update"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:529
msgid "GeoX Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:561
msgid "GeoX Update Interval"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:96
msgid "Global Client Fingerprint"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:42
msgid "Global Mode"
msgstr ""
#: applications/luci-app-nikki/root/usr/share/rpcd/acl.d/luci-app-nikki.json:3
msgid "Grant access to nikki procedures"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:87
msgid "Group"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:143
msgid "HTTP Port"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:38
msgid "How To Use"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:49
msgid "IPv4 DNS Hijack"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:55
msgid "IPv4 Proxy"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:52
msgid "IPv6 DNS Hijack"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:58
msgid "IPv6 Proxy"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:385
msgid "Ignore Sniff Domain Name"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:135
msgid "Inbound Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:107
msgid "Inbound Interface"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:102
msgid "LAN Proxy"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:82
msgid "Local"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:25
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:53
msgid "Log"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:30
msgid "Log Level"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:199
msgid "MTU"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:46
msgid "Match Process"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:350
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:513
msgid "Matcher"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:541
msgid "Memory Conservative Loader"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:151
msgid "Mixed Port"
msgstr ""
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:29
msgid "Mixin Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:565
msgid "Mixin File Content"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:26
msgid "Mixin Option"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:39
msgid "Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:433
msgid "Name"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:334
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:353
msgid "Nameserver"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:38
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:3
msgid "Nikki"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:524
msgid "No Resolve"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:447
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:517
msgid "Node"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:15
msgid "Not Running"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:89
msgid "Open Dashboard"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:53
msgid "Outbound Interface"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:165
msgid "Overwrite Authentication"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:219
msgid "Overwrite DNS Hijack"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:411
msgid "Overwrite Destination"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:251
msgid "Overwrite Fake-IP Filter"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:375
msgid "Overwrite Force Sniff Domain Name"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:294
msgid "Overwrite Hosts"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:382
msgid "Overwrite Ignore Sniff Domain Name"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:313
msgid "Overwrite Nameserver"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:336
msgid "Overwrite Nameserver Policy"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:389
msgid "Overwrite Sniff By Protocol"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:182
msgid "Password"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:567
msgid "Please go to the editor tab to edit the file for mixin"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:408
msgid "Port"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:78
msgid "Prefer"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:509
msgid "Process Name"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:18
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:21
msgid "Profile"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:49
msgid "Profile for Startup"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:402
msgid "Protocol"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:99
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:159
msgid "Proxy"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:29
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:31
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:37
msgid "Proxy Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:45
msgid "Proxy Provider:"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:98
msgid "Random"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:39
msgid "Redirect Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:155
msgid "Redirect Port"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:69
msgid "Reload Service"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:81
msgid "Remote"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:270
msgid "Respect Rules"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:76
msgid "Restart Service"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:64
msgid "Router Proxy"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:414
msgid "Rule Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:43
msgid "Rule Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:41
msgid "Rule Provider:"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:502
msgid "Rule Set"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:15
msgid "Running"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:147
msgid "SOCKS Port"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:133
msgid "Safe Paths"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:129
msgid "Save Proxy Selection"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:114
msgid "Scheduled Restart"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:54
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:87
msgid "Scroll To Bottom"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:150
msgid "Skip System IPv6 Check"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:392
msgid "Sniff By Protocol"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:369
msgid "Sniff Pure IP"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:363
msgid "Sniff Redir-Host"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:355
msgid "Sniffer Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:192
msgid "Stack"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:540
msgid "Standard Loader"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:110
msgid "Start Delay"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:40
msgid "Status"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:28
msgid "Subscription"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:34
msgid "Subscription Name"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:66
msgid "Subscription Url"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:107
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:37
msgid "Subscription:"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:76
msgid "TCP Concurrent"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:88
msgid "TCP Keep Alive Idle"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:92
msgid "TCP Keep Alive Interval"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:36
msgid "TCP Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:40
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:46
msgid "TPROXY Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:160
msgid "TPROXY Port"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:186
msgid "TUN Config"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:41
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:47
msgid "TUN Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:122
msgid "Test Profile"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:42
msgid "Total"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:38
msgid "Transparent Proxy with Mihomo on OpenWrt."
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:327
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:436
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:500
msgid "Type"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:43
msgid "UDP Mode"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:109
msgid "UI Name"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:106
msgid "UI Path"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:112
msgid "UI Url"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:70
msgid "Unify Delay"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:32
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:41
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:48
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:55
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:66
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:72
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:78
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:84
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:90
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:94
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:97
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:107
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:110
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:113
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:122
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:127
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:131
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:139
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:145
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:149
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:153
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:157
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:162
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:189
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:194
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:201
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:205
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:211
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:215
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:232
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:237
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:242
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:248
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:260
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:266
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:272
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:278
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:284
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:290
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:359
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:365
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:371
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:533
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:539
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:544
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:547
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:550
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:553
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:557
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:563
msgid "Unmodified"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:60
msgid "Update"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:52
msgid "Update At"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:83
msgid "Update Dashboard"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:479
msgid "Update Interval"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:20
msgid "Upload Profile"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:442
msgid "Url"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:288
msgid "Use Hosts"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:282
msgid "Use System Hosts"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:37
msgid "Used"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:81
msgid "User"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:70
msgid "User Agent"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:179
msgid "Username"
msgstr ""

View File

@ -0,0 +1,952 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: PACKAGE VERSION\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:120
msgid "API Listen"
msgstr "API 监听"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:125
msgid "API Secret"
msgstr "API 密钥"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:69
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:119
msgid "Access Control"
msgstr "访问控制"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:169
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:174
msgid "All Port"
msgstr "全部端口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:137
msgid "Allow Lan"
msgstr "允许局域网访问"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:262
msgid "Allow Mode"
msgstr "白名单模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:94
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:13
msgid "App Config"
msgstr "插件配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:27
msgid "App Log"
msgstr "插件日志"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:43
msgid "App Version"
msgstr "插件版本"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:485
msgid "Append Rule"
msgstr "追加规则"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:416
msgid "Append Rule Provider"
msgstr "追加规则提供者"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:472
msgid "Behavior"
msgstr "行为"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:261
msgid "Block Mode"
msgstr "黑名单模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:162
msgid "Bypass"
msgstr "绕过"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:164
msgid "Bypass China Mainland IP"
msgstr "绕过中国大陆 IP"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:177
msgid "Bypass DSCP"
msgstr "绕过 DSCP"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:93
msgid "CGroup"
msgstr "控制组"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:29
msgid "Choose File"
msgstr "选择文件"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:99
msgid "Choose Profile"
msgstr "选择配置文件"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:31
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:64
msgid "Clear Log"
msgstr "清空日志"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:170
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:175
msgid "Commonly Used Port"
msgstr "常用端口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:131
msgid "Core Environment Variable Config"
msgstr "核心环境变量配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:60
msgid "Core Log"
msgstr "核心日志"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:128
msgid "Core Only"
msgstr "仅核心"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:57
msgid "Core Status"
msgstr "核心状态"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:50
msgid "Core Version"
msgstr "核心版本"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:117
msgid "Cron Expression"
msgstr "Cron 表达式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:228
msgid "DNS Config"
msgstr "DNS 配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:230
msgid "DNS Listen"
msgstr "DNS 监听"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:241
msgid "DNS Mode"
msgstr "DNS 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:93
msgid "Debug Log"
msgstr "调试日志"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:507
msgid "Destination IP"
msgstr "目标 IP"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:511
msgid "Destination IP Geo"
msgstr "目标 IPGeo"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:508
msgid "Destination Port"
msgstr "目标端口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:167
msgid "Destination TCP Port to Proxy"
msgstr "要代理的 TCP 目标端口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:172
msgid "Destination UDP Port to Proxy"
msgstr "要代理的 UDP 目标端口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:188
msgid "Device Name"
msgstr "设备名称"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:44
msgid "Direct Mode"
msgstr "直连模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:67
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:73
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:79
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:85
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:132
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:140
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:206
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:216
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:238
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:267
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:273
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:279
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:285
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:291
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:360
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:366
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:372
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:558
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:38
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:45
msgid "Disable"
msgstr "禁用"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:147
msgid "Disable ECN of quic-go"
msgstr "禁用 quic-go 的显式拥塞通知"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:144
msgid "Disable GSO of quic-go"
msgstr "禁用 quic-go 的通用分段卸载"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:141
msgid "Disable Loopback Detector"
msgstr "禁用回环检测"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:82
msgid "Disable TCP Keep Alive"
msgstr "禁用 TCP Keep Alive"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:276
msgid "DoH Prefer HTTP/3"
msgstr "DoH 优先 HTTP/3"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:308
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:503
msgid "Domain Name"
msgstr "域名"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:510
msgid "Domain Name Geo"
msgstr "域名Geo"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:505
msgid "Domain Name Keyword"
msgstr "域名(关键字)"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:506
msgid "Domain Name Regex"
msgstr "域名(正则表达式)"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:504
msgid "Domain Name Suffix"
msgstr "域名(后缀)"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:168
msgid "Edit Authentications"
msgstr "编辑身份验证"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:222
msgid "Edit DNS Hijacks"
msgstr "编辑 DNS 劫持"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:254
msgid "Edit Fake-IP Filters"
msgstr "编辑 Fake-IP 过滤列表"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:297
msgid "Edit Hosts"
msgstr "编辑 Hosts"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:339
msgid "Edit Nameserver Policies"
msgstr "编辑 DNS 服务器查询策略"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:316
msgid "Edit Nameservers"
msgstr "编辑 DNS 服务器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:419
msgid "Edit Rule Providers"
msgstr "编辑规则提供者"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:488
msgid "Edit Rules"
msgstr "编辑规则"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:32
msgid "Edit Subscription"
msgstr "编辑订阅"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:27
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:45
msgid "Editor"
msgstr "编辑器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:96
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:68
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:74
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:80
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:86
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:133
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:141
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:176
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:207
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:217
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:239
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:268
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:274
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:280
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:286
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:292
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:305
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:324
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:347
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:357
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:361
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:367
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:373
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:399
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:427
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:496
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:559
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:567
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:33
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:66
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:77
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:104
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:127
msgid "Enable"
msgstr "启用"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:213
msgid "Endpoint Independent NAT"
msgstr "独立于端点的 NAT"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:47
msgid "Expire At"
msgstr "到期时间"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:104
msgid "External Control Config"
msgstr "外部控制配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:264
msgid "Fake-IP Cache"
msgstr "Fake-IP 缓存"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:258
msgid "Fake-IP Filter Mode"
msgstr "Fake-IP 过滤模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:61
msgid "Fake-IP Ping Hijack"
msgstr "Fake-IP Ping 劫持"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:246
msgid "Fake-IP Range"
msgstr "Fake-IP 范围"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:125
msgid "Fast Reload"
msgstr "快速重载"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:466
msgid "File Format"
msgstr "文件格式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:460
msgid "File Path"
msgstr "文件路径"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:454
msgid "File Size Limit"
msgstr "文件大小限制"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:48
msgid "File for Mixin"
msgstr "用于混入的文件"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:50
msgid "File for Reserved IP"
msgstr "IPv4 保留地址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:51
msgid "File for Reserved IP6"
msgstr "IPv6 保留地址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:103
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:33
msgid "File:"
msgstr "文件:"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:378
msgid "Force Sniff Domain Name"
msgstr "强制嗅探的域名"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:203
msgid "GSO"
msgstr "通用分段卸载"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:209
msgid "GSO Max Size"
msgstr "分段最大长度"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:28
msgid "General Config"
msgstr "全局配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:97
msgid "Generate & Download"
msgstr "生成并下载"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:537
msgid "GeoData Loader"
msgstr "GeoData 加载器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:531
msgid "GeoIP Format"
msgstr "GeoIP 格式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:552
msgid "GeoIP(ASN) Url"
msgstr "GeoIP(ASN) 下载地址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:549
msgid "GeoIP(DAT) Url"
msgstr "GeoIP(DAT) 下载地址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:546
msgid "GeoIP(MMDB) Url"
msgstr "GeoIP(MMDB) 下载地址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:543
msgid "GeoSite Url"
msgstr "GeoSite 下载地址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:555
msgid "GeoX Auto Update"
msgstr "定时更新GeoX文件"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:529
msgid "GeoX Config"
msgstr "GeoX 配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:561
msgid "GeoX Update Interval"
msgstr "GeoX 文件更新间隔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:96
msgid "Global Client Fingerprint"
msgstr "全局客户端指纹"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:42
msgid "Global Mode"
msgstr "全局模式"
#: applications/luci-app-nikki/root/usr/share/rpcd/acl.d/luci-app-nikki.json:3
msgid "Grant access to nikki procedures"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:87
msgid "Group"
msgstr "用户组"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:143
msgid "HTTP Port"
msgstr "HTTP 端口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:38
msgid "How To Use"
msgstr "使用说明"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:49
msgid "IPv4 DNS Hijack"
msgstr "IPv4 DNS 劫持"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:55
msgid "IPv4 Proxy"
msgstr "IPv4 代理"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:52
msgid "IPv6 DNS Hijack"
msgstr "IPv6 DNS 劫持"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:58
msgid "IPv6 Proxy"
msgstr "IPv6 代理"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:385
msgid "Ignore Sniff Domain Name"
msgstr "忽略嗅探的域名"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:135
msgid "Inbound Config"
msgstr "入站配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:107
msgid "Inbound Interface"
msgstr "入站接口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:102
msgid "LAN Proxy"
msgstr "局域网代理"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:82
msgid "Local"
msgstr "本地"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:25
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:53
msgid "Log"
msgstr "日志"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:30
msgid "Log Level"
msgstr "日志级别"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:199
msgid "MTU"
msgstr "最大传输单元"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:46
msgid "Match Process"
msgstr "匹配进程"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:350
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:513
msgid "Matcher"
msgstr "匹配"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:541
msgid "Memory Conservative Loader"
msgstr "为内存受限设备优化的加载器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:151
msgid "Mixed Port"
msgstr "混合端口"
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:29
msgid "Mixin Config"
msgstr "混入配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:565
msgid "Mixin File Content"
msgstr "混入文件内容"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:26
msgid "Mixin Option"
msgstr "混入选项"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:39
msgid "Mode"
msgstr "模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:433
msgid "Name"
msgstr "名称"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:334
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:353
msgid "Nameserver"
msgstr "DNS 服务器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:38
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:3
msgid "Nikki"
msgstr ""
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:524
msgid "No Resolve"
msgstr "不解析"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:447
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:517
msgid "Node"
msgstr "节点"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:15
msgid "Not Running"
msgstr "未在运行"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:89
msgid "Open Dashboard"
msgstr "打开面板"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:53
msgid "Outbound Interface"
msgstr "出站接口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:165
msgid "Overwrite Authentication"
msgstr "覆盖身份验证"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:219
msgid "Overwrite DNS Hijack"
msgstr "覆盖 DNS 劫持"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:411
msgid "Overwrite Destination"
msgstr "将嗅探结果作为连接目标"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:251
msgid "Overwrite Fake-IP Filter"
msgstr "覆盖 Fake-IP 过滤列表"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:375
msgid "Overwrite Force Sniff Domain Name"
msgstr "覆盖强制嗅探的域名"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:294
msgid "Overwrite Hosts"
msgstr "覆盖 Hosts"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:382
msgid "Overwrite Ignore Sniff Domain Name"
msgstr "覆盖忽略嗅探的域名"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:313
msgid "Overwrite Nameserver"
msgstr "覆盖 DNS 服务器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:336
msgid "Overwrite Nameserver Policy"
msgstr "覆盖 DNS 服务器查询策略"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:389
msgid "Overwrite Sniff By Protocol"
msgstr "覆盖按协议嗅探"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:182
msgid "Password"
msgstr "密码"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:567
msgid "Please go to the editor tab to edit the file for mixin"
msgstr "请前往编辑器标签编辑用于混入的文件"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:408
msgid "Port"
msgstr "端口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:78
msgid "Prefer"
msgstr "优先"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:509
msgid "Process Name"
msgstr "进程名"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:18
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:21
msgid "Profile"
msgstr "配置文件"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:49
msgid "Profile for Startup"
msgstr "用于启动的配置文件"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:402
msgid "Protocol"
msgstr "协议"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:99
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:159
msgid "Proxy"
msgstr "代理"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:29
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:31
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:37
msgid "Proxy Config"
msgstr "代理配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:45
msgid "Proxy Provider:"
msgstr "代理提供者:"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:98
msgid "Random"
msgstr "随机"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:39
msgid "Redirect Mode"
msgstr "Redirect 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:155
msgid "Redirect Port"
msgstr "Redirect 端口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:69
msgid "Reload Service"
msgstr "重载服务"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:81
msgid "Remote"
msgstr "远程"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:270
msgid "Respect Rules"
msgstr "遵循分流规则"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:76
msgid "Restart Service"
msgstr "重启服务"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:64
msgid "Router Proxy"
msgstr "路由器代理"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:414
msgid "Rule Config"
msgstr "规则配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:43
msgid "Rule Mode"
msgstr "规则模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:41
msgid "Rule Provider:"
msgstr "规则提供者:"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:502
msgid "Rule Set"
msgstr "规则集"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:15
msgid "Running"
msgstr "运行中"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:147
msgid "SOCKS Port"
msgstr "SOCKS 端口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:133
msgid "Safe Paths"
msgstr "安全路径"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:129
msgid "Save Proxy Selection"
msgstr "保存节点/策略组选择"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:114
msgid "Scheduled Restart"
msgstr "定时重启"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:54
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:87
msgid "Scroll To Bottom"
msgstr "滚动到底部"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:150
msgid "Skip System IPv6 Check"
msgstr "跳过系统 IPv6 检查"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:392
msgid "Sniff By Protocol"
msgstr "按协议嗅探"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:369
msgid "Sniff Pure IP"
msgstr "嗅探纯 IP 连接"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:363
msgid "Sniff Redir-Host"
msgstr "嗅探 Redir-Host 流量"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:355
msgid "Sniffer Config"
msgstr "嗅探器配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:192
msgid "Stack"
msgstr "栈"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:540
msgid "Standard Loader"
msgstr "标准加载器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:110
msgid "Start Delay"
msgstr "启动延迟"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:40
msgid "Status"
msgstr "状态"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:28
msgid "Subscription"
msgstr "订阅"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:34
msgid "Subscription Name"
msgstr "订阅名称"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:66
msgid "Subscription Url"
msgstr "订阅链接"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:107
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:37
msgid "Subscription:"
msgstr "订阅:"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:76
msgid "TCP Concurrent"
msgstr "TCP 并发"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:88
msgid "TCP Keep Alive Idle"
msgstr "TCP Keep Alive 空闲"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:92
msgid "TCP Keep Alive Interval"
msgstr "TCP Keep Alive 间隔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:36
msgid "TCP Mode"
msgstr "TCP 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:40
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:46
msgid "TPROXY Mode"
msgstr "TPROXY 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:160
msgid "TPROXY Port"
msgstr "TPROXY 端口"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:186
msgid "TUN Config"
msgstr "TUN 配置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:41
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:47
msgid "TUN Mode"
msgstr "TUN 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:122
msgid "Test Profile"
msgstr "检查配置文件"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:42
msgid "Total"
msgstr "总量"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:38
msgid "Transparent Proxy with Mihomo on OpenWrt."
msgstr "在 OpenWrt 上使用 Mihomo 进行透明代理。"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:327
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:436
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:500
msgid "Type"
msgstr "类型"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:43
msgid "UDP Mode"
msgstr "UDP 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:109
msgid "UI Name"
msgstr "UI 名称"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:106
msgid "UI Path"
msgstr "UI 路径"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:112
msgid "UI Url"
msgstr "UI 下载地址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:70
msgid "Unify Delay"
msgstr "统一延迟"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:32
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:41
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:48
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:55
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:66
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:72
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:78
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:84
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:90
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:94
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:97
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:107
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:110
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:113
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:122
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:127
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:131
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:139
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:145
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:149
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:153
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:157
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:162
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:189
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:194
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:201
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:205
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:211
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:215
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:232
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:237
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:242
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:248
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:260
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:266
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:272
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:278
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:284
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:290
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:359
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:365
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:371
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:533
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:539
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:544
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:547
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:550
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:553
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:557
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:563
msgid "Unmodified"
msgstr "不修改"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:60
msgid "Update"
msgstr "更新"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:52
msgid "Update At"
msgstr "更新时间"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:83
msgid "Update Dashboard"
msgstr "更新面板"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:479
msgid "Update Interval"
msgstr "更新间隔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:20
msgid "Upload Profile"
msgstr "上传配置文件"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:442
msgid "Url"
msgstr "下载地址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:288
msgid "Use Hosts"
msgstr "使用 Hosts"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:282
msgid "Use System Hosts"
msgstr "使用系统的 Hosts"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:37
msgid "Used"
msgstr "已使用"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:81
msgid "User"
msgstr "用户"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:70
msgid "User Agent"
msgstr "用户代理UA"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:179
msgid "Username"
msgstr "用户名"

View File

@ -0,0 +1,952 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Project-Id-Version: PACKAGE VERSION\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: zh_Hant\n"
"MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:120
msgid "API Listen"
msgstr "API 監聽"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:125
msgid "API Secret"
msgstr "API 密鑰"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:69
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:119
msgid "Access Control"
msgstr "存取控制"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:169
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:174
msgid "All Port"
msgstr "所有埠"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:137
msgid "Allow Lan"
msgstr "允許區域網路存取"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:262
msgid "Allow Mode"
msgstr "白名單模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:94
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:13
msgid "App Config"
msgstr "應用程式設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:27
msgid "App Log"
msgstr "應用程式日誌"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:43
msgid "App Version"
msgstr "應用程式版本"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:485
msgid "Append Rule"
msgstr "追加規則"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:416
msgid "Append Rule Provider"
msgstr "追加規則提供者"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:472
msgid "Behavior"
msgstr "行為"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:261
msgid "Block Mode"
msgstr "黑名單模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:162
msgid "Bypass"
msgstr "繞過"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:164
msgid "Bypass China Mainland IP"
msgstr "繞過中國大陸 IP"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:177
msgid "Bypass DSCP"
msgstr "繞過 DSCP"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:93
msgid "CGroup"
msgstr "控制群組"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:29
msgid "Choose File"
msgstr "選擇檔案"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:99
msgid "Choose Profile"
msgstr "選擇設定檔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:31
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:64
msgid "Clear Log"
msgstr "清空日誌"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:170
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:175
msgid "Commonly Used Port"
msgstr "常用埠"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:131
msgid "Core Environment Variable Config"
msgstr "核心環境變數設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:60
msgid "Core Log"
msgstr "核心日誌"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:128
msgid "Core Only"
msgstr "僅核心"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:57
msgid "Core Status"
msgstr "核心狀態"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:50
msgid "Core Version"
msgstr "核心版本"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:117
msgid "Cron Expression"
msgstr "Cron 表達式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:228
msgid "DNS Config"
msgstr "DNS 設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:230
msgid "DNS Listen"
msgstr "DNS 監聽"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:241
msgid "DNS Mode"
msgstr "DNS 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:93
msgid "Debug Log"
msgstr "除錯日誌"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:507
msgid "Destination IP"
msgstr "目標 IP"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:511
msgid "Destination IP Geo"
msgstr "目標 IP地理位置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:508
msgid "Destination Port"
msgstr "目標埠"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:167
msgid "Destination TCP Port to Proxy"
msgstr "要代理的 TCP 目標埠"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:172
msgid "Destination UDP Port to Proxy"
msgstr "要代理的 UDP 目標埠"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:188
msgid "Device Name"
msgstr "裝置名稱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:44
msgid "Direct Mode"
msgstr "直連模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:67
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:73
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:79
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:85
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:132
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:140
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:206
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:216
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:238
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:267
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:273
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:279
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:285
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:291
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:360
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:366
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:372
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:558
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:38
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:45
msgid "Disable"
msgstr "停用"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:147
msgid "Disable ECN of quic-go"
msgstr "停用 quic-go 的顯式擁塞通知"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:144
msgid "Disable GSO of quic-go"
msgstr "停用 quic-go 的通用分段卸載"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:141
msgid "Disable Loopback Detector"
msgstr "停用迴路檢測"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:82
msgid "Disable TCP Keep Alive"
msgstr "停用 TCP Keep Alive"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:276
msgid "DoH Prefer HTTP/3"
msgstr "DoH 優先使用 HTTP/3"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:308
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:503
msgid "Domain Name"
msgstr "網域名稱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:510
msgid "Domain Name Geo"
msgstr "網域名稱(地理位置)"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:505
msgid "Domain Name Keyword"
msgstr "網域名稱(關鍵字)"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:506
msgid "Domain Name Regex"
msgstr "網域名稱(正則表達式)"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:504
msgid "Domain Name Suffix"
msgstr "網域名稱(後綴)"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:168
msgid "Edit Authentications"
msgstr "編輯身分驗證"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:222
msgid "Edit DNS Hijacks"
msgstr "編輯 DNS 劫持"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:254
msgid "Edit Fake-IP Filters"
msgstr "編輯 Fake-IP 過濾清單"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:297
msgid "Edit Hosts"
msgstr "編輯 Hosts"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:339
msgid "Edit Nameserver Policies"
msgstr "編輯 DNS 伺服器查詢策略"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:316
msgid "Edit Nameservers"
msgstr "編輯 DNS 伺服器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:419
msgid "Edit Rule Providers"
msgstr "編輯規則提供者"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:488
msgid "Edit Rules"
msgstr "編輯規則"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:32
msgid "Edit Subscription"
msgstr "編輯訂閱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:27
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:45
msgid "Editor"
msgstr "編輯器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:96
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:68
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:74
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:80
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:86
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:133
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:141
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:176
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:207
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:217
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:239
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:268
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:274
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:280
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:286
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:292
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:305
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:324
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:347
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:357
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:361
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:367
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:373
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:399
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:427
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:496
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:559
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:567
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:33
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:66
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:77
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:104
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:127
msgid "Enable"
msgstr "啟用"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:213
msgid "Endpoint Independent NAT"
msgstr "端點獨立 NAT"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:47
msgid "Expire At"
msgstr "到期時間"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:104
msgid "External Control Config"
msgstr "外部控制設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:264
msgid "Fake-IP Cache"
msgstr "Fake-IP 快取"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:258
msgid "Fake-IP Filter Mode"
msgstr "Fake-IP 過濾模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:61
msgid "Fake-IP Ping Hijack"
msgstr "Fake-IP Ping 劫持"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:246
msgid "Fake-IP Range"
msgstr "Fake-IP 範圍"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:125
msgid "Fast Reload"
msgstr "快速重載"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:466
msgid "File Format"
msgstr "檔案格式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:460
msgid "File Path"
msgstr "檔案路徑"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:454
msgid "File Size Limit"
msgstr "檔案大小限制"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:48
msgid "File for Mixin"
msgstr "用於混入的檔案"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:50
msgid "File for Reserved IP"
msgstr "IPv4 保留地址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:51
msgid "File for Reserved IP6"
msgstr "IPv6 保留地址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:103
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:33
msgid "File:"
msgstr "檔案:"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:378
msgid "Force Sniff Domain Name"
msgstr "強制嗅探的網域名稱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:203
msgid "GSO"
msgstr "通用分段卸載"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:209
msgid "GSO Max Size"
msgstr "分段最大長度"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:28
msgid "General Config"
msgstr "一般設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:97
msgid "Generate & Download"
msgstr "生成並下載"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:537
msgid "GeoData Loader"
msgstr "GeoData 載入器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:531
msgid "GeoIP Format"
msgstr "GeoIP 格式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:552
msgid "GeoIP(ASN) Url"
msgstr "GeoIP(ASN) 下載網址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:549
msgid "GeoIP(DAT) Url"
msgstr "GeoIP(DAT) 下載網址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:546
msgid "GeoIP(MMDB) Url"
msgstr "GeoIP(MMDB) 下載網址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:543
msgid "GeoSite Url"
msgstr "GeoSite 下載網址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:555
msgid "GeoX Auto Update"
msgstr "定時更新 GeoX 檔案"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:529
msgid "GeoX Config"
msgstr "GeoX 設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:561
msgid "GeoX Update Interval"
msgstr "GeoX 檔案更新間隔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:96
msgid "Global Client Fingerprint"
msgstr "全局客戶端指紋"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:42
msgid "Global Mode"
msgstr "全局模式"
#: applications/luci-app-nikki/root/usr/share/rpcd/acl.d/luci-app-nikki.json:3
msgid "Grant access to nikki procedures"
msgstr "授予存取 nikki 程序的權限"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:87
msgid "Group"
msgstr "使用者群組"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:143
msgid "HTTP Port"
msgstr "HTTP 埠"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:38
msgid "How To Use"
msgstr "使用說明"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:49
msgid "IPv4 DNS Hijack"
msgstr "IPv4 DNS 劫持"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:55
msgid "IPv4 Proxy"
msgstr "IPv4 代理"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:52
msgid "IPv6 DNS Hijack"
msgstr "IPv6 DNS 劫持"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:58
msgid "IPv6 Proxy"
msgstr "IPv6 代理"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:385
msgid "Ignore Sniff Domain Name"
msgstr "忽略嗅探的網域名稱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:135
msgid "Inbound Config"
msgstr "入站設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:107
msgid "Inbound Interface"
msgstr "入站介面"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:102
msgid "LAN Proxy"
msgstr "區域網路代理"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:82
msgid "Local"
msgstr "本地"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:25
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:53
msgid "Log"
msgstr "日誌"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:30
msgid "Log Level"
msgstr "日誌等級"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:199
msgid "MTU"
msgstr "最大傳輸單元"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:46
msgid "Match Process"
msgstr "匹配程序"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:350
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:513
msgid "Matcher"
msgstr "匹配器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:541
msgid "Memory Conservative Loader"
msgstr "為記憶體受限裝置最佳化的載入器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:151
msgid "Mixed Port"
msgstr "混合埠"
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:29
msgid "Mixin Config"
msgstr "混入設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:565
msgid "Mixin File Content"
msgstr "混入檔案內容"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:26
msgid "Mixin Option"
msgstr "混入選項"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:39
msgid "Mode"
msgstr "模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:433
msgid "Name"
msgstr "名稱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:334
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:353
msgid "Nameserver"
msgstr "DNS 伺服器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:38
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:3
msgid "Nikki"
msgstr "Nikki"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:524
msgid "No Resolve"
msgstr "不解析"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:447
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:517
msgid "Node"
msgstr "節點"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:15
msgid "Not Running"
msgstr "未在執行"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:89
msgid "Open Dashboard"
msgstr "開啟儀表板"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:53
msgid "Outbound Interface"
msgstr "出站介面"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:165
msgid "Overwrite Authentication"
msgstr "覆寫身分驗證"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:219
msgid "Overwrite DNS Hijack"
msgstr "覆寫 DNS 劫持"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:411
msgid "Overwrite Destination"
msgstr "將嗅探結果作為連線目標"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:251
msgid "Overwrite Fake-IP Filter"
msgstr "覆寫 Fake-IP 過濾清單"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:375
msgid "Overwrite Force Sniff Domain Name"
msgstr "覆寫強制嗅探的網域名稱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:294
msgid "Overwrite Hosts"
msgstr "覆寫 Hosts"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:382
msgid "Overwrite Ignore Sniff Domain Name"
msgstr "覆寫忽略嗅探的網域名稱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:313
msgid "Overwrite Nameserver"
msgstr "覆寫 DNS 伺服器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:336
msgid "Overwrite Nameserver Policy"
msgstr "覆寫 DNS 伺服器查詢策略"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:389
msgid "Overwrite Sniff By Protocol"
msgstr "覆寫按協定嗅探"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:182
msgid "Password"
msgstr "密碼"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:567
msgid "Please go to the editor tab to edit the file for mixin"
msgstr "請前往編輯器標籤編輯用於混入的檔案"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:408
msgid "Port"
msgstr "埠"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:78
msgid "Prefer"
msgstr "優先"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:509
msgid "Process Name"
msgstr "程序名稱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:18
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:21
msgid "Profile"
msgstr "設定檔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:49
msgid "Profile for Startup"
msgstr "用於啟動的設定檔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:402
msgid "Protocol"
msgstr "協定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:99
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:159
msgid "Proxy"
msgstr "代理"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:29
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:31
#: applications/luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json:37
msgid "Proxy Config"
msgstr "代理設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:45
msgid "Proxy Provider:"
msgstr "代理提供者:"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:98
msgid "Random"
msgstr "隨機"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:39
msgid "Redirect Mode"
msgstr "Redirect 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:155
msgid "Redirect Port"
msgstr "Redirect 埠"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:69
msgid "Reload Service"
msgstr "重新載入服務"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:81
msgid "Remote"
msgstr "遠端"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:270
msgid "Respect Rules"
msgstr "遵循分流規則"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:76
msgid "Restart Service"
msgstr "重新啟動服務"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:64
msgid "Router Proxy"
msgstr "路由器代理"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:414
msgid "Rule Config"
msgstr "規則設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:43
msgid "Rule Mode"
msgstr "規則模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:41
msgid "Rule Provider:"
msgstr "規則提供者:"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:502
msgid "Rule Set"
msgstr "規則集"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:15
msgid "Running"
msgstr "執行中"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:147
msgid "SOCKS Port"
msgstr "SOCKS 埠"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:133
msgid "Safe Paths"
msgstr "安全路徑"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:129
msgid "Save Proxy Selection"
msgstr "儲存節點/策略群組選擇"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:114
msgid "Scheduled Restart"
msgstr "定時重新啟動"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:54
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js:87
msgid "Scroll To Bottom"
msgstr "捲動到底部"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:150
msgid "Skip System IPv6 Check"
msgstr "跳過系統 IPv6 檢查"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:392
msgid "Sniff By Protocol"
msgstr "按協定嗅探"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:369
msgid "Sniff Pure IP"
msgstr "嗅探純 IP 連線"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:363
msgid "Sniff Redir-Host"
msgstr "嗅探 Redir-Host 流量"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:355
msgid "Sniffer Config"
msgstr "嗅探器設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:192
msgid "Stack"
msgstr "堆疊"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:540
msgid "Standard Loader"
msgstr "標準載入器"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:110
msgid "Start Delay"
msgstr "啟動延遲"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:40
msgid "Status"
msgstr "狀態"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:28
msgid "Subscription"
msgstr "訂閱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:34
msgid "Subscription Name"
msgstr "訂閱名稱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:66
msgid "Subscription Url"
msgstr "訂閱連結"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:107
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js:37
msgid "Subscription:"
msgstr "訂閱:"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:76
msgid "TCP Concurrent"
msgstr "TCP 並發"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:88
msgid "TCP Keep Alive Idle"
msgstr "TCP Keep Alive 閒置"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:92
msgid "TCP Keep Alive Interval"
msgstr "TCP Keep Alive 間隔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:36
msgid "TCP Mode"
msgstr "TCP 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:40
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:46
msgid "TPROXY Mode"
msgstr "TPROXY 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:160
msgid "TPROXY Port"
msgstr "TPROXY 埠"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:186
msgid "TUN Config"
msgstr "TUN 設定"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:41
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:47
msgid "TUN Mode"
msgstr "TUN 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:122
msgid "Test Profile"
msgstr "檢查設定檔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:42
msgid "Total"
msgstr "總計"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:38
msgid "Transparent Proxy with Mihomo on OpenWrt."
msgstr "在 OpenWrt 上使用 Mihomo 進行透明代理."
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:327
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:436
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:500
msgid "Type"
msgstr "類型"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:43
msgid "UDP Mode"
msgstr "UDP 模式"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:109
msgid "UI Name"
msgstr "UI 名稱"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:106
msgid "UI Path"
msgstr "UI 路徑"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:112
msgid "UI Url"
msgstr "UI 下載網址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:70
msgid "Unify Delay"
msgstr "統一延遲"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:32
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:41
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:48
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:55
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:66
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:72
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:78
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:84
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:90
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:94
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:97
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:107
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:110
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:113
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:122
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:127
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:131
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:139
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:145
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:149
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:153
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:157
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:162
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:189
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:194
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:201
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:205
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:211
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:215
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:232
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:237
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:242
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:248
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:260
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:266
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:272
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:278
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:284
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:290
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:359
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:365
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:371
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:533
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:539
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:544
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:547
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:550
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:553
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:557
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:563
msgid "Unmodified"
msgstr "不修改"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:60
msgid "Update"
msgstr "更新"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:52
msgid "Update At"
msgstr "更新時間"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js:83
msgid "Update Dashboard"
msgstr "更新儀表板"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:479
msgid "Update Interval"
msgstr "更新間隔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:20
msgid "Upload Profile"
msgstr "上傳設定檔"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:442
msgid "Url"
msgstr "下載網址"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:288
msgid "Use Hosts"
msgstr "使用 Hosts"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:282
msgid "Use System Hosts"
msgstr "使用系統的 Hosts"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:37
msgid "Used"
msgstr "已使用"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js:81
msgid "User"
msgstr "使用者"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js:70
msgid "User Agent"
msgstr "使用者代理UA"
#: applications/luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js:179
msgid "Username"
msgstr "使用者名稱"

View File

@ -0,0 +1,60 @@
{
"admin/services/nikki": {
"title": "Nikki",
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-nikki" ],
"uci": { "nikki": true }
}
},
"admin/services/nikki/config": {
"title": "App Config",
"order": 10,
"action": {
"type": "view",
"path": "nikki/app"
}
},
"admin/services/nikki/profile": {
"title": "Profile",
"order": 20,
"action": {
"type": "view",
"path": "nikki/profile"
}
},
"admin/services/nikki/mixin": {
"title": "Mixin Config",
"order": 30,
"action": {
"type": "view",
"path": "nikki/mixin"
}
},
"admin/services/nikki/proxy": {
"title": "Proxy Config",
"order": 40,
"action": {
"type": "view",
"path": "nikki/proxy"
}
},
"admin/services/nikki/editor": {
"title": "Editor",
"order": 50,
"action": {
"type": "view",
"path": "nikki/editor"
}
},
"admin/services/nikki/log": {
"title": "Log",
"order": 60,
"action": {
"type": "view",
"path": "nikki/log"
}
}
}

View File

@ -0,0 +1,41 @@
{
"luci-app-nikki": {
"description": "Grant access to nikki procedures",
"read": {
"uci": [ "nikki" ],
"ubus": {
"rc": [ "*" ],
"luci.nikki": [ "*" ]
},
"file": {
"/etc/nikki/profiles/*.yaml": ["read"],
"/etc/nikki/profiles/*.yml": ["read"],
"/etc/nikki/subscriptions/*.yaml": ["read"],
"/etc/nikki/subscriptions/*.yml": ["read"],
"/etc/nikki/mixin.yaml": ["read"],
"/etc/nikki/run/config.yaml": ["read"],
"/etc/nikki/run/providers/rule/*": ["read"],
"/etc/nikki/run/providers/proxy/*": ["read"],
"/etc/nikki/nftables/reserved_ip.nft": ["read"],
"/etc/nikki/nftables/reserved_ip6.nft": ["read"],
"/var/log/nikki/*.log": ["read"]
}
},
"write": {
"uci": [ "nikki" ],
"file": {
"/etc/nikki/profiles/*.yaml": ["write"],
"/etc/nikki/profiles/*.yml": ["write"],
"/etc/nikki/subscriptions/*.yaml": ["write"],
"/etc/nikki/subscriptions/*.yml": ["write"],
"/etc/nikki/mixin.yaml": ["write"],
"/etc/nikki/run/config.yaml": ["write"],
"/etc/nikki/run/providers/rule/*": ["write"],
"/etc/nikki/run/providers/proxy/*": ["write"],
"/etc/nikki/nftables/reserved_ip.nft": ["write"],
"/etc/nikki/nftables/reserved_ip6.nft": ["write"],
"/var/log/nikki/*.log": ["write"]
}
}
}
}

View File

@ -0,0 +1,81 @@
#!/usr/bin/ucode
'use strict';
import { access, popen, writefile } from 'fs';
import { get_users, get_groups, get_cgroups } from '/etc/nikki/ucode/include.uc';
const methods = {
version: {
call: function() {
let process;
let app = '';
if (system('command -v opkg') == 0) {
process = popen('opkg list-installed luci-app-nikki | cut -d " " -f 3');
if (process) {
app = trim(process.read('all'));
process.close();
}
} else if (system('command -v apk') == 0) {
process = popen('apk list -I luci-app-nikki | cut -d " " -f 1 | cut -d "-" -f 4');
if (process) {
app = trim(process.read('all'));
process.close();
}
}
let core = '';
process = popen('mihomo -v | grep Mihomo | cut -d " " -f 3');
if (process) {
core = trim(process.read('all'));
process.close();
}
return { app: app, core: core };
}
},
profile: {
args: { defaults: {} },
call: function(req) {
let profile = {};
const defaults = req.args?.defaults ?? {};
const filepath = '/etc/nikki/run/config.yaml';
const tmpFilepath = '/var/run/nikki/profile.json';
if (access(filepath, 'r')) {
writefile(tmpFilepath, defaults);
const command = `yq -p yaml -o json eval-all 'select(fi == 0) *? select(fi == 1)' ${tmpFilepath} ${filepath}`;
const process = popen(command);
if (process) {
profile = json(process);
process.close();
}
}
return profile;
}
},
update_subscription: {
args: { section_id: 'section_id' },
call: function(req) {
let success = false;
const section_id = req.args?.section_id;
if (section_id) {
success = system(['service', 'nikki', 'update_subscription', section_id]) == 0;
}
return { success: success };
}
},
get_identifiers: {
call: function() {
const users = filter(get_users(), (x) => x != '');
const groups = filter(get_groups(), (x) => x != '');
const cgroups = filter(get_cgroups(), (x) => x != '' && index(x, 'services/nikki') < 0);
return { users: users, groups: groups, cgroups: cgroups };
}
},
debug: {
call: function() {
const success = system('/etc/nikki/scripts/debug.sh > /var/log/nikki/debug.log') == 0;
return { success: success };
}
}
};
return { 'luci.nikki': methods };

173
luci-app-openclash/Makefile Normal file
View File

@ -0,0 +1,173 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclash
PKG_VERSION:=0.46.133
PKG_MAINTAINER:=vernesong <https://github.com/vernesong/OpenClash>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)/config
config PACKAGE_kmod-inet-diag
default y if PACKAGE_$(PKG_NAME)
config PACKAGE_luci-compat
default y if PACKAGE_$(PKG_NAME)
config PACKAGE_kmod-nft-tproxy
default y if PACKAGE_firewall4
config PACKAGE_kmod-ipt-nat
default y if ! PACKAGE_firewall4
config PACKAGE_iptables-mod-tproxy
default y if ! PACKAGE_firewall4
config PACKAGE_iptables-mod-extra
default y if ! PACKAGE_firewall4
config PACKAGE_dnsmasq_full_ipset
default y if ! PACKAGE_firewall4
config PACKAGE_dnsmasq_full_nftset
default y if PACKAGE_firewall4
config PACKAGE_ipset
default y if ! PACKAGE_firewall4
endef
define Package/$(PKG_NAME)
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=LuCI support for clash
PKGARCH:=all
DEPENDS:=+dnsmasq-full +bash +curl +ca-bundle +ip-full \
+ruby +ruby-yaml +kmod-tun +unzip
MAINTAINER:=vernesong
endef
define Package/$(PKG_NAME)/description
A LuCI support for clash
endef
define Build/Prepare
$(CP) $(CURDIR)/root $(PKG_BUILD_DIR)
$(CP) $(CURDIR)/luasrc $(PKG_BUILD_DIR)
$(foreach po,$(wildcard ${CURDIR}/po/zh-cn/*.po), \
po2lmo $(po) $(PKG_BUILD_DIR)/$(patsubst %.po,%.lmo,$(notdir $(po)));)
chmod 0755 $(PKG_BUILD_DIR)/root/etc/init.d/openclash
chmod -R 0755 $(PKG_BUILD_DIR)/root/usr/share/openclash/
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/config
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/rule_provider
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/backup
mkdir -p $(PKG_BUILD_DIR)/root/etc/openclash/core
mkdir -p $(PKG_BUILD_DIR)/root/usr/share/openclash/backup
cp -f "$(PKG_BUILD_DIR)/root/etc/config/openclash" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_rules.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_rules.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_rules_2.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_rules_2.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_hosts.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_hosts.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_fake_filter.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_fake_filter.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_domain_dns.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_domain_dns.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_domain_dns_policy.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_domain_dns_policy.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_fallback_filter.yaml" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_fallback_filter.yaml" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_sniffer.yaml" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_sniffer.yaml" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_localnetwork_ipv4.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_localnetwork_ipv4.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_localnetwork_ipv6.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_localnetwork_ipv6.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_chnroute_pass.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_chnroute_pass.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_chnroute6_pass.list" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_chnroute6_pass.list" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_firewall_rules.sh" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_firewall_rules.sh" >/dev/null 2>&1
cp -f "$(PKG_BUILD_DIR)/root/etc/openclash/custom/openclash_custom_overwrite.sh" "$(PKG_BUILD_DIR)/root/usr/share/openclash/backup/openclash_custom_overwrite.sh" >/dev/null 2>&1
exit 0
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/$(PKG_NAME)/conffiles
endef
define Package/$(PKG_NAME)/preinst
#!/bin/sh
if [ -f "/etc/config/openclash" ]; then
cp -f "/etc/config/openclash" "/tmp/openclash.bak" >/dev/null 2>&1
cp -rf "/etc/openclash" "/tmp/openclash" >/dev/null 2>&1
cp -rf "/usr/share/openclash/ui/yacd" "/tmp/openclash_yacd" >/dev/null 2>&1
cp -rf "/usr/share/openclash/ui/dashboard" "/tmp/openclash_dashboard" >/dev/null 2>&1
cp -rf "/www/luci-static/resources/openclash/pac" "/tmp/pac" >/dev/null 2>&1
fi
exit 0
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
exit 0
endef
define Package/$(PKG_NAME)/prerm
#!/bin/sh
uci -q set openclash.config.enable=0
uci -q commit openclash
[ -n "$(pidof clash)" ] && /etc/init.d/openclash stop 2>/dev/null
cp -f "/etc/config/openclash" "/tmp/openclash.bak" >/dev/null 2>&1
cp -rf "/etc/openclash" "/tmp/openclash" >/dev/null 2>&1
cp -rf "/usr/share/openclash/ui/yacd" "/tmp/openclash_yacd" >/dev/null 2>&1
cp -rf "/usr/share/openclash/ui/dashboard" "/tmp/openclash_dashboard" >/dev/null 2>&1
cp -rf "/www/luci-static/resources/openclash/pac" "/tmp/pac" >/dev/null 2>&1
exit 0
endef
define Package/$(PKG_NAME)/postrm
#!/bin/sh
DEFAULT_DNSMASQ_CFGID="$$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}' 2>/dev/null)"
if [ -f "/tmp/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID" ]; then
DNSMASQ_CONF_DIR="$$(awk -F '=' '/^conf-dir=/ {print $2}' "/tmp/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID" 2>/dev/null)"
else
DNSMASQ_CONF_DIR="/tmp/dnsmasq.d"
fi
DNSMASQ_CONF_DIR=$${DNSMASQ_CONF_DIR%*/}
rm -rf /etc/openclash >/dev/null 2>&1
rm -rf /etc/config/openclash >/dev/null 2>&1
rm -rf /tmp/openclash.log >/dev/null 2>&1
rm -rf /tmp/openclash_start.log >/dev/null 2>&1
rm -rf /tmp/openclash_last_version >/dev/null 2>&1
rm -rf /tmp/openclash_config.tmp >/dev/null 2>&1
rm -rf /tmp/openclash.change >/dev/null 2>&1
rm -rf /tmp/Proxy_Group >/dev/null 2>&1
rm -rf /tmp/rules_name >/dev/null 2>&1
rm -rf /tmp/rule_providers_name >/dev/null 2>&1
rm -rf /tmp/clash_last_version >/dev/null 2>&1
rm -rf /usr/share/openclash/backup >/dev/null 2>&1
rm -rf ${DNSMASQ_CONF_DIR}/dnsmasq_openclash_custom_domain.conf >/dev/null 2>&1
rm -rf ${DNSMASQ_CONF_DIR}/dnsmasq_openclash_chnroute_pass.conf >/dev/null 2>&1
rm -rf ${DNSMASQ_CONF_DIR}/dnsmasq_openclash_chnroute6_pass.conf >/dev/null 2>&1
rm -rf /tmp/dler* >/dev/null 2>&1
rm -rf /tmp/etc/openclash >/dev/null 2>&1
rm -rf /tmp/openclash_edit_file_name >/dev/null 2>&1
rm -rf /tmp/openclash_announcement >/dev/null 2>&1
rm -rf /usr/share/openclash/ui/xd >/dev/null 2>&1
rm -rf /www/luci-static/resources/openclash/pac >/dev/null 2>&1
sed -i '/OpenClash Append/,/OpenClash Append End/d' "/usr/lib/lua/luci/model/network.lua" >/dev/null 2>&1
sed -i '/.*kB maximum content size*/c\HTTP_MAX_CONTENT = 1024*100 -- 100 kB maximum content size' /usr/lib/lua/luci/http.lua >/dev/null 2>&1
sed -i '/.*kB maximum content size*/c\export let HTTP_MAX_CONTENT = 1024*100; // 100 kB maximum content size' /usr/share/ucode/luci/http.uc >/dev/null 2>&1
uci -q delete firewall.openclash
uci -q commit firewall
[ -f "/etc/config/ucitrack" ] && {
uci -q delete ucitrack.@openclash[-1]
uci -q commit ucitrack
}
rm -rf /tmp/luci-*
exit 0
endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
$(INSTALL_DATA) $(PKG_BUILD_DIR)/*.*.lmo $(1)/usr/lib/lua/luci/i18n/
$(CP) $(PKG_BUILD_DIR)/root/* $(1)/
$(CP) $(PKG_BUILD_DIR)/luasrc/* $(1)/usr/lib/lua/luci/
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require("luci.model.uci").cursor()
m = SimpleForm("openclash",translate("OpenClash"))
m.description = translate("A Clash Client For OpenWrt")
m.reset = false
m.submit = false
m:section(SimpleSection).template = "openclash/status"
if uci:get("openclash", "config", "dler_token") then
m:append(Template("openclash/dlercloud"))
end
m:append(Template("openclash/myip"))
m:append(Template("openclash/developer"))
m:append(Template("openclash/select_git_cdn"))
m:append(Template("openclash/config_edit"))
m:append(Template("openclash/config_upload"))
return m

View File

@ -0,0 +1,573 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local json = require "luci.jsonc"
local datatype = require "luci.cbi.datatypes"
font_green = [[<b style=color:green>]]
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
local op_mode = string.sub(luci.sys.exec('uci get openclash.config.operation_mode 2>/dev/null'),0,-2)
if not op_mode then op_mode = "redir-host" end
local lan_ip = fs.lanip()
m = Map("openclash", translate("Overwrite Settings"))
m.pageaction = false
m.description = translate("Note: To restore the default configuration, try accessing:").." <a href='javascript:void(0)' onclick='javascript:restore_config(this)'>http://"..lan_ip.."/cgi-bin/luci/admin/services/openclash/restore</a>"..
"<br/>"..font_green..translate("For More Useful Meta Core Functions Go Wiki")..": "..font_off.."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://wiki.metacubex.one/\")'>"..translate("https://wiki.metacubex.one/").."</a>"
s = m:section(TypedSection, "openclash")
s.anonymous = true
s:tab("settings", translate("General Settings"))
s:tab("dns", "DNS "..translate("Settings"))
s:tab("meta", translate("Meta Settings"))
s:tab("rules", translate("Rules Setting"))
s:tab("developer", translate("Developer Settings"))
----- General Settings
o = s:taboption("settings", ListValue, "interface_name", translate("Bind Network Interface"))
local de_int = SYS.exec("ip route |grep 'default' |awk '{print $5}' 2>/dev/null") or SYS.exec("/usr/share/openclash/openclash_get_network.lua 'dhcp'")
o.description = translate("Default Interface Name:").." "..font_green..bold_on..de_int..bold_off..font_off..translate(",Try Enable If Network Loopback")
local interfaces = SYS.exec("ls -l /sys/class/net/ 2>/dev/null |awk '{print $9}' 2>/dev/null")
for interface in string.gmatch(interfaces, "%S+") do
o:value(interface)
end
o:value("0", translate("Disable"))
o.default = "0"
o = s:taboption("settings", Value, "tolerance", translate("URL-Test Group Tolerance").."(ms)")
o.description = translate("Switch To The New Proxy When The Delay Difference Between Old and The Fastest Currently is Greater Than This Value")
o:value("0", translate("Disable"))
o:value("100")
o:value("150")
o.datatype = "uinteger"
o.default = "0"
o = s:taboption("settings", Value, "urltest_interval_mod", translate("URL-Test Interval Modify").."(s)")
o.description = translate("Modify The URL-Test Interval In The Config")
o:value("0", translate("Disable"))
o:value("180")
o:value("300")
o.datatype = "uinteger"
o.default = "0"
o = s:taboption("settings", Value, "urltest_address_mod", translate("URL-Test Address Modify"))
o.description = translate("Modify The URL-Test Address In The Config")
o:value("0", translate("Disable"))
o:value("http://www.gstatic.com/generate_204")
o:value("http://cp.cloudflare.com/generate_204")
o:value("https://cp.cloudflare.com/generate_204")
o:value("http://captive.apple.com/generate_204")
o.default = "0"
o = s:taboption("settings", Value, "github_address_mod", translate("Github Address Modify"))
o.description = translate("Modify The Github Address In The Config And OpenClash With Proxy(CDN) To Prevent File Download Faild. Format Reference:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://ghp.ci/\")'>https://ghp.ci/</a>"
o:value("0", translate("Disable"))
o:value("https://fastly.jsdelivr.net/")
o:value("https://testingcf.jsdelivr.net/")
o:value("https://cdn.jsdelivr.net/")
o.default = "0"
o = s:taboption("settings", ListValue, "log_level", translate("Log Level"))
o.description = translate("Select Core's Log Level")
o:value("0", translate("Disable"))
o:value("info", translate("Info Mode"))
o:value("warning", translate("Warning Mode"))
o:value("error", translate("Error Mode"))
o:value("debug", translate("Debug Mode"))
o:value("silent", translate("Silent Mode"))
o.default = "0"
o = s:taboption("settings", Value, "dns_port")
o.title = translate("DNS Port")
o.default = "7874"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "proxy_port")
o.title = translate("Redir Port")
o.default = "7892"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "tproxy_port")
o.title = translate("TProxy Port")
o.default = "7895"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "http_port")
o.title = translate("HTTP(S) Port")
o.default = "7890"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "socks_port")
o.title = translate("SOCKS5 Port")
o.default = "7891"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
o = s:taboption("settings", Value, "mixed_port")
o.title = translate("Mixed Port")
o.default = "7893"
o.datatype = "port"
o.rmempty = false
o.description = translate("Please Make Sure Ports Available")
---- DNS Settings
o = s:taboption("dns", Flag, "enable_custom_dns", font_red..bold_on..translate("Custom DNS Setting")..bold_off..font_off)
o.description = font_red..bold_on..translate("Set OpenClash Upstream DNS Resolve Server")..bold_off..font_off
o.default = 0
o = s:taboption("dns", Flag, "enable_respect_rules", font_red..bold_on..translate("Respect Rules").."(respect-rules)"..bold_off..font_off)
o.description = font_red..bold_on..translate("Whether or not The Connection to the DNS Server Follow the Rules in Config")..bold_off..font_off
o.default = 0
o = s:taboption("dns", Flag, "append_wan_dns", translate("Append Upstream DNS"))
o.description = translate("Append The Upstream Assigned DNS And Gateway IP To The Nameserver")
o.default = 1
o = s:taboption("dns", Flag, "append_default_dns", translate("Append Default DNS"))
o.description = translate("Automatically Append Compliant DNS to default-nameserver")
o.default = 1
if op_mode == "fake-ip" then
o = s:taboption("dns", Value, "fakeip_range", translate("Fake-IP Range (IPv4 Cidr)"))
o.description = translate("Set Fake-IP Range (IPv4 Cidr)")
o:value("0", translate("Disable"))
o:value("198.18.0.1/16")
o.default = "0"
o.placeholder = "198.18.0.1/16"
function o.validate(self, value)
if value == "0" then
return "0"
end
if datatype.cidr4(value) then
return value
end
return "198.18.0.1/16"
end
o = s:taboption("dns", Flag, "store_fakeip", translate("Persistence Fake-IP"))
o.description = font_red..bold_on..translate("Cache Fake-IP DNS Resolution Records To File, Improve The Response Speed After Startup")..bold_off..font_off
o.default = 1
end
o = s:taboption("dns", Flag, "custom_fallback_filter", translate("Fallback-Filter"))
o.description = translate("Take Effect If Fallback DNS Setted, Prevent DNS Pollution")
o.default = 0
custom_fallback_filter = s:taboption("dns", Value, "custom_fallback_fil")
custom_fallback_filter.template = "cbi/tvalue"
custom_fallback_filter.rows = 20
custom_fallback_filter.wrap = "off"
custom_fallback_filter:depends("custom_fallback_filter", "1")
function custom_fallback_filter.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml") or ""
end
function custom_fallback_filter.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml", value)
end
end
end
if op_mode == "fake-ip" then
o = s:taboption("dns", Flag, "custom_fakeip_filter", translate("Fake-IP-Filter"))
o.default = 0
o = s:taboption("dns", ListValue, "custom_fakeip_filter_mode", translate("Fake-IP-Filter-Mode"))
o.description = translate("Fake-IP is not returned if the matching succeeds when blacklist mode or Fake-IP is returned if the matching succeeds when whitelist mode")
o.default = "blacklist"
o:value("blacklist", translate("Blacklist Mode"))
o:value("whitelist", translate("Whitelist Mode"))
o:depends("custom_fakeip_filter", "1")
custom_fake_black = s:taboption("dns", Value, "custom_fake_filter")
custom_fake_black.template = "cbi/tvalue"
custom_fake_black.description = translate("Domain Names In The List Do Not Return Fake-IP, One rule per line")
custom_fake_black.rows = 20
custom_fake_black.wrap = "off"
custom_fake_black:depends("custom_fakeip_filter", "1")
function custom_fake_black.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_fake_filter.list") or ""
end
function custom_fake_black.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_fake_filter.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_fake_filter.list", value)
end
end
end
end
o = s:taboption("dns", Flag, "custom_name_policy", translate("Nameserver-Policy"))
o.default = 0
custom_domain_dns_policy = s:taboption("dns", Value, "custom_domain_dns_core")
custom_domain_dns_policy.template = "cbi/tvalue"
custom_domain_dns_policy.description = translate("Domain Names In The List Use The Custom DNS Server, But Still Return Fake-IP Results, One rule per line")
custom_domain_dns_policy.rows = 20
custom_domain_dns_policy.wrap = "off"
custom_domain_dns_policy:depends("custom_name_policy", "1")
function custom_domain_dns_policy.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list") or ""
end
function custom_domain_dns_policy.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list", value)
end
end
end
o = s:taboption("dns", Flag, "custom_host", translate("Hosts"))
o.default = 0
custom_hosts = s:taboption("dns", Value, "custom_hosts")
custom_hosts.template = "cbi/tvalue"
custom_hosts.description = translate("Custom Hosts Here, You May Need to Turn off The Rebinding Protection Option of Dnsmasq When Hosts Has Set a Reserved Address, For More Go:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://lancellc.gitbook.io/clash/clash-config-file/dns/host\")'>https://lancellc.gitbook.io/clash/clash-config-file/dns/host</a>"
custom_hosts.rows = 20
custom_hosts.wrap = "off"
custom_hosts:depends("custom_host", "1")
function custom_hosts.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_hosts.list") or ""
end
function custom_hosts.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_hosts.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_hosts.list", value)
end
end
end
-- Meta
o = s:taboption("meta", Flag, "enable_tcp_concurrent", font_red..bold_on..translate("Enable Tcp Concurrent")..bold_off..font_off)
o.description = font_red..bold_on..translate("TCP Concurrent Request IPs, Choose The Lowest Latency One To Connection")..bold_off..font_off
o.default = "0"
o = s:taboption("meta", Flag, "enable_unified_delay", font_red..bold_on..translate("Enable Unified Delay")..bold_off..font_off)
o.description = font_red..bold_on..translate("Change The Delay Calculation Method To Remove Extra Delays Such as Handshaking")..bold_off..font_off
o.default = "0"
o = s:taboption("meta", ListValue, "find_process_mode", translate("Enable Process Rule"))
o.description = translate("Whether to Enable Process Rules, Only Works on Routerself, If You Are Not Sure, Please Choose off Which Useful in Router Environment, Depend on kmod-inet-diag")
o:value("0", translate("Disable"))
o:value("off", translate("OFF "))
o:value("always", translate("Always "))
o:value("strict", translate("strict "))
o.default = "0"
o = s:taboption("meta", ListValue, "global_client_fingerprint", translate("Client Fingerprint"))
o.description = translate("Change The Client Fingerprint, Only Support TLS Transport in TCP/GRPC/WS/HTTP For Vless/Vmess and Trojan")
o:value("0", translate("Disable"))
o:value("none", translate("None "))
o:value("random", translate("Random"))
o:value("chrome", translate("Chrome"))
o:value("firefox", translate("Firefox"))
o:value("safari", translate("Safari"))
o:value("ios", translate("IOS"))
o:value("android", translate("Android"))
o:value("edge", translate("Edge"))
o:value("360", translate("360"))
o:value("qq", translate("QQ"))
o.default = "0"
o = s:taboption("meta", ListValue, "geodata_loader", translate("Geodata Loader Mode"))
o:value("0", translate("Disable"))
o:value("memconservative", translate("Memconservative"))
o:value("standard", translate("Standard"))
o.default = "0"
o = s:taboption("meta", Flag, "enable_geoip_dat", translate("Enable GeoIP Dat"))
o.description = translate("Replace GEOIP MMDB With GEOIP Dat, Large Size File")..", "..font_red..bold_on..translate("Need Download First")..bold_off..font_off
o.default = 0
o = s:taboption("meta", Flag, "enable_meta_sniffer", font_red..bold_on..translate("Enable Sniffer")..bold_off..font_off)
o.description = font_red..bold_on..translate("Sniffer Will Prevent Domain Name Proxy and DNS Hijack Failure")..bold_off..font_off
o.default = 1
o = s:taboption("meta", Flag, "enable_meta_sniffer_pure_ip", translate("Forced Sniff Pure IP"))
o.description = translate("Forced Sniff Pure IP Connections")
o.default = 1
o:depends("enable_meta_sniffer", "1")
o = s:taboption("meta", Flag, "enable_meta_sniffer_custom", translate("Custom Sniffer Settings"))
o.description = translate("Custom The Force and Skip Sniffing Doamin Lists")
o.default = 0
o:depends("enable_meta_sniffer", "1")
sniffer_custom = s:taboption("meta", Value, "sniffer_custom")
sniffer_custom:depends("enable_meta_sniffer_custom", "1")
sniffer_custom.template = "cbi/tvalue"
sniffer_custom.rows = 20
sniffer_custom.wrap = "off"
function sniffer_custom.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_sniffer.yaml") or ""
end
function sniffer_custom.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_sniffer.yaml")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_sniffer.yaml", value)
end
end
end
---- Rules Settings
o = s:taboption("rules", Flag, "rule_source", translate("Enable Other Rules"))
o.description = translate("Use Other Rules")
o.default = 0
o = s:taboption("rules", Flag, "enable_rule_proxy", translate("Rule Match Proxy Mode"))
o.description = translate("Append Some Rules to Config, Allow Only Traffic Proxies that Match the Rule, Prevent BT/P2P Passing")
o.default = 0
o = s:taboption("rules", Flag, "enable_custom_clash_rules", font_red..bold_on..translate("Custom Clash Rules")..bold_off..font_off)
o.description = translate("Use Custom Rules")
o.default = 0
custom_rules = s:taboption("rules", Value, "custom_rules")
custom_rules:depends("enable_custom_clash_rules", 1)
custom_rules.template = "cbi/tvalue"
custom_rules.description = font_green..bold_on..translate("(Priority)")..bold_off..font_off.." "..translate("Custom Rules Here, For More Go:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://wiki.metacubex.one/config/rules/\")'>https://wiki.metacubex.one/config/rules/</a>".." ,"..translate("IP To CIDR:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"http://ip2cidr.com\")'>http://ip2cidr.com</a>"
custom_rules.rows = 20
custom_rules.wrap = "off"
function custom_rules.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_rules.list") or ""
end
function custom_rules.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_rules.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_rules.list", value)
end
end
end
custom_rules_2 = s:taboption("rules", Value, "custom_rules_2")
custom_rules_2:depends("enable_custom_clash_rules", 1)
custom_rules_2.template = "cbi/tvalue"
custom_rules_2.description = font_green..bold_on..translate("(Extended)")..bold_off..font_off.." "..translate("Custom Rules Here, For More Go:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://wiki.metacubex.one/config/rules/\")'>https://wiki.metacubex.one/config/rules/</a>".." ,"..translate("IP To CIDR:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"http://ip2cidr.com\")'>http://ip2cidr.com</a>"
custom_rules_2.rows = 20
custom_rules_2.wrap = "off"
function custom_rules_2.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_rules_2.list") or ""
end
function custom_rules_2.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_rules_2.list")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_rules_2.list", value)
end
end
end
---- developer
o = s:taboption("developer", Value, "ymchange_custom")
o.template = "cbi/tvalue"
o.description = translate("Custom Config Overwrite Scripts Which Will Run After Plugin Own Completely, Please Be Careful, The Wrong Changes May Lead to Exceptions")
o.rows = 30
o.wrap = "off"
function o.cfgvalue(self, section)
return NXFS.readfile("/etc/openclash/custom/openclash_custom_overwrite.sh") or ""
end
function o.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile("/etc/openclash/custom/openclash_custom_overwrite.sh")
if value ~= old_value then
NXFS.writefile("/etc/openclash/custom/openclash_custom_overwrite.sh", value)
end
end
end
-- [[ Edit Custom DNS ]] --
ds = m:section(TypedSection, "dns_servers", translate("Add Custom DNS Servers")..translate("(Take Effect After Choose Above)"))
ds.anonymous = true
ds.addremove = true
ds.sortable = true
ds.template = "openclash/tblsection_dns"
ds.extedit = luci.dispatcher.build_url("admin/services/openclash/custom-dns-edit/%s")
function ds.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(ds.extedit % sid)
return
end
end
---- enable flag
o = ds:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- group
o = ds:option(ListValue, "group", translate("DNS Server Group"))
o:value("nameserver", translate("NameServer "))
o:value("fallback", translate("FallBack "))
o:value("default", translate("Default-NameServer"))
o.default = "nameserver"
o.rempty = false
---- IP address
o = ds:option(Value, "ip", translate("DNS Server Address"))
o.placeholder = translate("Not Null")
o.datatype = "or(host, string)"
o.rmempty = true
---- port
o = ds:option(Value, "port", translate("DNS Server Port"))
o.datatype = "port"
o.rempty = true
---- type
o = ds:option(ListValue, "type", translate("DNS Server Type"))
o:value("udp", translate("UDP"))
o:value("tcp", translate("TCP"))
o:value("tls", translate("TLS"))
o:value("https", translate("HTTPS"))
o:value("quic", translate("QUIC"))
o.default = "udp"
o.rempty = false
---- Disable-IPv6
o = ds:option(Flag, "disable_ipv6", translate("Disable-IPv6"))
o.rmempty = false
o.default = o.disbled
-- [[ Other Rules Manage ]]--
ss = m:section(TypedSection, "other_rules", translate("Other Rules Edit")..translate("(Take Effect After Choose Above)"))
ss.anonymous = true
ss.addremove = true
ss.sortable = true
ss.template = "cbi/tblsection"
ss.extedit = luci.dispatcher.build_url("admin/services/openclash/other-rules-edit/%s")
function ss.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(ss.extedit % sid)
return
end
end
o = ss:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = ss:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = ss:option(DummyValue, "rule_name", translate("Other Rules Name"))
function o.cfgvalue(...)
if Value.cfgvalue(...) == "lhie1" then
return translate("lhie1 Rules")
else
return translate("None")
end
end
o = ss:option(DummyValue, "Note", translate("Note"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
-- [[ Edit Authentication ]] --
s = m:section(TypedSection, "authentication", translate("Set Authentication of SOCKS5/HTTP(S)"))
s.anonymous = true
s.addremove = true
s.sortable = false
s.template = "cbi/tblsection"
s.rmempty = false
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- username
o = s:option(Value, "username", translate("Username"))
o.placeholder = translate("Not Null")
o.rempty = true
---- password
o = s:option(Value, "password", translate("Password"))
o.placeholder = translate("Not Null")
o.rmempty = true
local t = {
{Commit, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/config_editor"))
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,226 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local json = require "luci.jsonc"
local sid = arg[1]
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
m = Map(openclash, translate("Config Subscribe Edit"))
m.pageaction = false
m.description=translate("Convert Subscribe function of Online is Supported By subconverter Written By tindy X") ..
"<br/>"..
"<br/>"..translate("API By tindy X & lhie1")..
"<br/>"..
"<br/>"..translate("Subconverter external configuration (subscription conversion template) Description: https://github.com/tindy2013/subconverter#external-configuration-file")..
"<br/>"..
"<br/>"..translate("If you need to customize the external configuration file (subscription conversion template), please write it according to the instructions, upload it to the accessible location of the external network, and fill in the address correctly when using it")..
"<br/>"..
"<br/>"..translate("If you have a recommended external configuration file (subscription conversion template), you can modify by following The file format of /usr/share/openclash/res/sub_ini.list and pr")
m.redirect = luci.dispatcher.build_url("admin/services/openclash/config-subscribe")
if m.uci:get(openclash, sid) ~= "config_subscribe" then
luci.http.redirect(m.redirect)
return
end
-- [[ Config Subscribe Setting ]]--
s = m:section(NamedSection, sid, "config_subscribe")
s.anonymous = true
s.addremove = false
---- name
o = s:option(Value, "name", translate("Config Alias"))
o.description = font_red..bold_on..translate("Name For Distinguishing")..bold_off..font_off
o.placeholder = translate("config")
o.rmempty = true
---- address
o = s:option(Value, "address", translate("Subscribe Address"))
o.template = "cbi/tvalue"
o.rows = 10
o.wrap = "off"
o.description = font_red..bold_on..translate("SS/SSR/Vmess or Other Link And Subscription Address is Supported When Online Subscription Conversion is Enabled, Multiple Links Should be One Per Line or Separated By |")..bold_off..font_off
o.placeholder = translate("Not Null")
o.rmempty = false
function o.validate(self, value)
if value then
value = value:gsub("\r\n?", "\n")
value = value:gsub("%c*$", "")
end
return value
end
local sub_path = "/tmp/dler_sub"
local info, token, get_sub, sub_info
local token = uci:get("openclash", "config", "dler_token")
if token then
get_sub = string.format("curl -sL -H 'Content-Type: application/json' --connect-timeout 2 -d '{\"access_token\":\"%s\"}' -X POST https://dler.cloud/api/v1/managed/clash -o %s", token, sub_path)
if not nixio.fs.access(sub_path) then
luci.sys.exec(get_sub)
else
if fs.readfile(sub_path) == "" or not fs.readfile(sub_path) then
luci.sys.exec(get_sub)
end
end
sub_info = fs.readfile(sub_path)
if sub_info then
sub_info = json.parse(sub_info)
end
if sub_info and sub_info.ret == 200 then
o:value(sub_info.smart)
o:value(sub_info.ss)
o:value(sub_info.vmess)
o:value(sub_info.trojan)
else
fs.unlink(sub_path)
end
end
---- UA
o = s:option(Value, "sub_ua", "User-Agent")
o.description = font_red..bold_on..translate("Used for Downloading Subscriptions, Defaults to Clash")..bold_off..font_off
o:value("clash.meta")
o:value("clash-verge/v1.5.1")
o:value("Clash")
o.default = "clash.meta"
o.rmempty = true
---- subconverter
o = s:option(Flag, "sub_convert", translate("Subscribe Convert Online"))
o.description = translate("Convert Subscribe Online With Template")
o.default = 0
---- Convert Address
o = s:option(Value, "convert_address", translate("Convert Address"))
o.rmempty = true
o.description = font_red..bold_on..translate("Note: There is A Risk of Privacy Leakage in Online Convert")..bold_off..font_off
o:depends("sub_convert", "1")
o:value("https://api.dler.io/sub", translate("api.dler.io")..translate("(Default)"))
o:value("https://api.wcc.best/sub", translate("api.wcc.best"))
o.default = "https://api.dler.io/sub"
o.placeholder = "https://api.dler.io/sub"
---- Template
o = s:option(ListValue, "template", translate("Template Name"))
o.rmempty = true
o:depends("sub_convert", "1")
file = io.open("/usr/share/openclash/res/sub_ini.list", "r");
for l in file:lines() do
if l ~= "" and l ~= nil then
o:value(string.sub(luci.sys.exec(string.format("echo '%s' |awk -F ',' '{print $1}' 2>/dev/null",l)),1,-2))
end
end
file:close()
o:value("0", translate("Custom Template"))
---- Custom Template
o = s:option(Value, "custom_template_url", translate("Custom Template URL"))
o.rmempty = true
o.placeholder = translate("Not Null")
o.datatype = "or(host, string)"
o:depends("template", "0")
---- emoji
o = s:option(ListValue, "emoji", translate("Emoji"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- udp
o = s:option(ListValue, "udp", translate("UDP Enable"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- skip-cert-verify
o = s:option(ListValue, "skip_cert_verify", translate("skip-cert-verify"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- sort
o = s:option(ListValue, "sort", translate("Sort"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- node type
o = s:option(ListValue, "node_type", translate("Append Node Type"))
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- rule provider
o = s:option(ListValue, "rule_provider", translate("Use Rule Provider"))
o.description = font_red..bold_on..translate("Note: Please Make Sure Backend Service Supports This Feature")..bold_off..font_off
o.rmempty = false
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o:depends("sub_convert", "1")
---- custom params
o = s:option(DynamicList, "custom_params", translate("Custom Params"))
o.description = font_red..bold_on..translate("eg: \"rename=match@replace\" , \"rename=\\s+([2-9])[xX]@ (HIGH:$1)\"")..bold_off..font_off
o.rmempty = false
o:depends("sub_convert", "1")
---- key
o = s:option(DynamicList, "keyword", font_red..bold_on..translate("Keyword Match")..bold_off..font_off)
o.description = font_red..bold_on..translate("eg: hk or tw&bgp")..bold_off..font_off
o.rmempty = true
---- exkey
o = s:option(DynamicList, "ex_keyword", font_red..bold_on..translate("Exclude Keyword Match")..bold_off..font_off)
o.description = font_red..bold_on..translate("eg: hk or tw&bgp")..bold_off..font_off
o.rmempty = true
---- de_exkey
o = s:option(MultiValue, "de_ex_keyword", font_red..bold_on..translate("Exclude Keyword Match Default")..bold_off..font_off)
o.rmempty = true
o:depends("sub_convert", 0)
o:value("过期时间")
o:value("剩余流量")
o:value("TG群")
o:value("官网")
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,154 @@
local m, s, o
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
m = Map("openclash", translate("Config Subscribe"))
m.pageaction = false
s = m:section(TypedSection, "openclash")
s.anonymous = true
---- update Settings
o = s:option(Flag, "auto_update", translate("Auto Update"))
o.description = translate("Auto Update Server subscription")
o.default = 0
o = s:option(ListValue, "config_auto_update_mode", translate("Update Mode"))
o:depends("auto_update", "1")
o:value("0", translate("Appointment Mode"))
o:value("1", translate("Loop Mode"))
o.default = "0"
o.rmempty = true
o = s:option(ListValue, "config_update_week_time", translate("Update Time (Every Week)"))
o:depends("config_auto_update_mode", "0")
o:value("*", translate("Every Day"))
o:value("1", translate("Every Monday"))
o:value("2", translate("Every Tuesday"))
o:value("3", translate("Every Wednesday"))
o:value("4", translate("Every Thursday"))
o:value("5", translate("Every Friday"))
o:value("6", translate("Every Saturday"))
o:value("0", translate("Every Sunday"))
o.default = "1"
o.rmempty = true
o = s:option(ListValue, "auto_update_time", translate("Update time (every day)"))
o:depends("config_auto_update_mode", "0")
for t = 0,23 do
o:value(t, t..":00")
end
o.default = "0"
o.rmempty = true
o = s:option(Value, "config_update_interval", translate("Update Interval(min)"))
o.default = "60"
o.datatype = "integer"
o:depends("config_auto_update_mode", "1")
o.rmempty = true
-- [[ Edit Server ]] --
s = m:section(TypedSection, "config_subscribe", translate("Config Subscribe Edit"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/config-subscribe-edit/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
return
end
end
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- name
o = s:option(DummyValue, "name", translate("Config Alias"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("config")
end
---- address
o = s:option(DummyValue, "address", translate("Subscribe Address"))
function o.cfgvalue(...)
if Value.cfgvalue(...) then
if string.len(Value.cfgvalue(...)) <= 50 then
return Value.cfgvalue(...)
else
return string.sub(Value.cfgvalue(...), 1, 50) .. " ..."
end
end
return translate("None")
end
---- template
o = s:option(DummyValue, "template", translate("Template Name"))
function o.cfgvalue(...)
if Value.cfgvalue(...) ~= "0" then
return Value.cfgvalue(...) or translate("None")
else
return translate("Custom Template")
end
end
---- update
o = s:option(DummyValue, "name", translate("Update"))
o.template = "openclash/update_config"
local t = {
{Commit, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
m.uci:commit("openclash")
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Update Config")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
uci:foreach("openclash", "config_subscribe",
function(s)
if s.name ~= "" and s.name ~= nil and s.enabled == "1" then
local back_cfg_path_yaml="/etc/openclash/backup/" .. s.name .. ".yaml"
local back_cfg_path_yml="/etc/openclash/backup/" .. s.name .. ".yml"
fs.unlink(back_cfg_path_yaml)
fs.unlink(back_cfg_path_yml)
end
end)
SYS.call("/usr/share/openclash/openclash.sh >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,454 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require("luci.model.uci").cursor()
local CHIF = "0"
font_green = [[<b style=color:green>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
align_mid = [[<p align="center">]]
align_mid_off = [[</p>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
function default_config_set(f)
local cf = uci:get("openclash", "config", "config_path")
if cf == "/etc/openclash/config/"..f or not cf or cf == "" or not fs.isfile(cf) then
if CHIF == "1" and cf == "/etc/openclash/config/"..f then
return
end
local fis = fs.glob("/etc/openclash/config/*")[1]
if fis ~= nil then
fcf = fs.basename(fis)
if fcf then
uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..fcf)
uci:commit("openclash")
end
else
uci:set("openclash", "config", "config_path", "/etc/openclash/config/config.yaml")
uci:commit("openclash")
end
end
end
ful = SimpleForm("upload", translate("Config Manage"), nil)
ful.reset = false
ful.submit = false
sul =ful:section(SimpleSection, "")
o = sul:option(FileUpload, "")
o.template = "openclash/upload"
um = sul:option(DummyValue, "", nil)
um.template = "openclash/dvalue"
local dir, fd, clash
clash = "/etc/openclash/clash"
dir = "/etc/openclash/config/"
bakck_dir="/etc/openclash/backup"
proxy_pro_dir="/etc/openclash/proxy_provider/"
rule_pro_dir="/etc/openclash/rule_provider/"
core_dir="/etc/openclash/core/core/"
backup_dir="/tmp/"
create_bakck_dir=fs.mkdir(bakck_dir)
create_proxy_pro_dir=fs.mkdir(proxy_pro_dir)
create_rule_pro_dir=fs.mkdir(rule_pro_dir)
HTTP.setfilehandler(
function(meta, chunk, eof)
local fp = HTTP.formvalue("file_type")
if not fd then
if not meta then return end
if fp == "config" then
if meta and chunk then fd = nixio.open(dir .. meta.file, "w") end
elseif fp == "proxy-provider" then
if meta and chunk then fd = nixio.open(proxy_pro_dir .. meta.file, "w") end
elseif fp == "rule-provider" then
if meta and chunk then fd = nixio.open(rule_pro_dir .. meta.file, "w") end
elseif fp == "clash_meta" then
create_core_dir=fs.mkdir(core_dir)
if meta and chunk then fd = nixio.open(core_dir .. meta.file, "w") end
elseif fp == "backup-file" then
if meta and chunk then fd = nixio.open(backup_dir .. meta.file, "w") end
end
if not fd then
um.value = translate("upload file error.")
return
end
end
if chunk and fd then
fd:write(chunk)
end
if eof and fd then
fd:close()
fd = nil
if fp == "config" then
CHIF = "1"
if IsYamlFile(meta.file) then
local yamlbackup="/etc/openclash/backup/" .. meta.file
local c=fs.copy(dir .. meta.file,yamlbackup)
default_config_set(meta.file)
end
if IsYmlFile(meta.file) then
local ymlname=string.lower(string.sub(meta.file,0,-5))
local ymlbackup="/etc/openclash/backup/".. ymlname .. ".yaml"
local c=fs.rename(dir .. meta.file,"/etc/openclash/config/".. ymlname .. ".yaml")
local c=fs.copy("/etc/openclash/config/".. ymlname .. ".yaml",ymlbackup)
local yamlname=ymlname .. ".yaml"
default_config_set(yamlname)
end
um.value = translate("File saved to") .. ' "/etc/openclash/config/"'
elseif fp == "proxy-provider" then
um.value = translate("File saved to") .. ' "/etc/openclash/proxy_provider/"'
elseif fp == "rule-provider" then
um.value = translate("File saved to") .. ' "/etc/openclash/rule_provider/"'
elseif fp == "clash_meta" then
local archive_path = core_dir .. meta.file
if string.lower(string.sub(meta.file, -7, -1)) == ".tar.gz" then
-- tar.gz
os.execute(string.format("tar -C '/etc/openclash/core/core' -xzf '%s' >/dev/null 2>&1", archive_path))
os.execute(string.format("rm -f '%s' >/dev/null 2>&1", archive_path))
local first_file_cmd = "find /etc/openclash/core/core -type f ! -name '*.tar.gz' ! -name '*.tar' ! -name '*.gz' 2>/dev/null | head -1"
local first_file = io.popen(first_file_cmd):read("*line")
if first_file and first_file ~= "" then
os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", first_file, fp))
end
elseif string.lower(string.sub(meta.file, -4, -1)) == ".tar" then
-- tar
os.execute(string.format("tar -C '/etc/openclash/core/core' -xf '%s' >/dev/null 2>&1", archive_path))
os.execute(string.format("rm -f '%s' >/dev/null 2>&1", archive_path))
local first_file_cmd = "find /etc/openclash/core/core -type f ! -name '*.tar' ! -name '*.gz' 2>/dev/null | head -1"
local first_file = io.popen(first_file_cmd):read("*line")
if first_file and first_file ~= "" then
os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", first_file, fp))
end
elseif string.lower(string.sub(meta.file, -3, -1)) == ".gz" then
-- gz
os.execute(string.format("gzip -fd '%s' >/dev/null 2>&1", archive_path))
os.execute(string.format("rm -f '%s' >/dev/null 2>&1", archive_path))
local first_file_cmd = "find /etc/openclash/core/core -type f ! -name '*.gz' 2>/dev/null | head -1"
local first_file = io.popen(first_file_cmd):read("*line")
if first_file and first_file ~= "" then
os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", first_file, fp))
end
else
os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", (core_dir .. meta.file), fp))
end
os.execute(string.format("chmod 4755 '/etc/openclash/core/%s' >/dev/null 2>&1", fp))
os.execute(string.format("rm -rf %s >/dev/null 2>&1", core_dir))
um.value = translate("File saved to") .. ' "/etc/openclash/core/"'
elseif fp == "backup-file" then
os.execute("tar -C '/etc/openclash/' -xzf %s >/dev/null 2>&1" % (backup_dir .. meta.file))
os.execute("mv /etc/openclash/openclash /etc/config/openclash >/dev/null 2>&1")
fs.unlink(backup_dir .. meta.file)
um.value = translate("Backup File Restore Successful!")
end
fs.unlink("/tmp/Proxy_Group")
end
end
)
if HTTP.formvalue("upload") then
if not um.value then
um.value = translate("No Specify Upload File")
end
end
local e,a={}
for t,o in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(o)
if a then
e[t]={}
e[t].name=fs.basename(o)
BACKUP_FILE="/etc/openclash/backup/".. e[t].name
if fs.mtime(BACKUP_FILE) then
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",fs.mtime(BACKUP_FILE))
else
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",a.mtime)
end
if uci:get("openclash", "config", "config_path") and string.sub(uci:get("openclash", "config", "config_path"), 23, -1) == e[t].name then
e[t].state=translate("Enabled")
else
e[t].state=translate("Disabled")
end
e[t].size=fs.filesize(a.size)
e[t].remove=0
end
end
form=SimpleForm("config_file_list",translate("Config File List"))
form.reset=false
form.submit=false
tb=form:section(Table,e)
st=tb:option(DummyValue,"state",translate("State"))
nm=tb:option(DummyValue,"name",translate("Config Alias"))
sb=tb:option(DummyValue,"name",translate("Subscription Info"))
mt=tb:option(DummyValue,"mtime",translate("Update Time"))
sz=tb:option(DummyValue,"size",translate("Size"))
st.template="openclash/cfg_check"
sb.template="openclash/sub_info_show"
btnis=tb:option(Button,"switch",translate("SwiTch"))
btnis.template="openclash/other_button"
btnis.render=function(o,t,a)
if not e[t] then return false end
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
a.display=""
else
a.display="none"
end
o.inputstyle="apply"
Button.render(o,t,a)
end
btnis.write=function(a,t)
fs.unlink("/tmp/Proxy_Group")
uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..e[t].name)
uci:commit("openclash")
HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config"))
end
btnrn=tb:option(DummyValue,"/etc/openclash/config/",translate("Rename"))
btnrn.template="openclash/input_rename"
btnrn.rawhtml = true
btnrn.render=function(c,t,a)
if not e[t] then return end
c.value = e[t].name
DummyValue.render(c,t,a)
end
local actions = tb:option(ListValue, "actions", translate("Other"))
actions.render = function(self, t, a)
if not e[t] then return end
self.keylist = {}
self.vallist = {}
-- Edit
table.insert(self.keylist, "edit")
table.insert(self.vallist, translate("Edit"))
-- Copy
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
table.insert(self.keylist, "copy")
table.insert(self.vallist, translate("Copy Config"))
end
-- Download
table.insert(self.keylist, "download")
table.insert(self.vallist, translate("Download Config"))
-- Download Running
if NXFS.access("/etc/openclash/"..e[t].name) then
table.insert(self.keylist, "download_run")
table.insert(self.vallist, translate("Download Running Config"))
end
-- Remove
table.insert(self.keylist, "remove")
table.insert(self.vallist, translate("Remove"))
ListValue.render(self, t, a)
end
local btnapply = tb:option(Button, "apply", translate("Apply"))
btnapply.inputstyle = "apply"
btnapply.write = function(self, t)
if not e[t] then return end
local action = self.map:formvalue("cbid." .. self.map.config .. "." .. t .. ".actions")
if action == "edit" then
local file_path = "etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "config", "%s") % file_path)
elseif action == "copy" then
local num = 1
while true do
num = num + 1
if not fs.isfile("/etc/openclash/config/"..fs.filename(e[t].name).."("..num..")"..".yaml") then
fs.copy("/etc/openclash/config/"..e[t].name, "/etc/openclash/config/"..fs.filename(e[t].name).."("..num..")"..".yaml")
break
end
end
HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config"))
elseif action == "download" then
local sPath, sFile, fd, block
sPath = "/etc/openclash/config/"..e[t].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then return end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block == 0) then break end
HTTP.write(block)
end
fd:close()
HTTP.close()
elseif action == "download_run" then
local sPath, sFile, fd, block
sPath = "/etc/openclash/"..e[t].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then return end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block == 0) then break end
HTTP.write(block)
end
fd:close()
HTTP.close()
elseif action == "remove" then
fs.unlink("/tmp/Proxy_Group")
fs.unlink("/etc/openclash/backup/"..fs.basename(e[t].name))
fs.unlink("/etc/openclash/history/"..fs.filename(e[t].name)..".db")
fs.unlink("/etc/openclash/"..fs.basename(e[t].name))
local a=fs.unlink("/etc/openclash/config/"..fs.basename(e[t].name))
default_config_set(fs.basename(e[t].name))
if a then table.remove(e,t) end
HTTP.redirect(DISP.build_url("admin", "services", "openclash","config"))
end
end
p = SimpleForm("provider_file_manage",translate("Provider File Manage"))
p.reset = false
p.submit = false
local provider_manage = {
{proxy_mg, rule_mg, game_mg}
}
promg = p:section(Table, provider_manage)
o = promg:option(Button, "proxy_mg", " ")
o.inputtitle = translate("Proxy Provider File List")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage"))
end
o = promg:option(Button, "rule_mg", " ")
o.inputtitle = translate("Rule Providers File List")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage"))
end
o = promg:option(Button, "game_mg", " ")
o.inputtitle = translate("Game Rules File List")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-file-manage"))
end
m = SimpleForm("openclash",translate("Config File Edit"))
m.reset = false
m.submit = false
local tab = {
{user, default}
}
s = m:section(Table, tab)
s.description = align_mid..translate("Support syntax check, press").." "..font_green..bold_on.."F10"..bold_off..font_off.." "..translate("to control diff option, press").." "..font_green..bold_on.."F11"..bold_off..font_off.." "..translate("to enter full screen editing mode")..align_mid_off
s.anonymous = true
s.addremove = false
local conf = uci:get("openclash", "config", "config_path")
local dconf = "/usr/share/openclash/res/default.yaml"
if not conf then conf = "/etc/openclash/config/config.yaml" end
local conf_name = fs.basename(conf)
if not conf_name then conf_name = "config.yaml" end
local sconf = "/etc/openclash/"..conf_name
sev = s:option(TextValue, "user")
---sev.description = align_mid..translate("Modify Your Config file:").." "..font_green..bold_on..conf_name..bold_off..font_off.." "..translate("Here, Except The Settings That Were Taken Over")..align_mid_off
sev.rows = 40
sev.wrap = "off"
sev.cfgvalue = function(self, section)
return NXFS.readfile(conf) or NXFS.readfile(dconf) or ""
end
sev.write = function(self, section, value)
if (CHIF == "0") then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile(conf)
if value ~= old_value then
NXFS.writefile(conf, value)
end
end
end
def = s:option(TextValue, "default")
if fs.isfile(sconf) then
---def.description = align_mid..translate("Config File Edited By OpenClash For Running")..align_mid_off
else
---def.description = align_mid..translate("Default Config File With Correct Template")..align_mid_off
end
def.rows = 40
def.wrap = "off"
def.readonly = true
def.cfgvalue = function(self, section)
return NXFS.readfile(sconf) or NXFS.readfile(dconf) or ""
end
def.write = function(self, section, value)
end
local t = {
{Commit, Create, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
uci:commit("openclash")
end
o = a:option(DummyValue, "Create", " ")
o.rawhtml = true
o.template = "openclash/input_file_name"
o.value = "/etc/openclash/config/"
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
uci:set("openclash", "config", "enable", 1)
uci:commit("openclash")
SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/config_editor"))
return ful , form , p , m

View File

@ -0,0 +1,170 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local SYS = require "luci.sys"
local sid = arg[1]
font_red = [[<b style=color:red>]]
font_green = [[<b style=color:green>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
m = Map(openclash, translate("Add Custom DNS Servers"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/config-overwrite")
if m.uci:get(openclash, sid) ~= "dns_servers" then
luci.http.redirect(m.redirect)
return
end
-- [[ Edit Custom DNS ]] --
s = m:section(NamedSection, sid, "dns_servers")
s.anonymous = true
s.addremove = false
---- group
o = s:option(ListValue, "group", translate("DNS Server Group"))
o.description = font_red..bold_on..translate("NameServer Group Must Be Set")..bold_off..font_off
o:value("nameserver", translate("NameServer "))
o:value("fallback", translate("FallBack "))
o:value("default", translate("Default-NameServer"))
o.default = "nameserver"
o.rempty = false
---- IP address
o = s:option(Value, "ip", translate("DNS Server Address"))
o.description = translate("Do Not Add Type Ahead")
o.placeholder = translate("Not Null")
o.datatype = "or(host, string)"
o.rmempty = true
---- port
o = s:option(Value, "port", translate("DNS Server Port"))
o.description = translate("Require When Use Non-Standard Port")
o.datatype = "port"
o.rempty = true
---- type
o = s:option(ListValue, "type", translate("DNS Server Type"))
o.description = translate("Communication protocol")
o:value("udp", translate("UDP"))
o:value("tcp", translate("TCP"))
o:value("tls", translate("TLS"))
o:value("https", translate("HTTPS"))
o:value("quic", translate("QUIC"))
o.default = "udp"
o.rempty = false
---- interface
o = s:option(Value, "interface", translate("Specific Interface"))
o.description = translate("DNS Lookup Only Through The Specific Interface")
local interfaces = SYS.exec("ls -l /sys/class/net/ 2>/dev/null |awk '{print $9}' 2>/dev/null")
for interface in string.gmatch(interfaces, "%S+") do
o:value(interface)
end
o:value("Disable", translate("Disable"))
o.default = "Disable"
o.rempty = false
---- direct-nameserver
o = s:option(Flag, "direct_nameserver", translate("Direct Nameserver"), translate("Use For Domain Need Direct"))
o.rmempty = false
o.default = o.disbled
---- Node Domain Resolve
o = s:option(Flag, "node_resolve", translate("Node Domain Resolve"), translate("Use For Node Domain Resolve"))
o.rmempty = false
o.default = o.disbled
---- Force HTTP/3
o = s:option(Flag, "http3", translate("Force HTTP/3"), translate("Force HTTP/3 to connect"))
o:depends("type", "https")
o.rmempty = false
o.default = o.disbled
---- Skip-cert-verify
o = s:option(Flag, "skip_cert_verify", translate("skip-cert-verify"), translate("skip-cert-verify"))
o:depends("type", "https")
o:depends("type", "quic")
o.rmempty = false
o.default = o.disbled
---- ECS Subnet
o = s:option(Value, "ecs_subnet", translate("ECS Subnet"),translate("Specify the ECS Subnet Address"))
o:depends("type", "https")
o.rmempty = true
o.datatype = "ipaddr"
o:value("1.1.1.1/24")
---- ECS Override
o = s:option(Flag, "ecs_override", translate("ECS Override"),translate("Override the ECS Subnet Address"))
o:depends("type", "https")
o.rmempty = false
o.default = o.disbled
---- disable-ipv4
o = s:option(Flag, "disable_ipv4", translate("Disable-IPv4"),translate("Drop The Type of DNS Responsed"))
o.rmempty = false
o.default = o.disbled
---- disable-ipv6
o = s:option(Flag, "disable_ipv6", translate("Disable-IPv6"),translate("Drop The Type of DNS Responsed"))
o.rmempty = false
o.default = o.disbled
---- Proxy group
o = s:option(Value, "specific_group", translate("Specific Group (Support Regex)"))
o.description = translate("Group Use For Proxy The DNS")
o:depends("group", "nameserver")
o:depends("group", "fallback")
local groupnames,filename
filename = m.uci:get(openclash, "config", "config_path")
if filename then
groupnames = SYS.exec(string.format('ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "YAML.load_file(\'%s\')[\'proxy-groups\'].each do |i| puts i[\'name\']+\'##\' end" 2>/dev/null',filename))
if groupnames then
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
o:value(groupname)
end
end
end
end
m.uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
o:value(s.name)
end
end)
o:value("DIRECT")
o:value("GLOBAL")
o:value("Disable", translate("Disable"))
o.default = "Disable"
o.rempty = false
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,123 @@
local rule_form
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local g,h={}
for n,m in ipairs(fs.glob("/etc/openclash/game_rules/*"))do
h=fs.stat(m)
if h then
g[n]={}
g[n].num=string.format(n)
g[n].name=fs.basename(m)
g[n].mtime=os.date("%Y-%m-%d %H:%M:%S",h.mtime)
g[n].size=fs.filesize(h.size)
g[n].remove=0
g[n].enable=false
end
end
rule_form=SimpleForm("game_rules_file_list",translate("Game Rules File List"))
rule_form.reset=false
rule_form.submit=false
tb2=rule_form:section(Table,g)
nu2=tb2:option(DummyValue,"num",translate("Serial Number"))
nm2=tb2:option(DummyValue,"name",translate("File Name"))
mt2=tb2:option(DummyValue,"mtime",translate("Update Time"))
sz2=tb2:option(DummyValue,"size",translate("Size"))
btned1=tb2:option(Button,"edit",translate("Edit"))
btned1.render=function(g,n,h)
g.inputstyle="apply"
Button.render(g,n,h)
end
btned1.write=function(h,n)
local file_path = "etc/openclash/game_rules/" .. fs.basename(g[n].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "game-rules-file-manage", "%s") %file_path)
end
btndl2 = tb2:option(Button,"download2",translate("Download Config"))
btndl2.template="openclash/other_button"
btndl2.render=function(m,n,h)
m.inputstyle="remove"
Button.render(m,n,h)
end
btndl2.write = function (h,n)
local sPath, sFile, fd, block
sPath = "/etc/openclash/game_rules/"..g[n].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then
return
end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block ==0) then
break
else
HTTP.write(block)
end
end
fd:close()
HTTP.close()
end
btnrm2=tb2:option(Button,"remove2",translate("Remove"))
btnrm2.render=function(g,n,h)
g.inputstyle="reset"
Button.render(g,n,h)
end
btnrm2.write=function(h,n)
local h=fs.unlink("/etc/openclash/game_rules/"..luci.openclash.basename(g[n].name))
if h then table.remove(g,n)end
return h
end
local t = {
{Refresh, Create, Delete_all, Apply}
}
a = rule_form:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-file-manage"))
end
o = a:option(DummyValue, "Create", " ")
o.rawhtml = true
o.template = "openclash/input_file_name"
o.value = "/etc/openclash/game_rules/"
o = a:option(Button, "Delete_all", " ")
o.inputtitle = translate("Delete All File")
o.inputstyle = "remove"
o.write = function()
luci.sys.call("rm -rf /etc/openclash/game_rules/* >/dev/null 2>&1")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-file-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
rule_form:append(Template("openclash/toolbar_show"))
return rule_form

View File

@ -0,0 +1,100 @@
local form, m
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
m = SimpleForm("openclash", translate("Game Rules List"))
m.description=translate("Rule Project:").." SSTap-Rule ( https://github.com/FQrabbit/SSTap-Rule )"
m.reset = false
m.submit = false
local t = {
{Refresh, Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
SYS.call("rm -rf /tmp/rules_name 2>/dev/null")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-settings"))
end
if not NXFS.access("/tmp/rules_name") then
SYS.call("awk -F ',' '{print $1}' /usr/share/openclash/res/game_rules.list > /tmp/rules_name 2>/dev/null")
end
file = io.open("/tmp/rules_name", "r");
---- Rules List
local e={},o,t
if NXFS.access("/tmp/rules_name") then
for o in file:lines() do
table.insert(e,o)
end
for t,o in ipairs(e) do
e[t]={}
e[t].num=string.format(t)
e[t].name=o
e[t].filename=string.sub(luci.sys.exec(string.format("grep -F '%s,' /usr/share/openclash/res/game_rules.list |awk -F ',' '{print $3}' 2>/dev/null",e[t].name)),1,-2)
if e[t].filename == "" then
e[t].filename=string.sub(luci.sys.exec(string.format("grep -F '%s,' /usr/share/openclash/res/game_rules.list |awk -F ',' '{print $2}' 2>/dev/null",e[t].name)),1,-2)
end
RULE_FILE="/etc/openclash/game_rules/".. e[t].filename
if fs.mtime(RULE_FILE) then
e[t].size=fs.filesize(fs.stat(RULE_FILE).size)
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",fs.mtime(RULE_FILE))
else
e[t].size="/"
e[t].mtime="/"
end
if fs.isfile(RULE_FILE) then
e[t].exist=translate("Exist")
else
e[t].exist=translate("Not Exist")
end
e[t].remove=0
end
end
file:close()
form=SimpleForm("filelist")
form.reset=false
form.submit=false
tb=form:section(Table,e)
nu=tb:option(DummyValue,"num",translate("Serial Number"))
st=tb:option(DummyValue,"exist",translate("State"))
st.template="openclash/cfg_check"
nm=tb:option(DummyValue,"name",translate("Rule Name"))
fm=tb:option(DummyValue,"filename",translate("File Name"))
sz=tb:option(DummyValue,"size",translate("Size"))
mt=tb:option(DummyValue,"mtime",translate("Update Time"))
btnis=tb:option(DummyValue,"filename",translate("Download Rule"))
btnis.template="openclash/download_rule"
btnrm=tb:option(Button,"remove",translate("Remove"))
btnrm.render=function(e,t,a)
e.inputstyle="reset"
Button.render(e,t,a)
end
btnrm.write=function(a,t)
fs.unlink("/etc/openclash/game_rules/"..e[t].filename)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-manage"))
end
return m, form

View File

@ -0,0 +1,176 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local sid = arg[1]
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
m = Map(openclash, translate("Edit Group"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers")
if m.uci:get(openclash, sid) ~= "groups" then
luci.http.redirect(m.redirect)
return
end
-- [[ Groups Setting ]]--
s = m:section(NamedSection, sid, "groups")
s.anonymous = true
s.addremove = false
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
o = s:option(ListValue, "type", translate("Group Type"))
o.rmempty = false
o.description = translate("Choose The Operation Mode")
o:value("select", translate("Manual-Select"))
o:value("smart", translate("Smart-Select"))
o:value("url-test", translate("URL-Test"))
o:value("fallback", translate("Fallback"))
o:value("load-balance", translate("Load-Balance"))
o:value("relay", translate("Relay-Traffic"))
o = s:option(Value, "name", translate("Group Name"))
o.rmempty = false
o.default = "Group - "..sid
o = s:option(ListValue, "disable_udp", translate("Disable UDP"))
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o.rmempty = true
o = s:option(Value, "test_url", translate("Test URL"))
o:value("http://cp.cloudflare.com/generate_204")
o:value("http://www.gstatic.com/generate_204")
o:value("https://cp.cloudflare.com/generate_204")
o.rmempty = true
o:depends("type", "url-test")
o:depends("type", "fallback")
o:depends("type", "load-balance")
o:depends("type", "smart")
o = s:option(Value, "test_interval", translate("Test Interval(s)"))
o.default = "300"
o.rmempty = true
o:depends("type", "url-test")
o:depends("type", "fallback")
o:depends("type", "load-balance")
o:depends("type", "smart")
o = s:option(ListValue, "strategy", translate("Strategy Type"))
o.rmempty = true
o.description = translate("Choose The Load-Balance's Strategy Type")
o:value("round-robin", translate("Round-robin"))
o:value("consistent-hashing", translate("Consistent-hashing"))
o:value("sticky-sessions", translate("Sticky-sessions"))
o:depends("type", "load-balance")
o = s:option(ListValue, "strategy_smart", translate("Strategy Type"))
o.rmempty = true
o.description = translate("Choose The Smart's Strategy Type")
o:value("round-robin", translate("Round-robin"))
o:value("sticky-sessions", translate("Sticky-sessions"))
o:depends("type", "smart")
o = s:option(ListValue, "uselightgbm", translate("Uselightgbm"))
o.description = translate("Use LightGBM Model For Smart Group Weight Prediction")
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o.rmempty = true
o:depends("type", "smart")
o = s:option(ListValue, "collectdata", translate("Collectdata"))
o.description = translate("Collect Datas For Smart Group Model Training")
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = "false"
o.rmempty = true
o:depends("type", "smart")
o = s:option(Value, "policy_priority", translate("Policy Priority"))
o.description = translate("The Priority Of The Nodes, The Higher Than 1, The More Likely It Is To Be Selected, The Default Is 1, Support Regex")
o.rmempty = true
o.placeholder = "Premium:0.9;SG:1.3"
o.rmempty = true
o:depends("type", "smart")
o = s:option(Value, "tolerance", translate("Tolerance(ms)"))
o.default = "150"
o.rmempty = true
o:depends("type", "url-test")
o = s:option(Value, "policy_filter", translate("Provider Filter"))
o.rmempty = true
o.placeholder = "bgp|sg"
o = s:option(DynamicList, "other_group", translate("Other Group (Support Regex)"))
o.description = font_red..bold_on..translate("The Added Proxy Groups Must Exist Except 'DIRECT' & 'REJECT' & 'REJECT-DROP' & 'PASS' & 'GLOBAL'")..bold_off..font_off
o:value("all", translate("All Groups"))
uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil and s.name ~= m.uci:get(openclash, sid, "name") then
o:value(s.name)
end
end)
o:value("DIRECT")
o:value("REJECT")
o:value("REJECT-DROP")
o:value("PASS")
o:value("GLOBAL")
o.rmempty = true
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
sys.call("/usr/share/openclash/yml_groups_name_ch.sh")
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,22 @@
--
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
m = Map("openclash", translate("Server Logs"))
s = m:section(TypedSection, "openclash")
m.pageaction = false
s.anonymous = true
s.addremove=false
log = s:option(TextValue, "clog")
log.readonly=true
log.pollcheck=true
log.template="openclash/log"
log.description = translate("")
log.rows = 29
m:append(Template("openclash/toolbar_show"))
m:append(Template("openclash/config_editor"))
return m

View File

@ -0,0 +1,73 @@
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local fs = require "luci.openclash"
local file_path = ""
local edit_file_name = "/tmp/openclash_edit_file_name"
for i = 2, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
end
--re-get file path to save
if NXFS.readfile(edit_file_name) ~= file_path and fs.isfile(file_path) then
NXFS.writefile(edit_file_name, file_path)
else
if not fs.isfile(file_path) and fs.isfile(edit_file_name) then
file_path = NXFS.readfile(edit_file_name)
fs.unlink(edit_file_name)
end
end
m = Map("openclash", translate("File Edit"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/"..arg[1])
s = m:section(TypedSection, "openclash")
s.anonymous = true
s.addremove=false
o = s:option(TextValue, "edit_file")
o.rows = 50
o.wrap = "off"
function o.write(self, section, value)
if value then
value = value:gsub("\r\n?", "\n")
local old_value = NXFS.readfile(file_path)
if value ~= old_value then
NXFS.writefile(file_path, value)
end
end
end
function o.cfgvalue(self, section)
return NXFS.readfile(file_path) or ""
end
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
luci.http.redirect(m.redirect)
end
m:append(Template("openclash/config_editor"))
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,347 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local sid = arg[1]
font_red = [[<b style=color:red>]]
font_green = [[<b style=color:green>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
m = Map(openclash, translate("Other Rules Edit"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/config-overwrite")
if m.uci:get(openclash, sid) ~= "other_rules" then
luci.http.redirect(m.redirect)
return
end
-- [[ Other Rules Setting ]]--
s = m:section(NamedSection, sid, "other_rules")
s.anonymous = true
s.addremove = false
o = s:option(Value, "Note", translate("Note"))
o.default = "default"
o.rmempty = false
o = s:option(ListValue, "config", translate("Config File"))
local e,a={}
local groupnames,filename
local group_list = {}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
if e[t].name == m.uci:get(openclash, sid, "config") then
filename = e[t].name
groupnames = sys.exec(string.format('ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "YAML.load_file(\'%s\')[\'proxy-groups\'].each do |i| puts i[\'name\']+\'##\' end" 2>/dev/null',f))
if groupnames then
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
table.insert(group_list, groupname)
end
end
end
end
end
end
m.uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
table.insert(group_list, s.name)
end
end)
table.sort(group_list)
table.insert(group_list, "DIRECT")
table.insert(group_list, "REJECT")
table.insert(group_list, "REJECT-DROP")
table.insert(group_list, "PASS")
table.insert(group_list, "GLOBAL")
o = s:option(Button, translate("Get Group Names"))
o.title = translate("Get Group Names")
o.inputtitle = translate("Get Group Names")
o.description = translate("Get Group Names After Select Config File")
o.inputstyle = "reload"
o.write = function()
m.uci:commit("openclash")
luci.http.redirect(luci.dispatcher.build_url("admin/services/openclash/other-rules-edit/%s") % sid)
end
if group_list ~= nil and filename ~= nil then
o = s:option(ListValue, "rule_name", translate("Other Rules Name"))
o.rmempty = true
o:value("lhie1", translate("lhie1 Rules"))
o = s:option(ListValue, "GlobalTV", translate("GlobalTV"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "AsianTV", translate("AsianTV"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "MainlandTV", translate("CN Mainland TV"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Proxy", translate("Proxy"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Youtube", translate("Youtube"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Bilibili", translate("Bilibili"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Bahamut", translate("Bahamut"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "HBOMax", translate("HBO Max"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Pornhub", translate("Pornhub"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Apple", translate("Apple"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "AppleTV", translate("Apple TV"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "GoogleFCM", translate("Google FCM"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Scholar", translate("Scholar"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Microsoft", translate("Microsoft"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "AI_Suite", translate("AI Suite"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Netflix", translate("Netflix"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Disney", translate("Disney Plus"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Discovery", translate("Discovery Plus"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "DAZN", translate("DAZN"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Spotify", translate("Spotify"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Steam", translate("Steam"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "TikTok", translate("TikTok"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "miHoYo", translate("miHoYo"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Speedtest", translate("Speedtest"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Telegram", translate("Telegram"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Crypto", translate("Crypto"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Discord", translate("Discord"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "PayPal", translate("PayPal"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "AdBlock", translate("AdBlock"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "HTTPDNS", translate("HTTPDNS"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Domestic", translate("Domestic"))
o:depends("rule_name", "lhie1")
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "Others", translate("Others"))
o:depends("rule_name", "lhie1")
o.rmempty = true
o.description = translate("Choose Proxy Groups, Base On Your Config File").." ( "..font_green..bold_on..filename..bold_off..font_off.." )"
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
end
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
--luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
return m

View File

@ -0,0 +1,194 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local sys = require "luci.sys"
local sid = arg[1]
local fs = require "luci.openclash"
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
m = Map(openclash, translate("Edit Proxy-Provider"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers")
if m.uci:get(openclash, sid) ~= "proxy-provider" then
luci.http.redirect(m.redirect)
return
end
-- [[ Provider Setting ]]--
s = m:section(NamedSection, sid, "proxy-provider")
s.anonymous = true
s.addremove = false
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
o = s:option(Flag, "manual", translate("Custom Tag"))
o.rmempty = false
o.default = "0"
o.description = translate("Mark as Custom Node to Prevent Retention config from being Deleted When Enabled")
o = s:option(ListValue, "type", translate("Provider Type"))
o.rmempty = true
o.description = translate("Choose The Provider Type")
o:value("http")
o:value("file")
o:value("inline")
o = s:option(Value, "name", translate("Provider Name"))
o.rmempty = false
o.default = "Proxy-provider - "..sid
o = s:option(ListValue, "path", translate("Provider Path"))
o.description = translate("Update Your Proxy Provider File From Config Luci Page")
local p,h={}
for t,f in ipairs(fs.glob("/etc/openclash/proxy_provider/*"))do
h=fs.stat(f)
if h then
p[t]={}
p[t].name=fs.basename(f)
if IsYamlFile(p[t].name) or IsYmlFile(p[t].name) then
o:value("./proxy_provider/"..p[t].name)
end
end
end
o.rmempty = false
o:depends("type", "file")
o = s:option(Value, "provider_url", translate("Provider URL"))
o.rmempty = false
o:depends("type", "http")
o = s:option(Value, "provider_filter", translate("Provider Filter"))
o.rmempty = true
o.placeholder = "bgp|sg"
o = s:option(Value, "provider_interval", translate("Provider Interval(s)"))
o.default = "3600"
o.rmempty = false
o:depends("type", "http")
o = s:option(ListValue, "health_check", translate("Provider Health Check"))
o:value("false", translate("Disable"))
o:value("true", translate("Enable"))
o.default = true
o = s:option(Value, "health_check_url", translate("Health Check URL"))
o:value("http://cp.cloudflare.com/generate_204")
o:value("http://www.gstatic.com/generate_204")
o:value("https://cp.cloudflare.com/generate_204")
o.rmempty = false
o = s:option(Value, "health_check_interval", translate("Health Check Interval(s)"))
o.default = "300"
o.rmempty = false
-- [[ other-setting ]]--
o = s:option(Value, "other_parameters", translate("Other Parameters"))
o.template = "cbi/tvalue"
o.rows = 20
o.wrap = "off"
o.description = font_red..bold_on..translate("Edit Your Other Parameters Here")..bold_off..font_off
o.rmempty = true
function o.cfgvalue(self, section)
if self.map:get(section, "other_parameters") == nil then
return "# Example:\n"..
"# Only support YAML, four spaces need to be reserved at the beginning of each line to maintain formatting alignment\n"..
"# 示例:\n"..
"# 仅支持 YAML, 每行行首需要多保留四个空格以使脚本处理后能够与上方配置保持格式对齐\n"..
"# header:\n"..
"# User-Agent:\n"..
"# - \"Clash/v1.18.0\"\n"..
"# - \"mihomo/1.18.3\"\n"..
"# Authorization:\n"..
"# - \"token 1231231\"\n"..
"# override:\n"..
"# skip-cert-verify: true\n"..
"# additional-prefix: \"provider1 prefix |\"\n"..
"# additional-suffix: \"| provider1 suffix\"\n"..
"# proxy-name:\n"..
"# - pattern: \"IPLC-(.*?)倍\"\n"..
"# target: \"iplc x $1\"\n"..
"# exclude-type: \"ss|http\"\n"..
"\n"..
"# inline Example:\n"..
"# payload:\n"..
"# - name: \"ss1\"\n"..
"# type: ss\n"..
"# server: server\n"..
"# port: 443\n"..
"# cipher: chacha20-ietf-poly1305\n"..
"# password: \"password\""
else
return Value.cfgvalue(self, section)
end
end
function o.validate(self, value)
if value then
value = value:gsub("\r\n?", "\n")
value = value:gsub("%c*$", "")
end
return value
end
o = s:option(DynamicList, "groups", translate("Proxy Group (Support Regex)"))
o.description = font_red..bold_on..translate("No Need Set when Config Create, The added Proxy Groups Must Exist")..bold_off..font_off
o.rmempty = true
o:value("all", translate("All Groups"))
m.uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
o:value(s.name)
end
end)
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
m:append(Template("openclash/config_editor"))
return m

View File

@ -0,0 +1,123 @@
local proxy_form
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local p,r={}
for x,y in ipairs(fs.glob("/etc/openclash/proxy_provider/*"))do
r=fs.stat(y)
if r then
p[x]={}
p[x].num=string.format(x)
p[x].name=fs.basename(y)
p[x].mtime=os.date("%Y-%m-%d %H:%M:%S",r.mtime)
p[x].size=fs.filesize(r.size)
p[x].remove=0
p[x].enable=false
end
end
proxy_form=SimpleForm("proxy_provider_file_list",translate("Proxy Provider File List"))
proxy_form.reset=false
proxy_form.submit=false
tb1=proxy_form:section(Table,p)
nu1=tb1:option(DummyValue,"num",translate("Serial Number"))
nm1=tb1:option(DummyValue,"name",translate("File Name"))
mt1=tb1:option(DummyValue,"mtime",translate("Update Time"))
sz1=tb1:option(DummyValue,"size",translate("Size"))
btned1=tb1:option(Button,"edit",translate("Edit"))
btned1.render=function(p,x,r)
p.inputstyle="apply"
Button.render(p,x,r)
end
btned1.write=function(r,x)
local file_path = "etc/openclash/proxy_provider/" .. fs.basename(p[x].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "proxy-provider-file-manage", "%s") %file_path)
end
btndl1 = tb1:option(Button,"download1",translate("Download Config"))
btndl1.template="openclash/other_button"
btndl1.render=function(y,x,r)
y.inputstyle="remove"
Button.render(y,x,r)
end
btndl1.write = function (r,x)
local sPath, sFile, fd, block
sPath = "/etc/openclash/proxy_provider/"..p[x].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then
return
end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block ==0) then
break
else
HTTP.write(block)
end
end
fd:close()
HTTP.close()
end
btnrm1=tb1:option(Button,"remove1",translate("Remove"))
btnrm1.render=function(p,x,r)
p.inputstyle="reset"
Button.render(p,x,r)
end
btnrm1.write=function(r,x)
local r=fs.unlink("/etc/openclash/proxy_provider/"..luci.openclash.basename(p[x].name))
if r then table.remove(p,x)end
return r
end
local t = {
{Refresh, Create, Delete_all, Apply}
}
a = proxy_form:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage"))
end
o = a:option(DummyValue, "Create", " ")
o.rawhtml = true
o.template = "openclash/input_file_name"
o.value = "/etc/openclash/proxy_provider/"
o = a:option(Button, "Delete_all", " ")
o.inputtitle = translate("Delete All File")
o.inputstyle = "remove"
o.write = function()
luci.sys.call("rm -rf /etc/openclash/proxy_provider/* >/dev/null 2>&1")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
proxy_form:append(Template("openclash/toolbar_show"))
return proxy_form

View File

@ -0,0 +1,208 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local sid = arg[1]
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
m = Map(openclash, translate("Edit Rule Providers"))
m.pageaction = false
m.description=translate("规则集使用介绍https://wiki.metacubex.one/config/rule-providers/content/")
m.redirect = luci.dispatcher.build_url("admin/services/openclash/rule-providers-settings")
if m.uci:get(openclash, sid) ~= "rule_providers" then
luci.http.redirect(m.redirect)
return
end
-- [[ Rule Providers Setting ]]--
s = m:section(NamedSection, sid, "rule_providers")
s.anonymous = true
s.addremove = false
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
o = s:option(Value, "name", translate("Rule Providers Name"))
o.rmempty = false
o.default = "Rule-provider - "..sid
o = s:option(ListValue, "type", translate("Rule Providers Type"))
o.rmempty = true
o.description = translate("Choose The Rule Providers Type")
o:value("http")
o:value("file")
o:value("inline")
o = s:option(ListValue, "format", translate("Rule Format"))
o.rmempty = true
o.description = translate("Choose The Rule File Format, For More Info:").." ".."<a href='javascript:void(0)' onclick='javascript:return winOpen(\"https://wiki.metacubex.one/config/rule-providers/content/\")'>https://wiki.metacubex.one/config/rule-providers/content/</a>"
o:value("yaml")
o:value("text")
o:value("mrs")
o:depends("type", "file")
o:depends("type", "http")
o = s:option(ListValue, "behavior", translate("Rule Behavior"))
o.rmempty = true
o.description = translate("Choose The Rule Behavior")
o:value("domain")
o:value("ipcidr")
o:value("classical", translate("classical").." "..translate("(Not Support mrs Format)"))
o = s:option(ListValue, "path", translate("Rule Providers Path"))
o.description = translate("Update Your Rule Providers File From Config Luci Page")
local p,h={}
for t,f in ipairs(fs.glob("/etc/openclash/rule_provider/*"))do
h=fs.stat(f)
if h then
p[t]={}
p[t].name=fs.basename(f)
o:value("./rule_provider/"..p[t].name)
end
end
for t,f in ipairs(fs.glob("/etc/openclash/game_rules/*"))do
h=fs.stat(f)
if h then
p[t]={}
p[t].name=fs.basename(f)
o:value("./game_rules/"..p[t].name)
end
end
o.rmempty = false
o:depends("type", "file")
o = s:option(Value, "url", translate("Rule Providers URL"))
o.rmempty = false
o:depends("type", "http")
o = s:option(Value, "interval", translate("Rule Providers Interval(s)"))
o.default = "86400"
o.rmempty = false
o:depends("type", "http")
o = s:option(ListValue, "position", translate("Append Position"))
o.rmempty = false
o:value("0", translate("Priority Match"))
o:value("1", translate("Extended Match"))
o = s:option(ListValue, "group", translate("Set Proxy Group"))
o.description = font_red..bold_on..translate("The Added Proxy Groups Must Exist Except 'DIRECT' & 'REJECT' & 'REJECT-DROP' & 'PASS' & 'GLOBAL'")..bold_off..font_off
o.rmempty = true
local groupnames, filename
local group_list = {}
filename = m.uci:get(openclash, "config", "config_path")
if filename then
groupnames = sys.exec(string.format('ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "YAML.load_file(\'%s\')[\'proxy-groups\'].each do |i| puts i[\'name\']+\'##\' end" 2>/dev/null',filename))
if groupnames then
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
table.insert(group_list, groupname)
end
end
end
end
m.uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
table.insert(group_list, s.name)
end
end)
table.sort(group_list)
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o:value("DIRECT")
o:value("REJECT")
o:value("REJECT-DROP")
o:value("PASS")
o:value("GLOBAL")
-- [[ other-setting ]]--
o = s:option(Value, "other_parameters", translate("Other Parameters"))
o.template = "cbi/tvalue"
o.rows = 20
o.wrap = "off"
o.description = font_red..bold_on..translate("Edit Your Other Parameters Here")..bold_off..font_off
o.rmempty = true
function o.cfgvalue(self, section)
if self.map:get(section, "other_parameters") == nil then
return "# Example:\n"..
"# Only support YAML, four spaces need to be reserved at the beginning of each line to maintain formatting alignment\n"..
"# 示例:\n"..
"# 仅支持 YAML, 每行行首需要多保留四个空格以使脚本处理后能够与上方配置保持格式对齐\n"..
"# inline Example:\n"..
"# payload:\n"..
"# - '.blogger.com'\n"..
"# - '*.*.microsoft.com'\n"..
"# - 'books.itunes.apple.com'\n"
else
return Value.cfgvalue(self, section)
end
end
function o.validate(self, value)
if value then
value = value:gsub("\r\n?", "\n")
value = value:gsub("%c*$", "")
end
return value
end
local t = {
{Commit, Back}
}
a = m:section(Table, t)
o = a:option(Button,"Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
sys.call("/usr/share/openclash/yml_groups_name_ch.sh")
luci.http.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
m:append(Template("openclash/config_editor"))
return m

View File

@ -0,0 +1,123 @@
local rule_form
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
local g,h={}
for n,m in ipairs(fs.glob("/etc/openclash/rule_provider/*"))do
h=fs.stat(m)
if h then
g[n]={}
g[n].num=string.format(n)
g[n].name=fs.basename(m)
g[n].mtime=os.date("%Y-%m-%d %H:%M:%S",h.mtime)
g[n].size=fs.filesize(h.size)
g[n].remove=0
g[n].enable=false
end
end
rule_form=SimpleForm("rule_provider_file_list",translate("Rule Providers File List"))
rule_form.reset=false
rule_form.submit=false
tb2=rule_form:section(Table,g)
nu2=tb2:option(DummyValue,"num",translate("Serial Number"))
nm2=tb2:option(DummyValue,"name",translate("File Name"))
mt2=tb2:option(DummyValue,"mtime",translate("Update Time"))
sz2=tb2:option(DummyValue,"size",translate("Size"))
btned1=tb2:option(Button,"edit",translate("Edit"))
btned1.render=function(g,n,h)
g.inputstyle="apply"
Button.render(g,n,h)
end
btned1.write=function(h,n)
local file_path = "etc/openclash/rule_provider/" .. fs.basename(g[n].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "rule-providers-file-manage", "%s") %file_path)
end
btndl2 = tb2:option(Button,"download2",translate("Download Config"))
btndl2.template="openclash/other_button"
btndl2.render=function(m,n,h)
m.inputstyle="remove"
Button.render(m,n,h)
end
btndl2.write = function (h,n)
local sPath, sFile, fd, block
sPath = "/etc/openclash/rule_provider/"..g[n].name
sFile = NXFS.basename(sPath)
if fs.isdirectory(sPath) then
fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r")
sFile = sFile .. ".tar.gz"
else
fd = nixio.open(sPath, "r")
end
if not fd then
return
end
HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile})
HTTP.prepare_content("application/octet-stream")
while true do
block = fd:read(nixio.const.buffersize)
if (not block) or (#block ==0) then
break
else
HTTP.write(block)
end
end
fd:close()
HTTP.close()
end
btnrm2=tb2:option(Button,"remove2",translate("Remove"))
btnrm2.render=function(g,n,h)
g.inputstyle="reset"
Button.render(g,n,h)
end
btnrm2.write=function(h,n)
local h=fs.unlink("/etc/openclash/rule_provider/"..luci.openclash.basename(g[n].name))
if h then table.remove(g,n)end
return h
end
local t = {
{Refresh, Create, Delete_all, Apply}
}
a = rule_form:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage"))
end
o = a:option(DummyValue, "Create", " ")
o.rawhtml = true
o.template = "openclash/input_file_name"
o.value = "/etc/openclash/rule_provider/"
o = a:option(Button, "Delete_all", " ")
o.inputtitle = translate("Delete All File")
o.inputstyle = "remove"
o.write = function()
luci.sys.call("rm -rf /etc/openclash/rule_provider/* >/dev/null 2>&1")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
rule_form:append(Template("openclash/toolbar_show"))
return rule_form

View File

@ -0,0 +1,106 @@
local form, m
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
m = SimpleForm("openclash", translate("Other Rule Providers List"))
m.description=translate("Rule Project:").." lhie1 ( https://github.com/dler-io/Rules )<br/>"..
translate("Rule Project:").." ACL4SSR ( https://github.com/ACL4SSR/ACL4SSR/tree/master )"
m.reset = false
m.submit = false
local t = {
{Apply}
}
a = m:section(Table, t)
o = a:option(Button, "Refresh", " ")
o.inputtitle = translate("Refresh Page")
o.inputstyle = "apply"
o.write = function()
SYS.call("rm -rf /tmp/rule_providers_name 2>/dev/null")
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-manage"))
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-settings"))
end
if not NXFS.access("/tmp/rule_providers_name") then
SYS.call("awk -v d=',' -F ',' '{print $4d$5}' /usr/share/openclash/res/rule_providers.list > /tmp/rule_providers_name 2>/dev/null")
end
file = io.open("/tmp/rule_providers_name", "r");
---- Rules List
local e={},o,t
if NXFS.access("/tmp/rule_providers_name") then
for o in file:lines() do
table.insert(e,o)
end
for t,o in ipairs(e) do
e[t]={}
e[t].num=string.format(t)
e[t].name=string.sub(luci.sys.exec(string.format("grep -F '%s' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $1}' 2>/dev/null",o)),1,-2)
e[t].lfilename=string.sub(luci.sys.exec(string.format("grep -F '%s' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $6}' 2>/dev/null",o)),1,-2)
if e[t].lfilename == "" then
e[t].lfilename=string.sub(luci.sys.exec(string.format("grep -F '%s' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $5}' 2>/dev/null",o)),1,-2)
end
e[t].filename=o
e[t].author=string.sub(luci.sys.exec(string.format("grep -F '%s' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $2}' 2>/dev/null",o)),1,-2)
e[t].rule_type=string.sub(luci.sys.exec(string.format("grep -F '%s' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $3}' 2>/dev/null",o)),1,-2)
RULE_FILE="/etc/openclash/rule_provider/".. e[t].lfilename
if fs.mtime(RULE_FILE) then
e[t].size=fs.filesize(fs.stat(RULE_FILE).size)
e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",fs.mtime(RULE_FILE))
else
e[t].size="/"
e[t].mtime="/"
end
if fs.isfile(RULE_FILE) then
e[t].exist=translate("Exist")
else
e[t].exist=translate("Not Exist")
end
e[t].remove=0
end
end
file:close()
form=SimpleForm("filelist")
form.reset=false
form.submit=false
tb=form:section(Table,e)
nu=tb:option(DummyValue,"num",translate("Serial Number"))
st=tb:option(DummyValue,"exist",translate("State"))
st.template="openclash/cfg_check"
tp=tb:option(DummyValue,"rule_type",translate("Rule Type"))
nm=tb:option(DummyValue,"name",translate("Rule Name"))
au=tb:option(DummyValue,"author",translate("Rule Author"))
fm=tb:option(DummyValue,"lfilename",translate("File Name"))
sz=tb:option(DummyValue,"size",translate("Size"))
mt=tb:option(DummyValue,"mtime",translate("Update Time"))
btnis=tb:option(DummyValue,"filename",translate("Download Rule"))
btnis.template="openclash/download_rule"
btnrm=tb:option(Button,"remove",translate("Remove"))
btnrm.render=function(e,t,a)
e.inputstyle="reset"
Button.render(e,t,a)
end
btnrm.write=function(a,t)
fs.unlink("/etc/openclash/rule_provider/"..e[t].lfilename)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-manage"))
end
return m, form

View File

@ -0,0 +1,286 @@
local m, s, o
local openclash = "openclash"
local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local UTIL = require "luci.util"
local fs = require "luci.openclash"
local uci = require "luci.model.uci".cursor()
m = Map(openclash, translate("Rule Providers Append"))
m.pageaction = false
m.description=translate("Attention:")..
"<br/>"..translate("The game proxy is a test function and does not guarantee the availability of rules")..
"<br/>"..translate("Preparation steps:")..
"<br/>"..translate("1. Check the policy group and node you are going to use, Policy group type suggestion: fallback, game nodes must be support UDP and not a Vmess")..
"<br/>"..translate("2. Click the <manage third party game rules> or <manage third party rule set> button to enter the rule list and download the rules you want to use")..
"<br/>"..translate("3. On this page, set the corresponding configuration file and policy group of the rule you have downloaded, and save the settings")..
"<br/>"..
"<br/>"..translate("When setting this page, if the groups is empty, please go to the <Onekey Create> page to add")..
"<br/>"..
"<br/>"..translate("Introduction to rule set usage: https://wiki.metacubex.one/config/rule-providers/content/")
function IsRuleFile(e)
e=e or""
local e=string.lower(string.sub(e,-6,-1))
return e==".rules"
end
function IsYamlFile(e)
e=e or""
local e=string.lower(string.sub(e,-5,-1))
return e == ".yaml"
end
function IsYmlFile(e)
e=e or""
local e=string.lower(string.sub(e,-4,-1))
return e == ".yml"
end
local groupnames, filename
local group_list = {}
filename = m.uci:get(openclash, "config", "config_path")
if filename then
groupnames = SYS.exec(string.format('ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "YAML.load_file(\'%s\')[\'proxy-groups\'].each do |i| puts i[\'name\']+\'##\' end" 2>/dev/null',filename))
if groupnames then
for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do
if groupname ~= nil and groupname ~= "" then
table.insert(group_list, groupname)
end
end
end
end
m.uci:foreach("openclash", "groups",
function(s)
if s.name ~= "" and s.name ~= nil then
table.insert(group_list, s.name)
end
end)
table.sort(group_list)
table.insert(group_list, "DIRECT")
table.insert(group_list, "REJECT")
table.insert(group_list, "REJECT-DROP")
table.insert(group_list, "PASS")
table.insert(group_list, "GLOBAL")
-- [[ Edit Game Rule ]] --
s = m:section(TypedSection, "game_config", translate("Game Rules Append"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "openclash/tblsection"
s.rmempty = false
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- config
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
---- rule name
o = s:option(DynamicList, "rule_name", translate("Game Rule's Name"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/game_rules/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].filename=fs.basename(f)
if IsRuleFile(e[t].filename) then
e[t].name=string.gsub(luci.sys.exec(string.format("grep ',%s$' /usr/share/openclash/res/game_rules.list |awk -F ',' '{print $1}' 2>/dev/null",e[t].filename)), "[\r\n]", "")
if e[t].name ~= "" and e[t].name ~= nil then
o:value(e[t].name)
end
end
end
end
o.rmempty = true
---- Proxy Group
o = s:option(ListValue, "group", translate("Select Proxy Group"))
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
-- [[ Edit Other Rule Provider ]] --
s = m:section(TypedSection, "rule_provider_config", translate("Other Rule Providers Append"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "openclash/tblsection"
s.rmempty = false
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
---- config
o = s:option(ListValue, "config", translate("Config File"))
o:value("all", translate("Use For All Config File"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].name=fs.basename(f)
if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then
o:value(e[t].name)
end
end
end
---- rule name
o = s:option(DynamicList, "rule_name", translate("Rule Provider's Name"))
local e,a={}
for t,f in ipairs(fs.glob("/etc/openclash/rule_provider/*"))do
a=fs.stat(f)
if a then
e[t]={}
e[t].filename=fs.basename(f)
if IsYamlFile(e[t].filename) or IsYmlFile(e[t].filename) then
e[t].name=string.gsub(luci.sys.exec(string.format("grep ',%s$' /usr/share/openclash/res/rule_providers.list |awk -F ',' '{print $1}' 2>/dev/null",e[t].filename)), "[\r\n]", "")
if e[t].name ~= "" and e[t].name ~= nil then
o:value(e[t].name)
end
end
end
end
o.rmempty = true
---- Proxy Group
o = s:option(ListValue, "group", translate("Select Proxy Group"))
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(Value, "interval", translate("Rule Providers Interval(s)"))
o.default = "86400"
o.rmempty = false
---- position
o = s:option(ListValue, "position", translate("Append Position"))
o.rmempty = false
o:value("0", translate("Priority Match"))
o:value("1", translate("Extended Match"))
-- [[ Edit Custom Rule Provider ]] --
s = m:section(TypedSection, "rule_providers", translate("Custom Rule Providers Append"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "openclash/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/rule-providers-config/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
return
end
end
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = s:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = s:option(DummyValue, "name", translate("Rule Providers Name"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
---- Proxy Group
o = s:option(ListValue, "group", translate("Select Proxy Group"))
o.rmempty = true
for _, groupname in ipairs(group_list) do
o:value(groupname)
end
o = s:option(ListValue, "position", translate("Append Position"))
o.rmempty = false
o:value("0", translate("Priority Match"))
o:value("1", translate("Extended Match"))
local rm = {
{rule_mg, pro_mg}
}
rmg = m:section(Table, rm)
o = rmg:option(Button, "rule_mg", " ")
o.inputtitle = translate("Game Rules Manage")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-manage"))
end
o = rmg:option(Button, "pro_mg", " ")
o.inputtitle = translate("Other Rule Provider Manage")
o.inputstyle = "reload"
o.write = function()
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-manage"))
end
local t = {
{Commit, Apply}
}
ss = m:section(Table, t)
o = ss:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
end
o = ss:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/toolbar_show"))
return m

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,281 @@
local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
font_red = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
m = Map(openclash, translate("Onekey Create (Servers&Groups manage)"))
m.pageaction = false
m.description=translate("Attention:")..
"<br/>"..translate("1. Before modifying the configuration file, please click the button below to read the configuration file")..
"<br/>"..translate("2. Proxy-providers address can be directly filled in the subscription link")..
"<br/>"..
"<br/>"..translate("Introduction to proxy usage: https://wiki.metacubex.one/config/proxies/")..
"<br/>"..translate("Introduction to proxy-provider usage: https://wiki.metacubex.one/config/proxy-providers/")
s = m:section(TypedSection, "openclash")
s.anonymous = true
o = s:option(Flag, "create_config", translate("Create Config"))
o.description = font_red .. bold_on .. translate("Create Config By One-Click Only Need Proxies") .. bold_off .. font_off
o.default = 0
o = s:option(ListValue, "rule_sources", translate("Choose Template For Create Config"))
o.description = translate("Use Other Rules To Create Config")
o:depends("create_config", 1)
o:value("lhie1", translate("lhie1 Rules"))
o = s:option(Flag, "mix_proxies", translate("Mix Proxies"))
o.description = font_red .. bold_on .. translate("Mix This Page's Proxies") .. bold_off .. font_off
o:depends("create_config", 1)
o.default = 0
o = s:option(Flag, "servers_update", translate("Keep Settings"))
o.description = font_red .. bold_on .. translate("Only Update Servers Below When Subscription") .. bold_off .. font_off
o.default = 0
o = s:option(DynamicList, "new_servers_group", translate("New Servers Group (Support Regex)"))
o.description = translate("Set The New Subscribe Server's Default Proxy Groups")
o.rmempty = true
o:depends("servers_update", 1)
o:value("all", translate("All Groups"))
m.uci:foreach("openclash", "groups",
function(s)
o:value(s.name)
end)
-- [[ Groups Manage ]]--
s = m:section(TypedSection, "groups", translate("Proxy Groups(No Need Set when Config Create)"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "openclash/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/groups-config/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
return
end
end
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = s:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = s:option(DummyValue, "type", translate("Group Type"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "name", translate("Group Name"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
-- [[ Proxy-Provider Manage ]]--
s = m:section(TypedSection, "proxy-provider", translate("Proxy-Provider"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "openclash/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/proxy-provider-config/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
return
end
end
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = s:option(Flag, "manual", translate("Custom Tag"))
o.rmempty = false
o.default = "0"
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "0"
end
o = s:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = s:option(DummyValue, "type", translate("Provider Type"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "name", translate("Provider Name"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
-- [[ Servers Manage ]]--
s = m:section(TypedSection, "servers", translate("Proxies"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "openclash/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/servers-config/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
return
end
end
---- enable flag
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = o.enabled
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "1"
end
o = s:option(Flag, "manual", translate("Custom Tag"))
o.rmempty = false
o.default = "0"
o.cfgvalue = function(...)
return Flag.cfgvalue(...) or "0"
end
o = s:option(DummyValue, "config", translate("Config File"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("all")
end
o = s:option(DummyValue, "type", translate("Type"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "name", translate("Server Alias"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "server", translate("Server Address"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "port", translate("Server Port"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "udp", translate("UDP Support"))
function o.cfgvalue(...)
if Value.cfgvalue(...) == "true" then
return translate("Enable")
elseif Value.cfgvalue(...) == "false" then
return translate("Disable")
else
return translate("None")
end
end
local tt = {
{Delete_Unused_Servers, Delete_Servers, Delete_Proxy_Provider, Delete_Groups}
}
b = m:section(Table, tt)
o = b:option(Button,"Delete_Unused_Servers", " ")
o.inputtitle = translate("Delete Unused Servers")
o.inputstyle = "reset"
o.write = function()
m.uci:set("openclash", "config", "enable", 0)
m.uci:commit("openclash")
luci.sys.call("sh /usr/share/openclash/cfg_unused_servers_del.sh 2>/dev/null")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "servers"))
end
o = b:option(Button,"Delete_Servers", " ")
o.inputtitle = translate("Delete Servers")
o.inputstyle = "reset"
o.write = function()
m.uci:set("openclash", "config", "enable", 0)
m.uci:delete_all("openclash", "servers", function(s) return true end)
m.uci:commit("openclash")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "servers"))
end
o = b:option(Button,"Delete_Proxy_Provider", " ")
o.inputtitle = translate("Delete Proxy Providers")
o.inputstyle = "reset"
o.write = function()
m.uci:set("openclash", "config", "enable", 0)
m.uci:delete_all("openclash", "proxy-provider", function(s) return true end)
m.uci:commit("openclash")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "servers"))
end
o = b:option(Button,"Delete_Groups", " ")
o.inputtitle = translate("Delete Groups")
o.inputstyle = "reset"
o.write = function()
m.uci:set("openclash", "config", "enable", 0)
m.uci:delete_all("openclash", "groups", function(s) return true end)
m.uci:commit("openclash")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "servers"))
end
local t = {
{Load_Config, Commit, Apply}
}
a = m:section(Table, t)
o = a:option(Button,"Load_Config", " ")
o.inputtitle = translate("Read Config")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
luci.sys.call("/usr/share/openclash/yml_groups_get.sh 2>/dev/null &")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash"))
end
o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
fs.unlink("/tmp/Proxy_Group")
m.uci:commit("openclash")
end
o = a:option(Button, "Apply", " ")
o.inputtitle = translate("Apply Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
fs.unlink("/tmp/Proxy_Group")
luci.sys.call("/usr/share/openclash/yml_groups_set.sh >/dev/null 2>&1 &")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash"))
end
m:append(Template("openclash/toolbar_show"))
return m

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,289 @@
--[[
LuCI - Filesystem tools
Description:
A module offering often needed filesystem manipulation functions
FileId:
$Id$
License:
Copyright 2008 Steven Barth <steven@midlink.org>
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.
]]--
local io = require "io"
local os = require "os"
local ltn12 = require "luci.ltn12"
local fs = require "nixio.fs"
local nutil = require "nixio.util"
local uci = require "luci.model.uci".cursor()
local SYS = require "luci.sys"
local type = type
local string = string
--- LuCI filesystem library.
module "luci.openclash"
--- Test for file access permission on given path.
-- @class function
-- @name access
-- @param str String value containing the path
-- @return Number containing the return code, 0 on sucess or nil on error
-- @return String containing the error description (if any)
-- @return Number containing the os specific errno (if any)
access = fs.access
--- Evaluate given shell glob pattern and return a table containing all matching
-- file and directory entries.
-- @class function
-- @name glob
-- @param filename String containing the path of the file to read
-- @return Table containing file and directory entries or nil if no matches
-- @return String containing the error description (if no matches)
-- @return Number containing the os specific errno (if no matches)
function glob(...)
local iter, code, msg = fs.glob(...)
if iter then
return nutil.consume(iter)
else
return nil, code, msg
end
end
--- Checks wheather the given path exists and points to a regular file.
-- @param filename String containing the path of the file to test
-- @return Boolean indicating wheather given path points to regular file
function isfile(filename)
return fs.stat(filename, "type") == "reg"
end
--- Checks wheather the given path exists and points to a directory.
-- @param dirname String containing the path of the directory to test
-- @return Boolean indicating wheather given path points to directory
function isdirectory(dirname)
return fs.stat(dirname, "type") == "dir"
end
--- Read the whole content of the given file into memory.
-- @param filename String containing the path of the file to read
-- @return String containing the file contents or nil on error
-- @return String containing the error message on error
readfile = fs.readfile
--- Write the contents of given string to given file.
-- @param filename String containing the path of the file to read
-- @param data String containing the data to write
-- @return Boolean containing true on success or nil on error
-- @return String containing the error message on error
writefile = fs.writefile
--- Copies a file.
-- @param source Source file
-- @param dest Destination
-- @return Boolean containing true on success or nil on error
copy = fs.datacopy
--- Renames a file.
-- @param source Source file
-- @param dest Destination
-- @return Boolean containing true on success or nil on error
rename = fs.move
--- Get the last modification time of given file path in Unix epoch format.
-- @param path String containing the path of the file or directory to read
-- @return Number containing the epoch time or nil on error
-- @return String containing the error description (if any)
-- @return Number containing the os specific errno (if any)
function mtime(path)
return fs.stat(path, "mtime")
end
--- Set the last modification time of given file path in Unix epoch format.
-- @param path String containing the path of the file or directory to read
-- @param mtime Last modification timestamp
-- @param atime Last accessed timestamp
-- @return 0 in case of success nil on error
-- @return String containing the error description (if any)
-- @return Number containing the os specific errno (if any)
function utime(path, mtime, atime)
return fs.utimes(path, atime, mtime)
end
--- Return the last element - usually the filename - from the given path with
-- the directory component stripped.
-- @class function
-- @name basename
-- @param path String containing the path to strip
-- @return String containing the base name of given path
-- @see dirname
basename = fs.basename
--- Return the directory component of the given path with the last element
-- stripped of.
-- @class function
-- @name dirname
-- @param path String containing the path to strip
-- @return String containing the directory component of given path
-- @see basename
dirname = fs.dirname
--- Return a table containing all entries of the specified directory.
-- @class function
-- @name dir
-- @param path String containing the path of the directory to scan
-- @return Table containing file and directory entries or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function dir(...)
local iter, code, msg = fs.dir(...)
if iter then
local t = nutil.consume(iter)
t[#t+1] = "."
t[#t+1] = ".."
return t
else
return nil, code, msg
end
end
--- Create a new directory, recursively on demand.
-- @param path String with the name or path of the directory to create
-- @param recursive Create multiple directory levels (optional, default is true)
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function mkdir(path, recursive)
return recursive and fs.mkdirr(path) or fs.mkdir(path)
end
--- Remove the given empty directory.
-- @class function
-- @name rmdir
-- @param path String containing the path of the directory to remove
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
rmdir = fs.rmdir
local stat_tr = {
reg = "regular",
dir = "directory",
lnk = "link",
chr = "character device",
blk = "block device",
fifo = "fifo",
sock = "socket"
}
--- Get information about given file or directory.
-- @class function
-- @name stat
-- @param path String containing the path of the directory to query
-- @return Table containing file or directory properties or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function stat(path, key)
local data, code, msg = fs.stat(path)
if data then
data.mode = data.modestr
data.type = stat_tr[data.type] or "?"
end
return key and data and data[key] or data, code, msg
end
--- Set permissions on given file or directory.
-- @class function
-- @name chmod
-- @param path String containing the path of the directory
-- @param perm String containing the permissions to set ([ugoa][+-][rwx])
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
chmod = fs.chmod
--- Create a hard- or symlink from given file (or directory) to specified target
-- file (or directory) path.
-- @class function
-- @name link
-- @param path1 String containing the source path to link
-- @param path2 String containing the destination path for the link
-- @param symlink Boolean indicating wheather to create a symlink (optional)
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
function link(src, dest, sym)
return sym and fs.symlink(src, dest) or fs.link(src, dest)
end
--- Remove the given file.
-- @class function
-- @name unlink
-- @param path String containing the path of the file to remove
-- @return Number with the return code, 0 on sucess or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
unlink = fs.unlink
--- Retrieve target of given symlink.
-- @class function
-- @name readlink
-- @param path String containing the path of the symlink to read
-- @return String containing the link target or nil on error
-- @return String containing the error description on error
-- @return Number containing the os specific errno on error
readlink = fs.readlink
function filename(str)
if not str then
return nil
end
local idx = str:match(".+()%.%w+$")
if idx then
return str:sub(1, idx-1)
else
return str
end
end
function filesize(e)
local t=0
local a={' KB',' MB',' GB',' TB',' PB'}
if e < 0 then
e = -e
end
repeat
e=e/1024
t=t+1
until(e<=1024)
return string.format("%.1f",e)..a[t] or "0.0 KB"
end
function lanip()
local lan_int_name = uci:get("openclash", "config", "lan_interface_name") or "0"
local lan_ip
if lan_int_name == "0" then
lan_ip = SYS.exec("uci -q get network.lan.ipaddr 2>/dev/null |awk -F '/' '{print $1}' 2>/dev/null |tr -d '\n'")
else
lan_ip = SYS.exec(string.format("ip address show %s 2>/dev/null | grep -w 'inet' 2>/dev/null | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -1 | tr -d '\n'", lan_int_name))
end
if not lan_ip or lan_ip == "" then
lan_ip = SYS.exec("ip address show $(uci -q -p /tmp/state get network.lan.ifname || uci -q -p /tmp/state get network.lan.device) | grep -w 'inet' 2>/dev/null | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -1 | tr -d '\n'")
end
if not lan_ip or lan_ip == "" then
lan_ip = SYS.exec("ip addr show 2>/dev/null | grep -w 'inet' | grep 'global' | grep 'brd' | grep -Eo 'inet [0-9\.]+' | awk '{print $2}' | head -n 1 | tr -d '\n'")
end
return lan_ip
end

View File

@ -0,0 +1,18 @@
<%+cbi/valueheader%>
<div>
<%
local val = self:cfgvalue(section)
if val == translate("Enable") or val == translate("Enabled") or val == translate("Config Normal") or val == translate("Exist") then
%>
<div style="color: green; font-weight:bold;">
<%
else
%>
<div style="color: red; font-weight:bold;">
<%
end
write(pcdata(val))
%>
</div>
</div>
<%+cbi/valuefooter%>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,498 @@
<style>
.CodeMirror {
text-align: left !important;
font-size: 15px;
line-height: 150%;
resize: both !important;
}
.CodeMirror-merge {
border: none !important;
}
.CodeMirror-merge-r-chunk {
background: #0095ff2e !important;
}
.CodeMirror-merge-2pane .CodeMirror-merge-gap {
height: 700px !important;
}
.CodeMirror-merge-gap {
background: #d0cfcf !important;
}
.CodeMirror-merge-r-connect {
fill: #0095ff2e !important;
stroke: #0095ff2e !important;
}
.CodeMirror-vscrollbar-oc {
display: none !important;
}
.CodeMirror, .CodeMirror-line {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
box-sizing: border-box;
word-break: break-all !important;
overflow-wrap: break-word !important;
}
</style>
<link rel="stylesheet" href="/luci-static/resources/openclash/lib/codemirror.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/material.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/material-log.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/theme/idea.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/fold/foldgutter.css"/>
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/lint/lint.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/display/fullscreen.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/dialog/dialog.css">
<link rel="stylesheet" href="/luci-static/resources/openclash/addon/search/matchesonscrollbar.css">
<script src="/luci-static/resources/openclash/lib/codemirror.js"></script>
<script src="/luci-static/resources/openclash/mode/yaml/yaml.js"></script>
<script src="/luci-static/resources/openclash/mode/lua/lua.js"></script>
<script src="/luci-static/resources/openclash/mode/shell/shell.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/foldcode.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/foldgutter.js"></script>
<script src="/luci-static/resources/openclash/addon/fold/indent-fold.js"></script>
<script src="/luci-static/resources/openclash/addon/edit/matchbrackets.js"></script>
<script src="/luci-static/resources/openclash/addon/selection/active-line.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/lint.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/yaml-lint.js"></script>
<script src="/luci-static/resources/openclash/addon/lint/js-yaml.min.js"></script>
<script src="/luci-static/resources/openclash/addon/display/fullscreen.js"></script>
<script src="/luci-static/resources/openclash/addon/display/autorefresh.js"></script>
<script src="/luci-static/resources/openclash/addon/dialog/dialog.js"></script>
<script src="/luci-static/resources/openclash/addon/search/searchcursor.js"></script>
<script src="/luci-static/resources/openclash/addon/search/search.js"></script>
<script src="/luci-static/resources/openclash/addon/scroll/annotatescrollbar.js"></script>
<script src="/luci-static/resources/openclash/addon/search/matchesonscrollbar.js"></script>
<script src="/luci-static/resources/openclash/addon/search/jump-to-line.js"></script>
<script src="/luci-static/resources/openclash/addon/merge/diff_match_patch.js"></script>
<script src="/luci-static/resources/openclash/addon/merge/merge.js"></script>
<link rel=stylesheet href="/luci-static/resources/openclash/addon/merge/merge.css">
<%-
local uci = require("luci.model.uci").cursor()
local fs = require "luci.openclash"
local conf = uci:get("openclash", "config", "config_path")
if not conf then conf = "/etc/openclash/config/config.yaml" end
local conf_name = fs.basename(conf)
if not conf_name then conf_name = "config.yaml" end
local sconf = "/etc/openclash/"..conf_name
-%>
<table id="my_editor_div" style="width: 100%; text-align: center; display: none;">
<tr>
<td style="width: 50%; overflow: hidden;"><%:Modify Your Config file:%> <b style=color:green><%=conf_name%></b> <%:Here, Except The Settings That Were Taken Over%></td>
<%-
if fs.isfile(sconf) then
-%>
<td style="width: 50%; overflow: hidden;"><%:Config File Edited By OpenClash For Running%></td>
<%-
else
-%>
<td style="width: 50%; overflow: hidden;"><%:Default Config File With Correct Template%></td>
<%-
end
-%>
</tr>
</table>
<script type="text/javascript">//<![CDATA[
function isDarkBackground(element) {
var style = window.getComputedStyle(element);
var bgColor = style.backgroundColor;
let r, g, b;
if (/rgb\(/.test(bgColor)) {
var rgb = bgColor.match(/\d+/g);
r = parseInt(rgb);
g = parseInt(rgb);
b = parseInt(rgb);
} else if (/#/.test(bgColor)) {
if (bgColor.length === 4) {
r = parseInt(bgColor + bgColor, 16);
g = parseInt(bgColor + bgColor, 16);
b = parseInt(bgColor + bgColor, 16);
} else {
r = parseInt(bgColor.slice(1, 3), 16);
g = parseInt(bgColor.slice(3, 5), 16);
b = parseInt(bgColor.slice(5, 7), 16);
}
} else {
return false;
}
var luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
return luminance < 128;
};
function merge_editor(id, id2, target, target2, readOnly, readOnly2, wid, height)
{
var value, orig1, orig2, merge_editor, descr, gap, vscrollbar, vscrollbar_oc, panes = 2, highlight = true, connect = null, collapse = false;
descr = document.getElementById("my_editor_div");
target.parentNode.parentNode.parentNode.insertBefore(descr, target.parentNode.parentNode);
descr.style.display="inline-table"
value = id.value;
if (value == null) {
editor(id, readOnly, wid, height);
editor(id2, readOnly2, wid, height);
}
else {
target2.parentNode.style.display="none";
orig1 = '';
orig2 = id2.value;
target.style.height= "700px"
id.style.display = "none";
id2.style.display = "none";
merge_editor = CodeMirror.MergeView(target, {
mode: "text/yaml",
value: value,
origLeft: panes == 3 ? orig1 : null,
orig: orig2,
theme: "material",
lineNumbers: true,
autoRefresh: true,
styleActiveLine: true,
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
lint: true,
highlightDifferences: highlight,
connect: connect,
collapseIdentical: collapse,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
extraKeys: {
"F10": function() {
merge_editor.setShowDifferences(highlight = !highlight);
},
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
gap = document.getElementsByClassName("CodeMirror-merge-gap")[0];
vscrollbar = document.getElementsByClassName("CodeMirror-vscrollbar")[0];
vscrollbar_oc = document.getElementsByClassName("CodeMirror-vscrollbar-oc")[0];
if (cm.getOption("fullScreen")) {
console.log(gap.style.display)
gap.style.display = "none";
vscrollbar.className = "CodeMirror-vscrollbar-oc";
}
else {
gap.style.display = "";
vscrollbar_oc.className = "CodeMirror-vscrollbar";
};
},
"Esc": function(cm) {
gap = document.getElementsByClassName("CodeMirror-merge-gap")[0];
vscrollbar = document.getElementsByClassName("CodeMirror-vscrollbar")[0];
vscrollbar_oc = document.getElementsByClassName("CodeMirror-vscrollbar-oc")[0];
if (cm.getOption("fullScreen")) {
cm.setOption("fullScreen", false);
gap.style.display = "";
vscrollbar_oc.className = "CodeMirror-vscrollbar";
};
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
cm.replaceSelection(spaces)
}
}
}
});
merge_editor.edit.on("change", function(cm) {
id.value = cm.getValue();
});
if (wid && height) {
merge_editor.editor().setSize(wid, height);
merge_editor.rightOriginal().setSize(wid, height);
};
};
};
function editor(id, readOnly, wid, height)
{
var editor = CodeMirror.fromTextArea(id, {
mode: "text/yaml",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "material",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
lint: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
cm.replaceSelection(spaces)
}
}
}
});
if (readOnly == "true") {
editor.setOption("readOnly","true");
};
if (wid && height) {
editor.setSize(wid, height);
};
};
function shell_editor(id, readOnly, wid, height)
{
var editor = CodeMirror.fromTextArea(id, {
mode: "shell",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "material",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
cm.replaceSelection(spaces)
}
}
}
});
if (wid && height) {
editor.setSize(wid, height);
};
};
function other_editor(id, readOnly, wid, height)
{
var editor = CodeMirror.fromTextArea(id, {
mode: "lua",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "material",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
},
"Tab": function(cm) {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
var spaces = Array(cm.getOption("indentUnit") + 1).join(" ")
cm.replaceSelection(spaces)
}
}
}
});
if (wid && height) {
editor.setSize(wid, height);
};
};
function other_log_area(id, readOnly, wid, height)
{
var other_log_area = CodeMirror.fromTextArea(id, {
mode: "lua",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "idea",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
}
}
});
if (readOnly == "true") {
other_log_area.setOption("readOnly","true");
};
if (wid && height) {
other_log_area.setSize(wid, height);
};
other_log_area.markText({line:0,ch:0},{line:9999,ch:9999}, {css: 'font-size:13px'});
};
var custom_cfg_value = document.getElementById("cbid.openclash." + window.location.pathname.split("/")[window.location.pathname.split("/").length - 1] + ".other_parameters");
if (custom_cfg_value) {
editor(custom_cfg_value, 'false', '100%', '300px');
};
var custom_firewall = document.getElementById("cbid.openclash.config.firewall_custom");
var custom_ymchange = document.getElementById("cbid.openclash.config.ymchange_custom");
if (custom_firewall) {
shell_editor(custom_firewall, 'false', "100%", '300px');
};
if (custom_ymchange) {
shell_editor(custom_ymchange, 'false', "100%", '300px');
};
var myEditor_use = document.getElementById("cbid.table.1.user");
var myEditor_def = document.getElementById("cbid.table.1.default");
if (myEditor_use && myEditor_def) {
var myEditor_div_use = document.getElementById("cbi-table-1-user");
var myEditor_div_def = document.getElementById("cbi-table-1-default");
myEditor_div_def.parentNode.parentNode.style.cssText="text-align: left !important"
myEditor_div_def.parentNode.className='cbi-value';
myEditor_div_def.parentNode.style.display="revert"
myEditor_div_def.parentNode.style.width="100%"
myEditor_div_use.parentNode.className='cbi-value';
myEditor_div_use.parentNode.style.display="revert"
myEditor_div_use.parentNode.style.width="100%"
merge_editor(myEditor_use, myEditor_def, myEditor_div_use, myEditor_div_def, 'true', 'false', 'auto', '700px');
};
var myEditor_hosts = document.getElementById("cbid.openclash.config.custom_hosts");
var myEditor_fall_fil = document.getElementById("cbid.openclash.config.custom_fallback_fil");
var myEditor_name_pol = document.getElementById("cbid.openclash.config.custom_domain_dns_core");
var myEditor_name_cus_r1 = document.getElementById("cbid.openclash.config.custom_rules_2");
var myEditor_name_cus_r2 = document.getElementById("cbid.openclash.config.custom_rules");
var myEditor_fake_filter = document.getElementById("cbid.openclash.config.custom_fake_filter");
var myEditor_custom_domain_dns = document.getElementById("cbid.openclash.config.custom_domain_dns");
var myEditor_edit_file = document.getElementById("cbid.openclash.config.edit_file");
var myEditor_custom_sniffer = document.getElementById("cbid.openclash.config.sniffer_custom");
if (myEditor_hosts) {
editor(myEditor_hosts, 'false', '100%', '300px');
};
if (myEditor_edit_file) {
editor(myEditor_edit_file, 'false', '100%', '700px');
};
if (myEditor_fall_fil) {
editor(myEditor_fall_fil, 'false', '100%', '300px');
};
if (myEditor_name_pol) {
editor(myEditor_name_pol, 'false', '100%', '300px');
};
if (myEditor_name_cus_r1) {
editor(myEditor_name_cus_r1, 'false', '100%', '300px');
editor(myEditor_name_cus_r2, 'false', '100%', '300px');
};
if (myEditor_fake_filter) {
other_editor(myEditor_fake_filter, 'false', '100%', '300px');
};
if (myEditor_custom_domain_dns) {
other_editor(myEditor_custom_domain_dns, 'false', '100%', '300px');
};
if (myEditor_custom_sniffer) {
other_editor(myEditor_custom_sniffer, 'false', '100%', '300px');
};
var core_log = document.getElementById("core_log");
var oc_log = document.getElementById("cbid.openclash.config.clog");
if (core_log && oc_log) {
var core_editor = CodeMirror.fromTextArea(core_log, {
mode: "lua",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "idea",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
}
}
});
var oc_editor = CodeMirror.fromTextArea(oc_log, {
mode: "lua",
autoRefresh: true,
styleActiveLine: true,
lineNumbers: true,
theme: "idea",
lineWrapping: true,
matchBrackets: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
extraKeys: {
"F11": function(cm) {
cm.setOption("fullScreen", !cm.getOption("fullScreen"));
},
"Esc": function(cm) {
if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false);
}
}
});
core_editor.setSize("100%", "540px");
core_editor.setOption("readOnly","true");
oc_editor.setSize("100%", "540px");
oc_editor.setOption("readOnly","true");
if (isDarkBackground(document.body)) {
core_editor.setOption('theme', 'material-log');
oc_editor.setOption('theme', 'material-log');
};
};
var proxy_mg = document.getElementById('cbi-table-1-proxy_mg');
var rule_mg = document.getElementById('cbi-table-1-rule_mg');
var game_mg = document.getElementById('cbi-table-1-game_mg');
var Commit = document.getElementById('cbi-table-1-Commit');
var Apply = document.getElementById('cbi-table-1-Apply');
var Create = document.getElementById('cbi-table-1-Create');
if (proxy_mg) {
proxy_mg.style.textAlign="center";
rule_mg.style.textAlign="center";
game_mg.style.textAlign="center";
Commit.style.textAlign="center";
Apply.style.textAlign="center";
Create.style.textAlign="center";
};
//]]>
</script>

Some files were not shown because too many files have changed in this diff Show More