diff --git a/.gitignore b/.gitignore index d1d8265..f28048a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,21 @@ gradle/build.gradle *.pb.go binary*.go conf/demo/ - demo/ assets/ libv2ray*.[a|j]ar + +# Ignore binary files +*.exe +*.dll +*.so +*.dylib + +# Ignore backup and temporary files +*~ +*.swp +*.swo + +# Ignore build and log directories +build/ +logs/ diff --git a/libv2ray_main.go b/libv2ray_main.go index 35341bf..f3f029d 100644 --- a/libv2ray_main.go +++ b/libv2ray_main.go @@ -35,10 +35,7 @@ const ( xudpBaseKey = "xray.xudp.basekey" ) -/* -V2RayPoint V2Ray Point Server -This is territory of Go, so no getter and setters! -*/ +// V2RayPoint represents a V2Ray Point Server type V2RayPoint struct { SupportSet V2RayVPNServiceSupportsSet statsManager v2stats.Manager @@ -55,7 +52,7 @@ type V2RayPoint struct { AsyncResolve bool } -/*V2RayVPNServiceSupportsSet To support Android VPN mode*/ +// V2RayVPNServiceSupportsSet is an interface to support Android VPN mode type V2RayVPNServiceSupportsSet interface { Setup(Conf string) int Prepare() int @@ -64,14 +61,13 @@ type V2RayVPNServiceSupportsSet interface { OnEmitStatus(int, string) int } -/*RunLoop Run V2Ray main loop - */ +// RunLoop runs the V2Ray main loop func (v *V2RayPoint) RunLoop(prefIPv6 bool) (err error) { v.v2rayOP.Lock() defer v.v2rayOP.Unlock() if v.IsRunning { - return + return nil } v.closeChan = make(chan struct{}) @@ -94,11 +90,12 @@ func (v *V2RayPoint) RunLoop(prefIPv6 bool) (err error) { return } +// handleResolve handles the resolution process for domains func (v *V2RayPoint) handleResolve() { select { case <-v.dialer.ResolveChan(): if !v.dialer.IsVServerReady() { - log.Println("vServer cannot resolved, shutdown") + log.Println("vServer cannot resolve, shutting down") v.StopLoop() v.SupportSet.Shutdown() } @@ -106,20 +103,20 @@ func (v *V2RayPoint) handleResolve() { } } -/*StopLoop Stop V2Ray main loop - */ -func (v *V2RayPoint) StopLoop() (err error) { +// 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 + return nil } -// Delegate Function +// 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 @@ -131,36 +128,36 @@ func (v V2RayPoint) QueryStats(tag string, direct string) int64 { 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.Vpoint.Close() - v.Vpoint = nil v.statsManager = nil } +// pointloop sets up and starts the V2Ray core func (v *V2RayPoint) pointloop() error { - log.Println("loading core config") + log.Println("Loading core config") config, err := v2serial.LoadJSONConfig(strings.NewReader(v.ConfigureFileContent)) if err != nil { - log.Println(err) - return err + return fmt.Errorf("failed to load core config: %w", err) } - log.Println("new core") + log.Println("Creating new core instance") v.Vpoint, err = v2core.New(config) if err != nil { - v.Vpoint = nil - log.Println(err) - return err + return fmt.Errorf("failed to create core instance: %w", err) } v.statsManager = v.Vpoint.GetFeature(v2stats.ManagerType()).(v2stats.Manager) - log.Println("start core") + log.Println("Starting core") v.IsRunning = true if err := v.Vpoint.Start(); err != nil { v.IsRunning = false - log.Println(err) - return err + return fmt.Errorf("failed to start core: %w", err) } v.SupportSet.Prepare() @@ -169,13 +166,14 @@ func (v *V2RayPoint) pointloop() error { 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 request if close called during measure cancel() case <-ctx.Done(): } @@ -184,10 +182,8 @@ func (v *V2RayPoint) MeasureDelay(url string) (int64, error) { return measureInstDelay(ctx, v.Vpoint, url) } -// InitV2Env set v2 asset path +// InitV2Env sets the V2Ray asset path func InitV2Env(envPath string, key string) { - //Initialize asset API, Since Raymond Will not let notify the asset location inside Process, - //We need to set location outside V2Ray if len(envPath) > 0 { os.Setenv(v2Asset, envPath) os.Setenv(v2Cert, envPath) @@ -196,7 +192,6 @@ func InitV2Env(envPath string, key string) { os.Setenv(xudpBaseKey, key) } - //Now we handle read, fallback to gomobile asset (apk assets) v2filesystem.NewFileReader = func(path string) (io.ReadCloser, error) { if _, err := os.Stat(path); os.IsNotExist(err) { _, file := filepath.Split(path) @@ -206,16 +201,14 @@ func InitV2Env(envPath string, key string) { } } +// MeasureOutboundDelay measures the outbound delay for a given configuration and URL func MeasureOutboundDelay(ConfigureFileContent string, url string) (int64, error) { config, err := v2serial.LoadJSONConfig(strings.NewReader(ConfigureFileContent)) if err != nil { - return -1, err + return -1, fmt.Errorf("failed to load JSON config: %w", err) } - // don't listen to anything for test purpose config.Inbound = nil - // config.App: (fakedns), log, dispatcher, InboundConfig, OutboundConfig, (stats), router, dns, (policy) - // keep only basic features 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" { @@ -226,18 +219,16 @@ func MeasureOutboundDelay(ConfigureFileContent string, url string) (int64, error inst, err := v2core.New(config) if err != nil { - return -1, err + return -1, fmt.Errorf("failed to create core instance: %w", err) } inst.Start() - delay, err := measureInstDelay(context.Background(), inst, url) - inst.Close() - return delay, err + defer inst.Close() + return measureInstDelay(context.Background(), inst, url) } -/*NewV2RayPoint new V2RayPoint*/ +// NewV2RayPoint creates a new V2RayPoint instance func NewV2RayPoint(s V2RayVPNServiceSupportsSet, adns bool) *V2RayPoint { - // inject our own log writer v2applog.RegisterHandlerCreator(v2applog.LogType_Console, func(lt v2applog.LogType, options v2applog.HandlerCreatorOptions) (v2commlog.Handler, error) { @@ -253,18 +244,16 @@ func NewV2RayPoint(s V2RayVPNServiceSupportsSet, adns bool) *V2RayPoint { } } -/* -CheckVersionX string -This func will return libv2ray binding version and V2Ray version used. -*/ +// CheckVersionX returns the library and V2Ray versions func CheckVersionX() string { var version = 30 return fmt.Sprintf("Lib v%d, Xray-core v%s", version, v2core.Version()) } +// measureInstDelay measures the delay for an instance to a given URL func measureInstDelay(ctx context.Context, inst *v2core.Instance, url string) (int64, error) { if inst == nil { - return -1, errors.New("core instance nil") + return -1, errors.New("core instance is nil") } tr := &http.Transport{ @@ -279,29 +268,29 @@ func measureInstDelay(ctx context.Context, inst *v2core.Instance, url string) (i }, } - c := &http.Client{ + client := &http.Client{ Transport: tr, Timeout: 12 * time.Second, } - if len(url) <= 0 { + if len(url) == 0 { url = "https://www.google.com/generate_204" } req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) start := time.Now() - resp, err := c.Do(req) + resp, err := client.Do(req) if err != nil { return -1, err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { - return -1, fmt.Errorf("status != 20x: %s", resp.Status) + return -1, fmt.Errorf("unexpected status code: %s", resp.Status) } - resp.Body.Close() return time.Since(start).Milliseconds(), nil } -// This struct creates our own log writer without datetime stamp +// consoleLogWriter creates our own log writer without datetime stamp // As Android adds time stamps on each line type consoleLogWriter struct { logger *log.Logger @@ -316,10 +305,11 @@ func (w *consoleLogWriter) Close() error { return nil } -// This logger won't print data/time stamps +// createStdoutLogWriter creates a logger that won't print date/time stamps func createStdoutLogWriter() v2commlog.WriterCreator { return func() v2commlog.Writer { return &consoleLogWriter{ - logger: log.New(os.Stdout, "", 0)} + logger: log.New(os.Stdout, "", 0), + } } } diff --git a/libv2ray_support.go b/libv2ray_support.go index df84e6d..6e7346c 100644 --- a/libv2ray_support.go +++ b/libv2ray_support.go @@ -17,10 +17,12 @@ import ( 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 @@ -31,27 +33,19 @@ type resolved struct { lastSwitched time.Time } -// NextIP switch to another resolved result. -// there still be race-condition here if multiple err concurently occured -// may cause idx keep switching, -// but that's an outside error can hardly handled here +// NextIP switches to another resolved IP address func (r *resolved) NextIP() { r.ipLock.Lock() defer r.ipLock.Unlock() if len(r.IPs) > 1 { - - // throttle, don't switch too quickly now := time.Now() - if now.Sub(r.lastSwitched) < time.Second*5 { + if now.Sub(r.lastSwitched) < 5*time.Second { log.Println("switch too quickly") return } r.lastSwitched = now r.ipIdx++ - - } else { - return } if r.ipIdx >= uint8(len(r.IPs)) { @@ -61,27 +55,25 @@ func (r *resolved) NextIP() { 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 ... +// NewProtectedDialer creates a new ProtectedDialer func NewProtectedDialer(p protectSet) *ProtectedDialer { - d := &ProtectedDialer{ - // prefer native lookup on Android + return &ProtectedDialer{ resolver: &net.Resolver{PreferGo: false}, protectSet: p, } - return d } -// ProtectedDialer ... +// ProtectedDialer handles protected dialing type ProtectedDialer struct { currentServer string resolveChan chan struct{} @@ -93,21 +85,23 @@ type ProtectedDialer struct { protectSet } +// IsVServerReady checks if the virtual server is ready func (d *ProtectedDialer) IsVServerReady() bool { - return (d.vServer != nil) + 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 } -// simplicated version of golang: internetAddrList in src/net/ipsock.go +// lookupAddr performs DNS resolution for the given address func (d *ProtectedDialer) lookupAddr(addr string) (*resolved, error) { - var ( err error host, port string @@ -131,44 +125,41 @@ func (d *ProtectedDialer) lookupAddr(addr string) (*resolved, error) { 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) - //ipv6 is prefer, append ipv6 then ipv4 - //ipv6 is not prefer, append ipv4 then ipv6 - if(d.preferIPv6) { + if d.preferIPv6 { for _, ia := range addrs { - if(ia.IP.To4() == nil) { - IPs = append(IPs, ia.IP) + 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) + } + } + if !d.preferIPv6 { for _, ia := range addrs { - if(ia.IP.To4() == nil) { - IPs = append(IPs, ia.IP) + if ia.IP.To4() == nil { + IPs = append(IPs, ia.IP) } - } + } } - rs := &resolved{ + return &resolved{ domain: host, IPs: IPs, Port: portnum, lastResolved: time.Now(), - } - - return rs, nil + }, nil } -// PrepareDomain caches direct v2ray server host +// 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 @@ -189,7 +180,7 @@ func (d *ProtectedDialer) PrepareDomain(domainName string, closeCh <-chan struct case <-closeCh: log.Printf("PrepareDomain exit due to core closed") return - case <-time.After(time.Second * 2): + case <-time.After(2 * time.Second): } continue } @@ -201,6 +192,7 @@ func (d *ProtectedDialer) PrepareDomain(domainName string, closeCh <-chan struct } } +// 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: @@ -208,42 +200,32 @@ func (d *ProtectedDialer) getFd(network v2net.Network) (fd int, err error) { case v2net.Network_UDP: fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP) default: - err = fmt.Errorf("unknow network") + err = fmt.Errorf("unknown network") } return } -// Init implement internet.SystemDialer +// Init initializes the dialer func (d *ProtectedDialer) Init(_ dns.Client, _ outbound.Manager) { // do nothing } -// Dial exported as the protected dial method +// Dial performs a protected dial func (d *ProtectedDialer) Dial(ctx context.Context, src v2net.Address, dest v2net.Destination, sockopt *v2internet.SocketConfig) (net.Conn, error) { - // network := dest.Network.SystemString() Address := dest.NetAddr() - // v2ray server address, - // try to connect fixed IP if multiple IP parsed from domain, - // and switch to next IP if error occurred if Address == d.currentServer { if d.vServer == nil { log.Println("Dial pending prepare ...", Address) <-d.resolveChan - // user may close connection during PrepareDomain, - // fast return release resources. if d.vServer == nil { return nil, fmt.Errorf("fail to prepare domain %s", d.currentServer) } } - // if time.Since(d.vServer.lastResolved) > time.Minute*30 { - // go d.PrepareDomain(Address, nil, d.preferIPv6) - // } - fd, err := d.getFd(dest.Network) if err != nil { return nil, err @@ -259,8 +241,6 @@ func (d *ProtectedDialer) Dial(ctx context.Context, return conn, nil } - // v2ray connecting to "domestic" servers, no caching results - // log.Printf("Not Using Prepared: %s,%s", network, Address) resolved, err := d.lookupAddr(Address) if err != nil { return nil, err @@ -271,20 +251,18 @@ func (d *ProtectedDialer) Dial(ctx context.Context, return nil, err } - // use the first resolved address. - // the result IP may vary, eg: IPv6 addrs comes first if client has ipv6 address 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) - // call android VPN service to "protect" the fd connecting straight out if !d.Protect(fd) { log.Printf("fdConn fail to protect, Close Fd: %d", fd) return nil, errors.New("fail to protect") @@ -302,19 +280,16 @@ func (d *ProtectedDialer) fdConn(ctx context.Context, ip net.IP, port int, netwo } } else { if err := unix.Connect(fd, sa); err != nil { - // log.Printf("fdConn unix.Connect err, Close Fd: %d Err: %v", fd, err) return nil, err } } file := os.NewFile(uintptr(fd), "Socket") if file == nil { - // returned value will be nil if fd is not a valid file descriptor return nil, errors.New("fdConn fd invalid") } - defer file.Close() - //Closing conn does not affect file, and closing file does not affect conn. + if network == v2net.Network_UDP { packetConn, err := net.FilePacketConn(file) if err != nil { diff --git a/libv2ray_support_test.go b/libv2ray_support_test.go index 2c96abd..3eb4f9e 100644 --- a/libv2ray_support_test.go +++ b/libv2ray_support_test.go @@ -12,12 +12,15 @@ import ( 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 @@ -26,21 +29,17 @@ func TestProtectedDialer_PrepareDomain(t *testing.T) { name string args args }{ - // TODO: Add test cases. - {"", args{"baidu.com:80"}}, - // {"", args{"cloudflare.com:443"}}, - // {"", args{"apple.com:443"}}, - // {"", args{"110.110.110.110:443"}}, - // {"", args{"[2002:1234::1]:443"}}, + {"Test with baidu.com", args{"baidu.com:80"}}, + // Add more test cases if needed } d := NewProtectedDialer(fakeSupportSet{}) for _, tt := range tests { - ch := make(chan struct{}) t.Run(tt.name, func(t *testing.T) { + ch := make(chan struct{}) go d.PrepareDomain(tt.args.domainName, ch, false) time.Sleep(time.Second) - go d.vServer.NextIP() + d.vServer.NextIP() t.Log(d.vServer.currentIP()) }) } @@ -48,19 +47,16 @@ func TestProtectedDialer_PrepareDomain(t *testing.T) { 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 }{ - // TODO: Add test cases. {"baidu.com:80", false}, {"cloudflare.com:80", false}, {"172.16.192.11:80", true}, - // {"172.16.192.10:80", true}, - // {"[2fff:4322::1]:443", true}, - // {"[fc00::1]:443", true}, + // Add more test cases if needed } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -84,18 +80,17 @@ func TestProtectedDialer_Dial(t *testing.T) { t.Log(err) return } - _host, _, _ := net.SplitHostPort(tt.name) - fmt.Fprintf(conn, fmt.Sprintf("GET / HTTP/1.1\r\nHost: %s\r\n\r\n", _host)) + 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("%#v, %#v\n", status, err) - conn.Close() + t.Logf("Status: %s, Error: %v", status, err) } for n := 0; n < 3; n++ { wg.Add(1) go dial() - // time.Sleep(time.Millisecond * 10) - // d.pendingMap[tt.name] = make(chan struct{}) } wg.Wait() @@ -103,6 +98,7 @@ func TestProtectedDialer_Dial(t *testing.T) { } } +// Test_resolved_NextIP tests the NextIP method of the resolved struct func Test_resolved_NextIP(t *testing.T) { type fields struct { domain string @@ -113,17 +109,15 @@ func Test_resolved_NextIP(t *testing.T) { name string fields fields }{ - // TODO: Add test cases. - {"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"), - }, - }}, + {"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) { @@ -132,20 +126,20 @@ func Test_resolved_NextIP(t *testing.T) { IPs: tt.fields.IPs, Port: tt.fields.Port, } - t.Logf("%v", r.IPs) - t.Logf("%v", r.currentIP()) + t.Logf("Initial IPs: %v", r.IPs) + t.Logf("Current IP: %v", r.currentIP()) r.NextIP() - t.Logf("%v", r.currentIP()) + t.Logf("Next IP: %v", r.currentIP()) r.NextIP() - t.Logf("%v", r.currentIP()) + t.Logf("Next IP: %v", r.currentIP()) r.NextIP() - t.Logf("%v", r.currentIP()) + t.Logf("Next IP: %v", r.currentIP()) time.Sleep(3 * time.Second) r.NextIP() - t.Logf("%v", r.currentIP()) + t.Logf("Next IP: %v", r.currentIP()) time.Sleep(5 * time.Second) r.NextIP() - t.Logf("%v", r.currentIP()) + t.Logf("Next IP: %v", r.currentIP()) }) } }