🗽 Sync 2026-06-03 14:05:40

This commit is contained in:
github-actions[bot] committed 2026-06-03 14:05:40 +08:00
1 parent 82314810a3
commit 50bf1ec00a
61 files changed
+6813 -15676

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=dns2tcp PKG_NAME:=dns2tcp
PKG_VERSION:=1.1.2 PKG_VERSION:=1.1.2
PKG_RELEASE:=2 PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/zfl9/dns2tcp/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/zfl9/dns2tcp/tar.gz/v$(PKG_VERSION)?
@@ -1,210 +0,0 @@
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];
+94 -69
View File
@@ -3,51 +3,77 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=luci-app-ssr-plus LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=193 PKG_VERSION:=190
PKG_RELEASE:=16 PKG_RELEASE:=17
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG \ 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_DNS2TCP \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNSPROXY \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \ 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_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_NONE_Server \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Trojan
LUCI_TITLE:=SS/SSR/V2Ray/Trojan/NaiveProxy/Tuic/ShadowTLS/Hysteria/Socks5/Clash LuCI interface LUCI_TITLE:=SS/SSR/V2Ray/Trojan/NaiveProxy/Tuic/ShadowTLS/Hysteria/Socks5/Tun LuCI interface
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
LUCI_DEPENDS:= \ LUCI_DEPENDS:= \
+libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \ +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \ +jq +ip-full +lua +lua-neturl +libuci-lua +microsocks \
+resolveip +curl +nping +unzip +xz-utils \ +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 \
+PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG:chinadns-ng \ +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_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_Shadow_TLS:shadow-tls \
+PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks:ipt2socks \
+PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun:kcptun-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun:kcptun-client \
+PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy:naiveproxy \ +PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy:naiveproxy \
+PACKAGE_$(PKG_NAME)_INCLUDE_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_Client:shadowsocks-rust-sslocal \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server:shadowsocks-rust-ssserver \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server:shadowsocks-rust-ssserver \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs:simple-obfs-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs:simple-obfs-client \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin:v2ray-plugin \ +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-local \
+PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-redir \ +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_ShadowsocksR_Libev_Server:shadowsocksr-libev-ssr-server \
+PACKAGE_$(PKG_NAME)_INCLUDE_Trojan:trojan
define Package/$(PKG_NAME)/config define Package/$(PKG_NAME)/config
select PACKAGE_luci-lua-runtime if PACKAGE_$(PKG_NAME)
choice choice
prompt "Transparent Proxy Backend" prompt "Transparent Proxy Backend"
@@ -77,24 +103,37 @@ endchoice
choice choice
prompt "Shadowsocks Client Selection" prompt "Shadowsocks Client Selection"
default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client if aarch64 || x86_64
default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client
bool "None" bool "None"
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client
bool "Shadowsocks-libev"
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client
bool "Shadowsocks Rust" bool "Shadowsocks-rust"
depends on aarch64||arm||i386||mips||mipsel||x86_64
depends on !(TARGET_x86_geode||TARGET_x86_legacy)
endchoice endchoice
choice choice
prompt "Shadowsocks Server Selection" 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 default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server
bool "None" bool "None"
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server
bool "Shadowsocks-libev"
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server
bool "Shadowsocks Rust" bool "Shadowsocks-rust"
depends on aarch64||arm||i386||mips||mipsel||x86_64
depends on !(TARGET_x86_geode||TARGET_x86_legacy)
endchoice endchoice
choice choice
@@ -105,35 +144,45 @@ choice
config PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY config PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY
bool "None" bool "None"
config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray
bool "V2ray-core"
config PACKAGE_$(PKG_NAME)_INCLUDE_Xray config PACKAGE_$(PKG_NAME)_INCLUDE_Xray
bool "Xray-core" bool "Xray-core"
select PACKAGE_xray-core
endchoice endchoice
config PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG config PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG
bool "Include ChinaDNS-NG" bool "Include ChinaDNS-NG"
default y if aarch64||arm||i386||x86_64 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
config PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS config PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS
bool "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 default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy config PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client
bool "Include HTTP(S) Proxy Server" bool "Include Tuic-Client"
select PACKAGE_3proxy select PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG
default y if (aarch64||arm||i386||loongarch64||riscv64||x86_64) select PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks
depends on aarch64||arm||i386||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo depends on !(TARGET_x86_geode||TARGET_x86_legacy)
bool "Include Mihomo (Clash Support)" default n
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 config PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS
bool "Include Shadow-TLS" bool "Include Shadow-TLS"
@@ -143,6 +192,10 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS
depends on !(TARGET_x86_geode||TARGET_x86_legacy) depends on !(TARGET_x86_geode||TARGET_x86_legacy)
default n default n
config PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks
bool "Include IPT2Socks"
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun config PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun
bool "Include Kcptun" bool "Include Kcptun"
default n default n
@@ -152,9 +205,13 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy
depends on !(arc||armeb||mips||mips64||powerpc||TARGET_gemini) depends on !(arc||armeb||mips||mips64||powerpc||TARGET_gemini)
default n default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Redsocks2
bool "Include Redsocks2"
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs
bool "Include Shadowsocks Simple Obfs Plugin" bool "Include Shadowsocks Simple Obfs Plugin"
default n default y
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin
bool "Include Shadowsocks V2ray Plugin" bool "Include Shadowsocks V2ray Plugin"
@@ -166,6 +223,11 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client
config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server
bool "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 default n
endef endef
@@ -178,41 +240,4 @@ endef
include $(TOPDIR)/feeds/luci/luci.mk 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 # call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,2 @@
/*! 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,7 +1,6 @@
local m, s, o local m, s, o
local cbi = require "luci.cbi" local cbi = require "luci.cbi"
local uci = require "luci.model.uci".cursor() local uci = require "luci.model.uci".cursor()
local URL = require "url"
-- 获取 LAN IP 地址 -- 获取 LAN IP 地址
function lanip() function lanip()
@@ -31,32 +30,16 @@ local lan_ip = lanip()
local server_table = {} local server_table = {}
local type_table = {} local type_table = {}
local function is_finded(e) local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= "" return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', 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 end
uci:foreach("shadowsocksr", "servers", function(s) uci:foreach("shadowsocksr", "servers", function(s)
if s.type ~= "tun" and s.alias then if s.alias then
server_table[s[".name"]] = "[%s]:%s" % {string.upper(s.v2ray_protocol or s.type), s.alias} server_table[s[".name"]] = "[%s]:%s" % {string.upper(s.v2ray_protocol or s.type), s.alias}
elseif s.type ~= "tun" and s.server and s.server_port then elseif 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} 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 end
if s.type and s.type ~= "tun" then if s.type then
type_table[s[".name"]] = s.type type_table[s[".name"]] = s.type
end end
end) end)
@@ -96,13 +79,6 @@ o.datatype = "uinteger"
o:depends("enable_switch", "1") o:depends("enable_switch", "1")
o.default = 3 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 = 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/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")) o:value("https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt", translate("Loyalsoldier/v2ray-rules-dat"))
@@ -116,6 +92,145 @@ 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: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.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 = 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.rmempty = false
o.default = "1" o.default = "1"
@@ -151,13 +266,45 @@ end
-- [[ SOCKS5 Proxy ]]-- -- [[ SOCKS5 Proxy ]]--
s = m:section(TypedSection, "socks5_proxy", translate("Global SOCKS5 Proxy Server")) s = m:section(TypedSection, "socks5_proxy", translate("Global SOCKS5 Proxy Server"))
s.anonymous = true 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 -- Enable/Disable Option
o = s:option(Flag, "enabled", translate("Enable")) o = s:option(Flag, "enabled", translate("Enable"))
o.default = 0 o.default = 0
o.rmempty = false 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 -- Socks Auth
if is_finded("xray") then if is_finded("xray") then
o = s:option(ListValue, "socks5_auth", translate("Socks5 Auth Mode"), translate("Socks protocol auth methods, default:noauth.")) o = s:option(ListValue, "socks5_auth", translate("Socks5 Auth Mode"), translate("Socks protocol auth methods, default:noauth."))
@@ -165,6 +312,13 @@ o.default = "noauth"
o:value("noauth", "NOAUTH") o:value("noauth", "NOAUTH")
o:value("password", "PASSWORD") o:value("password", "PASSWORD")
o.rmempty = true 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 -- Socks User
o = s:option(Value, "socks5_user", translate("Socks5 User"), translate("Only when Socks5 Auth Mode is password valid, Mandatory.")) o = s:option(Value, "socks5_user", translate("Socks5 User"), translate("Only when Socks5 Auth Mode is password valid, Mandatory."))
@@ -181,6 +335,13 @@ o:depends("socks5_auth", "password")
o = s:option(Flag, "socks5_mixed", translate("Enabled Mixed"), translate("Mixed as an alias of socks, default:Enabled.")) o = s:option(Flag, "socks5_mixed", translate("Enabled Mixed"), translate("Mixed as an alias of socks, default:Enabled."))
o.default = "1" o.default = "1"
o.rmempty = false 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 end
-- Local Port -- Local Port
@@ -189,37 +350,6 @@ o.datatype = "port"
o.default = 1080 o.default = 1080
o.rmempty = false 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 ]]-- -- [[ fragmen Settings ]]--
if is_finded("xray") then if is_finded("xray") then
s = m:section(TypedSection, "global_xray_fragment", translate("Xray Fragment Settings")) 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) local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= "" return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
end end
local function is_installed(e) local function is_installed(e)
@@ -142,14 +142,16 @@ local function set_apply_on_parse(map)
local old = map.on_after_save local old = map.on_after_save
map.on_after_save = function(self) map.on_after_save = function(self)
if old then old(self) end if old then old(self) end
--map:set("@global[0]", "timestamp", os.time()) map:set("@global[0]", "timestamp", os.time())
end end
end end
end end
local has_ss_rust = is_finded("sslocal") or is_finded("ssserver") local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
local has_mihomo = is_finded("mihomo") 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_xray = is_finded("xray")
local has_hysteria2 = is_finded("hysteria")
local server_table = {} local server_table = {}
local encrypt_methods = { local encrypt_methods = {
@@ -254,56 +256,6 @@ local tls_flows = {
"none" "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 = Map("shadowsocksr", translate("Edit ShadowSocksR Server"))
m.redirect = url("servers") m.redirect = url("servers")
if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then
@@ -312,14 +264,6 @@ if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then
end end
-- 保存&应用成功后跳转到节点列表 -- 保存&应用成功后跳转到节点列表
set_apply_on_parse(m) 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 ]]-- -- [[ Servers Setting ]]--
s = m:section(NamedSection, sid, "servers") s = m:section(NamedSection, sid, "servers")
@@ -331,27 +275,88 @@ o.rawhtml = true
o.template = "shadowsocksr/ssrurl" o.template = "shadowsocksr/ssrurl"
o.value = sid 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 核心 -- 新增一个选择框,用于选择 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")) o = s:option(ListValue, "type", translate("Server Node Type"))
if is_finded("xray") then if is_finded("xray") or is_finded("v2ray") then
o:value("v2ray", translate("V2Ray/XRay")) o:value("v2ray", translate("V2Ray/XRay"))
end end
if is_finded("ssr-redir") then if is_finded("ssr-redir") then
o:value("ssr", translate("ShadowsocksR")) o:value("ssr", translate("ShadowsocksR"))
end end
if has_mihomo then if has_ss_rust or has_ss_libev then
o:value("ss", translate("ShadowSocks")) o:value("ss", translate("ShadowSocks"))
end end
if has_ss_rust then if is_finded("trojan") then
o:value("ss-rust", translate("ShadowSocks")) o:value("trojan", translate("Trojan"))
end end
if is_finded("naive") then if is_finded("naive") then
o:value("naiveproxy", translate("NaiveProxy")) o:value("naiveproxy", translate("NaiveProxy"))
end end
if is_finded("mihomo") then if is_finded("hysteria") then
o:value("clash", translate("Clash/Mihomo")) o:value("hysteria2", translate("Hysteria2"))
end end
if is_finded("mihomo") then if is_finded("tuic-client") then
o:value("tuic", translate("TUIC")) o:value("tuic", translate("TUIC"))
end end
if is_finded("shadow-tls") and is_finded("sslocal") then if is_finded("shadow-tls") and is_finded("sslocal") then
@@ -360,51 +365,78 @@ end
if is_finded("ipt2socks") then if is_finded("ipt2socks") then
o:value("socks5", translate("Socks5")) o:value("socks5", translate("Socks5"))
end end
if is_finded("redsocks2") then
o:value("tun", translate("Network Tunnel"))
end
local old_cfgvalue = o.cfgvalue local old_cfgvalue = o.cfgvalue
o.cfgvalue = function(self, section) o.cfgvalue = function(self, section)
local val = self.map.uci:get("shadowsocksr", section, "type") local val = self.map.uci:get("shadowsocksr", section, "type")
if old_cfgvalue then if val == "ss-rust" or val == "ss-libev" then
return "ss"
end
if old_cfgvalue then
return old_cfgvalue(self, section) return old_cfgvalue(self, section)
end end
return val return val
end 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.description = translate("Using incorrect encryption mothod may causes service fail to start")
o = s:option(Value, "alias", translate("Alias(optional)")) o = s:option(Value, "alias", translate("Alias(optional)"))
local function clash_source_formvalue(map, section, option) o = s:option(ListValue, "iface", translate("Network interface to use"))
local value = map:formvalue("cbid." .. map.config .. "." .. section .. "." .. option) for _, e in ipairs(luci.sys.net.devices()) do
if value == nil then if e ~= "lo" then
value = map.uci:get(map.config, section, option) o:value(e)
end end
return trim(value or "")
end end
o:depends("type", "tun")
o.description = translate("Redirect traffic to this network interface")
local function validate_clash_source(self, value, section) -- 新增一个选择框,用于选择 Shadowsocks 具体版本(仅当节点类型为 ss 或其具体子类型时显示)
local clash_url = clash_source_formvalue(self.map, section, "clash_url") o = s:option(ListValue, "_ss_core", string.format("<b><span style='color:red;'>%s</span></b>", translatef("%s Node Use Version", "ShadowSocks")))
local clash_path = clash_source_formvalue(self.map, section, "clash_path") o.description = translate("Selection ShadowSocks Node Use Version.")
if clash_url == "" and clash_path == "" then if has_ss_rust then
return nil, translate("Please specify either a Clash subscription URL or a local YAML path.") o:value("ss-rust", translate("ShadowSocks-rust Version"))
end
return value
end end
if has_ss_libev then
o = s:option(Value, "clash_url", translate("Clash Subscription URL")) o:value("ss-libev", translate("ShadowSocks-libev Version"))
o.placeholder = "https://example.com/config.yaml" 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.rmempty = true o.rmempty = true
o:depends("type", "clash") -- 保存时,将选择的值直接写入当前节点的 type 字段
o.validate = validate_clash_source o.write = function(self, section, value)
if value and value ~= "" then
o = s:option(Value, "clash_path", translate("Clash YAML Path")) self.map.uci:set("shadowsocksr", section, "type", value)
o.placeholder = "/etc/ssrplus/clash/custom.yaml" end
o.rmempty = true end
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 = s:option(ListValue, "v2ray_protocol", translate("V2Ray/XRay protocol"))
o:value("vless", translate("VLESS")) o:value("vless", translate("VLESS"))
@@ -422,54 +454,30 @@ o:value("http", translate("HTTP"))
o:depends("type", "v2ray") o:depends("type", "v2ray")
o = s:option(Value, "server", translate("Server Address")) o = s:option(Value, "server", translate("Server Address"))
o.datatype = "or(host,ip6addr)" o.datatype = "host"
o.rmempty = false o.rmempty = false
o:depends("type", "ssr") o:depends("type", "ssr")
o:depends("type", "ss") o:depends("type", "ss")
o:depends("type", "ss-rust") o:depends("type", "v2ray")
o:depends("type", "trojan") o:depends("type", "trojan")
o:depends("type", "naiveproxy") o:depends("type", "naiveproxy")
o:depends("type", "hysteria2") o:depends("type", "hysteria2")
o:depends("type", "tuic") o:depends("type", "tuic")
o:depends("type", "shadowtls") o:depends("type", "shadowtls")
o:depends("type", "socks5") 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 = s:option(Value, "server_port", translate("Server Port"))
o.datatype = "port" o.datatype = "port"
o.rmempty = true o.rmempty = true
o:depends("type", "ssr") o:depends("type", "ssr")
o:depends("type", "ss") o:depends("type", "ss")
o:depends("type", "ss-rust") o:depends("type", "v2ray")
o:depends("type", "trojan") o:depends("type", "trojan")
o:depends("type", "naiveproxy") o:depends("type", "naiveproxy")
o:depends("type", "hysteria2") o:depends("type", "hysteria2")
o:depends("type", "tuic") o:depends("type", "tuic")
o:depends("type", "shadowtls") o:depends("type", "shadowtls")
o:depends("type", "socks5") 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 = s:option(Flag, "auth_enable", translate("Enable Authentication"))
o.rmempty = false o.rmempty = false
@@ -490,7 +498,6 @@ o.password = true
o.rmempty = true o.rmempty = true
o:depends("type", "ssr") o:depends("type", "ssr")
o:depends("type", "ss") o:depends("type", "ss")
o:depends("type", "ss-rust")
o:depends("type", "trojan") o:depends("type", "trojan")
o:depends("type", "naiveproxy") o:depends("type", "naiveproxy")
o:depends("type", "shadowtls") o:depends("type", "shadowtls")
@@ -517,7 +524,6 @@ for _, v in ipairs(encrypt_methods_ss) do
end end
end end
o.rmempty = true o.rmempty = true
o:depends("type", "ss-rust")
o:depends("type", "ss") o:depends("type", "ss")
o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"}) o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"})
@@ -536,30 +542,22 @@ o.default = "1"
o = s:option(Flag, "enable_plugin", translate("Enable Plugin")) o = s:option(Flag, "enable_plugin", translate("Enable Plugin"))
o.rmempty = true o.rmempty = true
o:depends("type", "ss") o:depends("type", "ss")
o:depends("type", "ss-rust")
o.default = "0" o.default = "0"
-- Shadowsocks Plugin -- Shadowsocks Plugin
o = s:option(ListValue, "plugin", translate("Obfs")) o = s:option(ListValue, "plugin", translate("Obfs"))
o:value("none", translate("None")) o:value("none", translate("None"))
if has_mihomo or is_finded("obfs-local") then if is_finded("obfs-local") then
o:value("obfs-local", translate("obfs-local")) o:value("obfs-local", translate("obfs-local"))
end end
if has_mihomo or is_finded("v2ray-plugin") then if is_finded("v2ray-plugin") then
o:value("v2ray-plugin", translate("v2ray-plugin")) o:value("v2ray-plugin", translate("v2ray-plugin"))
end end
if has_mihomo then
o:value("gost-plugin", translate("gost-plugin"))
end
if is_finded("xray-plugin") then if is_finded("xray-plugin") then
o:value("xray-plugin", translate("xray-plugin")) o:value("xray-plugin", translate("xray-plugin"))
end end
if has_mihomo or is_finded("shadow-tls") then if is_finded("shadow-tls") then
o:value("shadow-tls", translate("Shadow-TLS")) o:value("shadow-tls", translate("shadow-tls"))
end
if has_mihomo then
o:value("restls", translate("restls"))
o:value("kcptun", translate("kcptun"))
end end
o:value("custom", translate("Custom")) o:value("custom", translate("Custom"))
o.rmempty = true o.rmempty = true
@@ -595,21 +593,6 @@ o:depends("type", "ssr")
-- [[ Hysteria2 ]]-- -- [[ 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 = s:option(Value, "hy2_auth", translate("Users Authentication"))
o:depends("type", "hysteria2") o:depends("type", "hysteria2")
o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
@@ -618,7 +601,7 @@ o.rmempty = false
o = s:option(Flag, "flag_port_hopping", translate("Enable Port Hopping")) o = s:option(Flag, "flag_port_hopping", translate("Enable Port Hopping"))
o:depends("type", "hysteria2") o:depends("type", "hysteria2")
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false}) o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
o.rmempty = true o.rmempty = true
o.default = "0" o.default = "0"
@@ -660,30 +643,17 @@ o.rmempty = true
o.default = "0" o.default = "0"
o = s:option(Value, "obfs_type", translate("Obfuscation Type")) 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 = "hysteria2", flag_obfs = true})
o:depends({type = "v2ray", v2ray_protocol = "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 = 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.password = true
o.rmempty = true o.rmempty = true
o:depends({type = "hysteria2", flag_obfs = true}) o.placeholder = "cry_me_a_r1ver"
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 = s:option(Flag, "flag_quicparam", translate("Hysterir QUIC parameters"))
o:depends("type", "hysteria2") o:depends("type", "hysteria2")
@@ -778,13 +748,13 @@ o:depends("type", "shadowtls")
if is_finded("sslocal") then if is_finded("sslocal") then
o:value("sslocal", translate("ShadowSocks-rust Version")) o:value("sslocal", translate("ShadowSocks-rust Version"))
end end
if is_finded("xray") then if is_finded("xray") or is_finded("v2ray") then
o:value("vmess", translate("Vmess Protocol")) o:value("vmess", translate("Vmess Protocol"))
end end
o.default = "sslocal" o.default = "sslocal"
o.rmempty = false 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:depends({type = "shadowtls", chain_type = "sslocal"})
o.rmempty = true o.rmempty = true
@@ -818,7 +788,7 @@ o:depends("type", "tuic")
--Tuic IP --Tuic IP
o = s:option(Value, "tuic_ip", translate("TUIC Server IP Address")) o = s:option(Value, "tuic_ip", translate("TUIC Server IP Address"))
o.rmempty = true o.rmempty = true
o.datatype = "ipaddr" o.datatype = "ip4addr"
o.default = "" o.default = ""
o:depends("type", "tuic") o:depends("type", "tuic")
@@ -999,7 +969,7 @@ o = s:option(Value, "ws_heartbeatPeriod", translate("HeartbeatPeriod(second)"))
o.datatype = "integer" o.datatype = "integer"
o:depends("transport", "ws") o:depends("transport", "ws")
if is_finded("xray") then if is_finded("v2ray") then
-- WS前置数据 -- WS前置数据
o = s:option(Value, "ws_ed", translate("Max Early Data")) o = s:option(Value, "ws_ed", translate("Max Early Data"))
o:depends("ws_ed_enable", true) o:depends("ws_ed_enable", true)
@@ -1240,12 +1210,12 @@ o.default = "0"
o.rmempty = true o.rmempty = true
o = s:option(DynamicList, "local_addresses", translate("Local addresses")) o = s:option(DynamicList, "local_addresses", translate("Local addresses"))
--o.datatype = "cidr" o.datatype = "cidr"
o:depends({type = "v2ray", v2ray_protocol = "wireguard"}) o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
o.rmempty = true o.rmempty = true
o = s:option(DynamicList, "reserved", translate("Reserved bytes(optional)")) o = s:option(DynamicList, "reserved", translate("Reserved bytes(optional)"))
o.description = translate("Supports decimal numbers separated by \",\" or Base64-encoded strings, with a maximum length of 3 bytes.") o.description = translate("Decimal numbers separated by \",\" or Base64-encoded strings.")
o:depends({type = "v2ray", v2ray_protocol = "wireguard"}) o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
o.rmempty = true o.rmempty = true
@@ -1290,18 +1260,12 @@ o:depends("transport", "grpc")
o = s:option(Flag, "enable_finalmask", translate("FinalMask")) o = s:option(Flag, "enable_finalmask", translate("FinalMask"))
o.rmempty = true o.rmempty = true
o.default = "0" o.default = "0"
local protocols = s.fields["v2ray_protocol"].keylist o:depends({type = "v2ray", v2ray_protocol = "vless"})
if protocols and type(protocols) == "table" and #protocols > 0 then o:depends({type = "v2ray", v2ray_protocol = "vmess"})
for _, proto in ipairs(protocols) do o:depends({type = "v2ray", v2ray_protocol = "trojan"})
if not proto:find("^_") then o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"})
if proto == "hysteria2" then o:depends({type = "v2ray", v2ray_protocol = "wireguard"})
o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false}) o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
elseif proto ~= "socks" and proto ~= "http" then
o:depends({type = "v2ray", v2ray_protocol = proto})
end
end
end
end
o = s:option(TextValue, "finalmask", " ") o = s:option(TextValue, "finalmask", " ")
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" .. o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
@@ -1398,18 +1362,7 @@ if is_finded("xray") then
o = s:option(Flag, "enable_ech", translate("Enable ECH(optional)")) o = s:option(Flag, "enable_ech", translate("Enable ECH(optional)"))
o.rmempty = true o.rmempty = true
o.default = "0" o.default = "0"
local protocols = s.fields["v2ray_protocol"].keylist o:depends({type = "v2ray", tls = true})
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 = s:option(TextValue, "ech_config", translate("ECH Config"))
o.description = translate( o.description = translate(
@@ -1692,41 +1645,100 @@ o.rmempty = true
o.default = "0" o.default = "0"
o:depends("type", "ssr") o:depends("type", "ssr")
o:depends("type", "ss") o:depends("type", "ss")
o:depends("type", "ss-rust")
o:depends("type", "trojan") o:depends("type", "trojan")
o:depends("type", "hysteria2") o:depends("type", "hysteria2")
o:depends({type = "v2ray", v2ray_protocol = "vless", transport = "xhttp"}) o:depends({type = "v2ray", v2ray_protocol = "vless", transport = "xhttp"})
o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) 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 = s:option(Flag, "switch_enable", translate("Enable Auto Switch"))
o.rmempty = false o.rmempty = false
o.default = "1" 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 if is_finded("kcptun-client") then
o = s:option(Flag, "kcp_enable", translate("KcpTun Enable")) o = s:option(Flag, "kcp_enable", translate("KcpTun Enable"))
o.rmempty = true o.rmempty = true
o.default = "0" o.default = "0"
o:depends("type", "ssr") o:depends("type", "ssr")
o:depends("type", "ss-rust")
o:depends("type", "ss") o:depends("type", "ss")
o = s:option(Value, "kcp_port", translate("KcpTun Port")) o = s:option(Value, "kcp_port", translate("KcpTun Port"))
o.datatype = "portrange" o.datatype = "portrange"
o.default = 4000 o.default = 4000
o:depends("type", "ssr") o:depends("type", "ssr")
o:depends("type", "ss-rust")
o:depends("type", "ss") o:depends("type", "ss")
o = s:option(Value, "kcp_password", translate("KcpTun Password")) o = s:option(Value, "kcp_password", translate("KcpTun Password"))
o.password = true o.password = true
o:depends("type", "ssr") o:depends("type", "ssr")
o:depends("type", "ss-rust")
o:depends("type", "ss") o:depends("type", "ss")
o = s:option(Value, "kcp_param", translate("KcpTun Param")) o = s:option(Value, "kcp_param", translate("KcpTun Param"))
o.default = "--nocomp" o.default = "--nocomp"
o:depends("type", "ssr") o:depends("type", "ssr")
o:depends("type", "ss-rust")
o:depends("type", "ss") o:depends("type", "ss")
end end
@@ -4,7 +4,6 @@
local m, s, sec, o local m, s, sec, o
local uci = require "luci.model.uci".cursor() local uci = require "luci.model.uci".cursor()
local URL = require "url"
-- 获取 LAN IP 地址 -- 获取 LAN IP 地址
function lanip() function lanip()
@@ -32,62 +31,69 @@ end
local lan_ip = lanip() local lan_ip = lanip()
local validation = require "luci.cbi.datatypes" local validation = require "luci.cbi.datatypes"
local clash_nodes = {}
local function is_finded(e) local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= "" return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
end end
local function clash_display_name(s) m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/TUN etc.</h3>"))
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" m:section(SimpleSection).template = "shadowsocksr/status"
local server_table = {} local server_table = {}
local server_order = {}
uci:foreach("shadowsocksr", "servers", function(s) uci:foreach("shadowsocksr", "servers", function(s)
if s.type == "clash" then if s.alias 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} server_table[s[".name"]] = "[%s]:%s" % {string.upper(s.v2ray_protocol or s.type), s.alias}
elseif s.type ~= "tun" and s.server and s.server_port then elseif 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} 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
end) end)
local key_table = {}
for key, _ in pairs(server_table) do
table.insert(key_table, key)
end
table.sort(key_table)
-- [[ Global Setting ]]-- -- [[ Global Setting ]]--
s = m:section(TypedSection, "global") s = m:section(TypedSection, "global")
s.anonymous = true s.anonymous = true
o = s:option(ListValue, "global_server", translate("Main Server")) o = s:option(ListValue, "global_server", translate("Main Server"))
o:value("nil", translate("Disable")) o:value("nil", translate("Disable"))
for _, key in ipairs(server_order) do for _, key in pairs(key_table) do
o:value(key, server_table[key]) o:value(key, server_table[key])
end end
o.default = "nil" o.default = "nil"
o.rmempty = false o.rmempty = false
o = s:option(DummyValue, "_clash_panel", translate("Clash Panel")) o = s:option(ListValue, "udp_relay_server", translate("Game Mode UDP Server"))
o.template = "shadowsocksr/clash_main_panel" o:value("", translate("Disable"))
o.clash_nodes = clash_nodes 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(ListValue, "threads", translate("Multi Threads Option")) o = s:option(ListValue, "threads", translate("Multi Threads Option"))
o:value("0", translate("Auto Threads")) o:value("0", translate("Auto Threads"))
@@ -106,6 +112,7 @@ o = s:option(ListValue, "run_mode", translate("Running Mode"))
o:value("gfw", translate("GFW List Mode")) o:value("gfw", translate("GFW List Mode"))
o:value("router", translate("IP Route Mode")) o:value("router", translate("IP Route Mode"))
o:value("all", translate("Global Mode")) o:value("all", translate("Global Mode"))
o:value("oversea", translate("Oversea Mode"))
o.default = gfw o.default = gfw
o = s:option(ListValue, "dports", translate("Proxy Ports")) o = s:option(ListValue, "dports", translate("Proxy Ports"))
@@ -121,13 +128,21 @@ o = s:option(ListValue, "pdnsd_enable", translate("Resolve Dns Mode"))
if is_finded("dns2tcp") then if is_finded("dns2tcp") then
o:value("1", translate("Use DNS2TCP query")) o:value("1", translate("Use DNS2TCP query"))
end 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 if is_finded("mosdns") then
o:value("4", translate("Use MosDNS query")) 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"))
end end
if is_finded("chinadns-ng") then if is_finded("chinadns-ng") then
o:value("6", translate("Use ChinaDNS-NG query and cache")) o:value("6", translate("Use ChinaDNS-NG query and cache"))
end end
o:value("7", translate("Prefer module built-in DNS"))
o:value("0", translate("Use Local DNS Service listen port 5335")) o:value("0", translate("Use Local DNS Service listen port 5335"))
o.default = 1 o.default = 1
@@ -143,8 +158,11 @@ 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.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("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("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", "1")
o:depends("pdnsd_enable", "7") o:depends("pdnsd_enable", "2")
o:depends("pdnsd_enable", "3")
o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)")
o.datatype = "ip4addrport" o.datatype = "ip4addrport"
o.default = "8.8.4.4:53" o.default = "8.8.4.4:53"
@@ -158,15 +176,61 @@ 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:value("tcp://1.1.1.1:53,tcp://1.0.0.1:53", translate("Cloudflare DNS"))
o:depends("pdnsd_enable", "4") 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.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, "filter_aaaa", translate("Disable IPv6 for Overseas FQDN")) o = s:option(Flag, "mosdns_ipv6", translate("Disable IPv6 in MOSDNS query mode"))
o:depends("pdnsd_enable", "1")
o:depends("pdnsd_enable", "4") o:depends("pdnsd_enable", "4")
o:depends("pdnsd_enable", "7")
o.rmempty = false o.rmempty = false
o.default = "1" 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 if is_finded("chinadns-ng") then
o = s:option(Value, "chinadns_ng_tunnel_forward", translate("Anti-pollution DNS Server")) 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)")) o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)"))
@@ -181,7 +245,6 @@ if is_finded("chinadns-ng") then
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)")) 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("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
o:depends("pdnsd_enable", "6") o:depends("pdnsd_enable", "6")
o.default = "8.8.4.4:53"
o.description = translate( o.description = translate(
"<ul>" .. "<ul>" ..
"<li>" .. translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") .. "</li>" .. "<li>" .. translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") .. "</li>" ..
@@ -207,8 +270,11 @@ 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("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("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: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:depends({pdnsd_enable = "6", run_mode = "router"})
o.default = "wan"
o.description = translate("Custom DNS Server format as IP:PORT (default: disabled)") o.description = translate("Custom DNS Server format as IP:PORT (default: disabled)")
o.validate = function(self, value, section) o.validate = function(self, value, section)
if (section and value) then if (section and value) then
@@ -227,7 +293,5 @@ if is_finded("chinadns-ng") then
end end
end end
m:append(Template("shadowsocksr/client_dns_defaults")) m:section(SimpleSection).template = "shadowsocksr/status_bottom"
m:append(Template("shadowsocksr/status_bottom"))
return m return m
@@ -1,30 +0,0 @@
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,6 +3,10 @@ require "nixio.fs"
require "luci.sys" require "luci.sys"
local m, s, o 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") m = Map("shadowsocksr")
s = m:section(TypedSection, "access_control") s = m:section(TypedSection, "access_control")
@@ -125,6 +129,40 @@ o.remove = function(self, section, value)
nixio.fs.writefile(denydomainconf, "") nixio.fs.writefile(denydomainconf, "")
end 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 if luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 then
m.apply_on_parse = true m.apply_on_parse = true
function m.on_apply(self) function m.on_apply(self)
@@ -132,6 +170,4 @@ if luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 then
end end
end end
m:append(Template("shadowsocksr/control_layout"))
return m return m
@@ -50,7 +50,6 @@ luci.http.setfilehandler(function(meta, chunk, eof)
luci.sys.call("rm -rf " .. temp_dir) luci.sys.call("rm -rf " .. temp_dir)
nixio.fs.remove(file_path) nixio.fs.remove(file_path)
fd = nixio.open(file_path, "w") 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") luci.sys.call("echo '' > /var/log/ssrplus.log")
end end
@@ -97,7 +96,6 @@ luci.http.setfilehandler(function(meta, chunk, eof)
-- 清理临时文件 -- 清理临时文件
luci.sys.call("rm -rf " .. temp_dir) luci.sys.call("rm -rf " .. temp_dir)
nixio.fs.remove(file_path) nixio.fs.remove(file_path)
luci.sys.call("ls /tmp/sub_md5_* >/dev/null 2>&1 && rm -f /tmp/sub_md5_*")
end end
end) end)
@@ -7,39 +7,68 @@ require "nixio.fs"
local m, s, o local m, s, o
local sid = arg[1] local sid = arg[1]
-- 加密方式(SS/SSR用)
local encrypt_methods = { local encrypt_methods = {
"rc4-md5", "rc4-md5-6", "rc4", "table", "rc4-md5",
"aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "rc4-md5-6",
"aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "rc4",
"bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", "table",
"cast5-cfb", "des-cfb", "idea-cfb", "rc2-cfb", "seed-cfb", "aes-128-cfb",
"salsa20", "chacha20", "chacha20-ietf" "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 = { local encrypt_methods_ss = {
"aes-128-gcm", "aes-192-gcm", "aes-256-gcm", -- aead
"chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", "aes-128-gcm",
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" "aes-192-gcm",
} "aes-256-gcm",
"chacha20-ietf-poly1305",
-- Shadowsocks 加密方法(Xray 版) "xchacha20-ietf-poly1305",
local v_ss_encrypt_method_list = { -- aead 2022
"aes-128-cfb", "aes-256-cfb", "aes-128-gcm", "aes-256-gcm", "2022-blake3-aes-128-gcm",
"chacha20", "chacha20-ietf", "chacha20-poly1305", "chacha20-ietf-poly1305" "2022-blake3-aes-256-gcm",
} "2022-blake3-chacha20-poly1305"
--[[ stream
-- 伪装类型(用于 mKCP/QUIC) "table",
local header_type_list = { "rc4",
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard" "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" ]]
} }
local protocol = {"origin"} local protocol = {"origin"}
obfs = {"plain", "http_simple", "http_post"} obfs = {"plain", "http_simple", "http_post"}
m = Map("shadowsocksr", translate("Edit ShadowSocksR Server")) m = Map("shadowsocksr", translate("Edit ShadowSocksR Server"))
m.redirect = luci.dispatcher.build_url("admin/services/shadowsocksr/server")
m.redirect = luci.dispatcher.build_url("admin/services/shadowsocksr/server")
if m.uci:get("shadowsocksr", sid) ~= "server_config" then if m.uci:get("shadowsocksr", sid) ~= "server_config" then
luci.http.redirect(m.redirect) luci.http.redirect(m.redirect)
return return
@@ -50,26 +79,18 @@ s = m:section(NamedSection, sid, "server_config")
s.anonymous = true s.anonymous = true
s.addremove = false s.addremove = false
-- ========== 基本设置 ==========
o = s:option(Flag, "enable", translate("Enable")) o = s:option(Flag, "enable", translate("Enable"))
o.default = 1 o.default = 1
o.rmempty = false o.rmempty = false
o = s:option(ListValue, "type", translate("Server Type")) o = s:option(ListValue, "type", translate("Server Type"))
o:value("socks5", translate("Socks5")) o:value("socks5", translate("Socks5"))
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 if nixio.fs.access("/usr/bin/ssserver") or nixio.fs.access("/usr/bin/ss-server") then
o:value("ss", translate("ShadowSocks")) o:value("ss", translate("ShadowSocks"))
end end
if nixio.fs.access("/usr/bin/ssr-server") then if nixio.fs.access("/usr/bin/ssr-server") then
o:value("ssr", translate("ShadowsocksR")) o:value("ssr", translate("ShadowsocksR"))
end end
-- Xray 协议支持
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.default = "socks5"
o = s:option(Value, "server_port", translate("Server Port")) o = s:option(Value, "server_port", translate("Server Port"))
@@ -93,30 +114,32 @@ o:depends("type", "socks5")
o = s:option(Value, "password", translate("Password")) o = s:option(Value, "password", translate("Password"))
o.password = true o.password = true
o.rmempty = false 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")) o = s:option(ListValue, "encrypt_method", translate("Encrypt Method"))
for _, v in ipairs(encrypt_methods) do o:value(v) end for _, v in ipairs(encrypt_methods) do
o:value(v)
end
o.rmempty = false o.rmempty = false
o:depends("type", "ssr") o:depends("type", "ssr")
o = s:option(ListValue, "encrypt_method_ss", translate("Encrypt Method")) 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.rmempty = false
o:depends("type", "ss") o:depends("type", "ss")
o = s:option(ListValue, "protocol", translate("Protocol")) 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.rmempty = false
o:depends("type", "ssr") o:depends("type", "ssr")
o = s:option(ListValue, "obfs", translate("Obfs")) 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.rmempty = false
o:depends("type", "ssr") o:depends("type", "ssr")
@@ -128,269 +151,4 @@ o.rmempty = false
o:depends("type", "ss") o:depends("type", "ss")
o:depends("type", "ssr") o:depends("type", "ssr")
-- ========== Xray 协议通用字段 ========== return m
-- 用户备注
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,13 +105,8 @@ end
o.rmempty = false o.rmempty = false
o = sec:option(DummyValue, "type", translate("Server Type")) o = sec:option(DummyValue, "type", translate("Server Type"))
function o.cfgvalue(self, section) function o.cfgvalue(...)
local val = Value.cfgvalue(self, section) or "ss" return Value.cfgvalue(...) 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 end
o = sec:option(DummyValue, "server_port", translate("Server Port")) o = sec:option(DummyValue, "server_port", translate("Server Port"))
@@ -140,4 +135,4 @@ function o.cfgvalue(...)
return Value.cfgvalue(...) or "-" return Value.cfgvalue(...) or "-"
end end
return m return m
@@ -1,35 +1,36 @@
-- Licensed to the public under the GNU General Public License v3. -- Licensed to the public under the GNU General Public License v3.
require "luci.http" require "luci.http"
require "luci.sys" require "luci.sys"
require "luci.util"
require "nixio.fs" require "nixio.fs"
require "luci.dispatcher" require "luci.dispatcher"
require "luci.model.uci" require "luci.model.uci"
local nixio = require "nixio"
local json = require "luci.jsonc"
local cbi = require "luci.cbi" local cbi = require "luci.cbi"
local uci = require "luci.model.uci".cursor() local uci = require "luci.model.uci".cursor()
local URL = require "url"
local m, s, o, node local m, s, o, node
local server_count = 0 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) local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= "" return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
end end
local function is_js_luci() local function is_js_luci()
return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
end 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 function apply_redirect(m)
local tmp_uci_file = "/etc/config/" .. "shadowsocksr" .. "_redirect" local tmp_uci_file = "/etc/config/" .. "shadowsocksr" .. "_redirect"
if m.redirect and m.redirect ~= "" then if m.redirect and m.redirect ~= "" then
@@ -64,341 +65,50 @@ local function set_apply_on_parse(map)
local old = map.on_after_save local old = map.on_after_save
map.on_after_save = function(self) map.on_after_save = function(self)
if old then old(self) end if old then old(self) end
-- map:set("@global[0]", "timestamp", os.time()) map:set("@global[0]", "timestamp", os.time())
end end
end end
end end
local function trim(text) local has_ss_rust = is_finded("sslocal") or is_finded("ssserver")
if not text or text == "" then local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local")
return "" local has_trojan = is_finded("trojan")
end local has_xray = is_finded("xray")
return (text:gsub("^%s*(.-)%s*$", "%1")) 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)") })
end end
local function parse_nonnegative_int(value) if has_trojan then
local number = tonumber(value) table.insert(tj_type_list, { id = "trojan", name = translate("Trojan") })
if not number then end
return nil if has_xray then
end table.insert(tj_type_list, { id = "v2ray", name = translate("Xray (Trojan)") })
number = math.floor(number)
if number < 0 then
return nil
end
return number
end end
local function in_list(list, target) if has_ss_rust then
for _, item in ipairs(list) do table.insert(ss_type_list, { id = "ss-rust", name = translate("ShadowSocks-rust Version") })
if item == target then
return true
end
end
return false
end end
if has_ss_libev then
local function upload_alias(filename) table.insert(ss_type_list, { id = "ss-libev", name = translate("ShadowSocks-libev Version") })
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 end
if has_xray then
local function hash_file(path) table.insert(ss_type_list, { id = "v2ray", name = translate("Xray (ShadowSocks)") })
local cmd = "md5sum " .. luci.util.shellquote(path) .. " 2>/dev/null | awk '{print $1}'"
return trim(luci.sys.exec(cmd))
end 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) uci:foreach("shadowsocksr", "servers", function(s)
server_count = server_count + 1 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) 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")) 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 -- Server Subscribe
s = m:section(TypedSection, "server_subscribe") s = m:section(TypedSection, "server_subscribe")
@@ -408,13 +118,6 @@ o = s:option(Flag, "auto_update", translate("Auto Update"))
o.rmempty = false o.rmempty = false
o.description = translate("Auto Update Server subscription, GFW list and CHN route") 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 = s:option(ListValue, "auto_update_week_time", translate("Update cycle (Day/Week)"))
o:value('*', translate("Every Day")) o:value('*', translate("Every Day"))
o:value("1", translate("Every Monday")) o:value("1", translate("Every Monday"))
@@ -426,7 +129,7 @@ o:value("6", translate("Every Saturday"))
o:value("0", translate("Every Sunday")) o:value("0", translate("Every Sunday"))
o.default = "*" o.default = "*"
o.rmempty = true o.rmempty = true
o:depends({auto_update = "1", config_auto_update_mode = "0"}) o:depends("auto_update", "1")
o = s:option(ListValue, "auto_update_day_time", translate("Regular update (Hour)")) o = s:option(ListValue, "auto_update_day_time", translate("Regular update (Hour)"))
for t = 0, 23 do for t = 0, 23 do
@@ -434,7 +137,7 @@ for t = 0, 23 do
end end
o.default = 2 o.default = 2
o.rmempty = true o.rmempty = true
o:depends({auto_update = "1", config_auto_update_mode = "0"}) o:depends("auto_update", "1")
o = s:option(ListValue, "auto_update_min_time", translate("Regular update (Min)")) o = s:option(ListValue, "auto_update_min_time", translate("Regular update (Min)"))
for i = 0, 59 do for i = 0, 59 do
@@ -442,83 +145,178 @@ for i = 0, 59 do
end end
o.default = 30 o.default = 30
o.rmempty = true o.rmempty = true
o:depends({auto_update = "1", config_auto_update_mode = "0"}) o:depends("auto_update", "1")
o = s:option(Value, "config_update_interval", translate("Update Interval(min)")) -- 确保 hy2_type_list 不为空
o.default = "60" if #hy2_type_list > 0 then
o.datatype = "uinteger" 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.rmempty = true o.rmempty = true
o:depends({auto_update = "1", config_auto_update_mode = "1"})
o = s:option(Flag, "_subscribe_advanced_toggle", translate("Subscribe Advanced Settings")) o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"))
o.rmempty = false o.description = translate(
o.default = "0" "<ul>" ..
o.write = function() end "<li>" .. translate("If the node address is a domain name, this DNS will be used for resolution.") .. "</li>" ..
o.remove = function() end "<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(Value, "filter_words", translate("Subscribe Filter Words")) o = s:option(Value, "filter_words", translate("Subscribe Filter Words"))
o.rmempty = true o.rmempty = true
o.description = translate("Filter Words splited by /") 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 = s:option(Value, "save_words", translate("Subscribe Save Words"))
o.rmempty = true o.rmempty = true
o.description = translate("Save Words splited by /") o.description = translate("Save Words splited by /")
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1") 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 = s:option(Flag, "allow_insecure", translate("Allow subscribe Insecure nodes By default")) o = s:option(Flag, "allow_insecure", translate("Allow subscribe Insecure nodes By default"))
o.rmempty = false o.rmempty = false
o.description = translate("Subscribe nodes allows insecure connection as TLS client (insecure)") o.description = translate("Subscribe nodes allows insecure connection as TLS client (insecure)")
o.default = "0" 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 = s:option(Flag, "switch", translate("Subscribe Default Auto-Switch"))
o.rmempty = false o.rmempty = false
o.description = translate("Subscribe new add server default Auto-Switch on") o.description = translate("Subscribe new add server default Auto-Switch on")
o.default = "1" 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 = s:option(Flag, "proxy", translate("Through proxy update"))
o.rmempty = false o.rmempty = false
o.description = translate("Through proxy update list, Not Recommended ") 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 = s:option(Button, "subscribe", translate("Update All Subscribe Servers"))
o.rawhtml = true o.rawhtml = true
o.template = "shadowsocksr/subscribe" 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 = s:option(Button, "delete", translate("Delete All Subscribe Servers"))
o.inputstyle = "reset" o.inputstyle = "reset"
o.description = string.format(translate("Server Count") .. ": %d", server_count) o.description = string.format(translate("Server Count") .. ": %d", server_count)
o.write = function() o.write = function()
uci:delete_all("shadowsocksr", "servers", function(s) luci.http.redirect(url("delete"))
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 end
o = s:option(Value, "url_test_url", translate("URL Test Address")) o = s:option(Value, "url_test_url", translate("URL Test Address"))
@@ -537,187 +335,57 @@ 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 (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("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: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 ]]-- -- [[ Servers Manage ]]--
s = m:section(TypedSection, "servers") s = m:section(TypedSection, "servers")
s.anonymous = true s.anonymous = true
s.addremove = true s.addremove = true
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 = "cbi/tblsection"
s.template = "shadowsocksr/server_table"
set_apply_on_parse(m) 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.sortable = true
--[[
function s.cfgsections(self) s.extedit = url("servers", "%s")
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, ...) function s.create(self, ...)
local used_sid = {} local sid = TypedSection.create(self, ...)
local next_sid = 1 if sid then
local newsid = "cfg" .. sid:sub(-6)
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) self.map.uci:delete(self.config, sid)
local id = get_next_sid() -- 重命名 section
local newsid = string.format("cfg%02x%s", id, suffix) self.map.uci:section(self.config, self.sectiontype, newsid)
local success = self.map.uci:section(self.config, self.sectiontype, newsid) luci.http.redirect(self.extedit % newsid)
if success then return
--self.map.uci:save(self.config) end
luci.http.redirect(self.extedit % newsid)
return
end
end
end end
]]--
o = s:option(DummyValue, "type", translate("Type")) o = s:option(DummyValue, "type", translate("Type"))
function o.cfgvalue(self, section) function o.cfgvalue(self, section)
local cfg = get_server(section) return m:get(section, "v2ray_protocol") or Value.cfgvalue(self, section) or translate("None")
return cfg.v2ray_protocol or cfg.type or translate("None")
end end
o = s:option(DummyValue, "alias", translate("Alias")) o = s:option(DummyValue, "alias", translate("Alias"))
function o.cfgvalue(self, section) function o.cfgvalue(...)
return get_server(section).alias or translate("None") 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"
end end
o = s:option(DummyValue, "server_port", translate("Socket Connected")) o = s:option(DummyValue, "server_port", translate("Socket Connected"))
o.template = "shadowsocksr/socket" o.template = "shadowsocksr/socket"
o.width = "10%" 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) o.render = function(self, section, scope)
local cfg = get_server(section) local cfg = s:cfgvalue(section) or {}
local stype = cfg.type self.transport = cfg.transport
self.type = stype or "" self.type = cfg.type
self.proto = cfg.v2ray_protocol or "" self.v2ray_protocol = cfg.v2ray_protocol
self.reality = cfg.reality or "" if self.transport == 'ws' then
if stype == "clash" then self.ws_path = cfg.ws_path
self.transport = "" self.tls = cfg.tls
self.ws_path = "" self.tls_host = cfg.tls_host
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 end
DummyValue.render(self, section, scope) DummyValue.render(self, section, scope)
end end
@@ -725,15 +393,6 @@ end
o = s:option(DummyValue, "server", translate("Ping Latency")) o = s:option(DummyValue, "server", translate("Ping Latency"))
o.template = "shadowsocksr/ping" o.template = "shadowsocksr/ping"
o.width = "10%" 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') local global_server = uci:get_first('shadowsocksr', 'global', 'global_server')
@@ -748,10 +407,11 @@ node.render = function(self, section, scope)
Button.render(self, section, scope) Button.render(self, section, scope)
end end
node.write = function(self, section) node.write = function(self, section)
local safe_section = luci.util.shellquote(section) uci:set("shadowsocksr", '@global[0]', 'global_server', section)
local cmd = string.format("uci set shadowsocksr.@global[0].global_server=%s && uci commit shadowsocksr", safe_section) uci:save("shadowsocksr")
luci.sys.call(cmd) uci:commit("shadowsocksr")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "restart")) luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &")
luci.http.redirect(url("restart"))
end end
o = s:option(Flag, "switch_enable", translate("Auto Switch")) o = s:option(Flag, "switch_enable", translate("Auto Switch"))
@@ -762,15 +422,4 @@ end
m:append(cbi.Template("shadowsocksr/server_list")) 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 return m
@@ -7,22 +7,15 @@ local m, s, o
local redir_run = 0 local redir_run = 0
local reudp_run = 0 local reudp_run = 0
local sock5_run = 0 local sock5_run = 0
local http_run = 0
local server_run = 0 local server_run = 0
local kcptun_run = 0 local kcptun_run = 0
local tunnel_run = 0 local tunnel_run = 0
local gfw_count = 0 local gfw_count = 0
local ad_count = 0 local ad_count = 0
local ip_count = 0 local ip_count = 0
local Process_list = luci.sys.exec("busybox ps -w 2>/dev/null || busybox ps") local nfip_count = 0
local Process_list = luci.sys.exec("busybox ps -w")
local uci = require "luci.model.uci".cursor() 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 -- html constants
font_blue = [[<b style=color:green>]] font_blue = [[<b style=color:green>]]
style_blue = [[<b style=color:red>]] style_blue = [[<b style=color:red>]]
@@ -59,6 +52,10 @@ if nixio.fs.access("/etc/ssrplus/applechina.conf") then
apple_count = tonumber(luci.sys.exec("cat /etc/ssrplus/applechina.conf | wc -l")) apple_count = tonumber(luci.sys.exec("cat /etc/ssrplus/applechina.conf | wc -l"))
end 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 if Process_list:find("udp.only.ssr.reudp") then
reudp_run = 1 reudp_run = 1
end end
@@ -77,10 +74,6 @@ if Process_list:find("tcp.udp.ssr.local") then
sock5_run = 1 sock5_run = 1
end 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 if Process_list:find("tcp.udp.ssr.retcp") then
redir_run = 1 redir_run = 1
reudp_run = 1 reudp_run = 1
@@ -110,38 +103,6 @@ if Process_list:find("local.udp.ssr.retcp") then
sock5_run = 1 sock5_run = 1
end 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 if Process_list:find("kcptun.client") then
kcptun_run = 1 kcptun_run = 1
end end
@@ -150,21 +111,11 @@ if Process_list:find("ssr.server") then
server_run = 1 server_run = 1
end 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 if Process_list:find("ssrplus/bin/dns2tcp") or
Process_list:find("ssrplus/bin/mosdns") or Process_list:find("ssrplus/bin/mosdns") or
Process_list:find("chinadns.*127.0.0.1.*5335") then Process_list:find("dnsproxy.*127.0.0.1.*5335") or
pdnsd_run = 1 Process_list:find("chinadns.*127.0.0.1.*5335") or
end (Process_list:find("ssrplus.dns") and Process_list:find("dns2socks.*127.0.0.1.*127.0.0.1.5335")) then
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 pdnsd_run = 1
end end
@@ -206,16 +157,6 @@ else
s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off
end 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 = m:field(DummyValue, "server_run", translate("Local Servers"))
s.rawhtml = true s.rawhtml = true
if server_run == 1 then if server_run == 1 then
@@ -270,6 +211,13 @@ if uci:get_first("shadowsocksr", 'global', 'apple_optimization', '0') ~= '0' the
s.value = apple_count .. " " .. translate("Records") s.value = apple_count .. " " .. translate("Records")
end 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 if uci:get_first("shadowsocksr", 'global', 'adblock', '0') == '1' then
s = m:field(DummyValue, "ad_data", translate("Advertising Data")) s = m:field(DummyValue, "ad_data", translate("Advertising Data"))
s.rawhtml = true s.rawhtml = true
@@ -1,143 +0,0 @@
<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
@@ -1,104 +0,0 @@
<%+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%>
@@ -1,74 +0,0 @@
<%+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%>
@@ -1,4 +0,0 @@
<%+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%>
@@ -1,78 +0,0 @@
<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>
@@ -1,446 +0,0 @@
<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>
@@ -1,9 +0,0 @@
<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,17 +1,3 @@
<%+cbi/valueheader%> <%+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%> <%+cbi/valuefooter%>
File diff suppressed because it is too large. Load diff
@@ -1,304 +0,0 @@
<%-
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 -->
@@ -1,166 +0,0 @@
<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,26 +1,9 @@
<%+cbi/valueheader%> <%+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="transport" hint="<%=self.transport%>"></span>
<span class="wsPath" hint="<%=self.ws_path%>"></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="tls" hint="<%=self.tls%>"></span>
<span class="type" hint="<%=self.type%>"></span> <span class="tlshost" hint="<%=self.tls_host%>"></span>
<span class="proto" hint="<%=self.proto%>"></span> <span class="hy2type" hint="<%=self.type%>"></span>
<span class="reality" hint="<%=self.reality%>"></span> <span class="hy2proto" hint="<%=self.v2ray_protocol%>"></span>
<%+cbi/valuefooter%> <%+cbi/valuefooter%>
@@ -1,15 +1,25 @@
<%+cbi/valueheader%> <%+cbi/valueheader%>
<% <%
local map = self.map 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_rust = luci.sys.exec('type -t -p sslocal 2>/dev/null || type -t -p ssserver 2>/dev/null') ~= ""
local has_mihomo = luci.sys.exec('type -t -p mihomo -p /usr/libexec/mihomo 2>/dev/null') ~= "" local has_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_xray = luci.sys.exec('type -t -p xray 2>/dev/null') ~= "" local has_xray = luci.sys.exec('type -t -p xray 2>/dev/null') ~= ""
-%> -%>
<script type="text/javascript"> <script type="text/javascript">
//<![CDATA[ //<![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_rust = "<%=has_ss_rust%>"
let has_mihomo = "<%=has_mihomo%>" let has_ss_libev = "<%=has_ss_libev%>"
let has_hysteria = "<%=has_hysteria%>"
let has_trojan = "<%=has_trojan%>"
let has_xray = "<%=has_xray%>" let has_xray = "<%=has_xray%>"
function padright(str, cnt, pad) { function padright(str, cnt, pad) {
@@ -32,13 +42,6 @@ function b64DecodeUnicode(str) {
}).join('')); }).join(''));
} }
function normalizeHost(host) {
if (host && host[0] === "[" && host[host.length - 1] === "]") {
return host.slice(1, -1);
}
return host;
}
function b64decutf8safe(str) { function b64decutf8safe(str) {
var l; var l;
str = str.replace(/-/g, "+").replace(/_/g, "/"); str = str.replace(/-/g, "+").replace(/_/g, "/");
@@ -122,9 +125,38 @@ function import_ssr_url(btn, urlname, sid) {
return false; return false;
} }
// Xray Hysteria2 导入函数 // 普通 Hysteria2 导入函数
function importAsXrayHy2() { function importAsNormalHy2() {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = "v2ray" 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"
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event); document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0]; document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = (ssu[0] === "hy2") ? "hysteria2" : ssu[0];
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].dispatchEvent(event); document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].dispatchEvent(event);
@@ -152,14 +184,36 @@ function import_ssr_url(btn, urlname, sid) {
} }
} }
if (has_xray !== "true") { // 主逻辑:智能核心选择(支持回退)
s.innerHTML = "<font style=\'color:red\'><%:No available Xray core to import this Hysteria2 node.%></font>"; function hasCore(core) {
return false; if (core === "hysteria") return has_hysteria === "true";
} if (core === "xray") return has_xray === "true";
importAsXrayHy2(); 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();
document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : ""; document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = url.hash ? decodeURIComponent(url.hash.slice(1)) : "";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = normalizeHost(url.hostname); document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port || "443"; 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].value = decodeURIComponent(url.username);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].dispatchEvent(event); document.getElementsByName('cbid.shadowsocksr.' + sid + '.hy2_auth')[0].dispatchEvent(event);
@@ -194,9 +248,9 @@ function import_ssr_url(btn, urlname, sid) {
s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>"; s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
return false; return false;
case "ss": case "ss":
var url0 = (ssu[1] || ""); var url0 = (ssu[1] || "");
var param = ""; var param = "";
// 先分离 #(alias) // 先分离 #(alias)
var hashIndex = url0.indexOf("#"); var hashIndex = url0.indexOf("#");
@@ -213,346 +267,374 @@ function import_ssr_url(btn, urlname, sid) {
url0 = url0.substring(0, queryIndex); url0 = url0.substring(0, queryIndex);
} }
var params = Object.fromEntries(new URLSearchParams(query)); var params = Object.fromEntries(new URLSearchParams(query));
function decodeURIComponentSafe(value) { function importAsNormalSS() {
try { // 普通 SS 导入逻辑
return decodeURIComponent(value); // 判断是否 SIP002 格式(即含 @)
} catch (e) { if (url0.indexOf("@") !== -1) {
return value; // === 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;
} }
} // 如果没有冒号,且不是 base64 格式,我们可以尝试补齐它
if (userInfo.indexOf(":") === -1) {
function decodeBase64Safe(value) { userInfo = "none:" + userInfo;
try {
return b64decsafe(value);
} catch (e) {
return "";
} }
} // console.log("userInfo after decode:", userInfo);
var userInfoSplitIndex = userInfo.indexOf(":");
function parseSSHostPort(serverPart) { if(userInfoSplitIndex < 0) {
serverPart = (serverPart || "").trim(); // 格式错误
var ipv6Match = serverPart.match(/^\[([^\]]+)\]:(\d+)$/); s.innerHTML = "<font style='color:red'><%:Userinfo format error.%></font>";
if (ipv6Match) { return false;
return { server: ipv6Match[1], port: ipv6Match[2] };
} }
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 colonIndex = serverPart.lastIndexOf(":"); var server = serverInfo[0];
if (colonIndex > 0 && serverPart.indexOf(":") === colonIndex) { var port = serverInfo[1];
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) { if (params.plugin) {
var pluginInfo = decodeURIComponentSafe(params.plugin); var pluginParams = decodeURIComponent(params.plugin).split(";");
var pluginParts = pluginInfo.split(";"); plugin = pluginParams.shift();
plugin = pluginParts.shift() || ""; pluginOpts = pluginParams.join(";");
pluginOpts = pluginParts.join(";"); }
if (plugin === "simple-obfs") { } else {
plugin = "obfs-local"; // === Base64 SS2022 / 普通格式 的整体编码格式 ===
} // 先 URL 解码整个字符串
} else if (params["shadow-tls"]) { 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"]) {
try { try {
var decodedTls = JSON.parse(b64decsafe(decodeURIComponentSafe((params["shadow-tls"] || "").replace(/ /g, "+")))); var decoded_tls = JSON.parse(atob(decodeURIComponent(params["shadow-tls"])));
plugin = "shadow-tls"; plugin = "shadow-tls";
var versionFlag = ""; var versionFlag = "";
if (decodedTls.version && !isNaN(decodedTls.version)) { if (decoded_tls.version && !isNaN(decoded_tls.version)) {
versionFlag = "v" + decodedTls.version + "=1;"; versionFlag = "v" + decoded_tls.version + "=1;";
} }
pluginOpts = versionFlag + "host=" + (decodedTls.host || "") + ";passwd=" + (decodedTls.password || ""); pluginOpts = versionFlag + "host=" + (decoded_tls.host || "") + ";passwd=" + (decoded_tls.password || "");
} catch (e) { } catch (e) {
console.log("shadow-tls decode failed:", e); console.log("shadow-tls decode failed:", e);
} }
} }
return {
plugin: plugin,
pluginOpts: pluginOpts
};
} }
function parseSSNode() { // 判断密码是否经过url编码
var userInfo = ""; const isURLEncodedPassword = function(pwd) {
var serverPart = ""; if (!/%[0-9A-Fa-f]{2}/.test(pwd)) return false;
var atIndex = url0.lastIndexOf("@"); try {
const decoded = decodeURIComponent(pwd.replace(/\+/g, "%20"));
if (atIndex !== -1) { const reencoded = encodeURIComponent(decoded);
var rawUserInfo = decodeURIComponentSafe(url0.substring(0, atIndex)); return reencoded === pwd;
var decodedUserInfo = decodeBase64Safe(rawUserInfo); } catch (e) {
userInfo = decodedUserInfo && decodedUserInfo.indexOf(":") !== -1 ? decodedUserInfo : rawUserInfo; return false;
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 (userInfo.indexOf(":") === -1) {
userInfo = "none:" + userInfo;
}
var userInfoSplitIndex = userInfo.indexOf(":");
if (userInfoSplitIndex < 0) {
return { error: "<%:Userinfo format error.%>" };
}
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." };
}
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;
}
}
if (isURLEncodedPassword(password)) {
password = decodeURIComponent(password);
}
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) : ""
};
} }
if (isURLEncodedPassword(password)) {
function importAsNormalSS(ssNode) { password = decodeURIComponent(password); // 解码URL编码
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;
}
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 { } else {
finalSSBackend = preferredSSBackend(); password = password; // 保持原始值
} }
if (finalSSBackend === null) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Shadowsocks or Xray) to import this node.%></font>"; // === 填充配置项 ===
//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 || "";
}
} 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); // 触发事件
//}
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); // 触发事件
}
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做密码
}
}
// 已编码的别名
// 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 : ""));
} catch(e) {
alert(e);
return false; return false;
} }
if (finalSSBackend === "v2ray" && ssNode.plugin && ssNode.plugin !== "none") { // Check if the elements exist before trying to modify them
s.innerHTML = "<font style='color:red'>Xray fallback does not support importing Shadowsocks links with SIP003 plugins.</font>"; function setElementValue(name, value) {
return false; const element = document.getElementsByName(name)[0];
if (element) {
if (typeof value === 'boolean') {
element.checked = value;
} else {
element.value = value;
}
}
} }
if (finalSSBackend === "v2ray") importAsXraySS(ssNode); function dispatchEventIfExists(name, event) {
else importAsNormalSS(ssNode); 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;
} else {
if (hasSSCore()) finalSSType = false;
else if (hasXrayCore()) finalSSType = true;
else finalSSType = null;
}
}
if (finalSSType === null) {
s.innerHTML = "<font style=\'color:red\'><%:No available core (Shadowsocks or Xray) to import this node.%></font>";
return false; return false;
}
if (finalSSType === true) importAsXraySS();
else importAsNormalSS();
return false;
case "ssr": case "ssr":
var sstr = b64decsafe((ssu[1] || "").replace(/#.*/, "").trim()); var sstr = b64decsafe((ssu[1] || "").replace(/#.*/, "").trim());
var ploc = sstr.indexOf("/?"); var ploc = sstr.indexOf("/?");
@@ -603,7 +685,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 + '.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].value = "trojan";
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event); document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = normalizeHost(url.hostname); document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port || "80"; 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 + '.password')[0].value = decodeURIComponent(url.username);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true;
@@ -632,7 +714,7 @@ function import_ssr_url(btn, urlname, sid) {
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event); document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.v2ray_protocol')[0].value = "trojan"; 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 + '.v2ray_protocol')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = normalizeHost(url.hostname); document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port || "80"; 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 + '.password')[0].value = decodeURIComponent(url.username);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true; document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true;
@@ -732,13 +814,36 @@ function import_ssr_url(btn, urlname, sid) {
return true; return true;
} }
// 主逻辑:Trojan 统一使用 Xray // 主逻辑:智能核心选择(支持回退)
function hasTrojanCore() { return has_trojan === "true"; }
function hasXrayCore() { return has_xray === "true"; } function hasXrayCore() { return has_xray === "true"; }
if (!hasXrayCore()) { let finalTrojanMode = null;
s.innerHTML = "<font style=\'color:red\'><%:No available core (Xray) to import this node.%></font>"; 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>";
return false; return false;
} }
importAsXrayTrojan(); if (finalTrojanMode === true) importAsXrayTrojan();
else importAsNormalTrojan();
return false; return false;
case "vmess": case "vmess":
var sstr = b64DecodeUnicode((ssu[1] || "").replace(/#.*/, "").trim()); var sstr = b64DecodeUnicode((ssu[1] || "").replace(/#.*/, "").trim());
@@ -871,7 +976,7 @@ function import_ssr_url(btn, urlname, sid) {
element.dispatchEvent(event); element.dispatchEvent(event);
} }
} }
var server = normalizeHost(url.hostname); var server = url.hostname;
/* 如果带 [],再去掉 [] */ /* 如果带 [],再去掉 [] */
if (server[0] === "[" && server[server.length - 1] === "]") { if (server[0] === "[" && server[server.length - 1] === "]") {
server = server.slice(1, -1); server = server.slice(1, -1);
@@ -1036,7 +1141,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].value = ssu[0];
document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event); document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = normalizeHost(url.hostname); document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = url.hostname;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = url.port; 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_uuid')[0].value = method;
document.getElementsByName('cbid.shadowsocksr.' + sid + '.tuic_ip')[0].value = params.ip || ""; document.getElementsByName('cbid.shadowsocksr.' + sid + '.tuic_ip')[0].value = params.ip || "";
@@ -4,8 +4,7 @@ XHR.poll(3, '<%=url([[admin]], [[services]], [[shadowsocksr]], [[run]])%>', null
var tb = document.getElementById('shadowsocksr_status'); var tb = document.getElementById('shadowsocksr_status');
if (data && tb) { if (data && tb) {
if (data.running) { if (data.running) {
var text = data.status_text || '<%:RUNNING%>'; var links = '<em><b style=color:green>ShadowsocksR Plus+ <%:RUNNING%></b></em>';
var links = '<em><b style=color:green>ShadowsocksR Plus+ ' + text + '</b></em>';
tb.innerHTML = links; tb.innerHTML = links;
} else { } else {
tb.innerHTML = '<em><b style=color:red>ShadowsocksR Plus+ <%:NOT RUNNING%></b></em>'; tb.innerHTML = '<em><b style=color:red>ShadowsocksR Plus+ <%:NOT RUNNING%></b></em>';
@@ -1,257 +1,18 @@
<%+cbi/valueheader%> <%+cbi/valueheader%>
<% <script type="text/javascript">//<![CDATA[
local dsp = require "luci.dispatcher" function subscribe(btn,dataname) {
local subscribe_url = dsp.build_url("admin", "services", "shadowsocksr", "subscribe") btn.disabled = true;
local get_log_url = dsp.build_url("admin", "services", "shadowsocksr", "get_log") btn.value = '<%:Refresh...%> ';
local servers_url = dsp.build_url("admin", "services", "shadowsocksr", "servers") murl=dataname;
local autostart = self.map and self.map.ssr_subscribe_autostart XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr","subscribe")%>', { set:murl }, function(x,rv) {
-%> // 先简单刷新,后期如果重构会考虑下如何组织lua shell JavaScript之间的代码逻辑和各自的调用逻辑
<div id="ssr-subscribe-modal-wrap"> window.location.reload()
<style type="text/css"> // btn.disabled = false;
#ssr-subscribe-modal-mask { // btn.value = '<%:Refresh Data %>';
display: none; });
position: fixed; return false;
inset: 0; }
background: rgba(0, 0, 0, 0.35); //]]></script>
z-index: 2000; <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> -->
#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%> <%+cbi/valuefooter%>
@@ -1,490 +0,0 @@
<%-
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>
@@ -1,47 +0,0 @@
<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>
@@ -1,182 +0,0 @@
<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
@@ -1,22 +0,0 @@
#!/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
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,192 @@
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,8 +31,11 @@ touch /etc/ssrplus/china_ssr.txt
touch /etc/ssrplus/deny.list touch /etc/ssrplus/deny.list
touch /etc/ssrplus/white.list touch /etc/ssrplus/white.list
touch /etc/ssrplus/black.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_base.conf
touch /etc/ssrplus/gfw_list.conf touch /etc/ssrplus/gfw_list.conf
touch /etc/ssrplus/oversea_list.conf
touch /etc/ssrplus/ad.conf touch /etc/ssrplus/ad.conf
touch /etc/config/shadowsocksr touch /etc/config/shadowsocksr
@@ -47,20 +50,16 @@ if [ -s "/etc/config/shadowsocksr" ]; then
uci -q set shadowsocksr.@server_subscribe[0].auto_update_min_time='0' uci -q set shadowsocksr.@server_subscribe[0].auto_update_min_time='0'
fi fi
if ! uci -q get shadowsocksr.@server_subscribe[0].config_auto_update_mode > /dev/null; then if ! uci -q get shadowsocksr.@server_subscribe[0].ss_type > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].config_auto_update_mode='0' uci -q set shadowsocksr.@server_subscribe[0].ss_type='ss-rust'
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 fi
if ! uci -q get shadowsocksr.@server_subscribe[0].user_agent > /dev/null; then 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' uci -q set shadowsocksr.@server_subscribe[0].user_agent='v2rayN/9.99'
fi fi
if ! uci -q get shadowsocksr.@server_subscribe[0].proxy > /dev/null; then if ! uci -q get shadowsocksr.@server_subscribe[0].xray_hy2_type > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].proxy='1' uci -q set shadowsocksr.@server_subscribe[0].xray_hy2_type='hysteria2'
fi fi
if ! uci -q get shadowsocksr.@global_xray_fragment[0] > /dev/null; then if ! uci -q get shadowsocksr.@global_xray_fragment[0] > /dev/null; then
@@ -69,61 +68,8 @@ if [ -s "/etc/config/shadowsocksr" ]; then
uci -q set shadowsocksr.@global_xray_fragment[0].noise='0' uci -q set shadowsocksr.@global_xray_fragment[0].noise='0'
fi fi
if ! uci -q get shadowsocksr.@global[0].component_mirror > /dev/null; then uci -q commit shadowsocksr
uci -q set shadowsocksr.@global[0].component_mirror='direct' fi
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 [ -s "/etc/config/shadowsocksr" ] || /etc/init.d/shadowsocksr reset
+107 -88
View File
@@ -10,58 +10,6 @@
LOCK_FILE="/var/lock/ssr-monitor.lock" LOCK_FILE="/var/lock/ssr-monitor.lock"
[ -f "$LOCK_FILE" ] && exit 2 [ -f "$LOCK_FILE" ] && exit 2
touch "$LOCK_FILE" 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 server_process_count=$1
redir_tcp_process=$2 redir_tcp_process=$2
redir_udp_process=$3 redir_udp_process=$3
@@ -73,8 +21,6 @@ if [ -z "$pdnsd_process" ]; then
fi fi
i=0 i=0
GLOBAL_SERVER=$(uci_get_by_type global global_server) 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) server=$(uci_get_by_name $GLOBAL_SERVER server)
kcp_port=$(uci_get_by_name $GLOBAL_SERVER kcp_port) kcp_port=$(uci_get_by_name $GLOBAL_SERVER kcp_port)
server_port=$(uci_get_by_name $GLOBAL_SERVER server_port) server_port=$(uci_get_by_name $GLOBAL_SERVER server_port)
@@ -86,12 +32,8 @@ while [ "1" == "1" ]; do #死循环
sleep 000030s sleep 000030s
#redir tcp #redir tcp
if [ "$redir_tcp_process" -gt 0 ]; then if [ "$redir_tcp_process" -gt 0 ]; then
icount=$(redir_tcp_running) icount=$(busybox ps -w | grep ssr-retcp | grep -v grep | wc -l)
local tcp_expected="$redir_tcp_process" if [ "$icount" == 0 ]; then
if [ "$GLOBAL_TYPE" = "socks5" ]; then
tcp_expected="$EXPECTED_THREADS"
fi
if [ "$icount" -lt "$tcp_expected" ]; then
logger -t "$NAME" "ssrplus redir tcp error.restart!" logger -t "$NAME" "ssrplus redir tcp error.restart!"
echolog "ssrplus redir tcp error.restart!" echolog "ssrplus redir tcp error.restart!"
/etc/init.d/shadowsocksr restart /etc/init.d/shadowsocksr restart
@@ -100,12 +42,8 @@ while [ "1" == "1" ]; do #死循环
fi fi
#redir udp #redir udp
if [ "$redir_udp_process" -gt 0 ]; then if [ "$redir_udp_process" -gt 0 ]; then
icount=$(redir_udp_running) icount=$(busybox ps -w | grep ssr-reudp | grep -v grep | wc -l)
local udp_expected="$redir_udp_process" if [ "$icount" == 0 ]; then
if [ "$GLOBAL_TYPE" = "socks5" ]; then
udp_expected="$EXPECTED_THREADS"
fi
if [ "$icount" -lt "$udp_expected" ]; then
logger -t "$NAME" "ssrplus redir udp error.restart!" logger -t "$NAME" "ssrplus redir udp error.restart!"
echolog "ssrplus redir udp error.restart!" echolog "ssrplus redir udp error.restart!"
/etc/init.d/shadowsocksr restart /etc/init.d/shadowsocksr restart
@@ -114,18 +52,18 @@ while [ "1" == "1" ]; do #死循环
fi fi
#server #server
if [ "$server_process_count" -gt 0 ]; then if [ "$server_process_count" -gt 0 ]; then
icount=$(match_proc_count 'ssr-server|ss-server-|xray-server-') icount=$(busybox ps -w | grep ssr-server | grep -v grep | wc -l)
if [ "$icount" -lt "$server_process_count" ]; then #如果进程挂掉就重启它 if [ "$icount" -lt "$server_process_count" ]; then #如果进程挂掉就重启它
logger -t "$NAME" "ssrplus server error.restart!" logger -t "$NAME" "ssrplus server error.restart!"
echolog "ssrplus server error.restart!" echolog "ssrplus server error.restart!"
kill_matched_procs 'ssr-server|ss-server-' kill -9 $(busybox ps -w | grep ssr-server | grep -v grep | awk '{print $1}') >/dev/null 2>&1
/etc/init.d/shadowsocksr restart /etc/init.d/shadowsocksr restart
exit 0 exit 0
fi fi
fi fi
#kcptun #kcptun
if [ "$kcp_process" -gt 0 ]; then if [ "$kcp_process" -gt 0 ]; then
icount=$(ps_list | grep kcptun-client | grep -v grep | wc -l) icount=$(busybox ps -w | grep kcptun-client | grep -v grep | wc -l)
if [ "$icount" -lt "$kcp_process" ]; then #如果进程挂掉就重启它 if [ "$icount" -lt "$kcp_process" ]; then #如果进程挂掉就重启它
logger -t "$NAME" "ssrplus kcptun error.restart!" logger -t "$NAME" "ssrplus kcptun error.restart!"
echolog "ssrplus kcptun error.restart!" echolog "ssrplus kcptun error.restart!"
@@ -135,43 +73,132 @@ while [ "1" == "1" ]; do #死循环
fi fi
#localsocks #localsocks
if [ "$local_process" -gt 0 ]; then if [ "$local_process" -gt 0 ]; then
icount=$(match_proc_count 'ssr-local|ss-local|tuic-local') icount=$(busybox ps -w | grep ssr-local | grep -v grep | wc -l)
if [ "$icount" -lt "$local_process" ]; then #如果进程挂掉就重启它 if [ "$icount" -lt "$local_process" ]; then #如果进程挂掉就重启它
logger -t "$NAME" "global socks server error.restart!" logger -t "$NAME" "global socks server error.restart!"
echolog "global socks server error.restart!" echolog "global socks server error.restart!"
kill_matched_procs 'ssr-local|ss-local|tuic-local' kill -9 $(busybox ps -w | grep ssr-local | grep -v grep | awk '{print $1}') >/dev/null 2>&1
/etc/init.d/shadowsocksr restart /etc/init.d/shadowsocksr restart
exit 0 exit 0
fi fi
fi fi
#dns2tcp #dns2tcp
if [ "$pdnsd_process" -eq 1 ]; then if [ "$pdnsd_process" -eq 1 ]; then
icount=$(ps_list | grep $TMP_BIN_PATH/dns2tcp | grep -v grep | wc -l) icount=$(busybox ps -w | grep $TMP_BIN_PATH/dns2tcp | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它 if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "dns2tcp tunnel error.restart!" logger -t "$NAME" "dns2tcp tunnel error.restart!"
echolog "dns2tcp tunnel error.restart!" echolog "dns2tcp tunnel error.restart!"
dnsserver=$(uci_get_by_type global tunnel_forward 8.8.4.4:53) dnsserver=$(uci_get_by_type global tunnel_forward 8.8.4.4:53)
kill -9 $(ps_list | grep $TMP_BIN_PATH/dns2tcp | grep -v grep | awk '{print $1}') >/dev/null 2>&1 kill -9 $(busybox ps -w | grep $TMP_BIN_PATH/dns2tcp | grep -v grep | awk '{print $1}') >/dev/null 2>&1
start_dns2tcp "$dnsserver" 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
fi fi
#mosdns #mosdns
elif [ "$pdnsd_process" -eq 4 ]; then elif [ "$pdnsd_process" -eq 4 ]; then
icount=$(ps_list | grep $TMP_BIN_PATH/mosdns | grep -v grep | wc -l) icount=$(busybox ps -w | grep $TMP_BIN_PATH/mosdns | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它 if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "mosdns tunnel error.restart!" logger -t "$NAME" "mosdns tunnel error.restart!"
echolog "mosdns tunnel error.restart!" echolog "mosdns tunnel error.restart!"
dnsserver=$(uci_get_by_type global tunnel_forward 8.8.4.4:53) dnsserver=$(uci_get_by_type global tunnel_forward 8.8.4.4:53)
kill -9 $(ps_list | grep $TMP_BIN_PATH/mosdns | grep -v grep | awk '{print $1}') >/dev/null 2>&1 kill -9 $(busybox ps -w | 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 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 fi
#chinadns-ng(proxy) #chinadns-ng(proxy)
elif [ "$pdnsd_process" -eq 6 ]; then elif [ "$pdnsd_process" -eq 6 ]; then
icount=$(ps_list | grep -e ssrplus-dns -e "chinadns-ng -b 127.0.0.1 -l $tmp_dns_port" | grep -v grep | wc -l) 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)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它 if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "chinadns-ng $dnsserver tunnel error.restart!" logger -t "$NAME" "chinadns-ng $dnsserver tunnel error.restart!"
echolog "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) dnsserver=$(uci_get_by_type global chinadns_ng_tunnel_forward 8.8.4.4:53)
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 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
local chinadns_ng_proto="$(uci_get_by_type global chinadns_ng_proto)" local chinadns_ng_proto="$(uci_get_by_type global chinadns_ng_proto)"
local chinadns_ng_dns="" local chinadns_ng_dns=""
IFS=',' IFS=','
@@ -197,18 +224,10 @@ while [ "1" == "1" ]; do #死循环
dnsserver="$chinadns_ng_dns" 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 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 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 fi
#chinadns-ng(china) #chinadns-ng(china)
if [ "$(uci -q get "dhcp.@dnsmasq[0]._unused_ssrp_changed")" = "1" ]; then if [ "$(uci -q get "dhcp.@dnsmasq[0]._unused_ssrp_changed")" = "1" ]; then
icount=$(ps_list | grep $TMP_BIN_PATH/chinadns-ng | grep -v grep | wc -l) icount=$(busybox ps -w | grep $TMP_BIN_PATH/chinadns-ng | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它 if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "chinadns-ng tunnel error.restart!" logger -t "$NAME" "chinadns-ng tunnel error.restart!"
echolog "chinadns-ng tunnel error.restart!" echolog "chinadns-ng tunnel error.restart!"
@@ -218,7 +237,7 @@ while [ "1" == "1" ]; do #死循环
"wan") chinadns="$wandns" ;; "wan") chinadns="$wandns" ;;
""|"wan_114") chinadns="$wandns,114.114.114.114" ;; ""|"wan_114") chinadns="$wandns,114.114.114.114" ;;
esac esac
kill -9 $(ps_list | grep $TMP_BIN_PATH/chinadns-ng | grep -v grep | awk '{print $1}') >/dev/null 2>&1 kill -9 $(busybox ps -w | 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 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
fi fi
+105 -41
View File
@@ -85,14 +85,13 @@ usage() {
-e <extra_options> extra options for iptables -e <extra_options> extra options for iptables
-o apply the rules to the OUTPUT chain -o apply the rules to the OUTPUT chain
-O apply the global 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, TPROXY is required
-U enable udprelay mode, using different IP -U enable udprelay mode, using different IP
and ports for TCP and UDP and ports for TCP and UDP
-y disable all auxiliary UDP rules when UDP -f flush the rules
transparent proxy is unavailable
-f flush the rules
-g gfwlist mode -g gfwlist mode
-r router mode -r router mode
-c oversea mode
-z all mode -z all mode
# New persistence management options (use different letters to avoid conflicts) # New persistence management options (use different letters to avoid conflicts)
@@ -275,7 +274,7 @@ flush_nftables() {
# Optional: force delete all ss_spec related sets (even if table was accidentally deleted) # 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 \ for setname in ss_spec_lan_ac ss_spec_wan_ac ssr_gen_router \
china fplan bplan gmlan whitelist blacklist gfwlist music; do china fplan bplan gmlan oversea whitelist blacklist netflix gfwlist music; do
$NFT delete set inet ss_spec $setname 2>/dev/null $NFT delete set inet ss_spec $setname 2>/dev/null
$NFT delete set ip ss_spec_mangle $setname 2>/dev/null $NFT delete set ip ss_spec_mangle $setname 2>/dev/null
done done
@@ -311,7 +310,7 @@ flush_iptables_legacy() {
ip route del local 0.0.0.0/0 dev lo table 999 2>/dev/null ip route del local 0.0.0.0/0 dev lo table 999 2>/dev/null
fi fi
for setname in ss_spec_lan_ac ss_spec_wan_ac ssr_gen_router \ for setname in ss_spec_lan_ac ss_spec_wan_ac ssr_gen_router \
china fplan bplan gmlan whitelist blacklist gfwlist music; do china fplan bplan gmlan oversea whitelist blacklist netflix gfwlist music; do
ipset -X $setname 2>/dev/null ipset -X $setname 2>/dev/null
done done
[ -n "$FWI" ] && echo '#!/bin/sh' >$FWI [ -n "$FWI" ] && echo '#!/bin/sh' >$FWI
@@ -334,7 +333,7 @@ ipset_nft() {
fi fi
# Create necessary collections # Create necessary collections
for setname in china gmlan fplan bplan whitelist blacklist music; do for setname in china gmlan fplan bplan whitelist blacklist netflix music; do
if ! $NFT list set inet ss_spec $setname >/dev/null 2>&1; then 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 $NFT add set inet ss_spec $setname '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
else else
@@ -393,6 +392,13 @@ ipset_nft() {
$NFT add rule inet ss_spec ss_spec_wan_ac meta l4proto tcp ip daddr @music return $NFT add rule inet ss_spec ss_spec_wan_ac meta l4proto tcp ip daddr @music return
fi 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 # Set up mode-specific rules
case "$RUNMODE" in case "$RUNMODE" in
router) router)
@@ -421,6 +427,14 @@ 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 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 $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) all)
if $NFT list chain inet ss_spec ss_spec_wan_fw >/dev/null 2>&1; then 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 $NFT add rule inet ss_spec ss_spec_wan_ac jump ss_spec_wan_fw
@@ -436,10 +450,9 @@ ipset_iptables() {
$IPT -N SS_SPEC_WAN_AC 2>/dev/null $IPT -N SS_SPEC_WAN_AC 2>/dev/null
$IPT -F SS_SPEC_WAN_AC $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 127.0.0.0/8 -j RETURN
[ -n "$server" ] && $IPT -I SS_SPEC_WAN_AC -p tcp ! --dport 53 -d "$server" -j RETURN $IPT -I SS_SPEC_WAN_AC -p tcp ! --dport 53 -d "$server" -j RETURN
ipset -N gmlan hash:net 2>/dev/null ipset -N gmlan hash:net 2>/dev/null
for ip in $LAN_GM_IP; do ipset -! add gmlan "$ip"; done for ip in $LAN_GM_IP; do ipset -! add gmlan "$ip"; done
@@ -461,6 +474,12 @@ 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 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 $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) all)
$IPT -A SS_SPEC_WAN_AC -j SS_SPEC_WAN_FW $IPT -A SS_SPEC_WAN_AC -j SS_SPEC_WAN_FW
;; ;;
@@ -491,6 +510,25 @@ ipset_iptables() {
for ip in $WAN_BP_IP; do ipset -! add whitelist "$ip"; done for ip in $WAN_BP_IP; do ipset -! add whitelist "$ip"; done
for ip in $WAN_FW_IP; do ipset -! add blacklist "$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 $? return $?
} }
@@ -524,6 +562,22 @@ fw_rule_nft() {
fi fi
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 $? return $?
} }
@@ -616,7 +670,7 @@ ac_rule_nft() {
fi fi
# Block UDP port 443 when TPROXY not Enable # Block UDP port 443 when TPROXY not Enable
if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then if [ -z "$TPROXY" ]; then
# Add UDP 443 block rule # Add UDP 443 block rule
if [ -z "$Interface" ]; then if [ -z "$Interface" ]; then
if [ -n "$MATCH_SET" ]; then if [ -n "$MATCH_SET" ]; then
@@ -663,11 +717,10 @@ ac_rule_nft() {
case "$OUTPUT" in case "$OUTPUT" in
1) 1)
# Create ss_spec_output tcp chain # Create ss_spec_output tcp chain
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then 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; }' $NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null $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 # Exclude special local addresses
if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
@@ -688,11 +741,10 @@ ac_rule_nft() {
;; ;;
2) 2)
# Create ss_spec_output tcp chain # Create ss_spec_output tcp chain
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then 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; }' $NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null $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 # Exclude special local addresses
if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
@@ -706,12 +758,11 @@ ac_rule_nft() {
for ip in $(gen_spec_iplist); do for ip in $(gen_spec_iplist); do
[ -n "$ip" ] && $NFT add element inet ss_spec ssr_gen_router "{ $ip }" 2>/dev/null [ -n "$ip" ] && $NFT add element inet ss_spec ssr_gen_router "{ $ip }" 2>/dev/null
done done
if ! $NFT list chain inet ss_spec ss_spec_router >/dev/null 2>&1; then 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 $NFT add chain inet ss_spec ss_spec_router 2>/dev/null
fi fi
$NFT flush chain inet ss_spec ss_spec_router 2>/dev/null $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 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_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 $NFT add rule inet ss_spec ss_spec_output $TCP_EXT_ARGS jump ss_spec_router comment "\"$TAG\"" 2>/dev/null
;; ;;
@@ -741,7 +792,7 @@ ac_rule_iptables() {
EOF EOF
# Block UDP port 443 when TPROXY not Enable # Block UDP port 443 when TPROXY not Enable
if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then if [ -z "$TPROXY" ]; then
# Add UDP 443 block rule # Add UDP 443 block rule
if [ -z "$Interface" ]; then if [ -z "$Interface" ]; then
# Global rules # Global rules
@@ -794,11 +845,10 @@ ac_rule_iptables() {
create ssr_gen_router hash:net create ssr_gen_router hash:net
$(gen_spec_iplist | sed -e "s/^/add ssr_gen_router /") $(gen_spec_iplist | sed -e "s/^/add ssr_gen_router /")
EOF EOF
$IPT -N SS_SPEC_ROUTER 2>/dev/null $IPT -N SS_SPEC_ROUTER 2>/dev/null
$IPT -F 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 -m set --match-set ssr_gen_router dst -j RETURN && \ $IPT -A SS_SPEC_ROUTER -j SS_SPEC_WAN_FW
$IPT -A SS_SPEC_ROUTER -j SS_SPEC_WAN_FW
$IPT -I OUTPUT 1 -p tcp -m comment --comment "$TAG" -j SS_SPEC_ROUTER $IPT -I OUTPUT 1 -p tcp -m comment --comment "$TAG" -j SS_SPEC_ROUTER
;; ;;
esac esac
@@ -956,6 +1006,14 @@ 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 $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 $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) 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 $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
;; ;;
@@ -1032,8 +1090,8 @@ tp_rule_iptables() {
do do
$ipt -A SS_SPEC_TPROXY -p udp -d "$net" -j RETURN $ipt -A SS_SPEC_TPROXY -p udp -d "$net" -j RETURN
done done
[ -n "$SERVER" ] && $ipt -A SS_SPEC_TPROXY -p udp ! --dport 53 -d "$SERVER" -j RETURN $ipt -A SS_SPEC_TPROXY -p udp ! --dport 53 -d "$SERVER" -j RETURN
[ -n "$SERVER" ] && [ "$server" != "$SERVER" ] && ipset -! add whitelist "$SERVER" [ "$server" != "$SERVER" ] && ipset -! add whitelist "$SERVER"
if [ -f "${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}" ]; then if [ -f "${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}" ]; then
while IFS= read -r ip; do while IFS= read -r ip; do
[ -n "$ip" ] && ipset add whitelist "$ip" -exist [ -n "$ip" ] && ipset add whitelist "$ip" -exist
@@ -1061,6 +1119,11 @@ 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 $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} $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) all)
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK} $ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
;; ;;
@@ -1432,7 +1495,7 @@ restore_from_persistence() {
fi fi
} }
while getopts ":m:s:l:S:L:i:e:a:B:b:w:p:G:D:F:N:M:I:oOuUyfgrzAKPCRXh" arg; do while getopts ":m:s:l:S:L:i:e:a:B:b:w:p:G:D:F:N:M:I:oOuUfgrczAKPCRXh" arg; do
case "$arg" in case "$arg" in
m) m)
Interface=$OPTARG Interface=$OPTARG
@@ -1500,15 +1563,15 @@ while getopts ":m:s:l:S:L:i:e:a:B:b:w:p:G:D:F:N:M:I:oOuUyfgrzAKPCRXh" arg; do
U) U)
TPROXY=2 TPROXY=2
;; ;;
y)
DISABLE_UDP_RULES=1
;;
g) g)
RUNMODE=gfw RUNMODE=gfw
;; ;;
r) r)
RUNMODE=router RUNMODE=router
;; ;;
c)
RUNMODE=oversea
;;
z) z)
RUNMODE=all RUNMODE=all
;; ;;
@@ -1640,7 +1703,7 @@ runmode_change() {
} }
# Main process # Main process
if [ -n "$local_port" ]; then if [ -n "$server" ] && [ -n "$local_port" ]; then
if ! echo "$local_port" | grep -qE '^[0-9]+$'; then if ! echo "$local_port" | grep -qE '^[0-9]+$'; then
loger 3 "Invalid local port: $local_port" loger 3 "Invalid local port: $local_port"
exit 1 exit 1
@@ -1652,6 +1715,7 @@ if [ -n "$local_port" ]; then
LOCAL_PORT=$local_port LOCAL_PORT=$local_port
;; ;;
2) 2)
: ${SERVER:?"You must assign an ip for the udp relay server."}
: ${LOCAL_PORT:?"You must assign a port for the udp relay server."} : ${LOCAL_PORT:?"You must assign a port for the udp relay server."}
;; ;;
esac esac
+116 -243
View File
@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh /etc/rc.common
# #
# Copyright (C) 2017 openwrt-ssr # Copyright (C) 2017 openwrt-ssr
# Copyright (C) 2017 yushi studio <ywb94@qq.com> # Copyright (C) 2017 yushi studio <ywb94@qq.com>
@@ -7,276 +7,149 @@
# See /LICENSE for more information. # 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 . $IPKG_INSTROOT/etc/init.d/shadowsocksr
LOCK_FILE="/var/lock/ssr-switch.lock" LOCK_FILE="/var/lock/ssr-switch.lock"
PROBE_SID="" [ -f "$LOCK_FILE" ] && exit 2
touch "$LOCK_FILE"
LOG_FILE=/var/log/ssrplus.log
cycle_time=60 cycle_time=60
switch_time=3 switch_time=3
probe_socks_port=10800 normal_flag=0
direct_fail_count=0 server_locate=0
direct_fail_limit=3 server_count=0
probe_url="$(uci_get_by_type server_subscribe url_test_url)" ENABLE_SERVER=nil
[ -n "$probe_url" ] || probe_url="https://www.google.com/generate_204" [ -n "$1" ] && cycle_time=$1
PROBE_INSTANCE_KEY="ssr-switch-probe-local" [ -n "$2" ] && switch_time=$2
DEFAULT_SERVER=$(uci_get_by_type global global_server)
CURRENT_SERVER=$DEFAULT_SERVER
DEFAULT_SERVER="$(uci_get_by_type global global_server)" #判断代理是否正常
CURRENT_SERVER="$DEFAULT_SERVER" check_proxy() {
ENABLE_SERVER="nil" local result=0
local try_count=$(uci_get_by_type global switch_try_count 3)
cleanup_probe_socks() { for i in $(seq 1 $try_count); do
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 /usr/bin/ssr-check www.google.com 80 $switch_time 1
sleep 1 if [ "$?" == "0" ]; then
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 # echolog "Check Google Proxy Success, count=$i"
rm -f "$TMP_PATH/tcp-http-ssr-switch-local.json" 2>/dev/null result=0
rm -rf "$TMP_PATH/ss-${PROBE_INSTANCE_KEY}" "$TMP_PATH/tuic-${PROBE_INSTANCE_KEY}" 2>/dev/null break
rm -f /tmp/ssr-switch-probe.log 2>/dev/null else
} # echolog "Check Google Proxy Fail, count=$i"
/usr/bin/ssr-check www.baidu.com 80 $switch_time 1
start_probe_socks() { if [ "$?" == "0" ]; then
local sid="$1" result=1
local server_type else
result=2
server_type="$(uci_get_by_name "$sid" type)" fi
[ "$server_type" = "clash" ] && return 1 fi
[ "$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 sleep 1
done done
return 1 return $result
} }
test_proxy() { test_proxy() {
local sid="$1" local servername=$(uci_get_by_name $1 server)
[ "$(uci_get_by_name "$sid" type)" = "clash" ] && return 1 local serverport=$(uci_get_by_name $1 server_port)
[ "$(uci_get_by_name "$sid" type)" = "tuic" ] && return 1 ipset add whitelist $servername 2>/dev/null
start_probe_socks "$sid" || { tcping -q -c 3 -i 1 -t 2 -p $serverport $servername
cleanup_probe_socks if [ "$?" -gt "0" ]; then
ipset del whitelist $servername 2>/dev/null
return 1 return 1
} fi
check_proxy_via_socks /usr/bin/ssr-check $servername $serverport $switch_time
local ret=$? local ret=$?
cleanup_probe_socks ipset del whitelist $servername 2>/dev/null
return $ret if [ "$ret" == "0" ]; then
return 0
else
return 1
fi
} }
search_proxy() { search_proxy() {
local sid="$1" let server_count=server_count+1
[ "$(uci_get_by_name "$sid" switch_enable 0)" = "1" ] || return 1 [ "$normal_flag" == "1" -a "$server_count" -le "$server_locate" ] && return 0
[ "$(uci_get_by_name "$sid" type)" = "clash" ] && return 1 [ "$(uci_get_by_name $1 switch_enable 0)" != "1" ] && return 1
[ "$(uci_get_by_name "$sid" type)" = "tuic" ] && return 1 [ $ENABLE_SERVER != nil ] && return 0
[ "$sid" = "$CURRENT_SERVER" ] && return 1 [ "$1" == "$CURRENT_SERVER" ] && return 0
[ "$ENABLE_SERVER" != "nil" ] && return 0 local servername=$(uci_get_by_name $1 server)
local serverport=$(uci_get_by_name $1 server_port)
if test_proxy "$sid"; then ipset add whitelist $servername 2>/dev/null
ENABLE_SERVER="$sid" /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
return 0 return 0
else
return 1
fi fi
return 1
} }
#选择可用的代理
select_proxy() { select_proxy() {
config_load "$NAME" config_load $NAME
ENABLE_SERVER="nil" ENABLE_SERVER=nil
mkdir -p /var/run /var/etc
server_count=0
config_foreach search_proxy servers 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() { switch_proxy() {
set_main_server "$1" || return 1 /etc/init.d/shadowsocksr restart $1
/etc/init.d/shadowsocksr restart "$1"
return 0 return 0
} }
start() { start() {
while [ "1" = "1" ]; do #不支持kcptun启用时的切换
sleep "0000$cycle_time" [ $(uci_get_by_name $DEFAULT_SERVER kcp_enable) = "1" ] && return 1
run_once 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
done 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.

Before

Width:  |  Height:  |  Size: 215 KiB

@@ -1,939 +0,0 @@
#!/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,6 +2,7 @@
require "luci.sys" require "luci.sys"
local ucursor = require "luci.model.uci".cursor() local ucursor = require "luci.model.uci".cursor()
local datatypes = require "luci.cbi.datatypes"
local json = require "luci.jsonc" local json = require "luci.jsonc"
local server_section = arg[1] local server_section = arg[1]
@@ -11,6 +12,12 @@ local socks_port = arg[4] or "0"
local chain = arg[5] or "0" local chain = arg[5] or "0"
local GLOBAL = {
DNS_SERVER = {},
DNS_HOSTNAME = {},
VPS_EXCLUDE = {}
}
-- 辅助函数:拆分字符串(若 luci.util 未加载则定义) -- 辅助函数:拆分字符串(若 luci.util 未加载则定义)
local function split(str, pat) local function split(str, pat)
local t = {} local t = {}
@@ -31,52 +38,27 @@ local server = ucursor:get_all("shadowsocksr", server_section)
local socks_server = ucursor:get_all("shadowsocksr", "@socks5_proxy[0]") or {} 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_fragment = ucursor:get_all("shadowsocksr", "@global_xray_fragment[0]") or {}
local xray_noise = ucursor:get_all("shadowsocksr", "@xray_noise_packets[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 outbound_settings = nil
local xray_version = nil local xray_version = nil
local xray_version_val = 0 local xray_version_val = 0
local xray_builtin_dns = nil
local node_id = server_section local node_id = server_section
local remarks = server.alias or "" local remarks = server.alias or ""
local b64decode = nixio.bin.b64decode local b64decode = nixio.bin.b64decode
local b64encode = nixio.bin.b64encode local b64encode = nixio.bin.b64encode
local effective_node_local_port = tonumber(server.local_port) or tonumber(default_node_local_port) or 1234
if server.type == "ss-rust" then -- 解析 URL(简单实现,仅用于 DoH)
server.type = "ss" 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 }
end end
local function parse_realm_uri(uri) if server.type == "ss-rust" or server.type == "ss-libev" then
if type(uri) ~= "string" then return nil end server.type = "ss"
-- 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 end
-- base64 解码 -- base64 解码
@@ -138,25 +120,9 @@ local function cleanEmptyTables(t)
return next(t) and t or nil return next(t) and t or nil
end 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) local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= "" return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= ""
end end
-- 获取 Xray 版本号 -- 获取 Xray 版本号
@@ -237,33 +203,20 @@ function wireguard()
-- 处理 reserved 字段,支持逗号分隔的数字或 Base64 编码 -- 处理 reserved 字段,支持逗号分隔的数字或 Base64 编码
local reserved = nil local reserved = nil
if server.reserved then if server.reserved then
local all_bytes = {} local bytes = {}
local reserved_values = server.reserved if not server.reserved:match("[^%d,]+") then
-- 纯数字和逗号,解析为数字列表
-- 确保是 table 类型 server.reserved:gsub("%d+", function(b)
if type(reserved_values) ~= "table" then bytes[#bytes + 1] = tonumber(b)
reserved_values = {reserved_values} end)
end else
-- Base64 编码的二进制数据
for _, reserved_str in ipairs(reserved_values) do local result = base64Decode(server.reserved)
if type(reserved_str) == "string" then for i = 1, #result do
if not reserved_str:match("[^%d,]+") then bytes[i] = result:byte(i)
-- 数字和逗号格式
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
end end
reserved = #all_bytes > 0 and all_bytes or nil reserved = #bytes > 0 and bytes or nil
end end
outbound_settings = { outbound_settings = {
@@ -273,22 +226,15 @@ function wireguard()
{ {
publicKey = server.peer_pubkey, publicKey = server.peer_pubkey,
preSharedKey = server.preshared_key, preSharedKey = server.preshared_key,
endpoint = format_host_port(server.server, server.server_port), endpoint = server.server .. ":" .. server.server_port,
keepAlive = tonumber(server.keepaliveperiod), keepAlive = tonumber(server.keepaliveperiod),
allowedIPs = (server.allowedips) or nil, allowedIPs = (server.allowedips) or nil,
} }
}, },
kernelMode = (server.kernelmode == "1") and true or false, noKernelTun = (server.kernelmode == "1") and true or false,
reserved = reserved, reserved = reserved,
mtu = tonumber(server.mtu) 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 end
function xray_hysteria2() function xray_hysteria2()
outbound_settings = { outbound_settings = {
@@ -353,35 +299,6 @@ local Xray = {
-- 初始化 outbounds 表 -- 初始化 outbounds 表
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 -- 添加 dokodemo-door 配置,如果 local_port 不为 0
if local_port ~= "0" then if local_port ~= "0" then
@@ -432,11 +349,11 @@ if proto and proto:find("tcp") and socks_port ~= "0" then
-- socks -- socks
protocol = "socks", protocol = "socks",
port = tonumber(socks_port), port = tonumber(socks_port),
settings = { settings = {
auth = socks_server.socks5_auth or "noauth", auth = socks_server.socks5_auth or "noauth",
udp = true, udp = true,
mixed = ((socks_server.socks5_mixed == '1') and true or false) or nil, mixed = ((socks_server.socks5_mixed == '1') and true or false) or (socks_server.server == 'same') and nil,
accounts = (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth") and { accounts = (socks_server.server ~= "same" and (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth")) and {
{ {
user = socks_server.socks5_user, user = socks_server.socks5_user,
pass = socks_server.socks5_pass pass = socks_server.socks5_pass
@@ -615,7 +532,7 @@ Xray.outbounds = {
local finalmask = {} local finalmask = {}
local PT = server.v2ray_protocol local PT = server.v2ray_protocol
local TP = server.transport local TP = server.transport
if TP == "kcp" then if server.transport == "kcp" then
local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat", local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"} dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
local udp = {} local udp = {}
@@ -633,36 +550,14 @@ Xray.outbounds = {
udp[#udp+1] = c udp[#udp+1] = c
finalmask.udp = udp finalmask.udp = udp
elseif PT == "hysteria2" then elseif PT == "hysteria2" then
local udp = {}
if (server.flag_obfs == "1" and (server.obfs_type and server.obfs_type ~= "")) then if (server.flag_obfs == "1" and (server.obfs_type and server.obfs_type ~= "")) then
local o = { finalmask.udp = {{
type = "salamander", type = server.obfs_type,
settings = server.salamander and { settings = server.salamander and {
password = server.salamander, password = server.salamander
packetSize = server.obfs_type == "gecko" and "512-1200" or nil
} or nil } or nil
} }}
udp[#udp+1] = o
end 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 up = tonumber(server.uplink_capacity) or 0
local down = tonumber(server.downlink_capacity) or 0 local down = tonumber(server.downlink_capacity) or 0
finalmask.quicParams = { finalmask.quicParams = {
@@ -750,6 +645,7 @@ Xray.outbounds = {
end)(), end)(),
sockopt = { sockopt = {
mark = 255, mark = 255,
domainStrategy = server.domain_strategy or "UseIP",
tcpFastOpen = (function() tcpFastOpen = (function()
if server.transport == "xhttp" then if server.transport == "xhttp" then
return (server.fast_open == "1") and true or false return (server.fast_open == "1") and true or false
@@ -777,24 +673,18 @@ Xray.outbounds = {
} }
} }
if xray_builtin_dns then table.insert(Xray.outbounds, {
table.insert(Xray.outbounds, { protocol = "freedom",
protocol = "dns", tag = "direct",
tag = "builtin-dns-out", settings = {
settings = { domainStrategy = server.domain_strategy or "UseIP" -- 可根据需要改为 direct_dns_query_strategy
network = "tcp", },
address = xray_builtin_dns.address, streamSettings = {
port = xray_builtin_dns.port sockopt = {
mark = 255
} }
}) }
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 配置 -- 添加带有 fragment 设置的 dialerproxy 配置
if xray_fragment.fragment ~= "0" or (xray_fragment.noise ~= "0" and xray_noise.enabled ~= "0") then if xray_fragment.fragment ~= "0" or (xray_fragment.noise ~= "0" and xray_noise.enabled ~= "0") then
@@ -825,6 +715,121 @@ if xray_fragment.fragment ~= "0" or (xray_fragment.noise ~= "0" and xray_noise.e
}) })
end 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 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 cipher13 = "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384"
local trojan = { local trojan = {
@@ -872,7 +877,7 @@ local trojan = {
} }
} }
local naiveproxy = { local naiveproxy = {
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), proxy = (server.username and server.password and server.server and server.server_port) and "https://" .. server.username .. ":" .. server.password .. "@" .. 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), 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 ["insecure-concurrency"] = tonumber(server.concurrency) or 1
} }
@@ -893,16 +898,16 @@ local hysteria2 = {
server.server_port and server.server_port and
( (
server.port_range and server.port_range and
(format_host_port(server.server, server.server_port) .. "," .. string.gsub(server.port_range, ":", "-")) (server.server .. ":" .. server.server_port .. "," .. string.gsub(server.port_range, ":", "-"))
or or
(format_host_port(server.server, server.server_port)) (server.server .. ":" .. server.server_port)
) )
or or
( (
server.port_range and server.port_range and
format_host(server.server) .. ":" .. string.gsub(server.port_range, ":", "-") server.server .. ":" .. string.gsub(server.port_range, ":", "-")
or or
server.server and format_host_port(server.server, "443") server.server and server.server .. ":443"
) )
), ),
bandwidth = (server.uplink_capacity or server.downlink_capacity) and { bandwidth = (server.uplink_capacity or server.downlink_capacity) and {
@@ -1001,7 +1006,7 @@ local hysteria2 = {
} }
local shadowtls = { local shadowtls = {
client = { client = {
server_addr = server.server_port and format_host_port(server.server, server.server_port) or nil, server_addr = server.server_port and server.server .. ":" .. server.server_port or nil,
listen = "127.0.0.1:" .. tonumber(local_port), listen = "127.0.0.1:" .. tonumber(local_port),
tls_names = server.shadowtls_sni, tls_names = server.shadowtls_sni,
password = server.password password = server.password
@@ -1015,7 +1020,7 @@ local chain_sslocal = {
locals = local_port ~= "0" and { locals = local_port ~= "0" and {
{ {
local_address = "0.0.0.0", local_address = "0.0.0.0",
local_port = (chain_local_port == "0" and effective_node_local_port or tonumber(chain_local_port)), local_port = (chain_local_port == "0" and tonumber(server.local_port) or tonumber(chain_local_port)),
mode = (proto:find("tcp,udp") and "tcp_and_udp") or proto .. "_only", mode = (proto:find("tcp,udp") and "tcp_and_udp") or proto .. "_only",
protocol = "redir", protocol = "redir",
tcp_redir = "redirect", tcp_redir = "redirect",
@@ -1044,7 +1049,7 @@ local chain_sslocal = {
local chain_vmess = { local chain_vmess = {
inbounds = (local_port ~= "0") and { inbounds = (local_port ~= "0") and {
{ {
port = (chain_local_port == "0" and effective_node_local_port or tonumber(chain_local_port)), port = (chain_local_port == "0" and tonumber(server.local_port) or tonumber(chain_local_port)),
protocol = "dokodemo-door", protocol = "dokodemo-door",
settings = { settings = {
network = proto, network = proto,
@@ -1080,7 +1085,7 @@ local chain_vmess = {
} }
local tuic = { local tuic = {
relay = { relay = {
server = server.server_port and format_host_port(server.server, server.server_port), server = server.server_port and server.server .. ":" .. server.server_port,
ip = server.tuic_ip, ip = server.tuic_ip,
uuid = server.tuic_uuid, uuid = server.tuic_uuid,
password = server.tuic_passwd, password = server.tuic_passwd,
@@ -0,0 +1,95 @@
#!/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,12 +18,38 @@ case "$USE_TABLES" in
exit 1 exit 1
;; ;;
esac 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 mkdir -p $TMP_DNSMASQ_PATH
run_mode=$(normalize_run_mode) run_mode=$(uci_get_by_type global run_mode router)
cp -rf /etc/ssrplus/gfw_list.conf $TMP_DNSMASQ_PATH/ if [ "$run_mode" = "oversea" ]; then
cp -rf /etc/ssrplus/gfw_base.conf $TMP_DNSMASQ_PATH/ 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
for conf_file in gfw_base.conf gfw_list.conf; do for conf_file in gfw_base.conf gfw_list.conf; do
conf="$TMP_DNSMASQ_PATH/$conf_file" conf="$TMP_DNSMASQ_PATH/$conf_file"
@@ -41,41 +67,36 @@ for conf_file in gfw_base.conf gfw_list.conf; do
fi fi
done 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 # 此处使用 for 方式读取 防止 /etc/ssrplus/ 目录下的 black.list white.list deny.list 等2个或多个文件一行中存在空格 比如:# abc.com 而丢失:server
# Optimize: Batch filter using grep # Optimize: Batch filter using grep
for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list; do for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list; do
if [ -s "$list_file" ]; then 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 if [ -s "${list_file}.clean" ]; then
for target_file in "$TMP_DNSMASQ_PATH/gfw_list.conf" "$TMP_DNSMASQ_PATH/gfw_base.conf"; do for target_file in "$TMP_DNSMASQ_PATH/gfw_list.conf" "$TMP_DNSMASQ_PATH/gfw_base.conf"; do
[ -f "$target_file" ] || continue if [ -f "$target_file" ]; then
tmp_file="${target_file}.tmp" grep -v -F -f "${list_file}.clean" "$target_file" > "${target_file}.tmp"
awk -v list="${list_file}.clean" ' mv "${target_file}.tmp" "$target_file"
BEGIN { fi
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 done
fi fi
rm -f "${list_file}.clean" rm -f "${list_file}.clean"
@@ -93,45 +114,19 @@ fi
cat /etc/ssrplus/deny.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/address=\/&\//" >$TMP_DNSMASQ_PATH/denylist.conf 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 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 if [ -f "$TMP_DNSMASQ_PATH/ad.conf" ]; then
for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list; do for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list /etc/ssrplus/netflix.list; do
if [ -s "$list_file" ]; then 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 if [ -s "${list_file}.clean" ]; then
tmp_file="$TMP_DNSMASQ_PATH/ad.conf.tmp" grep -v -F -f "${list_file}.clean" "$TMP_DNSMASQ_PATH/ad.conf" > "$TMP_DNSMASQ_PATH/ad.conf.tmp"
awk -v list="${list_file}.clean" ' mv "$TMP_DNSMASQ_PATH/ad.conf.tmp" "$TMP_DNSMASQ_PATH/ad.conf"
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 fi
rm -f "${list_file}.clean" rm -f "${list_file}.clean"
fi fi
done done
fi fi
else else
rm -f "$TMP_DNSMASQ_PATH/ad.conf" rm -f $TMP_DNSMASQ_PATH/ad.conf
fi fi
+175
View File
@@ -0,0 +1,175 @@
#!/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 config global
option global_server 'nil' option global_server 'nil'
option netflix_server 'nil'
option netflix_proxy '0'
option threads '0' option threads '0'
option run_mode 'router' option run_mode 'router'
option dports '2' option dports '2'
option custom_ports '80,443' option custom_ports '80,443'
option pdnsd_enable '1' option pdnsd_enable '1'
option filter_aaaa '1' option prefer_nft '1'
option tunnel_forward '8.8.4.4:53' option tunnel_forward '8.8.4.4:53'
option monitor_enable '1' option monitor_enable '1'
option enable_switch '1' option enable_switch '1'
option switch_time '667' option switch_time '667'
option switch_timeout '5' option switch_timeout '5'
option switch_try_count '3' option switch_try_count '3'
option default_node_local_port '1234' option shunt_dns '1'
option component_mirror 'direct'
option gfwlist_url 'https://fastly.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt' 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 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' option adblock_url 'https://anti-ad.net/anti-ad-for-dnsmasq.conf'
config server_subscribe config server_subscribe
option proxy '1' option proxy '0'
option auto_update '1' option auto_update '1'
option config_auto_update_mode '0'
option config_update_interval '60'
option auto_update_week_time '*' option auto_update_week_time '*'
option auto_update_day_time '2' option auto_update_day_time '2'
option auto_update_min_time '0' option auto_update_min_time '0'
option url_test_url 'https://www.google.com/generate_204' option url_test_url 'https://www.google.com/generate_204'
option user_agent 'v2rayN/9.99' option user_agent 'v2rayN/9.99'
option filter_words '过期/套餐/剩余/网址/QQ群/官网/防失联/回国' option filter_words '过期/套餐/剩余/QQ群/官网/防失联/回国'
config access_control config access_control
option lan_ac_mode '0' option lan_ac_mode '0'
@@ -47,11 +47,6 @@ config socks5_proxy
option server 'nil' option server 'nil'
option local_port '1080' option local_port '1080'
config http_proxy
option server 'nil'
option local_port '3128'
option http_auth 'none'
config server_global config server_global
option enable_server '0' option enable_server '0'
@@ -59,9 +54,3 @@ config global_xray_fragment
option fragment '0' option fragment '0'
option noise '0' option noise '0'
config clash_client_group
option enabled '0'
option remarks ''
option ip_addr ''
option client_mac ''
option policy_group ''
@@ -1,43 +1,6 @@
#!/bin/sh #!/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 /usr/bin/lua /usr/share/shadowsocksr/update.lua
sleep 2s sleep 2s
/usr/share/shadowsocksr/chinaipset.sh /var/etc/ssrplus/china_ssr.txt /usr/share/shadowsocksr/chinaipset.sh /var/etc/ssrplus/china_ssr.txt
sleep 2s sleep 2s
/usr/bin/lua /usr/share/shadowsocksr/subscribe.lua /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,6 +250,10 @@ if args then
update(uci:get_first("shadowsocksr", "global", "adblock_url"), "/etc/ssrplus/ad.conf", args, TMP_DNSMASQ_PATH .. "/ad.conf") update(uci:get_first("shadowsocksr", "global", "adblock_url"), "/etc/ssrplus/ad.conf", args, TMP_DNSMASQ_PATH .. "/ad.conf")
os.exit(0) os.exit(0)
end 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 else
log("正在更新【GFW列表】数据库") log("正在更新【GFW列表】数据库")
update(uci:get_first("shadowsocksr", "global", "gfwlist_url"), "/etc/ssrplus/gfw_list.conf", "gfw_data", TMP_DNSMASQ_PATH .. "/gfw_list.conf") update(uci:get_first("shadowsocksr", "global", "gfwlist_url"), "/etc/ssrplus/gfw_list.conf", "gfw_data", TMP_DNSMASQ_PATH .. "/gfw_list.conf")
@@ -263,4 +267,10 @@ else
log("正在更新【广告屏蔽】数据库") log("正在更新【广告屏蔽】数据库")
update(uci:get_first("shadowsocksr", "global", "adblock_url"), "/etc/ssrplus/ad.conf", "ad_data", TMP_DNSMASQ_PATH .. "/ad.conf") update(uci:get_first("shadowsocksr", "global", "adblock_url"), "/etc/ssrplus/ad.conf", "ad_data", TMP_DNSMASQ_PATH .. "/ad.conf")
end 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 end
File diff suppressed because it is too large. Load diff
+14
View File
@@ -0,0 +1,14 @@
#!/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
+30 -25
View File
@@ -1,30 +1,32 @@
# SPDX-License-Identifier: GPL-2.0
#
# Copyright (C) 2024-2026 Anya Lin <hukk1996@gmail.com>
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=mihomo PKG_NAME:=mihomo
PKG_VERSION:=1.19.26 PKG_VERSION:=1.19.26
PKG_RELEASE:=11 PKG_RELEASE:=12
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:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_URL:=https://codeload.github.com/metacubex/mihomo/tar.gz/v$(PKG_VERSION)?
PKG_MIRROR_HASH:=skip PKG_HASH:=skip
PKG_LICENSE:=GPL3.0+ PKG_MAINTAINER:=Anya Lin <hukk1996@gmail.com>
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com> PKG_LICENSE:=GPL-2.0
PKG_LICENSE_FILES:=LICENSE
PKG_BUILD_DEPENDS:=golang/host PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1 PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16 PKG_BUILD_FLAGS:=no-mips16
PKG_BUILD_VERSION:=v$(PKG_VERSION)
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
GO_PKG:=github.com/metacubex/mihomo GO_PKG:=github.com/metacubex/mihomo
GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME)
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_TAGS:=with_gvisor GO_PKG_TAGS:=with_gvisor
GO_PKG_INSTALL_BIN_PATH:=/usr/libexec
include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
@@ -32,24 +34,27 @@ include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/mihomo define Package/mihomo
SECTION:=net SECTION:=net
CATEGORY:=Network CATEGORY:=Network
TITLE:=A rule based proxy in Go. TITLE:=Another Mihomo Kernel.
URL:=https://wiki.metacubex.one URL:=https://wiki.metacubex.one
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle DEPENDS:=$(GO_ARCH_DEPENDS)
PROVIDES:=mihomo
ALTERNATIVES:=\ ALTERNATIVES:=\
300:/usr/bin/mihomo:/usr/libexec/mihomo 100:/usr/bin/mihomo:/usr/libexec/mihomo-core
endef USERID:=mihomo=7890:mihomo=7890
define Package/mihomo/description
Mihomo is a rule based proxy in Go.
endef
define Package/mihomo/install
$(call GoPackage/Package/Install/Bin,$(1))
endef endef
define Build/Prepare define Build/Prepare
$(Build/Prepare/Default) $(Build/Prepare/Default)
$(RM) -r $(PKG_BUILD_DIR)/rules/logic_test # 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
endef endef
$(eval $(call GoBinPackage,mihomo)) $(eval $(call GoBinPackage,mihomo))
+70
View File
@@ -0,0 +1,70 @@
#
# 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
@@ -0,0 +1,12 @@
# 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)
@@ -0,0 +1,11 @@
--- 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)
@@ -0,0 +1,16 @@
--- 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)