Compare commits

...

65 Commits

Author SHA1 Message Date
mhsanaei
04c6b2722b README: Persian 2025-02-03 20:30:42 +01:00
mhsanaei
94d651fc93 v2.5.1 2025-02-03 17:40:33 +01:00
mhsanaei
aae0cb37b7 Xray Core v25.1.30 2025-02-03 17:37:01 +01:00
Zahar Izmailov
b922d986d6 Improved database model migration and added indexing (#2655) 2025-02-03 13:36:03 +01:00
Abolfazl Fazilat
8a7cffd63f Completed translation for missing text (#2653) 2025-01-31 18:54:52 +01:00
Sanaei
c8e8c97afc Merge pull request #2652 from Incognito-Coder/main
Some Improvement
2025-01-31 16:01:14 +01:00
AghayeCoder
46ba4c4518 fix getSubGroupClients for enable/disable and edit clients. 2025-01-31 17:27:09 +03:30
AghayeCoder
a787ab497c switcher for outbound traffic 2025-01-31 17:24:03 +03:30
mhsanaei
3be204f272 v2.5.0 2025-01-28 00:09:20 +01:00
mhsanaei
de13729a97 update dependencies 2025-01-28 00:09:12 +01:00
mhsanaei
468eb8b908 Update release.yml
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2025-01-27 01:13:28 +01:00
mhsanaei
e95a748e77 docker
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2025-01-27 01:13:12 +01:00
mhsanaei
34e2d961f5 quiet build
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2025-01-27 01:09:39 +01:00
mhsanaei
b4a1d81444 [subJson] better direct options
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2025-01-26 19:43:02 +01:00
mhsanaei
46ef506aa6 TLS - serverNameToVerify 2025-01-26 19:33:50 +01:00
mhsanaei
51220917c4 outbound Traffic - default false
if you need it you need to change it manually to true
2025-01-26 11:22:42 +01:00
mhsanaei
b34956647b minor change 2025-01-26 11:07:45 +01:00
mhsanaei
5c4e2dfd39 default setting - scStreamUpServerSecs 2025-01-26 11:07:35 +01:00
Ivan Zorin
dd4c2adb37 Fix non-MultiUser dbInbounds (#2649) 2025-01-26 11:03:06 +01:00
mhsanaei
2dec7f48f5 bug fix - get client ips 2025-01-24 17:16:16 +01:00
Ali Rahimi
04cf250a54 json post base path bug fixed (#2647)
* json post base path bug fixed

* added comment field to group client management
2025-01-23 21:33:47 +01:00
mhsanaei
ac9ab828b5 GO v1.23.5 + update dependencies 2025-01-21 23:04:03 +01:00
Zahar Izmailov
4dd40f6f19 Add checkpoint handling in CloseDB function (#2646)
* Add checkpoint handling in CloseDB function

---------

Co-authored-by: Zakhar Izmaylov <ptdev@kedruss.ru>
2025-01-21 22:55:21 +01:00
mhsanaei
7911eeb69f XHTTP - scStreamUpServerSecs 2025-01-21 22:43:13 +01:00
Ali Rahimi
6e9180a665 Group Management of Subscription Clients (#2644)
* add group user with the same subscription id to all inbounds

* code format compare

* add await for reset client traffic

* en language changed

* added client traffic syncer job

* handle exist email duplicate in sub group

* multi reset and delete request for clients group

* add client traffic syncer setting option

* vi translate file updated

* auto open qr-modal bug fixed
2025-01-21 03:01:54 +01:00
Zahar Izmailov
66fe84181b Enhance database initialization in db.go (#2645)
- Updated GORM configuration to skip default transactions and prepare statements.
- Modified the database connection string to include caching and journal mode settings.
- Executed several PRAGMA statements to optimize SQLite performance and enable foreign key support.

These changes improve database handling and performance in the application.

Co-authored-by: Zakhar Izmaylov <ptdev@kedruss.ru>
2025-01-21 02:59:30 +01:00
Tara Rostami
7b7eb98acb Minor Fixes (CSS) (#2641) 2025-01-15 12:50:50 +01:00
Alex Churin
7eb5afdd8d wireguard modal fix (#2640) 2025-01-14 16:08:19 +01:00
xtclovver
522ccda71c Update translate.ru_RU.toml (#2637)
Fixed ru translation
2025-01-13 18:23:19 +01:00
mortefy
f780efb430 Fixed ru translation (#2638) 2025-01-13 18:22:35 +01:00
mhsanaei
783f1a073e time Location - local 2025-01-11 15:13:49 +01:00
Dmitiry Vinogradov
a4c38ec8ae fail2ban service in docker container (#2632)
docker container

Co-authored-by: Dmitrij Vinogradov <dmitrij.vinogradov@gmail.com>
2025-01-11 13:41:48 +01:00
Igor Semenov
0c47771671 Add Russian domains (#2635)
Add Russian domains
2025-01-11 13:39:32 +01:00
Tara Rostami
67920a1962 Minor Fixes (UI) (#2636)
* Minor Fixes (UI)

* Update custom.min.css
2025-01-11 13:38:26 +01:00
mhsanaei
49d3957c07 bug fix 2025-01-05 21:04:18 +01:00
mhsanaei
ee946ceab2 iplimit: ipRegex improved
When the client has MUX enabled, a TCP or UDP prefix appears before the IP address. We initially weren’t aware of this behavior, but we have now resolved the issue.
2025-01-05 18:58:51 +01:00
LoST
b650064177 Firewall improvements (#2630)
* The menu has been completed

*Added firewall shutdown

*Improved port removal process (optional)
2025-01-05 16:01:56 +01:00
mhsanaei
9fb9d7201e Add custom v2ray rules for Russia
runetfreedom/russia-v2ray-rules-dat
2025-01-05 15:39:40 +01:00
mhsanaei
4a3b9b913d bug fix - reality settings 2025-01-01 22:49:29 +01:00
mhsanaei
da674d44cf httpupgrade: remove host from header 2025-01-01 20:18:28 +01:00
LoST
cc3252531b Added "comment" in all languages (#2631)
- Added a "comment" in all languages

---------

Co-authored-by: mhsanaei <ho3ein.sanaei@gmail.com>
2025-01-01 19:13:55 +01:00
mhsanaei
284731deeb xmux - hMaxReusableSecs 2025-01-01 18:48:47 +01:00
mhsanaei
9bc5c1d070 tcpNoDelay to penetrate 2025-01-01 18:42:50 +01:00
mhsanaei
26a7700557 Xray core buggy version removed
v1.8.24 and
major >=25.1.1
2025-01-01 18:36:46 +01:00
mhsanaei
b6a919218a Xray Core v25.1.1 2025-01-01 18:30:36 +01:00
mhsanaei
6cc07254e0 Fallback - Reality
this is not my problem they don't mention it on their document
2024-12-30 00:38:49 +01:00
mhsanaei
4ad5a5aba4 update dependencies 2024-12-28 15:15:01 +01:00
mhsanaei
7ab8164de4 Xray Core v24.12.28 2024-12-28 15:14:39 +01:00
MHSanaei
195effd177 v2.4.11 2024-12-27 21:49:45 +01:00
MHSanaei
747ad3b9c8 bug fix - outbound xhttp link 2024-12-27 21:30:51 +01:00
mhsanaei
cf879f9527 bug fix - vmess tls 2024-12-27 17:59:59 +01:00
mhsanaei
04c658f1a0 Client: Comment
now you can add Comment and write anything you want to remember
2024-12-27 13:48:07 +01:00
mhsanaei
2ab1a174db moment v2.30.1 2024-12-25 20:27:48 +01:00
mhsanaei
dfca2af997 axios v1.7.9 2024-12-25 19:09:54 +01:00
Baton34
0a7d15b48c RU: translation fix (#2620) 2024-12-22 14:10:06 +01:00
mhsanaei
518bc72f90 media - telebot 2024-12-21 14:00:21 +01:00
mhsanaei
3c65209ce9 Stargazers over Time - variant=adaptive 2024-12-21 13:52:05 +01:00
LoST
174535b05d unnecessary sudo removed (#2619)
All the commands do not work exactly, but since we already work in the beginning on behalf of root, then sudo is not necessary at the beginning. Is that right? 

P.S. when selected in the menu, it says that "command not found".
2024-12-20 23:48:10 +01:00
Tara Rostami
fff54fe7f3 fix bug (#2618) 2024-12-20 19:06:19 +01:00
LoST
0859d230b0 Firewall management: improved (#2614)
* fix permissions

* Update install func + add/edit func open/close ports + status firewall

* hotfix

* subport
2024-12-20 18:43:47 +01:00
KiselevAlexander
02998c5467 Added ip limit data and controls to client info modal (#2617) 2024-12-20 18:34:30 +01:00
Tara Rostami
3f38c42852 fail2ban: better ipv6 validation (#2615) 2024-12-20 18:33:27 +01:00
mhsanaei
49295661fd fail2ban - unban fix
sorry for my mistake
2024-12-19 23:24:10 +01:00
mhsanaei
e4301f8d93 fail2ban - allports 2024-12-19 22:49:25 +01:00
mhsanaei
c6586f8df2 fix typo 2024-12-19 14:53:09 +01:00
69 changed files with 2073 additions and 479 deletions

View File

@@ -7,7 +7,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -31,6 +31,8 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with:
install: true
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3

View File

@@ -72,7 +72,7 @@ jobs:
export GOARCH=s390x export GOARCH=s390x
export CC=s390x-linux-gnu-gcc export CC=s390x-linux-gnu-gcc
fi fi
go build -o xui-release -v main.go go build -ldflags "-w -s" -o xui-release -v main.go
mkdir x-ui mkdir x-ui
cp xui-release x-ui/ cp xui-release x-ui/
@@ -83,43 +83,43 @@ jobs:
cd x-ui/bin cd x-ui/bin
# Download dependencies # Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v24.12.18/" Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.1.30/"
if [ "${{ matrix.platform }}" == "amd64" ]; then if [ "${{ matrix.platform }}" == "amd64" ]; then
wget ${Xray_URL}Xray-linux-64.zip wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip rm -f Xray-linux-64.zip
elif [ "${{ matrix.platform }}" == "arm64" ]; then elif [ "${{ matrix.platform }}" == "arm64" ]; then
wget ${Xray_URL}Xray-linux-arm64-v8a.zip wget -q ${Xray_URL}Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip rm -f Xray-linux-arm64-v8a.zip
elif [ "${{ matrix.platform }}" == "armv7" ]; then elif [ "${{ matrix.platform }}" == "armv7" ]; then
wget ${Xray_URL}Xray-linux-arm32-v7a.zip wget -q ${Xray_URL}Xray-linux-arm32-v7a.zip
unzip Xray-linux-arm32-v7a.zip unzip Xray-linux-arm32-v7a.zip
rm -f Xray-linux-arm32-v7a.zip rm -f Xray-linux-arm32-v7a.zip
elif [ "${{ matrix.platform }}" == "armv6" ]; then elif [ "${{ matrix.platform }}" == "armv6" ]; then
wget ${Xray_URL}Xray-linux-arm32-v6.zip wget -q ${Xray_URL}Xray-linux-arm32-v6.zip
unzip Xray-linux-arm32-v6.zip unzip Xray-linux-arm32-v6.zip
rm -f Xray-linux-arm32-v6.zip rm -f Xray-linux-arm32-v6.zip
elif [ "${{ matrix.platform }}" == "386" ]; then elif [ "${{ matrix.platform }}" == "386" ]; then
wget ${Xray_URL}Xray-linux-32.zip wget -q ${Xray_URL}Xray-linux-32.zip
unzip Xray-linux-32.zip unzip Xray-linux-32.zip
rm -f Xray-linux-32.zip rm -f Xray-linux-32.zip
elif [ "${{ matrix.platform }}" == "armv5" ]; then elif [ "${{ matrix.platform }}" == "armv5" ]; then
wget ${Xray_URL}Xray-linux-arm32-v5.zip wget -q ${Xray_URL}Xray-linux-arm32-v5.zip
unzip Xray-linux-arm32-v5.zip unzip Xray-linux-arm32-v5.zip
rm -f Xray-linux-arm32-v5.zip rm -f Xray-linux-arm32-v5.zip
elif [ "${{ matrix.platform }}" == "s390x" ]; then elif [ "${{ matrix.platform }}" == "s390x" ]; then
wget ${Xray_URL}Xray-linux-s390x.zip wget -q ${Xray_URL}Xray-linux-s390x.zip
unzip Xray-linux-s390x.zip unzip Xray-linux-s390x.zip
rm -f Xray-linux-s390x.zip rm -f Xray-linux-s390x.zip
fi fi
rm -f geoip.dat geosite.dat rm -f geoip.dat geosite.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
mv xray xray-linux-${{ matrix.platform }} mv xray xray-linux-${{ matrix.platform }}
cd ../.. cd ../..

View File

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

View File

@@ -27,14 +27,14 @@ case $1 in
esac esac
mkdir -p build/bin mkdir -p build/bin
cd build/bin cd build/bin
wget "https://github.com/XTLS/Xray-core/releases/download/v24.12.18/Xray-linux-${ARCH}.zip" wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.1.30/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}" mv xray "xray-linux-${FNAME}"
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
wget -O geoip_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_VN.dat https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
cd ../../ cd ../../

View File

@@ -15,7 +15,7 @@ COPY . .
ENV CGO_ENABLED=1 ENV CGO_ENABLED=1
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
RUN go build -o build/x-ui main.go RUN go build -ldflags "-w -s" -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH" RUN ./DockerInit.sh "$TARGETARCH"
# ======================================================== # ========================================================
@@ -48,6 +48,7 @@ RUN chmod +x \
/app/x-ui \ /app/x-ui \
/usr/bin/x-ui /usr/bin/x-ui
ENV X_UI_ENABLE_FAIL2BAN="true"
VOLUME [ "/etc/x-ui" ] VOLUME [ "/etc/x-ui" ]
CMD [ "./x-ui" ] CMD [ "./x-ui" ]
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ] ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]

View File

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) [English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center"> <p align="center">
<picture> <picture>
@@ -569,7 +569,8 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
<img alt="3x-ui" src="./media/06-configs-light.png"> <img alt="3x-ui" src="./media/06-configs-light.png">
</picture> </picture>
<picture> <picture>
<img alt="3x-ui" src="./media/7.png"> <source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture> </picture>
## Un agradecimiento especial a ## Un agradecimiento especial a
@@ -579,8 +580,8 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Reconocimientos ## 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._ - [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._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _Un dominio alojado en Vietnam y una lista de bloqueo con la máxima eficiencia para vietnamitas._ - [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 ## Estrellas a lo largo del tiempo
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg)](https://starchart.cc/MHSanaei/3x-ui) [![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)

527
README.fa_IR.md Normal file
View File

@@ -0,0 +1,527 @@
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
<img alt="3x-ui" src="./media/3x-ui-light.png">
</picture>
</p>
**یک پنل وب پیشرفته • ساخته شده بر پایه Xray Core**
[![](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)
> **سلب مسئولیت:** این پروژه صرفاً برای اهداف آموزشی و تحقیقاتی است. استفاده از آن برای مقاصد غیرقانونی یا در محیط‌های عملیاتی ممنوع است.
**اگر این پروژه برای شما مفید بوده، می‌توانید با دادن یک**:star2: از آن حمایت کنید.
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## نصب و ارتقا
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/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 20.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 8.0+
- Rocky Linux 8+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- 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)

View File

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) [English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center"> <p align="center">
<picture> <picture>
@@ -578,7 +578,8 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
<img alt="3x-ui" src="./media/06-configs-light.png"> <img alt="3x-ui" src="./media/06-configs-light.png">
</picture> </picture>
<picture> <picture>
<img alt="3x-ui" src="./media/7.png"> <source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture> </picture>
## A Special Thanks to ## A Special Thanks to
@@ -588,8 +589,8 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
## Acknowledgment ## Acknowledgment
- [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._ - [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._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._ - [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._
## Stargazers over Time ## Stargazers over Time
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg)](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

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) [English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center"> <p align="center">
<picture> <picture>
@@ -576,7 +576,8 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
<img alt="3x-ui" src="./media/06-configs-light.png"> <img alt="3x-ui" src="./media/06-configs-light.png">
</picture> </picture>
<picture> <picture>
<img alt="3x-ui" src="./media/7.png"> <source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture> </picture>
## Особая благодарность ## Особая благодарность
@@ -586,8 +587,8 @@ 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._ - [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._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._ - [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)](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

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md) [English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center"> <p align="center">
<picture> <picture>
@@ -569,7 +569,8 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
<img alt="3x-ui" src="./media/06-configs-light.png"> <img alt="3x-ui" src="./media/06-configs-light.png">
</picture> </picture>
<picture> <picture>
<img alt="3x-ui" src="./media/7.png"> <source media="(prefers-color-scheme: dark)" srcset="./media/07-bot-dark.png">
<img alt="3x-ui" src="./media/07-bot-light.png">
</picture> </picture>
## 特别感谢 ## 特别感谢
@@ -579,8 +580,8 @@ 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._ - [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._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._ - [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趋势 ## Star趋势
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg)](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

@@ -1 +1 @@
2.4.10 2.5.1

View File

@@ -26,20 +26,35 @@ const (
) )
func initModels() error { func initModels() error {
models := []interface{}{ // Order matters: first create tables without dependencies
baseModels := []interface{}{
&model.User{}, &model.User{},
&model.Inbound{},
&model.OutboundTraffics{},
&model.Setting{}, &model.Setting{},
&model.InboundClientIps{},
&xray.ClientTraffic{},
} }
for _, model := range models {
// Migrate base models
for _, model := range baseModels {
if err := db.AutoMigrate(model); err != nil { if err := db.AutoMigrate(model); err != nil {
log.Printf("Error auto migrating model: %v", err) log.Printf("Error auto migrating base model: %v", err)
return err return err
} }
} }
// Then migrate models with dependencies
dependentModels := []interface{}{
&model.Inbound{},
&model.OutboundTraffics{},
&model.InboundClientIps{},
&xray.ClientTraffic{},
}
for _, model := range dependentModels {
if err := db.AutoMigrate(model); err != nil {
log.Printf("Error auto migrating dependent model: %v", err)
return err
}
}
return nil return nil
} }
@@ -82,9 +97,31 @@ func InitDB(dbPath string) error {
} }
c := &gorm.Config{ c := &gorm.Config{
Logger: gormLogger, Logger: gormLogger,
SkipDefaultTransaction: true,
PrepareStmt: true,
} }
db, err = gorm.Open(sqlite.Open(dbPath), c)
dsn := dbPath + "?cache=shared&_journal_mode=WAL&_synchronous=NORMAL"
db, err = gorm.Open(sqlite.Open(dsn), c)
if err != nil {
return err
}
sqlDB, err := db.DB()
if err != nil {
return err
}
_, err = sqlDB.Exec("PRAGMA cache_size = -64000;")
if err != nil {
return err
}
_, err = sqlDB.Exec("PRAGMA temp_store = MEMORY;")
if err != nil {
return err
}
_, err = sqlDB.Exec("PRAGMA foreign_keys = ON;")
if err != nil { if err != nil {
return err return err
} }
@@ -101,6 +138,11 @@ func InitDB(dbPath string) error {
func CloseDB() error { func CloseDB() error {
if db != nil { if db != nil {
if err := Checkpoint(); err != nil {
log.Printf("error executing checkpoint: %v", err)
}
sqlDB, err := db.DB() sqlDB, err := db.DB()
if err != nil { if err != nil {
return err return err

View File

@@ -29,14 +29,14 @@ type User struct {
type Inbound struct { type Inbound struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
UserId int `json:"-"` UserId int `json:"-" gorm:"index"`
Up int64 `json:"up" form:"up"` Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"`
Total int64 `json:"total" form:"total"` Total int64 `json:"total" form:"total"`
Remark string `json:"remark" form:"remark"` Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id;constraint:OnDelete:CASCADE" json:"clientStats"`
// config part // config part
Listen string `json:"listen" form:"listen"` Listen string `json:"listen" form:"listen"`
@@ -98,5 +98,6 @@ type Client struct {
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
TgID int64 `json:"tgId" form:"tgId"` TgID int64 `json:"tgId" form:"tgId"`
SubID string `json:"subId" form:"subId"` SubID string `json:"subId" form:"subId"`
Comment string `json:"comment" form:"comment"`
Reset int `json:"reset" form:"reset"` Reset int `json:"reset" form:"reset"`
} }

View File

@@ -11,6 +11,7 @@ services:
- $PWD/cert/:/root/cert/ - $PWD/cert/:/root/cert/
environment: environment:
XRAY_VMESS_AEAD_FORCED: "false" XRAY_VMESS_AEAD_FORCED: "false"
X_UI_ENABLE_FAIL2BAN: "true"
tty: true tty: true
network_mode: host network_mode: host
restart: unless-stopped restart: unless-stopped

63
go.mod
View File

@@ -1,46 +1,45 @@
module x-ui module x-ui
go 1.23.4 go 1.23.5
require ( require (
github.com/gin-contrib/gzip v1.0.1 github.com/gin-contrib/gzip v1.2.2
github.com/gin-contrib/sessions v1.0.1 github.com/gin-contrib/sessions v1.0.2
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/goccy/go-json v0.10.4 github.com/goccy/go-json v0.10.5
github.com/mymmrac/telego v0.31.4 github.com/mymmrac/telego v0.32.0
github.com/nicksnyder/go-i18n/v2 v2.4.1 github.com/nicksnyder/go-i18n/v2 v2.5.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.3 github.com/pelletier/go-toml/v2 v2.2.3
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.24.11 github.com/shirou/gopsutil/v4 v4.25.1
github.com/valyala/fasthttp v1.58.0 github.com/valyala/fasthttp v1.58.0
github.com/xtls/xray-core v1.8.25-0.20241218133935-cab2fdefd321 github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
golang.org/x/text v0.21.0 golang.org/x/text v0.21.0
google.golang.org/grpc v1.69.2 google.golang.org/grpc v1.70.0
gorm.io/driver/sqlite v1.5.7 gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12 gorm.io/gorm v1.25.12
) )
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bytedance/sonic v1.12.6 // indirect github.com/bytedance/sonic v1.12.8 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/cloudflare/circl v1.5.0 // indirect github.com/cloudflare/circl v1.5.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect github.com/ebitengine/purego v0.8.2 // indirect
github.com/ebitengine/purego v0.8.1 // indirect github.com/fasthttp/router v1.5.4 // indirect
github.com/fasthttp/router v1.5.3 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/gin-contrib/sse v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.3.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/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/go-playground/validator/v10 v10.24.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/google/pprof v0.0.0-20250202011525-fc3143867406 // indirect
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect github.com/gorilla/sessions v1.4.0 // indirect
@@ -58,11 +57,11 @@ require (
github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo/v2 v2.22.0 // indirect github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/pires/go-proxyproto v0.8.0 // indirect github.com/pires/go-proxyproto v0.8.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // 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/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect github.com/quic-go/quic-go v0.49.0 // indirect
github.com/refraction-networking/utls v1.6.7 // indirect github.com/refraction-networking/utls v1.6.7 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect
@@ -83,20 +82,20 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/mock v0.5.0 // indirect go.uber.org/mock v0.5.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.12.0 // indirect golang.org/x/arch v0.13.0 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.29.0 // indirect
golang.org/x/time v0.8.0 // indirect golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.28.0 // indirect golang.org/x/tools v0.29.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
google.golang.org/protobuf v1.36.0 // indirect google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 // indirect gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect
) )

151
go.sum
View File

@@ -4,38 +4,37 @@ github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJS
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM= 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 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fasthttp/router v1.5.3 h1:BFWXqa3e4thRI3MgPKTNtz0Oiq6UYN2OsEtb+YQ5TMI= github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8=
github.com/fasthttp/router v1.5.3/go.mod h1:b864KkDIapOYh77AVG/SNkwfRZ6k6ecWvD+ZRXmP5pw= github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc=
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE= github.com/gin-contrib/gzip v1.2.2 h1:iUU/EYCM8ENfkjmZaVrxbjF/ZC267Iqv5S0MMCMEliI=
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4= github.com/gin-contrib/gzip v1.2.2/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI= github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM= github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 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/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 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
@@ -51,12 +50,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 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-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 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= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -68,8 +67,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/google/pprof v0.0.0-20250202011525-fc3143867406 h1:wlQI2cYY0BsWmmPPAnxfQ8SDW0S3Jasn+4B8kXFxprg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20250202011525-fc3143867406/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
@@ -106,21 +105,21 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.31.4 h1:NpiNl0P/8eydknka/k6XaaaWVj5BKMlM3Ibba63QTBU= github.com/mymmrac/telego v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY=
github.com/mymmrac/telego v0.31.4/go.mod h1:T12js1PgbYDYznvoN05MSMuPMfWTYo7D9LKl5cPFWiI= github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M=
github.com/nicksnyder/go-i18n/v2 v2.4.1 h1:zwzjtX4uYyiaU02K5Ia3zSkpJZrByARkRB4V3YPrr0g= github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.4.1/go.mod h1:++Pl70FR6Cki7hdzZRnEEqdc2dJt+SAGotyFg/SvZMk= github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= 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/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
@@ -135,8 +134,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= 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/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s=
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@@ -153,17 +152,19 @@ github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 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.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
@@ -189,38 +190,38 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM= github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM=
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg= github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg=
github.com/xtls/xray-core v1.8.25-0.20241218133935-cab2fdefd321 h1:vk+n1RmfhFCj5xSi4I6C3USpcUQ48H3lt/QrtARVz1M= github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb h1:VBzDZ4XHT9FM0S3qvXp6KuTOk7mXQQm0pjOW0Neeog4=
github.com/xtls/xray-core v1.8.25-0.20241218133935-cab2fdefd321/go.mod h1:DCaUwrBk1RIC7hWg/wGoAynE69g3ptua1sEr8i0BWxA= github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb/go.mod h1:EKhNEDY/WIB+ylEbVv2pzUaP08W/lt0sYmJQ9FJruko=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 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/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -229,24 +230,24 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -260,8 +261,8 @@ gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= 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 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE= gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 h1:P+U/06iIKPQ3DLcg+zBfSCia1luZ2msPZrJ8jYDFPs0=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -2,6 +2,7 @@
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' green='\033[0;32m'
blue='\033[0;34m'
yellow='\033[0;33m' yellow='\033[0;33m'
plain='\033[0m' plain='\033[0m'
@@ -260,24 +261,24 @@ install_x-ui() {
systemctl start x-ui systemctl start x-ui
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..." echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
echo -e "" echo -e ""
echo -e "x-ui control menu usages: " echo -e "┌───────────────────────────────────────────────────────┐
echo -e "----------------------------------------------" ${blue}x-ui control menu usages (subcommands):${plain}
echo -e "SUBCOMMANDS:" │ │
echo -e "x-ui - Admin Management Script" ${blue}x-ui${plain} - Admin Management Script
echo -e "x-ui start - Start" ${blue}x-ui start${plain} - Start │
echo -e "x-ui stop - Stop" ${blue}x-ui stop${plain} - Stop │
echo -e "x-ui restart - Restart" ${blue}x-ui restart${plain} - Restart │
echo -e "x-ui status - Current Status" ${blue}x-ui status${plain} - Current Status
echo -e "x-ui settings - Current Settings" ${blue}x-ui settings${plain} - Current Settings
echo -e "x-ui enable - Enable Autostart on OS Startup" ${blue}x-ui enable${plain} - Enable Autostart on OS Startup
echo -e "x-ui disable - Disable Autostart on OS Startup" ${blue}x-ui disable${plain} - Disable Autostart on OS Startup
echo -e "x-ui log - Check logs" ${blue}x-ui log${plain} - Check logs │
echo -e "x-ui banlog - Check Fail2ban ban logs" ${blue}x-ui banlog${plain} - Check Fail2ban ban logs
echo -e "x-ui update - Update" ${blue}x-ui update${plain} - Update │
echo -e "x-ui legacy - legacy version" ${blue}x-ui legacy${plain} - legacy version
echo -e "x-ui install - Install" ${blue}x-ui install${plain} - Install │
echo -e "x-ui uninstall - Uninstall" ${blue}x-ui uninstall${plain} - Uninstall │
echo -e "----------------------------------------------" └───────────────────────────────────────────────────────┘"
} }
echo -e "${green}Running...${plain}" echo -e "${green}Running...${plain}"

BIN
media/07-bot-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
media/07-bot-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -21,7 +21,7 @@ type SubJsonService struct {
configJson map[string]interface{} configJson map[string]interface{}
defaultOutbounds []json_util.RawMessage defaultOutbounds []json_util.RawMessage
fragment string fragment string
noises string noises string
mux string mux string
inboundService service.InboundService inboundService service.InboundService
@@ -61,7 +61,7 @@ func NewSubJsonService(fragment string, noises string, mux string, rules string,
configJson: configJson, configJson: configJson,
defaultOutbounds: defaultOutbounds, defaultOutbounds: defaultOutbounds,
fragment: fragment, fragment: fragment,
noises: noises, noises: noises,
mux: mux, mux: mux,
SubService: subService, SubService: subService,
} }
@@ -217,7 +217,7 @@ func (s *SubJsonService) streamData(stream string) map[string]interface{} {
delete(streamSettings, "sockopt") delete(streamSettings, "sockopt")
if s.fragment != "" { if s.fragment != "" {
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "tcpNoDelay": true}`) streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpMptcp": true, "penetrate": true}`)
} }
// remove proxy protocol // remove proxy protocol

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -493,6 +493,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
headers = [], headers = [],
scMaxBufferedPosts = 30, scMaxBufferedPosts = 30,
scMaxEachPostBytes = "1000000", scMaxEachPostBytes = "1000000",
scStreamUpServerSecs = "20-80",
noSSEHeader = false, noSSEHeader = false,
xPaddingBytes = "100-1000", xPaddingBytes = "100-1000",
mode = MODE_OPTION.AUTO, mode = MODE_OPTION.AUTO,
@@ -503,6 +504,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
this.headers = headers; this.headers = headers;
this.scMaxBufferedPosts = scMaxBufferedPosts; this.scMaxBufferedPosts = scMaxBufferedPosts;
this.scMaxEachPostBytes = scMaxEachPostBytes; this.scMaxEachPostBytes = scMaxEachPostBytes;
this.scStreamUpServerSecs = scStreamUpServerSecs;
this.noSSEHeader = noSSEHeader; this.noSSEHeader = noSSEHeader;
this.xPaddingBytes = xPaddingBytes; this.xPaddingBytes = xPaddingBytes;
this.mode = mode; this.mode = mode;
@@ -523,6 +525,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
XrayCommonClass.toHeaders(json.headers), XrayCommonClass.toHeaders(json.headers),
json.scMaxBufferedPosts, json.scMaxBufferedPosts,
json.scMaxEachPostBytes, json.scMaxEachPostBytes,
json.scStreamUpServerSecs,
json.noSSEHeader, json.noSSEHeader,
json.xPaddingBytes, json.xPaddingBytes,
json.mode, json.mode,
@@ -536,6 +539,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
headers: XrayCommonClass.toV2Headers(this.headers, false), headers: XrayCommonClass.toV2Headers(this.headers, false),
scMaxBufferedPosts: this.scMaxBufferedPosts, scMaxBufferedPosts: this.scMaxBufferedPosts,
scMaxEachPostBytes: this.scMaxEachPostBytes, scMaxEachPostBytes: this.scMaxEachPostBytes,
scStreamUpServerSecs: this.scStreamUpServerSecs,
noSSEHeader: this.noSSEHeader, noSSEHeader: this.noSSEHeader,
xPaddingBytes: this.xPaddingBytes, xPaddingBytes: this.xPaddingBytes,
mode: this.mode, mode: this.mode,
@@ -550,6 +554,7 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion = TLS_VERSION_OPTION.TLS13, maxVersion = TLS_VERSION_OPTION.TLS13,
cipherSuites = '', cipherSuites = '',
rejectUnknownSni = false, rejectUnknownSni = false,
serverNameToVerify = 'dns.google',
disableSystemRoot = false, disableSystemRoot = false,
enableSessionResumption = false, enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()], certificates = [new TlsStreamSettings.Cert()],
@@ -562,6 +567,7 @@ class TlsStreamSettings extends XrayCommonClass {
this.maxVersion = maxVersion; this.maxVersion = maxVersion;
this.cipherSuites = cipherSuites; this.cipherSuites = cipherSuites;
this.rejectUnknownSni = rejectUnknownSni; this.rejectUnknownSni = rejectUnknownSni;
this.serverNameToVerify = serverNameToVerify;
this.disableSystemRoot = disableSystemRoot; this.disableSystemRoot = disableSystemRoot;
this.enableSessionResumption = enableSessionResumption; this.enableSessionResumption = enableSessionResumption;
this.certs = certificates; this.certs = certificates;
@@ -593,6 +599,7 @@ class TlsStreamSettings extends XrayCommonClass {
json.maxVersion, json.maxVersion,
json.cipherSuites, json.cipherSuites,
json.rejectUnknownSni, json.rejectUnknownSni,
json.serverNameToVerify,
json.disableSystemRoot, json.disableSystemRoot,
json.enableSessionResumption, json.enableSessionResumption,
certs, certs,
@@ -608,6 +615,7 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion: this.maxVersion, maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites, cipherSuites: this.cipherSuites,
rejectUnknownSni: this.rejectUnknownSni, rejectUnknownSni: this.rejectUnknownSni,
serverNameToVerify: this.serverNameToVerify,
disableSystemRoot: this.disableSystemRoot, disableSystemRoot: this.disableSystemRoot,
enableSessionResumption: this.enableSessionResumption, enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs), certificates: TlsStreamSettings.toJsonArray(this.certs),
@@ -758,7 +766,7 @@ class RealityStreamSettings extends XrayCommonClass {
json.maxClient, json.maxClient,
json.maxTimediff, json.maxTimediff,
json.shortIds, json.shortIds,
json.settings, settings,
); );
} }
@@ -816,7 +824,7 @@ class SockoptStreamSettings extends XrayCommonClass {
mark = 0, mark = 0,
tproxy = "off", tproxy = "off",
tcpMptcp = false, tcpMptcp = false,
tcpNoDelay = false, penetrate = false,
domainStrategy = DOMAIN_STRATEGY_OPTION.USE_IP, domainStrategy = DOMAIN_STRATEGY_OPTION.USE_IP,
tcpMaxSeg = 1440, tcpMaxSeg = 1440,
dialerProxy = "", dialerProxy = "",
@@ -834,7 +842,7 @@ class SockoptStreamSettings extends XrayCommonClass {
this.mark = mark; this.mark = mark;
this.tproxy = tproxy; this.tproxy = tproxy;
this.tcpMptcp = tcpMptcp; this.tcpMptcp = tcpMptcp;
this.tcpNoDelay = tcpNoDelay; this.penetrate = penetrate;
this.domainStrategy = domainStrategy; this.domainStrategy = domainStrategy;
this.tcpMaxSeg = tcpMaxSeg; this.tcpMaxSeg = tcpMaxSeg;
this.dialerProxy = dialerProxy; this.dialerProxy = dialerProxy;
@@ -855,7 +863,7 @@ class SockoptStreamSettings extends XrayCommonClass {
json.mark, json.mark,
json.tproxy, json.tproxy,
json.tcpMptcp, json.tcpMptcp,
json.tcpNoDelay, json.penetrate,
json.domainStrategy, json.domainStrategy,
json.tcpMaxSeg, json.tcpMaxSeg,
json.dialerProxy, json.dialerProxy,
@@ -876,7 +884,7 @@ class SockoptStreamSettings extends XrayCommonClass {
mark: this.mark, mark: this.mark,
tproxy: this.tproxy, tproxy: this.tproxy,
tcpMptcp: this.tcpMptcp, tcpMptcp: this.tcpMptcp,
tcpNoDelay: this.tcpNoDelay, penetrate: this.penetrate,
domainStrategy: this.domainStrategy, domainStrategy: this.domainStrategy,
tcpMaxSeg: this.tcpMaxSeg, tcpMaxSeg: this.tcpMaxSeg,
dialerProxy: this.dialerProxy, dialerProxy: this.dialerProxy,
@@ -1278,7 +1286,7 @@ class Inbound extends XrayCommonClass {
obj.mode = xhttp.mode; obj.mode = xhttp.mode;
} }
if (security === 'tls') { if (tls === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
obj.sni = this.stream.tls.sni; obj.sni = this.stream.tls.sni;
} }
@@ -1778,6 +1786,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
enable = true, enable = true,
tgId = '', tgId = '',
subId = RandomUtil.randomLowerAndNum(16), subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0 reset = 0
) { ) {
super(); super();
@@ -1790,6 +1799,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
this.enable = enable; this.enable = enable;
this.tgId = tgId; this.tgId = tgId;
this.subId = subId; this.subId = subId;
this.comment = comment;
this.reset = reset; this.reset = reset;
} }
@@ -1804,6 +1814,7 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
json.enable, json.enable,
json.tgId, json.tgId,
json.subId, json.subId,
json.comment,
json.reset, json.reset,
); );
} }
@@ -1884,6 +1895,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
enable = true, enable = true,
tgId = '', tgId = '',
subId = RandomUtil.randomLowerAndNum(16), subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0 reset = 0
) { ) {
super(); super();
@@ -1896,6 +1908,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
this.enable = enable; this.enable = enable;
this.tgId = tgId; this.tgId = tgId;
this.subId = subId; this.subId = subId;
this.comment = comment;
this.reset = reset; this.reset = reset;
} }
@@ -1910,6 +1923,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.enable, json.enable,
json.tgId, json.tgId,
json.subId, json.subId,
json.comment,
json.reset, json.reset,
); );
} }
@@ -2020,6 +2034,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
enable = true, enable = true,
tgId = '', tgId = '',
subId = RandomUtil.randomLowerAndNum(16), subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0 reset = 0
) { ) {
super(); super();
@@ -2031,6 +2046,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
this.enable = enable; this.enable = enable;
this.tgId = tgId; this.tgId = tgId;
this.subId = subId; this.subId = subId;
this.comment = comment;
this.reset = reset; this.reset = reset;
} }
@@ -2044,6 +2060,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
enable: this.enable, enable: this.enable,
tgId: this.tgId, tgId: this.tgId,
subId: this.subId, subId: this.subId,
comment: this.comment,
reset: this.reset, reset: this.reset,
}; };
} }
@@ -2058,6 +2075,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
json.enable, json.enable,
json.tgId, json.tgId,
json.subId, json.subId,
json.comment,
json.reset, json.reset,
); );
} }
@@ -2177,6 +2195,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
enable = true, enable = true,
tgId = '', tgId = '',
subId = RandomUtil.randomLowerAndNum(16), subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0 reset = 0
) { ) {
super(); super();
@@ -2189,6 +2208,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
this.enable = enable; this.enable = enable;
this.tgId = tgId; this.tgId = tgId;
this.subId = subId; this.subId = subId;
this.comment = comment;
this.reset = reset; this.reset = reset;
} }
@@ -2203,6 +2223,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
enable: this.enable, enable: this.enable,
tgId: this.tgId, tgId: this.tgId,
subId: this.subId, subId: this.subId,
comment: this.comment,
reset: this.reset, reset: this.reset,
}; };
} }
@@ -2218,6 +2239,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
json.enable, json.enable,
json.tgId, json.tgId,
json.subId, json.subId,
json.comment,
json.reset, json.reset,
); );
} }

