mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-29 19:32:51 +00:00
Compare commits
11 commits
3c1634ca7c
...
6a17285935
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a17285935 | ||
|
|
4b03e9d919 | ||
|
|
7e9c3bdbaf | ||
|
|
957f3dbb54 | ||
|
|
05e60af283 | ||
|
|
5e40458116 | ||
|
|
baf6fdd29d | ||
|
|
45f78d3521 | ||
|
|
01f984e054 | ||
|
|
e4ba5ba53a | ||
|
|
6ff555c8bb |
30 changed files with 288 additions and 36 deletions
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
2.6.3
|
2.6.5
|
||||||
13
install.sh
13
install.sh
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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') {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@
|
||||||
"remark" = "ملاحظة"
|
"remark" = "ملاحظة"
|
||||||
"protocol" = "بروتوكول"
|
"protocol" = "بروتوكول"
|
||||||
"port" = "بورت"
|
"port" = "بورت"
|
||||||
|
"portMap" = "خريطة البورت"
|
||||||
"traffic" = "الترافيك"
|
"traffic" = "الترافيك"
|
||||||
"details" = "تفاصيل"
|
"details" = "تفاصيل"
|
||||||
"transportConfig" = "نقل"
|
"transportConfig" = "نقل"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@
|
||||||
"remark" = "نام"
|
"remark" = "نام"
|
||||||
"protocol" = "پروتکل"
|
"protocol" = "پروتکل"
|
||||||
"port" = "پورت"
|
"port" = "پورت"
|
||||||
|
"portMap" = "پورتهای نظیر"
|
||||||
"traffic" = "ترافیک"
|
"traffic" = "ترافیک"
|
||||||
"details" = "توضیحات"
|
"details" = "توضیحات"
|
||||||
"transportConfig" = "نحوه اتصال"
|
"transportConfig" = "نحوه اتصال"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@
|
||||||
"remark" = "備考"
|
"remark" = "備考"
|
||||||
"protocol" = "プロトコル"
|
"protocol" = "プロトコル"
|
||||||
"port" = "ポート"
|
"port" = "ポート"
|
||||||
|
"portMap" = "ポートマッピング"
|
||||||
"traffic" = "トラフィック"
|
"traffic" = "トラフィック"
|
||||||
"details" = "詳細情報"
|
"details" = "詳細情報"
|
||||||
"transportConfig" = "トランスポート設定"
|
"transportConfig" = "トランスポート設定"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@
|
||||||
"remark" = "Примечание"
|
"remark" = "Примечание"
|
||||||
"protocol" = "Протокол"
|
"protocol" = "Протокол"
|
||||||
"port" = "Порт"
|
"port" = "Порт"
|
||||||
|
"portMap" = "Порт-маппинг"
|
||||||
"traffic" = "Трафик"
|
"traffic" = "Трафик"
|
||||||
"details" = "Подробнее"
|
"details" = "Подробнее"
|
||||||
"transportConfig" = "Транспорт"
|
"transportConfig" = "Транспорт"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@
|
||||||
"remark" = "Примітка"
|
"remark" = "Примітка"
|
||||||
"protocol" = "Протокол"
|
"protocol" = "Протокол"
|
||||||
"port" = "Порт"
|
"port" = "Порт"
|
||||||
|
"portMap" = "Порт-перехід"
|
||||||
"traffic" = "Трафік"
|
"traffic" = "Трафік"
|
||||||
"details" = "Деталі"
|
"details" = "Деталі"
|
||||||
"transportConfig" = "Транспорт"
|
"transportConfig" = "Транспорт"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@
|
||||||
"remark" = "备注"
|
"remark" = "备注"
|
||||||
"protocol" = "协议"
|
"protocol" = "协议"
|
||||||
"port" = "端口"
|
"port" = "端口"
|
||||||
|
"portMap" = "端口映射"
|
||||||
"traffic" = "流量"
|
"traffic" = "流量"
|
||||||
"details" = "详细信息"
|
"details" = "详细信息"
|
||||||
"transportConfig" = "传输配置"
|
"transportConfig" = "传输配置"
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@
|
||||||
"remark" = "備註"
|
"remark" = "備註"
|
||||||
"protocol" = "協議"
|
"protocol" = "協議"
|
||||||
"port" = "埠"
|
"port" = "埠"
|
||||||
|
"portMap" = "埠映射"
|
||||||
"traffic" = "流量"
|
"traffic" = "流量"
|
||||||
"details" = "詳細資訊"
|
"details" = "詳細資訊"
|
||||||
"transportConfig" = "傳輸配置"
|
"transportConfig" = "傳輸配置"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue