Introduce CoreController.QueryAllOutboundTrafficStats to collect and reset all outbound traffic counters, returning a single-line string of semicolon-separated entries in the format: tag,direction,value; It skips counters with non-positive values and returns an empty string if statsManager is nil or no relevant counters exist. Also update CheckVersionX version constant from 36 to 37.
391 lines
11 KiB
Go
391 lines
11 KiB
Go
package libv2ray
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
coreapplog "github.com/xtls/xray-core/app/log"
|
|
corecommlog "github.com/xtls/xray-core/common/log"
|
|
corenet "github.com/xtls/xray-core/common/net"
|
|
corefilesystem "github.com/xtls/xray-core/common/platform/filesystem"
|
|
"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"
|
|
_ "github.com/xtls/xray-core/main/distro/all"
|
|
browser_dialer "github.com/xtls/xray-core/transport/internet/browser_dialer"
|
|
mobasset "golang.org/x/mobile/asset"
|
|
)
|
|
|
|
// Constants for environment variables
|
|
const (
|
|
coreAsset = "xray.location.asset"
|
|
coreCert = "xray.location.cert"
|
|
xudpBaseKey = "xray.xudp.basekey"
|
|
tunFdKey = "xray.tun.fd"
|
|
browserDialerAddress = "xray.browser.dialer"
|
|
)
|
|
|
|
// CoreController represents a controller for managing Xray core instance lifecycle
|
|
type CoreController struct {
|
|
CallbackHandler CoreCallbackHandler
|
|
statsManager corestats.Manager
|
|
coreMutex sync.Mutex
|
|
coreInstance *core.Instance
|
|
IsRunning bool
|
|
}
|
|
|
|
// CoreCallbackHandler defines interface for receiving callbacks and notifications from the core service
|
|
type CoreCallbackHandler interface {
|
|
Startup() int
|
|
Shutdown() int
|
|
OnEmitStatus(int, string) int
|
|
}
|
|
|
|
// consoleLogWriter implements a log writer without datetime stamps
|
|
// as Android system already adds timestamps to each log line
|
|
type consoleLogWriter struct {
|
|
logger *log.Logger // Standard logger
|
|
}
|
|
|
|
// setEnvVariable safely sets an environment variable and logs any errors encountered.
|
|
func setEnvVariable(key, value string) {
|
|
if err := os.Setenv(key, value); err != nil {
|
|
log.Printf("Failed to set environment variable %s: %v. Please check your configuration.", key, err)
|
|
}
|
|
}
|
|
|
|
// InitCoreEnv initializes environment variables and file system handlers for the core
|
|
// It sets up asset path, certificate path, XUDP base key and customizes the file reader
|
|
// to support Android asset system
|
|
func InitCoreEnv(envPath string, key string) {
|
|
// Set asset/cert paths
|
|
if len(envPath) > 0 {
|
|
setEnvVariable(coreAsset, envPath)
|
|
setEnvVariable(coreCert, envPath)
|
|
}
|
|
|
|
// Set XUDP encryption key
|
|
if len(key) > 0 {
|
|
setEnvVariable(xudpBaseKey, key)
|
|
}
|
|
|
|
// Custom file reader with path validation
|
|
corefilesystem.NewFileReader = func(path string) (io.ReadCloser, error) {
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
_, file := filepath.Split(path)
|
|
return mobasset.Open(file)
|
|
}
|
|
return os.Open(path)
|
|
}
|
|
}
|
|
|
|
// NewCoreController initializes and returns a new CoreController instance
|
|
// Sets up the console log handler and associates it with the provided callback handler
|
|
func NewCoreController(s CoreCallbackHandler) *CoreController {
|
|
// Register custom logger
|
|
if err := coreapplog.RegisterHandlerCreator(
|
|
coreapplog.LogType_Console,
|
|
func(lt coreapplog.LogType, options coreapplog.HandlerCreatorOptions) (corecommlog.Handler, error) {
|
|
return corecommlog.NewLogger(createStdoutLogWriter()), nil
|
|
},
|
|
); err != nil {
|
|
log.Printf("Failed to register log handler: %v", err)
|
|
}
|
|
|
|
return &CoreController{
|
|
CallbackHandler: s,
|
|
}
|
|
}
|
|
|
|
// StartLoop initializes and starts the core processing loop
|
|
// Thread-safe method that configures and runs the Xray core with the provided configuration
|
|
// Returns immediately if the core is already running
|
|
func (x *CoreController) StartLoop(configContent string, tunFd int32) (err error) {
|
|
// Set TUN fd key, 0 means do not use TUN
|
|
setEnvVariable(tunFdKey, strconv.Itoa(int(tunFd)))
|
|
|
|
x.coreMutex.Lock()
|
|
defer x.coreMutex.Unlock()
|
|
|
|
if x.IsRunning {
|
|
log.Println("Core is already running")
|
|
return nil
|
|
}
|
|
|
|
return x.doStartLoop(configContent)
|
|
}
|
|
|
|
// StopLoop safely stops the core processing loop and releases resources
|
|
// Thread-safe method that shuts down the core instance and triggers necessary callbacks
|
|
func (x *CoreController) StopLoop() error {
|
|
x.coreMutex.Lock()
|
|
defer x.coreMutex.Unlock()
|
|
|
|
if x.IsRunning {
|
|
x.doShutdown()
|
|
x.CallbackHandler.OnEmitStatus(0, "Core stopped")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// QueryStats retrieves and resets traffic statistics for a specific outbound tag and direction
|
|
// Returns the accumulated traffic value and resets the counter to zero
|
|
// Returns 0 if the stats manager is not initialized or the counter doesn't exist
|
|
func (x *CoreController) QueryStats(tag string, direct string) int64 {
|
|
if x.statsManager == nil {
|
|
return 0
|
|
}
|
|
counter := x.statsManager.GetCounter(fmt.Sprintf("outbound>>>%s>>>traffic>>>%s", tag, direct))
|
|
if counter == nil {
|
|
return 0
|
|
}
|
|
return counter.Set(0)
|
|
}
|
|
|
|
// 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 // Skip counters with non-positive values
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Simplify config for testing
|
|
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)
|
|
}
|
|
|
|
// CheckVersionX returns the library and Xray versions
|
|
func CheckVersionX() string {
|
|
var version = 37
|
|
return fmt.Sprintf("Lib v%d, Xray-core v%s", version, core.Version())
|
|
}
|
|
|
|
// ReconcileBrowserDialer updates the browser dialer address and reloads its configuration
|
|
// If the dialer address is empty, it will disable the browser dialer and close existing connections
|
|
func ReconcileBrowserDialer(dialerAddr string) {
|
|
setEnvVariable(browserDialerAddress, dialerAddr)
|
|
browser_dialer.Reload()
|
|
}
|
|
|
|
// doShutdown shuts down the Xray instance and cleans up resources
|
|
func (x *CoreController) doShutdown() {
|
|
if x.coreInstance != nil {
|
|
if err := x.coreInstance.Close(); err != nil {
|
|
log.Printf("core shutdown error: %v", err)
|
|
}
|
|
x.coreInstance = nil
|
|
}
|
|
x.IsRunning = false
|
|
x.statsManager = nil
|
|
}
|
|
|
|
// doStartLoop sets up and starts the Xray core
|
|
func (x *CoreController) doStartLoop(configContent string) error {
|
|
log.Println("initializing core...")
|
|
config, err := coreserial.LoadJSONConfig(strings.NewReader(configContent))
|
|
if err != nil {
|
|
return fmt.Errorf("config error: %w", err)
|
|
}
|
|
|
|
x.coreInstance, err = core.New(config)
|
|
if err != nil {
|
|
return fmt.Errorf("core init failed: %w", err)
|
|
}
|
|
x.statsManager = x.coreInstance.GetFeature(corestats.ManagerType()).(corestats.Manager)
|
|
|
|
log.Println("starting core...")
|
|
x.IsRunning = true
|
|
if err := x.coreInstance.Start(); err != nil {
|
|
x.IsRunning = false
|
|
return fmt.Errorf("startup failed: %w", err)
|
|
}
|
|
|
|
x.CallbackHandler.Startup()
|
|
x.CallbackHandler.OnEmitStatus(0, "Started successfully, running")
|
|
|
|
log.Println("Starting core successfully")
|
|
return nil
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
if url == "" {
|
|
url = "https://www.google.com/generate_204"
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return -1, fmt.Errorf("failed to create HTTP request: %w", err)
|
|
}
|
|
|
|
var minDuration int64 = -1
|
|
success := false
|
|
var lastErr error
|
|
|
|
// Add exception handling and increase retry attempts
|
|
const attempts = 2
|
|
for i := 0; i < attempts; i++ {
|
|
select {
|
|
case <-ctx.Done():
|
|
// Return immediately when context is canceled
|
|
if !success {
|
|
return -1, ctx.Err()
|
|
}
|
|
return minDuration, nil
|
|
default:
|
|
// Continue execution
|
|
}
|
|
|
|
start := time.Now()
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
// Ensure response body is closed
|
|
defer func(resp *http.Response) {
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
}(resp)
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
|
lastErr = fmt.Errorf("invalid status: %s", resp.Status)
|
|
continue
|
|
}
|
|
|
|
// Handle possible errors when reading response body
|
|
if _, err := io.Copy(io.Discard, resp.Body); 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
|
|
}
|
|
|
|
// Log writer implementation
|
|
func (w *consoleLogWriter) Write(s string) error {
|
|
w.logger.Print(s)
|
|
return nil
|
|
}
|
|
|
|
func (w *consoleLogWriter) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// createStdoutLogWriter creates a logger that won't print date/time stamps
|
|
func createStdoutLogWriter() corecommlog.WriterCreator {
|
|
return func() corecommlog.Writer {
|
|
return &consoleLogWriter{
|
|
logger: log.New(os.Stdout, "", 0),
|
|
}
|
|
}
|
|
}
|