diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6c2a6989..d68ea808 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,8 @@ on:
- '**.go'
- 'go.mod'
- 'go.sum'
- - 'x-ui.service'
+ - 'x-ui.service.debian'
+ - 'x-ui.service.rhel'
jobs:
build:
@@ -78,14 +79,15 @@ jobs:
mkdir x-ui
cp xui-release x-ui/
- cp x-ui.service x-ui/
+ cp x-ui.service.debian x-ui/
+ cp x-ui.service.rhel x-ui/
cp x-ui.sh x-ui/
mv x-ui/xui-release x-ui/x-ui
mkdir x-ui/bin
cd x-ui/bin
# Download dependencies
- Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.12.2/"
+ Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.12.8/"
if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip
@@ -183,7 +185,7 @@ jobs:
cd x-ui\bin
# Download Xray for Windows
- $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.12.2/"
+ $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.12.8/"
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
Remove-Item "Xray-windows-64.zip"
diff --git a/go.mod b/go.mod
index 458eefcb..494e5890 100644
--- a/go.mod
+++ b/go.mod
@@ -9,22 +9,23 @@ require (
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.3.1
- github.com/nicksnyder/go-i18n/v2 v2.6.0
+ github.com/mymmrac/telego v1.4.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.25.11
+ github.com/shirou/gopsutil/v4 v4.25.12
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.68.0
github.com/xlzd/gotp v0.1.0
- github.com/xtls/xray-core v1.251202.0
+ github.com/xtls/xray-core v1.251208.0
go.uber.org/atomic v1.11.0
- golang.org/x/crypto v0.45.0
- golang.org/x/sys v0.38.0
- golang.org/x/text v0.31.0
- google.golang.org/grpc v1.77.0
+ golang.org/x/crypto v0.46.0
+ golang.org/x/sys v0.39.0
+ golang.org/x/text v0.32.0
+ google.golang.org/grpc v1.78.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
@@ -35,23 +36,22 @@ require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
- github.com/cloudflare/circl v1.6.1 // indirect
+ github.com/cloudflare/circl v1.6.2 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
- github.com/gabriel-vasile/mimetype v1.4.11 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // 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.28.0 // indirect
- github.com/goccy/go-yaml v1.19.0 // indirect
+ github.com/go-playground/validator/v10 v10.30.1 // indirect
+ github.com/goccy/go-yaml v1.19.1 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
- github.com/gorilla/websocket v1.5.3 // indirect
github.com/grbit/go-json v0.11.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@@ -62,18 +62,18 @@ require (
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mattn/go-sqlite3 v1.14.32 // indirect
- github.com/miekg/dns v1.1.68 // indirect
+ github.com/mattn/go-sqlite3 v1.14.33 // indirect
+ github.com/miekg/dns v1.1.69 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
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.57.1 // indirect
+ github.com/quic-go/quic-go v0.58.0 // indirect
github.com/refraction-networking/utls v1.8.1 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
- github.com/sagernet/sing v0.7.13 // indirect
+ github.com/sagernet/sing v0.7.14 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
@@ -82,22 +82,22 @@ require (
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
- github.com/valyala/fastjson v1.6.4 // indirect
+ github.com/valyala/fastjson v1.6.7 // 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
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.23.0 // indirect
- golang.org/x/mod v0.30.0 // indirect
- golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.18.0 // indirect
+ golang.org/x/mod v0.31.0 // indirect
+ golang.org/x/net v0.48.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
golang.org/x/time v0.14.0 // indirect
- golang.org/x/tools v0.39.0 // indirect
+ golang.org/x/tools v0.40.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-20251202230838-ff82c1b0f217 // indirect
- google.golang.org/protobuf v1.36.10 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
lukechampine.com/blake3 v1.4.1 // indirect
)
diff --git a/go.sum b/go.sum
index 287a33bb..e5127e10 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
-github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
-github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+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=
@@ -12,8 +12,8 @@ github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPII
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
-github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
-github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
+github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
+github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -24,8 +24,8 @@ github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mT
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
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/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
-github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
+github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
+github.com/gabriel-vasile/mimetype v1.4.12/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=
@@ -53,12 +53,12 @@ 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.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
-github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
+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/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
-github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
+github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -120,19 +120,19 @@ github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIi
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/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.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
-github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
-github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
-github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
+github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
+github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
+github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
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.3.1 h1:dI5D8LKWBw241W02LmJqoSLZXW3tuLokxVoNbIZUYQg=
-github.com/mymmrac/telego v1.3.1/go.mod h1:3D0h4jJ3OzubY/gI4xDIGx4jkY26fmcPyqx0Lq24+zI=
-github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
-github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
+github.com/mymmrac/telego v1.4.0 h1:z74W5lfOTgLplQXuZPjDsRvvvI0iQatO2gp/XZz7s3I=
+github.com/mymmrac/telego v1.4.0/go.mod h1:u9fKXZSOCOdMj6K0U69fQqeAvDE+2RGkHKkDksijp3o=
+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=
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=
@@ -147,8 +147,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
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.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
-github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
+github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
+github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@@ -157,14 +157,14 @@ 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.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
-github.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
+github.com/sagernet/sing v0.7.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
+github.com/sagernet/sing v0.7.14/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/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
-github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
-github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
+github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
+github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
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=
@@ -193,8 +193,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.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
-github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
-github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
+github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
+github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
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,8 +203,8 @@ 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.251202.0 h1:VwoBnq9IRTbYWEBhR0CqEw2cNjTlXYH6WxzKbSjx+XE=
-github.com/xtls/xray-core v1.251202.0/go.mod h1:kclzboEF0g6VBrp9/NXm8C0Aj64SDBt52OfthH1LSr4=
+github.com/xtls/xray-core v1.251208.0 h1:9jIXi+9KXnfmT5esSYNf9VAQlQkaAP8bG413B0eyAes=
+github.com/xtls/xray-core v1.251208.0/go.mod h1:kclzboEF0g6VBrp9/NXm8C0Aj64SDBt52OfthH1LSr4=
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,44 +225,46 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
+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.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
-golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
-golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
-golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
-golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
-golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
-golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
-golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
-golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
+golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+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=
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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
-golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
+golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
-golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
+golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
+golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
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-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
-google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
-google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
-google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
-google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
+google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
+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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/install.sh b/install.sh
index eabbad34..4dba8f17 100644
--- a/install.sh
+++ b/install.sh
@@ -8,6 +8,9 @@ plain='\033[0m'
cur_dir=$(pwd)
+xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}"
+xui_service="${XUI_SERVICE:=/etc/systemd/system}"
+
# check root
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
@@ -158,7 +161,7 @@ setup_ssl_certificate() {
local webKeyFile="/root/cert/${domain}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
- /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
+ ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
echo -e "${green}SSL certificate installed and configured successfully!${plain}"
return 0
else
@@ -215,15 +218,15 @@ EOF
fi
chmod 755 ${certDir}/* 2>/dev/null
- /usr/local/x-ui/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1
+ ${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1
echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}"
return 0
}
# Comprehensive manual SSL certificate issuance via acme.sh
ssl_cert_issue() {
- local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
- local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
+ local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
+ local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
# check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
@@ -366,7 +369,7 @@ ssl_cert_issue() {
local webKeyFile="/root/cert/${domain}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
- /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
+ ${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile"
echo -e "${green}Certificate paths set for the panel${plain}"
echo -e "${green}Certificate File: $webCertFile${plain}"
echo -e "${green}Private Key File: $webKeyFile${plain}"
@@ -451,11 +454,11 @@ prompt_and_setup_ssl() {
}
config_after_install() {
- local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
- local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
- local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
+ local existing_hasDefaultCredential=$(${xui_folder}/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
+ local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
+ local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
# Properly detect empty cert by checking if cert: line exists and has content after it
- local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
+ local existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
local URL_lists=(
"https://api4.ipify.org"
"https://ipv4.icanhazip.com"
@@ -487,7 +490,7 @@ config_after_install() {
echo -e "${yellow}Generated random port: ${config_port}${plain}"
fi
- /usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
+ ${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
echo ""
echo -e "${green}═══════════════════════════════════════════${plain}"
@@ -515,7 +518,7 @@ config_after_install() {
else
local config_webBasePath=$(gen_random_string 18)
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
- /usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}"
+ ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}"
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
# If the panel is already installed but no certificate is configured, prompt for SSL now
@@ -539,7 +542,7 @@ config_after_install() {
local config_password=$(gen_random_string 10)
echo -e "${yellow}Default credentials detected. Security update required...${plain}"
- /usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}"
+ ${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}"
echo -e "Generated new random login credentials:"
echo -e "###############################################"
echo -e "${green}Username: ${config_username}${plain}"
@@ -551,7 +554,7 @@ config_after_install() {
# Existing install: if no cert configured, prompt user to set domain or self-signed
# Properly detect empty cert by checking if cert: line exists and has content after it
- existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
+ existing_cert=$(${xui_folder}/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
if [[ -z "$existing_cert" ]]; then
echo ""
echo -e "${green}═══════════════════════════════════════════${plain}"
@@ -566,11 +569,11 @@ config_after_install() {
fi
fi
- /usr/local/x-ui/x-ui migrate
+ ${xui_folder}/x-ui migrate
}
install_x-ui() {
- cd /usr/local/
+ cd ${xui_folder%/x-ui}/
# Download resources
if [ $# == 0 ]; then
@@ -614,13 +617,13 @@ install_x-ui() {
fi
# Stop x-ui service and remove old resources
- if [[ -e /usr/local/x-ui/ ]]; then
+ if [[ -e ${xui_folder}/ ]]; then
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
- rm /usr/local/x-ui/ -rf
+ rm ${xui_folder}/ -rf
fi
# Extract resources and set permissions
@@ -643,6 +646,20 @@ install_x-ui() {
chmod +x /usr/bin/x-ui
mkdir -p /var/log/x-ui
config_after_install
+
+ # Etckeeper compatibility
+ if [ -d "/etc/.git" ]; then
+ if [ -f "/etc/.gitignore" ]; then
+ if ! grep -q "x-ui/x-ui.db" "/etc/.gitignore"; then
+ echo "" >> "/etc/.gitignore"
+ echo "x-ui/x-ui.db" >> "/etc/.gitignore"
+ echo -e "${green}Added x-ui.db to /etc/.gitignore for etckeeper${plain}"
+ fi
+ else
+ echo "x-ui/x-ui.db" > "/etc/.gitignore"
+ echo -e "${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}"
+ fi
+ fi
if [[ $release == "alpine" ]]; then
curl -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
@@ -654,7 +671,18 @@ install_x-ui() {
rc-update add x-ui
rc-service x-ui start
else
- cp -f x-ui.service /etc/systemd/system/
+ if [ -f "x-ui.service" ]; then
+ cp -f x-ui.service ${xui_service}/
+ else
+ case "${release}" in
+ ubuntu | debian | armbian)
+ cp -f x-ui.service.debian ${xui_service}/x-ui.service
+ ;;
+ *)
+ cp -f x-ui.service.rhel ${xui_service}/x-ui.service
+ ;;
+ esac
+ fi
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui
diff --git a/sub/subService.go b/sub/subService.go
index 55bddf7f..ade871df 100644
--- a/sub/subService.go
+++ b/sub/subService.go
@@ -179,9 +179,15 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
if inbound.Protocol != model.VMESS {
return ""
}
+ var address string
+ if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
+ address = s.address
+ } else {
+ address = inbound.Listen
+ }
obj := map[string]any{
"v": "2",
- "add": s.address,
+ "add": address,
"port": inbound.Port,
"type": "none",
}
@@ -317,7 +323,13 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
}
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
- address := s.address
+ var address string
+ if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
+ address = s.address
+ } else {
+ address = inbound.Listen
+ }
+
if inbound.Protocol != model.VLESS {
return ""
}
@@ -523,7 +535,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
}
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
- address := s.address
+ var address string
+ if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
+ address = s.address
+ } else {
+ address = inbound.Listen
+ }
if inbound.Protocol != model.Trojan {
return ""
}
@@ -719,7 +736,12 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
}
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
- address := s.address
+ var address string
+ if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
+ address = s.address
+ } else {
+ address = inbound.Listen
+ }
if inbound.Protocol != model.Shadowsocks {
return ""
}
diff --git a/update.sh b/update.sh
index 214463b3..f7b499ca 100755
--- a/update.sh
+++ b/update.sh
@@ -6,6 +6,9 @@ blue='\033[0;34m'
yellow='\033[0;33m'
plain='\033[0m'
+xui_folder="${XUI_MAIN_FOLDER:=/usr/local/x-ui}"
+xui_service="${XUI_SERVICE:=/etc/systemd/system}"
+
# Don't edit this config
b_source="${BASH_SOURCE[0]}"
while [ -h "$b_source" ]; do
@@ -479,13 +482,13 @@ prompt_and_setup_ssl() {
config_after_update() {
echo -e "${yellow}x-ui settings:${plain}"
- /usr/local/x-ui/x-ui setting -show true
- /usr/local/x-ui/x-ui migrate
+ ${xui_folder}/x-ui setting -show true
+ ${xui_folder}/x-ui migrate
# Properly detect empty cert by checking if cert: line exists and has content after it
- local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true 2>/dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
- local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
- local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
+ local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2>/dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
+ local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
+ local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
# Get server IP
local URL_lists=(
@@ -508,7 +511,7 @@ config_after_update() {
if [[ ${#existing_webBasePath} -lt 4 ]]; then
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
local config_webBasePath=$(gen_random_string 18)
- /usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}"
+ ${xui_folder}/x-ui setting -webBasePath "${config_webBasePath}"
existing_webBasePath="${config_webBasePath}"
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
fi
@@ -553,10 +556,10 @@ config_after_update() {
}
update_x-ui() {
- cd /usr/local/
+ cd ${xui_folder%/x-ui}/
- if [ -f "/usr/local/x-ui/x-ui" ]; then
- current_xui_version=$(/usr/local/x-ui/x-ui -v)
+ if [ -f "${xui_folder}/x-ui" ]; then
+ current_xui_version=$(${xui_folder}/x-ui -v)
echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
else
_fail "ERROR: Current x-ui version: unknown"
@@ -582,7 +585,7 @@ update_x-ui() {
fi
fi
- if [[ -e /usr/local/x-ui/ ]]; then
+ if [[ -e ${xui_folder}/ ]]; then
echo -e "${green}Stopping x-ui...${plain}"
if [[ $release == "alpine" ]]; then
if [ -f "/etc/init.d/x-ui" ]; then
@@ -595,11 +598,11 @@ update_x-ui() {
_fail "ERROR: x-ui service unit not installed."
fi
else
- if [ -f "/etc/systemd/system/x-ui.service" ]; then
+ if [ -f "${xui_service}/x-ui.service" ]; then
systemctl stop x-ui >/dev/null 2>&1
systemctl disable x-ui >/dev/null 2>&1
echo -e "${green}Removing old systemd unit version...${plain}"
- rm /etc/systemd/system/x-ui.service -f >/dev/null 2>&1
+ rm ${xui_service}/x-ui.service -f >/dev/null 2>&1
systemctl daemon-reload >/dev/null 2>&1
else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
@@ -607,15 +610,17 @@ update_x-ui() {
fi
fi
echo -e "${green}Removing old x-ui version...${plain}"
- rm /usr/bin/x-ui -f >/dev/null 2>&1
- rm /usr/local/x-ui/x-ui.service -f >/dev/null 2>&1
- rm /usr/local/x-ui/x-ui -f >/dev/null 2>&1
- rm /usr/local/x-ui/x-ui.sh -f >/dev/null 2>&1
+ rm ${xui_folder} -f >/dev/null 2>&1
+ rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1
+ rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1
+ rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1
+ rm ${xui_folder}/x-ui -f >/dev/null 2>&1
+ rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1
echo -e "${green}Removing old xray version...${plain}"
- rm /usr/local/x-ui/bin/xray-linux-amd64 -f >/dev/null 2>&1
+ rm ${xui_folder}/bin/xray-linux-amd64 -f >/dev/null 2>&1
echo -e "${green}Removing old README and LICENSE file...${plain}"
- rm /usr/local/x-ui/bin/README.md -f >/dev/null 2>&1
- rm /usr/local/x-ui/bin/LICENSE -f >/dev/null 2>&1
+ rm ${xui_folder}/bin/README.md -f >/dev/null 2>&1
+ rm ${xui_folder}/bin/LICENSE -f >/dev/null 2>&1
else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
_fail "ERROR: x-ui not installed."
@@ -645,16 +650,16 @@ update_x-ui() {
fi
fi
- chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1
+ chmod +x ${xui_folder}/x-ui.sh >/dev/null 2>&1
chmod +x /usr/bin/x-ui >/dev/null 2>&1
mkdir -p /var/log/x-ui >/dev/null 2>&1
echo -e "${green}Changing owner...${plain}"
- chown -R root:root /usr/local/x-ui >/dev/null 2>&1
+ chown -R root:root ${xui_folder} >/dev/null 2>&1
- if [ -f "/usr/local/x-ui/bin/config.json" ]; then
+ if [ -f "${xui_folder}/bin/config.json" ]; then
echo -e "${green}Changing on config file permissions...${plain}"
- chmod 640 /usr/local/x-ui/bin/config.json >/dev/null 2>&1
+ chmod 640 ${xui_folder}/bin/config.json >/dev/null 2>&1
fi
if [[ $release == "alpine" ]]; then
@@ -671,9 +676,22 @@ update_x-ui() {
rc-update add x-ui >/dev/null 2>&1
rc-service x-ui start >/dev/null 2>&1
else
- echo -e "${green}Installing systemd unit...${plain}"
- cp -f x-ui.service /etc/systemd/system/ >/dev/null 2>&1
- chown root:root /etc/systemd/system/x-ui.service >/dev/null 2>&1
+ if [ -f "x-ui.service" ]; then
+ echo -e "${green}Installing systemd unit...${plain}"
+ cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1
+ else
+ case "${release}" in
+ ubuntu | debian | armbian)
+ echo -e "${green}Installing debian-like systemd unit...${plain}"
+ cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1
+ ;;
+ *)
+ echo -e "${green}Installing rhel-like systemd unit...${plain}"
+ cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1
+ ;;
+ esac
+ fi
+ chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1
systemctl daemon-reload >/dev/null 2>&1
systemctl enable x-ui >/dev/null 2>&1
systemctl start x-ui >/dev/null 2>&1
diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js
index 15410750..ba2304ef 100644
--- a/web/assets/js/model/inbound.js
+++ b/web/assets/js/model/inbound.js
@@ -857,6 +857,7 @@ class SockoptStreamSettings extends XrayCommonClass {
V6Only = false,
tcpWindowClamp = 600,
interfaceName = "",
+ trustedXForwardedFor = [],
) {
super();
this.acceptProxyProtocol = acceptProxyProtocol;
@@ -875,6 +876,7 @@ class SockoptStreamSettings extends XrayCommonClass {
this.V6Only = V6Only;
this.tcpWindowClamp = tcpWindowClamp;
this.interfaceName = interfaceName;
+ this.trustedXForwardedFor = trustedXForwardedFor;
}
static fromJson(json = {}) {
@@ -896,11 +898,12 @@ class SockoptStreamSettings extends XrayCommonClass {
json.V6Only,
json.tcpWindowClamp,
json.interface,
+ json.trustedXForwardedFor || [],
);
}
toJson() {
- return {
+ const result = {
acceptProxyProtocol: this.acceptProxyProtocol,
tcpFastOpen: this.tcpFastOpen,
mark: this.mark,
@@ -918,6 +921,10 @@ class SockoptStreamSettings extends XrayCommonClass {
tcpWindowClamp: this.tcpWindowClamp,
interface: this.interfaceName,
};
+ if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
+ result.trustedXForwardedFor = this.trustedXForwardedFor;
+ }
+ return result;
}
}
@@ -1214,6 +1221,14 @@ class Inbound extends XrayCommonClass {
return false;
}
+ // Vision seed applies only when vision flow is selected
+ canEnableVisionSeed() {
+ if (!this.canEnableTlsFlow()) return false;
+ const clients = this.settings?.vlesses;
+ if (!Array.isArray(clients)) return false;
+ return clients.some(c => c?.flow === TLS_FLOW_CONTROL.VISION || c?.flow === TLS_FLOW_CONTROL.VISION_UDP443);
+ }
+
canEnableReality() {
if (![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
return ["tcp", "http", "grpc", "xhttp"].includes(this.network);
@@ -1870,6 +1885,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
encryption = "none",
fallbacks = [],
selectedAuth = undefined,
+ testseed = [900, 500, 900, 256],
) {
super(protocol);
this.vlesses = vlesses;
@@ -1877,6 +1893,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
this.encryption = encryption;
this.fallbacks = fallbacks;
this.selectedAuth = selectedAuth;
+ this.testseed = testseed;
}
addFallback() {
@@ -1888,13 +1905,20 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
}
static fromJson(json = {}) {
+ // Ensure testseed is always initialized as an array
+ let testseed = [900, 500, 900, 256];
+ if (json.testseed && Array.isArray(json.testseed) && json.testseed.length >= 4) {
+ testseed = json.testseed;
+ }
+
const obj = new Inbound.VLESSSettings(
Protocols.VLESS,
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption,
json.encryption,
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
- json.selectedAuth
+ json.selectedAuth,
+ testseed
);
return obj;
}
@@ -1920,6 +1944,10 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
json.selectedAuth = this.selectedAuth;
}
+ if (this.testseed && this.testseed.length >= 4) {
+ json.testseed = this.testseed;
+ }
+
return json;
}
diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js
index c727abae..295ac812 100644
--- a/web/assets/js/model/outbound.js
+++ b/web/assets/js/model/outbound.js
@@ -432,6 +432,7 @@ class SockoptStreamSettings extends CommonClass {
tcpMptcp = false,
penetrate = false,
addressPortStrategy = Address_Port_Strategy.NONE,
+ trustedXForwardedFor = [],
) {
super();
this.dialerProxy = dialerProxy;
@@ -440,6 +441,7 @@ class SockoptStreamSettings extends CommonClass {
this.tcpMptcp = tcpMptcp;
this.penetrate = penetrate;
this.addressPortStrategy = addressPortStrategy;
+ this.trustedXForwardedFor = trustedXForwardedFor;
}
static fromJson(json = {}) {
@@ -450,12 +452,13 @@ class SockoptStreamSettings extends CommonClass {
json.tcpKeepAliveInterval,
json.tcpMptcp,
json.penetrate,
- json.addressPortStrategy
+ json.addressPortStrategy,
+ json.trustedXForwardedFor || []
);
}
toJson() {
- return {
+ const result = {
dialerProxy: this.dialerProxy,
tcpFastOpen: this.tcpFastOpen,
tcpKeepAliveInterval: this.tcpKeepAliveInterval,
@@ -463,6 +466,10 @@ class SockoptStreamSettings extends CommonClass {
penetrate: this.penetrate,
addressPortStrategy: this.addressPortStrategy
};
+ if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
+ result.trustedXForwardedFor = this.trustedXForwardedFor;
+ }
+ return result;
}
}
@@ -614,6 +621,13 @@ class Outbound extends CommonClass {
return false;
}
+ // Vision seed applies only when vision flow is selected
+ canEnableVisionSeed() {
+ if (!this.canEnableTlsFlow()) return false;
+ const flow = this.settings?.flow;
+ return flow === TLS_FLOW_CONTROL.VISION || flow === TLS_FLOW_CONTROL.VISION_UDP443;
+ }
+
canEnableReality() {
if (![Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
return ["tcp", "http", "grpc", "xhttp"].includes(this.stream.network);
@@ -1050,13 +1064,15 @@ Outbound.VmessSettings = class extends CommonClass {
}
};
Outbound.VLESSSettings = class extends CommonClass {
- constructor(address, port, id, flow, encryption) {
+ constructor(address, port, id, flow, encryption, testpre = 0, testseed = [900, 500, 900, 256]) {
super();
this.address = address;
this.port = port;
this.id = id;
this.flow = flow;
this.encryption = encryption;
+ this.testpre = testpre;
+ this.testseed = testseed;
}
static fromJson(json = {}) {
@@ -1066,18 +1082,27 @@ Outbound.VLESSSettings = class extends CommonClass {
json.port,
json.id,
json.flow,
- json.encryption
+ json.encryption,
+ json.testpre || 0,
+ json.testseed && json.testseed.length >= 4 ? json.testseed : [900, 500, 900, 256]
);
}
toJson() {
- return {
+ const result = {
address: this.address,
port: this.port,
id: this.id,
flow: this.flow,
encryption: this.encryption,
};
+ if (this.testpre > 0) {
+ result.testpre = this.testpre;
+ }
+ if (this.testseed && this.testseed.length >= 4) {
+ result.testseed = this.testseed;
+ }
+ return result;
}
};
Outbound.TrojanSettings = class extends CommonClass {
diff --git a/web/assets/js/websocket.js b/web/assets/js/websocket.js
new file mode 100644
index 00000000..5b8a3948
--- /dev/null
+++ b/web/assets/js/websocket.js
@@ -0,0 +1,145 @@
+/**
+ * WebSocket client for real-time updates
+ */
+class WebSocketClient {
+ constructor(basePath = '') {
+ this.basePath = basePath;
+ this.ws = null;
+ this.reconnectAttempts = 0;
+ this.maxReconnectAttempts = 10;
+ this.reconnectDelay = 1000;
+ this.listeners = new Map();
+ this.isConnected = false;
+ this.shouldReconnect = true;
+ }
+
+ connect() {
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+ return;
+ }
+
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ // Ensure basePath ends with '/' for proper URL construction
+ let basePath = this.basePath || '';
+ if (basePath && !basePath.endsWith('/')) {
+ basePath += '/';
+ }
+ const wsUrl = `${protocol}//${window.location.host}${basePath}ws`;
+
+ console.log('WebSocket connecting to:', wsUrl, 'basePath:', this.basePath);
+
+ try {
+ this.ws = new WebSocket(wsUrl);
+
+ this.ws.onopen = () => {
+ console.log('WebSocket connected');
+ this.isConnected = true;
+ this.reconnectAttempts = 0;
+ this.emit('connected');
+ };
+
+ this.ws.onmessage = (event) => {
+ try {
+ // Validate message size (prevent memory issues)
+ const maxMessageSize = 10 * 1024 * 1024; // 10MB
+ if (event.data && event.data.length > maxMessageSize) {
+ console.error('WebSocket message too large:', event.data.length, 'bytes');
+ this.ws.close();
+ return;
+ }
+
+ const message = JSON.parse(event.data);
+ if (!message || typeof message !== 'object') {
+ console.error('Invalid WebSocket message format');
+ return;
+ }
+
+ this.handleMessage(message);
+ } catch (e) {
+ console.error('Failed to parse WebSocket message:', e);
+ }
+ };
+
+ this.ws.onerror = (error) => {
+ console.error('WebSocket error:', error);
+ this.emit('error', error);
+ };
+
+ this.ws.onclose = () => {
+ console.log('WebSocket disconnected');
+ this.isConnected = false;
+ this.emit('disconnected');
+
+ if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
+ this.reconnectAttempts++;
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
+ console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
+ setTimeout(() => this.connect(), delay);
+ }
+ };
+ } catch (e) {
+ console.error('Failed to create WebSocket connection:', e);
+ this.emit('error', e);
+ }
+ }
+
+ handleMessage(message) {
+ const { type, payload, time } = message;
+
+ // Emit to specific type listeners
+ this.emit(type, payload, time);
+
+ // Emit to all listeners
+ this.emit('message', { type, payload, time });
+ }
+
+ on(event, callback) {
+ if (!this.listeners.has(event)) {
+ this.listeners.set(event, []);
+ }
+ this.listeners.get(event).push(callback);
+ }
+
+ off(event, callback) {
+ if (!this.listeners.has(event)) {
+ return;
+ }
+ const callbacks = this.listeners.get(event);
+ const index = callbacks.indexOf(callback);
+ if (index > -1) {
+ callbacks.splice(index, 1);
+ }
+ }
+
+ emit(event, ...args) {
+ if (this.listeners.has(event)) {
+ this.listeners.get(event).forEach(callback => {
+ try {
+ callback(...args);
+ } catch (e) {
+ console.error('Error in WebSocket event handler:', e);
+ }
+ });
+ }
+ }
+
+ disconnect() {
+ this.shouldReconnect = false;
+ if (this.ws) {
+ this.ws.close();
+ this.ws = null;
+ }
+ }
+
+ send(data) {
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+ this.ws.send(JSON.stringify(data));
+ } else {
+ console.warn('WebSocket is not connected');
+ }
+ }
+}
+
+// Create global WebSocket client instance
+// Safely get basePath from global scope (defined in page.html)
+window.wsClient = new WebSocketClient(typeof basePath !== 'undefined' ? basePath : '');
diff --git a/web/controller/inbound.go b/web/controller/inbound.go
index eeb160d6..8317de31 100644
--- a/web/controller/inbound.go
+++ b/web/controller/inbound.go
@@ -8,6 +8,7 @@ import (
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/session"
+ "github.com/mhsanaei/3x-ui/v2/web/websocket"
"github.com/gin-gonic/gin"
)
@@ -125,6 +126,9 @@ func (a *InboundController) addInbound(c *gin.Context) {
if needRestart {
a.xrayService.SetToNeedRestart()
}
+ // Broadcast inbounds update via WebSocket
+ inbounds, _ := a.inboundService.GetInbounds(user.Id)
+ websocket.BroadcastInbounds(inbounds)
}
// delInbound deletes an inbound configuration by its ID.
@@ -143,6 +147,10 @@ func (a *InboundController) delInbound(c *gin.Context) {
if needRestart {
a.xrayService.SetToNeedRestart()
}
+ // Broadcast inbounds update via WebSocket
+ user := session.GetLoginUser(c)
+ inbounds, _ := a.inboundService.GetInbounds(user.Id)
+ websocket.BroadcastInbounds(inbounds)
}
// updateInbound updates an existing inbound configuration.
@@ -169,6 +177,10 @@ func (a *InboundController) updateInbound(c *gin.Context) {
if needRestart {
a.xrayService.SetToNeedRestart()
}
+ // Broadcast inbounds update via WebSocket
+ user := session.GetLoginUser(c)
+ inbounds, _ := a.inboundService.GetInbounds(user.Id)
+ websocket.BroadcastInbounds(inbounds)
}
// getClientIps retrieves the IP addresses associated with a client by email.
diff --git a/web/controller/server.go b/web/controller/server.go
index 292ef338..5b39700e 100644
--- a/web/controller/server.go
+++ b/web/controller/server.go
@@ -9,6 +9,7 @@ import (
"github.com/mhsanaei/3x-ui/v2/web/global"
"github.com/mhsanaei/3x-ui/v2/web/service"
+ "github.com/mhsanaei/3x-ui/v2/web/websocket"
"github.com/gin-gonic/gin"
)
@@ -67,6 +68,8 @@ func (a *ServerController) refreshStatus() {
// collect cpu history when status is fresh
if a.lastStatus != nil {
a.serverService.AppendCpuSample(time.Now(), a.lastStatus.Cpu)
+ // Broadcast status update via WebSocket
+ websocket.BroadcastStatus(a.lastStatus)
}
}
@@ -155,9 +158,16 @@ func (a *ServerController) stopXrayService(c *gin.Context) {
err := a.serverService.StopXrayService()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err)
+ websocket.BroadcastXrayState("error", err.Error())
return
}
jsonMsg(c, I18nWeb(c, "pages.xray.stopSuccess"), err)
+ websocket.BroadcastXrayState("stop", "")
+ websocket.BroadcastNotification(
+ I18nWeb(c, "pages.xray.stopSuccess"),
+ "Xray service has been stopped",
+ "warning",
+ )
}
// restartXrayService restarts the Xray service.
@@ -165,9 +175,16 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
err := a.serverService.RestartXrayService()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.xray.restartError"), err)
+ websocket.BroadcastXrayState("error", err.Error())
return
}
jsonMsg(c, I18nWeb(c, "pages.xray.restartSuccess"), err)
+ websocket.BroadcastXrayState("running", "")
+ websocket.BroadcastNotification(
+ I18nWeb(c, "pages.xray.restartSuccess"),
+ "Xray service has been restarted successfully",
+ "success",
+ )
}
// getLogs retrieves the application logs based on count, level, and syslog filters.
diff --git a/web/controller/websocket.go b/web/controller/websocket.go
new file mode 100644
index 00000000..0ad5c845
--- /dev/null
+++ b/web/controller/websocket.go
@@ -0,0 +1,189 @@
+package controller
+
+import (
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/mhsanaei/3x-ui/v2/logger"
+ "github.com/mhsanaei/3x-ui/v2/util/common"
+ "github.com/mhsanaei/3x-ui/v2/web/session"
+ "github.com/mhsanaei/3x-ui/v2/web/websocket"
+
+ "github.com/gin-gonic/gin"
+ ws "github.com/gorilla/websocket"
+)
+
+const (
+ // Time allowed to write a message to the peer
+ writeWait = 10 * time.Second
+
+ // Time allowed to read the next pong message from the peer
+ pongWait = 60 * time.Second
+
+ // Send pings to peer with this period (must be less than pongWait)
+ pingPeriod = (pongWait * 9) / 10
+
+ // Maximum message size allowed from peer
+ maxMessageSize = 512
+)
+
+var upgrader = ws.Upgrader{
+ ReadBufferSize: 4096, // Increased from 1024 for better performance
+ WriteBufferSize: 4096, // Increased from 1024 for better performance
+ CheckOrigin: func(r *http.Request) bool {
+ // Check origin for security
+ origin := r.Header.Get("Origin")
+ if origin == "" {
+ // Allow connections without Origin header (same-origin requests)
+ return true
+ }
+ // Get the host from the request
+ host := r.Host
+ // Extract scheme and host from origin
+ originURL := origin
+ // Simple check: origin should match the request host
+ // This prevents cross-origin WebSocket hijacking
+ if strings.HasPrefix(originURL, "http://") || strings.HasPrefix(originURL, "https://") {
+ // Extract host from origin
+ originHost := strings.TrimPrefix(strings.TrimPrefix(originURL, "http://"), "https://")
+ if idx := strings.Index(originHost, "/"); idx != -1 {
+ originHost = originHost[:idx]
+ }
+ if idx := strings.Index(originHost, ":"); idx != -1 {
+ originHost = originHost[:idx]
+ }
+ // Compare hosts (without port)
+ requestHost := host
+ if idx := strings.Index(requestHost, ":"); idx != -1 {
+ requestHost = requestHost[:idx]
+ }
+ return originHost == requestHost || originHost == "" || requestHost == ""
+ }
+ return false
+ },
+}
+
+// WebSocketController handles WebSocket connections for real-time updates
+type WebSocketController struct {
+ BaseController
+ hub *websocket.Hub
+}
+
+// NewWebSocketController creates a new WebSocket controller
+func NewWebSocketController(hub *websocket.Hub) *WebSocketController {
+ return &WebSocketController{
+ hub: hub,
+ }
+}
+
+// HandleWebSocket handles WebSocket connections
+func (w *WebSocketController) HandleWebSocket(c *gin.Context) {
+ // Check authentication
+ if !session.IsLogin(c) {
+ logger.Warningf("Unauthorized WebSocket connection attempt from %s", getRemoteIp(c))
+ c.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+
+ // Upgrade connection to WebSocket
+ conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
+ if err != nil {
+ logger.Error("Failed to upgrade WebSocket connection:", err)
+ return
+ }
+
+ // Create client
+ clientID := uuid.New().String()
+ client := &websocket.Client{
+ ID: clientID,
+ Hub: w.hub,
+ Send: make(chan []byte, 512), // Increased from 256 to 512 to prevent overflow
+ Topics: make(map[websocket.MessageType]bool),
+ }
+
+ // Register client
+ w.hub.Register(client)
+ logger.Debugf("WebSocket client %s registered from %s", clientID, getRemoteIp(c))
+
+ // Start goroutines for reading and writing
+ go w.writePump(client, conn)
+ go w.readPump(client, conn)
+}
+
+// readPump pumps messages from the WebSocket connection to the hub
+func (w *WebSocketController) readPump(client *websocket.Client, conn *ws.Conn) {
+ defer func() {
+ if r := common.Recover("WebSocket readPump panic"); r != nil {
+ logger.Error("WebSocket readPump panic recovered:", r)
+ }
+ w.hub.Unregister(client)
+ conn.Close()
+ }()
+
+ conn.SetReadDeadline(time.Now().Add(pongWait))
+ conn.SetPongHandler(func(string) error {
+ conn.SetReadDeadline(time.Now().Add(pongWait))
+ return nil
+ })
+ conn.SetReadLimit(maxMessageSize)
+
+ for {
+ _, message, err := conn.ReadMessage()
+ if err != nil {
+ if ws.IsUnexpectedCloseError(err, ws.CloseGoingAway, ws.CloseAbnormalClosure) {
+ logger.Debugf("WebSocket read error for client %s: %v", client.ID, err)
+ }
+ break
+ }
+
+ // Validate message size
+ if len(message) > maxMessageSize {
+ logger.Warningf("WebSocket message from client %s exceeds max size: %d bytes", client.ID, len(message))
+ continue
+ }
+
+ // Handle incoming messages (e.g., subscription requests)
+ // For now, we'll just log them
+ logger.Debugf("Received WebSocket message from client %s: %s", client.ID, string(message))
+ }
+}
+
+// writePump pumps messages from the hub to the WebSocket connection
+func (w *WebSocketController) writePump(client *websocket.Client, conn *ws.Conn) {
+ ticker := time.NewTicker(pingPeriod)
+ defer func() {
+ if r := common.Recover("WebSocket writePump panic"); r != nil {
+ logger.Error("WebSocket writePump panic recovered:", r)
+ }
+ ticker.Stop()
+ conn.Close()
+ }()
+
+ for {
+ select {
+ case message, ok := <-client.Send:
+ conn.SetWriteDeadline(time.Now().Add(writeWait))
+ if !ok {
+ // Hub closed the channel
+ conn.WriteMessage(ws.CloseMessage, []byte{})
+ return
+ }
+
+ // Send each message individually (no batching)
+ // This ensures each JSON message is sent separately and can be parsed correctly
+ if err := conn.WriteMessage(ws.TextMessage, message); err != nil {
+ logger.Debugf("WebSocket write error for client %s: %v", client.ID, err)
+ return
+ }
+
+ case <-ticker.C:
+ conn.SetWriteDeadline(time.Now().Add(writeWait))
+ if err := conn.WriteMessage(ws.PingMessage, nil); err != nil {
+ logger.Debugf("WebSocket ping error for client %s: %v", client.ID, err)
+ return
+ }
+ }
+ }
+}
diff --git a/web/global/global.go b/web/global/global.go
index 025fa081..f72c7bfe 100644
--- a/web/global/global.go
+++ b/web/global/global.go
@@ -17,6 +17,7 @@ var (
type WebServer interface {
GetCron() *cron.Cron // Get the cron scheduler
GetCtx() context.Context // Get the server context
+ GetWSHub() interface{} // Get the WebSocket hub (using interface{} to avoid circular dependency)
}
// SubServer interface defines methods for accessing the subscription server instance.
diff --git a/web/html/common/page.html b/web/html/common/page.html
index c0a7ca63..0af63afb 100644
--- a/web/html/common/page.html
+++ b/web/html/common/page.html
@@ -49,6 +49,7 @@
const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath;
+
{{ end }}
{{ define "page/body_end" }}
diff --git a/web/html/form/outbound.html b/web/html/form/outbound.html
index aa6aa323..511caefe 100644
--- a/web/html/form/outbound.html
+++ b/web/html/form/outbound.html
@@ -1,6 +1,7 @@
{{define "form/outbound"}}
- {outModal.toggleJson(activeKey == '2'); }">
+ {outModal.toggleJson(activeKey == '2'); }">
@@ -8,8 +9,10 @@
[[ y ]]
-
-
+
+
@@ -59,12 +62,13 @@
-
-
-
+
+
+
Noise [[ index + 1 ]]
- outbound.settings.delNoise(index)"
+ outbound.settings.delNoise(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }">
@@ -108,7 +112,7 @@
[[ s ]]
-
+
@@ -129,21 +133,21 @@
-
-
- {{ i18n "reset" }}
-
- {{ i18n "pages.xray.wireguard.secretKey" }}
-
-
-
+
+
+ {{ i18n "reset" }}
+
+ {{ i18n "pages.xray.wireguard.secretKey" }}
+
+
+
-
-
+
+
-
+
[[ wds ]]
@@ -171,8 +175,11 @@
-
- Peer [[ index + 1 ]] outbound.settings.delPeer(index)" :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }">
+
+ Peer [[ index + 1 ]] outbound.settings.delPeer(index)"
+ :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }">
@@ -190,7 +197,8 @@
-
+
@@ -210,7 +218,7 @@
-
+
@@ -239,6 +247,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -264,7 +299,8 @@
- [[ method_name ]]
+ [[ method_name
+ ]]
@@ -279,7 +315,8 @@
-
+
TCP (RAW)
mKCP
WebSocket
@@ -290,7 +327,8 @@
- outbound.stream.tcp.type = checked ? 'http' : 'none'">
+ outbound.stream.tcp.type = checked ? 'http' : 'none'">
@@ -353,7 +391,7 @@
-
+
@@ -390,7 +428,8 @@
[[ key ]]
-
+
@@ -437,7 +476,8 @@
-
+
[[ alpn ]]
@@ -485,7 +525,8 @@
-
+
[[ key ]]
@@ -501,6 +542,15 @@
+
+
+ CF-Connecting-IP
+ X-Real-IP
+ True-Client-IP
+ X-Client-IP
+
+
@@ -526,11 +576,12 @@
-
+
-{{end}}
+{{end}}
\ No newline at end of file
diff --git a/web/html/form/protocol/vless.html b/web/html/form/protocol/vless.html
index 140b9c1a..bdf75be3 100644
--- a/web/html/form/protocol/vless.html
+++ b/web/html/form/protocol/vless.html
@@ -1,29 +1,36 @@
{{define "form/vless"}}
-
+
{{template "form/client"}}
-
+
-
+
| [[ client.email ]] |
[[ client.id ]] |
-
-
+
+
-
- X25519 (not Post-Quantum)
- ML-KEM-768 (Post-Quantum)
+
+ X25519 (not
+ Post-Quantum)
+ ML-KEM-768
+ (Post-Quantum)
@@ -34,22 +41,32 @@
- Get New keys
+ Get New
+ keys
Clear
+
-
+
-
+
-
- Fallback [[ index + 1 ]] inbound.settings.delFallback(index)" :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }">
+
+ Fallback [[ index + 1 ]] inbound.settings.delFallback(index)"
+ :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }">
@@ -64,9 +81,56 @@
-
+
-{{end}}
+
+
+
+
+
+ updateTestseed(0, val)" :min="0" :max="9999"
+ :style="{ width: '100%' }"
+ placeholder="900" addon-before="[0]">
+
+
+ updateTestseed(1, val)" :min="0" :max="9999"
+ :style="{ width: '100%' }"
+ placeholder="500" addon-before="[1]">
+
+
+ updateTestseed(2, val)" :min="0" :max="9999"
+ :style="{ width: '100%' }"
+ placeholder="900" addon-before="[2]">
+
+
+ updateTestseed(3, val)" :min="0" :max="9999"
+ :style="{ width: '100%' }"
+ placeholder="256" addon-before="[3]">
+
+
+
+
+ Rand
+
+
+ Reset
+
+
+
+
+
+
+{{end}}
\ No newline at end of file
diff --git a/web/html/form/stream/stream_sockopt.html b/web/html/form/stream/stream_sockopt.html
index 4480594a..062b83df 100644
--- a/web/html/form/stream/stream_sockopt.html
+++ b/web/html/form/stream/stream_sockopt.html
@@ -61,6 +61,15 @@
+
+
+ CF-Connecting-IP
+ X-Real-IP
+ True-Client-IP
+ X-Client-IP
+
+
{{end}}
diff --git a/web/html/form/tls_settings.html b/web/html/form/tls_settings.html
index c3844a7f..3723130e 100644
--- a/web/html/form/tls_settings.html
+++ b/web/html/form/tls_settings.html
@@ -60,16 +60,20 @@
+
-
- {{ i18n "pages.inbounds.certificatePath" }}
- {{ i18n "pages.inbounds.certificateContent" }}
+
+ {{ i18n "pages.inbounds.certificatePath" }}
+ {{ i18n "pages.inbounds.certificateContent" }}
-
-
+
+
+
+
+
+
diff --git a/web/html/inbounds.html b/web/html/inbounds.html
index 86bde2c8..4e1149ae 100644
--- a/web/html/inbounds.html
+++ b/web/html/inbounds.html
@@ -1128,8 +1128,11 @@
},
openEditClient(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+ if (!dbInbound) return;
clients = this.getInboundClients(dbInbound);
+ if (!clients || !Array.isArray(clients)) return;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+ if (index < 0) return;
clientModal.show({
title: '{{ i18n "pages.client.edit"}}',
okText: '{{ i18n "pages.client.submitEdit"}}',
@@ -1144,11 +1147,14 @@
});
},
findIndexOfClient(protocol, clients, client) {
+ if (!clients || !Array.isArray(clients) || !client) {
+ return -1;
+ }
switch (protocol) {
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
- return clients.findIndex(item => item.password === client.password && item.email === client.email);
- default: return clients.findIndex(item => item.id === client.id && item.email === client.email);
+ return clients.findIndex(item => item && item.password === client.password && item.email === client.email);
+ default: return clients.findIndex(item => item && item.id === client.id && item.email === client.email);
}
},
async addClient(clients, dbInboundId, modal) {
@@ -1271,11 +1277,15 @@
},
showInfo(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+ if (!dbInbound) return;
index = 0;
if (dbInbound.isMultiUser()) {
inbound = dbInbound.toInbound();
- clients = inbound.clients;
- index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+ clients = inbound && inbound.clients ? inbound.clients : null;
+ if (clients && Array.isArray(clients)) {
+ index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+ if (index < 0) index = 0;
+ }
}
newDbInbound = this.checkFallback(dbInbound);
infoModal.show(newDbInbound, index);
@@ -1288,9 +1298,12 @@
async switchEnableClient(dbInboundId, client) {
this.loading()
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+ if (!dbInbound) return;
inbound = dbInbound.toInbound();
- clients = inbound.clients;
+ clients = inbound && inbound.clients ? inbound.clients : null;
+ if (!clients || !Array.isArray(clients)) return;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+ if (index < 0 || !clients[index]) return;
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index], dbInboundId, clientId);
@@ -1303,7 +1316,9 @@
}
},
getInboundClients(dbInbound) {
- return dbInbound.toInbound().clients;
+ if (!dbInbound) return null;
+ const inbound = dbInbound.toInbound();
+ return inbound && inbound.clients ? inbound.clients : null;
},
resetClientTraffic(client, dbInboundId, confirmation = true) {
if (confirmation) {
@@ -1443,7 +1458,12 @@
formatLastOnline(email) {
const ts = this.getLastOnline(email)
if (!ts) return '-'
- return IntlUtil.formatDate(ts)
+ // Check if IntlUtil is available (may not be loaded yet)
+ if (typeof IntlUtil !== 'undefined' && IntlUtil.formatDate) {
+ return IntlUtil.formatDate(ts)
+ }
+ // Fallback to simple date formatting if IntlUtil is not available
+ return new Date(ts).toLocaleString()
},
isRemovable(dbInboundId) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
@@ -1567,13 +1587,88 @@
}
this.loading();
this.getDefaultSettings();
- if (this.isRefreshEnabled) {
- this.startDataRefreshLoop();
+
+ // Initial data fetch
+ this.getDBInbounds().then(() => {
+ this.loading(false);
+ });
+
+ // Setup WebSocket for real-time updates
+ if (window.wsClient) {
+ window.wsClient.connect();
+
+ // Listen for inbounds updates
+ window.wsClient.on('inbounds', (payload) => {
+ if (payload && Array.isArray(payload)) {
+ // Use setInbounds to properly convert to DBInbound objects with methods
+ this.setInbounds(payload);
+ this.searchInbounds(this.searchKey);
+ }
+ });
+
+ // Listen for traffic updates
+ window.wsClient.on('traffic', (payload) => {
+ if (payload && payload.clientTraffics && Array.isArray(payload.clientTraffics)) {
+ // Update client traffic statistics
+ payload.clientTraffics.forEach(clientTraffic => {
+ const dbInbound = this.dbInbounds.find(ib => {
+ if (!ib) return false;
+ const clients = this.getInboundClients(ib);
+ return clients && Array.isArray(clients) && clients.some(c => c && c.email === clientTraffic.email);
+ });
+ if (dbInbound && dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) {
+ const stats = dbInbound.clientStats.find(s => s && s.email === clientTraffic.email);
+ if (stats) {
+ stats.up = clientTraffic.up || stats.up;
+ stats.down = clientTraffic.down || stats.down;
+ stats.total = clientTraffic.total || stats.total;
+ }
+ }
+ });
+ }
+
+ // Update online clients list in real-time
+ if (payload && Array.isArray(payload.onlineClients)) {
+ this.onlineClients = payload.onlineClients;
+ // Recalculate client counts to update online status
+ 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);
+ }
+ });
+ }
+
+ // Update last online map in real-time
+ if (payload && payload.lastOnlineMap && typeof payload.lastOnlineMap === 'object') {
+ this.lastOnlineMap = { ...this.lastOnlineMap, ...payload.lastOnlineMap };
+ }
+ });
+
+ // Notifications disabled - white notifications are not needed
+
+ // Fallback to polling if WebSocket fails
+ window.wsClient.on('error', () => {
+ console.warn('WebSocket connection failed, falling back to polling');
+ if (this.isRefreshEnabled) {
+ this.startDataRefreshLoop();
+ }
+ });
+
+ window.wsClient.on('disconnected', () => {
+ if (window.wsClient.reconnectAttempts >= window.wsClient.maxReconnectAttempts) {
+ console.warn('WebSocket reconnection failed, falling back to polling');
+ if (this.isRefreshEnabled) {
+ this.startDataRefreshLoop();
+ }
+ }
+ });
+ } else {
+ // Fallback to polling if WebSocket is not available
+ if (this.isRefreshEnabled) {
+ this.startDataRefreshLoop();
+ }
}
- else {
- this.getDBInbounds();
- }
- this.loading(false);
},
computed: {
total() {
diff --git a/web/html/index.html b/web/html/index.html
index 9cbb019d..bbbbb708 100644
--- a/web/html/index.html
+++ b/web/html/index.html
@@ -1102,6 +1102,20 @@
});
fileInput.click();
},
+ startPolling() {
+ // Fallback polling mechanism
+ const pollInterval = setInterval(async () => {
+ if (window.wsClient && window.wsClient.isConnected) {
+ clearInterval(pollInterval);
+ return;
+ }
+ try {
+ await this.getStatus();
+ } catch (e) {
+ console.error(e);
+ }
+ }, 2000);
+ },
},
async mounted() {
if (window.location.protocol !== "https:") {
@@ -1113,13 +1127,57 @@
this.ipLimitEnable = msg.obj.ipLimitEnable;
}
- while (true) {
- try {
- await this.getStatus();
- } catch (e) {
- console.error(e);
- }
- await PromiseUtil.sleep(2000);
+ // Initial status fetch
+ await this.getStatus();
+
+ // Setup WebSocket for real-time updates
+ if (window.wsClient) {
+ window.wsClient.connect();
+
+ // Listen for status updates
+ window.wsClient.on('status', (payload) => {
+ this.setStatus(payload);
+ });
+
+ // Listen for Xray state changes
+ window.wsClient.on('xray_state', (payload) => {
+ if (this.status && this.status.xray) {
+ this.status.xray.state = payload.state;
+ this.status.xray.errorMsg = payload.errorMsg || '';
+ switch (payload.state) {
+ case 'running':
+ this.status.xray.color = "green";
+ this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusRunning" }}';
+ break;
+ case 'stop':
+ this.status.xray.color = "orange";
+ this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusStop" }}';
+ break;
+ case 'error':
+ this.status.xray.color = "red";
+ this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusError" }}';
+ break;
+ }
+ }
+ });
+
+ // Notifications disabled - white notifications are not needed
+
+ // Fallback to polling if WebSocket fails
+ window.wsClient.on('error', () => {
+ console.warn('WebSocket connection failed, falling back to polling');
+ this.startPolling();
+ });
+
+ window.wsClient.on('disconnected', () => {
+ if (window.wsClient.reconnectAttempts >= window.wsClient.maxReconnectAttempts) {
+ console.warn('WebSocket reconnection failed, falling back to polling');
+ this.startPolling();
+ }
+ });
+ } else {
+ // Fallback to polling if WebSocket is not available
+ this.startPolling();
}
},
});
diff --git a/web/html/modals/inbound_modal.html b/web/html/modals/inbound_modal.html
index 3c844381..c3883285 100644
--- a/web/html/modals/inbound_modal.html
+++ b/web/html/modals/inbound_modal.html
@@ -6,7 +6,8 @@