From 79114c968b2658c9b34d976bd4b43f65a588a58d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Jun 2026 02:25:48 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Sync=202026-06-03=2002:25:48?= 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 | 1051 +++--- .../luasrc/view/shadowsocksr/server_table.htm | 304 ++ .../servers_subscribe_url_style.htm | 166 + .../luasrc/view/shadowsocksr/socket.htm | 25 +- .../luasrc/view/shadowsocksr/ssrurl.htm | 783 ++--- .../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 | 2083 ++++++++---- .../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 0 -> 220503 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 - xray-core/Makefile | 2 +- xray-core/patches/AllowInsecure.patch | 16 + 63 files changed, 15671 insertions(+), 6792 deletions(-) create mode 100644 dns2tcp/patches/0001-add-A-option-to-filter-AAAA-answers.patch delete mode 100644 luci-app-ssr-plus/htdocs/luci-static/resources/view/shadowsocksr/Sortable.min.js create mode 100644 luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/component.lua create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/advanced_switch_compact.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_yaml_upload.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/client_dns_defaults.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/control_layout.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/server_table.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/servers_subscribe_url_style.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_enabled_autosave.htm create mode 100644 luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_schedule_compact.htm create mode 100644 luci-app-ssr-plus/root/etc/hotplug.d/iface/99-ssrplus-pppoe delete mode 100644 luci-app-ssr-plus/root/etc/ssrplus/netflix.list delete mode 100644 luci-app-ssr-plus/root/etc/ssrplus/oversea_list.conf create mode 100644 luci-app-ssr-plus/root/usr/share/shadowsocksr/Country.mmdb create mode 100644 luci-app-ssr-plus/root/usr/share/shadowsocksr/clash_yaml.lua delete mode 100755 luci-app-ssr-plus/root/usr/share/shadowsocksr/genred2config.sh delete mode 100755 luci-app-ssr-plus/root/usr/share/shadowsocksr/hy2_test.sh create mode 100755 luci-app-ssr-plus/root/usr/share/shadowsocksr/update_components.sh delete mode 100755 mihomo/.prepare.sh delete mode 100644 trojan/Makefile delete mode 100644 trojan/boost-version.mk delete mode 100644 trojan/patches/001-force-openssl-version.patch delete mode 100644 trojan/patches/002-Fix-boost1.89-build.patch create mode 100644 xray-core/patches/AllowInsecure.patch diff --git a/dns2tcp/Makefile b/dns2tcp/Makefile index c19e1ebcc..6926858f3 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:=1 +PKG_RELEASE:=2 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 new file mode 100644 index 000000000..a11ba1003 --- /dev/null +++ b/dns2tcp/patches/0001-add-A-option-to-filter-AAAA-answers.patch @@ -0,0 +1,210 @@ +diff --git a/dns2tcp.c b/dns2tcp.c +index b289598..542f839 100644 +--- a/dns2tcp.c ++++ b/dns2tcp.c +@@ -35,6 +35,10 @@ + #define PORTSTRLEN 6 /* "65535" (include \0) */ + + #define DNS_MSGSZ 1472 /* mtu:1500 - iphdr:20 - udphdr:8 */ ++#define DNS_HDRLEN 12 ++ ++#define DNS_TYPE_A 1 ++#define DNS_TYPE_AAAA 28 + + /* ======================== helper ======================== */ + +@@ -128,6 +132,14 @@ typedef struct { + union skaddr srcaddr; + } ctx_t; + ++typedef struct { ++ size_t start; ++ size_t end; ++ uint16_t type; ++ uint8_t section; ++ bool drop; ++} dns_rr_t; ++ + /* ======================== global-vars ======================== */ + + enum { +@@ -135,6 +147,7 @@ enum { + FLAG_REUSE_PORT = 1 << 1, /* udp listen */ + FLAG_VERBOSE = 1 << 2, /* logging */ + FLAG_LOCAL_ADDR = 1 << 3, /* tcp local addr */ ++ FLAG_FILTER_AAAA = 1 << 4, /* strip all AAAA records from dns response */ + }; + + #define has_flag(flag) (g_flags & (flag)) +@@ -166,6 +179,126 @@ static void tcp_connect_cb(evloop_t *evloop, evio_t *watcher, int events); + static void tcp_sendmsg_cb(evloop_t *evloop, evio_t *watcher, int events); + static void tcp_recvmsg_cb(evloop_t *evloop, evio_t *watcher, int events); + ++static uint16_t read_be16(const void *ptr) { ++ const uint8_t *bytes = ptr; ++ return ((uint16_t)bytes[0] << 8) | bytes[1]; ++} ++ ++static void write_be16(void *ptr, uint16_t value) { ++ uint8_t *bytes = ptr; ++ bytes[0] = value >> 8; ++ bytes[1] = value & 0xff; ++} ++ ++static bool dns_skip_name(const uint8_t *msg, size_t msglen, size_t offset, size_t *next_offset) { ++ while (offset < msglen) { ++ uint8_t len = msg[offset]; ++ if (len == 0) { ++ *next_offset = offset + 1; ++ return true; ++ } ++ if ((len & 0xc0) == 0xc0) { ++ if (offset + 1 >= msglen) ++ return false; ++ *next_offset = offset + 2; ++ return true; ++ } ++ if ((len & 0xc0) || offset + 1 + len > msglen) ++ return false; ++ offset += 1 + len; ++ } ++ ++ return false; ++} ++ ++static size_t filter_aaaa_records(uint8_t *msg, size_t msglen) { ++ if (!has_flag(FLAG_FILTER_AAAA) || msglen < DNS_HDRLEN) ++ return msglen; ++ ++ uint16_t qdcount = read_be16(msg + 4); ++ uint16_t counts[] = { ++ read_be16(msg + 6), ++ read_be16(msg + 8), ++ read_be16(msg + 10), ++ }; ++ uint32_t rrcount = counts[0] + counts[1] + counts[2]; ++ ++ if (rrcount == 0 || qdcount > msglen / 5 || rrcount > msglen / 11) ++ return msglen; ++ ++ size_t offset = DNS_HDRLEN; ++ for (uint16_t i = 0; i < qdcount; i++) { ++ if (!dns_skip_name(msg, msglen, offset, &offset) || offset + 4 > msglen) ++ return msglen; ++ offset += 4; ++ } ++ ++ size_t rr_start = offset; ++ dns_rr_t *rrs = calloc(rrcount, sizeof(*rrs)); ++ if (!rrs) ++ return msglen; ++ ++ uint32_t rridx = 0; ++ for (uint8_t section = 0; section < 3; section++) { ++ for (uint16_t i = 0; i < counts[section]; i++) { ++ dns_rr_t *rr = &rrs[rridx++]; ++ rr->start = offset; ++ rr->section = section; ++ ++ if (!dns_skip_name(msg, msglen, offset, &offset) || offset + 10 > msglen) ++ goto out; ++ ++ rr->type = read_be16(msg + offset); ++ uint16_t rdlen = read_be16(msg + offset + 8); ++ rr->end = offset + 10 + rdlen; ++ if (rr->end > msglen) ++ goto out; ++ ++ offset = rr->end; ++ } ++ } ++ ++ size_t rest_start = offset; ++ bool changed = false; ++ uint16_t keep_counts[] = { counts[0], counts[1], counts[2] }; ++ ++ for (uint32_t i = 0; i < rrcount; i++) { ++ if (rrs[i].type == DNS_TYPE_AAAA) { ++ rrs[i].drop = true; ++ keep_counts[rrs[i].section]--; ++ changed = true; ++ } ++ } ++ ++ if (changed) { ++ size_t write_offset = rr_start; ++ ++ for (uint32_t i = 0; i < rrcount; i++) { ++ if (rrs[i].drop) ++ continue; ++ ++ size_t rrlen = rrs[i].end - rrs[i].start; ++ if (write_offset != rrs[i].start) ++ memmove(msg + write_offset, msg + rrs[i].start, rrlen); ++ write_offset += rrlen; ++ } ++ ++ if (write_offset != rest_start) ++ memmove(msg + write_offset, msg + rest_start, msglen - rest_start); ++ ++ msglen = write_offset + (msglen - rest_start); ++ write_be16(msg + 6, keep_counts[0]); ++ write_be16(msg + 8, keep_counts[1]); ++ write_be16(msg + 10, keep_counts[2]); ++ log_verbose("filter AAAA records, answer:%hu -> %hu, authority:%hu -> %hu, additional:%hu -> %hu", ++ counts[0], keep_counts[0], counts[1], keep_counts[1], counts[2], keep_counts[2]); ++ } ++ ++out: ++ free(rrs); ++ return msglen; ++} ++ + static void print_help(void) { + printf("usage: dns2tcp <-L listen> <-R remote> [options...]\n" + " -L 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 54a9ccf1e..3ee704b49 100644 --- a/luci-app-ssr-plus/Makefile +++ b/luci-app-ssr-plus/Makefile @@ -3,77 +3,51 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=luci-app-ssr-plus LUCI_PKGARCH:=all PKG_NAME:=luci-app-ssr-plus -PKG_VERSION:=190 -PKG_RELEASE:=15 +PKG_VERSION:=193 +PKG_RELEASE:=16 PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS_RUST \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNS2TCP \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNSPROXY \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy \ + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Redsocks2 \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Trojan + CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server -LUCI_TITLE:=SS/SSR/V2Ray/Trojan/NaiveProxy/Tuic/ShadowTLS/Hysteria/Socks5/Tun LuCI interface +LUCI_TITLE:=SS/SSR/V2Ray/Trojan/NaiveProxy/Tuic/ShadowTLS/Hysteria/Socks5/Clash LuCI interface LUCI_PKGARCH:=all LUCI_DEPENDS:= \ - +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \ - +jq +ip-full +lua +lua-neturl +libuci-lua +microsocks \ - +tcping +resolveip +shadowsocksr-libev-ssr-check +curl +nping \ - +PACKAGE_$(PKG_NAME)_INCLUDE_V2ray:curl \ - +PACKAGE_$(PKG_NAME)_INCLUDE_V2ray:v2ray-core \ - +PACKAGE_$(PKG_NAME)_INCLUDE_Xray:curl \ - +PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \ + +libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \ + +jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \ + +resolveip +curl +nping +unzip +xz-utils \ +PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG:chinadns-ng \ - +PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS:dns2socks \ - +PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS_RUST:dns2socks-rust \ - +PACKAGE_$(PKG_NAME)_INCLUDE_DNSPROXY:dnsproxy \ +PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS:mosdns \ - +PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria:hysteria \ - +PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client:tuic-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS:shadow-tls \ - +PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks:ipt2socks \ +PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun:kcptun-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy:naiveproxy \ - +PACKAGE_$(PKG_NAME)_INCLUDE_Redsocks2:redsocks2 \ - +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client:shadowsocks-libev-ss-local \ - +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client:shadowsocks-libev-ss-redir \ - +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server:shadowsocks-libev-ss-server \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client:shadowsocks-rust-sslocal \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server:shadowsocks-rust-ssserver \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs:simple-obfs-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin:v2ray-plugin \ +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-local \ +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-redir \ - +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server:shadowsocksr-libev-ssr-server \ - +PACKAGE_$(PKG_NAME)_INCLUDE_Trojan:trojan + +PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server:shadowsocksr-libev-ssr-server define Package/$(PKG_NAME)/config -select PACKAGE_luci-lua-runtime if PACKAGE_$(PKG_NAME) choice prompt "Transparent Proxy Backend" @@ -103,37 +77,24 @@ endchoice choice prompt "Shadowsocks Client Selection" - default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client if aarch64 || x86_64 - default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client + default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client bool "None" - config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client - bool "Shadowsocks-libev" - config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client - bool "Shadowsocks-rust" - depends on aarch64||arm||i386||mips||mipsel||x86_64 - depends on !(TARGET_x86_geode||TARGET_x86_legacy) + bool "Shadowsocks Rust" endchoice choice prompt "Shadowsocks Server Selection" - default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server if aarch64 - default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server if i386||x86_64||arm default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server bool "None" - config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server - bool "Shadowsocks-libev" - config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server - bool "Shadowsocks-rust" - depends on aarch64||arm||i386||mips||mipsel||x86_64 - depends on !(TARGET_x86_geode||TARGET_x86_legacy) + bool "Shadowsocks Rust" endchoice choice @@ -144,45 +105,35 @@ choice config PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY bool "None" - config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray - bool "V2ray-core" - config PACKAGE_$(PKG_NAME)_INCLUDE_Xray bool "Xray-core" + select PACKAGE_xray-core endchoice config PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG bool "Include ChinaDNS-NG" - default n - -config PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS - bool "Include DNS2socks" - default y - -config PACKAGE_$(PKG_NAME)_INCLUDE_DNS2SOCKS_RUST - bool "Include DNS2socks-Rust" - default n - -config PACKAGE_$(PKG_NAME)_INCLUDE_DNSPROXY - bool "Include DNSproxy" - default n + default y if aarch64||arm||i386||x86_64 config PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS bool "Include MosDNS" - default y if aarch64||i386||x86_64 - -config PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria - bool "Include Hysteria" - select PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG default n -config PACKAGE_$(PKG_NAME)_INCLUDE_Tuic_Client - bool "Include Tuic-Client" - select PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG - select PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks - depends on aarch64||arm||i386||x86_64 - depends on !(TARGET_x86_geode||TARGET_x86_legacy) - default n +config PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy + bool "Include HTTP(S) Proxy Server" + select PACKAGE_3proxy + default y if (aarch64||arm||i386||loongarch64||riscv64||x86_64) + +config PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo + bool "Include Mihomo (Clash Support)" + select PACKAGE_mihomo + depends on aarch64||arm||i386||loongarch64||riscv64||x86_64 + default y if aarch64||arm||i386||loongarch64||riscv64||x86_64 + +config PACKAGE_$(PKG_NAME)_INCLUDE_GeoData + bool "Include GeoData (GeoIP and GeoSite)" + select PACKAGE_v2ray-geoip + select PACKAGE_v2ray-geosite + default y if i386||x86_64 config PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS bool "Include Shadow-TLS" @@ -192,10 +143,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS depends on !(TARGET_x86_geode||TARGET_x86_legacy) default n -config PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks - bool "Include IPT2Socks" - default n - config PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun bool "Include Kcptun" default n @@ -205,13 +152,9 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy depends on !(arc||armeb||mips||mips64||powerpc||TARGET_gemini) default n -config PACKAGE_$(PKG_NAME)_INCLUDE_Redsocks2 - bool "Include Redsocks2" - default n - config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs bool "Include Shadowsocks Simple Obfs Plugin" - default y + default n config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin bool "Include Shadowsocks V2ray Plugin" @@ -223,11 +166,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server bool "Include ShadowsocksR Libev Server" - default y if i386||x86_64||arm - -config PACKAGE_$(PKG_NAME)_INCLUDE_Trojan - bool "Include Trojan" - select PACKAGE_$(PKG_NAME)_INCLUDE_IPT2Socks default n endef @@ -240,4 +178,41 @@ endef include $(TOPDIR)/feeds/luci/luci.mk +define Package/$(PKG_NAME)/install + ifneq ($(wildcard ${CURDIR}/luasrc),) + $(INSTALL_DIR) $(1)$(LUCI_LIBRARYDIR) + cp -pR $(PKG_BUILD_DIR)/luasrc/* $(1)$(LUCI_LIBRARYDIR)/ + $(FIND) $(1)$(LUCI_LIBRARYDIR)/ -type f -name '*.luadoc' | $(XARGS) rm + $(if $(CONFIG_LUCI_SRCDIET),$(call SrcDiet,$(1)$(LUCI_LIBRARYDIR)/),true) + $(call SubstituteVersion,$(1)$(LUCI_LIBRARYDIR)/) + endif + ifneq ($(wildcard ${CURDIR}/ucode),) + $(INSTALL_DIR) $(1)$(UCODE_LIBRARYDIR) + cp -pR $(PKG_BUILD_DIR)/ucode/* $(1)$(UCODE_LIBRARYDIR)/ + $(call SubstituteVersion,$(1)$(UCODE_LIBRARYDIR)/) + endif + ifneq ($(wildcard ${CURDIR}/htdocs),) + $(INSTALL_DIR) $(1)$(HTDOCS) + cp -pR $(PKG_BUILD_DIR)/htdocs/* $(1)$(HTDOCS)/ + $(if $(CONFIG_LUCI_JSMIN),$(call JsMin,$(1)$(HTDOCS)/),true) + $(if $(CONFIG_LUCI_CSSTIDY),$(call CssTidy,$(1)$(HTDOCS)/),true) + endif + ifneq ($(wildcard ${CURDIR}/root),) + $(INSTALL_DIR) $(1)/ + cp -pR $(PKG_BUILD_DIR)/root/* $(1)/ + endif + ifneq ($(wildcard ${CURDIR}/src),) + $(call Build/Install/Default) + $(CP) $(PKG_INSTALL_DIR)/* $(1)/ + endif + if [ -d "$(1)/usr/bin" ]; then \ + chmod 0755 "$(1)/usr/bin/ssr-monitor" 2>/dev/null || true; \ + chmod 0755 "$(1)/usr/bin/ssr-rules" 2>/dev/null || true; \ + chmod 0755 "$(1)/usr/bin/ssr-switch" 2>/dev/null || true; \ + fi + if [ -f "$(1)/etc/hotplug.d/iface/99-ssrplus-pppoe" ]; then \ + chmod 0755 "$(1)/etc/hotplug.d/iface/99-ssrplus-pppoe" 2>/dev/null || true; \ + fi +endef + # call BuildPackage - OpenWrt buildroot signature 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 deleted file mode 100644 index 8148b9dda..000000000 --- a/luci-app-ssr-plus/htdocs/luci-static/resources/view/shadowsocksr/Sortable.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! Sortable 1.15.7 - MIT | git://github.com/SortableJS/Sortable.git */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n"===e[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 +local function parse_nonnegative_int(value) + local number = tonumber(value) + if not number then + return nil + end + number = math.floor(number) + if number < 0 then + return nil + end + return number end -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) +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 else - sh_uci_commit(config) + 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 end end end + + return rules end -local function url(...) - local url = string.format("admin/services/%s", "shadowsocksr") - local args = { ... } - for i, v in ipairs(args) do - if v and v ~= "" then - url = url .. "/" .. v +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 + } end end - return require "luci.dispatcher".build_url(url) + 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("'", "'\\''") .. "'" end function index() @@ -54,31 +623,47 @@ 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", {autoapply = true}), cbi("shadowsocksr/client-config")), _("Servers Nodes"), 20).leaf = true + entry({"admin", "services", "shadowsocksr", "servers"}, arcombine(cbi("shadowsocksr/servers"), 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")) - --[[ 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情况 + 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 --[[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) @@ -137,34 +722,639 @@ function check_ip() end function subscribe() - luci.sys.call("/usr/bin/lua /usr/share/shadowsocksr/subscribe.lua >>/var/log/ssrplus.log") + 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.http.prepare_content("application/json") - luci.http.write_json({ret = 1}) + 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) end function act_status() local e = {} - e.running = luci.sys.call("busybox ps -w | grep ssr-retcp | grep -v grep >/dev/null") == 0 + e.running = global_client_running() + if e.running then + e.status_text = get_running_status_text() + end 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 = luci.sys.call("command -v nft >/dev/null") == 0 + local use_nft = use_fw4_backend() local iret = false if domain then if use_nft then @@ -173,46 +1363,52 @@ function act_ping() iret = luci.sys.call("ipset add ss_spec_wan_ac " .. domain .. " 2>/dev/null") == 0 end end - -- Hysteria2 节点检测 + -- Hysteria2 节点轻量 UDP 端口检测 if proto:find("hysteria2") or type:find("hysteria2") 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 + 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 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 (host and host ~= "") then + if not is_ip or probe_host ~= domain then local resolve_arg = "" - local final_domain = domain - if is_ip and host and host ~= "" then + local final_domain = probe_host + if is_ip and probe_host and probe_host ~= "" then -- IP 模式下使用 --resolve 强制指定 SNI,解决 TLS 握手失败 - resolve_arg = string.format("--resolve '%s:%d:%s' ", host, port, domain) - final_domain = host + resolve_arg = string.format("--resolve '%s:%d:%s' ", probe_host, port, domain) 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, address + resolve_arg, + (probe_host and probe_host ~= "") and ("-H " .. luci.util.shellquote("Host: " .. probe_host) .. " ") or "", + address ) result = luci.sys.exec(cmd) or "" success = (string.match(result, "http_code=(%d+)") == "101") @@ -229,13 +1425,17 @@ 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 - -- 解析延迟 (优先用 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 + -- 延迟:优先 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 end else -- 3. 非 WebSocket 节点的探测逻辑 (TCP / ICMP / UDP) @@ -247,12 +1447,19 @@ function act_ping() socket:close() 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 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 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 = tonumber(luci.sys.exec(icmp_cmd)) + e.ping = normalize_ping_ms(tonumber(luci.sys.exec(icmp_cmd))) end if not e.ping then @@ -260,7 +1467,23 @@ 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 = math.floor(ping_num) end + 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 end end end @@ -274,13 +1497,26 @@ 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 = {} - e.ret = luci.sys.call("/usr/bin/ssr-check www." .. luci.http.formvalue("set") .. ".com 80 3 1") + 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") luci.http.prepare_content("application/json") luci.http.write_json(e) end @@ -297,9 +1533,13 @@ function check_port() local s local server_name = "" local uci = require "luci.model.uci".cursor() - local use_nft = luci.sys.call("command -v nft >/dev/null") == 0 + local use_nft = use_fw4_backend() 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 @@ -307,15 +1547,18 @@ function check_port() end -- 临时加入 set + local is_ipv6 = is_ipv6_address(s.server) local iret = false - 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 + 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 end -- TCP 测试 - local socket = nixio.socket("inet", "stream") + local socket = nixio.socket(is_ipv6 and "inet6" or "inet", "stream") socket:setopt("socket", "rcvtimeo", 3) socket:setopt("socket", "sndtimeo", 3) local ret = socket:connect(s.server, s.server_port) @@ -347,109 +1590,15 @@ 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() - 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.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 2e5d38aff..a1023748b 100644 --- a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua +++ b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua @@ -1,6 +1,7 @@ local m, s, o local cbi = require "luci.cbi" local uci = require "luci.model.uci".cursor() +local URL = require "url" -- 获取 LAN IP 地址 function lanip() @@ -30,16 +31,32 @@ local lan_ip = lanip() local server_table = {} local type_table = {} local function is_finded(e) - return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= "" + return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= "" +end + +local function clash_display_name(s) + if s.type ~= "clash" or not s.clash_url or s.clash_url == "" then + return nil + end + local ok, parsed = pcall(URL.parse, s.clash_url) + if ok and parsed and parsed.host then + return "[CLASH]:" .. parsed.host + end + return "[CLASH]" end uci:foreach("shadowsocksr", "servers", function(s) - if s.alias then + if s.type ~= "tun" and s.alias then server_table[s[".name"]] = "[%s]:%s" % {string.upper(s.v2ray_protocol or s.type), s.alias} - elseif s.server and s.server_port then + elseif s.type ~= "tun" and s.server and s.server_port then server_table[s[".name"]] = "[%s]:%s:%s" % {string.upper(s.v2ray_protocol or s.type), s.server, s.server_port} + elseif s.type ~= "tun" then + local display_name = clash_display_name(s) + if display_name then + server_table[s[".name"]] = display_name + end end - if s.type then + if s.type and s.type ~= "tun" then type_table[s[".name"]] = s.type end end) @@ -79,6 +96,13 @@ o.datatype = "uinteger" o:depends("enable_switch", "1") o.default = 3 +s:append(cbi.Template("shadowsocksr/advanced_switch_compact")) + +o = s:option(Value, "default_node_local_port", translate("Default Node Local Port")) +o.datatype = "port" +o.default = 1234 +o.rmempty = false + o = s:option(Value, "gfwlist_url", translate("gfwlist Update url")) o:value("https://fastly.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt", translate("v2fly/domain-list-community")) o:value("https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt", translate("Loyalsoldier/v2ray-rules-dat")) @@ -92,145 +116,6 @@ o:value("https://ispip.clang.cn/all_cn_cidr.txt", translate("Clang.CN.CIDR")) o:value("https://fastly.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china.txt", translate("china-operator-ip")) o.default = "https://ispip.clang.cn/all_cn.txt" -o = s:option(Flag, "netflix_enable", translate("Enable Netflix Mode")) -o.description = translate("When disabled shunt mode, will same time stopped shunt service.") -o.rmempty = false - -o = s:option(Value, "nfip_url", translate("nfip_url")) -o:value("https://fastly.jsdelivr.net/gh/QiuSimons/Netflix_IP/NF_only.txt", translate("Netflix IP Only")) -o:value("https://fastly.jsdelivr.net/gh/QiuSimons/Netflix_IP/getflix.txt", translate("Netflix and AWS")) -o.default = "https://fastly.jsdelivr.net/gh/QiuSimons/Netflix_IP/NF_only.txt" -o.description = translate("Customize Netflix IP Url") -o:depends("netflix_enable", "1") - -o = s:option(ListValue, "shunt_dns_mode", translate("DNS Query Mode For Shunt Mode")) -if is_finded("dns2socks") then - o:value("1", translate("Use DNS2SOCKS query and cache")) -end -if is_finded("dns2socks-rust") then - o:value("2", translate("Use DNS2SOCKS-RUST query and cache")) -end -if is_finded("mosdns") then - o:value("3", translate("Use MosDNS query")) -end -if is_finded("dnsproxy") then - o:value("4", translate("Use DNSPROXY query and cache")) -end -if is_finded("chinadns-ng") then - o:value("5", translate("Use ChinaDNS-NG query and cache")) -end -o:depends("netflix_enable", "1") -o.default = 1 - -o = s:option(Value, "shunt_dnsserver", translate("Anti-pollution DNS Server For Shunt Mode")) -o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)")) -o:value("8.8.8.8:53", translate("Google Public DNS (8.8.8.8)")) -o:value("208.67.222.222:53", translate("OpenDNS (208.67.222.222)")) -o:value("208.67.220.220:53", translate("OpenDNS (208.67.220.220)")) -o:value("209.244.0.3:53", translate("Level 3 Public DNS (209.244.0.3)")) -o:value("209.244.0.4:53", translate("Level 3 Public DNS (209.244.0.4)")) -o:value("4.2.2.1:53", translate("Level 3 Public DNS (4.2.2.1)")) -o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)")) -o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)")) -o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)")) -o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)")) -o:depends("shunt_dns_mode", "1") -o:depends("shunt_dns_mode", "2") -o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") -o.datatype = "ip4addrport" - -o = s:option(Value, "shunt_mosdns_dnsserver", translate("Anti-pollution DNS Server")) -o:value("tcp://8.8.4.4:53,tcp://8.8.8.8:53", translate("Google Public DNS")) -o:value("tcp://208.67.222.222:53,tcp://208.67.220.220:53", translate("OpenDNS")) -o:value("tcp://209.244.0.3:53,tcp://209.244.0.4:53", translate("Level 3 Public DNS-1 (209.244.0.3-4)")) -o:value("tcp://4.2.2.1:53,tcp://4.2.2.2:53", translate("Level 3 Public DNS-2 (4.2.2.1-2)")) -o:value("tcp://4.2.2.3:53,tcp://4.2.2.4:53", translate("Level 3 Public DNS-3 (4.2.2.3-4)")) -o:value("tcp://1.1.1.1:53,tcp://1.0.0.1:53", translate("Cloudflare DNS")) -o:depends("shunt_dns_mode", "3") -o.description = translate("Custom DNS Server format as tcp://IP:PORT or tls://DOMAIN:PORT (tcp://8.8.8.8 or tls://dns.google:853)") - -o = s:option(Flag, "shunt_mosdns_ipv6", translate("Disable IPv6 In MosDNS Query Mode (Shunt Mode)")) -o:depends("shunt_dns_mode", "3") -o.rmempty = false -o.default = "0" - -if is_finded("dnsproxy") then - o = s:option(ListValue, "shunt_parse_method", translate("Select DNS parse Mode")) - o.description = translate( - "" - ) - 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" @@ -266,45 +151,13 @@ end -- [[ SOCKS5 Proxy ]]-- s = m:section(TypedSection, "socks5_proxy", translate("Global SOCKS5 Proxy Server")) s.anonymous = true +-- s.description = translate("Only Same as Global Server is supported here. If the current main program supports a built-in SOCKS5 listener, it will be enabled in-process; otherwise SSR Plus will fall back to microsocks.") -- Enable/Disable Option o = s:option(Flag, "enabled", translate("Enable")) o.default = 0 o.rmempty = false --- Server Selection -o = s:option(ListValue, "server", translate("Server")) -o:value("same", translate("Same as Global Server")) -for _, key in pairs(key_table) do - o:value(key, server_table[key]) -end -o.default = "same" -o.rmempty = false - --- Dynamic value handling based on enabled/disabled state -o.cfgvalue = function(self, section) - local enabled = m:get(section, "enabled") - if enabled == "0" then - return m:get(section, "old_server") - end - return Value.cfgvalue(self, section)-- Default to `same` when enabled -end - -o.write = function(self, section, value) - local enabled = m:get(section, "enabled") - if enabled == "0" then - local old_server = Value.cfgvalue(self, section) - if old_server ~= "nil" then - m:set(section, "old_server", old_server) - end - m:set(section, "server", "nil") - else - m:del(section, "old_server") - -- Write the value normally when enabled - Value.write(self, section, value) - end -end - -- Socks Auth if is_finded("xray") then o = s:option(ListValue, "socks5_auth", translate("Socks5 Auth Mode"), translate("Socks protocol auth methods, default:noauth.")) @@ -312,13 +165,6 @@ o.default = "noauth" o:value("noauth", "NOAUTH") o:value("password", "PASSWORD") o.rmempty = true -for key, server_type in pairs(type_table) do - if server_type == "v2ray" then - -- 如果服务器类型是 v2ray,则设置依赖项显示 - o:depends("server", key) - end -end -o:depends({server = "same", disable = true}) -- Socks User o = s:option(Value, "socks5_user", translate("Socks5 User"), translate("Only when Socks5 Auth Mode is password valid, Mandatory.")) @@ -335,13 +181,6 @@ o:depends("socks5_auth", "password") o = s:option(Flag, "socks5_mixed", translate("Enabled Mixed"), translate("Mixed as an alias of socks, default:Enabled.")) o.default = "1" o.rmempty = false -for key, server_type in pairs(type_table) do - if server_type == "v2ray" then - -- 如果服务器类型是 v2ray,则设置依赖项显示 - o:depends("server", key) - end -end -o:depends({server = "same", disable = true}) end -- Local Port @@ -350,6 +189,37 @@ o.datatype = "port" o.default = 1080 o.rmempty = false +if is_finded("3proxy") then + -- [[ HTTP/HTTPS Proxy ]]-- + s = m:section(TypedSection, "http_proxy", translate("Global HTTP/HTTPS Proxy Server")) + s.anonymous = true + -- s.description = translate("Only Same as Global Server is supported here. HTTP/HTTPS proxy service is provided through 3proxy.") + + o = s:option(Flag, "enabled", translate("Enable")) + o.default = 0 + o.rmempty = false + + o = s:option(ListValue, "http_auth", translate("HTTP Auth Mode"), translate("HTTP proxy auth method, default:none.")) + o.default = "none" + o:value("none", "NONE") + o:value("password", "PASSWORD") + o.rmempty = false + + o = s:option(Value, "http_user", translate("HTTP User"), translate("Only when HTTP Auth Mode is password valid, Mandatory.")) + o.rmempty = true + o:depends("http_auth", "password") + + o = s:option(Value, "http_pass", translate("HTTP Password"), translate("Only when HTTP Auth Mode is password valid, Not mandatory.")) + o.password = true + o.rmempty = true + o:depends("http_auth", "password") + + o = s:option(Value, "local_port", translate("Local Port")) + o.datatype = "port" + o.default = 3128 + o.rmempty = false +end + -- [[ fragmen Settings ]]-- if is_finded("xray") then s = m:section(TypedSection, "global_xray_fragment", translate("Xray Fragment Settings")) 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 567989ba0..d06abbdc1 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" 2>/dev/null', e)) ~= "" + return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= "" end local function is_installed(e) @@ -142,16 +142,14 @@ local function set_apply_on_parse(map) local old = map.on_after_save map.on_after_save = function(self) if old then old(self) end - map:set("@global[0]", "timestamp", os.time()) + --map:set("@global[0]", "timestamp", os.time()) end end end local has_ss_rust = is_finded("sslocal") or is_finded("ssserver") -local has_ss_libev = is_finded("ss-redir") or is_finded("ss-local") -local has_trojan = is_finded("trojan") +local has_mihomo = is_finded("mihomo") local has_xray = is_finded("xray") -local has_hysteria2 = is_finded("hysteria") local server_table = {} local encrypt_methods = { @@ -256,6 +254,56 @@ local tls_flows = { "none" } +local function migrate_xray_protocol_nodes() + local changed = false + + uci:foreach("shadowsocksr", "servers", function(section) + local sid = section[".name"] + local stype = section.type + local proto = section.v2ray_protocol + local escaped_sid = luci.util.shellquote(sid) + + if stype == "ss" or stype == "ss-libev" then + if has_mihomo then + luci.sys.call(string.format("uci set shadowsocksr.%s.type='ss'", escaped_sid)) + changed = true + elseif has_ss_rust then + luci.sys.call(string.format("uci set shadowsocksr.%s.type='ss-rust'", escaped_sid)) + changed = true + elseif has_xray then + luci.sys.call(string.format("uci set shadowsocksr.%s.type='v2ray' && " .. "uci set shadowsocksr.%s.v2ray_protocol='shadowsocks'", escaped_sid, escaped_sid)) + changed = true + end + elseif stype == "v2ray" and proto == "shadowsocks" and has_mihomo then + luci.sys.call(string.format("uci set shadowsocksr.%s.type='ss' && " .. "uci delete shadowsocksr.%s.v2ray_protocol", escaped_sid, escaped_sid)) + changed = true + end + if stype == "hysteria2" then + luci.sys.call(string.format("uci set shadowsocksr.%s.type='v2ray' && " .. "uci set shadowsocksr.%s.v2ray_protocol='hysteria2'", escaped_sid, escaped_sid)) + changed = true + elseif stype == "trojan" then + luci.sys.call(string.format("uci set shadowsocksr.%s.type='v2ray' && " .. "uci set shadowsocksr.%s.v2ray_protocol='trojan'", escaped_sid, escaped_sid)) + changed = true + end + end) + local subscribe_sid = uci:get_first("shadowsocksr", "server_subscribe") + if subscribe_sid then + local old_options = {"xray_hy2_type", "xray_tj_type", "ss_type"} + local escaped_sub_sid = luci.util.shellquote(subscribe_sid) + for _, opt in ipairs(old_options) do + if uci:get("shadowsocksr", subscribe_sid, opt) then + luci.sys.call(string.format("uci delete shadowsocksr.%s.%s", escaped_sub_sid, opt)) + changed = true + end + end + end + if changed then + luci.sys.call("uci commit shadowsocksr") + end +end + +migrate_xray_protocol_nodes() + m = Map("shadowsocksr", translate("Edit ShadowSocksR Server")) m.redirect = url("servers") if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then @@ -264,6 +312,14 @@ if not sid or m.uci:get("shadowsocksr", sid) ~= "servers" then end -- 保存&应用成功后跳转到节点列表 set_apply_on_parse(m) +local old_after_save = m.on_after_save +m.on_after_save = function(self) + if old_after_save then old_after_save(self) end + local node_type = self.uci:get("shadowsocksr", sid, "type") + if node_type == "clash" then + luci.sys.call(string.format("/etc/init.d/shadowsocksr clash_cache %s >/dev/null 2>&1 &", sid)) + end +end -- [[ Servers Setting ]]-- s = m:section(NamedSection, sid, "servers") @@ -275,88 +331,27 @@ o.rawhtml = true o.template = "shadowsocksr/ssrurl" o.value = sid --- 新增一个选择框,用于选择 Xray 或 Hysteria2 核心 -o = s:option(ListValue, "_xray_hy2_type", string.format("%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") or is_finded("v2ray") then +if is_finded("xray") then o:value("v2ray", translate("V2Ray/XRay")) end if is_finded("ssr-redir") then o:value("ssr", translate("ShadowsocksR")) end -if has_ss_rust or has_ss_libev then - o:value("ss", translate("ShadowSocks")) +if has_mihomo then + o:value("ss", translate("ShadowSocks")) end -if is_finded("trojan") then - o:value("trojan", translate("Trojan")) +if has_ss_rust then + o:value("ss-rust", translate("ShadowSocks")) end if is_finded("naive") then o:value("naiveproxy", translate("NaiveProxy")) end -if is_finded("hysteria") then - o:value("hysteria2", translate("Hysteria2")) +if is_finded("mihomo") then + o:value("clash", translate("Clash/Mihomo")) end -if is_finded("tuic-client") then +if is_finded("mihomo") then o:value("tuic", translate("TUIC")) end if is_finded("shadow-tls") and is_finded("sslocal") then @@ -365,78 +360,51 @@ end if is_finded("ipt2socks") then o:value("socks5", translate("Socks5")) end -if is_finded("redsocks2") then - o:value("tun", translate("Network Tunnel")) -end local old_cfgvalue = o.cfgvalue o.cfgvalue = function(self, section) - local val = self.map.uci:get("shadowsocksr", section, "type") - if val == "ss-rust" or val == "ss-libev" then - return "ss" - end - if old_cfgvalue then + local val = self.map.uci:get("shadowsocksr", section, "type") + if old_cfgvalue then return old_cfgvalue(self, section) - end - return val + end + return val end --- 重写 write,当用户选择 "ss" 时不写入(由 _ss_core 负责写入具体核心) -local old_write = o.write -o.write = function(self, section, value) - if value == "ss" then - return -- 不做任何写入,等待 _ss_core 写入 - end - if old_write then - old_write(self, section, value) - else - self.map.uci:set("shadowsocksr", section, "type", value) - end -end - o.description = translate("Using incorrect encryption mothod may causes service fail to start") o = s:option(Value, "alias", translate("Alias(optional)")) -o = s:option(ListValue, "iface", translate("Network interface to use")) -for _, e in ipairs(luci.sys.net.devices()) do - if e ~= "lo" then - o:value(e) +local function clash_source_formvalue(map, section, option) + local value = map:formvalue("cbid." .. map.config .. "." .. section .. "." .. option) + if value == nil then + value = map.uci:get(map.config, section, option) end + return trim(value or "") end -o:depends("type", "tun") -o.description = translate("Redirect traffic to this network interface") --- 新增一个选择框,用于选择 Shadowsocks 具体版本(仅当节点类型为 ss 或其具体子类型时显示) -o = s:option(ListValue, "_ss_core", string.format("%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")) +local function validate_clash_source(self, value, section) + local clash_url = clash_source_formvalue(self.map, section, "clash_url") + local clash_path = clash_source_formvalue(self.map, section, "clash_path") + if clash_url == "" and clash_path == "" then + return nil, translate("Please specify either a Clash subscription URL or a local YAML path.") + end + return value end -if has_ss_libev then - o:value("ss-libev", translate("ShadowSocks-libev Version")) -end -o.cfgvalue = function(self, section) - -- 读取当前节点的 type 值,如果已经是具体核心则显示对应的选项 - local node_type = self.map.uci:get("shadowsocksr", section, "type") - if node_type == "ss-rust" or node_type == "ss-libev" then - return node_type - end - -- 如果全局 ss_type 有值且为具体核心则返回该值 - local ss_type = self.map.uci:get("shadowsocksr", "@server_subscribe[0]", "ss_type") - if ss_type == "ss-rust" or ss_type == "ss-libev" then - return ss_type - end - -- 如果节点 type 是旧的 "ss",则返回空,手动选择 - return nil -end --- 显示条件:当节点类型为 "ss" 或其具体核心时显示 -o:depends("type", "ss") + +o = s:option(Value, "clash_url", translate("Clash Subscription URL")) +o.placeholder = "https://example.com/config.yaml" o.rmempty = true --- 保存时,将选择的值直接写入当前节点的 type 字段 -o.write = function(self, section, value) - if value and value ~= "" then - self.map.uci:set("shadowsocksr", section, "type", value) - end -end +o:depends("type", "clash") +o.validate = validate_clash_source + +o = s:option(Value, "clash_path", translate("Clash YAML Path")) +o.placeholder = "/etc/ssrplus/clash/custom.yaml" +o.rmempty = true +o:depends("type", "clash") +o.validate = validate_clash_source + +o = s:option(Value, "clash_user_agent", translate("Clash User-Agent")) +o.default = "clash" +o.rmempty = false +o:depends("type", "clash") o = s:option(ListValue, "v2ray_protocol", translate("V2Ray/XRay protocol")) o:value("vless", translate("VLESS")) @@ -454,30 +422,54 @@ o:value("http", translate("HTTP")) o:depends("type", "v2ray") o = s:option(Value, "server", translate("Server Address")) -o.datatype = "host" +o.datatype = "or(host,ip6addr)" o.rmempty = false o:depends("type", "ssr") o:depends("type", "ss") -o:depends("type", "v2ray") +o:depends("type", "ss-rust") o:depends("type", "trojan") o:depends("type", "naiveproxy") o:depends("type", "hysteria2") o:depends("type", "tuic") o:depends("type", "shadowtls") o:depends("type", "socks5") +local protocols = s.fields["v2ray_protocol"].keylist +if protocols and type(protocols) == "table" and #protocols > 0 then + for _, proto in ipairs(protocols) do + if not proto:find("^_") then + if proto == "hysteria2" then + o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false}) + else + o:depends({type = "v2ray", v2ray_protocol = proto}) + end + end + end +end o = s:option(Value, "server_port", translate("Server Port")) o.datatype = "port" o.rmempty = true o:depends("type", "ssr") o:depends("type", "ss") -o:depends("type", "v2ray") +o:depends("type", "ss-rust") o:depends("type", "trojan") o:depends("type", "naiveproxy") o:depends("type", "hysteria2") o:depends("type", "tuic") o:depends("type", "shadowtls") o:depends("type", "socks5") +local protocols = s.fields["v2ray_protocol"].keylist +if protocols and type(protocols) == "table" and #protocols > 0 then + for _, proto in ipairs(protocols) do + if not proto:find("^_") then + if proto == "hysteria2" then + o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false}) + else + o:depends({type = "v2ray", v2ray_protocol = proto}) + end + end + end +end o = s:option(Flag, "auth_enable", translate("Enable Authentication")) o.rmempty = false @@ -498,6 +490,7 @@ o.password = true o.rmempty = true o:depends("type", "ssr") o:depends("type", "ss") +o:depends("type", "ss-rust") o:depends("type", "trojan") o:depends("type", "naiveproxy") o:depends("type", "shadowtls") @@ -524,6 +517,7 @@ for _, v in ipairs(encrypt_methods_ss) do end end o.rmempty = true +o:depends("type", "ss-rust") o:depends("type", "ss") o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"}) @@ -542,22 +536,30 @@ o.default = "1" o = s:option(Flag, "enable_plugin", translate("Enable Plugin")) o.rmempty = true o:depends("type", "ss") +o:depends("type", "ss-rust") o.default = "0" -- Shadowsocks Plugin o = s:option(ListValue, "plugin", translate("Obfs")) o:value("none", translate("None")) -if is_finded("obfs-local") then +if has_mihomo or is_finded("obfs-local") then o:value("obfs-local", translate("obfs-local")) end -if is_finded("v2ray-plugin") then +if has_mihomo or is_finded("v2ray-plugin") then o:value("v2ray-plugin", translate("v2ray-plugin")) end +if has_mihomo then + o:value("gost-plugin", translate("gost-plugin")) +end if is_finded("xray-plugin") then o:value("xray-plugin", translate("xray-plugin")) end -if is_finded("shadow-tls") then - o:value("shadow-tls", translate("shadow-tls")) +if has_mihomo or is_finded("shadow-tls") then + o:value("shadow-tls", translate("Shadow-TLS")) +end +if has_mihomo then + o:value("restls", translate("restls")) + o:value("kcptun", translate("kcptun")) end o:value("custom", translate("Custom")) o.rmempty = true @@ -593,6 +595,21 @@ o:depends("type", "ssr") -- [[ Hysteria2 ]]-- +o = s:option(Flag, "hysteria2_realms", translate("Hysteria2 Realms")) +o.default = "0" +if xray_version_val > 260509 then + o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) +else + o:depends({type = "v2ray", v2ray_protocol = "__hide"}) +end + +o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") +o:depends("hysteria2_realms", true) + +o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) +o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } +o:depends("hysteria2_realms", true) + o = s:option(Value, "hy2_auth", translate("Users Authentication")) o:depends("type", "hysteria2") o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) @@ -601,7 +618,7 @@ o.rmempty = false o = s:option(Flag, "flag_port_hopping", translate("Enable Port Hopping")) o:depends("type", "hysteria2") -o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) +o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false}) o.rmempty = true o.default = "0" @@ -643,17 +660,30 @@ o.rmempty = true o.default = "0" o = s:option(Value, "obfs_type", translate("Obfuscation Type")) +o:value("", translate("Disable")) +o:value("salamander") +o:value("gecko") +o.rmempty = true o:depends({type = "hysteria2", flag_obfs = true}) o:depends({type = "v2ray", v2ray_protocol = "hysteria2", flag_obfs = true}) -o.rmempty = true -o.placeholder = "salamander" o = s:option(Value, "salamander", translate("Obfuscation Password")) -o:depends({type = "hysteria2", flag_obfs = true}) -o:depends({type = "v2ray", v2ray_protocol = "hysteria2", flag_obfs = true}) o.password = true o.rmempty = true -o.placeholder = "cry_me_a_r1ver" +o:depends({type = "hysteria2", flag_obfs = true}) +local obfs = s.fields["obfs_type"].keylist +if obfs and type(obfs) == "table" and #obfs > 0 then + for _, v in ipairs(obfs) do + if v and v ~= "" then + o:depends({ + type = "v2ray", + v2ray_protocol = "hysteria2", + obfs_type = v, + flag_obfs = true + }) + end + end +end o = s:option(Flag, "flag_quicparam", translate("Hysterir QUIC parameters")) o:depends("type", "hysteria2") @@ -748,13 +778,13 @@ o:depends("type", "shadowtls") if is_finded("sslocal") then o:value("sslocal", translate("ShadowSocks-rust Version")) end -if is_finded("xray") or is_finded("v2ray") then +if is_finded("xray") then o:value("vmess", translate("Vmess Protocol")) end o.default = "sslocal" o.rmempty = false -o = s:option(Value, "sslocal_password",translate("Shadowsocks password")) +o = s:option(Value, "sslocal_password",translate("Shadowsocks Password")) o:depends({type = "shadowtls", chain_type = "sslocal"}) o.rmempty = true @@ -788,7 +818,7 @@ o:depends("type", "tuic") --Tuic IP o = s:option(Value, "tuic_ip", translate("TUIC Server IP Address")) o.rmempty = true -o.datatype = "ip4addr" +o.datatype = "ipaddr" o.default = "" o:depends("type", "tuic") @@ -969,7 +999,7 @@ o = s:option(Value, "ws_heartbeatPeriod", translate("HeartbeatPeriod(second)")) o.datatype = "integer" o:depends("transport", "ws") -if is_finded("v2ray") then +if is_finded("xray") then -- WS前置数据 o = s:option(Value, "ws_ed", translate("Max Early Data")) o:depends("ws_ed_enable", true) @@ -1210,12 +1240,12 @@ o.default = "0" o.rmempty = true o = s:option(DynamicList, "local_addresses", translate("Local addresses")) -o.datatype = "cidr" +--o.datatype = "cidr" o:depends({type = "v2ray", v2ray_protocol = "wireguard"}) o.rmempty = true o = s:option(DynamicList, "reserved", translate("Reserved bytes(optional)")) -o.description = translate("Decimal numbers separated by \",\" or Base64-encoded strings.") +o.description = translate("Supports decimal numbers separated by \",\" or Base64-encoded strings, with a maximum length of 3 bytes.") o:depends({type = "v2ray", v2ray_protocol = "wireguard"}) o.rmempty = true @@ -1260,12 +1290,18 @@ o:depends("transport", "grpc") o = s:option(Flag, "enable_finalmask", translate("FinalMask")) o.rmempty = true o.default = "0" -o:depends({type = "v2ray", v2ray_protocol = "vless"}) -o:depends({type = "v2ray", v2ray_protocol = "vmess"}) -o:depends({type = "v2ray", v2ray_protocol = "trojan"}) -o:depends({type = "v2ray", v2ray_protocol = "shadowsocks"}) -o:depends({type = "v2ray", v2ray_protocol = "wireguard"}) -o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) +local protocols = s.fields["v2ray_protocol"].keylist +if protocols and type(protocols) == "table" and #protocols > 0 then + for _, proto in ipairs(protocols) do + if not proto:find("^_") then + if proto == "hysteria2" then + o:depends({type = "v2ray", v2ray_protocol = "hysteria2", hysteria2_realms = false}) + elseif proto ~= "socks" and proto ~= "http" then + o:depends({type = "v2ray", v2ray_protocol = proto}) + end + end + end +end o = s:option(TextValue, "finalmask", " ") o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "
" .. @@ -1362,7 +1398,18 @@ if is_finded("xray") then o = s:option(Flag, "enable_ech", translate("Enable ECH(optional)")) o.rmempty = true o.default = "0" - o:depends({type = "v2ray", tls = true}) + local protocols = s.fields["v2ray_protocol"].keylist + if protocols and type(protocols) == "table" and #protocols > 0 then + for _, proto in ipairs(protocols) do + if not proto:find("^_") then + if proto == "hysteria2" then + o:depends({type = "v2ray", v2ray_protocol = "hysteria2", tls = true, hysteria2_realms = false}) + else + o:depends({type = "v2ray", v2ray_protocol = proto, tls = true}) + end + end + end + end o = s:option(TextValue, "ech_config", translate("ECH Config")) o.description = translate( @@ -1645,100 +1692,41 @@ o.rmempty = true o.default = "0" o:depends("type", "ssr") o:depends("type", "ss") +o:depends("type", "ss-rust") o:depends("type", "trojan") o:depends("type", "hysteria2") o:depends({type = "v2ray", v2ray_protocol = "vless", transport = "xhttp"}) o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) -o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve")) -o.description = translate( - "
    " .. - "
  • " .. 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 7978c57b0..1fd8e775b 100644 --- a/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua +++ b/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua @@ -4,6 +4,7 @@ local m, s, sec, o local uci = require "luci.model.uci".cursor() +local URL = require "url" -- 获取 LAN IP 地址 function lanip() @@ -31,69 +32,62 @@ end local lan_ip = lanip() local validation = require "luci.cbi.datatypes" +local clash_nodes = {} local function is_finded(e) - return luci.sys.exec(string.format('type -t -p "%s" 2>/dev/null', e)) ~= "" + return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= "" end -m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("

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

")) +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:section(SimpleSection).template = "shadowsocksr/status" local server_table = {} +local server_order = {} uci:foreach("shadowsocksr", "servers", function(s) - if s.alias then + if s.type == "clash" then + clash_nodes[s[".name"]] = true + end + + if s.type ~= "tun" and s.alias then server_table[s[".name"]] = "[%s]:%s" % {string.upper(s.v2ray_protocol or s.type), s.alias} - elseif s.server and s.server_port then + elseif s.type ~= "tun" and s.server and s.server_port then server_table[s[".name"]] = "[%s]:%s:%s" % {string.upper(s.v2ray_protocol or s.type), s.server, s.server_port} + elseif s.type ~= "tun" then + local display_name = clash_display_name(s) + if display_name then + server_table[s[".name"]] = display_name + end + end + if s.type ~= "tun" and server_table[s[".name"]] then + table.insert(server_order, s[".name"]) end end) -local key_table = {} -for key, _ in pairs(server_table) do - table.insert(key_table, key) -end - -table.sort(key_table) - -- [[ Global Setting ]]-- s = m:section(TypedSection, "global") s.anonymous = true o = s:option(ListValue, "global_server", translate("Main Server")) o:value("nil", translate("Disable")) -for _, key in pairs(key_table) do +for _, key in ipairs(server_order) do o:value(key, server_table[key]) end o.default = "nil" o.rmempty = false -o = s:option(ListValue, "udp_relay_server", translate("Game Mode UDP Server")) -o:value("", translate("Disable")) -o:value("same", translate("Same as Global Server")) -for _, key in pairs(key_table) do - o:value(key, server_table[key]) -end - -if uci:get_first("shadowsocksr", 'global', 'netflix_enable', '0') == '1' then - o = s:option(ListValue, "netflix_server", translate("Netflix Node")) - o:value("nil", translate("Disable")) - o:value("same", translate("Same as Global Server")) - for _, key in pairs(key_table) do - o:value(key, server_table[key]) - end - o.default = "nil" - o.rmempty = false - - o = s:option(Flag, "netflix_proxy", translate("External Proxy Mode")) - o.rmempty = false - o.description = translate("Forward Netflix Proxy through Main Proxy") - o.default = "0" -end - --- [[ Use nftables/iptables ]]-- -o = s:option(ListValue, "prefer_nft", translate("Prefer firewall tools")) -o.default = "1" -o:value("0", "Iptables") -o:value("1", "Nftables") +o = s:option(DummyValue, "_clash_panel", translate("Clash Panel")) +o.template = "shadowsocksr/clash_main_panel" +o.clash_nodes = clash_nodes o = s:option(ListValue, "threads", translate("Multi Threads Option")) o:value("0", translate("Auto Threads")) @@ -112,7 +106,6 @@ o = s:option(ListValue, "run_mode", translate("Running Mode")) o:value("gfw", translate("GFW List Mode")) o:value("router", translate("IP Route Mode")) o:value("all", translate("Global Mode")) -o:value("oversea", translate("Oversea Mode")) o.default = gfw o = s:option(ListValue, "dports", translate("Proxy Ports")) @@ -128,21 +121,13 @@ o = s:option(ListValue, "pdnsd_enable", translate("Resolve Dns Mode")) if is_finded("dns2tcp") then o:value("1", translate("Use DNS2TCP query")) end -if is_finded("dns2socks") then - o:value("2", translate("Use DNS2SOCKS query and cache")) -end -if is_finded("dns2socks-rust") then - o:value("3", translate("Use DNS2SOCKS-RUST query and cache")) -end if is_finded("mosdns") then - o:value("4", translate("Use MOSDNS query (Not Support Oversea Mode)")) -end -if is_finded("dnsproxy") then - o:value("5", translate("Use DNSPROXY query and cache")) + o:value("4", translate("Use MosDNS query")) end if is_finded("chinadns-ng") then o:value("6", translate("Use ChinaDNS-NG query and cache")) end +o:value("7", translate("Prefer module built-in DNS")) o:value("0", translate("Use Local DNS Service listen port 5335")) o.default = 1 @@ -158,11 +143,8 @@ o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)")) o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)")) o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)")) o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)")) -o:value("114.114.114.114:53", translate("Oversea Mode DNS-1 (114.114.114.114)")) -o:value("114.114.115.115:53", translate("Oversea Mode DNS-2 (114.114.115.115)")) o:depends("pdnsd_enable", "1") -o:depends("pdnsd_enable", "2") -o:depends("pdnsd_enable", "3") +o:depends("pdnsd_enable", "7") o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") o.datatype = "ip4addrport" o.default = "8.8.4.4:53" @@ -176,61 +158,15 @@ o:value("tcp://4.2.2.3:53,tcp://4.2.2.4:53", translate("Level 3 Public DNS-3 (4. o:value("tcp://1.1.1.1:53,tcp://1.0.0.1:53", translate("Cloudflare DNS")) o:depends("pdnsd_enable", "4") o.description = translate("Custom DNS Server format as tcp://IP:PORT or tls://DOMAIN:PORT (tcp://8.8.8.8 or tls://dns.google:853)") +o.default = "tcp://8.8.4.4:53,tcp://8.8.8.8:53" -o = s:option(Flag, "mosdns_ipv6", translate("Disable IPv6 in MOSDNS query mode")) +o = s:option(Flag, "filter_aaaa", translate("Disable IPv6 for Overseas FQDN")) +o:depends("pdnsd_enable", "1") o:depends("pdnsd_enable", "4") +o:depends("pdnsd_enable", "7") o.rmempty = false o.default = "1" -if is_finded("dnsproxy") then - o = s:option(ListValue, "parse_method", translate("Select DNS parse Mode")) - o.description = translate( - "" - ) - 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)")) @@ -245,6 +181,7 @@ if is_finded("chinadns-ng") then o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)")) o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)")) o:depends("pdnsd_enable", "6") + o.default = "8.8.4.4:53" o.description = translate( "