View File

@@ -293,9 +293,9 @@ class xHTTPStreamSettings extends CommonClass {
xmux = { xmux = {
maxConcurrency: "16-32", maxConcurrency: "16-32",
maxConnections: 0, maxConnections: 0,
cMaxReuseTimes: "64-128", cMaxReuseTimes: 0,
cMaxLifetimeMs: 0, hMaxRequestTimes: "600-900",
hMaxRequestTimes: "800-900", hMaxReusableSecs: "1800-3000",
hKeepAlivePeriod: 0, hKeepAlivePeriod: 0,
}, },
) { ) {
@@ -330,8 +330,8 @@ class xHTTPStreamSettings extends CommonClass {
maxConcurrency: this.xmux.maxConcurrency, maxConcurrency: this.xmux.maxConcurrency,
maxConnections: this.xmux.maxConnections, maxConnections: this.xmux.maxConnections,
cMaxReuseTimes: this.xmux.cMaxReuseTimes, cMaxReuseTimes: this.xmux.cMaxReuseTimes,
cMaxLifetimeMs: this.xmux.cMaxLifetimeMs,
hMaxRequestTimes: this.xmux.hMaxRequestTimes, hMaxRequestTimes: this.xmux.hMaxRequestTimes,
hMaxReusableSecs: this.xmux.hMaxReusableSecs,
hKeepAlivePeriod: this.xmux.hKeepAlivePeriod, hKeepAlivePeriod: this.xmux.hKeepAlivePeriod,
}, },
}; };
@@ -411,14 +411,14 @@ class SockoptStreamSettings extends CommonClass {
tcpFastOpen = false, tcpFastOpen = false,
tcpKeepAliveInterval = 0, tcpKeepAliveInterval = 0,
tcpMptcp = false, tcpMptcp = false,
tcpNoDelay = false penetrate = false
) { ) {
super(); super();
this.dialerProxy = dialerProxy; this.dialerProxy = dialerProxy;
this.tcpFastOpen = tcpFastOpen; this.tcpFastOpen = tcpFastOpen;
this.tcpKeepAliveInterval = tcpKeepAliveInterval; this.tcpKeepAliveInterval = tcpKeepAliveInterval;
this.tcpMptcp = tcpMptcp; this.tcpMptcp = tcpMptcp;
this.tcpNoDelay = tcpNoDelay; this.penetrate = penetrate;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@@ -428,7 +428,7 @@ class SockoptStreamSettings extends CommonClass {
json.tcpFastOpen, json.tcpFastOpen,
json.tcpKeepAliveInterval, json.tcpKeepAliveInterval,
json.tcpMptcp, json.tcpMptcp,
json.tcpNoDelay, json.penetrate,
); );
} }
@@ -438,7 +438,7 @@ class SockoptStreamSettings extends CommonClass {
tcpFastOpen: this.tcpFastOpen, tcpFastOpen: this.tcpFastOpen,
tcpKeepAliveInterval: this.tcpKeepAliveInterval, tcpKeepAliveInterval: this.tcpKeepAliveInterval,
tcpMptcp: this.tcpMptcp, tcpMptcp: this.tcpMptcp,
tcpNoDelay: this.tcpNoDelay, penetrate: this.penetrate,
}; };
} }
} }
@@ -721,6 +721,7 @@ class Outbound extends CommonClass {
let headerType = url.searchParams.get('headerType') ?? undefined; let headerType = url.searchParams.get('headerType') ?? undefined;
let host = url.searchParams.get('host') ?? undefined; let host = url.searchParams.get('host') ?? undefined;
let path = url.searchParams.get('path') ?? undefined; let path = url.searchParams.get('path') ?? undefined;
let mode = url.searchParams.get('mode') ?? undefined;
if (type === 'tcp' || type === 'none') { if (type === 'tcp' || type === 'none') {
stream.tcp = new TcpStreamSettings(headerType ?? 'none', host, path); stream.tcp = new TcpStreamSettings(headerType ?? 'none', host, path);

View File

@@ -26,6 +26,7 @@ class AllSetting {
this.xrayTemplateConfig = ""; this.xrayTemplateConfig = "";
this.secretEnable = false; this.secretEnable = false;
this.subEnable = false; this.subEnable = false;
this.subSyncEnable = true;
this.subListen = ""; this.subListen = "";
this.subPort = 2096; this.subPort = 2096;
this.subPath = "/sub/"; this.subPath = "/sub/";
@@ -43,7 +44,7 @@ class AllSetting {
this.subJsonMux = ""; this.subJsonMux = "";
this.subJsonRules = ""; this.subJsonRules = "";
this.timeLocation = "Asia/Tehran"; this.timeLocation = "Local";
if (data == null) { if (data == null) {
return return

View File

@@ -70,6 +70,41 @@ class HttpUtil {
} }
return msg; return msg;
} }
static async jsonPost(url, data) {
let msg;
try {
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
const resp = await fetch(basePath + url.replace(/^\/+|\/+$/g, ''), requestOptions);
const response = await resp.json();
msg = this._respToMsg({data : response});
} catch (e) {
msg = new Msg(false, e.toString());
}
this._handleMsg(msg);
return msg;
}
static async postWithModalJson(url, data, modal) {
if (modal) {
modal.loading(true);
}
const msg = await this.jsonPost(url, data);
if (modal) {
modal.loading(false);
if (msg instanceof Msg && msg.success) {
modal.close();
}
}
return msg;
}
} }
class PromiseUtil { class PromiseUtil {

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,10 @@
package controller package controller
import ( import (
"errors"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"x-ui/database/model" "x-ui/database/model"
"x-ui/web/service" "x-ui/web/service"
"x-ui/web/session" "x-ui/web/session"
@@ -33,9 +33,13 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/clientIps/:email", a.getClientIps) g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps) g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient", a.addInboundClient) g.POST("/addClient", a.addInboundClient)
g.POST("/addGroupClient", a.addGroupInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient) g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/delGroupClients", a.delGroupClients)
g.POST("/updateClient/:clientId", a.updateInboundClient) g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/updateClients", a.updateGroupInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic) g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetGroupClientTraffic", a.resetGroupClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics) g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics) g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients) g.POST("/delDepletedClients/:id", a.delDepletedClients)
@@ -190,6 +194,34 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
} }
} }
func (a *InboundController) addGroupInboundClient(c *gin.Context) {
var requestData []model.Inbound
err := c.ShouldBindJSON(&requestData)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
needRestart := true
for _, data := range requestData {
needRestart, err = a.inboundService.AddInboundClient(&data)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
}
jsonMsg(c, "Client(s) added", nil)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) delInboundClient(c *gin.Context) { func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@@ -211,6 +243,38 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
} }
} }
func (a *InboundController) delGroupClients(c *gin.Context) {
var requestData []struct {
InboundID int `json:"inboundId"`
ClientID string `json:"clientId"`
}
if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, "Invalid request data", err)
return
}
needRestart := false
for _, req := range requestData {
needRestartTmp, err := a.inboundService.DelInboundClient(req.InboundID, req.ClientID)
if err != nil {
jsonMsg(c, "Failed to delete client", err)
return
}
if needRestartTmp {
needRestart = true
}
}
jsonMsg(c, "Clients deleted successfully", nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) updateInboundClient(c *gin.Context) { func (a *InboundController) updateInboundClient(c *gin.Context) {
clientId := c.Param("clientId") clientId := c.Param("clientId")
@@ -234,6 +298,56 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
} }
} }
func (a *InboundController) updateGroupInboundClient(c *gin.Context) {
var requestData []map[string]interface{}
if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
needRestart := false
for _, item := range requestData {
inboundMap, ok := item["inbound"].(map[string]interface{})
if !ok {
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'inbound' to map"))
return
}
clientId, ok := item["clientId"].(string)
if !ok {
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'clientId' to string"))
return
}
inboundJSON, err := json.Marshal(inboundMap)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
var inboundModel model.Inbound
if err := json.Unmarshal(inboundJSON, &inboundModel); err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
if restart, err := a.inboundService.UpdateInboundClient(&inboundModel, clientId); err != nil {
jsonMsg(c, "Something went wrong!", err)
return
} else {
needRestart = needRestart || restart
}
}
jsonMsg(c, "Client updated", nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetClientTraffic(c *gin.Context) { func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@@ -253,6 +367,44 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
} }
} }
func (a *InboundController) resetGroupClientTraffic(c *gin.Context) {
var requestData []struct {
InboundID int `json:"inboundId"` // Map JSON "inboundId" to struct field "InboundID"
Email string `json:"email"` // Map JSON "email" to struct field "Email"
}
// Parse JSON body directly using ShouldBindJSON
if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, "Invalid request data", err)
return
}
needRestart := false
// Process each request data
for _, req := range requestData {
needRestartTmp, err := a.inboundService.ResetClientTraffic(req.InboundID, req.Email)
if err != nil {
jsonMsg(c, "Failed to reset client traffic", err)
return
}
// If any request requires a restart, set needRestart to true
if needRestartTmp {
needRestart = true
}
}
// Send response back to the client
jsonMsg(c, "Traffic reset for all clients", nil)
// Restart the service if required
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetAllTraffics(c *gin.Context) { func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics() err := a.inboundService.ResetAllTraffics()
if err != nil { if err != nil {

View File

@@ -40,6 +40,7 @@ type AllSetting struct {
TimeLocation string `json:"timeLocation" form:"timeLocation"` TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"` SecretEnable bool `json:"secretEnable" form:"secretEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"` SubEnable bool `json:"subEnable" form:"subEnable"`
SubSyncEnable bool `json:"subSyncEnable" form:"subSyncEnable"`
SubListen string `json:"subListen" form:"subListen"` SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"` SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"` SubPath string `json:"subPath" form:"subPath"`

View File

@@ -23,13 +23,15 @@
</tr-qr-bg> </tr-qr-bg>
</tr-qr-box> </tr-qr-box>
</template> </template>
<template v-for="(row, index) in qrModal.qrcodes"> <template v-if="!isJustSub">
<tr-qr-box class="qr-box"> <template v-for="(row, index) in qrModal.qrcodes">
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag> <tr-qr-box class="qr-box">
<tr-qr-bg class="qr-bg"> <a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas> <tr-qr-bg class="qr-bg">
</tr-qr-bg> <canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
</tr-qr-box> </tr-qr-bg>
</tr-qr-box>
</template>
</template> </template>
</tr-qr-modal> </tr-qr-modal>
</a-modal> </a-modal>
@@ -43,12 +45,14 @@
qrcodes: [], qrcodes: [],
clipboard: null, clipboard: null,
visible: false, visible: false,
isJustSub: false,
subId: '', subId: '',
show: function(title = '', dbInbound, client) { show: function(title = '', dbInbound, client, isJustSub = false) {
this.title = title; this.title = title;
this.dbInbound = dbInbound; this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.client = client; this.client = client;
this.isJustSub = isJustSub;
this.subId = ''; this.subId = '';
this.qrcodes = []; this.qrcodes = [];
if (this.inbound.protocol == Protocols.WIREGUARD) { if (this.inbound.protocol == Protocols.WIREGUARD) {
@@ -76,7 +80,9 @@
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#qrcode-modal', el: '#qrcode-modal',
data: { data: {
qrModal: qrModal, qrModal: qrModal,get isJustSub(){
return qrModal.isJustSub
}
}, },
methods: { methods: {
copyToClipboard(elementId, content) { copyToClipboard(elementId, content) {

View File

@@ -14,10 +14,10 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1"> <a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number> <a-input-number v-model.number="clientsBulkModal.firstNum" :min="1"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1"> <a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number> <a-input-number v-model.number="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0"> <a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0">
<a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input> <a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
@@ -26,7 +26,7 @@
<a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input> <a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2"> <a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number> <a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS"> <a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
<a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
@@ -61,7 +61,7 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number style="width: 50%" v-model="clientsBulkModal.tgId" min="0"></a-input-number> <a-input-number style="width: 50%" v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="app.ipLimitEnable"> <a-form-item v-if="app.ipLimitEnable">
<template slot="label"> <template slot="label">
@@ -73,7 +73,7 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model="clientsBulkModal.limitIp" min="0"></a-input-number> <a-input-number v-model.number="clientsBulkModal.limitIp" min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
@@ -85,7 +85,7 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number> <a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch> <a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>

View File

@@ -16,7 +16,16 @@
title: '', title: '',
okText: '', okText: '',
isEdit: false, isEdit: false,
group: {
canGroup: true,
isGroup: false,
currentClient: null,
inbounds: [],
clients: [],
editIds: []
},
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
dbInbounds: null,
inbound: new Inbound(), inbound: new Inbound(),
clients: [], clients: [],
clientStats: [], clientStats: [],
@@ -25,33 +34,126 @@
clientIps: null, clientIps: null,
delayedStart: false, delayedStart: false,
ok() { ok() {
if (clientModal.isEdit) { if (app.subSettings.enable && clientModal.group.isGroup && clientModal.group.canGroup) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId); const currentClient = clientModal.group.currentClient;
const { limitIp, comment, totalGB, expiryTime, reset, enable, subId, tgId, flow } = currentClient;
const uniqueEmails = clientModalApp.makeGroupEmailsUnique(clientModal.dbInbounds, currentClient.email, clientModal.group.clients);
clientModal.group.clients.forEach((client, index) => {
client.email = uniqueEmails[index];
client.limitIp = limitIp;
client.comment = comment;
client.totalGB = totalGB;
client.expiryTime = expiryTime;
client.reset = reset;
client.enable = enable;
if (subId) {
client.subId = subId;
}
if (tgId) {
client.tgId = tgId;
}
if (flow) {
client.flow = flow;
}
});
if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds, clientModal.group.editIds);
} else {
ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds);
}
} else { } else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id); if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
} }
}, },
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) { show({
title = '',
okText = '{{ i18n "sure" }}',
index = null,
dbInbound = null,
dbInbounds = null,
confirm = () => {
},
isEdit = false
}) {
this.group = {
canGroup: true,
isGroup: false,
currentClient: null,
inbounds: [],
clients: [],
editIds: []
}
this.dbInbounds = dbInbounds;
this.visible = true; this.visible = true;
this.title = title; this.title = title;
this.okText = okText; this.okText = okText;
this.isEdit = isEdit; this.isEdit = isEdit;
this.dbInbound = new DBInbound(dbInbound); if (app.subSettings.enable && dbInbounds !== null && Array.isArray(dbInbounds)) {
this.inbound = dbInbound.toInbound(); if (isEdit) {
this.clients = this.inbound.clients; this.showProcess(dbInbound, index);
this.index = index === null ? this.clients.length : index; let processSingleEdit = true
this.delayedStart = false; if (this.group.canGroup) {
if (isEdit) { this.group.currentClient = this.clients[this.index]
if (this.clients[index].expiryTime < 0) { const response = app.getSubGroupClients(dbInbounds, this.group.currentClient)
this.delayedStart = true; if (response.clients.length > 1) {
this.group.isGroup = true;
this.group.inbounds = response.inbounds
this.group.clients = response.clients
this.group.editIds = response.editIds
if (this.clients[index].expiryTime < 0) {
this.delayedStart = true;
}
processSingleEdit = false
}
}
if (processSingleEdit) {
this.singleEditClientProcess(index)
}
} else {
this.group.isGroup = true;
dbInbounds.forEach((dbInboundItem) => {
this.showProcess(dbInboundItem);
if (this.dbInbound.isMultiUser()) {
this.addClient(this.inbound.protocol, this.clients);
this.group.inbounds.push(dbInboundItem.id)
this.group.clients.push(this.clients[this.index])
}
})
this.group.currentClient = this.clients[this.index]
} }
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
} else { } else {
this.addClient(this.inbound.protocol, this.clients); this.showProcess(dbInbound, index);
if (isEdit) {
this.singleEditClientProcess(index)
} else {
this.addClient(this.inbound.protocol, this.clients);
}
} }
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email); this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm; this.confirm = confirm;
}, },
showProcess(dbInbound, index = null) {
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
if (this.dbInbound.isMultiUser()) {
this.clients = this.inbound.clients;
this.index = index === null ? this.clients.length : index;
this.delayedStart = false;
}
},
singleEditClientProcess(index) {
if (this.clients[index].expiryTime < 0) {
this.delayedStart = true;
}
this.oldClientId = this.getClientId(this.dbInbound.protocol, this.clients[index]);
},
getClientId(protocol, client) { getClientId(protocol, client) {
switch (protocol) { switch (protocol) {
case Protocols.TROJAN: return client.password; case Protocols.TROJAN: return client.password;
@@ -72,7 +174,7 @@
clientModal.visible = false; clientModal.visible = false;
clientModal.loading(false); clientModal.loading(false);
}, },
loading(loading=true) { loading(loading = true) {
clientModal.confirmLoading = loading; clientModal.confirmLoading = loading;
}, },
}; };
@@ -94,6 +196,18 @@
get isEdit() { get isEdit() {
return this.clientModal.isEdit; return this.clientModal.isEdit;
}, },
get isGroup() {
return this.clientModal.group.isGroup;
},
get isGroupEdit() {
return this.clientModal.group.canGroup;
},
set isGroupEdit(value) {
this.clientModal.group.canGroup = value;
if (!value) {
this.clientModal.singleEditClientProcess(this.clientModal.index)
}
},
get datepicker() { get datepicker() {
return app.datepicker; return app.datepicker;
}, },
@@ -120,6 +234,36 @@
}, },
}, },
methods: { methods: {
makeGroupEmailsUnique(dbInbounds, baseEmail, groupClients) {
// Extract the base part of the email (before the "__" if present)
const match = baseEmail.match(/^(.*?)__/);
const base = match ? match[1] : baseEmail;
// Generate initial emails for each client in the group
const generatedEmails = groupClients.map((_, index) => `${base}__${index + 1}`);
// Function to check if an email already exists in dbInbounds but belongs to a different subId
const isDuplicate = (emailToCheck, clientSubId) => {
return dbInbounds.some((dbInbound) => {
const settings = JSON.parse(dbInbound.settings);
const clients = settings && settings.clients ? settings.clients : [];
return clients.some(client => client.email === emailToCheck && client.subId !== clientSubId);
});
};
// Check if any of the generated emails are duplicates
const hasDuplicates = generatedEmails.some((email, index) => {
return isDuplicate(email, groupClients[index].subId);
});
// If duplicates exist, add a random string to the base email to ensure uniqueness
if (hasDuplicates) {
const randomString = `-${RandomUtil.randomLowerAndNum(4)}`;
return groupClients.map((_, index) => `${base}${randomString}__${index + 1}`);
}
return generatedEmails;
},
async getDBClientIps(email) { async getDBClientIps(email) {
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`); const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
if (!msg.success) { if (!msg.success) {
@@ -147,7 +291,22 @@
} catch (error) { } catch (error) {
} }
}, },
resetClientTraffic(email, dbInboundId, iconElement) { async resetClientTrafficHandler(client, dbInboundId, clients = []) {
if (clients.length > 0) {
const resetRequests = clients
.filter(client => {
const inbound = clientModal.dbInbounds.find(inbound => inbound.id === client.inboundId);
return inbound && app.hasClientStats(inbound, client.email);
}).map(client => ({ inboundId: client.inboundId, email: client.email}));
return HttpUtil.postWithModalJson('/panel/inbound/resetGroupClientTraffic', resetRequests, null)
} else {
return HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email)
}
},
resetClientTraffic(client, dbInboundId, iconElement) {
const subGroup = app.subSettings.enable && clientModal.group.isGroup && clientModal.group.canGroup && clientModal.dbInbounds && clientModal.dbInbounds.length > 0 ? app.getSubGroupClients(clientModal.dbInbounds, client) : [];
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
@@ -156,8 +315,8 @@
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
iconElement.disabled = true; iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email); const msg = await this.resetClientTrafficHandler(client, dbInboundId, clients);
if (msg.success) { if (msg && msg.success) {
this.clientModal.clientStats.up = 0; this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0; this.clientModal.clientStats.down = 0;
} }

View File

@@ -7,7 +7,7 @@
<a-input v-model.trim="fakednsModal.fakeDns.ipPool"></a-input> <a-input v-model.trim="fakednsModal.fakeDns.ipPool"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.fakedns.poolSize" }}'> <a-form-item label='{{ i18n "pages.xray.fakedns.poolSize" }}'>
<a-input-number style="width: 100%;" type="number" min="1" v-model.trim="fakednsModal.fakeDns.poolSize"></a-input-number> <a-input-number v-model.number="fakednsModal.fakeDns.poolSize" :min="1"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>

View File

@@ -3,6 +3,18 @@
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'> <a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch> <a-switch v-model="client.enable"></a-switch>
</a-form-item> </a-form-item>
<a-form-item v-if="isEdit && app.subSettings.enable && isGroup">
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.client.isGroupEditDesc" }}</span>
</template>
{{ i18n "pages.client.isGroupEdit" }}
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-switch v-model="isGroupEdit"></a-switch>
</a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
@@ -66,7 +78,10 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number style="width: 50%" v-model="client.tgId" min="0"></a-input-number> <a-input-number style="width: 50%" v-model.number="client.tgId" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
<a-input v-model.trim="client.comment"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="app.ipLimitEnable"> <a-form-item v-if="app.ipLimitEnable">
<template slot="label"> <template slot="label">
@@ -78,7 +93,7 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model="client.limitIp" min="0"></a-input-number> <a-input-number v-model.number="client.limitIp" min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit"> <a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit">
<template slot="label"> <template slot="label">
@@ -120,7 +135,7 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number> <a-input-number v-model.number="client._totalGB" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'> <a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'>
<a-tag :color="clientUsageColor(clientStats, app.trafficDiff)"> <a-tag :color="clientUsageColor(clientStats, app.trafficDiff)">
@@ -131,7 +146,7 @@
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet" <a-icon type="retweet"
@click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" @click="resetClientTraffic(client,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon> v-if="client.email.length > 0"></a-icon>
</a-tooltip> </a-tooltip>
</a-form-item> </a-form-item>

View File

@@ -41,7 +41,7 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number> <a-input-number v-model.number="dbInbound.totalGB" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>

View File

@@ -392,14 +392,14 @@
<a-form-item label="Max Reuse Times"> <a-form-item label="Max Reuse Times">
<a-input v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input> <a-input v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Max Lifetime (ms)">
<a-input v-model="outbound.stream.xhttp.xmux.cMaxLifetimeMs"></a-input>
</a-form-item>
<a-form-item label="Max Request Times"> <a-form-item label="Max Request Times">
<a-input v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input> <a-input v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Max Reusable Secs">
<a-input v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input>
</a-form-item>
<a-form-item label='Keep Alive Period'> <a-form-item label='Keep Alive Period'>
<a-input v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input> <a-input-number v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
@@ -469,13 +469,13 @@
<a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch> <a-switch v-model="outbound.stream.sockopt.tcpFastOpen"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="Keep Alive Interval"> <a-form-item label="Keep Alive Interval">
<a-input-number v-model="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number> <a-input-number v-model.number="outbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Multipath TCP"> <a-form-item label="Multipath TCP">
<a-switch v-model.trim="outbound.stream.sockopt.tcpMptcp"></a-switch> <a-switch v-model.trim="outbound.stream.sockopt.tcpMptcp"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="TCP No-Delay" v-if="outbound.stream.sockopt.tcpMptcp"> <a-form-item label="Penetrate">
<a-switch v-model="outbound.stream.sockopt.tcpNoDelay"></a-switch> <a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
</a-form-item> </a-form-item>
</template> </template>
@@ -486,10 +486,10 @@
</a-form-item> </a-form-item>
<template v-if="outbound.mux.enabled"> <template v-if="outbound.mux.enabled">
<a-form-item label="Concurrency"> <a-form-item label="Concurrency">
<a-input-number v-model="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number> <a-input-number v-model.number="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="xudp Concurrency"> <a-form-item label="xudp Concurrency">
<a-input-number v-model="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number> <a-input-number v-model.number="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="xudp UDP 443"> <a-form-item label="xudp UDP 443">
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme">

View File

@@ -18,7 +18,7 @@
</table> </table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<template v-if="inbound.isTcp && !inbound.stream.isReality"> <template v-if="inbound.isTcp">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button> <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
@@ -42,7 +42,7 @@
<a-input v-model="fallback.dest"></a-input> <a-input v-model="fallback.dest"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='xVer'> <a-form-item label='xVer'>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number> <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-divider style="margin:5px 0;"></a-divider> <a-divider style="margin:5px 0;"></a-divider>

View File

@@ -18,7 +18,7 @@
</table> </table>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<template v-if="inbound.isTcp && !inbound.stream.isReality"> <template v-if="inbound.isTcp">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks"> <a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button> <a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
@@ -42,7 +42,7 @@
<a-input v-model="fallback.dest"></a-input> <a-input v-model="fallback.dest"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='xVer'> <a-form-item label='xVer'>
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number> <a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
<a-divider style="margin:5px 0;"></a-divider> <a-divider style="margin:5px 0;"></a-divider>

View File

@@ -10,7 +10,7 @@
<a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input> <a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('host', '')"></a-button> <a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('', '')"></a-button>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers"> <a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">

View File

@@ -6,22 +6,22 @@
</a-form-item> </a-form-item>
<template v-if="inbound.stream.sockoptSwitch"> <template v-if="inbound.stream.sockoptSwitch">
<a-form-item label="Route Mark"> <a-form-item label="Route Mark">
<a-input-number v-model="inbound.stream.sockopt.mark" :min="0"></a-input-number> <a-input-number v-model.number="inbound.stream.sockopt.mark" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP Keep Alive Interval"> <a-form-item label="TCP Keep Alive Interval">
<a-input-number v-model="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number> <a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP Keep Alive Idle"> <a-form-item label="TCP Keep Alive Idle">
<a-input-number v-model="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number> <a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP Max Seg"> <a-form-item label="TCP Max Seg">
<a-input-number v-model="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number> <a-input-number v-model.number="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP User Timeout"> <a-form-item label="TCP User Timeout">
<a-input-number v-model="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number> <a-input-number v-model.number="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="TCP Window Clamp"> <a-form-item label="TCP Window Clamp">
<a-input-number v-model="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number> <a-input-number v-model.number="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Proxy Protocol"> <a-form-item label="Proxy Protocol">
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch> <a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
@@ -32,8 +32,8 @@
<a-form-item label="Multipath TCP"> <a-form-item label="Multipath TCP">
<a-switch v-model.trim="inbound.stream.sockopt.tcpMptcp"></a-switch> <a-switch v-model.trim="inbound.stream.sockopt.tcpMptcp"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="TCP No-Delay" v-if="inbound.stream.sockopt.tcpMptcp"> <a-form-item label="Penetrate">
<a-switch v-model.trim="inbound.stream.sockopt.tcpNoDelay"></a-switch> <a-switch v-model.trim="inbound.stream.sockopt.penetrate"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="V6 Only"> <a-form-item label="V6 Only">
<a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch> <a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch>

View File

@@ -7,7 +7,7 @@
<a-input v-model.trim="inbound.stream.xhttp.path"></a-input> <a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button icon="plus" size="small" @click="inbound.stream.xhttp.addHeader('host', '')"></a-button> <a-button icon="plus" size="small" @click="inbound.stream.xhttp.addHeader('', '')"></a-button>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.xhttp.headers"> <a-input-group compact v-for="(header, index) in inbound.stream.xhttp.headers">
@@ -17,7 +17,7 @@
</a-input> </a-input>
<a-input style="width: 50%" v-model.trim="header.value" <a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'> placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.stream.xhttp.removeHeader(index)">-</a-button> <a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.xhttp.removeHeader(index)"></a-button>
</a-input> </a-input>
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
@@ -28,11 +28,14 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="Max Buffered Upload" v-if="inbound.stream.xhttp.mode === 'packet-up'"> <a-form-item label="Max Buffered Upload" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input v-model.trim="inbound.stream.xhttp.scMaxBufferedPosts"></a-input> <a-input-number v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label="Max Upload Size (Byte)" v-if="inbound.stream.xhttp.mode === 'packet-up'"> <a-form-item label="Max Upload Size (Byte)" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input> <a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Stream-Up Server" v-if="inbound.stream.xhttp.mode === 'stream-up'">
<a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
</a-form-item>
<a-form-item label="Padding Bytes"> <a-form-item label="Padding Bytes">
<a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input> <a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input>
</a-form-item> </a-form-item>

View File

@@ -57,6 +57,9 @@
<a-form-item label="Session Resumption"> <a-form-item label="Session Resumption">
<a-switch v-model="inbound.stream.tls.enableSessionResumption"></a-switch> <a-switch v-model="inbound.stream.tls.enableSessionResumption"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label="Server Name To Verify">
<a-input v-model.trim="inbound.stream.tls.serverNameToVerify"></a-input>
</a-form-item>
<template v-for="cert,index in inbound.stream.tls.certs"> <template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'> <a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid"> <a-radio-group v-model="cert.useFile" button-style="solid">

View File

@@ -12,7 +12,7 @@
<template slot="title">{{ i18n "info" }}</template> <template slot="title">{{ i18n "info" }}</template>
<a-icon style="font-size: 24px;" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon> <a-icon style="font-size: 24px;" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip v-if="hasClientStats(record, client.email)">
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'> <a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon> <a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon>

View File

@@ -185,6 +185,33 @@
<a-tag>↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag> <a-tag>↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
</td> </td>
</tr> </tr>
<tr v-if="infoModal.clientSettings.comment">
<td>{{ i18n "comment" }}</td>
<td>
<a-tooltip :title="[[ infoModal.clientSettings.comment ]]">
<a-tag class="info-large-tag">[[ infoModal.clientSettings.comment ]]</a-tag>
</a-tooltip>
</td>
</tr>
<tr v-if="app.ipLimitEnable">
<td>{{ i18n "pages.inbounds.IPLimit" }}</td>
<td>
<a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
</td>
</tr>
<tr v-if="app.ipLimitEnable">
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
<td>
<a-tag>[[ infoModal.clientIps ]]</a-tag>
<a-icon type="sync" :spin="refreshing" @click="refreshIPs" style="margin: 0 5px;"></a-icon>
<a-tooltip :title="[[ dbInbound.address ]]">
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
</template>
<a-icon type="delete" @click="clearClientIps"></a-icon>
</a-tooltip>
</td>
</tr>
</table> </table>
<table style="display: inline-table; margin-block: 10px; width: 100%; text-align: center;"> <table style="display: inline-table; margin-block: 10px; width: 100%; text-align: center;">
<tr> <tr>
@@ -400,7 +427,7 @@
</tr> </tr>
<tr> <tr>
<td colspan="2"> <td colspan="2">
<tr-info-row v-for="(link,index) in infoModal.links" class="tr-info-row"> <tr-info-row class="tr-info-row">
<tr-info-title class="tr-info-title"> <tr-info-title class="tr-info-title">
<a-tag color="blue">Config</a-tag> <a-tag color="blue">Config</a-tag>
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
@@ -417,6 +444,18 @@
</template> </template>
</a-modal> </a-modal>
<script> <script>
function refreshIPs(email) {
return HttpUtil.post(`/panel/inbound/clientIps/${email}`).then((msg) => {
if (msg.success) {
try {
return JSON.parse(msg.obj).join(', ');
} catch (e) {
return msg.obj;
}
}
});
}
const infoModal = { const infoModal = {
visible: false, visible: false,
inbound: new Inbound(), inbound: new Inbound(),
@@ -431,6 +470,7 @@
isExpired: false, isExpired: false,
subLink: '', subLink: '',
subJsonLink: '', subJsonLink: '',
clientIps: '',
show(dbInbound, index) { show(dbInbound, index) {
this.index = index; this.index = index;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
@@ -438,6 +478,12 @@
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null; this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry; 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) : []; 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 (this.inbound.protocol == Protocols.WIREGUARD) { if (this.inbound.protocol == Protocols.WIREGUARD) {
this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\r\n') this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\r\n')
} else { } else {
@@ -466,6 +512,7 @@
el: '#inbound-info-modal', el: '#inbound-info-modal',
data: { data: {
infoModal, infoModal,
refreshing: false,
get dbInbound() { get dbInbound() {
return this.infoModal.dbInbound; return this.infoModal.dbInbound;
}, },
@@ -502,6 +549,26 @@
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down; remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;
return remained > 0 ? sizeFormat(remained) : '-'; return remained > 0 ? sizeFormat(remained) : '-';
}, },
refreshIPs() {
this.refreshing = true;
refreshIPs(this.infoModal.clientStats.email)
.then((ips) => {
this.infoModal.clientIps = ips;
})
.finally(() => {
this.refreshing = false;
});
},
clearClientIps() {
HttpUtil.post(`/panel/inbound/clearClientIps/${this.infoModal.clientStats.email}`)
.then((msg) => {
if (!msg.success) {
return;
}
this.infoModal.clientIps = 'No IP Record';
})
.catch(() => {});
},
}, },
}); });
</script> </script>

View File

@@ -14,6 +14,7 @@
confirmLoading: false, confirmLoading: false,
okText: '{{ i18n "sure" }}', okText: '{{ i18n "sure" }}',
isEdit: false, isEdit: false,
isGroup: false,
confirm: null, confirm: null,
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
@@ -61,6 +62,9 @@
get isEdit() { get isEdit() {
return inModal.isEdit; return inModal.isEdit;
}, },
get isGroup() {
return inModal.isGroup;
},
get client() { get client() {
return inModal.inbound.clients[0]; return inModal.inbound.clients[0];
}, },

View File

@@ -224,6 +224,10 @@
<a-icon type="rest"></a-icon> <a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }} {{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item> </a-menu-item>
<a-menu-item v-if="subSettings.enable && dbInbounds.length > 0" key="addGroupClient">
<a-icon type="usergroup-add"></a-icon>
{{ i18n "pages.client.groupAdd"}}
</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</a-col> </a-col>
@@ -859,6 +863,9 @@
case "delDepletedClients": case "delDepletedClients":
this.delDepletedClients(-1) this.delDepletedClients(-1)
break; break;
case "addGroupClient":
this.openGroupAddClient()
break;
} }
}, },
clickAction(action, dbInbound) { clickAction(action, dbInbound) {
@@ -1004,6 +1011,21 @@
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal); await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
}, },
openGroupAddClient() {
clientModal.show({
title: '{{ i18n "pages.client.groupAdd"}}',
okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbounds: this.dbInbounds,
confirm: async (clients, dbInboundIds) => {
await this.addGroupClient(clients, dbInboundIds, clientModal).then((res) => {
if(res){
this.showQrcode(dbInboundIds[0],clients[0], true)
}
});
},
isEdit: false
});
},
openAddClient(dbInboundId) { openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientModal.show({ clientModal.show({
@@ -1011,7 +1033,11 @@
okText: '{{ i18n "pages.client.submitAdd"}}', okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbound: dbInbound, dbInbound: dbInbound,
confirm: async (clients, dbInboundId) => { confirm: async (clients, dbInboundId) => {
await this.addClient(clients, dbInboundId, clientModal); await this.addClient(clients, dbInboundId, clientModal).then((res) => {
if(res){
this.showQrcode(dbInboundId,clients)
}
});
}, },
isEdit: false isEdit: false
}); });
@@ -1034,6 +1060,7 @@
clientModal.show({ clientModal.show({
title: '{{ i18n "pages.client.edit"}}', title: '{{ i18n "pages.client.edit"}}',
okText: '{{ i18n "pages.client.submitEdit"}}', okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbounds: this.dbInbounds,
dbInbound: dbInbound, dbInbound: dbInbound,
index: index, index: index,
confirm: async (client, dbInboundId, clientId) => { confirm: async (client, dbInboundId, clientId) => {
@@ -1059,12 +1086,36 @@
}; };
await this.submit(`/panel/inbound/addClient`, data, modal); await this.submit(`/panel/inbound/addClient`, data, modal);
}, },
async addGroupClient(clients, dbInboundIds, modal) {
const data = []
dbInboundIds.forEach((dbInboundId, index) => {
data.push({
id: dbInboundId,
settings: '{"clients": [' + clients[index].toString() + ']}',
})
})
return await this.submit(`/panel/inbound/addGroupClient`, data, modal, true)
},
async updateClient(client, dbInboundId, clientId) { async updateClient(client, dbInboundId, clientId) {
const data = { if (Array.isArray(client) && Array.isArray(dbInboundId) && Array.isArray(clientId)){
id: dbInboundId, const data = []
settings: '{"clients": [' + client.toString() + ']}', client.forEach((client, index) => {
}; data.push({
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal); clientId: clientId[index],
inbound: {
id: dbInboundId[index],
settings: '{"clients": [' + client.toString() + ']}',
}
})
})
await this.submit(`/panel/inbound/updateClients`, data, clientModal, true);
}else{
const data = {
id: dbInboundId,
settings: '{"clients": [' + client.toString() + ']}',
};
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
}
}, },
resetTraffic(dbInboundId) { resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -1092,22 +1143,100 @@
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId), onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
}); });
}, },
delClient(dbInboundId, client,confirmation = true) { async delClientHandler(dbInboundId, currentClient, clients = []) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); if (clients.length > 0) {
clientId = this.getClientId(dbInbound.protocol, client); const deleteRequestData = [];
for (const client of clients) {
const dbInbound = this.dbInbounds.find(inbound => inbound.id === client.inboundId);
if (dbInbound) {
const inbound = dbInbound.toInbound();
if (inbound && inbound.clients && inbound.clients.length === 1) {
let newClient = Inbound.Settings.getSettings(inbound.protocol).toString();
newClient = JSON.parse(newClient);
newClient = newClient && newClient.clients && newClient.clients.length > 0 ? JSON.stringify(newClient.clients[0], null, 2) : null;
if (newClient) {
const data = {
id: client.inboundId,
settings: '{"clients": [' + newClient + ']}',
};
await this.submit(`/panel/inbound/addClient`, data, null);
}
}
deleteRequestData.push({
inboundId: client.inboundId,
clientId: client.clientId,
});
}
}
await this.submit('/panel/inbound/delGroupClients', deleteRequestData, null, true);
} else {
const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
const clientId = this.getClientId(dbInbound.protocol, currentClient);
await this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
}
},
delClient(dbInboundId, currentClient, confirmation = true) {
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, currentClient) : [];
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
if (confirmation){ if (confirmation){
const clientEmails = clients.length > 0 ? clients.map(item => item.email) : currentClient.email
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email, title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + clientEmails,
content: '{{ i18n "pages.inbounds.deleteClientContent"}}', content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`), onOk: () => this.delClientHandler(dbInboundId, currentClient, clients),
}); });
} else { } else {
this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`); this.delClientHandler(dbInboundId, currentClient, clients)
} }
}, },
getSubGroupClients(dbInbounds, currentClient) {
const response = {
inbounds: [],
clients: [],
editIds: [],
};
if (!Array.isArray(dbInbounds) || dbInbounds.length === 0) {
return response;
}
if (!currentClient || !currentClient.subId) {
return response;
}
dbInbounds.forEach((dbInboundItem) => {
try {
const dbInbound = new DBInbound(dbInboundItem);
if (!dbInbound) {
return;
}
const inbound = dbInbound.toInbound();
if (!inbound || !Array.isArray(inbound.clients)) {
return;
}
inbound.clients.forEach((client) => {
if (client.subId === currentClient.subId) {
client.inboundId = dbInboundItem.id;
client.clientId = this.getClientId(dbInbound.protocol, client);
response.inbounds.push(dbInboundItem.id);
response.clients.push(client);
response.editIds.push(client.clientId);
}
});
} catch (error) {
console.error("Error processing dbInboundItem:", dbInboundItem, error);
}
});
return response;
},
getClientId(protocol, client) { getClientId(protocol, client) {
switch (protocol) { switch (protocol) {
case Protocols.TROJAN: return client.password; case Protocols.TROJAN: return client.password;
@@ -1135,10 +1264,10 @@
} }
return newDbInbound; return newDbInbound;
}, },
showQrcode(dbInboundId, client) { showQrcode(dbInboundId, client, isJustSub = false) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = this.checkFallback(dbInbound); newDbInbound = this.checkFallback(dbInbound);
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client); qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client, isJustSub);
}, },
showInfo(dbInboundId, client) { showInfo(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -1158,36 +1287,61 @@
}, },
async switchEnableClient(dbInboundId, client) { async switchEnableClient(dbInboundId, client) {
this.loading() this.loading()
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, client) : [];
inbound = dbInbound.toInbound(); if (subGroup && subGroup.clients && subGroup.clients.length > 0){
clients = inbound.clients; await this.updateClient(subGroup.clients.map(item => {
index = this.findIndexOfClient(dbInbound.protocol, clients, client); item.enable = !item.enable
clients[index].enable = !clients[index].enable; return item
clientId = this.getClientId(dbInbound.protocol, clients[index]); }), subGroup.inbounds, subGroup.editIds);
await this.updateClient(clients[index], dbInboundId, clientId); }else{
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
inbound = dbInbound.toInbound();
clients = inbound.clients;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index], dbInboundId, clientId);
}
this.loading(false); this.loading(false);
}, },
async submit(url, data, modal) { async submit(url, data, model, isJson = false) {
const msg = await HttpUtil.postWithModal(url, data, modal); const msg = isJson ? await HttpUtil.postWithModalJson(url, data, model) : await HttpUtil.postWithModal(url, data, model);
if (msg.success) { if (msg.success) {
await this.getDBInbounds(); await this.getDBInbounds();
} }
return msg
}, },
getInboundClients(dbInbound) { getInboundClients(dbInbound) {
return dbInbound.toInbound().clients; return dbInbound.toInbound().clients;
}, },
resetClientTrafficHandler(client, dbInboundId, clients = []) {
if (clients.length > 0){
const resetRequests = clients
.filter(client => {
const inbound = this.dbInbounds.find(inbound => inbound.id === client.inboundId);
return inbound && this.hasClientStats(inbound, client.email);
}).map(client => ({ inboundId: client.inboundId, email: client.email}));
this.submit('/panel/inbound/resetGroupClientTraffic', resetRequests, null, true)
}else {
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
}
},
resetClientTraffic(client, dbInboundId, confirmation = true) { resetClientTraffic(client, dbInboundId, confirmation = true) {
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, client) : [];
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
if (confirmation){ if (confirmation){
const clientEmails = clients.length > 0 ? clients.map(item => item.email) : client.email
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email, title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + clientEmails,
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email), onOk: () => this.resetClientTrafficHandler(client, dbInboundId, clients),
}) })
} else { } else {
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email); this.resetClientTrafficHandler(client, dbInboundId, clients);
} }
}, },
resetAllTraffic() { resetAllTraffic() {
@@ -1223,6 +1377,10 @@
isExpiry(dbInbound, index) { isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index); return dbInbound.toInbound().isExpiry(index);
}, },
hasClientStats(dbInbound, email) {
if (email.length == 0) return 0;
return !!dbInbound.clientStats.find(stats => stats.email === email);
},
getUpStats(dbInbound, email) { getUpStats(dbInbound, email) {
if (email.length == 0) return 0; if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email); clientStats = dbInbound.clientStats.find(stats => stats.email === email);
@@ -1310,7 +1468,7 @@
if (clients != null){ if (clients != null){
clients.forEach(c => { clients.forEach(c => {
if (c.subId && c.subId.length>0){ if (c.subId && c.subId.length>0){
subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId) subLinks.push(this.subSettings.subURI + c.subId)
} }
}) })
} }
@@ -1337,7 +1495,7 @@
if (clients != null){ if (clients != null){
clients.forEach(c => { clients.forEach(c => {
if (c.subId && c.subId.length>0){ if (c.subId && c.subId.length>0){
subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId) subLinks.push(this.subSettings.subURI + c.subId)
} }
}) })
} }

