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"}} - + @@ -8,8 +9,10 @@ [[ y ]] - - + + @@ -59,12 +62,13 @@ - - - + + + Noise [[ index + 1 ]] - @@ -108,7 +112,7 @@ [[ s ]] - + @@ -129,21 +133,21 @@ - - + + - + [[ wds ]] @@ -171,8 +175,11 @@ - - Peer [[ index + 1 ]] + + Peer [[ index + 1 ]] @@ -190,7 +197,8 @@ @@ -210,7 +218,7 @@ - + + + @@ -264,7 +299,8 @@