Compare commits

..

107 Commits

Author SHA1 Message Date
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
mhsanaei
b7164805f8 v2.6.2 2025-07-06 20:58:10 +02:00
mhsanaei
bbdeb65291 new alternative to get public IP address 2025-07-06 20:45:58 +02:00
Shishkevich D.
038cf34219 chore: return automatic generation of shadowsocks keys 2025-07-06 15:20:41 +07:00
Shishkevich D.
98a1517470 fix: login title shifts the input fields
* chore: revert "fix: reduced login title font-size for mobile (#3105)"

* chore: short login title translation for russian

* chore: change login title translation for ukrainian
2025-07-06 13:22:09 +07:00
mhsanaei
ce76cedb0d fixed: mux #3185 2025-07-04 14:02:33 +02:00
mhsanaei
24a313d605 fixed: type #3186 2025-07-04 13:25:24 +02:00
mhsanaei
c81c27073c v2.6.1 2025-07-02 11:28:24 +02:00
Shishkevich D.
5d11e6e13f chore: reset two-factor authentication after changing admin credentials (#3029)
* chore: add `resetTwoFactor` argument for main.go

fixes #3025

* chore: reset two-factor authentication after changing admin credentials

* chore: reset two-factor authentication after changing admin credentials

---------

Co-authored-by: somebodywashere <68244480+somebodywashere@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2025-07-02 11:25:25 +02:00
mhsanaei
f3d0b92e4a update dependencies 2025-07-02 11:17:26 +02:00
Shishkevich D.
c8c0e77714 chore: mark 2053 port as unsecured 2025-06-25 00:38:03 +07:00
Shishkevich D.
49b8f46864 fix: selecting a supported language
english could be selected by default at first load, even if the user's language was supported by the panel
2025-06-25 00:30:08 +07:00
Shishkevich D.
cad07be847 chore: russian language improvements 2025-06-23 19:42:44 +07:00
Shishkevich D.
4b20f16024 refactor: new loading logic, icons for tabs 2025-06-22 00:27:09 +07:00
Shishkevich D.
d642774a44 refactor: use new page templates 2025-06-21 15:38:43 +07:00
Shishkevich D.
1644904755 chore: clients comment improvement
- use default ant components instead custom styles
- remove comments in inbound info modal (duplicates information)
- enhance tooltip usage
2025-06-21 15:24:52 +07:00
Vadim Iskuchekov
5c10035bd9 feat: add comments under client id (#3131) 2025-06-21 14:55:35 +07:00
Shishkevich D.
2e6faf69e6 fix: generate correct keys for shadowsocks inbounds 2025-06-20 19:30:46 +07:00
Sanaei
f88b7b07f0 Merge pull request #3125 from xujie86/patch-2
Add RHEL on x-ui.sh
2025-06-19 11:36:53 +03:30
xujie86
e5752239f4 Update x-ui.sh 2025-06-19 14:38:24 +08:00
Shishkevich D.
cb22b4ad47 chore: add new dns features from v25.6.8
* chore: add new dns params

* chore: add `DNS Presets` modal

* chore: edit file names
2025-06-18 23:24:18 +07:00
spatiumstas
6a2e0071cf fix: reduced login title font-size for mobile (#3105) 2025-06-18 22:31:10 +07:00
Shishkevich D.
f86219f4de refactor: use math.MaxUint16 when checking port 2025-06-17 22:45:03 +07:00
Shishkevich D.
e272c160b1 chore: add download config button for wireguard 2025-06-17 22:25:24 +07:00
xujie86
ba50c99c10 chore: add RHEL system to install.sh
fixed #3097
2025-06-16 18:11:23 +07:00
Shishkevich D.
00b61de646 chore: add translations for routing table 2025-06-16 12:26:49 +07:00
Shishkevich D.
dff4ad31ff chore: up minimal xray-core version to v25.6.8 2025-06-16 12:22:56 +07:00
Shishkevich D.
13baf77893 chore: build image in docker compose
this makes it possible to run development build using Docker.
2025-06-13 15:25:13 +07:00
Sanaei
4531574de3 Merge pull request #3087 from rammiah/main
feat: support metrics config
2025-06-11 17:13:46 +03:30
rammiah
d1e07954c5 feat: support metrics config 2025-06-11 18:42:41 +08:00
Jay
d9922d93af chore: update the installation command (#3069)
* install command had an extra $ at the first of the line

* install command had an extra $ at the first of the line

* install command had an extra $ at the first of the line

* install command had an extra $ at the first of the line

* install command had an extra $ at the first of the line

* install command had an extra $ at the first of the line
2025-06-10 19:17:31 +03:30
Shishkevich D.
c7d315f848 chore: clean readme, add link to wiki (#3045) 2025-06-05 18:56:33 +07:00
Shishkevich D.
1781790dce fix: don't show ip limit for some protocols (#3064)
this causes the modal window to fail because protocols like wireguard, dokodemo and so on do not have clients (like vless/vmess, shadowsocks, etc).
2025-06-05 18:12:06 +07:00
spatiumstas
29f950046a feat: add command list in telegram bot (#3027) 2025-05-28 15:26:29 +07:00
Shishkevich D.
5dae785786 chore: X_UI_ENABLE_FAIL2BAN -> XUI_ENABLE_FAIL2BAN (#3030) 2025-05-22 08:21:23 +02:00
Ali Golzar
1b1cbfff42 feat: support .env file (#3013) 2025-05-17 12:33:22 +02:00
Pk-web6936
c93467b852 Code refactoring (#3011)
* Code refactoring 

read without -r will mangle backslashes
https://github.com/koalaman/shellcheck/wiki/SC2162

* Update x-ui.sh
2025-05-16 20:23:57 +02:00
Shishkevich D.
c988d55256 fix: handle inbounds interaction errors (#3009)
eliminates messages like: “Inbound created successfully (Port 100 is already busy)”.
2025-05-16 23:56:56 +07:00
118 changed files with 5311 additions and 80803 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

@@ -8,6 +8,7 @@ on:
branches:
- main
paths:
- '.github/workflows/release.yml'
- '**.js'
- '**.css'
- '**.html'
@@ -31,10 +32,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 +43,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 +84,7 @@ jobs:
cd x-ui/bin
# Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.5.16/"
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.8.29/"
if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip

9
.gitignore vendored
View File

@@ -29,9 +29,12 @@ 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
docker-compose.override.yml
# Ignore .env (Environment Variables) file
.env

View File

@@ -1,7 +1,7 @@
#!/bin/sh
# Start fail2ban
[ $X_UI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start
[ $XUI_ENABLE_FAIL2BAN == "true" ] && fail2ban-client -x start
# Run x-ui
exec /app/x-ui

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.5.16/Xray-linux-${ARCH}.zip"
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.8.29/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
@@ -48,7 +48,7 @@ RUN chmod +x \
/app/x-ui \
/usr/bin/x-ui
ENV X_UI_ENABLE_FAIL2BAN="true"
ENV XUI_ENABLE_FAIL2BAN="true"
VOLUME [ "/etc/x-ui" ]
CMD [ "./x-ui" ]
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]

View File

@@ -7,17 +7,39 @@
</picture>
</p>
**لوحة تحكم ويب متقدمة • مبنية على Xray Core**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة.
> **تنبيه:** المشروع ده للتعلم الشخصي والتواصل فقط. رجاءً استخدمه بشكل قانوني.
> [!IMPORTANT]
> هذا المشروع مخصص للاستخدام الشخصي والاتصال فقط، يرجى عدم استخدامه لأغراض غير قانونية، يرجى عدم استخدامه في بيئة الإنتاج.
**لو المشروع عجبك وساعدك، ممكن تدعمنا بعمل** :star2:
كمشروع محسن من مشروع X-UI الأصلي، يوفر 3X-UI استقرارًا محسنًا ودعمًا أوسع للبروتوكولات وميزات إضافية.
## البدء السريع
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
للحصول على الوثائق الكاملة، يرجى زيارة [ويكي المشروع](https://github.com/MHSanaei/3x-ui/wiki).
## شكر خاص إلى
- [alireza0](https://github.com/alireza0/)
## الاعتراف
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (الترخيص: **GPL-3.0**): _قواعد توجيه v2ray/xray و v2ray/xray-clients المحسنة مع النطاقات الإيرانية المدمجة وتركيز على الأمان وحظر الإعلانات._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (الترخيص: **GPL-3.0**): _يحتوي هذا المستودع على قواعد توجيه V2Ray محدثة تلقائيًا بناءً على بيانات النطاقات والعناوين المحظورة في روسيا._
## دعم المشروع
**إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
@@ -26,544 +48,9 @@
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## التثبيت والتحديث
## النجوم عبر الزمن
لتثبيت المشروع أو تحديثه، نفذ الأمر ده:
```bash
bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.6.0/install.sh)
```
## تثبيت النسخة القديمة (مش موصى بيها)
لو عايز تثبت نسخة معينة، استخدم الأمر ده، مثلاً نسخة `v1.7.9`:
```bash
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
```
## شهادة SSL
<details>
<summary>اضغط لعرض تفاصيل شهادة SSL</summary>
### ACME
عشان تدير شهادات SSL باستخدام ACME:
1. تأكد إن الدومين بتاعك متربط صح بالسيرفر.
2. شغّل أمر `x-ui` في الترمينال واختار خيار "إدارة شهادات SSL".
3. هتلاقي الخيارات دي:
- **Get SSL:** الحصول على شهادة SSL.
- **Revoke:** إلغاء شهادة SSL موجودة.
- **Force Renew:** تجديد شهادة SSL بالقوة.
- **Show Existing Domains:** عرض كل شهادات الدومين المتوفرة على السيرفر.
- **Set Certificate Paths for the Panel:** تعيين مسارات الشهادة عشان البانل يستخدمها.
### Certbot
لتثبيت واستخدام Certbot:
```sh
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
### Cloudflare
السكريبت بتاع الإدارة فيه آلية مدمجة للتقديم على شهادة SSL من خلال Cloudflare. عشان تستخدمها، هتحتاج:
- بريد إلكتروني مسجل على Cloudflare.
- الـ Global API Key بتاع Cloudflare.
- الدومين لازم يكون مربوط للسيرفر الحالي عن طريق Cloudflare.
**كيفية الحصول على Global API Key من Cloudflare:**
1. شغّل أمر `x-ui` في الترمينال واختار "Cloudflare SSL Certificate".
2. ادخل على [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens).
3. دوس على "View Global API Key" (شوف الصورة التوضيحية):
![](media/APIKey1.PNG)
4. يمكن تحتاج تعيد تسجيل الدخول، وبعدها هتظهر الـ API Key (شوف الصورة التوضيحية):
![](media/APIKey2.png)
عند الاستخدام، ادخل اسم الدومين، البريد الإلكتروني، وAPI Key. المخطط كالتالي:
![](media/DetailEnter.png)
</details>
## التثبيت والتحديث يدويًا
<details>
<summary>اضغط لعرض تفاصيل التثبيت اليدوي</summary>
#### الاستخدام
1. لتحميل أحدث نسخة من الباكدج المظغوطة مباشرة على السيرفر، نفذ الأمر التالي:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. بعد تحميل الباكدج، نفذ الأوامر دي للتثبيت أو التحديث:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
## التثبيت باستخدام Docker
<details>
<summary>اضغط لعرض تفاصيل Docker</summary>
#### الاستخدام
1. **تثبيت Docker:**
```sh
bash <(curl -sSL https://get.docker.com)
```
2. **نسخ مستودع المشروع:**
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. **تشغيل الخدمة:**
```sh
docker compose up -d
```
ممكن تضيف الخيار ```--pull always``` عشان Docker يسحب أحدث صورة لو موجودة. (راجع [مستندات Docker](https://docs.docker.com/reference/cli/docker/container/run/#pull) للمزيد من التفاصيل).
**أو**
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
4. **التحديث إلى أحدث نسخة:**
```sh
cd 3x-ui
docker compose down
docker compose pull 3x-ui
docker compose up -d
```
5. **إزالة 3x-ui من Docker:**
```sh
docker stop 3x-ui
docker rm 3x-ui
cd --
rm -r 3x-ui
```
</details>
## إعدادات Nginx
<details>
<summary>اضغط لعرض إعدادات البروكسي العكسي (Reverse Proxy)</summary>
#### Reverse Proxy باستخدام Nginx
```nginx
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
#### استخدام Nginx مع sub-path
- تأكد إن "URI Path" في إعدادات `/sub` للبانل متطابق.
- لازم يكون رابط `url` في إعدادات البانل منتهي بـ `/`.
```nginx
location /sub {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
</details>
## نظام التشغيل الموصى به
- Ubuntu 22.04+
- Debian 12+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 9.5+
- Rocky Linux 9.5+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## المعماريات والأجهزة المدعومة
<details>
<summary>اضغط لعرض تفاصيل المعماريات والأجهزة المدعومة</summary>
منصتنا بتدعم مجموعة متنوعة من المعماريات والأجهزة عشان تناسب بيئات مختلفة. أبرز المعماريات هي:
- **amd64:** المعمارية القياسية للكمبيوترات الشخصية والسيرفرات.
- **x86 / i386:** مستخدمة على نطاق واسع في أجهزة الديسكتوب واللاب توب.
- **armv8 / arm64 / aarch64:** موجهة للأجهزة المحمولة والمضمنة زي Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS، وغيرها.
- **armv7 / arm / arm32:** للأجهزة المحمولة والأجهزة المضمنة القديمة، مثل Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2.
- **armv6 / arm / arm32:** للأجهزة المضمنة القديمة جداً، زي Raspberry Pi 1, Raspberry Pi Zero/Zero W.
- **armv5 / arm / arm32:** معمارية أقدم مرتبطة بالأنظمة المضمنة القديمة.
- **s390x:** مستخدمة في الحواسيب الرئيسية من IBM وتوفر أداء عالي واعتمادية للمشاريع الكبيرة.
</details>
## اللغات
- English (إنجليزي)
- Persian (فارسي)
- Traditional Chinese (الصيني التقليدي)
- Simplified Chinese (الصيني المبسط)
- Japanese (ياباني)
- Russian (روسي)
- Vietnamese (فيتنامي)
- Spanish (إسباني)
- Indonesian (إندونيسي)
- Ukrainian (أوكراني)
- Turkish (تركي)
- Português (Brazil) (برتغالي - البرازيل)
## الميزات
- مراقبة حالة النظام
- البحث داخل كل الإدخالات والعملاء
- ثيم داكن وفاتح
- دعم تعدد المستخدمين والبروتوكولات
- دعم بروتوكولات زي VMESS، VLESS، Trojan، Shadowsocks، Dokodemo-door، Socks، HTTP، wireguard
- دعم بروتوكولات XTLS الأصلية مثل RPRX-Direct، Vision، REALITY
- إحصائيات الترافيك، تحديد حد للترافيك، وتحديد فترة انتهاء الصلاحية
- قوالب إعدادات Xray قابلة للتخصيص
- دعم لوحة تحكم عبر HTTPS (بإستخدام الدومين الخاص وشهادة SSL)
- دعم التقديم على شهادات SSL بلمسة واحدة والتجديد التلقائي
- لمزيد من إعدادات التخصيص المتقدمة، راجع إعدادات البانل
- إصلاح مسارات الـ API (سيتم إنشاء إعدادات للمستخدم مع الـ API)
- دعم تغيير الإعدادات من خلال الخيارات المتوفرة في البانل
- دعم تصدير/استيراد قاعدة البيانات من خلال البانل
## إعدادات البانل الافتراضية
<details>
<summary>اضغط لعرض تفاصيل الإعدادات الافتراضية</summary>
### اسم المستخدم، الباسورد، البورت ومسار الويب الأساسي
لو مش هتعدل الإعدادات دي، هتتولد تلقائياً (ده مش بينطبق على Docker).
**الإعدادات الافتراضية لـ Docker:**
- **اسم المستخدم:** admin
- **الباسورد:** admin
- **البورت:** 2053
### إدارة قاعدة البيانات:
ممكن تعمل نسخ احتياطية واسترجاع لقاعدة البيانات مباشرة من البانل.
- **مسار قاعدة البيانات:**
- `/etc/x-ui/x-ui.db`
### المسار الأساسي للويب
1. **إعادة تعيين المسار الأساسي:**
- افتح الترمينال.
- نفذ أمر `x-ui`.
- اختار خيار "إعادة تعيين المسار الأساسي للويب".
2. **توليد أو تخصيص المسار:**
- المسار هيتولد تلقائياً، أو ممكن تدخل مسار مخصص.
3. **عرض الإعدادات الحالية:**
- لمشاهدة الإعدادات الحالية، نفذ أمر `x-ui settings` في الترمينال أو استخدم خيار "عرض الإعدادات الحالية" في البانل.
### توصية الأمان:
- لتحسين الأمان، استخدم كلمة طويلة وعشوائية في مسار URL الخاص بالبانل.
**مثال:**
- `http://ip:port/*webbasepath*/panel`
- `http://domain:port/*webbasepath*/panel`
</details>
## إعداد WARP
<details>
<summary>اضغط لعرض تفاصيل إعداد WARP</summary>
#### الاستخدام
**لإصدارات `v2.1.0` وما بعدها:**
WARP مدمج ومش محتاج تثبيت إضافي. فعل الإعدادات المطلوبة من خلال البانل.
</details>
## تحديد IP
<details>
<summary>اضغط لعرض تفاصيل تحديد IP</summary>
#### الاستخدام
**ملحوظة:** تحديد IP ممكن مايشتغلش صح مع IP Tunnel.
- **للإصدارات حتى `v1.6.1`:**
- تحديد IP مدمج في البانل.
**للإصدارات `v1.7.0` وما بعدها:**
لتفعيل وظيفة تحديد IP، هتحتاج تثبيت `fail2ban` والملفات المطلوبة من خلال الخطوات دي:
1. شغل أمر `x-ui` في الترمينال واختار "إدارة تحديد IP".
2. هتلاقي الخيارات التالية:
- **تغيير مدة الحظر:** لتعديل مدة الحظر.
- **رفع الحظر عن الجميع:** لإلغاء كل الحظر الحالي.
- **عرض السجلات:** لمراجعة السجلات.
- **حالة Fail2ban:** لمراجعة حالة fail2ban.
- **إعادة تشغيل Fail2ban:** لإعادة تشغيل خدمة fail2ban.
- **إلغاء تثبيت Fail2ban:** لإلغاء تثبيت fail2ban مع إعداداته.
3. حدد مسار سجل الوصول على البانل من خلال ضبط `Xray Configs/log/Access log` على `./access.log` ثم احفظ وأعد تشغيل x-ui.
- **للإصدارات قبل `v2.1.3`:**
- هتحتاج تضبط مسار سجل الوصول يدويًا في إعدادات Xray:
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
- **للإصدارات `v2.1.3` وما بعدها:**
- في خيار لضبط `access.log` مباشرة من البانل.
</details>
## بوت Telegram
<details>
<summary>اضغط لعرض تفاصيل بوت Telegram</summary>
#### الاستخدام
تدعم لوحة التحكم إشعارات بترافيك يومي، تسجيل الدخول للبانل، نسخ احتياطية للقاعدة، حالة النظام، معلومات العملاء، وغيرها من الوظائف عن طريق بوت Telegram. عشان تستخدم البوت، لازم تضبط معلمات البوت في البانل، ومن ضمنهم:
- توكن Telegram
- ID شات الأدمن (يمكن إدخال أكثر من واحد بفواصل)
- وقت الإشعار (باستخدام صيغة cron)
- إشعار بتاريخ انتهاء الصلاحية
- إشعار حد الترافيك
- نسخ احتياطية للقاعدة
- إشعار حمل المعالج
**صيغة مرجعية:**
- `30 * * * * *` - إشعار عند الـ 30 ثانية من كل دقيقة.
- `0 */10 * * * *` - إشعار عند أول ثانية من كل 10 دقايق.
- `@hourly` - إشعار كل ساعة.
- `@daily` - إشعار يومي (في تمام منتصف الليل).
- `@weekly` - إشعار أسبوعي.
- `@every 8h` - إشعار كل 8 ساعات.
### ميزات بوت Telegram
- تقارير دورية.
- إشعارات عند تسجيل الدخول.
- إشعار عند تجاوز حمل المعالج.
- تنبيهات قبل انتهاء الصلاحية أو وصول حد الترافيك.
- دعم قوائم تقارير العملاء لو تم إضافة اسم مستخدم Telegram للمستخدم في الإعدادات.
- إمكانية البحث عن تقرير الترافيك باستخدام UUID (VMESS/VLESS) أو الباسورد (TROJAN) بشكل مجهول.
- بوت يعتمد على القوائم.
- البحث عن العملاء بالإيميل (للأدمن فقط).
- استعراض كافة الإدخالات.
- عرض حالة السيرفر.
- استعراض العملاء المستنفدين.
- استقبال النسخ الاحتياطية عند الطلب وفي التقارير الدورية.
- بوت متعدد اللغات.
### إعداد بوت Telegram
- ابدأ [Botfather](https://t.me/BotFather) في حساب Telegram بتاعك:
![Botfather](./media/botfather.png)
- أنشئ بوت جديد باستخدام أمر /newbot: هيسألك سؤالين، اسم للبوت واسم مستخدم (لازم ينتهي بكلمة "bot").
![Create new bot](./media/newbot.png)
- شغل البوت اللي أنشأته. هتلاقي رابط البوت بعد كده.
![token](./media/token.png)
- ادخل على البانل واضبط إعدادات بوت Telegram زي ما هو موضح:
![Panel Config](./media/panel-bot-config.png)
ادخل توكن البوت في الحقل رقم 3.
ادخل ID المستخدم في الحقل رقم 4. الحسابات اللي بالـ ID ده هيبقى ليها صلاحية الأدمن. (يمكن إدخال أكثر من واحد بفواصل)
- كيفية الحصول على ID حساب Telegram؟ استخدم [هذا البوت](https://t.me/useridinfobot). شغله وهيدي الـ ID بتاعك.
![User ID](./media/user-id.png)
</details>
## مسارات API
<details>
<summary>اضغط لعرض تفاصيل مسارات API</summary>
#### الاستخدام
- [توثيق الـ API](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` مع بيانات المستخدم باستخدام `POST`: `{username: '', password: ''}` لتسجيل الدخول.
- المسار الأساسي لـ `/panel/api/inbounds` للعمليات التالية:
| الطريقة | المسار | الفعل |
| :-----: | ------------------------------------ | ------------------------------------------ |
| `GET` | `"/list"` | استرجاع كل الإدخالات |
| `GET` | `"/get/:id"` | استرجاع إدخال بالـ id |
| `GET` | `"/getClientTraffics/:email"` | استرجاع ترافيك عميل بالإيميل |
| `GET` | `"/getClientTrafficsById/:id"` | استرجاع ترافيك عميل بالـ id |
| `GET` | `"/createbackup"` | البوت بيرسل نسخة احتياطية للأدمن |
| `POST` | `"/add"` | إضافة إدخال |
| `POST` | `"/del/:id"` | حذف إدخال |
| `POST` | `"/update/:id"` | تحديث إدخال |
| `POST` | `"/clientIps/:email"` | استرجاع عنوان IP للعميل |
| `POST` | `"/clearClientIps/:email"` | مسح عنوان IP للعميل |
| `POST` | `"/addClient"` | إضافة عميل للإدخال |
| `POST` | `"/:id/delClient/:clientId"` | حذف عميل باستخدام clientId\* |
| `POST` | `"/updateClient/:clientId"` | تحديث بيانات عميل باستخدام clientId\* |
| `POST` | `"/:id/resetClientTraffic/:email"` | إعادة ضبط ترافيك عميل |
| `POST` | `"/resetAllTraffics"` | إعادة ضبط الترافيك لكل الإدخالات |
| `POST` | `"/resetAllClientTraffics/:id"` | إعادة ضبط ترافيك كل العملاء في إدخال معين |
| `POST` | `"/delDepletedClients/:id"` | حذف العملاء المستنفدين في الإدخال (-1: الكل) |
| `POST` | `"/onlines"` | استرجاع قائمة العملاء الأونلاين (الإيميلات) |
\*- بالنسبة لحقل `clientId`:
- استخدم `client.id` لـ VMESS و VLESS.
- استخدم `client.password` لـ TROJAN.
- استخدم `client.email` لـ Shadowsocks.
- [![Run In Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
</details>
## المتغيرات البيئية
<details>
<summary>اضغط لعرض تفاصيل المتغيرات البيئية</summary>
#### الاستخدام
| المتغير | النوع | القيمة الافتراضية |
| ---------------- | :--------------------------------------------: | ------------------: |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
مثال:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
## المعاينة
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
<img alt="3x-ui" src="./media/04-add-client-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
<img alt="3x-ui" src="./media/05-settings-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
<img alt="3x-ui" src="./media/06-configs-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture>
## شكر خاص لـ
- [alireza0](https://github.com/alireza0/)
## التحيات
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (الرخصة: **GPL-3.0**): _قواعد محدثة لتوجيهات v2ray/xray مع تضمين دومينات إيرانية وتركيز على الأمان وحجب الإعلانات._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (الرخصة: **GPL-3.0**): _المستودع ده بيحتوي على قواعد توجيه v2ray/xray محدثة تلقائيًا بناءً على بيانات الدومينات والعناوين المحظورة في روسيا._
## عدد النجوم مع مرور الوقت
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -7,17 +7,39 @@
</picture>
</p>
**Un Panel Web Avanzado • Construido sobre Xray Core**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
**3X-UI** — panel de control avanzado basado en web de código abierto diseñado para gestionar el servidor Xray-core. Ofrece una interfaz fácil de usar para configurar y monitorear varios protocolos VPN y proxy.
> **Descargo de responsabilidad:** Este proyecto es solo para aprendizaje personal y comunicación, por favor no lo uses con fines ilegales, por favor no lo uses en un entorno de producción
> [!IMPORTANT]
> Este proyecto es solo para uso personal y comunicación, por favor no lo use para fines ilegales, por favor no lo use en un entorno de producción.
**Si este proyecto te es útil, podrías considerar darle una**:star2:
Como una versión mejorada del proyecto X-UI original, 3X-UI proporciona mayor estabilidad, soporte más amplio de protocolos y características adicionales.
## Inicio Rápido
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
Para documentación completa, visita la [Wiki del proyecto](https://github.com/MHSanaei/3x-ui/wiki).
## Un Agradecimiento Especial a
- [alireza0](https://github.com/alireza0/)
## Reconocimientos
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Licencia: **GPL-3.0**): _Reglas de enrutamiento mejoradas para v2ray/xray y v2ray/xray-clients con dominios iraníes incorporados y un enfoque en seguridad y bloqueo de anuncios._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (Licencia: **GPL-3.0**): _Este repositorio contiene reglas de enrutamiento V2Ray actualizadas automáticamente basadas en datos de dominios y direcciones bloqueadas en Rusia._
## Apoyar el Proyecto
**Si este proyecto te es útil, puedes darle una**:star2:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
@@ -26,564 +48,9 @@
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Instalar y Actualizar
## Estrellas a lo Largo del Tiempo
```
bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.6.0/install.sh)
```
## Instalar versión antigua (no recomendamos)
Para instalar la versión deseada, utiliza el siguiente comando de instalación. Por ejemplo, ver `v1.7.9`:
```
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
```
## Certificado SSL
<details>
<summary>Haga clic para ver los detalles del certificado SSL</summary>
### ACME
Para gestionar certificados SSL utilizando ACME:
1. Asegúrate de que tu dominio esté correctamente resuelto al servidor.
2. Ejecuta el comando `x-ui` en la terminal y elige `Gestión de Certificados SSL`.
3. Se te presentarán las siguientes opciones:
- **Get SSL:** Obtener certificados SSL.
- **Revoke:** Revocar certificados SSL existentes.
- **Force Renew:** Forzar la renovación de certificados SSL.
- **Show Existing Domains:** Mostrar todos los certificados de dominio disponibles en el servidor.
- **Set Certificate Paths for the Panel:** Especificar el certificado para tu dominio que será utilizado por el panel.
### Certbot
Para instalar y usar Certbot:
```sh
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
### Cloudflare
El script de gestión incluye una aplicación de certificado SSL integrada para Cloudflare. Para usar este script para solicitar un certificado, necesitas lo siguiente:
- Correo electrónico registrado en Cloudflare
- Clave API Global de Cloudflare
- El nombre de dominio debe estar resuelto al servidor actual a través de Cloudflare
**Cómo obtener la Clave API Global de Cloudflare:**
1. Ejecuta el comando `x-ui` en la terminal y elige `Certificado SSL de Cloudflare`.
2. Visita el enlace: [Tokens de API de Cloudflare](https://dash.cloudflare.com/profile/api-tokens).
3. Haz clic en "Ver Clave API Global" (consulta la captura de pantalla a continuación):
![](media/APIKey1.PNG)
4. Es posible que necesites volver a autenticar tu cuenta. Después de eso, se mostrará la Clave API (consulta la captura de pantalla a continuación):
![](media/APIKey2.png)
Al utilizarlo, simplemente ingresa tu `nombre de dominio`, `correo electrónico` y `CLAVE API`. El diagrama es el siguiente:
![](media/DetailEnter.png)
</details>
## Instalación y Actualización Manual
<details>
<summary>Haz clic para más detalles de la instalación manual</summary>
#### Uso
1. Para descargar la última versión del paquete comprimido directamente en tu servidor, ejecuta el siguiente comando:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
*) XUI_ARCH="amd64" ;;
esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. Una vez que se haya descargado el paquete comprimido, ejecuta los siguientes comandos para instalar o actualizar x-ui:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
*) XUI_ARCH="amd64" ;;
esac
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
## Instalar con Docker
<details>
<summary>Haz clic para más detalles del Docker</summary>
#### Uso
1. Instala Docker:
```sh
bash <(curl -sSL https://get.docker.com)
```
2. Clona el Repositorio del Proyecto:
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. Inicia el Servicio
```sh
docker compose up -d
```
O tambien
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
actualizar a la última versión
```sh
cd 3x-ui
docker compose down
docker compose pull 3x-ui
docker compose up -d
```
eliminar 3x-ui de docker
```sh
docker stop 3x-ui
docker rm 3x-ui
cd --
rm -r 3x-ui
```
</details>
## Configuración de Nginx
<details>
<summary>Haga clic aquí para configurar el proxy inverso</summary>
#### Proxy inverso Nginx
```nginx
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
#### Nginx sub-path
- EAsegúrese de que la "Ruta Raíz de la URL del Panel" en la configuración del panel `/sub` es la misma.
- El `url` en la configuración del panel debe terminar con `/`.
```nginx
location /sub {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
</details>
## SO Recomendados
- Ubuntu 22.04+
- Debian 12+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 9.5+
- Rocky Linux 9.5+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## Arquitecturas y Dispositivos Compatibles
<details>
<summary>Haz clic para detalles de arquitecturas y dispositivos compatibles</summary>
Nuestra plataforma ofrece compatibilidad con una amplia gama de arquitecturas y dispositivos, garantizando flexibilidad en diversos entornos informáticos. A continuación se presentan las principales arquitecturas que admitimos:
- **amd64**: Esta arquitectura predominante es la estándar para computadoras personales y servidores, y admite la mayoría de los sistemas operativos modernos sin problemas.
- **x86 / i386**: Ampliamente adoptada en computadoras de escritorio y portátiles, esta arquitectura cuenta con un amplio soporte de numerosos sistemas operativos y aplicaciones, incluidos, entre otros, Windows, macOS y sistemas Linux.
- **armv8 / arm64 / aarch64**: Diseñada para dispositivos móviles y embebidos contemporáneos, como teléfonos inteligentes y tabletas, esta arquitectura está ejemplificada por dispositivos como Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, entre otros.
- **armv7 / arm / arm32**: Sirve como arquitectura para dispositivos móviles y embebidos más antiguos, y sigue siendo ampliamente utilizada en dispositivos como Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, entre otros.
- **armv6 / arm / arm32**: Orientada a dispositivos embebidos muy antiguos, esta arquitectura, aunque menos común, todavía se utiliza. Dispositivos como Raspberry Pi 1, Raspberry Pi Zero/Zero W, dependen de esta arquitectura.
- **armv5 / arm / arm32**: Una arquitectura más antigua asociada principalmente con sistemas embebidos tempranos, es menos común hoy en día pero aún puede encontrarse en dispositivos heredados como versiones antiguas de Raspberry Pi y algunos teléfonos inteligentes más antiguos.
</details>
## Idiomas
- Arabic (Árabe)
- English (inglés)
- Persian (persa)
- Traditional Chinese (chino tradicional)
- Simplified Chinese (chino simplificado)
- Japanese (japonés)
- Russian (ruso)
- Vietnamese (vietnamita)
- Spanish (español)
- Indonesian (indonesio)
- Ukrainian (ucraniano)
- Turkish (turco)
- Português (Brazil) (portugués (Brasil))
## Características
- Monitoreo del Estado del Sistema
- Búsqueda dentro de todas las reglas de entrada y clientes
- Tema Oscuro/Claro
- Soporta multiusuario y multiprotocolo
- Soporta protocolos, incluyendo VMess, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, wireguard
- Soporta Protocolos nativos XTLS, incluyendo RPRX-Direct, Visión, REALITY
- Estadísticas de tráfico, límite de tráfico, límite de tiempo de vencimiento
- Plantillas de configuración de Xray personalizables
- Soporta acceso HTTPS al panel (dominio proporcionado por uno mismo + certificado SSL)
- Soporta la solicitud y renovación automática de certificados SSL con un clic
- Para elementos de configuración más avanzados, consulta el panel
- Corrige rutas de API (la configuración del usuario se creará con la API)
- Soporta cambiar las configuraciones por diferentes elementos proporcionados en el panel.
- Soporta exportar/importar base de datos desde el panel
## Configuración Predeterminada del Panel
<details>
<summary>Haz clic para ver los detalles de la configuración predeterminada</summary>
### Nombre de usuario, Contraseña, Puerto y Ruta Base Web
Si elige no modificar estas configuraciones, se generarán aleatoriamente (esto no se aplica a Docker).
**Configuraciones predeterminadas para Docker:**
- **Nombre de usuario:** admin
- **Contraseña:** admin
- **Puerto:** 2053
### Gestión de la Base de Datos:
Puedes realizar copias de seguridad y restauraciones de la base de datos directamente desde el panel.
- **Ruta de la Base de Datos:**
- `/etc/x-ui/x-ui.db`
### Ruta Base Web
1. **Restablecer la Ruta Base Web:**
- Abre tu terminal.
- Ejecuta el comando `x-ui`.
- Selecciona la opción `Restablecer la Ruta Base Web`.
2. **Generar o Personalizar la Ruta:**
- La ruta se generará aleatoriamente, o puedes ingresar una ruta personalizada.
3. **Ver Configuración Actual:**
- Para ver tu configuración actual, utiliza el comando `x-ui settings` en el terminal o selecciona `Ver Configuración Actual` en `x-ui`.
### Recomendación de Seguridad:
- Para mayor seguridad, utiliza una palabra larga y aleatoria en la estructura de tu URL.
**Ejemplos:**
- `http://ip:port/*webbasepath*/panel`
- `http://domain:port/*webbasepath*/panel`
</details>
## Configuración de WARP
<details>
<summary>Haz clic para ver los detalles de la configuración de WARP</summary>
#### Uso
**Para versiones `v2.1.0` y posteriores:**
WARP está integrado, no se requiere instalación adicional. Simplemente habilita la configuración necesaria en el panel.
</details>
## Límite de IP
<details>
<summary>Haz clic para ver los detalles del límite de IP</summary>
#### Uso
**Nota:** El Límite de IP no funcionará correctamente cuando uses Túnel IP.
- **Para versiones hasta `v1.6.1`:**
- El límite de IP está integrado en el panel.
**Para versiones `v1.7.0` y posteriores:**
Para habilitar la funcionalidad de límite de IP, necesitas instalar `fail2ban` y los archivos requeridos siguiendo estos pasos:
1. Ejecuta el comando `x-ui` en el terminal, luego elige `Gestión de Límite de IP`.
2. Verás las siguientes opciones:
- **Cambiar la Duración del Bloqueo:** Ajustar la duración de los bloqueos.
- **Desbloquear a Todos:** Levantar todos los bloqueos actuales.
- **Revisar los Registros:** Revisar los registros.
- **Estado de Fail2ban:** Verificar el estado de `fail2ban`.
- **Reiniciar Fail2ban:** Reiniciar el servicio `fail2ban`.
- **Desinstalar Fail2ban:** Desinstalar Fail2ban con la configuración.
3. Agrega una ruta para el registro de acceso en el panel configurando `Xray Configs/log/Access log` a `./access.log`, luego guarda y reinicia Xray.
- **Para versiones anteriores a `v2.1.3`:**
- Necesitas configurar manualmente la ruta del registro de acceso en tu configuración de Xray:
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
- **Para versiones `v2.1.3` y posteriores:**
- Hay una opción para configurar `access.log` directamente desde el panel.
</details>
## Bot de Telegram
<details>
<summary>Haz clic para más detalles del bot de Telegram</summary>
#### Uso
El panel web admite tráfico diario, inicio de sesión en el panel, copia de seguridad de la base de datos, estado del sistema, información del cliente y otras notificaciones y funciones a través del Bot de Telegram. Para usar el bot, debes establecer los parámetros relacionados con el bot en el panel, que incluyen:
- Token de Telegram
- ID de chat de administrador(es)
- Hora de Notificación (en sintaxis cron)
- Notificación de Fecha de Caducidad
- Notificación de Capacidad de Tráfico
- Copia de seguridad de la base de datos
- Notificación de Carga de CPU
**Sintaxis de referencia:**
- `30 \* \* \* \* \*` - Notifica a los 30s de cada punto
- `0 \*/10 \* \* \* \*` - Notifica en el primer segundo de cada 10 minutos
- `@hourly` - Notificación por hora
- `@daily` - Notificación diaria (00:00 de la mañana)
- `@weekly` - Notificación semanal
- `@every 8h` - Notifica cada 8 horas
### Funcionalidades del Bot de Telegram
- Reporte periódico
- Notificación de inicio de sesión
- Notificación de umbral de CPU
- Umbral de Notificación para Fecha de Caducidad y Tráfico para informar con anticipación
- Soporte para menú de reporte de cliente si el nombre de usuario de Telegram del cliente se agrega a las configuraciones de usuario
- Soporte para reporte de tráfico de Telegram buscado con UUID (VMESS/VLESS) o Contraseña (TROJAN) - anónimamente
- Bot basado en menú
- Buscar cliente por correo electrónico (solo administrador)
- Ver todas las Entradas
- Ver estado del servidor
- Ver clientes agotados
- Recibir copia de seguridad bajo demanda y en informes periódicos
- Bot multilingüe
### Configuración del Bot de Telegram
- Inicia [Botfather](https://t.me/BotFather) en tu cuenta de Telegram:
![Botfather](./media/botfather.png)
- Crea un nuevo bot usando el comando /newbot: Te hará 2 preguntas, Un nombre y un nombre de usuario para tu bot. Ten en cuenta que el nombre de usuario debe terminar con la palabra "bot".
![Create new bot](./media/newbot.png)
- Inicia el bot que acabas de crear. Puedes encontrar el enlace a tu bot aquí.
![token](./media/token.png)
- Ingresa a tu panel y configura los ajustes del bot de Telegram como se muestra a continuación:
![Panel Config](./media/panel-bot-config.png)
Ingresa el token de tu bot en el campo de entrada número 3.
Ingresa el ID de chat de usuario en el campo de entrada número 4. Las cuentas de Telegram con esta ID serán los administradores del bot. (Puedes ingresar más de uno, solo sepáralos con ,)
- ¿Cómo obtener el ID de chat de Telegram? Usa este [bot](https://t.me/useridinfobot), Inicia el bot y te dará el ID de chat del usuario de Telegram.
![User ID](./media/user-id.png)
</details>
## Rutas de API
<details>
<summary>Haz clic para más detalles de las rutas de API</summary>
#### Uso
- [Documentación de API](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` con `POST` datos de usuario: `{username: '', password: ''}` para iniciar sesión
- `/panel/api/inbounds` base para las siguientes acciones:
| Método | Ruta | Acción |
| :----: | ---------------------------------- | --------------------------------------------------------- |
| `GET` | `"/list"` | Obtener todas los Entradas |
| `GET` | `"/get/:id"` | Obtener Entrada con inbound.id |
| `GET` | `"/getClientTraffics/:email"` | Obtener Tráficos del Cliente con email |
| `GET` | `"/createbackup"` | El bot de Telegram envía copia de seguridad a los admins |
| `POST` | `"/add"` | Agregar Entrada |
| `POST` | `"/del/:id"` | Eliminar Entrada |
| `POST` | `"/update/:id"` | Actualizar Entrada |
| `POST` | `"/clientIps/:email"` | Dirección IP del Cliente |
| `POST` | `"/clearClientIps/:email"` | Borrar Dirección IP del Cliente |
| `POST` | `"/addClient"` | Agregar Cliente a la Entrada |
| `POST` | `"/:id/delClient/:clientId"` | Eliminar Cliente por clientId\* |
| `POST` | `"/updateClient/:clientId"` | Actualizar Cliente por clientId\* |
| `POST` | `"/:id/resetClientTraffic/:email"` | Restablecer Tráfico del Cliente |
| `POST` | `"/resetAllTraffics"` | Restablecer tráfico de todos las Entradas |
| `POST` | `"/resetAllClientTraffics/:id"` | Restablecer tráfico de todos los clientes en una Entrada |
| `POST` | `"/delDepletedClients/:id"` | Eliminar clientes agotados de la entrada (-1: todos) |
| `POST` | `"/onlines"` | Obtener usuarios en línea (lista de correos electrónicos) |
\*- El campo `clientId` debe llenarse por:
- `client.id` para VMESS y VLESS
- `client.password` para TROJAN
- `client.email` para Shadowsocks
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
</details>
## Variables de Entorno
<details>
<summary>Haz clic para más detalles de las variables de entorno</summary>
#### Uso
| Variable | Tipo | Predeterminado |
| -------------- | :--------------------------------------------: | :------------- |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
Ejemplo:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
## Vista previa
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
<img alt="3x-ui" src="./media/04-add-client-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
<img alt="3x-ui" src="./media/05-settings-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
<img alt="3x-ui" src="./media/06-configs-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture>
## Un agradecimiento especial a
- [alireza0](https://github.com/alireza0/)
## Reconocimientos
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Licencia: **GPL-3.0**): _Reglas de enrutamiento mejoradas de v2ray/xray y v2ray/xray-clients con dominios iraníes integrados y un enfoque en seguridad y bloqueo de anuncios._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _Este repositorio contiene reglas de enrutamiento de V2Ray actualizadas automáticamente basadas en datos de dominios y direcciones bloqueados en Rusia._
## Estrellas a lo largo del tiempo
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -7,17 +7,39 @@
</picture>
</p>
**یک پنل وب پیشرفته • ساخته شده بر پایه Xray Core**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکل‌های مختلف VPN و پراکسی ارائه می‌دهد.
> **سلب مسئولیت:** این پروژه صرفاً برای اهداف آموزشی و تحقیقاتی است. استفاده از آن برای مقاصد غیرقانونی یا در محیط‌های عملیاتی ممنوع است.
> [!IMPORTANT]
> این پروژه فقط برای استفاده شخصی و ارتباطات است، لطفاً از آن برای اهداف غیرقانونی استفاده نکنید، لطفاً از آن در محیط تولید استفاده نکنید.
**اگر این پروژه برای شما مفید بوده، می‌توانید با دادن یک**:star2: از آن حمایت کنید.
به عنوان یک نسخه بهبود یافته از پروژه اصلی X-UI، 3X-UI پایداری بهتر، پشتیبانی گسترده‌تر از پروتکل‌ها و ویژگی‌های اضافی را ارائه می‌دهد.
## شروع سریع
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
برای مستندات کامل، لطفاً به [ویکی پروژه](https://github.com/MHSanaei/3x-ui/wiki) مراجعه کنید.
## تشکر ویژه از
- [alireza0](https://github.com/alireza0/)
## قدردانی
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (مجوز: **GPL-3.0**): _قوانین مسیریابی بهبود یافته v2ray/xray و v2ray/xray-clients با دامنه‌های ایرانی داخلی و تمرکز بر امنیت و مسدود کردن تبلیغات._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (مجوز: **GPL-3.0**): _این مخزن شامل قوانین مسیریابی V2Ray به‌روزرسانی شده خودکار بر اساس داده‌های دامنه‌ها و آدرس‌های مسدود شده در روسیه است._
## پشتیبانی از پروژه
**اگر این پروژه برای شما مفید است، می‌توانید به آن یک**:star2: بدهید
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
@@ -26,503 +48,9 @@
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## نصب و ارتقا
## ستاره‌ها در طول زمان
```
bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.6.0/install.sh)
```
## نصب نسخه‌های قدیمی (توصیه نمی‌شود)
برای نصب نسخه خاصی از دستور زیر استفاده کنید. مثال برای نسخه `v1.7.9`:
```
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
```
## گواهی SSL
<details>
<summary>جزئیات گواهی SSL</summary>
### ACME
برای مدیریت گواهی‌های SSL با استفاده از ACME:
1. اطمینان حاصل کنید دامنه شما به درستی به سرور متصل است.
2. دستور `x-ui` را در ترمینال اجرا کرده و گزینه `مدیریت گواهی SSL` را انتخاب کنید.
3. گزینه‌های زیر نمایش داده می‌شوند:
- **دریافت SSL:** دریافت گواهی SSL
- **لغو:** لغو گواهی‌های موجود
- **تمدید اجباری:** تمدید اجباری گواهی‌ها
- **نمایش دامنه‌های موجود:** نمایش تمام دامنه‌های دارای گواهی
- **تنظیم مسیر گواهی برای پنل:** تنظیم مسیر گواهی برای دامنه شما
### Certbot
نصب و استفاده از Certbot:
```sh
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
### Cloudflare
اسکریپت داخلی برای دریافت گواهی SSL از Cloudflare. نیازمند:
- ایمیل ثبت‌شده در Cloudflare
- کلید API جهانی Cloudflare
- دامنه باید از طریق Cloudflare به سرور متصل باشد
**دریافت کلید API جهانی Cloudflare:**
1. دستور `x-ui` را اجرا و گزینه `گواهی SSL کلادفلر` را انتخاب کنید.
2. به لینک [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) مراجعه کنید.
3. روی "View Global API Key" کلیک کنید:
![](media/APIKey1.PNG)
4. پس از احراز هویت، کلید API نمایش داده می‌شود:
![](media/APIKey2.png)
در هنگام استفاده، نام دامنه، ایمیل و کلید API را وارد کنید:
![](media/DetailEnter.png)
</details>
## نصب دستی و ارتقا
<details>
<summary>جزئیات نصب دستی</summary>
#### استفاده
1. دریافت آخرین نسخه از سرور:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. نصب یا ارتقا:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
## نصب با Docker
<details>
<summary>جزئیات Docker</summary>
#### استفاده
1. **نصب Docker:**
```sh
bash <(curl -sSL https://get.docker.com)
```
2. **کلون پروژه:**
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. **راه‌اندازی سرویس:**
```sh
docker compose up -d
```
یا
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
4. **به‌روزرسانی:**
```sh
cd 3x-ui
docker compose down
docker compose pull 3x-ui
docker compose up -d
```
5. **حذف:**
```sh
docker stop 3x-ui
docker rm 3x-ui
cd --
rm -r 3x-ui
```
</details>
## تنظیمات Nginx
<details>
<summary>پیکربندی Reverse Proxy</summary>
#### Nginx Reverse Proxy
```nginx
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
#### مسیر فرعی در Nginx
- اطمینان حاصل کنید "URI Path" در تنظیمات پنل یکسان باشد.
- `url` در تنظیمات پنل باید با `/` پایان یابد.
```nginx
location /sub {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
</details>
## سیستم‌عامل‌های توصیه شده
- Ubuntu 22.04+
- Debian 12+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 9.5+
- Rocky Linux 9.5+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## معماری‌ها و دستگاه‌های پشتیبانی شده
<details>
<summary>جزئیات معماری‌ها و دستگاه‌ها</summary>
- **amd64**: معماری استاندارد برای کامپیوترهای شخصی و سرورها
- **x86 / i386**: سیستم‌های دسکتاپ و لپ‌تاپ
- **armv8 / arm64 / aarch64**: دستگاه‌های موبایل و embedded مانند Raspberry Pi 4
- **armv7 / arm / arm32**: دستگاه‌های قدیمی مانند Orange Pi Zero
- **armv6 / arm / arm32**: دستگاه‌های بسیار قدیمی مانند Raspberry Pi 1
- **armv5 / arm / arm32**: سیستم‌های embedded قدیمی
- **s390x**: کامپیوترهای IBM mainframe
</details>
## زبان‌های پشتیبانی شده
- انگلیسی
- فارسی
- چینی سنتی
- چینی ساده‌شده
- ژاپنی
- روسی
- ویتنامی
- اسپانیایی
- اندونزیایی
- اوکراینی
- ترکی
- پرتغالی (برزیل)
## ویژگی‌ها
- مانیتورینگ وضعیت سیستم
- جستجو در بین inboundها و کلاینت‌ها
- تم تاریک/روشن
- پشتیبانی از چند کاربر و پروتکل
- پروتکل‌های VMESS، VLESS، Trojan، Shadowsocks، Dokodemo-door، Socks، HTTP، WireGuard
- پشتیبانی از XTLS شامل RPRX-Direct، Vision، REALITY
- آمار ترافیک، محدودیت ترافیک، محدودیت زمانی
- تنظیمات سفارشی Xray
- پشتیبانی از HTTPS برای پنل
- دریافت خودکار گواهی SSL
- مسیرهای API اصلاح شده
- پشتیبانی از تغییر تنظیمات از طریق پنل
- امکان export/import دیتابیس
## تنظیمات پیش‌فرض پنل
<details>
<summary>جزئیات تنظیمات پیش‌فرض</summary>
### نام کاربری، رمز عبور، پورت و مسیر وب
در صورت عدم تغییر، این موارد به صورت تصادفی ایجاد می‌شوند (به جز Docker).
**تنظیمات پیش‌فرض Docker:**
- **نام کاربری:** admin
- **رمز عبور:** admin
- **پورت:** 2053
### مدیریت دیتابیس:
امکان Backup و Restore دیتابیس از طریق پنل.
- **مسیر دیتابیس:**
- `/etc/x-ui/x-ui.db`
### مسیر پایه وب
1. **بازنشانی مسیر:**
- اجرای دستور `x-ui`
- انتخاب گزینه `Reset Web Base Path`
2. **ساخت یا تنظیم مسیر:**
- مسیر به صورت تصادفی ساخته شده یا قابل تنظیم است
3. **مشاهده تنظیمات فعلی:**
- استفاده از دستور `x-ui settings` یا `View Current Settings` در `x-ui`
**توصیه امنیتی:**
- استفاده از مسیرهای طولانی و تصادفی برای افزایش امنیت
**مثال:**
- `http://ip:port/*webbasepath*/panel`
- `http://domain:port/*webbasepath*/panel`
</details>
## پیکربندی WARP
<details>
<summary>جزئیات WARP</summary>
#### استفاده
**برای نسخه‌های `v2.1.0` و جدیدتر:**
WARP به صورت داخلی پشتیبانی می‌شود. تنها نیاز به فعال‌سازی در پنل است.
</details>
## محدودیت IP
<details>
<summary>جزئیات محدودیت IP</summary>
#### استفاده
**توجه:** محدودیت IP در صورت استفاده از IP Tunnel کار نمی‌کند.
- **تا نسخه `v1.6.1`:**
- محدودیت IP به صورت داخلی در پنل وجود دارد
**برای نسخه‌های `v1.7.0` و جدیدتر:**
برای فعال‌سازی نیاز به نصب `fail2ban` است:
1. اجرای دستور `x-ui` و انتخاب `مدیریت محدودیت IP`
2. گزینه‌های موجود:
- **تغییر مدت زمان Ban**
- **حذف تمام Banها**
- **مشاهده لاگ‌ها**
- **وضعیت Fail2ban**
- **راه‌اندازی مجدد Fail2ban**
- **حذف Fail2ban**
3. تنظیم مسیر `Access log` در پنل به `./access.log` و ذخیره و راه‌اندازی مجدد Xray
- **قبل از نسخه `v2.1.3`:**
- تنظیم دستی `access.log` در تنظیمات Xray:
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
- **از نسخه `v2.1.3`:**
- امکان تنظیم `access.log` از طریق پنل
</details>
## ربات تلگرام
<details>
<summary>جزئیات ربات تلگرام</summary>
#### استفاده
ربات تلگرام برای اطلاع‌رسانی ترافیک، ورود به پنل، Backup دیتابیس و ... استفاده می‌شود. نیازمند تنظیم:
- توکن تلگرام
- Chat ID ادمین‌ها
- زمان اطلاع‌رسانی (Cron syntax)
- اطلاع‌رسانی انقضا
- اطلاع‌رسانی ترافیک
- Backup دیتابیس
- اطلاع‌رسانی مصرف CPU
**سینتکس نمونه:**
- `30 \* \* \* \* \*` - اطلاع در ثانیه 30 هر دقیقه
- `@hourly` - هر ساعت
- `@daily` - هر روز
### ویژگی‌های ربات
- گزارش دوره‌ای
- اطلاع ورود به پنل
- اطلاع مصرف CPU
- اطلاع پیش‌از موعد انقضا و ترافیک
- گزارش ترافیک کلاینت‌ها
- منوی مبتنی بر دستور
- جستجوی کلاینت بر اساس ایمیل
- بررسی inboundها
- بررسی وضعیت سرور
- دریافت Backup
- چندزبانه
### راه‌اندازی ربات
- شروع [Botfather](https://t.me/BotFather) در تلگرام:
![Botfather](./media/botfather.png)
- ساخت ربات جدید با دستور /newbot:
![Create new bot](./media/newbot.png)
- شروع ربات ساخته شده:
![token](./media/token.png)
- تنظیمات پنل:
![Panel Config](./media/panel-bot-config.png)
وارد کردن توکن و Chat ID (دریافت از [این ربات](https://t.me/useridinfobot)):
![User ID](./media/user-id.png)
</details>
## مسیرهای API
<details>
<summary>جزئیات API</summary>
#### استفاده
- [مستندات API](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` با `POST` داده کاربر: `{username: '', password: ''}`
| Method | مسیر | عملکرد |
| :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | دریافت تمام inboundها |
| `GET` | `"/get/:id"` | دریافت inbound بر اساس id |
| `POST` | `"/add"` | افزودن inbound |
| `POST` | `"/del/:id"` | حذف inbound |
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
</details>
## متغیرهای محیطی
<details>
<summary>جزئیات متغیرها</summary>
#### استفاده
| متغیر | نوع | پیش‌فرض |
| ------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER| `string` | `"bin"` |
مثال:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
## پیش‌نمایش
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
## قدردانی ویژه از
- [alireza0](https://github.com/alireza0/)
## تشکر و قدردانی
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (مجوز: **GPL-3.0**)
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (مجوز: **GPL-3.0**)
## Stargazers over Time
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)

600
README.md
View File

@@ -1,4 +1,4 @@
[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
[English](/README.md) | [فارسی](/README.fa_IR.md) | [العربية](/README.ar_EG.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
@@ -7,582 +7,26 @@
</picture>
</p>
**An Advanced Web Panel • Built on Xray Core**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
**3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols.
> **Disclaimer:** This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment
> [!IMPORTANT]
> This project is only for personal using, please do not use it for illegal purposes, please do not use it in a production environment.
**If this project is helpful to you, you may wish to give it a**:star2:
As an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features.
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
## Quick Start
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Install & Upgrade
```
bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.6.0/install.sh)
```bash
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## Install legacy Version (we don't recommend)
To install your desired version, use following installation command. e.g., ver `v1.7.9`:
```
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
```
## SSL Certificate
<details>
<summary>Click for SSL Certificate details</summary>
### ACME
To manage SSL certificates using ACME:
1. Ensure your domain is correctly resolved to the server.
2. Run the `x-ui` command in the terminal, then choose `SSL Certificate Management`.
3. You will be presented with the following options:
- **Get SSL:** Obtain SSL certificates.
- **Revoke:** Revoke existing SSL certificates.
- **Force Renew:** Force renewal of SSL certificates.
- **Show Existing Domains:** Display all domain certificates available on the server.
- **Set Certificate Paths for the Panel:** Specify the certificate for your domain to be used by the panel.
### Certbot
To install and use Certbot:
```sh
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
### Cloudflare
The management script includes a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following:
- Cloudflare registered email
- Cloudflare Global API Key
- The domain name must be resolved to the current server through Cloudflare
**How to get the Cloudflare Global API Key:**
1. Run the `x-ui` command in the terminal, then choose `Cloudflare SSL Certificate`.
2. Visit the link: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens).
3. Click on "View Global API Key" (see the screenshot below):
![](media/APIKey1.PNG)
4. You may need to re-authenticate your account. After that, the API Key will be shown (see the screenshot below):
![](media/APIKey2.png)
When using, just enter your `domain name`, `email`, and `API KEY`. The diagram is as follows:
![](media/DetailEnter.png)
</details>
## Manual Install & Upgrade
<details>
<summary>Click for manual install details</summary>
#### Usage
1. To download the latest version of the compressed package directly to your server, run the following command:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. Once the compressed package is downloaded, execute the following commands to install or upgrade x-ui:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
## Install with Docker
<details>
<summary>Click for Docker details</summary>
#### Usage
1. **Install Docker:**
```sh
bash <(curl -sSL https://get.docker.com)
```
2. **Clone the Project Repository:**
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. **Start the Service:**
```sh
docker compose up -d
```
Add ```--pull always``` flag to make docker automatically recreate container if a newer image is pulled. See https://docs.docker.com/reference/cli/docker/container/run/#pull for more info.
**OR**
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
4. **Update to the Latest Version:**
```sh
cd 3x-ui
docker compose down
docker compose pull 3x-ui
docker compose up -d
```
5. **Remove 3x-ui from Docker:**
```sh
docker stop 3x-ui
docker rm 3x-ui
cd --
rm -r 3x-ui
```
</details>
## Nginx Settings
<details>
<summary>Click for Reverse Proxy Configuration</summary>
#### Nginx Reverse Proxy
```nginx
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
#### Nginx sub-path
- Ensure that the "URI Path" in the `/sub` panel settings is the same.
- The `url` in the panel settings needs to end with `/`.
```nginx
location /sub {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
</details>
## Recommended OS
- Ubuntu 22.04+
- Debian 12+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 9.5+
- Rocky Linux 9.5+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## Supported Architectures and Devices
<details>
<summary>Click for Supported Architectures and devices details</summary>
Our platform offers compatibility with a diverse range of architectures and devices, ensuring flexibility across various computing environments. The following are key architectures that we support:
- **amd64**: This prevalent architecture is the standard for personal computers and servers, accommodating most modern operating systems seamlessly.
- **x86 / i386**: Widely adopted in desktop and laptop computers, this architecture enjoys broad support from numerous operating systems and applications, including but not limited to Windows, macOS, and Linux systems.
- **armv8 / arm64 / aarch64**: Tailored for contemporary mobile and embedded devices, such as smartphones and tablets, this architecture is exemplified by devices like Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, and more.
- **armv7 / arm / arm32**: Serving as the architecture for older mobile and embedded devices, it remains widely utilized in devices like Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, among others.
- **armv6 / arm / arm32**: Geared towards very old embedded devices, this architecture, while less prevalent, is still in use. Devices such as Raspberry Pi 1, Raspberry Pi Zero/Zero W, rely on this architecture.
- **armv5 / arm / arm32**: An older architecture primarily associated with early embedded systems, it is less common today but may still be found in legacy devices like early Raspberry Pi versions and some older smartphones.
- **s390x**: This architecture is commonly used in IBM mainframe computers and offers high performance and reliability for enterprise workloads.
</details>
## Languages
- Arabic
- English
- Persian
- Traditional Chinese
- Simplified Chinese
- Japanese
- Russian
- Vietnamese
- Spanish
- Indonesian
- Ukrainian
- Turkish
- Português (Brazil)
## Features
- System Status Monitoring
- Search within all inbounds and clients
- Dark/Light theme
- Supports multi-user and multi-protocol
- Supports protocols, including VMESS, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, wireguard
- Supports XTLS native Protocols, including RPRX-Direct, Vision, REALITY
- Traffic statistics, traffic limit, expiration time limit
- Customizable Xray configuration templates
- Supports HTTPS access panel (self-provided domain name + SSL certificate)
- Supports One-Click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel
- Fixes API routes (user setting will be created with API)
- Supports changing configs by different items provided in the panel.
- Supports export/import database from the panel
## Default Panel Settings
<details>
<summary>Click for default settings details</summary>
### Username, Password, Port, and Web Base Path
If you choose not to modify these settings, they will be generated randomly (this does not apply to Docker).
**Default Settings for Docker:**
- **Username:** admin
- **Password:** admin
- **Port:** 2053
### Database Management:
You can conveniently perform database Backups and Restores directly from the panel.
- **Database Path:**
- `/etc/x-ui/x-ui.db`
### Web Base Path
1. **Reset Web Base Path:**
- Open your terminal.
- Run the `x-ui` command.
- Select the option to `Reset Web Base Path`.
2. **Generate or Customize Path:**
- The path will be randomly generated, or you can enter a custom path.
3. **View Current Settings:**
- To view your current settings, use the `x-ui settings` command in the terminal or `View Current Settings` in `x-ui`
### Security Recommendation:
- For enhanced security, use a long, random word in your URL structure.
**Examples:**
- `http://ip:port/*webbasepath*/panel`
- `http://domain:port/*webbasepath*/panel`
</details>
## WARP Configuration
<details>
<summary>Click for WARP configuration details</summary>
#### Usage
**For versions `v2.1.0` and later:**
WARP is built-in, and no additional installation is required. Simply turn on the necessary configuration in the panel.
</details>
## IP Limit
<details>
<summary>Click for IP limit details</summary>
#### Usage
**Note:** IP Limit won't work correctly when using IP Tunnel.
- **For versions up to `v1.6.1`:**
- The IP limit is built-in to the panel
**For versions `v1.7.0` and newer:**
To enable the IP Limit functionality, you need to install `fail2ban` and its required files by following these steps:
1. Run the `x-ui` command in the terminal, then choose `IP Limit Management`.
2. You will see the following options:
- **Change Ban Duration:** Adjust the duration of bans.
- **Unban Everyone:** Lift all current bans.
- **Check Logs:** Review the logs.
- **Fail2ban Status:** Check the status of `fail2ban`.
- **Restart Fail2ban:** Restart the `fail2ban` service.
- **Uninstall Fail2ban:** Uninstall Fail2ban with configuration.
3. Add a path for the access log on the panel by setting `Xray Configs/log/Access log` to `./access.log` then save and restart xray.
- **For versions before `v2.1.3`:**
- You need to set the access log path manually in your Xray configuration:
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
- **For versions `v2.1.3` and newer:**
- There is an option for configuring `access.log` directly from the panel.
</details>
## Telegram Bot
<details>
<summary>Click for Telegram bot details</summary>
#### Usage
The web panel supports daily traffic, panel login, database backup, system status, client info, and other notification and functions through the Telegram Bot. To use the bot, you need to set the bot-related parameters in the panel, including:
- Telegram Token
- Admin Chat ID(s)
- Notification Time (in cron syntax)
- Expiration Date Notification
- Traffic Cap Notification
- Database Backup
- CPU Load Notification
**Reference syntax:**
- `30 \* \* \* \* \*` - Notify at the 30s of each point
- `0 \*/10 \* \* \* \*` - Notify at the first second of each 10 minutes
- `@hourly` - Hourly notification
- `@daily` - Daily notification (00:00 in the morning)
- `@weekly` - weekly notification
- `@every 8h` - Notify every 8 hours
### Telegram Bot Features
- Report periodic
- Login notification
- CPU threshold notification
- Threshold for Expiration time and Traffic to report in advance
- Support client report menu if client's telegram username added to the user's configurations
- Support telegram traffic report searched with UUID (VMESS/VLESS) or Password (TROJAN) - anonymously
- Menu-based bot
- Search client by email (only admin)
- Check all inbounds
- Check server status
- Check depleted users
- Receive backup by request and in periodic reports
- Multi-language bot
### Setting up Telegram bot
- Start [Botfather](https://t.me/BotFather) in your Telegram account:
![Botfather](./media/botfather.png)
- Create a new Bot using /newbot command: It will ask you 2 questions, A name and a username for your bot. Note that the username has to end with the word "bot".
![Create new bot](./media/newbot.png)
- Start the bot you've just created. You can find the link to your bot here.
![token](./media/token.png)
- Enter your panel and config Telegram bot settings like below:
![Panel Config](./media/panel-bot-config.png)
Enter your bot token in input field number 3.
Enter the user ID in input field number 4. The Telegram accounts with this id will be the bot admin. (You can enter more than one, Just separate them with ,)
- How to get Telegram user ID? Use this [bot](https://t.me/useridinfobot), Start the bot and it will give you the Telegram user ID.
![User ID](./media/user-id.png)
</details>
## API Routes
<details>
<summary>Click for API routes details</summary>
#### Usage
- [API Documentation](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` with `POST` user data: `{username: '', password: ''}` for login
- `/panel/api/inbounds` base for following actions:
| Method | Path | Action |
| :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | Get all inbounds |
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
| `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email |
| `GET` | `"/getClientTrafficsById/:id"` | Get client's traffic By ID |
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
| `POST` | `"/add"` | Add inbound |
| `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound |
| `POST` | `"/clientIps/:email"` | Client Ip address |
| `POST` | `"/clearClientIps/:email"` | Clear Client Ip address |
| `POST` | `"/addClient"` | Add Client to inbound |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* |
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* |
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
| `POST` | `"/onlines"` | Get Online users ( list of emails ) |
\*- The field `clientId` should be filled by:
- `client.id` for VMESS and VLESS
- `client.password` for TROJAN
- `client.email` for Shadowsocks
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
</details>
## Environment Variables
<details>
<summary>Click for environment variables details</summary>
#### Usage
| Variable | Type | Default |
| -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
Example:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
## Preview
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
<img alt="3x-ui" src="./media/04-add-client-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
<img alt="3x-ui" src="./media/05-settings-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
<img alt="3x-ui" src="./media/06-configs-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture>
For full documentation, please visit the [project Wiki](https://github.com/MHSanaei/3x-ui/wiki).
## A Special Thanks to
@@ -593,6 +37,20 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _This repository contains automatically updated V2Ray routing rules based on data on blocked domains and addresses in Russia._
## Support project
**If this project is helpful to you, you may wish to give it a**:star2:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Stargazers over Time
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -7,17 +7,39 @@
</picture>
</p>
**Продвинутая веб-панель • Построена на основе Xray Core**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов.
> **Отказ от ответственности:** Этот проект предназначен только для личного обучения и общения. Пожалуйста, не используйте его в незаконных целях и не применяйте в производственной среде.
> [!IMPORTANT]
> Этот проект предназначен только для личного использования, пожалуйста, не используйте его в незаконных целях и в производственной среде.
**Если этот проект оказался полезным для вас, вы можете оценить его, поставив звёздочку** :star2:
Как улучшенная версия оригинального проекта X-UI, 3X-UI обеспечивает повышенную стабильность, более широкую поддержку протоколов и дополнительные функции.
## Быстрый старт
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
Полную документацию смотрите в [вики проекта](https://github.com/MHSanaei/3x-ui/wiki).
## Особая благодарность
- [alireza0](https://github.com/alireza0/)
## Благодарности
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (Лицензия: **GPL-3.0**): _Улучшенные правила маршрутизации для v2ray/xray и v2ray/xray-clients со встроенными иранскими доменами и фокусом на безопасность и блокировку рекламы._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (Лицензия: **GPL-3.0**): _Этот репозиторий содержит автоматически обновляемые правила маршрутизации V2Ray на основе данных о заблокированных доменах и адресах в России._
## Поддержка проекта
**Если этот проект полезен для вас, вы можете поставить ему**:star2:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
@@ -26,571 +48,9 @@
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Установка и обновление
## Звезды с течением времени
```
bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.6.0/install.sh)
```
## Установить старую версию (мы не рекомендуем)
Чтобы установить желаемую версию, используйте следующую команду установки. Например, ver `v1.7.9`:
```
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
```
## SSL Сертификат
<details>
<summary>Нажмите для получения информации об SSL сертификате</summary>
### ACME
Для управления SSL сертификатами с помощью ACME:
1. Убедитесь, что ваш домен правильно настроен и указывает на сервер.
2. Выполните команду `x-ui` в терминале, затем выберите `SSL Certificate Management`.
3. Вам будут предложены следующие опции:
- **Get SSL:** Получить SSL сертификаты.
- **Revoke:** Отозвать существующие SSL сертификаты.
- **Force Renew:** Принудительно перевыпустить SSL сертификаты.
- **Show Existing Domains:** Отобразить все сертификаты доменов, доступные на сервере.
- **Set Certificate Paths for the Panel:** Укажите сертификат для вашего домена, который будет использоваться панелью.
### Certbot
Для установки и использования Certbot:
```sh
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d вашдомен.com
certbot renew --dry-run
```
### Cloudflare
Скрипт управления включает встроенное приложение для получения SSL сертификата через Cloudflare. Чтобы использовать этот скрипт для запроса сертификата, вам потребуется следующее:
- Email, зарегистрированный в Cloudflare
- Глобальный API-ключ Cloudflare
- Доменное имя должно указывать на текущий сервер через Cloudflare
**Как получить глобальный API-ключ Cloudflare:**
1. Выполните команду `x-ui` в терминале, затем выберите `Cloudflare SSL Certificate`.
2. Перейдите по ссылке: [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens).
3. Нажмите на "View Global API Key" (см. скриншот ниже):
![](media/APIKey1.PNG)
4. Возможно, вам потребуется повторно пройти аутентификацию. После этого ключ API будет отображён (см. скриншот ниже):
![](media/APIKey2.png)
При использовании просто введите ваше `доменное имя`, `email` и `API-ключ`. Схема приведена ниже:
![](media/DetailEnter.png)
</details>
## Ручная установка и обновление
<details>
<summary>Нажмите для получения информации о ручной установке</summary>
#### Использование
1. Чтобы скачать последнюю версию архива напрямую на ваш сервер, выполните следующую команду:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. После загрузки архива выполните следующие команды для установки или обновления x-ui:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
## Установка с помощью Docker
<details>
<summary>Нажмите для получения информации о Docker</summary>
#### Использование
1. **Установите Docker:**
```sh
bash <(curl -sSL https://get.docker.com)
```
2. **Склонируйте репозиторий проекта:**
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. **Запустите сервис:**
```sh
docker compose up -d
```
Добавьте параметр ```--pull always``` для автоматического обновления контейнера, когда публикуется новый образ. Подробности: https://docs.docker.com/reference/cli/docker/container/run/#pull
**ИЛИ**
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
4. **Обновление до последней версии:**
```sh
cd 3x-ui
docker compose down
docker compose pull 3x-ui
docker compose up -d
```
5. **Удаление 3x-ui из Docker:**
```sh
docker stop 3x-ui
docker rm 3x-ui
cd --
rm -r 3x-ui
```
</details>
## Настройки Nginx
<details>
<summary>Нажмите чтобы просмотреть конфигурацию обратного прокси-сервера</summary>
#### Обратный прокси-сервер Nginx
```nginx
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
#### Nginx sub-path
- Убедитесь, что "корневой путь URL адреса панели" в настройках панели и `/sub` совпадают.
- В настройках панели `url` должен заканчиваться на `/`.
```nginx
location /sub {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
</details>
## Рекомендуемые ОС
- Ubuntu 22.04+
- Debian 12+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 9.5+
- Rocky Linux 9.5+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## Поддерживаемые архитектуры и устройства
<details>
<summary>Нажмите для получения информации о поддерживаемых архитектурах и устройствах</summary>
Наша платформа поддерживает разнообразные архитектуры и устройства, обеспечивая гибкость в различных вычислительных средах. Вот основные архитектуры, которые мы поддерживаем:
- **amd64**: Эта распространенная архитектура является стандартом для персональных компьютеров и серверов, обеспечивая беспроблемную работу большинства современных операционных систем.
- **x86 / i386**: Широко используется в настольных и портативных компьютерах. Эта архитектура имеет широкую поддержку со стороны множества операционных систем и приложений, включая, но не ограничиваясь, Windows, macOS и Linux.
- **armv8 / arm64 / aarch64**: Предназначена для современных мобильных и встроенных устройств, таких как смартфоны и планшеты. Эта архитектура представлена устройствами, такими как Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS и другими.
- **armv7 / arm / arm32**: Служит архитектурой для старых мобильных и встроенных устройств, оставаясь широко используемой в таких устройствах, как Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2 и других.
- **armv6 / arm / arm32**: Ориентирована на очень старые встроенные устройства, эта архитектура, хотя и менее распространенная, всё ещё используется. Например, такие устройства, как Raspberry Pi 1, Raspberry Pi Zero/Zero W, полагаются на эту архитектуру.
- **armv5 / arm / arm32**: Более старая архитектура, ассоциируемая с ранними встроенными системами, сегодня менее распространена, но всё ещё может быть найдена в устаревших устройствах, таких как ранние версии Raspberry Pi и некоторые старые смартфоны.
- **s390x**: Эта архитектура обычно используется в мейнфреймах IBM и обеспечивает высокую производительность и надежность для корпоративных рабочих нагрузок.
</details>
## Языки
- Arabic (арабский)
- English (английский)
- Persian (персидский)
- Traditional Chinese (традиционный китайский)
- Simplified Chinese (упрощенный китайский)
- Japanese (японский)
- Russian (русский)
- Vietnamese (вьетнамский)
- Spanish (испанский)
- Indonesian (индонезийский)
- Ukrainian (украинский)
- Turkish (турецкий)
- Português (Brazil) (португальский (Бразилия))
## Возможности
- Мониторинг состояния системы
- Поиск по всем входящим подключениям и клиентам
- Тёмная/светлая тема
- Поддержка нескольких пользователей и протоколов
- Поддержка протоколов, включая VMESS, VLESS, Trojan, Shadowsocks, Dokodemo-door, Socks, HTTP, WireGuard
- Поддержка протоколов XTLS, включая RPRX-Direct, Vision, REALITY
- Статистика трафика, ограничение трафика, ограничение по времени истечения
- Настраиваемые шаблоны конфигурации Xray
- Поддержка HTTPS доступа к панели (ваше доменное имя + SSL сертификат)
- Поддержка установки SSL-сертификата в один клик и автоматического перевыпуска
- Для получения более продвинутых настроек обращайтесь к панели
- Исправляет маршруты API (настройка пользователя будет создана через API)
- Поддержка изменения конфигураций по различным элементам, предоставленным в панели
- Поддержка экспорта/импорта базы данных из панели
## Настройки панели по умолчанию
<details>
<summary>Нажмите для получения информации о настройках по умолчанию</summary>
### Имя пользователя, Пароль, Порт и Web Base Path
Если вы не измените эти настройки, они будут сгенерированы случайным образом (это не относится к Docker).
**Настройки по умолчанию для Docker:**
- **Имя пользователя:** admin
- **Пароль:** admin
- **Порт:** 2053
### Управление базой данных:
Вы можете удобно выполнять резервное копирование и восстановление базы данных прямо из панели.
- **Путь к базе данных:**
- `/etc/x-ui/x-ui.db`
### Webbasepath
1. **Сбросить webbasepath:**
- Откройте терминал.
- Выполните команду `x-ui`.
- Выберите опцию `Reset Web Base Path`.
2. **Генерация или настройка пути:**
- Путь будет сгенерирован случайным образом, или вы можете ввести собственный путь.
3. **Просмотр текущих настроек:**
- Чтобы просмотреть текущие настройки, используйте команду `x-ui settings` в терминале или опцию `View Current Settings` в `x-ui`.
### Рекомендации по безопасности:
- Для повышения безопасности используйте длинное случайное слово в структуре вашего URL.
**Примеры:**
- `http://ip_адрес:порт/*webbasepath*/panel`
- `http://домен:порт/*webbasepath*/panel`
</details>
## Настройка WARP
<details>
<summary>Нажмите для получения информации о настройке WARP</summary>
#### Использование
**Для версий `v2.1.0` и новее:**
WARP встроен, и дополнительная установка не требуется. Просто включите необходимую конфигурацию в панели.
</details>
## Ограничение IP
<details>
<summary>Нажмите для получения информации об ограничении IP</summary>
#### Использование
**Примечание:** Ограничение IP не будет работать корректно при использовании IP Tunnel.
- **Для версий до `v1.6.1`:**
- Ограничение IP встроено в панель.
**Для версий `v1.7.0` и новее:**
Чтобы включить функциональность ограничения IP, вам нужно установить `fail2ban` и его необходимые файлы, выполнив следующие шаги:
1. Выполните команду `x-ui` в терминале, затем выберите `IP Limit Management`.
2. Вам будут предложены следующие опции:
- **Change Ban Duration:** Отрегулировать длительность блокировок.
- **Unban Everyone:** Снять все текущие блокировки.
- **Check Logs:** Просмотреть логи.
- **Fail2ban Status:** Проверить статус `fail2ban`.
- **Restart Fail2ban:** Перезапустить службу `fail2ban`.
- **Uninstall Fail2ban:** Удалить Fail2ban с его конфигурацией.
3. Добавьте путь к логам доступа в панели, установив `Xray Configs/log/Access log` в `./access.log`, затем сохраните и перезапустите xray.
- **Для версий до `v2.1.3`:**
- Вам нужно вручную установить путь к логам доступа в вашей конфигурации Xray:
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
- **Для версий `v2.1.3` и новее:**
- Есть возможность настройки `access.log` непосредственно из панели.
</details>
## Телеграм-бот
<details>
<summary>Нажмите для получения информации о телеграм-боте</summary>
#### Использование
Веб-панель поддерживает уведомления и функции, такие как ежедневный трафик, вход в панель, резервное копирование базы данных, состояние системы, информация о клиентах и другие, через телеграм-бота. Чтобы использовать бота, вам нужно настроить параметры, связанные с ботом, в панели, включая:
- Токен Telegram
- ID чата админа(-ов)
- Время уведомлений (в синтаксисе cron)
- Уведомления о дате истечения
- Уведомления о лимите трафика
- Резервное копирование базы данных
- Уведомления о загрузке CPU
**Примеры синтаксиса:**
- `30 * * * * *` - Уведомлять на 30-й секунде каждого часа
- `0 */10 * * * *` - Уведомлять на первой секунде каждых 10 минут
- `@hourly` - Ежечасное уведомление
- `@daily` - Ежедневное уведомление (в 00:00)
- `@weekly` - Еженедельное уведомление
- `@every 8h` - Уведомлять каждые 8 часов
### Возможности телеграм-бота
- Периодические отчеты
- Уведомления о входе
- Уведомления о пороге загруженности процессора
- Уведомления о времени истечения и трафике заранее
- Поддерживает меню отчетов клиента, если имя пользователя телеграм клиента добавлено в конфигурации пользователя
- Поддержка отчета о трафике через Telegram, поиск по UUID (VMESS/VLESS) или паролю (TROJAN) - анонимно
- Бот, основанный на меню
- Поиск клиента по email (только администратор)
- Проверка всех входящих соединений
- Проверка состояния сервера
- Проверка истекших пользователей
- Получение резервных копий по запросу и в периодических отчётах
- Многоязычный бот
### Настройка телеграм-бота
- Запустите [Botfather](https://t.me/BotFather) в вашем аккаунте Telegram:
![Botfather](./media/botfather.png)
- Создайте нового бота с помощью команды /newbot: у вас спросят 2 вопроса: отображаемое имя и имя пользователя для вашего бота. Обратите внимание, что имя пользователя должно заканчиваться на слово "bot".
![Создать нового бота](./media/newbot.png)
- Запустите созданного бота. Ссылку на вашего бота можно найти здесь.
![токен](./media/token.png)
- Перейдите в панель и настройте параметры телеграм-бота следующим образом:
![Настройки панели](./media/panel-bot-config.png)
Введите токен вашего бота в поле ввода номер 3.
Введите ID пользователя в поле ввода номер 4. Telegram-аккаунты с этим ID будут администраторами бота. (Вы можете ввести несколько ID, разделяя их запятой)
- Как получить ID пользователя Telegram? Используйте этот [бот](https://t.me/useridinfobot). Запустите бота, и он отобразит ваш ID пользователя Telegram.
![ID пользователя](./media/user-id.png)
</details>
## Маршруты API
<details>
<summary>Нажмите для получения информации о маршрутах API</summary>
#### Использование
- [API документация](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` с `POST`-данными: `{username: '', password: ''}` для входа
- `/panel/api/inbounds` это базовый путь для следующих действий:
| Метод | Путь | Действие
| :----: | -----------------------------------| -------------------------------------------
| `GET` | `"/list"` | Получить все входящие соединения
| `GET` | `"/get/:id"` | Получить входящее соединение с inbound.id
| `GET` | `"/getClientTraffics/:email"` | Получить трафик клиента по email
| `GET` | `"/getClientTrafficsById/:id"` | Получить трафик клиента по ID
| `GET` | `"/createbackup"` | Telegram-бот отправит резервную копию администраторам
| `POST` | `"/add"` | Добавить входящее соединение
| `POST` | `"/del/:id"` | Удалить входящее соединение
| `POST` | `"/update/:id"` | Обновить входящее соединение
| `POST` | `"/clientIps/:email"` | IP-адрес клиента
| `POST` | `"/clearClientIps/:email"` | Очистить IP-адреса клиента
| `POST` | `"/addClient"` | Добавить клиента к входящему соединению
| `POST` | `"/:id/delClient/:clientId"` | Удалить клиента по clientId\*
| `POST` | `"/updateClient/:clientId"` | Обновить клиента по clientId\*
| `POST` | `"/:id/resetClientTraffic/:email"` | Сбросить трафик клиента
| `POST` | `"/resetAllTraffics"` | Сбросить трафик всех входящих соединений
| `POST` | `"/resetAllClientTraffics/:id"` | Сбросить трафик всех клиентов в входящем соединении
| `POST` | `"/delDepletedClients/:id"` | Удалить истекших клиентов в входящем соединении (-1: всех)
| `POST` | `"/onlines"` | Получить пользователей, которые находятся онлайн (список email'ов)
\*- Поле `clientId` должно быть заполнено следующим образом:
- `client.id` для VMESS и VLESS
- `client.password` для TROJAN
- `client.email` для Shadowsocks
</details>
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
</details>
## Переменные среды
<details>
<summary>Нажмите для получения информации о переменных среды</summary>
#### Использование
| Переменная | Тип | Значение по умолчанию |
| ---------------- | :------------------------------------------: | :-------------------- |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
Пример:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
## Предварительный Просмотр
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
<img alt="3x-ui" src="./media/04-add-client-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
<img alt="3x-ui" src="./media/05-settings-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
<img alt="3x-ui" src="./media/06-configs-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture>
## Особая благодарность
- [alireza0](https://github.com/alireza0/)
## Благодарности
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _Этот репозиторий содержит автоматически обновляемые правила маршрутизации V2Ray на основе данных о заблокированных доменах и адресах в России._
## Число звёзд со временем
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -7,17 +7,39 @@
</picture>
</p>
**一个更好的面板 • 基于Xray Core构建**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/actions)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg?style=for-the-badge)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg?style=for-the-badge)](https://github.com/MHSanaei/3x-ui/releases/latest)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true&style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0.en.html)
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
**3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。
> **Disclaimer:** 此项目仅供个人学习交流,请不要用于非法目的,请不要在生产环境中使用。
> [!IMPORTANT]
> 本项目仅用于个人使用和通信,请勿将其用于非法目的,请勿在生产环境中使用。
**如果此项目对你有用,请给一个**:star2:
作为原始 X-UI 项目的增强版本3X-UI 提供了更好的稳定性、更广泛的协议支持和额外的功能。
## 快速开始
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
完整文档请参阅 [项目Wiki](https://github.com/MHSanaei/3x-ui/wiki)。
## 特别感谢
- [alireza0](https://github.com/alireza0/)
## 致谢
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (许可证: **GPL-3.0**): _增强的 v2ray/xray 和 v2ray/xray-clients 路由规则,内置伊朗域名,专注于安全性和广告拦截。_
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (许可证: **GPL-3.0**): _此仓库包含基于俄罗斯被阻止域名和地址数据自动更新的 V2Ray 路由规则。_
## 支持项目
**如果这个项目对您有帮助,您可以给它一个**:star2:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
@@ -26,564 +48,9 @@
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## 安装 & 升级
## 随时间变化的星标数
```
bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/refs/tags/v2.6.0/install.sh)
```
## 安装旧版本 (我们不建议)
要安装您想要的版本请使用以下安装命令。例如ver `v1.7.9`:
```
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
```
### SSL证书
<details>
<summary>点击查看SSL证书详情</summary>
### ACME
使用ACME管理SSL证书
1. 确保您的域名正确解析到服务器。
2. 在终端中运行 `x-ui` 命令,然后选择 `SSL证书管理`
3. 您将看到以下选项:
- **Get SSL:** 获取SSL证书。
- **Revoke:** 吊销现有的SSL证书。
- **Force Renew:** 强制更新SSL证书。
- **Show Existing Domains:** 显示服务器上所有可用的域证书。
- **Set Certificate Paths for the Panel:** 指定用于面板的域证书。
### Certbot
安装并使用Certbot
```sh
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
### Cloudflare
管理脚本内置了Cloudflare的SSL证书申请。要使用此脚本申请证书您需要以下信息
- Cloudflare注册的电子邮件
- Cloudflare全局API密钥
- 域名必须通过Cloudflare解析到当前服务器
**如何获取Cloudflare全局API密钥**
1. 在终端中运行 `x-ui` 命令,然后选择 `Cloudflare SSL证书`
2. 访问链接:[Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens)。
3. 点击“查看全局API密钥”参见下图
![](media/APIKey1.PNG)
4. 您可能需要重新验证您的账户。之后将显示API密钥参见下图
![](media/APIKey2.png)
使用时,只需输入您的 `域名``电子邮件``API密钥`。如下图所示:
![](media/DetailEnter.png)
</details>
## 手动安装 & 升级
<details>
<summary>点击查看 手动安装 & 升级</summary>
#### 使用
1. 若要将最新版本的压缩包直接下载到服务器,请运行以下命令:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
*) XUI_ARCH="amd64" ;;
esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. 下载压缩包后,执行以下命令安装或升级 x-ui
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
*) XUI_ARCH="amd64" ;;
esac
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
## 通过Docker安装
<details>
<summary>点击查看 通过Docker安装</summary>
#### 使用
1. 安装Docker
```sh
bash <(curl -sSL https://get.docker.com)
```
2. 克隆仓库:
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. 运行服务:
```sh
docker compose up -d
```
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
更新至最新版本
```sh
cd 3x-ui
docker compose down
docker compose pull 3x-ui
docker compose up -d
```
从Docker中删除3x-ui
```sh
docker stop 3x-ui
docker rm 3x-ui
cd --
rm -r 3x-ui
```
</details>
## Nginx 设置
<details>
<summary>点击查看 反向代理配置</summary>
#### Nginx反向代理
```nginx
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
#### Nginx子路径
- 确保 `/sub` 面板设置中的"面板url根路径"一致
- 面板设置中的 `url` 需要以 `/` 结尾
```nginx
location /sub {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
</details>
## 建议使用的操作系统
- Ubuntu 22.04+
- Debian 12+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 9.5+
- Rocky Linux 9.5+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Virtuozzo Linux 8+
- Windows x64
## 支持的架构和设备
<details>
<summary>点击查看 支持的架构和设备</summary>
我们的平台提供与各种架构和设备的兼容性,确保在各种计算环境中的灵活性。以下是我们支持的关键架构:
- **amd64**: 这种流行的架构是个人计算机和服务器的标准,可以无缝地适应大多数现代操作系统。
- **x86 / i386**: 这种架构在台式机和笔记本电脑中被广泛采用,得到了众多操作系统和应用程序的广泛支持,包括但不限于 Windows、macOS 和 Linux 系统。
- **armv8 / arm64 / aarch64**: 这种架构专为智能手机和平板电脑等当代移动和嵌入式设备量身定制,以 Raspberry Pi 4、Raspberry Pi 3、Raspberry Pi Zero 2/Zero 2 W、Orange Pi 3 LTS 等设备为例。
- **armv7 / arm / arm32**: 作为较旧的移动和嵌入式设备的架构它仍然广泛用于Orange Pi Zero LTS、Orange Pi PC Plus、Raspberry Pi 2等设备。
- **armv6 / arm / arm32**: 这种架构面向非常老旧的嵌入式设备虽然不太普遍但仍在使用中。Raspberry Pi 1、Raspberry Pi Zero/Zero W 等设备都依赖于这种架构。
- **armv5 / arm / arm32**: 它是一种主要与早期嵌入式系统相关的旧架构,目前不太常见,但仍可能出现在早期 Raspberry Pi 版本和一些旧智能手机等传统设备中。
</details>
## Languages
- Arabic (阿拉伯)
- English英语
- Persian波斯语
- Traditional Chinese繁体中文
- Simplified Chinese简体中文
- Japanese日语
- Russian俄语
- Vietnamese越南语
- Spanish西班牙语
- Indonesian印尼语
- Ukrainian乌克兰语
- Turkish土耳其语
- Português (Brazil)(葡萄牙语(巴西))
## Features
- 系统状态监控
- 在所有入站和客户端中搜索
- 深色/浅色主题
- 支持多用户和多协议
- 支持多种协议,包括 VMess、VLESS、Trojan、Shadowsocks、Dokodemo-door、Socks、HTTP、wireguard
- 支持 XTLS 原生协议,包括 RPRX-Direct、Vision、REALITY
- 流量统计、流量限制、过期时间限制
- 可自定义的 Xray配置模板
- 支持HTTPS访问面板自建域名+SSL证书
- 支持一键式SSL证书申请和自动续费
- 更多高级配置项目请参考面板
- 修复了 API 路由(用户设置将使用 API 创建)
- 支持通过面板中提供的不同项目更改配置。
- 支持从面板导出/导入数据库
## 默认面板设置
<details>
<summary>点击查看默认设置详情</summary>
### 用户名、密码、端口和 Web Base Path
如果您选择不修改这些设置,它们将随机生成(不适用于 Docker
**Docker 的默认设置:**
- **用户名:** admin
- **密码:** admin
- **端口:** 2053
### 数据库管理:
您可以直接在面板中方便地进行数据库备份和还原。
- **数据库路径:**
- `/etc/x-ui/x-ui.db`
### Web 基础路径
1. **重置 Web 基础路径:**
- 打开终端。
- 运行 `x-ui` 命令。
- 选择 `重置 Web 基础路径` 选项。
2. **生成或自定义路径:**
- 路径将会随机生成,或者您可以输入自定义路径。
3. **查看当前设置:**
- 要查看当前设置,请在终端中使用 `x-ui settings` 命令,或在 `x-ui` 面板中点击 `查看当前设置`。
### 安全建议:
- 为了提高安全性建议在URL结构中使用一个长的随机词。
**示例:**
- `http://ip:port/*webbasepath*/panel`
- `http://domain:port/*webbasepath*/panel`
</details>
## WARP 配置
<details>
<summary>点击查看 WARP 配置详情</summary>
#### 使用方法
**对于 `v2.1.0` 及之后的版本:**
WARP 已内置,无需额外安装。只需在面板中开启相关配置即可。
</details>
## IP 限制
<details>
<summary>点击查看 IP 限制详情</summary>
#### 使用方法
**注意:** 当使用 IP 隧道时IP 限制将无法正常工作。
- **对于 `v1.6.1` 及之前的版本:**
- IP 限制功能已内置于面板中。
**对于 `v1.7.0` 及更新的版本:**
要启用 IP 限制功能,您需要安装 `fail2ban` 及其所需的文件,步骤如下:
1. 在终端中运行 `x-ui` 命令,然后选择 `IP 限制管理`。
2. 您将看到以下选项:
- **更改封禁时长:** 调整封禁时长。
- **解除所有封禁:** 解除当前的所有封禁。
- **查看日志:** 查看日志。
- **Fail2ban 状态:** 检查 `fail2ban` 的状态。
- **重启 Fail2ban:** 重启 `fail2ban` 服务。
- **卸载 Fail2ban:** 卸载带有配置的 Fail2ban。
3. 在面板中通过设置 `Xray 配置/log/访问日志` 为 `./access.log` 添加访问日志路径,然后保存并重启 Xray。
- **对于 `v2.1.3` 之前的版本:**
- 您需要在 Xray 配置中手动设置访问日志路径:
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
- **对于 `v2.1.3` 及之后的版本:**
- 面板中直接提供了配置 `access.log` 的选项。
</details>
## Telegram 机器人
<details>
<summary>点击查看 Telegram 机器人</summary>
#### 使用
Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备份、系统状态、客户端信息等通知和功能。要使用机器人,您需要在面板中设置机器人相关参数,包括:
- 电报令牌
- 管理员聊天 ID
- 通知时间cron 语法)
- 到期日期通知
- 流量上限通知
- 数据库备份
- CPU 负载通知
**参考:**
- `30 \* \* \* \* \*` - 在每个点的 30 秒处通知
- `0 \*/10 \* \* \* \*` - 每 10 分钟的第一秒通知
- `@hourly` - 每小时通知
- `@daily` - 每天通知 (00:00)
- `@weekly` - 每周通知
- `@every 8h` - 每8小时通知
### Telegram Bot 功能
- 定期报告
- 登录通知
- CPU 阈值通知
- 提前报告的过期时间和流量阈值
- 如果将客户的电报用户名添加到用户的配置中,则支持客户端报告菜单
- 支持使用UUIDVMESS/VLESS或密码TROJAN搜索报文流量报告 - 匿名
- 基于菜单的机器人
- 通过电子邮件搜索客户端(仅限管理员)
- 检查所有入库
- 检查服务器状态
- 检查耗尽的用户
- 根据请求和定期报告接收备份
- 多语言机器人
### 注册 Telegram bot
- 与 [Botfather](https://t.me/BotFather) 对话:
![Botfather](./media/botfather.png)
- 使用 /newbot 创建新机器人你需要提供机器人名称以及用户名注意名称中末尾要包含“bot”
![创建机器人](./media/newbot.png)
- 启动您刚刚创建的机器人。可以在此处找到机器人的链接。
![令牌](./media/token.png)
- 输入您的面板并配置 Telegram 机器人设置,如下所示:
![面板设置](./media/panel-bot-config.png)
在输入字段编号 3 中输入机器人令牌。
在输入字段编号 4 中输入用户 ID。具有此 id 的 Telegram 帐户将是机器人管理员。 (您可以输入多个,只需将它们用“ ,”分开即可)
- 如何获取TG ID? 使用 [bot](https://t.me/useridinfobot) 启动机器人,它会给你 Telegram 用户 ID。
![用户 ID](./media/user-id.png)
</details>
## API 路由
<details>
<summary>点击查看 API 路由</summary>
#### 使用
- [API 文档](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` 使用 `POST` 用户名称 & 密码: `{username: '', password: ''}` 登录
- `/panel/api/inbounds` 以下操作的基础:
| 方法 | 路径 | 操作 |
| :----: | ---------------------------------- | --------------------------------- |
| `GET` | `"/list"` | 获取所有入站 |
| `GET` | `"/get/:id"` | 获取所有入站以及inbound.id |
| `GET` | `"/getClientTraffics/:email"` | 通过电子邮件获取客户端流量 |
| `GET` | `"/createbackup"` | Telegram 机器人向管理员发送备份 |
| `POST` | `"/add"` | 添加入站 |
| `POST` | `"/del/:id"` | 删除入站 |
| `POST` | `"/update/:id"` | 更新入站 |
| `POST` | `"/clientIps/:email"` | 客户端 IP 地址 |
| `POST` | `"/clearClientIps/:email"` | 清除客户端 IP 地址 |
| `POST` | `"/addClient"` | 将客户端添加到入站 |
| `POST` | `"/:id/delClient/:clientId"` | 通过 clientId\* 删除客户端 |
| `POST` | `"/updateClient/:clientId"` | 通过 clientId\* 更新客户端 |
| `POST` | `"/:id/resetClientTraffic/:email"` | 重置客户端的流量 |
| `POST` | `"/resetAllTraffics"` | 重置所有入站的流量 |
| `POST` | `"/resetAllClientTraffics/:id"` | 重置入站中所有客户端的流量 |
| `POST` | `"/delDepletedClients/:id"` | 删除入站耗尽的客户端 -1 all |
| `POST` | `"/onlines"` | 获取在线用户 电子邮件列表 |
\*- `clientId` 项应该使用下列数据
- `client.id` VMESS and VLESS
- `client.password` TROJAN
- `client.email` Shadowsocks
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
</details>
## 环境变量
<details>
<summary>点击查看 环境变量</summary>
#### Usage
| 变量 | Type | 默认 |
| -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
例子:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
## 预览
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/03-add-inbound-dark.png">
<img alt="3x-ui" src="./media/03-add-inbound-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/04-add-client-dark.png">
<img alt="3x-ui" src="./media/04-add-client-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/05-settings-dark.png">
<img alt="3x-ui" src="./media/05-settings-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/06-configs-dark.png">
<img alt="3x-ui" src="./media/06-configs-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture>
## 特别感谢
- [alireza0](https://github.com/alireza0/)
## 致谢
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (License: **GPL-3.0**): _This repository contains automatically updated V2Ray routing rules based on data on blocked domains and addresses in Russia._
## Star趋势
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)

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.0
2.6.7

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,6 @@ 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"`
}

View File

@@ -1,14 +1,16 @@
services:
3x-ui:
image: ghcr.io/mhsanaei/3x-ui:latest
container_name: 3x-ui
hostname: yourhostname
3xui:
build:
context: .
dockerfile: ./Dockerfile
container_name: 3xui_app
# hostname: yourhostname <- optional
volumes:
- $PWD/db/:/etc/x-ui/
- $PWD/cert/:/root/cert/
environment:
XRAY_VMESS_AEAD_FORCED: "false"
X_UI_ENABLE_FAIL2BAN: "true"
XUI_ENABLE_FAIL2BAN: "true"
tty: true
network_mode: host
restart: unless-stopped

90
go.mod
View File

@@ -1,48 +1,46 @@
module x-ui
go 1.24.3
go 1.25.0
require (
github.com/gin-contrib/gzip v1.2.3
github.com/gin-contrib/sessions v1.0.3
github.com/gin-gonic/gin v1.10.0
github.com/gin-contrib/sessions v1.0.4
github.com/gin-gonic/gin v1.10.1
github.com/goccy/go-json v0.10.5
github.com/google/uuid v1.6.0
github.com/mymmrac/telego v0.32.0
github.com/joho/godotenv v1.5.1
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.4
github.com/valyala/fasthttp v1.62.0
github.com/shirou/gopsutil/v4 v4.25.7
github.com/valyala/fasthttp v1.65.0
github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.250306.1-0.20250516121834-800b8b50cc01
github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5
go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.38.0
golang.org/x/text v0.25.0
google.golang.org/grpc v1.72.1
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12
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.2
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/sonic v1.14.0 // 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.3 // indirect
github.com/fasthttp/router v1.5.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/ebitengine/purego v0.8.4 // 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.26.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // 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
@@ -51,52 +49,52 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // 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.51.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.5 // indirect
github.com/sagernet/sing-shadowsocks v0.2.8 // 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
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20250516070713-4df2ec9a5b47 // indirect
github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f // 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.17.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/time v0.12.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-20250409194420-de1ac958c67a // 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
)

219
go.sum
View File

@@ -1,43 +1,38 @@
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.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/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/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
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=
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/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/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
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=
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
github.com/gin-contrib/sessions v1.0.3 h1:AZ4j0AalLsGqdrKNbbrKcXx9OJZqViirvNGsJTxcQps=
github.com/gin-contrib/sessions v1.0.3/go.mod h1:5i4XMx4KPtQihnzxEqG9u1K446lO3G19jAi2GtbfsAI=
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/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/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.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=
@@ -49,10 +44,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.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/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/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 +59,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-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/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=
@@ -84,41 +75,39 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.18.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.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/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=
@@ -131,55 +120,50 @@ 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.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
github.com/quic-go/quic-go v0.51.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.5 h1:gNMwZCLPqR+4e0g6dwi0sSsrvOmoMjpZgqxKsuJZatc=
github.com/sagernet/sing v0.7.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
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.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
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=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
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.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg=
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=
@@ -188,68 +172,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-20250516070713-4df2ec9a5b47 h1:9aJWkgWBwZ83l3j7+hBh3SurvRKuNfCgsSner5n6BcM=
github.com/xtls/reality v0.0.0-20250516070713-4df2ec9a5b47/go.mod h1:bJdU3ExzfUlY40Xxfibq3THW9IHiE8mHu/tEzud5JWM=
github.com/xtls/xray-core v1.250306.1-0.20250516121834-800b8b50cc01 h1:zthqg0MKk0yJBguuUmE+/Ya0nXGEz8OV7YucmkT2zII=
github.com/xtls/xray-core v1.250306.1-0.20250516121834-800b8b50cc01/go.mod h1:BNFvL6I5sEaw1bZELtteqijPEugqfQaG+dH75gSaHrc=
github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f h1:o1Kryl9qEYYzNep9RId9DM1kBn8tBrcK5UJnti/l0NI=
github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5 h1:rBqCVgic8yIUVHB4h26K8JNuwJuNj45egsdXxwEvA7E=
github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5/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.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
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.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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-20250409194420-de1ac958c67a h1:GIqLhp/cYUkuGuiT+vJk8vhOP86L4+SP5j8yXgeVpvI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
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=
@@ -259,12 +243,11 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
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/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
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,25 +39,12 @@ 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)
apt-get update && apt-get install -y -q wget curl tar tzdata
;;
centos | almalinux | rocky | ol)
centos | rhel | almalinux | rocky | ol)
yum -y update && yum install -y -q wget curl tar tzdata
;;
fedora | amzn | virtuozzo)
@@ -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,11 +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 https://api.ipify.org)
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)
@@ -112,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}"
@@ -141,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
@@ -171,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

22
main.go
View File

@@ -13,11 +13,12 @@ import (
"x-ui/database"
"x-ui/logger"
"x-ui/sub"
"x-ui/util/crypto"
"x-ui/web"
"x-ui/web/global"
"x-ui/web/service"
"x-ui/util/crypto"
"github.com/joho/godotenv"
"github.com/op/go-logging"
)
@@ -39,6 +40,8 @@ func runWebServer() {
log.Fatalf("Unknown log level: %v", config.GetLogLevel())
}
godotenv.Load()
err := database.InitDB(config.GetDBPath())
if err != nil {
log.Fatalf("Error initializing database: %v", err)
@@ -229,7 +232,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
}
}
func updateSetting(port int, username string, password string, webBasePath string, listenIP string) {
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println("Database initialization failed:", err)
@@ -266,6 +269,17 @@ func updateSetting(port int, username string, password string, webBasePath strin
}
}
if resetTwoFactor {
err := settingService.SetTwoFactorEnable(false)
if err != nil {
fmt.Println("Failed to reset two-factor authentication:", err)
} else {
settingService.SetTwoFactorToken("")
fmt.Println("Two-factor authentication reset successfully")
}
}
if listenIP != "" {
err := settingService.SetListen(listenIP)
if err != nil {
@@ -373,6 +387,7 @@ func main() {
var reset bool
var show bool
var getCert bool
var resetTwoFactor bool
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
settingCmd.BoolVar(&show, "show", false, "Display current settings")
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
@@ -380,6 +395,7 @@ func main() {
settingCmd.StringVar(&password, "password", "", "Set login password")
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
settingCmd.StringVar(&listenIP, "listenIP", "", "set panel listenIP IP")
settingCmd.BoolVar(&resetTwoFactor, "resetTwoFactor", false, "Reset two-factor authentication settings")
settingCmd.BoolVar(&getListen, "getListen", false, "Display current panel listenIP IP")
settingCmd.BoolVar(&getCert, "getCert", false, "Display current certificate settings")
settingCmd.StringVar(&webCertFile, "webCert", "", "Set path to public key file for panel")
@@ -424,7 +440,7 @@ func main() {
if reset {
resetSetting()
} else {
updateSetting(port, username, password, webBasePath, listenIP)
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor)
}
if show {
showSetting(show)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 206 KiB

View File

@@ -209,9 +209,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 +264,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)

View File

@@ -437,6 +437,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 +632,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

@@ -559,7 +559,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 +575,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 +596,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 +609,8 @@ class TlsStreamSettings extends XrayCommonClass {
json.enableSessionResumption,
certs,
json.alpn,
json.echServerKeys,
json.echForceQuery,
settings,
);
}
@@ -621,6 +627,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 +641,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
keyFile = '',
certificate = '',
key = '',
ocspStapling = 3600,
oneTimeLoading = false,
usage = USAGE_OPTION.ENCIPHERMENT,
buildChain = false,
@@ -644,7 +651,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 +662,6 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
true,
json.certificateFile,
json.keyFile, '', '',
json.ocspStapling,
json.oneTimeLoading,
json.usage,
json.buildChain,
@@ -666,7 +671,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 +683,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 +691,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 +703,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 +731,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 +747,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 +762,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 +772,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 +788,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 +803,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 +819,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
json.fingerprint,
json.serverName,
json.spiderX,
json.mldsa65Verify
);
}
toJson() {
@@ -814,6 +828,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
fingerprint: this.fingerprint,
serverName: this.serverName,
spiderX: this.spiderX,
mldsa65Verify: this.mldsa65Verify
};
}
};
@@ -1027,27 +1042,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 +1051,6 @@ class Inbound extends XrayCommonClass {
streamSettings = new StreamSettings(),
tag = '',
sniffing = new Sniffing(),
allocate = new Allocate(),
clientStats = '',
) {
super();
@@ -1068,7 +1061,6 @@ class Inbound extends XrayCommonClass {
this.stream = streamSettings;
this.tag = tag;
this.sniffing = sniffing;
this.allocate = allocate;
this.clientStats = clientStats;
}
getClientStats() {
@@ -1233,7 +1225,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) {
@@ -1249,7 +1240,6 @@ class Inbound extends XrayCommonClass {
id: clientId,
scy: security,
net: this.stream.network,
type: 'none',
tls: tls,
};
const network = this.stream.network;
@@ -1284,7 +1274,7 @@ class Inbound extends XrayCommonClass {
const xhttp = this.stream.xhttp;
obj.path = xhttp.path;
obj.host = xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host');
obj.mode = xhttp.mode;
obj.type = xhttp.mode;
}
if (tls === 'tls') {
@@ -1367,6 +1357,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);
}
@@ -1386,6 +1379,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);
}
@@ -1463,6 +1459,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);
}
@@ -1541,6 +1540,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);
}
@@ -1560,6 +1562,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 {
@@ -1674,14 +1679,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 {
@@ -1692,7 +1696,6 @@ class Inbound extends XrayCommonClass {
streamSettings: streamSettings,
tag: this.tag,
sniffing: this.sniffing.toJson(),
allocate: this.allocate.toJson(),
clientStats: this.clientStats
};
}
@@ -1788,7 +1791,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;
@@ -1802,6 +1807,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 = {}) {
@@ -1817,6 +1824,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
);
}
get _expiryTime() {
@@ -1897,7 +1906,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;
@@ -1911,6 +1922,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 = {}) {
@@ -1926,6 +1939,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
);
}
@@ -2036,7 +2051,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;
@@ -2049,6 +2066,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() {
@@ -2063,6 +2082,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,
};
}
@@ -2078,6 +2099,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
);
}
@@ -2197,7 +2220,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;
@@ -2211,6 +2236,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() {
@@ -2226,6 +2253,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,
};
}
@@ -2242,6 +2271,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
);
}
@@ -2277,12 +2308,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;
}
@@ -2292,6 +2325,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
Protocols.DOKODEMO,
json.address,
json.port,
XrayCommonClass.toHeaders(json.portMap),
json.network,
json.followRedirect,
);
@@ -2301,6 +2335,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
};
}
};
@@ -616,11 +624,27 @@ class Outbound extends CommonClass {
}
canEnableMux() {
if (this.settings.flow && this.settings.flow != '') {
// Disable Mux if flow is set
if (this.settings.flow && this.settings.flow !== '') {
this.mux.enabled = false;
return false;
}
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.HTTP, Protocols.Socks].includes(this.protocol);
// Disable Mux if network is xhttp
if (this.stream.network === 'xhttp') {
this.mux.enabled = false;
return false;
}
// Allow Mux only for these protocols
return [
Protocols.VMess,
Protocols.VLESS,
Protocols.Trojan,
Protocols.Shadowsocks,
Protocols.HTTP,
Protocols.Socks
].includes(this.protocol);
}
hasVnext() {
@@ -762,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') {
@@ -771,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+)(.*)$/;
@@ -893,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 = {}) {
@@ -906,6 +934,7 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
json.packets,
json.length,
json.interval,
json.maxSplit
);
}
};
@@ -914,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 = {}) {
@@ -927,6 +958,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
json.type,
json.packet,
json.delay,
json.applyTo
);
}
@@ -935,6 +967,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
type: this.type,
packet: this.packet,
delay: this.delay,
applyTo: this.applyTo
};
}
};
@@ -962,7 +995,7 @@ Outbound.DNSSettings = class extends CommonClass {
network = 'udp',
address = '',
port = 53,
nonIPQuery = 'drop',
nonIPQuery = 'reject',
blockTypes = []
) {
super();

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

@@ -138,8 +138,14 @@ class RandomUtil {
}
}
static randomShadowsocksPassword() {
const array = new Uint8Array(32);
static randomShadowsocksPassword(method = SSMethods.BLAKE3_AES_256_GCM) {
let length = 32;
if ([SSMethods.BLAKE3_AES_128_GCM].includes(method)) {
length = 16;
}
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
@@ -789,6 +795,25 @@ class LanguageManager {
if (window.navigator) {
lang = window.navigator.language || window.navigator.userLanguage;
const simularLangs = [
["ar", this.supportedLanguages[0].value],
["fa", this.supportedLanguages[2].value],
["ja", this.supportedLanguages[5].value],
["ru", this.supportedLanguages[6].value],
["vi", this.supportedLanguages[7].value],
["es", this.supportedLanguages[8].value],
["id", this.supportedLanguages[9].value],
["uk", this.supportedLanguages[10].value],
["tr", this.supportedLanguages[11].value],
["pt", this.supportedLanguages[12].value],
]
simularLangs.forEach((pair) => {
if (lang === pair[0]) {
lang = pair[1];
}
});
if (LanguageManager.isSupportLanguage(lang)) {
CookieManager.setCookie("lang", lang, 150);
} else {

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,7 @@ 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", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
}
for _, route := range inboundRoutes {

View File

@@ -104,8 +104,12 @@ func (a *InboundController) addInbound(c *gin.Context) {
needRestart := false
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, err)
if err == nil && needRestart {
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -118,8 +122,12 @@ func (a *InboundController) delInbound(c *gin.Context) {
}
needRestart := true
needRestart, err = a.inboundService.DelInbound(id)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, err)
if err == nil && needRestart {
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -140,8 +148,12 @@ func (a *InboundController) updateInbound(c *gin.Context) {
}
needRestart := true
inbound, needRestart, err = a.inboundService.UpdateInbound(inbound)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, err)
if err == nil && needRestart {
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
@@ -327,3 +339,28 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
func (a *InboundController) onlines(c *gin.Context) {
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
}
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,16 @@ 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)
}
func (a *ServerController) refreshStatus() {
@@ -132,6 +137,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 +247,22 @@ 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)
}

View File

@@ -2,6 +2,7 @@ package entity
import (
"crypto/tls"
"math"
"net"
"strings"
"time"
@@ -38,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"`
@@ -78,11 +79,11 @@ func (s *AllSetting) CheckValid() error {
}
}
if s.WebPort <= 0 || s.WebPort > 65535 {
if s.WebPort <= 0 || s.WebPort > math.MaxUint16 {
return common.NewError("web port is not a valid port:", s.WebPort)
}
if s.SubPort <= 0 || s.SubPort > 65535 {
if s.SubPort <= 0 || s.SubPort > math.MaxUint16 {
return common.NewError("Sub port is not a valid port:", s.SubPort)
}

View File

@@ -1,31 +0,0 @@
{{define "head"}}
<head>
<meta charset="UTF-8">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex,nofollow">
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.min.css?{{ .cur_ver }}">
<style>
[v-cloak] {
display: none;
}
/* vazirmatn-regular - arabic_latin_latin-ext */
@font-face {
font-display: swap;
font-family: 'Vazirmatn';
font-style: normal;
font-weight: 400;
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol';
}
</style>
<title>{{ .host }} {{ i18n .title}}</title>
</head>
<div id="message"></div>
{{end}}

View File

@@ -1,14 +0,0 @@
{{define "js"}}
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
<script>
const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath;
</script>
{{end}}

58
web/html/common/page.html Normal file
View File

@@ -0,0 +1,58 @@
{{ define "page/head_start" }}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex,nofollow">
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.min.css?{{ .cur_ver }}">
<style>
[v-cloak] {
display: none;
}
/* vazirmatn-regular - arabic_latin_latin-ext */
@font-face {
font-display: swap;
font-family: 'Vazirmatn';
font-style: normal;
font-weight: 400;
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
</style>
<title>{{ .host }} {{ i18n .title}}</title>
{{ end }}
{{ define "page/head_end" }}
</head>
{{ end }}
{{ define "page/body_start" }}
<body>
<div id="message"></div>
{{ end }}
{{ define "page/body_scripts" }}
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
<script>
const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath;
</script>
{{ end }}
{{ define "page/body_end" }}
</body>
</html>
{{ end }}

View File

@@ -41,14 +41,28 @@
</template>
</template>
<template slot="client" slot-scope="text, client">
<a-tooltip>
<template slot="title">
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
</template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
</a-tooltip> [[ client.email ]]
<a-space direction="horizontal" :size="2">
<a-tooltip>
<template slot="title">
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
</template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
</a-tooltip>
<a-space direction="vertical" :size="2">
<span class="client-email">[[ client.email ]]</span>
<template v-if="client.comment && client.comment.trim()">
<a-tooltip v-if="client.comment.length > 50" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">
[[ client.comment ]]
</template>
<span class="client-comment">[[ client.comment.substring(0, 47) + '...' ]]</span>
</a-tooltip>
<span v-else class="client-comment">[[ client.comment ]]</span>
</template>
</a-space>
</a-space>
</template>
<template slot="traffic" slot-scope="text, client">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
@@ -84,6 +98,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">
@@ -264,4 +282,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

@@ -22,7 +22,7 @@
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "password" }}
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS"@click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"></a-icon>
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)"type="sync"> </a-icon>
</a-tooltip>
</template>
@@ -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' >
@@ -444,13 +452,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

@@ -31,7 +31,7 @@
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template> Password <a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"></a-icon>
</template> Password <a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.settings.password"></a-input>

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

@@ -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,6 +103,21 @@
<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 -->

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,9 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
{{ template "page/head_start" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
.ant-card-hoverable {
margin-inline: 0.3rem;
}
.ant-alert-error {
margin-inline: 0.3rem;
}
}
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-card-dark h2 {
color: var(--dark-color-text-primary);
@@ -79,371 +68,434 @@
}
}
</style>
{{ template "page/head_end" .}}
<body>
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar>
<a-layout id="content-layout">
<a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip">
<transition name="list" appear>
<a-alert type="error" v-if="showAlert" :style="{ marginBottom: '10px' }"
message='{{ i18n "secAlertTitle" }}'
color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable>
</a-alert>
</transition>
<transition name="list" appear>
<template>
<a-row v-if="!status.isLoaded">
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin>
{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar>
<a-layout id="content-layout">
<a-layout-content>
<a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip">
<transition name="list" appear>
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
message='{{ i18n "secAlertTitle" }}'
color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable>
</a-alert>
</transition>
<transition name="list" appear>
<template>
<a-row v-if="!loadingStates.fetched">
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card>
</a-row>
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
<a-col>
<a-card hoverable>
<a-row :gutter="[0, isMobile ? 16 : 0]">
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color"
:percent="status.cpu.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.cpu" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]]
<a-tooltip>
<a-icon type="area-chart"></a-icon>
<template slot="title">
<div><b>{{ i18n "pages.index.logicalProcessors" }}:</b> [[ (status.logicalPro) ]]</div>
<div><b>{{ i18n "pages.index.frequency" }}:</b> [[ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
</template>
</a-tooltip>
</div>
</a-col>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color"
:percent="status.mem.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.memory"}}:</b> [[ SizeFormatter.sizeFormat(status.mem.current) ]] / [[ SizeFormatter.sizeFormat(status.mem.total) ]]
</div>
</a-col>
</a-row>
</a-col>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color"
:percent="status.swap.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]]
</div>
</a-col>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color"
:percent="status.disk.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.storage"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]] / [[ SizeFormatter.sizeFormat(status.disk.total) ]]
</div>
</a-col>
</a-row>
</a-col>
</a-row>
</a-card>
</a-row>
<a-row v-else>
<a-row>
<a-card hoverable>
<a-row>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color"
:percent="status.cpu.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.cpu" }}:</b> [[ CPUFormatter.cpuCoreFormat(status.cpuCores) ]]
<a-tooltip>
<a-icon type="area-chart"></a-icon>
<template slot="title">
<div><b>{{ i18n "pages.index.logicalProcessors" }}:</b> [[ (status.logicalPro) ]]</div>
<div><b>{{ i18n "pages.index.frequency" }}:</b> [[ CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
</template>
</a-tooltip>
</div>
</a-col>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color"
:percent="status.mem.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.memory"}}:</b> [[ SizeFormatter.sizeFormat(status.mem.current) ]] / [[ SizeFormatter.sizeFormat(status.mem.total) ]]
</div>
</a-col>
</a-row>
</a-col>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color"
:percent="status.swap.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]]
</div>
</a-col>
<a-col :span="12" :style="{ textAlign: 'center' }">
<a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color"
:percent="status.disk.percent"></a-progress>
<div>
<b>{{ i18n "pages.index.storage"}}:</b> [[ SizeFormatter.sizeFormat(status.disk.current) ]] / [[ SizeFormatter.sizeFormat(status.disk.total) ]]
</div>
</a-col>
</a-row>
</a-col>
</a-row>
</a-card>
</a-row>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<template #title>
<a-space direction="horizontal">
<span>{{ i18n "pages.index.xrayStatus" }}</span>
<a-tag v-if="isMobile && status.xray.version != 'Unknown'" color="green">
v[[ status.xray.version ]]
</a-tag>
</a-space>
</a-col>
<a-col :sm="24" :lg="12">
<a-card hoverable>
<template #title>
<a-space direction="horizontal">
<span>{{ i18n "pages.index.xrayStatus" }}</span>
<a-tag v-if="isMobile && status.xray.version != 'Unknown'" color="green">
v[[ status.xray.version ]]
</a-tag>
</a-space>
</template>
<template #extra>
<template v-if="status.xray.state != 'error'">
<a-badge status="processing" class="running-animation" :text="status.xray.stateMsg" :color="status.xray.color"/>
</template>
<template #extra>
<template v-if="status.xray.state != 'error'">
<a-badge status="processing" class="running-animation" :text="status.xray.stateMsg" :color="status.xray.color"/>
</template>
<template v-else>
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<span slot="title">
<a-row type="flex" align="middle" justify="space-between">
<a-col>
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
</a-col>
<a-col>
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-tag>
</a-col>
</a-row>
</span>
<template slot="content">
<span :style="{ maxWidth: '400px' }" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
</template>
<a-badge :text="status.xray.stateMsg" :color="status.xray.color"/>
</a-popover>
</template>
</template>
<template #actions>
<a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }">
<a-icon type="poweroff"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
</a-space>
<a-space direction="horizontal" @click="restartXrayService" :style="{ justifyContent: 'center' }">
<a-icon type="reload"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
</a-space>
<a-space direction="horizontal" @click="openSelectV2rayVersion" :style="{ justifyContent: 'center' }">
<a-icon type="tool"></a-icon>
<span v-if="!isMobile">
[[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]]
<template v-else>
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<span slot="title">
<a-row type="flex" align="middle" justify="space-between">
<a-col>
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
</a-col>
<a-col>
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-icon>
</a-col>
</a-row>
</span>
</a-space>
<template slot="content">
<span :style="{ maxWidth: '400px' }" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
</template>
<a-badge :text="status.xray.stateMsg" :color="status.xray.color"/>
</a-popover>
</template>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "menu.link" }}' hoverable>
<template #actions>
<a-space direction="horizontal" @click="openLogs()" :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="openConfig" :style="{ justifyContent: 'center' }">
<a-icon type="control"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
</a-space>
<a-space direction="horizontal" @click="openBackup" :style="{ justifyContent: 'center' }">
<a-icon type="cloud-server"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
</a-space>
</template>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='3X-UI' hoverable>
<a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
<a rel="noopener" href="https://t.me/XrayUI" target="_blank"><a-tag color="green">@XrayUI</a-tag></a>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.operationHours" }}' hoverable>
<a-tag :color="status.xray.color">Xray: [[ TimeFormatter.formatSecond(status.appStats.uptime) ]]</a-tag>
<a-tag color="green">OS: [[ TimeFormatter.formatSecond(status.uptime) ]]</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.systemLoad" }}' hoverable>
</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>
</a-space>
<a-space direction="horizontal" @click="restartXrayService" :style="{ justifyContent: 'center' }">
<a-icon type="reload"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
</a-space>
<a-space direction="horizontal" @click="openSelectV2rayVersion" :style="{ justifyContent: 'center' }">
<a-icon type="tool"></a-icon>
<span v-if="!isMobile">
[[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]]
</span>
</a-space>
</template>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "menu.link" }}' hoverable>
<template #actions>
<a-space direction="horizontal" @click="openLogs()" :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="openConfig" :style="{ justifyContent: 'center' }">
<a-icon type="control"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
</a-space>
<a-space direction="horizontal" @click="openBackup" :style="{ justifyContent: 'center' }">
<a-icon type="cloud-server"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
</a-space>
</template>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='3X-UI' hoverable>
<a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank">
<a-tag color="green">
<a-tooltip>
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<template slot="title">
{{ i18n "pages.index.systemLoadDesc" }}
</template>
</a-tooltip>
<span>v{{ .cur_ver }}</span>
</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "usage"}}' hoverable>
<a-tag color="green"> {{ i18n "pages.index.memory" }}: [[ SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag>
<a-tag color="green"> {{ i18n "pages.index.threads" }}: [[ status.appStats.threads ]] </a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.overallSpeed" }}' hoverable>
<a-row :gutter="isMobile ? [8,8] : 0">
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.upload" }}' :value="SizeFormatter.sizeFormat(status.netIO.up)">
<template #prefix>
<a-icon type="arrow-up" />
</template>
<template #suffix>
/s
</template>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.download" }}' :value="SizeFormatter.sizeFormat(status.netIO.down)">
<template #prefix>
<a-icon type="arrow-down" />
</template>
<template #suffix>
/s
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.totalData" }}' hoverable>
<a-row :gutter="isMobile ? [8,8] : 0">
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.sent" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.sent)">
<template #prefix>
<a-icon type="cloud-upload" />
</template>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.received" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.recv)">
<template #prefix>
<a-icon type="cloud-download" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.ipAddresses" }}' hoverable>
<template #extra>
<a-tooltip>
<template #title>
{{ i18n "pages.index.toggleIpVisibility" }}
</a>
<a rel="noopener" href="https://t.me/XrayUI" target="_blank">
<a-tag color="green">
<span>@XrayUI</span>
</a-tag>
</a>
<a rel="noopener" href="https://github.com/MHSanaei/3x-ui/wiki" target="_blank">
<a-tag color="purple">
<span>{{ i18n "pages.index.documentation" }}</span>
</a-tag>
</a>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.operationHours" }}' hoverable>
<a-tag :color="status.xray.color">Xray: [[ TimeFormatter.formatSecond(status.appStats.uptime) ]]</a-tag>
<a-tag color="green">OS: [[ TimeFormatter.formatSecond(status.uptime) ]]</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.systemLoad" }}' hoverable>
<a-tag color="green">
<a-tooltip>
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<template slot="title">
{{ i18n "pages.index.systemLoadDesc" }}
</template>
</a-tooltip>
</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "usage"}}' hoverable>
<a-tag color="green"> {{ i18n "pages.index.memory" }}: [[ SizeFormatter.sizeFormat(status.appStats.mem) ]] </a-tag>
<a-tag color="green"> {{ i18n "pages.index.threads" }}: [[ status.appStats.threads ]] </a-tag>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.overallSpeed" }}' hoverable>
<a-row :gutter="isMobile ? [8,8] : 0">
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.upload" }}' :value="SizeFormatter.sizeFormat(status.netIO.up)">
<template #prefix>
<a-icon type="arrow-up" />
</template>
<a-icon :type="showIp ? 'eye' : 'eye-invisible'" :style="{ fontSize: '1rem' }" @click="showIp = !showIp"></a-icon>
</a-tooltip>
</template>
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
<a-col :span="isMobile ? 24 : 12">
<a-custom-statistic title="IPv4" :value="status.publicIP.ipv4">
<template #prefix>
<a-icon type="global" />
</template>
</a-custom-statistic>
</a-col>
<a-col :span="isMobile ? 24 : 12">
<a-custom-statistic title="IPv6" :value="status.publicIP.ipv6">
<template #prefix>
<a-icon type="global" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.connectionCount" }}' hoverable>
<a-row :gutter="isMobile ? [8,8] : 0">
<a-col :span="12">
<a-custom-statistic title="TCP" :value="status.tcpCount">
<template #prefix>
<a-icon type="swap" />
</template>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title="UDP" :value="status.udpCount">
<template #prefix>
<a-icon type="swap" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
</template>
</transition>
</a-spin>
</a-layout-content>
</a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true"
@ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
<a-collapse default-active-key="1">
<a-collapse-panel key="1" header='Xray'>
<a-alert type="warning" :style="{ marginBottom: '12px', width: '100%' }" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
<a-list class="ant-version-list" bordered :style="{ width: '100%' }">
<a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag>
<a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio>
</a-list-item>
</a-list>
</a-collapse-panel>
<a-collapse-panel key="2" header='Geofiles'>
<a-list class="ant-version-list" bordered :style="{ width: '100%' }">
<a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
<a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/>
</a-list-item>
</a-list>
</a-collapse-panel>
</a-collapse>
</a-modal>
<a-modal id="log-modal" v-model="logModal.visible"
:closable="true" @cancel="() => logModal.visible = false"
:class="themeSwitcher.currentTheme"
width="800px" footer="">
<template slot="title">
{{ i18n "pages.index.logs" }}
<a-icon :spin="logModal.loading"
type="sync"
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
:disabled="logModal.loading"
@click="openLogs()">
</a-icon>
</template>
<a-form layout="inline">
<a-form-item :style="{ marginRight: '0.5rem' }">
<a-input-group compact>
<a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }"
@change="openLogs()" :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-select size="small" v-model="logModal.level" :style="{ width: '95px' }"
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="debug">Debug</a-select-option>
<a-select-option value="info">Info</a-select-option>
<a-select-option value="notice">Notice</a-select-option>
<a-select-option value="warning">Warning</a-select-option>
<a-select-option value="err">Error</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item>
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
</a-form-item>
<a-form-item :style="{ float: 'right' }">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.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="logModal.formattedLogs"></div>
</a-modal>
<a-modal id="backup-modal"
v-model="backupModal.visible"
title='{{ i18n "pages.index.backupTitle" }}'
:closable="true"
footer=""
:class="themeSwitcher.currentTheme">
<a-list class="ant-backup-list" bordered :style="{ width: '100%' }">
<a-list-item class="ant-backup-list-item">
<a-list-item-meta>
<template #title>{{ i18n "pages.index.exportDatabase" }}</template>
<template #description>{{ i18n "pages.index.exportDatabaseDesc" }}</template>
</a-list-item-meta>
<a-button @click="exportDatabase()" type="primary" icon="download"/>
</a-list-item>
<a-list-item class="ant-backup-list-item">
<a-list-item-meta>
<template #title>{{ i18n "pages.index.importDatabase" }}</template>
<template #description>{{ i18n "pages.index.importDatabaseDesc" }}</template>
</a-list-item-meta>
<a-button @click="importDatabase()" type="primary" icon="upload" />
</a-list-item>
</a-list>
</a-modal>
<template #suffix>
/s
</template>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.download" }}' :value="SizeFormatter.sizeFormat(status.netIO.down)">
<template #prefix>
<a-icon type="arrow-down" />
</template>
<template #suffix>
/s
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.totalData" }}' hoverable>
<a-row :gutter="isMobile ? [8,8] : 0">
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.sent" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.sent)">
<template #prefix>
<a-icon type="cloud-upload" />
</template>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title='{{ i18n "pages.index.received" }}' :value="SizeFormatter.sizeFormat(status.netTraffic.recv)">
<template #prefix>
<a-icon type="cloud-download" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.ipAddresses" }}' hoverable>
<template #extra>
<a-tooltip :placement="isMobile ? 'topRight' : 'top'">
<template #title>
{{ i18n "pages.index.toggleIpVisibility" }}
</template>
<a-icon :type="showIp ? 'eye' : 'eye-invisible'" :style="{ fontSize: '1rem' }" @click="showIp = !showIp"></a-icon>
</a-tooltip>
</template>
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
<a-col :span="isMobile ? 24 : 12">
<a-custom-statistic title="IPv4" :value="status.publicIP.ipv4">
<template #prefix>
<a-icon type="global" />
</template>
</a-custom-statistic>
</a-col>
<a-col :span="isMobile ? 24 : 12">
<a-custom-statistic title="IPv6" :value="status.publicIP.ipv6">
<template #prefix>
<a-icon type="global" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :lg="12">
<a-card title='{{ i18n "pages.index.connectionCount" }}' hoverable>
<a-row :gutter="isMobile ? [8,8] : 0">
<a-col :span="12">
<a-custom-statistic title="TCP" :value="status.tcpCount">
<template #prefix>
<a-icon type="swap" />
</template>
</a-custom-statistic>
</a-col>
<a-col :span="12">
<a-custom-statistic title="UDP" :value="status.udpCount">
<template #prefix>
<a-icon type="swap" />
</template>
</a-custom-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
</template>
</transition>
</a-spin>
</a-layout-content>
</a-layout>
{{template "js" .}}
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' :closable="true"
@ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
<a-collapse default-active-key="1">
<a-collapse-panel key="1" header='Xray'>
<a-alert type="warning" :style="{ marginBottom: '12px', width: '100%' }" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
<a-list class="ant-version-list" bordered :style="{ width: '100%' }">
<a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag>
<a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio>
</a-list-item>
</a-list>
</a-collapse-panel>
<a-collapse-panel key="2" header='Geofiles'>
<a-list class="ant-version-list" bordered :style="{ width: '100%' }">
<a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
<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>
<a-modal id="log-modal" v-model="logModal.visible"
:closable="true" @cancel="() => logModal.visible = false"
:class="themeSwitcher.currentTheme"
width="800px" footer="">
<template slot="title">
{{ i18n "pages.index.logs" }}
<a-icon :spin="logModal.loading"
type="sync"
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
:disabled="logModal.loading"
@click="openLogs()">
</a-icon>
</template>
<a-form layout="inline">
<a-form-item :style="{ marginRight: '0.5rem' }">
<a-input-group compact>
<a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }"
@change="openLogs()" :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-select size="small" v-model="logModal.level" :style="{ width: '95px' }"
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="debug">Debug</a-select-option>
<a-select-option value="info">Info</a-select-option>
<a-select-option value="notice">Notice</a-select-option>
<a-select-option value="warning">Warning</a-select-option>
<a-select-option value="err">Error</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item>
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
</a-form-item>
<a-form-item :style="{ float: 'right' }">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.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="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" }}'
:closable="true"
footer=""
:class="themeSwitcher.currentTheme">
<a-list class="ant-backup-list" bordered :style="{ width: '100%' }">
<a-list-item class="ant-backup-list-item">
<a-list-item-meta>
<template #title>{{ i18n "pages.index.exportDatabase" }}</template>
<template #description>{{ i18n "pages.index.exportDatabaseDesc" }}</template>
</a-list-item-meta>
<a-button @click="exportDatabase()" type="primary" icon="download"/>
</a-list-item>
<a-list-item class="ant-backup-list-item">
<a-list-item-meta>
<template #title>{{ i18n "pages.index.importDatabase" }}</template>
<template #description>{{ i18n "pages.index.importDatabaseDesc" }}</template>
</a-list-item-meta>
<a-button @click="importDatabase()" type="primary" icon="upload" />
</a-list-item>
</a-list>
</a-modal>
</a-layout>
{{template "page/body_scripts" .}}
{{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}}
{{template "component/aCustomStatistic" .}}
@@ -476,7 +528,7 @@
}
class Status {
constructor(data, isLoaded = false) {
constructor(data) {
this.cpu = new CurTotal(0, 0);
this.cpuCores = 0;
this.logicalPro = 0;
@@ -500,7 +552,6 @@
return;
}
this.isLoaded = isLoaded;
this.cpu = new CurTotal(data.cpu, 100);
this.cpuCores = data.cpuCores;
this.logicalPro = data.logicalPro;
@@ -604,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() {
@@ -620,32 +725,41 @@
mixins: [MediaQueryMixin],
data: {
themeSwitcher,
loadingStates: {
fetched: false,
spinning: false
},
status: new Status(),
versionModal,
logModal,
xraylogModal,
backupModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
showAlert: false,
showIp: false
showIp: false,
ipLimitEnable: false,
},
methods: {
loading(spinning, tip = '{{ i18n "loading"}}') {
this.spinning = spinning;
this.loadingStates.spinning = spinning;
this.loadingTip = tip;
},
async getStatus() {
try {
const msg = await HttpUtil.post('/server/status');
if (msg.success) {
if (!this.loadingStates.fetched) {
this.loadingStates.fetched = true;
}
this.setStatus(msg.obj, true);
}
} catch (e) {
console.error("Failed to get status:", e);
}
},
setStatus(data, isLoaded = false) {
this.status = new Status(data, isLoaded);
setStatus(data) {
this.status = new Status(data);
},
async openSelectV2rayVersion() {
this.loading(true);
@@ -672,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);
},
});
@@ -712,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');
@@ -764,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();
@@ -775,5 +911,4 @@
},
});
</script>
</body>
</html>
{{ template "page/body_end" .}}

