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

     1  package outbound
     2  
     3  import (
     4  	"context"
     5  	"net"
     6  	"sort"
     7  	"time"
     8  
     9  	"github.com/sagernet/sing-box/adapter"
    10  	"github.com/sagernet/sing-box/common/urltest"
    11  	C "github.com/sagernet/sing-box/constant"
    12  	"github.com/sagernet/sing-box/log"
    13  	"github.com/sagernet/sing-box/option"
    14  	"github.com/sagernet/sing/common"
    15  	"github.com/sagernet/sing/common/batch"
    16  	E "github.com/sagernet/sing/common/exceptions"
    17  	M "github.com/sagernet/sing/common/metadata"
    18  	N "github.com/sagernet/sing/common/network"
    19  )
    20  
    21  var (
    22  	_ adapter.Outbound      = (*URLTest)(nil)
    23  	_ adapter.OutboundGroup = (*URLTest)(nil)
    24  )
    25  
    26  type URLTest struct {
    27  	myOutboundAdapter
    28  	tags      []string
    29  	link      string
    30  	interval  time.Duration
    31  	tolerance uint16
    32  	group     *URLTestGroup
    33  }
    34  
    35  func NewURLTest(router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) {
    36  	outbound := &URLTest{
    37  		myOutboundAdapter: myOutboundAdapter{
    38  			protocol: C.TypeURLTest,
    39  			router:   router,
    40  			logger:   logger,
    41  			tag:      tag,
    42  		},
    43  		tags:      options.Outbounds,
    44  		link:      options.URL,
    45  		interval:  time.Duration(options.Interval),
    46  		tolerance: options.Tolerance,
    47  	}
    48  	if len(outbound.tags) == 0 {
    49  		return nil, E.New("missing tags")
    50  	}
    51  	return outbound, nil
    52  }
    53  
    54  func (s *URLTest) Network() []string {
    55  	if s.group == nil {
    56  		return []string{N.NetworkTCP, N.NetworkUDP}
    57  	}
    58  	return s.group.Select(N.NetworkTCP).Network()
    59  }
    60  
    61  func (s *URLTest) Start() error {
    62  	outbounds := make([]adapter.Outbound, 0, len(s.tags))
    63  	for i, tag := range s.tags {
    64  		detour, loaded := s.router.Outbound(tag)
    65  		if !loaded {
    66  			return E.New("outbound ", i, " not found: ", tag)
    67  		}
    68  		outbounds = append(outbounds, detour)
    69  	}
    70  	s.group = NewURLTestGroup(s.router, s.logger, outbounds, s.link, s.interval, s.tolerance)
    71  	return s.group.Start()
    72  }
    73  
    74  func (s URLTest) Close() error {
    75  	return common.Close(
    76  		common.PtrOrNil(s.group),
    77  	)
    78  }
    79  
    80  func (s *URLTest) Now() string {
    81  	return s.group.Select(N.NetworkTCP).Tag()
    82  }
    83  
    84  func (s *URLTest) All() []string {
    85  	return s.tags
    86  }
    87  
    88  func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
    89  	outbound := s.group.Select(network)
    90  	conn, err := outbound.DialContext(ctx, network, destination)
    91  	if err == nil {
    92  		return conn, nil
    93  	}
    94  	s.logger.ErrorContext(ctx, err)
    95  	go s.group.checkOutbounds()
    96  	outbounds := s.group.Fallback(outbound)
    97  	for _, fallback := range outbounds {
    98  		conn, err = fallback.DialContext(ctx, network, destination)
    99  		if err == nil {
   100  			return conn, nil
   101  		}
   102  	}
   103  	return nil, err
   104  }
   105  
   106  func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
   107  	outbound := s.group.Select(N.NetworkUDP)
   108  	conn, err := outbound.ListenPacket(ctx, destination)
   109  	if err == nil {
   110  		return conn, nil
   111  	}
   112  	s.logger.ErrorContext(ctx, err)
   113  	go s.group.checkOutbounds()
   114  	outbounds := s.group.Fallback(outbound)
   115  	for _, fallback := range outbounds {
   116  		conn, err = fallback.ListenPacket(ctx, destination)
   117  		if err == nil {
   118  			return conn, nil
   119  		}
   120  	}
   121  	return nil, err
   122  }
   123  
   124  func (s *URLTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
   125  	return NewConnection(ctx, s, conn, metadata)
   126  }
   127  
   128  func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
   129  	return NewPacketConnection(ctx, s, conn, metadata)
   130  }
   131  
   132  type URLTestGroup struct {
   133  	router    adapter.Router
   134  	logger    log.Logger
   135  	outbounds []adapter.Outbound
   136  	link      string
   137  	interval  time.Duration
   138  	tolerance uint16
   139  	history   *urltest.HistoryStorage
   140  
   141  	ticker *time.Ticker
   142  	close  chan struct{}
   143  }
   144  
   145  func NewURLTestGroup(router adapter.Router, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16) *URLTestGroup {
   146  	if link == "" {
   147  		//goland:noinspection HttpUrlsUsage
   148  		link = "http://www.gstatic.com/generate_204"
   149  	}
   150  	if interval == 0 {
   151  		interval = C.DefaultURLTestInterval
   152  	}
   153  	if tolerance == 0 {
   154  		tolerance = 50
   155  	}
   156  	var history *urltest.HistoryStorage
   157  	if clashServer := router.ClashServer(); clashServer != nil {
   158  		history = clashServer.HistoryStorage()
   159  	} else {
   160  		history = urltest.NewHistoryStorage()
   161  	}
   162  	return &URLTestGroup{
   163  		router:    router,
   164  		logger:    logger,
   165  		outbounds: outbounds,
   166  		link:      link,
   167  		interval:  interval,
   168  		tolerance: tolerance,
   169  		history:   history,
   170  		close:     make(chan struct{}),
   171  	}
   172  }
   173  
   174  func (g *URLTestGroup) Start() error {
   175  	g.ticker = time.NewTicker(g.interval)
   176  	go g.loopCheck()
   177  	return nil
   178  }
   179  
   180  func (g *URLTestGroup) Close() error {
   181  	g.ticker.Stop()
   182  	close(g.close)
   183  	return nil
   184  }
   185  
   186  func (g *URLTestGroup) Select(network string) adapter.Outbound {
   187  	var minDelay uint16
   188  	var minTime time.Time
   189  	var minOutbound adapter.Outbound
   190  	for _, detour := range g.outbounds {
   191  		if !common.Contains(detour.Network(), network) {
   192  			continue
   193  		}
   194  		history := g.history.LoadURLTestHistory(RealTag(detour))
   195  		if history == nil {
   196  			continue
   197  		}
   198  		if minDelay == 0 || minDelay > history.Delay+g.tolerance || minDelay > history.Delay-g.tolerance && minTime.Before(history.Time) {
   199  			minDelay = history.Delay
   200  			minTime = history.Time
   201  			minOutbound = detour
   202  		}
   203  	}
   204  	if minOutbound == nil {
   205  		for _, detour := range g.outbounds {
   206  			if !common.Contains(detour.Network(), network) {
   207  				continue
   208  			}
   209  			minOutbound = detour
   210  			break
   211  		}
   212  	}
   213  	return minOutbound
   214  }
   215  
   216  func (g *URLTestGroup) Fallback(used adapter.Outbound) []adapter.Outbound {
   217  	outbounds := make([]adapter.Outbound, 0, len(g.outbounds)-1)
   218  	for _, detour := range g.outbounds {
   219  		if detour != used {
   220  			outbounds = append(outbounds, detour)
   221  		}
   222  	}
   223  	sort.SliceStable(outbounds, func(i, j int) bool {
   224  		oi := outbounds[i]
   225  		oj := outbounds[j]
   226  		hi := g.history.LoadURLTestHistory(RealTag(oi))
   227  		if hi == nil {
   228  			return false
   229  		}
   230  		hj := g.history.LoadURLTestHistory(RealTag(oj))
   231  		if hj == nil {
   232  			return false
   233  		}
   234  		return hi.Delay < hj.Delay
   235  	})
   236  	return outbounds
   237  }
   238  
   239  func (g *URLTestGroup) loopCheck() {
   240  	go g.checkOutbounds()
   241  	for {
   242  		select {
   243  		case <-g.close:
   244  			return
   245  		case <-g.ticker.C:
   246  			g.checkOutbounds()
   247  		}
   248  	}
   249  }
   250  
   251  func (g *URLTestGroup) checkOutbounds() {
   252  	b, _ := batch.New(context.Background(), batch.WithConcurrencyNum[any](10))
   253  	checked := make(map[string]bool)
   254  	for _, detour := range g.outbounds {
   255  		tag := detour.Tag()
   256  		realTag := RealTag(detour)
   257  		if checked[realTag] {
   258  			continue
   259  		}
   260  		history := g.history.LoadURLTestHistory(realTag)
   261  		if history != nil && time.Now().Sub(history.Time) < g.interval {
   262  			continue
   263  		}
   264  		checked[realTag] = true
   265  		p, loaded := g.router.Outbound(realTag)
   266  		if !loaded {
   267  			continue
   268  		}
   269  		b.Go(realTag, func() (any, error) {
   270  			ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
   271  			defer cancel()
   272  			t, err := urltest.URLTest(ctx, g.link, p)
   273  			if err != nil {
   274  				g.logger.Debug("outbound ", tag, " unavailable: ", err)
   275  				g.history.DeleteURLTestHistory(realTag)
   276  			} else {
   277  				g.logger.Debug("outbound ", tag, " available: ", t, "ms")
   278  				g.history.StoreURLTestHistory(realTag, &urltest.History{
   279  					Time:  time.Now(),
   280  					Delay: t,
   281  				})
   282  			}
   283  			return nil, nil
   284  		})
   285  	}
   286  	b.Wait()
   287  }