outbound.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. package service
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net"
  6. "net/http"
  7. "os"
  8. "strconv"
  9. "sync"
  10. "time"
  11. "github.com/mhsanaei/3x-ui/v3/config"
  12. "github.com/mhsanaei/3x-ui/v3/database"
  13. "github.com/mhsanaei/3x-ui/v3/database/model"
  14. "github.com/mhsanaei/3x-ui/v3/logger"
  15. "github.com/mhsanaei/3x-ui/v3/util/json_util"
  16. "github.com/mhsanaei/3x-ui/v3/xray"
  17. "gorm.io/gorm"
  18. )
  19. // OutboundService provides business logic for managing Xray outbound configurations.
  20. // It handles outbound traffic monitoring and statistics.
  21. type OutboundService struct{}
  22. // httpTestSemaphore serialises HTTP-mode probes (each one spawns a temp xray
  23. // instance, which is too expensive to run in parallel). TCP-mode probes are
  24. // dial-only and don't need the semaphore.
  25. var httpTestSemaphore sync.Mutex
  26. func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
  27. var err error
  28. db := database.GetDB()
  29. tx := db.Begin()
  30. defer func() {
  31. if err != nil {
  32. tx.Rollback()
  33. } else {
  34. tx.Commit()
  35. }
  36. }()
  37. err = s.addOutboundTraffic(tx, traffics)
  38. if err != nil {
  39. return err, false
  40. }
  41. return nil, false
  42. }
  43. func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
  44. if len(traffics) == 0 {
  45. return nil
  46. }
  47. var err error
  48. for _, traffic := range traffics {
  49. if traffic.IsOutbound {
  50. var outbound model.OutboundTraffics
  51. err = tx.Model(&model.OutboundTraffics{}).Where("tag = ?", traffic.Tag).
  52. FirstOrCreate(&outbound).Error
  53. if err != nil {
  54. return err
  55. }
  56. outbound.Tag = traffic.Tag
  57. outbound.Up = outbound.Up + traffic.Up
  58. outbound.Down = outbound.Down + traffic.Down
  59. outbound.Total = outbound.Up + outbound.Down
  60. err = tx.Save(&outbound).Error
  61. if err != nil {
  62. return err
  63. }
  64. }
  65. }
  66. return nil
  67. }
  68. func (s *OutboundService) GetOutboundsTraffic() ([]*model.OutboundTraffics, error) {
  69. db := database.GetDB()
  70. var traffics []*model.OutboundTraffics
  71. err := db.Model(model.OutboundTraffics{}).Find(&traffics).Error
  72. if err != nil {
  73. logger.Warning("Error retrieving OutboundTraffics: ", err)
  74. return nil, err
  75. }
  76. return traffics, nil
  77. }
  78. func (s *OutboundService) ResetOutboundTraffic(tag string) error {
  79. db := database.GetDB()
  80. whereText := "tag "
  81. if tag == "-alltags-" {
  82. whereText += " <> ?"
  83. } else {
  84. whereText += " = ?"
  85. }
  86. result := db.Model(model.OutboundTraffics{}).
  87. Where(whereText, tag).
  88. Updates(map[string]any{"up": 0, "down": 0, "total": 0})
  89. err := result.Error
  90. if err != nil {
  91. return err
  92. }
  93. return nil
  94. }
  95. // TestOutboundResult represents the result of testing an outbound.
  96. // Delay is in milliseconds. Endpoints is only populated for TCP-mode
  97. // probes; HTTP mode reports the round-trip delay measured by xray's
  98. // burstObservatory probe.
  99. type TestOutboundResult struct {
  100. Success bool `json:"success"`
  101. Delay int64 `json:"delay"`
  102. Error string `json:"error,omitempty"`
  103. Mode string `json:"mode,omitempty"`
  104. Endpoints []TestEndpointResult `json:"endpoints,omitempty"`
  105. }
  106. // TestEndpointResult is one entry in a TCP-mode probe — the per-endpoint
  107. // dial outcome for outbounds that expose multiple servers/peers.
  108. type TestEndpointResult struct {
  109. Address string `json:"address"`
  110. Success bool `json:"success"`
  111. Delay int64 `json:"delay"`
  112. Error string `json:"error,omitempty"`
  113. }
  114. // TestOutbound dispatches to the chosen probe mode:
  115. // - mode="tcp": dial the outbound's host:port directly. No xray spin-up,
  116. // parallel-safe, ~100ms per endpoint. Doesn't validate the proxy
  117. // protocol — only that the remote is reachable on TCP.
  118. // - mode="" or "http": spin a temp xray instance, route a real HTTP
  119. // request through it, return delay + a DNS/Connect/TLS/TTFB breakdown.
  120. // Authoritative but expensive and serialised by httpTestSemaphore.
  121. //
  122. // allOutboundsJSON is only consulted in HTTP mode (it backs
  123. // sockopt.dialerProxy chains during test).
  124. func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allOutboundsJSON string, mode string) (*TestOutboundResult, error) {
  125. if mode == "tcp" {
  126. return s.testOutboundTCP(outboundJSON)
  127. }
  128. return s.testOutboundHTTP(outboundJSON, testURL, allOutboundsJSON)
  129. }
  130. func (s *OutboundService) testOutboundTCP(outboundJSON string) (*TestOutboundResult, error) {
  131. var ob map[string]any
  132. if err := json.Unmarshal([]byte(outboundJSON), &ob); err != nil {
  133. return &TestOutboundResult{Mode: "tcp", Success: false, Error: fmt.Sprintf("Invalid outbound JSON: %v", err)}, nil
  134. }
  135. tag, _ := ob["tag"].(string)
  136. protocol, _ := ob["protocol"].(string)
  137. if protocol == "blackhole" || protocol == "freedom" || tag == "blocked" {
  138. return &TestOutboundResult{Mode: "tcp", Success: false, Error: "Outbound has no testable endpoint"}, nil
  139. }
  140. endpoints := extractOutboundEndpoints(ob)
  141. if len(endpoints) == 0 {
  142. return &TestOutboundResult{Mode: "tcp", Success: false, Error: "No testable endpoint"}, nil
  143. }
  144. results := make([]TestEndpointResult, len(endpoints))
  145. var wg sync.WaitGroup
  146. for i := range endpoints {
  147. wg.Add(1)
  148. go func(i int) {
  149. defer wg.Done()
  150. results[i] = probeEndpoint(endpoints[i], 5*time.Second)
  151. }(i)
  152. }
  153. wg.Wait()
  154. var bestDelay int64 = -1
  155. var firstErr string
  156. for _, r := range results {
  157. if r.Success {
  158. if bestDelay < 0 || r.Delay < bestDelay {
  159. bestDelay = r.Delay
  160. }
  161. } else if firstErr == "" {
  162. firstErr = r.Error
  163. }
  164. }
  165. mode := "tcp"
  166. if endpoints[0].Network == "udp" {
  167. mode = "udp"
  168. }
  169. out := &TestOutboundResult{Mode: mode, Endpoints: results}
  170. if bestDelay >= 0 {
  171. out.Success = true
  172. out.Delay = bestDelay
  173. } else {
  174. out.Error = firstErr
  175. if out.Error == "" {
  176. out.Error = "All endpoints unreachable"
  177. }
  178. }
  179. return out, nil
  180. }
  181. // outboundEndpoint is a host:port plus the transport its proxy actually
  182. // listens on. WireGuard (and WARP, which is WireGuard) is UDP-only, so a
  183. // TCP dial to its peer endpoint always times out — the probe must match
  184. // the transport of the outbound being tested.
  185. type outboundEndpoint struct {
  186. Address string
  187. Network string
  188. }
  189. func probeEndpoint(ep outboundEndpoint, timeout time.Duration) TestEndpointResult {
  190. if ep.Network == "udp" {
  191. return probeUDPEndpoint(ep.Address, timeout)
  192. }
  193. return probeTCPEndpoint(ep.Address, timeout)
  194. }
  195. func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult {
  196. r := TestEndpointResult{Address: endpoint}
  197. start := time.Now()
  198. conn, err := net.DialTimeout("tcp", endpoint, timeout)
  199. r.Delay = time.Since(start).Milliseconds()
  200. if err != nil {
  201. r.Error = err.Error()
  202. return r
  203. }
  204. conn.Close()
  205. r.Success = true
  206. return r
  207. }
  208. // probeUDPEndpoint sends a single byte and waits briefly for a reply or
  209. // an ICMP-driven error. WireGuard won't answer an unauthenticated byte,
  210. // so a read timeout is the normal "endpoint reachable" outcome; a
  211. // concrete error (e.g. ECONNREFUSED, "host unreachable") fails the probe.
  212. func probeUDPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult {
  213. r := TestEndpointResult{Address: endpoint}
  214. start := time.Now()
  215. conn, err := net.DialTimeout("udp", endpoint, timeout)
  216. if err != nil {
  217. r.Delay = time.Since(start).Milliseconds()
  218. r.Error = err.Error()
  219. return r
  220. }
  221. defer conn.Close()
  222. if _, werr := conn.Write([]byte{0}); werr != nil {
  223. r.Delay = time.Since(start).Milliseconds()
  224. r.Error = werr.Error()
  225. return r
  226. }
  227. _ = conn.SetReadDeadline(time.Now().Add(timeout))
  228. buf := make([]byte, 64)
  229. _, rerr := conn.Read(buf)
  230. r.Delay = time.Since(start).Milliseconds()
  231. if rerr != nil {
  232. if nerr, ok := rerr.(net.Error); ok && nerr.Timeout() {
  233. r.Success = true
  234. return r
  235. }
  236. r.Error = rerr.Error()
  237. return r
  238. }
  239. r.Success = true
  240. return r
  241. }
  242. func extractOutboundEndpoints(ob map[string]any) []outboundEndpoint {
  243. protocol, _ := ob["protocol"].(string)
  244. settings, _ := ob["settings"].(map[string]any)
  245. if settings == nil {
  246. return nil
  247. }
  248. // Hysteria is QUIC/UDP — detect via the outer protocol or via
  249. // streamSettings.network so a trojan-with-hysteria transport gets
  250. // probed over UDP too. kcp and quic are also UDP-based.
  251. network := "tcp"
  252. if protocol == "hysteria" || protocol == "wireguard" {
  253. network = "udp"
  254. }
  255. if stream, ok := ob["streamSettings"].(map[string]any); ok {
  256. if n, _ := stream["network"].(string); n == "hysteria" || n == "kcp" || n == "quic" {
  257. network = "udp"
  258. }
  259. }
  260. var out []outboundEndpoint
  261. addServer := func(addr any, port any) {
  262. host, _ := addr.(string)
  263. p := numAsInt(port)
  264. if host != "" && p > 0 {
  265. out = append(out, outboundEndpoint{Address: fmt.Sprintf("%s:%d", host, p), Network: network})
  266. }
  267. }
  268. switch protocol {
  269. case "vmess":
  270. if vnext, ok := settings["vnext"].([]any); ok {
  271. for _, v := range vnext {
  272. if vm, ok := v.(map[string]any); ok {
  273. addServer(vm["address"], vm["port"])
  274. }
  275. }
  276. }
  277. case "vless":
  278. addServer(settings["address"], settings["port"])
  279. case "hysteria":
  280. addServer(settings["address"], settings["port"])
  281. case "trojan", "shadowsocks", "http", "socks":
  282. if servers, ok := settings["servers"].([]any); ok {
  283. for _, sv := range servers {
  284. if sm, ok := sv.(map[string]any); ok {
  285. addServer(sm["address"], sm["port"])
  286. }
  287. }
  288. }
  289. case "wireguard":
  290. if peers, ok := settings["peers"].([]any); ok {
  291. for _, p := range peers {
  292. if pm, ok := p.(map[string]any); ok {
  293. if ep, _ := pm["endpoint"].(string); ep != "" {
  294. out = append(out, outboundEndpoint{Address: ep, Network: network})
  295. }
  296. }
  297. }
  298. }
  299. }
  300. return out
  301. }
  302. func numAsInt(v any) int {
  303. switch n := v.(type) {
  304. case float64:
  305. return int(n)
  306. case int:
  307. return n
  308. case int64:
  309. return int(n)
  310. case string:
  311. if i, err := strconv.Atoi(n); err == nil {
  312. return i
  313. }
  314. }
  315. return 0
  316. }
  317. // testOutboundHTTP spins up a temporary xray instance whose only job is
  318. // to run a burstObservatory probe against the target outbound, then polls
  319. // xray's metrics /debug/vars endpoint until that outbound is reported
  320. // alive (success) or the deadline expires (failure). The probe lives
  321. // inside xray, so the measured delay and any failure reason reflect what
  322. // xray itself sees over the real proxy chain — no SOCKS round-trip on
  323. // the client side.
  324. func (s *OutboundService) testOutboundHTTP(outboundJSON string, testURL string, allOutboundsJSON string) (*TestOutboundResult, error) {
  325. if testURL == "" {
  326. testURL = "https://www.google.com/generate_204"
  327. }
  328. if !httpTestSemaphore.TryLock() {
  329. return &TestOutboundResult{
  330. Mode: "http",
  331. Success: false,
  332. Error: "Another outbound test is already running, please wait",
  333. }, nil
  334. }
  335. defer httpTestSemaphore.Unlock()
  336. var testOutbound map[string]any
  337. if err := json.Unmarshal([]byte(outboundJSON), &testOutbound); err != nil {
  338. return &TestOutboundResult{Mode: "http", Success: false, Error: fmt.Sprintf("Invalid outbound JSON: %v", err)}, nil
  339. }
  340. outboundTag, _ := testOutbound["tag"].(string)
  341. if outboundTag == "" {
  342. return &TestOutboundResult{Mode: "http", Success: false, Error: "Outbound has no tag"}, nil
  343. }
  344. if protocol, _ := testOutbound["protocol"].(string); protocol == "blackhole" || outboundTag == "blocked" {
  345. return &TestOutboundResult{Mode: "http", Success: false, Error: "Blocked/blackhole outbound cannot be tested"}, nil
  346. }
  347. var allOutbounds []any
  348. if allOutboundsJSON != "" {
  349. if err := json.Unmarshal([]byte(allOutboundsJSON), &allOutbounds); err != nil {
  350. return &TestOutboundResult{Mode: "http", Success: false, Error: fmt.Sprintf("Invalid allOutbounds JSON: %v", err)}, nil
  351. }
  352. }
  353. if len(allOutbounds) == 0 {
  354. allOutbounds = []any{testOutbound}
  355. }
  356. metricsPort, err := findAvailablePort()
  357. if err != nil {
  358. return &TestOutboundResult{Mode: "http", Success: false, Error: fmt.Sprintf("Failed to find available port: %v", err)}, nil
  359. }
  360. testConfig := s.createTestConfig(outboundTag, allOutbounds, metricsPort, testURL)
  361. testConfigPath, err := createTestConfigPath()
  362. if err != nil {
  363. return &TestOutboundResult{Mode: "http", Success: false, Error: fmt.Sprintf("Failed to create test config path: %v", err)}, nil
  364. }
  365. defer os.Remove(testConfigPath)
  366. testProcess := xray.NewTestProcess(testConfig, testConfigPath)
  367. defer func() {
  368. if testProcess.IsRunning() {
  369. testProcess.Stop()
  370. }
  371. }()
  372. if err := testProcess.Start(); err != nil {
  373. return &TestOutboundResult{Mode: "http", Success: false, Error: fmt.Sprintf("Failed to start test xray instance: %v", err)}, nil
  374. }
  375. if err := waitForPort(metricsPort, 5*time.Second); err != nil {
  376. if !testProcess.IsRunning() {
  377. result := testProcess.GetResult()
  378. return &TestOutboundResult{Mode: "http", Success: false, Error: fmt.Sprintf("Xray process exited: %s", result)}, nil
  379. }
  380. return &TestOutboundResult{Mode: "http", Success: false, Error: fmt.Sprintf("Xray failed to start metrics listener: %v", err)}, nil
  381. }
  382. if !testProcess.IsRunning() {
  383. result := testProcess.GetResult()
  384. return &TestOutboundResult{Mode: "http", Success: false, Error: fmt.Sprintf("Xray process exited: %s", result)}, nil
  385. }
  386. return pollObservatoryResult(testProcess, metricsPort, outboundTag, 12*time.Second), nil
  387. }
  388. // createTestConfig builds a probe-only xray config: the original outbounds
  389. // are kept as-is so dialerProxy chains still resolve, a burstObservatory
  390. // is wired to probe the target tag, and a metrics listener exposes the
  391. // observatory snapshot via /debug/vars. No inbound or routing rules are
  392. // needed — burstObservatory issues the probe traffic itself.
  393. func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []any, metricsPort int, probeURL string) *xray.Config {
  394. processedOutbounds := make([]any, len(allOutbounds))
  395. for i, ob := range allOutbounds {
  396. outbound, ok := ob.(map[string]any)
  397. if !ok {
  398. processedOutbounds[i] = ob
  399. continue
  400. }
  401. if protocol, ok := outbound["protocol"].(string); ok && protocol == "wireguard" {
  402. if settings, ok := outbound["settings"].(map[string]any); ok {
  403. settings["noKernelTun"] = true
  404. } else {
  405. outbound["settings"] = map[string]any{"noKernelTun": true}
  406. }
  407. }
  408. processedOutbounds[i] = outbound
  409. }
  410. outboundsJSON, _ := json.Marshal(processedOutbounds)
  411. routingJSON, _ := json.Marshal(map[string]any{
  412. "domainStrategy": "AsIs",
  413. "rules": []any{},
  414. })
  415. burstObservatoryJSON, _ := json.Marshal(map[string]any{
  416. "subjectSelector": []string{outboundTag},
  417. "pingConfig": map[string]any{
  418. "destination": probeURL,
  419. "interval": "1s",
  420. "connectivity": "",
  421. "timeout": "5s",
  422. "samplingCount": 1,
  423. },
  424. })
  425. metricsJSON, _ := json.Marshal(map[string]any{
  426. "tag": "test-metrics",
  427. "listen": fmt.Sprintf("127.0.0.1:%d", metricsPort),
  428. })
  429. logConfig := map[string]any{
  430. "loglevel": "warning",
  431. "access": "none",
  432. "error": "none",
  433. "dnsLog": false,
  434. }
  435. logJSON, _ := json.Marshal(logConfig)
  436. cfg := &xray.Config{
  437. LogConfig: json_util.RawMessage(logJSON),
  438. InboundConfigs: []xray.InboundConfig{},
  439. OutboundConfigs: json_util.RawMessage(string(outboundsJSON)),
  440. RouterConfig: json_util.RawMessage(string(routingJSON)),
  441. Policy: json_util.RawMessage(`{}`),
  442. Stats: json_util.RawMessage(`{}`),
  443. BurstObservatory: json_util.RawMessage(string(burstObservatoryJSON)),
  444. Metrics: json_util.RawMessage(string(metricsJSON)),
  445. }
  446. return cfg
  447. }
  448. // observatoryEntry mirrors the per-outbound shape published by xray's
  449. // observatory under /debug/vars.
  450. type observatoryEntry struct {
  451. Alive bool `json:"alive"`
  452. Delay int64 `json:"delay"`
  453. LastSeenTime int64 `json:"last_seen_time"`
  454. LastTryTime int64 `json:"last_try_time"`
  455. OutboundTag string `json:"outbound_tag"`
  456. }
  457. // pollObservatoryResult repeatedly reads /debug/vars and returns as soon
  458. // as the target outbound reports alive=true. burstObservatory updates the
  459. // snapshot after each ping (interval=1s, timeout=5s), so a healthy
  460. // outbound usually surfaces within ~2s and the timeout caps the wait for
  461. // truly dead ones.
  462. func pollObservatoryResult(testProcess *xray.Process, metricsPort int, tag string, timeout time.Duration) *TestOutboundResult {
  463. url := fmt.Sprintf("http://127.0.0.1:%d/debug/vars", metricsPort)
  464. client := &http.Client{Timeout: 2 * time.Second}
  465. deadline := time.Now().Add(timeout)
  466. var lastEntry observatoryEntry
  467. var sawEntry bool
  468. for time.Now().Before(deadline) {
  469. if !testProcess.IsRunning() {
  470. result := testProcess.GetResult()
  471. return &TestOutboundResult{Mode: "http", Success: false, Error: fmt.Sprintf("Xray process exited: %s", result)}
  472. }
  473. entry, ok := fetchObservatoryEntry(client, url, tag)
  474. if ok {
  475. if entry.Alive {
  476. delay := entry.Delay
  477. if delay <= 0 {
  478. delay = 1
  479. }
  480. return &TestOutboundResult{Mode: "http", Success: true, Delay: delay}
  481. }
  482. lastEntry = entry
  483. sawEntry = true
  484. }
  485. time.Sleep(400 * time.Millisecond)
  486. }
  487. msg := "Probe timed out — outbound did not become reachable"
  488. if sawEntry && lastEntry.LastTryTime > 0 {
  489. msg = fmt.Sprintf("All probes failed (last attempt %ds ago)", time.Now().Unix()-lastEntry.LastTryTime)
  490. }
  491. return &TestOutboundResult{Mode: "http", Success: false, Error: msg}
  492. }
  493. func fetchObservatoryEntry(client *http.Client, url, tag string) (observatoryEntry, bool) {
  494. resp, err := client.Get(url)
  495. if err != nil {
  496. return observatoryEntry{}, false
  497. }
  498. defer resp.Body.Close()
  499. if resp.StatusCode != http.StatusOK {
  500. return observatoryEntry{}, false
  501. }
  502. var payload struct {
  503. Observatory map[string]observatoryEntry `json:"observatory"`
  504. }
  505. if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
  506. return observatoryEntry{}, false
  507. }
  508. if entry, ok := payload.Observatory[tag]; ok {
  509. return entry, true
  510. }
  511. for _, entry := range payload.Observatory {
  512. if entry.OutboundTag == tag {
  513. return entry, true
  514. }
  515. }
  516. return observatoryEntry{}, false
  517. }
  518. // waitForPort polls until the given TCP port is accepting connections or the timeout expires.
  519. func waitForPort(port int, timeout time.Duration) error {
  520. deadline := time.Now().Add(timeout)
  521. for time.Now().Before(deadline) {
  522. conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), 100*time.Millisecond)
  523. if err == nil {
  524. conn.Close()
  525. return nil
  526. }
  527. time.Sleep(50 * time.Millisecond)
  528. }
  529. return fmt.Errorf("port %d not ready after %v", port, timeout)
  530. }
  531. // findAvailablePort finds an available port for testing
  532. func findAvailablePort() (int, error) {
  533. listener, err := net.Listen("tcp", ":0")
  534. if err != nil {
  535. return 0, err
  536. }
  537. defer listener.Close()
  538. addr := listener.Addr().(*net.TCPAddr)
  539. return addr.Port, nil
  540. }
  541. // createTestConfigPath returns a unique path for a temporary xray config file in the bin folder.
  542. // The temp file is created and closed so the path is reserved; Start() will overwrite it.
  543. func createTestConfigPath() (string, error) {
  544. tmpFile, err := os.CreateTemp(config.GetBinFolderPath(), "xray_test_*.json")
  545. if err != nil {
  546. return "", err
  547. }
  548. path := tmpFile.Name()
  549. if err := tmpFile.Close(); err != nil {
  550. os.Remove(path)
  551. return "", err
  552. }
  553. return path, nil
  554. }