mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-13 13:57:59 +00:00
Add url speed test for outbound (#3767)
* add outbound testing functionality with configurable test URL * use no kernel tun for conflict errors
This commit is contained in:
parent
4a455aa532
commit
4779939424
22 changed files with 746 additions and 102 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
@ -34,9 +37,10 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/warp/:action", a.warp)
|
g.POST("/warp/:action", a.warp)
|
||||||
g.POST("/update", a.updateSetting)
|
g.POST("/update", a.updateSetting)
|
||||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||||
|
g.POST("/testOutbound", a.testOutbound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getXraySetting retrieves the Xray configuration template and inbound tags.
|
// getXraySetting retrieves the Xray configuration template, inbound tags, and outbound test URL.
|
||||||
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||||
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
|
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -48,15 +52,28 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + " }"
|
outboundTestUrl, _ := a.SettingService.GetXrayOutboundTestUrl()
|
||||||
|
if outboundTestUrl == "" {
|
||||||
|
outboundTestUrl = "https://www.google.com/generate_204"
|
||||||
|
}
|
||||||
|
urlJSON, _ := json.Marshal(outboundTestUrl)
|
||||||
|
xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + ", \"outboundTestUrl\": " + string(urlJSON) + " }"
|
||||||
jsonObj(c, xrayResponse, nil)
|
jsonObj(c, xrayResponse, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateSetting updates the Xray configuration settings.
|
// updateSetting updates the Xray configuration settings.
|
||||||
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||||
xraySetting := c.PostForm("xraySetting")
|
xraySetting := c.PostForm("xraySetting")
|
||||||
err := a.XraySettingService.SaveXraySetting(xraySetting)
|
if err := a.XraySettingService.SaveXraySetting(xraySetting); err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
outboundTestUrl := c.PostForm("outboundTestUrl")
|
||||||
|
if outboundTestUrl == "" {
|
||||||
|
outboundTestUrl = "https://www.google.com/generate_204"
|
||||||
|
}
|
||||||
|
_ = a.SettingService.SetXrayOutboundTestUrl(outboundTestUrl)
|
||||||
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDefaultXrayConfig retrieves the default Xray configuration.
|
// getDefaultXrayConfig retrieves the default Xray configuration.
|
||||||
|
|
@ -118,3 +135,24 @@ func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
|
||||||
}
|
}
|
||||||
jsonObj(c, "", nil)
|
jsonObj(c, "", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testOutbound tests an outbound configuration and returns the delay/response time.
|
||||||
|
// Optional form "allOutbounds": JSON array of all outbounds; used to resolve sockopt.dialerProxy dependencies.
|
||||||
|
func (a *XraySettingController) testOutbound(c *gin.Context) {
|
||||||
|
outboundJSON := c.PostForm("outbound")
|
||||||
|
testURL := c.PostForm("testURL")
|
||||||
|
allOutboundsJSON := c.PostForm("allOutbounds")
|
||||||
|
|
||||||
|
if outboundJSON == "" {
|
||||||
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("outbound parameter is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := a.OutboundService.TestOutbound(outboundJSON, testURL, allOutboundsJSON)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonObj(c, result, nil)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,22 @@
|
||||||
<a-row :xs="24" :sm="24" :lg="12">
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
||||||
<template slot="message">
|
<template slot="message">
|
||||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
<a-icon type="exclamation-circle" theme="filled"
|
||||||
|
:style="{ color: '#FFA031' }"></a-icon>
|
||||||
<span>{{ i18n "pages.xray.generalConfigsDesc" }}</span>
|
<span>{{ i18n "pages.xray.generalConfigsDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.FreedomStrategy" }}</template>
|
<template #title>{{ i18n "pages.xray.FreedomStrategy" }}</template>
|
||||||
<template #description>{{ i18n "pages.xray.FreedomStrategyDesc" }}</template>
|
<template #description>{{ i18n "pages.xray.FreedomStrategyDesc"
|
||||||
|
}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select v-model="freedomStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
|
<a-select v-model="freedomStrategy"
|
||||||
|
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
:style="{ width: '100%' }">
|
:style="{ width: '100%' }">
|
||||||
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">
|
<a-select-option v-for="s in OutboundDomainStrategies"
|
||||||
|
:value="s">
|
||||||
<span>[[ s ]]</span>
|
<span>[[ s ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
|
@ -23,42 +27,63 @@
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.RoutingStrategy" }}</template>
|
<template #title>{{ i18n "pages.xray.RoutingStrategy" }}</template>
|
||||||
<template #description>{{ i18n "pages.xray.RoutingStrategyDesc" }}</template>
|
<template #description>{{ i18n "pages.xray.RoutingStrategyDesc"
|
||||||
|
}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select v-model="routingStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
|
<a-select v-model="routingStrategy"
|
||||||
|
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
:style="{ width: '100%' }">
|
:style="{ width: '100%' }">
|
||||||
<a-select-option v-for="s in routingDomainStrategies" :value="s">
|
<a-select-option v-for="s in routingDomainStrategies"
|
||||||
|
:value="s">
|
||||||
<span>[[ s ]]</span>
|
<span>[[ s ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
|
<a-setting-list-item paddings="small">
|
||||||
|
<template #title>{{ i18n "pages.xray.outboundTestUrl" }}</template>
|
||||||
|
<template #description>{{ i18n "pages.xray.outboundTestUrlDesc"
|
||||||
|
}}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-input v-model="outboundTestUrl"
|
||||||
|
:placeholder="'https://www.google.com/generate_204'"
|
||||||
|
:style="{ width: '100%' }"></a-input>
|
||||||
|
</template>
|
||||||
|
</a-setting-list-item>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
<a-collapse-panel key="2" header='{{ i18n "pages.xray.statistics" }}'>
|
<a-collapse-panel key="2" header='{{ i18n "pages.xray.statistics" }}'>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.statsInboundUplink" }}</template>
|
<template #title>{{ i18n "pages.xray.statsInboundUplink"
|
||||||
<template #description>{{ i18n "pages.xray.statsInboundUplinkDesc" }}</template>
|
}}</template>
|
||||||
|
<template #description>{{ i18n "pages.xray.statsInboundUplinkDesc"
|
||||||
|
}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-switch v-model="statsInboundUplink"></a-switch>
|
<a-switch v-model="statsInboundUplink"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.statsInboundDownlink" }}</template>
|
<template #title>{{ i18n "pages.xray.statsInboundDownlink"
|
||||||
<template #description>{{ i18n "pages.xray.statsInboundDownlinkDesc" }}</template>
|
}}</template>
|
||||||
|
<template #description>{{ i18n "pages.xray.statsInboundDownlinkDesc"
|
||||||
|
}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-switch v-model="statsInboundDownlink"></a-switch>
|
<a-switch v-model="statsInboundDownlink"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.statsOutboundUplink" }}</template>
|
<template #title>{{ i18n "pages.xray.statsOutboundUplink"
|
||||||
<template #description>{{ i18n "pages.xray.statsOutboundUplinkDesc" }}</template>
|
}}</template>
|
||||||
|
<template #description>{{ i18n "pages.xray.statsOutboundUplinkDesc"
|
||||||
|
}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-switch v-model="statsOutboundUplink"></a-switch>
|
<a-switch v-model="statsOutboundUplink"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.statsOutboundDownlink" }}</template>
|
<template #title>{{ i18n "pages.xray.statsOutboundDownlink"
|
||||||
<template #description>{{ i18n "pages.xray.statsOutboundDownlinkDesc" }}</template>
|
}}</template>
|
||||||
|
<template #description>{{ i18n
|
||||||
|
"pages.xray.statsOutboundDownlinkDesc" }}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-switch v-model="statsOutboundDownlink"></a-switch>
|
<a-switch v-model="statsOutboundDownlink"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -68,16 +93,20 @@
|
||||||
<a-row :xs="24" :sm="24" :lg="12">
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
||||||
<template slot="message">
|
<template slot="message">
|
||||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
<a-icon type="exclamation-circle" theme="filled"
|
||||||
|
:style="{ color: '#FFA031' }"></a-icon>
|
||||||
<span>{{ i18n "pages.xray.logConfigsDesc" }}</span>
|
<span>{{ i18n "pages.xray.logConfigsDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.logLevel" }}</template>
|
<template #title>{{ i18n "pages.xray.logLevel" }}</template>
|
||||||
<template #description>{{ i18n "pages.xray.logLevelDesc" }}</template>
|
<template #description>{{ i18n "pages.xray.logLevelDesc"
|
||||||
|
}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select v-model="logLevel" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }">
|
<a-select v-model="logLevel"
|
||||||
|
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
|
:style="{ width: '100%' }">
|
||||||
<a-select-option v-for="s in log.loglevel" :value="s">
|
<a-select-option v-for="s in log.loglevel" :value="s">
|
||||||
<span>[[ s ]]</span>
|
<span>[[ s ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
|
|
@ -86,10 +115,13 @@
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.accessLog" }}</template>
|
<template #title>{{ i18n "pages.xray.accessLog" }}</template>
|
||||||
<template #description>{{ i18n "pages.xray.accessLogDesc" }}</template>
|
<template #description>{{ i18n "pages.xray.accessLogDesc"
|
||||||
|
}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select v-model="accessLog" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }">
|
<a-select v-model="accessLog"
|
||||||
<a-select-option value=''>
|
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
|
:style="{ width: '100%' }">
|
||||||
|
<a-select-option value>
|
||||||
<span>Empty</span>
|
<span>Empty</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
<a-select-option v-for="s in log.access" :value="s">
|
<a-select-option v-for="s in log.access" :value="s">
|
||||||
|
|
@ -100,10 +132,13 @@
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.errorLog" }}</template>
|
<template #title>{{ i18n "pages.xray.errorLog" }}</template>
|
||||||
<template #description>{{ i18n "pages.xray.errorLogDesc" }}</template>
|
<template #description>{{ i18n "pages.xray.errorLogDesc"
|
||||||
|
}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select v-model="errorLog" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }">
|
<a-select v-model="errorLog"
|
||||||
<a-select-option value=''>
|
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
|
:style="{ width: '100%' }">
|
||||||
|
<a-select-option value>
|
||||||
<span>Empty</span>
|
<span>Empty</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
<a-select-option v-for="s in log.error" :value="s">
|
<a-select-option v-for="s in log.error" :value="s">
|
||||||
|
|
@ -114,11 +149,13 @@
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.maskAddress" }}</template>
|
<template #title>{{ i18n "pages.xray.maskAddress" }}</template>
|
||||||
<template #description>{{ i18n "pages.xray.maskAddressDesc" }}</template>
|
<template #description>{{ i18n "pages.xray.maskAddressDesc"
|
||||||
|
}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select v-model="maskAddressLog" :dropdown-class-name="themeSwitcher.currentTheme"
|
<a-select v-model="maskAddressLog"
|
||||||
|
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
:style="{ width: '100%' }">
|
:style="{ width: '100%' }">
|
||||||
<a-select-option value=''>
|
<a-select-option value>
|
||||||
<span>Empty</span>
|
<span>Empty</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
<a-select-option v-for="s in log.maskAddress" :value="s">
|
<a-select-option v-for="s in log.maskAddress" :value="s">
|
||||||
|
|
@ -139,7 +176,8 @@
|
||||||
<a-row :xs="24" :sm="24" :lg="12">
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
||||||
<template slot="message">
|
<template slot="message">
|
||||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
<a-icon type="exclamation-circle" theme="filled"
|
||||||
|
:style="{ color: '#FFA031' }"></a-icon>
|
||||||
<span>{{ i18n "pages.xray.blockConfigsDesc" }}</span>
|
<span>{{ i18n "pages.xray.blockConfigsDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
|
|
@ -153,17 +191,21 @@
|
||||||
<a-row :xs="24" :sm="24" :lg="12">
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
<a-alert type="warning" :style="{ textAlign: 'center' }">
|
||||||
<template slot="message">
|
<template slot="message">
|
||||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
<a-icon type="exclamation-circle" theme="filled"
|
||||||
<span>{{ i18n "pages.xray.blockConnectionsConfigsDesc" }}</span>
|
:style="{ color: '#FFA031' }"></a-icon>
|
||||||
|
<span>{{ i18n "pages.xray.blockConnectionsConfigsDesc"
|
||||||
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.blockips" }}</template>
|
<template #title>{{ i18n "pages.xray.blockips" }}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select mode="tags" v-model="blockedIPs" :style="{ width: '100%' }"
|
<a-select mode="tags" v-model="blockedIPs"
|
||||||
|
:style="{ width: '100%' }"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
|
<a-select-option :value="p.value" :label="p.label"
|
||||||
|
v-for="p in settingsData.IPsOptions">
|
||||||
<span>[[ p.label ]]</span>
|
<span>[[ p.label ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
|
@ -172,28 +214,35 @@
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.blockdomains" }}</template>
|
<template #title>{{ i18n "pages.xray.blockdomains" }}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select mode="tags" v-model="blockedDomains" :style="{ width: '100%' }"
|
<a-select mode="tags" v-model="blockedDomains"
|
||||||
|
:style="{ width: '100%' }"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.BlockDomainsOptions">
|
<a-select-option :value="p.value" :label="p.label"
|
||||||
|
v-for="p in settingsData.BlockDomainsOptions">
|
||||||
<span>[[ p.label ]]</span>
|
<span>[[ p.label ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-row :xs="24" :sm="24" :lg="12">
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
<a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
|
<a-alert type="warning"
|
||||||
|
:style="{ textAlign: 'center', marginTop: '20px' }">
|
||||||
<template slot="message">
|
<template slot="message">
|
||||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
<a-icon type="exclamation-circle" theme="filled"
|
||||||
<span>{{ i18n "pages.xray.directConnectionsConfigsDesc" }}</span>
|
:style="{ color: '#FFA031' }"></a-icon>
|
||||||
|
<span>{{ i18n "pages.xray.directConnectionsConfigsDesc"
|
||||||
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.directips" }}</template>
|
<template #title>{{ i18n "pages.xray.directips" }}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select mode="tags" :style="{ width: '100%' }" v-model="directIPs"
|
<a-select mode="tags" :style="{ width: '100%' }"
|
||||||
|
v-model="directIPs"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
|
<a-select-option :value="p.value" :label="p.label"
|
||||||
|
v-for="p in settingsData.IPsOptions">
|
||||||
<span>[[ p.label ]]</span>
|
<span>[[ p.label ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
|
@ -202,18 +251,22 @@
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.directdomains" }}</template>
|
<template #title>{{ i18n "pages.xray.directdomains" }}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select mode="tags" :style="{ width: '100%' }" v-model="directDomains"
|
<a-select mode="tags" :style="{ width: '100%' }"
|
||||||
|
v-model="directDomains"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.DomainsOptions">
|
<a-select-option :value="p.value" :label="p.label"
|
||||||
|
v-for="p in settingsData.DomainsOptions">
|
||||||
<span>[[ p.label ]]</span>
|
<span>[[ p.label ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-row :xs="24" :sm="24" :lg="12">
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
<a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
|
<a-alert type="warning"
|
||||||
|
:style="{ textAlign: 'center', marginTop: '20px' }">
|
||||||
<template slot="message">
|
<template slot="message">
|
||||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
<a-icon type="exclamation-circle" theme="filled"
|
||||||
|
:style="{ color: '#FFA031' }"></a-icon>
|
||||||
<span>{{ i18n "pages.xray.ipv4RoutingDesc" }}</span>
|
<span>{{ i18n "pages.xray.ipv4RoutingDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
|
|
@ -221,18 +274,22 @@
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.xray.ipv4Routing" }}</template>
|
<template #title>{{ i18n "pages.xray.ipv4Routing" }}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-select mode="tags" :style="{ width: '100%' }" v-model="ipv4Domains"
|
<a-select mode="tags" :style="{ width: '100%' }"
|
||||||
|
v-model="ipv4Domains"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
|
<a-select-option :value="p.value" :label="p.label"
|
||||||
|
v-for="p in settingsData.ServicesOptions">
|
||||||
<span>[[ p.label ]]</span>
|
<span>[[ p.label ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-row :xs="24" :sm="24" :lg="12">
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
<a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
|
<a-alert type="warning"
|
||||||
|
:style="{ textAlign: 'center', marginTop: '20px' }">
|
||||||
<template slot="message">
|
<template slot="message">
|
||||||
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
|
<a-icon type="exclamation-circle" theme="filled"
|
||||||
|
:style="{ color: '#FFA031' }"></a-icon>
|
||||||
{{ i18n "pages.xray.warpRoutingDesc" }}
|
{{ i18n "pages.xray.warpRoutingDesc" }}
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
|
|
@ -241,20 +298,24 @@
|
||||||
<template #title>{{ i18n "pages.xray.warpRouting" }}</template>
|
<template #title>{{ i18n "pages.xray.warpRouting" }}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<template v-if="WarpExist">
|
<template v-if="WarpExist">
|
||||||
<a-select mode="tags" :style="{ width: '100%' }" v-model="warpDomains"
|
<a-select mode="tags" :style="{ width: '100%' }"
|
||||||
|
v-model="warpDomains"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
|
<a-select-option :value="p.value" :label="p.label"
|
||||||
|
v-for="p in settingsData.ServicesOptions">
|
||||||
<span>[[ p.label ]]</span>
|
<span>[[ p.label ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
|
<a-button type="primary" icon="cloud"
|
||||||
|
@click="showWarp()">WARP</a-button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
<a-collapse-panel key="6" header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
|
<a-collapse-panel key="6"
|
||||||
|
header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
|
||||||
<a-space direction="horizontal" :style="{ padding: '0 20px' }">
|
<a-space direction="horizontal" :style="{ padding: '0 20px' }">
|
||||||
<a-button type="danger" @click="resetXrayConfigToDefault">
|
<a-button type="danger" @click="resetXrayConfigToDefault">
|
||||||
<span>{{ i18n "pages.settings.resetDefaultConfig" }}</span>
|
<span>{{ i18n "pages.settings.resetDefaultConfig" }}</span>
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,22 @@
|
||||||
<a-col :xs="12" :sm="12" :lg="12">
|
<a-col :xs="12" :sm="12" :lg="12">
|
||||||
<a-space direction="horizontal" size="small">
|
<a-space direction="horizontal" size="small">
|
||||||
<a-button type="primary" icon="plus" @click="addOutbound">
|
<a-button type="primary" icon="plus" @click="addOutbound">
|
||||||
<span v-if="!isMobile">{{ i18n "pages.xray.outbound.addOutbound" }}</span>
|
<span v-if="!isMobile">{{ i18n
|
||||||
|
"pages.xray.outbound.addOutbound" }}</span>
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
|
<a-button type="primary" icon="cloud"
|
||||||
|
@click="showWarp()">WARP</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }">
|
<a-col :xs="12" :sm="12" :lg="12" :style="{ textAlign: 'right' }">
|
||||||
<a-button-group>
|
<a-button-group>
|
||||||
<a-button icon="sync" @click="refreshOutboundTraffic()" :loading="refreshing"></a-button>
|
<a-button icon="sync" @click="refreshOutboundTraffic()"
|
||||||
<a-popconfirm placement="topRight" @confirm="resetOutboundTraffic(-1)"
|
:loading="refreshing"></a-button>
|
||||||
|
<a-popconfirm placement="topRight"
|
||||||
|
@confirm="resetOutboundTraffic(-1)"
|
||||||
title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
|
title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
|
||||||
:overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}'
|
:overlay-class-name="themeSwitcher.currentTheme"
|
||||||
|
ok-text='{{ i18n "reset"}}'
|
||||||
cancel-text='{{ i18n "cancel"}}'>
|
cancel-text='{{ i18n "cancel"}}'>
|
||||||
<a-icon slot="icon" type="question-circle-o"
|
<a-icon slot="icon" type="question-circle-o"
|
||||||
:style="{ color: themeSwitcher.isDarkTheme ? '#008771' : '#008771' }"></a-icon>
|
:style="{ color: themeSwitcher.isDarkTheme ? '#008771' : '#008771' }"></a-icon>
|
||||||
|
|
@ -23,8 +28,10 @@
|
||||||
</a-button-group>
|
</a-button-group>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-table :columns="outboundColumns" bordered :row-key="r => r.key" :data-source="outboundData"
|
<a-table :columns="outboundColumns" bordered :row-key="r => r.key"
|
||||||
:scroll="isMobile ? {} : { x: 800 }" :pagination="false" :indent-size="0"
|
:data-source="outboundData"
|
||||||
|
:scroll="isMobile ? {} : { x: 800 }" :pagination="false"
|
||||||
|
:indent-size="0"
|
||||||
:locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
|
:locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
|
||||||
<template slot="action" slot-scope="text, outbound, index">
|
<template slot="action" slot-scope="text, outbound, index">
|
||||||
<span>[[ index+1 ]]</span>
|
<span>[[ index+1 ]]</span>
|
||||||
|
|
@ -32,7 +39,8 @@
|
||||||
<a-icon @click="e => e.preventDefault()" type="more"
|
<a-icon @click="e => e.preventDefault()" type="more"
|
||||||
:style="{ fontSize: '16px', textDecoration: 'bold' }"></a-icon>
|
:style="{ fontSize: '16px', textDecoration: 'bold' }"></a-icon>
|
||||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||||
<a-menu-item v-if="index>0" @click="setFirstOutbound(index)">
|
<a-menu-item v-if="index>0"
|
||||||
|
@click="setFirstOutbound(index)">
|
||||||
<a-icon type="vertical-align-top"></a-icon>
|
<a-icon type="vertical-align-top"></a-icon>
|
||||||
<span>{{ i18n "pages.xray.rules.first"}}</span>
|
<span>{{ i18n "pages.xray.rules.first"}}</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
|
@ -56,21 +64,64 @@
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</template>
|
</template>
|
||||||
<template slot="address" slot-scope="text, outbound, index">
|
<template slot="address" slot-scope="text, outbound, index">
|
||||||
<p :style="{ margin: '0 5px' }" v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
|
<p :style="{ margin: '0 5px' }"
|
||||||
|
v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
|
||||||
</template>
|
</template>
|
||||||
<template slot="protocol" slot-scope="text, outbound, index">
|
<template slot="protocol" slot-scope="text, outbound, index">
|
||||||
<a-tag :style="{ margin: '0' }" color="purple">[[ outbound.protocol ]]</a-tag>
|
<a-tag :style="{ margin: '0' }" color="purple">[[ outbound.protocol
|
||||||
|
]]</a-tag>
|
||||||
<template
|
<template
|
||||||
v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||||
<a-tag :style="{ margin: '0' }" color="blue">[[ outbound.streamSettings.network ]]</a-tag>
|
<a-tag :style="{ margin: '0' }" color="blue">[[
|
||||||
<a-tag :style="{ margin: '0' }" v-if="outbound.streamSettings.security=='tls'" color="green">tls</a-tag>
|
outbound.streamSettings.network ]]</a-tag>
|
||||||
<a-tag :style="{ margin: '0' }" v-if="outbound.streamSettings.security=='reality'"
|
<a-tag :style="{ margin: '0' }"
|
||||||
|
v-if="outbound.streamSettings.security=='tls'"
|
||||||
|
color="green">tls</a-tag>
|
||||||
|
<a-tag :style="{ margin: '0' }"
|
||||||
|
v-if="outbound.streamSettings.security=='reality'"
|
||||||
color="green">reality</a-tag>
|
color="green">reality</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template slot="traffic" slot-scope="text, outbound, index">
|
<template slot="traffic" slot-scope="text, outbound, index">
|
||||||
<a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag>
|
<a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
<template slot="test" slot-scope="text, outbound, index">
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">{{ i18n "pages.xray.outbound.test"
|
||||||
|
}}</template>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
icon="thunderbolt"
|
||||||
|
:loading="outboundTestStates[index] && outboundTestStates[index].testing"
|
||||||
|
@click="testOutbound(index)"
|
||||||
|
:disabled="(outbound.protocol === 'blackhole' || outbound.tag === 'blocked') || (outboundTestStates[index] && outboundTestStates[index].testing)">
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<template slot="testResult" slot-scope="text, outbound, index">
|
||||||
|
<div
|
||||||
|
v-if="outboundTestStates[index] && outboundTestStates[index].result">
|
||||||
|
<a-tag v-if="outboundTestStates[index].result.success"
|
||||||
|
color="green">
|
||||||
|
[[ outboundTestStates[index].result.delay ]]ms
|
||||||
|
<span v-if="outboundTestStates[index].result.statusCode">
|
||||||
|
([[ outboundTestStates[index].result.statusCode
|
||||||
|
]])</span>
|
||||||
|
</a-tag>
|
||||||
|
<a-tooltip v-else
|
||||||
|
:title="outboundTestStates[index].result.error">
|
||||||
|
<a-tag color="red">
|
||||||
|
Failed
|
||||||
|
</a-tag>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-else-if="outboundTestStates[index] && outboundTestStates[index].testing">
|
||||||
|
<a-icon type="loading" />
|
||||||
|
</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-space>
|
</a-space>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
{{ template "page/head_start" .}}
|
{{ template "page/head_start" .}}
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}">
|
<link rel="stylesheet"
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
|
href="{{ .base_path }}assets/codemirror/codemirror.min.css?{{ .cur_ver }}">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
|
<link rel="stylesheet"
|
||||||
|
href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
|
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
|
||||||
{{ template "page/head_end" .}}
|
{{ template "page/head_end" .}}
|
||||||
|
|
||||||
|
|
@ -10,10 +13,13 @@
|
||||||
<a-sidebar></a-sidebar>
|
<a-sidebar></a-sidebar>
|
||||||
<a-layout id="content-layout">
|
<a-layout id="content-layout">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
<a-spin :spinning="loadingStates.spinning" :delay="500"
|
||||||
|
tip='{{ i18n "loading"}}'>
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
<a-alert type="error" v-if="showAlert && loadingStates.fetched"
|
||||||
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
:style="{ marginBottom: '10px' }"
|
||||||
|
message='{{ i18n "secAlertTitle" }}' color="red"
|
||||||
|
description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
|
|
@ -26,19 +32,25 @@
|
||||||
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
||||||
<a-col>
|
<a-col>
|
||||||
<a-card hoverable>
|
<a-card hoverable>
|
||||||
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
|
<a-row
|
||||||
|
:style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
|
||||||
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
|
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
|
||||||
<a-space direction="horizontal">
|
<a-space direction="horizontal">
|
||||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">
|
<a-button type="primary" :disabled="saveBtnDisable"
|
||||||
|
@click="updateXraySetting">
|
||||||
{{ i18n "pages.xray.save" }}
|
{{ i18n "pages.xray.save" }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">
|
<a-button type="danger" :disabled="!saveBtnDisable"
|
||||||
|
@click="restartXray">
|
||||||
{{ i18n "pages.xray.restart" }}
|
{{ i18n "pages.xray.restart" }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-popover v-if="restartResult" :overlay-class-name="themeSwitcher.currentTheme">
|
<a-popover v-if="restartResult"
|
||||||
<span slot="title">{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
|
:overlay-class-name="themeSwitcher.currentTheme">
|
||||||
|
<span slot="title">{{ i18n
|
||||||
|
"pages.index.xrayErrorPopoverTitle" }}</span>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<span :style="{ maxWidth: '400px' }" v-for="line in restartResult.split('\n')">[[ line
|
<span :style="{ maxWidth: '400px' }"
|
||||||
|
v-for="line in restartResult.split('\n')">[[ line
|
||||||
]]</span>
|
]]</span>
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="question-circle"></a-icon>
|
<a-icon type="question-circle"></a-icon>
|
||||||
|
|
@ -48,10 +60,13 @@
|
||||||
<a-col :xs="24" :sm="14">
|
<a-col :xs="24" :sm="14">
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<a-back-top :target="() => document.getElementById('content-layout')"
|
<a-back-top
|
||||||
|
:target="() => document.getElementById('content-layout')"
|
||||||
visibility-height="200"></a-back-top>
|
visibility-height="200"></a-back-top>
|
||||||
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
|
<a-alert type="warning"
|
||||||
message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
|
:style="{ float: 'right', width: 'fit-content' }"
|
||||||
|
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||||
|
show-icon>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -60,7 +75,8 @@
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col>
|
<a-col>
|
||||||
<a-tabs default-active-key="tpl-basic" @change="(activeKey) => { this.changePage(activeKey); }"
|
<a-tabs default-active-key="tpl-basic"
|
||||||
|
@change="(activeKey) => { this.changePage(activeKey); }"
|
||||||
:class="themeSwitcher.currentTheme">
|
:class="themeSwitcher.currentTheme">
|
||||||
<a-tab-pane key="tpl-basic" :style="{ paddingTop: '20px' }">
|
<a-tab-pane key="tpl-basic" :style="{ paddingTop: '20px' }">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
|
|
@ -83,21 +99,24 @@
|
||||||
</template>
|
</template>
|
||||||
{{ template "settings/xray/outbounds" . }}
|
{{ template "settings/xray/outbounds" . }}
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="tpl-reverse" :style="{ paddingTop: '20px' }" force-render="true">
|
<a-tab-pane key="tpl-reverse" :style="{ paddingTop: '20px' }"
|
||||||
|
force-render="true">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<a-icon type="import"></a-icon>
|
<a-icon type="import"></a-icon>
|
||||||
<span>{{ i18n "pages.xray.outbound.reverse"}}</span>
|
<span>{{ i18n "pages.xray.outbound.reverse"}}</span>
|
||||||
</template>
|
</template>
|
||||||
{{ template "settings/xray/reverse" . }}
|
{{ template "settings/xray/reverse" . }}
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="tpl-balancer" :style="{ paddingTop: '20px' }" force-render="true">
|
<a-tab-pane key="tpl-balancer" :style="{ paddingTop: '20px' }"
|
||||||
|
force-render="true">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<a-icon type="cluster"></a-icon>
|
<a-icon type="cluster"></a-icon>
|
||||||
<span>{{ i18n "pages.xray.Balancers"}}</span>
|
<span>{{ i18n "pages.xray.Balancers"}}</span>
|
||||||
</template>
|
</template>
|
||||||
{{ template "settings/xray/balancers" . }}
|
{{ template "settings/xray/balancers" . }}
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="tpl-dns" :style="{ paddingTop: '20px' }" force-render="true">
|
<a-tab-pane key="tpl-dns" :style="{ paddingTop: '20px' }"
|
||||||
|
force-render="true">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<a-icon type="database"></a-icon>
|
<a-icon type="database"></a-icon>
|
||||||
<span>DNS</span>
|
<span>DNS</span>
|
||||||
|
|
@ -120,14 +139,18 @@
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
{{template "page/body_scripts" .}}
|
{{template "page/body_scripts" .}}
|
||||||
<script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
|
<script
|
||||||
<script src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script>
|
src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
|
||||||
|
<script
|
||||||
|
src="{{ .base_path }}assets/codemirror/codemirror.min.js?{{ .cur_ver }}"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
|
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
|
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
|
<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script>
|
<script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script>
|
<script
|
||||||
<script src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
|
src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script>
|
||||||
|
<script
|
||||||
|
src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script>
|
<script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
|
<script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
|
||||||
<script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
|
<script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
|
||||||
|
|
@ -181,11 +204,13 @@
|
||||||
];
|
];
|
||||||
|
|
||||||
const outboundColumns = [
|
const outboundColumns = [
|
||||||
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
|
{ title: "#", align: 'center', width: 60, scopedSlots: { customRender: 'action' } },
|
||||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
||||||
{ title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
|
{ title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
|
||||||
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
|
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', width: 50, scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', width: 50, scopedSlots: { customRender: 'traffic' } },
|
||||||
|
{ title: '{{ i18n "pages.xray.outbound.testResult" }}', align: 'center', width: 120, scopedSlots: { customRender: 'testResult' } },
|
||||||
|
{ title: '{{ i18n "pages.xray.outbound.test" }}', align: 'center', width: 60, scopedSlots: { customRender: 'test' } },
|
||||||
];
|
];
|
||||||
|
|
||||||
const reverseColumns = [
|
const reverseColumns = [
|
||||||
|
|
@ -228,8 +253,11 @@
|
||||||
},
|
},
|
||||||
oldXraySetting: '',
|
oldXraySetting: '',
|
||||||
xraySetting: '',
|
xraySetting: '',
|
||||||
|
outboundTestUrl: 'https://www.google.com/generate_204',
|
||||||
|
oldOutboundTestUrl: 'https://www.google.com/generate_204',
|
||||||
inboundTags: [],
|
inboundTags: [],
|
||||||
outboundsTraffic: [],
|
outboundsTraffic: [],
|
||||||
|
outboundTestStates: {}, // Track testing state and results for each outbound
|
||||||
saveBtnDisable: true,
|
saveBtnDisable: true,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
restartResult: '',
|
restartResult: '',
|
||||||
|
|
@ -337,14 +365,14 @@
|
||||||
},
|
},
|
||||||
defaultObservatory: {
|
defaultObservatory: {
|
||||||
subjectSelector: [],
|
subjectSelector: [],
|
||||||
probeURL: "http://www.google.com/gen_204",
|
probeURL: "https://www.google.com/generate_204",
|
||||||
probeInterval: "10m",
|
probeInterval: "10m",
|
||||||
enableConcurrency: true
|
enableConcurrency: true
|
||||||
},
|
},
|
||||||
defaultBurstObservatory: {
|
defaultBurstObservatory: {
|
||||||
subjectSelector: [],
|
subjectSelector: [],
|
||||||
pingConfig: {
|
pingConfig: {
|
||||||
destination: "http://www.google.com/gen_204",
|
destination: "https://www.google.com/generate_204",
|
||||||
interval: "30m",
|
interval: "30m",
|
||||||
connectivity: "http://connectivitycheck.platform.hicloud.com/generate_204",
|
connectivity: "http://connectivitycheck.platform.hicloud.com/generate_204",
|
||||||
timeout: "10s",
|
timeout: "10s",
|
||||||
|
|
@ -375,12 +403,17 @@
|
||||||
this.oldXraySetting = xs;
|
this.oldXraySetting = xs;
|
||||||
this.xraySetting = xs;
|
this.xraySetting = xs;
|
||||||
this.inboundTags = result.inboundTags;
|
this.inboundTags = result.inboundTags;
|
||||||
|
this.outboundTestUrl = result.outboundTestUrl || 'https://www.google.com/generate_204';
|
||||||
|
this.oldOutboundTestUrl = this.outboundTestUrl;
|
||||||
this.saveBtnDisable = true;
|
this.saveBtnDisable = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async updateXraySetting() {
|
async updateXraySetting() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post("/panel/xray/update", { xraySetting: this.xraySetting });
|
const msg = await HttpUtil.post("/panel/xray/update", {
|
||||||
|
xraySetting: this.xraySetting,
|
||||||
|
outboundTestUrl: this.outboundTestUrl || 'https://www.google.com/generate_204'
|
||||||
|
});
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
await this.getXraySetting();
|
await this.getXraySetting();
|
||||||
|
|
@ -595,6 +628,73 @@
|
||||||
outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
|
outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
|
||||||
this.outboundSettings = JSON.stringify(outbounds);
|
this.outboundSettings = JSON.stringify(outbounds);
|
||||||
},
|
},
|
||||||
|
async testOutbound(index) {
|
||||||
|
const outbound = this.templateSettings.outbounds[index];
|
||||||
|
if (!outbound) {
|
||||||
|
Vue.prototype.$message.error('{{ i18n "pages.xray.outbound.testError" }}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outbound.protocol === 'blackhole' || outbound.tag === 'blocked') {
|
||||||
|
Vue.prototype.$message.warning('{{ i18n "pages.xray.outbound.testError" }}: blocked/blackhole outbound');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize test state for this outbound if not exists
|
||||||
|
if (!this.outboundTestStates[index]) {
|
||||||
|
this.$set(this.outboundTestStates, index, {
|
||||||
|
testing: false,
|
||||||
|
result: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set testing state
|
||||||
|
this.$set(this.outboundTestStates[index], 'testing', true);
|
||||||
|
this.$set(this.outboundTestStates[index], 'result', null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const outboundJSON = JSON.stringify(outbound);
|
||||||
|
const testURL = this.outboundTestUrl || 'https://www.google.com/generate_204';
|
||||||
|
const allOutboundsJSON = JSON.stringify(this.templateSettings.outbounds || []);
|
||||||
|
|
||||||
|
const msg = await HttpUtil.post("/panel/xray/testOutbound", {
|
||||||
|
outbound: outboundJSON,
|
||||||
|
testURL: testURL,
|
||||||
|
allOutbounds: allOutboundsJSON
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update test state
|
||||||
|
this.$set(this.outboundTestStates[index], 'testing', false);
|
||||||
|
|
||||||
|
if (msg.success && msg.obj) {
|
||||||
|
const result = msg.obj;
|
||||||
|
this.$set(this.outboundTestStates[index], 'result', result);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
Vue.prototype.$message.success(
|
||||||
|
`{{ i18n "pages.xray.outbound.testSuccess" }}: ${result.delay}ms (${result.statusCode})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Vue.prototype.$message.error(
|
||||||
|
`{{ i18n "pages.xray.outbound.testFailed" }}: ${result.error || 'Unknown error'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$set(this.outboundTestStates[index], 'result', {
|
||||||
|
success: false,
|
||||||
|
error: msg.msg || '{{ i18n "pages.xray.outbound.testError" }}'
|
||||||
|
});
|
||||||
|
Vue.prototype.$message.error(msg.msg || '{{ i18n "pages.xray.outbound.testError" }}');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.$set(this.outboundTestStates[index], 'testing', false);
|
||||||
|
this.$set(this.outboundTestStates[index], 'result', {
|
||||||
|
success: false,
|
||||||
|
error: error.message || '{{ i18n "pages.xray.outbound.testError" }}'
|
||||||
|
});
|
||||||
|
Vue.prototype.$message.error('{{ i18n "pages.xray.outbound.testError" }}: ' + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
addReverse() {
|
addReverse() {
|
||||||
reverseModal.show({
|
reverseModal.show({
|
||||||
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
|
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
|
||||||
|
|
@ -981,7 +1081,7 @@
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
await PromiseUtil.sleep(800);
|
await PromiseUtil.sleep(800);
|
||||||
this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
|
this.saveBtnDisable = this.oldXraySetting === this.xraySetting && this.oldOutboundTestUrl === this.outboundTestUrl;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ func (j *XrayTrafficJob) Run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast traffic update via WebSocket with accumulated values from database
|
// Broadcast traffic update via WebSocket with accumulated values from database
|
||||||
trafficUpdate := map[string]interface{}{
|
trafficUpdate := map[string]any{
|
||||||
"traffics": traffics,
|
"traffics": traffics,
|
||||||
"clientTraffics": clientTraffics,
|
"clientTraffics": clientTraffics,
|
||||||
"onlineClients": onlineClients,
|
"onlineClients": onlineClients,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,23 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"github.com/mhsanaei/3x-ui/v2/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
@ -13,6 +27,9 @@ import (
|
||||||
// It handles outbound traffic monitoring and statistics.
|
// It handles outbound traffic monitoring and statistics.
|
||||||
type OutboundService struct{}
|
type OutboundService struct{}
|
||||||
|
|
||||||
|
// testSemaphore limits concurrent outbound tests to prevent resource exhaustion.
|
||||||
|
var testSemaphore sync.Mutex
|
||||||
|
|
||||||
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
||||||
var err error
|
var err error
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
@ -100,3 +117,308 @@ func (s *OutboundService) ResetOutboundTraffic(tag string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestOutboundResult represents the result of testing an outbound
|
||||||
|
type TestOutboundResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Delay int64 `json:"delay"` // Delay in milliseconds
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
StatusCode int `json:"statusCode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOutbound tests an outbound by creating a temporary xray instance and measuring response time.
|
||||||
|
// allOutboundsJSON must be a JSON array of all outbounds; they are copied into the test config unchanged.
|
||||||
|
// Only the test inbound and a route rule (to the tested outbound tag) are added.
|
||||||
|
func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allOutboundsJSON string) (*TestOutboundResult, error) {
|
||||||
|
if testURL == "" {
|
||||||
|
testURL = "https://www.google.com/generate_204"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit to one concurrent test at a time
|
||||||
|
if !testSemaphore.TryLock() {
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: "Another outbound test is already running, please wait",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
defer testSemaphore.Unlock()
|
||||||
|
|
||||||
|
// Parse the outbound being tested to get its tag
|
||||||
|
var testOutbound map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(outboundJSON), &testOutbound); err != nil {
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("Invalid outbound JSON: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
outboundTag, _ := testOutbound["tag"].(string)
|
||||||
|
if outboundTag == "" {
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: "Outbound has no tag",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if protocol, _ := testOutbound["protocol"].(string); protocol == "blackhole" || outboundTag == "blocked" {
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: "Blocked/blackhole outbound cannot be tested",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use all outbounds when provided; otherwise fall back to single outbound
|
||||||
|
var allOutbounds []any
|
||||||
|
if allOutboundsJSON != "" {
|
||||||
|
if err := json.Unmarshal([]byte(allOutboundsJSON), &allOutbounds); err != nil {
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("Invalid allOutbounds JSON: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(allOutbounds) == 0 {
|
||||||
|
allOutbounds = []any{testOutbound}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find an available port for test inbound
|
||||||
|
testPort, err := findAvailablePort()
|
||||||
|
if err != nil {
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("Failed to find available port: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all outbounds as-is, add only test inbound and route rule
|
||||||
|
testConfig := s.createTestConfig(outboundTag, allOutbounds, testPort)
|
||||||
|
|
||||||
|
// Use a temporary config file so the main config.json is never overwritten
|
||||||
|
testConfigPath, err := createTestConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("Failed to create test config path: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
defer os.Remove(testConfigPath) // ensure temp file is removed even if process is not stopped
|
||||||
|
|
||||||
|
// Create temporary xray process with its own config file
|
||||||
|
testProcess := xray.NewTestProcess(testConfig, testConfigPath)
|
||||||
|
defer func() {
|
||||||
|
if testProcess.IsRunning() {
|
||||||
|
testProcess.Stop()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start the test process
|
||||||
|
if err := testProcess.Start(); err != nil {
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("Failed to start test xray instance: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for xray to start listening on the test port
|
||||||
|
if err := waitForPort(testPort, 3*time.Second); err != nil {
|
||||||
|
if !testProcess.IsRunning() {
|
||||||
|
result := testProcess.GetResult()
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("Xray process exited: %s", result),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("Xray failed to start listening: %v", err),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if process is still running
|
||||||
|
if !testProcess.IsRunning() {
|
||||||
|
result := testProcess.GetResult()
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: fmt.Sprintf("Xray process exited: %s", result),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the connection through proxy
|
||||||
|
delay, statusCode, err := s.testConnection(testPort, testURL)
|
||||||
|
if err != nil {
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TestOutboundResult{
|
||||||
|
Success: true,
|
||||||
|
Delay: delay,
|
||||||
|
StatusCode: statusCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestConfig creates a test config by copying all outbounds unchanged and adding
|
||||||
|
// only the test inbound (SOCKS) and a route rule that sends traffic to the given outbound tag.
|
||||||
|
func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []any, testPort int) *xray.Config {
|
||||||
|
// Test inbound (SOCKS proxy) - only addition to inbounds
|
||||||
|
testInbound := xray.InboundConfig{
|
||||||
|
Tag: "test-inbound",
|
||||||
|
Listen: json_util.RawMessage(`"127.0.0.1"`),
|
||||||
|
Port: testPort,
|
||||||
|
Protocol: "socks",
|
||||||
|
Settings: json_util.RawMessage(`{"auth":"noauth","udp":true}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outbounds: copy all, but set noKernelTun=true for WireGuard outbounds
|
||||||
|
processedOutbounds := make([]any, len(allOutbounds))
|
||||||
|
for i, ob := range allOutbounds {
|
||||||
|
outbound, ok := ob.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
processedOutbounds[i] = ob
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if protocol, ok := outbound["protocol"].(string); ok && protocol == "wireguard" {
|
||||||
|
// Set noKernelTun to true for WireGuard outbounds
|
||||||
|
if settings, ok := outbound["settings"].(map[string]any); ok {
|
||||||
|
settings["noKernelTun"] = true
|
||||||
|
} else {
|
||||||
|
// Create settings if it doesn't exist
|
||||||
|
outbound["settings"] = map[string]any{
|
||||||
|
"noKernelTun": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processedOutbounds[i] = outbound
|
||||||
|
}
|
||||||
|
outboundsJSON, _ := json.Marshal(processedOutbounds)
|
||||||
|
|
||||||
|
// Create routing rule to route all traffic through test outbound
|
||||||
|
routingRules := []map[string]any{
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"outboundTag": outboundTag,
|
||||||
|
"network": "tcp,udp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
routingJSON, _ := json.Marshal(map[string]any{
|
||||||
|
"domainStrategy": "AsIs",
|
||||||
|
"rules": routingRules,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disable logging for test process to avoid creating orphaned log files
|
||||||
|
logConfig := map[string]any{
|
||||||
|
"loglevel": "warning",
|
||||||
|
"access": "none",
|
||||||
|
"error": "none",
|
||||||
|
"dnsLog": false,
|
||||||
|
}
|
||||||
|
logJSON, _ := json.Marshal(logConfig)
|
||||||
|
|
||||||
|
// Create minimal config
|
||||||
|
cfg := &xray.Config{
|
||||||
|
LogConfig: json_util.RawMessage(logJSON),
|
||||||
|
InboundConfigs: []xray.InboundConfig{
|
||||||
|
testInbound,
|
||||||
|
},
|
||||||
|
OutboundConfigs: json_util.RawMessage(string(outboundsJSON)),
|
||||||
|
RouterConfig: json_util.RawMessage(string(routingJSON)),
|
||||||
|
Policy: json_util.RawMessage(`{}`),
|
||||||
|
Stats: json_util.RawMessage(`{}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// testConnection tests the connection through the proxy and measures delay.
|
||||||
|
// It performs a warmup request first to establish the SOCKS connection and populate DNS caches,
|
||||||
|
// then measures the second request for a more accurate latency reading.
|
||||||
|
func (s *OutboundService) testConnection(proxyPort int, testURL string) (int64, int, error) {
|
||||||
|
// Create SOCKS5 proxy URL
|
||||||
|
proxyURL := fmt.Sprintf("socks5://127.0.0.1:%d", proxyPort)
|
||||||
|
|
||||||
|
// Parse proxy URL
|
||||||
|
proxyURLParsed, err := url.Parse(proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, common.NewErrorf("Invalid proxy URL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTP client with proxy and keep-alive for connection reuse
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyURL(proxyURLParsed),
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
MaxIdleConns: 1,
|
||||||
|
IdleConnTimeout: 10 * time.Second,
|
||||||
|
DisableCompression: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warmup request: establishes SOCKS/TLS connection, DNS, and TCP to the target.
|
||||||
|
// This mirrors real-world usage where connections are reused.
|
||||||
|
warmupResp, err := client.Get(testURL)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, common.NewErrorf("Request failed: %v", err)
|
||||||
|
}
|
||||||
|
io.Copy(io.Discard, warmupResp.Body)
|
||||||
|
warmupResp.Body.Close()
|
||||||
|
|
||||||
|
// Measure the actual request on the warm connection
|
||||||
|
startTime := time.Now()
|
||||||
|
resp, err := client.Get(testURL)
|
||||||
|
delay := time.Since(startTime).Milliseconds()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, common.NewErrorf("Request failed: %v", err)
|
||||||
|
}
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
return delay, resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForPort polls until the given TCP port is accepting connections or the timeout expires.
|
||||||
|
func waitForPort(port int, timeout time.Duration) error {
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 100*time.Millisecond)
|
||||||
|
if err == nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("port %d not ready after %v", port, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findAvailablePort finds an available port for testing
|
||||||
|
func findAvailablePort() (int, error) {
|
||||||
|
listener, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
addr := listener.Addr().(*net.TCPAddr)
|
||||||
|
return addr.Port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestConfigPath returns a unique path for a temporary xray config file in the bin folder.
|
||||||
|
// The temp file is created and closed so the path is reserved; Start() will overwrite it.
|
||||||
|
func createTestConfigPath() (string, error) {
|
||||||
|
tmpFile, err := os.CreateTemp(config.GetBinFolderPath(), "xray_test_*.json")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
path := tmpFile.Name()
|
||||||
|
if err := tmpFile.Close(); err != nil {
|
||||||
|
os.Remove(path)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@ var defaultValueMap = map[string]string{
|
||||||
"warp": "",
|
"warp": "",
|
||||||
"externalTrafficInformEnable": "false",
|
"externalTrafficInformEnable": "false",
|
||||||
"externalTrafficInformURI": "",
|
"externalTrafficInformURI": "",
|
||||||
|
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
|
||||||
|
|
||||||
// LDAP defaults
|
// LDAP defaults
|
||||||
"ldapEnable": "false",
|
"ldapEnable": "false",
|
||||||
"ldapHost": "",
|
"ldapHost": "",
|
||||||
|
|
@ -271,6 +273,14 @@ func (s *SettingService) GetXrayConfigTemplate() (string, error) {
|
||||||
return s.getString("xrayTemplateConfig")
|
return s.getString("xrayTemplateConfig")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetXrayOutboundTestUrl() (string, error) {
|
||||||
|
return s.getString("xrayOutboundTestUrl")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetXrayOutboundTestUrl(url string) error {
|
||||||
|
return s.setString("xrayOutboundTestUrl", url)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetListen() (string, error) {
|
func (s *SettingService) GetListen() (string, error) {
|
||||||
return s.getString("webListen")
|
return s.getString("webListen")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "اختار استراتيجية المخرجات للشبكة في بروتوكول الحرية."
|
"FreedomStrategyDesc" = "اختار استراتيجية المخرجات للشبكة في بروتوكول الحرية."
|
||||||
"RoutingStrategy" = "استراتيجية التوجيه العامة"
|
"RoutingStrategy" = "استراتيجية التوجيه العامة"
|
||||||
"RoutingStrategyDesc" = "حدد استراتيجية التوجيه الإجمالية لحل كل الطلبات."
|
"RoutingStrategyDesc" = "حدد استراتيجية التوجيه الإجمالية لحل كل الطلبات."
|
||||||
|
"outboundTestUrl" = "رابط اختبار المخرج"
|
||||||
|
"outboundTestUrlDesc" = "الرابط المستخدم عند اختبار اتصال المخرج"
|
||||||
"Torrent" = "حظر بروتوكول التورنت"
|
"Torrent" = "حظر بروتوكول التورنت"
|
||||||
"Inbounds" = "الإدخالات"
|
"Inbounds" = "الإدخالات"
|
||||||
"InboundsDesc" = "قبول العملاء المعينين."
|
"InboundsDesc" = "قبول العملاء المعينين."
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "Set the output strategy for the network in the Freedom Protocol."
|
"FreedomStrategyDesc" = "Set the output strategy for the network in the Freedom Protocol."
|
||||||
"RoutingStrategy" = "Overall Routing Strategy"
|
"RoutingStrategy" = "Overall Routing Strategy"
|
||||||
"RoutingStrategyDesc" = "Set the overall traffic routing strategy for resolving all requests."
|
"RoutingStrategyDesc" = "Set the overall traffic routing strategy for resolving all requests."
|
||||||
|
"outboundTestUrl" = "Outbound Test URL"
|
||||||
|
"outboundTestUrlDesc" = "URL used when testing outbound connectivity."
|
||||||
"Torrent" = "Block BitTorrent Protocol"
|
"Torrent" = "Block BitTorrent Protocol"
|
||||||
"Inbounds" = "Inbounds"
|
"Inbounds" = "Inbounds"
|
||||||
"InboundsDesc" = "Accepting the specific clients."
|
"InboundsDesc" = "Accepting the specific clients."
|
||||||
|
|
@ -523,6 +525,12 @@
|
||||||
"accountInfo" = "Account Information"
|
"accountInfo" = "Account Information"
|
||||||
"outboundStatus" = "Outbound Status"
|
"outboundStatus" = "Outbound Status"
|
||||||
"sendThrough" = "Send Through"
|
"sendThrough" = "Send Through"
|
||||||
|
"test" = "Test"
|
||||||
|
"testResult" = "Test Result"
|
||||||
|
"testing" = "Testing connection..."
|
||||||
|
"testSuccess" = "Test successful"
|
||||||
|
"testFailed" = "Test failed"
|
||||||
|
"testError" = "Failed to test outbound"
|
||||||
|
|
||||||
[pages.xray.balancer]
|
[pages.xray.balancer]
|
||||||
"addBalancer" = "Add Balancer"
|
"addBalancer" = "Add Balancer"
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "Establece la estrategia de salida de la red en el Protocolo Freedom."
|
"FreedomStrategyDesc" = "Establece la estrategia de salida de la red en el Protocolo Freedom."
|
||||||
"RoutingStrategy" = "Configurar Estrategia de Enrutamiento de Dominios"
|
"RoutingStrategy" = "Configurar Estrategia de Enrutamiento de Dominios"
|
||||||
"RoutingStrategyDesc" = "Establece la estrategia general de enrutamiento para la resolución de DNS."
|
"RoutingStrategyDesc" = "Establece la estrategia general de enrutamiento para la resolución de DNS."
|
||||||
|
"outboundTestUrl" = "URL de prueba de outbound"
|
||||||
|
"outboundTestUrlDesc" = "URL usada al probar la conectividad del outbound"
|
||||||
"Torrent" = "Prohibir Uso de BitTorrent"
|
"Torrent" = "Prohibir Uso de BitTorrent"
|
||||||
"Inbounds" = "Entrante"
|
"Inbounds" = "Entrante"
|
||||||
"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
|
"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "تعیین میکند Freedom استراتژی خروجی شبکه را برای پروتکل"
|
"FreedomStrategyDesc" = "تعیین میکند Freedom استراتژی خروجی شبکه را برای پروتکل"
|
||||||
"RoutingStrategy" = "استراتژی کلی مسیریابی"
|
"RoutingStrategy" = "استراتژی کلی مسیریابی"
|
||||||
"RoutingStrategyDesc" = "استراتژی کلی مسیریابی برای حل تمام درخواستها را تعیین میکند"
|
"RoutingStrategyDesc" = "استراتژی کلی مسیریابی برای حل تمام درخواستها را تعیین میکند"
|
||||||
|
"outboundTestUrl" = "آدرس تست خروجی"
|
||||||
|
"outboundTestUrlDesc" = "آدرسی که برای تست اتصال خروجی استفاده میشود."
|
||||||
"Torrent" = "مسدودسازی پروتکل بیتتورنت"
|
"Torrent" = "مسدودسازی پروتکل بیتتورنت"
|
||||||
"Inbounds" = "ورودیها"
|
"Inbounds" = "ورودیها"
|
||||||
"InboundsDesc" = "پذیرش کلاینت خاص"
|
"InboundsDesc" = "پذیرش کلاینت خاص"
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom."
|
"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom."
|
||||||
"RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
|
"RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
|
||||||
"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan."
|
"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan."
|
||||||
|
"outboundTestUrl" = "URL tes outbound"
|
||||||
|
"outboundTestUrlDesc" = "URL yang digunakan saat menguji konektivitas outbound"
|
||||||
"Torrent" = "Blokir Protokol BitTorrent"
|
"Torrent" = "Blokir Protokol BitTorrent"
|
||||||
"Inbounds" = "Masuk"
|
"Inbounds" = "Masuk"
|
||||||
"InboundsDesc" = "Menerima klien tertentu."
|
"InboundsDesc" = "Menerima klien tertentu."
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "Freedomプロトコル内のネットワークの出力戦略を設定する"
|
"FreedomStrategyDesc" = "Freedomプロトコル内のネットワークの出力戦略を設定する"
|
||||||
"RoutingStrategy" = "ルーティングドメイン戦略設定"
|
"RoutingStrategy" = "ルーティングドメイン戦略設定"
|
||||||
"RoutingStrategyDesc" = "DNS解決の全体的なルーティング戦略を設定する"
|
"RoutingStrategyDesc" = "DNS解決の全体的なルーティング戦略を設定する"
|
||||||
|
"outboundTestUrl" = "アウトバウンドテスト URL"
|
||||||
|
"outboundTestUrlDesc" = "アウトバウンド接続テストに使用する URL。既定値"
|
||||||
"Torrent" = "BitTorrent プロトコルをブロック"
|
"Torrent" = "BitTorrent プロトコルをブロック"
|
||||||
"Inbounds" = "インバウンドルール"
|
"Inbounds" = "インバウンドルール"
|
||||||
"InboundsDesc" = "特定のクライアントからのトラフィックを受け入れる"
|
"InboundsDesc" = "特定のクライアントからのトラフィックを受け入れる"
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "Definir a estratégia de saída para a rede no Protocolo Freedom."
|
"FreedomStrategyDesc" = "Definir a estratégia de saída para a rede no Protocolo Freedom."
|
||||||
"RoutingStrategy" = "Estratégia Geral de Roteamento"
|
"RoutingStrategy" = "Estratégia Geral de Roteamento"
|
||||||
"RoutingStrategyDesc" = "Definir a estratégia geral de roteamento de tráfego para resolver todas as solicitações."
|
"RoutingStrategyDesc" = "Definir a estratégia geral de roteamento de tráfego para resolver todas as solicitações."
|
||||||
|
"outboundTestUrl" = "URL de teste de outbound"
|
||||||
|
"outboundTestUrlDesc" = "URL usada ao testar conectividade do outbound"
|
||||||
"Torrent" = "Bloquear Protocolo BitTorrent"
|
"Torrent" = "Bloquear Protocolo BitTorrent"
|
||||||
"Inbounds" = "Inbounds"
|
"Inbounds" = "Inbounds"
|
||||||
"InboundsDesc" = "Aceitar clientes específicos."
|
"InboundsDesc" = "Aceitar clientes específicos."
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "Установка стратегии вывода сети в протоколе Freedom"
|
"FreedomStrategyDesc" = "Установка стратегии вывода сети в протоколе Freedom"
|
||||||
"RoutingStrategy" = "Настройка маршрутизации доменов"
|
"RoutingStrategy" = "Настройка маршрутизации доменов"
|
||||||
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
|
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
|
||||||
|
"outboundTestUrl" = "URL для теста исходящего"
|
||||||
|
"outboundTestUrlDesc" = "URL для проверки подключения исходящего"
|
||||||
"Torrent" = "Заблокировать BitTorrent"
|
"Torrent" = "Заблокировать BitTorrent"
|
||||||
"Inbounds" = "Входящие подключения"
|
"Inbounds" = "Входящие подключения"
|
||||||
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
|
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "Freedom Protokolünde ağın çıkış stratejisini ayarlayın."
|
"FreedomStrategyDesc" = "Freedom Protokolünde ağın çıkış stratejisini ayarlayın."
|
||||||
"RoutingStrategy" = "Genel Yönlendirme Stratejisi"
|
"RoutingStrategy" = "Genel Yönlendirme Stratejisi"
|
||||||
"RoutingStrategyDesc" = "Tüm istekleri çözmek için genel trafik yönlendirme stratejisini ayarlayın."
|
"RoutingStrategyDesc" = "Tüm istekleri çözmek için genel trafik yönlendirme stratejisini ayarlayın."
|
||||||
|
"outboundTestUrl" = "Outbound test URL"
|
||||||
|
"outboundTestUrlDesc" = "Outbound bağlantı testinde kullanılan URL"
|
||||||
"Torrent" = "BitTorrent Protokolünü Engelle"
|
"Torrent" = "BitTorrent Protokolünü Engelle"
|
||||||
"Inbounds" = "Gelenler"
|
"Inbounds" = "Gelenler"
|
||||||
"InboundsDesc" = "Belirli müşterileri kabul eder."
|
"InboundsDesc" = "Belirli müşterileri kabul eder."
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "Установити стратегію виведення для мережі в протоколі свободи."
|
"FreedomStrategyDesc" = "Установити стратегію виведення для мережі в протоколі свободи."
|
||||||
"RoutingStrategy" = "Загальна стратегія маршрутизації"
|
"RoutingStrategy" = "Загальна стратегія маршрутизації"
|
||||||
"RoutingStrategyDesc" = "Установити загальну стратегію маршрутизації трафіку для вирішення всіх запитів."
|
"RoutingStrategyDesc" = "Установити загальну стратегію маршрутизації трафіку для вирішення всіх запитів."
|
||||||
|
"outboundTestUrl" = "URL тесту outbound"
|
||||||
|
"outboundTestUrlDesc" = "URL для перевірки з'єднання outbound"
|
||||||
"Torrent" = "Блокувати протокол BitTorrent"
|
"Torrent" = "Блокувати протокол BitTorrent"
|
||||||
"Inbounds" = "Вхідні"
|
"Inbounds" = "Вхідні"
|
||||||
"InboundsDesc" = "Прийняття певних клієнтів."
|
"InboundsDesc" = "Прийняття певних клієнтів."
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "Đặt chiến lược đầu ra của mạng trong Giao thức Freedom."
|
"FreedomStrategyDesc" = "Đặt chiến lược đầu ra của mạng trong Giao thức Freedom."
|
||||||
"RoutingStrategy" = "Cấu hình Chiến lược Định tuyến Tên miền"
|
"RoutingStrategy" = "Cấu hình Chiến lược Định tuyến Tên miền"
|
||||||
"RoutingStrategyDesc" = "Đặt chiến lược định tuyến tổng thể cho việc giải quyết DNS."
|
"RoutingStrategyDesc" = "Đặt chiến lược định tuyến tổng thể cho việc giải quyết DNS."
|
||||||
|
"outboundTestUrl" = "URL kiểm tra outbound"
|
||||||
|
"outboundTestUrlDesc" = "URL dùng khi kiểm tra kết nối outbound"
|
||||||
"Torrent" = "Cấu hình sử dụng BitTorrent"
|
"Torrent" = "Cấu hình sử dụng BitTorrent"
|
||||||
"Inbounds" = "Đầu vào"
|
"Inbounds" = "Đầu vào"
|
||||||
"InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể."
|
"InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể."
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "设置 Freedom 协议中网络的输出策略"
|
"FreedomStrategyDesc" = "设置 Freedom 协议中网络的输出策略"
|
||||||
"RoutingStrategy" = "配置路由域策略"
|
"RoutingStrategy" = "配置路由域策略"
|
||||||
"RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略"
|
"RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略"
|
||||||
|
"outboundTestUrl" = "出站测试 URL"
|
||||||
|
"outboundTestUrlDesc" = "测试出站连接时使用的 URL"
|
||||||
"Torrent" = "屏蔽 BitTorrent 协议"
|
"Torrent" = "屏蔽 BitTorrent 协议"
|
||||||
"Inbounds" = "入站规则"
|
"Inbounds" = "入站规则"
|
||||||
"InboundsDesc" = "接受来自特定客户端的流量"
|
"InboundsDesc" = "接受来自特定客户端的流量"
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,8 @@
|
||||||
"FreedomStrategyDesc" = "設定 Freedom 協議中網路的輸出策略"
|
"FreedomStrategyDesc" = "設定 Freedom 協議中網路的輸出策略"
|
||||||
"RoutingStrategy" = "配置路由域策略"
|
"RoutingStrategy" = "配置路由域策略"
|
||||||
"RoutingStrategyDesc" = "設定 DNS 解析的整體路由策略"
|
"RoutingStrategyDesc" = "設定 DNS 解析的整體路由策略"
|
||||||
|
"outboundTestUrl" = "出站測試 URL"
|
||||||
|
"outboundTestUrlDesc" = "測試出站連線時使用的 URL"
|
||||||
"Torrent" = "遮蔽 BitTorrent 協議"
|
"Torrent" = "遮蔽 BitTorrent 協議"
|
||||||
"Inbounds" = "入站規則"
|
"Inbounds" = "入站規則"
|
||||||
"InboundsDesc" = "接受來自特定客戶端的流量"
|
"InboundsDesc" = "接受來自特定客戶端的流量"
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func BroadcastInbounds(inbounds any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BroadcastOutbounds broadcasts outbounds list update to all connected clients
|
// BroadcastOutbounds broadcasts outbounds list update to all connected clients
|
||||||
func BroadcastOutbounds(outbounds interface{}) {
|
func BroadcastOutbounds(outbounds any) {
|
||||||
hub := GetHub()
|
hub := GetHub()
|
||||||
if hub != nil {
|
if hub != nil {
|
||||||
hub.Broadcast(MessageTypeOutbounds, outbounds)
|
hub.Broadcast(MessageTypeOutbounds, outbounds)
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,15 @@ func NewProcess(xrayConfig *Config) *Process {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewTestProcess creates a new Xray process that uses a specific config file path.
|
||||||
|
// Used for test runs (e.g. outbound test) so the main config.json is not overwritten.
|
||||||
|
// The config file at configPath is removed when the process is stopped.
|
||||||
|
func NewTestProcess(xrayConfig *Config, configPath string) *Process {
|
||||||
|
p := &Process{newTestProcess(xrayConfig, configPath)}
|
||||||
|
runtime.SetFinalizer(p, stopProcess)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
type process struct {
|
type process struct {
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
|
|
||||||
|
|
@ -118,10 +127,11 @@ type process struct {
|
||||||
|
|
||||||
onlineClients []string
|
onlineClients []string
|
||||||
|
|
||||||
config *Config
|
config *Config
|
||||||
logWriter *LogWriter
|
configPath string // if set, use this path instead of GetConfigPath() and remove on Stop
|
||||||
exitErr error
|
logWriter *LogWriter
|
||||||
startTime time.Time
|
exitErr error
|
||||||
|
startTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// newProcess creates a new internal process struct for Xray.
|
// newProcess creates a new internal process struct for Xray.
|
||||||
|
|
@ -134,6 +144,13 @@ func newProcess(config *Config) *process {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newTestProcess creates a process that writes and runs with a specific config path.
|
||||||
|
func newTestProcess(config *Config, configPath string) *process {
|
||||||
|
p := newProcess(config)
|
||||||
|
p.configPath = configPath
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
// IsRunning returns true if the Xray process is currently running.
|
// IsRunning returns true if the Xray process is currently running.
|
||||||
func (p *process) IsRunning() bool {
|
func (p *process) IsRunning() bool {
|
||||||
if p.cmd == nil || p.cmd.Process == nil {
|
if p.cmd == nil || p.cmd.Process == nil {
|
||||||
|
|
@ -238,6 +255,9 @@ func (p *process) Start() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
configPath := GetConfigPath()
|
configPath := GetConfigPath()
|
||||||
|
if p.configPath != "" {
|
||||||
|
configPath = p.configPath
|
||||||
|
}
|
||||||
err = os.WriteFile(configPath, data, fs.ModePerm)
|
err = os.WriteFile(configPath, data, fs.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.NewErrorf("Failed to write configuration file: %v", err)
|
return common.NewErrorf("Failed to write configuration file: %v", err)
|
||||||
|
|
@ -278,6 +298,16 @@ func (p *process) Stop() error {
|
||||||
return errors.New("xray is not running")
|
return errors.New("xray is not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove temporary config file used for test runs so main config is never touched
|
||||||
|
if p.configPath != "" {
|
||||||
|
if p.configPath != GetConfigPath() {
|
||||||
|
// Check if file exists before removing
|
||||||
|
if _, err := os.Stat(p.configPath); err == nil {
|
||||||
|
_ = os.Remove(p.configPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return p.cmd.Process.Kill()
|
return p.cmd.Process.Kill()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue