Compare commits

..

No commits in common. "main" and "v2.6.6" have entirely different histories.
main ... v2.6.6

59 changed files with 74214 additions and 528 deletions

2
.github/FUNDING.yml vendored
View file

@ -1,6 +1,6 @@
# These are supported funding model platforms
github: MHSanaei
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username

View file

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
submodules: true

View file

@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5

4
.gitignore vendored
View file

@ -29,9 +29,9 @@ main
.DS_Store
Thumbs.db
# Ignore Go build files
# Ignore Go specific files
*.exe
x-ui.db
*.exe~
# Ignore Docker specific files
docker-compose.override.yml

View file

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

View file

@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## النجوم عبر الزمن

View file

@ -48,7 +48,7 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Estrellas a lo Largo del Tiempo

View file

@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## ستاره‌ها در طول زمان

View file

@ -48,7 +48,7 @@ For full documentation, please visit the [project Wiki](https://github.com/MHSan
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Stargazers over Time

View file

@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## Звезды с течением времени

View file

@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## 随时间变化的星标数

View file

@ -3,10 +3,7 @@ package config
import (
_ "embed"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
)
@ -57,32 +54,12 @@ func GetBinFolderPath() string {
return binFolderPath
}
func getBaseDir() string {
exePath, err := os.Executable()
if err != nil {
return "."
}
exeDir := filepath.Dir(exePath)
exeDirLower := strings.ToLower(filepath.ToSlash(exeDir))
if strings.Contains(exeDirLower, "/appdata/local/temp/") || strings.Contains(exeDirLower, "/go-build") {
wd, err := os.Getwd()
if err != nil {
return "."
}
return wd
}
return exeDir
}
func GetDBFolderPath() string {
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
if dbFolderPath != "" {
return dbFolderPath
if dbFolderPath == "" {
dbFolderPath = "/etc/x-ui"
}
if runtime.GOOS == "windows" {
return getBaseDir()
}
return "/etc/x-ui"
return dbFolderPath
}
func GetDBPath() string {
@ -91,54 +68,8 @@ func GetDBPath() string {
func GetLogFolder() string {
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
if logFolderPath != "" {
return logFolderPath
if logFolderPath == "" {
logFolderPath = "/var/log"
}
if runtime.GOOS == "windows" {
return getBaseDir()
}
return "/var/log"
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Sync()
}
func init() {
if runtime.GOOS != "windows" {
return
}
if os.Getenv("XUI_DB_FOLDER") != "" {
return
}
oldDBFolder := "/etc/x-ui"
oldDBPath := fmt.Sprintf("%s/%s.db", oldDBFolder, GetName())
newDBFolder := GetDBFolderPath()
newDBPath := fmt.Sprintf("%s/%s.db", newDBFolder, GetName())
_, err := os.Stat(newDBPath)
if err == nil {
return // new exists
}
_, err = os.Stat(oldDBPath)
if os.IsNotExist(err) {
return // old does not exist
}
_ = copyFile(oldDBPath, newDBPath) // ignore error
return logFolderPath
}

8
go.mod
View file

@ -1,6 +1,6 @@
module x-ui
go 1.25.0
go 1.24.5
require (
github.com/gin-contrib/gzip v1.2.3
@ -15,7 +15,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.7
github.com/valyala/fasthttp v1.65.0
github.com/valyala/fasthttp v1.64.0
github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.250803.0
go.uber.org/atomic v1.11.0
@ -56,7 +56,7 @@ require (
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mattn/go-sqlite3 v1.14.30 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@ -92,7 +92,7 @@ require (
golang.org/x/tools v0.36.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect

12
go.sum
View file

@ -95,8 +95,8 @@ github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr32
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -162,8 +162,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@ -226,8 +226,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=

View file

@ -7,6 +7,7 @@ yellow='\033[0;33m'
plain='\033[0m'
cur_dir=$(pwd)
show_ip_service_lists=("https://api.ipify.org" "https://4.ident.me")
# check root
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
@ -57,7 +58,7 @@ install_base() {
zypper refresh && zypper -q install -y wget curl tar timezone
;;
*)
apt-get update && apt-get install -y -q wget curl tar tzdata
apt-get update && apt install -y -q wget curl tar tzdata
;;
esac
}
@ -72,18 +73,10 @@ config_after_install() {
local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local URL_lists=(
"https://api4.ipify.org"
"https://ipv4.icanhazip.com"
"https://v4.api.ipinfo.io/ip"
"https://ipv4.myexternalip.com/raw"
"https://4.ident.me"
"https://check-host.net/ip"
)
local server_ip=""
for ip_address in "${URL_lists[@]}"; do
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
if [[ -n "${server_ip}" ]]; then
for ip_service_addr in "${show_ip_service_lists[@]}"; do
local server_ip=$(curl -s --max-time 3 ${ip_service_addr} 2>/dev/null)
if [ -n "${server_ip}" ]; then
break
fi
done

View file

@ -209,10 +209,9 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
var streamSettings map[string]any
json.Unmarshal([]byte(stream), &streamSettings)
security, _ := streamSettings["security"].(string)
switch security {
case "tls":
if security == "tls" {
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
case "reality":
} else if security == "reality" {
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
}
delete(streamSettings, "sockopt")

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,7 @@
@import "../lib/style/index.less";
@import "../lib/style/components.less";
@green-6: #008771;
@primary-color: @green-6;
@border-radius-base: 1rem;
@progress-remaining-color: #EDEDED;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1710,7 +1710,7 @@ class Inbound extends XrayCommonClass {
toJson() {
let streamSettings;
if (this.canEnableStream() || this.stream?.sockopt) {
if (this.canEnableStream()) {
streamSettings = this.stream.toJson();
}
return {

View file

@ -919,14 +919,12 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
constructor(
packets = '1-3',
length = '',
interval = '',
maxSplit = ''
interval = ''
) {
super();
this.packets = packets;
this.length = length;
this.interval = interval;
this.maxSplit = maxSplit;
}
static fromJson(json = {}) {
@ -934,7 +932,6 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
json.packets,
json.length,
json.interval,
json.maxSplit
);
}
};
@ -943,14 +940,12 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
constructor(
type = 'rand',
packet = '10-20',
delay = '10-16',
applyTo = 'ip'
delay = '10-16'
) {
super();
this.type = type;
this.packet = packet;
this.delay = delay;
this.applyTo = applyTo;
}
static fromJson(json = {}) {
@ -958,7 +953,6 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
json.type,
json.packet,
json.delay,
json.applyTo
);
}
@ -967,7 +961,6 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
type: this.type,
packet: this.packet,
delay: this.delay,
applyTo: this.applyTo
};
}
};
@ -995,7 +988,7 @@ Outbound.DNSSettings = class extends CommonClass {
network = 'udp',
address = '',
port = 53,
nonIPQuery = 'reject',
nonIPQuery = 'drop',
blockTypes = []
) {
super();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./vue.common.prod.js')
} else {
module.exports = require('./vue.common.dev.js')
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

6
web/assets/vue/vue.esm.browser.min.js vendored Normal file

File diff suppressed because one or more lines are too long

11918
web/assets/vue/vue.esm.js Normal file

File diff suppressed because it is too large Load diff

11932
web/assets/vue/vue.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
if (process.env.NODE_ENV === 'production') {
module.exports = require('./vue.runtime.common.prod.js')
} else {
module.exports = require('./vue.runtime.common.dev.js')
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

6
web/assets/vue/vue.runtime.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,76 @@
import Vue from './vue.runtime.common.js'
export default Vue
// this should be kept in sync with src/v3/index.ts
export const {
version,
// refs
ref,
shallowRef,
isRef,
toRef,
toRefs,
unref,
proxyRefs,
customRef,
triggerRef,
computed,
// reactive
reactive,
isReactive,
isReadonly,
isShallow,
isProxy,
shallowReactive,
markRaw,
toRaw,
readonly,
shallowReadonly,
// watch
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
// effectScope
effectScope,
onScopeDispose,
getCurrentScope,
// provide / inject
provide,
inject,
// lifecycle
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onActivated,
onDeactivated,
onServerPrefetch,
onRenderTracked,
onRenderTriggered,
// v2 only
set,
del,
// v3 compat
h,
getCurrentInstance,
useSlots,
useAttrs,
mergeDefaults,
nextTick,
useCssModule,
useCssVars,
defineComponent,
defineAsyncComponent
} = Vue

View file

@ -2,10 +2,10 @@ package entity
import (
"crypto/tls"
"math"
"net"
"strings"
"time"
"math"
"x-ui/util/common"
)
@ -39,8 +39,8 @@ type AllSetting struct {
TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubTitle string `json:"subTitle" form:"subTitle"`
SubListen string `json:"subListen" form:"subListen"`

View file

@ -42,9 +42,6 @@
<a-form-item label='Interval'>
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
</a-form-item>
<a-form-item label='Max Split'>
<a-input v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
</a-form-item>
</template>
<!-- Switch for Noises -->
@ -78,11 +75,6 @@
<a-form-item label='Delay'>
<a-input v-model.trim="noise.delay"></a-input>
</a-form-item>
<a-form-item label='Apply To'>
<a-select v-model="noise.applyTo" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
</a-form>
</template>
</template>
@ -105,7 +97,7 @@
</a-form-item>
<a-form-item label='non-IP queries'>
<a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option>
<a-select-option v-for="s in ['drop','skip']" :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types' >

View file

@ -1119,11 +1119,7 @@
protocol: inbound.protocol,
settings: inbound.settings.toString(),
};
if (inbound.canEnableStream()){
data.streamSettings = inbound.stream.toString();
} else if (inbound.stream?.sockopt) {
data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
}
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
data.sniffing = inbound.sniffing.toString();
data.allocate = inbound.allocate.toString();
@ -1143,11 +1139,7 @@
protocol: inbound.protocol,
settings: inbound.settings.toString(),
};
if (inbound.canEnableStream()){
data.streamSettings = inbound.stream.toString();
} else if (inbound.stream?.sockopt) {
data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
}
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
data.sniffing = inbound.sniffing.toString();
data.allocate = inbound.allocate.toString();

View file

@ -207,7 +207,7 @@
settings: {
domainStrategy: "AsIs",
noises: [
{ type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" },
{ type: "rand", packet: "10-20", delay: "10-16" },
],
},
},
@ -397,7 +397,7 @@
}
},
addNoise() {
const newNoise = { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" };
const newNoise = { type: "rand", packet: "10-20", delay: "10-16" };
this.noisesArray = [...this.noisesArray, newNoise];
},
removeNoise(index) {
@ -420,11 +420,6 @@
updatedNoises[index] = { ...updatedNoises[index], delay: value };
this.noisesArray = updatedNoises;
},
updateNoiseApplyTo(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], applyTo: value };
this.noisesArray = updatedNoises;
},
},
computed: {
fragment: {

View file

@ -90,18 +90,6 @@
placeholder="10-20"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>ApplyTo</template>
<template #control>
<a-select :value="noise.applyTo" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"
@change="(value) => updateNoiseApplyTo(index, value)">
<a-select-option :value="p" :label="p" v-for="p in ['ip', 'ipv4', 'ipv6']" :key="p">
<span>[[ p ]]</span>
</a-select-option>
</a-select>
</template>
</a-setting-list-item>
<a-space direction="horizontal" :style="{ padding: '10px 20px' }">
<a-button v-if="noisesArray.length > 1" type="danger"
@click="removeNoise(index)">Remove</a-button>

View file

@ -11,6 +11,7 @@ import (
"sort"
"time"
"slices"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
@ -57,21 +58,21 @@ func (j *CheckClientIpJob) Run() {
func (j *CheckClientIpJob) clearAccessLog() {
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
j.checkError(err)
defer logAccessP.Close()
accessLogPath, err := xray.GetAccessLogPath()
j.checkError(err)
file, err := os.Open(accessLogPath)
j.checkError(err)
defer file.Close()
_, err = io.Copy(logAccessP, file)
j.checkError(err)
logAccessP.Close()
file.Close()
err = os.Truncate(accessLogPath, 0)
j.checkError(err)
j.lastClear = time.Now().Unix()
}
@ -192,6 +193,10 @@ func (j *CheckClientIpJob) checkError(e error) {
}
}
func (j *CheckClientIpJob) contains(s []string, str string) bool {
return slices.Contains(s, str)
}
func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
db := database.GetDB()
InboundClientIps := &model.InboundClientIps{}

View file

@ -177,16 +177,15 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
// Secure client ID
for _, client := range clients {
switch inbound.Protocol {
case "trojan":
if inbound.Protocol == "trojan" {
if client.Password == "" {
return inbound, false, common.NewError("empty client ID")
}
case "shadowsocks":
} else if inbound.Protocol == "shadowsocks" {
if client.Email == "" {
return inbound, false, common.NewError("empty client ID")
}
default:
} else {
if client.ID == "" {
return inbound, false, common.NewError("empty client ID")
}
@ -437,16 +436,15 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
// Secure client ID
for _, client := range clients {
switch oldInbound.Protocol {
case "trojan":
if oldInbound.Protocol == "trojan" {
if client.Password == "" {
return false, common.NewError("empty client ID")
}
case "shadowsocks":
} else if oldInbound.Protocol == "shadowsocks" {
if client.Email == "" {
return false, common.NewError("empty client ID")
}
default:
} else {
if client.ID == "" {
return false, common.NewError("empty client ID")
}
@ -633,14 +631,13 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
clientIndex := -1
for index, oldClient := range oldClients {
oldClientId := ""
switch oldInbound.Protocol {
case "trojan":
if oldInbound.Protocol == "trojan" {
oldClientId = oldClient.Password
newClientId = clients[0].Password
case "shadowsocks":
} else if oldInbound.Protocol == "shadowsocks" {
oldClientId = oldClient.Email
newClientId = clients[0].Email
default:
} else {
oldClientId = oldClient.ID
newClientId = clients[0].ID
}
@ -1247,12 +1244,11 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
switch inbound.Protocol {
case "trojan":
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
case "shadowsocks":
} else if inbound.Protocol == "shadowsocks" {
clientId = oldClient.Email
default:
} else {
clientId = oldClient.ID
}
break
@ -1332,12 +1328,11 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
switch inbound.Protocol {
case "trojan":
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
case "shadowsocks":
} else if inbound.Protocol == "shadowsocks" {
clientId = oldClient.Email
default:
} else {
clientId = oldClient.ID
}
clientOldEnabled = oldClient.Enable
@ -1396,12 +1391,11 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
switch inbound.Protocol {
case "trojan":
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
case "shadowsocks":
} else if inbound.Protocol == "shadowsocks" {
clientId = oldClient.Email
default:
} else {
clientId = oldClient.ID
}
break
@ -1454,12 +1448,11 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
switch inbound.Protocol {
case "trojan":
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
case "shadowsocks":
} else if inbound.Protocol == "shadowsocks" {
clientId = oldClient.Email
default:
} else {
clientId = oldClient.ID
}
break
@ -1515,12 +1508,11 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
switch inbound.Protocol {
case "trojan":
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
case "shadowsocks":
} else if inbound.Protocol == "shadowsocks" {
clientId = oldClient.Email
default:
} else {
clientId = oldClient.ID
}
break

View file

@ -235,21 +235,8 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
}
// IP fetching with caching
showIp4ServiceLists := []string{
"https://api4.ipify.org",
"https://ipv4.icanhazip.com",
"https://v4.api.ipinfo.io/ip",
"https://ipv4.myexternalip.com/raw",
"https://4.ident.me",
"https://check-host.net/ip",
}
showIp6ServiceLists := []string{
"https://api6.ipify.org",
"https://ipv6.icanhazip.com",
"https://v6.api.ipinfo.io/ip",
"https://ipv6.myexternalip.com/raw",
"https://6.ident.me",
}
showIp4ServiceLists := []string{"https://api.ipify.org", "https://4.ident.me"}
showIp6ServiceLists := []string{"https://api6.ipify.org", "https://6.ident.me"}
if s.cachedIPv4 == "" {
for _, ip4Service := range showIp4ServiceLists {

View file

@ -40,6 +40,7 @@ var (
isRunning bool
hostname string
hashStorage *global.HashStorage
handler *th.Handler
// clients data to adding new client
receiver_inbound_ID int
@ -640,14 +641,13 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 4 {
num, err := strconv.Atoi(dataArray[3])
if err == nil {
switch num {
case -2:
if num == -2 {
inputNumber = 0
case -1:
} else if num == -1 {
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
default:
} else {
inputNumber = (inputNumber * 10) + num
}
}
@ -704,10 +704,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
@ -719,14 +715,13 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 3 {
num, err := strconv.Atoi(dataArray[2])
if err == nil {
switch num {
case -2:
if num == -2 {
inputNumber = 0
case -1:
} else if num == -1 {
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
default:
} else {
inputNumber = (inputNumber * 10) + num
}
}
@ -849,14 +844,13 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 4 {
num, err := strconv.Atoi(dataArray[3])
if err == nil {
switch num {
case -2:
if num == -2 {
inputNumber = 0
case -1:
} else if num == -1 {
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
default:
} else {
inputNumber = (inputNumber * 10) + num
}
}
@ -925,10 +919,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
@ -940,14 +930,13 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 3 {
num, err := strconv.Atoi(dataArray[2])
if err == nil {
switch num {
case -2:
if num == -2 {
inputNumber = 0
case -1:
} else if num == -1 {
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
default:
} else {
inputNumber = (inputNumber * 10) + num
}
}
@ -1046,14 +1035,13 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 4 {
num, err := strconv.Atoi(dataArray[3])
if err == nil {
switch num {
case -2:
if num == -2 {
inputNumber = 0
case -1:
} else if num == -1 {
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
default:
} else {
inputNumber = (inputNumber * 10) + num
}
}
@ -1113,10 +1101,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
@ -1128,14 +1112,13 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if len(dataArray) == 3 {
num, err := strconv.Atoi(dataArray[2])
if err == nil {
switch num {
case -2:
if num == -2 {
inputNumber = 0
case -1:
} else if num == -1 {
if inputNumber > 0 {
inputNumber = (inputNumber / 10)
}
default:
} else {
inputNumber = (inputNumber * 10) + num
}
}
@ -1305,10 +1288,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(callbackQuery.Message.GetChat().ID, message_text)
}
@ -1545,10 +1524,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
case "add_client_default_ip_limit":
@ -1559,10 +1534,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
if err != nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
return
}
t.addClient(chatId, message_text, messageId)
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
case "add_client_submit_disable":
@ -1627,10 +1598,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
return
}
valid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails)
if err != nil {
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove())
return
}
for _, valid_emails := range valid_emails {
traffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails)
@ -1793,10 +1760,6 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
}
jsonString, err := t.BuildJSONForProtocol(inbound.Protocol)
if err != nil {
logger.Warning("BuildJSONForProtocol run failed:", err)
return false, errors.New("failed to build JSON for protocol")
}
newInbound := &model.Inbound{
Id: receiver_inbound_ID,
@ -2045,11 +2008,10 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
}
msg := ""
switch status {
case LoginSuccess:
if status == LoginSuccess {
msg += t.I18nBot("tgbot.messages.loginSuccess")
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
case LoginFail:
} else if status == LoginFail {
msg += t.I18nBot("tgbot.messages.loginFailed")
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
msg += t.I18nBot("tgbot.messages.password", "Password=="+password)
@ -2209,22 +2171,6 @@ func (t *Tgbot) clientInfoMsg(
expiryTime = t.I18nBot("tgbot.unlimited")
} else if diff > 172800 || !traffic.Enable {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
if diff > 0 {
days := diff / 86400
hours := (diff % 86400) / 3600
minutes := (diff % 3600) / 60
remainingTime := ""
if days > 0 {
remainingTime += fmt.Sprintf("%d %s ", days, t.I18nBot("tgbot.days"))
}
if hours > 0 {
remainingTime += fmt.Sprintf("%d %s ", hours, t.I18nBot("tgbot.hours"))
}
if minutes > 0 {
remainingTime += fmt.Sprintf("%d %s", minutes, t.I18nBot("tgbot.minutes"))
}
expiryTime += fmt.Sprintf(" (%s)", remainingTime)
}
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
flag = true

View file

@ -561,25 +561,24 @@
"resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة"
[tgbot]
"keyboardClosed" = "❌ لوحة المفاتيح مغلقة!"
"noResult" = "❗ لا يوجد نتائج!"
"noQuery" = "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!"
"wentWrong" = "❌ حدث خطأ ما!"
"noIpRecord" = "❗ لا يوجد سجل IP!"
"noInbounds" = "❗ لم يتم العثور على أي وارد!"
"unlimited" = "♾ غير محدود (إعادة تعيين)"
"add" = "إضافة"
"keyboardClosed" = "❌ الكيبورد المخصص اتقفلت!"
"noResult" = "❗ مفيش نتيجة!"
"noQuery" = "❌ مش لاقي السؤال! استخدم الأمر تاني!"
"wentWrong" = "❌ حصل خطأ!"
"noIpRecord" = "❗ مفيش سجل IP!"
"noInbounds" = "❗ مفيش إدخال متواجد!"
"unlimited" = "♾ غير محدود (إعادة ضبط)"
"add" = "أضف"
"month" = "شهر"
"months" = "أشهر"
"months" = "شهور"
"day" = "يوم"
"days" = "أيام"
"hours" = "ساعات"
"minutes" = "دقائق"
"unknown" = "غير معروف"
"inbounds" = "الواردات"
"unknown" = "مش معروف"
"inbounds" = "الإدخالات"
"clients" = "العملاء"
"offline" = "🔴 غير متصل"
"online" = "🟢 متصل"
"offline" = "🔴 أوفلاين"
"online" = "🟢 أونلاين"
[tgbot.commands]
"unknown" = "❗ أمر مش معروف."

View file

@ -574,7 +574,6 @@
"day" = "Day"
"days" = "Days"
"hours" = "Hours"
"minutes" = "Minutes"
"unknown" = "Unknown"
"inbounds" = "Inbounds"
"clients" = "Clients"

View file

@ -91,7 +91,7 @@
"invalidFormData" = "El formato de los datos de entrada es inválido."
"emptyUsername" = "Por favor ingresa el nombre de usuario."
"emptyPassword" = "Por favor ingresa la contraseña."
"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
"successLogin" = "Has iniciado sesión en tu cuenta correctamente."
[pages.index]
@ -535,9 +535,9 @@
[pages.settings.security]
"admin" = "Credenciales de administrador"
"twoFactor" = "Autenticación de dos factores"
"twoFactorEnable" = "Habilitar 2FA"
"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
"twoFactor" = "Autenticación de dos factores"
"twoFactorEnable" = "Habilitar 2FA"
"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
"twoFactorModalSetTitle" = "Activar autenticación de dos factores"
"twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores"
"twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:"
@ -561,24 +561,23 @@
"resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente"
[tgbot]
"keyboardClosed" = "❌ Teclado cerrado!"
"noResult" = "❗ ¡No hay resultados!"
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando de nuevo!"
"keyboardClosed" = "❌ ¡Teclado personalizado cerrado!"
"noResult" = "❗ ¡Sin resultados!"
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor utiliza el comando nuevamente!"
"wentWrong" = "❌ ¡Algo salió mal!"
"noIpRecord" = "❗ ¡No hay registro de IP!"
"noIpRecord" = "❗ ¡Sin Registro de IP!"
"noInbounds" = "❗ ¡No se encontraron entradas!"
"unlimited" = "♾ Ilimitado (Restablecer)"
"add" = "Añadir"
"unlimited" = "♾ Ilimitado"
"add" = "Agregar"
"month" = "Mes"
"months" = "Meses"
"day" = "Día"
"days" = "Días"
"hours" = "Horas"
"minutes" = "Minutos"
"unknown" = "Desconocido"
"inbounds" = "Entradas"
"clients" = "Clientes"
"offline" = "🔴 Desconectado"
"offline" = "🔴 Sin conexión"
"online" = "🟢 En línea"
[tgbot.commands]

View file

@ -561,23 +561,22 @@
"resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی"
[tgbot]
"keyboardClosed" = "❌ صفحه کلید بسته شد!"
"noResult" = "❗ نتیجه ای یافت نشد!"
"noQuery" = "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!"
"wentWrong" = "❌ مشکلی پیش آمد!"
"noIpRecord" = "❗ رکورد آی پی وجود ندارد!"
"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!"
"noResult" = "❗ نتیجهای یافت نشد!"
"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!"
"wentWrong" = "❌ مشکلی رخ داده است!"
"noIpRecord" = "❗ رکورد IP یافت نشد!"
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
"unlimited" = "♾ نامحدود(ریست)"
"add" = "افزودن"
"unlimited" = "♾ - نامحدود(ریست)"
"add" = "اضافه کردن"
"month" = "ماه"
"months" = "ماه"
"months" = "ماه"
"day" = "روز"
"days" = "روز"
"hours" = "ساعت"
"minutes" = "دقیقه"
"hours" = "ساعت‌"
"unknown" = "نامشخص"
"inbounds" = "ورودی ها"
"clients" = اربران"
"inbounds" = "ورودیها"
"clients" = لاینت‌ها"
"offline" = "🔴 آفلاین"
"online" = "🟢 آنلاین"

View file

@ -561,22 +561,21 @@
"resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar"
[tgbot]
"keyboardClosed" = "❌ Keyboard ditutup!"
"keyboardClosed" = "❌ Papan ketik kustom ditutup!"
"noResult" = "❗ Tidak ada hasil!"
"noQuery" = "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!"
"wentWrong" = "❌ Terjadi kesalahan!"
"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!"
"wentWrong" = "❌ Ada yang salah!"
"noIpRecord" = "❗ Tidak ada Catatan IP!"
"noInbounds" = "❗ Tidak ada inbound yang ditemukan!"
"unlimited" = "♾ Tidak terbatas (Reset)"
"noInbounds" = "❗ Tidak ada masuk ditemukan!"
"unlimited" = "♾ Tak terbatas"
"add" = "Tambah"
"month" = "Bulan"
"months" = "Bulan"
"day" = "Hari"
"days" = "Hari"
"hours" = "Jam"
"minutes" = "Menit"
"unknown" = "Tidak diketahui"
"inbounds" = "Inbound"
"inbounds" = "Masuk"
"clients" = "Klien"
"offline" = "🔴 Offline"
"online" = "🟢 Online"

View file

@ -561,22 +561,21 @@
"resetOutboundTrafficError" = "送信トラフィックのリセットエラー"
[tgbot]
"keyboardClosed" = "❌ キーボードを閉じました!"
"keyboardClosed" = "❌ カスタムキーボードが閉じられました!"
"noResult" = "❗ 結果がありません!"
"noQuery" = "❌ クエリが見つかりません!コマンドを再利用してください!"
"wentWrong" = "❌ 何かがうまくいかなかった!"
"noIpRecord" = "❗ IPレコードがありません!"
"noInbounds" = "❗ インバウンドが見つかりません!"
"unlimited" = "♾ 無制限(リセット)"
"noQuery" = "❌ クエリが見つかりませんでした!もう一度コマンドを使用してください!"
"wentWrong" = "❌ 問題が発生しました!"
"noIpRecord" = "❗ IP記録がありません!"
"noInbounds" = "❗ インバウンド接続が見つかりません!"
"unlimited" = "♾ 無制限"
"add" = "追加"
"month" = "月"
"months" = "月"
"months" = "月"
"day" = "日"
"days" = "日"
"days" = "日"
"hours" = "時間"
"minutes" = "分"
"unknown" = "不明"
"inbounds" = "インバウンド"
"inbounds" = "インバウンド接続"
"clients" = "クライアント"
"offline" = "🔴 オフライン"
"online" = "🟢 オンライン"

View file

@ -561,22 +561,21 @@
"resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída"
[tgbot]
"keyboardClosed" = "❌ Teclado fechado!"
"keyboardClosed" = "❌ Teclado personalizado fechado!"
"noResult" = "❗ Nenhum resultado!"
"noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!"
"wentWrong" = "❌ Algo deu errado!"
"noIpRecord" = "❗ Nenhum registro de IP!"
"noInbounds" = "❗ Nenhum inbound encontrado!"
"unlimited" = "♾ Ilimitado (Reset)"
"noInbounds" = "❗ Nenhuma entrada encontrada!"
"unlimited" = "♾ Ilimitado (Reiniciar)"
"add" = "Adicionar"
"month" = "Mês"
"months" = "Meses"
"day" = "Dia"
"days" = "Dias"
"hours" = "Horas"
"minutes" = "Minutos"
"unknown" = "Desconhecido"
"inbounds" = "Inbounds"
"inbounds" = "Entradas"
"clients" = "Clientes"
"offline" = "🔴 Offline"
"online" = "🟢 Online"

View file

@ -574,7 +574,6 @@
"day" = "День"
"days" = "Дней"
"hours" = "Часов"
"minutes" = "Минуты"
"unknown" = "Неизвестно"
"inbounds" = "Инбаунды"
"clients" = "Клиенты"

View file

@ -561,23 +561,22 @@
"resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata"
[tgbot]
"keyboardClosed" = "❌ Klavye kapatıldı!"
"keyboardClosed" = "❌ Özel klavye kapalı!"
"noResult" = "❗ Sonuç yok!"
"noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!"
"wentWrong" = "❌ Bir şeyler yanlış gitti!"
"noIpRecord" = "❗ IP Kaydı Yok!"
"noInbounds" = "❗ Gelen bağlantı bulunamadı!"
"unlimited" = "♾ Sınırsız (Sıfırla)"
"noIpRecord" = "❗ IP Kaydı yok!"
"noInbounds" = "❗ Gelen bulunamadı!"
"unlimited" = "♾ Sınırsız(Sıfırla)"
"add" = "Ekle"
"month" = "Ay"
"months" = "Aylar"
"day" = "Gün"
"days" = "Günler"
"hours" = "Saatler"
"minutes" = "Dakika"
"unknown" = "Bilinmeyen"
"unknown" = "Bilinmiyor"
"inbounds" = "Gelenler"
"clients" = "İstemciler"
"clients" = "Müşteriler"
"offline" = "🔴 Çevrimdışı"
"online" = "🟢 Çevrimiçi"

View file

@ -561,20 +561,19 @@
"resetOutboundTrafficError" = "Помилка скидання вихідного трафіку"
[tgbot]
"keyboardClosed" = "❌ Клавіатуру закрито!"
"keyboardClosed" = "❌ Спеціальна клавіатура закрита!"
"noResult" = "❗ Немає результату!"
"noQuery" = "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!"
"noQuery" = "❌ Запит не знайдено! Скористайтеся командою ще раз!"
"wentWrong" = "❌ Щось пішло не так!"
"noIpRecord" = "❗ Немає запису IP!"
"noInbounds" = "❗ Вхідні не знайдені!"
"unlimited" = "♾ Необмежено (Скинути)"
"noIpRecord" = "❗ Немає IP-запису!"
"noInbounds" = "❗ Вхідних не знайдено!"
"unlimited" = "♾ Необмежений (скинути)"
"add" = "Додати"
"month" = "Місяць"
"months" = "Місяці"
"day" = "День"
"days" = "Дні"
"hours" = "Години"
"minutes" = "Хвилини"
"hours" = "Годинник"
"unknown" = "Невідомо"
"inbounds" = "Вхідні"
"clients" = "Клієнти"

View file

@ -91,7 +91,7 @@
"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
"emptyUsername" = "Vui lòng nhập tên người dùng."
"emptyPassword" = "Vui lòng nhập mật khẩu."
"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
"successLogin" = "Bạn đã đăng nhập vào tài khoản thành công."
[pages.index]
@ -535,9 +535,9 @@
[pages.settings.security]
"admin" = "Thông tin đăng nhập quản trị viên"
"twoFactor" = "Xác thực hai yếu tố"
"twoFactorEnable" = "Bật 2FA"
"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
"twoFactor" = "Xác thực hai yếu tố"
"twoFactorEnable" = "Bật 2FA"
"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
"twoFactorModalSetTitle" = "Bật xác thực hai yếu tố"
"twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố"
"twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:"
@ -561,23 +561,22 @@
"resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi"
[tgbot]
"keyboardClosed" = "❌ Bàn phím đã đóng!"
"keyboardClosed" = "❌ Bàn phím tùy chỉnh đã đóng!"
"noResult" = "❗ Không có kết quả!"
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!"
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lệnh lại!"
"wentWrong" = "❌ Đã xảy ra lỗi!"
"noIpRecord" = "❗ Không có bản ghi IP!"
"noInbounds" = "❗ Không tìm thấy inbound!"
"unlimited" = "♾ Không giới hạn (Đặt lại)"
"unlimited" = "♾ Không giới hạn"
"add" = "Thêm"
"month" = "Tháng"
"months" = "Tháng"
"day" = "Ngày"
"days" = "Ngày"
"hours" = "Giờ"
"minutes" = "Phút"
"unknown" = "Không xác định"
"inbounds" = "Inbound"
"clients" = "Client"
"unknown" = "Không rõ"
"inbounds" = "Vào"
"clients" = "Các người dùng"
"offline" = "🔴 Ngoại tuyến"
"online" = "🟢 Trực tuyến"

View file

@ -563,20 +563,19 @@
[tgbot]
"keyboardClosed" = "❌ 自定义键盘已关闭!"
"noResult" = "❗ 没有结果!"
"noQuery" = "❌ 未找到查询!请再次使用该命令!"
"noQuery" = "❌ 未找到查询!请重新使用命令!"
"wentWrong" = "❌ 出了点问题!"
"noIpRecord" = "❗ 没有IP记录"
"noInbounds" = "❗ 未找到入站"
"unlimited" = "♾ 无限(重置)"
"noIpRecord" = "❗ 没有 IP 记录!"
"noInbounds" = "❗ 没有找到入站连接"
"unlimited" = "♾ 无限"
"add" = "添加"
"month" = "月"
"months" = "月"
"day" = "天"
"days" = "天"
"hours" = "小时"
"minutes" = "分钟"
"unknown" = "未知"
"inbounds" = "入站"
"inbounds" = "入站连接"
"clients" = "客户端"
"offline" = "🔴 离线"
"online" = "🟢 在线"

View file

@ -563,23 +563,22 @@
[tgbot]
"keyboardClosed" = "❌ 自定義鍵盤已關閉!"
"noResult" = "❗ 沒有結果!"
"noQuery" = "❌ 未找到查詢!請再次使用該命令!"
"noQuery" = "❌ 未找到查詢!請重新使用命令!"
"wentWrong" = "❌ 出了點問題!"
"noIpRecord" = "❗ 沒有IP記錄"
"noInbounds" = "❗ 未找到入站"
"unlimited" = "♾ 無限(重置)"
"add" = "添加"
"noIpRecord" = "❗ 沒有 IP 記錄!"
"noInbounds" = "❗ 沒有找到入站連線"
"unlimited" = "♾ 無限"
"add" = "新增"
"month" = "月"
"months" = "月"
"day" = "天"
"days" = "天"
"hours" = "小時"
"minutes" = "分鐘"
"unknown" = "未知"
"inbounds" = "入站"
"inbounds" = "入站連線"
"clients" = "客戶端"
"offline" = "🔴 離線"
"online" = "🟢 線"
"online" = "🟢 "
[tgbot.commands]
"unknown" = "❗ 未知命令"

249
x-ui.sh
View file

@ -398,6 +398,37 @@ show_log() {
esac
}
show_banlog() {
local system_log="/var/log/fail2ban.log"
echo -e "${green}Checking ban logs...${plain}\n"
if ! systemctl is-active --quiet fail2ban; then
echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
fi
if [[ -f "$system_log" ]]; then
echo -e "${green}Recent system ban activities from fail2ban.log:${plain}"
grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}"
echo ""
fi
if [[ -f "${iplimit_banned_log_path}" ]]; then
echo -e "${green}3X-IPL ban log entries:${plain}"
if [[ -s "${iplimit_banned_log_path}" ]]; then
grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}"
else
echo -e "${yellow}Ban log file is empty${plain}"
fi
else
echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}"
fi
echo -e "\n${green}Current jail status:${plain}"
fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}"
}
bbr_menu() {
echo -e "${green}\t1.${plain} Enable BBR"
echo -e "${green}\t2.${plain} Disable BBR"
@ -974,7 +1005,7 @@ ssl_cert_issue() {
# install socat second
case "${release}" in
ubuntu | debian | armbian)
apt-get update && apt-get install socat -y
apt update && apt install socat -y
;;
centos | rhel | almalinux | rocky | ol)
yum -y update && yum -y install socat
@ -1299,7 +1330,81 @@ run_speedtest() {
speedtest
}
create_iplimit_jails() {
# Use default bantime if not passed => 30 minutes
local bantime="${1:-30}"
# Uncomment 'allowipv6 = auto' in fail2ban.conf
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
# On Debian 12+ fail2ban's default backend should be changed to systemd
if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then
sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf
fi
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
[3x-ipl]
enabled=true
backend=auto
filter=3x-ipl
action=3x-ipl
logpath=${iplimit_log_path}
maxretry=2
findtime=32
bantime=${bantime}m
EOF
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
[Definition]
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
ignoreregex =
EOF
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
before = iptables-allports.conf
[Definition]
actionstart = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -j f2b-<name>
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
[Init]
name = default
protocol = tcp
chain = INPUT
EOF
echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}"
}
iplimit_remove_conflicts() {
local jail_files=(
/etc/fail2ban/jail.conf
/etc/fail2ban/jail.local
)
for file in "${jail_files[@]}"; do
# Check for [3x-ipl] config in jail file then remove it
if test -f "${file}" && grep -qw '3x-ipl' ${file}; then
sed -i "/\[3x-ipl\]/,/^$/d" ${file}
echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n"
fi
done
}
ip_validation() {
ipv6_regex="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$"
@ -1409,22 +1514,14 @@ install_iplimit() {
# Check the OS and install necessary packages
case "${release}" in
ubuntu)
apt-get update
if [[ "${os_version}" -ge 24 ]]; then
apt-get install python3-pip -y
apt update && apt install python3-pip -y
python3 -m pip install pyasynchat --break-system-packages
fi
apt-get install fail2ban -y
apt update && apt install fail2ban -y
;;
debian)
apt-get update
if [ "$os_version" -ge 12 ]; then
apt-get install -y python3-systemd
fi
apt-get install -y fail2ban
;;
armbian)
apt-get update && apt-get install fail2ban -y
debian | armbian)
apt update && apt install fail2ban -y
;;
centos | rhel | almalinux | rocky | ol)
yum update -y && yum install epel-release -y
@ -1535,129 +1632,11 @@ remove_iplimit() {
esac
}
show_banlog() {
local system_log="/var/log/fail2ban.log"
echo -e "${green}Checking ban logs...${plain}\n"
if ! systemctl is-active --quiet fail2ban; then
echo -e "${red}Fail2ban service is not running!${plain}\n"
return 1
fi
if [[ -f "$system_log" ]]; then
echo -e "${green}Recent system ban activities from fail2ban.log:${plain}"
grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}"
echo ""
fi
if [[ -f "${iplimit_banned_log_path}" ]]; then
echo -e "${green}3X-IPL ban log entries:${plain}"
if [[ -s "${iplimit_banned_log_path}" ]]; then
grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}"
else
echo -e "${yellow}Ban log file is empty${plain}"
fi
else
echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}"
fi
echo -e "\n${green}Current jail status:${plain}"
fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}"
}
create_iplimit_jails() {
# Use default bantime if not passed => 30 minutes
local bantime="${1:-30}"
# Uncomment 'allowipv6 = auto' in fail2ban.conf
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
# On Debian 12+ fail2ban's default backend should be changed to systemd
if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then
sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf
fi
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
[3x-ipl]
enabled=true
backend=auto
filter=3x-ipl
action=3x-ipl
logpath=${iplimit_log_path}
maxretry=2
findtime=32
bantime=${bantime}m
EOF
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
[Definition]
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
ignoreregex =
EOF
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
[INCLUDES]
before = iptables-allports.conf
[Definition]
actionstart = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -j f2b-<name>
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
[Init]
name = default
protocol = tcp
chain = INPUT
EOF
echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}"
}
iplimit_remove_conflicts() {
local jail_files=(
/etc/fail2ban/jail.conf
/etc/fail2ban/jail.local
)
for file in "${jail_files[@]}"; do
# Check for [3x-ipl] config in jail file then remove it
if test -f "${file}" && grep -qw '3x-ipl' ${file}; then
sed -i "/\[3x-ipl\]/,/^$/d" ${file}
echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n"
fi
done
}
SSH_port_forwarding() {
local URL_lists=(
"https://api4.ipify.org"
"https://ipv4.icanhazip.com"
"https://v4.api.ipinfo.io/ip"
"https://ipv4.myexternalip.com/raw"
"https://4.ident.me"
"https://check-host.net/ip"
)
local server_ip=""
for ip_address in "${URL_lists[@]}"; do
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
if [[ -n "${server_ip}" ]]; then
break
fi
done
local server_ip=$(curl -s --max-time 3 https://api.ipify.org)
if [ -z "$server_ip" ]; then
server_ip=$(curl -s --max-time 3 https://4.ident.me)
fi
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local existing_listenIP=$(/usr/local/x-ui/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')