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/utils" "github.com/metacubex/mihomo/component/dialer" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/provider" ) type Fallback struct { *GroupBase disableUDP bool testUrl string selected string expectedStatus string } func (f *Fallback) Now() string { proxy := f.findAliveProxy(false) return proxy.Name() } // DialContext implements C.ProxyAdapter func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { proxy := f.findAliveProxy(true) c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...) if err == nil { c.AppendToChains(f) } else { f.onDialFailed(proxy.Type(), err) } if N.NeedHandshake(c) { c = callback.NewFirstWriteCallBackConn(c, func(err error) { if err == nil { f.onDialSuccess() } else { f.onDialFailed(proxy.Type(), err) } }) } return c, err } // ListenPacketContext implements C.ProxyAdapter func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { proxy := f.findAliveProxy(true) pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...) if err == nil { pc.AppendToChains(f) } return pc, err } // SupportUDP implements C.ProxyAdapter func (f *Fallback) SupportUDP() bool { if f.disableUDP { return false } proxy := f.findAliveProxy(false) return proxy.SupportUDP() } // IsL3Protocol implements C.ProxyAdapter func (f *Fallback) IsL3Protocol(metadata *C.Metadata) bool { return f.findAliveProxy(false).IsL3Protocol(metadata) } // MarshalJSON implements C.ProxyAdapter func (f *Fallback) MarshalJSON() ([]byte, error) { all := []string{} for _, proxy := range f.GetProxies(false) { all = append(all, proxy.Name()) } return json.Marshal(map[string]any{ "type": f.Type().String(), "now": f.Now(), "all": all, "testUrl": f.testUrl, "expectedStatus": f.expectedStatus, }) } // Unwrap implements C.ProxyAdapter func (f *Fallback) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { proxy := f.findAliveProxy(touch) return proxy } func (f *Fallback) findAliveProxy(touch bool) C.Proxy { proxies := f.GetProxies(touch) for _, proxy := range proxies { if len(f.selected) == 0 { if proxy.AliveForTestUrl(f.testUrl) { return proxy } } else { if proxy.Name() == f.selected { if proxy.AliveForTestUrl(f.testUrl) { return proxy } else { f.selected = "" } } } } return proxies[0] } func (f *Fallback) Set(name string) error { var p C.Proxy for _, proxy := range f.GetProxies(false) { if proxy.Name() == name { p = proxy break } } if p == nil { return errors.New("proxy not exist") } f.selected = name if !p.AliveForTestUrl(f.testUrl) { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000)) defer cancel() expectedStatus, _ := utils.NewIntRanges[uint16](f.expectedStatus) _, _ = p.URLTest(ctx, f.testUrl, expectedStatus) } return nil } func (f *Fallback) ForceSet(name string) { f.selected = name } func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { return &Fallback{ GroupBase: NewGroupBase(GroupBaseOption{ outbound.BaseOption{ Name: option.Name, Type: C.Fallback, Interface: option.Interface, RoutingMark: option.RoutingMark, }, option.Filter, option.ExcludeFilter, option.ExcludeType, providers, }), disableUDP: option.DisableUDP, testUrl: option.URL, expectedStatus: option.ExpectedStatus, } }