Compare commits

..

10 Commits

Author SHA1 Message Date
mhsanaei
49430b3991 Update docker.yml 2025-09-24 15:42:01 +02:00
mhsanaei
104526aab2 v2.8.4 2025-09-24 11:47:43 +02:00
mhsanaei
a0c07241c0 minor changes 2025-09-24 11:47:14 +02:00
mhsanaei
adf3242602 bug fix 2025-09-24 11:44:02 +02:00
mhsanaei
3f62592e4b API improve security: returns 404 for unauthenticated API requests 2025-09-24 11:29:55 +02:00
Дмитрий Олегович Саенко
02bff4db6c max port to 65535 (#3536)
* add EXPOSE port in Dockerfile

* fix: max port 65 531 -> 65 535

* fix

---------

Co-authored-by: mhsanaei <ho3ein.sanaei@gmail.com>
2025-09-23 19:43:56 +02:00
Happ-dev
8ff4e1ff31 Add Happ client export open link (#3542)
Co-authored-by: y.sivushkin <y.sivushkin@corp.101xp.com>
2025-09-23 16:46:45 +02:00
mhsanaei
26c6438ec2 fix api : subid, uuid from inbound settings 2025-09-23 11:52:40 +02:00
Evgeny Volferts
b3e96230c4 Add Alpine Linux support (#3534)
* Add Alpine linux support

* Fix for reading logs
2025-09-22 21:56:43 +02:00
mhsanaei
1016f3b4f9 fix: outbound address for vless 2025-09-22 00:20:05 +02:00
21 changed files with 380 additions and 162 deletions

View File

@@ -1,7 +1,9 @@
name: Release 3X-UI for Docker name: Release 3X-UI for Docker
permissions: permissions:
contents: read contents: read
packages: write packages: write
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
@@ -13,48 +15,48 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
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@v5
with: with:
images: | images: |
hsanaeii/3x-ui hsanaeii/3x-ui
ghcr.io/mhsanaei/3x-ui ghcr.io/mhsanaei/3x-ui
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=ref,event=tag type=ref,event=tag
type=pep440,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@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
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@v3
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@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} 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@v6
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386 platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1 +1 @@
2.8.3 2.8.4

2
go.mod
View File

@@ -95,7 +95,7 @@ require (
golang.org/x/tools v0.37.0 // indirect golang.org/x/tools v0.37.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-20250908214217-97024824d090 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.9 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
lukechampine.com/blake3 v1.4.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect

2
go.sum
View File

@@ -236,6 +236,8 @@ 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-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=

View File

@@ -56,6 +56,9 @@ install_base() {
opensuse-tumbleweed) opensuse-tumbleweed)
zypper refresh && zypper -q install -y wget curl tar timezone zypper refresh && zypper -q install -y wget curl tar timezone
;; ;;
alpine)
apk update && apk add wget curl tar tzdata
;;
*) *)
apt-get update && apt-get install -y -q wget curl tar tzdata apt-get update && apt-get install -y -q wget curl tar tzdata
;; ;;
@@ -177,7 +180,11 @@ install_x-ui() {
# Stop x-ui service and remove old resources # Stop x-ui service and remove old resources
if [[ -e /usr/local/x-ui/ ]]; then if [[ -e /usr/local/x-ui/ ]]; then
systemctl stop x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm /usr/local/x-ui/ -rf rm /usr/local/x-ui/ -rf
fi fi
@@ -201,10 +208,18 @@ install_x-ui() {
chmod +x /usr/bin/x-ui chmod +x /usr/bin/x-ui
config_after_install config_after_install
cp -f x-ui.service /etc/systemd/system/ if [[ $release == "alpine" ]]; then
systemctl daemon-reload wget -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
systemctl enable x-ui chmod +x /etc/init.d/x-ui
systemctl start x-ui rc-update add x-ui
rc-service x-ui start
else
cp -f x-ui.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui
fi
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..." echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
echo -e "" echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐ echo -e "┌───────────────────────────────────────────────────────┐

View File

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

View File

@@ -1,7 +1,10 @@
package controller package controller
import ( import (
"net/http"
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/session"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -21,11 +24,21 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
return a return a
} }
// checkAPIAuth is a middleware that returns 404 for unauthenticated API requests
// to hide the existence of API endpoints from unauthorized users
func (a *APIController) checkAPIAuth(c *gin.Context) {
if !session.IsLogin(c) {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.Next()
}
// initRouter sets up the API routes for inbounds, server, and other endpoints. // initRouter sets up the API routes for inbounds, server, and other endpoints.
func (a *APIController) initRouter(g *gin.RouterGroup) { func (a *APIController) initRouter(g *gin.RouterGroup) {
// Main API group // Main API group
api := g.Group("/panel/api") api := g.Group("/panel/api")
api.Use(a.checkLogin) api.Use(a.checkAPIAuth)
// Inbounds API // Inbounds API
inbounds := api.Group("/inbounds") inbounds := api.Group("/inbounds")

View File

@@ -39,8 +39,9 @@ func NewIndexController(g *gin.RouterGroup) *IndexController {
// initRouter sets up the routes for index, login, logout, and two-factor authentication. // initRouter sets up the routes for index, login, logout, and two-factor authentication.
func (a *IndexController) initRouter(g *gin.RouterGroup) { func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index) g.GET("/", a.index)
g.POST("/login", a.login)
g.GET("/logout", a.logout) g.GET("/logout", a.logout)
g.POST("/login", a.login)
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable) g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
} }

View File

@@ -8,8 +8,6 @@ import (
type XUIController struct { type XUIController struct {
BaseController BaseController
inboundController *InboundController
serverController *ServerController
settingController *SettingController settingController *SettingController
xraySettingController *XraySettingController xraySettingController *XraySettingController
} }
@@ -31,8 +29,6 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/settings", a.settings) g.GET("/settings", a.settings)
g.GET("/xray", a.xraySettings) g.GET("/xray", a.xraySettings)
a.inboundController = NewInboundController(g)
a.serverController = NewServerController(g)
a.settingController = NewSettingController(g) a.settingController = NewSettingController(g)
a.xraySettingController = NewXraySettingController(g) a.xraySettingController = NewXraySettingController(g)
} }

View File

@@ -28,7 +28,7 @@
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'> <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="inbound.port" :min="1" :max="65531"></a-input-number> <a-input-number v-model.number="inbound.port" :min="1" :max="65535"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@@ -52,7 +52,8 @@
<br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0"> <br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
<span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0"> <span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
<strong>{{ i18n "pages.inbounds.lastReset" }}:</strong> <strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
<span v-if="datepicker == 'gregorian'">[[ moment(dbInbound.lastTrafficResetTime).format('YYYY-MM-DD HH:mm:ss') ]]</span> <span v-if="datepicker == 'gregorian'">[[
moment(dbInbound.lastTrafficResetTime).format('YYYY-MM-DD HH:mm:ss') ]]</span>
<span v-else>[[ DateUtil.convertToJalalian(moment(dbInbound.lastTrafficResetTime)) ]]</span> <span v-else>[[ DateUtil.convertToJalalian(moment(dbInbound.lastTrafficResetTime)) ]]</span>
</span> </span>
</template> </template>

View File

@@ -3,12 +3,14 @@
<a-divider :style="{ margin: '5px 0 0' }"></a-divider> <a-divider :style="{ margin: '5px 0 0' }"></a-divider>
<a-form-item label="External Proxy"> <a-form-item label="External Proxy">
<a-switch v-model="externalProxy"></a-switch> <a-switch v-model="externalProxy"></a-switch>
<a-button icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '10px' }" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button> <a-button icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '10px' }" size="small"
@click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
</a-form-item> </a-form-item>
<a-input-group :style="{ margin: '8px 0' }" compact v-for="(row, index) in inbound.stream.externalProxy"> <a-input-group :style="{ margin: '8px 0' }" compact v-for="(row, index) in inbound.stream.externalProxy">
<template> <template>
<a-tooltip title="Force TLS"> <a-tooltip title="Force TLS">
<a-select v-model="row.forceTls" :style="{ width: '20%', margin: '0px' }" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="row.forceTls" :style="{ width: '20%', margin: '0px' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option> <a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
<a-select-option value="none">{{ i18n "none" }}</a-select-option> <a-select-option value="none">{{ i18n "none" }}</a-select-option>
<a-select-option value="tls">TLS</a-select-option> <a-select-option value="tls">TLS</a-select-option>
@@ -17,7 +19,7 @@
</template> </template>
<a-input :style="{ width: '30%' }" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input> <a-input :style="{ width: '30%' }" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'> <a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
<a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65531"></a-input-number> <a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65535"></a-input-number>
</a-tooltip> </a-tooltip>
<a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'> <a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
<template slot="addonAfter"> <template slot="addonAfter">

View File

@@ -7,12 +7,13 @@
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input> <a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'> <a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65531"></a-input-number> <a-input-number v-model.number="dnsModal.dnsServer.port" :min="1" :max="65535"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}'> <a-form-item label='{{ i18n "pages.xray.dns.strategy" }}'>
<a-select v-model="dnsModal.dnsServer.queryStrategy" :style="{ width: '100%' }" <a-select v-model="dnsModal.dnsServer.queryStrategy" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]] </a-select-option> <a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']"> [[ l ]]
</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-divider :style="{ margin: '5px 0' }"></a-divider> <a-divider :style="{ margin: '5px 0' }"></a-divider>

