Compare commits

...

11 commits

Author SHA1 Message Date
mhsanaei
6a17285935
remove: glibc check
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
now you can install on all OS like ubuntu 20 or 18
2025-08-04 19:16:56 +02:00
mhsanaei
4b03e9d919
v2.6.5 2025-08-04 19:12:37 +02:00
mhsanaei
7e9c3bdbaf
fix: sub enable warning 2025-08-04 19:09:01 +02:00
fgsfds
957f3dbb54
Added xray access log viewer (#3309)
* added xray access log viewer

* made modal window width adaptive

* hide logs button if xray logs are disabled
2025-08-04 18:47:48 +02:00
mhsanaei
05e60af283
fix: IPLimitlog display 2025-08-04 18:23:37 +02:00
mhsanaei
5e40458116
fix: simplify error handling 2025-08-04 18:01:32 +02:00
Sanaei
baf6fdd29d
fix portMap json (#3312)
Co-authored-by: Alireza Ahmadi <alireza7@gmail.com>
2025-08-04 17:16:11 +02:00
Sanaei
45f78d3521
Merge pull request #3311 from MHSanaei/dokodemo_portMap
add dokodemo port mapping
2025-08-04 17:01:57 +02:00
Alireza Ahmadi
01f984e054 add dokodemo port mapping 2025-08-04 16:45:09 +02:00
Sanaei
e4ba5ba53a
add ech support (#3310)
Co-authored-by: Alireza Ahmadi <alireza7@gmail.com>
2025-08-04 16:27:57 +02:00
mhsanaei
6ff555c8bb
runs-on: ubuntu-latest 2025-08-04 14:39:12 +02:00
30 changed files with 288 additions and 36 deletions

View file

@ -7,7 +7,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -31,7 +31,7 @@ jobs:
- 386 - 386
- armv5 - armv5
- s390x - s390x
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4

View file

@ -1 +1 @@
2.6.3 2.6.5

View file

@ -40,19 +40,6 @@ arch() {
echo "Arch: $(arch)" echo "Arch: $(arch)"
check_glibc_version() {
glibc_version=$(ldd --version | head -n1 | awk '{print $NF}')
required_version="2.32"
if [[ "$(printf '%s\n' "$required_version" "$glibc_version" | sort -V | head -n1)" != "$required_version" ]]; then
echo -e "${red}GLIBC version $glibc_version is too old! Required: 2.32 or higher${plain}"
echo "Please upgrade to a newer version of your operating system to get a higher GLIBC version."
exit 1
fi
echo "GLIBC version: $glibc_version (meets requirement of 2.32+)"
}
check_glibc_version
install_base() { install_base() {
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)

View file

@ -560,6 +560,8 @@ class TlsStreamSettings extends XrayCommonClass {
enableSessionResumption = false, enableSessionResumption = false,
certificates = [new TlsStreamSettings.Cert()], certificates = [new TlsStreamSettings.Cert()],
alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1], alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
echServerKeys = '',
echForceQuery = 'none',
settings = new TlsStreamSettings.Settings() settings = new TlsStreamSettings.Settings()
) { ) {
super(); super();
@ -573,6 +575,8 @@ class TlsStreamSettings extends XrayCommonClass {
this.enableSessionResumption = enableSessionResumption; this.enableSessionResumption = enableSessionResumption;
this.certs = certificates; this.certs = certificates;
this.alpn = alpn; this.alpn = alpn;
this.echServerKeys = echServerKeys;
this.echForceQuery = echForceQuery;
this.settings = settings; this.settings = settings;
} }
@ -592,7 +596,7 @@ class TlsStreamSettings extends XrayCommonClass {
} }
if (!ObjectUtil.isEmpty(json.settings)) { if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure, json.settings.fingerprint, json.settings.serverName, json.settings.domains); settings = new TlsStreamSettings.Settings(json.settings.allowInsecure, json.settings.fingerprint, json.settings.echConfigList);
} }
return new TlsStreamSettings( return new TlsStreamSettings(
json.serverName, json.serverName,
@ -605,6 +609,8 @@ class TlsStreamSettings extends XrayCommonClass {
json.enableSessionResumption, json.enableSessionResumption,
certs, certs,
json.alpn, json.alpn,
json.echServerKeys,
json.echForceQuery,
settings, settings,
); );
} }
@ -621,6 +627,8 @@ class TlsStreamSettings extends XrayCommonClass {
enableSessionResumption: this.enableSessionResumption, enableSessionResumption: this.enableSessionResumption,
certificates: TlsStreamSettings.toJsonArray(this.certs), certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn, alpn: this.alpn,
echServerKeys: this.echServerKeys,
echForceQuery: this.echForceQuery,
settings: this.settings, settings: this.settings,
}; };
} }
@ -701,21 +709,25 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor( constructor(
allowInsecure = false, allowInsecure = false,
fingerprint = UTLS_FINGERPRINT.UTLS_CHROME, fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
echConfigList = '',
) { ) {
super(); super();
this.allowInsecure = allowInsecure; this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.echConfigList = echConfigList;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new TlsStreamSettings.Settings( return new TlsStreamSettings.Settings(
json.allowInsecure, json.allowInsecure,
json.fingerprint, json.fingerprint,
json.echConfigList,
); );
} }
toJson() { toJson() {
return { return {
allowInsecure: this.allowInsecure, allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
echConfigList: this.echConfigList
}; };
} }
}; };
@ -1375,6 +1387,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (type == "tcp" && !ObjectUtil.isEmpty(flow)) { if (type == "tcp" && !ObjectUtil.isEmpty(flow)) {
params.set("flow", flow); params.set("flow", flow);
} }
@ -1474,6 +1489,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.settings.allowInsecure) { if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1"); params.set("allowInsecure", "1");
} }
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }
@ -1552,6 +1570,9 @@ class Inbound extends XrayCommonClass {
if (this.stream.tls.settings.allowInsecure) { if (this.stream.tls.settings.allowInsecure) {
params.set("allowInsecure", "1"); params.set("allowInsecure", "1");
} }
if (this.stream.tls.settings.echConfigList?.length > 0) {
params.set("ech", this.stream.tls.settings.echConfigList);
}
if (!ObjectUtil.isEmpty(this.stream.tls.sni)) { if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
params.set("sni", this.stream.tls.sni); params.set("sni", this.stream.tls.sni);
} }
@ -2291,12 +2312,14 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
protocol, protocol,
address, address,
port, port,
portMap = [],
network = 'tcp,udp', network = 'tcp,udp',
followRedirect = false followRedirect = false
) { ) {
super(protocol); super(protocol);
this.address = address; this.address = address;
this.port = port; this.port = port;
this.portMap = portMap;
this.network = network; this.network = network;
this.followRedirect = followRedirect; this.followRedirect = followRedirect;
} }
@ -2306,6 +2329,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
Protocols.DOKODEMO, Protocols.DOKODEMO,
json.address, json.address,
json.port, json.port,
XrayCommonClass.toHeaders(json.portMap),
json.network, json.network,
json.followRedirect, json.followRedirect,
); );
@ -2315,6 +2339,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
return { return {
address: this.address, address: this.address,
port: this.port, port: this.port,
portMap: XrayCommonClass.toV2Headers(this.portMap, false),
network: this.network, network: this.network,
followRedirect: this.followRedirect, followRedirect: this.followRedirect,
}; };

