diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md index b30538e5..18c430d9 100644 --- a/docs/configuration/experimental/cache-file.md +++ b/docs/configuration/experimental/cache-file.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! question "Since sing-box 1.8.0" !!! quote "Changes in sing-box 1.9.0" diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md index 6d86dc84..656d53c4 100644 --- a/docs/configuration/experimental/cache-file.zh.md +++ b/docs/configuration/experimental/cache-file.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! question "自 sing-box 1.8.0 起" !!! quote "sing-box 1.9.0 中的更改" diff --git a/docs/configuration/outbound/urltest.md b/docs/configuration/outbound/urltest.md index f4b3b0aa..0353e479 100644 --- a/docs/configuration/outbound/urltest.md +++ b/docs/configuration/outbound/urltest.md @@ -14,7 +14,8 @@ "interval": "", "tolerance": 0, "idle_timeout": "", - "interrupt_exist_connections": false + "interrupt_exist_connections": false, + "randomize": false } ``` @@ -47,3 +48,9 @@ The idle timeout. `30m` will be used if empty. Interrupt existing connections when the selected outbound has changed. Only inbound connections are affected by this setting, internal connections will always be interrupted. + +#### randomize + +Outbound would be selected randomly within the best latency in the tolerance range. It's deactivated by default. + +The interrupt_exist_connections will be ignored if the randomize is activated. \ No newline at end of file diff --git a/docs/configuration/outbound/urltest.zh.md b/docs/configuration/outbound/urltest.zh.md index 4372298a..fddb8da2 100644 --- a/docs/configuration/outbound/urltest.zh.md +++ b/docs/configuration/outbound/urltest.zh.md @@ -14,7 +14,8 @@ "interval": "", "tolerance": 50, "idle_timeout": "", - "interrupt_exist_connections": false + "interrupt_exist_connections": false, + "randomize": false } ``` @@ -46,4 +47,11 @@ 当选定的出站发生更改时,中断现有连接。 -仅入站连接受此设置影响,内部连接将始终被中断。 \ No newline at end of file +仅入站连接受此设置影响,内部连接将始终被中断。 + + +#### randomize + +出站将在容忍范围内的最佳延迟内随机选择。 默认情况下它处于禁用状态。 + +如果激活了随机化,则interrupt_exist_connections将被忽略。 \ No newline at end of file diff --git a/option/group.go b/option/group.go index 72a0f637..f420cda0 100644 --- a/option/group.go +++ b/option/group.go @@ -13,4 +13,5 @@ type URLTestOutboundOptions struct { Tolerance uint16 `json:"tolerance,omitempty"` IdleTimeout Duration `json:"idle_timeout,omitempty"` InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` + Randomize bool `json:"randomize,omitempty"` } diff --git a/outbound/urltest.go b/outbound/urltest.go index aa7cff6c..3cac8641 100644 --- a/outbound/urltest.go +++ b/outbound/urltest.go @@ -2,6 +2,7 @@ package outbound import ( "context" + "math/rand" "net" "sync" "time" @@ -38,6 +39,7 @@ type URLTest struct { idleTimeout time.Duration group *URLTestGroup interruptExternalConnections bool + randomize bool } 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, idleTimeout: time.Duration(options.IdleTimeout), interruptExternalConnections: options.InterruptExistConnections, + randomize: options.Randomize, } if len(outbound.tags) == 0 { return nil, E.New("missing tags") @@ -83,6 +86,7 @@ func (s *URLTest) Start() error { s.tolerance, s.idleTimeout, s.interruptExternalConnections, + s.randomize, ) if err != nil { return err @@ -126,16 +130,20 @@ func (s *URLTest) CheckOutbounds() { func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { s.group.Touch() var outbound adapter.Outbound - switch N.NetworkName(network) { - case N.NetworkTCP: - outbound = s.group.selectedOutboundTCP - case N.NetworkUDP: - outbound = s.group.selectedOutboundUDP - default: - return nil, E.Extend(N.ErrUnknownNetwork, network) - } - if outbound == nil { - outbound, _ = s.group.Select(network) + if s.randomize { + outbound = s.group.selectRandomOutbound(network) + } else { + switch N.NetworkName(network) { + case N.NetworkTCP: + outbound = s.group.selectedOutboundTCP + case N.NetworkUDP: + outbound = s.group.selectedOutboundUDP + default: + return nil, E.Extend(N.ErrUnknownNetwork, network) + } + if outbound == nil { + outbound, _ = s.group.Select(network) + } } if outbound == nil { return nil, E.New("missing supported outbound") @@ -151,9 +159,14 @@ func (s *URLTest) DialContext(ctx context.Context, network string, destination M func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { s.group.Touch() - outbound := s.group.selectedOutboundUDP - if outbound == nil { - outbound, _ = s.group.Select(N.NetworkUDP) + var outbound adapter.Outbound + if s.randomize { + 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 { + outbound, _ = s.group.Select(N.NetworkUDP) + } } if outbound == nil { return nil, E.New("missing supported outbound") @@ -196,6 +209,9 @@ type URLTestGroup struct { pauseManager pause.Manager selectedOutboundTCP adapter.Outbound selectedOutboundUDP adapter.Outbound + randomize bool + bestTCPLatencyOutbounds []adapter.Outbound + bestUDPLatencyOutbounds []adapter.Outbound interruptGroup *interrupt.Group interruptExternalConnections bool @@ -216,6 +232,7 @@ func NewURLTestGroup( tolerance uint16, idleTimeout time.Duration, interruptExternalConnections bool, + randomize bool, ) (*URLTestGroup, error) { if interval == 0 { interval = C.DefaultURLTestInterval @@ -250,6 +267,7 @@ func NewURLTestGroup( pauseManager: service.FromContext[pause.Manager](ctx), interruptGroup: interrupt.NewGroup(), interruptExternalConnections: interruptExternalConnections, + randomize: randomize, }, nil } @@ -349,6 +367,9 @@ func (g *URLTestGroup) loopCheck() { } g.pauseManager.WaitActive() g.CheckOutbounds(false) + if g.randomize { + g.selectBestLatencyOutbounds() + } } } @@ -357,7 +378,15 @@ func (g *URLTestGroup) CheckOutbounds(force bool) { } 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.randomize { + g.selectBestLatencyOutbounds() + } + return result, nil } 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) } } + +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 + } + } + 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) + } + 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] +}