View File

@@ -1,6 +1,4 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
{{ template "page/head_start" .}}
<style>
html * {
-webkit-font-smoothing: antialiased;
@@ -447,174 +445,174 @@
margin: 2px 0 4px;
}
</style>
{{ template "page/head_end" .}}
<body>
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<transition name="list" appear>
<a-layout-content class="under" :style="{ minHeight: '0' }">
<div class="waves-header">
<div class="waves-inner-header"></div>
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" />
</g>
</svg>
</div>
<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>
</a-col>
</a-row>
</a-layout-content>
</transition>
</a-layout>
{{template "js" .}}
{{template "component/aThemeSwitch" .}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
themeSwitcher,
loading: false,
user: {
username: "",
password: "",
twoFactorCode: ""
},
twoFactorEnable: false,
lang: ""
{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<transition name="list" appear>
<a-layout-content class="under" :style="{ minHeight: '0' }">
<div class="waves-header">
<div class="waves-inner-header"></div>
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" />
</g>
</svg>
</div>
<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="one-time-code" 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>
</a-col>
</a-row>
</a-layout-content>
</transition>
</a-layout>
{{template "page/body_scripts" .}}
{{template "component/aThemeSwitch" .}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
themeSwitcher,
loading: false,
user: {
username: "",
password: "",
twoFactorCode: ""
},
async mounted() {
this.lang = LanguageManager.getLanguage();
this.twoFactorEnable = await this.getTwoFactorEnable();
twoFactorEnable: false,
lang: ""
},
async mounted() {
this.lang = LanguageManager.getLanguage();
this.twoFactorEnable = await this.getTwoFactorEnable();
},
methods: {
async login() {
this.loading = true;
const msg = await HttpUtil.post('/login', this.user);
this.loading = false;
if (msg.success) {
location.href = basePath + 'panel/';
}
},
methods: {
async login() {
this.loading = true;
const msg = await HttpUtil.post('/login', this.user);
this.loading = false;
if (msg.success) {
location.href = basePath + 'panel/';
}
},
async getTwoFactorEnable() {
this.loading = true;
const msg = await HttpUtil.post('/getTwoFactorEnable');
this.loading = false;
if (msg.success) {
this.twoFactorEnable = msg.obj;
return msg.obj;
}
},
async getTwoFactorEnable() {
this.loading = true;
const msg = await HttpUtil.post('/getTwoFactorEnable');
this.loading = false;
if (msg.success) {
this.twoFactorEnable = msg.obj;
return msg.obj;
}
},
});
},
});
document.addEventListener("DOMContentLoaded", function () {
var animationDelay = 2000;
initHeadline();
document.addEventListener("DOMContentLoaded", function () {
var animationDelay = 2000;
initHeadline();
function initHeadline() {
animateHeadline(document.querySelectorAll('.headline'));
}
function initHeadline() {
animateHeadline(document.querySelectorAll('.headline'));
}
function animateHeadline(headlines) {
var duration = animationDelay;
headlines.forEach(function (headline) {
setTimeout(function () {
hideWord(headline.querySelector('.is-visible'));
}, duration);
});
}
function hideWord(word) {
var nextWord = takeNext(word);
switchWord(word, nextWord);
function animateHeadline(headlines) {
var duration = animationDelay;
headlines.forEach(function (headline) {
setTimeout(function () {
hideWord(nextWord);
}, animationDelay);
}
hideWord(headline.querySelector('.is-visible'));
}, duration);
});
}
function takeNext(word) {
return word.nextElementSibling ? word.nextElementSibling : word.parentElement.firstElementChild;
}
function hideWord(word) {
var nextWord = takeNext(word);
switchWord(word, nextWord);
setTimeout(function () {
hideWord(nextWord);
}, animationDelay);
}
function switchWord(oldWord, newWord) {
oldWord.classList.remove('is-visible');
oldWord.classList.add('is-hidden');
newWord.classList.remove('is-hidden');
newWord.classList.add('is-visible');
}
});
</script>
</body>
</html>
function takeNext(word) {
return word.nextElementSibling ? word.nextElementSibling : word.parentElement.firstElementChild;
}
function switchWord(oldWord, newWord) {
oldWord.classList.remove('is-visible');
oldWord.classList.add('is-hidden');
newWord.classList.remove('is-hidden');
newWord.classList.add('is-visible');
}
});
</script>
{{ template "page/body_end" .}}

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

