github.com/metacubex/mihomo@v1.18.5/adapter/outboundgroup/urltest.go (about)

     1  package outboundgroup
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/metacubex/mihomo/adapter/outbound"
    12  	"github.com/metacubex/mihomo/common/callback"
    13  	N "github.com/metacubex/mihomo/common/net"
    14  	"github.com/metacubex/mihomo/common/singledo"
    15  	"github.com/metacubex/mihomo/common/utils"
    16  	"github.com/metacubex/mihomo/component/dialer"
    17  	C "github.com/metacubex/mihomo/constant"
    18  	"github.com/metacubex/mihomo/constant/provider"
    19  )
    20  
    21  type urlTestOption func(*URLTest)
    22  
    23  func urlTestWithTolerance(tolerance uint16) urlTestOption {
    24  	return func(u *URLTest) {
    25  		u.tolerance = tolerance
    26  	}
    27  }
    28  
    29  type URLTest struct {
    30  	*GroupBase
    31  	selected       string
    32  	testUrl        string
    33  	expectedStatus string
    34  	tolerance      uint16
    35  	disableUDP     bool
    36  	Hidden         bool
    37  	Icon           string
    38  	fastNode       C.Proxy
    39  	fastSingle     *singledo.Single[C.Proxy]
    40  }
    41  
    42  func (u *URLTest) Now() string {
    43  	return u.fast(false).Name()
    44  }
    45  
    46  func (u *URLTest) Set(name string) error {
    47  	var p C.Proxy
    48  	for _, proxy := range u.GetProxies(false) {
    49  		if proxy.Name() == name {
    50  			p = proxy
    51  			break
    52  		}
    53  	}
    54  	if p == nil {
    55  		return errors.New("proxy not exist")
    56  	}
    57  	u.selected = name
    58  	u.fast(false)
    59  	return nil
    60  }
    61  
    62  func (u *URLTest) ForceSet(name string) {
    63  	u.selected = name
    64  }
    65  
    66  // DialContext implements C.ProxyAdapter
    67  func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
    68  	proxy := u.fast(true)
    69  	c, err = proxy.DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
    70  	if err == nil {
    71  		c.AppendToChains(u)
    72  	} else {
    73  		u.onDialFailed(proxy.Type(), err)
    74  	}
    75  
    76  	if N.NeedHandshake(c) {
    77  		c = callback.NewFirstWriteCallBackConn(c, func(err error) {
    78  			if err == nil {
    79  				u.onDialSuccess()
    80  			} else {
    81  				u.onDialFailed(proxy.Type(), err)
    82  			}
    83  		})
    84  	}
    85  
    86  	return c, err
    87  }
    88  
    89  // ListenPacketContext implements C.ProxyAdapter
    90  func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
    91  	pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
    92  	if err == nil {
    93  		pc.AppendToChains(u)
    94  	}
    95  
    96  	return pc, err
    97  }
    98  
    99  // Unwrap implements C.ProxyAdapter
   100  func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
   101  	return u.fast(touch)
   102  }
   103  
   104  func (u *URLTest) fast(touch bool) C.Proxy {
   105  
   106  	proxies := u.GetProxies(touch)
   107  	if u.selected != "" {
   108  		for _, proxy := range proxies {
   109  			if !proxy.AliveForTestUrl(u.testUrl) {
   110  				continue
   111  			}
   112  			if proxy.Name() == u.selected {
   113  				u.fastNode = proxy
   114  				return proxy
   115  			}
   116  		}
   117  	}
   118  
   119  	elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
   120  		fast := proxies[0]
   121  		minDelay := fast.LastDelayForTestUrl(u.testUrl)
   122  		fastNotExist := true
   123  
   124  		for _, proxy := range proxies[1:] {
   125  			if u.fastNode != nil && proxy.Name() == u.fastNode.Name() {
   126  				fastNotExist = false
   127  			}
   128  
   129  			if !proxy.AliveForTestUrl(u.testUrl) {
   130  				continue
   131  			}
   132  
   133  			delay := proxy.LastDelayForTestUrl(u.testUrl)
   134  			if delay < minDelay {
   135  				fast = proxy
   136  				minDelay = delay
   137  			}
   138  
   139  		}
   140  		// tolerance
   141  		if u.fastNode == nil || fastNotExist || !u.fastNode.AliveForTestUrl(u.testUrl) || u.fastNode.LastDelayForTestUrl(u.testUrl) > fast.LastDelayForTestUrl(u.testUrl)+u.tolerance {
   142  			u.fastNode = fast
   143  		}
   144  		return u.fastNode, nil
   145  	})
   146  	if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
   147  		u.Touch()
   148  	}
   149  
   150  	return elm
   151  }
   152  
   153  // SupportUDP implements C.ProxyAdapter
   154  func (u *URLTest) SupportUDP() bool {
   155  	if u.disableUDP {
   156  		return false
   157  	}
   158  	return u.fast(false).SupportUDP()
   159  }
   160  
   161  // IsL3Protocol implements C.ProxyAdapter
   162  func (u *URLTest) IsL3Protocol(metadata *C.Metadata) bool {
   163  	return u.fast(false).IsL3Protocol(metadata)
   164  }
   165  
   166  // MarshalJSON implements C.ProxyAdapter
   167  func (u *URLTest) MarshalJSON() ([]byte, error) {
   168  	all := []string{}
   169  	for _, proxy := range u.GetProxies(false) {
   170  		all = append(all, proxy.Name())
   171  	}
   172  	return json.Marshal(map[string]any{
   173  		"type":           u.Type().String(),
   174  		"now":            u.Now(),
   175  		"all":            all,
   176  		"testUrl":        u.testUrl,
   177  		"expectedStatus": u.expectedStatus,
   178  		"fixed":          u.selected,
   179  		"hidden":         u.Hidden,
   180  		"icon":           u.Icon,
   181  	})
   182  }
   183  
   184  func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) {
   185  	var wg sync.WaitGroup
   186  	var lock sync.Mutex
   187  	mp := map[string]uint16{}
   188  	proxies := u.GetProxies(false)
   189  	for _, proxy := range proxies {
   190  		proxy := proxy
   191  		wg.Add(1)
   192  		go func() {
   193  			delay, err := proxy.URLTest(ctx, u.testUrl, expectedStatus)
   194  			if err == nil {
   195  				lock.Lock()
   196  				mp[proxy.Name()] = delay
   197  				lock.Unlock()
   198  			}
   199  
   200  			wg.Done()
   201  		}()
   202  	}
   203  	wg.Wait()
   204  
   205  	if len(mp) == 0 {
   206  		return mp, fmt.Errorf("get delay: all proxies timeout")
   207  	} else {
   208  		return mp, nil
   209  	}
   210  }
   211  
   212  func parseURLTestOption(config map[string]any) []urlTestOption {
   213  	opts := []urlTestOption{}
   214  
   215  	// tolerance
   216  	if elm, ok := config["tolerance"]; ok {
   217  		if tolerance, ok := elm.(int); ok {
   218  			opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
   219  		}
   220  	}
   221  
   222  	return opts
   223  }
   224  
   225  func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
   226  	urlTest := &URLTest{
   227  		GroupBase: NewGroupBase(GroupBaseOption{
   228  			outbound.BaseOption{
   229  				Name:        option.Name,
   230  				Type:        C.URLTest,
   231  				Interface:   option.Interface,
   232  				RoutingMark: option.RoutingMark,
   233  			},
   234  
   235  			option.Filter,
   236  			option.ExcludeFilter,
   237  			option.ExcludeType,
   238  			option.TestTimeout,
   239  			option.MaxFailedTimes,
   240  			providers,
   241  		}),
   242  		fastSingle:     singledo.NewSingle[C.Proxy](time.Second * 10),
   243  		disableUDP:     option.DisableUDP,
   244  		testUrl:        option.URL,
   245  		expectedStatus: option.ExpectedStatus,
   246  		Hidden:         option.Hidden,
   247  		Icon:           option.Icon,
   248  	}
   249  
   250  	for _, option := range options {
   251  		option(urlTest)
   252  	}
   253  
   254  	return urlTest
   255  }