View file

@ -354,13 +354,15 @@ class TlsStreamSettings extends CommonClass {
serverName = '', serverName = '',
alpn = [], alpn = [],
fingerprint = '', fingerprint = '',
allowInsecure = false allowInsecure = false,
echConfigList = '',
) { ) {
super(); super();
this.serverName = serverName; this.serverName = serverName;
this.alpn = alpn; this.alpn = alpn;
this.fingerprint = fingerprint; this.fingerprint = fingerprint;
this.allowInsecure = allowInsecure; this.allowInsecure = allowInsecure;
this.echConfigList = echConfigList;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@ -369,6 +371,7 @@ class TlsStreamSettings extends CommonClass {
json.alpn, json.alpn,
json.fingerprint, json.fingerprint,
json.allowInsecure, json.allowInsecure,
json.echConfigList,
); );
} }
@ -378,6 +381,7 @@ class TlsStreamSettings extends CommonClass {
alpn: this.alpn, alpn: this.alpn,
fingerprint: this.fingerprint, fingerprint: this.fingerprint,
allowInsecure: this.allowInsecure, allowInsecure: this.allowInsecure,
echConfigList: this.echConfigList
}; };
} }
} }
@ -782,7 +786,8 @@ class Outbound extends CommonClass {
let alpn = url.searchParams.get('alpn'); let alpn = url.searchParams.get('alpn');
let allowInsecure = url.searchParams.get('allowInsecure'); let allowInsecure = url.searchParams.get('allowInsecure');
let sni = url.searchParams.get('sni') ?? ''; let sni = url.searchParams.get('sni') ?? '';
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1); let ech = url.searchParams.get('ech') ?? '';
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1, ech);
} }
if (security == 'reality') { if (security == 'reality') {

View file

@ -108,8 +108,8 @@ func (a *InboundController) addInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@ -126,8 +126,8 @@ func (a *InboundController) delInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@ -152,8 +152,8 @@ func (a *InboundController) updateInbound(c *gin.Context) {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, err) jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
if err == nil && needRestart { if needRestart {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
@ -342,25 +342,25 @@ func (a *InboundController) onlines(c *gin.Context) {
func (a *InboundController) updateClientTraffic(c *gin.Context) { func (a *InboundController) updateClientTraffic(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
// Define the request structure for traffic update // Define the request structure for traffic update
type TrafficUpdateRequest struct { type TrafficUpdateRequest struct {
Upload int64 `json:"upload"` Upload int64 `json:"upload"`
Download int64 `json:"download"` Download int64 `json:"download"`
} }
var request TrafficUpdateRequest var request TrafficUpdateRequest
err := c.ShouldBindJSON(&request) err := c.ShouldBindJSON(&request)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
return return
} }
err = a.inboundService.UpdateClientTrafficByEmail(email, request.Upload, request.Download) err = a.inboundService.UpdateClientTrafficByEmail(email, request.Upload, request.Download)
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return return
} }
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil) jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
} }

View file

@ -46,11 +46,13 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/installXray/:version", a.installXray) g.POST("/installXray/:version", a.installXray)
g.POST("/updateGeofile/:fileName", a.updateGeofile) g.POST("/updateGeofile/:fileName", a.updateGeofile)
g.POST("/logs/:count", a.getLogs) g.POST("/logs/:count", a.getLogs)
g.POST("/xraylogs/:count", a.getXrayLogs)
g.POST("/getConfigJson", a.getConfigJson) g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb) g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB) g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert) g.POST("/getNewX25519Cert", a.getNewX25519Cert)
g.POST("/getNewmldsa65", a.getNewmldsa65) g.POST("/getNewmldsa65", a.getNewmldsa65)
g.POST("/getNewEchCert", a.getNewEchCert)
} }
func (a *ServerController) refreshStatus() { func (a *ServerController) refreshStatus() {
@ -133,6 +135,12 @@ func (a *ServerController) getLogs(c *gin.Context) {
jsonObj(c, logs, nil) jsonObj(c, logs, nil)
} }
func (a *ServerController) getXrayLogs(c *gin.Context) {
count := c.Param("count")
logs := a.serverService.GetXrayLogs(count)
jsonObj(c, logs, nil)
}
func (a *ServerController) getConfigJson(c *gin.Context) { func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson() configJson, err := a.serverService.GetConfigJson()
if err != nil { if err != nil {
@ -208,3 +216,13 @@ func (a *ServerController) getNewmldsa65(c *gin.Context) {
} }
jsonObj(c, cert, nil) jsonObj(c, cert, nil)
} }
func (a *ServerController) getNewEchCert(c *gin.Context) {
sni := c.PostForm("sni")
cert, err := a.serverService.GetNewEchCert(sni)
if err != nil {
jsonMsg(c, "get ech certificate", err)
return
}
jsonObj(c, cert, nil)
}

View file

@ -44,7 +44,7 @@
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable"> <a-form-item v-if="client.email && app.subSettings?.enable">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">

View file

@ -6,6 +6,19 @@
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'> <a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input-number v-model.number="inbound.settings.port"></a-input-number> <a-input-number v-model.number="inbound.settings.port"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.portMap"}}'>
<a-button size="small" @click="inbound.settings.portMap.push({name: '', value: ''})">+</a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(pm, index) in inbound.settings.portMap">
<a-input style="width: 50%" v-model.trim="pm.name" placeholder='{{ i18n "pages.inbounds.port"}}'>
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="pm.value" placeholder='{{ i18n "pages.inbounds.targetAddress" }}'>
<a-button slot="addonAfter" size="small" @click="inbound.settings.portMap.splice(index,1)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'> <a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp,udp">TCP,UDP</a-select-option> <a-select-option value="tcp,udp">TCP,UDP</a-select-option>

View file

@ -106,6 +106,21 @@
<a-switch v-model="cert.buildChain"></a-switch> <a-switch v-model="cert.buildChain"></a-switch>
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label='ECH key'>
<a-input v-model="inbound.stream.tls.echServerKeys"></a-input>
</a-form-item>
<a-form-item label='ECH config'>
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
</a-form-item>
<a-form-item label='ECH force query'>
<a-select v-model="inbound.stream.tls.echForceQuery"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
</a-form-item>
</template> </template>
<!-- reality settings --> <!-- reality settings -->

View file

@ -167,7 +167,10 @@
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span> <span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
</a-col> </a-col>
<a-col> <a-col>
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-tag> <a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-icon>
</a-col>
<a-col>
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openXrayLogs()"></a-icon>
</a-col> </a-col>
</a-row> </a-row>
</span> </span>
@ -179,6 +182,10 @@
</template> </template>
</template> </template>
<template #actions> <template #actions>
<a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" :style="{ justifyContent: 'center' }">
<a-icon type="bars"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
</a-space>
<a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }"> <a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }">
<a-icon type="poweroff"></a-icon> <a-icon type="poweroff"></a-icon>
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span> <span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
@ -422,6 +429,40 @@
</a-form> </a-form>
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div> <div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div>
</a-modal> </a-modal>
<a-modal id="xraylog-modal"
v-model="xraylogModal.visible"
:closable="true" @cancel="() => xraylogModal.visible = false"
:class="themeSwitcher.currentTheme"
width="80vw"
footer="">
<template slot="title">
{{ i18n "pages.index.logs" }}
<a-icon :spin="xraylogModal.loading"
type="sync"
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
:disabled="xraylogModal.loading"
@click="openXrayLogs()">
</a-icon>
</template>
<a-form layout="inline">
<a-form-item :style="{ marginRight: '0.5rem' }">
<a-input-group compact>
<a-select size="small" v-model="xraylogModal.rows" :style="{ width: '70px' }"
@change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
<a-select-option value="100">100</a-select-option>
<a-select-option value="500">500</a-select-option>
</a-select>
</a-input-group>
</a-form-item>
<a-form-item :style="{ float: 'right' }">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
</a-form-item>
</a-form>
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="xraylogModal.formattedLogs"></div>
</a-modal>
<a-modal id="backup-modal" <a-modal id="backup-modal"
v-model="backupModal.visible" v-model="backupModal.visible"
title='{{ i18n "pages.index.backupTitle" }}' title='{{ i18n "pages.index.backupTitle" }}'
@ -606,6 +647,57 @@
}, },
}; };
const xraylogModal = {
visible: false,
logs: [],
rows: 20,
loading: false,
show(logs) {
this.visible = true;
this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = '';
logs.forEach((log, index) => {
if(index > 0) formattedLogs += '<br>';
const parts = log.split(' ');
if(parts.length === 9) {
const dateTime = `<b>${parts[0]} ${parts[1]}</b>`;
const from = `<b>${parts[3]}</b>`;
const to = `<b>${parts[5].replace(/^\/+/, "")}</b>`;
let outboundColor = '';
if (parts[8].startsWith('blocked')) {
outboundColor = ' style="color: #e04141;"';
}
else if (!parts[8].startsWith('direct')) {
outboundColor = ' style="color: #3c89e8;"';
}
formattedLogs += `<span${outboundColor}>
${dateTime}
${parts[2]}
${from}
${parts[4]}
${to}
${parts.slice(6).join(' ')}
</span>`;
} else {
formattedLogs += `<span>${parts.join(' ')}</span>`;
}
});
return formattedLogs;
},
hide() {
this.visible = false;
},
};
const backupModal = { const backupModal = {
visible: false, visible: false,
show() { show() {
@ -629,10 +721,12 @@
status: new Status(), status: new Status(),
versionModal, versionModal,
logModal, logModal,
xraylogModal,
backupModal, backupModal,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
showAlert: false, showAlert: false,
showIp: false showIp: false,
ipLimitEnable: false,
}, },
methods: { methods: {
loading(spinning, tip = '{{ i18n "loading"}}') { loading(spinning, tip = '{{ i18n "loading"}}') {
@ -721,6 +815,16 @@
await PromiseUtil.sleep(500); await PromiseUtil.sleep(500);
logModal.loading = false; logModal.loading = false;
}, },
async openXrayLogs(){
xraylogModal.loading = true;
const msg = await HttpUtil.post('server/xraylogs/'+xraylogModal.rows);
if (!msg.success) {
return;
}
xraylogModal.show(msg.obj);
await PromiseUtil.sleep(500);
xraylogModal.loading = false;
},
async openConfig() { async openConfig() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson'); const msg = await HttpUtil.post('server/getConfigJson');
@ -773,6 +877,12 @@
if (window.location.protocol !== "https:") { if (window.location.protocol !== "https:") {
this.showAlert = true; this.showAlert = true;
} }
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (msg.success) {
this.ipLimitEnable = msg.obj.ipLimitEnable;
}
while (true) { while (true) {
try { try {
await this.getStatus(); await this.getStatus();

View file

@ -39,7 +39,7 @@
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="app.subSettings.enable"> <a-form-item v-if="app.subSettings?.enable">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">

View file

@ -199,7 +199,7 @@
<a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag> <a-tag>[[ infoModal.clientSettings.limitIp ]]</a-tag>
</td> </td>
</tr> </tr>
<tr v-if="app.ipLimitEnable"> <tr v-if="app.ipLimitEnable && infoModal.clientSettings.limitIp > 0">
<td>{{ i18n "pages.inbounds.IPLimitlog" }}</td> <td>{{ i18n "pages.inbounds.IPLimitlog" }}</td>
<td> <td>
<a-tag>[[ infoModal.clientIps ]]</a-tag> <a-tag>[[ infoModal.clientIps ]]</a-tag>

View file

@ -152,6 +152,16 @@
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed; inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify; inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
}, },
async getNewEchCert() {
inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewEchCert', {sni: inModal.inbound.stream.tls.sni});
inModal.loading(false);
if (!msg.success) {
return;
}
inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
},
}, },
}); });

View file

@ -21,7 +21,7 @@
</a-space> </a-space>
</template> </template>
<tr-qr-modal class="qr-modal"> <tr-qr-modal class="qr-modal">
<template v-if="app.subSettings.enable && qrModal.subId"> <template v-if="app.subSettings?.enable && qrModal.subId">
<tr-qr-box class="qr-box"> <tr-qr-box class="qr-box">
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag> <a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}}</span></a-tag>
<tr-qr-bg class="qr-bg-sub"> <tr-qr-bg class="qr-bg-sub">