View File

@@ -39,7 +39,7 @@
<template #title>{{ i18n "pages.settings.panelPort"}}</template> <template #title>{{ i18n "pages.settings.panelPort"}}</template>
<template #description>{{ i18n "pages.settings.panelPortDesc"}}</template> <template #description>{{ i18n "pages.settings.panelPortDesc"}}</template>
<template #control> <template #control>
<a-input-number :min="1" :min="65531" v-model="allSetting.webPort" :style="{ width: '100%' }"></a-input> <a-input-number :min="1" :min="65535" v-model="allSetting.webPort" :style="{ width: '100%' }"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
@@ -137,7 +137,8 @@
<template #title>{{ i18n "pages.settings.datepicker"}}</template> <template #title>{{ i18n "pages.settings.datepicker"}}</template>
<template #description>{{ i18n "pages.settings.datepickerDescription"}}</template> <template #description>{{ i18n "pages.settings.datepickerDescription"}}</template>
<template #control> <template #control>
<a-select :style="{ width: '100%' }" :dropdown-class-name="themeSwitcher.currentTheme" v-model="datepicker"> <a-select :style="{ width: '100%' }" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="datepicker">
<a-select-option v-for="item in datepickerList" :value="item.value"> <a-select-option v-for="item in datepickerList" :value="item.value">
<span v-text="item.name"></span> <span v-text="item.name"></span>
</a-select-option> </a-select-option>

View File

@@ -40,7 +40,7 @@
<template #title>{{ i18n "pages.settings.subPort"}}</template> <template #title>{{ i18n "pages.settings.subPort"}}</template>
<template #description>{{ i18n "pages.settings.subPortDesc"}}</template> <template #description>{{ i18n "pages.settings.subPortDesc"}}</template>
<template #control> <template #control>
<a-input-number v-model="allSetting.subPort" :min="1" :min="65531" <a-input-number v-model="allSetting.subPort" :min="1" :min="65535"
:style="{ width: '100%' }"></a-input-number> :style="{ width: '100%' }"></a-input-number>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
@@ -48,13 +48,10 @@
<template #title>{{ i18n "pages.settings.subPath"}}</template> <template #title>{{ i18n "pages.settings.subPath"}}</template>
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template> <template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
<template #control> <template #control>
<a-input <a-input type="text" v-model="allSetting.subPath"
type="text"
v-model="allSetting.subPath"
@input="allSetting.subPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')" @input="allSetting.subPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
@blur="allSetting.subPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subPath)" @blur="allSetting.subPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subPath)"
placeholder="/sub/" placeholder="/sub/"></a-input>
></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">

View File

@@ -218,6 +218,8 @@
<a-menu-item key="android-npvtunnel" <a-menu-item key="android-npvtunnel"
@click="copy(app.subUrl)">NPV @click="copy(app.subUrl)">NPV
Tunnel</a-menu-item> Tunnel</a-menu-item>
<a-menu-item key="android-happ"
@click="open('happ://add/' + encodeURIComponent(app.subUrl))">Happ</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</a-col> </a-col>
@@ -244,6 +246,8 @@
@click="copy(npvtunUrl)">NPV @click="copy(npvtunUrl)">NPV
Tunnel Tunnel
</a-menu-item> </a-menu-item>
<a-menu-item key="ios-happ"
@click="open(happUrl)">Happ</a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
</a-col> </a-col>

