Implementing select random outbound

This commit is contained in:
ashly-right 2024-03-10 04:58:00 +01:00
parent 6e1407f7f8
commit 8c893f5180
No known key found for this signature in database
GPG Key ID: 2C70FC9CFBECF3DE
2 changed files with 130 additions and 34 deletions

View File

@ -13,4 +13,5 @@ type URLTestOutboundOptions struct {
Tolerance uint16 `json:"tolerance,omitempty"` Tolerance uint16 `json:"tolerance,omitempty"`
IdleTimeout Duration `json:"idle_timeout,omitempty"` IdleTimeout Duration `json:"idle_timeout,omitempty"`
InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"`
Randomized bool `json:"randomized,omitempty"`
} }

View File

@ -2,6 +2,7 @@ package outbound
import ( import (
"context" "context"
"math/rand"
"net" "net"
"sync" "sync"
"time" "time"
@ -38,6 +39,7 @@ type URLTest struct {
idleTimeout time.Duration idleTimeout time.Duration
group *URLTestGroup group *URLTestGroup
interruptExternalConnections bool interruptExternalConnections bool
randomized bool
} }
func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) { func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) {
@ -57,6 +59,7 @@ func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLo
tolerance: options.Tolerance, tolerance: options.Tolerance,
idleTimeout: time.Duration(options.IdleTimeout), idleTimeout: time.Duration(options.IdleTimeout),
interruptExternalConnections: options.InterruptExistConnections, interruptExternalConnections: options.InterruptExistConnections,
randomized: options.Randomized,
} }
if len(outbound.tags) == 0 { if len(outbound.tags) == 0 {
return nil, E.New("missing tags") return nil, E.New("missing tags")
@ -83,6 +86,7 @@ func (s *URLTest) Start() error {
s.tolerance, s.tolerance,
s.idleTimeout, s.idleTimeout,
s.interruptExternalConnections, s.interruptExternalConnections,
s.randomized,
) )
if err != nil { if err != nil {
return err return err
@ -126,6 +130,9 @@ func (s *URLTest) CheckOutbounds() {
func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
s.group.Touch() s.group.Touch()
var outbound adapter.Outbound var outbound adapter.Outbound
if s.randomized {
outbound = s.group.selectRandomOutbound(network)
} else {
switch N.NetworkName(network) { switch N.NetworkName(network) {
case N.NetworkTCP: case N.NetworkTCP:
outbound = s.group.selectedOutboundTCP outbound = s.group.selectedOutboundTCP
@ -137,6 +144,7 @@ func (s *URLTest) DialContext(ctx context.Context, network string, destination M
if outbound == nil { if outbound == nil {
outbound, _ = s.group.Select(network) outbound, _ = s.group.Select(network)
} }
}
if outbound == nil { if outbound == nil {
return nil, E.New("missing supported outbound") return nil, E.New("missing supported outbound")
} }
@ -151,10 +159,15 @@ func (s *URLTest) DialContext(ctx context.Context, network string, destination M
func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
s.group.Touch() s.group.Touch()
outbound := s.group.selectedOutboundUDP var outbound adapter.Outbound
if s.randomized {
outbound = s.group.selectRandomOutbound(N.NetworkUDP) // Since ListenPacket is for UDP, we pass "N.NetworkUDP" as the network type
} else {
outbound = s.group.selectedOutboundUDP
if outbound == nil { if outbound == nil {
outbound, _ = s.group.Select(N.NetworkUDP) outbound, _ = s.group.Select(N.NetworkUDP)
} }
}
if outbound == nil { if outbound == nil {
return nil, E.New("missing supported outbound") return nil, E.New("missing supported outbound")
} }
@ -196,6 +209,9 @@ type URLTestGroup struct {
pauseManager pause.Manager pauseManager pause.Manager
selectedOutboundTCP adapter.Outbound selectedOutboundTCP adapter.Outbound
selectedOutboundUDP adapter.Outbound selectedOutboundUDP adapter.Outbound
randomized bool
bestTCPLatencyOutbounds []adapter.Outbound
bestUDPLatencyOutbounds []adapter.Outbound
interruptGroup *interrupt.Group interruptGroup *interrupt.Group
interruptExternalConnections bool interruptExternalConnections bool
@ -216,6 +232,7 @@ func NewURLTestGroup(
tolerance uint16, tolerance uint16,
idleTimeout time.Duration, idleTimeout time.Duration,
interruptExternalConnections bool, interruptExternalConnections bool,
randomized bool,
) (*URLTestGroup, error) { ) (*URLTestGroup, error) {
if interval == 0 { if interval == 0 {
interval = C.DefaultURLTestInterval interval = C.DefaultURLTestInterval
@ -250,6 +267,7 @@ func NewURLTestGroup(
pauseManager: service.FromContext[pause.Manager](ctx), pauseManager: service.FromContext[pause.Manager](ctx),
interruptGroup: interrupt.NewGroup(), interruptGroup: interrupt.NewGroup(),
interruptExternalConnections: interruptExternalConnections, interruptExternalConnections: interruptExternalConnections,
randomized: randomized,
}, nil }, nil
} }
@ -349,6 +367,9 @@ func (g *URLTestGroup) loopCheck() {
} }
g.pauseManager.WaitActive() g.pauseManager.WaitActive()
g.CheckOutbounds(false) g.CheckOutbounds(false)
if g.randomized {
g.selectBestLatencyOutbounds()
}
} }
} }
@ -357,7 +378,15 @@ func (g *URLTestGroup) CheckOutbounds(force bool) {
} }
func (g *URLTestGroup) URLTest(ctx context.Context) (map[string]uint16, error) { func (g *URLTestGroup) URLTest(ctx context.Context) (map[string]uint16, error) {
return g.urlTest(ctx, false) result, err := g.urlTest(ctx, false)
if err != nil {
return nil, err
}
if g.randomized {
g.selectBestLatencyOutbounds()
}
return result, nil
} }
func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint16, error) { func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint16, error) {
@ -423,3 +452,69 @@ func (g *URLTestGroup) performUpdateCheck() {
g.interruptGroup.Interrupt(g.interruptExternalConnections) g.interruptGroup.Interrupt(g.interruptExternalConnections)
} }
} }
func (g *URLTestGroup) selectBestLatencyOutbounds() {
var bestTCPLatency uint16
var bestUDPLatency uint16
var bestTCPOutbounds []adapter.Outbound
var bestUDPOutbounds []adapter.Outbound
for _, detour := range g.outbounds {
history := g.history.LoadURLTestHistory(RealTag(detour))
if history == nil {
continue
}
if common.Contains(detour.Network(), N.NetworkTCP) {
if bestTCPLatency == 0 || history.Delay < bestTCPLatency {
bestTCPLatency = history.Delay
}
} else if common.Contains(detour.Network(), N.NetworkUDP) {
if bestUDPLatency == 0 || history.Delay < bestUDPLatency {
bestUDPLatency = history.Delay
}
}
}
for _, detour := range g.outbounds {
history := g.history.LoadURLTestHistory(RealTag(detour))
if history == nil {
continue
}
if common.Contains(detour.Network(), N.NetworkTCP) && history.Delay <= bestTCPLatency+g.tolerance {
bestTCPOutbounds = append(bestTCPOutbounds, detour)
} else if common.Contains(detour.Network(), N.NetworkUDP) && history.Delay <= bestUDPLatency+g.tolerance {
bestUDPOutbounds = append(bestUDPOutbounds, detour)
}
}
g.bestTCPLatencyOutbounds = bestTCPOutbounds
g.bestUDPLatencyOutbounds = bestUDPOutbounds
}
// selectRandomOutbound selects an outbound randomly among the outbounds with the best latency
func (g *URLTestGroup) selectRandomOutbound(network string) adapter.Outbound {
var bestOutbounds []adapter.Outbound
switch network {
case N.NetworkTCP:
bestOutbounds = g.bestTCPLatencyOutbounds
case N.NetworkUDP:
bestOutbounds = g.bestUDPLatencyOutbounds
default:
return nil
}
if len(bestOutbounds) == 0 {
return nil
}
randIndex := rand.Intn(len(bestOutbounds))
g.logger.Debug("Random outbound selection: ", bestOutbounds[randIndex].Tag())
return bestOutbounds[randIndex]
}