🎨 Sync 2026-06-04 22:17:22

This commit is contained in:
github-actions[bot] committed 2026-06-04 22:17:22 +08:00
1 parent ca5e3fba09
commit effa70d504
71 files changed
+15700 -6848

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=dns2tcp
PKG_VERSION:=1.1.2
PKG_RELEASE:=3
PKG_RELEASE:=4
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/zfl9/dns2tcp/tar.gz/v$(PKG_VERSION)?
@@ -0,0 +1,210 @@
diff --git a/dns2tcp.c b/dns2tcp.c
index b289598..542f839 100644
--- a/dns2tcp.c
+++ b/dns2tcp.c
@@ -35,6 +35,10 @@
#define PORTSTRLEN 6 /* "65535" (include \0) */
#define DNS_MSGSZ 1472 /* mtu:1500 - iphdr:20 - udphdr:8 */
+#define DNS_HDRLEN 12
+
+#define DNS_TYPE_A 1
+#define DNS_TYPE_AAAA 28
/* ======================== helper ======================== */
@@ -128,6 +132,14 @@ typedef struct {
union skaddr srcaddr;
} ctx_t;
+typedef struct {
+ size_t start;
+ size_t end;
+ uint16_t type;
+ uint8_t section;
+ bool drop;
+} dns_rr_t;
+
/* ======================== global-vars ======================== */
enum {
@@ -135,6 +147,7 @@ enum {
FLAG_REUSE_PORT = 1 << 1, /* udp listen */
FLAG_VERBOSE = 1 << 2, /* logging */
FLAG_LOCAL_ADDR = 1 << 3, /* tcp local addr */
+ FLAG_FILTER_AAAA = 1 << 4, /* strip all AAAA records from dns response */
};
#define has_flag(flag) (g_flags & (flag))
@@ -166,6 +179,126 @@ static void tcp_connect_cb(evloop_t *evloop, evio_t *watcher, int events);
static void tcp_sendmsg_cb(evloop_t *evloop, evio_t *watcher, int events);
static void tcp_recvmsg_cb(evloop_t *evloop, evio_t *watcher, int events);
+static uint16_t read_be16(const void *ptr) {
+ const uint8_t *bytes = ptr;
+ return ((uint16_t)bytes[0] << 8) | bytes[1];
+}
+
+static void write_be16(void *ptr, uint16_t value) {
+ uint8_t *bytes = ptr;
+ bytes[0] = value >> 8;
+ bytes[1] = value & 0xff;
+}
+
+static bool dns_skip_name(const uint8_t *msg, size_t msglen, size_t offset, size_t *next_offset) {
+ while (offset < msglen) {
+ uint8_t len = msg[offset];
+ if (len == 0) {
+ *next_offset = offset + 1;
+ return true;
+ }
+ if ((len & 0xc0) == 0xc0) {
+ if (offset + 1 >= msglen)
+ return false;
+ *next_offset = offset + 2;
+ return true;
+ }
+ if ((len & 0xc0) || offset + 1 + len > msglen)
+ return false;
+ offset += 1 + len;
+ }
+
+ return false;
+}
+
+static size_t filter_aaaa_records(uint8_t *msg, size_t msglen) {
+ if (!has_flag(FLAG_FILTER_AAAA) || msglen < DNS_HDRLEN)
+ return msglen;
+
+ uint16_t qdcount = read_be16(msg + 4);
+ uint16_t counts[] = {
+ read_be16(msg + 6),
+ read_be16(msg + 8),
+ read_be16(msg + 10),
+ };
+ uint32_t rrcount = counts[0] + counts[1] + counts[2];
+
+ if (rrcount == 0 || qdcount > msglen / 5 || rrcount > msglen / 11)
+ return msglen;
+
+ size_t offset = DNS_HDRLEN;
+ for (uint16_t i = 0; i < qdcount; i++) {
+ if (!dns_skip_name(msg, msglen, offset, &offset) || offset + 4 > msglen)
+ return msglen;
+ offset += 4;
+ }
+
+ size_t rr_start = offset;
+ dns_rr_t *rrs = calloc(rrcount, sizeof(*rrs));
+ if (!rrs)
+ return msglen;
+
+ uint32_t rridx = 0;
+ for (uint8_t section = 0; section < 3; section++) {
+ for (uint16_t i = 0; i < counts[section]; i++) {
+ dns_rr_t *rr = &rrs[rridx++];
+ rr->start = offset;
+ rr->section = section;
+
+ if (!dns_skip_name(msg, msglen, offset, &offset) || offset + 10 > msglen)
+ goto out;
+
+ rr->type = read_be16(msg + offset);
+ uint16_t rdlen = read_be16(msg + offset + 8);
+ rr->end = offset + 10 + rdlen;
+ if (rr->end > msglen)
+ goto out;
+
+ offset = rr->end;
+ }
+ }
+
+ size_t rest_start = offset;
+ bool changed = false;
+ uint16_t keep_counts[] = { counts[0], counts[1], counts[2] };
+
+ for (uint32_t i = 0; i < rrcount; i++) {
+ if (rrs[i].type == DNS_TYPE_AAAA) {
+ rrs[i].drop = true;
+ keep_counts[rrs[i].section]--;
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ size_t write_offset = rr_start;
+
+ for (uint32_t i = 0; i < rrcount; i++) {
+ if (rrs[i].drop)
+ continue;
+
+ size_t rrlen = rrs[i].end - rrs[i].start;
+ if (write_offset != rrs[i].start)
+ memmove(msg + write_offset, msg + rrs[i].start, rrlen);
+ write_offset += rrlen;
+ }
+
+ if (write_offset != rest_start)
+ memmove(msg + write_offset, msg + rest_start, msglen - rest_start);
+
+ msglen = write_offset + (msglen - rest_start);
+ write_be16(msg + 6, keep_counts[0]);
+ write_be16(msg + 8, keep_counts[1]);
+ write_be16(msg + 10, keep_counts[2]);
+ log_verbose("filter AAAA records, answer:%hu -> %hu, authority:%hu -> %hu, additional:%hu -> %hu",
+ counts[0], keep_counts[0], counts[1], keep_counts[1], counts[2], keep_counts[2]);
+ }
+
+out:
+ free(rrs);
+ return msglen;
+}
+
static void print_help(void) {
printf("usage: dns2tcp <-L listen> <-R remote> [options...]\n"
" -L <ip[#port]> udp listen address, port default to 53\n"
@@ -174,6 +307,7 @@ static void print_help(void) {
" -s <syncnt> set TCP_SYNCNT option for tcp socket\n"
" -6 set IPV6_V6ONLY option for udp socket\n"
" -r set SO_REUSEPORT option for udp socket\n"
+ " -A strip all AAAA records from dns response\n"
" -v print verbose log, used for debugging\n"
" -V print version number of dns2tcp and exit\n"
" -h print help information of dns2tcp and exit\n"
@@ -257,7 +391,7 @@ static void parse_opt(int argc, char *argv[]) {
opterr = 0;
int shortopt;
- const char *optstr = "L:R:l:s:6rafvVh";
+ const char *optstr = "L:R:l:s:6rAafvVh";
while ((shortopt = getopt(argc, argv, optstr)) != -1) {
switch (shortopt) {
case 'L':
@@ -295,6 +429,9 @@ static void parse_opt(int argc, char *argv[]) {
case 'r':
add_flag(FLAG_REUSE_PORT);
break;
+ case 'A':
+ add_flag(FLAG_FILTER_AAAA);
+ break;
case 'a':
/* nop */
break;
@@ -399,6 +536,7 @@ int main(int argc, char *argv[]) {
if (g_syn_cnt) log_info("enable TCP_SYNCNT:%hhu sockopt", g_syn_cnt);
if (has_flag(FLAG_IPV6_V6ONLY)) log_info("enable IPV6_V6ONLY sockopt");
if (has_flag(FLAG_REUSE_PORT)) log_info("enable SO_REUSEPORT sockopt");
+ if (has_flag(FLAG_FILTER_AAAA)) log_info("enable AAAA record filter");
log_verbose("print the verbose log");
g_listen_fd = create_socket(skaddr_family(&g_listen_skaddr), SOCK_DGRAM);
@@ -532,6 +670,9 @@ static void tcp_recvmsg_cb(evloop_t *evloop, evio_t *watcher, int events __unuse
uint16_t msglen;
if (ctx->nbytes < 2 || ctx->nbytes < 2 + (msglen = ntohs(*(uint16_t *)buffer))) return;
+ msglen = filter_aaaa_records((uint8_t *)buffer + 2, msglen);
+ *(uint16_t *)buffer = htons(msglen);
+
ssize_t nsend = sendto(g_listen_fd, buffer + 2, msglen, 0, &ctx->srcaddr.sa, skaddr_len(&ctx->srcaddr));
if (nsend < 0 || verbose) {
char ip[IP6STRLEN];
+69 -94
View File
@@ -3,77 +3,51 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=190
PKG_RELEASE:=18
PKG_VERSION:=193
PKG_RELEASE:=19
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS_RUST \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNS2TCP \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNSPROXY \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Redsocks2 \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Trojan
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server
LUCI_TITLE:=SS/SSR/V2Ray/Trojan/NaiveProxy/Tuic/ShadowTLS/Hysteria/Socks5/Tun LuCI interface
LUCI_TITLE:=SS/SSR/V2Ray/Trojan/NaiveProxy/Tuic/ShadowTLS/Hysteria/Socks5/Clash LuCI interface
LUCI_PKGARCH:=all
LUCI_DEPENDS:= \
+coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \
+jq +ip-full +lua +lua-neturl +libuci-lua +microsocks \
+tcping +resolveip +shadowsocksr-libev-ssr-check +curl +nping \
+PACKAGE_$(PKG_NAME)_INCLUDE_V2ray:curl \
+PACKAGE_$(PKG_NAME)_INCLUDE_V2ray:v2ray-core \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:curl \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \
+libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
+resolveip +curl +nping +unzip +xz-utils \
+PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG:chinadns-ng \
+PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS:dns2socks \
+PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS_RUST:dns2socks-rust \
+PACKAGE_$(PKG_NAME)_INCLUDE_DNSPROXY:dnsproxy \
+PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS:mosdns \
+PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria:hysteria \
+PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client:tuic-client \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS:shadow-tls \
+PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks:ipt2socks \
+PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun:kcptun-client \
+PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy:naiveproxy \
+PACKAGE_$(PKG_NAME)_INCLUDE_Redsocks2:redsocks2 \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client:shadowsocks-libev-ss-local \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client:shadowsocks-libev-ss-redir \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server:shadowsocks-libev-ss-server \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client:shadowsocks-rust-sslocal \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server:shadowsocks-rust-ssserver \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs:simple-obfs-client \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin:v2ray-plugin \
+PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-local \
+PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-redir \
+PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server:shadowsocksr-libev-ssr-server \
+PACKAGE_$(PKG_NAME)_INCLUDE_Trojan:trojan
+PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server:shadowsocksr-libev-ssr-server
define Package/$(PKG_NAME)/config
select PACKAGE_luci-lua-runtime if PACKAGE_$(PKG_NAME)
choice
prompt "Transparent Proxy Backend"
@@ -103,37 +77,24 @@ endchoice
choice
prompt "Shadowsocks Client Selection"
default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client if aarch64 || x86_64
default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client
default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client
bool "None"
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client
bool "Shadowsocks-libev"
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client
bool "Shadowsocks-rust"
depends on aarch64||arm||i386||mips||mipsel||x86_64
depends on !(TARGET_x86_geode||TARGET_x86_legacy)
bool "Shadowsocks Rust"
endchoice
choice
prompt "Shadowsocks Server Selection"
default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server if aarch64
default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server if i386||x86_64||arm
default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server
bool "None"
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server
bool "Shadowsocks-libev"
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server
bool "Shadowsocks-rust"
depends on aarch64||arm||i386||mips||mipsel||x86_64
depends on !(TARGET_x86_geode||TARGET_x86_legacy)
bool "Shadowsocks Rust"
endchoice
choice
@@ -144,45 +105,35 @@ choice
config PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY
bool "None"
config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray
bool "V2ray-core"
config PACKAGE_$(PKG_NAME)_INCLUDE_Xray
bool "Xray-core"
select PACKAGE_xray-core
endchoice
config PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG
bool "Include ChinaDNS-NG"
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS
bool "Include DNS2socks"
default y
config PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS_RUST
bool "Include DNS2socks-Rust"
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_DNSPROXY
bool "Include DNSproxy"
default n
default y if aarch64||arm||i386||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS
bool "Include MosDNS"
default y if aarch64||i386||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria
bool "Include Hysteria"
select PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client
bool "Include Tuic-Client"
select PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG
select PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks
depends on aarch64||arm||i386||x86_64
depends on !(TARGET_x86_geode||TARGET_x86_legacy)
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy
bool "Include HTTP(S) Proxy Server"
select PACKAGE_3proxy
default y if (aarch64||arm||i386||loongarch64||riscv64||x86_64)
config PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo
bool "Include Mihomo (Clash Support)"
select PACKAGE_mihomo
depends on aarch64||arm||i386||loongarch64||riscv64||x86_64
default y if aarch64||arm||i386||loongarch64||riscv64||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_GeoData
bool "Include GeoData (GeoIP and GeoSite)"
select PACKAGE_v2ray-geoip
select PACKAGE_v2ray-geosite
default y if i386||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS
bool "Include Shadow-TLS"
@@ -192,10 +143,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS
depends on !(TARGET_x86_geode||TARGET_x86_legacy)
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks
bool "Include IPT2Socks"
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun
bool "Include Kcptun"
default n
@@ -205,13 +152,9 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy
depends on !(arc||armeb||mips||mips64||powerpc||TARGET_gemini)
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Redsocks2
bool "Include Redsocks2"
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs
bool "Include Shadowsocks Simple Obfs Plugin"
default y
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin
bool "Include Shadowsocks V2ray Plugin"
@@ -223,11 +166,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client
config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server
bool "Include ShadowsocksR Libev Server"
default y if i386||x86_64||arm
config PACKAGE_$(PKG_NAME)_INCLUDE_Trojan
bool "Include Trojan"
select PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks
default n
endef
@@ -240,4 +178,41 @@ endef
include $(TOPDIR)/feeds/luci/luci.mk
define Package/$(PKG_NAME)/install
ifneq ($(wildcard ${CURDIR}/luasrc),)
$(INSTALL_DIR) $(1)$(LUCI_LIBRARYDIR)
cp -pR $(PKG_BUILD_DIR)/luasrc/* $(1)$(LUCI_LIBRARYDIR)/
$(FIND) $(1)$(LUCI_LIBRARYDIR)/ -type f -name '*.luadoc' | $(XARGS) rm
$(if $(CONFIG_LUCI_SRCDIET),$(call SrcDiet,$(1)$(LUCI_LIBRARYDIR)/),true)
$(call SubstituteVersion,$(1)$(LUCI_LIBRARYDIR)/)
endif
ifneq ($(wildcard ${CURDIR}/ucode),)
$(INSTALL_DIR) $(1)$(UCODE_LIBRARYDIR)
cp -pR $(PKG_BUILD_DIR)/ucode/* $(1)$(UCODE_LIBRARYDIR)/
$(call SubstituteVersion,$(1)$(UCODE_LIBRARYDIR)/)
endif
ifneq ($(wildcard ${CURDIR}/htdocs),)
$(INSTALL_DIR) $(1)$(HTDOCS)
cp -pR $(PKG_BUILD_DIR)/htdocs/* $(1)$(HTDOCS)/
$(if $(CONFIG_LUCI_JSMIN),$(call JsMin,$(1)$(HTDOCS)/),true)
$(if $(CONFIG_LUCI_CSSTIDY),$(call CssTidy,$(1)$(HTDOCS)/),true)
endif
ifneq ($(wildcard ${CURDIR}/root),)
$(INSTALL_DIR) $(1)/
cp -pR $(PKG_BUILD_DIR)/root/* $(1)/
endif
ifneq ($(wildcard ${CURDIR}/src),)
$(call Build/Install/Default)
$(CP) $(PKG_INSTALL_DIR)/* $(1)/
endif
if [ -d "$(1)/usr/bin" ]; then \
chmod 0755 "$(1)/usr/bin/ssr-monitor" 2>/dev/null || true; \
chmod 0755 "$(1)/usr/bin/ssr-rules" 2>/dev/null || true; \
chmod 0755 "$(1)/usr/bin/ssr-switch" 2>/dev/null || true; \
fi
if [ -f "$(1)/etc/hotplug.d/iface/99-ssrplus-pppoe" ]; then \
chmod 0755 "$(1)/etc/hotplug.d/iface/99-ssrplus-pppoe" 2>/dev/null || true; \
fi
endef
# call BuildPackage - OpenWrt buildroot signature
@@ -1,2 +0,0 @@
/*! Sortable 1.15.7 - MIT | git://github.com/SortableJS/Sortable.git */
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n<e;n++)o[n]=t[n];return o}function i(t,e,n){return(e=function(t){t=function(t,e){if("object"!=typeof t||!t)return t;var n=t[Symbol.toPrimitive];if(void 0===n)return("string"===e?String:Number)(t);e=n.call(t,e||"default");if("object"!=typeof e)return e;throw new TypeError("@@toPrimitive must return a primitive value.")}(t,"string");return"symbol"==typeof t?t:t+""}(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function a(){return(a=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)!{}.hasOwnProperty.call(o,n)||(t[n]=o[n])}return t}).apply(null,arguments)}function r(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?r(Object(n),!0).forEach(function(t){i(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):r(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function l(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};var n,o={};for(n in t)if({}.hasOwnProperty.call(t,n)){if(-1!==e.indexOf(n))continue;o[n]=t[n]}return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],-1===e.indexOf(n)&&{}.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function e(t){return function(t){if(Array.isArray(t))return o(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return o(t,e);var n={}.toString.call(t).slice(8,-1);return"Map"===(n="Object"===n&&t.constructor?t.constructor.name:n)||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?o(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(t){return(s="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function t(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),c=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),d=t(/iP(ad|od|hone)/i),n=t(/chrome/i)&&t(/android/i),h={capture:!1,passive:!1};function f(t,e,n){t.addEventListener(e,n,!y&&h)}function p(t,e,n){t.removeEventListener(e,n,!y&&h)}function g(t,e){if(e&&(">"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function m(t){return t.host&&t!==document&&t.host.nodeType&&t.host!==t?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&g(t,e)||o&&t===n)return t}while(t!==n&&(t=m(t)))}return null}var v,b=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(b," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(b," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function D(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function E(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&("none"!==R(i,"transform")||n&&"static"!==R(i,"position"))){var h=i.getBouLine truncated
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,7 @@
local m, s, o
local cbi = require "luci.cbi"
local uci = require "luci.model.uci".cursor()
local URL = require "url"
-- 获取 LAN IP 地址
function lanip()
@@ -30,16 +31,32 @@ local lan_ip = lanip()
local server_table = {}
local type_table = {}
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
end
local function clash_display_name(s)
if s.type ~= "clash" or not s.clash_url or s.clash_url == "" then
return nil
end
local ok, parsed = pcall(URL.parse, s.clash_url)
if ok and parsed and parsed.host then
return "[CLASH]:" .. parsed.host
end
return "[CLASH]"
end
uci:foreach("shadowsocksr", "servers", function(s)
if s.alias then
if s.type ~= "tun" and s.alias then
server_table[s[".name"]] = "[%s]:%s" % {string.upper(s.v2ray_protocol or s.type), s.alias}
elseif s.server and s.server_port then
elseif s.type ~= "tun" and s.server and s.server_port then
server_table[s[".name"]] = "[%s]:%s:%s" % {string.upper(s.v2ray_protocol or s.type), s.server, s.server_port}
elseif s.type ~= "tun" then
local display_name = clash_display_name(s)
if display_name then
server_table[s[".name"]] = display_name
end
end
if s.type then
if s.type and s.type ~= "tun" then
type_table[s[".name"]] = s.type
end
end)
@@ -79,6 +96,13 @@ o.datatype = "uinteger"
o:depends("enable_switch", "1")
o.default = 3
s:append(cbi.Template("shadowsocksr/advanced_switch_compact"))
o = s:option(Value, "default_node_local_port", translate("Default Node Local Port"))
o.datatype = "port"
o.default = 1234
o.rmempty = false
o = s:option(Value, "gfwlist_url", translate("gfwlist Update url"))
o:value("https://fastly.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt", translate("v2fly/domain-list-community"))
o:value("https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt", translate("Loyalsoldier/v2ray-rules-dat"))
@@ -92,145 +116,6 @@ o:value("https://ispip.clang.cn/all_cn_cidr.txt", translate("Clang.CN.CIDR"))
o:value("https://fastly.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china.txt", translate("china-operator-ip"))
o.default = "https://ispip.clang.cn/all_cn.txt"
o = s:option(Flag, "netflix_enable", translate("Enable Netflix Mode"))
o.description = translate("When disabled shunt mode, will same time stopped shunt service.")
o.rmempty = false
o = s:option(Value, "nfip_url", translate("nfip_url"))
o:value("https://fastly.jsdelivr.net/gh/QiuSimons/Netflix_IP/NF_only.txt", translate("Netflix IP Only"))
o:value("https://fastly.jsdelivr.net/gh/QiuSimons/Netflix_IP/getflix.txt", translate("Netflix and AWS"))
o.default = "https://fastly.jsdelivr.net/gh/QiuSimons/Netflix_IP/NF_only.txt"
o.description = translate("Customize Netflix IP Url")
o:depends("netflix_enable", "1")
o = s:option(ListValue, "shunt_dns_mode", translate("DNS Query Mode For Shunt Mode"))
if is_finded("dns2socks") then
o:value("1", translate("Use DNS2SOCKS query and cache"))
end
if is_finded("dns2socks-rust") then
o:value("2", translate("Use DNS2SOCKS-RUST query and cache"))
end
if is_finded("mosdns") then
o:value("3", translate("Use MosDNS query"))
end
if is_finded("dnsproxy") then
o:value("4", translate("Use DNSPROXY query and cache"))
end
if is_finded("chinadns-ng") then
o:value("5", translate("Use ChinaDNS-NG query and cache"))
end
o:depends("netflix_enable", "1")
o.default = 1
o = s:option(Value, "shunt_dnsserver", translate("Anti-pollution DNS Server For Shunt Mode"))
o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)"))
o:value("8.8.8.8:53", translate("Google Public DNS (8.8.8.8)"))
o:value("208.67.222.222:53", translate("OpenDNS (208.67.222.222)"))
o:value("208.67.220.220:53", translate("OpenDNS (208.67.220.220)"))
o:value("209.244.0.3:53", translate("Level 3 Public DNS (209.244.0.3)"))
o:value("209.244.0.4:53", translate("Level 3 Public DNS (209.244.0.4)"))
o:value("4.2.2.1:53", translate("Level 3 Public DNS (4.2.2.1)"))
o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)"))
o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)"))
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)"))
o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
o:depends("shunt_dns_mode", "1")
o:depends("shunt_dns_mode", "2")
o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)")
o.datatype = "ip4addrport"
o = s:option(Value, "shunt_mosdns_dnsserver", translate("Anti-pollution DNS Server"))
o:value("tcp://8.8.4.4:53,tcp://8.8.8.8:53", translate("Google Public DNS"))
o:value("tcp://208.67.222.222:53,tcp://208.67.220.220:53", translate("OpenDNS"))
o:value("tcp://209.244.0.3:53,tcp://209.244.0.4:53", translate("Level 3 Public DNS-1 (209.244.0.3-4)"))
o:value("tcp://4.2.2.1:53,tcp://4.2.2.2:53", translate("Level 3 Public DNS-2 (4.2.2.1-2)"))
o:value("tcp://4.2.2.3:53,tcp://4.2.2.4:53", translate("Level 3 Public DNS-3 (4.2.2.3-4)"))
o:value("tcp://1.1.1.1:53,tcp://1.0.0.1:53", translate("Cloudflare DNS"))
o:depends("shunt_dns_mode", "3")
o.description = translate("Custom DNS Server format as tcp://IP:PORT or tls://DOMAIN:PORT (tcp://8.8.8.8 or tls://dns.google:853)")
o = s:option(Flag, "shunt_mosdns_ipv6", translate("Disable IPv6 In MosDNS Query Mode (Shunt Mode)"))
o:depends("shunt_dns_mode", "3")
o.rmempty = false
o.default = "0"
if is_finded("dnsproxy") then
o = s:option(ListValue, "shunt_parse_method", translate("Select DNS parse Mode"))
o.description = translate(
"<ul>" ..
"<li>" .. translate("When use DNS list file, please ensure list file exists and is formatted correctly.") .. "</li>" ..
"<li>" .. translate("Tips: Dnsproxy DNS Parse List Path:") ..
" <a href='http://" .. lan_ip .. "/cgi-bin/luci/admin/services/shadowsocksr/control' target='_blank'>" ..
translate("Click here to view or manage the DNS list file") .. "</a>" .. "</li>" ..
"</ul>"
)
o:value("single_dns", translate("Set Single DNS"))
o:value("parse_file", translate("Use DNS List File"))
o:depends("shunt_dns_mode", "4")
o.rmempty = true
o.default = "single_dns"
o = s:option(Value, "dnsproxy_shunt_forward", translate("Anti-pollution DNS Server"))
o:value("sdns://AgUAAAAAAAAABzguOC40LjQgsKKKE4EwvtIbNjGjagI2607EdKSVHowYZtyvD9iPrkkHOC44LjQuNAovZG5zLXF1ZXJ5", translate("Google DNSCrypt SDNS"))
o:value("sdns://AgcAAAAAAAAAACC2vD25TAYM7EnyCH8Xw1-0g5OccnTsGH9vQUUH0njRtAxkbnMudHduaWMudHcKL2Rucy1xdWVyeQ", translate("TWNIC-101 DNSCrypt SDNS"))
o:value("sdns://AgcAAAAAAAAADzE4NS4yMjIuMjIyLjIyMiAOp5Svj-oV-Fz-65-8H2VKHLKJ0egmfEgrdPeAQlUFFA8xODUuMjIyLjIyMi4yMjIKL2Rucy1xdWVyeQ", translate("dns.sb DNSCrypt SDNS"))
o:value("sdns://AgMAAAAAAAAADTE0OS4xMTIuMTEyLjkgsBkgdEu7dsmrBT4B4Ht-BQ5HPSD3n3vqQ1-v5DydJC8SZG5zOS5xdWFkOS5uZXQ6NDQzCi9kbnMtcXVlcnk", translate("Quad9 DNSCrypt SDNS"))
o:value("sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", translate("AdGuard DNSCrypt SDNS"))
o:value("sdns://AgcAAAAAAAAABzEuMC4wLjGgENk8mGSlIfMGXMOlIlCcKvq7AVgcrZxtjon911-ep0cg63Ul-I8NlFj4GplQGb_TTLiczclX57DvMV8Q-JdjgRgSZG5zLmNsb3VkZmxhcmUuY29tCi9kbnMtcXVlcnk", translate("Cloudflare DNSCrypt SDNS"))
o:value("sdns://AgcAAAAAAAAADjEwNC4xNi4yNDkuMjQ5ABJjbG91ZGZsYXJlLWRucy5jb20KL2Rucy1xdWVyeQ", translate("cloudflare-dns.com DNSCrypt SDNS"))
o:depends("shunt_parse_method", "single_dns")
o.description = translate("Custom DNS Server (support: IP:Port or tls://IP:Port or https://IP/dns-query and other format).")
o = s:option(ListValue, "shunt_upstreams_logic_mode", translate("Defines the upstreams logic mode"))
o.description = translate(
"<ul>" ..
"<li>" .. translate("Defines the upstreams logic mode, possible values: load_balance, parallel, fastest_addr (default: load_balance).") .. "</li>" .. "<li>" .. translate("When two or more DNS servers are deployed, enable this function.") .. "</li>" ..
"</ul>"
)
o:value("load_balance", translate("load_balance"))
o:value("parallel", translate("parallel"))
o:value("fastest_addr", translate("fastest_addr"))
o:depends("shunt_parse_method", "parse_file")
o.rmempty = true
o.default = "load_balance"
o = s:option(Flag, "shunt_dnsproxy_ipv6", translate("Disable IPv6 query mode"))
o.description = translate("When disabled, all AAAA requests are not resolved.")
o:depends("shunt_parse_method", "single_dns")
o:depends("shunt_parse_method", "parse_file")
o.rmempty = false
o.default = "1"
end
if is_finded("chinadns-ng") then
o = s:option(Value, "chinadns_ng_shunt_dnsserver", translate("Anti-pollution DNS Server For Shunt Mode"))
o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)"))
o:value("8.8.8.8:53", translate("Google Public DNS (8.8.8.8)"))
o:value("208.67.222.222:53", translate("OpenDNS (208.67.222.222)"))
o:value("208.67.220.220:53", translate("OpenDNS (208.67.220.220)"))
o:value("209.244.0.3:53", translate("Level 3 Public DNS (209.244.0.3)"))
o:value("209.244.0.4:53", translate("Level 3 Public DNS (209.244.0.4)"))
o:value("4.2.2.1:53", translate("Level 3 Public DNS (4.2.2.1)"))
o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)"))
o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)"))
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)"))
o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
o:depends("shunt_dns_mode", "5")
o.description = translate(
"<ul>" ..
"<li>" .. translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") .. "</li>" ..
"<li>" .. translate("Muitiple DNS server can saperate with ','") .. "</li>" ..
"</ul>"
)
o = s:option(ListValue, "chinadns_ng_shunt_proto", translate("ChinaDNS-NG shunt query protocol"))
o:value("none", translate("UDP/TCP upstream"))
o:value("tcp", translate("TCP upstream"))
o:value("udp", translate("UDP upstream"))
o:value("tls", translate("DoT upstream (Need use wolfssl version)"))
o:depends("shunt_dns_mode", "5")
end
o = s:option(Flag, "apple_optimization", translate("Apple domains optimization"), translate("For Apple domains equipped with Chinese mainland CDN, always responsive to Chinese CDN IP addresses"))
o.rmempty = false
o.default = "1"
@@ -266,45 +151,13 @@ end
-- [[ SOCKS5 Proxy ]]--
s = m:section(TypedSection, "socks5_proxy", translate("Global SOCKS5 Proxy Server"))
s.anonymous = true
-- s.description = translate("Only Same as Global Server is supported here. If the current main program supports a built-in SOCKS5 listener, it will be enabled in-process; otherwise SSR Plus will fall back to microsocks.")
-- Enable/Disable Option
o = s:option(Flag, "enabled", translate("Enable"))
o.default = 0
o.rmempty = false
-- Server Selection
o = s:option(ListValue, "server", translate("Server"))
o:value("same", translate("Same as Global Server"))
for _, key in pairs(key_table) do
o:value(key, server_table[key])
end
o.default = "same"
o.rmempty = false
-- Dynamic value handling based on enabled/disabled state
o.cfgvalue = function(self, section)
local enabled = m:get(section, "enabled")
if enabled == "0" then
return m:get(section, "old_server")
end
return Value.cfgvalue(self, section)-- Default to `same` when enabled
end
o.write = function(self, section, value)
local enabled = m:get(section, "enabled")
if enabled == "0" then
local old_server = Value.cfgvalue(self, section)
if old_server ~= "nil" then
m:set(section, "old_server", old_server)
end
m:set(section, "server", "nil")
else
m:del(section, "old_server")
-- Write the value normally when enabled
Value.write(self, section, value)
end
end
-- Socks Auth
if is_finded("xray") then
o = s:option(ListValue, "socks5_auth", translate("Socks5 Auth Mode"), translate("Socks protocol auth methods, default:noauth."))
@@ -312,13 +165,6 @@ o.default = "noauth"
o:value("noauth", "NOAUTH")
o:value("password", "PASSWORD")
o.rmempty = true
for key, server_type in pairs(type_table) do
if server_type == "v2ray" then
-- 如果服务器类型是 v2ray,则设置依赖项显示
o:depends("server", key)
end
end
o:depends({server = "same", disable = true})
-- Socks User
o = s:option(Value, "socks5_user", translate("Socks5 User"), translate("Only when Socks5 Auth Mode is password valid, Mandatory."))
@@ -335,13 +181,6 @@ o:depends("socks5_auth", "password")
o = s:option(Flag, "socks5_mixed", translate("Enabled Mixed"), translate("Mixed as an alias of socks, default:Enabled."))
o.default = "1"
o.rmempty = false
for key, server_type in pairs(type_table) do
if server_type == "v2ray" then
-- 如果服务器类型是 v2ray,则设置依赖项显示
o:depends("server", key)
end
end
o:depends({server = "same", disable = true})
end
-- Local Port
@@ -350,6 +189,37 @@ o.datatype = "port"
o.default = 1080
o.rmempty = false
if is_finded("3proxy") then
-- [[ HTTP/HTTPS Proxy ]]--
s = m:section(TypedSection, "http_proxy", translate("Global HTTP/HTTPS Proxy Server"))
s.anonymous = true
-- s.description = translate("Only Same as Global Server is supported here. HTTP/HTTPS proxy service is provided through 3proxy.")
o = s:option(Flag, "enabled", translate("Enable"))
o.default = 0
o.rmempty = false
o = s:option(ListValue, "http_auth", translate("HTTP Auth Mode"), translate("HTTP proxy auth method, default:none."))
o.default = "none"
o:value("none", "NONE")
o:value("password", "PASSWORD")
o.rmempty = false
o = s:option(Value, "http_user", translate("HTTP User"), translate("Only when HTTP Auth Mode is password valid, Mandatory."))
o.rmempty = true
o:depends("http_auth", "password")
o = s:option(Value, "http_pass", translate("HTTP Password"), translate("Only when HTTP Auth Mode is password valid, Not mandatory."))
o.password = true
o.rmempty = true
o:depends("http_auth", "password")
o = s:option(Value, "local_port", translate("Local Port"))
o.datatype = "port"
o.default = 3128
o.rmempty = false
end
-- [[ fragmen Settings ]]--
if is_finded("xray") then
s = m:section(TypedSection, "global_xray_fragment", translate("Xray Fragment Settings"))
@@ -20,7 +20,7 @@ local xray_version_val = 0
-- 确保正确判断程序是否存在
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
end
local function is_installed(e)
@@ -142,16 +142,14 @@ local function set_apply_on_parse(map)
local old = map.on_after_save
map.on_after_save = function(self)
if old then old(self) end
map:set("@global[0]", "timestamp", os.time())
--map:set("@global[0]", "timestamp", os.time())
end
end
end
local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local")
local has_trojan = is_finded("trojan")
local has_mihomo = is_finded("mihomo")
local has_xray = is_finded("xray")
local has_hysteria2 = is_finded("hysteria")
local server_table = {}
local encrypt_methods = {
@@ -256,6 +254,56 @@ local tls_flows = {
"none"
}
local function migrate_xray_protocol_nodes()
local changed = false
uci:foreach("shadowsocksr", "servers", function(section)
local sid = section[".name"]
local stype = section.type
local proto = section.v2ray_protocol
local escaped_sid = luci.util.shellquote(sid)
if stype == "ss" or stype == "ss-libev" then
if has_mihomo then
luci.sys.call(string.format("uci set shadowsocksr.%s.type='ss'", escaped_sid))
changed = true
elseif has_ss_rust then
luci.sys.call(string.format("uci set shadowsocksr.%s.type='ss-rust'", escaped_sid))
changed = true
elseif has_xray then
luci.sys.call(string.format("uci set shadowsocksr.%s.type='v2ray' && " .. "uci set shadowsocksr.%s.v2ray_protocol='shadowsocks'", escaped_sid, escaped_sid))
changed = true
end
elseif stype == "v2ray" and proto == "shadowsocks" and has_mihomo then
luci.sys.call(string.format("uci set shadowsocksr.%s.type='ss' && " .. "uci delete shadowsocksr.%s.v2ray_protocol", escaped_sid, escaped_sid))
changed = true
end
if stype == "hysteria2" then
luci.sys.call(string.format("uci set shadowsocksr.%s.type='v2ray' && " .. "uci set shadowsocksr.%s.v2ray_protocol='hysteria2'", escaped_sid, escaped_sid))
changed = true
elseif stype == "trojan" then
luci.sys.call(string.format("uci set shadowsocksr.%s.type='v2ray' && " .. "uci set shadowsocksr.%s.v2ray_protocol='trojan'", escaped_sid, escaped_sid))
changed = true
end
end)
local subscribe_sid = uci:get_first("shadowsocksr", "server_subscribe")
if subscribe_sid then
local old_options = {"xray_hy2_type", "xray_tj_type", "ss_type"}
local escaped_sub_sid = luci.util.shellquote(subscribe_sid)
for _, opt in ipairs(old_options) do
if uci:get("shadowsocksr", subscribe_sid, opt) then
luci.sys.call(string.format("uci delete shadowsocksr.%s.%s", escaped_sub_sid, opt))
changed = true
end
end
end
if changed then
luci.sys.call("uci commit shadowsocksr")
end
end
migrate_xray_protocol_nodes()
m = Map("shadowsocksr", translate("Edit ShadowSocksR Server"))
m.redirect = url("servers")
if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then
@@ -264,6 +312,14 @@ if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then
end
-- 保存&应用成功后跳转到节点列表
set_apply_on_parse(m)
local old_after_save = m.on_after_save
m.on_after_save = function(self)
if old_after_save then old_after_save(self) end
local node_type = self.uci:get("shadowsocksr", sid, "type")
if node_type == "clash" then
luci.sys.call(string.format("/etc/init.d/shadowsocksr clash_cache %s >/dev/null 2>&1 &", sid))
end
end
-- [[ Servers Setting ]]--
s = m:section(NamedSection, sid, "servers")
@@ -275,88 +331,27 @@ o.rawhtml = true
o.template = "shadowsocksr/ssrurl"
o.value = sid
-- 新增一个选择框,用于选择 Xray 或 Hysteria2 核心
o = s:option(ListValue, "_xray_hy2_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Hysteria2")))
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
-- 注意:Auto 选项使用特殊字符串 "__auto__" 而不是空字符串
o:value("__auto__", translate("Auto"))
if has_hysteria2 then
o:value("hysteria2", translate("Hysteria2"))
end
if has_xray then
o:value("v2ray", translate("Xray (Hysteria2)"))
end
-- 读取全局 xray_hy2_type
o.cfgvalue = function(self, section)
local val = uci:get("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type")
if val == nil or val == "" then
return "__auto__" -- 对应 Auto 选项
end
return val
end
o.rmempty = true
-- 保存时更新全局配置
o.write = function(self, section, value)
if value == "__auto__" then
-- 删除全局配置
uci:delete("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type")
else
-- 设置具体值
uci:set("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type", value)
end
end
-- 新增一个选择框,用于选择 Xray 或 Trojan 核心
o = s:option(ListValue, "_xray_tj_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Trojan")))
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
-- 注意:Auto 选项使用特殊字符串 "__auto__" 而不是空字符串
o:value("__auto__", translate("Auto"))
if has_hysteria2 then
o:value("trojan", translate("Trojan"))
end
if has_xray then
o:value("v2ray", translate("Xray (Trojan)"))
end
-- 读取全局 xray_tj_type
o.cfgvalue = function(self, section)
local val = uci:get("shadowsocksr", "@server_subscribe[0]", "xray_tj_type")
if val == nil or val == "" then
return "__auto__" -- 对应 Auto 选项
end
return val
end
o.rmempty = true
-- 保存时更新全局配置
o.write = function(self, section, value)
if value == "__auto__" then
-- 删除全局配置
uci:delete("shadowsocksr", "@server_subscribe[0]", "xray_tj_type")
else
-- 设置具体值
uci:set("shadowsocksr", "@server_subscribe[0]", "xray_tj_type", value)
end
end
o = s:option(ListValue, "type", translate("Server Node Type"))
if is_finded("xray") or is_finded("v2ray") then
if is_finded("xray") then
o:value("v2ray", translate("V2Ray/XRay"))
end
if is_finded("ssr-redir") then
o:value("ssr", translate("ShadowsocksR"))
end
if has_ss_rust or has_ss_libev then
o:value("ss", translate("ShadowSocks"))
if has_mihomo then
o:value("ss", translate("ShadowSocks"))
end
if is_finded("trojan") then
o:value("trojan", translate("Trojan"))
if has_ss_rust then
o:value("ss-rust", translate("ShadowSocks"))
end
if is_finded("naive") then
o:value("naiveproxy", translate("NaiveProxy"))
end
if is_finded("hysteria") then
o:value("hysteria2", translate("Hysteria2"))
if is_finded("mihomo") then
o:value("clash", translate("Clash/Mihomo"))
end
if is_finded("tuic-client") then
if is_finded("mihomo") then
o:value("tuic", translate("TUIC"))
end
if is_finded("shadow-tls") and is_finded("sslocal") then
@@ -365,78 +360,51 @@ end
if is_finded("ipt2socks") then
o:value("socks5", translate("Socks5"))
end
if is_finded("redsocks2") then
o:value("tun", translate("Network Tunnel"))
end
local old_cfgvalue = o.cfgvalue
o.cfgvalue = function(self, section)
local val = self.map.uci:get("shadowsocksr", section, "type")
if val == "ss-rust" or val == "ss-libev" then
return "ss"
end
if old_cfgvalue then
local val = self.map.uci:get("shadowsocksr", section, "type")
if old_cfgvalue then
return old_cfgvalue(self, section)
end
return val
end
return val
end
-- 重写 write,当用户选择 "ss" 时不写入(由 _ss_core 负责写入具体核心)
local old_write = o.write
o.write = function(self, section, value)
if value == "ss" then
return -- 不做任何写入,等待 _ss_core 写入
end
if old_write then
old_write(self, section, value)
else
self.map.uci:set("shadowsocksr", section, "type", value)
end
end
o.description = translate("Using incorrect encryption mothod may causes service fail to start")
o = s:option(Value, "alias", translate("Alias(optional)"))
o = s:option(ListValue, "iface", translate("Network interface to use"))
for _, e in ipairs(luci.sys.net.devices()) do
if e ~= "lo" then
o:value(e)
local function clash_source_formvalue(map, section, option)
local value = map:formvalue("cbid." .. map.config .. "." .. section .. "." .. option)
if value == nil then
value = map.uci:get(map.config, section, option)
end
return trim(value or "")
end
o:depends("type", "tun")
o.description = translate("Redirect traffic to this network interface")
-- 新增一个选择框,用于选择 Shadowsocks 具体版本(仅当节点类型为 ss 或其具体子类型时显示)
o = s:option(ListValue, "_ss_core", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
o.description = translate("Selection ShadowSocks Node Use Version.")
if has_ss_rust then
o:value("ss-rust", translate("ShadowSocks-rust Version"))
local function validate_clash_source(self, value, section)
local clash_url = clash_source_formvalue(self.map, section, "clash_url")
local clash_path = clash_source_formvalue(self.map, section, "clash_path")
if clash_url == "" and clash_path == "" then
return nil, translate("Please specify either a Clash subscription URL or a local YAML path.")
end
return value
end
if has_ss_libev then
o:value("ss-libev", translate("ShadowSocks-libev Version"))
end
o.cfgvalue = function(self, section)
-- 读取当前节点的 type 值,如果已经是具体核心则显示对应的选项
local node_type = self.map.uci:get("shadowsocksr", section, "type")
if node_type == "ss-rust" or node_type == "ss-libev" then
return node_type
end
-- 如果全局 ss_type 有值且为具体核心则返回该值
local ss_type = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type")
if ss_type == "ss-rust" or ss_type == "ss-libev" then
return ss_type
end
-- 如果节点 type 是旧的 "ss",则返回空,手动选择
return nil
end
-- 显示条件:当节点类型为 "ss" 或其具体核心时显示
o:depends("type", "ss")
o = s:option(Value, "clash_url", translate("Clash Subscription URL"))
o.placeholder = "https://example.com/config.yaml"
o.rmempty = true
-- 保存时,将选择的值直接写入当前节点的 type 字段
o.write = function(self, section, value)
if value and value ~= "" then
self.map.uci:set("shadowsocksr", section, "type", value)
end
end
o:depends("type", "clash")
o.validate = validate_clash_source
o = s:option(Value, "clash_path", translate("Clash YAML Path"))
o.placeholder = "/etc/ssrplus/clash/custom.yaml"
o.rmempty = true
o:depends("type", "clash")
o.validate = validate_clash_source
o = s:option(Value, "clash_user_agent", translate("Clash User-Agent"))
o.default = "clash"
o.rmempty = false
o:depends("type", "clash")
o = s:option(ListValue, "v2ray_protocol", translate("V2Ray/XRay protocol"))
o:value("vless", translate("VLESS"))
@@ -454,30 +422,54 @@ o:value("http", translate("HTTP"))
o:depends("type", "v2ray")
o = s:option(Value, "server", translate("Server Address"))
o.datatype = "host"
o.datatype = "or(host,ip6addr)"
o.rmempty = false
o:depends("type", "ssr")
o:depends("type", "ss")
o:depends("type", "v2ray")
o:depends("type", "ss-rust")
o:depends("type", "trojan")
o:depends("type", "naiveproxy")
o:depends("type", "hysteria2")
o:depends("type", "tuic")
o:depends("type", "shadowtls")
o:depends("type", "socks5")
local protocols = s.fields["v2ray_protocol"].keylist
if protocols and type(protocols) == "table" and #protocols > 0 then
for _, proto in ipairs(protocols) do
if not proto:find("^_") then
if proto == "hysteria2" then
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false})
else
o:depends({type = "v2ray", v2ray_protocol = proto})
end
end
end
end
o = s:option(Value, "server_port", translate("Server Port"))
o.datatype = "port"
o.rmempty = true
o:depends("type", "ssr")
o:depends("type", "ss")
o:depends("type", "v2ray")
o:depends("type", "ss-rust")
o:depends("type", "trojan")
o:depends("type", "naiveproxy")
o:depends("type", "hysteria2")
o:depends("type", "tuic")
o:depends("type", "shadowtls")
o:depends("type", "socks5")
local protocols = s.fields["v2ray_protocol"].keylist
if protocols and type(protocols) == "table" and #protocols > 0 then
for _, proto in ipairs(protocols) do
if not proto:find("^_") then
if proto == "hysteria2" then
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false})
else
o:depends({type = "v2ray", v2ray_protocol = proto})
end
end
end
end
o = s:option(Flag, "auth_enable", translate("Enable Authentication"))
o.rmempty = false
@@ -498,6 +490,7 @@ o.password = true
o.rmempty = true
o:depends("type", "ssr")
o:depends("type", "ss")
o:depends("type", "ss-rust")
o:depends("type", "trojan")
o:depends("type", "naiveproxy")
o:depends("type", "shadowtls")
@@ -524,6 +517,7 @@ for _, v in ipairs(encrypt_methods_ss) do
end
end
o.rmempty = true
o:depends("type", "ss-rust")
o:depends("type", "ss")
o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"})
@@ -542,22 +536,30 @@ o.default = "1"
o = s:option(Flag, "enable_plugin", translate("Enable Plugin"))
o.rmempty = true
o:depends("type", "ss")
o:depends("type", "ss-rust")
o.default = "0"
-- Shadowsocks Plugin
o = s:option(ListValue, "plugin", translate("Obfs"))
o:value("none", translate("None"))
if is_finded("obfs-local") then
if has_mihomo or is_finded("obfs-local") then
o:value("obfs-local", translate("obfs-local"))
end
if is_finded("v2ray-plugin") then
if has_mihomo or is_finded("v2ray-plugin") then
o:value("v2ray-plugin", translate("v2ray-plugin"))
end
if has_mihomo then
o:value("gost-plugin", translate("gost-plugin"))
end
if is_finded("xray-plugin") then
o:value("xray-plugin", translate("xray-plugin"))
end
if is_finded("shadow-tls") then
o:value("shadow-tls", translate("shadow-tls"))
if has_mihomo or is_finded("shadow-tls") then
o:value("shadow-tls", translate("Shadow-TLS"))
end
if has_mihomo then
o:value("restls", translate("restls"))
o:value("kcptun", translate("kcptun"))
end
o:value("custom", translate("Custom"))
o.rmempty = true
@@ -593,6 +595,21 @@ o:depends("type", "ssr")
-- [[ Hysteria2 ]]--
o = s:option(Flag, "hysteria2_realms", translate("Hysteria2 Realms"))
o.default = "0"
if xray_version_val > 260509 then
o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
else
o:depends({type = "v2ray", v2ray_protocol = "__hide"})
end
o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends("hysteria2_realms", true)
o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN"))
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends("hysteria2_realms", true)
o = s:option(Value, "hy2_auth", translate("Users Authentication"))
o:depends("type", "hysteria2")
o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
@@ -601,7 +618,7 @@ o.rmempty = false
o = s:option(Flag, "flag_port_hopping", translate("Enable Port Hopping"))
o:depends("type", "hysteria2")
o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false})
o.rmempty = true
o.default = "0"
@@ -643,17 +660,30 @@ o.rmempty = true
o.default = "0"
o = s:option(Value, "obfs_type", translate("Obfuscation Type"))
o:value("", translate("Disable"))
o:value("salamander")
o:value("gecko")
o.rmempty = true
o:depends({type = "hysteria2", flag_obfs = true})
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", flag_obfs = true})
o.rmempty = true
o.placeholder = "salamander"
o = s:option(Value, "salamander", translate("Obfuscation Password"))
o:depends({type = "hysteria2", flag_obfs = true})
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", flag_obfs = true})
o.password = true
o.rmempty = true
o.placeholder = "cry_me_a_r1ver"
o:depends({type = "hysteria2", flag_obfs = true})
local obfs = s.fields["obfs_type"].keylist
if obfs and type(obfs) == "table" and #obfs > 0 then
for _, v in ipairs(obfs) do
if v and v ~= "" then
o:depends({
type = "v2ray",
v2ray_protocol = "hysteria2",
obfs_type = v,
flag_obfs = true
})
end
end
end
o = s:option(Flag, "flag_quicparam", translate("Hysterir QUIC parameters"))
o:depends("type", "hysteria2")
@@ -748,13 +778,13 @@ o:depends("type", "shadowtls")
if is_finded("sslocal") then
o:value("sslocal", translate("ShadowSocks-rust Version"))
end
if is_finded("xray") or is_finded("v2ray") then
if is_finded("xray") then
o:value("vmess", translate("Vmess Protocol"))
end
o.default = "sslocal"
o.rmempty = false
o = s:option(Value, "sslocal_password",translate("Shadowsocks password"))
o = s:option(Value, "sslocal_password",translate("Shadowsocks Password"))
o:depends({type = "shadowtls", chain_type = "sslocal"})
o.rmempty = true
@@ -788,7 +818,7 @@ o:depends("type", "tuic")
--Tuic IP
o = s:option(Value, "tuic_ip", translate("TUIC Server IP Address"))
o.rmempty = true
o.datatype = "ip4addr"
o.datatype = "ipaddr"
o.default = ""
o:depends("type", "tuic")
@@ -969,7 +999,7 @@ o = s:option(Value, "ws_heartbeatPeriod", translate("HeartbeatPeriod(second)"))
o.datatype = "integer"
o:depends("transport", "ws")
if is_finded("v2ray") then
if is_finded("xray") then
-- WS前置数据
o = s:option(Value, "ws_ed", translate("Max Early Data"))
o:depends("ws_ed_enable", true)
@@ -1210,12 +1240,12 @@ o.default = "0"
o.rmempty = true
o = s:option(DynamicList, "local_addresses", translate("Local addresses"))
o.datatype = "cidr"
--o.datatype = "cidr"
o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
o.rmempty = true
o = s:option(DynamicList, "reserved", translate("Reserved bytes(optional)"))
o.description = translate("Decimal numbers separated by \",\" or Base64-encoded strings.")
o.description = translate("Supports decimal numbers separated by \",\" or Base64-encoded strings, with a maximum length of 3 bytes.")
o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
o.rmempty = true
@@ -1260,12 +1290,18 @@ o:depends("transport", "grpc")
o = s:option(Flag, "enable_finalmask", translate("FinalMask"))
o.rmempty = true
o.default = "0"
o:depends({type = "v2ray", v2ray_protocol = "vless"})
o:depends({type = "v2ray", v2ray_protocol = "vmess"})
o:depends({type = "v2ray", v2ray_protocol = "trojan"})
o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"})
o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
local protocols = s.fields["v2ray_protocol"].keylist
if protocols and type(protocols) == "table" and #protocols > 0 then
for _, proto in ipairs(protocols) do
if not proto:find("^_") then
if proto == "hysteria2" then
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false})
elseif proto ~= "socks" and proto ~= "http" then
o:depends({type = "v2ray", v2ray_protocol = proto})
end
end
end
end
o = s:option(TextValue, "finalmask", " ")
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
@@ -1362,7 +1398,18 @@ if is_finded("xray") then
o = s:option(Flag, "enable_ech", translate("Enable ECH(optional)"))
o.rmempty = true
o.default = "0"
o:depends({type = "v2ray", tls = true})
local protocols = s.fields["v2ray_protocol"].keylist
if protocols and type(protocols) == "table" and #protocols > 0 then
for _, proto in ipairs(protocols) do
if not proto:find("^_") then
if proto == "hysteria2" then
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", tls = true, hysteria2_realms = false})
else
o:depends({type = "v2ray", v2ray_protocol = proto, tls = true})
end
end
end
end
o = s:option(TextValue, "ech_config", translate("ECH Config"))
o.description = translate(
@@ -1643,100 +1690,41 @@ o.rmempty = true
o.default = "0"
o:depends("type", "ssr")
o:depends("type", "ss")
o:depends("type", "ss-rust")
o:depends("type", "trojan")
o:depends("type", "hysteria2")
o:depends({type = "v2ray", v2ray_protocol = "vless", transport = "xhttp"})
o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"))
o.description = translate(
"<ul>" ..
"<li>" .. translate("If the node address is a domain name, this DNS will be used for resolution.") .. "</li>" ..
"<li>" .. string.format('<font style=\'color:red;\'>%s</font>', translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead.")) .. "</li>" ..
"</ul>"
)
o:value("", translate("Auto"))
o:value("tcp", translate("TCP"))
o:value("udp", translate("UDP"))
o:value("https", translate("DoH"))
o:depends("type", "v2ray")
o = s:option(Value, "domain_resolver_dns", translate("DNS"))
o.datatype = "or(ipaddr,ipaddrport)"
o:value("114.114.114.114")
o:value("223.5.5.5:53")
o.default = "114.114.114.114"
o:depends("domain_resolver", "tcp")
o:depends("domain_resolver", "udp")
o = s:option(Value, "domain_resolver_dns_https", translate("DNS"))
o:value("https://120.53.53.53/dns-query", translate("DNSPod"))
o:value("https://223.5.5.5/dns-query", translate("AliDNS"))
o.default = "https://120.53.53.53/dns-query"
o:depends("domain_resolver", "https")
o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"))
o.description = translate(
"<ul>" ..
"<li>" .. translate("If is domain name, The requested domain name will be resolved to IP before connect.") .. "</li>" ..
"<li>" .. string.format('<font style=\'color:red;\'>%s</font>', translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead.")) .. "</li>" ..
"</ul>"
)
o.default = ""
o:value("", translate("Auto"))
o:value("UseIPv4v6", translate("Prefer IPv4"))
o:value("UseIPv6v4", translate("Prefer IPv6"))
o:value("UseIPv4", translate("IPv4 Only"))
o:value("UseIPv6", translate("IPv6 Only"))
o:depends("type", "v2ray")
local v2ray_protocols = s.fields["v2ray_protocol"]
if #v2ray_protocols > 0 then
for i, v in ipairs(v2ray_protocols) do
if not v:find("^_") then
s.fields["server"]:depends({ ["v2ray_protocol"] = v })
s.fields["server_port"]:depends({ ["v2ray_protocol"] = v })
s.fields["domain_resolver"]:depends({ ["v2ray_protocol"] = v })
s.fields["domain_strategy"]:depends({ ["v2ray_protocol"] = v })
if v ~= "hysteria2" then
s.fields["fast_open"]:depends({ ["v2ray_protocol"] = v })
s.fields["mptcp"]:depends({ ["v2ray_protocol"] = v })
end
end
end
end
o = s:option(Flag, "switch_enable", translate("Enable Auto Switch"))
o.rmempty = false
o.default = "1"
o = s:option(Value, "local_port", translate("Local Port"))
o.datatype = "port"
o.default = 1234
o.rmempty = false
if is_finded("kcptun-client") then
o = s:option(Flag, "kcp_enable", translate("KcpTun Enable"))
o.rmempty = true
o.default = "0"
o:depends("type", "ssr")
o:depends("type", "ss-rust")
o:depends("type", "ss")
o = s:option(Value, "kcp_port", translate("KcpTun Port"))
o.datatype = "portrange"
o.default = 4000
o:depends("type", "ssr")
o:depends("type", "ss-rust")
o:depends("type", "ss")
o = s:option(Value, "kcp_password", translate("KcpTun Password"))
o.password = true
o:depends("type", "ssr")
o:depends("type", "ss-rust")
o:depends("type", "ss")
o = s:option(Value, "kcp_param", translate("KcpTun Param"))
o.default = "--nocomp"
o:depends("type", "ssr")
o:depends("type", "ss-rust")
o:depends("type", "ss")
end
@@ -4,6 +4,7 @@
local m, s, sec, o
local uci = require "luci.model.uci".cursor()
local URL = require "url"
-- 获取 LAN IP 地址
function lanip()
@@ -31,69 +32,62 @@ end
local lan_ip = lanip()
local validation = require "luci.cbi.datatypes"
local clash_nodes = {}
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
end
m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/TUN etc.</h3>"))
local function clash_display_name(s)
if s.type ~= "clash" or not s.clash_url or s.clash_url == "" then
return nil
end
local ok, parsed = pcall(URL.parse, s.clash_url)
if ok and parsed and parsed.host then
return "[CLASH]:" .. parsed.host
end
return "[CLASH]"
end
m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/CLASH etc.</h3>"))
m:section(SimpleSection).template = "shadowsocksr/status"
local server_table = {}
local server_order = {}
uci:foreach("shadowsocksr", "servers", function(s)
if s.alias then
if s.type == "clash" then
clash_nodes[s[".name"]] = true
end
if s.type ~= "tun" and s.alias then
server_table[s[".name"]] = "[%s]:%s" % {string.upper(s.v2ray_protocol or s.type), s.alias}
elseif s.server and s.server_port then
elseif s.type ~= "tun" and s.server and s.server_port then
server_table[s[".name"]] = "[%s]:%s:%s" % {string.upper(s.v2ray_protocol or s.type), s.server, s.server_port}
elseif s.type ~= "tun" then
local display_name = clash_display_name(s)
if display_name then
server_table[s[".name"]] = display_name
end
end
if s.type ~= "tun" and server_table[s[".name"]] then
table.insert(server_order, s[".name"])
end
end)
local key_table = {}
for key, _ in pairs(server_table) do
table.insert(key_table, key)
end
table.sort(key_table)
-- [[ Global Setting ]]--
s = m:section(TypedSection, "global")
s.anonymous = true
o = s:option(ListValue, "global_server", translate("Main Server"))
o:value("nil", translate("Disable"))
for _, key in pairs(key_table) do
for _, key in ipairs(server_order) do
o:value(key, server_table[key])
end
o.default = "nil"
o.rmempty = false
o = s:option(ListValue, "udp_relay_server", translate("Game Mode UDP Server"))
o:value("", translate("Disable"))
o:value("same", translate("Same as Global Server"))
for _, key in pairs(key_table) do
o:value(key, server_table[key])
end
if uci:get_first("shadowsocksr", 'global', 'netflix_enable', '0') == '1' then
o = s:option(ListValue, "netflix_server", translate("Netflix Node"))
o:value("nil", translate("Disable"))
o:value("same", translate("Same as Global Server"))
for _, key in pairs(key_table) do
o:value(key, server_table[key])
end
o.default = "nil"
o.rmempty = false
o = s:option(Flag, "netflix_proxy", translate("External Proxy Mode"))
o.rmempty = false
o.description = translate("Forward Netflix Proxy through Main Proxy")
o.default = "0"
end
-- [[ Use nftables/iptables ]]--
o = s:option(ListValue, "prefer_nft", translate("Prefer firewall tools"))
o.default = "1"
o:value("0", "Iptables")
o:value("1", "Nftables")
o = s:option(DummyValue, "_clash_panel", translate("Clash Panel"))
o.template = "shadowsocksr/clash_main_panel"
o.clash_nodes = clash_nodes
o = s:option(ListValue, "threads", translate("Multi Threads Option"))
o:value("0", translate("Auto Threads"))
@@ -112,7 +106,6 @@ o = s:option(ListValue, "run_mode", translate("Running Mode"))
o:value("gfw", translate("GFW List Mode"))
o:value("router", translate("IP Route Mode"))
o:value("all", translate("Global Mode"))
o:value("oversea", translate("Oversea Mode"))
o.default = gfw
o = s:option(ListValue, "dports", translate("Proxy Ports"))
@@ -128,21 +121,13 @@ o = s:option(ListValue, "pdnsd_enable", translate("Resolve Dns Mode"))
if is_finded("dns2tcp") then
o:value("1", translate("Use DNS2TCP query"))
end
if is_finded("dns2socks") then
o:value("2", translate("Use DNS2SOCKS query and cache"))
end
if is_finded("dns2socks-rust") then
o:value("3", translate("Use DNS2SOCKS-RUST query and cache"))
end
if is_finded("mosdns") then
o:value("4", translate("Use MOSDNS query (Not Support Oversea Mode)"))
end
if is_finded("dnsproxy") then
o:value("5", translate("Use DNSPROXY query and cache"))
o:value("4", translate("Use MosDNS query"))
end
if is_finded("chinadns-ng") then
o:value("6", translate("Use ChinaDNS-NG query and cache"))
end
o:value("7", translate("Prefer module built-in DNS"))
o:value("0", translate("Use Local DNS Service listen port 5335"))
o.default = 1
@@ -158,11 +143,8 @@ o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)"))
o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)"))
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)"))
o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
o:value("114.114.114.114:53", translate("Oversea Mode DNS-1 (114.114.114.114)"))
o:value("114.114.115.115:53", translate("Oversea Mode DNS-2 (114.114.115.115)"))
o:depends("pdnsd_enable", "1")
o:depends("pdnsd_enable", "2")
o:depends("pdnsd_enable", "3")
o:depends("pdnsd_enable", "7")
o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)")
o.datatype = "ip4addrport"
o.default = "8.8.4.4:53"
@@ -176,61 +158,15 @@ o:value("tcp://4.2.2.3:53,tcp://4.2.2.4:53", translate("Level 3 Public DNS-3 (4.
o:value("tcp://1.1.1.1:53,tcp://1.0.0.1:53", translate("Cloudflare DNS"))
o:depends("pdnsd_enable", "4")
o.description = translate("Custom DNS Server format as tcp://IP:PORT or tls://DOMAIN:PORT (tcp://8.8.8.8 or tls://dns.google:853)")
o.default = "tcp://8.8.4.4:53,tcp://8.8.8.8:53"
o = s:option(Flag, "mosdns_ipv6", translate("Disable IPv6 in MOSDNS query mode"))
o = s:option(Flag, "filter_aaaa", translate("Disable IPv6 for Overseas FQDN"))
o:depends("pdnsd_enable", "1")
o:depends("pdnsd_enable", "4")
o:depends("pdnsd_enable", "7")
o.rmempty = false
o.default = "1"
if is_finded("dnsproxy") then
o = s:option(ListValue, "parse_method", translate("Select DNS parse Mode"))
o.description = translate(
"<ul>" ..
"<li>" .. translate("When use DNS list file, please ensure list file exists and is formatted correctly.") .. "</li>" ..
"<li>" .. translate("Tips: Dnsproxy DNS Parse List Path:") ..
" <a href='http://" .. lan_ip .. "/cgi-bin/luci/admin/services/shadowsocksr/control' target='_blank'>" ..
translate("Click here to view or manage the DNS list file") .. "</a>" .. "</li>" ..
"</ul>"
)
o:value("single_dns", translate("Set Single DNS"))
o:value("parse_file", translate("Use DNS List File"))
o:depends("pdnsd_enable", "5")
o.rmempty = true
o.default = "single_dns"
o = s:option(Value, "dnsproxy_tunnel_forward", translate("Anti-pollution DNS Server"))
o:value("sdns://AgUAAAAAAAAABzguOC40LjQgsKKKE4EwvtIbNjGjagI2607EdKSVHowYZtyvD9iPrkkHOC44LjQuNAovZG5zLXF1ZXJ5", translate("Google DNSCrypt SDNS"))
o:value("sdns://AgcAAAAAAAAAACC2vD25TAYM7EnyCH8Xw1-0g5OccnTsGH9vQUUH0njRtAxkbnMudHduaWMudHcKL2Rucy1xdWVyeQ", translate("TWNIC-101 DNSCrypt SDNS"))
o:value("sdns://AgcAAAAAAAAADzE4NS4yMjIuMjIyLjIyMiAOp5Svj-oV-Fz-65-8H2VKHLKJ0egmfEgrdPeAQlUFFA8xODUuMjIyLjIyMi4yMjIKL2Rucy1xdWVyeQ", translate("dns.sb DNSCrypt SDNS"))
o:value("sdns://AgMAAAAAAAAADTE0OS4xMTIuMTEyLjkgsBkgdEu7dsmrBT4B4Ht-BQ5HPSD3n3vqQ1-v5DydJC8SZG5zOS5xdWFkOS5uZXQ6NDQzCi9kbnMtcXVlcnk", translate("Quad9 DNSCrypt SDNS"))
o:value("sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20", translate("AdGuard DNSCrypt SDNS"))
o:value("sdns://AgcAAAAAAAAABzEuMC4wLjGgENk8mGSlIfMGXMOlIlCcKvq7AVgcrZxtjon911-ep0cg63Ul-I8NlFj4GplQGb_TTLiczclX57DvMV8Q-JdjgRgSZG5zLmNsb3VkZmxhcmUuY29tCi9kbnMtcXVlcnk", translate("Cloudflare DNSCrypt SDNS"))
o:value("sdns://AgcAAAAAAAAADjEwNC4xNi4yNDkuMjQ5ABJjbG91ZGZsYXJlLWRucy5jb20KL2Rucy1xdWVyeQ", translate("cloudflare-dns.com DNSCrypt SDNS"))
o:depends("parse_method", "single_dns")
o.description = translate("Custom DNS Server (support: IP:Port or tls://IP:Port or https://IP/dns-query and other format).")
o = s:option(ListValue, "upstreams_logic_mode", translate("Defines the upstreams logic mode"))
o.description = translate(
"<ul>" ..
"<li>" .. translate("Defines the upstreams logic mode, possible values: load_balance, parallel, fastest_addr (default: load_balance).") .. "</li>" ..
"<li>" .. translate("When two or more DNS servers are deployed, enable this function.") .. "</li>" ..
"</ul>"
)
o:value("load_balance", translate("load_balance"))
o:value("parallel", translate("parallel"))
o:value("fastest_addr", translate("fastest_addr"))
o:depends("parse_method", "parse_file")
o.rmempty = true
o.default = "load_balance"
o = s:option(Flag, "dnsproxy_ipv6", translate("Disable IPv6 query mode"))
o.description = translate("When disabled, all AAAA requests are not resolved.")
o:depends("parse_method", "single_dns")
o:depends("parse_method", "parse_file")
o.rmempty = false
o.default = "1"
end
if is_finded("chinadns-ng") then
o = s:option(Value, "chinadns_ng_tunnel_forward", translate("Anti-pollution DNS Server"))
o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)"))
@@ -245,6 +181,7 @@ if is_finded("chinadns-ng") then
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)"))
o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
o:depends("pdnsd_enable", "6")
o.default = "8.8.4.4:53"
o.description = translate(
"<ul>" ..
"<li>" .. translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") .. "</li>" ..
@@ -270,11 +207,8 @@ if is_finded("chinadns-ng") then
o:value("101.226.4.6:53", translate("360 Security DNS (China Telecom) (101.226.4.6)"))
o:value("123.125.81.6:53", translate("360 Security DNS (China Unicom) (123.125.81.6)"))
o:value("1.2.4.8:53", translate("CNNIC SDNS (1.2.4.8)"))
o:depends({pdnsd_enable = "1", run_mode = "router"})
o:depends({pdnsd_enable = "2", run_mode = "router"})
o:depends({pdnsd_enable = "3", run_mode = "router"})
o:depends({pdnsd_enable = "5", run_mode = "router"})
o:depends({pdnsd_enable = "6", run_mode = "router"})
o.default = "wan"
o.description = translate("Custom DNS Server format as IP:PORT (default: disabled)")
o.validate = function(self, value, section)
if (section and value) then
@@ -293,5 +227,7 @@ if is_finded("chinadns-ng") then
end
end
m:section(SimpleSection).template = "shadowsocksr/status_bottom"
m:append(Template("shadowsocksr/client_dns_defaults"))
m:append(Template("shadowsocksr/status_bottom"))
return m
@@ -0,0 +1,30 @@
local m, s, o
m = SimpleForm(
"component_update",
translate("Component Update"),
translate("Check installed component versions and upgrade them online from the upstream release page.")
)
m.reset = false
m.submit = false
s = m:section(SimpleSection)
o = m:field(ListValue, "component_mirror", translate("Mirror URL"))
o:value("direct", translate("GitHub Direct"))
o:value("ghproxy", "mirror.ghproxy.com")
o:value("ghproxy_cc", "ghproxy.cc")
o:value("ghfast", "ghfast.top")
o:value("jsdelivr", "cdn.jsdelivr.net")
o.rmempty = false
o.cfgvalue = function(self)
return m.uci:get_first("shadowsocksr", "global", "component_mirror") or "direct"
end
o.write = function(self, section, value)
m.uci:set("shadowsocksr", "@global[0]", "component_mirror", value)
m.uci:commit("shadowsocksr")
end
s.template = "shadowsocksr/component"
return m
@@ -3,10 +3,6 @@ require "nixio.fs"
require "luci.sys"
local m, s, o
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
end
m = Map("shadowsocksr")
s = m:section(TypedSection, "access_control")
@@ -129,40 +125,6 @@ o.remove = function(self, section, value)
nixio.fs.writefile(denydomainconf, "")
end
s:tab("netflix", translate("Netflix Domain List"))
local netflixconf = "/etc/ssrplus/netflix.list"
o = s:taboption("netflix", TextValue, "netflixconf")
o.rows = 13
o.wrap = "off"
o.rmempty = true
o.cfgvalue = function(self, section)
return nixio.fs.readfile(netflixconf) or " "
end
o.write = function(self, section, value)
nixio.fs.writefile(netflixconf, value:gsub("\r\n", "\n"))
end
o.remove = function(self, section, value)
nixio.fs.writefile(netflixconf, "")
end
if is_finded("dnsproxy") then
s:tab("dnsproxy", translate("Dnsproxy Parse List"))
local dnsproxyconf = "/etc/ssrplus/dnsproxy_dns.list"
o = s:taboption("dnsproxy", TextValue, "dnsproxyconf", "", "<font style=color:red>" .. translate("Specifically for edit dnsproxy DNS parse files.") .. "</font>")
o.rows = 13
o.wrap = "off"
o.rmempty = true
o.cfgvalue = function(self, section)
return nixio.fs.readfile(dnsproxyconf) or " "
end
o.write = function(self, section, value)
nixio.fs.writefile(dnsproxyconf, value:gsub("\r\n", "\n"))
end
o.remove = function(self, section, value)
nixio.fs.writefile(dnsproxyconf, "")
end
end
if luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 then
m.apply_on_parse = true
function m.on_apply(self)
@@ -170,4 +132,6 @@ if luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 then
end
end
m:append(Template("shadowsocksr/control_layout"))
return m
@@ -50,6 +50,7 @@ luci.http.setfilehandler(function(meta, chunk, eof)
luci.sys.call("rm -rf " .. temp_dir)
nixio.fs.remove(file_path)
fd = nixio.open(file_path, "w")
luci.sys.call("ls /tmp/sub_md5_* >/dev/null 2>&1 && rm -f /tmp/sub_md5_*")
luci.sys.call("echo '' > /var/log/ssrplus.log")
end
@@ -96,6 +97,7 @@ luci.http.setfilehandler(function(meta, chunk, eof)
-- 清理临时文件
luci.sys.call("rm -rf " .. temp_dir)
nixio.fs.remove(file_path)
luci.sys.call("ls /tmp/sub_md5_* >/dev/null 2>&1 && rm -f /tmp/sub_md5_*")
end
end)
@@ -7,68 +7,39 @@ require "nixio.fs"
local m, s, o
local sid = arg[1]
-- 加密方式(SS/SSR用)
local encrypt_methods = {
"rc4-md5",
"rc4-md5-6",
"rc4",
"table",
"aes-128-cfb",
"aes-192-cfb",
"aes-256-cfb",
"aes-128-ctr",
"aes-192-ctr",
"aes-256-ctr",
"bf-cfb",
"camellia-128-cfb",
"camellia-192-cfb",
"camellia-256-cfb",
"cast5-cfb",
"des-cfb",
"idea-cfb",
"rc2-cfb",
"seed-cfb",
"salsa20",
"chacha20",
"chacha20-ietf"
"rc4-md5", "rc4-md5-6", "rc4", "table",
"aes-128-cfb", "aes-192-cfb", "aes-256-cfb",
"aes-128-ctr", "aes-192-ctr", "aes-256-ctr",
"bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb",
"cast5-cfb", "des-cfb", "idea-cfb", "rc2-cfb", "seed-cfb",
"salsa20", "chacha20", "chacha20-ietf"
}
local encrypt_methods_ss = {
-- aead
"aes-128-gcm",
"aes-192-gcm",
"aes-256-gcm",
"chacha20-ietf-poly1305",
"xchacha20-ietf-poly1305",
-- aead 2022
"2022-blake3-aes-128-gcm",
"2022-blake3-aes-256-gcm",
"2022-blake3-chacha20-poly1305"
--[[ stream
"table",
"rc4",
"rc4-md5",
"aes-128-cfb",
"aes-192-cfb",
"aes-256-cfb",
"aes-128-ctr",
"aes-192-ctr",
"aes-256-ctr",
"bf-cfb",
"camellia-128-cfb",
"camellia-192-cfb",
"camellia-256-cfb",
"salsa20",
"chacha20",
"chacha20-ietf" ]]
"aes-128-gcm", "aes-192-gcm", "aes-256-gcm",
"chacha20-ietf-poly1305", "xchacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
-- Shadowsocks 加密方法(Xray 版)
local v_ss_encrypt_method_list = {
"aes-128-cfb", "aes-256-cfb", "aes-128-gcm", "aes-256-gcm",
"chacha20", "chacha20-ietf", "chacha20-poly1305", "chacha20-ietf-poly1305"
}
-- 伪装类型(用于 mKCP/QUIC)
local header_type_list = {
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard"
}
local protocol = {"origin"}
obfs = {"plain", "http_simple", "http_post"}
m = Map("shadowsocksr", translate("Edit ShadowSocksR Server"))
m.redirect = luci.dispatcher.build_url("admin/services/shadowsocksr/server")
if m.uci:get("shadowsocksr", sid) ~= "server_config" then
luci.http.redirect(m.redirect)
return
@@ -79,18 +50,26 @@ s = m:section(NamedSection, sid, "server_config")
s.anonymous = true
s.addremove = false
-- ========== 基本设置 ==========
o = s:option(Flag, "enable", translate("Enable"))
o.default = 1
o.rmempty = false
o = s:option(ListValue, "type", translate("Server Type"))
o:value("socks5", translate("Socks5"))
if nixio.fs.access("/usr/bin/ssserver") or nixio.fs.access("/usr/bin/ss-server") then
if nixio.fs.access("/usr/bin/mihomo") or nixio.fs.access("/usr/libexec/mihomo") or nixio.fs.access("/usr/bin/ssserver") or nixio.fs.access("/usr/libexec/ssserver") then
o:value("ss", translate("ShadowSocks"))
end
if nixio.fs.access("/usr/bin/ssr-server") then
o:value("ssr", translate("ShadowsocksR"))
end
-- Xray 协议支持
if nixio.fs.access("/usr/bin/xray") or nixio.fs.access("/usr/libexec/xray") then
o:value("vmess", "VMess (Xray)")
o:value("vless", "VLESS (Xray)")
o:value("trojan", "Trojan (Xray)")
o:value("shadowsocks", "Shadowsocks (Xray)")
end
o.default = "socks5"
o = s:option(Value, "server_port", translate("Server Port"))
@@ -114,32 +93,30 @@ o:depends("type", "socks5")
o = s:option(Value, "password", translate("Password"))
o.password = true
o.rmempty = false
o:depends("type", "socks5")
o:depends("type", "ss")
o:depends("type", "ssr")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
-- ========== SS/SSR 协议特有 ==========
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:depends("type", "ssr")
o = s:option(ListValue, "encrypt_method_ss", translate("Encrypt Method"))
for _, v in ipairs(encrypt_methods_ss) do
o:value(v)
end
for _, v in ipairs(encrypt_methods_ss) do o:value(v) end
o.rmempty = false
o:depends("type", "ss")
o = s:option(ListValue, "protocol", translate("Protocol"))
for _, v in ipairs(protocol) do
o:value(v)
end
for _, v in ipairs(protocol) do o:value(v) end
o.rmempty = false
o:depends("type", "ssr")
o = s:option(ListValue, "obfs", translate("Obfs"))
for _, v in ipairs(obfs) do
o:value(v)
end
for _, v in ipairs(obfs) do o:value(v) end
o.rmempty = false
o:depends("type", "ssr")
@@ -151,4 +128,269 @@ o.rmempty = false
o:depends("type", "ss")
o:depends("type", "ssr")
return m
-- ========== Xray 协议通用字段 ==========
-- 用户备注
o = s:option(Value, "remarks", translate("Remarks"))
o.default = translate("Remarks")
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
-- 用户等级
o = s:option(Value, "level", translate("User Level"))
o.datatype = "uinteger"
o.default = 1
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "shadowsocks")
o:depends("type", "trojan")
-- ========== Xray 协议认证字段 ==========
-- UUID (用于 vmess/vless)
o = s:option(Value, "uuid", translate("UUID"))
o.description = translate("Required for VMess/VLESS. Generate with: uuidgen")
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
-- Trojan 密码
o = s:option(Value, "trojan_password", translate("Trojan Password"))
o.password = true
o.rmempty = true
o:depends("type", "trojan")
-- Shadowsocks 密码和加密(Xray 内置)
o = s:option(Value, "ss_password", translate("Shadowsocks Password"))
o.password = true
o.rmempty = true
o:depends("type", "shadowsocks")
o = s:option(ListValue, "ss_method", translate("Encrypt Method"))
for _, v in ipairs(v_ss_encrypt_method_list) do o:value(v) end
o.default = "chacha20-ietf-poly1305"
o.rmempty = true
o:depends("type", "shadowsocks")
o = s:option(ListValue, "ss_network", translate("Transport"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("tcp,udp", "TCP,UDP")
o.default = "tcp,udp"
o.rmempty = true
o:depends("type", "shadowsocks")
-- VMess alterId
o = s:option(Value, "alter_id", translate("Alter ID"))
o.datatype = "uinteger"
o.default = 0
o.rmempty = true
o:depends("type", "vmess")
-- VLESS decryption
o = s:option(Value, "decryption", translate("Decryption"))
o.default = "none"
o.rmempty = true
o:depends("type", "vless")
-- ========== TLS / XTLS 设置 ==========
o = s:option(Flag, "tls", translate("TLS"))
o.default = 0
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
o = s:option(Flag, "xtls", translate("XTLS"))
o.default = 0
o.rmempty = true
o:depends({ type = "vless", tls = "1" })
o = s:option(ListValue, "flow", translate("Flow"))
o:value("xtls-rprx-origin", "xtls-rprx-origin")
o:value("xtls-rprx-origin-udp443", "xtls-rprx-origin-udp443")
o:value("xtls-rprx-direct", "xtls-rprx-direct")
o:value("xtls-rprx-direct-udp443", "xtls-rprx-direct-udp443")
o:value("xtls-rprx-splice", "xtls-rprx-splice")
o:value("xtls-rprx-splice-udp443", "xtls-rprx-splice-udp443")
o.default = "xtls-rprx-direct"
o.rmempty = true
o:depends("xtls", "1")
o = s:option(Value, "tls_serverName", translate("Server Name (SNI)"))
o.rmempty = true
o:depends("tls", "1")
o = s:option(Value, "tls_certificateFile", translate("Certificate File Path"))
o.description = translate("e.g.: /etc/ssl/fullchain.pem")
o.rmempty = true
o:depends("tls", "1")
o = s:option(Value, "tls_keyFile", translate("Private Key File Path"))
o.description = translate("e.g.: /etc/ssl/private.key")
o.rmempty = true
o:depends("tls", "1")
-- ========== 传输层设置 (Transport) ==========
o = s:option(ListValue, "transport", translate("Transport Protocol"))
o:value("tcp", "TCP")
o:value("mkcp", "mKCP")
o:value("ws", "WebSocket")
o:value("h2", "HTTP/2")
o:value("ds", "DomainSocket")
o:value("quic", "QUIC")
o.default = "tcp"
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
-- ----- WebSocket 设置 -----
o = s:option(Value, "ws_host", translate("WebSocket Host"))
o.rmempty = true
o:depends("transport", "ws")
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
o = s:option(Value, "ws_path", translate("WebSocket Path"))
o.default = "/"
o.rmempty = true
o:depends("transport", "ws")
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
-- ----- HTTP/2 设置 -----
o = s:option(Value, "h2_host", translate("HTTP/2 Host"))
o.rmempty = true
o:depends("transport", "h2")
o:depends("type", "vmess")
o:depends("type", "vless")
o = s:option(Value, "h2_path", translate("HTTP/2 Path"))
o.default = "/"
o.rmempty = true
o:depends("transport", "h2")
o:depends("type", "vmess")
o:depends("type", "vless")
-- ----- TCP 伪装设置 -----
o = s:option(ListValue, "tcp_guise", translate("TCP Camouflage Type"))
o:value("none", "none")
o:value("http", "http")
o.default = "none"
o.rmempty = true
o:depends("transport", "tcp")
o:depends("type", "vmess")
o:depends("type", "vless")
o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host"))
o.rmempty = true
o:depends("tcp_guise", "http")
o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
o.rmempty = true
o:depends("tcp_guise", "http")
-- ----- mKCP 设置 -----
o = s:option(ListValue, "mkcp_guise", translate("mKCP Camouflage Type"))
for _, v in ipairs(header_type_list) do o:value(v) end
o.default = "none"
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_mtu", translate("KCP MTU"))
o.datatype = "uinteger"
o.default = 1350
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_tti", translate("KCP TTI"))
o.datatype = "uinteger"
o.default = 20
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_uplinkCapacity", translate("KCP Uplink Capacity"))
o.datatype = "uinteger"
o.default = 5
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_downlinkCapacity", translate("KCP Downlink Capacity"))
o.datatype = "uinteger"
o.default = 20
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Flag, "mkcp_congestion", translate("KCP Congestion Control"))
o.default = 0
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_readBufferSize", translate("KCP Read Buffer Size"))
o.datatype = "uinteger"
o.default = 1
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_writeBufferSize", translate("KCP Write Buffer Size"))
o.datatype = "uinteger"
o.default = 1
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_seed", translate("KCP Seed"))
o.datatype = "uinteger"
o.rmempty = true
o:depends("transport", "mkcp")
-- ----- DomainSocket 设置 -----
o = s:option(Value, "ds_path", translate("DomainSocket Path"))
o.description = translate("A legal file path. This file must not exist before running.")
o.rmempty = true
o:depends("transport", "ds")
-- ----- QUIC 设置 -----
o = s:option(ListValue, "quic_security", translate("QUIC Security"))
o:value("none", "none")
o:value("aes-128-gcm", "aes-128-gcm")
o:value("chacha20-poly1305", "chacha20-poly1305")
o.default = "none"
o.rmempty = true
o:depends("transport", "quic")
o = s:option(Value, "quic_key", translate("QUIC Key"))
o.rmempty = true
o:depends("transport", "quic")
o = s:option(ListValue, "quic_guise", translate("QUIC Camouflage Type"))
for _, v in ipairs(header_type_list) do o:value(v) end
o.default = "none"
o.rmempty = true
o:depends("transport", "quic")
-- ========== 访问控制 ==========
o = s:option(Flag, "bind_local", translate("Bind Local Only"))
o.description = translate("When selected, it can only be accessed locally. Recommended when using reverse proxies.")
o.default = 0
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
o = s:option(Flag, "accept_lan", translate("Accept LAN Access"))
o.description = translate("When selected, it can be accessed from LAN. This may not be safe!")
o.default = 0
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
return m
@@ -105,8 +105,13 @@ end
o.rmempty = false
o = sec:option(DummyValue, "type", translate("Server Type"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or "ss"
function o.cfgvalue(self, section)
local val = Value.cfgvalue(self, section) or "ss"
if val == "vmess" then return "VMess (Xray)"
elseif val == "vless" then return "VLESS (Xray)"
elseif val == "trojan" then return "Trojan (Xray)"
elseif val == "shadowsocks" then return "Shadowsocks (Xray)"
else return val end
end
o = sec:option(DummyValue, "server_port", translate("Server Port"))
@@ -135,4 +140,4 @@ function o.cfgvalue(...)
return Value.cfgvalue(...) or "-"
end
return m
return m
@@ -1,36 +1,35 @@
-- Licensed to the public under the GNU General Public License v3.
require "luci.http"
require "luci.sys"
require "luci.util"
require "nixio.fs"
require "luci.dispatcher"
require "luci.model.uci"
local nixio = require "nixio"
local json = require "luci.jsonc"
local cbi = require "luci.cbi"
local uci = require "luci.model.uci".cursor()
local URL = require "url"
local m, s, o, node
local server_count = 0
local server_cache = {}
local server_sections = {}
local detect_cache = {}
local CLASH_YAML_DIR = "/etc/ssrplus/clash"
local CLASH_YAML_HELPER = "/usr/share/shadowsocksr/clash_yaml.lua"
local DEFAULT_SERVER_PAGE_SIZE = 50
local SERVER_PAGE_SIZE_OPTIONS = {25, 50, 100, 200, 0}
-- 确保正确判断程序是否存在
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
end
local function is_js_luci()
return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
end
local function url(...)
local url = string.format("admin/services/%s", "shadowsocksr")
local args = { ... }
for i, v in ipairs(args) do
if v and v ~= "" then
url = url .. "/" .. v
end
end
return require "luci.dispatcher".build_url(url)
end
-- 默认的保存并应用行为
-- 保存并应用行为
local function apply_redirect(m)
local tmp_uci_file = "/etc/config/" .. "shadowsocksr" .. "_redirect"
if m.redirect and m.redirect ~= "" then
@@ -65,50 +64,341 @@ local function set_apply_on_parse(map)
local old = map.on_after_save
map.on_after_save = function(self)
if old then old(self) end
map:set("@global[0]", "timestamp", os.time())
-- map:set("@global[0]", "timestamp", os.time())
end
end
end
local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local")
local has_trojan = is_finded("trojan")
local has_xray = is_finded("xray")
local has_hysteria2 = is_finded("hysteria")
local ss_type_list = {}
local tj_type_list = {}
local hy2_type_list = {}
if has_hysteria2 then
table.insert(hy2_type_list, { id = "hysteria2", name = translate("Hysteria2") })
end
if has_xray then
table.insert(hy2_type_list, { id = "v2ray", name = translate("Xray (Hysteria2)") })
local function trim(text)
if not text or text == "" then
return ""
end
return (text:gsub("^%s*(.-)%s*$", "%1"))
end
if has_trojan then
table.insert(tj_type_list, { id = "trojan", name = translate("Trojan") })
end
if has_xray then
table.insert(tj_type_list, { id = "v2ray", name = translate("Xray (Trojan)") })
local function parse_nonnegative_int(value)
local number = tonumber(value)
if not number then
return nil
end
number = math.floor(number)
if number < 0 then
return nil
end
return number
end
if has_ss_rust then
table.insert(ss_type_list, { id = "ss-rust", name = translate("ShadowSocks-rust Version") })
local function in_list(list, target)
for _, item in ipairs(list) do
if item == target then
return true
end
end
return false
end
if has_ss_libev then
table.insert(ss_type_list, { id = "ss-libev", name = translate("ShadowSocks-libev Version") })
local function upload_alias(filename)
local stem = tostring(filename or ""):gsub("\\", "/"):match("([^/]+)$") or "custom"
stem = stem:gsub("%.%w+$", "")
stem = trim(stem):gsub("[%c\r\n]+", " "):gsub("%s+", " ")
if stem == "" then
stem = "custom"
end
return "Clash_" .. stem
end
if has_xray then
table.insert(ss_type_list, { id = "v2ray", name = translate("Xray (ShadowSocks)") })
local function hash_file(path)
local cmd = "md5sum " .. luci.util.shellquote(path) .. " 2>/dev/null | awk '{print $1}'"
return trim(luci.sys.exec(cmd))
end
local function preprocess_clash_yaml(input_path, output_path)
local cmd = string.format(
"/usr/bin/lua %s prepare %s %s >/dev/null 2>&1",
luci.util.shellquote(CLASH_YAML_HELPER),
luci.util.shellquote(input_path),
luci.util.shellquote(output_path)
)
return luci.sys.call(cmd) == 0
end
local function clash_path_in_use(path, exclude_sid)
local in_use = false
uci:foreach("shadowsocksr", "servers", function(section)
if section[".name"] ~= exclude_sid and section.clash_path == path then
in_use = true
return false
end
end)
return in_use
end
local function cleanup_old_clash_path(old_path, new_path, sid)
if not old_path or old_path == "" or old_path == new_path then
return
end
if old_path:sub(1, #CLASH_YAML_DIR + 1) ~= CLASH_YAML_DIR .. "/" then
return
end
if clash_path_in_use(old_path, sid) then
return
end
nixio.fs.remove(old_path)
end
local function find_uploaded_clash_section(upload_name, final_path)
local sid
uci:foreach("shadowsocksr", "servers", function(section)
if section.type ~= "clash" or section.yaml_upload ~= "1" then
return
end
if upload_name ~= "" and section.yaml_upload_name == upload_name then
sid = section[".name"]
return false
end
if final_path ~= "" and section.clash_path == final_path then
sid = section[".name"]
return false
end
end)
return sid
end
local function save_uploaded_clash_node(upload_name, final_path)
local sid = find_uploaded_clash_section(upload_name, final_path)
local old_path
local alias
if not sid then
sid = uci:add("shadowsocksr", "servers")
end
if not sid then
return nil
end
old_path = uci:get("shadowsocksr", sid, "clash_path")
alias = uci:get("shadowsocksr", sid, "alias")
if not alias or alias == "" then
alias = upload_alias(upload_name)
end
uci:set("shadowsocksr", sid, "type", "clash")
uci:set("shadowsocksr", sid, "alias", alias)
uci:set("shadowsocksr", sid, "server", "127.0.0.1")
uci:set("shadowsocksr", sid, "server_port", "0")
uci:delete("shadowsocksr", sid, "clash_url")
uci:set("shadowsocksr", sid, "clash_path", final_path)
uci:set("shadowsocksr", sid, "clash_user_agent", uci:get("shadowsocksr", sid, "clash_user_agent") or "clash")
if not uci:get("shadowsocksr", sid, "switch_enable") then
uci:set("shadowsocksr", sid, "switch_enable", uci:get_first("shadowsocksr", "server_subscribe", "switch", "1") or "1")
end
uci:set("shadowsocksr", sid, "yaml_upload", "1")
uci:set("shadowsocksr", sid, "yaml_upload_name", upload_name)
uci:save("shadowsocksr")
uci:commit("shadowsocksr")
cleanup_old_clash_path(old_path, final_path, sid)
luci.sys.call(string.format("/etc/init.d/shadowsocksr clash_cache %s >/dev/null 2>&1 &", luci.util.shellquote(sid)))
return sid, alias
end
local has_mihomo = is_finded("mihomo")
local upload_fd
local upload_tmp_path
local upload_filename
local upload_message
local upload_errmessage
if has_mihomo then
luci.http.setfilehandler(function(meta, chunk, eof)
if not meta or meta.name ~= "clash_yaml_file" then
return
end
if not upload_fd then
if not meta.file or meta.file == "" then
return
end
upload_filename = tostring(meta.file):gsub("[\r\n]", "")
upload_tmp_path = string.format("/tmp/ssrplus-clash-upload-%d-%d.yaml", nixio.getpid(), os.time())
upload_fd = nixio.open(upload_tmp_path, "w")
if not upload_fd then
upload_errmessage = translate("Failed to create temporary YAML upload file.")
upload_tmp_path = nil
upload_filename = nil
return
end
end
if chunk and upload_fd then
upload_fd:write(chunk)
end
if eof and upload_fd then
upload_fd:close()
upload_fd = nil
end
end)
if luci.http.formvalue("upload_clash_yaml") then
if not upload_tmp_path or not upload_filename or not nixio.fs.access(upload_tmp_path) then
upload_errmessage = upload_errmessage or translate("No custom YAML file was selected.")
else
local hash
local final_path
local tmp_output = string.format("%s/.upload-%d-%d.yaml", CLASH_YAML_DIR, nixio.getpid(), os.time())
local sid
local alias
nixio.fs.mkdirr(CLASH_YAML_DIR)
nixio.fs.remove(tmp_output)
if preprocess_clash_yaml(upload_tmp_path, tmp_output) then
hash = hash_file(tmp_output)
if hash == "" then
nixio.fs.remove(tmp_output)
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
else
final_path = string.format("%s/%s.yaml", CLASH_YAML_DIR, hash)
luci.sys.call(string.format("mv -f %s %s", luci.util.shellquote(tmp_output), luci.util.shellquote(final_path)))
sid, alias = save_uploaded_clash_node(upload_filename, final_path)
if sid then
upload_message = string.format(translate("Custom YAML imported successfully: %s"), alias or sid)
else
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
end
end
else
nixio.fs.remove(tmp_output)
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
end
end
if upload_tmp_path then
nixio.fs.remove(upload_tmp_path)
end
end
end
local function preserve_when_hidden(opt, controller, enabled_value)
local original_parse = opt.parse
opt.parse = function(self, section, novld)
local current = self.map:get(section, controller)
if current == nil then
current = self.map:formvalue("cbid." .. self.map.config .. "." .. section .. "." .. controller)
end
if tostring(current or "") ~= tostring(enabled_value) then
return
end
return original_parse(self, section, novld)
end
end
local function migrate_legacy_subscribe_urls()
local subscribe_sid = uci:get_first("shadowsocksr", "server_subscribe")
if not subscribe_sid then
return
end
local legacy_urls = uci:get_list("shadowsocksr", subscribe_sid, "subscribe_url") or {}
if #legacy_urls == 0 then
return
end
local has_items = false
uci:foreach("shadowsocksr", "server_subscribe_item", function()
has_items = true
return false
end)
if has_items then
return
end
for index, url in ipairs(legacy_urls) do
local trimmed = trim(url)
if trimmed ~= "" then
local sid_output = luci.sys.exec("uci add shadowsocksr server_subscribe_item")
local sid = sid_output:match("%S+")
if sid and sid ~= "" then
local escaped_sid = luci.util.shellquote(sid)
local alias = string.format("Subscribe %d", index)
local escaped_alias = luci.util.shellquote(alias)
local escaped_url = luci.util.shellquote(trimmed)
luci.sys.call("uci set shadowsocksr." .. escaped_sid .. ".enabled=1")
luci.sys.call("uci set shadowsocksr." .. escaped_sid .. ".alias=" .. escaped_alias)
luci.sys.call("uci set shadowsocksr." .. escaped_sid .. ".url=" .. escaped_url)
end
end
end
luci.sys.call("uci delete shadowsocksr." .. subscribe_sid .. ".subscribe_url")
luci.sys.call("uci commit shadowsocksr")
end
local function clash_host_port(clash_url)
if not clash_url or clash_url == "" then
return nil, nil
end
local ok, parsed = pcall(URL.parse, clash_url)
if not ok or not parsed then
return nil, nil
end
local host = parsed.host
local port = parsed.port
if not port or port == "" then
port = (parsed.scheme == "http") and "80" or "443"
end
return host, port
end
migrate_legacy_subscribe_urls()
uci:foreach("shadowsocksr", "servers", function(s)
server_count = server_count + 1
server_sections[#server_sections + 1] = s[".name"]
server_cache[s[".name"]] = {
type = s.type,
v2ray_protocol = s.v2ray_protocol,
alias = s.alias,
server_port = s.server_port,
server = s.server,
transport = s.transport,
ws_path = s.ws_path,
ws_host = s.ws_host,
tls_host = s.tls_host,
tls = s.tls,
reality = s.reality,
clash_url = s.clash_url
}
end)
local function get_server(section)
return server_cache[section] or {}
end
do
local raw = nixio.fs.readfile("/tmp/ssrplus_server_detect.json")
if raw and raw ~= "" then
local parsed = json.parse(raw)
if type(parsed) == "table" then
detect_cache = parsed
end
end
end
m = Map("shadowsocksr", translate("Servers subscription and manage"))
if upload_errmessage then
m.errmessage = upload_errmessage
elseif upload_message then
m.message = upload_message
end
local style_section = m:section(SimpleSection)
style_section.template = "shadowsocksr/servers_subscribe_url_style"
-- Server Subscribe
s = m:section(TypedSection, "server_subscribe")
@@ -118,6 +408,13 @@ o = s:option(Flag, "auto_update", translate("Auto Update"))
o.rmempty = false
o.description = translate("Auto Update Server subscription, GFW list and CHN route")
o = s:option(ListValue, "config_auto_update_mode", translate("Update Mode"))
o:value("0", translate("Appointment Mode"))
o:value("1", translate("Loop Mode"))
o.default = "0"
o.rmempty = true
o:depends("auto_update", "1")
o = s:option(ListValue, "auto_update_week_time", translate("Update cycle (Day/Week)"))
o:value('*', translate("Every Day"))
o:value("1", translate("Every Monday"))
@@ -129,7 +426,7 @@ o:value("6", translate("Every Saturday"))
o:value("0", translate("Every Sunday"))
o.default = "*"
o.rmempty = true
o:depends("auto_update", "1")
o:depends({auto_update = "1", config_auto_update_mode = "0"})
o = s:option(ListValue, "auto_update_day_time", translate("Regular update (Hour)"))
for t = 0, 23 do
@@ -137,7 +434,7 @@ for t = 0, 23 do
end
o.default = 2
o.rmempty = true
o:depends("auto_update", "1")
o:depends({auto_update = "1", config_auto_update_mode = "0"})
o = s:option(ListValue, "auto_update_min_time", translate("Regular update (Min)"))
for i = 0, 59 do
@@ -145,178 +442,83 @@ for i = 0, 59 do
end
o.default = 30
o.rmempty = true
o:depends("auto_update", "1")
o:depends({auto_update = "1", config_auto_update_mode = "0"})
-- 确保 hy2_type_list 不为空
if #hy2_type_list > 0 then
local sid = uci:get_first("shadowsocksr", "server_subscribe")
if not sid then
uci:foreach("shadowsocksr", "server_subscribe", function(section)
sid = section[".name"]
return false
end)
end
if sid then
local old_val = uci:get("shadowsocksr", sid, "xray_hy2_type")
if old_val and old_val ~= "" then
if (old_val == "hysteria2" and not has_hysteria2) or
(old_val == "v2ray" and not has_xray) then
-- 核心不可用,设置为空(删除配置)
uci:set("shadowsocksr", sid, "xray_hy2_type", "")
uci:commit("shadowsocksr")
end
end
end
o = s:option(ListValue, "xray_hy2_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Hysteria2")))
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
o:value("", translate("Auto"))
for _, v in ipairs(hy2_type_list) do
o:value(v.id, v.name) -- 存储 "Xray" / "Hysteria2",但 UI 显示完整名称
end
end
-- 确保 tj_type_list 不为空
if #tj_type_list > 0 then
local sid = uci:get_first("shadowsocksr", "server_subscribe")
if not sid then
uci:foreach("shadowsocksr", "server_subscribe", function(section)
sid = section[".name"]
return false
end)
end
if sid then
local old_val = uci:get("shadowsocksr", sid, "xray_tj_type")
if old_val and old_val ~= "" then
if (old_val == "trojan" and not has_trojan) or
(old_val == "v2ray" and not has_xray) then
-- 核心不可用,设置为空(删除配置)
uci:set("shadowsocksr", sid, "xray_tj_type", "")
uci:commit("shadowsocksr")
end
end
end
o = s:option(ListValue, "xray_tj_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Type", "Trojan")))
o.description = translate("The configured type also applies to the core specified when manually importing nodes.")
o:value("", translate("Auto"))
for _, v in ipairs(tj_type_list) do
o:value(v.id, v.name) -- 存储 "Xray" / "Trojan",但 UI 显示完整名称
end
end
-- 确保 ss_type_list 不为空
if #ss_type_list > 0 then
local sid = uci:get_first("shadowsocksr", "server_subscribe")
if not sid then
uci:foreach("shadowsocksr", "server_subscribe", function(section)
sid = section[".name"]
return false
end)
end
if sid then
local old_val = uci:get("shadowsocksr", sid, "ss_type")
if old_val and old_val ~= "" then
if (old_val == "ss-rust" and not has_ss_rust) or
(old_val == "ss-libev" and not has_ss_libev) or
(old_val == "v2ray" and not has_xray) then
-- 核心不可用,设置为空(删除配置)
uci:set("shadowsocksr", sid, "ss_type", "")
uci:commit("shadowsocksr")
end
end
end
o = s:option(ListValue, "ss_type", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
o.description = translate("Selection ShadowSocks Node Use Version.")
o:value("", translate("Auto"))
for _, v in ipairs(ss_type_list) do
o:value(v.id, v.name) -- 存储 "ss-libev" / "ss-rust",但 UI 显示完整名称
end
end
o = s:option(DynamicList, "subscribe_url", translate("Subscribe URL"))
o = s:option(Value, "config_update_interval", translate("Update Interval(min)"))
o.default = "60"
o.datatype = "uinteger"
o.rmempty = true
o:depends({auto_update = "1", config_auto_update_mode = "1"})
o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"))
o.description = translate(
"<ul>" ..
"<li>" .. translate("If the node address is a domain name, this DNS will be used for resolution.") .. "</li>" ..
"<li>" .. string.format('<font style=\'color:red;\'>%s</font>', translate("Supports only Xray node types.")) .. "</li>" ..
"<li>" .. string.format('<font style=\'color:red;\'>%s</font>', translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead.")) .. "</li>" ..
"</ul>"
)
o:value("", translate("Auto"))
o:value("tcp", translate("TCP"))
o:value("udp", translate("UDP"))
o:value("https", translate("DoH"))
o = s:option(Value, "domain_resolver_dns", translate("DNS"))
o.datatype = "or(ipaddr,ipaddrport)"
o:value("114.114.114.114")
o:value("223.5.5.5:53")
o.default = "114.114.114.114"
o:depends("domain_resolver", "tcp")
o:depends("domain_resolver", "udp")
o = s:option(Value, "domain_resolver_dns_https", translate("DNS"))
o:value("https://120.53.53.53/dns-query", "DNSPod")
o:value("https://223.5.5.5/dns-query", "AliDNS")
o.default = o.keylist[1]
o:depends("domain_resolver", "https")
o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"))
o.description = translate(
"<ul>" ..
"<li>" .. translate("If is domain name, The requested domain name will be resolved to IP before connect.") .. "</li>" ..
"<li>" .. string.format('<font style=\'color:red;\'>%s</font>', translate("Supports only Xray node types.")) .. "</li>" ..
"<li>" .. string.format('<font style=\'color:red;\'>%s</font>', translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead.")) .. "</li>" ..
"</ul>"
)
o.default = ""
o:value("", translate("Auto"))
o:value("UseIPv4v6", translate("Prefer IPv4"))
o:value("UseIPv6v4", translate("Prefer IPv6"))
o:value("UseIPv4", translate("IPv4 Only"))
o:value("UseIPv6", translate("IPv6 Only"))
o = s:option(Flag, "_subscribe_advanced_toggle", translate("Subscribe Advanced Settings"))
o.rmempty = false
o.default = "0"
o.write = function() end
o.remove = function() end
o = s:option(Value, "filter_words", translate("Subscribe Filter Words"))
o.rmempty = true
o.description = translate("Filter Words splited by /")
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
o = s:option(Value, "save_words", translate("Subscribe Save Words"))
o.rmempty = true
o.description = translate("Save Words splited by /")
o = s:option(Button, "update_Sub", translate("Update Subscribe List"))
o.inputstyle = "reload"
o.description = translate("Update subscribe url list first")
o.write = function()
uci:commit("shadowsocksr")
luci.sys.exec("rm -rf /tmp/sub_md5_*")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers"))
end
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
o = s:option(Flag, "allow_insecure", translate("Allow subscribe Insecure nodes By default"))
o.rmempty = false
o.description = translate("Subscribe nodes allows insecure connection as TLS client (insecure)")
o.default = "0"
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
o = s:option(Flag, "switch", translate("Subscribe Default Auto-Switch"))
o.rmempty = false
o.description = translate("Subscribe new add server default Auto-Switch on")
o.default = "1"
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
o = s:option(Flag, "proxy", translate("Through proxy update"))
o.rmempty = false
o.description = translate("Through proxy update list, Not Recommended ")
o.default = "1"
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
o = s:option(Button, "_save_subscribe_settings", translate("Save Subscribe Settings"))
o.inputstyle = "save"
o.description = translate("Save current subscribe settings")
o.write = function() end
o = s:option(Button, "subscribe", translate("Update All Subscribe Servers"))
o.rawhtml = true
o.template = "shadowsocksr/subscribe"
o.write = function(self, section)
self.map.ssr_subscribe_requested = true
end
o = s:option(Button, "delete", translate("Delete All Subscribe Servers"))
o.inputstyle = "reset"
o.description = string.format(translate("Server Count") .. ": %d", server_count)
o.write = function()
luci.http.redirect(url("delete"))
uci:delete_all("shadowsocksr", "servers", function(s)
if s.hashkey or s.isSubscribe then
return true
else
return false
end
end)
uci:save("shadowsocksr")
uci:commit("shadowsocksr")
for file in nixio.fs.glob("/tmp/sub_md5_*") do
nixio.fs.remove(file)
end
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "delete"))
return
end
o = s:option(Value, "url_test_url", translate("URL Test Address"))
@@ -335,57 +537,187 @@ o:value("curl", "Curl")
o:value("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Linux")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Windows")
o:value("v2rayN/9.99", "v2rayN")
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
if has_mihomo then
o = s:option(DummyValue, "_upload_clash_yaml", translate("Upload Custom YAML File"))
o.template = "shadowsocksr/clash_yaml_upload"
o.description = translate("Upload a custom Clash/Mihomo YAML file. The file will be preprocessed and saved as a local Clash node.")
end
s:append(cbi.Template("shadowsocksr/subscribe_schedule_compact"))
s = m:section(TypedSection, "server_subscribe_item", translate("Subscribe URL"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "shadowsocksr/subscribe_actions_footer"
--s.template = "cbi/tblsection"
--s.template_addremove = "shadowsocksr/subscribe_actions_footer"
s.description = translate("Manage multiple subscribe URLs, including Clash subscriptions. Only enabled entries are included when updating all subscriptions.")
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = "1"
o.width = "1%"
function o.cfgvalue(...)
return Flag.cfgvalue(...) or "1"
end
o = s:option(Value, "alias", translate("Alias"))
o.rmempty = true
o.width = "7.5rem"
function o.cfgvalue(self, section)
return Value.cfgvalue(self, section) or string.format("Subscribe %s", section:sub(-4))
end
o = s:option(Value, "url", translate("Subscribe URL"))
o.rmempty = false
-- [[ Servers Manage ]]--
s = m:section(TypedSection, "servers")
s.anonymous = true
s.addremove = true
s.template = "cbi/tblsection"
s.description = translate("Node order can be dragged with the mouse and takes effect immediately. The automatic switch order of server nodes is consistent with the node order in the table.")
s.template = "shadowsocksr/server_table"
set_apply_on_parse(m)
s:append(cbi.Template("shadowsocksr/optimize_cbi_ui"))
s.extedit = luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers", "%s")
local server_page_size = parse_nonnegative_int(luci.http.formvalue("server_page_size"))
if not server_page_size or not in_list(SERVER_PAGE_SIZE_OPTIONS, server_page_size) then
server_page_size = DEFAULT_SERVER_PAGE_SIZE
end
local server_page_count = 1
if server_page_size > 0 and server_count > 0 then
server_page_count = math.ceil(server_count / server_page_size)
end
local server_page = parse_nonnegative_int(luci.http.formvalue("server_page")) or 1
if server_page < 1 then
server_page = 1
end
if server_page > server_page_count then
server_page = server_page_count
end
local visible_server_sections = server_sections
local server_first_index = server_count > 0 and 1 or 0
local server_last_index = server_count
if server_page_size > 0 then
local first = ((server_page - 1) * server_page_size) + 1
local last = math.min(first + server_page_size - 1, server_count)
visible_server_sections = {}
server_first_index = server_count > 0 and first or 0
server_last_index = server_count > 0 and last or 0
for index = first, last do
visible_server_sections[#visible_server_sections + 1] = server_sections[index]
end
end
s.sortable = true
--[[
s.extedit = url("servers", "%s")
function s.cfgsections(self)
return visible_server_sections
end
s.server_page = server_page
s.server_page_size = server_page_size
s.server_page_count = server_page_count
s.server_page_sizes = SERVER_PAGE_SIZE_OPTIONS
s.server_total = server_count
s.server_first_index = server_first_index
s.server_last_index = server_last_index
s.server_base_url = luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers")
function s.create(self, ...)
local sid = TypedSection.create(self, ...)
if sid then
local newsid = "cfg" .. sid:sub(-6)
-- 删除匿名
local used_sid = {}
local next_sid = 1
self.map.uci:foreach(self.config, self.sectiontype, function(s)
local num = s[".name"]:match("^cfg(%x%x)")
if num then
local n = tonumber(num, 16)
if n then
used_sid[n] = true
end
end
end)
local function get_next_sid()
while used_sid[next_sid] do
next_sid = next_sid + 1
end
used_sid[next_sid] = true
return next_sid
end
local sid = TypedSection.create(self, ...)
if sid then
local suffix = sid:sub(-4)
self.map.uci:delete(self.config, sid)
-- 重命名 section
self.map.uci:section(self.config, self.sectiontype, newsid)
luci.http.redirect(self.extedit % newsid)
return
end
local id = get_next_sid()
local newsid = string.format("cfg%02x%s", id, suffix)
local success = self.map.uci:section(self.config, self.sectiontype, newsid)
if success then
--self.map.uci:save(self.config)
luci.http.redirect(self.extedit % newsid)
return
end
end
end
]]--
o = s:option(DummyValue, "type", translate("Type"))
function o.cfgvalue(self, section)
return m:get(section, "v2ray_protocol") or Value.cfgvalue(self, section) or translate("None")
local cfg = get_server(section)
return cfg.v2ray_protocol or cfg.type or translate("None")
end
o = s:option(DummyValue, "alias", translate("Alias"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("None")
end
o = s:option(DummyValue, "server_port", translate("Server Port"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or "N/A"
function o.cfgvalue(self, section)
return get_server(section).alias or translate("None")
end
o = s:option(DummyValue, "server_port", translate("Socket Connected"))
o.template = "shadowsocksr/socket"
o.width = "10%"
function o.cfgvalue(self, section)
self.detect_cache = detect_cache[section]
local cfg = get_server(section)
local stype = cfg.type
if stype == "clash" then
return "N/A"
end
return cfg.server_port
end
o.render = function(self, section, scope)
local cfg = s:cfgvalue(section) or {}
self.transport = cfg.transport
self.type = cfg.type
self.v2ray_protocol = cfg.v2ray_protocol
if self.transport == 'ws' then
self.ws_path = cfg.ws_path
self.tls = cfg.tls
self.tls_host = cfg.tls_host
local cfg = get_server(section)
local stype = cfg.type
self.type = stype or ""
self.proto = cfg.v2ray_protocol or ""
self.reality = cfg.reality or ""
if stype == "clash" then
self.transport = ""
self.ws_path = ""
self.ws_host = ""
self.tls_host = ""
self.tls = ""
self.reality = ""
else
self.transport = cfg.transport or ""
self.ws_host = cfg.ws_host or ""
self.tls_host = cfg.tls_host or ""
if self.transport == 'ws' then
self.ws_path = cfg.ws_path or ""
self.tls = cfg.tls or ""
else
self.ws_path = ""
self.tls = ""
end
end
DummyValue.render(self, section, scope)
end
@@ -393,6 +725,15 @@ end
o = s:option(DummyValue, "server", translate("Ping Latency"))
o.template = "shadowsocksr/ping"
o.width = "10%"
function o.cfgvalue(self, section)
self.detect_cache = detect_cache[section]
local cfg = get_server(section)
self.type = cfg.type or ""
if cfg.type == "clash" then
return "N/A"
end
return cfg.server or "N/A"
end
local global_server = uci:get_first('shadowsocksr', 'global', 'global_server')
@@ -407,11 +748,10 @@ node.render = function(self, section, scope)
Button.render(self, section, scope)
end
node.write = function(self, section)
uci:set("shadowsocksr", '@global[0]', 'global_server', section)
uci:save("shadowsocksr")
uci:commit("shadowsocksr")
luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &")
luci.http.redirect(url("restart"))
local safe_section = luci.util.shellquote(section)
local cmd = string.format("uci set shadowsocksr.@global[0].global_server=%s && uci commit shadowsocksr", safe_section)
luci.sys.call(cmd)
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "restart"))
end
o = s:option(Flag, "switch_enable", translate("Auto Switch"))
@@ -422,4 +762,15 @@ end
m:append(cbi.Template("shadowsocksr/server_list"))
m.commit_handler = function(self)
if not self.ssr_subscribe_requested then
return
end
for _, config in ipairs(self.parsechain or {}) do
self.uci:commit(config)
end
self.ssr_subscribe_autostart = true
end
return m
@@ -7,15 +7,22 @@ local m, s, o
local redir_run = 0
local reudp_run = 0
local sock5_run = 0
local http_run = 0
local server_run = 0
local kcptun_run = 0
local tunnel_run = 0
local gfw_count = 0
local ad_count = 0
local ip_count = 0
local nfip_count = 0
local Process_list = luci.sys.exec("busybox ps -w")
local Process_list = luci.sys.exec("busybox ps -w 2>/dev/null || busybox ps")
local uci = require "luci.model.uci".cursor()
local global_server = uci:get_first("shadowsocksr", "global", "global_server", "nil")
local global_type = global_server ~= "nil" and (uci:get("shadowsocksr", global_server, "type") or "") or ""
local global_socks_enabled = uci:get_first("shadowsocksr", "socks5_proxy", "enabled", "0") == "1"
local global_socks_server = uci:get_first("shadowsocksr", "socks5_proxy", "server", "nil")
local global_http_enabled = uci:get_first("shadowsocksr", "http_proxy", "enabled", "0") == "1"
local has_3proxy = nixio.fs.access("/usr/bin/3proxy") or nixio.fs.access("/usr/libexec/3proxy") or nixio.fs.access("/bin/3proxy")
local pdnsd_mode = uci:get_first("shadowsocksr", 'global', 'pdnsd_enable', '0')
-- html constants
font_blue = [[<b style=color:green>]]
style_blue = [[<b style=color:red>]]
@@ -52,10 +59,6 @@ if nixio.fs.access("/etc/ssrplus/applechina.conf") then
apple_count = tonumber(luci.sys.exec("cat /etc/ssrplus/applechina.conf | wc -l"))
end
if nixio.fs.access("/etc/ssrplus/netflixip.list") then
nfip_count = tonumber(luci.sys.exec("cat /etc/ssrplus/netflixip.list | wc -l"))
end
if Process_list:find("udp.only.ssr.reudp") then
reudp_run = 1
end
@@ -74,6 +77,10 @@ if Process_list:find("tcp.udp.ssr.local") then
sock5_run = 1
end
if has_3proxy and Process_list:find("3proxy%-ssr%-http%.cfg") then
http_run = 1
end
if Process_list:find("tcp.udp.ssr.retcp") then
redir_run = 1
reudp_run = 1
@@ -103,6 +110,38 @@ if Process_list:find("local.udp.ssr.retcp") then
sock5_run = 1
end
if global_type == "socks5" and Process_list:find("ipt2socks") then
if Process_list:find("%-T") or Process_list:find("%-%-tcp%-only") then
redir_run = 1
end
if Process_list:find("%-U") or Process_list:find("%-%-udp%-only") then
reudp_run = 1
end
if global_socks_enabled and (global_socks_server == "same" and global_socks_server == global_server) then
sock5_run = 1
end
end
if (global_type == "clash" or global_type == "tuic" or global_type == "ss") and Process_list:find("ssr%-retcp") then
redir_run = 1
reudp_run = 1
if global_socks_enabled and (global_socks_server == "same" and global_socks_server == global_server) then
sock5_run = 1
end
end
if (global_type == "clash" or global_type == "tuic" or global_type == "ss") and Process_list:find("mihomo") and (Process_list:find("/clash%-") or Process_list:find("/tuic%-") or Process_list:find("/ss%-")) then
redir_run = 1
reudp_run = 1
if global_socks_enabled and (global_socks_server == "same" and global_socks_server == global_server) then
sock5_run = 1
end
end
if has_3proxy and global_http_enabled and http_run == 0 and Process_list:find("3proxy%-ssr%-http%.cfg") then
http_run = 1
end
if Process_list:find("kcptun.client") then
kcptun_run = 1
end
@@ -111,11 +150,21 @@ if Process_list:find("ssr.server") then
server_run = 1
end
if Process_list:find("mihomo") and Process_list:find("/ss%-server%-") then
server_run = 1
end
if Process_list:find("ssrplus/bin/dns2tcp") or
Process_list:find("ssrplus/bin/mosdns") or
Process_list:find("dnsproxy.*127.0.0.1.*5335") or
Process_list:find("chinadns.*127.0.0.1.*5335") or
(Process_list:find("ssrplus.dns") and Process_list:find("dns2socks.*127.0.0.1.*127.0.0.1.5335")) then
Process_list:find("chinadns.*127.0.0.1.*5335") then
pdnsd_run = 1
end
if pdnsd_mode == "7" and (global_type == "clash" or global_type == "tuic" or global_type == "ss") and Process_list:find("ssr%-retcp") then
pdnsd_run = 1
end
if pdnsd_mode == "7" and global_type == "v2ray" and Process_list:find("ssr%-retcp%.json") then
pdnsd_run = 1
end
@@ -157,6 +206,16 @@ else
s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off
end
if has_3proxy then
s = m:field(DummyValue, "http_run", translate("Global HTTP/HTTPS Proxy Server"))
s.rawhtml = true
if http_run == 1 then
s.value = font_blue .. bold_on .. translate("Running") .. bold_off .. font_off
else
s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off
end
end
s = m:field(DummyValue, "server_run", translate("Local Servers"))
s.rawhtml = true
if server_run == 1 then
@@ -211,13 +270,6 @@ if uci:get_first("shadowsocksr", 'global', 'apple_optimization', '0') ~= '0' the
s.value = apple_count .. " " .. translate("Records")
end
if uci:get_first("shadowsocksr", 'global', 'netflix_enable', '0') ~= '0' then
s = m:field(DummyValue, "nfip_data", translate("Netflix IP Data"))
s.rawhtml = true
s.template = "shadowsocksr/refresh"
s.value = nfip_count .. " " .. translate("Records")
end
if uci:get_first("shadowsocksr", 'global', 'adblock', '0') == '1' then
s = m:field(DummyValue, "ad_data", translate("Advertising Data"))
s.rawhtml = true
@@ -0,0 +1,143 @@
<style type="text/css">
.ssr-advanced-switch-inline {
display: flex;
gap: 2%;
align-items: flex-start;
flex-wrap: nowrap;
}
.ssr-advanced-switch-item {
width: 30%;
min-width: 0;
}
.ssr-advanced-switch-item input {
width: 100%;
box-sizing: border-box;
}
.ssr-advanced-switch-subtitle {
font-size: 12px;
color: #666;
margin-bottom: 4px;
line-height: 1.2;
}
.ssr-advanced-switch-hidden {
display: none !important;
}
</style>
<script type="text/javascript">
//<![CDATA[
function ssrCompactAdvancedSwitch() {
function getInputByName(suffix) {
return document.querySelector('input[name$=".' + suffix + '"]');
}
function getRowByName(suffix) {
var el = getInputByName(suffix);
return el ? el.closest('.cbi-value') : null;
}
function compactSwitchRow() {
var timeRow = getRowByName('switch_time');
var timeoutRow = getRowByName('switch_timeout');
var tryRow = getRowByName('switch_try_count');
if (!timeRow || !timeoutRow || !tryRow) return;
var timeField = timeRow.querySelector('.cbi-value-field');
var timeoutField = timeoutRow.querySelector('.cbi-value-field');
var tryField = tryRow.querySelector('.cbi-value-field');
if (!timeField || !timeoutField || !tryField) return;
if (timeField.querySelector('.ssr-advanced-switch-inline')) {
return;
}
var timeInput = timeField.querySelector('input');
var timeoutInput = timeoutField.querySelector('input');
var tryInput = tryField.querySelector('input');
if (!timeInput || !timeoutInput || !tryInput) return;
var wrap = document.createElement('div');
wrap.className = 'ssr-advanced-switch-inline';
function makeItem(title, input) {
var item = document.createElement('div');
item.className = 'ssr-advanced-switch-item';
var subtitle = document.createElement('div');
subtitle.className = 'ssr-advanced-switch-subtitle';
subtitle.textContent = title;
item.appendChild(subtitle);
item.appendChild(input);
return item;
}
wrap.appendChild(makeItem('<%:Second%>', timeInput));
wrap.appendChild(makeItem('<%:Timeout%>', timeoutInput));
wrap.appendChild(makeItem('<%:Try Count%>', tryInput));
timeField.innerHTML = '';
timeField.appendChild(wrap);
timeoutRow.classList.add('ssr-advanced-switch-hidden');
tryRow.classList.add('ssr-advanced-switch-hidden');
}
compactSwitchRow();
}
(function() {
var retryCount = 0;
var maxRetries = 30;
var retryTimer = null;
var observer = null;
function hasCompactLayout() {
return !!document.querySelector('.ssr-advanced-switch-inline');
}
function tryCompact() {
ssrCompactAdvancedSwitch();
if (hasCompactLayout()) {
if (retryTimer) {
window.clearTimeout(retryTimer);
retryTimer = null;
}
if (observer) {
observer.disconnect();
observer = null;
}
return;
}
if (retryCount >= maxRetries) {
return;
}
retryCount = retryCount + 1;
retryTimer = window.setTimeout(tryCompact, 200);
}
function startWatcher() {
tryCompact();
if (window.MutationObserver) {
observer = new MutationObserver(function() {
if (!hasCompactLayout()) {
tryCompact();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startWatcher);
window.addEventListener('load', tryCompact);
} else {
startWatcher();
}
})();
//]]>
</script>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,104 @@
<%+header%>
<h2><%:Clash Panel%> - <%=alias%></h2>
<div class="cbi-map-descr">
<%:This panel is available only when the current Clash total node is active.%>
</div>
<div class="cbi-section-actions" style="margin: 1em 0;">
<input type="button" id="clash-panel-refresh" class="btn cbi-button cbi-button-apply" value="<%:Refresh%>" />
</div>
<div id="clash-panel-status" style="margin: 1em 0; color: #666;"></div>
<div id="clash-panel-groups"></div>
<script type="text/javascript">
//<![CDATA[
(function() {
const sid = <%=string.format("%q", sid)%>;
const statusEl = document.getElementById('clash-panel-status');
const groupsEl = document.getElementById('clash-panel-groups');
const refreshBtn = document.getElementById('clash-panel-refresh');
function setStatus(text, color) {
statusEl.style.color = color || '#666';
statusEl.textContent = text;
}
function renderGroups(groups) {
groupsEl.innerHTML = '';
if (!groups || !groups.length) {
groupsEl.innerHTML = '<p><%:No switchable proxy groups found.%></p>';
return;
}
groups.forEach(function(group) {
const wrapper = document.createElement('div');
wrapper.className = 'cbi-value';
wrapper.style.marginBottom = '1em';
const label = document.createElement('label');
label.className = 'cbi-value-title';
label.textContent = group.name + (group.type ? ' (' + group.type + ')' : '');
const field = document.createElement('div');
field.className = 'cbi-value-field';
const select = document.createElement('select');
select.className = 'cbi-input-select';
group.all.forEach(function(name) {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
if (name === group.now) {
option.selected = true;
}
select.appendChild(option);
});
select.onchange = function() {
setStatus('<%:Switching...%>', '#0072c3');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "clash_switch")%>', {
sid: sid,
group: group.name,
name: select.value
}, function(x, result) {
if (result && result.success) {
setStatus('<%:Switched successfully.%>', '#249400');
loadGroups();
} else {
setStatus('<%:Switch failed.%>', '#ff0000');
}
});
};
field.appendChild(select);
wrapper.appendChild(label);
wrapper.appendChild(field);
groupsEl.appendChild(wrapper);
});
}
function loadGroups() {
setStatus('<%:Loading...%>', '#666');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "clash_groups")%>', {
sid: sid
}, function(x, result) {
if (!result || !result.active) {
setStatus('<%:This Clash total node is not active. Apply this node first.%>', '#ff7700');
groupsEl.innerHTML = '';
return;
}
setStatus('<%:Ready.%>', '#249400');
renderGroups(result.groups || []);
});
}
refreshBtn.onclick = loadGroups;
loadGroups();
})();
//]]>
</script>
<%+footer%>
@@ -0,0 +1,74 @@
<%+cbi/valueheader%>
<%
local stype = self.map:get(section, "type")
-%>
<script type="text/javascript">
if (typeof(window.ssrClashRefreshSubscription) !== "function") {
window.ssrClashRefreshSubscription = function(btn, sid) {
var original = btn.value;
btn.disabled = true;
btn.value = '<%:Reloading YAML...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "clash_refresh")%>', {
sid: sid
}, function(x, result) {
btn.disabled = false;
btn.value = original;
if (result && result.success) {
btn.value = result.reapplied ? '<%:Reapply%>' : '<%:YAML Reloaded!%>';
setTimeout(function() { btn.value = original; }, 1200);
} else {
btn.value = '<%:YAML Reload Failed!%>';
setTimeout(function() { btn.value = original; }, 1500);
}
});
return false;
}
}
if (typeof(window.ssrClashResetDefaults) !== "function") {
window.ssrClashResetDefaults = function(btn, sid) {
var original = btn.value;
if (!window.confirm('<%:Reset saved Mihomo proxy-group selections and restore YAML defaults?%>')) {
return false;
}
btn.disabled = true;
btn.value = '<%:Resetting Defaults...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "clash_reset_defaults")%>', {
sid: sid
}, function(x, result) {
btn.disabled = false;
btn.value = original;
if (result && result.success) {
btn.value = result.reapplied ? '<%:Defaults Restored!%>' : '<%:Defaults Cleared!%>';
setTimeout(function() { btn.value = original; }, 1500);
} else {
btn.value = '<%:Reset Defaults Failed!%>';
setTimeout(function() { btn.value = original; }, 1600);
}
});
return false;
}
}
</script>
<% if stype == "clash" then %>
<input
type="button"
class="btn cbi-button"
value="<%:Reload YAML%>"
title="<%:Reload YAML%>"
onclick="return ssrClashRefreshSubscription(this, '<%=section%>');" />
<input
type="button"
class="btn cbi-button"
value="<%:Reset Default Proxies Rules%>"
title="<%:Reset Default Proxies Rules%>"
onclick="return ssrClashResetDefaults(this, '<%=section%>');" />
<input
type="button"
class="btn cbi-button cbi-button-apply"
value="<%:Panel%>"
title="<%:Panel%>"
onclick="window.open('<%=luci.dispatcher.build_url('admin', 'services', 'shadowsocksr', 'clash_panel')%>?sid=<%=section%>', '_blank');" />
<% else %>
-
<% end %>
<%+cbi/valuefooter%>
@@ -0,0 +1,4 @@
<%+cbi/valueheader%>
<input class="cbi-input-file" style="width: 400px" type="file" id="clash_yaml_file" name="clash_yaml_file" accept=".yaml,.yml" />
<input type="submit" class="cbi-button cbi-input-apply" name="upload_clash_yaml" value="<%:Upload%>" />
<%+cbi/valuefooter%>
@@ -0,0 +1,78 @@
<script type="text/javascript">
//<![CDATA[
(function() {
function getField(name) {
return document.querySelector('select[name$=".' + name + '"], input[name$=".' + name + '"]');
}
function ensureDefaultValue(modeValue, fieldName) {
var modeEl = getField('pdnsd_enable');
var dnsEl = getField(fieldName);
if (!modeEl || !dnsEl || modeEl.value !== modeValue) {
return;
}
var current = (dnsEl.value || '').trim();
var placeholder = (dnsEl.options && dnsEl.selectedIndex >= 0 && dnsEl.options[dnsEl.selectedIndex])
? (dnsEl.options[dnsEl.selectedIndex].text || '').trim()
: '';
if (current !== '' && current !== '-- 请选择 --' && current !== '-- Please choose --' &&
placeholder !== '-- 请选择 --' && placeholder !== '-- Please choose --') {
return;
}
if (dnsEl.tagName === 'SELECT') {
var firstValue = '';
for (var i = 0; i < dnsEl.options.length; i++) {
var option = dnsEl.options[i];
if (option && option.value) {
firstValue = option.value;
break;
}
}
if (firstValue) {
dnsEl.value = firstValue;
}
} else if (dnsEl.dataset && dnsEl.dataset.default) {
dnsEl.value = dnsEl.dataset.default;
}
}
function ensureMosdnsDefault() {
ensureDefaultValue('4', 'tunnel_forward_mosdns');
}
function ensureChinaDnsNgDefault() {
ensureDefaultValue('6', 'chinadns_ng_tunnel_forward');
}
function bindModeChange() {
var modeEl = getField('pdnsd_enable');
if (!modeEl || modeEl.dataset.ssrDnsDefaultBound) {
return;
}
modeEl.dataset.ssrDnsDefaultBound = '1';
modeEl.addEventListener('change', function() {
window.setTimeout(function() {
ensureMosdnsDefault();
ensureChinaDnsNgDefault();
}, 0);
});
}
function initMosdnsDefault() {
bindModeChange();
ensureMosdnsDefault();
ensureChinaDnsNgDefault();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMosdnsDefault);
window.addEventListener('load', initMosdnsDefault);
} else {
initMosdnsDefault();
}
})();
//]]>
</script>
@@ -0,0 +1,446 @@
<script type="text/javascript">
//<![CDATA[
var componentMeta = {
xray: {
label: '<%:Xray-core%>',
help: '<%:The online upgrade downloads the matching Xray-core linux archive for the current ARCH from the official GitHub release page.%>'
},
mihomo: {
label: '<%:Mihomo%>',
help: '<%:The online upgrade only tracks the latest stable Mihomo release and downloads the matching linux archive for the current ARCH from the official GitHub release page.%>'
},
naiveproxy: {
label: '<%:NaiveProxy%>',
help: '<%:The online upgrade tracks the latest NaiveProxy release and prefers OpenWrt static archives matching the current ARCH before falling back to other release assets.%>'
}
};
var geoMeta = {
country_mmdb: {
label: '<%:Country MMDB%>',
help: '<%:Update the Country.mmdb database used by Mihomo/Clash runtime when available.%>'
},
geosite: {
label: '<%:GeoSite Database%>',
help: '<%:Update the GeoSite.dat database used by Mihomo/Clash runtime when available.%>'
},
v2ray_geo: {
label: '<%:V2Ray GEO Databases%>',
help: '<%:Update geoip.dat and geosite.dat used by Xray/V2Ray runtime when available.%>'
}
};
var componentMessageMap = {
'Already up to date': '<%:Already up to date%>',
'Upgrade completed': '<%:Upgrade completed%>',
'Unsupported ARCH': '<%:Unsupported ARCH%>',
'Failed to fetch release metadata': '<%:Failed to fetch release metadata%>',
'Matching release asset not found': '<%:Matching release asset not found%>',
'Missing unzip support': '<%:Missing unzip support%>',
'Missing gzip support': '<%:Missing gzip support%>',
'Missing xz support': '<%:Missing xz support%>',
'Failed to create temp directory': '<%:Failed to create temp directory%>',
'Download failed': '<%:Download failed%>',
'Extract failed': '<%:Extract failed%>',
'xray binary not found in archive': '<%:xray binary not found in archive%>',
'mihomo binary not found in archive': '<%:mihomo binary not found in archive%>',
'naive binary not found in archive': '<%:naive binary not found in archive%>',
'Install failed': '<%:Install failed%>',
'Installed binary failed to run': '<%:Installed binary failed to run%>'
};
function selectedComponent() {
var selector = document.getElementById('component-selector');
return selector ? selector.value : 'xray';
}
function selectedMirror() {
var selector = document.getElementById('component-mirror');
return selector ? selector.value : 'direct';
}
function componentHelp(component) {
return componentMeta[component] ? componentMeta[component].help : '';
}
function displayMessage(message) {
return componentMessageMap[message] || message || '';
}
function displayError(error) {
if (!error) {
return '';
}
var map = {
unsupported_arch: '<%:Unsupported ARCH%>',
fetch_failed: '<%:Failed to fetch release metadata%>',
asset_not_found: '<%:Matching release asset not found%>',
unsupported_component: '<%:Unsupported component%>',
unknown_error: '<%:Unknown error%>'
};
return map[error] || error;
}
function renderComponentInfo(data, checkedRemote) {
var panel = document.getElementById('component-status');
var upgradeButton = document.getElementById('component-upgrade');
var action = document.getElementById('component-action');
var help = document.getElementById('component-help');
var html = [];
var component = data && data.component ? data.component : selectedComponent();
if (!panel) {
return;
}
if (!data) {
panel.innerHTML = '<span style="color:red"><%:Failed to query component information.%></span>';
if (upgradeButton) upgradeButton.disabled = true;
return;
}
if (help) {
help.innerHTML = componentHelp(component);
}
html.push('<div><strong><%:Installed Version%>:</strong> ' + (data.current_version || '<%:Not installed%>') + '</div>');
html.push('<div><strong><%:Latest Version%>:</strong> ' + (data.latest_version || '<%:Unknown%>') + '</div>');
html.push('<div><strong><%:Current ARCH%>:</strong> ' + (data.arch || '<%:Unknown%>') + '</div>');
html.push('<div><strong><%:Release Asset%>:</strong> ' + (data.asset || '<%:Unknown%>') + '</div>');
if (data.error) {
html.push('<div style="color:red"><%:Status%>: ' + displayError(data.error) + '</div>');
} else if (!checkedRemote) {
html.push('<div style="color:#666"><%:Status%>: <%:No Check%></div>');
} else if (data.can_upgrade) {
html.push('<div style="color:#d48806"><%:Status%>: <%:Upgrade available%></div>');
} else {
html.push('<div style="color:green"><%:Status%>: <%:Already up to date%></div>');
}
panel.innerHTML = html.join('');
if (upgradeButton) {
upgradeButton.disabled = !!data.error || !checkedRemote || !data.latest_version || !data.can_upgrade;
}
if (action) {
action.innerHTML = data.message
? '<span style="color:' + (data.success ? 'green' : 'red') + '">' + displayMessage(data.message) + '</span>'
: '';
}
}
function queryComponentInfo(button, doUpgrade) {
var component = selectedComponent();
var mirror = selectedMirror();
var action = document.getElementById('component-action');
var url = doUpgrade
? '<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "component_upgrade")%>'
: '<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "component_status")%>';
if (button) {
button.disabled = true;
button.value = doUpgrade ? '<%:Upgrading...%>' : '<%:Checking...%>';
}
if (action) {
action.innerHTML = doUpgrade ? '<%:Processing...%>' : '';
}
XHR.get(url, { component: component, mirror: mirror }, function(x, rv) {
renderComponentInfo(rv, true);
if (button) {
button.disabled = false;
button.value = doUpgrade ? '<%:Upgrade%>' : '<%:Check Update%>';
}
});
}
function queryLocalComponentInfo() {
var action = document.getElementById('component-action');
if (action) {
action.innerHTML = '';
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "component_local_status")%>', {
component: selectedComponent(),
mirror: selectedMirror()
}, function(x, rv) {
renderComponentInfo(rv, false);
});
}
function onComponentChange() {
queryLocalComponentInfo();
}
function onMirrorChange() {
var mirror = selectedMirror();
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "component_set_mirror")%>', {
mirror: mirror
}, function() {
queryLocalComponentInfo();
});
}
function selectedGeoComponent() {
var selector = document.getElementById('geo-selector');
return selector ? selector.value : 'country_mmdb';
}
function geoHelp(component) {
return geoMeta[component] ? geoMeta[component].help : '';
}
function renderGeoInfo(data) {
var panel = document.getElementById('geo-status');
var upgradeButton = document.getElementById('geo-upgrade');
var action = document.getElementById('geo-action');
var help = document.getElementById('geo-help');
var html = [];
var component = data && data.component ? data.component : selectedGeoComponent();
if (!panel) {
return;
}
if (!data) {
panel.innerHTML = '<span style="color:red"><%:Failed to query geo database information.%></span>';
if (upgradeButton) upgradeButton.disabled = true;
return;
}
if (help) {
help.innerHTML = geoHelp(component);
}
html.push('<div><strong><%:Current Version%>:</strong> ' + (data.current_version || '<%:File Not Exist%>') + '</div>');
if (data.component !== 'v2ray_geo' && data.current_version_extra) {
html.push('<div><strong><%:Additional Version%>:</strong> ' + data.current_version_extra + '</div>');
}
if (data.error) {
html.push('<div style="color:red"><%:Status%>: ' + displayError(data.error) + '</div>');
} else if (data.message) {
html.push('<div style="color:green"><%:Status%>: ' + (data.success ? displayMessage(data.message) : (data.message || '<%:Unknown%>')) + '</div>');
} else {
html.push('<div style="color:#666"><%:Status%>: <%:Ready.%></div>');
}
panel.innerHTML = html.join('');
if (upgradeButton) {
upgradeButton.disabled = false;
}
if (action) {
action.innerHTML = data.message
? '<span style="color:' + (data.success ? 'green' : 'red') + '">' + displayMessage(data.message) + '</span>'
: '';
}
}
function queryGeoInfo(button) {
var component = selectedGeoComponent();
var mirror = selectedMirror();
var action = document.getElementById('geo-action');
var url = '<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "geo_upgrade")%>';
if (button) {
button.disabled = true;
button.value = '<%:Checking...%>';
}
if (action) {
action.innerHTML = '<%:Processing...%>';
}
XHR.get(url, { component: component, mirror: mirror }, function(x, rv) {
renderGeoInfo(rv);
if (button) {
button.disabled = false;
button.value = '<%:CHECK AND UPDATE%>';
}
});
}
function queryLocalGeoInfo() {
var action = document.getElementById('geo-action');
if (action) {
action.innerHTML = '';
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "geo_local_status")%>', {
component: selectedGeoComponent(),
mirror: selectedMirror()
}, function(x, rv) {
renderGeoInfo(rv);
});
}
function onGeoChange() {
queryLocalGeoInfo();
}
window.setTimeout(function() {
onComponentChange();
onGeoChange();
}, 0);
//]]>
</script>
<style>
.component-upgrade-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 10px;
}
.component-upgrade-card {
line-height: 1.8;
}
.component-upgrade-help {
margin-top: 6px;
color: #666;
}
.component-upgrade-result {
margin-top: 8px;
}
.component-selector {
min-width: 180px;
width: 100%;
}
.component-layout {
display: flex;
gap: 24px;
align-items: flex-start;
}
.component-left {
width: 320px;
min-width: 260px;
}
.component-right {
flex: 1;
min-width: 0;
}
.component-select-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.component-select-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.component-select-label {
font-size: 12px;
line-height: 1.3;
color: #666;
}
.component-select-label strong {
display: block;
font-size: 13px;
color: #444;
}
@media (max-width: 900px) {
.component-layout {
flex-direction: column;
}
.component-left {
width: 100%;
min-width: 0;
}
}
</style>
<fieldset class="cbi-section">
<legend><%:Core Components%></legend>
<div class="cbi-value">
<div class="cbi-value-field component-upgrade-card">
<div class="component-layout">
<div class="component-left">
<div class="component-select-stack">
<div class="component-select-item">
<div class="component-select-label">
<strong><%:Download Source%></strong>
<span><%:Address%></span>
</div>
<select id="component-mirror" class="cbi-input-select component-selector" onchange="onMirrorChange()">
<option value="direct" selected="selected"><%:GitHub Direct%></option>
<option value="ghproxy">mirror.ghproxy.com</option>
<option value="ghproxy_cc">ghproxy.cc</option>
<option value="ghfast">ghfast.top</option>
<option value="jsdelivr">cdn.jsdelivr.net</option>
</select>
</div>
<div class="component-select-item">
<div class="component-select-label">
<strong><%:Component%></strong>
<span><%:Package Name%></span>
</div>
<select id="component-selector" class="cbi-input-select component-selector" onchange="onComponentChange()">
<option value="xray"><%:Xray-core%></option>
<option value="mihomo"><%:Mihomo%></option>
<option value="naiveproxy"><%:NaiveProxy%></option>
</select>
</div>
</div>
</div>
<div class="component-right">
<div id="component-status"><em><%:Collecting data...%></em></div>
<div class="component-upgrade-help" id="component-help"></div>
<div class="component-upgrade-actions">
<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Check Update%>" onclick="queryComponentInfo(this, false)" />
<input type="button" class="btn cbi-button cbi-button-apply" id="component-upgrade" value="<%:Upgrade%>" onclick="queryComponentInfo(this, true)" />
</div>
<div class="component-upgrade-result" id="component-action"></div>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="cbi-section">
<legend><%:Geo Database Update%></legend>
<div class="cbi-value">
<div class="cbi-value-field component-upgrade-card">
<div class="component-layout">
<div class="component-left">
<div class="component-select-stack">
<div class="component-select-item">
<div class="component-select-label">
<strong><%:Database%></strong>
<span><%:Geo Resource%></span>
</div>
<select id="geo-selector" class="cbi-input-select component-selector" onchange="onGeoChange()">
<option value="country_mmdb"><%:Country MMDB%></option>
<option value="geosite"><%:GeoSite Database%></option>
<option value="v2ray_geo"><%:V2Ray GEO Databases%></option>
</select>
</div>
</div>
</div>
<div class="component-right">
<div id="geo-status"><em><%:Collecting data...%></em></div>
<div class="component-upgrade-help" id="geo-help"></div>
<div class="component-upgrade-actions">
<input type="button" class="btn cbi-button cbi-button-apply" id="geo-upgrade" value="<%:CHECK AND UPDATE%>" onclick="queryGeoInfo(this)" />
</div>
<div class="component-upgrade-result" id="geo-action"></div>
</div>
</div>
</div>
</div>
</fieldset>
@@ -0,0 +1,9 @@
<style type="text/css">
[data-page="admin-services-shadowsocksr-control"] .cbi-section-node-tabbed {
min-height: 20rem;
}
[data-page="admin-services-shadowsocksr-control"] .cbi-tabcontainer[data-tab-active="true"] {
min-height: 20rem;
}
</style>
@@ -1,3 +1,17 @@
<%+cbi/valueheader%>
<span class="pingtime" hint="<%=self:cfgvalue(section)%>">-- ms</span>
<%
local hint = self:cfgvalue(section)
local cache = self.detect_cache
local is_clash = self.type == "clash"
local text = is_clash and "N/A" or "--- ms"
local color = is_clash and "#999999" or "#ff0000"
if not is_clash and cache and tonumber(cache.ping or 0) > 0 then
local ping = tonumber(cache.ping)
text = tostring(ping) .. " ms"
if ping < 300 then color = "#ff3300" end
if ping < 200 then color = "#ff7700" end
if ping < 100 then color = "#249400" end
end
%>
<span class="pingtime" hint="<%=hint%>" data-sid="<%=section%>"><font style="color:<%=color%>"><%=text%></font></span>
<%+cbi/valuefooter%>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,304 @@
<%-
local rowcnt = 0
function rowstyle()
rowcnt = rowcnt + 1
if rowcnt % 2 == 0 then
return " cbi-rowstyle-1"
else
return " cbi-rowstyle-2"
end
end
function width(o)
if o.width then
if type(o.width) == "number" then
return ' style="width:%dpx"' % o.width
end
return ' style="width:%s"' % o.width
end
return ""
end
local has_titles = false
local has_descriptions = false
local anonclass = (not self.anonymous or self.sectiontitle) and "named" or "anonymous"
local titlename = ifattr(not self.anonymous or self.sectiontitle, "data-title", translate("Name"))
local i, k
for i, k in pairs(self.children) do
if not k.typename then
k.typename = k.template and k.template:gsub("^.+/", "") or ""
end
if not has_titles and k.title and #k.title > 0 then
has_titles = true
end
if not has_descriptions and k.description and #k.description > 0 then
has_descriptions = true
end
end
local total = tonumber(self.server_total) or 0
local page = tonumber(self.server_page) or 1
local page_size = tonumber(self.server_page_size) or 0
local page_count = tonumber(self.server_page_count) or 1
local first_index = tonumber(self.server_first_index) or 0
local last_index = tonumber(self.server_last_index) or 0
local page_sizes = self.server_page_sizes or {}
local base_url = self.server_base_url or ""
local function page_url(target_page, target_size)
return string.format(
"%s?server_page=%d&server_page_size=%d",
base_url,
target_page,
target_size
)
end
function render_titles()
if not has_titles then
return
end
%><tr class="tr cbi-section-table-titles <%=anonclass%>"<%=titlename%>><%
local i, k
for i, k in ipairs(self.children) do
if not k.optional then
%><th class="th cbi-section-table-cell"<%=
width(k) .. attr("data-widget", k.typename) %>><%
if k.titleref then
%><a title="<%=self.titledesc or translate("Go to relevant configuration page")%>" class="cbi-title-ref" href="<%=k.titleref%>"><%
end
write(k.title)
if k.titleref then
%></a><%
end
%></th><%
end
end
if self.sortable or self.extedit or self.addremove then
%><th class="th cbi-section-table-cell cbi-section-actions"></th><%
end
%></tr><%
rowcnt = rowcnt + 1
end
function render_descriptions()
if not has_descriptions then
return
end
%><tr class="tr cbi-section-table-descr <%=anonclass%>"><%
local i, k
for i, k in ipairs(self.children) do
if not k.optional then
%><th class="th cbi-section-table-cell"<%=
width(k) .. attr("data-widget", k.typename) %>><%
write(k.description)
%></th><%
end
end
if self.sortable or self.extedit or self.addremove then
%><th class="th cbi-section-table-cell cbi-section-actions"></th><%
end
%></tr><%
rowcnt = rowcnt + 1
end
function render_pager()
if total == 0 then
return
end
%>
<div class="ssr-server-pager">
<span class="ssr-server-pager-summary">
<%=string.format(translate("Showing %d-%d of %d nodes"), first_index, last_index, total)%>
</span>
<span class="ssr-server-pager-pages">
<% if page_count > 1 then %>
<% if page > 1 then %>
<a class="cbi-button cbi-button-link" href="<%=page_url(page - 1, page_size)%>"><%:Prev%></a>
<% end %>
<% for current = 1, page_count do %>
<% if current == page then %>
<strong><%=current%></strong>
<% else %>
<a class="cbi-button cbi-button-link" href="<%=page_url(current, page_size)%>"><%=current%></a>
<% end %>
<% end %>
<% if page < page_count then %>
<a class="cbi-button cbi-button-link" href="<%=page_url(page + 1, page_size)%>"><%:Next%></a>
<% end %>
<% end %>
</span>
<span class="ssr-server-pager-sizes">
<%:Per page%>:
<% for _, size in ipairs(page_sizes) do %>
<%
local label = size == 0 and translate("All") or tostring(size)
local selected = size == page_size
%>
<% if selected then %>
<strong><%=label%></strong>
<% else %>
<a class="cbi-button cbi-button-link" href="<%=page_url(1, size)%>"><%=label%></a>
<% end %>
<% end %>
</span>
</div>
<input type="hidden" id="cbi-server-page" value="<%=page%>" />
<input type="hidden" id="cbi-server-page-size" value="<%=page_size%>" />
<%
end
-%>
<style type="text/css">
.ssr-server-pager {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 0.75rem;
align-items: center;
margin: 0.5rem 0 0.75rem;
}
.ssr-server-pager-pages,
.ssr-server-pager-sizes {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
}
.ssr-server-pager .cbi-button-link {
padding: 0 0.45rem;
min-width: 1.75rem;
text-align: center;
}
</style>
<!-- tblsection -->
<div class="cbi-section cbi-tblsection" id="cbi-<%=self.config%>-<%=self.sectiontype%>">
<% if self.title and #self.title > 0 then -%>
<h3><%=self.title%></h3>
<%- end %>
<%- if self.sortable then -%>
<input type="hidden" id="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" name="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" value="" />
<%- end -%>
<div class="cbi-section-descr"><%=self.description%></div>
<% render_pager() %>
<table class="table cbi-section-table">
<%-
render_titles()
render_descriptions()
local isempty, section, i, k = true, nil, nil
for i, k in ipairs(self:cfgsections()) do
isempty = false
section = k
local sectionname = striptags((type(self.sectiontitle) == "function") and self:sectiontitle(section) or k)
local sectiontitle = ifattr(sectionname and (not self.anonymous or self.sectiontitle), "data-title", sectionname, true)
local colorclass = (self.extedit or self.rowcolors) and rowstyle() or ""
local scope = {
valueheader = "cbi/cell_valueheader",
valuefooter = "cbi/cell_valuefooter"
}
-%>
<tr class="tr cbi-section-table-row<%=colorclass%>" id="cbi-<%=self.config%>-<%=section%>"<%=sectiontitle%>>
<%-
local node
for k, node in ipairs(self.children) do
if not node.optional then
node:render(section, scope or {})
end
end
-%>
<%- if self.sortable or self.extedit or self.addremove then -%>
<td class="td cbi-section-table-cell nowrap cbi-section-actions">
<div>
<% if self.extedit then -%>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>"
<%- if type(self.extedit) == "string" then
%> onclick="location.href='<%=self.extedit:format(section)%>'"
<%- elseif type(self.extedit) == "function" then
%> onclick="location.href='<%=self:extedit(section)%>'"
<%- end
%> alt="<%:Edit%>" title="<%:Edit%>" />
<% end; if self.addremove then %>
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>"
onclick="
var sid = '<%=k%>';
XHR.post('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/delete_node")%>', {
sid: sid
}, function(x, result) {
result = result || {};
if (result.ret == 1) {
window.location.reload();
return;
}
});
return false;
"
/>
<%- end -%>
</div>
</td>
<%- end -%>
</tr>
<%- end -%>
<%- if isempty then -%>
<tr class="tr cbi-section-table-row placeholder">
<td class="td"><em><%:This section contains no values yet%></em></td>
</tr>
<%- end -%>
</table>
<% render_pager() %>
<% if self.error then %>
<div class="cbi-section-error">
<ul><% for _, c in pairs(self.error) do for _, e in ipairs(c) do -%>
<li><%=pcdata(e):gsub("\n", "<br />")%></li>
<%- end end %></ul>
</div>
<% end %>
<%- if self.addremove then -%>
<% if self.template_addremove then include(self.template_addremove) else -%>
<div class="cbi-section-create cbi-tblsection-create">
<% if self.anonymous then %>
<input class="btn cbi-button cbi-button-add" type="submit" value="<%:Add%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" title="<%:Add%>" />
<% else %>
<% if self.invalid_cts then -%>
<div class="cbi-section-error"><%:Invalid%></div>
<%- end %>
<div>
<input type="text" class="cbi-section-create-name" id="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" data-type="uciname" data-optional="true" onkeyup="cbi_validate_named_section_add(this)" />
</div>
<input class="btn cbi-button cbi-button-add" type="submit" onclick="this.form.cbi_state='add-section'; return true" value="<%:Add%>" title="<%:Add%>" disabled="" />
<% end %>
</div>
<%- end %>
<%- end -%>
</div>
<!-- /tblsection -->
@@ -0,0 +1,166 @@
<style type="text/css">
#cbi-shadowsocksr-server_subscribe_item > h3 {
font-weight: 800 !important;
margin-bottom: 0.45rem !important;
}
#cbi-shadowsocksr-server_subscribe_item > .cbi-section-descr {
margin-bottom: 0.7rem !important;
}
#cbi-shadowsocksr-server_subscribe_item > .table {
table-layout: fixed !important;
width: 100% !important;
}
#cbi-shadowsocksr-server_subscribe_item .th,
#cbi-shadowsocksr-server_subscribe_item .td {
vertical-align: middle !important;
line-height: 1 !important;
}
#cbi-shadowsocksr-server_subscribe_item .table.cbi-section-table .th,
#cbi-shadowsocksr-server_subscribe_item .table.cbi-section-table .td {
padding: 4px 6px !important;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row {
text-align: left !important;
height: auto !important;
min-height: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row .td {
text-align: left !important;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row.placeholder {
display: none !important;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-value {
margin-bottom: 0;
min-height: 0 !important;
padding: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field {
width: auto !important;
}
#cbi-shadowsocksr-server_subscribe_item input[type="text"] {
width: 100%;
height: 2.1rem !important;
line-height: 1.15 !important;
font-size: 1.08rem !important;
padding-top: 0.22rem !important;
padding-bottom: 0.22rem !important;
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
min-width: 0;
box-sizing: border-box;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-value-field {
margin-left: 0;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-value label.cbi-value-title {
display: none;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="enabled"] > div {
display: flex;
justify-content: center !important;
align-items: center !important;
min-height: 1.7rem;
width: 100% !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="enabled"] [data-ui-widget*="Checkbox"] {
display: flex !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="enabled"] input[type="checkbox"] {
margin-left: 0 !important;
margin-right: 0 !important;
margin-top: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .th:nth-child(1),
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="enabled"] {
width: 4.2rem !important;
min-width: 4.2rem !important;
max-width: 4.2rem !important;
text-align: center !important;
white-space: nowrap;
padding-left: 4px !important;
padding-right: 2px !important;
}
#cbi-shadowsocksr-server_subscribe_item .th:nth-child(2),
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="alias"] {
width: 10.2rem !important;
min-width: 10.2rem !important;
max-width: 10.2rem !important;
}
#cbi-shadowsocksr-server_subscribe_item .th:nth-child(3),
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="url"] {
width: auto !important;
min-width: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .th.cbi-section-actions,
#cbi-shadowsocksr-server_subscribe_item .td.cbi-section-actions {
width: 7.4rem !important;
min-width: 7.4rem !important;
max-width: 7.4rem !important;
text-align: right !important;
white-space: nowrap;
padding-left: 0 !important;
padding-right: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-section-actions > div {
display: flex;
justify-content: flex-end;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-section-actions .btn {
height: 2.1rem !important;
line-height: 1.1 !important;
padding-top: 0.22rem !important;
padding-bottom: 0.22rem !important;
padding-left: 0.9rem !important;
padding-right: 0.9rem !important;
margin: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="url"] input[type="text"] {
font-family: "Menlo", "Monaco", "Consolas", monospace;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="alias"] .cbi-input-text,
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="url"] .cbi-input-text {
width: 100% !important;
max-width: none !important;
min-width: 0 !important;
display: block !important;
margin: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="alias"] .cbi-input-text {
font-size: 0.96rem !important;
}
@media (max-width: 900px) {
#cbi-shadowsocksr-server_subscribe_item > .table {
display: block;
overflow-x: auto;
}
}
</style>
@@ -1,9 +1,26 @@
<%+cbi/valueheader%>
<span class="socket-connected" hint="<%=self:cfgvalue(section)%>">wait</span>
<%
local hint = self:cfgvalue(section)
local cache = self.detect_cache
local text = "N/A"
local color = "#999999"
if cache then
if cache.socket then
text = "ok"
color = "#249400"
else
text = "fail"
color = "#ff0000"
end
end
%>
<span class="socket-connected" hint="<%=hint%>"><font style="color:<%=color%>"><%=text%></font></span>
<span class="transport" hint="<%=self.transport%>"></span>
<span class="wsPath" hint="<%=self.ws_path%>"></span>
<span class="wsHost" hint="<%=self.ws_host%>"></span>
<span class="tlsHost" hint="<%=self.tls_host%>"></span>
<span class="tls" hint="<%=self.tls%>"></span>
<span class="tlshost" hint="<%=self.tls_host%>"></span>
<span class="hy2type" hint="<%=self.type%>"></span>
<span class="hy2proto" hint="<%=self.v2ray_protocol%>"></span>
<span class="type" hint="<%=self.type%>"></span>
<span class="proto" hint="<%=self.proto%>"></span>
<span class="reality" hint="<%=self.reality%>"></span>
<%+cbi/valuefooter%>
@@ -1,25 +1,15 @@
<%+cbi/valueheader%>
<%
local map = self.map
local ss_type = map:get("@server_subscribe[0]", "ss_type")
local xray_hy2_type = map:get("@server_subscribe[0]", "xray_hy2_type")
local xray_tj_type = map:get("@server_subscribe[0]", "xray_tj_type")
local has_ss_rust = luci.sys.exec('type -t -p sslocal 2>/dev/null || type -t -p ssserver 2>/dev/null') ~= ""
local has_ss_libev = luci.sys.exec('type -t -p ss-redir 2>/dev/null || type -t -p ss-local 2>/dev/null') ~= ""
local has_hysteria = luci.sys.exec('type -t -p hysteria 2>/dev/null') ~= ""
local has_trojan = luci.sys.exec('type -t -p trojan 2>/dev/null') ~= ""
local has_mihomo = luci.sys.exec('type -t -p mihomo -p /usr/libexec/mihomo 2>/dev/null') ~= ""
local has_xray = luci.sys.exec('type -t -p xray 2>/dev/null') ~= ""
-%>
<script type="text/javascript">
//<![CDATA[
let ss_type = "<%=ss_type%>"
let xray_hy2_type = "<%=xray_hy2_type%>"
let xray_tj_type = "<%=xray_tj_type%>"
let has_ss_rust = "<%=has_ss_rust%>"
let has_ss_libev = "<%=has_ss_libev%>"
let has_hysteria = "<%=has_hysteria%>"
let has_trojan = "<%=has_trojan%>"
let has_mihomo = "<%=has_mihomo%>"
let has_xray = "<%=has_xray%>"
function padright(str, cnt, pad) {
@@ -42,6 +32,13 @@ function b64DecodeUnicode(str) {
}).join(''));
}
function normalizeHost(host) {
if (host && host[0] === "[" && host[host.length - 1] === "]") {
return host.slice(1, -1);
}
return host;
}
function b64decutf8safe(str) {
var l;
str = str.replace(/-/g, "+").replace(/_/g, "/");
@@ -125,38 +122,9 @@ function import_ssr_url(btn, urlname, sid) {
return false;
}
// 普通 Hysteria2 导入函数
function importAsNormalHy2() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
if (params.get("protocol") && params.get("protocol").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_transport')[0].checked = true; // 设置 flag_transport 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_transport')[0].dispatchEvent(event); // 触发事件
document.getElementsByName('cbid.shadowsocksr.' + sid + '.transport_protocol')[0].value = params.get("protocol") || "udp";
}
if (params.get("lazy") === "1") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].checked = true;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].dispatchEvent(event);
}
if (params.get("sni") || params.get("alpn")) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; // 设置 tls 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event); // 触发事件
if (params.get("sni")) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = params.get("sni") || "";
}
if (params.get("alpn") && params.get("alpn").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_alpn')[0].value = params.get("alpn") || "";
}
}
if (params.get("pinSHA256") && params.get("pinSHA256").trim() !== "") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.pinsha256')[0].value = params.get("pinSHA256") || "";
}
}
// Xray Hysteria2 导入函数
function importAsXrayHy2() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray"
// Xray Hysteria2 导入函数
function importAsXrayHy2() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray"
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].dispatchEvent(event);
@@ -184,36 +152,14 @@ function import_ssr_url(btn, urlname, sid) {
}
}
// 主逻辑:智能核心选择(支持回退)
function hasCore(core) {
if (core === "hysteria") return has_hysteria === "true";
if (core === "xray") return has_xray === "true";
return false;
}
let finalMode = null; // true=Xray, false=普通, null=无核心
if (xray_hy2_type === "v2ray") {
if (hasCore("xray")) finalMode = true;
else if (hasCore("hysteria")) finalMode = false;
else finalMode = null;
} else if (xray_hy2_type === "hysteria2") {
if (hasCore("hysteria")) finalMode = false;
else if (hasCore("xray")) finalMode = true;
else finalMode = null;
} else {
// auto 或空:Hysteria2 无 type 参数,优先普通 Hysteria2
if (hasCore("hysteria")) finalMode = false;
else if (hasCore("xray")) finalMode = true;
else finalMode = null;
}
if (finalMode === null) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Hysteria2 or Xray) to import this node.%></font>";
return false;
}
if (finalMode === true) importAsXrayHy2();
else importAsNormalHy2();
if (has_xray !== "true") {
s.innerHTML = "<font style=\'color:red\'><%:No available Xray core to import this Hysteria2 node.%></font>";
return false;
}
importAsXrayHy2();
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = normalizeHost(url.hostname);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port || "443";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].value = decodeURIComponent(url.username);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].dispatchEvent(event);
@@ -248,9 +194,9 @@ function import_ssr_url(btn, urlname, sid) {
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return false;
case "ss":
var url0 = (ssu[1] || "");
var param = "";
case "ss":
var url0 = (ssu[1] || "");
var param = "";
// 先分离 #(alias)
var hashIndex = url0.indexOf("#");
@@ -267,374 +213,346 @@ function import_ssr_url(btn, urlname, sid) {
url0 = url0.substring(0, queryIndex);
}
var params = Object.fromEntries(new URLSearchParams(query));
var params = Object.fromEntries(new URLSearchParams(query));
function importAsNormalSS() {
// 普通 SS 导入逻辑
// 判断是否 SIP002 格式(即含 @)
if (url0.indexOf("@") !== -1) {
// === SIP002 格式 ===
var sipIndex = url0.indexOf("@");
// 先 URL 解码 base64 再解码
var userInfoB64 = decodeURIComponent(url0.substring(0, sipIndex));
// console.log("userInfoB64:", userInfoB64);
var userInfo = b64decsafe(userInfoB64);
if (userInfo && userInfo.indexOf(":") !== -1) {
var userInfo = userInfo;
} else {
var userInfo = userInfoB64;
function decodeURIComponentSafe(value) {
try {
return decodeURIComponent(value);
} catch (e) {
return value;
}
// 如果没有冒号,且不是 base64 格式,我们可以尝试补齐它
if (userInfo.indexOf(":") === -1) {
userInfo = "none:" + userInfo;
}
// console.log("userInfo after decode:", userInfo);
var userInfoSplitIndex = userInfo.indexOf(":");
if(userInfoSplitIndex < 0) {
// 格式错误
s.innerHTML = "<font style='color:red'><%:Userinfo format error.%></font>";
return false;
}
var method = userInfo.substring(0, userInfoSplitIndex);
// console.log("method:", method);
var password = userInfo.substring(userInfoSplitIndex + 1);
// console.log("password:", password);
var serverPart = url0.substring(sipIndex + 1);
var serverInfo = serverPart.split(":");
}
var server = serverInfo[0];
var port = serverInfo[1];
function decodeBase64Safe(value) {
try {
return b64decsafe(value);
} catch (e) {
return "";
}
}
function parseSSHostPort(serverPart) {
serverPart = (serverPart || "").trim();
var ipv6Match = serverPart.match(/^\[([^\]]+)\]:(\d+)$/);
if (ipv6Match) {
return { server: ipv6Match[1], port: ipv6Match[2] };
}
var colonIndex = serverPart.lastIndexOf(":");
if (colonIndex > 0 && serverPart.indexOf(":") === colonIndex) {
return {
server: serverPart.substring(0, colonIndex),
port: serverPart.substring(colonIndex + 1)
};
}
return null;
}
function parseSSPluginInfo() {
var plugin = "";
var pluginOpts = "";
var plugin = "", pluginOpts = "";
if (params.plugin) {
var pluginParams = decodeURIComponent(params.plugin).split(";");
plugin = pluginParams.shift();
pluginOpts = pluginParams.join(";");
}
} else {
// === Base64 SS2022 / 普通格式 的整体编码格式 ===
// 先 URL 解码整个字符串
var decodedUrl0 = decodeURIComponent(url0);
var sstr = b64decsafe(decodedUrl0);
if (!sstr) {
s.innerHTML = "<font style='color:red'><%:Base64 sstr failed.%></font>";
return false;
}
// 支持 SS2022 / 普通格式
var regex2022 = /^([^:]+):([^:]+):([^@]+)@([^:]+):(\d+)$/;
var regexNormal = /^([^:]+):([^@]+)@([^:]+):(\d+)$/;
var m2022 = sstr.match(regex2022);
var mNormal = sstr.match(regexNormal);
if (m2022) {
var method = m2022[1];
var password = m2022[2] + ":" + m2022[3];
var server = m2022[4];
var port = m2022[5];
} else if (mNormal) {
var method = mNormal[1];
var password = mNormal[2];
var server = mNormal[3];
var port = mNormal[4];
} else {
s.innerHTML = "<font style='color:red'><%:SS URL base64 sstr format not recognized.%></font>";
return false;
}
var plugin = "", pluginOpts = "";
if (params["shadow-tls"]) {
var pluginInfo = decodeURIComponentSafe(params.plugin);
var pluginParts = pluginInfo.split(";");
plugin = pluginParts.shift() || "";
pluginOpts = pluginParts.join(";");
if (plugin === "simple-obfs") {
plugin = "obfs-local";
}
} else if (params["shadow-tls"]) {
try {
var decoded_tls = JSON.parse(atob(decodeURIComponent(params["shadow-tls"])));
var decodedTls = JSON.parse(b64decsafe(decodeURIComponentSafe((params["shadow-tls"] || "").replace(/ /g, "+"))));
plugin = "shadow-tls";
var versionFlag = "";
if (decoded_tls.version && !isNaN(decoded_tls.version)) {
versionFlag = "v" + decoded_tls.version + "=1;";
if (decodedTls.version && !isNaN(decodedTls.version)) {
versionFlag = "v" + decodedTls.version + "=1;";
}
pluginOpts = versionFlag + "host=" + (decoded_tls.host || "") + ";passwd=" + (decoded_tls.password || "");
pluginOpts = versionFlag + "host=" + (decodedTls.host || "") + ";passwd=" + (decodedTls.password || "");
} catch (e) {
console.log("shadow-tls decode failed:", e);
}
}
return {
plugin: plugin,
pluginOpts: pluginOpts
};
}
// 判断密码是否经过url编码
const isURLEncodedPassword = function(pwd) {
if (!/%[0-9A-Fa-f]{2}/.test(pwd)) return false;
try {
const decoded = decodeURIComponent(pwd.replace(/\+/g, "%20"));
const reencoded = encodeURIComponent(decoded);
return reencoded === pwd;
} catch (e) {
return false;
function parseSSNode() {
var userInfo = "";
var serverPart = "";
var atIndex = url0.lastIndexOf("@");
if (atIndex !== -1) {
var rawUserInfo = decodeURIComponentSafe(url0.substring(0, atIndex));
var decodedUserInfo = decodeBase64Safe(rawUserInfo);
userInfo = decodedUserInfo && decodedUserInfo.indexOf(":") !== -1 ? decodedUserInfo : rawUserInfo;
serverPart = url0.substring(atIndex + 1);
} else {
var decodedUrl0 = decodeBase64Safe(decodeURIComponentSafe(url0));
if (!decodedUrl0) {
return { error: "<%:Base64 sstr failed.%>" };
}
atIndex = decodedUrl0.lastIndexOf("@");
if (atIndex === -1) {
return { error: "<%:SS URL base64 sstr format not recognized.%>" };
}
userInfo = decodedUrl0.substring(0, atIndex);
serverPart = decodedUrl0.substring(atIndex + 1);
}
}
if (isURLEncodedPassword(password)) {
password = decodeURIComponent(password); // 解码URL编码
} else {
password = password; // 保持原始值
}
// === 填充配置项 ===
//var has_ss_type = (ss_type === "ss-rust") ? "ss-rust" : "ss-libev";
// 设置 _ss_core 的值
var ssCore = (ss_type === "ss-rust" || ss_type === "ss-libev")
? ss_type
: (has_ss_rust ? "ss-rust" : (has_ss_libev ? "ss-libev" : "ss-libev"));
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = ssu[0];
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '._ss_core')[0].value = ssCore;
document.getElementsByName('cbid.shadowsocksr.' + sid + '._ss_core')[0].dispatchEvent(event);
//document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].value = has_ss_type;
//document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = server;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = port;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.password')[0].value = password || "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.encrypt_method_ss')[0].value = method;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.encrypt_method_ss')[0].dispatchEvent(event);
if (plugin && plugin !== "none") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].checked = true; // 设置 enable_plugin 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].dispatchEvent(event); // 触发事件
document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin')[0].value = plugin;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin')[0].dispatchEvent(event);
if (plugin !== undefined) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin_opts')[0].value = pluginOpts || "";
if (userInfo.indexOf(":") === -1) {
userInfo = "none:" + userInfo;
}
} else {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].checked = false;
}
//if (params.udp === "1") {
// document.getElementsByName('cbid.shadowsocksr.' + sid + '.uot')[0].checked = true; // 设置 uot 为 true
// document.getElementsByName('cbid.shadowsocksr.' + sid + '.uot')[0].dispatchEvent(event); // 触发事件
//}
var userInfoSplitIndex = userInfo.indexOf(":");
if (userInfoSplitIndex < 0) {
return { error: "<%:Userinfo format error.%>" };
}
if (params.tfo === "1") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].checked = true; // 设置 fast_open 为 true
document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].dispatchEvent(event); // 触发事件
}
var method = userInfo.substring(0, userInfoSplitIndex);
var password = userInfo.substring(userInfoSplitIndex + 1);
var serverInfo = parseSSHostPort(serverPart);
if (!serverInfo) {
return { error: "SS server address format error." };
}
if (param != undefined) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = decodeURIComponent(param);
}
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return true;
}
function importAsXraySS() {
try {
// Xray SS 导入逻辑
// 拆分 @,判断是否是 base64 userinfo 的格式
var parts = url0.split("@");
if (parts.length > 1) {
// @ 前是 base64(method:password),后面是 server:port?params
var userinfo = b64decsafe(parts[0]);
var sepIndex = userinfo.indexOf(":");
if (sepIndex > -1) {
method = userinfo.slice(0, sepIndex);
password = userinfo.slice(sepIndex + 1); //一些链接用明文uuid做密码
const isURLEncodedPassword = function(pwd) {
if (!/%[0-9A-Fa-f]{2}/.test(pwd)) return false;
try {
const decoded = decodeURIComponent(pwd.replace(/\+/g, "%20"));
const reencoded = encodeURIComponent(decoded);
return reencoded === pwd;
} catch (e) {
return false;
}
}
// 已编码的别名
// var url = new URL("http://" + url0 + (param ? "#" + encodeURIComponent(param) : ""));
// 未编码的别名
// var url = new URL("http://" + url0 + (param ? "#" + param : ""));
// 兼容已编码和未编码的别名
var remarks = param ? decodeURIComponent(param) : "";
var encodedremarks = encodeURIComponent(remarks);
var url = new URL("http://" + url0 + (encodedremarks ? "#" + encodedremarks : ""));
if (isURLEncodedPassword(password)) {
password = decodeURIComponent(password);
}
} catch(e) {
alert(e);
var pluginInfo = parseSSPluginInfo();
return {
method: method,
password: password,
server: normalizeHost(serverInfo.server),
port: serverInfo.port,
plugin: pluginInfo.plugin,
pluginOpts: pluginInfo.pluginOpts,
alias: param != undefined ? decodeURIComponentSafe(param) : ""
};
}
function importAsNormalSS(ssNode) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = has_mihomo === "true" ? "ss" : "ss-rust";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = ssNode.server;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = ssNode.port;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.password')[0].value = ssNode.password || "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.encrypt_method_ss')[0].value = ssNode.method;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.encrypt_method_ss')[0].dispatchEvent(event);
if (ssNode.plugin && ssNode.plugin !== "none") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].checked = true;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin')[0].value = ssNode.plugin;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin_opts')[0].value = ssNode.pluginOpts || "";
} else {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].checked = false;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].dispatchEvent(event);
}
if (params.tfo === "1") {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].checked = true;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.fast_open')[0].dispatchEvent(event);
}
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = ssNode.alias;
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return true;
}
function importAsXraySS(ssNode) {
function setElementValue(name, value) {
const element = document.getElementsByName(name)[0];
if (element) {
if (typeof value === 'boolean') {
element.checked = value;
} else {
element.value = value;
}
}
}
function dispatchEventIfExists(name, event) {
const element = document.getElementsByName(name)[0];
if (element) {
element.dispatchEvent(event);
}
}
setElementValue('cbid.shadowsocksr.' + sid + '.alias', ssNode.alias);
setElementValue('cbid.shadowsocksr.' + sid + '.type', "v2ray");
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.type', event);
setElementValue('cbid.shadowsocksr.' + sid + '.v2ray_protocol', "shadowsocks");
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.v2ray_protocol', event);
setElementValue('cbid.shadowsocksr.' + sid + '.server', ssNode.server);
setElementValue('cbid.shadowsocksr.' + sid + '.server_port', ssNode.port || "80");
setElementValue('cbid.shadowsocksr.' + sid + '.password', ssNode.password || "");
setElementValue('cbid.shadowsocksr.' + sid + '.transport',
params.type === "http" ? "h2" :
(["xhttp", "splithttp"].includes(params.type) ? "xhttp" :
(["tcp", "raw"].includes(params.type) ? "raw" :
(params.type || "raw")))
);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.transport', event);
setElementValue('cbid.shadowsocksr.' + sid + '.encrypt_method_ss', ssNode.method || params.encryption || "none");
if (params.udp === "1") {
setElementValue('cbid.shadowsocksr.' + sid + '.uot', true);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.uot', event);
}
if (params.fm && params.fm.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.enable_finalmask', true);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_finalmask', event);
setElementValue('cbid.shadowsocksr.' + sid + '.finalmask', params.fm || "");
}
if (params.tfo === "1") {
setElementValue('cbid.shadowsocksr.' + sid + '.fast_open', true);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.fast_open', event);
}
if ([ "tls", "xtls", "reality" ].includes(params.security)) {
setElementValue('cbid.shadowsocksr.' + sid + '.' + params.security, true);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.' + params.security, event);
if (params.security === "tls") {
if (params.ech && params.ech.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.enable_ech', true);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_ech', event);
setElementValue('cbid.shadowsocksr.' + sid + '.ech_config', params.ech || "");
}
var allowInsecureValue = params.allowInsecure || params.insecure;
if (allowInsecureValue === "1" || allowInsecureValue === "true") {
setElementValue('cbid.shadowsocksr.' + sid + '.insecure', true);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.insecure', event);
}
if (params.pcs && params.pcs.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.tls_CertSha', params.pcs);
}
if (params.vcn && params.vcn.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.tls_CertByName', params.vcn);
}
}
if (params.security === "reality") {
setElementValue('cbid.shadowsocksr.' + sid + '.reality_publickey', params.pbk ? decodeURIComponentSafe(params.pbk) : "");
setElementValue('cbid.shadowsocksr.' + sid + '.reality_shortid', params.sid || "");
setElementValue('cbid.shadowsocksr.' + sid + '.reality_spiderx', params.spx ? decodeURIComponentSafe(params.spx) : "");
if (params.pqv && params.pqv.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.enable_mldsa65verify', true);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_mldsa65verify', event);
setElementValue('cbid.shadowsocksr.' + sid + '.reality_mldsa65verify', params.pqv || "");
}
}
setElementValue('cbid.shadowsocksr.' + sid + '.tls_flow', params.flow || "none");
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.tls_flow', event);
setElementValue('cbid.shadowsocksr.' + sid + '.tls_alpn', params.alpn || "");
setElementValue('cbid.shadowsocksr.' + sid + '.fingerprint', params.fp || "");
setElementValue('cbid.shadowsocksr.' + sid + '.tls_host', params.sni || "");
}
switch (params.type) {
case "ws":
if (params.security !== "tls") {
setElementValue('cbid.shadowsocksr.' + sid + '.ws_host', params.host ? decodeURIComponentSafe(params.host) : "");
}
setElementValue('cbid.shadowsocksr.' + sid + '.ws_path', params.path ? decodeURIComponentSafe(params.path) : "/");
break;
case "httpupgrade":
if (params.security !== "tls") {
setElementValue('cbid.shadowsocksr.' + sid + '.httpupgrade_host', params.host ? decodeURIComponentSafe(params.host) : "");
}
setElementValue('cbid.shadowsocksr.' + sid + '.httpupgrade_path', params.path ? decodeURIComponentSafe(params.path) : "/");
break;
case "xhttp":
case "splithttp":
setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_mode', params.mode || "auto");
setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_host', params.host ? decodeURIComponentSafe(params.host) : "");
setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_path', params.path ? decodeURIComponentSafe(params.path) : "/");
if (params.extra && params.extra.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra', true);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra', event);
setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_extra', params.extra || "");
}
break;
case "kcp":
setElementValue('cbid.shadowsocksr.' + sid + '.kcp_guise', params.headerType || "none");
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.kcp_guise', event);
setElementValue('cbid.shadowsocksr.' + sid + '.kcp_domain', params.host || "");
setElementValue('cbid.shadowsocksr.' + sid + '.seed', params.seed || "");
break;
case "http":
case "h2":
setElementValue('cbid.shadowsocksr.' + sid + '.h2_host', params.host ? decodeURIComponentSafe(params.host) : "");
setElementValue('cbid.shadowsocksr.' + sid + '.h2_path', params.path ? decodeURIComponentSafe(params.path) : "");
break;
case "quic":
setElementValue('cbid.shadowsocksr.' + sid + '.quic_guise', params.headerType || "none");
setElementValue('cbid.shadowsocksr.' + sid + '.quic_security', params.quicSecurity || "none");
setElementValue('cbid.shadowsocksr.' + sid + '.quic_key', params.key || "");
break;
case "grpc":
setElementValue('cbid.shadowsocksr.' + sid + '.serviceName', params.serviceName || "");
setElementValue('cbid.shadowsocksr.' + sid + '.grpc_mode', params.mode || "gun");
break;
case "tcp":
case "raw":
setElementValue('cbid.shadowsocksr.' + sid + '.tcp_guise', params.headerType || "none");
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.tcp_guise', event);
if (params.headerType === "http") {
setElementValue('cbid.shadowsocksr.' + sid + '.http_host', params.host ? decodeURIComponentSafe(params.host) : "");
setElementValue('cbid.shadowsocksr.' + sid + '.http_path', params.path ? decodeURIComponentSafe(params.path) : "");
}
break;
}
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return true;
}
function preferredSSBackend() {
if (has_mihomo === "true") return "ss";
if (has_ss_rust === "true") return "ss-rust";
if (has_xray === "true") return "v2ray";
return null;
}
var ssNode = parseSSNode();
if (ssNode.error) {
s.innerHTML = "<font style='color:red'>" + ssNode.error + "</font>";
return false;
}
// Check if the elements exist before trying to modify them
function setElementValue(name, value) {
const element = document.getElementsByName(name)[0];
if (element) {
if (typeof value === 'boolean') {
element.checked = value;
} else {
element.value = value;
}
}
}
function dispatchEventIfExists(name, event) {
const element = document.getElementsByName(name)[0];
if (element) {
element.dispatchEvent(event);
}
}
setElementValue('cbid.shadowsocksr.' + sid + '.alias', url.hash ? decodeURIComponent(url.hash.slice(1)) : "");
setElementValue('cbid.shadowsocksr.' + sid + '.type', "v2ray");
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.type', event);
setElementValue('cbid.shadowsocksr.' + sid + '.v2ray_protocol', "shadowsocks");
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.v2ray_protocol', event);
setElementValue('cbid.shadowsocksr.' + sid + '.server', url.hostname);
setElementValue('cbid.shadowsocksr.' + sid + '.server_port', url.port || "80");
setElementValue('cbid.shadowsocksr.' + sid + '.password', password || url.username);
setElementValue('cbid.shadowsocksr.' + sid + '.transport',
params.type === "http" ? "h2" :
(["xhttp", "splithttp"].includes(params.type) ? "xhttp" :
(["tcp", "raw"].includes(params.type) ? "raw" :
(params.type || "raw")))
);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.transport', event);
setElementValue('cbid.shadowsocksr.' + sid + '.encrypt_method_ss', method || params.encryption || "none");
if (params.udp === "1") {
setElementValue('cbid.shadowsocksr.' + sid + '.uot', true); // 设置 uot 为 true
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.uot', event); // 触发事件
}
if (params.fm && params.fm.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.enable_finalmask', true); // 设置 enable_finalmask 为 true
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_finalmask', event); // 触发事件
setElementValue('cbid.shadowsocksr.' + sid + '.finalmask', params.fm || "");
}
if (params.tfo === "1") {
setElementValue('cbid.shadowsocksr.' + sid + '.fast_open', true); // 设置 fast_open 为 true
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.fast_open', event); // 触发事件
}
if ([ "tls", "xtls", "reality" ].includes(params.security)) {
setElementValue('cbid.shadowsocksr.' + sid + '.' + params.security, true);
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.' + params.security, event);
if (params.security === "tls") {
if (params.ech && params.ech.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.enable_ech', true); // 设置 enable_ech 为 true
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_ech', event); // 触发事件
setElementValue('cbid.shadowsocksr.' + sid + '.ech_config', params.ech || "");
}
var allowInsecureValue = params.allowInsecure || params.insecure;
if (allowInsecureValue === "1" || allowInsecureValue === "true") {
setElementValue('cbid.shadowsocksr.' + sid + '.insecure', true); // 设置 insecure 为 true
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.insecure', event); // 触发事件
}
if (params.pcs && params.pcs.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.tls_CertSha', params.pcs);
}
if (params.vcn && params.vcn.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.tls_CertByName', params.vcn);
}
}
if (params.security === "reality") {
setElementValue('cbid.shadowsocksr.' + sid + '.reality_publickey', params.pbk ? decodeURIComponent(params.pbk) : "");
setElementValue('cbid.shadowsocksr.' + sid + '.reality_shortid', params.sid || "");
setElementValue('cbid.shadowsocksr.' + sid + '.reality_spiderx', params.spx ? decodeURIComponent(params.spx) : "");
if (params.pqv && params.pqv.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.enable_mldsa65verify', true); // 设置 enable_mldsa65verify 为 true
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_mldsa65verify', event); // 触发事件
setElementValue('cbid.shadowsocksr.' + sid + '.reality_mldsa65verify', params.pqv || "");
}
}
setElementValue('cbid.shadowsocksr.' + sid + '.tls_flow', params.flow || "none");
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.tls_flow', event);
setElementValue('cbid.shadowsocksr.' + sid + '.tls_alpn', params.alpn || "");
setElementValue('cbid.shadowsocksr.' + sid + '.fingerprint', params.fp || "");
setElementValue('cbid.shadowsocksr.' + sid + '.tls_host', params.sni || "");
}
switch (params.type) {
case "ws":
if (params.security !== "tls") {
setElementValue('cbid.shadowsocksr.' + sid + '.ws_host', params.host ? decodeURIComponent(params.host) : "");
}
setElementValue('cbid.shadowsocksr.' + sid + '.ws_path', params.path ? decodeURIComponent(params.path) : "/");
break;
case "httpupgrade":
if (params.security !== "tls") {
setElementValue('cbid.shadowsocksr.' + sid + '.httpupgrade_host', params.host ? decodeURIComponent(params.host) : "");
}
setElementValue('cbid.shadowsocksr.' + sid + '.httpupgrade_path', params.path ? decodeURIComponent(params.path) : "/");
break;
case "xhttp":
case "splithttp":
setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_mode', params.mode || "auto");
setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_host', params.host ? decodeURIComponent(params.host) : "");
setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_path', params.path ? decodeURIComponent(params.path) : "/");
if (params.extra && params.extra.trim() !== "") {
setElementValue('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra', true); // 设置 enable_xhttp_extra 为 true
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra', event); // 触发事件
setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_extra', params.extra || "");
}
break;
case "kcp":
setElementValue('cbid.shadowsocksr.' + sid + '.kcp_guise', params.headerType || "none");
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.kcp_guise', event);
setElementValue('cbid.shadowsocksr.' + sid + '.kcp_domain', params.host || "");
setElementValue('cbid.shadowsocksr.' + sid + '.seed', params.seed || "");
break;
case "http":
/* this is non-standard, bullshit */
case "h2":
setElementValue('cbid.shadowsocksr.' + sid + '.h2_host', params.host ? decodeURIComponent(params.host) : "");
setElementValue('cbid.shadowsocksr.' + sid + '.h2_path', params.path ? decodeURIComponent(params.path) : "");
break;
case "quic":
setElementValue('cbid.shadowsocksr.' + sid + '.quic_guise', params.headerType || "none");
setElementValue('cbid.shadowsocksr.' + sid + '.quic_security', params.quicSecurity || "none");
setElementValue('cbid.shadowsocksr.' + sid + '.quic_key', params.key || "");
break;
case "grpc":
setElementValue('cbid.shadowsocksr.' + sid + '.serviceName', params.serviceName || "");
setElementValue('cbid.shadowsocksr.' + sid + '.grpc_mode', params.mode || "gun");
break;
case "tcp":
case "raw":
setElementValue('cbid.shadowsocksr.' + sid + '.tcp_guise', params.headerType || "none");
dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.tcp_guise', event);
if (params.headerType === "http") {
setElementValue('cbid.shadowsocksr.' + sid + '.http_host', params.host ? decodeURIComponent(params.host) : "");
setElementValue('cbid.shadowsocksr.' + sid + '.http_path', params.path ? decodeURIComponent(params.path) : "");
}
break;
}
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return true;
}
// 主逻辑:智能核心选择(支持回退)
function hasSSCore() { return has_ss_rust === "true" || has_ss_libev === "true"; }
function hasXrayCore() { return has_xray === "true"; }
let finalSSType = null; // true=Xray, false=普通SS, null=无核心
if (ss_type === "v2ray") {
if (hasXrayCore()) finalSSType = true;
else if (hasSSCore()) finalSSType = false;
else finalSSType = null;
} else if (ss_type === "ss-rust" || ss_type === "ss-libev") {
let userSSCore = (ss_type === "ss-rust" && has_ss_rust === "true") || (ss_type === "ss-libev" && has_ss_libev === "true");
if (userSSCore) finalSSType = false;
else {
let otherSSCore = (ss_type === "ss-rust" && has_ss_libev === "true") || (ss_type === "ss-libev" && has_ss_rust === "true");
if (otherSSCore) finalSSType = false;
else if (hasXrayCore()) finalSSType = true;
else finalSSType = null;
}
} else {
// auto 或空:根据 type 参数决定
let hasType = params.type && params.type !== "";
if (hasType) {
if (hasXrayCore()) finalSSType = true;
else if (hasSSCore()) finalSSType = false;
else finalSSType = null;
let finalSSBackend;
const finalXray = (params.security && params.security !== "none") || // TLS/XTLS/Reality
(params.type && ["ws", "httpupgrade", "xhttp", "splithttp", "grpc", "quic", "kcp", "http", "h2"].includes(params.type));
if (finalXray && has_xray === "true") {
finalSSBackend = "v2ray";
} else {
if (hasSSCore()) finalSSType = false;
else if (hasXrayCore()) finalSSType = true;
else finalSSType = null;
finalSSBackend = preferredSSBackend();
}
}
if (finalSSType === null) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Shadowsocks or Xray) to import this node.%></font>";
if (finalSSBackend === null) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Shadowsocks or Xray) to import this node.%></font>";
return false;
}
if (finalSSBackend === "v2ray" && ssNode.plugin && ssNode.plugin !== "none") {
s.innerHTML = "<font style='color:red'>Xray fallback does not support importing Shadowsocks links with SIP003 plugins.</font>";
return false;
}
if (finalSSBackend === "v2ray") importAsXraySS(ssNode);
else importAsNormalSS(ssNode);
return false;
}
if (finalSSType === true) importAsXraySS();
else importAsNormalSS();
return false;
case "ssr":
var sstr = b64decsafe((ssu[1] || "").replace(/#.*/, "").trim());
var ploc = sstr.indexOf("/?");
@@ -685,7 +603,7 @@ function import_ssr_url(btn, urlname, sid) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "trojan";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = normalizeHost(url.hostname);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port || "80";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.password')[0].value = decodeURIComponent(url.username);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true;
@@ -714,7 +632,7 @@ function import_ssr_url(btn, urlname, sid) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = "trojan";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = normalizeHost(url.hostname);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port || "80";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.password')[0].value = decodeURIComponent(url.username);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true;
@@ -814,36 +732,13 @@ function import_ssr_url(btn, urlname, sid) {
return true;
}
// 主逻辑:智能核心选择(支持回退)
function hasTrojanCore() { return has_trojan === "true"; }
// 主逻辑:Trojan 统一使用 Xray
function hasXrayCore() { return has_xray === "true"; }
let finalTrojanMode = null;
if (xray_tj_type === "v2ray") {
if (hasXrayCore()) finalTrojanMode = true;
else if (hasTrojanCore()) finalTrojanMode = false;
else finalTrojanMode = null;
} else if (xray_tj_type === "trojan") {
if (hasTrojanCore()) finalTrojanMode = false;
else if (hasXrayCore()) finalTrojanMode = true;
else finalTrojanMode = null;
} else {
let hasType = params.get("type") && params.get("type") !== "";
if (hasType) {
if (hasXrayCore()) finalTrojanMode = true;
else if (hasTrojanCore()) finalTrojanMode = false;
else finalTrojanMode = null;
} else {
if (hasTrojanCore()) finalTrojanMode = false;
else if (hasXrayCore()) finalTrojanMode = true;
else finalTrojanMode = null;
}
}
if (finalTrojanMode === null) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Trojan or Xray) to import this node.%></font>";
if (!hasXrayCore()) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Xray) to import this node.%></font>";
return false;
}
if (finalTrojanMode === true) importAsXrayTrojan();
else importAsNormalTrojan();
importAsXrayTrojan();
return false;
case "vmess":
var sstr = b64DecodeUnicode((ssu[1] || "").replace(/#.*/, "").trim());
@@ -976,7 +871,7 @@ function import_ssr_url(btn, urlname, sid) {
element.dispatchEvent(event);
}
}
var server = url.hostname;
var server = normalizeHost(url.hostname);
/* 如果带 [],再去掉 [] */
if (server[0] === "[" && server[server.length - 1] === "]") {
server = server.slice(1, -1);
@@ -1141,7 +1036,7 @@ function import_ssr_url(btn, urlname, sid) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = ssu[0];
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
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 + '.tuic_uuid')[0].value = method;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tuic_ip')[0].value = params.ip || "";
@@ -4,7 +4,8 @@ XHR.poll(3, '<%=url([[admin]], [[services]], [[shadowsocksr]], [[run]])%>', null
var tb = document.getElementById('shadowsocksr_status');
if (data && tb) {
if (data.running) {
var links = '<em><b style=color:green>ShadowsocksR Plus+ <%:RUNNING%></b></em>';
var text = data.status_text || '<%:RUNNING%>';
var links = '<em><b style=color:green>ShadowsocksR Plus+ ' + text + '</b></em>';
tb.innerHTML = links;
} else {
tb.innerHTML = '<em><b style=color:red>ShadowsocksR Plus+ <%:NOT RUNNING%></b></em>';
@@ -1,18 +1,257 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function subscribe(btn,dataname) {
btn.disabled = true;
btn.value = '<%:Refresh...%> ';
murl=dataname;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr","subscribe")%>', { set:murl }, function(x,rv) {
// 先简单刷新,后期如果重构会考虑下如何组织lua shell JavaScript之间的代码逻辑和各自的调用逻辑
window.location.reload()
// btn.disabled = false;
// btn.value = '<%:Refresh Data %>';
});
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Update All Subscribe Servers%> " onclick="return subscribe(this,'<%=self.option%>')" />
<!-- <span id="<%=self.option%>-status"><em><%=self.value%></em></span> -->
<%
local dsp = require "luci.dispatcher"
local subscribe_url = dsp.build_url("admin", "services", "shadowsocksr", "subscribe")
local get_log_url = dsp.build_url("admin", "services", "shadowsocksr", "get_log")
local servers_url = dsp.build_url("admin", "services", "shadowsocksr", "servers")
local autostart = self.map and self.map.ssr_subscribe_autostart
-%>
<div id="ssr-subscribe-modal-wrap">
<style type="text/css">
#ssr-subscribe-modal-mask {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
z-index: 2000;
}
#ssr-subscribe-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(900px, calc(100vw - 32px));
max-height: min(720px, calc(100vh - 80px));
overflow: auto;
background: #fff;
border-radius: 10px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
z-index: 2001;
padding: 12px 16px 14px;
}
#ssr-subscribe-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
#ssr-subscribe-modal-title {
font-size: 17px;
font-weight: 600;
color: #333;
}
#ssr-subscribe-modal-status {
margin: 0 0 8px;
color: #666;
}
#ssr-subscribe-modal-status.ssr-subscribe-highlight {
font-size: 18px;
font-weight: 700;
line-height: 1.5;
}
#ssr-subscribe-log {
margin: 0;
padding: 14px 16px;
min-height: 420px;
max-height: min(520px, calc(100vh - 220px));
overflow: auto;
background: #111827;
color: #e5e7eb;
border-radius: 8px;
box-sizing: border-box;
white-space: pre-wrap;
word-break: break-word;
font: 13px/1.5 Menlo, Monaco, Consolas, monospace;
}
@media (max-width: 700px) {
#ssr-subscribe-modal {
width: calc(100vw - 16px);
max-height: calc(100vh - 24px);
padding: 10px 12px 12px;
}
#ssr-subscribe-modal-header {
flex-direction: column;
align-items: flex-start;
}
#ssr-subscribe-log {
max-height: calc(100vh - 260px);
min-height: 280px;
}
}
</style>
<script type="text/javascript">//<![CDATA[
(function() {
var pollTimer = null;
var running = false;
var autoStart = <%=autostart and "true" or "false"%>;
function getEl(id) {
return document.getElementById(id);
}
function setStatus(text, color) {
var status = getEl('ssr-subscribe-modal-status');
if (!status) {
return;
}
status.className = 'ssr-subscribe-highlight';
status.style.color = color || '#666';
status.textContent = text;
}
function updateLog(text) {
var log = getEl('ssr-subscribe-log');
if (!log) {
return;
}
log.textContent = text || '';
log.scrollTop = log.scrollHeight;
}
function appendLog(text) {
var log = getEl('ssr-subscribe-log');
if (!log || !text) {
return;
}
log.textContent = (log.textContent ? log.textContent.replace(/\s*$/, '') + '\n' : '') + text;
log.scrollTop = log.scrollHeight;
}
function clearPoll() {
if (pollTimer) {
window.clearInterval(pollTimer);
pollTimer = null;
}
}
function fetchLog() {
XHR.get('<%=get_log_url%>', null, function(x) {
if (x && x.status === 200) {
updateLog(x.responseText || '');
}
});
}
function openModal() {
var mask = getEl('ssr-subscribe-modal-mask');
var modal = getEl('ssr-subscribe-modal');
if (mask) {
mask.style.display = 'block';
}
if (modal) {
modal.style.display = 'block';
}
}
function closeModal(force) {
if (running && !force) {
return false;
}
clearPoll();
var mask = getEl('ssr-subscribe-modal-mask');
var modal = getEl('ssr-subscribe-modal');
if (mask) {
mask.style.display = 'none';
}
if (modal) {
modal.style.display = 'none';
}
return false;
}
function finishSubscribe(success) {
running = false;
clearPoll();
fetchLog();
if (success) {
var remain = 5;
appendLog('订阅已结束,将在倒数5秒后返回页面');
setStatus('订阅已结束,将在倒数' + remain + '秒后返回页面', '#249400');
var countdown = window.setInterval(function() {
remain = remain - 1;
if (remain <= 0) {
window.clearInterval(countdown);
closeModal(true);
window.location.href = '<%=servers_url%>';
return;
}
setStatus('订阅已结束,将在倒数' + remain + '秒后返回页面', '#249400');
}, 1000);
return;
}
setStatus('<%:Subscribe failed, refreshing page...%>', '#ff0000');
window.setTimeout(function() {
closeModal(true);
window.location.href = '<%=servers_url%>';
}, 1200);
}
function startSubscribe(options, done) {
options = options || {};
running = true;
updateLog('');
setStatus(options.title || '<%:Starting subscription update...%>', '#0072c3');
openModal();
clearPoll();
fetchLog();
pollTimer = window.setInterval(fetchLog, 1000);
XHR.get('<%=subscribe_url%>', options.sid ? { sid: options.sid } : null, function(x, rv) {
var ok = !!(rv && typeof rv.ret !== 'undefined' && Number(rv.ret) === 0);
if (typeof done === 'function') {
done(ok);
}
finishSubscribe(ok);
});
}
window.ssrSubscribeRun = startSubscribe;
window.setTimeout(function() {
var closeBtn = getEl('ssr-subscribe-modal-close');
var mask = getEl('ssr-subscribe-modal-mask');
if (closeBtn) {
closeBtn.onclick = function() {
return closeModal(false);
};
}
if (mask) {
mask.onclick = function() {
return closeModal(false);
};
}
if (autoStart) {
startSubscribe();
}
}, 0);
})();
//]]></script>
<input class="btn cbi-button cbi-button-apply" type="submit"<%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
<div id="ssr-subscribe-modal-mask"></div>
<div id="ssr-subscribe-modal" role="dialog" aria-modal="true" aria-labelledby="ssr-subscribe-modal-title">
<div id="ssr-subscribe-modal-header">
<div id="ssr-subscribe-modal-title"><%:Subscribe Log%></div>
<input type="button" id="ssr-subscribe-modal-close" class="btn cbi-button" value="<%:Close%>" />
</div>
<div id="ssr-subscribe-modal-status"><%:Ready.%></div>
<pre id="ssr-subscribe-log"></pre>
</div>
</div>
<%+cbi/valuefooter%>
@@ -0,0 +1,490 @@
<%-
local rowcnt = 0
local function rowstyle()
rowcnt = rowcnt + 1
if rowcnt % 2 == 0 then
return " cbi-rowstyle-1"
end
return " cbi-rowstyle-2"
end
local function width(o)
if o.width then
if type(o.width) == "number" then
return ' style="width:%dpx"' % o.width
end
return ' style="width:%s"' % o.width
end
return ""
end
-%>
<script type="text/javascript">//<![CDATA[
window.ssrUpdateSubscribeItem = window.ssrUpdateSubscribeItem || function(btn) {
if (!btn) return false;
var sid = btn.getAttribute('data-sid') || '';
var title = btn.getAttribute('data-title') || '';
var original = btn.getAttribute('data-original-value') || btn.value || 'Update';
btn.disabled = true;
btn.value = '<%:Updating...%>';
window.ssrSubscribeRun = window.ssrSubscribeRun || function(_, done) {
if (done) done();
};
window.ssrSubscribeRun({
sid: sid,
title: title
}, function() {
btn.disabled = false;
btn.value = original;
});
return false;
};
function saveAllSubscribeItems(btn) {
// var btn = document.querySelector('#ssr-subscribe-toolbar .cbi-button-save');
if (!btn) return;
var originalText = btn.value;
btn.disabled = true;
btn.value = '<%:Saving...%>';
var fieldsMap = {};
var allInputs = document.querySelectorAll('[name^="cbid.shadowsocksr."]');
allInputs.forEach(function(input) {
var match = input.name.match(/^cbid\.shadowsocksr\.([^.]+)\.(url|alias|enabled)$/);
if (!match) return;
var sid = match[1];
var field = match[2];
if (!fieldsMap[sid]) fieldsMap[sid] = {};
if (field === 'enabled') {
fieldsMap[sid][field] = input.checked ? '1' : '0';
} else {
fieldsMap[sid][field] = input.value;
}
});
var promises = [];
for (var sid in fieldsMap) {
var fields = fieldsMap[sid];
for (var field in fields) {
var value = fields[field];
var promise = new Promise(function(resolve, reject) {
XHR.post(
'<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "toggle_subscribe_item_enabled")%>',
{ sid: sid, field: field, value: value },
function(x, rv) {
if (rv && Number(rv.ret) === 1) {
resolve();
} else {
reject(new Error('<%:Save failed:%> ' + sid + '.' + field + ': ' + (rv && rv.error ? rv.error : '<%:Unknown error%>')));
}
}
);
});
promises.push(promise);
}
}
if (promises.length === 0) {
btn.disabled = false;
btn.value = originalText;
alert('<%:No subscription items found%>');
return;
}
Promise.all(promises).then(function() {
btn.disabled = false;
btn.value = originalText;
alert('<%:All settings saved successfully.%>');
}).catch(function(err) {
btn.disabled = false;
btn.value = originalText;
alert('<%:Save failed:%>' + err.message);
});
}
function deleteSubscribeItem(btn) {
var sid = btn.getAttribute('data-sid');
if (!sid) return;
if (!confirm('<%:Are you sure you want to delete subscribe link: %s ?%>'.replace('%s', sid))) return;
btn.disabled = true;
XHR.post('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "delete_subscribe_item")%>',
{ sid: sid },
function(x, rv) {
btn.disabled = false;
if (rv && rv.ret === 1) {
var row = btn.closest('tr');
if (row) row.remove();
var table = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table');
if (table.querySelectorAll('.cbi-section-table-row:not(.cbi-section-table-titles)').length === 0) {
var placeholder = document.createElement('tr');
placeholder.className = 'tr cbi-section-table-row placeholder';
placeholder.innerHTML = '<td class="td" colspan="4"><em><%:This section contains no values yet%></em></td>';
table.appendChild(placeholder);
}
} else {
alert('<%:Delete failed:%> ' + (rv ? rv.error : '<%:Unknown error%>'));
}
}
);
}
function createEmptyRow(sid) {
var templateRow = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row:not(.cbi-section-table-titles):not(.placeholder)');
if (!templateRow) {
console.error('No template row found');
return null;
}
var newRow = templateRow.cloneNode(true);
newRow.id = 'cbi-shadowsocksr-server_subscribe_item-' + sid;
var inputs = newRow.querySelectorAll('input');
inputs.forEach(function(input) {
if (input.name) {
input.name = input.name.replace(/cbid\.shadowsocksr\.[^.]+\./, 'cbid.shadowsocksr.' + sid + '.');
}
if (input.id) {
input.id = input.id.replace(/cfg[^.]+/, sid);
}
if (input.type === 'checkbox') {
input.checked = false;
input.defaultChecked = false;
} else if (input.type === 'text' || input.type === 'url') {
input.value = '';
input.defaultValue = '';
}
input.disabled = false;
});
var delBtn = newRow.querySelector('.cbi-button-remove');
if (delBtn) {
delBtn.setAttribute('data-sid', sid);
delBtn.removeAttribute('onclick');
delBtn.onclick = function(e) { deleteSubscribeItem(this); return false; };
}
var updateBtn = newRow.querySelector('.cbi-button-apply');
if (updateBtn) {
updateBtn.setAttribute('data-sid', sid);
}
return newRow;
}
function addSubscribeItem() {
var btn = document.getElementById('ssr-add-btn');
if (!btn) return;
btn.disabled = true;
btn.value = '<%:Adding...%>';
XHR.post(
'<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "add_subscribe_item")%>',
{},
function(x, rv) {
btn.disabled = false;
btn.value = '<%:Add%>';
if (rv && rv.ret === 1 && rv.sid) {
var newRow = createEmptyRow(rv.sid);
if (newRow) {
var aliasInput = newRow.querySelector('input[name$=".alias"]');
if (aliasInput) {
aliasInput.value = rv.alias || '';
aliasInput.defaultValue = rv.alias || '';
}
var urlInput = newRow.querySelector('input[name$=".url"]');
if (urlInput) {
urlInput.value = rv.url || '';
urlInput.defaultValue = rv.url || '';
}
var enabledInput = newRow.querySelector('input[type="checkbox"]');
if (enabledInput) {
var checked = (rv.enabled === '1');
enabledInput.checked = checked;
enabledInput.defaultChecked = checked;
}
var table = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table');
var placeholder = table.querySelector('.placeholder');
if (placeholder) placeholder.remove();
table.appendChild(newRow);
}
} else {
alert('<%:Add failed:%> ' + (rv && rv.error ? rv.error : '<%:Unknown error%>'));
}
}
);
}
//]]></script>
<div class="cbi-section cbi-tblsection" id="cbi-<%=self.config%>-<%=self.sectiontype%>">
<% if self.title and #self.title > 0 then -%>
<h3><%=self.title%></h3>
<%- end %>
<%- if self.sortable then -%>
<input type="hidden" id="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" name="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" value="" />
<%- end -%>
<div class="cbi-section-descr"><%=self.description%></div>
<table class="table cbi-section-table">
<tr class="tr cbi-section-table-titles anonymous">
<% for _, node in ipairs(self.children) do if not node.optional and node.option ~= "_update" then -%>
<th class="th cbi-section-table-cell"<%=width(node) .. attr("data-widget", node.typename or "")%>><%=node.title%></th>
<% end end -%>
<th class="th cbi-section-table-cell cbi-section-actions" style="width:1%"><%:Actions%></th>
</tr>
<%
local isempty = true
for i, section in ipairs(self:cfgsections()) do
isempty = false
local colorclass = rowstyle()
local scope = {
valueheader = "cbi/cell_valueheader",
valuefooter = "cbi/cell_valuefooter"
}
-%>
<tr class="tr cbi-section-table-row<%=colorclass%>" id="cbi-<%=self.config%>-<%=section%>">
<%
for _, node in ipairs(self.children) do
if not node.optional and node.option ~= "_update" then
node:render(section, scope)
end
end
local alias = self.map:get(section, "alias") or string.format("Subscribe %s", section:sub(-4))
local update_title = "Subscribe: " .. tostring(alias)
-%>
<td class="td cbi-section-table-cell nowrap cbi-section-actions">
<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="<%=section%>" onclick="deleteSubscribeItem(this); return false;" />
<input
type="button"
class="btn cbi-button cbi-button-apply"
value="<%:Update%>"
data-original-value="<%:Update%>"
<%=attr("data-sid", section) .. attr("data-title", update_title)%>
onclick="return window.ssrUpdateSubscribeItem(this)"
/>
</div>
</td>
</tr>
<%- end -%>
<%- if isempty then -%>
<tr class="tr cbi-section-table-row placeholder">
<td class="td" colspan="4"><em><%:This section contains no values yet%></em></td>
</tr>
<%- end -%>
</table>
</div>
<div class="cbi-section-create cbi-tblsection-create ssr-subscribe-toolbar" id="ssr-subscribe-toolbar">
<input class="btn cbi-button cbi-button-add" type="button" value="<%:Add%>" id="ssr-add-btn" title="<%:Add%>" />
<input class="btn cbi-button cbi-button-save" type="button" value="<%:Save%>" title="<%:Save%>" onclick="saveAllSubscribeItems(this)" />
<!-- <input class="btn cbi-button cbi-button-save ssr-subscribe-save-settings" type="submit" value="<%:Save%>" title="<%:Save%>" name="cbi.apply" /> -->
<div class="ssr-subscribe-toolbar-slot ssr-subscribe-toolbar-slot-delete"></div>
<div class="ssr-subscribe-toolbar-slot ssr-subscribe-toolbar-slot-update"></div>
</div>
<script type="text/javascript">//<![CDATA[
(function() {
function styleActionButton(btn, extraClass) {
if (!btn) return;
btn.classList.remove('cbi-button-apply', 'cbi-button-reset');
btn.classList.add('cbi-button-add');
if (extraClass) btn.classList.add(extraClass);
}
function mountSubscribeActions() {
var toolbar = document.getElementById('ssr-subscribe-toolbar');
var subscribeSection = document.getElementById('cbi-shadowsocksr-server_subscribe');
if (!toolbar || !subscribeSection) return;
if (toolbar.getAttribute('data-ssr-subscribe-actions-ready') === '1') return;
var deleteSlot = toolbar.querySelector('.ssr-subscribe-toolbar-slot-delete');
var updateSlot = toolbar.querySelector('.ssr-subscribe-toolbar-slot-update');
var updateRow = subscribeSection.querySelector('.cbi-value[id$="-subscribe"]');
var deleteRow = subscribeSection.querySelector('.cbi-value[id$="-delete"]');
if (!deleteSlot || !updateSlot || !updateRow || !deleteRow) return;
var modalWrap = updateRow.querySelector('#ssr-subscribe-modal-wrap');
if (modalWrap) {
var updateBtn = modalWrap.querySelector('input[type="submit"]');
styleActionButton(updateBtn, 'ssr-subscribe-update-all');
updateSlot.appendChild(modalWrap);
}
var deleteBtn = deleteRow.querySelector('input[type="submit"]');
if (deleteBtn) {
styleActionButton(deleteBtn, 'ssr-subscribe-delete-all');
deleteSlot.appendChild(deleteBtn);
}
updateRow.style.display = 'none';
deleteRow.style.display = 'none';
toolbar.setAttribute('data-ssr-subscribe-actions-ready', '1');
}
function bindAddButton() {
var addBtn = document.getElementById('ssr-add-btn');
if (addBtn && !addBtn.getAttribute('data-bound')) {
addBtn.addEventListener('click', addSubscribeItem);
addBtn.setAttribute('data-bound', '1');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
mountSubscribeActions();
bindAddButton();
});
window.addEventListener('load', function() {
mountSubscribeActions();
bindAddButton();
});
} else {
window.setTimeout(mountSubscribeActions, 0);
window.setTimeout(bindAddButton, 0);
}
})();
//]]></script>
<style type="text/css">
#ssr-subscribe-toolbar {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
#ssr-subscribe-toolbar > .btn.cbi-button-add,
#ssr-subscribe-toolbar > .btn.cbi-button-save {
margin: 0;
}
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-delete,
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-update {
display: flex;
align-items: center;
}
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-delete {
margin-left: auto;
}
#ssr-subscribe-toolbar #ssr-subscribe-modal-wrap,
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot input.btn {
display: flex;
align-items: center;
}
#ssr-subscribe-toolbar #ssr-subscribe-modal-wrap {
margin: 0 0 0 12px;
}
#ssr-subscribe-toolbar .ssr-subscribe-delete-all {
margin-right: 0 !important;
}
#ssr-subscribe-toolbar > .btn.cbi-button-add,
#ssr-subscribe-toolbar > .btn.cbi-button-save,
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot .btn {
min-height: 2.1rem;
padding-top: 0.18rem;
padding-bottom: 0.18rem;
}
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot .btn.cbi-button-add {
margin: 0;
width: auto;
max-width: 100%;
justify-content: center;
text-align: center;
}
#ssr-subscribe-toolbar .ssr-subscribe-delete-all {
background: #5f6874 !important;
border-color: #5f6874 !important;
color: #fff !important;
}
/* 电脑端按钮间距 */
#cbi-shadowsocksr-server_subscribe_item .ssr-subscribe-actions {
display: flex;
flex-direction: row;
gap: 2px; /* 按钮之间增加 8px 间距 */
align-items: center;
white-space: nowrap;
}
/* 移除电脑端默认外边距,统一由 gap 控制间距 */
#cbi-shadowsocksr-server_subscribe_item .ssr-subscribe-actions .btn {
margin: 0 !important;
}
/* 清除手机端伪元素问题(出现断层显示)*/
@media screen and (max-width: 1152px) {
td[data-title]::after {
content: none !important;
display: none !important;
background: none !important;
}
}
@media (max-width: 900px) {
#ssr-subscribe-toolbar {
flex-wrap: wrap;
}
#ssr-subscribe-toolbar > .btn.cbi-button-add,
#ssr-subscribe-toolbar > .btn.cbi-button-save,
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-delete,
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-update {
margin-left: 0;
}
/* 订阅列表 table 卡片化 */
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table,
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table tbody,
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table tr,
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table td {
display: block !important;
width: 100% !important;
box-sizing: border-box;
}
/* 隐藏表头 */
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-titles {
display: none !important;
}
/* 操作列,将 td 容器调整为 100% */
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table td.cbi-section-actions {
width: 100% !important;
max-width: 100% !important;
display: block !important;
box-sizing: border-box !important;
}
/* 按钮区域 */
#cbi-shadowsocksr-server_subscribe_item .ssr-subscribe-actions {
display: flex !important;
gap: 8px;
width: 100%;
}
/* 保存、删除、更新 三个按钮均分 */
#cbi-shadowsocksr-server_subscribe_item .ssr-subscribe-actions .btn {
flex: 1;
margin: 0 !important;
white-space: nowrap;
}
/* 修复手机端卡片化布局中 placeholder 行的显示问题 */
#cbi-shadowsocksr-server_subscribe_item .placeholder td {
display: table-cell !important;
}
}
</style>
@@ -0,0 +1,47 @@
<script type="text/javascript">//<![CDATA[
(function() {
function bindSubscribeItemEnabledAutoSave() {
var root = document.getElementById('cbi-shadowsocksr-server_subscribe_item');
if (!root || root.getAttribute('data-enabled-autosave-bound') === '1') {
return;
}
root.addEventListener('change', function(ev) {
var target = ev.target;
if (!target || target.type !== 'checkbox' || !target.name) {
return;
}
var matched = target.name.match(/^cbid\.shadowsocksr\.([^.]+)\.enabled$/);
if (!matched) {
return;
}
var sid = matched[1];
var original = target.checked;
target.disabled = true;
XHR.post(
'<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "toggle_subscribe_item_enabled")%>',
{ sid: sid, enabled: original ? '1' : '0' },
function(x, rv) {
target.disabled = false;
if (!(rv && Number(rv.ret) === 1)) {
target.checked = !original;
alert('Failed to save subscription enable state.');
}
}
);
});
root.setAttribute('data-enabled-autosave-bound', '1');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindSubscribeItemEnabledAutoSave);
window.addEventListener('load', bindSubscribeItemEnabledAutoSave);
} else {
window.setTimeout(bindSubscribeItemEnabledAutoSave, 0);
}
})();
//]]></script>
@@ -0,0 +1,182 @@
<style type="text/css">
.ssr-schedule-inline {
display: flex;
gap: 2%;
align-items: flex-start;
flex-wrap: nowrap;
}
.ssr-schedule-item {
width: 30%;
min-width: 0;
}
.ssr-schedule-item select {
width: 100%;
}
.ssr-schedule-subtitle {
font-size: 12px;
color: #666;
margin-bottom: 4px;
line-height: 1.2;
}
.ssr-schedule-hidden {
display: none !important;
}
</style>
<script type="text/javascript">
//<![CDATA[
function ssrCompactSubscribeSchedule() {
function getSelectByName(suffix) {
return document.querySelector('select[name$=".' + suffix + '"], input[name$=".' + suffix + '"]');
}
function shortLabel(row, fallback) {
if (!row) return fallback;
var label = row.querySelector('.cbi-value-title');
if (!label) return fallback;
var text = (label.textContent || '').trim();
var matched = text.match(/\(([^)]+)\)/);
return matched ? matched[1] : fallback;
}
function getRowByName(suffix) {
var el = getSelectByName(suffix);
return el ? el.closest('.cbi-value') : null;
}
function compactScheduleRow() {
var modeRow = getRowByName('config_auto_update_mode');
var weekRow = getRowByName('auto_update_week_time');
var hourRow = getRowByName('auto_update_day_time');
var minRow = getRowByName('auto_update_min_time');
var intervalRow = getRowByName('config_update_interval');
if (!weekRow || !hourRow || !minRow) return;
var weekField = weekRow.querySelector('.cbi-value-field');
var hourField = hourRow.querySelector('.cbi-value-field');
var minField = minRow.querySelector('.cbi-value-field');
if (!weekField || !hourField || !minField) return;
if (weekField.querySelector('.ssr-schedule-inline')) {
return;
}
var weekSelect = weekField.querySelector('select');
var hourSelect = hourField.querySelector('select');
var minSelect = minField.querySelector('select');
if (!weekSelect || !hourSelect || !minSelect) return;
var wrap = document.createElement('div');
wrap.className = 'ssr-schedule-inline';
function makeItem(title, select) {
var item = document.createElement('div');
item.className = 'ssr-schedule-item';
var subtitle = document.createElement('div');
subtitle.className = 'ssr-schedule-subtitle';
subtitle.textContent = title;
item.appendChild(subtitle);
item.appendChild(select);
return item;
}
wrap.appendChild(makeItem(shortLabel(weekRow, '<%:Day/Week%>'), weekSelect));
wrap.appendChild(makeItem(shortLabel(hourRow, '<%:Hour%>'), hourSelect));
wrap.appendChild(makeItem(shortLabel(minRow, '<%:Min%>'), minSelect));
weekField.innerHTML = '';
weekField.appendChild(wrap);
hourRow.classList.add('ssr-schedule-hidden');
minRow.classList.add('ssr-schedule-hidden');
function applyModeVisibility() {
var modeEl = getSelectByName('config_auto_update_mode');
var loopMode = modeEl && modeEl.value === '1';
if (weekRow) {
weekRow.classList.toggle('ssr-schedule-hidden', loopMode);
}
if (hourRow) {
hourRow.classList.add('ssr-schedule-hidden');
}
if (minRow) {
minRow.classList.add('ssr-schedule-hidden');
}
if (intervalRow) {
intervalRow.classList.toggle('ssr-schedule-hidden', !loopMode);
}
}
if (modeRow) {
var modeSelect = modeRow.querySelector('select');
if (modeSelect && !modeSelect.dataset.ssrLoopBound) {
modeSelect.dataset.ssrLoopBound = '1';
modeSelect.addEventListener('change', applyModeVisibility);
}
}
applyModeVisibility();
}
compactScheduleRow();
}
(function() {
var retryCount = 0;
var maxRetries = 30;
var retryTimer = null;
var observer = null;
function hasCompactLayout() {
return !!document.querySelector('.ssr-schedule-inline');
}
function tryCompactSchedule() {
ssrCompactSubscribeSchedule();
if (hasCompactLayout()) {
if (retryTimer) {
window.clearTimeout(retryTimer);
retryTimer = null;
}
if (observer) {
observer.disconnect();
observer = null;
}
return;
}
if (retryCount >= maxRetries) {
return;
}
retryCount = retryCount + 1;
retryTimer = window.setTimeout(tryCompactSchedule, 200);
}
function startCompactWatcher() {
tryCompactSchedule();
if (window.MutationObserver) {
observer = new MutationObserver(function() {
if (!hasCompactLayout()) {
tryCompactSchedule();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startCompactWatcher);
window.addEventListener('load', tryCompactSchedule);
} else {
startCompactWatcher();
}
})();
//]]>
</script>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,22 @@
#!/bin/sh
[ "$ACTION" = "ifup" ] || exit 0
[ "$INTERFACE" = "wan" ] || exit 0
WAN_PROTO="$(uci -q get network.wan.proto 2>/dev/null)"
[ "$WAN_PROTO" = "pppoe" ] || exit 0
LOCK_FILE="/var/run/ssrplus-pppoe-hotplug.lock"
(
if ! mkdir "$LOCK_FILE" 2>/dev/null; then
exit 0
fi
trap 'rmdir "$LOCK_FILE" 2>/dev/null' EXIT INT TERM
sleep 10
/etc/init.d/shadowsocksr restart >/dev/null 2>&1
) >/dev/null 2>&1 &
exit 0
File diff suppressed because it is too large. Load diff
@@ -1,25 +0,0 @@
amazonaws.com
aws.amazon.com
awsstatic.com
fast.com
netflix.com
netflix.net
nflxext.com
nflximg.net
nflxso.net
nflxvideo.net
netflixdnstest0.com
netflixdnstest1.com
netflixdnstest2.com
netflixdnstest3.com
netflixdnstest4.com
netflixdnstest5.com
netflixdnstest6.com
netflixdnstest7.com
netflixdnstest8.com
netflixdnstest9.com
hulu.com
huluim.com
hbonow.com
hbogo.com
hbo.com
@@ -1,192 +0,0 @@
server=/v.youku.com/127.0.0.1#5335
server=/api.youku.com/127.0.0.1#5335
server=/v2.tudou.com/127.0.0.1#5335
server=/www.tudou.com/127.0.0.1#5335
server=/s.plcloud.music.qq.com/127.0.0.1#5335
server=/i.y.qq.com/127.0.0.1#5335
server=/hot.vrs.sohu.com/127.0.0.1#5335
server=/live.tv.sohu.com/127.0.0.1#5335
server=/pad.tv.sohu.com/127.0.0.1#5335
server=/my.tv.sohu.com/127.0.0.1#5335
server=/hot.vrs.letv.com/127.0.0.1#5335
server=/data.video.qiyi.com/127.0.0.1#5335
server=/cache.video.qiyi.com/127.0.0.1#5335
server=/cache.vip.qiyi.com/127.0.0.1#5335
server=/vv.video.qq.com/127.0.0.1#5335
server=/tt.video.qq.com/127.0.0.1#5335
server=/ice.video.qq.com/127.0.0.1#5335
server=/tjsa.video.qq.com/127.0.0.1#5335
server=/a10.video.qq.com/127.0.0.1#5335
server=/xyy.video.qq.com/127.0.0.1#5335
server=/vcq.video.qq.com/127.0.0.1#5335
server=/vsh.video.qq.com/127.0.0.1#5335
server=/vbj.video.qq.com/127.0.0.1#5335
server=/bobo.video.qq.com/127.0.0.1#5335
server=/flvs.video.qq.com/127.0.0.1#5335
server=/bkvv.video.qq.com/127.0.0.1#5335
server=/info.zb.qq.com/127.0.0.1#5335
server=/geo.js.kankan.xunlei.com/127.0.0.1#5335
server=/web-play.pptv.com/127.0.0.1#5335
server=/web-play.pplive.cn/127.0.0.1#5335
server=/dyn.ugc.pps.tv/127.0.0.1#5335
server=/v.pps.tv/127.0.0.1#5335
server=/inner.kandian.com/127.0.0.1#5335
server=/ipservice.163.com/127.0.0.1#5335
server=/so.open.163.com/127.0.0.1#5335
server=/zb.s.qq.com/127.0.0.1#5335
server=/ip.kankan.xunlei.com/127.0.0.1#5335
server=/vxml.56.com/127.0.0.1#5335
server=/music.sina.com.cn/127.0.0.1#5335
server=/play.baidu.com/127.0.0.1#5335
server=/v.iask.com/127.0.0.1#5335
server=/tv.weibo.com/127.0.0.1#5335
server=/wtv.v.iask.com/127.0.0.1#5335
server=/video.sina.com.cn/127.0.0.1#5335
server=/www.yinyuetai.com/127.0.0.1#5335
server=/api.letv.com/127.0.0.1#5335
server=/live.gslb.letv.com/127.0.0.1#5335
server=/static.itv.letv.com/127.0.0.1#5335
server=/ip.apps.cntv.cn/127.0.0.1#5335
server=/vdn.apps.cntv.cn/127.0.0.1#5335
server=/vdn.live.cntv.cn/127.0.0.1#5335
server=/vip.sports.cntv.cn/127.0.0.1#5335
server=/a.play.api.3g.youku.com/127.0.0.1#5335
server=/i.play.api.3g.youku.com/127.0.0.1#5335
server=/api.3g.youku.com/127.0.0.1#5335
server=/tv.api.3g.youku.com/127.0.0.1#5335
server=/play.api.3g.youku.com/127.0.0.1#5335
server=/play.api.3g.tudou.com/127.0.0.1#5335
server=/tv.api.3g.tudou.com/127.0.0.1#5335
server=/api.3g.tudou.com/127.0.0.1#5335
server=/api.tv.sohu.com/127.0.0.1#5335
server=/access.tv.sohu.com/127.0.0.1#5335
server=/iface.iqiyi.com/127.0.0.1#5335
server=/iface2.iqiyi.com/127.0.0.1#5335
server=/cache.m.iqiyi.com/127.0.0.1#5335
server=/dynamic.app.m.letv.com/127.0.0.1#5335
server=/dynamic.meizi.app.m.letv.com/127.0.0.1#5335
server=/dynamic.search.app.m.letv.com/127.0.0.1#5335
server=/dynamic.live.app.m.letv.com/127.0.0.1#5335
server=/listso.m.areainfo.ppstream.com/127.0.0.1#5335
server=/epg.api.pptv.com/127.0.0.1#5335
server=/play.api.pptv.com/127.0.0.1#5335
server=/m.letv.com/127.0.0.1#5335
server=/interface.bilibili.com/127.0.0.1#5335
server=/3g.music.qq.com/127.0.0.1#5335
server=/mqqplayer.3g.qq.com/127.0.0.1#5335
server=/proxy.music.qq.com/127.0.0.1#5335
server=/proxymc.qq.com/127.0.0.1#5335
server=/ip2.kugou.com/127.0.0.1#5335
server=/ip.kugou.com/127.0.0.1#5335
server=/client.api.ttpod.com/127.0.0.1#5335
server=/mobi.kuwo.cn/127.0.0.1#5335
server=/mobilefeedback.kugou.com/127.0.0.1#5335
server=/tingapi.ting.baidu.com/127.0.0.1#5335
server=/music.baidu.com/127.0.0.1#5335
server=/serviceinfo.sdk.duomi.com/127.0.0.1#5335
server=/music.163.com/127.0.0.1#5335
server=/www.xiami.com/127.0.0.1#5335
server=/spark.api.xiami.com/127.0.0.1#5335
server=/iplocation.geo.qiyi.com/127.0.0.1#5335
server=/sns.video.qq.com/127.0.0.1#5335
server=/v5.pc.duomi.com/127.0.0.1#5335
server=/tms.is.ysten.com/127.0.0.1#5335
server=/internal.check.duokanbox.com/127.0.0.1#5335
server=/openapi.youku.com/127.0.0.1#5335
server=/y.qq.com/127.0.0.1#5335
ipset=/v.youku.com/oversea
ipset=/api.youku.com/oversea
ipset=/v2.tudou.com/oversea
ipset=/www.tudou.com/oversea
ipset=/s.plcloud.music.qq.com/oversea
ipset=/i.y.qq.com/oversea
ipset=/hot.vrs.sohu.com/oversea
ipset=/live.tv.sohu.com/oversea
ipset=/pad.tv.sohu.com/oversea
ipset=/my.tv.sohu.com/oversea
ipset=/hot.vrs.letv.com/oversea
ipset=/data.video.qiyi.com/oversea
ipset=/cache.video.qiyi.com/oversea
ipset=/cache.vip.qiyi.com/oversea
ipset=/vv.video.qq.com/oversea
ipset=/tt.video.qq.com/oversea
ipset=/ice.video.qq.com/oversea
ipset=/tjsa.video.qq.com/oversea
ipset=/a10.video.qq.com/oversea
ipset=/xyy.video.qq.com/oversea
ipset=/vcq.video.qq.com/oversea
ipset=/vsh.video.qq.com/oversea
ipset=/vbj.video.qq.com/oversea
ipset=/bobo.video.qq.com/oversea
ipset=/flvs.video.qq.com/oversea
ipset=/bkvv.video.qq.com/oversea
ipset=/info.zb.qq.com/oversea
ipset=/geo.js.kankan.xunlei.com/oversea
ipset=/web-play.pptv.com/oversea
ipset=/web-play.pplive.cn/oversea
ipset=/dyn.ugc.pps.tv/oversea
ipset=/v.pps.tv/oversea
ipset=/inner.kandian.com/oversea
ipset=/ipservice.163.com/oversea
ipset=/so.open.163.com/oversea
ipset=/zb.s.qq.com/oversea
ipset=/ip.kankan.xunlei.com/oversea
ipset=/vxml.56.com/oversea
ipset=/music.sina.com.cn/oversea
ipset=/play.baidu.com/oversea
ipset=/v.iask.com/oversea
ipset=/tv.weibo.com/oversea
ipset=/wtv.v.iask.com/oversea
ipset=/video.sina.com.cn/oversea
ipset=/www.yinyuetai.com/oversea
ipset=/api.letv.com/oversea
ipset=/live.gslb.letv.com/oversea
ipset=/static.itv.letv.com/oversea
ipset=/ip.apps.cntv.cn/oversea
ipset=/vdn.apps.cntv.cn/oversea
ipset=/vdn.live.cntv.cn/oversea
ipset=/vip.sports.cntv.cn/oversea
ipset=/a.play.api.3g.youku.com/oversea
ipset=/i.play.api.3g.youku.com/oversea
ipset=/api.3g.youku.com/oversea
ipset=/tv.api.3g.youku.com/oversea
ipset=/play.api.3g.youku.com/oversea
ipset=/play.api.3g.tudou.com/oversea
ipset=/tv.api.3g.tudou.com/oversea
ipset=/api.3g.tudou.com/oversea
ipset=/api.tv.sohu.com/oversea
ipset=/access.tv.sohu.com/oversea
ipset=/iface.iqiyi.com/oversea
ipset=/iface2.iqiyi.com/oversea
ipset=/cache.m.iqiyi.com/oversea
ipset=/dynamic.app.m.letv.com/oversea
ipset=/dynamic.meizi.app.m.letv.com/oversea
ipset=/dynamic.search.app.m.letv.com/oversea
ipset=/dynamic.live.app.m.letv.com/oversea
ipset=/listso.m.areainfo.ppstream.com/oversea
ipset=/epg.api.pptv.com/oversea
ipset=/play.api.pptv.com/oversea
ipset=/m.letv.com/oversea
ipset=/interface.bilibili.com/oversea
ipset=/3g.music.qq.com/oversea
ipset=/mqqplayer.3g.qq.com/oversea
ipset=/proxy.music.qq.com/oversea
ipset=/proxymc.qq.com/oversea
ipset=/ip2.kugou.com/oversea
ipset=/ip.kugou.com/oversea
ipset=/client.api.ttpod.com/oversea
ipset=/mobi.kuwo.cn/oversea
ipset=/mobilefeedback.kugou.com/oversea
ipset=/tingapi.ting.baidu.com/oversea
ipset=/music.baidu.com/oversea
ipset=/serviceinfo.sdk.duomi.com/oversea
ipset=/music.163.com/oversea
ipset=/www.xiami.com/oversea
ipset=/spark.api.xiami.com/oversea
ipset=/iplocation.geo.qiyi.com/oversea
ipset=/sns.video.qq.com/oversea
ipset=/v5.pc.duomi.com/oversea
ipset=/tms.is.ysten.com/oversea
ipset=/internal.check.duokanbox.com/oversea
ipset=/openapi.youku.com/oversea
ipset=/y.qq.com/oversea
@@ -31,15 +31,18 @@ touch /etc/ssrplus/china_ssr.txt
touch /etc/ssrplus/deny.list
touch /etc/ssrplus/white.list
touch /etc/ssrplus/black.list
touch /etc/ssrplus/netflix.list
touch /etc/ssrplus/netflixip.list
touch /etc/ssrplus/gfw_base.conf
touch /etc/ssrplus/gfw_list.conf
touch /etc/ssrplus/oversea_list.conf
touch /etc/ssrplus/ad.conf
touch /etc/config/shadowsocksr
if [ -s "/etc/config/shadowsocksr" ]; then
if ! uci -q get shadowsocksr.@server_subscribe_item[0] > /dev/null; then
sid_output="$(uci add shadowsocksr server_subscribe_item)"
uci -q set shadowsocksr.$sid_output.enabled=1
uci -q set shadowsocksr.$sid_output.alias="Subscribe default"
fi
if uci -q get shadowsocksr.@server_subscribe[0].auto_update_time > /dev/null; then
uci -q delete shadowsocksr.@server_subscribe[0].auto_update_time
fi
@@ -50,16 +53,20 @@ if [ -s "/etc/config/shadowsocksr" ]; then
uci -q set shadowsocksr.@server_subscribe[0].auto_update_min_time='0'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].ss_type > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].ss_type='ss-rust'
if ! uci -q get shadowsocksr.@server_subscribe[0].config_auto_update_mode > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].config_auto_update_mode='0'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].config_update_interval > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].config_update_interval='60'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].user_agent > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].user_agent='v2rayN/9.99'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].xray_hy2_type > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].xray_hy2_type='hysteria2'
if ! uci -q get shadowsocksr.@server_subscribe[0].proxy > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].proxy='1'
fi
if ! uci -q get shadowsocksr.@global_xray_fragment[0] > /dev/null; then
@@ -68,8 +75,61 @@ if [ -s "/etc/config/shadowsocksr" ]; then
uci -q set shadowsocksr.@global_xray_fragment[0].noise='0'
fi
uci -q commit shadowsocksr
fi
if ! uci -q get shadowsocksr.@global[0].component_mirror > /dev/null; then
uci -q set shadowsocksr.@global[0].component_mirror='direct'
fi
if ! uci -q get shadowsocksr.@global[0].filter_aaaa > /dev/null; then
legacy_filter_aaaa="$(uci -q get shadowsocksr.@global[0].mosdns_ipv6)"
uci -q set shadowsocksr.@global[0].filter_aaaa="${legacy_filter_aaaa:-1}"
fi
legacy_dns_mode="$(uci -q get shadowsocksr.@global[0].pdnsd_enable)"
if [ "$legacy_dns_mode" = "2" ] || [ "$legacy_dns_mode" = "3" ]; then
uci -q set shadowsocksr.@global[0].pdnsd_enable='1'
fi
if uci -q get shadowsocksr.@global[0].mosdns_ipv6 > /dev/null; then
uci -q delete shadowsocksr.@global[0].mosdns_ipv6
fi
if ! uci -q get shadowsocksr.@http_proxy[0] > /dev/null; then
uci -q add shadowsocksr http_proxy
uci -q set shadowsocksr.@http_proxy[0].server='nil'
uci -q set shadowsocksr.@http_proxy[0].local_port='3128'
uci -q set shadowsocksr.@http_proxy[0].http_auth='none'
fi
ss_rust_bin="$(command -v ssserver 2>/dev/null)"
[ -n "$ss_rust_bin" ] || [ ! -x /usr/libexec/ssserver ] || ss_rust_bin="/usr/libexec/ssserver"
mihomo_bin="$(command -v mihomo 2>/dev/null)"
[ -n "$mihomo_bin" ] || [ ! -x /usr/libexec/mihomo ] || mihomo_bin="/usr/libexec/mihomo"
xray_bin="$(command -v xray 2>/dev/null)"
[ -n "$xray_bin" ] || [ ! -x /usr/libexec/xray ] || xray_bin="/usr/libexec/xray"
for section in $(uci -q show shadowsocksr | sed -n "s/^shadowsocksr\\.\\([^.=][^.=]*\\)=servers$/\\1/p"); do
node_type="$(uci -q get shadowsocksr.${section}.type)"
case "$node_type" in
ss|ss-libev)
if [ -n "$mihomo_bin" ]; then
uci -q set shadowsocksr.${section}.type='ss'
elif [ -n "$ss_rust_bin" ]; then
uci -q set shadowsocksr.${section}.type='ss-rust'
elif [ -n "$xray_bin" ]; then
uci -q set shadowsocksr.${section}.type='v2ray'
uci -q set shadowsocksr.${section}.v2ray_protocol='shadowsocks'
fi
;;
v2ray)
if [ "$(uci -q get shadowsocksr.${section}.v2ray_protocol)" = "shadowsocks" ] && [ -n "$mihomo_bin" ]; then
uci -q set shadowsocksr.${section}.type='ss'
uci -q delete shadowsocksr.${section}.v2ray_protocol
fi
;;
esac
done
uci -q commit shadowsocksr
fi
[ -s "/etc/config/shadowsocksr" ] || /etc/init.d/shadowsocksr reset
+88 -107
View File
@@ -10,6 +10,58 @@
LOCK_FILE="/var/lock/ssr-monitor.lock"
[ -f "$LOCK_FILE" ] && exit 2
touch "$LOCK_FILE"
ps_list() {
if busybox ps -w >/dev/null 2>&1; then
busybox ps -w
else
busybox ps
fi
}
match_proc_count() {
local pattern="$1"
ps_list | grep -E "$pattern" | grep -v grep | wc -l
}
redir_tcp_running() {
local ssr_count
local ipt_count
ssr_count=$(match_proc_count 'ssr-retcp')
ipt_count=$(ps_list | grep 'ipt2socks' | grep -v grep | grep -E ' -T( |$)| --tcp-only( |$)' | wc -l)
echo $((ssr_count + ipt_count))
}
redir_udp_running() {
local ssr_count
local ipt_count
ssr_count=$(match_proc_count 'ssr-reudp')
ipt_count=$(ps_list | grep 'ipt2socks' | grep -v grep | grep -E ' -U( |$)| --udp-only( |$)' | wc -l)
echo $((ssr_count + ipt_count))
}
kill_matched_procs() {
local pattern="$1"
ps_list | grep -E "$pattern" | grep -v grep | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
}
get_expected_threads() {
local threads
threads=$(uci_get_by_type global threads 0)
if [ "$threads" = "0" ] || [ -z "$threads" ]; then
threads=$(grep -c '^processor' /proc/cpuinfo 2>/dev/null)
fi
case "$threads" in
''|*[!0-9]*)
threads=1
;;
esac
[ "$threads" -lt 1 ] && threads=1
echo "$threads"
}
server_process_count=$1
redir_tcp_process=$2
redir_udp_process=$3
@@ -21,6 +73,8 @@ if [ -z "$pdnsd_process" ]; then
fi
i=0
GLOBAL_SERVER=$(uci_get_by_type global global_server)
GLOBAL_TYPE=$(uci_get_by_name $GLOBAL_SERVER type)
EXPECTED_THREADS=$(get_expected_threads)
server=$(uci_get_by_name $GLOBAL_SERVER server)
kcp_port=$(uci_get_by_name $GLOBAL_SERVER kcp_port)
server_port=$(uci_get_by_name $GLOBAL_SERVER server_port)
@@ -32,8 +86,12 @@ while [ "1" == "1" ]; do #死循环
sleep 000030s
#redir tcp
if [ "$redir_tcp_process" -gt 0 ]; then
icount=$(busybox ps -w | grep ssr-retcp | grep -v grep | wc -l)
if [ "$icount" == 0 ]; then
icount=$(redir_tcp_running)
local tcp_expected="$redir_tcp_process"
if [ "$GLOBAL_TYPE" = "socks5" ]; then
tcp_expected="$EXPECTED_THREADS"
fi
if [ "$icount" -lt "$tcp_expected" ]; then
logger -t "$NAME" "ssrplus redir tcp error.restart!"
echolog "ssrplus redir tcp error.restart!"
/etc/init.d/shadowsocksr restart
@@ -42,8 +100,12 @@ while [ "1" == "1" ]; do #死循环
fi
#redir udp
if [ "$redir_udp_process" -gt 0 ]; then
icount=$(busybox ps -w | grep ssr-reudp | grep -v grep | wc -l)
if [ "$icount" == 0 ]; then
icount=$(redir_udp_running)
local udp_expected="$redir_udp_process"
if [ "$GLOBAL_TYPE" = "socks5" ]; then
udp_expected="$EXPECTED_THREADS"
fi
if [ "$icount" -lt "$udp_expected" ]; then
logger -t "$NAME" "ssrplus redir udp error.restart!"
echolog "ssrplus redir udp error.restart!"
/etc/init.d/shadowsocksr restart
@@ -52,18 +114,18 @@ while [ "1" == "1" ]; do #死循环
fi
#server
if [ "$server_process_count" -gt 0 ]; then
icount=$(busybox ps -w | grep ssr-server | grep -v grep | wc -l)
icount=$(match_proc_count 'ssr-server|ss-server-|xray-server-')
if [ "$icount" -lt "$server_process_count" ]; then #如果进程挂掉就重启它
logger -t "$NAME" "ssrplus server error.restart!"
echolog "ssrplus server error.restart!"
kill -9 $(busybox ps -w | grep ssr-server | grep -v grep | awk '{print $1}') >/dev/null 2>&1
kill_matched_procs 'ssr-server|ss-server-'
/etc/init.d/shadowsocksr restart
exit 0
fi
fi
#kcptun
if [ "$kcp_process" -gt 0 ]; then
icount=$(busybox ps -w | grep kcptun-client | grep -v grep | wc -l)
icount=$(ps_list | grep kcptun-client | grep -v grep | wc -l)
if [ "$icount" -lt "$kcp_process" ]; then #如果进程挂掉就重启它
logger -t "$NAME" "ssrplus kcptun error.restart!"
echolog "ssrplus kcptun error.restart!"
@@ -73,132 +135,43 @@ while [ "1" == "1" ]; do #死循环
fi
#localsocks
if [ "$local_process" -gt 0 ]; then
icount=$(busybox ps -w | grep ssr-local | grep -v grep | wc -l)
icount=$(match_proc_count 'ssr-local|ss-local|tuic-local')
if [ "$icount" -lt "$local_process" ]; then #如果进程挂掉就重启它
logger -t "$NAME" "global socks server error.restart!"
echolog "global socks server error.restart!"
kill -9 $(busybox ps -w | grep ssr-local | grep -v grep | awk '{print $1}') >/dev/null 2>&1
kill_matched_procs 'ssr-local|ss-local|tuic-local'
/etc/init.d/shadowsocksr restart
exit 0
fi
fi
#dns2tcp
if [ "$pdnsd_process" -eq 1 ]; then
icount=$(busybox ps -w | grep $TMP_BIN_PATH/dns2tcp | grep -v grep | wc -l)
icount=$(ps_list | grep $TMP_BIN_PATH/dns2tcp | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "dns2tcp tunnel error.restart!"
echolog "dns2tcp tunnel error.restart!"
dnsserver=$(uci_get_by_type global tunnel_forward 8.8.4.4:53)
kill -9 $(busybox ps -w | grep $TMP_BIN_PATH/dns2tcp | grep -v grep | awk '{print $1}') >/dev/null 2>&1
ln_start_bin $(first_type dns2tcp) dns2tcp -L "127.0.0.1#$dns_port" -R "${dnsserver/:/#}"
fi
#dns2socks
elif [ "$pdnsd_process" -eq 2 ]; then
icount=$(busybox ps -w | grep -e ssrplus-dns -e "dns2socks 127.0.0.1 $tmp_dns_port" | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "dns2socks $dnsserver tunnel error.restart!"
echolog "dns2socks $dnsserver tunnel error.restart!"
dnsserver=$(uci_get_by_type global tunnel_forward 8.8.4.4:53)
kill -9 $(busybox ps -w | grep ssrplus-dns | grep -v grep | awk '{print $1}') >/dev/null 2>&1
kill -9 $(busybox ps -w | grep "dns2socks 127.0.0.1 $tmp_dns_port" | grep -v grep | awk '{print $1}') >/dev/null 2>&1
ln_start_bin $(first_type microsocks) microsocks -i 127.0.0.1 -p $tmp_dns_port ssrplus-dns
ln_start_bin $(first_type dns2socks) dns2socks 127.0.0.1:$tmp_dns_port $dnsserver 127.0.0.1:$dns_port -q
fi
#dns2socks-rust
elif [ "$pdnsd_process" -eq 3 ]; then
icount=$(busybox ps -w | grep -e ssrplus-dns -e "dns2socks-rust -s socks5://127.0.0.1 $tmp_dns_port" | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "dns2socks-rust $dnsserver tunnel error.restart!"
echolog "dns2socks-rust $dnsserver tunnel error.restart!"
dnsserver=$(uci_get_by_type global tunnel_forward 8.8.4.4:53)
kill -9 $(busybox ps -w | grep ssrplus-dns | grep -v grep | awk '{print $1}') >/dev/null 2>&1
kill -9 $(busybox ps -w | grep "dns2socks-rust -s socks5://127.0.0.1 $tmp_dns_port" | grep -v grep | awk '{print $1}') >/dev/null 2>&1
ln_start_bin $(first_type microsocks) microsocks -i 127.0.0.1 -p $tmp_dns_port ssrplus-dns
ln_start_bin $(first_type dns2socks) dns2socks-rust -s socks5://127.0.0.1:$tmp_dns_port -d $dnsserver -l 127.0.0.1:$dns_port -f -c
kill -9 $(ps_list | grep $TMP_BIN_PATH/dns2tcp | grep -v grep | awk '{print $1}') >/dev/null 2>&1
start_dns2tcp "$dnsserver"
fi
#mosdns
elif [ "$pdnsd_process" -eq 4 ]; then
icount=$(busybox ps -w | grep $TMP_BIN_PATH/mosdns | grep -v grep | wc -l)
icount=$(ps_list | grep $TMP_BIN_PATH/mosdns | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "mosdns tunnel error.restart!"
echolog "mosdns tunnel error.restart!"
dnsserver=$(uci_get_by_type global tunnel_forward 8.8.4.4:53)
kill -9 $(busybox ps -w | grep $TMP_BIN_PATH/mosdns | grep -v grep | awk '{print $1}') >/dev/null 2>&1
kill -9 $(ps_list | grep $TMP_BIN_PATH/mosdns | grep -v grep | awk '{print $1}') >/dev/null 2>&1
ln_start_bin $(first_type mosdns) mosdns start -c /etc/mosdns/config.yaml
#dnsproxy
elif [ "$pdnsd_process" -eq 5 ]; then
icount=$(busybox ps -w | grep -e ssrplus-dns -e "dnsproxy -l 127.0.0.1 -p $tmp_dns_port" | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "dnsproxy $dnsserver tunnel error.restart!"
echolog "dnsproxy $dnsserver tunnel error.restart!"
local dnsproxy_dnsserver="$(uci_get_by_type global parse_method)"
if [ -n "$dnsproxy_dnsserver" ] && [ "$dnsproxy_dnsserver" != "parse_file" ]; then
dnsserver="$(uci_get_by_type global dnsproxy_tunnel_forward 8.8.4.4:53)"
fi
kill -9 $(busybox ps -w | grep "dnsproxy -l 127.0.0.1 -p $tmp_dns_port" | grep -v grep | awk '{print $1}') >/dev/null 2>&1
dnsproxy_ipv6="$(uci_get_by_type global dnsproxy_ipv6)"
disabled_ipv6="--ipv6-disabled"
fi
if [ "$dnsproxy_dnsserver" != "parse_file" ]; then
ln_start_bin $(first_type dnsproxy) dnsproxy -l 127.0.0.1 -p $tmp_dns_port -p $dns_port -u $dnsserver $disabled_ipv6 --cache --cache-min-ttl=3600
else
dnsproxy_dnsserver_file="$TMP_PATH/dnsproxy_dns.list"
cleaned_file="$TMP_PATH/cleaned_dns.list"
temp_file="$TMP_PATH/temp_dns.list"
> "$cleaned_file"
# 清理输入文件并去重
while IFS= read -r line || [ -n "$line" ]; do
line=$(echo "$line" | sed -E 's/^[ \t\r]+//; s/[ \t\r]+$//')
[ -z "$line" ] && continue
echo "$line" | grep -qE '^#' && continue
echo "$line" >> "$cleaned_file"
done < "/etc/ssrplus/dnsproxy_dns.list"
# 获取清理后文件的MD5
cleaned_md5=$(md5sum "$cleaned_file" | awk '{print $1}')
if [ ! -f "$dnsproxy_dnsserver_file" ]; then
cp "$cleaned_file" "$dnsproxy_dnsserver_file"
else
target_md5=$(md5sum "$dnsproxy_dnsserver_file" | awk '{print $1}')
if [ "$cleaned_md5" != "$target_md5" ]; then
> "$temp_file"
# 保留目标文件中也存在于清理文件的记录(去重)
while IFS= read -r line; do
line=$(echo "$line" | sed -E 's/^[ \t\r]+//; s/[ \t\r]+$//')
if grep -qixF "$line" "$cleaned_file" && ! grep -qixF "$line" "$temp_file"; then
echo "$line" >> "$temp_file"
fi
done < "$dnsproxy_dnsserver_file"
# 添加清理文件中有但目标文件没有的记录(去重)
while IFS= read -r line; do
line=$(echo "$line" | sed -E 's/^[ \t\r]+//; s/[ \t\r]+$//')
if ! grep -qixF "$line" "$temp_file"; then
echo "$line" >> "$temp_file"
fi
done < "$cleaned_file"
temp_md5=$(md5sum "$temp_file" | awk '{print $1}')
if [ "$temp_md5" != "$target_md5" ]; then
mv "$temp_file" "$dnsproxy_dnsserver_file"
else
rm -f "$temp_file"
fi
fi
fi
rm -f "$cleaned_file"
if [ -n "$dnsproxy_dnsserver_file" ] && [ -s "$dnsproxy_dnsserver_file" ]; then
local upstreams_logic_mode="$(uci_get_by_type global upstreams_logic_mode)"
ln_start_bin $(first_type dnsproxy) dnsproxy -l 127.0.0.1 -p $tmp_dns_port -p $dns_port -u $dnsproxy_dnsserver_file $disabled_ipv6 --cache --cache-min-ttl=3600 --upstream-mode=$upstreams_logic_mode
fi
fi
fi
#chinadns-ng(proxy)
elif [ "$pdnsd_process" -eq 6 ]; then
icount=$(busybox ps -w | grep -e ssrplus-dns -e "chinadns-ng -b 127.0.0.1 -l $tmp_dns_port" | grep -v grep | wc -l)
icount=$(ps_list | grep -e ssrplus-dns -e "chinadns-ng -b 127.0.0.1 -l $tmp_dns_port" | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "chinadns-ng $dnsserver tunnel error.restart!"
echolog "chinadns-ng $dnsserver tunnel error.restart!"
dnsserver=$(uci_get_by_type global chinadns_ng_tunnel_forward 8.8.4.4:53)
kill -9 $(busybox ps -w | grep "chinadns-ng -b 127.0.0.1 -l $tmp_dns_port" | grep -v grep | awk '{print $1}') >/dev/null 2>&1
kill -9 $(ps_list | grep "chinadns-ng -b 127.0.0.1 -l $tmp_dns_port" | grep -v grep | awk '{print $1}') >/dev/null 2>&1
local chinadns_ng_proto="$(uci_get_by_type global chinadns_ng_proto)"
local chinadns_ng_dns=""
IFS=','
@@ -224,10 +197,18 @@ while [ "1" == "1" ]; do #死循环
dnsserver="$chinadns_ng_dns"
ln_start_bin $(first_type chinadns-ng) chinadns-ng -b 127.0.0.1 -l $tmp_dns_port -l $dns_port -p 3 -d gfw $dnsserver -N --filter-qtype 64,65 -f -r --cache 4096 --cache-stale 86400 --cache-refresh 20
fi
#built-in dns
elif [ "$pdnsd_process" -eq 7 ]; then
if ! is_builtin_dns_active; then
logger -t "$NAME" "builtin dns error.restart!"
echolog "builtin dns error.restart!"
/etc/init.d/shadowsocksr restart
exit 0
fi
fi
#chinadns-ng(china)
if [ "$(uci -q get "dhcp.@dnsmasq[0]._unused_ssrp_changed")" = "1" ]; then
icount=$(busybox ps -w | grep $TMP_BIN_PATH/chinadns-ng | grep -v grep | wc -l)
icount=$(ps_list | grep $TMP_BIN_PATH/chinadns-ng | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "chinadns-ng tunnel error.restart!"
echolog "chinadns-ng tunnel error.restart!"
@@ -237,7 +218,7 @@ while [ "1" == "1" ]; do #死循环
"wan") chinadns="$wandns" ;;
""|"wan_114") chinadns="$wandns,114.114.114.114" ;;
esac
kill -9 $(busybox ps -w | grep $TMP_BIN_PATH/chinadns-ng | grep -v grep | awk '{print $1}') >/dev/null 2>&1
kill -9 $(ps_list | grep $TMP_BIN_PATH/chinadns-ng | grep -v grep | awk '{print $1}') >/dev/null 2>&1
ln_start_bin $(first_type chinadns-ng) chinadns-ng -l $china_dns_port -4 china -p 3 -c ${chinadns/:/#} -t 127.0.0.1#$dns_port -N -f -r
fi
fi
+41 -105
View File
@@ -85,13 +85,14 @@ usage() {
-e <extra_options> extra options for iptables
-o apply the rules to the OUTPUT chain
-O apply the global rules to the OUTPUT chain
-u enable udprelay mode, TPROXY is required
-U enable udprelay mode, using different IP
and ports for TCP and UDP
-f flush the rules
-u enable udprelay mode, TPROXY is required
-U enable udprelay mode, using different IP
and ports for TCP and UDP
-y disable all auxiliary UDP rules when UDP
transparent proxy is unavailable
-f flush the rules
-g gfwlist mode
-r router mode
-c oversea mode
-z all mode
# New persistence management options (use different letters to avoid conflicts)
@@ -274,7 +275,7 @@ flush_nftables() {
# Optional: force delete all ss_spec related sets (even if table was accidentally deleted)
for setname in ss_spec_lan_ac ss_spec_wan_ac ssr_gen_router \
china fplan bplan gmlan oversea whitelist blacklist netflix gfwlist music; do
china fplan bplan gmlan whitelist blacklist gfwlist music; do
$NFT delete set inet ss_spec $setname 2>/dev/null
$NFT delete set ip ss_spec_mangle $setname 2>/dev/null
done
@@ -310,7 +311,7 @@ flush_iptables_legacy() {
ip route del local 0.0.0.0/0 dev lo table 999 2>/dev/null
fi
for setname in ss_spec_lan_ac ss_spec_wan_ac ssr_gen_router \
china fplan bplan gmlan oversea whitelist blacklist netflix gfwlist music; do
china fplan bplan gmlan whitelist blacklist gfwlist music; do
ipset -X $setname 2>/dev/null
done
[ -n "$FWI" ] && echo '#!/bin/sh' >$FWI
@@ -333,7 +334,7 @@ ipset_nft() {
fi
# Create necessary collections
for setname in china gmlan fplan bplan whitelist blacklist netflix music; do
for setname in china gmlan fplan bplan whitelist blacklist music; do
if ! $NFT list set inet ss_spec $setname >/dev/null 2>&1; then
$NFT add set inet ss_spec $setname '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
else
@@ -392,13 +393,6 @@ ipset_nft() {
$NFT add rule inet ss_spec ss_spec_wan_ac meta l4proto tcp ip daddr @music return
fi
# Shunt/Netflix rules
if [ -f "$SHUNT_LIST" ]; then
for ip in $(cat "$SHUNT_LIST" 2>/dev/null); do
[ -n "$ip" ] && $NFT add element inet ss_spec netflix "{ $ip }" 2>/dev/null
done
fi
# Set up mode-specific rules
case "$RUNMODE" in
router)
@@ -427,14 +421,6 @@ ipset_nft() {
$NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @gfwlist jump ss_spec_wan_fw
$NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @gmlan ip daddr != @china jump ss_spec_wan_fw
;;
oversea)
if ! $NFT list set inet ss_spec oversea >/dev/null 2>&1; then
$NFT add set inet ss_spec oversea '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
fi
$NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @oversea jump ss_spec_wan_fw
$NFT add rule inet ss_spec ss_spec_wan_ac ip saddr @gmlan jump ss_spec_wan_fw
$NFT add rule inet ss_spec ss_spec_wan_ac ip daddr @china jump ss_spec_wan_fw
;;
all)
if $NFT list chain inet ss_spec ss_spec_wan_fw >/dev/null 2>&1; then
$NFT add rule inet ss_spec ss_spec_wan_ac jump ss_spec_wan_fw
@@ -450,9 +436,10 @@ ipset_iptables() {
$IPT -N SS_SPEC_WAN_AC 2>/dev/null
$IPT -F SS_SPEC_WAN_AC
$IPT -I SS_SPEC_WAN_AC -m mark --mark 255 -j RETURN
$IPT -I SS_SPEC_WAN_AC -p tcp --dport 53 -d 127.0.0.0/8 -j RETURN
$IPT -I SS_SPEC_WAN_AC -p tcp ! --dport 53 -d "$server" -j RETURN
[ -n "$server" ] && $IPT -I SS_SPEC_WAN_AC -p tcp ! --dport 53 -d "$server" -j RETURN
ipset -N gmlan hash:net 2>/dev/null
for ip in $LAN_GM_IP; do ipset -! add gmlan "$ip"; done
@@ -474,12 +461,6 @@ ipset_iptables() {
$IPT -A SS_SPEC_WAN_AC -m set --match-set gfwlist dst -j SS_SPEC_WAN_FW
$IPT -A SS_SPEC_WAN_AC -m set --match-set gmlan src -m set ! --match-set china dst -j SS_SPEC_WAN_FW
;;
oversea)
ipset -N oversea hash:net 2>/dev/null
$IPT -I SS_SPEC_WAN_AC -m set --match-set oversea dst -j SS_SPEC_WAN_FW
$IPT -A SS_SPEC_WAN_AC -m set --match-set gmlan src -j SS_SPEC_WAN_FW
$IPT -A SS_SPEC_WAN_AC -m set --match-set china dst -j SS_SPEC_WAN_FW
;;
all)
$IPT -A SS_SPEC_WAN_AC -j SS_SPEC_WAN_FW
;;
@@ -510,25 +491,6 @@ ipset_iptables() {
for ip in $WAN_BP_IP; do ipset -! add whitelist "$ip"; done
for ip in $WAN_FW_IP; do ipset -! add blacklist "$ip"; done
if [ "$SHUNT_PORT" != "0" ]; then
ipset -N netflix hash:net 2>/dev/null
for ip in $(cat "${SHUNT_LIST:=/dev/null}" 2>/dev/null); do ipset -! add netflix "$ip"; done
case "$SHUNT_PORT" in
0) ;;
1)
$IPT -I SS_SPEC_WAN_AC -p tcp -m set --match-set netflix dst -j REDIRECT --to-ports "$local_port"
;;
*)
$IPT -I SS_SPEC_WAN_AC -p tcp -m set --match-set netflix dst -j REDIRECT --to-ports "$SHUNT_PORT"
if [ "$SHUNT_PROXY" = "1" ]; then
$IPT -I SS_SPEC_WAN_AC -p tcp -d "$SHUNT_IP" -j REDIRECT --to-ports "$local_port"
else
ipset -! add whitelist "$SHUNT_IP"
fi
;;
esac
fi
return $?
}
@@ -562,22 +524,6 @@ fw_rule_nft() {
fi
fi
if [ "$SHUNT_PORT" != "0" ] && [ -f "$SHUNT_LIST" ]; then
case "$SHUNT_PORT" in
1)
$NFT add rule inet ss_spec ss_spec_wan_ac $TCP_EXT_ARGS ip daddr @netflix counter redirect to :$local_port
;;
*)
$NFT add rule inet ss_spec ss_spec_wan_ac $TCP_EXT_ARGS ip daddr @netflix counter redirect to :$SHUNT_PORT
if [ "$SHUNT_PROXY" = "1" ]; then
$NFT add rule inet ss_spec ss_spec_wan_ac $TCP_EXT_ARGS ip daddr $SHUNT_IP counter redirect to :$local_port
else
[ -n "$SHUNT_IP" ] && $NFT add element inet ss_spec whitelist "{ $SHUNT_IP }" 2>/dev/null
fi
;;
esac
fi
return $?
}
@@ -670,7 +616,7 @@ ac_rule_nft() {
fi
# Block UDP port 443 when TPROXY not Enable
if [ -z "$TPROXY" ]; then
if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then
# Add UDP 443 block rule
if [ -z "$Interface" ]; then
if [ -n "$MATCH_SET" ]; then
@@ -717,10 +663,11 @@ ac_rule_nft() {
case "$OUTPUT" in
1)
# Create ss_spec_output tcp chain
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
$NFT add rule inet ss_spec ss_spec_output meta mark 255 return 2>/dev/null
# Exclude special local addresses
if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
@@ -741,10 +688,11 @@ ac_rule_nft() {
;;
2)
# Create ss_spec_output tcp chain
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
$NFT add rule inet ss_spec ss_spec_output meta mark 255 return 2>/dev/null
# Exclude special local addresses
if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
@@ -758,11 +706,12 @@ ac_rule_nft() {
for ip in $(gen_spec_iplist); do
[ -n "$ip" ] && $NFT add element inet ss_spec ssr_gen_router "{ $ip }" 2>/dev/null
done
if ! $NFT list chain inet ss_spec ss_spec_router >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_router 2>/dev/null
fi
$NFT flush chain inet ss_spec ss_spec_router 2>/dev/null
$NFT add rule inet ss_spec ss_spec_router ip daddr @ssr_gen_router return 2>/dev/null
if ! $NFT list chain inet ss_spec ss_spec_router >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_router 2>/dev/null
fi
$NFT flush chain inet ss_spec ss_spec_router 2>/dev/null
$NFT add rule inet ss_spec ss_spec_router meta mark 255 return 2>/dev/null
$NFT add rule inet ss_spec ss_spec_router ip daddr @ssr_gen_router return 2>/dev/null
$NFT add rule inet ss_spec ss_spec_router jump ss_spec_wan_fw 2>/dev/null
$NFT add rule inet ss_spec ss_spec_output $TCP_EXT_ARGS jump ss_spec_router comment "\"$TAG\"" 2>/dev/null
;;
@@ -792,7 +741,7 @@ ac_rule_iptables() {
EOF
# Block UDP port 443 when TPROXY not Enable
if [ -z "$TPROXY" ]; then
if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then
# Add UDP 443 block rule
if [ -z "$Interface" ]; then
# Global rules
@@ -845,10 +794,11 @@ ac_rule_iptables() {
create ssr_gen_router hash:net
$(gen_spec_iplist | sed -e "s/^/add ssr_gen_router /")
EOF
$IPT -N SS_SPEC_ROUTER 2>/dev/null
$IPT -F SS_SPEC_ROUTER 2>/dev/null
$IPT -A SS_SPEC_ROUTER -m set --match-set ssr_gen_router dst -j RETURN && \
$IPT -A SS_SPEC_ROUTER -j SS_SPEC_WAN_FW
$IPT -N SS_SPEC_ROUTER 2>/dev/null
$IPT -F SS_SPEC_ROUTER 2>/dev/null
$IPT -A SS_SPEC_ROUTER -m mark --mark 255 -j RETURN && \
$IPT -A SS_SPEC_ROUTER -m set --match-set ssr_gen_router dst -j RETURN && \
$IPT -A SS_SPEC_ROUTER -j SS_SPEC_WAN_FW
$IPT -I OUTPUT 1 -p tcp -m comment --comment "$TAG" -j SS_SPEC_ROUTER
;;
esac
@@ -1006,14 +956,6 @@ tp_rule_nft() {
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip daddr @gfwlist counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan ip daddr != @china counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
;;
oversea)
if ! $NFT list set ip ss_spec_mangle oversea >/dev/null 2>&1; then
$NFT add set ip ss_spec_mangle oversea '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
fi
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip saddr @oversea counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip daddr @china counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
;;
all)
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
;;
@@ -1090,8 +1032,8 @@ tp_rule_iptables() {
do
$ipt -A SS_SPEC_TPROXY -p udp -d "$net" -j RETURN
done
$ipt -A SS_SPEC_TPROXY -p udp ! --dport 53 -d "$SERVER" -j RETURN
[ "$server" != "$SERVER" ] && ipset -! add whitelist "$SERVER"
[ -n "$SERVER" ] && $ipt -A SS_SPEC_TPROXY -p udp ! --dport 53 -d "$SERVER" -j RETURN
[ -n "$SERVER" ] && [ "$server" != "$SERVER" ] && ipset -! add whitelist "$SERVER"
if [ -f "${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}" ]; then
while IFS= read -r ip; do
[ -n "$ip" ] && ipset add whitelist "$ip" -exist
@@ -1119,11 +1061,6 @@ tp_rule_iptables() {
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -m set --match-set gfwlist dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set gmlan src -m set ! --match-set china dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
;;
oversea)
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -m set --match-set oversea src -m dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set gmlan src -m set -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -m set --match-set china dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
;;
all)
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
;;
@@ -1495,7 +1432,7 @@ restore_from_persistence() {
fi
}
while getopts ":m:s:l:S:L:i:e:a:B:b:w:p:G:D:F:N:M:I:oOuUfgrczAKPCRXh" arg; do
while getopts ":m:s:l:S:L:i:e:a:B:b:w:p:G:D:F:N:M:I:oOuUyfgrzAKPCRXh" arg; do
case "$arg" in
m)
Interface=$OPTARG
@@ -1563,15 +1500,15 @@ while getopts ":m:s:l:S:L:i:e:a:B:b:w:p:G:D:F:N:M:I:oOuUfgrczAKPCRXh" arg; do
U)
TPROXY=2
;;
y)
DISABLE_UDP_RULES=1
;;
g)
RUNMODE=gfw
;;
r)
RUNMODE=router
;;
c)
RUNMODE=oversea
;;
z)
RUNMODE=all
;;
@@ -1703,7 +1640,7 @@ runmode_change() {
}
# Main process
if [ -n "$server" ] && [ -n "$local_port" ]; then
if [ -n "$local_port" ]; then
if ! echo "$local_port" | grep -qE '^[0-9]+$'; then
loger 3 "Invalid local port: $local_port"
exit 1
@@ -1715,7 +1652,6 @@ if [ -n "$server" ] && [ -n "$local_port" ]; then
LOCAL_PORT=$local_port
;;
2)
: ${SERVER:?"You must assign an ip for the udp relay server."}
: ${LOCAL_PORT:?"You must assign a port for the udp relay server."}
;;
esac
+243 -116
View File
@@ -1,4 +1,4 @@
#!/bin/sh /etc/rc.common
#!/bin/sh
#
# Copyright (C) 2017 openwrt-ssr
# Copyright (C) 2017 yushi studio <ywb94@qq.com>
@@ -7,149 +7,276 @@
# See /LICENSE for more information.
#
ORIG_ARG1="${1:-}"
ORIG_ARG2="${2:-}"
ORIG_ARG3="${3:-}"
[ -f /lib/functions.sh ] && . /lib/functions.sh
. $IPKG_INSTROOT/etc/init.d/shadowsocksr
LOCK_FILE="/var/lock/ssr-switch.lock"
[ -f "$LOCK_FILE" ] && exit 2
touch "$LOCK_FILE"
LOG_FILE=/var/log/ssrplus.log
PROBE_SID=""
cycle_time=60
switch_time=3
normal_flag=0
server_locate=0
server_count=0
ENABLE_SERVER=nil
[ -n "$1" ] && cycle_time=$1
[ -n "$2" ] && switch_time=$2
DEFAULT_SERVER=$(uci_get_by_type global global_server)
CURRENT_SERVER=$DEFAULT_SERVER
probe_socks_port=10800
direct_fail_count=0
direct_fail_limit=3
probe_url="$(uci_get_by_type server_subscribe url_test_url)"
[ -n "$probe_url" ] || probe_url="https://www.google.com/generate_204"
PROBE_INSTANCE_KEY="ssr-switch-probe-local"
#判断代理是否正常
check_proxy() {
local result=0
local try_count=$(uci_get_by_type global switch_try_count 3)
for i in $(seq 1 $try_count); do
/usr/bin/ssr-check www.google.com 80 $switch_time 1
if [ "$?" == "0" ]; then
# echolog "Check Google Proxy Success, count=$i"
result=0
break
else
# echolog "Check Google Proxy Fail, count=$i"
/usr/bin/ssr-check www.baidu.com 80 $switch_time 1
if [ "$?" == "0" ]; then
result=1
else
result=2
fi
fi
DEFAULT_SERVER="$(uci_get_by_type global global_server)"
CURRENT_SERVER="$DEFAULT_SERVER"
ENABLE_SERVER="nil"
cleanup_probe_socks() {
ps_list | grep -v "grep" | grep -E "tcp-http-ssr-switch-local\\.json|ss-${PROBE_INSTANCE_KEY}/config\\.yaml|tuic-${PROBE_INSTANCE_KEY}/config\\.yaml" | awk '{print $1}' | xargs kill >/dev/null 2>&1
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
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 -f /tmp/ssr-switch-probe.log 2>/dev/null
}
start_probe_socks() {
local sid="$1"
local server_type
server_type="$(uci_get_by_name "$sid" type)"
[ "$server_type" = "clash" ] && return 1
[ "$server_type" = "tuic" ] && return 1
[ "$server_type" = "socks5" ] && return 1
cleanup_probe_socks
LOCAL_SERVER="$sid"
local_config_file="$TMP_PATH/tcp-http-ssr-switch-local.json"
_local="2"
local_enable=0
tmp_local_port=
mode="tcp,udp"
if [ "$server_type" = "v2ray" ]; then
export SSR_SWITCH_PROBE=1
gen_config_file "$LOCAL_SERVER" "$server_type" 4 0 "$probe_socks_port" >/tmp/ssr-switch-probe.log 2>&1 || return 1
unset SSR_SWITCH_PROBE
local xray_bin
xray_bin="$(first_type xray)"
[ -x "$xray_bin" ] || return 1
"$xray_bin" run -c "$local_config_file" >>/tmp/ssr-switch-probe.log 2>&1 &
else
export SSR_SWITCH_PROBE=1
export SSR_SWITCH_PROBE_INSTANCE_KEY="$PROBE_INSTANCE_KEY"
start_local_with_port "$probe_socks_port" >/tmp/ssr-switch-probe.log 2>&1 || return 1
unset SSR_SWITCH_PROBE_INSTANCE_KEY
unset SSR_SWITCH_PROBE
fi
sleep 2
return 0
}
probe_http_ok() {
case "$1" in
[1-5][0-9][0-9])
return 0
;;
esac
return 1
}
check_direct_probe() {
local code
code="$(curl -k -sS -L -o /dev/null \
--connect-timeout "$switch_time" \
--max-time "$switch_time" \
-w '%{http_code}' \
"$probe_url" 2>/dev/null || true)"
probe_http_ok "$code"
}
check_proxy_via_socks() {
local try_count
local code
local proxy_host
try_count="$(uci_get_by_type global switch_try_count 3)"
for i in $(seq 1 "$try_count"); do
for proxy_host in "127.0.0.1:$probe_socks_port" "[::1]:$probe_socks_port"; do
code="$(curl -k -sS -L -o /dev/null \
--connect-timeout "$switch_time" \
--max-time "$switch_time" \
--socks5-hostname "$proxy_host" \
-w '%{http_code}' \
"$probe_url" 2>/dev/null || true)"
probe_http_ok "$code" && return 0
done
sleep 1
done
return $result
return 1
}
test_proxy() {
local servername=$(uci_get_by_name $1 server)
local serverport=$(uci_get_by_name $1 server_port)
ipset add whitelist $servername 2>/dev/null
tcping -q -c 3 -i 1 -t 2 -p $serverport $servername
if [ "$?" -gt "0" ]; then
ipset del whitelist $servername 2>/dev/null
local sid="$1"
[ "$(uci_get_by_name "$sid" type)" = "clash" ] && return 1
[ "$(uci_get_by_name "$sid" type)" = "tuic" ] && return 1
start_probe_socks "$sid" || {
cleanup_probe_socks
return 1
fi
/usr/bin/ssr-check $servername $serverport $switch_time
}
check_proxy_via_socks
local ret=$?
ipset del whitelist $servername 2>/dev/null
if [ "$ret" == "0" ]; then
return 0
else
return 1
fi
cleanup_probe_socks
return $ret
}
search_proxy() {
let server_count=server_count+1
[ "$normal_flag" == "1" -a "$server_count" -le "$server_locate" ] && return 0
[ "$(uci_get_by_name $1 switch_enable 0)" != "1" ] && return 1
[ $ENABLE_SERVER != nil ] && return 0
[ "$1" == "$CURRENT_SERVER" ] && return 0
local servername=$(uci_get_by_name $1 server)
local serverport=$(uci_get_by_name $1 server_port)
ipset add whitelist $servername 2>/dev/null
/usr/bin/ssr-check $servername $serverport $switch_time
local ret=$?
ipset del whitelist $servername 2>/dev/null
if [ "$ret" == "0" ]; then
server_locate=$server_count
ENABLE_SERVER=$1
local sid="$1"
[ "$(uci_get_by_name "$sid" switch_enable 0)" = "1" ] || return 1
[ "$(uci_get_by_name "$sid" type)" = "clash" ] && return 1
[ "$(uci_get_by_name "$sid" type)" = "tuic" ] && return 1
[ "$sid" = "$CURRENT_SERVER" ] && return 1
[ "$ENABLE_SERVER" != "nil" ] && return 0
if test_proxy "$sid"; then
ENABLE_SERVER="$sid"
return 0
else
return 1
fi
return 1
}
#选择可用的代理
select_proxy() {
config_load $NAME
ENABLE_SERVER=nil
mkdir -p /var/run /var/etc
server_count=0
config_load "$NAME"
ENABLE_SERVER="nil"
config_foreach search_proxy servers
}
#切换代理
set_main_server() {
local sid="$1"
[ -n "$sid" ] || return 1
uci set shadowsocksr.@global[0].global_server="$sid"
uci commit shadowsocksr
return 0
}
switch_proxy() {
/etc/init.d/shadowsocksr restart $1
set_main_server "$1" || return 1
/etc/init.d/shadowsocksr restart "$1"
return 0
}
start() {
#不支持kcptun启用时的切换
[ $(uci_get_by_name $DEFAULT_SERVER kcp_enable) = "1" ] && return 1
while [ "1" == "1" ]; do #死循环
sleep 0000$cycle_time
LOGTIME=$(date "+%Y-%m-%d %H:%M:%S")
#判断当前代理是否为缺省服务器
if [ "$CURRENT_SERVER" != "$DEFAULT_SERVER" ]; then
#echo "not default proxy"
echolog "Current server is not default Main server, try to switch back."
#检查缺省服务器是否正常
if test_proxy $DEFAULT_SERVER; then
#echo "switch to default proxy"
echolog "Main server is avilable."
#缺省服务器正常,切换回来
CURRENT_SERVER=$DEFAULT_SERVER
switch_proxy $CURRENT_SERVER
echolog "switch to default "$(uci_get_by_name $CURRENT_SERVER alias)" proxy!"
else
echolog "Main server is NOT avilable.Continue using current server."
fi
fi
#判断当前代理是否正常
#echolog "Start checking if the current server is available."
check_proxy
current_ret=$?
if [ "$current_ret" == "1" ]; then
#当前代理错误,判断有无可用的服务器
#echo "current error"
echolog "Current server error, try to switch another server."
select_proxy
if [ "$ENABLE_SERVER" != nil ]; then
#有其他服务器可用,进行切换
#echo $(uci_get_by_name $new_proxy server)
echolog "Another server is avilable, now switching server."
CURRENT_SERVER=$ENABLE_SERVER
switch_proxy $CURRENT_SERVER
normal_flag=1
echolog "Switch to "$(uci_get_by_name $CURRENT_SERVER alias)" proxy!"
else
switch_proxy $CURRENT_SERVER
normal_flag=1
echolog "Try restart current server."
fi
else
normal_flag=0
# echolog "ShadowsocksR No Problem."
fi
while [ "1" = "1" ]; do
sleep "0000$cycle_time"
run_once
done
}
run_once() {
DEFAULT_SERVER="$(uci_get_by_type global global_server)"
[ "$DEFAULT_SERVER" = "nil" ] && {
direct_fail_count=0
return 0
}
[ "$(uci_get_by_name "$DEFAULT_SERVER" type)" = "clash" ] && {
direct_fail_count=0
cleanup_probe_socks
return 0
}
[ "$(uci_get_by_name "$DEFAULT_SERVER" type)" = "tuic" ] && {
direct_fail_count=0
cleanup_probe_socks
return 0
}
[ "$(uci_get_by_name "$DEFAULT_SERVER" kcp_enable 0)" = "1" ] && {
direct_fail_count=0
cleanup_probe_socks
return 1
}
CURRENT_SERVER="$DEFAULT_SERVER"
if check_direct_probe; then
direct_fail_count=0
return 0
fi
direct_fail_count=$((direct_fail_count + 1))
[ "$direct_fail_count" -lt "$direct_fail_limit" ] && return 0
direct_fail_count=0
if test_proxy "$CURRENT_SERVER"; then
return 0
fi
echolog "Current server error, try to switch another server."
select_proxy
if [ "$ENABLE_SERVER" != "nil" ]; then
CURRENT_SERVER="$ENABLE_SERVER"
switch_proxy "$CURRENT_SERVER"
echolog "Switch to $(uci_get_by_name "$CURRENT_SERVER" alias) proxy!"
else
switch_proxy "$CURRENT_SERVER"
echolog "Try restart current server."
fi
return 0
}
probe_once() {
local sid="$1"
[ -n "$sid" ] || {
echo "missing_sid"
return 1
}
cleanup_probe_socks
if ! start_probe_socks "$sid"; then
echo "probe_start:fail"
return 1
fi
echo "probe_start:ok"
echo "probe:curl_ipv4"
curl -k -sS -L -o /dev/null \
--connect-timeout "$switch_time" \
--max-time "$switch_time" \
--socks5-hostname "127.0.0.1:$probe_socks_port" \
-w 'code=%{http_code}\n' \
"$probe_url" 2>/dev/null || true
echo "probe:curl_ipv6"
curl -g -k -sS -L -o /dev/null \
--connect-timeout "$switch_time" \
--max-time "$switch_time" \
--socks5-hostname "[::1]:$probe_socks_port" \
-w 'code=%{http_code}\n' \
"$probe_url" 2>/dev/null || true
cleanup_probe_socks
return 0
}
main() {
case "${ORIG_ARG1:-}" in
probe)
PROBE_SID="${ORIG_ARG2:-}"
probe_once "$PROBE_SID"
;;
once)
run_once
;;
start)
[ -n "$ORIG_ARG2" ] && cycle_time="$ORIG_ARG2"
[ -n "$ORIG_ARG3" ] && switch_time="$ORIG_ARG3"
[ -f "$LOCK_FILE" ] && exit 2
touch "$LOCK_FILE"
start
;;
*)
[ -n "$ORIG_ARG1" ] && cycle_time="$ORIG_ARG1"
[ -n "$ORIG_ARG2" ] && switch_time="$ORIG_ARG2"
[ -f "$LOCK_FILE" ] && exit 2
touch "$LOCK_FILE"
start
;;
esac
}
if [ "${0##*/}" = "ssr-switch" ]; then
main "$@"
fi
Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

@@ -0,0 +1,939 @@
#!/usr/bin/lua
require "nixio.fs"
require "luci.model.uci"
local ok_lyaml, lyaml = pcall(require, "lyaml")
if not ok_lyaml then
io.stderr:write("lyaml_not_found\n")
os.exit(2)
end
local uci = require "luci.model.uci".cursor()
local ok_jsonc, jsonc = pcall(require, "luci.jsonc")
local function read_file(path)
local data = nixio.fs.readfile(path)
if not data or data == "" then
return nil
end
return data
end
local function write_file(path, data)
return nixio.fs.writefile(path, data)
end
local function load_yaml(path)
local raw = read_file(path)
if not raw then
return nil, "read_failed"
end
local ok, parsed = pcall(lyaml.load, raw)
if not ok or type(parsed) ~= "table" then
return nil, "parse_failed"
end
return parsed
end
local function dump_yaml(path, data)
local ok, rendered = pcall(lyaml.dump, { data })
if not ok or not rendered then
return nil, "dump_failed"
end
write_file(path, rendered)
return true
end
local function split_filter_words(text)
local items = {}
for part in tostring(text or ""):gmatch("[^/]+") do
if part ~= "" then
items[#items + 1] = part
end
end
return items
end
local function trim(value)
return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
end
local function parse_csv_line(line)
local cols = {}
local cur = ""
local in_quote = false
local i = 1
while i <= #line do
local ch = line:sub(i, i)
if ch == '"' then
if in_quote and line:sub(i + 1, i + 1) == '"' then
cur = cur .. '"'
i = i + 1
else
in_quote = not in_quote
end
elseif ch == "," and not in_quote then
cols[#cols + 1] = cur
cur = ""
else
cur = cur .. ch
end
i = i + 1
end
cols[#cols + 1] = cur
return cols
end
local function read_clash_client_rules_csv(sid)
local rows = {}
sid = trim(sid)
if sid == "" then
return rows
end
local csv_path = string.format("/etc/ssrplus/clash/%s.csv", sid)
local raw = read_file(csv_path)
if not raw or raw == "" then
return rows
end
local first = true
for line in tostring(raw):gsub("\r", ""):gmatch("[^\n]+") do
local text = trim(line)
if text ~= "" then
if first and text:lower() == "enabled,client,policy,remarks,client_mac" then
first = false
else
local cols = parse_csv_line(line)
if #cols >= 4 then
rows[#rows + 1] = {
enabled = cols[1],
ip_addr = trim(cols[2] or ""),
policy_group = trim(cols[3] or ""),
remarks = trim(cols[4] or ""),
client_mac = trim(cols[5] or "")
}
end
end
end
end
return rows
end
local function has_proxy_sections(doc)
return type(doc.proxies) == "table" or type(doc["proxy-providers"]) == "table"
end
local function validate(path)
local doc = load_yaml(path)
if not doc then
return false
end
return has_proxy_sections(doc)
end
local function filter(path, filter_words)
local doc, err = load_yaml(path)
if not doc then
io.stderr:write(err or "parse_failed", "\n")
return false
end
local words = split_filter_words(filter_words)
if #words == 0 then
return true
end
local removed = {}
local proxies = {}
for _, proxy in ipairs(doc.proxies or {}) do
local name = tostring(proxy.name or "")
local matched = false
for _, word in ipairs(words) do
if name:find(word, 1, true) then
matched = true
removed[name] = true
break
end
end
if not matched then
proxies[#proxies + 1] = proxy
end
end
doc.proxies = proxies
for _, group in ipairs(doc["proxy-groups"] or {}) do
if type(group.proxies) == "table" then
local kept = {}
for _, name in ipairs(group.proxies) do
if not removed[tostring(name)] then
kept[#kept + 1] = name
end
end
group.proxies = kept
end
end
local count = 0
for _ in pairs(removed) do
count = count + 1
end
dump_yaml(path, doc)
io.stdout:write(tostring(count), "\n")
return true
end
local function deep_merge(dst, src)
if type(dst) ~= "table" or type(src) ~= "table" then
return src
end
for k, v in pairs(src) do
if type(v) == "table" and type(dst[k]) == "table" then
dst[k] = deep_merge(dst[k], v)
else
dst[k] = v
end
end
return dst
end
local function strip_runtime_conflicts(doc)
doc.tun = nil
doc.listeners = nil
doc["redir-port"] = nil
doc["tproxy-port"] = nil
doc["socks-port"] = nil
doc["mixed-port"] = nil
doc.port = nil
doc["external-controller"] = nil
doc.secret = nil
doc["allow-lan"] = nil
if type(doc.dns) == "table" then
doc.dns["fake-ip-range"] = nil
doc.dns["fake-ip-filter"] = nil
end
end
local function group_requires_candidates(group)
local gtype = tostring(group and group.type or ""):lower()
return gtype == "select"
or gtype == "fallback"
or gtype == "load-balance"
or gtype == "url-test"
or gtype == "relay"
end
local function has_nonempty_sequence(value)
return type(value) == "table" and next(value) ~= nil
end
local function fill_empty_proxy_groups(doc)
local changed = 0
for _, group in ipairs(doc["proxy-groups"] or {}) do
if type(group) == "table"
and group_requires_candidates(group)
and not has_nonempty_sequence(group.proxies)
and not has_nonempty_sequence(group.use)
then
group.proxies = { "DIRECT" }
changed = changed + 1
end
end
return changed
end
local function strip_incompatible_script_rules(doc)
local kept = {}
local removed = 0
local has_script_rule = false
for _, rule in ipairs(doc.rules or {}) do
local text = tostring(rule or "")
if text:match("^SCRIPT,") then
removed = removed + 1
else
kept[#kept + 1] = rule
if text:match("^SCRIPT,") then
has_script_rule = true
end
end
end
if removed > 0 then
doc.rules = kept
end
if not has_script_rule then
doc.script = nil
end
return removed
end
local function prepare(input_path, output_path)
local doc, err = load_yaml(input_path)
if not doc then
io.stderr:write(err or "parse_failed", "\n")
return false
end
if not has_proxy_sections(doc) then
io.stderr:write("missing_proxy_sections\n")
return false
end
strip_runtime_conflicts(doc)
local filled_groups = fill_empty_proxy_groups(doc)
local stripped_rules = strip_incompatible_script_rules(doc)
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)
io.stdout:write(string.format("filled_groups=%d stripped_script_rules=%d\n", filled_groups, stripped_rules))
return true
end
local function merge(raw_path, overlay_path, output_path)
local raw_doc, raw_err = load_yaml(raw_path)
if not raw_doc then
io.stderr:write(raw_err or "parse_failed", "\n")
return false
end
local overlay_doc, overlay_err = load_yaml(overlay_path)
if not overlay_doc then
io.stderr:write(overlay_err or "parse_failed", "\n")
return false
end
strip_runtime_conflicts(raw_doc)
local filled_groups = fill_empty_proxy_groups(raw_doc)
local stripped_rules = strip_incompatible_script_rules(raw_doc)
local merged = deep_merge(raw_doc, overlay_doc)
local ok, rendered = pcall(lyaml.dump, { merged })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
io.stdout:write(string.format("filled_groups=%d stripped_script_rules=%d\n", filled_groups, stripped_rules))
return true
end
local function append_client_policy_rules(runtime_path, sid)
local doc, err = load_yaml(runtime_path)
if not doc then
io.stderr:write(err or "parse_failed", "\n")
return false
end
local valid_policies = {}
for _, proxy in ipairs(doc.proxies or {}) do
if type(proxy) == "table" and proxy.name and proxy.name ~= "" then
valid_policies[tostring(proxy.name)] = true
end
end
for _, group in ipairs(doc["proxy-groups"] or {}) do
if type(group) == "table" and group.name and group.name ~= "" then
valid_policies[tostring(group.name)] = true
end
end
local custom_rules = {}
for _, section in ipairs(read_clash_client_rules_csv(sid)) do
if tostring(section.enabled or "0") == "1" then
local ip_addr = tostring(section.ip_addr or "")
local policy_group = tostring(section.policy_group or "")
if ip_addr ~= "" and policy_group ~= "" and valid_policies[policy_group] then
if not ip_addr:find("/", 1, true) then
ip_addr = ip_addr .. "/32"
end
custom_rules[#custom_rules + 1] = string.format("SRC-IP-CIDR,%s,%s", ip_addr, policy_group)
end
end
end
if #custom_rules == 0 then
io.stdout:write("client_rules=0\n")
return true
end
local existing_rules = {}
for _, rule in ipairs(doc.rules or {}) do
local text = tostring(rule or "")
if not text:match("^SRC%-IP%-CIDR,") then
existing_rules[#existing_rules + 1] = rule
end
end
doc.rules = {}
for _, rule in ipairs(custom_rules) do
doc.rules[#doc.rules + 1] = rule
end
for _, rule in ipairs(existing_rules) do
doc.rules[#doc.rules + 1] = rule
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(runtime_path, rendered)
io.stdout:write(string.format("client_rules=%d\n", #custom_rules))
return true
end
local function bool_enabled(value)
return value == "1" or value == 1 or value == true or value == "true"
end
local function split_csv(value)
local items = {}
for part in tostring(value or ""):gmatch("[^,%s]+") do
items[#items + 1] = part
end
return items
end
local function get_server_field(sid, option, default)
local value = uci:get("shadowsocksr", sid, option)
if value == nil or value == "" then
return default
end
return value
end
local function get_filter_aaaa()
local value = uci:get_first("shadowsocksr", "global", "filter_aaaa", "1")
if value == nil or value == "" then
value = uci:get_first("shadowsocksr", "global", "mosdns_ipv6", "1")
end
return value
end
local function build_tuic_runtime_doc(sid, local_port, socks_port, mode)
local server = get_server_field(sid, "server", "")
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local tuic_ip = get_server_field(sid, "tuic_ip", "")
local tls_host = get_server_field(sid, "tls_host", "")
local ipstack_prefer = get_server_field(sid, "ipstack_prefer", "")
local dns_mode = uci:get_first("shadowsocksr", "global", "pdnsd_enable", "0")
local proxy = {
name = sid,
type = "tuic",
server = server,
port = server_port,
uuid = get_server_field(sid, "tuic_uuid", ""),
password = get_server_field(sid, "tuic_passwd", ""),
["udp-relay-mode"] = get_server_field(sid, "udp_relay_mode", "native"),
["congestion-controller"] = get_server_field(sid, "congestion_control", "cubic"),
["skip-cert-verify"] = bool_enabled(get_server_field(sid, "insecure", "0")),
["disable-sni"] = bool_enabled(get_server_field(sid, "disable_sni", "0")),
["reduce-rtt"] = bool_enabled(get_server_field(sid, "zero_rtt_handshake", "0"))
}
if tuic_ip ~= "" then
proxy.ip = tuic_ip
end
if tls_host ~= "" then
proxy.sni = tls_host
end
local alpn = split_csv(get_server_field(sid, "tuic_alpn", ""))
if #alpn > 0 then
proxy.alpn = alpn
end
local heartbeat = tonumber(get_server_field(sid, "heartbeat", "0"))
if heartbeat and heartbeat > 0 then
proxy["heartbeat-interval"] = heartbeat * 1000
end
local timeout = tonumber(get_server_field(sid, "timeout", "0"))
if timeout and timeout > 0 then
proxy["request-timeout"] = timeout * 1000
end
local max_udp_packet_size = tonumber(get_server_field(sid, "tuic_max_package_size", "0"))
if max_udp_packet_size and max_udp_packet_size > 0 then
proxy["max-udp-relay-packet-size"] = max_udp_packet_size
end
if ipstack_prefer ~= "" then
proxy["ip-version"] = ipstack_prefer == "v6first" and "ipv6-prefer" or "ipv4-prefer"
end
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 = { sid }
}
},
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 generate_tuic_runtime(sid, output_path, local_port, socks_port, mode)
local doc = build_tuic_runtime_doc(sid, 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 parse_plugin_opts(value)
local result = {}
for part in tostring(value or ""):gmatch("[^;]+") do
local key, val = part:match("^%s*([^=]+)=?(.*)%s*$")
if key and key ~= "" then
result[key] = val or ""
end
end
return result
end
local function split_host_port(value)
local text = tostring(value or "")
if text == "" then
return "", ""
end
local host, port = text:match("^%[(.-)%]:(%d+)$")
if host and port then
return host, port
end
host, port = text:match("^(.-):(%d+)$")
if host and port then
return host, port
end
return text, ""
end
local function bool_default(value, default)
if value == nil or value == "" then
return default
end
return bool_enabled(value)
end
local function number_or_nil(value)
if value == nil or value == "" then
return nil
end
return tonumber(value)
end
local function string_or_nil(value)
if value == nil or value == "" then
return nil
end
return tostring(value)
end
local function pick_plugin_opt(plugin_opts, ...)
for i = 1, select("#", ...) do
local key = select(i, ...)
local value = plugin_opts[key]
if value ~= nil and value ~= "" then
return value
end
end
return nil
end
local function parse_plugin_headers(plugin_opts)
local headers = {}
local raw_headers = pick_plugin_opt(plugin_opts, "headers", "header")
if raw_headers and ok_jsonc and jsonc then
local decoded = jsonc.parse(raw_headers)
if type(decoded) == "table" then
for key, value in pairs(decoded) do
headers[tostring(key)] = tostring(value)
end
end
end
if raw_headers and next(headers) == nil then
for part in tostring(raw_headers):gmatch("[^|,]+") do
local key, value = part:match("^%s*([^=:]+)%s*[:=]%s*(.-)%s*$")
if key and key ~= "" and value and value ~= "" then
headers[key] = value
end
end
end
for key, value in pairs(plugin_opts) do
local header_name = key:match("^headers[%.:](.+)$")
or key:match("^header[%.:](.+)$")
or key:match("^header_(.+)$")
if header_name and header_name ~= "" and value ~= "" then
headers[header_name] = value
end
end
return next(headers) and headers or nil
end
local function get_plugin_client_fingerprint(sid, plugin_opts)
return string_or_nil(
pick_plugin_opt(
plugin_opts,
"client-fingerprint",
"client_fingerprint",
"fingerprint"
) or get_server_field(sid, "fingerprint", "")
)
end
local function normalize_plugin_name(plugin)
local value = tostring(plugin or ""):lower()
if value == "" or value == "none" then
return ""
end
if value == "simple-obfs" then
return "obfs-local"
end
if value == "obfs" then
return "obfs-local"
end
if value == "shadowtls" then
return "shadow-tls"
end
if value == "gost" then
return "gost-plugin"
end
if value == "kcp-tun" then
return "kcptun"
end
return value
end
local function build_shadowsocks_plugin(proxy, sid)
local plugin = normalize_plugin_name(get_server_field(sid, "plugin", ""))
local plugin_opts = parse_plugin_opts(get_server_field(sid, "plugin_opts", ""))
if plugin == "" then
return
end
if plugin == "obfs-local" then
proxy.plugin = "obfs"
proxy["plugin-opts"] = {
mode = pick_plugin_opt(plugin_opts, "obfs", "mode") or "http",
host = string_or_nil(pick_plugin_opt(plugin_opts, "obfs-host", "obfs_host", "host"))
}
return
end
if plugin == "v2ray-plugin" or plugin == "xray-plugin" then
proxy.plugin = "v2ray-plugin"
proxy["plugin-opts"] = {
mode = pick_plugin_opt(plugin_opts, "mode") or "websocket",
tls = bool_default(pick_plugin_opt(plugin_opts, "tls"), false),
fingerprint = string_or_nil(pick_plugin_opt(plugin_opts, "fingerprint")),
["skip-cert-verify"] = bool_default(pick_plugin_opt(plugin_opts, "skip-cert-verify", "skip_cert_verify", "insecure"), false),
host = string_or_nil(pick_plugin_opt(plugin_opts, "host")),
path = string_or_nil(pick_plugin_opt(plugin_opts, "path")),
mux = bool_default(pick_plugin_opt(plugin_opts, "mux"), false),
headers = parse_plugin_headers(plugin_opts),
["v2ray-http-upgrade"] = bool_default(pick_plugin_opt(plugin_opts, "v2ray-http-upgrade", "v2ray_http_upgrade"), false)
}
return
end
if plugin == "gost-plugin" then
proxy.plugin = "gost-plugin"
proxy["plugin-opts"] = {
mode = pick_plugin_opt(plugin_opts, "mode") or "websocket",
tls = bool_default(pick_plugin_opt(plugin_opts, "tls"), false),
fingerprint = string_or_nil(pick_plugin_opt(plugin_opts, "fingerprint")),
["skip-cert-verify"] = bool_default(pick_plugin_opt(plugin_opts, "skip-cert-verify", "skip_cert_verify", "insecure"), false),
host = string_or_nil(pick_plugin_opt(plugin_opts, "host")),
path = string_or_nil(pick_plugin_opt(plugin_opts, "path")),
mux = bool_default(pick_plugin_opt(plugin_opts, "mux"), false),
headers = parse_plugin_headers(plugin_opts)
}
return
end
if plugin == "shadow-tls" then
local host, port = split_host_port(pick_plugin_opt(plugin_opts, "host") or "")
local version
if plugin_opts.v3 == "1" or plugin_opts.version == "3" then
version = 3
elseif plugin_opts.v2 == "1" or plugin_opts.version == "2" then
version = 2
elseif plugin_opts.v1 == "1" or plugin_opts.version == "1" then
version = 1
end
proxy.plugin = "shadow-tls"
proxy["client-fingerprint"] = get_plugin_client_fingerprint(sid, plugin_opts)
proxy["plugin-opts"] = {
host = host ~= "" and host or nil,
port = number_or_nil(port),
password = string_or_nil(pick_plugin_opt(plugin_opts, "passwd", "password")),
version = version
}
return
end
if plugin == "restls" then
proxy.plugin = "restls"
proxy["client-fingerprint"] = get_plugin_client_fingerprint(sid, plugin_opts)
proxy["plugin-opts"] = {
host = string_or_nil(pick_plugin_opt(plugin_opts, "host")),
password = string_or_nil(pick_plugin_opt(plugin_opts, "passwd", "password")),
["version-hint"] = string_or_nil(pick_plugin_opt(plugin_opts, "version-hint", "version_hint")),
["restls-script"] = string_or_nil(pick_plugin_opt(plugin_opts, "restls-script", "restls_script"))
}
return
end
if plugin == "kcptun" then
proxy.plugin = "kcptun"
proxy["plugin-opts"] = {
key = string_or_nil(pick_plugin_opt(plugin_opts, "key", "passwd", "password")),
crypt = string_or_nil(pick_plugin_opt(plugin_opts, "crypt")),
mode = string_or_nil(pick_plugin_opt(plugin_opts, "mode")),
conn = number_or_nil(pick_plugin_opt(plugin_opts, "conn")),
autoexpire = number_or_nil(pick_plugin_opt(plugin_opts, "autoexpire")),
scavengettl = number_or_nil(pick_plugin_opt(plugin_opts, "scavengettl")),
mtu = number_or_nil(pick_plugin_opt(plugin_opts, "mtu")),
ratelimit = number_or_nil(pick_plugin_opt(plugin_opts, "ratelimit")),
sndwnd = number_or_nil(pick_plugin_opt(plugin_opts, "sndwnd")),
rcvwnd = number_or_nil(pick_plugin_opt(plugin_opts, "rcvwnd")),
datashard = number_or_nil(pick_plugin_opt(plugin_opts, "datashard")),
parityshard = number_or_nil(pick_plugin_opt(plugin_opts, "parityshard")),
dscp = number_or_nil(pick_plugin_opt(plugin_opts, "dscp")),
nocomp = bool_default(pick_plugin_opt(plugin_opts, "nocomp"), false),
acknodelay = bool_default(pick_plugin_opt(plugin_opts, "acknodelay"), false),
nodelay = number_or_nil(pick_plugin_opt(plugin_opts, "nodelay")),
interval = number_or_nil(pick_plugin_opt(plugin_opts, "interval")),
resend = number_or_nil(pick_plugin_opt(plugin_opts, "resend")),
sockbuf = number_or_nil(pick_plugin_opt(plugin_opts, "sockbuf")),
smuxver = number_or_nil(pick_plugin_opt(plugin_opts, "smuxver")),
smuxbuf = number_or_nil(pick_plugin_opt(plugin_opts, "smuxbuf")),
framesize = number_or_nil(pick_plugin_opt(plugin_opts, "framesize")),
streambuf = number_or_nil(pick_plugin_opt(plugin_opts, "streambuf")),
keepalive = number_or_nil(pick_plugin_opt(plugin_opts, "keepalive"))
}
return
end
proxy.plugin = plugin
if next(plugin_opts) then
proxy["plugin-opts"] = plugin_opts
end
end
local function build_kcptun_plugin(proxy, sid)
if not bool_enabled(get_server_field(sid, "kcp_enable", "0")) then
return
end
proxy.plugin = "kcptun"
proxy.port = tonumber(get_server_field(sid, "kcp_port", "0")) or proxy.port
proxy["plugin-opts"] = {
key = get_server_field(sid, "kcp_password", ""),
mode = "fast",
mtu = 1350
}
end
local function build_shadowsocks_runtime_doc(sid, local_port, socks_port, mode)
local dns_mode = uci:get_first("shadowsocksr", "global", "pdnsd_enable", "0")
local server = get_server_field(sid, "server", "")
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local method = get_server_field(sid, "encrypt_method_ss", "none")
local password = get_server_field(sid, "password", "")
local proxy = {
name = sid,
type = "ss",
server = server,
port = server_port,
cipher = method,
password = password,
udp = true,
tfo = bool_enabled(get_server_field(sid, "fast_open", "0"))
}
if get_server_field(sid, "type", "") == "ss" then
build_kcptun_plugin(proxy, sid)
end
if proxy.plugin == nil then
build_shadowsocks_plugin(proxy, sid)
end
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 = { sid }
}
},
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"
}
}
local listen_port = tonumber(local_port)
local socks_listen = tonumber(socks_port)
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 generate_shadowsocks_runtime(sid, output_path, local_port, socks_port, mode)
local doc = build_shadowsocks_runtime_doc(sid, local_port, socks_port, mode)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local function build_shadowsocks_server_doc(sid)
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local method = get_server_field(sid, "encrypt_method_ss", "aes-128-gcm")
local password = get_server_field(sid, "password", "")
local listener = {
name = sid,
type = "shadowsocks",
listen = "::",
port = server_port,
cipher = method,
password = password,
udp = true,
tfo = bool_enabled(get_server_field(sid, "fast_open", "0"))
}
local plugin = normalize_plugin_name(get_server_field(sid, "plugin", ""))
if plugin == "obfs-local" then
local plugin_opts = parse_plugin_opts(get_server_field(sid, "plugin_opts", ""))
listener.obfs = plugin_opts.obfs or plugin_opts.mode or "http"
listener.obfs_opts = {
mode = plugin_opts.obfs or plugin_opts.mode or "http",
host = plugin_opts["obfs-host"] or plugin_opts.obfs_host or plugin_opts.host or nil
}
end
return {
["allow-lan"] = true,
["bind-address"] = "*",
["log-level"] = "silent",
["find-process-mode"] = "off",
listeners = { listener }
}
end
local function generate_shadowsocks_server(sid, output_path)
local doc = build_shadowsocks_server_doc(sid)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local action = arg[1]
if action == "validate" then
os.exit(validate(arg[2]) and 0 or 1)
elseif action == "filter" then
os.exit(filter(arg[2], arg[3]) and 0 or 1)
elseif action == "prepare" then
os.exit(prepare(arg[2], arg[3]) and 0 or 1)
elseif action == "merge" then
os.exit(merge(arg[2], arg[3], arg[4]) and 0 or 1)
elseif action == "append_client_policy_rules" then
os.exit(append_client_policy_rules(arg[2], arg[3]) and 0 or 1)
elseif action == "tuic" then
os.exit(generate_tuic_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss" then
os.exit(generate_shadowsocks_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss_server" then
os.exit(generate_shadowsocks_server(arg[2], arg[3]) and 0 or 1)
else
io.stderr:write("usage: clash_yaml.lua validate <yaml> | filter <yaml> <words> | prepare <input> <output> | merge <raw> <overlay> <output> | append_client_policy_rules <runtime_yaml> <sid> | tuic <sid> <output> <local_port> [socks_port] [mode] | ss <sid> <output> <local_port> [socks_port] [mode] | ss_server <sid> <output>\n")
os.exit(1)
end
@@ -2,7 +2,6 @@
require "luci.sys"
local ucursor = require "luci.model.uci".cursor()
local datatypes = require "luci.cbi.datatypes"
local json = require "luci.jsonc"
local server_section = arg[1]
@@ -12,12 +11,6 @@ local socks_port = arg[4] or "0"
local chain = arg[5] or "0"
local GLOBAL = {
DNS_SERVER = {},
DNS_HOSTNAME = {},
VPS_EXCLUDE = {}
}
-- 辅助函数:拆分字符串(若 luci.util 未加载则定义)
local function split(str, pat)
local t = {}
@@ -38,27 +31,52 @@ local server = ucursor:get_all("shadowsocksr", server_section)
local socks_server = ucursor:get_all("shadowsocksr", "@socks5_proxy[0]") or {}
local xray_fragment = ucursor:get_all("shadowsocksr", "@global_xray_fragment[0]") or {}
local xray_noise = ucursor:get_all("shadowsocksr", "@xray_noise_packets[0]") or {}
local default_node_local_port = ucursor:get_first("shadowsocksr", "global", "default_node_local_port", "1234")
local dns_mode = ucursor:get_first("shadowsocksr", "global", "pdnsd_enable", "0")
local dns_ipv4_only = ucursor:get_first("shadowsocksr", "global", "filter_aaaa")
if not dns_ipv4_only or dns_ipv4_only == "" then
dns_ipv4_only = ucursor:get_first("shadowsocksr", "global", "mosdns_ipv6", "1")
end
local builtin_dns_server = ucursor:get_first("shadowsocksr", "global", "tunnel_forward", "8.8.4.4:53")
local outbound_settings = nil
local xray_version = nil
local xray_version_val = 0
local xray_builtin_dns = nil
local node_id = server_section
local remarks = server.alias or ""
local b64decode = nixio.bin.b64decode
local b64encode = nixio.bin.b64encode
local effective_node_local_port = tonumber(server.local_port) or tonumber(default_node_local_port) or 1234
-- 解析 URL(简单实现,仅用于 DoH)
local function parseURL(url)
if not url then return nil end
local schema, rest = url:match("^(https?)://(.*)$")
if not schema then return nil end
local host, port_str = rest:match("^([^:]+):?(%d*)/?.*$")
local port = tonumber(port_str) or (schema == "https" and 443 or 80)
return { host = host, port = port, schema = schema }
if server.type == "ss-rust" then
server.type = "ss"
end
if server.type == "ss-rust" or server.type == "ss-libev" then
server.type = "ss"
local function parse_realm_uri(uri)
if type(uri) ~= "string" then return nil end
-- realm://token@server/realm_id?query
local token, server_url, realm_id, query = trim(uri):match("^realm://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not token or not server_url or not realm_id then return nil end
realm_id = realm_id:gsub("/+$", "")
local realm = {
token = token,
server_url = server_url,
realm_id = realm_id
}
-- 解析 query 中的 stun=
if query and query ~= "" then
local stun_servers = {}
for key, value in query:gmatch("([^&=?]+)=([^&]+)") do
if key == "stun" and value ~= "" then
stun_servers[#stun_servers + 1] = value
end
end
if #stun_servers > 0 then
realm.stun_servers = stun_servers
end
end
return realm
end
-- base64 解码
@@ -120,9 +138,25 @@ local function cleanEmptyTables(t)
return next(t) and t or nil
end
local function format_host(host)
host = tostring(host or "")
if host ~= "" and host:find(":", 1, true) and not host:match("^%[.*%]$") then
return "[" .. host .. "]"
end
return host
end
local function format_host_port(host, port)
host = format_host(host)
if port == nil or port == "" then
return host
end
return host .. ":" .. tostring(port)
end
-- 确保正确判断程序是否存在
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
end
-- 获取 Xray 版本号
@@ -203,20 +237,33 @@ function wireguard()
-- 处理 reserved 字段,支持逗号分隔的数字或 Base64 编码
local reserved = nil
if server.reserved then
local bytes = {}
if not server.reserved:match("[^%d,]+") then
-- 纯数字和逗号,解析为数字列表
server.reserved:gsub("%d+", function(b)
bytes[#bytes + 1] = tonumber(b)
end)
else
-- Base64 编码的二进制数据
local result = base64Decode(server.reserved)
for i = 1, #result do
bytes[i] = result:byte(i)
local all_bytes = {}
local reserved_values = server.reserved
-- 确保是 table 类型
if type(reserved_values) ~= "table" then
reserved_values = {reserved_values}
end
for _, reserved_str in ipairs(reserved_values) do
if type(reserved_str) == "string" then
if not reserved_str:match("[^%d,]+") then
-- 数字和逗号格式
reserved_str:gsub("%d+", function(b)
all_bytes[#all_bytes + 1] = tonumber(b)
end)
else
-- Base64 格式
local result = base64Decode(reserved_str)
if result then
for i = 1, #result do
all_bytes[#all_bytes + 1] = result:byte(i)
end
end
end
end
end
reserved = #bytes > 0 and bytes or nil
reserved = #all_bytes > 0 and all_bytes or nil
end
outbound_settings = {
@@ -226,15 +273,22 @@ function wireguard()
{
publicKey = server.peer_pubkey,
preSharedKey = server.preshared_key,
endpoint = server.server .. ":" .. server.server_port,
endpoint = format_host_port(server.server, server.server_port),
keepAlive = tonumber(server.keepaliveperiod),
allowedIPs = (server.allowedips) or nil,
}
},
noKernelTun = (server.kernelmode == "1") and true or false,
kernelMode = (server.kernelmode == "1") and true or false,
reserved = reserved,
mtu = tonumber(server.mtu)
}
if server.finalmask and server.finalmask ~= "" then
local ok, fm = pcall(json.parse, base64Decode(server.finalmask))
if ok and type(fm) == "table" then
outbound_settings.streamSettings = outbound_settings.streamSettings or {}
outbound_settings.streamSettings.finalmask = fm
end
end
end
function xray_hysteria2()
outbound_settings = {
@@ -299,6 +353,35 @@ local Xray = {
-- 初始化 outbounds 表
outbounds = {},
}
if server.type == "v2ray" and dns_mode == "7" and os.getenv("SSR_SWITCH_PROBE") ~= "1" then
local dns_host = builtin_dns_server:match("^([^:]+)") or "8.8.4.4"
local dns_port = tonumber(builtin_dns_server:match(":(%d+)$")) or 53
Xray.dns = {
queryStrategy = (dns_ipv4_only == "1") and "UseIPv4" or "UseIP",
servers = {
string.format("tcp://%s:%d", dns_host, dns_port)
}
}
table.insert(Xray.inbounds, {
listen = "127.0.0.1",
port = 5335,
protocol = "dokodemo-door",
settings = {
address = dns_host,
port = dns_port,
network = "tcp,udp"
},
tag = "builtin-dns-in"
})
xray_builtin_dns = {
address = dns_host,
port = dns_port
}
end
-- 传入连接
-- 添加 dokodemo-door 配置,如果 local_port 不为 0
if local_port ~= "0" then
@@ -349,11 +432,11 @@ if proto and proto:find("tcp") and socks_port ~= "0" then
-- socks
protocol = "socks",
port = tonumber(socks_port),
settings = {
settings = {
auth = socks_server.socks5_auth or "noauth",
udp = true,
mixed = ((socks_server.socks5_mixed == '1') and true or false) or (socks_server.server == 'same') and nil,
accounts = (socks_server.server ~= "same" and (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth")) and {
mixed = ((socks_server.socks5_mixed == '1') and true or false) or nil,
accounts = (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth") and {
{
user = socks_server.socks5_user,
pass = socks_server.socks5_pass
@@ -529,7 +612,7 @@ Xray.outbounds = {
local finalmask = {}
local PT = server.v2ray_protocol
local TP = server.transport
if server.transport == "kcp" then
if TP == "kcp" then
local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
local udp = {}
@@ -547,14 +630,36 @@ Xray.outbounds = {
udp[#udp+1] = c
finalmask.udp = udp
elseif PT == "hysteria2" then
local udp = {}
if (server.flag_obfs == "1" and (server.obfs_type and server.obfs_type ~= "")) then
finalmask.udp = {{
type = server.obfs_type,
local o = {
type = "salamander",
settings = server.salamander and {
password = server.salamander
password = server.salamander,
packetSize = server.obfs_type == "gecko" and "512-1200" or nil
} or nil
}}
}
udp[#udp+1] = o
end
if server.hysteria2_realms then
local realm = parse_realm_uri(server.hysteria2_realm_url)
local url, stun
if realm then
if realm.token and realm.server_url and realm.realm_id then
url = "realm://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
end
stun = realm.stun_servers or server.hysteria2_realm_stun
end
local r = {
type = "realm",
settings = {
url = url,
stunServers = stun
}
}
udp[#udp+1] = r
end
finalmask.udp = udp
local up = tonumber(server.uplink_capacity) or 0
local down = tonumber(server.downlink_capacity) or 0
finalmask.quicParams = {
@@ -642,7 +747,6 @@ Xray.outbounds = {
end)(),
sockopt = {
mark = 255,
domainStrategy = server.domain_strategy or "UseIP",
tcpFastOpen = (function()
if server.transport == "xhttp" then
return (server.fast_open == "1") and true or false
@@ -670,18 +774,24 @@ Xray.outbounds = {
}
}
table.insert(Xray.outbounds, {
protocol = "freedom",
tag = "direct",
settings = {
domainStrategy = server.domain_strategy or "UseIP" -- 可根据需要改为 direct_dns_query_strategy
},
streamSettings = {
sockopt = {
mark = 255
if xray_builtin_dns then
table.insert(Xray.outbounds, {
protocol = "dns",
tag = "builtin-dns-out",
settings = {
network = "tcp",
address = xray_builtin_dns.address,
port = xray_builtin_dns.port
}
}
})
})
Xray.routing = Xray.routing or {}
Xray.routing.rules = Xray.routing.rules or {}
table.insert(Xray.routing.rules, {
type = "field",
inboundTag = { "builtin-dns-in" },
outboundTag = "builtin-dns-out"
})
end
-- 添加带有 fragment 设置的 dialerproxy 配置
if xray_fragment.fragment ~= "0" or (xray_fragment.noise ~= "0" and xray_noise.enabled ~= "0") then
@@ -712,121 +822,6 @@ if xray_fragment.fragment ~= "0" or (xray_fragment.noise ~= "0" and xray_noise.e
})
end
-- Xray DNS 解析配置
if datatypes.hostname(server.server) and server.domain_resolver and (server.domain_resolver_dns or server.domain_resolver_dns_https) then
-- 解析 DNS 服务器配置
local dns_proto = server.domain_resolver
local config_address
local config_port
if dns_proto == "https" then
local _a = parseURL(server.domain_resolver_dns_https)
if _a then
config_address = server.domain_resolver_dns_https
config_port = _a.port or 443
if _a.hostname and datatypes.hostname(_a.hostname) then
GLOBAL.DNS_HOSTNAME[_a.hostname] = true
end
end
else
local server_address = server.domain_resolver_dns
config_port = 53
local parts = split(server_address, ":")
if #parts > 1 then
server_address = parts[1]
config_port = tonumber(parts[#parts])
end
config_address = server_address
if dns_proto == "tcp" then
config_address = dns_proto .. "://" .. server_address .. ":" .. config_port
end
end
-- 存入 GLOBAL.DNS_SERVER(去重)
local dns_key = dns_proto .. "|" .. config_address .. "|" .. tostring(config_port)
if not GLOBAL.DNS_SERVER[dns_key] then
GLOBAL.DNS_SERVER[dns_key] = {
tag = "dns-node-" .. node_id,
address = config_address,
port = config_port,
finalQuery = true,
disableCache = false,
serveStale = true,
serveExpiredTTL = 30,
domains = {}
}
end
-- 添加当前节点域名到该 DNS 服务器的 domains 列表
local domain = "full:" .. server.server
local exists
for _, d in ipairs(GLOBAL.DNS_SERVER[dns_key].domains) do
if d == domain then exists = true; break end
end
if not exists then
table.insert(GLOBAL.DNS_SERVER[dns_key].domains, domain)
end
GLOBAL.VPS_EXCLUDE[server.server] = true
-- 构建 Xray.dns
local dns_servers = { "localhost" }
for key, dns_server in pairs(GLOBAL.DNS_SERVER) do
table.insert(dns_servers, {
tag = dns_server.tag,
address = dns_server.address,
port = dns_server.port,
domains = dns_server.domains,
finalQuery = dns_server.finalQuery,
serveStale = dns_server.serveStale,
serveExpiredTTL = dns_server.serveExpiredTTL,
disableCache = dns_server.disableCache
})
end
Xray.dns = {
servers = dns_servers,
disableFallback = true,
disableFallbackIfMatch = true,
useSystemHosts = true,
queryStrategy = "UseIP",
disableCache = false,
tag = "dns-global" -- 用于 routing
}
end
-- 代理出站的 tag(与 outbound 中的 tag 保持一致)
local proxy_tag = (remarks ~= nil and remarks ~= "") and (node_id .. ":" .. remarks) or node_id
-- 构建 routing 规则表
local routing_rules = {}
-- 1. 为每个自定义 DNS 服务器添加直连规则(inboundTag 匹配 dns-node-xxx)
if GLOBAL.DNS_SERVER and next(GLOBAL.DNS_SERVER) then
for _, dns_server in pairs(GLOBAL.DNS_SERVER) do
table.insert(routing_rules, {
inboundTag = { dns_server.tag },
outboundTag = "direct"
})
end
end
-- 2. 添加全局 DNS 直连规则(需要 Xray.dns 中已设置 tag = "dns-global")
if Xray.dns and Xray.dns.tag then
table.insert(routing_rules, {
inboundTag = { "dns-global" },
outboundTag = "direct"
})
end
-- 3. 添加默认规则:所有 TCP/UDP 流量走代理
table.insert(routing_rules, {
network = "tcp,udp",
ruleTag = "default",
outboundTag = proxy_tag
})
-- 构建 routing 对象
Xray.routing = {
rules = routing_rules,
domainStrategy = "AsIs"
}
local cipher = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:AES128-SHA:AES256-SHA:DES-CBC3-SHA"
local cipher13 = "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384"
local trojan = {
@@ -874,7 +869,7 @@ local trojan = {
}
}
local naiveproxy = {
proxy = (server.username and server.password and server.server and server.server_port) and "https://" .. server.username .. ":" .. server.password .. "@" .. server.server .. ":" .. server.server_port,
proxy = (server.username and server.password and server.server and server.server_port) and "https://" .. server.username .. ":" .. server.password .. "@" .. format_host_port(server.server, server.server_port),
listen = (proto == "redir") and "redir" .. "://0.0.0.0:" .. tonumber(local_port) or "socks" .. "://0.0.0.0:" .. tonumber(local_port),
["insecure-concurrency"] = tonumber(server.concurrency) or 1
}
@@ -895,16 +890,16 @@ local hysteria2 = {
server.server_port and
(
server.port_range and
(server.server .. ":" .. server.server_port .. "," .. string.gsub(server.port_range, ":", "-"))
(format_host_port(server.server, server.server_port) .. "," .. string.gsub(server.port_range, ":", "-"))
or
(server.server .. ":" .. server.server_port)
(format_host_port(server.server, server.server_port))
)
or
(
server.port_range and
server.server .. ":" .. string.gsub(server.port_range, ":", "-")
format_host(server.server) .. ":" .. string.gsub(server.port_range, ":", "-")
or
server.server and server.server .. ":443"
server.server and format_host_port(server.server, "443")
)
),
bandwidth = (server.uplink_capacity or server.downlink_capacity) and {
@@ -1003,7 +998,7 @@ local hysteria2 = {
}
local shadowtls = {
client = {
server_addr = server.server_port and server.server .. ":" .. server.server_port or nil,
server_addr = server.server_port and format_host_port(server.server, server.server_port) or nil,
listen = "127.0.0.1:" .. tonumber(local_port),
tls_names = server.shadowtls_sni,
password = server.password
@@ -1017,7 +1012,7 @@ local chain_sslocal = {
locals = local_port ~= "0" and {
{
local_address = "0.0.0.0",
local_port = (chain_local_port == "0" and tonumber(server.local_port) or tonumber(chain_local_port)),
local_port = (chain_local_port == "0" and effective_node_local_port or tonumber(chain_local_port)),
mode = (proto:find("tcp,udp") and "tcp_and_udp") or proto .. "_only",
protocol = "redir",
tcp_redir = "redirect",
@@ -1046,7 +1041,7 @@ local chain_sslocal = {
local chain_vmess = {
inbounds = (local_port ~= "0") and {
{
port = (chain_local_port == "0" and tonumber(server.local_port) or tonumber(chain_local_port)),
port = (chain_local_port == "0" and effective_node_local_port or tonumber(chain_local_port)),
protocol = "dokodemo-door",
settings = {
network = proto,
@@ -1082,7 +1077,7 @@ local chain_vmess = {
}
local tuic = {
relay = {
server = server.server_port and server.server .. ":" .. server.server_port,
server = server.server_port and format_host_port(server.server, server.server_port),
ip = server.tuic_ip,
uuid = server.tuic_uuid,
password = server.tuic_passwd,
@@ -1,95 +0,0 @@
#!/bin/sh
argv1=$1
argv2=$2
argv3=$3
argv4=$4
argv5=$5
argv6=$6
argv7=$7
argv8=$8
argv9=$9
cat <<-EOF >$argv1
base {
log_debug = off;
log_info = off;
log = stderr;
daemon = on;
redirector = iptables;
reuseport = on;
}
EOF
tcp() {
if [ "$argv7" == "0" ]; then
cat <<-EOF >>$argv1
redsocks {
bind = "0.0.0.0:$argv4";
relay = "$argv5:$argv6";
type = socks5;
autoproxy = 0;
timeout = 10;
}
EOF
else
cat <<-EOF >>$argv1
redsocks {
bind = "0.0.0.0:$argv4";
relay = "$argv5:$argv6";
type = socks5;
autoproxy = 0;
timeout = 10;
login = "$argv8";
password = "$argv9";
}
EOF
fi
}
udp() {
if [ "$argv7" == "0" ]; then
cat <<-EOF >>$argv1
redudp {
bind = "0.0.0.0:$argv4";
relay = "$argv5:$argv6";
type = socks5;
udp_timeout = 10;
}
EOF
else
cat <<-EOF >>$argv1
redudp {
bind = "0.0.0.0:$argv4";
relay = "$argv5:$argv6";
type = socks5;
udp_timeout = 10;
login = "$argv8";
password = "$argv9";
}
EOF
fi
}
case "$argv2" in
socks5)
case "$argv3" in
tcp)
tcp
;;
udp)
udp
;;
*)
tcp
udp
;;
esac
;;
*)
cat <<-EOF >>$argv1
redsocks {
bind = "0.0.0.0:$argv4";
type = direct;
interface = $argv3;
autoproxy = 0;
timeout = 10;
}
EOF
;;
esac
@@ -18,38 +18,12 @@ case "$USE_TABLES" in
exit 1
;;
esac
netflix() {
local port="$1"
if [ -f "$TMP_DNSMASQ_PATH/gfw_list.conf" ] && [ -s /etc/ssrplus/netflix.list ]; then
grep -vE '^\s*#|^\s*$' /etc/ssrplus/netflix.list > /tmp/ssrplus_netflix.list.clean
if [ -s /tmp/ssrplus_netflix.list.clean ]; then
grep -v -F -f /tmp/ssrplus_netflix.list.clean "$TMP_DNSMASQ_PATH/gfw_list.conf" > "$TMP_DNSMASQ_PATH/gfw_list.conf.tmp"
mv "$TMP_DNSMASQ_PATH/gfw_list.conf.tmp" "$TMP_DNSMASQ_PATH/gfw_list.conf"
if [ -f "$TMP_DNSMASQ_PATH/gfw_base.conf" ]; then
grep -v -F -f /tmp/ssrplus_netflix.list.clean "$TMP_DNSMASQ_PATH/gfw_base.conf" > "$TMP_DNSMASQ_PATH/gfw_base.conf.tmp"
mv "$TMP_DNSMASQ_PATH/gfw_base.conf.tmp" "$TMP_DNSMASQ_PATH/gfw_base.conf"
fi
fi
rm -f /tmp/ssrplus_netflix.list.clean
fi
if [ "$nft_support" = "1" ]; then
# 移除 ipset
cat /etc/ssrplus/netflix.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/server=\/&\/127.0.0.1#$port\nnftset=\/&\/inet#ss_spec#netflix/" >$TMP_DNSMASQ_PATH/netflix_forward.conf
elif [ "$nft_support" = "0" ]; then
cat /etc/ssrplus/netflix.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/server=\/&\/127.0.0.1#$port\nipset=\/&\/netflix/" >$TMP_DNSMASQ_PATH/netflix_forward.conf
fi
}
mkdir -p $TMP_DNSMASQ_PATH
run_mode=$(uci_get_by_type global run_mode router)
run_mode=$(normalize_run_mode)
if [ "$run_mode" = "oversea" ]; then
cp -rf /etc/ssrplus/oversea_list.conf $TMP_DNSMASQ_PATH/
else
cp -rf /etc/ssrplus/gfw_list.conf $TMP_DNSMASQ_PATH/
cp -rf /etc/ssrplus/gfw_base.conf $TMP_DNSMASQ_PATH/
fi
cp -rf /etc/ssrplus/gfw_list.conf $TMP_DNSMASQ_PATH/
cp -rf /etc/ssrplus/gfw_base.conf $TMP_DNSMASQ_PATH/
for conf_file in gfw_base.conf gfw_list.conf; do
conf="$TMP_DNSMASQ_PATH/$conf_file"
@@ -67,36 +41,41 @@ for conf_file in gfw_base.conf gfw_list.conf; do
fi
done
if [ "$(uci_get_by_type global netflix_enable 0)" == "1" ]; then
# 只有开启 NetFlix分流 才需要取值
SHUNT_SERVER=$(uci_get_by_type global netflix_server nil)
else
# 没有开启 设置为 nil
SHUNT_SERVER=nil
fi
case "$SHUNT_SERVER" in
nil)
rm -f $TMP_DNSMASQ_PATH/netflix_forward.conf
;;
$(uci_get_by_type global global_server nil) | $switch_server | same)
netflix $dns_port
;;
*)
netflix $tmp_shunt_dns_port
;;
esac
# 此处使用 for 方式读取 防止 /etc/ssrplus/ 目录下的 black.list white.list deny.list 等2个或多个文件一行中存在空格 比如:# abc.com 而丢失:server
# Optimize: Batch filter using grep
for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list; do
if [ -s "$list_file" ]; then
grep -vE '^\s*#|^\s*$' "$list_file" > "${list_file}.clean"
# 清理注释和空行
grep -vE '^\s*#|^\s*$' "$list_file" | sed 's/\r//g' > "${list_file}.clean"
if [ -s "${list_file}.clean" ]; then
for target_file in "$TMP_DNSMASQ_PATH/gfw_list.conf" "$TMP_DNSMASQ_PATH/gfw_base.conf"; do
if [ -f "$target_file" ]; then
grep -v -F -f "${list_file}.clean" "$target_file" > "${target_file}.tmp"
mv "${target_file}.tmp" "$target_file"
fi
[ -f "$target_file" ] || continue
tmp_file="${target_file}.tmp"
awk -v list="${list_file}.clean" '
BEGIN {
while ((getline line < list) > 0) {
gsub(/\r/, "", line)
if (line != "") {
domain[line] = 1
# 同时支持 *.domain
domain["*." line] = 1
}
}
close(list)
}
{
# 提取 server=/domain/xxx
if (match($0, /^server=\/([^\/]+)\//, m)) {
if (m[1] in domain) next
}
# 提取 ipset=/domain/xxx
if (match($0, /^ipset=\/([^\/]+)\//, m)) {
if (m[1] in domain) next
}
print
}
' "$target_file" > "$tmp_file"
mv "$tmp_file" "$target_file"
done
fi
rm -f "${list_file}.clean"
@@ -114,19 +93,45 @@ fi
cat /etc/ssrplus/deny.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/address=\/&\//" >$TMP_DNSMASQ_PATH/denylist.conf
if [ "$(uci_get_by_type global adblock 0)" == "1" ]; then
cp -f /etc/ssrplus/ad.conf $TMP_DNSMASQ_PATH/
cp -f /etc/ssrplus/ad.conf "$TMP_DNSMASQ_PATH/"
if [ -f "$TMP_DNSMASQ_PATH/ad.conf" ]; then
for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list /etc/ssrplus/netflix.list; do
for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list; do
if [ -s "$list_file" ]; then
grep -vE '^\s*#|^\s*$' "$list_file" > "${list_file}.clean"
# 清理注释 & 空行
grep -vE '^\s*#|^\s*$' "$list_file" | sed 's/\r//g' > "${list_file}.clean"
if [ -s "${list_file}.clean" ]; then
grep -v -F -f "${list_file}.clean" "$TMP_DNSMASQ_PATH/ad.conf" > "$TMP_DNSMASQ_PATH/ad.conf.tmp"
mv "$TMP_DNSMASQ_PATH/ad.conf.tmp" "$TMP_DNSMASQ_PATH/ad.conf"
tmp_file="$TMP_DNSMASQ_PATH/ad.conf.tmp"
awk -v list="${list_file}.clean" '
BEGIN {
while ((getline line < list) > 0) {
gsub(/\r/, "", line)
if (line != "") {
domain[line] = 1
# 支持泛域名
domain["*." line] = 1
}
}
close(list)
}
{
keep = 1
# 精确匹配 server=/domain/
if (match($0, /^server=\/([^\/]+)\//, m)) {
if (m[1] in domain) keep = 0
}
# 精确匹配 ipset=/domain/
if (match($0, /^ipset=\/([^\/]+)\//, m)) {
if (m[1] in domain) keep = 0
}
if (keep) print
}
' "$TMP_DNSMASQ_PATH/ad.conf" > "$tmp_file"
mv "$tmp_file" "$TMP_DNSMASQ_PATH/ad.conf"
fi
rm -f "${list_file}.clean"
fi
done
fi
else
rm -f $TMP_DNSMASQ_PATH/ad.conf
rm -f "$TMP_DNSMASQ_PATH/ad.conf"
fi
@@ -1,175 +0,0 @@
#!/bin/sh
# /usr/share/shadowsocksr/test.sh
CONFIG="shadowsocksr"
LOCK_PATH=/tmp/lock
TMP_PATH=/var/etc/ssrplus
uci_get_by_name() {
local ret=$(uci -q get $CONFIG.$1.$2 2>/dev/null)
echo "${ret:=$3}"
}
uci_get_by_type() {
local ret=$(uci -q get $CONFIG.@$1[0].$2 2>/dev/null)
echo "${ret:=$3}"
}
check_port_exists() {
local port=$1
local protocol=$2
[ -n "$protocol" ] || protocol="tcp,udp"
local result=
if [ "$protocol" = "tcp" ]; then
result=$(netstat -tln | grep -c ":$port ")
elif [ "$protocol" = "udp" ]; then
result=$(netstat -uln | grep -c ":$port ")
elif [ "$protocol" = "tcp,udp" ]; then
result=$(netstat -tuln | grep -c ":$port ")
fi
echo "${result}"
}
set_cache_var() {
local key="${1}"
shift 1
local val="$@"
[ -n "${key}" ] && [ -n "${val}" ] && {
sed -i "/${key}=/d" $TMP_PATH/var >/dev/null 2>&1
echo "${key}=\"${val}\"" >> $TMP_PATH/var
eval ${key}=\"${val}\"
}
}
get_cache_var() {
local key="${1}"
[ -n "${key}" ] && [ -s "$TMP_PATH/var" ] && {
echo $(cat $TMP_PATH/var | grep "^${key}=" | awk -F '=' '{print $2}' | tail -n 1 | awk -F'"' '{print $2}')
}
}
#uci_get_by_port() {
# local port=$1
# while netstat -tuln 2>/dev/null | grep -q ":${port} "; do
# port=$((port + 1))
# done
# echo $port
uci_get_by_port() {
local default_start_port=2001
local min_port=1025
local max_port=49151
local port="$1"
local protocol=$(echo "$2" | tr 'A-Z' 'a-z')
local LOCK_FILE="${LOCK_PATH}/${CONFIG}_get_prot.lock"
while ! mkdir "$LOCK_FILE" 2>/dev/null; do
sleep 1
done
if [ "$port" = "auto" ]; then
local now last_time diff last_port
now=$(date +%s 2>/dev/null)
last_time=$(get_cache_var "last_get_new_port_time")
if [ -n "$now" ] && [ -n "$last_time" ]; then
diff=$(expr "$now" - "$last_time")
[ "$diff" -lt 0 ] && diff=$(expr 0 - "$diff")
else
diff=999
fi
if [ "$diff" -gt 10 ]; then
port=$default_start_port
else
last_port=$(get_cache_var "last_get_new_port_auto")
if [ -n "$last_port" ]; then
port=$(expr "$last_port" + 1)
else
port=$default_start_port
fi
fi
fi
[ "$port" -lt $min_port -o "$port" -gt $max_port ] && port=$default_start_port
local start_port="$port"
while :; do
if [ "$(check_port_exists "$port" "$protocol")" = 0 ]; then
break
fi
port=$(expr "$port" + 1)
if [ "$port" -gt $max_port ]; then
port=$min_port
fi
[ "$port" = "$start_port" ] && {
rmdir "$LOCK_FILE" 2>/dev/null
return 1
}
done
if [ "$1" = "auto" ]; then
set_cache_var "last_get_new_port_auto" "$port"
[ -n "$now" ] && set_cache_var "last_get_new_port_time" "$now"
fi
rmdir "$LOCK_FILE" 2>/dev/null
echo "$port"
}
url_test_hy2() {
local node_id=$1
# 读取配置
local server=$(uci_get_by_name ${node_id} server)
local port=$(uci_get_by_name ${node_id} server_port)
local auth=$(uci_get_by_name ${node_id} hy2_auth)
local tls=$(uci_get_by_name ${node_id} tls)
local insecure=$(uci_get_by_name ${node_id} insecure)
local tls_host=$(uci_get_by_name ${node_id} tls_host)
# 获取本地端口
# local tmp_port=$(uci_get_by_port 48900 tcp,udp)
local tmp_port=$(uci_get_by_port auto tcp,udp)
# 生成Hysteria2配置文件
local config_file="/tmp/hy2_test_${node_id}.yaml"
cat > "$config_file" <<-EOF
server: ${server}:${port}
auth: "${auth}"
tls:
insecure: true
EOF
# 如果 tls_host 非空,动态添加 sni 行
[ -n "$tls_host" ] && echo " sni: \"${tls_host}\"" >> "$config_file"
# 追加 socks5 监听配置
cat >> "$config_file" <<-EOF
socks5:
listen: 127.0.0.1:${tmp_port}
EOF
# echo "Debug: 配置文件已生成: $config_file" >&2
# 启动Hysteria2客户端
hysteria client --disable-update-check -c "$config_file" >/dev/null 2>&1 &
local pid=$!
echo $pid > "/tmp/hy2_test_${node_id}.pid"
# 等待端口启动
sleep 1
# 测试代理
# local result=$(curl --connect-timeout 3 --max-time 3 -s -o /dev/null -I -w "%{http_code}:%{time_pretransfer}" --socks5 127.0.0.1:${tmp_port} "${probeUrl}" 2>/dev/null)
local curlx="socks5h://127.0.0.1:${tmp_port}"
local probeUrl=$(uci_get_by_type server_subscribe url_test_url https://www.google.com/generate_204)
local result=$(curl --connect-timeout 3 --max-time 5 -o /dev/null -I -skL -w "%{http_code}:%{time_pretransfer}" -x ${curlx} "${probeUrl}" 2>/dev/null)
# 清理
# kill -9 $pid 2>/dev/null
local pid_file="/tmp/hy2_test_${node_id}.pid"
[ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1
pgrep -af "hysteria.*${config_file}" | awk '! /test\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1
rm -f "$config_file" "$pid_file"
echo $result
}
case $1 in
url_test_hy2)
url_test_hy2 $2
;;
esac
@@ -1,35 +1,35 @@
config global
option global_server 'nil'
option netflix_server 'nil'
option netflix_proxy '0'
option threads '0'
option run_mode 'router'
option dports '2'
option custom_ports '80,443'
option pdnsd_enable '1'
option prefer_nft '1'
option filter_aaaa '1'
option tunnel_forward '8.8.4.4:53'
option monitor_enable '1'
option enable_switch '1'
option switch_time '667'
option switch_timeout '5'
option switch_try_count '3'
option shunt_dns '1'
option default_node_local_port '1234'
option component_mirror 'direct'
option gfwlist_url 'https://fastly.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt'
option chnroute_url 'https://ispip.clang.cn/all_cn.txt'
option nfip_url 'https://fastly.jsdelivr.net/gh/QiuSimons/Netflix_IP/NF_only.txt'
option adblock_url 'https://anti-ad.net/anti-ad-for-dnsmasq.conf'
config server_subscribe
option proxy '0'
option proxy '1'
option auto_update '1'
option config_auto_update_mode '0'
option config_update_interval '60'
option auto_update_week_time '*'
option auto_update_day_time '2'
option auto_update_min_time '0'
option url_test_url 'https://www.google.com/generate_204'
option user_agent 'v2rayN/9.99'
option filter_words '过期/套餐/剩余/QQ群/官网/防失联/回国'
option filter_words '过期/套餐/剩余/网址/QQ群/官网/防失联/回国'
config access_control
option lan_ac_mode '0'
@@ -47,6 +47,11 @@ config socks5_proxy
option server 'nil'
option local_port '1080'
config http_proxy
option server 'nil'
option local_port '3128'
option http_auth 'none'
config server_global
option enable_server '0'
@@ -54,3 +59,9 @@ config global_xray_fragment
option fragment '0'
option noise '0'
config clash_client_group
option enabled '0'
option remarks ''
option ip_addr ''
option client_mac ''
option policy_group ''
@@ -1,6 +1,43 @@
#!/bin/sh
LOCK_DIR="/var/run/ssrplusupdate.lock"
LOOP_STAMP="/var/run/ssrplusupdate.loop"
MODE="$(uci -q get shadowsocksr.@server_subscribe[0].config_auto_update_mode 2>/dev/null || echo 0)"
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT INT TERM
if [ "$1" = "loop" ]; then
[ "$(uci -q get shadowsocksr.@server_subscribe[0].auto_update 2>/dev/null || echo 0)" = "1" ] || exit 0
[ "$MODE" = "1" ] || exit 0
INTERVAL="$(uci -q get shadowsocksr.@server_subscribe[0].config_update_interval 2>/dev/null || echo 60)"
case "$INTERVAL" in
''|*[!0-9]*)
INTERVAL=60
;;
esac
[ "$INTERVAL" -gt 0 ] 2>/dev/null || INTERVAL=60
NOW="$(date +%s)"
LAST_RUN=0
[ -f "$LOOP_STAMP" ] && LAST_RUN="$(cat "$LOOP_STAMP" 2>/dev/null || echo 0)"
case "$LAST_RUN" in
''|*[!0-9]*)
LAST_RUN=0
;;
esac
if [ $((NOW - LAST_RUN)) -lt $((INTERVAL * 60)) ]; then
exit 0
fi
fi
/usr/bin/lua /usr/share/shadowsocksr/update.lua
sleep 2s
/usr/share/shadowsocksr/chinaipset.sh /var/etc/ssrplus/china_ssr.txt
sleep 2s
/usr/bin/lua /usr/share/shadowsocksr/subscribe.lua
if [ "$1" = "loop" ]; then
date +%s > "$LOOP_STAMP"
fi
File diff suppressed because it is too large. Load diff
@@ -250,10 +250,6 @@ if args then
update(uci:get_first("shadowsocksr", "global", "adblock_url"), "/etc/ssrplus/ad.conf", args, TMP_DNSMASQ_PATH .. "/ad.conf")
os.exit(0)
end
if args == "nfip_data" then
update(uci:get_first("shadowsocksr", "global", "nfip_url"), "/etc/ssrplus/netflixip.list", args, TMP_DNSMASQ_PATH .. "/netflixip.list")
os.exit(0)
end
else
log("正在更新【GFW列表】数据库")
update(uci:get_first("shadowsocksr", "global", "gfwlist_url"), "/etc/ssrplus/gfw_list.conf", "gfw_data", TMP_DNSMASQ_PATH .. "/gfw_list.conf")
@@ -267,10 +263,4 @@ else
log("正在更新【广告屏蔽】数据库")
update(uci:get_first("shadowsocksr", "global", "adblock_url"), "/etc/ssrplus/ad.conf", "ad_data", TMP_DNSMASQ_PATH .. "/ad.conf")
end
if uci:get_first("shadowsocksr", "global", "netflix_enable", "0") == "1" then
log("正在更新【Netflix IP段】数据库")
update(uci:get_first("shadowsocksr", "global", "nfip_url"), "/etc/ssrplus/netflixip.list", "nfip_data", TMP_DNSMASQ_PATH .. "/netflixip.list")
end
-- log("正在更新【Netflix IP段】数据库")
-- update(uci:get_first("shadowsocksr", "global", "nfip_url"), "/etc/ssrplus/netflixip.list", "nfip_data")
end
File diff suppressed because it is too large. Load diff
@@ -1,4 +1,7 @@
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
.btn,
.cbi-button {
@apply inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-2xl border px-3 py-1.5 text-sm font-medium antialiased shadow-sm max-md:text-base max-md:font-medium;
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=0.12.0
PKG_RELEASE:=17
PKG_VERSION:=0.12.1
PKG_RELEASE:=18
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=
@@ -1,2 +1,2 @@
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-leading:initial;--tw-font-weight:initial;--tw-duration:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-content:"";--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-space-y-reverse:0;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-2xl:calc(var(--radius-base)*2);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,Line truncated
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0;--tw-leading:initial;--tw-font-weight:initial;--tw-duration:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-content:"";--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-space-y-reverse:0;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-2xl:calc(var(--radius-base)*2);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--container-max-width:var(--container-max-width)}*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,Line truncated
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
VERSION="$1"
CURDIR="$2"
BIN_PATH="$3"
if [ -d "$CURDIR/.git" ]; then
config="$CURDIR/.git/config"
else
config="$(sed "s|^gitdir:\s*|$CURDIR/|;s|$|/config|" "$CURDIR/.git")"
fi
[ -n "$(sed -En '/^\[remote /{h;:top;n;/^\[/b;s,(https?://gitcode\.(com|net)),\1,;T top;H;x;s|\n\s*|: |;p;}' "$config")" ] && {
echo -e "#!/bin/sh\necho $VERSION" > "$BIN_PATH"
}
exit 0
+25 -30
View File
@@ -1,32 +1,30 @@
# SPDX-License-Identifier: GPL-2.0
#
# Copyright (C) 2024-2026 Anya Lin <hukk1996@gmail.com>
include $(TOPDIR)/rules.mk
PKG_NAME:=mihomo
PKG_VERSION:=1.19.26
PKG_RELEASE:=12
PKG_RELEASE:=13
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
PKG_SOURCE_VERSION:=v$(PKG_VERSION)
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/metacubex/mihomo/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MIRROR_HASH:=skip
PKG_MAINTAINER:=Anya Lin <hukk1996@gmail.com>
PKG_LICENSE:=GPL-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_LICENSE:=GPL3.0+
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com>
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=github.com/metacubex/mihomo
PKG_BUILD_VERSION:=v$(PKG_VERSION)
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
PKG_BUILD_TIME:=$(shell date -u +%FT%TZ%z)
GO_PKG_LDFLAGS_X:=\
$(GO_PKG)/constant.Version=v$(PKG_VERSION) \
$(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
GO_PKG:=github.com/metacubex/mihomo
GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
GO_PKG_TAGS:=with_gvisor
GO_PKG_INSTALL_BIN_PATH:=/usr/libexec
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
@@ -34,27 +32,24 @@ include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/mihomo
SECTION:=net
CATEGORY:=Network
TITLE:=Another Mihomo Kernel.
TITLE:=A rule based proxy in Go.
URL:=https://wiki.metacubex.one
DEPENDS:=$(GO_ARCH_DEPENDS)
PROVIDES:=mihomo
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
ALTERNATIVES:=\
100:/usr/bin/mihomo:/usr/libexec/mihomo-core
USERID:=mihomo=7890:mihomo=7890
300:/usr/bin/mihomo:/usr/libexec/mihomo
endef
define Package/mihomo/description
Mihomo is a rule based proxy in Go.
endef
define Package/mihomo/install
$(call GoPackage/Package/Install/Bin,$(1))
endef
define Build/Prepare
$(Build/Prepare/Default)
# rm unit test
rm -f $(PKG_BUILD_DIR)/rules/logic_test/logic_test.go
endef
define Package/mihomo/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(CURDIR)/.prepare.sh $(VERSION) $(CURDIR) $(PKG_INSTALL_DIR)/usr/bin/$(PKG_NAME)
$(INSTALL_DIR) $(1)/usr/libexec/
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/$(PKG_NAME) $(1)/usr/libexec/mihomo-core
$(RM) -r $(PKG_BUILD_DIR)/rules/logic_test
endef
$(eval $(call GoBinPackage,mihomo))
+28 -50
View File
@@ -1,65 +1,41 @@
#
# Copyright (C) 2024 sbwml <admin@cooluc.com>
# Copyright (c) 2025-2026 sirpdboy <herboy2008@gmail.com>
#
# This is free software, licensed under the GPL-3.0 License.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=ookla-speedtest
PKG_VERSION:=1.2.0
PKG_RELEASE:=5
PKG_RELEASE:=6
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
ifeq ($(ARCH),aarch64)
PKG_ARCH_SUFFIX:=aarch64
PKG_HASH:=skip
else ifeq ($(ARCH),arm)
ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE))))
ifeq ($(ARM_CPU_FEATURES),)
PKG_ARCH_SUFFIX:=armel
PKG_HASH:=skip
else
PKG_ARCH_SUFFIX:=armhf
PKG_HASH:=skip
endif
else ifeq ($(ARCH),i386)
PKG_ARCH_SUFFIX:=i386
PKG_HASH:=skip
else ifeq ($(ARCH),x86_64)
PKG_ARCH_SUFFIX:=x86_64
PKG_HASH:=skip
endif
PKG_SOURCE:=ookla-speedtest-$(PKG_VERSION)-linux-$(PKG_ARCH_SUFFIX).tgz
PKG_SOURCE_URL:=https://install.speedtest.net/app/cli
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)-$(PKG_ARCH_SUFFIX)
PKG_MAINTAINER:=sbwml <admin@cooluc.com>
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
SECTION:=net
CATEGORY:=Network
SUBMENU:=Speed Test
TITLE:=Speedtest CLI by Ookla
URL:=https://www.speedtest.net/
DEPENDS:=@(aarch64||arm||i386||x86_64) +ca-certificates
URL:=https://www.speedtest.net/
TITLE:=Speedtest from Ookla
SECTION:=net
CATEGORY:=Network
endef
ifeq ($(ARCH),x86_64)
BIN_PATH:=./files/x86_64
endif
ifeq ($(ARCH),i386)
BIN_PATH:=./files/i386
endif
ifeq ($(ARCH),arm)
BIN_PATH:=./files/armhf
endif
ifeq ($(ARCH),aarch64)
BIN_PATH:=./files/aarch64
endif
define Package/$(PKG_NAME)/description
Speedtest CLI by Ookla is the official command line client
for testing internet bandwidth using speedtest.net servers.
Speedtest CLI brings the trusted technology and global server network behind Speedtest to the command line.
Built for software developers, system administrators and computer enthusiasts alike,
Speedtest CLI is the first official Linux-native Speedtest application backed by Ookla®.
endef
define Build/Prepare
( \
pushd $(PKG_BUILD_DIR) ; \
$(TAR) -zxf $(DL_DIR)/ookla-speedtest-$(PKG_VERSION)-linux-$(PKG_ARCH_SUFFIX).tgz -C . ; \
popd ; \
)
endef
define Build/Configure
endef
define Build/Compile
@@ -67,7 +43,9 @@ endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/speedtest $(1)/usr/bin/ookla-speedtest
$(CP) $(BIN_PATH)/speedtest $(1)/usr/bin
endef
$(eval $(call BuildPackage,$(PKG_NAME)))
ifeq ($(ARCH),$(filter $(ARCH), x86_64 i386 arm aarch64))
$(eval $(call BuildPackage,$(PKG_NAME)))
endif
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4 -2
View File
@@ -14,12 +14,14 @@ include $(TOPDIR)/rules.mk
#
PKG_NAME:=shadowsocks-libev
PKG_VERSION:=3.3.5
PKG_RELEASE:=3
PKG_RELEASE:=4
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/shadowsocks/shadowsocks-libev.git
PKG_SOURCE_DATE:=2025-1-20
PKG_SOURCE_VERSION:=9afa3cacf947f910be46b69fc5a7a1fdd02fd5e6
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_SOURCE:=$(PKG_SOURCE_SUBDIR).tar.xz
PKG_MIRROR_HASH:=skip
PKG_MAINTAINER:=Yousong Zhou <yszhou4tech@gmail.com>
@@ -53,7 +55,7 @@ define Package/shadowsocks-libev-config/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_DATA) ./files/shadowsocks-libev.config $(1)/etc/config/shadowsocks-libev
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/shadowsocks-libev.init $(1)/etc/init.d/shadowsocks-libev
# $(INSTALL_BIN) ./files/shadowsocks-libev.init $(1)/etc/init.d/shadowsocks-libev
endef
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=sing-box
PKG_VERSION:=1.13.12
PKG_RELEASE:=16
PKG_VERSION:=1.13.13
PKG_RELEASE:=17
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
-70
View File
@@ -1,70 +0,0 @@
#
# Copyright (C) 2018-2019 wongsyrone
#
# This is free software, licensed under the GNU General Public License v3.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=trojan
PKG_VERSION:=1.16.0
PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/trojan-gfw/trojan/tar.gz/v$(PKG_VERSION)?
PKG_HASH:=skip
PKG_BUILD_PARALLEL:=1
PKG_BUILD_DEPENDS:=openssl
PKG_LICENSE:=GPL-3.0
PKG_LICENSE_FILE:=LICENSE
PKG_MAINTAINER:=GreaterFire
include $(INCLUDE_DIR)/package.mk
include $(INCLUDE_DIR)/cmake.mk
include ./boost-version.mk
TARGET_CXXFLAGS += -Wall -Wextra
TARGET_CXXFLAGS += $(FPIC)
# LTO
TARGET_CXXFLAGS += -flto
TARGET_LDFLAGS += -flto
# CXX standard
TARGET_CXXFLAGS += -std=c++11
TARGET_CXXFLAGS := $(filter-out -O%,$(TARGET_CXXFLAGS)) -O3
TARGET_CXXFLAGS += -ffunction-sections -fdata-sections
TARGET_LDFLAGS += -Wl,--gc-sections
CMAKE_OPTIONS += \
-DENABLE_MYSQL=OFF \
-DENABLE_NAT=ON \
-DENABLE_REUSE_PORT=ON \
-DENABLE_SSL_KEYLOG=ON \
-DENABLE_TLS13_CIPHERSUITES=ON \
-DFORCE_TCP_FASTOPEN=OFF \
-DSYSTEMD_SERVICE=OFF \
-DOPENSSL_USE_STATIC_LIBS=FALSE \
-DBoost_DEBUG=ON \
-DBoost_NO_BOOST_CMAKE=ON
define Package/trojan
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
TITLE:=An unidentifiable mechanism that helps you bypass GFW
URL:=https://github.com/trojan-gfw/trojan
DEPENDS:= \
+libpthread +libstdcpp +libopenssl \
+boost +boost-program_options +boost-date_time \
$(if $(filter y,$(NEED_BOOST_SYSTEM)),,+boost-system)
endef
define Package/trojan/install
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/trojan $(1)/usr/sbin/trojan
endef
$(eval $(call BuildPackage,trojan))
-12
View File
@@ -1,12 +0,0 @@
# boost-version.mk
BOOST_MAKEFILE := $(firstword $(shell find -L $(TOPDIR) -type f -path "*/boost/Makefile"))
BOOST_PKG_VERSION := $(shell grep '^PKG_VERSION:=' $(BOOST_MAKEFILE) | head -n1 | cut -d= -f2)
BOOST_VER_MAJOR := $(word 1,$(subst ., ,$(BOOST_PKG_VERSION)))
BOOST_VER_MINOR := $(word 2,$(subst ., ,$(BOOST_PKG_VERSION)))
BOOST_VER_PATCH := $(word 3,$(subst ., ,$(BOOST_PKG_VERSION)))
BOOST_VERSION_CODE := $(shell echo $$(($(BOOST_VER_MAJOR)*100000 + $(BOOST_VER_MINOR)*100 + $(BOOST_VER_PATCH))))
NEED_BOOST_SYSTEM := $(if $(shell [ $(BOOST_VERSION_CODE) -ge 108900 ] && echo y),y,n)
@@ -1,11 +0,0 @@
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -43,7 +43,7 @@ if(MSVC)
add_definitions(-DBOOST_DATE_TIME_NO_LIB)
endif()
-find_package(OpenSSL 1.1.0 REQUIRED)
+find_package(OpenSSL 1.1.1 REQUIRED)
include_directories(${OPENSSL_INCLUDE_DIR})
target_link_libraries(trojan ${OPENSSL_LIBRARIES})
if(OPENSSL_VERSION VERSION_GREATER_EQUAL 1.1.1)
@@ -1,16 +0,0 @@
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -36,7 +36,12 @@ set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
target_link_libraries(trojan ${CMAKE_THREAD_LIBS_INIT})
-find_package(Boost 1.66.0 REQUIRED COMPONENTS system program_options)
+find_package(Boost 1.66.0 REQUIRED)
+if (Boost_MAJOR_VERSION LESS_EQUAL 1 AND Boost_MINOR_VERSION LESS 89)
+ find_package(Boost 1.66.0 REQUIRED COMPONENTS system program_options)
+else()
+ find_package(Boost 1.66.0 REQUIRED COMPONENTS program_options)
+endif()
include_directories(${Boost_INCLUDE_DIR})
target_link_libraries(trojan ${Boost_LIBRARIES})
if(MSVC)