View File

@@ -12,13 +12,14 @@
<a-layout-content> <a-layout-content>
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'> <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear> <transition name="list" appear>
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }" message='{{ i18n "secAlertTitle" }}' <a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable> message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
</a-alert> </a-alert>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-row v-if="!loadingStates.fetched"> <a-row v-if="!loadingStates.fetched">
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }"> <a-card
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin> <a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card> </a-card>
</a-row> </a-row>
@@ -37,7 +38,8 @@
<a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme"> <a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme">
<span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span> <span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
<template slot="content"> <template slot="content">
<span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line ]]</span> <span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line
]]</span>
</template> </template>
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-popover> </a-popover>
@@ -537,6 +539,7 @@
serverObj = o.settings.vnext; serverObj = o.settings.vnext;
break; break;
case Protocols.VLESS: case Protocols.VLESS:
return [o.settings?.address + ':' + o.settings?.port];
case Protocols.HTTP: case Protocols.HTTP:
case Protocols.Socks: case Protocols.Socks:
case Protocols.Shadowsocks: case Protocols.Shadowsocks:

View File

@@ -35,6 +35,25 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
return nil, err return nil, err
} }
// Enrich client stats with UUID/SubId from inbound settings
for _, inbound := range inbounds {
clients, _ := s.GetClients(inbound)
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
continue
}
// Build a map email -> client
cMap := make(map[string]model.Client, len(clients))
for _, c := range clients {
cMap[strings.ToLower(c.Email)] = c
}
for i := range inbound.ClientStats {
email := strings.ToLower(inbound.ClientStats[i].Email)
if c, ok := cMap[email]; ok {
inbound.ClientStats[i].UUID = c.ID
inbound.ClientStats[i].SubId = c.SubID
}
}
}
return inbounds, nil return inbounds, nil
} }
@@ -47,6 +66,24 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
return nil, err return nil, err
} }
// Enrich client stats with UUID/SubId from inbound settings
for _, inbound := range inbounds {
clients, _ := s.GetClients(inbound)
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
continue
}
cMap := make(map[string]model.Client, len(clients))
for _, c := range clients {
cMap[strings.ToLower(c.Email)] = c
}
for i := range inbound.ClientStats {
email := strings.ToLower(inbound.ClientStats[i].Email)
if c, ok := cMap[email]; ok {
inbound.ClientStats[i].UUID = c.ID
inbound.ClientStats[i].SubId = c.SubID
}
}
}
return inbounds, nil return inbounds, nil
} }

View File