View file

@ -2,6 +2,7 @@ package service
import ( import (
"archive/zip" "archive/zip"
"bufio"
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -329,7 +330,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
continue continue
} }
if major > 25 || (major == 25 && minor > 7) || (major == 25 && minor == 7 && patch >= 26) { if major > 25 || (major == 25 && minor > 8) || (major == 25 && minor == 8 && patch >= 3) {
versions = append(versions, release.TagName) versions = append(versions, release.TagName)
} }
} }
@ -481,6 +482,37 @@ func (s *ServerService) GetLogs(count string, level string, syslog string) []str
return lines return lines
} }
func (s *ServerService) GetXrayLogs(count string) []string {
c, _ := strconv.Atoi(count)
var lines []string
pathToAccessLog, err := xray.GetAccessLogPath()
if err != nil {
return lines
}
file, err := os.Open(pathToAccessLog)
if err != nil {
return lines
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == "" || strings.Contains(line, "api -> api") {
continue
}
lines = append(lines, line)
}
if len(lines) > c {
lines = lines[len(lines)-c:]
}
return lines
}
func (s *ServerService) GetConfigJson() (any, error) { func (s *ServerService) GetConfigJson() (any, error) {
config, err := s.xrayService.GetXrayConfig() config, err := s.xrayService.GetXrayConfig()
if err != nil { if err != nil {
@ -743,3 +775,27 @@ func (s *ServerService) GetNewmldsa65() (any, error) {
return keyPair, nil return keyPair, nil
} }
func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
if len(lines) < 4 {
return nil, common.NewError("invalid ech cert")
}
configList := lines[1]
serverKeys := lines[3]
return map[string]interface{}{
"echServerKeys": serverKeys,
"echConfigList": configList,
}, nil
}

View file

@ -158,6 +158,7 @@
"remark" = "ملاحظة" "remark" = "ملاحظة"
"protocol" = "بروتوكول" "protocol" = "بروتوكول"
"port" = "بورت" "port" = "بورت"
"portMap" = "خريطة البورت"
"traffic" = "الترافيك" "traffic" = "الترافيك"
"details" = "تفاصيل" "details" = "تفاصيل"
"transportConfig" = "نقل" "transportConfig" = "نقل"

View file

@ -158,6 +158,7 @@
"remark" = "Remark" "remark" = "Remark"
"protocol" = "Protocol" "protocol" = "Protocol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Details" "details" = "Details"
"transportConfig" = "Transport" "transportConfig" = "Transport"

View file

@ -158,6 +158,7 @@
"remark" = "Notas" "remark" = "Notas"
"protocol" = "Protocolo" "protocol" = "Protocolo"
"port" = "Puerto" "port" = "Puerto"
"portMap" = "Puertos de Destino"
"traffic" = "Tráfico" "traffic" = "Tráfico"
"details" = "Detalles" "details" = "Detalles"
"transportConfig" = "Transporte" "transportConfig" = "Transporte"

View file

@ -158,6 +158,7 @@
"remark" = "نام" "remark" = "نام"
"protocol" = "پروتکل" "protocol" = "پروتکل"
"port" = "پورت" "port" = "پورت"
"portMap" = "پورت‌های نظیر"
"traffic" = "ترافیک" "traffic" = "ترافیک"
"details" = "توضیحات" "details" = "توضیحات"
"transportConfig" = "نحوه اتصال" "transportConfig" = "نحوه اتصال"

View file

@ -158,6 +158,7 @@
"remark" = "Catatan" "remark" = "Catatan"
"protocol" = "Protokol" "protocol" = "Protokol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Rincian" "details" = "Rincian"
"transportConfig" = "Transport" "transportConfig" = "Transport"

View file

@ -158,6 +158,7 @@
"remark" = "備考" "remark" = "備考"
"protocol" = "プロトコル" "protocol" = "プロトコル"
"port" = "ポート" "port" = "ポート"
"portMap" = "ポートマッピング"
"traffic" = "トラフィック" "traffic" = "トラフィック"
"details" = "詳細情報" "details" = "詳細情報"
"transportConfig" = "トランスポート設定" "transportConfig" = "トランスポート設定"

View file

@ -158,6 +158,7 @@
"remark" = "Observação" "remark" = "Observação"
"protocol" = "Protocolo" "protocol" = "Protocolo"
"port" = "Porta" "port" = "Porta"
"portMap" = "Porta Mapeada"
"traffic" = "Tráfego" "traffic" = "Tráfego"
"details" = "Detalhes" "details" = "Detalhes"
"transportConfig" = "Transporte" "transportConfig" = "Transporte"

View file

@ -158,6 +158,7 @@
"remark" = "Примечание" "remark" = "Примечание"
"protocol" = "Протокол" "protocol" = "Протокол"
"port" = "Порт" "port" = "Порт"
"portMap" = "Порт-маппинг"
"traffic" = "Трафик" "traffic" = "Трафик"
"details" = "Подробнее" "details" = "Подробнее"
"transportConfig" = "Транспорт" "transportConfig" = "Транспорт"

View file

@ -158,6 +158,7 @@
"remark" = "Açıklama" "remark" = "Açıklama"
"protocol" = "Protokol" "protocol" = "Protokol"
"port" = "Port" "port" = "Port"
"portMap" = "Port Atama"
"traffic" = "Trafik" "traffic" = "Trafik"
"details" = "Detaylar" "details" = "Detaylar"
"transportConfig" = "Taşıma" "transportConfig" = "Taşıma"

View file

@ -158,6 +158,7 @@
"remark" = "Примітка" "remark" = "Примітка"
"protocol" = "Протокол" "protocol" = "Протокол"
"port" = "Порт" "port" = "Порт"
"portMap" = "Порт-перехід"
"traffic" = "Трафік" "traffic" = "Трафік"
"details" = "Деталі" "details" = "Деталі"
"transportConfig" = "Транспорт" "transportConfig" = "Транспорт"

View file

@ -158,6 +158,7 @@
"remark" = "Chú thích" "remark" = "Chú thích"
"protocol" = "Giao thức" "protocol" = "Giao thức"
"port" = "Cổng" "port" = "Cổng"
"portMap" = "Cổng tạo"
"traffic" = "Lưu lượng" "traffic" = "Lưu lượng"
"details" = "Chi tiết" "details" = "Chi tiết"
"transportConfig" = "Giao vận" "transportConfig" = "Giao vận"

View file

@ -158,6 +158,7 @@
"remark" = "备注" "remark" = "备注"
"protocol" = "协议" "protocol" = "协议"
"port" = "端口" "port" = "端口"
"portMap" = "端口映射"
"traffic" = "流量" "traffic" = "流量"
"details" = "详细信息" "details" = "详细信息"
"transportConfig" = "传输配置" "transportConfig" = "传输配置"

View file

@ -158,6 +158,7 @@
"remark" = "備註" "remark" = "備註"
"protocol" = "協議" "protocol" = "協議"
"port" = "埠" "port" = "埠"
"portMap" = "埠映射"
"traffic" = "流量" "traffic" = "流量"
"details" = "詳細資訊" "details" = "詳細資訊"
"transportConfig" = "傳輸配置" "transportConfig" = "傳輸配置"