package outboundgroup import ( "context" "encoding/json" "errors" "time" "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/common/callback" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/singledo" "github.com/metacubex/mihomo/component/dialer" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/provider" ) type urlTestOption func(*URLTest) func urlTestWithTolerance(tolerance uint16) urlTestOption { return func(u *URLTest) { u.tolerance = tolerance } } type URLTest struct { *GroupBase selected string testUrl string expectedStatus string tolerance uint16 disableUDP bool fastNode C.Proxy fastSingle *singledo.Single[C.Proxy] } func (u *URLTest) Now() string { return u.fast(false).Name() } func (u *URLTest) Set(name string) error { var p C.Proxy for _, proxy := range u.GetProxies(false) { if proxy.Name() == name { p = proxy break } } if p == nil { return errors.New("proxy not exist") } u.selected = name u.fast(false) return nil } func (u *URLTest) ForceSet(name string) { u.selected = name } // DialContext implements C.ProxyAdapter func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) { proxy := u.fast(true) c, err = proxy.DialContext(ctx, metadata, u.Base.DialOptions(opts...)...) if err == nil { c.AppendToChains(u) } else { u.onDialFailed(proxy.Type(), err) } if N.NeedHandshake(c) { c = callback.NewFirstWriteCallBackConn(c, func(err error) { if err == nil { u.onDialSuccess() } else { u.onDialFailed(proxy.Type(), err) } }) } return c, err } // ListenPacketContext implements C.ProxyAdapter func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...) if err == nil { pc.AppendToChains(u) } return pc, err } // Unwrap implements C.ProxyAdapter func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { return u.fast(touch) } func (u *URLTest) fast(touch bool) C.Proxy { proxies := u.GetProxies(touch) if u.selected != "" { for _, proxy := range proxies { if !proxy.AliveForTestUrl(u.testUrl) { continue } if proxy.Name() == u.selected { u.fastNode = proxy return proxy } } } elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) { fast := proxies[0] minDelay := fast.LastDelayForTestUrl(u.testUrl) fastNotExist := true for _, proxy := range proxies[1:] { if u.fastNode != nil && proxy.Name() == u.fastNode.Name() { fastNotExist = false } if !proxy.AliveForTestUrl(u.testUrl) { continue } delay := proxy.LastDelayForTestUrl(u.testUrl) if delay < minDelay { fast = proxy minDelay = delay } } // tolerance if u.fastNode == nil || fastNotExist || !u.fastNode.AliveForTestUrl(u.testUrl) || u.fastNode.LastDelayForTestUrl(u.testUrl) > fast.LastDelayForTestUrl(u.testUrl)+u.tolerance { u.fastNode = fast } return u.fastNode, nil }) if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again u.Touch() } return elm } // SupportUDP implements C.ProxyAdapter func (u *URLTest) SupportUDP() bool { if u.disableUDP { return false } return u.fast(false).SupportUDP() } // IsL3Protocol implements C.ProxyAdapter func (u *URLTest) IsL3Protocol(metadata *C.Metadata) bool { return u.fast(false).IsL3Protocol(metadata) } // MarshalJSON implements C.ProxyAdapter func (u *URLTest) MarshalJSON() ([]byte, error) { all := []string{} for _, proxy := range u.GetProxies(false) { all = append(all, proxy.Name()) } return json.Marshal(map[string]any{ "type": u.Type().String(), "now": u.Now(), "all": all, "testUrl": u.testUrl, "expectedStatus": u.expectedStatus, "fixed": u.selected, }) } func parseURLTestOption(config map[string]any) []urlTestOption { opts := []urlTestOption{} // tolerance if elm, ok := config["tolerance"]; ok { if tolerance, ok := elm.(int); ok { opts = append(opts, urlTestWithTolerance(uint16(tolerance))) } } return opts } func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest { urlTest := &URLTest{ GroupBase: NewGroupBase(GroupBaseOption{ outbound.BaseOption{ Name: option.Name, Type: C.URLTest, Interface: option.Interface, RoutingMark: option.RoutingMark, }, option.Filter, option.ExcludeFilter, option.ExcludeType, providers, }), fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), disableUDP: option.DisableUDP, testUrl: option.URL, expectedStatus: option.ExpectedStatus, } for _, option := range options { option(urlTest) } return urlTest }