github.com/inazumav/sing-box@v0.0.0-20230926072359-ab51429a14f1/outbound/urltest.go (about)

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