This commit is contained in:
MHSanaei 2026-02-09 21:41:25 +01:00
parent 1a8b29aad3
commit a6d9de10bc
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
12 changed files with 320 additions and 157 deletions

View file

@ -54,7 +54,7 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
} }
outboundTestUrl, _ := a.SettingService.GetXrayOutboundTestUrl() outboundTestUrl, _ := a.SettingService.GetXrayOutboundTestUrl()
if outboundTestUrl == "" { if outboundTestUrl == "" {
outboundTestUrl = "http://www.google.com/gen_204" outboundTestUrl = "https://www.google.com/generate_204"
} }
urlJSON, _ := json.Marshal(outboundTestUrl) urlJSON, _ := json.Marshal(outboundTestUrl)
xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + ", \"outboundTestUrl\": " + string(urlJSON) + " }" xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + ", \"outboundTestUrl\": " + string(urlJSON) + " }"
@ -70,7 +70,7 @@ func (a *XraySettingController) updateSetting(c *gin.Context) {
} }
outboundTestUrl := c.PostForm("outboundTestUrl") outboundTestUrl := c.PostForm("outboundTestUrl")
if outboundTestUrl == "" { if outboundTestUrl == "" {
outboundTestUrl = "http://www.google.com/gen_204" outboundTestUrl = "https://www.google.com/generate_204"
} }
_ = a.SettingService.SetXrayOutboundTestUrl(outboundTestUrl) _ = a.SettingService.SetXrayOutboundTestUrl(outboundTestUrl)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)

View file

@ -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,11 +27,14 @@
</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>
@ -35,37 +42,48 @@
</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.outboundTestUrl" }}</template> <template #title>{{ i18n "pages.xray.outboundTestUrl" }}</template>
<template #description>{{ i18n "pages.xray.outboundTestUrlDesc" }}</template> <template #description>{{ i18n "pages.xray.outboundTestUrlDesc"
}}</template>
<template #control> <template #control>
<a-input v-model="outboundTestUrl" :placeholder="'http://www.google.com/gen_204'" :style="{ width: '100%' }"></a-input> <a-input v-model="outboundTestUrl"
:placeholder="'https://www.google.com/generate_204'"
:style="{ width: '100%' }"></a-input>
</template> </template>
</a-setting-list-item> </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>
@ -75,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>
@ -93,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">
@ -107,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">
@ -121,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">
@ -146,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>
@ -160,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>
@ -179,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>
@ -209,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>
@ -228,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>
@ -248,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>

View file

@ -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,15 +64,21 @@
</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>
@ -73,30 +87,37 @@
</template> </template>
<template slot="test" slot-scope="text, outbound, index"> <template slot="test" slot-scope="text, outbound, index">
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.xray.outbound.test" }}</template> <template slot="title">{{ i18n "pages.xray.outbound.test"
}}</template>
<a-button <a-button
type="primary" type="primary"
shape="circle" shape="circle"
icon="thunderbolt" icon="thunderbolt"
:loading="outboundTestStates[index] && outboundTestStates[index].testing" :loading="outboundTestStates[index] && outboundTestStates[index].testing"
@click="testOutbound(index)" @click="testOutbound(index)"
:disabled="outboundTestStates[index] && outboundTestStates[index].testing"> :disabled="(outbound.protocol === 'blackhole' || outbound.tag === 'blocked') || (outboundTestStates[index] && outboundTestStates[index].testing)">
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</template> </template>
<template slot="testResult" slot-scope="text, outbound, index"> <template slot="testResult" slot-scope="text, outbound, index">
<div v-if="outboundTestStates[index] && outboundTestStates[index].result"> <div
<a-tag v-if="outboundTestStates[index].result.success" color="green"> v-if="outboundTestStates[index] && outboundTestStates[index].result">
<a-tag v-if="outboundTestStates[index].result.success"
color="green">
[[ outboundTestStates[index].result.delay ]]ms [[ outboundTestStates[index].result.delay ]]ms
<span v-if="outboundTestStates[index].result.statusCode"> (HTTP [[ outboundTestStates[index].result.statusCode ]])</span> <span v-if="outboundTestStates[index].result.statusCode">
([[ outboundTestStates[index].result.statusCode
]])</span>
</a-tag> </a-tag>
<a-tooltip v-else :title="outboundTestStates[index].result.error"> <a-tooltip v-else
:title="outboundTestStates[index].result.error">
<a-tag color="red"> <a-tag color="red">
Failed Failed
</a-tag> </a-tag>
</a-tooltip> </a-tooltip>
</div> </div>
<span v-else-if="outboundTestStates[index] && outboundTestStates[index].testing"> <span
v-else-if="outboundTestStates[index] && outboundTestStates[index].testing">
<a-icon type="loading" /> <a-icon type="loading" />
</span> </span>
<span v-else>-</span> <span v-else>-</span>

