package outboundgroup import ( "context" "encoding/json" "github.com/Dreamacro/clash/log" "go.uber.org/atomic" "time" "github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/component/dialer" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/constant/provider" ) type Fallback struct { *outbound.Base disableUDP bool filter string single *singledo.Single[[]C.Proxy] providers []provider.ProxyProvider failedTimes *atomic.Int32 failedTime *atomic.Int64 } 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) f.failedTimes.Store(-1) f.failedTime.Store(-1) } else { f.onDialFailed() } 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) f.failedTimes.Store(-1) f.failedTime.Store(-1) } else { f.onDialFailed() } return pc, err } func (f *Fallback) onDialFailed() { if f.failedTime.Load() == -1 { log.Warnln("%s first failed", f.Name()) now := time.Now().UnixMilli() f.failedTime.Store(now) f.failedTimes.Store(1) } else { if f.failedTime.Load()-time.Now().UnixMilli() > 5*time.Second.Milliseconds() { f.failedTimes.Store(-1) f.failedTime.Store(-1) } else { failedCount := f.failedTimes.Inc() log.Warnln("%s failed count: %d", f.Name(), failedCount) if failedCount >= 5 { log.Warnln("because %s failed multiple times, active health check", f.Name()) for _, proxyProvider := range f.providers { go proxyProvider.HealthCheck() } f.failedTimes.Store(-1) f.failedTime.Store(-1) } } } } // SupportUDP implements C.ProxyAdapter func (f *Fallback) SupportUDP() bool { if f.disableUDP { return false } proxy := f.findAliveProxy(false) return proxy.SupportUDP() } // MarshalJSON implements C.ProxyAdapter func (f *Fallback) MarshalJSON() ([]byte, error) { all := []string{} for _, proxy := range f.proxies(false) { all = append(all, proxy.Name()) } return json.Marshal(map[string]any{ "type": f.Type().String(), "now": f.Now(), "all": all, }) } // Unwrap implements C.ProxyAdapter func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy { proxy := f.findAliveProxy(true) return proxy } func (f *Fallback) proxies(touch bool) []C.Proxy { elm, _, _ := f.single.Do(func() ([]C.Proxy, error) { return getProvidersProxies(f.providers, touch, f.filter), nil }) return elm } func (f *Fallback) findAliveProxy(touch bool) C.Proxy { proxies := f.proxies(touch) for _, proxy := range proxies { if proxy.Alive() { return proxy } } return proxies[0] } func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { return &Fallback{ Base: outbound.NewBase(outbound.BaseOption{ Name: option.Name, Type: C.Fallback, Interface: option.Interface, RoutingMark: option.RoutingMark, }), single: singledo.NewSingle[[]C.Proxy](defaultGetProxiesDuration), providers: providers, disableUDP: option.DisableUDP, filter: option.Filter, failedTimes: atomic.NewInt32(-1), failedTime: atomic.NewInt64(-1), } }