Merge tag 'v1.14.0'

This commit is contained in:
Shtorm
2026-09-04 01:08:30 +03:00
987 changed files with 141481 additions and 11302 deletions
+28 -23
View File
@@ -4,15 +4,14 @@ import (
"context"
"net"
"regexp"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/interrupt"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
@@ -26,9 +25,9 @@ func RegisterSelector(registry *outbound.Registry) {
}
var (
_ adapter.OutboundGroup = (*Selector)(nil)
_ adapter.ConnectionHandlerEx = (*Selector)(nil)
_ adapter.PacketConnectionHandlerEx = (*Selector)(nil)
_ adapter.OutboundGroup = (*Selector)(nil)
_ adapter.ConnectionHandler = (*Selector)(nil)
_ adapter.PacketConnectionHandler = (*Selector)(nil)
)
type Selector struct {
@@ -41,6 +40,7 @@ type Selector struct {
defaultTag string
outbounds map[string]adapter.Outbound
selected common.TypedValue[adapter.Outbound]
history *urltest.HistoryStorage
interruptGroup *interrupt.Group
interruptExternalConnections bool
@@ -64,6 +64,7 @@ func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextL
tags: options.Outbounds,
defaultTag: options.Default,
outbounds: make(map[string]adapter.Outbound),
history: service.PtrFromContext[urltest.HistoryStorage](ctx),
interruptGroup: interrupt.NewGroup(),
interruptExternalConnections: options.InterruptExistConnections,
@@ -160,6 +161,9 @@ func (s *Selector) SelectOutbound(tag string) bool {
}
}
s.interruptGroup.Interrupt(s.interruptExternalConnections)
if s.history != nil {
s.history.NotifyUpdated()
}
return true
}
@@ -179,41 +183,42 @@ func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
return s.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx), interrupt.IsProviderConnectionFromContext(ctx)), nil
}
func (s *Selector) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx)
selected := s.selected.Load()
conn = s.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx), interrupt.IsProviderConnectionFromContext(ctx))
if outboundHandler, isHandler := selected.(adapter.ConnectionHandlerEx); isHandler {
outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose)
if outboundHandler, isHandler := selected.(adapter.ConnectionHandler); isHandler {
outboundHandler.NewConnection(ctx, conn, metadata, onClose)
} else {
s.connection.NewConnection(ctx, selected, conn, metadata, onClose)
}
}
func (s *Selector) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx)
selected := s.selected.Load()
conn = s.interruptGroup.NewSingPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx), interrupt.IsProviderConnectionFromContext(ctx))
if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandlerEx); isHandler {
outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose)
if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandler); isHandler {
outboundHandler.NewPacketConnection(ctx, conn, metadata, onClose)
} else {
s.connection.NewPacketConnection(ctx, selected, conn, metadata, onClose)
}
}
func (s *Selector) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
selected := s.selected.Load()
if !common.Contains(selected.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag())
func RealTag(outboundManager adapter.OutboundManager, detour adapter.Outbound) string {
tag := detour.Tag()
for {
group, isGroup := detour.(adapter.OutboundGroup)
if !isGroup {
return tag
}
tag = group.Now()
var loaded bool
detour, loaded = outboundManager.Outbound(tag)
if !loaded {
return tag
}
}
return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
}
func RealTag(detour adapter.Outbound) string {
if group, isGroup := detour.(adapter.OutboundGroup); isGroup {
return group.Now()
}
return detour.Tag()
}
func (s *Selector) onProviderUpdated(tag string) error {
+151 -78
View File
@@ -2,6 +2,7 @@ package group
import (
"context"
"maps"
"net"
"regexp"
"sync"
@@ -15,7 +16,6 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"
@@ -30,7 +30,10 @@ func RegisterURLTest(registry *outbound.Registry) {
outbound.Register[option.URLTestOutboundOptions](registry, C.TypeURLTest, NewURLTest)
}
var _ adapter.OutboundGroup = (*URLTest)(nil)
var (
_ adapter.OutboundGroup = (*URLTest)(nil)
_ adapter.InterfaceUpdateListener = (*URLTest)(nil)
)
type URLTest struct {
outbound.Adapter
@@ -44,6 +47,7 @@ type URLTest struct {
tolerance uint16
idleTimeout time.Duration
group *URLTestGroup
checkAccess sync.Mutex
interruptExternalConnections bool
provider adapter.ProviderManager
@@ -156,7 +160,29 @@ func (s *URLTest) URLTest(ctx context.Context) (map[string]uint16, error) {
}
func (s *URLTest) CheckOutbounds() {
s.group.CheckOutbounds(true)
s.group.CheckOutbounds(s.ctx, true)
}
func (s *URLTest) PerformUpdateCheck() {
s.group.performUpdateCheck()
}
func (s *URLTest) InterfaceUpdated(ctx context.Context) {
group := s.group
if group == nil {
return
}
if group.pause.IsDevicePaused() || group.pause.IsNetworkPaused() {
return
}
go func() {
s.checkAccess.Lock()
defer s.checkAccess.Unlock()
if ctx.Err() != nil {
return
}
group.CheckOutbounds(ctx, true)
}()
}
func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
@@ -203,33 +229,18 @@ func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (ne
return nil, err
}
func (s *URLTest) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
func (s *URLTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx)
conn = s.group.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx), interrupt.IsProviderConnectionFromContext(ctx))
s.connection.NewConnection(ctx, s, conn, metadata, onClose)
}
func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx)
conn = s.group.interruptGroup.NewSingPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx), interrupt.IsProviderConnectionFromContext(ctx))
s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose)
}
func (s *URLTest) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
s.group.Touch()
selected := s.group.selectedOutboundTCP
if selected == nil {
selected, _ = s.group.Select(N.NetworkTCP)
}
if selected == nil {
return nil, E.New("missing supported outbound")
}
if !common.Contains(selected.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag())
}
return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
}
func (s *URLTest) onProviderUpdated(tag string) error {
_, loaded := s.providers[tag]
if !loaded {
@@ -298,13 +309,14 @@ type URLTestGroup struct {
interval time.Duration
tolerance uint16
idleTimeout time.Duration
history adapter.URLTestHistoryStorage
history *urltest.HistoryStorage
checking atomic.Bool
selectedOutboundTCP adapter.Outbound
selectedOutboundUDP adapter.Outbound
interruptGroup *interrupt.Group
interruptExternalConnections bool
access sync.Mutex
updateAccess sync.Mutex
ticker *time.Ticker
close chan struct{}
started bool
@@ -324,13 +336,9 @@ func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManage
if interval > idleTimeout {
return nil, E.New("interval must be less or equal than idle_timeout")
}
var history adapter.URLTestHistoryStorage
if historyFromCtx := service.PtrFromContext[urltest.HistoryStorage](ctx); historyFromCtx != nil {
history = historyFromCtx
} else if clashServer := service.FromContext[adapter.ClashServer](ctx); clashServer != nil {
history = clashServer.HistoryStorage()
} else {
history = urltest.NewHistoryStorage()
history := service.PtrFromContext[urltest.HistoryStorage](ctx)
if history == nil {
return nil, E.New("missing URL test history storage")
}
return &URLTestGroup{
ctx: ctx,
@@ -354,7 +362,7 @@ func (g *URLTestGroup) PostStart() {
defer g.access.Unlock()
g.started = true
g.lastActive.Store(time.Now())
go g.CheckOutbounds(false)
go g.CheckOutbounds(g.ctx, false)
}
func (g *URLTestGroup) Touch() {
@@ -393,14 +401,14 @@ func (g *URLTestGroup) Select(network string) (adapter.Outbound, bool) {
switch network {
case N.NetworkTCP:
if g.selectedOutboundTCP != nil {
if history := g.history.LoadURLTestHistory(RealTag(g.selectedOutboundTCP)); history != nil {
if history := g.history.LoadURLTestHistory(RealTag(g.outbound, g.selectedOutboundTCP)); history != nil {
minOutbound = g.selectedOutboundTCP
minDelay = history.Delay
}
}
case N.NetworkUDP:
if g.selectedOutboundUDP != nil {
if history := g.history.LoadURLTestHistory(RealTag(g.selectedOutboundUDP)); history != nil {
if history := g.history.LoadURLTestHistory(RealTag(g.outbound, g.selectedOutboundUDP)); history != nil {
minOutbound = g.selectedOutboundUDP
minDelay = history.Delay
}
@@ -410,7 +418,7 @@ func (g *URLTestGroup) Select(network string) (adapter.Outbound, bool) {
if !common.Contains(detour.Network(), network) {
continue
}
history := g.history.LoadURLTestHistory(RealTag(detour))
history := g.history.LoadURLTestHistory(RealTag(g.outbound, detour))
if history == nil {
continue
}
@@ -434,7 +442,7 @@ func (g *URLTestGroup) Select(network string) (adapter.Outbound, bool) {
func (g *URLTestGroup) loopCheck(ticker *time.Ticker, closeChan <-chan struct{}) {
if time.Since(g.lastActive.Load()) > g.interval {
g.lastActive.Store(time.Now())
g.CheckOutbounds(false)
g.CheckOutbounds(g.ctx, false)
}
for {
select {
@@ -453,63 +461,24 @@ func (g *URLTestGroup) loopCheck(ticker *time.Ticker, closeChan <-chan struct{})
g.access.Unlock()
return
}
g.CheckOutbounds(false)
g.CheckOutbounds(g.ctx, false)
}
}
func (g *URLTestGroup) CheckOutbounds(force bool) {
_, _ = g.urlTest(g.ctx, force)
func (g *URLTestGroup) CheckOutbounds(ctx context.Context, force bool) {
_, _ = g.urlTest(ctx, force)
}
func (g *URLTestGroup) URLTest(ctx context.Context) (map[string]uint16, error) {
return g.urlTest(ctx, false)
return g.urlTest(ctx, true)
}
func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint16, error) {
result := make(map[string]uint16)
if g.checking.Swap(true) {
return result, nil
return make(map[string]uint16), nil
}
defer g.checking.Store(false)
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
checked := make(map[string]bool)
var resultAccess sync.Mutex
for _, detour := range g.outbounds {
tag := detour.Tag()
realTag := RealTag(detour)
if checked[realTag] {
continue
}
history := g.history.LoadURLTestHistory(realTag)
if !force && history != nil && time.Since(history.Time) < g.interval {
continue
}
checked[realTag] = true
p, loaded := g.outbound.Outbound(realTag)
if !loaded {
continue
}
b.Go(realTag, func() (any, error) {
testCtx, cancel := context.WithTimeout(g.ctx, C.TCPTimeout)
defer cancel()
t, err := urltest.URLTest(testCtx, g.link, p)
if err != nil {
g.logger.Debug("outbound ", tag, " unavailable: ", err)
g.history.DeleteURLTestHistory(realTag)
} else {
g.logger.Debug("outbound ", tag, " available: ", t, "ms")
g.history.StoreURLTestHistory(realTag, &adapter.URLTestHistory{
Time: time.Now(),
Delay: t,
})
resultAccess.Lock()
result[tag] = t
resultAccess.Unlock()
}
return nil, nil
})
}
b.Wait()
result := URLTestOutbounds(ctx, g.outbound, g.history, g.logger, g.outbounds, g.link, g.interval, force)
select {
case <-ctx.Done():
default:
@@ -518,7 +487,111 @@ func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint
return result, nil
}
type urlTestResult struct {
delay uint16
err error
}
type urlTestBatch struct {
ctx context.Context
outbound adapter.OutboundManager
history *urltest.HistoryStorage
logger log.Logger
batch *batch.Batch[any]
checked map[string]bool
groups []adapter.OutboundGroup
access sync.Mutex
result map[string]uint16
}
func URLTestOutbounds(ctx context.Context, outboundManager adapter.OutboundManager, history *urltest.HistoryStorage, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, force bool) map[string]uint16 {
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
testBatch := &urlTestBatch{
ctx: ctx,
outbound: outboundManager,
history: history,
logger: logger,
batch: b,
checked: make(map[string]bool),
result: make(map[string]uint16),
}
testBatch.test(outbounds, link, interval, force)
b.Wait()
for _, outboundGroup := range testBatch.groups {
groupHistory := history.LoadURLTestHistory(RealTag(outboundManager, outboundGroup))
if groupHistory != nil {
testBatch.result[outboundGroup.Tag()] = groupHistory.Delay
}
}
return testBatch.result
}
func (b *urlTestBatch) test(outbounds []adapter.Outbound, link string, interval time.Duration, force bool) {
for _, detour := range outbounds {
tag := detour.Tag()
if b.checked[tag] {
continue
}
switch nested := detour.(type) {
case *URLTest:
b.checked[tag] = true
b.groups = append(b.groups, nested)
b.batch.Go(tag, func() (any, error) {
nestedResult, _ := nested.group.urlTest(b.ctx, force)
b.access.Lock()
maps.Copy(b.result, nestedResult)
b.access.Unlock()
return nil, nil
})
case adapter.OutboundGroup:
b.checked[tag] = true
b.groups = append(b.groups, nested)
b.test(common.FilterNotNil(common.Map(nested.All(), func(it string) adapter.Outbound {
member, _ := b.outbound.Outbound(it)
return member
})), link, interval, force)
default:
history := b.history.LoadURLTestHistory(tag)
if !force && history != nil && time.Since(history.Time) < interval {
continue
}
b.checked[tag] = true
b.batch.Go(tag, func() (any, error) {
testCtx, cancel := context.WithTimeout(b.ctx, C.TCPTimeout)
defer cancel()
testChan := make(chan urlTestResult, 1)
go func() {
delay, testErr := urltest.URLTest(testCtx, link, detour)
testChan <- urlTestResult{delay, testErr}
}()
var testResult urlTestResult
select {
case testResult = <-testChan:
case <-testCtx.Done():
testResult.err = testCtx.Err()
}
if testResult.err != nil {
b.logger.Debug("outbound ", tag, " unavailable: ", testResult.err)
b.history.DeleteURLTestHistory(tag)
} else {
b.logger.Debug("outbound ", tag, " available: ", testResult.delay, "ms")
b.history.StoreURLTestHistory(tag, &adapter.URLTestHistory{
Time: time.Now(),
Delay: testResult.delay,
})
b.access.Lock()
b.result[tag] = testResult.delay
b.access.Unlock()
}
return nil, nil
})
}
}
}
func (g *URLTestGroup) performUpdateCheck() {
g.updateAccess.Lock()
defer g.updateAccess.Unlock()
var updated bool
if outbound, exists := g.Select(N.NetworkTCP); outbound != nil && (g.selectedOutboundTCP == nil || (exists && outbound != g.selectedOutboundTCP)) {
if g.selectedOutboundTCP != nil {