Compare commits

...

33 Commits

Author SHA1 Message Date
kazan417
38d87230d3 Update x-ui.sh (#3947)
looks like now cert management is option 19
2026-03-18 19:45:45 +01:00
MHSanaei
f0f98c7122 Add Go code analyzer workflow 2026-03-17 23:01:15 +01:00
Abdalrahman
554981d9d3 feat(tgbot): send connection links and qrs on client creation (closes #3320)\n\n- Refactored inline keyboards into getCommonClientButtons to respect DRY\n- Extended SubmitAddClient callback handlers to dispatch individual links and QR codes to the bot chat on success. (#3888) 2026-03-17 22:09:49 +01:00
Nikolay
a08f1c6c13 Update translate.ru_RU.toml (#3889)
Change to plural (geofiles, not geofile)
2026-03-17 21:24:09 +01:00
Alimpo
7f7ae0c547 fix: stop overwriting client_traffics.enable with JSON enable in GetClientTrafficByEmail (#3931)
When a client hit traffic/expiry limit, disableInvalidClients sets
client_traffics.enable=false and removes the user from Xray. GetClientTrafficByEmail
was overwriting that with settings.clients[].enable (admin config), so
ResetClientTraffic never saw the client as disabled and did not re-add
the user. Clients could not connect until manually disabled/re-enabled.
Now the DB runtime enable flag is preserved; reset correctly re-adds
the user to Xray.
2026-03-17 21:20:24 +01:00
HamidReza Sadeghzadeh
60abeaad66 fix: Ban new IPs with fail2ban instead of disconnected the client. (#3919)
* fix: Ban new IPs with fail2ban  instead of disconnected the client.

* fix: Remove unused strconv import

* fix: Revert log fail2ban format
2026-03-17 21:18:10 +01:00
dependabot[bot]
a6d0100381 Bump docker/metadata-action from 5 to 6 (#3942)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 21:10:09 +01:00
dependabot[bot]
6767f76ccf Bump actions/upload-artifact from 4 to 7 (#3941)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 21:09:56 +01:00
dependabot[bot]
e4add73c9e Bump actions/checkout from 5 to 6 (#3940)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 21:05:43 +01:00
dependabot[bot]
ff72090e1a Bump docker/setup-buildx-action from 3 to 4 (#3938)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 21:05:28 +01:00
dependabot[bot]
a3e1bd59df Bump docker/build-push-action from 6 to 7 (#3937)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 21:05:07 +01:00
dependabot[bot]
5bbb48a8fd Bump docker/setup-qemu-action from 3 to 4 (#3936)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 21:04:54 +01:00
dependabot[bot]
ee84d585f9 Bump docker/login-action from 3 to 4 (#3939)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 21:04:41 +01:00
Sanaei
7b03346cfc Set package ecosystem to GitHub Actions in dependabot.yml 2026-03-17 21:03:32 +01:00
MHSanaei
258b08fff3 Update fail2ban filter regex in x-ui.sh 2026-03-08 11:53:34 +01:00
Aleksei Sidorenko
a2097ad062 feat: mask password in telegram notification on 2FA failure (#3884) 2026-03-04 18:26:53 +01:00
MHSanaei
52fdf5d429 v2.8.11 2026-03-04 13:54:01 +01:00
MHSanaei
34d8885075 Adjust KCP MTU when selecting xDNS mask 2026-03-04 13:39:14 +01:00
MHSanaei
5740996436 update dependencies 2026-03-04 13:05:29 +01:00
Artur
874aae8080 Add cron to ubuntu packages (#3875) 2026-03-04 12:36:45 +01:00
子寒
842fae18d7 Add 'default' runlevel to x-ui service in Alpine (#3854)
it should be 'default' runlevel when add x-ui service to openrc, default is 'sysinit' runlevel. 'sysinit' runlevel is unnecessary,maybe.
if not, there is an error when call to function 'check_enabled()' as command 'grep default -c' can`t print 'default' runlevel.

check_enabled() {
    if [[ $release == "alpine" ]]; then
        if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then
            return 0
        else
            return 1
        fi
2026-03-04 12:32:01 +01:00
Happ-dev
ccd223aeea Fix DeepLink for Happ, remove encoding URL (#3863)
Co-authored-by: y.sivushkin <y.sivushkin@corp.101xp.com>
2026-03-04 12:29:46 +01:00
Aleksei Sidorenko
96b8fe472c Fix: escape HTML characters in tgbot start command (#3883) 2026-03-04 11:35:24 +01:00
Nabi KaramAliZadeh
59b695ba83 fix: remove excluded paths from gzip middleware in router initialization (#3860) 2026-03-01 15:18:16 +01:00
Alireza Ahmadi
159b85f979 Merge pull request #3828 from MHSanaei/restartXrayOption
[feat] restart xray-core from cli #3825
2026-02-25 21:09:42 +01:00
Alireza Ahmadi
3ec5b3589f fix windows build 2026-02-20 02:07:46 +01:00
Alireza Ahmadi
2b1d3e7347 [feat] restart xray-core from cli #3825 2026-02-20 00:03:16 +01:00
MHSanaei
37f0880f8f Bump Go to 1.26 2026-02-16 01:10:43 +01:00
MHSanaei
5b796672e9 Improve telego client robustness and retries
Add a createRobustFastHTTPClient helper to configure fasthttp.Client with better timeouts, connection limits, retries and optional SOCKS5 proxy dialing. Validate and sanitize proxy and API server URLs instead of returning early on invalid values, and build telego.Bot options dynamically. Reduce long-polling timeout to detect connection issues faster and adjust update retrieval comments. Implement exponential-backoff retry logic for SendMessage calls to handle transient connection/timeouts and improve delivery reliability; also reduce inter-message delay for better throughput.
2026-02-14 22:49:19 +01:00
MHSanaei
3fa0da38c9 Add timeouts and delays to backup sends
Add rate-limit friendly delays and context timeouts when sending backups via Telegram. Iterate admin IDs with index to sleep 1s between sends; add 30s context.WithTimeout for each SendDocument call and defer file.Close() for opened files; insert a 500ms pause between sending DB and config files. These changes improve resource cleanup and reduce chance of Telegram rate-limit/timeout failures.
2026-02-14 22:31:41 +01:00
MHSanaei
8eb1225734 translate bug fix #3789 2026-02-14 21:41:20 +01:00
MHSanaei
e5c0fe3edf bug fix #3785 2026-02-11 22:21:09 +01:00
MHSanaei
f4057989f5 Require HTTP 200 from curl before using IP
Replace simple curl+trim checks with a response+http_code parse to ensure the remote URL returns HTTP 200 and a non-empty body before assigning server_ip. Changes applied to install.sh, update.sh and x-ui.sh: use curl -w to append the status code, extract http_code and ip_result, and only set server_ip when http_code == 200 and ip_result is non-empty. This makes the IP discovery more robust against error pages or partial responses while keeping the existing timeout behavior.
2026-02-11 21:32:23 +01:00
47 changed files with 757 additions and 362 deletions

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -15,13 +15,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
with: with:
submodules: true submodules: true
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: | images: |
hsanaeii/3x-ui hsanaeii/3x-ui
@@ -32,28 +32,28 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
with: with:
install: true install: true
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }} password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: . context: .
push: true push: true

View File

@@ -2,11 +2,9 @@ name: Release 3X-UI
on: on:
workflow_dispatch: workflow_dispatch:
release:
types: [published]
push: push:
branches: branches:
- main - '**'
tags: tags:
- "v*.*.*" - "v*.*.*"
paths: paths:
@@ -20,9 +18,48 @@ on:
- 'x-ui.service.debian' - 'x-ui.service.debian'
- 'x-ui.service.arch' - 'x-ui.service.arch'
- 'x-ui.service.rhel' - 'x-ui.service.rhel'
pull_request:
jobs: jobs:
analyze:
name: Analyze Go code
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "These files are not gofmt-formatted:"
echo "$unformatted"
exit 1
fi
- name: Run go vet
run: go vet ./...
- name: Run staticcheck
uses: dominikh/staticcheck-action@v1
with:
version: "latest"
install-go: false
- name: Run tests
run: go test -race -shuffle=on ./...
build: build:
needs: analyze
permissions: permissions:
contents: write contents: write
strategy: strategy:
@@ -38,7 +75,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
@@ -133,19 +170,17 @@ jobs:
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
- name: Upload files to Artifacts - name: Upload files to Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: x-ui-linux-${{ matrix.platform }} name: x-ui-linux-${{ matrix.platform }}
path: ./x-ui-linux-${{ matrix.platform }}.tar.gz path: ./x-ui-linux-${{ matrix.platform }}.tar.gz
- name: Upload files to GH release - name: Upload files to GH release
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
if: | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
(github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }} tag: ${{ github.ref_name }}
file: x-ui-linux-${{ matrix.platform }}.tar.gz file: x-ui-linux-${{ matrix.platform }}.tar.gz
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
overwrite: true overwrite: true
@@ -156,6 +191,7 @@ jobs:
# ================================= # =================================
build-windows: build-windows:
name: Build for Windows name: Build for Windows
needs: analyze
permissions: permissions:
contents: write contents: write
strategy: strategy:
@@ -165,7 +201,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
@@ -230,19 +266,17 @@ jobs:
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip" Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
- name: Upload files to Artifacts - name: Upload files to Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: x-ui-windows-amd64 name: x-ui-windows-amd64
path: ./x-ui-windows-amd64.zip path: ./x-ui-windows-amd64.zip
- name: Upload files to GH release - name: Upload files to GH release
uses: svenstaro/upload-release-action@v2 uses: svenstaro/upload-release-action@v2
if: | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
(github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }} tag: ${{ github.ref_name }}
file: x-ui-windows-amd64.zip file: x-ui-windows-amd64.zip
asset_name: x-ui-windows-amd64.zip asset_name: x-ui-windows-amd64.zip
overwrite: true overwrite: true

View File

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

View File

@@ -1 +1 @@
2.8.10 2.8.11

25
go.mod
View File

@@ -1,22 +1,22 @@
module github.com/mhsanaei/3x-ui/v2 module github.com/mhsanaei/3x-ui/v2
go 1.25.7 go 1.26.0
require ( require (
github.com/gin-contrib/gzip v1.2.5 github.com/gin-contrib/gzip v1.2.5
github.com/gin-contrib/sessions v1.0.4 github.com/gin-contrib/sessions v1.0.4
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.12.0
github.com/go-ldap/ldap/v3 v3.4.12 github.com/go-ldap/ldap/v3 v3.4.12
github.com/goccy/go-json v0.10.5 github.com/goccy/go-json v0.10.5
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mymmrac/telego v1.6.0 github.com/mymmrac/telego v1.7.0
github.com/nicksnyder/go-i18n/v2 v2.6.1 github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.26.1 github.com/shirou/gopsutil/v4 v4.26.2
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.69.0 github.com/valyala/fasthttp v1.69.0
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
@@ -25,7 +25,7 @@ require (
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.48.0
golang.org/x/sys v0.41.0 golang.org/x/sys v0.41.0
golang.org/x/text v0.34.0 golang.org/x/text v0.34.0
google.golang.org/grpc v1.78.0 google.golang.org/grpc v1.79.1
gorm.io/driver/sqlite v1.6.0 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
) )
@@ -39,7 +39,7 @@ require (
github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/ebitengine/purego v0.9.1 // indirect github.com/ebitengine/purego v0.10.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // 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-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
@@ -60,7 +60,7 @@ require (
github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.34 // indirect github.com/mattn/go-sqlite3 v1.14.34 // indirect
github.com/miekg/dns v1.1.72 // indirect github.com/miekg/dns v1.1.72 // indirect
@@ -72,29 +72,30 @@ require (
github.com/quic-go/quic-go v0.59.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect github.com/refraction-networking/utls v1.8.2 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.7.18 // indirect github.com/sagernet/sing v0.8.1 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastjson v1.6.7 // indirect github.com/valyala/fastjson v1.6.10 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.24.0 // indirect golang.org/x/arch v0.24.0 // indirect
golang.org/x/exp v0.0.0-20260209203927-2842357ff358 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/mod v0.33.0 // indirect golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.50.0 // indirect golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.42.0 // indirect golang.org/x/tools v0.42.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
lukechampine.com/blake3 v1.4.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect

68
go.sum
View File

@@ -14,6 +14,8 @@ github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uS
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
@@ -21,8 +23,8 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13/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 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
@@ -33,8 +35,8 @@ github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kb
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
@@ -115,8 +117,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
@@ -128,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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v1.6.0 h1:Zc8rgyHozvd/7ZgyrigyHdAF9koHYMfilYfyB6wlFC0= github.com/mymmrac/telego v1.7.0 h1:yRO/l00tFGG4nY66ufUKb4ARqv7qx9+LsjQv/b0NEyo=
github.com/mymmrac/telego v1.6.0/go.mod h1:xt6ZWA8zi8KmuzryE1ImEdl9JSwjHNpM4yhC7D8hU4Y= github.com/mymmrac/telego v1.7.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@@ -154,12 +156,12 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E= github.com/sagernet/sing v0.8.1 h1:Li+zg4xdiMsvdX4j50TPqmSG8LF/TB9US2qlAN40izU=
github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.8.1/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 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo= github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc= github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 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/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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -185,8 +187,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
@@ -201,18 +203,20 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
@@ -225,12 +229,12 @@ golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -253,10 +257,10 @@ 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= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -76,7 +76,7 @@ is_port_in_use() {
install_base() { install_base() {
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get update && apt-get install -y -q curl tar tzdata socat ca-certificates apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates
;; ;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates dnf -y update && dnf install -y -q curl tar tzdata socat ca-certificates
@@ -654,8 +654,11 @@ config_after_install() {
) )
local server_ip="" local server_ip=""
for ip_address in "${URL_lists[@]}"; do for ip_address in "${URL_lists[@]}"; do
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
if [[ -n "${server_ip}" ]]; then local http_code=$(echo "$response" | tail -n1)
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
server_ip="${ip_result}"
break break
fi fi
done done

View File

@@ -16,6 +16,7 @@ import (
"github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/sub" "github.com/mhsanaei/3x-ui/v2/sub"
"github.com/mhsanaei/3x-ui/v2/util/crypto" "github.com/mhsanaei/3x-ui/v2/util/crypto"
"github.com/mhsanaei/3x-ui/v2/util/sys"
"github.com/mhsanaei/3x-ui/v2/web" "github.com/mhsanaei/3x-ui/v2/web"
"github.com/mhsanaei/3x-ui/v2/web/global" "github.com/mhsanaei/3x-ui/v2/web/global"
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
@@ -70,7 +71,7 @@ func runWebServer() {
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
// Trap shutdown signals // Trap shutdown signals
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM) signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, sys.SIGUSR1)
for { for {
sig := <-sigCh sig := <-sigCh
@@ -108,6 +109,12 @@ func runWebServer() {
return return
} }
log.Println("Sub server restarted successfully.") log.Println("Sub server restarted successfully.")
case sys.SIGUSR1:
logger.Info("Received USR1 signal, restarting xray-core...")
err := server.RestartXray()
if err != nil {
logger.Error("Failed to restart xray-core:", err)
}
default: default:
// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown --- // --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---

View File

@@ -143,11 +143,11 @@ func (a *SUBController) subs(c *gin.Context) {
// Add headers // Add headers
header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) header := fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
profileUrl := a.subProfileUrl profileUrl := a.subProfileUrl
if profileUrl == "" { if profileUrl == "" {
profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI) profileUrl = fmt.Sprintf("%s://%s%s", scheme, hostWithPort, c.Request.RequestURI)
} }
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules) a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle, a.subSupportUrl, profileUrl, a.subAnnounce, a.subEnableRouting, a.subRoutingRules)
if a.subEncrypt { if a.subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))

View File

@@ -687,8 +687,11 @@ config_after_update() {
) )
local server_ip="" local server_ip=""
for ip_address in "${URL_lists[@]}"; do for ip_address in "${URL_lists[@]}"; do
server_ip=$(${curl_bin} -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
if [[ -n "${server_ip}" ]]; then local http_code=$(echo "$response" | tail -n1)
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
server_ip="${ip_result}"
break break
fi fi
done done

View File

@@ -7,11 +7,14 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"sync" "sync"
"syscall"
"github.com/shirou/gopsutil/v4/net" "github.com/shirou/gopsutil/v4/net"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
var SIGUSR1 = syscall.SIGUSR1
func GetTCPCount() (int, error) { func GetTCPCount() (int, error) {
stats, err := net.Connections("tcp") stats, err := net.Connections("tcp")
if err != nil { if err != nil {

View File

@@ -12,8 +12,11 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
) )
var SIGUSR1 = syscall.SIGUSR1
func getLinesNum(filename string) (int, error) { func getLinesNum(filename string) (int, error) {
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {

View File

@@ -12,6 +12,8 @@ import (
"github.com/shirou/gopsutil/v4/net" "github.com/shirou/gopsutil/v4/net"
) )
var SIGUSR1 = syscall.Signal(0)
// GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp"). // GetConnectionCount returns the number of active connections for the specified protocol ("tcp" or "udp").
func GetConnectionCount(proto string) (int, error) { func GetConnectionCount(proto string) (int, error) {
if proto != "tcp" && proto != "udp" { if proto != "tcp" && proto != "udp" {

View File

@@ -144,7 +144,7 @@
return this.app.subUrl; return this.app.subUrl;
}, },
happUrl() { happUrl() {
return `happ://add/${encodeURIComponent(this.app.subUrl)}`; return `happ://add/${this.app.subUrl}`;
} }
}, },
methods: { methods: {

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"time"
"github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
@@ -193,6 +194,37 @@ func (a *InboundController) getClientIps(c *gin.Context) {
return return
} }
// Prefer returning a normalized string list for consistent UI rendering
type ipWithTimestamp struct {
IP string `json:"ip"`
Timestamp int64 `json:"timestamp"`
}
var ipsWithTime []ipWithTimestamp
if err := json.Unmarshal([]byte(ips), &ipsWithTime); err == nil && len(ipsWithTime) > 0 {
formatted := make([]string, 0, len(ipsWithTime))
for _, item := range ipsWithTime {
if item.IP == "" {
continue
}
if item.Timestamp > 0 {
ts := time.Unix(item.Timestamp, 0).Local().Format("2006-01-02 15:04:05")
formatted = append(formatted, fmt.Sprintf("%s (%s)", item.IP, ts))
continue
}
formatted = append(formatted, item.IP)
}
jsonObj(c, formatted, nil)
return
}
var oldIps []string
if err := json.Unmarshal([]byte(ips), &oldIps); err == nil && len(oldIps) > 0 {
jsonObj(c, oldIps, nil)
return
}
// If parsing fails, return as string
jsonObj(c, ips, nil) jsonObj(c, ips, nil)
} }

View File

@@ -1,6 +1,7 @@
package controller package controller
import ( import (
"fmt"
"net/http" "net/http"
"text/template" "text/template"
"time" "time"
@@ -71,14 +72,22 @@ func (a *IndexController) login(c *gin.Context) {
return return
} }
user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode) user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
timeStr := time.Now().Format("2006-01-02 15:04:05") timeStr := time.Now().Format("2006-01-02 15:04:05")
safeUser := template.HTMLEscapeString(form.Username) safeUser := template.HTMLEscapeString(form.Username)
safePass := template.HTMLEscapeString(form.Password) safePass := template.HTMLEscapeString(form.Password)
if user == nil { if user == nil {
logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c)) logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0)
notifyPass := safePass
if checkErr != nil && checkErr.Error() == "invalid 2fa code" {
translatedError := a.tgbot.I18nBot("tgbot.messages.2faFailed")
notifyPass = fmt.Sprintf("*** (%s)", translatedError)
}
a.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0)
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword")) pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
return return
} }

View File

@@ -612,7 +612,7 @@
</a-divider> </a-divider>
<a-form-item label='Type'> <a-form-item label='Type'>
<a-select v-model="mask.type" <a-select v-model="mask.type"
@change="(type) => mask.settings = mask._getDefaultSettings(type, {})" @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(outbound.stream.network === 'kcp') { outbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<!-- Salamander for Hysteria2 only --> <!-- Salamander for Hysteria2 only -->
<a-select-option v-if="outbound.protocol === Protocols.Hysteria" <a-select-option v-if="outbound.protocol === Protocols.Hysteria"
@@ -643,9 +643,9 @@
<a-select-option v-if="outbound.stream.network === 'kcp'" <a-select-option v-if="outbound.stream.network === 'kcp'"
value="mkcp-original"> value="mkcp-original">
mKCP Original</a-select-option> mKCP Original</a-select-option>
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP --> <!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP/KCP -->
<a-select-option <a-select-option
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp'].includes(outbound.stream.network)" v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(outbound.stream.network)"
value="xdns"> value="xdns">
xDNS (Experimental)</a-select-option> xDNS (Experimental)</a-select-option>
</a-select> </a-select>

View File

@@ -18,7 +18,7 @@
</a-divider> </a-divider>
<a-form-item label='Type'> <a-form-item label='Type'>
<a-select v-model="mask.type" <a-select v-model="mask.type"
@change="(type) => mask.settings = mask._getDefaultSettings(type, {})" @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<!-- mKCP-specific masks --> <!-- mKCP-specific masks -->
<a-select-option v-if="inbound.stream.network === 'kcp'" <a-select-option v-if="inbound.stream.network === 'kcp'"
@@ -48,9 +48,9 @@
<a-select-option v-if="inbound.stream.network === 'kcp'" <a-select-option v-if="inbound.stream.network === 'kcp'"
value="xicmp"> value="xicmp">
xICMP (Experimental)</a-select-option> xICMP (Experimental)</a-select-option>
<!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP --> <!-- xDNS for TCP/WS/HTTPUpgrade/XHTTP/KCP -->
<a-select-option <a-select-option
v-if="['tcp', 'ws', 'httpupgrade', 'xhttp'].includes(inbound.stream.network)" v-if="['tcp', 'ws', 'httpupgrade', 'xhttp', 'kcp'].includes(inbound.stream.network)"
value="xdns"> value="xdns">
xDNS (Experimental)</a-select-option> xDNS (Experimental)</a-select-option>
</a-select> </a-select>

View File

@@ -260,15 +260,31 @@
v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0"> v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td> <td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
<td> <td>
<a-tag>[[ infoModal.clientIps ]]</a-tag> <div
<a-icon type="sync" :spin="refreshing" @click="refreshIPs" style="max-height: 150px; overflow-y: auto; text-align: left;">
:style="{ margin: '0 5px' }"></a-icon> <div
<a-tooltip :title="[[ dbInbound.address ]]"> v-if="infoModal.clientIpsArray && infoModal.clientIpsArray.length > 0">
<template slot="title"> <a-tag
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span> v-for="(ipInfo, idx) in infoModal.clientIpsArray"
</template> :key="idx"
<a-icon type="delete" @click="clearClientIps"></a-icon> color="blue"
</a-tooltip> style="margin: 2px 0; display: block; font-family: monospace; font-size: 11px;">
[[ formatIpInfo(ipInfo) ]]
</a-tag>
</div>
<a-tag v-else>[[ infoModal.clientIps || 'No IP Record'
]]</a-tag>
</div>
<div style="margin-top: 5px;">
<a-icon type="sync" :spin="refreshing" @click="refreshIPs"
:style="{ margin: '0 5px' }"></a-icon>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
</template>
<a-icon type="delete" @click="clearClientIps"></a-icon>
</a-tooltip>
</div>
</td> </td>
</tr> </tr>
</table> </table>
@@ -542,12 +558,73 @@
<script> <script>
function refreshIPs(email) { function refreshIPs(email) {
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => { return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
if (msg.success) { if (!msg.success) {
try { return { text: 'No IP Record', array: [] };
return JSON.parse(msg.obj).join(', '); }
} catch (e) {
return msg.obj; const formatIpRecord = (record) => {
if (record == null) {
return '';
} }
if (typeof record === 'string' || typeof record === 'number') {
return String(record);
}
const ip = record.ip || record.IP || '';
const timestamp = record.timestamp || record.Timestamp || 0;
if (!ip) {
return String(record);
}
if (!timestamp) {
return String(ip);
}
const date = new Date(Number(timestamp) * 1000);
const timeStr = date
.toLocaleString('en-GB', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
.replace(',', '');
return `${ip} (${timeStr})`;
};
try {
let ips = msg.obj;
// If msg.obj is a string, try to parse it
if (typeof ips === 'string') {
try {
ips = JSON.parse(ips);
} catch (e) {
return { text: String(ips), array: [String(ips)] };
}
}
// Normalize single object response to array
if (ips && !Array.isArray(ips) && typeof ips === 'object') {
ips = [ips];
}
// New format or object array
if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') {
const result = ips.map((item) => formatIpRecord(item)).filter(Boolean);
return { text: result.join(' | '), array: result };
}
// Old format - simple array of IPs
if (Array.isArray(ips) && ips.length > 0) {
const result = ips.map((ip) => String(ip));
return { text: result.join(', '), array: result };
}
// Fallback for any other format
return { text: String(ips), array: [String(ips)] };
} catch (e) {
return { text: 'Error loading IPs', array: [] };
} }
}); });
} }
@@ -566,6 +643,7 @@
subLink: '', subLink: '',
subJsonLink: '', subJsonLink: '',
clientIps: '', clientIps: '',
clientIpsArray: [],
show(dbInbound, index) { show(dbInbound, index) {
this.index = index; this.index = index;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
@@ -583,8 +661,9 @@
].includes(this.inbound.protocol) ].includes(this.inbound.protocol)
) { ) {
if (app.ipLimitEnable && this.clientSettings.limitIp) { if (app.ipLimitEnable && this.clientSettings.limitIp) {
refreshIPs(this.clientStats.email).then((ips) => { refreshIPs(this.clientStats.email).then((result) => {
this.clientIps = ips; this.clientIps = result.text;
this.clientIpsArray = result.array;
}) })
} }
} }
@@ -655,6 +734,35 @@
}, },
}, },
methods: { methods: {
formatIpInfo(ipInfo) {
if (ipInfo == null) {
return '';
}
if (typeof ipInfo === 'string' || typeof ipInfo === 'number') {
return String(ipInfo);
}
const ip = ipInfo.ip || ipInfo.IP || '';
const timestamp = ipInfo.timestamp || ipInfo.Timestamp || 0;
if (!ip) {
return String(ipInfo);
}
if (!timestamp) {
return String(ip);
}
const date = new Date(Number(timestamp) * 1000);
const timeStr = date
.toLocaleString('en-GB', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
.replace(',', '');
return `${ip} (${timeStr})`;
},
copy(content) { copy(content) {
ClipboardManager ClipboardManager
.copyText(content) .copyText(content)
@@ -672,8 +780,9 @@
refreshIPs() { refreshIPs() {
this.refreshing = true; this.refreshing = true;
refreshIPs(this.infoModal.clientStats.email) refreshIPs(this.infoModal.clientStats.email)
.then((ips) => { .then((result) => {
this.infoModal.clientIps = ips; this.infoModal.clientIps = result.text;
this.infoModal.clientIpsArray = result.array;
}) })
.finally(() => { .finally(() => {
this.refreshing = false; this.refreshing = false;
@@ -686,6 +795,7 @@
return; return;
} }
this.infoModal.clientIps = 'No IP Record'; this.infoModal.clientIps = 'No IP Record';
this.infoModal.clientIpsArray = [];
}) })
.catch(() => {}); .catch(() => {});
}, },

View File

@@ -206,7 +206,7 @@
<a-menu-item key="android-npvtunnel" @click="copy(app.subUrl)">NPV <a-menu-item key="android-npvtunnel" @click="copy(app.subUrl)">NPV
Tunnel</a-menu-item> Tunnel</a-menu-item>
<a-menu-item key="android-happ" <a-menu-item key="android-happ"
@click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item> @click="open('happ://add/' + app.subUrl)">Happ</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</a-col> </a-col>

View File

@@ -10,7 +10,6 @@ import (
"regexp" "regexp"
"runtime" "runtime"
"sort" "sort"
"strconv"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database"
@@ -319,13 +318,14 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
} }
} }
// Convert back to slice and sort by timestamp (newest first) // Convert back to slice and sort by timestamp (oldest first)
// This ensures we always protect the original/current connections and ban new excess ones.
allIps := make([]IPWithTimestamp, 0, len(ipMap)) allIps := make([]IPWithTimestamp, 0, len(ipMap))
for ip, timestamp := range ipMap { for ip, timestamp := range ipMap {
allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp}) allIps = append(allIps, IPWithTimestamp{IP: ip, Timestamp: timestamp})
} }
sort.Slice(allIps, func(i, j int) bool { sort.Slice(allIps, func(i, j int) bool {
return allIps[i].Timestamp > allIps[j].Timestamp // Descending order (newest first) return allIps[i].Timestamp < allIps[j].Timestamp // Ascending order (oldest first)
}) })
shouldCleanLog := false shouldCleanLog := false
@@ -345,23 +345,17 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
if len(allIps) > limitIp { if len(allIps) > limitIp {
shouldCleanLog = true shouldCleanLog = true
// Keep only the newest IPs (up to limitIp) // Keep the oldest IPs (currently active connections) and ban the new excess ones.
keptIps := allIps[:limitIp] keptIps := allIps[:limitIp]
disconnectedIps := allIps[limitIp:] bannedIps := allIps[limitIp:]
// Log the disconnected IPs (old ones) // Log banned IPs in the format fail2ban filters expect: [LIMIT_IP] Email = X || Disconnecting OLD IP = Y || Timestamp = Z
for _, ipTime := range disconnectedIps { for _, ipTime := range bannedIps {
j.disAllowedIps = append(j.disAllowedIps, ipTime.IP) j.disAllowedIps = append(j.disAllowedIps, ipTime.IP)
log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp) log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp)
} }
// Actually disconnect old IPs by temporarily removing and re-adding user // Update database with only the currently active (kept) IPs
// This forces Xray to drop existing connections from old IPs
if len(disconnectedIps) > 0 {
j.disconnectClientTemporarily(inbound, clientEmail, clients)
}
// Update database with only the newest IPs
jsonIps, _ := json.Marshal(keptIps) jsonIps, _ := json.Marshal(keptIps)
inboundClientIps.Ips = string(jsonIps) inboundClientIps.Ips = string(jsonIps)
} else { } else {
@@ -378,67 +372,12 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
} }
if len(j.disAllowedIps) > 0 { if len(j.disAllowedIps) > 0 {
logger.Infof("[LIMIT_IP] Client %s: Kept %d newest IPs, disconnected %d old IPs", clientEmail, limitIp, len(j.disAllowedIps)) logger.Infof("[LIMIT_IP] Client %s: Kept %d current IPs, queued %d new IPs for fail2ban", clientEmail, limitIp, len(j.disAllowedIps))
} }
return shouldCleanLog return shouldCleanLog
} }
// disconnectClientTemporarily removes and re-adds a client to force disconnect old connections
func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, clientEmail string, clients []model.Client) {
var xrayAPI xray.XrayAPI
// Get panel settings for API port
db := database.GetDB()
var apiPort int
var apiPortSetting model.Setting
if err := db.Where("key = ?", "xrayApiPort").First(&apiPortSetting).Error; err == nil {
apiPort, _ = strconv.Atoi(apiPortSetting.Value)
}
if apiPort == 0 {
apiPort = 10085 // Default API port
}
err := xrayAPI.Init(apiPort)
if err != nil {
logger.Warningf("[LIMIT_IP] Failed to init Xray API for disconnection: %v", err)
return
}
defer xrayAPI.Close()
// Find the client config
var clientConfig map[string]any
for _, client := range clients {
if client.Email == clientEmail {
// Convert client to map for API
clientBytes, _ := json.Marshal(client)
json.Unmarshal(clientBytes, &clientConfig)
break
}
}
if clientConfig == nil {
return
}
// Remove user to disconnect all connections
err = xrayAPI.RemoveUser(inbound.Tag, clientEmail)
if err != nil {
logger.Warningf("[LIMIT_IP] Failed to remove user %s: %v", clientEmail, err)
return
}
// Wait a moment for disconnection to take effect
time.Sleep(100 * time.Millisecond)
// Re-add user to allow new connections
err = xrayAPI.AddUser(string(inbound.Protocol), inbound.Tag, clientConfig)
if err != nil {
logger.Warningf("[LIMIT_IP] Failed to re-add user %s: %v", clientEmail, err)
}
}
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) { func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
inbound := &model.Inbound{} inbound := &model.Inbound{}

View File

@@ -271,10 +271,7 @@ func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[s
// Delete in batches // Delete in batches
for i := 0; i < len(toDelete); i += batchSize { for i := 0; i < len(toDelete); i += batchSize {
end := i + batchSize end := min(i+batchSize, len(toDelete))
if end > len(toDelete) {
end = len(toDelete)
}
batch := toDelete[i:end] batch := toDelete[i:end]
for _, c := range batch { for _, c := range batch {

View File

@@ -37,7 +37,7 @@ type SettingService interface {
// InitLocalizer initializes the internationalization system with embedded translation files. // InitLocalizer initializes the internationalization system with embedded translation files.
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
// set default bundle to english // set default bundle to English
i18nBundle = i18n.NewBundle(language.MustParse("en-US")) i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)

View File

@@ -2032,7 +2032,6 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
return nil, err return nil, err
} }
if t != nil && client != nil { if t != nil && client != nil {
t.Enable = client.Enable
t.UUID = client.ID t.UUID = client.ID
t.SubId = client.SubID t.SubId = client.SubID
return t, nil return t, nil
@@ -2141,6 +2140,43 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
if err != nil { if err != nil {
return "", err return "", err
} }
if InboundClientIps.Ips == "" {
return "", nil
}
// Try to parse as new format (with timestamps)
type IPWithTimestamp struct {
IP string `json:"ip"`
Timestamp int64 `json:"timestamp"`
}
var ipsWithTime []IPWithTimestamp
err = json.Unmarshal([]byte(InboundClientIps.Ips), &ipsWithTime)
// If successfully parsed as new format, return with timestamps
if err == nil && len(ipsWithTime) > 0 {
return InboundClientIps.Ips, nil
}
// Otherwise, assume it's old format (simple string array)
// Try to parse as simple array and convert to new format
var oldIps []string
err = json.Unmarshal([]byte(InboundClientIps.Ips), &oldIps)
if err == nil && len(oldIps) > 0 {
// Convert old format to new format with current timestamp
newIpsWithTime := make([]IPWithTimestamp, len(oldIps))
for i, ip := range oldIps {
newIpsWithTime[i] = IPWithTimestamp{
IP: ip,
Timestamp: time.Now().Unix(),
}
}
result, _ := json.Marshal(newIpsWithTime)
return string(result), nil
}
// Return as-is if parsing fails
return InboundClientIps.Ips, nil return InboundClientIps.Ips, nil
} }

View File

@@ -5,7 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@@ -108,7 +108,7 @@ var defaultValueMap = map[string]string{
// It handles configuration storage, retrieval, and validation for all system settings. // It handles configuration storage, retrieval, and validation for all system settings.
type SettingService struct{} type SettingService struct{}
func (s *SettingService) GetDefaultJsonConfig() (any, error) { func (s *SettingService) GetDefaultJSONConfig() (any, error) {
var jsonData any var jsonData any
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData) err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
if err != nil { if err != nil {
@@ -125,7 +125,7 @@ func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
return nil, err return nil, err
} }
allSetting := &entity.AllSetting{} allSetting := &entity.AllSetting{}
t := reflect.TypeOf(allSetting).Elem() t := reflect.TypeFor[entity.AllSetting]()
v := reflect.ValueOf(allSetting).Elem() v := reflect.ValueOf(allSetting).Elem()
fields := reflect_util.GetFields(t) fields := reflect_util.GetFields(t)
@@ -607,7 +607,7 @@ func (s *SettingService) GetIpLimitEnable() (bool, error) {
return (accessLogPath != "none" && accessLogPath != ""), nil return (accessLogPath != "none" && accessLogPath != ""), nil
} }
// LDAP exported getters // GetLdapEnable returns whether LDAP is enabled.
func (s *SettingService) GetLdapEnable() (bool, error) { func (s *SettingService) GetLdapEnable() (bool, error) {
return s.getBool("ldapEnable") return s.getBool("ldapEnable")
} }
@@ -694,7 +694,7 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
} }
v := reflect.ValueOf(allSetting).Elem() v := reflect.ValueOf(allSetting).Elem()
t := reflect.TypeOf(allSetting).Elem() t := reflect.TypeFor[entity.AllSetting]()
fields := reflect_util.GetFields(t) fields := reflect_util.GetFields(t)
errs := make([]error, 0) errs := make([]error, 0)
for _, field := range fields { for _, field := range fields {
@@ -719,25 +719,25 @@ func (s *SettingService) GetDefaultXrayConfig() (any, error) {
} }
func extractHostname(host string) string { func extractHostname(host string) string {
h, _, err := net.SplitHostPort(host) h, _, err := net.SplitHostPort(host)
// Err is not nil means host does not contain port // Err is not nil means host does not contain port
if err != nil { if err != nil {
h = host h = host
} }
ip := net.ParseIP(h) ip := net.ParseIP(h)
// If it's not an IP, return as is // If it's not an IP, return as is
if ip == nil { if ip == nil {
return h return h
} }
// If it's an IPv4, return as is // If it's an IPv4, return as is
if ip.To4() != nil { if ip.To4() != nil {
return h return h
} }
// IPv6 needs bracketing // IPv6 needs bracketing
return "[" + h + "]" return "[" + h + "]"
} }
func (s *SettingService) GetDefaultSettings(host string) (any, error) { func (s *SettingService) GetDefaultSettings(host string) (any, error) {

View File

@@ -5,8 +5,10 @@ import (
"crypto/rand" "crypto/rand"
"embed" "embed"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"html"
"io" "io"
"math/big" "math/big"
"net" "net"
@@ -14,6 +16,7 @@ import (
"net/url" "net/url"
"os" "os"
"regexp" "regexp"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -271,41 +274,78 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
return nil return nil
} }
// createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling
func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client {
client := &fasthttp.Client{
// Connection timeouts
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
MaxIdleConnDuration: 60 * time.Second,
MaxConnDuration: 0, // unlimited, but controlled by MaxIdleConnDuration
MaxIdemponentCallAttempts: 3,
ReadBufferSize: 4096,
WriteBufferSize: 4096,
MaxConnsPerHost: 100,
MaxConnWaitTimeout: 10 * time.Second,
DisableHeaderNamesNormalizing: false,
DisablePathNormalizing: false,
// Retry on connection errors
RetryIf: func(request *fasthttp.Request) bool {
// Retry on connection errors for GET requests
return string(request.Header.Method()) == "GET" || string(request.Header.Method()) == "POST"
},
}
// Set proxy if provided
if proxyUrl != "" {
client.Dial = fasthttpproxy.FasthttpSocksDialer(proxyUrl)
}
return client
}
// NewBot creates a new Telegram bot instance with optional proxy and API server settings. // NewBot creates a new Telegram bot instance with optional proxy and API server settings.
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) { func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
if proxyUrl == "" && apiServerUrl == "" { // Validate proxy URL if provided
return telego.NewBot(token)
}
if proxyUrl != "" { if proxyUrl != "" {
if !strings.HasPrefix(proxyUrl, "socks5://") { if !strings.HasPrefix(proxyUrl, "socks5://") {
logger.Warning("Invalid socks5 URL, using default") logger.Warning("Invalid socks5 URL, ignoring proxy")
return telego.NewBot(token) proxyUrl = "" // Clear invalid proxy
} else {
_, err := url.Parse(proxyUrl)
if err != nil {
logger.Warningf("Can't parse proxy URL, ignoring proxy: %v", err)
proxyUrl = ""
}
} }
}
_, err := url.Parse(proxyUrl) // Validate API server URL if provided
if err != nil { if apiServerUrl != "" {
logger.Warningf("Can't parse proxy URL, using default instance for tgbot: %v", err) if !strings.HasPrefix(apiServerUrl, "http") {
return telego.NewBot(token) logger.Warning("Invalid http(s) URL for API server, using default")
apiServerUrl = ""
} else {
_, err := url.Parse(apiServerUrl)
if err != nil {
logger.Warningf("Can't parse API server URL, using default: %v", err)
apiServerUrl = ""
}
} }
return telego.NewBot(token, telego.WithFastHTTPClient(&fasthttp.Client{
Dial: fasthttpproxy.FasthttpSocksDialer(proxyUrl),
}))
} }
if !strings.HasPrefix(apiServerUrl, "http") { // Create robust fasthttp client
logger.Warning("Invalid http(s) URL, using default") client := t.createRobustFastHTTPClient(proxyUrl)
return telego.NewBot(token)
// Build bot options
var options []telego.BotOption
options = append(options, telego.WithFastHTTPClient(client))
if apiServerUrl != "" {
options = append(options, telego.WithAPIServer(apiServerUrl))
} }
_, err := url.Parse(apiServerUrl) return telego.NewBot(token, options...)
if err != nil {
logger.Warningf("Can't parse API server URL, using default instance for tgbot: %v", err)
return telego.NewBot(token)
}
return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
} }
// IsRunning checks if the Telegram bot is currently running. // IsRunning checks if the Telegram bot is currently running.
@@ -389,7 +429,7 @@ func (t *Tgbot) decodeQuery(query string) (string, error) {
// OnReceive starts the message receiving loop for the Telegram bot. // OnReceive starts the message receiving loop for the Telegram bot.
func (t *Tgbot) OnReceive() { func (t *Tgbot) OnReceive() {
params := telego.GetUpdatesParams{ params := telego.GetUpdatesParams{
Timeout: 30, // Increased timeout to reduce API calls Timeout: 20, // Reduced timeout to detect connection issues faster
} }
// Strict singleton: never start a second long-polling loop. // Strict singleton: never start a second long-polling loop.
tgBotMutex.Lock() tgBotMutex.Lock()
@@ -407,7 +447,7 @@ func (t *Tgbot) OnReceive() {
botWG.Add(1) botWG.Add(1)
tgBotMutex.Unlock() tgBotMutex.Unlock()
// Get updates channel using the context. // Get updates channel using the context with shorter timeout for better error recovery
updates, _ := bot.UpdatesViaLongPolling(ctx, &params) updates, _ := bot.UpdatesViaLongPolling(ctx, &params)
go func() { go func() {
defer botWG.Done() defer botWG.Done()
@@ -613,7 +653,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
msg += t.I18nBot("tgbot.commands.help") msg += t.I18nBot("tgbot.commands.help")
msg += t.I18nBot("tgbot.commands.pleaseChoose") msg += t.I18nBot("tgbot.commands.pleaseChoose")
case "start": case "start":
msg += t.I18nBot("tgbot.commands.start", "Firstname=="+message.From.FirstName) msg += t.I18nBot("tgbot.commands.start", "Firstname=="+html.EscapeString(message.From.FirstName))
if isAdmin { if isAdmin {
msg += t.I18nBot("tgbot.commands.welcome", "Hostname=="+hostname) msg += t.I18nBot("tgbot.commands.welcome", "Hostname=="+hostname)
} }
@@ -1886,6 +1926,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} else { } else {
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
t.sendClientIndividualLinks(chatId, client_Email)
t.sendClientQRLinks(chatId, client_Email)
} }
case "add_client_submit_enable": case "add_client_submit_enable":
client_Enable = true client_Enable = true
@@ -1896,6 +1938,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} else { } else {
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove()) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.successfulOperation"), tu.ReplyKeyboardRemove())
t.sendClientIndividualLinks(chatId, client_Email)
t.sendClientQRLinks(chatId, client_Email)
} }
case "reset_all_traffics_cancel": case "reset_all_traffics_cancel":
t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID()) t.deleteMessageTgBot(chatId, callbackQuery.Message.GetMessageID())
@@ -2246,10 +2290,36 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
if len(replyMarkup) > 0 && n == (len(allMessages)-1) { if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
params.ReplyMarkup = replyMarkup[0] params.ReplyMarkup = replyMarkup[0]
} }
_, err := bot.SendMessage(context.Background(), &params)
if err != nil { // Retry logic with exponential backoff for connection errors
logger.Warning("Error sending telegram message :", err) maxRetries := 3
for attempt := range maxRetries {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
_, err := bot.SendMessage(ctx, &params)
cancel()
if err == nil {
break // Success
}
// Check if error is a connection error
errStr := err.Error()
isConnectionError := strings.Contains(errStr, "connection") ||
strings.Contains(errStr, "timeout") ||
strings.Contains(errStr, "closed")
if isConnectionError && attempt < maxRetries-1 {
// Exponential backoff: 1s, 2s, 4s
backoff := time.Duration(1<<uint(attempt)) * time.Second
logger.Warningf("Connection error sending telegram message (attempt %d/%d), retrying in %v: %v",
attempt+1, maxRetries, backoff, err)
time.Sleep(backoff)
} else {
logger.Warning("Error sending telegram message:", err)
break
}
} }
// Reduced delay to improve performance (only needed for rate limiting) // Reduced delay to improve performance (only needed for rate limiting)
if n < len(allMessages)-1 { // Only delay between messages, not after the last one if n < len(allMessages)-1 { // Only delay between messages, not after the last one
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@@ -2584,8 +2654,12 @@ func (t *Tgbot) SendBackupToAdmins() {
if !t.IsRunning() { if !t.IsRunning() {
return return
} }
for _, adminId := range adminIds { for i, adminId := range adminIds {
t.sendBackup(int64(adminId)) t.sendBackup(int64(adminId))
// Add delay between sends to avoid Telegram rate limits
if i < len(adminIds)-1 {
time.Sleep(1 * time.Second)
}
} }
} }
@@ -2650,7 +2724,7 @@ func (t *Tgbot) prepareServerUsageInfo() string {
info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown")) info += t.I18nBot("tgbot.messages.ip", "IP=="+t.I18nBot("tgbot.unknown"))
info += "\r\n" info += "\r\n"
} else { } else {
for i := 0; i < len(netInterfaces); i++ { for i := range netInterfaces {
if (netInterfaces[i].Flags & net.FlagUp) != 0 { if (netInterfaces[i].Flags & net.FlagUp) != 0 {
addrs, _ := netInterfaces[i].Addrs() addrs, _ := netInterfaces[i].Addrs()
@@ -2719,29 +2793,29 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
// getInboundUsages retrieves and formats inbound usage information. // getInboundUsages retrieves and formats inbound usage information.
func (t *Tgbot) getInboundUsages() string { func (t *Tgbot) getInboundUsages() string {
info := "" var info strings.Builder
// get traffic // get traffic
inbounds, err := t.inboundService.GetAllInbounds() inbounds, err := t.inboundService.GetAllInbounds()
if err != nil { if err != nil {
logger.Warning("GetAllInbounds run failed:", err) logger.Warning("GetAllInbounds run failed:", err)
info += t.I18nBot("tgbot.answers.getInboundsFailed") info.WriteString(t.I18nBot("tgbot.answers.getInboundsFailed"))
} else { } else {
// NOTE:If there no any sessions here,need to notify here // NOTE:If there no any sessions here,need to notify here
// TODO:Sub-node push, automatic conversion format // TODO:Sub-node push, automatic conversion format
for _, inbound := range inbounds { for _, inbound := range inbounds {
info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark) info.WriteString(t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark))
info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)) info.WriteString(t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)))
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)) info.WriteString(t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)))
if inbound.ExpiryTime == 0 { if inbound.ExpiryTime == 0 {
info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")) info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")))
} else { } else {
info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")) info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")))
} }
info += "\r\n" info.WriteString("\r\n")
} }
} }
return info return info.String()
} }
// getInbounds creates an inline keyboard with all inbounds. // getInbounds creates an inline keyboard with all inbounds.
@@ -2991,12 +3065,9 @@ func (t *Tgbot) clientInfoMsg(
status := t.I18nBot("tgbot.offline") status := t.I18nBot("tgbot.offline")
isOnline := false isOnline := false
if p.IsRunning() { if p.IsRunning() {
for _, online := range p.GetOnlineClients() { if slices.Contains(p.GetOnlineClients(), traffic.Email) {
if online == traffic.Email { status = t.I18nBot("tgbot.online")
status = t.I18nBot("tgbot.online") isOnline = true
isOnline = true
break
}
} }
} }
@@ -3083,9 +3154,41 @@ func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
ips = t.I18nBot("tgbot.noIpRecord") ips = t.I18nBot("tgbot.noIpRecord")
} }
formattedIps := ips
if err == nil && len(ips) > 0 {
type ipWithTimestamp struct {
IP string `json:"ip"`
Timestamp int64 `json:"timestamp"`
}
var ipsWithTime []ipWithTimestamp
if json.Unmarshal([]byte(ips), &ipsWithTime) == nil && len(ipsWithTime) > 0 {
lines := make([]string, 0, len(ipsWithTime))
for _, item := range ipsWithTime {
if item.IP == "" {
continue
}
if item.Timestamp > 0 {
ts := time.Unix(item.Timestamp, 0).Format("2006-01-02 15:04:05")
lines = append(lines, fmt.Sprintf("%s (%s)", item.IP, ts))
continue
}
lines = append(lines, item.IP)
}
if len(lines) > 0 {
formattedIps = strings.Join(lines, "\n")
}
} else {
var oldIps []string
if json.Unmarshal([]byte(ips), &oldIps) == nil && len(oldIps) > 0 {
formattedIps = strings.Join(oldIps, "\n")
}
}
}
output := "" output := ""
output += t.I18nBot("tgbot.messages.email", "Email=="+email) output += t.I18nBot("tgbot.messages.email", "Email=="+email)
output += t.I18nBot("tgbot.messages.ips", "IPs=="+ips) output += t.I18nBot("tgbot.messages.ips", "IPs=="+formattedIps)
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
@@ -3203,6 +3306,27 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
} }
} }
// getCommonClientButtons returns the shared inline keyboard rows for client configuration
func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton {
return [][]telego.InlineKeyboardButton{
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
),
}
}
// addClient handles the process of adding a new client to an inbound. // addClient handles the process of adding a new client to an inbound.
func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) { func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
@@ -3213,91 +3337,40 @@ func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
protocol := inbound.Protocol protocol := inbound.Protocol
var protocolRows [][]telego.InlineKeyboardButton
switch protocol { switch protocol {
case model.VMESS, model.VLESS: case model.VMESS, model.VLESS:
inlineKeyboard := tu.InlineKeyboard( protocolRows = [][]telego.InlineKeyboardButton{
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_id")).WithCallbackData("add_client_ch_default_id"),
), ),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
),
)
if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
} }
case model.Trojan: case model.Trojan:
inlineKeyboard := tu.InlineKeyboard( protocolRows = [][]telego.InlineKeyboardButton{
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_tr"),
), ),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
),
)
if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
} }
case model.Shadowsocks: case model.Shadowsocks:
inlineKeyboard := tu.InlineKeyboard( protocolRows = [][]telego.InlineKeyboardButton{
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_sh"), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_password")).WithCallbackData("add_client_ch_default_pass_sh"),
), ),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
tu.InlineKeyboardButton("ip limit").WithCallbackData("add_client_ch_default_ip_limit"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
),
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
),
)
if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
} }
} }
commonRows := t.getCommonClientButtons()
inlineKeyboard := tu.InlineKeyboard(append(protocolRows, commonRows...)...)
if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
}
} }
// searchInbound searches for inbounds by remark and sends the results. // searchInbound searches for inbounds by remark and sends the results.
@@ -3329,11 +3402,11 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
t.SendMsgToTgbot(chatId, info) t.SendMsgToTgbot(chatId, info)
if len(inbound.ClientStats) > 0 { if len(inbound.ClientStats) > 0 {
output := "" var output strings.Builder
for _, traffic := range inbound.ClientStats { for _, traffic := range inbound.ClientStats {
output += t.clientInfoMsg(&traffic, true, true, true, true, true, true) output.WriteString(t.clientInfoMsg(&traffic, true, true, true, true, true, true))
} }
t.SendMsgToTgbot(chatId, output) t.SendMsgToTgbot(chatId, output.String())
} }
} }
} }
@@ -3563,13 +3636,17 @@ func (t *Tgbot) sendBackup(chatId int64) {
logger.Error("Error in trigger a checkpoint operation: ", err) logger.Error("Error in trigger a checkpoint operation: ", err)
} }
// Send database backup
file, err := os.Open(config.GetDBPath()) file, err := os.Open(config.GetDBPath())
if err == nil { if err == nil {
defer file.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
document := tu.Document( document := tu.Document(
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(context.Background(), document) _, err = bot.SendDocument(ctx, document)
if err != nil { if err != nil {
logger.Error("Error in uploading backup: ", err) logger.Error("Error in uploading backup: ", err)
} }
@@ -3577,13 +3654,20 @@ func (t *Tgbot) sendBackup(chatId int64) {
logger.Error("Error in opening db file for backup: ", err) logger.Error("Error in opening db file for backup: ", err)
} }
// Small delay between file sends
time.Sleep(500 * time.Millisecond)
// Send config.json backup
file, err = os.Open(xray.GetConfigPath()) file, err = os.Open(xray.GetConfigPath())
if err == nil { if err == nil {
defer file.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
document := tu.Document( document := tu.Document(
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(context.Background(), document) _, err = bot.SendDocument(ctx, document)
if err != nil { if err != nil {
logger.Error("Error in uploading config.json: ", err) logger.Error("Error in uploading config.json: ", err)
} }

View File

@@ -33,7 +33,7 @@ func (s *UserService) GetFirstUser() (*model.User, error) {
return user, nil return user, nil
} }
func (s *UserService) CheckUser(username string, password string, twoFactorCode string) *model.User { func (s *UserService) CheckUser(username string, password string, twoFactorCode string) (*model.User, error) {
db := database.GetDB() db := database.GetDB()
user := &model.User{} user := &model.User{}
@@ -43,17 +43,16 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
First(user). First(user).
Error Error
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
return nil return nil, errors.New("invalid credentials")
} else if err != nil { } else if err != nil {
logger.Warning("check user err:", err) logger.Warning("check user err:", err)
return nil return nil, err
} }
// If LDAP enabled and local password check fails, attempt LDAP auth
if !crypto.CheckPasswordHash(user.Password, password) { if !crypto.CheckPasswordHash(user.Password, password) {
ldapEnabled, _ := s.settingService.GetLdapEnable() ldapEnabled, _ := s.settingService.GetLdapEnable()
if !ldapEnabled { if !ldapEnabled {
return nil return nil, errors.New("invalid credentials")
} }
host, _ := s.settingService.GetLdapHost() host, _ := s.settingService.GetLdapHost()
@@ -77,15 +76,14 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
} }
ok, err := ldaputil.AuthenticateUser(cfg, username, password) ok, err := ldaputil.AuthenticateUser(cfg, username, password)
if err != nil || !ok { if err != nil || !ok {
return nil return nil, errors.New("invalid credentials")
} }
// On successful LDAP auth, continue 2FA checks below
} }
twoFactorEnable, err := s.settingService.GetTwoFactorEnable() twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
if err != nil { if err != nil {
logger.Warning("check two factor err:", err) logger.Warning("check two factor err:", err)
return nil return nil, err
} }
if twoFactorEnable { if twoFactorEnable {
@@ -93,15 +91,15 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
if err != nil { if err != nil {
logger.Warning("check two factor token err:", err) logger.Warning("check two factor token err:", err)
return nil return nil, err
} }
if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode { if gotp.NewDefaultTOTP(twoFactorToken).Now() != twoFactorCode {
return nil return nil, errors.New("invalid 2fa code")
} }
} }
return user return user, nil
} }
func (s *UserService) UpdateUser(id int, username string, password string) error { func (s *UserService) UpdateUser(id int, username string, password string) error {

View File

@@ -525,6 +525,12 @@
"accountInfo" = "معلومات الحساب" "accountInfo" = "معلومات الحساب"
"outboundStatus" = "حالة المخرج" "outboundStatus" = "حالة المخرج"
"sendThrough" = "أرسل من خلال" "sendThrough" = "أرسل من خلال"
"test" = "اختبار"
"testResult" = "نتيجة الاختبار"
"testing" = "جاري اختبار الاتصال..."
"testSuccess" = "الاختبار ناجح"
"testFailed" = "فشل الاختبار"
"testError" = "فشل اختبار المخرج"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "أضف موازن تحميل" "addBalancer" = "أضف موازن تحميل"
@@ -657,6 +663,7 @@
"userSaved" = "✅ حفظت بيانات مستخدم Telegram." "userSaved" = "✅ حفظت بيانات مستخدم Telegram."
"loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n" "loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n"
"loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n" "loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n"
"2faFailed" = "فشل 2FA"
"report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n" "report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n"
"datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n" "datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n"
"hostname" = "💻 السيرفر: {{ .Hostname }}\r\n" "hostname" = "💻 السيرفر: {{ .Hostname }}\r\n"

View File

@@ -663,6 +663,7 @@
"userSaved" = "✅ Telegram User saved." "userSaved" = "✅ Telegram User saved."
"loginSuccess" = "✅ Logged in to the panel successfully.\r\n" "loginSuccess" = "✅ Logged in to the panel successfully.\r\n"
"loginFailed" = "❗Login attempt to the panel failed.\r\n" "loginFailed" = "❗Login attempt to the panel failed.\r\n"
"2faFailed" = "2FA Failed"
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n" "report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
"datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n" "datetime" = "⏰ Date&Time: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n"

View File

@@ -525,6 +525,12 @@
"accountInfo" = "Información de la Cuenta" "accountInfo" = "Información de la Cuenta"
"outboundStatus" = "Estado de Salida" "outboundStatus" = "Estado de Salida"
"sendThrough" = "Enviar a través de" "sendThrough" = "Enviar a través de"
"test" = "Probar"
"testResult" = "Resultado de la prueba"
"testing" = "Probando conexión..."
"testSuccess" = "Prueba exitosa"
"testFailed" = "Prueba fallida"
"testError" = "Error al probar la salida"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Agregar equilibrador" "addBalancer" = "Agregar equilibrador"
@@ -657,6 +663,7 @@
"userSaved" = "✅ Usuario de Telegram guardado." "userSaved" = "✅ Usuario de Telegram guardado."
"loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n" "loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n"
"loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n" "loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n"
"2faFailed" = "Error de 2FA"
"report" = "🕰 Informes programados: {{ .RunTime }}\r\n" "report" = "🕰 Informes programados: {{ .RunTime }}\r\n"
"datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n" "datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n"
"hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n" "hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n"

View File

@@ -525,6 +525,12 @@
"accountInfo" = "اطلاعات حساب" "accountInfo" = "اطلاعات حساب"
"outboundStatus" = "وضعیت خروجی" "outboundStatus" = "وضعیت خروجی"
"sendThrough" = "ارسال با" "sendThrough" = "ارسال با"
"test" = "تست"
"testResult" = "نتیجه تست"
"testing" = "در حال تست اتصال..."
"testSuccess" = "تست موفقیت‌آمیز"
"testFailed" = "تست ناموفق"
"testError" = "خطا در تست خروجی"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "افزودن بالانسر" "addBalancer" = "افزودن بالانسر"
@@ -657,6 +663,7 @@
"userSaved" = "✅ کاربر تلگرام ذخیره شد." "userSaved" = "✅ کاربر تلگرام ذخیره شد."
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n" "loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
"loginFailed" = "❗️ ورود به پنل ناموفق‌بود \r\n" "loginFailed" = "❗️ ورود به پنل ناموفق‌بود \r\n"
"2faFailed" = "خطای 2FA"
"report" = "🕰 گزارشات‌زمان‌بندی‌شده: {{ .RunTime }}\r\n" "report" = "🕰 گزارشات‌زمان‌بندی‌شده: {{ .RunTime }}\r\n"
"datetime" = "⏰ تاریخ‌وزمان: {{ .DateTime }}\r\n" "datetime" = "⏰ تاریخ‌وزمان: {{ .DateTime }}\r\n"
"hostname" = "💻 نام‌میزبان: {{ .Hostname }}\r\n" "hostname" = "💻 نام‌میزبان: {{ .Hostname }}\r\n"

View File

@@ -525,6 +525,12 @@
"accountInfo" = "Informasi Akun" "accountInfo" = "Informasi Akun"
"outboundStatus" = "Status Keluar" "outboundStatus" = "Status Keluar"
"sendThrough" = "Kirim Melalui" "sendThrough" = "Kirim Melalui"
"test" = "Tes"
"testResult" = "Hasil Tes"
"testing" = "Menguji koneksi..."
"testSuccess" = "Tes berhasil"
"testFailed" = "Tes gagal"
"testError" = "Gagal menguji outbound"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Tambahkan Penyeimbang" "addBalancer" = "Tambahkan Penyeimbang"
@@ -657,6 +663,7 @@
"userSaved" = "✅ Pengguna Telegram tersimpan." "userSaved" = "✅ Pengguna Telegram tersimpan."
"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n" "loginSuccess" = "✅ Berhasil masuk ke panel.\r\n"
"loginFailed" = "❗️ Gagal masuk ke panel.\r\n" "loginFailed" = "❗️ Gagal masuk ke panel.\r\n"
"2faFailed" = "2FA Gagal"
"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n" "report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n"
"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n" "datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n"

View File

@@ -525,6 +525,12 @@
"accountInfo" = "アカウント情報" "accountInfo" = "アカウント情報"
"outboundStatus" = "アウトバウンドステータス" "outboundStatus" = "アウトバウンドステータス"
"sendThrough" = "送信経路" "sendThrough" = "送信経路"
"test" = "テスト"
"testResult" = "テスト結果"
"testing" = "接続をテスト中..."
"testSuccess" = "テスト成功"
"testFailed" = "テスト失敗"
"testError" = "アウトバウンドのテストに失敗しました"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "負荷分散追加" "addBalancer" = "負荷分散追加"
@@ -657,6 +663,7 @@
"userSaved" = "✅ Telegramユーザーが保存されました。" "userSaved" = "✅ Telegramユーザーが保存されました。"
"loginSuccess" = "✅ パネルに正常にログインしました。\r\n" "loginSuccess" = "✅ パネルに正常にログインしました。\r\n"
"loginFailed" = "❗️ パネルのログインに失敗しました。\r\n" "loginFailed" = "❗️ パネルのログインに失敗しました。\r\n"
"2faFailed" = "2FAエラー"
"report" = "🕰 定期報告:{{ .RunTime }}\r\n" "report" = "🕰 定期報告:{{ .RunTime }}\r\n"
"datetime" = "⏰ 日時:{{ .DateTime }}\r\n" "datetime" = "⏰ 日時:{{ .DateTime }}\r\n"
"hostname" = "💻 ホスト名:{{ .Hostname }}\r\n" "hostname" = "💻 ホスト名:{{ .Hostname }}\r\n"

View File

@@ -525,6 +525,12 @@
"accountInfo" = "Informações da Conta" "accountInfo" = "Informações da Conta"
"outboundStatus" = "Status de Saída" "outboundStatus" = "Status de Saída"
"sendThrough" = "Enviar Através de" "sendThrough" = "Enviar Através de"
"test" = "Testar"
"testResult" = "Resultado do teste"
"testing" = "Testando conexão..."
"testSuccess" = "Teste bem-sucedido"
"testFailed" = "Teste falhou"
"testError" = "Falha ao testar saída"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Adicionar Balanceador" "addBalancer" = "Adicionar Balanceador"
@@ -657,6 +663,7 @@
"userSaved" = "✅ Usuário do Telegram salvo." "userSaved" = "✅ Usuário do Telegram salvo."
"loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n" "loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n"
"loginFailed" = "❗Tentativa de login no painel falhou.\r\n" "loginFailed" = "❗Tentativa de login no painel falhou.\r\n"
"2faFailed" = "Falha no 2FA"
"report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n" "report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n"
"datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n" "datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n" "hostname" = "💻 Host: {{ .Hostname }}\r\n"

View File

@@ -149,7 +149,7 @@
"geofileUpdateDialogDesc" = "Это обновит файл #filename#." "geofileUpdateDialogDesc" = "Это обновит файл #filename#."
"geofilesUpdateDialogDesc" = "Это обновит все геофайлы." "geofilesUpdateDialogDesc" = "Это обновит все геофайлы."
"geofilesUpdateAll" = "Обновить все" "geofilesUpdateAll" = "Обновить все"
"geofileUpdatePopover" = "Геофайл успешно обновлён" "geofileUpdatePopover" = "Геофайлы успешно обновлены"
"dontRefresh" = "Установка в процессе. Не обновляйте страницу" "dontRefresh" = "Установка в процессе. Не обновляйте страницу"
"logs" = "Журнал" "logs" = "Журнал"
"config" = "Конфигурация" "config" = "Конфигурация"
@@ -525,6 +525,12 @@
"accountInfo" = "Информация об учетной записи" "accountInfo" = "Информация об учетной записи"
"outboundStatus" = "Статус исходящего подключения" "outboundStatus" = "Статус исходящего подключения"
"sendThrough" = "Отправить через" "sendThrough" = "Отправить через"
"test" = "Тест"
"testResult" = "Результат теста"
"testing" = "Тестирование соединения..."
"testSuccess" = "Тест успешен"
"testFailed" = "Тест не пройден"
"testError" = "Не удалось протестировать исходящее подключение"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Создать балансировщик" "addBalancer" = "Создать балансировщик"
@@ -657,6 +663,7 @@
"userSaved" = "✅ Пользователь Telegram сохранен." "userSaved" = "✅ Пользователь Telegram сохранен."
"loginSuccess" = "✅ Успешный вход в панель.\r\n" "loginSuccess" = "✅ Успешный вход в панель.\r\n"
"loginFailed" = "❗️ Ошибка входа в панель.\r\n" "loginFailed" = "❗️ Ошибка входа в панель.\r\n"
"2faFailed" = "Ошибка 2FA"
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n" "report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n" "datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n" "hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"

View File

@@ -525,6 +525,12 @@
"accountInfo" = "Hesap Bilgileri" "accountInfo" = "Hesap Bilgileri"
"outboundStatus" = "Giden Durumu" "outboundStatus" = "Giden Durumu"
"sendThrough" = "Üzerinden Gönder" "sendThrough" = "Üzerinden Gönder"
"test" = "Test"
"testResult" = "Test Sonucu"
"testing" = "Bağlantı test ediliyor..."
"testSuccess" = "Test başarılı"
"testFailed" = "Test başarısız"
"testError" = "Giden test edilemedi"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Dengeleyici Ekle" "addBalancer" = "Dengeleyici Ekle"
@@ -657,6 +663,7 @@
"userSaved" = "✅ Telegram Kullanıcısı kaydedildi." "userSaved" = "✅ Telegram Kullanıcısı kaydedildi."
"loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n" "loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n"
"loginFailed" = "❗Panele giriş denemesi başarısız oldu.\r\n" "loginFailed" = "❗Panele giriş denemesi başarısız oldu.\r\n"
"2faFailed" = "2FA Hatası"
"report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n" "report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n"
"datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n" "datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n"
"hostname" = "💻 Sunucu: {{ .Hostname }}\r\n" "hostname" = "💻 Sunucu: {{ .Hostname }}\r\n"

View File

@@ -525,6 +525,12 @@
"accountInfo" = "Інформація про обліковий запис" "accountInfo" = "Інформація про обліковий запис"
"outboundStatus" = "Статус виходу" "outboundStatus" = "Статус виходу"
"sendThrough" = "Надіслати через" "sendThrough" = "Надіслати через"
"test" = "Тест"
"testResult" = "Результат тесту"
"testing" = "Тестування з'єднання..."
"testSuccess" = "Тест успішний"
"testFailed" = "Тест не пройдено"
"testError" = "Не вдалося протестувати вихідне з'єднання"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Додати балансир" "addBalancer" = "Додати балансир"
@@ -657,6 +663,7 @@
"userSaved" = "✅ Користувача Telegram збережено." "userSaved" = "✅ Користувача Telegram збережено."
"loginSuccess" = "✅ Успішно ввійшли в панель\r\n" "loginSuccess" = "✅ Успішно ввійшли в панель\r\n"
"loginFailed" = "❗️ Помилка входу в панель.\r\n" "loginFailed" = "❗️ Помилка входу в панель.\r\n"
"2faFailed" = "Помилка 2FA"
"report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n" "report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n"
"datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n" "datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n"
"hostname" = "💻 Хост: {{ .Hostname }}\r\n" "hostname" = "💻 Хост: {{ .Hostname }}\r\n"

View File

@@ -525,6 +525,12 @@
"accountInfo" = "Thông tin tài khoản" "accountInfo" = "Thông tin tài khoản"
"outboundStatus" = "Trạng thái đầu ra" "outboundStatus" = "Trạng thái đầu ra"
"sendThrough" = "Gửi qua" "sendThrough" = "Gửi qua"
"test" = "Kiểm tra"
"testResult" = "Kết quả kiểm tra"
"testing" = "Đang kiểm tra kết nối..."
"testSuccess" = "Kiểm tra thành công"
"testFailed" = "Kiểm tra thất bại"
"testError" = "Không thể kiểm tra đầu ra"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Thêm cân bằng" "addBalancer" = "Thêm cân bằng"
@@ -657,6 +663,7 @@
"userSaved" = "✅ Người dùng Telegram đã được lưu." "userSaved" = "✅ Người dùng Telegram đã được lưu."
"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n" "loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n"
"loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n" "loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n"
"2faFailed" = "Lỗi 2FA"
"report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n" "report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n"
"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n" "datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n"
"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n" "hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n"

View File

@@ -525,6 +525,12 @@
"accountInfo" = "帐户信息" "accountInfo" = "帐户信息"
"outboundStatus" = "出站状态" "outboundStatus" = "出站状态"
"sendThrough" = "发送通过" "sendThrough" = "发送通过"
"test" = "测试"
"testResult" = "测试结果"
"testing" = "正在测试连接..."
"testSuccess" = "测试成功"
"testFailed" = "测试失败"
"testError" = "测试出站失败"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "添加负载均衡" "addBalancer" = "添加负载均衡"
@@ -657,6 +663,7 @@
"userSaved" = "✅ 电报用户已保存。" "userSaved" = "✅ 电报用户已保存。"
"loginSuccess" = "✅ 成功登录到面板。\r\n" "loginSuccess" = "✅ 成功登录到面板。\r\n"
"loginFailed" = "❗️ 面板登录失败。\r\n" "loginFailed" = "❗️ 面板登录失败。\r\n"
"2faFailed" = "2FA 失败"
"report" = "🕰 定时报告:{{ .RunTime }}\r\n" "report" = "🕰 定时报告:{{ .RunTime }}\r\n"
"datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n" "datetime" = "⏰ 日期时间:{{ .DateTime }}\r\n"
"hostname" = "💻 主机名:{{ .Hostname }}\r\n" "hostname" = "💻 主机名:{{ .Hostname }}\r\n"

View File

@@ -525,6 +525,12 @@
"accountInfo" = "帳戶資訊" "accountInfo" = "帳戶資訊"
"outboundStatus" = "出站狀態" "outboundStatus" = "出站狀態"
"sendThrough" = "傳送通過" "sendThrough" = "傳送通過"
"test" = "測試"
"testResult" = "測試結果"
"testing" = "正在測試連接..."
"testSuccess" = "測試成功"
"testFailed" = "測試失敗"
"testError" = "測試出站失敗"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "新增負載均衡" "addBalancer" = "新增負載均衡"
@@ -657,6 +663,7 @@
"userSaved" = "✅ 電報使用者已儲存。" "userSaved" = "✅ 電報使用者已儲存。"
"loginSuccess" = "✅ 成功登入到面板。\r\n" "loginSuccess" = "✅ 成功登入到面板。\r\n"
"loginFailed" = "❗️ 面板登入失敗。\r\n" "loginFailed" = "❗️ 面板登入失敗。\r\n"
"2faFailed" = "2FA 失敗"
"report" = "🕰 定時報告:{{ .RunTime }}\r\n" "report" = "🕰 定時報告:{{ .RunTime }}\r\n"
"datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n" "datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n"
"hostname" = "💻 主機名:{{ .Hostname }}\r\n" "hostname" = "💻 主機名:{{ .Hostname }}\r\n"

View File

@@ -200,7 +200,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/api/"}))) engine.Use(gzip.Gzip(gzip.DefaultCompression))
assetsBasePath := basePath + "assets/" assetsBasePath := basePath + "assets/"
store := cookie.NewStore(secret) store := cookie.NewStore(secret)
@@ -490,3 +490,7 @@ func (s *Server) GetCron() *cron.Cron {
func (s *Server) GetWSHub() any { func (s *Server) GetWSHub() any {
return s.wsHub return s.wsHub
} }
func (s *Server) RestartXray() error {
return s.xrayService.RestartXray(true)
}

View File

@@ -10,4 +10,9 @@ depend() {
} }
start_pre(){ start_pre(){
cd /usr/local/x-ui cd /usr/local/x-ui
}
reload() {
ebegin "Reloading ${RC_SVCNAME}"
kill -USR1 $pidfile
eend $?
} }

View File

@@ -9,6 +9,7 @@ Environment="XRAY_VMESS_AEAD_FORCED=false"
Type=simple Type=simple
WorkingDirectory=/usr/lib/x-ui/ WorkingDirectory=/usr/lib/x-ui/
ExecStart=/usr/lib/x-ui/x-ui ExecStart=/usr/lib/x-ui/x-ui
ExecReload=kill -USR1 $MAINPID
Restart=on-failure Restart=on-failure
RestartSec=5s RestartSec=5s

View File

@@ -9,6 +9,7 @@ Environment="XRAY_VMESS_AEAD_FORCED=false"
Type=simple Type=simple
WorkingDirectory=/usr/local/x-ui/ WorkingDirectory=/usr/local/x-ui/
ExecStart=/usr/local/x-ui/x-ui ExecStart=/usr/local/x-ui/x-ui
ExecReload=kill -USR1 $MAINPID
Restart=on-failure Restart=on-failure
RestartSec=5s RestartSec=5s

View File

@@ -9,6 +9,7 @@ Environment="XRAY_VMESS_AEAD_FORCED=false"
Type=simple Type=simple
WorkingDirectory=/usr/local/x-ui/ WorkingDirectory=/usr/local/x-ui/
ExecStart=/usr/local/x-ui/x-ui ExecStart=/usr/local/x-ui/x-ui
ExecReload=kill -USR1 $MAINPID
Restart=on-failure Restart=on-failure
RestartSec=5s RestartSec=5s

84
x-ui.sh
View File

@@ -317,12 +317,12 @@ check_config() {
start >/dev/null 2>&1 start >/dev/null 2>&1
else else
LOGE "IP certificate setup failed." LOGE "IP certificate setup failed."
echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}" echo -e "${yellow}You can try again via option 19 (SSL Certificate Management).${plain}"
start >/dev/null 2>&1 start >/dev/null 2>&1
fi fi
else else
echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}"
echo -e "${yellow}For security, please configure SSL certificate using option 18 (SSL Certificate Management)${plain}" echo -e "${yellow}For security, please configure SSL certificate using option 19 (SSL Certificate Management)${plain}"
fi fi
fi fi
} }
@@ -408,6 +408,16 @@ restart() {
fi fi
} }
restart_xray() {
systemctl reload x-ui
LOGI "xray-core Restart signal sent successfully, Please check the log information to confirm whether xray restarted successfully"
sleep 2
show_xray_status
if [[ $# == 0 ]]; then
before_show_menu
fi
}
status() { status() {
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
rc-service x-ui status rc-service x-ui status
@@ -421,7 +431,7 @@ status() {
enable() { enable() {
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
rc-update add x-ui rc-update add x-ui default
else else
systemctl enable x-ui systemctl enable x-ui
fi fi
@@ -2002,7 +2012,7 @@ EOF
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
[Definition] [Definition]
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR> failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*Disconnecting OLD IP\s*=\s*<ADDR>\s*\|\|\s*Timestamp\s*=\s*\d+
ignoreregex = ignoreregex =
EOF EOF
@@ -2062,11 +2072,15 @@ SSH_port_forwarding() {
) )
local server_ip="" local server_ip=""
for ip_address in "${URL_lists[@]}"; do for ip_address in "${URL_lists[@]}"; do
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
if [[ -n "${server_ip}" ]]; then local http_code=$(echo "$response" | tail -n1)
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
server_ip="${ip_result}"
break break
fi fi
done done
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}') local existing_listenIP=$(${xui_folder}/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')
@@ -2150,6 +2164,7 @@ show_usage() {
${blue}x-ui start${plain} - Start │ ${blue}x-ui start${plain} - Start │
${blue}x-ui stop${plain} - Stop │ ${blue}x-ui stop${plain} - Stop │
${blue}x-ui restart${plain} - Restart │ ${blue}x-ui restart${plain} - Restart │
| ${blue}x-ui restart-xray${plain} - Restart Xray │
${blue}x-ui status${plain} - Current Status │ ${blue}x-ui status${plain} - Current Status │
${blue}x-ui settings${plain} - Current Settings │ ${blue}x-ui settings${plain} - Current Settings │
${blue}x-ui enable${plain} - Enable Autostart on OS Startup │ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
@@ -2185,25 +2200,26 @@ show_menu() {
${green}11.${plain} Start │ ${green}11.${plain} Start │
${green}12.${plain} Stop │ ${green}12.${plain} Stop │
${green}13.${plain} Restart │ ${green}13.${plain} Restart │
${green}14.${plain} Check Status | ${green}14.${plain} Restart Xray
${green}15.${plain} Logs Management ${green}15.${plain} Check Status
${green}16.${plain} Logs Management │
│────────────────────────────────────────────────│ │────────────────────────────────────────────────│
${green}16.${plain} Enable Autostart │ ${green}17.${plain} Enable Autostart │
${green}17.${plain} Disable Autostart │ ${green}18.${plain} Disable Autostart │
│────────────────────────────────────────────────│ │────────────────────────────────────────────────│
${green}18.${plain} SSL Certificate Management │ ${green}19.${plain} SSL Certificate Management │
${green}19.${plain} Cloudflare SSL Certificate │ ${green}20.${plain} Cloudflare SSL Certificate │
${green}20.${plain} IP Limit Management │ ${green}21.${plain} IP Limit Management │
${green}21.${plain} Firewall Management │ ${green}22.${plain} Firewall Management │
${green}22.${plain} SSH Port Forwarding Management │ ${green}23.${plain} SSH Port Forwarding Management │
│────────────────────────────────────────────────│ │────────────────────────────────────────────────│
${green}23.${plain} Enable BBR │ ${green}24.${plain} Enable BBR │
${green}24.${plain} Update Geo Files │ ${green}25.${plain} Update Geo Files │
${green}25.${plain} Speedtest by Ookla │ ${green}26.${plain} Speedtest by Ookla │
╚────────────────────────────────────────────────╝ ╚────────────────────────────────────────────────╝
" "
show_status show_status
echo && read -rp "Please enter your selection [0-25]: " num echo && read -rp "Please enter your selection [0-26]: " num
case "${num}" in case "${num}" in
0) 0)
@@ -2249,43 +2265,46 @@ show_menu() {
check_install && restart check_install && restart
;; ;;
14) 14)
check_install && status check_install && restart_xray
;; ;;
15) 15)
check_install && show_log check_install && status
;; ;;
16) 16)
check_install && enable check_install && show_log
;; ;;
17) 17)
check_install && disable check_install && enable
;; ;;
18) 18)
ssl_cert_issue_main check_install && disable
;; ;;
19) 19)
ssl_cert_issue_CF ssl_cert_issue_main
;; ;;
20) 20)
iplimit_main ssl_cert_issue_CF
;; ;;
21) 21)
firewall_menu iplimit_main
;; ;;
22) 22)
SSH_port_forwarding firewall_menu
;; ;;
23) 23)
bbr_menu SSH_port_forwarding
;; ;;
24) 24)
update_geo bbr_menu
;; ;;
25) 25)
update_geo
;;
26)
run_speedtest run_speedtest
;; ;;
*) *)
LOGE "Please enter the correct number [0-25]" LOGE "Please enter the correct number [0-26]"
;; ;;
esac esac
} }
@@ -2301,6 +2320,9 @@ if [[ $# > 0 ]]; then
"restart") "restart")
check_install 0 && restart 0 check_install 0 && restart 0
;; ;;
"restart-xray")
check_install 0 && restart_xray 0
;;
"status") "status")
check_install 0 && status 0 check_install 0 && status 0
;; ;;