@@ -46,8 +46,8 @@ var (
hashStorage *global.HashStorage hashStorage *global.HashStorage
// Performance improvements // Performance improvements
messageWorkerPool chan struct{} // Semaphore for limiting concurrent message processing messageWorkerPool chan struct{} // Semaphore for limiting concurrent message processing
optimizedHTTPClient *http.Client // HTTP client with connection pooling and timeouts optimizedHTTPClient *http.Client // HTTP client with connection pooling and timeouts
// Simple cache for frequently accessed data // Simple cache for frequently accessed data
statusCache struct { statusCache struct {
@@ -359,7 +359,7 @@ func (t *Tgbot) OnReceive() {
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
// Use goroutine with worker pool for concurrent command processing // Use goroutine with worker pool for concurrent command processing
go func() { go func() {
messageWorkerPool <- struct{}{} // Acquire worker messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker defer func() { <-messageWorkerPool }() // Release worker
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
@@ -371,7 +371,7 @@ func (t *Tgbot) OnReceive() {
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
// Use goroutine with worker pool for concurrent callback processing // Use goroutine with worker pool for concurrent callback processing
go func() { go func() {
messageWorkerPool <- struct{}{} // Acquire worker messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker defer func() { <-messageWorkerPool }() // Release worker
delete(userStates, query.Message.GetChat().ID) delete(userStates, query.Message.GetChat().ID)

View File

@@ -95,10 +95,9 @@ type Server struct {
httpServer *http.Server httpServer *http.Server
listener net.Listener listener net.Listener
index *controller.IndexController index *controller.IndexController
server *controller.ServerController panel *controller.XUIController
panel *controller.XUIController api *controller.APIController
api *controller.APIController
xrayService service.XrayService xrayService service.XrayService
settingService service.SettingService settingService service.SettingService
@@ -264,7 +263,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
g := engine.Group(basePath) g := engine.Group(basePath)
s.index = controller.NewIndexController(g) s.index = controller.NewIndexController(g)
s.server = controller.NewServerController(g)
s.panel = controller.NewXUIController(g) s.panel = controller.NewXUIController(g)
s.api = controller.NewAPIController(g) s.api = controller.NewAPIController(g)

13
x-ui.rc Normal file
View File

@@ -0,0 +1,13 @@
#!/sbin/openrc-run
command="/usr/local/x-ui/x-ui"
command_background=true
pidfile="/run/x-ui.pid"
description="x-ui Service"
procname="x-ui"
depend() {
need net
}
start_pre(){
cd /usr/local/x-ui
}

251
x-ui.sh
View File

@@ -153,11 +153,19 @@ uninstall() {
fi fi
return 0 return 0
fi fi
systemctl stop x-ui
systemctl disable x-ui if [[ $release == "alpine" ]]; then
rm /etc/systemd/system/x-ui.service -f rc-service x-ui stop
systemctl daemon-reload rc-update del x-ui
systemctl reset-failed rm /etc/init.d/x-ui -f
else
systemctl stop x-ui
systemctl disable x-ui
rm /etc/systemd/system/x-ui.service -f
systemctl daemon-reload
systemctl reset-failed
fi
rm /etc/x-ui/ -rf rm /etc/x-ui/ -rf
rm /usr/local/x-ui/ -rf rm /usr/local/x-ui/ -rf
@@ -286,7 +294,11 @@ start() {
echo "" echo ""
LOGI "Panel is running, No need to start again, If you need to restart, please select restart" LOGI "Panel is running, No need to start again, If you need to restart, please select restart"
else else
systemctl start x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui start
else
systemctl start x-ui
fi
sleep 2 sleep 2
check_status check_status
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
@@ -307,7 +319,11 @@ stop() {
echo "" echo ""
LOGI "Panel stopped, No need to stop again!" LOGI "Panel stopped, No need to stop again!"
else else
systemctl stop x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
sleep 2 sleep 2
check_status check_status
if [[ $? == 1 ]]; then if [[ $? == 1 ]]; then
@@ -323,7 +339,11 @@ stop() {
} }
restart() { restart() {
systemctl restart x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui restart
else
systemctl restart x-ui
fi
sleep 2 sleep 2
check_status check_status
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
@@ -337,14 +357,22 @@ restart() {
} }
status() { status() {
systemctl status x-ui -l if [[ $release == "alpine" ]]; then
rc-service x-ui status
else
systemctl status x-ui -l
fi
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
before_show_menu before_show_menu
fi fi
} }
enable() { enable() {
systemctl enable x-ui if [[ $release == "alpine" ]]; then
rc-update add x-ui
else
systemctl enable x-ui
fi
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
LOGI "x-ui Set to boot automatically on startup successfully" LOGI "x-ui Set to boot automatically on startup successfully"
else else
@@ -357,7 +385,11 @@ enable() {
} }
disable() { disable() {
systemctl disable x-ui if [[ $release == "alpine" ]]; then
rc-update del x-ui
else
systemctl disable x-ui
fi
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
LOGI "x-ui Autostart Cancelled successfully" LOGI "x-ui Autostart Cancelled successfully"
else else
@@ -370,32 +402,54 @@ disable() {
} }
show_log() { show_log() {
echo -e "${green}\t1.${plain} Debug Log" if [[ $release == "alpine" ]]; then
echo -e "${green}\t2.${plain} Clear All logs" echo -e "${green}\t1.${plain} Debug Log"
echo -e "${green}\t0.${plain} Back to Main Menu" echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice read -rp "Choose an option: " choice
case "$choice" in case "$choice" in
0) 0)
show_menu show_menu
;; ;;
1) 1)
journalctl -u x-ui -e --no-pager -f -p debug grep -F 'x-ui[' /var/log/messages
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
before_show_menu before_show_menu
fi fi
;; ;;
2) *)
sudo journalctl --rotate echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
sudo journalctl --vacuum-time=1s show_log
echo "All Logs cleared." ;;
restart esac
;; else
*) echo -e "${green}\t1.${plain} Debug Log"
echo -e "${red}Invalid option. Please select a valid number.${plain}\n" echo -e "${green}\t2.${plain} Clear All logs"
show_log echo -e "${green}\t0.${plain} Back to Main Menu"
;; read -rp "Choose an option: " choice
esac
case "$choice" in
0)
show_menu
;;
1)
journalctl -u x-ui -e --no-pager -f -p debug
if [[ $# == 0 ]]; then
before_show_menu
fi
;;
2)
sudo journalctl --rotate
sudo journalctl --vacuum-time=1s
echo "All Logs cleared."
restart
;;
*)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
show_log
;;
esac
fi
} }
bbr_menu() { bbr_menu() {
@@ -464,6 +518,9 @@ enable_bbr() {
arch | manjaro | parch) arch | manjaro | parch)
pacman -Sy --noconfirm ca-certificates pacman -Sy --noconfirm ca-certificates
;; ;;
alpine)
apk add ca-certificates
;;
*) *)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 exit 1
@@ -500,23 +557,42 @@ update_shell() {
# 0: running, 1: not running, 2: not installed # 0: running, 1: not running, 2: not installed
check_status() { check_status() {
if [[ ! -f /etc/systemd/system/x-ui.service ]]; then if [[ $release == "alpine" ]]; then
return 2 if [[ ! -f /etc/init.d/x-ui ]]; then
fi return 2
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1) fi
if [[ "${temp}" == "running" ]]; then if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then
return 0 return 0
else
return 1
fi
else else
return 1 if [[ ! -f /etc/systemd/system/x-ui.service ]]; then
return 2
fi
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
if [[ "${temp}" == "running" ]]; then
return 0
else
return 1
fi
fi fi
} }
check_enabled() { check_enabled() {
temp=$(systemctl is-enabled x-ui) if [[ $release == "alpine" ]]; then
if [[ "${temp}" == "enabled" ]]; then if [[ $(rc-update show | grep -F 'x-ui' | grep default -c) == 1 ]]; then
return 0 return 0
else
return 1
fi
else else
return 1 temp=$(systemctl is-enabled x-ui)
if [[ "${temp}" == "enabled" ]]; then
return 0
else
return 1
fi
fi fi
} }
@@ -798,7 +874,11 @@ update_geo() {
show_menu show_menu
;; ;;
1) 1)
systemctl stop x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip.dat geosite.dat rm -f geoip.dat geosite.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
@@ -806,7 +886,11 @@ update_geo() {
restart restart
;; ;;
2) 2)
systemctl stop x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip_IR.dat geosite_IR.dat rm -f geoip_IR.dat geosite_IR.dat
wget -O geoip_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat wget -O geoip_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat wget -O geosite_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
@@ -814,7 +898,11 @@ update_geo() {
restart restart
;; ;;
3) 3)
systemctl stop x-ui if [[ $release == "alpine" ]]; then
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip_RU.dat geosite_RU.dat rm -f geoip_RU.dat geosite_RU.dat
wget -O geoip_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat wget -O geoip_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat wget -O geosite_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
@@ -985,6 +1073,9 @@ ssl_cert_issue() {
arch | manjaro | parch) arch | manjaro | parch)
pacman -Sy --noconfirm socat pacman -Sy --noconfirm socat
;; ;;
alpine)
apk add socat
;;
*) *)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 exit 1
@@ -1335,7 +1426,11 @@ iplimit_main() {
read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM read -rp "Please enter new Ban Duration in Minutes [default 30]: " NUM
if [[ $NUM =~ ^[0-9]+$ ]]; then if [[ $NUM =~ ^[0-9]+$ ]]; then
create_iplimit_jails ${NUM} create_iplimit_jails ${NUM}
systemctl restart fail2ban if [[ $release == "alpine" ]]; then
rc-service fail2ban restart
else
systemctl restart fail2ban
fi
else else
echo -e "${red}${NUM} is not a number! Please, try again.${plain}" echo -e "${red}${NUM} is not a number! Please, try again.${plain}"
fi fi
@@ -1388,7 +1483,11 @@ iplimit_main() {
iplimit_main iplimit_main
;; ;;
9) 9)
systemctl restart fail2ban if [[ $release == "alpine" ]]; then
rc-service fail2ban restart
else
systemctl restart fail2ban
fi
iplimit_main iplimit_main
;; ;;
10) 10)
@@ -1436,6 +1535,9 @@ install_iplimit() {
arch | manjaro | parch) arch | manjaro | parch)
pacman -Syu --noconfirm fail2ban pacman -Syu --noconfirm fail2ban
;; ;;
alpine)
apk add fail2ban
;;
*) *)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 exit 1
@@ -1472,12 +1574,21 @@ install_iplimit() {
create_iplimit_jails create_iplimit_jails
# Launching fail2ban # Launching fail2ban
if ! systemctl is-active --quiet fail2ban; then if [[ $release == "alpine" ]]; then
systemctl start fail2ban if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then
rc-service fail2ban start
else
rc-service fail2ban restart
fi
rc-update add fail2ban
else else
systemctl restart fail2ban if ! systemctl is-active --quiet fail2ban; then
systemctl start fail2ban
else
systemctl restart fail2ban
fi
systemctl enable fail2ban
fi fi
systemctl enable fail2ban
echo -e "${green}IP Limit installed and configured successfully!${plain}\n" echo -e "${green}IP Limit installed and configured successfully!${plain}\n"
before_show_menu before_show_menu
@@ -1493,13 +1604,21 @@ remove_iplimit() {
rm -f /etc/fail2ban/filter.d/3x-ipl.conf rm -f /etc/fail2ban/filter.d/3x-ipl.conf
rm -f /etc/fail2ban/action.d/3x-ipl.conf rm -f /etc/fail2ban/action.d/3x-ipl.conf
rm -f /etc/fail2ban/jail.d/3x-ipl.conf rm -f /etc/fail2ban/jail.d/3x-ipl.conf
systemctl restart fail2ban if [[ $release == "alpine" ]]; then
rc-service fail2ban restart
else
systemctl restart fail2ban
fi
echo -e "${green}IP Limit removed successfully!${plain}\n" echo -e "${green}IP Limit removed successfully!${plain}\n"
before_show_menu before_show_menu
;; ;;
2) 2)
rm -rf /etc/fail2ban rm -rf /etc/fail2ban
systemctl stop fail2ban if [[ $release == "alpine" ]]; then
rc-service fail2ban stop
else
systemctl stop fail2ban
fi
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get remove -y fail2ban apt-get remove -y fail2ban
@@ -1517,6 +1636,9 @@ remove_iplimit() {
arch | manjaro | parch) arch | manjaro | parch)
pacman -Rns --noconfirm fail2ban pacman -Rns --noconfirm fail2ban
;; ;;
alpine)
apk del fail2ban
;;
*) *)
echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n" echo -e "${red}Unsupported operating system. Please uninstall Fail2ban manually.${plain}\n"
exit 1 exit 1
@@ -1540,9 +1662,16 @@ show_banlog() {
echo -e "${green}Checking ban logs...${plain}\n" echo -e "${green}Checking ban logs...${plain}\n"
if ! systemctl is-active --quiet fail2ban; then if [[ $release == "alpine" ]]; then
echo -e "${red}Fail2ban service is not running!${plain}\n" if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then
return 1 echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
fi
else
if ! systemctl is-active --quiet fail2ban; then
echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
fi
fi fi
if [[ -f "$system_log" ]]; then if [[ -f "$system_log" ]]; then