mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-27 20:53:01 +00:00
add outbound testing functionality with configurable test URL
This commit is contained in:
parent
d8fb09faae
commit
9f841662f1
21 changed files with 478 additions and 14 deletions
|
|
@ -1,6 +1,9 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
|
@ -34,9 +37,10 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/warp/:action", a.warp)
|
||||
g.POST("/update", a.updateSetting)
|
||||
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) {
|
||||
xraySetting, err := a.SettingService.GetXrayConfigTemplate()
|
||||
if err != nil {
|
||||
|
|
@ -48,15 +52,28 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
|||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + " }"
|
||||
outboundTestUrl, _ := a.SettingService.GetXrayOutboundTestUrl()
|
||||
if outboundTestUrl == "" {
|
||||
outboundTestUrl = "http://www.google.com/gen_204"
|
||||
}
|
||||
urlJSON, _ := json.Marshal(outboundTestUrl)
|
||||
xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + ", \"outboundTestUrl\": " + string(urlJSON) + " }"
|
||||
jsonObj(c, xrayResponse, nil)
|
||||
}
|
||||
|
||||
// updateSetting updates the Xray configuration settings.
|
||||
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||
xraySetting := c.PostForm("xraySetting")
|
||||
err := a.XraySettingService.SaveXraySetting(xraySetting)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
if err := a.XraySettingService.SaveXraySetting(xraySetting); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
outboundTestUrl := c.PostForm("outboundTestUrl")
|
||||
if outboundTestUrl == "" {
|
||||
outboundTestUrl = "http://www.google.com/gen_204"
|
||||
}
|
||||
_ = a.SettingService.SetXrayOutboundTestUrl(outboundTestUrl)
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)
|
||||
}
|
||||
|
||||
// getDefaultXrayConfig retrieves the default Xray configuration.
|
||||
|
|
@ -118,3 +135,24 @@ func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
|
|||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@
|
|||
</a-select>
|
||||
</template>
|
||||
</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="'http://www.google.com/gen_204'" :style="{ width: '100%' }"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="2" header='{{ i18n "pages.xray.statistics" }}'>
|
||||
<a-setting-list-item paddings="small">
|
||||
|
|
|
|||
|
|
@ -71,6 +71,36 @@
|
|||
<template slot="traffic" slot-scope="text, outbound, index">
|
||||
<a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag>
|
||||
</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="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"> (HTTP [[ 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-space>
|
||||
{{end}}
|
||||
|
|
@ -181,11 +181,13 @@
|
|||
];
|
||||
|
||||
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 "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
|
||||
{ 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.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 = [
|
||||
|
|
@ -228,8 +230,11 @@
|
|||
},
|
||||
oldXraySetting: '',
|
||||
xraySetting: '',
|
||||
outboundTestUrl: 'http://www.google.com/gen_204',
|
||||
oldOutboundTestUrl: 'http://www.google.com/gen_204',
|
||||
inboundTags: [],
|
||||
outboundsTraffic: [],
|
||||
outboundTestStates: {}, // Track testing state and results for each outbound
|
||||
saveBtnDisable: true,
|
||||
refreshing: false,
|
||||
restartResult: '',
|
||||
|
|
@ -375,12 +380,17 @@
|
|||
this.oldXraySetting = xs;
|
||||
this.xraySetting = xs;
|
||||
this.inboundTags = result.inboundTags;
|
||||
this.outboundTestUrl = result.outboundTestUrl || 'http://www.google.com/gen_204';
|
||||
this.oldOutboundTestUrl = this.outboundTestUrl;
|
||||
this.saveBtnDisable = true;
|
||||
}
|
||||
},
|
||||
async updateXraySetting() {
|
||||
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 || 'http://www.google.com/gen_204'
|
||||
});
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
await this.getXraySetting();
|
||||
|
|
@ -595,6 +605,68 @@
|
|||
outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 || 'http://www.google.com/gen_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 (HTTP ${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() {
|
||||
reverseModal.show({
|
||||
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
|
||||
|
|
@ -981,7 +1053,7 @@
|
|||
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(800);
|
||||
this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
|
||||
this.saveBtnDisable = this.oldXraySetting === this.xraySetting && this.oldOutboundTestUrl === this.outboundTestUrl;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package job
|
|||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
|
@ -388,7 +387,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
|||
// disconnectClientTemporarily removes and re-adds a client to force disconnect old connections
|
||||
func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, clientEmail string, clients []model.Client) {
|
||||
var xrayAPI xray.XrayAPI
|
||||
|
||||
|
||||
// Get panel settings for API port
|
||||
db := database.GetDB()
|
||||
var apiPort int
|
||||
|
|
@ -396,7 +395,7 @@ func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, c
|
|||
if err := db.Where("key = ?", "xrayApiPort").First(&apiPortSetting).Error; err == nil {
|
||||
apiPort, _ = strconv.Atoi(apiPortSetting.Value)
|
||||
}
|
||||
|
||||
|
||||
if apiPort == 0 {
|
||||
apiPort = 10085 // Default API port
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/mhsanaei/3x-ui/v2/database"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"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"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
|
@ -100,3 +111,231 @@ func (s *OutboundService) ResetOutboundTraffic(tag string) error {
|
|||
|
||||
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 = "http://www.google.com/gen_204"
|
||||
}
|
||||
|
||||
// Parse the outbound being tested to get its tag
|
||||
var testOutbound map[string]interface{}
|
||||
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
|
||||
}
|
||||
|
||||
// Use all outbounds when provided; otherwise fall back to single outbound
|
||||
var allOutbounds []interface{}
|
||||
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 = []interface{}{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()
|
||||
// Give it a moment to clean up
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
// 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 a bit for xray to start
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 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 []interface{}, 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 as-is, no tag or structure changes
|
||||
outboundsJSON, _ := json.Marshal(allOutbounds)
|
||||
|
||||
// Create routing rule to route all traffic through test outbound
|
||||
routingRules := []map[string]interface{}{
|
||||
{
|
||||
"type": "field",
|
||||
"outboundTag": outboundTag,
|
||||
"network": "tcp,udp",
|
||||
},
|
||||
}
|
||||
|
||||
routingJSON, _ := json.Marshal(map[string]interface{}{
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": routingRules,
|
||||
})
|
||||
|
||||
// Create minimal config
|
||||
config := &xray.Config{
|
||||
LogConfig: json_util.RawMessage(`{
|
||||
"loglevel":"info",
|
||||
"access":"` + config.GetBinFolderPath() + `/access_tests.log",
|
||||
"error":"` + config.GetBinFolderPath() + `/error_tests.log",
|
||||
"dnsLog":true
|
||||
}`),
|
||||
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 config
|
||||
}
|
||||
|
||||
// testConnection tests the connection through the proxy and measures delay
|
||||
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
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURLParsed),
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
}).DialContext,
|
||||
},
|
||||
}
|
||||
|
||||
// Measure time
|
||||
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)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return delay, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// 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 file is not created; the path is reserved by creating and then removing an empty temp file.
|
||||
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
|
||||
}
|
||||
if err := os.Remove(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ var defaultValueMap = map[string]string{
|
|||
"ldapDefaultTotalGB": "0",
|
||||
"ldapDefaultExpiryDays": "0",
|
||||
"ldapDefaultLimitIP": "0",
|
||||
"xrayOutboundTestUrl": "http://www.google.com/gen_204",
|
||||
}
|
||||
|
||||
// SettingService provides business logic for application settings management.
|
||||
|
|
@ -271,6 +272,14 @@ func (s *SettingService) GetXrayConfigTemplate() (string, error) {
|
|||
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) {
|
||||
return s.getString("webListen")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "اختار استراتيجية المخرجات للشبكة في بروتوكول الحرية."
|
||||
"RoutingStrategy" = "استراتيجية التوجيه العامة"
|
||||
"RoutingStrategyDesc" = "حدد استراتيجية التوجيه الإجمالية لحل كل الطلبات."
|
||||
"outboundTestUrl" = "رابط اختبار المخرج"
|
||||
"outboundTestUrlDesc" = "الرابط المستخدم عند اختبار اتصال المخرج"
|
||||
"Torrent" = "حظر بروتوكول التورنت"
|
||||
"Inbounds" = "الإدخالات"
|
||||
"InboundsDesc" = "قبول العملاء المعينين."
|
||||
|
|
|
|||
|
|
@ -427,6 +427,8 @@
|
|||
"information" = "Information"
|
||||
"language" = "Language"
|
||||
"telegramBotLanguage" = "Telegram Bot Language"
|
||||
"outboundTestURL" = "Outbound Test URL"
|
||||
"outboundTestURLDesc" = "URL used to test outbound connection"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray Configs"
|
||||
|
|
@ -460,6 +462,8 @@
|
|||
"FreedomStrategyDesc" = "Set the output strategy for the network in the Freedom Protocol."
|
||||
"RoutingStrategy" = "Overall Routing Strategy"
|
||||
"RoutingStrategyDesc" = "Set the overall traffic routing strategy for resolving all requests."
|
||||
"outboundTestUrl" = "Outbound Test URL"
|
||||
"outboundTestUrlDesc" = "URL used when testing outbound connectivity. Default is http://www.google.com/gen_204"
|
||||
"Torrent" = "Block BitTorrent Protocol"
|
||||
"Inbounds" = "Inbounds"
|
||||
"InboundsDesc" = "Accepting the specific clients."
|
||||
|
|
@ -523,6 +527,12 @@
|
|||
"accountInfo" = "Account Information"
|
||||
"outboundStatus" = "Outbound Status"
|
||||
"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]
|
||||
"addBalancer" = "Add Balancer"
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Establece la estrategia de salida de la red en el Protocolo Freedom."
|
||||
"RoutingStrategy" = "Configurar Estrategia de Enrutamiento de Dominios"
|
||||
"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"
|
||||
"Inbounds" = "Entrante"
|
||||
"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
|
||||
|
|
|
|||
|
|
@ -427,6 +427,8 @@
|
|||
"information" = "اطلاعات"
|
||||
"language" = "زبان"
|
||||
"telegramBotLanguage" = "زبان ربات تلگرام"
|
||||
"outboundTestURL" = "آدرس تست خروجی"
|
||||
"outboundTestURLDesc" = "آدرسی که برای تست اتصال خروجی استفاده میشود"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "پیکربندی ایکسری"
|
||||
|
|
@ -460,6 +462,8 @@
|
|||
"FreedomStrategyDesc" = "تعیین میکند Freedom استراتژی خروجی شبکه را برای پروتکل"
|
||||
"RoutingStrategy" = "استراتژی کلی مسیریابی"
|
||||
"RoutingStrategyDesc" = "استراتژی کلی مسیریابی برای حل تمام درخواستها را تعیین میکند"
|
||||
"outboundTestUrl" = "آدرس تست خروجی"
|
||||
"outboundTestUrlDesc" = "آدرسی که برای تست اتصال خروجی استفاده میشود. پیشفرض: http://www.google.com/gen_204"
|
||||
"Torrent" = "مسدودسازی پروتکل بیتتورنت"
|
||||
"Inbounds" = "ورودیها"
|
||||
"InboundsDesc" = "پذیرش کلاینت خاص"
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom."
|
||||
"RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
|
||||
"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"
|
||||
"Inbounds" = "Masuk"
|
||||
"InboundsDesc" = "Menerima klien tertentu."
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Freedomプロトコル内のネットワークの出力戦略を設定する"
|
||||
"RoutingStrategy" = "ルーティングドメイン戦略設定"
|
||||
"RoutingStrategyDesc" = "DNS解決の全体的なルーティング戦略を設定する"
|
||||
"outboundTestUrl" = "アウトバウンドテスト URL"
|
||||
"outboundTestUrlDesc" = "アウトバウンド接続テストに使用する URL。既定値"
|
||||
"Torrent" = "BitTorrent プロトコルをブロック"
|
||||
"Inbounds" = "インバウンドルール"
|
||||
"InboundsDesc" = "特定のクライアントからのトラフィックを受け入れる"
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Definir a estratégia de saída para a rede no Protocolo Freedom."
|
||||
"RoutingStrategy" = "Estratégia Geral de Roteamento"
|
||||
"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"
|
||||
"Inbounds" = "Inbounds"
|
||||
"InboundsDesc" = "Aceitar clientes específicos."
|
||||
|
|
|
|||
|
|
@ -427,6 +427,8 @@
|
|||
"information" = "Информация"
|
||||
"language" = "Язык интерфейса"
|
||||
"telegramBotLanguage" = "Язык Telegram-бота"
|
||||
"outboundTestURL" = "URL для тестирования исходящего подключения"
|
||||
"outboundTestURLDesc" = "URL, используемый для тестирования исходящего подключения"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Настройки Xray"
|
||||
|
|
@ -460,6 +462,8 @@
|
|||
"FreedomStrategyDesc" = "Установка стратегии вывода сети в протоколе Freedom"
|
||||
"RoutingStrategy" = "Настройка маршрутизации доменов"
|
||||
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
|
||||
"outboundTestUrl" = "URL для теста исходящего"
|
||||
"outboundTestUrlDesc" = "URL для проверки подключения исходящего. По умолчанию: http://www.google.com/gen_204"
|
||||
"Torrent" = "Заблокировать BitTorrent"
|
||||
"Inbounds" = "Входящие подключения"
|
||||
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Freedom Protokolünde ağın çıkış stratejisini ayarlayın."
|
||||
"RoutingStrategy" = "Genel Yönlendirme Stratejisi"
|
||||
"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"
|
||||
"Inbounds" = "Gelenler"
|
||||
"InboundsDesc" = "Belirli müşterileri kabul eder."
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "Установити стратегію виведення для мережі в протоколі свободи."
|
||||
"RoutingStrategy" = "Загальна стратегія маршрутизації"
|
||||
"RoutingStrategyDesc" = "Установити загальну стратегію маршрутизації трафіку для вирішення всіх запитів."
|
||||
"outboundTestUrl" = "URL тесту outbound"
|
||||
"outboundTestUrlDesc" = "URL для перевірки з'єднання outbound"
|
||||
"Torrent" = "Блокувати протокол BitTorrent"
|
||||
"Inbounds" = "Вхідні"
|
||||
"InboundsDesc" = "Прийняття певних клієнтів."
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"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"
|
||||
"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"
|
||||
"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ể."
|
||||
|
|
|
|||
|
|
@ -427,6 +427,8 @@
|
|||
"information" = "信息"
|
||||
"language" = "语言"
|
||||
"telegramBotLanguage" = "Telegram 机器人语言"
|
||||
"outboundTestURL" = "出站测试 URL"
|
||||
"outboundTestURLDesc" = "用于测试出站连接的 URL"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray 配置"
|
||||
|
|
@ -460,6 +462,8 @@
|
|||
"FreedomStrategyDesc" = "设置 Freedom 协议中网络的输出策略"
|
||||
"RoutingStrategy" = "配置路由域策略"
|
||||
"RoutingStrategyDesc" = "设置 DNS 解析的整体路由策略"
|
||||
"outboundTestUrl" = "出站测试 URL"
|
||||
"outboundTestUrlDesc" = "测试出站连接时使用的 URL,默认为 http://www.google.com/gen_204"
|
||||
"Torrent" = "屏蔽 BitTorrent 协议"
|
||||
"Inbounds" = "入站规则"
|
||||
"InboundsDesc" = "接受来自特定客户端的流量"
|
||||
|
|
|
|||
|
|
@ -460,6 +460,8 @@
|
|||
"FreedomStrategyDesc" = "設定 Freedom 協議中網路的輸出策略"
|
||||
"RoutingStrategy" = "配置路由域策略"
|
||||
"RoutingStrategyDesc" = "設定 DNS 解析的整體路由策略"
|
||||
"outboundTestUrl" = "出站測試 URL"
|
||||
"outboundTestUrlDesc" = "測試出站連線時使用的 URL"
|
||||
"Torrent" = "遮蔽 BitTorrent 協議"
|
||||
"Inbounds" = "入站規則"
|
||||
"InboundsDesc" = "接受來自特定客戶端的流量"
|
||||
|
|
|
|||
|
|
@ -110,6 +110,15 @@ func NewProcess(xrayConfig *Config) *Process {
|
|||
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 {
|
||||
cmd *exec.Cmd
|
||||
|
||||
|
|
@ -118,10 +127,11 @@ type process struct {
|
|||
|
||||
onlineClients []string
|
||||
|
||||
config *Config
|
||||
logWriter *LogWriter
|
||||
exitErr error
|
||||
startTime time.Time
|
||||
config *Config
|
||||
configPath string // if set, use this path instead of GetConfigPath() and remove on Stop
|
||||
logWriter *LogWriter
|
||||
exitErr error
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (p *process) IsRunning() bool {
|
||||
if p.cmd == nil || p.cmd.Process == nil {
|
||||
|
|
@ -238,6 +255,9 @@ func (p *process) Start() (err error) {
|
|||
}
|
||||
|
||||
configPath := GetConfigPath()
|
||||
if p.configPath != "" {
|
||||
configPath = p.configPath
|
||||
}
|
||||
err = os.WriteFile(configPath, data, fs.ModePerm)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
// 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" {
|
||||
return p.cmd.Process.Kill()
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue