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 }