@@ -47,7 +47,7 @@
}
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
} else {
this.addClient(this.inbound.protocol, this.clients);
this.addClient(this.inbound, this.clients);
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
@@ -59,12 +59,12 @@
default: return client.id;
}
},
addClient(protocol, clients) {
switch (protocol) {
addClient(inbound, clients) {
switch (inbound.protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.VMESS());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method));
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method)));
default: return null;
}
},

View File

@@ -0,0 +1,110 @@
{{define "modals/dnsPresetsModal"}}
<a-modal id="dnsPresets-modal" v-model="dnsPresetsModal.visible" :title="dnsPresetsModal.title" :closable="true"
:mask-closable="false" :footer="null" :class="themeSwitcher.currentTheme">
<a-list class="ant-dns-presets-list" bordered :style="{ width: '100%' }">
<a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
<a-row justify="space-between" align="middle">
<a-col :span="12">
<a-space direction="vertical" size="small">
<span class="ant-dns-presets-list-name">[[ dns.name ]]</span>
<a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag>
</a-space>
</a-col>
<a-col :span="12" :style="{ textAlign: 'right' }">
<a-button type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
</a-col>
</a-row>
</a-list-item>
</a-list>
</a-modal>
<style>
.dark .ant-dns-presets-list {
border-color: var(--dark-color-stroke)
}
.dark .ant-dns-presets-list-name {
color: var(--dark-color-text-primary);
}
</style>
<script>
const dnsPresetsDatabase = [
{
name: 'Google DNS',
family: false,
data: [
"8.8.8.8",
"8.8.4.4",
"2001:4860:4860::8888",
"2001:4860:4860::8844"
]
},
{
name: 'Cloudflare DNS',
family: false,
data: [
"1.1.1.1",
"1.0.0.1",
"2606:4700:4700::1111",
"2606:4700:4700::1001"
]
},
{
name: 'Adguard DNS',
family: false,
data: [
"94.140.14.14",
"94.140.15.15",
"2a10:50c0::ad1:ff",
"2a10:50c0::ad2:ff"
]
},
{
name: 'Adguard Family DNS',
family: true,
data: [
"94.140.14.14",
"94.140.15.15",
"2a10:50c0::ad1:ff",
"2a10:50c0::ad2:ff"
]
},
{
name: 'Cloudflare Family DNS',
family: true,
data: [
"1.1.1.3",
"1.0.0.3",
"2606:4700:4700::1113",
"2606:4700:4700::1003"
]
}
]
const dnsPresetsModal = {
title: '',
visible: false,
selected: null,
install(selectedPreset) {
return ObjectUtil.execute(dnsPresetsModal.selected, selectedPreset);
},
show({ title = '', selected = (selectedPreset) => { }, isEdit = false }) {
this.title = title;
this.selected = selected;
this.visible = true;
},
close() {
dnsPresetsModal.visible = false;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#dnsPresets-modal',
data: {
dnsPresetsModal: dnsPresetsModal,
}
});
</script>
{{end}}

