Files
2dust fd8b3ce164 Split delay/stats helpers into utils file
Moves outbound traffic and delay-related helper methods out of `libv2ray_main.go` into a new `libv2ray_utils.go` to keep core lifecycle code focused. The refactor also trims now-unused imports in the main file, removes the old `QueryStats` helper, and bumps `libVersion` from 39 to 40.
2026-08-20 14:57:13 +08:00

177 lines
4.3 KiB
Go

package libv2ray
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"time"
corenet "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/serial"
core "github.com/xtls/xray-core/core"
corestats "github.com/xtls/xray-core/features/stats"
coreserial "github.com/xtls/xray-core/infra/conf/serial"
)
// QueryAllOutboundTrafficStats retrieves and resets all outbound traffic counters.
// Returns a single-line text in format: tag,direction,value;tag,direction,value;
// Returns an empty string if the stats manager is not initialized or no counters exist.
func (x *CoreController) QueryAllOutboundTrafficStats() string {
if x.statsManager == nil {
return ""
}
var b strings.Builder
x.statsManager.VisitCounters(func(name string, counter corestats.Counter) bool {
parts := strings.Split(name, ">>>")
if len(parts) != 4 || parts[0] != "outbound" || parts[2] != "traffic" {
return true
}
tag := parts[1]
direct := parts[3]
value := counter.Set(0)
if value <= 0 {
return true
}
b.WriteString(tag)
b.WriteByte(',')
b.WriteString(direct)
b.WriteByte(',')
b.WriteString(strconv.FormatInt(value, 10))
b.WriteByte(';')
return true
})
return b.String()
}
// MeasureDelay measures network latency to a specified URL through the current core instance
// Uses a 12-second timeout context and returns the round-trip time in milliseconds
// An error is returned if the connection fails or returns an unexpected status
func (x *CoreController) MeasureDelay(url string) (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
defer cancel()
return measureInstDelay(ctx, x.coreInstance, url)
}
// MeasureOutboundDelay measures the outbound delay for a given configuration and URL
func MeasureOutboundDelay(ConfigureFileContent string, url string) (int64, error) {
config, err := coreserial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
if err != nil {
return -1, fmt.Errorf("config load error: %w", err)
}
config.Inbound = nil
var essentialApp []*serial.TypedMessage
for _, app := range config.App {
if app.Type == "xray.app.proxyman.OutboundConfig" ||
app.Type == "xray.app.dispatcher.Config" ||
app.Type == "xray.app.log.Config" {
essentialApp = append(essentialApp, app)
}
}
config.App = essentialApp
inst, err := core.New(config)
if err != nil {
return -1, fmt.Errorf("instance creation failed: %w", err)
}
if err := inst.Start(); err != nil {
return -1, fmt.Errorf("startup failed: %w", err)
}
defer inst.Close()
return measureInstDelay(context.Background(), inst, url)
}
// measureInstDelay measures the delay for an instance to a given URL
func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int64, error) {
if inst == nil {
return -1, errors.New("core instance is nil")
}
if url == "" {
url = "https://www.google.com/generate_204"
}
tr := &http.Transport{
TLSHandshakeTimeout: 6 * time.Second,
DisableKeepAlives: false,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := corenet.ParseDestination(fmt.Sprintf("%s:%s", network, addr))
if err != nil {
return nil, err
}
return core.Dial(ctx, inst, dest)
},
}
client := &http.Client{
Transport: tr,
Timeout: 12 * time.Second,
}
var minDuration int64 = -1
success := false
var lastErr error
defer tr.CloseIdleConnections()
const attempts = 2
for i := 0; i < attempts; i++ {
select {
case <-ctx.Done():
if !success {
return -1, ctx.Err()
}
return minDuration, nil
default:
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
lastErr = fmt.Errorf("failed to create HTTP request: %w", err)
continue
}
start := time.Now()
resp, err := client.Do(req)
if err != nil {
lastErr = err
continue
}
_, err = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
lastErr = fmt.Errorf("invalid status: %s", resp.Status)
continue
}
if err != nil {
lastErr = fmt.Errorf("failed to read response body: %w", err)
continue
}
duration := time.Since(start).Milliseconds()
if !success || duration < minDuration {
minDuration = duration
}
success = true
}
if !success {
return -1, lastErr
}
return minDuration, nil
}