Compare commits

...

80 Commits

Author SHA1 Message Date
mhsanaei
7b039d219e v2.6.8 2025-09-08 00:29:30 +02:00
mhsanaei
dbec28b915 remove unsupported cipher method 2025-09-07 22:55:37 +02:00
mhsanaei
e5126806d7 xray core v25.9.5 2025-09-07 22:45:20 +02:00
Sanaei
b008ff4ad2 Vlessenc (#3426)
* mlkem768

* VlessEnc
2025-09-07 22:35:38 +02:00
Danil S.
da6b89fdcd chore: login improvements (#3408)
- added client-side form validation
- now with slow internet otp input does not appear later than all input
2025-09-04 12:11:52 +02:00
MHSanaei
d7882c25d1 removed domainMatcher 2025-09-04 12:07:39 +02:00
Ali Golzar
ed2a0a0bcf fix: prevent client updated_at from resetting when parent inbound is updated 2025-09-02 13:30:41 +03:30
Ali Golzar
4a0914cb1e feat: add "Last Online" column to client list and modal (Closes #3402) (#3405)
* feat: persist client last online and expose API

* feat(ui): show client last online in table and info modal

* i18n: add “Last Online” across locales

* chore: format timestamps as HH:mm:ss
2025-08-31 18:33:50 +02:00
Darkcyankitty
664269d513 x-ui.service hardneing (#3397) 2025-08-31 15:26:46 +02:00
Ali Golzar
d0796b26c9 fix(ui): hide Created/Updated columns and fix issues in small displays (#3400)
- Hide the “Created” and “Updated” columns in the clients
- Ensures the “All-time Traffic” column no longer overlaps with adjacent columns.
- Improves layout readability and prevents UI cluttering after the v2.6.7 update.

Closes #3399
2025-08-30 23:01:57 +02:00
mhsanaei
2750f46c01 v2.6.7 2025-08-30 16:05:33 +02:00
mhsanaei
023eb513e4 Xray Core v25.8.29 2025-08-30 10:03:32 +02:00
mhsanaei
0c7b59ed47 removed: Allocate 2025-08-28 10:15:04 +02:00
Ali Golzar
3087c1b123 Add all-time traffic for inbounds and clients (#3387)
* feat(db): add allTime field to Inbound and ClientTraffic models

* feat(inbound): increment all_time for inbounds and clients on traffic updates

calculate correct all_time traffic on migrate command

* feat(ui): show all-time traffic column for inbounds and its clients

* i18n: add pages.inbounds.allTimeTraffic label across locales

* Add All Time Traffic Usage in inbounds page top banner
2025-08-28 01:10:50 +02:00
Ali Golzar
2198397197 Created / Updated fields for clients (#3384)
* feat(backend): add created_at/updated_at to clients and maintain on create/update
backfill existing clients and set updated_at on mutations

* feat(frontend): carry created_at/updated_at in client models and round-trip via JSON

* feat(frontend): display Created and Updated columns in client table with proper date formatting

* i18n: add pages.inbounds.createdAt/updatedAt across all locales

* Update inbound.go

Remove duplicate code
2025-08-27 19:30:49 +02:00
Igor Finagin
d10c312e62 AutoFill OTP (#3381)
https://developer.apple.com/documentation/security/enabling-password-autofill-on-an-html-input-element
2025-08-25 13:42:02 +02:00
mhsanaei
24a3411465 more list for public IP address 2025-08-21 14:24:25 +02:00
Alireza Ahmand
2198e7a28f feat: Add remaining time to tgbot #3355 (#3360) 2025-08-17 13:43:25 +02:00
mhsanaei
6b23b416a7 minor changes 2025-08-17 13:37:49 +02:00
mhsanaei
16f53ce4c2 go v1.25 2025-08-17 12:27:21 +02:00
mhsanaei
27445b30e9 DNS outbound: Set "reject" as the default value for nonIPQuery 2025-08-17 12:22:33 +02:00
mhsanaei
3d0212c21d fix: fail2ban on Debian 12 #1701 2025-08-15 13:34:02 +02:00
mhsanaei
978755960f actions/checkout from 4 to 5 2025-08-14 18:41:53 +02:00
mhsanaei
9b51e9a5c5 Freedom: Add maxSplit fragment option; Add applyTo noises option 2025-08-14 18:38:56 +02:00
fgsfds
6879a8fbcb Moved DB to same app folder on Windows (#3340)
* moved db to user folder on windows

* moved db to local appdata

* made getDBFolderPath func private

* added getWindowsDbPath() func

* fix

---------

Co-authored-by: mhsanaei <ho3ein.sanaei@gmail.com>
2025-08-13 23:19:59 +02:00
mhsanaei
7258841491 Update FUNDING.yml 2025-08-12 13:00:16 +02:00
mhsanaei
23dd80fbb0 remove unnecessary ant files 2025-08-12 12:57:02 +02:00
mhsanaei
6556884c7f remove unnecessary vue files 2025-08-12 12:56:49 +02:00
Alireza Ahmadi
d5c532c64f fix saving sockopt 2025-08-09 16:07:33 +02:00
mhsanaei
ad5f774a1e Axios v1.11.0 2025-08-09 13:46:28 +02:00
g0l4
aa285914fa chore: update polygon token name (#3338) 2025-08-09 08:18:56 +02:00
mhsanaei
4d02756e1e v2.6.6 2025-08-08 20:52:04 +02:00
Alireza Ahmadi
825d93d95f upgrade telego (#3334) 2025-08-08 20:41:06 +02:00
mhsanaei
5ea6386815 better musl libc usage
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2025-08-08 19:55:24 +02:00
mhsanaei
d064e85ecd update dependencies 2025-08-08 18:56:47 +02:00
mhsanaei
9fc03bd10a remove ocspStapling 2025-08-08 18:55:52 +02:00
fgsfds
ae08a29cde fix: Xray restarting after being manually stopped (#2896) (#3329) 2025-08-07 23:35:11 +05:00
mhsanaei
4f25eb230e musl: new version 2025-08-06 23:35:20 +02:00
somebodywashere
ce72d53d1a fix: inbounds slow loading (#3228) (#3322)
especially encountered on big amount of clients
2025-08-06 15:42:32 +02:00
fgsfds
5e641ff9e8 Added Update all geofiles button (#3318)
* added Update all geofiles button

* localized update all string
2025-08-06 11:20:07 +02:00
Sanaei
58898e5758 Merge pull request #3317 from MHSanaei/dekodemo_sockopt
add sockopt to dockodemo
2025-08-05 16:51:26 +02:00
Alireza Ahmadi
569550d5f6 add sockopt to dockodemo 2025-08-05 14:02:23 +02:00
fgsfds
419ea63dd0 Added filters to the xray logs viewer (#3314)
* added filters to xray logs viewer

* better freedom/blackhole tags handling

* better freedom/blackhole tags handling 2

* fix comments

* fix comments 2
2025-08-05 12:10:14 +02:00
mhsanaei
6a17285935 remove: glibc check
now you can install on all OS like ubuntu 20 or 18
2025-08-04 19:16:56 +02:00
mhsanaei
4b03e9d919 v2.6.5 2025-08-04 19:12:37 +02:00
mhsanaei
7e9c3bdbaf fix: sub enable warning 2025-08-04 19:09:01 +02:00
fgsfds
957f3dbb54 Added xray access log viewer (#3309)
* added xray access log viewer

* made modal window width adaptive

* hide logs button if xray logs are disabled
2025-08-04 18:47:48 +02:00
mhsanaei
05e60af283 fix: IPLimitlog display 2025-08-04 18:23:37 +02:00
mhsanaei
5e40458116 fix: simplify error handling 2025-08-04 18:01:32 +02:00
Sanaei
baf6fdd29d fix portMap json (#3312)
Co-authored-by: Alireza Ahmadi <alireza7@gmail.com>
2025-08-04 17:16:11 +02:00
Sanaei
45f78d3521 Merge pull request #3311 from MHSanaei/dokodemo_portMap
add dokodemo port mapping
2025-08-04 17:01:57 +02:00
Alireza Ahmadi
01f984e054 add dokodemo port mapping 2025-08-04 16:45:09 +02:00
Sanaei
e4ba5ba53a add ech support (#3310)
Co-authored-by: Alireza Ahmadi <alireza7@gmail.com>
2025-08-04 16:27:57 +02:00
mhsanaei
6ff555c8bb runs-on: ubuntu-latest 2025-08-04 14:39:12 +02:00
elseif
3c1634ca7c use musl libc toolchains for build statically linked binaries (#3191) 2025-08-04 11:50:39 +02:00
mhsanaei
561c4810be default Max Age to 360min 2025-08-04 11:38:23 +02:00
mhsanaei
eb1b96643d update languages 2025-08-04 11:22:09 +02:00
mhsanaei
de5314c01f fix: pqv for sub #3306 2025-08-04 10:24:21 +02:00
mhsanaei
1088d1faf3 minor changes 2025-08-04 01:30:01 +02:00
mhsanaei
267024c43f xray core v25.8.3 2025-08-04 01:28:14 +02:00
mhsanaei
0d595f56e4 change a-input to a-textarea 2025-08-04 00:57:06 +02:00
fgsfds
a4c4f9efb3 kill process instead of sending SIGTERM on Windows (#3304) 2025-08-04 00:45:50 +02:00
mhsanaei
73a5722cca v2.6.3 2025-08-03 12:22:28 +02:00
mhsanaei
30264043f8 Xray core: old version removed 2025-08-03 12:22:06 +02:00
mhsanaei
c6062eb15c outbound: mldsa65Verify 2025-08-03 12:09:37 +02:00
mhsanaei
f1b7944828 pqv: mldsa65Verify 2025-08-03 12:01:49 +02:00
mhsanaei
7a57b31ff3 remove password type 2025-07-28 14:44:00 +02:00
mhsanaei
6e1b949081 Reality: min & max client ver 2025-07-28 13:45:47 +02:00
Mikhail Grigorev
0ad708b1b6 Added list of services for get public IP address (IP v4 and v6) (#3216)
* Fixed get public IP address

* Remove https://ifconfig.io/ip and https://ipinfo.tw/ip

---------

Co-authored-by: Mikhail Grigorev <grigorev_mm@magnit.ru>
2025-07-27 17:24:11 +02:00
X-Oracle
71f13ebcbd small improvement (#3277) 2025-07-27 17:22:59 +02:00
mhsanaei
f5f4a530cc xray core v25.7.26 2025-07-27 16:54:35 +02:00
Sanaei
702f03e4b7 Merge pull request #3279 from MHSanaei/mldsa65
add mldsa65
2025-07-27 16:52:44 +02:00
Alireza Ahmadi
487ec74e0b add mldsa65 2025-07-25 01:22:01 +02:00
mhsanaei
b4dae36345 xray core v25.7.24 2025-07-24 15:26:55 +02:00
mhsanaei
761728255c GO v1.24.5 + update dependencies 2025-07-24 15:17:01 +02:00
Azavax
b1ab156e42 Endpoint for updating client traffic by email (#3259)
* Update api.go

* Update inbound.go

* Update inbound.go
2025-07-22 23:43:48 +02:00
xujie86
fa45bf87de Update install.sh (#3267) 2025-07-22 23:28:56 +02:00
xujie86
75416eebd7 Increase the number of characters for webBasePath (#3239) 2025-07-22 12:53:12 +02:00
mhsanaei
87042d77ba ocspStapling set to 0 2025-07-22 12:49:02 +02:00
Danilo Nascimento
011e0f309a fix writeCrashReport() typo (#3218) 2025-07-10 02:36:02 +02:00
93 changed files with 2125 additions and 74862 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,6 +1,6 @@
# 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
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username

View File

@@ -7,10 +7,10 @@ on:
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
submodules: true

View File

@@ -7,7 +7,10 @@ on:
push:
branches:
- main
tags:
- "v*.*.*"
paths:
- '.github/workflows/release.yml'
- '**.js'
- '**.css'
- '**.html'
@@ -31,10 +34,10 @@ jobs:
- 386
- armv5
- s390x
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v5
@@ -42,51 +45,37 @@ jobs:
go-version-file: go.mod
check-latest: true
- name: Install dependencies
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
- name: Build 3X-UI
run: |
export CGO_ENABLED=1
export GOOS=linux
export GOARCH=${{ matrix.platform }}
if [ "${{ matrix.platform }}" == "arm64" ]; then
export GOARCH=arm64
export CC=aarch64-linux-gnu-gcc
elif [ "${{ matrix.platform }}" == "armv7" ]; then
export GOARCH=arm
export GOARM=7
export CC=arm-linux-gnueabihf-gcc
elif [ "${{ matrix.platform }}" == "armv6" ]; then
export GOARCH=arm
export GOARM=6
export CC=arm-linux-gnueabihf-gcc
elif [ "${{ matrix.platform }}" == "386" ]; then
export GOARCH=386
export CC=i686-linux-gnu-gcc
elif [ "${{ matrix.platform }}" == "armv5" ]; then
export GOARCH=arm
export GOARM=5
export CC=arm-linux-gnueabi-gcc
elif [ "${{ matrix.platform }}" == "s390x" ]; then
export GOARCH=s390x
export CC=s390x-linux-gnu-gcc
fi
go build -ldflags "-w -s" -o xui-release -v main.go
# Use Bootlin prebuilt cross-toolchains (musl 1.2.5 in stable series)
case "${{ matrix.platform }}" in
amd64) BOOTLIN_ARCH="x86-64" ;;
arm64) BOOTLIN_ARCH="aarch64" ;;
armv7) BOOTLIN_ARCH="armv7-eabihf"; export GOARCH=arm GOARM=7 ;;
armv6) BOOTLIN_ARCH="armv6-eabihf"; export GOARCH=arm GOARM=6 ;;
armv5) BOOTLIN_ARCH="armv5-eabi"; export GOARCH=arm GOARM=5 ;;
386) BOOTLIN_ARCH="x86-i686" ;;
s390x) BOOTLIN_ARCH="s390x-z13" ;;
esac
echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})"
TARBALL_BASE="https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/"
TARBALL_URL=$(curl -fsSL "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1)
[ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
echo "Downloading: $TARBALL_URL"
cd /tmp
curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
tar -xf "$(basename "$TARBALL_URL")"
TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH"
export CC=$(realpath "$(find "$TOOLCHAIN_DIR/bin" -name '*-gcc.br_real' -type f -executable | head -n1)")
[ -z "$CC" ] && { echo "No gcc.br_real found in $TOOLCHAIN_DIR/bin" >&2; exit 1; }
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
cp xui-release x-ui/
@@ -97,7 +86,7 @@ jobs:
cd x-ui/bin
# Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.6.8/"
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.9.5/"
if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip

4
.gitignore vendored
View File

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

View File

@@ -27,7 +27,7 @@ case $1 in
esac
mkdir -p build/bin
cd build/bin
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.6.8/Xray-linux-${ARCH}.zip"
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.9.5/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}"

View File

@@ -1,7 +1,7 @@
# ========================================================
# Stage: Builder
# ========================================================
FROM golang:1.24-alpine AS builder
FROM golang:1.25-alpine AS builder
WORKDIR /app
ARG TARGETARCH

View File

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

View File

@@ -48,7 +48,7 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## 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>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## ستاره‌ها در طول زمان

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,10 @@ package config
import (
_ "embed"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
)
@@ -54,12 +57,32 @@ func GetBinFolderPath() string {
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 {
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
if dbFolderPath == "" {
dbFolderPath = "/etc/x-ui"
if dbFolderPath != "" {
return dbFolderPath
}
return dbFolderPath
if runtime.GOOS == "windows" {
return getBaseDir()
}
return "/etc/x-ui"
}
func GetDBPath() string {
@@ -68,8 +91,54 @@ func GetDBPath() string {
func GetLogFolder() string {
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
if logFolderPath == "" {
logFolderPath = "/var/log"
if logFolderPath != "" {
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.2
2.6.8

View File

@@ -32,6 +32,7 @@ type Inbound struct {
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
Total int64 `json:"total" form:"total"`
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"`
Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
@@ -45,7 +46,6 @@ type Inbound struct {
StreamSettings string `json:"streamSettings" form:"streamSettings"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Sniffing string `json:"sniffing" form:"sniffing"`
Allocate string `json:"allocate" form:"allocate"`
}
type OutboundTraffics struct {
@@ -80,7 +80,6 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
StreamSettings: json_util.RawMessage(i.StreamSettings),
Tag: i.Tag,
Sniffing: json_util.RawMessage(i.Sniffing),
Allocate: json_util.RawMessage(i.Allocate),
}
}
@@ -104,4 +103,13 @@ type Client struct {
SubID string `json:"subId" form:"subId"`
Comment string `json:"comment" form:"comment"`
Reset int `json:"reset" form:"reset"`
CreatedAt int64 `json:"created_at,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}
type VLESSSettings struct {
Clients []Client `json:"clients"`
Decryption string `json:"decryption"`
Encryption string `json:"encryption"`
Fallbacks []any `json:"fallbacks"`
}

75
go.mod
View File

@@ -1,6 +1,6 @@
module x-ui
go 1.24.4
go 1.25.0
require (
github.com/gin-contrib/gzip v1.2.3
@@ -9,41 +9,39 @@ require (
github.com/goccy/go-json v0.10.5
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mymmrac/telego v0.32.0
github.com/mymmrac/telego v1.3.0
github.com/nicksnyder/go-i18n/v2 v2.6.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.6
github.com/valyala/fasthttp v1.63.0
github.com/shirou/gopsutil/v4 v4.25.8
github.com/valyala/fasthttp v1.65.0
github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.250608.0
github.com/xtls/xray-core v1.250905.0
go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.39.0
golang.org/x/text v0.26.0
google.golang.org/grpc v1.73.0
golang.org/x/crypto v0.41.0
golang.org/x/text v0.28.0
google.golang.org/grpc v1.75.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.30.0
gorm.io/gorm v1.30.2
)
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // 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/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // 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.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
@@ -54,24 +52,24 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.52.0 // indirect
github.com/refraction-networking/utls v1.7.3 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/refraction-networking/utls v1.8.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.6.6 // indirect
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect
github.com/sagernet/sing v0.7.7 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
@@ -82,23 +80,22 @@ require (
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20250627141458-e62c4aed0d57 // indirect
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.2 // indirect
go.uber.org/mock v0.6.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.34.0 // indirect
golang.org/x/arch v0.21.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/time v0.13.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
google.golang.org/protobuf v1.36.8 // 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
)

189
go.sum
View File

@@ -1,19 +1,18 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJSGpevP+8Pk5bANX7fJacO2w04aqLiC5I=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
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/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
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.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -22,10 +21,8 @@ github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mT
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
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/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
@@ -36,8 +33,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -51,8 +48,6 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
@@ -66,8 +61,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ=
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
@@ -92,37 +85,31 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
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.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY=
github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M=
github.com/mymmrac/telego v1.3.0 h1:y2bDDCioLgkcs+5luUaPgTNHKel1Qh30iUxFcMUrowg=
github.com/mymmrac/telego v1.3.0/go.mod h1:0D2l/IA/gUFn4oqsi1O4/tSnlezw5jNV/ReFRDUEKk8=
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/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
@@ -135,41 +122,36 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo=
github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.6.6 h1:3JkvJ0vqDj/jJcx0a+ve/6lMOrSzZm30I3wrIuZtmRE=
github.com/sagernet/sing v0.6.6/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
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/sagernet/sing v0.7.7 h1:o46FzVZS+wKbBMEkMEdEHoVZxyM9jvfRpKXc7pEgS/c=
github.com/sagernet/sing v0.7.7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
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.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.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
@@ -182,8 +164,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.63.0 h1:DisIL8OjB7ul2d7cBaMRcKTQDYnrGy56R4FCiuDP0Ns=
github.com/valyala/fasthttp v1.63.0/go.mod h1:REc4IeW+cAEyLrRPa5A81MIjvz0QE1laoTX2EaPHKJM=
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@@ -192,68 +174,68 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xtls/reality v0.0.0-20250627141458-e62c4aed0d57 h1:CJzC54UytAYnNbJSlAFi9MXOofSUAtpoQTKIA3hUpj8=
github.com/xtls/reality v0.0.0-20250627141458-e62c4aed0d57/go.mod h1:yD47RN65bDLZgyHWMfFDiqlzrq4usDMt/Xzsk6tMbhw=
github.com/xtls/xray-core v1.250608.0 h1:/M0LwzFeFAZf+vdZQhqNYjUXDfPv5hOeYw6jsiAdgWI=
github.com/xtls/xray-core v1.250608.0/go.mod h1:MkfIs2WZ5VLtZHAwDKosSS05Kx5zFFOzvly7Hy6pfPs=
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU=
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
github.com/xtls/xray-core v1.250905.0 h1:VNL3l/6fcwyeYXJTRbf+TYqPfJYkk0Wmmz7qoQNkxY8=
github.com/xtls/xray-core v1.250905.0/go.mod h1:WB/73DmN9Vs7lxtx4Xc/D0Ub1VUu06hAh1mMh8JN2uM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
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.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
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/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
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/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -265,10 +247,9 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5 h1:sfK5nHuG7lRFZ2FdTT3RimOqWBg8IrVm+/Vko1FVOsk=
gvisor.dev/gvisor v0.0.0-20250428193742-2d800c3129d5/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs=
gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
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/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -39,19 +39,6 @@ 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() {
case "${release}" in
ubuntu | debian | armbian)
@@ -70,7 +57,7 @@ install_base() {
zypper refresh && zypper -q install -y wget curl tar timezone
;;
*)
apt-get update && apt install -y -q wget curl tar tzdata
apt-get update && apt-get install -y -q wget curl tar tzdata
;;
esac
}
@@ -85,14 +72,25 @@ config_after_install() {
local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local server_ip=$(curl -s --max-time 3 https://api.ipify.org)
if [ -z "$server_ip" ]; then
server_ip=$(curl -s --max-time 3 https://4.ident.me)
fi
local URL_lists=(
"https://api4.ipify.org"
"https://ipv4.icanhazip.com"
"https://v4.api.ipinfo.io/ip"
"https://ipv4.myexternalip.com/raw"
"https://4.ident.me"
"https://check-host.net/ip"
)
local server_ip=""
for ip_address in "${URL_lists[@]}"; do
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
if [[ -n "${server_ip}" ]]; then
break
fi
done
if [[ ${#existing_webBasePath} -lt 4 ]]; then
if [[ "$existing_hasDefaultCredential" == "true" ]]; then
local config_webBasePath=$(gen_random_string 15)
local config_webBasePath=$(gen_random_string 18)
local config_username=$(gen_random_string 10)
local config_password=$(gen_random_string 10)
@@ -115,7 +113,7 @@ config_after_install() {
echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}"
echo -e "###############################################"
else
local config_webBasePath=$(gen_random_string 15)
local config_webBasePath=$(gen_random_string 18)
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}"
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
@@ -144,6 +142,7 @@ config_after_install() {
install_x-ui() {
cd /usr/local/
# Download resources
if [ $# == 0 ]; then
tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then
@@ -174,30 +173,35 @@ install_x-ui() {
exit 1
fi
fi
wget -O /usr/bin/x-ui-temp https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
# Stop x-ui service and remove old resources
if [[ -e /usr/local/x-ui/ ]]; then
systemctl stop x-ui
rm /usr/local/x-ui/ -rf
fi
# Extract resources and set permissions
tar zxvf x-ui-linux-$(arch).tar.gz
rm x-ui-linux-$(arch).tar.gz -f
cd x-ui
chmod +x x-ui
chmod +x x-ui.sh
# Check the system's architecture and rename the file accordingly
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
mv bin/xray-linux-$(arch) bin/xray-linux-arm
chmod +x bin/xray-linux-arm
fi
chmod +x x-ui bin/xray-linux-$(arch)
cp -f x-ui.service /etc/systemd/system/
wget -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
chmod +x /usr/local/x-ui/x-ui.sh
# Update x-ui cli and se set permission
mv -f /usr/bin/x-ui-temp /usr/bin/x-ui
chmod +x /usr/bin/x-ui
config_after_install
cp -f x-ui.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui

View File

@@ -53,7 +53,6 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
gJson := g.Group(a.subJsonPath)
gLink.GET(":subid", a.subs)
gJson.GET(":subid", a.subJsons)
}
@@ -85,7 +84,7 @@ func (a *SUBController) subs(c *gin.Context) {
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
if a.subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
@@ -119,7 +118,7 @@ func (a *SUBController) subJsons(c *gin.Context) {
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
c.String(200, jsonSub)
}

View File

@@ -184,8 +184,14 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
var newOutbounds []json_util.RawMessage
switch inbound.Protocol {
case "vmess", "vless":
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
case "vmess":
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, ""))
case "vless":
var vlessSettings model.VLESSSettings
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
newOutbounds = append(newOutbounds,
s.genVnext(inbound, streamSettings, client, vlessSettings.Encryption))
case "trojan", "shadowsocks":
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
}
@@ -209,9 +215,10 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
var streamSettings map[string]any
json.Unmarshal([]byte(stream), &streamSettings)
security, _ := streamSettings["security"].(string)
if security == "tls" {
switch security {
case "tls":
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
} else if security == "reality" {
case "reality":
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
}
delete(streamSettings, "sockopt")
@@ -263,6 +270,7 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
rltyData["show"] = false
rltyData["publicKey"] = rltyClientSettings["publicKey"]
rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
rltyData["mldsa65Verify"] = rltyClientSettings["mldsa65Verify"]
// Set random data
rltyData["spiderX"] = "/" + random.Seq(15)
@@ -282,7 +290,7 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
return rltyData
}
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, encryption string) json_util.RawMessage {
outbound := Outbound{}
usersData := make([]UserVnext, 1)
@@ -293,7 +301,7 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
}
if inbound.Protocol == model.VLESS {
usersData[0].Flow = client.Flow
usersData[0].Encryption = "none"
usersData[0].Encryption = encryption
}
vnextData := make([]VnextSetting, 1)

View File

@@ -313,6 +313,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if inbound.Protocol != model.VLESS {
return ""
}
var vlessSettings model.VLESSSettings
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
var stream map[string]any
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound)
@@ -327,6 +330,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
port := inbound.Port
streamNetwork := stream["network"].(string)
params := make(map[string]string)
if vlessSettings.Encryption != "" {
params["encryption"] = vlessSettings.Encryption
}
params["type"] = streamNetwork
switch streamNetwork {
@@ -437,6 +443,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["fp"] = fp
}
}
if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok {
if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 {
params["pqv"] = pqv
}
}
params["spx"] = "/" + random.Seq(15)
}
@@ -627,6 +638,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["fp"] = fp
}
}
if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok {
if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 {
params["pqv"] = pqv
}
}
params["spx"] = "/" + random.Seq(15)
}

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

@@ -6,6 +6,7 @@ class DBInbound {
this.up = 0;
this.down = 0;
this.total = 0;
this.allTime = 0;
this.remark = "";
this.enable = true;
this.expiryTime = 0;

View File

@@ -11,10 +11,8 @@ const Protocols = {
const SSMethods = {
AES_256_GCM: 'aes-256-gcm',
AES_128_GCM: 'aes-128-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
XCHACHA20_POLY1305: 'xchacha20-poly1305',
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
@@ -559,7 +557,9 @@ class TlsStreamSettings extends XrayCommonClass {
disableSystemRoot = false,
enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()],
alpn = [ALPN_OPTION.H3, ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
echServerKeys = '',
echForceQuery = 'none',
settings = new TlsStreamSettings.Settings()
) {
super();
@@ -573,6 +573,8 @@ class TlsStreamSettings extends XrayCommonClass {
this.enableSessionResumption = enableSessionResumption;
this.certs = certificates;
this.alpn = alpn;
this.echServerKeys = echServerKeys;
this.echForceQuery = echForceQuery;
this.settings = settings;
}
@@ -592,7 +594,7 @@ class TlsStreamSettings extends XrayCommonClass {
}
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(
json.serverName,
@@ -605,6 +607,8 @@ class TlsStreamSettings extends XrayCommonClass {
json.enableSessionResumption,
certs,
json.alpn,
json.echServerKeys,
json.echForceQuery,
settings,
);
}
@@ -621,6 +625,8 @@ class TlsStreamSettings extends XrayCommonClass {
enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn,
echServerKeys: this.echServerKeys,
echForceQuery: this.echForceQuery,
settings: this.settings,
};
}
@@ -633,7 +639,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
keyFile = '',
certificate = '',
key = '',
ocspStapling = 3600,
oneTimeLoading = false,
usage = USAGE_OPTION.ENCIPHERMENT,
buildChain = false,
@@ -644,7 +649,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
this.keyFile = keyFile;
this.cert = Array.isArray(certificate) ? certificate.join('\n') : certificate;
this.key = Array.isArray(key) ? key.join('\n') : key;
this.ocspStapling = ocspStapling;
this.oneTimeLoading = oneTimeLoading;
this.usage = usage;
this.buildChain = buildChain
@@ -656,7 +660,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
true,
json.certificateFile,
json.keyFile, '', '',
json.ocspStapling,
json.oneTimeLoading,
json.usage,
json.buildChain,
@@ -666,7 +669,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
false, '', '',
json.certificate.join('\n'),
json.key.join('\n'),
json.ocspStapling,
json.oneTimeLoading,
json.usage,
json.buildChain,
@@ -679,7 +681,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
return {
certificateFile: this.certFile,
keyFile: this.keyFile,
ocspStapling: this.ocspStapling,
oneTimeLoading: this.oneTimeLoading,
usage: this.usage,
buildChain: this.buildChain,
@@ -688,7 +689,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
return {
certificate: this.cert.split('\n'),
key: this.key.split('\n'),
ocspStapling: this.ocspStapling,
oneTimeLoading: this.oneTimeLoading,
usage: this.usage,
buildChain: this.buildChain,
@@ -701,21 +701,25 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(
allowInsecure = false,
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
echConfigList = '',
) {
super();
this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint;
this.echConfigList = echConfigList;
}
static fromJson(json = {}) {
return new TlsStreamSettings.Settings(
json.allowInsecure,
json.fingerprint,
json.echConfigList,
);
}
toJson() {
return {
allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint,
echConfigList: this.echConfigList
};
}
};
@@ -725,13 +729,14 @@ class RealityStreamSettings extends XrayCommonClass {
constructor(
show = false,
xver = 0,
dest = 'yahoo.com:443',
serverNames = 'yahoo.com,www.yahoo.com',
dest = 'google.com:443',
serverNames = 'google.com,www.google.com',
privateKey = '',
minClient = '',
maxClient = '',
minClientVer = '',
maxClientVer = '',
maxTimediff = 0,
shortIds = RandomUtil.randomShortIds(),
mldsa65Seed = '',
settings = new RealityStreamSettings.Settings()
) {
super();
@@ -740,10 +745,11 @@ class RealityStreamSettings extends XrayCommonClass {
this.dest = dest;
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
this.privateKey = privateKey;
this.minClient = minClient;
this.maxClient = maxClient;
this.minClientVer = minClientVer;
this.maxClientVer = maxClientVer;
this.maxTimediff = maxTimediff;
this.shortIds = Array.isArray(shortIds) ? shortIds.join(",") : shortIds;
this.mldsa65Seed = mldsa65Seed;
this.settings = settings;
}
@@ -754,7 +760,8 @@ class RealityStreamSettings extends XrayCommonClass {
json.settings.publicKey,
json.settings.fingerprint,
json.settings.serverName,
json.settings.spiderX
json.settings.spiderX,
json.settings.mldsa65Verify,
);
}
return new RealityStreamSettings(
@@ -763,10 +770,11 @@ class RealityStreamSettings extends XrayCommonClass {
json.dest,
json.serverNames,
json.privateKey,
json.minClient,
json.maxClient,
json.minClientVer,
json.maxClientVer,
json.maxTimediff,
json.shortIds,
json.mldsa65Seed,
settings,
);
}
@@ -778,10 +786,11 @@ class RealityStreamSettings extends XrayCommonClass {
dest: this.dest,
serverNames: this.serverNames.split(","),
privateKey: this.privateKey,
minClient: this.minClient,
maxClient: this.maxClient,
minClientVer: this.minClientVer,
maxClientVer: this.maxClientVer,
maxTimediff: this.maxTimediff,
shortIds: this.shortIds.split(","),
mldsa65Seed: this.mldsa65Seed,
settings: this.settings,
};
}
@@ -792,13 +801,15 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
publicKey = '',
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
serverName = '',
spiderX = '/'
spiderX = '/',
mldsa65Verify = ''
) {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.spiderX = spiderX;
this.mldsa65Verify = mldsa65Verify;
}
static fromJson(json = {}) {
return new RealityStreamSettings.Settings(
@@ -806,6 +817,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
json.fingerprint,
json.serverName,
json.spiderX,
json.mldsa65Verify
);
}
toJson() {
@@ -814,6 +826,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
fingerprint: this.fingerprint,
serverName: this.serverName,
spiderX: this.spiderX,
mldsa65Verify: this.mldsa65Verify
};
}
};
@@ -1027,27 +1040,6 @@ class Sniffing extends XrayCommonClass {
}
}
class Allocate extends XrayCommonClass {
constructor(
strategy = "always",
refresh = 5,
concurrency = 3,
) {
super();
this.strategy = strategy;
this.refresh = refresh;
this.concurrency = concurrency;
}
static fromJson(json = {}) {
return new Allocate(
json.strategy,
json.refresh,
json.concurrency,
);
}
}
class Inbound extends XrayCommonClass {
constructor(
port = RandomUtil.randomInteger(10000, 60000),
@@ -1057,7 +1049,6 @@ class Inbound extends XrayCommonClass {
streamSettings = new StreamSettings(),
tag = '',
sniffing = new Sniffing(),
allocate = new Allocate(),
clientStats = '',
) {
super();
@@ -1068,7 +1059,6 @@ class Inbound extends XrayCommonClass {
this.stream = streamSettings;
this.tag = tag;
this.sniffing = sniffing;
this.allocate = allocate;
this.clientStats = clientStats;
}
getClientStats() {
@@ -1233,7 +1223,6 @@ class Inbound extends XrayCommonClass {
this.stream = new StreamSettings();
this.tag = '';
this.sniffing = new Sniffing();
this.allocate = new Allocate();
}
genVmessLink(address = '', port = this.port, forceTls, remark = '', clientId, security) {
@@ -1310,6 +1299,7 @@ class Inbound extends XrayCommonClass {
const security = forceTls == 'same' ? this.stream.security : forceTls;
const params = new Map();
params.set("type", this.stream.network);
params.set("encryption", this.settings.encryption);
switch (type) {
case "tcp":
const tcp = this.stream.tcp;
@@ -1366,6 +1356,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(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)) {
params.set("flow", flow);
}
@@ -1385,6 +1378,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.mldsa65Verify)) {
params.set("pqv", this.stream.reality.settings.mldsa65Verify);
}
if (type == 'tcp' && !ObjectUtil.isEmpty(flow)) {
params.set("flow", flow);
}
@@ -1462,6 +1458,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.settings.allowInsecure) {
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)) {
params.set("sni", this.stream.tls.sni);
}
@@ -1540,6 +1539,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.settings.allowInsecure) {
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)) {
params.set("sni", this.stream.tls.sni);
}
@@ -1559,6 +1561,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.mldsa65Verify)) {
params.set("pqv", this.stream.reality.settings.mldsa65Verify);
}
}
else {
@@ -1673,14 +1678,13 @@ class Inbound extends XrayCommonClass {
StreamSettings.fromJson(json.streamSettings),
json.tag,
Sniffing.fromJson(json.sniffing),
Allocate.fromJson(json.allocate),
json.clientStats
)
}
toJson() {
let streamSettings;
if (this.canEnableStream()) {
if (this.canEnableStream() || this.stream?.sockopt) {
streamSettings = this.stream.toJson();
}
return {
@@ -1691,7 +1695,6 @@ class Inbound extends XrayCommonClass {
streamSettings: streamSettings,
tag: this.tag,
sniffing: this.sniffing.toJson(),
allocate: this.allocate.toJson(),
clientStats: this.clientStats
};
}
@@ -1787,7 +1790,9 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0
reset = 0,
created_at = undefined,
updated_at = undefined
) {
super();
this.id = id;
@@ -1801,6 +1806,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
this.subId = subId;
this.comment = comment;
this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
}
static fromJson(json = {}) {
@@ -1816,6 +1823,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
);
}
get _expiryTime() {
@@ -1849,13 +1858,16 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
constructor(
protocol,
vlesses = [new Inbound.VLESSSettings.VLESS()],
decryption = 'none',
fallbacks = []
decryption = "none",
encryption = "",
fallbacks = [],
) {
super(protocol);
this.vlesses = vlesses;
this.decryption = decryption;
this.encryption = encryption;
this.fallbacks = fallbacks;
this.selectedAuth = "X25519, not Post-Quantum";
}
addFallback() {
@@ -1866,22 +1878,43 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
this.fallbacks.splice(index, 1);
}
// decryption should be set to static value
static fromJson(json = {}) {
return new Inbound.VLESSSettings(
const obj = new Inbound.VLESSSettings(
Protocols.VLESS,
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption || 'none',
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),);
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption,
json.encryption,
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || [])
);
obj.selectedAuth = json.selectedAuth || "X25519, not Post-Quantum";
return obj;
}
toJson() {
return {
const json = {
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
decryption: this.decryption,
fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
};
if (this.decryption) {
json.decryption = this.decryption;
}
if (this.encryption) {
json.encryption = this.encryption;
}
if (this.fallbacks && this.fallbacks.length > 0) {
json.fallbacks = Inbound.VLESSSettings.toJsonArray(this.fallbacks);
}
if (this.selectedAuth) {
json.selectedAuth = this.selectedAuth;
}
return json;
}
};
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
@@ -1896,7 +1929,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0
reset = 0,
created_at = undefined,
updated_at = undefined
) {
super();
this.id = id;
@@ -1910,6 +1945,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
this.subId = subId;
this.comment = comment;
this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
}
static fromJson(json = {}) {
@@ -1925,6 +1962,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
);
}
@@ -2035,7 +2074,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0
reset = 0,
created_at = undefined,
updated_at = undefined
) {
super();
this.password = password;
@@ -2048,6 +2089,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
this.subId = subId;
this.comment = comment;
this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
}
toJson() {
@@ -2062,6 +2105,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
subId: this.subId,
comment: this.comment,
reset: this.reset,
created_at: this.created_at,
updated_at: this.updated_at,
};
}
@@ -2077,6 +2122,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
);
}
@@ -2196,7 +2243,9 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0
reset = 0,
created_at = undefined,
updated_at = undefined
) {
super();
this.method = method;
@@ -2210,6 +2259,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
this.subId = subId;
this.comment = comment;
this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
}
toJson() {
@@ -2225,6 +2276,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
subId: this.subId,
comment: this.comment,
reset: this.reset,
created_at: this.created_at,
updated_at: this.updated_at,
};
}
@@ -2241,6 +2294,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
);
}
@@ -2276,12 +2331,14 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
protocol,
address,
port,
portMap = [],
network = 'tcp,udp',
followRedirect = false
) {
super(protocol);
this.address = address;
this.port = port;
this.portMap = portMap;
this.network = network;
this.followRedirect = followRedirect;
}
@@ -2291,6 +2348,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
Protocols.DOKODEMO,
json.address,
json.port,
XrayCommonClass.toHeaders(json.portMap),
json.network,
json.followRedirect,
);
@@ -2300,6 +2358,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
return {
address: this.address,
port: this.port,
portMap: XrayCommonClass.toV2Headers(this.portMap, false),
network: this.network,
followRedirect: this.followRedirect,
};

View File

@@ -354,13 +354,15 @@ class TlsStreamSettings extends CommonClass {
serverName = '',
alpn = [],
fingerprint = '',
allowInsecure = false
allowInsecure = false,
echConfigList = '',
) {
super();
this.serverName = serverName;
this.alpn = alpn;
this.fingerprint = fingerprint;
this.allowInsecure = allowInsecure;
this.echConfigList = echConfigList;
}
static fromJson(json = {}) {
@@ -369,6 +371,7 @@ class TlsStreamSettings extends CommonClass {
json.alpn,
json.fingerprint,
json.allowInsecure,
json.echConfigList,
);
}
@@ -378,6 +381,7 @@ class TlsStreamSettings extends CommonClass {
alpn: this.alpn,
fingerprint: this.fingerprint,
allowInsecure: this.allowInsecure,
echConfigList: this.echConfigList
};
}
}
@@ -388,7 +392,8 @@ class RealityStreamSettings extends CommonClass {
fingerprint = '',
serverName = '',
shortId = '',
spiderX = '/'
spiderX = '',
mldsa65Verify = ''
) {
super();
this.publicKey = publicKey;
@@ -396,6 +401,7 @@ class RealityStreamSettings extends CommonClass {
this.serverName = serverName;
this.shortId = shortId
this.spiderX = spiderX;
this.mldsa65Verify = mldsa65Verify;
}
static fromJson(json = {}) {
return new RealityStreamSettings(
@@ -404,6 +410,7 @@ class RealityStreamSettings extends CommonClass {
json.serverName,
json.shortId,
json.spiderX,
json.mldsa65Verify
);
}
toJson() {
@@ -413,6 +420,7 @@ class RealityStreamSettings extends CommonClass {
serverName: this.serverName,
shortId: this.shortId,
spiderX: this.spiderX,
mldsa65Verify: this.mldsa65Verify
};
}
};
@@ -778,7 +786,8 @@ class Outbound extends CommonClass {
let alpn = url.searchParams.get('alpn');
let allowInsecure = url.searchParams.get('allowInsecure');
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') {
@@ -787,7 +796,8 @@ class Outbound extends CommonClass {
let sni = url.searchParams.get('sni') ?? '';
let sid = url.searchParams.get('sid') ?? '';
let spx = url.searchParams.get('spx') ?? '';
stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx);
let pqv = url.searchParams.get('pqv') ?? '';
stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx, pqv);
}
const regex = /([^@]+):\/\/([^@]+)@(.+):(\d+)(.*)$/;
@@ -803,7 +813,7 @@ class Outbound extends CommonClass {
var settings;
switch (protocol) {
case Protocols.VLESS:
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '');
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '', url.searchParams.get('encryption') ?? 'none');
break;
case Protocols.Trojan:
settings = new Outbound.TrojanSettings(address, port, userData);
@@ -909,12 +919,14 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
constructor(
packets = '1-3',
length = '',
interval = ''
interval = '',
maxSplit = ''
) {
super();
this.packets = packets;
this.length = length;
this.interval = interval;
this.maxSplit = maxSplit;
}
static fromJson(json = {}) {
@@ -922,6 +934,7 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
json.packets,
json.length,
json.interval,
json.maxSplit
);
}
};
@@ -930,12 +943,14 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
constructor(
type = 'rand',
packet = '10-20',
delay = '10-16'
delay = '10-16',
applyTo = 'ip'
) {
super();
this.type = type;
this.packet = packet;
this.delay = delay;
this.applyTo = applyTo;
}
static fromJson(json = {}) {
@@ -943,6 +958,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
json.type,
json.packet,
json.delay,
json.applyTo
);
}
@@ -951,6 +967,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
type: this.type,
packet: this.packet,
delay: this.delay,
applyTo: this.applyTo
};
}
};
@@ -978,7 +995,7 @@ Outbound.DNSSettings = class extends CommonClass {
network = 'udp',
address = '',
port = 53,
nonIPQuery = 'drop',
nonIPQuery = 'reject',
blockTypes = []
) {
super();
@@ -1029,13 +1046,13 @@ Outbound.VmessSettings = class extends CommonClass {
}
};
Outbound.VLESSSettings = class extends CommonClass {
constructor(address, port, id, flow, encryption = 'none') {
constructor(address, port, id, flow, encryption) {
super();
this.address = address;
this.port = port;
this.id = id;
this.flow = flow;
this.encryption = encryption
this.encryption = encryption;
}
static fromJson(json = {}) {
@@ -1054,7 +1071,7 @@ Outbound.VLESSSettings = class extends CommonClass {
vnext: [{
address: this.address,
port: this.port,
users: [{ id: this.id, flow: this.flow, encryption: 'none', }],
users: [{ id: this.id, flow: this.flow, encryption: this.encryption }],
}],
};
}

View File

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

View File

@@ -134,7 +134,7 @@ class DateUtil {
}
static formatMillis(millis) {
return moment(millis).format('YYYY-M-D H:m:s');
return moment(millis).format('YYYY-M-D HH:mm:ss');
}
static firstDayOfMonth() {

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

@@ -47,6 +47,8 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
{"POST", "/onlines", a.inboundController.onlines},
{"POST", "/lastOnline", a.inboundController.lastOnline},
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
}
for _, route := range inboundRoutes {

View File

@@ -108,8 +108,8 @@ func (a *InboundController) addInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, err)
if err == nil && needRestart {
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -126,8 +126,8 @@ func (a *InboundController) delInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, err)
if err == nil && needRestart {
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -152,8 +152,8 @@ func (a *InboundController) updateInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, err)
if err == nil && needRestart {
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -339,3 +339,33 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
func (a *InboundController) onlines(c *gin.Context) {
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
}
func (a *InboundController) lastOnline(c *gin.Context) {
data, err := a.inboundService.GetClientsLastOnline()
jsonObj(c, data, err)
}
func (a *InboundController) updateClientTraffic(c *gin.Context) {
email := c.Param("email")
// Define the request structure for traffic update
type TrafficUpdateRequest struct {
Upload int64 `json:"upload"`
Download int64 `json:"download"`
}
var request TrafficUpdateRequest
err := c.ShouldBindJSON(&request)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
return
}
err = a.inboundService.UpdateClientTrafficByEmail(email, request.Upload, request.Download)
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
}

View File

@@ -17,7 +17,8 @@ var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
type ServerController struct {
BaseController
serverService service.ServerService
serverService service.ServerService
settingService service.SettingService
lastStatus *service.Status
lastGetStatusTime time.Time
@@ -44,12 +45,17 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray)
g.POST("/updateGeofile", a.updateGeofile)
g.POST("/updateGeofile/:fileName", a.updateGeofile)
g.POST("/logs/:count", a.getLogs)
g.POST("/xraylogs/:count", a.getXrayLogs)
g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
g.POST("/getNewmldsa65", a.getNewmldsa65)
g.POST("/getNewEchCert", a.getNewEchCert)
g.POST("/getNewVlessEnc", a.getNewVlessEnc)
}
func (a *ServerController) refreshStatus() {
@@ -132,6 +138,50 @@ func (a *ServerController) getLogs(c *gin.Context) {
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) {
configJson, err := a.serverService.GetConfigJson()
if err != nil {
@@ -198,3 +248,31 @@ func (a *ServerController) getNewX25519Cert(c *gin.Context) {
}
jsonObj(c, cert, nil)
}
func (a *ServerController) getNewmldsa65(c *gin.Context) {
cert, err := a.serverService.GetNewmldsa65()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewmldsa65Error"), err)
return
}
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)
}
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
out, err := a.serverService.GetNewVlessEnc()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewVlessEncError"), err)
return
}
jsonObj(c, out, nil)
}

View File

@@ -2,10 +2,10 @@ package entity
import (
"crypto/tls"
"math"
"net"
"strings"
"time"
"math"
"x-ui/util/common"
)
@@ -39,8 +39,8 @@ type AllSetting struct {
TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubTitle string `json:"subTitle" form:"subTitle"`
SubListen string `json:"subListen" form:"subListen"`

View File

@@ -33,12 +33,17 @@
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
</template>
<template slot="online" slot-scope="text, client, index">
<template v-if="client.enable && isClientOnline(client.email)">
<a-tag color="green">{{ i18n "online" }}</a-tag>
</template>
<template v-else>
<a-tag>{{ i18n "offline" }}</a-tag>
</template>
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content" >
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
</template>
<template v-if="client.enable && isClientOnline(client.email)">
<a-tag color="green">{{ i18n "online" }}</a-tag>
</template>
<template v-else>
<a-tag>{{ i18n "offline" }}</a-tag>
</template>
</a-popover>
</template>
<template slot="client" slot-scope="text, client">
<a-space direction="horizontal" :size="2">
@@ -98,6 +103,10 @@
</table>
</a-popover>
</template>
<template slot="allTime" slot-scope="text, client">
<a-tag>[[ SizeFormatter.sizeFormat(getAllTimeClient(record, client.email)) ]]</a-tag>
</template>
<template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client.expiryTime !=0 && client.reset >0">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
@@ -278,4 +287,30 @@
</a-badge>
</a-popover>
</template>
<template slot="createdAt" slot-scope="text, client, index">
<template v-if="client.created_at">
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(client.created_at) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(client.created_at)) ]]
</template>
</template>
<template v-else>
-
</template>
</template>
<template slot="updatedAt" slot-scope="text, client, index">
<template v-if="client.updated_at">
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(client.updated_at) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(client.updated_at)) ]]
</template>
</template>
<template v-else>
-
</template>
</template>
{{end}}

View File

@@ -1,15 +0,0 @@
{{define "form/allocate"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='Strategy'>
<a-select v-model="inbound.allocate.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['always','random']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Refresh'>
<a-input-number v-model.number="inbound.allocate.refresh" min="0"></a-input-number>
</a-form-item>
<a-form-item label='Concurrency'>
<a-input-number v-model.number="inbound.allocate.concurrency" min="0"></a-input-number>
</a-form-item>
</a-form>
{{end}}

View File

@@ -44,7 +44,7 @@
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
</a-select>
</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">
<a-tooltip>
<template slot="title">

View File

@@ -121,13 +121,4 @@
</a-collapse-panel>
</a-collapse>
<!-- allocate -->
<!-- Temporarily disabled until we accepts range for port allocation
<a-collapse>
<a-collapse-panel header='Allocate'>
{{template "form/allocate"}}
</a-collapse-panel>
</a-collapse>
-->
{{end}}

View File

@@ -42,6 +42,9 @@
<a-form-item label='Interval'>
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
</a-form-item>
<a-form-item label='Max Split'>
<a-input v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
</a-form-item>
</template>
<!-- Switch for Noises -->
@@ -75,6 +78,11 @@
<a-form-item label='Delay'>
<a-input v-model.trim="noise.delay"></a-input>
</a-form-item>
<a-form-item label='Apply To'>
<a-select v-model="noise.applyTo" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</a-form>
</template>
</template>
@@ -97,7 +105,7 @@
</a-form-item>
<a-form-item label='non-IP queries'>
<a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['drop','skip']" :value="s">[[ s ]]</a-select-option>
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types' >
@@ -218,6 +226,11 @@
</template>
<!-- vless settings -->
<template v-if="outbound.protocol === Protocols.VLESS">
<a-form-item label='encryption'>
<a-input v-model.trim="outbound.settings.encryption"></a-input>
</a-form-item>
</template>
<template v-if="outbound.canEnableTlsFlow()">
<a-form-item label='Flow'>
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
@@ -444,13 +457,16 @@
</a-select>
</a-form-item>
<a-form-item label="Short ID">
<a-input v-model.trim="outbound.stream.reality.shortId" :style="{ width: '250px' }"></a-input>
<a-input v-model.trim="outbound.stream.reality.shortId"></a-input>
</a-form-item>
<a-form-item label="SpiderX">
<a-input v-model.trim="outbound.stream.reality.spiderX" :style="{ width: '250px' }"></a-input>
<a-input v-model.trim="outbound.stream.reality.spiderX"></a-input>
</a-form-item>
<a-form-item label="Public Key">
<a-input v-model.trim="outbound.stream.reality.publicKey"></a-input>
<a-textarea v-model.trim="outbound.stream.reality.publicKey"></a-textarea>
</a-form-item>
<a-form-item label="mldsa65 Verify">
<a-textarea v-model.trim="outbound.stream.reality.mldsa65Verify"></a-textarea>
</a-form-item>
</template>
</template>

View File

@@ -6,6 +6,19 @@
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input-number v-model.number="inbound.settings.port"></a-input-number>
</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-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
<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-form-item>
</a-form>
<!-- sockopt -->
<template>
{{template "form/streamSockopt"}}
</template>
{{end}}

View File

@@ -18,7 +18,29 @@
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp">
<template v-if="!inbound.stream.isTLS || !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Authentication">
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="X25519, not Post-Quantum">X25519 (not Post-Quantum)</a-select-option>
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 (Post-Quantum)</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="decryption">
<a-input v-model.trim="inbound.settings.decryption"></a-input>
</a-form-item>
<a-form-item label="encryption">
<a-input v-model="inbound.settings.encryption" disabled></a-input>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New keys</a-button>
<a-button danger @click="clearKeys">Clear</a-button>
</a-space>
</a-form-item>
</a-form>
</template>
<template v-if="inbound.isTcp && !inbound.settings.encryption">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>

View File

@@ -21,14 +21,12 @@
<a-form-item label='Max Time Diff (ms)'>
<a-input-number v-model.number="inbound.stream.reality.maxTimediff" :min="0"></a-input-number>
</a-form-item>
<!-- we also have this but i think it's not necessary
<a-form-item label='Min Client'>
<a-input v-model.trim="inbound.stream.reality.minClient"></a-input>
<a-form-item label='Min Client Ver'>
<a-input v-model.trim="inbound.stream.reality.minClientVer"></a-input>
</a-form-item>
<a-form-item label='Max Client'>
<a-input v-model.trim="inbound.stream.reality.maxClient"></a-input>
<a-form-item label='Max Client Ver'>
<a-input v-model.trim="inbound.stream.reality.maxClientVer"></a-input>
</a-form-item>
-->
<a-form-item>
<template slot="label">
<a-tooltip>
@@ -38,19 +36,28 @@
type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
<a-textarea v-model.trim="inbound.stream.reality.shortIds"></a-textarea>
</a-form-item>
<a-form-item label='SpiderX'>
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model="inbound.stream.reality.settings.publicKey"></a-input>
<a-textarea v-model="inbound.stream.reality.settings.publicKey"></a-textarea>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="password" v-model="inbound.stream.reality.privateKey"></a-input>
<a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
</a-form-item>
<a-form-item label="mldsa65 Seed">
<a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea>
</a-form-item>
<a-form-item label="mldsa65 Verify">
<a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
</a-form-item>
</template>
{{end}}

View File

@@ -5,13 +5,13 @@
<a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="inbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
<a-radio-button value="tls">TLS</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality() && !inbound.settings.encryption" value="reality">Reality</a-radio-button>
<a-radio-button v-if="!inbound.settings.encryption" value="tls">TLS</a-radio-button>
</a-radio-group>
</a-form-item>
<!-- tls settings -->
<template v-if="inbound.stream.isTls">
<template v-if="inbound.stream.isTls && !inbound.settings.encryption">
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
</a-form-item>
@@ -85,15 +85,12 @@
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
<a-input v-model="cert.cert"></a-input>
<a-textarea v-model="cert.cert"></a-textarea>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
<a-input type="password" v-model="cert.key"></a-input>
<a-textarea v-model="cert.key"></a-textarea>
</a-form-item>
</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-switch v-model="cert.oneTimeLoading"></a-switch>
</a-form-item>
@@ -106,10 +103,25 @@
<a-switch v-model="cert.buildChain"></a-switch>
</a-form-item>
</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>
<!-- reality settings -->
<template v-if="inbound.stream.isReality">
<template v-if="inbound.stream.isReality && !inbound.settings.encryption">
{{template "form/realitySettings"}}
</template>
</a-form>

View File

@@ -167,28 +167,35 @@
<a-col>
<a-card size="small" :style="{ padding: '16px' }" hoverable>
<a-row>
<a-col :sm="12" :md="6">
<a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
<template #prefix>
<a-icon type="swap"></a-icon>
</template>
</a-custom-statistic>
</a-col>
<a-col :sm="12" :md="6">
<a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' :value="SizeFormatter.sizeFormat(total.up + total.down)" :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix>
<a-icon type="pie-chart"></a-icon>
</template>
</a-custom-statistic>
</a-col>
<a-col :sm="12" :md="6">
<a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}' :value="SizeFormatter.sizeFormat(total.allTime)" :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix>
<a-icon type="history"></a-icon>
</template>
</a-custom-statistic>
</a-col>
<a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix>
<a-icon type="bars"></a-icon>
</template>
</a-custom-statistic>
</a-col>
<a-col :sm="12" :md="6">
<a-col :sm="12" :md="4">
<a-custom-statistic title='{{ i18n "clients" }}' value=" " :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix>
<a-space direction="horizontal">
@@ -405,9 +412,9 @@
<span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]]
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</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>
</div>
</template>
@@ -419,9 +426,9 @@
<span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]]
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</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>
</div>
</template>
@@ -433,9 +440,9 @@
<span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]]
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</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>
</div>
</template>
@@ -447,9 +454,9 @@
<span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]]
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</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>
</div>
</template>
@@ -484,6 +491,9 @@
</a-tag>
</a-popover>
</template>
<template slot="allTimeInbound" slot-scope="text, dbInbound">
<a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0) ]]</a-tag>
</template>
<template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
</template>
@@ -534,9 +544,9 @@
<span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]]
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</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>
</div>
</template>
@@ -548,9 +558,9 @@
<span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]]
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</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>
</div>
</template>
@@ -562,9 +572,9 @@
<span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]]
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</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>
</div>
</template>
@@ -576,9 +586,9 @@
<span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template #title>
[[ getClientWithComment(clientEmail, dbInbound.id).comment ]]
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]]
</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>
</div>
</template>
@@ -696,7 +706,7 @@
}, {
title: '{{ i18n "pages.inbounds.enable" }}',
align: 'center',
width: 30,
width: 35,
scopedSlots: { customRender: 'enable' },
}, {
title: '{{ i18n "pages.inbounds.remark" }}',
@@ -721,8 +731,13 @@
}, {
title: '{{ i18n "pages.inbounds.traffic" }}',
align: 'center',
width: 60,
width: 90,
scopedSlots: { customRender: 'traffic' },
}, {
title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
align: 'center',
width: 70,
scopedSlots: { customRender: 'allTimeInbound' },
}, {
title: '{{ i18n "pages.inbounds.expireDate" }}',
align: 'center',
@@ -755,10 +770,11 @@
const innerColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 35, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
];
@@ -791,6 +807,7 @@
defaultKey: '',
clientCount: [],
onlineClients: [],
lastOnlineMap: {},
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
@@ -811,17 +828,6 @@
loading(spinning = true) {
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() {
this.refreshing = true;
const msg = await HttpUtil.post('/panel/inbound/list');
@@ -830,6 +836,7 @@
return;
}
await this.getLastOnlineMap();
await this.getOnlineUsers();
this.setInbounds(msg.obj);
@@ -844,6 +851,11 @@
}
this.onlineClients = msg.obj != null ? msg.obj : [];
},
async getLastOnlineMap() {
const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
if (!msg.success || !msg.obj) return;
this.lastOnlineMap = msg.obj || {}
},
async getDefaultSettings() {
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (!msg.success) {
@@ -893,7 +905,7 @@
}
},
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;
clientStats = dbInbound.clientStats
now = new Date().getTime()
@@ -901,6 +913,9 @@
clientCount = clients.length;
if (dbInbound.enable) {
clients.forEach(client => {
if (client.comment) {
comments.set(client.email, client.comment)
}
if (client.enable) {
active.push(client.email);
if (this.isClientOnline(client.email)) online.push(client.email);
@@ -929,6 +944,7 @@
depleted: depleted,
expiring: expiring,
online: online,
comments: comments,
};
},
@@ -1082,7 +1098,6 @@
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.sniffing.toString(),
allocate: baseInbound.allocate.toString(),
};
await this.submit('/panel/inbound/add', data, inModal);
},
@@ -1126,9 +1141,12 @@
protocol: inbound.protocol,
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.allocate = inbound.allocate.toString();
await this.submit('/panel/inbound/add', data, inModal);
},
@@ -1146,9 +1164,12 @@
protocol: inbound.protocol,
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.allocate = inbound.allocate.toString();
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
},
@@ -1416,6 +1437,12 @@
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientStats ? clientStats.up + clientStats.down : 0;
},
getAllTimeClient(dbInbound, email) {
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
if (!clientStats) return 0;
return clientStats.allTime || (clientStats.up + clientStats.down);
},
getRemStats(dbInbound, email) {
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
@@ -1473,6 +1500,17 @@
isClientOnline(email) {
return this.onlineClients.includes(email);
},
getLastOnline(email) {
return this.lastOnlineMap[email] || null
},
formatLastOnline(email) {
const ts = this.getLastOnline(email)
if (!ts) return '-'
if (this.datepicker === 'gregorian') {
return DateUtil.formatMillis(ts)
}
return DateUtil.convertToJalalian(moment(ts))
},
isRemovable(dbInboundId) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
},
@@ -1605,11 +1643,12 @@
},
computed: {
total() {
let down = 0, up = 0;
let down = 0, up = 0, allTime = 0;
let clients = 0, deactive = [], depleted = [], expiring = [];
this.dbInbounds.forEach(dbInbound => {
down += dbInbound.down;
up += dbInbound.up;
allTime += (dbInbound.allTime || (dbInbound.up + dbInbound.down));
if (this.clientCount[dbInbound.id]) {
clients += this.clientCount[dbInbound.id].clients;
deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
@@ -1620,6 +1659,7 @@
return {
down: down,
up: up,
allTime: allTime,
clients: clients,
deactive: deactive,
depleted: depleted,

View File

@@ -167,7 +167,7 @@
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
</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-row>
</span>
@@ -179,6 +179,10 @@
</template>
</template>
<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-icon type="poweroff"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
@@ -376,6 +380,9 @@
<a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/>
</a-list-item>
</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>
</a-modal>
@@ -422,6 +429,48 @@
</a-form>
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div>
</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"
v-model="backupModal.visible"
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 = {
visible: false,
show() {
@@ -629,10 +732,12 @@
status: new Status(),
versionModal,
logModal,
xraylogModal,
backupModal,
loadingTip: '{{ i18n "loading"}}',
showAlert: false,
showIp: false
showIp: false,
ipLimitEnable: false,
},
methods: {
loading(spinning, tip = '{{ i18n "loading"}}') {
@@ -681,16 +786,22 @@
});
},
updateGeofile(fileName) {
const isSingleFile = !!fileName;
this.$confirm({
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"}}',
class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
versionModal.hide();
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);
},
});
@@ -721,6 +832,16 @@
await PromiseUtil.sleep(500);
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() {
this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson');
@@ -773,6 +894,12 @@
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (msg.success) {
this.ipLimitEnable = msg.obj.ipLimitEnable;
}
while (true) {
try {
await this.getStatus();

View File

@@ -466,71 +466,83 @@
</g>
</svg>
</div>
<a-row type="flex" justify="center" align="middle" :style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }">
<a-row type="flex" justify="center" align="middle"
:style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }">
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" :style="{ margin: '3rem 0' }">
<div class="setting-section">
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}' placement="bottomRight" trigger="click">
<template slot="content">
<a-space direction="vertical" :size="10">
<a-theme-switch-login></a-theme-switch-login>
<span>{{ i18n "pages.settings.language" }}</span>
<a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang" @change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</a-space>
</template>
<a-button shape="circle" icon="setting"></a-button>
</a-popover>
</div>
<a-row type="flex" justify="center">
<a-col :style="{ width: '100%' }">
<h2 class="title headline zoom">
<span class="words-wrapper">
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
<b>{{ i18n "pages.login.title" }}</b>
</span>
</h2>
</a-col>
</a-row>
<a-row type="flex" justify="center">
<a-col span="24">
<a-form>
<a-space direction="vertical" size="middle">
<a-form-item>
<a-input autocomplete="username" name="username" v-model.trim="user.username"
placeholder='{{ i18n "username" }}' @keydown.enter.native="login" autofocus>
<a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon>
</a-input>
</a-form-item>
<a-form-item>
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
</a-input-password>
</a-form-item>
<a-form-item v-if="twoFactorEnable">
<a-input autocomplete="totp" name="twoFactorCode" v-model.trim="user.twoFactorCode"
placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login">
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
</a-input>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<div :style="{ height: '50px', marginTop: '1rem', ...loading ? { width: '52px' } : { display: 'inline-block' } }" class="wave-btn-bg wave-btn-bg-cl">
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login"
:icon="loading ? 'poweroff' : undefined">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
</div>
</a-row>
</a-form-item>
</a-space>
</a-form>
</a-col>
</a-row>
<template v-if="!loadingStates.fetched">
<div :style="{ textAlign: 'center' }">
<a-spin size="large" />
</div>
</template>
<template v-else>
<div class="setting-section">
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
placement="bottomRight" trigger="click">
<template slot="content">
<a-space direction="vertical" :size="10">
<a-theme-switch-login></a-theme-switch-login>
<span>{{ i18n "pages.settings.language" }}</span>
<a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang"
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</a-space>
</template>
<a-button shape="circle" icon="setting"></a-button>
</a-popover>
</div>
<a-row type="flex" justify="center">
<a-col :style="{ width: '100%' }">
<h2 class="title headline zoom">
<span class="words-wrapper">
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
<b>{{ i18n "pages.login.title" }}</b>
</span>
</h2>
</a-col>
</a-row>
<a-row type="flex" justify="center">
<a-col span="24">
<a-form @submit.prevent="login">
<a-space direction="vertical" size="middle">
<a-form-item>
<a-input autocomplete="username" name="username" v-model.trim="user.username"
placeholder='{{ i18n "username" }}' autofocus required>
<a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon>
</a-input>
</a-form-item>
<a-form-item>
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
placeholder='{{ i18n "password" }}' required>
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
</a-input-password>
</a-form-item>
<a-form-item v-if="twoFactorEnable">
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
placeholder='{{ i18n "twoFactorCode" }}' required>
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
</a-input>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<div
:style="{ height: '50px', marginTop: '1rem', ...loadingStates.spinning ? { width: '52px' } : { display: 'inline-block' } }"
class="wave-btn-bg wave-btn-bg-cl">
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
</a-button>
</div>
</a-row>
</a-form-item>
</a-space>
</a-form>
</a-col>
</a-row>
</template>
</a-col>
</a-row>
</a-layout-content>
@@ -544,7 +556,10 @@
el: '#app',
data: {
themeSwitcher,
loading: false,
loadingStates: {
fetched: false,
spinning: false
},
user: {
username: "",
password: "",
@@ -559,19 +574,23 @@
},
methods: {
async login() {
this.loading = true;
this.loadingStates.spinning = true;
const msg = await HttpUtil.post('/login', this.user);
this.loading = false;
if (msg.success) {
location.href = basePath + 'panel/';
}
this.loadingStates.spinning = false;
},
async getTwoFactorEnable() {
this.loading = true;
const msg = await HttpUtil.post('/getTwoFactorEnable');
this.loading = false;
if (msg.success) {
this.twoFactorEnable = msg.obj;
this.loadingStates.fetched = true;
return msg.obj;
}
},

View File

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

View File

@@ -101,6 +101,12 @@
{{ i18n "security" }}
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
<br />
<td>Authentication</td>
<a-tag :color="inbound.settings.selectedAuth ? 'green' : 'red'">[[ inbound.settings.selectedAuth ? inbound.settings.selectedAuth : '' ]]</a-tag>
<br />
{{ i18n "encryption" }}
<a-tag :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
<br />
<template v-if="inbound.stream.security != 'none'">
{{ i18n "domainName" }}
<a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
@@ -185,6 +191,44 @@
<a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.createdAt" }}</td>
<td>
<template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
<template v-if="app.datepicker === 'gregorian'">
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.created_at) ]]</a-tag>
</template>
<template v-else>
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.created_at)) ]]</a-tag>
</template>
</template>
<template v-else>
<a-tag>-</a-tag>
</template>
</td>
</tr>
<tr>
<td>{{ i18n "pages.inbounds.updatedAt" }}</td>
<td>
<template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
<template v-if="app.datepicker === 'gregorian'">
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.updated_at) ]]</a-tag>
</template>
<template v-else>
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.updated_at)) ]]</a-tag>
</template>
</template>
<template v-else>
<a-tag>-</a-tag>
</template>
</td>
</tr>
<tr>
<td>{{ i18n "lastOnline" }}</td>
<td>
<a-tag>[[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]</a-tag>
</td>
</tr>
<tr v-if="infoModal.clientSettings.comment">
<td>{{ i18n "comment" }}</td>
<td>
@@ -199,7 +243,7 @@
<a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
</td>
</tr>
<tr v-if="app.ipLimitEnable">
<tr v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
<td>
<a-tag>[[ infoModal.clientIps ]]</a-tag>

View File

@@ -1,9 +1,7 @@
{{define "modals/inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title"
:dialog-style="{ top: '20px' }" @ok="inModal.ok"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.currentTheme"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
@ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}}
</a-modal>
<script>
@@ -20,7 +18,7 @@
ok() {
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
},
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => {}, isEdit = false }) {
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => { }, isEdit = false }) {
this.title = title;
this.okText = okText;
if (inbound) {
@@ -41,7 +39,7 @@
inModal.visible = false;
inModal.loading(false);
},
loading(loading=true) {
loading(loading = true) {
inModal.confirmLoading = loading;
},
};
@@ -105,9 +103,9 @@
},
SSMethodChange() {
this.inModal.inbound.settings.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
if (this.inModal.inbound.isSSMultiUser) {
if (this.inModal.inbound.settings.shadowsockses.length ==0){
if (this.inModal.inbound.settings.shadowsockses.length == 0) {
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
}
if (!this.inModal.inbound.isSS2022) {
@@ -123,7 +121,7 @@
client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
})
} else {
if (this.inModal.inbound.settings.shadowsockses.length > 0){
if (this.inModal.inbound.settings.shadowsockses.length > 0) {
this.inModal.inbound.settings.shadowsockses = [];
}
}
@@ -141,9 +139,55 @@
}
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
},
async getNewmldsa65() {
inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewmldsa65');
inModal.loading(false);
if (!msg.success) {
return;
}
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
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;
},
async getNewVlessEnc() {
inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewVlessEnc');
inModal.loading(false);
if (!msg.success) {
return;
}
const auths = msg.obj.auths || [];
const selected = inModal.inbound.settings.selectedAuth;
const block = auths.find(a => a.label === selected);
if (!block) {
console.error("No auth block for", selected);
return;
}
inModal.inbound.settings.decryption = block.decryption;
inModal.inbound.settings.encryption = block.encryption;
},
clearKeys() {
this.inbound.settings.decryption = 'none';
this.inbound.settings.encryption = '';
}
},
});
</script>
{{end}}
{{end}}