View File

@@ -1,57 +0,0 @@
{{define "modals/fakednsModal"}}
<a-modal id="fakedns-modal" v-model="fakednsModal.visible" :title="fakednsModal.title" @ok="fakednsModal.ok"
:closable="true" :mask-closable="false"
:ok-text="fakednsModal.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='{{ i18n "pages.xray.fakedns.ipPool" }}'>
<a-input v-model.trim="fakednsModal.fakeDns.ipPool"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.fakedns.poolSize" }}'>
<a-input-number v-model.number="fakednsModal.fakeDns.poolSize" :min="1"></a-input-number>
</a-form-item>
</a-form>
</a-modal>
<script>
const fakednsModal = {
title: '',
visible: false,
okText: '{{ i18n "confirm" }}',
isEdit: false,
confirm: null,
fakeDns: {
ipPool: "198.18.0.0/16",
poolSize: 65535,
},
ok() {
ObjectUtil.execute(fakednsModal.confirm, fakednsModal.fakeDns);
},
show({ title='', okText='{{ i18n "confirm" }}', fakeDns, confirm=(fakeDns)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
this.fakeDns = fakeDns;
} else {
this.fakeDns = {
ipPool: "198.18.0.0/16",
poolSize: 65535,
}
}
this.isEdit = isEdit;
},
close() {
fakednsModal.visible = false;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#fakedns-modal',
data: {
fakednsModal: fakednsModal,
}
});
</script>
{{end}}

View File

@@ -199,7 +199,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>
@@ -433,6 +433,9 @@
<a-tooltip title='{{ i18n "copy" }}'>
<a-button :style="{ minWidth: '24px' }" size="small" icon="snippets" @click="copy(infoModal.links[index])"></a-button>
</a-tooltip>
<a-tooltip title='{{ i18n "download" }}'>
<a-button :style="{ minWidth: '24px' }" size="small" icon="download" @click="FileManager.downloadTextFile(infoModal.links[index], `peer-${index + 1}.conf`)"></a-button>
</a-tooltip>
</tr-info-title>
<div v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)" :style="{ borderRadius: '1rem', padding: '0.5rem' }" class="client-table-odd-row">
</div>
@@ -478,10 +481,19 @@
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
if (app.ipLimitEnable && this.clientSettings.limitIp) {
refreshIPs(this.clientStats.email).then((ips) => {
this.clientIps = ips;
})
if (
[
Protocols.VMESS,
Protocols.VLESS,
Protocols.TROJAN,
Protocols.SHADOWSOCKS
].includes(this.inbound.protocol)
) {
if (app.ipLimitEnable && this.clientSettings.limitIp) {
refreshIPs(this.clientStats.email).then((ips) => {
this.clientIps = ips;
})
}
}
if (this.inbound.protocol == Protocols.WIREGUARD) {
this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\r\n')

View File

@@ -104,6 +104,8 @@
}
},
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){
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
@@ -117,6 +119,9 @@
client.method = "";
})
}
this.inModal.inbound.settings.shadowsockses.forEach(client => {
client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
})
} else {
if (this.inModal.inbound.settings.shadowsockses.length > 0){
this.inModal.inbound.settings.shadowsockses = [];
@@ -136,7 +141,27 @@
}
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;
},
},
});

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

@@ -15,8 +15,8 @@
<p>{{ i18n "pages.settings.security.twoFactorModalSecondStep" }}</p>
<a-input v-model.trim="twoFactorModal.enteredCode" :style="{ width: '100%' }"></a-input>
</template>
<template v-if="twoFactorModal.type === 'remove'">
<p>{{ i18n "pages.settings.security.twoFactorModalRemoveStep" }}</p>
<template v-if="twoFactorModal.type === 'confirm'">
<p>[[ twoFactorModal.description ]]</p>
<a-input v-model.trim="twoFactorModal.enteredCode" :style="{ width: '100%' }"></a-input>
</template>
<template slot="footer">
@@ -32,6 +32,7 @@
<script>
const twoFactorModal = {
title: '',
description: '',
fileName: '',
token: '',
enteredCode: '',
@@ -45,17 +46,6 @@
ObjectUtil.execute(twoFactorModal.confirm, true)
twoFactorModal.close()
switch (twoFactorModal.type) {
case 'set':
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
break;
case 'remove':
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
break;
default:
break;
}
} else {
Vue.prototype.$message['error']('{{ i18n "pages.settings.security.twoFactorModalError" }}')
}
@@ -67,11 +57,13 @@
},
show: function ({
title = '',
description = '',
token = '',
type = 'set',
confirm = (success) => { }
}) {
this.title = title;
this.description = description;
this.token = token;
this.visible = true;
this.confirm = confirm;

View File

@@ -6,6 +6,16 @@
<a-form-item label='{{ i18n "pages.xray.outbound.address" }}'>
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65531"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}'>
<a-select v-model="dnsModal.dnsServer.queryStrategy" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]] </a-select-option>
</a-select>
</a-form-item>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
<a-form-item label='{{ i18n "pages.xray.dns.domains" }}'>
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.domains.push('')"></a-button>
<template v-for="(domain, index) in dnsModal.dnsServer.domains">
@@ -15,15 +25,6 @@
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}' v-if="isAdvanced">
<a-select v-model="dnsModal.dnsServer.queryStrategy" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]] </a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Skip Fallback' v-if="isAdvanced">
<a-switch v-model="dnsModal.dnsServer.skipFallback"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.expectIPs"}}'>
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.expectIPs.push('')"></a-button>
<template v-for="(domain, index) in dnsModal.dnsServer.expectIPs">
@@ -33,31 +34,50 @@
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.unexpectIPs"}}'>
<a-button icon="plus" size="small" type="primary" @click="dnsModal.dnsServer.unexpectedIPs.push('')"></a-button>
<template v-for="(domain, index) in dnsModal.dnsServer.unexpectedIPs">
<a-input v-model.trim="dnsModal.dnsServer.unexpectedIPs[index]">
<a-button icon="minus" size="small" slot="addonAfter"
@click="dnsModal.dnsServer.unexpectedIPs.splice(index,1)"></a-button>
</a-input>
</template>
</a-form-item>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
<a-form-item label='Skip Fallback'>
<a-switch v-model="dnsModal.dnsServer.skipFallback"></a-switch>
</a-form-item>
<a-form-item label='Disable Cache'>
<a-switch v-model="dnsModal.dnsServer.disableCache"></a-switch>
</a-form-item>
<a-form-item label='Final Query'>
<a-switch v-model="dnsModal.dnsServer.finalQuery"></a-switch>
</a-form-item>
</a-form>
</a-modal>
<script>
const defaultDnsObject = {
address: "localhost",
port: 53,
domains: [],
expectIPs: [],
unexpectedIPs: [],
queryStrategy: 'UseIP',
skipFallback: true,
disableCache: false,
finalQuery: false
}
const dnsModal = {
title: '',
visible: false,
okText: '{{ i18n "confirm" }}',
isEdit: false,
confirm: null,
dnsServer: {
address: "localhost",
domains: [],
expectIPs: [],
queryStrategy: 'UseIP',
skipFallback: true,
dnsServer: { ...defaultDnsObject },
ok() {
ObjectUtil.execute(dnsModal.confirm, { ...dnsModal.dnsServer });
},
ok() {
domains = dnsModal.dnsServer.domains.filter(d => d.length > 0);
expectIPs = dnsModal.dnsServer.expectIPs.filter(ip => ip.length > 0);
dnsModal.dnsServer.domains = domains;
dnsModal.dnsServer.expectIPs = expectIPs;
newDnsServer = (domains.length > 0 || expectIPs.length > 0) ? dnsModal.dnsServer : dnsModal.dnsServer.address;
ObjectUtil.execute(dnsModal.confirm, newDnsServer);
},
show({
title = '',
okText = '{{ i18n "confirm" }}',
@@ -69,28 +89,28 @@
this.okText = okText;
this.confirm = confirm;
this.visible = true;
this.isEdit = isEdit;
if (isEdit) {
if (typeof dnsServer == 'object') {
this.dnsServer = dnsServer;
} else {
this.dnsServer = {
address: dnsServer ?? "",
domains: [],
expectIPs: [],
queryStrategy: 'UseIP',
skipFallback: true,
}
switch (typeof dnsServer) {
case 'string':
const dnsObj = { ...defaultDnsObject };
dnsObj.address = dnsServer;
this.dnsServer = dnsObj;
break;
case 'object':
this.dnsServer = dnsServer;
break;
}
} else {
this.dnsServer = {
address: "localhost",
domains: [],
expectIPs: [],
queryStrategy: 'UseIP',
skipFallback: true,
}
this.dnsServer = { ...defaultDnsObject };
this.dnsServer.domains = [];
this.dnsServer.expectIPs = [];
this.dnsServer.unexpectedIPs = [];
}
this.isEdit = isEdit;
},
close() {
dnsModal.visible = false;
@@ -101,13 +121,6 @@
el: '#dns-modal',
data: {
dnsModal: dnsModal,
},
computed: {
isAdvanced: {
get: function () {
return dnsModal.dnsServer.domains.length > 0;
}
}
}
});
</script>

View File

@@ -0,0 +1,56 @@
{{define "modals/fakednsModal"}}
<a-modal id="fakedns-modal" v-model="fakednsModal.visible" :title="fakednsModal.title" @ok="fakednsModal.ok"
:closable="true" :mask-closable="false" :ok-text="fakednsModal.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='{{ i18n "pages.xray.fakedns.ipPool" }}'>
<a-input v-model.trim="fakednsModal.fakeDns.ipPool"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.fakedns.poolSize" }}'>
<a-input-number v-model.number="fakednsModal.fakeDns.poolSize" :min="1"></a-input-number>
</a-form-item>
</a-form>
</a-modal>
<script>
const fakednsDefaultData = {
ipPool: "198.18.0.0/16",
poolSize: 65535,
}
const fakednsModal = {
title: '',
visible: false,
okText: '{{ i18n "confirm" }}',
isEdit: false,
confirm: null,
fakeDns: { ...fakednsDefaultData },
ok() {
ObjectUtil.execute(fakednsModal.confirm, fakednsModal.fakeDns);
},
show({ title = '', okText = '{{ i18n "confirm" }}', fakeDns, confirm = (fakeDns) => { }, isEdit = false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if (isEdit) {
this.fakeDns = fakeDns;
} else {
this.fakeDns = { ...fakednsDefaultData }
}
this.isEdit = isEdit;
},
close() {
fakednsModal.visible = false;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#fakedns-modal',
data: {
fakednsModal: fakednsModal,
}
});
</script>
{{end}}

View File

@@ -1,6 +1,4 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
{{ template "page/head_start" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
@@ -60,68 +58,103 @@
margin-block-end: 12px;
}
</style>
<body>
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar>
<a-layout id="content-layout">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear>
<a-alert type="error" v-if="confAlerts.length>0" :style="{ marginBottom: '10px' }"
message='{{ i18n "secAlertTitle" }}'
color="red"
show-icon closable>
<template slot="description">
<b>{{ i18n "secAlertConf" }}</b>
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
</template>
</a-alert>
</transition>
<a-space direction="vertical">
<a-card hoverable :style="{ marginBottom: '.5rem', overflowX: 'hidden' }">
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
<a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
</a-space>
</a-col>
<a-col :xs="24" :sm="14">
<template>
<div>
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
message='{{ i18n "pages.settings.infoDesc" }}'
show-icon>
</a-alert>
</div>
</template>
</a-col>
</a-row>
</a-card>
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings" }}' :style="{ paddingTop: '20px' }">
{{ template "settings/panel/general" . }}
</a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings" }}' :style="{ paddingTop: '20px' }">
{{ template "settings/panel/security" . }}
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings" }}' :style="{ paddingTop: '20px' }">
{{ template "settings/panel/telegram" . }}
</a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}' :style="{ paddingTop: '20px' }">
{{ template "settings/panel/subscription/general" . }}
</a-tab-pane>
<a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }">
{{ template "settings/panel/subscription/json" . }}
</a-tab-pane>
</a-tabs>
</a-space>
</a-spin>
</a-layout-content>
</a-layout>
{{ template "page/head_end" .}}
{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<a-sidebar></a-sidebar>
<a-layout id="content-layout">
<a-layout-content>
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear>
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }"
message='{{ i18n "secAlertTitle" }}'
color="red"
show-icon closable>
<template slot="description">
<b>{{ i18n "secAlertConf" }}</b>
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
</template>
</a-alert>
</transition>
<transition name="list" appear>
<template>
<a-row v-if="!loadingStates.fetched">
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card>
</a-row>
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
<a-col>
<a-card hoverable>
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
<a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
</a-space>
</a-col>
<a-col :xs="24" :sm="14">
<template>
<div>
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
message='{{ i18n "pages.settings.infoDesc" }}'
show-icon>
</a-alert>
</div>
</template>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col>
<a-tabs default-active-key="1">
<a-tab-pane key="1" :style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="setting"></a-icon>
<span>{{ i18n "pages.settings.panelSettings" }}</span>
</template>
{{ template "settings/panel/general" . }}
</a-tab-pane>
<a-tab-pane key="2" :style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="safety"></a-icon>
<span>{{ i18n "pages.settings.securitySettings" }}</span>
</template>
{{ template "settings/panel/security" . }}
</a-tab-pane>
<a-tab-pane key="3" :style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="message"></a-icon>
<span>{{ i18n "pages.settings.TGBotSettings" }}</span>
</template>
{{ template "settings/panel/telegram" . }}
</a-tab-pane>
<a-tab-pane key="4" :style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="cloud-server"></a-icon>
<span>{{ i18n "pages.settings.subSettings" }}</span>
</template>
{{ template "settings/panel/subscription/general" . }}
</a-tab-pane>
<a-tab-pane key="5" v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="code"></a-icon>
<span>{{ i18n "pages.settings.subSettings" }} (JSON)</span>
</template>
{{ template "settings/panel/subscription/json" . }}
</a-tab-pane>
</a-tabs>
</a-col>
</a-row>
</template>
</transition>
</a-spin>
</a-layout-content>
</a-layout>
{{template "js" .}}
</a-layout>
{{template "page/body_scripts" .}}
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/otpauth/otpauth.umd.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
@@ -132,10 +165,14 @@
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
mixins: [MediaQueryMixin],
el: '#app',
data: {
themeSwitcher,
spinning: false,
loadingStates: {
fetched: false,
spinning: false
},
oldAllSetting: new AllSetting(),
allSetting: new AllSetting(),
saveBtnDisable: true,
@@ -170,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" },
],
},
},
@@ -248,13 +285,16 @@
},
methods: {
loading(spinning = true) {
this.spinning = spinning;
this.loadingStates.spinning = spinning;
},
async getAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/all");
this.loading(false);
if (msg.success) {
if (!this.loadingStates.fetched) {
this.loadingStates.fetched = true
}
this.oldAllSetting = new AllSetting(msg.obj);
this.allSetting = new AllSetting(msg.obj);
app.changeRemarkSample();
@@ -270,12 +310,30 @@
}
},
async updateUser() {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/updateUser", this.user);
this.loading(false);
if (msg.success) {
this.user = {};
window.location.replace(basePath + "logout");
const sendUpdateUserRequest = async () => {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/updateUser", this.user);
this.loading(false);
if (msg.success) {
this.user = {};
window.location.replace(basePath + "logout");
}
}
if (this.allSetting.twoFactorEnable) {
twoFactorModal.show({
title: '{{ i18n "pages.settings.security.twoFactorModalChangeCredentialsTitle" }}',
description: '{{ i18n "pages.settings.security.twoFactorModalChangeCredentialsStep" }}',
token: this.allSetting.twoFactorToken,
type: 'confirm',
confirm: (success) => {
if (success) {
sendUpdateUserRequest();
}
}
})
} else {
sendUpdateUserRequest();
}
},
async restartPanel() {
@@ -313,6 +371,8 @@
type: 'set',
confirm: (success) => {
if (success) {
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
this.allSetting.twoFactorToken = newTwoFactorToken
}
@@ -322,10 +382,13 @@
} else {
twoFactorModal.show({
title: '{{ i18n "pages.settings.security.twoFactorModalDeleteTitle" }}',
description: '{{ i18n "pages.settings.security.twoFactorModalRemoveStep" }}',
token: this.allSetting.twoFactorToken,
type: 'remove',
type: 'confirm',
confirm: (success) => {
if (success) {
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
this.allSetting.twoFactorEnable = false
this.allSetting.twoFactorToken = ""
}
@@ -334,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) {
@@ -357,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: {
@@ -508,7 +576,7 @@
if (!this.allSetting) return [];
var alerts = []
if (window.location.protocol !== "https:") alerts.push('{{ i18n "secAlertSSL" }}');
if (this.allSetting.webPort == 54321) alerts.push('{{ i18n "secAlertPanelPort" }}');
if (this.allSetting.webPort === 2053) alerts.push('{{ i18n "secAlertPanelPort" }}');
panelPath = window.location.pathname.split('/').length < 4
if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}');
if (this.allSetting.subEnable) {
@@ -531,5 +599,4 @@
}
});
</script>
</body>
</html>
{{ template "page/body_end" .}}

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

@@ -1,5 +1,5 @@
{{define "settings/xray/advanced"}}
<a-space direction="vertical" size="small">
<a-space direction="vertical" size="small" :style="{ marginTop: '20px' }">
<a-list-item-meta title='{{ i18n "pages.xray.Template"}}'
description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta>
<a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" :style="{ margin: '10px 0' }"

View File

@@ -135,7 +135,7 @@
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel key="4" header='{{ i18n "pages.xray.blockConfigs"}}'>
<a-collapse-panel key="4" header='{{ i18n "pages.xray.basicRouting"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message">
@@ -144,22 +144,12 @@
</template>
</a-alert>
</a-row>
<a-setting-list-item paddings="small">
<a-setting-list-item paddings="small" :style="{ marginBottom: '20px' }">
<template #title>{{ i18n "pages.xray.Torrent"}}</template>
<template #description>{{ i18n "pages.xray.TorrentDesc"}}</template>
<template #control>
<a-switch v-model="torrentSettings"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.Family"}}</template>
<template #description>{{ i18n "pages.xray.FamilyDesc"}}</template>
<template #control>
<a-switch v-model="familyProtectSettings"></a-switch>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel key="5" header='{{ i18n "pages.xray.basicRouting"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message">

View File

@@ -29,7 +29,7 @@
<template #control>
<a-select v-model="dnsStrategy" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']">
<a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']">
<span>[[ l ]]</span>
</a-select-option>
</a-select>
@@ -56,6 +56,14 @@
<a-switch v-model="dnsDisableFallbackIfMatch"></a-switch>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.dns.useSystemHosts" }}</template>
<template #description>{{ i18n "pages.xray.dns.useSystemHostsDesc" }}</template>
<template #control>
<a-switch v-model="dnsUseSystemHosts"></a-switch>
</template>
</a-setting-list-item>
</template>
</a-collapse-panel>
<template v-if="enableDNS">
@@ -102,9 +110,12 @@
</template>
<template v-else>
<a-empty description='{{ i18n "emptyDnsDesc" }}' :style="{ margin: '10px' }">
<a-button type="primary" icon="plus" @click="addDNSServer()" :style="{ marginTop: '10px' }">
<span>{{ i18n "pages.xray.dns.add" }}</span>
</a-button>
<a-button-group>
<a-button type="primary" icon="plus" @click="addDNSServer()">
<span>{{ i18n "pages.xray.dns.add" }}</span>
</a-button>
<a-button type="primary" icon="menu" @click="openDNSPresets()"></a-button>
</a-button-group>
</a-empty>
</template>
</a-collapse-panel>

File diff suppressed because it is too large Load Diff

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

@@ -81,5 +81,9 @@
}
]
},
"stats": {}
}
"stats": {},
"metrics": {
"tag": "metrics_out",
"listen": "127.0.0.1:11111"
}
}

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,53 @@ 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{}
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
}
}
}
}
}
}
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
}
}
m["updated_at"] = now
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 +403,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 +492,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 +518,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 +714,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 +753,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 +914,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,6 +962,7 @@ 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 {
@@ -906,10 +1011,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 +1355,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 +1382,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 +1441,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 +1469,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 +1506,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 +1533,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 +1565,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 +1592,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 +1627,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 +1654,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 +1906,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 +2037,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 +2094,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

View File

@@ -2,6 +2,7 @@ package service
import (
"archive/zip"
"bufio"
"bytes"
"encoding/json"
"fmt"
@@ -94,21 +95,34 @@ type ServerService struct {
inboundService InboundService
cachedIPv4 string
cachedIPv6 string
noIPv6 bool
}
func getPublicIP(url string) string {
resp, err := http.Get(url)
client := &http.Client{
Timeout: 3 * time.Second,
}
resp, err := client.Get(url)
if err != nil {
return "N/A"
}
defer resp.Body.Close()
// Don't retry if access is blocked or region-restricted
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnavailableForLegalReasons {
return "N/A"
}
if resp.StatusCode != http.StatusOK {
return "N/A"
}
ip, err := io.ReadAll(resp.Body)
if err != nil {
return "N/A"
}
ipString := string(ip)
ipString := strings.TrimSpace(string(ip))
if ipString == "" {
return "N/A"
}
@@ -221,10 +235,44 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
}
// IP fetching with caching
if s.cachedIPv4 == "" || s.cachedIPv6 == "" {
s.cachedIPv4 = getPublicIP("https://api.ipify.org")
s.cachedIPv6 = getPublicIP("https://api6.ipify.org")
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 == "" {
for _, ip4Service := range showIp4ServiceLists {
s.cachedIPv4 = getPublicIP(ip4Service)
if s.cachedIPv4 != "N/A" {
break
}
}
}
if s.cachedIPv6 == "" && !s.noIPv6 {
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
@@ -295,7 +343,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
continue
}
if major > 25 || (major == 25 && minor > 3) || (major == 25 && minor == 3 && patch >= 3) {
if major > 25 || (major == 25 && minor > 8) || (major == 25 && minor == 8 && patch >= 3) {
versions = append(versions, release.TagName)
}
}
@@ -312,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)
@@ -447,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 {
@@ -632,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
@@ -683,3 +821,53 @@ 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
}

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",
@@ -322,10 +322,18 @@ func (s *SettingService) GetTwoFactorEnable() (bool, error) {
return s.getBool("twoFactorEnable")
}
func (s *SettingService) SetTwoFactorEnable(value bool) error {
return s.setBool("twoFactorEnable", value)
}
func (s *SettingService) GetTwoFactorToken() (string, error) {
return s.getString("twoFactorToken")
}
func (s *SettingService) SetTwoFactorToken(value string) error {
return s.setString("twoFactorToken", value)
}
func (s *SettingService) GetPort() (int, error) {
return s.getInt("webPort")
}

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
@@ -147,6 +147,19 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
return err
}
// After bot initialization, set up bot commands with localized descriptions
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")},
{Command: "status", Description: t.I18nBot("tgbot.commands.statusDesc")},
{Command: "id", Description: t.I18nBot("tgbot.commands.idDesc")},
},
})
if err != nil {
logger.Warning("Failed to set bot commands:", err)
}
// Start receiving Telegram bot messages
if !isRunning {
logger.Info("Telegram bot receiver started")
@@ -208,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
@@ -242,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":
@@ -271,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)
@@ -296,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)
@@ -321,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)
@@ -346,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)
@@ -371,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)
@@ -404,6 +421,7 @@ func (t *Tgbot) OnReceive() {
}
}
}
return nil
}, th.AnyMessage())
botHandler.Start()
@@ -622,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
}
}
@@ -685,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 {
@@ -696,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
}
}
@@ -825,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
}
}
@@ -900,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 {
@@ -911,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
}
}
@@ -935,7 +965,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData(t.encodeQuery("add_client_default_traffic_exp")),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumber", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("add_client_reset_exp_c "+strconv.Itoa(inputNumber))),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmNumberAdd", "Num=="+strconv.Itoa(inputNumber))).WithCallbackData(t.encodeQuery("add_client_reset_exp_c "+strconv.Itoa(inputNumber))),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton("1").WithCallbackData(t.encodeQuery("add_client_reset_exp_in "+strconv.Itoa(inputNumber)+" 1")),
@@ -1016,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
}
}
@@ -1082,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 {
@@ -1093,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
}
}
@@ -1144,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(
@@ -1271,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 {
@@ -1507,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":
@@ -1517,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":
@@ -1581,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)
@@ -1743,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,
@@ -1846,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)
}
@@ -1991,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)
@@ -2154,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
@@ -2752,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)
}
@@ -2766,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)
}
@@ -2790,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)
}
@@ -2811,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)
}
@@ -2829,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)
}
}
@@ -2840,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)
}
}
@@ -2855,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)
}
}
@@ -2868,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
@@ -2891,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

@@ -79,6 +79,16 @@ func (s *UserService) UpdateUser(id int, username string, password string) error
return err
}
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
if err != nil {
return err
}
if twoFactorEnable {
s.settingService.SetTwoFactorEnable(false)
s.settingService.SetTwoFactorToken("")
}
return db.Model(model.User{}).
Where("id = ?", id).
Updates(map[string]any{"username": username, "password": hashedPassword}).

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

@@ -126,11 +126,14 @@
"totalData" = "إجمالي البيانات"
"sent" = "مرسل"
"received" = "مستقبل"
"documentation" = "التوثيق"
"xraySwitchVersionDialog" = "هل تريد حقًا تغيير إصدار Xray؟"
"xraySwitchVersionDialogDesc" = "سيؤدي هذا إلى تغيير إصدار Xray إلى #version#."
"xraySwitchVersionPopover" = "تم تحديث Xray بنجاح"
"geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟"
"geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#."
"geofilesUpdateDialogDesc" = "سيؤدي هذا إلى تحديث كافة الملفات."
"geofilesUpdateAll" = "تحديث الكل"
"geofileUpdatePopover" = "تم تحديث ملف الجغرافيا بنجاح"
"dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة"
"logs" = "السجلات"
@@ -148,6 +151,8 @@
"getConfigError" = "حدث خطأ أثناء استرجاع ملف الإعدادات"
[pages.inbounds]
"allTimeTraffic" = "إجمالي حركة المرور"
"allTimeTrafficUsage" = "إجمالي الاستخدام طوال الوقت"
"title" = "الإدخالات"
"totalDownUp" = "إجمالي المرسل/المستقبل"
"totalUsage" = "إجمالي الاستخدام"
@@ -157,10 +162,13 @@
"remark" = "ملاحظة"
"protocol" = "بروتوكول"
"port" = "بورت"
"portMap" = "خريطة البورت"
"traffic" = "الترافيك"
"details" = "تفاصيل"
"transportConfig" = "نقل"
"expireDate" = "المدة"
"createdAt" = "تاريخ الإنشاء"
"updatedAt" = "تاريخ التحديث"
"resetTraffic" = "إعادة ضبط الترافيك"
"addInbound" = "أضف إدخال"
"generalActions" = "إجراءات عامة"
@@ -172,8 +180,6 @@
"deleteClient" = "حذف العميل"
"deleteClientContent" = "متأكد إنك عايز تحذف العميل؟"
"resetTrafficContent" = "متأكد إنك عايز تعيد ضبط الترافيك؟"
"inboundUpdateSuccess" = "تم تحديث الوارد بنجاح."
"inboundCreateSuccess" = "تم إنشاء الوارد بنجاح."
"copyLink" = "انسخ الرابط"
"address" = "العنوان"
"network" = "الشبكة"
@@ -259,6 +265,7 @@
"resetInboundClientTrafficSuccess" = "تم إعادة تعيين حركة المرور"
"trafficGetError" = "خطأ في الحصول على حركات المرور"
"getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519."
"getNewmldsa65Error" = "حدث خطاء في الحصول على mldsa65."
[pages.inbounds.stream.general]
"request" = "طلب"
@@ -399,7 +406,6 @@
"generalConfigsDesc" = "الخيارات دي هتحدد التعديلات العامة."
"logConfigs" = "السجلات"
"logConfigsDesc" = "السجلات ممكن تأثر على كفاءة السيرفر. ننصح بتفعيلها بحكمة لما تكون محتاجها."
"blockConfigs" = "درع الحماية"
"blockConfigsDesc" = "الخيارات دي هتحجب الترافيك بناءً على بروتوكولات ومواقع محددة."
"basicRouting" = "توجيه أساسي"
"blockConnectionsConfigsDesc" = "الخيارات دي هتحجب الترافيك بناءً على الدولة المطلوبة."
@@ -419,9 +425,6 @@
"RoutingStrategy" = "استراتيجية التوجيه العامة"
"RoutingStrategyDesc" = "حدد استراتيجية التوجيه الإجمالية لحل كل الطلبات."
"Torrent" = "حظر بروتوكول التورنت"
"TorrentDesc" = "بيحجب بروتوكول التورنت."
"Family" = "حماية العيلة"
"FamilyDesc" = "بيحجب المحتويات الكبار وعناوين المواقع الضارة."
"Inbounds" = "الإدخالات"
"InboundsDesc" = "قبول العملاء المعينين."
"Outbounds" = "المخرجات"
@@ -521,6 +524,12 @@
"edit" = "عدل السيرفر"
"domains" = "الدومينات"
"expectIPs" = "العناوين المتوقعة"
"unexpectIPs" = "عناوين IP غير متوقعة"
"useSystemHosts" = "استخدام ملف Hosts الخاص بالنظام"
"useSystemHostsDesc" = "استخدام ملف hosts من نظام مثبت"
"usePreset" = "استخدام النموذج"
"dnsPresetTitle" = "قوالب DNS"
"dnsPresetFamily" = "العائلي"
[pages.xray.fakedns]
"add" = "أضف Fake DNS"
@@ -539,6 +548,8 @@
"twoFactorModalFirstStep" = "1. امسح رمز QR هذا في تطبيق المصادقة أو انسخ الرمز الموجود بجانب رمز QR والصقه في التطبيق"
"twoFactorModalSecondStep" = "2. أدخل الرمز من التطبيق"
"twoFactorModalRemoveStep" = "أدخل الرمز من التطبيق لإزالة المصادقة الثنائية."
"twoFactorModalChangeCredentialsTitle" = "تغيير بيانات الاعتماد"
"twoFactorModalChangeCredentialsStep" = "أدخل الرمز من التطبيق لتغيير بيانات اعتماد المسؤول."
"twoFactorModalSetSuccess" = "تم إنشاء المصادقة الثنائية بنجاح"
"twoFactorModalDeleteSuccess" = "تم حذف المصادقة الثنائية بنجاح"
"twoFactorModalError" = "رمز خاطئ"
@@ -554,24 +565,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" = "❗ أمر مش معروف."
@@ -588,6 +600,10 @@
"restartSuccess" = "✅ العملية نجحت!"
"restartFailed" = "❗ حصل خطأ في العملية.\r\n\r\n<code>Error: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core مش شغال."
"startDesc" = "عرض القائمة الرئيسية"
"helpDesc" = "مساعدة البوت"
"statusDesc" = "التحقق من حالة البوت"
"idDesc" = "عرض معرف Telegram الخاص بك"
[tgbot.messages]
"cpuThreshold" = "🔴 حمل المعالج {{ .Percent }}% عدى الحد المسموح ({{ .Threshold }}%)"
@@ -635,7 +651,6 @@
"refreshedOn" = "\r\n📋🔄 اتحدّث في: {{ .Time }}\r\n\r\n"
"yes" = "✅ أيوه"
"no" = "❌ لأ"
"received_id" = "🔑📥 الـ ID اتحدث."
"received_password" = "🔑📥 الباسورد اتحدث."
"received_email" = "📧📥 الإيميل اتحدث."
@@ -655,7 +670,6 @@
"FailedResetTraffic" = "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ❌ فشل \n\n🛠 الخطأ: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 عملية إعادة ضبط الترافيك خلصت لكل العملاء."
[tgbot.buttons]
"closeKeyboard" = "❌ اقفل الكيبورد"
"cancel" = "❌ إلغاء"
@@ -689,7 +703,6 @@
"limitTraffic" = "🚧 حد الترافيك"
"getBanLogs" = "احصل على سجلات الحظر"
"allClients" = "كل العملاء"
"addClient" = "إضافة عميل"
"submitDisable" = "إرسال كمعطّل ☑️"
"submitEnable" = "إرسال كمفعّل ✅"
@@ -701,7 +714,6 @@
"ResetAllTraffics" = "إعادة ضبط جميع الترافيك"
"SortedTrafficUsageReport" = "تقرير استخدام الترافيك المرتب"
[tgbot.answers]
"successfulOperation" = "✅ العملية نجحت!"
"errorOperation" = "❗ حصل خطأ في العملية."

View File

@@ -126,11 +126,14 @@
"totalData" = "Total Data"
"sent" = "Sent"
"received" = "Received"
"documentation" = "Documentation"
"xraySwitchVersionDialog" = "Do you really want to change the Xray version?"
"xraySwitchVersionDialogDesc" = "This will change the Xray version to #version#."
"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"
@@ -148,6 +151,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"
@@ -157,10 +162,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"
@@ -257,7 +265,7 @@
"resetInboundClientTrafficSuccess" = "Traffic has been reset."
"trafficGetError" = "Error getting traffics."
"getNewX25519CertError" = "Error while obtaining the X25519 certificate."
"getNewmldsa65Error" = "Error while obtaining mldsa65."
[pages.inbounds.stream.general]
"request" = "Request"
@@ -339,7 +347,7 @@
"subEnable" = "Enable Subscription Service"
"subEnableDesc" = "Enables the subscription service."
"subTitle" = "Subscription Title"
"subTitleDesc" = "Title shown in VPN client"
"subTitleDesc" = "Title shown in VPN client"
"subListen" = "Listen IP"
"subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)"
"subPort" = "Listen Port"
@@ -398,7 +406,6 @@
"generalConfigsDesc" = "These options will determine general adjustments."
"logConfigs" = "Log"
"logConfigsDesc" = "Logs may affect your server's efficiency. It is recommended to enable it wisely only in case of your needs"
"blockConfigs" = "Protection Shield"
"blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites."
"basicRouting" = "Basic Routing"
"blockConnectionsConfigsDesc" = "These options will block traffic based on the specific requested country."
@@ -418,9 +425,6 @@
"RoutingStrategy" = "Overall Routing Strategy"
"RoutingStrategyDesc" = "Set the overall traffic routing strategy for resolving all requests."
"Torrent" = "Block BitTorrent Protocol"
"TorrentDesc" = "Blocks BitTorrent protocol."
"Family" = "Family Protection"
"FamilyDesc" = "Blocks adult content, and malware websites."
"Inbounds" = "Inbounds"
"InboundsDesc" = "Accepting the specific clients."
"Outbounds" = "Outbounds"
@@ -520,6 +524,12 @@
"edit" = "Edit Server"
"domains" = "Domains"
"expectIPs" = "Expect IPs"
"unexpectIPs" = "Unexpect IPs"
"useSystemHosts" = "Use System Hosts"
"useSystemHostsDesc" = "Use the hosts file from an installed system"
"usePreset" = "Use Preset"
"dnsPresetTitle" = "DNS Presets"
"dnsPresetFamily" = "Family"
[pages.xray.fakedns]
"add" = "Add Fake DNS"
@@ -538,6 +548,8 @@
"twoFactorModalFirstStep" = "1. Scan this QR code in the app for authentication or copy the token near the QR code and paste it into the app"
"twoFactorModalSecondStep" = "2. Enter the code from the app"
"twoFactorModalRemoveStep" = "Enter the code from the application to remove two-factor authentication."
"twoFactorModalChangeCredentialsTitle" = "Change credentials"
"twoFactorModalChangeCredentialsStep" = "Enter the code from the application to change administrator credentials."
"twoFactorModalSetSuccess" = "Two-factor authentication has been successfully established"
"twoFactorModalDeleteSuccess" = "Two-factor authentication has been successfully deleted"
"twoFactorModalError" = "Wrong code"
@@ -566,6 +578,7 @@
"day" = "Day"
"days" = "Days"
"hours" = "Hours"
"minutes" = "Minutes"
"unknown" = "Unknown"
"inbounds" = "Inbounds"
"clients" = "Clients"
@@ -587,6 +600,10 @@
"restartSuccess" = "✅ Operation successful!"
"restartFailed" = "❗ Error in operation.\r\n\r\n<code>Error: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core is not running."
"startDesc" = "Show the main menu"
"helpDesc" = "Bot help"
"statusDesc" = "Check bot status"
"idDesc" = "Show your Telegram ID"
[tgbot.messages]
"cpuThreshold" = "🔴 CPU Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%"
@@ -634,7 +651,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."
@@ -654,7 +670,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"
@@ -688,7 +703,6 @@
"limitTraffic" = "🚧 Traffic Limit"
"getBanLogs" = "Get Ban Logs"
"allClients" = "All Clients"
"addClient" = "Add Client"
"submitDisable" = "Submit As Disable ☑️"
"submitEnable" = "Submit As Enable ✅"

Some files were not shown because too many files have changed in this diff Show More