Merge branch 'main' into feature/multi-server-support

This commit is contained in:
Sanaei 2025-08-14 18:06:16 +02:00 committed by GitHub
commit 3299d15f28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 710 additions and 74153 deletions

2
.github/FUNDING.yml vendored
View file

@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: MHSanaei
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username

View file

@ -7,7 +7,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -8,6 +8,7 @@ on:
branches: branches:
- main - main
paths: paths:
- '.github/workflows/release.yml'
- '**.js' - '**.js'
- '**.css' - '**.css'
- '**.html' - '**.html'
@ -31,7 +32,7 @@ jobs:
- 386 - 386
- armv5 - armv5
- s390x - s390x
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -42,51 +43,37 @@ jobs:
go-version-file: go.mod go-version-file: go.mod
check-latest: true check-latest: true
- name: Install dependencies - name: Build 3X-UI
run: |
sudo apt-get update
if [ "${{ matrix.platform }}" == "arm64" ]; then
sudo apt install gcc-aarch64-linux-gnu
elif [ "${{ matrix.platform }}" == "armv7" ]; then
sudo apt install gcc-arm-linux-gnueabihf
elif [ "${{ matrix.platform }}" == "armv6" ]; then
sudo apt install gcc-arm-linux-gnueabihf
elif [ "${{ matrix.platform }}" == "386" ]; then
sudo apt install gcc-i686-linux-gnu
elif [ "${{ matrix.platform }}" == "armv5" ]; then
sudo apt install gcc-arm-linux-gnueabi
elif [ "${{ matrix.platform }}" == "s390x" ]; then
sudo apt install gcc-s390x-linux-gnu
fi
- name: Build 3x-ui
run: | run: |
export CGO_ENABLED=1 export CGO_ENABLED=1
export GOOS=linux export GOOS=linux
export GOARCH=${{ matrix.platform }} export GOARCH=${{ matrix.platform }}
if [ "${{ matrix.platform }}" == "arm64" ]; then # Use Bootlin prebuilt cross-toolchains (musl 1.2.5 in stable series)
export GOARCH=arm64 case "${{ matrix.platform }}" in
export CC=aarch64-linux-gnu-gcc amd64) BOOTLIN_ARCH="x86-64" ;;
elif [ "${{ matrix.platform }}" == "armv7" ]; then arm64) BOOTLIN_ARCH="aarch64" ;;
export GOARCH=arm armv7) BOOTLIN_ARCH="armv7-eabihf"; export GOARCH=arm GOARM=7 ;;
export GOARM=7 armv6) BOOTLIN_ARCH="armv6-eabihf"; export GOARCH=arm GOARM=6 ;;
export CC=arm-linux-gnueabihf-gcc armv5) BOOTLIN_ARCH="armv5-eabi"; export GOARCH=arm GOARM=5 ;;
elif [ "${{ matrix.platform }}" == "armv6" ]; then 386) BOOTLIN_ARCH="x86-i686" ;;
export GOARCH=arm s390x) BOOTLIN_ARCH="s390x-z13" ;;
export GOARM=6 esac
export CC=arm-linux-gnueabihf-gcc echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})"
elif [ "${{ matrix.platform }}" == "386" ]; then TARBALL_BASE="https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/"
export GOARCH=386 TARBALL_URL=$(curl -fsSL "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1)
export CC=i686-linux-gnu-gcc [ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
elif [ "${{ matrix.platform }}" == "armv5" ]; then echo "Downloading: $TARBALL_URL"
export GOARCH=arm cd /tmp
export GOARM=5 curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
export CC=arm-linux-gnueabi-gcc tar -xf "$(basename "$TARBALL_URL")"
elif [ "${{ matrix.platform }}" == "s390x" ]; then TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
export GOARCH=s390x export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH"
export CC=s390x-linux-gnu-gcc export CC=$(realpath "$(find "$TOOLCHAIN_DIR/bin" -name '*-gcc.br_real' -type f -executable | head -n1)")
fi [ -z "$CC" ] && { echo "No gcc.br_real found in $TOOLCHAIN_DIR/bin" >&2; exit 1; }
go build -ldflags "-w -s" -o xui-release -v main.go cd -
go build -ldflags "-w -s -linkmode external -extldflags '-static'" -o xui-release -v main.go
file xui-release
ldd xui-release || echo "Static binary confirmed"
mkdir x-ui mkdir x-ui
cp xui-release x-ui/ cp xui-release x-ui/

4
.gitignore vendored
View file

@ -29,9 +29,9 @@ main
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Ignore Go specific files # Ignore Go build files
*.exe *.exe
*.exe~ x-ui.db
# Ignore Docker specific files # Ignore Docker specific files
docker-compose.override.yml docker-compose.override.yml

View file

@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## النجوم عبر الزمن ## النجوم عبر الزمن

View file

@ -48,7 +48,7 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Estrellas a lo Largo del Tiempo ## Estrellas a lo Largo del Tiempo

View file

@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## ستاره‌ها در طول زمان ## ستاره‌ها در طول زمان

View file

@ -48,7 +48,7 @@ For full documentation, please visit the [project Wiki](https://github.com/MHSan
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Stargazers over Time ## Stargazers over Time

View file

@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Звезды с течением времени ## Звезды с течением времени

View file

@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p> </p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC` - USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A` - POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv` - LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## 随时间变化的星标数 ## 随时间变化的星标数

View file

@ -3,7 +3,10 @@ package config
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath"
"runtime"
"strings" "strings"
) )
@ -54,12 +57,32 @@ func GetBinFolderPath() string {
return binFolderPath return binFolderPath
} }
func getBaseDir() string {
exePath, err := os.Executable()
if err != nil {
return "."
}
exeDir := filepath.Dir(exePath)
exeDirLower := strings.ToLower(filepath.ToSlash(exeDir))
if strings.Contains(exeDirLower, "/appdata/local/temp/") || strings.Contains(exeDirLower, "/go-build") {
wd, err := os.Getwd()
if err != nil {
return "."
}
return wd
}
return exeDir
}
func GetDBFolderPath() string { func GetDBFolderPath() string {
dbFolderPath := os.Getenv("XUI_DB_FOLDER") dbFolderPath := os.Getenv("XUI_DB_FOLDER")
if dbFolderPath == "" { if dbFolderPath != "" {
dbFolderPath = "/etc/x-ui"
}
return dbFolderPath return dbFolderPath
}
if runtime.GOOS == "windows" {
return getBaseDir()
}
return "/etc/x-ui"
} }
func GetDBPath() string { func GetDBPath() string {
@ -68,8 +91,54 @@ func GetDBPath() string {
func GetLogFolder() string { func GetLogFolder() string {
logFolderPath := os.Getenv("XUI_LOG_FOLDER") logFolderPath := os.Getenv("XUI_LOG_FOLDER")
if logFolderPath == "" { if logFolderPath != "" {
logFolderPath = "/var/log"
}
return logFolderPath return logFolderPath
}
if runtime.GOOS == "windows" {
return getBaseDir()
}
return "/var/log"
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Sync()
}
func init() {
if runtime.GOOS != "windows" {
return
}
if os.Getenv("XUI_DB_FOLDER") != "" {
return
}
oldDBFolder := "/etc/x-ui"
oldDBPath := fmt.Sprintf("%s/%s.db", oldDBFolder, GetName())
newDBFolder := GetDBFolderPath()
newDBPath := fmt.Sprintf("%s/%s.db", newDBFolder, GetName())
_, err := os.Stat(newDBPath)
if err == nil {
return // new exists
}
_, err = os.Stat(oldDBPath)
if os.IsNotExist(err) {
return // old does not exist
}
_ = copyFile(oldDBPath, newDBPath) // ignore error
} }

View file

@ -1 +1 @@
2.6.3 2.6.6

33
go.mod
View file

@ -9,7 +9,7 @@ require (
github.com/goccy/go-json v0.10.5 github.com/goccy/go-json v0.10.5
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mymmrac/telego v0.32.0 github.com/mymmrac/telego v1.2.0
github.com/nicksnyder/go-i18n/v2 v2.6.0 github.com/nicksnyder/go-i18n/v2 v2.6.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
@ -19,8 +19,8 @@ require (
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.250803.0 github.com/xtls/xray-core v1.250803.0
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.40.0 golang.org/x/crypto v0.41.0
golang.org/x/text v0.27.0 golang.org/x/text v0.28.0
google.golang.org/grpc v1.74.2 google.golang.org/grpc v1.74.2
gorm.io/driver/sqlite v1.6.0 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.30.1 gorm.io/gorm v1.30.1
@ -31,11 +31,9 @@ require (
github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/ebitengine/purego v0.8.4 // indirect github.com/ebitengine/purego v0.8.4 // indirect
github.com/fasthttp/router v1.5.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
@ -70,9 +68,8 @@ require (
github.com/refraction-networking/utls v1.8.0 // indirect github.com/refraction-networking/utls v1.8.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.6.6 // indirect github.com/sagernet/sing v0.7.5 // indirect
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect github.com/sagernet/sing-shadowsocks v0.2.8 // indirect
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect github.com/tklauser/numcpus v0.10.0 // indirect
@ -87,18 +84,18 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/mock v0.5.2 // indirect go.uber.org/mock v0.5.2 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.19.0 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/mod v0.26.0 // indirect golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.42.0 // indirect golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.35.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.35.0 // indirect golang.org/x/tools v0.36.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
lukechampine.com/blake3 v1.4.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect
) )

70
go.sum
View file

@ -4,14 +4,12 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 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.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -21,8 +19,6 @@ 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/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8=
github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
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 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
@ -87,10 +83,8 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -110,8 +104,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY= github.com/mymmrac/telego v1.2.0 h1:CHmR9eiugpTiF/ttppmK89E6mcu9d4DmNQS0tMpUs6M=
github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M= github.com/mymmrac/telego v1.2.0/go.mod h1:OiCm4QjqB/ZY2E4VAmkVH8EeLLhM4QuFuO1KOCuvGoM=
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= 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/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@ -138,12 +132,10 @@ 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/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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.6.6 h1:3JkvJ0vqDj/jJcx0a+ve/6lMOrSzZm30I3wrIuZtmRE= github.com/sagernet/sing v0.7.5 h1:gNMwZCLPqR+4e0g6dwi0sSsrvOmoMjpZgqxKsuJZatc=
github.com/sagernet/sing v0.6.6/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa24bR+2i53WBCz1nW/Pc47oVYauC4=
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4= 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/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM= github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
@ -153,7 +145,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@ -207,14 +198,14 @@ go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= 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= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -223,24 +214,24 @@ 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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= 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/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM= google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -254,8 +245,7 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View file

@ -40,19 +40,6 @@ arch() {
echo "Arch: $(arch)" echo "Arch: $(arch)"
check_glibc_version() {
glibc_version=$(ldd --version | head -n1 | awk '{print $NF}')
required_version="2.32"
if [[ "$(printf '%s\n' "$required_version" "$glibc_version" | sort -V | head -n1)" != "$required_version" ]]; then
echo -e "${red}GLIBC version $glibc_version is too old! Required: 2.32 or higher${plain}"
echo "Please upgrade to a newer version of your operating system to get a higher GLIBC version."
exit 1
fi
echo "GLIBC version: $glibc_version (meets requirement of 2.32+)"
}
check_glibc_version
install_base() { install_base() {
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)

File diff suppressed because one or more lines are too long

View file

@ -1,7 +0,0 @@
@import "../lib/style/index.less";
@import "../lib/style/components.less";
@green-6: #008771;
@primary-color: @green-6;
@border-radius-base: 1rem;
@progress-remaining-color: #EDEDED;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -560,6 +560,8 @@ class TlsStreamSettings extends XrayCommonClass {
enableSessionResumption = false, enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()], certificates = [new TlsStreamSettings.Cert()],
alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1], alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
echServerKeys = '',
echForceQuery = 'none',
settings = new TlsStreamSettings.Settings() settings = new TlsStreamSettings.Settings()
) { ) {
super(); super();
@ -573,6 +575,8 @@ class TlsStreamSettings extends XrayCommonClass {
this.enableSessionResumption = enableSessionResumption; this.enableSessionResumption = enableSessionResumption;
this.certs = certificates; this.certs = certificates;
this.alpn = alpn; this.alpn = alpn;
this.echServerKeys = echServerKeys;
this.echForceQuery = echForceQuery;
this.settings = settings; this.settings = settings;
} }
@ -592,7 +596,7 @@ class TlsStreamSettings extends XrayCommonClass {
} }
if (!ObjectUtil.isEmpty(json.settings)) { if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure, json.settings.fingerprint, json.settings.serverName, json.settings.domains); settings = new TlsStreamSettings.Settings(json.settings.allowInsecure, json.settings.fingerprint, json.settings.echConfigList);
} }
return new TlsStreamSettings( return new TlsStreamSettings(
json.serverName, json.serverName,
@ -605,6 +609,8 @@ class TlsStreamSettings extends XrayCommonClass {
json.enableSessionResumption, json.enableSessionResumption,
certs, certs,
json.alpn, json.alpn,
json.echServerKeys,
json.echForceQuery,
settings, settings,
); );
} }
@ -621,6 +627,8 @@ class TlsStreamSettings extends XrayCommonClass {
enableSessionResumption: this.enableSessionResumption, enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs), certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn, alpn: this.alpn,
echServerKeys: this.echServerKeys,
echForceQuery: this.echForceQuery,
settings: this.settings, settings: this.settings,
}; };
} }
@ -633,7 +641,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
keyFile = '', keyFile = '',
certificate = '', certificate = '',
key = '', key = '',
ocspStapling = 0,
oneTimeLoading = false, oneTimeLoading = false,
usage = USAGE_OPTION.ENCIPHERMENT, usage = USAGE_OPTION.ENCIPHERMENT,
buildChain = false, buildChain = false,
@ -644,7 +651,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
this.keyFile = keyFile; this.keyFile = keyFile;
this.cert = Array.isArray(certificate) ? certificate.join('\n') : certificate; this.cert = Array.isArray(certificate) ? certificate.join('\n') : certificate;
this.key = Array.isArray(key) ? key.join('\n') : key; this.key = Array.isArray(key) ? key.join('\n') : key;
this.ocspStapling = ocspStapling;
this.oneTimeLoading = oneTimeLoading; this.oneTimeLoading = oneTimeLoading;
this.usage = usage; this.usage = usage;
this.buildChain = buildChain this.buildChain = buildChain
@ -656,7 +662,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
true, true,
json.certificateFile, json.certificateFile,
json.keyFile, '', '', json.keyFile, '', '',
json.ocspStapling,
json.oneTimeLoading, json.oneTimeLoading,
json.usage, json.usage,
json.buildChain, json.buildChain,
@ -666,7 +671,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
false, '', '', false, '', '',
json.certificate.join('\n'), json.certificate.join('\n'),
json.key.join('\n'), json.key.join('\n'),
json.ocspStapling,
json.oneTimeLoading, json.oneTimeLoading,
json.usage, json.usage,
json.buildChain, json.buildChain,
@ -679,7 +683,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
return { return {
certificateFile: this.certFile, certificateFile: this.certFile,
keyFile: this.keyFile, keyFile: this.keyFile,
ocspStapling: this.ocspStapling,
oneTimeLoading: this.oneTimeLoading, oneTimeLoading: this.oneTimeLoading,
usage: this.usage, usage: this.usage,
buildChain: this.buildChain, buildChain: this.buildChain,
@ -688,7 +691,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
return { return {
certificate: this.cert.split('\n'), certificate: this.cert.split('\n'),
key: this.key.split('\n'), key: this.key.split('\n'),
ocspStapling: this.ocspStapling,
oneTimeLoading: this.oneTimeLoading, oneTimeLoading: this.oneTimeLoading,
usage: this.usage, usage: this.usage,
buildChain: this.buildChain, buildChain: this.buildChain,
@ -701,21 +703,25 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor( constructor(
allowInsecure = false, allowInsecure = false,
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME, fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
echConfigList = '',
) { ) {
super(); super();
this.allowInsecure = allowInsecure; this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.echConfigList = echConfigList;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new TlsStreamSettings.Settings( return new TlsStreamSettings.Settings(
json.allowInsecure, json.allowInsecure,
json.fingerprint, json.fingerprint,
json.echConfigList,
); );
} }
toJson() { toJson() {
return { return {
allowInsecure: this.allowInsecure, allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
echConfigList: this.echConfigList
}; };
} }
}; };
@ -1375,6 +1381,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (type == "tcp" && !ObjectUtil.isEmpty(flow)) { if (type == "tcp" && !ObjectUtil.isEmpty(flow)) {
params.set("flow", flow); params.set("flow", flow);
} }
@ -1474,6 +1483,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.settings.allowInsecure) { if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1"); params.set("allowInsecure", "1");
} }
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }
@ -1552,6 +1564,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.settings.allowInsecure) { if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1"); params.set("allowInsecure", "1");
} }
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }
@ -1695,7 +1710,7 @@ class Inbound extends XrayCommonClass {
toJson() { toJson() {
let streamSettings; let streamSettings;
if (this.canEnableStream()) { if (this.canEnableStream() || this.stream?.sockopt) {
streamSettings = this.stream.toJson(); streamSettings = this.stream.toJson();
} }
return { return {
@ -2291,12 +2306,14 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
protocol, protocol,
address, address,
port, port,
portMap = [],
network = 'tcp,udp', network = 'tcp,udp',
followRedirect = false followRedirect = false
) { ) {
super(protocol); super(protocol);
this.address = address; this.address = address;
this.port = port; this.port = port;
this.portMap = portMap;
this.network = network; this.network = network;
this.followRedirect = followRedirect; this.followRedirect = followRedirect;
} }
@ -2306,6 +2323,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
Protocols.DOKODEMO, Protocols.DOKODEMO,
json.address, json.address,
json.port, json.port,
XrayCommonClass.toHeaders(json.portMap),
json.network, json.network,
json.followRedirect, json.followRedirect,
); );
@ -2315,6 +2333,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
return { return {
address: this.address, address: this.address,
port: this.port, port: this.port,
portMap: XrayCommonClass.toV2Headers(this.portMap, false),
network: this.network, network: this.network,
followRedirect: this.followRedirect, followRedirect: this.followRedirect,
}; };

View file

@ -354,13 +354,15 @@ class TlsStreamSettings extends CommonClass {
serverName = '', serverName = '',
alpn = [], alpn = [],
fingerprint = '', fingerprint = '',
allowInsecure = false allowInsecure = false,
echConfigList = '',
) { ) {
super(); super();
this.serverName = serverName; this.serverName = serverName;
this.alpn = alpn; this.alpn = alpn;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.allowInsecure = allowInsecure; this.allowInsecure = allowInsecure;
this.echConfigList = echConfigList;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@ -369,6 +371,7 @@ class TlsStreamSettings extends CommonClass {
json.alpn, json.alpn,
json.fingerprint, json.fingerprint,
json.allowInsecure, json.allowInsecure,
json.echConfigList,
); );
} }
@ -378,6 +381,7 @@ class TlsStreamSettings extends CommonClass {
alpn: this.alpn, alpn: this.alpn,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
allowInsecure: this.allowInsecure, allowInsecure: this.allowInsecure,
echConfigList: this.echConfigList
}; };
} }
} }
@ -782,7 +786,8 @@ class Outbound extends CommonClass {
let alpn = url.searchParams.get('alpn'); let alpn = url.searchParams.get('alpn');
let allowInsecure = url.searchParams.get('allowInsecure'); let allowInsecure = url.searchParams.get('allowInsecure');
let sni = url.searchParams.get('sni') ?? ''; let sni = url.searchParams.get('sni') ?? '';
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1); let ech = url.searchParams.get('ech') ?? '';
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1, ech);
} }
if (security == 'reality') { if (security == 'reality') {

View file

@ -7,7 +7,7 @@ class AllSetting {
this.webCertFile = ""; this.webCertFile = "";
this.webKeyFile = ""; this.webKeyFile = "";
this.webBasePath = "/"; this.webBasePath = "/";
this.sessionMaxAge = 60; this.sessionMaxAge = 360;
this.pageSize = 50; this.pageSize = 50;
this.expireDiff = 0; this.expireDiff = 0;
this.trafficDiff = 0; this.trafficDiff = 0;

File diff suppressed because it is too large Load diff

View file

@ -1,5 +0,0 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./vue.common.prod.js')
} else {
module.exports = require('./vue.common.dev.js')
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +0,0 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./vue.runtime.common.prod.js')
} else {
module.exports = require('./vue.runtime.common.dev.js')
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,76 +0,0 @@
import Vue from './vue.runtime.common.js'
export default Vue
// this should be kept in sync with src/v3/index.ts
export const {
version,
// refs
ref,
shallowRef,
isRef,
toRef,
toRefs,
unref,
proxyRefs,
customRef,
triggerRef,
computed,
// reactive
reactive,
isReactive,
isReadonly,
isShallow,
isProxy,
shallowReactive,
markRaw,
toRaw,
readonly,
shallowReadonly,
// watch
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
// effectScope
effectScope,
onScopeDispose,
getCurrentScope,
// provide / inject
provide,
inject,
// lifecycle
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onActivated,
onDeactivated,
onServerPrefetch,
onRenderTracked,
onRenderTriggered,
// v2 only
set,
del,
// v3 compat
h,
getCurrentInstance,
useSlots,
useAttrs,
mergeDefaults,
nextTick,
useCssModule,
useCssVars,
defineComponent,
defineAsyncComponent
} = Vue

View file

@ -120,8 +120,8 @@ func (a *InboundController) addInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@ -138,8 +138,8 @@ func (a *InboundController) delInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@ -164,8 +164,8 @@ func (a *InboundController) updateInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }

View file

@ -18,6 +18,7 @@ type ServerController struct {
BaseController BaseController
serverService service.ServerService serverService service.ServerService
settingService service.SettingService
lastStatus *service.Status lastStatus *service.Status
lastGetStatusTime time.Time lastGetStatusTime time.Time
@ -44,13 +45,16 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/stopXrayService", a.stopXrayService) g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService) g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray) g.POST("/installXray/:version", a.installXray)
g.POST("/updateGeofile", a.updateGeofile)
g.POST("/updateGeofile/:fileName", a.updateGeofile) g.POST("/updateGeofile/:fileName", a.updateGeofile)
g.POST("/logs/:count", a.getLogs) g.POST("/logs/:count", a.getLogs)
g.POST("/xraylogs/:count", a.getXrayLogs)
g.POST("/getConfigJson", a.getConfigJson) g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb) g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB) g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert) g.POST("/getNewX25519Cert", a.getNewX25519Cert)
g.POST("/getNewmldsa65", a.getNewmldsa65) g.POST("/getNewmldsa65", a.getNewmldsa65)
g.POST("/getNewEchCert", a.getNewEchCert)
} }
func (a *ServerController) refreshStatus() { func (a *ServerController) refreshStatus() {
@ -133,6 +137,50 @@ func (a *ServerController) getLogs(c *gin.Context) {
jsonObj(c, logs, nil) jsonObj(c, logs, nil)
} }
func (a *ServerController) getXrayLogs(c *gin.Context) {
count := c.Param("count")
filter := c.PostForm("filter")
showDirect := c.PostForm("showDirect")
showBlocked := c.PostForm("showBlocked")
showProxy := c.PostForm("showProxy")
var freedoms []string
var blackholes []string
//getting tags for freedom and blackhole outbounds
config, err := a.settingService.GetDefaultXrayConfig()
if err == nil && config != nil {
if cfgMap, ok := config.(map[string]interface{}); ok {
if outbounds, ok := cfgMap["outbounds"].([]interface{}); ok {
for _, outbound := range outbounds {
if obMap, ok := outbound.(map[string]interface{}); ok {
switch obMap["protocol"] {
case "freedom":
if tag, ok := obMap["tag"].(string); ok {
freedoms = append(freedoms, tag)
}
case "blackhole":
if tag, ok := obMap["tag"].(string); ok {
blackholes = append(blackholes, tag)
}
}
}
}
}
}
}
if len(freedoms) == 0 {
freedoms = []string{"direct"}
}
if len(blackholes) == 0 {
blackholes = []string{"blocked"}
}
logs := a.serverService.GetXrayLogs(count, filter, showDirect, showBlocked, showProxy, freedoms, blackholes)
jsonObj(c, logs, nil)
}
func (a *ServerController) getConfigJson(c *gin.Context) { func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson() configJson, err := a.serverService.GetConfigJson()
if err != nil { if err != nil {
@ -208,3 +256,13 @@ func (a *ServerController) getNewmldsa65(c *gin.Context) {
} }
jsonObj(c, cert, nil) jsonObj(c, cert, nil)
} }
func (a *ServerController) getNewEchCert(c *gin.Context) {
sni := c.PostForm("sni")
cert, err := a.serverService.GetNewEchCert(sni)
if err != nil {
jsonMsg(c, "get ech certificate", err)
return
}
jsonObj(c, cert, nil)
}

View file

@ -44,7 +44,7 @@
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable"> <a-form-item v-if="client.email && app.subSettings?.enable">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">

View file

@ -6,6 +6,19 @@
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'> <a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input-number v-model.number="inbound.settings.port"></a-input-number> <a-input-number v-model.number="inbound.settings.port"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.portMap"}}'>
<a-button size="small" @click="inbound.settings.portMap.push({name: '', value: ''})">+</a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(pm, index) in inbound.settings.portMap">
<a-input style="width: 50%" v-model.trim="pm.name" placeholder='{{ i18n "pages.inbounds.port"}}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="pm.value" placeholder='{{ i18n "pages.inbounds.targetAddress" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.settings.portMap.splice(index,1)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'> <a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp,udp">TCP,UDP</a-select-option> <a-select-option value="tcp,udp">TCP,UDP</a-select-option>
@ -17,4 +30,8 @@
<a-switch v-model="inbound.settings.followRedirect"></a-switch> <a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- sockopt -->
<template>
{{template "form/streamSockopt"}}
</template>
{{end}} {{end}}

View file

@ -91,9 +91,6 @@
<a-textarea v-model="cert.key"></a-textarea> <a-textarea v-model="cert.key"></a-textarea>
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label='OCSP stapling'>
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="One Time Loading"> <a-form-item label="One Time Loading">
<a-switch v-model="cert.oneTimeLoading"></a-switch> <a-switch v-model="cert.oneTimeLoading"></a-switch>
</a-form-item> </a-form-item>
@ -106,6 +103,21 @@
<a-switch v-model="cert.buildChain"></a-switch> <a-switch v-model="cert.buildChain"></a-switch>
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label='ECH key'>
<a-input v-model="inbound.stream.tls.echServerKeys"></a-input>
</a-form-item>
<a-form-item label='ECH config'>
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
</a-form-item>
<a-form-item label='ECH force query'>
<a-select v-model="inbound.stream.tls.echForceQuery"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
</a-form-item>
</template> </template>
<!-- reality settings --> <!-- reality settings -->

View file

@ -405,9 +405,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@ -419,9 +419,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@ -433,9 +433,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@ -447,9 +447,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@ -534,9 +534,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@ -548,9 +548,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@ -562,9 +562,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@ -576,9 +576,9 @@
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]] [[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</template> </template>
<a-icon type="message" v-if="getClientWithComment(clientEmail, dbInbound.id).comment"></a-icon> <a-icon type="message" v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@ -811,17 +811,6 @@
loading(spinning = true) { loading(spinning = true) {
this.loadingStates.spinning = spinning; this.loadingStates.spinning = spinning;
}, },
getClientWithComment(email, inboundId) {
const dbInbound = this.dbInbounds.find(inbound => inbound.id === inboundId);
if (!dbInbound) return { email, comment: '' };
const inboundSettings = JSON.parse(dbInbound.settings);
if (inboundSettings.clients) {
const client = inboundSettings.clients.find(c => c.email === email);
return client ? { email: client.email, comment: client.comment || '' } : { email, comment: '' };
}
return { email, comment: '' };
},
async getDBInbounds() { async getDBInbounds() {
this.refreshing = true; this.refreshing = true;
const msg = await HttpUtil.post('/panel/inbound/list'); const msg = await HttpUtil.post('/panel/inbound/list');
@ -893,7 +882,7 @@
} }
}, },
getClientCounts(dbInbound, inbound) { getClientCounts(dbInbound, inbound) {
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = []; let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [], comments = new Map();
clients = inbound.clients; clients = inbound.clients;
clientStats = dbInbound.clientStats clientStats = dbInbound.clientStats
now = new Date().getTime() now = new Date().getTime()
@ -901,6 +890,9 @@
clientCount = clients.length; clientCount = clients.length;
if (dbInbound.enable) { if (dbInbound.enable) {
clients.forEach(client => { clients.forEach(client => {
if (client.comment) {
comments.set(client.email, client.comment)
}
if (client.enable) { if (client.enable) {
active.push(client.email); active.push(client.email);
if (this.isClientOnline(client.email)) online.push(client.email); if (this.isClientOnline(client.email)) online.push(client.email);
@ -929,6 +921,7 @@
depleted: depleted, depleted: depleted,
expiring: expiring, expiring: expiring,
online: online, online: online,
comments: comments,
}; };
}, },
@ -1126,7 +1119,11 @@
protocol: inbound.protocol, protocol: inbound.protocol,
settings: inbound.settings.toString(), settings: inbound.settings.toString(),
}; };
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canEnableStream()){
data.streamSettings = inbound.stream.toString();
} else if (inbound.stream?.sockopt) {
data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
}
data.sniffing = inbound.sniffing.toString(); data.sniffing = inbound.sniffing.toString();
data.allocate = inbound.allocate.toString(); data.allocate = inbound.allocate.toString();
@ -1146,7 +1143,11 @@
protocol: inbound.protocol, protocol: inbound.protocol,
settings: inbound.settings.toString(), settings: inbound.settings.toString(),
}; };
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canEnableStream()){
data.streamSettings = inbound.stream.toString();
} else if (inbound.stream?.sockopt) {
data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
}
data.sniffing = inbound.sniffing.toString(); data.sniffing = inbound.sniffing.toString();
data.allocate = inbound.allocate.toString(); data.allocate = inbound.allocate.toString();

View file

@ -167,7 +167,7 @@
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span> <span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
</a-col> </a-col>
<a-col> <a-col>
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-tag> <a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-icon>
</a-col> </a-col>
</a-row> </a-row>
</span> </span>
@ -179,6 +179,10 @@
</template> </template>
</template> </template>
<template #actions> <template #actions>
<a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" :style="{ justifyContent: 'center' }">
<a-icon type="bars"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
</a-space>
<a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }"> <a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }">
<a-icon type="poweroff"></a-icon> <a-icon type="poweroff"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span> <span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
@ -376,6 +380,9 @@
<a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/> <a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/>
</a-list-item> </a-list-item>
</a-list> </a-list>
<div style="margin-top: 5px; display: flex; justify-content: flex-end;">
<a-button @click="updateGeofile('')">{{ i18n "pages.index.geofilesUpdateAll" }}</a-button>
</div>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-modal> </a-modal>
@ -422,6 +429,48 @@
</a-form> </a-form>
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div> <div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div>
</a-modal> </a-modal>
<a-modal id="xraylog-modal"
v-model="xraylogModal.visible"
:closable="true" @cancel="() => xraylogModal.visible = false"
:class="themeSwitcher.currentTheme"
width="80vw"
footer="">
<template slot="title">
{{ i18n "pages.index.logs" }}
<a-icon :spin="xraylogModal.loading"
type="sync"
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
:disabled="xraylogModal.loading"
@click="openXrayLogs()">
</a-icon>
</template>
<a-form layout="inline">
<a-form-item :style="{ marginRight: '0.5rem' }">
<a-input-group compact>
<a-select size="small" v-model="xraylogModal.rows" :style="{ width: '70px' }"
@change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
<a-select-option value="100">100</a-select-option>
<a-select-option value="500">500</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item label="Filter:">
<a-input size="small" v-model="xraylogModal.filter" @keyup.enter="openXrayLogs()"></a-input>
</a-form-item>
<a-form-item>
<a-checkbox v-model="xraylogModal.showDirect" @change="openXrayLogs()">Direct</a-checkbox>
<a-checkbox v-model="xraylogModal.showBlocked" @change="openXrayLogs()">Blocked</a-checkbox>
<a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
</a-form-item>
<a-form-item :style="{ float: 'right' }">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
</a-form-item>
</a-form>
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="xraylogModal.formattedLogs"></div>
</a-modal>
<a-modal id="backup-modal" <a-modal id="backup-modal"
v-model="backupModal.visible" v-model="backupModal.visible"
title='{{ i18n "pages.index.backupTitle" }}' title='{{ i18n "pages.index.backupTitle" }}'
@ -606,6 +655,60 @@
}, },
}; };
const xraylogModal = {
visible: false,
logs: [],
rows: 20,
showDirect: true,
showBlocked: true,
showProxy: true,
loading: false,
show(logs) {
this.visible = true;
this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = '';
logs.forEach((log, index) => {
if(index > 0) formattedLogs += '<br>';
const parts = log.split(' ');
if(parts.length === 10) {
const dateTime = `<b>${parts[0]} ${parts[1]}</b>`;
const from = `<b>${parts[3]}</b>`;
const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`;
let outboundColor = '';
if (parts[9] === "b") {
outboundColor = ' style="color: #e04141;"'; //red for blocked
}
else if (parts[9] === "p") {
outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
}
formattedLogs += `<span${outboundColor}>
${dateTime}
${parts[2]}
${from}
${parts[4]}
${to}
${parts.slice(6, 9).join(' ')}
</span>`;
} else {
formattedLogs += `<span>${log}</span>`;
}
});
return formattedLogs;
},
hide() {
this.visible = false;
},
};
const backupModal = { const backupModal = {
visible: false, visible: false,
show() { show() {
@ -629,10 +732,12 @@
status: new Status(), status: new Status(),
versionModal, versionModal,
logModal, logModal,
xraylogModal,
backupModal, backupModal,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
showAlert: false, showAlert: false,
showIp: false showIp: false,
ipLimitEnable: false,
}, },
methods: { methods: {
loading(spinning, tip = '{{ i18n "loading"}}') { loading(spinning, tip = '{{ i18n "loading"}}') {
@ -681,16 +786,22 @@
}); });
}, },
updateGeofile(fileName) { updateGeofile(fileName) {
const isSingleFile = !!fileName;
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.index.geofileUpdateDialog" }}', title: '{{ i18n "pages.index.geofileUpdateDialog" }}',
content: '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName), content: isSingleFile
? '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName)
: '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
okText: '{{ i18n "confirm"}}', okText: '{{ i18n "confirm"}}',
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}'); this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
await HttpUtil.post(`/server/updateGeofile/${fileName}`); const url = isSingleFile
? `/server/updateGeofile/${fileName}`
: `/server/updateGeofile`;
await HttpUtil.post(url);
this.loading(false); this.loading(false);
}, },
}); });
@ -721,6 +832,16 @@
await PromiseUtil.sleep(500); await PromiseUtil.sleep(500);
logModal.loading = false; logModal.loading = false;
}, },
async openXrayLogs(){
xraylogModal.loading = true;
const msg = await HttpUtil.post('server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy});
if (!msg.success) {
return;
}
xraylogModal.show(msg.obj);
await PromiseUtil.sleep(500);
xraylogModal.loading = false;
},
async openConfig() { async openConfig() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson'); const msg = await HttpUtil.post('server/getConfigJson');
@ -773,6 +894,12 @@
if (window.location.protocol !== "https:") { if (window.location.protocol !== "https:") {
this.showAlert = true; this.showAlert = true;
} }
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (msg.success) {
this.ipLimitEnable = msg.obj.ipLimitEnable;
}
while (true) { while (true) {
try { try {
await this.getStatus(); await this.getStatus();

View file

@ -39,7 +39,7 @@
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="app.subSettings.enable"> <a-form-item v-if="app.subSettings?.enable">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">

View file

@ -199,7 +199,7 @@
<a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag> <a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
</td> </td>
</tr> </tr>
<tr v-if="app.ipLimitEnable"> <tr v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td> <td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
<td> <td>
<a-tag>[[ infoModal.clientIps ]]</a-tag> <a-tag>[[ infoModal.clientIps ]]</a-tag>

View file

@ -152,6 +152,16 @@
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed; inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify; inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
}, },
async getNewEchCert() {
inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewEchCert', {sni: inModal.inbound.stream.tls.sni});
inModal.loading(false);
if (!msg.success) {
return;
}
inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
},
}, },
}); });

View file

@ -21,7 +21,7 @@
</a-space> </a-space>
</template> </template>
<tr-qr-modal class="qr-modal"> <tr-qr-modal class="qr-modal">
<template v-if="app.subSettings.enable && qrModal.subId"> <template v-if="app.subSettings?.enable && qrModal.subId">
<tr-qr-box class="qr-box"> <tr-qr-box class="qr-box">
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag> <a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
<tr-qr-bg class="qr-bg-sub"> <tr-qr-bg class="qr-bg-sub">

View file

@ -16,7 +16,7 @@ func NewCheckXrayRunningJob() *CheckXrayRunningJob {
} }
func (j *CheckXrayRunningJob) Run() { func (j *CheckXrayRunningJob) Run() {
if j.xrayService.IsXrayRunning() { if !j.xrayService.DidXrayCrash() {
j.checkTime = 0 j.checkTime = 0
} else { } else {
j.checkTime++ j.checkTime++

View file

@ -2,6 +2,7 @@ package service
import ( import (
"archive/zip" "archive/zip"
"bufio"
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -329,7 +330,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
continue continue
} }
if major > 25 || (major == 25 && minor > 7) || (major == 25 && minor == 7 && patch >= 26) { if major > 25 || (major == 25 && minor > 8) || (major == 25 && minor == 8 && patch >= 3) {
versions = append(versions, release.TagName) versions = append(versions, release.TagName)
} }
} }
@ -346,7 +347,6 @@ func (s *ServerService) StopXrayService() error {
} }
func (s *ServerService) RestartXrayService() error { func (s *ServerService) RestartXrayService() error {
s.xrayService.StopXray()
err := s.xrayService.RestartXray(true) err := s.xrayService.RestartXray(true)
if err != nil { if err != nil {
logger.Error("start xray failed:", err) logger.Error("start xray failed:", err)
@ -481,6 +481,81 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
return lines return lines
} }
func (s *ServerService) GetXrayLogs(
count string,
filter string,
showDirect string,
showBlocked string,
showProxy string,
freedoms []string,
blackholes []string) []string {
countInt, _ := strconv.Atoi(count)
var lines []string
pathToAccessLog, err := xray.GetAccessLogPath()
if err != nil {
return lines
}
file, err := os.Open(pathToAccessLog)
if err != nil {
return lines
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.Contains(line, "api -> api") {
//skipping empty lines and api calls
continue
}
if filter != "" && !strings.Contains(line, filter) {
//applying filter if it's not empty
continue
}
//adding suffixes to further distinguish entries by outbound
if hasSuffix(line, freedoms) {
if showDirect == "false" {
continue
}
line = line + " f"
} else if hasSuffix(line, blackholes) {
if showBlocked == "false" {
continue
}
line = line + " b"
} else {
if showProxy == "false" {
continue
}
line = line + " p"
}
lines = append(lines, line)
}
if len(lines) > countInt {
lines = lines[len(lines)-countInt:]
}
return lines
}
func hasSuffix(line string, suffixes []string) bool {
for _, sfx := range suffixes {
if strings.HasSuffix(line, sfx+"]") {
return true
}
}
return false
}
func (s *ServerService) GetConfigJson() (any, error) { func (s *ServerService) GetConfigJson() (any, error) {
config, err := s.xrayService.GetXrayConfig() config, err := s.xrayService.GetXrayConfig()
if err != nil { if err != nil {
@ -666,6 +741,19 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
return nil return nil
} }
var errorMessages []string
if fileName == "" {
for _, file := range files {
destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), file.FileName)
if err := downloadFile(file.URL, destPath); err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", file.FileName, err))
}
}
} else {
destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), fileName)
var fileURL string var fileURL string
for _, file := range files { for _, file := range files {
if file.FileName == fileName { if file.FileName == fileName {
@ -675,18 +763,21 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
} }
if fileURL == "" { if fileURL == "" {
return common.NewErrorf("File '%s' not found in the list of Geofiles", fileName) errorMessages = append(errorMessages, fmt.Sprintf("File '%s' not found in the list of Geofiles", fileName))
} }
destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), fileName)
if err := downloadFile(fileURL, destPath); err != nil { if err := downloadFile(fileURL, destPath); err != nil {
return common.NewErrorf("Error downloading Geofile '%s': %v", fileName, err) errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", fileName, err))
}
} }
err := s.RestartXrayService() err := s.RestartXrayService()
if err != nil { if err != nil {
return common.NewErrorf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err) errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err))
}
if len(errorMessages) > 0 {
return common.NewErrorf("%s", strings.Join(errorMessages, "\r\n"))
} }
return nil return nil
@ -743,3 +834,27 @@ func (s *ServerService) GetNewmldsa65() (any, error) {
return keyPair, nil return keyPair, nil
} }
func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
if len(lines) < 4 {
return nil, common.NewError("invalid ech cert")
}
configList := lines[1]
serverKeys := lines[3]
return map[string]interface{}{
"echServerKeys": serverKeys,
"echConfigList": configList,
}, nil
}

View file

@ -32,7 +32,7 @@ var defaultValueMap = map[string]string{
"webKeyFile": "", "webKeyFile": "",
"secret": random.Seq(32), "secret": random.Seq(32),
"webBasePath": "/", "webBasePath": "/",
"sessionMaxAge": "60", "sessionMaxAge": "360",
"pageSize": "50", "pageSize": "50",
"expireDiff": "0", "expireDiff": "0",
"trafficDiff": "0", "trafficDiff": "0",

View file

@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"crypto/rand" "crypto/rand"
"embed" "embed"
"encoding/base64" "encoding/base64"
@ -148,7 +149,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
} }
// After bot initialization, set up bot commands with localized descriptions // After bot initialization, set up bot commands with localized descriptions
err = bot.SetMyCommands(&telego.SetMyCommandsParams{ err = bot.SetMyCommands(context.Background(), &telego.SetMyCommandsParams{
Commands: []telego.BotCommand{ Commands: []telego.BotCommand{
{Command: "start", Description: t.I18nBot("tgbot.commands.startDesc")}, {Command: "start", Description: t.I18nBot("tgbot.commands.startDesc")},
{Command: "help", Description: t.I18nBot("tgbot.commands.helpDesc")}, {Command: "help", Description: t.I18nBot("tgbot.commands.helpDesc")},
@ -221,8 +222,9 @@ func (t *Tgbot) SetHostname() {
} }
func (t *Tgbot) Stop() { func (t *Tgbot) Stop() {
if botHandler != nil {
botHandler.Stop() botHandler.Stop()
bot.StopLongPolling() }
logger.Info("Stop Telegram receiver ...") logger.Info("Stop Telegram receiver ...")
isRunning = false isRunning = false
adminIds = nil adminIds = nil
@ -255,26 +257,29 @@ func (t *Tgbot) OnReceive() {
Timeout: 10, Timeout: 10,
} }
updates, _ := bot.UpdatesViaLongPolling(&params) updates, _ := bot.UpdatesViaLongPolling(context.Background(), &params)
botHandler, _ = th.NewBotHandler(bot, updates) botHandler, _ = th.NewBotHandler(bot, updates)
botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove()) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
return nil
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
return nil
}, th.AnyCommand()) }, th.AnyCommand())
botHandler.HandleCallbackQuery(func(_ *telego.Bot, query telego.CallbackQuery) { botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
delete(userStates, query.Message.GetChat().ID) delete(userStates, query.Message.GetChat().ID)
t.answerCallback(&query, checkAdmin(query.From.ID)) t.answerCallback(&query, checkAdmin(query.From.ID))
return nil
}, th.AnyCallbackQueryWithMessage()) }, th.AnyCallbackQueryWithMessage())
botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
if userState, exists := userStates[message.Chat.ID]; exists { if userState, exists := userStates[message.Chat.ID]; exists {
switch userState { switch userState {
case "awaiting_id": case "awaiting_id":
@ -284,7 +289,7 @@ func (t *Tgbot) OnReceive() {
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text) t.addClient(message.Chat.ID, message_text)
return return nil
} }
client_Id = strings.TrimSpace(message.Text) client_Id = strings.TrimSpace(message.Text)
@ -309,7 +314,7 @@ func (t *Tgbot) OnReceive() {
if client_TrPassword == strings.TrimSpace(message.Text) { if client_TrPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
return return nil
} }
client_TrPassword = strings.TrimSpace(message.Text) client_TrPassword = strings.TrimSpace(message.Text)
@ -334,7 +339,7 @@ func (t *Tgbot) OnReceive() {
if client_ShPassword == strings.TrimSpace(message.Text) { if client_ShPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
return return nil
} }
client_ShPassword = strings.TrimSpace(message.Text) client_ShPassword = strings.TrimSpace(message.Text)
@ -359,7 +364,7 @@ func (t *Tgbot) OnReceive() {
if client_Email == strings.TrimSpace(message.Text) { if client_Email == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
return return nil
} }
client_Email = strings.TrimSpace(message.Text) client_Email = strings.TrimSpace(message.Text)
@ -384,7 +389,7 @@ func (t *Tgbot) OnReceive() {
if client_Comment == strings.TrimSpace(message.Text) { if client_Comment == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
return return nil
} }
client_Comment = strings.TrimSpace(message.Text) client_Comment = strings.TrimSpace(message.Text)
@ -417,6 +422,7 @@ func (t *Tgbot) OnReceive() {
} }
} }
} }
return nil
}, th.AnyMessage()) }, th.AnyMessage())
botHandler.Start() botHandler.Start()
@ -699,7 +705,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} }
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(chatId, message_text, messageId) t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
case "add_client_limit_traffic_in": case "add_client_limit_traffic_in":
if len(dataArray) >= 2 { if len(dataArray) >= 2 {
@ -914,7 +920,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} }
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(chatId, message_text, messageId) t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
case "add_client_reset_exp_in": case "add_client_reset_exp_in":
if len(dataArray) >= 2 { if len(dataArray) >= 2 {
@ -1096,7 +1102,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} }
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(chatId, message_text, messageId) t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
case "add_client_ip_limit_in": case "add_client_ip_limit_in":
if len(dataArray) >= 2 { if len(dataArray) >= 2 {
@ -1157,8 +1163,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return return
} }
} }
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "clear_ips": case "clear_ips":
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@ -1285,7 +1289,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(chatId, message_text) t.addClient(callbackQuery.Message.GetChat().ID, message_text)
} }
return return
} else { } else {
@ -1859,7 +1863,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
if len(replyMarkup) > 0 && n == (len(allMessages)-1) { if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
params.ReplyMarkup = replyMarkup[0] params.ReplyMarkup = replyMarkup[0]
} }
_, err := bot.SendMessage(&params) _, err := bot.SendMessage(context.Background(), &params)
if err != nil { if err != nil {
logger.Warning("Error sending telegram message :", err) logger.Warning("Error sending telegram message :", err)
} }
@ -2765,7 +2769,7 @@ func (t *Tgbot) sendBackup(chatId int64) {
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(document) _, err = bot.SendDocument(context.Background(), document)
if err != nil { if err != nil {
logger.Error("Error in uploading backup: ", err) logger.Error("Error in uploading backup: ", err)
} }
@ -2779,7 +2783,7 @@ func (t *Tgbot) sendBackup(chatId int64) {
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(document) _, err = bot.SendDocument(context.Background(), document)
if err != nil { if err != nil {
logger.Error("Error in uploading config.json: ", err) logger.Error("Error in uploading config.json: ", err)
} }
@ -2803,7 +2807,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(document) _, err = bot.SendDocument(context.Background(), document)
if err != nil { if err != nil {
logger.Error("Error in uploading IPLimitBannedPrevLog: ", err) logger.Error("Error in uploading IPLimitBannedPrevLog: ", err)
} }
@ -2824,7 +2828,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(document) _, err = bot.SendDocument(context.Background(), document)
if err != nil { if err != nil {
logger.Error("Error in uploading IPLimitBannedLog: ", err) logger.Error("Error in uploading IPLimitBannedLog: ", err)
} }
@ -2842,7 +2846,7 @@ func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
CallbackQueryID: id, CallbackQueryID: id,
Text: message, Text: message,
} }
if err := bot.AnswerCallbackQuery(&params); err != nil { if err := bot.AnswerCallbackQuery(context.Background(), &params); err != nil {
logger.Warning(err) logger.Warning(err)
} }
} }
@ -2853,7 +2857,7 @@ func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyb
MessageID: messageID, MessageID: messageID,
ReplyMarkup: inlineKeyboard, ReplyMarkup: inlineKeyboard,
} }
if _, err := bot.EditMessageReplyMarkup(&params); err != nil { if _, err := bot.EditMessageReplyMarkup(context.Background(), &params); err != nil {
logger.Warning(err) logger.Warning(err)
} }
} }
@ -2868,7 +2872,7 @@ func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlin
if len(inlineKeyboard) > 0 { if len(inlineKeyboard) > 0 {
params.ReplyMarkup = inlineKeyboard[0] params.ReplyMarkup = inlineKeyboard[0]
} }
if _, err := bot.EditMessageText(&params); err != nil { if _, err := bot.EditMessageText(context.Background(), &params); err != nil {
logger.Warning(err) logger.Warning(err)
} }
} }
@ -2881,7 +2885,7 @@ func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSecon
} }
// Send the message // Send the message
sentMsg, err := bot.SendMessage(&telego.SendMessageParams{ sentMsg, err := bot.SendMessage(context.Background(), &telego.SendMessageParams{
ChatID: tu.ID(chatId), ChatID: tu.ID(chatId),
Text: msg, Text: msg,
ReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value ReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value
@ -2904,7 +2908,7 @@ func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
ChatID: tu.ID(chatId), ChatID: tu.ID(chatId),
MessageID: messageID, MessageID: messageID,
} }
if err := bot.DeleteMessage(&params); err != nil { if err := bot.DeleteMessage(context.Background(), &params); err != nil {
logger.Warning("Failed to delete message:", err) logger.Warning("Failed to delete message:", err)
} else { } else {
logger.Info("Message deleted successfully") logger.Info("Message deleted successfully")

View file

@ -3,6 +3,7 @@ package service
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"runtime"
"sync" "sync"
"x-ui/logger" "x-ui/logger"
@ -14,7 +15,8 @@ import (
var ( var (
p *xray.Process p *xray.Process
lock sync.Mutex lock sync.Mutex
isNeedXrayRestart atomic.Bool isNeedXrayRestart atomic.Bool // Indicates that restart was requested for Xray
isManuallyStopped atomic.Bool // Indicates that Xray was stopped manually from the panel
result string result string
) )
@ -32,7 +34,16 @@ func (s *XrayService) GetXrayErr() error {
if p == nil { if p == nil {
return nil return nil
} }
return p.GetErr()
err := p.GetErr()
if runtime.GOOS == "windows" && err.Error() == "exit status 1" {
// exit status 1 on Windows means that Xray process was killed
// as we kill process to stop in on Windows, this is not an error
return nil
}
return err
} }
func (s *XrayService) GetXrayResult() string { func (s *XrayService) GetXrayResult() string {
@ -45,7 +56,15 @@ func (s *XrayService) GetXrayResult() string {
if p == nil { if p == nil {
return "" return ""
} }
result = p.GetResult() result = p.GetResult()
if runtime.GOOS == "windows" && result == "exit status 1" {
// exit status 1 on Windows means that Xray process was killed
// as we kill process to stop in on Windows, this is not an error
return ""
}
return result return result
} }
@ -184,7 +203,8 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic,
func (s *XrayService) RestartXray(isForce bool) error { func (s *XrayService) RestartXray(isForce bool) error {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
logger.Debug("restart xray, force:", isForce) logger.Debug("restart Xray, force:", isForce)
isManuallyStopped.Store(false)
xrayConfig, err := s.GetXrayConfig() xrayConfig, err := s.GetXrayConfig()
if err != nil { if err != nil {
@ -192,8 +212,8 @@ func (s *XrayService) RestartXray(isForce bool) error {
} }
if s.IsXrayRunning() { if s.IsXrayRunning() {
if !isForce && p.GetConfig().Equals(xrayConfig) { if !isForce && p.GetConfig().Equals(xrayConfig) && !isNeedXrayRestart.Load() {
logger.Debug("It does not need to restart xray") logger.Debug("It does not need to restart Xray")
return nil return nil
} }
p.Stop() p.Stop()
@ -205,12 +225,14 @@ func (s *XrayService) RestartXray(isForce bool) error {
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (s *XrayService) StopXray() error { func (s *XrayService) StopXray() error {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
isManuallyStopped.Store(true)
logger.Debug("Attempting to stop Xray...") logger.Debug("Attempting to stop Xray...")
if s.IsXrayRunning() { if s.IsXrayRunning() {
return p.Stop() return p.Stop()
@ -225,3 +247,8 @@ func (s *XrayService) SetToNeedRestart() {
func (s *XrayService) IsNeedRestartAndSetFalse() bool { func (s *XrayService) IsNeedRestartAndSetFalse() bool {
return isNeedXrayRestart.CompareAndSwap(true, false) return isNeedXrayRestart.CompareAndSwap(true, false)
} }
// Check if Xray is not running and wasn't stopped manually, i.e. crashed
func (s *XrayService) DidXrayCrash() bool {
return !s.IsXrayRunning() && !isManuallyStopped.Load()
}

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "تم تحديث Xray بنجاح" "xraySwitchVersionPopover" = "تم تحديث Xray بنجاح"
"geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟" "geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟"
"geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#." "geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#."
"geofilesUpdateDialogDesc" = "سيؤدي هذا إلى تحديث كافة الملفات."
"geofilesUpdateAll" = "تحديث الكل"
"geofileUpdatePopover" = "تم تحديث ملف الجغرافيا بنجاح" "geofileUpdatePopover" = "تم تحديث ملف الجغرافيا بنجاح"
"dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة" "dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة"
"logs" = "السجلات" "logs" = "السجلات"
@ -158,6 +160,7 @@
"remark" = "ملاحظة" "remark" = "ملاحظة"
"protocol" = "بروتوكول" "protocol" = "بروتوكول"
"port" = "بورت" "port" = "بورت"
"portMap" = "خريطة البورت"
"traffic" = "الترافيك" "traffic" = "الترافيك"
"details" = "تفاصيل" "details" = "تفاصيل"
"transportConfig" = "نقل" "transportConfig" = "نقل"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray updated successfully" "xraySwitchVersionPopover" = "Xray updated successfully"
"geofileUpdateDialog" = "Do you really want to update the geofile?" "geofileUpdateDialog" = "Do you really want to update the geofile?"
"geofileUpdateDialogDesc" = "This will update the #filename# file." "geofileUpdateDialogDesc" = "This will update the #filename# file."
"geofilesUpdateDialogDesc" = "This will update all geofiles."
"geofilesUpdateAll" = "Update all"
"geofileUpdatePopover" = "Geofile updated successfully" "geofileUpdatePopover" = "Geofile updated successfully"
"dontRefresh" = "Installation is in progress, please do not refresh this page" "dontRefresh" = "Installation is in progress, please do not refresh this page"
"logs" = "Logs" "logs" = "Logs"
@ -158,6 +160,7 @@
"remark" = "Remark" "remark" = "Remark"
"protocol" = "Protocol" "protocol" = "Protocol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Details" "details" = "Details"
"transportConfig" = "Transport" "transportConfig" = "Transport"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray se actualizó correctamente" "xraySwitchVersionPopover" = "Xray se actualizó correctamente"
"geofileUpdateDialog" = "¿Realmente deseas actualizar el geofichero?" "geofileUpdateDialog" = "¿Realmente deseas actualizar el geofichero?"
"geofileUpdateDialogDesc" = "Esto actualizará el archivo #filename#." "geofileUpdateDialogDesc" = "Esto actualizará el archivo #filename#."
"geofilesUpdateDialogDesc" = "Esto actualizará todos los archivos."
"geofilesUpdateAll" = "Actualizar todo"
"geofileUpdatePopover" = "Geofichero actualizado correctamente" "geofileUpdatePopover" = "Geofichero actualizado correctamente"
"dontRefresh" = "La instalación está en progreso, por favor no actualices esta página." "dontRefresh" = "La instalación está en progreso, por favor no actualices esta página."
"logs" = "Registros" "logs" = "Registros"
@ -158,6 +160,7 @@
"remark" = "Notas" "remark" = "Notas"
"protocol" = "Protocolo" "protocol" = "Protocolo"
"port" = "Puerto" "port" = "Puerto"
"portMap" = "Puertos de Destino"
"traffic" = "Tráfico" "traffic" = "Tráfico"
"details" = "Detalles" "details" = "Detalles"
"transportConfig" = "Transporte" "transportConfig" = "Transporte"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray با موفقیت به‌روز شد" "xraySwitchVersionPopover" = "Xray با موفقیت به‌روز شد"
"geofileUpdateDialog" = "آیا واقعاً می‌خواهید فایل جغرافیایی را به‌روز کنید؟" "geofileUpdateDialog" = "آیا واقعاً می‌خواهید فایل جغرافیایی را به‌روز کنید؟"
"geofileUpdateDialogDesc" = "این عمل فایل #filename# را به‌روز می‌کند." "geofileUpdateDialogDesc" = "این عمل فایل #filename# را به‌روز می‌کند."
"geofilesUpdateDialogDesc" = "با این کار همه فایل‌ها به‌روزرسانی می‌شوند."
"geofilesUpdateAll" = "همه را به‌روزرسانی کنید"
"geofileUpdatePopover" = "فایل جغرافیایی با موفقیت به‌روز شد" "geofileUpdatePopover" = "فایل جغرافیایی با موفقیت به‌روز شد"
"dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید" "dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید"
"logs" = "گزارش‌ها" "logs" = "گزارش‌ها"
@ -158,6 +160,7 @@
"remark" = "نام" "remark" = "نام"
"protocol" = "پروتکل" "protocol" = "پروتکل"
"port" = "پورت" "port" = "پورت"
"portMap" = "پورت‌های نظیر"
"traffic" = "ترافیک" "traffic" = "ترافیک"
"details" = "توضیحات" "details" = "توضیحات"
"transportConfig" = "نحوه اتصال" "transportConfig" = "نحوه اتصال"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray berhasil diperbarui" "xraySwitchVersionPopover" = "Xray berhasil diperbarui"
"geofileUpdateDialog" = "Apakah Anda yakin ingin memperbarui geofile?" "geofileUpdateDialog" = "Apakah Anda yakin ingin memperbarui geofile?"
"geofileUpdateDialogDesc" = "Ini akan memperbarui file #filename#." "geofileUpdateDialogDesc" = "Ini akan memperbarui file #filename#."
"geofilesUpdateDialogDesc" = "Ini akan memperbarui semua berkas."
"geofilesUpdateAll" = "Perbarui semua"
"geofileUpdatePopover" = "Geofile berhasil diperbarui" "geofileUpdatePopover" = "Geofile berhasil diperbarui"
"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini" "dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini"
"logs" = "Log" "logs" = "Log"
@ -158,6 +160,7 @@
"remark" = "Catatan" "remark" = "Catatan"
"protocol" = "Protokol" "protocol" = "Protokol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Rincian" "details" = "Rincian"
"transportConfig" = "Transport" "transportConfig" = "Transport"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xrayの更新が成功しました" "xraySwitchVersionPopover" = "Xrayの更新が成功しました"
"geofileUpdateDialog" = "ジオファイルを本当に更新しますか?" "geofileUpdateDialog" = "ジオファイルを本当に更新しますか?"
"geofileUpdateDialogDesc" = "これにより#filename#ファイルが更新されます。" "geofileUpdateDialogDesc" = "これにより#filename#ファイルが更新されます。"
"geofilesUpdateDialogDesc" = "これにより、すべてのファイルが更新されます。"
"geofilesUpdateAll" = "すべて更新"
"geofileUpdatePopover" = "ジオファイルの更新が成功しました" "geofileUpdatePopover" = "ジオファイルの更新が成功しました"
"dontRefresh" = "インストール中、このページをリロードしないでください" "dontRefresh" = "インストール中、このページをリロードしないでください"
"logs" = "ログ" "logs" = "ログ"
@ -158,6 +160,7 @@
"remark" = "備考" "remark" = "備考"
"protocol" = "プロトコル" "protocol" = "プロトコル"
"port" = "ポート" "port" = "ポート"
"portMap" = "ポートマッピング"
"traffic" = "トラフィック" "traffic" = "トラフィック"
"details" = "詳細情報" "details" = "詳細情報"
"transportConfig" = "トランスポート設定" "transportConfig" = "トランスポート設定"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray atualizado com sucesso" "xraySwitchVersionPopover" = "Xray atualizado com sucesso"
"geofileUpdateDialog" = "Você realmente deseja atualizar o geofile?" "geofileUpdateDialog" = "Você realmente deseja atualizar o geofile?"
"geofileUpdateDialogDesc" = "Isso atualizará o arquivo #filename#." "geofileUpdateDialogDesc" = "Isso atualizará o arquivo #filename#."
"geofilesUpdateDialogDesc" = "Isso atualizará todos os arquivos."
"geofilesUpdateAll" = "Atualizar tudo"
"geofileUpdatePopover" = "Geofile atualizado com sucesso" "geofileUpdatePopover" = "Geofile atualizado com sucesso"
"dontRefresh" = "Instalação em andamento, por favor não atualize a página" "dontRefresh" = "Instalação em andamento, por favor não atualize a página"
"logs" = "Logs" "logs" = "Logs"
@ -158,6 +160,7 @@
"remark" = "Observação" "remark" = "Observação"
"protocol" = "Protocolo" "protocol" = "Protocolo"
"port" = "Porta" "port" = "Porta"
"portMap" = "Porta Mapeada"
"traffic" = "Tráfego" "traffic" = "Tráfego"
"details" = "Detalhes" "details" = "Detalhes"
"transportConfig" = "Transporte" "transportConfig" = "Transporte"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray успешно обновлён" "xraySwitchVersionPopover" = "Xray успешно обновлён"
"geofileUpdateDialog" = "Вы действительно хотите обновить геофайл?" "geofileUpdateDialog" = "Вы действительно хотите обновить геофайл?"
"geofileUpdateDialogDesc" = "Это обновит файл #filename#." "geofileUpdateDialogDesc" = "Это обновит файл #filename#."
"geofilesUpdateDialogDesc" = "Это обновит все геофайлы."
"geofilesUpdateAll" = "Обновить все"
"geofileUpdatePopover" = "Геофайл успешно обновлён" "geofileUpdatePopover" = "Геофайл успешно обновлён"
"dontRefresh" = "Установка в процессе. Не обновляйте страницу" "dontRefresh" = "Установка в процессе. Не обновляйте страницу"
"logs" = "Журнал" "logs" = "Журнал"
@ -158,6 +160,7 @@
"remark" = "Примечание" "remark" = "Примечание"
"protocol" = "Протокол" "protocol" = "Протокол"
"port" = "Порт" "port" = "Порт"
"portMap" = "Порт-маппинг"
"traffic" = "Трафик" "traffic" = "Трафик"
"details" = "Подробнее" "details" = "Подробнее"
"transportConfig" = "Транспорт" "transportConfig" = "Транспорт"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray başarıyla güncellendi" "xraySwitchVersionPopover" = "Xray başarıyla güncellendi"
"geofileUpdateDialog" = "Geofile'ı gerçekten güncellemek istiyor musunuz?" "geofileUpdateDialog" = "Geofile'ı gerçekten güncellemek istiyor musunuz?"
"geofileUpdateDialogDesc" = "Bu işlem #filename# dosyasını güncelleyecektir." "geofileUpdateDialogDesc" = "Bu işlem #filename# dosyasını güncelleyecektir."
"geofilesUpdateDialogDesc" = "Bu, tüm dosyaları güncelleyecektir."
"geofilesUpdateAll" = "Tümünü güncelle"
"geofileUpdatePopover" = "Geofile başarıyla güncellendi" "geofileUpdatePopover" = "Geofile başarıyla güncellendi"
"dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin" "dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin"
"logs" = "Günlükler" "logs" = "Günlükler"
@ -158,6 +160,7 @@
"remark" = "Açıklama" "remark" = "Açıklama"
"protocol" = "Protokol" "protocol" = "Protokol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Atama"
"traffic" = "Trafik" "traffic" = "Trafik"
"details" = "Detaylar" "details" = "Detaylar"
"transportConfig" = "Taşıma" "transportConfig" = "Taşıma"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray успішно оновлено" "xraySwitchVersionPopover" = "Xray успішно оновлено"
"geofileUpdateDialog" = "Ви дійсно хочете оновити геофайл?" "geofileUpdateDialog" = "Ви дійсно хочете оновити геофайл?"
"geofileUpdateDialogDesc" = "Це оновить файл #filename#." "geofileUpdateDialogDesc" = "Це оновить файл #filename#."
"geofilesUpdateDialogDesc" = "Це оновить усі геофайли."
"geofilesUpdateAll" = "Оновити все"
"geofileUpdatePopover" = "Геофайл успішно оновлено" "geofileUpdatePopover" = "Геофайл успішно оновлено"
"dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку" "dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку"
"logs" = "Журнали" "logs" = "Журнали"
@ -158,6 +160,7 @@
"remark" = "Примітка" "remark" = "Примітка"
"protocol" = "Протокол" "protocol" = "Протокол"
"port" = "Порт" "port" = "Порт"
"portMap" = "Порт-перехід"
"traffic" = "Трафік" "traffic" = "Трафік"
"details" = "Деталі" "details" = "Деталі"
"transportConfig" = "Транспорт" "transportConfig" = "Транспорт"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray đã được cập nhật thành công" "xraySwitchVersionPopover" = "Xray đã được cập nhật thành công"
"geofileUpdateDialog" = "Bạn có chắc chắn muốn cập nhật geofile không?" "geofileUpdateDialog" = "Bạn có chắc chắn muốn cập nhật geofile không?"
"geofileUpdateDialogDesc" = "Hành động này sẽ cập nhật tệp #filename#." "geofileUpdateDialogDesc" = "Hành động này sẽ cập nhật tệp #filename#."
"geofilesUpdateDialogDesc" = "Thao tác này sẽ cập nhật tất cả các tập tin."
"geofilesUpdateAll" = "Cập nhật tất cả"
"geofileUpdatePopover" = "Geofile đã được cập nhật thành công" "geofileUpdatePopover" = "Geofile đã được cập nhật thành công"
"dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này." "dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này."
"logs" = "Nhật ký" "logs" = "Nhật ký"
@ -158,6 +160,7 @@
"remark" = "Chú thích" "remark" = "Chú thích"
"protocol" = "Giao thức" "protocol" = "Giao thức"
"port" = "Cổng" "port" = "Cổng"
"portMap" = "Cổng tạo"
"traffic" = "Lưu lượng" "traffic" = "Lưu lượng"
"details" = "Chi tiết" "details" = "Chi tiết"
"transportConfig" = "Giao vận" "transportConfig" = "Giao vận"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray 更新成功" "xraySwitchVersionPopover" = "Xray 更新成功"
"geofileUpdateDialog" = "您确定要更新地理文件吗?" "geofileUpdateDialog" = "您确定要更新地理文件吗?"
"geofileUpdateDialogDesc" = "这将更新 #filename# 文件。" "geofileUpdateDialogDesc" = "这将更新 #filename# 文件。"
"geofilesUpdateDialogDesc" = "这将更新所有文件。"
"geofilesUpdateAll" = "全部更新"
"geofileUpdatePopover" = "地理文件更新成功" "geofileUpdatePopover" = "地理文件更新成功"
"dontRefresh" = "安装中,请勿刷新此页面" "dontRefresh" = "安装中,请勿刷新此页面"
"logs" = "日志" "logs" = "日志"
@ -158,6 +160,7 @@
"remark" = "备注" "remark" = "备注"
"protocol" = "协议" "protocol" = "协议"
"port" = "端口" "port" = "端口"
"portMap" = "端口映射"
"traffic" = "流量" "traffic" = "流量"
"details" = "详细信息" "details" = "详细信息"
"transportConfig" = "传输配置" "transportConfig" = "传输配置"

View file

@ -132,6 +132,8 @@
"xraySwitchVersionPopover" = "Xray 更新成功" "xraySwitchVersionPopover" = "Xray 更新成功"
"geofileUpdateDialog" = "您確定要更新地理檔案嗎?" "geofileUpdateDialog" = "您確定要更新地理檔案嗎?"
"geofileUpdateDialogDesc" = "這將更新 #filename# 檔案。" "geofileUpdateDialogDesc" = "這將更新 #filename# 檔案。"
"geofilesUpdateDialogDesc" = "這將更新所有文件。"
"geofilesUpdateAll" = "全部更新"
"geofileUpdatePopover" = "地理檔案更新成功" "geofileUpdatePopover" = "地理檔案更新成功"
"dontRefresh" = "安裝中,請勿重新整理此頁面" "dontRefresh" = "安裝中,請勿重新整理此頁面"
"logs" = "日誌" "logs" = "日誌"
@ -158,6 +160,7 @@
"remark" = "備註" "remark" = "備註"
"protocol" = "協議" "protocol" = "協議"
"port" = "埠" "port" = "埠"
"portMap" = "埠映射"
"traffic" = "流量" "traffic" = "流量"
"details" = "詳細資訊" "details" = "詳細資訊"
"transportConfig" = "傳輸配置" "transportConfig" = "傳輸配置"