View File

@@ -21,7 +21,7 @@
</a-space>
</template>
<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">
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
<tr-qr-bg class="qr-bg-sub">

View File

@@ -1,11 +1,6 @@
{{define "modals/ruleModal"}}
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='Domain Matcher'>
<a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
@@ -123,7 +118,6 @@
confirm: null,
rule: {
type: "field",
domainMatcher: "",
domain: "",
ip: "",
port: "",
@@ -157,7 +151,6 @@
this.confirm = confirm;
this.visible = true;
if (isEdit) {
this.rule.domainMatcher = rule.domainMatcher;
this.rule.domain = rule.domain ? rule.domain.join(',') : [];
this.rule.ip = rule.ip ? rule.ip.join(',') : [];
this.rule.port = rule.port;
@@ -172,7 +165,6 @@
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
} else {
this.rule = {
domainMatcher: "",
domain: "",
ip: "",
port: "",
@@ -214,7 +206,6 @@
rule = {};
newRule = {};
rule.type = "field";
rule.domainMatcher = value.domainMatcher;
rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
rule.port = value.port;

View File

@@ -207,7 +207,7 @@
settings: {
domainStrategy: "AsIs",
noises: [
{ type: "rand", packet: "10-20", delay: "10-16" },
{ type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" },
],
},
},
@@ -397,7 +397,7 @@
}
},
addNoise() {
const newNoise = { type: "rand", packet: "10-20", delay: "10-16" };
const newNoise = { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" };
this.noisesArray = [...this.noisesArray, newNoise];
},
removeNoise(index) {
@@ -420,6 +420,11 @@
updatedNoises[index] = { ...updatedNoises[index], delay: value };
this.noisesArray = updatedNoises;
},
updateNoiseApplyTo(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], applyTo: value };
this.noisesArray = updatedNoises;
},
},
computed: {
fragment: {

View File

@@ -90,6 +90,18 @@
placeholder="10-20"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>ApplyTo</template>
<template #control>
<a-select :value="noise.applyTo" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"
@change="(value) => updateNoiseApplyTo(index, value)">
<a-select-option :value="p" :label="p" v-for="p in ['ip', 'ipv4', 'ipv6']" :key="p">
<span>[[ p ]]</span>
</a-select-option>
</a-select>
</template>
</a-setting-list-item>
<a-space direction="horizontal" :style="{ padding: '10px 20px' }">
<a-button v-if="noisesArray.length > 1" type="danger"
@click="removeNoise(index)">Remove</a-button>

View File

@@ -11,7 +11,6 @@ import (
"sort"
"time"
"slices"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
@@ -58,21 +57,21 @@ func (j *CheckClientIpJob) Run() {
func (j *CheckClientIpJob) clearAccessLog() {
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
j.checkError(err)
defer logAccessP.Close()
accessLogPath, err := xray.GetAccessLogPath()
j.checkError(err)
file, err := os.Open(accessLogPath)
j.checkError(err)
defer file.Close()
_, err = io.Copy(logAccessP, file)
j.checkError(err)
logAccessP.Close()
file.Close()
err = os.Truncate(accessLogPath, 0)
j.checkError(err)
j.lastClear = time.Now().Unix()
}
@@ -193,10 +192,6 @@ func (j *CheckClientIpJob) checkError(e error) {
}
}
func (j *CheckClientIpJob) contains(s []string, str string) bool {
return slices.Contains(s, str)
}
func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
db := database.GetDB()
InboundClientIps := &model.InboundClientIps{}

View File

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

View File

@@ -175,17 +175,42 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
return inbound, false, err
}
// Ensure created_at and updated_at on clients in settings
if len(clients) > 0 {
var settings map[string]any
if err2 := json.Unmarshal([]byte(inbound.Settings), &settings); err2 == nil && settings != nil {
now := time.Now().Unix() * 1000
updatedClients := make([]model.Client, 0, len(clients))
for _, c := range clients {
if c.CreatedAt == 0 {
c.CreatedAt = now
}
c.UpdatedAt = now
updatedClients = append(updatedClients, c)
}
settings["clients"] = updatedClients
if bs, err3 := json.MarshalIndent(settings, "", " "); err3 == nil {
inbound.Settings = string(bs)
} else {
logger.Debug("Unable to marshal inbound settings with timestamps:", err3)
}
} else if err2 != nil {
logger.Debug("Unable to parse inbound settings for timestamps:", err2)
}
}
// Secure client ID
for _, client := range clients {
if inbound.Protocol == "trojan" {
switch inbound.Protocol {
case "trojan":
if client.Password == "" {
return inbound, false, common.NewError("empty client ID")
}
} else if inbound.Protocol == "shadowsocks" {
case "shadowsocks":
if client.Email == "" {
return inbound, false, common.NewError("empty client ID")
}
} else {
default:
if client.ID == "" {
return inbound, false, common.NewError("empty client ID")
}
@@ -319,6 +344,65 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
return inbound, false, err
}
// Ensure created_at and updated_at exist in inbound.Settings clients
{
var oldSettings map[string]any
_ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
emailToCreated := map[string]int64{}
emailToUpdated := map[string]int64{}
if oldSettings != nil {
if oc, ok := oldSettings["clients"].([]any); ok {
for _, it := range oc {
if m, ok2 := it.(map[string]any); ok2 {
if email, ok3 := m["email"].(string); ok3 {
switch v := m["created_at"].(type) {
case float64:
emailToCreated[email] = int64(v)
case int64:
emailToCreated[email] = v
}
switch v := m["updated_at"].(type) {
case float64:
emailToUpdated[email] = int64(v)
case int64:
emailToUpdated[email] = v
}
}
}
}
}
}
var newSettings map[string]any
if err2 := json.Unmarshal([]byte(inbound.Settings), &newSettings); err2 == nil && newSettings != nil {
now := time.Now().Unix() * 1000
if nSlice, ok := newSettings["clients"].([]any); ok {
for i := range nSlice {
if m, ok2 := nSlice[i].(map[string]any); ok2 {
email, _ := m["email"].(string)
if _, ok3 := m["created_at"]; !ok3 {
if v, ok4 := emailToCreated[email]; ok4 && v > 0 {
m["created_at"] = v
} else {
m["created_at"] = now
}
}
// Preserve client's updated_at if present; do not bump on parent inbound update
if _, hasUpdated := m["updated_at"]; !hasUpdated {
if v, ok4 := emailToUpdated[email]; ok4 && v > 0 {
m["updated_at"] = v
}
}
nSlice[i] = m
}
}
newSettings["clients"] = nSlice
if bs, err3 := json.MarshalIndent(newSettings, "", " "); err3 == nil {
inbound.Settings = string(bs)
}
}
}
}
oldInbound.Up = inbound.Up
oldInbound.Down = inbound.Down
oldInbound.Total = inbound.Total
@@ -331,7 +415,6 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Settings = inbound.Settings
oldInbound.StreamSettings = inbound.StreamSettings
oldInbound.Sniffing = inbound.Sniffing
oldInbound.Allocate = inbound.Allocate
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
} else {
@@ -421,6 +504,17 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
}
interfaceClients := settings["clients"].([]any)
// Add timestamps for new clients being appended
nowTs := time.Now().Unix() * 1000
for i := range interfaceClients {
if cm, ok := interfaceClients[i].(map[string]any); ok {
if _, ok2 := cm["created_at"]; !ok2 {
cm["created_at"] = nowTs
}
cm["updated_at"] = nowTs
interfaceClients[i] = cm
}
}
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return false, err
@@ -436,15 +530,16 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
// Secure client ID
for _, client := range clients {
if oldInbound.Protocol == "trojan" {
switch oldInbound.Protocol {
case "trojan":
if client.Password == "" {
return false, common.NewError("empty client ID")
}
} else if oldInbound.Protocol == "shadowsocks" {
case "shadowsocks":
if client.Email == "" {
return false, common.NewError("empty client ID")
}
} else {
default:
if client.ID == "" {
return false, common.NewError("empty client ID")
}
@@ -631,13 +726,14 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
clientIndex := -1
for index, oldClient := range oldClients {
oldClientId := ""
if oldInbound.Protocol == "trojan" {
switch oldInbound.Protocol {
case "trojan":
oldClientId = oldClient.Password
newClientId = clients[0].Password
} else if oldInbound.Protocol == "shadowsocks" {
case "shadowsocks":
oldClientId = oldClient.Email
newClientId = clients[0].Email
} else {
default:
oldClientId = oldClient.ID
newClientId = clients[0].ID
}
@@ -669,6 +765,25 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
return false, err
}
settingsClients := oldSettings["clients"].([]any)
// Preserve created_at and set updated_at for the replacing client
var preservedCreated any
if clientIndex >= 0 && clientIndex < len(settingsClients) {
if oldMap, ok := settingsClients[clientIndex].(map[string]any); ok {
if v, ok2 := oldMap["created_at"]; ok2 {
preservedCreated = v
}
}
}
if len(interfaceClients) > 0 {
if newMap, ok := interfaceClients[0].(map[string]any); ok {
if preservedCreated == nil {
preservedCreated = time.Now().Unix() * 1000
}
newMap["created_at"] = preservedCreated
newMap["updated_at"] = time.Now().Unix() * 1000
interfaceClients[0] = newMap
}
}
settingsClients[clientIndex] = interfaceClients[0]
oldSettings["clients"] = settingsClients
@@ -811,8 +926,9 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic
if traffic.IsInbound {
err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
Updates(map[string]any{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
"all_time": gorm.Expr("COALESCE(all_time, 0) + ?", traffic.Up+traffic.Down),
}).Error
if err != nil {
return err
@@ -858,10 +974,12 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
dbClientTraffics[dbTraffic_index].AllTime += (traffics[traffic_index].Up + traffics[traffic_index].Down)
// Add user in onlineUsers array on traffic
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
onlineClients = append(onlineClients, traffics[traffic_index].Email)
dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()
}
break
}
@@ -906,10 +1024,16 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
oldExpiryTime := c["expiryTime"].(float64)
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
c["expiryTime"] = newExpiryTime
c["updated_at"] = time.Now().Unix() * 1000
dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime
break
}
}
// Backfill created_at and updated_at
if _, ok := c["created_at"]; !ok {
c["created_at"] = time.Now().Unix() * 1000
}
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
settings["clients"] = newClients
@@ -1244,11 +1368,12 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
switch inbound.Protocol {
case "trojan":
clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" {
case "shadowsocks":
clientId = oldClient.Email
} else {
default:
clientId = oldClient.ID
}
break
@@ -1270,6 +1395,7 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["tgId"] = tgId
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
@@ -1328,11 +1454,12 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
switch inbound.Protocol {
case "trojan":
clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" {
case "shadowsocks":
clientId = oldClient.Email
} else {
default:
clientId = oldClient.ID
}
clientOldEnabled = oldClient.Enable
@@ -1355,6 +1482,7 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["enable"] = !clientOldEnabled
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
@@ -1391,11 +1519,12 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
switch inbound.Protocol {
case "trojan":
clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" {
case "shadowsocks":
clientId = oldClient.Email
} else {
default:
clientId = oldClient.ID
}
break
@@ -1417,6 +1546,7 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["limitIp"] = count
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
@@ -1448,11 +1578,12 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
switch inbound.Protocol {
case "trojan":
clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" {
case "shadowsocks":
clientId = oldClient.Email
} else {
default:
clientId = oldClient.ID
}
break
@@ -1474,6 +1605,7 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["expiryTime"] = expiry_time
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
@@ -1508,11 +1640,12 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
switch inbound.Protocol {
case "trojan":
clientId = oldClient.Password
} else if inbound.Protocol == "shadowsocks" {
case "shadowsocks":
clientId = oldClient.Email
} else {
default:
clientId = oldClient.ID
}
break
@@ -1534,6 +1667,7 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
c := clients[client_index].(map[string]any)
if c["email"] == clientEmail {
c["totalGB"] = totalGB * 1024 * 1024 * 1024
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
}
@@ -1785,6 +1919,21 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
return nil, nil
}
func (s *InboundService) UpdateClientTrafficByEmail(email string, upload int64, download int64) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", email).
Updates(map[string]any{"up": upload, "down": download})
err := result.Error
if err != nil {
logger.Warningf("Error updating ClientTraffic with email %s: %v", email, err)
return err
}
return nil
}
func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, error) {
db := database.GetDB()
var traffics []xray.ClientTraffic
@@ -1901,6 +2050,25 @@ func (s *InboundService) MigrationRequirements() {
}
}()
// Calculate and backfill all_time from up+down for inbounds and clients
err = tx.Exec(`
UPDATE inbounds
SET all_time = IFNULL(up, 0) + IFNULL(down, 0)
WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0
`).Error
if err != nil {
return
}
err = tx.Exec(`
UPDATE client_traffics
SET all_time = IFNULL(up, 0) + IFNULL(down, 0)
WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0
`).Error
if err != nil {
return
}
// Fix inbounds based problems
var inbounds []*model.Inbound
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
@@ -1939,6 +2107,11 @@ func (s *InboundService) MigrationRequirements() {
c["flow"] = ""
}
}
// Backfill created_at and updated_at
if _, ok := c["created_at"]; !ok {
c["created_at"] = time.Now().Unix() * 1000
}
c["updated_at"] = time.Now().Unix() * 1000
newClients = append(newClients, any(c))
}
settings["clients"] = newClients
@@ -2027,6 +2200,20 @@ func (s *InboundService) GetOnlineClients() []string {
return p.GetOnlineClients()
}
func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
db := database.GetDB()
var rows []xray.ClientTraffic
err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
result := make(map[string]int64, len(rows))
for _, r := range rows {
result[r.Email] = r.LastOnline
}
return result, nil
}
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
db := database.GetDB()

