mirror of
https://github.com/2dust/AndroidLibXrayLite.git
synced 2025-06-28 14:19:51 +00:00
Refactor the code, simplify functions and features, and remove unnecessary features. (#141)
* Update main.yml * Update main.yml * Update libv2ray_main.go * Update libv2ray_main.go * Update libv2ray_main.go * Update libv2ray_main.go * Update libv2ray_main.go * Delete libv2ray_support_test.go * Delete libv2ray_support.go * Update libv2ray_main.go * Update libv2ray_main.go * Update main.yml
This commit is contained in:
parent
e01192ac83
commit
08aa01a2c2
4 changed files with 157 additions and 648 deletions
10
.github/workflows/main.yml
vendored
10
.github/workflows/main.yml
vendored
|
@ -52,6 +52,14 @@ jobs:
|
||||||
gomobile init
|
gomobile init
|
||||||
go mod tidy
|
go mod tidy
|
||||||
gomobile bind -v -androidapi 21 -trimpath -ldflags='-s -w -buildid=' ./
|
gomobile bind -v -androidapi 21 -trimpath -ldflags='-s -w -buildid=' ./
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
if: github.event.inputs.release_tag == ''
|
||||||
|
uses: actions/upload-artifact@v4.6.2
|
||||||
|
with:
|
||||||
|
name: libv2ray
|
||||||
|
path: |
|
||||||
|
${{ github.workspace }}/libv2ray*r
|
||||||
|
|
||||||
- name: Upload AndroidLibXrayLite to release
|
- name: Upload AndroidLibXrayLite to release
|
||||||
if: github.event.inputs.release_tag != ''
|
if: github.event.inputs.release_tag != ''
|
||||||
|
@ -59,4 +67,4 @@ jobs:
|
||||||
with:
|
with:
|
||||||
file: ./libv2ray*r
|
file: ./libv2ray*r
|
||||||
tag: ${{ github.event.inputs.release_tag }}
|
tag: ${{ github.event.inputs.release_tag }}
|
||||||
file_glob: true
|
file_glob: true
|
336
libv2ray_main.go
336
libv2ray_main.go
|
@ -14,185 +14,61 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
mobasset "golang.org/x/mobile/asset"
|
coreapplog "github.com/xtls/xray-core/app/log"
|
||||||
|
corecommlog "github.com/xtls/xray-core/common/log"
|
||||||
v2net "github.com/xtls/xray-core/common/net"
|
corenet "github.com/xtls/xray-core/common/net"
|
||||||
v2filesystem "github.com/xtls/xray-core/common/platform/filesystem"
|
corefilesystem "github.com/xtls/xray-core/common/platform/filesystem"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"github.com/xtls/xray-core/common/serial"
|
||||||
v2core "github.com/xtls/xray-core/core"
|
core "github.com/xtls/xray-core/core"
|
||||||
v2stats "github.com/xtls/xray-core/features/stats"
|
corestats "github.com/xtls/xray-core/features/stats"
|
||||||
v2serial "github.com/xtls/xray-core/infra/conf/serial"
|
coreserial "github.com/xtls/xray-core/infra/conf/serial"
|
||||||
_ "github.com/xtls/xray-core/main/distro/all"
|
_ "github.com/xtls/xray-core/main/distro/all"
|
||||||
v2internet "github.com/xtls/xray-core/transport/internet"
|
mobasset "golang.org/x/mobile/asset"
|
||||||
|
|
||||||
v2applog "github.com/xtls/xray-core/app/log"
|
|
||||||
v2commlog "github.com/xtls/xray-core/common/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
v2Asset = "xray.location.asset"
|
coreAsset = "xray.location.asset"
|
||||||
v2Cert = "xray.location.cert"
|
coreCert = "xray.location.cert"
|
||||||
xudpBaseKey = "xray.xudp.basekey"
|
xudpBaseKey = "xray.xudp.basekey"
|
||||||
)
|
)
|
||||||
|
|
||||||
// V2RayPoint represents a V2Ray Point Server
|
// CoreController represents a controller for managing Xray core instance lifecycle
|
||||||
type V2RayPoint struct {
|
type CoreController struct {
|
||||||
SupportSet V2RayVPNServiceSupportsSet
|
CallbackHandler CoreCallbackHandler
|
||||||
statsManager v2stats.Manager
|
statsManager corestats.Manager
|
||||||
|
coreMutex sync.Mutex
|
||||||
dialer *ProtectedDialer
|
CoreInstance *core.Instance
|
||||||
v2rayOP sync.Mutex
|
IsRunning bool
|
||||||
closeChan chan struct{}
|
|
||||||
|
|
||||||
Vpoint *v2core.Instance
|
|
||||||
IsRunning bool
|
|
||||||
|
|
||||||
DomainName string
|
|
||||||
ConfigureFileContent string
|
|
||||||
AsyncResolve bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// V2RayVPNServiceSupportsSet is an interface to support Android VPN mode
|
// CoreCallbackHandler defines interface for receiving callbacks and notifications from the core service
|
||||||
type V2RayVPNServiceSupportsSet interface {
|
type CoreCallbackHandler interface {
|
||||||
Setup(Conf string) int
|
Startup() int
|
||||||
Prepare() int
|
|
||||||
Shutdown() int
|
Shutdown() int
|
||||||
Protect(int) bool
|
Protect(int) bool
|
||||||
OnEmitStatus(int, string) int
|
OnEmitStatus(int, string) int
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunLoop runs the V2Ray main loop
|
// consoleLogWriter implements a log writer without datetime stamps
|
||||||
func (v *V2RayPoint) RunLoop(prefIPv6 bool) (err error) {
|
// as Android system already adds timestamps to each log line
|
||||||
v.v2rayOP.Lock()
|
type consoleLogWriter struct {
|
||||||
defer v.v2rayOP.Unlock()
|
logger *log.Logger
|
||||||
|
|
||||||
if v.IsRunning {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v.closeChan = make(chan struct{})
|
|
||||||
v.dialer.PrepareResolveChan()
|
|
||||||
|
|
||||||
go v.handleResolve()
|
|
||||||
|
|
||||||
prepareDomain := func() {
|
|
||||||
v.dialer.PrepareDomain(v.DomainName, v.closeChan, prefIPv6)
|
|
||||||
close(v.dialer.ResolveChan())
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.AsyncResolve {
|
|
||||||
go prepareDomain()
|
|
||||||
} else {
|
|
||||||
prepareDomain()
|
|
||||||
}
|
|
||||||
|
|
||||||
err = v.pointloop()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleResolve handles the resolution process for domains
|
// InitCoreEnv initializes environment variables and file system handlers for the core
|
||||||
func (v *V2RayPoint) handleResolve() {
|
// It sets up asset path, certificate path, XUDP base key and customizes the file reader
|
||||||
select {
|
// to support Android asset system
|
||||||
case <-v.dialer.ResolveChan():
|
func InitCoreEnv(envPath string, key string) {
|
||||||
if !v.dialer.IsVServerReady() {
|
|
||||||
log.Println("vServer cannot resolve, shutting down")
|
|
||||||
v.StopLoop()
|
|
||||||
v.SupportSet.Shutdown()
|
|
||||||
}
|
|
||||||
case <-v.closeChan:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopLoop stops the V2Ray main loop
|
|
||||||
func (v *V2RayPoint) StopLoop() error {
|
|
||||||
v.v2rayOP.Lock()
|
|
||||||
defer v.v2rayOP.Unlock()
|
|
||||||
|
|
||||||
if v.IsRunning {
|
|
||||||
close(v.closeChan)
|
|
||||||
v.shutdownInit()
|
|
||||||
v.SupportSet.OnEmitStatus(0, "Closed")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryStats returns the traffic stats for a given tag and direction
|
|
||||||
func (v V2RayPoint) QueryStats(tag string, direct string) int64 {
|
|
||||||
if v.statsManager == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
counter := v.statsManager.GetCounter(fmt.Sprintf("outbound>>>%s>>>traffic>>>%s", tag, direct))
|
|
||||||
if counter == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return counter.Set(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// shutdownInit shuts down the V2Ray instance and cleans up resources
|
|
||||||
func (v *V2RayPoint) shutdownInit() {
|
|
||||||
if v.Vpoint != nil {
|
|
||||||
v.Vpoint.Close()
|
|
||||||
v.Vpoint = nil
|
|
||||||
}
|
|
||||||
v.IsRunning = false
|
|
||||||
v.statsManager = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pointloop sets up and starts the V2Ray core
|
|
||||||
func (v *V2RayPoint) pointloop() error {
|
|
||||||
log.Println("Loading core config")
|
|
||||||
config, err := v2serial.LoadJSONConfig(strings.NewReader(v.ConfigureFileContent))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load core config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Creating new core instance")
|
|
||||||
v.Vpoint, err = v2core.New(config)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create core instance: %w", err)
|
|
||||||
}
|
|
||||||
v.statsManager = v.Vpoint.GetFeature(v2stats.ManagerType()).(v2stats.Manager)
|
|
||||||
|
|
||||||
log.Println("Starting core")
|
|
||||||
v.IsRunning = true
|
|
||||||
if err := v.Vpoint.Start(); err != nil {
|
|
||||||
v.IsRunning = false
|
|
||||||
return fmt.Errorf("failed to start core: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.SupportSet.Prepare()
|
|
||||||
v.SupportSet.Setup("")
|
|
||||||
v.SupportSet.OnEmitStatus(0, "Running")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MeasureDelay measures the delay to a given URL
|
|
||||||
func (v *V2RayPoint) MeasureDelay(url string) (int64, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-v.closeChan:
|
|
||||||
cancel()
|
|
||||||
case <-ctx.Done():
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return measureInstDelay(ctx, v.Vpoint, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitV2Env sets the V2Ray asset path
|
|
||||||
func InitV2Env(envPath string, key string) {
|
|
||||||
if len(envPath) > 0 {
|
if len(envPath) > 0 {
|
||||||
os.Setenv(v2Asset, envPath)
|
os.Setenv(coreAsset, envPath)
|
||||||
os.Setenv(v2Cert, envPath)
|
os.Setenv(coreCert, envPath)
|
||||||
}
|
}
|
||||||
if len(key) > 0 {
|
if len(key) > 0 {
|
||||||
os.Setenv(xudpBaseKey, key)
|
os.Setenv(xudpBaseKey, key)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v2filesystem.NewFileReader = func(path string) (io.ReadCloser, error) {
|
corefilesystem.NewFileReader = func(path string) (io.ReadCloser, error) {
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
_, file := filepath.Split(path)
|
_, file := filepath.Split(path)
|
||||||
return mobasset.Open(file)
|
return mobasset.Open(file)
|
||||||
|
@ -201,9 +77,77 @@ func InitV2Env(envPath string, key string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
coreapplog.RegisterHandlerCreator(coreapplog.LogType_Console,
|
||||||
|
func(lt coreapplog.LogType,
|
||||||
|
options coreapplog.HandlerCreatorOptions) (corecommlog.Handler, error) {
|
||||||
|
return corecommlog.NewLogger(createStdoutLogWriter()), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
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) (err error) {
|
||||||
|
x.coreMutex.Lock()
|
||||||
|
defer x.coreMutex.Unlock()
|
||||||
|
|
||||||
|
if x.IsRunning {
|
||||||
|
log.Println("The instance is already running")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = x.doStartLoop(configContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
log.Println("Shut down the running instance")
|
||||||
|
x.CallbackHandler.OnEmitStatus(0, "Closed")
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// MeasureOutboundDelay measures the outbound delay for a given configuration and URL
|
||||||
func MeasureOutboundDelay(ConfigureFileContent string, url string) (int64, error) {
|
func MeasureOutboundDelay(ConfigureFileContent string, url string) (int64, error) {
|
||||||
config, err := v2serial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
|
config, err := coreserial.LoadJSONConfig(strings.NewReader(ConfigureFileContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, fmt.Errorf("failed to load JSON config: %w", err)
|
return -1, fmt.Errorf("failed to load JSON config: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -217,7 +161,7 @@ func MeasureOutboundDelay(ConfigureFileContent string, url string) (int64, error
|
||||||
}
|
}
|
||||||
config.App = essentialApp
|
config.App = essentialApp
|
||||||
|
|
||||||
inst, err := v2core.New(config)
|
inst, err := core.New(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, fmt.Errorf("failed to create core instance: %w", err)
|
return -1, fmt.Errorf("failed to create core instance: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -227,31 +171,53 @@ func MeasureOutboundDelay(ConfigureFileContent string, url string) (int64, error
|
||||||
return measureInstDelay(context.Background(), inst, url)
|
return measureInstDelay(context.Background(), inst, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewV2RayPoint creates a new V2RayPoint instance
|
// CheckVersionX returns the library and Xray versions
|
||||||
func NewV2RayPoint(s V2RayVPNServiceSupportsSet, adns bool) *V2RayPoint {
|
func CheckVersionX() string {
|
||||||
v2applog.RegisterHandlerCreator(v2applog.LogType_Console,
|
var version = 31
|
||||||
func(lt v2applog.LogType,
|
return fmt.Sprintf("Lib v%d, Xray-core v%s", version, core.Version())
|
||||||
options v2applog.HandlerCreatorOptions) (v2commlog.Handler, error) {
|
|
||||||
return v2commlog.NewLogger(createStdoutLogWriter()), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
dialer := NewProtectedDialer(s)
|
|
||||||
v2internet.UseAlternativeSystemDialer(dialer)
|
|
||||||
return &V2RayPoint{
|
|
||||||
SupportSet: s,
|
|
||||||
dialer: dialer,
|
|
||||||
AsyncResolve: adns,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckVersionX returns the library and V2Ray versions
|
// doShutdown shuts down the Xray instance and cleans up resources
|
||||||
func CheckVersionX() string {
|
func (x *CoreController) doShutdown() {
|
||||||
var version = 30
|
if x.CoreInstance != nil {
|
||||||
return fmt.Sprintf("Lib v%d, Xray-core v%s", version, v2core.Version())
|
x.CoreInstance.Close()
|
||||||
|
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("Loading core config")
|
||||||
|
config, err := coreserial.LoadJSONConfig(strings.NewReader(configContent))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load core config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Creating new core instance")
|
||||||
|
x.CoreInstance, err = core.New(config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create core instance: %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("failed to start core: %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
|
// measureInstDelay measures the delay for an instance to a given URL
|
||||||
func measureInstDelay(ctx context.Context, inst *v2core.Instance, url string) (int64, error) {
|
func measureInstDelay(ctx context.Context, inst *core.Instance, url string) (int64, error) {
|
||||||
if inst == nil {
|
if inst == nil {
|
||||||
return -1, errors.New("core instance is nil")
|
return -1, errors.New("core instance is nil")
|
||||||
}
|
}
|
||||||
|
@ -260,11 +226,11 @@ func measureInstDelay(ctx context.Context, inst *v2core.Instance, url string) (i
|
||||||
TLSHandshakeTimeout: 6 * time.Second,
|
TLSHandshakeTimeout: 6 * time.Second,
|
||||||
DisableKeepAlives: true,
|
DisableKeepAlives: true,
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
dest, err := v2net.ParseDestination(fmt.Sprintf("%s:%s", network, addr))
|
dest, err := corenet.ParseDestination(fmt.Sprintf("%s:%s", network, addr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return v2core.Dial(ctx, inst, dest)
|
return core.Dial(ctx, inst, dest)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,12 +256,6 @@ func measureInstDelay(ctx context.Context, inst *v2core.Instance, url string) (i
|
||||||
return time.Since(start).Milliseconds(), nil
|
return time.Since(start).Milliseconds(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// consoleLogWriter creates our own log writer without datetime stamp
|
|
||||||
// As Android adds time stamps on each line
|
|
||||||
type consoleLogWriter struct {
|
|
||||||
logger *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *consoleLogWriter) Write(s string) error {
|
func (w *consoleLogWriter) Write(s string) error {
|
||||||
w.logger.Print(s)
|
w.logger.Print(s)
|
||||||
return nil
|
return nil
|
||||||
|
@ -306,8 +266,8 @@ func (w *consoleLogWriter) Close() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// createStdoutLogWriter creates a logger that won't print date/time stamps
|
// createStdoutLogWriter creates a logger that won't print date/time stamps
|
||||||
func createStdoutLogWriter() v2commlog.WriterCreator {
|
func createStdoutLogWriter() corecommlog.WriterCreator {
|
||||||
return func() v2commlog.Writer {
|
return func() corecommlog.Writer {
|
||||||
return &consoleLogWriter{
|
return &consoleLogWriter{
|
||||||
logger: log.New(os.Stdout, "", 0),
|
logger: log.New(os.Stdout, "", 0),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,314 +0,0 @@
|
||||||
package libv2ray
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
v2net "github.com/xtls/xray-core/common/net"
|
|
||||||
"github.com/xtls/xray-core/features/dns"
|
|
||||||
"github.com/xtls/xray-core/features/outbound"
|
|
||||||
v2internet "github.com/xtls/xray-core/transport/internet"
|
|
||||||
)
|
|
||||||
|
|
||||||
// protectSet defines an interface for protecting sockets
|
|
||||||
type protectSet interface {
|
|
||||||
Protect(int) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolved holds the resolved IP addresses and associated metadata
|
|
||||||
type resolved struct {
|
|
||||||
domain string
|
|
||||||
IPs []net.IP
|
|
||||||
Port int
|
|
||||||
lastResolved time.Time
|
|
||||||
ipIdx uint8
|
|
||||||
ipLock sync.Mutex
|
|
||||||
lastSwitched time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextIP switches to another resolved IP address
|
|
||||||
func (r *resolved) NextIP() {
|
|
||||||
r.ipLock.Lock()
|
|
||||||
defer r.ipLock.Unlock()
|
|
||||||
|
|
||||||
if len(r.IPs) > 1 {
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(r.lastSwitched) < 5*time.Second {
|
|
||||||
log.Println("switch too quickly")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.lastSwitched = now
|
|
||||||
r.ipIdx++
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ipIdx >= uint8(len(r.IPs)) {
|
|
||||||
r.ipIdx = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("switched to next IP: %v", r.IPs[r.ipIdx])
|
|
||||||
}
|
|
||||||
|
|
||||||
// currentIP returns the current IP address
|
|
||||||
func (r *resolved) currentIP() net.IP {
|
|
||||||
r.ipLock.Lock()
|
|
||||||
defer r.ipLock.Unlock()
|
|
||||||
if len(r.IPs) > 0 {
|
|
||||||
return r.IPs[r.ipIdx]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewProtectedDialer creates a new ProtectedDialer
|
|
||||||
func NewProtectedDialer(p protectSet) *ProtectedDialer {
|
|
||||||
return &ProtectedDialer{
|
|
||||||
resolver: &net.Resolver{PreferGo: false},
|
|
||||||
protectSet: p,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProtectedDialer handles protected dialing
|
|
||||||
type ProtectedDialer struct {
|
|
||||||
currentServer string
|
|
||||||
resolveChan chan struct{}
|
|
||||||
preferIPv6 bool
|
|
||||||
|
|
||||||
vServer *resolved
|
|
||||||
resolver *net.Resolver
|
|
||||||
|
|
||||||
protectSet
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsVServerReady checks if the virtual server is ready
|
|
||||||
func (d *ProtectedDialer) IsVServerReady() bool {
|
|
||||||
return d.vServer != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareResolveChan prepares the resolve channel
|
|
||||||
func (d *ProtectedDialer) PrepareResolveChan() {
|
|
||||||
d.resolveChan = make(chan struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveChan returns the resolve channel
|
|
||||||
func (d *ProtectedDialer) ResolveChan() chan struct{} {
|
|
||||||
return d.resolveChan
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookupAddr performs DNS resolution for the given address
|
|
||||||
func (d *ProtectedDialer) lookupAddr(addr string) (*resolved, error) {
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
host, port string
|
|
||||||
portnum int
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if host, port, err = net.SplitHostPort(addr); err != nil {
|
|
||||||
log.Printf("PrepareDomain SplitHostPort Err: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if portnum, err = d.resolver.LookupPort(ctx, "tcp", port); err != nil {
|
|
||||||
log.Printf("PrepareDomain LookupPort Err: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := d.resolver.LookupIPAddr(ctx, host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(addrs) == 0 {
|
|
||||||
return nil, fmt.Errorf("domain %s Failed to resolve", addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
IPs := make([]net.IP, 0)
|
|
||||||
if d.preferIPv6 {
|
|
||||||
for _, ia := range addrs {
|
|
||||||
if ia.IP.To4() == nil {
|
|
||||||
IPs = append(IPs, ia.IP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, ia := range addrs {
|
|
||||||
if ia.IP.To4() != nil {
|
|
||||||
IPs = append(IPs, ia.IP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !d.preferIPv6 {
|
|
||||||
for _, ia := range addrs {
|
|
||||||
if ia.IP.To4() == nil {
|
|
||||||
IPs = append(IPs, ia.IP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &resolved{
|
|
||||||
domain: host,
|
|
||||||
IPs: IPs,
|
|
||||||
Port: portnum,
|
|
||||||
lastResolved: time.Now(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareDomain caches the resolved IP addresses for the given domain
|
|
||||||
func (d *ProtectedDialer) PrepareDomain(domainName string, closeCh <-chan struct{}, prefIPv6 bool) {
|
|
||||||
log.Printf("Preparing Domain: %s", domainName)
|
|
||||||
d.currentServer = domainName
|
|
||||||
d.preferIPv6 = prefIPv6
|
|
||||||
|
|
||||||
maxRetry := 10
|
|
||||||
for {
|
|
||||||
if maxRetry == 0 {
|
|
||||||
log.Println("PrepareDomain maxRetry reached. exiting.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved, err := d.lookupAddr(domainName)
|
|
||||||
if err != nil {
|
|
||||||
maxRetry--
|
|
||||||
log.Printf("PrepareDomain err: %v\n", err)
|
|
||||||
select {
|
|
||||||
case <-closeCh:
|
|
||||||
log.Printf("PrepareDomain exit due to core closed")
|
|
||||||
return
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
d.vServer = resolved
|
|
||||||
log.Printf("Prepare Result:\n Domain: %s\n Port: %d\n IPs: %v\n",
|
|
||||||
resolved.domain, resolved.Port, resolved.IPs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFd returns a file descriptor for the given network
|
|
||||||
func (d *ProtectedDialer) getFd(network v2net.Network) (fd int, err error) {
|
|
||||||
switch network {
|
|
||||||
case v2net.Network_TCP:
|
|
||||||
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP)
|
|
||||||
case v2net.Network_UDP:
|
|
||||||
fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unknown network")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes the dialer
|
|
||||||
func (d *ProtectedDialer) Init(_ dns.Client, _ outbound.Manager) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial performs a protected dial
|
|
||||||
func (d *ProtectedDialer) Dial(ctx context.Context,
|
|
||||||
src v2net.Address, dest v2net.Destination, sockopt *v2internet.SocketConfig) (net.Conn, error) {
|
|
||||||
|
|
||||||
Address := dest.NetAddr()
|
|
||||||
|
|
||||||
if Address == d.currentServer {
|
|
||||||
if d.vServer == nil {
|
|
||||||
log.Println("Dial pending prepare ...", Address)
|
|
||||||
<-d.resolveChan
|
|
||||||
|
|
||||||
if d.vServer == nil {
|
|
||||||
return nil, fmt.Errorf("fail to prepare domain %s", d.currentServer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fd, err := d.getFd(dest.Network)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
curIP := d.vServer.currentIP()
|
|
||||||
conn, err := d.fdConn(ctx, curIP, d.vServer.Port, dest.Network, fd)
|
|
||||||
if err != nil {
|
|
||||||
d.vServer.NextIP()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Printf("Using Prepared: %s", curIP)
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved, err := d.lookupAddr(Address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fd, err := d.getFd(dest.Network)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.fdConn(ctx, resolved.IPs[0], resolved.Port, dest.Network, fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DestIpAddress returns the destination IP address
|
|
||||||
func (d *ProtectedDialer) DestIpAddress() net.IP {
|
|
||||||
return d.vServer.currentIP()
|
|
||||||
}
|
|
||||||
|
|
||||||
// fdConn establishes a connection using the given file descriptor
|
|
||||||
func (d *ProtectedDialer) fdConn(ctx context.Context, ip net.IP, port int, network v2net.Network, fd int) (net.Conn, error) {
|
|
||||||
defer unix.Close(fd)
|
|
||||||
|
|
||||||
if !d.Protect(fd) {
|
|
||||||
log.Printf("fdConn fail to protect, Close Fd: %d", fd)
|
|
||||||
return nil, errors.New("fail to protect")
|
|
||||||
}
|
|
||||||
|
|
||||||
sa := &unix.SockaddrInet6{
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
copy(sa.Addr[:], ip.To16())
|
|
||||||
|
|
||||||
if network == v2net.Network_UDP {
|
|
||||||
if err := unix.Bind(fd, &unix.SockaddrInet6{}); err != nil {
|
|
||||||
log.Printf("fdConn unix.Bind err, Close Fd: %d Err: %v", fd, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := unix.Connect(fd, sa); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file := os.NewFile(uintptr(fd), "Socket")
|
|
||||||
if file == nil {
|
|
||||||
return nil, errors.New("fdConn fd invalid")
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
if network == v2net.Network_UDP {
|
|
||||||
packetConn, err := net.FilePacketConn(file)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("fdConn FilePacketConn Close Fd: %d Err: %v", fd, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &v2internet.PacketConnWrapper{
|
|
||||||
Conn: packetConn,
|
|
||||||
Dest: &net.UDPAddr{
|
|
||||||
IP: ip,
|
|
||||||
Port: port,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
} else {
|
|
||||||
conn, err := net.FileConn(file)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("fdConn FileConn Close Fd: %d Err: %v", fd, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,145 +0,0 @@
|
||||||
package libv2ray
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
v2net "github.com/xtls/xray-core/common/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
// fakeSupportSet is a mock implementation of the protectSet interface
|
|
||||||
type fakeSupportSet struct{}
|
|
||||||
|
|
||||||
// Protect is a mock implementation that always returns true
|
|
||||||
func (f fakeSupportSet) Protect(int) bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProtectedDialer_PrepareDomain tests the PrepareDomain method of the ProtectedDialer
|
|
||||||
func TestProtectedDialer_PrepareDomain(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
domainName string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
}{
|
|
||||||
{"Test with baidu.com", args{"baidu.com:80"}},
|
|
||||||
// Add more test cases if needed
|
|
||||||
}
|
|
||||||
d := NewProtectedDialer(fakeSupportSet{})
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
ch := make(chan struct{})
|
|
||||||
go d.PrepareDomain(tt.args.domainName, ch, false)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
d.vServer.NextIP()
|
|
||||||
t.Log(d.vServer.currentIP())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProtectedDialer_Dial tests the Dial method of the ProtectedDialer
|
|
||||||
func TestProtectedDialer_Dial(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{"baidu.com:80", false},
|
|
||||||
{"cloudflare.com:80", false},
|
|
||||||
{"172.16.192.11:80", true},
|
|
||||||
// Add more test cases if needed
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
ch := make(chan struct{})
|
|
||||||
|
|
||||||
d := NewProtectedDialer(fakeSupportSet{})
|
|
||||||
d.currentServer = tt.name
|
|
||||||
|
|
||||||
go d.PrepareDomain(tt.name, ch, false)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
dial := func() {
|
|
||||||
defer wg.Done()
|
|
||||||
dest, _ := v2net.ParseDestination("tcp:" + tt.name)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
conn, err := d.Dial(ctx, nil, dest, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Log(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
host, _, _ := net.SplitHostPort(tt.name)
|
|
||||||
fmt.Fprintf(conn, "GET / HTTP/1.1\r\nHost: %s\r\n\r\n", host)
|
|
||||||
status, err := bufio.NewReader(conn).ReadString('\n')
|
|
||||||
t.Logf("Status: %s, Error: %v", status, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for n := 0; n < 3; n++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go dial()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test_resolved_NextIP tests the NextIP method of the resolved struct
|
|
||||||
func Test_resolved_NextIP(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
domain string
|
|
||||||
IPs []net.IP
|
|
||||||
Port int
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
}{
|
|
||||||
{"test1", fields{
|
|
||||||
domain: "www.baidu.com",
|
|
||||||
IPs: []net.IP{
|
|
||||||
net.ParseIP("1.2.3.4"),
|
|
||||||
net.ParseIP("4.3.2.1"),
|
|
||||||
net.ParseIP("1234::1"),
|
|
||||||
net.ParseIP("4321::1"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
r := &resolved{
|
|
||||||
domain: tt.fields.domain,
|
|
||||||
IPs: tt.fields.IPs,
|
|
||||||
Port: tt.fields.Port,
|
|
||||||
}
|
|
||||||
t.Logf("Initial IPs: %v", r.IPs)
|
|
||||||
t.Logf("Current IP: %v", r.currentIP())
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("Next IP: %v", r.currentIP())
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("Next IP: %v", r.currentIP())
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("Next IP: %v", r.currentIP())
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("Next IP: %v", r.currentIP())
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
r.NextIP()
|
|
||||||
t.Logf("Next IP: %v", r.currentIP())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue