From 2b1d3e73475ed160e6f58ffd8d08c01c9acb5ca7 Mon Sep 17 00:00:00 2001 From: Alireza Ahmadi Date: Fri, 20 Feb 2026 00:03:16 +0100 Subject: [PATCH 01/36] [feat] restart xray-core from cli #3825 --- main.go | 8 +++++- web/web.go | 4 +++ x-ui.rc | 5 ++++ x-ui.service.arch | 1 + x-ui.service.debian | 1 + x-ui.service.rhel | 1 + x-ui.sh | 68 ++++++++++++++++++++++++++++----------------- 7 files changed, 62 insertions(+), 26 deletions(-) diff --git a/main.go b/main.go index 8096616c..bc17aad6 100644 --- a/main.go +++ b/main.go @@ -70,7 +70,7 @@ func runWebServer() { sigCh := make(chan os.Signal, 1) // Trap shutdown signals - signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM) + signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGUSR1) for { sig := <-sigCh @@ -108,6 +108,12 @@ func runWebServer() { return } log.Println("Sub server restarted successfully.") + case syscall.SIGUSR1: + logger.Info("Received USR1 signal, restarting xray-core...") + err := server.RestartXray() + if err != nil { + logger.Error("Failed to restart xray-core:", err) + } default: // --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown --- diff --git a/web/web.go b/web/web.go index 300572a3..71845a14 100644 --- a/web/web.go +++ b/web/web.go @@ -490,3 +490,7 @@ func (s *Server) GetCron() *cron.Cron { func (s *Server) GetWSHub() any { return s.wsHub } + +func (s *Server) RestartXray() error { + return s.xrayService.RestartXray(true) +} diff --git a/x-ui.rc b/x-ui.rc index 1323d76a..cfb54211 100644 --- a/x-ui.rc +++ b/x-ui.rc @@ -10,4 +10,9 @@ depend() { } start_pre(){ cd /usr/local/x-ui +} +reload() { + ebegin "Reloading ${RC_SVCNAME}" + kill -USR1 $pidfile + eend $? } \ No newline at end of file diff --git a/x-ui.service.arch b/x-ui.service.arch index b6e01141..9060fbf6 100644 --- a/x-ui.service.arch +++ b/x-ui.service.arch @@ -9,6 +9,7 @@ Environment="XRAY_VMESS_AEAD_FORCED=false" Type=simple WorkingDirectory=/usr/lib/x-ui/ ExecStart=/usr/lib/x-ui/x-ui +ExecReload=kill -USR1 $MAINPID Restart=on-failure RestartSec=5s diff --git a/x-ui.service.debian b/x-ui.service.debian index 037f88bb..4902c5fe 100644 --- a/x-ui.service.debian +++ b/x-ui.service.debian @@ -9,6 +9,7 @@ Environment="XRAY_VMESS_AEAD_FORCED=false" Type=simple WorkingDirectory=/usr/local/x-ui/ ExecStart=/usr/local/x-ui/x-ui +ExecReload=kill -USR1 $MAINPID Restart=on-failure RestartSec=5s diff --git a/x-ui.service.rhel b/x-ui.service.rhel index 30652d52..696a9c12 100644 --- a/x-ui.service.rhel +++ b/x-ui.service.rhel @@ -9,6 +9,7 @@ Environment="XRAY_VMESS_AEAD_FORCED=false" Type=simple WorkingDirectory=/usr/local/x-ui/ ExecStart=/usr/local/x-ui/x-ui +ExecReload=kill -USR1 $MAINPID Restart=on-failure RestartSec=5s diff --git a/x-ui.sh b/x-ui.sh index 2bd125ab..933a964e 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -408,6 +408,16 @@ restart() { fi } +restart_xray() { + systemctl reload x-ui + LOGI "xray-core Restart signal sent successfully, Please check the log information to confirm whether xray restarted successfully" + sleep 2 + show_xray_status + if [[ $# == 0 ]]; then + before_show_menu + fi +} + status() { if [[ $release == "alpine" ]]; then rc-service x-ui status @@ -2154,6 +2164,7 @@ show_usage() { │ ${blue}x-ui start${plain} - Start │ │ ${blue}x-ui stop${plain} - Stop │ │ ${blue}x-ui restart${plain} - Restart │ +| ${blue}x-ui restart-xray${plain} - Restart Xray │ │ ${blue}x-ui status${plain} - Current Status │ │ ${blue}x-ui settings${plain} - Current Settings │ │ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │ @@ -2189,25 +2200,26 @@ show_menu() { │ ${green}11.${plain} Start │ │ ${green}12.${plain} Stop │ │ ${green}13.${plain} Restart │ -│ ${green}14.${plain} Check Status │ -│ ${green}15.${plain} Logs Management │ +| ${green}14.${plain} Restart Xray │ +│ ${green}15.${plain} Check Status │ +│ ${green}16.${plain} Logs Management │ │────────────────────────────────────────────────│ -│ ${green}16.${plain} Enable Autostart │ -│ ${green}17.${plain} Disable Autostart │ +│ ${green}17.${plain} Enable Autostart │ +│ ${green}18.${plain} Disable Autostart │ │────────────────────────────────────────────────│ -│ ${green}18.${plain} SSL Certificate Management │ -│ ${green}19.${plain} Cloudflare SSL Certificate │ -│ ${green}20.${plain} IP Limit Management │ -│ ${green}21.${plain} Firewall Management │ -│ ${green}22.${plain} SSH Port Forwarding Management │ +│ ${green}19.${plain} SSL Certificate Management │ +│ ${green}20.${plain} Cloudflare SSL Certificate │ +│ ${green}21.${plain} IP Limit Management │ +│ ${green}22.${plain} Firewall Management │ +│ ${green}23.${plain} SSH Port Forwarding Management │ │────────────────────────────────────────────────│ -│ ${green}23.${plain} Enable BBR │ -│ ${green}24.${plain} Update Geo Files │ -│ ${green}25.${plain} Speedtest by Ookla │ +│ ${green}24.${plain} Enable BBR │ +│ ${green}25.${plain} Update Geo Files │ +│ ${green}26.${plain} Speedtest by Ookla │ ╚────────────────────────────────────────────────╝ " show_status - echo && read -rp "Please enter your selection [0-25]: " num + echo && read -rp "Please enter your selection [0-26]: " num case "${num}" in 0) @@ -2253,43 +2265,46 @@ show_menu() { check_install && restart ;; 14) - check_install && status + check_install && restart_xray ;; 15) - check_install && show_log + check_install && status ;; 16) - check_install && enable + check_install && show_log ;; 17) - check_install && disable + check_install && enable ;; 18) - ssl_cert_issue_main + check_install && disable ;; 19) - ssl_cert_issue_CF + ssl_cert_issue_main ;; 20) - iplimit_main + ssl_cert_issue_CF ;; 21) - firewall_menu + iplimit_main ;; 22) - SSH_port_forwarding + firewall_menu ;; 23) - bbr_menu + SSH_port_forwarding ;; 24) - update_geo + bbr_menu ;; 25) + update_geo + ;; + 26) run_speedtest ;; *) - LOGE "Please enter the correct number [0-25]" + LOGE "Please enter the correct number [0-26]" ;; esac } @@ -2305,6 +2320,9 @@ if [[ $# > 0 ]]; then "restart") check_install 0 && restart 0 ;; + "restart-xray") + check_install 0 && restart_xray 0 + ;; "status") check_install 0 && status 0 ;; From 3ec5b3589f3358878738b0bfc52784ee924fbcdd Mon Sep 17 00:00:00 2001 From: Alireza Ahmadi Date: Fri, 20 Feb 2026 02:07:46 +0100 Subject: [PATCH 02/36] fix windows build --- main.go | 5 +++-- util/sys/sys_darwin.go | 3 +++ util/sys/sys_linux.go | 3 +++ util/sys/sys_windows.go | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index bc17aad6..f8d3357b 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/sub" "github.com/mhsanaei/3x-ui/v2/util/crypto" + "github.com/mhsanaei/3x-ui/v2/util/sys" "github.com/mhsanaei/3x-ui/v2/web" "github.com/mhsanaei/3x-ui/v2/web/global" "github.com/mhsanaei/3x-ui/v2/web/service" @@ -70,7 +71,7 @@ func runWebServer() { sigCh := make(chan os.Signal, 1) // Trap shutdown signals - signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGUSR1) + signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, sys.SIGUSR1) for { sig := <-sigCh @@ -108,7 +109,7 @@ func runWebServer() { return } log.Println("Sub server restarted successfully.") - case syscall.SIGUSR1: + case sys.SIGUSR1: logger.Info("Received USR1 signal, restarting xray-core...") err := server.RestartXray() if err != nil { diff --git a/util/sys/sys_darwin.go b/util/sys/sys_darwin.go index b635b549..b44d7689 100644 --- a/util/sys/sys_darwin.go +++ b/util/sys/sys_darwin.go @@ -7,11 +7,14 @@ import ( "encoding/binary" "fmt" "sync" + "syscall" "github.com/shirou/gopsutil/v4/net" "golang.org/x/sys/unix" ) +var SIGUSR1 = syscall.SIGUSR1 + func GetTCPCount() (int, error) { stats, err := net.Connections("tcp") if err != nil { diff --git a/util/sys/sys_linux.go b/util/sys/sys_linux.go index 23483b57..5b1b1127 100644 --- a/util/sys/sys_linux.go +++ b/util/sys/sys_linux.go @@ -12,8 +12,11 @@ import ( "strconv" "strings" "sync" + "syscall" ) +var SIGUSR1 = syscall.SIGUSR1 + func getLinesNum(filename string) (int, error) { file, err := os.Open(filename) if err != nil { diff --git a/util/sys/sys_windows.go b/util/sys/sys_windows.go index 186fa4bb..9b6d659f 100644 --- a/util/sys/sys_windows.go +++ b/util/sys/sys_windows.go @@ -12,6 +12,8 @@ import ( "github.com/shirou/gopsutil/v4/net" ) +var SIGUSR1 = syscall.Signal(0) + // GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp"). func GetConnectionCount(proto string) (int, error) { if proto != "tcp" && proto != "udp" { From 59b695ba831be9547149db3525420b836d160272 Mon Sep 17 00:00:00 2001 From: Nabi KaramAliZadeh Date: Sun, 1 Mar 2026 17:48:16 +0330 Subject: [PATCH 03/36] fix: remove excluded paths from gzip middleware in router initialization (#3860) --- web/web.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/web.go b/web/web.go index 71845a14..60934048 100644 --- a/web/web.go +++ b/web/web.go @@ -200,7 +200,7 @@ func (s *Server) initRouter() (*gin.Engine, error) { if err != nil { return nil, err } - engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/api/"}))) + engine.Use(gzip.Gzip(gzip.DefaultCompression)) assetsBasePath := basePath + "assets/" store := cookie.NewStore(secret) From 96b8fe472ce2600906237ebc120e9c99983bc4d5 Mon Sep 17 00:00:00 2001 From: Aleksei Sidorenko <88515338+rydve@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:35:24 +0300 Subject: [PATCH 04/36] Fix: escape HTML characters in tgbot start command (#3883) --- web/service/tgbot.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 3ff80b40..6bb335b9 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "html" "io" "math/big" "net" @@ -651,7 +652,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo msg += t.I18nBot("tgbot.commands.help") msg += t.I18nBot("tgbot.commands.pleaseChoose") case "start": - msg += t.I18nBot("tgbot.commands.start", "Firstname=="+message.From.FirstName) + msg += t.I18nBot("tgbot.commands.start", "Firstname=="+html.EscapeString(message.From.FirstName)) if isAdmin { msg += t.I18nBot("tgbot.commands.welcome", "Hostname=="+hostname) } From ccd223aeea82e246e4575bf49fa6419e65be3901 Mon Sep 17 00:00:00 2001 From: Happ-dev Date: Wed, 4 Mar 2026 14:29:46 +0300 Subject: [PATCH 05/36] Fix DeepLink for Happ, remove encoding URL (#3863) Co-authored-by: y.sivushkin --- web/assets/js/subscription.js | 2 +- web/html/settings/panel/subscription/subpage.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/assets/js/subscription.js b/web/assets/js/subscription.js index b79d361c..228dcfa0 100644 --- a/web/assets/js/subscription.js +++ b/web/assets/js/subscription.js @@ -144,7 +144,7 @@ return this.app.subUrl; }, happUrl() { - return `happ://add/${encodeURIComponent(this.app.subUrl)}`; + return `happ://add/${this.app.subUrl}`; } }, methods: { diff --git a/web/html/settings/panel/subscription/subpage.html b/web/html/settings/panel/subscription/subpage.html index c59f68ee..794c67c3 100644 --- a/web/html/settings/panel/subscription/subpage.html +++ b/web/html/settings/panel/subscription/subpage.html @@ -206,7 +206,7 @@ NPV Tunnel Happ + @click="open('happ://add/' + app.subUrl)">Happ From 842fae18d7acd5d42300052a54d3bbf950cea8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=90=E5=AF=92?= <42313988+linkerlau@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:32:01 +0800 Subject: [PATCH 06/36] Add 'default' runlevel to x-ui service in Alpine (#3854) it should be 'default' runlevel when add x-ui service to openrc, default is 'sysinit' runlevel. 'sysinit' runlevel is unnecessary,maybe. if not, there is an error when call to function 'check_enabled()' as command 'grep default -c' can`t print 'default' runlevel. check_enabled() { if [[ $release == "alpine" ]]; then if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then return 0 else return 1 fi --- x-ui.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-ui.sh b/x-ui.sh index 933a964e..0a2b818b 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -431,7 +431,7 @@ status() { enable() { if [[ $release == "alpine" ]]; then - rc-update add x-ui + rc-update add x-ui default else systemctl enable x-ui fi From 874aae8080b587877ef3cf7d0048f79e09fb7233 Mon Sep 17 00:00:00 2001 From: Artur <45873008+xqzts@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:36:45 +0300 Subject: [PATCH 07/36] Add cron to ubuntu packages (#3875) --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 46207777..9d1aeb6b 100644 --- a/install.sh +++ b/install.sh @@ -76,7 +76,7 @@ is_port_in_use() { install_base() { case "${release}" in ubuntu | debian | armbian) - apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates + apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates From 57409964369a239093998a98fba9167c198db01c Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 4 Mar 2026 13:05:29 +0100 Subject: [PATCH 08/36] update dependencies --- go.mod | 21 ++++++++++---------- go.sum | 42 +++++++++++++++++++++------------------- web/job/ldap_sync_job.go | 5 +---- web/locale/locale.go | 2 +- web/service/setting.go | 8 ++++---- web/service/tgbot.go | 36 ++++++++++++++++------------------ 6 files changed, 56 insertions(+), 58 deletions(-) diff --git a/go.mod b/go.mod index 411794af..97e2e38d 100644 --- a/go.mod +++ b/go.mod @@ -5,18 +5,18 @@ go 1.26.0 require ( github.com/gin-contrib/gzip v1.2.5 github.com/gin-contrib/sessions v1.0.4 - github.com/gin-gonic/gin v1.11.0 + github.com/gin-gonic/gin v1.12.0 github.com/go-ldap/ldap/v3 v3.4.12 github.com/goccy/go-json v0.10.5 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 - github.com/mymmrac/telego v1.6.0 + github.com/mymmrac/telego v1.7.0 github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/pelletier/go-toml/v2 v2.2.4 github.com/robfig/cron/v3 v3.0.1 - github.com/shirou/gopsutil/v4 v4.26.1 + github.com/shirou/gopsutil/v4 v4.26.2 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/valyala/fasthttp v1.69.0 github.com/xlzd/gotp v0.1.0 @@ -39,7 +39,7 @@ require ( github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/ebitengine/purego v0.9.1 // indirect + github.com/ebitengine/purego v0.10.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect @@ -60,7 +60,7 @@ require ( github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect + github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v1.14.34 // indirect github.com/miekg/dns v1.1.72 // indirect @@ -72,29 +72,30 @@ require ( github.com/quic-go/quic-go v0.59.0 // indirect github.com/refraction-networking/utls v1.8.2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/sagernet/sing v0.7.18 // indirect + github.com/sagernet/sing v0.8.1 // indirect github.com/sagernet/sing-shadowsocks v0.2.9 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fastjson v1.6.7 // indirect + github.com/valyala/fastjson v1.6.10 // indirect github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/arch v0.24.0 // indirect - golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect + golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.50.0 // indirect + golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.42.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/protobuf v1.36.11 // indirect gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect lukechampine.com/blake3 v1.4.1 // indirect diff --git a/go.sum b/go.sum index 7e18ac8d..9b78e860 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= -github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= @@ -35,8 +35,8 @@ github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kb github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= -github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= @@ -117,8 +117,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= -github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= +github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= @@ -130,8 +130,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0= -github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y= +github.com/mymmrac/telego v1.7.0 h1:yRO/l00tFGG4nY66ufUKb4ARqv7qx9+LsjQv/b0NEyo= +github.com/mymmrac/telego v1.7.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= @@ -156,12 +156,12 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E= -github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.1 h1:Li+zg4xdiMsvdX4j50TPqmSG8LF/TB9US2qlAN40izU= +github.com/sagernet/sing v0.8.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM= github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= -github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo= -github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= +github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= +github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -187,8 +187,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= -github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= -github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= @@ -203,6 +203,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= @@ -227,12 +229,12 @@ golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= -golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -255,8 +257,8 @@ golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+Z golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/web/job/ldap_sync_job.go b/web/job/ldap_sync_job.go index a947eb73..7edb8178 100644 --- a/web/job/ldap_sync_job.go +++ b/web/job/ldap_sync_job.go @@ -271,10 +271,7 @@ func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[s // Delete in batches for i := 0; i < len(toDelete); i += batchSize { - end := i + batchSize - if end > len(toDelete) { - end = len(toDelete) - } + end := min(i+batchSize, len(toDelete)) batch := toDelete[i:end] for _, c := range batch { diff --git a/web/locale/locale.go b/web/locale/locale.go index c469911a..73da75b4 100644 --- a/web/locale/locale.go +++ b/web/locale/locale.go @@ -37,7 +37,7 @@ type SettingService interface { // InitLocalizer initializes the internationalization system with embedded translation files. func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { - // set default bundle to english + // set default bundle to English i18nBundle = i18n.NewBundle(language.MustParse("en-US")) i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) diff --git a/web/service/setting.go b/web/service/setting.go index 33012c39..5c93e9fd 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -108,7 +108,7 @@ var defaultValueMap = map[string]string{ // It handles configuration storage, retrieval, and validation for all system settings. type SettingService struct{} -func (s *SettingService) GetDefaultJsonConfig() (any, error) { +func (s *SettingService) GetDefaultJSONConfig() (any, error) { var jsonData any err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData) if err != nil { @@ -125,7 +125,7 @@ func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) { return nil, err } allSetting := &entity.AllSetting{} - t := reflect.TypeOf(allSetting).Elem() + t := reflect.TypeFor[entity.AllSetting]() v := reflect.ValueOf(allSetting).Elem() fields := reflect_util.GetFields(t) @@ -607,7 +607,7 @@ func (s *SettingService) GetIpLimitEnable() (bool, error) { return (accessLogPath != "none" && accessLogPath != ""), nil } -// LDAP exported getters +// GetLdapEnable returns whether LDAP is enabled. func (s *SettingService) GetLdapEnable() (bool, error) { return s.getBool("ldapEnable") } @@ -694,7 +694,7 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error { } v := reflect.ValueOf(allSetting).Elem() - t := reflect.TypeOf(allSetting).Elem() + t := reflect.TypeFor[entity.AllSetting]() fields := reflect_util.GetFields(t) errs := make([]error, 0) for _, field := range fields { diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 6bb335b9..6a49f1d3 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -16,6 +16,7 @@ import ( "net/url" "os" "regexp" + "slices" "strconv" "strings" "sync" @@ -2719,7 +2720,7 @@ func (t *Tgbot) prepareServerUsageInfo() string { info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown")) info += "\r\n" } else { - for i := 0; i < len(netInterfaces); i++ { + for i := range netInterfaces { if (netInterfaces[i].Flags & net.FlagUp) != 0 { addrs, _ := netInterfaces[i].Addrs() @@ -2788,29 +2789,29 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim // getInboundUsages retrieves and formats inbound usage information. func (t *Tgbot) getInboundUsages() string { - info := "" + var info strings.Builder // get traffic inbounds, err := t.inboundService.GetAllInbounds() if err != nil { logger.Warning("GetAllInbounds run failed:", err) - info += t.I18nBot("tgbot.answers.getInboundsFailed") + info.WriteString(t.I18nBot("tgbot.answers.getInboundsFailed")) } else { // NOTE:If there no any sessions here,need to notify here // TODO:Sub-node push, automatic conversion format for _, inbound := range inbounds { - info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) - info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) - info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) + info.WriteString(t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)) + info.WriteString(t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))) + info.WriteString(t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))) if inbound.ExpiryTime == 0 { - info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")) + info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))) } else { - info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) + info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))) } - info += "\r\n" + info.WriteString("\r\n") } } - return info + return info.String() } // getInbounds creates an inline keyboard with all inbounds. @@ -3060,12 +3061,9 @@ func (t *Tgbot) clientInfoMsg( status := t.I18nBot("tgbot.offline") isOnline := false if p.IsRunning() { - for _, online := range p.GetOnlineClients() { - if online == traffic.Email { - status = t.I18nBot("tgbot.online") - isOnline = true - break - } + if slices.Contains(p.GetOnlineClients(), traffic.Email) { + status = t.I18nBot("tgbot.online") + isOnline = true } } @@ -3430,11 +3428,11 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) { t.SendMsgToTgbot(chatId, info) if len(inbound.ClientStats) > 0 { - output := "" + var output strings.Builder for _, traffic := range inbound.ClientStats { - output += t.clientInfoMsg(&traffic, true, true, true, true, true, true) + output.WriteString(t.clientInfoMsg(&traffic, true, true, true, true, true, true)) } - t.SendMsgToTgbot(chatId, output) + t.SendMsgToTgbot(chatId, output.String()) } } } From 34d88850754351c680ba7a26753684be83524e0a Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 4 Mar 2026 13:39:14 +0100 Subject: [PATCH 09/36] Adjust KCP MTU when selecting xDNS mask --- web/html/form/outbound.html | 6 +++--- web/html/form/stream/stream_finalmask.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/html/form/outbound.html b/web/html/form/outbound.html index a2de920a..21bc11fc 100644 --- a/web/html/form/outbound.html +++ b/web/html/form/outbound.html @@ -612,7 +612,7 @@ mKCP Original - + xDNS (Experimental) diff --git a/web/html/form/stream/stream_finalmask.html b/web/html/form/stream/stream_finalmask.html index 35962dfa..0b6418b7 100644 --- a/web/html/form/stream/stream_finalmask.html +++ b/web/html/form/stream/stream_finalmask.html @@ -18,7 +18,7 @@ xICMP (Experimental) - + xDNS (Experimental) From 52fdf5d4296b4534e25d6221d82ec7d819a9b952 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 4 Mar 2026 13:54:01 +0100 Subject: [PATCH 10/36] v2.8.11 --- config/version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/version b/config/version index 39e84545..05822b7e 100644 --- a/config/version +++ b/config/version @@ -1 +1 @@ -2.8.10 \ No newline at end of file +2.8.11 \ No newline at end of file From a2097ad06218d49f5aad20254b7bb8c2a9fc0e03 Mon Sep 17 00:00:00 2001 From: Aleksei Sidorenko <88515338+rydve@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:26:53 +0300 Subject: [PATCH 11/36] feat: mask password in telegram notification on 2FA failure (#3884) --- web/controller/index.go | 13 +++++++++++-- web/service/user.go | 20 +++++++++----------- web/translation/translate.ar_EG.toml | 1 + web/translation/translate.en_US.toml | 1 + web/translation/translate.es_ES.toml | 1 + web/translation/translate.fa_IR.toml | 1 + web/translation/translate.id_ID.toml | 1 + web/translation/translate.ja_JP.toml | 1 + web/translation/translate.pt_BR.toml | 1 + web/translation/translate.ru_RU.toml | 1 + web/translation/translate.tr_TR.toml | 1 + web/translation/translate.uk_UA.toml | 1 + web/translation/translate.vi_VN.toml | 1 + web/translation/translate.zh_CN.toml | 1 + web/translation/translate.zh_TW.toml | 1 + 15 files changed, 33 insertions(+), 13 deletions(-) diff --git a/web/controller/index.go b/web/controller/index.go index 5f9e1c2c..605f874f 100644 --- a/web/controller/index.go +++ b/web/controller/index.go @@ -4,6 +4,7 @@ import ( "net/http" "text/template" "time" + "fmt" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/web/service" @@ -71,14 +72,22 @@ func (a *IndexController) login(c *gin.Context) { return } - user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode) + user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode) timeStr := time.Now().Format("2006-01-02 15:04:05") safeUser := template.HTMLEscapeString(form.Username) safePass := template.HTMLEscapeString(form.Password) if user == nil { logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c)) - a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0) + + notifyPass := safePass + + if checkErr != nil && checkErr.Error() == "invalid 2fa code" { + translatedError := a.tgbot.I18nBot("tgbot.messages.2faFailed") + notifyPass = fmt.Sprintf("*** (%s)", translatedError) + } + + a.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0) pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword")) return } diff --git a/web/service/user.go b/web/service/user.go index 1bde69f6..0a2a3f3e 100644 --- a/web/service/user.go +++ b/web/service/user.go @@ -33,7 +33,7 @@ func (s *UserService) GetFirstUser() (*model.User, error) { return user, nil } -func (s *UserService) CheckUser(username string, password string, twoFactorCode string) *model.User { +func (s *UserService) CheckUser(username string, password string, twoFactorCode string) (*model.User, error) { db := database.GetDB() user := &model.User{} @@ -43,17 +43,16 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode First(user). Error if err == gorm.ErrRecordNotFound { - return nil + return nil, errors.New("invalid credentials") } else if err != nil { logger.Warning("check user err:", err) - return nil + return nil, err } - // If LDAP enabled and local password check fails, attempt LDAP auth if !crypto.CheckPasswordHash(user.Password, password) { ldapEnabled, _ := s.settingService.GetLdapEnable() if !ldapEnabled { - return nil + return nil, errors.New("invalid credentials") } host, _ := s.settingService.GetLdapHost() @@ -77,15 +76,14 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode } ok, err := ldaputil.AuthenticateUser(cfg, username, password) if err != nil || !ok { - return nil + return nil, errors.New("invalid credentials") } - // On successful LDAP auth, continue 2FA checks below } twoFactorEnable, err := s.settingService.GetTwoFactorEnable() if err != nil { logger.Warning("check two factor err:", err) - return nil + return nil, err } if twoFactorEnable { @@ -93,15 +91,15 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode if err != nil { logger.Warning("check two factor token err:", err) - return nil + return nil, err } if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode { - return nil + return nil, errors.New("invalid 2fa code") } } - return user + return user, nil } func (s *UserService) UpdateUser(id int, username string, password string) error { diff --git a/web/translation/translate.ar_EG.toml b/web/translation/translate.ar_EG.toml index 582747d8..3fbcee6e 100644 --- a/web/translation/translate.ar_EG.toml +++ b/web/translation/translate.ar_EG.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ حفظت بيانات مستخدم Telegram." "loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n" "loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n" +"2faFailed" = "فشل 2FA" "report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n" "datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n" "hostname" = "💻 السيرفر: {{ .Hostname }}\r\n" diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 4ea136ec..7d6ab162 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ Telegram User saved." "loginSuccess" = "✅ Logged in to the panel successfully.\r\n" "loginFailed" = "❗️Login attempt to the panel failed.\r\n" +"2faFailed" = "2FA Failed" "report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n" "datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n" diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml index d018f94c..14429228 100644 --- a/web/translation/translate.es_ES.toml +++ b/web/translation/translate.es_ES.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ Usuario de Telegram guardado." "loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n" "loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n" +"2faFailed" = "Error de 2FA" "report" = "🕰 Informes programados: {{ .RunTime }}\r\n" "datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n" "hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n" diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index 144e68a7..cc2220fd 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ کاربر تلگرام ذخیره شد." "loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n" "loginFailed" = "❗️ ورود به پنل ناموفق‌بود \r\n" +"2faFailed" = "خطای 2FA" "report" = "🕰 گزارشات‌زمان‌بندی‌شده: {{ .RunTime }}\r\n" "datetime" = "⏰ تاریخ‌وزمان: {{ .DateTime }}\r\n" "hostname" = "💻 نام‌میزبان: {{ .Hostname }}\r\n" diff --git a/web/translation/translate.id_ID.toml b/web/translation/translate.id_ID.toml index e82288ca..65fc04af 100644 --- a/web/translation/translate.id_ID.toml +++ b/web/translation/translate.id_ID.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ Pengguna Telegram tersimpan." "loginSuccess" = "✅ Berhasil masuk ke panel.\r\n" "loginFailed" = "❗️ Gagal masuk ke panel.\r\n" +"2faFailed" = "2FA Gagal" "report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n" "datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n" diff --git a/web/translation/translate.ja_JP.toml b/web/translation/translate.ja_JP.toml index 4227809a..d7ff3451 100644 --- a/web/translation/translate.ja_JP.toml +++ b/web/translation/translate.ja_JP.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ Telegramユーザーが保存されました。" "loginSuccess" = "✅ パネルに正常にログインしました。\r\n" "loginFailed" = "❗️ パネルのログインに失敗しました。\r\n" +"2faFailed" = "2FAエラー" "report" = "🕰 定期報告:{{ .RunTime }}\r\n" "datetime" = "⏰ 日時:{{ .DateTime }}\r\n" "hostname" = "💻 ホスト名:{{ .Hostname }}\r\n" diff --git a/web/translation/translate.pt_BR.toml b/web/translation/translate.pt_BR.toml index 4533d104..dc04c98f 100644 --- a/web/translation/translate.pt_BR.toml +++ b/web/translation/translate.pt_BR.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ Usuário do Telegram salvo." "loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n" "loginFailed" = "❗️Tentativa de login no painel falhou.\r\n" +"2faFailed" = "Falha no 2FA" "report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n" "datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 79c305a2..8a403a1c 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ Пользователь Telegram сохранен." "loginSuccess" = "✅ Успешный вход в панель.\r\n" "loginFailed" = "❗️ Ошибка входа в панель.\r\n" +"2faFailed" = "Ошибка 2FA" "report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n" "datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n" "hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n" diff --git a/web/translation/translate.tr_TR.toml b/web/translation/translate.tr_TR.toml index 4b3c3bc5..57b84e07 100644 --- a/web/translation/translate.tr_TR.toml +++ b/web/translation/translate.tr_TR.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ Telegram Kullanıcısı kaydedildi." "loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n" "loginFailed" = "❗️Panele giriş denemesi başarısız oldu.\r\n" +"2faFailed" = "2FA Hatası" "report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n" "datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n" "hostname" = "💻 Sunucu: {{ .Hostname }}\r\n" diff --git a/web/translation/translate.uk_UA.toml b/web/translation/translate.uk_UA.toml index e4a53794..b08ddbec 100644 --- a/web/translation/translate.uk_UA.toml +++ b/web/translation/translate.uk_UA.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ Користувача Telegram збережено." "loginSuccess" = "✅ Успішно ввійшли в панель\r\n" "loginFailed" = "❗️ Помилка входу в панель.\r\n" +"2faFailed" = "Помилка 2FA" "report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n" "datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n" "hostname" = "💻 Хост: {{ .Hostname }}\r\n" diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml index 81b9e4cb..a4d667d0 100644 --- a/web/translation/translate.vi_VN.toml +++ b/web/translation/translate.vi_VN.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ Người dùng Telegram đã được lưu." "loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n" "loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n" +"2faFailed" = "Lỗi 2FA" "report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n" "datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n" "hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n" diff --git a/web/translation/translate.zh_CN.toml b/web/translation/translate.zh_CN.toml index 3d1b5ded..103698f6 100644 --- a/web/translation/translate.zh_CN.toml +++ b/web/translation/translate.zh_CN.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ 电报用户已保存。" "loginSuccess" = "✅ 成功登录到面板。\r\n" "loginFailed" = "❗️ 面板登录失败。\r\n" +"2faFailed" = "2FA 失败" "report" = "🕰 定时报告:{{ .RunTime }}\r\n" "datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n" "hostname" = "💻 主机名:{{ .Hostname }}\r\n" diff --git a/web/translation/translate.zh_TW.toml b/web/translation/translate.zh_TW.toml index 0eb73b8a..ab083f2c 100644 --- a/web/translation/translate.zh_TW.toml +++ b/web/translation/translate.zh_TW.toml @@ -663,6 +663,7 @@ "userSaved" = "✅ 電報使用者已儲存。" "loginSuccess" = "✅ 成功登入到面板。\r\n" "loginFailed" = "❗️ 面板登入失敗。\r\n" +"2faFailed" = "2FA 失敗" "report" = "🕰 定時報告:{{ .RunTime }}\r\n" "datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n" "hostname" = "💻 主機名:{{ .Hostname }}\r\n" From 258b08fff35fc6c445b77149996f6dd9f7b4a789 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 8 Mar 2026 11:53:34 +0100 Subject: [PATCH 12/36] Update fail2ban filter regex in x-ui.sh --- x-ui.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-ui.sh b/x-ui.sh index 0a2b818b..2e555b25 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -2012,7 +2012,7 @@ EOF cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf [Definition] datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S -failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*SRC\s*=\s* +failregex = \[LIMIT_IP\]\s*Email\s*=\s*.+\s*\|\|\s*Disconnecting OLD IP\s*=\s*\s*\|\|\s*Timestamp\s*=\s*\d+ ignoreregex = EOF From 7b03346cfcb50b49b124cc8333aeeadeb76a8455 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Tue, 17 Mar 2026 21:03:32 +0100 Subject: [PATCH 13/36] Set package ecosystem to GitHub Actions in dependabot.yml --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..0d08e261 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" From ee84d585f9e52a8fa794c16eb765f1b1dc1411b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:04:41 +0100 Subject: [PATCH 14/36] Bump docker/login-action from 3 to 4 (#3939) Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 39ddf2e0..921a8e5c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -40,13 +40,13 @@ jobs: install: true - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} From 5bbb48a8fd1ef3e15af4d95cb2c8aa48188e337e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:04:54 +0100 Subject: [PATCH 15/36] Bump docker/setup-qemu-action from 3 to 4 (#3936) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 921a8e5c..53d81cdc 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -32,7 +32,7 @@ jobs: type=semver,pattern={{version}} - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From a3e1bd59df6725e815ce190575a2050f46c74a8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:05:07 +0100 Subject: [PATCH 16/36] Bump docker/build-push-action from 6 to 7 (#3937) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6...v7) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 53d81cdc..df62ebd4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -53,7 +53,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . push: true From ff72090e1a0514a2a270d34df3d15b300ebc28b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:05:28 +0100 Subject: [PATCH 17/36] Bump docker/setup-buildx-action from 3 to 4 (#3938) Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index df62ebd4..343a9339 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -35,7 +35,7 @@ jobs: uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 with: install: true From e4add73c9e9f22cca7560907e2fab702e08d96ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:05:43 +0100 Subject: [PATCH 18/36] Bump actions/checkout from 5 to 6 (#3940) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 343a9339..eeaaebcb 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b8d6902..18cf2667 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 @@ -165,7 +165,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 From 6767f76ccf2b0e6c5463731686d094d2fa659baf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:09:56 +0100 Subject: [PATCH 19/36] Bump actions/upload-artifact from 4 to 7 (#3941) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 18cf2667..9e94fb74 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -133,7 +133,7 @@ jobs: run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui - name: Upload files to Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: x-ui-linux-${{ matrix.platform }} path: ./x-ui-linux-${{ matrix.platform }}.tar.gz @@ -230,7 +230,7 @@ jobs: Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip" - name: Upload files to Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: x-ui-windows-amd64 path: ./x-ui-windows-amd64.zip From a6d0100381c5f3150d5d5b53b2f450b5b2dc308e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:10:09 +0100 Subject: [PATCH 20/36] Bump docker/metadata-action from 5 to 6 (#3942) Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/v5...v6) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index eeaaebcb..0dd4847d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -21,7 +21,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | hsanaeii/3x-ui From 60abeaad66a14e29250f0e0b3f2ee9b45c53cbc2 Mon Sep 17 00:00:00 2001 From: HamidReza Sadeghzadeh Date: Tue, 17 Mar 2026 23:48:10 +0330 Subject: [PATCH 21/36] fix: Ban new IPs with fail2ban instead of disconnected the client. (#3919) * fix: Ban new IPs with fail2ban instead of disconnected the client. * fix: Remove unused strconv import * fix: Revert log fail2ban format --- web/job/check_client_ip_job.go | 79 ++++------------------------------ 1 file changed, 9 insertions(+), 70 deletions(-) diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go index d3c1a1d1..cbc352dc 100644 --- a/web/job/check_client_ip_job.go +++ b/web/job/check_client_ip_job.go @@ -10,7 +10,6 @@ import ( "regexp" "runtime" "sort" - "strconv" "time" "github.com/mhsanaei/3x-ui/v2/database" @@ -319,13 +318,14 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun } } - // Convert back to slice and sort by timestamp (newest first) + // Convert back to slice and sort by timestamp (oldest first) + // This ensures we always protect the original/current connections and ban new excess ones. allIps := make([]IPWithTimestamp, 0, len(ipMap)) for ip, timestamp := range ipMap { allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp}) } sort.Slice(allIps, func(i, j int) bool { - return allIps[i].Timestamp > allIps[j].Timestamp // Descending order (newest first) + return allIps[i].Timestamp < allIps[j].Timestamp // Ascending order (oldest first) }) shouldCleanLog := false @@ -345,23 +345,17 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun if len(allIps) > limitIp { shouldCleanLog = true - // Keep only the newest IPs (up to limitIp) + // Keep the oldest IPs (currently active connections) and ban the new excess ones. keptIps := allIps[:limitIp] - disconnectedIps := allIps[limitIp:] + bannedIps := allIps[limitIp:] - // Log the disconnected IPs (old ones) - for _, ipTime := range disconnectedIps { + // Log banned IPs in the format fail2ban filters expect: [LIMIT_IP] Email = X || Disconnecting OLD IP = Y || Timestamp = Z + for _, ipTime := range bannedIps { j.disAllowedIps = append(j.disAllowedIps, ipTime.IP) log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp) } - // Actually disconnect old IPs by temporarily removing and re-adding user - // This forces Xray to drop existing connections from old IPs - if len(disconnectedIps) > 0 { - j.disconnectClientTemporarily(inbound, clientEmail, clients) - } - - // Update database with only the newest IPs + // Update database with only the currently active (kept) IPs jsonIps, _ := json.Marshal(keptIps) inboundClientIps.Ips = string(jsonIps) } else { @@ -378,67 +372,12 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun } if len(j.disAllowedIps) > 0 { - logger.Infof("[LIMIT_IP] Client %s: Kept %d newest IPs, disconnected %d old IPs", clientEmail, limitIp, len(j.disAllowedIps)) + logger.Infof("[LIMIT_IP] Client %s: Kept %d current IPs, queued %d new IPs for fail2ban", clientEmail, limitIp, len(j.disAllowedIps)) } return shouldCleanLog } -// disconnectClientTemporarily removes and re-adds a client to force disconnect old connections -func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, clientEmail string, clients []model.Client) { - var xrayAPI xray.XrayAPI - - // Get panel settings for API port - db := database.GetDB() - var apiPort int - var apiPortSetting model.Setting - if err := db.Where("key = ?", "xrayApiPort").First(&apiPortSetting).Error; err == nil { - apiPort, _ = strconv.Atoi(apiPortSetting.Value) - } - - if apiPort == 0 { - apiPort = 10085 // Default API port - } - - err := xrayAPI.Init(apiPort) - if err != nil { - logger.Warningf("[LIMIT_IP] Failed to init Xray API for disconnection: %v", err) - return - } - defer xrayAPI.Close() - - // Find the client config - var clientConfig map[string]any - for _, client := range clients { - if client.Email == clientEmail { - // Convert client to map for API - clientBytes, _ := json.Marshal(client) - json.Unmarshal(clientBytes, &clientConfig) - break - } - } - - if clientConfig == nil { - return - } - - // Remove user to disconnect all connections - err = xrayAPI.RemoveUser(inbound.Tag, clientEmail) - if err != nil { - logger.Warningf("[LIMIT_IP] Failed to remove user %s: %v", clientEmail, err) - return - } - - // Wait a moment for disconnection to take effect - time.Sleep(100 * time.Millisecond) - - // Re-add user to allow new connections - err = xrayAPI.AddUser(string(inbound.Protocol), inbound.Tag, clientConfig) - if err != nil { - logger.Warningf("[LIMIT_IP] Failed to re-add user %s: %v", clientEmail, err) - } -} - func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) { db := database.GetDB() inbound := &model.Inbound{} From 7f7ae0c547dccea93607d21a6283c91165ce52a5 Mon Sep 17 00:00:00 2001 From: Alimpo <42714856+Alimpo@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:50:24 +0330 Subject: [PATCH 22/36] fix: stop overwriting client_traffics.enable with JSON enable in GetClientTrafficByEmail (#3931) When a client hit traffic/expiry limit, disableInvalidClients sets client_traffics.enable=false and removes the user from Xray. GetClientTrafficByEmail was overwriting that with settings.clients[].enable (admin config), so ResetClientTraffic never saw the client as disabled and did not re-add the user. Clients could not connect until manually disabled/re-enabled. Now the DB runtime enable flag is preserved; reset correctly re-adds the user to Xray. --- web/service/inbound.go | 1 - 1 file changed, 1 deletion(-) diff --git a/web/service/inbound.go b/web/service/inbound.go index 101c79d9..8a3a4ae2 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -2032,7 +2032,6 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl return nil, err } if t != nil && client != nil { - t.Enable = client.Enable t.UUID = client.ID t.SubId = client.SubID return t, nil From a08f1c6c13521cff13e7786bbefc2d83026c9e61 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Tue, 17 Mar 2026 23:24:09 +0300 Subject: [PATCH 23/36] Update translate.ru_RU.toml (#3889) Change to plural (geofiles, not geofile) --- web/translation/translate.ru_RU.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 8a403a1c..0425db96 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -149,7 +149,7 @@ "geofileUpdateDialogDesc" = "Это обновит файл #filename#." "geofilesUpdateDialogDesc" = "Это обновит все геофайлы." "geofilesUpdateAll" = "Обновить все" -"geofileUpdatePopover" = "Геофайл успешно обновлён" +"geofileUpdatePopover" = "Геофайлы успешно обновлены" "dontRefresh" = "Установка в процессе. Не обновляйте страницу" "logs" = "Журнал" "config" = "Конфигурация" From 554981d9d347c88a5c5973aa1fd711676c0cd7e9 Mon Sep 17 00:00:00 2001 From: Abdalrahman Date: Tue, 17 Mar 2026 23:09:49 +0200 Subject: [PATCH 24/36] feat(tgbot): send connection links and qrs on client creation (closes #3320)\n\n- Refactored inline keyboards into getCommonClientButtons to respect DRY\n- Extended SubmitAddClient callback handlers to dispatch individual links and QR codes to the bot chat on success. (#3888) --- web/service/tgbot.go | 102 ++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 64 deletions(-) diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 6a49f1d3..1649f2ed 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -1926,6 +1926,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool } else { t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) + t.sendClientIndividualLinks(chatId, client_Email) + t.sendClientQRLinks(chatId, client_Email) } case "add_client_submit_enable": client_Enable = true @@ -1936,6 +1938,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool } else { t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) + t.sendClientIndividualLinks(chatId, client_Email) + t.sendClientQRLinks(chatId, client_Email) } case "reset_all_traffics_cancel": t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) @@ -3302,6 +3306,27 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { } } +// getCommonClientButtons returns the shared inline keyboard rows for client configuration +func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton { + return [][]telego.InlineKeyboardButton{ + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), + ), + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), + ), + } +} + // addClient handles the process of adding a new client to an inbound. func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) { inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) @@ -3312,91 +3337,40 @@ func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) { protocol := inbound.Protocol + var protocolRows [][]telego.InlineKeyboardButton switch protocol { case model.VMESS, model.VLESS: - inlineKeyboard := tu.InlineKeyboard( + protocolRows = [][]telego.InlineKeyboardButton{ tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"), ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), - ), - ) - if len(messageID) > 0 { - t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) - } else { - t.SendMsgToTgbot(chatId, msg, inlineKeyboard) } case model.Trojan: - inlineKeyboard := tu.InlineKeyboard( + protocolRows = [][]telego.InlineKeyboardButton{ tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"), ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), - tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), - ), - ) - if len(messageID) > 0 { - t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) - } else { - t.SendMsgToTgbot(chatId, msg, inlineKeyboard) } case model.Shadowsocks: - inlineKeyboard := tu.InlineKeyboard( + protocolRows = [][]telego.InlineKeyboardButton{ tu.InlineKeyboardRow( tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_sh"), ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"), - tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"), - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"), - ), - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"), - ), - ) - - if len(messageID) > 0 { - t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) - } else { - t.SendMsgToTgbot(chatId, msg, inlineKeyboard) } } + commonRows := t.getCommonClientButtons() + inlineKeyboard := tu.InlineKeyboard(append(protocolRows, commonRows...)...) + + if len(messageID) > 0 { + t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard) + } else { + t.SendMsgToTgbot(chatId, msg, inlineKeyboard) + } + } // searchInbound searches for inbounds by remark and sends the results. From f0f98c712225269e1a3db1796eff88ebecaf2a2a Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 17 Mar 2026 22:30:05 +0100 Subject: [PATCH 25/36] Add Go code analyzer workflow --- .github/workflows/release.yml | 56 ++++++++++++++++++++++++++++------- web/controller/index.go | 10 +++---- web/service/user.go | 2 +- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e94fb74..ed9417c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,11 +2,9 @@ name: Release 3X-UI on: workflow_dispatch: - release: - types: [published] push: branches: - - main + - '**' tags: - "v*.*.*" paths: @@ -20,9 +18,48 @@ on: - 'x-ui.service.debian' - 'x-ui.service.arch' - 'x-ui.service.rhel' + pull_request: jobs: + analyze: + name: Analyze Go code + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - name: Check formatting + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "These files are not gofmt-formatted:" + echo "$unformatted" + exit 1 + fi + + - name: Run go vet + run: go vet ./... + + - name: Run staticcheck + uses: dominikh/staticcheck-action@v1 + with: + version: "latest" + install-go: false + + - name: Run tests + run: go test -race -shuffle=on ./... + build: + needs: analyze permissions: contents: write strategy: @@ -140,12 +177,10 @@ jobs: - name: Upload files to GH release uses: svenstaro/upload-release-action@v2 - if: | - (github.event_name == 'release' && github.event.action == 'published') || - (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') with: repo_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} + tag: ${{ github.ref_name }} file: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz overwrite: true @@ -156,6 +191,7 @@ jobs: # ================================= build-windows: name: Build for Windows + needs: analyze permissions: contents: write strategy: @@ -237,12 +273,10 @@ jobs: - name: Upload files to GH release uses: svenstaro/upload-release-action@v2 - if: | - (github.event_name == 'release' && github.event.action == 'published') || - (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') with: repo_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ github.ref }} + tag: ${{ github.ref_name }} file: x-ui-windows-amd64.zip asset_name: x-ui-windows-amd64.zip overwrite: true diff --git a/web/controller/index.go b/web/controller/index.go index 605f874f..dd58e5e5 100644 --- a/web/controller/index.go +++ b/web/controller/index.go @@ -1,10 +1,10 @@ package controller import ( + "fmt" "net/http" "text/template" "time" - "fmt" "github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/web/service" @@ -79,12 +79,12 @@ func (a *IndexController) login(c *gin.Context) { if user == nil { logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c)) - - notifyPass := safePass - + + notifyPass := safePass + if checkErr != nil && checkErr.Error() == "invalid 2fa code" { translatedError := a.tgbot.I18nBot("tgbot.messages.2faFailed") - notifyPass = fmt.Sprintf("*** (%s)", translatedError) + notifyPass = fmt.Sprintf("*** (%s)", translatedError) } a.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0) diff --git a/web/service/user.go b/web/service/user.go index 0a2a3f3e..6fcf17e7 100644 --- a/web/service/user.go +++ b/web/service/user.go @@ -95,7 +95,7 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode } if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode { - return nil, errors.New("invalid 2fa code") + return nil, errors.New("invalid 2fa code") } } From 38d87230d326dd9ffd9ef9bc29b2e1c70a5b3f88 Mon Sep 17 00:00:00 2001 From: kazan417 Date: Thu, 19 Mar 2026 01:45:45 +0700 Subject: [PATCH 26/36] Update x-ui.sh (#3947) looks like now cert management is option 19 --- x-ui.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-ui.sh b/x-ui.sh index 2e555b25..e9e2d831 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -317,12 +317,12 @@ check_config() { start >/dev/null 2>&1 else LOGE "IP certificate setup failed." - echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}" + echo -e "${yellow}You can try again via option 19 (SSL Certificate Management).${plain}" start >/dev/null 2>&1 fi else echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" - echo -e "${yellow}For security, please configure SSL certificate using option 18 (SSL Certificate Management)${plain}" + echo -e "${yellow}For security, please configure SSL certificate using option 19 (SSL Certificate Management)${plain}" fi fi } From 7e6d80efa5de0990fc701d75230a9505289f9de0 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 1 Apr 2026 13:47:27 +0200 Subject: [PATCH 27/36] Bump Go and dependency versions Update go toolchain to 1.26.1 and upgrade multiple direct and indirect modules (examples: github.com/gin-contrib/gzip v1.2.6, github.com/gin-contrib/sessions v1.1.0, github.com/go-ldap/ldap/v3 v3.4.13, github.com/goccy/go-json v0.10.6, github.com/pelletier/go-toml/v2 v2.3.0, github.com/shirou/gopsutil/v4 v4.26.3, github.com/xtls/xray-core v1.260327.0, golang.org/x/crypto v0.49.0, google.golang.org/grpc v1.80.0). go.sum updated accordingly to lock the new versions. Routine dependency refresh to pull in fixes and improvements. --- go.mod | 64 ++++++++++++++--------------- go.sum | 128 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 96 insertions(+), 96 deletions(-) diff --git a/go.mod b/go.mod index 97e2e38d..a51dc36b 100644 --- a/go.mod +++ b/go.mod @@ -1,52 +1,52 @@ module github.com/mhsanaei/3x-ui/v2 -go 1.26.0 +go 1.26.1 require ( - github.com/gin-contrib/gzip v1.2.5 - github.com/gin-contrib/sessions v1.0.4 + github.com/gin-contrib/gzip v1.2.6 + github.com/gin-contrib/sessions v1.1.0 github.com/gin-gonic/gin v1.12.0 - github.com/go-ldap/ldap/v3 v3.4.12 - github.com/goccy/go-json v0.10.5 + github.com/go-ldap/ldap/v3 v3.4.13 + github.com/goccy/go-json v0.10.6 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 github.com/mymmrac/telego v1.7.0 github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 - github.com/pelletier/go-toml/v2 v2.2.4 + github.com/pelletier/go-toml/v2 v2.3.0 github.com/robfig/cron/v3 v3.0.1 - github.com/shirou/gopsutil/v4 v4.26.2 + github.com/shirou/gopsutil/v4 v4.26.3 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/valyala/fasthttp v1.69.0 github.com/xlzd/gotp v0.1.0 - github.com/xtls/xray-core v1.260206.0 + github.com/xtls/xray-core v1.260327.0 go.uber.org/atomic v1.11.0 - golang.org/x/crypto v0.48.0 - golang.org/x/sys v0.41.0 - golang.org/x/text v0.34.0 - google.golang.org/grpc v1.79.1 + golang.org/x/crypto v0.49.0 + golang.org/x/sys v0.42.0 + golang.org/x/text v0.35.0 + google.golang.org/grpc v1.80.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 ) require ( github.com/Azure/go-ntlmssp v0.1.0 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect - github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 // indirect - github.com/bytedance/gopkg v0.1.3 // indirect + github.com/andybalholm/brotli v1.2.1 // indirect + github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/sonic v1.15.0 // indirect - github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/bytedance/sonic/loader v0.5.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-contrib/sse v1.1.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/google/btree v1.1.3 // indirect github.com/gorilla/context v1.1.2 // indirect @@ -57,12 +57,12 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/juju/ratelimit v1.0.2 // indirect - github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.34 // indirect + github.com/mattn/go-sqlite3 v1.14.38 // indirect github.com/miekg/dns v1.1.72 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -70,9 +70,9 @@ require ( github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect - github.com/refraction-networking/utls v1.8.2 // indirect + github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/sagernet/sing v0.8.1 // indirect + github.com/sagernet/sing v0.8.4 // indirect github.com/sagernet/sing-shadowsocks v0.2.9 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect @@ -82,20 +82,20 @@ require ( github.com/valyala/fastjson v1.6.10 // indirect github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect - github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect + github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/arch v0.24.0 // indirect - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/arch v0.25.0 // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.43.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/protobuf v1.36.11 // indirect gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect lukechampine.com/blake3 v1.4.1 // indirect diff --git a/go.sum b/go.sum index 9b78e860..4946712b 100644 --- a/go.sum +++ b/go.sum @@ -4,16 +4,16 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178 h1:bSq8n+gX4oO/qnM3MKf4kroW75n+phO9Qp6nigJKZ1E= -github.com/apernet/quic-go v0.57.2-0.20260111184307-eec823306178/go.mod h1:N1WIjPphkqs4efXWuyDNQ6OjjIK04vM3h+bEgwV+eVU= -github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= -github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU= +github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= -github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= -github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= +github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= @@ -29,18 +29,18 @@ github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9 github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= -github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= -github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= -github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= -github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg= +github.com/gin-contrib/gzip v1.2.6/go.mod h1:BQy8/+JApnRjAVUplSGZiVtD2k8GmIE2e9rYu/hLzzU= +github.com/gin-contrib/sessions v1.1.0 h1:00mhHfNEGF5sP2fwxa98aRqj1FOJdL6IkR86n2hOiBo= +github.com/gin-contrib/sessions v1.1.0/go.mod h1:TyYZDIs6qCQg2SOoYPgMT9pAkmZceVNEJMcv5qbIy60= +github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko= +github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= -github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= +github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= +github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -54,10 +54,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= -github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= @@ -107,8 +107,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -117,12 +117,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= -github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= -github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4= +github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -138,8 +138,8 @@ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0C github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -150,18 +150,18 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= -github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= -github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= +github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs= +github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sagernet/sing v0.8.1 h1:Li+zg4xdiMsvdX4j50TPqmSG8LF/TB9US2qlAN40izU= -github.com/sagernet/sing v0.8.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.8.4 h1:Fj+jlY3F8vhcRfz/G/P3Dwcs5wqnmyNPT7u1RVVmjFI= +github.com/sagernet/sing v0.8.4/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM= github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= -github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= -github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -195,10 +195,10 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= -github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM= -github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ= -github.com/xtls/xray-core v1.260206.0 h1:gY8IV6u76CW93txL9QmacgZ0Udxr2Q3e9qUxXAhdHqI= -github.com/xtls/xray-core v1.260206.0/go.mod h1:GyFIgVGRJkt3eyV/NMcdxOKXcJPqGGpyupHzy16uJhU= +github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8= +github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= +github.com/xtls/xray-core v1.260327.0 h1:g4TzxMwyPrxslZh6uD+FiG3lXKTrnNO+b4ky2OhogHE= +github.com/xtls/xray-core v1.260327.0/go.mod h1:OXMlhBloFry8mw0KwWLWLd3RQyXJzEYsCGlgsX36h60= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= @@ -225,42 +225,42 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= -golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 169b216d7eba1641f46dd8ab4b64dfb8f0a5cc2f Mon Sep 17 00:00:00 2001 From: Yunheng Liu <121078488+Kookiejarz@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:59:48 -0400 Subject: [PATCH 28/36] perf: replace /dev/urandom | tr with openssl rand to fix CPU spike (#3887) --- install.sh | 21 +++++++++++---------- update.sh | 21 +++++++++++---------- x-ui.sh | 5 +++-- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/install.sh b/install.sh index 9d1aeb6b..af6b8a51 100644 --- a/install.sh +++ b/install.sh @@ -76,37 +76,38 @@ is_port_in_use() { install_base() { case "${release}" in ubuntu | debian | armbian) - apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates + apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates + dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates openssl ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update && yum install -y curl tar tzdata socat ca-certificates + yum -y update && yum install -y curl tar tzdata socat ca-certificates openssl else - dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates + dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates openssl fi ;; arch | manjaro | parch) - pacman -Syu && pacman -Syu --noconfirm curl tar tzdata socat ca-certificates + pacman -Syu && pacman -Syu --noconfirm curl tar tzdata socat ca-certificates openssl ;; opensuse-tumbleweed | opensuse-leap) - zypper refresh && zypper -q install -y curl tar timezone socat ca-certificates + zypper refresh && zypper -q install -y curl tar timezone socat ca-certificates openssl ;; alpine) - apk update && apk add curl tar tzdata socat ca-certificates + apk update && apk add curl tar tzdata socat ca-certificates openssl ;; *) - apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates + apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates openssl ;; esac } gen_random_string() { local length="$1" - local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1 && apt-get install -y -q curl tar tzdata socat >/dev/null 2>&1 + apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata socat openssl >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1 + dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat openssl >/dev/null 2>&1 ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata socat >/dev/null 2>&1 + yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata socat openssl >/dev/null 2>&1 else - dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat >/dev/null 2>&1 + dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat openssl >/dev/null 2>&1 fi ;; arch | manjaro | parch) - pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata socat >/dev/null 2>&1 + pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata socat openssl >/dev/null 2>&1 ;; opensuse-tumbleweed | opensuse-leap) - zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone socat >/dev/null 2>&1 + zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone socat openssl >/dev/null 2>&1 ;; alpine) - apk update >/dev/null 2>&1 && apk add curl tar tzdata socat >/dev/null 2>&1 + apk update >/dev/null 2>&1 && apk add curl tar tzdata socat openssl>/dev/null 2>&1 ;; *) - apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata socat >/dev/null 2>&1 + apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata socat openssl >/dev/null 2>&1 ;; esac } diff --git a/x-ui.sh b/x-ui.sh index e9e2d831..e26dcce2 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -243,8 +243,9 @@ reset_user() { gen_random_string() { local length="$1" - local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' Date: Fri, 17 Apr 2026 06:19:45 -0400 Subject: [PATCH 29/36] Fix SSL domain setup on reinstall: reuse existing certs and avoid false success/failure logs (#4004) * perf: replace /dev/urandom | tr with openssl rand to fix CPU spike * fix: add cron to default package installation and improve SSL certificate handling * Reworked `--installcert` success criteria, cleanup behavior adjusted. --- install.sh | 95 +++++++++++++++++++++++++++++++--------------------- update.sh | 97 +++++++++++++++++++++++++++++++++--------------------- x-ui.sh | 59 ++++++++++++++++++++------------- 3 files changed, 154 insertions(+), 97 deletions(-) diff --git a/install.sh b/install.sh index af6b8a51..a4c71460 100644 --- a/install.sh +++ b/install.sh @@ -79,26 +79,26 @@ install_base() { apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates openssl + dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update && yum install -y curl tar tzdata socat ca-certificates openssl + yum -y update && yum install -y cronie curl tar tzdata socat ca-certificates openssl else - dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates openssl + dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl fi ;; arch | manjaro | parch) - pacman -Syu && pacman -Syu --noconfirm curl tar tzdata socat ca-certificates openssl + pacman -Syu && pacman -Syu --noconfirm cronie curl tar tzdata socat ca-certificates openssl ;; opensuse-tumbleweed | opensuse-leap) - zypper refresh && zypper -q install -y curl tar timezone socat ca-certificates openssl + zypper refresh && zypper -q install -y cron curl tar timezone socat ca-certificates openssl ;; alpine) - apk update && apk add curl tar tzdata socat ca-certificates openssl + apk update && apk add dcron curl tar tzdata socat ca-certificates openssl ;; *) - apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates openssl + apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl ;; esac } @@ -379,15 +379,15 @@ ssl_cert_issue() { break done echo -e "${green}Your domain is: ${domain}, checking it...${plain}" + SSL_ISSUED_DOMAIN="${domain}" - # check if there already exists a certificate - local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') - if [ "${currentCert}" == "${domain}" ]; then - local certInfo=$(~/.acme.sh/acme.sh --list) - echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}" - echo -e "${yellow}Current certificate details:${plain}" - echo "$certInfo" - return 1 + # detect existing certificate and reuse it if present + local cert_exists=0 + if ~/.acme.sh/acme.sh --list 2>/dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then + cert_exists=1 + local certInfo=$(~/.acme.sh/acme.sh --list 2>/dev/null | grep -F "${domain}") + echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}" + [[ -n "${certInfo}" ]] && echo "$certInfo" else echo -e "${green}Your domain is ready for issuing certificates now...${plain}" fi @@ -414,16 +414,20 @@ ssl_cert_issue() { echo -e "${yellow}Stopping panel temporarily...${plain}" systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null - # issue the certificate - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force - ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force - if [ $? -ne 0 ]; then - echo -e "${red}Issuing certificate failed, please check logs.${plain}" - rm -rf ~/.acme.sh/${domain} - systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null - return 1 + if [[ ${cert_exists} -eq 0 ]]; then + # issue the certificate + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force + if [ $? -ne 0 ]; then + echo -e "${red}Issuing certificate failed, please check logs.${plain}" + rm -rf ~/.acme.sh/${domain} + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + return 1 + else + echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" + fi else - echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" + echo -e "${green}Using existing certificate, installing certificates...${plain}" fi # Setup reload command @@ -453,17 +457,27 @@ ssl_cert_issue() { fi # install the certificate - ~/.acme.sh/acme.sh --installcert -d ${domain} \ + local installOutput="" + installOutput=$(~/.acme.sh/acme.sh --installcert -d ${domain} \ --key-file /root/cert/${domain}/privkey.pem \ - --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" + --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" 2>&1) + local installRc=$? + echo "${installOutput}" - if [ $? -ne 0 ]; then + local installWroteFiles=0 + if echo "${installOutput}" | grep -q "Installing key to:" && echo "${installOutput}" | grep -q "Installing full chain to:"; then + installWroteFiles=1 + fi + + if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && ( ${installRc} -eq 0 || ${installWroteFiles} -eq 1 ) ]]; then + echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" + else echo -e "${red}Installing certificate failed, exiting.${plain}" - rm -rf ~/.acme.sh/${domain} + if [[ ${cert_exists} -eq 0 ]]; then + rm -rf ~/.acme.sh/${domain} + fi systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null return 1 - else - echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" fi # enable auto-renew @@ -536,14 +550,21 @@ prompt_and_setup_ssl() { 1) # User chose Let's Encrypt domain option echo -e "${green}Using Let's Encrypt for domain certificate...${plain}" - ssl_cert_issue - # Extract the domain that was used from the certificate - local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') - if [[ -n "${cert_domain}" ]]; then - SSL_HOST="${cert_domain}" - echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" + if ssl_cert_issue; then + local cert_domain="${SSL_ISSUED_DOMAIN}" + if [[ -z "${cert_domain}" ]]; then + cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') + fi + + if [[ -n "${cert_domain}" ]]; then + SSL_HOST="${cert_domain}" + echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" + else + echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" + SSL_HOST="${server_ip}" + fi else - echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" + echo -e "${red}SSL certificate setup failed for domain mode.${plain}" SSL_HOST="${server_ip}" fi ;; diff --git a/update.sh b/update.sh index b9cb3ddc..654b7748 100755 --- a/update.sh +++ b/update.sh @@ -109,29 +109,29 @@ install_base() { echo -e "${green}Updating and install dependency packages...${plain}" case "${release}" in ubuntu | debian | armbian) - apt-get update >/dev/null 2>&1 && apt-get install -y -q curl tar tzdata socat openssl >/dev/null 2>&1 + apt-get update >/dev/null 2>&1 && apt-get install -y -q cron curl tar tzdata socat openssl >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat openssl >/dev/null 2>&1 + dnf -y update >/dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1 ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update >/dev/null 2>&1 && yum install -y -q curl tar tzdata socat openssl >/dev/null 2>&1 + yum -y update >/dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1 else - dnf -y update >/dev/null 2>&1 && dnf install -y -q curl tar tzdata socat openssl >/dev/null 2>&1 + dnf -y update >/dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1 fi ;; arch | manjaro | parch) - pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm curl tar tzdata socat openssl >/dev/null 2>&1 + pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm cronie curl tar tzdata socat openssl >/dev/null 2>&1 ;; opensuse-tumbleweed | opensuse-leap) - zypper refresh >/dev/null 2>&1 && zypper -q install -y curl tar timezone socat openssl >/dev/null 2>&1 + zypper refresh >/dev/null 2>&1 && zypper -q install -y cron curl tar timezone socat openssl >/dev/null 2>&1 ;; alpine) - apk update >/dev/null 2>&1 && apk add curl tar tzdata socat openssl>/dev/null 2>&1 + apk update >/dev/null 2>&1 && apk add dcron curl tar tzdata socat openssl>/dev/null 2>&1 ;; *) - apt-get update >/dev/null 2>&1 && apt install -y -q curl tar tzdata socat openssl >/dev/null 2>&1 + apt-get update >/dev/null 2>&1 && apt install -y -q cron curl tar tzdata socat openssl >/dev/null 2>&1 ;; esac } @@ -402,15 +402,15 @@ ssl_cert_issue() { break done echo -e "${green}Your domain is: ${domain}, checking it...${plain}" + SSL_ISSUED_DOMAIN="${domain}" - # check if there already exists a certificate - local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') - if [ "${currentCert}" == "${domain}" ]; then - local certInfo=$(~/.acme.sh/acme.sh --list) - echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}" - echo -e "${yellow}Current certificate details:${plain}" - echo "$certInfo" - return 1 + # detect existing certificate and reuse it if present + local cert_exists=0 + if ~/.acme.sh/acme.sh --list 2>/dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then + cert_exists=1 + local certInfo=$(~/.acme.sh/acme.sh --list 2>/dev/null | grep -F "${domain}") + echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}" + [[ -n "${certInfo}" ]] && echo "$certInfo" else echo -e "${green}Your domain is ready for issuing certificates now...${plain}" fi @@ -437,16 +437,20 @@ ssl_cert_issue() { echo -e "${yellow}Stopping panel temporarily...${plain}" systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null - # issue the certificate - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force - ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force - if [ $? -ne 0 ]; then - echo -e "${red}Issuing certificate failed, please check logs.${plain}" - rm -rf ~/.acme.sh/${domain} - systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null - return 1 + if [[ ${cert_exists} -eq 0 ]]; then + # issue the certificate + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force + if [ $? -ne 0 ]; then + echo -e "${red}Issuing certificate failed, please check logs.${plain}" + rm -rf ~/.acme.sh/${domain} + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + return 1 + else + echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" + fi else - echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" + echo -e "${green}Using existing certificate, installing certificates...${plain}" fi # Setup reload command @@ -476,17 +480,27 @@ ssl_cert_issue() { fi # install the certificate - ~/.acme.sh/acme.sh --installcert -d ${domain} \ + local installOutput="" + installOutput=$(~/.acme.sh/acme.sh --installcert -d ${domain} \ --key-file /root/cert/${domain}/privkey.pem \ - --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" + --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" 2>&1) + local installRc=$? + echo "${installOutput}" - if [ $? -ne 0 ]; then + local installWroteFiles=0 + if echo "${installOutput}" | grep -q "Installing key to:" && echo "${installOutput}" | grep -q "Installing full chain to:"; then + installWroteFiles=1 + fi + + if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && ( ${installRc} -eq 0 || ${installWroteFiles} -eq 1 ) ]]; then + echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" + else echo -e "${red}Installing certificate failed, exiting.${plain}" - rm -rf ~/.acme.sh/${domain} + if [[ ${cert_exists} -eq 0 ]]; then + rm -rf ~/.acme.sh/${domain} + fi systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null return 1 - else - echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" fi # enable auto-renew @@ -556,14 +570,21 @@ prompt_and_setup_ssl() { 1) # User chose Let's Encrypt domain option echo -e "${green}Using Let's Encrypt for domain certificate...${plain}" - ssl_cert_issue - # Extract the domain that was used from the certificate - local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') - if [[ -n "${cert_domain}" ]]; then - SSL_HOST="${cert_domain}" - echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" + if ssl_cert_issue; then + local cert_domain="${SSL_ISSUED_DOMAIN}" + if [[ -z "${cert_domain}" ]]; then + cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') + fi + + if [[ -n "${cert_domain}" ]]; then + SSL_HOST="${cert_domain}" + echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" + else + echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" + SSL_HOST="${server_ip}" + fi else - echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" + echo -e "${red}SSL certificate setup failed for domain mode.${plain}" SSL_HOST="${server_ip}" fi ;; diff --git a/x-ui.sh b/x-ui.sh index e26dcce2..9ce7a066 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -1371,14 +1371,15 @@ ssl_cert_issue() { break done LOGD "Your domain is: ${domain}, checking it..." + SSL_ISSUED_DOMAIN="${domain}" - # check if there already exists a certificate - local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') - if [ "${currentCert}" == "${domain}" ]; then - local certInfo=$(~/.acme.sh/acme.sh --list) - LOGE "System already has certificates for this domain. Cannot issue again. Current certificate details:" - LOGI "$certInfo" - exit 1 + # detect existing certificate and reuse it if present + local cert_exists=0 + if ~/.acme.sh/acme.sh --list 2>/dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then + cert_exists=1 + local certInfo=$(~/.acme.sh/acme.sh --list 2>/dev/null | grep -F "${domain}") + LOGI "Existing certificate found for ${domain}, will reuse it." + [[ -n "${certInfo}" ]] && LOGI "${certInfo}" else LOGI "Your domain is ready for issuing certificates now..." fi @@ -1401,15 +1402,19 @@ ssl_cert_issue() { fi LOGI "Will use port: ${WebPort} to issue certificates. Please make sure this port is open." - # issue the certificate - ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force - ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force - if [ $? -ne 0 ]; then - LOGE "Issuing certificate failed, please check logs." - rm -rf ~/.acme.sh/${domain} - exit 1 + if [[ ${cert_exists} -eq 0 ]]; then + # issue the certificate + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force + if [ $? -ne 0 ]; then + LOGE "Issuing certificate failed, please check logs." + rm -rf ~/.acme.sh/${domain} + exit 1 + else + LOGE "Issuing certificate succeeded, installing certificates..." + fi else - LOGE "Issuing certificate succeeded, installing certificates..." + LOGI "Using existing certificate, installing certificates..." fi reloadCmd="x-ui restart" @@ -1439,16 +1444,26 @@ ssl_cert_issue() { fi # install the certificate - ~/.acme.sh/acme.sh --installcert -d ${domain} \ + local installOutput="" + installOutput=$(~/.acme.sh/acme.sh --installcert -d ${domain} \ --key-file /root/cert/${domain}/privkey.pem \ - --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" + --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" 2>&1) + local installRc=$? + echo "${installOutput}" - if [ $? -ne 0 ]; then - LOGE "Installing certificate failed, exiting." - rm -rf ~/.acme.sh/${domain} - exit 1 - else + local installWroteFiles=0 + if echo "${installOutput}" | grep -q "Installing key to:" && echo "${installOutput}" | grep -q "Installing full chain to:"; then + installWroteFiles=1 + fi + + if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && ( ${installRc} -eq 0 || ${installWroteFiles} -eq 1 ) ]]; then LOGI "Installing certificate succeeded, enabling auto renew..." + else + LOGE "Installing certificate failed, exiting." + if [[ ${cert_exists} -eq 0 ]]; then + rm -rf ~/.acme.sh/${domain} + fi + exit 1 fi # enable auto-renew From fec714a2431c482024a0952982fa36f38935e7ed Mon Sep 17 00:00:00 2001 From: lolka1333 Date: Sun, 19 Apr 2026 22:01:00 +0300 Subject: [PATCH 30/36] fix: enhance WebSocket stability, resolve XHTTP configurations and fix UI loading shifts (#3997) * feat: implement real-time traffic monitoring and UI updates using a high-performance WebSocket hub and background job system * feat: add bulk client management support and improve inbound data handling * Fix bug * **Fixes & Changes:** 1. **Fixed XPadding Placement Dropdown**: - Added the missing `cookie` and `query` options to `xPaddingPlacement` (`stream_xhttp.html`). - *Why:* Previously, users wanting `cookie` obfuscation were forced to use the `header` placement string. This caused Xray-core to blindly intercept the entire monolithic HTTP Cookie header, failing internal padding-length validations and causing the inbound to silently drop the connection. 2. **Fixed Uplink Data Placement Validation**: - Replaced the unsupported `query` option with `cookie` in `uplinkDataPlacement`. - *Why:* Xray-core's `transport_internet.go` explicitly forbids `query` as an uplink placement option. Selecting it from the UI previously sent a payload that would cause Xray-core to instantly throw an `unsupported uplink data placement: query` panic. Adding `cookie` perfectly aligns the UI with Xray-core restrictions. ### Related Issues - Resolves #3992 * This commit fixes structural payload issues preventing XHTTP from functioning correctly and eliminates WebSocket log spam. - **[Fix X-Padding UI]** Added missing `cookie` and `query` options to X-Padding Placement. Fixes the issue where using Cookie fallback triggers whole HTTP Cookie header interception and silent drop in Xray-core. (Resolves [#3992](https://github.com/MHSanaei/3x-ui/issues/3992)) - **[Fix Uplink Data Options]** Replaced the invalid `query` option with `cookie` in Uplink Data Placement dropdown to prevent Xray-core backend panic `unsupported uplink data placement: query`. - **[Fix WebSockets Spam]** Boosted `maxMessageSize` boundary to 100MB and gracefully handled fallback fetch signals via `broadcastInvalidate` to avoid buffer dropping spam. (Resolves [#3984](https://github.com/MHSanaei/3x-ui/issues/3984)) * Fix * gofmt * fix(websocket): resolve channel race condition and graceful shutdown deadlock * Fix: inbounds switch * Change max quantity from 10000 to 500 * fix --- docker-compose.yml | 2 +- install.sh | 2 +- update.sh | 2 +- web/assets/js/model/dbinbound.js | 25 +++- web/controller/websocket.go | 6 +- web/html/form/stream/stream_xhttp.html | 3 + web/html/inbounds.html | 152 +++++++++++++++++++------ web/html/index.html | 6 +- web/html/modals/client_bulk_modal.html | 6 +- web/html/modals/client_modal.html | 8 +- web/html/settings.html | 7 +- web/html/xray.html | 15 ++- web/job/xray_traffic_job.go | 32 +++--- web/service/xray.go | 40 ++++--- web/websocket/hub.go | 96 ++++++++++------ web/websocket/notifier.go | 21 ++++ 16 files changed, 291 insertions(+), 132 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 198df198..53784309 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,4 +13,4 @@ services: XUI_ENABLE_FAIL2BAN: "true" tty: true network_mode: host - restart: unless-stopped + restart: unless-stopped \ No newline at end of file diff --git a/install.sh b/install.sh index a4c71460..aa4f5dfb 100644 --- a/install.sh +++ b/install.sh @@ -602,7 +602,7 @@ prompt_and_setup_ssl() { # 3.1 Request Domain to compose Panel URL later read -rp "Please enter domain name certificate issued for: " custom_domain - custom_domain="${custom_domain// /}" # Убираем пробелы + custom_domain="${custom_domain// /}" # Remove spaces # 3.2 Loop for Certificate Path while true; do diff --git a/update.sh b/update.sh index 654b7748..c9985d82 100755 --- a/update.sh +++ b/update.sh @@ -630,7 +630,7 @@ prompt_and_setup_ssl() { # 3.1 Request Domain to compose Panel URL later read -rp "Please enter domain name certificate issued for: " custom_domain - custom_domain="${custom_domain// /}" # Убираем пробелы + custom_domain="${custom_domain// /}" # Remove spaces # 3.2 Loop for Certificate Path while true; do diff --git a/web/assets/js/model/dbinbound.js b/web/assets/js/model/dbinbound.js index befc618e..c347a7eb 100644 --- a/web/assets/js/model/dbinbound.js +++ b/web/assets/js/model/dbinbound.js @@ -90,7 +90,16 @@ class DBInbound { return this.expiryTime < new Date().getTime(); } + invalidateCache() { + this._cachedInbound = null; + this._clientStatsMap = null; + } + toInbound() { + if (this._cachedInbound) { + return this._cachedInbound; + } + let settings = {}; if (!ObjectUtil.isEmpty(this.settings)) { settings = JSON.parse(this.settings); @@ -116,7 +125,21 @@ class DBInbound { sniffing: sniffing, clientStats: this.clientStats, }; - return Inbound.fromJson(config); + + this._cachedInbound = Inbound.fromJson(config); + return this._cachedInbound; + } + + getClientStats(email) { + if (!this._clientStatsMap) { + this._clientStatsMap = new Map(); + if (this.clientStats && Array.isArray(this.clientStats)) { + for (const stats of this.clientStats) { + this._clientStatsMap.set(stats.email, stats); + } + } + } + return this._clientStatsMap.get(email); } isMultiUser() { diff --git a/web/controller/websocket.go b/web/controller/websocket.go index 0ad5c845..dfb59709 100644 --- a/web/controller/websocket.go +++ b/web/controller/websocket.go @@ -30,8 +30,10 @@ const ( ) var upgrader = ws.Upgrader{ - ReadBufferSize: 4096, // Increased from 1024 for better performance - WriteBufferSize: 4096, // Increased from 1024 for better performance + ReadBufferSize: 32768, + WriteBufferSize: 32768, + EnableCompression: true, // Negotiate permessage-deflate compression if the client supports it + CheckOrigin: func(r *http.Request) bool { // Check origin for security origin := r.Header.Get("Origin") diff --git a/web/html/form/stream/stream_xhttp.html b/web/html/form/stream/stream_xhttp.html index 447612c9..8fe836d0 100644 --- a/web/html/form/stream/stream_xhttp.html +++ b/web/html/form/stream/stream_xhttp.html @@ -70,6 +70,8 @@ queryInHeader header + cookie + query @@ -127,6 +129,7 @@ Default (body) body header + cookie query diff --git a/web/html/inbounds.html b/web/html/inbounds.html index b945da90..231fc0c0 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -6,7 +6,7 @@ - + @@ -14,10 +14,7 @@ - - - +
@@ -1101,7 +1098,10 @@ } data.sniffing = inbound.sniffing.toString(); - await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, data, inModal); + const formData = new FormData(); + Object.keys(data).forEach(key => formData.append(key, data[key])); + + await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, formData, inModal); }, openAddClient(dbInboundId) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); @@ -1291,9 +1291,36 @@ infoModal.show(newDbInbound, index); }, switchEnable(dbInboundId, state) { - dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); + let dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); + if (!dbInbound) return; dbInbound.enable = state; - this.submit(`/panel/api/inbounds/update/${dbInboundId}`, dbInbound); + let inbound = dbInbound.toInbound(); + const data = { + up: dbInbound.up, + down: dbInbound.down, + total: dbInbound.total, + remark: dbInbound.remark, + enable: dbInbound.enable, + expiryTime: dbInbound.expiryTime, + trafficReset: dbInbound.trafficReset, + lastTrafficResetTime: dbInbound.lastTrafficResetTime, + + listen: inbound.listen, + port: inbound.port, + protocol: inbound.protocol, + settings: inbound.settings.toString(), + }; + if (inbound.canEnableStream()) { + data.streamSettings = inbound.stream.toString(); + } else if (inbound.stream?.sockopt) { + data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2); + } + data.sniffing = inbound.sniffing.toString(); + + const formData = new FormData(); + Object.keys(data).forEach(key => formData.append(key, data[key])); + + this.submit(`/panel/api/inbounds/update/${dbInboundId}`, formData); }, async switchEnableClient(dbInboundId, client) { this.loading() @@ -1367,42 +1394,54 @@ isExpiry(dbInbound, index) { return dbInbound.toInbound().isExpiry(index); }, + getClientStats(dbInbound, email) { + if (!dbInbound) return null; + if (!dbInbound._clientStatsMap) { + dbInbound._clientStatsMap = new Map(); + if (dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) { + for (const stats of dbInbound.clientStats) { + dbInbound._clientStatsMap.set(stats.email, stats); + } + } + } + return dbInbound._clientStatsMap.get(email); + }, getUpStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); return clientStats ? clientStats.up : 0; }, getDownStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); return clientStats ? clientStats.down : 0; }, getSumStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); return clientStats ? clientStats.up + clientStats.down : 0; }, getAllTimeClient(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return 0; return clientStats.allTime || (clientStats.up + clientStats.down); }, getRemStats(dbInbound, email) { - if (email.length == 0) return 0; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 0; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return 0; - remained = clientStats.total - (clientStats.up + clientStats.down); + let remained = clientStats.total - (clientStats.up + clientStats.down); return remained > 0 ? remained : 0; }, clientStatsColor(dbInbound, email) { - if (email.length == 0) return ColorUtils.clientUsageColor(); - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return ColorUtils.clientUsageColor(); + let clientStats = this.getClientStats(dbInbound, email); return ColorUtils.clientUsageColor(clientStats, app.trafficDiff) }, statsProgress(dbInbound, email) { - if (email.length == 0) return 100; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return 100; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return 0; if (clientStats.total == 0) return 100; return 100 * (clientStats.down + clientStats.up) / clientStats.total; @@ -1415,11 +1454,11 @@ return 100 * (1 - (remainedSeconds / resetSeconds)); }, statsExpColor(dbInbound, email) { - if (email.length == 0) return '#7a316f'; - clientStats = dbInbound.clientStats.find(stats => stats.email === email); + if (!email || email.length == 0) return '#7a316f'; + let clientStats = this.getClientStats(dbInbound, email); if (!clientStats) return '#7a316f'; - statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total); - expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime); + let statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total); + let expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime); switch (true) { case statsColor == "red" || expColor == "red": return "#cf3c3c"; // Red @@ -1432,12 +1471,12 @@ } }, isClientEnabled(dbInbound, email) { - clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null; + let clientStats = dbInbound ? this.getClientStats(dbInbound, email) : null; return clientStats ? clientStats['enable'] : true; }, isClientDepleted(dbInbound, email) { - if (!email || !dbInbound || !dbInbound.clientStats) return false; - const stats = dbInbound.clientStats.find(s => s.email === email); + if (!email || !dbInbound) return false; + const stats = this.getClientStats(dbInbound, email); if (!stats) return false; const total = stats.total ?? 0; const used = (stats.up ?? 0) + (stats.down ?? 0); @@ -1557,12 +1596,18 @@ pagination(obj) { if (this.pageSize > 0 && obj.length > this.pageSize) { // Set page options based on object size - sizeOptions = []; - for (i = this.pageSize; i <= obj.length; i = i + this.pageSize) { - sizeOptions.push(i.toString()); + let sizeOptions = [this.pageSize.toString()]; + const increments = [2, 5, 10, 20]; + for (const m of increments) { + const val = this.pageSize * m; + if (val < obj.length && val <= 1000) { + sizeOptions.push(val.toString()); + } } // Add option to see all in one page - sizeOptions.push(i.toString()); + if (!sizeOptions.includes(obj.length.toString())) { + sizeOptions.push(obj.length.toString()); + } p = { showSizeChanger: true, @@ -1605,11 +1650,25 @@ } }); + // Listen for invalidate signals (sent when payload is too large for WebSocket) + // The server sends a lightweight notification and we re-fetch via REST API + let invalidateTimer = null; + window.wsClient.on('invalidate', (payload) => { + if (payload && (payload.type === 'inbounds' || payload.type === 'traffic')) { + // Debounce to avoid flooding the REST API with multiple invalidate signals + if (invalidateTimer) clearTimeout(invalidateTimer); + invalidateTimer = setTimeout(() => { + invalidateTimer = null; + this.getDBInbounds(); + }, 1000); + } + }); + // Listen for traffic updates window.wsClient.on('traffic', (payload) => { // Note: Do NOT update total consumed traffic (stats.up, stats.down) from this event // because clientTraffics contains delta/incremental values, not total accumulated values. - // Total traffic is updated via the 'inbounds' event which contains accumulated values from database. + // Total traffic is updated via the 'inbounds' WebSocket event (or 'invalidate' fallback for large panels). // Update online clients list in real-time if (payload && Array.isArray(payload.onlineClients)) { @@ -1627,22 +1686,27 @@ this.onlineClients = nextOnlineClients; if (onlineChanged) { // Recalculate client counts to update online status + // Use $set for Vue 2 reactivity — direct array index assignment is not reactive this.dbInbounds.forEach(dbInbound => { const inbound = this.inbounds.find(ib => ib.id === dbInbound.id); if (inbound && this.clientCount[dbInbound.id]) { - this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound); + this.$set(this.clientCount, dbInbound.id, this.getClientCounts(dbInbound, inbound)); } }); + // Always trigger UI refresh — not just when filter is enabled if (this.enableFilter) { this.filterInbounds(); + } else { + this.searchInbounds(this.searchKey); } } } // Update last online map in real-time + // Replace entirely (server sends the full map) to avoid unbounded growth from deleted clients if (payload && payload.lastOnlineMap && typeof payload.lastOnlineMap === 'object') { - this.lastOnlineMap = { ...this.lastOnlineMap, ...payload.lastOnlineMap }; + this.lastOnlineMap = payload.lastOnlineMap; } }); @@ -1697,4 +1761,18 @@ }, }); + {{ template "page/body_end" .}} \ No newline at end of file diff --git a/web/html/index.html b/web/html/index.html index bbbbb708..47645f7d 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -6,7 +6,7 @@ - + @@ -15,9 +15,7 @@