🏅 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 6d4cdd8118
commit 4b85448aee
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_NAME:=dae
PKG_VERSION:=2026.06.14 PKG_VERSION:=2026.06.14
PKG_RELEASE:=10 PKG_RELEASE:=11
PKG_SOURCE:=dae-src-2026.06.14-fa89ea7197a5.tar.gz PKG_SOURCE:=dae-src-2026.06.14-fa89ea7197a5.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
+13
View File
@@ -46,6 +46,19 @@ start_service() {
stop_service() { stop_service() {
rm -rf "$LOG_DIR" 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() { service_triggers() {
+1 -1
View File
@@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=daed PKG_NAME:=daed
PKG_VERSION:=2026.06.14 PKG_VERSION:=2026.06.14
PKG_RELEASE:=11 PKG_RELEASE:=12
PKG_SOURCE:=daed-src-2026.06.14-4e215048068a.tar.gz PKG_SOURCE:=daed-src-2026.06.14-4e215048068a.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src 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() { stop_service() {
rm -f "$LOG" rm -f "$LOG"
# daed creates /run/netns/daens on start; a stale entry makes the next # kill lingering procs in daens: TERM first, then KILL for stubborn ones
# start fail with "failed to create netns: ... file exists". Clean it here. for pid in $(ip netns pids daens 2>/dev/null); do kill "$pid" 2>/dev/null; done
ip netns del daens 2>/dev/null || true 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() { service_triggers() {
+25 -22
View File
@@ -2,38 +2,40 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=gecoosac PKG_NAME:=gecoosac
PKG_VERSION:=2.2.20251015 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_ARCH:=ac_linux_arm64
PKG_HASH:=skip PKG_HASH:=skip
else ifeq ($(ARCH),arm) else ifneq (,$(filter arm%,$(ARCH_ID)))
PKG_ARCH:=ac_linux_arm PKG_ARCH:=ac_linux_arm
PKG_HASH:=skip PKG_HASH:=skip
else ifeq ($(ARCH),mips) else ifneq (,$(filter mipsel%,$(ARCH_ID)))
PKG_ARCH:=ac_linux_mips
PKG_HASH:=skip
else ifeq ($(ARCH),mipsel)
PKG_ARCH:=ac_linux_mipsle PKG_ARCH:=ac_linux_mipsle
PKG_HASH:=skip PKG_HASH:=skip
else ifeq ($(ARCH),i386) else ifneq (,$(filter mips%,$(ARCH_ID)))
PKG_ARCH:=ac_linux_386 PKG_ARCH:=ac_linux_mips
PKG_HASH:=skip PKG_HASH:=skip
else ifeq ($(ARCH),x86_64) else ifneq (,$(filter i386%,$(ARCH_ID)))
PKG_ARCH:=ac_linux_amd64 PKG_ARCH:=ac_linux_386
PKG_HASH:=skip PKG_HASH:=skip
else else
PKG_HASH:=skip PKG_HASH:=skip
endif 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)? 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) 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 include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME) define Package/$(PKG_NAME)
@@ -59,14 +61,15 @@ endef
define Package/$(PKG_NAME)/install define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin $(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_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/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 endef
$(eval $(call BuildPackage,$(PKG_NAME))) $(eval $(call BuildPackage,$(PKG_NAME)))
+8 -3
View File
@@ -318,6 +318,8 @@ ensure_dirs() {
} }
start_service() { start_service() {
local runtime_upload_dir runtime_db_dir runtime_piddir
init_conf init_conf
normalize_conf normalize_conf
@@ -333,6 +335,9 @@ start_service() {
fi fi
ensure_dirs || return 1 ensure_dirs || return 1
runtime_upload_dir="${upload_dir%/}/"
runtime_db_dir="${db_dir%/}/"
runtime_piddir="${piddir%/}/"
if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then
if [ "$crt_file" = "$DEFAULT_CRT_FILE" ] && [ "$key_file" = "$DEFAULT_KEY_FILE" ]; then if [ "$crt_file" = "$DEFAULT_CRT_FILE" ] && [ "$key_file" = "$DEFAULT_KEY_FILE" ]; then
@@ -352,13 +357,13 @@ start_service() {
procd_open_instance gecoosac procd_open_instance gecoosac
procd_set_param command "$PROG" 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 -p "$port"
procd_append_param command -debug "$debug" 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 -token "1"
procd_append_param command -lang "$lang" procd_append_param command -lang "$lang"
procd_append_param command -piddir "$piddir" procd_append_param command -piddir "$runtime_piddir"
if [ "$isonlyoneprot" = "0" ]; then if [ "$isonlyoneprot" = "0" ]; then
procd_append_param command -isonlyoneprot "0" procd_append_param command -isonlyoneprot "0"
procd_append_param command -mp "$m_port" procd_append_param command -mp "$m_port"
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-daede PKG_NAME:=luci-app-daede
PKG_VERSION:=1.14.1 PKG_VERSION:=1.14.6
PKG_RELEASE:=20 PKG_RELEASE:=21
PKG_MAINTAINER:=kenzok8 PKG_MAINTAINER:=kenzok8
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
@@ -2,6 +2,7 @@
'use strict'; 'use strict';
'require uci'; 'require uci';
'require network';
'require view'; 'require view';
'require view.daede.backend as backend'; 'require view.daede.backend as backend';
'require view.daede.styles as styles'; 'require view.daede.styles as styles';
@@ -13,7 +14,13 @@ return view.extend({
_loadContext: function() { _loadContext: function() {
return backend.detectBackend().then(function(ctx) { return backend.detectBackend().then(function(ctx) {
return uci.load(ctx.backend.uci).catch(function() {}).then(function() { 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 /* Logging — folded into the main form so the single Save button covers it
too (no separate native save bar) */ 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 = m.section(form.NamedSection, 'config', 'dae', _('Logging'));
s.addremove = false; s.addremove = false;
o = s.option(form.Value, 'log_maxsize', _('Max Log Size (MB)'), o = s.option(form.Value, 'log_maxsize', _('Max Log Size (MB)'),
@@ -33,7 +33,7 @@ function renderDaedSettings() {
_('daede Settings'), _('daede Settings'),
null, null,
m.render(), m.render(),
_('Log Advanced Settings'), _('Log Settings'),
['log_maxsize', 'log_maxbackups'] ['log_maxsize', 'log_maxbackups']
).then(function(card) { ).then(function(card) {
/* native Save/Apply footer is suppressed view-wide, so daed carries its /* 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}', '.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; /* themes (esp. Argon) restyle checkboxes into faint, near-invisible boxes;
force native rendering + brand accent so the checkmark is always clear */ 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}', '.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 /* line the Subscriptions and Nodes grid tables up column-for-column. fixed
layout + explicit per-column percentages make the widths authoritative 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-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,.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,.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 select{padding-right:28px!important}',
'.dd-converter .cbi-button{padding:4px 12px!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}', '.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{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 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%}', '.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 */ /* selects cap at 200px on wide screens, shrink to the column on mobile (no overflow) */
'.dd-geo-row select{width:180px;flex:0 0 auto}', '.dd-geo-row select{width:100%!important;max-width:200px;box-sizing:border-box}',
'.dd-geo-chk{display:flex;align-items:center;gap:6px}', '.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-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{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}', '.dd-up-log.show{display:block}',
@@ -473,8 +475,8 @@ return view.extend({
E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, _('Source')), presetSel ]), E('div', { 'class': 'dd-geo-row' }, [ E('label', {}, _('Source')), presetSel ]),
customRows, customRows,
E('div', { 'class': 'dd-geo-row' }, [ E('div', { 'class': 'dd-geo-row' }, [
E('label', {}, _('Auto-update')), E('label', { 'class': 'dd-geo-auto' }, [ E('span', {}, _('Auto-update')), autoCb ]),
E('div', { 'class': 'dd-geo-chk' }, [ autoCb, freqSel ]) freqSel
]), ]),
E('div', { 'class': 'dd-geo-actions' }, [ saveBtn ]) E('div', { 'class': 'dd-geo-actions' }, [ saveBtn ])
]) ])
+20 -2
View File
@@ -117,8 +117,8 @@ msgstr "保留旧日志数"
msgid "Number of rotated log files to keep." msgid "Number of rotated log files to keep."
msgstr "轮转后保留的旧日志文件数。" msgstr "轮转后保留的旧日志文件数。"
msgid "Log Advanced Settings" msgid "Log Settings"
msgstr "日志高级设置" msgstr "日志设置"
# ---- config.js: dae Settings ---- # ---- config.js: dae Settings ----
msgid "dae Parameters" msgid "dae Parameters"
@@ -1014,3 +1014,21 @@ msgid ""
"%s is running. Deleting the daens netns now will break its networking until " "%s is running. Deleting the daens netns now will break its networking until "
"you restart it. Continue?" "you restart it. Continue?"
msgstr "%s 正在运行。现在删除 daens netns 会中断其网络,直到重启它。确定继续?" 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." msgid "Number of rotated log files to keep."
msgstr "轮转后保留的旧日志文件数。" msgstr "轮转后保留的旧日志文件数。"
msgid "Log Advanced Settings" msgid "Log Settings"
msgstr "日志高级设置" msgstr "日志设置"
# ---- config.js: dae Settings ---- # ---- config.js: dae Settings ----
msgid "dae Parameters" msgid "dae Parameters"
@@ -1014,3 +1014,21 @@ msgid ""
"%s is running. Deleting the daens netns now will break its networking until " "%s is running. Deleting the daens netns now will break its networking until "
"you restart it. Continue?" "you restart it. Continue?"
msgstr "%s 正在运行。现在删除 daens netns 会中断其网络,直到重启它。确定继续?" 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_NAME:=luci-app-passwall
PKG_VERSION:=26.6.2 PKG_VERSION:=26.6.2
PKG_RELEASE:=162 PKG_RELEASE:=163
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -905,7 +905,7 @@ add_firewall_rule() {
#echolog "使用缓存加载chnroute..." #echolog "使用缓存加载chnroute..."
nft -f $RULES_PATH/chnroute.nft nft -f $RULES_PATH/chnroute.nft
else 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 fi
gen_nftset $NFTSET_BLACK ipv4_addr "2d" 0 gen_nftset $NFTSET_BLACK ipv4_addr "2d" 0
gen_nftset $NFTSET_WHITE ipv4_addr "2d" 0 gen_nftset $NFTSET_WHITE ipv4_addr "2d" 0
@@ -921,7 +921,7 @@ add_firewall_rule() {
#echolog "使用缓存加载chnroute6..." #echolog "使用缓存加载chnroute6..."
nft -f $RULES_PATH/chnroute6.nft nft -f $RULES_PATH/chnroute6.nft
else 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 fi
gen_nftset $NFTSET_BLACK6 ipv6_addr "2d" 0 gen_nftset $NFTSET_BLACK6 ipv6_addr "2d" 0
gen_nftset $NFTSET_WHITE6 ipv6_addr "2d" 0 gen_nftset $NFTSET_WHITE6 ipv6_addr "2d" 0
@@ -957,8 +957,8 @@ add_firewall_rule() {
#直连列表 #直连列表
[ "$USE_DIRECT_LIST_ALL" = "1" ] && { [ "$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 | 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 | 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 "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_WHITE6 "0"
[ "$USE_GEOVIEW" = "1" ] && { [ "$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') 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 if [ -n "$GEOIP_CODE" ]; then
@@ -971,8 +971,8 @@ add_firewall_rule() {
#代理列表 #代理列表
[ "$USE_PROXY_LIST_ALL" = "1" ] && { [ "$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 | 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 | 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 "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLACK6 "0"
[ "$USE_GEOVIEW" = "1" ] && { [ "$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') 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 if [ -n "$GEOIP_CODE" ]; then
@@ -985,8 +985,8 @@ add_firewall_rule() {
#屏蔽列表 #屏蔽列表
[ "$USE_BLOCK_LIST_ALL" = "1" ] && { [ "$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 | 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 | 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 "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_BLOCK6 "0"
[ "$USE_GEOVIEW" = "1" ] && { [ "$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') 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 if [ -n "$GEOIP_CODE" ]; then
@@ -1002,8 +1002,8 @@ add_firewall_rule() {
local GEOIP_CODE="" local GEOIP_CODE=""
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}') local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
for shunt_id in $shunt_ids; do 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 | 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 | 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 "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6 "0"
[ "$USE_GEOVIEW" = "1" ] && { [ "$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') 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" [ -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_NAME:=luci-app-run
PKG_VERSION:=1.0.0 PKG_VERSION:=1.0.0
PKG_RELEASE:=3 PKG_RELEASE:=4
LUCI_TITLE:=LuCI support for running makeself .run installers LUCI_TITLE:=LuCI support for running makeself .run installers
LUCI_DESCRIPTION:=Upload and execute makeself-generated .run installers from LuCI. LUCI_DESCRIPTION:=Upload and execute makeself-generated .run installers from LuCI.
@@ -11,8 +11,14 @@ LUCI_PKGARCH:=all
PKG_MAINTAINER:=wukongdaily <2666180@gmail.com> 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 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' method: 'status'
}); });
var getVersion = rpc.declare({
object: 'luci-app-run',
method: 'version'
});
var readLog = rpc.declare({ var readLog = rpc.declare({
object: 'luci-app-run', object: 'luci-app-run',
method: 'read_log', method: 'read_log',
@@ -140,10 +145,29 @@ return view.extend({
logOffset: 0, logOffset: 0,
currentUploadId: null, currentUploadId: null,
appVersion: 'unknown',
load: function () { load: function () {
return getStatus().catch(function () { var self = this;
return {}; 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', { var runButton = E('button', {
class: 'btn cbi-button cbi-button-action', class: 'btn cbi-button cbi-button-action',
disabled: true, disabled: true,
style: 'min-width:140px;padding:8px 32px;', style: 'min-width:140px;margin-left:15px;',
click: function (ev) { click: function (ev) {
ev.preventDefault(); ev.preventDefault();
self.startRun(runButton, state); self.startRun(runButton, state);
@@ -227,7 +251,7 @@ return view.extend({
}, [ }, [
E('h3', [_('upload_title')]), E('h3', [_('upload_title')]),
E('p', [state]), E('p', [state]),
E('p', [pickButton, ' ', runButton, cleanButton]), E('p', [pickButton, runButton, cleanButton]),
progress, progress,
fileInput fileInput
]); ]);
+257 -246
View File
@@ -13,83 +13,83 @@ umask 077
mkdir -p "$UPLOAD_DIR" "$RUN_DIR" mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
reply() { reply() {
printf '%s\n' "$1" printf '%s\n' "$1"
} }
json_error() { json_error() {
local msg="$1" local msg="$1"
json_init json_init
json_add_int code 1 json_add_int code 1
json_add_string error "$msg" json_add_string error "$msg"
json_dump json_dump
} }
json_ok() { json_ok() {
json_init json_init
json_add_int code 0 json_add_int code 0
[ -n "$1" ] && json_add_string message "$1" [ -n "$1" ] && json_add_string message "$1"
json_dump json_dump
} }
sanitize_name() { sanitize_name() {
local name="$1" local name="$1"
name="${name##*/}" name="${name##*/}"
name="${name##*\\}" name="${name##*\\}"
printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_' printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_'
} }
valid_id() { valid_id() {
case "$1" in case "$1" in
""|*[!A-Za-z0-9_-]*) return 1 ;; ""|*[!A-Za-z0-9_-]*) return 1 ;;
esac esac
return 0 return 0
} }
load_input() { load_input() {
local input local input
input="$(cat)" input="$(cat)"
json_load "$input" >/dev/null 2>&1 json_load "$input" >/dev/null 2>&1
} }
get_string() { get_string() {
json_get_var "$1" "$1" json_get_var "$1" "$1"
} }
get_int() { get_int() {
json_get_var "$1" "$1" json_get_var "$1" "$1"
} }
new_token() { new_token() {
local token local token
token="$(hexdump -n 16 -e '16/1 "%02x"' /dev/urandom 2>/dev/null)" 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)}')" [ -n "$token" ] || token="$(date +%s)_$$_$(awk 'BEGIN{srand();print int(rand()*1000000)}')"
printf '%s' "$token" | tr -c 'A-Za-z0-9_' '_' printf '%s' "$token" | tr -c 'A-Za-z0-9_' '_'
} }
session_dir() { session_dir() {
printf '%s/%s' "$UPLOAD_DIR" "$1" printf '%s/%s' "$UPLOAD_DIR" "$1"
} }
write_state() { write_state() {
local pid="$1" local pid="$1"
local file="$2" local file="$2"
local log="$3" local log="$3"
local started="$4" local started="$4"
{ {
printf 'PID=%s\n' "$pid" printf 'PID=%s\n' "$pid"
printf 'FILE=%s\n' "$file" printf 'FILE=%s\n' "$file"
printf 'LOG=%s\n' "$log" printf 'LOG=%s\n' "$log"
printf 'STARTED=%s\n' "$started" printf 'STARTED=%s\n' "$started"
} > "$STATE_FILE" } > "$STATE_FILE"
} }
read_state() { read_state() {
[ -f "$STATE_FILE" ] && . "$STATE_FILE" [ -f "$STATE_FILE" ] && . "$STATE_FILE"
} }
is_running() { is_running() {
local pid="$1" local pid="$1"
[ -n "$pid" ] && [ -d "/proc/$pid" ] [ -n "$pid" ] && [ -d "/proc/$pid" ]
} }
method_list() { method_list() {
@@ -114,235 +114,246 @@ method_list() {
"read_log": { "read_log": {
"offset": "Integer" "offset": "Integer"
}, },
"cleanup": {} "cleanup": {},
"version": {}
} }
JSON JSON
} }
upload_start() { upload_start() {
load_input load_input
get_string filename get_string filename
get_int size get_int size
[ -n "$filename" ] || { json_error "Missing filename"; return; } [ -n "$filename" ] || { json_error "Missing filename"; return; }
[ -n "$size" ] || size=0 [ -n "$size" ] || size=0
[ "$size" -le "$MAX_SIZE" ] 2>/dev/null || { json_error "File is larger than 256 MiB"; return; } [ "$size" -le "$MAX_SIZE" ] 2>/dev/null || { json_error "File is larger than 256 MiB"; return; }
local clean id token dir file local clean id token dir file
clean="$(sanitize_name "$filename")" clean="$(sanitize_name "$filename")"
case "$clean" in case "$clean" in
*.run|*.sh) ;; *.run|*.sh) ;;
*) json_error "Only .run and .sh files are accepted"; return ;; *) json_error "Only .run and .sh files are accepted"; return ;;
esac esac
id="$(date +%s)_$$" id="$(date +%s)_$$"
token="$(new_token)" token="$(new_token)"
dir="$(session_dir "$id")" dir="$(session_dir "$id")"
file="$dir/$clean" file="$dir/$clean"
mkdir -p "$dir" || { json_error "Unable to create upload directory"; return; } mkdir -p "$dir" || { json_error "Unable to create upload directory"; return; }
: > "$file" || { json_error "Unable to create upload file"; return; } : > "$file" || { json_error "Unable to create upload file"; return; }
printf '%s\n' "$clean" > "$dir/name" printf '%s\n' "$clean" > "$dir/name"
printf '%s\n' "$size" > "$dir/size" printf '%s\n' "$size" > "$dir/size"
printf '%s\n' "$token" > "$dir/token" printf '%s\n' "$token" > "$dir/token"
json_init json_init
json_add_int code 0 json_add_int code 0
json_add_string id "$id" json_add_string id "$id"
json_add_string token "$token" json_add_string token "$token"
json_add_string filename "$clean" json_add_string filename "$clean"
json_dump json_dump
} }
upload_chunk() { upload_chunk() {
load_input load_input
get_string id get_string id
get_string data get_string data
get_int index get_int index
valid_id "$id" || { json_error "Invalid upload id"; return; } valid_id "$id" || { json_error "Invalid upload id"; return; }
[ -n "$data" ] || { json_error "Missing chunk data"; return; } [ -n "$data" ] || { json_error "Missing chunk data"; return; }
local dir name file local dir name file
dir="$(session_dir "$id")" dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; } [ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)" name="$(cat "$dir/name" 2>/dev/null)"
file="$dir/$name" file="$dir/$name"
printf '%s' "$data" | base64 -d >> "$file" 2>/dev/null || { printf '%s' "$data" | base64 -d >> "$file" 2>/dev/null || {
json_error "Unable to decode upload chunk" json_error "Unable to decode upload chunk"
return return
} }
json_init json_init
json_add_int code 0 json_add_int code 0
json_add_int index "$index" json_add_int index "$index"
json_add_int received "$(wc -c < "$file" 2>/dev/null)" json_add_int received "$(wc -c < "$file" 2>/dev/null)"
json_dump json_dump
} }
upload_finish() { upload_finish() {
load_input load_input
get_string id get_string id
valid_id "$id" || { json_error "Invalid upload id"; return; } valid_id "$id" || { json_error "Invalid upload id"; return; }
local dir name file expected actual local dir name file expected actual
dir="$(session_dir "$id")" dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; } [ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)" name="$(cat "$dir/name" 2>/dev/null)"
expected="$(cat "$dir/size" 2>/dev/null)" expected="$(cat "$dir/size" 2>/dev/null)"
file="$dir/$name" file="$dir/$name"
actual="$(wc -c < "$file" 2>/dev/null)" actual="$(wc -c < "$file" 2>/dev/null)"
[ -s "$file" ] || { json_error "Uploaded file is empty"; return; } [ -s "$file" ] || { json_error "Uploaded file is empty"; return; }
if [ -n "$expected" ] && [ "$expected" -gt 0 ] 2>/dev/null; then if [ -n "$expected" ] && [ "$expected" -gt 0 ] 2>/dev/null; then
[ "$actual" -eq "$expected" ] 2>/dev/null || { [ "$actual" -eq "$expected" ] 2>/dev/null || {
json_error "Upload size mismatch" json_error "Upload size mismatch"
return return
} }
fi fi
chmod 700 "$file" || { json_error "Unable to chmod uploaded file"; return; } chmod 700 "$file" || { json_error "Unable to chmod uploaded file"; return; }
json_init json_init
json_add_int code 0 json_add_int code 0
json_add_string id "$id" json_add_string id "$id"
json_add_string filename "$name" json_add_string filename "$name"
json_add_int size "$actual" json_add_int size "$actual"
json_dump json_dump
} }
run_installer() { run_installer() {
load_input load_input
get_string id get_string id
valid_id "$id" || { json_error "Invalid upload id"; return; } valid_id "$id" || { json_error "Invalid upload id"; return; }
local dir name file log work pid now local dir name file log work pid now
dir="$(session_dir "$id")" dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; } [ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)" name="$(cat "$dir/name" 2>/dev/null)"
file="$dir/$name" file="$dir/$name"
# 确保文件可执行(.run 和 .sh 都需要) # 确保文件可执行(.run 和 .sh 都需要)
chmod 700 "$file" 2>/dev/null || true chmod 700 "$file" 2>/dev/null || true
[ -x "$file" ] || { [ -x "$file" ] || {
json_error "Uploaded file is not executable. Please check file permissions." json_error "Uploaded file is not executable. Please check file permissions."
return return
} }
read_state read_state
if is_running "$PID"; then if is_running "$PID"; then
json_error "Another installer is already running" json_error "Another installer is already running"
return return
fi fi
now="$(date '+%Y-%m-%d %H:%M:%S')" now="$(date '+%Y-%m-%d %H:%M:%S')"
work="$RUN_DIR/$id" work="$RUN_DIR/$id"
log="$work/output.log" log="$work/output.log"
mkdir -p "$work" || { json_error "Unable to create run directory"; return; } mkdir -p "$work" || { json_error "Unable to create run directory"; return; }
( (
cd "$work" || exit 1 cd "$work" || exit 1
printf 'luci-app-run: started %s\n' "$now" printf 'luci-app-run: started %s\n' "$now"
printf 'luci-app-run: executing %s\n\n' "$file" printf 'luci-app-run: executing %s\n\n' "$file"
"$file" "$file"
rc=$? rc=$?
printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')" printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
exit "$rc" exit "$rc"
) > "$log" 2>&1 & ) > "$log" 2>&1 &
pid="$!" pid="$!"
write_state "$pid" "$file" "$log" "$now" write_state "$pid" "$file" "$log" "$now"
json_init json_init
json_add_int code 0 json_add_int code 0
json_add_int pid "$pid" json_add_int pid "$pid"
json_add_string log "$log" json_add_string log "$log"
json_dump json_dump
} }
status() { status() {
read_state read_state
json_init json_init
json_add_int code 0 json_add_int code 0
if is_running "$PID"; then if is_running "$PID"; then
json_add_boolean running 1 json_add_boolean running 1
else else
json_add_boolean running 0 json_add_boolean running 0
fi fi
[ -n "$PID" ] && json_add_int pid "$PID" [ -n "$PID" ] && json_add_int pid "$PID"
[ -n "$FILE" ] && json_add_string file "$FILE" [ -n "$FILE" ] && json_add_string file "$FILE"
[ -n "$LOG" ] && json_add_string log "$LOG" [ -n "$LOG" ] && json_add_string log "$LOG"
[ -n "$STARTED" ] && json_add_string started "$STARTED" [ -n "$STARTED" ] && json_add_string started "$STARTED"
json_dump json_dump
} }
read_log() { read_log() {
load_input load_input
get_int offset get_int offset
[ -n "$offset" ] || offset=0 [ -n "$offset" ] || offset=0
read_state read_state
[ -n "$LOG" ] && [ -f "$LOG" ] || { [ -n "$LOG" ] && [ -f "$LOG" ] || {
json_init json_init
json_add_int code 0 json_add_int code 0
json_add_string data "" json_add_string data ""
json_add_int offset 0 json_add_int offset 0
json_add_boolean running 0 json_add_boolean running 0
json_dump json_dump
return return
} }
local size data start local size data start
size="$(wc -c < "$LOG" 2>/dev/null)" size="$(wc -c < "$LOG" 2>/dev/null)"
[ "$offset" -le "$size" ] 2>/dev/null || offset=0 [ "$offset" -le "$size" ] 2>/dev/null || offset=0
start=$((offset + 1)) start=$((offset + 1))
data="$(tail -c +"$start" "$LOG" 2>/dev/null)" data="$(tail -c +"$start" "$LOG" 2>/dev/null)"
json_init json_init
json_add_int code 0 json_add_int code 0
json_add_string data "$data" json_add_string data "$data"
json_add_int offset "$size" json_add_int offset "$size"
if is_running "$PID"; then if is_running "$PID"; then
json_add_boolean running 1 json_add_boolean running 1
else else
json_add_boolean running 0 json_add_boolean running 0
fi fi
json_dump json_dump
} }
cleanup() { cleanup() {
read_state read_state
if is_running "$PID"; then if is_running "$PID"; then
json_error "Cannot clean up while installer is running" json_error "Cannot clean up while installer is running"
return return
fi 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" version() {
mkdir -p "$UPLOAD_DIR" "$RUN_DIR" local ver="unknown"
json_ok "Cleaned" [ -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 case "$1" in
list) list)
method_list method_list
;; ;;
call) call)
case "$2" in case "$2" in
upload_start) upload_start ;; upload_start) upload_start ;;
upload_chunk) upload_chunk ;; upload_chunk) upload_chunk ;;
upload_finish) upload_finish ;; upload_finish) upload_finish ;;
run) run_installer ;; run) run_installer ;;
status) status ;; status) status ;;
read_log) read_log ;; read_log) read_log ;;
cleanup) cleanup ;; cleanup) cleanup ;;
*) json_error "Unknown method" ;; version) version ;;
esac *) json_error "Unknown method" ;;
;; esac
*) ;;
reply '{}' *)
;; reply '{}'
esac ;;
esac
+7 -7
View File
@@ -3,8 +3,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=luci-app-ssr-plus LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=193 PKG_VERSION:=195
PKG_RELEASE:=27 PKG_RELEASE:=28
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -34,15 +34,16 @@ LUCI_DEPENDS:= \
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \ +jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
+resolveip +curl +nping +unzip +xz-utils \ +resolveip +curl +nping +unzip +xz-utils \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \ +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_ChinaDNS_NG:chinadns-ng \
+PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS:mosdns \ +PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS:mosdns \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS:shadow-tls \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS:shadow-tls \
+PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun:kcptun-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun:kcptun-client \
+PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy:naiveproxy \ +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_Client:shadowsocks-rust-sslocal \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server:shadowsocks-rust-ssserver \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server:shadowsocks-rust-ssserver \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs:simple-obfs-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs:simple-obfs-client \
@@ -104,7 +105,6 @@ endchoice
choice choice
prompt "V2ray-core Selection" prompt "V2ray-core Selection"
default PACKAGE_$(PKG_NAME)_INCLUDE_Xray
default PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY default PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY
config 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_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_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", "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]] --[[Backup]]
entry({"admin", "services", "shadowsocksr", "backup"}, call("create_backup")).leaf = true entry({"admin", "services", "shadowsocksr", "backup"}, call("create_backup")).leaf = true
entry({'admin', 'services', "shadowsocksr", 'ip'}, call('check_ip')) -- 获取ip情况 entry({'admin', 'services', "shadowsocksr", 'ip'}, call('check_ip')) -- 获取ip情况
@@ -873,6 +875,13 @@ function add_subscribe_item()
return return
end 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.prepare_content("application/json")
luci.http.write_json({ ret = 1, sid = sid, alias = alias, enabled = "1" }) luci.http.write_json({ ret = 1, sid = sid, alias = alias, enabled = "1" })
end end
@@ -1320,6 +1329,84 @@ function clash_client_rule_clear()
}) })
end 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() function act_ping()
local e = {} local e = {}
local domain = luci.http.formvalue("domain") local domain = luci.http.formvalue("domain")
@@ -321,6 +321,10 @@ m.on_after_save = function(self)
end end
end end
local server_header = Template("/shadowsocksr/server_header")
server_header.section = sid
m:append(server_header)
-- [[ Servers Setting ]]-- -- [[ Servers Setting ]]--
s = m:section(NamedSection, sid, "servers") s = m:section(NamedSection, sid, "servers")
s.anonymous = true s.anonymous = true
@@ -417,6 +421,9 @@ end
if is_finded("xray") then if is_finded("xray") then
o:value("hysteria2", translate("Hysteria2")) o:value("hysteria2", translate("Hysteria2"))
end end
if is_finded("mihomo") then
o:value("snell", translate("Snell"))
end
o:value("socks", translate("Socks")) o:value("socks", translate("Socks"))
o:value("http", translate("HTTP")) o:value("http", translate("HTTP"))
o:depends("type", "v2ray") 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 = "shadowsocks"})
o:depends({type = "v2ray", v2ray_protocol = "trojan"}) 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")) o = s:option(ListValue, "encrypt_method", translate("Encrypt Method"))
for _, v in ipairs(encrypt_methods) do for _, v in ipairs(encrypt_methods) do
o:value(v) o:value(v)
@@ -946,6 +958,28 @@ o.rmempty = true
o.default = "5" o.default = "5"
o:depends({type = "v2ray", v2ray_protocol = "socks"}) 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 = s:option(ListValue, "transport", translate("Transport"))
o:value("raw", "RAW (TCP)") 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 if xray_version_val >= 260131 then
-- Xray 版本大于等于 26.1.31 -- Xray 版本大于等于 26.1.31
-- [[ Xray TLS pinSHA256 ]] -- -- [[ 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.rmempty = true
o:depends({type = "v2ray", tls = 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 ]] -- -- [[ 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.rmempty = true
o:depends({type = "v2ray", tls = 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 end
-- [[ Hysteria2 TLS pinSHA256 ]] -- -- [[ Hysteria2 TLS pinSHA256 ]] --
@@ -7,7 +7,7 @@ require "nixio.fs"
local m, s, o local m, s, o
local sid = arg[1] local sid = arg[1]
-- 加密方式(SS/SSR用) -- 加密方式(SS/SSR用)
local encrypt_methods = { local encrypt_methods = {
"rc4-md5", "rc4-md5-6", "rc4", "table", "rc4-md5", "rc4-md5-6", "rc4", "table",
"aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "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" "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
} }
-- Shadowsocks 加密方法(Xray 版) -- Shadowsocks 加密方法(Xray 版)
local v_ss_encrypt_method_list = { local v_ss_encrypt_method_list = {
"aes-128-cfb", "aes-256-cfb", "aes-128-gcm", "aes-256-gcm", "aes-128-cfb", "aes-256-cfb", "aes-128-gcm", "aes-256-gcm",
"chacha20", "chacha20-ietf", "chacha20-poly1305", "chacha20-ietf-poly1305" "chacha20", "chacha20-ietf", "chacha20-poly1305", "chacha20-ietf-poly1305"
} }
-- 伪装类型(用于 mKCP/QUIC) -- 伪装类型(用于 mKCP/QUIC)
local header_type_list = { local header_type_list = {
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard" "none", "srtp", "utp", "wechat-video", "dtls", "wireguard"
} }
@@ -50,7 +50,7 @@ s = m:section(NamedSection, sid, "server_config")
s.anonymous = true s.anonymous = true
s.addremove = false s.addremove = false
-- ========== 基本设置 ========== -- ========== 基本设置 ==========
o = s:option(Flag, "enable", translate("Enable")) o = s:option(Flag, "enable", translate("Enable"))
o.default = 1 o.default = 1
o.rmempty = false o.rmempty = false
@@ -63,12 +63,14 @@ end
if nixio.fs.access("/usr/bin/ssr-server") then if nixio.fs.access("/usr/bin/ssr-server") then
o:value("ssr", translate("ShadowsocksR")) o:value("ssr", translate("ShadowsocksR"))
end end
-- Xray 协议支持 -- Xray 协议支持
if nixio.fs.access("/usr/bin/xray") or nixio.fs.access("/usr/libexec/xray") then if nixio.fs.access("/usr/bin/mihomo") or nixio.fs.access("/usr/libexec/mihomo")
o:value("vmess", "VMess (Xray)") or nixio.fs.access("/usr/bin/xray") or nixio.fs.access("/usr/libexec/xray")
o:value("vless", "VLESS (Xray)") then
o:value("trojan", "Trojan (Xray)") o:value("vmess", "VMess (Mihomo/Xray)")
o:value("shadowsocks", "Shadowsocks (Xray)") o:value("vless", "VLESS (Mihomo/Xray)")
o:value("trojan", "Trojan (Mihomo/Xray)")
o:value("shadowsocks", "Shadowsocks (Mihomo/Xray)")
end end
o.default = "socks5" o.default = "socks5"
@@ -99,7 +101,7 @@ o:depends("type", "ssr")
o:depends("type", "trojan") o:depends("type", "trojan")
o:depends("type", "shadowsocks") o:depends("type", "shadowsocks")
-- ========== SS/SSR 协议特有 ========== -- ========== SS/SSR 协议特有 ==========
o = s:option(ListValue, "encrypt_method", translate("Encrypt Method")) o = s:option(ListValue, "encrypt_method", translate("Encrypt Method"))
for _, v in ipairs(encrypt_methods) do o:value(v) end for _, v in ipairs(encrypt_methods) do o:value(v) end
o.rmempty = false o.rmempty = false
@@ -128,8 +130,8 @@ o.rmempty = false
o:depends("type", "ss") o:depends("type", "ss")
o:depends("type", "ssr") o:depends("type", "ssr")
-- ========== Xray 协议通用字段 ========== -- ========== Xray 协议通用字段 ==========
-- 用户备注 -- 用户备注
o = s:option(Value, "remarks", translate("Remarks")) o = s:option(Value, "remarks", translate("Remarks"))
o.default = translate("Remarks") o.default = translate("Remarks")
o.rmempty = true o.rmempty = true
@@ -138,7 +140,7 @@ o:depends("type", "vless")
o:depends("type", "trojan") o:depends("type", "trojan")
o:depends("type", "shadowsocks") o:depends("type", "shadowsocks")
-- 用户等级 -- 用户等级
o = s:option(Value, "level", translate("User Level")) o = s:option(Value, "level", translate("User Level"))
o.datatype = "uinteger" o.datatype = "uinteger"
o.default = 1 o.default = 1
@@ -148,21 +150,21 @@ o:depends("type", "vless")
o:depends("type", "shadowsocks") o:depends("type", "shadowsocks")
o:depends("type", "trojan") o:depends("type", "trojan")
-- ========== Xray 协议认证字段 ========== -- ========== Xray 协议认证字段 ==========
-- UUID (用于 vmess/vless) -- UUID (用于 vmess/vless)
o = s:option(Value, "uuid", translate("UUID")) o = s:option(Value, "uuid", translate("UUID"))
o.description = translate("Required for VMess/VLESS. Generate with: uuidgen") o.description = translate("Required for VMess/VLESS. Generate with: uuidgen")
o.rmempty = true o.rmempty = true
o:depends("type", "vmess") o:depends("type", "vmess")
o:depends("type", "vless") o:depends("type", "vless")
-- Trojan 密码 -- Trojan 密码
o = s:option(Value, "trojan_password", translate("Trojan Password")) o = s:option(Value, "trojan_password", translate("Trojan Password"))
o.password = true o.password = true
o.rmempty = true o.rmempty = true
o:depends("type", "trojan") o:depends("type", "trojan")
-- Shadowsocks 密码和加密(Xray 内置) -- Shadowsocks 密码和加密(Xray 内置)
o = s:option(Value, "ss_password", translate("Shadowsocks Password")) o = s:option(Value, "ss_password", translate("Shadowsocks Password"))
o.password = true o.password = true
o.rmempty = true o.rmempty = true
@@ -195,7 +197,7 @@ o.default = "none"
o.rmempty = true o.rmempty = true
o:depends("type", "vless") o:depends("type", "vless")
-- ========== TLS / XTLS 设置 ========== -- ========== TLS / XTLS 设置 ==========
o = s:option(Flag, "tls", translate("TLS")) o = s:option(Flag, "tls", translate("TLS"))
o.default = 0 o.default = 0
o.rmempty = true o.rmempty = true
@@ -234,7 +236,7 @@ o.description = translate("e.g.: /etc/ssl/private.key")
o.rmempty = true o.rmempty = true
o:depends("tls", "1") o:depends("tls", "1")
-- ========== 传输层设置 (Transport) ========== -- ========== 传输层设置 (Transport) ==========
o = s:option(ListValue, "transport", translate("Transport Protocol")) o = s:option(ListValue, "transport", translate("Transport Protocol"))
o:value("tcp", "TCP") o:value("tcp", "TCP")
o:value("mkcp", "mKCP") o:value("mkcp", "mKCP")
@@ -248,7 +250,7 @@ o:depends("type", "vmess")
o:depends("type", "vless") o:depends("type", "vless")
o:depends("type", "trojan") o:depends("type", "trojan")
-- ----- WebSocket 设置 ----- -- ----- WebSocket 设置 -----
o = s:option(Value, "ws_host", translate("WebSocket Host")) o = s:option(Value, "ws_host", translate("WebSocket Host"))
o.rmempty = true o.rmempty = true
o:depends("transport", "ws") o:depends("transport", "ws")
@@ -264,7 +266,7 @@ o:depends("type", "vmess")
o:depends("type", "vless") o:depends("type", "vless")
o:depends("type", "trojan") o:depends("type", "trojan")
-- ----- HTTP/2 设置 ----- -- ----- HTTP/2 设置 -----
o = s:option(Value, "h2_host", translate("HTTP/2 Host")) o = s:option(Value, "h2_host", translate("HTTP/2 Host"))
o.rmempty = true o.rmempty = true
o:depends("transport", "h2") o:depends("transport", "h2")
@@ -278,7 +280,7 @@ o:depends("transport", "h2")
o:depends("type", "vmess") o:depends("type", "vmess")
o:depends("type", "vless") o:depends("type", "vless")
-- ----- TCP 伪装设置 ----- -- ----- TCP 伪装设置 -----
o = s:option(ListValue, "tcp_guise", translate("TCP Camouflage Type")) o = s:option(ListValue, "tcp_guise", translate("TCP Camouflage Type"))
o:value("none", "none") o:value("none", "none")
o:value("http", "http") o:value("http", "http")
@@ -296,7 +298,7 @@ o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
o.rmempty = true o.rmempty = true
o:depends("tcp_guise", "http") o:depends("tcp_guise", "http")
-- ----- mKCP 设置 ----- -- ----- mKCP 设置 -----
o = s:option(ListValue, "mkcp_guise", translate("mKCP Camouflage Type")) o = s:option(ListValue, "mkcp_guise", translate("mKCP Camouflage Type"))
for _, v in ipairs(header_type_list) do o:value(v) end for _, v in ipairs(header_type_list) do o:value(v) end
o.default = "none" o.default = "none"
@@ -349,13 +351,13 @@ o.datatype = "uinteger"
o.rmempty = true o.rmempty = true
o:depends("transport", "mkcp") o:depends("transport", "mkcp")
-- ----- DomainSocket 设置 ----- -- ----- DomainSocket 设置 -----
o = s:option(Value, "ds_path", translate("DomainSocket Path")) o = s:option(Value, "ds_path", translate("DomainSocket Path"))
o.description = translate("A legal file path. This file must not exist before running.") o.description = translate("A legal file path. This file must not exist before running.")
o.rmempty = true o.rmempty = true
o:depends("transport", "ds") o:depends("transport", "ds")
-- ----- QUIC 设置 ----- -- ----- QUIC 设置 -----
o = s:option(ListValue, "quic_security", translate("QUIC Security")) o = s:option(ListValue, "quic_security", translate("QUIC Security"))
o:value("none", "none") o:value("none", "none")
o:value("aes-128-gcm", "aes-128-gcm") o:value("aes-128-gcm", "aes-128-gcm")
@@ -374,7 +376,7 @@ o.default = "none"
o.rmempty = true o.rmempty = true
o:depends("transport", "quic") o:depends("transport", "quic")
-- ========== 访问控制 ========== -- ========== 访问控制 ==========
o = s:option(Flag, "bind_local", translate("Bind Local Only")) 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.description = translate("When selected, it can only be accessed locally. Recommended when using reverse proxies.")
o.default = 0 o.default = 0
@@ -458,6 +458,7 @@ o.remove = function() end
o = s:option(Value, "filter_words", translate("Subscribe Filter Words")) o = s:option(Value, "filter_words", translate("Subscribe Filter Words"))
o.rmempty = true o.rmempty = true
o.default = "过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国"
o.description = translate("Filter Words splited by /") o.description = translate("Filter Words splited by /")
o:depends("_subscribe_advanced_toggle", "1") o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_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 = s:option(DummyValue, "_upload_clash_yaml", translate("Upload Custom YAML File"))
o.template = "shadowsocksr/clash_yaml_upload" 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.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 end
s:append(cbi.Template("shadowsocksr/subscribe_schedule_compact")) 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 if Process_list:find("%-U") or Process_list:find("%-%-udp%-only") then
reudp_run = 1 reudp_run = 1
end 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 sock5_run = 1
end end
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 if (global_type == "clash" or global_type == "tuic" or global_type == "ss") and Process_list:find("ssr%-retcp") then
redir_run = 1 redir_run = 1
reudp_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 sock5_run = 1
end end
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 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 redir_run = 1
reudp_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 sock5_run = 1
end end
end end
@@ -4,7 +4,7 @@ local dsp = require "luci.dispatcher"
<script type="text/javascript"> <script type="text/javascript">
//<![CDATA[ //<![CDATA[
function clearlog(btn) { 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) { function(x, data) {
if (x && x.status == 200) { if (x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea'); 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) { function(x, data) {
if (x && x.status == 200) { if (x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea'); 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; return;
} }
port.innerHTML = '<font style=\"color:#0072c3\">connect</font>'; 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, index,
sid: dom.getAttribute("data-sid"), sid: dom.getAttribute("data-sid"),
domain: dom.getAttribute("hint"), domain: dom.getAttribute("hint"),
@@ -280,7 +280,7 @@ end
var server_page = pageElem ? pageElem.value : '<%=self.server_page or 1%>'; var server_page = pageElem ? pageElem.value : '<%=self.server_page or 1%>';
var server_page_size = pageSizeElem ? pageSizeElem.value : '<%=self.server_page_size or 0%>'; var server_page_size = pageSizeElem ? pageSizeElem.value : '<%=self.server_page_size or 0%>';
if (input) input.value = order; 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, order: order,
page: server_page, page: server_page,
page_size: server_page_size page_size: server_page_size
@@ -248,7 +248,7 @@ end
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" <input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>"
onclick=" onclick="
var sid = '<%=k%>'; 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 sid: sid
}, function(x, result) { }, function(x, result) {
result = result || {}; result = result || {};
@@ -3,7 +3,7 @@
local map = self.map 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_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') ~= "" local has_xray = luci.sys.exec('type -t -p xray 2>/dev/null') ~= ""
-%> -%>
<script type="text/javascript"> <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_ss_rust = "<%=has_ss_rust%>"
let has_mihomo = "<%=has_mihomo%>" let has_mihomo = "<%=has_mihomo%>"
let has_xray = "<%=has_xray%>" let has_xray = "<%=has_xray%>"
function hasMihomoOrXray() {
return has_mihomo === "true" || has_xray === "true";
}
function padright(str, cnt, pad) { function padright(str, cnt, pad) {
return str + Array(cnt + 1).join(pad); return str + Array(cnt + 1).join(pad);
@@ -112,6 +115,31 @@ function import_ssr_url(btn, urlname, sid) {
var event = document.createEvent("HTMLEvents"); var event = document.createEvent("HTMLEvents");
event.initEvent("change", true, true); event.initEvent("change", true, true);
switch (ssu[0]) { 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 "hysteria2":
case "hy2": case "hy2":
try { try {
@@ -122,8 +150,8 @@ function import_ssr_url(btn, urlname, sid) {
return false; return false;
} }
// Xray Hysteria2 导入函数 // Mihomo/Xray Hysteria2 导入函数
function importAsXrayHy2() { function importAsV2RayHy2() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray" document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray"
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event); document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0]; 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") { if (!hasMihomoOrXray()) {
s.innerHTML = "<font style=\'color:red\'><%:No available Xray core to import this Hysteria2 node.%></font>"; s.innerHTML = "<font style=\'color:red\'><%:No available Mihomo or Xray core to import this Hysteria2 node.%></font>";
return false; return false;
} }
importAsXrayHy2(); importAsV2RayHy2();
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : ""; 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); document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = normalizeHost(url.hostname);
@@ -535,19 +563,19 @@ function import_ssr_url(btn, urlname, sid) {
} }
let finalSSBackend; 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)); (params.type && ["ws", "httpupgrade", "xhttp", "splithttp", "grpc", "quic", "kcp", "http", "h2"].includes(params.type));
if (finalXray && has_xray === "true") { if (finalV2Ray && hasMihomoOrXray()) {
finalSSBackend = "v2ray"; finalSSBackend = "v2ray";
} else { } else {
finalSSBackend = preferredSSBackend(); finalSSBackend = preferredSSBackend();
} }
if (finalSSBackend === null) { 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; return false;
} }
if (finalSSBackend === "v2ray" && ssNode.plugin && ssNode.plugin !== "none") { 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; return false;
} }
if (finalSSBackend === "v2ray") importAsXraySS(ssNode); if (finalSSBackend === "v2ray") importAsXraySS(ssNode);
@@ -625,8 +653,8 @@ function import_ssr_url(btn, urlname, sid) {
return true; return true;
} }
// Xray Trojan 导入逻辑 // Mihomo/Xray Trojan 导入逻辑
function importAsXrayTrojan() { function importAsV2RayTrojan() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : ""; 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].value = "v2ray";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event); document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
@@ -732,13 +760,12 @@ function import_ssr_url(btn, urlname, sid) {
return true; return true;
} }
// 主逻辑:Trojan 统一使用 Xray // 主逻辑:Trojan 使用 Mihomo/Xray 兼容的 v2ray 节点格式
function hasXrayCore() { return has_xray === "true"; } if (!hasMihomoOrXray()) {
if (!hasXrayCore()) { s.innerHTML = "<font style=\'color:red\'><%:No available core (Mihomo or Xray) to import this node.%></font>";
s.innerHTML = "<font style=\'color:red\'><%:No available core (Xray) to import this node.%></font>";
return false; return false;
} }
importAsXrayTrojan(); importAsV2RayTrojan();
return false; return false;
case "vmess": case "vmess":
var sstr = b64DecodeUnicode((ssu[1] || "").replace(/#.*/, "").trim()); 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) { function createEmptyRow(sid) {
var templateRow = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row:not(.cbi-section-table-titles):not(.placeholder)'); var templateRow = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row:not(.cbi-section-table-titles):not(.placeholder)');
if (!templateRow) { var newRow = templateRow ? templateRow.cloneNode(true) : createFallbackRow(sid);
console.error('No template row found'); newRow.id = 'cbi-shadowsocksr-' + sid;
return null;
}
var newRow = templateRow.cloneNode(true);
newRow.id = 'cbi-shadowsocksr-server_subscribe_item-' + sid;
var inputs = newRow.querySelectorAll('input'); var inputs = newRow.querySelectorAll('input');
inputs.forEach(function(input) { inputs.forEach(function(input) {
if (input.name) { 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) { 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') { if (input.type === 'checkbox') {
input.checked = false; input.checked = false;
@@ -161,6 +194,10 @@ end
input.disabled = false; 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'); var delBtn = newRow.querySelector('.cbi-button-remove');
if (delBtn) { if (delBtn) {
delBtn.setAttribute('data-sid', sid); 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" ps_list | grep -v "grep" | grep -q "ssr-retcp"
;; ;;
v2ray) 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 return 1
@@ -284,6 +284,22 @@ get_ss_server_mihomo_runtime_file() {
echo "$(get_ss_server_mihomo_workdir "$1")/config.yaml" 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() { link_mihomo_geodata() {
local workdir="$1" local workdir="$1"
@@ -366,6 +382,41 @@ prepare_ss_server_mihomo_runtime_config() {
fi 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() { download_clash_config() {
local sid="$1" local sid="$1"
local clash_url="$(uci_get_by_name "$sid" clash_url)" local clash_url="$(uci_get_by_name "$sid" clash_url)"
@@ -419,7 +470,7 @@ clash_cache() {
filter_clash_yaml_proxies() { filter_clash_yaml_proxies() {
local yaml_file="$1" 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 names_file="$TMP_PATH/clash-proxy-names.$$"
local removed_file="$TMP_PATH/clash-proxy-removed.$$" local removed_file="$TMP_PATH/clash-proxy-removed.$$"
local removed_count=0 local removed_count=0
@@ -697,6 +748,70 @@ use_mihomo_for_ss_rust_server() {
has_mihomo_binary 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() { ln_start_bin() {
local file_func=${1} local file_func=${1}
local ln_name=${2} local ln_name=${2}
@@ -1289,9 +1404,15 @@ start_udp() {
fi fi
;; ;;
v2ray) v2ray)
gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_port if has_mihomo_binary; then
ln_start_bin $(first_type xray) v2ray run -c $udp_config_file redir_udp=0
echolog "UDP TPROXY Relay:$($(first_type xray) version | head -1) Started!" 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 trojan) #client
gen_config_file $UDP_RELAY_SERVER $type 2 $tmp_udp_local_port 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) local chain_type=$(uci_get_by_name $UDP_RELAY_SERVER chain_type)
case ${chain_type} in case ${chain_type} in
vmess) vmess)
ln_start_bin $(first_type xray) v2ray run -c $udp_config_file 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
echolog "UDP TPROXY Relay:shadow-tls chain-to $($(first_type xray) --version) Started!" :
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) sslocal)
ln_start_bin $(first_type sslocal) sslocal -c $udp_config_file ln_start_bin $(first_type sslocal) sslocal -c $udp_config_file
@@ -1327,16 +1452,16 @@ start_udp() {
esac esac
;; ;;
socks5) socks5)
if [ "$(uci_get_by_name $UDP_RELAY_SERVER auth_enable 0)" == "1" ]; then if start_socks5_udp_with_ipt2socks "$UDP_RELAY_SERVER" "$tmp_udp_port" "$threads"; then
local auth="-a $(uci_get_by_name $UDP_RELAY_SERVER username) -k $(uci_get_by_name $UDP_RELAY_SERVER password)" :
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 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 esac
} }
@@ -1395,11 +1520,20 @@ start_local() {
fi fi
;; ;;
v2ray) 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 gen_config_file $LOCAL_SERVER $type 4 0 $local_port
ln_start_bin $(first_type xray) v2ray run -c $local_config_file 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 fi
echolog "Global_Socks5:$($(first_type xray) version | head -1) Started!"
;; ;;
trojan) #client trojan) #client
gen_config_file $LOCAL_SERVER $type 4 $local_port 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) local chain_type=$(uci_get_by_name $LOCAL_SERVER chain_type)
case ${chain_type} in case ${chain_type} in
vmess) vmess)
ln_start_bin $(first_type xray) v2ray run -c $local_config_file 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
echolog "Global Socks5 Proxy:shadow-tls chain-to$($(first_type xray) --version) Started!" :
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) sslocal)
ln_start_bin $(first_type sslocal) sslocal -c $local_config_file ln_start_bin $(first_type sslocal) sslocal -c $local_config_file
@@ -1443,16 +1581,22 @@ start_local() {
esac esac
fi fi
;; ;;
clash) clash)
echolog "Global_Socks5:Clash total node is only supported when using Same as Global Server." 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 return 1
;; fi
socks5) ;;
echolog "Global_Socks5:Socks5 transparent-only node cannot be used as generic Socks outbound." *)
return 1 local listenip='-i 0.0.0.0'
;;
*)
local listenip='-i 0.0.0.0'
[ -e /proc/sys/net/ipv6 ] && listenip='-i ::' [ -e /proc/sys/net/ipv6 ] && listenip='-i ::'
ln_start_bin $(first_type microsocks) microsocks $listenip -p $local_port tcp-udp-ssr-local ln_start_bin $(first_type microsocks) microsocks $listenip -p $local_port tcp-udp-ssr-local
echolog "Global_Socks5:$type Started!" echolog "Global_Socks5:$type Started!"
@@ -1725,9 +1869,16 @@ Start_Run() {
fi fi
;; ;;
v2ray) v2ray)
gen_config_file $GLOBAL_SERVER $type 1 $tcp_port $socks_port if start_v2ray_with_mihomo "$GLOBAL_SERVER" "$tcp_port" "$socks_port" "$GLOBAL_SERVER" "redir" "ssr-retcp" "Main node"; then
ln_start_bin $(first_type xray) v2ray run -c $tcp_config_file :
echolog "Main node:$($(first_type xray) version | head -1) Started!" 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) trojan)
gen_config_file $GLOBAL_SERVER $type 1 $tcp_port gen_config_file $GLOBAL_SERVER $type 1 $tcp_port
@@ -1759,24 +1910,28 @@ Start_Run() {
shadowtls) shadowtls)
if [ -z "$socks_port" ]; then 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}"
gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" 0 chain gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" 0 chain
else 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}"
gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" $socks_port chain gen_config_file $GLOBAL_SERVER $type 1 "10${tmp_tcp_local_port}" $socks_port chain
fi fi
local chain_type=$(uci_get_by_name $GLOBAL_SERVER chain_type) local chain_type=$(uci_get_by_name $GLOBAL_SERVER chain_type)
case ${chain_type} in case ${chain_type} in
vmess) vmess)
ln_start_bin $(first_type shadow-tls) shadow-tls config --config $chain_config_file 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 if start_v2ray_with_mihomo "$GLOBAL_SERVER" "$tcp_port" "$socks_port" "${GLOBAL_SERVER}-shadowtls" "redir" "shadowtls-vmess" "Main node:shadow-tls chain-to"; then
echolog "Mian node:shadow-tls chain-to $($(first_type xray) --version) Started!" :
;; else
sslocal) ln_start_bin $(first_type xray) v2ray run -c $tcp_config_file
ln_start_bin $(first_type shadow-tls) shadow-tls config --config $chain_config_file echolog "Mian node:shadow-tls chain-to $($(first_type xray) --version) Started!"
ln_start_bin $(first_type sslocal) sslocal -c $tcp_config_file fi
echolog "Main node:shadow-tls chain-to $($(first_type sslocal) --version) Started!" ;;
;; sslocal)
esac 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) clash)
local clash_socks_port=0 local clash_socks_port=0
@@ -1802,23 +1957,14 @@ Start_Run() {
echolog "Main node:Mihomo (Clash) Started!" echolog "Main node:Mihomo (Clash) Started!"
;; ;;
socks5) socks5)
local ipt2socks_bin="$(first_type ipt2socks)" if start_socks5_redir_with_ipt2socks "$GLOBAL_SERVER" "$tcp_port" "$threads"; then
local auth_opts="" :
[ -x "$ipt2socks_bin" ] || { elif start_v2ray_with_mihomo "$GLOBAL_SERVER" "$tcp_port" "$socks_port" "$GLOBAL_SERVER" "redir" "ssr-retcp" "Main node"; then
echolog "Main node:Socks5 缺少 ipt2socks,无法启动透明代理。" echolog "Main node:Socks5 缺少 ipt2socks,已回退到 Mihomo。"
else
echolog "Main node:Socks5 缺少 ipt2socks,且 Mihomo 不可用。"
return 1 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 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 esac
redir_tcp=1 redir_tcp=1
@@ -2018,6 +2164,7 @@ start_server() {
local tcp_guise=$(uci_get_by_name "$1" tcp_guise "none") 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_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 tcp_guise_http_path=$(uci_get_by_name "$1" tcp_guise_http_path "")
local mihomo_server_started=0
# 确定密码/UUID # 确定密码/UUID
local password="" local password=""
@@ -2036,26 +2183,37 @@ start_server() {
echolog "Server: $type 服务端缺少密码/UUID,跳过启动" echolog "Server: $type 服务端缺少密码/UUID,跳过启动"
return 1 return 1
} }
local config_file="$TMP_PATH/xray-server-$server_count.json" local mihomo_bin="$(first_type mihomo)"
if [ -x "$mihomo_bin" ] && prepare_v2ray_server_mihomo_runtime_config "$1"; then
# 生成 Xray 配置 local server_workdir="$(get_v2ray_server_mihomo_workdir "$1")"
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" 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)"
if [ $? -ne 0 ]; then mihomo_server_started=1
echolog "Server: Xray $type 配置生成失败"
return 1
fi fi
local xray_bin=$(first_type xray) if [ "$mihomo_server_started" != "1" ]; then
if [ -z "$xray_bin" ]; then
echolog "Server: xray-core 未安装,无法启动 $type 服务端" local config_file="$TMP_PATH/xray-server-$server_count.json"
return 1
# 生成 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 fi
;;
ln_start_bin "$xray_bin" xray-server-$server_count run -c "$config_file"
echolog "Server: Xray $type 服务端 Server$server_count 已启动 (端口: $port)"
;;
esac esac
server_port=$(uci_get_by_name $1 server_port) server_port=$(uci_get_by_name $1 server_port)
if [ "$USE_TABLES" = "nftables" ]; then if [ "$USE_TABLES" = "nftables" ]; then
@@ -63,6 +63,13 @@ if [ -s "/etc/config/shadowsocksr" ]; then
uci -q set shadowsocksr.@server_subscribe[0].proxy='1' uci -q set shadowsocksr.@server_subscribe[0].proxy='1'
fi 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 if ! uci -q get shadowsocksr.@global_xray_fragment[0] > /dev/null; then
uci -q add shadowsocksr global_xray_fragment uci -q add shadowsocksr global_xray_fragment
uci -q set shadowsocksr.@global_xray_fragment[0].fragment='0' 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" ENABLE_SERVER="nil"
cleanup_probe_socks() { 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 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 -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 rm -f /tmp/ssr-switch-probe.log 2>/dev/null
} }
@@ -54,21 +54,13 @@ start_probe_socks() {
tmp_local_port= tmp_local_port=
mode="tcp,udp" mode="tcp,udp"
if [ "$server_type" = "v2ray" ]; then export SSR_SWITCH_PROBE=1
export SSR_SWITCH_PROBE=1 export SSR_SWITCH_PROBE_INSTANCE_KEY="$PROBE_INSTANCE_KEY"
gen_config_file "$LOCAL_SERVER" "$server_type" 4 0 "$probe_socks_port" >/tmp/ssr-switch-probe.log 2>&1 || return 1 start_local_with_port "$probe_socks_port" >/tmp/ssr-switch-probe.log 2>&1
unset SSR_SWITCH_PROBE local rc=$?
local xray_bin unset SSR_SWITCH_PROBE_INSTANCE_KEY
xray_bin="$(first_type xray)" unset SSR_SWITCH_PROBE
[ -x "$xray_bin" ] || return 1 [ "$rc" = "0" ] || 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
sleep 2 sleep 2
return 0 return 0
@@ -1,5 +1,6 @@
#!/usr/bin/lua #!/usr/bin/lua
require "nixio"
require "nixio.fs" require "nixio.fs"
require "luci.model.uci" require "luci.model.uci"
@@ -582,6 +583,376 @@ local function string_or_nil(value)
return tostring(value) return tostring(value)
end 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, ...) local function pick_plugin_opt(plugin_opts, ...)
for i = 1, select("#", ...) do for i = 1, select("#", ...) do
local key = select(i, ...) local key = select(i, ...)
@@ -661,7 +1032,7 @@ local function normalize_plugin_name(plugin)
return value return value
end 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 = normalize_plugin_name(get_server_field(sid, "plugin", ""))
local plugin_opts = parse_plugin_opts(get_server_field(sid, "plugin_opts", "")) 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 return true
end 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 function build_shadowsocks_server_doc(sid)
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0 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") 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 return true
end 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] local action = arg[1]
if action == "validate" then if action == "validate" then
os.exit(validate(arg[2]) and 0 or 1) 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) os.exit(generate_tuic_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss" then elseif action == "ss" then
os.exit(generate_shadowsocks_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1) 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 elseif action == "ss_server" then
os.exit(generate_shadowsocks_server(arg[2], arg[3]) and 0 or 1) 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 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) os.exit(1)
end 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 local effective_node_local_port = tonumber(server.local_port) or tonumber(default_node_local_port) or 1234
if server.type == "ss-rust" then if server.type == "ss-rust" then
server.type = "ss" server.type = "ss"
end end
local function parse_realm_uri(uri) local function parse_realm_uri(uri)
if type(uri) ~= "string" then return nil end if type(uri) ~= "string" then return nil end
-- realm://token@server/realm_id?query -- realm[+http]://token@server/realm_id?query
local token, server_url, realm_id, query = trim(uri):match("^realm://([^@]+)@([^/]+)/([^?]*)%??(.*)$") local scheme, token, server_url, realm_id, query = trim(uri):match("^(realm%+http|realm)://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not token or not server_url or not realm_id then return nil end if not scheme or not token or not server_url or not realm_id then return nil end
realm_id = realm_id:gsub("/+$", "") realm_id = realm_id:gsub("/+$", "")
local realm = { local realm = {
scheme = scheme,
token = token, token = token,
server_url = server_url, server_url = server_url,
realm_id = realm_id realm_id = realm_id
} }
-- 解析 query 中的 stun= -- 解析 query 中的 stun=
if query and query ~= "" then local stun_servers
local stun_servers = {} for value in (query or ""):gmatch("[?&]?[Ss][Tt][Uu][Nn]=([^&]+)") do
for key, value in query:gmatch("([^&=?]+)=([^&]+)") do if not stun_servers then stun_servers = {} end
if key == "stun" and value ~= "" then stun_servers[#stun_servers + 1] = value
stun_servers[#stun_servers + 1] = value
end
end
if #stun_servers > 0 then
realm.stun_servers = stun_servers
end
end end
realm.stun_servers = stun_servers
return realm return realm
end end
@@ -428,22 +424,24 @@ end
-- 开启 socks 代理 -- 开启 socks 代理
-- 检查是否启用 socks 代理 -- 检查是否启用 socks 代理
if proto and proto:find("tcp") and socks_port ~= "0" then if proto and proto:find("tcp") and socks_port ~= "0" then
table.insert(Xray.inbounds, { local auth = (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth")
-- socks and {{
protocol = "socks", user = socks_server.socks5_user,
port = tonumber(socks_port), pass = socks_server.socks5_pass
}} or nil
table.insert(Xray.inbounds, {
-- socks
protocol = "socks",
port = tonumber(socks_port),
settings = { settings = {
auth = socks_server.socks5_auth or "noauth", auth = socks_server.socks5_auth or "noauth",
udp = true, udp = true,
mixed = ((socks_server.socks5_mixed == '1') and true or false) or nil, mixed = socks_server.socks5_mixed == "1" or nil,
accounts = (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth") and { accounts = (xray_version_val <= 260503) and auth or nil,
{ users = (xray_version_val > 260503) and auth or nil
user = socks_server.socks5_user, }
pass = socks_server.socks5_pass })
}
} or nil
} or nil
})
end end
-- 传出连接 -- 传出连接
@@ -645,9 +643,7 @@ Xray.outbounds = {
local realm = parse_realm_uri(server.hysteria2_realm_url) local realm = parse_realm_uri(server.hysteria2_realm_url)
local url, stun local url, stun
if realm then if realm then
if realm.token and realm.server_url and realm.realm_id then url = realm.scheme .. "://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
url = "realm://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
end
stun = realm.stun_servers or server.hysteria2_realm_stun stun = realm.stun_servers or server.hysteria2_realm_stun
end end
local r = { local r = {
@@ -29,7 +29,7 @@ config server_subscribe
option auto_update_min_time '0' option auto_update_min_time '0'
option url_test_url 'https://www.google.com/generate_204' option url_test_url 'https://www.google.com/generate_204'
option user_agent 'v2rayN/9.99' option user_agent 'v2rayN/9.99'
option filter_words '过期/套餐/剩余/网址/QQ群/官网/防失联/回国' option filter_words '过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国'
config access_control config access_control
option lan_ac_mode '0' 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 proxy = ucic:get_first(name, 'server_subscribe', 'proxy', '0')
local switch = ucic:get_first(name, 'server_subscribe', 'switch', '1') local switch = ucic:get_first(name, 'server_subscribe', 'switch', '1')
local allow_insecure = ucic:get_first(name, 'server_subscribe', 'allow_insecure', '0') 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 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 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 local_clash_dir = "/etc/ssrplus/clash"
local target_subscribe_sid = tostring(arg and arg[1] or ""):gsub("^%s*(.-)%s*$", "%1") local target_subscribe_sid = tostring(arg and arg[1] or ""):gsub("^%s*(.-)%s*$", "%1")
@@ -125,6 +128,11 @@ local function urlEncode(szText)
return str return str
end 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) local function get_urldecode(h)
return schar(tonumber(h, 16)) return schar(tonumber(h, 16))
end end
@@ -397,6 +405,7 @@ local function parseAnytlsShare(content)
if not userinfo or not hostinfo then if not userinfo or not hostinfo then
return nil return nil
end end
hostinfo = hostinfo:gsub("/+$", "")
local password = UrlDecode(userinfo) local password = UrlDecode(userinfo)
local server, port = hostinfo:match("^(.+):(%d+)$") local server, port = hostinfo:match("^(.+):(%d+)$")
@@ -468,6 +477,389 @@ local function buildAnytlsClashYaml(entries, group_name)
return table.concat(lines, "\n") .. "\n" return table.concat(lines, "\n") .. "\n"
end 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 function processLocalClashSubscription(path, alias)
local result = { local result = {
type = "clash", type = "clash",
@@ -499,7 +891,7 @@ local function processData(szType, content, cfgid)
local url = URL.parse("http://" .. content) local url = URL.parse("http://" .. content)
local params = url.query local params = url.query
if not has_xray then if not has_xray and not has_mihomo then
return nil return nil
end end
@@ -562,6 +954,30 @@ local function processData(szType, content, cfgid)
-- 处理 fast open 参数 -- 处理 fast open 参数
result.fast_open = params.tfo result.fast_open = params.tfo
end 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 elseif szType == 'ssr' then
-- 去掉前后空白和#注释 -- 去掉前后空白和#注释
local link = trim(content:gsub("#.*$", "")) local link = trim(content:gsub("#.*$", ""))
@@ -1168,7 +1584,7 @@ local function processData(szType, content, cfgid)
end end
-- 自动决定模式(true=Xray, false=普通 Trojan) -- 自动决定模式(true=Xray, false=普通 Trojan)
if not has_xray then if not has_xray and not has_mihomo then
return nil return nil
end end
@@ -1575,6 +1991,56 @@ local function curl(url, user_agent)
return stdout, md5 return stdout, md5
end 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 function collect_wan_interfaces()
local ifaces = {} local ifaces = {}
local seen = {} local seen = {}
@@ -1845,6 +2311,7 @@ end
-- 加载订阅未变化的节点用于防止被误删 -- 加载订阅未变化的节点用于防止被误删
local function loadOldNodes(groupHash) local function loadOldNodes(groupHash)
local nodes = {} local nodes = {}
local count = 0
cache[groupHash] = {} cache[groupHash] = {}
nodeResult[#nodeResult + 1] = nodes nodeResult[#nodeResult + 1] = nodes
local index = #nodeResult local index = #nodeResult
@@ -1854,8 +2321,22 @@ local function loadOldNodes(groupHash)
local section = setmetatable({}, {__index = s}) local section = setmetatable({}, {__index = s})
nodes[s.hashkey] = section nodes[s.hashkey] = section
cache[groupHash][s.hashkey] = section cache[groupHash][s.hashkey] = section
count = count + 1
end end
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 end
local function get_section_ss_backend(section) 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 return has_ss_node and needs_refresh
end 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 function preserve_unselected_groups(selected_hashes)
local preserved = {} local preserved = {}
@@ -1918,7 +2451,8 @@ local execute = function()
local selected_hashes = {} local selected_hashes = {}
for _, item in ipairs(subscribe_items) do 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 end
if target_subscribe_sid ~= "" then if target_subscribe_sid ~= "" then
@@ -1927,21 +2461,38 @@ local execute = function()
for _, item in ipairs(subscribe_items) do for _, item in ipairs(subscribe_items) do
local url = item.url 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) log("raw 长度: "..#raw)
local groupHash = md5(url) local groupHash = md5(fetch_url)
local old_md5 = read_old_md5(groupHash) local old_md5 = read_old_md5(groupHash)
log("处理订阅: " .. url) 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("groupHash: " .. groupHash)
log("old_md5: " .. tostring(old_md5)) log("old_md5: " .. tostring(old_md5))
log("new_md5: " .. tostring(new_md5)) log("new_md5: " .. tostring(new_md5))
local backend_refresh = group_needs_ss_backend_refresh(groupHash) 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 if #raw == 0 then
log(url .. ': 获取内容为空') log(url .. ': 获取内容为空')
loadOldNodes(groupHash) 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) log("订阅未变化, 跳过无需更新的订阅: " .. url)
-- 防止 diff 阶段误删未更新订阅节点 -- 防止 diff 阶段误删未更新订阅节点
loadOldNodes(groupHash) loadOldNodes(groupHash)
@@ -1955,6 +2506,12 @@ local execute = function()
if backend_refresh and old_md5 and new_md5 == old_md5 then if backend_refresh and old_md5 and new_md5 == old_md5 then
log("检测到 SS 后端偏好变化,强制重建订阅节点: " .. url) log("检测到 SS 后端偏好变化,强制重建订阅节点: " .. url)
end 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 updated = true
-- 保存更新后的 MD5 值到以 groupHash 为标识的临时文件中,用于下次订阅更新时进行对比 -- 保存更新后的 MD5 值到以 groupHash 为标识的临时文件中,用于下次订阅更新时进行对比
write_new_md5(groupHash, new_md5) write_new_md5(groupHash, new_md5)
@@ -1967,7 +2524,7 @@ local execute = function()
if isClashYAML(raw) then if isClashYAML(raw) then
is_clash_subscription = true 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 if result and not check_filer(result) and not cache[groupHash][result.hashkey] then
result.grouphashkey = groupHash result.grouphashkey = groupHash
table.insert(nodeResult[index], result) table.insert(nodeResult[index], result)
@@ -2014,7 +2571,7 @@ local execute = function()
if line:match("^anytls://") then if line:match("^anytls://") then
local parsed = parseAnytlsShare(line:gsub("^anytls://", "")) local parsed = parseAnytlsShare(line:gsub("^anytls://", ""))
if parsed then if parsed then
table.insert(anytls_nodes, parsed) table.insert(anytls_nodes, anytls_to_mihomo_node(parsed))
end end
elseif line ~= "" then elseif line ~= "" then
table.insert(normal_nodes, node) table.insert(normal_nodes, node)
@@ -2022,19 +2579,8 @@ local execute = function()
end end
if #anytls_nodes > 0 then if #anytls_nodes > 0 then
local parsed_url = URL.parse(url) for _, parsed in ipairs(anytls_nodes) do
local alias = "Clash_" .. (parsed_url.host or groupHash) table.insert(normal_nodes, parsed)
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)
end end
end end
@@ -2046,10 +2592,12 @@ local execute = function()
if not is_clash_subscription then if not is_clash_subscription then
for _, v in ipairs(nodes) do 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() xpcall(function()
local result local result
if szType then if type(v) == "table" then
result = v
elseif szType then
result = processData(szType, v) result = processData(szType, v)
elseif not szType then elseif not szType then
local node = trim(v) local node = trim(v)
@@ -2061,7 +2609,7 @@ local execute = function()
if dat[3] then if dat[3] then
dat3 = "://" .. dat[3] dat3 = "://" .. dat[3]
end 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) result = processData(dat[1], dat[2] .. dat3)
else else
result = processData(dat[1], base64Decode(dat[2])) result = processData(dat[1], base64Decode(dat[2]))
@@ -2110,7 +2658,44 @@ local execute = function()
end end
-- 清理临时字段 -- 清理临时字段
node.raw_alias = nil 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 node.grouphashkey = groupHash
table.insert(nodeResult[index], node) table.insert(nodeResult[index], node)
cache[groupHash][node.hashkey] = node cache[groupHash][node.hashkey] = node
+2 -2
View File
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow PKG_NAME:=natflow
PKG_VERSION:=20260531 PKG_VERSION:=20260531
PKG_RELEASE:=16 PKG_RELEASE:=17
PKG_SOURCE:=$(PKG_VERSION).tar.xz PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=804f71f7e21827961ac35469b97c2cddc7c381cb PKG_SOURCE_VERSION:=e6ed6e88085c8e724f17212de66bd8ca3571a0bf
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com> PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
PKG_LICENSE:=GPL-2.0 PKG_LICENSE:=GPL-2.0