mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-13 11:39:13 +00:00
Merge branch 'main' into feature/multi-server-support
This commit is contained in:
commit
3b262cf180
25 changed files with 444 additions and 180 deletions
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
|
@ -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:
|
||||||
|
@ -27,7 +29,7 @@ jobs:
|
||||||
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
|
||||||
|
@ -47,7 +49,7 @@ jobs:
|
||||||
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
|
||||||
|
@ -55,6 +57,6 @@ jobs:
|
||||||
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 }}
|
|
@ -1 +1 @@
|
||||||
2.8.3
|
2.8.4
|
2
go.mod
2
go.mod
|
@ -96,7 +96,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
2
go.sum
|
@ -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=
|
||||||
|
|
15
install.sh
15
install.sh
|
@ -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
|
||||||
;;
|
;;
|
||||||
|
@ -184,7 +187,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
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service x-ui stop
|
||||||
|
else
|
||||||
systemctl stop x-ui
|
systemctl stop x-ui
|
||||||
|
fi
|
||||||
rm /usr/local/x-ui/ -rf
|
rm /usr/local/x-ui/ -rf
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -208,10 +215,18 @@ install_x-ui() {
|
||||||
chmod +x /usr/bin/x-ui
|
chmod +x /usr/bin/x-ui
|
||||||
config_after_install
|
config_after_install
|
||||||
|
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
wget -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
|
||||||
|
chmod +x /etc/init.d/x-ui
|
||||||
|
rc-update add x-ui
|
||||||
|
rc-service x-ui start
|
||||||
|
else
|
||||||
cp -f x-ui.service /etc/systemd/system/
|
cp -f x-ui.service /etc/systemd/system/
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable x-ui
|
systemctl enable x-ui
|
||||||
systemctl start 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 "┌───────────────────────────────────────────────────────┐
|
||||||
|
|
50
sub/sub.go
50
sub/sub.go
|
@ -98,8 +98,14 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set base_path based on LinksPath for template rendering
|
// Set base_path based on LinksPath for template rendering
|
||||||
|
// Ensure LinksPath ends with "/" for proper asset URL generation
|
||||||
|
basePath := LinksPath
|
||||||
|
if basePath != "/" && !strings.HasSuffix(basePath, "/") {
|
||||||
|
basePath += "/"
|
||||||
|
}
|
||||||
|
logger.Debug("sub: Setting base_path to:", basePath)
|
||||||
engine.Use(func(c *gin.Context) {
|
engine.Use(func(c *gin.Context) {
|
||||||
c.Set("base_path", LinksPath)
|
c.Set("base_path", basePath)
|
||||||
})
|
})
|
||||||
|
|
||||||
Encrypt, err := s.settingService.GetSubEncrypt()
|
Encrypt, err := s.settingService.GetSubEncrypt()
|
||||||
|
@ -179,22 +185,48 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
|
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mount assets in multiple paths to handle different URL patterns
|
||||||
|
var assetsFS http.FileSystem
|
||||||
if _, err := os.Stat("web/assets"); err == nil {
|
if _, err := os.Stat("web/assets"); err == nil {
|
||||||
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
|
assetsFS = http.FS(os.DirFS("web/assets"))
|
||||||
if linksPathForAssets != "/assets" {
|
|
||||||
engine.StaticFS(linksPathForAssets, http.FS(os.DirFS("web/assets")))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
||||||
engine.StaticFS("/assets", http.FS(subFS))
|
assetsFS = http.FS(subFS)
|
||||||
if linksPathForAssets != "/assets" {
|
|
||||||
engine.StaticFS(linksPathForAssets, http.FS(subFS))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.Error("sub: failed to mount embedded assets:", err)
|
logger.Error("sub: failed to mount embedded assets:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if assetsFS != nil {
|
||||||
|
engine.StaticFS("/assets", assetsFS)
|
||||||
|
if linksPathForAssets != "/assets" {
|
||||||
|
engine.StaticFS(linksPathForAssets, assetsFS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add middleware to handle dynamic asset paths with subid
|
||||||
|
if LinksPath != "/" {
|
||||||
|
engine.Use(func(c *gin.Context) {
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
// Check if this is an asset request with subid pattern: /sub/path/{subid}/assets/...
|
||||||
|
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
|
||||||
|
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
|
||||||
|
// Extract the asset path after /assets/
|
||||||
|
assetsIndex := strings.Index(path, "/assets/")
|
||||||
|
if assetsIndex != -1 {
|
||||||
|
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
|
||||||
|
if assetPath != "" {
|
||||||
|
// Serve the asset file
|
||||||
|
c.FileFromFS(assetPath, assetsFS)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g := engine.Group("/")
|
g := engine.Group("/")
|
||||||
|
|
||||||
s.sub = NewSUBController(
|
s.sub = NewSUBController(
|
||||||
|
|
|
@ -87,7 +87,20 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||||
if !a.jsonEnabled {
|
if !a.jsonEnabled {
|
||||||
subJsonURL = ""
|
subJsonURL = ""
|
||||||
}
|
}
|
||||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL)
|
// Get base_path from context (set by middleware)
|
||||||
|
basePath, exists := c.Get("base_path")
|
||||||
|
if !exists {
|
||||||
|
basePath = "/"
|
||||||
|
}
|
||||||
|
// Add subId to base_path for asset URLs
|
||||||
|
basePathStr := basePath.(string)
|
||||||
|
if basePathStr == "/" {
|
||||||
|
basePathStr = "/" + subId + "/"
|
||||||
|
} else {
|
||||||
|
// Remove trailing slash if exists, add subId, then add trailing slash
|
||||||
|
basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/"
|
||||||
|
}
|
||||||
|
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr)
|
||||||
c.HTML(200, "subpage.html", gin.H{
|
c.HTML(200, "subpage.html", gin.H{
|
||||||
"title": "subscription.title",
|
"title": "subscription.title",
|
||||||
"cur_ver": config.GetVersion(),
|
"cur_ver": config.GetVersion(),
|
||||||
|
|
|
@ -1169,7 +1169,7 @@ func (s *SubService) joinPathWithID(basePath, subId string) string {
|
||||||
|
|
||||||
// BuildPageData parses header and prepares the template view model.
|
// BuildPageData parses header and prepares the template view model.
|
||||||
// BuildPageData constructs page data for rendering the subscription information page.
|
// BuildPageData constructs page data for rendering the subscription information page.
|
||||||
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
|
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData {
|
||||||
download := common.FormatTraffic(traffic.Down)
|
download := common.FormatTraffic(traffic.Down)
|
||||||
upload := common.FormatTraffic(traffic.Up)
|
upload := common.FormatTraffic(traffic.Up)
|
||||||
total := "∞"
|
total := "∞"
|
||||||
|
@ -1188,7 +1188,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
|
||||||
|
|
||||||
return PageData{
|
return PageData{
|
||||||
Host: hostHeader,
|
Host: hostHeader,
|
||||||
BasePath: "/", // kept as "/"; templates now use context base_path injected from router
|
BasePath: basePath,
|
||||||
SId: subId,
|
SId: subId,
|
||||||
Download: download,
|
Download: download,
|
||||||
Upload: upload,
|
Upload: upload,
|
||||||
|
|
|
@ -142,6 +142,9 @@
|
||||||
},
|
},
|
||||||
npvtunUrl() {
|
npvtunUrl() {
|
||||||
return this.app.subUrl;
|
return this.app.subUrl;
|
||||||
|
},
|
||||||
|
happUrl() {
|
||||||
|
return `happ://add/${encodeURIComponent(this.app.subUrl)}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -32,8 +30,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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -35,8 +35,8 @@
|
||||||
<a-space direction="vertical" :size="10">
|
<a-space direction="vertical" :size="10">
|
||||||
<a-theme-switch-login></a-theme-switch-login>
|
<a-theme-switch-login></a-theme-switch-login>
|
||||||
<span>{{ i18n "pages.settings.language" }}</span>
|
<span>{{ i18n "pages.settings.language" }}</span>
|
||||||
<a-select ref="selectLang" class="w-100" v-model="lang"
|
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
|
||||||
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||||
<span v-text="l.name"></span>
|
<span v-text="l.name"></span>
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
</a-input>
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
<a-input-password autocomplete="current-password" name="password" v-model.trim="user.password"
|
||||||
placeholder='{{ i18n "password" }}' required>
|
placeholder='{{ i18n "password" }}' required>
|
||||||
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||||
</a-input-password>
|
</a-input-password>
|
||||||
|
@ -81,7 +81,8 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-row justify="center" class="centered">
|
<a-row justify="center" class="centered">
|
||||||
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem" :style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
|
||||||
|
:style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
||||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
||||||
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||||
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -38,6 +38,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +69,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,6 @@ type Server struct {
|
||||||
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
|
||||||
|
|
||||||
|
@ -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.NewMultiServerController(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
13
x-ui.rc
Normal 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
|
||||||
|
}
|
129
x-ui.sh
129
x-ui.sh
|
@ -153,11 +153,19 @@ uninstall() {
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service x-ui stop
|
||||||
|
rc-update del x-ui
|
||||||
|
rm /etc/init.d/x-ui -f
|
||||||
|
else
|
||||||
systemctl stop x-ui
|
systemctl stop x-ui
|
||||||
systemctl disable x-ui
|
systemctl disable x-ui
|
||||||
rm /etc/systemd/system/x-ui.service -f
|
rm /etc/systemd/system/x-ui.service -f
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl reset-failed
|
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
|
||||||
|
|
||||||
|
@ -285,8 +293,12 @@ start() {
|
||||||
if [[ $? == 0 ]]; then
|
if [[ $? == 0 ]]; then
|
||||||
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
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service x-ui start
|
||||||
else
|
else
|
||||||
systemctl start x-ui
|
systemctl start x-ui
|
||||||
|
fi
|
||||||
sleep 2
|
sleep 2
|
||||||
check_status
|
check_status
|
||||||
if [[ $? == 0 ]]; then
|
if [[ $? == 0 ]]; then
|
||||||
|
@ -306,8 +318,12 @@ stop() {
|
||||||
if [[ $? == 1 ]]; then
|
if [[ $? == 1 ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
LOGI "Panel stopped, No need to stop again!"
|
LOGI "Panel stopped, No need to stop again!"
|
||||||
|
else
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service x-ui stop
|
||||||
else
|
else
|
||||||
systemctl stop x-ui
|
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() {
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service x-ui restart
|
||||||
|
else
|
||||||
systemctl restart x-ui
|
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() {
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service x-ui status
|
||||||
|
else
|
||||||
systemctl status x-ui -l
|
systemctl status x-ui -l
|
||||||
|
fi
|
||||||
if [[ $# == 0 ]]; then
|
if [[ $# == 0 ]]; then
|
||||||
before_show_menu
|
before_show_menu
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-update add x-ui
|
||||||
|
else
|
||||||
systemctl enable x-ui
|
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() {
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-update del x-ui
|
||||||
|
else
|
||||||
systemctl disable x-ui
|
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,6 +402,27 @@ disable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
show_log() {
|
show_log() {
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
echo -e "${green}\t1.${plain} Debug Log"
|
||||||
|
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||||
|
read -rp "Choose an option: " choice
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
0)
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
1)
|
||||||
|
grep -F 'x-ui[' /var/log/messages
|
||||||
|
if [[ $# == 0 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
|
||||||
|
show_log
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
echo -e "${green}\t1.${plain} Debug Log"
|
echo -e "${green}\t1.${plain} Debug Log"
|
||||||
echo -e "${green}\t2.${plain} Clear All logs"
|
echo -e "${green}\t2.${plain} Clear All logs"
|
||||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||||
|
@ -396,6 +449,7 @@ show_log() {
|
||||||
show_log
|
show_log
|
||||||
;;
|
;;
|
||||||
esac
|
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,6 +557,16 @@ update_shell() {
|
||||||
|
|
||||||
# 0: running, 1: not running, 2: not installed
|
# 0: running, 1: not running, 2: not installed
|
||||||
check_status() {
|
check_status() {
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
if [[ ! -f /etc/init.d/x-ui ]]; then
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
if [[ $(rc-service x-ui status | grep -F 'status: started' -c) == 1 ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
if [[ ! -f /etc/systemd/system/x-ui.service ]]; then
|
if [[ ! -f /etc/systemd/system/x-ui.service ]]; then
|
||||||
return 2
|
return 2
|
||||||
fi
|
fi
|
||||||
|
@ -509,15 +576,24 @@ check_status() {
|
||||||
else
|
else
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
check_enabled() {
|
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
|
||||||
|
else
|
||||||
temp=$(systemctl is-enabled x-ui)
|
temp=$(systemctl is-enabled x-ui)
|
||||||
if [[ "${temp}" == "enabled" ]]; then
|
if [[ "${temp}" == "enabled" ]]; then
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
check_uninstall() {
|
check_uninstall() {
|
||||||
|
@ -798,7 +874,11 @@ update_geo() {
|
||||||
show_menu
|
show_menu
|
||||||
;;
|
;;
|
||||||
1)
|
1)
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service x-ui stop
|
||||||
|
else
|
||||||
systemctl stop x-ui
|
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)
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service x-ui stop
|
||||||
|
else
|
||||||
systemctl stop x-ui
|
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)
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service x-ui stop
|
||||||
|
else
|
||||||
systemctl stop x-ui
|
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}
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service fail2ban restart
|
||||||
|
else
|
||||||
systemctl restart fail2ban
|
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)
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service fail2ban restart
|
||||||
|
else
|
||||||
systemctl restart fail2ban
|
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 [[ $release == "alpine" ]]; then
|
||||||
|
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
|
||||||
if ! systemctl is-active --quiet fail2ban; then
|
if ! systemctl is-active --quiet fail2ban; then
|
||||||
systemctl start fail2ban
|
systemctl start fail2ban
|
||||||
else
|
else
|
||||||
systemctl restart fail2ban
|
systemctl restart fail2ban
|
||||||
fi
|
fi
|
||||||
systemctl enable fail2ban
|
systemctl enable fail2ban
|
||||||
|
fi
|
||||||
|
|
||||||
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
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service fail2ban restart
|
||||||
|
else
|
||||||
systemctl restart fail2ban
|
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
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
rc-service fail2ban stop
|
||||||
|
else
|
||||||
systemctl stop fail2ban
|
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,10 +1662,17 @@ show_banlog() {
|
||||||
|
|
||||||
echo -e "${green}Checking ban logs...${plain}\n"
|
echo -e "${green}Checking ban logs...${plain}\n"
|
||||||
|
|
||||||
|
if [[ $release == "alpine" ]]; then
|
||||||
|
if [[ $(rc-service fail2ban status | grep -F 'status: started' -c) == 0 ]]; then
|
||||||
|
echo -e "${red}Fail2ban service is not running!${plain}\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
if ! systemctl is-active --quiet fail2ban; then
|
if ! systemctl is-active --quiet fail2ban; then
|
||||||
echo -e "${red}Fail2ban service is not running!${plain}\n"
|
echo -e "${red}Fail2ban service is not running!${plain}\n"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -f "$system_log" ]]; then
|
if [[ -f "$system_log" ]]; then
|
||||||
echo -e "${green}Recent system ban activities from fail2ban.log:${plain}"
|
echo -e "${green}Recent system ban activities from fail2ban.log:${plain}"
|
||||||
|
|
Loading…
Reference in a new issue