github.com/sagernet/sing-box@v1.9.0-rc.20/outbound/urltest.go (about)

     1  package outbound
     2  
     3  import (
     4  	"context"
     5  	"net"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/sagernet/sing-box/adapter"
    10  	"github.com/sagernet/sing-box/common/interrupt"
    11  	"github.com/sagernet/sing-box/common/urltest"
    12  	C "github.com/sagernet/sing-box/constant"
    13  	"github.com/sagernet/sing-box/log"
    14  	"github.com/sagernet/sing-box/option"
    15  	"github.com/sagernet/sing/common"
    16  	"github.com/sagernet/sing/common/atomic"
    17  	"github.com/sagernet/sing/common/batch"
    18  	E "github.com/sagernet/sing/common/exceptions"
    19  	M "github.com/sagernet/sing/common/metadata"
    20  	N "github.com/sagernet/sing/common/network"
    21  	"github.com/sagernet/sing/service"
    22  	"github.com/sagernet/sing/service/pause"
    23  )
    24  
    25  var (
    26  	_ adapter.Outbound                = (*URLTest)(nil)
    27  	_ adapter.OutboundGroup           = (*URLTest)(nil)
    28  	_ adapter.InterfaceUpdateListener = (*URLTest)(nil)
    29  )
    30  
    31  type URLTest struct {
    32  	myOutboundAdapter
    33  	ctx                          context.Context
    34  	tags                         []string
    35  	link                         string
    36  	interval                     time.Duration
    37  	tolerance                    uint16
    38  	idleTimeout                  time.Duration
    39  	group                        *URLTestGroup
    40  	interruptExternalConnections bool
    41  }
    42  
    43  func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) {
    44  	outbound := &URLTest{
    45  		myOutboundAdapter: myOutboundAdapter{
    46  			protocol:     C.TypeURLTest,
    47  			network:      []string{N.NetworkTCP, N.NetworkUDP},
    48  			router:       router,
    49  			logger:       logger,
    50  			tag:          tag,
    51  			dependencies: options.Outbounds,
    52  		},
    53  		ctx:                          ctx,
    54  		tags:                         options.Outbounds,
    55  		link:                         options.URL,
    56  		interval:                     time.Duration(options.Interval),
    57  		tolerance:                    options.Tolerance,
    58  		idleTimeout:                  time.Duration(options.IdleTimeout),
    59  		interruptExternalConnections: options.InterruptExistConnections,
    60  	}
    61  	if len(outbound.tags) == 0 {
    62  		return nil, E.New("missing tags")
    63  	}
    64  	return outbound, nil
    65  }
    66  
    67  func (s *URLTest) Start() error {
    68  	outbounds := make([]adapter.Outbound, 0, len(s.tags))
    69  	for i, tag := range s.tags {
    70  		detour, loaded := s.router.Outbound(tag)
    71  		if !loaded {
    72  			return E.New("outbound ", i, " not found: ", tag)
    73  		}
    74  		outbounds = append(outbounds, detour)
    75  	}
    76  	group, err := NewURLTestGroup(
    77  		s.ctx,
    78  		s.router,
    79  		s.logger,
    80  		outbounds,
    81  		s.link,
    82  		s.interval,
    83  		s.tolerance,
    84  		s.idleTimeout,
    85  		s.interruptExternalConnections,
    86  	)
    87  	if err != nil {
    88  		return err
    89  	}
    90  	s.group = group
    91  	return nil
    92  }
    93  
    94  func (s *URLTest) PostStart() error {
    95  	s.group.PostStart()
    96  	return nil
    97  }
    98  
    99  func (s *URLTest) Close() error {
   100  	return common.Close(
   101  		common.PtrOrNil(s.group),
   102  	)
   103  }
   104  
   105  func (s *URLTest) Now() string {
   106  	if s.group.selectedOutboundTCP != nil {
   107  		return s.group.selectedOutboundTCP.Tag()
   108  	} else if s.group.selectedOutboundUDP != nil {
   109  		return s.group.selectedOutboundUDP.Tag()
   110  	}
   111  	return ""
   112  }
   113  
   114  func (s *URLTest) All() []string {
   115  	return s.tags
   116  }
   117  
   118  func (s *URLTest) URLTest(ctx context.Context) (map[string]uint16, error) {
   119  	return s.group.URLTest(ctx)
   120  }
   121  
   122  func (s *URLTest) CheckOutbounds() {
   123  	s.group.CheckOutbounds(true)
   124  }
   125  
   126  func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
   127  	s.group.Touch()
   128  	var outbound adapter.Outbound
   129  	switch N.NetworkName(network) {
   130  	case N.NetworkTCP:
   131  		outbound = s.group.selectedOutboundTCP
   132  	case N.NetworkUDP:
   133  		outbound = s.group.selectedOutboundUDP
   134  	default:
   135  		return nil, E.Extend(N.ErrUnknownNetwork, network)
   136  	}
   137  	if outbound == nil {
   138  		outbound, _ = s.group.Select(network)
   139  	}
   140  	if outbound == nil {
   141  		return nil, E.New("missing supported outbound")
   142  	}
   143  	conn, err := outbound.DialContext(ctx, network, destination)
   144  	if err == nil {
   145  		return s.group.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil
   146  	}
   147  	s.logger.ErrorContext(ctx, err)
   148  	s.group.history.DeleteURLTestHistory(outbound.Tag())
   149  	return nil, err
   150  }
   151  
   152  func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
   153  	s.group.Touch()
   154  	outbound := s.group.selectedOutboundUDP
   155  	if outbound == nil {
   156  		outbound, _ = s.group.Select(N.NetworkUDP)
   157  	}
   158  	if outbound == nil {
   159  		return nil, E.New("missing supported outbound")
   160  	}
   161  	conn, err := outbound.ListenPacket(ctx, destination)
   162  	if err == nil {
   163  		return s.group.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil
   164  	}
   165  	s.logger.ErrorContext(ctx, err)
   166  	s.group.history.DeleteURLTestHistory(outbound.Tag())
   167  	return nil, err
   168  }
   169  
   170  func (s *URLTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
   171  	ctx = interrupt.ContextWithIsExternalConnection(ctx)
   172  	return NewConnection(ctx, s, conn, metadata)
   173  }
   174  
   175  func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
   176  	ctx = interrupt.ContextWithIsExternalConnection(ctx)
   177  	return NewPacketConnection(ctx, s, conn, metadata)
   178  }
   179  
   180  func (s *URLTest) InterfaceUpdated() {
   181  	go s.group.CheckOutbounds(true)
   182  	return
   183  }
   184  
   185  type URLTestGroup struct {
   186  	ctx                          context.Context
   187  	router                       adapter.Router
   188  	logger                       log.Logger
   189  	outbounds                    []adapter.Outbound
   190  	link                         string
   191  	interval                     time.Duration
   192  	tolerance                    uint16
   193  	idleTimeout                  time.Duration
   194  	history                      *urltest.HistoryStorage
   195  	checking                     atomic.Bool
   196  	pauseManager                 pause.Manager
   197  	selectedOutboundTCP          adapter.Outbound
   198  	selectedOutboundUDP          adapter.Outbound
   199  	interruptGroup               *interrupt.Group
   200  	interruptExternalConnections bool
   201  
   202  	access     sync.Mutex
   203  	ticker     *time.Ticker
   204  	close      chan struct{}
   205  	started    bool
   206  	lastActive atomic.TypedValue[time.Time]
   207  }
   208  
   209  func NewURLTestGroup(
   210  	ctx context.Context,
   211  	router adapter.Router,
   212  	logger log.Logger,
   213  	outbounds []adapter.Outbound,
   214  	link string,
   215  	interval time.Duration,
   216  	tolerance uint16,
   217  	idleTimeout time.Duration,
   218  	interruptExternalConnections bool,
   219  ) (*URLTestGroup, error) {
   220  	if interval == 0 {
   221  		interval = C.DefaultURLTestInterval
   222  	}
   223  	if tolerance == 0 {
   224  		tolerance = 50
   225  	}
   226  	if idleTimeout == 0 {
   227  		idleTimeout = C.DefaultURLTestIdleTimeout
   228  	}
   229  	if interval > idleTimeout {
   230  		return nil, E.New("interval must be less or equal than idle_timeout")
   231  	}
   232  	var history *urltest.HistoryStorage
   233  	if history = service.PtrFromContext[urltest.HistoryStorage](ctx); history != nil {
   234  	} else if clashServer := router.ClashServer(); clashServer != nil {
   235  		history = clashServer.HistoryStorage()
   236  	} else {
   237  		history = urltest.NewHistoryStorage()
   238  	}
   239  	return &URLTestGroup{
   240  		ctx:                          ctx,
   241  		router:                       router,
   242  		logger:                       logger,
   243  		outbounds:                    outbounds,
   244  		link:                         link,
   245  		interval:                     interval,
   246  		tolerance:                    tolerance,
   247  		idleTimeout:                  idleTimeout,
   248  		history:                      history,
   249  		close:                        make(chan struct{}),
   250  		pauseManager:                 service.FromContext[pause.Manager](ctx),
   251  		interruptGroup:               interrupt.NewGroup(),
   252  		interruptExternalConnections: interruptExternalConnections,
   253  	}, nil
   254  }
   255  
   256  func (g *URLTestGroup) PostStart() {
   257  	g.started = true
   258  	g.lastActive.Store(time.Now())
   259  	go g.CheckOutbounds(false)
   260  }
   261  
   262  func (g *URLTestGroup) Touch() {
   263  	if !g.started {
   264  		return
   265  	}
   266  	if g.ticker != nil {
   267  		g.lastActive.Store(time.Now())
   268  		return
   269  	}
   270  	g.access.Lock()
   271  	defer g.access.Unlock()
   272  	if g.ticker != nil {
   273  		return
   274  	}
   275  	g.ticker = time.NewTicker(g.interval)
   276  	go g.loopCheck()
   277  }
   278  
   279  func (g *URLTestGroup) Close() error {
   280  	if g.ticker == nil {
   281  		return nil
   282  	}
   283  	g.ticker.Stop()
   284  	close(g.close)
   285  	return nil
   286  }
   287  
   288  func (g *URLTestGroup) Select(network string) (adapter.Outbound, bool) {
   289  	var minDelay uint16
   290  	var minOutbound adapter.Outbound
   291  	switch network {
   292  	case N.NetworkTCP:
   293  		if g.selectedOutboundTCP != nil {
   294  			if history := g.history.LoadURLTestHistory(RealTag(g.selectedOutboundTCP)); history != nil {
   295  				minOutbound = g.selectedOutboundTCP
   296  				minDelay = history.Delay
   297  			}
   298  		}
   299  	case N.NetworkUDP:
   300  		if g.selectedOutboundUDP != nil {
   301  			if history := g.history.LoadURLTestHistory(RealTag(g.selectedOutboundUDP)); history != nil {
   302  				minOutbound = g.selectedOutboundUDP
   303  				minDelay = history.Delay
   304  			}
   305  		}
   306  	}
   307  	for _, detour := range g.outbounds {
   308  		if !common.Contains(detour.Network(), network) {
   309  			continue
   310  		}
   311  		history := g.history.LoadURLTestHistory(RealTag(detour))
   312  		if history == nil {
   313  			continue
   314  		}
   315  		if minDelay == 0 || minDelay > history.Delay+g.tolerance {
   316  			minDelay = history.Delay
   317  			minOutbound = detour
   318  		}
   319  	}
   320  	if minOutbound == nil {
   321  		for _, detour := range g.outbounds {
   322  			if !common.Contains(detour.Network(), network) {
   323  				continue
   324  			}
   325  			return detour, false
   326  		}
   327  		return nil, false
   328  	}
   329  	return minOutbound, true
   330  }
   331  
   332  func (g *URLTestGroup) loopCheck() {
   333  	if time.Now().Sub(g.lastActive.Load()) > g.interval {
   334  		g.lastActive.Store(time.Now())
   335  		g.CheckOutbounds(false)
   336  	}
   337  	for {
   338  		select {
   339  		case <-g.close:
   340  			return
   341  		case <-g.ticker.C:
   342  		}
   343  		if time.Now().Sub(g.lastActive.Load()) > g.idleTimeout {
   344  			g.access.Lock()
   345  			g.ticker.Stop()
   346  			g.ticker = nil
   347  			g.access.Unlock()
   348  			return
   349  		}
   350  		g.pauseManager.WaitActive()
   351  		g.CheckOutbounds(false)
   352  	}
   353  }
   354  
   355  func (g *URLTestGroup) CheckOutbounds(force bool) {
   356  	_, _ = g.urlTest(g.ctx, force)
   357  }
   358  
   359  func (g *URLTestGroup) URLTest(ctx context.Context) (map[string]uint16, error) {
   360  	return g.urlTest(ctx, false)
   361  }
   362  
   363  func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint16, error) {
   364  	result := make(map[string]uint16)
   365  	if g.checking.Swap(true) {
   366  		return result, nil
   367  	}
   368  	defer g.checking.Store(false)
   369  	b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
   370  	checked := make(map[string]bool)
   371  	var resultAccess sync.Mutex
   372  	for _, detour := range g.outbounds {
   373  		tag := detour.Tag()
   374  		realTag := RealTag(detour)
   375  		if checked[realTag] {
   376  			continue
   377  		}
   378  		history := g.history.LoadURLTestHistory(realTag)
   379  		if !force && history != nil && time.Now().Sub(history.Time) < g.interval {
   380  			continue
   381  		}
   382  		checked[realTag] = true
   383  		p, loaded := g.router.Outbound(realTag)
   384  		if !loaded {
   385  			continue
   386  		}
   387  		b.Go(realTag, func() (any, error) {
   388  			ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
   389  			defer cancel()
   390  			t, err := urltest.URLTest(ctx, g.link, p)
   391  			if err != nil {
   392  				g.logger.Debug("outbound ", tag, " unavailable: ", err)
   393  				g.history.DeleteURLTestHistory(realTag)
   394  			} else {
   395  				g.logger.Debug("outbound ", tag, " available: ", t, "ms")
   396  				g.history.StoreURLTestHistory(realTag, &urltest.History{
   397  					Time:  time.Now(),
   398  					Delay: t,
   399  				})
   400  				resultAccess.Lock()
   401  				result[tag] = t
   402  				resultAccess.Unlock()
   403  			}
   404  			return nil, nil
   405  		})
   406  	}
   407  	b.Wait()
   408  	g.performUpdateCheck()
   409  	return result, nil
   410  }
   411  
   412  func (g *URLTestGroup) performUpdateCheck() {
   413  	var updated bool
   414  	if outbound, exists := g.Select(N.NetworkTCP); outbound != nil && (g.selectedOutboundTCP == nil || (exists && outbound != g.selectedOutboundTCP)) {
   415  		g.selectedOutboundTCP = outbound
   416  		updated = true
   417  	}
   418  	if outbound, exists := g.Select(N.NetworkUDP); outbound != nil && (g.selectedOutboundUDP == nil || (exists && outbound != g.selectedOutboundUDP)) {
   419  		g.selectedOutboundUDP = outbound
   420  		updated = true
   421  	}
   422  	if updated {
   423  		g.interruptGroup.Interrupt(g.interruptExternalConnections)
   424  	}
   425  }