View File

@@ -268,6 +268,7 @@
<a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'> <a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'>
<a-list item-layout="horizontal"> <a-list item-layout="horizontal">
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subSyncEnable"}}' desc='{{ i18n "pages.settings.subSyncEnableDesc"}}' v-model="allSetting.subSyncEnable"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
@@ -381,14 +382,40 @@
</a-col> </a-col>
</a-row> </a-row>
<a-collapse v-if="enableDirect" style="margin-top: 14px;"> <a-collapse v-if="enableDirect" style="margin-top: 14px;">
<a-collapse-panel header='{{ i18n "pages.xray.directips"}}'> <a-collapse-panel header='{{ i18n "pages.settings.direct"}}'>
<a-list-item style="padding: 10px 20px"> <a-list-item>
<a-checkbox-group v-model="directIPs" :options="IPsOptions"></a-checkbox-group> <a-row style="padding: 0 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.directips" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<a-select mode="tags" style="width: 100%"
v-model="directIPs"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in directIPsOptions"> [[ p.label ]]
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-list-item> </a-list-item>
</a-collapse-panel> <a-list-item>
<a-collapse-panel header='{{ i18n "pages.xray.directdomains"}}'> <a-row style="padding: 0 20px">
<a-list-item style="padding: 10px 20px"> <a-col :lg="24" :xl="12">
<a-checkbox-group v-model="directDomains" :options="DomainsOptions"></a-checkbox-group> <a-list-item-meta
title='{{ i18n "pages.xray.directdomains" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<a-select mode="tags" style="width: 100%"
v-model="directDomains"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in diretDomainsOptions"> [[ p.label ]]
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-list-item> </a-list-item>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
@@ -439,7 +466,7 @@
sockopt: { sockopt: {
tcpKeepAliveIdle: 100, tcpKeepAliveIdle: 100,
tcpMptcp: true, tcpMptcp: true,
tcpNoDelay: true penetrate: true
} }
} }
}, },
@@ -476,25 +503,26 @@
] ]
}, },
], ],
IPsOptions: [ directIPsOptions: [
{ label: 'Private IP', value: 'private' }, { label: 'Private IP', value: 'geoip:private' },
{ label: '🇮🇷 Iran', value: 'ir' }, { label: '🇮🇷 Iran', value: 'geoip:ir' },
{ label: '🇨🇳 China', value: 'cn' }, { label: '🇨🇳 China', value: 'geoip:cn' },
{ label: '🇷🇺 Russia', value: 'ru' }, { label: '🇷🇺 Russia', value: 'geoip:ru' },
{ label: '🇻🇳 Vietnam', value: 'vn' }, { label: '🇻🇳 Vietnam', value: 'geoip:vn' },
{ label: '🇪🇸 Spain', value: 'es' }, { label: '🇪🇸 Spain', value: 'geoip:es' },
{ label: '🇮🇩 Indonesia', value: 'id' }, { label: '🇮🇩 Indonesia', value: 'geoip:id' },
{ label: '🇺🇦 Ukraine', value: 'ua' }, { label: '🇺🇦 Ukraine', value: 'geoip:ua' },
{ label: '🇹🇷 Türkiye', value: 'tr' }, { label: '🇹🇷 Türkiye', value: 'geoip:tr' },
{ label: '🇧🇷 Brazil', value: 'br' }, { label: '🇧🇷 Brazil', value: 'geoip:br' },
], ],
DomainsOptions: [ diretDomainsOptions: [
{ label: '🇮🇷 Iran', value: 'ir' }, { label: 'Private DNS', value: 'geosite:private' },
{ label: '🇨🇳 China', value: 'cn' }, { label: '🇮🇷 Iran', value: 'geosite:category-ir' },
{ label: '🇷🇺 Russia', value: 'ru' }, { label: '🇨🇳 China', value: 'geosite:cn' },
{ label: 'Apple', value: 'apple' }, { label: '🇷🇺 Russia', value: 'geosite:category-ru' },
{ label: 'Meta', value: 'meta' }, { label: 'Apple', value: 'geosite:apple' },
{ label: 'Google', value: 'google' }, { label: 'Meta', value: 'geosite:meta' },
{ label: 'Google', value: 'geosite:google' },
], ],
get remarkModel() { get remarkModel() {
rm = this.allSetting.remarkModel; rm = this.allSetting.remarkModel;
@@ -752,7 +780,7 @@
const rules = JSON.parse(this.allSetting.subJsonRules); const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return []; if (!Array.isArray(rules)) return [];
const ipRule = rules.find(r => r.ip); const ipRule = rules.find(r => r.ip);
return ipRule?.ip.map(d => d.replace("geoip:", "")) ?? []; return ipRule?.ip ?? [];
}, },
set: function (v) { set: function (v) {
let rules = JSON.parse(this.allSetting.subJsonRules); let rules = JSON.parse(this.allSetting.subJsonRules);
@@ -766,7 +794,7 @@
rules[ruleIndex].ip = []; rules[ruleIndex].ip = [];
v.forEach(d => { v.forEach(d => {
rules[ruleIndex].ip.push("geoip:" + d); rules[ruleIndex].ip.push(d);
}); });
} }
this.allSetting.subJsonRules = JSON.stringify(rules); this.allSetting.subJsonRules = JSON.stringify(rules);
@@ -778,34 +806,18 @@
const rules = JSON.parse(this.allSetting.subJsonRules); const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return []; if (!Array.isArray(rules)) return [];
const domainRule = rules.find(r => r.domain); const domainRule = rules.find(r => r.domain);
return domainRule?.domain.map(d => { return domainRule?.domain ?? [];
if (d.startsWith("geosite:category-")) {
return d.replace("geosite:category-", "");
}
return d.replace("geosite:", "");
})
?? [];
}, },
set: function (v) { set: function (v) {
let rules = JSON.parse(this.allSetting.subJsonRules); let rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return; if (!Array.isArray(rules)) return;
if (v.length == 0) { if (v.length == 0) {
rules = rules.filter(r => !r.domain); rules = rules.filter(r => !r.domain);
} else { } else {
let ruleIndex = rules.findIndex(r => r.domain); let ruleIndex = rules.findIndex(r => r.domain);
if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[0]) - 1; if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[0]) - 1;
rules[ruleIndex].domain = []; rules[ruleIndex].domain = v;
v.forEach(d => {
let category = '';
if (["cn", "apple", "meta", "google"].includes(d)) {
category = "";
} else if (["ru", "ir"].includes(d)) {
category = "category-";
}
rules[ruleIndex].domain.push("geosite:" + category + d);
});
} }
this.allSetting.subJsonRules = JSON.stringify(rules); this.allSetting.subJsonRules = JSON.stringify(rules);
} }

