Compare commits

...

11 commits
v2.8.5 ... main

Author SHA1 Message Date
mhsanaei
784ed39930
update dependencies
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
2025-11-09 00:56:14 +01:00
fgsfds
538f7fd5d7
Fix: Incorrect time in xray logs (#3587)
* fixed timezone in xray logs

* remove leading / at the address
2025-11-09 00:42:02 +01:00
fgsfds
cf38226b5d
Add update-all-geofiles key to x-ui.sh (#3586)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
* added update-all-geofiles key to x-ui.sh that updated all geofiles

* fix

* text fixes

* typo fix

* cleanup
2025-11-07 19:26:43 +01:00
lillinlin
575ee854c8
Better Random Reality (#3585)
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
* Update reality_targets.js

* Update inbound.js
2025-11-02 14:46:50 +01:00
OleksandrParshyn
9936af80dd
Fix: Invoke service.StopBot() in signal handlers (#3583)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
Ensures the global Telegram bot stop function (`service.StopBot()`) is called upon receiving system signals (SIGHUP for restart, SIGINT/SIGTERM for shutdown). This complements the changes in `tgbot.go` to guarantee a clean shutdown of the Telegram bot's Long Polling operation, fully resolving the 409 Conflict issue during panel restarts or shutdowns.

Changes:
- Added `service.StopBot()` call to the `syscall.SIGHUP` handler.
- Added `service.StopBot()` call to the default shutdown handler.
2025-11-01 14:33:35 +01:00
Дмитрий Олегович Саенко
4a75bd0a48
Feature: add setting certs for subscription while generating for panel (#3578)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
2025-11-01 13:10:27 +01:00
Rashid Yusubov
b0c223c631
fix: improve russian localization (#3576)
* fix: improve russian localization

* fix: updating the Russian translation according to the suggestions
2025-11-01 13:07:49 +01:00
Denis Gorelov
313b51f96f
feat: Add random Reality Target/SNI selection from 52 popular services (#3577)
* feat: Add random Reality Target/SNI selection from 52 popular services

- Created reality_targets.js with list of 52 popular services
- Updated RealityStreamSettings to use random targets by default
- Added UI randomize buttons with sync icon in Reality settings form
- Implemented randomizeRealityTarget() method in inbound modal
- Replaces hardcoded google.com with diverse global services

* fix

---------

Co-authored-by: mhsanaei <ho3ein.sanaei@gmail.com>
2025-11-01 13:07:05 +01:00
OleksandrParshyn
020cd63e22
Fix: Graceful Telegram bot shutdown to prevent 409 Conflict (#3580)
* Fix: Graceful Telegram bot shutdown to prevent 409 Conflict

Introduces a `botCancel` context and a global `StopBot()` function to ensure the Telegram bot's Long Polling operation is safely terminated (via context cancellation) before the service restarts. This prevents the "Conflict: another update consumer is running" (409) error upon panel restart.

Changes:
- Added `botCancel context.CancelFunc` to manage context cancellation.
- Implemented global `StopBot()` function.
- Updated `Tgbot.Stop()` to call `StopBot()`.
- Modified `Tgbot.OnReceive()` to use the new cancellable context for `UpdatesViaLongPolling`.

* Fix: Prevent race condition and goroutine leak in TgBot

Addresses a critical race condition on the global `botCancel` variable, which could occur if `Tgbot.OnReceive()` was called concurrently (e.g., during rapid panel restarts or unexpected behavior).

Changes in tgbot.go:
- Added `tgBotMutex sync.Mutex` to ensure thread safety.
- Protected `botCancel` creation and assignment in `OnReceive()` using the mutex, and added a check to prevent overwriting an active context, which avoids goroutine leaks.
- Protected the cancellation and cleanup logic in `StopBot()` with the mutex.

* Refactor: Replace time.Sleep with sync.WaitGroup for reliable TgBot shutdown

Replaced the unreliable `time.Sleep(1 * time.Second)` in `service.StopBot()` with `sync.WaitGroup`. This ensures the Long Polling goroutine is explicitly waited for and reliably exits before the panel continues, preventing potential resource leaks and incomplete shutdowns during restarts.

Changes:
- Added `botWG sync.WaitGroup` variable.
- Updated `service.StopBot()` to call `botWG.Wait()` instead of `time.Sleep()`.
- Modified `Tgbot.OnReceive()` to correctly use `botWG.Add(1)` and `defer botWG.Done()` within the Long Polling goroutine.
- Corrected the goroutine structure in `OnReceive()` to properly encapsulate all message handling logic.
2025-11-01 13:01:44 +01:00
BOplaid
6e46e9b16e
Improve English README (#3579) 2025-11-01 12:48:16 +01:00
mhsanaei
713a7328f6
gofmt
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
2025-10-21 13:02:55 +02:00
21 changed files with 738 additions and 581 deletions

View file

@ -18,7 +18,7 @@
**3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols.
> [!IMPORTANT]
> This project is only for personal using, please do not use it for illegal purposes, please do not use it in a production environment.
> This project is only for personal usage, please do not use it for illegal purposes, and please do not use it in a production environment.
As an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features.

40
go.mod
View file

@ -1,45 +1,45 @@
module github.com/mhsanaei/3x-ui/v2
go 1.25.2
go 1.25.4
require (
github.com/gin-contrib/gzip v1.2.4
github.com/gin-contrib/gzip v1.2.5
github.com/gin-contrib/sessions v1.0.4
github.com/gin-gonic/gin v1.11.0
github.com/go-ldap/ldap/v3 v3.4.12
github.com/goccy/go-json v0.10.5
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/mymmrac/telego v1.3.0
github.com/mymmrac/telego v1.3.1
github.com/nicksnyder/go-i18n/v2 v2.6.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.9
github.com/shirou/gopsutil/v4 v4.25.10
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.67.0
github.com/valyala/fasthttp v1.68.0
github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.250911.1-0.20251015080723-b69a376aa1b6
github.com/xtls/xray-core v1.251015.0
go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.43.0
golang.org/x/sys v0.37.0
golang.org/x/sys v0.38.0
golang.org/x/text v0.30.0
google.golang.org/grpc v1.76.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.0
gorm.io/gorm v1.31.1
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/Azure/go-ntlmssp v0.0.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/ebitengine/purego v0.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
@ -57,7 +57,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@ -70,17 +70,17 @@ require (
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/quic-go/quic-go v0.56.0 // indirect
github.com/refraction-networking/utls v1.8.1 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.7.12 // indirect
github.com/sagernet/sing v0.7.13 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
@ -89,15 +89,15 @@ require (
github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
lukechampine.com/blake3 v1.4.1 // indirect

80
go.sum
View file

@ -1,5 +1,5 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Azure/go-ntlmssp v0.0.1 h1:NqbqUHiVYjwBDsxM1KrllG7rnoHpcp40EWrpffsgcUc=
github.com/Azure/go-ntlmssp v0.0.1/go.mod h1:P/Wrai1IsNvkfWRRN0jvRobt7ZJdz4sHQ3dOjiEGDt0=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
@ -8,10 +8,10 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
@ -23,14 +23,14 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v1.2.4 h1:yNz4EhPC2kHSZJD1oc1zwp7MLEhEZ3goQeGM3a1b6jU=
github.com/gin-contrib/gzip v1.2.4/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
@ -107,8 +107,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -130,8 +130,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v1.3.0 h1:y2bDDCioLgkcs+5luUaPgTNHKel1Qh30iUxFcMUrowg=
github.com/mymmrac/telego v1.3.0/go.mod h1:0D2l/IA/gUFn4oqsi1O4/tSnlezw5jNV/ReFRDUEKk8=
github.com/mymmrac/telego v1.3.1 h1:dI5D8LKWBw241W02LmJqoSLZXW3tuLokxVoNbIZUYQg=
github.com/mymmrac/telego v1.3.1/go.mod h1:3D0h4jJ3OzubY/gI4xDIGx4jkY26fmcPyqx0Lq24+zI=
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@ -148,8 +148,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY=
github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c=
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@ -158,24 +158,26 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.7.12 h1:MpMbO56crPRZTbltoj1wGk4Xj9+GiwH1wTO4s3fz1EA=
github.com/sagernet/sing v0.7.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
github.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU=
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.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.6.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.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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
@ -184,14 +186,14 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac=
github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@ -202,8 +204,8 @@ github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535 h1:nwobseOLLRtdbP6z7Z2aVI97u8ZptTgD1ofovhAKmeU=
github.com/xtls/reality v0.0.0-20251014195629-e4eec4520535/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ=
github.com/xtls/xray-core v1.250911.1-0.20251015080723-b69a376aa1b6 h1:gwgJxWb9OABUJAYxiS33nQzk3MRVjidzBnHBrzKnxOw=
github.com/xtls/xray-core v1.250911.1-0.20251015080723-b69a376aa1b6/go.mod h1:72ZU/srfutsNPmw9y8SCGRy0iccvshIRk8BNGR8D2Ik=
github.com/xtls/xray-core v1.251015.0 h1:P7b3vt8ShhH31k4h6VJ/Pxar3tY9eK+7S8eygd6rsP0=
github.com/xtls/xray-core v1.251015.0/go.mod h1:72ZU/srfutsNPmw9y8SCGRy0iccvshIRk8BNGR8D2Ik=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@ -226,24 +228,24 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
@ -256,8 +258,8 @@ golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+Z
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
@ -273,8 +275,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=

View file

@ -248,7 +248,7 @@ install_x-ui() {
${blue}x-ui log${plain} - Check logs │
${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
${blue}x-ui update${plain} - Update │
${blue}x-ui legacy${plain} - legacy version │
${blue}x-ui legacy${plain} - Legacy version │
${blue}x-ui install${plain} - Install │
${blue}x-ui uninstall${plain} - Uninstall │
└───────────────────────────────────────────────────────┘"

22
main.go
View file

@ -78,6 +78,10 @@ func runWebServer() {
case syscall.SIGHUP:
logger.Info("Received SIGHUP signal. Restarting servers...")
// --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart ---
service.StopBot()
// --
err := server.Stop()
if err != nil {
logger.Debug("Error stopping web server:", err)
@ -106,6 +110,10 @@ func runWebServer() {
log.Println("Sub server restarted successfully.")
default:
// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---
service.StopBot()
// ------------------------------------------------------------
server.Stop()
subServer.Stop()
log.Println("Shutting down servers.")
@ -321,6 +329,20 @@ func updateCert(publicKey string, privateKey string) {
} else {
fmt.Println("set certificate private key success")
}
err = settingService.SetSubCertFile(publicKey)
if err != nil {
fmt.Println("set certificate for subscription public key failed:", err)
} else {
fmt.Println("set certificate for subscription public key success")
}
err = settingService.SetSubKeyFile(privateKey)
if err != nil {
fmt.Println("set certificate for subscription private key failed:", err)
} else {
fmt.Println("set certificate for subscription private key success")
}
} else {
fmt.Println("both public and private key should be entered.")
}

View file

@ -247,7 +247,7 @@ update_x-ui() {
${blue}x-ui log${plain} - Check logs │
${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
${blue}x-ui update${plain} - Update │
${blue}x-ui legacy${plain} - legacy version │
${blue}x-ui legacy${plain} - Legacy version │
${blue}x-ui install${plain} - Install │
${blue}x-ui uninstall${plain} - Uninstall │
└───────────────────────────────────────────────────────┘"

View file

@ -1,144 +1,142 @@
package ldaputil
import (
"crypto/tls"
"fmt"
"crypto/tls"
"fmt"
"github.com/go-ldap/ldap/v3"
"github.com/go-ldap/ldap/v3"
)
type Config struct {
Host string
Port int
UseTLS bool
BindDN string
Password string
BaseDN string
UserFilter string
UserAttr string
FlagField string
TruthyVals []string
Invert bool
Host string
Port int
UseTLS bool
BindDN string
Password string
BaseDN string
UserFilter string
UserAttr string
FlagField string
TruthyVals []string
Invert bool
}
// FetchVlessFlags returns map[email]enabled
func FetchVlessFlags(cfg Config) (map[string]bool, error) {
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn
var err error
if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else {
conn, err = ldap.Dial("tcp", addr)
}
if err != nil {
return nil, err
}
defer conn.Close()
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn
var err error
if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else {
conn, err = ldap.Dial("tcp", addr)
}
if err != nil {
return nil, err
}
defer conn.Close()
if cfg.BindDN != "" {
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
return nil, err
}
}
if cfg.BindDN != "" {
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
return nil, err
}
}
if cfg.UserFilter == "" {
cfg.UserFilter = "(objectClass=person)"
}
if cfg.UserAttr == "" {
cfg.UserAttr = "mail"
}
// if field not set we fallback to legacy vless_enabled
if cfg.FlagField == "" {
cfg.FlagField = "vless_enabled"
}
if cfg.UserFilter == "" {
cfg.UserFilter = "(objectClass=person)"
}
if cfg.UserAttr == "" {
cfg.UserAttr = "mail"
}
// if field not set we fallback to legacy vless_enabled
if cfg.FlagField == "" {
cfg.FlagField = "vless_enabled"
}
req := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
cfg.UserFilter,
[]string{cfg.UserAttr, cfg.FlagField},
nil,
)
req := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
cfg.UserFilter,
[]string{cfg.UserAttr, cfg.FlagField},
nil,
)
res, err := conn.Search(req)
if err != nil {
return nil, err
}
res, err := conn.Search(req)
if err != nil {
return nil, err
}
result := make(map[string]bool, len(res.Entries))
for _, e := range res.Entries {
user := e.GetAttributeValue(cfg.UserAttr)
if user == "" {
continue
}
val := e.GetAttributeValue(cfg.FlagField)
enabled := false
for _, t := range cfg.TruthyVals {
if val == t {
enabled = true
break
}
}
if cfg.Invert {
enabled = !enabled
}
result[user] = enabled
}
return result, nil
result := make(map[string]bool, len(res.Entries))
for _, e := range res.Entries {
user := e.GetAttributeValue(cfg.UserAttr)
if user == "" {
continue
}
val := e.GetAttributeValue(cfg.FlagField)
enabled := false
for _, t := range cfg.TruthyVals {
if val == t {
enabled = true
break
}
}
if cfg.Invert {
enabled = !enabled
}
result[user] = enabled
}
return result, nil
}
// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
func AuthenticateUser(cfg Config, username, password string) (bool, error) {
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn
var err error
if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else {
conn, err = ldap.Dial("tcp", addr)
}
if err != nil {
return false, err
}
defer conn.Close()
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn
var err error
if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else {
conn, err = ldap.Dial("tcp", addr)
}
if err != nil {
return false, err
}
defer conn.Close()
// Optional initial bind for search
if cfg.BindDN != "" {
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
return false, err
}
}
// Optional initial bind for search
if cfg.BindDN != "" {
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
return false, err
}
}
if cfg.UserFilter == "" {
cfg.UserFilter = "(objectClass=person)"
}
if cfg.UserAttr == "" {
cfg.UserAttr = "uid"
}
if cfg.UserFilter == "" {
cfg.UserFilter = "(objectClass=person)"
}
if cfg.UserAttr == "" {
cfg.UserAttr = "uid"
}
// Build filter to find specific user
filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username))
req := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
filter,
[]string{"dn"},
nil,
)
res, err := conn.Search(req)
if err != nil {
return false, err
}
if len(res.Entries) == 0 {
return false, nil
}
userDN := res.Entries[0].DN
// Try to bind as the user
if err := conn.Bind(userDN, password); err != nil {
return false, nil
}
return true, nil
// Build filter to find specific user
filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username))
req := ldap.NewSearchRequest(
cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
filter,
[]string{"dn"},
nil,
)
res, err := conn.Search(req)
if err != nil {
return false, err
}
if len(res.Entries) == 0 {
return false, nil
}
userDN := res.Entries[0].DN
// Try to bind as the user
if err := conn.Bind(userDN, password); err != nil {
return false, nil
}
return true, nil
}

View file

@ -729,8 +729,8 @@ class RealityStreamSettings extends XrayCommonClass {
constructor(
show = false,
xver = 0,
target = 'google.com:443',
serverNames = 'google.com,www.google.com',
target = '',
serverNames = '',
privateKey = '',
minClientVer = '',
maxClientVer = '',
@ -740,6 +740,14 @@ class RealityStreamSettings extends XrayCommonClass {
settings = new RealityStreamSettings.Settings()
) {
super();
// If target/serverNames are not provided, use random values
if (!target && !serverNames) {
const randomTarget = typeof getRandomRealityTarget !== 'undefined'
? getRandomRealityTarget()
: { target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' };
target = randomTarget.target;
serverNames = randomTarget.sni;
}
this.show = show;
this.xver = xver;
this.target = target;

View file

@ -0,0 +1,31 @@
// List of popular services for VLESS Reality Target/SNI randomization
const REALITY_TARGETS = [
{ target: 'www.icloud.com:443', sni: 'www.icloud.com,icloud.com' },
{ target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' },
{ target: 'www.tesla.com:443', sni: 'www.tesla.com,tesla.com' },
{ target: 'www.sony.com:443', sni: 'www.sony.com,sony.com' },
{ target: 'www.nvidia.com:443', sni: 'www.nvidia.com,nvidia.com' },
{ target: 'www.amd.com:443', sni: 'www.amd.com,amd.com' },
{ target: 'azure.microsoft.com:443', sni: 'azure.microsoft.com,www.azure.com' },
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com,amazon.com' },
{ target: 'www.bing.com:443', sni: 'www.bing.com,bing.com' },
{ target: 'www.oracle.com:443', sni: 'www.oracle.com,oracle.com' },
{ target: 'www.intel.com:443', sni: 'www.intel.com,intel.com' },
{ target: 'www.microsoft.com:443', sni: 'www.microsoft.com,microsoft.com' },
{ target: 'www.amazon.com:443', sni: 'www.amazon.com,amazon.com' }
];
/**
* Returns a random Reality target configuration from the predefined list
* @returns {Object} Object with target and sni properties
*/
function getRandomRealityTarget() {
const randomIndex = Math.floor(Math.random() * REALITY_TARGETS.length);
const selected = REALITY_TARGETS[randomIndex];
// Return a copy to avoid reference issues
return {
target: selected.target,
sni: selected.sni
};
}

View file

@ -77,27 +77,27 @@ type AllSetting struct {
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
// LDAP settings
LdapEnable bool `json:"ldapEnable" form:"ldapEnable"`
LdapHost string `json:"ldapHost" form:"ldapHost"`
LdapPort int `json:"ldapPort" form:"ldapPort"`
LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"`
LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"`
LdapPassword string `json:"ldapPassword" form:"ldapPassword"`
LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"`
LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"`
LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"` // e.g., mail or uid
LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"`
LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"`
LdapEnable bool `json:"ldapEnable" form:"ldapEnable"`
LdapHost string `json:"ldapHost" form:"ldapHost"`
LdapPort int `json:"ldapPort" form:"ldapPort"`
LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"`
LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"`
LdapPassword string `json:"ldapPassword" form:"ldapPassword"`
LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"`
LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"`
LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"` // e.g., mail or uid
LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"`
LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"`
// Generic flag configuration
LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"`
LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"`
LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"`
LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"`
LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"`
LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"`
LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"`
LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"`
LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"`
LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"`
LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"`
LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"`
LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"`
LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"`
LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"`
LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"`
LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"`
LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"`
// JSON subscription routing rules
}

View file

@ -12,10 +12,26 @@
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Target'>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template> Target <a-icon @click="randomizeRealityTarget()"
type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.reality.target"></a-input>
</a-form-item>
<a-form-item label='SNI'>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template> SNI <a-icon @click="randomizeRealityTarget()"
type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
</a-form-item>
<a-form-item label='Max Time Diff (ms)'>

View file

@ -602,6 +602,7 @@
{{template "page/body_scripts" .}}
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
{{template "component/aSidebar" .}}

View file

@ -844,9 +844,11 @@
text = `<td>${log.Email}</td>`;
}
const { locale, timeZone } = Intl.DateTimeFormat().resolvedOptions();
formattedLogs += `
<tr ${outboundColor}>
<td><b>${new Date(log.DateTime).toLocaleString()}</b></td>
<td><b>${new Date(log.DateTime).toLocaleString(locale, { timeZone })}</b></td>
<td>${log.FromAddress}</td>
<td>${log.ToAddress}</td>
<td>${log.Inbound}</td>

View file

@ -158,6 +158,13 @@
this.inbound.stream.reality.mldsa65Seed = '';
this.inbound.stream.reality.settings.mldsa65Verify = '';
},
randomizeRealityTarget() {
if (typeof getRandomRealityTarget !== 'undefined') {
const randomTarget = getRandomRealityTarget();
this.inbound.stream.reality.target = randomTarget.target;
this.inbound.stream.reality.serverNames = randomTarget.sni;
}
},
async getNewEchCert() {
inModal.loading(true);
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });

View file

@ -1569,21 +1569,20 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
return !clientOldEnabled, needRestart, nil
}
// SetClientEnableByEmail sets client enable state to desired value; returns (changed, needRestart, error)
func (s *InboundService) SetClientEnableByEmail(clientEmail string, enable bool) (bool, bool, error) {
current, err := s.checkIsEnabledByEmail(clientEmail)
if err != nil {
return false, false, err
}
if current == enable {
return false, false, nil
}
newEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail)
if err != nil {
return false, needRestart, err
}
return newEnabled == enable, needRestart, nil
current, err := s.checkIsEnabledByEmail(clientEmail)
if err != nil {
return false, false, err
}
if current == enable {
return false, false, nil
}
newEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail)
if err != nil {
return false, needRestart, err
}
return newEnabled == enable, needRestart, nil
}
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) {

View file

@ -794,17 +794,17 @@ func (s *ServerService) GetXrayLogs(
for i, part := range parts {
if i == 0 {
dateTime, err := time.Parse("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1])
dateTime, err := time.ParseInLocation("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1], time.Local)
if err != nil {
continue
}
entry.DateTime = dateTime
entry.DateTime = dateTime.UTC()
}
if part == "from" {
entry.FromAddress = parts[i+1]
entry.FromAddress = strings.TrimLeft(parts[i+1], "/")
} else if part == "accepted" {
entry.ToAddress = parts[i+1]
entry.ToAddress = strings.TrimLeft(parts[i+1], "/")
} else if strings.HasPrefix(part, "[") {
entry.Inbound = part[1:]
} else if strings.HasSuffix(part, "]") {

View file

@ -74,26 +74,26 @@ var defaultValueMap = map[string]string{
"externalTrafficInformEnable": "false",
"externalTrafficInformURI": "",
// LDAP defaults
"ldapEnable": "false",
"ldapHost": "",
"ldapPort": "389",
"ldapUseTLS": "false",
"ldapBindDN": "",
"ldapPassword": "",
"ldapBaseDN": "",
"ldapUserFilter": "(objectClass=person)",
"ldapUserAttr": "mail",
"ldapVlessField": "vless_enabled",
"ldapSyncCron": "@every 1m",
"ldapFlagField": "",
"ldapTruthyValues": "true,1,yes,on",
"ldapInvertFlag": "false",
"ldapInboundTags": "",
"ldapAutoCreate": "false",
"ldapAutoDelete": "false",
"ldapDefaultTotalGB": "0",
"ldapDefaultExpiryDays": "0",
"ldapDefaultLimitIP": "0",
"ldapEnable": "false",
"ldapHost": "",
"ldapPort": "389",
"ldapUseTLS": "false",
"ldapBindDN": "",
"ldapPassword": "",
"ldapBaseDN": "",
"ldapUserFilter": "(objectClass=person)",
"ldapUserAttr": "mail",
"ldapVlessField": "vless_enabled",
"ldapSyncCron": "@every 1m",
"ldapFlagField": "",
"ldapTruthyValues": "true,1,yes,on",
"ldapInvertFlag": "false",
"ldapInboundTags": "",
"ldapAutoCreate": "false",
"ldapAutoDelete": "false",
"ldapDefaultTotalGB": "0",
"ldapDefaultExpiryDays": "0",
"ldapDefaultLimitIP": "0",
}
// SettingService provides business logic for application settings management.
@ -479,10 +479,18 @@ func (s *SettingService) GetSubDomain() (string, error) {
return s.getString("subDomain")
}
func (s *SettingService) SetSubCertFile(subCertFile string) error {
return s.setString("subCertFile", subCertFile)
}
func (s *SettingService) GetSubCertFile() (string, error) {
return s.getString("subCertFile")
}
func (s *SettingService) SetSubKeyFile(subKeyFile string) error {
return s.setString("subKeyFile", subKeyFile)
}
func (s *SettingService) GetSubKeyFile() (string, error) {
return s.getString("subKeyFile")
}
@ -565,83 +573,83 @@ func (s *SettingService) GetIpLimitEnable() (bool, error) {
// LDAP exported getters
func (s *SettingService) GetLdapEnable() (bool, error) {
return s.getBool("ldapEnable")
return s.getBool("ldapEnable")
}
func (s *SettingService) GetLdapHost() (string, error) {
return s.getString("ldapHost")
return s.getString("ldapHost")
}
func (s *SettingService) GetLdapPort() (int, error) {
return s.getInt("ldapPort")
return s.getInt("ldapPort")
}
func (s *SettingService) GetLdapUseTLS() (bool, error) {
return s.getBool("ldapUseTLS")
return s.getBool("ldapUseTLS")
}
func (s *SettingService) GetLdapBindDN() (string, error) {
return s.getString("ldapBindDN")
return s.getString("ldapBindDN")
}
func (s *SettingService) GetLdapPassword() (string, error) {
return s.getString("ldapPassword")
return s.getString("ldapPassword")
}
func (s *SettingService) GetLdapBaseDN() (string, error) {
return s.getString("ldapBaseDN")
return s.getString("ldapBaseDN")
}
func (s *SettingService) GetLdapUserFilter() (string, error) {
return s.getString("ldapUserFilter")
return s.getString("ldapUserFilter")
}
func (s *SettingService) GetLdapUserAttr() (string, error) {
return s.getString("ldapUserAttr")
return s.getString("ldapUserAttr")
}
func (s *SettingService) GetLdapVlessField() (string, error) {
return s.getString("ldapVlessField")
return s.getString("ldapVlessField")
}
func (s *SettingService) GetLdapSyncCron() (string, error) {
return s.getString("ldapSyncCron")
return s.getString("ldapSyncCron")
}
func (s *SettingService) GetLdapFlagField() (string, error) {
return s.getString("ldapFlagField")
return s.getString("ldapFlagField")
}
func (s *SettingService) GetLdapTruthyValues() (string, error) {
return s.getString("ldapTruthyValues")
return s.getString("ldapTruthyValues")
}
func (s *SettingService) GetLdapInvertFlag() (bool, error) {
return s.getBool("ldapInvertFlag")
return s.getBool("ldapInvertFlag")
}
func (s *SettingService) GetLdapInboundTags() (string, error) {
return s.getString("ldapInboundTags")
return s.getString("ldapInboundTags")
}
func (s *SettingService) GetLdapAutoCreate() (bool, error) {
return s.getBool("ldapAutoCreate")
return s.getBool("ldapAutoCreate")
}
func (s *SettingService) GetLdapAutoDelete() (bool, error) {
return s.getBool("ldapAutoDelete")
return s.getBool("ldapAutoDelete")
}
func (s *SettingService) GetLdapDefaultTotalGB() (int, error) {
return s.getInt("ldapDefaultTotalGB")
return s.getInt("ldapDefaultTotalGB")
}
func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) {
return s.getInt("ldapDefaultExpiryDays")
return s.getInt("ldapDefaultExpiryDays")
}
func (s *SettingService) GetLdapDefaultLimitIP() (int, error) {
return s.getInt("ldapDefaultLimitIP")
return s.getInt("ldapDefaultLimitIP")
}
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {

View file

@ -38,7 +38,15 @@ import (
)
var (
bot *telego.Bot
bot *telego.Bot
// botCancel stores the function to cancel the context, stopping Long Polling gracefully.
botCancel context.CancelFunc
// tgBotMutex protects concurrent access to botCancel variable
tgBotMutex sync.Mutex
// botWG waits for the OnReceive Long Polling goroutine to finish.
botWG sync.WaitGroup
botHandler *th.BotHandler
adminIds []int64
isRunning bool
@ -306,8 +314,13 @@ func (t *Tgbot) SetHostname() {
hostname = host
}
// Stop stops the Telegram bot and cleans up resources.
// Stop safely stops the Telegram bot's Long Polling operation.
// This method now calls the global StopBot function and cleans up other resources.
func (t *Tgbot) Stop() {
// Call the global StopBot function to gracefully shut down Long Polling
StopBot()
// Stop the bot handler (in case the goroutine hasn't exited yet)
if botHandler != nil {
botHandler.Stop()
}
@ -316,6 +329,27 @@ func (t *Tgbot) Stop() {
adminIds = nil
}
// StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context.
// This is the global function called from main.go's signal handler and t.Stop().
func StopBot() {
tgBotMutex.Lock()
defer tgBotMutex.Unlock()
if botCancel != nil {
logger.Info("Sending cancellation signal to Telegram bot...")
// Calling botCancel() cancels the context passed to UpdatesViaLongPolling,
// which stops the Long Polling operation and closes the updates channel,
// allowing the th.Start() goroutine to exit cleanly.
botCancel()
botCancel = nil
// Giving the goroutine a small delay to exit cleanly.
botWG.Wait()
logger.Info("Telegram bot successfully stopped.")
}
}
// encodeQuery encodes the query string if it's longer than 64 characters.
func (t *Tgbot) encodeQuery(query string) string {
// NOTE: we only need to hash for more than 64 chars
@ -345,188 +379,207 @@ func (t *Tgbot) OnReceive() {
params := telego.GetUpdatesParams{
Timeout: 30, // Increased timeout to reduce API calls
}
// --- GRACEFUL SHUTDOWN FIX: Context creation ---
tgBotMutex.Lock()
updates, _ := bot.UpdatesViaLongPolling(context.Background(), &params)
// Create a context with cancellation and store the cancel function.
var ctx context.Context
botHandler, _ = th.NewBotHandler(bot, updates)
// Check if botCancel is already set (to prevent race condition overwrite and goroutine leak)
if botCancel == nil {
ctx, botCancel = context.WithCancel(context.Background())
} else {
// If botCancel is already set, use a non-cancellable context for this redundant call.
// This prevents overwriting the active botCancel and causing a goroutine leak from the previous call.
logger.Warning("TgBot OnReceive called concurrently. Using background context for redundant call.")
ctx = context.Background() // <<< ИЗМЕНЕНИЕ
}
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
delete(userStates, message.Chat.ID)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
return nil
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
tgBotMutex.Unlock()
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
// Use goroutine with worker pool for concurrent command processing
go func() {
messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker
// Get updates channel using the context.
updates, _ := bot.UpdatesViaLongPolling(ctx, &params)
botWG.Go(func() {
botHandler, _ = th.NewBotHandler(bot, updates)
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
delete(userStates, message.Chat.ID)
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
}()
return nil
}, th.AnyCommand())
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
return nil
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
// Use goroutine with worker pool for concurrent callback processing
go func() {
messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
// Use goroutine with worker pool for concurrent command processing
go func() {
messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker
delete(userStates, query.Message.GetChat().ID)
t.answerCallback(&query, checkAdmin(query.From.ID))
}()
return nil
}, th.AnyCallbackQueryWithMessage())
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
if userState, exists := userStates[message.Chat.ID]; exists {
switch userState {
case "awaiting_id":
if client_Id == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
return nil
}
client_Id = strings.TrimSpace(message.Text)
if t.isSingleWord(client_Id) {
userStates[message.Chat.ID] = "awaiting_id"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_password_tr":
if client_TrPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_TrPassword = strings.TrimSpace(message.Text)
if t.isSingleWord(client_TrPassword) {
userStates[message.Chat.ID] = "awaiting_password_tr"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_password_sh":
if client_ShPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_ShPassword = strings.TrimSpace(message.Text)
if t.isSingleWord(client_ShPassword) {
userStates[message.Chat.ID] = "awaiting_password_sh"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_email":
if client_Email == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_Email = strings.TrimSpace(message.Text)
if t.isSingleWord(client_Email) {
userStates[message.Chat.ID] = "awaiting_email"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_comment":
if client_Comment == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_Comment = strings.TrimSpace(message.Text)
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
}()
return nil
}, th.AnyCommand())
} else {
if message.UsersShared != nil {
if checkAdmin(message.From.ID) {
for _, sharedUser := range message.UsersShared.Users {
userID := sharedUser.UserID
needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID)
if needRestart {
t.xrayService.SetToNeedRestart()
}
output := ""
if err != nil {
output += t.I18nBot("tgbot.messages.selectUserFailed")
} else {
output += t.I18nBot("tgbot.messages.userSaved")
}
t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove())
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
// Use goroutine with worker pool for concurrent callback processing
go func() {
messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker
delete(userStates, query.Message.GetChat().ID)
t.answerCallback(&query, checkAdmin(query.From.ID))
}()
return nil
}, th.AnyCallbackQueryWithMessage())
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
if userState, exists := userStates[message.Chat.ID]; exists {
switch userState {
case "awaiting_id":
if client_Id == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
return nil
}
client_Id = strings.TrimSpace(message.Text)
if t.isSingleWord(client_Id) {
userStates[message.Chat.ID] = "awaiting_id"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_password_tr":
if client_TrPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_TrPassword = strings.TrimSpace(message.Text)
if t.isSingleWord(client_TrPassword) {
userStates[message.Chat.ID] = "awaiting_password_tr"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_password_sh":
if client_ShPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_ShPassword = strings.TrimSpace(message.Text)
if t.isSingleWord(client_ShPassword) {
userStates[message.Chat.ID] = "awaiting_password_sh"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_email":
if client_Email == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_Email = strings.TrimSpace(message.Text)
if t.isSingleWord(client_Email) {
userStates[message.Chat.ID] = "awaiting_email"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_comment":
if client_Comment == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_Comment = strings.TrimSpace(message.Text)
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
} else {
if message.UsersShared != nil {
if checkAdmin(message.From.ID) {
for _, sharedUser := range message.UsersShared.Users {
userID := sharedUser.UserID
needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID)
if needRestart {
t.xrayService.SetToNeedRestart()
}
output := ""
if err != nil {
output += t.I18nBot("tgbot.messages.selectUserFailed")
} else {
output += t.I18nBot("tgbot.messages.userSaved")
}
t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove())
}
} else {
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove())
}
} else {
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove())
}
}
}
return nil
}, th.AnyMessage())
return nil
}, th.AnyMessage())
botHandler.Start()
botHandler.Start()
})
}
// answerCommand processes incoming command messages from Telegram users.

View file

@ -7,7 +7,7 @@ import (
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/util/crypto"
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
"github.com/xlzd/gotp"
"gorm.io/gorm"
)
@ -49,38 +49,38 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
return nil
}
// If LDAP enabled and local password check fails, attempt LDAP auth
if !crypto.CheckPasswordHash(user.Password, password) {
ldapEnabled, _ := s.settingService.GetLdapEnable()
if !ldapEnabled {
return nil
}
// If LDAP enabled and local password check fails, attempt LDAP auth
if !crypto.CheckPasswordHash(user.Password, password) {
ldapEnabled, _ := s.settingService.GetLdapEnable()
if !ldapEnabled {
return nil
}
host, _ := s.settingService.GetLdapHost()
port, _ := s.settingService.GetLdapPort()
useTLS, _ := s.settingService.GetLdapUseTLS()
bindDN, _ := s.settingService.GetLdapBindDN()
ldapPass, _ := s.settingService.GetLdapPassword()
baseDN, _ := s.settingService.GetLdapBaseDN()
userFilter, _ := s.settingService.GetLdapUserFilter()
userAttr, _ := s.settingService.GetLdapUserAttr()
host, _ := s.settingService.GetLdapHost()
port, _ := s.settingService.GetLdapPort()
useTLS, _ := s.settingService.GetLdapUseTLS()
bindDN, _ := s.settingService.GetLdapBindDN()
ldapPass, _ := s.settingService.GetLdapPassword()
baseDN, _ := s.settingService.GetLdapBaseDN()
userFilter, _ := s.settingService.GetLdapUserFilter()
userAttr, _ := s.settingService.GetLdapUserAttr()
cfg := ldaputil.Config{
Host: host,
Port: port,
UseTLS: useTLS,
BindDN: bindDN,
Password: ldapPass,
BaseDN: baseDN,
UserFilter: userFilter,
UserAttr: userAttr,
}
ok, err := ldaputil.AuthenticateUser(cfg, username, password)
if err != nil || !ok {
return nil
}
// On successful LDAP auth, continue 2FA checks below
}
cfg := ldaputil.Config{
Host: host,
Port: port,
UseTLS: useTLS,
BindDN: bindDN,
Password: ldapPass,
BaseDN: baseDN,
UserFilter: userFilter,
UserAttr: userAttr,
}
ok, err := ldaputil.AuthenticateUser(cfg, username, password)
if err != nil || !ok {
return nil
}
// On successful LDAP auth, continue 2FA checks below
}
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
if err != nil {

View file

@ -32,7 +32,7 @@
"copySuccess" = "Скопировано"
"sure" = "Да"
"encryption" = "Шифрование"
"useIPv4ForHost" = "Использовать IPv4 для хоста"
"useIPv4ForHost" = "Использовать IPv4 для подключения к хосту"
"transmission" = "Транспорт"
"host" = "Хост"
"path" = "Путь"
@ -46,8 +46,8 @@
"online" = "Онлайн"
"domainName" = "Домен"
"monitor" = "Мониторинг IP"
"certificate" = "SSL сертификат"
"fail" = "Ошибка"
"certificate" = "SSL-сертификат"
"fail" = "Сбой"
"comment" = "Комментарий"
"success" = "Успешно"
"lastOnline" = "Был(а) в сети"
@ -55,17 +55,17 @@
"install" = "Установка"
"clients" = "Клиенты"
"usage" = "Использование"
"twoFactorCode" = "Код"
"twoFactorCode" = "Код 2FA"
"remained" = "Остаток"
"security" = "Безопасность"
"secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, не вводите конфиденциальную информацию, пока не установите SSL сертификат для защиты соединения"
"secAlertConf" = "Некоторые настройки уязвимы для атак. Чтобы в будущем не было проблем, нужно усилить защиту."
"secAlertSSL" = "Ваше подключение к панели не защищено. Установите SSL сертификат для защиты данных."
"secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите случайный или просто другой порт."
"secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Сделайте адрес сложным."
"secAlertSubURI" = "URI-адрес подписки по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес."
"secAlertSubJsonURI" = "URI-адрес по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-адрес."
"secAlertSsl" = "Соединение не защищено. Не вводите конфиденциальные данные до установки SSL-сертификата."
"secAlertConf" = "Некоторые настройки уязвимы. Рекомендуется усилить защиту для предотвращения атак."
"secAlertSSL" = "Подключение к панели не защищено. Установите SSL-сертификат для защиты данных."
"secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите нестандартный или случайный порт."
"secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Настройте уникальный и сложный URI."
"secAlertSubURI" = "URI подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес."
"secAlertSubJsonURI" = "URI JSON-подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес."
"emptyDnsDesc" = "Нет добавленных DNS-серверов."
"emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов."
"emptyBalancersDesc" = "Нет добавленных балансировщиков."
@ -83,15 +83,15 @@
"individualLinks" = "Индивидуальные ссылки"
"active" = "Активна"
"inactive" = "Неактивна"
"unlimited" = "Безлимит"
"noExpiry" = "Без срока"
"unlimited" = "Неограниченно"
"noExpiry" = "Бессрочно"
[menu]
"theme" = "Тема"
"dark" = "Темная"
"ultraDark" = "Очень темная"
"dashboard" = "Дашборд"
"inbounds" = "Инбаунды"
"inbounds" = "Подключения"
"settings" = "Настройки"
"xray" = "Настройки Xray"
"logout" = "Выход"
@ -107,7 +107,7 @@
"emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверные данные учетной записи."
"successLogin" = "Вы успешно вошли в аккаунт"
"successLogin" = "Вход выполнен успешно"
[pages.index]
"title" = "Дашборд"
@ -122,7 +122,7 @@
"stopXray" = "Остановить"
"restartXray" = "Перезапустить"
"xraySwitch" = "Выбор версии"
"xraySwitchClick" = "Выберите желаемую версию"
"xraySwitchClick" = "Выберите нужную версию"
"xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки"
"xrayStatusUnknown" = "Неизвестно"
"xrayStatusRunning" = "Запущен"
@ -134,7 +134,7 @@
"systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут"
"connectionCount" = "Количество соединений"
"ipAddresses" = "IP-адреса сервера"
"toggleIpVisibility" = "Переключить видимость IP-адресов сервера"
"toggleIpVisibility" = "Скрыть или показать IP-адреса сервера"
"overallSpeed" = "Общая скорость передачи трафика"
"upload" = "Отправка"
"download" = "Загрузка"
@ -168,10 +168,10 @@
[pages.inbounds]
"allTimeTraffic" = "Общий трафик"
"allTimeTrafficUsage" = "Общее использование за все время"
"title" = "Инбаунды"
"totalDownUp" = "Объем отправленного/полученного трафика"
"title" = "Подключения"
"totalDownUp" = "Отправлено/получено"
"totalUsage" = "Всего трафика"
"inboundCount" = "Всего инбаундов"
"inboundCount" = "Всего подключений"
"operate" = "Меню"
"enable" = "Включить"
"remark" = "Примечание"
@ -185,13 +185,13 @@
"createdAt" = "Создано"
"updatedAt" = "Обновлено"
"resetTraffic" = "Сброс трафика"
"addInbound" = "Создать инбаунд"
"addInbound" = "Создать подключение"
"generalActions" = "Общие действия"
"autoRefresh" = "Автообновление"
"autoRefreshInterval" = "Интервал"
"modifyInbound" = "Изменить инбаунд"
"deleteInbound" = "Удалить инбаунд"
"deleteInboundContent" = "Вы уверены, что хотите удалить инбаунд?"
"modifyInbound" = "Изменить подключение"
"deleteInbound" = "Удалить подключение"
"deleteInboundContent" = "Вы уверены, что хотите удалить подключение?"
"deleteClient" = "Удалить клиента"
"deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
"resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?"
@ -214,11 +214,11 @@
"export" = "Экспорт ссылок"
"clone" = "Клонировать"
"cloneInbound" = "Клонировать"
"cloneInboundContent" = "Будут клонированы все настройки инбаундов, кроме списка клиентов, порта и IP-адреса прослушивания"
"cloneInboundContent" = "Будут клонированы все настройки подключений, кроме списка клиентов, порта и IP-адреса прослушивания"
"cloneInboundOk" = "Клонировано"
"resetAllTraffic" = "Сброс трафика всех инбаундов"
"resetAllTrafficTitle" = "Сброс трафика всех инбаундов"
"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех инбаундов?"
"resetAllTraffic" = "Сброс трафика всех подключений"
"resetAllTrafficTitle" = "Сброс трафика всех подключений"
"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?"
"resetInboundClientTraffics" = "Сброс трафика клиента"
"resetInboundClientTrafficTitle" = "Сброс трафика клиентов"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?"
@ -231,7 +231,7 @@
"email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email"
"IPLimit" = "Лимит по количеству IP"
"IPLimitDesc" = "Ограничение количества одновременных подключений с разных IP(0 отключить)"
"IPLimitDesc" = "Ограничение числа одновременных подключений с разных IP (0 отключить)"
"IPLimitlog" = "Лог IP-адресов"
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)"
"IPLimitlogclear" = "Очистить лог"
@ -240,19 +240,19 @@
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'"
"info" = "Информация"
"same" = "Тот же"
"inboundData" = "Данные инбаундов"
"exportInbound" = "Экспорт инбаундов"
"inboundData" = "Данные подключений"
"exportInbound" = "Экспорт подключений"
"import" = "Импортировать"
"importInbound" = "Импорт инбаундов"
"importInbound" = "Импорт подключений"
"periodicTrafficResetTitle" = "Сброс трафика"
"periodicTrafficResetDesc" = "Автоматический сброс счетчика трафика через указанные интервалы"
"lastReset" = "Последний сброс"
[pages.client]
"add" = "Создать клиента"
"add" = "Добавить клиента"
"edit" = "Редактировать клиента"
"submitAdd" = "Добавить"
"submitEdit" = "Сохранить"
"submitEdit" = "Сохранить изменения"
"clientCount" = "Количество клиентов"
"bulk" = "Добавить несколько"
"method" = "Метод"
@ -276,13 +276,13 @@
"obtain" = "Получить"
"updateSuccess" = "Обновление прошло успешно"
"logCleanSuccess" = "Лог был очищен"
"inboundsUpdateSuccess" = "Инбаунды успешно обновлены"
"inboundUpdateSuccess" = "Инбаунд успешно обновлено"
"inboundCreateSuccess" = "Инбаунд успешно создано"
"inboundDeleteSuccess" = "Инбаунд успешно удалено"
"inboundClientAddSuccess" = "Клиент(ы) инбаунда добавлен(ы)"
"inboundClientDeleteSuccess" = "Клиент инбаунда удалён"
"inboundClientUpdateSuccess" = "Клиент инбаунда обновлён"
"inboundsUpdateSuccess" = "Подключения успешно обновлены"
"inboundUpdateSuccess" = "Подключение успешно обновлено"
"inboundCreateSuccess" = "Подключение успешно создано"
"inboundDeleteSuccess" = "Подключение успешно удалено"
"inboundClientAddSuccess" = "Клиент(ы) подключения добавлен(ы)"
"inboundClientDeleteSuccess" = "Клиент подключения удалён"
"inboundClientUpdateSuccess" = "Клиент подключения обновлён"
"delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены"
"resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен"
"resetAllTrafficSuccess" = "Весь трафик сброшен"
@ -310,7 +310,7 @@
[pages.settings]
"title" = "Настройки"
"save" = "Сохранить"
"infoDesc" = "Каждое внесённое изменение должно быть сохранено. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу."
"infoDesc" = "Сохраните изменения и перезапустите панель для их применения."
"restartPanel" = "Перезапуск панели"
"restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера"
"restartPanelSuccess" = "Панель успешно перезапущена"
@ -318,11 +318,11 @@
"resetDefaultConfig" = "Восстановить настройки по умолчанию"
"panelSettings" = "Панель"
"securitySettings" = "Учетная запись"
"TGBotSettings" = "Telegram"
"TGBotSettings" = "Telegram-Бот"
"panelListeningIP" = "IP-адрес для управления панелью"
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
"panelListeningDomain" = "Домен панели"
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы подключаться с любых доменов и IP-адресов"
"panelListeningDomainDesc" = "Оставьте пустым для подключения с любых доменов и IP."
"panelPort" = "Порт панели"
"panelPortDesc" = "Порт, на котором работает панель"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
@ -332,11 +332,11 @@
"panelUrlPath" = "Корневой путь URL адреса панели"
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'"
"pageSize" = "Размер нумерации страниц"
"pageSizeDesc" = "Определить размер страницы для таблицы инбаундов. Установите 0, чтобы отключить"
"pageSizeDesc" = "Определить размер страницы для таблицы подключений. Установите 0, чтобы отключить"
"remarkModel" = "Модель примечания и символ разделения"
"datepicker" = "Выбор даты"
"datepicker" = "Тип календаря"
"datepickerPlaceholder" = "Выберите дату"
"datepickerDescription" = "Запланированные задачи будут выполняться в выбранное время"
"datepickerDescription" = "Запланированные задачи будут выполняться в соответствии с этим календарем."
"sampleRemark" = "Пример примечания"
"oldUsername" = "Текущий логин"
"currentPassword" = "Текущий пароль"
@ -346,7 +346,7 @@
"telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота"
"telegramToken" = "Токен Telegram бота"
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
"telegramProxy" = "Прокси Socks5"
"telegramProxy" = "Прокси-сервер Socks5"
"telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству."
"telegramAPIServer" = "API-сервер Telegram"
"telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию."
@ -451,11 +451,11 @@
"RoutingStrategy" = "Настройка маршрутизации доменов"
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
"Torrent" = "Заблокировать BitTorrent"
"Inbounds" = "Инбаунды"
"Inbounds" = "Входящие подключения"
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
"Outbounds" = "Аутбаунды"
"Outbounds" = "Исходящие подключения"
"Balancers" = "Балансировщик"
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить аутбаунды для этого сервера"
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера"
"Routings" = "Маршрутизация"
"RoutingsDesc" = "Важен приоритет каждого правила!"
"completeTemplate" = "Все"
@ -486,8 +486,8 @@
"down" = "Опустить вниз"
"source" = "Источник"
"dest" = "Пункт назначения"
"inbound" = "Инбаунд"
"outbound" = "Аутбаунд"
"inbound" = "Входящее подключение"
"outbound" = "Исходящее подключение"
"balancer" = "Балансировщик"
"info" = "Информация"
"add" = "Создать правило"
@ -495,9 +495,9 @@
"useComma" = "Элементы, разделённые запятыми"
[pages.xray.outbound]
"addOutbound" = "Создать аутбаунд"
"addOutbound" = "Создать исходящее подключение"
"addReverse" = "Создать реверс-прокси"
"editOutbound" = "Изменить аутбаунд"
"editOutbound" = "Изменить исходящее подключение"
"editReverse" = "Редактировать реверс-прокси"
"tag" = "Тег"
"tagDesc" = "Уникальный тег"
@ -511,7 +511,7 @@
"intercon" = "Соединение"
"settings" = "Настройки"
"accountInfo" = "Информация об учетной записи"
"outboundStatus" = "Статус аутбаунда"
"outboundStatus" = "Статус исходящего подключения"
"sendThrough" = "Отправить через"
[pages.xray.balancer]
@ -587,8 +587,8 @@
"modifyUser" = "Вы успешно изменили учетные данные администратора."
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
"getOutboundTrafficError" = "Ошибка получения трафика аутбаунда"
"resetOutboundTrafficError" = "Ошибка сброса трафика аутбаунда"
"getOutboundTrafficError" = "Ошибка получения трафика исходящего подключения"
"resetOutboundTrafficError" = "Ошибка сброса трафика исходящего подключения"
[tgbot]
"keyboardClosed" = "❌ Клавиатура закрыта."
@ -596,7 +596,7 @@
"noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду."
"wentWrong" = "❌ Что-то пошло не так..."
"noIpRecord" = "❗ Нет записей об IP-адресе."
"noInbounds" = "❗ У вас не настроено ни одного инбаунда."
"noInbounds" = "❗ У вас не настроено ни одного входящего подключения."
"unlimited" = "♾ Безлимит"
"add" = "Добавить"
"month" = "Месяц"
@ -606,7 +606,7 @@
"hours" = "Часов"
"minutes" = "Минуты"
"unknown" = "Неизвестно"
"inbounds" = "Инбаунды"
"inbounds" = "Входящие подключения"
"clients" = "Клиенты"
"offline" = "🔴 Офлайн"
"online" = "🟢 Онлайн"
@ -620,7 +620,7 @@
"status" = "✅ Бот функционирует нормально."
"usage" = "❗ Пожалуйста, укажите email для поиска."
"getID" = "🆔 Ваш User ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска инбаундов (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска входящих подключений (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
"helpClientCommands" = "💲 Для просмотра информации о вашей подписке используйте команду:\r\n<code>/usage [Email]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Ядро Xray успешно перезапущено."
@ -656,7 +656,7 @@
"username" = "👤 Имя пользователя: {{ .Username }}\r\n"
"password" = "👤 Пароль: {{ .Password }}\r\n"
"time" = "⏰ Время: {{ .Time }}\r\n"
"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n"
"inbound" = "📍 Входящее подключение: {{ .Remark }}\r\n"
"port" = "🔌 Порт: {{ .Port }}\r\n"
"expire" = "📅 Дата окончания: {{ .Time }}\r\n"
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n"
@ -685,12 +685,12 @@
"pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль."
"email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email."
"comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий."
"inbound_client_data_id" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!"
"inbound_client_data_pass" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!"
"inbound_client_data_id" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!"
"inbound_client_data_pass" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!"
"cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄"
"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}"
"using_default_value" = "Используется значение по умолчанию👌"
"incorrect_input" ="Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫"
"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}"
"using_default_value" = "Используется значение по умолчанию👌"
"incorrect_input" = "Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫"
"AreYouSure" = "Вы уверены? 🤔"
"SuccessResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно"
"FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠 Ошибка: [ {{ .ErrorMessage }} ]"
@ -707,7 +707,7 @@
"confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?"
"dbBackup" = "📂 Бэкап БД"
"serverUsage" = "💻 Состояние сервера"
"getInbounds" = "🔌 Инбаунды"
"getInbounds" = "🔌 Входящие подключения"
"depleteSoon" = "⚠️ Скоро конец"
"clientUsage" = "Статистика клиента"
"onlines" = "🟢 Онлайн"
@ -731,7 +731,7 @@
"allClients" = "👥 Все клиенты"
"addClient" = " Новый клиент"
"submitDisable" = "Добавить отключенным ☑️"
"submitEnable" = "Добавить включенныи ✅"
"submitEnable" = "Добавить включенным ✅"
"use_default" = "🏷️ Использовать по умолчанию"
"change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 Пароль"
@ -743,7 +743,7 @@
[tgbot.answers]
"successfulOperation" = "✅ Успешно!"
"errorOperation" = "❗ Ошибка в операции."
"getInboundsFailed" = "❌ Не удалось получить инбаунды."
"getInboundsFailed" = "❌ Не удалось получить входящие подключения."
"getClientsFailed" = "❌ Не удалось получить клиентов."
"canceled" = "❌ {{ .Email }}: Операция отменена."
"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен."
@ -760,5 +760,5 @@
"enableSuccess" = "✅ {{ .Email }}: Включено успешно."
"disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
"askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Выберите клиента для инбаунда {{ .Inbound }}"
"chooseInbound" = "Выберите инбаунд"
"chooseClient" = "Выберите клиента для входящего подключения {{ .Inbound }}"
"chooseInbound" = "Выберите входящее подключение"

94
x-ui.sh
View file

@ -863,10 +863,32 @@ delete_ports() {
fi
}
update_all_geofiles() {
update_main_geofiles
update_ir_geofiles
update_ru_geofiles
}
update_main_geofiles() {
wget -O geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
}
update_ir_geofiles() {
wget -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
}
update_ru_geofiles() {
wget -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
}
update_geo() {
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}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)"
echo -e "${green}\t4.${plain} All"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice
@ -877,41 +899,25 @@ update_geo() {
show_menu
;;
1)
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip.dat geosite.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
update_main_geofiles
echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
restart
;;
2)
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip_IR.dat geosite_IR.dat
wget -O geoip_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
update_ir_geofiles
echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
restart
;;
3)
if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip_RU.dat geosite_RU.dat
wget -O geoip_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
update_ru_geofiles
echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
restart
;;
4)
update_all_geofiles
echo -e "${green}All geo files have been updated successfully!${plain}"
restart
;;
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
update_geo
@ -1869,24 +1875,25 @@ SSH_port_forwarding() {
}
show_usage() {
echo -e "┌───────────────────────────────────────────────────────┐
${blue}x-ui control menu usages (subcommands):${plain}
│ │
${blue}x-ui${plain} - Admin Management Script │
${blue}x-ui start${plain} - Start │
${blue}x-ui stop${plain} - Stop │
${blue}x-ui restart${plain} - Restart │
${blue}x-ui status${plain} - Current Status │
${blue}x-ui settings${plain} - Current Settings │
${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
${blue}x-ui disable${plain} - Disable Autostart on OS Startup │
${blue}x-ui log${plain} - Check logs │
${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
${blue}x-ui update${plain} - Update │
${blue}x-ui legacy${plain} - legacy version │
${blue}x-ui install${plain} - Install │
${blue}x-ui uninstall${plain} - Uninstall │
└───────────────────────────────────────────────────────┘"
echo -e "┌────────────────────────────────────────────────────────────────┐
${blue}x-ui control menu usages (subcommands):${plain}
│ │
${blue}x-ui${plain} - Admin Management Script │
${blue}x-ui start${plain} - Start │
${blue}x-ui stop${plain} - Stop │
${blue}x-ui restart${plain} - Restart │
${blue}x-ui status${plain} - Current Status │
${blue}x-ui settings${plain} - Current Settings │
${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
${blue}x-ui disable${plain} - Disable Autostart on OS Startup │
${blue}x-ui log${plain} - Check logs │
${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
${blue}x-ui update${plain} - Update │
${blue}x-ui update-all-geofiles${plain} - Update all geo files │
${blue}x-ui legacy${plain} - Legacy version │
${blue}x-ui install${plain} - Install │
${blue}x-ui uninstall${plain} - Uninstall │
└────────────────────────────────────────────────────────────────┘"
}
show_menu() {
@ -2056,6 +2063,9 @@ if [[ $# > 0 ]]; then
"uninstall")
check_install 0 && uninstall 0
;;
"update-all-geofiles")
check_install 0 && update_all_geofiles 0 && restart 0
;;
*) show_usage ;;
esac
else