View file

@ -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>
@ -230,8 +253,8 @@
}, },
oldXraySetting: '', oldXraySetting: '',
xraySetting: '', xraySetting: '',
outboundTestUrl: 'http://www.google.com/gen_204', outboundTestUrl: 'https://www.google.com/generate_204',
oldOutboundTestUrl: 'http://www.google.com/gen_204', oldOutboundTestUrl: 'https://www.google.com/generate_204',
inboundTags: [], inboundTags: [],
outboundsTraffic: [], outboundsTraffic: [],
outboundTestStates: {}, // Track testing state and results for each outbound outboundTestStates: {}, // Track testing state and results for each outbound
@ -342,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",
@ -380,7 +403,7 @@
this.oldXraySetting = xs; this.oldXraySetting = xs;
this.xraySetting = xs; this.xraySetting = xs;
this.inboundTags = result.inboundTags; this.inboundTags = result.inboundTags;
this.outboundTestUrl = result.outboundTestUrl || 'http://www.google.com/gen_204'; this.outboundTestUrl = result.outboundTestUrl || 'https://www.google.com/generate_204';
this.oldOutboundTestUrl = this.outboundTestUrl; this.oldOutboundTestUrl = this.outboundTestUrl;
this.saveBtnDisable = true; this.saveBtnDisable = true;
} }
@ -389,7 +412,7 @@
this.loading(true); this.loading(true);
const msg = await HttpUtil.post("/panel/xray/update", { const msg = await HttpUtil.post("/panel/xray/update", {
xraySetting: this.xraySetting, xraySetting: this.xraySetting,
outboundTestUrl: this.outboundTestUrl || 'http://www.google.com/gen_204' outboundTestUrl: this.outboundTestUrl || 'https://www.google.com/generate_204'
}); });
this.loading(false); this.loading(false);
if (msg.success) { if (msg.success) {
@ -612,6 +635,11 @@
return; 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 // Initialize test state for this outbound if not exists
if (!this.outboundTestStates[index]) { if (!this.outboundTestStates[index]) {
this.$set(this.outboundTestStates, index, { this.$set(this.outboundTestStates, index, {
@ -626,7 +654,7 @@
try { try {
const outboundJSON = JSON.stringify(outbound); const outboundJSON = JSON.stringify(outbound);
const testURL = this.outboundTestUrl || 'http://www.google.com/gen_204'; const testURL = this.outboundTestUrl || 'https://www.google.com/generate_204';
const allOutboundsJSON = JSON.stringify(this.templateSettings.outbounds || []); const allOutboundsJSON = JSON.stringify(this.templateSettings.outbounds || []);
const msg = await HttpUtil.post("/panel/xray/testOutbound", { const msg = await HttpUtil.post("/panel/xray/testOutbound", {
@ -644,7 +672,7 @@
if (result.success) { if (result.success) {
Vue.prototype.$message.success( Vue.prototype.$message.success(
`{{ i18n "pages.xray.outbound.testSuccess" }}: ${result.delay}ms (HTTP ${result.statusCode})` `{{ i18n "pages.xray.outbound.testSuccess" }}: ${result.delay}ms (${result.statusCode})`
); );
} else { } else {
Vue.prototype.$message.error( Vue.prototype.$message.error(

View file

@ -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,

View file

@ -1,12 +1,15 @@
package service package service
import ( import (
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"sync"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/config" "github.com/mhsanaei/3x-ui/v2/config"
@ -24,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()
@ -125,11 +131,20 @@ type TestOutboundResult struct {
// Only the test inbound and a route rule (to the tested outbound tag) are added. // 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) { func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allOutboundsJSON string) (*TestOutboundResult, error) {
if testURL == "" { if testURL == "" {
testURL = "http://www.google.com/gen_204" 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 // Parse the outbound being tested to get its tag
var testOutbound map[string]interface{} var testOutbound map[string]any
if err := json.Unmarshal([]byte(outboundJSON), &testOutbound); err != nil { if err := json.Unmarshal([]byte(outboundJSON), &testOutbound); err != nil {
return &TestOutboundResult{ return &TestOutboundResult{
Success: false, Success: false,
@ -143,9 +158,15 @@ func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allO
Error: "Outbound has no tag", Error: "Outbound has no tag",
}, nil }, 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 // Use all outbounds when provided; otherwise fall back to single outbound
var allOutbounds []interface{} var allOutbounds []any
if allOutboundsJSON != "" { if allOutboundsJSON != "" {
if err := json.Unmarshal([]byte(allOutboundsJSON), &allOutbounds); err != nil { if err := json.Unmarshal([]byte(allOutboundsJSON), &allOutbounds); err != nil {
return &TestOutboundResult{ return &TestOutboundResult{
@ -155,7 +176,7 @@ func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allO
} }
} }
if len(allOutbounds) == 0 { if len(allOutbounds) == 0 {
allOutbounds = []interface{}{testOutbound} allOutbounds = []any{testOutbound}
} }
// Find an available port for test inbound // Find an available port for test inbound
@ -185,8 +206,6 @@ func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allO
defer func() { defer func() {
if testProcess.IsRunning() { if testProcess.IsRunning() {
testProcess.Stop() testProcess.Stop()
// Give it a moment to clean up
time.Sleep(500 * time.Millisecond)
} }
}() }()
@ -198,8 +217,20 @@ func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allO
}, nil }, nil
} }
// Wait a bit for xray to start // Wait for xray to start listening on the test port
time.Sleep(1 * time.Second) 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 // Check if process is still running
if !testProcess.IsRunning() { if !testProcess.IsRunning() {
@ -228,7 +259,7 @@ func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allO
// createTestConfig creates a test config by copying all outbounds unchanged and adding // 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. // only the test inbound (SOCKS) and a route rule that sends traffic to the given outbound tag.
func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []interface{}, testPort int) *xray.Config { func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []any, testPort int) *xray.Config {
// Test inbound (SOCKS proxy) - only addition to inbounds // Test inbound (SOCKS proxy) - only addition to inbounds
testInbound := xray.InboundConfig{ testInbound := xray.InboundConfig{
Tag: "test-inbound", Tag: "test-inbound",
@ -239,16 +270,20 @@ func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []in
} }
// Outbounds: copy all, but set noKernelTun=true for WireGuard outbounds // Outbounds: copy all, but set noKernelTun=true for WireGuard outbounds
processedOutbounds := make([]interface{}, len(allOutbounds)) processedOutbounds := make([]any, len(allOutbounds))
for i, ob := range allOutbounds { for i, ob := range allOutbounds {
outbound := ob.(map[string]interface{}) outbound, ok := ob.(map[string]any)
if !ok {
processedOutbounds[i] = ob
continue
}
if protocol, ok := outbound["protocol"].(string); ok && protocol == "wireguard" { if protocol, ok := outbound["protocol"].(string); ok && protocol == "wireguard" {
// Set noKernelTun to true for WireGuard outbounds // Set noKernelTun to true for WireGuard outbounds
if settings, ok := outbound["settings"].(map[string]interface{}); ok { if settings, ok := outbound["settings"].(map[string]any); ok {
settings["noKernelTun"] = true settings["noKernelTun"] = true
} else { } else {
// Create settings if it doesn't exist // Create settings if it doesn't exist
outbound["settings"] = map[string]interface{}{ outbound["settings"] = map[string]any{
"noKernelTun": true, "noKernelTun": true,
} }
} }
@ -258,7 +293,7 @@ func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []in
outboundsJSON, _ := json.Marshal(processedOutbounds) outboundsJSON, _ := json.Marshal(processedOutbounds)
// Create routing rule to route all traffic through test outbound // Create routing rule to route all traffic through test outbound
routingRules := []map[string]interface{}{ routingRules := []map[string]any{
{ {
"type": "field", "type": "field",
"outboundTag": outboundTag, "outboundTag": outboundTag,
@ -266,19 +301,23 @@ func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []in
}, },
} }
routingJSON, _ := json.Marshal(map[string]interface{}{ routingJSON, _ := json.Marshal(map[string]any{
"domainStrategy": "AsIs", "domainStrategy": "AsIs",
"rules": routingRules, "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 // Create minimal config
config := &xray.Config{ cfg := &xray.Config{
LogConfig: json_util.RawMessage(`{ LogConfig: json_util.RawMessage(logJSON),
"loglevel":"info",
"access":"` + config.GetBinFolderPath() + `/access_tests.log",
"error":"` + config.GetBinFolderPath() + `/error_tests.log",
"dnsLog":true
}`),
InboundConfigs: []xray.InboundConfig{ InboundConfigs: []xray.InboundConfig{
testInbound, testInbound,
}, },
@ -288,10 +327,12 @@ func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []in
Stats: json_util.RawMessage(`{}`), Stats: json_util.RawMessage(`{}`),
} }
return config return cfg
} }
// testConnection tests the connection through the proxy and measures delay // 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) { func (s *OutboundService) testConnection(proxyPort int, testURL string) (int64, int, error) {
// Create SOCKS5 proxy URL // Create SOCKS5 proxy URL
proxyURL := fmt.Sprintf("socks5://127.0.0.1:%d", proxyPort) proxyURL := fmt.Sprintf("socks5://127.0.0.1:%d", proxyPort)
@ -302,18 +343,32 @@ func (s *OutboundService) testConnection(proxyPort int, testURL string) (int64,
return 0, 0, common.NewErrorf("Invalid proxy URL: %v", err) return 0, 0, common.NewErrorf("Invalid proxy URL: %v", err)
} }
// Create HTTP client with proxy // Create HTTP client with proxy and keep-alive for connection reuse
client := &http.Client{ client := &http.Client{
Timeout: 30 * time.Second, Timeout: 10 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURLParsed), Proxy: http.ProxyURL(proxyURLParsed),
DialContext: (&net.Dialer{ DialContext: (&net.Dialer{
Timeout: 10 * time.Second, Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext, }).DialContext,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
MaxIdleConns: 1,
IdleConnTimeout: 10 * time.Second,
DisableCompression: true,
}, },
} }
// Measure time // 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() startTime := time.Now()
resp, err := client.Get(testURL) resp, err := client.Get(testURL)
delay := time.Since(startTime).Milliseconds() delay := time.Since(startTime).Milliseconds()
@ -321,11 +376,26 @@ func (s *OutboundService) testConnection(proxyPort int, testURL string) (int64,
if err != nil { if err != nil {
return 0, 0, common.NewErrorf("Request failed: %v", err) return 0, 0, common.NewErrorf("Request failed: %v", err)
} }
defer resp.Body.Close() io.Copy(io.Discard, resp.Body)
resp.Body.Close()
return delay, resp.StatusCode, nil 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 // findAvailablePort finds an available port for testing
func findAvailablePort() (int, error) { func findAvailablePort() (int, error) {
listener, err := net.Listen("tcp", ":0") listener, err := net.Listen("tcp", ":0")
@ -339,7 +409,7 @@ func findAvailablePort() (int, error) {
} }
// createTestConfigPath returns a unique path for a temporary xray config file in the bin folder. // createTestConfigPath returns a unique path for a temporary xray config file in the bin folder.
// The file is not created; the path is reserved by creating and then removing an empty temp file. // The temp file is created and closed so the path is reserved; Start() will overwrite it.
func createTestConfigPath() (string, error) { func createTestConfigPath() (string, error) {
tmpFile, err := os.CreateTemp(config.GetBinFolderPath(), "xray_test_*.json") tmpFile, err := os.CreateTemp(config.GetBinFolderPath(), "xray_test_*.json")
if err != nil { if err != nil {
@ -350,8 +420,5 @@ func createTestConfigPath() (string, error) {
os.Remove(path) os.Remove(path)
return "", err return "", err
} }
if err := os.Remove(path); err != nil {
return "", err
}
return path, nil return path, nil
} }

View file

@ -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": "",
@ -99,7 +101,6 @@ var defaultValueMap = map[string]string{
"ldapDefaultTotalGB": "0", "ldapDefaultTotalGB": "0",
"ldapDefaultExpiryDays": "0", "ldapDefaultExpiryDays": "0",
"ldapDefaultLimitIP": "0", "ldapDefaultLimitIP": "0",
"xrayOutboundTestUrl": "http://www.google.com/gen_204",
} }
// SettingService provides business logic for application settings management. // SettingService provides business logic for application settings management.

View file

@ -427,8 +427,6 @@
"information" = "Information" "information" = "Information"
"language" = "Language" "language" = "Language"
"telegramBotLanguage" = "Telegram Bot Language" "telegramBotLanguage" = "Telegram Bot Language"
"outboundTestURL" = "Outbound Test URL"
"outboundTestURLDesc" = "URL used to test outbound connection"
[pages.xray] [pages.xray]
"title" = "Xray Configs" "title" = "Xray Configs"
@ -463,7 +461,7 @@
"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" "outboundTestUrl" = "Outbound Test URL"
"outboundTestUrlDesc" = "URL used when testing outbound connectivity. Default is http://www.google.com/gen_204" "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."

View file

@ -427,8 +427,6 @@
"information" = "اطلاعات" "information" = "اطلاعات"
"language" = "زبان" "language" = "زبان"
"telegramBotLanguage" = "زبان ربات تلگرام" "telegramBotLanguage" = "زبان ربات تلگرام"
"outboundTestURL" = "آدرس تست خروجی"
"outboundTestURLDesc" = "آدرسی که برای تست اتصال خروجی استفاده می‌شود"
[pages.xray] [pages.xray]
"title" = "پیکربندی ایکس‌ری" "title" = "پیکربندی ایکس‌ری"
@ -463,7 +461,7 @@
"RoutingStrategy" = "استراتژی کلی مسیریابی" "RoutingStrategy" = "استراتژی کلی مسیریابی"
"RoutingStrategyDesc" = "استراتژی کلی مسیریابی برای حل تمام درخواست‌ها را تعیین می‌کند" "RoutingStrategyDesc" = "استراتژی کلی مسیریابی برای حل تمام درخواست‌ها را تعیین می‌کند"
"outboundTestUrl" = "آدرس تست خروجی" "outboundTestUrl" = "آدرس تست خروجی"
"outboundTestUrlDesc" = "آدرسی که برای تست اتصال خروجی استفاده می‌شود. پیش‌فرض: http://www.google.com/gen_204" "outboundTestUrlDesc" = "آدرسی که برای تست اتصال خروجی استفاده می‌شود."
"Torrent" = "مسدودسازی پروتکل بیت‌تورنت" "Torrent" = "مسدودسازی پروتکل بیت‌تورنت"
"Inbounds" = "ورودی‌ها" "Inbounds" = "ورودی‌ها"
"InboundsDesc" = "پذیرش کلاینت خاص" "InboundsDesc" = "پذیرش کلاینت خاص"

View file

@ -427,8 +427,6 @@
"information" = "Информация" "information" = "Информация"
"language" = "Язык интерфейса" "language" = "Язык интерфейса"
"telegramBotLanguage" = "Язык Telegram-бота" "telegramBotLanguage" = "Язык Telegram-бота"
"outboundTestURL" = "URL для тестирования исходящего подключения"
"outboundTestURLDesc" = "URL, используемый для тестирования исходящего подключения"
[pages.xray] [pages.xray]
"title" = "Настройки Xray" "title" = "Настройки Xray"
@ -463,7 +461,7 @@
"RoutingStrategy" = "Настройка маршрутизации доменов" "RoutingStrategy" = "Настройка маршрутизации доменов"
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS" "RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
"outboundTestUrl" = "URL для теста исходящего" "outboundTestUrl" = "URL для теста исходящего"
"outboundTestUrlDesc" = "URL для проверки подключения исходящего. По умолчанию: http://www.google.com/gen_204" "outboundTestUrlDesc" = "URL для проверки подключения исходящего"
"Torrent" = "Заблокировать BitTorrent" "Torrent" = "Заблокировать BitTorrent"
"Inbounds" = "Входящие подключения" "Inbounds" = "Входящие подключения"
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов" "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"

View file

@ -427,8 +427,6 @@
"information" = "信息" "information" = "信息"
"language" = "语言" "language" = "语言"
"telegramBotLanguage" = "Telegram 机器人语言" "telegramBotLanguage" = "Telegram 机器人语言"
"outboundTestURL" = "出站测试 URL"
"outboundTestURLDesc" = "用于测试出站连接的 URL"
[pages.xray] [pages.xray]
"title" = "Xray 配置" "title" = "Xray 配置"
@ -463,7 +461,7 @@
"RoutingStrategy" = "配置路由域策略" "RoutingStrategy" = "配置路由域策略"
"RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略" "RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略"
"outboundTestUrl" = "出站测试 URL" "outboundTestUrl" = "出站测试 URL"
"outboundTestUrlDesc" = "测试出站连接时使用的 URL,默认为 http://www.google.com/gen_204" "outboundTestUrlDesc" = "测试出站连接时使用的 URL"
"Torrent" = "屏蔽 BitTorrent 协议" "Torrent" = "屏蔽 BitTorrent 协议"
"Inbounds" = "入站规则" "Inbounds" = "入站规则"
"InboundsDesc" = "接受来自特定客户端的流量" "InboundsDesc" = "接受来自特定客户端的流量"

View file

@ -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)