View File

@@ -143,6 +143,7 @@
</a-select> </a-select>
</a-col> </a-col>
</a-row> </a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.outboundTraffic"}}' desc='{{ i18n "pages.xray.outboundTrafficDesc"}}' v-model="outboundTraffic"></setting-list-item>
</a-list-item> </a-list-item>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.logConfigs" }}'> <a-collapse-panel header='{{ i18n "pages.xray.logConfigs" }}'>
@@ -901,8 +902,8 @@
{ label: 'Private IPs', value: 'geoip:private' }, { label: 'Private IPs', value: 'geoip:private' },
{ label: '🇮🇷 Iran', value: 'ext:geoip_IR.dat:ir' }, { label: '🇮🇷 Iran', value: 'ext:geoip_IR.dat:ir' },
{ label: '🇨🇳 China', value: 'geoip:cn' }, { label: '🇨🇳 China', value: 'geoip:cn' },
{ label: '🇷🇺 Russia', value: 'geoip:ru' }, { label: '🇷🇺 Russia', value: 'ext:geoip_RU.dat:ru' },
{ label: '🇻🇳 Vietnam', value: 'ext:geoip_VN.dat:vn' }, { label: '🇻🇳 Vietnam', value: 'geoip:vn' },
{ label: '🇪🇸 Spain', value: 'geoip:es' }, { label: '🇪🇸 Spain', value: 'geoip:es' },
{ label: '🇮🇩 Indonesia', value: 'geoip:id' }, { label: '🇮🇩 Indonesia', value: 'geoip:id' },
{ label: '🇺🇦 Ukraine', value: 'geoip:ua' }, { label: '🇺🇦 Ukraine', value: 'geoip:ua' },
@@ -915,15 +916,16 @@
{ label: '🇮🇷 .ایران', value: 'regexp:.*\\.xn--mgba3a4f16a$' }, { label: '🇮🇷 .ایران', value: 'regexp:.*\\.xn--mgba3a4f16a$' },
{ label: '🇨🇳 China', value: 'geosite:cn' }, { label: '🇨🇳 China', value: 'geosite:cn' },
{ label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' }, { label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' },
{ label: '🇷🇺 Russia', value: 'geosite:category-ru' }, { label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru' }, { label: '🇷🇺 .ru', value: 'regexp:.*\\.ru$' },
{ label: '🇻🇳 Vietnam', value: 'ext:geosite_VN.dat:vn' }, { label: '🇷🇺 .su', value: 'regexp:.*\\.su$' },
{ label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' },
{ label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' }, { label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' },
], ],
BlockDomainsOptions: [ BlockDomainsOptions: [
{ label: 'Ads All', value: 'geosite:category-ads-all' }, { label: 'Ads All', value: 'geosite:category-ads-all' },
{ label: 'Ads IR 🇮🇷', value: 'ext:geosite_IR.dat:category-ads-all' }, { label: 'Ads IR 🇮🇷', value: 'ext:geosite_IR.dat:category-ads-all' },
{ label: 'Ads VN 🇻🇳', value: 'ext:geosite_VN.dat:ads' }, { label: 'Ads RU 🇷🇺', value: 'ext:geosite_RU.dat:category-ads-all' },
{ label: 'Malware 🇮🇷', value: 'ext:geosite_IR.dat:malware' }, { label: 'Malware 🇮🇷', value: 'ext:geosite_IR.dat:malware' },
{ label: 'Phishing 🇮🇷', value: 'ext:geosite_IR.dat:phishing' }, { label: 'Phishing 🇮🇷', value: 'ext:geosite_IR.dat:phishing' },
{ label: 'Cryptominers 🇮🇷', value: 'ext:geosite_IR.dat:cryptominers' }, { label: 'Cryptominers 🇮🇷', value: 'ext:geosite_IR.dat:cryptominers' },
@@ -932,9 +934,10 @@
{ label: '🇮🇷 .ایران', value: 'regexp:.*\\.xn--mgba3a4f16a$' }, { label: '🇮🇷 .ایران', value: 'regexp:.*\\.xn--mgba3a4f16a$' },
{ label: '🇨🇳 China', value: 'geosite:cn' }, { label: '🇨🇳 China', value: 'geosite:cn' },
{ label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' }, { label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' },
{ label: '🇷🇺 Russia', value: 'geosite:category-ru' }, { label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru' }, { label: '🇷🇺 .ru', value: 'regexp:.*\\.ru' },
{ label: '🇻🇳 Vietnam', value: 'ext:geosite_VN.dat:vn' }, { label: '🇷🇺 .su', value: 'regexp:.*\\.su$' },
{ label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' },
{ label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' }, { label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' },
], ],
ServicesOptions: [ ServicesOptions: [
@@ -1821,6 +1824,18 @@
this.templateSettings = newTemplateSettings; this.templateSettings = newTemplateSettings;
} }
}, },
outboundTraffic: {
get: function () {
if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy.system.statsOutboundDownlink) return false;
return this.templateSettings.policy.system.statsOutboundDownlink;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.policy.system.statsOutboundDownlink = newValue;
newTemplateSettings.policy.system.statsOutboundUplink = newValue;
this.templateSettings = newTemplateSettings;
}
},
maskAddressLog: { maskAddressLog: {
get: function () { get: function () {
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.maskAddress) return ""; if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.maskAddress) return "";

View File

@@ -106,7 +106,7 @@ func (j *CheckClientIpJob) hasLimitIp() bool {
func (j *CheckClientIpJob) processLogFile() bool { func (j *CheckClientIpJob) processLogFile() bool {
ipRegex := regexp.MustCompile(`from \[?([0-9a-fA-F:.]+)\]?:\d+ accepted`) ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`)
emailRegex := regexp.MustCompile(`email: (.+)$`) emailRegex := regexp.MustCompile(`email: (.+)$`)
accessLogPath, _ := xray.GetAccessLogPath() accessLogPath, _ := xray.GetAccessLogPath()

View File

@@ -0,0 +1,178 @@
package job
import (
"encoding/json"
"fmt"
"time"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/xray"
"gorm.io/gorm"
)
type SyncClientTrafficJob struct {
subClientsCollection map[string][]string
}
func NewClientTrafficSyncJob() *SyncClientTrafficJob {
return new(SyncClientTrafficJob)
}
func (j *SyncClientTrafficJob) Run() {
// Step 1: Group clients by SubID
subClientsCollection := j.collectClientsGroupedBySubId()
// Step 2: Sync client traffics for each SubID group
for subId, emails := range subClientsCollection {
err := j.syncClientTraffics(map[string][]string{subId: emails})
if err != nil {
logger.Error("Failed to sync traffics for SubID ", subId, ": ", err)
}
}
}
// collectClientsGroupedBySubId groups clients by their SubIDs
func (j *SyncClientTrafficJob) collectClientsGroupedBySubId() map[string][]string {
db := database.GetDB()
result := make(map[string][]string)
// Fetch all inbounds
var inbounds []*model.Inbound
if err := db.Model(&model.Inbound{}).Find(&inbounds).Error; err != nil {
logger.Error("Error fetching inbounds: ", err)
return result // Return empty map on error
}
// Process each inbound
for _, inbound := range inbounds {
if inbound.Settings == "" {
continue
}
settingsMap, err := parseSettings(inbound.Settings, uint(inbound.Id))
if err != nil {
logger.Error(err)
continue
}
clients, ok := settingsMap["clients"].([]interface{})
if !ok {
continue
}
processClients(clients, result)
}
// Remove SubIDs with one or fewer emails
filterSingleEmailSubIDs(result)
return result
}
// parseSettings unmarshals the JSON settings and returns it as a map
func parseSettings(settings string, inboundID uint) (map[string]interface{}, error) {
if !json.Valid([]byte(settings)) {
return nil, fmt.Errorf("Invalid JSON format in Settings for inbound ID %d", inboundID)
}
var tempData map[string]interface{}
if err := json.Unmarshal([]byte(settings), &tempData); err != nil {
return nil, fmt.Errorf("Error unmarshalling settings for inbound ID %d: %v", inboundID, err)
}
return tempData, nil
}
// processClients extracts SubID and email from the clients and populates the result map
func processClients(clients []interface{}, result map[string][]string) {
for _, client := range clients {
clientMap, ok := client.(map[string]interface{})
if !ok {
continue
}
subId, ok := clientMap["subId"].(string)
if !ok || subId == "" {
continue
}
email, ok := clientMap["email"].(string)
if !ok || email == "" {
continue
}
result[subId] = append(result[subId], email)
}
}
// filterSingleEmailSubIDs removes SubIDs with one or fewer emails from the result map
func filterSingleEmailSubIDs(result map[string][]string) {
for subId, emails := range result {
if len(emails) <= 1 {
delete(result, subId)
}
}
}
// syncClientTraffics synchronizes traffic data for each SubID group
func (j *SyncClientTrafficJob) syncClientTraffics(result map[string][]string) error {
for subId, emails := range result {
db := database.GetDB()
// Step 1: Calculate maxUp and maxDown (outside transaction)
var maxUp, maxDown int64
err := calculateMaxTraffic(db, emails, &maxUp, &maxDown)
if err != nil {
logger.Error("Failed to calculate max traffic for SubID ", subId, ": ", err)
continue
}
// Step 2: Update traffic data with retry mechanism
err = retryOperation(func() error {
return updateTraffic(db, emails, maxUp, maxDown)
}, 5, 100*time.Millisecond)
if err != nil {
logger.Error("Failed to update client traffics for SubID ", subId, ": ", err)
}
}
return nil
}
// calculateMaxTraffic calculates max up and down traffic for a group of emails
func calculateMaxTraffic(db *gorm.DB, emails []string, maxUp, maxDown *int64) error {
return db.Model(&xray.ClientTraffic{}).
Where("email IN ?", emails).
Select("MAX(up) AS max_up, MAX(down) AS max_down").
Row().
Scan(maxUp, maxDown)
}
// updateTraffic updates the traffic data in the database within a transaction
func updateTraffic(db *gorm.DB, emails []string, maxUp, maxDown int64) error {
return db.Transaction(func(tx *gorm.DB) error {
return tx.Model(&xray.ClientTraffic{}).
Where("email IN ?", emails).
Updates(map[string]interface{}{
"up": maxUp,
"down": maxDown,
}).Error
})
}
// retryOperation retries an operation multiple times with a delay
func retryOperation(operation func() error, maxRetries int, delay time.Duration) error {
var err error
for i := 0; i < maxRetries; i++ {
err = operation()
if err == nil {
return nil
}
logger.Info(fmt.Sprintf("Retry %d/%d failed: %v", i+1, maxRetries, err))
time.Sleep(delay)
}
return err
}

View File

@@ -51,8 +51,8 @@
"system": { "system": {
"statsInboundDownlink": true, "statsInboundDownlink": true,
"statsInboundUplink": true, "statsInboundUplink": true,
"statsOutboundDownlink": true, "statsOutboundDownlink": false,
"statsOutboundUplink": true "statsOutboundUplink": false
} }
}, },
"routing": { "routing": {

View File

@@ -286,8 +286,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
} }
if (major == 1 && minor == 8 && patch == 24) || if (major == 1 && minor == 8 && patch == 24) ||
(major == 24 && ((minor > 11) || (minor == 11 && patch >= 30))) || (major >= 25) {
(major > 24) {
versions = append(versions, release.TagName) versions = append(versions, release.TagName)
} }
} }

View File

@@ -37,7 +37,7 @@ var defaultValueMap = map[string]string{
"expireDiff": "0", "expireDiff": "0",
"trafficDiff": "0", "trafficDiff": "0",
"remarkModel": "-ieo", "remarkModel": "-ieo",
"timeLocation": "Asia/Tehran", "timeLocation": "Local",
"tgBotEnable": "false", "tgBotEnable": "false",
"tgBotToken": "", "tgBotToken": "",
"tgBotProxy": "", "tgBotProxy": "",
@@ -50,6 +50,7 @@ var defaultValueMap = map[string]string{
"tgLang": "en-US", "tgLang": "en-US",
"secretEnable": "false", "secretEnable": "false",
"subEnable": "false", "subEnable": "false",
"subSyncEnable": "true",
"subListen": "", "subListen": "",
"subPort": "2096", "subPort": "2096",
"subPath": "/sub/", "subPath": "/sub/",
@@ -416,6 +417,14 @@ func (s *SettingService) GetSubEnable() (bool, error) {
return s.getBool("subEnable") return s.getBool("subEnable")
} }
func (s *SettingService) GetSubSyncEnable() (bool, error) {
return s.getBool("subSyncEnable")
}
func (s *SettingService) SetSubSyncEnable(value bool) error {
return s.setBool("subSyncEnable", value)
}
func (s *SettingService) GetSubListen() (string, error) { func (s *SettingService) GetSubListen() (string, error) {
return s.getString("subListen") return s.getString("subListen")
} }
@@ -544,6 +553,7 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
"defaultKey": func() (interface{}, error) { return s.GetKeyFile() }, "defaultKey": func() (interface{}, error) { return s.GetKeyFile() },
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotEnabled() }, "tgBotEnable": func() (interface{}, error) { return s.GetTgbotEnabled() },
"subEnable": func() (interface{}, error) { return s.GetSubEnable() }, "subEnable": func() (interface{}, error) { return s.GetSubEnable() },
"subSyncEnable": func() (interface{}, error) { return s.GetSubSyncEnable() },
"subURI": func() (interface{}, error) { return s.GetSubURI() }, "subURI": func() (interface{}, error) { return s.GetSubURI() },
"subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() }, "subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() }, "remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },

View File

@@ -44,6 +44,7 @@
"monitor" = "Listen IP" "monitor" = "Listen IP"
"certificate" = "Digital Certificate" "certificate" = "Digital Certificate"
"fail" = "Failed" "fail" = "Failed"
"comment" = "Comment"
"success" = "Successfully" "success" = "Successfully"
"getVersion" = "Get Version" "getVersion" = "Get Version"
"install" = "Install" "install" = "Install"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "Add Client" "add" = "Add Client"
"groupAdd" = "Add subscription user"
"isGroupEdit" = "Group editing"
"isGroupEditDesc" = "All clients with the same subscription are edited"
"edit" = "Edit Client" "edit" = "Edit Client"
"submitAdd" = "Add Client" "submitAdd" = "Add Client"
"submitEdit" = "Save Changes" "submitEdit" = "Save Changes"
@@ -286,6 +290,8 @@
"subSettings" = "Subscription" "subSettings" = "Subscription"
"subEnable" = "Enable Subscription Service" "subEnable" = "Enable Subscription Service"
"subEnableDesc" = "Enables the subscription service." "subEnableDesc" = "Enables the subscription service."
"subSyncEnable" = "Enable Subscription Sync"
"subSyncEnableDesc" = "Traffic from clients with the same subscription will be synchronized every 10 seconds."
"subListen" = "Listen IP" "subListen" = "Listen IP"
"subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)" "subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)"
"subPort" = "Listen Port" "subPort" = "Listen Port"
@@ -367,6 +373,8 @@
"errorLogDesc" = "The file path for the error log. The special value 'none' disabled error logs" "errorLogDesc" = "The file path for the error log. The special value 'none' disabled error logs"
"dnsLog" = "DNS Log" "dnsLog" = "DNS Log"
"dnsLogDesc" = "Whether to enable DNS query logs" "dnsLogDesc" = "Whether to enable DNS query logs"
"outboundTraffic" = "Outbounds Traffic"
"outboundTrafficDesc" = "Whether to enable outbound traffic"
"maskAddress" = "Mask Address" "maskAddress" = "Mask Address"
"maskAddressDesc" = "IP address mask, when enabled, will automatically replace the IP address that appears in the log." "maskAddressDesc" = "IP address mask, when enabled, will automatically replace the IP address that appears in the log."

View File

@@ -44,6 +44,7 @@
"monitor" = "Listening IP" "monitor" = "Listening IP"
"certificate" = "Certificado Digital" "certificate" = "Certificado Digital"
"fail" = "Falló" "fail" = "Falló"
"comment" = "Comentario"
"success" = "Éxito" "success" = "Éxito"
"getVersion" = "Obtener versión" "getVersion" = "Obtener versión"
"install" = "Instalar" "install" = "Instalar"
@@ -189,6 +190,7 @@
[pages.client] [pages.client]
"add" = "Agregar Cliente" "add" = "Agregar Cliente"
"groupAdd" = "Agregar usuario de suscripción"
"edit" = "Editar Cliente" "edit" = "Editar Cliente"
"submitAdd" = "Agregar Cliente" "submitAdd" = "Agregar Cliente"
"submitEdit" = "Guardar Cambios" "submitEdit" = "Guardar Cambios"
@@ -286,6 +288,8 @@
"subSettings" = "Suscripción" "subSettings" = "Suscripción"
"subEnable" = "Habilitar Servicio" "subEnable" = "Habilitar Servicio"
"subEnableDesc" = "Función de suscripción con configuración separada." "subEnableDesc" = "Función de suscripción con configuración separada."
"subSyncEnable" = "Habilitar sincronización de suscripciones"
"subSyncEnableDesc" = "El tráfico de los clientes con la misma suscripción se sincronizará cada 10 segundos."
"subListen" = "Listening IP" "subListen" = "Listening IP"
"subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs." "subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
"subPort" = "Puerto de Suscripción" "subPort" = "Puerto de Suscripción"
@@ -367,6 +371,8 @@
"errorLogDesc" = "La ruta del archivo para el registro de errores. El valor especial 'none' desactiva los registros de errores." "errorLogDesc" = "La ruta del archivo para el registro de errores. El valor especial 'none' desactiva los registros de errores."
"dnsLog" = "Registro DNS" "dnsLog" = "Registro DNS"
"dnsLogDesc" = "Si habilitar los registros de consulta DNS" "dnsLogDesc" = "Si habilitar los registros de consulta DNS"
"outboundTraffic" = "Tráfico saliente"
"outboundTrafficDesc" = "Si se debe habilitar el tráfico saliente"
"maskAddress" = "Enmascarar Dirección" "maskAddress" = "Enmascarar Dirección"
"maskAddressDesc" = "Máscara de dirección IP, cuando se habilita, reemplazará automáticamente la dirección IP que aparece en el registro." "maskAddressDesc" = "Máscara de dirección IP, cuando se habilita, reemplazará automáticamente la dirección IP que aparece en el registro."

View File

@@ -44,6 +44,7 @@
"monitor" = "آی‌پی اتصال" "monitor" = "آی‌پی اتصال"
"certificate" = "گواهی دیجیتال" "certificate" = "گواهی دیجیتال"
"fail" = "ناموفق" "fail" = "ناموفق"
"comment" = "توضیحات"
"success" = "موفق" "success" = "موفق"
"getVersion" = "دریافت نسخه" "getVersion" = "دریافت نسخه"
"install" = "نصب" "install" = "نصب"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "کاربر جدید" "add" = "کاربر جدید"
"groupAdd" = "افزودن کاربر سابسکریپشن"
"isGroupEdit" = "ویرایش گروهی"
"isGroupEditDesc" = "همه کاربران با سابسکریپشن یکسان ویرایش می شوند"
"edit" = "ویرایش کاربر" "edit" = "ویرایش کاربر"
"submitAdd" = "اضافه کردن" "submitAdd" = "اضافه کردن"
"submitEdit" = "ذخیره تغییرات" "submitEdit" = "ذخیره تغییرات"
@@ -286,6 +290,8 @@
"subSettings" = "سابسکریپشن" "subSettings" = "سابسکریپشن"
"subEnable" = "فعال‌سازی سرویس سابسکریپشن" "subEnable" = "فعال‌سازی سرویس سابسکریپشن"
"subEnableDesc" = "سرویس سابسکریپشن‌ را فعال‌می‌کند" "subEnableDesc" = "سرویس سابسکریپشن‌ را فعال‌می‌کند"
"subSyncEnable" = "فعال‌سازی همگام سازی سابسکریپشن"
"subSyncEnableDesc" = "ترافیک کلاینت هایی که سابسکریپشن یکسان دارند هر ۱۰ ثانیه همگام می‌شوند."
"subListen" = "آدرس آی‌پی" "subListen" = "آدرس آی‌پی"
"subListenDesc" = "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید" "subListenDesc" = "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید"
"subPort" = "پورت" "subPort" = "پورت"
@@ -367,6 +373,8 @@
"errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند" "errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند"
"dnsLog" = "گزارش DNS" "dnsLog" = "گزارش DNS"
"dnsLogDesc" = "آیا ثبت‌های درخواست DNS را فعال کنید" "dnsLogDesc" = "آیا ثبت‌های درخواست DNS را فعال کنید"
"outboundTraffic" = "ترافیک خروجی"
"outboundTrafficDesc" = "فعال کردن ترافیک خروجی"
"maskAddress" = "پنهان کردن آدرس" "maskAddress" = "پنهان کردن آدرس"
"maskAddressDesc" = "پوشش آدرس IP، هنگامی که فعال می‌شود، به طور خودکار آدرس IP که در لاگ ظاهر می‌شود را جایگزین می‌کند." "maskAddressDesc" = "پوشش آدرس IP، هنگامی که فعال می‌شود، به طور خودکار آدرس IP که در لاگ ظاهر می‌شود را جایگزین می‌کند."

View File

@@ -44,6 +44,7 @@
"monitor" = "IP Pemantauan" "monitor" = "IP Pemantauan"
"certificate" = "Sertifikat Digital" "certificate" = "Sertifikat Digital"
"fail" = "Gagal" "fail" = "Gagal"
"comment" = "Komentar"
"success" = "Berhasil" "success" = "Berhasil"
"getVersion" = "Dapatkan Versi" "getVersion" = "Dapatkan Versi"
"install" = "Instal" "install" = "Instal"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "Tambah Klien" "add" = "Tambah Klien"
"groupAdd" = "Tambahkan pengguna langganan"
"isGroupEdit" = "Pengeditan grup"
"isGroupEditDesc" = "Semua klien dengan langganan yang sama akan diedit"
"edit" = "Edit Klien" "edit" = "Edit Klien"
"submitAdd" = "Tambah Klien" "submitAdd" = "Tambah Klien"
"submitEdit" = "Simpan Perubahan" "submitEdit" = "Simpan Perubahan"
@@ -286,6 +290,8 @@
"subSettings" = "Langganan" "subSettings" = "Langganan"
"subEnable" = "Aktifkan Layanan Langganan" "subEnable" = "Aktifkan Layanan Langganan"
"subEnableDesc" = "Mengaktifkan layanan langganan." "subEnableDesc" = "Mengaktifkan layanan langganan."
"subSyncEnable" = "Aktifkan Sinkronisasi Langganan"
"subSyncEnableDesc" = "Lalu lintas dari klien dengan langganan yang sama akan disinkronkan setiap 10 detik."
"subListen" = "IP Pendengar" "subListen" = "IP Pendengar"
"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)" "subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
"subPort" = "Port Pendengar" "subPort" = "Port Pendengar"
@@ -367,6 +373,8 @@
"errorLogDesc" = "Jalur file untuk log kesalahan. Nilai khusus 'tidak ada' menonaktifkan log kesalahan" "errorLogDesc" = "Jalur file untuk log kesalahan. Nilai khusus 'tidak ada' menonaktifkan log kesalahan"
"dnsLog" = "Log DNS" "dnsLog" = "Log DNS"
"dnsLogDesc" = "Apakah akan mengaktifkan log kueri DNS" "dnsLogDesc" = "Apakah akan mengaktifkan log kueri DNS"
"outboundTraffic" = "Lalu Lintas Keluar"
"outboundTrafficDesc" = "Apakah mengaktifkan lalu lintas keluar"
"maskAddress" = "Alamat Masker" "maskAddress" = "Alamat Masker"
"maskAddressDesc" = "Masker alamat IP, ketika diaktifkan, akan secara otomatis mengganti alamat IP yang muncul di log." "maskAddressDesc" = "Masker alamat IP, ketika diaktifkan, akan secara otomatis mengganti alamat IP yang muncul di log."

View File

@@ -44,6 +44,7 @@
"monitor" = "監視" "monitor" = "監視"
"certificate" = "証明書" "certificate" = "証明書"
"fail" = "失敗" "fail" = "失敗"
"comment" = "コメント"
"success" = "成功" "success" = "成功"
"getVersion" = "バージョン取得" "getVersion" = "バージョン取得"
"install" = "インストール" "install" = "インストール"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "クライアント追加" "add" = "クライアント追加"
"groupAdd" = "サブスクリプション ユーザーの追加"
"isGroupEdit" = "グループの編集"
"isGroupEditDesc" = "同じサブスクリプションを持つすべてのクライアントが編集されます"
"edit" = "クライアント編集" "edit" = "クライアント編集"
"submitAdd" = "クライアント追加" "submitAdd" = "クライアント追加"
"submitEdit" = "変更を保存" "submitEdit" = "変更を保存"
@@ -286,6 +290,8 @@
"subSettings" = "サブスクリプション設定" "subSettings" = "サブスクリプション設定"
"subEnable" = "サブスクリプションサービスを有効にする" "subEnable" = "サブスクリプションサービスを有効にする"
"subEnableDesc" = "サブスクリプションサービス機能を有効にする" "subEnableDesc" = "サブスクリプションサービス機能を有効にする"
"subSyncEnable" = "サブスクリプション同期を有効にする"
"subSyncEnableDesc" = "同じサブスクリプションを持つクライアントからのトラフィックは 10 秒ごとに同期されます。"
"subListen" = "監視IP" "subListen" = "監視IP"
"subListenDesc" = "サブスクリプションサービスが監視するIPアドレス空白にするとすべてのIPを監視" "subListenDesc" = "サブスクリプションサービスが監視するIPアドレス空白にするとすべてのIPを監視"
"subPort" = "監視ポート" "subPort" = "監視ポート"
@@ -367,6 +373,8 @@
"errorLogDesc" = "エラーログのファイルパス。特殊値 'none' はエラーログを無効にします" "errorLogDesc" = "エラーログのファイルパス。特殊値 'none' はエラーログを無効にします"
"dnsLog" = "DNS ログ" "dnsLog" = "DNS ログ"
"dnsLogDesc" = "DNSクエリのログを有効にするかどうか" "dnsLogDesc" = "DNSクエリのログを有効にするかどうか"
"outboundTraffic" = "アウトバウンドトラフィック"
"outboundTrafficDesc" = "アウトバウンドトラフィックを有効にするかどうか"
"maskAddress" = "アドレスをマスク" "maskAddress" = "アドレスをマスク"
"maskAddressDesc" = "IPアドレスをマスクし、有効にするとログに表示されるIPアドレスを自動的に置き換えます" "maskAddressDesc" = "IPアドレスをマスクし、有効にするとログに表示されるIPアドレスを自動的に置き換えます"

View File

@@ -44,6 +44,7 @@
"monitor" = "IP de Escuta" "monitor" = "IP de Escuta"
"certificate" = "Certificado Digital" "certificate" = "Certificado Digital"
"fail" = "Falhou" "fail" = "Falhou"
"comment" = "Comentário"
"success" = "Com Sucesso" "success" = "Com Sucesso"
"getVersion" = "Obter Versão" "getVersion" = "Obter Versão"
"install" = "Instalar" "install" = "Instalar"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "Adicionar Cliente" "add" = "Adicionar Cliente"
"groupAdd" = "Adicionar usuário de assinatura"
"isGroupEdit" = "Edição de grupo"
"isGroupEditDesc" = "Todos os clientes com a mesma assinatura são editados"
"edit" = "Editar Cliente" "edit" = "Editar Cliente"
"submitAdd" = "Adicionar Cliente" "submitAdd" = "Adicionar Cliente"
"submitEdit" = "Salvar Alterações" "submitEdit" = "Salvar Alterações"
@@ -286,6 +290,8 @@
"subSettings" = "Assinatura" "subSettings" = "Assinatura"
"subEnable" = "Ativar Serviço de Assinatura" "subEnable" = "Ativar Serviço de Assinatura"
"subEnableDesc" = "Ativa o serviço de assinatura." "subEnableDesc" = "Ativa o serviço de assinatura."
"subSyncEnable" = "Habilitar sincronização de assinatura"
"subSyncEnableDesc" = "O tráfego de clientes com a mesma assinatura será sincronizado a cada 10 segundos."
"subListen" = "IP de Escuta" "subListen" = "IP de Escuta"
"subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)" "subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)"
"subPort" = "Porta de Escuta" "subPort" = "Porta de Escuta"
@@ -367,6 +373,8 @@
"errorLogDesc" = "O caminho do arquivo para o log de erros. O valor especial 'none' desativa os logs de erro." "errorLogDesc" = "O caminho do arquivo para o log de erros. O valor especial 'none' desativa os logs de erro."
"dnsLog" = "Log DNS" "dnsLog" = "Log DNS"
"dnsLogDesc" = "Se ativar logs de consulta DNS" "dnsLogDesc" = "Se ativar logs de consulta DNS"
"outboundTraffic" = "Tráfego de saída"
"outboundTrafficDesc" = "Se deve habilitar o tráfego de saída"
"maskAddress" = "Mascarar Endereço" "maskAddress" = "Mascarar Endereço"
"maskAddressDesc" = "Máscara de endereço IP, quando ativado, substitui automaticamente o endereço IP que aparece no log." "maskAddressDesc" = "Máscara de endereço IP, quando ativado, substitui automaticamente o endereço IP que aparece no log."

View File

@@ -41,16 +41,17 @@
"offline" = "Офлайн" "offline" = "Офлайн"
"online" = "Онлайн" "online" = "Онлайн"
"domainName" = "Домен" "domainName" = "Домен"
"monitor" = "Порт IP" "monitor" = "Слушать IP"
"certificate" = "Цифровой сертификат" "certificate" = "Цифровой сертификат"
"fail" = "Неудачно" "fail" = "Неудачно"
"comment" = "Комментарий"
"success" = "Успешно" "success" = "Успешно"
"getVersion" = "Узнать версию" "getVersion" = "Узнать версию"
"install" = "Установка" "install" = "Установка"
"clients" = "Клиенты" "clients" = "Клиенты"
"usage" = "Использование" "usage" = "Использование"
"secretToken" = "Секретный токен" "secretToken" = "Секретный токен"
"remained" = "остались" "remained" = "Остаток"
"security" = "Безопасность" "security" = "Безопасность"
"secAlertTitle" = "Предупреждение системы безопасности" "secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных" "secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
@@ -78,7 +79,7 @@
"invalidFormData" = "Недопустимый формат данных" "invalidFormData" = "Недопустимый формат данных"
"emptyUsername" = "Введите имя пользователя" "emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль" "emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль" "wrongUsernameOrPassword" = "Неверное имя пользователя, пароль или секретный токен."
"successLogin" = "Успешный вход" "successLogin" = "Успешный вход"
[pages.index] [pages.index]
@@ -108,7 +109,7 @@
"config" = "Конфигурация" "config" = "Конфигурация"
"backup" = "Бэкап и восстановление" "backup" = "Бэкап и восстановление"
"backupTitle" = "База данных бэкапа и восстановления" "backupTitle" = "База данных бэкапа и восстановления"
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных" "backupDescription" = "Рекомендуется сделать резервную копию перед восстановлением базы данных."
"exportDatabase" = "Экспорт базы данных" "exportDatabase" = "Экспорт базы данных"
"importDatabase" = "Импорт базы данных" "importDatabase" = "Импорт базы данных"
@@ -146,7 +147,7 @@
"meansNoLimit" = "= Без ограничений (значение: ГБ)" "meansNoLimit" = "= Без ограничений (значение: ГБ)"
"totalFlow" = "Общий расход" "totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы не истекало" "leaveBlankToNeverExpire" = "Оставьте пустым, чтобы не истекало"
"noRecommendKeepDefault" = "Не рекомендуется оставлять настройки по умолчанию" "noRecommendKeepDefault" = "Рекомендуется оставить настройки по умолчанию"
"certificatePath" = "Путь к файлу" "certificatePath" = "Путь к файлу"
"certificateContent" = "Содержимое файла" "certificateContent" = "Содержимое файла"
"publicKey" = "Публичный ключ" "publicKey" = "Публичный ключ"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "Добавить пользователя" "add" = "Добавить пользователя"
"groupAdd" = "Добавить пользователя подписки"
"isGroupEdit" = "Групповое редактирование"
"isGroupEditDesc" = "Все клиенты с одинаковой подпиской редактируются"
"edit" = "Редактировать пользователя" "edit" = "Редактировать пользователя"
"submitAdd" = "Добавить пользователя" "submitAdd" = "Добавить пользователя"
"submitEdit" = "Сохранить изменения" "submitEdit" = "Сохранить изменения"
@@ -286,6 +290,8 @@
"subSettings" = "Подписка" "subSettings" = "Подписка"
"subEnable" = "Включить службу" "subEnable" = "Включить службу"
"subEnableDesc" = "Функция подписки с отдельной конфигурацией" "subEnableDesc" = "Функция подписки с отдельной конфигурацией"
"subSyncEnable" = "Включить синхронизацию подписки"
"subSyncEnableDesc" = "Трафик от клиентов с одинаковой подпиской будет синхронизироваться каждые 10 секунд."
"subListen" = "Прослушивание IP" "subListen" = "Прослушивание IP"
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса" "subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
"subPort" = "Порт подписки" "subPort" = "Порт подписки"
@@ -367,6 +373,8 @@
"errorLogDesc" = "Путь к файлу журнала ошибок. Специальное значение «none» отключает журналы ошибок." "errorLogDesc" = "Путь к файлу журнала ошибок. Специальное значение «none» отключает журналы ошибок."
"dnsLog" = "DNS Журнал" "dnsLog" = "DNS Журнал"
"dnsLogDesc" = "Включить логи запросов DNS" "dnsLogDesc" = "Включить логи запросов DNS"
"outboundTraffic" = "Исходящий трафик"
"outboundTrafficDesc" = "Включить исходящий трафик"
"maskAddress" = "Маскировать Адрес" "maskAddress" = "Маскировать Адрес"
"maskAddressDesc" = "Маска IP-адреса, при активации автоматически заменяет IP-адрес, который появляется в логе." "maskAddressDesc" = "Маска IP-адреса, при активации автоматически заменяет IP-адрес, который появляется в логе."

View File

@@ -44,6 +44,7 @@
"monitor" = "Dinleme IP" "monitor" = "Dinleme IP"
"certificate" = "Dijital Sertifika" "certificate" = "Dijital Sertifika"
"fail" = "Başarısız" "fail" = "Başarısız"
"comment" = "Yorum"
"success" = "Başarılı" "success" = "Başarılı"
"getVersion" = "Sürümü Al" "getVersion" = "Sürümü Al"
"install" = "Yükle" "install" = "Yükle"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "Müşteri Ekle" "add" = "Müşteri Ekle"
"groupAdd" = "Abonelik kullanıcısı ekle"
"isGroupEdit" = "Grup düzenleme"
"isGroupEditDesc" = "Aynı aboneliğe sahip tüm istemciler düzenlendi"
"edit" = "Müşteriyi Düzenle" "edit" = "Müşteriyi Düzenle"
"submitAdd" = "Müşteri Ekle" "submitAdd" = "Müşteri Ekle"
"submitEdit" = "Değişiklikleri Kaydet" "submitEdit" = "Değişiklikleri Kaydet"
@@ -286,6 +290,8 @@
"subSettings" = "Abonelik" "subSettings" = "Abonelik"
"subEnable" = "Abonelik Hizmetini Etkinleştir" "subEnable" = "Abonelik Hizmetini Etkinleştir"
"subEnableDesc" = "Abonelik hizmetini etkinleştirir." "subEnableDesc" = "Abonelik hizmetini etkinleştirir."
"subSyncEnable" = "Abonelik Senkronizasyonunu Etkinleştir"
"subSyncEnableDesc" = "Aynı aboneliğe sahip istemcilerden gelen trafik her 10 saniyede bir senkronize edilecektir."
"subListen" = "Dinleme IP" "subListen" = "Dinleme IP"
"subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)" "subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)"
"subPort" = "Dinleme Portu" "subPort" = "Dinleme Portu"
@@ -367,6 +373,8 @@
"errorLogDesc" = "Hata günlüğü için dosya yolu. 'none' özel değeri hata günlüklerini devre dışı bırakır" "errorLogDesc" = "Hata günlüğü için dosya yolu. 'none' özel değeri hata günlüklerini devre dışı bırakır"
"dnsLog" = "DNS Günlüğü" "dnsLog" = "DNS Günlüğü"
"dnsLogDesc" = "DNS sorgu günlüklerini etkinleştirin" "dnsLogDesc" = "DNS sorgu günlüklerini etkinleştirin"
"outboundTraffic" = "Gidenler trafiği"
"outboundTrafficDesc" = ıkış trafiğini etkinleştirip etkinleştirmeyeceğiniz"
"maskAddress" = "Adres Maskesi" "maskAddress" = "Adres Maskesi"
"maskAddressDesc" = "IP adresi maskesi, etkinleştirildiğinde, günlükte görünen IP adresini otomatik olarak değiştirecektir." "maskAddressDesc" = "IP adresi maskesi, etkinleştirildiğinde, günlükte görünen IP adresini otomatik olarak değiştirecektir."

View File

@@ -44,6 +44,7 @@
"monitor" = "Слухати IP" "monitor" = "Слухати IP"
"certificate" = "Цифровий сертифікат" "certificate" = "Цифровий сертифікат"
"fail" = "Помилка" "fail" = "Помилка"
"comment" = "Коментар"
"success" = "Успішно" "success" = "Успішно"
"getVersion" = "Отримати версію" "getVersion" = "Отримати версію"
"install" = "Встановити" "install" = "Встановити"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "Додати клієнта" "add" = "Додати клієнта"
"groupAdd" = "Додати підписаного користувача"
"isGroupEdit" = "Редагування групи"
"isGroupEditDesc" = "Всі клієнти з однаковою підпискою редагуються"
"edit" = "Редагувати клієнта" "edit" = "Редагувати клієнта"
"submitAdd" = "Додати клієнта" "submitAdd" = "Додати клієнта"
"submitEdit" = "Зберегти зміни" "submitEdit" = "Зберегти зміни"
@@ -286,6 +290,8 @@
"subSettings" = "Підписка" "subSettings" = "Підписка"
"subEnable" = "Увімкнути службу підписки" "subEnable" = "Увімкнути службу підписки"
"subEnableDesc" = "Вмикає службу підписки." "subEnableDesc" = "Вмикає службу підписки."
"subSyncEnable" = "Увімкнути синхронізацію підписки"
"subSyncEnableDesc" = "Трафік від клієнтів з однаковою підпискою буде синхронізовано кожні 10 секунд."
"subListen" = "Слухати IP" "subListen" = "Слухати IP"
"subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)" "subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)"
"subPort" = "Слухати порт" "subPort" = "Слухати порт"
@@ -367,6 +373,8 @@
"errorLogDesc" = "Шлях до файлу журналу помилок. Спеціальне значення 'none' вимикає журнали помилок" "errorLogDesc" = "Шлях до файлу журналу помилок. Спеціальне значення 'none' вимикає журнали помилок"
"dnsLog" = "Журнал DNS" "dnsLog" = "Журнал DNS"
"dnsLogDesc" = "Чи включити журнали запитів DNS" "dnsLogDesc" = "Чи включити журнали запитів DNS"
"outboundTraffic" = "Вихідний трафік"
"outboundTrafficDesc" = "Чи потрібно увімкнути вихідний трафік"
"maskAddress" = "Маскувати Адресу" "maskAddress" = "Маскувати Адресу"
"maskAddressDesc" = "Маска IP-адреси, при активації автоматично замінює IP-адресу, яка з'являється у журналі." "maskAddressDesc" = "Маска IP-адреси, при активації автоматично замінює IP-адресу, яка з'являється у журналі."

View File

@@ -44,6 +44,7 @@
"monitor" = "Listening IP" "monitor" = "Listening IP"
"certificate" = "Chứng chỉ số" "certificate" = "Chứng chỉ số"
"fail" = "Thất bại" "fail" = "Thất bại"
"comment" = "Bình luận"
"success" = "Thành công" "success" = "Thành công"
"getVersion" = "Lấy phiên bản" "getVersion" = "Lấy phiên bản"
"install" = "Cài đặt" "install" = "Cài đặt"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "Thêm người dùng" "add" = "Thêm người dùng"
"groupAdd" = "Thêm người dùng đăng ký"
"isGroupEdit" = "Chỉnh sửa nhóm"
"isGroupEditDesc" = "Tất cả người dùng có cùng đăng ký đều có thể được chỉnh sửa"
"edit" = "Chỉnh sửa người dùng" "edit" = "Chỉnh sửa người dùng"
"submitAdd" = "Thêm" "submitAdd" = "Thêm"
"submitEdit" = "Lưu thay đổi" "submitEdit" = "Lưu thay đổi"
@@ -286,6 +290,8 @@
"subSettings" = "Gói đăng ký" "subSettings" = "Gói đăng ký"
"subEnable" = "Bật dịch vụ" "subEnable" = "Bật dịch vụ"
"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng" "subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng"
"subSyncEnable" = "Bật đồng bộ đăng ký"
"subSyncEnableDesc" = "Lưu lượng truy cập từ các máy khách có cùng đăng ký sẽ được đồng bộ sau mỗi 10 giây."
"subListen" = "Listening IP" "subListen" = "Listening IP"
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP" "subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
"subPort" = "Cổng gói đăng ký" "subPort" = "Cổng gói đăng ký"
@@ -367,6 +373,8 @@
"errorLogDesc" = "Đường dẫn tệp cho nhật ký lỗi. Nhật ký lỗi bị vô hiệu hóa có giá trị đặc biệt 'không'" "errorLogDesc" = "Đường dẫn tệp cho nhật ký lỗi. Nhật ký lỗi bị vô hiệu hóa có giá trị đặc biệt 'không'"
"dnsLog" = "Nhật ký DNS" "dnsLog" = "Nhật ký DNS"
"dnsLogDesc" = "Có bật nhật ký truy vấn DNS không" "dnsLogDesc" = "Có bật nhật ký truy vấn DNS không"
"outboundTraffic" = "Lưu lượng đi ra"
"outboundTrafficDesc" = "Có nên bật lưu lượng ra không"
"maskAddress" = "Ẩn Địa Chỉ" "maskAddress" = "Ẩn Địa Chỉ"
"maskAddressDesc" = "Mặt nạ địa chỉ IP, khi được bật, sẽ tự động thay thế địa chỉ IP xuất hiện trong nhật ký." "maskAddressDesc" = "Mặt nạ địa chỉ IP, khi được bật, sẽ tự động thay thế địa chỉ IP xuất hiện trong nhật ký."

View File

@@ -44,6 +44,7 @@
"monitor" = "监听" "monitor" = "监听"
"certificate" = "数字证书" "certificate" = "数字证书"
"fail" = "失败" "fail" = "失败"
"comment" = "评论"
"success" = "成功" "success" = "成功"
"getVersion" = "获取版本" "getVersion" = "获取版本"
"install" = "安装" "install" = "安装"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "添加客户端" "add" = "添加客户端"
"groupAdd" = "添加订阅用户"
"isGroupEdit" = "群组编辑"
"isGroupEditDesc" = "所有具有相同订阅的客户端均被编辑"
"edit" = "编辑客户端" "edit" = "编辑客户端"
"submitAdd" = "添加客户端" "submitAdd" = "添加客户端"
"submitEdit" = "保存修改" "submitEdit" = "保存修改"
@@ -286,6 +290,8 @@
"subSettings" = "订阅设置" "subSettings" = "订阅设置"
"subEnable" = "启用订阅服务" "subEnable" = "启用订阅服务"
"subEnableDesc" = "启用订阅服务功能" "subEnableDesc" = "启用订阅服务功能"
"subSyncEnable" = "启用订阅同步"
"subSyncEnableDesc" = "具有相同订阅的客户端的流量将每 10 秒同步一次。"
"subListen" = "监听 IP" "subListen" = "监听 IP"
"subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP" "subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP"
"subPort" = "监听端口" "subPort" = "监听端口"
@@ -367,6 +373,8 @@
"errorLogDesc" = "错误日志的文件路径。特殊值 'none' 禁用错误日志" "errorLogDesc" = "错误日志的文件路径。特殊值 'none' 禁用错误日志"
"dnsLog" = "DNS 日志" "dnsLog" = "DNS 日志"
"dnsLogDesc" = "是否启用 DNS 查询日志" "dnsLogDesc" = "是否启用 DNS 查询日志"
"outboundTraffic" = "出站流量"
"outboundTrafficDesc" = "是否启用出站流量"
"maskAddress" = "隐藏地址" "maskAddress" = "隐藏地址"
"maskAddressDesc" = "IP 地址掩码,启用时会自动替换日志中出现的 IP 地址。" "maskAddressDesc" = "IP 地址掩码,启用时会自动替换日志中出现的 IP 地址。"

View File

@@ -44,6 +44,7 @@
"monitor" = "監聽" "monitor" = "監聽"
"certificate" = "憑證" "certificate" = "憑證"
"fail" = "失敗" "fail" = "失敗"
"comment" = "評論"
"success" = "成功" "success" = "成功"
"getVersion" = "獲取版本" "getVersion" = "獲取版本"
"install" = "安裝" "install" = "安裝"
@@ -189,6 +190,9 @@
[pages.client] [pages.client]
"add" = "新增客戶端" "add" = "新增客戶端"
"groupAdd" = "新增訂閱使用者"
"isGroupEdit" = "群組編輯"
"isGroupEditDesc" = "所有具有相同訂閱的用戶端都被編輯"
"edit" = "編輯客戶端" "edit" = "編輯客戶端"
"submitAdd" = "新增客戶端" "submitAdd" = "新增客戶端"
"submitEdit" = "儲存修改" "submitEdit" = "儲存修改"
@@ -286,6 +290,8 @@
"subSettings" = "訂閱設定" "subSettings" = "訂閱設定"
"subEnable" = "啟用訂閱服務" "subEnable" = "啟用訂閱服務"
"subEnableDesc" = "啟用訂閱服務功能" "subEnableDesc" = "啟用訂閱服務功能"
"subSyncEnable" = "啟用訂閱同步"
"subSyncEnableDesc" = "來自具有相同訂閱的客戶端的流量將每 10 秒同步一次。"
"subListen" = "監聽 IP" "subListen" = "監聽 IP"
"subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP" "subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP"
"subPort" = "監聽埠" "subPort" = "監聽埠"
@@ -367,6 +373,8 @@
"errorLogDesc" = "錯誤日誌的檔案路徑。特殊值 'none' 禁用錯誤日誌" "errorLogDesc" = "錯誤日誌的檔案路徑。特殊值 'none' 禁用錯誤日誌"
"dnsLog" = "DNS 日誌" "dnsLog" = "DNS 日誌"
"dnsLogDesc" = "是否啟用 DNS 查詢日誌" "dnsLogDesc" = "是否啟用 DNS 查詢日誌"
"outboundTraffic" = "出站流量"
"outboundTrafficDesc" = "是否啟用出站流量"
"maskAddress" = "隱藏地址" "maskAddress" = "隱藏地址"
"maskAddressDesc" = "IP 地址掩碼,啟用時會自動替換日誌中出現的 IP 地址。" "maskAddressDesc" = "IP 地址掩碼,啟用時會自動替換日誌中出現的 IP 地址。"

View File

@@ -260,6 +260,13 @@ func (s *Server) startTask() {
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob()) s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
}() }()
isSubEnable, err1 := s.settingService.GetSubEnable()
isSubSyncEnable, err2 := s.settingService.GetSubSyncEnable()
if err1 == nil && err2 == nil && isSubEnable && isSubSyncEnable {
// Sync the client traffic with the same SubId every 10 seconds
s.cron.AddJob("@every 10s", job.NewClientTrafficSyncJob())
}
// check client ips from log file every 10 sec // check client ips from log file every 10 sec
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob()) s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())

300
x-ui.sh
View File

@@ -2,6 +2,7 @@
red='\033[0;31m' red='\033[0;31m'
green='\033[0;32m' green='\033[0;32m'
blue='\033[0;34m'
yellow='\033[0;33m' yellow='\033[0;33m'
plain='\033[0m' plain='\033[0m'
@@ -682,10 +683,13 @@ show_xray_status() {
} }
firewall_menu() { firewall_menu() {
echo -e "${green}\t1.${plain} Install Firewall & open ports" echo -e "${green}\t1.${plain} ${green}Install${plain} Firewall"
echo -e "${green}\t2.${plain} Allowed List" echo -e "${green}\t2.${plain} Port List [numbered]"
echo -e "${green}\t3.${plain} Delete Ports from List" echo -e "${green}\t3.${plain} ${green}Open${plain} Ports"
echo -e "${green}\t4.${plain} Disable Firewall" echo -e "${green}\t4.${plain} ${red}Delete${plain} Ports from List"
echo -e "${green}\t5.${plain} ${green}Enable${plain} Firewall"
echo -e "${green}\t6.${plain} ${red}Disable${plain} Firewall"
echo -e "${green}\t7.${plain} Firewall Status"
echo -e "${green}\t0.${plain} Back to Main Menu" echo -e "${green}\t0.${plain} Back to Main Menu"
read -p "Choose an option: " choice read -p "Choose an option: " choice
case "$choice" in case "$choice" in
@@ -693,19 +697,31 @@ firewall_menu() {
show_menu show_menu
;; ;;
1) 1)
open_ports install_firewall
firewall_menu firewall_menu
;; ;;
2) 2)
sudo ufw status ufw status numbered
firewall_menu firewall_menu
;; ;;
3) 3)
delete_ports open_ports
firewall_menu firewall_menu
;; ;;
4) 4)
sudo ufw disable delete_ports
firewall_menu
;;
5)
ufw enable
firewall_menu
;;
6)
ufw disable
firewall_menu
;;
7)
ufw status verbose
firewall_menu firewall_menu
;; ;;
*) *)
@@ -715,7 +731,7 @@ firewall_menu() {
esac esac
} }
open_ports() { install_firewall() {
if ! command -v ufw &>/dev/null; then if ! command -v ufw &>/dev/null; then
echo "ufw firewall is not installed. Installing now..." echo "ufw firewall is not installed. Installing now..."
apt-get update apt-get update
@@ -733,13 +749,16 @@ open_ports() {
ufw allow ssh ufw allow ssh
ufw allow http ufw allow http
ufw allow https ufw allow https
ufw allow 2053/tcp ufw allow 2053/tcp #webPort
ufw allow 2096/tcp #subport
# Enable the firewall # Enable the firewall
ufw --force enable ufw --force enable
fi fi
}
# Prompt the user to enter a list of ports open_ports() {
# Prompt the user to enter the ports they want to open
read -p "Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): " ports read -p "Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): " ports
# Check if the input is valid # Check if the input is valid
@@ -755,66 +774,110 @@ open_ports() {
# Split the range into start and end ports # Split the range into start and end ports
start_port=$(echo $port | cut -d'-' -f1) start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2) end_port=$(echo $port | cut -d'-' -f2)
# Open the port range
ufw allow $start_port:$end_port/tcp ufw allow $start_port:$end_port/tcp
ufw allow $start_port:$end_port/udp ufw allow $start_port:$end_port/udp
else else
# Open the single port
ufw allow "$port" ufw allow "$port"
fi fi
done done
# Confirm that the ports are open # Confirm that the ports are opened
echo "The following ports are now open:" echo "Opened the specified ports:"
ufw status | grep "ALLOW" | grep -Eo "[0-9]+(/[a-z]+)?" for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
echo "Firewall status:" start_port=$(echo $port | cut -d'-' -f1)
ufw status verbose end_port=$(echo $port | cut -d'-' -f2)
# Check if the port range has been successfully opened
(ufw status | grep -q "$start_port:$end_port") && echo "$start_port-$end_port"
else
# Check if the individual port has been successfully opened
(ufw status | grep -q "$port") && echo "$port"
fi
done
} }
delete_ports() { delete_ports() {
# Prompt the user to enter the ports they want to delete # Display current rules with numbers
read -p "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports echo "Current UFW rules:"
ufw status numbered
# Check if the input is valid # Ask the user how they want to delete rules
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then echo "Do you want to delete rules by:"
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2 echo "1) Rule numbers"
echo "2) Ports"
read -p "Enter your choice (1 or 2): " choice
if [[ $choice -eq 1 ]]; then
# Deleting by rule numbers
read -p "Enter the rule numbers you want to delete (1, 2, etc.): " rule_numbers
# Validate the input
if ! [[ $rule_numbers =~ ^([0-9]+)(,[0-9]+)*$ ]]; then
echo "Error: Invalid input. Please enter a comma-separated list of rule numbers." >&2
exit 1
fi
# Split numbers into an array
IFS=',' read -ra RULE_NUMBERS <<<"$rule_numbers"
for rule_number in "${RULE_NUMBERS[@]}"; do
# Delete the rule by number
ufw delete "$rule_number" || echo "Failed to delete rule number $rule_number"
done
echo "Selected rules have been deleted."
elif [[ $choice -eq 2 ]]; then
# Deleting by ports
read -p "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports
# Validate the input
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2
exit 1
fi
# Split ports into an array
IFS=',' read -ra PORT_LIST <<<"$ports"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
# Split the port range
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Delete the port range
ufw delete allow $start_port:$end_port/tcp
ufw delete allow $start_port:$end_port/udp
else
# Delete a single port
ufw delete allow "$port"
fi
done
# Confirmation of deletion
echo "Deleted the specified ports:"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Check if the port range has been deleted
(ufw status | grep -q "$start_port:$end_port") || echo "$start_port-$end_port"
else
# Check if the individual port has been deleted
(ufw status | grep -q "$port") || echo "$port"
fi
done
else
echo "${red}Error:${plain} Invalid choice. Please enter 1 or 2." >&2
exit 1 exit 1
fi fi
# Delete the specified ports using ufw
IFS=',' read -ra PORT_LIST <<<"$ports"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
# Split the range into start and end ports
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Delete the port range
ufw delete allow $start_port:$end_port/tcp
ufw delete allow $start_port:$end_port/udp
else
ufw delete allow "$port"
fi
done
# Confirm that the ports are deleted
echo "Deleted the specified ports:"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Check if the port range has been successfully deleted
(ufw status | grep -q "$start_port:$end_port") || echo "$start_port-$end_port"
else
# Check if the individual port has been successfully deleted
(ufw status | grep -q "$port") || echo "$port"
fi
done
} }
update_geo() { update_geo() {
echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)" echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)"
echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)" echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)"
echo -e "${green}\t3.${plain} vuong2023 (geoip_VN.dat, geosite_VN.dat)" echo -e "${green}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)"
echo -e "${green}\t0.${plain} Back to Main Menu" echo -e "${green}\t0.${plain} Back to Main Menu"
read -p "Choose an option: " choice read -p "Choose an option: " choice
@@ -842,10 +905,10 @@ update_geo() {
;; ;;
3) 3)
systemctl stop x-ui systemctl stop x-ui
rm -f geoip_VN.dat geosite_VN.dat rm -f geoip_RU.dat geosite_RU.dat
wget -O geoip_VN.dat -N https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geoip.dat wget -O geoip_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_VN.dat -N https://github.com/vuong2023/vn-v2ray-rules/releases/latest/download/geosite.dat wget -O geosite_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
echo -e "${green}vuong2023 datasets have been updated successfully!${plain}" echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
restart restart
;; ;;
*) *)
@@ -1310,7 +1373,7 @@ EOF
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES] [INCLUDES]
before = iptables-common.conf before = iptables-allports.conf
[Definition] [Definition]
actionstart = <iptables> -N f2b-<name> actionstart = <iptables> -N f2b-<name>
@@ -1353,13 +1416,18 @@ iplimit_remove_conflicts() {
done done
} }
ip_validation() {
ipv6_regex="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$"
ipv4_regex="^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)$"
}
iplimit_main() { iplimit_main() {
echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit" echo -e "\n${green}\t1.${plain} Install Fail2ban and configure IP Limit"
echo -e "${green}\t2.${plain} Change Ban Duration" echo -e "${green}\t2.${plain} Change Ban Duration"
echo -e "${green}\t3.${plain} Unban Everyone" echo -e "${green}\t3.${plain} Unban Everyone"
echo -e "${green}\t4.${plain} Ban Logs" echo -e "${green}\t4.${plain} Ban Logs"
echo -e "${green}\t5.${plain} Unban an IP Address" echo -e "${green}\t5.${plain} Ban an IP Address"
echo -e "${green}\t6.${plain} Ban an IP Address" echo -e "${green}\t6.${plain} Unban an IP Address"
echo -e "${green}\t7.${plain} Real-Time Logs" echo -e "${green}\t7.${plain} Real-Time Logs"
echo -e "${green}\t8.${plain} Service Status" echo -e "${green}\t8.${plain} Service Status"
echo -e "${green}\t9.${plain} Service Restart" echo -e "${green}\t9.${plain} Service Restart"
@@ -1391,7 +1459,7 @@ iplimit_main() {
3) 3)
confirm "Proceed with Unbanning everyone from IP Limit jail?" "y" confirm "Proceed with Unbanning everyone from IP Limit jail?" "y"
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
systemctl restart fail2ban fail2ban-client reload --restart --unban 3x-ipl
truncate -s 0 "${iplimit_banned_log_path}" truncate -s 0 "${iplimit_banned_log_path}"
echo -e "${green}All users Unbanned successfully.${plain}" echo -e "${green}All users Unbanned successfully.${plain}"
iplimit_main iplimit_main
@@ -1406,7 +1474,8 @@ iplimit_main() {
;; ;;
5) 5)
read -rp "Enter the IP address you want to ban: " ban_ip read -rp "Enter the IP address you want to ban: " ban_ip
if [[ $ban_ip =~ ^(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9]))$ || $ban_ip =~ ^(([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})$ ]]; then ip_validation
if [[ $ban_ip =~ $ipv4_regex || $ban_ip =~ $ipv6_regex ]]; then
fail2ban-client set 3x-ipl banip "$ban_ip" fail2ban-client set 3x-ipl banip "$ban_ip"
echo -e "${green}IP Address ${ban_ip} has been banned successfully.${plain}" echo -e "${green}IP Address ${ban_ip} has been banned successfully.${plain}"
else else
@@ -1416,7 +1485,8 @@ iplimit_main() {
;; ;;
6) 6)
read -rp "Enter the IP address you want to unban: " unban_ip read -rp "Enter the IP address you want to unban: " unban_ip
if [[ $unban_ip =~ ^(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9]))$ || $unban_ip =~ ^(([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})$ ]]; then ip_validation
if [[ $unban_ip =~ $ipv4_regex || $unban_ip =~ $ipv6_regex ]]; then
fail2ban-client set 3x-ipl unbanip "$unban_ip" fail2ban-client set 3x-ipl unbanip "$unban_ip"
echo -e "${green}IP Address ${unban_ip} has been unbanned successfully.${plain}" echo -e "${green}IP Address ${unban_ip} has been unbanned successfully.${plain}"
else else
@@ -1651,61 +1721,63 @@ SSH_port_forwarding() {
} }
show_usage() { show_usage() {
echo "x-ui control menu usages: " echo -e "┌───────────────────────────────────────────────────────┐
echo "------------------------------------------" ${blue}x-ui control menu usages (subcommands):${plain}
echo -e "SUBCOMMANDS:" │ │
echo -e "x-ui - Admin Management Script" ${blue}x-ui${plain} - Admin Management Script
echo -e "x-ui start - Start" ${blue}x-ui start${plain} - Start │
echo -e "x-ui stop - Stop" ${blue}x-ui stop${plain} - Stop │
echo -e "x-ui restart - Restart" ${blue}x-ui restart${plain} - Restart │
echo -e "x-ui status - Current Status" ${blue}x-ui status${plain} - Current Status
echo -e "x-ui settings - Current Settings" ${blue}x-ui settings${plain} - Current Settings
echo -e "x-ui enable - Enable Autostart on OS Startup" ${blue}x-ui enable${plain} - Enable Autostart on OS Startup
echo -e "x-ui disable - Disable Autostart on OS Startup" ${blue}x-ui disable${plain} - Disable Autostart on OS Startup
echo -e "x-ui log - Check logs" ${blue}x-ui log${plain} - Check logs │
echo -e "x-ui banlog - Check Fail2ban ban logs" ${blue}x-ui banlog${plain} - Check Fail2ban ban logs
echo -e "x-ui update - Update" ${blue}x-ui update${plain} - Update │
echo -e "x-ui custom - custom version" ${blue}x-ui legacy${plain} - legacy version
echo -e "x-ui install - Install" ${blue}x-ui install${plain} - Install │
echo -e "x-ui uninstall - Uninstall" ${blue}x-ui uninstall${plain} - Uninstall │
echo "------------------------------------------" └───────────────────────────────────────────────────────┘"
} }
show_menu() { show_menu() {
echo -e " echo -e "
${green}3X-UI Panel Management Script${plain} ╔────────────────────────────────────────────────╗
${green}0.${plain} Exit Script ${green}3X-UI Panel Management Script${plain}
———————————————— ${green}0.${plain} Exit Script │
${green}1.${plain} Install │────────────────────────────────────────────────│
${green}2.${plain} Update ${green}1.${plain} Install │
${green}3.${plain} Update Menu ${green}2.${plain} Update
${green}4.${plain} Legacy Version ${green}3.${plain} Update Menu │
${green}5.${plain} Uninstall ${green}4.${plain} Legacy Version │
———————————————— ${green}5.${plain} Uninstall │
${green}6.${plain} Reset Username & Password & Secret Token │────────────────────────────────────────────────│
${green}7.${plain} Reset Web Base Path ${green}6.${plain} Reset Username & Password & Secret Token │
${green}8.${plain} Reset Settings ${green}7.${plain} Reset Web Base Path │
${green}9.${plain} Change Port ${green}8.${plain} Reset Settings │
${green}10.${plain} View Current Settings ${green}9.${plain} Change Port │
———————————————— ${green}10.${plain} View Current Settings │
${green}11.${plain} Start │────────────────────────────────────────────────│
${green}12.${plain} Stop ${green}11.${plain} Start │
${green}13.${plain} Restart ${green}12.${plain} Stop │
${green}14.${plain} Check Status ${green}13.${plain} Restart │
${green}15.${plain} Logs Management ${green}14.${plain} Check Status │
———————————————— ${green}15.${plain} Logs Management │
${green}16.${plain} Enable Autostart │────────────────────────────────────────────────│
${green}17.${plain} Disable Autostart ${green}16.${plain} Enable Autostart
———————————————— ${green}17.${plain} Disable Autostart │
${green}18.${plain} SSL Certificate Management │────────────────────────────────────────────────│
${green}19.${plain} Cloudflare SSL Certificate ${green}18.${plain} SSL Certificate Management │
${green}20.${plain} IP Limit Management ${green}19.${plain} Cloudflare SSL Certificate │
${green}21.${plain} Firewall Management ${green}20.${plain} IP Limit Management
${green}22.${plain} SSH Port Forwarding Management ${green}21.${plain} Firewall Management │
———————————————— ${green}22.${plain} SSH Port Forwarding Management │
${green}23.${plain} Enable BBR │────────────────────────────────────────────────│
${green}24.${plain} Update Geo Files ${green}23.${plain} Enable BBR │
${green}25.${plain} Speedtest by Ookla ${green}24.${plain} Update Geo Files │
${green}25.${plain} Speedtest by Ookla │
╚────────────────────────────────────────────────╝
" "
show_status show_status
echo && read -p "Please enter your selection [0-25]: " num echo && read -p "Please enter your selection [0-25]: " num

View File

@@ -2,9 +2,9 @@ package xray
type ClientTraffic struct { type ClientTraffic struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
InboundId int `json:"inboundId" form:"inboundId"` InboundId int `json:"inboundId" form:"inboundId" gorm:"index;not null"`
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
Email string `json:"email" form:"email" gorm:"unique"` Email string `json:"email" form:"email" gorm:"uniqueIndex"`
Up int64 `json:"up" form:"up"` Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`