From 50bf1ec00a58adad770bb0a721d9b6d8d6c1d335 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Jun 2026 14:05:40 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=BD=20Sync=202026-06-03=2014:05:40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dns2tcp/Makefile | 2 +- ...-add-A-option-to-filter-AAAA-answers.patch | 210 -- luci-app-ssr-plus/Makefile | 163 +- .../view/shadowsocksr/Sortable.min.js | 2 + .../luasrc/controller/shadowsocksr.lua | 1499 +-------- .../model/cbi/shadowsocksr/advanced.lua | 250 +- .../model/cbi/shadowsocksr/client-config.lua | 434 +-- .../luasrc/model/cbi/shadowsocksr/client.lua | 156 +- .../model/cbi/shadowsocksr/component.lua | 30 - .../luasrc/model/cbi/shadowsocksr/control.lua | 40 +- .../luasrc/model/cbi/shadowsocksr/log.lua | 2 - .../model/cbi/shadowsocksr/server-config.lua | 372 +-- .../luasrc/model/cbi/shadowsocksr/server.lua | 11 +- .../luasrc/model/cbi/shadowsocksr/servers.lua | 781 ++--- .../luasrc/model/cbi/shadowsocksr/status.lua | 84 +- .../shadowsocksr/advanced_switch_compact.htm | 143 - .../view/shadowsocksr/clash_main_panel.htm | 1044 ------ .../luasrc/view/shadowsocksr/clash_panel.htm | 104 - .../view/shadowsocksr/clash_panel_button.htm | 74 - .../view/shadowsocksr/clash_yaml_upload.htm | 4 - .../view/shadowsocksr/client_dns_defaults.htm | 78 - .../luasrc/view/shadowsocksr/component.htm | 446 --- .../view/shadowsocksr/control_layout.htm | 9 - .../luasrc/view/shadowsocksr/ping.htm | 16 +- .../luasrc/view/shadowsocksr/server_list.htm | 1063 +++--- .../luasrc/view/shadowsocksr/server_table.htm | 304 -- .../servers_subscribe_url_style.htm | 166 - .../luasrc/view/shadowsocksr/socket.htm | 25 +- .../luasrc/view/shadowsocksr/ssrurl.htm | 793 +++-- .../luasrc/view/shadowsocksr/status.htm | 3 +- .../luasrc/view/shadowsocksr/subscribe.htm | 271 +- .../shadowsocksr/subscribe_actions_footer.htm | 490 --- .../subscribe_enabled_autosave.htm | 47 - .../subscribe_schedule_compact.htm | 182 -- luci-app-ssr-plus/po/templates/ssr-plus.pot | 2542 ++++++--------- luci-app-ssr-plus/po/zh_Hans/ssr-plus.po | 2869 +++++++---------- .../root/etc/hotplug.d/iface/99-ssrplus-pppoe | 22 - .../root/etc/init.d/shadowsocksr | 2105 +++++------- .../root/etc/ssrplus/netflix.list | 25 + .../root/etc/ssrplus/oversea_list.conf | 192 ++ .../root/etc/uci-defaults/luci-ssr-plus | 72 +- luci-app-ssr-plus/root/usr/bin/ssr-monitor | 195 +- luci-app-ssr-plus/root/usr/bin/ssr-rules | 146 +- luci-app-ssr-plus/root/usr/bin/ssr-switch | 359 +-- .../root/usr/share/shadowsocksr/Country.mmdb | Bin 220503 -> 0 bytes .../usr/share/shadowsocksr/clash_yaml.lua | 939 ------ .../usr/share/shadowsocksr/gen_config.lua | 347 +- .../usr/share/shadowsocksr/genred2config.sh | 95 + .../root/usr/share/shadowsocksr/gfw2ipset.sh | 123 +- .../root/usr/share/shadowsocksr/hy2_test.sh | 175 + .../share/shadowsocksr/shadowsocksr.config | 25 +- .../usr/share/shadowsocksr/ssrplusupdate.sh | 37 - .../root/usr/share/shadowsocksr/subscribe.lua | 1307 +++----- .../root/usr/share/shadowsocksr/update.lua | 10 + .../share/shadowsocksr/update_components.sh | 1428 -------- mihomo/.prepare.sh | 14 + mihomo/Makefile | 55 +- trojan/Makefile | 70 + trojan/boost-version.mk | 12 + .../patches/001-force-openssl-version.patch | 11 + trojan/patches/002-Fix-boost1.89-build.patch | 16 + 61 files changed, 6813 insertions(+), 15676 deletions(-) delete mode 100644 dns2tcp/patches/0001-add-A-option-to-filter-AAAA-answers.patch create mode 100644 luci-app-ssr-plus/htdocs/luci-static/resources/view/shadowsocksr/Sortable.min.js delete mode 100644 luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/component.lua delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/advanced_switch_compact.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_yaml_upload.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/client_dns_defaults.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/control_layout.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/server_table.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/servers_subscribe_url_style.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_enabled_autosave.htm delete mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_schedule_compact.htm delete mode 100644 luci-app-ssr-plus/root/etc/hotplug.d/iface/99-ssrplus-pppoe create mode 100644 luci-app-ssr-plus/root/etc/ssrplus/netflix.list create mode 100644 luci-app-ssr-plus/root/etc/ssrplus/oversea_list.conf delete mode 100644 luci-app-ssr-plus/root/usr/share/shadowsocksr/Country.mmdb delete mode 100644 luci-app-ssr-plus/root/usr/share/shadowsocksr/clash_yaml.lua create mode 100755 luci-app-ssr-plus/root/usr/share/shadowsocksr/genred2config.sh create mode 100755 luci-app-ssr-plus/root/usr/share/shadowsocksr/hy2_test.sh delete mode 100755 luci-app-ssr-plus/root/usr/share/shadowsocksr/update_components.sh create mode 100755 mihomo/.prepare.sh create mode 100644 trojan/Makefile create mode 100644 trojan/boost-version.mk create mode 100644 trojan/patches/001-force-openssl-version.patch create mode 100644 trojan/patches/002-Fix-boost1.89-build.patch diff --git a/dns2tcp/Makefile b/dns2tcp/Makefile index 6926858f..e241421b 100644 --- a/dns2tcp/Makefile +++ b/dns2tcp/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dns2tcp PKG_VERSION:=1.1.2 -PKG_RELEASE:=2 +PKG_RELEASE:=3 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/zfl9/dns2tcp/tar.gz/v$(PKG_VERSION)? diff --git a/dns2tcp/patches/0001-add-A-option-to-filter-AAAA-answers.patch b/dns2tcp/patches/0001-add-A-option-to-filter-AAAA-answers.patch deleted file mode 100644 index a11ba100..00000000 --- a/dns2tcp/patches/0001-add-A-option-to-filter-AAAA-answers.patch +++ /dev/null @@ -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 udp listen address, port default to 53\n" -@@ -174,6 +307,7 @@ static void print_help(void) { - " -s 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]; diff --git a/luci-app-ssr-plus/Makefile b/luci-app-ssr-plus/Makefile index 3ee704b4..2479f41d 100644 --- a/luci-app-ssr-plus/Makefile +++ b/luci-app-ssr-plus/Makefile @@ -3,51 +3,77 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=luci-app-ssr-plus LUCI_PKGARCH:=all PKG_NAME:=luci-app-ssr-plus -PKG_VERSION:=193 -PKG_RELEASE:=16 +PKG_VERSION:=190 +PKG_RELEASE:=17 PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS_RUST \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNS2TCP \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNSPROXY \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Redsocks2 \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_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_DEPENDS:= \ - +libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \ - +jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \ - +resolveip +curl +nping +unzip +xz-utils \ + +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \ + +jq +ip-full +lua +lua-neturl +libuci-lua +microsocks \ + +tcping +resolveip +shadowsocksr-libev-ssr-check +curl +nping \ + +PACKAGE_$(PKG_NAME)_INCLUDE_V2ray:curl \ + +PACKAGE_$(PKG_NAME)_INCLUDE_V2ray:v2ray-core \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Xray:curl \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \ +PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG:chinadns-ng \ + +PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS:dns2socks \ + +PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS_RUST:dns2socks-rust \ + +PACKAGE_$(PKG_NAME)_INCLUDE_DNSPROXY:dnsproxy \ +PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS:mosdns \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria:hysteria \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client:tuic-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS:shadow-tls \ + +PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks:ipt2socks \ +PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun:kcptun-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy:naiveproxy \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Redsocks2:redsocks2 \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client:shadowsocks-libev-ss-local \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client:shadowsocks-libev-ss-redir \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server:shadowsocks-libev-ss-server \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client:shadowsocks-rust-sslocal \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server:shadowsocks-rust-ssserver \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs:simple-obfs-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin:v2ray-plugin \ +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-local \ +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-redir \ - +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server:shadowsocksr-libev-ssr-server + +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server:shadowsocksr-libev-ssr-server \ + +PACKAGE_$(PKG_NAME)_INCLUDE_Trojan:trojan define Package/$(PKG_NAME)/config +select PACKAGE_luci-lua-runtime if PACKAGE_$(PKG_NAME) choice prompt "Transparent Proxy Backend" @@ -77,24 +103,37 @@ endchoice choice 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 bool "None" + config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client + bool "Shadowsocks-libev" + 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 choice prompt "Shadowsocks Server Selection" + default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server if aarch64 + default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server if i386||x86_64||arm default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server bool "None" + config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server + bool "Shadowsocks-libev" + config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server - bool "Shadowsocks Rust" + bool "Shadowsocks-rust" + depends on aarch64||arm||i386||mips||mipsel||x86_64 + depends on !(TARGET_x86_geode||TARGET_x86_legacy) endchoice choice @@ -105,35 +144,45 @@ choice config PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY bool "None" + config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray + bool "V2ray-core" + config PACKAGE_$(PKG_NAME)_INCLUDE_Xray bool "Xray-core" - select PACKAGE_xray-core endchoice config PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG bool "Include ChinaDNS-NG" - default 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 bool "Include MosDNS" + default y if aarch64||i386||x86_64 + +config PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria + bool "Include Hysteria" + select PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG default n -config PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy - bool "Include HTTP(S) Proxy Server" - select PACKAGE_3proxy - default y if (aarch64||arm||i386||loongarch64||riscv64||x86_64) - -config PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo - bool "Include Mihomo (Clash Support)" - select PACKAGE_mihomo - depends on aarch64||arm||i386||loongarch64||riscv64||x86_64 - default y if aarch64||arm||i386||loongarch64||riscv64||x86_64 - -config PACKAGE_$(PKG_NAME)_INCLUDE_GeoData - bool "Include GeoData (GeoIP and GeoSite)" - select PACKAGE_v2ray-geoip - select PACKAGE_v2ray-geosite - default y if i386||x86_64 +config PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client + bool "Include Tuic-Client" + select PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG + select PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks + depends on aarch64||arm||i386||x86_64 + depends on !(TARGET_x86_geode||TARGET_x86_legacy) + default n config PACKAGE_$(PKG_NAME)_INCLUDE_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) default n +config PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks + bool "Include IPT2Socks" + default n + config PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun bool "Include Kcptun" default n @@ -152,9 +205,13 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy depends on !(arc||armeb||mips||mips64||powerpc||TARGET_gemini) default n +config PACKAGE_$(PKG_NAME)_INCLUDE_Redsocks2 + bool "Include Redsocks2" + default n + config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs bool "Include Shadowsocks Simple Obfs Plugin" - default n + default y config PACKAGE_$(PKG_NAME)_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 bool "Include ShadowsocksR Libev Server" + default y if i386||x86_64||arm + +config PACKAGE_$(PKG_NAME)_INCLUDE_Trojan + bool "Include Trojan" + select PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks default n endef @@ -178,41 +240,4 @@ endef include $(TOPDIR)/feeds/luci/luci.mk -define Package/$(PKG_NAME)/install - ifneq ($(wildcard ${CURDIR}/luasrc),) - $(INSTALL_DIR) $(1)$(LUCI_LIBRARYDIR) - cp -pR $(PKG_BUILD_DIR)/luasrc/* $(1)$(LUCI_LIBRARYDIR)/ - $(FIND) $(1)$(LUCI_LIBRARYDIR)/ -type f -name '*.luadoc' | $(XARGS) rm - $(if $(CONFIG_LUCI_SRCDIET),$(call SrcDiet,$(1)$(LUCI_LIBRARYDIR)/),true) - $(call SubstituteVersion,$(1)$(LUCI_LIBRARYDIR)/) - endif - ifneq ($(wildcard ${CURDIR}/ucode),) - $(INSTALL_DIR) $(1)$(UCODE_LIBRARYDIR) - cp -pR $(PKG_BUILD_DIR)/ucode/* $(1)$(UCODE_LIBRARYDIR)/ - $(call SubstituteVersion,$(1)$(UCODE_LIBRARYDIR)/) - endif - ifneq ($(wildcard ${CURDIR}/htdocs),) - $(INSTALL_DIR) $(1)$(HTDOCS) - cp -pR $(PKG_BUILD_DIR)/htdocs/* $(1)$(HTDOCS)/ - $(if $(CONFIG_LUCI_JSMIN),$(call JsMin,$(1)$(HTDOCS)/),true) - $(if $(CONFIG_LUCI_CSSTIDY),$(call CssTidy,$(1)$(HTDOCS)/),true) - endif - ifneq ($(wildcard ${CURDIR}/root),) - $(INSTALL_DIR) $(1)/ - cp -pR $(PKG_BUILD_DIR)/root/* $(1)/ - endif - ifneq ($(wildcard ${CURDIR}/src),) - $(call Build/Install/Default) - $(CP) $(PKG_INSTALL_DIR)/* $(1)/ - endif - if [ -d "$(1)/usr/bin" ]; then \ - chmod 0755 "$(1)/usr/bin/ssr-monitor" 2>/dev/null || true; \ - chmod 0755 "$(1)/usr/bin/ssr-rules" 2>/dev/null || true; \ - chmod 0755 "$(1)/usr/bin/ssr-switch" 2>/dev/null || true; \ - fi - if [ -f "$(1)/etc/hotplug.d/iface/99-ssrplus-pppoe" ]; then \ - chmod 0755 "$(1)/etc/hotplug.d/iface/99-ssrplus-pppoe" 2>/dev/null || true; \ - fi -endef - # call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-ssr-plus/htdocs/luci-static/resources/view/shadowsocksr/Sortable.min.js b/luci-app-ssr-plus/htdocs/luci-static/resources/view/shadowsocksr/Sortable.min.js new file mode 100644 index 00000000..8148b9dd --- /dev/null +++ b/luci-app-ssr-plus/htdocs/luci-static/resources/view/shadowsocksr/Sortable.min.js @@ -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[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=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function jt(t){$&&$.parentNode[K]._isOutsideThisEl(t.target)}function Ht(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Rt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Ht.supportPointer&&"PointerEvent"in window&&(!u||d),emptyInsertThreshold:5};for(n in G.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Xt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&Pt,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?f(t,"pointerdown",this._onTapStart):(f(t,"mousedown",this._onTapStart),f(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(f(t,"dragover",this),f(t,"dragenter",this)),_t.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,N())}function Lt(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Kt(t){t.draggable=!1}function Wt(){Ot=!1}function zt(t){return setTimeout(t,0)}function Gt(t){return clearTimeout(t)}Ht.prototype={constructor:Ht,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(bt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,$):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Mt.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Mt.push(o)}}(o),!$&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||nt===l)){if(rt=j(l),lt=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return Z({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),q("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return Z({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),q("filter",n,{evt:e}),!0}))return void(i&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!$&&n.parentNode===r&&(o=X(n),tt=r,Q=($=n).parentNode,et=$.nextSibling,nt=n,ct=a.group,dt={target:Ht.dragged=$,clientX:(e||t).clientX,clientY:(e||t).clientY},gt=dt.clientX-o.left,mt=dt.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,$.style["will-change"]="all",o=function(){q("delayEnded",i,{evt:t}),Ht.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!c&&i.nativeDraggable&&($.draggable=!0),i._triggerDragStart(t,e),Z({sortable:i,name:"choose",originalEvent:t}),k($,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){E($,t.trim(),Kt)}),f(l,"dragover",Ft),f(l,"mousemove",Ft),f(l,"touchmove",Ft),a.supportPointer?(f(l,"pointerup",i._onDrop),this.nativeDraggable||f(l,"pointercancel",i._onDrop)):(f(l,"mouseup",i._onDrop),f(l,"touchend",i._onDrop),f(l,"touchcancel",i._onDrop)),c&&this.nativeDraggable&&(this.options.touchStartThreshold=4,$.draggable=!0),q("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():Ht.eventCanceled?this._onDrop():(a.supportPointer?(f(l,"pointerup",i._disableDelayedDrag),f(l,"pointercancel",i._disableDelayedDrag)):(f(l,"mouseup",i._disableDelayedDrag),f(l,"touchend",i._disableDelayedDrag),f(l,"touchcancel",i._disableDelayedDrag)),f(l,"mousemove",i._delayedDragTouchMoveHandler),f(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&f(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){$&&Kt($),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?f(document,"pointermove",this._onTouchMove):f(document,e?"touchmove":"mousemove",this._onTouchMove):(f($,"dragend",this),f(tt,"dragstart",this._onDragStart));try{document.selection?zt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Et=!1,tt&&$?(q("dragStarted",this,{evt:e}),this.nativeDraggable&&f(document,"dragover",jt),n=this.options,t||k($,n.dragClass,!1),k($,n.ghostClass,!0),Ht.active=this,t&&this._appendGhost(),Z({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(ht){this._lastX=ht.clientX,this._lastY=ht.clientY,Yt();for(var t=document.elementFromPoint(ht.clientX,ht.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(ht.clientX,ht.clientY))!==e;)e=t;if($.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:ht.clientX,clientY:ht.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=m(t=e));Bt()}},_onTouchMove:function(t){if(dt){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=J&&D(J,!0),a=J&&r&&r.a,l=J&&r&&r.d,e=Nt&&Dt&&S(Dt),a=(i.clientX-dt.clientX+o.x)/(a||1)+(e?e[0]-xt[0]:0)/(a||1),l=(i.clientY-dt.clientY+o.y)/(l||1)+(e?e[1]-xt[1]:0)/(l||1);if(!Ht.active&&!Et){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,J),e?t.clientX<_.left-10||t.clientY/dev/null 2>&1") == 0 end -local function sanitize_mac(value) - value = trim(value):upper():gsub("-", ":") - if value:match("^%x%x:%x%x:%x%x:%x%x:%x%x:%x%x$") then - return value - end - return "" -end - -local function normalize_client_ip(value) - value = trim(value) - if value == "" then - return "" - end - if datatypes.cidr4(value) or datatypes.ip4addr(value) then - return value - end - return "" -end - -local function get_clash_client_rule_csv_path(sid) - sid = trim(sid) - if sid == "" then - return nil - end - return string.format("%s/%s.csv", CLASH_RULES_DIR, sid) -end - -local function csv_escape(value) - value = tostring(value or "") - if value:find('[",\n\r]') then - return '"' .. value:gsub('"', '""') .. '"' - end - return value -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 +local function uci_save(cursor, config, commit, apply) + if is_old_uci() then + cursor:save(config) + if commit then + cursor:commit(config) + if apply then + luci.sys.call("/etc/init.d/" .. config .. " reload > /dev/null 2>&1 &") + end + end + else + commit = true + if commit then + if apply then + cursor:commit(config) 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 rules = {} - local csv_path = get_clash_client_rule_csv_path(sid) - if not csv_path or not nixio.fs.access(csv_path) then - return rules - end - - local raw = nixio.fs.readfile(csv_path) - if not raw or raw == "" then - return rules - 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 - rules[#rules + 1] = { - id = tostring(#rules + 1), - enabled = cols[1] == "1" or tostring(cols[1] or ""):lower() == "true", - ip_addr = trim(cols[2] or ""), - policy_group = trim(cols[3] or ""), - remarks = trim(cols[4] or ""), - client_mac = sanitize_mac(cols[5] or "") - } - end + sh_uci_commit(config) end end end - - return rules end -local function write_clash_client_rules_csv(sid, rows) - local csv_path = get_clash_client_rule_csv_path(sid) - if not csv_path then - return false - end - - nixio.fs.mkdirr(CLASH_RULES_DIR) - local lines = { "enabled,client,policy,remarks,client_mac" } - for _, row in ipairs(rows or {}) do - lines[#lines + 1] = table.concat({ - row.enabled == "1" and "1" or "0", - csv_escape(row.ip_addr or ""), - csv_escape(row.policy_group or ""), - csv_escape(row.remarks or ""), - csv_escape(row.client_mac or "") - }, ",") - end - - return nixio.fs.writefile(csv_path, table.concat(lines, "\n") .. "\n") -end - -local function collect_lan_clients() - local clients = {} - local seen = {} - - luci.sys.net.host_hints(function(mac, ipv4, _, name) - local ip = trim(ipv4) - local norm_mac = sanitize_mac(mac) - if ip ~= "" and not seen[ip] then - seen[ip] = true - clients[#clients + 1] = { - ip = ip, - mac = norm_mac, - name = trim(name) ~= "" and trim(name) or ip - } - end - end) - - table.sort(clients, function(a, b) - return tostring(a.name or a.ip) < tostring(b.name or b.ip) - end) - - return clients -end - -local function read_clash_client_rules(sid) - return read_clash_client_rules_csv(sid) -end - -local function normalize_ping_ms(value, scale) - local num = tonumber(value) - if not num or num <= 0 then - return nil - end - local scaled = scale and (num * scale) or num - if scaled > 0 and scaled < 1 then - return 1 - end - return math.floor(scaled + 0.5) -end - -local function detect_tls_handshake_ms(domain, port, path, resolve_host, server_ip, is_websocket) - if not domain or domain == "" or not port or port <= 0 then - return nil - end - - local final_host = (resolve_host and resolve_host ~= "") and resolve_host or domain - local resolve_arg = "" - if server_ip and server_ip ~= "" and final_host ~= server_ip then - resolve_arg = string.format("--resolve '%s:%d:%s' ", final_host, port, server_ip) - end - - local ws_headers = "" - if is_websocket then - ws_headers = string.format( - "-H %s -H %s -H %s -H %s ", - luci.util.shellquote("Connection: Upgrade"), - luci.util.shellquote("Upgrade: websocket"), - luci.util.shellquote("Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ=="), - luci.util.shellquote("Sec-WebSocket-Version: 13") - ) - end - - local host_header = (final_host and final_host ~= "") and ("-H " .. luci.util.shellquote("Host: " .. final_host) .. " ") or "" - local url = string.format("https://%s:%d%s", final_host, port, path or "") - local cmd = string.format( - "curl --http1.1 -m 3 -ksS -o /dev/null %s%s%s -w 'time_connect=%%{time_connect}\\ntime_appconnect=%%{time_appconnect}\\nhttp_code=%%{http_code}' '%s' 2>/dev/null", - resolve_arg, host_header, ws_headers, url - ) - local result = luci.sys.exec(cmd) or "" - local appconnect = tonumber(result:match("time_appconnect=([0-9.]+)")) - if appconnect and appconnect > 0 then - return normalize_ping_ms(appconnect, 1000) - end - local connect = tonumber(result:match("time_connect=([0-9.]+)")) - if connect and connect > 0 then - return normalize_ping_ms(connect, 1000) - end - return nil -end - -local function urlencode(str) - if not str then return "" end - return tostring(str):gsub("[^%w%-_%.~]", function(c) - return string.format("%%%02X", string.byte(c)) - end) -end - -local function is_ipv6_address(addr) - addr = tostring(addr or "") - return addr ~= "" and addr:find(":", 1, true) ~= nil -end - -local function is_local_target(addr) - addr = tostring(addr or ""):lower() - if addr == "" then - return false - end - - if addr == "localhost" or addr == "::1" or addr:match("%.local$") then - return true - end - - if is_ipv6_address(addr) then - return addr:match("^fe[89ab]") ~= nil or addr:match("^fc") ~= nil or addr:match("^fd") ~= nil - end - - local o1, o2 = addr:match("^(%d+)%.(%d+)%.") - o1 = tonumber(o1) - o2 = tonumber(o2) - if not o1 or not o2 then - return false - end - - return o1 == 10 - or o1 == 127 - or (o1 == 169 and o2 == 254) - or (o1 == 172 and o2 >= 16 and o2 <= 31) - or (o1 == 192 and o2 == 168) -end - -local function detect_tcp_connect_ms(domain, port) - if not domain or domain == "" or not port or port <= 0 then - return nil - end - - local ip_version_arg = is_ipv6_address(domain) and "-6 " or "" - local cmd = string.format( - "nping %s--tcp-connect -q -c 1 -p %d %s 2>/dev/null", - ip_version_arg, - port, - luci.util.shellquote(domain) - ) - local result = luci.sys.exec(cmd) or "" - local success = tonumber(result:match("Successful connections:%s*([0-9]+)")) - if success and success > 0 then - local avg_rtt = tonumber(result:match("Avg rtt:%s*([0-9.]+)ms")) - if avg_rtt and avg_rtt > 0 and avg_rtt < 1 and not is_local_target(domain) then - return nil - end - return normalize_ping_ms(avg_rtt) - end - - return nil -end - -local function get_clash_secret(sid) - return sid .. "_ssrplus_clash" -end - -local function get_clash_cache_file(sid) - return "/etc/ssrplus/clash/" .. sid .. ".yaml" -end - -local function get_clash_state_file(sid) - return "/etc/ssrplus/clash/" .. sid .. ".cache.db" -end - -local function clash_process_running() - return luci.sys.call("(busybox ps -w 2>/dev/null || busybox ps) | grep ssr-retcp | grep -v grep >/dev/null") == 0 -end - -local function global_client_running() - local process_list = luci.sys.exec("busybox ps -w 2>/dev/null || busybox ps") - 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 "" - - if process_list:find("tcp.only.ssr.retcp") - or process_list:find("tcp.udp.ssr.retcp") - or process_list:find("local.ssr.retcp") - or process_list:find("local.udp.ssr.retcp") then - return true - end - - if (global_type == "clash" or global_type == "tuic" or global_type == "ss") - and process_list:find("ssr%-retcp") then - return true - 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 - return true - end - - if global_type == "socks5" - and process_list:find("ipt2socks") - and (process_list:find("%-T") or process_list:find("%-%-tcp%-only")) then - return true - end - - return false -end - -local function get_active_node_runtime(sid) - if not sid or sid == "" or sid == "nil" or uci:get("shadowsocksr", sid) ~= "servers" then - return nil, nil - end - - local stype = (uci:get("shadowsocksr", sid, "type") or ""):lower() - local proto = (uci:get("shadowsocksr", sid, "v2ray_protocol") or ""):lower() - local backend - local protocol - - if stype == "ss" then - backend = translate("Mihomo") - protocol = translate("Shadowsocks") - elseif stype == "clash" then - backend = translate("Mihomo") - protocol = translate("Clash") - elseif stype == "tuic" then - backend = translate("Mihomo") - protocol = translate("TUIC") - elseif stype == "ssr" then - backend = translate("ShadowsocksR") - elseif stype == "ss-rust" then - backend = translate("Shadowsocks-rust") - elseif stype == "v2ray" then - local proto_map = { - vmess = "VMess", - vless = "VLESS", - trojan = "Trojan", - socks = "SOCKS5", - hysteria2 = "Hysteria2", - shadowsocks = "Shadowsocks", - http = "HTTP" - } - backend = translate("Xray") - if proto_map[proto] then - protocol = translate(proto_map[proto]) - end - elseif stype == "trojan" then - backend = translate("Trojan") - elseif stype == "naiveproxy" then - backend = translate("NaiveProxy") - elseif stype == "socks5" then - backend = translate("SOCKS5") - elseif stype == "shadowtls" then - backend = translate("ShadowTLS") - elseif stype == "hysteria2" then - backend = translate("Hysteria2") - end - - if not backend or backend == "" then - backend = trim(stype) - end - - return backend, protocol -end - -local function get_running_status_text() - local sid = uci:get_first("shadowsocksr", "global", "global_server", "nil") - local backend, protocol = get_active_node_runtime(sid) - - if backend and backend ~= "" and protocol and protocol ~= "" and backend ~= protocol then - return string.format(translate("RUNNING in %s (%s) Mode"), backend, protocol) - end - - if backend and backend ~= "" then - return string.format(translate("RUNNING in %s Mode"), backend) - end - - return translate("RUNNING") -end - -local function is_active_clash_node(sid) - if not sid then return false end - if uci:get("shadowsocksr", sid) ~= "servers" then return false end - if uci:get("shadowsocksr", sid, "type") ~= "clash" then return false end - if uci:get_first("shadowsocksr", "global", "global_server") ~= sid then return false end - return clash_process_running() -end - -local function resolve_active_clash_sid(sid) - if is_active_clash_node(sid) then - return sid - end - - local current_sid = uci:get_first("shadowsocksr", "global", "global_server") - if is_active_clash_node(current_sid) then - return current_sid - end - - return nil -end - -local function clash_api_request(sid, method, path, body) - sid = resolve_active_clash_sid(sid) - if not sid then - return nil - end - local secret = get_clash_secret(sid) - local cmd = string.format( - "curl -sL -m 5 --retry 1 -w '\n__CURL_STATUS__:%%{http_code}' -H %s -H %s -X %s http://127.0.0.1:%s%s %s", - luci.util.shellquote("Content-Type: application/json"), - luci.util.shellquote("Authorization: Bearer " .. secret), - method, - CLASH_API_PORT, - path, - body and ("-d " .. luci.util.shellquote(body)) or "" - ) - local output = luci.sys.exec(cmd) - if not output or output == "" then - return nil - end - local body_output = output:gsub("\n__CURL_STATUS__:%d%d%d%s*$", "") - local code = output:match("__CURL_STATUS__:(%d%d%d)") - return { - code = tonumber(code), - body = body_output - } -end - -local function use_fw4_backend() - return luci.sys.call("command -v fw4 >/dev/null") == 0 -end - -local function parse_clash_groups(raw) - local info = json.parse(raw or "") - local proxies = info and info.proxies or nil - local groups = {} - if type(proxies) ~= "table" then - return groups - end - for name, value in pairs(proxies) do - if type(value) == "table" and type(value.all) == "table" and #value.all > 0 then - groups[#groups + 1] = { - name = name, - type = value.type or "", - now = value.now or "", - all = value.all - } +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 - table.sort(groups, function(a, b) return tostring(a.name) < tostring(b.name) end) - return groups -end - -local function parse_kv_output(raw) - local data = {} - for line in tostring(raw or ""):gmatch("[^\r\n]+") do - local key, value = line:match("^([%w_]+)=(.*)$") - if key then - data[key] = value - end - end - return data -end - -local function with_detect_cache_lock(fn) - for _ = 1, 40 do - if nixio.fs.mkdir(SERVER_DETECT_LOCK) then - local ok, ret = pcall(fn) - nixio.fs.rmdir(SERVER_DETECT_LOCK) - if ok then - return ret - end - return nil - end - nixio.nanosleep(0, 50000000) - end - return nil -end - -local function load_detect_cache() - local raw = nixio.fs.readfile(SERVER_DETECT_CACHE) - if not raw or raw == "" then - return {} - end - local parsed = json.parse(raw) - return type(parsed) == "table" and parsed or {} -end - -local function save_detect_cache_entry(sid, data) - if not sid or sid == "" then - return - end - with_detect_cache_lock(function() - local cache = load_detect_cache() - cache[sid] = data - nixio.fs.writefile(SERVER_DETECT_CACHE, json.stringify(cache)) - end) -end - -local function read_component_state(component, action) - if not SUPPORTED_COMPONENTS[component] then - return nil, 400, "unsupported_component" - end - - local mirror = luci.http.formvalue("mirror") or "" - local cmd = string.format( - "COMPONENT_MIRROR=%s /bin/sh %s %s 2>/dev/null", - luci.util.shellquote(mirror), - luci.util.shellquote(COMPONENT_HELPER), - luci.util.shellquote(component .. "_" .. action) - ) - return parse_kv_output(luci.sys.exec(cmd)) -end - -local function read_geo_state(component, action) - if not SUPPORTED_GEO_COMPONENTS[component] then - return nil, 400, "unsupported_component" - end - - local mirror = luci.http.formvalue("mirror") or "" - local cmd = string.format( - "COMPONENT_MIRROR=%s /bin/sh %s %s 2>/dev/null", - luci.util.shellquote(mirror), - luci.util.shellquote(COMPONENT_HELPER), - luci.util.shellquote(component .. "_" .. action) - ) - return parse_kv_output(luci.sys.exec(cmd)) -end - -local function write_component_json(data) - luci.http.prepare_content("application/json") - luci.http.write_json({ - component = data.component or "xray", - installed = data.installed == "1", - current_version = data.current_version or "", - latest_version = data.latest_version or "", - previous_version = data.previous_version or "", - arch = data.arch or "", - asset = data.asset or "", - can_upgrade = data.can_upgrade == "1", - success = data.success == "1", - error = data.error or "", - message = data.message or "" - }) -end - -local function write_geo_json(data) - luci.http.prepare_content("application/json") - luci.http.write_json({ - component = data.component or "country_mmdb", - installed = data.installed == "1", - current_version = data.current_version or "", - current_version_extra = data.current_version_extra or "", - latest_version = data.latest_version or "", - can_upgrade = data.can_upgrade == "1", - success = data.success == "1", - error = data.error or "", - message = data.message or "" - }) -end - -local function shell_quote(value) - return "'" .. tostring(value or ""):gsub("'", "'\\''") .. "'" + return require "luci.dispatcher".build_url(url) end function index() @@ -623,47 +54,31 @@ function index() page.dependent = true page.acl_depends = { "luci-app-ssr-plus" } entry({"admin", "services", "shadowsocksr", "client"}, cbi("shadowsocksr/client"), _("SSR Client"), 10).leaf = true - entry({"admin", "services", "shadowsocksr", "servers"}, arcombine(cbi("shadowsocksr/servers"), cbi("shadowsocksr/client-config")), _("Servers Nodes"), 20).leaf = true + entry({"admin", "services", "shadowsocksr", "servers"}, arcombine(cbi("shadowsocksr/servers", {autoapply = true}), cbi("shadowsocksr/client-config")), _("Servers Nodes"), 20).leaf = true entry({"admin", "services", "shadowsocksr", "control"}, cbi("shadowsocksr/control"), _("Access Control"), 30).leaf = true entry({"admin", "services", "shadowsocksr", "advanced"}, cbi("shadowsocksr/advanced"), _("Advanced Settings"), 50).leaf = true entry({"admin", "services", "shadowsocksr", "server"}, arcombine(cbi("shadowsocksr/server"), cbi("shadowsocksr/server-config")), _("SSR Server"), 60).leaf = true - entry({"admin", "services", "shadowsocksr", "component"}, cbi("shadowsocksr/component"), _("Component Update"), 65).leaf = true entry({"admin", "services", "shadowsocksr", "status"}, form("shadowsocksr/status"), _("Status"), 70).leaf = true entry({"admin", "services", "shadowsocksr", "check"}, call("check_status")) entry({"admin", "services", "shadowsocksr", "refresh"}, call("refresh_data")) entry({"admin", "services", "shadowsocksr", "subscribe"}, call("subscribe")) - entry({"admin", "services", "shadowsocksr", "component_local_status"}, call("component_local_status")).leaf = true - entry({"admin", "services", "shadowsocksr", "component_set_mirror"}, call("component_set_mirror")).leaf = true - entry({"admin", "services", "shadowsocksr", "component_status"}, call("component_status")).leaf = true - entry({"admin", "services", "shadowsocksr", "component_upgrade"}, call("component_upgrade")).leaf = true - entry({"admin", "services", "shadowsocksr", "geo_local_status"}, call("geo_local_status")).leaf = true - entry({"admin", "services", "shadowsocksr", "geo_status"}, call("geo_status")).leaf = true - entry({"admin", "services", "shadowsocksr", "geo_upgrade"}, call("geo_upgrade")).leaf = true entry({"admin", "services", "shadowsocksr", "checkport"}, call("check_port")) entry({"admin", "services", "shadowsocksr", "log"}, form("shadowsocksr/log"), _("Log"), 80).leaf = true entry({"admin", "services", "shadowsocksr", "get_log"}, call("get_log")).leaf = true entry({"admin", "services", "shadowsocksr", "clear_log"}, call("clear_log")).leaf = true entry({"admin", "services", "shadowsocksr", "run"}, call("act_status")) entry({"admin", "services", "shadowsocksr", "ping"}, call("act_ping")) - entry({"admin", "services", "shadowsocksr", "save_order"}, call("save_order")).leaf = true - entry({"admin", "services", "shadowsocksr", "delete_node"}, call("act_delete_node")).leaf = true - entry({"admin", "services", "shadowsocksr", "add_subscribe_item"}, call("add_subscribe_item")).leaf = true - entry({"admin", "services", "shadowsocksr", "delete_subscribe_item"}, call("delete_subscribe_item")).leaf = true - entry({"admin", "services", "shadowsocksr", "toggle_subscribe_item_enabled"}, call("toggle_subscribe_item_enabled")).leaf = true entry({"admin", "services", "shadowsocksr", "reset"}, call("act_reset")) entry({"admin", "services", "shadowsocksr", "restart"}, call("act_restart")) entry({"admin", "services", "shadowsocksr", "delete"}, call("act_delete")) - entry({"admin", "services", "shadowsocksr", "clash_panel"}, call("clash_panel")).leaf = true - entry({"admin", "services", "shadowsocksr", "clash_groups"}, call("clash_groups")).leaf = true - entry({"admin", "services", "shadowsocksr", "clash_switch"}, call("clash_switch")).leaf = true - entry({"admin", "services", "shadowsocksr", "clash_refresh"}, call("clash_refresh")).leaf = true - entry({"admin", "services", "shadowsocksr", "clash_reset_defaults"}, call("clash_reset_defaults")).leaf = true - entry({"admin", "services", "shadowsocksr", "clash_client_policies"}, call("clash_client_policies")).leaf = true - entry({"admin", "services", "shadowsocksr", "clash_client_rule_save"}, call("clash_client_rule_save")).leaf = true - entry({"admin", "services", "shadowsocksr", "clash_client_rule_clear"}, call("clash_client_rule_clear")).leaf = true + --[[ API ]] + entry({"admin", "services", "shadowsocksr", "add_node"}, call("act_add_node")).leaf = true + entry({"admin", "services", "shadowsocksr", "remove_node"}, call("act_remove")) + entry({"admin", "services", "shadowsocksr", "save_node_order"}, call("act_save_order")).leaf = true + entry({"admin", "services", "shadowsocksr", "get_now_use_node"}, call("act_get_now_use_node")).leaf = true + entry({'admin', 'services', "shadowsocksr", 'ip'}, call('check_ip')) -- 获取ip情况 --[[Backup]] entry({"admin", "services", "shadowsocksr", "backup"}, call("create_backup")).leaf = true - entry({'admin', 'services', "shadowsocksr", 'ip'}, call('check_ip')) -- 获取ip情况 end function check_site(host, port) @@ -722,639 +137,34 @@ function check_ip() end function subscribe() - nixio.fs.remove(SERVER_DETECT_CACHE) - local sid = luci.http.formvalue("sid") or "" - local subscribe_arg = sid ~= "" and (" " .. luci.util.shellquote(sid)) or "" - local ret = luci.sys.call(": > /var/log/ssrplus.log && /usr/bin/lua /usr/share/shadowsocksr/subscribe.lua" .. subscribe_arg .. " >>/var/log/ssrplus.log 2>&1") + luci.sys.call("/usr/bin/lua /usr/share/shadowsocksr/subscribe.lua >>/var/log/ssrplus.log") luci.http.prepare_content("application/json") - luci.http.write_json({ret = ret}) -end - -function save_order() - local order = luci.http.formvalue("order") or "" - local page = parse_nonnegative_int(luci.http.formvalue("page")) or 1 - local page_size = parse_nonnegative_int(luci.http.formvalue("page_size")) or 0 - local sids = {} - local all_sections = {} - local server_sections = {} - local server_positions = {} - local section_index = {} - local page_start - local page_end - - for sid in order:gmatch("%S+") do - if uci:get("shadowsocksr", sid) == "servers" and not section_index[sid] then - sids[#sids + 1] = sid - section_index[sid] = true - end - end - - uci:foreach("shadowsocksr", nil, function(section) - all_sections[#all_sections + 1] = section[".name"] - if section[".type"] == "servers" then - server_sections[#server_sections + 1] = section[".name"] - server_positions[#server_positions + 1] = #all_sections - end - end) - - page_start = 1 - page_end = #server_sections - if page_size > 0 then - page_start = ((math.max(page, 1) - 1) * page_size) + 1 - page_end = math.min(page_start + page_size - 1, #server_sections) - end - - local page_sections = {} - local page_lookup = {} - for index = page_start, page_end do - local sid = server_sections[index] - if sid then - page_sections[#page_sections + 1] = sid - page_lookup[sid] = true - end - end - - local valid = #sids > 0 and #sids == #page_sections - if valid then - for _, sid in ipairs(sids) do - if not page_lookup[sid] then - valid = false - break - end - end - end - - if valid then - for offset, sid in ipairs(sids) do - local absolute_index = page_start + offset - 1 - local position = server_positions[absolute_index] - if not position then - valid = false - break - end - - local cmd = string.format( - "uci -q reorder %s=%d >/dev/null 2>&1", - shell_quote("shadowsocksr." .. sid), - position - 1 - ) - - if luci.sys.call(cmd) ~= 0 then - valid = false - break - end - all_sections[position] = sid - end - if valid then - valid = luci.sys.call("uci -q commit shadowsocksr >/dev/null 2>&1") == 0 - end - end - - luci.http.prepare_content("application/json") - luci.http.write_json({ - ret = valid and 1 or 0, - count = #sids, - page = page, - page_size = page_size - }) -end - -function act_delete_node() - local sid = luci.http.formvalue("sid") - - if not sid or sid == "" then - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "missing sid" }) - return - end - - local del_cmd = luci.sys.call("uci -q delete shadowsocksr." .. sid) - if del_cmd ~= 0 then - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "delete failed" }) - return - end - - local ret_cmd = luci.sys.call("uci -q commit shadowsocksr >/dev/null 2>&1") - if ret_cmd ~= 0 then - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "commit failed" }) - return - end - - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 1, sid = sid }) -end - -function add_subscribe_item() - local sid = luci.sys.exec("uci add shadowsocksr server_subscribe_item"):gsub("%s+", "") - - if not sid or sid == "" then - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "add failed" }) - return - end - - local alias = string.format("Subscribe %s", sid:sub(-4)) - - -- set enabled - local subscribe_enabled = luci.sys.call("uci -q set shadowsocksr." .. sid .. ".enabled=1") - if subscribe_enabled ~= 0 then - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "set enabled failed" }) - return - end - - -- set alias - local subscribe_alias = luci.sys.call("uci -q set shadowsocksr." .. sid .. ".alias='" .. alias .. "'") - if subscribe_alias ~= 0 then - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "set alias failed" }) - return - end - - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 1, sid = sid, alias = alias, enabled = "1" }) -end - -function delete_subscribe_item() - local sid = trim(luci.http.formvalue("sid")) - if sid == "" then - luci.http.status(400, "Bad Request") - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "missing sid" }) - return - end - - local delete_subscribe_set = luci.sys.call("uci -q delete shadowsocksr." .. sid .. " 2>/dev/null") - if delete_subscribe_set ~= 0 then - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "delete failed" }) - return - end - - -- commit - local delete_subscribe_cmd = luci.sys.call("uci -q commit shadowsocksr >/dev/null 2>&1") - if delete_subscribe_cmd ~= 0 then - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "commit failed" }) - return - end - - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 1, sid = sid }) -end - -function toggle_subscribe_item_enabled() - local sid = trim(luci.http.formvalue("sid")) - local field = luci.http.formvalue("field") - local value = luci.http.formvalue("value") - - -- 兼容旧调用方式(只传 enabled) - if not field then - field = "enabled" - value = luci.http.formvalue("enabled") == "1" and "1" or "0" - end - - -- 参数校验 - if sid == "" then - luci.http.status(400, "Bad Request") - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "missing sid" }) - return - end - - -- 检查 sid 对应的 section 类型 - if uci:get("shadowsocksr", sid) ~= "server_subscribe_item" then - luci.http.status(400, "Bad Request") - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "invalid_sid" }) - return - end - - -- 白名单:只允许以下字段 - local allowed_fields = { enabled = true, alias = true, url = true } - if not allowed_fields[field] then - luci.http.status(400, "Bad Request") - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "unsupported field" }) - return - end - - -- 处理字段值 - if field == "enabled" then - value = (value == "1" or value == "true") and "1" or "0" - elseif field == "alias" or field == "url" then - value = value and trim(value) or "" - end - - -- 转义 value 中的单引号(避免破坏 uci 命令) - local escaped_value = value:gsub("'", "'\\''") - - -- 使用外部 uci 命令设置值 - local set_cmd = string.format("uci -q set shadowsocksr.%s.%s='%s'", sid, field, escaped_value) - local set_ret = luci.sys.call(set_cmd .. " >/dev/null 2>&1") - if set_ret ~= 0 then - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "uci set failed" }) - return - end - - -- 提交更改 - local ret_cmd = luci.sys.call("uci -q commit shadowsocksr >/dev/null 2>&1") - if ret_cmd ~= 0 then - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 0, error = "commit failed" }) - return - end - - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 1, sid = sid, field = field, value = value }) -end - -function component_status() - local component = luci.http.formvalue("component") - local data, status, err = read_component_state(component, "info") - if not data then - luci.http.status(status or 500, "Bad Request") - write_component_json({component = component, error = err or "bad_request"}) - return - end - write_component_json(data) -end - -function component_local_status() - local component = luci.http.formvalue("component") - local data, status, err = read_component_state(component, "local_info") - if not data then - luci.http.status(status or 500, "Bad Request") - write_component_json({component = component, error = err or "bad_request"}) - return - end - write_component_json(data) -end - -function component_set_mirror() - local mirror = luci.http.formvalue("mirror") or "direct" - local allowed = { - direct = true, - ghproxy = true, - ghproxy_cc = true, - ghfast = true, - jsdelivr = true - } - - if not allowed[mirror] then - mirror = "direct" - end - - uci:set("shadowsocksr", "@global[0]", "component_mirror", mirror) - uci:commit("shadowsocksr") - - luci.http.prepare_content("application/json") - luci.http.write_json({ ret = 1, mirror = mirror }) -end - -function component_upgrade() - local component = luci.http.formvalue("component") - local data, status, err = read_component_state(component, "upgrade") - if not data then - luci.http.status(status or 500, "Bad Request") - write_component_json({component = component, error = err or "bad_request", success = "0"}) - return - end - - local info = read_component_state(component, "info") - if info then - for key, value in pairs(info) do - if data[key] == nil or data[key] == "" then - data[key] = value - end - end - if data.success == "1" then - data.can_upgrade = info.can_upgrade - end - end - - write_component_json(data) -end - -function geo_status() - local component = luci.http.formvalue("component") - local data, status, err = read_geo_state(component, "info") - if not data then - luci.http.status(status or 500, "Bad Request") - write_geo_json({component = component, error = err or "bad_request"}) - return - end - write_geo_json(data) -end - -function geo_local_status() - local component = luci.http.formvalue("component") - local data, status, err = read_geo_state(component, "local_info") - if not data then - luci.http.status(status or 500, "Bad Request") - write_geo_json({component = component, error = err or "bad_request"}) - return - end - write_geo_json(data) -end - -function geo_upgrade() - local component = luci.http.formvalue("component") - local data, status, err = read_geo_state(component, "upgrade") - if not data then - luci.http.status(status or 500, "Bad Request") - write_geo_json({component = component, error = err or "bad_request", success = "0"}) - return - end - write_geo_json(data) + luci.http.write_json({ret = 1}) end function act_status() local e = {} - e.running = global_client_running() - if e.running then - e.status_text = get_running_status_text() - end + e.running = luci.sys.call("busybox ps -w | grep ssr-retcp | grep -v grep >/dev/null") == 0 luci.http.prepare_content("application/json") luci.http.write_json(e) end -function clash_panel() - local sid = luci.http.formvalue("sid") - if uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then - luci.http.status(404, "Not Found") - return - end - luci.template.render("shadowsocksr/clash_panel", { - sid = sid, - alias = uci:get("shadowsocksr", sid, "alias") or sid - }) -end - -function clash_groups() - local sid = luci.http.formvalue("sid") - local groups = {} - local active_sid = resolve_active_clash_sid(sid) - local active = active_sid ~= nil - if active then - local raw = clash_api_request(active_sid, "GET", "/proxies") - if raw then - groups = parse_clash_groups(raw.body) - end - end - luci.http.prepare_content("application/json") - luci.http.write_json({ - active = active, - sid = active_sid, - groups = groups - }) -end - -function clash_switch() - local sid = luci.http.formvalue("sid") - local group = luci.http.formvalue("group") - local name = luci.http.formvalue("name") - if not sid or not group or not name then - luci.http.status(400, "Bad Request") - return - end - local active_sid = resolve_active_clash_sid(sid) - if not active_sid then - luci.http.status(409, "Conflict") - luci.http.prepare_content("application/json") - luci.http.write_json({success = false, message = "inactive"}) - return - end - local body = string.format('{"name":"%s"}', tostring(name):gsub('"', '\\"')) - local path = "/proxies/" .. urlencode(group) - local ret = clash_api_request(active_sid, "PUT", path, body) - luci.http.prepare_content("application/json") - luci.http.write_json({success = ret ~= nil and ret.code and ret.code >= 200 and ret.code < 300, sid = active_sid}) -end - -function clash_refresh() - local sid = luci.http.formvalue("sid") - if not sid or uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then - luci.http.status(400, "Bad Request") - luci.http.prepare_content("application/json") - luci.http.write_json({success = false}) - return - end - local cmd = string.format("/etc/init.d/shadowsocksr clash_cache %s >/dev/null 2>&1", luci.util.shellquote(sid)) - local ok = luci.sys.call(cmd) == 0 - local reapplied = false - if ok and is_active_clash_node(sid) then - luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &") - reapplied = true - end - luci.http.prepare_content("application/json") - luci.http.write_json({ - success = ok, - cached = nixio.fs.access(get_clash_cache_file(sid)), - reapplied = reapplied - }) -end - -function clash_reset_defaults() - local sid = luci.http.formvalue("sid") - if not sid or uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then - luci.http.status(400, "Bad Request") - luci.http.prepare_content("application/json") - luci.http.write_json({success = false}) - return - end - - local state_file = get_clash_state_file(sid) - local cleared = false - if nixio.fs.access(state_file) then - cleared = nixio.fs.remove(state_file) or false - else - cleared = true - end - - local reapplied = false - if cleared and is_active_clash_node(sid) then - luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &") - reapplied = true - end - - luci.http.prepare_content("application/json") - luci.http.write_json({ - success = cleared, - reapplied = reapplied - }) -end - -function clash_client_policies() - local sid = luci.http.formvalue("sid") - local active_sid = resolve_active_clash_sid(sid) - local groups = {} - if active_sid then - local raw = clash_api_request(active_sid, "GET", "/proxies") - if raw then - groups = parse_clash_groups(raw.body) - end - end - - local policies = {} - local seen = {} - for _, group in ipairs(groups or {}) do - if group.name and not seen[group.name] then - seen[group.name] = true - policies[#policies + 1] = { - name = group.name, - label = group.name, - type = group.type or "" - } - end - for _, proxy_name in ipairs(group.all or {}) do - if proxy_name and proxy_name ~= "" and not seen[proxy_name] then - seen[proxy_name] = true - policies[#policies + 1] = { - name = proxy_name, - label = proxy_name, - type = "proxy" - } - end - end - end - - table.sort(policies, function(a, b) - return tostring(a.label or a.name) < tostring(b.label or b.name) - end) - - luci.http.prepare_content("application/json") - luci.http.write_json({ - active = active_sid ~= nil, - sid = active_sid, - clients = collect_lan_clients(), - rules = read_clash_client_rules(sid), - policies = policies - }) -end - -function clash_client_rule_save() - local sid = luci.http.formvalue("sid") - local rows = {} - local max_rows = tonumber(luci.http.formvalue("count") or "0") or 0 - - for index = 1, math.min(max_rows, 256) do - local prefix = string.format("rule_%d_", index) - local ip_addr = normalize_client_ip(luci.http.formvalue(prefix .. "ip_addr")) - local enabled = luci.http.formvalue(prefix .. "enabled") == "1" and "1" or "0" - local remarks = trim(luci.http.formvalue(prefix .. "remarks")) - local policy_group = trim(luci.http.formvalue(prefix .. "policy_group")) - local client_mac = sanitize_mac(luci.http.formvalue(prefix .. "client_mac")) - - if ip_addr ~= "" and policy_group ~= "" then - rows[#rows + 1] = { - enabled = enabled, - remarks = remarks, - ip_addr = ip_addr, - client_mac = client_mac, - policy_group = policy_group - } - end - end - - if not sid or sid == "" or uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then - luci.http.status(400, "Bad Request") - luci.http.prepare_content("application/json") - luci.http.write_json({ success = false, error = "invalid_sid" }) - return - end - - write_clash_client_rules_csv(sid, rows) - - local active_sid = resolve_active_clash_sid(sid) - local current_sid = uci:get_first("shadowsocksr", "global", "global_server") - local reapplied = false - if sid and sid ~= "" and uci:get("shadowsocksr", sid) == "servers" - and uci:get("shadowsocksr", sid, "type") == "clash" - and current_sid == sid then - luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &") - reapplied = true - end - - luci.http.prepare_content("application/json") - luci.http.write_json({ - success = true, - count = #rows, - reapplied = reapplied, - rules = read_clash_client_rules(sid) - }) -end - -function clash_client_rule_clear() - local sid = luci.http.formvalue("sid") - - if not sid or sid == "" or uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then - luci.http.status(400, "Bad Request") - luci.http.prepare_content("application/json") - luci.http.write_json({ success = false, error = "invalid_sid" }) - return - end - - local csv_path = get_clash_client_rule_csv_path(sid) - if csv_path and nixio.fs.access(csv_path) then - nixio.fs.remove(csv_path) - end - - local current_sid = uci:get_first("shadowsocksr", "global", "global_server") - local reapplied = false - if sid and sid ~= "" and uci:get("shadowsocksr", sid) == "servers" - and uci:get("shadowsocksr", sid, "type") == "clash" - and current_sid == sid then - luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &") - reapplied = true - end - - luci.http.prepare_content("application/json") - luci.http.write_json({ - success = true, - reapplied = reapplied, - rules = {} - }) -end - function act_ping() local e = {} local domain = luci.http.formvalue("domain") local port = tonumber(luci.http.formvalue("port") or 0) local transport = (luci.http.formvalue("transport") or ""):lower() local wsPath = luci.http.formvalue("wsPath") or "" - local host = luci.http.formvalue("host") or "" - local tls_host = luci.http.formvalue("tlsHost") or "" local tls = luci.http.formvalue("tls") + local host = luci.http.formvalue("host") local type = (luci.http.formvalue("type") or ""):lower() local proto = (luci.http.formvalue("proto") or ""):lower() - local reality = luci.http.formvalue("reality") - local sid = luci.http.formvalue("sid") e.index = luci.http.formvalue("index") - if sid and sid ~= "" and uci:get("shadowsocksr", sid) == "servers" then - domain = uci:get("shadowsocksr", sid, "server") or domain - port = tonumber(uci:get("shadowsocksr", sid, "server_port") or port or 0) - transport = (uci:get("shadowsocksr", sid, "transport") or transport or ""):lower() - wsPath = uci:get("shadowsocksr", sid, "ws_path") or wsPath - host = uci:get("shadowsocksr", sid, "ws_host") or host - tls_host = uci:get("shadowsocksr", sid, "tls_host") or tls_host - tls = uci:get("shadowsocksr", sid, "tls") or tls - type = (uci:get("shadowsocksr", sid, "type") or type or ""):lower() - proto = (uci:get("shadowsocksr", sid, "v2ray_protocol") or proto or ""):lower() - reality = uci:get("shadowsocksr", sid, "reality") or reality - end - local is_ip = domain and domain:match("^%d+%.%d+%.%d+%.%d+$") - local probe_host = (tls_host ~= "" and tls_host) or (host ~= "" and host) or domain - local is_reality = (reality == "1" or reality == "true") - local prefers_handshake_latency = (type == "v2ray") and not is_reality -- 临时放行防火墙逻辑 - local use_nft = use_fw4_backend() + local use_nft = luci.sys.call("command -v nft >/dev/null") == 0 local iret = false if domain then if use_nft then @@ -1363,52 +173,46 @@ function act_ping() iret = luci.sys.call("ipset add ss_spec_wan_ac " .. domain .. " 2>/dev/null") == 0 end end - -- Hysteria2 节点轻量 UDP 端口检测 + -- Hysteria2 节点检测 if proto:find("hysteria2") or type:find("hysteria2") then - local udp_cmd = string.format("nping --udp -c 1 -p %d %s 2>/dev/null", port, domain) - local udp_raw = luci.sys.exec(udp_cmd) or "" - local udp_rtt = udp_raw:match("Avg rtt:%s*([0-9.]+)ms") - local udp_unreachable = udp_raw:match("[Pp]ort [Uu]nreachable") or udp_raw:match("ICMP") - local udp_sent = udp_raw:match("Raw packets sent:%s*1") - - -- UDP 服务通常不会主动回包,未收到应答不等于端口不可用。 - -- 仅在出现明显的不可达迹象时标记 fail,其余视为轻量可达。 - e.socket = (udp_unreachable == nil) and (udp_sent ~= nil) - e.ping = udp_rtt and normalize_ping_ms(udp_rtt) or nil - - if not e.ping then - local icmp_cmd = string.format("ping -c 1 -W 1 %s 2>/dev/null | grep -o 'time=[0-9.]*' | cut -d= -f2", domain) - e.ping = normalize_ping_ms(tonumber(luci.sys.exec(icmp_cmd))) - end - if not e.ping then + local node_id = e.index + -- 调用Shell测试脚本 + local cmd = string.format( + "/usr/share/shadowsocksr/hy2_test.sh url_test_hy2 %s", + node_id + ) + local res = luci.sys.exec(cmd) or "" + -- 解析结果 + local http_code, time_pre = string.match(res, "(%d+):([%d%.]+)") + if http_code == "200" or http_code == "204" then + e.socket = true + e.ping = math.floor(tonumber(time_pre or 0) * 1000) + else + e.socket = false e.ping = 0 end elseif transport == "ws" then -- WebSocket 探测 local result = "" local success = false - local icmp_cmd = string.format("ping -c 1 -W 1 %s 2>/dev/null | grep -o 'time=[0-9.]*' | cut -d= -f2", domain) - e.ping = normalize_ping_ms(tonumber(luci.sys.exec(icmp_cmd))) -- WebSocket 探测 (适用于域名,或带 SNI 的 IP) - if not is_ip or probe_host ~= domain then + if not is_ip or (host and host ~= "") then local resolve_arg = "" - local final_domain = probe_host - if is_ip and probe_host and probe_host ~= "" then + local final_domain = domain + if is_ip and host and host ~= "" then -- IP 模式下使用 --resolve 强制指定 SNI,解决 TLS 握手失败 - resolve_arg = string.format("--resolve '%s:%d:%s' ", probe_host, port, domain) + resolve_arg = string.format("--resolve '%s:%d:%s' ", host, port, domain) + final_domain = host end local prefix = (tls == '1') and "https://" or "http://" local address = prefix .. final_domain .. ':' .. port .. wsPath local cmd = string.format( "curl --http1.1 -m 2 -ksN -o /dev/null %s" .. "-w 'time_connect=%%{time_connect}\\nhttp_code=%%{http_code}' " .. - "%s" .. "-H 'Connection: Upgrade' -H 'Upgrade: websocket' " .. "-H 'Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==' " .. "-H 'Sec-WebSocket-Version: 13' '%s'", - resolve_arg, - (probe_host and probe_host ~= "") and ("-H " .. luci.util.shellquote("Host: " .. probe_host) .. " ") or "", - address + resolve_arg, address ) result = luci.sys.exec(cmd) or "" success = (string.match(result, "http_code=(%d+)") == "101") @@ -1425,17 +229,13 @@ function act_ping() --luci.sys.exec(string.format("echo 'Node %s (ws) failed deep test, using TCP fallback' >> /tmp/ping.log", domain)) end e.socket = success - -- 延迟:优先 ping,再 curl,最后 nping tcp-connect - if not e.ping then - local ping_time = tonumber(string.match(result, "time_connect=(%d+.%d%d%d)")) - local appconnect_time = tonumber(string.match(result, "time_appconnect=(%d+.%d%d%d)")) - if appconnect_time and appconnect_time > 0 then - e.ping = normalize_ping_ms(appconnect_time, 1000) - elseif ping_time and ping_time > 0 then - e.ping = normalize_ping_ms(ping_time, 1000) - else - e.ping = detect_tcp_connect_ms(domain, port) or 0 - end + -- 解析延迟 (优先用 curl 数据,失败则回退到 tcping) + local ping_time = tonumber(string.match(result, "time_connect=(%d+.%d%d%d)")) + if ping_time and ping_time > 0 then + e.ping = math.floor(ping_time * 1000) + else + local tcping_cmd = string.format("tcping -q -c 1 -t 1 -p %d %s 2>/dev/null | grep -o 'time=[0-9]*' | cut -d= -f2", port, domain) + e.ping = tonumber(luci.sys.exec(tcping_cmd)) or 0 end else -- 3. 非 WebSocket 节点的探测逻辑 (TCP / ICMP / UDP) @@ -1447,19 +247,12 @@ function act_ping() socket:close() end - if prefers_handshake_latency and (tls == "1" or tls_host ~= "" or proto == "vless" or proto == "vmess") then - e.ping = detect_tls_handshake_ms(domain, port, "", probe_host, domain, false) - end - - -- 延迟:优先真实握手,再 nping tcp-connect -> ping -> nping(udp) - if not e.ping then - if not is_reality then - e.ping = detect_tcp_connect_ms(domain, port) - end - end + -- 延迟:tcping -> ping -> nping(udp) + local tcping_cmd = string.format("tcping -q -c 1 -t 1 -p %d %s 2>/dev/null | grep -o 'time=[0-9]*' | cut -d= -f2", port, domain) + e.ping = tonumber(luci.sys.exec(tcping_cmd)) if not e.ping then local icmp_cmd = string.format("ping -c 1 -W 1 %s 2>/dev/null | grep -o 'time=[0-9.]*' | cut -d= -f2", domain) - e.ping = normalize_ping_ms(tonumber(luci.sys.exec(icmp_cmd))) + e.ping = tonumber(luci.sys.exec(icmp_cmd)) end if not e.ping then @@ -1467,23 +260,7 @@ function act_ping() local udp_res = luci.sys.exec(udp_cmd) if udp_res and udp_res ~= "" then local ping_num = tonumber(udp_res) - if ping_num then e.ping = normalize_ping_ms(ping_num) end - end - end - - if (not e.ping or e.ping == 0) and domain and port > 0 then - local schemes = { "https", "http" } - for _, scheme in ipairs(schemes) do - local connect_cmd = string.format( - "curl -m 2 -ksS -o /dev/null -w 'time_connect=%%{time_connect}' %s://%s:%d 2>/dev/null", - scheme, domain, port - ) - local connect_res = luci.sys.exec(connect_cmd) or "" - local connect_time = tonumber(connect_res:match("time_connect=([0-9.]+)")) - if connect_time and connect_time > 0 then - e.ping = normalize_ping_ms(connect_time, 1000) - break - end + if ping_num then e.ping = math.floor(ping_num) end end end end @@ -1497,26 +274,13 @@ function act_ping() end end - if sid and sid ~= "" then - save_detect_cache_entry(sid, { - server = domain or "", - port = port or 0, - type = type or "", - proto = proto or "", - socket = e.socket and true or false, - ping = tonumber(e.ping) or 0, - time = os.time() - }) - end - luci.http.prepare_content("application/json") luci.http.write_json(e) end function check_status() local e = {} - local target = luci.http.formvalue("set") or "" - e.ret = luci.sys.call("curl -m 3 -sS -o /dev/null http://www." .. target .. ".com >/dev/null 2>&1") + e.ret = luci.sys.call("/usr/bin/ssr-check www." .. luci.http.formvalue("set") .. ".com 80 3 1") luci.http.prepare_content("application/json") luci.http.write_json(e) end @@ -1533,13 +297,9 @@ function check_port() local s local server_name = "" local uci = require "luci.model.uci".cursor() - local use_nft = use_fw4_backend() + local use_nft = luci.sys.call("command -v nft >/dev/null") == 0 uci:foreach("shadowsocksr", "servers", function(s) - if s.type == "clash" then - retstring = retstring .. string.format("[%s] Clash panel node.
", s.alias or s[".name"]) - return - end if s.alias then server_name = s.alias elseif s.server and s.server_port then @@ -1547,18 +307,15 @@ function check_port() end -- 临时加入 set - local is_ipv6 = is_ipv6_address(s.server) local iret = false - if not is_ipv6 then - if use_nft then - iret = luci.sys.call("nft add element inet ss_spec ss_spec_wan_ac { " .. s.server .. " } 2>/dev/null") == 0 - else - iret = luci.sys.call("ipset add ss_spec_wan_ac " .. s.server .. " 2>/dev/null") == 0 - end + if use_nft then + iret = luci.sys.call("nft add element inet ss_spec ss_spec_wan_ac { " .. s.server .. " } 2>/dev/null") == 0 + else + iret = luci.sys.call("ipset add ss_spec_wan_ac " .. s.server .. " 2>/dev/null") == 0 end -- TCP 测试 - local socket = nixio.socket(is_ipv6 and "inet6" or "inet", "stream") + local socket = nixio.socket("inet", "stream") socket:setopt("socket", "rcvtimeo", 3) socket:setopt("socket", "sndtimeo", 3) local ret = socket:connect(s.server, s.server_port) @@ -1590,15 +347,109 @@ function act_reset() end function act_restart() - luci.sys.call("/etc/init.d/shadowsocksr restart > /dev/null 2>&1 &") luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr")) end function act_delete() - luci.sys.call("/etc/init.d/shadowsocksr restart > /dev/null 2>&1 &") + uci:delete_all("shadowsocksr", "servers", function(s) + if s.hashkey or s.isSubscribe then + return true + else + return false + end + end) + uci:commit("shadowsocksr") + for file in nixio.fs.glob("/tmp/sub_md5_*") do + nixio.fs.remove(file) + end + luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &") luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers")) end +function act_add_node() + local redirect = luci.http.formvalue("redirect") + local used_sid = {} + local next_sid = 1 + + uci:foreach("shadowsocksr", "servers", function(s) + local num = s[".name"]:match("^cfg(%x%x)") + if num then + local n = tonumber(num, 16) + used_sid[n] = true + 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 = uci:section("shadowsocksr", "servers", nil) + local suffix = sid:sub(-4) + uci:delete("shadowsocksr", sid) + + local id = get_next_sid() + local cfgid = string.format("cfg%02x%s", id, suffix) + uci:section("shadowsocksr", "servers", cfgid) + uci_save(uci, "shadowsocksr") + + if redirect == "1" then + luci.http.redirect(url("servers", cfgid)) + else + luci.http.write_json({ result = cfgid }) + end +end + +function act_remove() + local id = luci.http.formvalue("id") + if id then + uci:delete("shadowsocksr", id) + uci:commit("shadowsocksr") + end + luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers")) +end + +function act_save_order() + local ids = luci.http.formvalue("ids") or "" + local new_order = {} + for id in ids:gmatch("([^,]+)") do + new_order[#new_order + 1] = id + end + + for idx, name in ipairs(new_order) do + luci.sys.call(string.format("uci -q reorder %s.%s=%d", "shadowsocksr", name, idx - 1)) + end + + sh_uci_commit("shadowsocksr") + luci.http.write_json({ status = "ok" }) +end + +function act_get_now_use_node() + local result = {} + local tcp_node = uci:get_first("shadowsocksr", "global", "global_server") + if tcp_node then + result["TCP"] = tcp_node + end + local udp_node = uci:get_first("shadowsocksr", "global", "udp_relay_server") + if udp_node then + result["UDP"] = udp_node + end + local netflix_node = uci:get_first("shadowsocksr", "global", "netflix_server") + if netflix_node then + result["netflix"] = netflix_node + end + local socks5_node = uci:get_first("shadowsocksr", "socks5_proxy", "server") + if socks5_node then + result["socks5"] = socks5_node + end + + luci.http.prepare_content("application/json") + luci.http.write_json(result) +end + function get_log() luci.http.write(luci.sys.exec("[ -f '/var/log/ssrplus.log' ] && cat /var/log/ssrplus.log")) end diff --git a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua index a1023748..2e5d38af 100644 --- a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua +++ b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua @@ -1,7 +1,6 @@ local m, s, o local cbi = require "luci.cbi" local uci = require "luci.model.uci".cursor() -local URL = require "url" -- 获取 LAN IP 地址 function lanip() @@ -31,32 +30,16 @@ local lan_ip = lanip() local server_table = {} local type_table = {} local function is_finded(e) - return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= "" -end - -local function clash_display_name(s) - if s.type ~= "clash" or not s.clash_url or s.clash_url == "" then - return nil - end - local ok, parsed = pcall(URL.parse, s.clash_url) - if ok and parsed and parsed.host then - return "[CLASH]:" .. parsed.host - end - return "[CLASH]" + return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= "" end 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} - 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} - 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 and s.type ~= "tun" then + if s.type then type_table[s[".name"]] = s.type end end) @@ -96,13 +79,6 @@ o.datatype = "uinteger" o:depends("enable_switch", "1") o.default = 3 -s:append(cbi.Template("shadowsocksr/advanced_switch_compact")) - -o = s:option(Value, "default_node_local_port", translate("Default Node Local Port")) -o.datatype = "port" -o.default = 1234 -o.rmempty = false - o = s:option(Value, "gfwlist_url", translate("gfwlist Update url")) o:value("https://fastly.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt", translate("v2fly/domain-list-community")) o:value("https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt", translate("Loyalsoldier/v2ray-rules-dat")) @@ -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.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( + "" + ) + 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( + "
    " .. + "
  • " .. translate("Defines the upstreams logic mode, possible values: load_balance, parallel, fastest_addr (default: load_balance).") .. "
  • " .. "
  • " .. translate("When two or more DNS servers are deployed, enable this function.") .. "
  • " .. + "
" + ) + 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( + "
    " .. + "
  • " .. translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") .. "
  • " .. + "
  • " .. translate("Muitiple DNS server can saperate with ','") .. "
  • " .. + "
" + ) + + o = s:option(ListValue, "chinadns_ng_shunt_proto", translate("ChinaDNS-NG shunt query protocol")) + o:value("none", translate("UDP/TCP upstream")) + o:value("tcp", translate("TCP upstream")) + o:value("udp", translate("UDP upstream")) + o:value("tls", translate("DoT upstream (Need use wolfssl version)")) + o:depends("shunt_dns_mode", "5") +end + o = s:option(Flag, "apple_optimization", translate("Apple domains optimization"), translate("For Apple domains equipped with Chinese mainland CDN, always responsive to Chinese CDN IP addresses")) o.rmempty = false o.default = "1" @@ -151,13 +266,45 @@ end -- [[ SOCKS5 Proxy ]]-- s = m:section(TypedSection, "socks5_proxy", translate("Global SOCKS5 Proxy Server")) s.anonymous = true --- s.description = translate("Only Same as Global Server is supported here. If the current main program supports a built-in SOCKS5 listener, it will be enabled in-process; otherwise SSR Plus will fall back to microsocks.") -- Enable/Disable Option o = s:option(Flag, "enabled", translate("Enable")) o.default = 0 o.rmempty = false +-- Server Selection +o = s:option(ListValue, "server", translate("Server")) +o:value("same", translate("Same as Global Server")) +for _, key in pairs(key_table) do + o:value(key, server_table[key]) +end +o.default = "same" +o.rmempty = false + +-- Dynamic value handling based on enabled/disabled state +o.cfgvalue = function(self, section) + local enabled = m:get(section, "enabled") + if enabled == "0" then + return m:get(section, "old_server") + end + return Value.cfgvalue(self, section)-- Default to `same` when enabled +end + +o.write = function(self, section, value) + local enabled = m:get(section, "enabled") + if enabled == "0" then + local old_server = Value.cfgvalue(self, section) + if old_server ~= "nil" then + m:set(section, "old_server", old_server) + end + m:set(section, "server", "nil") + else + m:del(section, "old_server") + -- Write the value normally when enabled + Value.write(self, section, value) + end +end + -- Socks Auth if is_finded("xray") then o = s:option(ListValue, "socks5_auth", translate("Socks5 Auth Mode"), translate("Socks protocol auth methods, default:noauth.")) @@ -165,6 +312,13 @@ o.default = "noauth" o:value("noauth", "NOAUTH") o:value("password", "PASSWORD") o.rmempty = true +for key, server_type in pairs(type_table) do + if server_type == "v2ray" then + -- 如果服务器类型是 v2ray,则设置依赖项显示 + o:depends("server", key) + end +end +o:depends({server = "same", disable = true}) -- Socks User o = s:option(Value, "socks5_user", translate("Socks5 User"), translate("Only when Socks5 Auth Mode is password valid, Mandatory.")) @@ -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.default = "1" o.rmempty = false +for key, server_type in pairs(type_table) do + if server_type == "v2ray" then + -- 如果服务器类型是 v2ray,则设置依赖项显示 + o:depends("server", key) + end +end +o:depends({server = "same", disable = true}) end -- Local Port @@ -189,37 +350,6 @@ o.datatype = "port" o.default = 1080 o.rmempty = false -if is_finded("3proxy") then - -- [[ HTTP/HTTPS Proxy ]]-- - s = m:section(TypedSection, "http_proxy", translate("Global HTTP/HTTPS Proxy Server")) - s.anonymous = true - -- s.description = translate("Only Same as Global Server is supported here. HTTP/HTTPS proxy service is provided through 3proxy.") - - o = s:option(Flag, "enabled", translate("Enable")) - o.default = 0 - o.rmempty = false - - o = s:option(ListValue, "http_auth", translate("HTTP Auth Mode"), translate("HTTP proxy auth method, default:none.")) - o.default = "none" - o:value("none", "NONE") - o:value("password", "PASSWORD") - o.rmempty = false - - o = s:option(Value, "http_user", translate("HTTP User"), translate("Only when HTTP Auth Mode is password valid, Mandatory.")) - o.rmempty = true - o:depends("http_auth", "password") - - o = s:option(Value, "http_pass", translate("HTTP Password"), translate("Only when HTTP Auth Mode is password valid, Not mandatory.")) - o.password = true - o.rmempty = true - o:depends("http_auth", "password") - - o = s:option(Value, "local_port", translate("Local Port")) - o.datatype = "port" - o.default = 3128 - o.rmempty = false -end - -- [[ fragmen Settings ]]-- if is_finded("xray") then s = m:section(TypedSection, "global_xray_fragment", translate("Xray Fragment Settings")) diff --git a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua index d06abbdc..567989ba 100644 --- a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua +++ b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua @@ -20,7 +20,7 @@ local xray_version_val = 0 -- 确保正确判断程序是否存在 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 is_installed(e) @@ -142,14 +142,16 @@ local function set_apply_on_parse(map) local old = map.on_after_save map.on_after_save = function(self) if old then old(self) end - --map:set("@global[0]", "timestamp", os.time()) + map:set("@global[0]", "timestamp", os.time()) end end end local has_ss_rust = is_finded("sslocal") or is_finded("ssserver") -local has_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_hysteria2 = is_finded("hysteria") local server_table = {} local encrypt_methods = { @@ -254,56 +256,6 @@ local tls_flows = { "none" } -local function migrate_xray_protocol_nodes() - local changed = false - - uci:foreach("shadowsocksr", "servers", function(section) - local sid = section[".name"] - local stype = section.type - local proto = section.v2ray_protocol - local escaped_sid = luci.util.shellquote(sid) - - if stype == "ss" or stype == "ss-libev" then - if has_mihomo then - luci.sys.call(string.format("uci set shadowsocksr.%s.type='ss'", escaped_sid)) - changed = true - elseif has_ss_rust then - luci.sys.call(string.format("uci set shadowsocksr.%s.type='ss-rust'", escaped_sid)) - changed = true - elseif has_xray then - luci.sys.call(string.format("uci set shadowsocksr.%s.type='v2ray' && " .. "uci set shadowsocksr.%s.v2ray_protocol='shadowsocks'", escaped_sid, escaped_sid)) - changed = true - end - elseif stype == "v2ray" and proto == "shadowsocks" and has_mihomo then - luci.sys.call(string.format("uci set shadowsocksr.%s.type='ss' && " .. "uci delete shadowsocksr.%s.v2ray_protocol", escaped_sid, escaped_sid)) - changed = true - end - if stype == "hysteria2" then - luci.sys.call(string.format("uci set shadowsocksr.%s.type='v2ray' && " .. "uci set shadowsocksr.%s.v2ray_protocol='hysteria2'", escaped_sid, escaped_sid)) - changed = true - elseif stype == "trojan" then - luci.sys.call(string.format("uci set shadowsocksr.%s.type='v2ray' && " .. "uci set shadowsocksr.%s.v2ray_protocol='trojan'", escaped_sid, escaped_sid)) - changed = true - end - end) - local subscribe_sid = uci:get_first("shadowsocksr", "server_subscribe") - if subscribe_sid then - local old_options = {"xray_hy2_type", "xray_tj_type", "ss_type"} - local escaped_sub_sid = luci.util.shellquote(subscribe_sid) - for _, opt in ipairs(old_options) do - if uci:get("shadowsocksr", subscribe_sid, opt) then - luci.sys.call(string.format("uci delete shadowsocksr.%s.%s", escaped_sub_sid, opt)) - changed = true - end - end - end - if changed then - luci.sys.call("uci commit shadowsocksr") - end -end - -migrate_xray_protocol_nodes() - m = Map("shadowsocksr", translate("Edit ShadowSocksR Server")) m.redirect = url("servers") if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then @@ -312,14 +264,6 @@ if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then end -- 保存&应用成功后跳转到节点列表 set_apply_on_parse(m) -local old_after_save = m.on_after_save -m.on_after_save = function(self) - if old_after_save then old_after_save(self) end - local node_type = self.uci:get("shadowsocksr", sid, "type") - if node_type == "clash" then - luci.sys.call(string.format("/etc/init.d/shadowsocksr clash_cache %s >/dev/null 2>&1 &", sid)) - end -end -- [[ Servers Setting ]]-- s = m:section(NamedSection, sid, "servers") @@ -331,27 +275,88 @@ o.rawhtml = true o.template = "shadowsocksr/ssrurl" o.value = sid +-- 新增一个选择框,用于选择 Xray 或 Hysteria2 核心 +o = s:option(ListValue, "_xray_hy2_type", string.format("%s", translatef("%s Node Use Type", "Hysteria2"))) +o.description = translate("The configured type also applies to the core specified when manually importing nodes.") +-- 注意:Auto 选项使用特殊字符串 "__auto__" 而不是空字符串 +o:value("__auto__", translate("Auto")) +if has_hysteria2 then + o:value("hysteria2", translate("Hysteria2")) +end +if has_xray then + o:value("v2ray", translate("Xray (Hysteria2)")) +end +-- 读取全局 xray_hy2_type +o.cfgvalue = function(self, section) + local val = uci:get("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type") + if val == nil or val == "" then + return "__auto__" -- 对应 Auto 选项 + end + return val +end +o.rmempty = true +-- 保存时更新全局配置 +o.write = function(self, section, value) + if value == "__auto__" then + -- 删除全局配置 + uci:delete("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type") + else + -- 设置具体值 + uci:set("shadowsocksr", "@server_subscribe[0]", "xray_hy2_type", value) + end +end + -- 新增一个选择框,用于选择 Xray 或 Trojan 核心 +o = s:option(ListValue, "_xray_tj_type", string.format("%s", translatef("%s Node Use Type", "Trojan"))) +o.description = translate("The configured type also applies to the core specified when manually importing nodes.") +-- 注意:Auto 选项使用特殊字符串 "__auto__" 而不是空字符串 +o:value("__auto__", translate("Auto")) +if has_hysteria2 then + o:value("trojan", translate("Trojan")) +end +if has_xray then + o:value("v2ray", translate("Xray (Trojan)")) +end +-- 读取全局 xray_tj_type +o.cfgvalue = function(self, section) + local val = uci:get("shadowsocksr", "@server_subscribe[0]", "xray_tj_type") + if val == nil or val == "" then + return "__auto__" -- 对应 Auto 选项 + end + return val +end +o.rmempty = true +-- 保存时更新全局配置 +o.write = function(self, section, value) + if value == "__auto__" then + -- 删除全局配置 + uci:delete("shadowsocksr", "@server_subscribe[0]", "xray_tj_type") + else + -- 设置具体值 + uci:set("shadowsocksr", "@server_subscribe[0]", "xray_tj_type", value) + end +end + o = s:option(ListValue, "type", translate("Server Node Type")) -if is_finded("xray") then +if is_finded("xray") or is_finded("v2ray") then o:value("v2ray", translate("V2Ray/XRay")) end if is_finded("ssr-redir") then o:value("ssr", translate("ShadowsocksR")) end -if has_mihomo then - o:value("ss", translate("ShadowSocks")) +if has_ss_rust or has_ss_libev then + o:value("ss", translate("ShadowSocks")) end -if has_ss_rust then - o:value("ss-rust", translate("ShadowSocks")) +if is_finded("trojan") then + o:value("trojan", translate("Trojan")) end if is_finded("naive") then o:value("naiveproxy", translate("NaiveProxy")) end -if is_finded("mihomo") then - o:value("clash", translate("Clash/Mihomo")) +if is_finded("hysteria") then + o:value("hysteria2", translate("Hysteria2")) end -if is_finded("mihomo") then +if is_finded("tuic-client") then o:value("tuic", translate("TUIC")) end if is_finded("shadow-tls") and is_finded("sslocal") then @@ -360,51 +365,78 @@ end if is_finded("ipt2socks") then o:value("socks5", translate("Socks5")) end +if is_finded("redsocks2") then + o:value("tun", translate("Network Tunnel")) +end local old_cfgvalue = o.cfgvalue o.cfgvalue = function(self, section) - local val = self.map.uci:get("shadowsocksr", section, "type") - if old_cfgvalue then + local val = self.map.uci:get("shadowsocksr", section, "type") + if val == "ss-rust" or val == "ss-libev" then + return "ss" + end + if old_cfgvalue then return old_cfgvalue(self, section) - end - return val + end + return val end +-- 重写 write,当用户选择 "ss" 时不写入(由 _ss_core 负责写入具体核心) +local old_write = o.write +o.write = function(self, section, value) + if value == "ss" then + return -- 不做任何写入,等待 _ss_core 写入 + end + if old_write then + old_write(self, section, value) + else + self.map.uci:set("shadowsocksr", section, "type", value) + end +end + o.description = translate("Using incorrect encryption mothod may causes service fail to start") o = s:option(Value, "alias", translate("Alias(optional)")) -local function clash_source_formvalue(map, section, option) - local value = map:formvalue("cbid." .. map.config .. "." .. section .. "." .. option) - if value == nil then - value = map.uci:get(map.config, section, option) +o = s:option(ListValue, "iface", translate("Network interface to use")) +for _, e in ipairs(luci.sys.net.devices()) do + if e ~= "lo" then + o:value(e) end - return trim(value or "") end +o:depends("type", "tun") +o.description = translate("Redirect traffic to this network interface") -local function validate_clash_source(self, value, section) - local clash_url = clash_source_formvalue(self.map, section, "clash_url") - local clash_path = clash_source_formvalue(self.map, section, "clash_path") - if clash_url == "" and clash_path == "" then - return nil, translate("Please specify either a Clash subscription URL or a local YAML path.") - end - return value +-- 新增一个选择框,用于选择 Shadowsocks 具体版本(仅当节点类型为 ss 或其具体子类型时显示) +o = s:option(ListValue, "_ss_core", string.format("%s", translatef("%s Node Use Version", "ShadowSocks"))) +o.description = translate("Selection ShadowSocks Node Use Version.") +if has_ss_rust then + o:value("ss-rust", translate("ShadowSocks-rust Version")) end - -o = s:option(Value, "clash_url", translate("Clash Subscription URL")) -o.placeholder = "https://example.com/config.yaml" +if has_ss_libev then + o:value("ss-libev", translate("ShadowSocks-libev Version")) +end +o.cfgvalue = function(self, section) + -- 读取当前节点的 type 值,如果已经是具体核心则显示对应的选项 + local node_type = self.map.uci:get("shadowsocksr", section, "type") + if node_type == "ss-rust" or node_type == "ss-libev" then + return node_type + end + -- 如果全局 ss_type 有值且为具体核心则返回该值 + local ss_type = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type") + if ss_type == "ss-rust" or ss_type == "ss-libev" then + return ss_type + end + -- 如果节点 type 是旧的 "ss",则返回空,手动选择 + return nil +end +-- 显示条件:当节点类型为 "ss" 或其具体核心时显示 +o:depends("type", "ss") o.rmempty = true -o:depends("type", "clash") -o.validate = validate_clash_source - -o = s:option(Value, "clash_path", translate("Clash YAML Path")) -o.placeholder = "/etc/ssrplus/clash/custom.yaml" -o.rmempty = true -o:depends("type", "clash") -o.validate = validate_clash_source - -o = s:option(Value, "clash_user_agent", translate("Clash User-Agent")) -o.default = "clash" -o.rmempty = false -o:depends("type", "clash") +-- 保存时,将选择的值直接写入当前节点的 type 字段 +o.write = function(self, section, value) + if value and value ~= "" then + self.map.uci:set("shadowsocksr", section, "type", value) + end +end o = s:option(ListValue, "v2ray_protocol", translate("V2Ray/XRay protocol")) o:value("vless", translate("VLESS")) @@ -422,54 +454,30 @@ o:value("http", translate("HTTP")) o:depends("type", "v2ray") o = s:option(Value, "server", translate("Server Address")) -o.datatype = "or(host,ip6addr)" +o.datatype = "host" o.rmempty = false o:depends("type", "ssr") o:depends("type", "ss") -o:depends("type", "ss-rust") +o:depends("type", "v2ray") o:depends("type", "trojan") o:depends("type", "naiveproxy") o:depends("type", "hysteria2") o:depends("type", "tuic") o:depends("type", "shadowtls") o:depends("type", "socks5") -local protocols = s.fields["v2ray_protocol"].keylist -if protocols and type(protocols) == "table" and #protocols > 0 then - for _, proto in ipairs(protocols) do - if not proto:find("^_") then - if proto == "hysteria2" then - o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false}) - else - o:depends({type = "v2ray", v2ray_protocol = proto}) - end - end - end -end o = s:option(Value, "server_port", translate("Server Port")) o.datatype = "port" o.rmempty = true o:depends("type", "ssr") o:depends("type", "ss") -o:depends("type", "ss-rust") +o:depends("type", "v2ray") o:depends("type", "trojan") o:depends("type", "naiveproxy") o:depends("type", "hysteria2") o:depends("type", "tuic") o:depends("type", "shadowtls") o:depends("type", "socks5") -local protocols = s.fields["v2ray_protocol"].keylist -if protocols and type(protocols) == "table" and #protocols > 0 then - for _, proto in ipairs(protocols) do - if not proto:find("^_") then - if proto == "hysteria2" then - o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false}) - else - o:depends({type = "v2ray", v2ray_protocol = proto}) - end - end - end -end o = s:option(Flag, "auth_enable", translate("Enable Authentication")) o.rmempty = false @@ -490,7 +498,6 @@ o.password = true o.rmempty = true o:depends("type", "ssr") o:depends("type", "ss") -o:depends("type", "ss-rust") o:depends("type", "trojan") o:depends("type", "naiveproxy") o:depends("type", "shadowtls") @@ -517,7 +524,6 @@ for _, v in ipairs(encrypt_methods_ss) do end end o.rmempty = true -o:depends("type", "ss-rust") o:depends("type", "ss") o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"}) @@ -536,30 +542,22 @@ o.default = "1" o = s:option(Flag, "enable_plugin", translate("Enable Plugin")) o.rmempty = true o:depends("type", "ss") -o:depends("type", "ss-rust") o.default = "0" -- Shadowsocks Plugin o = s:option(ListValue, "plugin", translate("Obfs")) o:value("none", translate("None")) -if has_mihomo or is_finded("obfs-local") then +if is_finded("obfs-local") then o:value("obfs-local", translate("obfs-local")) end -if has_mihomo or is_finded("v2ray-plugin") then +if is_finded("v2ray-plugin") then o:value("v2ray-plugin", translate("v2ray-plugin")) end -if has_mihomo then - o:value("gost-plugin", translate("gost-plugin")) -end if is_finded("xray-plugin") then o:value("xray-plugin", translate("xray-plugin")) end -if has_mihomo or is_finded("shadow-tls") then - o:value("shadow-tls", translate("Shadow-TLS")) -end -if has_mihomo then - o:value("restls", translate("restls")) - o:value("kcptun", translate("kcptun")) +if is_finded("shadow-tls") then + o:value("shadow-tls", translate("shadow-tls")) end o:value("custom", translate("Custom")) o.rmempty = true @@ -595,21 +593,6 @@ o:depends("type", "ssr") -- [[ Hysteria2 ]]-- -o = s:option(Flag, "hysteria2_realms", translate("Hysteria2 Realms")) -o.default = "0" -if xray_version_val > 260509 then - o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) -else - o:depends({type = "v2ray", v2ray_protocol = "__hide"}) -end - -o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") -o:depends("hysteria2_realms", true) - -o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) -o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } -o:depends("hysteria2_realms", true) - o = s:option(Value, "hy2_auth", translate("Users Authentication")) o:depends("type", "hysteria2") o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) @@ -618,7 +601,7 @@ o.rmempty = false o = s:option(Flag, "flag_port_hopping", translate("Enable Port Hopping")) o:depends("type", "hysteria2") -o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false}) +o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) o.rmempty = true o.default = "0" @@ -660,30 +643,17 @@ o.rmempty = true o.default = "0" o = s:option(Value, "obfs_type", translate("Obfuscation Type")) -o:value("", translate("Disable")) -o:value("salamander") -o:value("gecko") -o.rmempty = true o:depends({type = "hysteria2", flag_obfs = true}) o:depends({type = "v2ray", v2ray_protocol = "hysteria2", flag_obfs = true}) +o.rmempty = true +o.placeholder = "salamander" o = s:option(Value, "salamander", translate("Obfuscation Password")) +o:depends({type = "hysteria2", flag_obfs = true}) +o:depends({type = "v2ray", v2ray_protocol = "hysteria2", flag_obfs = true}) o.password = true o.rmempty = true -o:depends({type = "hysteria2", flag_obfs = true}) -local obfs = s.fields["obfs_type"].keylist -if obfs and type(obfs) == "table" and #obfs > 0 then - for _, v in ipairs(obfs) do - if v and v ~= "" then - o:depends({ - type = "v2ray", - v2ray_protocol = "hysteria2", - obfs_type = v, - flag_obfs = true - }) - end - end -end +o.placeholder = "cry_me_a_r1ver" o = s:option(Flag, "flag_quicparam", translate("Hysterir QUIC parameters")) o:depends("type", "hysteria2") @@ -778,13 +748,13 @@ o:depends("type", "shadowtls") if is_finded("sslocal") then o:value("sslocal", translate("ShadowSocks-rust Version")) end -if is_finded("xray") then +if is_finded("xray") or is_finded("v2ray") then o:value("vmess", translate("Vmess Protocol")) end o.default = "sslocal" o.rmempty = false -o = s:option(Value, "sslocal_password",translate("Shadowsocks Password")) +o = s:option(Value, "sslocal_password",translate("Shadowsocks password")) o:depends({type = "shadowtls", chain_type = "sslocal"}) o.rmempty = true @@ -818,7 +788,7 @@ o:depends("type", "tuic") --Tuic IP o = s:option(Value, "tuic_ip", translate("TUIC Server IP Address")) o.rmempty = true -o.datatype = "ipaddr" +o.datatype = "ip4addr" o.default = "" o:depends("type", "tuic") @@ -999,7 +969,7 @@ o = s:option(Value, "ws_heartbeatPeriod", translate("HeartbeatPeriod(second)")) o.datatype = "integer" o:depends("transport", "ws") -if is_finded("xray") then +if is_finded("v2ray") then -- WS前置数据 o = s:option(Value, "ws_ed", translate("Max Early Data")) o:depends("ws_ed_enable", true) @@ -1240,12 +1210,12 @@ o.default = "0" o.rmempty = true o = s:option(DynamicList, "local_addresses", translate("Local addresses")) ---o.datatype = "cidr" +o.datatype = "cidr" o:depends({type = "v2ray", v2ray_protocol = "wireguard"}) o.rmempty = true o = s:option(DynamicList, "reserved", translate("Reserved bytes(optional)")) -o.description = translate("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.rmempty = true @@ -1290,18 +1260,12 @@ o:depends("transport", "grpc") o = s:option(Flag, "enable_finalmask", translate("FinalMask")) o.rmempty = true o.default = "0" -local protocols = s.fields["v2ray_protocol"].keylist -if protocols and type(protocols) == "table" and #protocols > 0 then - for _, proto in ipairs(protocols) do - if not proto:find("^_") then - if proto == "hysteria2" then - o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false}) - elseif proto ~= "socks" and proto ~= "http" then - o:depends({type = "v2ray", v2ray_protocol = proto}) - end - end - end -end +o:depends({type = "v2ray", v2ray_protocol = "vless"}) +o:depends({type = "v2ray", v2ray_protocol = "vmess"}) +o:depends({type = "v2ray", v2ray_protocol = "trojan"}) +o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"}) +o:depends({type = "v2ray", v2ray_protocol = "wireguard"}) +o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) o = s:option(TextValue, "finalmask", " ") o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "
" .. @@ -1398,18 +1362,7 @@ if is_finded("xray") then o = s:option(Flag, "enable_ech", translate("Enable ECH(optional)")) o.rmempty = true o.default = "0" - local protocols = s.fields["v2ray_protocol"].keylist - if protocols and type(protocols) == "table" and #protocols > 0 then - for _, proto in ipairs(protocols) do - if not proto:find("^_") then - if proto == "hysteria2" then - o:depends({type = "v2ray", v2ray_protocol = "hysteria2", tls = true, hysteria2_realms = false}) - else - o:depends({type = "v2ray", v2ray_protocol = proto, tls = true}) - end - end - end - end + o:depends({type = "v2ray", tls = true}) o = s:option(TextValue, "ech_config", translate("ECH Config")) o.description = translate( @@ -1692,41 +1645,100 @@ o.rmempty = true o.default = "0" o:depends("type", "ssr") o:depends("type", "ss") -o:depends("type", "ss-rust") o:depends("type", "trojan") o:depends("type", "hysteria2") o:depends({type = "v2ray", v2ray_protocol = "vless", transport = "xhttp"}) o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) +o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve")) +o.description = translate( + "
    " .. + "
  • " .. translate("If the node address is a domain name, this DNS will be used for resolution.") .. "
  • " .. + "
  • " .. string.format('%s', translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead.")) .. "
  • " .. + "
" +) +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( + "
    " .. + "
  • " .. translate("If is domain name, The requested domain name will be resolved to IP before connect.") .. "
  • " .. + "
  • " .. string.format('%s', translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead.")) .. "
  • " .. + "
" +) +o.default = "" +o:value("", translate("Auto")) +o:value("UseIPv4v6", translate("Prefer IPv4")) +o:value("UseIPv6v4", translate("Prefer IPv6")) +o:value("UseIPv4", translate("IPv4 Only")) +o:value("UseIPv6", translate("IPv6 Only")) +o:depends("type", "v2ray") + +local v2ray_protocols = s.fields["v2ray_protocol"] +if #v2ray_protocols > 0 then + for i, v in ipairs(v2ray_protocols) do + if not v:find("^_") then + s.fields["server"]:depends({ ["v2ray_protocol"] = v }) + s.fields["server_port"]:depends({ ["v2ray_protocol"] = v }) + s.fields["domain_resolver"]:depends({ ["v2ray_protocol"] = v }) + s.fields["domain_strategy"]:depends({ ["v2ray_protocol"] = v }) + + if v ~= "hysteria2" then + s.fields["fast_open"]:depends({ ["v2ray_protocol"] = v }) + s.fields["mptcp"]:depends({ ["v2ray_protocol"] = v }) + end + end + end +end + o = s:option(Flag, "switch_enable", translate("Enable Auto Switch")) o.rmempty = false o.default = "1" +o = s:option(Value, "local_port", translate("Local Port")) +o.datatype = "port" +o.default = 1234 +o.rmempty = false + if is_finded("kcptun-client") then o = s:option(Flag, "kcp_enable", translate("KcpTun Enable")) o.rmempty = true o.default = "0" o:depends("type", "ssr") - o:depends("type", "ss-rust") o:depends("type", "ss") o = s:option(Value, "kcp_port", translate("KcpTun Port")) o.datatype = "portrange" o.default = 4000 o:depends("type", "ssr") - o:depends("type", "ss-rust") o:depends("type", "ss") o = s:option(Value, "kcp_password", translate("KcpTun Password")) o.password = true o:depends("type", "ssr") - o:depends("type", "ss-rust") o:depends("type", "ss") o = s:option(Value, "kcp_param", translate("KcpTun Param")) o.default = "--nocomp" o:depends("type", "ssr") - o:depends("type", "ss-rust") o:depends("type", "ss") end diff --git a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua index 1fd8e775..7978c57b 100644 --- a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua +++ b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua @@ -4,7 +4,6 @@ local m, s, sec, o local uci = require "luci.model.uci".cursor() -local URL = require "url" -- 获取 LAN IP 地址 function lanip() @@ -32,62 +31,69 @@ end local lan_ip = lanip() local validation = require "luci.cbi.datatypes" -local clash_nodes = {} local function is_finded(e) - return luci.sys.exec(string.format('type -t -p "%s" -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 - -m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("

Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/CLASH etc.

")) +m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("

Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/TUN etc.

")) m:section(SimpleSection).template = "shadowsocksr/status" local server_table = {} -local server_order = {} uci:foreach("shadowsocksr", "servers", function(s) - if s.type == "clash" then - clash_nodes[s[".name"]] = true - end - - 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} - 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} - elseif s.type ~= "tun" then - local display_name = clash_display_name(s) - if display_name then - server_table[s[".name"]] = display_name - end - end - if s.type ~= "tun" and server_table[s[".name"]] then - table.insert(server_order, s[".name"]) end end) +local key_table = {} +for key, _ in pairs(server_table) do + table.insert(key_table, key) +end + +table.sort(key_table) + -- [[ Global Setting ]]-- s = m:section(TypedSection, "global") s.anonymous = true o = s:option(ListValue, "global_server", translate("Main Server")) o:value("nil", translate("Disable")) -for _, key in ipairs(server_order) do +for _, key in pairs(key_table) do o:value(key, server_table[key]) end o.default = "nil" o.rmempty = false -o = s:option(DummyValue, "_clash_panel", translate("Clash Panel")) -o.template = "shadowsocksr/clash_main_panel" -o.clash_nodes = clash_nodes +o = s:option(ListValue, "udp_relay_server", translate("Game Mode UDP Server")) +o:value("", translate("Disable")) +o:value("same", translate("Same as Global Server")) +for _, key in pairs(key_table) do + o:value(key, server_table[key]) +end + +if uci:get_first("shadowsocksr", 'global', 'netflix_enable', '0') == '1' then + o = s:option(ListValue, "netflix_server", translate("Netflix Node")) + o:value("nil", translate("Disable")) + o:value("same", translate("Same as Global Server")) + for _, key in pairs(key_table) do + o:value(key, server_table[key]) + end + o.default = "nil" + o.rmempty = false + + o = s:option(Flag, "netflix_proxy", translate("External Proxy Mode")) + o.rmempty = false + o.description = translate("Forward Netflix Proxy through Main Proxy") + o.default = "0" +end + +-- [[ Use nftables/iptables ]]-- +o = s:option(ListValue, "prefer_nft", translate("Prefer firewall tools")) +o.default = "1" +o:value("0", "Iptables") +o:value("1", "Nftables") o = s:option(ListValue, "threads", translate("Multi Threads Option")) 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("router", translate("IP Route Mode")) o:value("all", translate("Global Mode")) +o:value("oversea", translate("Oversea Mode")) o.default = gfw o = s:option(ListValue, "dports", translate("Proxy Ports")) @@ -121,13 +128,21 @@ o = s:option(ListValue, "pdnsd_enable", translate("Resolve Dns Mode")) if is_finded("dns2tcp") then o:value("1", translate("Use DNS2TCP query")) end +if is_finded("dns2socks") then + o:value("2", translate("Use DNS2SOCKS query and cache")) +end +if is_finded("dns2socks-rust") then + o:value("3", translate("Use DNS2SOCKS-RUST query and cache")) +end if is_finded("mosdns") then - o:value("4", translate("Use MosDNS query")) + 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 if is_finded("chinadns-ng") then o:value("6", translate("Use ChinaDNS-NG query and cache")) end -o:value("7", translate("Prefer module built-in DNS")) o:value("0", translate("Use Local DNS Service listen port 5335")) o.default = 1 @@ -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.4:53", translate("Level 3 Public DNS (4.2.2.4)")) o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)")) +o:value("114.114.114.114:53", translate("Oversea Mode DNS-1 (114.114.114.114)")) +o:value("114.114.115.115:53", translate("Oversea Mode DNS-2 (114.114.115.115)")) o:depends("pdnsd_enable", "1") -o:depends("pdnsd_enable", "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.datatype = "ip4addrport" 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:depends("pdnsd_enable", "4") o.description = translate("Custom DNS Server format as tcp://IP:PORT or tls://DOMAIN:PORT (tcp://8.8.8.8 or tls://dns.google:853)") -o.default = "tcp://8.8.4.4:53,tcp://8.8.8.8:53" -o = s:option(Flag, "filter_aaaa", translate("Disable IPv6 for Overseas FQDN")) -o:depends("pdnsd_enable", "1") +o = s:option(Flag, "mosdns_ipv6", translate("Disable IPv6 in MOSDNS query mode")) o:depends("pdnsd_enable", "4") -o:depends("pdnsd_enable", "7") o.rmempty = false o.default = "1" +if is_finded("dnsproxy") then + o = s:option(ListValue, "parse_method", translate("Select DNS parse Mode")) + o.description = translate( + "" + ) + 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( + "
    " .. + "
  • " .. translate("Defines the upstreams logic mode, possible values: load_balance, parallel, fastest_addr (default: load_balance).") .. "
  • " .. + "
  • " .. translate("When two or more DNS servers are deployed, enable this function.") .. "
  • " .. + "
" + ) + o:value("load_balance", translate("load_balance")) + o:value("parallel", translate("parallel")) + o:value("fastest_addr", translate("fastest_addr")) + o:depends("parse_method", "parse_file") + o.rmempty = true + o.default = "load_balance" + + o = s:option(Flag, "dnsproxy_ipv6", translate("Disable IPv6 query mode")) + o.description = translate("When disabled, all AAAA requests are not resolved.") + o:depends("parse_method", "single_dns") + o:depends("parse_method", "parse_file") + o.rmempty = false + o.default = "1" +end + if is_finded("chinadns-ng") then o = s:option(Value, "chinadns_ng_tunnel_forward", translate("Anti-pollution DNS Server")) o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)")) @@ -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("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)")) o:depends("pdnsd_enable", "6") - o.default = "8.8.4.4:53" o.description = translate( "