View File

@@ -2,6 +2,7 @@ package service
import (
"archive/zip"
"bufio"
"bytes"
"encoding/json"
"fmt"
@@ -234,23 +235,44 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
}
// IP fetching with caching
showIp4ServiceLists := []string{
"https://api4.ipify.org",
"https://ipv4.icanhazip.com",
"https://v4.api.ipinfo.io/ip",
"https://ipv4.myexternalip.com/raw",
"https://4.ident.me",
"https://check-host.net/ip",
}
showIp6ServiceLists := []string{
"https://api6.ipify.org",
"https://ipv6.icanhazip.com",
"https://v6.api.ipinfo.io/ip",
"https://ipv6.myexternalip.com/raw",
"https://6.ident.me",
}
if s.cachedIPv4 == "" {
s.cachedIPv4 = getPublicIP("https://api.ipify.org")
if s.cachedIPv4 == "N/A" {
s.cachedIPv4 = getPublicIP("https://4.ident.me")
for _, ip4Service := range showIp4ServiceLists {
s.cachedIPv4 = getPublicIP(ip4Service)
if s.cachedIPv4 != "N/A" {
break
}
}
}
if s.cachedIPv6 == "" && !s.noIPv6 {
s.cachedIPv6 = getPublicIP("https://api6.ipify.org")
if s.cachedIPv6 == "N/A" {
s.cachedIPv6 = getPublicIP("https://6.ident.me")
if s.cachedIPv6 == "N/A" {
s.noIPv6 = true
for _, ip6Service := range showIp6ServiceLists {
s.cachedIPv6 = getPublicIP(ip6Service)
if s.cachedIPv6 != "N/A" {
break
}
}
}
if s.cachedIPv6 == "N/A" {
s.noIPv6 = true
}
status.PublicIP.IPv4 = s.cachedIPv4
status.PublicIP.IPv6 = s.cachedIPv6
@@ -321,7 +343,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
continue
}
if major > 25 || (major == 25 && minor > 6) || (major == 25 && minor == 6 && patch >= 8) {
if major > 25 || (major == 25 && minor > 8) || (major == 25 && minor == 8 && patch >= 3) {
versions = append(versions, release.TagName)
}
}
@@ -338,7 +360,6 @@ func (s *ServerService) StopXrayService() error {
}
func (s *ServerService) RestartXrayService() error {
s.xrayService.StopXray()
err := s.xrayService.RestartXray(true)
if err != nil {
logger.Error("start xray failed:", err)
@@ -473,6 +494,81 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
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) {
config, err := s.xrayService.GetXrayConfig()
if err != nil {
@@ -658,27 +754,43 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
return nil
}
var fileURL string
for _, file := range files {
if file.FileName == fileName {
fileURL = file.URL
break
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)
if fileURL == "" {
return common.NewErrorf("File '%s' not found in the list of Geofiles", fileName)
}
var fileURL string
for _, file := range files {
if file.FileName == fileName {
fileURL = file.URL
break
}
}
destPath := fmt.Sprintf("%s/%s", config.GetBinFolderPath(), fileName)
if fileURL == "" {
errorMessages = append(errorMessages, fmt.Sprintf("File '%s' not found in the list of Geofiles", fileName))
}
if err := downloadFile(fileURL, destPath); err != nil {
return common.NewErrorf("Error downloading Geofile '%s': %v", fileName, err)
if err := downloadFile(fileURL, destPath); err != nil {
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", fileName, err))
}
}
err := s.RestartXrayService()
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
@@ -709,3 +821,103 @@ func (s *ServerService) GetNewX25519Cert() (any, error) {
return keyPair, nil
}
func (s *ServerService) GetNewmldsa65() (any, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "mldsa65")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
SeedLine := strings.Split(lines[0], ":")
VerifyLine := strings.Split(lines[1], ":")
seed := strings.TrimSpace(SeedLine[1])
verify := strings.TrimSpace(VerifyLine[1])
keyPair := map[string]any{
"seed": seed,
"verify": verify,
}
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
}
type AuthBlock struct {
Label string `json:"label"`
Decryption string `json:"decryption"`
Encryption string `json:"encryption"`
}
func (s *ServerService) GetNewVlessEnc() (any, error) {
cmd := exec.Command(xray.GetBinaryPath(), "vlessenc")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
var blocks []AuthBlock
var current *AuthBlock
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Authentication:") {
if current != nil {
blocks = append(blocks, *current)
}
current = &AuthBlock{Label: strings.TrimSpace(strings.TrimPrefix(line, "Authentication:"))}
} else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 && current != nil {
key := strings.Trim(parts[0], `" `)
val := strings.Trim(parts[1], `" `)
switch key {
case "decryption":
current.Decryption = val
case "encryption":
current.Encryption = val
}
}
}
}
if current != nil {
blocks = append(blocks, *current)
}
return map[string]any{
"auths": blocks,
}, nil
}

View File

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

View File

@@ -1,6 +1,7 @@
package service
import (
"context"
"crypto/rand"
"embed"
"encoding/base64"
@@ -39,7 +40,6 @@ var (
isRunning bool
hostname string
hashStorage *global.HashStorage
handler *th.Handler
// clients data to adding new client
receiver_inbound_ID int
@@ -148,7 +148,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
}
// 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{
{Command: "start", Description: t.I18nBot("tgbot.commands.startDesc")},
{Command: "help", Description: t.I18nBot("tgbot.commands.helpDesc")},
@@ -221,8 +221,9 @@ func (t *Tgbot) SetHostname() {
}
func (t *Tgbot) Stop() {
botHandler.Stop()
bot.StopLongPolling()
if botHandler != nil {
botHandler.Stop()
}
logger.Info("Stop Telegram receiver ...")
isRunning = false
adminIds = nil
@@ -255,26 +256,29 @@ func (t *Tgbot) OnReceive() {
Timeout: 10,
}
updates, _ := bot.UpdatesViaLongPolling(&params)
updates, _ := bot.UpdatesViaLongPolling(context.Background(), &params)
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)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
return nil
}, 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)
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
return nil
}, 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)
t.answerCallback(&query, checkAdmin(query.From.ID))
return nil
}, 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 {
switch userState {
case "awaiting_id":
@@ -284,7 +288,7 @@ func (t *Tgbot) OnReceive() {
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
return
return nil
}
client_Id = strings.TrimSpace(message.Text)
@@ -309,7 +313,7 @@ func (t *Tgbot) OnReceive() {
if client_TrPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return
return nil
}
client_TrPassword = strings.TrimSpace(message.Text)
@@ -334,7 +338,7 @@ func (t *Tgbot) OnReceive() {
if client_ShPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return
return nil
}
client_ShPassword = strings.TrimSpace(message.Text)
@@ -359,7 +363,7 @@ func (t *Tgbot) OnReceive() {
if client_Email == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return
return nil
}
client_Email = strings.TrimSpace(message.Text)
@@ -384,7 +388,7 @@ func (t *Tgbot) OnReceive() {
if client_Comment == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return
return nil
}
client_Comment = strings.TrimSpace(message.Text)
@@ -417,6 +421,7 @@ func (t *Tgbot) OnReceive() {
}
}
}
return nil
}, th.AnyMessage())
botHandler.Start()
@@ -635,13 +640,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 4 {
num, err := strconv.Atoi(dataArray[3])
if err == nil {
if num == -2 {
switch num {
case -2:
inputNumber = 0
} else if num == -1 {
case -1:
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
} else {
default:
inputNumber = (inputNumber * 10) + num
}
}
@@ -698,8 +704,12 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId)
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
case "add_client_limit_traffic_in":
if len(dataArray) >= 2 {
@@ -709,13 +719,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 3 {
num, err := strconv.Atoi(dataArray[2])
if err == nil {
if num == -2 {
switch num {
case -2:
inputNumber = 0
} else if num == -1 {
case -1:
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
} else {
default:
inputNumber = (inputNumber * 10) + num
}
}
@@ -838,13 +849,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 4 {
num, err := strconv.Atoi(dataArray[3])
if err == nil {
if num == -2 {
switch num {
case -2:
inputNumber = 0
} else if num == -1 {
case -1:
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
} else {
default:
inputNumber = (inputNumber * 10) + num
}
}
@@ -913,8 +925,12 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId)
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
case "add_client_reset_exp_in":
if len(dataArray) >= 2 {
@@ -924,13 +940,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 3 {
num, err := strconv.Atoi(dataArray[2])
if err == nil {
if num == -2 {
switch num {
case -2:
inputNumber = 0
} else if num == -1 {
case -1:
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
} else {
default:
inputNumber = (inputNumber * 10) + num
}
}
@@ -1029,13 +1046,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 4 {
num, err := strconv.Atoi(dataArray[3])
if err == nil {
if num == -2 {
switch num {
case -2:
inputNumber = 0
} else if num == -1 {
case -1:
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
} else {
default:
inputNumber = (inputNumber * 10) + num
}
}
@@ -1095,8 +1113,12 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId)
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
case "add_client_ip_limit_in":
if len(dataArray) >= 2 {
@@ -1106,13 +1128,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 3 {
num, err := strconv.Atoi(dataArray[2])
if err == nil {
if num == -2 {
switch num {
case -2:
inputNumber = 0
} else if num == -1 {
case -1:
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
} else {
default:
inputNumber = (inputNumber * 10) + num
}
}
@@ -1157,8 +1180,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "clear_ips":
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
@@ -1284,8 +1305,12 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text)
t.addClient(callbackQuery.Message.GetChat().ID, message_text)
}
return
} else {
@@ -1520,6 +1545,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
case "add_client_default_ip_limit":
@@ -1530,6 +1559,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
case "add_client_submit_disable":
@@ -1594,6 +1627,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
valid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove())
return
}
for _, valid_emails := range valid_emails {
traffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails)
@@ -1756,6 +1793,10 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
}
jsonString, err := t.BuildJSONForProtocol(inbound.Protocol)
if err != nil {
logger.Warning("BuildJSONForProtocol run failed:", err)
return false, errors.New("failed to build JSON for protocol")
}
newInbound := &model.Inbound{
Id: receiver_inbound_ID,
@@ -1859,7 +1900,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
params.ReplyMarkup = replyMarkup[0]
}
_, err := bot.SendMessage(&params)
_, err := bot.SendMessage(context.Background(), &params)
if err != nil {
logger.Warning("Error sending telegram message :", err)
}
@@ -2004,10 +2045,11 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
}
msg := ""
if status == LoginSuccess {
switch status {
case LoginSuccess:
msg += t.I18nBot("tgbot.messages.loginSuccess")
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
} else if status == LoginFail {
case LoginFail:
msg += t.I18nBot("tgbot.messages.loginFailed")
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
msg += t.I18nBot("tgbot.messages.password", "Password=="+password)
@@ -2167,6 +2209,22 @@ func (t *Tgbot) clientInfoMsg(
expiryTime = t.I18nBot("tgbot.unlimited")
} else if diff > 172800 || !traffic.Enable {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
if diff > 0 {
days := diff / 86400
hours := (diff % 86400) / 3600
minutes := (diff % 3600) / 60
remainingTime := ""
if days > 0 {
remainingTime += fmt.Sprintf("%d %s ", days, t.I18nBot("tgbot.days"))
}
if hours > 0 {
remainingTime += fmt.Sprintf("%d %s ", hours, t.I18nBot("tgbot.hours"))
}
if minutes > 0 {
remainingTime += fmt.Sprintf("%d %s", minutes, t.I18nBot("tgbot.minutes"))
}
expiryTime += fmt.Sprintf(" (%s)", remainingTime)
}
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
flag = true
@@ -2765,7 +2823,7 @@ func (t *Tgbot) sendBackup(chatId int64) {
tu.ID(chatId),
tu.File(file),
)
_, err = bot.SendDocument(document)
_, err = bot.SendDocument(context.Background(), document)
if err != nil {
logger.Error("Error in uploading backup: ", err)
}
@@ -2779,7 +2837,7 @@ func (t *Tgbot) sendBackup(chatId int64) {
tu.ID(chatId),
tu.File(file),
)
_, err = bot.SendDocument(document)
_, err = bot.SendDocument(context.Background(), document)
if err != nil {
logger.Error("Error in uploading config.json: ", err)
}
@@ -2803,7 +2861,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
tu.ID(chatId),
tu.File(file),
)
_, err = bot.SendDocument(document)
_, err = bot.SendDocument(context.Background(), document)
if err != nil {
logger.Error("Error in uploading IPLimitBannedPrevLog: ", err)
}
@@ -2824,7 +2882,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
tu.ID(chatId),
tu.File(file),
)
_, err = bot.SendDocument(document)
_, err = bot.SendDocument(context.Background(), document)
if err != nil {
logger.Error("Error in uploading IPLimitBannedLog: ", err)
}
@@ -2842,7 +2900,7 @@ func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
CallbackQueryID: id,
Text: message,
}
if err := bot.AnswerCallbackQuery(&params); err != nil {
if err := bot.AnswerCallbackQuery(context.Background(), &params); err != nil {
logger.Warning(err)
}
}
@@ -2853,7 +2911,7 @@ func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyb
MessageID: messageID,
ReplyMarkup: inlineKeyboard,
}
if _, err := bot.EditMessageReplyMarkup(&params); err != nil {
if _, err := bot.EditMessageReplyMarkup(context.Background(), &params); err != nil {
logger.Warning(err)
}
}
@@ -2868,7 +2926,7 @@ func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlin
if len(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)
}
}
@@ -2881,7 +2939,7 @@ func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSecon
}
// Send the message
sentMsg, err := bot.SendMessage(&telego.SendMessageParams{
sentMsg, err := bot.SendMessage(context.Background(), &telego.SendMessageParams{
ChatID: tu.ID(chatId),
Text: msg,
ReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value
@@ -2904,7 +2962,7 @@ func (t *Tgbot) deleteMessageTgBot(chatId int64, messageID int) {
ChatID: tu.ID(chatId),
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)
} else {
logger.Info("Message deleted successfully")

View File

@@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"errors"
"runtime"
"sync"
"x-ui/logger"
@@ -14,7 +15,8 @@ import (
var (
p *xray.Process
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
)
@@ -32,7 +34,16 @@ func (s *XrayService) GetXrayErr() error {
if p == 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 {
@@ -45,7 +56,15 @@ func (s *XrayService) GetXrayResult() string {
if p == nil {
return ""
}
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
}
@@ -184,7 +203,8 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, []*xray.ClientTraffic,
func (s *XrayService) RestartXray(isForce bool) error {
lock.Lock()
defer lock.Unlock()
logger.Debug("restart xray, force:", isForce)
logger.Debug("restart Xray, force:", isForce)
isManuallyStopped.Store(false)
xrayConfig, err := s.GetXrayConfig()
if err != nil {
@@ -192,8 +212,8 @@ func (s *XrayService) RestartXray(isForce bool) error {
}
if s.IsXrayRunning() {
if !isForce && p.GetConfig().Equals(xrayConfig) {
logger.Debug("It does not need to restart xray")
if !isForce && p.GetConfig().Equals(xrayConfig) && !isNeedXrayRestart.Load() {
logger.Debug("It does not need to restart Xray")
return nil
}
p.Stop()
@@ -205,12 +225,14 @@ func (s *XrayService) RestartXray(isForce bool) error {
if err != nil {
return err
}
return nil
}
func (s *XrayService) StopXray() error {
lock.Lock()
defer lock.Unlock()
isManuallyStopped.Store(true)
logger.Debug("Attempting to stop Xray...")
if s.IsXrayRunning() {
return p.Stop()
@@ -225,3 +247,8 @@ func (s *XrayService) SetToNeedRestart() {
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
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

@@ -50,6 +50,7 @@
"fail" = "فشل"
"comment" = "تعليق"
"success" = "تم بنجاح"
"lastOnline" = "آخر متصل"
"getVersion" = "جيب النسخة"
"install" = "تثبيت"
"clients" = "عملاء"
@@ -132,6 +133,8 @@
"xraySwitchVersionPopover" = "تم تحديث Xray بنجاح"
"geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟"
"geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#."
"geofilesUpdateDialogDesc" = "سيؤدي هذا إلى تحديث كافة الملفات."
"geofilesUpdateAll" = "تحديث الكل"
"geofileUpdatePopover" = "تم تحديث ملف الجغرافيا بنجاح"
"dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة"
"logs" = "السجلات"
@@ -149,6 +152,8 @@
"getConfigError" = "حدث خطأ أثناء استرجاع ملف الإعدادات"
[pages.inbounds]
"allTimeTraffic" = "إجمالي حركة المرور"
"allTimeTrafficUsage" = "إجمالي الاستخدام طوال الوقت"
"title" = "الإدخالات"
"totalDownUp" = "إجمالي المرسل/المستقبل"
"totalUsage" = "إجمالي الاستخدام"
@@ -158,10 +163,13 @@
"remark" = "ملاحظة"
"protocol" = "بروتوكول"
"port" = "بورت"
"portMap" = "خريطة البورت"
"traffic" = "الترافيك"
"details" = "تفاصيل"
"transportConfig" = "نقل"
"expireDate" = "المدة"
"createdAt" = "تاريخ الإنشاء"
"updatedAt" = "تاريخ التحديث"
"resetTraffic" = "إعادة ضبط الترافيك"
"addInbound" = "أضف إدخال"
"generalActions" = "إجراءات عامة"
@@ -173,8 +181,6 @@
"deleteClient" = "حذف العميل"
"deleteClientContent" = "متأكد إنك عايز تحذف العميل؟"
"resetTrafficContent" = "متأكد إنك عايز تعيد ضبط الترافيك؟"
"inboundUpdateSuccess" = "تم تحديث الوارد بنجاح."
"inboundCreateSuccess" = "تم إنشاء الوارد بنجاح."
"copyLink" = "انسخ الرابط"
"address" = "العنوان"
"network" = "الشبكة"
@@ -260,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "تم إعادة تعيين حركة المرور"
"trafficGetError" = "خطأ في الحصول على حركات المرور"
"getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519."
"getNewmldsa65Error" = "حدث خطاء في الحصول على mldsa65."
"getNewVlessEncError" = "حدث خطأ أثناء الحصول على VlessEnc."
[pages.inbounds.stream.general]
"request" = "طلب"
@@ -559,24 +567,25 @@
"resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة"
[tgbot]
"keyboardClosed" = "❌ الكيبورد المخصص اتقفلت!"
"noResult" = "❗ مفيش نتيجة!"
"noQuery" = "❌ مش لاقي السؤال! استخدم الأمر تاني!"
"wentWrong" = "❌ حصل خطأ!"
"noIpRecord" = "❗ مفيش سجل IP!"
"noInbounds" = "❗ مفيش إدخال متواجد!"
"unlimited" = "♾ غير محدود (إعادة ضبط)"
"add" = "أضف"
"keyboardClosed" = "❌ لوحة المفاتيح مغلقة!"
"noResult" = "❗ لا يوجد نتائج!"
"noQuery" = "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!"
"wentWrong" = "❌ حدث خطأ ما!"
"noIpRecord" = "❗ لا يوجد سجل IP!"
"noInbounds" = "❗ لم يتم العثور على أي وارد!"
"unlimited" = "♾ غير محدود (إعادة تعيين)"
"add" = "إضافة"
"month" = "شهر"
"months" = "شهور"
"months" = "أشهر"
"day" = "يوم"
"days" = "أيام"
"hours" = "ساعات"
"unknown" = "مش معروف"
"inbounds" = "الإدخالات"
"minutes" = "دقائق"
"unknown" = "غير معروف"
"inbounds" = "الواردات"
"clients" = "العملاء"
"offline" = "🔴 أوفلاين"
"online" = "🟢 أونلاين"
"offline" = "🔴 غير متصل"
"online" = "🟢 متصل"
[tgbot.commands]
"unknown" = "❗ أمر مش معروف."
@@ -644,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 اتحدّث في: {{ .Time }}\r\n\r\n"
"yes" = "✅ أيوه"
"no" = "❌ لأ"
"received_id" = "🔑📥 الـ ID اتحدث."
"received_password" = "🔑📥 الباسورد اتحدث."
"received_email" = "📧📥 الإيميل اتحدث."
@@ -664,7 +672,6 @@
"FailedResetTraffic" = "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ❌ فشل \n\n🛠 الخطأ: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 عملية إعادة ضبط الترافيك خلصت لكل العملاء."
[tgbot.buttons]
"closeKeyboard" = "❌ اقفل الكيبورد"
"cancel" = "❌ إلغاء"
@@ -698,7 +705,6 @@
"limitTraffic" = "🚧 حد الترافيك"
"getBanLogs" = "احصل على سجلات الحظر"
"allClients" = "كل العملاء"
"addClient" = "إضافة عميل"
"submitDisable" = "إرسال كمعطّل ☑️"
"submitEnable" = "إرسال كمفعّل ✅"
@@ -710,7 +716,6 @@
"ResetAllTraffics" = "إعادة ضبط جميع الترافيك"
"SortedTrafficUsageReport" = "تقرير استخدام الترافيك المرتب"
[tgbot.answers]
"successfulOperation" = "✅ العملية نجحت!"
"errorOperation" = "❗ حصل خطأ في العملية."

View File

@@ -50,6 +50,7 @@
"fail" = "Failed"
"comment" = "Comment"
"success" = "Successfully"
"lastOnline" = "Last Online"
"getVersion" = "Get Version"
"install" = "Install"
"clients" = "Clients"
@@ -132,6 +133,8 @@
"xraySwitchVersionPopover" = "Xray updated successfully"
"geofileUpdateDialog" = "Do you really want to update the geofile?"
"geofileUpdateDialogDesc" = "This will update the #filename# file."
"geofilesUpdateDialogDesc" = "This will update all geofiles."
"geofilesUpdateAll" = "Update all"
"geofileUpdatePopover" = "Geofile updated successfully"
"dontRefresh" = "Installation is in progress, please do not refresh this page"
"logs" = "Logs"
@@ -149,6 +152,8 @@
"getConfigError" = "An error occurred while retrieving the config file."
[pages.inbounds]
"allTimeTraffic" = "All-time Traffic"
"allTimeTrafficUsage" = "All Time Total Usage"
"title" = "Inbounds"
"totalDownUp" = "Total Sent/Received"
"totalUsage" = "Total Usage"
@@ -158,10 +163,13 @@
"remark" = "Remark"
"protocol" = "Protocol"
"port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic"
"details" = "Details"
"transportConfig" = "Transport"
"expireDate" = "Duration"
"createdAt" = "Created"
"updatedAt" = "Updated"
"resetTraffic" = "Reset Traffic"
"addInbound" = "Add Inbound"
"generalActions" = "General Actions"
@@ -258,7 +266,8 @@
"resetInboundClientTrafficSuccess" = "Traffic has been reset."
"trafficGetError" = "Error getting traffics."
"getNewX25519CertError" = "Error while obtaining the X25519 certificate."
"getNewmldsa65Error" = "Error while obtaining mldsa65."
"getNewVlessEncError" = "Error while obtaining VlessEnc."
[pages.inbounds.stream.general]
"request" = "Request"
@@ -571,6 +580,7 @@
"day" = "Day"
"days" = "Days"
"hours" = "Hours"
"minutes" = "Minutes"
"unknown" = "Unknown"
"inbounds" = "Inbounds"
"clients" = "Clients"
@@ -643,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n\r\n"
"yes" = "✅ Yes"
"no" = "❌ No"
"received_id" = "🔑📥 ID updated."
"received_password" = "🔑📥 Password updated."
"received_email" = "📧📥 Email updated."
@@ -663,7 +672,6 @@
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ❌ Failed \n\n🛠 Error: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Traffic reset process finished for all clients."
[tgbot.buttons]
"closeKeyboard" = "❌ Close Keyboard"
"cancel" = "❌ Cancel"
@@ -697,7 +705,6 @@
"limitTraffic" = "🚧 Traffic Limit"
"getBanLogs" = "Get Ban Logs"
"allClients" = "All Clients"
"addClient" = "Add Client"
"submitDisable" = "Submit As Disable ☑️"
"submitEnable" = "Submit As Enable ✅"

View File

@@ -50,6 +50,7 @@
"fail" = "Falló"
"comment" = "Comentario"
"success" = "Éxito"
"lastOnline" = "Última conexión"
"getVersion" = "Obtener versión"
"install" = "Instalar"
"clients" = "Clientes"
@@ -91,7 +92,7 @@
"invalidFormData" = "El formato de los datos de entrada es inválido."
"emptyUsername" = "Por favor ingresa el nombre de usuario."
"emptyPassword" = "Por favor ingresa la contraseña."
"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
"successLogin" = "Has iniciado sesión en tu cuenta correctamente."
[pages.index]
@@ -117,8 +118,6 @@
"operationHours" = "Tiempo de Funcionamiento"
"systemLoad" = "Carga del Sistema"
"systemLoadDesc" = "promedio de carga del sistema en los últimos 1, 5 y 15 minutos"
"connectionTcpCountDesc" = "Conexiones TCP totales en todas las tarjetas de red."
"connectionUdpCountDesc" = "Conexiones UDP totales en todas las tarjetas de red."
"connectionCount" = "Número de Conexiones"
"ipAddresses" = "Direcciones IP"
"toggleIpVisibility" = "Alternar visibilidad de la IP"
@@ -134,6 +133,8 @@
"xraySwitchVersionPopover" = "Xray se actualizó correctamente"
"geofileUpdateDialog" = "¿Realmente deseas actualizar el geofichero?"
"geofileUpdateDialogDesc" = "Esto actualizará el archivo #filename#."
"geofilesUpdateDialogDesc" = "Esto actualizará todos los archivos."
"geofilesUpdateAll" = "Actualizar todo"
"geofileUpdatePopover" = "Geofichero actualizado correctamente"
"dontRefresh" = "La instalación está en progreso, por favor no actualices esta página."
"logs" = "Registros"
@@ -151,6 +152,8 @@
"getConfigError" = "Ocurrió un error al obtener el archivo de configuración"
[pages.inbounds]
"allTimeTraffic" = "Tráfico Total"
"allTimeTrafficUsage" = "Uso total de todos los tiempos"
"title" = "Entradas"
"totalDownUp" = "Subidas/Descargas Totales"
"totalUsage" = "Uso Total"
@@ -160,10 +163,13 @@
"remark" = "Notas"
"protocol" = "Protocolo"
"port" = "Puerto"
"portMap" = "Puertos de Destino"
"traffic" = "Tráfico"
"details" = "Detalles"
"transportConfig" = "Transporte"
"expireDate" = "Fecha de Expiración"
"createdAt" = "Creado"
"updatedAt" = "Actualizado"
"resetTraffic" = "Restablecer Tráfico"
"addInbound" = "Agregar Entrada"
"generalActions" = "Acciones Generales"
@@ -175,8 +181,6 @@
"deleteClient" = "Eliminar cliente"
"deleteClientContent" = "¿Está seguro de que desea eliminar el cliente?"
"resetTrafficContent" = "¿Confirmar restablecimiento de tráfico?"
"inboundUpdateSuccess" = "La entrada se ha actualizado correctamente."
"inboundCreateSuccess" = "La entrada se ha creado correctamente."
"copyLink" = "Copiar Enlace"
"address" = "Dirección"
"network" = "Red"
@@ -262,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "El tráfico ha sido reiniciado"
"trafficGetError" = "Error al obtener los tráficos"
"getNewX25519CertError" = "Error al obtener el certificado X25519."
"getNewmldsa65Error" = "Error al obtener el certificado mldsa65."
"getNewVlessEncError" = "Error al obtener el certificado VlessEnc."
[pages.inbounds.stream.general]
"request" = "Pedido"
@@ -535,9 +541,9 @@
[pages.settings.security]
"admin" = "Credenciales de administrador"
"twoFactor" = "Autenticación de dos factores"
"twoFactorEnable" = "Habilitar 2FA"
"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
"twoFactor" = "Autenticación de dos factores"
"twoFactorEnable" = "Habilitar 2FA"
"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
"twoFactorModalSetTitle" = "Activar autenticación de dos factores"
"twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores"
"twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:"
@@ -561,23 +567,24 @@
"resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente"
[tgbot]
"keyboardClosed" = "❌ ¡Teclado personalizado cerrado!"
"noResult" = "❗ ¡Sin resultados!"
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor utiliza el comando nuevamente!"
"keyboardClosed" = "❌ Teclado cerrado!"
"noResult" = "❗ ¡No hay resultados!"
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando de nuevo!"
"wentWrong" = "❌ ¡Algo salió mal!"
"noIpRecord" = "❗ ¡Sin Registro de IP!"
"noIpRecord" = "❗ ¡No hay registro de IP!"
"noInbounds" = "❗ ¡No se encontraron entradas!"
"unlimited" = "♾ Ilimitado"
"add" = "Agregar"
"unlimited" = "♾ Ilimitado (Restablecer)"
"add" = "Añadir"
"month" = "Mes"
"months" = "Meses"
"day" = "Día"
"days" = "Días"
"hours" = "Horas"
"minutes" = "Minutos"
"unknown" = "Desconocido"
"inbounds" = "Entradas"
"clients" = "Clientes"
"offline" = "🔴 Sin conexión"
"offline" = "🔴 Desconectado"
"online" = "🟢 En línea"
[tgbot.commands]
@@ -646,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n\r\n"
"yes" = "✅ Sí"
"no" = "❌ No"
"received_id" = "🔑📥 ID actualizado."
"received_password" = "🔑📥 Contraseña actualizada."
"received_email" = "📧📥 Correo electrónico actualizado."
@@ -666,7 +672,6 @@
"FailedResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ❌ Fallido \n\n🛠 Error: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Proceso de reinicio de tráfico finalizado para todos los clientes."
[tgbot.buttons]
"closeKeyboard" = "❌ Cerrar Teclado"
"cancel" = "❌ Cancelar"
@@ -700,7 +705,6 @@
"limitTraffic" = "🚧 Límite de tráfico"
"getBanLogs" = "Registros de prohibición"
"allClients" = "Todos los Clientes"
"addClient" = "Añadir cliente"
"submitDisable" = "Enviar como deshabilitado ☑️"
"submitEnable" = "Enviar como habilitado ✅"
@@ -712,7 +716,6 @@
"ResetAllTraffics" = "Reiniciar todo el tráfico"
"SortedTrafficUsageReport" = "Informe de uso de tráfico ordenado"
[tgbot.answers]
"successfulOperation" = "✅ ¡Exitosa!"
"errorOperation" = "❗ Error en la Operación."

View File

@@ -50,6 +50,7 @@
"fail" = "ناموفق"
"comment" = "توضیحات"
"success" = "موفق"
"lastOnline" = "آخرین فعالیت"
"getVersion" = "دریافت نسخه"
"install" = "نصب"
"clients" = "کاربران"
@@ -117,8 +118,6 @@
"operationHours" = "مدت‌کارکرد"
"systemLoad" = "بارسیستم"
"systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته"
"connectionTcpCountDesc" = "در تمام‌شبکه‌ها TCP مجموع‌اتصالات"
"connectionUdpCountDesc" = "در تمام‌شبکه‌ها UDP مجموع‌اتصالات"
"connectionCount" = "تعداد کانکشن ها"
"ipAddresses" = "آدرس‌های IP"
"toggleIpVisibility" = "تغییر وضعیت نمایش IP"
@@ -134,6 +133,8 @@
"xraySwitchVersionPopover" = "Xray با موفقیت به‌روز شد"
"geofileUpdateDialog" = "آیا واقعاً می‌خواهید فایل جغرافیایی را به‌روز کنید؟"
"geofileUpdateDialogDesc" = "این عمل فایل #filename# را به‌روز می‌کند."
"geofilesUpdateDialogDesc" = "با این کار همه فایل‌ها به‌روزرسانی می‌شوند."
"geofilesUpdateAll" = "همه را به‌روزرسانی کنید"
"geofileUpdatePopover" = "فایل جغرافیایی با موفقیت به‌روز شد"
"dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید"
"logs" = "گزارش‌ها"
@@ -151,6 +152,8 @@
"getConfigError" = "خطا در دریافت فایل پیکربندی"
[pages.inbounds]
"allTimeTraffic" = "کل ترافیک"
"allTimeTrafficUsage" = "کل استفاده در تمام مدت"
"title" = "کاربران"
"totalDownUp" = "دریافت/ارسال کل"
"totalUsage" = "‌‌‌مصرف کل"
@@ -160,10 +163,13 @@
"remark" = "نام"
"protocol" = "پروتکل"
"port" = "پورت"
"portMap" = "پورت‌های نظیر"
"traffic" = "ترافیک"
"details" = "توضیحات"
"transportConfig" = "نحوه اتصال"
"expireDate" = "مدت زمان"
"createdAt" = "ایجاد"
"updatedAt" = "به‌روزرسانی"
"resetTraffic" = "ریست ترافیک"
"addInbound" = "افزودن ورودی"
"generalActions" = "عملیات کلی"
@@ -175,8 +181,6 @@
"deleteClient" = "حذف کاربر"
"deleteClientContent" = "آیا مطمئن به حذف کاربر هستید؟"
"resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید؟"
"inboundUpdateSuccess" = "ورودی با موفقیت به‌روزرسانی شد."
"inboundCreateSuccess" = "ورودی با موفقیت ایجاد شد."
"copyLink" = "کپی لینک"
"address" = "آدرس"
"network" = "شبکه"
@@ -262,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "ترافیک بازنشانی شد"
"trafficGetError" = "خطا در دریافت ترافیک‌ها"
"getNewX25519CertError" = "خطا در دریافت گواهی X25519."
"getNewmldsa65Error" = "خطا در دریافت گواهی mldsa65."
"getNewVlessEncError" = "خطا در دریافت گواهی VlessEnc."
[pages.inbounds.stream.general]
"request" = "درخواست"
@@ -358,16 +364,16 @@
"subDomainDesc" = "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید‌"
"subUpdates" = "فاصله بروزرسانی‌ سابسکریپشن"
"subUpdatesDesc" = "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت"
"externalTrafficInformEnable" = "اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformEnableDesc" = "مصرف ترافیک به سرویس خارجی ارسال می شود"
"externalTrafficInformURI" = "لینک اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformURIDesc" = "ترافیک های مصرفی به این لینک هم ارسال می شود"
"subEncrypt" = "کدگذاری"
"subEncryptDesc" = "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه"
"subShowInfo" = "نمایش اطلاعات مصرف"
"subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد"
"subURI" = "پروکسی معکوس URI مسیر"
"subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر"
"externalTrafficInformEnable" = "اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformEnableDesc" = "مصرف ترافیک به سرویس خارجی ارسال می شود"
"externalTrafficInformURI" = "لینک اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformURIDesc" = "ترافیک های مصرفی به این لینک هم ارسال می شود"
"fragment" = "فرگمنت"
"fragmentDesc" = "فعال کردن فرگمنت برای بسته‌ی نخست تی‌ال‌اس"
"fragmentSett" = "تنظیمات فرگمنت"
@@ -561,22 +567,23 @@
"resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی"
[tgbot]
"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!"
"noResult" = "❗ نتیجهای یافت نشد!"
"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!"
"wentWrong" = "❌ مشکلی رخ داده است!"
"noIpRecord" = "❗ رکورد IP یافت نشد!"
"keyboardClosed" = "❌ صفحه کلید بسته شد!"
"noResult" = "❗ نتیجه ای یافت نشد!"
"noQuery" = "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!"
"wentWrong" = "❌ مشکلی پیش آمد!"
"noIpRecord" = "❗ رکورد آی پی وجود ندارد!"
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
"unlimited" = "♾ - نامحدود(ریست)"
"add" = "اضافه کردن"
"unlimited" = "♾ نامحدود(ریست)"
"add" = "افزودن"
"month" = "ماه"
"months" = "ماه"
"months" = "ماه"
"day" = "روز"
"days" = "روز"
"hours" = "ساعت"
"hours" = "ساعت"
"minutes" = "دقیقه"
"unknown" = "نامشخص"
"inbounds" = "ورودیها"
"clients" = لاینت‌ها"
"inbounds" = "ورودی ها"
"clients" = اربران"
"offline" = "🔴 آفلاین"
"online" = "🟢 آنلاین"
@@ -646,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n\r\n"
"yes" = "✅ بله"
"no" = "❌ خیر"
"received_id" = "🔑📥 شناسه به‌روزرسانی شد."
"received_password" = "🔑📥 رمز عبور به‌روزرسانی شد."
"received_email" = "📧📥 ایمیل به‌روزرسانی شد."
@@ -666,7 +672,6 @@
"FailedResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠 خطا: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 فرآیند بازنشانی ترافیک برای همه مشتریان به پایان رسید."
[tgbot.buttons]
"closeKeyboard" = "❌ بستن کیبورد"
"cancel" = "❌ لغو"
@@ -700,7 +705,6 @@
"limitTraffic" = "🚧 محدودیت ترافیک"
"getBanLogs" = "گزارش های بلوک را دریافت کنید"
"allClients" = "همه مشتریان"
"addClient" = "افزودن مشتری"
"submitDisable" = "ارسال به عنوان غیرفعال ☑️"
"submitEnable" = "ارسال به عنوان فعال ✅"
@@ -712,7 +716,6 @@
"ResetAllTraffics" = "بازنشانی همه ترافیک‌ها"
"SortedTrafficUsageReport" = "گزارش استفاده از ترافیک مرتب‌شده"
[tgbot.answers]
"successfulOperation" = "✅ انجام شد!"
"errorOperation" = "❗ خطا در عملیات."

View File

@@ -50,6 +50,7 @@
"fail" = "Gagal"
"comment" = "Komentar"
"success" = "Berhasil"
"lastOnline" = "Terakhir online"
"getVersion" = "Dapatkan Versi"
"install" = "Instal"
"clients" = "Klien"
@@ -117,8 +118,6 @@
"operationHours" = "Waktu Aktif"
"systemLoad" = "Beban Sistem"
"systemLoadDesc" = "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir"
"connectionTcpCountDesc" = "Total koneksi TCP di seluruh sistem"
"connectionUdpCountDesc" = "Total koneksi UDP di seluruh sistem"
"connectionCount" = "Statistik Koneksi"
"ipAddresses" = "Alamat IP"
"toggleIpVisibility" = "Alihkan visibilitas IP"
@@ -134,6 +133,8 @@
"xraySwitchVersionPopover" = "Xray berhasil diperbarui"
"geofileUpdateDialog" = "Apakah Anda yakin ingin memperbarui geofile?"
"geofileUpdateDialogDesc" = "Ini akan memperbarui file #filename#."
"geofilesUpdateDialogDesc" = "Ini akan memperbarui semua berkas."
"geofilesUpdateAll" = "Perbarui semua"
"geofileUpdatePopover" = "Geofile berhasil diperbarui"
"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini"
"logs" = "Log"
@@ -151,6 +152,8 @@
"getConfigError" = "Terjadi kesalahan saat mengambil file konfigurasi"
[pages.inbounds]
"allTimeTraffic" = "Total Lalu Lintas"
"allTimeTrafficUsage" = "Total Penggunaan Sepanjang Waktu"
"title" = "Masuk"
"totalDownUp" = "Total Terkirim/Diterima"
"totalUsage" = "Penggunaan Total"
@@ -160,10 +163,13 @@
"remark" = "Catatan"
"protocol" = "Protokol"
"port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic"
"details" = "Rincian"
"transportConfig" = "Transport"
"expireDate" = "Durasi"
"createdAt" = "Dibuat"
"updatedAt" = "Diperbarui"
"resetTraffic" = "Reset Traffic"
"addInbound" = "Tambahkan Masuk"
"generalActions" = "Tindakan Umum"
@@ -175,8 +181,6 @@
"deleteClient" = "Hapus Klien"
"deleteClientContent" = "Apakah Anda yakin ingin menghapus klien?"
"resetTrafficContent" = "Apakah Anda yakin ingin mereset traffic?"
"inboundUpdateSuccess" = "Inbound berhasil diperbarui."
"inboundCreateSuccess" = "Inbound berhasil dibuat."
"copyLink" = "Salin URL"
"address" = "Alamat"
"network" = "Jaringan"
@@ -262,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "Lalu lintas telah direset"
"trafficGetError" = "Gagal mendapatkan data lalu lintas"
"getNewX25519CertError" = "Terjadi kesalahan saat mendapatkan sertifikat X25519."
"getNewmldsa65Error" = "Terjadi kesalahan saat mendapatkan sertifikat mldsa65."
"getNewVlessEncError" = "Terjadi kesalahan saat mendapatkan sertifikat VlessEnc."
[pages.inbounds.stream.general]
"request" = "Permintaan"
@@ -421,7 +427,6 @@
"RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan."
"Torrent" = "Blokir Protokol BitTorrent"
"TorrentDesc" = "Memblokir protokol BitTorrent."
"Inbounds" = "Masuk"
"InboundsDesc" = "Menerima klien tertentu."
"Outbounds" = "Keluar"
@@ -562,21 +567,22 @@
"resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar"
[tgbot]
"keyboardClosed" = "❌ Papan ketik kustom ditutup!"
"keyboardClosed" = "❌ Keyboard ditutup!"
"noResult" = "❗ Tidak ada hasil!"
"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!"
"wentWrong" = "❌ Ada yang salah!"
"noQuery" = "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!"
"wentWrong" = "❌ Terjadi kesalahan!"
"noIpRecord" = "❗ Tidak ada Catatan IP!"
"noInbounds" = "❗ Tidak ada masuk ditemukan!"
"unlimited" = "♾ Tak terbatas"
"noInbounds" = "❗ Tidak ada inbound yang ditemukan!"
"unlimited" = "♾ Tidak terbatas (Reset)"
"add" = "Tambah"
"month" = "Bulan"
"months" = "Bulan"
"day" = "Hari"
"days" = "Hari"
"hours" = "Jam"
"minutes" = "Menit"
"unknown" = "Tidak diketahui"
"inbounds" = "Masuk"
"inbounds" = "Inbound"
"clients" = "Klien"
"offline" = "🔴 Offline"
"online" = "🟢 Online"
@@ -647,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n"
"yes" = "✅ Ya"
"no" = "❌ Tidak"
"received_id" = "🔑📥 ID diperbarui."
"received_password" = "🔑📥 Kata sandi diperbarui."
"received_email" = "📧📥 Email diperbarui."
@@ -667,7 +672,6 @@
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ❌ Gagal \n\n🛠 Kesalahan: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Proses reset traffic selesai untuk semua klien."
[tgbot.buttons]
"closeKeyboard" = "❌ Tutup Papan Ketik"
"cancel" = "❌ Batal"
@@ -701,7 +705,6 @@
"limitTraffic" = "🚧 Batas Lalu Lintas"
"getBanLogs" = "Dapatkan Log Pemblokiran"
"allClients" = "Semua Klien"
"addClient" = "Tambah Klien"
"submitDisable" = "Kirim Sebagai Nonaktif ☑️"
"submitEnable" = "Kirim Sebagai Aktif ✅"
@@ -713,7 +716,6 @@
"ResetAllTraffics" = "Reset Semua Lalu Lintas"
"SortedTrafficUsageReport" = "Laporan Penggunaan Lalu Lintas yang Terurut"
[tgbot.answers]
"successfulOperation" = "✅ Operasi berhasil!"
"errorOperation" = "❗ Kesalahan dalam operasi."

View File

@@ -50,6 +50,7 @@
"fail" = "失敗"
"comment" = "コメント"
"success" = "成功"
"lastOnline" = "最終オンライン"
"getVersion" = "バージョン取得"
"install" = "インストール"
"clients" = "クライアント"
@@ -117,8 +118,6 @@
"operationHours" = "システム稼働時間"
"systemLoad" = "システム負荷"
"systemLoadDesc" = "過去1、5、15分間のシステム平均負荷"
"connectionTcpCountDesc" = "システム内のすべてのTCP接続数"
"connectionUdpCountDesc" = "システム内のすべてのUDP接続数"
"connectionCount" = "接続数"
"ipAddresses" = "IPアドレス"
"toggleIpVisibility" = "IPの表示を切り替える"
@@ -134,6 +133,8 @@
"xraySwitchVersionPopover" = "Xrayの更新が成功しました"
"geofileUpdateDialog" = "ジオファイルを本当に更新しますか?"
"geofileUpdateDialogDesc" = "これにより#filename#ファイルが更新されます。"
"geofilesUpdateDialogDesc" = "これにより、すべてのファイルが更新されます。"
"geofilesUpdateAll" = "すべて更新"
"geofileUpdatePopover" = "ジオファイルの更新が成功しました"
"dontRefresh" = "インストール中、このページをリロードしないでください"
"logs" = "ログ"
@@ -151,6 +152,8 @@
"getConfigError" = "設定ファイルの取得中にエラーが発生しました"
[pages.inbounds]
"allTimeTraffic" = "総トラフィック"
"allTimeTrafficUsage" = "これまでの総使用量"
"title" = "インバウンド一覧"
"totalDownUp" = "総アップロード / ダウンロード"
"totalUsage" = "総使用量"
@@ -160,10 +163,13 @@
"remark" = "備考"
"protocol" = "プロトコル"
"port" = "ポート"
"portMap" = "ポートマッピング"
"traffic" = "トラフィック"
"details" = "詳細情報"
"transportConfig" = "トランスポート設定"
"expireDate" = "有効期限"
"createdAt" = "作成"
"updatedAt" = "更新"
"resetTraffic" = "トラフィックリセット"
"addInbound" = "インバウンド追加"
"generalActions" = "一般操作"
@@ -175,8 +181,6 @@
"deleteClient" = "クライアント削除"
"deleteClientContent" = "クライアントを削除してもよろしいですか?"
"resetTrafficContent" = "トラフィックをリセットしてもよろしいですか?"
"inboundUpdateSuccess" = "インバウンドが正常に更新されました。"
"inboundCreateSuccess" = "インバウンドが正常に作成されました。"
"copyLink" = "リンクをコピー"
"address" = "アドレス"
"network" = "ネットワーク"
@@ -262,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "トラフィックがリセットされました"
"trafficGetError" = "トラフィックの取得中にエラーが発生しました"
"getNewX25519CertError" = "X25519証明書の取得中にエラーが発生しました。"
"getNewmldsa65Error" = "mldsa65証明書の取得中にエラーが発生しました。"
"getNewVlessEncError" = "VlessEnc証明書の取得中にエラーが発生しました。"
[pages.inbounds.stream.general]
"request" = "リクエスト"
@@ -561,21 +567,22 @@
"resetOutboundTrafficError" = "送信トラフィックのリセットエラー"
[tgbot]
"keyboardClosed" = "❌ カスタムキーボード閉じられました!"
"keyboardClosed" = "❌ キーボード閉じました!"
"noResult" = "❗ 結果がありません!"
"noQuery" = "❌ クエリが見つかりませんでした!もう一度コマンドを使用してください!"
"wentWrong" = "❌ 問題が発生しました!"
"noIpRecord" = "❗ IP記録がありません!"
"noInbounds" = "❗ インバウンド接続が見つかりません!"
"unlimited" = "♾ 無制限"
"noQuery" = "❌ クエリが見つかりませんコマンドを再利用してください!"
"wentWrong" = "❌ 何かがうまくいかなかった!"
"noIpRecord" = "❗ IPレコードがありません!"
"noInbounds" = "❗ インバウンドが見つかりません!"
"unlimited" = "♾ 無制限(リセット)"
"add" = "追加"
"month" = "月"
"months" = "月"
"months" = "月"
"day" = "日"
"days" = "日"
"days" = "日"
"hours" = "時間"
"minutes" = "分"
"unknown" = "不明"
"inbounds" = "インバウンド接続"
"inbounds" = "インバウンド"
"clients" = "クライアント"
"offline" = "🔴 オフライン"
"online" = "🟢 オンライン"
@@ -646,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 更新時間:{{ .Time }}\r\n\r\n"
"yes" = "✅ はい"
"no" = "❌ いいえ"
"received_id" = "🔑📥 IDが更新されました。"
"received_password" = "🔑📥 パスワードが更新されました。"
"received_email" = "📧📥 メールが更新されました。"
@@ -666,7 +672,6 @@
"FailedResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠 エラー: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 すべてのクライアントのトラフィックリセットが完了しました。"
[tgbot.buttons]
"closeKeyboard" = "❌ キーボードを閉じる"
"cancel" = "❌ キャンセル"
@@ -700,7 +705,6 @@
"limitTraffic" = "🚧 トラフィック制限"
"getBanLogs" = "禁止ログ"
"allClients" = "すべてのクライアント"
"addClient" = "クライアントを追加"
"submitDisable" = "無効として送信 ☑️"
"submitEnable" = "有効として送信 ✅"
@@ -712,7 +716,6 @@
"ResetAllTraffics" = "すべてのトラフィックをリセット"
"SortedTrafficUsageReport" = "ソートされたトラフィック使用レポート"
[tgbot.answers]
"successfulOperation" = "✅ 成功!"
"errorOperation" = "❗ 操作エラー。"

View File

@@ -50,6 +50,7 @@
"fail" = "Falhou"
"comment" = "Comentário"
"success" = "Com Sucesso"
"lastOnline" = "Última vez online"
"getVersion" = "Obter Versão"
"install" = "Instalar"
"clients" = "Clientes"
@@ -117,8 +118,6 @@
"operationHours" = "Tempo de Atividade"
"systemLoad" = "Carga do Sistema"
"systemLoadDesc" = "Média de carga do sistema nos últimos 1, 5 e 15 minutos"
"connectionTcpCountDesc" = "Total de conexões TCP no sistema"
"connectionUdpCountDesc" = "Total de conexões UDP no sistema"
"connectionCount" = "Estatísticas de Conexão"
"ipAddresses" = "Endereços IP"
"toggleIpVisibility" = "Alternar visibilidade do IP"
@@ -134,6 +133,8 @@
"xraySwitchVersionPopover" = "Xray atualizado com sucesso"
"geofileUpdateDialog" = "Você realmente deseja atualizar o geofile?"
"geofileUpdateDialogDesc" = "Isso atualizará o arquivo #filename#."
"geofilesUpdateDialogDesc" = "Isso atualizará todos os arquivos."
"geofilesUpdateAll" = "Atualizar tudo"
"geofileUpdatePopover" = "Geofile atualizado com sucesso"
"dontRefresh" = "Instalação em andamento, por favor não atualize a página"
"logs" = "Logs"
@@ -151,6 +152,8 @@
"getConfigError" = "Ocorreu um erro ao recuperar o arquivo de configuração"
[pages.inbounds]
"allTimeTraffic" = "Tráfego Total"
"allTimeTrafficUsage" = "Uso total de todos os tempos"
"title" = "Inbounds"
"totalDownUp" = "Total Enviado/Recebido"
"totalUsage" = "Uso Total"
@@ -160,10 +163,13 @@
"remark" = "Observação"
"protocol" = "Protocolo"
"port" = "Porta"
"portMap" = "Porta Mapeada"
"traffic" = "Tráfego"
"details" = "Detalhes"
"transportConfig" = "Transporte"
"expireDate" = "Duração"
"createdAt" = "Criado"
"updatedAt" = "Atualizado"
"resetTraffic" = "Redefinir Tráfego"
"addInbound" = "Adicionar Inbound"
"generalActions" = "Ações Gerais"
@@ -175,8 +181,6 @@
"deleteClient" = "Excluir Cliente"
"deleteClientContent" = "Tem certeza de que deseja excluir o cliente?"
"resetTrafficContent" = "Tem certeza de que deseja redefinir o tráfego?"
"inboundUpdateSuccess" = "A entrada foi atualizada com sucesso."
"inboundCreateSuccess" = "A entrada foi criada com sucesso."
"copyLink" = "Copiar URL"
"address" = "Endereço"
"network" = "Rede"
@@ -262,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "O tráfego foi reiniciado"
"trafficGetError" = "Erro ao obter tráfegos"
"getNewX25519CertError" = "Erro ao obter o certificado X25519."
"getNewmldsa65Error" = "Erro ao obter o certificado mldsa65."
"getNewVlessEncError" = "Erro ao obter o certificado VlessEnc."
[pages.inbounds.stream.general]
"request" = "Requisição"
@@ -561,21 +567,22 @@
"resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída"
[tgbot]
"keyboardClosed" = "❌ Teclado personalizado fechado!"
"keyboardClosed" = "❌ Teclado fechado!"
"noResult" = "❗ Nenhum resultado!"
"noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!"
"wentWrong" = "❌ Algo deu errado!"
"noIpRecord" = "❗ Nenhum registro de IP!"
"noInbounds" = "❗ Nenhuma entrada encontrada!"
"unlimited" = "♾ Ilimitado (Reiniciar)"
"noInbounds" = "❗ Nenhum inbound encontrado!"
"unlimited" = "♾ Ilimitado (Reset)"
"add" = "Adicionar"
"month" = "Mês"
"months" = "Meses"
"day" = "Dia"
"days" = "Dias"
"hours" = "Horas"
"minutes" = "Minutos"
"unknown" = "Desconhecido"
"inbounds" = "Entradas"
"inbounds" = "Inbounds"
"clients" = "Clientes"
"offline" = "🔴 Offline"
"online" = "🟢 Online"
@@ -646,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 Atualizado em: {{ .Time }}\r\n\r\n"
"yes" = "✅ Sim"
"no" = "❌ Não"
"received_id" = "🔑📥 ID atualizado."
"received_password" = "🔑📥 Senha atualizada."
"received_email" = "📧📥 E-mail atualizado."
@@ -666,7 +672,6 @@
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ❌ Falhou \n\n🛠 Erro: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Processo de redefinição de tráfego concluído para todos os clientes."
[tgbot.buttons]
"closeKeyboard" = "❌ Fechar teclado"
"cancel" = "❌ Cancelar"
@@ -700,7 +705,6 @@
"limitTraffic" = "🚧 Limite de tráfego"
"getBanLogs" = "Obter logs de banimento"
"allClients" = "Todos os clientes"
"addClient" = "Adicionar Cliente"
"submitDisable" = "Enviar como Desativado ☑️"
"submitEnable" = "Enviar como Ativado ✅"
@@ -712,7 +716,6 @@
"ResetAllTraffics" = "Redefinir Todo o Tráfego"
"SortedTrafficUsageReport" = "Relatório de Uso de Tráfego Ordenado"
[tgbot.answers]
"successfulOperation" = "✅ Operação bem-sucedida!"
"errorOperation" = "❗ Erro na operação."

View File

@@ -50,6 +50,7 @@
"fail" = "Ошибка"
"comment" = "Комментарий"
"success" = "Успешно"
"lastOnline" = "Был(а) в сети"
"getVersion" = "Узнать версию"
"install" = "Установка"
"clients" = "Клиенты"
@@ -117,8 +118,6 @@
"operationHours" = "Время работы системы"
"systemLoad" = "Нагрузка на систему"
"systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут"
"connectionTcpCountDesc" = "Общее количество подключений TCP по всем сетевым картам."
"connectionUdpCountDesc" = "Общее количество подключений UDP по всем сетевым картам."
"connectionCount" = "Количество соединений"
"ipAddresses" = "IP-адреса сервера"
"toggleIpVisibility" = "Переключить видимость IP-адресов сервера"
@@ -134,6 +133,8 @@
"xraySwitchVersionPopover" = "Xray успешно обновлён"
"geofileUpdateDialog" = "Вы действительно хотите обновить геофайл?"
"geofileUpdateDialogDesc" = "Это обновит файл #filename#."
"geofilesUpdateDialogDesc" = "Это обновит все геофайлы."
"geofilesUpdateAll" = "Обновить все"
"geofileUpdatePopover" = "Геофайл успешно обновлён"
"dontRefresh" = "Установка в процессе. Не обновляйте страницу"
"logs" = "Журнал"
@@ -151,6 +152,8 @@
"getConfigError" = "Произошла ошибка при получении конфигурационного файла"
[pages.inbounds]
"allTimeTraffic" = "Общий трафик"
"allTimeTrafficUsage" = "Общее использование за все время"
"title" = "Инбаунды"
"totalDownUp" = "Объем отправленного/полученного трафика"
"totalUsage" = "Всего трафика"
@@ -160,10 +163,13 @@
"remark" = "Примечание"
"protocol" = "Протокол"
"port" = "Порт"
"portMap" = "Порт-маппинг"
"traffic" = "Трафик"
"details" = "Подробнее"
"transportConfig" = "Транспорт"
"expireDate" = "Дата окончания"
"createdAt" = "Создано"
"updatedAt" = "Обновлено"
"resetTraffic" = "Сброс трафика"
"addInbound" = "Создать инбаунд"
"generalActions" = "Общие действия"
@@ -175,8 +181,6 @@
"deleteClient" = "Удалить клиента"
"deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
"resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?"
"inboundUpdateSuccess" = "Инбаунд успешно обновлен."
"inboundCreateSuccess" = "Инбаунд успешно создан."
"copyLink" = "Копировать ссылку"
"address" = "Адрес"
"network" = "Сеть"
@@ -262,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "Трафик сброшен"
"trafficGetError" = "Ошибка получения данных о трафике"
"getNewX25519CertError" = "Ошибка при получении сертификата X25519."
"getNewmldsa65Error" = "Ошибка при получении сертификата mldsa65."
"getNewVlessEncError" = "Ошибка при получении сертификата VlessEnc."
[pages.inbounds.stream.general]
"request" = "Запрос"
@@ -574,6 +580,7 @@
"day" = "День"
"days" = "Дней"
"hours" = "Часов"
"minutes" = "Минуты"
"unknown" = "Неизвестно"
"inbounds" = "Инбаунды"
"clients" = "Клиенты"
@@ -646,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n\r\n"
"yes" = "✅ Да"
"no" = "❌ Нет"
"received_id" = "🔑📥 ID обновлён."
"received_password" = "🔑📥 Пароль обновлён."
"received_email" = "📧📥 Email обновлен."
@@ -666,7 +672,6 @@
"FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠 Ошибка: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Сброс трафика завершён для всех клиентов."
[tgbot.buttons]
"closeKeyboard" = "❌ Закрыть клавиатуру"
"cancel" = "❌ Отмена"
@@ -700,7 +705,6 @@
"limitTraffic" = "🚧 Лимит трафика"
"getBanLogs" = "📄 Лог банов"
"allClients" = "👥 Все клиенты"
"addClient" = " Новый клиент"
"submitDisable" = "Добавить отключенным ☑️"
"submitEnable" = "Добавить включенныи ✅"
@@ -712,7 +716,6 @@
"ResetAllTraffics" = "Сбросить весь трафик"
"SortedTrafficUsageReport" = "Отсортированный отчет об использовании трафика"
[tgbot.answers]
"successfulOperation" = "✅ Успешно!"
"errorOperation" = "❗ Ошибка в операции."

View File

@@ -50,6 +50,7 @@
"fail" = "Başarısız"
"comment" = "Yorum"
"success" = "Başarılı"
"lastOnline" = "Son çevrimiçi"
"getVersion" = "Sürümü Al"
"install" = "Yükle"
"clients" = "Müşteriler"
@@ -117,8 +118,6 @@
"operationHours" = "Çalışma Süresi"
"systemLoad" = "Sistem Yükü"
"systemLoadDesc" = "Geçmiş 1, 5 ve 15 dakika için sistem yük ortalaması"
"connectionTcpCountDesc" = "Sistem genelinde toplam TCP bağlantıları"
"connectionUdpCountDesc" = "Sistem genelinde toplam UDP bağlantıları"
"connectionCount" = "Bağlantı İstatistikleri"
"ipAddresses" = "IP adresleri"
"toggleIpVisibility" = "IP görünürlüğünü değiştir"
@@ -134,6 +133,8 @@
"xraySwitchVersionPopover" = "Xray başarıyla güncellendi"
"geofileUpdateDialog" = "Geofile'ı gerçekten güncellemek istiyor musunuz?"
"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"
"dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin"
"logs" = "Günlükler"
@@ -151,6 +152,8 @@
"getConfigError" = "Yapılandırma dosyası alınırken bir hata oluştu"
[pages.inbounds]
"allTimeTraffic" = "Toplam Trafik"
"allTimeTrafficUsage" = "Tüm Zamanların Toplam Kullanımı"
"title" = "Gelenler"
"totalDownUp" = "Toplam Gönderilen/Alınan"
"totalUsage" = "Toplam Kullanım"
@@ -160,10 +163,13 @@
"remark" = "Açıklama"
"protocol" = "Protokol"
"port" = "Port"
"portMap" = "Port Atama"
"traffic" = "Trafik"
"details" = "Detaylar"
"transportConfig" = "Taşıma"
"expireDate" = "Süre"
"createdAt" = "Oluşturuldu"
"updatedAt" = "Güncellendi"
"resetTraffic" = "Trafiği Sıfırla"
"addInbound" = "Gelen Ekle"
"generalActions" = "Genel Eylemler"
@@ -175,8 +181,6 @@
"deleteClient" = "Müşteriyi Sil"
"deleteClientContent" = "Müşteriyi silmek istediğinizden emin misiniz?"
"resetTrafficContent" = "Trafiği sıfırlamak istediğinizden emin misiniz?"
"inboundUpdateSuccess" = "Gelen bağlantı başarıyla güncellendi."
"inboundCreateSuccess" = "Gelen bağlantı başarıyla oluşturuldu."
"copyLink" = "URL'yi Kopyala"
"address" = "Adres"
"network" = "Ağ"
@@ -262,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "Trafik sıfırlandı"
"trafficGetError" = "Trafik bilgisi alınırken hata oluştu"
"getNewX25519CertError" = "X25519 sertifikası alınırken hata oluştu."
"getNewmldsa65Error" = "mldsa65 sertifikası alınırken hata oluştu."
"getNewVlessEncError" = "VlessEnc sertifikası alınırken hata oluştu."
[pages.inbounds.stream.general]
"request" = "İstek"
@@ -561,22 +567,23 @@
"resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata"
[tgbot]
"keyboardClosed" = "❌ Özel klavye kapalı!"
"keyboardClosed" = "❌ Klavye kapatıldı!"
"noResult" = "❗ Sonuç yok!"
"noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!"
"wentWrong" = "❌ Bir şeyler yanlış gitti!"
"noIpRecord" = "❗ IP Kaydı yok!"
"noInbounds" = "❗ Gelen bulunamadı!"
"unlimited" = "♾ Sınırsız(Sıfırla)"
"noIpRecord" = "❗ IP Kaydı Yok!"
"noInbounds" = "❗ Gelen bağlantı bulunamadı!"
"unlimited" = "♾ Sınırsız (Sıfırla)"
"add" = "Ekle"
"month" = "Ay"
"months" = "Aylar"
"day" = "Gün"
"days" = "Günler"
"hours" = "Saatler"
"unknown" = "Bilinmiyor"
"minutes" = "Dakika"
"unknown" = "Bilinmeyen"
"inbounds" = "Gelenler"
"clients" = "Müşteriler"
"clients" = "İstemciler"
"offline" = "🔴 Çevrimdışı"
"online" = "🟢 Çevrimiçi"
@@ -646,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 Yenilendi: {{ .Time }}\r\n\r\n"
"yes" = "✅ Evet"
"no" = "❌ Hayır"
"received_id" = "🔑📥 Kimlik güncellendi."
"received_password" = "🔑📥 Şifre güncellendi."
"received_email" = "📧📥 E-posta güncellendi."
@@ -666,7 +672,6 @@
"FailedResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠 Hata: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Tüm müşteriler için trafik sıfırlama işlemi tamamlandı."
[tgbot.buttons]
"closeKeyboard" = "❌ Klavyeyi Kapat"
"cancel" = "❌ İptal"
@@ -700,7 +705,6 @@
"limitTraffic" = "🚧 Trafik Sınırı"
"getBanLogs" = "Yasak Günlüklerini Al"
"allClients" = "Tüm Müşteriler"
"addClient" = "Müşteri Ekle"
"submitDisable" = "Devre Dışı Olarak Gönder ☑️"
"submitEnable" = "Etkin Olarak Gönder ✅"
@@ -712,7 +716,6 @@
"ResetAllTraffics" = "Tüm Trafikleri Sıfırla"
"SortedTrafficUsageReport" = "Sıralı Trafik Kullanım Raporu"
[tgbot.answers]
"successfulOperation" = "✅ İşlem başarılı!"
"errorOperation" = "❗ İşlemde hata."

View File

@@ -50,6 +50,7 @@
"fail" = "Помилка"
"comment" = "Коментар"
"success" = "Успішно"
"lastOnline" = "Був(ла) онлайн"
"getVersion" = "Отримати версію"
"install" = "Встановити"
"clients" = "Клієнти"
@@ -117,8 +118,6 @@
"operationHours" = "Час роботи"
"systemLoad" = "Завантаження системи"
"systemLoadDesc" = "Середнє завантаження системи за останні 1, 5 і 15 хвилин"
"connectionTcpCountDesc" = "Загальна кількість TCP-з'єднань у системі"
"connectionUdpCountDesc" = "Загальна кількість UDP-з'єднань у системі"
"connectionCount" = "Статистика з'єднання"
"ipAddresses" = "IP-адреси"
"toggleIpVisibility" = "Перемкнути видимість IP"
@@ -134,6 +133,8 @@
"xraySwitchVersionPopover" = "Xray успішно оновлено"
"geofileUpdateDialog" = "Ви дійсно хочете оновити геофайл?"
"geofileUpdateDialogDesc" = "Це оновить файл #filename#."
"geofilesUpdateDialogDesc" = "Це оновить усі геофайли."
"geofilesUpdateAll" = "Оновити все"
"geofileUpdatePopover" = "Геофайл успішно оновлено"
"dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку"
"logs" = "Журнали"
@@ -151,6 +152,8 @@
"getConfigError" = "Виникла помилка під час отримання файлу конфігурації"
[pages.inbounds]
"allTimeTraffic" = "Загальний трафік"
"allTimeTrafficUsage" = "Загальне використання за весь час"
"title" = "Вхідні"
"totalDownUp" = "Всього надісланих/отриманих"
"totalUsage" = "Всього використанно"
@@ -160,10 +163,13 @@
"remark" = "Примітка"
"protocol" = "Протокол"
"port" = "Порт"
"portMap" = "Порт-перехід"
"traffic" = "Трафік"
"details" = "Деталі"
"transportConfig" = "Транспорт"
"expireDate" = "Тривалість"
"createdAt" = "Створено"
"updatedAt" = "Оновлено"
"resetTraffic" = "Скинути трафік"
"addInbound" = "Додати вхідний"
"generalActions" = "Загальні дії"
@@ -175,8 +181,6 @@
"deleteClient" = "Видалити клієнта"
"deleteClientContent" = "Ви впевнені, що хочете видалити клієнт?"
"resetTrafficContent" = "Ви впевнені, що хочете скинути трафік?"
"inboundUpdateSuccess" = "Вхідне підключення успішно оновлено."
"inboundCreateSuccess" = "Вхідне підключення успішно створено."
"copyLink" = "Копіювати URL"
"address" = "Адреса"
"network" = "Мережа"
@@ -262,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "Трафік скинуто"
"trafficGetError" = "Помилка отримання даних про трафік"
"getNewX25519CertError" = "Помилка при отриманні сертифіката X25519."
"getNewmldsa65Error" = "Помилка при отриманні сертифіката mldsa65."
"getNewVlessEncError" = "Помилка при отриманні сертифіката VlessEnc."
[pages.inbounds.stream.general]
"request" = "Запит"
@@ -561,19 +567,20 @@
"resetOutboundTrafficError" = "Помилка скидання вихідного трафіку"
[tgbot]
"keyboardClosed" = "❌ Спеціальна клавіатура закрита!"
"keyboardClosed" = "❌ Клавіатуру закрито!"
"noResult" = "❗ Немає результату!"
"noQuery" = "❌ Запит не знайдено! Скористайтеся командою ще раз!"
"noQuery" = "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!"
"wentWrong" = "❌ Щось пішло не так!"
"noIpRecord" = "❗ Немає IP-запису!"
"noInbounds" = "❗ Вхідних не знайдено!"
"unlimited" = "♾ Необмежений (скинути)"
"noIpRecord" = "❗ Немає запису IP!"
"noInbounds" = "❗ Вхідні не знайдені!"
"unlimited" = "♾ Необмежено (Скинути)"
"add" = "Додати"
"month" = "Місяць"
"months" = "Місяці"
"day" = "День"
"days" = "Дні"
"hours" = "Годинник"
"hours" = "Години"
"minutes" = "Хвилини"
"unknown" = "Невідомо"
"inbounds" = "Вхідні"
"clients" = "Клієнти"
@@ -646,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 Оновлено: {{ .Time }}\r\n\r\n"
"yes" = "✅ Так"
"no" = "❌ Ні"
"received_id" = "🔑📥 ID оновлено."
"received_password" = "🔑📥 Пароль оновлено."
"received_email" = "📧📥 Електронна пошта оновлена."
@@ -666,7 +672,6 @@
"FailedResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ❌ Невдача \n\n🛠 Помилка: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Процес скидання трафіку завершено для всіх клієнтів."
[tgbot.buttons]
"closeKeyboard" = "❌ Закрити клавіатуру"
"cancel" = "❌ Скасувати"
@@ -700,7 +705,6 @@
"limitTraffic" = "🚧 Ліміт трафіку"
"getBanLogs" = "Отримати журнали заборон"
"allClients" = "Всі Клієнти"
"addClient" = "Додати клієнта"
"submitDisable" = "Надіслати як вимкнено ☑️"
"submitEnable" = "Надіслати як увімкнено ✅"
@@ -712,7 +716,6 @@
"ResetAllTraffics" = "Скинути весь трафік"
"SortedTrafficUsageReport" = "Відсортований звіт про використання трафіку"
[tgbot.answers]
"successfulOperation" = "✅ Операція успішна!"
"errorOperation" = "❗ Помилка в роботі."

View File

@@ -50,6 +50,7 @@
"fail" = "Thất bại"
"comment" = "Bình luận"
"success" = "Thành công"
"lastOnline" = "Lần online gần nhất"
"getVersion" = "Lấy phiên bản"
"install" = "Cài đặt"
"clients" = "Các khách hàng"
@@ -91,7 +92,7 @@
"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
"emptyUsername" = "Vui lòng nhập tên người dùng."
"emptyPassword" = "Vui lòng nhập mật khẩu."
"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
"successLogin" = "Bạn đã đăng nhập vào tài khoản thành công."
[pages.index]
@@ -117,8 +118,6 @@
"operationHours" = "Thời gian hoạt động"
"systemLoad" = "Tải hệ thống"
"systemLoadDesc" = "trung bình tải hệ thống trong 1, 5 và 15 phút qua"
"connectionTcpCountDesc" = "Tổng số kết nối TCP trên tất cả các thẻ mạng."
"connectionUdpCountDesc" = "Tổng số kết nối UDP trên tất cả các thẻ mạng."
"connectionCount" = "Số lượng kết nối"
"ipAddresses" = "Địa chỉ IP"
"toggleIpVisibility" = "Chuyển đổi hiển thị IP"
@@ -134,6 +133,8 @@
"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?"
"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"
"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ý"
@@ -151,6 +152,8 @@
"getConfigError" = "Lỗi xảy ra khi truy xuất tệp cấu hình"
[pages.inbounds]
"allTimeTraffic" = "Tổng Lưu Lượng"
"allTimeTrafficUsage" = "Tổng mức sử dụng mọi lúc"
"title" = "Điểm vào (Inbounds)"
"totalDownUp" = "Tổng tải lên/tải xuống"
"totalUsage" = "Tổng sử dụng"
@@ -160,10 +163,13 @@
"remark" = "Chú thích"
"protocol" = "Giao thức"
"port" = "Cổng"
"portMap" = "Cổng tạo"
"traffic" = "Lưu lượng"
"details" = "Chi tiết"
"transportConfig" = "Giao vận"
"expireDate" = "Ngày hết hạn"
"createdAt" = "Tạo lúc"
"updatedAt" = "Cập nhật"
"resetTraffic" = "Đặt lại lưu lượng"
"addInbound" = "Thêm điểm vào"
"generalActions" = "Hành động chung"
@@ -175,8 +181,6 @@
"deleteClient" = "Xóa người dùng"
"deleteClientContent" = "Bạn có chắc chắn muốn xóa người dùng không?"
"resetTrafficContent" = "Xác nhận đặt lại lưu lượng?"
"inboundUpdateSuccess" = "Đã cập nhật kết nối inbound thành công."
"inboundCreateSuccess" = "Đã tạo kết nối inbound thành công."
"copyLink" = "Sao chép liên kết"
"address" = "Địa chỉ"
"network" = "Mạng"
@@ -262,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "Đã đặt lại lưu lượng"
"trafficGetError" = "Lỗi khi lấy thông tin lưu lượng"
"getNewX25519CertError" = "Lỗi khi lấy chứng chỉ X25519."
"getNewmldsa65Error" = "Lỗi khi lấy chứng chỉ mldsa65."
"getNewVlessEncError" = "Lỗi khi lấy chứng chỉ VlessEnc."
[pages.inbounds.stream.general]
"request" = "Lời yêu cầu"
@@ -535,9 +541,9 @@
[pages.settings.security]
"admin" = "Thông tin đăng nhập quản trị viên"
"twoFactor" = "Xác thực hai yếu tố"
"twoFactorEnable" = "Bật 2FA"
"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
"twoFactor" = "Xác thực hai yếu tố"
"twoFactorEnable" = "Bật 2FA"
"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
"twoFactorModalSetTitle" = "Bật xác thực hai yếu tố"
"twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố"
"twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:"
@@ -561,22 +567,23 @@
"resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi"
[tgbot]
"keyboardClosed" = "❌ Bàn phím tùy chỉnh đã đóng!"
"keyboardClosed" = "❌ Bàn phím đã đóng!"
"noResult" = "❗ Không có kết quả!"
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lệnh lại!"
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!"
"wentWrong" = "❌ Đã xảy ra lỗi!"
"noIpRecord" = "❗ Không có bản ghi IP!"
"noInbounds" = "❗ Không tìm thấy inbound!"
"unlimited" = "♾ Không giới hạn"
"unlimited" = "♾ Không giới hạn (Đặt lại)"
"add" = "Thêm"
"month" = "Tháng"
"months" = "Tháng"
"day" = "Ngày"
"days" = "Ngày"
"hours" = "Giờ"
"unknown" = "Không rõ"
"inbounds" = "Vào"
"clients" = "Các người dùng"
"minutes" = "Phút"
"unknown" = "Không xác định"
"inbounds" = "Inbound"
"clients" = "Client"
"offline" = "🔴 Ngoại tuyến"
"online" = "🟢 Trực tuyến"
@@ -646,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n\r\n"
"yes" = "✅ Có"
"no" = "❌ Không"
"received_id" = "🔑📥 ID đã được cập nhật."
"received_password" = "🔑📥 Mật khẩu đã được cập nhật."
"received_email" = "📧📥 Email đã được cập nhật."
@@ -666,7 +672,6 @@
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠 Lỗi: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Quá trình đặt lại lưu lượng đã hoàn tất cho tất cả khách hàng."
[tgbot.buttons]
"closeKeyboard" = "❌ Đóng Bàn Phím"
"cancel" = "❌ Hủy"
@@ -700,7 +705,6 @@
"limitTraffic" = "🚧 Giới hạn lưu lượng"
"getBanLogs" = "Cấm nhật ký"
"allClients" = "Tất cả Khách hàng"
"addClient" = "Thêm Khách Hàng"
"submitDisable" = "Gửi Dưới Dạng Vô Hiệu ☑️"
"submitEnable" = "Gửi Dưới Dạng Kích Hoạt ✅"
@@ -712,7 +716,6 @@
"ResetAllTraffics" = "Đặt lại tất cả lưu lượng"
"SortedTrafficUsageReport" = "Báo cáo sử dụng lưu lượng đã sắp xếp"
[tgbot.answers]
"successfulOperation" = "✅ Thành công!"
"errorOperation" = "❗ Lỗi Trong Quá Trình Thực Hiện."

View File

@@ -50,6 +50,7 @@
"fail" = "失败"
"comment" = "评论"
"success" = "成功"
"lastOnline" = "上次在线"
"getVersion" = "获取版本"
"install" = "安装"
"clients" = "客户端"
@@ -117,8 +118,6 @@
"operationHours" = "系统正常运行时间"
"systemLoad" = "系统负载"
"systemLoadDesc" = "过去 1、5 和 15 分钟的系统平均负载"
"connectionTcpCountDesc" = "系统中所有 TCP 连接数"
"connectionUdpCountDesc" = "系统中所有 UDP 连接数"
"connectionCount" = "连接数"
"ipAddresses" = "IP地址"
"toggleIpVisibility" = "切换IP可见性"
@@ -134,6 +133,8 @@
"xraySwitchVersionPopover" = "Xray 更新成功"
"geofileUpdateDialog" = "您确定要更新地理文件吗?"
"geofileUpdateDialogDesc" = "这将更新 #filename# 文件。"
"geofilesUpdateDialogDesc" = "这将更新所有文件。"
"geofilesUpdateAll" = "全部更新"
"geofileUpdatePopover" = "地理文件更新成功"
"dontRefresh" = "安装中,请勿刷新此页面"
"logs" = "日志"
@@ -151,6 +152,8 @@
"getConfigError" = "检索配置文件时出错"
[pages.inbounds]
"allTimeTraffic" = "累计总流量"
"allTimeTrafficUsage" = "所有时间总使用量"
"title" = "入站列表"
"totalDownUp" = "总上传 / 下载"
"totalUsage" = "总用量"
@@ -160,10 +163,13 @@
"remark" = "备注"
"protocol" = "协议"
"port" = "端口"
"portMap" = "端口映射"
"traffic" = "流量"
"details" = "详细信息"
"transportConfig" = "传输配置"
"expireDate" = "到期时间"
"createdAt" = "创建时间"
"updatedAt" = "更新时间"
"resetTraffic" = "重置流量"
"addInbound" = "添加入站"
"generalActions" = "通用操作"
@@ -175,8 +181,6 @@
"deleteClient" = "删除客户端"
"deleteClientContent" = "确定要删除客户端吗?"
"resetTrafficContent" = "确定要重置流量吗?"
"inboundUpdateSuccess" = "入站连接已成功更新。"
"inboundCreateSuccess" = "入站连接已成功创建。"
"copyLink" = "复制链接"
"address" = "地址"
"network" = "网络"
@@ -262,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "流量已重置"
"trafficGetError" = "获取流量数据时出错"
"getNewX25519CertError" = "获取X25519证书时出错。"
"getNewmldsa65Error" = "获取mldsa65证书时出错。"
"getNewVlessEncError" = "获取VlessEnc证书时出错。"
[pages.inbounds.stream.general]
"request" = "请求"
@@ -563,19 +569,20 @@
[tgbot]
"keyboardClosed" = "❌ 自定义键盘已关闭!"
"noResult" = "❗ 没有结果!"
"noQuery" = "❌ 未找到查询!请重新使用命令!"
"noQuery" = "❌ 未找到查询!请再次使用命令!"
"wentWrong" = "❌ 出了点问题!"
"noIpRecord" = "❗ 没有 IP 记录!"
"noInbounds" = "❗ 没有找到入站连接"
"unlimited" = "♾ 无限"
"noIpRecord" = "❗ 没有IP记录"
"noInbounds" = "❗ 找到入站!"
"unlimited" = "♾ 无限(重置)"
"add" = "添加"
"month" = "月"
"months" = "月"
"day" = "天"
"days" = "天"
"hours" = "小时"
"minutes" = "分钟"
"unknown" = "未知"
"inbounds" = "入站连接"
"inbounds" = "入站"
"clients" = "客户端"
"offline" = "🔴 离线"
"online" = "🟢 在线"
@@ -646,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 刷新时间:{{ .Time }}\r\n\r\n"
"yes" = "✅ 是的"
"no" = "❌ 没有"
"received_id" = "🔑📥 ID 已更新。"
"received_password" = "🔑📥 密码已更新。"
"received_email" = "📧📥 邮箱已更新。"
@@ -666,7 +672,6 @@
"FailedResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ❌ 失败 \n\n🛠 错误: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 所有客户的流量重置已完成。"
[tgbot.buttons]
"closeKeyboard" = "❌ 关闭键盘"
"cancel" = "❌ 取消"
@@ -700,7 +705,6 @@
"limitTraffic" = "🚧 流量限制"
"getBanLogs" = "禁止日志"
"allClients" = "所有客户"
"addClient" = "添加客户"
"submitDisable" = "提交为禁用 ☑️"
"submitEnable" = "提交为启用 ✅"
@@ -712,7 +716,6 @@
"ResetAllTraffics" = "重置所有流量"
"SortedTrafficUsageReport" = "排序的流量使用报告"
[tgbot.answers]
"successfulOperation" = "✅ 成功!"
"errorOperation" = "❗ 操作错误。"

View File

@@ -50,6 +50,7 @@
"fail" = "失敗"
"comment" = "評論"
"success" = "成功"
"lastOnline" = "上次上線"
"getVersion" = "獲取版本"
"install" = "安裝"
"clients" = "客戶端"
@@ -117,8 +118,6 @@
"operationHours" = "系統正常執行時間"
"systemLoad" = "系統負載"
"systemLoadDesc" = "過去 1、5 和 15 分鐘的系統平均負載"
"connectionTcpCountDesc" = "系統中所有 TCP 連線數"
"connectionUdpCountDesc" = "系統中所有 UDP 連線數"
"connectionCount" = "連線數"
"ipAddresses" = "IP地址"
"toggleIpVisibility" = "切換IP可見性"
@@ -134,6 +133,8 @@
"xraySwitchVersionPopover" = "Xray 更新成功"
"geofileUpdateDialog" = "您確定要更新地理檔案嗎?"
"geofileUpdateDialogDesc" = "這將更新 #filename# 檔案。"
"geofilesUpdateDialogDesc" = "這將更新所有文件。"
"geofilesUpdateAll" = "全部更新"
"geofileUpdatePopover" = "地理檔案更新成功"
"dontRefresh" = "安裝中,請勿重新整理此頁面"
"logs" = "日誌"
@@ -151,6 +152,8 @@
"getConfigError" = "檢索設定檔時發生錯誤"
[pages.inbounds]
"allTimeTraffic" = "累計總流量"
"allTimeTrafficUsage" = "所有时间总使用量"
"title" = "入站列表"
"totalDownUp" = "總上傳 / 下載"
"totalUsage" = "總用量"
@@ -160,25 +163,24 @@
"remark" = "備註"
"protocol" = "協議"
"port" = "埠"
"portMap" = "埠映射"
"traffic" = "流量"
"details" = "詳細資訊"
"transportConfig" = "傳輸配置"
"expireDate" = "到期時間"
"createdAt" = "建立時間"
"updatedAt" = "更新時間"
"resetTraffic" = "重置流量"
"addInbound" = "新增入站"
"generalActions" = "通用操作"
"autoRefresh" = "自動刷新"
"autoRefreshInterval" = "間隔"
"create" = "新增"
"update" = "修改"
"modifyInbound" = "修改入站"
"deleteInbound" = "刪除入站"
"deleteInboundContent" = "確定要刪除入站嗎?"
"deleteClient" = "刪除客戶端"
"deleteClientContent" = "確定要刪除客戶端嗎?"
"resetTrafficContent" = "確定要重置流量嗎?"
"inboundUpdateSuccess" = "入站連接已成功更新。"
"inboundCreateSuccess" = "入站連接已成功建立。"
"copyLink" = "複製連結"
"address" = "地址"
"network" = "網路"
@@ -264,6 +266,8 @@
"resetInboundClientTrafficSuccess" = "流量已重置"
"trafficGetError" = "取得流量資料時發生錯誤"
"getNewX25519CertError" = "取得X25519憑證時發生錯誤。"
"getNewmldsa65Error" = "取得mldsa65憑證時發生錯誤。"
"getNewVlessEncError" = "取得VlessEnc憑證時發生錯誤。"
[pages.inbounds.stream.general]
"request" = "請求"
@@ -565,22 +569,23 @@
[tgbot]
"keyboardClosed" = "❌ 自定義鍵盤已關閉!"
"noResult" = "❗ 沒有結果!"
"noQuery" = "❌ 未找到查詢!請重新使用命令!"
"noQuery" = "❌ 未找到查詢!請再次使用命令!"
"wentWrong" = "❌ 出了點問題!"
"noIpRecord" = "❗ 沒有 IP 記錄!"
"noInbounds" = "❗ 沒有找到入站連線"
"unlimited" = "♾ 無限"
"add" = "新增"
"noIpRecord" = "❗ 沒有IP記錄"
"noInbounds" = "❗ 找到入站!"
"unlimited" = "♾ 無限(重置)"
"add" = "添加"
"month" = "月"
"months" = "月"
"day" = "天"
"days" = "天"
"hours" = "小時"
"minutes" = "分鐘"
"unknown" = "未知"
"inbounds" = "入站連線"
"inbounds" = "入站"
"clients" = "客戶端"
"offline" = "🔴 離線"
"online" = "🟢 線"
"online" = "🟢 線"
[tgbot.commands]
"unknown" = "❗ 未知命令"
@@ -648,7 +653,6 @@
"refreshedOn" = "\r\n📋🔄 重新整理時間:{{ .Time }}\r\n\r\n"
"yes" = "✅ 是的"
"no" = "❌ 沒有"
"received_id" = "🔑📥 ID 已更新。"
"received_password" = "🔑📥 密碼已更新。"
"received_email" = "📧📥 電子郵件已更新。"
@@ -668,7 +672,6 @@
"FailedResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠 錯誤: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 所有客戶的流量重置已完成。"
[tgbot.buttons]
"closeKeyboard" = "❌ 關閉鍵盤"
"cancel" = "❌ 取消"
@@ -702,7 +705,6 @@
"limitTraffic" = "🚧 流量限制"
"getBanLogs" = "禁止日誌"
"allClients" = "所有客戶"
"addClient" = "新增客戶"
"submitDisable" = "以停用方式送出 ☑️"
"submitEnable" = "以啟用方式送出 ✅"
@@ -714,7 +716,6 @@
"ResetAllTraffics" = "重設所有流量"
"SortedTrafficUsageReport" = "排序過的流量使用報告"
[tgbot.answers]
"successfulOperation" = "✅ 成功!"
"errorOperation" = "❗ 操作錯誤。"

View File

@@ -12,4 +12,4 @@ Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target

251
x-ui.sh
View File

@@ -214,7 +214,7 @@ reset_webbasepath() {
return
fi
config_webBasePath=$(gen_random_string 10)
config_webBasePath=$(gen_random_string 18)
# Apply the new web base path setting
/usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}" >/dev/null 2>&1
@@ -398,37 +398,6 @@ show_log() {
esac
}
show_banlog() {
local system_log="/var/log/fail2ban.log"
echo -e "${green}Checking ban logs...${plain}\n"
if ! systemctl is-active --quiet fail2ban; then
echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
fi
if [[ -f "$system_log" ]]; then
echo -e "${green}Recent system ban activities from fail2ban.log:${plain}"
grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}"
echo ""
fi
if [[ -f "${iplimit_banned_log_path}" ]]; then
echo -e "${green}3X-IPL ban log entries:${plain}"
if [[ -s "${iplimit_banned_log_path}" ]]; then
grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}"
else
echo -e "${yellow}Ban log file is empty${plain}"
fi
else
echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}"
fi
echo -e "\n${green}Current jail status:${plain}"
fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}"
}
bbr_menu() {
echo -e "${green}\t1.${plain} Enable BBR"
echo -e "${green}\t2.${plain} Disable BBR"
@@ -1005,7 +974,7 @@ ssl_cert_issue() {
# install socat second
case "${release}" in
ubuntu | debian | armbian)
apt update && apt install socat -y
apt-get update && apt-get install socat -y
;;
centos | rhel | almalinux | rocky | ol)
yum -y update && yum -y install socat
@@ -1330,81 +1299,7 @@ run_speedtest() {
speedtest
}
create_iplimit_jails() {
# Use default bantime if not passed => 30 minutes
local bantime="${1:-30}"
# Uncomment 'allowipv6 = auto' in fail2ban.conf
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
# On Debian 12+ fail2ban's default backend should be changed to systemd
if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then
sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf
fi
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
[3x-ipl]
enabled=true
backend=auto
filter=3x-ipl
action=3x-ipl
logpath=${iplimit_log_path}
maxretry=2
findtime=32
bantime=${bantime}m
EOF
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
[Definition]
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
ignoreregex =
EOF
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
before = iptables-allports.conf
[Definition]
actionstart = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -j f2b-<name>
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
[Init]
name = default
protocol = tcp
chain = INPUT
EOF
echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}"
}
iplimit_remove_conflicts() {
local jail_files=(
/etc/fail2ban/jail.conf
/etc/fail2ban/jail.local
)
for file in "${jail_files[@]}"; do
# Check for [3x-ipl] config in jail file then remove it
if test -f "${file}" && grep -qw '3x-ipl' ${file}; then
sed -i "/\[3x-ipl\]/,/^$/d" ${file}
echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n"
fi
done
}
ip_validation() {
ipv6_regex="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$"
@@ -1514,14 +1409,22 @@ install_iplimit() {
# Check the OS and install necessary packages
case "${release}" in
ubuntu)
apt-get update
if [[ "${os_version}" -ge 24 ]]; then
apt update && apt install python3-pip -y
apt-get install python3-pip -y
python3 -m pip install pyasynchat --break-system-packages
fi
apt update && apt install fail2ban -y
apt-get install fail2ban -y
;;
debian | armbian)
apt update && apt install fail2ban -y
debian)
apt-get update
if [ "$os_version" -ge 12 ]; then
apt-get install -y python3-systemd
fi
apt-get install -y fail2ban
;;
armbian)
apt-get update && apt-get install fail2ban -y
;;
centos | rhel | almalinux | rocky | ol)
yum update -y && yum install epel-release -y
@@ -1632,11 +1535,129 @@ remove_iplimit() {
esac
}
SSH_port_forwarding() {
local server_ip=$(curl -s --max-time 3 https://api.ipify.org)
if [ -z "$server_ip" ]; then
server_ip=$(curl -s --max-time 3 https://4.ident.me)
show_banlog() {
local system_log="/var/log/fail2ban.log"
echo -e "${green}Checking ban logs...${plain}\n"
if ! systemctl is-active --quiet fail2ban; then
echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
fi
if [[ -f "$system_log" ]]; then
echo -e "${green}Recent system ban activities from fail2ban.log:${plain}"
grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}"
echo ""
fi
if [[ -f "${iplimit_banned_log_path}" ]]; then
echo -e "${green}3X-IPL ban log entries:${plain}"
if [[ -s "${iplimit_banned_log_path}" ]]; then
grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}"
else
echo -e "${yellow}Ban log file is empty${plain}"
fi
else
echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}"
fi
echo -e "\n${green}Current jail status:${plain}"
fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}"
}
create_iplimit_jails() {
# Use default bantime if not passed => 30 minutes
local bantime="${1:-30}"
# Uncomment 'allowipv6 = auto' in fail2ban.conf
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
# On Debian 12+ fail2ban's default backend should be changed to systemd
if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then
sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf
fi
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
[3x-ipl]
enabled=true
backend=auto
filter=3x-ipl
action=3x-ipl
logpath=${iplimit_log_path}
maxretry=2
findtime=32
bantime=${bantime}m
EOF
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
[Definition]
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
ignoreregex =
EOF
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
before = iptables-allports.conf
[Definition]
actionstart = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -j f2b-<name>
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
[Init]
name = default
protocol = tcp
chain = INPUT
EOF
echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}"
}
iplimit_remove_conflicts() {
local jail_files=(
/etc/fail2ban/jail.conf
/etc/fail2ban/jail.local
)
for file in "${jail_files[@]}"; do
# Check for [3x-ipl] config in jail file then remove it
if test -f "${file}" && grep -qw '3x-ipl' ${file}; then
sed -i "/\[3x-ipl\]/,/^$/d" ${file}
echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n"
fi
done
}
SSH_port_forwarding() {
local URL_lists=(
"https://api4.ipify.org"
"https://ipv4.icanhazip.com"
"https://v4.api.ipinfo.io/ip"
"https://ipv4.myexternalip.com/raw"
"https://4.ident.me"
"https://check-host.net/ip"
)
local server_ip=""
for ip_address in "${URL_lists[@]}"; do
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
if [[ -n "${server_ip}" ]]; then
break
fi
done
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local existing_listenIP=$(/usr/local/x-ui/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')

View File

@@ -7,7 +7,9 @@ type ClientTraffic struct {
Email string `json:"email" form:"email" gorm:"unique"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
AllTime int64 `json:"allTime" form:"allTime"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
Total int64 `json:"total" form:"total"`
Reset int `json:"reset" form:"reset" gorm:"default:0"`
LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"`
}

View File

@@ -14,7 +14,6 @@ type InboundConfig struct {
StreamSettings json_util.RawMessage `json:"streamSettings"`
Tag string `json:"tag"`
Sniffing json_util.RawMessage `json:"sniffing"`
Allocate json_util.RawMessage `json:"allocate"`
}
func (c *InboundConfig) Equals(other *InboundConfig) bool {
@@ -39,8 +38,5 @@ func (c *InboundConfig) Equals(other *InboundConfig) bool {
if !bytes.Equal(c.Sniffing, other.Sniffing) {
return false
}
if !bytes.Equal(c.Allocate, other.Allocate) {
return false
}
return true
}

View File

@@ -25,7 +25,7 @@ func (lw *LogWriter) Write(m []byte) (n int, err error) {
if crashRegex.MatchString(message) {
logger.Debug("Core crash detected:\n", message)
lw.lastLine = message
err1 := writeCrachReport(m)
err1 := writeCrashReport(m)
if err1 != nil {
logger.Error("Unable to write crash report:", err1)
}

View File

@@ -239,10 +239,15 @@ func (p *process) Stop() error {
if !p.IsRunning() {
return errors.New("xray is not running")
}
return p.cmd.Process.Signal(syscall.SIGTERM)
if runtime.GOOS == "windows" {
return p.cmd.Process.Kill()
} else {
return p.cmd.Process.Signal(syscall.SIGTERM)
}
}
func writeCrachReport(m []byte) error {
func writeCrashReport(m []byte) error {
crashReportPath := config.GetBinFolderPath() + "/core_crash_" + time.Now().Format("20060102_150405") + ".log"
return os.WriteFile(crashReportPath, m, os.ModePerm)
}