🏅 Sync 2026-06-22 01:15:34

This commit is contained in:
github-actions[bot] committed 2026-06-22 01:15:34 +08:00
1 parent 325683c378
commit 5ab6870564
41 files changed
+3203 -1407

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=dae
PKG_VERSION:=2026.06.14
PKG_RELEASE:=10
PKG_RELEASE:=11
PKG_SOURCE:=dae-src-2026.06.14-fa89ea7197a5.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
+13
View File
@@ -46,6 +46,19 @@ start_service() {
stop_service() {
rm -rf "$LOG_DIR"
# kill lingering procs in daens: TERM first, then KILL for stubborn ones
for pid in $(ip netns pids daens 2>/dev/null); do kill "$pid" 2>/dev/null; done
if [ -n "$(ip netns pids daens 2>/dev/null)" ]; then
sleep 1
for pid in $(ip netns pids daens 2>/dev/null); do kill -9 "$pid" 2>/dev/null; done
fi
# cleanup daens netns; ip-full needed for netns, busybox fallback via umount
if ! ip netns del daens 2>/dev/null; then
umount /run/netns/daens 2>/dev/null
rm -f /run/netns/daens
fi
# clean orphaned dae0
ip link del dae0 2>/dev/null || true
}
service_triggers() {
+1 -1
View File
@@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=daed
PKG_VERSION:=2026.06.14
PKG_RELEASE:=11
PKG_RELEASE:=12
PKG_SOURCE:=daed-src-2026.06.14-4e215048068a.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
+13 -3
View File
@@ -40,9 +40,19 @@ start_service() {
stop_service() {
rm -f "$LOG"
# daed creates /run/netns/daens on start; a stale entry makes the next
# start fail with "failed to create netns: ... file exists". Clean it here.
ip netns del daens 2>/dev/null || true
# kill lingering procs in daens: TERM first, then KILL for stubborn ones
for pid in $(ip netns pids daens 2>/dev/null); do kill "$pid" 2>/dev/null; done
if [ -n "$(ip netns pids daens 2>/dev/null)" ]; then
sleep 1
for pid in $(ip netns pids daens 2>/dev/null); do kill -9 "$pid" 2>/dev/null; done
fi
# cleanup daens netns; ip-full needed for netns, busybox fallback via umount
if ! ip netns del daens 2>/dev/null; then
umount /run/netns/daens 2>/dev/null
rm -f /run/netns/daens
fi
# clean orphaned dae0
ip link del dae0 2>/dev/null || true
}
service_triggers() {
+25 -22
View File
@@ -2,38 +2,40 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=gecoosac
PKG_VERSION:=2.2.20251015
PKG_RELEASE:=12
PKG_RELEASE:=13
ifeq ($(ARCH),aarch64)
PKG_MAINTAINER:=lwb1978 <lwb1978@gmail.com>
PKG_LICENSE:=AGPL-3.0-only
PKG_LICENSE_FILES:=LICENSE
ARCH_ID:=$(strip $(if $(ARCH_PACKAGES),$(ARCH_PACKAGES),$(ARCH)))
ifneq (,$(filter x86_64,$(ARCH_ID)))
PKG_ARCH:=ac_linux_amd64
PKG_HASH:=skip
else ifneq (,$(filter aarch64%,$(ARCH_ID)))
PKG_ARCH:=ac_linux_arm64
PKG_HASH:=skip
else ifeq ($(ARCH),arm)
else ifneq (,$(filter arm%,$(ARCH_ID)))
PKG_ARCH:=ac_linux_arm
PKG_HASH:=skip
else ifeq ($(ARCH),mips)
PKG_ARCH:=ac_linux_mips
PKG_HASH:=skip
else ifeq ($(ARCH),mipsel)
else ifneq (,$(filter mipsel%,$(ARCH_ID)))
PKG_ARCH:=ac_linux_mipsle
PKG_HASH:=skip
else ifeq ($(ARCH),i386)
PKG_ARCH:=ac_linux_386
else ifneq (,$(filter mips%,$(ARCH_ID)))
PKG_ARCH:=ac_linux_mips
PKG_HASH:=skip
else ifeq ($(ARCH),x86_64)
PKG_ARCH:=ac_linux_amd64
else ifneq (,$(filter i386%,$(ARCH_ID)))
PKG_ARCH:=ac_linux_386
PKG_HASH:=skip
else
PKG_HASH:=skip
endif
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(ARCH_PACKAGES)
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(ARCH_ID)
PKG_SOURCE_URL:=https://github.com/laipeng668/luci-app-gecoosac/releases/download/V$(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:=lwb1978 <lwb1978@gmail.com>
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
@@ -59,14 +61,15 @@ endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/$(PKG_NAME) $(1)/usr/bin
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/etc/init.d/gecoosac $(1)/etc/init.d/gecoosac
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/gecoosac $(1)/etc/config/gecoosac
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) ./files/etc/uci-defaults/gecoosac $(1)/etc/uci-defaults/gecoosac
$(INSTALL_DIR) $(1)/etc/gecoosac/tls
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) $(PKG_BUILD_DIR)/$(PKG_NAME) $(1)/usr/bin
$(INSTALL_BIN) ./files/etc/init.d/gecoosac $(1)/etc/init.d/gecoosac
$(INSTALL_BIN) ./files/etc/uci-defaults/gecoosac $(1)/etc/uci-defaults/gecoosac
$(INSTALL_CONF) ./files/etc/config/gecoosac $(1)/etc/config/gecoosac
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
+8 -3
View File
@@ -318,6 +318,8 @@ ensure_dirs() {
}
start_service() {
local runtime_upload_dir runtime_db_dir runtime_piddir
init_conf
normalize_conf
@@ -333,6 +335,9 @@ start_service() {
fi
ensure_dirs || return 1
runtime_upload_dir="${upload_dir%/}/"
runtime_db_dir="${db_dir%/}/"
runtime_piddir="${piddir%/}/"
if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then
if [ "$crt_file" = "$DEFAULT_CRT_FILE" ] && [ "$key_file" = "$DEFAULT_KEY_FILE" ]; then
@@ -352,13 +357,13 @@ start_service() {
procd_open_instance gecoosac
procd_set_param command "$PROG"
procd_append_param command -f "$upload_dir"
procd_append_param command -f "$runtime_upload_dir"
procd_append_param command -p "$port"
procd_append_param command -debug "$debug"
procd_append_param command -dbpath "$db_dir"
procd_append_param command -dbpath "$runtime_db_dir"
procd_append_param command -token "1"
procd_append_param command -lang "$lang"
procd_append_param command -piddir "$piddir"
procd_append_param command -piddir "$runtime_piddir"
if [ "$isonlyoneprot" = "0" ]; then
procd_append_param command -isonlyoneprot "0"
procd_append_param command -mp "$m_port"
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede
PKG_VERSION:=1.14.1
PKG_RELEASE:=20
PKG_VERSION:=1.14.6
PKG_RELEASE:=21
PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
@@ -2,6 +2,7 @@
'use strict';
'require uci';
'require network';
'require view';
'require view.daede.backend as backend';
'require view.daede.styles as styles';
@@ -13,7 +14,13 @@ return view.extend({
_loadContext: function() {
return backend.detectBackend().then(function(ctx) {
return uci.load(ctx.backend.uci).catch(function() {}).then(function() {
return ctx;
/* enumerate L3 devices so the dae LAN/WAN fields offer a dropdown
(still free-text, so undetectable WANs can be typed) */
return network.getDevices().catch(function() { return []; }).then(function(devs) {
ctx.netDevs = (devs || []).map(function(d) { return d.getName(); })
.filter(function(n) { return n && n !== 'dae0'; }).sort();
return ctx;
});
});
});
},
@@ -327,6 +327,23 @@ function renderDaeForms(ctx) {
/* Logging — folded into the main form so the single Save button covers it
too (no separate native save bar) */
/* Network interfaces — override dae auto WAN detect, fails on legacy swconfig (#15).
Combobox: dropdown of real devices but still free-text for undetectable WANs */
const netDevs = (ctx && ctx.netDevs) || [];
s = m.section(form.NamedSection, 'config', 'dae', _('Network interfaces'));
s.addremove = false;
o = s.option(form.Value, 'lan_interface', _('LAN interface'),
_('Interface to proxy the LAN from. Usually br-lan.'));
o.default = 'br-lan';
o.placeholder = 'br-lan';
netDevs.forEach(function(d) { o.value(d); });
o = s.option(form.Value, 'wan_interface', _('WAN interface'),
_('Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set it, e.g. eth0.2.'));
o.default = 'auto';
o.placeholder = 'auto';
o.value('auto', 'auto');
netDevs.forEach(function(d) { o.value(d); });
s = m.section(form.NamedSection, 'config', 'dae', _('Logging'));
s.addremove = false;
o = s.option(form.Value, 'log_maxsize', _('Max Log Size (MB)'),
@@ -33,7 +33,7 @@ function renderDaedSettings() {
_('daede Settings'),
null,
m.render(),
_('Log Advanced Settings'),
_('Log Settings'),
['log_maxsize', 'log_maxbackups']
).then(function(card) {
/* native Save/Apply footer is suppressed view-wide, so daed carries its
@@ -72,7 +72,7 @@ const CSS = [
'.dd-settings-card .cbi-section-remove .cbi-button{border-color:#d96d6d !important;color:#d96d6d !important}',
/* themes (esp. Argon) restyle checkboxes into faint, near-invisible boxes;
force native rendering + brand accent so the checkmark is always clear */
'.dd-settings-card input[type="checkbox"]{-webkit-appearance:checkbox !important;appearance:checkbox !important;accent-color:#4aa065;width:16px;height:16px;background:none !important;border:0 !important;box-shadow:none !important;background-image:none !important}',
'.dd-settings-card input[type="checkbox"]{-webkit-appearance:checkbox !important;appearance:checkbox !important;width:16px;height:16px;background:none !important;border:0 !important;box-shadow:none !important;background-image:none !important}',
'.dd-settings-card input[type="checkbox"]:before,.dd-settings-card input[type="checkbox"]:after{content:none !important;display:none !important}',
/* line the Subscriptions and Nodes grid tables up column-for-column. fixed
layout + explicit per-column percentages make the widths authoritative
@@ -210,6 +210,7 @@ const CSS = [
'.dd-conv-import,.dd-conv-toolbar,.dd-conv-heading{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
'.dd-converter input,.dd-converter select,.dd-converter .cbi-button{box-sizing:border-box!important;height:32px!important;min-height:32px!important;margin:0!important;font-size:12px!important;line-height:22px!important}',
'.dd-converter input,.dd-converter select{padding:4px 8px!important;border:1px solid rgba(128,128,128,.28)!important;border-radius:6px!important;background-color:transparent;color:inherit;box-shadow:none!important}',
'.dd-converter input::placeholder,.dd-conv-yaml::placeholder,.dd-conv-filter::placeholder,.dd-conv-airport-name::placeholder{color:inherit;opacity:.55}',
'.dd-converter select{padding-right:28px!important}',
'.dd-converter .cbi-button{padding:4px 12px!important}',
'.dd-conv-url-row input,.dd-conv-filter,.dd-conv-target,.dd-conv-ua,.dd-conv-yaml{box-sizing:border-box;border:1px solid rgba(128,128,128,.28);border-radius:6px;background:transparent;color:inherit;font-size:12px;padding:7px 9px}',
@@ -55,9 +55,11 @@ const CSS = [
'.dd-geo-row{display:grid;grid-template-columns:96px 1fr;gap:10px;align-items:center;font-size:12px;padding:6px 0}',
'.dd-geo-row label{opacity:.75;font-weight:600}',
'.dd-geo-row input[type=text],.dd-geo-row select{font-size:12px;padding:4px 8px;border:1px solid rgba(128,128,128,.35);border-radius:5px;background:transparent;color:inherit;width:100%}',
/* fixed width so the Source and Auto-update selects line up consistently */
'.dd-geo-row select{width:180px;flex:0 0 auto}',
'.dd-geo-chk{display:flex;align-items:center;gap:6px}',
/* selects cap at 200px on wide screens, shrink to the column on mobile (no overflow) */
'.dd-geo-row select{width:100%!important;max-width:200px;box-sizing:border-box}',
'.dd-geo-auto{display:inline-flex;align-items:center;gap:6px}',
/* Argon shifts label>checkbox down (top:.4rem); reset so flex centers it */
'.dd-geo-auto input[type="checkbox"]{position:static;top:auto;right:auto;margin:0}',
'.dd-geo-actions{margin-top:10px;display:flex;gap:10px;align-items:center}',
'.dd-up-log{margin-top:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;padding:10px;border:1px solid rgba(128,128,128,.14);border-radius:8px;max-height:200px;overflow:auto;white-space:pre-wrap;word-break:break-all;display:none;background:inherit;color:#4a8c63}',
'.dd-up-log.show{display:block}',
@@ -473,8 +475,8 @@ return view.extend({
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, _('Source')), presetSel ]),
customRows,
E('div', { 'class': 'dd-geo-row' }, [
E('label', {}, _('Auto-update')),
E('div', { 'class': 'dd-geo-chk' }, [ autoCb, freqSel ])
E('label', { 'class': 'dd-geo-auto' }, [ E('span', {}, _('Auto-update')), autoCb ]),
freqSel
]),
E('div', { 'class': 'dd-geo-actions' }, [ saveBtn ])
])
+20 -2
View File
@@ -117,8 +117,8 @@ msgstr "保留旧日志数"
msgid "Number of rotated log files to keep."
msgstr "轮转后保留的旧日志文件数。"
msgid "Log Advanced Settings"
msgstr "日志高级设置"
msgid "Log Settings"
msgstr "日志设置"
# ---- config.js: dae Settings ----
msgid "dae Parameters"
@@ -1014,3 +1014,21 @@ msgid ""
"%s is running. Deleting the daens netns now will break its networking until "
"you restart it. Continue?"
msgstr "%s 正在运行。现在删除 daens netns 会中断其网络,直到重启它。确定继续?"
# ---- dae.js: Network interfaces ----
msgid "Network interfaces"
msgstr "网络接口"
msgid "LAN interface"
msgstr "LAN 接口"
msgid "WAN interface"
msgstr "WAN 接口"
msgid "Interface to proxy the LAN from. Usually br-lan."
msgstr "要绑定的 LAN 接口,通常是 br-lan。"
msgid ""
"Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set "
"it, e.g. eth0.2."
msgstr "上行 WAN 接口。auto 按默认路由自动检测;老式 swconfig 交换机或多 WAN 时手动指定,例如 eth0.2。"
+20 -2
View File
@@ -117,8 +117,8 @@ msgstr "保留旧日志数"
msgid "Number of rotated log files to keep."
msgstr "轮转后保留的旧日志文件数。"
msgid "Log Advanced Settings"
msgstr "日志高级设置"
msgid "Log Settings"
msgstr "日志设置"
# ---- config.js: dae Settings ----
msgid "dae Parameters"
@@ -1014,3 +1014,21 @@ msgid ""
"%s is running. Deleting the daens netns now will break its networking until "
"you restart it. Continue?"
msgstr "%s 正在运行。现在删除 daens netns 会中断其网络,直到重启它。确定继续?"
# ---- dae.js: Network interfaces ----
msgid "Network interfaces"
msgstr "网络接口"
msgid "LAN interface"
msgstr "LAN 接口"
msgid "WAN interface"
msgstr "WAN 接口"
msgid "Interface to proxy the LAN from. Usually br-lan."
msgstr "要绑定的 LAN 接口,通常是 br-lan。"
msgid ""
"Uplink. auto = detect by default route; on legacy swconfig or multi-WAN set "
"it, e.g. eth0.2."
msgstr "上行 WAN 接口。auto 按默认路由自动检测;老式 swconfig 交换机或多 WAN 时手动指定,例如 eth0.2。"
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.6.2
PKG_RELEASE:=162
PKG_RELEASE:=163
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -905,7 +905,7 @@ add_firewall_rule() {
#echolog "使用缓存加载chnroute..."
nft -f $RULES_PATH/chnroute.nft
else
cat $RULES_PATH/chnroute | tr -s '\n' | grep -v "^#" | gen_nftset $NFTSET_CHN ipv4_addr "2d" 0
cat $RULES_PATH/chnroute | tr -s '\n' | sed 's/#.*//' | gen_nftset $NFTSET_CHN ipv4_addr "2d" 0
fi
gen_nftset $NFTSET_BLACK ipv4_addr "2d" 0
gen_nftset $NFTSET_WHITE ipv4_addr "2d" 0
@@ -921,7 +921,7 @@ add_firewall_rule() {
#echolog "使用缓存加载chnroute6..."
nft -f $RULES_PATH/chnroute6.nft
else
cat $RULES_PATH/chnroute6 | tr -s '\n' | grep -v "^#" | gen_nftset $NFTSET_CHN6 ipv6_addr "2d" 0
cat $RULES_PATH/chnroute6 | tr -s '\n' | sed 's/#.*//' | gen_nftset $NFTSET_CHN6 ipv6_addr "2d" 0
fi
gen_nftset $NFTSET_BLACK6 ipv6_addr "2d" 0
gen_nftset $NFTSET_WHITE6 ipv6_addr "2d" 0
@@ -957,8 +957,8 @@ add_firewall_rule() {
#直连列表
[ "$USE_DIRECT_LIST_ALL" = "1" ] && {
cat $RULES_PATH/direct_ip | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_WHITE "0"
cat $RULES_PATH/direct_ip | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_WHITE6 "0"
cat $RULES_PATH/direct_ip | sed 's/#.*//' | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_WHITE "0"
cat $RULES_PATH/direct_ip | sed 's/#.*//' | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_WHITE6 "0"
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/direct_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ]; then
@@ -971,8 +971,8 @@ add_firewall_rule() {
#代理列表
[ "$USE_PROXY_LIST_ALL" = "1" ] && {
cat $RULES_PATH/proxy_ip | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_BLACK "0"
cat $RULES_PATH/proxy_ip | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLACK6 "0"
cat $RULES_PATH/proxy_ip | sed 's/#.*//' | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_BLACK "0"
cat $RULES_PATH/proxy_ip | sed 's/#.*//' | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLACK6 "0"
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/proxy_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ]; then
@@ -985,8 +985,8 @@ add_firewall_rule() {
#屏蔽列表
[ "$USE_BLOCK_LIST_ALL" = "1" ] && {
cat $RULES_PATH/block_ip | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_BLOCK "0"
cat $RULES_PATH/block_ip | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLOCK6 "0"
cat $RULES_PATH/block_ip | sed 's/#.*//' | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_BLOCK "0"
cat $RULES_PATH/block_ip | sed 's/#.*//' | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLOCK6 "0"
[ "$USE_GEOVIEW" = "1" ] && {
local GEOIP_CODE=$(cat $RULES_PATH/block_ip | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
if [ -n "$GEOIP_CODE" ]; then
@@ -1002,8 +1002,8 @@ add_firewall_rule() {
local GEOIP_CODE=""
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
for shunt_id in $shunt_ids; do
config_n_get $shunt_id ip_list | grep -v "^#" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT "0"
config_n_get $shunt_id ip_list | grep -v "^#" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6 "0"
config_n_get $shunt_id ip_list | sed 's/#.*//' | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT "0"
config_n_get $shunt_id ip_list | sed 's/#.*//' | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6 "0"
[ "$USE_GEOVIEW" = "1" ] && {
local geoip_code=$(config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | sed -e "/^$/d" | grep -E "^geoip:" | grep -v "^geoip:private" | sed -E 's/^geoip:(.*)/\1/' | sed ':a;N;$!ba;s/\n/,/g')
[ -n "$geoip_code" ] && GEOIP_CODE="${GEOIP_CODE:+$GEOIP_CODE,}$geoip_code"
+9 -3
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-run
PKG_VERSION:=1.0.0
PKG_RELEASE:=3
PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for running makeself .run installers
LUCI_DESCRIPTION:=Upload and execute makeself-generated .run installers from LuCI.
@@ -11,8 +11,14 @@ LUCI_PKGARCH:=all
PKG_MAINTAINER:=wukongdaily <2666180@gmail.com>
# 定义版本号用于缓存刷新
define Package/luci-app-run/postinst
#!/bin/sh
# 设置版本号到临时文件,用于前端资源缓存刷新
echo "$(PKG_VERSION)-$(PKG_RELEASE)" > /tmp/luci-app-run.version
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
# call BuildPackage - OpenWrt buildroot signature
@@ -101,6 +101,11 @@ var getStatus = rpc.declare({
method: 'status'
});
var getVersion = rpc.declare({
object: 'luci-app-run',
method: 'version'
});
var readLog = rpc.declare({
object: 'luci-app-run',
method: 'read_log',
@@ -140,10 +145,29 @@ return view.extend({
logOffset: 0,
currentUploadId: null,
appVersion: 'unknown',
load: function () {
return getStatus().catch(function () {
return {};
var self = this;
return getVersion().then(function (res) {
if (res && res.version) {
self.appVersion = res.version;
// 检查版本是否变化,如果变化则强制刷新页面
var storedVersion = localStorage.getItem('luci-app-run-version');
if (storedVersion && storedVersion !== self.appVersion) {
localStorage.setItem('luci-app-run-version', self.appVersion);
window.location.reload(true);
} else {
localStorage.setItem('luci-app-run-version', self.appVersion);
}
}
return getStatus().catch(function () {
return {};
});
}).catch(function () {
return getStatus().catch(function () {
return {};
});
});
},
@@ -180,7 +204,7 @@ return view.extend({
var runButton = E('button', {
class: 'btn cbi-button cbi-button-action',
disabled: true,
style: 'min-width:140px;padding:8px 32px;',
style: 'min-width:140px;margin-left:15px;',
click: function (ev) {
ev.preventDefault();
self.startRun(runButton, state);
@@ -227,7 +251,7 @@ return view.extend({
}, [
E('h3', [_('upload_title')]),
E('p', [state]),
E('p', [pickButton, ' ', runButton, cleanButton]),
E('p', [pickButton, runButton, cleanButton]),
progress,
fileInput
]);
+257 -246
View File
@@ -13,83 +13,83 @@ umask 077
mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
reply() {
printf '%s\n' "$1"
printf '%s\n' "$1"
}
json_error() {
local msg="$1"
json_init
json_add_int code 1
json_add_string error "$msg"
json_dump
local msg="$1"
json_init
json_add_int code 1
json_add_string error "$msg"
json_dump
}
json_ok() {
json_init
json_add_int code 0
[ -n "$1" ] && json_add_string message "$1"
json_dump
json_init
json_add_int code 0
[ -n "$1" ] && json_add_string message "$1"
json_dump
}
sanitize_name() {
local name="$1"
name="${name##*/}"
name="${name##*\\}"
printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_'
local name="$1"
name="${name##*/}"
name="${name##*\\}"
printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_'
}
valid_id() {
case "$1" in
""|*[!A-Za-z0-9_-]*) return 1 ;;
esac
return 0
case "$1" in
""|*[!A-Za-z0-9_-]*) return 1 ;;
esac
return 0
}
load_input() {
local input
input="$(cat)"
json_load "$input" >/dev/null 2>&1
local input
input="$(cat)"
json_load "$input" >/dev/null 2>&1
}
get_string() {
json_get_var "$1" "$1"
json_get_var "$1" "$1"
}
get_int() {
json_get_var "$1" "$1"
json_get_var "$1" "$1"
}
new_token() {
local token
token="$(hexdump -n 16 -e '16/1 "%02x"' /dev/urandom 2>/dev/null)"
[ -n "$token" ] || token="$(date +%s)_$$_$(awk 'BEGIN{srand();print int(rand()*1000000)}')"
printf '%s' "$token" | tr -c 'A-Za-z0-9_' '_'
local token
token="$(hexdump -n 16 -e '16/1 "%02x"' /dev/urandom 2>/dev/null)"
[ -n "$token" ] || token="$(date +%s)_$$_$(awk 'BEGIN{srand();print int(rand()*1000000)}')"
printf '%s' "$token" | tr -c 'A-Za-z0-9_' '_'
}
session_dir() {
printf '%s/%s' "$UPLOAD_DIR" "$1"
printf '%s/%s' "$UPLOAD_DIR" "$1"
}
write_state() {
local pid="$1"
local file="$2"
local log="$3"
local started="$4"
{
printf 'PID=%s\n' "$pid"
printf 'FILE=%s\n' "$file"
printf 'LOG=%s\n' "$log"
printf 'STARTED=%s\n' "$started"
} > "$STATE_FILE"
local pid="$1"
local file="$2"
local log="$3"
local started="$4"
{
printf 'PID=%s\n' "$pid"
printf 'FILE=%s\n' "$file"
printf 'LOG=%s\n' "$log"
printf 'STARTED=%s\n' "$started"
} > "$STATE_FILE"
}
read_state() {
[ -f "$STATE_FILE" ] && . "$STATE_FILE"
[ -f "$STATE_FILE" ] && . "$STATE_FILE"
}
is_running() {
local pid="$1"
[ -n "$pid" ] && [ -d "/proc/$pid" ]
local pid="$1"
[ -n "$pid" ] && [ -d "/proc/$pid" ]
}
method_list() {
@@ -114,235 +114,246 @@ method_list() {
"read_log": {
"offset": "Integer"
},
"cleanup": {}
"cleanup": {},
"version": {}
}
JSON
}
upload_start() {
load_input
get_string filename
get_int size
[ -n "$filename" ] || { json_error "Missing filename"; return; }
[ -n "$size" ] || size=0
[ "$size" -le "$MAX_SIZE" ] 2>/dev/null || { json_error "File is larger than 256 MiB"; return; }
local clean id token dir file
clean="$(sanitize_name "$filename")"
case "$clean" in
*.run|*.sh) ;;
*) json_error "Only .run and .sh files are accepted"; return ;;
esac
id="$(date +%s)_$$"
token="$(new_token)"
dir="$(session_dir "$id")"
file="$dir/$clean"
mkdir -p "$dir" || { json_error "Unable to create upload directory"; return; }
: > "$file" || { json_error "Unable to create upload file"; return; }
printf '%s\n' "$clean" > "$dir/name"
printf '%s\n' "$size" > "$dir/size"
printf '%s\n' "$token" > "$dir/token"
json_init
json_add_int code 0
json_add_string id "$id"
json_add_string token "$token"
json_add_string filename "$clean"
json_dump
load_input
get_string filename
get_int size
[ -n "$filename" ] || { json_error "Missing filename"; return; }
[ -n "$size" ] || size=0
[ "$size" -le "$MAX_SIZE" ] 2>/dev/null || { json_error "File is larger than 256 MiB"; return; }
local clean id token dir file
clean="$(sanitize_name "$filename")"
case "$clean" in
*.run|*.sh) ;;
*) json_error "Only .run and .sh files are accepted"; return ;;
esac
id="$(date +%s)_$$"
token="$(new_token)"
dir="$(session_dir "$id")"
file="$dir/$clean"
mkdir -p "$dir" || { json_error "Unable to create upload directory"; return; }
: > "$file" || { json_error "Unable to create upload file"; return; }
printf '%s\n' "$clean" > "$dir/name"
printf '%s\n' "$size" > "$dir/size"
printf '%s\n' "$token" > "$dir/token"
json_init
json_add_int code 0
json_add_string id "$id"
json_add_string token "$token"
json_add_string filename "$clean"
json_dump
}
upload_chunk() {
load_input
get_string id
get_string data
get_int index
valid_id "$id" || { json_error "Invalid upload id"; return; }
[ -n "$data" ] || { json_error "Missing chunk data"; return; }
local dir name file
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
file="$dir/$name"
printf '%s' "$data" | base64 -d >> "$file" 2>/dev/null || {
json_error "Unable to decode upload chunk"
return
}
json_init
json_add_int code 0
json_add_int index "$index"
json_add_int received "$(wc -c < "$file" 2>/dev/null)"
json_dump
load_input
get_string id
get_string data
get_int index
valid_id "$id" || { json_error "Invalid upload id"; return; }
[ -n "$data" ] || { json_error "Missing chunk data"; return; }
local dir name file
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
file="$dir/$name"
printf '%s' "$data" | base64 -d >> "$file" 2>/dev/null || {
json_error "Unable to decode upload chunk"
return
}
json_init
json_add_int code 0
json_add_int index "$index"
json_add_int received "$(wc -c < "$file" 2>/dev/null)"
json_dump
}
upload_finish() {
load_input
get_string id
valid_id "$id" || { json_error "Invalid upload id"; return; }
local dir name file expected actual
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
expected="$(cat "$dir/size" 2>/dev/null)"
file="$dir/$name"
actual="$(wc -c < "$file" 2>/dev/null)"
[ -s "$file" ] || { json_error "Uploaded file is empty"; return; }
if [ -n "$expected" ] && [ "$expected" -gt 0 ] 2>/dev/null; then
[ "$actual" -eq "$expected" ] 2>/dev/null || {
json_error "Upload size mismatch"
return
}
fi
chmod 700 "$file" || { json_error "Unable to chmod uploaded file"; return; }
json_init
json_add_int code 0
json_add_string id "$id"
json_add_string filename "$name"
json_add_int size "$actual"
json_dump
load_input
get_string id
valid_id "$id" || { json_error "Invalid upload id"; return; }
local dir name file expected actual
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
expected="$(cat "$dir/size" 2>/dev/null)"
file="$dir/$name"
actual="$(wc -c < "$file" 2>/dev/null)"
[ -s "$file" ] || { json_error "Uploaded file is empty"; return; }
if [ -n "$expected" ] && [ "$expected" -gt 0 ] 2>/dev/null; then
[ "$actual" -eq "$expected" ] 2>/dev/null || {
json_error "Upload size mismatch"
return
}
fi
chmod 700 "$file" || { json_error "Unable to chmod uploaded file"; return; }
json_init
json_add_int code 0
json_add_string id "$id"
json_add_string filename "$name"
json_add_int size "$actual"
json_dump
}
run_installer() {
load_input
get_string id
valid_id "$id" || { json_error "Invalid upload id"; return; }
local dir name file log work pid now
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
file="$dir/$name"
# 确保文件可执行(.run 和 .sh 都需要)
chmod 700 "$file" 2>/dev/null || true
[ -x "$file" ] || {
json_error "Uploaded file is not executable. Please check file permissions."
return
}
read_state
if is_running "$PID"; then
json_error "Another installer is already running"
return
fi
now="$(date '+%Y-%m-%d %H:%M:%S')"
work="$RUN_DIR/$id"
log="$work/output.log"
mkdir -p "$work" || { json_error "Unable to create run directory"; return; }
(
cd "$work" || exit 1
printf 'luci-app-run: started %s\n' "$now"
printf 'luci-app-run: executing %s\n\n' "$file"
"$file"
rc=$?
printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
exit "$rc"
) > "$log" 2>&1 &
pid="$!"
write_state "$pid" "$file" "$log" "$now"
json_init
json_add_int code 0
json_add_int pid "$pid"
json_add_string log "$log"
json_dump
load_input
get_string id
valid_id "$id" || { json_error "Invalid upload id"; return; }
local dir name file log work pid now
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
file="$dir/$name"
# 确保文件可执行(.run 和 .sh 都需要)
chmod 700 "$file" 2>/dev/null || true
[ -x "$file" ] || {
json_error "Uploaded file is not executable. Please check file permissions."
return
}
read_state
if is_running "$PID"; then
json_error "Another installer is already running"
return
fi
now="$(date '+%Y-%m-%d %H:%M:%S')"
work="$RUN_DIR/$id"
log="$work/output.log"
mkdir -p "$work" || { json_error "Unable to create run directory"; return; }
(
cd "$work" || exit 1
printf 'luci-app-run: started %s\n' "$now"
printf 'luci-app-run: executing %s\n\n' "$file"
"$file"
rc=$?
printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
exit "$rc"
) > "$log" 2>&1 &
pid="$!"
write_state "$pid" "$file" "$log" "$now"
json_init
json_add_int code 0
json_add_int pid "$pid"
json_add_string log "$log"
json_dump
}
status() {
read_state
json_init
json_add_int code 0
if is_running "$PID"; then
json_add_boolean running 1
else
json_add_boolean running 0
fi
[ -n "$PID" ] && json_add_int pid "$PID"
[ -n "$FILE" ] && json_add_string file "$FILE"
[ -n "$LOG" ] && json_add_string log "$LOG"
[ -n "$STARTED" ] && json_add_string started "$STARTED"
json_dump
read_state
json_init
json_add_int code 0
if is_running "$PID"; then
json_add_boolean running 1
else
json_add_boolean running 0
fi
[ -n "$PID" ] && json_add_int pid "$PID"
[ -n "$FILE" ] && json_add_string file "$FILE"
[ -n "$LOG" ] && json_add_string log "$LOG"
[ -n "$STARTED" ] && json_add_string started "$STARTED"
json_dump
}
read_log() {
load_input
get_int offset
[ -n "$offset" ] || offset=0
read_state
[ -n "$LOG" ] && [ -f "$LOG" ] || {
json_init
json_add_int code 0
json_add_string data ""
json_add_int offset 0
json_add_boolean running 0
json_dump
return
}
local size data start
size="$(wc -c < "$LOG" 2>/dev/null)"
[ "$offset" -le "$size" ] 2>/dev/null || offset=0
start=$((offset + 1))
data="$(tail -c +"$start" "$LOG" 2>/dev/null)"
json_init
json_add_int code 0
json_add_string data "$data"
json_add_int offset "$size"
if is_running "$PID"; then
json_add_boolean running 1
else
json_add_boolean running 0
fi
json_dump
load_input
get_int offset
[ -n "$offset" ] || offset=0
read_state
[ -n "$LOG" ] && [ -f "$LOG" ] || {
json_init
json_add_int code 0
json_add_string data ""
json_add_int offset 0
json_add_boolean running 0
json_dump
return
}
local size data start
size="$(wc -c < "$LOG" 2>/dev/null)"
[ "$offset" -le "$size" ] 2>/dev/null || offset=0
start=$((offset + 1))
data="$(tail -c +"$start" "$LOG" 2>/dev/null)"
json_init
json_add_int code 0
json_add_string data "$data"
json_add_int offset "$size"
if is_running "$PID"; then
json_add_boolean running 1
else
json_add_boolean running 0
fi
json_dump
}
cleanup() {
read_state
if is_running "$PID"; then
json_error "Cannot clean up while installer is running"
return
fi
read_state
if is_running "$PID"; then
json_error "Cannot clean up while installer is running"
return
fi
rm -rf "$UPLOAD_DIR" "$RUN_DIR" "$STATE_FILE"
mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
json_ok "Cleaned"
}
rm -rf "$UPLOAD_DIR" "$RUN_DIR" "$STATE_FILE"
mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
json_ok "Cleaned"
version() {
local ver="unknown"
[ -f "/tmp/luci-app-run.version" ] && ver="$(cat /tmp/luci-app-run.version)"
json_init
json_add_int code 0
json_add_string version "$ver"
json_dump
}
case "$1" in
list)
method_list
;;
call)
case "$2" in
upload_start) upload_start ;;
upload_chunk) upload_chunk ;;
upload_finish) upload_finish ;;
run) run_installer ;;
status) status ;;
read_log) read_log ;;
cleanup) cleanup ;;
*) json_error "Unknown method" ;;
esac
;;
*)
reply '{}'
;;
esac
list)
method_list
;;
call)
case "$2" in
upload_start) upload_start ;;
upload_chunk) upload_chunk ;;
upload_finish) upload_finish ;;
run) run_installer ;;
status) status ;;
read_log) read_log ;;
cleanup) cleanup ;;
version) version ;;
*) json_error "Unknown method" ;;
esac
;;
*)
reply '{}'
;;
esac
+7 -7
View File
@@ -3,8 +3,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=193
PKG_RELEASE:=27
PKG_VERSION:=195
PKG_RELEASE:=28
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -34,15 +34,16 @@ LUCI_DEPENDS:= \
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
+resolveip +curl +nping +unzip +xz-utils \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:coreutils-timeout \
+PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy:3proxy \
+PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo:mihomo \
+PACKAGE_$(PKG_NAME)_INCLUDE_GeoData:v2ray-geoip \
+PACKAGE_$(PKG_NAME)_INCLUDE_GeoData:v2ray-geosite \
+PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG:chinadns-ng \
+PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS:mosdns \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS:shadow-tls \
+PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun:kcptun-client \
+PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy:naiveproxy \
+PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy:3proxy \
+PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo:mihomo \
+PACKAGE_$(PKG_NAME)_INCLUDE_GeoData:v2ray-geoip \
+PACKAGE_$(PKG_NAME)_INCLUDE_GeoData:v2ray-geosite \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client:shadowsocks-rust-sslocal \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server:shadowsocks-rust-ssserver \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs:simple-obfs-client \
@@ -104,7 +105,6 @@ endchoice
choice
prompt "V2ray-core Selection"
default PACKAGE_$(PKG_NAME)_INCLUDE_Xray
default PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY
config PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY
@@ -661,6 +661,8 @@ function index()
entry({"admin", "services", "shadowsocksr", "clash_client_policies"}, call("clash_client_policies")).leaf = true
entry({"admin", "services", "shadowsocksr", "clash_client_rule_save"}, call("clash_client_rule_save")).leaf = true
entry({"admin", "services", "shadowsocksr", "clash_client_rule_clear"}, call("clash_client_rule_clear")).leaf = true
entry({"admin", "services", "shadowsocksr", "fetch_certsha256"}, call("fetch_certsha256")).leaf = true
entry({"admin", "services", "shadowsocksr", "fetch_certbyname"}, call("fetch_certbyname")).leaf = true
--[[Backup]]
entry({"admin", "services", "shadowsocksr", "backup"}, call("create_backup")).leaf = true
entry({'admin', 'services', "shadowsocksr", 'ip'}, call('check_ip')) -- 获取ip情况
@@ -873,6 +875,13 @@ function add_subscribe_item()
return
end
local commit_subscribe = luci.sys.call("uci -q commit shadowsocksr >/dev/null 2>&1")
if commit_subscribe ~= 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 0, error = "commit failed" })
return
end
luci.http.prepare_content("application/json")
luci.http.write_json({ ret = 1, sid = sid, alias = alias, enabled = "1" })
end
@@ -1320,6 +1329,84 @@ function clash_client_rule_clear()
})
end
function fetch_certsha256()
local function fetch_cert_sha256(host, port, sni, timeout)
if not host then return "" end
port = tonumber(port) or 443
sni = sni or host
timeout = tonumber(timeout) or 5
local cmd = string.format(
"timeout %d openssl s_client -connect %s:%d -servername %s -showcerts </dev/null 2>/dev/null " ..
"| awk 'BEGIN{c=0}/BEGIN CERT/{c++} c==1{print} /END CERT/{if(c==1)exit}' " ..
"| openssl x509 -outform der 2>/dev/null " ..
"| sha256sum 2>/dev/null",
timeout, host, port, sni
)
local out = trim(luci.sys.exec(cmd))
local fp = out:match("^([0-9a-fA-F]+)")
if not fp or fp:lower():match("^e3b0c44298fc1c149afbf4c8996fb924") then
return ""
end
return fp:upper()
end
local sid = luci.http.formvalue("sid") or ""
local address = (sid ~= "") and uci:get("shadowsocksr", sid, "server") or ""
local port_raw = (sid ~= "") and uci:get("shadowsocksr", sid, "server_port") or ""
local port = tonumber(port_raw) or 0
local sni = (id ~= "") and uci:get("shadowsocksr", sid, "tls_host") or ""
sni = (sni and sni ~= "") and sni or address
if address == "" or port == 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ code = 0, msg = "Address or Port is invalid" })
return
end
local data = fetch_cert_sha256(address, port, sni, 5)
luci.http.prepare_content("application/json")
luci.http.write_json(data ~= "" and { code = 1, data = data } or { code = 0 })
end
function fetch_certbyname()
local function fetch_cert_byname(host, port, sni, timeout)
if not host then return "" end
port = tonumber(port) or 443
sni = sni or host
timeout = tonumber(timeout) or 5
local cmd = string.format(
"timeout %d openssl s_client -connect %s:%d -servername %s -showcerts </dev/null 2>/dev/null " ..
"| openssl x509 -noout -subject 2>/dev/null " ..
"| awk '{gsub(/^.*=[[:space:]]*/, \"\"); gsub(/,.*$/, \"\"); print}'",
timeout, host, port, sni
)
local out = trim(luci.sys.exec(cmd))
if out == "" then
return ""
end
return out
end
local sid = luci.http.formvalue("sid") or ""
local address = (sid ~= "") and uci:get("shadowsocksr", sid, "server") or ""
local port_raw = (sid ~= "") and uci:get("shadowsocksr", sid, "server_port") or ""
local port = tonumber(port_raw) or 0
local sni = (id ~= "") and uci:get("shadowsocksr", sid, "tls_host") or ""
sni = (sni and sni ~= "") and sni or address
if address == "" or port == 0 then
luci.http.prepare_content("application/json")
luci.http.write_json({ code = 0, msg = "Address or Port is invalid" })
return
end
local data = fetch_cert_byname(address, port, sni, 5)
luci.http.prepare_content("application/json")
luci.http.write_json(data ~= "" and { code = 1, data = data } or { code = 0 })
end
function act_ping()
local e = {}
local domain = luci.http.formvalue("domain")
@@ -321,6 +321,10 @@ m.on_after_save = function(self)
end
end
local server_header = Template("/shadowsocksr/server_header")
server_header.section = sid
m:append(server_header)
-- [[ Servers Setting ]]--
s = m:section(NamedSection, sid, "servers")
s.anonymous = true
@@ -417,6 +421,9 @@ end
if is_finded("xray") then
o:value("hysteria2", translate("Hysteria2"))
end
if is_finded("mihomo") then
o:value("snell", translate("Snell"))
end
o:value("socks", translate("Socks"))
o:value("http", translate("HTTP"))
o:depends("type", "v2ray")
@@ -500,6 +507,11 @@ o:depends({type = "v2ray", v2ray_protocol = "socks", socks_ver = "5", auth_enabl
o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"})
o:depends({type = "v2ray", v2ray_protocol = "trojan"})
o = s:option(Value, "snell_psk", translate("Snell PSK"))
o.password = true
o.rmempty = true
o:depends({type = "v2ray", v2ray_protocol = "snell"})
o = s:option(ListValue, "encrypt_method", translate("Encrypt Method"))
for _, v in ipairs(encrypt_methods) do
o:value(v)
@@ -946,6 +958,28 @@ o.rmempty = true
o.default = "5"
o:depends({type = "v2ray", v2ray_protocol = "socks"})
o = s:option(ListValue, "snell_version", translate("Snell Version"))
o:value("1", "v1")
o:value("2", "v2")
o:value("3", "v3")
o:value("4", "v4")
o:value("5", "v5")
o.default = "4"
o.rmempty = true
o:depends({type = "v2ray", v2ray_protocol = "snell"})
o = s:option(ListValue, "snell_obfs", translate("Snell Obfs"))
o:value("", translate("Disable"))
o:value("http", "HTTP")
o:value("tls", "TLS")
o.default = ""
o.rmempty = true
o:depends({type = "v2ray", v2ray_protocol = "snell"})
o = s:option(Value, "snell_obfs_host", translate("Snell Obfs Host"))
o.rmempty = true
o:depends({type = "v2ray", v2ray_protocol = "snell"})
-- 传输协议
o = s:option(ListValue, "transport", translate("Transport"))
o:value("raw", "RAW (TCP)")
@@ -1510,14 +1544,18 @@ o.description = translate("If true, allowss insecure connection at TLS client, e
if xray_version_val >= 260131 then
-- Xray 版本大于等于 26.1.31
-- [[ Xray TLS pinSHA256 ]] --
o = s:option(Value, "tls_CertSha", translate("TLS Chain Fingerprint (SHA256)"), translate("Once set, connects only when the server’s chain fingerprint matches."))
o = s:option(Value, "tls_CertSha", translate("TLS Chain Fingerprint (SHA256)"))
o.rmempty = true
o:depends({type = "v2ray", tls = true})
o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") ..
string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
-- [[ Xray TLS verify leaf certificate name ]] --
o = s:option(Value, "tls_CertByName", translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o = s:option(Value, "tls_CertByName", translate("TLS Certificate Name (CertName)"))
o.rmempty = true
o:depends({type = "v2ray", tls = true})
o.description = translate("TLS is used to verify the leaf certificate name.") ..
string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertByName(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
end
-- [[ Hysteria2 TLS pinSHA256 ]] --
@@ -7,7 +7,7 @@ require "nixio.fs"
local m, s, o
local sid = arg[1]
-- 加密方式(SS/SSR用)
-- 加密方式(SS/SSR用)
local encrypt_methods = {
"rc4-md5", "rc4-md5-6", "rc4", "table",
"aes-128-cfb", "aes-192-cfb", "aes-256-cfb",
@@ -23,13 +23,13 @@ local encrypt_methods_ss = {
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
-- Shadowsocks 加密方法(Xray 版)
-- Shadowsocks 加密方法(Xray 版)
local v_ss_encrypt_method_list = {
"aes-128-cfb", "aes-256-cfb", "aes-128-gcm", "aes-256-gcm",
"chacha20", "chacha20-ietf", "chacha20-poly1305", "chacha20-ietf-poly1305"
}
-- 伪装类型(用于 mKCP/QUIC)
-- 伪装类型(用于 mKCP/QUIC)
local header_type_list = {
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard"
}
@@ -50,7 +50,7 @@ s = m:section(NamedSection, sid, "server_config")
s.anonymous = true
s.addremove = false
-- ========== 基本设置 ==========
-- ========== 基本设置 ==========
o = s:option(Flag, "enable", translate("Enable"))
o.default = 1
o.rmempty = false
@@ -63,12 +63,14 @@ end
if nixio.fs.access("/usr/bin/ssr-server") then
o:value("ssr", translate("ShadowsocksR"))
end
-- Xray 协议支持
if nixio.fs.access("/usr/bin/xray") or nixio.fs.access("/usr/libexec/xray") then
o:value("vmess", "VMess (Xray)")
o:value("vless", "VLESS (Xray)")
o:value("trojan", "Trojan (Xray)")
o:value("shadowsocks", "Shadowsocks (Xray)")
-- Xray 协议支持
if nixio.fs.access("/usr/bin/mihomo") or nixio.fs.access("/usr/libexec/mihomo")
or nixio.fs.access("/usr/bin/xray") or nixio.fs.access("/usr/libexec/xray")
then
o:value("vmess", "VMess (Mihomo/Xray)")
o:value("vless", "VLESS (Mihomo/Xray)")
o:value("trojan", "Trojan (Mihomo/Xray)")
o:value("shadowsocks", "Shadowsocks (Mihomo/Xray)")
end
o.default = "socks5"
@@ -99,7 +101,7 @@ o:depends("type", "ssr")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
-- ========== SS/SSR 协议特有 ==========
-- ========== SS/SSR 协议特有 ==========
o = s:option(ListValue, "encrypt_method", translate("Encrypt Method"))
for _, v in ipairs(encrypt_methods) do o:value(v) end
o.rmempty = false
@@ -128,8 +130,8 @@ o.rmempty = false
o:depends("type", "ss")
o:depends("type", "ssr")
-- ========== Xray 协议通用字段 ==========
-- 用户备注
-- ========== Xray 协议通用字段 ==========
-- 用户备注
o = s:option(Value, "remarks", translate("Remarks"))
o.default = translate("Remarks")
o.rmempty = true
@@ -138,7 +140,7 @@ o:depends("type", "vless")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
-- 用户等级
-- 用户等级
o = s:option(Value, "level", translate("User Level"))
o.datatype = "uinteger"
o.default = 1
@@ -148,21 +150,21 @@ o:depends("type", "vless")
o:depends("type", "shadowsocks")
o:depends("type", "trojan")
-- ========== Xray 协议认证字段 ==========
-- UUID (用于 vmess/vless)
-- ========== Xray 协议认证字段 ==========
-- UUID (用于 vmess/vless)
o = s:option(Value, "uuid", translate("UUID"))
o.description = translate("Required for VMess/VLESS. Generate with: uuidgen")
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
-- Trojan 密码
-- Trojan 密码
o = s:option(Value, "trojan_password", translate("Trojan Password"))
o.password = true
o.rmempty = true
o:depends("type", "trojan")
-- Shadowsocks 密码和加密(Xray 内置)
-- Shadowsocks 密码和加密(Xray 内置)
o = s:option(Value, "ss_password", translate("Shadowsocks Password"))
o.password = true
o.rmempty = true
@@ -195,7 +197,7 @@ o.default = "none"
o.rmempty = true
o:depends("type", "vless")
-- ========== TLS / XTLS 设置 ==========
-- ========== TLS / XTLS 设置 ==========
o = s:option(Flag, "tls", translate("TLS"))
o.default = 0
o.rmempty = true
@@ -234,7 +236,7 @@ o.description = translate("e.g.: /etc/ssl/private.key")
o.rmempty = true
o:depends("tls", "1")
-- ========== 传输层设置 (Transport) ==========
-- ========== 传输层设置 (Transport) ==========
o = s:option(ListValue, "transport", translate("Transport Protocol"))
o:value("tcp", "TCP")
o:value("mkcp", "mKCP")
@@ -248,7 +250,7 @@ o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
-- ----- WebSocket 设置 -----
-- ----- WebSocket 设置 -----
o = s:option(Value, "ws_host", translate("WebSocket Host"))
o.rmempty = true
o:depends("transport", "ws")
@@ -264,7 +266,7 @@ o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
-- ----- HTTP/2 设置 -----
-- ----- HTTP/2 设置 -----
o = s:option(Value, "h2_host", translate("HTTP/2 Host"))
o.rmempty = true
o:depends("transport", "h2")
@@ -278,7 +280,7 @@ o:depends("transport", "h2")
o:depends("type", "vmess")
o:depends("type", "vless")
-- ----- TCP 伪装设置 -----
-- ----- TCP 伪装设置 -----
o = s:option(ListValue, "tcp_guise", translate("TCP Camouflage Type"))
o:value("none", "none")
o:value("http", "http")
@@ -296,7 +298,7 @@ o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
o.rmempty = true
o:depends("tcp_guise", "http")
-- ----- mKCP 设置 -----
-- ----- mKCP 设置 -----
o = s:option(ListValue, "mkcp_guise", translate("mKCP Camouflage Type"))
for _, v in ipairs(header_type_list) do o:value(v) end
o.default = "none"
@@ -349,13 +351,13 @@ o.datatype = "uinteger"
o.rmempty = true
o:depends("transport", "mkcp")
-- ----- DomainSocket 设置 -----
-- ----- DomainSocket 设置 -----
o = s:option(Value, "ds_path", translate("DomainSocket Path"))
o.description = translate("A legal file path. This file must not exist before running.")
o.rmempty = true
o:depends("transport", "ds")
-- ----- QUIC 设置 -----
-- ----- QUIC 设置 -----
o = s:option(ListValue, "quic_security", translate("QUIC Security"))
o:value("none", "none")
o:value("aes-128-gcm", "aes-128-gcm")
@@ -374,7 +376,7 @@ o.default = "none"
o.rmempty = true
o:depends("transport", "quic")
-- ========== 访问控制 ==========
-- ========== 访问控制 ==========
o = s:option(Flag, "bind_local", translate("Bind Local Only"))
o.description = translate("When selected, it can only be accessed locally. Recommended when using reverse proxies.")
o.default = 0
@@ -458,6 +458,7 @@ o.remove = function() end
o = s:option(Value, "filter_words", translate("Subscribe Filter Words"))
o.rmempty = true
o.default = "过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国"
o.description = translate("Filter Words splited by /")
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
@@ -544,6 +545,26 @@ if has_mihomo then
o = s:option(DummyValue, "_upload_clash_yaml", translate("Upload Custom YAML File"))
o.template = "shadowsocksr/clash_yaml_upload"
o.description = translate("Upload a custom Clash/Mihomo YAML file. The file will be preprocessed and saved as a local Clash node.")
o = s:option(Flag, "sub_convert", translate("Subscribe Convert Online"))
o.description = translate("Convert subscriptions to Clash/Mihomo YAML with a subscription template URL.")
o.default = "0"
o.rmempty = false
o = s:option(Value, "convert_address", translate("Convert Address"))
o.default = "https://api.asailor.org/sub"
o.placeholder = "https://api.asailor.org/sub"
o:value("https://api.asailor.org/sub", "api.asailor.org")
o:value("https://api.wcc.best/sub", "api.wcc.best")
o.rmempty = true
o:depends("sub_convert", "1")
o = s:option(Value, "template_url", translate("Subscribe Template URL"))
o.default = "https://raw.githubusercontent.com/Fzlwhyc/OpenClash-Templates/main//fzlwhyc-openclash.ini"
o.placeholder = "https://raw.githubusercontent.com/Fzlwhyc/OpenClash-Templates/main//fzlwhyc-openclash.ini"
o:value("https://raw.githubusercontent.com/Fzlwhyc/OpenClash-Templates/main//fzlwhyc-openclash.ini", "Fzlwhyc")
o.rmempty = true
o:depends("sub_convert", "1")
end
s:append(cbi.Template("shadowsocksr/subscribe_schedule_compact"))
@@ -117,7 +117,7 @@ if global_type == "socks5" and Process_list:find("ipt2socks") then
if Process_list:find("%-U") or Process_list:find("%-%-udp%-only") then
reudp_run = 1
end
if global_socks_enabled and (global_socks_server == "same" and global_socks_server == global_server) then
if global_socks_enabled and (global_socks_server == "same" or global_socks_server == global_server) then
sock5_run = 1
end
end
@@ -125,7 +125,7 @@ end
if (global_type == "clash" or global_type == "tuic" or global_type == "ss") and Process_list:find("ssr%-retcp") then
redir_run = 1
reudp_run = 1
if global_socks_enabled and (global_socks_server == "same" and global_socks_server == global_server) then
if global_socks_enabled and (global_socks_server == "same" or global_socks_server == global_server) then
sock5_run = 1
end
end
@@ -133,7 +133,7 @@ end
if (global_type == "clash" or global_type == "tuic" or global_type == "ss") and Process_list:find("mihomo") and (Process_list:find("/clash%-") or Process_list:find("/tuic%-") or Process_list:find("/ss%-")) then
redir_run = 1
reudp_run = 1
if global_socks_enabled and (global_socks_server == "same" and global_socks_server == global_server) then
if global_socks_enabled and (global_socks_server == "same" or global_socks_server == global_server) then
sock5_run = 1
end
end
@@ -4,7 +4,7 @@ local dsp = require "luci.dispatcher"
<script type="text/javascript">
//<![CDATA[
function clearlog(btn) {
XHR.get('<%=dsp.build_url("admin/services/shadowsocksr/clear_log")%>', null,
XHR.get('<%=dsp.build_url("admin", "services", "shadowsocksr", "clear_log")%>', null,
function(x, data) {
if (x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
@@ -15,7 +15,7 @@ local dsp = require "luci.dispatcher"
);
}
XHR.poll(5, '<%=dsp.build_url("admin/services/shadowsocksr/get_log")%>', null,
XHR.poll(5, '<%=dsp.build_url("admin", "services", "shadowsocksr", "get_log")%>', null,
function(x, data) {
if (x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
@@ -0,0 +1,78 @@
<script type="text/javascript">
//<![CDATA[
let sid = "<%=self.section%>";
function fetchCertSha256(el) {
if (el.getAttribute("data-loading") === "1") return;
el.setAttribute("data-loading", "1");
el.style.pointerEvents = "none";
el.style.color = "red";
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "fetch_certsha256")%>',
{ sid: sid },
function(x, res) {
el.setAttribute("data-loading", "0");
el.style.pointerEvents = "";
el.style.color = "";
if (!x || x.status !== 200) {
alert("<%:Error%>");
return;
}
if (!res || res.code !== 1 || !res.data) {
alert(res && res.msg ? res.msg : "<%:Fetch Failed%> !");
return;
}
const inputs = document.querySelectorAll('input[id$=".tls_CertSha"]');
if (inputs.length > 0) {
inputs.forEach(function(input) {
input.value = res.data;
// 触发 change 事件,让 LuCI 检测到字段已修改
const event = new Event('change', { bubbles: true });
input.dispatchEvent(event);
});
// 成功提示(只有找到输入框时才提示)
alert(res && res.msg ? res.msg : "<%:Successfully retrieved certificate fingerprint:%> " + res.data);
} else {
// 未找到输入框时提示
alert("<%:Certificate fingerprint retrieved successfully, but no input field found!%>");
}
});
}
function fetchCertByName(el) {
if (el.getAttribute("data-loading") === "1") return;
el.setAttribute("data-loading", "1");
el.style.pointerEvents = "none";
el.style.color = "red";
XHR.get
('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "fetch_certbyname")%>',
{ sid: sid },
function(x, res) {
el.setAttribute("data-loading", "0");
el.style.pointerEvents = "";
el.style.color = "";
if (!x || x.status !== 200) {
alert("<%:Error%>");
return;
}
if (!res || res.code !== 1 || !res.data) {
alert(res && res.msg ? res.msg : "<%:Fetch Failed%> !");
return;
}
const inputs = document.querySelectorAll('input[id$=".tls_CertByName"]');
if (inputs.length > 0) {
inputs.forEach(function(input) {
input.value = res.data;
// 触发 change 事件,让 LuCI 检测到字段已修改
const event = new Event('change', { bubbles: true });
input.dispatchEvent(event);
});
// 成功提示(只有找到输入框时才提示)
alert(res && res.msg ? res.msg : "<%:Successfully retrieved certificate name:%> " + res.data);
} else {
// 未找到输入框时提示
alert("<%:Certificate name retrieved successfully, but no input field found!%>");
}
});
}
//]]>
</script>
@@ -165,7 +165,7 @@ end
return;
}
port.innerHTML = '<font style=\"color:#0072c3\">connect</font>';
XHR.get('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/ping")%>', {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "ping")%>', {
index,
sid: dom.getAttribute("data-sid"),
domain: dom.getAttribute("hint"),
@@ -280,7 +280,7 @@ end
var server_page = pageElem ? pageElem.value : '<%=self.server_page or 1%>';
var server_page_size = pageSizeElem ? pageSizeElem.value : '<%=self.server_page_size or 0%>';
if (input) input.value = order;
XHR.post('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/save_order")%>', {
XHR.post('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "save_order")%>', {
order: order,
page: server_page,
page_size: server_page_size
@@ -248,7 +248,7 @@ end
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>"
onclick="
var sid = '<%=k%>';
XHR.post('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/delete_node")%>', {
XHR.post('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "delete_node")%>', {
sid: sid
}, function(x, result) {
result = result || {};
@@ -3,7 +3,7 @@
local map = self.map
local has_ss_rust = luci.sys.exec('type -t -p sslocal 2>/dev/null || type -t -p ssserver 2>/dev/null') ~= ""
local has_mihomo = luci.sys.exec('type -t -p mihomo -p /usr/libexec/mihomo 2>/dev/null') ~= ""
local has_mihomo = luci.sys.exec('type -t -p mihomo 2>/dev/null || { test -x /usr/libexec/mihomo && echo /usr/libexec/mihomo; }') ~= ""
local has_xray = luci.sys.exec('type -t -p xray 2>/dev/null') ~= ""
-%>
<script type="text/javascript">
@@ -11,6 +11,9 @@ local has_xray = luci.sys.exec('type -t -p xray 2>/dev/null') ~= ""
let has_ss_rust = "<%=has_ss_rust%>"
let has_mihomo = "<%=has_mihomo%>"
let has_xray = "<%=has_xray%>"
function hasMihomoOrXray() {
return has_mihomo === "true" || has_xray === "true";
}
function padright(str, cnt, pad) {
return str + Array(cnt + 1).join(pad);
@@ -112,6 +115,31 @@ function import_ssr_url(btn, urlname, sid) {
var event = document.createEvent("HTMLEvents");
event.initEvent("change", true, true);
switch (ssu[0]) {
case "snell":
if (has_mihomo !== "true") {
s.innerHTML = "<font style=\'color:red\'><%:No available Mihomo core to import this Snell node.%></font>";
return false;
}
try {
var url = new URL("http://" + ssu[1]);
var params = url.searchParams;
} catch(e) {
alert(e);
return false;
}
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = "snell";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = normalizeHost(url.hostname);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.snell_psk')[0].value = decodeURIComponent(url.username || params.get("psk") || params.get("password") || "");
document.getElementsByName('cbid.shadowsocksr.' + sid + '.snell_version')[0].value = params.get("version") || params.get("v") || "4";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.snell_obfs')[0].value = params.get("obfs") || params.get("obfs-mode") || params.get("obfs_mode") || "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.snell_obfs_host')[0].value = params.get("obfs-host") || params.get("obfs_host") || params.get("host") || "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return false;
case "hysteria2":
case "hy2":
try {
@@ -122,8 +150,8 @@ function import_ssr_url(btn, urlname, sid) {
return false;
}
// Xray Hysteria2 导入函数
function importAsXrayHy2() {
// Mihomo/Xray Hysteria2 导入函数
function importAsV2RayHy2() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray"
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
@@ -152,11 +180,11 @@ function import_ssr_url(btn, urlname, sid) {
}
}
if (has_xray !== "true") {
s.innerHTML = "<font style=\'color:red\'><%:No available Xray core to import this Hysteria2 node.%></font>";
if (!hasMihomoOrXray()) {
s.innerHTML = "<font style=\'color:red\'><%:No available Mihomo or Xray core to import this Hysteria2 node.%></font>";
return false;
}
importAsXrayHy2();
importAsV2RayHy2();
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = normalizeHost(url.hostname);
@@ -535,19 +563,19 @@ function import_ssr_url(btn, urlname, sid) {
}
let finalSSBackend;
const finalXray = (params.security && params.security !== "none") || // TLS/XTLS/Reality
const finalV2Ray = (params.security && params.security !== "none") || // TLS/XTLS/Reality
(params.type && ["ws", "httpupgrade", "xhttp", "splithttp", "grpc", "quic", "kcp", "http", "h2"].includes(params.type));
if (finalXray && has_xray === "true") {
if (finalV2Ray && hasMihomoOrXray()) {
finalSSBackend = "v2ray";
} else {
finalSSBackend = preferredSSBackend();
}
if (finalSSBackend === null) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Shadowsocks or Xray) to import this node.%></font>";
s.innerHTML = "<font style=\'color:red\'><%:No available core (Shadowsocks, Mihomo or Xray) to import this node.%></font>";
return false;
}
if (finalSSBackend === "v2ray" && ssNode.plugin && ssNode.plugin !== "none") {
s.innerHTML = "<font style='color:red'>Xray fallback does not support importing Shadowsocks links with SIP003 plugins.</font>";
s.innerHTML = "<font style='color:red'>V2Ray fallback does not support importing Shadowsocks links with SIP003 plugins.</font>";
return false;
}
if (finalSSBackend === "v2ray") importAsXraySS(ssNode);
@@ -625,8 +653,8 @@ function import_ssr_url(btn, urlname, sid) {
return true;
}
// Xray Trojan 导入逻辑
function importAsXrayTrojan() {
// Mihomo/Xray Trojan 导入逻辑
function importAsV2RayTrojan() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
@@ -732,13 +760,12 @@ function import_ssr_url(btn, urlname, sid) {
return true;
}
// 主逻辑:Trojan 统一使用 Xray
function hasXrayCore() { return has_xray === "true"; }
if (!hasXrayCore()) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Xray) to import this node.%></font>";
// 主逻辑:Trojan 使用 Mihomo/Xray 兼容的 v2ray 节点格式
if (!hasMihomoOrXray()) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Mihomo or Xray) to import this node.%></font>";
return false;
}
importAsXrayTrojan();
importAsV2RayTrojan();
return false;
case "vmess":
var sstr = b64DecodeUnicode((ssu[1] || "").replace(/#.*/, "").trim());
@@ -133,23 +133,56 @@ end
);
}
function createSubscribeCell(sid, name, title, widget, html) {
var td = document.createElement('td');
td.className = 'td cbi-value-field';
td.setAttribute('data-name', name);
if (widget) td.setAttribute('data-widget', widget);
if (title) td.setAttribute('data-title', title);
var wrap = document.createElement('div');
wrap.id = 'cbi-shadowsocksr-' + sid + '-' + name;
wrap.innerHTML = html;
td.appendChild(wrap);
return td;
}
function createFallbackRow(sid) {
var newRow = document.createElement('tr');
newRow.className = 'tr cbi-section-table-row cbi-rowstyle-1';
newRow.appendChild(createSubscribeCell(sid, 'enabled', '<%:Enable%>', 'Flag',
'<input type="hidden" value="0" name="cbi.cbe.shadowsocksr.' + sid + '.enabled" />' +
'<input class="cbi-input-checkbox" type="checkbox" value="1" name="cbid.shadowsocksr.' + sid + '.enabled" id="cbid.shadowsocksr.' + sid + '.enabled" />'));
newRow.appendChild(createSubscribeCell(sid, 'alias', '<%:Alias%>', 'Value',
'<input class="cbi-input-text" type="text" name="cbid.shadowsocksr.' + sid + '.alias" id="cbid.shadowsocksr.' + sid + '.alias" value="" />'));
newRow.appendChild(createSubscribeCell(sid, 'url', '<%:Subscribe URL%>', 'Value',
'<input class="cbi-input-text" type="text" name="cbid.shadowsocksr.' + sid + '.url" id="cbid.shadowsocksr.' + sid + '.url" value="" />'));
var actions = document.createElement('td');
actions.className = 'td cbi-section-table-cell nowrap cbi-section-actions';
actions.innerHTML = '<div class="ssr-subscribe-actions">' +
'<input class="btn cbi-button cbi-button-save" type="button" value="<%:Save%>" title="<%:Save%>" onclick="saveAllSubscribeItems(this)" />' +
'<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" data-sid="' + sid + '" onclick="deleteSubscribeItem(this); return false;" />' +
'<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Update%>" data-original-value="<%:Update%>" data-sid="' + sid + '" data-title="Subscribe" onclick="return window.ssrUpdateSubscribeItem(this)" />' +
'</div>';
newRow.appendChild(actions);
return newRow;
}
function createEmptyRow(sid) {
var templateRow = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row:not(.cbi-section-table-titles):not(.placeholder)');
if (!templateRow) {
console.error('No template row found');
return null;
}
var newRow = templateRow.cloneNode(true);
newRow.id = 'cbi-shadowsocksr-server_subscribe_item-' + sid;
var newRow = templateRow ? templateRow.cloneNode(true) : createFallbackRow(sid);
newRow.id = 'cbi-shadowsocksr-' + sid;
var inputs = newRow.querySelectorAll('input');
inputs.forEach(function(input) {
if (input.name) {
input.name = input.name.replace(/cbid\.shadowsocksr\.[^.]+\./, 'cbid.shadowsocksr.' + sid + '.');
input.name = input.name.replace(/(cbid|cbi\.cbe)\.shadowsocksr\.[^.]+\./, '$1.shadowsocksr.' + sid + '.');
}
if (input.id) {
input.id = input.id.replace(/cfg[^.]+/, sid);
input.id = input.id.replace(/(cbid|cbi)-shadowsocksr-[^-]+-/, '$1-shadowsocksr-' + sid + '-');
input.id = input.id.replace(/(cbid|cbi\.cbe)\.shadowsocksr\.[^.]+\./, '$1.shadowsocksr.' + sid + '.');
}
if (input.type === 'checkbox') {
input.checked = false;
@@ -161,6 +194,10 @@ end
input.disabled = false;
});
newRow.querySelectorAll('[id^="cbi-shadowsocksr-"]').forEach(function(node) {
node.id = node.id.replace(/cbi-shadowsocksr-[^-]+-/, 'cbi-shadowsocksr-' + sid + '-');
});
var delBtn = newRow.querySelector('.cbi-button-remove');
if (delBtn) {
delBtn.setAttribute('data-sid', sid);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+241 -83
View File
@@ -214,7 +214,7 @@ is_builtin_dns_active() {
ps_list | grep -v "grep" | grep -q "ssr-retcp"
;;
v2ray)
ps_list | grep -v "grep" | grep -q "$TMP_PATH/.*ssr-retcp\\.json"
ps_list | grep -v "grep" | grep -Eq "$TMP_PATH/.*ssr-retcp\\.(json|yaml)"
;;
*)
return 1
@@ -284,6 +284,22 @@ get_ss_server_mihomo_runtime_file() {
echo "$(get_ss_server_mihomo_workdir "$1")/config.yaml"
}
get_v2ray_mihomo_workdir() {
echo "$TMP_PATH/v2ray-$1"
}
get_v2ray_mihomo_runtime_file() {
echo "$(get_v2ray_mihomo_workdir "$1")/config.yaml"
}
get_v2ray_server_mihomo_workdir() {
echo "$TMP_PATH/v2ray-server-$1"
}
get_v2ray_server_mihomo_runtime_file() {
echo "$(get_v2ray_server_mihomo_workdir "$1")/config.yaml"
}
link_mihomo_geodata() {
local workdir="$1"
@@ -366,6 +382,41 @@ prepare_ss_server_mihomo_runtime_config() {
fi
}
prepare_v2ray_mihomo_runtime_config() {
local sid="$1"
local local_port="$2"
local socks_port="$3"
local instance_key="${4:-$sid}"
local run_mode="${5:-redir}"
local runtime_file="$(get_v2ray_mihomo_runtime_file "$instance_key")"
local workdir="$(get_v2ray_mihomo_workdir "$instance_key")"
[ -n "$sid" ] || return 1
[ -n "$local_port" ] || return 1
mkdir -p "$workdir"
if ! /usr/bin/lua "$CLASH_YAML_HELPER" v2ray "$sid" "$runtime_file" "$local_port" "$socks_port" "$run_mode" >/dev/null 2>&1; then
echolog "V2Ray/Xray 单节点 Mihomo 运行配置生成失败:$sid"
return 1
fi
link_mihomo_geodata "$workdir"
}
prepare_v2ray_server_mihomo_runtime_config() {
local sid="$1"
local runtime_file="$(get_v2ray_server_mihomo_runtime_file "$sid")"
local workdir="$(get_v2ray_server_mihomo_workdir "$sid")"
[ -n "$sid" ] || return 1
mkdir -p "$workdir"
if ! /usr/bin/lua "$CLASH_YAML_HELPER" v2ray_server "$sid" "$runtime_file" >/dev/null 2>&1; then
echolog "V2Ray/Xray 服务端 Mihomo 配置生成失败:$sid"
return 1
fi
}
download_clash_config() {
local sid="$1"
local clash_url="$(uci_get_by_name "$sid" clash_url)"
@@ -419,7 +470,7 @@ clash_cache() {
filter_clash_yaml_proxies() {
local yaml_file="$1"
local filter_words="$(uci_get_by_type server_subscribe filter_words '过期时间/剩余流量')"
local filter_words="$(uci_get_by_type server_subscribe filter_words '过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国')"
local names_file="$TMP_PATH/clash-proxy-names.$$"
local removed_file="$TMP_PATH/clash-proxy-removed.$$"
local removed_count=0
@@ -697,6 +748,70 @@ use_mihomo_for_ss_rust_server() {
has_mihomo_binary
}
start_v2ray_with_mihomo() {
local sid="$1"
local local_port="$2"
local socks_port="$3"
local instance_key="$4"
local run_mode="$5"
local process_name="$6"
local log_prefix="$7"
local mihomo_bin="$(first_type mihomo)"
[ -x "$mihomo_bin" ] || return 1
prepare_v2ray_mihomo_runtime_config "$sid" "$local_port" "$socks_port" "$instance_key" "$run_mode" || return 1
local workdir="$(get_v2ray_mihomo_workdir "$instance_key")"
local protocol_name="$(uci_get_by_name "$sid" v2ray_protocol)"
[ -n "$protocol_name" ] || protocol_name="$(uci_get_by_name "$sid" type)"
ln_start_bin "$mihomo_bin" "$process_name" -d "$workdir" -f "$workdir/config.yaml"
echolog "$log_prefix:Mihomo ($protocol_name) Started!"
return 0
}
start_socks5_udp_with_ipt2socks() {
local sid="$1"
local local_port="$2"
local threads="$3"
local ipt2socks_bin="$(first_type ipt2socks)"
local auth_opts=""
[ -x "$ipt2socks_bin" ] || return 1
if [ "$(uci_get_by_name "$sid" auth_enable 0)" = "1" ]; then
auth_opts="-a $(uci_get_by_name "$sid" username) -k $(uci_get_by_name "$sid" password)"
fi
for i in $(seq 1 $threads); do
ln_start_bin "$ipt2socks_bin" ipt2socks -U -r -b 0.0.0.0 -4 \
-s "$(uci_get_by_name "$sid" server)" \
-p "$(uci_get_by_name "$sid" server_port)" \
-l "$local_port" $auth_opts
done
echolog "UDP TPROXY Relay:Socks5 via IPT2Socks $threads Threads Started!"
return 0
}
start_socks5_redir_with_ipt2socks() {
local sid="$1"
local local_port="$2"
local threads="$3"
local ipt2socks_bin="$(first_type ipt2socks)"
local auth_opts=""
[ -x "$ipt2socks_bin" ] || return 1
if [ "$(uci_get_by_name "$sid" auth_enable 0)" = "1" ]; then
auth_opts="-a $(uci_get_by_name "$sid" username) -k $(uci_get_by_name "$sid" password)"
fi
for i in $(seq 1 $threads); do
ln_start_bin "$ipt2socks_bin" ipt2socks \
-T -R -r -b 0.0.0.0 -4 \
-s "$(uci_get_by_name "$sid" server)" \
-p "$(uci_get_by_name "$sid" server_port)" \
-l "$local_port" $auth_opts
done
echolog "Main node:Socks5 via IPT2Socks $threads Threads Started!"
return 0
}
ln_start_bin() {
local file_func=${1}
local ln_name=${2}
@@ -1289,9 +1404,15 @@ start_udp() {
fi
;;
v2ray)
gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_port
ln_start_bin $(first_type xray) v2ray run -c $udp_config_file
echolog "UDP TPROXY Relay:$($(first_type xray) version | head -1) Started!"
if has_mihomo_binary; then
redir_udp=0
ARG_UDP=""
echolog "UDP TPROXY Relay:V2Ray/Xray 单节点由主 Mihomo 实例处理。"
else
gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_port
ln_start_bin $(first_type xray) v2ray run -c $udp_config_file
echolog "UDP TPROXY Relay:$($(first_type xray) version | head -1) Started!"
fi
;;
trojan) #client
gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_local_port
@@ -1317,8 +1438,12 @@ start_udp() {
local chain_type=$(uci_get_by_name $UDP_RELAY_SERVER chain_type)
case ${chain_type} in
vmess)
ln_start_bin $(first_type xray) v2ray run -c $udp_config_file
echolog "UDP TPROXY Relay:shadow-tls chain-to $($(first_type xray) --version) Started!"
if start_v2ray_with_mihomo "$UDP_RELAY_SERVER" "$tmp_udp_port" "0" "${UDP_RELAY_SERVER}-shadowtls-udp" "redir" "shadowtls-vmess-udp" "UDP TPROXY Relay:shadow-tls chain-to"; then
:
else
ln_start_bin $(first_type xray) v2ray run -c $udp_config_file
echolog "UDP TPROXY Relay:shadow-tls chain-to $($(first_type xray) --version) Started!"
fi
;;
sslocal)
ln_start_bin $(first_type sslocal) sslocal -c $udp_config_file
@@ -1327,16 +1452,16 @@ start_udp() {
esac
;;
socks5)
if [ "$(uci_get_by_name $UDP_RELAY_SERVER auth_enable 0)" == "1" ]; then
local auth="-a $(uci_get_by_name $UDP_RELAY_SERVER username) -k $(uci_get_by_name $UDP_RELAY_SERVER password)"
if start_socks5_udp_with_ipt2socks "$UDP_RELAY_SERVER" "$tmp_udp_port" "$threads"; then
:
elif has_mihomo_binary; then
redir_udp=0
ARG_UDP=""
echolog "UDP TPROXY Relay:Socks5 缺少 ipt2socks,已回退到主 Mihomo 实例处理。"
else
echolog "UDP TPROXY Relay:Socks5 缺少 ipt2socks,且 Mihomo 不可用。"
return 1
fi
for i in $(seq 1 $threads); do
ln_start_bin $(first_type ipt2socks) ipt2socks -U -r -b 0.0.0.0 -4 \
-s $(uci_get_by_name $UDP_RELAY_SERVER server) \
-p $(uci_get_by_name $UDP_RELAY_SERVER server_port) \
-l $tmp_udp_port $auth
done
echolog "UDP TPROXY Relay:Socks5 via IPT2Socks $threads Threads Started!"
;;
esac
}
@@ -1395,11 +1520,20 @@ start_local() {
fi
;;
v2ray)
if [ "$_local" == "2" ]; then
local instance_key="${LOCAL_SERVER}-local"
[ -n "$probe_instance_key" ] && instance_key="$probe_instance_key"
if start_v2ray_with_mihomo "$LOCAL_SERVER" "$local_port" "$local_port" "$instance_key" "socks" "v2ray-local" "Global_Socks5"; then
:
elif [ "$(uci_get_by_name "$LOCAL_SERVER" v2ray_protocol)" = "snell" ]; then
echolog "Global_Socks5:Snell 需要 Mihomo 内核,且当前 Mihomo 启动失败。"
return 1
elif [ "$_local" == "2" ]; then
gen_config_file $LOCAL_SERVER $type 4 0 $local_port
ln_start_bin $(first_type xray) v2ray run -c $local_config_file
echolog "Global_Socks5:$($(first_type xray) version | head -1) Started!"
else
return 1
fi
echolog "Global_Socks5:$($(first_type xray) version | head -1) Started!"
;;
trojan) #client
gen_config_file $LOCAL_SERVER $type 4 $local_port
@@ -1433,8 +1567,12 @@ start_local() {
local chain_type=$(uci_get_by_name $LOCAL_SERVER chain_type)
case ${chain_type} in
vmess)
ln_start_bin $(first_type xray) v2ray run -c $local_config_file
echolog "Global Socks5 Proxy:shadow-tls chain-to$($(first_type xray) --version) Started!"
if start_v2ray_with_mihomo "$LOCAL_SERVER" "$local_port" "$local_port" "${LOCAL_SERVER}-shadowtls-local" "socks" "shadowtls-vmess-local" "Global Socks5 Proxy:shadow-tls chain-to"; then
:
else
ln_start_bin $(first_type xray) v2ray run -c $local_config_file
echolog "Global Socks5 Proxy:shadow-tls chain-to$($(first_type xray) --version) Started!"
fi
;;
sslocal)
ln_start_bin $(first_type sslocal) sslocal -c $local_config_file
@@ -1443,16 +1581,22 @@ start_local() {
esac
fi
;;
clash)
echolog "Global_Socks5:Clash total node is only supported when using Same as Global Server."
clash)
echolog "Global_Socks5:Clash total node is only supported when using Same as Global Server."
return 1
;;
socks5)
local instance_key="${LOCAL_SERVER}-local"
[ -n "$probe_instance_key" ] && instance_key="$probe_instance_key"
if start_v2ray_with_mihomo "$LOCAL_SERVER" "$local_port" "$local_port" "$instance_key" "socks" "socks5-local" "Global_Socks5"; then
:
else
echolog "Global_Socks5:Socks5 单节点需要 Mihomo 才能作为本地 SOCKS 出口。"
return 1
;;
socks5)
echolog "Global_Socks5:Socks5 transparent-only node cannot be used as generic Socks outbound."
return 1
;;
*)
local listenip='-i 0.0.0.0'
fi
;;
*)
local listenip='-i 0.0.0.0'
[ -e /proc/sys/net/ipv6 ] && listenip='-i ::'
ln_start_bin $(first_type microsocks) microsocks $listenip -p $local_port tcp-udp-ssr-local
echolog "Global_Socks5:$type Started!"
@@ -1725,9 +1869,16 @@ Start_Run() {
fi
;;
v2ray)
gen_config_file $GLOBAL_SERVER $type 1 $tcp_port $socks_port
ln_start_bin $(first_type xray) v2ray run -c $tcp_config_file
echolog "Main node:$($(first_type xray) version | head -1) Started!"
if start_v2ray_with_mihomo "$GLOBAL_SERVER" "$tcp_port" "$socks_port" "$GLOBAL_SERVER" "redir" "ssr-retcp" "Main node"; then
:
elif [ "$(uci_get_by_name "$GLOBAL_SERVER" v2ray_protocol)" = "snell" ]; then
echolog "Main node:Snell 需要 Mihomo 内核,且当前 Mihomo 启动失败。"
return 1
else
gen_config_file $GLOBAL_SERVER $type 1 $tcp_port $socks_port
ln_start_bin $(first_type xray) v2ray run -c $tcp_config_file
echolog "Main node:$($(first_type xray) version | head -1) Started!"
fi
;;
trojan)
gen_config_file $GLOBAL_SERVER $type 1 $tcp_port
@@ -1759,24 +1910,28 @@ Start_Run() {
shadowtls)
if [ -z "$socks_port" ]; then
gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}"
gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" 0 chain
else
gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}"
gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" $socks_port chain
fi
local chain_type=$(uci_get_by_name $GLOBAL_SERVER chain_type)
case ${chain_type} in
vmess)
ln_start_bin $(first_type shadow-tls) shadow-tls config --config $chain_config_file
ln_start_bin $(first_type xray) v2ray run -c $tcp_config_file
echolog "Mian node:shadow-tls chain-to $($(first_type xray) --version) Started!"
;;
sslocal)
ln_start_bin $(first_type shadow-tls) shadow-tls config --config $chain_config_file
ln_start_bin $(first_type sslocal) sslocal -c $tcp_config_file
echolog "Main node:shadow-tls chain-to $($(first_type sslocal) --version) Started!"
;;
esac
gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" 0 chain
else
gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}"
gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" $socks_port chain
fi
local chain_type=$(uci_get_by_name $GLOBAL_SERVER chain_type)
case ${chain_type} in
vmess)
ln_start_bin $(first_type shadow-tls) shadow-tls config --config $chain_config_file
if start_v2ray_with_mihomo "$GLOBAL_SERVER" "$tcp_port" "$socks_port" "${GLOBAL_SERVER}-shadowtls" "redir" "shadowtls-vmess" "Main node:shadow-tls chain-to"; then
:
else
ln_start_bin $(first_type xray) v2ray run -c $tcp_config_file
echolog "Mian node:shadow-tls chain-to $($(first_type xray) --version) Started!"
fi
;;
sslocal)
ln_start_bin $(first_type shadow-tls) shadow-tls config --config $chain_config_file
ln_start_bin $(first_type sslocal) sslocal -c $tcp_config_file
echolog "Main node:shadow-tls chain-to $($(first_type sslocal) --version) Started!"
;;
esac
;;
clash)
local clash_socks_port=0
@@ -1802,23 +1957,14 @@ Start_Run() {
echolog "Main node:Mihomo (Clash) Started!"
;;
socks5)
local ipt2socks_bin="$(first_type ipt2socks)"
local auth_opts=""
[ -x "$ipt2socks_bin" ] || {
echolog "Main node:Socks5 缺少 ipt2socks,无法启动透明代理。"
if start_socks5_redir_with_ipt2socks "$GLOBAL_SERVER" "$tcp_port" "$threads"; then
:
elif start_v2ray_with_mihomo "$GLOBAL_SERVER" "$tcp_port" "$socks_port" "$GLOBAL_SERVER" "redir" "ssr-retcp" "Main node"; then
echolog "Main node:Socks5 缺少 ipt2socks,已回退到 Mihomo。"
else
echolog "Main node:Socks5 缺少 ipt2socks,且 Mihomo 不可用。"
return 1
}
if [ "$(uci_get_by_name $GLOBAL_SERVER auth_enable 0)" = "1" ]; then
auth_opts="-a $(uci_get_by_name $GLOBAL_SERVER username) -k $(uci_get_by_name $GLOBAL_SERVER password)"
fi
for i in $(seq 1 $threads); do
ln_start_bin "$ipt2socks_bin" ipt2socks \
-T -R -r -b 0.0.0.0 -4 \
-s "$(uci_get_by_name $GLOBAL_SERVER server)" \
-p "$(uci_get_by_name $GLOBAL_SERVER server_port)" \
-l "$tcp_port" $auth_opts
done
echolog "Main node:Socks5 via IPT2Socks $threads Threads Started!"
;;
esac
redir_tcp=1
@@ -2018,6 +2164,7 @@ start_server() {
local tcp_guise=$(uci_get_by_name "$1" tcp_guise "none")
local tcp_guise_http_host=$(uci_get_by_name "$1" tcp_guise_http_host "")
local tcp_guise_http_path=$(uci_get_by_name "$1" tcp_guise_http_path "")
local mihomo_server_started=0
# 确定密码/UUID
local password=""
@@ -2036,26 +2183,37 @@ start_server() {
echolog "Server: $type 服务端缺少密码/UUID,跳过启动"
return 1
}
local config_file="$TMP_PATH/xray-server-$server_count.json"
# 生成 Xray 配置
generate_xray_config "$type" "$port" "$password" "$security" "$method" "$network" "$ws_path" "$ws_host" "$grpc_service" "$alter_id" "$tcp_guise" "$tcp_guise_http_host" "$tcp_guise_http_path" "$config_file"
if [ $? -ne 0 ]; then
echolog "Server: Xray $type 配置生成失败"
return 1
local mihomo_bin="$(first_type mihomo)"
if [ -x "$mihomo_bin" ] && prepare_v2ray_server_mihomo_runtime_config "$1"; then
local server_workdir="$(get_v2ray_server_mihomo_workdir "$1")"
ln_start_bin "$mihomo_bin" mihomo-server-$server_count -d "$server_workdir" -f "$server_workdir/config.yaml"
echolog "Server: Mihomo $type 服务端 Server$server_count 已启动 (端口: $port)"
mihomo_server_started=1
fi
local xray_bin=$(first_type xray)
if [ -z "$xray_bin" ]; then
echolog "Server: xray-core 未安装,无法启动 $type 服务端"
return 1
if [ "$mihomo_server_started" != "1" ]; then
local config_file="$TMP_PATH/xray-server-$server_count.json"
# 生成 Xray 配置
generate_xray_config "$type" "$port" "$password" "$security" "$method" "$network" "$ws_path" "$ws_host" "$grpc_service" "$alter_id" "$tcp_guise" "$tcp_guise_http_host" "$tcp_guise_http_path" "$config_file"
if [ $? -ne 0 ]; then
echolog "Server: Xray $type 配置生成失败"
return 1
fi
local xray_bin=$(first_type xray)
if [ -z "$xray_bin" ]; then
echolog "Server: xray-core 未安装,无法启动 $type 服务端"
return 1
fi
ln_start_bin "$xray_bin" xray-server-$server_count run -c "$config_file"
echolog "Server: Xray $type 服务端 Server$server_count 已启动 (端口: $port)"
fi
ln_start_bin "$xray_bin" xray-server-$server_count run -c "$config_file"
echolog "Server: Xray $type 服务端 Server$server_count 已启动 (端口: $port)"
;;
;;
esac
server_port=$(uci_get_by_name $1 server_port)
if [ "$USE_TABLES" = "nftables" ]; then
@@ -63,6 +63,13 @@ if [ -s "/etc/config/shadowsocksr" ]; then
uci -q set shadowsocksr.@server_subscribe[0].proxy='1'
fi
current_filter_words="$(uci -q get shadowsocksr.@server_subscribe[0].filter_words)"
if [ -z "$current_filter_words" ]; then
uci -q set shadowsocksr.@server_subscribe[0].filter_words='过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国'
elif ! printf '%s\n' "$current_filter_words" | grep -q '重置'; then
uci -q set shadowsocksr.@server_subscribe[0].filter_words="${current_filter_words}/重置"
fi
if ! uci -q get shadowsocksr.@global_xray_fragment[0] > /dev/null; then
uci -q add shadowsocksr global_xray_fragment
uci -q set shadowsocksr.@global_xray_fragment[0].fragment='0'
+10 -18
View File
@@ -29,11 +29,11 @@ CURRENT_SERVER="$DEFAULT_SERVER"
ENABLE_SERVER="nil"
cleanup_probe_socks() {
ps_list | grep -v "grep" | grep -E "tcp-http-ssr-switch-local\\.json|ss-${PROBE_INSTANCE_KEY}/config\\.yaml|tuic-${PROBE_INSTANCE_KEY}/config\\.yaml" | awk '{print $1}' | xargs kill >/dev/null 2>&1
ps_list | grep -v "grep" | grep -E "tcp-http-ssr-switch-local\\.json|ss-${PROBE_INSTANCE_KEY}/config\\.yaml|tuic-${PROBE_INSTANCE_KEY}/config\\.yaml|v2ray-${PROBE_INSTANCE_KEY}/config\\.yaml" | awk '{print $1}' | xargs kill >/dev/null 2>&1
sleep 1
ps_list | grep -v "grep" | grep -E "tcp-http-ssr-switch-local\\.json|ss-${PROBE_INSTANCE_KEY}/config\\.yaml|tuic-${PROBE_INSTANCE_KEY}/config\\.yaml" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
ps_list | grep -v "grep" | grep -E "tcp-http-ssr-switch-local\\.json|ss-${PROBE_INSTANCE_KEY}/config\\.yaml|tuic-${PROBE_INSTANCE_KEY}/config\\.yaml|v2ray-${PROBE_INSTANCE_KEY}/config\\.yaml" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
rm -f "$TMP_PATH/tcp-http-ssr-switch-local.json" 2>/dev/null
rm -rf "$TMP_PATH/ss-${PROBE_INSTANCE_KEY}" "$TMP_PATH/tuic-${PROBE_INSTANCE_KEY}" 2>/dev/null
rm -rf "$TMP_PATH/ss-${PROBE_INSTANCE_KEY}" "$TMP_PATH/tuic-${PROBE_INSTANCE_KEY}" "$TMP_PATH/v2ray-${PROBE_INSTANCE_KEY}" 2>/dev/null
rm -f /tmp/ssr-switch-probe.log 2>/dev/null
}
@@ -54,21 +54,13 @@ start_probe_socks() {
tmp_local_port=
mode="tcp,udp"
if [ "$server_type" = "v2ray" ]; then
export SSR_SWITCH_PROBE=1
gen_config_file "$LOCAL_SERVER" "$server_type" 4 0 "$probe_socks_port" >/tmp/ssr-switch-probe.log 2>&1 || return 1
unset SSR_SWITCH_PROBE
local xray_bin
xray_bin="$(first_type xray)"
[ -x "$xray_bin" ] || return 1
"$xray_bin" run -c "$local_config_file" >>/tmp/ssr-switch-probe.log 2>&1 &
else
export SSR_SWITCH_PROBE=1
export SSR_SWITCH_PROBE_INSTANCE_KEY="$PROBE_INSTANCE_KEY"
start_local_with_port "$probe_socks_port" >/tmp/ssr-switch-probe.log 2>&1 || return 1
unset SSR_SWITCH_PROBE_INSTANCE_KEY
unset SSR_SWITCH_PROBE
fi
export SSR_SWITCH_PROBE=1
export SSR_SWITCH_PROBE_INSTANCE_KEY="$PROBE_INSTANCE_KEY"
start_local_with_port "$probe_socks_port" >/tmp/ssr-switch-probe.log 2>&1
local rc=$?
unset SSR_SWITCH_PROBE_INSTANCE_KEY
unset SSR_SWITCH_PROBE
[ "$rc" = "0" ] || return 1
sleep 2
return 0
@@ -1,5 +1,6 @@
#!/usr/bin/lua
require "nixio"
require "nixio.fs"
require "luci.model.uci"
@@ -582,6 +583,376 @@ local function string_or_nil(value)
return tostring(value)
end
local build_shadowsocks_plugin
local function first_nonempty(...)
for i = 1, select("#", ...) do
local value = select(i, ...)
if value ~= nil and value ~= "" then
return value
end
end
return nil
end
local function split_alpn(value)
local items = {}
for part in tostring(value or ""):gmatch("[^,;|%s]+") do
items[#items + 1] = part
end
return items
end
local function parse_wireguard_reserved(sid)
local raw = get_server_field(sid, "reserved", nil)
local values = {}
local bytes = {}
if raw == nil or raw == "" then
return nil
end
if type(raw) == "table" then
values = raw
else
values = { raw }
end
for _, item in ipairs(values) do
local text = tostring(item or "")
if text ~= "" then
if not text:match("[^%d,]+") then
for byte in text:gmatch("%d+") do
bytes[#bytes + 1] = tonumber(byte)
end
else
local decoded = nixio.bin.b64decode(text)
if decoded then
for i = 1, #decoded do
bytes[#bytes + 1] = decoded:byte(i)
end
end
end
end
end
return #bytes > 0 and bytes or nil
end
local function split_local_addresses(value)
local items = {}
if type(value) == "table" then
for _, item in ipairs(value) do
if item and item ~= "" then
items[#items + 1] = tostring(item)
end
end
elseif value and value ~= "" then
for item in tostring(value):gmatch("[^,%s]+") do
items[#items + 1] = item
end
end
return items
end
local function split_wireguard_addresses(value)
local ip, ipv6
for _, item in ipairs(split_local_addresses(value)) do
if item:find(":", 1, true) then
ipv6 = ipv6 or item
else
ip = ip or item
end
end
return ip, ipv6
end
local function apply_v2ray_tls_options(proxy, sid)
local tls = get_server_field(sid, "tls", "0")
local reality = get_server_field(sid, "reality", "0")
if tls ~= "1" and reality ~= "1" then
return
end
proxy.tls = true
proxy.servername = string_or_nil(get_server_field(sid, "tls_host", ""))
proxy["client-fingerprint"] = string_or_nil(get_server_field(sid, "fingerprint", ""))
proxy.fingerprint = string_or_nil(get_server_field(sid, "tls_CertSha", ""))
proxy["skip-cert-verify"] = bool_enabled(get_server_field(sid, "insecure", "0"))
local alpn = split_alpn(get_server_field(sid, "tls_alpn", ""))
if #alpn > 0 then
proxy.alpn = alpn
end
if reality == "1" then
proxy["reality-opts"] = {
["public-key"] = string_or_nil(get_server_field(sid, "reality_publickey", "")),
["short-id"] = string_or_nil(get_server_field(sid, "reality_shortid", "")),
["support-x25519mlkem768"] = bool_enabled(get_server_field(sid, "enable_mldsa65verify", "0"))
}
end
if get_server_field(sid, "enable_ech", "0") == "1" then
proxy["ech-opts"] = {
enable = true,
config = string_or_nil(get_server_field(sid, "ech_config", ""))
}
end
end
local function apply_trojan_tls_options(proxy, sid)
proxy.sni = string_or_nil(get_server_field(sid, "tls_host", ""))
proxy["client-fingerprint"] = string_or_nil(get_server_field(sid, "fingerprint", ""))
proxy.fingerprint = string_or_nil(get_server_field(sid, "tls_CertSha", ""))
proxy["skip-cert-verify"] = bool_enabled(get_server_field(sid, "insecure", "0"))
local alpn = split_alpn(get_server_field(sid, "tls_alpn", ""))
if #alpn > 0 then
proxy.alpn = alpn
end
if get_server_field(sid, "reality", "0") == "1" then
proxy["reality-opts"] = {
["public-key"] = string_or_nil(get_server_field(sid, "reality_publickey", "")),
["short-id"] = string_or_nil(get_server_field(sid, "reality_shortid", ""))
}
end
end
local function apply_v2ray_transport_options(proxy, sid)
local transport = get_server_field(sid, "transport", "raw")
if transport == "raw" or transport == "tcp" or transport == "" then
return
end
if transport == "ws" then
proxy.network = "ws"
proxy["ws-opts"] = {
path = string_or_nil(get_server_field(sid, "ws_path", "")),
headers = first_nonempty(get_server_field(sid, "ws_host", ""), get_server_field(sid, "tls_host", "")) and {
Host = first_nonempty(get_server_field(sid, "ws_host", ""), get_server_field(sid, "tls_host", ""))
} or nil
}
elseif transport == "httpupgrade" then
proxy.network = "ws"
proxy["ws-opts"] = {
path = string_or_nil(get_server_field(sid, "httpupgrade_path", "")),
headers = first_nonempty(get_server_field(sid, "httpupgrade_host", ""), get_server_field(sid, "tls_host", "")) and {
Host = first_nonempty(get_server_field(sid, "httpupgrade_host", ""), get_server_field(sid, "tls_host", ""))
} or nil,
["v2ray-http-upgrade"] = true
}
elseif transport == "h2" then
local host = first_nonempty(get_server_field(sid, "h2_host", ""), get_server_field(sid, "tls_host", ""))
proxy.network = "h2"
proxy["h2-opts"] = {
host = host and split_alpn(host) or nil,
path = string_or_nil(get_server_field(sid, "h2_path", ""))
}
elseif transport == "grpc" then
proxy.network = "grpc"
proxy["grpc-opts"] = {
["grpc-service-name"] = string_or_nil(get_server_field(sid, "serviceName", ""))
}
elseif transport == "xhttp" and proxy.type == "vless" then
proxy.network = "xhttp"
proxy["xhttp-opts"] = {
path = string_or_nil(get_server_field(sid, "xhttp_path", "")),
host = string_or_nil(get_server_field(sid, "xhttp_host", "")),
mode = string_or_nil(get_server_field(sid, "xhttp_mode", ""))
}
end
end
local function can_mihomo_handle_v2ray_transport(protocol, sid)
local transport = get_server_field(sid, "transport", "raw")
if transport == "" or transport == "raw" or transport == "tcp" then
if get_server_field(sid, "tcp_guise", "none") == "http" then
return false
end
return true
end
if protocol == "socks" or protocol == "http" then
return false
end
if transport == "ws" or transport == "httpupgrade" or transport == "h2" or transport == "grpc" then
return true
end
return protocol == "vless" and transport == "xhttp"
end
local function build_v2ray_mihomo_proxy(sid)
local node_type = get_server_field(sid, "type", "")
local protocol = get_server_field(sid, "v2ray_protocol", "vmess")
local proxy = {
name = sid,
server = get_server_field(sid, "server", ""),
port = tonumber(get_server_field(sid, "server_port", "0")) or 0,
udp = true,
tfo = bool_enabled(get_server_field(sid, "fast_open", "0"))
}
if node_type == "socks5" then
proxy.type = "socks5"
if get_server_field(sid, "auth_enable", "0") == "1" then
proxy.username = string_or_nil(get_server_field(sid, "username", ""))
proxy.password = string_or_nil(get_server_field(sid, "password", ""))
end
elseif protocol == "vmess" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "vmess"
proxy.uuid = first_nonempty(get_server_field(sid, "vmess_id", ""), get_server_field(sid, "vmess_uuid", "")) or ""
proxy.alterId = tonumber(get_server_field(sid, "alter_id", "0")) or 0
proxy.cipher = first_nonempty(get_server_field(sid, "security", ""), get_server_field(sid, "vmess_method", ""), "auto")
apply_v2ray_tls_options(proxy, sid)
apply_v2ray_transport_options(proxy, sid)
elseif protocol == "vless" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "vless"
proxy.uuid = get_server_field(sid, "vmess_id", "")
proxy.flow = string_or_nil(get_server_field(sid, "tls_flow", ""))
proxy.encryption = get_server_field(sid, "vless_encryption", "")
apply_v2ray_tls_options(proxy, sid)
apply_v2ray_transport_options(proxy, sid)
elseif protocol == "trojan" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "trojan"
proxy.password = get_server_field(sid, "password", "")
apply_trojan_tls_options(proxy, sid)
apply_v2ray_transport_options(proxy, sid)
elseif protocol == "shadowsocks" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "ss"
proxy.cipher = get_server_field(sid, "encrypt_method_ss", "none")
proxy.password = get_server_field(sid, "password", "")
build_shadowsocks_plugin(proxy, sid)
elseif protocol == "hysteria2" then
proxy.type = "hysteria2"
proxy.password = get_server_field(sid, "hy2_auth", "")
proxy.ports = string_or_nil(get_server_field(sid, "port_range", ""))
proxy.up = string_or_nil(get_server_field(sid, "uplink_capacity", "")) and (get_server_field(sid, "uplink_capacity", "") .. " Mbps") or nil
proxy.down = string_or_nil(get_server_field(sid, "downlink_capacity", "")) and (get_server_field(sid, "downlink_capacity", "") .. " Mbps") or nil
proxy.sni = string_or_nil(get_server_field(sid, "tls_host", ""))
proxy.fingerprint = string_or_nil(get_server_field(sid, "tls_CertSha", ""))
proxy["skip-cert-verify"] = bool_enabled(get_server_field(sid, "insecure", "0"))
local alpn = split_alpn(get_server_field(sid, "tls_alpn", ""))
if #alpn > 0 then
proxy.alpn = alpn
end
if get_server_field(sid, "flag_obfs", "0") == "1" then
proxy.obfs = string_or_nil(get_server_field(sid, "obfs_type", ""))
proxy["obfs-password"] = string_or_nil(get_server_field(sid, "salamander", ""))
end
elseif protocol == "socks" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "socks5"
if get_server_field(sid, "socks_ver", "5") ~= "5" then
return nil
end
if get_server_field(sid, "auth_enable", "0") == "1" then
proxy.username = string_or_nil(get_server_field(sid, "username", ""))
proxy.password = string_or_nil(get_server_field(sid, "password", ""))
end
apply_v2ray_tls_options(proxy, sid)
elseif protocol == "http" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "http"
if get_server_field(sid, "auth_enable", "0") == "1" then
proxy.username = string_or_nil(get_server_field(sid, "username", ""))
proxy.password = string_or_nil(get_server_field(sid, "password", ""))
end
apply_v2ray_tls_options(proxy, sid)
elseif protocol == "wireguard" then
local ip, ipv6 = split_wireguard_addresses(get_server_field(sid, "local_addresses", ""))
proxy.type = "wireguard"
proxy["private-key"] = get_server_field(sid, "private_key", "")
proxy["public-key"] = get_server_field(sid, "peer_pubkey", "")
proxy["pre-shared-key"] = string_or_nil(get_server_field(sid, "preshared_key", ""))
proxy.ip = ip
proxy.ipv6 = ipv6
proxy["allowed-ips"] = split_local_addresses(get_server_field(sid, "allowedips", "0.0.0.0/0"))
proxy.reserved = parse_wireguard_reserved(sid)
proxy["persistent-keepalive"] = number_or_nil(get_server_field(sid, "keepaliveperiod", ""))
proxy.mtu = number_or_nil(get_server_field(sid, "mtu", ""))
elseif protocol == "snell" then
proxy.type = "snell"
proxy.psk = get_server_field(sid, "snell_psk", "")
proxy.version = number_or_nil(get_server_field(sid, "snell_version", ""))
local obfs_mode = string_or_nil(get_server_field(sid, "snell_obfs", ""))
local obfs_host = string_or_nil(get_server_field(sid, "snell_obfs_host", ""))
if obfs_mode or obfs_host then
proxy["obfs-opts"] = {
mode = obfs_mode,
host = obfs_host
}
end
else
return nil
end
if not proxy.server or proxy.server == "" or not proxy.port or proxy.port == 0 then
return nil
end
if proxy.type == "snell" and (not proxy.psk or proxy.psk == "") then
return nil
end
return proxy
end
local function build_single_proxy_runtime_doc(proxy, local_port, socks_port, mode)
local dns_mode = uci:get_first("shadowsocksr", "global", "pdnsd_enable", "0")
local listen_port = tonumber(local_port)
local socks_listen = tonumber(socks_port)
local doc = {
["allow-lan"] = true,
["bind-address"] = "0.0.0.0",
mode = "rule",
["log-level"] = "silent",
["find-process-mode"] = "off",
["unified-delay"] = true,
["tcp-concurrent"] = true,
["routing-mark"] = 255,
proxies = { proxy },
["proxy-groups"] = {
{
name = "PROXY",
type = "select",
proxies = { proxy.name }
}
},
rules = { "MATCH,PROXY" },
tun = { enable = false },
profile = { ["store-selected"] = true },
dns = {
enable = dns_mode == "7",
["enhanced-mode"] = "redir-host",
listen = "127.0.0.1:5335",
ipv6 = get_filter_aaaa() ~= "1"
}
}
if mode == "socks" then
doc["socks-port"] = listen_port
else
doc["redir-port"] = listen_port
doc["tproxy-port"] = listen_port
if socks_listen and socks_listen > 0 then
doc["socks-port"] = socks_listen
end
end
return doc
end
local function pick_plugin_opt(plugin_opts, ...)
for i = 1, select("#", ...) do
local key = select(i, ...)
@@ -661,7 +1032,7 @@ local function normalize_plugin_name(plugin)
return value
end
local function build_shadowsocks_plugin(proxy, sid)
function build_shadowsocks_plugin(proxy, sid)
local plugin = normalize_plugin_name(get_server_field(sid, "plugin", ""))
local plugin_opts = parse_plugin_opts(get_server_field(sid, "plugin_opts", ""))
@@ -871,6 +1242,22 @@ local function generate_shadowsocks_runtime(sid, output_path, local_port, socks_
return true
end
local function generate_v2ray_runtime(sid, output_path, local_port, socks_port, mode)
local proxy = build_v2ray_mihomo_proxy(sid)
if not proxy then
io.stderr:write("unsupported_or_invalid_v2ray_node\n")
return false
end
local doc = build_single_proxy_runtime_doc(proxy, local_port, socks_port, mode)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local function build_shadowsocks_server_doc(sid)
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local method = get_server_field(sid, "encrypt_method_ss", "aes-128-gcm")
@@ -916,6 +1303,86 @@ local function generate_shadowsocks_server(sid, output_path)
return true
end
local function build_mihomo_listener_doc(sid)
local ltype = get_server_field(sid, "type", "")
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local listener = {
name = sid,
type = ltype,
listen = "::",
port = server_port
}
if ltype == "vmess" then
listener.users = {
{
username = "1",
uuid = get_server_field(sid, "uuid", ""),
alterId = tonumber(get_server_field(sid, "alter_id", "0")) or 0
}
}
elseif ltype == "vless" then
listener.users = {
{
username = "1",
uuid = get_server_field(sid, "uuid", ""),
flow = string_or_nil(get_server_field(sid, "flow", ""))
}
}
elseif ltype == "trojan" then
listener.users = {
{
username = "1",
password = get_server_field(sid, "trojan_password", "")
}
}
elseif ltype == "shadowsocks" then
listener.type = "shadowsocks"
listener.cipher = get_server_field(sid, "security", "chacha20-ietf-poly1305")
listener.password = get_server_field(sid, "ss_password", "")
listener.udp = true
else
return nil
end
local network = get_server_field(sid, "network", "tcp")
if network == "ws" then
listener["ws-path"] = string_or_nil(get_server_field(sid, "ws_path", "/"))
elseif network == "grpc" then
listener["grpc-service-name"] = string_or_nil(get_server_field(sid, "grpc_service", ""))
end
local cert = get_server_field(sid, "certpath", "")
local key = get_server_field(sid, "keypath", "")
if cert ~= "" and key ~= "" then
listener.certificate = cert
listener["private-key"] = key
end
return {
["allow-lan"] = true,
["bind-address"] = "*",
["log-level"] = "silent",
["find-process-mode"] = "off",
listeners = { listener }
}
end
local function generate_mihomo_listener(sid, output_path)
local doc = build_mihomo_listener_doc(sid)
if not doc then
io.stderr:write("unsupported_listener\n")
return false
end
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local action = arg[1]
if action == "validate" then
os.exit(validate(arg[2]) and 0 or 1)
@@ -931,9 +1398,13 @@ elseif action == "tuic" then
os.exit(generate_tuic_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss" then
os.exit(generate_shadowsocks_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "v2ray" then
os.exit(generate_v2ray_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss_server" then
os.exit(generate_shadowsocks_server(arg[2], arg[3]) and 0 or 1)
elseif action == "v2ray_server" then
os.exit(generate_mihomo_listener(arg[2], arg[3]) and 0 or 1)
else
io.stderr:write("usage: clash_yaml.lua validate <yaml> | filter <yaml> <words> | prepare <input> <output> | merge <raw> <overlay> <output> | append_client_policy_rules <runtime_yaml> <sid> | tuic <sid> <output> <local_port> [socks_port] [mode] | ss <sid> <output> <local_port> [socks_port] [mode] | ss_server <sid> <output>\n")
io.stderr:write("usage: clash_yaml.lua validate <yaml> | filter <yaml> <words> | prepare <input> <output> | merge <raw> <overlay> <output> | append_client_policy_rules <runtime_yaml> <sid> | tuic <sid> <output> <local_port> [socks_port] [mode] | ss <sid> <output> <local_port> [socks_port] [mode] | v2ray <sid> <output> <local_port> [socks_port] [mode] | ss_server <sid> <output> | v2ray_server <sid> <output>\n")
os.exit(1)
end
@@ -50,32 +50,28 @@ local b64encode = nixio.bin.b64encode
local effective_node_local_port = tonumber(server.local_port) or tonumber(default_node_local_port) or 1234
if server.type == "ss-rust" then
server.type = "ss"
server.type = "ss"
end
local function parse_realm_uri(uri)
if type(uri) ~= "string" then return nil end
-- realm://token@server/realm_id?query
local token, server_url, realm_id, query = trim(uri):match("^realm://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not token or not server_url or not realm_id then return nil end
-- realm[+http]://token@server/realm_id?query
local scheme, token, server_url, realm_id, query = trim(uri):match("^(realm%+http|realm)://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not scheme or not token or not server_url or not realm_id then return nil end
realm_id = realm_id:gsub("/+$", "")
local realm = {
scheme = scheme,
token = token,
server_url = server_url,
realm_id = realm_id
}
-- 解析 query 中的 stun=
if query and query ~= "" then
local stun_servers = {}
for key, value in query:gmatch("([^&=?]+)=([^&]+)") do
if key == "stun" and value ~= "" then
stun_servers[#stun_servers + 1] = value
end
end
if #stun_servers > 0 then
realm.stun_servers = stun_servers
end
local stun_servers
for value in (query or ""):gmatch("[?&]?[Ss][Tt][Uu][Nn]=([^&]+)") do
if not stun_servers then stun_servers = {} end
stun_servers[#stun_servers + 1] = value
end
realm.stun_servers = stun_servers
return realm
end
@@ -428,22 +424,24 @@ end
-- 开启 socks 代理
-- 检查是否启用 socks 代理
if proto and proto:find("tcp") and socks_port ~= "0" then
table.insert(Xray.inbounds, {
-- socks
protocol = "socks",
port = tonumber(socks_port),
local auth = (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth")
and {{
user = socks_server.socks5_user,
pass = socks_server.socks5_pass
}} or nil
table.insert(Xray.inbounds, {
-- socks
protocol = "socks",
port = tonumber(socks_port),
settings = {
auth = socks_server.socks5_auth or "noauth",
udp = true,
mixed = ((socks_server.socks5_mixed == '1') and true or false) or nil,
accounts = (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth") and {
{
user = socks_server.socks5_user,
pass = socks_server.socks5_pass
}
} or nil
} or nil
})
mixed = socks_server.socks5_mixed == "1" or nil,
accounts = (xray_version_val <= 260503) and auth or nil,
users = (xray_version_val > 260503) and auth or nil
}
})
end
-- 传出连接
@@ -645,9 +643,7 @@ Xray.outbounds = {
local realm = parse_realm_uri(server.hysteria2_realm_url)
local url, stun
if realm then
if realm.token and realm.server_url and realm.realm_id then
url = "realm://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
end
url = realm.scheme .. "://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
stun = realm.stun_servers or server.hysteria2_realm_stun
end
local r = {
@@ -29,7 +29,7 @@ config server_subscribe
option auto_update_min_time '0'
option url_test_url 'https://www.google.com/generate_204'
option user_agent 'v2rayN/9.99'
option filter_words '过期/套餐/剩余/网址/QQ群/官网/防失联/回国'
option filter_words '过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国'
config access_control
option lan_ac_mode '0'
@@ -26,9 +26,12 @@ local ucic = require "luci.model.uci".cursor()
local proxy = ucic:get_first(name, 'server_subscribe', 'proxy', '0')
local switch = ucic:get_first(name, 'server_subscribe', 'switch', '1')
local allow_insecure = ucic:get_first(name, 'server_subscribe', 'allow_insecure', '0')
local filter_words = ucic:get_first(name, 'server_subscribe', 'filter_words', '过期/套餐/剩余/网址/QQ群/官网/防失联/回国')
local filter_words = ucic:get_first(name, 'server_subscribe', 'filter_words', '过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国')
local save_words = ucic:get_first(name, 'server_subscribe', 'save_words', '')
local user_agent = ucic:get_first(name, 'server_subscribe', 'user_agent', 'v2rayN/9.99')
local sub_convert = ucic:get_first(name, 'server_subscribe', 'sub_convert', '0')
local convert_address = ucic:get_first(name, 'server_subscribe', 'convert_address', 'https://api.asailor.org/sub')
local template_url = ucic:get_first(name, 'server_subscribe', 'template_url', '')
local local_clash_dir = "/etc/ssrplus/clash"
local target_subscribe_sid = tostring(arg and arg[1] or ""):gsub("^%s*(.-)%s*$", "%1")
@@ -125,6 +128,11 @@ local function urlEncode(szText)
return str
end
local function urlEncodeRFC3986(szText)
local str = tostring(szText or ""):gsub("([^0-9a-zA-Z%-_%.~])", get_urlencode)
return str
end
local function get_urldecode(h)
return schar(tonumber(h, 16))
end
@@ -397,6 +405,7 @@ local function parseAnytlsShare(content)
if not userinfo or not hostinfo then
return nil
end
hostinfo = hostinfo:gsub("/+$", "")
local password = UrlDecode(userinfo)
local server, port = hostinfo:match("^(.+):(%d+)$")
@@ -468,6 +477,389 @@ local function buildAnytlsClashYaml(entries, group_name)
return table.concat(lines, "\n") .. "\n"
end
local function anytls_to_mihomo_node(entry)
if not entry then
return nil
end
return {
type = "anytls",
alias = entry.name,
raw_alias = entry.name,
server = entry.server,
server_port = entry.port,
password = entry.password,
tls_host = entry.sni,
insecure = entry.allow_insecure and "1" or "0",
fingerprint = entry.client_fingerprint
}
end
local function split_csv_values(value)
local items = {}
for v in tostring(value or ""):gmatch("[^,;|%s]+") do
items[#items + 1] = v
end
return items
end
local function bool_from_flag(value)
return is_true_value(value) and true or nil
end
local function number_from_value(value)
if value == nil or value == "" then
return nil
end
return tonumber(value)
end
local function string_from_value(value)
if value == nil or value == "" then
return nil
end
return tostring(value)
end
local function split_plugin_opts(value)
local opts = {}
for part in tostring(value or ""):gmatch("[^;]+") do
local key, val = part:match("^%s*([^=]+)=?(.*)%s*$")
if key and key ~= "" then
opts[key] = val or ""
end
end
return opts
end
local function pick_plugin_opt(opts, ...)
for i = 1, select("#", ...) do
local key = select(i, ...)
if opts[key] ~= nil and opts[key] ~= "" then
return opts[key]
end
end
return nil
end
local function normalize_mihomo_plugin_name(plugin)
local value = tostring(plugin or ""):lower()
if value == "" or value == "none" then
return ""
end
if value == "simple-obfs" or value == "obfs" or value == "obfs-local" then
return "obfs"
end
if value == "xray-plugin" then
return "v2ray-plugin"
end
if value == "shadowtls" then
return "shadow-tls"
end
return value
end
local function apply_mihomo_ss_plugin(proxy, node)
local plugin = normalize_mihomo_plugin_name(node.plugin)
if plugin == "" then
return
end
local opts = split_plugin_opts(node.plugin_opts)
proxy.plugin = plugin
if plugin == "obfs" then
proxy["plugin-opts"] = {
mode = pick_plugin_opt(opts, "obfs", "mode") or "http",
host = string_from_value(pick_plugin_opt(opts, "obfs-host", "obfs_host", "host"))
}
elseif plugin == "shadow-tls" then
proxy["client-fingerprint"] = string_from_value(node.fingerprint)
proxy["plugin-opts"] = {
host = string_from_value(pick_plugin_opt(opts, "host")),
password = string_from_value(pick_plugin_opt(opts, "passwd", "password")),
version = number_from_value(pick_plugin_opt(opts, "version")) or (opts.v3 == "1" and 3) or (opts.v2 == "1" and 2) or (opts.v1 == "1" and 1) or nil
}
elseif plugin == "v2ray-plugin" then
proxy["plugin-opts"] = {
mode = pick_plugin_opt(opts, "mode") or "websocket",
tls = bool_from_flag(pick_plugin_opt(opts, "tls")),
host = string_from_value(pick_plugin_opt(opts, "host")),
path = string_from_value(pick_plugin_opt(opts, "path")),
mux = bool_from_flag(pick_plugin_opt(opts, "mux")),
["skip-cert-verify"] = bool_from_flag(pick_plugin_opt(opts, "skip-cert-verify", "skip_cert_verify", "insecure")),
["v2ray-http-upgrade"] = bool_from_flag(pick_plugin_opt(opts, "v2ray-http-upgrade", "v2ray_http_upgrade"))
}
else
proxy["plugin-opts"] = next(opts) and opts or nil
end
end
local function apply_mihomo_tls_options(proxy, node)
local tls_enabled = node.tls == "1" or node.reality == "1"
if not tls_enabled then
return
end
proxy.tls = true
proxy.servername = string_from_value(node.tls_host)
proxy.fingerprint = string_from_value(node.tls_CertSha)
proxy["client-fingerprint"] = string_from_value(node.fingerprint)
proxy["skip-cert-verify"] = bool_from_flag(node.insecure)
local alpn = split_csv_values(node.tls_alpn)
if #alpn > 0 then
proxy.alpn = alpn
end
if node.reality == "1" then
proxy["reality-opts"] = {
["public-key"] = string_from_value(node.reality_publickey),
["short-id"] = string_from_value(node.reality_shortid),
["support-x25519mlkem768"] = bool_from_flag(node.enable_mldsa65verify)
}
end
if node.enable_ech == "1" and node.ech_config and node.ech_config ~= "" then
proxy["ech-opts"] = {
enable = true,
config = node.ech_config
}
end
end
local function apply_mihomo_trojan_tls_options(proxy, node)
proxy.sni = string_from_value(node.tls_host)
proxy.fingerprint = string_from_value(node.tls_CertSha)
proxy["client-fingerprint"] = string_from_value(node.fingerprint)
proxy["skip-cert-verify"] = bool_from_flag(node.insecure)
local alpn = split_csv_values(node.tls_alpn)
if #alpn > 0 then
proxy.alpn = alpn
end
if node.reality == "1" then
proxy["reality-opts"] = {
["public-key"] = string_from_value(node.reality_publickey),
["short-id"] = string_from_value(node.reality_shortid)
}
end
end
local function apply_mihomo_transport_options(proxy, node)
local transport = node.transport or "raw"
if transport == "raw" or transport == "tcp" or transport == "" then
return
end
if transport == "httpupgrade" then
proxy.network = "ws"
proxy["ws-opts"] = {
path = string_from_value(node.httpupgrade_path),
headers = node.httpupgrade_host and node.httpupgrade_host ~= "" and { Host = node.httpupgrade_host } or nil,
["v2ray-http-upgrade"] = true
}
elseif transport == "ws" then
proxy.network = "ws"
proxy["ws-opts"] = {
path = string_from_value(node.ws_path),
headers = node.ws_host and node.ws_host ~= "" and { Host = node.ws_host } or nil
}
elseif transport == "h2" then
proxy.network = "h2"
proxy["h2-opts"] = {
host = node.h2_host and node.h2_host ~= "" and split_csv_values(node.h2_host) or nil,
path = string_from_value(node.h2_path)
}
elseif transport == "grpc" then
proxy.network = "grpc"
proxy["grpc-opts"] = {
["grpc-service-name"] = string_from_value(node.serviceName)
}
elseif transport == "xhttp" and proxy.type == "vless" then
proxy.network = "xhttp"
proxy["xhttp-opts"] = {
path = string_from_value(node.xhttp_path),
host = string_from_value(node.xhttp_host),
mode = string_from_value(node.xhttp_mode)
}
end
end
local function to_mihomo_proxy(node)
local proxy = {
name = node.alias,
server = node.server,
port = tonumber(node.server_port) or node.server_port,
udp = true
}
if node.type == "v2ray" and node.v2ray_protocol == "vmess" then
proxy.type = "vmess"
proxy.uuid = node.vmess_id
proxy.alterId = tonumber(node.alter_id) or 0
proxy.cipher = node.security or "auto"
proxy.tfo = bool_from_flag(node.fast_open)
apply_mihomo_tls_options(proxy, node)
apply_mihomo_transport_options(proxy, node)
elseif node.type == "v2ray" and node.v2ray_protocol == "vless" then
proxy.type = "vless"
proxy.uuid = node.vmess_id
proxy.flow = string_from_value(node.tls_flow)
proxy.encryption = node.vless_encryption or ""
proxy.tfo = bool_from_flag(node.fast_open)
apply_mihomo_tls_options(proxy, node)
apply_mihomo_transport_options(proxy, node)
elseif node.type == "ss" or node.type == "ss-rust" or (node.type == "v2ray" and node.v2ray_protocol == "shadowsocks") then
proxy.type = "ss"
proxy.cipher = node.encrypt_method_ss or node.encrypt_method
proxy.password = node.password
proxy.tfo = bool_from_flag(node.fast_open)
apply_mihomo_ss_plugin(proxy, node)
elseif node.type == "ssr" then
proxy.type = "ssr"
proxy.cipher = node.encrypt_method
proxy.password = node.password
proxy.obfs = node.obfs
proxy.protocol = node.protocol
proxy["obfs-param"] = string_from_value(node.obfs_param)
proxy["protocol-param"] = string_from_value(node.protocol_param)
proxy.tfo = bool_from_flag(node.fast_open)
elseif node.type == "v2ray" and node.v2ray_protocol == "trojan" then
proxy.type = "trojan"
proxy.password = node.password
proxy.tfo = bool_from_flag(node.fast_open)
apply_mihomo_trojan_tls_options(proxy, node)
apply_mihomo_transport_options(proxy, node)
elseif node.type == "tuic" then
local alpn = split_csv_values(node.tls_alpn)
local heartbeat = number_from_value(node.heartbeat)
local timeout = number_from_value(node.timeout)
proxy.type = "tuic"
proxy.uuid = node.tuic_uuid
proxy.password = node.tuic_passwd
proxy.ip = string_from_value(node.tuic_ip)
proxy.sni = string_from_value(node.tls_host)
proxy.alpn = (#alpn > 0) and alpn or nil
proxy["udp-relay-mode"] = string_from_value(node.udp_relay_mode)
proxy["congestion-controller"] = string_from_value(node.congestion_control)
proxy["heartbeat-interval"] = heartbeat and (heartbeat * 1000) or nil
proxy["request-timeout"] = timeout and (timeout * 1000) or nil
proxy["max-udp-relay-packet-size"] = number_from_value(node.tuic_max_package_size)
proxy["disable-sni"] = bool_from_flag(node.disable_sni)
proxy["reduce-rtt"] = bool_from_flag(node.zero_rtt_handshake)
proxy["skip-cert-verify"] = bool_from_flag(node.insecure)
elseif node.type == "v2ray" and (node.v2ray_protocol == "hysteria2" or node.v2ray_protocol == "hy2") then
local alpn = split_csv_values(node.tls_alpn)
proxy.type = "hysteria2"
proxy.password = node.hy2_auth
proxy.ports = string_from_value(node.port_range)
proxy.up = node.uplink_capacity and (tostring(node.uplink_capacity) .. " Mbps") or nil
proxy.down = node.downlink_capacity and (tostring(node.downlink_capacity) .. " Mbps") or nil
proxy.sni = string_from_value(node.tls_host)
proxy.fingerprint = string_from_value(node.tls_CertSha)
proxy["skip-cert-verify"] = bool_from_flag(node.insecure)
proxy.alpn = (#alpn > 0) and alpn or nil
if node.flag_obfs == "1" then
proxy.obfs = string_from_value(node.obfs_type)
proxy["obfs-password"] = string_from_value(node.salamander)
end
elseif node.type == "anytls" then
proxy.type = "anytls"
proxy.password = node.password
proxy.sni = string_from_value(node.tls_host)
proxy["client-fingerprint"] = string_from_value(node.fingerprint)
proxy["skip-cert-verify"] = bool_from_flag(node.insecure)
elseif node.type == "v2ray" and node.v2ray_protocol == "snell" then
proxy.type = "snell"
proxy.psk = node.snell_psk
proxy.version = tonumber(node.snell_version) or nil
local obfs_mode = string_from_value(node.snell_obfs)
local obfs_host = string_from_value(node.snell_obfs_host)
if obfs_mode or obfs_host then
proxy["obfs-opts"] = {
mode = obfs_mode,
host = obfs_host
}
end
else
return nil
end
if not proxy.name or proxy.name == "" or not proxy.server or not proxy.port then
return nil
end
return proxy
end
local function can_group_into_mihomo(node)
if not node then
return false
end
if node.type == "ssr" or node.type == "ss" or node.type == "ss-rust" then
return true
end
if node.type == "v2ray" and (
node.v2ray_protocol == "vmess"
or node.v2ray_protocol == "vless"
or node.v2ray_protocol == "shadowsocks"
or node.v2ray_protocol == "trojan"
or node.v2ray_protocol == "hysteria2"
or node.v2ray_protocol == "hy2"
or node.v2ray_protocol == "snell"
) then
return true
end
if node.type == "tuic" then
return true
end
if node.type == "anytls" then
return true
end
return false
end
local function buildMihomoSubscribeYaml(nodes, group_name)
local proxies = {}
local names = {}
for _, node in ipairs(nodes) do
local proxy = to_mihomo_proxy(node)
if proxy then
proxies[#proxies + 1] = proxy
names[#names + 1] = proxy.name
end
end
if #proxies == 0 then
return nil, 0
end
local doc = {
mode = "rule",
["log-level"] = "silent",
proxies = proxies,
["proxy-groups"] = {
{
name = group_name,
type = "select",
proxies = names
}
},
rules = { "MATCH," .. group_name }
}
local ok_lyaml, lyaml = pcall(require, "lyaml")
if ok_lyaml then
local ok_dump, dumped = pcall(lyaml.dump, { doc })
if ok_dump and dumped then
return dumped, #proxies
end
end
return nil, 0
end
local function processLocalClashSubscription(path, alias)
local result = {
type = "clash",
@@ -499,7 +891,7 @@ local function processData(szType, content, cfgid)
local url = URL.parse("http://" .. content)
local params = url.query
if not has_xray then
if not has_xray and not has_mihomo then
return nil
end
@@ -562,6 +954,30 @@ local function processData(szType, content, cfgid)
-- 处理 fast open 参数
result.fast_open = params.tfo
end
elseif szType == "snell" then
if not has_mihomo then
log("跳过 Snell 节点:本地未安装 mihomo。")
return nil
end
local url = URL.parse("http://" .. content)
local params = url.query or {}
local raw_alias = url.fragment and UrlDecode(url.fragment) or nil
local psk = url.user and UrlDecode(url.user) or first_nonempty(params, {"psk", "password"})
result.type = "v2ray"
result.v2ray_protocol = "snell"
result.raw_alias = raw_alias
result.alias = raw_alias
result.server = normalize_host(url.host)
result.server_port = url.port
result.snell_psk = psk
result.snell_version = first_nonempty(params, {"version", "v"}) or "4"
result.snell_obfs = first_nonempty(params, {"obfs", "obfs-mode", "obfs_mode"})
result.snell_obfs_host = first_nonempty(params, {"obfs-host", "obfs_host", "host"})
if not result.snell_psk or result.snell_psk == "" then
result.server = nil
end
elseif szType == 'ssr' then
-- 去掉前后空白和#注释
local link = trim(content:gsub("#.*$", ""))
@@ -1168,7 +1584,7 @@ local function processData(szType, content, cfgid)
end
-- 自动决定模式(true=Xray, false=普通 Trojan)
if not has_xray then
if not has_xray and not has_mihomo then
return nil
end
@@ -1575,6 +1991,56 @@ local function curl(url, user_agent)
return stdout, md5
end
local function build_convert_url(url)
if sub_convert ~= "1" then
return url, false
end
local tpl = trim(template_url or "")
local endpoint = trim(convert_address or "")
local exclude = trim(filter_words or "")
if tpl == "" or endpoint == "" then
return url, false
end
endpoint = endpoint:gsub("%s+$", ""):gsub("^%s+", ""):gsub("%z", ""):gsub("[\r\n]", "")
local sep = endpoint:find("?", 1, true) and "&" or "?"
local query = "target=clash"
.. "&new_name=true"
.. "&url=" .. urlEncodeRFC3986(url)
.. "&config=" .. urlEncodeRFC3986(tpl)
.. "&exclude=" .. urlEncodeRFC3986(exclude)
.. "&emoji=false"
.. "&list=false"
.. "&sort=false"
.. "&udp=true"
.. "&scv=" .. (allow_insecure == "1" and "true" or "false")
.. "&fdn=true"
return endpoint .. sep .. query, true
end
local function filterClashYamlRaw(raw)
if not raw or raw == "" then
return raw, 0
end
local tmp = string.format("/tmp/ssrplus-subscribe-filter-%d-%d.yaml", nixio.getpid(), os.time())
local ok = nixio.fs.writefile(tmp, raw)
if not ok then
return raw, 0
end
local removed = tonumber(trim(luci.sys.exec(string.format(
"/usr/bin/lua /usr/share/shadowsocksr/clash_yaml.lua filter %s %s 2>/dev/null",
shell_quote(tmp),
shell_quote(filter_words)
)))) or 0
local filtered = nixio.fs.readfile(tmp) or raw
nixio.fs.remove(tmp)
return filtered, removed
end
local function collect_wan_interfaces()
local ifaces = {}
local seen = {}
@@ -1845,6 +2311,7 @@ end
-- 加载订阅未变化的节点用于防止被误删
local function loadOldNodes(groupHash)
local nodes = {}
local count = 0
cache[groupHash] = {}
nodeResult[#nodeResult + 1] = nodes
local index = #nodeResult
@@ -1854,8 +2321,22 @@ local function loadOldNodes(groupHash)
local section = setmetatable({}, {__index = s})
nodes[s.hashkey] = section
cache[groupHash][s.hashkey] = section
count = count + 1
end
end)
return count
end
local function group_has_saved_nodes(groupHash)
local found = false
ucic:foreach(name, uciType, function(s)
if s.grouphashkey == groupHash and s.hashkey then
found = true
return false
end
end)
return found
end
local function get_section_ss_backend(section)
@@ -1901,6 +2382,58 @@ local function group_needs_ss_backend_refresh(groupHash)
return has_ss_node and needs_refresh
end
local function is_mihomo_subscribe_node(section)
if not section then
return false
end
if section.type == "clash" and section.clash_path and tostring(section.clash_path):match("%.mihomo%.yaml$") then
return true
end
if section.type == "ssr" then
return true
end
if section.type == "ss" or section.type == "ss-rust" then
return true
end
if section.type == "v2ray" and (
section.v2ray_protocol == "vmess"
or section.v2ray_protocol == "vless"
or section.v2ray_protocol == "shadowsocks"
or section.v2ray_protocol == "trojan"
or section.v2ray_protocol == "hysteria2"
or section.v2ray_protocol == "hy2"
or section.v2ray_protocol == "snell"
) then
return true
end
if section.type == "tuic" then
return true
end
return false
end
local function group_needs_mihomo_subscribe_refresh(groupHash)
if not has_mihomo then
return false
end
local needs_refresh = false
ucic:foreach(name, uciType, function(s)
if s.grouphashkey == groupHash then
if is_mihomo_subscribe_node(s) and s.type ~= "clash" then
needs_refresh = true
return false
end
if s.type == "clash" and s.clash_path and tostring(s.clash_path):match("%.anytls%.yaml$") then
needs_refresh = true
return false
end
end
end)
return needs_refresh
end
local function preserve_unselected_groups(selected_hashes)
local preserved = {}
@@ -1918,7 +2451,8 @@ local execute = function()
local selected_hashes = {}
for _, item in ipairs(subscribe_items) do
selected_hashes[md5(item.url)] = true
local fetch_url = build_convert_url(item.url)
selected_hashes[md5(fetch_url)] = true
end
if target_subscribe_sid ~= "" then
@@ -1927,21 +2461,38 @@ local execute = function()
for _, item in ipairs(subscribe_items) do
local url = item.url
local raw, new_md5 = curl(url, user_agent)
local fetch_url, converted = build_convert_url(url)
local raw, new_md5 = curl(fetch_url, user_agent)
local yaml_removed_count = 0
if isClashYAML(raw) then
raw, yaml_removed_count = filterClashYamlRaw(raw)
if yaml_removed_count and yaml_removed_count > 0 then
new_md5 = md5_string(raw)
end
end
log("raw 长度: "..#raw)
local groupHash = md5(url)
local groupHash = md5(fetch_url)
local old_md5 = read_old_md5(groupHash)
log("处理订阅: " .. url)
if converted then
log("使用订阅转换模板: " .. template_url)
log("转换服务: " .. convert_address)
end
if yaml_removed_count and yaml_removed_count > 0 then
log("Clash/Mihomo YAML 已按订阅过滤关键词移除节点数量: " .. tostring(yaml_removed_count))
end
log("groupHash: " .. groupHash)
log("old_md5: " .. tostring(old_md5))
log("new_md5: " .. tostring(new_md5))
local backend_refresh = group_needs_ss_backend_refresh(groupHash)
local mihomo_subscribe_refresh = group_needs_mihomo_subscribe_refresh(groupHash)
local missing_saved_nodes = old_md5 and new_md5 == old_md5 and not group_has_saved_nodes(groupHash)
if #raw == 0 then
log(url .. ': 获取内容为空')
loadOldNodes(groupHash)
elseif old_md5 and new_md5 == old_md5 and not backend_refresh then
elseif old_md5 and new_md5 == old_md5 and not backend_refresh and not mihomo_subscribe_refresh and not missing_saved_nodes then
log("订阅未变化, 跳过无需更新的订阅: " .. url)
-- 防止 diff 阶段误删未更新订阅节点
loadOldNodes(groupHash)
@@ -1955,6 +2506,12 @@ local execute = function()
if backend_refresh and old_md5 and new_md5 == old_md5 then
log("检测到 SS 后端偏好变化,强制重建订阅节点: " .. url)
end
if mihomo_subscribe_refresh and old_md5 and new_md5 == old_md5 then
log("检测到可迁移订阅节点,强制重建为 Mihomo 总节点: " .. url)
end
if missing_saved_nodes then
log("订阅内容未变化但本地节点不存在,强制重建订阅节点: " .. url)
end
updated = true
-- 保存更新后的 MD5 值到以 groupHash 为标识的临时文件中,用于下次订阅更新时进行对比
write_new_md5(groupHash, new_md5)
@@ -1967,7 +2524,7 @@ local execute = function()
if isClashYAML(raw) then
is_clash_subscription = true
local result = processClashSubscription(url)
local result = processClashSubscription(fetch_url)
if result and not check_filer(result) and not cache[groupHash][result.hashkey] then
result.grouphashkey = groupHash
table.insert(nodeResult[index], result)
@@ -2014,7 +2571,7 @@ local execute = function()
if line:match("^anytls://") then
local parsed = parseAnytlsShare(line:gsub("^anytls://", ""))
if parsed then
table.insert(anytls_nodes, parsed)
table.insert(anytls_nodes, anytls_to_mihomo_node(parsed))
end
elseif line ~= "" then
table.insert(normal_nodes, node)
@@ -2022,19 +2579,8 @@ local execute = function()
end
if #anytls_nodes > 0 then
local parsed_url = URL.parse(url)
local alias = "Clash_" .. (parsed_url.host or groupHash)
local local_path = string.format("%s/%s.anytls.yaml", local_clash_dir, groupHash)
local yaml = buildAnytlsClashYaml(anytls_nodes, "Proxy")
nixio.fs.mkdirr(local_clash_dir)
nixio.fs.writefile(local_path, yaml)
local result = processLocalClashSubscription(local_path, alias)
if result and not cache[groupHash][result.hashkey] then
result.grouphashkey = groupHash
table.insert(nodeResult[index], result)
cache[groupHash][result.hashkey] = result
log('成功导入 AnyTLS 转 Clash 总节点: ' .. result.alias)
for _, parsed in ipairs(anytls_nodes) do
table.insert(normal_nodes, parsed)
end
end
@@ -2046,10 +2592,12 @@ local execute = function()
if not is_clash_subscription then
for _, v in ipairs(nodes) do
if v and not string.match(v, "^%s*$") then
if v and (type(v) == "table" or not string.match(v, "^%s*$")) then
xpcall(function()
local result
if szType then
if type(v) == "table" then
result = v
elseif szType then
result = processData(szType, v)
elseif not szType then
local node = trim(v)
@@ -2061,7 +2609,7 @@ local execute = function()
if dat[3] then
dat3 = "://" .. dat[3]
end
if dat[1] == 'ss' or dat[1] == 'trojan' or dat[1] == 'tuic' then
if dat[1] == 'ss' or dat[1] == 'trojan' or dat[1] == 'tuic' or dat[1] == 'snell' then
result = processData(dat[1], dat[2] .. dat3)
else
result = processData(dat[1], base64Decode(dat[2]))
@@ -2110,7 +2658,44 @@ local execute = function()
end
-- 清理临时字段
node.raw_alias = nil
-- 存入 nodeResult
end
local mihomo_nodes = {}
local legacy_nodes = {}
for _, node in ipairs(groupRawNodes) do
if has_mihomo and can_group_into_mihomo(node) then
mihomo_nodes[#mihomo_nodes + 1] = node
else
legacy_nodes[#legacy_nodes + 1] = node
end
end
if #mihomo_nodes > 0 then
local parsed_url = URL.parse(url)
local alias = item.alias ~= "" and item.alias or ("Mihomo_" .. ((parsed_url and parsed_url.host) or groupHash))
local local_path = string.format("%s/%s.mihomo.yaml", local_clash_dir, groupHash)
local yaml, proxy_count = buildMihomoSubscribeYaml(mihomo_nodes, "Proxy")
if yaml and proxy_count > 0 then
nixio.fs.mkdirr(local_clash_dir)
nixio.fs.writefile(local_path, yaml)
local result = processLocalClashSubscription(local_path, alias)
if result and not cache[groupHash][result.hashkey] then
result.grouphashkey = groupHash
table.insert(nodeResult[index], result)
cache[groupHash][result.hashkey] = result
log('成功导入 Mihomo 总节点: ' .. result.alias .. ',包含节点数量: ' .. proxy_count)
end
else
log('Mihomo 总节点生成失败,回退为普通订阅节点。')
for _, node in ipairs(mihomo_nodes) do
legacy_nodes[#legacy_nodes + 1] = node
end
end
end
for _, node in ipairs(legacy_nodes) do
node.grouphashkey = groupHash
table.insert(nodeResult[index], node)
cache[groupHash][node.hashkey] = node
+2 -2
View File
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow
PKG_VERSION:=20260531
PKG_RELEASE:=16
PKG_RELEASE:=17
PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=804f71f7e21827961ac35469b97c2cddc7c381cb
PKG_SOURCE_VERSION:=e6ed6e88085c8e724f17212de66bd8ca3571a0bf
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
PKG_LICENSE:=GPL-2.0